1.正则化(Regularization):通过对问题的求解目标增加一定的先验限制或约束,引导问题的求解。
2.过拟合(Overfitting):指所得网络泛化能力差,表现为网络训练误差小,测试误差大,网络在训练集上表现极好,而在测试集上表现一般甚至很差。
3.伯努利分布(Bernoulli distribution):对于随机变量X,有参数p(04.Dropout通过对其输入进行一定概率的“舍弃”起到一种正则化的作用,可有效缓解网络过拟合。
对于一个正常的全连接网络而言,当前层每个神经元的值由前一层所有神经元共同决定,对于输入X,沿着网络前向传播,然后将误差回传,以此更新神经元之间的权重以及偏置参数。
当我们对其中的隐藏层使用Dropout后,隐藏层中的神经元以一定概率“睡眠”,通过对其随机置零实现,其更新参数的过程也发生了一些改变,具体地,过程如下:
(1)网络首先按照一定概率将隐藏层中的部分神经元随机置零,此时这些神经元将暂时处于“睡眠”状态;
(2)输入X沿着网络前向传播,然后通过误差回传更新神经元参数,需要注意的是此时只有未被“置零”的神经元参数被更新,处于“睡眠”状态的神经元参数与上一次参数更新的结果保持一致;
(3)将隐藏层所有神经元激活,此时之前未被“睡眠”的部分神经元参数得到更新,“睡眠”状态的神经元参数与上一次更新结果保持一致,然后重复(1)、(2)步骤;
之前在解决过拟合问题时,一般会采用“模型集成”的方法,在训练数据上训练多个模型,然后在测试数据上对这些模型采用一定的策略(投票法、平均法等)进行组合集成,以此对训练好的多个模型“取长补短”,使得最终模型拥有更好的泛化能力,缓解过拟合情况的发生。但是模型集成带来很直接的缺陷就是极其耗时以及过高的算力需求。
《Improving neural networks by preventing co-adaptation of feature detectors》一文中提出Dropout通过对隐藏层部分神经元随机置零的方式去缓解过拟合,并且也不用进行耗时的模型集成。正如这篇文章题目中所提到的“通过防止特征检测器之间的协同适应去改进神经网络”,Dropout可以从两个方面去缓解过拟合:
1)防止特征检测器之间的共适应关系:在隐藏层之间利用Dropout操作可使不同隐藏层之间的部分神经元之间断开连接,在每一批次数据进行训练的时候这些神经元不一定会同时激活。如此操作可使网络参数的更新不必完全依赖标准网络中存在特定连接关系的神经元之间的共同作用,以此使网络学习更鲁棒的特征。比如在标准全连接网络中判断一个物品是否是西瓜时,必须同时具备球体、绿色、有瓜蒂等特征,而采用Dropout操作训练的网络可以在缺失某些特征时也能准确预测结果。
2)类似采用平均法进行模型集成:Dropout操作对隐藏层中的神经元随机“丢弃”,每批次数据Dropout丢弃的神经元并不固定,这就导致此时的网络结构是不同的,所以进行网络训练时其实就相当于在训练不同的网络,整个网络训练过程就好比对不同的网络模型进行平均法集成,以此得到更好的泛化能力,有效缓解过拟合。
在进行代码实现时,实际是通过控制神经元的激活值是否置零实现,而置零操作通过伯努利分布随机生成0或1向量,将此向量与标准网络的神经元输出相乘,此时部分神经元的输出则被成功置零。比如,原始输出为y1=1,y2=2,此时与之对应生成向量r1=1,r2=0,将他们对应相乘则得到此时的输出y1’=1,y2’=0。
图中分别展示有无Dropout时对于黄色神经元输出结果的变化:
1)无dropout
z1=w1y1+w2y2
2)有dropout
1)以概率p随机将输入张量中的元素置零,并且对于多通道输入,每个通道的置零操作是独立的;
2)然后对其输出结果乘以1/(1-p)进行缩放;
这里对为什么要对Dropout的输出还要进行缩放进行解释:
首先需要明确在Pytorch实现Dropout时,其参数p是对部分神经元输出值进行置零的概率。因为Dropout在网络训练和评估时表现是不一样的,其在网络训练模式下以概率p对隐藏层部分神经元的输出随机置零,而在网络评估时是直接进行一致性映射,也就是直接对输入进行输出,不做操作,所以为了保证隐藏层神经元在训练和评估模式下输出结果的期望是一致的,我们需要在训练时对Dropout的输出结果进行缩放,并且缩放因子必须是1/(1-p)(后面的代码示例也证实了这一点)。
假设标准网络中隐藏层神经元输出为Y,故期望为E(Y)=Y,
在采用Dropout之后,若不进行缩放,则期望变为:
要想采用Dropout前后,神经元输出结果的期望保持不变,则需要对其输出进行缩放,如此期望即可保持不变:
torch.nn.Dropout(p=0.5, inplace=False)
参数名 | 含义 |
---|---|
p | 以概率p随机将输入张量中元素置零,通常取p=0.5效果最好 |
inplace | 默认inplace=False,当inplace=True时表示使用Dropout层的输出结果替换原本输入张量在内存中位置处存储的数据(其实就是使用输出结果在内存中覆盖输入)。 |
1)关于参数p
此处的p与伯努利分布中的参数p是有区别的:此处p是指以p概率将部分元素随机置零;而伯努利分布中随机变量以概率p取1为值。
2)关于inplace
可使用函数id(x)查看inplace在不同设置下,其输入和输出变量在内存中的地址是否一致验证inplace的作用,测试代码:
在执行Dropout之前以及之后均打印输入张量以及其对应的内存地址:
import torch
import torch.nn as nn
inp=torch.ones(size=(1,10),dtype=torch.float32)
print(id(inp))
print(inp)
print(inp.shape)
print('-'*25)
class TestDemo(nn.Module):
def __init__(self):
super(TestDemo, self).__init__()
self.drop=nn.Dropout(p=0.5,inplace=False)
def forward(self,x):
return self.drop(x)
out=TestDemo()(inp)
print(out)
print(out.shape)
print(id(out))
print('-'*25)
print(id(inp))
print(inp)
print(inp.shape)
print('-'*25)
inplace=True
2566402092704
tensor([[1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]])
torch.Size([1, 10])
-------------------------
tensor([[0., 0., 0., 0., 2., 2., 0., 2., 0., 2.]])
torch.Size([1, 10])
2566402092704
-------------------------
2566402092704
tensor([[0., 0., 0., 0., 2., 2., 0., 2., 0., 2.]])
torch.Size([1, 10])
-------------------------
inplace=False
2107016691504
tensor([[1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]])
torch.Size([1, 10])
-------------------------
tensor([[0., 0., 2., 2., 0., 2., 0., 0., 2., 0.]])
torch.Size([1, 10])
2107016690640
-------------------------
2107016691504
tensor([[1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]])
torch.Size([1, 10])
-------------------------
上面的测试结果可看到,当inplace=True时,输出向量直接原地替换原始输入,这样做可以节省内存,而当inplace=False时,输出变量和输入变量均有自己的一块内存存储数据。
注意Dropout在网络处于训练还是评估模式下,执行结果是有区别的:训练时,Dropout会以p概率对部分神经元值置零;评估时,Dropout不会对神经元进行操作,可以忽略。
inp=torch.ones(size=(1,10),dtype=torch.float32)
print(inp)
print(inp.shape)
print('-'*25)
class TestDemo(nn.Module):
def __init__(self):
super(TestDemo, self).__init__()
self.drop=nn.Dropout(p=0.5,inplace=False)
def forward(self,x):
return self.drop(x)
model=TestDemo()
model.train() # 设置网络为训练模式
out=model(inp)
print(out)
print(out.shape)
print('-'*25)
model.eval() # 设置网络为评估模式
out=model(inp)
print(out)
print(out.shape)
tensor([[1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]])
torch.Size([1, 10])
-------------------------
model.train():训练模式
tensor([[0., 0., 0., 2., 0., 2., 2., 2., 2., 2.]])
torch.Size([1, 10])
-------------------------
model.eval():评估模式
tensor([[1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]])
torch.Size([1, 10])
-------------------------
由上面测试结果可证实:Dropout层在网络训练模式和评估模式下表现不同,当处于评估模式时,Dropout不会对输入进行操作,直接输出一个一致性映射结果。此外,可以观察一下训练模式下的输出结果,Dropout首先对其输入进行部分置零,得到由0,1构成的输出;然后对该输出进行缩放因子为1/(1-p)=1/(1-0.5)=2的缩放得到由0,2构成的最终输出。
除了上面的nn.Dropout,Pytorch针对不同维度的张量分别更细致地实现了对应的Dropout1d和Dropout2d,在这里我也简单说一下:
1)nn.Dropout1d通常接在nn.Conv1d后面,用于一维张量的处理;输入形状为(N,C,L)或(C,L),输出结果与之对应。
2)nn.Dropout2d通常接在nn.Conv2d后面,用于二维张量的处理;输入形状为(N,C,H,W)或(N,C,L),输出结果与之对应)。
nn.Dropout2d为啥不支持(C,H,W)这是版本迭代中的历史遗留问题,更多参见Pytorch官网:https://pytorch.org/docs/stable/nn.html,不过我们进行网络训练时图像数据形状一般都是(N,C,H,W),所以也可以忽略这个点。
(1)https://zhuanlan.zhihu.com/p/38200980
(2)《Improving neural networks by preventing
co-adaptation of feature detectors》:https://arxiv.org/pdf/1207.0580.pdf