资讯动态

强化学习算法代码实战:DQN、PPO、SAC、TD3完整实现与调参指南

发布时间:2026/9/10 2:07:51 来源:尧图企业网站定制
简介面向研究者和工程师的强化学习算法代码集合覆盖了从经典表格型方法到深度强化学习的完整谱系包括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 结构但设计目标不同直接决定了代码里损失函数和更新频率的写法特性PPOSACTD3策略更新方式on-policyoff-policyoff-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, keepdimTrue)[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.2NOISE_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 超参数对照表先调什么、后调什么不同算法对超参数的敏感度差别很大我整理了一张优先级表算法关键超参数典型默认值优先调整位置DQNGAMMA / EPS_DECAY0.99 / 0.995epsilon 衰减速度PPOLAMBDA / clip0.95 / 0.2clip 太大波动太小不更新SACALPHA / 两个 Q 的 lr自动 / 3e-4熵 target 设定TD3POLICY_NOISE / 延迟步数0.2 / 2延迟同步频率训练过程中的评估函数也要固定写法评估时关闭探索噪声def eval_policy(policy, env, episodes10): returns [] for _ in range(episodes): state, _ env.reset() done False total 0.0 while not done: action policy.select_action(state, evalTrue) state, reward, done, _, _ env.step(action) total reward returns.append(total) return sum(returns) / len(returns)评估用确定性策略而不是采样策略否则同一份代码不同机器跑出来的评估分数可能差 20%。调参时先看这个分数是否稳定上升再看是不是需要降低学习率。5. 进阶方向离线强化学习、多智能体与工程化落地5.1 离线 RL 的代码差异以 IQL 为例离线强化学习和在线学习的最大区别是训练数据固定不再有 rollout 步骤。IQLImplicit 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::vectortorch::jit::IValue inputs; inputs.push_back(torch_tensor); at::Tensor action module.forward(inputs).toTensor(); float a action.itemfloat();注意导出前要把网络切到 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 记录五类日志而不是只盯 rewardreward 曲线只能告诉你对不对不能告诉你错在哪。我习惯每个 step 记录五类数据actor loss、critic loss、平均 Q 值、平均 advantage 绝对值、策略熵。如果 entropy 骤降而 Q 值还在涨说明探索已经消失如果 advantage 长期为负说明 value network 训练滞后需要调低学习率或增加价值网络的更新频率。6.4 用确定性环境验证奖励设计如果环境本身是随机的算法是否收敛和奖励设计难分彼此。先用 seed 固定环境奖励函数写成线性组合比如dist_reward - 0.1 * energy跑通后再引入随机性。固定随机种子还能让同一个 bug 在几次运行后复现而不是每次报错位置都不一样。6.5 缩小任务规模做回归测试训练一个完整机械臂任务可能要几小时但代码改动后不需要直接上大任务。把状态维度砍一半、动作周期缩短、奖励改成每一步都给的稠密版本在几分钟内就能拿到反馈。这个小规模的“玩具环境”要单独存为一个脚本每次改动完主代码就跑一遍确保没有破坏既有算法。RL 代码的重构频率比普通机器学习项目高这个回归脚本能省下大量痛苦。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价