简介:面向强化学习初学者与TensorFlow使用者,基于LunarLander登月器环境的强化学习案例提供了一套完整可运行的Python工程,解决从环境搭建、模型训练到效果验证的实践痛点。资源共6个文件,核心为3个Python脚本,分别承担训练、测试与工具函数;另含1个预训练模型权重h5、1段运行效果mp4视频与1份依赖说明txt,压缩包仅121KB,轻量易部署。已有333人学习,适用于希望快速上手DQN等值函数方法、并借助可视化结果理解智能体决策过程的读者。通过该工程可直观观察模型从随机坠毁到平稳着陆的逐步改进,同时依据模型权重与演示视频快速复现实验,是入门强化学习落地实践的理想参考。
1. LunarLander 为什么是强化学习第一个该跑通的环境
很多人第一次跑强化学习,都是从 CartPole 那个倒立摆开始的。两三百行代码,DQN 一挂上去,二十几分钟就能看到 reward 曲线稳定爬升,于是觉得自己已经入门了。直到把同一套代码原封不动甩给 LunarLander,才发现训练了一千局,飞船还在原地摆烂,连起落架都没碰过地。这个落差不是代码 bug,而是环境复杂度跳了一级:CartPole 的状态转移干净利落,LunarLander 落入 noisy 的物理模拟和稀疏的终端奖励里,原本被掩盖的问题全部浮出水面。
本文要拆的正是这个《基于LunarLander登陆器的强化学习案例(含PYTHON工程)》:从 Gym 环境本身的设定讲起,给出一套最小可复现的 Python 工程代码,然后逐步加 Double DQN、Dueling 结构这些改进,最后把训练里的坑挨个数一遍。适合已经学过 DQN 理论、但没在 LunarLander 上真正调通过的人,也适合想确认自己不是只会调包、而是真能理解每个参数为什么这么设的人。读完你能得到一份可以直接跑的训练脚本,以及一套判断“模型到底学没学会”的验证流程。
2. 拆解 LunarLander 环境:状态、动作、奖励与三个容易忽略的设定
2.1 状态空间与动作空间:8 维输入 + 4 个离散动作的设计意图
LunarLander 的观测是一个 8 维向量,很多教程只给一句“8 维状态”,但每个维度的含义直接影响你后面怎么处理数据。我习惯先把它们列成一张表再写代码:
| 维度 | 含义 | 大致范围 |
|---|---|---|
| 0 | 水平位置 x | 屏幕约 [-1.5, 1.5] |
| 1 | 垂直位置 y | 底部为 0,顶部约 1.2 |
| 2 | 水平速度 vx | 无固定边界 |
| 3 | 垂直速度 vy | 无固定边界 |
| 4 | 船体角度 | 弧度,约 [-π, π] |
| 5 | 角速度 | 无固定边界 |
| 6 | 左腿是否触地 | 0 或 1 |
| 7 | 右腿是否触地 | 0 或 1 |
前 6 维是连续量,后 2 维是布尔接触标志。注意到一个问题:x、角度、速度这几个量的尺度差很多,角度是以弧度为单位的,vx 可能到 1.5,vy 可能到 2 以上。网络直接吃原始值不是不能训,但后面我会讲到,做一点轻量归一化可以让 loss 更早稳定。
动作空间是 4 个离散值:0 表示不点火,1 表示左侧推进器点火,2 表示主推进器点火,3 表示右侧推进器点火。很多人会把索引记反,导致飞船永远往反方向飞,排查半天发现是动作映射搞错了。训练时用env.action_space.sample()采样随机动作,要记住这个采样器吃的是环境的随机种子,不是 Python 的 random 种子,后面避坑章节会细说。
2.2 奖励函数:四个数值来源与它们如何逼出“像人”的着陆策略
LunarLander 的奖励机制是整个训练行为的关键。它由四部分组成:
主推进器每点火一步,扣除 0.3;左右侧推进器每点火一步,扣除 0.03。这个差别不是随便定的,侧推进器用来调姿态和平移,耗油的单位功率本来就低,环境用数值告诉你“别乱喷主引擎”。两腿同时触地时给出 +100,机身其余部位触地或坠毁时给出 -100。除此之外,每一步的存活奖励是 0。
这意味着单步 reward 集中在 -0.3 到 0 之间,而 +100 和 -100 只出现在 episode 结尾。DQN 的损失函数靠 bootstrapping 把终端奖励往回传,但终端信号要穿过几十上百步才能影响到前面的 Q 值,这就是 LunarLander 比 CartPole 难训的根源:信号太稀疏。训练中你会经常看到一个典型现象——飞船贴着地面滑行,两个腿死活不同时着地,这不是网络傻了,而是它在“攒”那 +100,哪怕用侧推进器来回蹭也划算,因为单步惩罚才 0.03。这个局部最优是环境设计故意留下的坑,后面会讲怎么用超参对抗它。
2.3 Gym 接口差异与 seed 陷阱:为什么同一份代码换个版本就翻车
从 Gym 0.21 升级到 0.26 之后,reset()和step()的返回值变了。老版本env.reset()只返回观测,新版本返回(obs, info)的元组;老版本env.step(action)返回四项,新版本返回五项,多了一个truncated。网上大量存档代码还是老写法,直接跑新版 Gym 必炸。
seed 的设置方式也变了。老写法是env.seed(42),新版本要求写成env.reset(seed=42),并且env.action_space.seed(42)要单独设。如果你发现同样的超参、同样的代码,两次运行结果差异巨大,先检查这两行,再去怀疑网络初始化。还有一个很阴间的兼容问题:旧版 box2d 物理引擎在 numpy 1.24 之后会因为np.float被移除而直接报错,症状是make("LunarLander-v2")就崩。解法通常是把 numpy 降到 1.23,或者换新版 box2d-py。这类问题不是你的算法写错了,是环境版本在捣乱,排查顺序要放最前面。
3. 搭建可复现的 Python 工程:最小 DQN 代码与训练主循环
3.1 工程目录结构与最小依赖
先定一个干净的工程结构,后面所有改进都在这个基础上加,不额外引入复杂的训练框架。我一般会按功能拆成四个文件,把网络、缓冲区、训练主循环和评测脚本分开,这样换算法时只需要动其中一个文件。
lunar_lander_project/ ├── dqn_model.py # 网络定义,DQN / Double DQN / Dueling ├── replay_buffer.py # 经验回放缓冲区 ├── train.py # 训练主循环 └── evaluate.py # 加载模型、跑评测与视频回放依赖只需要三样:gym、torch、numpy。不需要额外装 Box2D,Gym 会在make("LunarLander-v2")时自动拉起依赖,但要注意我在 2.3 节提到的 numpy 版本兼容问题。如果只是跑通训练,用 CPU 就够了,LunarLander 状态维度低,网络也小,1500 局在普通笔记本上大约二十到四十分钟,取决于你的采样效率。
3.2 经验回放与网络定义:先写对数据结构,再谈训练
经验回放最重要的是把每条 transition 存成统一结构,采样时一次性转成 Tensor。用deque是常见做法,满了自动丢最旧样本,代码简单且内存有上限。这里有一个细节:done标志要转成float,不要直接用 bool 参与后续的 target 计算。虽然 bool 和 float 在乘法里能隐式转换,但显式转成 0.0 / 1.0 可以避免在 later 的 loss 计算里踩到类型不匹配的坑。
import random from collections import deque import torch class ReplayBuffer: def __init__(self, capacity=200_000): self.buffer = deque(maxlen=capacity) def push(self, state, action, reward, next_state, done): self.buffer.append((state, action, reward, next_state, done)) def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) # 注意 state 是 8 维 float,action 是 int,done 转 float states = torch.FloatTensor([t[0] for t in batch]) actions = torch.LongTensor([t[1] for t in batch]) rewards = torch.FloatTensor([t[2] for t in batch]) next_states = torch.FloatTensor([t[3] for t in batch]) dones = torch.FloatTensor([t[4] for t in batch]) return states, actions, rewards, next_states, dones def __len__(self): return len(self.buffer)缓冲区容量 200000 条,对 LunarLander 来说足够了。action 用LongTensor是因为后面要用gather()按索引取 Q 值;如果存成 one-hot 会白白占用内存,没必要。网络定义用三层全连接,隐藏层 64 个神经元就够,LunarLander 输入只有 8 维,网络再大容易过拟合到近期样本上,训出来反而毛刺多。
import torch.nn as nn class DQN(nn.Module): def __init__(self, state_dim=8, action_dim=4, hidden=64): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, action_dim), ) def forward(self, x): return self.net(x)输出层不加激活函数,因为 Q 值本身可以是负数,LunarLander 里主引擎点火就有 -0.3 的即时惩罚,Q 值经常落在负区间。很多人会在最后一层顺手加个 ReLU,直接把负 Q 值截断,训练直接崩,这个错我见人踩过不止一次。
3.3 训练主循环:epsilon 衰减、软更新与梯度裁剪
训练主循环是整篇工程的核心。我把关键参数先列出来,然后逐步解释每一段在干什么。
import gym import torch import torch.optim as optim from dqn_model import DQN from replay_buffer import ReplayBuffer env = gym.make("LunarLander-v2") obs_dim = 8 action_dim = 4 online_net = DQN(obs_dim, action_dim) target_net = DQN(obs_dim, action_dim) target_net.load_state_dict(online_net.state_dict()) optimizer = optim.Adam(online_net.parameters(), lr=1e-3) loss_fn = nn.MSELoss() buffer = ReplayBuffer(capacity=200_000) gamma = 0.99 batch_size = 64 epsilon = 1.0 epsilon_min = 0.01 epsilon_decay = 100_000 # 总步数内线性衰减到 epsilon_min tau = 0.005 # target 网络软更新系数 def select_action(state, epsilon): if random.random() < epsilon: return env.action_space.sample() with torch.no_grad(): q = online_net(torch.FloatTensor(state).unsqueeze(0)) return q.argmax().item() for episode in range(1500): state, _ = env.reset(seed=42) # 新版 Gym 接口 total_reward = 0 while True: action = select_action(state, epsilon) next_state, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated buffer.push(state, action, reward, next_state, done) state = next_state total_reward += reward # epsilon 线性衰减,每步都做 epsilon = max(epsilon_min, epsilon - 1.0 / epsilon_decay) if len(buffer) >= batch_size: s, a, r, ns, d = buffer.sample(batch_size) q_values = online_net(s).gather(1, a.unsqueeze(1)).squeeze(1) with torch.no_grad(): max_next_q = target_net(ns).max(1).values target = r + gamma * max_next_q * (1 - d) loss = loss_fn(q_values, target) optimizer.zero_grad() loss.backward() optimizer.step() # 软更新:target 网络逐步逼近 online 网络 for p, t_p in zip(online_net.parameters(), target_net.parameters()): t_p.data.copy_(tau * p.data + (1 - tau) * t_p.data) if done: break if episode % 50 == 0: print(f"episode {episode}, reward {total_reward:.1f}, epsilon {epsilon:.3f}")这里几个参数的设定逻辑要说明。gamma 取 0.99,因为 LunarLander 一个 episode 通常几十步到两百步,折扣太狠会让终端奖励传不回来,取 0.99 是常规做法。batch_size 取 64,小 batch 引入的噪声有助于探索,太大了 Q 值更新太平滑,早期容易陷在局部最优。epsilon 从 1.0 线性衰减到 0.01,衰减总步数 100000,这是个经验值,大约对应几百局游戏。衰减太快会过早失去探索能力,飞船永远学不会找着陆点;太慢则训练后期策略不稳定。
训练循环里有几个容易被忽视的点。done = terminated or truncated这一行要这样写,LunarLander 本身没有时间限制,但新版 Gym 默认加了 TimeLimit wrapper,truncated 为 True 时说明游戏被截断而非真正结束,不能让网络以为这是终点并据此把 Q 值归零。target 网络用软更新而不是每隔 N 步硬拷贝,是为了让目标 Q 值变化更平滑。硬更新每 1000 步复制一次,loss 曲线会呈现周期性的跳变;软更新每步都做,虽然计算量稍大,但训练过程更稳定,你观察 loss 曲线时不会被那些跳变误导。
第 1500 局的上限是一个参考,不是保证收敛的承诺。实际跑的时候,常见经验是 500 到 1200 局之间出现明显的起飞,reward 曲线开始摸到 200 附近。如果你的代码跑到 1500 局还在负分徘徊,先别急着换算法,按第 5 章的顺序排查。
3.4 中期怎么判断模型在学:loss、reward、epsilon 三条曲线怎么读
训练过程需要同时盯三样东西:loss、episode reward 和 epsilon。loss 降到接近 0 不代表学会了,因为 target 和预测值可能一起塌到一个错误的常量上,这叫“Q 值坍缩”。reward 曲线剧烈震荡是正常的,它受探索策略影响很大,你应该看趋势而不是单局值。epsilon 衰减到 0.01 之后,reward 曲线仍然没有抬头的迹象,大概率是网络结构或奖励处理出了问题,这时候继续训练只会浪费机器时间。
我自己训练时会每 50 局打印一次,同时把online_net.state_dict()存下来,每 200 局存一个检查点。不要只存最后一局,因为训练中期可能出现一个很好的策略,后面又因为采样噪声退化,有检查点可以随时倒回去。LunarLander 的“学会”不是一个稳定的终态,更像是一个会在好和坏之间反复横跳的过程,存档是最便宜的后悔药。
4. 从能跑到能收敛:奖励缩放、Double DQN 与 Dueling 结构
4.1 奖励缩放与状态归一化:先让 loss 的量级正常
第 3 章的代码能跑,但直接训练会有个隐性问题:reward 的量级。LunarLander 的 Q 网络输出的 target 动辄上百(+100 与 -100 的终端奖励),而 MSE loss 对这样的量级非常敏感。一个常见做法是在buffer.push之前把奖励除以一个常数,我习惯除以 10,把终端奖励缩到 ±10,单步惩罚缩到 ±0.03。这不是必须的,但能显著压低 loss 的绝对值,让学习率设置更宽容。
另一个相关操作是状态归一化。前面提过 x、vx、角度这几个量尺度不同,简单的做法是把状态向量的每一维减去均值除以标准差,或者直接按环境已知的边界做 min-max 归一化。LunarLander 的状态边界并不是完全已知的(速度无上限),所以我通常只在训练开始时采样几千条状态算一个粗略的 mean/std,后面固定住不再更新。这样做之后,网络前几层的权重初始化更不容易被某个大尺度维度主导。
4.2 Double DQN 的一行改动:Q 值高估在 LunarLander 上尤其明显
DQN 有一个被反复讨论的缺陷:max 操作会让 Q 值系统性高估。CartPole 上这个现象不明显,因为状态转移简单、奖励稳定,但 LunarLander 的稀疏奖励和随机初始条件会把高估放大。具体表现为:训练中后段 reward 曲线看着不错,但飞船的动作变得很“莽”,经常在快着陆时突然猛踩油门,这是 Q 值对“主引擎点火”这个动作过度乐观了。
Double DQN 的改动只需要在 target 计算那里换一种方式取最大值。原来是直接在 target 网络上取 max,现在改成两步:先用 online 网络选出最优动作,再用 target 网络去算这个动作的价值。
with torch.no_grad(): # 用 online 网络选动作,用 target 网络估值 next_actions = online_net(ns).argmax(1).unsqueeze(1) max_next_q = target_net(ns).gather(1, next_actions).squeeze(1) target = r + gamma * max_next_q * (1 - d)改动只有三行,换来的是高估问题的缓解。LunarLander 上你可以做一个简单对比实验:同一份超参,分别跑标准 DQN 和 Double DQN,观察 800 局之后的 reward 波动幅度,Double DQN 通常更小。这个对比本身就是一个很好的学习案例,能直观理解“高估”到底造成了什么后果。
4.3 Dueling 结构:把状态价值拆出来之后,采样效率为什么更高
Dueling DQN 的思路是把网络的输出拆成两个分支:状态价值 V 和动作优势 A。最后输出 V + A,但为了避免 V 和 A 之间有自由度偏移,要对 A 做中心化处理,通常减去 A 的均值。这个结构在 LunarLander 上很有效,因为很多状态下“现在该不该着陆”这个状态价值本身比“选哪个动作”更重要,拆开之后网络对状态价值的估计更容易稳定。
import torch.nn as nn class DuelingDQN(nn.Module): def __init__(self, state_dim=8, action_dim=4, hidden=64): super().__init__() self.feature = nn.Sequential( nn.Linear(state_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), ) self.value = nn.Linear(hidden, 1) self.advantage = nn.Linear(hidden, action_dim) def forward(self, x): feat = self.feature(x) v = self.value(feat) a = self.advantage(feat) # 中心化,消除 V 与 A 之间不必要的偏移 return v + a - a.mean(dim=-1, keepdim=True)注意a.mean(dim=-1, keepdim=True)保留了维度,这样广播时不会出错。用 Dueling 结构时,同等的训练轮数下,reward 曲线通常比普通 DQN 更早开始抬头。这里有个心理预期要摆正:Dueling 不是计算上的革命,它提升的是采样效率,也就是说同样的样本量下学得更好,但单局训练时间不会变短。
4.4 推荐超参与调参顺序:从一套能用的起点开始
下面这套参数是 LunarLander 上经过很多次验证的起点,不是最优的,但保证能收敛。我每次拿到一个类似的新环境,都先按这个组合跑一遍,再针对性地调。
| 参数 | 推荐值 | 调整方向说明 |
|---|---|---|
| 学习率 lr | 1e-3 | loss 震荡剧烈时降到 3e-4 |
| 折扣因子 gamma | 0.99 | 任务步数更长时调到 0.995 |
| batch_size | 64 | 训练不稳时试 128,但会变慢 |
| 经验池容量 | 200000 | 容量太小会导致样本多样性不足 |
| epsilon 最小值 | 0.01 | 后期需要更多探索时可保持 0.05 |
| epsilon 衰减总步数 | 100000 | 早期不收敛时优先加大这个值 |
| target 软更新 tau | 0.005 | 更新过慢时试 0.01,过快时试 0.002 |
调参顺序上有一条血泪经验:遇到不收敛,先查 epsilon 衰减和 reward 处理,再动学习率,最后才动网络结构。很多人一上来就把网络改成三层 256 维,结果训练时间翻倍,reward 曲线还比以前更抖,原因就是模型容量过大后,低频的 terminal reward 更难通过 bootstrapping 传播到所有参数上。小网络在 LunarLander 上不仅是够用,而且更容易收敛。
5. LunarLander 训练避坑:5 个让策略翻车的常见问题与排查
5.1 现象:loss 在下降,但 episode reward 纹丝不动
这是我见过最多人卡住的地方。loss 曲线一路降到零点几,看起来很健康,但每局 reward 始终在 -100 到 -50 之间徘徊,飞船跟喝醉了酒似的到处乱撞。
原因:loss 下降只说明网络在拟合当前 target,而 target 本身可能一直在低估所有动作的价值。最典型的场景是经验池里早期样本全是随机动作产生的,这些样本的 Q target 天然偏低,网络在反复强化“所有动作都不好”的信念。
解决:先检查done标志是否被正确处理,done对应的 target 必须不包含后继 Q 值,否则终端奖励会被严重稀释。然后检查 epsilon 是不是衰减太慢,导致后期还在大量采样低质量样本污染经验池。还有一个很隐蔽的 bug:buffer.push里 state 存的是 numpy 数组,如果后续代码不小心在原变量上做了 in-place 操作,缓冲区里的数据会被悄悄修改,Q 值就永远学不对。
5.2 现象:训练 1000 局,飞船还在原地打转,从不尝试下降
飞船离地一直悬停,只会左右横移,就是不往下喷主引擎。看起来像是网络没学会用主引擎,其实是探索出了问题。
原因:epsilon 衰减到 0.01 的速度太快,网络在早期就没有足够机会试出“主引擎点火能带来 +100”这个动作。特别是 LunarLander 的初始燃料和位置有随机性,有时候飞船出生在屏幕边缘,随机探索几百步也碰不到一次稳定的下降路径。
解决:把 epsilon_decay 从 100000 提到 200000 或更大,确保前几百局有大量随机动作去试错。另一个辅助手段是给主引擎动作的 Q 值一个小的初始偏置,比如在输出层把索引 2 对应输出初始化为 0.1,这不是理论上的标准做法,但确实是工程上常用的“启动引导”,能让网络早期更愿意触碰主引擎。
5.3 现象:reward 曲线中途突然清零或断崖下跌
前一秒还在稳定上升,某一局之后突然跌回 -150,并且长时间回不来。
原因:一个常见的直接原因是从 checkpoint 恢复训练时,没有把env.reset(seed=...)重设,导致随机种子跳到一个极端初始条件,产出连续多局坏样本,经验池被污染。另一个原因是训练后期 epsilon 压到 0.01 后,策略变得非常确定,一旦遇到没见过的初始条件,Q 值外推能力不足,就会连续犯错。
解决:恢复训练时同时重置环境和随机种子,并清空一遍体验池里外来路不明的早期样本。如果用的是第 3 章的代码,建议把env.reset(seed=42)改成在训练开始时调用一次,而不是每局都设同一个值,否则每局初始条件一模一样,策略严重缺乏泛化能力。
5.4 现象:CPU 训练慢到让人怀疑人生,每局要跑半分钟
LunarLander 状态维度低,网络很小,正常跑 1500 局应该在半小时量级。如果一局要半分钟,说明你的环境在渲染模式里拖慢速度。
原因:gym.make默认不一定开启渲染,但如果代码里残留env.render(),每次step()都要输出一帧画面,训练直接慢一个数量级。另一种情况是你在select_action里每次都用torch.no_grad()新建整个 forward 图,虽然不会计算梯度,但频繁的张量创建和上下文切换也会显著拖慢采样。
解决:训练循环里用env.action_space.sample()或者直接读网络输出,不要调用env.render()。如果确实需要录制视频来观察策略,用gym.wrappers.RecordVideo,它只在指定间隔保存帧,不会逐帧阻塞训练循环。
5.5 现象:换一个环境,同样的代码完全不收敛
LunarLander 上练得好好的,把gym.make换成别的连续控制环境,立马变废物。
原因:这是一个边界问题。LunarLander 的 8 维状态、4 个离散动作、有界奖励,决定了它恰好适合小型 DQN。连续动作空间环境需要策略梯度或 actor-critic 方法,状态维度高到几十上百时需要调网络宽度和容量。把 LunarLander 的经验直接搬过去,属于典型的不考虑任务特性的滥用。
解决:把 LunarLander 当作 DQN 类算法的试验台,而不是万能模板。换环境时重新审视三个东西:动作是离散还是连续、奖励是否稀疏、状态是否需要归一化。这三个判断定下来,再去决定网络结构和超参,而不是让代码先跑起来再说。
6. 用一套验证脚本判断策略真的学会了:回放视频与稳定性指标
训练结束时看着 reward 曲线爬上去,不算真学会,还要过一遍稳定性和行为层面的验证。LunarLander 官方判定“已解决”的标准是连续 100 局平均得分不低于 200,这个标准很严格,单看一局高分很容易被骗。我每次训练完都会写一个独立于训练脚本的评测函数,固定随机种子,批量跑几十局,同时统计平均分和成功率。
import gym import torch def evaluate(policy_net, episodes=30, seed=0, render=False): env = gym.make("LunarLander-v2") rewards = [] successes = 0 for _ in range(episodes): state, _ = env.reset(seed=seed) # 固定 seed,保证可复现 ep_reward = 0 while True: with torch.no_grad(): action = policy_net(torch.FloatTensor(state)).argmax().item() state, reward, terminated, truncated, _ = env.step(action) ep_reward += reward if render: env.render() if terminated or truncated: break rewards.append(ep_reward) if ep_reward >= 200: successes += 1 env.close() avg_reward = sum(rewards) / len(rewards) success_rate = successes / len(rewards) return avg_reward, success_rate # 用法:avg, rate = evaluate(online_net, episodes=30, seed=0)注意评测时不要传seed=0就完事了,多测几组种子,比如 0、1、2,因为 LunarLander 初始位置是随机的,一个种子下的高分可能是运气。我在实际项目里会把评测结果按“平均分 / 成功率 / 最低分”三个数一起看,成功率 60% 以上但最低分是 -300,说明策略偶发性崩盘,还不能投产。
评测之外,录一段回放视频是必要的。我用RecordVideo把评测过程存成 mp4,放慢速度看两遍,重点观察着陆动作有没有人味:正常策略会先从高处平移对准着陆台,然后开主引擎减速,落地前再关引擎轻微调整,让两个腿同时触地。如果飞船每一次都是歪着冲下去靠地面把腿怼上去,哪怕得分过了 200,这个策略也极度脆弱,换个初始条件就会翻车。行为层面的检查是数值指标替代不了的,这一步别省。
最后说句实在话。我自己最早跑 LunarLander 时,为了省时间把 epsilon 衰减到 0.01 的步数从 10 万缩到 5 千,结果训练了三小时,飞船永远在一个地方打转,最后发现是探索不够。后来老老实实把衰减拉长到 10 万步,问题自己消失了。这种经验值论文里不会告诉你,只有亲手调一遍才记得住。LunarLander 不是终点,但它把 DQN 这条线里的核心陷阱都摆在了桌面上,值得你多跑几遍、多改几个参数试试。希望帮到你。
本文还有配套的精品资源,点击获取