简介:面向研究者和工程师的强化学习算法代码集合,覆盖了从经典表格型方法到深度强化学习的完整谱系,包括Qlearning、Sarsa、SarsaLambda、DQN及其变体、策略梯度、AC/A2C、ACER、A3C、SAC、DDPG、TD3、TRPO、PPO、DPPO以及多智能体DDPG等主流算法,可帮助快速理解不同RL思路的代码实现与迁移应用。压缩包共168个文件、约11.19MB,以89个Python脚本作为算法核心实现,配套22个Markdown说明文档用于讲解原理与使用方式,9个YAML配置便于调整超参数,35张PNG与5张JPG展示了训练曲线及结果对比,另含ipynb示例和dat数据文件,目录按算法模块划分,定位清晰。目前已有411人学习下载。整套代码既可作为系统学习强化学习的代码图谱,也可直接用于算法对比实验和二次开发,借助文档和可视化结果能显著降低复现主流RL算法的门槛,适合学生、研究者和工程实践中需要快速搭建实验环境的读者。
1. 为什么需要一本“接地气”的强化学习算法 RL 代码大全
很多开发者第一次接触强化学习,是找一个开源 RL 库、装环境、跑通一个 CartPole 或 Pong,然后觉得自己已经入门了。可一旦换成自己的业务场景——离散动作变连续动作、奖励稀疏、状态维度上千——这些现成代码就开始失灵。原因多半不在环境,而在库里封装得太死,容不得你在 rollout 收集、GAE 计算、目标网络更新这些环节里做取舍。这篇文章不打算再讲一遍“什么是 DQN”的科普,而是按一份主流 RL 算法代码的整理思路,从理论到 PyTorch 实现、从训练循环到调参再到部署,完整走一遍。适合两类人:刚写完第一个 REINFORCE、想系统补全各类算法代码的人,以及手头有任务、需要快速选定并改出一个稳定 baseline 的工程师。
2. 主流强化学习算法的代码家族与选型逻辑
2.1 基于值的方法:从 DQN 到 Rainbow 的代码脉络
基于值的方法是离散动作空间的首选。DQN 的代码结构里,真正决定成败的不是网络本身,而是三个机制:经验回放、目标网络和 epsilon-greedy 探索。
经验回放本质上是一个固定容量的环形队列。训练时随机采样一个小批量,打破了相邻 transition 之间的相关性。目标网络每 C 步同步一次参数,给 TD target 一个缓慢变化的参照。这两个机制在代码里非常容易实现,但也是最容易被顺序写错的地方:先更新主网络再用新参数计算 target,和先同步 target 再算 loss,得到的训练曲线差别很大。
从 DQN 到 Double DQN、Dueling DQN、Rainbow,代码主线始终是“在同一个回归骨干上增加机制”,而不是换一套结构。如果你要给 Atari 类任务搭 baseline,优先看的依然是 DQN 系代码。图像输入任务里,卷积层初始化和 frame stack 怎么堆,也是基于值方法最先碰到的代码问题。
2.2 基于策略梯度的方法:REINFORCE、Actor-Critic 与 A2C
策略梯度方法直接优化带权重回报的损失。REINFORCE 的 PyTorch 实现只有十几行,但回报方差会让学习曲线像心电图。A2C 把收益换成优势函数 A(s,a)=Q(s,a)-V(s),用 Critic 网络预测 baseline,方差立刻降一截。A3C 是异步多进程版本,现在训练环境多数是向量化的,异步收益不再明显,工程代码里很少写。
这里有一个很多人绕不开的概念:rl 中 bc 是什么。BC(行为克隆)属于监督学习,输入状态输出动作,直接拷贝行为策略。它和 Actor-Critic 的区别在于没有 reward 反馈,纯模仿。A2C/PPO 这类 on-policy 算法代码里,如果你发现 loss 在降但回报不涨,先检查是否把优势算成了动作分布的超采样均值,这其实已经滑向了 BC 行为。
2.3 面向连续控制的主流选型:PPO、SAC、TD3 怎么选
连续控制下最常用的三个算法是 PPO、SAC 和 TD3。它们都能写成 actor-critic 结构,但设计目标不同,直接决定了代码里损失函数和更新频率的写法:
| 特性 | PPO | SAC | TD3 |
|---|---|---|---|
| 策略更新方式 | on-policy | off-policy | off-policy |
| 探索方式 | 随机策略采样 | 最大熵 | 目标策略平滑加噪声 |
| 样本效率 | 低-中 | 高 | 中-高 |
| 调参难度 | 低 | 中 | 中 |
| 典型场景 | 游戏、仿真控制 | 机械臂强化学习实战、运动控制 | 对稳定性要求高的连续控制 |
选择代码时,我一般用这样一个启发式规则:需要快速验证任务能不能学出来,用 PPO;样本获取成本高、想尽量压榨数据,用 SAC;环境带噪声且对稳定性要求极高,用 TD3。下面的函数是示意,但它说明了一个关键点:先定网络骨架,再定算法,主流 RL 代码共享同一套神经网络模块,区别只在 loss 和更新节奏。
def choose_backbone(action_type, sample_cost, noise_level): if action_type == "离散": return "DQN" if sample_cost == "低" else "PPO" if action_type == "连续": if noise_level == "高": return "TD3" return "PPO" if sample_cost == "低" else "SAC"选型之后,下一步就是把网络定义、经验池和 rollout 循环这三块公共代码抽出来,算法文件只保留各自的 loss 计算。这样维护多套 RL 代码时,不会出现改了一个文件的网络层、其他算法全部崩掉的情况。
3. 核心算法代码实现:DQN、PPO、SAC、TD3 的 PyTorch 落地
3.1 DQN 代码:经验回放、目标网络与 epsilon-greedy
一个最小 DQN 训练步,核心是采样小批量后计算 TD 误差:
# 从回放缓冲区按 batch 采样 state, action, reward, next_state, done = replay.sample(BATCH_SIZE) # 当前 Q 值:只取实际执行动作对应的值 q_values = online_net(state).gather(1, action) # 目标 Q 值:目标网络取 next_state 的最大 Q with torch.no_grad(): next_q = target_net(next_state).max(1, keepdim=True)[0] td_target = reward + GAMMA * next_q * (1 - done) loss = F.smooth_l1_loss(q_values, td_target) optimizer.zero_grad() loss.backward() optimizer.step() if step % TARGET_SYNC == 0: target_net.load_state_dict(online_net.state_dict())代码逻辑:q_values用 online 网络按实际执行的动作 gather 出来;td_target用 target 网络算下一步的最大 Q,再乘 GAMMA 并加上即时奖励。(1 - done)是终止状态已经把 Q 置零的常用写法。
参数说明:GAMMA 一般取 0.99,环境奖励稀疏可提到 0.995,但 GAMMA 越大,早期梯度越容易把价值估计推高。TARGET_SYNC 常用 1000 或 5000,太频繁会失去缓解自举偏差的作用,太久则训练曲线容易来回震荡。
3.2 PPO 代码:GAE 与 rollout 收集
PPO 有两个真正难写的点:GAE 计算和 clipped loss。GAE 在 rollout 结束后一次性算:
# 从最后一个 transition 往前推 GAE advantages = torch.zeros_like(rewards) last_gae = 0.0 for t in reversed(range(T)): if t == T - 1: next_value = value_net(next_state) else: next_value = values[t + 1] delta = rewards[t] + GAMMA * next_value * (1 - dones[t]) - values[t] last_gae = delta + GAMMA * LAMBDA * (1 - dones[t]) * last_gae advantages[t] = last_gaeGAE 中 LAMBDA 是偏差方差调节器,0.95 是 baseline。想更稳就降一点,想更敏锐就升一点。注意这里的values是同一个网络在 rollout 收集时缓存下来的,不能用最新参数重新算,否则 GAE 会被“未来信息”污染,优势估计失真。
策略更新时 clipped loss 的代码相对固定,主要记住 ratio 要用新旧 log_prob 的差再 exp,而不是直接用 action 概率相除。ppo 的 rollout 长度决定了 GAE 的视野,一般单次 rollout 用 2048 条 transition 起步,任务复杂可以提到 4096。
3.3 SAC 代码:双 Q、熵与自动温度调节
SAC 是 off-policy,代码上比较特别的是双 Q 取最小值和熵项。两个 Q 网络的存在是为了缓解过估计,loss 里取 min 的那一步是关键:
q1 = qf1(state).gather(1, action) q2 = qf2(state).gather(1, action) new_action, log_pi = policy_net(state) min_q_next = torch.min( qf1_target(next_state, new_action), qf2_target(next_state, new_action) ) td_target = reward + GAMMA * (1 - done) * (min_q_next - ALPHA * log_pi)代码里的 ALPHA 是熵权重。SAC 的自动温度调节会把 ALPHA 也当作可训练变量,目标是让平均熵维持在设定的 target 附近。代码里最容易错的是log_pi的符号:它已经被取成负的对数概率,所以是减去ALPHA * log_pi,等价于加一个熵项。如果符号写反,策略会被逼着做确定性动作,SAC 就退化成 DDPG,探索能力直接消失。
3.4 TD3 代码:延迟更新与目标策略平滑
TD3 的经典组合同样是双 Q 加延迟更新,但不用熵项,而是给 target 动作加噪声:
noise = (torch.randn_like(action) * POLICY_NOISE).clamp(-NOISE_CLIP, NOISE_CLIP) smoothed_action = (target_policy(next_state) + noise).clamp(action_low, action_high) target_q = torch.min( target_qf1(next_state, smoothed_action), target_qf2(next_state, smoothed_action) )参数说明:POLICY_NOISE 默认 0.2,NOISE_CLIP 默认 0.5。这个噪声就是目标策略平滑,迫使 Q 函数在动作附近不过于尖锐。TD3 的 Critic 每 2 步更新一次,Actor 和 target 网络也每 2 步同步一次,这就是很多 pytorch 版本 TD3 代码里if update_step % 2 == 0的来源。你从网上找的 td3 代码 pytorch 版本,如果发现训练一直不收敛,先检查 target 网络有没有冻结在奇数步。
4. 从代码到结果:环境搭建、训练循环与调参实战
4.1 最小可复现的环境依赖与入口
写 RL 代码最怕环境不一致。我的习惯是把依赖锁在一个最小集合里:gymnasium、torch、numpy,可视化任务再加 gymnasium[classic-control]。
pip install gymnasium[classic-control] torch numpy安装完先跑一个随机策略冒烟测试,确认env.step()返回五元组(obs, reward, done, truncated, info)。gymnasium 从 0.26 开始把 done 拆分成了 done 和 truncated,很多旧版 RL 代码跑在这里会直接报赋值错误,这是 2024 年后最常碰到的兼容性问题。框架方面,如果你只是学习和改算法,不建议一上来就上 Stable-Baselines3 或 RLlib,等你能手写 DQN 的循环,再去看框架源码会轻松很多。
4.2 训练循环里被低估的四个细节
训练循环看起来简单,但以下四个细节我每次都会被绊到:
- done 和 truncated 必须分开处理。done 表示回合到达终止状态,truncated 表示时间步截断,后者在计算 TD target 时不能把值置零。
- 网络输出必须做 detach。算 target 时要用 no_grad 或 detach,否则梯度图里会一直拖着 target 网络的分支,显存越占越多。
- 梯度裁剪是标配。RL 的 loss 比监督学习不稳定得多,
torch.nn.utils.clip_grad_norm_(policy.parameters(), 0.5)一行代码能把大部分发散问题挡在门外。 - 学习率调度不是必须,但线性衰减比 StepLR 更符合 RL 的训练节奏。
for step in range(total_steps): action = policy.select_action(state) next_state, reward, done, truncated, info = env.step(action) replay.store(state, action, reward, next_state, done or truncated) state = next_state if done or truncated: state, _ = env.reset()这里用done or truncated来决定是否重置环境,但存储进缓冲区时仍然分别记录两个标志。PPO 这类 on-policy 算法里,truncated 状态的处理尤其容易写错:如果一步采样超时也算终止,GAE 会低估后续收益。
4.3 超参数对照表:先调什么、后调什么
不同算法对超参数的敏感度差别很大,我整理了一张优先级表:
| 算法 | 关键超参数 | 典型默认值 | 优先调整位置 |
|---|---|---|---|
| DQN | GAMMA / EPS_DECAY | 0.99 / 0.995 | epsilon 衰减速度 |
| PPO | LAMBDA / clip | 0.95 / 0.2 | clip 太大波动,太小不更新 |
| SAC | ALPHA / 两个 Q 的 lr | 自动 / 3e-4 | 熵 target 设定 |
| TD3 | POLICY_NOISE / 延迟步数 | 0.2 / 2 | 延迟同步频率 |
训练过程中的评估函数也要固定写法,评估时关闭探索噪声:
def eval_policy(policy, env, episodes=10): returns = [] for _ in range(episodes): state, _ = env.reset() done = False total = 0.0 while not done: action = policy.select_action(state, eval=True) state, reward, done, _, _ = env.step(action) total += reward returns.append(total) return sum(returns) / len(returns)评估用确定性策略而不是采样策略,否则同一份代码,不同机器跑出来的评估分数可能差 20%。调参时先看这个分数是否稳定上升,再看是不是需要降低学习率。
5. 进阶方向:离线强化学习、多智能体与工程化落地
5.1 离线 RL 的代码差异:以 IQL 为例
离线强化学习和在线学习的最大区别是训练数据固定,不再有 rollout 步骤。IQL(Implicit Q-Learning)是离线 RL 里相当实用的算法,它的核心思路是不对超出行为策略数据分布的动作做 max,而是用 expectile 回归估计 Q 值。代码上比在线算法还要短:
def iql_loss(q_value, target_q_value, value_net, state, action, log_prob, tau, beta): # expectile 回归:拟合条件期望而不是最大期望 diff = target_q_value - value_net(state) weight = torch.where(diff > 0, tau, 1 - tau) value_loss = (weight * diff ** 2).mean() # 优势加权 BC:越优于行为策略的动作,权重越大 advantage = q_value - value_net(state).detach() policy_loss = (-torch.exp(advantage / beta) * log_prob).mean() return value_loss, policy_loss参数说明:tau 默认 0.7,它控制价值估计偏向高分位数还是均值;beta 默认 3.0,控制策略损失的锐利程度。iql 离线强化学习的代码里,最容易被忽略的是value_net(state).detach(),做策略梯度时不能把梯度传进价值网络。如果你从零开始写离线 RL,我建议先把 IQL 跑通,因为它只需要加载数据集,不用处理环境交互和回放,调试成本低很多。
5.2 多智能体强化学习的代码组织:MAPPO 与 QMIX 的取舍
多智能体强化学习不是简单地把多个智能体的代码复制几份。最常用的 MAPPO 和 QMIX 有一个共同点:actor 用局部观测,critic 用全局观测。代码组织上,网络输入维度就要分成obs_dim和global_obs_dim两套。
MAPPO 是 on-policy 路线,直接把 PPO 的 rollout 从单智能体改成多智能体共享经验,但计算 advantage 时必须按智能体 id 分别计算。QMIX 走 off-policy 路线,它把多个智能体的 Q 值用一个 mixing 网络融合,代码里会多出一个单调性约束层。如果你的任务里智能体数量不超过十个,我的建议是先上 MAPPO,因为它对奖励函数形式不敏感,改造成本最低。
多智能体场景里还有个陷阱:环境是不是部分可观测。如果每个智能体只看得到自己的局部状态,actor 网络第一层就必须是循环神经网络或者带历史堆叠,否则算法再先进也学不出来。
5.3 从 Python 到 C++:部署与再训练 Actor-Critic
训练完的网络要落地,最常见路径是 Python 训练、LibTorch 或 ONNX 推理。使用 C++ 训练强化学习 actor-critic 是更重度的做法,常见于机械臂控制器这类低延迟场景,Python 的 GIL 和动态类型开销在实时控制里是硬伤。我的做法是训练阶段用 PyTorch 完成,导出 TorchScript 后用 LibTorch 做推理:
torch::jit::Module module = torch::jit::load("actor.pt"); std::vector<torch::jit::IValue> inputs; inputs.push_back(torch_tensor); at::Tensor action = module.forward(inputs).toTensor(); float a = action.item<float>();注意导出前要把网络切到 eval 模式,并把输入归一化的均值和方差一起打包进模块。否则 C++ 侧拿到的是未经归一化的原始状态,训练时的分布假设全部失效。C++ 里做再训练的话,建议只更新最后一层全连接层,完整反向传播的工程复杂度会直线上升。
6. 让 RL 代码稳定收敛的 5 个验证与调试技巧
6.1 先用随机策略验证数据通路
训练前先把select_action换成env.action_space.sample(),跑一个完整的训练循环。这一步可以暴露八成以上的低级错误:网络输入维度不对、缓冲区大小写错、reward 类型是 int 导致反向传播报错。随机策略下 loss 和 reward 都应该在一个可预期的范围内,若 loss 直接炸成 NaN,说明网络初始化或者数据归一化有问题。
6.2 观察值和奖励各做一次归一化
RL 对状态尺度极其敏感,状态里同时存在 0.001 和 1000 的特征时,梯度基本被大尺度特征主导。常见做法是把观察值除以一个预计算的 running mean,奖励用 clip 到 [-10, 10]。evaluation 阶段不要用训练时的随机奖励归一化参数,要用固定参数,否则评估分数会被带偏。
6.3 记录五类日志,而不是只盯 reward
reward 曲线只能告诉你对不对,不能告诉你错在哪。我习惯每个 step 记录五类数据:actor loss、critic loss、平均 Q 值、平均 advantage 绝对值、策略熵。如果 entropy 骤降而 Q 值还在涨,说明探索已经消失;如果 advantage 长期为负,说明 value network 训练滞后,需要调低学习率或增加价值网络的更新频率。
6.4 用确定性环境验证奖励设计
如果环境本身是随机的,算法是否收敛和奖励设计难分彼此。先用 seed 固定环境,奖励函数写成线性组合,比如dist_reward - 0.1 * energy,跑通后再引入随机性。固定随机种子还能让同一个 bug 在几次运行后复现,而不是每次报错位置都不一样。
6.5 缩小任务规模做回归测试
训练一个完整机械臂任务可能要几小时,但代码改动后不需要直接上大任务。把状态维度砍一半、动作周期缩短、奖励改成每一步都给的稠密版本,在几分钟内就能拿到反馈。这个小规模的“玩具环境”要单独存为一个脚本,每次改动完主代码就跑一遍,确保没有破坏既有算法。RL 代码的重构频率比普通机器学习项目高,这个回归脚本能省下大量痛苦。
本文还有配套的精品资源,点击获取