深度学习系列文章目录
前言
看到一些好的东西就忍不住想记录下来,方便学习和记忆。本文讲一下BatchNorm 训练和推理过程中的一些解读。
参考如下:
Batchnorm原理:https://blog.csdn.net/qq_25737169/article/details/79048516
上篇博客的归纳整理
推理时BN和Conv融合:https://mp.weixin.qq.com/s/P94ACKuoA0YapBKlrgZl3A
一、Batchnorm
batchnorm顾名思义是对每batch个数据同时做一个norm,对一个神经元(或者一个卷积核)的输出减去一个batch统计得到的均值,除以标准差,然后乘以一个可学习的系数,再加上一个偏置,这个过程就完成了。
第一步:先求出此次批量数据x的均值,μβ=1m∑mi=1xi
第二步:求出此次批量数据的方差,σβ2=1m∑i=1m(xi−μβ)2
第三步:接下来就是对x做归一化,得到xi−
第四步:最重要的一步,引入缩放和平移变量γ和β ,计算归一化后的值,yi=γxi−+β
如果不加γ和β,直接归一化,是会打乱原有数据的分布,容易导致网络学不到任何东西,但是加入这两个参数后,事情就不一样了。先考虑特殊情况,假设γ是batch的方差,β是batch的均值,那么yi=γxi−+β得到的yi就是还原到了归一化之前的x,也就是缩放平移到了归一化前的分布,相当于batchnorm没有改变任何分布没有起作用。所以,加入了γ和β这两个参数后的batchnorm,保证了每一次数据归一化后还保留有之前学习来的特征分布,同时又能完成归一化的操作,加速训练。
在训练过程中,为保持稳定,一般使用滑动平均法更新均值和方差,滑动平均就是在更新当前值的时候,以一定比例保存之前的数值,以均值 为例,以一定比例 (例如这里0.99)保存之前的均值,当前只更新0.001倍的本Batch的均值,计算方法如下:
训练代码如下(示例):
def Batchnorm_simple_for_train(x, gamma, beta, bn_param):
"""
param:x : 输入数据,设shape(B,L)
param:gama : 缩放因子 γ
param:beta : 平移因子 β
param:bn_param : batchnorm所需要的一些参数
eps : 接近0的数,防止分母出现0
momentum : 动量参数,一般为0.9, 0.99, 0.999
running_mean :滑动平均的方式计算新的均值,训练时计算,为测试数据做准备
running_var : 滑动平均的方式计算新的方差,训练时计算,为测试数据做准备
"""
running_mean = bn_param['running_mean'] #shape = [B]
running_var = bn_param['running_var'] #shape = [B]
results = 0. # 建立一个新的变量
x_mean=x.mean(axis=0) # 计算x的均值
x_var=x.var(axis=0) # 计算方差
x_normalized=(x-x_mean)/np.sqrt(x_var+eps) # 归一化
results = gamma * x_normalized + beta # 缩放平移
running_mean = momentum * running_mean + (1 - momentum) * x_mean
running_var = momentum * running_var + (1 - momentum) * x_var
#记录新的值
bn_param['running_mean'] = running_mean
bn_param['running_var'] = running_var
return results , bn_param
在训练的时候事先计算好mean、var在测试的时候直接拿来用就行,不用计算均值和方差。
running_mean = momentum * running_mean + (1 - momentum) * x_mean
running_var = momentum * running_var + (1 - momentum) * x_var
测试代码如下(示例):
def Batchnorm_simple_for_test(x, gamma, beta, bn_param):
"""
param:x : 输入数据,设shape(B,L)
param:gama : 缩放因子 γ
param:beta : 平移因子 β
param:bn_param : batchnorm所需要的一些参数
eps : 接近0的数,防止分母出现0
momentum : 动量参数,一般为0.9, 0.99, 0.999
running_mean :滑动平均的方式计算新的均值,训练时计算,为测试数据做准备
running_var : 滑动平均的方式计算新的方差,训练时计算,为测试数据做准备
"""
running_mean = bn_param['running_mean'] #shape = [B]
running_var = bn_param['running_var'] #shape = [B]
results = 0. # 建立一个新的变量
x_normalized=(x-running_mean )/np.sqrt(running_var +eps) # 归一化
results = gamma * x_normalized + beta # 缩放平移
return results , bn_param
二、推理时BatchNorm和Conv融合
训练的时候,均值mean、方差var、γ 、β是一直在更新的,但是,在推理的时候,以上四个值都是固定了的,也就是推理的时候,均值和方差来自训练样本的数据分布。因此,在推理的时候,上面BN的计算公式可以变形为:
在均值mean、方差var、γ 、β都是固定值的时候,上面公式可以改写为:
推理的时候,Batch Norm层的4个参数是固定的常数,我们以一个三个神经元输入的全连接网络为例,如下图::
则全连接输出:
其中c 为偏置(这里为避免与上面的冲突,所以用 c 表示),那么全连接 + BN 一起,则是:
公式转换如下:
到这里大家应该清楚了,因为推理时,BN是一个线性的操作,也就是一个缩放+一个偏移,我们完全可以把这个线性操作叠加到前面的全连接层或者卷积层,只需要把全连接或者卷积层的权重乘以一个系数a (alpha),偏置从 c 变为 ac+b 就可以了了。完整的过程如下图:
在训练时候,在卷积层后面直接加BN层,训练完成后,我们只需要将网络中BN层去掉,读取原来的卷积层权重和偏置,以及BN层的四个参数(均值、方差、γ 、β),然后按照上面的计算方法替换卷积核的权重,更新偏置就可以了。
pytorch 官方实现https://github.com/pytorch/pytorch/blob/master/torch/nn/utils/fusion.py
ResNet18中一个卷积+BN层融合后代码如下(示例):
import torch
import torchvision
def fuse(conv, bn):
fused = torch.nn.Conv2d(
conv.in_channels,
conv.out_channels,
kernel_size=conv.kernel_size,
stride=conv.stride,
padding=conv.padding,
bias=True
)
# setting weights
w_conv = conv.weight.clone().view(conv.out_channels, -1)
w_bn = torch.diag(bn.weight.div(torch.sqrt(bn.eps+bn.running_var)))
fused.weight.copy_( torch.mm(w_bn, w_conv).view(fused.weight.size()) )
# setting bias
if conv.bias is not None:
b_conv = conv.bias
else:
b_conv = torch.zeros( conv.weight.size(0) )
b_bn = bn.bias - bn.weight.mul(bn.running_mean).div(
torch.sqrt(bn.running_var + bn.eps)
)
fused.bias.copy_( b_conv + b_bn )
return fused
# Testing
# we need to turn off gradient calculation because we didn't write it
torch.set_grad_enabled(False)
x = torch.randn(16, 3, 256, 256)
resnet18 = torchvision.models.resnet18(pretrained=True)
# removing all learning variables, etc
resnet18.eval()
model = torch.nn.Sequential(
resnet18.conv1,
resnet18.bn1
)
f1 = model.forward(x)
fused = fuse(model[0], model[1])
f2 = fused.forward(x)
d = (f1 - f2).mean().item()
print("error:",d)
Note: 融合BN仅限于Conv+BN或者是BN+Conv结构,中间不能加非线性层,例如Conv+Relu+BN那就不行了。当然,一般结构都是Conv+BN+Relu结构。
总结
至此,本人对Batch Norm有了一个更深的理解,后续在部署时推理效率提升可以使用这个方法。tips:一般需将模型转换成caffe模型来merge卷积和BN层能避免更多坑。