经典RL算法详解
Q-learning、DQN 及其衍生族系是值函数方法(value-based RL)的核心脉络,从表格型更新到深度网络近似,再到 Double/Dueling/Prioritized/Rainbow 的逐步改进,最终延伸到 DDPG/TD3/SAC 等连续动作空间算法——它们共同构成了现代强化学习的算法基石。本页深入对比 on-policy 与 off-policy,逐一拆解各算法的动机、公式与工程要点,并补充 2024–2025 年的前沿进展(REDQ、DroQ、CrossQ、Discrete SAC 等)。前置阅读:强化学习概览、里程碑。
在展开各算法之前,先简要回顾几个贯穿全页的基础概念:
- 马尔可夫决策过程(Markov Decision Process, MDP):强化学习的标准数学建模框架,由五元组 (S, A, P, R, γ) 描述——状态集 S、动作集 A、状态转移概率 P(s’|s,a)、奖励函数 R(s,a)、折扣因子 γ ∈ [0, 1]。MDP 假设”未来只取决于当前状态与动作”(马尔可夫性),这使得我们可以用递推的方式逐步逼近最优策略。
- 回报(Return):从时刻 t 起的折扣累积奖励
G_t = r_t + γ·r_{t+1} + γ²·r_{t+2} + …。γ < 1 保证无限期回报有界,也反映”近期奖励比远期奖励更重要”的直觉。 - 探索-利用权衡(Exploration-Exploitation Tradeoff):智能体在”尝试未知动作以发现更优策略”(探索)和”执行当前已知最优动作以获取奖励”(利用)之间必须持续权衡。ε-greedy、softmax 采样、熵正则化、Noisy Net 等都是不同探索机制的具体实现,贯穿本文几乎每一个算法。
- 行为策略与目标策略:行为策略(behavior policy)是智能体实际用来与环境交互、生成数据的策略;目标策略(target policy)是智能体想要优化、最终部署的策略。off-policy 的核心特征就是两者可以不同。
理解 off-policy 与 on-policy 最直观的方式是”学习方式”的比喻:
- Q-learning(off-policy)= 看别人经验就能进步——就像你站在赌桌旁看高手打牌,不管那位高手实际上做了什么动作,你都在心里默默记下”如果换成我,在当前局面下能采取的所有动作里最好的那个值是多少”。更新时你用的是”下一步所有动作中的最大值”(max),也就是你心目中的理想策略,而不是别人(或你自己当前策略)真实采取的动作。
- SARSA(on-policy)= 必须亲自体验才学到东西——你必须亲自下场打牌,用自己当前的策略真的选了某个动作 a’,然后看真实结果。更新时用的是”下一步实际采取的动作 a’ 的 Q 值”,行为策略与目标策略是同一个。
- DQN = 用神经网络当”价值账本”——当状态空间从几个格子变成几十万像素的图像时,Q 表存不下了,用一个神经网络 Q(s,a; θ) 来近似;为了让神经网络稳定学习,引入经验回放(打乱样本相关性)和目标网络(稳定训练目标)两大技巧。
- Double DQN = 防止”自卖自夸”——DQN 用同一个网络既选动作又估价值,容易过度乐观(Q 值过估计)。Double DQN 让一个网络”提名”动作、另一个网络”打分”,互相制衡。
- SAC = 一边完成任务一边保持好奇心——在奖励之外额外奖励”策略的不确定性/熵”,让智能体在学好任务的同时不放弃探索,避免陷入局部最优。
Q-learning 与 SARSA 的更新公式
Section titled “Q-learning 与 SARSA 的更新公式”设 Q(s, a) 为状态 s 下采取动作 a 的期望累积回报,α 为学习率,γ 为折扣因子,r 为即时奖励,s’ 为下一状态。两者的时序差分(TD)更新公式如下:
Q-learning(off-policy):
注意目标值里用的是 max——无论当前策略实际会选什么动作,都按”最优动作”来估价值。因此 Q-learning 直接学习最优策略 Q*,与生成数据的行为策略解耦,属于 off-policy。
SARSA(on-policy):
其中 a’ 是当前策略(如 ε-greedy)在 s’ 处实际采样得到的下一步动作。目标值依赖当前策略本身,因此是 on-policy。名称 SARSA 来自序列五元组 (S, A, R, S’, A’)。
关键区别:Q-learning 学的是”理想最优”,在探索性强(ε 大)的环境中训练出的 Q 值仍逼近最优;但部署时若策略与训练时不同,可能出现偏差。SARSA 学的是”当前策略的真实回报”,在含噪声/危险的环境中更保守——例如悬崖行走(Cliff Walking)中 SARSA 会学到远离悬崖的安全路径,而 Q-learning 在 ε-greedy 探索下会乐观估计悬崖边的值,实际走过去容易掉下去。
DQN:深度 Q 网络
Section titled “DQN:深度 Q 网络”当状态空间巨大或连续(如 Atari 游戏画面)时,Q 表不可行,改用深度神经网络近似 Q 函数 Q(s, a; θ)。直接用 Bellman 更新训练神经网络会遇到两个致命问题,DQN 用两个机制解决:
- 经验回放(Experience Replay):将每步转移 (s, a, r, s’) 存入回放缓冲区 D,训练时从中随机小批量采样。这打破了连续样本间的高度相关性,近似满足 i.i.d. 假设,同时提高数据利用效率。
- 目标网络(Target Network):维护一份延迟更新的网络 θ⁻ 用于计算目标值 y = r + γ · max Q(s’, a’; θ⁻),而主网络 θ 用于选动作和梯度更新。每隔 C 步把 θ 同步到 θ⁻。这避免了”追着自己跑”的正反馈导致训练发散。
损失函数(纯文本):
其中 (对非终止状态)。
Double DQN:缓解过估计
Section titled “Double DQN:缓解过估计”DQN 的 max 操作同时承担”选动作”和”估价值”两个角色,容易产生系统性正向偏差(过估计)。Double DQN 将二者分离:
用在线网络 θ 选出最优动作(argmax),再用目标网络 θ⁻ 对该动作估值。由于两个网络的估计误差不完全相关,正向偏差被相互抵消,显著降低过估计。
Dueling DQN:分解 Q 值
Section titled “Dueling DQN:分解 Q 值”将 Q 函数分解为状态价值 V(s) 与动作优势 A(s, a) 两部分:
减去均值是为了可识别性(identifiability)——否则 V 和 A 有无穷多种分解方式。当某些动作不影响状态价值时(例如待机状态),网络可以直接学 V(s) 而无需为每个动作重复学习,提升学习效率。
Prioritized Experience Replay(PER)
Section titled “Prioritized Experience Replay(PER)”标准经验回放均匀采样,但不同样本的学习价值不同——TD 误差大的样本携带更多新信息。PER 按 |TD 误差| 的大小决定采样概率:
其中 ε 防止 TD 误差为零的样本永不被采样,α 控制优先程度。同时引入重要性采样权重修正分布偏差,保证无偏性。
Rainbow DQN:六大改进的集成
Section titled “Rainbow DQN:六大改进的集成”Rainbow 将六种独立改进组合到 DQN 中,取得 1DQN 基线之上的显著提升:
- Double DQN(缓解过估计)
- Dueling DQN(值分解)
- Prioritized Experience Replay(优先采样)
- N-step Returns(多步回报,加速信用分配)
- Distributional RL(C51,预测回报分布而非期望)
- Noisy Net(参数空间探索,替代 ε-greedy)
连续动作空间的挑战
Section titled “连续动作空间的挑战”上述算法都针对离散动作空间(max 易计算)。当动作连续时(如机器人关节角度),max over a 不可解析求解,需要新思路:
DDPG(Deep Deterministic Policy Gradient):off-policy 的确定性策略梯度算法。同时维护一个策略网络 μ(s; θ_μ) 输出确定性动作和 Q 网络 Q(s, a; θ_Q)。Q 的更新用 max 替换为策略网络的输出:
策略网络通过最大化 的梯度更新。
加噪声(Ornstein-Uhlenbeck 或高斯)实现探索。
TD3(Twin Delayed DDPG):针对 DDPG 的 Q 值过估计问题,三处改进——(1) 双 Q 网络取较小值(clipped double Q)抑制过估计;(2) 策略网络更新频率低于 Q 网络(delayed update);(3) 目标策略平滑(target policy smoothing),对目标动作加 clipped 噪声使 Q 对动作的估计更平滑。
SAC(Soft Actor-Critic):基于最大熵框架,优化目标改为”回报加熵”:
其中 H 是策略熵,α 控制探索强度(可自动调节)。策略是随机的(stochastic),天然具备探索能力;最大熵项鼓励策略保持多样性,避免过早收敛到次优确定策略。SAC 是目前连续控制中最稳定、样本效率最高的算法之一。
DQN 训练流程
Section titled “DQN 训练流程”算法演进谱系
Section titled “算法演进谱系”用 Gymnasium + PyTorch 实现完整可运行的 DQN
Section titled “用 Gymnasium + PyTorch 实现完整可运行的 DQN”以下是一个完整可运行的 DQN(CartPole-v1 + PyTorch),包含网络定义、经验回放与目标网络,复制即可运行,约 3–5 分钟收敛到接近满分的平均回报:
import randomfrom collections import deque
import gymnasium as gymimport numpy as npimport torchimport torch.nn as nn
class QNet(nn.Module): # 两层 MLP 近似 Q 函数:输入观测,输出每个动作的 Q 值 def __init__(self, obs_dim, n_actions): super().__init__() self.net = nn.Sequential(nn.Linear(obs_dim, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, n_actions)) def forward(self, x): return self.net(x)
# ---------- 环境与超参数 ----------env = gym.make("CartPole-v1") # 倒立摆:4 维观测、2 个离散动作obs_dim = env.observation_space.shape[0]n_actions = env.action_space.ngamma, batch_size = 0.99, 64buffer = deque(maxlen=10000) # 经验回放池
policy_net = QNet(obs_dim, n_actions) # 主网络target_net = QNet(obs_dim, n_actions) # 目标网络(延迟更新)target_net.load_state_dict(policy_net.state_dict())optimizer = torch.optim.Adam(policy_net.parameters(), lr=1e-3)episode_rewards = []
for episode in range(400): obs, _ = env.reset() ep_ret = 0 for t in range(200): # ε-greedy:ε 从 1.0 线性衰减到 0.05 epsilon = max(0.05, 1.0 - episode / 300) if random.random() < epsilon: action = env.action_space.sample() else: with torch.no_grad(): action = policy_net(torch.FloatTensor(obs)).argmax().item() next_obs, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated buffer.append((obs, action, reward, next_obs, done)) # 存经验 obs = next_obs ep_ret += reward if done: break # 经验够一批后开始训练 if len(buffer) >= batch_size: batch = random.sample(buffer, batch_size) s = torch.FloatTensor([x[0] for x in batch]) a = torch.LongTensor([x[1] for x in batch]).unsqueeze(1) r = torch.FloatTensor([x[2] for x in batch]) s_n = torch.FloatTensor([x[3] for x in batch]) d = torch.BoolTensor([x[4] for x in batch]) q = policy_net(s).gather(1, a).squeeze() # 当前 Q 值 with torch.no_grad(): q_next = target_net(s_n).max(1)[0] # 目标网络估值 y = r + gamma * q_next * (~d) # TD 目标 loss = nn.MSELoss()(q, y) optimizer.zero_grad() loss.backward() optimizer.step() # 每回合结束同步目标网络(简单有效;也可改用软更新 τ=0.005) target_net.load_state_dict(policy_net.state_dict()) episode_rewards.append(ep_ret) if (episode + 1) % 50 == 0: avg = np.mean(episode_rewards[-50:]) print(f"回合 {episode + 1}, 最近 50 回合平均回报: {avg:.1f}")
print("训练完成:平均回报接近 200(CartPole 满分)即代表学会了平衡倒立摆")网络结构、优化器与梯度下降的细节参见梯度下降基础和PyTorch 指南。
用 Gymnasium + PyTorch 实现完整可运行的 SAC
Section titled “用 Gymnasium + PyTorch 实现完整可运行的 SAC”SAC 面向连续动作空间(CartPole 是离散动作环境,属于 DQN 的领域),因此这里改用 Gymnasium 的 Pendulum-v1(倒立摆,动作为施加的力矩,取值 -2 到 2)演示。SAC 的关键设计:随机策略 + 熵正则 + 双 Q 网络:
import gymnasium as gymimport numpy as npimport torchimport torch.nn as nnimport torch.nn.functional as Ffrom collections import dequefrom torch.distributions import Normal
class Policy(nn.Module): """高斯策略:输出动作均值与标准差,经 tanh 压缩到 [-1, 1]""" def __init__(self, obs_dim, action_dim): super().__init__() self.fc = nn.Sequential(nn.Linear(obs_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU()) self.mean = nn.Linear(256, action_dim) self.log_std = nn.Linear(256, action_dim)
def forward(self, obs): h = self.fc(obs) return self.mean(h), torch.clamp(self.log_std(h), -20, 2)
def sample(self, obs): mean, log_std = self.forward(obs) std = log_std.exp() z = Normal(mean, std).rsample() # 重参数化采样 action = torch.tanh(z) # 压缩到 [-1, 1] # 修正 tanh 变换的概率密度(雅可比行列式) log_prob = (Normal(mean, std).log_prob(z) - torch.log(1 - action**2 + 1e-6)) return action, log_prob.sum(dim=-1, keepdim=True)
class Critic(nn.Module): """双 Q 网络:输出两个 Q 值,取最小值抑制过估计""" def __init__(self, obs_dim, action_dim): super().__init__() self.q1 = nn.Sequential(nn.Linear(obs_dim + action_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, 1)) self.q2 = nn.Sequential(nn.Linear(obs_dim + action_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, 1))
def forward(self, obs, action): x = torch.cat([obs, action], dim=-1) return self.q1(x), self.q2(x)
# ---------- 环境与超参数 ----------env = gym.make("Pendulum-v1") # 连续动作:对摆施加力矩,取值 [-2, 2]obs_dim = env.observation_space.shape[0]action_dim = env.action_space.shape[0]max_action = float(env.action_space.high[0])
actor = Policy(obs_dim, action_dim)critic = Critic(obs_dim, action_dim)critic_target = Critic(obs_dim, action_dim) # 目标网络critic_target.load_state_dict(critic.state_dict())actor_opt = torch.optim.Adam(actor.parameters(), lr=3e-4)critic_opt = torch.optim.Adam(critic.parameters(), lr=3e-4)buffer = deque(maxlen=100000) # 经验回放池gamma, tau, alpha = 0.99, 0.005, 0.2 # 折扣因子、软更新系数、熵温度batch_size = 256episode_rewards = []
def update(): """采样一批数据,更新 Critic 与 Actor,再软更新目标网络""" idx = np.random.choice(len(buffer), batch_size, replace=False) batch = [buffer[i] for i in idx] s = torch.FloatTensor([b[0] for b in batch]) a = torch.FloatTensor([b[1] for b in batch]) r = torch.FloatTensor([b[2] for b in batch]).unsqueeze(1) s_n = torch.FloatTensor([b[3] for b in batch]) d = torch.FloatTensor([b[4] for b in batch]).unsqueeze(1) # Critic 目标:r + γ(1-d)·(min Q_target - α·log π(·|s')) with torch.no_grad(): a_n, log_p_n = actor.sample(s_n) q1_n, q2_n = critic_target(s_n, a_n) target = r + gamma * (1 - d) * (torch.min(q1_n, q2_n) - alpha * log_p_n) q1, q2 = critic(s, a) critic_loss = F.mse_loss(q1, target) + F.mse_loss(q2, target) critic_opt.zero_grad(); critic_loss.backward(); critic_opt.step() # Actor 更新:最大化 min Q,同时用熵正则 α·log π 鼓励探索 a_new, log_p_new = actor.sample(s) q1_new, q2_new = critic(s, a_new) actor_loss = (alpha * log_p_new - torch.min(q1_new, q2_new)).mean() actor_opt.zero_grad(); actor_loss.backward(); actor_opt.step() # 目标网络软更新:θ⁻ ← τ·θ + (1-τ)·θ⁻ for tp, p in zip(critic_target.parameters(), critic.parameters()): tp.data.copy_(tau * p.data + (1 - tau) * tp.data)
obs, _ = env.reset()ep_ret = 0for step in range(30000): # 1. 采样动作:策略自带随机性,无需额外探索噪声 with torch.no_grad(): action_unscaled, _ = actor.sample(torch.FloatTensor(obs)) action = action_unscaled.numpy() * max_action # 缩放到真实动作范围 next_obs, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated buffer.append((obs, action_unscaled.numpy(), reward, next_obs, done)) # 存未缩放动作 obs = next_obs ep_ret += reward if done: episode_rewards.append(ep_ret) obs, _ = env.reset() ep_ret = 0 # 2. 经验够一批后开始学习 if len(buffer) >= batch_size: update() # 3. 打印进度(Pendulum 回报为负,越接近 0 越好) if (step + 1) % 5000 == 0: avg = np.mean(episode_rewards[-20:]) if episode_rewards else 0.0 print(f"step {step + 1}, 最近 20 回合平均回报: {avg:.1f}")上例中 alpha 固定为 0.2 便于理解;实际工程推荐用自动温度调节(auto alpha,按目标熵动态调整)。SAC 在 MuJoCo 等连续控制 benchmark 上通常优于 DDPG/TD3。
- 离散动作首选 Rainbow/DQN 族,连续动作首选 SAC:SAC 在大多数连续控制 benchmark(MuJoCo)上样本效率和最终性能都优于 DDPG 和 TD3,且无需手调探索噪声,是目前的事实标准。
- 目标网络同步频率要匹配学习率:同步太频繁则失去稳定作用,太稀疏则目标过时;实践中 C 取 1(软更新 τ≈0.005)到几千步(硬更新)不等,需据环境调整。
- 回放缓冲区大小影响稳定性:太小则样本不够多样,太大则旧经验过时;对非平稳环境(如对手策略变化)用较小缓冲区,平稳环境用大缓冲区(百万级)。
- 奖励缩放很重要:SAC 对奖励尺度敏感,建议将奖励缩放到 O(1) 量级;过高奖励会导致 Q 值爆炸和训练发散。
- ε 衰减比固定 ε 好:从 1.0 线性衰减到 0.05,保证前期充分探索后期稳定利用;衰减速度要与回合数匹配。
- Double DQN 是几乎免费的午餐:实现成本极低(只需一行代码改动),几乎总能降低过估计、提升性能,应作为 DQN 的默认配置。
- 连续动作慎用确定性策略(DDPG):DDPG 对超参数极其敏感,探索依赖手动调噪声;TD3 改善了稳定性,SAC 用随机策略+熵正则彻底解决探索问题。
- 熵温度 α 的自适应调节:SAC 的 α 自动调节版本(根据目标熵动态调整)比固定 α 更鲁棒,推荐使用官方实现的 auto_alpha。
- Atari 游戏智能体:DeepMind 2015 年用 DQN 在 49 款 Atari 游戏上达到人类水平,开启了深度 RL 时代;Rainbow 进一步刷新纪录。这是值函数方法最经典的验证场。
- 围棋与棋类(AlphaZero):虽然 AlphaZero 以 MCTS + 自我对练为核心,但其评估函数借鉴了值函数思想,可看作 DQN 思路与搜索的结合。参见自我对弈。
- 机器人连续控制:MuJoCo、Isaac Gym 上的机械臂抓取、四足机器人行走(ANYmal)大量采用 SAC/TD3 训练策略,结合Sim-to-Real 迁移部署到真实硬件。
- 推荐与广告排序:部分工业推荐系统用 off-policy 方法(如 DQN 变体)优化长期用户留存,结合多臂老虎机做短期探索。
- LLM 对齐的雏形:RLHF 的 PPO 阶段继承了值函数/策略梯度框架,DQN 系列的许多技巧(回放、目标网络)影响了后续对齐算法设计。详见RLHF 与 LLM 训练。
典型类库与工具
Section titled “典型类库与工具”| 类库 | 语言 | 说明 |
|---|---|---|
| Stable-Baselines3 | Python | 最流行的 RL 算法库,提供 DQN、DDPG、SAC、TD3、PPO 等开箱即用实现,API 规范 |
| Ray RLlib | Python | 分布式 RL 框架,支持大规模训练,实现 DQN/Rainbow/SAC 等算法,适合工业级部署 |
| CleanRL | Python | 单文件实现的 RL 算法库,每个算法一个文件、可读性极高,适合学习与定制 |
| Tianshou | Python | 国产高性能 RL 库,基于 PyTorch,支持 DQN/DDPG/SAC 等,模块化设计便于研究 |
| Gymnasium | Python | Farama 维护的标准 RL 环境接口(Atari、MuJoCo、Box2D 等),算法库的事实标准环境 |
| Dopamine | Python | Google 出品的 DQN/Rainbow 研究框架,强调可复现性与快速实验 |
| 术语 | 英文 | 解释 |
|---|---|---|
| 离策略 | Off-policy | 行为策略(生成数据)与目标策略(学习的策略)不同的学习范式,可利用他人经验 |
| 在策略 | On-policy | 行为策略与目标策略相同,只能从自身经验学习,样本效率较低 |
| 时序差分 | Temporal Difference (TD) | 用当前估计加上即时奖励来更新价值函数的自举方法 |
| 经验回放 | Experience Replay | 将转移存入缓冲区并随机采样训练,打破数据相关性、提高利用率 |
| 目标网络 | Target Network | 延迟更新的网络,用于稳定地计算 TD 目标,避免正反馈发散 |
| 过估计 | Overestimation Bias | max 操作导致 Q 值系统性高估真实价值的现象 |
| 优势函数 | Advantage Function A(s,a) | 动作价值与状态价值之差 A(s,a) = Q(s,a) - V(s),衡量某动作相对平均的优劣 |
| 确定性策略梯度 | Deterministic Policy Gradient (DPG) | 策略输出确定性动作的梯度定理,是 DDPG/TD3 的理论基础 |
| 最大熵强化学习 | Maximum Entropy RL | 优化目标为”回报加策略熵”,鼓励探索与鲁棒性的 RL 框架,SAC 的理论基础 |
| 软更新 | Soft Update | 目标网络按 τ 比例混合主网络参数(θ⁻ ← τθ + (1-τ)θ⁻),比硬更新更平滑 |
- Mnih et al.,「Human-level control through deep reinforcement learning」(Nature 2015):DQN 原始论文,首次用深度网络+经验回放+目标网络在 Atari 上达到人类水平,深度 RL 的开山之作。
- van Hasselt et al.,「Deep Reinforcement Learning with Double Q-learning」(AAAI 2016):Double DQN 论文,指出 DQN 过估计问题并用双网络分离选动作与估值。
- Wang et al.,「Dueling Network Architectures for Deep Reinforcement Learning」(ICML 2016):Dueling DQN 论文,将 Q 分解为 V+A 提升学习效率。
- Schaul et al.,「Prioritized Experience Replay」(ICLR 2016):PER 论文,按 TD 误差优先采样关键经验。
- Hessel et al.,「Rainbow: Combining Improvements in Deep Reinforcement Learning」(AAAI 2018):Rainbow 论文,集成六种 DQN 改进,展示组合的累积效果。
- Lillicrap et al.,「Continuous Control with Deep Reinforcement Learning」(ICLR 2016):DDPG 论文,将 DPG 扩展到深度网络,解决连续动作空间。
- Fujimoto et al.,「Addressing Function Approximation Error in Actor-Critic Methods」(ICML 2018):TD3 论文,用双 Q 网络和延迟更新解决 DDPG 的过估计。
- Haarnoja et al.,「Soft Actor-Critic: Off-Policy Maximum Entropy Deep RL with a Stochastic Actor」(ICML 2018):SAC 论文,最大熵框架下的 off-policy 随机策略算法,连续控制的事实标准。
- Sutton & Barto,《Reinforcement Learning: An Introduction》第 2 版第 6 章:Q-learning 与 SARSA 的教科书级讲解,含悬崖行走等经典对比实验。