序言
问题缘起来自发现PPOTrainer
里并没有跟SFTTrainer
类似的peft_config
参数,而SFTTrainer
在带和不带peft_config
参数的情况下分别对应高效微调和全量微调。自然就会想到是否可以把PPO和PEFT结合,但是目前peft包和trl包上似乎还是存在这种兼容性的问题。
另一个问题就是奖励函数的设置,这个是RL从诞生以来一直存在的一个老大难问题。现在有很多方案,但是我始终觉得奖励模型应该与reference model一起训练是最好的,这就有点像GAN,肯定是可以实现的,无非是模块设计与效果的问题。
以下摘自与deepseek-r1的对话记录,是很有帮助的。
文章目录
1 典型的PPOTrainer与DPOTrainer参数
1.1 PPOTrainer
PPOTrainer是最常用的强化学习训练器,用于PPO算法训练。
关键参数:
model
: 要训练的模型(通常是AutoModelForCausalLM
或AutoModelForSeq2SeqLM
)config
: PPOTrainer配置对象(PPOConfig)tokenizer
: 用于文本处理的tokenizeroptimizer
: 自定义优化器(可选)lr_scheduler
: 学习率调度器(可选)
PPOConfig重要参数:
PPOConfig(
model_name="gpt2", # 模型名称
steps=20000, # 训练总步数
batch_size=256, # 每步的batch大小
mini_batch_size=8, # PPO更新时的小batch大小
ppo_epochs=4, # 每个样本的PPO迭代次数
learning_rate=1.41e-5, # 学习率
adap_kl_ctrl=True, # 是否使用自适应KL控制
init_kl_coef=0.2, # 初始KL系数
target=6.0, # KL散度目标值
horizon=10000, # KL散度计算的时间范围
gamma=1.0, # 折扣因子
lam=0.95, # GAE的lambda参数
cliprange=0.2, # PPO的剪切范围
cliprange_value=0.2, # 值函数的剪切范围
vf_coef=0.1, # 值函数损失系数
seed=0, # 随机种子
optimizer_kwargs={...}, # 优化器额外参数
)
1.2 SFTTrainer
用于监督式微调的Trainer,继承自HuggingFace的Trainer。
关键参数:
model
: 要微调的模型args
: TrainingArguments对象train_dataset
: 训练数据集eval_dataset
: 评估数据集(可选)tokenizer
: 用于文本处理的tokenizerdata_collator
: 数据整理器(可选)compute_metrics
: 计算指标的函数(可选)peft_config
: PEFT配置(用于参数高效微调)
1.3 RewardTrainer
用于训练奖励模型的Trainer。
关键参数:
model
: 奖励模型args
: TrainingArguments对象train_dataset
: 训练数据集eval_dataset
: 评估数据集(可选)tokenizer
: 用于文本处理的tokenizerdata_collator
: 数据整理器(可选)compute_metrics
: 计算指标的函数(可选)
1.4 DPOTrainer (Direct Preference Optimization)
用于直接偏好优化训练的Trainer。
关键参数:
DPOTrainer(
model, # 要训练的模型
ref_model=None, # 参考模型(如无则使用model)
beta=0.1, # DPO的温度参数
loss_type="sigmoid", # 损失类型(sigmoid或hinge)
args=None, # TrainingArguments
train_dataset=None, # 训练数据集
eval_dataset=None, # 评估数据集
tokenizer=None, # Tokenizer
data_collator=None, # 数据整理器
label_pad_token_id=-100, # 标签填充token ID
padding_value=0, # 填充值
truncation_mode="keep_end", # 截断模式
generate_during_eval=False, # 评估时是否生成
callbacks=None, # 回调函数
optimizers=(None, None), # 优化器和学习率调度器
)
1.5 通用训练参数
大多数Trainer都支持的通用参数:
output_dir
: 输出目录overwrite_output_dir
: 是否覆盖输出目录do_train
/do_eval
: 是否进行训练/评估evaluation_strategy
: 评估策略(“steps”, “epoch”, “no”)per_device_train_batch_size
: 每个设备的训练batch大小per_device_eval_batch_size
: 每个设备的评估batch大小gradient_accumulation_steps
: 梯度累积步数num_train_epochs
: 训练轮数max_steps
: 最大训练步数logging_dir
: 日志目录logging_strategy
: 日志记录策略save_strategy
: 模型保存策略fp16
/bf16
: 是否使用混合精度训练
1.6 XPO算法概述
这边偶然发现还有一个XPOTrainer
,不过看起来并不是很有用。
XPOTrainer 是 TRL 库中较新引入的一个 Trainer,它实现了 XPO (eXploration-Policy Optimization) 算法。这是一种新型的强化学习算法,专门为语言模型微调设计,旨在解决传统 PPO 在语言任务中的一些局限性。
XPO 算法的核心创新点在于将策略优化分解为两个部分:
探索阶段 (Exploration Phase)
- 使用一个"探索策略"生成多样化的响应
- 这个策略鼓励探索不同于当前策略的行为
- 通过KL散度控制探索程度
策略优化阶段 (Policy Optimization Phase)
- 基于探索阶段收集的数据优化主策略
- 使用类似PPO的优化目标但有所改进
- 更好地利用探索阶段收集的信息
XPO 相比 PPO 的优势:
更好的探索能力:
- 显式分离探索和利用阶段
- 避免PPO容易陷入局部最优的问题
更稳定的训练:
- 减少了策略更新的剧烈波动
- 通过探索策略缓冲了主策略的直接变化
更适合语言任务:
- 专门针对文本生成任务设计
- 更好地处理离散动作空间(文本token)
XPOTrainer 的参数与 PPOTrainer 类似但有一些特有参数:
XPOTrainer(
model, # 主策略模型
exploration_model=None, # 探索策略模型(默认使用主模型)
config=XPOConfig( # XPO特有配置
exploration_kl_coef=0.1, # 探索阶段的KL系数
exploration_beta=1.0, # 探索强度参数
dual_learning_rate=1e-5, # 探索策略的学习率
sample_from="exploration", # 采样来源("exploration"或"policy")
exploration_steps=1, # 每次策略更新前的探索步数
# 继承自PPOConfig的其他参数...
),
tokenizer=None,
dataset=None,
optimizer=None,
lr_scheduler=None,
)
XPOConfig 特有参数详解
exploration_kl_coef
(默认0.1):- 控制探索策略与主策略差异程度的KL系数
- 值越大探索行为越不同于主策略
exploration_beta
(默认1.0):- 探索阶段的温度参数
- 影响探索策略的随机性程度
dual_learning_rate
(默认1e-5):- 探索策略的独立学习率
- 可以与主策略学习率不同
sample_from
(默认"exploration"):- 决定从哪个策略采样生成响应
- “exploration”: 从探索策略采样(默认)
- “policy”: 直接从主策略采样
exploration_steps
(默认1):- 每次策略更新前执行的探索步数
- 影响探索与利用的比例
考虑使用 XPOTrainer 当:
- 任务需要大量探索(如开放域对话)
- PPO训练出现策略崩溃或模式坍塌
- 希望生成更多样化的响应
- 在复杂奖励函数下训练稳定性差
实践建议
初始设置:
config = XPOConfig( exploration_kl_coef=0.1, exploration_beta=1.0, dual_learning_rate=1e-5, learning_rate=1e-6, # 主策略学习率通常更小 batch_size=64, exploration_steps=2 )
监控指标:
- 探索KL散度(exploration_kl)
- 主策略KL散度(policy_kl)
- 探索与主策略的奖励差异
调优方向:
- 如果响应太保守: 增加exploration_beta
- 如果响应太随机: 减小exploration_kl_coef
- 如果训练不稳定: 减小dual_learning_rate
XPO是一种较新的算法,目前在实际应用中的最佳实践仍在探索中,建议从小规模实验开始逐步调整参数。
1.7 使用建议
对于RLHF流程,通常组合使用:
- SFTTrainer进行监督微调
- RewardTrainer训练奖励模型
- PPOTrainer进行PPO强化学习
参数调优建议:
- 从小学习率开始(如1e-5到1e-6)
- 调整batch size以适应显存
- 监控KL散度避免模型偏离过大
- 使用梯度累积来模拟更大的batch size
对于DPO训练:
- beta参数是关键,通常在0.1-0.5之间
- 确保参考模型与训练模型架构一致
2 PPO + LoRA的可行方案探讨
2.1 TRL兼容局限性
PPOTrainer
里面似乎并没有peft_config
这个选项,我思考了一下,本质上PPO也是在初始的reference模型上不断更新迭代,其实说实话也是可以用lora之类的方法实现的,也就是只在初始模型基础上更新lora块,似乎也是可行的,但PPOTrainer
里面并不支持这么做。
确实,从原理上讲,PPO训练完全可以与PEFT(如LoRA)结合,只更新适配器参数而非整个模型,但当前TRL库的PPOTrainer
确实没有直接内置对peft_config
的支持。不过,我们仍然可以通过一些方法实现这一目标。以下是详细分析和解决方案:
- 理论可行性依据:
- PPO算法的核心是在参考模型(reference model)附近进行策略优化,而LoRA等PEFT方法本质也是通过小量参数调整实现模型行为变化。
- 两者都依赖KL散度约束防止策略偏离过大,兼容性良好。
- 资源优势:
- 传统PPO需存储两份完整模型(当前策略+参考模型),而PEFT只需保存适配器参数,显存占用大幅降低。
- 适合在消费级硬件上微调大模型(如7B+)。
当前TRL的限制
PPOTrainer
的设计:- 默认假设用户需要更新完整模型权重。
- 参考模型(
ref_model
)通常直接复制自主模型,未考虑PEFT架构。
- 直接传递PEFT模型会报错:
model = AutoModelForCausalLM.from_pretrained(...) model = get_peft_model(model, LoraConfig(...)) trainer = PPOTrainer(model=model, ...) # 可能引发兼容性问题
这个目前测试下来确实是会报错的,但可能版本会有差异,下面手动调控确实是一种可行的方案,即手动调整get_peft_model
得到的模型中可训练的参数,即手动冻结掉非LoRA块的部分。
2.2 手动实现PPO+PEFT
步骤1:创建PEFT模型
from peft import LoraConfig, get_peft_model
from transformers import AutoModelForCausalLM
# 加载基础模型
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf")
# 添加LoRA适配器
peft_config = LoraConfig(
r=8,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.1,
bias="none"
)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters() # 确认仅训练适配层
步骤2:处理参考模型
# 方案A:直接复制PEFT模型(推荐)
ref_model = get_peft_model(
AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf"),
peft_config
)
ref_model.load_state_dict(model.state_dict()) # 同步初始权重
# 方案B:禁用参考模型梯度(节省显存)
for param in ref_model.parameters():
param.requires_grad = False
步骤3:自定义PPOTrainer
需要重写部分逻辑以适配PEFT:
from trl import PPOTrainer
class LoRAPPOTrainer(PPOTrainer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 确保优化器仅更新可训练参数
trainable_params = [p for p in self.model.parameters() if p.requires_grad]
self.optimizer = torch.optim.Adam(trainable_params, lr=1e-5)
步骤4:启动训练
trainer = LoRAPPOTrainer(
model=model,
ref_model=ref_model,
tokenizer=tokenizer,
config=ppo_config,
...
)
trainer.train()
当然没这么简单,PPOTrainer
的训练一般是需要手写循环的,不是直接一个trainer.train()
就可以解决的
关键注意事项
KL散度计算:
- PPO依赖原始模型和参考模型的logprob差异计算KL散度。
- PEFT模式下需确保token的logits计算正确流经LoRA层。
梯度更新:
- 检查
optimizer.param_groups
确认仅适配层参数被更新。
- 检查
保存与加载:
- 保存时需同时保存基础模型和适配器:
model.save_pretrained("output_dir", save_adapter=True)
- 保存时需同时保存基础模型和适配器:
性能监控:
- 比较完整PPO和PPO+LoRA的奖励曲线、KL散度变化。
使用bitsandbytes
量化
如果显存仍然不足,可结合8位量化:
from transformers import BitsAndBytesConfig
quant_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4"
)
model = AutoModelForCausalLM.from_pretrained(..., quantization_config=quant_config)
3 关于SFTConfig
这个东西似乎并不常用,因为通常都是直接用transformers.TrainingArguments
来配置,SFTConfig似乎用的很少,实则不然,这个原因是版本迭代导致的。
- 历史原因:早期TRL版本强制使用
TrainingArguments
,SFTConfig
是后来新增的。 - 教程惯性:大多数教程延续了旧版写法。
- 灵活性:部分开发者喜欢混用两种配置方式。
建议查看TRL官方文档的最新示例(如Llama-2微调脚本),现在已逐步转向SFTConfig
优先。
3.1 SFTConfig
vs TrainingArguments
的核心区别
特性 | SFTConfig (TRL专属) |
TrainingArguments (Transformers通用) |
---|---|---|
设计目标 | 针对SFT任务的特殊配置 | 通用训练参数 |
参数范围 | 包含数据格式、打包策略等SFT特有参数 | 基础训练超参数(LR、batch size等) |
依赖库 | 专为TRL的SFTTrainer 设计 |
适用于所有HuggingFace Trainer |
PEFT集成 | 直接支持peft_config 参数 |
需额外单独传递PEFT配置 |
3.2 SFTConfig
的特有参数
这些是TrainingArguments
中没有的SFT专属配置:
from trl import SFTConfig
sft_config = SFTConfig(
# 数据相关
dataset_text_field="text", # 指定数据集中的文本字段
max_seq_length=512, # 文本最大长度(非必须,可用packing替代)
packing=True, # 是否启用文本打包(高效填充)
# 文本处理
neftune_noise_alpha=0.1, # NEFTune噪声注入强度(防过拟合)
# 格式控制
formatting_func=None, # 自定义数据格式化函数
# PEFT集成
peft_config=peft_config, # 直接传递PEFT配置
# 继承自TrainingArguments的基础参数
output_dir="./results",
per_device_train_batch_size=8,
learning_rate=2e-5,
...
)
3.3 典型使用场景
场景1:需要高级SFT功能时
当需要使用以下特性时,优先用SFTConfig
:
- 文本打包(Packing):动态填充多个样本到一个batch
- NEFTune噪声注入:提高模型鲁棒性
- 自定义数据格式:通过
formatting_func
灵活处理数据
from trl import SFTTrainer, SFTConfig
sft_config = SFTConfig(
packing=True,
dataset_text_field="text",
neftune_noise_alpha=0.1,
per_device_train_batch_size=4,
learning_rate=1e-5,
output_dir="./sft_results"
)
trainer = SFTTrainer(
model=model,
args=sft_config, # 直接使用SFTConfig
train_dataset=dataset,
tokenizer=tokenizer
)
场景2:与现有HuggingFace生态集成
当需要与其他HF工具链(如accelerate
)深度集成时,可以用TrainingArguments
:
from transformers import TrainingArguments
from trl import SFTTrainer
training_args = TrainingArguments(
output_dir="./results",
per_device_train_batch_size=4,
learning_rate=1e-5,
)
trainer = SFTTrainer(
model=model,
args=training_args, # 使用标准TrainingArguments
train_dataset=dataset,
tokenizer=tokenizer,
dataset_text_field="text", # SFT特有参数需单独传递
packing=True # 而非通过SFTConfig
)
3.4 SFTConfig的关键功能
文本打包(Packing)
- 原理:将多个短样本拼接成一个固定长度序列(如
seq_len=2048
),避免填充浪费。 - 优势:提升GPU利用率(尤其对长上下文模型)
- 启用方式:
SFTConfig( packing=True, max_seq_length=2048 # 建议设为模型最大长度 )
NEFTune噪声注入
- 作用:在embedding层添加噪声,防止过拟合。
- 参数调节:
neftune_noise_alpha=0
:禁用(默认)0.1-0.3
:推荐范围
自定义数据格式化
当数据集需要预处理时:
def format_func(example):
return f"Instruction: {example['instruction']}\nOutput: {example['output']}"
SFTConfig(
formatting_func=format_func # 覆盖dataset_text_field
)
3.5 实践建议
新项目优先用
SFTConfig
:from trl import SFTConfig, SFTTrainer config = SFTConfig( packing=True, peft_config=lora_config, learning_rate=2e-5, ... ) trainer = SFTTrainer(..., args=config)
迁移现有项目时:
- 保留原来的
TrainingArguments
- 将SFT特有参数通过
SFTTrainer
直接传递:trainer = SFTTrainer( ..., args=existing_training_args, # 原有TrainingArguments packing=True, # 新增SFT参数 dataset_text_field="text" )
- 保留原来的
参数优先级:
- 通过
SFTTrainer
直接传递的参数(如packing
)会覆盖SFTConfig
中的设置。
- 通过
4 PPOTrainer与DPOTrainer示例分析
一个重要的区别PPO的奖励模型是需要事先给定好的,DPO天然的不需要奖励函数,这也是很神奇的地方,DPO的损失推导得到一个非常简洁的结果,尽管它也是从带KL惩罚项的那个原始RL目标函数公式推过来的,确实是很神奇。
下面两个示例使用的基础模型为 deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B
,训练数据就以 YeungNLP/firefly-train-1.1M
为例,这个数据集是一个典型的问答数据集,直接load_dataset即可,问题和答案的字段分别是input
和target
4.1 PPOTrainer示例
这个示例包含了数据处理、奖励模型构建和PPO训练全流程。
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForCausalLM
from trl import PPOTrainer, PPOConfig
import torch
import numpy as np
# 1. 加载模型和tokenizer
model_name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B"
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token # 设置pad token
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16,
device_map="auto"
)
model.gradient_checkpointing_enable() # 减少显存占用
# 2. 加载并预处理数据
dataset = load_dataset("YeungNLP/firefly-train-1.1M", split="train[:5000]") # 取前5000条作为示例
def format_prompt(example):
"""将input字段格式化为prompt"""
return f"### 问题:\n{example['input']}\n\n### 回答:"
prompts = [format_prompt(ex) for ex in dataset]
# 3. 初始化PPOTrainer
ppo_config = PPOConfig(
batch_size=32, # 每次rollout的prompt数量
mini_batch_size=8, # PPO更新时的batch大小
learning_rate=1.5e-5, # 学习率
gradient_accumulation_steps=4, # 梯度累积
log_with="wandb", # 可选: 使用wandb记录日志
project_kwargs={"project": "ppo-firefly-demo"},
)
trainer = PPOTrainer(
model=model,
config=ppo_config,
tokenizer=tokenizer,
)
# 4. 定义奖励函数 (简化版)
def calculate_rewards(texts):
"""自定义奖励逻辑:
这里简化实现为:
- 回答长度奖励 (鼓励详细回答)
- 关键词奖励 (鼓励包含特定关键词)
实际应用时应替换为真正的奖励模型或人工标注
"""
rewards = []
for text in texts:
# 基础奖励
reward = 0.1
# 长度奖励 (10-100字之间最佳)
answer_length = len(text.split())
if 10 <= answer_length <= 100:
reward += 0.3 * min(answer_length/100, 1)
# 关键词奖励
keywords = ["步骤", "原因", "例如", "首先"]
if any(kw in text for kw in keywords):
reward += 0.2
rewards.append(reward)
return torch.tensor(rewards, dtype=torch.float32)
# 5. 训练循环
for epoch in range(3): # 训练3个epoch
for batch_start in range(0, len(prompts), ppo_config.batch_size):
batch_prompts = prompts[batch_start:batch_start+ppo_config.batch_size]
# 生成响应
generation_output = trainer.generate(
batch_prompts,
max_new_tokens=128,
do_sample=True,
temperature=0.7,
top_p=0.9,
)
# 解码文本
generated_texts = [tokenizer.decode(output, skip_special_tokens=True)
for output in generation_output]
# 计算奖励
rewards = calculate_rewards(generated_texts)
# PPO更新步骤
trainer.step(batch_prompts, generation_output, rewards)
# 打印进度
print(f"Epoch {epoch+1} | Batch {batch_start//ppo_config.batch_size+1}")
print(f"Avg reward: {rewards.mean().item():.2f}")
print("Sample input:", batch_prompts[0][:100] + "...")
print("Sample output:", generated_texts[0][len(batch_prompts[0]):100] + "...\n")
# 6. 保存模型
model.save_pretrained("ppo_firefly_output")
tokenizer.save_pretrained("ppo_firefly_output")
关键点说明:
数据处理:
- 使用
input
字段作为问题,添加了中文提示模板 - 从1.1M数据中取5000条作为演示
- 使用
奖励函数:
- 实现了简单的启发式奖励(长度+关键词)
- 实际应用时应替换为:
- 训练好的奖励模型
- 人工标注API
- 更复杂的规则系统
生成参数:
temperature=0.7
:平衡生成多样性和质量top_p=0.9
:核采样(nucleus sampling)提高质量
训练优化:
- 使用梯度检查点(gradient checkpointing)节省显存
- 混合精度训练(自动根据硬件选择bf16/fp16)
进阶改进建议:
- 使用真实奖励模型:
reward_model = AutoModelForSequenceClassification.from_pretrained("some-reward-model")
def calculate_rewards(texts):
inputs = tokenizer(texts, padding=True, return_tensors="pt").to(model.device)
return reward_model(**inputs).logits[:, 0]
- 添加KL散度约束:
ppo_config = PPOConfig(
init_kl_coef=0.2,
target=6.0,
adap_kl_ctrl=True,
...
)
- 使用LoRA高效微调(待定):
from peft import LoraConfig, get_peft_model
peft_config = LoraConfig(
r=8,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.1,
)
model = get_peft_model(model, peft_config)
4.2 关于PPOTrainer的训练数据参数问题
容易发现,SFTTrainer
和DPOTrainer
等常见Trainer里都有训练数据的参数项,只有PPOTrainer
是没有的。
你提出了一个非常关键的问题!确实,PPOTrainer
的数据处理方式与常规的 Trainer
类有很大不同,它的数据流设计更加动态和交互式,这也是强化学习(RL)与监督学习(SFT)的核心区别之一。下面我会详细解释 PPOTrainer
的数据处理机制及参数配置。
1. PPOTrainer
数据流的特殊性
与 SFTTrainer
的静态数据集不同,PPOTrainer
的数据处理分为两个阶段:
- Rollout Phase( rollout 阶段):模型根据当前策略生成响应(即创建训练数据)
- Learning Phase(学习阶段):使用生成的响应和奖励进行策略优化
因此,PPOTrainer
不需要传统意义上的 train_dataset
,而是通过以下方式动态获取数据:
2. 关键数据相关参数
虽然在 PPOTrainer
的初始化参数中没有直接的数据集参数,但以下参数与数据生成和处理密切相关:
① 数据生成控制(PPOConfig
中)
from trl import PPOConfig
ppo_config = PPOConfig(
batch_size=256, # 每次rollout生成的样本数
mini_batch_size=32, # 每次PPO更新的小批量大小
rollout_accumulation_steps=1, # 累积多少步rollout才开始学习
seed=42, # 随机种子(影响生成多样性)
)
② 数据输入方式
实际训练时需要 手动传递 prompts 给 PPOTrainer.generate()
:
# 示例训练循环
for epoch in range(epochs):
# 1. 生成阶段:用当前模型生成响应
prompts = [...] # 你的输入prompt列表(核心数据源!)
generation_output = trainer.generate(
prompts,
max_length=128,
do_sample=True,
temperature=0.7
)
# 2. 计算奖励(需自定义奖励函数)
rewards = [reward_function(text) for text in generation_output]
# 3. 学习阶段
trainer.step(rewards, generation_output)
3. 数据准备的三种典型模式
模式1:固定Prompt池
# 预定义一组prompts(适用于静态任务)
fixed_prompts = [
"Explain the theory of relativity in simple terms:",
"Write a Python function to calculate factorial:",
...
]
# 训练时随机采样
prompts = np.random.choice(fixed_prompts, size=batch_size)
模式2:动态生成Prompt
# 适用于开放域任务(如对话)
def generate_prompts(batch_size):
return [input("Enter a prompt: ") for _ in range(batch_size)] # 或从API获取
模式3:从数据集加载
from datasets import load_dataset
dataset = load_dataset("anthropic/hh-rlhf")
prompts = [ex["prompt"] for ex in dataset["train"]]
4. 数据预处理技巧
① Prompt模板
建议为prompt添加统一格式:
def format_prompt(prompt):
return f"""### Instruction:
{prompt}
### Response:
"""
② 长度过滤
prompts = [p for p in prompts if len(tokenizer(p).input_ids) < 100]
③ 多样性控制
from collections import defaultdict
prompt_counts = defaultdict(int)
def select_prompt():
prompt = least_used_prompt(prompt_counts)
prompt_counts[prompt] += 1
return prompt
5. 为什么这样设计?
- RL的特性需求:强化学习需要 环境交互,而不仅仅是静态数据
- 在线学习:随着策略变化,模型需要生成新的响应(不同于SFT的固定数据集)
- 奖励信号依赖:必须先有模型输出,才能计算奖励(来自人类或奖励模型)
6. 完整训练示例
from trl import PPOTrainer, PPOConfig
# 初始化
ppo_config = PPOConfig(
batch_size=32,
mini_batch_size=8,
learning_rate=1e-5,
)
trainer = PPOTrainer(
model=model,
config=ppo_config,
tokenizer=tokenizer,
)
# 训练循环
for _ in range(100):
# 1. 生成数据
prompts = load_prompts(batch_size=32) # 你的数据加载逻辑
outputs = trainer.generate(prompts, max_length=128)
# 2. 计算奖励(这里需要自定义!)
rewards = [calculate_reward(text) for text in outputs]
# 3. PPO更新
trainer.step(prompts, outputs, rewards)
7. 常见问题解决方案
Q1:如何加载大规模数据集?
A:使用迭代器动态流式加载:
def prompt_generator(dataset, batch_size):
for i in range(0, len(dataset), batch_size):
yield dataset[i:i+batch_size]["prompt"]
Q2:如何保证prompt多样性?
A:
- 使用聚类算法对prompts分组
- 每批次从不同聚类中采样
Q3:如何处理长文本?
A:
- 在生成时设置
max_length
- 预处理时过滤过长prompts:
tokenizer(prompt, return_length=True).length <= max_seq_length
总结
PPOTrainer
的数据处理流程体现了RL的核心思想——通过交互动态生成训练数据。虽然看起来比SFT更复杂,但这种设计能够:
- 实现真正的在线学习
- 灵活适应不同奖励信号来源
- 支持开放域生成任务
4.3 DPOTrainer示例
这个示例使用的数据和模型与4.1 PPOTrainer示例是一样的
1. DPO 数据格式要求
DPO 需要包含 三元组 (prompt, chosen_response, rejected_response),典型结构如下:
{
"prompt": "如何泡一杯好喝的茶?",
"chosen": "首先选择优质茶叶...(详细步骤)", # 人类偏好的优质回答
"rejected": "把茶叶扔进水里" # 质量较差的回答
}
数据字段说明:
字段 | 必须 | 说明 |
---|---|---|
prompt |
是 | 输入的问题/指令 |
chosen |
是 | 被人类/奖励模型判定为优质的响应(来自微调模型或人工标注) |
rejected |
是 | 被判定为劣质的响应(来自基线模型或随机生成) |
metadata |
否 | 可选的附加信息(如评分差异、标注来源等) |
2. 完整训练代码示例
步骤1:加载并预处理数据
from datasets import load_dataset
import numpy as np
# 加载原始数据集(这里以firefly为例,实际DPO需要偏好数据)
dataset = load_dataset("YeungNLP/firefly-train-1.1M", split="train[:5000]")
# 模拟创建偏好数据(实际应用需真实标注)
def create_dpo_dataset(examples):
return {
"prompt": ["### 问题:\n" + q + "\n\n### 回答:" for q in examples["input"]],
"chosen": examples["target"], # 假设原始target是优质回答
"rejected": [t[:len(t)//2] + "..." for t in examples["target"]] # 模拟劣质回答(截断)
}
dpo_dataset = dataset.map(create_dpo_dataset, batched=True, remove_columns=dataset.column_names)
步骤2:初始化模型和Tokenizer
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig
model_name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B"
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
# 基础模型
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.bfloat16,
device_map="auto"
)
# 参考模型(通常是不微调的初始模型)
ref_model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.bfloat16,
device_map="auto"
)
# 可选:添加LoRA
peft_config = LoraConfig(
r=8,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.1,
)
model = get_peft_model(model, peft_config)
步骤3:配置DPOTrainer
from trl import DPOTrainer
from transformers import TrainingArguments
training_args = TrainingArguments(
output_dir="./dpo_results",
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=5e-6,
logging_steps=10,
save_steps=500,
fp16=True,
remove_unused_columns=False # DPO需要保留原始文本字段
)
dpo_trainer = DPOTrainer(
model=model,
ref_model=ref_model,
args=training_args,
beta=0.1, # DPO温度参数(关键!)
train_dataset=dpo_dataset,
tokenizer=tokenizer,
max_length=512,
max_prompt_length=256,
)
步骤4:启动训练
dpo_trainer.train()
# 保存适配器(如果用了LoRA)
model.save_pretrained("dpo_firefly_lora")
3. 关键参数解析
DPO特有参数:
参数 | 推荐值 | 说明 |
---|---|---|
beta |
0.1-0.5 | 控制偏离参考模型的强度(越大越允许偏离) |
loss_type |
“sigmoid” | 损失函数类型(可选"sigmoid"或"hinge") |
max_prompt_length |
256 | Prompt最大长度(超过部分截断) |
generate_during_eval |
True | 是否在评估时生成样本(可视化进度) |
数据预处理技巧:
平衡偏好对:
# 确保chosen和rejected长度差异不过大 dataset = dataset.filter(lambda x: 0.5 < len(x["chosen"])/len(x["rejected"]) < 2)
数据增强:
# 对同一prompt创建多个偏好对 expanded_data = [] for example in dataset: for _ in range(2): # 每个样本复制2次 expanded_data.append(example)
清洗低质量数据:
# 移除包含敏感词的样本 bad_words = ["不确定", "不知道"] dataset = dataset.filter(lambda x: not any(w in x["chosen"] for w in bad_words))
4. 真实场景数据准备建议
方案A:人工标注偏好
# 标注数据示例(JSON格式)
[
{
"prompt": "Python如何反转列表?",
"chosen": "可以使用lst[::-1]或list(reversed(lst))",
"rejected": "用for循环慢慢转",
"annotator": "expert_1",
"score_diff": 2 # chosen比rejected高2分(1-5分制)
}
]
方案B:利用现有排名数据
# 将排名数据转为DPO格式
def convert_rankings_to_dpo(ranked_examples):
return {
"prompt": ranked_examples["prompt"],
"chosen": ranked_examples["responses"][0], # 第1名
"rejected": ranked_examples["responses"][-1] # 最后一名
}
方案C:基于奖励模型生成
# 用RM筛选高低分回答
for prompt in prompts:
responses = generate_multiple_responses(prompt)
scores = reward_model(responses)
dpo_data.append({
"prompt": prompt,
"chosen": responses[scores.argmax()],
"rejected": responses[scores.argmin()]
})
5. 效果评估方法
人工检查:
# 生成对比示例 for i in range(3): print(f"Prompt: {dpo_dataset[i]['prompt']}") print(f"Before DPO: {dpo_dataset[i]['rejected']}") print(f"After DPO: {model.generate(dpo_dataset[i]['prompt'])}")
定量指标:
# 计算chosen/rejected的平均logprob差异 def compute_preference_accuracy(trainer): chosen_logps = trainer.eval_dataset["chosen_logps"] rejected_logps = trainer.eval_dataset["rejected_logps"] return (chosen_logps > rejected_logps).mean()
使用RM验证:
# 比较微调前后RM评分 post_train_scores = reward_model([model.generate(p) for p in test_prompts]) print(f"Average RM score improved by {post_train_scores.mean() - baseline_scores.mean():.2f}")
常见问题解决方案
Q1:如果没有现成的(chosen, rejected)对怎么办?
A:可以通过以下方法创建:
- 用不同温度采样生成多个回答,选best/worst
- 使用早期模型生成作为rejected,人工修正作为chosen
Q2:如何选择beta参数?
A:建议从0.1开始尝试:
- 如果模型变化太小 → 增大beta
- 如果输出变得不合理 → 减小beta
Q3:DPO训练后模型退化怎么办?
A:尝试:
- 添加KL散度惩罚:
DPOTrainer(..., beta=0.1, label_smoothing=0.1)
- 混合原始SFT数据:
trainer = DPOTrainer(..., precompute_ref_log_probs=True) # 保留参考模型logits
4.4 PPOTrainer为什么通常不能直接使用trainer.train()
进行训练,而需要手写循环依次step
这正是PPO与SFT/DPO在训练流程上的核心区别。下面我会详细解释这种设计差异的原因,并给出更工程化的PPO训练方案:
1. 设计差异的本质原因
训练类型 | 数据来源 | 训练模式 | Trainer设计 |
---|---|---|---|
SFT | 静态数据集 | 前向传播 | 标准.train() |
DPO | 静态偏好对 | 对比学习 | 标准.train() |
PPO | 动态生成+在线奖励 | 环境交互 | 必须手动.step() |
- PPO的特殊性:需要实时完成"生成→评分→更新"的闭环,无法预先准备数据
- RL的交互本质:强化学习本身就是通过
(行动→反馈→更新)
的循环进行的
2. 工程化PPO训练方案
虽然需要手动循环,但可以通过以下方式使其更模块化:
方案1:封装训练循环
class PPOTrainingLoop:
def __init__(self, trainer, reward_fn, prompts):
self.trainer = trainer
self.reward_fn = reward_fn # 奖励函数
self.prompt_pool = prompts # 初始prompt池
def run(self, epochs):
for epoch in range(epochs):
# 1. 动态获取prompts(可扩展为从数据库读取)
prompts = self.sample_prompts()
# 2. 生成响应
outputs = self.generate_responses(prompts)
# 3. 计算奖励
rewards = self.compute_rewards(outputs)
# 4. PPO更新
self.trainer.step(prompts, outputs, rewards)
def sample_prompts(self):
"""可扩展为更复杂的数据管理"""
return np.random.choice(self.prompt_pool, size=self.trainer.config.batch_size)
def generate_responses(self, prompts):
return self.trainer.generate(
prompts,
max_length=128,
do_sample=True
)
def compute_rewards(self, texts):
return self.reward_fn(texts) # 外部奖励函数
# 使用示例
loop = PPOTrainingLoop(ppo_trainer, calculate_rewards, initial_prompts)
loop.run(epochs=10)
方案2:使用RLHF框架封装
更复杂的生产级实现可以参考:
3. 为什么PPO不能像SFT那样.train()
?
核心原因在于数据流的动态性:
数据依赖模型:
每次迭代的训练数据需要当前策略模型生成,而SFT/DPO的数据是静态的。奖励实时计算:
需要等待生成文本被评分后才能计算损失,无法预先准备(input, label)
对。课程学习需求:
高级RLHF流程可能需要动态调整:if reward > threshold: prompts = get_harder_prompts() # 提升难度
4. 实际项目中的最佳实践
① 日志记录标准化
# 在step循环中添加监控
for step in range(total_steps):
metrics = trainer.step(...)
# 记录关键指标
wandb.log({
"reward": rewards.mean(),
"kl_div": metrics["kl"],
"lr": trainer.optimizer.param_groups[0]["lr"]
})
# 定期保存检查点
if step % 500 == 0:
trainer.save_pretrained(f"checkpoint-{step}")
② 动态Prompt管理
class PromptDatabase:
def __init__(self):
self.prompts = []
self.usage_count = defaultdict(int)
def add_prompt(self, text):
self.prompts.append(text)
def sample(self, size):
# 优先使用次数少的prompt
probs = 1 / (np.array([self.usage_count[p] for p in self.prompts]) + 1)
return np.random.choice(self.prompts, size=size, p=probs/probs.sum())
③ 安全中断恢复
# 检查是否存在之前的检查点
if os.path.exists("latest_checkpoint"):
trainer.load_state_dict(torch.load("latest_checkpoint"))
try:
while True:
trainer.step(...)
except KeyboardInterrupt:
torch.save(trainer.state_dict(), "latest_checkpoint")
5. 从SFT/DPO迁移到PPO的建议
如果你希望保持类似的开发体验,可以:
使用trlx库:
from trlx import train train( "ppo", model_path="your_sft_model", prompts=prompts, reward_fn=reward_function # 与DPO类似的接口 )
自定义Trainer包装器:
class PPOWrapper: def train(self): while not converged: self.step()
监控工具选择:
- Weights & Biases:实时可视化奖励曲线
- MLflow:参数和模型版本管理
总结
虽然PPO需要手动控制训练循环,但这恰恰提供了RLHF所需的灵活性。对于生产级应用,建议:
- 小规模实验:先用DPO快速验证
- 中等规模:使用封装好的PPO循环(如示例)
- 大规模部署:考虑trlx或Ray RLlib等专业框架
5 奖励函数的思考
能否联合训练奖励模型与reference模型
传统的RLHF流程中奖励模型(RM)和策略模型分开训练存在几个痛点:
- 两阶段训练的复杂性:需要先训练RM,再固定RM训练策略模型
- 奖励漂移(Reward Hacking):策略模型可能学会"欺骗"静态的RM
- 反馈延迟:RM无法实时适应策略模型的变化
以下是几种更先进的解决方案,包括类似对抗训练的方法:
方案1:联合训练奖励模型与策略模型(对抗式)
这种方法类似于GAN,让RM和策略模型在训练过程中相互博弈:
# 伪代码展示对抗训练框架
for epoch in range(epochs):
# 1. 策略模型生成响应
responses = policy_model.generate(prompts)
# 2. 更新奖励模型(区分优质/劣质响应)
reward_model.train_step(
good_responses=human_responses,
bad_responses=responses
)
# 3. 用更新后的RM训练策略模型
rewards = reward_model(responses)
policy_model.ppo_step(rewards)
代表工作:
- RAIN (Reward-Aware INstruction) :动态调整RM
- SPIN (Self-Play Fine-Tuning) :通过自我博弈迭代改进
方案2:直接偏好优化(DPO)
完全绕过奖励模型建模,直接使用偏好数据:
from trl import DPOTrainer
dpo_trainer = DPOTrainer(
model=model,
ref_model=ref_model, # 初始模型的副本
beta=0.1, # 温度参数
train_dataset=preference_dataset, # 需包含chosen/rejected样本
tokenizer=tokenizer,
)
优势:
- 无需单独训练RM
- 直接优化人类偏好
- 计算效率比PPO高3-5倍
方案3:在线奖励建模(Online RM)
让RM与策略模型同步更新:
# 初始化
policy_model = AutoModelForCausalLM.from_pretrained(...)
reward_model = AutoModelForSequenceClassification.from_pretrained(...)
for batch in data:
# 生成当前策略的响应
responses = policy_model.generate(batch["prompts"])
# 获取人类反馈(可以是人工标注或用户点击)
feedback = get_human_feedback(responses)
# 更新RM
reward_loss = reward_model.train_on_feedback(feedback)
# 用最新RM计算奖励
rewards = reward_model(responses)
# 更新策略
policy_model.ppo_step(rewards)
方案4:自我对抗学习(Self-Adversarial Learning)
让模型自身作为评判者:
# 使用模型自己的logits作为奖励信号
def self_reward(texts):
with torch.no_grad():
inputs = tokenizer(texts, return_tensors="pt", padding=True)
outputs = model(**inputs)
return outputs.logits.mean(dim=-1) # 取平均logit作为奖励
# 然后在PPO中使用
trainer.step(prompts, responses, self_reward(responses))
对比总结
方法 | 是否需要独立RM | 训练复杂度 | 抗奖励作弊能力 |
---|---|---|---|
传统PPO | 需要 | 高 | 弱 |
对抗联合训练 | 不需要 | 非常高 | 强 |
DPO | 不需要 | 中 | 中 |
在线RM | 需要(但动态) | 高 | 强 |
自我对抗 | 不需要 | 低 | 弱 |
推荐实践路径
小规模实验:先用DPO快速验证(代码见下方)
# DPO数据准备示例 dpo_dataset = Dataset.from_dict({ "prompt": ["解释量子纠缠"]*100, "chosen": ["量子纠缠是指...(优质回答)"]*100, "rejected": ["这是物理概念"]*100 # 劣质回答 })
中等规模:尝试在线RM更新
# 每K步更新一次RM if step % 100 == 0: reward_model.train_on_new_data(human_feedback)
大规模生产:考虑对抗训练框架如RAIN
未来方向
基于LLM的自动奖励:
# 用大模型(如GPT-4)做自动评估 def auto_reward(text): return gpt4.query(f"请为以下回答打分(0-5):{text}")
多奖励模型集成:
rewards = 0.3*rm1(text) + 0.7*rm2(text)
课程学习(Curriculum Learning):
- 逐步提高奖励标准
- 动态调整KL散度系数