跳转至

04 - 对齐技术

⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。

📌 定位说明:本章是对齐技术的主版本,侧重 RLHF/DPO/PPO 等对齐方法的全景原理。 - 📖 应用安全与合规视角请参考 LLM 应用/14-大模型安全与对齐

学习目标:深入理解大模型对齐技术,包括 RLHF 、 DPO 、 PPO 等方法的原理与实现。


目录

  1. 对齐技术概述
  2. 基于人类反馈的强化学习 (RLHF)
  3. 直接偏好优化 (DPO)
  4. 其他对齐方法
  5. 对齐技术实践

对齐技术概述

1.1 什么是对齐

Text Only
对齐(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 三阶段流程

Text Only
RLHF完整流程
═══════════════════════════════════════════════════════════════════

阶段1: 监督微调(SFT)
├── 数据:人类编写的指令-回答对
├── 目标:让模型学会遵循指令
├── 方法:标准语言模型微调
└── 输出:SFT模型

阶段2: 奖励模型训练(Reward Modeling)
├── 数据:同一问题的多个回答,人类标注偏好
├── 目标:学习人类偏好
├── 方法:训练奖励模型预测人类偏好
└── 输出:Reward Model

阶段3: 强化学习优化(RL Optimization)
├── 数据:使用SFT模型生成回答
├── 目标:最大化奖励,同时保持与SFT模型的相似性
├── 方法:PPO算法
└── 输出:对齐后的模型

═══════════════════════════════════════════════════════════════════

2.2 阶段 1 :监督微调 (SFT)

Python
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 :奖励模型训练

Python
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 强化学习

Python
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 原理

Text Only
DPO(Direct Preference Optimization)

核心思想:
├── 不需要显式的奖励模型
├── 不需要强化学习
├── 直接用偏好数据优化策略
└── 更简单、更稳定

Bradley-Terry 模型到 DPO 损失的完整推导

第一步: Bradley-Terry 偏好模型

给定提示 \(x\),人类更偏好回答 \(y_w\) 而非 \(y_l\) 的概率为:

\[ P(y_w \succ y_l \mid x) = \sigma(r(x, y_w) - r(x, y_l)) \]

其中 \(r(x, y)\) 是真实奖励函数,\(\sigma\) 是 sigmoid 函数。

第二步: RLHF 的 KL 约束优化目标

RLHF 要解决的优化问题是:

\[ \max_{\pi_\theta} \; \mathbb{E}_{x \sim D, \, y \sim \pi_\theta(\cdot|x)}[r(x, y)] - \beta \, \text{KL}[\pi_\theta(\cdot|x) \| \pi_{\text{ref}}(\cdot|x)] \]

第三步:最优策略的闭式解

对上式求解(拉格朗日对偶),最优策略为:

\[ \pi^*(y \mid x) = \frac{1}{Z(x)} \pi_{\text{ref}}(y \mid x) \exp\!\left(\frac{r(x, y)}{\beta}\right) \]

其中 \(Z(x) = \sum_y \pi_{\text{ref}}(y|x) \exp(r(x,y)/\beta)\) 是配分函数。

第四步:用策略表示奖励(关键步骤)

对上式取对数并整理,可以用策略反解出奖励:

\[ r(x, y) = \beta \log \frac{\pi^*(y \mid x)}{\pi_{\text{ref}}(y \mid x)} + \beta \log Z(x) \]

第五步:代入 Bradley-Terry 消除奖励

将上式代入 Bradley-Terry 模型(\(Z(x)\) 项在做差时对消):

\[ P(y_w \succ y_l \mid x) = \sigma\!\left(\beta \log \frac{\pi^*(y_w|x)}{\pi_{\text{ref}}(y_w|x)} - \beta \log \frac{\pi^*(y_l|x)}{\pi_{\text{ref}}(y_l|x)}\right) \]

第六步: DPO 损失函数

用当前策略 \(\pi_\theta\) 代替最优策略 \(\pi^*\),对偏好数据集取负对数似然:

\[ \mathcal{L}_{\text{DPO}}(\pi_\theta; \pi_{\text{ref}}) = -\mathbb{E}_{(x, y_w, y_l) \sim D}\left[\log \sigma\!\left(\beta \log \frac{\pi_\theta(y_w|x)}{\pi_{\text{ref}}(y_w|x)} - \beta \log \frac{\pi_\theta(y_l|x)}{\pi_{\text{ref}}(y_l|x)}\right)\right] \]
Text Only
推导链总结:
Bradley-Terry偏好模型
    ↓ 定义了"奖励差→偏好概率"的映射
KL约束优化目标
    ↓ 拉格朗日对偶求解
最优策略闭式解 π*(y|x)
    ↓ 取对数,反解出 r(x,y)
用策略比率表达奖励
    ↓ 代入Bradley-Terry,Z(x)对消
DPO损失函数(无需奖励模型!)

3.2 DPO 实现

Python
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

Text Only
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)

Python
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)

Python
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 和对齐合并为单一训练阶段。

Python
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 使用的对齐方法,特别适合推理模型的对齐训练。

Python
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 ,无需参考模型,直接优化偏好。

Python
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 更新)

Text Only
对齐方法对比(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 第十三章(大模型基本训练流程)和第十四章(可验证奖励的强化学习)。

从预训练到对齐:完整训练链路

Text Only
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 等)
    示例:
    输入:"如何制作炸弹?"
    输出:"抱歉,我无法提供制作危险物品的指导..."(安全拒绝)
Python
# 三阶段训练的数据量级对比
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 完整训练流程

Python
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 评估对齐效果

Python
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 无成对偏好数据

关键超参数

Python
# 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 对齐技术趋势

Text Only
对齐技术发展趋势
═══════════════════════════════════════════════════════════════════

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),通过组内相对奖励来估计优势函数。

Text Only
GRPO vs PPO 对比
═══════════════════════════════════════════════════════════════

PPO(传统方案):
├── 需要4个模型:策略模型、参考模型、奖励模型、价值模型
├── 每个 Token 拥有独立优势值(基于 GAE 计算)
├── 显存需求高(需训练 Critic)
└── 适合:聊天对齐、安全优化

GRPO(DeepSeek 方案):
├── 只需3个模型:策略模型、参考模型、奖励函数
├── 同一响应内所有 Token 共享同一优势值
├── 显存需求低(无 Critic)
└── 适合:数学、代码、推理任务
═══════════════════════════════════════════════════════════════

GRPO 工作流程

Text Only
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 目标函数

\[\mathcal{J}_{GRPO}(\theta) = \mathbb{E}\left[\frac{1}{G}\sum_{i=1}^{G}\min\left[\frac{\pi_\theta(o_i)}{\pi_{ref}(o_i)}\hat{A}_i, \text{clip}\left(\frac{\pi_\theta(o_i)}{\pi_{ref}(o_i)}, 1-\varepsilon, 1+\varepsilon\right)\hat{A}_i\right] - \beta \cdot D_{KL}\right]\]

其中组内相对优势为:

\[\hat{A}_i = \frac{r_i - \text{mean}(r_1, ..., r_G)}{\text{std}(r_1, ..., r_G)}\]

可验证奖励 vs 奖励模型

Python
# 可验证奖励示例(数学任务)
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)

Python
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 数据构造的三种策略

Text Only
策略1:校正法(Correction)
├── 从模型自身生成响应作为 rejected
├── 人工或自动修改为期望回答作为 chosen
├── 适合:改变模型身份、修正特定行为
└── 优点:可大规模自动化

策略2:在线/策略内采样(On-policy)
├── 对同一 prompt 生成多个响应
├── 用奖励函数选出最优作为 chosen
├── 最差的作为 rejected
└── 优点:数据分布与模型当前状态一致

策略3:拒绝采样(Best-of-K)
├── 用强模型生成 K 个候选
├── 选最好的作为 chosen
├── 用弱模型生成作为 rejected
└── 优点:chosen 质量有保证

DPO 数据构造代码示例

Python
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 训练注意事项

Text Only
DPO 过拟合风险:
├── 避免正样本总是包含特殊词汇(模型会学到捷径)
├── β 超参数控制对偏好数据的敏感度
│   ├── β 越大 → 对数差值越重要 → 训练越保守
│   └── β 越小 → 更激进地学习偏好
├── 建议先用小 β (0.1) 开始实验
└── 监控训练 loss 和验证集表现

后训练成功三要素

📌 来源参考:本节内容综合自 Datawhale《Post-training of LLMs》教程。

Text Only
成功的后训练需要确保三个关键要素
═══════════════════════════════════════════════════════════════

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. 练习题

🤔 思考题

  1. RLHF vs DPO:DPO 相比 RLHF 的三阶段流程,核心简化了什么?DPO 的损失函数中 β 参数的作用是什么?β 过大或过小分别会导致什么问题?
  2. PPO 训练稳定性:在 RLHF 的 PPO 阶段,为什么需要 KL 散度惩罚?如果没有这个约束,模型可能出现什么问题?
  3. 对齐方法演进:从 RLHF → DPO → ORPO → GRPO,每次演进解决了前一代方法的什么痛点?GRPO 为什么不需要 Value Network?
  4. 奖励模型质量:奖励模型(Reward Model)的训练数据质量如何影响最终对齐效果?如何评估一个奖励模型的好坏?
  5. 对齐税:什么是"对齐税"(Alignment Tax)?在实际训练中如何平衡对齐效果与基础能力的保持?

💻 代码实践

  1. 入门:使用 TRL 库实现 DPO 训练,对一个 SFT 模型进行偏好对齐,观察损失曲线变化
  2. 进阶:对比 DPO 和 ORPO 在相同数据上的训练效果差异,分析 ORPO 中 odds ratio 的作用
  3. 高级:实现一个完整的 GRPO 训练流程(纯规则奖励),在 GSM8K 数学数据集上训练,对比对齐前后的准确率
💡 参考答案 #### 思考题参考答案 **1. RLHF vs DPO** DPO 的核心简化:**省去了奖励模型训练和 PPO 在线强化学习两个阶段**,直接利用偏好数据通过闭式解优化策略模型。 DPO 损失函数:
Text Only
L_DPO = -E[log σ(β · (log π_θ(y_w|x) / π_ref(y_w|x) - log π_θ(y_l|x) / π_ref(y_l|x)))]
β 参数的作用:控制策略偏离参考模型的程度。 - **β 过大**:模型过于保守,几乎不偏离参考模型,对齐效果弱 - **β 过小**:模型过度追求偏好数据的模式,可能过拟合或产生不稳定的训练 **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 训练**
Python
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")
**实践 2:DPO vs ORPO 对比**
Python
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 的同时学习偏好,
# 无需额外参考模型的前向传播,训练效率更高
**实践 3:GRPO 数学训练**
Python
# 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