什么是预训练?
在上一章中,我们学习了训练的基本概念:前向传播、反向传播、梯度下降。现在让我们聚焦于大模型训练的第一阶段:预训练(Pre-training)。
定义
预训练(Pre-training)是指在大规模无标注文本数据上,通过自监督学习让模型学习语言的基础知识。
类比:
预训练就像一个人的基础教育:
- 预训练:小学到高中,学习语文、数学、历史、科学等基础知识(通用能力)
- 微调(Fine-tuning):大学或职业培训,学习专业技能(特定任务)
一个受过良好基础教育的人,可以快速适应各种专业领域; 一个经过充分预训练的模型,可以快速适应各种下游任务。
预训练 vs 传统监督学习
传统监督学习:
- 需要大量标注数据(输入-输出对)
- 针对特定任务训练
- 数据标注成本高
预训练:
- 使用无标注文本数据
- 通过自监督任务学习通用语言知识
- 数据获取成本低(互联网上有海量文本)
| 特性 | 传统监督学习 | 预训练 |
|---|---|---|
| 数据类型 | 标注数据(输入-标签对) | 无标注文本 |
| 数据规模 | 通常较小(万-百万级) | 极大(万亿Token级) |
| 训练目标 | 特定任务(分类、翻译等) | 通用语言理解 |
| 标注成本 | 高(人工标注) | 低(自动获取) |
| 模型能力 | 单一任务 | 多任务通用能力 |
预训练的核心任务:Next Token Prediction
大模型预训练的核心任务非常简单:预测下一个Token(Next Token Prediction)。
任务定义
给定前面的Token序列,预测下一个Token:
举例:
输入:"今天天气很"
- 模型需要预测:下一个Token是"好"的概率、是"差"的概率、是"热"的概率...
为什么这个任务有效?
自监督学习:
- 不需要人工标注
- 文本本身就包含了"答案"(下一个词就是标签)
- 可以利用互联网上的海量文本
学到的能力:
通过预测下一个Token,模型被迫学习:
-
语法:
- "我是"后面应该接名词("学生"、"老师")
- 不应该接动词(❌"跑步")
-
语义:
- "今天天气很"后面应该是天气相关的词("好"、"热"、"冷")
- 不应该是完全无关的词(❌"手机")
-
常识:
- "太阳从"后面是"东方升起"
- "水的沸点是"后面是"100摄氏度"
-
推理:
- "他很饿,所以他"后面可能是"吃饭"
- 需要理解因果关系
-
上下文理解:
- "小明今天没来,他"后面的"他"指的是"小明"
- 需要理解指代关系
具体例子
输入文本:"猫是一种哺乳动物,它们喜欢"
分解为训练样本:
| 输入序列 | 目标Token |
|---|---|
| "猫" | "是" |
| "猫是" | "一" |
| "猫是一" | "种" |
| "猫是一种" | "哺" |
| "猫是一种哺" | "乳" |
| "猫是一种哺乳" | "动" |
| "猫是一种哺乳动" | "物" |
| "猫是一种哺乳动物" | "," |
| "猫是一种哺乳动物," | "它" |
| "猫是一种哺乳动物,它" | "们" |
| "猫是一种哺乳动物,它们" | "喜" |
| "猫是一种哺乳动物,它们喜" | "欢" |
每个位置都是一个训练样本!
一句话可以产生多个训练样本,这就是为什么预训练可以使用海量数据。
数学形式
对于一个长度为 的序列 ,预训练的目标是最大化对数似然:
其中:
- :前面的所有Token
- :给定前文,预测第 个Token的概率
目标:让模型给正确Token的概率尽可能高。
Causal Language Modeling
这种"只看前文,预测下一个词"的方式叫做因果语言模型(Causal Language Modeling, CLM):
- 因果(Causal):只能看"过去",不能看"未来"
- 自回归(Autoregressive):逐个生成Token
实现方式:通过Causal Mask(因果掩码)实现。
在注意力计算中,防止位置 看到位置 的信息:
可视化(4个Token的序列):
看到的Token
t1 t2 t3 t4
预测 t1 [ ✓ ✗ ✗ ✗ ] 只能看t1
t2 [ ✓ ✓ ✗ ✗ ] 可以看t1,t2
t3 [ ✓ ✓ ✓ ✗ ] 可以看t1,t2,t3
t4 [ ✓ ✓ ✓ ✓ ] 可以看t1,t2,t3,t4
这保证了模型在预测 时,只能使用 之前的信息,符合真实生成场景。
预训练数据
预训练的效果很大程度上取决于数据的质量和规模。
数据来源
现代大模型的预训练数据主要来自:
1. 网页数据(Web Pages)
来源:
- Common Crawl:定期抓取全网的公开数据
- C4(Colossal Clean Crawled Corpus):清洗后的Common Crawl
规模:
- GPT-3:约 4100 亿Token
- LLaMA:约 1.4 万亿Token
优点:
- 规模巨大
- 覆盖广泛的主题
缺点:
- 质量参差不齐
- 包含噪声、垃圾内容
- 可能有偏见、错误信息
2. 书籍(Books)
来源:
- BookCorpus:约 11,000本书
- Project Gutenberg:公版书籍
- Books3:更大规模的书籍数据集
优点:
- 文本质量高
- 语言流畅、结构完整
- 包含深度内容和复杂推理
缺点:
- 规模相对较小
- 版权问题
3. 代码(Code)
来源:
- GitHub:公开代码仓库
- StackOverflow:问答数据
规模:
- GPT-3:未包含代码
- Codex/GPT-3.5:大量代码数据
- LLaMA:约 5%是代码
优点:
- 提升模型的代码能力
- 学习逻辑推理
- 提高结构化思维
4. 学术论文(Papers)
来源:
- arXiv:物理、数学、计算机等领域的预印本
- PubMed:医学论文
优点:
- 高质量、专业内容
- 提升科学推理能力
- 包含专业术语和知识
5. 对话数据(Conversations)
来源:
- Reddit:论坛讨论
- StackExchange:问答社区
优点:
- 对话风格的数据
- 提升问答能力
- 包含多样化观点
数据规模对比
| 模型 | 训练Token数 | 主要数据来源 |
|---|---|---|
| GPT-2 | 100 亿 | WebText |
| GPT-3 | 3000 亿 | Common Crawl, Books, Wikipedia |
| LLaMA-1 | 1.4 万亿 | Common Crawl, C4, GitHub, Books, arXiv, Wikipedia |
| LLaMA-2 | 2 万亿 | 公开数据(具体未披露) |
| GPT-4 | 未披露 | 可能 >10 万亿 |
趋势:数据规模不断增大。
数据预处理
原始数据需要清洗和预处理:
1. 去重(Deduplication)
问题:网页数据有大量重复内容(模板、转载等)
解决:
- 精确去重:完全相同的文本
- 近似去重:使用MinHash等算法找相似文本
重要性:去重可以显著提升模型质量,减少记忆效应。
2. 质量过滤(Quality Filtering)
过滤低质量内容:
- 过短或过长的文本
- 重复字符过多(如"aaaaaaaaa")
- 非自然语言(乱码)
- 色情、暴力、仇恨言论
方法:
- 启发式规则(长度、特殊字符比例等)
- 分类器(训练一个质量分类器)
- 困惑度过滤(用语言模型评估流畅度)
3. 敏感内容过滤
过滤:
- 个人隐私信息(PII):邮箱、电话、地址
- 版权内容
- 有害内容
4. Tokenization
将文本转换为Token序列:
"今天天气很好" → [1234, 5678, 9012, 3456, 7890]
常用方法:
- BPE(Byte Pair Encoding):GPT系列
- WordPiece:BERT
- SentencePiece:LLaMA
数据混合(Data Mixing)
不同来源的数据按一定比例混合:
LLaMA的数据混合(简化):
| 数据源 | 占比 | Token数 |
|---|---|---|
| CommonCrawl | 67% | ~1万亿 |
| C4 | 15% | ~2000亿 |
| GitHub | 4.5% | ~630亿 |
| Wikipedia | 4.5% | ~630亿 |
| Books | 4.5% | ~630亿 |
| ArXiv | 2.5% | ~350亿 |
| StackExchange | 2% | ~280亿 |
原则:
- 高质量数据(Books、Wikipedia)可以适当过采样
- 低质量数据(原始网页)需要过滤和降采样
- 代码数据的比例影响代码能力
预训练过程
完整流程
┌───────────────────────────────────────────────────────────┐
│ 1. 数据准备 │
│ - 收集数据(网页、书籍、代码等) │
│ - 清洗过滤(去重、质量过滤) │
│ - Tokenization │
│ - 构建训练数据集 │
└────────────────────┬──────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────┐
│ 2. 模型初始化 │
│ - 随机初始化所有参数 │
│ - 设置模型配置(层数、维度、头数等) │
└────────────────────┬──────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────┐
│ 3. 训练循环 │
│ ┌────────────────────────────────────────────┐ │
│ │ For each batch in dataset: │ │
│ │ 1. 采样一批文本(batch_size个序列) │ │
│ │ 2. 前向传播:计算logits和loss │ │
│ │ 3. 反向传播:计算梯度 │ │
│ │ 4. 参数更新:AdamW优化 │ │
│ │ 5. 学习率调整:warmup + cosine decay │ │
│ │ 6. 记录指标:loss、困惑度等 │ │
│ │ 7. 定期保存checkpoint │ │
│ └────────────────────────────────────────────┘ │
│ 重复数百万到数千万步 │
└────────────────────┬──────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────┐
│ 4. 模型评估 │
│ - 在验证集上计算困惑度(Perplexity) │
│ - 评估下游任务性能 │
│ - 选择最佳checkpoint │
└────────────────────┬──────────────────────────────────────┘
↓
预训练完成
训练代码(伪代码)
# 1. 模型和优化器初始化
model = GPT(vocab_size=50257, n_layer=12, n_head=12, n_embd=768)
model = model.to('cuda')
optimizer = AdamW(
model.parameters(),
lr=6e-4,
betas=(0.9, 0.95),
weight_decay=0.1
)
lr_scheduler = CosineAnnealingWarmup(
optimizer,
warmup_steps=2000,
max_steps=300000
)
# 2. 训练循环
global_step = 0
for epoch in range(num_epochs):
for batch in dataloader:
# (1) 准备数据
input_ids = batch['input_ids'].to('cuda') # [batch_size, seq_len]
# 目标是输入右移一位(预测下一个Token)
target_ids = input_ids[:, 1:] # [batch_size, seq_len-1]
input_ids = input_ids[:, :-1] # [batch_size, seq_len-1]
# (2) 前向传播
logits = model(input_ids) # [batch_size, seq_len-1, vocab_size]
# (3) 计算Loss
loss = F.cross_entropy(
logits.reshape(-1, logits.size(-1)), # [batch*seq, vocab]
target_ids.reshape(-1), # [batch*seq]
ignore_index=PAD_TOKEN_ID # 忽略padding
)
# (4) 反向传播
loss.backward()
# (5) 梯度裁剪(防止梯度爆炸)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# (6) 参数更新
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
# (7) 记录指标
global_step += 1
if global_step % 100 == 0:
perplexity = torch.exp(loss).item()
current_lr = lr_scheduler.get_last_lr()[0]
print(f"Step {global_step}: Loss={loss.item():.4f}, "
f"PPL={perplexity:.2f}, LR={current_lr:.6f}")
# (8) 保存checkpoint
if global_step % 10000 == 0:
torch.save({
'step': global_step,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'loss': loss.item(),
}, f'checkpoint_{global_step}.pt')
关键训练设置
Batch Size
有效Batch Size = batch_size × gradient_accumulation_steps × num_gpus
| 模型 | 有效Batch Size | Token数/Batch |
|---|---|---|
| GPT-2 | 512 | ~262K Token |
| GPT-3 | 3.2M Token | 3.2M Token |
| LLaMA-65B | 4M Token | 4M Token |
大Batch Size的优点:
- 梯度更稳定
- 训练更快(更少的参数更新次数)
实现大Batch Size:
- 梯度累积(Gradient Accumulation)
- 多GPU训练(Data Parallel、Model Parallel)
Sequence Length
序列长度:每个训练样本的Token数
| 模型 | 序列长度 |
|---|---|
| GPT-2 | 1024 |
| GPT-3 | 2048 |
| GPT-4 | 8192-32768 |
| Claude-2 | 100K |
趋势:序列长度不断增加,支持更长的上下文。
挑战:
- 内存消耗:(注意力计算)
- 计算量:与序列长度的平方成正比
训练步数
训练多少步?
| 模型 | 总Token数 | 训练步数 |
|---|---|---|
| GPT-3 | 300B | ~100K 步 |
| LLaMA-1 | 1.4T | ~350K 步 |
| LLaMA-2 | 2T | ~500K 步 |
Scaling Law:
更多的Token → 更好的性能
但收益递减:从1T到2T的提升 < 从100B到1T的提升
训练监控
1. Loss曲线
理想的Loss曲线:
Loss
^
|
|\
| \____
| ----___
| ------____
+-------------------------> Step
稳定下降,最终趋于平缓。
异常情况:
Loss
^
| /\
| /\ / \
| / \ / \
| / X \
+-----------------> Step
震荡、发散 → 学习率太大、数据有问题、数值不稳定
2. 困惑度(Perplexity, PPL)
定义:
或更准确地:
直观理解:
- PPL = 10:模型在每个位置平均要在10个词中选择
- PPL = 100:模型在每个位置平均要在100个词中选择
- PPL越小,模型越确定,效果越好
典型值:
| 模型 | 验证集PPL |
|---|---|
| 随机 | ~50000(词表大小) |
| 较差的模型 | ~1000 |
| 中等模型 | ~100 |
| 好的模型 | ~10-20 |
| 人类 | ~1-2(非常确定) |
3. 学习率曲线
Learning Rate
^
| ___-----\___
| / \___
| / \___
| / ---
| /
+----------------------------> Step
warmup peak cosine decay
4. 梯度范数
监控梯度的L2范数:
正常范围:0.1 ~ 10
异常:
-
100:梯度爆炸
- < 0.001:梯度消失
分布式训练
大模型通常无法在单个GPU上训练,需要分布式训练。
1. Data Parallel(数据并行)
思想:每个GPU有完整的模型副本,处理不同的数据
GPU 0: Model Copy → Batch 0
GPU 1: Model Copy → Batch 1
GPU 2: Model Copy → Batch 2
GPU 3: Model Copy → Batch 3
↓
Average Gradients
↓
Update All Copies
适用:中小型模型(能放进单GPU)
2. Model Parallel(模型并行)
思想:将模型拆分到多个GPU
Pipeline Parallelism:
GPU 0: Layer 1-3 → Batch 0
GPU 1: Layer 4-6 → (wait)
GPU 2: Layer 7-9 → (wait)
GPU 3: Layer 10-12 → (wait)
然后Batch 0传递到GPU 1 → GPU 2 → GPU 3
Tensor Parallelism:
将单层的矩阵乘法拆分到多个GPU:
适用:超大模型(单GPU放不下)
3. 混合并行
GPT-3、LLaMA等使用Data + Pipeline + Tensor并行的组合。
GPT-3 (175B)的训练:
- 使用10,000个GPU(NVIDIA V100)
- Pipeline Parallelism:将96层分到多个GPU
- Tensor Parallelism:将单层的矩阵分到多个GPU
- Data Parallelism:在此基础上做数据并行
训练时间:数周到数月。
预训练的挑战
1. 计算资源
成本:
- GPT-3(175B):估计 $4.6M(训练一次)
- LLaMA-65B:约 $2-3M
- 需要数千个GPU,数周时间
趋势:成本不断上升,但单位成本在下降。
2. 数据质量
问题:
- 互联网数据质量参差不齐
- 偏见、虚假信息
- 版权问题
解决:
- 更好的数据清洗
- 高质量数据集(Books、Papers)
- 合成数据(用模型生成)
3. 训练稳定性
问题:
- Loss突然爆炸
- 梯度消失/爆炸
- 数值不稳定(NaN、Inf)
解决:
- 梯度裁剪
- 更小的学习率
- 混合精度训练(FP16 + FP32)
- 更好的初始化
4. 评估困难
问题:
- 困惑度不能完全反映实际能力
- 需要在多种任务上评估
解决:
- 构建评估基准(MMLU、HellaSwag等)
- 人工评估
- 对比不同模型
预训练后的模型
预训练完成后,得到一个基础模型(Base Model):
能力
具备的能力:
- ✅ 语言理解:理解语法、语义
- ✅ 知识:从数据中学到的事实性知识
- ✅ 简单推理:因果、类比等
- ✅ 代码理解(如果训练数据包含代码)
- ✅ 多语言能力(如果训练数据包含多语言)
不具备的能力:
- ❌ 指令遵循:不会按照用户指令行事
- ❌ 对话能力:可能输出不连贯的文本
- ❌ 安全性:可能输出有害内容
- ❌ 针对性任务能力:需要微调
使用方式
Base Model的典型行为:
输入:"请写一首诗"
Base Model输出(续写模式):
请写一首诗,描述春天的美景。
请写一首诗,题目是《友谊》。
请写一首诗,用七言律诗的格式。
...
它只会续写,不会遵循指令!
为什么?
Base Model学习的是"文本接龙",它看到"请写一首诗",会认为这是一个列表的开头,继续生成类似的句子。
解决方案:需要进行指令微调(Instruction Tuning),这是下一阶段的训练。
小结
-
预训练定义:
- 在大规模无标注数据上的自监督学习
- 通过Next Token Prediction学习通用语言能力
-
核心任务:
- Next Token Prediction:
- 因果语言模型(Causal LM)
- 每个位置都是训练样本
-
预训练数据:
- 来源:网页、书籍、代码、论文、对话
- 规模:万亿Token级别
- 预处理:去重、过滤、清洗
-
训练过程:
- 大Batch Size(数百万Token)
- 长序列(2048-8192 Token)
- 数十万到数百万步
- 分布式训练(数千GPU)
-
监控指标:
- Loss:交叉熵损失
- Perplexity:困惑度
- 学习率曲线
- 梯度范数
-
挑战:
- 计算成本高(数百万美元)
- 数据质量问题
- 训练稳定性
- 评估困难
-
预训练后的模型:
- 具备通用语言能力
- 但不会指令遵循
- 需要后训练(指令微调、RLHF)
预训练是大模型能力的基础,它让模型从随机参数变成一个"读过万卷书"的语言大师。但要让它成为一个好的对话助手,还需要后续的训练——这就是我们下一章要讲的内容。