问题源于PPOTrainer
里并没有跟SFTTrainer
类似的peft_config
参数,而SFTTrainer
在带和不带peft_config
参数的情况下分别对应高效微调和全量微调。自然就会想到是否可以把PPO和PEFT结合,但是目前peft包和trl包上似乎还是存在这种兼容性的问题。
另一个问题就是奖励函数的设置,这个是RL从诞生以来一直存在的一个老大难问题。现在有很多方案,但是我始终觉得奖励模型应该与reference model一起训练是最好的,这就有点像GAN,肯定是可以实现的,无非是模块设计与效果的问题。
以下摘自与deepseek-r1的对话记录,是很有帮助的。
PS:关于PPO的部分有很大问题,似乎它并不是很熟悉这部分的内容,我暂时还没有修改PPO相关的内容,比如PPOTrainer根本就没有tokenizer这个参数,应该是processing_class,以及还缺少ref_model和reward_model,实际上PPOTrainer也是需要train_dataset的。我觉得应该不是版本问题,但是确实回答得有很大问题。
不过有问题也不是什么坏事。
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={...}, # 优化器额外参数
)
用于监督式微调的Trainer,继承自HuggingFace的Trainer。
关键参数:
model
: 要微调的模型args
: TrainingArguments对象train_dataset
: 训练数据集eval_dataset
: 评估数据集(可选)tokenizer
: 用于文本处理的tokenizerdata_collator
: 数据整理器(可选)compute_metrics
: 计算指标的函数(可选)peft_config
: PEFT配置(用于参数高效微调)用于训练奖励模型的Trainer。
关键参数:
model
: 奖励模型args
: TrainingArguments对象train_dataset
: 训练数据集eval_dataset
: 评估数据集(可选)tokenizer
: 用于文本处理的tokenizerdata_collator
: 数据整理器(可选)compute_metrics
: 计算指标的函数(可选)用于直接偏好优化训练的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), # 优化器和学习率调度器
)
大多数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
: 是否使用混合精度训练这边偶然发现还有一个XPOTrainer
,不过看起来并不是很有用。
XPOTrainer 是 TRL 库中较新引入的一个 Trainer,它实现了 XPO (eXploration-Policy Optimization) 算法。这是一种新型的强化学习算法,专门为语言模型微调设计,旨在解决传统 PPO 在语言任务中的一些局限性。
XPO 算法的核心创新点在于将策略优化分解为两个部分:
探索阶段 (Exploration Phase)
策略优化阶段 (Policy Optimization Phase)
XPO 相比 PPO 的优势:
更好的探索能力:
更稳定的训练:
更适合语言任务:
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):
exploration_beta
(默认1.0):
dual_learning_rate
(默认1e-5):
sample_from
(默认"exploration"):
exploration_steps
(默认1):
考虑使用 XPOTrainer 当:
实践建议
初始设置:
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
)
监控指标:
调优方向:
XPO是一种较新的算法,目前在实际应用中的最佳实践仍在探索中,建议从小规模实验开始逐步调整参数。
对于RLHF流程,通常组合使用:
参数调优建议:
对于DPO训练:
PPOTrainer
里面似乎并没有peft_config
这个选项,我思考了一下,本质上PPO也是在初始的reference模型上不断更新迭代,其实说实话也是可以用lora之类的方法实现的,也就是只在初始模型基础上更新lora块,似乎也是可行的,但PPOTrainer
里面并不支持这么做。
确实,从原理上讲,PPO训练完全可以与PEFT(如LoRA)结合,只更新适配器参数而非整个模型,但当前TRL库的PPOTrainer
确实没有直接内置对peft_config
的支持。不过,我们仍然可以通过一些方法实现这一目标。以下是详细分析和解决方案:
当前TRL的限制
PPOTrainer
的设计:
ref_model
)通常直接复制自主模型,未考虑PEFT架构。model = AutoModelForCausalLM.from_pretrained(...)
model = get_peft_model(model, LoraConfig(...))
trainer = PPOTrainer(model=model, ...) # 可能引发兼容性问题
这个目前测试下来确实是会报错的,但可能版本会有差异,下面手动调控确实是一种可行的方案,即手动调整get_peft_model
得到的模型中可训练的参数,即手动冻结掉非LoRA块的部分。
步骤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散度计算:
梯度更新:
optimizer.param_groups
确认仅适配层参数被更新。保存与加载:
model.save_pretrained("output_dir", save_adapter=True)
性能监控:
使用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)
这个东西似乎并不常用,因为通常都是直接用transformers.TrainingArguments
来配置,SFTConfig似乎用的很少,实则不然,这个原因是版本迭代导致的。
TrainingArguments
,SFTConfig
是后来新增的。建议查看TRL官方文档的最新示例(如Llama-2微调脚本),现在已逐步转向SFTConfig
优先。
SFTConfig
vs TrainingArguments
的核心区别特性 | SFTConfig (TRL专属) |
TrainingArguments (Transformers通用) |
---|---|---|
设计目标 | 针对SFT任务的特殊配置 | 通用训练参数 |
参数范围 | 包含数据格式、打包策略等SFT特有参数 | 基础训练超参数(LR、batch size等) |
依赖库 | 专为TRL的SFTTrainer 设计 |
适用于所有HuggingFace Trainer |
PEFT集成 | 直接支持peft_config 参数 |
需额外单独传递PEFT配置 |
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,
...
)
场景1:需要高级SFT功能时
当需要使用以下特性时,优先用SFTConfig
:
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
)
文本打包(Packing)
seq_len=2048
),避免填充浪费。SFTConfig(
packing=True,
max_seq_length=2048 # 建议设为模型最大长度
)
NEFTune噪声注入
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
)
新项目优先用SFTConfig
:
from trl import SFTConfig, SFTTrainer
config = SFTConfig(
packing=True,
peft_config=lora_config,
learning_rate=2e-5,
...
)
trainer = SFTTrainer(..., args=config)
迁移现有项目时:
TrainingArguments
SFTTrainer
直接传递:trainer = SFTTrainer(
...,
args=existing_training_args, # 原有TrainingArguments
packing=True, # 新增SFT参数
dataset_text_field="text"
)
参数优先级:
SFTTrainer
直接传递的参数(如packing
)会覆盖SFTConfig
中的设置。一个重要的区别PPO的奖励模型是需要事先给定好的,DPO天然的不需要奖励函数,这也是很神奇的地方,DPO的损失推导得到一个非常简洁的结果,尽管它也是从带KL惩罚项的那个原始RL目标函数公式推过来的,确实是很神奇。
下面两个示例使用的基础模型为 deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B
,训练数据就以 YeungNLP/firefly-train-1.1M
为例,这个数据集是一个典型的问答数据集,直接load_dataset即可,问题和答案的字段分别是input
和target
这个示例包含了数据处理、奖励模型构建和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
字段作为问题,添加了中文提示模板奖励函数:
生成参数:
temperature=0.7
:平衡生成多样性和质量top_p=0.9
:核采样(nucleus sampling)提高质量训练优化:
进阶改进建议:
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]
ppo_config = PPOConfig(
init_kl_coef=0.2,
target=6.0,
adap_kl_ctrl=True,
...
)
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)
容易发现,SFTTrainer
和DPOTrainer
等常见Trainer里都有训练数据的参数项,只有PPOTrainer
是没有的。
你提出了一个非常关键的问题!确实,PPOTrainer
的数据处理方式与常规的 Trainer
类有很大不同,它的数据流设计更加动态和交互式,这也是强化学习(RL)与监督学习(SFT)的核心区别之一。下面我会详细解释 PPOTrainer
的数据处理机制及参数配置。
1. PPOTrainer
数据流的特殊性
与 SFTTrainer
的静态数据集不同,PPOTrainer
的数据处理分为两个阶段:
因此,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. 为什么这样设计?
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:
Q3:如何处理长文本?
A:
max_length
tokenizer(prompt, return_length=True).length <= max_seq_length
总结
PPOTrainer
的数据处理流程体现了RL的核心思想——通过交互动态生成训练数据。虽然看起来比SFT更复杂,但这种设计能够:
这个示例使用的数据和模型与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:可以通过以下方法创建:
Q2:如何选择beta参数?
A:建议从0.1开始尝试:
Q3:DPO训练后模型退化怎么办?
A:尝试:
DPOTrainer(..., beta=0.1, label_smoothing=0.1)
trainer = DPOTrainer(..., precompute_ref_log_probs=True) # 保留参考模型logits
trainer.train()
进行训练,而需要手写循环依次step
这正是PPO与SFT/DPO在训练流程上的核心区别。下面我会详细解释这种设计差异的原因,并给出更工程化的PPO训练方案:
1. 设计差异的本质原因
训练类型 | 数据来源 | 训练模式 | Trainer设计 |
---|---|---|---|
SFT | 静态数据集 | 前向传播 | 标准.train() |
DPO | 静态偏好对 | 对比学习 | 标准.train() |
PPO | 动态生成+在线奖励 | 环境交互 | 必须手动.step() |
(行动→反馈→更新)
的循环进行的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框架封装
更复杂的生产级实现可以参考:
accelerate_ppo()
等高阶API3. 为什么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()
监控工具选择:
总结
虽然PPO需要手动控制训练循环,但这恰恰提供了RLHF所需的灵活性。对于生产级应用,建议:
能否联合训练奖励模型与reference模型
传统的RLHF流程中奖励模型(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)
代表工作:
方案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,
)
优势:
方案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):