04 - 对齐技术¶
⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。
📌 定位说明:本章是对齐技术的主版本,侧重 RLHF/DPO/PPO 等对齐方法的全景原理。 - 📖 应用安全与合规视角请参考 LLM 应用/14-大模型安全与对齐
学习目标:深入理解大模型对齐技术,包括 RLHF 、 DPO 、 PPO 等方法的原理与实现。
目录¶
对齐技术概述¶
1.1 什么是对齐¶
对齐(Alignment)
核心问题:如何让大模型的行为符合人类意图和价值观?
预训练的问题:
├── 模型从海量互联网数据学习
├── 可能包含有害、偏见、错误信息
├── 不理解人类真实意图
└── 可能产生不安全输出
对齐的目标:
├── 有用性(Helpful):回答用户问题
├── 诚实性(Honest):提供准确信息
├── 无害性(Harmless):避免有害输出
└── 可控性(Controllable):遵循指令
对齐的三种方法:
├── 1. 基于人类反馈的强化学习(RLHF)
├── 2. 直接偏好优化(DPO)
└── 3. 基于AI反馈的强化学习(RLAIF)
1.2 对齐技术对比¶
| 方法 | 复杂度 | 稳定性 | 效果 | 代表模型 |
|---|---|---|---|---|
| RLHF | 高 | 中 | 优秀 | GPT-4, Claude, InstructGPT |
| DPO | 中 | 高 | 优秀 | Zephyr, Neural-Chat |
| SLiC | 低 | 高 | 良好 | - |
| IPO | 中 | 高 | 良好 | - |
| KTO | 中 | 高 | 良好 | - |
| ORPO | 低 | 高 | 优秀 | Llama 3.1 |
| GRPO | 中 | 高 | 优秀 | DeepSeek-R1 |
| SimPO | 低 | 高 | 优秀 | - |
基于人类反馈的强化学习 (RLHF)¶
2.1 RLHF 三阶段流程¶
RLHF完整流程
═══════════════════════════════════════════════════════════════════
阶段1: 监督微调(SFT)
├── 数据:人类编写的指令-回答对
├── 目标:让模型学会遵循指令
├── 方法:标准语言模型微调
└── 输出:SFT模型
阶段2: 奖励模型训练(Reward Modeling)
├── 数据:同一问题的多个回答,人类标注偏好
├── 目标:学习人类偏好
├── 方法:训练奖励模型预测人类偏好
└── 输出:Reward Model
阶段3: 强化学习优化(RL Optimization)
├── 数据:使用SFT模型生成回答
├── 目标:最大化奖励,同时保持与SFT模型的相似性
├── 方法:PPO算法
└── 输出:对齐后的模型
═══════════════════════════════════════════════════════════════════
2.2 阶段 1 :监督微调 (SFT)¶
class SFTTrainer:
"""
监督微调训练器
"""
def __init__(self, model, tokenizer, learning_rate=1e-5):
self.model = model
self.tokenizer = tokenizer
self.optimizer = torch.optim.AdamW(
model.parameters(),
lr=learning_rate
)
def prepare_instruction_data(self, examples):
"""
准备指令数据
Args:
examples: [{"instruction": "...", "input": "...", "output": "..."}]
"""
formatted_texts = []
for example in examples:
# 构建prompt模板
if example.get('input'):
prompt = f"### Instruction:\n{example['instruction']}\n\n### Input:\n{example['input']}\n\n### Response:\n"
else:
prompt = f"### Instruction:\n{example['instruction']}\n\n### Response:\n"
# 完整文本(prompt + response)
full_text = prompt + example['output']
formatted_texts.append(full_text)
return formatted_texts
def train_step(self, batch):
"""
单步训练
"""
self.model.train()
# 前向传播
outputs = self.model(
input_ids=batch['input_ids'],
attention_mask=batch['attention_mask'],
labels=batch['labels']
)
loss = outputs.loss
# 反向传播
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
return loss.item()
# SFT数据格式示例
SFT_EXAMPLE = {
"instruction": "解释什么是机器学习",
"input": "",
"output": "机器学习是人工智能的一个分支,它使计算机能够从数据中学习而无需明确编程..."
}
2.3 阶段 2 :奖励模型训练¶
class RewardModel(nn.Module):
"""
奖励模型
基于Bradley-Terry模型,学习预测人类偏好
"""
def __init__(self, base_model):
super().__init__() # super()调用父类方法
self.base_model = base_model
# 在模型输出上添加奖励头
self.reward_head = nn.Linear(
base_model.config.hidden_size,
1,
bias=False
)
def forward(self, input_ids, attention_mask):
"""
前向传播
Returns:
rewards: [batch_size] 每个样本的奖励分数
"""
# 获取模型最后一层隐藏状态
outputs = self.base_model(
input_ids=input_ids,
attention_mask=attention_mask,
output_hidden_states=True
)
# 取最后一个token的隐藏状态
last_hidden = outputs.hidden_states[-1] # [batch, seq_len, hidden] # [-1]负索引取最后一个元素
# 找到每个序列的实际最后一个token(考虑padding)
sequence_lengths = attention_mask.sum(dim=1) - 1
batch_size = input_ids.size(0)
# 提取最后一个token的表示
last_token_hidden = last_hidden[
torch.arange(batch_size),
sequence_lengths
]
# 计算奖励
rewards = self.reward_head(last_token_hidden).squeeze(-1)
return rewards
class RewardModelTrainer:
"""
奖励模型训练器
"""
def __init__(self, reward_model, learning_rate=1e-5):
self.reward_model = reward_model
self.optimizer = torch.optim.AdamW(
reward_model.parameters(),
lr=learning_rate
)
def compute_preference_loss(self, chosen_rewards, rejected_rewards):
"""
计算偏好损失
使用Bradley-Terry模型:
P(y_w > y_l | x) = σ(r(x, y_w) - r(x, y_l))
Loss = -log σ(r_θ(x, y_w) - r_θ(x, y_l))
"""
# 计算奖励差距
reward_diff = chosen_rewards - rejected_rewards
# 使用log-sigmoid损失
loss = -F.logsigmoid(reward_diff).mean()
return loss
def train_step(self, batch):
"""
训练步骤
batch包含:
- chosen_input_ids: 人类偏好的回答
- rejected_input_ids: 人类不喜欢的回答
"""
self.reward_model.train()
# 计算chosen的奖励
chosen_rewards = self.reward_model(
input_ids=batch['chosen_input_ids'],
attention_mask=batch['chosen_attention_mask']
)
# 计算rejected的奖励
rejected_rewards = self.reward_model(
input_ids=batch['rejected_input_ids'],
attention_mask=batch['rejected_attention_mask']
)
# 计算损失
loss = self.compute_preference_loss(chosen_rewards, rejected_rewards)
# 反向传播
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
# 计算准确率
accuracy = (chosen_rewards > rejected_rewards).float().mean()
return {
'loss': loss.item(),
'accuracy': accuracy.item(),
'chosen_reward': chosen_rewards.mean().item(),
'rejected_reward': rejected_rewards.mean().item()
}
2.4 阶段 3 : PPO 强化学习¶
class PPOTrainer:
"""
PPO(Proximal Policy Optimization)训练器
"""
def __init__(
self,
policy_model, # 策略模型(要训练的模型)
reference_model, # 参考模型(SFT模型,不更新)
reward_model, # 奖励模型
value_model, # 价值模型(可选)
tokenizer, # 分词器(用于padding处理等)
learning_rate=1e-5,
clip_epsilon=0.2,
kl_coef=0.2,
gamma=0.99,
lam=0.95
):
self.policy_model = policy_model
self.reference_model = reference_model
self.reward_model = reward_model
self.value_model = value_model
self.tokenizer = tokenizer
self.optimizer = torch.optim.AdamW(
policy_model.parameters(),
lr=learning_rate
)
self.clip_epsilon = clip_epsilon
self.kl_coef = kl_coef
self.gamma = gamma
self.lam = lam
def generate_responses(self, prompts, max_length=256):
"""
生成回答
"""
self.policy_model.eval()
with torch.no_grad(): # 禁用梯度计算,节省内存(推理时使用)
outputs = self.policy_model.generate(
input_ids=prompts,
max_length=max_length,
do_sample=True,
temperature=0.7,
return_dict_in_generate=True,
output_scores=True
)
return outputs.sequences, outputs.scores
def compute_rewards(self, sequences, attention_mask):
"""
计算奖励
奖励 = 奖励模型分数 - KL惩罚
"""
with torch.no_grad():
# 奖励模型分数
reward_scores = self.reward_model(sequences, attention_mask)
# 计算KL散度(与参考模型的差异)
policy_logits = self.policy_model(sequences, attention_mask).logits
reference_logits = self.reference_model(sequences, attention_mask).logits
# KL散度计算
policy_probs = F.softmax(policy_logits, dim=-1)
reference_probs = F.softmax(reference_logits, dim=-1)
kl_div = (policy_probs * (torch.log(policy_probs + 1e-10) - torch.log(reference_probs + 1e-10))).sum(-1)
# 最终奖励
rewards = reward_scores - self.kl_coef * kl_div.mean(dim=1)
return rewards
def compute_advantages(self, rewards, values):
"""
计算优势函数(GAE)
"""
advantages = []
gae = 0
for t in reversed(range(len(rewards))):
if t == len(rewards) - 1:
next_value = 0
else:
next_value = values[t + 1]
delta = rewards[t] + self.gamma * next_value - values[t]
gae = delta + self.gamma * self.lam * gae
advantages.insert(0, gae)
return torch.tensor(advantages)
def ppo_loss(self, old_logprobs, new_logprobs, advantages):
"""
计算PPO损失
"""
# 计算比率
ratio = torch.exp(new_logprobs - old_logprobs)
# 裁剪目标
surr1 = ratio * advantages
surr2 = torch.clamp(ratio, 1 - self.clip_epsilon, 1 + self.clip_epsilon) * advantages
# PPO损失(取最小值,限制更新幅度)
loss = -torch.min(surr1, surr2).mean()
return loss
def train_step(self, batch):
"""
PPO训练步骤
"""
self.policy_model.train()
prompts = batch['prompts']
old_sequences = batch['sequences']
old_logprobs = batch['logprobs']
# 计算奖励
attention_mask = (old_sequences != self.tokenizer.pad_token_id).long()
rewards = self.compute_rewards(old_sequences, attention_mask)
# 计算价值(如果有价值模型)
if self.value_model is not None:
values = self.value_model(old_sequences, attention_mask)
advantages = self.compute_advantages(rewards, values)
else:
advantages = rewards
# 新的策略输出
outputs = self.policy_model(old_sequences, attention_mask)
new_logits = outputs.logits
# 计算新的log概率
new_logprobs = F.log_softmax(new_logits, dim=-1)
new_logprobs = torch.gather(new_logprobs, 2, old_sequences.unsqueeze(-1)).squeeze(-1) # unsqueeze增加一个维度
new_logprobs = new_logprobs.mean(dim=1) # 平均每个token的logprob
# 计算PPO损失
loss = self.ppo_loss(old_logprobs, new_logprobs, advantages)
# 反向传播
self.optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(self.policy_model.parameters(), 1.0)
self.optimizer.step()
return {
'loss': loss.item(),
'reward': rewards.mean().item(),
'advantage': advantages.mean().item()
}
直接偏好优化 (DPO)¶
3.1 DPO 原理¶
DPO(Direct Preference Optimization)
核心思想:
├── 不需要显式的奖励模型
├── 不需要强化学习
├── 直接用偏好数据优化策略
└── 更简单、更稳定
Bradley-Terry 模型到 DPO 损失的完整推导¶
第一步: Bradley-Terry 偏好模型
给定提示 \(x\),人类更偏好回答 \(y_w\) 而非 \(y_l\) 的概率为:
其中 \(r(x, y)\) 是真实奖励函数,\(\sigma\) 是 sigmoid 函数。
第二步: RLHF 的 KL 约束优化目标
RLHF 要解决的优化问题是:
第三步:最优策略的闭式解
对上式求解(拉格朗日对偶),最优策略为:
其中 \(Z(x) = \sum_y \pi_{\text{ref}}(y|x) \exp(r(x,y)/\beta)\) 是配分函数。
第四步:用策略表示奖励(关键步骤)
对上式取对数并整理,可以用策略反解出奖励:
第五步:代入 Bradley-Terry 消除奖励
将上式代入 Bradley-Terry 模型(\(Z(x)\) 项在做差时对消):
第六步: DPO 损失函数
用当前策略 \(\pi_\theta\) 代替最优策略 \(\pi^*\),对偏好数据集取负对数似然:
推导链总结:
Bradley-Terry偏好模型
↓ 定义了"奖励差→偏好概率"的映射
KL约束优化目标
↓ 拉格朗日对偶求解
最优策略闭式解 π*(y|x)
↓ 取对数,反解出 r(x,y)
用策略比率表达奖励
↓ 代入Bradley-Terry,Z(x)对消
DPO损失函数(无需奖励模型!)
3.2 DPO 实现¶
class DPOTrainer:
"""
DPO(Direct Preference Optimization)训练器
"""
def __init__(
self,
policy_model, # 策略模型
reference_model, # 参考模型(SFT模型,不更新)
beta=0.1, # 温度参数
learning_rate=1e-6
):
self.policy_model = policy_model
self.reference_model = reference_model
self.beta = beta
self.optimizer = torch.optim.AdamW(
policy_model.parameters(),
lr=learning_rate
)
# 参考模型不更新
for param in self.reference_model.parameters():
param.requires_grad = False
def compute_log_probs(self, model, input_ids, attention_mask, labels):
"""
计算序列的log概率
只对response部分计算(labels != -100)
"""
outputs = model(
input_ids=input_ids,
attention_mask=attention_mask
)
logits = outputs.logits
log_probs = F.log_softmax(logits, dim=-1)
# 收集目标token的log概率
# 移位以对齐预测
log_probs = log_probs[:, :-1, :]
labels = labels[:, 1:]
# 获取每个位置的目标token log概率
token_log_probs = torch.gather(
log_probs,
dim=2,
index=labels.unsqueeze(2)
).squeeze(2)
# 只对非padding位置求和
mask = (labels != -100).float()
token_log_probs = token_log_probs * mask
# 每个样本的总log概率
sequence_log_probs = token_log_probs.sum(dim=1)
return sequence_log_probs
def dpo_loss(self, policy_chosen_logps, policy_rejected_logps,
reference_chosen_logps, reference_rejected_logps):
"""
计算DPO损失
"""
# 计算log比率
policy_logratios = policy_chosen_logps - policy_rejected_logps
reference_logratios = reference_chosen_logps - reference_rejected_logps
# DPO损失
logits = self.beta * (policy_logratios - reference_logratios)
loss = -F.logsigmoid(logits).mean()
return loss
def train_step(self, batch):
"""
DPO训练步骤
batch包含:
- chosen_input_ids: 偏好的完整序列(prompt + chosen response)
- rejected_input_ids: 不喜欢的完整序列(prompt + rejected response)
"""
self.policy_model.train()
# 计算策略模型的log概率
policy_chosen_logps = self.compute_log_probs(
self.policy_model,
batch['chosen_input_ids'],
batch['chosen_attention_mask'],
batch['chosen_labels']
)
policy_rejected_logps = self.compute_log_probs(
self.policy_model,
batch['rejected_input_ids'],
batch['rejected_attention_mask'],
batch['rejected_labels']
)
# 计算参考模型的log概率(不计算梯度)
with torch.no_grad():
reference_chosen_logps = self.compute_log_probs(
self.reference_model,
batch['chosen_input_ids'],
batch['chosen_attention_mask'],
batch['chosen_labels']
)
reference_rejected_logps = self.compute_log_probs(
self.reference_model,
batch['rejected_input_ids'],
batch['rejected_attention_mask'],
batch['rejected_labels']
)
# 计算DPO损失
loss = self.dpo_loss(
policy_chosen_logps,
policy_rejected_logps,
reference_chosen_logps,
reference_rejected_logps
)
# 反向传播
self.optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(self.policy_model.parameters(), 1.0)
self.optimizer.step()
# 计算准确率(策略模型是否能正确排序)
with torch.no_grad():
chosen_rewards = self.beta * (policy_chosen_logps - reference_chosen_logps)
rejected_rewards = self.beta * (policy_rejected_logps - reference_rejected_logps)
accuracy = (chosen_rewards > rejected_rewards).float().mean()
return {
'loss': loss.item(),
'accuracy': accuracy.item(),
'chosen_reward': chosen_rewards.mean().item(),
'rejected_reward': rejected_rewards.mean().item()
}
3.3 DPO vs RLHF¶
DPO vs RLHF 对比
═══════════════════════════════════════════════════════════════════
特性 RLHF DPO
─────────────────────────────────────────────────────────────────
复杂度 高(三阶段) 低(单阶段)
稳定性 中(PPO可能不稳定) 高(监督学习)
训练速度 慢 快
内存需求 高(需要4个模型) 低(需要2个模型)
超参数 多(学习率、clip等) 少(主要是beta)
效果 优秀 优秀
模型需求:
RLHF: Policy + Reference + Reward + Value = 4个模型
DPO: Policy + Reference = 2个模型
推荐使用DPO的场景:
├── 计算资源有限
├── 快速迭代
├── 稳定性要求高
└── 首次尝试对齐
推荐使用RLHF的场景:
├── 追求极致效果
├── 有大量计算资源
├── 有经验丰富的团队
└── 生产级应用
═══════════════════════════════════════════════════════════════════
其他对齐方法¶
4.1 IPO (Identity Preference Optimization)¶
class IPOTrainer:
"""
IPO训练器
DPO的改进版本,使用均方误差代替对数损失
"""
def __init__(self, policy_model, reference_model, beta=0.1, learning_rate=1e-6):
self.policy_model = policy_model
self.reference_model = reference_model
self.beta = beta
self.optimizer = torch.optim.AdamW(policy_model.parameters(), lr=learning_rate)
def ipo_loss(self, policy_chosen_logps, policy_rejected_logps,
reference_chosen_logps, reference_rejected_logps):
"""
IPO损失
L = (log(π_θ(y_w|x)/π_ref(y_w|x)) - log(π_θ(y_l|x)/π_ref(y_l|x)) - 1/(2β))^2
"""
policy_logratios = policy_chosen_logps - policy_rejected_logps
reference_logratios = reference_chosen_logps - reference_rejected_logps
# IPO目标:让差距等于1/(2*beta)
diff = policy_logratios - reference_logratios
target = 1.0 / (2 * self.beta)
loss = ((diff - target) ** 2).mean()
return loss
4.2 KTO (Kahneman-Tversky Optimization)¶
class KTOTrainer:
"""
KTO训练器
不需要成对偏好数据,只需要好坏标签
"""
def __init__(self, policy_model, reference_model, beta=0.1, learning_rate=1e-6):
self.policy_model = policy_model
self.reference_model = reference_model
self.beta = beta
self.optimizer = torch.optim.AdamW(policy_model.parameters(), lr=learning_rate)
def kto_loss(self, policy_logps, reference_logps, is_desirable):
"""
KTO损失
对于desirable样本:最大化 r(x,y) - r(x,y')
对于undesirable样本:最小化 r(x,y) - r(x,y')
"""
# 隐式奖励
implicit_reward = self.beta * (policy_logps - reference_logps)
# Kahneman-Tversky损失
if is_desirable:
# 期望样本:奖励应该高
loss = (1 - implicit_reward.sigmoid()).log()
else:
# 不期望样本:奖励应该低
loss = implicit_reward.sigmoid().log()
return -loss.mean()
4.3 ORPO (Odds Ratio Preference Optimization)¶
ORPO 是 2024 年提出的重要对齐方法,被 Llama 3.1 等模型采用。它将 SFT 和对齐合并为单一训练阶段。
class ORPOTrainer:
"""
ORPO训练器(Odds Ratio Preference Optimization)
核心思想:在SFT训练中同时加入偏好优化
- 无需单独的SFT阶段
- 使用odds ratio来衡量偏好差异
- 被Llama 3.1采用
"""
def __init__(self, model, tokenizer, beta=0.1, learning_rate=1e-5):
self.model = model
self.tokenizer = tokenizer
self.beta = beta
self.optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)
def compute_orpo_loss(self, chosen_logps, rejected_logps):
"""
ORPO损失 = SFT损失 + 偏好损失
偏好损失使用log odds ratio:
L_pref = -log σ(log(odds_chosen) - log(odds_rejected))
其中 odds = p / (1 - p)
"""
# 计算odds ratio
# 为数值稳定性,使用log空间计算
log_odds_chosen = chosen_logps - torch.log(1 - chosen_logps.exp() + 1e-8)
log_odds_rejected = rejected_logps - torch.log(1 - rejected_logps.exp() + 1e-8)
# Odds Ratio损失
log_odds_ratio = log_odds_chosen - log_odds_rejected
orpo_loss = -F.logsigmoid(self.beta * log_odds_ratio).mean()
return orpo_loss
def train_step(self, batch):
"""
ORPO训练步骤
同时优化:
1. SFT损失:最大化chosen response的似然
2. 偏好损失:拉大chosen和rejected的差距
"""
self.model.train()
# 计算chosen的log概率(同时作为SFT损失)
chosen_outputs = self.model(
input_ids=batch['chosen_input_ids'],
attention_mask=batch['chosen_attention_mask'],
labels=batch['chosen_labels']
)
chosen_logps = self._compute_sequence_logprobs(chosen_outputs, batch['chosen_labels'])
sft_loss = chosen_outputs.loss
# 计算rejected的log概率(不使用no_grad,因为需要参与反向传播)
rejected_outputs = self.model(
input_ids=batch['rejected_input_ids'],
attention_mask=batch['rejected_attention_mask']
)
rejected_logps = self._compute_sequence_logprobs(rejected_outputs, batch['rejected_labels'])
# ORPO总损失
preference_loss = self.compute_orpo_loss(chosen_logps, rejected_logps)
total_loss = sft_loss + self.beta * preference_loss
# 反向传播
self.optimizer.zero_grad()
total_loss.backward()
self.optimizer.step()
return {
'sft_loss': sft_loss.item(),
'preference_loss': preference_loss.item(),
'total_loss': total_loss.item()
}
def _compute_sequence_logprobs(self, outputs, labels):
"""计算序列的平均log概率"""
logits = outputs.logits[:, :-1, :]
labels = labels[:, 1:]
log_probs = F.log_softmax(logits, dim=-1)
token_logps = log_probs.gather(2, labels.unsqueeze(-1)).squeeze(-1)
mask = (labels != -100).float()
return (token_logps * mask).sum(dim=1) / mask.sum(dim=1)
4.4 GRPO (Group Relative Policy Optimization)¶
GRPO 是 DeepSeek-R1 使用的对齐方法,特别适合推理模型的对齐训练。
class GRPOTrainer:
"""
GRPO训练器(Group Relative Policy Optimization)
核心思想:
- 对同一问题生成多个回答(group)
- 在group内部进行相对比较
- 无需单独的奖励模型
被DeepSeek-R1用于推理能力对齐
"""
def __init__(
self,
model,
ref_model,
tokenizer,
group_size=4, # 每组生成4个回答
beta=0.1,
learning_rate=1e-5
):
self.model = model
self.ref_model = ref_model
self.tokenizer = tokenizer
self.group_size = group_size
self.beta = beta
self.optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)
def generate_group(self, prompt, num_samples=None):
"""
为同一个prompt生成多个回答
"""
if num_samples is None:
num_samples = self.group_size
responses = []
for _ in range(num_samples):
output = self.model.generate(
prompt,
max_length=512,
do_sample=True,
temperature=0.7
)
responses.append(output)
return responses
def compute_group_advantages(self, rewards):
"""
计算组内相对优势
advantage_i = reward_i - mean(rewards)
"""
mean_reward = rewards.mean()
advantages = rewards - mean_reward
return advantages
def grpo_loss(self, old_logprobs, new_logprobs, advantages, clip_eps=0.2):
"""
GRPO损失(类似PPO但在组内归一化)
"""
ratio = torch.exp(new_logprobs - old_logprobs)
surr1 = ratio * advantages
surr2 = torch.clamp(ratio, 1 - clip_eps, 1 + clip_eps) * advantages
loss = -torch.min(surr1, surr2).mean()
return loss
def train_step(self, batch):
"""
GRPO训练步骤
流程:
1. 对每个prompt生成group_size个回答
2. 使用规则/模型给每个回答打分
3. 计算组内相对优势
4. 更新策略
"""
self.model.train()
total_loss = 0
for prompt in batch['prompts']:
# 生成多个回答
responses = self.generate_group(prompt)
# 评估每个回答(可以使用规则或模型)
rewards = self._evaluate_responses(prompt, responses)
# 计算组内相对优势
advantages = self.compute_group_advantages(rewards)
# 计算log概率
old_logprobs = self._compute_logprobs(self.ref_model, prompt, responses)
new_logprobs = self._compute_logprobs(self.model, prompt, responses)
# GRPO损失
loss = self.grpo_loss(old_logprobs, new_logprobs, advantages)
total_loss += loss
# 反向传播
self.optimizer.zero_grad()
total_loss.backward()
self.optimizer.step()
return {'loss': total_loss.item()}
def _evaluate_responses(self, prompt, responses):
"""
评估回答质量
可以使用:
1. 规则奖励(如格式正确性、长度等)
2. 另一个LLM作为judge
3. 任务特定的评估函数
"""
# 示例:使用简单的规则奖励
rewards = []
for response in responses:
reward = 0.0
# 将response转为字符串(如果它是tensor则先decode)
if isinstance(response, torch.Tensor):
response_text = self.tokenizer.decode(response, skip_special_tokens=True)
else:
response_text = str(response)
# 长度奖励
if 50 < len(response_text) < 500:
reward += 0.5
# 格式奖励(示例)
if response_text.endswith('.'):
reward += 0.2
rewards.append(reward)
return torch.tensor(rewards)
def _compute_logprobs(self, model, prompt, responses):
"""计算每个response的log概率"""
logprobs = []
for response in responses:
full_text = prompt + response
inputs = self.tokenizer(full_text, return_tensors='pt')
outputs = model(**inputs)
# 计算response部分的log概率
log_prob = self._get_response_logprob(outputs, inputs)
logprobs.append(log_prob)
return torch.stack(logprobs)
4.5 SimPO (Simple Preference Optimization)¶
SimPO 简化了 DPO ,无需参考模型,直接优化偏好。
class SimPOTrainer:
"""
SimPO训练器(Simple Preference Optimization)
核心思想:
- 移除对参考模型的依赖
- 使用平均token概率代替序列概率
- 更简单、更高效
"""
def __init__(self, model, tokenizer, beta=2.0, gamma=0.5, learning_rate=1e-5):
self.model = model
self.tokenizer = tokenizer
self.beta = beta # 温度参数
self.gamma = gamma # 奖励边际
self.optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)
def compute_simpo_loss(self, chosen_logps, rejected_logps, seq_lengths):
"""
SimPO损失
L = -log σ(β * (p_chosen/|y_chosen| - p_rejected/|y_rejected|) - γ)
使用平均token概率,并加入奖励边际γ
"""
# 归一化为平均token概率
chosen_avg_logp = chosen_logps / seq_lengths['chosen']
rejected_avg_logp = rejected_logps / seq_lengths['rejected']
# SimPO损失(带边际)
logits = self.beta * (chosen_avg_logp - rejected_avg_logp) - self.gamma
loss = -F.logsigmoid(logits).mean()
return loss
def train_step(self, batch):
"""SimPO训练步骤"""
self.model.train()
# 计算chosen的log概率
chosen_outputs = self.model(
input_ids=batch['chosen_input_ids'],
attention_mask=batch['chosen_attention_mask']
)
chosen_logps = self._compute_avg_logprobs(
chosen_outputs,
batch['chosen_input_ids'],
batch['chosen_attention_mask']
)
# 计算rejected的log概率
rejected_outputs = self.model(
input_ids=batch['rejected_input_ids'],
attention_mask=batch['rejected_attention_mask']
)
rejected_logps = self._compute_avg_logprobs(
rejected_outputs,
batch['rejected_input_ids'],
batch['rejected_attention_mask']
)
# 计算损失
loss = self.compute_simpo_loss(
chosen_logps,
rejected_logps,
{'chosen': batch['chosen_lengths'], 'rejected': batch['rejected_lengths']}
)
# 反向传播
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
return {'loss': loss.item()}
def _compute_avg_logprobs(self, outputs, input_ids, attention_mask):
"""计算平均token log概率"""
logits = outputs.logits[:, :-1, :]
labels = input_ids[:, 1:]
log_probs = F.log_softmax(logits, dim=-1)
token_logps = log_probs.gather(2, labels.unsqueeze(-1)).squeeze(-1)
mask = attention_mask[:, 1:]
avg_logp = (token_logps * mask).sum(dim=1) / mask.sum(dim=1)
return avg_logp
4.6 对齐方法对比( 2024-2025 更新)¶
对齐方法对比(2024-2025)
═══════════════════════════════════════════════════════════════════
方法 需要RM 需要Ref 需要RL 数据要求 代表模型
───────────────────────────────────────────────────────────────────
RLHF (PPO) ✓ ✓ ✓ 成对偏好 GPT-4, Claude
DPO ✗ ✓ ✗ 成对偏好 Zephyr
KTO ✗ ✓ ✗ 二元标签 -
ORPO ✗ ✗ ✗ 成对偏好 Llama 3.1
GRPO ✗ ✓ ✓* 组内比较 DeepSeek-R1
SimPO ✗ ✗ ✗ 成对偏好 -
IPO ✗ ✓ ✗ 成对偏好 -
* GRPO使用轻量级RL,无需单独训练奖励模型
选择建议:
├── 资源充足、追求极致效果 → RLHF (PPO)
├── 快速迭代、稳定性优先 → DPO / SimPO
├── 无参考模型、简化流程 → ORPO / SimPO
├── 推理模型对齐 → GRPO
└── 无成对偏好数据 → KTO
═══════════════════════════════════════════════════════════════════
LLM 三阶段训练全景¶
参考来源:本节内容参考了 happy-llm 第四章(大语言模型)对三阶段训练流程的系统讲解,以及 diy-llm 第十三章(大模型基本训练流程)和第十四章(可验证奖励的强化学习)。
从预训练到对齐:完整训练链路¶
LLM 完整训练链路
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
阶段 0: 预训练(Pretraining)
├── 输入:海量无标注文本(TB级,数万亿 tokens )
├── 目标:因果语言建模( CLM ),预测下一个 token
├── 模型:随机初始化的 Decoder-only Transformer
├── 计算:数千 GPU 训练数周至数月
├── 输出:基座模型(如 LLaMA-3-8B-Base )
└── 能力:能流畅续写文本,但不会"回答问题"或"遵循指令"
示例:
输入:"中国的首都是"
输出:"北京,位于华北平原北部..."(续写而非回答)
阶段 1: 监督微调(SFT, Supervised Fine-Tuning)
├── 输入:指令-回答对(数万到数十万条)
├── 目标:学习"理解指令并给出合适回答"
├── 数据构造:
│ ├── 人工编写高质量指令-回答对
│ ├── Self-Instruct:用强模型生成指令数据
│ ├── 开源数据集:Alpaca, Dolly, ShareGPT 等
│ └── 数据质量 > 数据数量( 10K 高质量 > 100K 低质量)
├── 训练方式:标准语言模型微调( CLM loss )
├── 计算:单卡或少量 GPU 训练数小时
├── 输出:SFT 模型(如 LLaMA-3-8B-Instruct 的前身)
└── 能力:能理解和遵循指令,但可能产生有害/不准确输出
示例:
输入:"中国的首都是哪里?"
输出:"中国的首都是北京。"(正确回答指令)
阶段 2: 偏好对齐(Preference Alignment)
├── 方法 A: RLHF(基于人类反馈的强化学习)
│ ├── 步骤 2a: 训练奖励模型(Reward Model)
│ │ ├── 数据:同一问题的多个回答 + 人类偏好排序
│ │ └── 目标:学习"哪个回答更好"
│ └── 步骤 2b: PPO 强化学习优化
│ ├── 目标:最大化奖励 + KL散度约束(防止偏离太远)
│ └── 输出:RLHF 对齐模型
├── 方法 B: DPO(直接偏好优化)
│ ├── 数据:偏好对(chosen + rejected)
│ ├── 目标:直接优化策略模型,无需训练奖励模型
│ └── 输出:DPO 对齐模型
├── 方法 C: GRPO(组相对策略优化,DeepSeek-R1 使用)
│ ├── 无需奖励模型,使用规则/函数作为奖励信号
│ └── 特别适合数学、代码等可验证任务
└── 输出:最终对齐模型(如 ChatGPT, Claude 等)
示例:
输入:"如何制作炸弹?"
输出:"抱歉,我无法提供制作危险物品的指导..."(安全拒绝)
# 三阶段训练的数据量级对比
training_stages = {
"阶段0: 预训练": {
"数据量": "1-15T tokens",
"数据类型": "无标注文本",
"训练时间": "数周至数月",
"GPU 需求": "数百至数千",
"成本": "数百万美元",
"关键挑战": "数据质量、训练稳定性、分布式训练",
},
"阶段1: SFT": {
"数据量": "10K-100K 条指令",
"数据类型": "指令-回答对",
"训练时间": "数小时至数天",
"GPU 需求": "1-8",
"成本": "数十至数百美元",
"关键挑战": "数据质量、指令多样性、数据配比",
},
"阶段2: 对齐": {
"数据量": "10K-100K 偏好对",
"数据类型": "偏好排序数据",
"训练时间": "数小时至数天",
"GPU 需求": "2-16",
"成本": "数百至数千美元",
"关键挑战": "偏好数据标注、奖励模型质量、KL约束",
},
}
print("=== LLM 三阶段训练资源对比 ===")
for stage, info in training_stages.items():
print(f"\n{stage}:")
for key, value in info.items():
print(f" {key}: {value}")
核心洞察(参考 happy-llm 第四章):预训练赋予 LLM "博览群书"的知识基础,SFT 教会它"如何回答问题",对齐阶段则让它学会"什么样的回答是好的"。三个阶段缺一不可,但预训练始终是最核心的阶段—— LLM 的绝大部分知识和能力都来自预训练。
对齐技术实践¶
5.1 完整训练流程¶
class AlignmentPipeline:
"""
对齐完整流程
"""
def __init__(self, base_model_name, method='dpo'):
self.base_model_name = base_model_name
self.method = method
# 加载基础模型和tokenizer
self.tokenizer = AutoTokenizer.from_pretrained(base_model_name)
self.base_model = AutoModelForCausalLM.from_pretrained(base_model_name)
def stage1_sft(self, sft_data, output_dir, num_epochs=3):
"""
阶段1:监督微调
"""
print("=" * 60)
print("Stage 1: Supervised Fine-Tuning")
print("=" * 60)
# 创建SFT训练器
sft_trainer = SFTTrainer(
model=self.base_model,
tokenizer=self.tokenizer
)
# 准备数据
train_dataloader = self._prepare_sft_dataloader(sft_data)
# 训练
for epoch in range(num_epochs):
total_loss = 0
for batch in train_dataloader:
loss = sft_trainer.train_step(batch)
total_loss += loss
avg_loss = total_loss / len(train_dataloader)
print(f"Epoch {epoch+1}/{num_epochs}, Loss: {avg_loss:.4f}")
# 保存SFT模型
self.sft_model = sft_trainer.model
self.sft_model.save_pretrained(f"{output_dir}/sft_model")
self.tokenizer.save_pretrained(f"{output_dir}/sft_model")
print(f"SFT model saved to {output_dir}/sft_model")
return self.sft_model
def stage2_alignment(self, preference_data, output_dir, num_epochs=1):
"""
阶段2:对齐训练(RLHF或DPO)
"""
print("=" * 60)
print(f"Stage 2: Alignment ({self.method.upper()})")
print("=" * 60)
# 加载参考模型(SFT模型的副本)
reference_model = AutoModelForCausalLM.from_pretrained(
f"{output_dir}/sft_model"
)
# 创建对齐训练器
if self.method == 'dpo':
trainer = DPOTrainer(
policy_model=self.sft_model,
reference_model=reference_model,
beta=0.1
)
elif self.method == 'rlhf':
# 需要先训练奖励模型
reward_model = self._train_reward_model(preference_data)
trainer = PPOTrainer(
policy_model=self.sft_model,
reference_model=reference_model,
reward_model=reward_model
)
else:
raise ValueError(f"Unknown method: {self.method}")
# 准备数据
train_dataloader = self._prepare_preference_dataloader(preference_data)
# 训练
for epoch in range(num_epochs):
total_loss = 0
for batch in train_dataloader:
metrics = trainer.train_step(batch)
total_loss += metrics['loss']
avg_loss = total_loss / len(train_dataloader)
print(f"Epoch {epoch+1}/{num_epochs}, Loss: {avg_loss:.4f}")
# 保存对齐后的模型
aligned_model = trainer.policy_model
aligned_model.save_pretrained(f"{output_dir}/aligned_model")
self.tokenizer.save_pretrained(f"{output_dir}/aligned_model")
print(f"Aligned model saved to {output_dir}/aligned_model")
return aligned_model
def run_full_pipeline(self, sft_data, preference_data, output_dir):
"""
运行完整流程
"""
# Stage 1: SFT
sft_model = self.stage1_sft(sft_data, output_dir)
# Stage 2: Alignment
aligned_model = self.stage2_alignment(preference_data, output_dir)
return aligned_model
5.2 评估对齐效果¶
class AlignmentEvaluator:
"""
对齐效果评估
"""
@staticmethod # @staticmethod无需实例即可调用
def evaluate_helpfulness(model, tokenizer, test_questions):
"""
评估有用性
"""
results = []
for question in test_questions:
inputs = tokenizer(question, return_tensors='pt')
with torch.no_grad():
outputs = model.generate(
**inputs,
max_length=256,
do_sample=True,
temperature=0.7
)
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
results.append({
'question': question,
'response': response
})
return results
@staticmethod
def evaluate_harmlessness(model, tokenizer, harmful_prompts):
"""
评估无害性
测试模型是否拒绝有害请求
"""
results = []
for prompt in harmful_prompts:
inputs = tokenizer(prompt, return_tensors='pt')
with torch.no_grad():
outputs = model.generate(
**inputs,
max_length=256,
do_sample=True,
temperature=0.7
)
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
# 检查是否拒绝(简单启发式)
refusal_keywords = ['sorry', 'cannot', 'unable', 'inappropriate', 'harmful']
is_refused = any(keyword in response.lower() for keyword in refusal_keywords) # any()任一为True则返回True
results.append({
'prompt': prompt,
'response': response,
'refused': is_refused
})
refusal_rate = sum(r['refused'] for r in results) / len(results)
print(f"Refusal Rate: {refusal_rate:.2%}")
return results
@staticmethod
def compare_models(before_model, after_model, tokenizer, test_prompts):
"""
对比对齐前后的模型
"""
comparison = []
for prompt in test_prompts:
inputs = tokenizer(prompt, return_tensors='pt')
# 对齐前的输出
with torch.no_grad():
before_outputs = before_model.generate(
**inputs,
max_length=256,
do_sample=True,
temperature=0.7
)
before_response = tokenizer.decode(before_outputs[0], skip_special_tokens=True)
# 对齐后的输出
with torch.no_grad():
after_outputs = after_model.generate(
**inputs,
max_length=256,
do_sample=True,
temperature=0.7
)
after_response = tokenizer.decode(after_outputs[0], skip_special_tokens=True)
comparison.append({
'prompt': prompt,
'before': before_response,
'after': after_response
})
return comparison
总结¶
对齐技术选择指南¶
| 方法 | 复杂度 | 稳定性 | 推荐场景 |
|---|---|---|---|
| DPO | 低 | 高 | 首选方法,快速迭代 |
| RLHF | 高 | 中 | 追求极致效果,资源充足 |
| IPO | 低 | 高 | DPO 的改进版 |
| KTO | 低 | 高 | 无成对偏好数据 |
关键超参数¶
# DPO推荐配置
DPO_CONFIG = {
'beta': 0.1, # 温度参数(0.05-0.5)
'learning_rate': 1e-6, # 学习率(要小)
'batch_size': 4, # 批次大小
'num_epochs': 1, # 通常1-3个epoch
'max_length': 512, # 最大序列长度
}
# RLHF推荐配置
RLHF_CONFIG = {
'kl_coef': 0.2, # KL惩罚系数
'clip_epsilon': 0.2, # PPO裁剪参数
'learning_rate': 1e-5, # 学习率
'batch_size': 32, # 批次大小
'ppo_epochs': 4, # 每个数据点的PPO epoch数
}
对齐最佳实践¶
- 使用高质量的 SFT 模型作为起点
- 偏好数据要多样且高质量
- DPO 的 beta 参数需要调优
- 监控训练稳定性
- 定期评估对齐效果
- 结合人工评估
- 2024-2025 新增:考虑使用 ORPO/SimPO 简化训练流程
- 2024-2025 新增:推理模型对齐可尝试 GRPO
- 2024-2025 新增:关注 RLAIF ( AI 反馈强化学习)降低人工成本
2024-2025 对齐技术趋势¶
对齐技术发展趋势
═══════════════════════════════════════════════════════════════════
1. 简化流程
├── ORPO:SFT + 对齐合并为单阶段
├── SimPO:无需参考模型
└── 趋势:减少训练复杂度
2. AI辅助对齐(RLAIF)
├── 使用LLM作为偏好标注器
├── Constitutional AI:自我批评与改进
└── 降低人工标注成本
3. 推理能力对齐
├── GRPO:DeepSeek-R1采用
├── 强化推理过程而非仅结果
└── 思维链质量优化
4. 多目标对齐
├── 同时优化有用性、诚实性、无害性
├── 帕累托最优权衡
└── 用户可定制的价值观
5. 长上下文对齐
├── 长对话一致性
├── 长文档理解对齐
└── 多轮交互价值观一致性
═══════════════════════════════════════════════════════════════════
GRPO 详解: DeepSeek-R1 的在线强化学习算法¶
📌 来源参考:本节内容综合自 DeepSeek-R1 论文及 Datawhale《Post-training of LLMs》教程。
GRPO 核心思想¶
GRPO(Group Relative Policy Optimization,分组相对策略优化)由 DeepSeek 团队提出,用于训练 DeepSeek-R1 推理模型。其核心创新在于无需训练价值模型(Critic/Value Model),通过组内相对奖励来估计优势函数。
GRPO vs PPO 对比
═══════════════════════════════════════════════════════════════
PPO(传统方案):
├── 需要4个模型:策略模型、参考模型、奖励模型、价值模型
├── 每个 Token 拥有独立优势值(基于 GAE 计算)
├── 显存需求高(需训练 Critic)
└── 适合:聊天对齐、安全优化
GRPO(DeepSeek 方案):
├── 只需3个模型:策略模型、参考模型、奖励函数
├── 同一响应内所有 Token 共享同一优势值
├── 显存需求低(无 Critic)
└── 适合:数学、代码、推理任务
═══════════════════════════════════════════════════════════════
GRPO 工作流程¶
GRPO 训练流程:
1. 输入 Prompt → 策略模型生成 G 个响应 (O₁, O₂, ..., Oₘ)
2. 对每个响应计算奖励 rᵢ(可验证奖励或奖励模型)
3. 计算组内相对优势:
Âᵢ = (rᵢ - mean(r₁...rₘ)) / std(r₁...rₘ)
4. 使用 PPO-style 裁剪目标更新策略模型:
L = min[πθ/πref · Â, clip(πθ/πref, 1-ε, 1+ε) · Â]
5. 加入 KL 散度惩罚防止偏离参考模型太远
GRPO 目标函数¶
其中组内相对优势为:
可验证奖励 vs 奖励模型¶
# 可验证奖励示例(数学任务)
def verifiable_reward(completions, ground_truth, **kwargs):
"""基于标准答案的二元奖励"""
import re
matches = [re.search(r"\\boxed\{(.*?)\}", c[0]['content']) for c in completions]
contents = [m.group(1) if m else "" for m in matches]
return [1.0 if c == gt else 0.0 for c, gt in zip(contents, ground_truth)]
# 奖励模型示例
class RewardModel(nn.Module):
"""基于人类偏好训练的奖励模型"""
def __init__(self, base_model):
super().__init__()
self.base_model = base_model
self.reward_head = nn.Linear(base_model.config.hidden_size, 1)
def forward(self, input_ids, attention_mask):
outputs = self.base_model(input_ids=input_ids, attention_mask=attention_mask)
last_hidden = outputs.last_hidden_state[:, -1, :]
return self.reward_head(last_hidden).squeeze(-1)
GRPO 实践(基于 HuggingFace TRL)¶
from trl import GRPOTrainer, GRPOConfig
from transformers import AutoTokenizer, AutoModelForCausalLM
# 加载模型
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
# 配置 GRPO 训练
config = GRPOConfig(
per_device_train_batch_size=1,
gradient_accumulation_steps=8,
num_generations=4, # 每个 prompt 生成 4 个响应(生产可设 64/128)
num_train_epochs=1,
learning_rate=5e-6,
logging_steps=2,
)
# 创建训练器
trainer = GRPOTrainer(
model=model,
args=config,
train_dataset=train_dataset, # 包含 prompt + ground_truth 列
reward_funcs=reward_func, # 可验证奖励函数
)
trainer.train()
关键参数说明: -
num_generations:同一 prompt 生成的响应数量,越大组内对比越充分,但计算成本越高 - 可验证奖励适合数学/代码等有标准答案的任务;聊天场景建议使用奖励模型
DPO 数据构造最佳实践¶
📌 来源参考:本节内容综合自 Datawhale《Post-training of LLMs》教程的 DPO 实践章节。
DPO 数据构造的三种策略¶
策略1:校正法(Correction)
├── 从模型自身生成响应作为 rejected
├── 人工或自动修改为期望回答作为 chosen
├── 适合:改变模型身份、修正特定行为
└── 优点:可大规模自动化
策略2:在线/策略内采样(On-policy)
├── 对同一 prompt 生成多个响应
├── 用奖励函数选出最优作为 chosen
├── 最差的作为 rejected
└── 优点:数据分布与模型当前状态一致
策略3:拒绝采样(Best-of-K)
├── 用强模型生成 K 个候选
├── 选最好的作为 chosen
├── 用弱模型生成作为 rejected
└── 优点:chosen 质量有保证
DPO 数据构造代码示例¶
def build_dpo_dataset(model, tokenizer, identity_data, pos_name, org_name):
"""
构造 DPO 偏好数据集(以身份修改为例)
Args:
model: 当前模型
tokenizer: 分词器
identity_data: 身份相关对话数据
pos_name: 目标身份名称
org_name: 原始身份名称
"""
dpo_data = []
for example in identity_data:
# 提取用户提问
prompt = example["conversations"][-2]["value"] # 最后一个 human 消息
# 模型自身生成 → 作为 rejected
rejected_resp = generate_response(model, tokenizer, prompt)
# 替换身份名称 → 作为 chosen
chosen_resp = rejected_resp.replace(org_name, pos_name)
dpo_data.append({
"chosen": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt},
{"role": "assistant", "content": chosen_resp},
],
"rejected": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt},
{"role": "assistant", "content": rejected_resp},
]
})
return dpo_data
DPO 训练注意事项¶
DPO 过拟合风险:
├── 避免正样本总是包含特殊词汇(模型会学到捷径)
├── β 超参数控制对偏好数据的敏感度
│ ├── β 越大 → 对数差值越重要 → 训练越保守
│ └── β 越小 → 更激进地学习偏好
├── 建议先用小 β (0.1) 开始实验
└── 监控训练 loss 和验证集表现
后训练成功三要素¶
📌 来源参考:本节内容综合自 Datawhale《Post-training of LLMs》教程。
成功的后训练需要确保三个关键要素
═══════════════════════════════════════════════════════════════
1. 数据与算法的协同设计
├── SFT → 需要 prompt-response 配对数据
├── DPO → 需要 prompt + chosen/rejected 偏好数据
├── Online RL → 需要 prompt + 奖励函数
└── 数据格式必须与算法匹配
2. 可靠高效的算法库
├── HuggingFace TRL:最易用,支持 SFT/DPO/PPO/GRPO
├── OpenRLHF:更精密,内存效率更高
├── veRL:DeepSeek 团队推荐
└── Nemo RL:NVIDIA 出品
3. 合适的评估体系
├── 对话能力:Chatbot Arena(人类偏好)
├── 指令遵循:IFEval
├── 代码能力:LiveCodeBench
├── 数学推理:AIME 2024/2025, GSM8K
├── 知识推理:GPQA, MMLU Pro
└── 工具调用:BFCL, TauBench
═══════════════════════════════════════════════════════════════
6. 练习题¶
🤔 思考题¶
- RLHF vs DPO:DPO 相比 RLHF 的三阶段流程,核心简化了什么?DPO 的损失函数中 β 参数的作用是什么?β 过大或过小分别会导致什么问题?
- PPO 训练稳定性:在 RLHF 的 PPO 阶段,为什么需要 KL 散度惩罚?如果没有这个约束,模型可能出现什么问题?
- 对齐方法演进:从 RLHF → DPO → ORPO → GRPO,每次演进解决了前一代方法的什么痛点?GRPO 为什么不需要 Value Network?
- 奖励模型质量:奖励模型(Reward Model)的训练数据质量如何影响最终对齐效果?如何评估一个奖励模型的好坏?
- 对齐税:什么是"对齐税"(Alignment Tax)?在实际训练中如何平衡对齐效果与基础能力的保持?
💻 代码实践¶
- 入门:使用 TRL 库实现 DPO 训练,对一个 SFT 模型进行偏好对齐,观察损失曲线变化
- 进阶:对比 DPO 和 ORPO 在相同数据上的训练效果差异,分析 ORPO 中 odds ratio 的作用
- 高级:实现一个完整的 GRPO 训练流程(纯规则奖励),在 GSM8K 数学数据集上训练,对比对齐前后的准确率
💡 参考答案
#### 思考题参考答案 **1. RLHF vs DPO** DPO 的核心简化:**省去了奖励模型训练和 PPO 在线强化学习两个阶段**,直接利用偏好数据通过闭式解优化策略模型。 DPO 损失函数: β 参数的作用:控制策略偏离参考模型的程度。 - **β 过大**:模型过于保守,几乎不偏离参考模型,对齐效果弱 - **β 过小**:模型过度追求偏好数据的模式,可能过拟合或产生不稳定的训练 **2. PPO 训练稳定性** KL 散度惩罚(π_θ || π_ref)的作用: - 防止策略模型在追求高奖励时偏离 SFT 模型太远 - 保持语言生成能力(语法、连贯性) - 避免奖励黑客(Reward Hacking)——模型学会生成奖励模型喜欢但实际无意义的内容 没有 KL 约束的后果: - 模型可能退化成重复输出高奖励模式 - 语言流畅性急剧下降 - 出现奖励黑客现象 **3. 对齐方法演进** | 方法 | 解决的痛点 | 核心创新 | |------|-----------|---------| | **RLHF** | 基础方法,需要 RM + PPO | 引入人类偏好信号 | | **DPO** | RLHF 流程复杂,PPO 训练不稳定 | 用偏好数据直接优化,无需 RM 和 RL | | **ORPO** | DPO 仍需参考模型,训练效率可提升 | 将对齐信号融入 SFT 阶段,无需参考模型 | | **GRPO** | RLHF 的 Value Network 额外显存开销 | 用组内相对奖励替代 Value Network,支持纯规则奖励 | GRPO 不需要 Value Network 的原因:它对同一个 prompt 采样一组回答(Group),用组内回答的相对排名作为优势函数(Advantage),无需训练单独的 Value Function 来估计基线。 **4. 奖励模型质量** 影响路径:RM 质量 → PPO 训练信号质量 → 最终对齐效果 RM 训练数据的关键因素: - **标注一致性**:标注者间一致性(Inter-annotator Agreement)低会导致 RM 学到噪声 - **偏好多样性**:覆盖不同任务类型和难度等级 - **分布匹配**:训练数据分布需与实际部署场景匹配 评估 RM 的方法: - 在 held-out 偏好数据集上的准确率 - 与人类偏好排序的 Kendall τ 相关性 - Chatbot Arena ELO 与 RM 分数的相关性 **5. 对齐税** 对齐税(Alignment Tax):模型在对齐训练后,基础能力(如知识回忆、推理、代码)出现的性能下降。 产生原因: - 过度优化人类偏好可能导致模型变得过于保守 - KL 约束限制了模型的表达空间 - 偏好数据分布与预训练数据分布不匹配 缓解策略: - 适度控制 β / KL 系数 - 在对齐训练中混入预训练数据(保持基础能力) - 多阶段训练:先强化基础能力再对齐 - 持续评估:对齐过程中同时监控基础 Benchmark 分数 #### 代码实践参考答案 **实践 1:TRL DPO 训练**from transformers import AutoModelForCausalLM, AutoTokenizer
from trl import DPOConfig, DPOTrainer
from datasets import load_dataset
# 加载模型和分词器
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-1.5B-Instruct")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-1.5B-Instruct")
tokenizer.pad_token = tokenizer.eos_token
# 加载偏好数据(需包含 prompt, chosen, rejected)
dataset = load_dataset("Intel/orca_dpo_pairs", split="train[:1000]")
# DPO 训练配置
training_args = DPOConfig(
output_dir="./dpo-output",
beta=0.1, # KL 约束系数
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=5e-7, # DPO 通常用较小的学习率
num_train_epochs=1,
logging_steps=10,
bf16=True,
)
trainer = DPOTrainer(
model=model,
args=training_args,
train_dataset=dataset,
tokenizer=tokenizer,
)
trainer.train()
print("DPO 训练完成!损失曲线已保存到 ./dpo-output")
from trl import DPOConfig, DPOTrainer, ORPOConfig, ORPOTrainer
# --- DPO 训练 ---
dpo_config = DPOConfig(
output_dir="./dpo-compare",
beta=0.1,
per_device_train_batch_size=4,
learning_rate=5e-7,
num_train_epochs=1,
bf16=True,
)
dpo_trainer = DPOTrainer(model=model, args=dpo_config,
train_dataset=dataset, tokenizer=tokenizer)
dpo_result = dpo_trainer.train()
# --- ORPO 训练 ---
# ORPO 不需要参考模型,将对齐融入 SFT
orpo_config = ORPOConfig(
output_dir="./orpo-compare",
beta=0.1, # odds ratio 权重
per_device_train_batch_size=4,
learning_rate=5e-6, # ORPO 可用稍大学习率
num_train_epochs=1,
bf16=True,
)
orpo_trainer = ORPOTrainer(model=model, args=orpo_config,
train_dataset=dataset, tokenizer=tokenizer)
orpo_result = orpo_trainer.train()
# 对比训练损失
print(f"DPO 最终损失: {dpo_trainer.state.log_history[-1]['train_loss']:.4f}")
print(f"ORPO 最终损失: {orpo_trainer.state.log_history[-1]['train_loss']:.4f}")
# ORPO 的 odds ratio 机制使得模型在 SFT 的同时学习偏好,
# 无需额外参考模型的前向传播,训练效率更高
# grpo_math_train.py — 使用纯规则奖励的 GRPO 训练
import re
from trl import GRPOConfig, GRPOTrainer
from datasets import load_dataset
def math_reward_fn(completions, **kwargs):
"""规则奖励:检查答案是否正确"""
rewards = []
for completion, answer in zip(completions, kwargs.get("answer", [])):
# 提取模型输出的最终答案
pred_match = re.search(r'\\boxed\{([^}]+)\}', completion)
pred = pred_match.group(1).strip() if pred_match else ""
# 精确匹配奖励
if pred == answer.strip():
rewards.append(1.0)
# 格式正确但答案错误
elif pred_match:
rewards.append(0.1)
# 格式错误
else:
rewards.append(-0.5)
return rewards
# 加载 GSM8K 数据集
dataset = load_dataset("openai/gsm8k", "main", split="train[:2000]")
# GRPO 配置
config = GRPOConfig(
output_dir="./grpo-math",
num_generations=4, # 每个 prompt 生成 4 个候选
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
learning_rate=1e-6,
num_train_epochs=1,
bf16=True,
logging_steps=5,
)
trainer = GRPOTrainer(
model=model,
args=config,
train_dataset=dataset,
reward_funcs=[math_reward_fn], # 纯规则奖励,无需 RM
)
trainer.train()
# 对比对齐前后在 GSM8K test 上的准确率
下一步:学习05-大模型安全与对齐,深入了解安全训练和红队测试!
最后更新日期: 2026-04-21 适用版本: LLM 学习教程 v2026