强化学习与RLHF
从马尔可夫决策到大语言模型的人类反馈优化
学习目标
- 理解强化学习的基本概念:智能体、环境、奖励、策略
- 掌握Q-Learning、策略梯度等核心算法原理
- 了解深度强化学习的发展与应用
- 深入理解RLHF的完整训练流程
- 掌握奖励模型的训练方法
- 了解PPO在大语言模型中的具体应用
- 对比DPO与RLHF的优缺点
- 能够使用TRL库进行RLHF训练实践
前置知识
- 机器学习基础概念
- 深度学习基础(神经网络、反向传播)
- 大语言模型基础概念
- 监督学习的基本流程
28.1 强化学习基础概念
强化学习(Reinforcement Learning, RL)是机器学习的一个重要分支,它通过让智能体(Agent)在环境(Environment)中采取行动,根据获得的奖励(Reward)来学习最优策略。与监督学习不同,强化学习没有明确的标签,而是通过试错来学习。
强化学习基本框架
动作 (a)
马尔可夫决策过程(MDP)
强化学习问题通常被形式化为马尔可夫决策过程(Markov Decision Process, MDP),它由五元组 $(S, A, P, R, \gamma)$ 定义:
状态空间 S:所有可能状态的集合
动作空间 A:所有可能动作的集合
转移概率 P:在状态 s 采取动作 a 后转移到状态 s' 的概率
奖励函数 R:在状态 s 采取动作 a 获得的即时奖励
折扣因子 γ:介于0到1之间,决定未来奖励的重要性
核心概念
- 策略(Policy):从状态到动作的映射,表示为 π(a|s)
- 价值函数(Value Function):从某个状态开始的期望累积奖励
- 动作价值函数(Q-Function):在某个状态采取某个动作后的期望累积奖励
- 回报(Return):从当前时刻开始的累积折扣奖励:$G_t = \sum_{k=0}^{\infty} \gamma^k r_{t+k}$
监督学习:有明确的标签,目标是最小化预测误差
强化学习:没有明确标签,目标是最大化长期累积奖励
28.2 核心算法:Q-Learning与策略梯度
强化学习算法主要分为两大类:基于价值的方法和基于策略的方法。Q-Learning是典型的基于价值的方法,而策略梯度则是基于策略的方法的代表。
Q-Learning算法
Q-Learning是一种无模型的强化学习算法,它通过学习一个Q值函数来找到最优策略。Q-Learning的核心思想是:在状态 s 采取动作 a 的价值等于即时奖励加上折扣后的下一个状态的最大Q值。
import numpy as np
import random
from collections import defaultdict
class QLearningAgent:
"""Q-Learning智能体"""
def __init__(self, state_size, action_size, learning_rate=0.1,
discount_factor=0.99, epsilon=0.1):
self.state_size = state_size
self.action_size = action_size
self.lr = learning_rate
self.gamma = discount_factor
self.epsilon = epsilon
# 初始化Q表
self.q_table = np.zeros((state_size, action_size))
def get_action(self, state):
"""ε-贪婪策略选择动作"""
if random.random() < self.epsilon:
# 探索:随机选择动作
return random.randint(0, self.action_size - 1)
else:
# 利用:选择Q值最大的动作
return np.argmax(self.q_table[state])
def update(self, state, action, reward, next_state, done):
"""更新Q值"""
if done:
target = reward
else:
target = reward + self.gamma * np.max(self.q_table[next_state])
# Q-Learning更新公式
self.q_table[state][action] += self.lr * (target - self.q_table[state][action])
def train(self, env, episodes=1000):
"""训练智能体"""
rewards_per_episode = []
for episode in range(episodes):
state = env.reset()
total_reward = 0
done = False
while not done:
action = self.get_action(state)
next_state, reward, done, _ = env.step(action)
self.update(state, action, reward, next_state, done)
state = next_state
total_reward += reward
rewards_per_episode.append(total_reward)
# 每100个episode打印一次
if (episode + 1) % 100 == 0:
avg_reward = np.mean(rewards_per_episode[-100:])
print(f"Episode {episode + 1}, Average Reward: {avg_reward:.2f}")
return rewards_per_episode
# 示例使用
# env = gym.make('FrozenLake-v1')
# agent = QLearningAgent(state_size=16, action_size=4)
# rewards = agent.train(env, episodes=5000)
print("Q-Learning算法示例代码")
策略梯度方法
策略梯度方法直接优化策略函数,通过梯度上升来最大化期望回报。与Q-Learning不同,策略梯度方法可以处理连续动作空间,并且能够学习随机策略。
import torch
import torch.nn as nn
import torch.optim as optim
from torch.distributions import Categorical
class PolicyNetwork(nn.Module):
"""策略网络:输入状态,输出动作概率分布"""
def __init__(self, state_size, action_size, hidden_size=64):
super(PolicyNetwork, self).__init__()
self.network = nn.Sequential(
nn.Linear(state_size, hidden_size),
nn.ReLU(),
nn.Linear(hidden_size, hidden_size),
nn.ReLU(),
nn.Linear(hidden_size, action_size),
nn.Softmax(dim=-1)
)
def forward(self, state):
return self.network(state)
class PolicyGradientAgent:
"""策略梯度智能体"""
def __init__(self, state_size, action_size, lr=0.001):
self.policy = PolicyNetwork(state_size, action_size)
self.optimizer = optim.Adam(self.policy.parameters(), lr=lr)
self.gamma = 0.99
# 存储轨迹
self.states = []
self.actions = []
self.rewards = []
def get_action(self, state):
"""根据策略网络选择动作"""
state_tensor = torch.FloatTensor(state).unsqueeze(0)
action_probs = self.policy(state_tensor)
# 从概率分布中采样
dist = Categorical(action_probs)
action = dist.sample()
# 记录状态和动作
self.states.append(state_tensor)
self.actions.append(action)
return action.item()
def store_reward(self, reward):
"""存储奖励"""
self.rewards.append(reward)
def update(self):
"""使用策略梯度更新网络"""
# 计算折扣回报
returns = []
G = 0
for reward in reversed(self.rewards):
G = reward + self.gamma * G
returns.insert(0, G)
returns = torch.FloatTensor(returns)
# 标准化回报(减少方差)
if len(returns) > 1:
returns = (returns - returns.mean()) / (returns.std() + 1e-8)
# 计算策略梯度损失
policy_loss = []
for log_prob, G in zip(
[torch.log(prob) for prob in self.policy(torch.cat(self.states)).gather(1, torch.stack(self.actions).unsqueeze(1)).squeeze()],
returns
):
policy_loss.append(-log_prob * G)
policy_loss = torch.stack(policy_loss).sum()
# 更新网络
self.optimizer.zero_grad()
policy_loss.backward()
self.optimizer.step()
# 清空轨迹
self.states = []
self.actions = []
self.rewards = []
return policy_loss.item()
print("策略梯度算法示例代码")
策略梯度方法的方差通常很大,因此需要使用各种方差减少技术,如基线(baseline)、优势函数(advantage function)等。
算法对比
| 特性 | Q-Learning | 策略梯度 |
|---|---|---|
| 方法类型 | 基于价值 | 基于策略 |
| 动作空间 | 离散 | 离散/连续 |
| 收敛性 | 需要满足某些条件 | 局部最优 |
| 样本效率 | 较低 | 较高 |
28.3 深度强化学习
深度强化学习(Deep Reinforcement Learning, Deep RL)是将深度学习与强化学习相结合的研究领域。它使用深度神经网络来近似价值函数或策略函数,从而能够处理高维状态空间的问题。
DQN
深度Q网络
2013-2015A3C
异步优势Actor-Critic
2016PPO
近端策略优化
2017SAC
软演员-评论家
2018DQN(深度Q网络)
DQN是深度强化学习的里程碑工作,它使用深度神经网络来近似Q值函数,并引入了经验回放(Experience Replay)和目标网络(Target Network)来稳定训练。
经验回放:存储历史经验,随机采样进行训练,打破数据相关性
目标网络:使用独立的网络计算目标值,减少训练不稳定
Actor-Critic方法
Actor-Critic方法结合了基于价值和基于策略的方法,Actor负责选择动作,Critic负责评估动作的价值。
Actor:策略网络,输入状态,输出动作分布
Critic:价值网络,输入状态(或状态-动作对),输出价值估计
优势:Actor的梯度由Critic提供,减少了方差
28.4 RLHF详解
RLHF(Reinforcement Learning from Human Feedback,基于人类反馈的强化学习)是近年来大语言模型训练中的重要技术。它通过人类的偏好反馈来优化模型,使其生成的内容更符合人类的期望和价值观。
RLHF训练流程
为什么需要RLHF?
传统的监督学习方法存在以下问题:
- 标注不一致:不同标注者可能给出不同的标签
- 难以定义"好"的回答:主观性强,难以用简单标签表示
- 安全和对齐问题:模型可能生成有害或偏见内容
RLHF通过以下方式解决这些问题:
- 比较学习:让人类比较两个回答的好坏,比直接打分更容易
- 间接优化:通过奖励模型间接优化人类偏好
- 灵活调整:可以根据不同场景调整偏好方向
标注成本高:需要大量人类标注数据
奖励模型偏差:如果标注数据有偏差,奖励模型也会学习到偏差
训练不稳定:PPO训练可能不稳定,需要仔细调参
28.5 奖励模型训练
奖励模型(Reward Model)是RLHF的核心组件之一。它的作用是学习人类的偏好,将人类的主观判断转化为可以用于优化的数值信号。
奖励模型的输入输出
- 输入:一个问题(prompt)和一个回答(response)
- 输出:一个标量分数,表示这个回答的质量
训练数据格式
奖励模型的训练数据通常由偏好对(preference pairs)组成:
每个样本包含:问题 q、回答 r₁、回答 r₂、偏好标签 y
其中 y = 1 表示 r₁ 优于 r₂,y = 0 表示 r₂ 优于 r₁
训练目标
奖励模型通常使用 Bradley-Terry 模型进行训练,目标是最大化正确偏好对的似然概率:
import torch
import torch.nn as nn
from transformers import AutoModelForSequenceClassification, AutoTokenizer
class RewardModel(nn.Module):
"""奖励模型:基于预训练语言模型"""
def __init__(self, model_name="gpt2", hidden_size=768):
super(RewardModel, self).__init__()
self.reward_model = AutoModelForSequenceClassification.from_pretrained(
model_name, num_labels=1
)
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
if self.tokenizer.pad_token is None:
self.tokenizer.pad_token = self.tokenizer.eos_token
def forward(self, input_ids, attention_mask):
"""前向传播,返回奖励分数"""
outputs = self.reward_model(input_ids, attention_mask=attention_mask)
return outputs.logits.squeeze(-1)
def reward_loss(reward_chosen, reward_rejected):
"""Bradley-Terry偏好损失"""
# 希望 chosen 的奖励高于 rejected
loss = -torch.log(torch.sigmoid(reward_chosen - reward_rejected))
return loss.mean()
def train_reward_model(model, train_dataloader, optimizer, epochs=3):
"""训练奖励模型"""
model.train()
for epoch in range(epochs):
total_loss = 0
for batch in train_dataloader:
# 编码 chosen 和 rejected 回答
chosen_input = model.tokenizer(
batch['chosen'],
padding=True,
truncation=True,
return_tensors='pt'
)
rejected_input = model.tokenizer(
batch['rejected'],
padding=True,
truncation=True,
return_tensors='pt'
)
# 计算奖励
reward_chosen = model(**chosen_input)
reward_rejected = model(**rejected_input)
# 计算损失
loss = reward_loss(reward_chosen, reward_rejected)
# 更新模型
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
avg_loss = total_loss / len(train_dataloader)
print(f"Epoch {epoch + 1}, Average Loss: {avg_loss:.4f}")
print("奖励模型训练示例代码")
准确率:在测试集上预测偏好的准确率
奖励分数分布:检查奖励分数是否符合预期
与人类评估的相关性:奖励模型的分数是否与人类评分相关
28.6 PPO在大语言模型中的应用
PPO(Proximal Policy Optimization,近端策略优化)是目前RLHF中最常用的强化学习算法。它通过限制策略更新的幅度来保证训练的稳定性。
PPO的核心思想
PPO的核心是 clipped surrogate objective,它限制了策略更新的幅度,防止更新过大导致训练不稳定:
import torch
import torch.nn as nn
import torch.optim as optim
from torch.distributions import Categorical
import numpy as np
class PPOAgent:
"""PPO智能体:用于大语言模型的RLHF训练"""
def __init__(self, policy_model, ref_model, reward_model,
lr=1e-5, clip_range=0.2, gamma=0.99):
self.policy_model = policy_model # 被优化的策略模型
self.ref_model = ref_model # 参考模型(SFT模型)
self.reward_model = reward_model # 奖励模型
self.optimizer = optim.Adam(self.policy_model.parameters(), lr=lr)
self.clip_range = clip_range
self.gamma = gamma
self.kl_coef = 0.1 # KL散度惩罚系数
def compute_kl_penalty(self, policy_logits, ref_logits):
"""计算KL散度惩罚"""
policy_dist = torch.distributions.Categorical(logits=policy_logits)
ref_dist = torch.distributions.Categorical(logits=ref_logits)
# KL(policy || ref)
kl = torch.distributions.kl_divergence(policy_dist, ref_dist)
return kl
def ppo_loss(self, old_log_probs, new_log_probs, advantages, clip_range):
"""PPO损失函数"""
# 比率:新策略 / 旧策略
ratio = torch.exp(new_log_probs - old_log_probs)
# 裁剪的目标函数
clipped_ratio = torch.clamp(ratio, 1 - clip_range, 1 + clip_range)
# 取两者中的最小值
surrogate1 = ratio * advantages
surrogate2 = clipped_ratio * advantages
return -torch.min(surrogate1, surrogate2).mean()
def compute_advantages(self, rewards, values, gamma=0.99, lam=0.95):
"""计算优势函数(GAE)"""
advantages = []
gae = 0
for reward, value in zip(reversed(rewards), reversed(values)):
next_value = values[values.index(value) + 1] if values.index(value) < len(values) - 1 else 0
delta = reward + gamma * next_value - value
gae = delta + gamma * lam * gae
advantages.insert(0, gae)
advantages = torch.FloatTensor(advantages)
# 标准化
if len(advantages) > 1:
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
return advantages
def update(self, rollout):
"""PPO更新"""
states = rollout['states']
actions = rollout['actions']
old_log_probs = rollout['log_probs']
rewards = rollout['rewards']
# 计算当前策略的log概率
new_log_probs = []
policy_logits_list = []
ref_logits_list = []
for state in states:
# 当前策略
state_tensor = torch.FloatTensor(state).unsqueeze(0)
policy_output = self.policy_model(state_tensor)
policy_logits = policy_output['logits']
policy_dist = torch.distributions.Categorical(logits=policy_logits)
new_log_prob = policy_dist.log_prob(torch.LongTensor([actions[states.index(state)]]))
new_log_probs.append(new_log_prob)
policy_logits_list.append(policy_logits)
# 参考模型
with torch.no_grad():
ref_output = self.ref_model(state_tensor)
ref_logits_list.append(ref_output['logits'])
new_log_probs = torch.stack(new_log_probs)
old_log_probs = torch.FloatTensor(old_log_probs)
# 计算优势函数
values = torch.zeros(len(rewards)) # 简化:假设value=0
advantages = self.compute_advantages(rewards, values)
# PPO损失
ppo_loss = self.ppo_loss(old_log_probs, new_log_probs, advantages, self.clip_range)
# KL惩罚
kl_penalties = []
for policy_logits, ref_logits in zip(policy_logits_list, ref_logits_list):
kl_penalty = self.compute_kl_penalty(policy_logits, ref_logits)
kl_penalties.append(kl_penalty)
kl_penalty = torch.stack(kl_penalties).mean()
# 总损失
total_loss = ppo_loss + self.kl_coef * kl_penalty
# 更新模型
self.optimizer.zero_grad()
total_loss.backward()
self.optimizer.step()
return ppo_loss.item(), kl_penalty.item()
print("PPO算法实现示例")
学习率调度:使用较小的学习率(如1e-5到5e-5)
KL惩罚:防止策略偏离参考模型太远
mini-batch更新:使用mini-batch进行更新以提高稳定性
28.7 DPO与RLHF的对比
DPO(Direct Preference Optimization,直接偏好优化)是近年来提出的一种替代RLHF的方法。它直接优化策略模型,不需要单独训练奖励模型。
DPO的核心思想
DPO的核心思想是:将奖励模型的优化目标直接转化为策略优化目标,从而跳过奖励模型的训练步骤。
import torch
import torch.nn.functional as F
def dpo_loss(policy_chosen_logps, policy_rejected_logps,
reference_chosen_logps, reference_rejected_logps,
beta=0.1):
"""
DPO损失函数
Args:
policy_chosen_logps: 策略模型对chosen回答的log概率
policy_rejected_logps: 策略模型对rejected回答的log概率
reference_chosen_logps: 参考模型对chosen回答的log概率
reference_rejected_logps: 参考模型对rejected回答的log概率
beta: 温度参数,控制与参考模型的偏离程度
Returns:
DPO损失
"""
# 计算log比率
chosen_log_ratio = policy_chosen_logps - reference_chosen_logps
rejected_log_ratio = policy_rejected_logps - reference_rejected_logps
# DPO损失
logits = beta * (chosen_log_ratio - rejected_log_ratio)
loss = -F.logsigmoid(logits).mean()
return loss
# 计算log概率的辅助函数
def compute_log_probs(model, input_ids, attention_mask):
"""计算模型对输入的log概率"""
outputs = model(input_ids, attention_mask=attention_mask)
logits = outputs.logits
# 使用log_softmax计算log概率
log_probs = F.log_softmax(logits, dim=-1)
# 取token级别的log概率并求和
token_log_probs = torch.gather(
log_probs[:, :-1],
dim=2,
index=input_ids[:, 1:].unsqueeze(2)
).squeeze(2)
return token_log_probs.sum(dim=-1)
print("DPO损失函数示例")
DPO vs RLHF对比
| 特性 | RLHF (PPO) | DPO |
|---|---|---|
| 训练流程 | 奖励模型 → PPO优化 | 直接优化策略 |
| 计算复杂度 | 高(需要多个模型) | 低(只需策略和参考模型) |
| 稳定性 | 需要仔细调参 | 更稳定 |
| 效果 | 通常更好 | 接近RLHF |
| 内存需求 | 高 | 低 |
选择RLHF:当计算资源充足,追求最佳效果时
选择DPO:当计算资源有限,需要快速迭代时
28.8 动手实践:TRL库
TRL(Transformer Reinforcement Learning)是Hugging Face提供的一个库,它简化了RLHF训练流程。TRL提供了SFTTrainer、RewardTrainer、PPOTrainer等组件,使得在大语言模型上进行RLHF训练变得更加容易。
安装与配置
# 安装TRL及其依赖
pip install trl
pip install transformers
pip install datasets
pip install peft # 用于LoRA
SFT(监督微调)阶段
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments
from trl import SFTTrainer
from datasets import load_dataset
# 加载模型和分词器
model_name = "gpt2"
model = AutoModelForCausalLM.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
# 加载数据集
dataset = load_dataset("imdb", split="train[:1000]")
def preprocess_function(examples):
"""预处理数据集"""
return tokenizer(
examples["text"],
truncation=True,
padding="max_length",
max_length=512
)
tokenized_dataset = dataset.map(preprocess_function, batched=True)
# 训练参数
training_args = TrainingArguments(
output_dir="./sft_model",
num_train_epochs=3,
per_device_train_batch_size=4,
learning_rate=2e-5,
warmup_steps=100,
logging_steps=10,
save_strategy="epoch",
)
# SFT训练器
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset,
tokenizer=tokenizer,
max_seq_length=512,
)
# 开始训练
trainer.train()
print("SFT训练完成!")
RLHF训练流程
from trl import PPOTrainer, PPOConfig, AutoModelForCausalLMWithValueHead
from transformers import AutoTokenizer
import torch
# 1. 加载SFT模型(作为初始策略)
model_name = "gpt2"
model = AutoModelForCausalLMWithValueHead.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
# 2. 加载参考模型(SFT模型的副本)
ref_model = AutoModelForCausalLMWithValueHead.from_pretrained(model_name)
# 3. PPO配置
ppo_config = PPOConfig(
learning_rate=1e-5,
batch_size=16,
mini_batch_size=4,
ppo_epochs=4,
kl_penalty="kl", # 或 "abs", "full"
init_kl_coef=0.2,
target_kl=6.0,
)
# 4. 创建PPO训练器
ppo_trainer = PPOTrainer(
config=ppo_config,
model=model,
ref_model=ref_model,
tokenizer=tokenizer,
)
# 5. 加载奖励模型(示例:使用简单的启发式奖励)
def reward_function(response):
"""简单的奖励函数(实际应使用训练好的奖励模型)"""
# 示例:奖励包含特定关键词的回答
good_keywords = ["helpful", "accurate", "clear"]
reward = sum(1 for word in good_keywords if word in response.lower())
return reward
# 6. 准备训练数据
prompts = [
"What is machine learning?",
"Explain neural networks",
"What is reinforcement learning?",
]
# 7. RLHF训练循环
for epoch in range(3):
print(f"\n=== Epoch {epoch + 1} ===")
for prompt in prompts:
# 编码prompt
query_tensors = tokenizer.encode(prompt, return_tensors="pt")[0]
# 生成回答
response_tensors = ppo_trainer.generate(
query_tensors.unsqueeze(0),
max_new_tokens=128,
do_sample=True,
top_k=50,
top_p=0.95,
)
# 解码回答
response = tokenizer.decode(response_tensors[0], skip_special_tokens=True)
# 计算奖励
reward = reward_function(response)
reward_tensors = [torch.tensor([reward])]
# PPO更新
stats = ppo_trainer.step(
[query_tensors],
[response_tensors[0]],
reward_tensors
)
print(f"Prompt: {prompt[:30]}...")
print(f"Reward: {reward:.2f}")
print(f"KL: {stats['ppo/kl']:.2f}")
print(f"Entropy: {stats['ppo/entropy']:.2f}")
print("\nRLHF训练完成!")
评估与监控
集成度高:SFT、奖励模型、PPO训练一站式解决
易于使用:提供简洁的API,快速开始训练
灵活可扩展:支持自定义训练流程和奖励函数
生态丰富:与Hugging Face其他库无缝集成
练习题
练习1:Q-Learning实现
实现一个Q-Learning智能体,在FrozenLake环境中训练,并绘制训练曲线。
提示:使用gym库创建环境,实现ε-贪婪策略,记录每个episode的奖励。
练习2:奖励模型训练
使用给定的偏好对数据,训练一个奖励模型,并评估其准确率。
提示:使用Bradley-Terry模型,计算chosen和rejected回答的奖励差异。
练习3:DPO vs RLHF对比实验
在相同的数据集上分别使用DPO和RLHF训练模型,比较两者的效果和训练效率。
提示:使用TRL库,记录训练时间、GPU内存使用、最终效果等指标进行对比。
本章小结
- 强化学习通过智能体与环境的交互来学习最优策略
- Q-Learning和策略梯度是两种核心的强化学习算法
- 深度强化学习使用神经网络来近似价值函数或策略
- RLHF通过人类反馈来优化大语言模型
- 奖励模型学习人类偏好,将主观判断转化为数值信号
- PPO是RLHF中最常用的优化算法
- DPO提供了一种更简单、更稳定的替代方案
- TRL库简化了RLHF训练流程,提供了完整的工具链