文章目录
推荐文档:
问题:什么是Batch Normalization?
回答:总结来说,Batch Normalization是2015年一篇论文中提出的数据归一化方法。它的作用:加快训练收敛速度,使训练过程更加稳定,尽量避免梯度爆炸或者梯度消失。次要作用:起到一定的正则化作用,限制模型复杂度,减少过拟合。
数据归一化指的是将数据按照比例放缩到特定的范围内,消除量纲和尺度之间的差异,毕竟让你的输入数据的值都在一个尺度之内,模型的参数也能够更好得配置,加快收敛速度。数据归一化的方法有非常多,这里不阐述。
至于减少过拟合则是附带的一个作用,毕竟你收敛更快,模型不需要一直学习去拟合,复杂度不需要这么高。过往主要是使用Dropout层来降低过拟合,Dropout层主要就是用来降低过拟合的,没有加快收敛的作用。
Batch Normalization,简写为BN,常用在激活层之前,可以确保输入在归一化后仍然通过非线性激活函数,保留模型的表达能力。如果在激活层后面使用BN,可能会破坏激活函数的输出分布,导致结果不理想。
问题:Batch Normalization和Dropout有什么不同
回答:Batch Normalization训练和推理的时候都参与,但是Dropout只参与训练,不参与推理。关于BN的作用,花书的观点是:“并不是一个优化方法,而是一自适应的重参数化方法”,它真正的作用应该是减少多层协同更新时上一层更新对后层更新效果干扰的问题?
问题:BN的代码实现?
回答:要学会BN的代码实现首先需要知道BN的计算公式。先对输入的一组数据计算均值和方差,然后对这组数据的每个值,利用均值和方差做几个计算得到一个新的值,这组数据后面就会变成均值为0,方差为1的一组数据。然后再对每个数据值做一个带偏置的线性变换,得到最终的结果,这个线性变换和Linear层差不多,里面的系数和偏置参数都是可以训练的。
计算均值和方差的一组数据是如何确定的呢?是对一个batch = 8,就是对8条samples作为一组数据进行均值和方差的计算。假设8张RGB图片,均值就是 先 对 8张 图片的 R通道 的值进行求和,然后除以对应的数量,得到均值,然后 G 通道也是这么做, B通道也是这么做。方差也是这么计算,这样我们能够对 R通道得到一个方差和均值,返回去对R通道的数据进行数据归一化。最终我们在R通道上的数据遵从一个正态分布,在另外的G和B通道也是这样,然后对于每个通道都设置一个缩放参数 γ (gamma) 和偏移参数 β (beta),就可以对每个通道的正态分布分别进行缩放和偏移,并且这两个参数是可以训练的,所以能够训练得到一个最合适的缩放和偏移。
class myBatchNorm2d(nn.Module):
def __init__(self, input_size = None , epsilon = 1e-3, momentum = 0.99):
super(myBatchNorm2d, self).__init__()
assert input_size, print('Missing input_size parameter.')
# 定义训练期间的 Batch 均值和方差
self.mu = torch.zeros(1, input_size)
# 方差是与平均值的平方偏差的平均值,即 var = mean(abs(x-x.mean())** 2)
self.var = torch.ones(1, input_size)
# 常数,为了数值稳定
self.epsilon = epsilon
# Exponential moving average for mu & var update
self.it_call = 0 # training iterations
self.momentum = momentum # EMA smoothing
# 可训练的参数
self.beta = torch.nn.Parameter(torch.zeros(1, input_size))
self.gamma = torch.nn.Parameter(torch.ones(1, input_size))
# Batch size on which the normalization is computed
self.batch_size = 0
def forward(self, x):
# [batch_size, input_size]
self.it_call += 1
if self.training :
if( self.batch_size == 0 ):
# First iteration : save batch_size
self.batch_size = x.shape[0]
# Training : compute BN pass
batch_mu = (x.sum(dim=0)/x.shape[0]).unsqueeze(0) # [1, input_size]
batch_var = (x.var(dim=0)/x.shape[0]).unsqueeze(0) # [1, input_size]
# [batch_size, input_size]
# 这里两行公式,直接把矩阵中的全部值都计算完了
x_normalized = (x-batch_mu)/torch.sqrt(batch_var + self.epsilon)
# [batch_size, input_size]
x_bn = self.gamma * x_normalized + self.beta
# 更新 mu & std
if(x.shape[0] == self.batch_size):
running_mu = batch_mu
running_var = batch_var
else:
running_mu = batch_mu*self.batch_size/x.shape[0]
running_var = batch_var*self.batch_size/x.shape[0]
self.mu = running_mu * (self.momentum/self.it_call) + \
self.mu * (1 - (self.momentum/self.it_call))
self.var = running_var * (self.momentum/self.it_call) + \
self.var * (1 - (self.momentum/self.it_call))
# 训练时,均值、方差分别是该批次内数据相应维度的均值与方差;推理时,均值、方差是基于所有批次的期望计算所得。
else:
# 推理 : compute BN pass using estimated mu & var
if (x.shape[0] == self.batch_size):
estimated_mu = self.mu
estimated_var = self.var
else :
estimated_mu = self.mu*x.shape[0]/self.batch_size
estimated_var = self.var*x.shape[0]/self.batch_size
x_normalized = (x-estimated_mu)/torch.sqrt(estimated_var + self.epsilon) # [batch_size, input_size]
x_bn = self.gamma * x_normalized + self.beta # [batch_size, input_size]
return x_bn # [batch_size, output_size=input_size]
问题:为什么训练的时候和推理的时候BN的操作会有区别?
回答:因为训练的时候Batch Size是固定的,假设为8,但是推理的时候Batch Size可能和训练时的不一样,毕竟我可以在推理的时候设置成1,也可以设置成4。BN的计算,Batch Size也参与了,所以不同 Batch Size会导致均值和方差计算结果的不同,最终导致的输出波动。
为了解决上面的问题,提出在训练阶段,通过指数移动平均(Exponential Moving Average, EMA)的方法,来得到一个相对好的均值和方差,存储在self.mu
和self.var
中。在推理的时候,如果你推理的Batch Size和训练的时候是一样的,那么直接使用self.mu
和self.var
,如果不是一样的,那么self.mu
和self.var
会按照 当前的Batch Size 和 训练时的Batch Size 的比例 来进行放缩,最后再对数据做归一化。