深入理解Batch Normalization
本文通过以下几点介绍Batch Normalization。
- 什么是Local Response Normalization (LRN)
- 什么是Batch Normalization (BN)
- 训练和预测阶段怎样做BN。(结合源码讲解)
- 实验验证BN效果
- 为什么BN的效果这么好
什么是 Local Response Normalization(LRN)
我们都知道,Normalization是深度神经网络中一项最常用也是最终要的技术之一。早在Alexnet时代,Normalization便已经被应用到了视觉分类任务中,也就是本节所要介绍的LRN。虽然后来的BN的出现,逐渐替代了LRN,但是在这里也简单地介绍以下。说到为什么要使用LRN就不得不提到神经生物学中的一个概念叫做 lateral inhibition(横向抑制),简单来讲就是兴奋的神经细胞抑制周围神经细胞的能力。应用到深度神经网络中,这种横向抑制的目的是进行局部对比度增强,以便将局部特征在下一层得到表达。
LRN是一个非训练层,即该层中不存在可训练的参数。假设某一个CNN层的输出为一个 WxHxC 的张量,要对该张量做LRN我们应该怎么做呢。假设我们取n的大小为3,现在我们要计算(x, y, i)位置上 LRN 后的值,我们便可以以 (x,y, i)点位中心,在channel维度范围内取一个1xn大小的网格,此时我们便可以通过(x, y, i)位置的数值和其周围的数值对(x, y, i)位置进行正则化,具体计算公式如下
其中 i 表示第 i 个通道,x,y分别表示在宽高维度所在的位置。 (k,α,β,n) 这些皆为超参数,其中k是为了防止除0的情况发生,α,β为常数,n为邻域的长度。边界情况则用0补齐。假设某个网格里面的数据长这样:[1,2,4]
,并且令 (k,α, β, n)=(0,1,1,N) (N为通道数), 则通过公式我们可以计算出
取一个3x3x4的Feature map进行LRN操作,效果如图所示(其中不同的颜色代表不同的通道,总的通道数为4。我们设置超参数 **(k,α, β, n)=(0,1,1,2)**。
Pytorch 源码中实现了LRN的操作,可以看到与Alexnet论文中的方法略有不同,α所乘的项由所有数值的平方和变成了所有数值的均值,但无论是使用均值还是平方和,两者都起到了 lateral inhibition(横向抑制)的作用。
那么使用LRN对我们的卷积神经网络有多大的提升呢。Alexnet论文中有这么一段话
Response normalization reduces our top-1 and top-5 error rates by 1.4% and 1.2%, respectively. We also verified the effectiveness of this scheme on the CIFAR-10 dataset: a four-layer CNN achieved a 13% test error rate without normalization and 11% with normalization3
也就是说在Imagenet数据集上获得了%1.4左右的提升,在CIFAR数据集上获得了2%的提升。提升效果还是比较明显。
什么是Batch Normalization
顾名思义,batch normalization嘛,就是“批规范化”。
如图所示,feature map:
包含 N 个样本,每个样本通道数为 C,高为 H,宽为 W。对其求均值和方差时,将在 N、H、W上操作,而保留通道 C 的维度。具体来说,就是把第1个样本的第1个通道,加上第2个样本第1个通道 ...... 加上第 N 个样本第1个通道,求平均,得到通道 1 的均值。对所有通道都施加一遍这个操作,就得到了所有通道的均值和方差。具体公式为:
最后我们将每个pixel对应的值减去均值,除以方差,就得到了规范化的结果。在此基础上,BN还增加了两个可训练的参数 γ,β。所以最终的表达式为:
}{\sigma_c(x)})+++\beta)
下面我们用pytorch简单实现以下BN层(仅训练阶段):
import torch class BatchNorm2d(torch.nn.Module): def __init__(self, channel, eps=1e-5, affine=True): super().__init__() # 初始化训练参数 self.gamma = torch.nn.Parameter(torch.ones(1, channel, 1, 1)) self.beta = torch.nn.Parameter(torch.zeros(1, channel, 1, 1)) self.eps = eps self.affine = affine def forward(self, input): # input shape must be (n, c, h, w) means = input.mean((0, 2, 3), keepdim=True) vars = torch.sqrt(((input-means)**2).sum((0, 2, 3), keepdim=True)) output = (input - means) / (vars + self.eps) if self.affine: output = output* self.gamma + self.beta return output
预测阶段要怎么做BN
我们知道BN在每一层计算的
与
都是基于当前batch中的训练数据,但是这就带来了一个问题:我们在预测阶段,有可能只需要预测一个样本或很少的样本,没有像训练样本中那么多的数据,此时
与
的计算一定是有偏估计,这个时候我们该如何进行计算呢?
利用BN训练好模型后,我们保留了每组mini-batch训练数据在网络中每一层的
与
。此时我们使用整个样本的统计量来对Test数据进行归一化,具体来说使用均值与方差的无偏估计:
得到每个特征的均值与方差的无偏估计后,我们对test数据采用同样的normalization方法:
在实际计算的过程中,我们并不会保留训练过程中的所有历史值,再求平均,因为训练早期模型为收敛时的值没有太大的价值,甚至会干扰对均值和方差的估计。因此我们可以加入一个momentum参数。 该参数作用于mean和variance的计算上,这里保留了历史batch里的mean和variance值,借鉴优化算法里的momentum算法将历史batch里的mean和variance的作用延续到当前batch。一般momentum的值为0.9 , 0.99等. 多个batch后, 即多个0.9连乘后,最早的batch的影响会变弱。
而最后的“scale and shift”操作则是为了让因训练所需而“刻意”加入的BN能够有可能还原最初的输入(即当
}+=+\sqrt{Var[x^{(k)}]},+\beta^{(k)}+=+E[x^{(k)})
),从而保证整个network的capacity。(有关capacity的解释:实际上BN可以看作是在原模型上加入的“新操作”,这个新操作很大可能会改变某层原来的输入。当然也可能不改变,不改变的时候就是“还原原来输入”。如此一来,既可以改变同时也可以保持原输入,那么模型的容纳能力(capacity)就提升了。)
下面我们用pytorch实现以下完整的BN:
import torch class BatchNorm2d(torch.nn.Module): def __init__(self, channel, eps=1e-5, affine=True, momentum=0.9): super().__init__() # 初始化训练参数 self.gamma = torch.nn.Parameter(torch.ones(1, channel, 1, 1)) self.beta = torch.nn.Parameter(torch.zeros(1, channel, 1, 1)) self.eps = eps self.affine = affine self.momentum = momentum self.register_buffer('running_mean', torch.zeros(channel)) self.register_buffer('running_var', torch.ones(channel)) def forward(self, input): # input shape must be (n, c, h, w) if self.training: means = input.mean((0, 2, 3), keepdim=True) vars = ((input-means)**2).sum((0, 2, 3), keepdim=True) self.running_mean = self.momentum * self.running_mean + (1-self.momentum) * means self.running_var = self.momentum * self.running_var + (1-self.momentum) * vars else: means = self.running_mean vars = self.running_var output = (input - means) / torch.sqrt(vars + self.eps) if self.affine: output = output* self.gamma + self.beta return output
实验验证BN效果
BN应用在深度神经网络中最主要的作用是加速神经网络训练,并使模型训练更加稳定(可以适应大的学习率,对参数初始化不敏感),避免了人工适应调整网络超参数。我们用pytorch实现一个简单的cnn网络来验证BN的效果。
import torch class LeNet(torch.nn.Module): def __init__(self, bn=False): super().__init__() if bn: self.conv = torch.nn.Sequential( torch.nn.Conv2d(1, 20, kernel_size=5), torch.nn.MaxPool2d(2), torch.nn.BatchNorm2d(20), torch.nn.ReLU(), torch.nn.Conv2d(20, 50, kernel_size=5), torch.nn.MaxPool2d(2), torch.nn.BatchNorm2d(50), torch.nn.ReLU() ) self.dense = torch.nn.Sequential( torch.nn.Linear(800, 500), torch.nn.BatchNorm1d(500), torch.nn.ReLU(), torch.nn.Linear(500, 10) ) else: self.conv = torch.nn.Sequential( torch.nn.Conv2d(1, 20, kernel_size=5), torch.nn.MaxPool2d(2), torch.nn.ReLU(), torch.nn.Conv2d(20, 50, kernel_size=5), torch.nn.MaxPool2d(2), torch.nn.ReLU() ) self.dense = torch.nn.Sequential( torch.nn.Linear(800, 500), torch.nn.ReLU(), torch.nn.Linear(500, 10) ) def forward(self, x): x = self.conv(x) x = torch.flatten(x, start_dim=1) x = self.dense(x) return x
在Mnist数据集进行训练,设置三个参数bn(是否使用bn),lr(learning rate),init_scale(参数初始化权重)。测试网络在各种情况下的表现。
import torchimport osimport torchvision.transforms as transformsfrom torchvision.datasets import mnistfrom tensorboardX import SummaryWriterfrom tqdm import tqdm# 设置训练超参数batch_size = 64epochs = 10num_class = 10eval_every = 100device = "cuda:0" bn = Truelr = 0.01init_scale = 1 data_transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize([0.485], [0.229])]) mnist_train = mnist.MNIST("~/.torch/dataset/mnist", download=True, train=True, transform=data_transform)mnist_test = mnist.MNIST("~/.torch/dataset/mnist", download=True, train=False, transform=data_transform)train_data_loader = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, ) test_data_loader = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False)net = LeNet(bn=bn) def init_weights(m): if isinstance(m, torch.nn.Linear): torch.nn.init.xavier_uniform_(m.weight.data, gain=init_scale) if isinstance(m, torch.nn.Conv2d): torch.nn.init.xavier_uniform_(m.weight.data, gain=init_scale) net.apply(init_weights)net.to(device) loss_func = torch.nn.CrossEntropyLoss()optimizer = torch.optim.SGD(net.parameters(), lr=lr) def evaluate(net, data_loader): # Evaluate net.eval() num_example = 0 right_example = 0 for x, label in tqdm(data_loader): x, label = x.to(device), label.to(device) output = net(x) pred_y = torch.argmax(output, 1) right_example += torch.sum(label == pred_y) num_example += label.shape[0] return right_example.float() / num_example writer = SummaryWriter("batch_normalization_experiment") n_iter = 0for e in range(epochs): # Train print("Start training epoch %d." % e) num_example = 0 right_example = 0 for x, label in train_data_loader: x, label = x.to(device), label.to(device) optimizer.zero_grad() net.train() output = net(x) pred_y = torch.argmax(output, 1) loss = loss_func(output, label) loss.backward() optimizer.step() if n_iter % eval_every == 0: # train_acc = evaluate(net, train_data_loader) test_acc = evaluate(net, test_data_loader) writer.add_scalars('data/lr-%f-scale-%f' % (lr, init_scale), {'with_bn' if bn else "without_bn": test_acc}, n_iter) n_iter += 1 print("Train epoch %d done. " % (e))
小学习率,小初始化权重
lr = 0.01, init_scale=1.0 (注:红线代表使用了BN,蓝线代表未使用bn。横轴为step,纵轴为accuracy。所有结果为在验证集上的结果,下同)
可以看到使用BN收敛速度快于未使用bn,而且在验证集的准确率波动比较小,最终在验证集上的准确率也有提升。
#### 大学习率,小初始化权重
lr=1.0, init_scale=1.0
可以看到如果学习率过大,不适用BN的模型已经不收敛了, 而使用bn后验证集上任然达到了0.992的准确率。
小学习率,大初始化权重
lr=0.1, init_scale=1.0
可以看到在大的参数初始化权重下,小的学习率让模型收敛速度变慢(BN),而不适用BN的情况下模型毫无收敛的趋势。
大学习率,大初始化权重
总的来说,BN对各种极端的超参数都有很强的适应能力。在训练的过程中使用BN我们完全可以使用较大的学习率加快收敛速度,而且不会影响模型最终的效果。BN通过将每一层网络的输入进行normalization,保证输入分布的均值与方差固定在一定范围内,并在一定程度上缓解了梯度消失,加速了模型收敛;并且BN使得网络对参数、激活函数更加具有鲁棒性,降低了神经网络模型训练和调参的复杂度;最后BN训练过程中由于使用mini-batch的mean/variance作为总体样本统计量估计,引入了随机噪声,在一定程度上对模型起到了正则化的效果。
为什么BN的效果这么好
原论文(Batch Normalization:Accelerating Deep Network Training by Reducing Internal Covariate Shift)中,BN的提出是为了解决Internal Covariate Shift (ICF),即在深层网络训练的过程中,由于网络中参数变化而引起内部结点数据分布发生变化的这一过程被称作Internal Covariate Shift。假如我们要训练一个神经网络分类器,我们从训练数据中拿到两个明显分布不同的两个batch。当我们用这两个batch分别去训练神经网络的时候,就会由于训练batch分布的剧烈波动导致收敛速度慢,甚至是神经网络性能的下降。这就是在输入数据上的Covariate shift。通常我们可以通过增大batch的大小,并充分对数据进行shuffle来保证每个batch的分布尽量接近原始数据的分布,从而减少Covariate shift带来的负面影响。
同样的对于神经网络的每一层的输入数据同样存在类似的问题,这就是Internal Covariate Shift。但是在训练过程中,神经网络的参数会不断地更新,会导致神经网络内部的输入输出分布的剧烈波动,我们并不能像处理输入数据那样去处理神经网络内部每一层的输入输出。BN的提出就是为了解决这个问题的。将每一层的输入分布都归一化到均值为0,方差为1上,减少了所谓的 Internal Covariate Shift,从而稳定乃至加速了训练。
但是我们仔细思考,均值和方差相同,数据分布就一定相同或者相近吗,显然是不是的。不管哪一层的输入都不可能严格满足正态分布,从而单纯地将均值方差标准化无法实现标准分布 N (0, 1) ;其次,就算能做到 N (0, 1) ,这种诠释也无法进一步解释其他归一化手段(如 Instance Normalization、Layer Normalization)起作用的原因。
个人感觉。BN 的作用更像是多种因素的复合结果,比如对于我们主流的激活函数来说, [−1,1] 基本上都是非线性较强的区间,所以将输入弄成均值为 0、方差为 1,也能更充分地发挥激活函数的非线性能力,不至于过于浪费神经网络的拟合能力。
再比如我们对神经网络的训练数据,特别是图像数据进行预处理的时候都要进行所谓的“白化”操作,最常见的白化操作便采用PCA对数据进行白化操作,最后达到的效果如图所示:
但是对于神经网络内部来说,做这样的白化操作代价过大,而且PCA操作也不是一个可微分的操作。所以我们用正则化操作来替代白化操作,也可以达到类似的效果,如图所示:
以上只是个人的一些不成熟的见解。How Does Batch Normalization Help Optimization?这篇文章从理论和实验出发详细分析了BN的作用机理,大致是说最终的结论是减去均值那一项,有助于降低神经网络梯度的 Lipschitz约束常数,而除以标准差的那一项,更多的是起到类似自适应学习率的作用,使得每个参数的更新更加同步,而不至于对某一层、某个参数过拟合。 有空深入学习以下与大家分享,这里就不作详细的介绍了。
参考文献
参考论文
[How Does Batch Normalization Help Optimization?
papers.nips.ccImageNet Classification with Deep Convolutional Neural Networkspapers.nips.ccBatch Normalization: Accelerating Deep Network Training b y Reducing Internal Covariate Shiftarxiv.org
参考博客
CS231n Convolutional Neural Networks for Visual Recognitioncs231n.github.io
BN究竟起了什么作用?一个闭门造车的分析mp.weixin.qq.com
Difference between Local Response Normalization and Batch Normalizationtowardsdatascience.com尹相楠:如何区分并记住常见的几种 Normalization 算法zhuanlan.zhihu.com
深度学习中 Batch Normalization为什么效果好?www.zhihu.com
Juliuszh:详解深度学习中的Normalization,BN/LN/WNzhuanlan.zhihu.com
编辑于 2019-10-17
「真诚赞赏,手留余香」
还没有人赞赏,快来当第一个赞赏的人吧!
来源: <https://zhuanlan.zhihu.com/p/87117010>
来自为知笔记(Wiz)
标签:nn,self,torch,Batch,batch,深入,BN,Normalization From: https://www.cnblogs.com/jsxyhelu/p/16955139.html