
如果你最近在折腾大语言模型的对齐训练那么Hugging Face生态下的RLHF流程一定是你绕不开的一环。我为了把这套训练流程完整跑通前后花了差不多一个月的时间从SFT到奖励模型再到PPO中间换过数据、调过参数、爆过显存也被各种版本的API坑过。这篇文章就把整套RLHF训练流程的实操经验整理出来从原理拆解到可直接复制的训练脚本尽量让你少走弯路。适合谁来读如果你已经跑过基础的大语言模型微调想进一步做对齐训练或者你正要评估用DPO还是PPO来做偏好优化这篇文章都有参考价值。我会把每一步为什么这么做讲清楚再给出一套基于Transformers、TRL、PEFT等Hugging Face库的落地实现方案。1. 先想明白RLHF到底在做什么1.1 三个关键环节SFT、RM、PPORLHF全称是Reinforcement Learning from Human Feedback中文叫基于人类反馈的强化学习。虽然名字里带着“强化学习”但完整的RLHF链路远不止强化学习这一步业内通常拆成三个阶段SFT监督微调、RM奖励模型、RL强化学习优化典型的是PPO。SFT阶段做的事情很朴素拿一批有人工标注的指令-回答对对基础模型做有监督微调让模型先学会“按指令说话”的基本范式。这一步解决的是冷启动问题。你直接用预训练模型做RLHF是不现实的因为基础模型只会续写文本根本不懂指令-回答的格式上来就做强化学习奖励信号会非常稀疏模型很难收敛。所以SFT是RLHF的第一块地基。RM阶段则是训练一个打分模型。这个模型读入“同一问题下的两个回答”然后判断哪个回答更符合人类偏好。它的输出是一个标量分数用来替代人工反馈。为什么需要这样一个模型因为强化学习需要密集的奖励信号而靠人工实时打分效率太低成本也太高。于是先用人类标注好的偏好数据训练一个代理奖励模型之后用它给策略模型的每个输出打分。PPO阶段是真正的强化学习优化。策略模型也就是经过SFT的模型每生成一个回答奖励模型就给它一个分数PPO算法根据这个分数调整策略模型的参数目标是让模型在“不偏离原始语义过多”的前提下获得更高的奖励分数。注意这里有个关键点PPO不是一味追求高分它会用KL散度约束策略模型防止模型变成奖励模型的“应声虫”。1.2 为什么必须分三步能跳过吗很多人会问我直接把SFT做完然后跳到PPO行不行答案是可以但效果通常不好。SFT虽然教会了模型指令遵循但它并不知道“什么样的回答是更好的回答”。两个回答都能流畅完成指令但一个更简洁、更诚实、更符合用户预期另一个啰嗦且带有事实性错误。SFT阶段的数据里没有这种偏好信息模型只能模仿标注者给出的单一答案。奖励模型存在的意义就是把“人类偏好”这种主观信号转化为可优化的目标函数。它让模型在不同回答之间有了比较的依据。所以RM不是可选项而是PPO能否有效工作的核心桥梁。当然如果你选择DPO方案可以跳过显式的奖励模型训练但它本质上仍然是在用人类偏好信号做优化只是把目标函数变了种形式。这个我在后面第五章会单独讲。1.3 工具选型为什么用Hugging Face生态RLHF涉及多个训练器、多个模型的协同如果全部从零手写工程量会非常大。Hugging Face生态的价值在于它把SFT、RM、PPO、DPO都封装成了统一的Trainer接口而且和PEFT参数高效微调、Accelerate、Datasets等组件天然打通。你用一套熟悉的高阶API就能把三个阶段全跑下来甚至可以在LoRA模式下完成全流程大幅降低显存门槛。选TRL库Transformer Reinforcement Learning还有个好处社区维护活跃新模型和新算法发布后适配速度通常比自研代码快很多。比如现在很多团队用Qwen系列或者Llama系列做RLHFTRL基本都能直接支持。2. 环境搭建与数据准备2.1 依赖安装与版本选择先装依赖。我建议用Python 3.10以上版本PyTorch按你的CUDA版本安装然后安装以下核心库pip install transformers datasets trl peft accelerate bitsandbytes这里有个非常重要的心得不同版本之间的API差异非常大。2024年之前SFTTrainer的用法和现在完全不同。早期版本里你直接传model、args、train_dataset现在版本更倾向于用SFTConfig统一管理训练参数代码结构更清晰。如果你参考网上老教程经常会遇到“参数不存在”或者“方法已废弃”的报错。我的建议是所有文档以官方最新API为准装完库之后先跑一遍官方示例确认版本行为。以我写这篇文章时常用的版本为例transformers4.40trl0.10。你在实践时不必强行追求最新版但最好固定版本避免训练中途突发兼容性问题。2.2 硬件门槛与显存估算硬件是很多人最关心的问题。我把常见规模模型在不同训练模式下的显存需求估算列成表格给你做个参考模型规模 | 训练模式 | 显存估算 | 推荐硬件 7B | 全量SFT | 30GB | 单张A100/A800或多张消费卡 7B | LoRA SFT | 18GB~24GB | 单张RTX 3090/4090 7B | RM训练 | 16GB~20GB | 单张RTX 3090/4090 7B | PPO | 40GB | 多卡或A100建议LoRA下跑 7B | DPO | 24GB~32GB | 单张RTX 4090可尝试 13B | LoRA SFT | 30GB | A100/A800补充一个估算逻辑模型权重占大头。一个7B参数的模型fp16精度下光权重就是约14GB训练还要额外存储梯度、优化器状态这部分通常是权重的1.5到2倍所以全量微调7B模型至少需要30GB以上显存。如果只更新LoRA参数那么模型本身冻结显存主要用来存中间激活值和少量可训练参数单张24GB的RTX 4090就能跑起来。PPO阶段会更吃力因为你需要同时加载四个模型策略模型、参考模型、奖励模型、Critic模型。这也是为什么我强烈建议PPO阶段用LoRA如果你非要全量跑PPO显存预算至少要翻倍。2.3 三种数据格式的规范不同的训练阶段需要不同结构的数据我直接用表格总结常见的字段设计阶段 | 数据字段 | 示例 SFT | prompt, completion | {prompt: 解释一下什么是TCP, completion: TCP是一种传输层协议...} RM | prompt, chosen, rejected | {prompt: TCP是什么, chosen: TCP是传输控制协议..., rejected: TCP是一个协议...} DPO | prompt, chosen, rejected | 与RM字段一致这里有个细节值得注意SFT阶段的数据不一定是prompt和completion两个字段。如果你用的是messages格式TRL也支持直接传入对话消息例如{messages: [{role: user, content: TCP是什么}, {role: assistant, content: TCP是传输控制协议...}]}这些数据最终都会通过tokenizer的chat template转换成模型输入。关键是要确保你的tokenizer配置了完整的chat template否则训练时模型根本不知道如何区分用户消息和助手消息生成的回答会变得乱七八糟。RM和DPO阶段使用的偏好数据要尤其注意质量。chosen和rejected之间的差异应该足够明显让奖励模型学到真正有价值的偏好信号。如果两个回答质量差不多标注人员选择起来都犹豫那么训练出来的奖励模型分数就是“掷骰子”后面PPO再优化也没有意义。2.4 把原始数据加工成训练集假设你已经准备了一份JSONL文件每行是一条样本。我们可以用Hugging Face的Datasets库直接加载from datasets import load_dataset sft_dataset load_dataset(json, data_filessft_data.jsonl, splittrain) rm_dataset load_dataset(json, data_filesrm_data.jsonl, splittrain) dpo_dataset load_dataset(json, data_filesdpo_data.jsonl, splittrain)加载之后可以先过滤掉超长样本避免训练时被超长序列撑爆显存sft_dataset sft_dataset.filter(lambda x: len(x[prompt]) len(x[completion]) 3000)这一步在实际项目中非常实用。我见过很多新手数据没有清洗直接开跑结果某个样本特别长模型直接OOM然后开始怀疑是代码配置的问题最后排查半天发现是数据的问题。3. 第一阶段实操SFT监督微调3.1 为什么要先做SFTSFT这一步的目的不只是“让模型学会指令格式”更重要的整理出一版行为可控的基础模型。如果SFT模型输出的质量太差后面奖励模型给出的分数会普遍偏低强化学习的信号噪声比就会很差。换句话说SFT模型决定了模型能力的天花板RLHF只是在这个天花板之下选择一个更符合人类偏好的行为方向。不要指望RLHF能带来SFT阶段没掌握的知识。另外SFT数据量不需要特别夸张。质量比数量重要得多。通常几千条覆盖各种指令类型的高质量数据就能让一个7B模型表现出明显的指令遵循能力。我见过有人放着几千万条数据全量微调结果模型反而变得呆板。不要迷信大数据量在指令微调阶段适度规模、充分多样性、严格去重才是关键。3.2 用SFTTrainer训练带LoRA的模型代码方面我建议直接使用TRL的SFTTrainer同时配合PEFT的LoRA配置。这样既能降低显存需求又能在训练结束后合并权重或者保留LoRA适配器灵活度很高。from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoTokenizer from peft import LoraConfig from trl import SFTConfig, SFTTrainer dataset load_dataset(json, data_filessft_data.jsonl, splittrain) # 加载模型和tokenizer model_name Qwen/Qwen2.5-7B tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained( model_name, torch_dtypeauto, device_mapauto, ) # LoRA配置 peft_config LoraConfig( r16, lora_alpha32, lora_dropout0.05, target_modules[q_proj, k_proj, v_proj, o_proj], task_typeCAUSAL_LM, ) # 训练参数 training_config SFTConfig( output_dir./sft_output, per_device_train_batch_size4, gradient_accumulation_steps8, learning_rate2e-5, num_train_epochs3, logging_steps10, save_steps500, max_seq_length2048, fp16True, gradient_checkpointingTrue, save_total_limit2, ) trainer SFTTrainer( modelmodel, tokenizertokenizer, train_datasetdataset, argstraining_config, peft_configpeft_config, ) trainer.train()如果你用的是最新版API可能会有个别参数名差异比如fp16在部分版本里改成了bf16或由accelerate统一管理。建议以官方文档为准。另外如果模型是基座模型不是chat模型记得通过add_special_tokens添加特殊标记或者确保tokenizer自带chat template。3.3 参数选择经验学习率这一块LoRA微调一般用1e-4到3e-4如果做全量微调建议1e-5到2e-5。我发现很多新手用全量微调的学习率去调LoRA效果会非常差。LoRA的参数量小学习率可以稍微激进一点但也不要超过5e-4否则训练Loss会飞大。训练轮数控制在2到4轮即可。SFT不是训练得越久越好模型跑太多轮会出现严重的重复生成和灾难性遗忘比如问什么都会回到训练数据里最常见的回答模式。我一般以验证集上的Loss作为指标如果再往下训练验证Loss开始上升就说明开始过拟合了需要提前停止。LoRA的rank设置也有讲究。rank太小模型表达能力不足rank太大又失去参数高效微调的优势。我常用的经验值是rank16、alpha32这个组合在7B模型上表现稳定。如果你数据量特别少可以在rank8以下尝试。4. 第二阶段实操训练奖励模型4.1 奖励模型是怎么“学习偏好”的奖励模型的训练目标本质上是一个二分类问题。输入是同一个prompt下的两个回答模型需要输出chosen比rejected好的概率。实现时模型会对每个回答给出一个分数然后通过log sigmoid损失来拉大两者分差。最终我们希望奖励模型给人类偏好的回答打高分给人类不喜欢的回答打低分。TRL里的RewardTrainer封装了完整的损失计算逻辑。它会读取数据集中的chosen和rejected字段自动计算对比损失。你不需要手动构造正负样本对但数据的预处理阶段一定要保证chosen和rejected都已经通过相同的prompt拼接好。如果你用的是对话模型记得两个回答都要经过同一个chat template。4.2 RewardTrainer实操奖励模型一般不需要很大7B或更小的模型往往就能胜任。因为它的任务相对简单就是给文本打分不需要生成能力。但有一点要注意奖励模型的tokenizer最好和策略模型保持一致否则后面对接PPO时代码里会出现很多兼容性麻烦。from transformers import (AutoModelForSequenceClassification, AutoTokenizer) from trl import RewardTrainer, RewardConfig model_path ./sft_output/merged # 或者直接使用基础模型 tokenizer AutoTokenizer.from_pretrained(model_path) model AutoModelForSequenceClassification.from_pretrained( model_path, num_labels1, ) reward_config RewardConfig( output_dir./rm_output, per_device_train_batch_size4, gradient_accumulation_steps8, learning_rate1e-5, num_train_epochs1, logging_steps10, save_steps500, max_length2048, fp16True, ) trainer RewardTrainer( modelmodel, tokenizertokenizer, train_datasetrm_dataset, argsreward_config, ) trainer.train()需要注意此处如果是用LoRA训练奖励模型同样可以叠加peft_config。我自己在训练奖励模型时倾向于使用较小的LoRA rank因为奖励模型的稳定性比表达能力更关键rank8往往就够。另外AutoModelForSequenceClassification的输出层是随机初始化的如果你打算后面合并权重再接PPO要确保手头有对应的ValueHead转换脚本而不是直接把分类头当作PPO的Value。4.3 评估奖励模型质量训练完奖励模型后一定要单独评估一下而不是直接丢给PPO。我常用的评估方法找一批验证集偏好数据统计奖励模型在这些数据上的准确率也就是chosen得分大于rejected得分的比例。如果准确率还没有达到80%以上建议先回头提升数据质量或增加训练数据否则后续强化学习很可能不稳定。还有一个经常被忽略的点需要观察奖励模型的分数分布。理想情况下分数应该是近似正态分布的而不是所有样本都打5分或都打0分。如果分数集中在一个区间说明模型没有学到区分度这时候你可以检查一下是否发生了特征退化比如模型只是通过回答长度来判断偏好。这种代理信号一旦被模型学会PPO阶段就会出现严重的奖励黑客。5. 第三阶段实操PPO与DPO两种路线5.1 PPO最正统但最复杂的强化学习路线PPOProximal Policy Optimization是目前最主流的RLHF优化算法。它在训练时需要加载策略模型Actor、参考模型Reference、奖励模型Reward和Critic模型Value。TRL库的PPOTrainer做了一件非常贴心的事它通过AutoModelForCausalLMWithValueHead包装策略模型把PPO需要的ValueHead和Actor合并到一个模型类里不用你自己维护两套权重。基本训练代码框架如下from transformers import AutoTokenizer, AutoModelForCausalLM from trl import PPOConfig, PPOTrainer, AutoModelForCausalLMWithValueHead import torch tokenizer AutoTokenizer.from_pretrained(./sft_output/merged) model AutoModelForCausalLMWithValueHead.from_pretrained( ./sft_output/merged, torch_dtypetorch.bfloat16, device_mapauto, ) # 加载奖励模型一般会用独立的reward pipeline from transformers import pipeline reward_pipeline pipeline( text-classification, model./rm_output/merged, device_mapauto, ) config PPOConfig( batch_size64, learning_rate1e-6, init_kl_coef0.2, target_kl0.1, log_withwandb, # 没有wandb可以改None ) ppo_trainer PPOTrainer( configconfig, modelmodel, tokenizertokenizer, )PPO训练不是简单的trainer.train()你需要手动编写训练循环因为每一步都要让模型生成回答喂给奖励模型打分再执行PPO优化import torch from tqdm import tqdm for epoch in range(2): for batch in tqdm(ppo_trainer.dataloader): query_tensors batch[query] # 模型生成回答 response_tensors ppo_trainer.generate( query_tensors, max_new_tokens256, pad_token_idtokenizer.eos_token_id, ) # 把生成结果转成文本调用奖励模型打分 responses [ tokenizer.decode(r[query.shape[0]:], skip_special_tokensTrue) for query, r in zip(query_tensors, response_tensors) ] rewards [] for q, r in zip(queries, responses): score reward_pipeline(q r)[0][score] rewards.append(torch.tensor(score)) # 执行PPO步骤 stats ppo_trainer.step(query_tensors, response_tensors, rewards) ppo_trainer.log_stats(stats, batch, rewards)关于step方法的细节它内部会计算策略模型输出和参考模型输出的KL散度然后结合奖励模型给出的分数计算总收益。init_kl_coef0.2的意思是允许模型有一定程度的偏离但偏离越多惩罚越大。target_kl0.1则是一个期望的KL目标用来动态调整KL系数。5.2 Actor-Critic与ValueHead的关系很多新手看到AutoModelForCausalLMWithValueHead会疑惑到底哪个部分是Actor价值头实际上这个类是在原语言模型之上加了一个线性层当作强化学习的Critic使用。语言模型的主体部分就是Actor而重新加上的这层v_head负责估计状态价值为PPO计算advantage提供基准确信。需要注意的是这个ValueHead本身就是随机初始化的所以PPO刚启动时Critic的预测会非常不准训练初期的loss抖动是正常的。如果发现Loss一直发散不下降可以先降低learning_rate甚至降到5e-7让Critic慢慢跟上。5.3 DPO不用奖励模型的轻量路线DPODirect Preference Optimization是这两年非常火的替代方案。它最大的优势在于不单独训练奖励模型也不需要复杂的强化学习循环而是直接利用偏好数据优化策略模型。它的思想是把奖励函数隐式表示为策略模型和参考模型之间的概率比。这样RLHF就可以像普通微调一样通过交叉熵损失来做。完整训练代码要简单很多from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoTokenizer from trl import DPOConfig, DPOTrainer dataset load_dataset(json, data_filesdpo_data.jsonl, splittrain) model AutoModelForCausalLM.from_pretrained(./sft_output/merged) ref_model AutoModelForCausalLM.from_pretrained(./sft_output/merged) tokenizer AutoTokenizer.from_pretrained(./sft_output/merged) config DPOConfig( beta0.1, output_dir./dpo_output, per_device_train_batch_size4, gradient_accumulation_steps8, learning_rate5e-6, num_train_epochs1, logging_steps10, save_steps500, max_length2048, fp16True, ) trainer DPOTrainer( modelmodel, ref_modelref_model, tokenizertokenizer, train_datasetdataset, argsconfig, ) trainer.train()这里需要说明的是DPO的beta参数。它控制了模型对参考模型的偏离程度beta越小模型越倾向于迎合偏好数据beta越大模型越保守。一般经验值在0.1到0.3之间。我自己在实践中发现DPO训练比PPO稳定得多不需要处理奖励黑客问题也不需要维护多个模型因此在预算有限或者追求稳定效果的场景里DPO往往是更好的选择。5.4 两条路线怎么选择把PPO和DPO放在一起对比差异很明显对比维度 | PPO | DPO 实现复杂度 | 高需要多模型加载和手动step | 低像普通监督微调 显存需求 | 高 | 低 训练稳定性 | 需要较多调参经验 | 相对稳定 数据需求 | 需要pair偏好数据奖励模型 | 需要pair偏好数据 效果上限 | 理论上更高可迭代优化 | 依赖单一目标函数 奖励黑客风险 | 存在 | 较低我的实际体会是如果你的数据量不大比如1万条以内DPO通常能带来足够好的效果提升而且实现成本低。如果你的团队做的是产品级模型且你有足够的计算资源和调参经验PPO仍然是目前效果上限更高的方案但你需要做好长期调试的准备。6. 常见问题与避坑记录6.1 显存不足怎么办显存不足几乎是每个人都会遇到的。我把几种常见情况的应对方案列出来开启gradient_checkpointingTrue用计算换显存效果非常明显。降低per_device_train_batch_size增加gradient_accumulation_steps保证等效batch size不变。使用LoRA或QLoRA把可训练参数压到1%以下。文本长度限制在训练字段里加max_seq_length实测很多数据里的超长样本是OOM元凶。PPO阶段建议将奖励模型加载到独立设备避免和策略模型抢占显存。6.2 奖励黑客问题怎么应对奖励黑客是指模型找到一种能骗过高分但实际回答质量很差的模式。比如奖励模型偏好长回答模型就会生成一堆车轱辘话凑字数。应对方法主要有三种第一在训练Reward时加入回答长度、重复度等特征作为惩罚项。第二在PPO中使用KL散度约束限制策略模型偏离参考模型太多。第三定期手动检查模型输出不能只盯奖励分数。我见过一个项目奖励分数涨得很好但生成的文本满是幻觉和重复后来发现奖励模型被“回答里包含更多标点符号”这个特征给骗了。6.3 模型输出退化怎么办如果训练结束后模型出现了明显的退化比如输出变得简短空洞或者对通用知识问题的回答开始胡编乱造优先检查两件事一是训练超参数是否过于激进。学习率太高、KL系数太小都会让模型在优化奖励的过程中丧失原有能力。二是是否做了充分的数据混合。RLHF训练时如果只用偏好数据模型很容易遗忘预训练阶段学到的通用知识。我通常会在PPO或DPO训练中混入一部分通用指令数据比例控制在10%到20%效果会明显好转。6.4 数据质量问题的信号有些规律可以帮助你快速判断是否是数据出了问题。如果训练SFT时Loss下降很快但验证集Loss反弹多半是数据重复或分布单一。如果奖励模型训练准确率卡在60%到70%上不去说明偏好数据本身存在大量噪音标注一致性差。如果DPO训练过程中chosen和rejected的log概率差始终上不来检查一下是否有大量pair的chosen比rejected长很多模型很可能在学“长度偏好”而不是“内容偏好”。6.5 新手常犯错误速查表错误类型 | 表现 | 正确做法 版本混用 | 按老教程API写代码报错 | 查看官方最新文档固定版本 SFT模型直接生成 | 回答不遵循指令格式 | 确认tokenizer已设置chat template 偏好数据未清洗 | RM训练后准确率低于80% | 检查chosen/rejected差异去重 PPO循环里未加eos | 生成长度爆炸 | 设置max_new_tokens和eos处理 参考模型权重加载错误 | KL散度异常高 | 确保ref_model与policy model初始权重一致最后分享一个我在实际项目里特别受益的技巧不要把三个阶段全部跑完再评估而是每生成一个中间节点都人工抽检一批输出。SFT完抽检100条RM完做一次排序校验PPO或DPO每训练几百步抽几条看效果。这样出了问题能快速定位到具体环节而不是等到全流程结束后对着一团糟的输出猜原因。RLHF的每一个阶段都是可以独立验证的把这个习惯保持住你会少踩掉至少一半的坑。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。