本文的交互式版本可以在这里找到:https://github.com/FrancescoSaverioZuppichini/DropBlock
DropBlock在我的计算机视觉库上可用:https://github.com/FrancescoSaverioZuppichini/glasses
今天我们将在PyTorch中实现DropBlock!
Ghiasi等人介绍的DropBlock是一种针对图像的正则化技术,在经验上比Dropout效果更好。为什么Dropout是不够的?
Dropout是一种正则化技术,它在将输入传递到下一层之前,随机删除(设置为零)部分输入。
如果你不熟悉它,我推荐斯坦福德的这些课堂讲稿(跳到Dropout部分)。
https://cs231n.github.io/neural-networks-2/
如果我们想在PyTorch中使用它,我们可以直接从库中导入它。让我们看一个例子!
import torch
import matplotlib.pyplot as plt
from torch import nn
# 保持一个通道以便更好地可视化
x = torch.ones((1, 1, 16, 16))
drop = nn.Dropout()
x_drop = drop(x)
to_plot = lambda x: x.squeeze(0).permute(1,2,0).numpy()
fig, axs = plt.subplots(1, 2)
axs[0].imshow(to_plot(x), cmap='gray')
axs[1].imshow(to_plot(x_drop), cmap='gray')
如你所见,输入的随机像素被删除!
这种技术在一维数据上效果很好,但在二维数据上,我们可以做得更好。
主要问题是,我们正在删除独立像素,而这在删除语义信息方面并不有效,因为邻居包含密切相关的信息。即使我们将一个元素归零,从邻居那里仍然可以获取重要信息。
让我们探讨一下特征图会发生什么。
在下面的代码中,我们首先获取图像,然后使用glasses创建预训练的resnet18(https://github.com/FrancescoSaverioZuppichini/glasses)。然后我们将图像输入,从第二层得到特征图。最后,我们展示了第一个通道的在有dropout和无dropout的激活情况
import requests
from glasses.models import AutoModel, AutoTransform
from PIL import Image
from io import BytesIO
# 获取图像
r = requests.get('https://upload.wikimedia.org/wikipedia/en/0/00/The_Child_aka_Baby_Yoda_%28Star_Wars%29.jpg')
img = Image.open(BytesIO(r.content))
# 使用glasses将其转换为正确的格式
x = AutoTransform.from_name('resnet18')(img)
# 进行预训练resnet18
model = AutoModel.from_pretrained('resnet18').eval()
with torch.no_grad():
model.encoder.features
model(x.unsqueeze(0))
features = model.encoder.features
# features是一个层的输出列表
#从第三层获取特征 -> [1, 128, 28, 28]
f = features[2]
# 应用 dropout + relu
f_drop = nn.Sequential(
nn.Dropout(),
nn.ReLU())(f)
# 只应用relu
f_l = nn.ReLU()(f)
# 获取第一个通道
f_l = f_l[:,0,:,:]
f_drop_l = f_drop[:,0,:,:]
fig, axs = plt.subplots(1, 2)
axs[0].imshow(f_l.squeeze().numpy())
axs[1].imshow(f_drop_l.squeeze().numpy())
左边是特征图,右边是有dropout的特征图。它们看起来非常相似,请注意,在每个区域中,即使某些单位为零,邻居仍然存在。这意味着,信息将传播到下一层,这并不理想。
DropBlock通过从特征映射中删除连续区域来解决此问题,下图显示了其主要思想。
Dropblock的工作原理如下
我们可以从定义具有正确参数的DropBlock层开始
from torch import nn
import torch
from torch import Tensor
class DropBlock(nn.Module):
def __init__(self, block_size: int, p: float = 0.5):
self.block_size = block_size
self.p = p
block_size是我们要从输入中删除的每个区域的大小,p是概率。
到现在为止,一直都还不错。现在棘手的部分,我们需要计算控制要删除的特征的gamma。如果我们想用p的概率保存每个激活,我们可以从平均值为p的伯努利分布中取样。问题是我们需要将block_size ** 2的块设置为零。
Gamma通过如下公式计算
乘法的左边是将被设为零的单位数。右边是有效区域,是dropblock未触及的像素数
class DropBlock(nn.Module):
def __init__(self, block_size: int, p: float = 0.5):
self.block_size = block_size
self.p = p
def calculate_gamma(self, x: Tensor) -> float:
"""计算gamma
Args:
x (Tensor): 输入张量
Returns:
Tensor: gamma
"""
invalid = (1 - self.p) / (self.block_size ** 2)
valid = (x.shape[-1] ** 2) / ((x.shape[-1] - self.block_size + 1) ** 2)
return invalid * valid
x = torch.ones(1, 8, 16, 16)
DropBlock(block_size=2).calculate_gamma(x)
# Output
0.14222222222222222
下一步是对一个掩码进行采样,该掩码的大小与使用均值为gamma的伯努利分布输入的大小相同,在PyTorch中非常简单
# x是输入
gamma = self.calculate_gamma(x)
mask = torch.bernoulli(torch.ones_like(x) * gamma)
接下来,我们需要将block_size的区域归零。我们可以使用kernel_size等于block_size的最大池和一个像素步长来创建。
请记住,掩码是一个二进制掩码(仅0和1),因此当maxpool在其 kernel_size中看到1时,它将输出一个1,通过使用1步长,我们确保在输出中创建一个大小为block_size x block_size的区域。因为我们想把它们归零,所以我们需要把它倒过来。
mask_block = 1 - F.max_pool2d(
mask,
kernel_size=(self.block_size, self.block_size),
stride=(1, 1),
padding=(self.block_size // 2, self.block_size // 2),
)
然后我们将归一化
x = mask_block * x * (mask_block.numel() / mask_block.sum())
import torch.nn.functional as F
class DropBlock(nn.Module):
def __init__(self, block_size: int, p: float = 0.5):
super().__init__()
self.block_size = block_size
self.p = p
def calculate_gamma(self, x: Tensor) -> float:
"""计算gamma
Args:
x (Tensor): 输入张量
Returns:
Tensor: gamma
"""
invalid = (1 - self.p) / (self.block_size ** 2)
valid = (x.shape[-1] ** 2) / ((x.shape[-1] - self.block_size + 1) ** 2)
return invalid * valid
def forward(self, x: Tensor) -> Tensor:
if self.training:
gamma = self.calculate_gamma(x)
mask = torch.bernoulli(torch.ones_like(x) * gamma)
mask_block = 1 - F.max_pool2d(
mask,
kernel_size=(self.block_size, self.block_size),
stride=(1, 1),
padding=(self.block_size // 2, self.block_size // 2),
)
x = mask_block * x * (mask_block.numel() / mask_block.sum())
return x
让我们用baby yoda进行测试,为简单起见,我们将在第一个通道中显示丢弃的单元
import torchvision.transforms as T
# 获取图像
r = requests.get('https://upload.wikimedia.org/wikipedia/en/0/00/The_Child_aka_Baby_Yoda_%28Star_Wars%29.jpg')
img = Image.open(BytesIO(r.content))
tr = T.Compose([
T.Resize((224, 224)),
T.ToTensor()
])
x = tr(img)
drop_block = DropBlock(block_size=19, p=0.8)
x_drop = drop_block(x)
fig, axs = plt.subplots(1, 2)
axs[0].imshow(to_plot(x))
axs[1].imshow(x_drop[0,:,:].squeeze().numpy())
看起来不错,让我们看看预训练模型的特征图(如前所述)
# 获取第三层的特征 -> [1, 128, 28, 28]
f = features[2]
# 应用dropout + relu
f_drop = nn.Sequential(
DropBlock(block_size=7, p=0.5),
nn.ReLU())(f)
# 只应用relu
f_l = nn.ReLU()(f)
# 获取第一个通道
f_l = f_l[:,0,:,:]
f_drop_l = f_drop[:,0,:,:]
fig, axs = plt.subplots(1, 2)
axs[0].imshow(f_l.squeeze().numpy())
axs[1].imshow(f_drop_l.squeeze().numpy())
我们成功地将连续区域归零,而不仅仅是单个单元。
顺便说一句,当block_size=1时,DropBlock等于Dropout
现在我们知道了如何在PyTorch中实现DropBlock,这是一种很酷的正则化技术。
论文给出了不同的实证结果。他们使用普通的resnet50并迭代添加不同的正则化,如下表所示
如你所见,ResNet-50+DropBlock
与SpatialDropout(PyTorch中的经典Dropout2d
文件)相比增加了1%。
☆ END ☆
如果看到这里,说明你喜欢这篇文章,请转发、点赞。微信搜索「uncle_pn」,欢迎添加小编微信「 woshicver」,每日朋友圈更新一篇高质量博文。
↓扫描二维码添加小编↓