常见的归一化操作有:批量归一化(Batch Normalization)、层归一化(Layer Normalization)、实例归一化(Instance Normalization)、组归一化(Group Normalization)等。
其归一化操作示意图如下:
(下图来自Group Normalization
论文,地址: https://arxiv.org/pdf/1803.08494.pdf)
在CV领域,深度网络中的数据维度一般是[N, C, H, W]格式,N是batch size,H/W是feature的高/宽,C是feature的channel,压缩H/W至一个维度。
其三维的表示如上图,假设单个方格的长度是1,那么其表示的是[6, 6,*, * ]
上图形象的表示了四种norm的工作方式:
BN应该是我们最熟悉的归一化操作了,批归一化的核心思想是:以一个小批量数据样本为单位在对应维度上进行标准化
。
数据归一化方法很简单,就是要让数据具有0均值和单位方差,如下式:
y
=
x
?
E
[
x
]
V
a
r
[
x
]
+
?
y = \frac{x - \mathrm{E}[x]}{\sqrt{\mathrm{Var}[x] + \epsilon}}
y=Var[x]+??x?E[x]?
但是如果简单的这么干,会降低层的表达能力。比如在使用sigmoid激活函数的时候,如果把数据限制到0均值单位方差,那么相当于只使用了激活函数中近似线性的部分,这显然会降低模型表达能力。
为此,作者又为BN增加了2个参数,用来保持模型的表达能力,这样就公式就变为下式的形式:
y
=
x
?
E
[
x
]
V
a
r
[
x
]
+
?
?
γ
+
β
y = \frac{x - \mathrm{E}[x]}{\sqrt{\mathrm{Var}[x] + \epsilon}} * \gamma + \beta
y=Var[x]+??x?E[x]??γ+β
批归一化的计算步骤如下:
在每个通道上
分别计算均值和标准差。
api官方文档: https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm1d.html
torch.nn.BatchNorm1d(
num_features, # 通道
eps=1e-05, # 避免归一化时分母为0
momentum=0.1, # 用来计算running_mean和running_var的一个量
affine=True, # 是否进行缩放平移,即gamma和beta参数是否启用
track_running_stats=True, # 是否统计全局的running_mean和running_var
device=None,
dtype=None
)
我们通过手动实现BatchNorm1d
来验证公式:
import torch
import torch.nn as nn
def bn_nlp():
batch_size = 2
seq_len = 3
embedding_dim = 4
input_x = torch.randn(batch_size, seq_len, embedding_dim) # N L C
print('原始的输入:\n', input_x)
# 1、调用官网api
# 设置affine=False,不启用gamma和beta参数
bn_op = nn.BatchNorm1d(num_features=embedding_dim, affine=False)
# 输入要求是 N C L,需要变换维度
# N is the batch size,
# C is the number of features or channels
# L is the sequence length
bn_y = bn_op(input_x.transpose(-1, -2)).transpose(-1, -2)
print('官方api的bn结果:\n', bn_y)
# 2、手动实现bn
# 在【每个通道】上分别计算均值和标准差
# 这里一共4(embedding_dim)个通道
# 即在batch维度和seq_len维度,求均值和标准差(即上图蓝色部分)
bn_mean = input_x.mean(dim=(0, 1), keepdim=True)
# unbiased=False 使用有偏估计来计算标准差
bn_std = input_x.std(dim=(0, 1), unbiased=False, keepdim=True)
print('均值:\n', bn_mean)
print('标准差:\n', bn_std)
eps = 1e-5
# note: 官方文档是将eps放入方差之中再开根号,不过这里对值影响不大
verify_bn_y = (input_x - bn_mean) / (bn_std + eps) # bn_mean和bn_std会触发广播机制
print('自己实现的bn结果:\n', verify_bn_y)
if __name__ == '__main__':
bn_nlp()
# 可以看到官方api的bn结果和自己计算的bn结果一致
原始的输入:
tensor([[[-1.9182, -0.8153, -0.2014, -0.0894],
[ 0.6366, -1.1906, -1.2189, -0.2368],
[ 2.1686, -0.3856, -0.1906, 0.9672]],
[[ 0.5857, -0.7613, -0.0867, -0.6334],
[ 0.1875, -1.3680, 0.2689, 0.5938],
[-0.8454, 1.4016, 0.7525, -0.8184]]])
官方api的bn结果:
tensor([[[-1.6096, -0.3228, -0.1488, -0.0839],
[ 0.3925, -0.7329, -1.8555, -0.3162],
[ 1.5930, 0.1468, -0.1306, 1.5814]],
[[ 0.3525, -0.2638, 0.0436, -0.9413],
[ 0.0405, -0.9268, 0.6400, 0.9928],
[-0.7689, 2.0996, 1.4513, -1.2329]]])
均值:
# (-1.9182+0.6366+2.1686+0.5857+0.1875-0.8454)/6 = 0.1358
tensor([[[ 0.1358, -0.5199, -0.1127, -0.0362]]])
标准差:
tensor([[[1.2761, 0.9151, 0.5961, 0.6345]]])
自己实现的bn结果:
tensor([[[-1.6096, -0.3228, -0.1488, -0.0839],
[ 0.3925, -0.7329, -1.8555, -0.3162],
[ 1.5930, 0.1468, -0.1306, 1.5814]],
[[ 0.3525, -0.2638, 0.0436, -0.9413],
[ 0.0405, -0.9268, 0.6400, 0.9928],
[-0.7689, 2.0996, 1.4513, -1.2329]]])
BN的适用性
不能使用BN的场景
在使用小batch size的时候不稳定
对于在线学习不好
对于循环神经网络不好,RNN不适合用BN的原因:Normalize的对象(position)来自不同分布。
改善流经网络的梯度
允许更大的学习率,大幅提高训练速度。现在我们可以采用初始很大的学习率,然后学习率的衰减速度也很大,因为这个算法收敛很快。当然这个算法即使你选择了较小的学习率,也比以前的收敛速度快,因为它具有快速训练收敛的特性;
减少对初始化的强烈依赖
改善正则化策略。作为正则化的一种形式,轻微减少了对dropout的需求。你再也不用去理会过拟合中drop out、L2正则项参数的选择问题,采用BN算法后,你可以移除这两项参数,或者可以选择更小的L2正则约束参数了,因为BN具有提高网络泛化能力的特性;
再也不需要使用使用局部响应归一化层了(局部响应归一化是Alexnet网络用到的方法),因为BN本身就是一个归一化网络层;
可以把训练数据彻底打乱(防止每批训练的时候,某一个样本都经常被挑选到,文献说这个可以提高1%的精度)。
在训练时,是对每一批的训练数据进行归一化,也即用每一批数据的均值和方差。
在推理时,比如进行一个样本的预测,就并没有batch的概念,因此用的是全量训练数据的均值和方差,可以通过移动平均法求得。
层归一化的公式,和批归一化相同:
y
=
x
?
E
[
x
]
V
a
r
[
x
]
+
?
?
γ
+
β
y = \frac{x - \mathrm{E}[x]}{\sqrt{\mathrm{Var}[x] + \epsilon}} * \gamma + \beta
y=Var[x]+??x?E[x]??γ+β
层规范化就是针对 BN 的不足而提出的。
LN 针对单个训练样本进行,不依赖于其他数据,因此可以避免 BN 中受 mini-batch 数据分布影响的问题
此外,LN 不需要保存 mini-batch 的均值和方差,节省了额外的存储空间。
需要注意的是:
BatchNorm就是通过对batch size这个维度归一化来让分布稳定下来。
LayerNorm则是通过对Hidden size这个维度归一化来让某层的分布稳定。
在BN和LN都能使用的场景中,BN的效果一般优于LN,原因是基于不同数据,同一特征得到的归一化特征更不容易损失信息。
但是有些场景是不能使用BN的,例如: batchsize较小或者在RNN中,这时候可以选择使用LN,LN得到的模型更稳定且起到正则化的作用。LN能应用到小批量和RNN中是因为LN的归一化统计量的计算是和batchsize没有关系的。
为何CV数据任务上很少用LN,用BN的比较多,而NLP上应用LN是比较多?
第一种解释如下:
我们用文本数据句话来说明BN和LN的操作区别。
我是中国人我爱中国
武汉抗疫非常成功0
大家好才是真的好0
人工智能很火000
上面的4条文本数据组成了一个batch的数据,那么BN的操作的时候,就会把4条文本相同位置的字来做归一化处理,例如:我、武、大、人(每个embedding第i个位置一起进行归一化),这里就破坏了一个字内在语义的联系。
而LN则是针对每一句话的每个token embedding做归一化处理。从这个角度看,LN就比较适合NLP任务,也就是bert和Transformer用的比较多。
第一个解释从反面证明BN不适合作归一化,它是对batch个词的某个embedding位置进行归一化,不合理。
第二个解释如下:
batch normalization不具备的两个功能:
1、layer normalization 有助于得到一个球体空间中符合均值为0、方差为1高斯分布的 embedding。NLP数据则是由embedding开始的,这个embedding并不是客观存在的,它是由我们设计的网络学习出来的。通过layer normalization得到的embedding是以坐标原点为中心,1为标准差,越往外越稀疏的球体空间中,这个正是我们理想的数据分布
。
2、layer normalization可以对transformer学习过程中由于多词条embedding累加可能带来的“尺度”问题施加约束,相当于对表达每个词一词多义的空间施加了约束,有效降低模型方差。简单来说,每个词有一片相对独立的小空间,通过在这个小空间中产生一个小的偏移来达到表示一词多义的效果。transformer每一层都做了这件事,也就是在不断调整每个词在空间中的位置,这个调整就可以由layer normalization 来实现,batch normalization是做不到的。
每个样本内的每个层级
,计算该层级上的均值和标准差。
api官方文档:https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html
torch.nn.LayerNorm(
normalized_shape,
eps=1e-05,
elementwise_affine=True,
bias=True,
device=None,
dtype=None
)
我们通过手动实现LayerNorm
来验证公式:
def ln_nlp():
batch_size = 2
seq_len = 3
embedding_dim = 4
input_x = torch.randn(batch_size, seq_len, embedding_dim) # N L C
print('原始的输入:\n', input_x)
# 1、调用官网api
# 设置elementwise_affine=False,不启用gamma和beta参数
ln_op = nn.LayerNorm(normalized_shape=embedding_dim, elementwise_affine=False)
# 输入要求是 [N, *],不需要变换维度
ln_y = ln_op(input_x)
print('官方api的ln结果:\n', ln_y)
# 2、手动实现layer norm
# 对于【每个样本内的每个层级】,计算该层级上的均值和标准差
# 这里有2(batch_size)个样本,每个样本有3(seq_len)个层级
ln_mean = input_x.mean(dim=-1, keepdim=True)
# unbiased=False 使用有偏估计来计算标准差
ln_std = input_x.std(dim=-1, unbiased=False, keepdim=True)
print('均值:\n', ln_mean)
print('标准差:\n', ln_std)
eps = 1e-5
# note: 官方文档是将eps放入方差之中再开根号,不过这里对值影响不大
verify_ln_y = (input_x - ln_mean) / (ln_std + eps) # 触发广播机制
print('自己实现的ln结果:\n', verify_ln_y)
if __name__ == '__main__':
ln_nlp()
# 可以看到官方api的ln结果和自己计算的ln结果一致
原始的输入:
tensor([[[-0.9624, 1.2447, 0.6740, 0.2548],
[-0.4195, 1.3283, -2.7728, 0.8382],
[ 0.8185, -0.5858, 0.0787, 0.6890]],
[[-0.8232, -2.5022, -0.7234, 0.3765],
[ 1.2651, -0.9825, -0.3684, -0.1102],
[ 0.0357, 1.5741, 1.1220, -0.5346]]])
官方api的ln结果:
tensor([[[-1.5608, 1.1621, 0.4580, -0.0592],
[-0.1028, 0.9989, -1.5861, 0.6900],
[ 1.0193, -1.4990, -0.3074, 0.7871]],
[[ 0.0922, -1.5400, 0.1892, 1.2586],
[ 1.5983, -1.1353, -0.3885, -0.0744],
[-0.6120, 1.2212, 0.6825, -1.2916]]])
均值:
tensor([[[ 0.3028], # (-0.9624+1.2447+0.6740+0.2548)/4 = 0.3028
[-0.2565],
[ 0.2501]],
[[-0.9180],
[-0.0490],
[ 0.5493]]])
标准差:
tensor([[[0.8106],
[1.5865],
[0.5576]],
[[1.0286],
[0.8222],
[0.8392]]])
自己实现的ln结果:
tensor([[[-1.5608, 1.1621, 0.4580, -0.0592],
[-0.1028, 0.9989, -1.5861, 0.6900],
[ 1.0193, -1.4990, -0.3074, 0.7871]],
[[ 0.0922, -1.5400, 0.1892, 1.2585],
[ 1.5983, -1.1353, -0.3885, -0.0744],
[-0.6120, 1.2212, 0.6825, -1.2916]]])
图像生成、风格迁移
等需要保留每个样本独特性的任务,因为它不会引入批次间的相关性,更适合处理单个样本或小批量的情况。api官方文档:https://pytorch.org/docs/stable/generated/torch.nn.InstanceNorm1d.html
torch.nn.InstanceNorm1d(
num_features,
eps=1e-05,
momentum=0.1,
affine=False,
track_running_stats=False,
device=None,
dtype=None
)
我们通过手动实现InstanceNorm1d
来验证公式:
def in_nlp():
batch_size = 2
seq_len = 3
embedding_dim = 4
input_x = torch.randn(batch_size, seq_len, embedding_dim) # N L C
print('原始的输入:\n', input_x)
# 1、调用官网api
# 设置affine=False,不启用gamma和beta参数
in_op = nn.InstanceNorm1d(num_features=embedding_dim, affine=False)
# 输入要求是 [N, C, L],需要变换维度
in_y = in_op(input_x.transpose(-1, -2)).transpose(-1, -2)
print('官方api的in结果:\n', in_y)
# 2、手动实现instant norm
# 对于每个输入样本,在每个通道上分别计算均值和标准差
# 这里有2(batch_size)个样本,有4(embedding_dim)个通道
in_mean = input_x.mean(dim=1, keepdim=True)
# unbiased=False 使用有偏估计来计算标准差
in_std = input_x.std(dim=1, unbiased=False, keepdim=True)
print('均值:\n', in_mean)
print('标准差:\n', in_std)
eps = 1e-5
# note: 官方文档是将eps放入方差之中再开根号,不过这里对值影响不大
verify_in_y = (input_x - in_mean) / (in_std + eps) # 触发广播机制
print('自己实现in结果:\n', verify_in_y)
if __name__ == '__main__':
in_nlp()
原始的输入:
tensor([[[ 1.4341, -0.4215, 1.1963, -0.6798],
[-0.4178, -0.3566, 0.6031, -0.9045],
[ 0.2921, -1.4179, 0.8111, -1.7165]],
[[-0.8753, 1.8243, 1.7770, -0.6461],
[ 0.6337, 1.9972, 0.1212, -1.1680],
[ 0.2313, 0.4167, -1.0360, -0.3761]]])
官方api的in结果:
tensor([[[ 1.3082, 0.6393, 1.3270, 0.9443],
[-1.1194, 0.7728, -1.0866, 0.4396],
[-0.1888, -1.4121, -0.2404, -1.3838]],
[[-1.3665, 0.5815, 1.2904, 0.2554],
[ 0.9986, 0.8257, -0.1440, -1.3323],
[ 0.3680, -1.4072, -1.1464, 1.0768]]])
均值:
# (1.4341-0.4178+0.2921)/3 = 0.4361
tensor([[[ 0.4361, -0.7320, 0.8701, -1.1003]],
[[-0.0034, 1.4127, 0.2874, -0.7301]]])
标准差:
tensor([[[0.7629, 0.4857, 0.2457, 0.4453]],
[[0.6380, 0.7078, 1.1544, 0.3287]]])
自己实现in结果:
tensor([[[ 1.3081, 0.6393, 1.3271, 0.9443],
[-1.1194, 0.7728, -1.0867, 0.4396],
[-0.1888, -1.4121, -0.2404, -1.3838]],
[[-1.3665, 0.5815, 1.2904, 0.2554],
[ 0.9986, 0.8257, -0.1440, -1.3323],
[ 0.3680, -1.4071, -1.1464, 1.0768]]])
我们已经知道对于BN来说,过小的batch size会导致其性能下降,一般来说每GPU上batch设为32最合适
但是对于一些其他深度学习任务batch size往往只有1-2,比如目标检测,图像分割,视频分类上,输入的图像数据很大,较大的batchsize显存吃不消。
另外,BN是在batch这个维度上Normalization,但是这个维度并不是固定不变的,比如训练和测试时一般不一样,一般都是训练的时候在训练集上通过滑动平均预先计算好平均-mean,和方差-variance参数,在测试的时候,不在计算这些值,而是直接调用这些预计算好的来用,但是,当训练数据和测试数据分布有差别是时,训练机上预计算好的数据并不能代表测试数据。
既然明确了问题,解决起来就简单了,归一化的时候避开batch这个维度是不是可行呢,于是就出现了layer normalization和instance normalization等工作,但是仍比不上GN。
GN的极端情况就是LN和IN,分别对应G等于1和G等于C,作者在论文中给出G设为32较好。
api的官方文档:https://pytorch.org/docs/stable/generated/torch.nn.GroupNorm.html
torch.nn.GroupNorm(
num_groups, # 分组的组数
num_channels, # channel的个数
eps=1e-05,
affine=True,
device=None,
dtype=None
)
我们通过手动实现GroupNorm
来验证公式:
def group_nlp():
batch_size = 2
seq_len = 3
embedding_dim = 4
input_x = torch.randn(batch_size, seq_len, embedding_dim) # N L C
print('原始的输入:\n', input_x)
# 1、调用官网api
# 设置affine=False,不启用gamma和beta参数
# 设置分为2组
group_op = nn.GroupNorm(num_groups=2, num_channels=embedding_dim, affine=False)
# 输入要求是 [N, C, *],需要变换维度
group_y = group_op(input_x.transpose(-1, -2)).transpose(-1, -2)
print('官方api的group结果:\n', group_y)
# 2、手动实现group norm
# 将输入按照通道切分为2组
g_input_xs = torch.split(input_x, split_size_or_sections=embedding_dim // 2, dim=-1)
# 循环2组,进行归一化
results = []
for index, g_input_x in enumerate(g_input_xs):
# 对于每个输入样本,在每个组上分别计算均值和标准差
group_mean = g_input_x.mean(dim=(1, 2), keepdim=True)
# unbiased=False 使用有偏估计来计算标准差
group_std = g_input_x.std(dim=(1, 2), unbiased=False, keepdim=True)
print(f'第{index + 1}组均值:\n', group_mean)
print(f'第{index + 1}组标准差:\n', group_std)
eps = 1e-5
# note: 官方文档是将eps放入方差之中再开根号,不过这里对值影响不大
g_result = (g_input_x - group_mean) / (group_std + eps)
results.append(g_result)
# 再次拼接
verify_gn_y = torch.cat(results, dim=-1)
print('自己实现group结果:\n', verify_gn_y)
if __name__ == '__main__':
group_nlp()
原始的输入:
tensor([[[ 0.6412, -0.9580, 0.1505, -0.9598],
[-0.2981, -1.5032, 0.3579, -0.8543],
[ 0.0351, -0.0369, -1.4433, 1.0080]],
[[-0.2616, 0.2139, -0.8719, 3.2135],
[-1.0790, 0.0833, 0.8177, -0.0801],
[ 1.2287, -2.2719, 0.6443, -0.3537]]])
官方api的group结果:
tensor([[[ 1.4229, -0.8651, 0.5148, -0.7823],
[ 0.0790, -1.6452, 0.7571, -0.6591],
[ 0.5557, 0.4527, -1.3472, 1.5167]],
[[ 0.0785, 0.5116, -1.0883, 2.0133],
[-0.6661, 0.3926, 0.1944, -0.4872],
[ 1.4360, -1.7527, 0.0627, -0.6949]]])
第1组均值:
tensor([[[-0.3533]], # (0.6412-0.2981+0.0351-0.9580-1.5032-0.0369)/6=-0.3533
[[-0.3477]]])
第1组标准差:
tensor([[[0.6989]],
[[1.0978]]])
第2组均值:
tensor([[[-0.2902]],# (0.1505+0.3579-1.4433-0.9598-0.8543+1.0080)/6=-0.2902
[[ 0.5616]]])
第2组标准差:
tensor([[[0.8559]],
[[1.3172]]])
自己实现group结果:
tensor([[[ 1.4229, -0.8651, 0.5148, -0.7823],
[ 0.0790, -1.6452, 0.7571, -0.6591],
[ 0.5557, 0.4527, -1.3472, 1.5167]],
[[ 0.0785, 0.5116, -1.0883, 2.0133],
[-0.6661, 0.3926, 0.1944, -0.4872],
[ 1.4360, -1.7527, 0.0627, -0.6949]]])