从0开始LLM-GPT-1

87 阅读5分钟

使用层归一化对激活进行归一化

由于梯度消失或爆炸等问题,训练具有多层的深度神经网络有时可能具有挑战性。这些问题导致训练动态不稳定,使网络难以有效调整其权重,这意味着学习过程很难为神经网络找到一组参数(权重),以最小化损失函数。换句话说,网络很难在一定程度上学习数据中的基本模式,从而无法做出准确的预测或决策。

层归一化背后的主要思想是调整神经网络层的激活(输出),使其平均值为 0,方差为 1,也称为单位方差。

这种调整加快了向有效重量的收敛速度,并确保了一致、可靠的训练。基于 DummyLayerNorm 占位符,在 GPT-2 和现代 transformer 架构中,层归一化通常在多头注意力模块之前和之后以及最终输出层之前应用。

image.png 通过以下代码重新创建图中所示的示例,其中实现了一个具有 5 个输入和 6 个输出的神经网络层,将其应用于两个输入示例:

torch.manual_seed(123)
batch_example = torch.randn(2, 5) #A
layer = nn.Sequential(nn.Linear(5, 6), nn.ReLU())
out = layer(batch_example)
print(out)
#结果
tensor([[0.2260, 0.3470, 0.0000, 0.2216, 0.0000, 0.0000],
        [0.2133, 0.2394, 0.0000, 0.5198, 0.3297, 0.0000]],
        grad_fn=<ReluBackward0>)

编码的神经网络层由一个线性层和一个非线性激活函数 ReLU(Rectified Linear Unit 的缩写)组成,它是神经网络中的标准激活函数。

def ReLU(X):
    if X >= 0:
        return x;
    else:
        return 0;

在对这些输出应用层归一化之前,检查均值和方差:

mean = out.mean(dim=-1, keepdim=True)
var = out.var(dim=-1, keepdim=True)
print("Mean:\n", mean)
print("Variance:\n", var)
#结果
Mean:
    tensor([[0.1324],
            [0.2170]], grad_fn=<MeanBackward1>)
Variance:
    tensor([[0.0231],
            [0.0398]], grad_fn=<VarBackward0>)

在均值或方差计算等操作中使用 keepdim=True 可确保输出张量保持与输入张量相同的形状,即使该操作沿 dim 指定的维度减少张量也是如此。例如,如果没有 keepdim=True,则返回的平均张量将是二维向量 [0.1324, 0.2170],而不是二维矩阵 [[0.1324], [0.2170]]。

dim 参数指定在张量中计算统计数据(此处为均值或方差)的维度。

计算张量均值时的 dim 参数图示。例如,如果我们有一个维度为 [行、列] 的 2D 张量(矩阵),则使用 dim=0 将跨行(垂直,如底部所示)执行操作,从而产生聚合每列数据的输出。使用 dim=1 或 dim=-1 将跨列执行操作(水平,如顶部所示),从而生成聚合每行数据的输出。 image.png 如图所示,对于二维张量(如矩阵),使用 dim=-1 进行均值或方差计算等运算与使用 dim=1 相同。这是因为 -1 指的是张量的最后一个维度,它对应于 2D 张量中的列。之后,当向 GPT 模型添加层归一化时,该模型生成形状为 [batch_size、num_tokens、embedding_size] 的 3D 张量,我们仍然可以使用 dim=-1 进行最后一个维度的归一化,避免从 dim=1 更改为 dim=2。

将层归一化应用于我们之前获得的层输出。该操作包括减去均值并除以方差的平方根(也称为标准差):

out_norm = (out - mean) / torch.sqrt(var)
mean = out_norm.mean(dim=-1, keepdim=True)
var = out_norm.var(dim=-1, keepdim=True)
print("Normalized layer outputs:\n", out_norm)
print("Mean:\n", mean)
print("Variance:\n", var)
#结果
Normalized layer outputs:
        tensor([[ 0.6159, 1.4126, -0.8719, 0.5872, -0.8719, -0.8719],
                [-0.0189, 0.1121, -1.0876, 1.5173, 0.5647, -1.0876]],
               grad_fn=<DivBackward0>)
Mean:
    tensor([[2.9802e-08],
            [3.9736e-08]], grad_fn=<MeanBackward1>)
Variance:
    tensor([[1.],
            [1.]], grad_fn=<VarBackward0>)

输出张量中的值 2.9802e-08 是 2.9802 × 10-8 的科学记数法,即十进制形式的 0.00000000298。该值非常接近 0,但由于计算机表示数字的精度有限,可能会累积较小的数值误差,因此它并不完全是 0。

为了提高可读性,可以通过将 sci_mode 设置为 False 来关闭打印张量值时的科学记数法:

torch.set_printoptions(sci_mode=False)
print("Mean:\n", mean)
print("Variance:\n", var)
Mean:
    tensor([[ 0.0000],
            [ 0.0000]], grad_fn=<MeanBackward1>)
Variance:
    tensor([[1.],
            [1.]], grad_fn=<VarBackward0>)

Listing 4.2 A 层归一化类

class LayerNorm(nn.Module):
	def __init__(self, emb_dim):
		super().__init__()
		self.eps = 1e-5
		self.scale = nn.Parameter(torch.ones(emb_dim))
		self.shift = nn.Parameter(torch.zeros(emb_dim))
	def forward(self, x):
		mean = x.mean(dim=-1, keepdim=True)
		var = x.var(dim=-1, keepdim=True, unbiased=False)
		norm_x = (x - mean) / torch.sqrt(var + self.eps)
		return self.scale * norm_x + self.shift

层归一化的这种特定实现在输入张量 x 的最后一个维度上运行,该维度表示嵌入维度 (emb_dim)。变量 eps 是添加到方差中的一个小常数 (epsilon),以防止在归一化过程中除以零。scale 和 shift 是两个可训练的参数(与输入的维度相同),如果确定这样做会提高模型在其训练任务中的性能,则 LLM 会在训练期间自动调整这些参数。这使模型能够学习最适合其正在处理的数据的适当缩放和移位。

偏差方差 在方差计算方法中,通过设置 unbiased=False 来选择实现细节。

在方差计算中,除以方差公式中的输入数 n。这种方法不应用贝塞尔校正,贝塞尔校正通常使用分母中的 n-1 而不是 n 来调整样本方差估计中的偏差。这一决定导致了所谓的偏差估计。对于大规模语言模型 (LLM),其中嵌入维度 n 非常大,使用 n 和 n-1 之间的差异几乎可以忽略不计。

选择这种方法是为了确保与 GPT-2 模型的归一化层兼容,并且因为它反映了 TensorFlow 的默认行为,该行为用于实现原始 GPT-2 模型。

ln = LayerNorm(emb_dim=5)
out_ln = ln(batch_example)
mean = out_ln.mean(dim=-1, keepdim=True)
var = out_ln.var(dim=-1, unbiased=False, keepdim=True)
print("Mean:\n", mean)
print("Variance:\n", var)
#结果
Mean:
    tensor([[ -0.0000],
            [ 0.0000]], grad_fn=<MeanBackward1>)
Variance:
    tensor([[1.0000],
            [1.0000]], grad_fn=<VarBackward0>)

层归一化与批量归一化 如果你熟悉批量归一化(一种常见且传统的神经网络归一化方法),您可能想知道它与层归一化相比如何。与跨批次维度归一化的批量归一化不同,层归一化将跨要素维度归一化。LLM 通常需要大量的计算资源,可用的硬件或特定用例可以决定训练或推理期间的批处理大小。由于层归一化独立于批处理大小对每个输入进行归一化,因此在这些场景中提供了更大的灵活性和稳定性。这对于分布式训练或在资源受限的环境中部署模型时特别有用。