手撕Transformer之Layer Normalization

文摘   科技   2024-10-06 08:50   江苏  
点击蓝字
 
关注我们










01


引言



本文是手撕Transformer系列的第五篇。它从头开始介绍层归一化(Layer Normalization),并用代码对其进行了实现。

闲话少说,我们直接开始吧!







02


背景介绍


一般来说,归一化是将特征转换到特定尺度下的过程。对特征进行归一化处理的方法有很多,最著名的是基于最小-最大特征缩放和基于Standard Score的缩放。

  • Min-Max Feature Scaling


最小-最大特征缩放将数值转换为 [0,1] 范围。其计算公式如下:

等式的右侧将每个值减去 X_min;当 X = X_min 时,分子变为 0。除以分母后,输出为 0。同样,当X = X_max 时,会出现新的最大值。当这个值除以 X_max - X_min 时,就变成了 1。这就是范围如何变为 0 和 1 之间的过程。


下面的代码示例演示了该过程:

import torchX = torch.Tensor([22, 5, 6, 8, 10, 19,2])X_max = X.max() # 22X_min = X.min() # 2
# [22-2, 5-2, 6-2, 8-2, 10-2, 19-2, 2-2] = numerator = X-X_min # [20, 3, 4, 6, 8, 17, 0]denominator = X_max-X_min # 22 - 2 = 20
# [20/20, 3/20, 4/20, 6/20, 8/20, 17/20, 0/20]X_new = numerator/denominator

输出如下:

tensor([1.00, 0.15, 0.20, 0.30, 0.40, 0.85, 0.00])
  • Standard Score

在标准化过程中,每个数值都要转换成标准分数。标准分数也称为 z 分数。方法是从每个数值中减去平均值,再除以标准差。

μ 代表数据的平均值或平均数。计算方法是将数据集中的所有数据相加,然后除以数据的数量 n:

σ 代表数据的标准差,即数值与平均值的平均离散度。如果数据集的标准差较小,则数值可能更接近平均值。如果标准差较大,则可能意味着数值分布的范围较大。它可以用以下公式计算。

第一步是找出每个点与平均值的偏差。只需从每个数值减去平均值即可。然后对这些值进行平方运算,去除所有负数。最后,将这些值相加,再除以数值的个数n。然后取平方根即可计算出标准偏差。如果不取平方根,σ² 就是方差。
下面的示例显示了以上计算步骤。
import torchX = torch.Tensor([22, 5, 6, 8, 10, 19,2])n = len(X)
mean = X.sum()/n # X.mean()std = (((X-mean)**2).sum()/n).sqrt() # X.std(unbiased=False)z_scores = (X - mean)/std
print(mean, std, z_scores, sep="\n")

输出如下:

tensor(10.2857)tensor(6.9016)tensor([ 1.6973, -0.7659, -0.6210, -0.3312, -0.0414,  1.2626, -1.2005])
可以看出,这些数值分布在 0 附近。这在意料之中,因为每个值都减去了平均值。现在,标准分也能传递类似的信息,而不需要很大的数值。由于平均值为 10.2857,因此很容易看出 10 的标准分刚好低于 0,为-0.0414。



03


为什么需要Normalization?


机器学习中使用归一化,是因为具有不同尺度特征的模型需要更长的时间来训练;这是因为梯度下降需要更多的时间来收敛。

还有学者认为缺乏归一化会导致梯度过大,最终爆炸,使模型训练变得不稳定。因此,在许多情况下,数据应在进入模型前应该进行规范化处理。





04


Layer Normalization


一般来说,层归一化可确保 "特定层中的所有神经元在给定输入的所有特征上具有有效的相同分布"。


对于每个输入(记为 x),可使用修改后的 z-分数方程计算层归一化:

  • μ 代表最后 D 个维度的平均值

  • σ² 代表最后 D 个维度的方差
  • ε 是一个极小的值,当 σ² 较小时,它能起到帮助作用
  • γ 和 β 是可学习的参数




05


 举个例子


为了演示层归一化的计算方法,我们将形状为 (4,5,3) 的张量在其矩阵中进行归一化,而矩阵的维度大小为 (5,3)。这意味着 D 为 2。

从上图中可以清楚地看出,每个矩阵的值都是根据同一矩阵中的其他值进行标准化得到的。可以使用 PyTorch 的统计函数实现层归一化,代码如下:
# Input Tensor: 4 matrices of 5 rows and 3 columnsX = torch.randint(0, 100, (4, 5, 3)).float()
# Shape to be Normalized: 5 rows, 3 columnsnormalized_shape = (5, 3)
# Number of Dimensions in the Shape to be NormalizedD = len(normalized_shape)
# Set the Default Values for Epsilon, Gamma, and Betaeps = 1e-5gamma = torch.ones(normalized_shape)beta = torch.zeros(normalized_shape)
print(X)
结果如下:
tensor([[[76.,  2., 43.],         [79., 50., 29.],         [59., 78., 73.],         [95., 94., 76.],         [ 9., 74., 64.]],
[[76., 87., 50.], [ 2., 65., 44.], [74., 9., 82.], [83., 54., 82.], [ 6., 97., 52.]],
[[88., 19., 95.], [14., 96., 96.], [93., 58., 0.], [19., 37., 6.], [28., 23., 7.]],
[[ 7., 54., 59.], [57., 30., 18.], [88., 89., 63.], [56., 75., 56.], [63., 23., 73.]]])
每个矩阵都可以通过一个循环进行标准化处理。在这个循环中,计算出平均值和方差。然后将这些值送入层归一化方程,计算矩阵的归一化值。
# Normalizefor i in range(0,4):               # loop through each matrix  mean = X[i].mean()               # mean           var = X[i].var(unbiased=False)   # variance  layer_norm = (X[i]-mean)/(torch.sqrt(var+eps))*gamma + beta 
print(f"μ = {mean:.4f}") print(f"σ^{2} = {var:.4f}") print(layer_norm) print("="*50)
得到结果如下:

通过观察上面的图像和代码输出,可以明显看出,层归一化计算出了本示例中每个矩阵值的 z 值变化。

使用 PyTorch 的 LayerNorm 模块可以计算出与上述相同的答案。该模块需要初始化待归一化的形状。然后,可以将张量传递给该模块,每个矩阵都会相应地归一化。
layer_normalization = nn.LayerNorm(normalized_shape) # nn.LayerNorm((5,3))layer_normalization(X)
输出如下:







06


 NLP中的应用


在自然语言处理中,层归一化发生在每个标记Token的嵌入维度上。对于有 2 个序列、3 个标记和 5 个元素嵌入的批次,其维度形状为(2, 3, 5)。由于最后一个维度将被规范化,因此 D 将为 1。嵌入的形状将是(5, )。必须将其初始化为一个元组,以确保其值能被 LayerNorm 模块提取出来。或者也可以使用 X.shape[-1]。

# Input Tensor: 2 sequences of 3 tokens with 5 dimensional embeddingsX = torch.randint(2, 3, 5)
# Shape to be Normalized: 5 dimensional embeddingnormalized_shape = (5,)
# Number of Dimensions in the Shape to be NormalizedD = len(normalized_shape) # 1
# Create the LayerNorm layer_normalization = nn.LayerNorm(normalized_shape)
# view the beta and gamma and betaprint(layer_normalization.state_dict())
输出如下:
OrderedDict([('weight', tensor([1., 1., 1., 1., 1.])),             ('bias',   tensor([0., 0., 0., 0., 0.]))])
上面是 γ 和 β 的值。下面可以看到,X 的值被初始化为整数,以方便演示层归一化对它们的影响。
print(X)
输出如下:
tensor([[[49., 90., 29., 76., 33.],         [86., 42., 20., 56., 79.],         [40., 49., 72., 16., 85.]],
[[44., 62., 14., 46., 5.], [22., 45., 8., 47., 78.], [96., 17., 7., 56., 60.]]])
也可以使用 PyTorch 代替 for 循环来计算每一行的平均值:
print(X.mean(2, keepdims=True) # maintains the dimensions of X)
输出如下:
tensor([[[55.4000],         [56.6000],         [52.4000]],
[[34.2000], [40.0000], [47.2000]]])
这些值可以在进行层归一化处理后参考,以了解每个值与平均值的差距。
print(layer_normalization(X))
输出如下:


对于第一个Token,90 的 z 值最大,为 1.4464,因为它与平均值 55.4 相差最远,而平均值 55.4 的 z 值为 0。





07


 Transformer中的实现


尽管 PyTorch 已内置了 LayerNorm 模块,但为了更好地理解其在Transformer模型中的使用,我们可以重新创建该模块。实现过程相对简单,并尽量在注释中进行代码解释。

class LayerNorm(nn.Module):
def __init__(self, features, eps=1e-5): super().__init__() # initialize gamma to be all ones self.gamma = nn.Parameter(torch.ones(features)) # initialize beta to be all zeros self.beta = nn.Parameter(torch.zeros(features)) # initialize epsilon self.eps = eps
def forward(self, src): # mean of the token embeddings mean = src.mean(-1, keepdim=True) # variance of the token embeddings var = src.var(-1, keepdim=True,unbiased=False) # return the normalized value return self.gamma * (src - mean) / torch.sqrt(var + self.eps) + self.beta

层归一化将在编码器中使用,因此将在下一篇文章 "编码器 "中演示其用法和残差连接。现在,重要的是了解其实现方法。


请不要忘记点赞和关注,以获取更多信息!






点击上方小卡片关注我




添加个人微信,进专属粉丝群!

AI算法之道
一个专注于深度学习、计算机视觉和自动驾驶感知算法的公众号,涵盖视觉CV、神经网络、模式识别等方面,包括相应的硬件和软件配置,以及开源项目等。
 最新文章