8761 字
23 分钟
强化学习入门与实践:Q-learning、Q 网络、DQN 与 PPO
2026-10-08

1. 基本强化学习算法主要包含哪些部分?#

可以分成三个层次:交互要素、数学基础、训练与评估系统。只记住算法名称,无法完成可靠的强化学习实验。

1.1 六个交互要素#

要素含义迷宫例子CartPole 例子
智能体 Agent做决策并学习的主体寻路机器人控制小车的程序
环境 Environment接收动作、返回观测与奖励的系统网格与障碍小车和杆的动力学
状态 State / 观测 Observation决策所需信息 / 实际可见信息当前位置位置、速度、杆角度、角速度
动作 Action智能体能实施的操作上下左右向左或向右施力
奖励 Reward每次交互的数值反馈每步 -1,到终点 +10通常每维持一步 +1
策略 Policy状态到动作或动作分布的映射选择最大 Q 值动作输出左右动作概率

状态与观测并不总相同。摄像头画面可能无法直接体现速度,单帧观测也可能不满足马尔可夫性质;可使用历史帧、状态估计或循环网络。

交互闭环是:

观测 s_t → 根据策略选 a_t → 环境执行 → 得到 r_t、s_{t+1}
↑ ↓
└──────── 学习并更新策略或价值函数 ──────┘

奖励可以每一步产生,也可以非常稀疏;强化学习并不要求只有回合结束才得到奖励。它与监督学习的关键区别是:行动会影响后续数据,通常没有逐状态的正确动作标签,目标是长期回报。

1.2 数学基础:MDP、回报、价值与优势#

马尔可夫决策过程写作 M=(S,A,P,R,γ)\mathcal M=(\mathcal S,\mathcal A,P,R,\gamma)。P(s′∣s,a)P(s'|s,a) 为状态转移,RR 描述奖励,γ∈[0,1]\gamma\in[0,1] 为折扣因子。马尔可夫性质表示:给定充分的当前状态与动作,下一状态分布不再需要额外历史信息。这里的转移同时依赖状态和动作。

采用 rtr_t 表示执行 ata_t 后得到的奖励:

Gt=∑k=0T−t−1γkrt+k,J(π)=Eπ[G0].G_t=\sum_{k=0}^{T-t-1}\gamma^k r_{t+k},\qquad J(\pi)=\mathbb E_\pi[G_0].

有限回合可以取 γ=1\gamma=1;无限时域使用 γ<1\gamma<1 有利于使有界奖励的回报有限。γ\gamma 还改变任务对远期收益的偏好,并非越大越好。

Vπ(s)=Eπ[Gt∣st=s],Qπ(s,a)=Eπ[Gt∣st=s,at=a].V^\pi(s)=\mathbb E_\pi[G_t\mid s_t=s],\qquad Q^\pi(s,a)=\mathbb E_\pi[G_t\mid s_t=s,a_t=a].Aπ(s,a)=Qπ(s,a)−Vπ(s).A^\pi(s,a)=Q^\pi(s,a)-V^\pi(s).

VV 回答“处于这个状态有多好”,QQ 回答“在这里做这个动作有多好”,AA 回答“该动作比该状态下平均决策好多少”。价值不是即时奖励,也不是动作概率。

贝尔曼最优方程把长期价值拆成即时奖励与下一状态价值:

Q∗(s,a)=E[rt+γmax⁡a′Q∗(st+1,a′)∣st=s,at=a].Q^*(s,a)=\mathbb E\left[r_t+\gamma\max_{a'}Q^*(s_{t+1},a')\mid s_t=s,a_t=a\right].

真实终止状态的后续价值取零。一般时间限制导致的采样截断不等于任务终止,需要保留 bootstrap;如果时限本身是任务的一部分,应将剩余时间纳入状态并按有限时域任务建模。Gymnasium 终止与截断说明

1.3 一个可用训练系统的七个环节#

  1. 任务建模:定义状态、动作、奖励、终止条件和真实评价指标。
  2. 表示选择:Q 表、Q 网络、策略网络、价值网络或世界模型。
  3. 数据采集:探索策略、环境交互、并行采样与回合重置。
  4. 数据存储:DQN 的经验回放池或 PPO 的当前轮轨迹缓冲区。
  5. 学习目标:TD 目标、策略梯度、优势估计与相应损失。
  6. 稳定机制:目标网络、裁剪、归一化、梯度控制与 KL 监控。
  7. 独立评估:固定预算、多随机种子、保存配置、检查失败与泛化。

1.4 算法分类的三个独立维度#

分类维度类型含义及例子
学什么Value-based学价值,再提取策略:Q-learning、DQN
学什么Policy-based直接优化策略:REINFORCE
学什么Actor-Critic策略负责动作,价值提供学习信号:PPO、SAC
用谁的数据On-policy主要学习当前采样策略产生的轨迹:标准 PPO
用谁的数据Off-policy学习目标策略可不同于采样策略,常复用历史数据:Q-learning、DQN、SAC
是否显式建模Model-free学习时不显式学习/使用环境转移模型:本文三个算法
是否显式建模Model-based用已知或学习的转移模型规划或训练:Dreamer、TD-MPC2

Off-policy 不等于 Offline RL。DQN 通常边交互边学;Offline RL 则从固定历史数据集学习,需处理未见动作的外推误差。PPO 可以在同一轮数据上更新多次,但标准实现不会无限复用很久以前的轨迹。

2. Q-learning:用一张表学习动作价值#

2.1 原理与公式#

对有限离散状态、离散动作,维护 Q[s,a]Q[s,a]。行为策略通常使用 ϵ\epsilon-greedy:以 ϵ\epsilon 概率随机探索,否则选择当前最大 Q 值动作。

δt=rt+γ(1−dt)max⁡a′Q(st+1,a′)−Q(st,at),\delta_t=r_t+\gamma(1-d_t)\max_{a'}Q(s_{t+1},a')-Q(s_t,a_t),Q(st,at)←Q(st,at)+αδt.Q(s_t,a_t)\leftarrow Q(s_t,a_t)+\alpha\delta_t.

dtd_t 表示真实终止,α\alpha 为更新步长,δt\delta_t 是 TD 误差。这是用一个交互样本近似贝尔曼更新,不需要知道转移概率。

例如旧值 Q(s,a)=2Q(s,a)=2、奖励为 1、下一状态最大 Q 值为 5,取 γ=0.9,α=0.1\gamma=0.9,\alpha=0.1,则目标为 5.55.5,新 Q 值为 2+0.1(5.5−2)=2.352+0.1(5.5-2)=2.35。如果此动作已终止,目标变成即时奖励 1。

Q-learning 的目标使用下一状态最大 Q 值,而不使用下一步实际探索动作,所以属于 Off-policy。SARSA 则使用实际下一动作 at+1a_{t+1} 的 Q(st+1,at+1)Q(s_{t+1},a_{t+1}),是理解 On-policy 的良好对照。

2.2 优点、限制与应用#

它结构简单、可解释、资源需求小,适合小型迷宫、有限状态控制器、教学和少量离散超参数组合的选择。存储量约为 ∣S∣∣A∣|\mathcal S||\mathcal A|;状态维度升高或连续化后,离散网格会迅速膨胀,未访问状态也不能自动泛化。

表格 Q-learning 的经典收敛结论需要有限 MDP、充分访问状态动作、合适的递减学习率等条件。固定学习率、有限训练次数和深度函数逼近不自动继承这些保证。下面的固定学习率示例是教学实验,不是收敛证明。

3. Q 网络与 DQN:模型和算法的区别#

3.1 Q 网络是什么?#

Q 网络用参数 θ\theta 近似动作价值 Qθ(s,a)Q_\theta(s,a)。对于少量离散动作,常输入状态,一次输出所有动作价值:

s∈Rd⟼[Qθ(s,0),…,Qθ(s,∣A∣−1)].s\in\mathbb R^d\longmapsto[Q_\theta(s,0),\dots,Q_\theta(s,|\mathcal A|-1)].

CartPole 中网络输入为 4 个状态量,输出为 2 个 Q 值。图像输入可使用 CNN;向量输入可使用 MLP;部分可观测任务可以增加历史编码。

输出层通常保持线性,不用 softmax:Q 值可以为负,可以大于 1;其尺度由奖励和折扣决定。只有根据 Q 值构造探索分布时,才可能额外使用 softmax。

连续动作也可以建立输入 (s,a)(s,a)、输出标量的 Q 网络,但最大化连续动作需要另外的优化机制或 Actor。标准 DQN 的“枚举所有动作后 argmax”不能直接扩展到任意连续动作。

神经网络提供参数共享与泛化机会,不能保证相邻状态动作相同,也不能保证总是比 Q 表更省样本。网络可能泛化错误,需要独立验证。

3.2 DQN 的完整组成#

DQN = Q 网络 + TD 学习 + 经验回放 + 目标网络 + 探索策略。

在线网络 QθQ_\theta 执行动作、接受梯度更新;目标网络 Qθ−Q_{\theta^-} 较慢更新,提供 TD 目标:

yt=rt+γ(1−dt)max⁡a′Qθ−(st+1,a′).y_t=r_t+\gamma(1-d_t)\max_{a'}Q_{\theta^-}(s_{t+1},a').L(θ)=E(s,a,r,s′,d)∼D[ℓ(Qθ(s,a),y)].L(\theta)=\mathbb E_{(s,a,r,s',d)\sim\mathcal D}\left[\ell(Q_\theta(s,a),y)\right].

ℓ\ell 可以是平方误差或 Huber 损失。目标计算不反向传播;经验池 D\mathcal D 保存转移,随机抽样可降低连续样本相关性并复用交互数据。目标网络定期复制在线网络,或使用软更新 θ−←τθ+(1−τ)θ−\theta^-\leftarrow\tau\theta+(1-\tau)\theta^-。

这些机制改善训练稳定性,但不能保证收敛。奖励尺度、探索不足、函数逼近误差仍可能让训练失败。DQN 原始论文

观察状态 → ε-greedy 动作 → 环境转移 → 保存到经验池
↓ 随机抽样
在线网络 Q(s,a) ← Huber 损失 ← 目标网络构造 TD 目标
↓ 周期性复制
目标网络

3.3 Double DQN 解决什么问题?#

标准 DQN 使用同一组目标估计选出最大动作并评价它,噪声会带来高估倾向。Double DQN 分离选择与评价:

a∗=arg⁡max⁡a′Qθ(s′,a′),y=r+γ(1−d)Qθ−(s′,a∗).a^*=\arg\max_{a'}Q_\theta(s',a'),\quad y=r+\gamma(1-d)Q_{\theta^-}(s',a^*).

它通常是值得尝试的低成本改进,仍不能消除所有估计误差。注意:有两个网络的标准 DQN 不等于 Double DQN;关键在 TD 目标是否分离动作选择与评价。Double DQN 论文

初音未来讲解 DQN 与 Double DQN 的动作选择和价值评价

图 1:标准 DQN 用目标网络计算下一状态的最大动作价值;Double DQN 用在线网络选动作、目标网络评价该动作。两种目标都使用下一状态 s′,d 表示真实终止。

3.4 场景与限制#

适合离散动作游戏、少量离散控制操作、选路或资源分配动作。连续高维状态可以输入网络;连续高维动作则优先考虑 SAC、TD3 或 PPO。推荐、调度等应用需要正确的反馈与环境建模,不能仅因“动作是离散的”就直接套 DQN。巨大动作集合使枚举昂贵,非法动作还需要一致的动作掩码。

4. PPO:直接学习策略,并抑制过大的更新#

4.1 Actor、Critic 与策略梯度#

Actor 输出 πθ(a∣s)\pi_\theta(a|s),Critic 估计 Vϕ(s)V_\phi(s)。离散动作可用分类分布;连续动作可用高斯分布。PPO 不局限于连续动作。

策略梯度的基本形式为:

∇θJ≈E[∇θlog⁡πθ(at∣st)A^t].\nabla_\theta J\approx\mathbb E[\nabla_\theta\log\pi_\theta(a_t|s_t)\hat A_t].

优势为正,提高动作概率;优势为负,降低动作概率。Critic 可降低估计方差,而不是替 Actor 直接决定动作。

4.2 PPO-Clip#

固定一轮用于采样的旧策略,保存动作的旧对数概率。定义比率 ρt\rho_t,避免与奖励符号 rtr_t 混淆:

ρt(θ)=πθ(at∣st)πθold(at∣st)=exp⁡(log⁡πθ−log⁡πθold).\rho_t(\theta)=\frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{old}}(a_t|s_t)} =\exp(\log\pi_\theta-\log\pi_{\theta_{old}}).Lclip(θ)=Et[min⁡(ρtA^t,clip⁡(ρt,1−ϵ,1+ϵ)A^t)].L^{clip}(\theta)=\mathbb E_t\left[\min\left(\rho_t\hat A_t, \operatorname{clip}(\rho_t,1-\epsilon,1+\epsilon)\hat A_t\right)\right].

例如 A^=2,ϵ=0.2,ρ=1.5\hat A=2,\epsilon=0.2,\rho=1.5,两项分别为 3 和 2.4,取 2.4,继续提高此样本的比率不再增加这部分收益。若 A^=−2,ρ=0.5\hat A=-2,\rho=0.5,两项为 -1 和 -1.6,取 -1.6,避免因过度降低坏动作概率而得到过多收益。

裁剪是目标函数中的抑制机制,不把新策略概率硬性限制在区间内,也不严格保证 KL 上界。共享参数与其他样本梯度仍能推动某些动作比率越界,因此还要检查学习率、KL、裁剪比例与评估回报。PPO 的 KL 惩罚版本与 Clip 版本是不同设计,不应理解为完全等效。PPO 原论文

初音未来用正负优势示例解释 PPO 裁剪目标中的 min

图 2:优势为正时抑制过度提高动作概率的收益,优势为负时抑制过度降低动作概率的收益。ρ 是同一状态动作的新旧策略概率比;裁剪作用于目标函数,不是策略概率的硬约束。

最小化形式的联合损失通常为:

Ltotal=−Lclip+cvE[(Vϕ(st)−R^t)2]−cHE[H(πθ(⋅∣st))].L_{total}=-L^{clip}+c_v\mathbb E[(V_\phi(s_t)-\hat R_t)^2]-c_H\mathbb E[H(\pi_\theta(\cdot|s_t))].

价值损失让 Critic 拟合回报目标,熵项鼓励探索;熵系数过大会妨碍策略变得明确。共享网络与分离网络都可使用。

4.3 GAE:兼顾偏差与方差#

δt=rt+γ(1−dt)V(st+1)−V(st),\delta_t=r_t+\gamma(1-d_t)V(s_{t+1})-V(s_t),A^t=δt+γλ(1−bt)A^t+1,R^t=A^t+Vold(st).\hat A_t=\delta_t+\gamma\lambda(1-b_t)\hat A_{t+1},\qquad\hat R_t=\hat A_t+V_{old}(s_t).

dtd_t 是真实终止,btb_t 是回合边界,包括真实终止与时间截断。这样,截断时 TD 目标保留下一状态价值,GAE 却不会递归到 reset 后的新回合。采样块结束时也停止递归,最后一步的 δ\delta 已包含下一状态价值。λ\lambda 越大通常保留更多长期误差信号,方差也可能更大。

4.4 训练流程与适用场景#

先固定采样策略收集一批轨迹,计算优势和价值目标,再对同一批数据做若干轮小批量更新,丢弃该批数据并重新采样。旧 log probability、优势与价值目标在更新期间必须固定,不随梯度更新重新计算。

PPO 适合能够大量并行采样的仿真、机器人控制、游戏和复杂策略训练,离散与连续动作均可处理。代价是采样需求常较大;真实交互昂贵时,应与更能复用数据的算法比较。裁剪增加实用稳定性,但奖励设计错误、过多更新轮数或优势计算错误都可能导致失败。

初音未来讲解 PPO 采样、GAE、固定批次更新与重新采样

图 3:PPO 在固定的当前轮轨迹上多次更新,旧 log_prob 保持不变,之后重新采样。边界信息在代码中分别表示真实终止与回合边界,以正确处理时间截断;优势和价值目标在本轮更新期间也保持固定。

5. 横向比较与算法选型#

维度表格 Q-learningQ 网络DQN / Double DQNPPO-Clip
本质学习算法价值函数模型深度价值学习算法策略优化算法
学习对象Q 表QθQ_\theta,训练方式另定动作价值策略及状态价值
状态空间小规模离散可为连续或高维可为连续或高维可为连续或高维
动作空间基础形式为离散取决于模型与优化方式标准形式为离散离散或连续
数据方式Off-policy本身不决定Off-policy,长期回放标准为 On-policy,当前轮复用
行动方式最大 Q 值 + 探索本身不规定最大 Q 值 + 探索从策略分布采样
稳定机制合适步长与充分访问本身没有回放、目标网络、梯度控制Clip、GAE、KL 监控
主要成本表大小取决于网络回放内存与训练计算大量轨迹采样与多轮更新
典型问题状态爆炸、探索不足价值拟合误差高估、训练震荡高方差、策略漂移
学习价值理解 TD 和探索理解函数逼近理解深度 RL 稳定机制理解 Actor-Critic 与策略梯度

不能脱离任务断言“PPO 比 DQN 好”或“DQN 一定收敛更快”。比较环境交互次数、墙钟时间、最终性能与稳定性会得出不同排序。

任务条件合理起点选型理由
小离散状态与动作Q-learning简单透明,便于建立基线
连续观测、少量离散动作DQN / Double DQN,另加 PPO 基线Q 网络适应连续观测,可复用经验
连续控制、交互较昂贵SAC / TD3 与 PPO 对照Off-policy 复用数据;具体效率需实测
并行仿真充分PPO实现成熟,适合批量轨迹采样
只有固定历史数据专门的 Offline RL避免直接对未覆盖动作进行乐观估计
部分可观测历史编码 / 循环策略先补充决策信息,再选算法

这些是实验起点,不是性能保证。SB3 实验建议

6. 实际案例代码:从零实现与运行#

6.1 依赖、运行与验收#

以下三个程序均为完整脚本,复制相应代码块到同名 .py 文件即可使用。Python 建议使用 3.10 或更高版本。Q-learning 仅用标准库;DQN/PPO 需要 PyTorch、NumPy、Gymnasium。

在已有或自行准备的 Python 环境中运行:

Terminal window
# 仅 DQN/PPO 需要此安装步骤;本文未自动安装这些依赖。
python -m pip install torch numpy "gymnasium[classic-control]"
python -m pip freeze > requirements.lock.txt
python q_learning.py --episodes 3000 --seed 42
python dqn_cartpole.py --steps 100000 --seed 42 --device cpu
python dqn_cartpole.py --steps 100000 --seed 42 --double --output runs/double_dqn
python ppo_cartpole.py --steps 100000 --seed 42 --device cpu

这些命令下载依赖,但脚本本身不会下载模型或数据。默认使用 CPU;指定 CUDA 前先确认所用 PyTorch 构建和显卡支持。种子有助于复现,但跨设备、驱动或库版本仍可能出现差异,不能保证位级一致。

迷宫验收:抵达终点、无非法位置,当前障碍布局的最短路径是 8 步。CartPole-v1 每回合最多 500 步;本文建议的教学验收为独立 20 回合平均回报达到 450,且在多个训练种子下稳定。这个标准是本文的实验标准,100000 步训练预算并不保证达到标准。

先使用 --steps 2048 做深度算法冒烟检查,验证环境交互、更新、存储与评估能执行;不能用此短预算判断算法优劣。Q-learning 与神经网络示例的环境不同,不能直接把它们的回报数值横向比较。

6.2 Q-learning:5×5 迷宫#

该案例沿用参考书的网格寻路主题,环境重新编写为无需 GUI 的教学实现。障碍为不可进入的墙,撞墙留在原地;不是参考书陷阱终止语义的逐行复刻。

"""无第三方依赖的 Q-learning:5×5 迷宫。Python 3.10+。"""
import argparse
import json
import random
from pathlib import Path
SIZE = 5
START, GOAL = (0, 0), (4, 4)
WALLS = {(1, 1), (1, 3), (2, 3), (3, 1)}
MOVES = ((-1, 0), (1, 0), (0, -1), (0, 1)) # 上、下、左、右
def transition(state, action):
"""撞墙留在原地;每步付出成本,鼓励找到短路径。"""
# 用 row * SIZE + col 将二维坐标编码为 Q 表的行号。
row, col = divmod(state, SIZE)
dr, dc = MOVES[action]
nxt = (row + dr, col + dc)
if not (0 <= nxt[0] < SIZE and 0 <= nxt[1] < SIZE) or nxt in WALLS:
nxt = (row, col)
terminated = nxt == GOAL
return nxt[0] * SIZE + nxt[1], 10.0 if terminated else -1.0, terminated
def greedy(values, rng):
# 随机打破并列,避免全零初始化时始终选择第一个动作。
best = max(values)
return rng.choice([i for i, v in enumerate(values) if v == best])
def train(episodes=3000, seed=42):
if episodes <= 0:
raise ValueError("episodes 必须为正整数")
rng = random.Random(seed)
# 每个状态占一行,四列分别对应上下左右动作的长期价值估计。
q = [[0.0] * 4 for _ in range(SIZE * SIZE)]
# alpha 控制 TD 更新步长;gamma 决定未来奖励的折扣程度。
alpha, gamma = 0.1, 0.95
for episode in range(episodes):
state = 0
# 随训练减少随机探索,但保留至少 5% 的探索机会。
epsilon = max(0.05, 1.0 - episode / (episodes * 0.8))
for _ in range(100):
action = rng.randrange(4) if rng.random() < epsilon else greedy(q[state], rng)
nxt, reward, terminated = transition(state, action)
# 真实终止不 bootstrap;100 步是采样保护上限,不属于任务终止。
target = reward + (0.0 if terminated else gamma * max(q[nxt]))
# target - Q(s,a) 是 TD 误差;只更新本次访问的状态动作。
q[state][action] += alpha * (target - q[state][action])
state = nxt
if terminated:
break
return q
def evaluate(q, seed=10042):
"""使用独立随机流,不探索、不更新 Q 表。"""
rng = random.Random(seed)
state, reward_sum = 0, 0.0
route = [START]
for _ in range(100):
state, reward, terminated = transition(state, greedy(q[state], rng))
route.append(divmod(state, SIZE))
reward_sum += reward
if terminated:
return {"success": True, "steps": len(route) - 1,
"return": reward_sum, "route": route}
return {"success": False, "steps": 100, "return": reward_sum, "route": route}
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--episodes", type=int, default=3000)
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--output", type=Path, default=Path("q_table.json"))
args = parser.parse_args()
table = train(args.episodes, args.seed)
# 用独立的随机流打破并列动作,不让评估消耗训练的随机序列。
result = evaluate(table, args.seed + 10000)
args.output.write_text(json.dumps({"seed": args.seed, "q": table,
"evaluation": result}, indent=2), encoding="utf-8")
print(json.dumps(result, ensure_ascii=False))

6.3 DQN 与 Double DQN:CartPole#

4 维连续观测、2 个离散动作,是理解“连续状态不等于连续动作”的案例。程序使用回放池、目标网络、Huber 损失与梯度裁剪;--double 只改变下一状态目标的计算方式。

"""教学版 DQN / Double DQN;状态为连续向量,动作只有左右两个。"""
import argparse
import json
import random
from collections import deque
from pathlib import Path
import gymnasium as gym
import numpy as np
import torch
from torch import nn
class QNetwork(nn.Module):
def __init__(self, obs_dim, actions):
super().__init__()
# 输出是无界 Q 值,不是概率,最后一层不能使用 softmax。
self.layers = nn.Sequential(nn.Linear(obs_dim, 64), nn.ReLU(),
nn.Linear(64, 64), nn.ReLU(),
nn.Linear(64, actions))
def forward(self, states):
return self.layers(states)
def evaluate(net, device, seed, episodes=20):
env = gym.make("CartPole-v1") # 单独的评估环境,避免改变训练状态。
scores = []
net.eval()
with torch.no_grad():
for episode in range(episodes):
obs, _ = env.reset(seed=seed + episode)
score = 0.0
while True:
x = torch.as_tensor(obs, dtype=torch.float32, device=device).unsqueeze(0)
action = net(x).argmax(dim=1).item() # 评估关闭 epsilon 探索。
obs, reward, terminated, truncated, _ = env.step(action)
score += reward
if terminated or truncated:
break
scores.append(score)
env.close()
net.train()
return {"mean": float(np.mean(scores)), "std": float(np.std(scores)), "scores": scores}
def main(args):
if args.steps <= 0:
raise ValueError("steps 必须为正整数")
random.seed(args.seed)
np.random.seed(args.seed)
torch.manual_seed(args.seed)
torch.set_num_threads(1) # 小网络在 CPU 上通常不需要大量线程。
device = torch.device(args.device)
env = gym.make("CartPole-v1")
env.action_space.seed(args.seed)
obs, _ = env.reset(seed=args.seed)
online = QNetwork(4, 2).to(device)
target = QNetwork(4, 2).to(device)
# 两个网络从相同参数出发;只有 online 的参数交给优化器更新。
target.load_state_dict(online.state_dict())
target.eval()
optimizer = torch.optim.Adam(online.parameters(), lr=1e-3)
# 有界经验池自动淘汰最旧转移,限制内存并复用历史交互数据。
replay = deque(maxlen=50000)
batch_size, gamma = 64, 0.99
history = []
episode_return = 0.0
for step in range(1, args.steps + 1):
# 前 20000 个环境步逐渐从随机探索转向利用,之后保持 5% 探索。
epsilon = max(0.05, 1.0 - 0.95 * step / 20000)
if random.random() < epsilon:
action = env.action_space.sample()
else:
with torch.no_grad():
action = online(torch.as_tensor(obs, dtype=torch.float32,
device=device).unsqueeze(0)).argmax(dim=1).item()
next_obs, reward, terminated, truncated, _ = env.step(action)
# 保存 reset 之前的 next_obs。只用 terminated 掩蔽 bootstrap。
replay.append((obs.copy(), action, reward, next_obs.copy(), terminated))
episode_return += reward
obs = next_obs
if terminated or truncated:
history.append(episode_return)
episode_return = 0.0
obs, _ = env.reset() # 不要每回合都用相同 seed 重置。
# 先积累 1000 条经验;之后每四个环境步做一次小批量更新。
if step >= 1000 and step % 4 == 0:
# 随机抽样降低连续转移的相关性;B 表示 batch_size。
states, actions, rewards, next_states, terminals = zip(*random.sample(replay, batch_size))
states = torch.as_tensor(np.array(states), dtype=torch.float32, device=device)
next_states = torch.as_tensor(np.array(next_states), dtype=torch.float32, device=device)
# actions 为 [B,1] 的整数索引,供 gather 选择每条经验的动作列。
actions = torch.as_tensor(actions, dtype=torch.int64, device=device).unsqueeze(1)
rewards = torch.as_tensor(rewards, dtype=torch.float32, device=device)
terminals = torch.as_tensor(terminals, dtype=torch.float32, device=device)
# 网络输出 [B,2];取实际执行动作的 Q 值,得到 [B]。
prediction = online(states).gather(1, actions).squeeze(1)
# TD 目标是固定监督信号,不对下一状态的网络输出反向传播。
with torch.no_grad():
if args.double:
# Double DQN:在线网络选动作,目标网络评价该动作。
next_actions = online(next_states).argmax(dim=1, keepdim=True)
next_q = target(next_states).gather(1, next_actions).squeeze(1)
else:
next_q = target(next_states).max(dim=1).values
td_target = rewards + gamma * (1.0 - terminals) * next_q
# Huber 损失减弱大 TD 误差的影响,比平方误差更不易受异常值支配。
loss = nn.functional.smooth_l1_loss(prediction, td_target)
optimizer.zero_grad()
loss.backward()
# 限制梯度范数,降低单次更新过大的风险;不裁剪 Q 值本身。
nn.utils.clip_grad_norm_(online.parameters(), 10.0)
optimizer.step()
if step % 1000 == 0: # 此处单位为环境步,而不是梯度更新次数。
target.load_state_dict(online.state_dict())
if step % 10000 == 0:
print(f"step={step} epsilon={epsilon:.3f} train_mean={np.mean(history[-20:]):.1f}")
# 最终评估关闭随机探索;训练回报含探索噪声,两者应分别记录。
result = evaluate(online, device, args.seed + 10000)
args.output.mkdir(parents=True, exist_ok=True)
# 这里只保存推理权重;恢复训练还需优化器、经验池及随机状态。
torch.save(online.state_dict(), args.output / "q_network.pt")
(args.output / "metrics.json").write_text(json.dumps({
"algorithm": "Double DQN" if args.double else "DQN", "seed": args.seed,
"env": "CartPole-v1", "steps": args.steps, "device": str(device),
"torch": torch.__version__, "gymnasium": gym.__version__,
"config": {"lr": 1e-3, "gamma": gamma, "batch": batch_size,
"buffer": 50000, "warmup": 1000, "train_freq": 4,
"target_interval": 1000, "epsilon_decay_steps": 20000},
"train_returns": history, "evaluation": result}, indent=2), encoding="utf-8")
env.close()
print(result)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--steps", type=int, default=100000)
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--device", default="cpu")
parser.add_argument("--double", action="store_true")
parser.add_argument("--output", type=Path, default=Path("runs/dqn"))
main(parser.parse_args())

注意 terminated or truncated 决定何时重置环境;TD 目标中只有 terminated 屏蔽下一状态价值。当前代码采用单环境且在 reset 前保存下一观测;使用自动重置的向量环境时必须根据其接口读取真正的 final observation。Gymnasium 环境接口

6.4 PPO-Clip + GAE:同一 CartPole 环境#

为降低环境安装复杂度、便于与 DQN 比较,本例使用 CartPole,而不是参考书的月球着陆主题。程序明确展示旧动作概率、两个 GAE 掩码、优势归一化、裁剪损失、价值损失、熵和 KL 提前停止。

"""从零实现离散动作 PPO-Clip + GAE;便于与 DQN 在同一环境比较。"""
import argparse
import json
from pathlib import Path
import gymnasium as gym
import numpy as np
import torch
from torch import nn
from torch.distributions import Categorical
class ActorCritic(nn.Module):
def __init__(self):
super().__init__()
# Actor 输出两个动作的 logits;Critic 输出一个状态价值,参数分开学习。
self.actor = nn.Sequential(nn.Linear(4, 64), nn.Tanh(),
nn.Linear(64, 64), nn.Tanh(), nn.Linear(64, 2))
self.critic = nn.Sequential(nn.Linear(4, 64), nn.Tanh(),
nn.Linear(64, 64), nn.Tanh(), nn.Linear(64, 1))
def forward(self, states):
# logits 交给 Categorical,内部稳定地计算归一化概率。
return Categorical(logits=self.actor(states)), self.critic(states).squeeze(-1)
def gae(rewards, values, next_values, terminals, boundaries, gamma=0.99, lam=0.95):
"""两个掩码:TD 在真实终止停止;GAE 在终止或截断停止跨回合传播。"""
advantage = np.zeros(len(rewards), dtype=np.float32)
# 从采样块末端反向递推;末步的 delta 已通过 next_values 保留 bootstrap。
carry = 0.0
for t in reversed(range(len(rewards))):
# 真实终止将下一状态价值置零;纯时间截断仍使用其价值。
delta = rewards[t] + gamma * (1.0 - terminals[t]) * next_values[t] - values[t]
# lam 调节多步误差权重;边界掩码防止传播到 reset 后的新回合。
carry = delta + gamma * lam * (1.0 - boundaries[t]) * carry
advantage[t] = carry
# 用未归一化优势加旧价值估计,构造 Critic 的固定回归目标。
return advantage, advantage + np.asarray(values, dtype=np.float32)
def evaluate(model, device, seed, episodes=20):
env = gym.make("CartPole-v1")
scores = []
with torch.no_grad():
for episode in range(episodes):
obs, _ = env.reset(seed=seed + episode)
score = 0.0
while True:
x = torch.as_tensor(obs, dtype=torch.float32, device=device).unsqueeze(0)
distribution, _ = model(x)
# 评估选择最高概率动作;训练时则从分布采样以保留探索。
action = distribution.probs.argmax(dim=1).item()
obs, reward, terminated, truncated, _ = env.step(action)
score += reward
if terminated or truncated:
break
scores.append(score)
env.close()
return {"mean": float(np.mean(scores)), "std": float(np.std(scores)), "scores": scores}
def main(args):
if args.steps <= 0:
raise ValueError("steps 必须为正整数")
np.random.seed(args.seed)
torch.manual_seed(args.seed)
torch.set_num_threads(1)
device = torch.device(args.device)
env = gym.make("CartPole-v1")
obs, _ = env.reset(seed=args.seed)
model = ActorCritic().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=3e-4)
# 一轮最多采集 1024 步,每批约 64 条;同一轮轨迹最多复用 10 遍。
rollout_size, minibatch, epochs = 1024, 64, 10
clip, target_kl = 0.2, 0.02
total_steps, episode_return = 0, 0.0
history, updates = [], []
while total_steps < args.steps:
# 每轮建立新 buffer;更新结束后丢弃,不能充当长期经验回放。
states, actions, old_logs = [], [], []
rewards, values, next_values, terminals, boundaries = [], [], [], [], []
for _ in range(min(rollout_size, args.steps - total_steps)):
x = torch.as_tensor(obs, dtype=torch.float32, device=device).unsqueeze(0)
# 采样阶段不建梯度图;策略在整轮采样期间保持不变。
with torch.no_grad():
distribution, value = model(x)
action = distribution.sample()
log_prob = distribution.log_prob(action)
next_obs, reward, terminated, truncated, _ = env.step(action.item())
with torch.no_grad():
# 必须在 reset 前评估下一状态;超时仍保留下一状态价值。
_, next_value = model(torch.as_tensor(next_obs, dtype=torch.float32,
device=device).unsqueeze(0))
states.append(obs.copy())
actions.append(action.item())
# 保存采样时的旧 log_prob;本轮优化时始终作为固定的比率分母。
old_logs.append(log_prob.item())
rewards.append(reward)
values.append(value.item())
next_values.append(next_value.item())
terminals.append(float(terminated))
boundaries.append(float(terminated or truncated))
total_steps += 1
episode_return += reward
obs = next_obs
if terminated or truncated:
history.append(episode_return)
episode_return = 0.0
obs, _ = env.reset()
adv, returns = gae(rewards, values, next_values, terminals, boundaries)
# 先算价值回归目标,再归一化优势,不能用归一化优势构造 returns。
adv = (adv - adv.mean()) / (adv.std() + 1e-8)
s = torch.as_tensor(np.array(states), dtype=torch.float32, device=device)
a = torch.as_tensor(actions, dtype=torch.int64, device=device)
old_log = torch.as_tensor(old_logs, dtype=torch.float32, device=device)
advantages = torch.as_tensor(adv, device=device)
targets = torch.as_tensor(returns, device=device)
stop = False
# 每遍重新打乱样本顺序;优势、价值目标与旧 log_prob 都不重算。
for _ in range(epochs):
for indices in np.array_split(np.random.permutation(len(states)),
max(1, (len(states) + minibatch - 1) // minibatch)):
idx = torch.as_tensor(indices, dtype=torch.int64, device=device)
distribution, predicted_values = model(s[idx])
# 用对数概率差计算新旧概率比,避免直接相除小概率。
log_ratio = distribution.log_prob(a[idx]) - old_log[idx]
ratio = log_ratio.exp()
with torch.no_grad():
# 当前小批量上估计旧策略到新策略的 KL,用于提前停止。
approximate_kl = ((ratio - 1) - log_ratio).mean().item()
if approximate_kl > 1.5 * target_kl:
stop = True # 辅助保护;clip 本身不能严格保证 KL 上界。
break
unclipped = ratio * advantages[idx]
clipped = ratio.clamp(1 - clip, 1 + clip) * advantages[idx]
# 优化器做最小化;加负号才对应最大化 PPO 的裁剪目标。
policy_loss = -torch.minimum(unclipped, clipped).mean()
value_loss = (predicted_values - targets[idx]).square().mean()
entropy = distribution.entropy().mean()
# 价值误差要最小化;熵项取负,鼓励策略保留适当探索。
loss = policy_loss + 0.5 * value_loss - 0.01 * entropy
optimizer.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), 0.5)
optimizer.step()
# KL 超阈值时同时退出剩余 epochs,然后收集新策略的数据。
if stop:
break
updates.append({"steps": total_steps, "approx_kl": approximate_kl,
"early_stop": stop})
if len(updates) % 10 == 0:
print(f"step={total_steps} train_mean={np.mean(history[-20:]):.1f} kl={approximate_kl:.4f}")
result = evaluate(model, device, args.seed + 10000)
args.output.mkdir(parents=True, exist_ok=True)
# 保存 Actor/Critic 权重与下方实验配置;此文件不含续训所需的优化器状态。
torch.save(model.state_dict(), args.output / "actor_critic.pt")
(args.output / "metrics.json").write_text(json.dumps({
"algorithm": "PPO-Clip", "seed": args.seed, "env": "CartPole-v1",
"steps": total_steps, "device": str(device), "torch": torch.__version__,
"gymnasium": gym.__version__, "config": {"lr": 3e-4, "gamma": 0.99,
"gae_lambda": 0.95, "rollout": rollout_size, "batch": minibatch,
"epochs": epochs, "clip": clip, "target_kl": target_kl,
"vf_coef": 0.5, "ent_coef": 0.01, "max_grad_norm": 0.5},
"train_returns": history, "updates": updates, "evaluation": result},
indent=2), encoding="utf-8")
env.close()
print(result)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--steps", type=int, default=100000)
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--device", default="cpu")
parser.add_argument("--output", type=Path, default=Path("runs/ppo"))
main(parser.parse_args())

这是离散动作教学版。迁移到连续动作时,Actor 应输出高斯分布参数,正确处理动作边界与 log probability;若使用 tanh 压缩动作,需要概率密度的变换修正,不能只替换输出维度。初学者可先使用成熟库的连续动作实现。

6.5 用成熟库建立对照基线#

从零实现便于理解,成熟实现便于排查错误。以下可另存为 sb3_baseline.py,依赖 stable-baselines3;它是额外对照,不是上面手写代码的必要依赖。

import gymnasium as gym
from stable_baselines3 import PPO
from stable_baselines3.common.evaluation import evaluate_policy
# 显式创建训练环境,记录模型和环境版本。
train_env = gym.make("CartPole-v1")
# MlpPolicy 适用于向量观测;下列参数与手写 PPO 的关键配置保持可比较。
model = PPO("MlpPolicy", train_env, seed=42, device="cpu",
learning_rate=3e-4, n_steps=1024, batch_size=64,
n_epochs=10, gamma=0.99, gae_lambda=0.95,
clip_range=0.2, ent_coef=0.01, target_kl=0.02, verbose=1)
# total_timesteps 是环境交互预算;库可能为完成最后一批轨迹而略超预算。
model.learn(total_timesteps=100000)
# 独立环境评估,避免评估改变训练环境的状态。
eval_env = gym.make("CartPole-v1")
eval_env.reset(seed=10042)
# deterministic=True 关闭动作采样;std 是回合间波动,不是训练种子间波动。
mean, std = evaluate_policy(model, eval_env, n_eval_episodes=20,
deterministic=True, warn=False)
print(f"evaluation: {mean:.1f} ± {std:.1f}")
model.save("ppo_cartpole_sb3")
eval_env.close()
train_env.close()

SB3 的 PPO 实现还有额外工程细节,不能认为与教学代码逐项等价;其标准 DQN 实现也没有自动包含 Double DQN、Dueling 或 PER。对照时核实实际实现,而不只看算法名字。SB3 PPO、SB3 DQN

7. 调参优化:先诊断,再搜索#

7.1 共同原则#

先检查环境、奖励和边界处理,再调整学习率与探索,最后扩大网络。一个错误的奖励方向或终止掩码不能通过扩大模型可靠地修复。

记录训练回报、独立评估回报、回合长度、总环境步数、训练时间与失败次数。环境有约束时还应记录碰撞率、违约率或资源消耗;平均奖励不能替代这些指标。

观测尺度差异很大时,可依据训练数据估计均值与方差。评估使用冻结的训练统计,不重新拟合。奖励缩放影响价值目标尺度;奖励裁剪则可能改变任务目标,应说明其后果。

7.2 建议起点与搜索范围#

以下范围是低维教学任务的实验候选,不是所有环境的默认最优值。

算法参数建议候选增大/减小的影响
Q-learningα\alpha0.05、0.1、0.2大时适应快但受噪声影响更大
Q-learningγ\gamma0.9、0.95、0.99大时更看重远期收益,传播更慢
Q-learning探索衰减在预算的 50%–80% 内衰减太快容易锁定较差路径
DQN学习率10−4,3×10−4,10−310^{-4},3\times10^{-4},10^{-3}太大可能 Q 值震荡,太小学习缓慢
DQNbatch32、64、128大批降低梯度噪声,也增加成本
DQN目标更新间隔500、1000、2000 环境步太快目标变化大,太慢目标滞后
DQN回放容量10000、50000、100000太小经验相关,太大可能保留过时经验
DQNϵ\epsilon 下限0.01、0.05、0.1高时探索多,但训练回报可能更低
PPO学习率10−4,3×10−410^{-4},3\times10^{-4}高时可能过快偏离旧策略
PPOclip range0.1、0.2、0.3小时约束更保守,但不保证严格 KL 上界
PPO每轮轨迹量512、1024、2048增大常降低估计噪声,也延迟更新
PPO更新 epochs3、5、10太多易对当前批数据过度更新
PPOGAE λ\lambda0.9、0.95、0.98大时更依赖长期信号,方差可能更高
PPO熵系数0、0.001、0.01太大不易收敛,太小可能过早确定策略

目标更新间隔必须写明单位。PPO 的总 rollout 数通常为 n_steps × n_envs;增加并行环境时应重新检查批大小、更新轮数和总交互预算。DQN 还需记录每环境步对应多少梯度更新。

7.3 常见症状及排查顺序#

症状可能原因最小排查与调整
Q-learning 反复撞墙并列动作偏置、探索不足、奖励不合理随机打破并列,延长探索,检查转移
DQN Q 值持续放大、回报下降目标实现错误、高估、学习率大查终止掩码和 detach,再降学习率/试 Double DQN
DQN 训练均值低、评估却较好epsilon 探索拖低训练成绩同时展示独立无探索评估
PPO 更新后回报骤降KL 大、学习率或 epochs 太大查旧 log probability,降学习率/epochs,监控 KL
PPO 熵很早接近零探索不足或优势异常查优势统计,再适度增加熵系数
Critic 损失高奖励尺度大、目标错或容量不足查 GAE/终止处理,检查目标尺度与解释方差
同 seed 也不同结果库、硬件、并行或 RNG 不同锁版本、记录设备与随机流;不要只记录一个 seed

价值损失数值越小不必然代表策略越好;奖励尺度会影响它。裁剪比例和 KL 是诊断指标,也没有普适最佳值。

7.4 公平的实验与超参数搜索#

先选少量关键参数做随机搜索,所有候选使用相同环境步预算和预先确定的训练种子。用验证种子筛选配置,最终结果使用未参与调参的评估种子与任务实例,避免在测试集上反复挑配置。

例如训练 seed 为 0–4,每个训练模型独立评估 20 回合,展示 5 个模型平均回报的均值与标准差,而不仅展示单个模型回合间标准差。记录最终回报、达到阈值所需步数、曲线面积、训练时间;性能曲线应展示多个训练种子的波动。

Terminal window
foreach ($seed in 0..4) {
python dqn_cartpole.py --seed $seed --steps 100000 --output "runs/dqn_$seed"
python ppo_cartpole.py --seed $seed --steps 100000 --output "runs/ppo_$seed"
}

教学脚本把关键参数固定以便阅读;做搜索时,可将所选参数逐项增加为命令行参数,并写入已有 metrics.json,避免只修改代码而漏记配置。

8. 改进方法:每个改进对应一个问题#

方法针对问题思路与注意事项
Double DQNQ 值高估分离选动作与评价,本文提供开关
Dueling Network很多动作价值相近分离状态价值与动作优势;通常用减优势均值消除不可辨识性
Prioritized Replay均匀采样忽略高 TD 误差样本按优先级采样,用重要性权重缓解分布偏差
多步 TD稀疏奖励传播慢聚合多步奖励;需正确处理终止与截断
Distributional RL仅建模期望忽略回报分布学回报分布;并不自动等于风险安全
Noisy Networks单一随机动作探索效率低在参数中引入可学习噪声,需验证探索效果
Rainbow多种 DQN 缺陷并存集成若干改进,应做消融而不是盲目堆叠
PPO KL 提前停止一轮内策略偏移过大达到 KL 阈值停止当前更新轮,仍需独立评估
学习率调度后期更新太激进随预算衰减学习率;也可能让后期适应变慢
循环策略 / 历史状态部分可观测补足速度、历史等信息;训练需保留序列语义
课程学习 / HER很难遇到成功或稀疏目标奖励逐步提高难度或重标记目标,要求任务结构适配
势函数奖励塑形奖励传播困难使用 γΦ(s′)−Φ(s)\gamma\Phi(s')-\Phi(s),在适用条件下保持最优策略

Rainbow 包括 Double Q、优先回放、Dueling、多步学习、回报分布建模与 Noisy Networks;回报分布建模指 distributional RL。Rainbow 论文

改进应有基线:每次引入一个变化,比较固定预算下多种子的表现。增加辅助奖励可能改变最优行为;尤其不能把“中间指标改善”当成“任务成功率提高”。

9. 应用场景:如何从案例迁移到科研问题#

9.1 迷宫与路径规划#

状态为位置或局部地图,动作为离散移动,奖励综合到达、步数和碰撞。简单已知静态迷宫优先与 BFS/A* 比较:如果没有未知动态或交互学习需求,传统搜索通常更直接。RL 更适合存在未知转移、动态障碍或需要从大量类似任务中学习策略的情况。

9.2 游戏与控制#

CartPole 是基础控制教学任务,不是实际机器人性能的证明。图像游戏可以将 MLP 换成 CNN,并检查帧堆叠与奖励预处理。机器人控制需要处理动作边界、控制频率、噪声、约束与仿真到真实的差异;实物验证前先在仿真中评估失败和约束指标。

9.3 参考书的旅行商问题:RL 调节遗传算法#

参考书第 8.6 节采用 Q-learning 选择遗传算法的交叉/变异参数,不能混同为“直接学习城市访问路线”。这是两层优化:外层 RL 选择参数,内层遗传算法更新种群并求解 TSP。

建模项可操作的定义
状态归一化平均/最佳路径长度、种群多样性、停滞代数、剩余预算
动作离散 (pc,pm)(p_c,p_m) 组合
转移用选定参数执行若干代遗传算法
奖励最佳路径长度下降幅度,必要时扣除计算成本
终止用尽预先给定的评估或代数预算
评价固定相同计算预算下最终路径长度、时间和跨实例表现

最小化路线长度 LL 时,可用 rt=(Lt−1best−Ltbest)/Lrefr_t=(L_{t-1}^{best}-L_t^{best})/L_{ref},Lref>0L_{ref}>0 由训练实例预先确定。若要最大化“适应度”,应明确其是否为 1/L1/L 等定义,不能直接把正的路径长度作为奖励最大化。

加入“剩余预算”可减少有限时域状态缺失;只有平均适应度通常不足以表达种群动态。若改为直接构造城市路线,状态至少包含当前位置和已访问城市集合,并约束非法重复访问,问题维度会显著增加。

公平基线包括固定参数遗传算法、随机参数选择和预设参数调度;统一种群规模、适应度评估次数与城市实例。不能让 RL 方案多运行遗传算法,再把改进全部归因于学习能力。上述内容是对教材案例的建模与实验建议,本教程未复现其完整 TSP 求解器。

10. 当前主流与值得关注的方向#

这里区分长期常用基线与有代表性的研究方向。下面是依据所列官方实现和论文做的应用判断,没有声称已统计全领域部署份额。

领域常用基线 / 代表方向与本文的关系
离散动作控制与游戏DQN 及 Double、Dueling、分布回报等改进;PPODQN 是价值学习基础,任务复杂时考虑增强版
连续控制PPO、SAC、TD3PPO 便于并行采样;SAC/TD3 复用经验
基于世界模型的控制DreamerV3、TD-MPC2学环境模型,在想象轨迹中学习或进行规划
固定历史数据CQL、IQL 等 Offline RL控制分布外动作估值,需数据覆盖分析
多智能体协作MAPPO、QMIX 等单智能体方法需处理非平稳性与协作信息
大模型后训练与推理PPO、GRPO 及相关变体动作为 token/回答,奖励与采样成本特殊
安全、泛化与迁移约束 RL、鲁棒训练、模仿与 RL 结合最终需要任务约束和分布外评估

SAC 把策略熵纳入目标,通常通过双 Q 网络等机制提高连续控制的实用性;TD3 通过双 Critic、延迟策略更新与目标动作平滑抑制函数逼近误差。它们是扩展学习的重要方向,具体优势需在相同交互预算下测试。SAC 论文、TD3 论文

DreamerV3 学习世界模型并在模型想象的轨迹中训练行为;TD-MPC2 利用潜在模型进行连续控制规划。它们是代表性研究路线,模型误差、训练成本和工程复杂度仍需评估,不能理解为已经普遍替代 PPO 或 SAC。DreamerV3 官方仓库、TD-MPC2 论文

CQL 通过保守的价值学习缓解 Offline RL 对分布外动作的乐观估计;IQL 是另一类离线学习方法。固定日志训练时,不能仅把 DQN 的 replay buffer 换成日志文件就认为问题已解决。CQL 论文、IQL 论文

GRPO 利用同一提示下多个回答的组内奖励构造相对优势,典型形式可省去单独的价值 Critic;DeepSeek-R1 是其代表性应用。它仍依赖可靠奖励、有效组内差异和采样预算,并不意味着比 PPO 在所有任务更好。DPO 是从偏好数据直接优化策略的相关路线,其典型训练流程不需要在线 RL 采样循环,不应与标准 PPO 混为一谈。DeepSeek-R1 论文、DPO 论文

11. 推荐学习与复现实验路线#

  1. 运行迷宫,手算一次 TD 更新;将探索关闭,观察是否更容易陷入差路径。
  2. 用 DQN 训练 CartPole,检查输出形状、TD 目标和终止掩码。
  3. 开启 Double DQN,固定预算和种子,比较评估均值与波动。
  4. 运行 PPO,核查旧 log probability 是否固定,以及 GAE 是否跨过回合边界。
  5. 用 SB3 建立对照;若成熟库学会而手写代码学不会,先检查实现再调参。
  6. 根据自己任务的动作空间、数据来源与交互成本,扩展到 SAC、离线学习或世界模型。

12. 参考资料与来源边界#

主要参考:《科研综合训练实践教程》2026.04.30 合稿,第八章。定位采用 PDF 文件页序号:基础与 Q-learning 从 PDF 第 226–238 页附近展开;DQN 第 238–245 页;PPO 第 245–253 页;调参/TSP 第 253–258 页;附件索引第 259 页。主体印刷页码约为 217–250,个别页面显示异常,因此以 PDF 页序号定位更可靠。

本文使用教材的六要素、基础算法路线与案例主题;公式澄清、独立评估、多随机种子、时间截断处理、完整教学代码和现代方法对照为重新组织与补充。教材中的作业、安装提示和文档内指令仅作为参考内容,未作为对本任务的操作授权。

进一步阅读:

13. 本次代码验证状态#

  • 已检查三个完整脚本及额外 SB3 示例的 Python 语法。
  • Q-learning:训练 seed 0–4,每个 3000 回合,独立评估均找到 8 步最短路径,未进入障碍,回报为 3(7 次 -1,终点 +10)。
  • 边界检查:越界移动留在原地、撞墙留在原地、抵达终点、拒绝非正训练回合数。
  • GAE 纯数值检查:真实终止关闭 bootstrap;截断保留下一状态价值但切断跨回合递归;采样块边界正确 bootstrap。
  • 当前验证运行时未安装 PyTorch、Gymnasium、Stable-Baselines3,因此未执行 DQN/PPO/SB3 的环境冒烟与完整训练,不提供未经实测的收敛曲线或成绩。
  • 待具备依赖后运行:python dqn_cartpole.py --steps 2048、python ppo_cartpole.py --steps 2048,再按第 6、7 节执行完整多种子训练。
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

强化学习入门与实践:Q-learning、Q 网络、DQN 与 PPO
https://minikou.cloud/posts/reinforcement_learning/
作者
minikou
发布于
2026-10-08
许可协议
Unlicensed

部分信息可能已经过时

目录