AI人类反馈强化学习实践笔记
在这次实践之前,我的模型训练经验主要停留在监督学习阶段:准备数据、训练、评估。
为什么需要 RLHF
在这次实践之前,我的模型训练经验主要停留在监督学习阶段:准备数据、训练、评估。看起来很完美,但实际用起来总有问题:
- 模型会按照训练数据的模式回答,但不考虑真实意图
- 对同一个问题多次询问,回答可能完全不同
- 明明知道不能说某些内容,换个问法就"忘了"
这些问题的根源在于:监督学习只教会模型"生成合理的文本",但没教会它"生成符合人类期望的文本"。
RLHF 的核心思路很简单:用人类标注的数据训练一个奖励模型,再用这个奖励模型指导原始模型生成更好的回答。说起来容易,做起来全是坑。
RLHF 的基本流程
先放一张流程图,把整体说清楚:
这个流程看起来很标准,但每个环节都有实践上的陷阱。
数据准备阶段
收集人类偏好数据
第一步是收集人类对模型回答的偏好数据。理论上需要准备大量成对的回答,让人工标注哪个更好。
我这里用的是开源的偏好数据集,包括:
- 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),本质上是模型在利用奖励模型的判断规则,而不是真正提升回答质量。
我用了几种方法来缓解:
- 在奖励函数中加入惩罚项
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
使用混合奖励:奖励模型的分数只占一部分,还要加上人工设计的规则。
定期人工抽检:如果发现模型在"作弊",及时调整奖励函数。
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/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。