现在几乎所有的神经网络都在使用批量归一化这个层。
但是沐神讲的不太懂,可以看看对源paper的理解:https://blog.csdn.net/hjimce/article/details/50866313
背景:
在网络上,数据一般都在输入层,也就是在最下面,但是损失函数在最上面,所以上面的层训练的会快一些,而下面的层训练的慢一些。
底部的层一变化,所有都要跟着一起变化,导致上面的层要重新学习很多次,导致收敛变慢。
所谓批量归一化,就是固定小批量里面的均值和方差。
γ和β是引入的课学习的参数
这里就有一个好玩的事情:
原本的BN的paper是想用它来减少内部协变量转移,但是后续有论文指出它只是通过在每个小批量里加入噪音来控制模型复杂度而已,因此没必要跟丢弃法混合使用。
uB是随机的偏移,seigmaB是随机缩放(因为每次每个批量都是随机取样,最终计算的均值和方差都是随机的,所以这就是一些噪音,随机性很大)
总结:
按照以前的方法,如果学习率选择较大,那么上层可能会出现梯度爆炸;如果学习率较小,那么下层可能根本算不动。而BN将每层的输入放在差不多的输入里,在直观上我们就可以用一个比较大的学习率。
结果
如下面两个图,左图为加入BN层的LeNet,右图为之前我跑的LeNet,可以看到加入了BN的网络收敛速度明显变快了,当然训练速度也变慢了整整一倍。
准确率从81%提升到了85%,而loss更是降低了一半,我们能够明显感受到,batch normalization对于收敛速度的提升非常大。
再看看之前我训练了50个epoch的LeNet,可以看到精度比10个epoch的加入BN的LeNet要好,所以可以大致从感受上理解,BN只是加快了函数收敛速度,我们可以用更小的epoch让模型训练到最优,但是对提升准确率没有可观影响。图中10个epoch的LeNet明显还没有收敛到下图的网络。
那不妨咱们用30个epoch计算一下加了BN的LeNet,可以看到精度少许下降,但是训练精度比用50个epoch的LeNet还要高,这时候可以认为模型已经收敛,只不过出现了严重的过拟合。但是过拟合往往是一件好事,剩下的就是解决过拟合问题罢了。
Q&A
1、丢弃法或者权重衰退常常用来解决过拟合,是控制模型复杂度的算法;但是批量归一化(BN)的目的主要是加快函数训练速率,不像前者会改变W,BN只会改变输出的特征数据。
2、BN一般用于很深的网络,浅层的MLP加上BN效果反而可能不好,因为只有网络足够深,底层的一点变动才可能堆叠起巨大的波澜。
3、BN其实就是对数据做了一个线性变换。
4、BN之后,地图更加平坦,所以学习起来收敛更快。
代码
import torch from torch import nn from d2l import torch as d2l def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):# 这一层的输入,γ和β,全局的均值,全局的方差(整个数据集的均值和方差),eps是一个很小值,防止除以0,momentum是更新整个数据值的均值和方差使用的 # 通过is_grad_enabled来判断当前模式是训练模式还是预测模式 if not torch.is_grad_enabled(): # 如果不要算梯度,一般就是在做inference,而不是training # 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差 X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps) # 因为推理用的是全局的mean和var,而训练用的是小批量的mean和var else: assert len(X.shape) in (2, 4) # 2一般是卷积层,4一般是全连接层,这里这个函数只支持这两种网络层,其它层咱不管 if len(X.shape) == 2: # 使用全连接层的情况,计算特征维上的均值和方差 mean = X.mean(dim=0) #按行计算,一行等于一个样本,一列是一个特征 var = ((X - mean) ** 2).mean(dim=0) else: # 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。 # 这里我们需要保持X的形状以便后面可以做广播运算 mean = X.mean(dim=(0, 2, 3), keepdim=True) #1234维度分别是bitch_size,channel,w,h,这里就是按通道求均值,也就是消去其他维度的值,只不过这里保留了维度 var = ((X - mean) ** 2).mean(dim=(0, 2, 3), keepdim=True) # 训练模式下,用当前的均值和方差做标准化 X_hat = (X - mean) / torch.sqrt(var + eps) # 更新移动平均的均值和方差 moving_mean = momentum * moving_mean + (1.0 - momentum) * mean moving_var = momentum * moving_var + (1.0 - momentum) * var Y = gamma * X_hat + beta # 缩放和移位 return Y, moving_mean.data, moving_var.data # 主要将变幻后的Y返回 # 定义BN的层 class BatchNorm(nn.Module): # num_features:完全连接层的输出数量或卷积层的输出通道数。 # num_dims:2表示完全连接层,4表示卷积层 def __init__(self, num_features, num_dims): super().__init__() if num_dims == 2: shape = (1, num_features) else: shape = (1, num_features, 1, 1) # 参与求梯度和迭代的拉伸和偏移参数,分别初始化成1和0 self.gamma = nn.Parameter(torch.ones(shape)) self.beta = nn.Parameter(torch.zeros(shape)) # 非模型参数的变量初始化为0和1 self.moving_mean = torch.zeros(shape) self.moving_var = torch.ones(shape) def forward(self, X): # 如果X不在内存上,将moving_mean和moving_var # 复制到X所在显存上 if self.moving_mean.device != X.device: self.moving_mean = self.moving_mean.to(X.device) self.moving_var = self.moving_var.to(X.device) # 保存更新过的moving_mean和moving_var Y, self.moving_mean, self.moving_var = batch_norm( X, self.gamma, self.beta, self.moving_mean, self.moving_var, eps=1e-5, momentum=0.9) return Y # 使用BN,应用于LeNet net = nn.Sequential( nn.Conv2d(1, 6, kernel_size=5), BatchNorm(6, num_dims=4), nn.Sigmoid(),# 6值得就是通道数,4是因为这是卷积层后用的,注意加在激活函数前面 nn.AvgPool2d(kernel_size=2, stride=2), nn.Conv2d(6, 16, kernel_size=5), BatchNorm(16, num_dims=4), nn.Sigmoid(), nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(), nn.Linear(16*4*4, 120), BatchNorm(120, num_dims=2), nn.Sigmoid(), # 线性层输出120 nn.Linear(120, 84), BatchNorm(84, num_dims=2), nn.Sigmoid(), nn.Linear(84, 10)) # 输出层不用加东西,后面用不到了只剩下Softmax了 lr, num_epochs, batch_size = 1.0, 10, 256 train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu()) # 我们可以看一看BN层的数据(虽然对于人类来说看起来是没啥意义的) net[1].gamma.reshape((-1,)), net[1].beta.reshape((-1,)) # 简易实现 # 调nn的包,可以使用现成的API,只不过少了一个维度参数,pytorch内部帮我们做了。 net = nn.Sequential( nn.Conv2d(1, 6, kernel_size=5), nn.BatchNorm2d(6), nn.Sigmoid(), nn.AvgPool2d(kernel_size=2, stride=2), nn.Conv2d(6, 16, kernel_size=5), nn.BatchNorm2d(16), nn.Sigmoid(), nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(), nn.Linear(256, 120), nn.BatchNorm1d(120), nn.Sigmoid(), nn.Linear(120, 84), nn.BatchNorm1d(84), nn.Sigmoid(), nn.Linear(84, 10)) d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())