DQN实战登月者:从CartPole到LunarLander的强化学习落地指南

发布时间:2026/10/20 16:16:17
DQN实战登月者:从CartPole到LunarLander的强化学习落地指南 强化学习圈子流传着一句话入门先玩 CartPole进阶看 LunarLander。我这几个月把 DQNDeep Q-Network在登月器降落任务上完整跑了一遍从看论文到调通代码中间踩了无数坑。这篇文章不打算贴一堆理论公式就完事而是把我从环境理解、网络搭建、超参数调整到最终看到模型稳稳落在平台上这个过程里所有值得记录的东西都写出来。项目本身选的是 OpenAI Gym 生态里非常经典的 LunarLander-v2 任务目标很直观控制一架小着陆器平滑降落到指定平台中间。适合刚学完 DQN 原理、准备动手写第一个实战项目但又被各种概念绕晕的人也适合已经跑通过 CartPole想知道怎么把经验迁移到更复杂连续控制任务的读者。我会尽量把每个“为什么这样做”讲清楚而不是只给你一段能跑却不理解的代码。1. 项目概览别小看这架小破船1.1 任务目标与环境表现LunarLander 表面上看只是个二维小游戏式仿真但它内部状态比 CartPole 复杂不少。智能体输出的动作会直接影响推进器和左右转向同时要处理重力、水平速度、着陆角度等多重约束。环境给每个 step 返回的奖励不是简单“碰平台就给 1 分”而是一个连续奖励信号靠近目标区域、机体姿态稳定都会获得正向反馈引擎喷射本身也要消耗燃料换算成惩罚。刚开始训练时智能体很容易一头扎进画面边界外或者直接硬着陆摔碎这都是常见现象不代表代码写错了。我在实践里把一次完整训练定义为 model 平均分从负数爬到 200 分以上并且能稳定维持 100 个 episode 不滑坡。这个标准在资料里常被说成“合格线”实际操作中不同随机种子对收敛速度影响非常大。同一个超参组合换一个种子可能 800 轮就达标也可能跑了 3000 轮还是原地转圈。所以建议新手一开始多试几个 seed不要因为一次跑得不好就急着大改参数。1.2 动作空间、状态空间与奖励拆解首先要搞清楚环境给的接口长什么样。LunarLander 的状态是一个 8 维的 float 向量前两个是着陆器在空间中的 x、y 坐标第三、四个是水平速度和垂直速度第五个是机体的倾斜角度第六个是角速度最后两个是左右两只腿是否同时接触地面的标志位。这个标志位不是数字而是布尔值神经网络输入里把它们当作数值 1 或 0 就行不用额外归一化。动作空间是离散的 4 个值0 表示不点火1 表示启动左向喷射2 表示启动主推进器向下喷3 表示向右喷射。注意这里没有“左右同时喷”或者“油门大小”的概念它已经在接口层把喷射指令抽成了离散选项。奖励设计的核心逻辑可以看作距离着陆区越远每一步扣得越多主引擎工作时有持续的小惩罚模块任意部位以过大速度撞击地面会得到一个较大的负分如果最终能稳稳停在平台中线上环境会额外发放一段正奖励。最大累计得分大约在 150 到 200 之间能稳定打出 250 以上说明训练效果已经很接近人类手动操作水准。2. DQN 训练机制先把“为什么”想透2.1 Q 函数与 Bellman 方程唯一绕不开的公式训练智能体最核心的思路是估计一个 Q 函数也就是“在某种状态下采取某个动作后未来累计奖励的期望”。如果我们真能把这个函数精确算出来每一步直接选 Q 值最大的动作就好。问题是状态空间太大没法用表格存于是我们让神经网络去逼近这个 Q 函数。DQN 这个名字里的 “Deep” 指的就是用深度网络代替表格。网络输入是当前状态输出是 4 个动作各自的 Q 值估计。更新权重的目标来自 Bellman 方程的一个具体变形当前状态动作对的 Q 值应当等于当前即时奖励加上下一个状态最大 Q 值乘以折扣因子 gamma。用公式表示就是Q(s, a) r gamma * max_a Q(s, a)每次采样一批经验把网络当前的 Q 输出作为预测值把上面的 r gamma * max_a Q_target(s, a) 作为目标值用最小均方误差来优化网络参数。这里出现了一个关键细节用来计算目标值的网络跟当前正在训练的网络如果完全共享同一套参数那么目标也会跟着每次梯度更新一起变动训练会非常不稳定。所以 DQN 落地时一般会再维护一份“目标网络”它的参数更新更慢或者不定期从训练网络复制过来。稳定训练的核心不是网络选得多深而是把目标和预测解耦。2.2 经验回放为什么必须打破样本相关性强化学习训练时的数据是自己跟环境交互产生的不是预先给好的静态数据集。这意味着相邻几步的样本高度相关而且数据分布会随着策略变化而不断偏移。如果每采集一步立即做一次梯度下降模型会很容易被最近的状态带偏学到的东西很快忘掉之前的数据。解决这个问题的方式就是经验回放。具体操作是把每次交互得到的“状态、动作、奖励、下一个状态、是否结束”这组数据存进一个固定大小的缓冲区训练时随机抽取一个小批量样本。这样打散了前后状态之间的相关性让每次梯度更新看到的样本尽量来自不同时期减少了训练方差。缓冲区大小一般设几万到几十万太大容易让模型在旧数据上被反复训练而跟不上新策略太小则又回到强相关的老路。我自己的经验是 10 万到 20 万是比较舒服的区间太大其实收益不明显还占内存。另一个训练细节是终端状态处理。如果一个 episode 结束了那么“下一个状态”处不应该再累加任何未来奖励因为后面已经没有后续了。代码里通常把这种情况下的 target 直接设成当前奖励 r而不是 r gamma * max Q。很多新手第一次跑出来的代码看起来逻辑正确但就是训练不稳定绝大多数问题都出在这要么忘了处理 done 标记要么把终止状态继续塞给目标网络去算最大 Q 值导致目标值虚高策略学出一堆冒险行为。2.3 Double DQN 与 Dueling DQN入门阶段到底要不要上很多教程默认把 DDQN、Dueling DQN、优先经验回放排成一整套。我个人看法是第一次做登月器项目先只上标准 DQN 加目标网络把流程跑通再说。等模型能勉强飞但分数不稳定时再考虑扩展。原因很简单标准 DQN 的过估计问题在这个环境里虽然存在但还没有严重到让训练完全崩掉先引入太多模块一旦不收敛你根本分不清是哪个模块出了问题。不过 Double DQN 的改动非常小性价比很高。它做的事情是在计算下一个状态的 Q 值目标时先用当前网络选出最优动作然后把这个动作对应的 Q 值拿到目标网络里查询而不是直接在目标网络中取最大值。这个动作拆解看似绕了一下本质是为了减少因为噪声导致的最大值偏大问题。我在这个项目里大概第三轮重新训练时加了这个改动训练曲线的整体抖动明显小了一些。如果你有信心排查问题建议直接一开始就实现后面也不用返工。3. 完整实操从随机乱飞到我眼里的“着陆大师”3.1 依赖准备与代码结构规划这个项目建议用 Python 3.9 以上版本强化学习环境直接用 Gymnasium 生态里维护较好的接口。需要安装的主要是 gymnasium、numpy 和 torch。代码不建议全部写在一个文件里虽然你能在笔记文档里看到很多单脚本就能搞定的小 demo但现实项目里还是要分文件管理一个文件放网络结构定义一个文件放经验回放缓冲区一个文件放训练循环还有一个文件放模型评估和渲染测试。我实际用的目录结构大概像下面这样lunar_lander_dqn/ ├── agent.py # DQN Agent封装网络与训练逻辑 ├── model.py # 神经网络载体 ├── replay.py # 经验回放 Buffer ├── train.py # 主训练入口 ├── evaluate.py # 渲染测试与保存模型 └── results/ # 模型权重与训练曲线设计这种结构不是为了好看而是为了方便调试。训练跑到一半你发现网络实现有问题只需要改 model.py不用翻遍几百行训练逻辑。缓冲区如果有人写错过一个维度独立模块里单测也更容易排查。新手最容易犯的错误是着急把代码全堆在一起最后出了 bug 只能全文件排查浪费时间。3.2 网络结构、优化器与超参数选择面对一个只有 8 维输入、4 个离散动作的简单任务网络不需要很深。我用的是两层全连接网络第一层从 8 映射到 128中间加 ReLU 激活第二层再从 128 映射到 64最后直接输出 4 个动作的 Q 值。曾经试过把中间层放到 256 或者三层结构收敛速度没有明显提升反而每次更新的计算量更大。这类控制任务本身状态维度低网络容量不是瓶颈足够的探索能力和稳定的目标计算才是关键。超参数方面我最终固定下来的组合是学习率1e-3折扣因子 gamma0.99经验回放容量10万batch size64初始 epsilon1.0最低 epsilon0.01epsilon 衰减步数大约 5 万个时间步从 1.0 线性降到 0.05 左右目标网络同步间隔每 3000 步更新一次每次 episode 最大步数1000这里每个参数都有说法。学习率 1e-3 是从 Adam 优化器的合理起步区间里选的gamma 0.99 意味着模型会比较重视长期回报比较适合需要连续多步调整姿态的降落任务目标网络同步间隔不能太大也不能太小间隔太大目标网络跟当前策略严重脱节间隔太小又失去了稳定目标的意义。3000 步在这个项目里大约对应 4 到 6 个 episode是一个比较均衡的节奏。3.3 训练主循环拆开每一步看它干了什么训练逻辑本身不算复杂但每个环节之间咬合得很紧密。大框架可以概括成这样从环境中拿到初始状态按照 epsilon-greedy 策略选择动作。也就是以 epsilon 概率随机探索否则用网络预测的 Q 值选最优动作执行动作从环境中拿到奖励、下一个状态、done 标志把这条经验存进缓冲区每隔一定步数从缓冲区随机取一个 batch计算预测 Q 值与目标 Q 值之间的均方误差反向传播更新当前网络参数定期把当前网络参数复制到目标网络不断重复同时按预设计划降低 epsilon。核心代码片段形如# 选择动作 if random.random() epsilon: action env.action_space.sample() else: with torch.no_grad(): q_values policy_net(state_tensor) action q_values.argmax().item() # 交互 next_state, reward, terminated, truncated, _ env.step(action) done terminated or truncated # 存储经验 replay_buffer.push(state, action, reward, next_state, done) # 从缓冲区采样更新 batch_state, batch_action, batch_reward, batch_next_state, batch_done replay_buffer.sample(batch_size) # 计算目标值 with torch.no_grad(): max_next_q target_net(batch_next_state).max(dim1, keepdimTrue).values target batch_reward (1 - batch_done.float()) * gamma * max_next_q注意里面我把 terminated 和 truncated 做了或运算统一当作 episode 结束。在 Gymnasium 新接口里两者是有区别的terminated 表示任务真的终止了比如坠毁truncated 表示只是步数超限被强制截断。对于 DQN 的贝尔曼更新这两者都应该当作“没有未来奖励”来处理。逻辑上稍微有点争议但在我跑出来的结果里统一处理的表现更好。训练过程中的数据记录也要用心。我不仅仅记录每个 episode 的总分还记录每个 episode 的平均 Q 值、平均奖励、收到的 done 次数等。这些数据在后续调参时能帮你区分“训练崩了”和“探索不够”这两种问题。一个 episode 分数很低但是 Q 值依然上涨说明网络在拟合一个有偏的估计Q 值往下掉但分数还行说明目标计算可能有问题或者 reward 的 scale 与预期不符。3.4 评估与可视化别只看训练分数训练结束后需要把模型单独拿出来做评估。评估时把 epsilon 强制设成 0也就是完全按照网络预测选择动作跑 100 个 episode 统计平均分、成功率和触地时的最大速度。不能只跑一个 episode 就算完因为环境里有随机初始条件单次的表现波动很大。只有当 100 个 episode 的平均分稳定在 200 以上我才会真正认为这个模型是可靠的。一开始我发现一个很迷惑的现象训练时最后几百个 episode 分数已经很高了可是 eval 模式下却频繁坠毁。后来检查发现是因为训练阶段我用的 epsilon 没有在评估时被关闭模型还在到处随机探索相当于蒙着眼开车。搜索代码里 epsilon 的使用范围就解决了。这个错误听起来很蠢但实际很容易犯尤其当你在代码里把 epsilon 直接写成了一个全局变量训练和评估共用了一份逻辑时。4. 踩坑记录我替你把坑先踩一遍4.1 症状一一开始就疯狂坠毁分数毫无上升如果你在头 500 个 episode 里看到模型每局都在几分钟内撞地分数稳定在 -300 以下先别急着改网络结构。我遇到的第一个坑是 epsilon 衰减太快五万个时间步听起来很多但每局最多 1000 步实际几十个 episode 后 epsilon 就被压到了很低水平。此时模型还没见过足够的失败样本过早进入纯利用阶段自然学不会复杂动作。我的调试思路是画一张 epsilon 与 episode 分数的对照曲线。发现分数曲线从上到下的拐点和 epsilon 下降曲线高度重合时问题基本就锁定在探索不够上。这时候把 epsilon 衰减步数拉长到 10 万到 20 万步会明显看到前期分数还是负数但不再是一直单调狂跌。4.2 症状二分数在中段震荡长期卡在 0 附近模型已经能飞起来却永远无法准确落在平台上这通常是目标值噪声太大。原因之一可能是奖励没有做 scale 处理。LunarLander 的环境奖励本身设计得比较合理范围也不夸张不需要额外归一化但如果你自己在 reward 上乘了一个系数就要小心梯度更新步长被放大损失函数来回震荡。另一个常见原因是 batch size 太小。我试过用 32 时训练曲线抖动非常明显改成 64 之后平滑了很多。直觉解释是batch size 太小意味着每次梯度更新从缓冲区采样的随机性更大相当于在数据样本上多了一层噪声。当然也不能一味调大过大会让更新过于平滑学习变慢。4.3 症状三训练一个多小时曲线还是“心电图”做了以上调整仍然不收敛就要考虑目标网络同步节奏和目标值计算是否真的正确。我在第二次完整重写时发现自己的目标值计算里多了一个维度错误把 batch_reward 重复扩展到了 batch_size 以外的长度。看似只多了一个括号训练时根本不会像普通 bug 一样报错因为形状上恰好能广播但目标值每个样本基本都错位了。建议你在第一次实现时就在关键位置打印张量的 shape从状态、动作、奖励到网络输出的每一步都确认信息。不要省略这一步跳跃维度的错误 99% 都不会直接报错但模型的表现会非常差。排除这种隐蔽 bug 之后再回头看超参数才有意义。另外我强烈推荐记录每一轮 batch 的损失值。如果损失值一直是 NaN 或者跳变到几千万首先怀疑是不是网络输入中包含 NaN。LunarLander 环境本身非常稳定但如果你自己加了状态裁剪或归一化时除以了某个可能为零的变量就会出问题。4.4 训练加速与性能优化技巧DQN 训练在 CPU 上也能跑但 PyTorch 在 GPU 上的矩阵运算要快很多。如果你的机器支持 CUDA记得把网络和 tensor 都显式地 .to(device)否则虽然能用速度会慢上 3 到 5 倍。还有一个非常容易被忽视的点每次 forward 预测 Q 值时如果只有一个状态数据也应该保持维度为 (1, state_dim) 而不是一个一维张量否则某些版本的 PyTorch 在 argmax 或 squeeze 时会出现奇怪偏差。训练效率方面要最大程度避免在 episode 循环里频繁做不必要的 tensor 与 numpy 转换这些转换虽然看起来不影响正确性但累积起来会让一个本来 20 分钟的训练跑到半小时以上。我后来把状态转 tensor 的工作放到了网络输入前统一批量处理就明显感受到了速度提升。5. 进阶扩展从“能飞”走向“飞得好”标准 DQN 训练到稳定之后模型已经能对平台的位置做出比较自然的反应。但它的策略还会带一些无意义的左右抖动尤其在接近地面时引擎点火时机看起来不是特别果断。这时候可以考虑引入优先级经验回放让模型偏向学习那些目标值与预测值偏差更大的样本从而更快地抓住“刚好卡在悬崖边”的那类关键状态。我在项目后期顺手把 Dueling DQN 网络结构也实现了一遍。它的思想是把网络的输出分成状态价值和动作优势两部分最后再组合成 Q 值。这个改动能缓解某些状态下所有动作回报相近时的学习效率问题代码上只需要换一下网络前向传播的写法。实际跑下来Dueling 结构在登月器任务上的收敛速度没有特别大的提升但最终的平均概率稍稳健一些。还有一个比较简单有效的改动是把单步奖励替换成 n 步累积奖励也就是多步 TD 估计这样做可以用更大的视野去估计未来回报训练早期走向成功状态的信号密度会高很多。如果想继续深挖还可以从软更新soft update方案入手。它不像硬更新那样定期一次性复制参数而是每次训练时按一个小比例比如 0.005 把当前网络参数慢慢混入目标网络。我实验时发现软更新能让训练曲线更平滑不过需要额外调 update 系数否则目标网络跟得太快稳定作用会减弱。我在实际跑完这一整套项目后的体会是DQN 看着门槛低但每一步都藏着容易翻车的细节。登月器任务最让人着迷的地方在于你能在几分钟内直观看到自己的策略从“乱飞”变成“会刹车”再变成“精准落地”这种反馈感是读多少论文都体会不到的。希望这篇文章能让你少走一些我走过的弯路安心享受模型第一次稳稳停在平台上那一刻的快乐。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询