在PyTorch中实现DropBlock

本文的交互式版本可以在这里找到:https://github.com/FrancescoSaverioZuppichini/DropBlock

DropBlock在我的计算机视觉库上可用:https://github.com/FrancescoSaverioZuppichini/glasses

介绍

今天我们将在PyTorch中实现DropBlock!

Ghiasi等人介绍的DropBlock是一种针对图像的正则化技术,在经验上比Dropout效果更好。为什么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')

在PyTorch中实现DropBlock_第1张图片

如你所见,输入的随机像素被删除!

这种技术在一维数据上效果很好,但在二维数据上,我们可以做得更好。

主要问题是,我们正在删除独立像素,而这在删除语义信息方面并不有效,因为邻居包含密切相关的信息。即使我们将一个元素归零,从邻居那里仍然可以获取重要信息。

让我们探讨一下特征图会发生什么。

在下面的代码中,我们首先获取图像,然后使用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())

在PyTorch中实现DropBlock_第2张图片

左边是特征图,右边是有dropout的特征图。它们看起来非常相似,请注意,在每个区域中,即使某些单位为零,邻居仍然存在。这意味着,信息将传播到下一层,这并不理想。

DropBlock

DropBlock通过从特征映射中删除连续区域来解决此问题,下图显示了其主要思想。

在PyTorch中实现DropBlock_第3张图片

Dropblock的工作原理如下

在PyTorch中实现DropBlock_第4张图片

实现

我们可以从定义具有正确参数的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通过如下公式计算

2108ce826f83ca42fd96363c6934e569.png

乘法的左边是将被设为零的单位数。右边是有效区域,是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())

在PyTorch中实现DropBlock_第5张图片

看起来不错,让我们看看预训练模型的特征图(如前所述)

# 获取第三层的特征 -> [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())

在PyTorch中实现DropBlock_第6张图片

我们成功地将连续区域归零,而不仅仅是单个单元。

顺便说一句,当block_size=1时,DropBlock等于Dropout

结论

现在我们知道了如何在PyTorch中实现DropBlock,这是一种很酷的正则化技术。

论文给出了不同的实证结果。他们使用普通的resnet50并迭代添加不同的正则化,如下表所示

在PyTorch中实现DropBlock_第7张图片

如你所见,ResNet-50+DropBlock与SpatialDropout(PyTorch中的经典Dropout2d文件)相比增加了1%。

☆ END ☆

如果看到这里,说明你喜欢这篇文章,请转发、点赞。微信搜索「uncle_pn」,欢迎添加小编微信「 woshicver」,每日朋友圈更新一篇高质量博文。

扫描二维码添加小编↓

在PyTorch中实现DropBlock_第8张图片

你可能感兴趣的:(python,深度学习,人工智能,机器学习,opencv)