无论是机器学习,还是深度学习,模型过拟合是很常见的问题,解决手段无非是两个层面,一个是算法层面,一个是数据层面。数据层面一般是使用数据增强手段,算法层面不外乎是:正则化、模型集成、earlystopping、dropout、BN等,本文重点详细讲解一下dropout和BN。
Dropout
背景
在2012年,Hinton在其论文《Improving neural networks by preventing co-adaptation of feature detectors》中提出Dropout。当一个复杂的前馈神经网络被训练在小的数据集时,容易造成过拟合。为了防止过拟合,可以通过阻止特征检测器的共同作用来提高神经网络的性能。
Dropout可以作为训练深度神经网络的一种trick供选择。在每个训练批次中,通过忽略一半的特征检测器(让一半的隐层节点值为0),可以明显地减少过拟合现象。这种方式可以减少特征检测器(隐层节点)间的相互作用,检测器相互作用是指某些检测器依赖其他检测器才能发挥作用。
Dropout说的简单一点就是:我们在前向传播的时候,让某个神经元的激活值以一定的概率p停止工作,这样可以使模型泛化性更强,因为它不会太依赖某些局部的特征。
Dropout具体工作流程
假设我们要训练这样一个神经网络,如图2所示。
文章图片
输入是x输出是y,正常的流程是:我们首先把x通过网络前向传播,然后把误差反向传播以决定如何更新参数让网络进行学习。使用Dropout之后,过程变成如下:(1)首先随机(临时)删掉网络中一半的隐藏神经元,输入输出神经元保持不变(图3中虚线为部分临时被删除的神经元)
文章图片
(2) 然后把输入x通过修改后的网络前向传播,然后把得到的损失结果通过修改的网络反向传播。一小批训练样本执行完这个过程后,在没有被删除的神经元上按照随机梯度下降法更新对应的参数(w,b)。(3)然后继续重复这一过程:. 恢复被删掉的神经元(此时被删除的神经元保持原样,而没有被删除的神经元已经有所更新). 从隐藏层神经元中随机选择一个一半大小的子集临时删除掉(备份被删除神经元的参数)。. 对一小批训练样本,先前向传播然后反向传播损失并根据随机梯度下降法更新参数(w,b) (没有被删除的那一部分参数得到更新,删除的神经元参数保持被删除前的结果)。不断重复这一过程。
dropout的实现细节
Dropout的具体工作流程上面已经详细的介绍过了,但是具体怎么让某些神经元以一定的概率停止工作(就是被删除掉)?代码层面如何实现呢?下面,我们具体讲解一下Dropout代码层面的一些公式推导及代码实现思路。(1)在训练模型阶段无可避免的,在训练网络的每个单元都要添加一道概率流程。
文章图片
没有Dropout的网络计算公式:
文章图片
采用Dropout的网络计算公式:
文章图片
上面公式中Bernoulli函数是为了生成概率r向量,也就是随机生成一个0、1的向量。代码层面实现让某个神经元以概率p停止工作,其实就是让它的激活函数值以概率p变为0。比如我们某一层网络神经元的个数为1000个,其激活函数输出值为y1、y2、y3、…、y1000,我们dropout比率选择0.4,那么这一层神经元经过dropout后,1000个神经元中会有大约400个的值被置为0。
训练和预测的差异
模型训练完了,总是要上线预测的,从上面讲述的dropout原理中我们知道,我们训练的时候会随机的丢弃一些神经元,但是预测的时候就没办法随机丢弃了,如果丢弃一些神经元,这会带来结果不稳定的问题,也就是给定一个测试数据,有时候输出a有时候输出b,结果不稳定,这是实际系统不能接受的,用户可能认为模型预测不准。那么如何解决呢?一种”补偿“的方案就是每个神经元的权重都乘以一个系数,使得训练数据和测试数据在总体上的期望是一致的。一般有两种实现方式,分别如下:
- 比如一个神经元的输出是x,假设p为每个神经元为保留的概率,那么在训练的时候它有p的概率参与训练,(1-p)的概率丢弃,那么它训练时输出的期望是p*x+(1-p)*0=px,在预测的时候,不会随机丢弃一些神经元,每个神经元都会参与预测,如果把神经元的权重乘以p,那么数学期望就是px,这样一来,训练和预测时,数学期望都是px,总体保持一致;
- 再比如一个神经元的输出也是x,假设p为每个神经元为丢弃的概率,那么在训练的时候它有1-p的概率参与训练,p的概率被丢弃,如果给每个神经元权重都乘以1/(1-p),那么它训练时输出的期望是[(1-p)x+p0]/(1-p)=x,预测时不需要对神经元权重进行放缩,这样一来,训练和预测时,数学期望都是x,总体保持一致;
我们对keras中Dropout实现函数做一些修改,让dropout函数可以单独运行。
# coding:utf-8
import numpy as np
def dropout(x, level):
if level < 0. or level >= 1:# level是概率值,必须在0~1之间
raise ValueError('Dropout level must be in interval [0, 1[.')
retain_prob = 1. - level
# 我们通过binomial函数,生成与x一样的维数向量。binomial函数就像抛硬币一样,我们可以把每个神经元当做抛硬币一样
# 硬币 正面的概率为p,n表示每个神经元试验的次数
# 因为我们每个神经元只需要抛一次就可以了所以n=1,size参数是我们有多少个硬币。
random_tensor = np.random.binomial(n=1, p=retain_prob, size=x.shape)# 即将生成一个0、1分布的向量,0表示这个神经元被屏蔽,不工作了,也就是dropout了
print(random_tensor)
x *= random_tensor
print(x)
x /= retain_prob
return x
# 对dropout的测试,大家可以跑一下上面的函数,了解一个输入x向量,经过dropout的结果
x = np.asarray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], dtype=np.float32)
dropout(x, 0.4)
当前Dropout被大量利用于全连接网络,而且一般认为设置为0.5或者0.3,而在卷积网络隐藏层中由于卷积自身的稀疏化以及稀疏化的ReLu函数的大量使用等原因,Dropout策略在卷积网络隐藏层中使用较少。总体而言,Dropout是一个超参,需要根据具体的网络、具体的应用领域进行尝试。
为什么dropout可以防止过拟合
- 在某种程度上,dropout相当于模型融合。dropout工作中,每次随机丢弃一部分神经元,每次丢弃都是随机的,不一样,相当于每次形成的网络结构都不一样,更新的参数都不一样,最后的预测结果类似于多个网络模型的集成的结果;
- dropout随机丢弃一部分神经元,减少了网络结构中的需要更新的参数,有利于减少过拟合;
- dropout随机丢弃一部分神经元,相当于这部分神经元对应的特征直接舍弃了,可以理解成从所有特征中挑选了一部分特征进行训练,每次选的特征集还不一样,这就是类似于RF中的列采样了,增强了模型的泛化能力,减少过拟合;
深度学习中的归一化(BN、LN、IN、GN) 为什么要归一化
神经网络学习过程的本质就是为了学习数据分布,如果我们没有做归一化处理,那么每一批次训练数据的分布不一样,从大的方向上看,神经网络则需要在这多个分布中找到平衡点,从小的方向上看,由于每层网络输入数据分布在不断变化,这也会导致每层网络在找平衡点,显然,神经网络就很难收敛了。当然,如果我们只是对输入的数据进行归一化处理(比如将输入的图像除以255,将其归到0到1之间),只能保证输入层数据分布是一样的,并不能保证每层网络输入数据分布是一样的,所以也需要在神经网络的中间层加入归一化处理。神经网络学习过程本质上就是为了学习数据分布,如果训练数据与测试数据的分布不同,网络的泛化能力就会严重降低。
四种归一化
在深度学习中,有多种归一化,接下来,我们先用一个示意图来形象的表现BN、LN、IN和GN的区别,在输入图片的维度为(NCHW)中,HW是被合成一个维度,这个是方便画出示意图,C和N各占一个维度。
文章图片
Batch Normalization 1.BN的计算就是把每个通道的NHW单独拿出来归一化处理
2.针对每个channel我们都有一组γ,β,所以可学习的参数为2*C
3.当batch size越小,BN的表现效果也越不好,因为计算过程中所得到的均值和方差不能代表全局
Layer Normalizaiton 1.LN的计算就是把每个CHW单独拿出来归一化处理,不受batchsize 的影响
2.常用在RNN网络,但如果输入的特征区别很大,那么就不建议使用它做归一化处理
Instance Normalization 1.IN的计算就是把每个HW单独拿出来归一化处理,不受通道和batchsize 的影响
2.常用在风格化迁移,但如果特征图可以用到通道之间的相关性,那么就不建议使用它做归一化处理
Group Normalizatio 1.GN的计算就是把先把通道C分成G组,然后把每个gHW单独拿出来归一化处理,最后把G组归一化之后的数据合并成CHW
2.GN介于LN和IN之间,当然可以说LN和IN就是GN的特列,比如G的大小为1或者为C
BN、LN、IN和GN这四个归一化的计算流程几乎是一样的,可以分为四步:
1.计算出均值
2.计算出方差
3.归一化处理到均值为0,方差为1
4.变化重构,恢复出这一层网络所要学到的分布
文章图片
训练的时候,是根据输入的每一批数据来计算均值和方差,那么测试的时候,平均值和方差是怎么来的?对于均值来说直接计算所有训练时batch 均值的平均值;然后对于标准偏差采用每个batch 方差的无偏估计。
文章图片
为什么BN可以缓解过拟合 原论文中的解释是这么的:
When training with Batch Normalization, a training example is seen in conjunction with other examples in the mini-batch, and the training network no longer producing deterministic values for a given training example. In our experiments, we found this effect to be advantageous to the generalization of the network. 大概意思是:在训练中,BN的使用使得一个mini-batch中的所有样本都被关联在了一起,因此网络不会从某一个训练样本中生成确定的结果。这句话什么意思呢?意思就是同样一个样本的输出不再仅仅取决于样本本身,也取决于跟这个样本属于同一个mini-batch的其它样本。同一个样本跟不同的样本组成一个mini-batch,它们的输出是不同的(仅限于训练阶段,在inference阶段是没有这种情况的)。我把这个理解成一种数据增强:同样一个样本在超平面上被拉扯,每次拉扯的方向的大小均有不同。不同于数据增强的是,这种拉扯是贯穿数据流过神经网络的整个过程的,意味着神经网络每一层的输入都被数据增强处理了。
BN的实现细节
在BN的实现中,有几个比较重要的参数,分别是running_mean、running_var、beta、gamma,动量momentum,下面分别说明。
- running_mean和running_var只在训练时前向传播中更新,每个batch数据中更新一次,不属于可学习的参数,因为不参与反向传播的更新;
- beta、gamma在反向传播中更新,属于可学习的参数,也是BN中唯一需要学习的参数;
- momentum,负责参与running_mean和running_var的更新,是running_mean和running_var的滑动平均系数,具体更新公式,看下面代码;
data = https://www.it610.com/article/np.array([[1, 2],
[1, 3],
[1, 4]]).astype(np.float32)
初始化过程:
class MyBN:
def __init__(self, momentum, eps, num_features):
"""
初始化参数值
:param momentum: 追踪样本整体均值和方差的动量
:param eps: 防止数值计算错误
:param num_features: 特征数量
"""
# 对每个batch的mean和var进行追踪统计
self._running_mean = 0
self._running_var = 1
# 更新self._running_xxx时的动量
self._momentum = momentum
# 防止分母计算为0
self._eps = eps
# 对应论文中需要更新的beta和gamma,采用pytorch文档中的初始化值
self._beta = np.zeros(shape=(num_features, ))
self._gamma = np.ones(shape=(num_features, ))
前向传播过程
def batch_norm(self, x):
"""
BN向传播
:param x: 数据
:return: BN输出
"""
#x为2维矩阵
x_mean = x.mean(axis=0)
x_var = x.var(axis=0)
# 对应running_mean的更新公式
self._running_mean = (1-self._momentum)*x_mean + self._momentum*self._running_mean
self._running_var = (1-self._momentum)*x_var + self._momentum*self._running_var
# 对应论文中计算BN的公式
x_hat = (x-x_mean)/np.sqrt(x_var+self._eps)
y = self._gamma*x_hat + self._beta
return y
在pytorch中,实现函数是torch.nn.BatchNorm1d(num_features, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True),
默认情况下,track_running_stats为True,表示训练阶段采用实时的batch均值和方差, 同时采用滑动平均来计算全局的running_mean和running_var,测试阶段采用当前的running_mean和running_var,如果track_running_stats=False,则训练阶段和测试阶段都采用实时的batch均值和方差。
也就是说:当track_running_stats为True时,那么在预测的时候,BN层的输出Y与输入X之间的关系是:Y = (X - running_mean) / sqrt(running_var + eps) * gamma + beta,此不赘言。其中gamma、beta为可学习参数(在pytorch中分别改叫weight和bias),训练时通过反向传播更新;而running_mean、running_var则是在前向时先由X计算出mean和var,再由mean和var以动量momentum来更新running_mean和running_var。所以在训练阶段,running_mean和running_var在每次前向时更新一次;在测试阶段,则通过net.eval()固定该BN层的running_mean和running_var,此时这两个值即为训练阶段最后一次前向时确定的值,并在整个测试阶段保持不变。
【Dropout和BN(层归一化)详解】从这个角度看,BN具有一定的正则化效果,在Batch Normalization中,由于我们使用mini-batch的均值与方差作为对整体训练样本均值与方差的估计,尽管每一个batch中的数据都是从总体样本中抽样得到,但不同mini-batch的均值与方差会有所不同,这就为网络的学习过程中增加了随机噪音,与Dropout通过关闭神经元给网络训练带来噪音类似,在一定程度上对模型起到了正则化的效果。原作者也证明了网络加入BN后,可以丢弃Dropout,模型也同样具有很好的泛化效果。