Hello,大家好,我是灿视!
今天我们来继上一篇 我丢!算法岗必问!建议收藏!的内容,推出的“我再丢”!
欢迎关注我们,设置星标,追更《百面计算机视觉第三版》!文末有《百面第三版的汇总链接》,欢迎查阅!
是采取类似于
的思路,在
块上随机进行对模块的删除,进而提高对模型的泛化能力。
如图所示,为
的具体做法。
用数学化的语言来表示下该过程就是:
若网络总共有
个
,我们给每个
都加上了一个概率
。
在训练时:根据
用一个
随机变量生成每个
的激活状态
,最终把
的
,从
调整成了
。
其中,当
时,表明这个
未被激活,此时
。特别地是。其中
是从
线性衰减到
,即
。
在预测的时候:
被定义为:
。相当于将
与该层的残差做了一个权值融合了。
个人觉得这样
有以下两个好处:
的方式,本质上一种模型融合的方案。由于训练时模型的深度随机,预测时模型的深度确定,事实上是在测试时把不同深度的模型融合了起来。
或者
都主要是在全连接层进行,这里是对整个网络进行
的。
这里给出一个参考代码如下:
class BottleNeck(nn.Module):
def __init__(self, in_channels, out_channels, stride):
super(BottleNeck, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
self.conv2 = nn.Sequential(
nn.Conv2d(in_channels=out_channels, out_channels=out_channels, kernel_size=3, stride=stride, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
self.conv3 = nn.Sequential(
nn.Conv2d(in_channels=out_channels, out_channels=(out_channels * 4), kernel_size=1),
nn.BatchNorm2d((out_channels * 4)),
nn.ReLU(inplace=True)
)
self.relu = nn.ReLU(inplace=True)
self.downsample = nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=(out_channels * 4), kernel_size=1, stride=stride),
nn.BatchNorm2d((out_channels * 4))
)
def forward(self, x, active):
if self.training:
if active == 1:
print("active")
identity = x
identity = self.downsample(identity)
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = x + identity
x = self.relu(x)
return(x)
else:
print("inactive")
x = self.downsample(x)
x = self.relu(x)
return(x)
else:
identity = x
identity = self.downsample(identity)
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = self.prob * x + identity
x = self.relu(x)
return(x)
目前为主,丢的主要是权重,或者是丢的是神经元。这里开始,我们要丢的是是网络的输入,当然网络输入不仅仅可以丢,也可以添加噪声(
等),这个是后面要做的内容。当然,还有一些对于输入图像进行
的操作(如
),我这里先打个样,看下如何对输入图像进行丢弃。后面补充下,其它丢弃输入图像的操作。
先看看
的做法:
图像上进行随机位置和一定大小的
进行
裁剪。一开始使用裁剪上采样等变换出复杂轮廓的
,后来发现简单的固定像素
就可以达到不错的效果,所以直接采用正方形
。
通过
的遮盖可以让网络学习到遮挡的特征。
不仅能够让模型学习到如何辨别他们,同时还能更好地结合上下文从而关注一些局部次要的特征。
的效果图如下所示:
参考代码如下:
import torch
import numpy as np
class Cutout(object):
"""Randomly mask out one or more patches from an image.
Args:
n_holes (int): Number of patches to cut out of each image.
length (int): The length (in pixels) of each square patch.
"""
def __init__(self, n_holes, length):
self.n_holes = n_holes
self.length = length
def __call__(self, img):
"""
Args:
img (Tensor): Tensor image of size (C, H, W).
Returns:
Tensor: Image with n_holes of dimension length x length cut out of it.
"""
h = img.size(1)
w = img.size(2)
mask = np.ones((h, w), np.float32)
for n in range(self.n_holes):
y = np.random.randint(h) # 返回随机数/数组(整数)
x = np.random.randint(w)
y1 = np.clip(y - self.length // 2, 0, h) #截取函数
y2 = np.clip(y + self.length // 2, 0, h) #用于截取数组中小于或者大于某值的部分,
x1 = np.clip(x - self.length // 2, 0, w) #并使得被截取的部分等于固定的值
x2 = np.clip(x + self.length // 2, 0, w)
mask[y1: y2, x1: x2] = 0.
mask = torch.from_numpy(mask) #数组转换成张量,且二者共享内存,对张量进行修改比如重新赋值,那么原始数组也会相应发生改变
mask = mask.expand_as(img) #把一个tensor变成和函数括号内一样形状的tensor
img = img * mask
return img
有两个超参,不同的任务,可以自己调调实验下效果。
首先直观的从图片中看下
的具体做法:
其中(b)表示的是随机
的效果,(c)为
掉相邻的一整片区域,即按
块随机扔。
其论文中的算法伪代码如下:
其中这个
的值,是依赖于
的值的。其计算过程如下:
可以解释为传统的
保留激活单元的概率, 则有效的区域为
,
为
的
. 实际上
中的
可能存在重叠的区域, 因此上述的公式仅仅只是一个估计. 实验中
设置为0.75~0.95, 并以此计算
的值。
给出一个参考的
版本的代码:
#!/usr/bin/env python
# -*- coding:utf8 -*-
import torch
import torch.nn.functional as F
from torch import nn
class Drop(nn.Module):
def __init__(self, drop_prob=0.1, block_size=7):
super(Drop, self).__init__()
self.drop_prob = drop_prob
self.block_size = block_size
def forward(self, x):
if self.drop_prob == 0:
return x
# 设置gamma,比gamma小的设置为1,大于gamma的为0,对应第五步
# 这样计算可以得到丢弃的比率的随机点个数
gamma = self.drop_prob / (self.block_size**2)
mask = (torch.rand(x.shape[0], *x.shape[2:]) < gamma).float()
mask = mask.to(x.device)
# compute block mask
block_mask = self._compute_block_mask(mask)
# apply block mask,为算法图的第六步
out = x * block_mask[:, None, :, :]
# Normalize the features,对应第七步
out = out * block_mask.numel() / block_mask.sum()
return out
def _compute_block_mask(self, mask):
# 取最大值,这样就能够取出一个block的块大小的1作为drop,当然需要翻转大小,使得1为0,0为1
block_mask = F.max_pool2d(input=mask[:, None, :, :],
kernel_size=(self.block_size,
self.block_size),
stride=(1, 1),
padding=self.block_size // 2)
if self.block_size % 2 == 0:
# 如果block大小是2的话,会边界会多出1,要去掉才能输出与原图一样大小.
block_mask = block_mask[:, :, :-1, :-1]
block_mask = 1 - block_mask.squeeze(1)
return block_mask
结合上一篇的三种
策略,我们主要从主要作用在全连接网络的
,作用在
层面的
,作用在
层面的
,作用在
层面的
,作用在输入层面的
等方式。给大家梳理了各个
方案,后面有一些列的工作是针对输入提出的正则化技巧(数据增强),在后面的文章,我们再进行补充~
这些方案具体怎么用?不好意思,需要你针对你自己的任务自己去调了。
在这里,我们要谈下,为何BN提出后,Dropout就不用了呢?
首先我们聊下在
中
的
:
nn.BatchNorm2d(self, num_features, eps=1e-5, momentum=0.1, affine=True, track_running_stats=True)
和
对于
层的状态,包含了
个参数:
。
假设我们的输入
的维度是
,那么我们我们在做
的时候,我们在
维度中“抽”出来一个通道的数据,则其维度为
。我们需要对这
个数据求均值
跟方差
,并用求得的均值与方差归一化,再缩放数据,得到
层的输出。
我们需要用滑动平均公式来更新
与
,
默认为0.1.
因此,Dropout在网络测试的时候神经元会产生“variance shift”,即“方差偏移”。试想若有图一中的神经响应
,当网络从训练转为测试时,
可以通过其随机失活保留率(即
)来缩放响应,并在学习中改变神经元的方差,而
仍然维持
的统计滑动方差(
)。这种方差不匹配可能导致数值不稳定。而随着网络越来越深,最终预测的数值偏差可能会累计,从而降低系统的性能。事实上,如果没有
,那么实际前馈中的神经元方差将与
所累计的滑动方差非常接近,这也保证了其较高的测试准确率。
下面有张图,也比较清楚的反映了,
与
在一起使用存在的问题:
那么怎么解决这样的variance shift的问题呢?有两种方案:
之后,连接一个
。
的公式让它对方差并不那么敏感。有工作是进一步拓展了高斯
(即不是满足伯努利分布,而是Mask满足高斯分布),提出了一个均匀分布
,这样做带来了一个好处就是这个形式的
(又称为
)对方差的偏移的敏感度降低了,总得来说就是整体方差偏地没有那么厉害了。而实验结果也是第二种整体上比第一个方案好,显得更加稳定。
这里先介绍这么多的Drop操作,那对于输入层的各种操作,我们放在数据增强那一块知识进行介绍,希望各位可以关注我,加我好友,一起交流!感谢大家的支持!
- END -
大家好,我是灿视。目前是位算法工程师 + 创业者 + 奶爸的时间管理者!
我曾在19,20年联合了各大厂面试官,连续推出两版《百面计算机视觉》,受到了广泛好评,帮助了数百位同学们斩获了BAT等大小厂算法Offer。现在,我们继续出发,持续更新最强算法面经。
我曾经花了4个月,跨专业从双非上岸华五软工硕士,也从不会编程到进入到百度与腾讯实习。