【本文已在同名 微信公众号 / 知乎 / 个人博客linsight.cn 上线】
之前在《transformer中normalization的二三事》从思路上梳理了关于常用的normalization的内容。发出之后收到了一些反馈,关于这些norm在实际使用中是怎么实现的,有一些疑问。
因此本篇从实现的角度,来看下这些norm在不同的场景下,到底做了什么。
代码已上传至https://github.com/Saicat/normalization_exp
二维数据
先看下二维数据的情况下normalization是怎么做的。二维数据一般可以对应到神经网络中的全连接层,比如CNN中分类网络最后几个特征层。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | import torchfrom torch import nn
 
 
 eps = 1e-8
 
 
 batch_size = 3
 feature_num = 4
 torch.manual_seed(0)
 inputs = torch.randn(batch_size, feature_num)
 print('二维输入:\n', inputs)
 
 | 
这里定义了一个3×4的矩阵,相当于batch
size=3,特征向量维度为4。得到的随机二维输入是
| 12
 3
 4
 
 | 二维输入:tensor([[ 1.5410, -0.2934, -2.1788,  0.5684],
 [-1.0845, -1.3986,  0.4033,  0.8380],
 [-0.7193, -0.4033, -0.5966,  0.1820]])
 
 | 
batchnorm
用pytorch自带的BatchNorm1d对二维输入进行操作
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | torch_bn = nn.BatchNorm1d(num_features=feature_num, affine=True)
 
 
 
 torch.manual_seed(1)
 torch_bn.weight = nn.Parameter(torch_bn.weight * torch.randn(feature_num))
 torch_bn.bias = nn.Parameter(torch_bn.bias + torch.randn(feature_num))
 print('weight:\n', torch_bn.weight)
 print('bias:\n', torch_bn.bias, '\n')
 
 
 torch_normed = torch_bn(inputs)
 print('torch bn结果:\n', torch_normed)
 
 | 
注意完整的batchnorm/layernorm等,是包括①归一化和②仿射变换(缩放+平移,也就是有可训练参数这部分)两步的。在BatchNorm接口中通过参数"affine"来决定是否进行放射变换。如果"affine"为False,相当于只是在某个维度上对数据进行了归一化处理。
而且pytorch中各种norm的接口初始化都把缩放系数初始化为1.0,平移系数初始化为0,相当于没有进行变换。为了把仿射变换的影响也一起对比,这里手动给缩放和平移系数都添加了一个随机数,变成如下数值
| 12
 3
 4
 5
 6
 
 | weight:Parameter containing:
 tensor([0.6614, 0.2669, 0.0617, 0.6213], requires_grad=True)
 bias:
 Parameter containing:
 tensor([-0.4519, -0.1661, -1.5228,  0.3817], requires_grad=True)
 
 | 
这里缩放系数weight和平移系数bias的维度都是4,对应特征向量的维度。
输入矩阵用官方接口batchnorm之后得到的结果如下
| 12
 3
 4
 5
 
 | torch bn结果:tensor([[ 0.4756,  0.0513, -1.6033,  0.4715],
 [-1.0197, -0.5421, -1.4535,  1.0937],
 [-0.8117, -0.0077, -1.5115, -0.4202]],
 grad_fn=<NativeBatchNormBackward0>)
 
 | 
接下来手动实现batchnorm
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | 
 
 mean = torch.mean(inputs, dim=0, keepdim=True)
 print('均值:\n', mean)
 std = torch.std(inputs, dim=0, keepdim=True, unbiased=False)
 print('标准差:\n', std, '\n')
 
 manual_normed = (inputs - mean) / (std + eps) * torch_bn.weight + torch_bn.bias
 print('手动bn结果:\n', manual_normed)
 
 
 isclose = torch.isclose(torch_normed, manual_normed, rtol=1e-4, atol=1e-4)
 print(isclose)
 
 | 
在dim=0这个维度上计算均值和标准差,即对整个batch内所有sample的同一个feature,进行操作,获得结果如下
| 12
 3
 4
 
 | 均值:tensor([[-0.0876, -0.6985, -0.7907,  0.5295]])
 标准差:
 tensor([[1.1612, 0.4971, 1.0630, 0.2692]])
 
 | 
均值和标准差的维度也是和特征向量的维度一致。这里计算mean和std的时候keepdim设置为True和False都可以,最后都会自动broadcast。
一个要注意的点是,计算std的时候unbiased要设置为False,表明这里是对标准差的有偏估计,否则算出来的结果和torch的batchnorm接口不一致。
用手动计算出来的均值和标准差对输入进行归一化,再进行放射变换,得到手动计算的batchnorm结果如下
| 12
 3
 4
 
 | 手动bn结果:tensor([[ 0.4756,  0.0514, -1.6033,  0.4715],
 [-1.0197, -0.5421, -1.4535,  1.0937],
 [-0.8117, -0.0077, -1.5115, -0.4202]], grad_fn=<AddBackward0>)
 
 | 
这里用torch.isclose接口验证官方batchnorm和手动计算的batchnorm是否相同
| 12
 3
 
 | tensor([[True, True, True, True],[True, True, True, True],
 [True, True, True, True]])
 
 | 
为什么没有用equal,因为发现两个结果会有一点点误差,相对误差大概在1e-5~1e-4之间,应该是因为使用的eps不同导致。
layernorm
看下layernorm对于二维数据的操作,还是用同样的3×4的输入
使用torch官方接口
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | torch_ln = nn.LayerNorm(normalized_shape=feature_num, elementwise_affine=True)
 
 
 
 torch.manual_seed(2)
 torch_ln.weight = nn.Parameter(torch_ln.weight * torch.randn(feature_num))
 torch_ln.bias = nn.Parameter(torch_ln.bias + torch.randn(feature_num))
 print('weight:\n', torch_ln.weight)
 print('bias:\n', torch_ln.bias, '\n')
 
 
 torch_normed = torch_ln(inputs)
 print('torch ln结果:\n', torch_normed)
 
 | 
得到layernorm仿射变换的系数如下
| 12
 3
 4
 5
 6
 
 | weight:Parameter containing:
 tensor([ 0.3923, -0.2236, -0.3195, -1.2050], requires_grad=True)
 bias:
 Parameter containing:
 tensor([ 1.0445, -0.6332,  0.5731,  0.5409], requires_grad=True)
 
 | 
维度依然是和特征向量的维度一致。
官方layernorm的结果是这样的
| 12
 3
 4
 5
 
 | torch ln结果:tensor([[ 1.5120, -0.6001,  1.0604, -0.0392],
 [ 0.7249, -0.3772,  0.3331, -0.9155],
 [ 0.6645, -0.6209,  0.7693, -1.4324]],
 grad_fn=<NativeLayerNormBackward0>)
 
 | 
接下来手动实现一下,和官方结果作对比。
在dim=1计算均值和向量
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | 
 
 mean = torch.mean(inputs, dim=1, keepdim=True)
 print('均值:\n', mean)
 std = torch.std(inputs, dim=1, keepdim=True, unbiased=False)
 print('标准差:\n', std, '\n')
 
 manual_normed = (inputs - mean) / (std + eps) * torch_ln.weight + torch_ln.bias
 print('手动ln结果:\n', manual_normed)
 
 
 isclose = torch.isclose(torch_normed, manual_normed, rtol=1e-4, atol=1e-4)
 print(isclose)
 
 | 
得到的均值和标准差是这样的
| 12
 3
 4
 5
 6
 7
 8
 
 | 均值:tensor([[-0.0907],
 [-0.3104],
 [-0.3843]])
 标准差:
 tensor([[1.3691],
 [0.9502],
 [0.3458]])
 
 | 
对输入进行归一化和仿射变换,结果如下,和官方接口结果一致
| 12
 3
 4
 5
 6
 7
 8
 
 | 手动ln结果:tensor([[ 1.5120, -0.6001,  1.0604, -0.0392],
 [ 0.7249, -0.3772,  0.3331, -0.9155],
 [ 0.6645, -0.6209,  0.7693, -1.4325]], grad_fn=<AddBackward0>)
 验证结果:
 tensor([[True, True, True, True],
 [True, True, True, True],
 [True, True, True, True]])
 
 | 
对比
对于二维输入,batchnorm和layernorm在做第①步归一化的时候,方向如下图
 
batchnorm在dim=0,即batch方向操作;而layernorm在dim=1,即特征向量内部进行操作。
但是无论是batchnorm还是layernorm,在做仿射变换的时候,使用的系数形状都和输入的特征向量相同,可以认为在放射变化这一步上,二者的操作是一样。
CV数据
再看下CV场景下的情况。
CV数据形状一般为[N,C,H,W],N为batch
size,C为channel即特征数,H和W分别是feature
map的高和宽。先定义一个CV输入数据
| 12
 3
 4
 5
 6
 7
 8
 
 | batch_size = 2
 channel = 2
 height = 2
 width = 3
 torch.manual_seed(3)
 inputs = torch.randn(batch_size, channel, height, width)
 print('四维输入:\n', inputs)
 
 | 
输入如下
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | 四维输入:tensor([[[[-0.0766,  0.3599, -0.7820],
 [ 0.0715,  0.6648, -0.2868]],
 
 [[ 1.6206, -1.5967,  0.4046],
 [ 0.6113,  0.7604, -0.0336]]],
 
 
 [[[-0.3448,  0.4937, -0.0776],
 [-1.8054,  0.4851,  0.2052]],
 
 [[ 0.3384,  1.3528,  0.3736],
 [ 0.0134,  0.7737, -0.1092]]]])
 
 | 
batchnorm
图像数据需要用BatchNorm2d,设置的特征数为channel
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | torch_bn = nn.BatchNorm2d(num_features=channel, affine=True)
 
 
 
 torch.manual_seed(4)
 torch_bn.weight = nn.Parameter(torch_bn.weight * torch.randn(channel))
 torch_bn.bias = nn.Parameter(torch_bn.bias + torch.randn(channel))
 print('weight:\n', torch_bn.weight)
 print('bias:\n', torch_bn.bias, '\n')
 
 
 torch_normed = torch_bn(inputs)
 print('torch bn结果:\n', torch_normed)
 
 | 
仿射变换的参数如下,形状和channel数是一致的,和二维数据的情况一样。这里同样手动给缩放和平移系数加了个随机数
| 12
 3
 4
 5
 6
 
 | weight:Parameter containing:
 tensor([-1.6053,  0.2325], requires_grad=True)
 bias:
 Parameter containing:
 tensor([2.2399, 0.8473], requires_grad=True)
 
 | 
用torch官方batchnorm2d得到的结果是
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | torch bn结果:tensor([[[[2.2043, 1.1275, 3.9442],
 [1.8388, 0.3753, 2.7226]],
 
 [[1.2185, 0.2591, 0.8559],
 [0.9175, 0.9620, 0.7252]]],
 
 
 [[[2.8658, 0.7975, 2.2066],
 [6.4684, 0.8186, 1.5090]],
 
 [[0.8362, 1.1387, 0.8467],
 [0.7392, 0.9660, 0.7027]]]], grad_fn=<NativeBatchNormBackward0>)
 
 | 
再来手动实现一下batchnorm2d
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | 
 manual_normed = []
 
 for c in range(channel):
 
 mean = torch.mean(inputs[:, c, :, :])
 std = torch.std(inputs[:, c, :, :], unbiased=False)
 normed = (inputs[:, c, :, :] - mean) / (std + eps) * torch_bn.weight[c] + torch_bn.bias[c]
 normed = normed.unsqueeze(1)
 manual_normed.append(normed)
 manual_normed = torch.cat(manual_normed, 1)
 print('手动bn结果:\n', manual_normed)
 
 
 isclose = torch.isclose(torch_normed, manual_normed, rtol=1e-4, atol=1e-4)
 print('验证结果:\n', isclose)
 
 | 
如同之前文章所解释,由于CV的卷积计算是通过二维滑动窗口在同一个输入平面上遍历所有位置,因此同一个channel下的多个值对于这个卷积和也是一种"batch"。
相当于对于每一个特征值,计算平均和标准差的范围是N×H×W。
 
手动计算得到的结果如下,和官方接口一致
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 
 | 手动bn结果:tensor([[[[2.2043, 1.1275, 3.9442],
 [1.8388, 0.3752, 2.7226]],
 
 [[1.2185, 0.2591, 0.8559],
 [0.9175, 0.9620, 0.7252]]],
 
 
 [[[2.8658, 0.7975, 2.2066],
 [6.4685, 0.8186, 1.5089]],
 
 [[0.8362, 1.1387, 0.8467],
 [0.7392, 0.9660, 0.7027]]]], grad_fn=<CatBackward0>)
 验证结果:
 tensor([[[[True, True, True],
 [True, True, True]],
 
 [[True, True, True],
 [True, True, True]]],
 
 
 [[[True, True, True],
 [True, True, True]],
 
 [[True, True, True],
 [True, True, True]]]])
 
 | 
layernorm
按照torch的layernorm官方接口文档,对于图像数据,layernorm是这样做的
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | torch_ln = nn.LayerNorm(
 normalized_shape=[channel, height, width],
 elementwise_affine=True
 )
 
 
 
 torch.manual_seed(5)
 torch_ln.weight = nn.Parameter(torch_ln.weight * torch.randn(channel, height, width))
 torch_ln.bias = nn.Parameter(torch_ln.bias + torch.randn(channel, height, width))
 print('weight:\n', torch_ln.weight)
 print('bias:\n', torch_ln.bias, '\n')
 
 
 torch_normed = torch_ln(inputs)
 print('torch ln结果:\n', torch_normed)
 
 | 
如同下面这个图所表示
 
此时仿射变化系数的形状是这样的,为[channel, height, width]
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | weight:Parameter containing:
 tensor([[[-0.4868, -0.6038, -0.5581],
 [ 0.6675, -0.1974,  1.9428]],
 
 [[-1.4017, -0.7626,  0.6312],
 [-0.8991, -0.5578,  0.6907]]], requires_grad=True)
 bias:
 Parameter containing:
 tensor([[[ 0.2225, -0.6662,  0.6846],
 [ 0.5740, -0.5829,  0.7679]],
 
 [[ 0.0571, -1.1894, -0.5659],
 [-0.8327,  0.9014,  0.2116]]], requires_grad=True)
 
 | 
即每个channel内的每一个特征值,都有单独的可训练的仿射变换系数。
layernorm的结果如下
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | torch ln结果:tensor([[[[ 0.3594, -0.8338,  1.3456],
 [ 0.5128, -0.7147, -0.3012]],
 
 [[-2.5939,  0.5089, -0.3546],
 [-1.3715,  0.4607,  0.0553]]],
 
 
 [[[ 0.5477, -0.9583,  0.8526],
 [-1.2112, -0.6760,  0.9378]],
 
 [[-0.3219, -2.4580, -0.3647],
 [-0.6744,  0.4171, -0.0264]]]], grad_fn=<NativeLayerNormBackward0>)
 
 | 
手动进行layernorm的归一化和仿射变换,和官方接口对比一下
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | 
 manual_normed = []
 
 for b in range(batch_size):
 
 mean = torch.mean(inputs[b, :, :, :])
 std = torch.std(inputs[b, :, :, :], unbiased=False)
 normed = (inputs[b, :, :, :] - mean) / (std + eps) * torch_ln.weight + torch_ln.bias
 normed = normed.unsqueeze(0)
 manual_normed.append(normed)
 manual_normed = torch.cat(manual_normed, 0)
 print('手动ln结果:\n', manual_normed)
 
 
 isclose = torch.isclose(torch_normed, manual_normed, rtol=1e-4, atol=1e-4)
 print('验证结果:\n', isclose)
 
 | 
这里计算均值和标准差,是把所有channel内的所有特征值放在一起算的,即每个样本只有一个标量的均值和一个标量的标准差。但是仿射变换的时候就每个特征值都有自己的参数。
手动计算的结果如下,和官方接口一致
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 
 | 手动ln结果:tensor([[[[ 0.3594, -0.8338,  1.3456],
 [ 0.5128, -0.7147, -0.3012]],
 
 [[-2.5939,  0.5090, -0.3546],
 [-1.3715,  0.4607,  0.0553]]],
 
 
 [[[ 0.5477, -0.9583,  0.8527],
 [-1.2112, -0.6760,  0.9378]],
 
 [[-0.3219, -2.4581, -0.3647],
 [-0.6744,  0.4171, -0.0264]]]], grad_fn=<CatBackward0>)
 验证结果:
 tensor([[[[True, True, True],
 [True, True, True]],
 
 [[True, True, True],
 [True, True, True]]],
 
 
 [[[True, True, True],
 [True, True, True]],
 
 [[True, True, True],
 [True, True, True]]]])
 
 | 
NLP数据
再看下在NLP场景下的情况。
先定义输入,N是batch size,S是sequence length,H是hidden size。
| 12
 3
 4
 5
 6
 7
 
 | batch_size = 2
 seq_len = 3
 hidden_size = 4
 torch.manual_seed(6)
 inputs = torch.randn(batch_size, seq_len, hidden_size)
 print('三维输入:\n', inputs)
 
 | 
batchnorm
用官方接口计算
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | torch_bn = nn.BatchNorm1d(num_features=hidden_size, affine=True)
 
 
 
 torch.manual_seed(7)
 torch_bn.weight = nn.Parameter(torch_bn.weight * torch.randn(hidden_size))
 torch_bn.bias = nn.Parameter(torch_bn.bias + torch.randn(hidden_size))
 print('weight:\n', torch_bn.weight)
 print('bias:\n', torch_bn.bias, '\n')
 
 
 torch_normed = torch_bn(inputs.transpose(1, 2)).transpose(1, 2)
 print('torch bn结果:\n', torch_normed)
 
 | 
根据官方接口的描述,输入的第二维应该为特征数,第三维为序列长度,因此这里对输入做了transpose,再把结果transpose回来。
结果如下
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | weight:Parameter containing:
 tensor([-0.1468,  0.7861,  0.9468, -1.1143], requires_grad=True)
 bias:
 Parameter containing:
 tensor([ 1.6908, -0.8948, -0.3556,  1.2324], requires_grad=True)
 
 torch bn结果:
 tensor([[[ 1.8740, -0.7037, -1.8222,  2.3385],
 [ 1.7413, -1.8119,  0.3641,  0.0200],
 [ 1.4615, -0.2676,  0.1081,  1.3450]],
 
 [[ 1.7084, -1.9653,  1.0169,  0.5785],
 [ 1.8213, -0.8614, -0.8056,  2.9892],
 [ 1.5383,  0.2409, -0.9949,  0.1231]]], grad_fn=<TransposeBackward0>)
 
 | 
可以看到batchnorm的仿射变化系数形状在各种情况下都保持和特征向量维度相同。
再来手动计算验证一下
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | 
 
 mean = torch.mean(inputs, dim=(0, 1) , keepdim=True)
 print('均值:\n', mean)
 std = torch.std(inputs, dim=(0, 1), keepdim=True, unbiased=False)
 print('标准差:\n', std, '\n')
 
 manual_normed = (inputs - mean) / (std + eps) * torch_bn.weight + torch_bn.bias
 print('手动bn结果:\n', manual_normed)
 
 
 isclose = torch.isclose(torch_normed, manual_normed, rtol=1e-4, atol=1e-4)
 print('验证结果:\n', isclose)
 
 | 
这里计算用于归一化均值和方差,是在dim=(0,1)范围内计算的,相当于把[N,
S, H]的输入拉平为[N×S,
H]的二维输入,再按二维输入的方式进行batchnorm。
结果如下
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 
 | 均值:tensor([[[-0.2151,  0.5444, -0.2633, -0.5424]]])
 标准差:
 tensor([[[0.7984, 0.3537, 0.7799, 0.7986]]])
 
 手动bn结果:
 tensor([[[ 1.8740, -0.7037, -1.8222,  2.3385],
 [ 1.7413, -1.8119,  0.3641,  0.0200],
 [ 1.4615, -0.2676,  0.1081,  1.3450]],
 
 [[ 1.7084, -1.9653,  1.0169,  0.5785],
 [ 1.8213, -0.8614, -0.8056,  2.9892],
 [ 1.5383,  0.2409, -0.9950,  0.1231]]], grad_fn=<AddBackward0>)
 验证结果:
 tensor([[[True, True, True, True],
 [True, True, True, True],
 [True, True, True, True]],
 
 [[True, True, True, True],
 [True, True, True, True],
 [True, True, True, True]]])
 
 | 
layernorm
终于来到NLP数据的layernorm,先确认一下,huggingface中bert是这么使用layernorm的
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | class BertSelfOutput(nn.Module):def __init__(self, config):
 super().__init__()
 self.dense = nn.Linear(config.hidden_size, config.hidden_size)
 self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
 self.dropout = nn.Dropout(config.hidden_dropout_prob)
 
 def forward(self, hidden_states: torch.normTensor, input_tensor: torch.Tensor) -> torch.Tensor:
 hidden_states = self.dense(hidden_states)
 hidden_states = self.dropout(hidden_states)
 hidden_states = self.LayerNorm(hidden_states + input_tensor)
 return hidden_states
 
 | 
用我们的数据跑一下
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | torch_ln = nn.LayerNorm(normalized_shape=hidden_size, elementwise_affine=True)
 
 
 
 torch.manual_seed(8)
 torch_ln.weight = nn.Parameter(torch_ln.weight * torch.randn(hidden_size))
 torch_ln.bias = nn.Parameter(torch_ln.bias + torch.randn(hidden_size))
 print('weight:\n', torch_ln.weight)
 print('bias:\n', torch_ln.bias, '\n')
 
 
 torch_normed = torch_ln(inputs)
 print('torch ln结果:\n', torch_normed)
 
 | 
仿射变化参数的形状和hidden size一致
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | weight:Parameter containing:
 tensor([ 0.2713, -1.2729,  0.5027,  0.4181], requires_grad=True)
 bias:
 Parameter containing:
 tensor([-0.6394, -0.6608, -0.1433, -0.1043], requires_grad=True)
 
 torch ln结果:
 tensor([[[-0.7547, -2.8528, -0.5092, -0.3423],
 [-1.0957, -0.8780,  0.2388,  0.2097],
 [-0.3502, -1.6158, -0.3133, -0.7224]],
 
 [[-0.9134, -0.4490,  0.6868, -0.3029],
 [-0.7116, -2.5589, -0.1039, -0.6493],
 [-0.5076, -2.1031, -0.9346, -0.1230]]],
 grad_fn=<NativeLayerNormBackward0>)
 
 | 
再来手动验证一下
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | 
 
 mean = torch.mean(inputs, dim=2, keepdim=True)
 print('均值:\n', mean)
 std = torch.std(inputs, dim=2, keepdim=True, unbiased=False)
 print('标准差:\n', std, '\n')
 
 manual_normed = (inputs - mean) / (std + eps) * torch_ln.weight + torch_ln.bias
 print('手动ln结果:\n', manual_normed)
 
 
 isclose = torch.isclose(torch_normed, manual_normed, rtol=1e-4, atol=1e-4)
 print('验证结果:\n', isclose)
 
 | 
得到的均值和标准差如下
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | 均值:tensor([[[-0.8469],
 [ 0.0745],
 [ 0.3386]],
 
 [[ 0.1364],
 [-0.7003],
 [ 0.2831]]])
 标准差:
 tensor([[[0.8578],
 [0.3354],
 [0.6505]],
 
 [[0.4426],
 [0.8448],
 [0.6816]]])
 
 | 
每个sample中的每个token,都有各自的均值和标准差,用于归一化。
最终结果如下
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | 手动ln结果:tensor([[[-0.7547, -2.8528, -0.5092, -0.3423],
 [-1.0957, -0.8780,  0.2388,  0.2097],
 [-0.3502, -1.6158, -0.3133, -0.7224]],
 
 [[-0.9134, -0.4490,  0.6868, -0.3029],
 [-0.7116, -2.5590, -0.1039, -0.6493],
 [-0.5076, -2.1031, -0.9347, -0.1230]]], grad_fn=<AddBackward0>)
 验证结果:
 tensor([[[True, True, True, True],
 [True, True, True, True],
 [True, True, True, True]],
 
 [[True, True, True, True],
 [True, True, True, True],
 [True, True, True, True]]])
 
 | 
归一化的输入能变回原输入吗
既然这些操作是先计算均值和标准差进行归一化,再进行仿射变换,那把仿射变换的参数设置为输入的均值和标准差,是不是就可以把归一化过的数据变回跟原数据一模一样了呢?
以二维情况为例,看下batchnorm是否能变回去。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 
 | batch_size = 3
 feature_num = 4
 torch.manual_seed(0)
 inputs = torch.randn(batch_size, feature_num)
 print('二维输入:\n', inputs)
 
 
 mean = torch.mean(inputs, dim=0, keepdim=True)
 
 std = torch.std(inputs, dim=0, keepdim=True, unbiased=False)
 
 
 
 torch_bn = nn.BatchNorm1d(num_features=feature_num, affine=True)
 
 
 torch_bn.weight = nn.Parameter(std)
 torch_bn.bias =  nn.Parameter(mean)
 
 
 
 
 torch_normed = torch_bn(inputs)
 print('torch bn结果:\n', torch_normed)
 
 isclose = torch.isclose(torch_normed, inputs, rtol=1e-4, atol=1e-4)
 print('验证结果:\n', isclose)
 
 | 
结果如下
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | 二维输入:tensor([[ 1.5410, -0.2934, -2.1788,  0.5684],
 [-1.0845, -1.3986,  0.4033,  0.8380],
 [-0.7193, -0.4033, -0.5966,  0.1820]])
 torch bn结果:
 tensor([[ 1.5410, -0.2934, -2.1788,  0.5684],
 [-1.0845, -1.3986,  0.4033,  0.8380],
 [-0.7193, -0.4033, -0.5966,  0.1821]],
 grad_fn=<NativeBatchNormBackward0>)
 验证结果:
 tensor([[True, True, True, True],
 [True, True, True, True],
 [True, True, True, True]])
 
 | 
确认了batchnorm是可以变回去的。
再来看下layernorm
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 
 | print('二维输入:\n', inputs)
 
 mean = torch.mean(inputs, dim=1, keepdim=True)
 
 std = torch.std(inputs, dim=1, keepdim=True, unbiased=False)
 
 
 
 torch_ln = nn.LayerNorm(normalized_shape=feature_num, elementwise_affine=True)
 
 
 torch_bn.weight = nn.Parameter(std)
 torch_bn.bias =  nn.Parameter(mean)
 
 
 
 
 torch_normed = torch_ln(inputs)
 print('torch ln结果:\n', torch_normed)
 
 isclose = torch.isclose(torch_normed, inputs, rtol=1e-4, atol=1e-4)
 print('验证结果:\n', isclose)
 
 | 
结果如下
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | 二维输入:tensor([[ 1.5410, -0.2934, -2.1788,  0.5684],
 [-1.0845, -1.3986,  0.4033,  0.8380],
 [-0.7193, -0.4033, -0.5966,  0.1820]])
 torch ln结果:
 tensor([[ 1.1918, -0.1481, -1.5251,  0.4814],
 [-0.8146, -1.1451,  0.7512,  1.2086],
 [-0.9685, -0.0551, -0.6140,  1.6376]],
 grad_fn=<NativeLayerNormBackward0>)
 验证结果:
 tensor([[False, False, False, False],
 [False, False, False, False],
 [False, False, False, False]])
 
 | 
发现layernorm并不能通过这种方式把归一化的输入变回原始值,因为layernorm归一化是在特征向量内进行的,所有特征值共享一个均值和方差,但是仿射变换的时候每个特征却有单独的系数。
对于CV数据和NLP数据也有一样的结论。
可以认为batchnorm的归一化和仿射变换是互为可逆的一对操作,而layernorm的归一化和仿射变换是在不同范围内的操作,是不可逆的。
小结
本篇从各种输入数据对batchnorm和layernorm做了手动复现。
需要注意到,batchnorm、layernorm等实际都包含两步操作:①归一化②仿射变换。
基本上,batchnorm可以总结为,对于特征向量中的每一个特征值,在一个"大范围"内进行归一化,这个"大范围"根据输入数据形状,可能是batch,可能是batch×序列长度,或者batch×feature
map大小。并且归一化和仿射变换在同一个方向上进行,因此这两个操作是互为可逆的。
而layernorm是在每个特征向量内部进行归一化处理,然后在另一个方向上使用仿射变换。由于归一化和仿射变换的方向不同,因此无法通过把仿射变换,把已经归一化的数据变换为原输入数据。
读到这了,来一发点赞收藏关注吧~
博客:http://www.linsight.cn/
知乎:Linsight
微信公众号:Linsight

【往期文章】
MoE模型的前世今生
LLM长上下文的问题
解锁大模型长上下文能力
理解Attention:从起源到MHA,MQA和GQA
Yi技术报告-划重点看细节
transformer中normalization的二三事
从代码实现看normalization-到底做了什么
稀疏注意力计算:sliding
window attention
理解LLM位置编码:RoPE
大模型算法题(1)
大模型算法题(2)
大模型算法题(3)
大模型算法题(4)
大模型算法题(5)
Reference
【1】LAYERNORM
https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html
【2】BATCHNORM1D
https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm1d.html
【3】BATCHNORM2D
https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html