第3课:语言模型的数学基础

第3课:语言模型的数学基础

引言

欢迎来到《从零构建大型语言模型:Python实现20亿参数LLM的完整指南》的第三课。在前两课中,我们回顾了大型语言模型的发展历程,并深入探讨了Transformer架构的核心组件。今天,我们将揭开语言模型背后的数学面纱,这些数学原理是理解和构建LLM的关键基础。

当我们谈论"从零构建"20亿参数的LLM时,"从零"并不意味着不理解底层原理就简单地调用框架API。相反,它要求我们对模型的每一个组成部分都有深刻的理解,包括其数学基础。只有这样,我们才能在遇到问题时进行有效的调试,并对模型进行有针对性的优化。

在本课中,我们将探讨概率语言模型的基础理论,理解交叉熵损失函数的工作原理,学习梯度下降和反向传播算法的核心思想,并讨论优化器选择与参数调整策略。这些知识将为我们后续实现和训练20亿参数LLM奠定坚实的理论基础。

让我们开始这段数学之旅!

1. 概率语言模型基础

语言建模的本质

从根本上讲,语言模型的核心任务是预测:给定历史上下文,预测下一个词的概率分布。形式化地表示,语言模型试图学习条件概率分布:

<math xmlns="http://www.w3.org/1998/Math/MathML"> P ( x t ∣ x ∗ < t ) = P ( x t ∣ x 1 , x 2 , . . . , x ∗ t − 1 ) P(x_t | x *{<t}) = P(x_t | x_1, x_2, ..., x*{t-1}) </math>P(xt∣x∗<t)=P(xt∣x1,x2,...,x∗t−1)

其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> x t x_t </math>xt 表示序列中的第 <math xmlns="http://www.w3.org/1998/Math/MathML"> t t </math>t 个标记(token), <math xmlns="http://www.w3.org/1998/Math/MathML"> x < t x_{<t} </math>x<t 表示所有先前的标记。

一个完整文本序列 <math xmlns="http://www.w3.org/1998/Math/MathML"> X = ( x 1 , x 2 , . . . , x T ) X = (x_1, x_2, ..., x_T) </math>X=(x1,x2,...,xT) 的联合概率可以通过链式法则分解为:

<math xmlns="http://www.w3.org/1998/Math/MathML"> P ( X ) = P ( x 1 ) ⋅ P ( x 2 ∣ x 1 ) ⋅ P ( x 3 ∣ x 1 , x 2 ) ⋅ . . . ⋅ P ( x T ∣ x 1 , x 2 , . . . , x T − 1 ) P(X) = P(x_1) \cdot P(x_2|x_1) \cdot P(x_3|x_1,x_2) \cdot ... \cdot P(x_T|x_1,x_2,...,x_{T-1}) </math>P(X)=P(x1)⋅P(x2∣x1)⋅P(x3∣x1,x2)⋅...⋅P(xT∣x1,x2,...,xT−1)

或更简洁地:

<math xmlns="http://www.w3.org/1998/Math/MathML"> P ( X ) = ∏ ∗ t = 1 T P ( x t ∣ x ∗ < t ) P(X) = \prod *{t=1}^{T} P(x_t | x*{<t}) </math>P(X)=∏∗t=1TP(xt∣x∗<t)

自回归语言模型

现代LLM如GPT系列采用自回归(autoregressive)方法,即依次生成每个标记,每次生成都依赖于之前生成的所有标记。这完美契合了语言的顺序本质,并与上述概率分解吻合。

在实践中,模型输出的是词汇表上的概率分布(通常通过softmax函数实现):

<math xmlns="http://www.w3.org/1998/Math/MathML"> P ( x t ∣ x < t ) = softmax ( h t ⋅ W + b ) P(x_t | x_{<t}) = \text{softmax}(h_t \cdot W + b) </math>P(xt∣x<t)=softmax(ht⋅W+b)

其中:

  • <math xmlns="http://www.w3.org/1998/Math/MathML"> h t h_t </math>ht 是表示上下文 <math xmlns="http://www.w3.org/1998/Math/MathML"> x < t x_{<t} </math>x<t 的隐藏状态向量
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> W W </math>W 是投影矩阵
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> b b </math>b 是偏置向量

困惑度:评估语言模型的指标

在讨论损失函数之前,我们先介绍评估语言模型的标准指标------困惑度(Perplexity)。困惑度衡量模型对测试数据的预测能力,定义为交叉熵损失的指数:

<math xmlns="http://www.w3.org/1998/Math/MathML"> PPL = exp ⁡ ( − 1 N ∑ ∗ i = 1 N log ⁡ P ( x i ∣ x ∗ < i ) ) \text{PPL} = \exp\left(-\frac{1}{N}\sum *{i=1}^{N}\log P(x_i|x*{<i})\right) </math>PPL=exp(−N1∑∗i=1NlogP(xi∣x∗<i))

直观上,困惑度表示模型在每个位置平均需要考虑的词汇选择数量。困惑度越低,表明模型预测越准确。例如,困惑度为10意味着模型在每个位置平均"困惑"于10个词中选择。

优秀的LLM困惑度通常很低:例如,在特定基准上,GPT-3的困惑度约为20,而人类的困惑度大约为12-16。

2. 交叉熵损失函数详解

为什么选择交叉熵?

交叉熵损失函数是训练语言模型最常用的目标函数。对于语言模型,我们希望最大化预测正确下一个词的概率,也就是最小化预测错误的损失。交叉熵正好量化了预测分布与真实分布之间的差异。

数学定义与直观解释

给定真实分布 <math xmlns="http://www.w3.org/1998/Math/MathML"> P P </math>P 和模型预测分布 <math xmlns="http://www.w3.org/1998/Math/MathML"> Q Q </math>Q,交叉熵定义为:

<math xmlns="http://www.w3.org/1998/Math/MathML"> H ( P , Q ) = − ∑ x P ( x ) log ⁡ Q ( x ) H(P, Q) = -\sum_{x} P(x) \log Q(x) </math>H(P,Q)=−∑xP(x)logQ(x)

在语言模型中,真实分布 <math xmlns="http://www.w3.org/1998/Math/MathML"> P P </math>P 是一个one-hot向量(只有真实词的位置为1,其他位置为0),所以交叉熵简化为:

<math xmlns="http://www.w3.org/1998/Math/MathML"> L = − log ⁡ Q ( x true ) L = -\log Q(x_{\text{true}}) </math>L=−logQ(xtrue)

其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> x true x_{\text{true}} </math>xtrue 是真实的下一个词。这也被称为负对数似然(Negative Log-Likelihood,NLL)。

对于一个长度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> T T </math>T 的序列,总损失为:

<math xmlns="http://www.w3.org/1998/Math/MathML"> L = − ∑ ∗ t = 1 T log ⁡ P ( x t ∣ x ∗ < t ) L = -\sum *{t=1}^{T} \log P(x_t | x*{<t}) </math>L=−∑∗t=1TlogP(xt∣x∗<t)

交叉熵损失的数值稳定性

在实际实现中,我们通常将softmax和交叉熵计算结合起来,避免数值溢出和提高计算效率:

python 复制代码
import torch
import torch.nn as nn
import torch.nn.functional as F
​
# 不稳定的实现(可能导致数值问题)
def unstable_cross_entropy(logits, target):
    probs = F.softmax(logits, dim=-1)
    return -torch.log(probs[target])
​
# 稳定的实现
def stable_cross_entropy(logits, target):
    return F.cross_entropy(logits, target)
​
# PyTorch内置的实现(推荐使用)
loss_fn = nn.CrossEntropyLoss()

PyTorch的CrossEntropyLoss已经实现了数值稳定的计算,它首先应用log_softmax,然后计算NLL损失,这避免了softmax计算中可能出现的数值溢出问题。

标签平滑

在训练大型模型时,一个常用的技巧是标签平滑(Label Smoothing)。它通过将目标分布从严格的one-hot向量"平滑"为分配给其他类别少量概率的分布,帮助模型避免过度自信,提高泛化能力:

<math xmlns="http://www.w3.org/1998/Math/MathML"> P smooth ( x ) = ( 1 − α ) ⋅ P ( x ) + α ⋅ 1 ∣ V ∣ P_{\text{smooth}}(x) = (1 - \alpha) \cdot P(x) + \alpha \cdot \frac{1}{|V|} </math>Psmooth(x)=(1−α)⋅P(x)+α⋅∣V∣1

其中:

  • <math xmlns="http://www.w3.org/1998/Math/MathML"> α \alpha </math>α 是平滑参数,通常设置为0.1左右
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> ∣ V ∣ |V| </math>∣V∣ 是词汇表大小
ruby 复制代码
class LabelSmoothingLoss(nn.Module):
    def __init__(self, smoothing=0.1, vocab_size=50257):
        super(LabelSmoothingLoss, self).__init__()
        self.smoothing = smoothing
        self.vocab_size = vocab_size
        
    def forward(self, pred, target):
        log_probs = F.log_softmax(pred, dim=-1)
        
        # 创建平滑标签
        target_dist = torch.zeros_like(pred)
        target_dist.fill_(self.smoothing / (self.vocab_size - 1))
        target_dist.scatter_(1, target.unsqueeze(1), 1 - self.smoothing)
        
        loss = -(target_dist * log_probs).sum(dim=-1).mean()
        return loss

在我们的20亿参数模型训练中,标签平滑将是提高模型性能和稳定性的重要技术。

3. 梯度下降与反向传播

优化问题的形式化

训练神经网络本质上是一个优化问题:寻找一组参数 <math xmlns="http://www.w3.org/1998/Math/MathML"> θ \theta </math>θ,使得在训练数据上的损失函数 <math xmlns="http://www.w3.org/1998/Math/MathML"> L ( θ ) L(\theta) </math>L(θ) 最小。在语言模型中,参数 <math xmlns="http://www.w3.org/1998/Math/MathML"> θ \theta </math>θ 包括注意力机制中的权重矩阵、前馈网络的参数、嵌入矩阵等。

梯度下降算法

梯度下降是解决这一优化问题的标准方法,其核心思想是沿着损失函数负梯度的方向更新参数:

<math xmlns="http://www.w3.org/1998/Math/MathML"> θ ∗ t + 1 = θ t − η ∇ ∗ θ L ( θ t ) \theta *{t+1} = \theta_t - \eta \nabla*{\theta} L(\theta_t) </math>θ∗t+1=θt−η∇∗θL(θt)

其中:

  • <math xmlns="http://www.w3.org/1998/Math/MathML"> θ t \theta_t </math>θt 是当前参数
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> η \eta </math>η 是学习率
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> ∇ θ L ( θ t ) \nabla_{\theta} L(\theta_t) </math>∇θL(θt) 是损失函数关于参数的梯度

在实际训练中,由于数据集通常很大,我们使用随机梯度下降(SGD)或小批量梯度下降,即在每次更新时只使用一个样本或一小批样本来计算梯度,而不是整个数据集。

反向传播算法

反向传播是计算神经网络中梯度的高效算法,基于链式法则和动态规划。以简化的前馈网络为例,考虑一个两层网络:

<math xmlns="http://www.w3.org/1998/Math/MathML"> z = W 2 ⋅ σ ( W 1 ⋅ x + b 1 ) + b 2 z = W_2 \cdot \sigma(W_1 \cdot x + b_1) + b_2 </math>z=W2⋅σ(W1⋅x+b1)+b2

其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> σ \sigma </math>σ 是激活函数。要计算损失函数 <math xmlns="http://www.w3.org/1998/Math/MathML"> L L </math>L 关于参数 <math xmlns="http://www.w3.org/1998/Math/MathML"> W 1 W_1 </math>W1、 <math xmlns="http://www.w3.org/1998/Math/MathML"> b 1 b_1 </math>b1、 <math xmlns="http://www.w3.org/1998/Math/MathML"> W 2 W_2 </math>W2、 <math xmlns="http://www.w3.org/1998/Math/MathML"> b 2 b_2 </math>b2 的梯度,反向传播按以下步骤进行:

  1. 前向传播:计算每一层的输出

    ini 复制代码
    a_1 = W_1 · x + b_1
    h_1 = σ(a_1)
    a_2 = W_2 · h_1 + b_2
    y_pred = σ(a_2)
    L = loss(y_pred, y_true)
  2. 反向传播:从输出层向输入层计算梯度

    bash 复制代码
    dL/da_2 = dL/dy_pred · dy_pred/da_2
    dL/dW_2 = dL/da_2 · dh_1^T
    dL/db_2 = dL/da_2
    dL/dh_1 = W_2^T · dL/da_2
    dL/da_1 = dL/dh_1 · dh_1/da_1
    dL/dW_1 = dL/da_1 · dx^T
    dL/db_1 = dL/da_1

在Transformer模型中,梯度计算更加复杂,因为它涉及多头注意力、残差连接和层归一化等组件。然而,基本原理仍然相同,深度学习框架如PyTorch和TensorFlow使用自动微分引擎自动处理这些复杂性。

梯度累积:大型模型的训练技巧

对于20亿参数的LLM,即使是小批量也可能超出GPU内存。这时,我们可以使用梯度累积技术,在更新参数之前在多个小批量上累积梯度:

ini 复制代码
model.zero_grad()  # 清除现有梯度
accumulated_steps = 4  # 梯度累积步数
​
for i, batch in enumerate(dataloader):
    # 前向传播
    outputs = model(batch['input_ids'])
    loss = loss_fn(outputs, batch['labels'])
    
    # 缩放损失(根据累积步数)
    loss = loss / accumulated_steps
    
    # 反向传播
    loss.backward()
    
    # 每accumulated_steps步更新一次参数
    if (i + 1) % accumulated_steps == 0:
        optimizer.step()
        model.zero_grad()

这使我们能够有效模拟更大的批量大小,而不会超出内存限制。

4. 优化器选择与参数调整策略

常用优化器对比

随着深度学习的发展,许多高级优化器被提出,以克服传统SGD的局限性。以下是最常用的几种优化器:

1. SGD(随机梯度下降)

最基本的优化器,直接沿着负梯度方向更新参数:

<math xmlns="http://www.w3.org/1998/Math/MathML"> θ ∗ t + 1 = θ t − η ∇ ∗ θ L ( θ t ) \theta *{t+1} = \theta_t - \eta \nabla*{\theta} L(\theta_t) </math>θ∗t+1=θt−η∇∗θL(θt)

优点:简单,理论保证 缺点:收敛慢,容易陷入局部最小值,对学习率敏感

2. SGD with Momentum

引入动量项,累积过去梯度的"惯性",帮助克服局部最小值和加速收敛:

<math xmlns="http://www.w3.org/1998/Math/MathML"> v ∗ t + 1 = γ v t + η ∇ ∗ θ L ( θ t ) v *{t+1} = \gamma v_t + \eta \nabla*{\theta} L(\theta_t) </math>v∗t+1=γvt+η∇∗θL(θt) <math xmlns="http://www.w3.org/1998/Math/MathML"> θ ∗ t + 1 = θ t − v ∗ t + 1 \theta *{t+1} = \theta_t - v*{t+1} </math>θ∗t+1=θt−v∗t+1

优点:加速收敛,减少震荡 缺点:仍需手动调整学习率

3. Adam(Adaptive Moment Estimation)

结合了动量和自适应学习率的优势,是训练大型语言模型最常用的优化器之一:

<math xmlns="http://www.w3.org/1998/Math/MathML"> m t = β 1 m ∗ t − 1 + ( 1 − β 1 ) ∇ ∗ θ L ( θ t ) m_t = \beta_1 m *{t-1} + (1 - \beta_1) \nabla*{\theta} L(\theta_t) </math>mt=β1m∗t−1+(1−β1)∇∗θL(θt) <math xmlns="http://www.w3.org/1998/Math/MathML"> v t = β 2 v ∗ t − 1 + ( 1 − β 2 ) ( ∇ ∗ θ L ( θ t ) ) 2 v_t = \beta_2 v *{t-1} + (1 - \beta_2) (\nabla*{\theta} L(\theta_t))^2 </math>vt=β2v∗t−1+(1−β2)(∇∗θL(θt))2 <math xmlns="http://www.w3.org/1998/Math/MathML"> m ^ ∗ t = m t 1 − β 1 t \hat{m}*t = \frac{m_t}{1 - \beta_1^t} </math>m^∗t=1−β1tmt <math xmlns="http://www.w3.org/1998/Math/MathML"> v ^ ∗ t = v t 1 − β 2 t \hat{v}* t = \frac{v_t}{1 - \beta_2^t} </math>v^∗t=1−β2tvt <math xmlns="http://www.w3.org/1998/Math/MathML"> θ ∗ t + 1 = θ t − η v ^ ∗ t + ϵ m ^ t \theta *{t+1} = \theta_t - \frac{\eta}{\sqrt{\hat{v}* t} + \epsilon} \hat{m}_t </math>θ∗t+1=θt−v^∗t +ϵηm^t

优点:收敛快,参数自适应,对超参数选择不敏感 缺点:在某些情况下泛化性能不如SGD

4. AdamW

Adam的变种,实现了更有效的权重衰减:

<math xmlns="http://www.w3.org/1998/Math/MathML"> θ ∗ t + 1 = θ t − η v ^ ∗ t + ϵ m ^ t − η λ θ t \theta *{t+1} = \theta_t - \frac{\eta}{\sqrt{\hat{v}* t} + \epsilon} \hat{m}_t - \eta \lambda \theta_t </math>θ∗t+1=θt−v^∗t +ϵηm^t−ηλθt

优点:改善Adam的泛化性能 缺点:增加了一个需要调整的超参数(权重衰减系数)

在训练大型语言模型时,AdamW通常是首选优化器,因为它结合了快速收敛和良好泛化性能。

ini 复制代码
import torch.optim as optim
​
# 优化器配置示例
optimizer = optim.AdamW(
    model.parameters(),
    lr=1e-4,  # 学习率
    betas=(0.9, 0.999),  # 动量参数
    eps=1e-8,  # 数值稳定性参数
    weight_decay=0.01  # 权重衰减系数
)

学习率调度策略

除了选择合适的优化器,学习率调度也是训练LLM的关键。以下是几种常用的学习率调度策略:

1. 线性预热(Linear Warmup)

从一个很小的学习率开始,在预热阶段线性增加到目标学习率,然后保持恒定或衰减:

python 复制代码
from torch.optim.lr_scheduler import LambdaLR
​
def get_linear_warmup_scheduler(optimizer, warmup_steps, total_steps):
    def lr_lambda(current_step):
        if current_step < warmup_steps:
            return float(current_step) / float(max(1, warmup_steps))
        return 1.0
        
    return LambdaLR(optimizer, lr_lambda)
2. 余弦退火(Cosine Annealing)

学习率按余弦函数从初始值衰减到最小值:

ini 复制代码
from torch.optim.lr_scheduler import CosineAnnealingLR
​
scheduler = CosineAnnealingLR(
    optimizer,
    T_max=total_steps,  # 一个完整周期的步数
    eta_min=1e-6  # 最小学习率
)
3. 线性预热 + 余弦衰减

这种组合策略在预热后应用余弦衰减,是训练大型语言模型的流行选择:

python 复制代码
def get_cosine_schedule_with_warmup(optimizer, warmup_steps, total_steps, min_lr=1e-6):
    def lr_lambda(current_step):
        if current_step < warmup_steps:
            return float(current_step) / float(max(1, warmup_steps))
        progress = float(current_step - warmup_steps) / float(max(1, total_steps - warmup_steps))
        return max(min_lr, 0.5 * (1.0 + math.cos(math.pi * progress)))
        
    return LambdaLR(optimizer, lr_lambda)

对于我们的20亿参数模型,建议使用预热步数为总步数的1-3%的线性预热 + 余弦衰减策略。

其他关键超参数

除了优化器和学习率,还有几个关键超参数需要调整:

1. 批量大小(Batch Size)

批量大小影响训练的稳定性、速度和最终性能。对于大型模型,通常受限于GPU内存,可以通过梯度累积模拟更大的批量。GPT-3使用的批量大小高达3.2M标记。

2. 权重衰减(Weight Decay)

权重衰减通过惩罚大权重值来防止过拟合。对于20亿参数模型,建议的权重衰减系数在0.01至0.1之间。

3. 梯度裁剪(Gradient Clipping)

梯度裁剪通过限制梯度的范数来防止梯度爆炸:

ini 复制代码
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
4. 退火因子(Annealing Factor)

对于使用学习率调度的训练,退火因子决定了最终学习率相对于初始学习率的比例。通常设置为0.1或0.01。

超参数搜索策略

对于如此大规模的模型,详尽的网格搜索是不现实的。相反,我们可以采用以下策略:

  1. 基于先验知识选择初始值:参考类似规模模型的成功配置
  2. 小规模原型验证:在较小模型上快速验证超参数组合
  3. 逐步放大:从小模型开始,逐步扩大规模,微调超参数
  4. 单一变量实验:每次只调整一个超参数,观察效果
  5. 监控早期信号:训练初期的损失曲线通常能提供超参数质量的信号

总结与展望

在本课中,我们探讨了语言模型的数学基础,包括概率语言模型的基本概念、交叉熵损失函数的工作原理、梯度下降与反向传播的核心思想,以及优化器选择与参数调整策略。这些理论基础是成功构建和训练大型语言模型的关键。

我们学习了:

  • 语言模型如何通过条件概率建模序列数据
  • 交叉熵如何量化预测与真实分布的差异
  • 反向传播如何高效计算复杂神经网络中的梯度
  • 不同优化器的特点及其在LLM训练中的应用
  • 学习率调度和其他超参数的调整策略

这些知识将直接应用于我们20亿参数LLM的训练过程,帮助我们做出明智的设计决策和解决训练中的挑战。

在下一课中,我们将转向更实际的内容,讨论LLM训练所需的硬件环境配置、软件依赖和开发工具链,为我们的动手实践做好准备。

延伸阅读

  1. Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning. MIT Press. (第3章和第8章)
  2. Ruder, S. (2016). An overview of gradient descent optimization algorithms. arXiv preprint.
  3. Loshchilov, I., & Hutter, F. (2019). Decoupled Weight Decay Regularization. ICLR.
  4. Smith, L. N. (2018). A disciplined approach to neural network hyper-parameters. arXiv preprint.
  5. Kaplan, J., et al. (2020). Scaling Laws for Neural Language Models. arXiv preprint.

思考问题:

  1. 为什么交叉熵比均方误差更适合语言模型训练?试分析两者在数学和直观上的区别。
  2. 在训练20亿参数LLM时,如何平衡批量大小、学习率和训练步数之间的关系?
  3. 梯度累积和数据并行是两种不同的训练大模型的方法。比较它们的异同,并讨论各自适用的场景。
  4. 假设你训练的模型出现了梯度爆炸现象(损失突然变为NaN),你会采取哪些步骤来诊断和解决这个问题?

期待在下一课中继续我们的大型语言模型构建之旅!

相关推荐
新智元14 分钟前
ChatGPT「学习模式」火爆上线,一大波教育AI连夜被端!24小时导师免费用
人工智能·openai
go546315846515 分钟前
基于YOLOP与GAN的图像修复与防御系统设计与实现
人工智能·深度学习·神经网络·机器学习·生成对抗网络·矩阵
居然JuRan34 分钟前
打破常规!OpenAI无向量化RAG技术全解析
人工智能
算家计算43 分钟前
从基础到自治:Agent开发进阶全流程与实战指南
人工智能·agent
POLOAPI1 小时前
GLM-4.5 凭什么成为国产开源大模型的新标杆?深度解析来了!
人工智能·开源
AscentStream1 小时前
Apache Pulsar × AI Agent:智能系统消息基础架构初探
人工智能·开源
SHIPKING3931 小时前
【机器学习&深度学习】DeepSpeed框架:高效分布式训练的开源利器
人工智能·深度学习·机器学习
竹子_231 小时前
《零基础入门AI:传统机器学习入门(从理论到Scikit-Learn实践)》
人工智能·机器学习·scikit-learn
Codebee1 小时前
OneCode3.0 框架深入研究与应用扩展
人工智能·全栈
大模型真好玩1 小时前
深入浅出LangChain AI Agent智能体开发教程(五)—LangChain接入工具基本流程
人工智能·python·mcp