LLM训练揭秘:预训练、微调、对齐全流程
深入了解大语言模型的完整训练流程,从预训练到对齐的每个环节。
前言
一个强大的大语言模型是如何诞生的?它经历了哪些训练阶段?每个阶段的目的和方法是什么?
今天,我们将揭开LLM训练的神秘面纱,带你了解从预训练到微调再到对齐的完整流程。
一、训练流程概览
三阶段训练范式
┌─────────────────────────────────────────────────────────────┐
│ LLM训练全景图 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 阶段一:预训练(Pre-training) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 海量文本 → 语言模型 → 基础模型(Base Model) │ │
│ │ 目标:学习语言知识和世界知识 │ │
│ │ 数据:TB级别文本 │ │
│ │ 计算:数千张GPU,数周时间 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ 阶段二:微调(Fine-tuning) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 任务数据 → 有监督学习 → 微调模型 │ │
│ │ 目标:适应特定任务或领域 │ │
│ │ 数据:数千到数百万条高质量数据 │ │
│ │ 计算:数十张GPU,数小时到数天 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ 阶段三:对齐(Alignment) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 人类反馈 → 强化学习 → 对齐模型 │ │
│ │ 目标:与人类价值观对齐 │ │
│ │ 数据:人类偏好标注 │ │
│ │ 计算:数百张GPU,数天时间 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
二、预训练:构建语言知识的基石
核心目标
预训练的目标是让模型学习通用的语言知识和世界知识:
预训练学到的能力:
语言层面:
├── 语法规则
├── 词义理解
├── 句子结构
└── 上下文关系
知识层面:
├── 事实知识(巴黎是法国首都)
├── 常识推理(水往低处流)
├── 专业知识(编程、医学、法律)
└── 文化知识(历史、艺术)
数据准备
# 预训练数据的来源和比例
training_data = {
"网页数据": {
"比例": "40%",
"来源": "Common Crawl",
"处理": "去重、清洗、过滤低质量内容"
},
"书籍": {
"比例": "20%",
"来源": "BookCorpus、Gutenberg",
"处理": "提取文本、清理格式"
},
"代码": {
"比例": "15%",
"来源": "GitHub",
"处理": "许可证过滤、去重"
},
"维基百科": {
"比例": "10%",
"来源": "Wikipedia",
"处理": "提取正文、清理标记"
},
"学术论文": {
"比例": "8%",
"来源": "arXiv、PubMed",
"处理": "提取摘要和正文"
},
"其他": {
"比例": "7%",
"来源": "Reddit、Stack Exchange等",
"处理": "质量过滤、去重"
}
}
数据处理流程
def preprocess_training_data(raw_text):
"""预训练数据处理流程"""
# 1. 文本清洗
text = clean_text(raw_text)
# - 去除HTML标签
# - 统一编码
# - 处理特殊字符
# 2. 质量过滤
if not is_high_quality(text):
return None
# - 语言检测
# - 困惑度过滤
# - 长度过滤
# 3. 去重
if is_duplicate(text, existing_texts):
return None
# 4. 分词
tokens = tokenizer.encode(text)
# 5. 构建训练样本
samples = create_samples(tokens, max_length=2048)
return samples
def is_high_quality(text):
"""质量过滤规则"""
# 长度检查
if len(text) < 100:
return False
# 语言检查
if not is_target_language(text):
return False
# 困惑度检查(用小模型计算)
perplexity = calculate_perplexity(text)
if perplexity > threshold:
return False
return True
训练目标
预训练的核心目标是下一个词预测(Next Token Prediction):
import torch
import torch.nn as nn
class PretrainingObjective(nn.Module):
def __init__(self, model):
super().__init__()
self.model = model
self.loss_fn = nn.CrossEntropyLoss()
def forward(self, input_ids, labels):
"""
input_ids: [batch, seq_len] 输入序列
labels: [batch, seq_len] 目标序列(input_ids左移一位)
"""
# 模型前向传播
logits = self.model(input_ids) # [batch, seq_len, vocab_size]
# 计算损失
loss = self.loss_fn(
logits.view(-1, logits.size(-1)), # [batch*seq_len, vocab_size]
labels.view(-1) # [batch*seq_len]
)
return loss
# 训练示例
def pretrain_step(model, batch, optimizer):
input_ids = batch['input_ids']
# 目标是预测下一个词
labels = torch.roll(input_ids, shifts=-1, dims=1)
# 前向传播
loss = model(input_ids, labels)
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
return loss.item()
Scaling Laws
模型的性能与三个因素相关:模型规模、数据量、计算量:
性能提升规律:
Loss
│
│╲
│ ╲
│ ╲
│ ╲________
│ ╲______
│ ╲________
└────────────────────────────→ 计算量
关键发现:
1. 性能随规模平滑提升
2. 没有明显的饱和迹象
3. 存在最优的训练计算分配
预训练的计算资源
# 预训练资源估算
def estimate_training_resources(params_count, tokens_count):
"""
估算训练所需资源
params_count: 参数量(如70B)
tokens_count: 训练token数(如2T)
"""
# 经验法则:每参数需要约20个训练token
optimal_tokens = params_count * 20
# FLOPs估算
# 训练一个token约需要 6 * 参数量 FLOPs
total_flops = 6 * params_count * tokens_count
# GPU计算能力(A100: 312 TFLOPS for BF16)
a100_flops = 312e12
# 理论计算时间(单卡)
theoretical_time = total_flops / a100_flops
# 实际时间(考虑利用率约40%)
actual_time = theoretical_time / 0.4
return {
"总FLOPs": f"{total_flops:.2e}",
"理论时间(单卡)": f"{theoretical_time/3600:.0f}小时",
"实际时间(单卡)": f"{actual_time/3600:.0f}小时",
"建议GPU数量": f"约{int(actual_time / (3600 * 24 * 30))}张A100·月"
}
# LLaMA-65B的估算
estimate_training_resources(65e9, 1.4e12)
三、微调:适应特定任务
微调的意义
预训练模型虽然学到了丰富的知识,但需要微调来:
- 适应特定任务:分类、生成、问答等
- 学习特定格式:指令格式、输出格式
- 注入领域知识:医疗、法律、金融等
微调方法分类
微调方法:
全量微调(Full Fine-tuning)
├── 更新所有参数
├── 效果最好
└── 资源需求最大
参数高效微调(PEFT)
├── LoRA
│ ├── 只训练低秩分解矩阵
│ └── 原始参数冻结
├── Adapter
│ ├── 在层间插入小模块
│ └── 只训练适配器
├── Prefix Tuning
│ ├── 添加可学习前缀
│ └── 只训练前缀向量
└── QLoRA
├── 量化 + LoRA
└── 进一步降低显存
LoRA详解
LoRA(Low-Rank Adaptation)是目前最流行的PEFT方法:
import torch
import torch.nn as nn
class LoRALayer(nn.Module):
"""
LoRA: 用低秩分解模拟权重更新
原始:W = W₀
LoRA:W = W₀ + BA
其中 B ∈ R^(d×r), A ∈ R^(r×k), r << min(d, k)
"""
def __init__(self, in_features, out_features, rank=8, alpha=16):
super().__init__()
# 原始权重(冻结)
self.weight = nn.Parameter(torch.zeros(out_features, in_features))
# LoRA权重(可训练)
self.lora_A = nn.Parameter(torch.randn(rank, in_features) * 0.01)
self.lora_B = nn.Parameter(torch.zeros(out_features, rank))
self.scaling = alpha / rank
def forward(self, x):
# 原始变换
result = nn.functional.linear(x, self.weight)
# LoRA增量
lora_output = (x @ self.lora_A.T) @ self.lora_B.T * self.scaling
return result + lora_output
# 应用LoRA到Transformer
def apply_lora_to_model(model, rank=8, target_modules=["q_proj", "v_proj"]):
"""将LoRA应用到指定模块"""
for name, module in model.named_modules():
if any(target in name for target in target_modules):
# 替换为LoRA层
# 实际实现更复杂,需要保持原始权重
pass
return model
指令微调
指令微调(Instruction Tuning)让模型学会遵循指令:
# 指令微调数据格式
instruction_data = [
{
"instruction": "将下面的句子翻译成英文",
"input": "我喜欢学习人工智能",
"output": "I like learning artificial intelligence"
},
{
"instruction": "总结下面的文章",
"input": "人工智能是计算机科学的一个分支...",
"output": "人工智能是研究如何让计算机模拟人类智能的学科。"
},
{
"instruction": "回答问题",
"input": "中国的首都是哪里?",
"output": "中国的首都是北京。"
}
]
# 格式化为模型输入
def format_instruction(sample):
if sample['input']:
prompt = f"""### 指令:
{sample['instruction']}
### 输入:
{sample['input']}
### 回答:
{sample['output']}"""
else:
prompt = f"""### 指令:
{sample['instruction']}
### 回答:
{sample['output']}"""
return prompt
微调代码示例
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer
from peft import LoraConfig, get_peft_model
# 加载模型
model = AutoModelForCausalLM.from_pretrained("base-model")
tokenizer = AutoTokenizer.from_pretrained("base-model")
# LoRA配置
lora_config = LoraConfig(
r=8, # 秩
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
# 应用LoRA
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 输出: trainable params: 4,194,304 || all params: 6,742,609,920 || trainable%: 0.06%
# 训练配置
training_args = TrainingArguments(
output_dir="./output",
num_train_epochs=3,
per_device_train_batch_size=4,
learning_rate=1e-4,
logging_steps=100,
save_steps=500,
)
# 训练
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
)
trainer.train()
四、对齐:与人类价值观一致
为什么需要对齐?
预训练和微调后的模型可能存在:
对齐问题:
1. 有害输出
用户:如何制作炸弹?
模型:[详细步骤...] ❌
2. 虚假信息
用户:第一个登上月球的人是谁?
模型:尼尔·阿姆斯特朗于1969年... ✓
用户:第一个登上火星的人是谁?
模型:埃隆·马斯克于2025年... ❌(幻觉)
3. 偏见问题
模型可能反映训练数据中的偏见
4. 不遵循指令
用户:用一句话总结
模型:[写了三段话] ❌
RLHF:人类反馈强化学习
RLHF(Reinforcement Learning from Human Feedback)是主流对齐方法:
RLHF流程:
阶段1:有监督微调(SFT)
┌────────────────────────────────────┐
│ 人类编写的高质量指令-回答对 │
│ ↓ │
│ 有监督训练 │
└────────────────────────────────────┘
↓
阶段2:奖励模型训练(RM)
┌────────────────────────────────────┐
│ 模型对同一指令生成多个回答 │
│ ↓ │
│ 人类对回答进行排序 │
│ ↓ │
│ 训练奖励模型预测人类偏好 │
└────────────────────────────────────┘
↓
阶段3:强化学习优化(PPO)
┌────────────────────────────────────┐
│ 用PPO算法优化语言模型 │
│ 奖励 = 奖励模型评分 - KL散度惩罚 │
└────────────────────────────────────┘
奖励模型训练
import torch
import torch.nn as nn
class RewardModel(nn.Module):
def __init__(self, base_model, hidden_size):
super().__init__()
self.base_model = base_model
# 奖励头:将隐藏状态映射到标量
self.reward_head = nn.Linear(hidden_size, 1)
def forward(self, input_ids, attention_mask):
# 获取隐藏状态
outputs = self.base_model(
input_ids=input_ids,
attention_mask=attention_mask
)
hidden_states = outputs.last_hidden_state
# 使用最后一个token的隐藏状态
last_hidden = hidden_states[:, -1, :]
# 计算奖励
reward = self.reward_head(last_hidden)
return reward
def train_reward_model(reward_model, comparisons, optimizer):
"""
使用比较数据训练奖励模型
comparisons: [(prompt, chosen_response, rejected_response), ...]
"""
total_loss = 0
for prompt, chosen, rejected in comparisons:
# 构建输入
chosen_input = tokenize(prompt + chosen)
rejected_input = tokenize(prompt + rejected)
# 计算奖励
chosen_reward = reward_model(**chosen_input)
rejected_reward = reward_model(**rejected_input)
# Bradley-Terry损失
# 鼓励chosen的奖励高于rejected
loss = -torch.log(
torch.sigmoid(chosen_reward - rejected_reward)
).mean()
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
return total_loss
PPO训练
import torch
from trl import PPOTrainer, PPOConfig
def ppo_training_step(policy_model, reward_model, tokenizer, prompt, ppo_trainer):
"""PPO训练步骤"""
# 编码输入
query_tensor = tokenizer.encode(prompt, return_tensors="pt")
# 生成回答
response_tensor = policy_model.generate(query_tensor)
response = tokenizer.decode(response_tensor[0])
# 计算奖励
full_text = prompt + response
reward = reward_model(tokenizer.encode(full_text, return_tensors="pt"))
# PPO更新
stats = ppo_trainer.step(
query_tensor,
response_tensor,
reward
)
return stats
DPO:直接偏好优化
DPO(Direct Preference Optimization)是RLHF的简化替代方案:
def dpo_loss(policy_model, reference_model, prompt, chosen, rejected, beta=0.1):
"""
DPO:直接优化偏好,无需训练奖励模型
核心:增大chosen的概率,减小rejected的概率
"""
# 计算log概率
def get_log_prob(model, text):
inputs = tokenizer(prompt + text, return_tensors="pt")
outputs = model(**inputs, labels=inputs["input_ids"])
return -outputs.loss
# 政策模型的log概率
policy_chosen_logp = get_log_prob(policy_model, chosen)
policy_rejected_logp = get_log_prob(policy_model, rejected)
# 参考模型的log概率(固定)
with torch.no_grad():
ref_chosen_logp = get_log_prob(reference_model, chosen)
ref_rejected_logp = get_log_prob(reference_model, rejected)
# DPO损失
chosen_reward = beta * (policy_chosen_logp - ref_chosen_logp)
rejected_reward = beta * (policy_rejected_logp - ref_rejected_logp)
loss = -torch.log(
torch.sigmoid(chosen_reward - rejected_reward)
).mean()
return loss
五、训练资源与成本
各阶段资源对比
训练阶段资源需求对比:
预训练:
├── 数据:TB级别
├── 计算资源:数千张GPU
├── 时间:数周到数月
└── 成本:数百万美元
微调:
├── 数据:数千到数百万条
├── 计算资源:数张到数十张GPU
├── 时间:数小时到数天
└── 成本:数十到数千美元
对齐:
├── 数据:数万条人类偏好
├── 计算资源:数十张GPU
├── 时间:数天
└── 成本:数千到数万美元
训练技巧
# 训练优化技巧
training_tricks = {
"混合精度训练": {
"方法": "BF16/FP16",
"效果": "减少显存,加速训练",
"代码": "model.to(torch.bfloat16)"
},
"梯度累积": {
"方法": "accumulate_grad_batches",
"效果": "模拟大batch训练",
"代码": "gradient_accumulation_steps=4"
},
"DeepSpeed/ZeRO": {
"方法": "分片优化器状态",
"效果": "降低显存占用",
"配置": "zero_stage=2"
},
"Flash Attention": {
"方法": "高效注意力计算",
"效果": "加速2-4倍",
"使用": "attn_implementation='flash_attention_2'"
},
"梯度检查点": {
"方法": "用计算换显存",
"效果": "降低30-50%显存",
"代码": "gradient_checkpointing=True"
}
}
小结
| 阶段 | 目标 | 数据 | 方法 | 资源需求 |
|---|---|---|---|---|
| 预训练 | 学习语言和世界知识 | TB级文本 | Next Token Prediction | 极高 |
| 微调 | 适应特定任务 | 高质量指令数据 | SFT/PEFT | 中等 |
| 对齐 | 与人类价值观一致 | 人类偏好数据 | RLHF/DPO | 中等 |
思考与练习
下期预告
下一篇文章,我们将深入探讨:Prompt Engineering:与大模型对话的艺术
会解答这些问题:
- 什么是好的Prompt?
- 有哪些高级的Prompt技巧?
- 如何系统地优化Prompt?
关注专栏,不错过后续更新!
作者:ECH00O00 本文首发于掘金专栏《AI科普实验室》 欢迎评论区交流讨论,点赞收藏就是最大的鼓励 ❤️