dage

3.3 预训练 - 让大语言模型学会预测下一个词

📖 目录


🎯 开篇:为什么要预训练?

三个核心问题

  1. 预训练解决什么问题? 模型刚初始化时,所有参数都是随机的,完全不懂语言。预训练让模型通过”阅读”海量文本,学会语言的基本规律。

  2. 为什么是”预测下一个词”? 这是最简单、最自然的学习任务。不需要人工标注,只要有文本就能训练。而且这个任务迫使模型理解语法、语义、常识。

  3. 预训练后模型会什么? 模型学会了”续写文本”的能力。给它”今天天气很”,它能预测”好”。但它还不会”回答问题”,需要后续的微调。


第一部分:预训练是什么?

这一部分解决什么问题?

核心问题:模型刚初始化时,参数都是随机数,完全不懂语言。如何让它学会语言的基本规律?

解决方案:预训练 —— 在海量无标注文本上,让模型学习”预测下一个词”。

突破点:

  • 不需要人工标注(文本本身就是标签)
  • 数据量巨大(整个互联网的文本)
  • 学到的知识可以迁移到各种下游任务

局限:预训练后的模型只会”续写”,不会”对话”,需要后续微调。

1.1 什么是预训练?

12岁版解释

想象你要教一个外星人学中文。你不会一开始就教它”如何写作文”,而是先让它读大量的中文书,让它熟悉中文的用词、语法、表达习惯。

预训练就是这个”大量阅读”的过程。

预训练的定义: 在大规模无标注文本上,训练模型预测下一个词,让模型学会语言的统计规律。

流程:

海量文本(几TB)
    ↓
随机初始化模型参数
    ↓
输入:"今天天气很"
模型预测:"好"(概率80%)、"冷"(概率15%)、"热"(概率5%)
    ↓
计算损失(预测和真实的差距)
    ↓
反向传播,更新参数
    ↓
重复数万亿次
    ↓
得到预训练模型(Base Model)

1.2 为什么是”预测下一个词”?

三个原因:

原因1:不需要标注

传统监督学习:
  需要:(问题, 答案) 对
  成本:人工标注,昂贵

预训练:
  需要:纯文本
  成本:几乎为0(互联网上到处都是)

原因2:任务自然

"今天天气很___"
  → 人类自然会填"好"
  → 模型也学习这个能力

原因3:迫使模型理解语言

要预测下一个词,模型必须理解:
  - 语法:主谓宾结构
  - 语义:"天气"和"好"的关系
  - 常识:"天气很好"是常见表达

类比

就像学英语,你不是先学语法规则,而是先大量阅读、听力,自然而然就懂了。

1.3 预训练 vs 微调

预训练微调
数据海量无标注文本(TB级)少量标注数据(GB级)
任务预测下一个词特定任务(对话、翻译等)
目标学习语言规律学习任务规律
结果Base Model(会续写)Chat Model(会对话)
时间数周到数月数小时到数天
成本极高(数百万美元)较低(数千到数万美元)

类比:

  • 预训练 = 读万卷书(打基础)
  • 微调 = 行万里路(学应用)

第二部分:预测下一个词的数学原理

这一部分解决什么问题?

核心问题:“预测下一个词”听起来简单,但在数学上如何实现?模型如何输出”概率”?

解决方案:通过Softmax函数,把模型的输出向量转换成概率分布。

突破点:

  • 模型输出的是词表中每个词的”得分”
  • Softmax把得分转换成概率(和为1)
  • 选择概率最高的词作为预测

局限:词表越大,计算越慢(需要计算所有词的概率)。

2.1 从输入到输出的完整流程

输入:一段文本

"今天天气很"

步骤1:分词

["今天", "天气", "很"]
→ Token IDs: [1234, 5678, 9012]

步骤2:Embedding

[1234, 5678, 9012]
→ 向量序列(每个Token变成512维向量)

步骤3:Transformer处理

向量序列 → Self-Attention → FFN → ...
→ 输出向量序列(每个位置一个向量)

步骤4:预测下一个词

取最后一个位置的输出向量(512维)
→ 通过线性层映射到词表大小(50000维)
→ 每个维度对应一个词的"得分"

步骤5:Softmax转概率

得分向量 [3.2, 1.5, 0.8, ..., -2.1]
→ Softmax
→ 概率向量 [0.45, 0.12, 0.08, ..., 0.001]

步骤6:选择预测词

找概率最高的词
→ 词ID: 7890
→ 查表得到词:"好"

2.2 Softmax函数详解

公式:

其中:

  • :第i个词的得分
  • :词表大小(如50000)
  • :自然常数(约2.718)

12岁版解释

Softmax就像一个”公平的评分系统”:

  1. 把每个得分转换成正数(通过)
  2. 除以总和,让所有概率加起来等于1
  3. 得分越高,概率越大

例子:

假设词表只有3个词:["好", "冷", "热"]
得分:[3.0, 1.0, 0.5]

步骤1:计算e^z
  e^3.0 = 20.09
  e^1.0 = 2.72
  e^0.5 = 1.65
  总和 = 24.46

步骤2:除以总和
  P("好") = 20.09 / 24.46 = 0.82 (82%)
  P("冷") = 2.72 / 24.46 = 0.11 (11%)
  P("热") = 1.65 / 24.46 = 0.07 (7%)

Softmax的特性:

  1. ✅ 输出都是正数
  2. ✅ 所有输出加起来等于1
  3. ✅ 得分越高,概率越大
  4. ✅ 放大差异(得分3.0比1.0高2倍,但概率82%比11%高7倍)

2.3 为什么不用简单归一化?

简单归一化:

问题1:无法处理负数

得分:[3.0, -1.0, 0.5]
简单归一化:
  总和 = 3.0 + (-1.0) + 0.5 = 2.5
  P(1) = 3.0 / 2.5 = 1.2 (120%???)
  P(2) = -1.0 / 2.5 = -0.4 (负概率???)

问题2:无法放大差异

得分:[100, 99, 1]
简单归一化:
  P(1) = 100/200 = 0.50 (50%)
  P(2) = 99/200 = 0.495 (49.5%)
  P(3) = 1/200 = 0.005 (0.5%)

Softmax:
  P(1) ≈ 0.73 (73%)
  P(2) ≈ 0.27 (27%)
  P(3) ≈ 0.00 (0%)

Softmax能更好地突出”最优选择”。

2.4 温度参数(Temperature)

公式:

温度的作用:

T = 1(标准):

得分:[3.0, 1.0, 0.5]
概率:[0.82, 0.11, 0.07]

T = 0.5(低温,更确定):

得分除以0.5:[6.0, 2.0, 1.0]
概率:[0.95, 0.04, 0.01]
→ 更集中在最高分

T = 2.0(高温,更随机):

得分除以2.0:[1.5, 0.5, 0.25]
概率:[0.62, 0.22, 0.16]
→ 更平均分布

应用场景

  • 低温(T<1):需要确定性输出(如代码生成、翻译)
  • 高温(T>1):需要创造性输出(如写诗、头脑风暴)

第三部分:损失函数详解

这一部分解决什么问题?

核心问题:模型预测出了概率分布,如何衡量”预测得好不好”?如何指导模型改进?

解决方案:交叉熵损失函数 —— 衡量预测分布和真实分布的差距。

突破点:

  • 损失越小,预测越准确
  • 可以通过梯度下降优化
  • 对数形式让计算更稳定

局限:只关注正确答案的概率,忽略了其他词的分布。

3.1 什么是损失函数?

12岁版解释

损失函数就像考试的”扣分规则”:

  • 答对了:扣0分
  • 答错了:扣很多分
  • 答得越离谱,扣分越多

模型的目标就是”减少扣分”。

定义: 损失函数衡量模型预测和真实答案的差距。

例子:

输入:"今天天气很"
真实答案:"好"

模型预测:
  P("好") = 0.82
  P("冷") = 0.11
  P("热") = 0.07

损失 = -log(0.82) = 0.198

损失的含义:

  • 损失 = 0:预测完全正确(P=1)
  • 损失 = 0.2:预测比较准确(P=0.82)
  • 损失 = 2.3:预测很差(P=0.1)
  • 损失 = ∞:预测完全错误(P=0)

3.2 交叉熵损失函数

公式:

其中:

  • :序列长度
  • :第t个位置的真实词
  • :模型预测的概率

12岁版解释

对每个位置:

  1. 看模型预测正确词的概率
  2. 取对数(log)
  3. 加负号(让损失为正数)
  4. 所有位置平均

详细例子:

输入序列:“今天天气很好”

分词:[“今天”, “天气”, “很”, “好”]

预测任务:

位置1:给定"今天",预测"天气"
位置2:给定"今天 天气",预测"很"
位置3:给定"今天 天气 很",预测"好"

模型预测:

位置1:P("天气") = 0.9
位置2:P("很") = 0.7
位置3:P("好") = 0.8

计算损失:

损失1 = -log(0.9) = 0.105
损失2 = -log(0.7) = 0.357
损失3 = -log(0.8) = 0.223

总损失 = (0.105 + 0.357 + 0.223) / 3 = 0.228

3.3 为什么用对数(log)?

原因1:数值稳定

概率很小时:
  P = 0.001
  直接用P:0.001(太小,容易下溢)
  用-log(P):6.91(合理范围)

原因2:乘法变加法

多个位置的联合概率:
  P(x1, x2, x3) = P(x1) × P(x2) × P(x3)
  = 0.9 × 0.7 × 0.8 = 0.504

取对数:
  log P(x1, x2, x3) = log P(x1) + log P(x2) + log P(x3)
  = log(0.9) + log(0.7) + log(0.8)
  = -0.105 + (-0.357) + (-0.223) = -0.685

加法比乘法更稳定,不容易数值溢出。

原因3:梯度更好

损失 = -log(P)
梯度 = -1/P

当P很小时(预测很差):
  梯度很大 → 更新幅度大 → 快速改进

当P接近1时(预测很好):
  梯度很小 → 更新幅度小 → 避免过度调整

3.4 困惑度(Perplexity, PPL)

定义:

其中是平均损失。

12岁版解释

困惑度表示”模型有多困惑”:

  • PPL = 1:完全不困惑(完美预测)
  • PPL = 10:在10个词中犹豫
  • PPL = 100:在100个词中犹豫
  • PPL越小越好

例子:

平均损失 = 0.228
PPL = e^0.228 = 1.256

含义:模型平均在1.256个词中犹豫
(接近完美预测)

实际模型的PPL:

GPT-2(小):PPL ≈ 35
GPT-3(大):PPL ≈ 20
GPT-4:PPL ≈ 15(估计)

PPL越小,模型越强

3.5 损失函数的直觉理解

情况1:预测很准

真实词:"好"
模型预测:P("好") = 0.95

损失 = -log(0.95) = 0.051(很小)

情况2:预测一般

真实词:"好"
模型预测:P("好") = 0.5

损失 = -log(0.5) = 0.693(中等)

情况3:预测很差

真实词:"好"
模型预测:P("好") = 0.01

损失 = -log(0.01) = 4.605(很大)

情况4:预测完全错误

真实词:"好"
模型预测:P("好") = 0.0001

损失 = -log(0.0001) = 9.210(极大)

关键洞察

损失函数”惩罚”模型给正确答案的低概率。模型必须学会给正确答案高概率,才能降低损失。

3.6 交叉熵损失的PyTorch实现

标准实现:

import torch
import torch.nn.functional as F
 
def cross_entropy_loss(logits, targets):
    """
    计算交叉熵损失
 
    参数:
        logits: [batch_size, seq_len, vocab_size] 模型输出的得分
        targets: [batch_size, seq_len] 真实的词ID
 
    返回:
        loss: 标量,平均损失
    """
    # 重塑为2D:[batch_size * seq_len, vocab_size]
    logits = logits.view(-1, logits.size(-1))
    targets = targets.view(-1)
 
    # 计算交叉熵(PyTorch内部会先做Softmax)
    loss = F.cross_entropy(logits, targets)
 
    return loss
 
# 使用示例
batch_size = 4
seq_len = 512
vocab_size = 50000
 
logits = torch.randn(batch_size, seq_len, vocab_size)  # 模型输出
targets = torch.randint(0, vocab_size, (batch_size, seq_len))  # 真实词ID
 
loss = cross_entropy_loss(logits, targets)
print(f"损失: {loss.item():.4f}")

数值稳定版本:

def stable_cross_entropy_loss(logits, targets):
    """
    数值稳定的交叉熵损失
 
    技巧:
    1. 减去最大值,避免exp溢出
    2. 使用log_softmax代替softmax + log
    """
    # 重塑
    logits = logits.view(-1, logits.size(-1))
    targets = targets.view(-1)
 
    # 方法1:使用log_softmax(推荐)
    log_probs = F.log_softmax(logits, dim=-1)
    loss = F.nll_loss(log_probs, targets)
 
    return loss
 
# 手动实现(理解原理)
def manual_cross_entropy(logits, targets):
    """手动实现交叉熵,展示数值稳定技巧"""
    # 减去最大值,避免exp(大数)溢出
    logits_max = logits.max(dim=-1, keepdim=True)[0]
    logits_stable = logits - logits_max
 
    # 计算log_softmax
    log_sum_exp = torch.log(torch.exp(logits_stable).sum(dim=-1, keepdim=True))
    log_probs = logits_stable - log_sum_exp
 
    # 选择正确词的log概率
    batch_size = logits.size(0)
    correct_log_probs = log_probs[range(batch_size), targets]
 
    # 平均损失(加负号)
    loss = -correct_log_probs.mean()
 
    return loss

标签平滑版本(Label Smoothing):

def label_smoothing_loss(logits, targets, smoothing=0.1):
    """
    标签平滑交叉熵损失
 
    作用:防止模型过度自信,提高泛化能力
 
    原理:
        原始标签:[0, 0, 1, 0, 0](one-hot)
        平滑后:[0.02, 0.02, 0.92, 0.02, 0.02]
 
    参数:
        smoothing: 平滑系数(通常0.1)
    """
    vocab_size = logits.size(-1)
 
    # 计算log_softmax
    log_probs = F.log_softmax(logits.view(-1, vocab_size), dim=-1)
    targets = targets.view(-1)
 
    # 创建平滑标签
    confidence = 1.0 - smoothing
    smooth_value = smoothing / (vocab_size - 1)
 
    # one-hot编码
    one_hot = torch.zeros_like(log_probs).scatter_(1, targets.unsqueeze(1), 1)
 
    # 应用平滑
    smooth_labels = one_hot * confidence + (1 - one_hot) * smooth_value
 
    # 计算损失
    loss = -(smooth_labels * log_probs).sum(dim=-1).mean()
 
    return loss
 
# 使用示例
logits = torch.randn(4, 512, 50000)
targets = torch.randint(0, 50000, (4, 512))
 
loss_standard = cross_entropy_loss(logits, targets)
loss_smooth = label_smoothing_loss(logits, targets, smoothing=0.1)
 
print(f"标准损失: {loss_standard.item():.4f}")
print(f"平滑损失: {loss_smooth.item():.4f}")

实践建议

  • 训练初期:使用标准交叉熵
  • 训练后期:考虑标签平滑(smoothing=0.1),提高泛化
  • 大模型:标签平滑效果更明显

第四部分:优化器与学习率

这一部分解决什么问题?

核心问题:知道了损失,如何更新参数?更新多少?如何避免训练不稳定?

解决方案:优化器(如AdamW)+ 学习率调度(如Cosine Decay)。

突破点:

  • Adam自适应调整每个参数的学习率
  • 学习率调度让训练更稳定
  • Warmup避免初期震荡

局限:超参数(学习率、warmup步数等)需要仔细调整。

4.1 什么是优化器?

12岁版解释

优化器就像一个”导航系统”:

  • 损失函数告诉你”现在在哪”
  • 梯度告诉你”应该往哪个方向走”
  • 优化器决定”走多大步”

目标:找到损失最小的参数。

梯度下降的基本思想:

当前参数:θ
损失:L(θ)
梯度:∇L(θ)(损失对参数的导数)

更新规则:
θ_new = θ - η × ∇L(θ)

其中η是学习率(步长)

例子:

假设只有一个参数θ,损失L(θ) = (θ - 5)²

当前:θ = 10
梯度:∇L = 2(θ - 5) = 2(10 - 5) = 10
学习率:η = 0.1

更新:θ_new = 10 - 0.1 × 10 = 9

重复多次后,θ会逐渐接近5(最优值)

4.2 为什么不用简单的梯度下降?

问题1:学习率难选

学习率太大:
  θ_new = θ - 1.0 × ∇L
  → 步子太大,跳过最优点,震荡

学习率太小:
  θ_new = θ - 0.0001 × ∇L
  → 步子太小,收敛太慢

问题2:不同参数需要不同学习率

参数1:梯度很大(变化快)→ 需要小学习率
参数2:梯度很小(变化慢)→ 需要大学习率

简单梯度下降:所有参数用同一个学习率 ❌

问题3:容易卡在鞍点

鞍点:梯度为0,但不是最优点
简单梯度下降:梯度为0 → 停止更新 ❌

4.3 Adam优化器

全称:Adaptive Moment Estimation(自适应矩估计)

核心思想:

  1. 动量(Momentum):考虑历史梯度,避免震荡
  2. 自适应学习率:每个参数有自己的学习率

公式:

# 一阶矩估计(梯度的指数移动平均)
m_t = β1 × m_{t-1} + (1 - β1) × g_t
 
# 二阶矩估计(梯度平方的指数移动平均)
v_t = β2 × v_{t-1} + (1 - β2) × g_t²
 
# 偏差修正
m_hat = m_t / (1 - β1^t)
v_hat = v_t / (1 - β2^t)
 
# 参数更新
θ_t = θ_{t-1} - η × m_hat / (√v_hat + ε)

参数:

  • :一阶矩的衰减率
  • :二阶矩的衰减率
  • :防止除零
  • :学习率(如0.0001)

12岁版解释

  • m_t:记住过去的梯度方向(像惯性)
  • v_t:记住过去的梯度大小(用来调整步长)
  • m_hat / √v_hat:梯度大的参数,步长小;梯度小的参数,步长大

AdamW = Adam + Weight Decay

# 在Adam的基础上,增加权重衰减
θ_t = θ_{t-1} - η × (m_hat / (√v_hat + ε) + λ × θ_{t-1})
 
其中λ是权重衰减系数(如0.01)

作用:防止过拟合,让参数不要太大。

4.4 学习率调度

问题:固定学习率不够好

训练初期:需要大学习率,快速接近最优点
训练后期:需要小学习率,精细调整

解决方案:学习率调度(Learning Rate Schedule)

常见策略:

1. Warmup(预热)

前N步:学习率从0线性增加到最大值

例如:
  步数0:η = 0
  步数500:η = 0.0001
  步数1000:η = 0.0002(最大值)

作用:避免初期梯度不稳定导致的震荡。

2. Cosine Decay(余弦衰减)

Warmup后:学习率按余弦曲线衰减到0

公式:
η_t = η_min + (η_max - η_min) × (1 + cos(πt/T)) / 2

其中:
  t:当前步数
  T:总步数
  η_max:最大学习率
  η_min:最小学习率(通常是η_max的10%)

3. 完整的学习率曲线

学习率
  ↑
  |     ╱‾‾‾╲
  |    ╱     ╲___
  |   ╱          ╲___
  |  ╱               ╲___
  | ╱                    ╲___
  |╱________________________╲___
  └────────────────────────────→ 步数
    Warmup    Cosine Decay

实际例子(GPT-3):

总步数:300B tokens
Warmup:375M tokens(0.125%)
最大学习率:6e-5
最小学习率:6e-6(最大值的10%)
优化器:AdamW
β1 = 0.9, β2 = 0.95
权重衰减:0.1

4.5 为什么需要Warmup?

问题:训练初期,参数是随机的,梯度不稳定

没有Warmup:

步数0:η = 0.0002(很大)
梯度:很大且不稳定
更新:θ - 0.0002 × 大梯度 = 跳得很远
结果:损失爆炸,训练崩溃 ❌

有Warmup:

步数0:η = 0(从0开始)
步数500:η = 0.0001(逐渐增大)
步数1000:η = 0.0002(达到最大值)

初期:小学习率 → 小步子 → 稳定
后期:大学习率 → 大步子 → 快速收敛

经验法则

Warmup步数通常是总步数的0.1%-1%。 例如:训练100万步,Warmup 1000-10000步。

4.6 优化器配置的完整代码

AdamW优化器配置:

import torch
from torch.optim import AdamW
 
# 模型参数
model = YourTransformerModel()
 
# 配置AdamW优化器
optimizer = AdamW(
    model.parameters(),
    lr=6e-4,              # 学习率(GPT-3用6e-5,小模型可以更大)
    betas=(0.9, 0.95),    # β1和β2(一阶和二阶矩的衰减率)
    eps=1e-8,             # 防止除零
    weight_decay=0.1      # 权重衰减(L2正则化)
)
 
print(f"优化器配置完成")
print(f"学习率: {optimizer.defaults['lr']}")
print(f"权重衰减: {optimizer.defaults['weight_decay']}")

分组参数配置(高级):

def configure_optimizers(model, learning_rate, weight_decay):
    """
    为不同参数组配置不同的优化策略
 
    策略:
    - 权重矩阵:应用权重衰减
    - 偏置和LayerNorm:不应用权重衰减
    """
    # 需要权重衰减的参数
    decay_params = []
    # 不需要权重衰减的参数
    no_decay_params = []
 
    for name, param in model.named_parameters():
        if not param.requires_grad:
            continue
 
        # 偏置和LayerNorm参数不应用权重衰减
        if 'bias' in name or 'norm' in name or 'ln' in name:
            no_decay_params.append(param)
        else:
            decay_params.append(param)
 
    # 创建参数组
    param_groups = [
        {'params': decay_params, 'weight_decay': weight_decay},
        {'params': no_decay_params, 'weight_decay': 0.0}
    ]
 
    optimizer = AdamW(param_groups, lr=learning_rate, betas=(0.9, 0.95))
 
    print(f"参数分组完成:")
    print(f"  需要权重衰减: {len(decay_params)}个参数")
    print(f"  不需要权重衰减: {len(no_decay_params)}个参数")
 
    return optimizer
 
# 使用示例
optimizer = configure_optimizers(
    model=model,
    learning_rate=6e-4,
    weight_decay=0.1
)

梯度裁剪配置:

def train_step_with_grad_clip(model, batch, optimizer, max_grad_norm=1.0):
    """
    训练步骤,包含梯度裁剪
 
    梯度裁剪作用:
    - 防止梯度爆炸
    - 稳定训练
    """
    # 前向传播
    loss = model(batch)
 
    # 反向传播
    loss.backward()
 
    # 梯度裁剪(在optimizer.step()之前)
    torch.nn.utils.clip_grad_norm_(
        model.parameters(),
        max_norm=max_grad_norm  # 梯度范数的最大值
    )
 
    # 参数更新
    optimizer.step()
    optimizer.zero_grad()
 
    return loss.item()
 
# 使用示例
for batch in dataloader:
    loss = train_step_with_grad_clip(
        model=model,
        batch=batch,
        optimizer=optimizer,
        max_grad_norm=1.0  # GPT-3使用1.0
    )

4.7 学习率调度的完整实现

线性Warmup + 余弦衰减:

import math
 
class CosineWarmupScheduler:
    """
    学习率调度器:Warmup + Cosine Decay
 
    参数:
        optimizer: 优化器
        warmup_steps: Warmup步数
        total_steps: 总训练步数
        min_lr_ratio: 最小学习率 / 最大学习率(通常0.1)
    """
    def __init__(self, optimizer, warmup_steps, total_steps, min_lr_ratio=0.1):
        self.optimizer = optimizer
        self.warmup_steps = warmup_steps
        self.total_steps = total_steps
        self.min_lr_ratio = min_lr_ratio
        self.base_lr = optimizer.defaults['lr']
        self.current_step = 0
 
    def step(self):
        """更新学习率"""
        self.current_step += 1
        lr = self.get_lr()
 
        for param_group in self.optimizer.param_groups:
            param_group['lr'] = lr
 
    def get_lr(self):
        """计算当前学习率"""
        step = self.current_step
 
        # Warmup阶段:线性增长
        if step < self.warmup_steps:
            lr = self.base_lr * step / self.warmup_steps
        # Cosine Decay阶段
        else:
            progress = (step - self.warmup_steps) / (self.total_steps - self.warmup_steps)
            lr = self.min_lr_ratio * self.base_lr + \
                 (self.base_lr - self.min_lr_ratio * self.base_lr) * \
                 0.5 * (1 + math.cos(math.pi * progress))
 
        return lr
 
# 使用示例
total_steps = 100000
warmup_steps = 2000
 
scheduler = CosineWarmupScheduler(
    optimizer=optimizer,
    warmup_steps=warmup_steps,
    total_steps=total_steps,
    min_lr_ratio=0.1
)
 
# 训练循环
for step, batch in enumerate(dataloader):
    # 前向+反向+更新
    loss = train_step_with_grad_clip(model, batch, optimizer)
 
    # 更新学习率
    scheduler.step()
 
    if step % 1000 == 0:
        current_lr = scheduler.get_lr()
        print(f"步数 {step}: 损失={loss:.4f}, 学习率={current_lr:.6f}")

使用PyTorch内置调度器:

from torch.optim.lr_scheduler import LambdaLR
 
def get_cosine_schedule_with_warmup(optimizer, warmup_steps, total_steps, min_lr_ratio=0.1):
    """
    创建Warmup + Cosine Decay调度器(PyTorch版本)
    """
    def lr_lambda(current_step):
        # Warmup
        if current_step < warmup_steps:
            return float(current_step) / float(max(1, warmup_steps))
        # Cosine Decay
        progress = float(current_step - warmup_steps) / float(max(1, total_steps - warmup_steps))
        return max(min_lr_ratio, 0.5 * (1.0 + math.cos(math.pi * progress)))
 
    return LambdaLR(optimizer, lr_lambda)
 
# 使用示例
scheduler = get_cosine_schedule_with_warmup(
    optimizer=optimizer,
    warmup_steps=2000,
    total_steps=100000,
    min_lr_ratio=0.1
)
 
# 训练循环
for batch in dataloader:
    loss = train_step_with_grad_clip(model, batch, optimizer)
    scheduler.step()  # 每步更新学习率

4.8 学习率曲线可视化

ASCII艺术图:Warmup + Cosine Decay

学习率
  ↑
0.0006|                    ╱‾‾‾‾‾╲
      |                   ╱       ╲
0.0005|                  ╱         ╲___
      |                 ╱              ╲___
0.0004|                ╱                   ╲___
      |               ╱                        ╲___
0.0003|              ╱                             ╲___
      |             ╱                                  ╲___
0.0002|            ╱                                       ╲___
      |           ╱                                            ╲___
0.0001|          ╱                                                 ╲___
      |     ╱‾‾‾                                                       ╲___
0.00006|____╱________________________________________________________________╲___
      └────────────────────────────────────────────────────────────────────→ 步数
      0    2k                                                            100k

      ←Warmup→←─────────────────── Cosine Decay ──────────────────────→

关键点:
- 步数0:学习率=0
- 步数2000:学习率=6e-4(最大值,Warmup结束)
- 步数100000:学习率=6e-5(最小值,训练结束)

不同学习率策略对比:

学习率
  ↑
  |  ┌─────────────────────────────────  固定学习率(不推荐)
  |  │
  |  │    ╱‾‾‾‾‾╲___                     Warmup + Cosine(推荐)
  |  │   ╱          ╲___
  |  │  ╱               ╲___
  |  │ ╱                    ╲___
  |  │╱                         ╲___
  |  ╱                               ╲___  线性衰减
  | ╱                                    ╲
  |╱______________________________________╲___
  └────────────────────────────────────────→ 步数

对比:
- 固定学习率:简单但效果差,后期无法精细调整
- 线性衰减:效果一般,衰减速度不够平滑
- Cosine衰减:效果最好,平滑衰减,广泛使用

第五部分:大规模训练技巧

这一部分解决什么问题?

核心问题:大模型参数量巨大(几十亿到万亿),单个GPU放不下,训练时间长达数月。如何高效训练?

解决方案:数据并行 + 模型并行 + 混合精度训练 + 梯度累积。

突破点:

  • 数据并行:多GPU同时处理不同数据
  • 模型并行:把模型切分到多个GPU
  • 混合精度:用FP16加速,节省内存
  • 梯度累积:模拟大batch size

局限:需要大量GPU(数百到数千块),成本极高。

5.1 为什么需要分布式训练?

单GPU的限制:

GPU内存:80GB(A100)
模型大小:7B参数 × 4字节/参数 = 28GB
优化器状态:28GB × 2 = 56GB(Adam需要存m和v)
梯度:28GB
激活值:10-20GB

总需求:28 + 56 + 28 + 20 = 132GB > 80GB ❌

训练时间:

假设:
  - 模型:7B参数
  - 数据:1T tokens
  - 单GPU吞吐:1000 tokens/秒
  - 需要时间:1T / 1000 = 10亿秒 ≈ 31年 ❌

解决方案:用多个GPU并行训练

5.2 数据并行(Data Parallelism)

核心思想:每个GPU有完整的模型副本,处理不同的数据

流程:

步骤1:每个GPU加载模型副本
  GPU 0: 模型副本
  GPU 1: 模型副本
  GPU 2: 模型副本
  GPU 3: 模型副本

步骤2:分配不同的数据
  GPU 0: batch 0 (数据0-31)
  GPU 1: batch 1 (数据32-63)
  GPU 2: batch 2 (数据64-95)
  GPU 3: batch 3 (数据96-127)

步骤3:各自计算梯度
  GPU 0: 梯度0
  GPU 1: 梯度1
  GPU 2: 梯度2
  GPU 3: 梯度3

步骤4:梯度平均(All-Reduce)
  平均梯度 = (梯度0 + 梯度1 + 梯度2 + 梯度3) / 4

步骤5:所有GPU用平均梯度更新参数
  所有GPU的参数保持同步

12岁版解释

就像4个学生一起做作业:

  • 每人有一份相同的题目(模型)
  • 每人做不同的练习题(数据)
  • 做完后,大家交流答案(梯度平均)
  • 所有人都学到了相同的知识(参数同步)

优点:

  • ✅ 实现简单
  • ✅ 加速明显(4个GPU ≈ 4倍速度)
  • ✅ 适合中小模型(<10B参数)

缺点:

  • ❌ 每个GPU需要存完整模型
  • ❌ 大模型放不下

5.3 模型并行(Model Parallelism)

核心思想:把模型切分到多个GPU

方式1:层间并行(Pipeline Parallelism)

GPU 0: Layer 1-8
GPU 1: Layer 9-16
GPU 2: Layer 17-24
GPU 3: Layer 25-32

数据流:
  输入 → GPU 0 → GPU 1 → GPU 2 → GPU 3 → 输出

问题:GPU利用率低

时间轴:
  GPU 0: [计算] [等待] [等待] [等待]
  GPU 1: [等待] [计算] [等待] [等待]
  GPU 2: [等待] [等待] [计算] [等待]
  GPU 3: [等待] [等待] [等待] [计算]

利用率:25% ❌

改进:微批次流水线(Micro-batch Pipeline)

把batch切成多个micro-batch,流水线处理

时间轴:
  GPU 0: [mb1] [mb2] [mb3] [mb4]
  GPU 1:      [mb1] [mb2] [mb3] [mb4]
  GPU 2:           [mb1] [mb2] [mb3] [mb4]
  GPU 3:                [mb1] [mb2] [mb3] [mb4]

利用率:提升到70-80% ✅

方式2:张量并行(Tensor Parallelism)

把每一层的矩阵切分到多个GPU

例如:线性层 Y = X @ W
  W的维度:[4096, 4096]
  切分成4份:每个GPU存 [4096, 1024]

GPU 0: Y0 = X @ W0
GPU 1: Y1 = X @ W1
GPU 2: Y2 = X @ W2
GPU 3: Y3 = X @ W3

最后拼接:Y = [Y0, Y1, Y2, Y3]

优点:

  • ✅ 可以训练超大模型
  • ✅ 突破单GPU内存限制

缺点:

  • ❌ 通信开销大
  • ❌ 实现复杂

5.4 混合精度训练(Mixed Precision Training)

核心思想:用FP16(16位浮点数)代替FP32(32位浮点数)

FP32 vs FP16:

FP32(单精度):
  - 32位:1位符号 + 8位指数 + 23位尾数
  - 范围:±3.4 × 10^38
  - 精度:7位有效数字

FP16(半精度):
  - 16位:1位符号 + 5位指数 + 10位尾数
  - 范围:±6.5 × 10^4
  - 精度:3位有效数字

优点:

内存:减少50%
  7B模型:28GB → 14GB

速度:提升2-3倍
  GPU的FP16计算单元更多

问题:数值不稳定

FP16范围小:
  - 太大的数:溢出(变成inf)
  - 太小的数:下溢(变成0)

例如:
  梯度 = 0.00001(FP32)
  → 0(FP16,下溢)❌

解决方案:混合精度

前向传播:FP16(快)
反向传播:FP16(快)
参数更新:FP32(精确)

关键技巧:
1. 损失缩放(Loss Scaling)
   损失 × 1000 → 梯度 × 1000 → 避免下溢
   更新时再除以1000

2. 主权重(Master Weights)
   用FP32存储参数的"主副本"
   FP16只用于计算

5.5 梯度累积(Gradient Accumulation)

问题:GPU内存有限,batch size受限

GPU内存:80GB
模型:28GB
单个样本:1GB
最大batch size:(80 - 28) / 1 = 52

但我们想要batch size = 512 ❌

解决方案:梯度累积

把大batch切成多个小batch,累积梯度

例如:batch size = 512,切成8个小batch(每个64)

步骤1:前向+反向(batch 0-63)
  计算梯度,不更新参数

步骤2:前向+反向(batch 64-127)
  累积梯度(梯度 += 新梯度)

...重复8次...

步骤8:前向+反向(batch 448-511)
  累积梯度

步骤9:参数更新
  用累积的梯度更新参数
  清零梯度

12岁版解释

就像搬砖:

  • 一次搬512块太重(内存不够)
  • 分8次搬,每次64块(梯度累积)
  • 搬完8次后,一起结算工资(参数更新)

代码示例:

accumulation_steps = 8
optimizer.zero_grad()
 
for i, batch in enumerate(dataloader):
    # 前向传播
    loss = model(batch)
 
    # 反向传播(梯度累积)
    loss = loss / accumulation_steps
    loss.backward()
 
    # 每accumulation_steps步更新一次
    if (i + 1) % accumulation_steps == 0:
        optimizer.step()
        optimizer.zero_grad()

5.6 实际训练配置(LLaMA-2 7B)

模型:7B参数
数据:2T tokens
GPU:2048 × A100 80GB
训练时间:约3周

并行策略:
  - 数据并行:256路(256个GPU组)
  - 张量并行:8路(每组8个GPU)
  - 总GPU数:256 × 8 = 2048

Batch size:
  - 全局batch size:4M tokens
  - 每个GPU:4M / 2048 = 2048 tokens
  - 序列长度:4096
  - 每个GPU样本数:2048 / 4096 = 0.5
  - 梯度累积:2步(0.5 × 2 = 1个样本)

优化器:AdamW
学习率:3e-4
Warmup:2000步
权重衰减:0.1
梯度裁剪:1.0

混合精度:FP16 + FP32主权重

5.7 分布式训练的完整代码框架

数据并行(DDP)实现:

import torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data.distributed import DistributedSampler
 
def setup_distributed():
    """初始化分布式训练环境"""
    # 初始化进程组
    dist.init_process_group(backend='nccl')  # NCCL适合GPU
 
    # 获取当前进程的rank和world_size
    rank = dist.get_rank()
    world_size = dist.get_world_size()
 
    # 设置当前进程使用的GPU
    torch.cuda.set_device(rank)
 
    return rank, world_size
 
def cleanup_distributed():
    """清理分布式训练环境"""
    dist.destroy_process_group()
 
def train_with_ddp():
    """使用DDP训练的完整示例"""
    # 1. 初始化分布式环境
    rank, world_size = setup_distributed()
    device = torch.device(f'cuda:{rank}')
 
    print(f"进程 {rank}/{world_size} 启动")
 
    # 2. 创建模型并移到GPU
    model = YourTransformerModel().to(device)
 
    # 3. 包装为DDP模型
    model = DDP(model, device_ids=[rank])
 
    # 4. 创建优化器
    optimizer = AdamW(model.parameters(), lr=6e-4, weight_decay=0.1)
 
    # 5. 创建数据加载器(使用DistributedSampler)
    dataset = YourDataset()
    sampler = DistributedSampler(
        dataset,
        num_replicas=world_size,
        rank=rank,
        shuffle=True
    )
    dataloader = torch.utils.data.DataLoader(
        dataset,
        batch_size=32,
        sampler=sampler,
        num_workers=4,
        pin_memory=True
    )
 
    # 6. 训练循环
    model.train()
    for epoch in range(num_epochs):
        # 每个epoch开始时设置sampler的epoch(确保每个epoch的shuffle不同)
        sampler.set_epoch(epoch)
 
        for batch_idx, batch in enumerate(dataloader):
            # 数据移到GPU
            batch = {k: v.to(device) for k, v in batch.items()}
 
            # 前向传播
            loss = model(batch)
 
            # 反向传播
            optimizer.zero_grad()
            loss.backward()
 
            # 梯度裁剪
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
 
            # 参数更新
            optimizer.step()
 
            # 只在rank 0打印日志
            if rank == 0 and batch_idx % 100 == 0:
                print(f"Epoch {epoch}, Batch {batch_idx}, Loss: {loss.item():.4f}")
 
    # 7. 清理
    cleanup_distributed()
 
# 启动命令(使用torchrun):
# torchrun --nproc_per_node=8 train.py

梯度累积实现:

def train_with_gradient_accumulation(
    model, dataloader, optimizer,
    accumulation_steps=8,
    max_grad_norm=1.0
):
    """
    使用梯度累积训练
 
    参数:
        accumulation_steps: 累积多少步后更新一次参数
    """
    model.train()
    optimizer.zero_grad()
 
    for batch_idx, batch in enumerate(dataloader):
        # 前向传播
        loss = model(batch)
 
        # 损失除以累积步数(重要!)
        loss = loss / accumulation_steps
 
        # 反向传播(梯度累积)
        loss.backward()
 
        # 每accumulation_steps步更新一次参数
        if (batch_idx + 1) % accumulation_steps == 0:
            # 梯度裁剪
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=max_grad_norm)
 
            # 参数更新
            optimizer.step()
            optimizer.zero_grad()
 
            if batch_idx % 100 == 0:
                print(f"Batch {batch_idx}, Loss: {loss.item() * accumulation_steps:.4f}")
 
# 使用示例
train_with_gradient_accumulation(
    model=model,
    dataloader=dataloader,
    optimizer=optimizer,
    accumulation_steps=8  # 模拟8倍的batch size
)

混合精度训练实现:

from torch.cuda.amp import autocast, GradScaler
 
def train_with_mixed_precision(model, dataloader, optimizer):
    """
    使用混合精度训练
 
    优点:
    - 速度提升2-3倍
    - 内存减少50%
    """
    model.train()
 
    # 创建GradScaler(用于损失缩放)
    scaler = GradScaler()
 
    for batch_idx, batch in enumerate(dataloader):
        optimizer.zero_grad()
 
        # 使用autocast自动混合精度
        with autocast():
            # 前向传播(自动使用FP16)
            loss = model(batch)
 
        # 损失缩放 + 反向传播
        scaler.scale(loss).backward()
 
        # 梯度裁剪(需要先unscale)
        scaler.unscale_(optimizer)
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
 
        # 参数更新(自动处理缩放)
        scaler.step(optimizer)
        scaler.update()
 
        if batch_idx % 100 == 0:
            print(f"Batch {batch_idx}, Loss: {loss.item():.4f}")
 
# 使用示例
train_with_mixed_precision(
    model=model,
    dataloader=dataloader,
    optimizer=optimizer
)

完整训练循环(DDP + 梯度累积 + 混合精度):

def train_complete(
    model, dataloader, optimizer, scheduler,
    rank, world_size,
    accumulation_steps=8,
    max_grad_norm=1.0,
    log_interval=100
):
    """
    完整的训练循环:DDP + 梯度累积 + 混合精度
    """
    model.train()
    scaler = GradScaler()
 
    for epoch in range(num_epochs):
        dataloader.sampler.set_epoch(epoch)
 
        for batch_idx, batch in enumerate(dataloader):
            # 数据移到GPU
            batch = {k: v.to(f'cuda:{rank}') for k, v in batch.items()}
 
            # 混合精度前向传播
            with autocast():
                loss = model(batch)
                loss = loss / accumulation_steps
 
            # 反向传播
            scaler.scale(loss).backward()
 
            # 每accumulation_steps步更新一次
            if (batch_idx + 1) % accumulation_steps == 0:
                # 梯度裁剪
                scaler.unscale_(optimizer)
                torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=max_grad_norm)
 
                # 参数更新
                scaler.step(optimizer)
                scaler.update()
                optimizer.zero_grad()
 
                # 学习率调度
                scheduler.step()
 
                # 日志(只在rank 0打印)
                if rank == 0 and (batch_idx + 1) % log_interval == 0:
                    current_lr = scheduler.get_last_lr()[0]
                    print(f"Epoch {epoch}, Batch {batch_idx + 1}, "
                          f"Loss: {loss.item() * accumulation_steps:.4f}, "
                          f"LR: {current_lr:.6f}")
 
# 启动命令:
# torchrun --nproc_per_node=8 train.py

5.8 混合精度训练的内存对比

FP32 vs FP16内存占用对比:

组件                    FP32        FP16        节省
─────────────────────────────────────────────────
模型参数(7B)          28 GB       14 GB       50%
优化器状态(Adam)      56 GB       28 GB       50%
梯度                    28 GB       14 GB       50%
激活值(估计)          20 GB       10 GB       50%
─────────────────────────────────────────────────
总计                   132 GB       66 GB       50%

结论:
- FP32:需要132GB,A100 80GB放不下 ❌
- FP16:需要66GB,A100 80GB可以放下 ✅

ASCII柱状图:

内存占用 (GB)
  ↑
140|  ████████████████████
   |  ████████████████████
120|  ████████████████████
   |  ████████████████████
100|  ████████████████████
   |  ████████████████████
 80|  ████████████████████  ─────────  A100容量上限
   |  ████████████████████  ██████████
 60|  ████████████████████  ██████████
   |  ████████████████████  ██████████
 40|  ████████████████████  ██████████
   |  ████████████████████  ██████████
 20|  ████████████████████  ██████████
   |  ████████████████████  ██████████
  0|  ────────────────────  ──────────
     └──────────────────────────────────
           FP32              FP16
          (132GB)           (66GB)

     ❌ 超出容量          ✅ 可以训练

5.9 并行策略对比分析

并行策略优点缺点通信开销适用场景
数据并行实现简单
加速明显
通信少
每个GPU需要完整模型
大模型放不下
低
(只需同步梯度)
中小模型
(<10B参数)
张量并行可训练超大模型
突破内存限制
通信开销大
实现复杂
高
(每层都需要通信)
大模型
(>10B参数)
流水线并行可训练超大模型
通信较少
GPU利用率低
需要微批次优化
中
(层间通信)
超大模型
(>100B参数)
混合并行结合各种优点
最高效
配置复杂
需要调优
中工业级训练
(GPT-3, LLaMA)

实际选择建议:

模型大小          推荐策略                    GPU数量
─────────────────────────────────────────────────
< 1B             数据并行                    8-32
1B - 10B         数据并行                    32-128
10B - 100B       数据并行 + 张量并行         128-1024
> 100B           数据并行 + 张量并行         1024+
                 + 流水线并行

第六部分:实际案例深度分析

这一部分解决什么问题?

核心问题:理论很美好,但实际训练大模型会遇到什么问题?如何配置超参数?

解决方案:通过GPT-3、LLaMA-2等真实案例,学习实际训练经验。

价值:

  • 了解工业级训练的完整流程
  • 学习超参数选择的经验法则
  • 避免常见的训练陷阱

6.1 GPT-3训练过程完整复盘

基本信息:

模型:GPT-3 175B
发布时间:2020年5月
训练机构:OpenAI
论文:Language Models are Few-Shot Learners

训练数据:

总量:300B tokens(约570GB文本)

数据来源:
1. Common Crawl(60%):410GB → 过滤后 180B tokens
   - 网页抓取数据
   - 质量参差不齐,需要大量过滤

2. WebText2(22%):66B tokens
   - Reddit高质量链接
   - 质量较高

3. Books1 + Books2(16%):48B tokens
   - 电子书数据
   - 长文本,有助于学习长程依赖

4. Wikipedia(3%):9B tokens
   - 高质量百科数据
   - 事实性强

数据混合策略:
- 不是均匀采样,而是按质量加权
- 高质量数据(Books, Wikipedia)重复采样
- 低质量数据(Common Crawl)降低采样率

模型架构:

参数量:175B
层数:96层
隐藏维度:12288
注意力头数:96
序列长度:2048
词表大小:50257

计算量:
- 每个token的FLOPs:约 6 × 175B = 1050 GFLOPs
- 总FLOPs:300B tokens × 1050 GFLOPs = 3.14 × 10^23 FLOPs

硬件配置:

GPU:10000+ × V100 32GB
训练时间:数月(具体未公开,估计3-4个月)
成本:估计400-500万美元(GPU租赁 + 电费)

并行策略:
- 数据并行:大规模
- 模型并行:每个模型分布在多个GPU
- 具体配置未公开

训练超参数:

优化器:AdamW
  β1 = 0.9
  β2 = 0.95
  ε = 1e-8
  权重衰减 = 0.1

学习率:6e-5(峰值)
  Warmup:375M tokens(0.125%)
  衰减:Cosine Decay到 6e-6(10%)

Batch size:3.2M tokens
  序列长度:2048
  样本数:3.2M / 2048 = 1562个样本

梯度裁剪:1.0
混合精度:FP16 + FP32主权重

训练过程中的关键事件:

阶段1:初期(0-50B tokens)

现象:
- 损失快速下降
- 困惑度从1000+降到100左右
- 模型开始学会基本语法

挑战:
- 训练不稳定,偶尔出现损失尖峰
- 需要仔细监控,及时调整学习率

解决:
- 使用较长的Warmup(375M tokens)
- 梯度裁剪设为1.0
- 监控梯度范数,发现异常立即回滚

阶段2:中期(50B-200B tokens)

现象:
- 损失稳定下降
- 困惑度降到30-40
- 模型开始展现few-shot能力

挑战:
- 训练速度变慢(损失下降变缓)
- 需要更多数据才能继续提升

解决:
- 保持学习率稳定(Cosine Decay的平台期)
- 增加数据多样性

阶段3:后期(200B-300B tokens)

现象:
- 损失缓慢下降
- 困惑度降到20左右
- 模型能力接近饱和

挑战:
- 边际收益递减
- 需要权衡训练成本和性能提升

决策:
- 在300B tokens停止训练
- 学习率已衰减到最小值

训练失败案例:

案例1:学习率过大导致训练崩溃

时间:训练初期(约10B tokens)
现象:
- 损失突然从3.5跳到100+
- 梯度范数爆炸(>1000)
- 模型输出全是乱码

原因:
- Warmup不足,学习率增长太快
- 某个batch的数据质量差,梯度异常

解决:
- 回滚到崩溃前的checkpoint
- 延长Warmup时间(从100M增加到375M tokens)
- 加强数据过滤

案例2:混合精度导致数值溢出

时间:训练中期(约150B tokens)
现象:
- 损失变成NaN
- 部分参数变成inf
- 训练无法继续

原因:
- FP16范围有限(±65504)
- 某些激活值过大,超出FP16范围

解决:
- 增加损失缩放系数(从1000增加到2000)
- 在关键层使用FP32(LayerNorm, Softmax)
- 监控激活值范围,及时调整

最终结果:

训练损失:约2.0
验证困惑度:约20
Zero-shot性能:显著提升
Few-shot性能:接近人类水平(某些任务)

关键发现:
1. 规模效应:模型越大,few-shot能力越强
2. 数据质量:高质量数据比数据量更重要
3. 训练稳定性:大模型训练需要极其小心

6.2 LLaMA-2 7B训练配置深度分析

为什么选择这些超参数?

学习率:3e-4(比GPT-3的6e-5大5倍)

原因:
1. 模型更小(7B vs 175B)
   - 小模型可以承受更大的学习率
   - 大模型需要更小心,避免不稳定

2. 数据更多(2T vs 300B tokens)
   - 更多数据需要更快的学习速度
   - 否则训练时间过长

3. 优化器配置不同
   - β2 = 0.95(vs GPT-3的0.95)
   - 更激进的二阶矩估计

经验法则:
- 1B模型:学习率约 1e-3
- 7B模型:学习率约 3e-4
- 70B模型:学习率约 1e-4
- 175B模型:学习率约 6e-5

Warmup:2000步(vs GPT-3的375M tokens)

为什么用"步数"而不是"tokens"?
- LLaMA-2论文用步数更方便
- 2000步 × 4M tokens/步 = 8B tokens
- 占总训练量的0.4%(2T tokens)

为什么比GPT-3少?
- GPT-3:375M / 300B = 0.125%
- LLaMA-2:8B / 2T = 0.4%
- 实际上LLaMA-2的Warmup比例更大!

原因:
- 更大的学习率需要更长的Warmup
- 确保训练初期稳定

Batch size:4M tokens(vs GPT-3的3.2M)

为什么更大?
1. 更多GPU(2048 vs 估计1000+)
   - 更大的batch size可以充分利用GPU

2. 更稳定的训练
   - 大batch size减少梯度噪声
   - 有助于大学习率的稳定性

3. 更快的收敛
   - 大batch size意味着更少的更新步数
   - 2T tokens / 4M = 500K步

权衡:
- 优点:训练稳定,收敛快
- 缺点:可能影响泛化能力(但影响不大)

并行策略:256×8(数据并行×张量并行)

为什么这样配置?

数据并行256路:
- 每路处理 4M / 256 = 16K tokens
- 每路8个GPU,每个GPU处理 16K / 8 = 2K tokens
- 序列长度4096,每个GPU处理0.5个样本
- 需要梯度累积2步

张量并行8路:
- 7B模型,每层约200M参数
- 8个GPU分摊,每个GPU约25M参数
- 单个A100 80GB可以轻松容纳
- 通信开销可控(8路不算多)

为什么不用更多张量并行?
- 16路:通信开销翻倍,收益递减
- 4路:每个GPU负担过重,内存紧张
- 8路:最佳平衡点

为什么不用流水线并行?
- 7B模型不够大,不需要流水线
- 流水线并行适合100B+模型

权重衰减:0.1(和GPT-3相同)

为什么是0.1?
- 经验值,广泛验证有效
- 太小(0.01):正则化不足,可能过拟合
- 太大(0.5):正则化过度,欠拟合

如何选择?
1. 从0.1开始
2. 观察训练/验证损失的差距
3. 差距大:增加权重衰减
4. 差距小:减少权重衰减

6.3 训练失败案例集锦

案例1:学习率选择不当

场景:训练一个3B模型
配置:学习率 = 1e-3(太大)

现象:
- 前100步:损失从8.0降到4.0(看起来很好)
- 100-200步:损失在3.5-4.5之间震荡
- 200步后:损失突然跳到10+,训练崩溃

诊断:
- 学习率过大,步子太大
- 初期运气好,没有遇到陡峭的梯度
- 后期遇到陡峭区域,一步跳出了最优区域

解决:
- 降低学习率到3e-4
- 增加Warmup步数(从1000增加到5000)
- 重新训练,稳定收敛

案例2:Warmup不足

场景:训练一个7B模型
配置:Warmup = 100步(太少)

现象:
- 前10步:损失从8.0降到6.0
- 10-50步:损失剧烈震荡(5.0-7.0)
- 50步后:损失稳定下降,但收敛速度慢

诊断:
- Warmup太短,学习率增长太快
- 初期梯度不稳定,导致参数震荡
- 虽然没有崩溃,但浪费了很多步数

解决:
- 增加Warmup到2000步
- 损失曲线变得平滑
- 收敛速度提升20%

案例3:梯度累积配置错误

场景:训练一个7B模型
配置:
- 目标batch size = 4M tokens
- 每个GPU = 4096 tokens
- GPU数量 = 1024
- 梯度累积 = 1步

问题:
- 实际batch size = 4096 × 1024 = 4.2M tokens ✓
- 但忘记在loss.backward()前除以accumulation_steps

现象:
- 损失下降非常慢
- 梯度范数异常大(>10)
- 训练效率低

诊断:
- 梯度累积时,每步的梯度都是完整的
- 累积N步后,梯度是N倍
- 如果不除以N,相当于学习率放大了N倍

解决:
- 在backward前:loss = loss / accumulation_steps
- 或在optimizer中调整学习率

案例4:混合精度的损失缩放问题

场景:训练一个13B模型
配置:FP16 + 损失缩放1000

现象:
- 前1000步:正常训练
- 1000步后:损失变成NaN
- 检查发现某些梯度变成inf

诊断:
- 损失缩放1000,梯度也放大1000倍
- 某些梯度本身就很大(如Embedding层)
- 放大后超出FP16范围(±65504)

解决:
- 使用动态损失缩放(GradScaler自动调整)
- 或降低初始缩放系数到512
- 监控梯度范围,及时调整

第七部分:故障排查完全指南

这一部分解决什么问题?

核心问题:训练大模型时会遇到各种问题,如何快速诊断和解决?

解决方案:系统化的故障排查流程和解决方案。

价值:

  • 节省调试时间
  • 避免重复踩坑
  • 提高训练成功率

7.1 梯度问题诊断与解决

问题1:梯度爆炸

识别特征:

# 监控梯度范数
total_norm = 0
for p in model.parameters():
    if p.grad is not None:
        param_norm = p.grad.data.norm(2)
        total_norm += param_norm.item() ** 2
total_norm = total_norm ** 0.5
 
print(f"梯度范数: {total_norm:.2f}")
 
# 判断标准
if total_norm > 100:
    print("⚠️ 梯度爆炸!")
elif total_norm > 10:
    print("⚠️ 梯度偏大,需要注意")
else:
    print("✅ 梯度正常")

常见原因:

1. 学习率过大
   - 症状:梯度范数>1000,损失突然跳到很大
   - 解决:降低学习率(减半或1/10)

2. 初始化不当
   - 症状:第一步就梯度爆炸
   - 解决:使用Xavier或He初始化

3. 数据异常
   - 症状:某些batch梯度特别大
   - 解决:检查数据,过滤异常样本

4. 网络结构问题
   - 症状:某些层的梯度特别大
   - 解决:添加LayerNorm,使用残差连接

解决方案:

# 方案1:梯度裁剪(最常用)
torch.nn.utils.clip_grad_norm_(
    model.parameters(),
    max_norm=1.0  # GPT-3使用1.0
)
 
# 方案2:降低学习率
optimizer = AdamW(model.parameters(), lr=3e-4)  # 原来6e-4
 
# 方案3:增加Warmup
scheduler = CosineWarmupScheduler(
    optimizer,
    warmup_steps=5000,  # 原来2000
    total_steps=100000
)
 
# 方案4:检查数据
def check_batch_quality(batch):
    """检查batch是否有异常"""
    # 检查是否有NaN
    if torch.isnan(batch['input_ids']).any():
        print("⚠️ 数据包含NaN")
        return False
 
    # 检查数值范围
    if batch['input_ids'].max() > vocab_size:
        print("⚠️ Token ID超出词表范围")
        return False
 
    return True

问题2:梯度消失

识别特征:

# 监控每层的梯度
for name, param in model.named_parameters():
    if param.grad is not None:
        grad_norm = param.grad.data.norm(2).item()
        print(f"{name}: {grad_norm:.6f}")
 
        if grad_norm < 1e-6:
            print(f"⚠️ {name} 梯度消失!")

常见原因:

1. 网络太深
   - 症状:底层梯度接近0
   - 解决:使用残差连接(Transformer已有)

2. 激活函数问题
   - 症状:Sigmoid/Tanh导致梯度消失
   - 解决:使用ReLU/GELU(Transformer已用GELU)

3. 学习率过小
   - 症状:所有层梯度都很小,损失下降极慢
   - 解决:增大学习率

4. 初始化问题
   - 症状:第一步梯度就很小
   - 解决:调整初始化方差

解决方案:

# 方案1:检查残差连接
class TransformerBlock(nn.Module):
    def forward(self, x):
        # 确保有残差连接
        x = x + self.attention(x)  # ✅
        x = x + self.ffn(x)        # ✅
        return x
 
# 方案2:增大学习率
optimizer = AdamW(model.parameters(), lr=1e-3)  # 原来3e-4
 
# 方案3:检查初始化
def check_initialization(model):
    """检查模型初始化是否合理"""
    for name, param in model.named_parameters():
        std = param.data.std().item()
        print(f"{name}: std={std:.6f}")
 
        if std < 1e-4:
            print(f"⚠️ {name} 初始化方差过小")
        elif std > 1.0:
            print(f"⚠️ {name} 初始化方差过大")

7.2 训练不稳定问题处理

问题1:损失突然上升

诊断流程:

def diagnose_loss_spike(loss_history, current_step):
    """诊断损失突然上升的原因"""
    # 1. 检查是否是损失尖峰
    recent_losses = loss_history[-100:]
    avg_loss = sum(recent_losses) / len(recent_losses)
    current_loss = loss_history[-1]
 
    if current_loss > avg_loss * 2:
        print(f"⚠️ 损失尖峰!当前: {current_loss:.4f}, 平均: {avg_loss:.4f}")
 
        # 2. 检查梯度
        total_norm = compute_grad_norm(model)
        if total_norm > 100:
            print("原因:梯度爆炸")
            return "gradient_explosion"
 
        # 3. 检查学习率
        current_lr = scheduler.get_last_lr()[0]
        if current_lr > 1e-3:
            print("原因:学习率过大")
            return "lr_too_high"
 
        # 4. 检查数据
        if not check_batch_quality(current_batch):
            print("原因:数据异常")
            return "bad_data"
 
    return "unknown"

解决方案:

# 方案1:回滚到之前的checkpoint
def rollback_to_checkpoint(model, optimizer, checkpoint_path):
    """回滚到之前的checkpoint"""
    checkpoint = torch.load(checkpoint_path)
    model.load_state_dict(checkpoint['model'])
    optimizer.load_state_dict(checkpoint['optimizer'])
    print(f"✅ 已回滚到步数 {checkpoint['step']}")
 
# 方案2:降低学习率
for param_group in optimizer.param_groups:
    param_group['lr'] *= 0.5
print(f"✅ 学习率降低到 {optimizer.param_groups[0]['lr']:.6f}")
 
# 方案3:跳过异常batch
if not check_batch_quality(batch):
    print("⚠️ 跳过异常batch")
    continue

问题2:困惑度震荡

现象:

步数    困惑度
1000    45.2
2000    42.8
3000    46.1  ← 上升了
4000    43.5
5000    47.3  ← 又上升了

诊断:

def diagnose_ppl_oscillation(ppl_history):
    """诊断困惑度震荡"""
    # 计算震荡幅度
    recent_ppl = ppl_history[-10:]
    ppl_std = np.std(recent_ppl)
    ppl_mean = np.mean(recent_ppl)
 
    oscillation_ratio = ppl_std / ppl_mean
 
    if oscillation_ratio > 0.1:
        print(f"⚠️ 困惑度震荡严重: {oscillation_ratio:.2%}")
 
        # 可能原因
        print("可能原因:")
        print("1. 学习率过大")
        print("2. Batch size过小")
        print("3. 数据分布不均匀")

解决方案:

# 方案1:降低学习率
optimizer = AdamW(model.parameters(), lr=1e-4)  # 原来3e-4
 
# 方案2:增大batch size
# 通过梯度累积实现
accumulation_steps = 16  # 原来8
 
# 方案3:数据shuffle
dataloader = DataLoader(
    dataset,
    batch_size=32,
    shuffle=True,  # 确保开启shuffle
    sampler=DistributedSampler(dataset, shuffle=True)
)

7.3 内存优化方案

问题:GPU内存溢出(OOM)

诊断:

def diagnose_memory_usage():
    """诊断内存使用情况"""
    # 获取当前内存使用
    allocated = torch.cuda.memory_allocated() / 1024**3  # GB
    reserved = torch.cuda.memory_reserved() / 1024**3    # GB
    max_allocated = torch.cuda.max_memory_allocated() / 1024**3
 
    print(f"当前分配: {allocated:.2f} GB")
    print(f"当前保留: {reserved:.2f} GB")
    print(f"峰值分配: {max_allocated:.2f} GB")
 
    # 分析内存占用
    print("\n内存占用分析:")
    print(f"模型参数: {get_model_memory(model):.2f} GB")
    print(f"优化器状态: {get_optimizer_memory(optimizer):.2f} GB")
    print(f"梯度: {get_gradient_memory(model):.2f} GB")
    print(f"激活值: {allocated - get_model_memory(model) - get_gradient_memory(model):.2f} GB")
 
def get_model_memory(model):
    """计算模型参数占用的内存"""
    param_size = 0
    for param in model.parameters():
        param_size += param.nelement() * param.element_size()
    return param_size / 1024**3
 
def get_optimizer_memory(optimizer):
    """计算优化器状态占用的内存"""
    # Adam: 2倍参数量(m和v)
    return get_model_memory(model) * 2
 
def get_gradient_memory(model):
    """计算梯度占用的内存"""
    grad_size = 0
    for param in model.parameters():
        if param.grad is not None:
            grad_size += param.grad.nelement() * param.grad.element_size()
    return grad_size / 1024**3

解决方案1:梯度检查点(Gradient Checkpointing)

from torch.utils.checkpoint import checkpoint
 
class TransformerBlockWithCheckpoint(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.attention = Attention(config)
        self.ffn = FeedForward(config)
 
    def forward(self, x):
        # 使用checkpoint,不保存中间激活值
        x = x + checkpoint(self.attention, x)
        x = x + checkpoint(self.ffn, x)
        return x
 
# 效果:
# - 内存减少:50-70%
# - 速度降低:20-30%(需要重新计算)
# - 适合:内存紧张时使用

解决方案2:减小batch size

# 原配置
batch_size = 32
accumulation_steps = 1
 
# 新配置(保持有效batch size不变)
batch_size = 16  # 减半
accumulation_steps = 2  # 翻倍
 
# 效果:
# - 内存减少:约30-40%
# - 速度影响:几乎没有

解决方案3:减小序列长度

# 原配置
seq_length = 4096
 
# 新配置
seq_length = 2048  # 减半
 
# 效果:
# - 内存减少:约40-50%
# - 注意:会影响模型能力(长文本处理)

解决方案4:使用更高效的注意力

# 原始注意力:O(n²)内存
def standard_attention(Q, K, V):
    scores = Q @ K.T  # [seq_len, seq_len] 占用大量内存
    attn = softmax(scores)
    return attn @ V
 
# Flash Attention:O(n)内存
from flash_attn import flash_attn_func
 
def flash_attention(Q, K, V):
    # 内存高效的实现
    return flash_attn_func(Q, K, V)
 
# 效果:
# - 内存减少:50-70%(长序列)
# - 速度提升:2-4倍
# - 需要:安装flash-attn库

7.4 学习率选择指南

方法1:学习率范围测试(LR Range Test)

def lr_range_test(model, dataloader, min_lr=1e-7, max_lr=1e-2, num_steps=1000):
    """
    学习率范围测试
 
    原理:
    - 从很小的学习率开始
    - 指数增长到很大的学习率
    - 记录每个学习率对应的损失
    - 找到损失下降最快的学习率
    """
    model.train()
    optimizer = AdamW(model.parameters(), lr=min_lr)
 
    lr_schedule = np.logspace(np.log10(min_lr), np.log10(max_lr), num_steps)
    losses = []
    lrs = []
 
    for step, batch in enumerate(dataloader):
        if step >= num_steps:
            break
 
        # 设置当前学习率
        current_lr = lr_schedule[step]
        for param_group in optimizer.param_groups:
            param_group['lr'] = current_lr
 
        # 训练一步
        loss = model(batch)
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()
 
        # 记录
        losses.append(loss.item())
        lrs.append(current_lr)
 
        if step % 100 == 0:
            print(f"步数 {step}, LR: {current_lr:.6f}, Loss: {loss.item():.4f}")
 
    # 找到最佳学习率
    # 方法1:损失最小的点
    best_idx = np.argmin(losses)
    best_lr_v1 = lrs[best_idx]
 
    # 方法2:损失下降最快的点(推荐)
    gradients = np.gradient(losses)
    best_idx = np.argmin(gradients)
    best_lr_v2 = lrs[best_idx]
 
    print(f"\n推荐学习率:")
    print(f"方法1(损失最小): {best_lr_v1:.6f}")
    print(f"方法2(下降最快): {best_lr_v2:.6f}")
    print(f"建议使用: {best_lr_v2 / 10:.6f} (最佳值的1/10)")
 
    return lrs, losses
 
# 使用示例
lrs, losses = lr_range_test(model, dataloader)
 
# 可视化(如果有matplotlib)
# plt.plot(lrs, losses)
# plt.xscale('log')
# plt.xlabel('Learning Rate')
# plt.ylabel('Loss')
# plt.show()

方法2:根据模型大小选择

def recommend_lr_by_model_size(num_params):
    """
    根据模型大小推荐学习率
 
    经验法则(AdamW):
    - 100M-1B: 1e-3
    - 1B-10B: 3e-4
    - 10B-100B: 1e-4
    - 100B+: 6e-5
    """
    if num_params < 1e9:  # <1B
        return 1e-3
    elif num_params < 10e9:  # 1B-10B
        return 3e-4
    elif num_params < 100e9:  # 10B-100B
        return 1e-4
    else:  # >100B
        return 6e-5
 
# 使用示例
num_params = sum(p.numel() for p in model.parameters())
recommended_lr = recommend_lr_by_model_size(num_params)
print(f"模型参数量: {num_params / 1e9:.2f}B")
print(f"推荐学习率: {recommended_lr:.6f}")

方法3:根据batch size调整

def adjust_lr_by_batch_size(base_lr, base_batch_size, actual_batch_size):
    """
    根据batch size调整学习率
 
    线性缩放规则:
    - batch size翻倍,学习率也翻倍
    - 但有上限,避免过大
 
    例如:
    - 基准:batch_size=256, lr=3e-4
    - 实际:batch_size=1024
    - 调整:lr = 3e-4 * (1024/256) = 1.2e-3
    """
    scale = actual_batch_size / base_batch_size
    adjusted_lr = base_lr * scale
 
    # 设置上限(避免过大)
    max_lr = base_lr * 4
    adjusted_lr = min(adjusted_lr, max_lr)
 
    print(f"基准batch size: {base_batch_size}, LR: {base_lr:.6f}")
    print(f"实际batch size: {actual_batch_size}, LR: {adjusted_lr:.6f}")
 
    return adjusted_lr
 
# 使用示例
base_lr = 3e-4
base_batch_size = 256
actual_batch_size = 1024
 
adjusted_lr = adjust_lr_by_batch_size(base_lr, base_batch_size, actual_batch_size)

Warmup步数选择:

def recommend_warmup_steps(total_steps, lr):
    """
    推荐Warmup步数
 
    经验法则:
    - 总步数的0.1%-1%
    - 学习率越大,Warmup越长
    """
    # 基础比例
    if lr >= 1e-3:
        ratio = 0.01  # 1%
    elif lr >= 3e-4:
        ratio = 0.005  # 0.5%
    else:
        ratio = 0.001  # 0.1%
 
    warmup_steps = int(total_steps * ratio)
 
    # 设置下限和上限
    warmup_steps = max(warmup_steps, 100)  # 至少100步
    warmup_steps = min(warmup_steps, 10000)  # 最多10000步
 
    print(f"总步数: {total_steps}")
    print(f"学习率: {lr:.6f}")
    print(f"推荐Warmup: {warmup_steps}步 ({ratio*100:.2f}%)")
 
    return warmup_steps
 
# 使用示例
total_steps = 100000
lr = 3e-4
warmup_steps = recommend_warmup_steps(total_steps, lr)

第八部分:深度数学推导

这一部分解决什么问题?

核心问题:为什么这些公式是这样的?背后的数学原理是什么?

解决方案:从信息论、概率论、优化理论角度深入理解。

价值:

  • 理解”为什么”而不只是”是什么”
  • 能够自己推导和改进算法
  • 面试和研究必备

8.1 Softmax的信息论解释

问题:为什么用exp而不是其他函数?

从最大熵原理推导:

假设我们有一个概率分布 ,需要满足:

  1. 所有概率非负:
  2. 概率和为1:
  3. 期望得分等于观测得分:

目标:在满足约束的前提下,找到熵最大的分布(最不确定的分布)

熵的定义:

拉格朗日函数:

求导并令其为0:

解得:

应用约束 :

最终得到:

令 (温度参数),就得到了Softmax公式!

关键洞察

Softmax是在给定约束下,熵最大的概率分布。这意味着它是”最不偏向任何选项”的分布,只根据得分来决定概率。

为什么不用其他函数?

尝试1:线性归一化

问题:

  • 无法处理负数
  • 不满足最大熵原理
  • 无法放大差异

尝试2:平方归一化

问题:

  • 负数变正数,丢失了符号信息
  • 不满足最大熵原理

尝试3:Sigmoid

问题:

  • 概率和不为1
  • 只适合二分类

结论:只有exp函数满足最大熵原理,且能处理任意实数。

8.2 交叉熵的深度推导

从KL散度推导交叉熵

KL散度定义(衡量两个分布的差异):

其中:

  • :真实分布
  • :模型预测分布

展开:

其中:

  • :真实分布的熵(常数)
  • :交叉熵

最小化KL散度:

由于 是常数,等价于:

在语言模型中:

  • 真实分布 :one-hot编码,,其他为0
  • 模型分布 :模型预测的概率分布

交叉熵简化为:

这就是我们使用的交叉熵损失!

关键洞察

最小化交叉熵 = 最小化KL散度 = 让模型分布尽可能接近真实分布。

为什么用log?

从信息论角度:

信息量的定义:

含义:

  • 概率越小的事件,信息量越大
  • 概率为1的事件,信息量为0

例子:

事件1:"太阳从东边升起" → P=1 → I=0(没有信息)
事件2:"明天下雨" → P=0.5 → I=1(有一些信息)
事件3:"中彩票" → P=0.0001 → I=13.3(信息量很大)

交叉熵的含义:

即:在真实分布P下,使用模型Q编码所需的平均信息量(比特数)。

最小化交叉熵 = 最小化编码长度 = 最优压缩

8.3 Adam的数学原理

问题:为什么Adam有效?

回顾Adam更新规则:

# 一阶矩(梯度的指数移动平均)
m_t = β1 × m_{t-1} + (1 - β1) × g_t
 
# 二阶矩(梯度平方的指数移动平均)
v_t = β2 × v_{t-1} + (1 - β2) × g_t²
 
# 偏差修正
m_hat = m_t / (1 - β1^t)
v_hat = v_t / (1 - β2^t)
 
# 参数更新
θ_t = θ_{t-1} - η × m_hat / (√v_hat + ε)

为什么需要一阶矩(动量)?

问题:梯度下降容易震荡

损失曲面(2D示例):
    ↑
    |     ╱╲╱╲╱╲
    |    ╱  ╲  ╲
    |   ╱    ╲  ╲
    |  ╱      ╲  ╲___
    | ╱        ╲      ╲___
    |╱__________╲__________╲___
    └────────────────────────→

梯度下降路径:
  起点 → ↘ ↗ ↘ ↗ ↘ ↗ → 终点
  (震荡,收敛慢)

动量路径:
  起点 → ↘ → → → → → 终点
  (平滑,收敛快)

数学解释:

一阶矩 是梯度的指数移动平均:

当 时,相当于对过去10步的梯度加权平均:

  • 当前梯度:权重 0.1
  • 1步前:权重 0.09
  • 2步前:权重 0.081
  • …

效果:

  • 如果梯度方向一致: 累积,加速收敛
  • 如果梯度方向震荡: 抵消,减少震荡

为什么需要二阶矩(自适应学习率)?

问题:不同参数需要不同学习率

参数1:梯度大(变化快)→ 需要小学习率
参数2:梯度小(变化慢)→ 需要大学习率

解决:用梯度的历史方差来调整学习率

  • 如果 大(梯度历史上很大):有效学习率小
  • 如果 小(梯度历史上很小):有效学习率大

为什么需要偏差修正?

问题:初始时 ,导致初期估计偏小

例子:

真实梯度:g = 1.0
β1 = 0.9

步数1:m_1 = 0.9 × 0 + 0.1 × 1.0 = 0.1(偏小!真实应该是1.0)
步数2:m_2 = 0.9 × 0.1 + 0.1 × 1.0 = 0.19(还是偏小)
步数3:m_3 = 0.9 × 0.19 + 0.1 × 1.0 = 0.271(逐渐接近)
...

偏差修正:

步数1:m_hat_1 = 0.1 / (1 - 0.9^1) = 0.1 / 0.1 = 1.0 ✅
步数2:m_hat_2 = 0.19 / (1 - 0.9^2) = 0.19 / 0.19 = 1.0 ✅

Adam vs 其他优化器:

优化器动量自适应LR偏差修正适用场景
SGD❌❌❌简单任务
Momentum✅❌❌凸优化
RMSprop❌✅❌RNN训练
Adam✅✅✅大模型训练
AdamW✅✅✅ + 权重衰减大模型预训练

8.4 Warmup的数学解释

问题:为什么初期需要小学习率?

从梯度方差的角度分析:

训练初期,参数是随机初始化的,梯度的方差很大:

初期:

  • 模型输出接近随机
  • 不同batch的梯度差异很大
  • 很大

后期:

  • 模型已经学到一些规律
  • 不同batch的梯度比较一致
  • 较小

梯度方差对训练的影响:

参数更新:

期望更新方向:

更新方差:

如果学习率太大:

Warmup的作用:

  • 初期:小学习率 → 小更新方差 → 稳定
  • 后期:大学习率 → 快速收敛

最优Warmup步数的理论分析:

根据经验和理论分析,最优Warmup步数约为:

其中:

  • :模型维度(参数量)
  • :最大学习率
  • :batch size

例子:

模型:7B参数
学习率:3e-4
Batch size:4M tokens

T_warmup ≈ 7e9 / ((3e-4)^2 × 4e6)
        ≈ 7e9 / 3.6e-1
        ≈ 2e10 步?(太大了)

实际:使用经验值 0.5% × 总步数 ≈ 2000步

理论公式给出的是上界,实际使用经验值即可。


🧪 自我检验:20道思考题

基础题(检验概念理解)

  1. 什么是预训练?它和微调有什么区别?

  2. 为什么预训练的任务是”预测下一个词”?

  3. Softmax函数的作用是什么?为什么不用简单归一化?

  4. 什么是温度参数(Temperature)?它如何影响输出?

  5. 什么是损失函数?它衡量什么?

  6. 为什么损失函数用对数(log)?

  7. 什么是困惑度(PPL)?它和损失的关系是什么?

中等题(检验原理理解)

  1. 从”今天天气很”到预测”好”,完整的计算流程是什么?

  2. 交叉熵损失函数的公式是什么?如何计算?

  3. 为什么需要优化器?简单的梯度下降有什么问题?

  4. Adam优化器的核心思想是什么?它解决了什么问题?

  5. 什么是学习率调度?为什么需要Warmup?

  6. 为什么需要分布式训练?单GPU有什么限制?

  7. 数据并行和模型并行有什么区别?各适合什么场景?

  8. 什么是混合精度训练?它如何加速训练?

进阶题(检验应用理解)

  1. 手算一个Softmax例子:得分[2.0, 1.0, 0.5],计算概率分布。

  2. 给定预测概率P(“好”)=0.6,计算损失和困惑度。

  3. 为什么FP16会导致数值不稳定?如何解决?

  4. 什么是梯度累积?它如何模拟大batch size?

  5. 如果你要训练一个7B模型,需要多少GPU?如何配置并行策略?


📚 参考答案

基础题答案

  1. 预训练和微调的区别

    • 预训练:在海量无标注文本上学习”预测下一个词”,让模型学会语言的基本规律。数据量大(TB级),时间长(数周到数月),成本高(数百万美元)。结果是Base Model,会续写文本。
    • 微调:在少量标注数据上学习特定任务(如对话、翻译),让模型学会任务规律。数据量小(GB级),时间短(数小时到数天),成本低(数千到数万美元)。结果是Chat Model,会对话。
  2. 为什么是”预测下一个词” 三个原因:

    • 不需要标注:文本本身就是标签,成本几乎为0
    • 任务自然:人类也是这样学语言的(大量阅读)
    • 迫使理解:要预测下一个词,模型必须理解语法、语义、常识
  3. Softmax的作用 把任意实数得分转换成概率分布(和为1,都是正数)。简单归一化无法处理负数,也无法放大差异。Softmax通过exp函数放大差异,让重要的值更突出。

  4. 温度参数的作用 控制输出的”确定性”:

    • 低温(T<1):概率更集中,输出更确定(适合代码生成、翻译)
    • 高温(T>1):概率更平均,输出更随机(适合创作、头脑风暴)
    • 标准(T=1):正常分布
  5. 损失函数的作用 衡量模型预测和真实答案的差距。损失越小,预测越准确。模型的目标是最小化损失。

  6. 为什么用对数 三个原因:

    • 数值稳定:概率很小时(如0.001),直接用容易下溢,用-log(0.001)=6.91更稳定
    • 乘法变加法:多个位置的联合概率是乘法,取对数后变加法,更稳定
    • 梯度更好:预测很差时梯度大,快速改进;预测很好时梯度小,避免过度调整
  7. 困惑度和损失的关系 PPL = e

中等题答案

  1. 完整的计算流程

    输入:"今天天气很"
    → 分词:["今天", "天气", "很"] → [1234, 5678, 9012]
    → Embedding:每个ID变成512维向量
    → 位置编码:加上位置信息
    → Transformer:Self-Attention + FFN处理
    → 取最后位置的输出向量(512维)
    → 线性层:映射到词表大小(50000维)
    → Softmax:转换成概率分布
    → 选择概率最高的词:7890 → "好"
    
  2. 交叉熵损失公式

    计算步骤:

    • 对每个位置,看模型预测正确词的概率
    • 取对数,加负号
    • 所有位置平均

    例如:P(“天气”)=0.9, P(“很”)=0.7, P(“好”)=0.8 损失 = -(log(0.9) + log(0.7) + log(0.8)) / 3 = 0.228

  3. 简单梯度下降的问题

    • 学习率难选:太大震荡,太小收敛慢
    • 所有参数用同一个学习率:不同参数需要不同学习率
    • 容易卡在鞍点:梯度为0就停止更新

    Adam解决:自适应调整每个参数的学习率,考虑历史梯度(动量)。

  4. Adam的核心思想 两个关键机制:

    • 动量(Momentum):记住过去的梯度方向,避免震荡
    • 自适应学习率:梯度大的参数用小学习率,梯度小的参数用大学习率

    AdamW = Adam + 权重衰减,防止过拟合。

  5. 学习率调度和Warmup 学习率调度:训练初期用大学习率快速接近最优点,后期用小学习率精细调整。常用Cosine Decay。

    Warmup:前N步学习率从0线性增加到最大值。作用是避免初期梯度不稳定导致的震荡。通常Warmup步数是总步数的0.1%-1%。

  6. 分布式训练的必要性 单GPU限制:

    • 内存:7B模型需要132GB(模型28GB + 优化器56GB + 梯度28GB + 激活20GB),但A100只有80GB
    • 时间:单GPU训练1T tokens需要31年

    解决:用多个GPU并行训练,加速并突破内存限制。

  7. 数据并行 vs 模型并行

    • 数据并行:每个GPU有完整模型副本,处理不同数据。优点是实现简单,加速明显。缺点是每个GPU需要存完整模型,大模型放不下。适合中小模型(<10B)。
    • 模型并行:把模型切分到多个GPU。优点是可以训练超大模型。缺点是通信开销大,实现复杂。适合大模型(>10B)。
  8. 混合精度训练 用FP16(16位)代替FP32(32位)进行计算。优点:内存减少50%,速度提升2-3倍。问题:FP16范围小,容易溢出或下溢。解决:前向和反向用FP16(快),参数更新用FP32(精确),损失缩放避免下溢。

进阶题答案

  1. 手算Softmax 得分:[2.0, 1.0, 0.5]

    步骤1:计算e

    • e^2.0 = 7.39
    • e^1.0 = 2.72
    • e^0.5 = 1.65
    • 总和 = 11.76

    步骤2:除以总和

    • P(1) = 7.39 / 11.76 = 0.63 (63%)
    • P(2) = 2.72 / 11.76 = 0.23 (23%)
    • P(3) = 1.65 / 11.76 = 0.14 (14%)
  2. 计算损失和困惑度 P(“好”) = 0.6

    损失 = -log(0.6) = 0.511 困惑度 = e^0.511 = 1.67

    含义:模型平均在1.67个词中犹豫,预测比较准确。

  3. FP16的数值不稳定 FP16范围小(±6.5×10

    • 太大的数溢出(变成inf)
    • 太小的数下溢(变成0),如梯度0.00001 → 0

    解决:

    • 损失缩放:损失×1000 → 梯度×1000,避免下溢,更新时再除以1000
    • 主权重:用FP32存储参数的”主副本”,FP16只用于计算
  4. 梯度累积 把大batch切成多个小batch,累积梯度后再更新。

    例如:想要batch size=512,但GPU只能放64

    • 前向+反向(batch 0-63),计算梯度,不更新
    • 前向+反向(batch 64-127),累积梯度
    • 重复8次
    • 用累积的梯度更新参数

    效果:模拟了batch size=512,但内存只需要64的大小。

  5. 训练7B模型的配置 参考LLaMA-2 7B:

    • GPU数量:2048 × A100 80GB
    • 并行策略:数据并行256路 × 张量并行8路
    • 全局batch size:4M tokens
    • 每个GPU:2048 tokens(约0.5个样本,需要梯度累积2步)
    • 优化器:AdamW,学习率3e-4
    • 混合精度:FP16 + FP32主权重
    • 训练时间:约3周(2T tokens)

📚 核心要点总结

一句话总结

预训练通过让模型在海量文本上学习"预测下一个词",让模型掌握语言的基本规律。核心是交叉熵损失函数 + Adam优化器 + 学习率调度 + 分布式训练。

关键概念

预训练流程:

  1. 任务:预测下一个词(自监督学习)
  2. 数据:海量无标注文本(TB级)
  3. 模型:Transformer架构
  4. 损失:交叉熵(衡量预测和真实的差距)
  5. 优化:AdamW + Cosine Decay + Warmup
  6. 训练:数据并行 + 模型并行 + 混合精度

核心公式:

Softmax:

交叉熵损失:

困惑度:

Adam更新:

实际训练参数(GPT-3)

模型:175B参数
数据:300B tokens
GPU:10000+ × V100
训练时间:数月

优化器:AdamW
  β1 = 0.9, β2 = 0.95
  权重衰减 = 0.1

学习率:6e-5
  Warmup:375M tokens
  Cosine Decay到6e-6

Batch size:3.2M tokens
混合精度:FP16 + FP32
梯度裁剪:1.0

下一步学习

完成预训练后,模型只会”续写”,还需要:

  1. 指令微调(SFT):教模型如何回答问题
  2. 人类反馈强化学习(RLHF):让回答更符合人类偏好
  3. 部署优化:量化、推理加速

🎓 延伸阅读

论文:

  1. Attention Is All You Need (2017) - Transformer原论文
  2. Language Models are Unsupervised Multitask Learners (2019) - GPT-2
  3. Language Models are Few-Shot Learners (2020) - GPT-3
  4. Training Compute-Optimal Large Language Models (2022) - Chinchilla定律

博客:

  1. The Illustrated Transformer - Jay Alammar
  2. How GPT-3 Works - Visualizations and Animations
  3. Scaling Laws for Neural Language Models - OpenAI

代码:

  1. nanoGPT - Andrej Karpathy(最简洁的GPT实现)
  2. Megatron-LM - NVIDIA(大规模训练框架)
  3. DeepSpeed - Microsoft(分布式训练优化)