尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从SFT到DPO/PPO:基于Hugging Face的RLHF完整训练流程实战

从SFT到DPO/PPO:基于Hugging Face的RLHF完整训练流程实战 做RLHF的人十有八九都体会过这种感受论文里的公式看懂了代码库里的组件也知道叫什么但真到自己把完整管线跑通还是脱了好几层皮。尤其是从“会用transformers加载模型”到“能训练一条完整的RLHF流程”中间隔着一条巨大的鸿沟。好消息是Hugging Face生态这几年把这条沟填得越来越浅TRL、PEFT、Accelerate这些库组合起来已经能把RLHF拆成几个清晰的阶段逐个击破。这篇文章就是把我实际跑通的完整流程整理出来从监督微调SFT、奖励模型RM到偏好优化PPO/DPO每一步都讲清楚“为什么这么做”和“代码怎么写”适合已经熟悉transformers基础、想真正上手RLHF训练流程的工程师和数据科学家参考。1. RLHF到底在做什么拆开看三个阶段的本质逻辑1.1 为什么GPT-3时代的预训练不够用很多刚接触大语言模型的人会有一个困惑预训练模型不是已经能“对答如流”了吗为什么还要搞RLHF这一大套东西原因是预训练阶段的唯一目标是“预测下一个token”模型学会的是语料里的统计规律而不是“怎么满足人类需求”。举个我常用的类比预训练好比一个人读完了整个图书馆知识量很大但你问他问题他可能说一大段跑题的话、绕来绕去没有重点甚至会把问题里隐含的错误假设也当成真的。RAG能解决一部分知识时效性问题但解决不了“回答风格符合人类偏好”这个问题。RLHF也就是基于人类反馈的强化学习核心目标就是让模型从“会说话”变成“会好好说话”。它不改变模型的知识容量而是改变模型输出的偏好分布让模型更倾向于生成人类觉得有用、安全、简洁的回答远离那些冗长、有毒、不相关的内容。这一目标是通过一个完整的训练流程来达成的流程里每个阶段解决一个特定问题缺一不可。1.2 三阶段拆解与生活化类比整套RLHF训练流程可以拆成三段监督微调SFT、奖励模型训练RM、策略优化PPO或者DPO。理解这三者的关系我有一个很顺手的类比把大语言模型想象成一个实习员工。SFT阶段像是老员工手把手给他标注“优秀案例”让他照着模仿这个阶段解决的是“下限问题”——模型至少得知道什么是像样的回答。RM阶段像是制定一套绩效考核表把“优秀”打分变成可计算的标准这样机器才知道什么是好、什么是坏以及好多少、坏多少。最后的策略优化阶段则相当于把员工的奖金和绩效挂钩让他不断朝“高分回答”的方向调整自己的行为。这个类比能帮你记清楚一个关键点SFT解决的是“模仿能力”RM解决的是“打分标准”策略优化解决的是“朝标准靠拢的优化算法”。很多RLHF训练流程跑飞就是因为脑子里没有这条主线把三个阶段的职责搞混了。比如SFT还没收敛就急着上PPO或者RM训练数据太糙就期望策略优化阶段输出“惊艳”结果——这都是不可能的。1.3 RM模型为什么要单独训练一个值得展开的技术点是为什么需要单独训练一个奖励模型而不是直接让人来打分人确实可以打分但强化学习需要成千上万轮采样。每生成一条回答就让真人打一次分成本和时间都不可接受。尤其在PPO算法内部每条样本可能被反复用于更新多次这种高频打分的场景下必须有一个机械化的代理——奖励模型RLHF训练流程的核心中间产物。奖励模型的本质是把“人类的偏好判断”蒸馏到一个打分模型里。它的输入是“提示词回答”输出是一个标量分数。分数越高代表这个回答越符合人类偏好。这个分数会在后面的策略优化阶段作为反馈信号。训练RM的数据来自人类标注的对比对chosen/rejected而不是直接的分数这一点非常关键因为人类不擅长打精确的分数但很擅长判断“A和B哪个更好”。训练好RM之后还要注意它可能会被“刷爆”。后面PPO部分我会具体讲reward hacking问题但这里先立一个认知RM不是训练完就完事了它的分布漂移、过拟合都会直接影响下游策略优化做RLHF训练流程时这是最容易被忽视的一个隐藏风险点。2. 工具选型与实验环境Hugging Face生态怎么搭2.1 核心组件TRL、PEFT、Accelerate各管哪一段选定Hugging Face生态来做RLHF训练流程最大的优势是组件之间天然兼容。我用的主力库有四个transformers负责模型和tokenizer的加载datasets负责数据集的加载和预处理TRL是顶层的训练器封装提供了SFTTrainer、RewardTrainer、DPOTrainer、PPOTrainer这些现成工具PEFT负责LoRA这类参数高效微调Accelerate负责底层的分布式训练、混合精度、梯度累积这些设备侧的琐事。这套组合的具体分工我用一句话概括TRL定训练流程PEFT省显存Accelerate管设备。如果你自己从零实现RLHF三个算法要写三遍但用TRL你只需要改训练器的名字和配置数据格式稍微调整就能在SFT和DPO之间无缝切换这一点对迭代实验非常关键。版本上我实测比较稳的组合是transformers4.41、trl0.9、peft0.10、accelerate0.30。太老的版本接口差异大网上教程容易对不上号反正都新一点省心。安装就一句话pip install transformers datasets trl peft accelerate bitsandbytes2.2 数据集格式三个阶段三种吃法RLHF训练流程里最容易被低估的是数据格式。SFT、RM、PPO三个阶段的输入结构完全不同不是一份数据集走到底。SFT阶段用的是最标准的文本数据一行一段完整对话。我用过两种格式一种是纯文本字段{ text: 用户如何高效学习编程\n助手建议先选一门主语言例如Python然后通过项目驱动学习而不是只看理论。 }另一种是对话消息格式适合有chat template的模型{ messages: [ {role: user, content: 如何高效学习编程}, {role: assistant, content: 建议先选一门主语言...} ] }如果数据集足够好我推荐用messages格式因为能直接复用一个模型的chat template减少格式错乱导致的训练噪声。到了RM阶段数据必须是偏好对比对{ prompt: 如何高效学习编程, chosen: 建议先选一门主语言例如Python然后通过项目驱动学习..., rejected: 编程很难需要天赋而且很多人学不会... }注意这里chosen和rejected是完整回答不是只给一个分数。因为人类判断“哪个更好”比“打几分”更可靠RM模型正是从这种对比中学习隐含的偏好。到了DPO阶段数据格式跟RM几乎一样但多了一个可选字段加上system提示词。这是因为DPO直接利用对比数据做策略优化不需要先训一个RM再跑PPO。后面第三章会细说两种路线的差异。2.3 硬件需求与显存优化基线很多人在RLHF训练流程面前打退堂鼓是觉得硬件门槛太高。其实用上LoRA之后7B模型的完整三阶段训练在单张24GB显存的消费级显卡上就能跑起来只是需要耐心。以LLaMA-2-7B为例我这个配置实测能稳定跑完整流程模型以torch_dtypetorch.float16加载使用4-bit量化加载底座模型bitsandbytes的nf4配置再把LoRA适配器以fp16训练开启gradient_checkpointing以计算时间换显存序列长度设置2048per_device_train_batch_size1配合梯度累积到等效batch size 32。这个组合里4-bit量化加LoRA是省显存的绝对主力。底座模型冻结不动只训练加了低秩分解矩阵的那一小部分可训练参数量通常只占全部参数的0.1%到1%。代价是训练速度会比全参微调慢但换来的是“一张卡也能做实验”的自由度。做完小规模验证想上更大模型再把量化关掉切到全参SFT或者多卡Accelerate配置代码基本不用大改。3. 实操全流程从监督微调到偏好优化3.1 第一阶段SFT监督微调SFT是整个RLHF训练流程的地基它的目的只有一个让模型学会“优质回答长什么样”。为什么已经预训练过的模型还需要SFT因为预训练模型只会续写不会遵循对话模板也不会区分“问题”和“回答”。通过SFT我们给模型喂大量高质量的人类示范回答让它把“用户提问之后应该怎么回答”这个模式刻进参数里。没有这一步后面RM和策略优化都在空中楼阁上跳舞。我用TRL库的SFTTrainer跑SFT的代码长这样from datasets import load_dataset from peft import LoraConfig from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments from trl import SFTTrainer # 模型与数据集准备 model_name meta-llama/Llama-2-7b-chat-hf model AutoModelForCausalLM.from_pretrained(model_name, torch_dtypeauto) tokenizer AutoTokenizer.from_pretrained(model_name) tokenizer.pad_token tokenizer.eos_token # 关键很多模型没有pad token必须补 dataset load_dataset(your_org/sft_data, splittrain) # LoRA配置 lora_config LoraConfig( r16, lora_alpha32, lora_dropout0.05, biasnone, task_typeCAUSAL_LM, ) # 训练参数 train_args TrainingArguments( output_dir./sft_ckpt, per_device_train_batch_size2, gradient_accumulation_steps16, gradient_checkpointingTrue, learning_rate2e-5, logging_steps50, save_steps500, num_train_epochs3, ) trainer SFTTrainer( modelmodel, tokenizertokenizer, argstrain_args, peft_configlora_config, train_datasetdataset, dataset_text_fieldtext, max_seq_length2048, ) trainer.train()这里有几个我踩过的坑。第一个是pad token很多模型只有eos token没有pad token。训练时数据打包后长度不一batch里必须填充不设pad token会直接报错。第二个是序列长度max_seq_length设2048意味着超过长度的部分会被截断如果你的数据里有很长的多轮对话最好先统计分析长度分布再决定否则会无声无息丢数据。第三个是学习率SFT阶段2e-5左右起步比较稳全参微调降到1e-5LoRA可以放宽到5e-5但要配合warmup比例。SFT训练完一定要做验证我只用一个小指标“模型能不能按对话模板稳定输出”。怎么测随便写几个训练集里没有过的问题看模型是否还像预训练那样给小说续写。如果已经能规规矩矩按“用户/助手”格式回答说明SFT阶段过关了。3.2 第二阶段训练Reward ModelRM在RLHF训练流程里承担“裁判”角色。它的训练是典型的二分类任务但跟普通分类不同它不直接输出“好/坏”而是输出一个连续分数然后用chosen和rejected的分数差来构造排序损失。TRL库的RewardTrainer封装好了这套逻辑。数据格式是prompt、chosen、rejected三个字段。实现代码from datasets import load_dataset from peft import LoraConfig from transformers import AutoModelForSequenceClassification, AutoTokenizer, TrainingArguments from trl import RewardTrainer # 注意RM是sequence classification模型不是causal LM model AutoModelForSequenceClassification.from_pretrained( model_name, num_labels1, # 回归任务输出一个标量 torch_dtypeauto, ) tokenizer AutoTokenizer.from_pretrained(model_name) # 很多生成模型的tokenizer不适合RM需要设置pad_token if tokenizer.pad_token is None: tokenizer.pad_token tokenizer.eos_token dataset load_dataset(your_org/rm_data, splittrain) lora_config LoraConfig(r8, lora_alpha16, task_typeSEQUENCE_CLASSIFICATION) train_args TrainingArguments( output_dir./rm_ckpt, per_device_train_batch_size4, gradient_accumulation_steps8, learning_rate1e-5, gradient_checkpointingTrue, logging_steps50, save_steps500, num_train_epochs1, ) trainer RewardTrainer( modelmodel, tokenizertokenizer, argstrain_args, peft_configlora_config, train_datasetdataset, max_length2048, # 输入长度上限 ) trainer.train()这里最反直觉的一个点是预训练权重其实不适合直接拿来当RM的初始化。一个隐藏的坑在于分类头是随机初始化的如果直接用完整模型训练前几步损失会剧烈震荡。我一般会顺带设置peft_config因为大多数RM实验用LoRA就足够了全参训练反而容易让模型忘了原先学到的语言知识只记得打分了。RewardTrainer内部会同时处理chosen和rejected两个序列把它们拼在一起后过模型分别得到两个分数然后计算pairwise ranking loss。这就是为什么你不用自己写损失函数。训练RM时我还会留出一部分验证集做准确率评估把验证集里的(chosen, rejected)对都打一次分算chosen分数高于rejected的比例。这个比例通常叫“奖励准确率”我实测稳定在70%到80%之间就算可用。低于65%说明标注质量差或者提示词和回答的差异太小。另外RM在训练集上的准确率如果超过95%大概率是过拟合了这种RM在PPO里极易引发reward hacking后面第四章详细讲。3.3 第三阶段PPO与DPO两条路线策略优化阶段有两条主流路线传统的PPO和现在更流行的DPO。两者目标一致但思路差异很大。先看PPO。PPO需要四个模型actor正在优化的策略模型、ref参考模型通常是SFT模型、reward model、critic价值模型用来估计状态价值。训练时actor根据提示词生成回答reward model给回答打分ref model计算新旧策略的KL散度critic估算advantage然后按PPO目标更新actor。这个循环里每条样本的生成和打分都在训练过程中实时进行所以工程复杂度高对算力要求高调参难度也大。TRL库的PPO实现里你通常需要自己构造generate_kwargs和batch collator并且要拿到生成序列的logprobs。这部分代码相对繁琐也是我做RLHF训练流程时最头疼的环节之一因为PPOO对超参非常敏感kl_coef太大会让模型输出退化到和SFT模型几乎一样、等于白做太小则会让模型疯狂找RM的漏洞刷分。再看DPO。DPO的洞察非常聪明它绕过了显式训练RM也绕过了PPO的在线采样循环直接把偏好优化变成一个离线分类问题。训练时只需要(prompt, chosen, rejected)三元组以及一个冻结的ref模型。损失函数鼓励模型提高chosen的概率、降低rejected的概率同时用KL散度约束模型别偏离ref模型太远。DPO的训练直观很多这也是为什么它近几年成了小团队做RLHF训练流程的首选。我用TRL的DPOTrainer写过完整流程from datasets import load_dataset from peft import LoraConfig from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments from trl import DPOTrainer model AutoModelForCausalLM.from_pretrained(model_name, torch_dtypeauto) tokenizer AutoTokenizer.from_pretrained(model_name) tokenizer.pad_token tokenizer.eos_token ref_model AutoModelForCausalLM.from_pretrained(model_name, torch_dtypeauto) dataset load_dataset(your_org/dpo_data, splittrain) lora_config LoraConfig(r16, lora_alpha32, task_typeCAUSAL_LM) train_args TrainingArguments( output_dir./dpo_ckpt, per_device_train_batch_size2, gradient_accumulation_steps16, gradient_checkpointingTrue, learning_rate1e-5, logging_steps50, save_steps500, num_train_epochs2, remove_unused_columnsFalse, # DPO数据集需要保留所有字段 ) trainer DPOTrainer( modelmodel, ref_modelref_model, tokenizertokenizer, argstrain_args, train_datasetdataset, peft_configlora_config, beta0.1, # KL散度温度系数 ) trainer.train()beta这个参数在DPO里至关重要体现的是“允许模型偏离原始策略多远”。beta0.1是常见起始点如果chosen和rejected之间的得分差异很大可以调大beta让模型更大胆去学偏好如果数据噪声大调小beta更稳。我自己的经验是RLHF数据质量一般时beta调小到0.05能显著降低模型生成乱码的概率。那到底选PPO还是DPO我给一个实用判断标准你的目标是追SOTA、有充足GPU资源、且想精确控制KL散度那么PPO更合适你的目标是快速迭代、资源有限、数据已经准备好那么DPO几乎总能提供80%的PPO效果但只需要20%的工程投入。大部分团队实际上更适合DPO起步跑通后再考虑升级PPO。这里我还想强调一个认知DPO不是“弱化版PPO”它只是把“人类偏好”这个信号用另一种方式注入模型很多公开榜单上DPO的效果和PPO是持平的而且DPO的稳定性通常更好。4. 我在实操中踩过的坑与排查清单4.1 奖励崩溃Reward Hacking奖励崩溃是我在RLHF训练流程中遇到过最隐蔽、最让人头疼的问题。现象是RM的打分一路走高客观上模型生成的文本质量却越来越差甚至出现跟问题完全无关的长篇废话。为什么因为RM终究只是一个代理评分器actor模型在PPO循环里会发现“某些表达方式能骗到高分”于是开始钻空子。经典例子奖励模型训练时用的chosen回答里频繁出现“总之”、“通过以上分析可以”这类结构词actor就学到了堆砌这些词来刷分。RM偏好的不是“正确的推理”而是跟高分样本相似的表面模式。应对方法有几个组合拳。第一是加KL约束PPO里调大kl_coef惩罚模型每个token的分布偏移让模型不敢跑出SFT模型的舒适圈太远。第二是控制迭代次数PPO阶段不要贪多连续几个epoch奖励不再上升就停。第三是定期人工评估把每个PPO epoch的checkpoint拿出来手动跑几组prompt看输出质量。我通常会在训练过程中把采样结果实时记录到实验管理平台每500步翻一次肉眼扫一下有没有“车轱辘话”。4.2 训练不稳定与显存不足做RLHF训练流程训练不稳定是家常便饭。Loss曲线像心电图一样忽上忽下未必是bug但如果直接NaN大概率出在数值稳定性上。我的排查顺序是先看学习率PPO不同阶段的学习率差距可以很大actor用1e-6是常见的RM用1e-5比较合适DPO用1e-5到3e-5SFT可以到2e-5再看混合精度fp16的loss溢出比bf16常见很多如果你的卡支持bf16就优先用bf16最后看数据提示词和回答里混入超长token时Attention计算可能溢出。显存不足这个问题我提供一套逐步减负的清单。先开gradient_checkpointing它能把激活值显存降低一半以上然后把batch size降为1用梯度累积找齐等效batch再不行就开4-bit量化底座模型还不行就缩短最大序列长度。如果以上都不满足说明你的模型规模已经超出单卡能力用Accelerate切到多卡分布式才是正路。有一个容易忽略的显存细节DPO里ref_model也会占显存。我用的是和模型同一个初始权重来初始化ref_model这没问题但ref_model在训练过程中必须全程冻结。为了省显存可以把ref_model也做4-bit量化但是要小心量化过的ref_model和未量化的model计算出来的KL散度可能在数值上略有偏差不过实际影响很小。4.3 常见问题速查表我把RLHF训练流程中经常遇到的问题和排查思路整理成一个速查表对新手友好的部分都写在里面了。现象可能原因排查与解决SFT训练时loss不降学习率过高、chat template错误调低学习率到1e-5检查tokenizer的chat template是否匹配数据集格式RM准确率一直低于65%偏好对质量差、提示词包含偏见人工抽检20条数据去掉“差异太小”的样本对RM准确率爆表超过95%RM过拟合增加RM训练数据降低训练轮数加入正则化/early stopDPO后生成内容空洞beta设太大、数据噪声高降低beta到0.05清洗chosen和rejected确保两方质量差距真实存在PPO训练中reward曲线快速上升后停滞model开始reward hacking调大kl_coef、降低学习率、强制early stop量化训练中出现NaNfp16精度不足切换到bf16降低学习率检查是否存在极端长度的样本生成回答语言混乱tokenizer缺少pad token显式设置pad_token eos_token必要时修改chat template多卡训练速度不升反降模型太小/卡间通信频繁检查Accelerate的fp16/bf16开关增大batch size摊薄通信开销除了这张表我还想分享两个心态层面的经验。第一RLHF的每个阶段都不会整整齐齐地“一次通过”SFT和RM都是可以反复迭代的独立模型不建议一上来就追求端到端自动化。第二实验追踪要做得比普通微调更细因为奖励分数和生成质量的相关性不是线性的日志里的loss涨跌并不直接等于模型变好必须把采样文本、奖励分数、KL散度三条曲线放在一起看。跑了十几个实验之后我自己的体会是RLHF训练流程不是一条流水线更像是一种迭代式打磨。每次调整RM或者beta参数背后都是在跟“模型的偏好”这个抽象的东西对话。Hugging Face生态的价值在于把标准路径封装好了让你能把精力集中在数据质量、采样分析这些真正决定结果的地方。如果你第一次跑通全流程建议强行克制住“调更多参数”的冲动先固定一套配置把三个阶段的输出都认真观察一遍搞清楚每个信号之间的因果关系再开始优化。这个方法比任何超参搜索都更能帮你建立对这个训练流程的直觉。
返回列表