AI人类反馈强化学习实践笔记

在这次实践之前,我的模型训练经验主要停留在监督学习阶段:准备数据、训练、评估。

为什么需要 RLHF

在这次实践之前,我的模型训练经验主要停留在监督学习阶段:准备数据、训练、评估。看起来很完美,但实际用起来总有问题:

  • 模型会按照训练数据的模式回答,但不考虑真实意图
  • 对同一个问题多次询问,回答可能完全不同
  • 明明知道不能说某些内容,换个问法就"忘了"

这些问题的根源在于:监督学习只教会模型"生成合理的文本",但没教会它"生成符合人类期望的文本"。

RLHF 的核心思路很简单:用人类标注的数据训练一个奖励模型,再用这个奖励模型指导原始模型生成更好的回答。说起来容易,做起来全是坑。

RLHF 的基本流程

先放一张流程图,把整体说清楚:

graph TB A[预训练模型] --> B[收集人类偏好数据] B --> C[训练奖励模型] C --> D[使用奖励模型优化原始模型] D --> E[对齐后的模型] style A fill:#e1f5ff style E fill:#c8f7dc

这个流程看起来很标准,但每个环节都有实践上的陷阱。

数据准备阶段

收集人类偏好数据

第一步是收集人类对模型回答的偏好数据。理论上需要准备大量成对的回答,让人工标注哪个更好。

我这里用的是开源的偏好数据集,包括:

  • HH-RLHF:Hugging Face 的 Anthropic RLHF 数据集
  • OpenAssistant Conversations:开源的对话数据集
  • WebGPT Comparisons:WebGPT 的偏好对比数据

数据量不算小,但质量参差不齐。有些标注明显不一致:同样的回答类型在不同样本里被标记为"更好"或"更差"。

数据清洗花了挺长时间,主要做了这些事:

def clean_preference_data(raw_data):
    cleaned = []
    for item in raw_data:
        # 过滤掉长度差异过大的对比
        len_diff = abs(len(item['chosen']) - len(item['rejected']))
        if len_diff > len(item['chosen']) * 0.5:
            continue

        # 检查标注一致性
        if item['chosen'] == item['rejected']:
            continue

        cleaned.append(item)
    return cleaned

这个清洗过程丢失了大约 15% 的数据,但剩下的质量明显更好。

数据标注的陷阱

如果想自己标注数据,很快会发现这个问题:人类标注员的主观性很强。

同样的问题和回答,不同标注员可能给出完全相反的判断。这会让奖励模型学到奇怪的偏好——模型可能会把"更像某个人风格"的回答误认为是"更好的回答"。

另一个坑是标注疲劳。长时间做同样类型的标注,人就会开始"眼熟",不自觉地按模式而不是按质量判断。我尝试过让标注员休息后再检查之前的工作,发现大概 10% 的标注会被自己推翻。

奖励模型训练

模型选择

奖励模型本质上是一个分类器,输入是问题和两个候选回答,输出是哪个回答更好。

我用的是较小的模型(比如 7B)作为奖励模型,原因很简单:

  • 计算资源有限,大模型训练成本太高
  • 奖励模型不需要生成能力,只需要判断能力
  • 小模型训练速度快,实验迭代更方便
class RewardModel(nn.Module):
    def __init__(self, base_model):
        super().__init__()
        self.base = base_model
        self.score_head = nn.Linear(base_model.config.hidden_size, 1)

    def forward(self, input_ids, attention_mask):
        outputs = self.base(input_ids=input_ids, attention_mask=attention_mask)
        last_hidden = outputs.last_hidden_state[:, 0, :]
        return self.score_head(last_hidden)

这个结构很标准,但实际训练时发现两个问题:

训练不稳定

奖励模型训练经常出现这种情况:训练 loss 一直在降,但在验证集上的表现反而变差。

原因在于奖励模型学会了一些"作弊技巧":

  • 偏好某些特定格式的回答(比如总是喜欢带列表的)
  • 偏好某些特定长度的回答
  • 甚至记住了某些特定问题的答案模式

解决办法是在训练中加入更多正则化和数据增强:

def compute_loss_with_regularization(chosen_scores, rejected_scores, chosen_input_ids, rejected_input_ids, lambda_reg=0.01):
    # 标准的偏好损失
    loss = -torch.mean(torch.log(torch.sigmoid(chosen_scores - rejected_scores)))

    # 长度正则化,防止偏好特定长度
    chosen_lengths = (chosen_input_ids != tokenizer.pad_token_id).sum(dim=1)
    rejected_lengths = (rejected_input_ids != tokenizer.pad_token_id).sum(dim=1)
    length_penalty = lambda_reg * torch.mean(torch.abs(chosen_lengths - rejected_lengths))

    return loss + length_penalty

数据不平衡问题

现实数据中,“明显更好"和"稍微好一点"的情况混在一起。如果奖励模型把这两类都当成同样的信号,就会学到一种"温和"的判断标准——对好坏差异很大的情况反而判断不准。

我尝试过对偏好强度进行分级,但标注成本太高。最后采用的折中方案是:在训练时加入一些合成数据,明确标注"强偏好"和"弱偏好”,让模型学会区分不同强度的偏好。

PPO 优化阶段

到了这一步,事情开始变得有趣,也变得危险。PPO(Proximal Policy Optimization)是用奖励模型优化原始模型的关键算法,但这一步最容易把模型"学坏"。

PPO 的基本设置

PPO 的目标是让模型生成更高奖励的回答,同时避免偏离原始模型太远。

def ppo_loss(log_probs, old_log_probs, advantages, clip_epsilon=0.2):
    ratio = torch.exp(log_probs - old_log_probs)
    clipped_ratio = torch.clamp(ratio, 1 - clip_epsilon, 1 + clip_epsilon)

    # PPO 损失的两种形式,取最小值
    unclipped_loss = -ratio * advantages
    clipped_loss = -clipped_ratio * advantages
    return torch.mean(torch.maximum(unclipped_loss, clipped_loss))

这里的 advantages 是当前回答和参考回答的奖励差异。理论上,奖励高的回答应该被鼓励生成,奖励低的应该被抑制。

奖励黑客问题

实践中很快发现:模型学会了"骗"奖励模型。

比如奖励模型偏好较长的回答,模型就开始生成大量重复内容;如果奖励模型偏好带分号的句子,模型就开始在奇怪的地方加分号。

这种现象叫做"奖励黑客"(Reward Hacking),本质上是模型在利用奖励模型的判断规则,而不是真正提升回答质量。

我用了几种方法来缓解:

  1. 在奖励函数中加入惩罚项
def adjusted_reward(original_reward, generated_text, reference_text):
    base_reward = original_reward

    # 长度约束:太长或太短的回答都惩罚
    answer_length = len(generated_text.split())
    if answer_length < 20 or answer_length > 500:
        base_reward -= 0.5

    # 重复性惩罚
    ngrams = set()
    for i in range(len(generated_text.split()) - 2):
        ngram = ' '.join(generated_text.split()[i:i+3])
        if ngram in ngrams:
            base_reward -= 0.1
        ngrams.add(ngram)

    return base_reward
  1. 使用混合奖励:奖励模型的分数只占一部分,还要加上人工设计的规则。

  2. 定期人工抽检:如果发现模型在"作弊",及时调整奖励函数。

KL 散度约束的平衡

PPO 算法有一个关键参数:KL 散度约束。这个约束防止模型偏离原始模型太远,但约束太强又会限制模型的改进空间。

我尝试了不同的 KL 约束强度:

  • 太强(0.1):模型几乎不改变,RLHF 效果不明显
  • 适中(0.02-0.05):模型有明显改善,但不会过度优化
  • 太弱(0.001):模型快速偏离原始行为,出现奇怪的回答模式

最终选择的是动态调整:训练初期用较大的 KL 约束,后期逐渐放松。

实际效果与结果

量化指标

训练完成后,我用几个指标评估效果:

  • 奖励模型准确率:从 68% 提升到 82%
  • 人工评估一致性:从 75% 提升到 88%
  • 拒绝回答率:从 5% 下降到 2%(适当减少了过度拒绝)

这些数字看起来不错,但更重要的是实际使用感受。

定性改善

实际使用中,最明显的改善是:

  • 模型更愿意按照指令格式回答了(之前经常忽略格式要求)
  • 对敏感问题的回答更加一致(不会一会说"我不知道"一会又开始回答)
  • 多轮对话的连贯性更好(不会突然"忘了"之前的上下文)

但也出现了一些新问题:

  • 有时候会变得"过于顺从",用户问什么就答什么,即使问题本身有问题
  • 创造性回答变少了(模型更倾向于"安全"的回答)
  • 对一些模糊问题的理解反而变差(过度依赖明确的指令)

资源消耗

整个流程的资源消耗不小:

  • 数据准备:1-2 周(主要花在数据清洗和质量检查)
  • 奖励模型训练:2-3 天(用 4 张 A100)
  • PPO 优化:5-7 天(用 8 张 A100)

这个成本对于个人开发者来说不算小,但对于企业应用来说还是可以接受的。

踩坑总结

这次实践中遇到的问题可以归纳成几类:

数据相关

  • 标注质量直接影响最终效果,不要在这个环节省钱
  • 数据清洗很重要,但不要过度清洗,会损失多样性
  • 自己标注时要注意标注疲劳,定期交叉验证

模型相关

  • 奖励模型不需要太大,但需要足够的数据训练
  • PPO 的超参数很敏感,不同数据集可能需要不同设置
  • KL 散度约束太强会限制模型能力,太弱又容易奖励黑客

工程相关

  • 训练过程中监控很重要,要定期检查生成的样本
  • 保留多个检查点,不要只保留"最佳"的那个
  • 准备好回滚方案,有时候"更新"反而不如"不更新"

结语

RLHF 说起来是个"让模型听人话"的技术,但实践中更像是在和模型做一场持续的博弈:你教它规则,它学会利用规则,你再修改规则,它再适应新的规则。

这个过程不会完美,也永远不会有完美的版本。但正是这种持续的调整和优化,让模型越来越接近真正"理解"人类期望的状态。

对我来说,这次实践最大的收获不是掌握了某个算法,而是理解了"对齐"这个问题的复杂性:它不只是技术问题,更是关于如何让 AI 和人类价值观真正协调一致的问题。

技术会继续进步,但这个"对齐"的博弈,可能才刚刚开始。

版权声明: 本文首发于 指尖魔法屋-AI人类反馈强化学习实践笔记https://blog.thinkmoon.cn/post/395-ai-rlhf-supervised-alignment-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!