批量归一化
在特征放缩中,我们知道了归一化、标准化可以去除特征之间的纲量。这样使得目标函数更圆滑,梯度下降的更快更稳定。
对于浅层网络只是用特征归一化预处理就可以使得整个网络比较稳定。但是对于深层网络而言,随着网络的层数增加靠近输出层的参数就会出现剧烈变动,模型就会不稳定。即便使用了特征归一化预处理,仍然只是对于浅层有用,向前传播到后面的层就会不太有用了!
假设正向传播为上图前四式所示,则最后一层(即w3)的导数为最后式。
层数增加会导致梯度递增或递减(最极端的情况就成了梯度爆炸或消失),然后反向传播回去浅层的导数就会非常大或小,参数也就变的非常大或小。导致临近输出层的层的导数叠加(如上图最后一式所示),使得最靠近输出层的参数变动越剧烈!
所以我们使用批量归一化层,对所有或者多数层的输出进行归一化,使得每一层都比较稳定!
通常批量归一化层放在仿射变换(全连接层、卷积层、序列层等)于激活函数之间!
对于全连接层、卷积层、序列层构建批量归一化层的方法稍有不同
对全连接层进行批量归一化
理论
假设:
- 全连接层的输入为
u
- 权重参数于偏差值分别为
W
、b
- 激活函数为
Φ
- 批量归一化运算符为
BN
则有,批量归一化的输出为:
其中输入到批量归一化层的输入x为:(u是仿射变换的输入,x是批零归一化的输入。因为批量归一化在仿射变换之后所以这么定义)
一个由m个样本组成(u)的小批量,仿射变换的输出为一个新的小批量B={x1,x2…,xm}。他们正式批量归一化的输入。小批量中任一样本x为d维向量,则经过批量归一化层后仍是d维向量(也就是说批量归一化只是对样本的特征进行归一化,并不会改变样本的维度)
对于批量归一化的具体步骤为一下几步。
-
第一步,对小批量求均值于方差 (注意其中的平方计算是按元素求平方 (x1,x2,x3)2= (x12,x22,x32),而不是向量的平方)
-
第二步,对每个特征进行标准化。 (使用按元素开方和按元素除法进行标准化) 这里的
ε
是一个极小值,目的是为了防止分母为0。
-
第三步,批量归一化层引入两个可学习的模型参数:拉伸(scale)参数
γ
和偏移(shift)参数β
,这两个参数形状相同,皆为d维向量。他们分别于x做元素乘法于加法。得到的y仍是一个d维向量。
这里引入两个参数是为了实现一种可能性:如果批量归一化没有效用甚至起反作用,那么学习的模型可以不使用批量归一化。
批量归一化的时候,我们可以将批量大小设置的大一些,这样可以使得均值与方差更准确一些!
预测时使用批量归一化
训练时有小批量所以可以使用其均值与方差,但是预测时只有一个样本并没有均值与方差可用。
一种方法是,通过移动平均(在训练时通过移动批量加和,最后求均值与方差的平均值) 估计整个数据集的样本均值与方差,并在预测时使用它们得到确定的输出。
在更新移动均值与方差的时候,加了一个百分比。新移动均值、方差 = 百分之多少的旧移动均值、方差 + (1 - 百分之多少的)计算出来的均值、方差。这样的好处是:有一个记忆,使得最终的移动均值、方差在全局更接近数据集的均值与方差!
可见批量归一化和丢弃层一样,在训练和预测中的计算结果也是不一样的!
实现
import torch
import numpy as np
from torch import nn
class MyBatchNorm(nn.Module):
def __init__(self, num_feature, num_dims, **kwargs):
super(MyBatchNorm, self).__init__(**kwargs)
# 经过全连接层的一个batch是二维的
if num_dims == 2:
# 每一个特征用一个参数
shape = (1, num_feature)
else:
# 每一个通道用一个参数
shape = (1, num_feature, 1, 1)
# 因为gamma是拉伸值所以不能为0,初始化为1
self.gamma = torch.ones(shape)
self.beta = torch.zeros(shape)
# 初始化的时候移动均值与方差不需要值
self.moving_mean = torch.zeros(shape)
self.moving_var = torch.zeros(shape)
def __batch_norm(self, x, gamma, beta, moving_mean, moving_var, eps, momentum):
# 在测试中直接使用移动均值、方差进行归一化
if not self.training:
x_hat = (x - moving_mean) / torch.sqrt(moving_var + eps)
else:
# 只允许全连接层与CNN的数据进行归一化(其他网络数据并没有实现)而它们的形状是2, 4
assert len(x.shape) in (2, 4)
# 对全连接层进行操作
if len(x.shape) == 2:
# keepdim 是为了保证可以进行广播操作的
mean = x.mean(dim = 0, keepdim = True)
var = ((x - mean) ** 2).mean(dim = 0)
else:
# 0, 2, 3 指的是对一个批量中的同一个通道进行均值。得到对应通道的均值(共有通道数个值)
mean = x.mean(dim = (0, 2, 3), keepdim = True)
var = ((x - mean) ** 2).mean(dim = (0, 2, 3), keepdim = True)
x_hat = (x - mean) / torch.sqrt(var + eps)
moving_mean = momentum * mean + (1 - momentum) * mean
moving_var = momentum * var + (1 - momentum) * var
y = gamma * x_hat + beta
return y, moving_mean, moving_var
def forward(self, x):
y, self.moving_mean, self.moving_var = self.__batch_norm(x, self.gamma, self.beta, self.moving_mean, self.moving_var, 1e-5, 0.9)
return y
测试:
leaner_features = np.arange(30)
# batch_size = 3, feature_dim = 10
leaner_features = torch.tensor(leaner_features).reshape((3, 10)).double()
rnn_features = np.arange(60)
# batch_size = 3, channel = 2, feature = (5, 2)
rnn_features = torch.tensor(rnn_features).reshape((3, 2, 5, 2)).double()
batchNorm = MyBatchNorm(2, 4)
out = batchNorm(rnn_features)
print(out, out.shape)
结果:
tensor([[[[-1.4776, -1.4173],
[-1.3570, -1.2967],
[-1.2364, -1.1761],
[-1.1158, -1.0554],
[-0.9951, -0.9348]],
[[-1.4776, -1.4173],
[-1.3570, -1.2967],
[-1.2364, -1.1761],
[-1.1158, -1.0554],
[-0.9951, -0.9348]]],
[[[-0.2714, -0.2111],
[-0.1508, -0.0905],
[-0.0302, 0.0302],
[ 0.0905, 0.1508],
[ 0.2111, 0.2714]],
[[-0.2714, -0.2111],
[-0.1508, -0.0905],
[-0.0302, 0.0302],
[ 0.0905, 0.1508],
[ 0.2111, 0.2714]]],
[[[ 0.9348, 0.9951],
[ 1.0554, 1.1158],
[ 1.1761, 1.2364],
[ 1.2967, 1.3570],
[ 1.4173, 1.4776]],
[[ 0.9348, 0.9951],
[ 1.0554, 1.1158],
[ 1.1761, 1.2364],
[ 1.2967, 1.3570],
[ 1.4173, 1.4776]]]], dtype=torch.float64) torch.Size([3, 2, 5, 2])
可见进行归一化了,并且输入得到的输出形状没有变!
RNN这些序列型网络也一样,只不过把最后CNN最后两维表示的矩阵换成了一维的词向量。第一维度代表批量大小,第二维度代表时间步数(Transformer层的话就代表一句话中的词数)。
对卷积网络进行批量归一化
对卷积层来说,批量归一化发生在卷积计算之后,激活函数之前。
如果卷积计算输出有多个通道,那么对每个通道分别做批量归一化 (因为批量归一化是对批量进行归一化,应用于每个特征之上,对于多通道来说特征是每个通道中的元素。不同通道之间不属于同一特征!),并且每个通道拥有独立的拉伸与偏移参数。设一个小批量的形状为(m,n,p,q),m为批量大小;n为通道数。那么我们需要对批量中的每个通道(m,p,q)中 m * p * q 个元素同时做批量归一化。对这些元素进行归一化的时候我们相同的均值与方差,即该通道中 m * p * q 个元素的均值与方差
对序列性网络进行批量归一化
与对卷积神经网络批量归一化一样,一个批量的数据形状为(m,n,p),m为批量大小;n为时间步数(即一个样本句子中词数);p为词向量维度。
(可以把n想象成通道数,因为词与词之间是并列行的。词中的每个元素就是特征)
所以我们只需要对批量中每个位置的词(形状为(m,p))中 m * p 个元素同时做批量归