DQN玩愤怒的小鸟:从环境搭建到训练避坑的完整实践指南 简介这是一份基于深度Q网络DQN实现“愤怒的小鸟”智能体的Python/TensorFlow资源包面向对强化学习、游戏AI及图像化环境决策感兴趣的开发者。包内共54个文件包含Python源码、可加载的预训练模型与checkpoint/meta、游戏画面png截图、gif演示动画、ogg/wav音频素材以及PyCharm项目配置压缩包约23.54MB目录将训练日志、图片素材和网络权重分开存放还保留了pyc编译产物方便直接加载继续训练或按模块阅读。已有1646人学习下载。围绕MDP建模、经验回放、双网络结构与ε-贪婪策略等关键机制资源展示了如何把游戏画面转为输入特征并完成从调整角度和力度到击打猪堡的决策训练闭环读者可据此复现DQN训练流程理解目标网络同步、损失计算与探索利用权衡并将同一套方法迁移到其他视觉决策任务。1. 为什么让 DQN 去玩愤怒的小鸟比跑 Atari 更值得一试如果你刚从 CartPole 或者某个 Atari 游戏跑通 DQN会觉得让智能体弹弓打鸟不过是换了个皮。真动手才发现不是这么回事愤怒的小鸟和 Atari 最大的区别在于决策结构。Atari 的每个动作是离散的按键而愤怒的小鸟一次完整的射击要同时决定发射角度和力度且这个决定做完之后接下来几秒的物理演化完全不受智能体控制。这意味着 DQN 面对的是一个半马尔可夫过程——动作是高维连续参数奖励在延迟几秒后才出现中间隔着弹道、碰撞、倒塌这些非平稳物理过程。标题里这个愤怒的小鸟 DQN本质上不是让智能体学会反应而是让它学会预判一次决策的长远后果。这篇文章就把这条链路从头拆到尾环境怎么搭、状态怎么表示、奖励怎么设计、训练怎么稳定、以及五个最容易翻车的坑。适合已经跑通基础 DQN哪怕是在 CartPole 上但想往真实物理游戏上迈一步的人也适合准备做游戏 AI 毕业设计的同学找一条可复现的路径。2. DQN 基础从 Q-Learning 到深度网络的这次关键跨越这一章不重新推导 DQN 的数学只讲清楚三件事为什么表格型 Q-Learning 在愤怒的小鸟上行不通、DQN 做了什么关键改动、以及一个能直接拿走的 PyTorch 实现骨架。代码跑通了后面几章才知道该改哪里。2.1 Q-Learning 的表格困境与 DQN 的解决思路Q-Learning 的核心是维护一张 Q(s, a) 查找表每次迭代更新表格里的一项Q(s, a) ← Q(s, a) α [r γ max_{a} Q(s, a) − Q(s, a)]问题在于CartPole 的状态是 4 维浮点数离散化成几十个格子还能塞进表里愤怒的小鸟如果直接读画面一帧 84×84 的灰度图就有 7000 多个像素每个像素 0 到 255 取值状态空间根本不可能枚举。就算你只取弹弓角度、小鸟位置、猪的位置这几个标量角度和距离的组合也是连续的表格会稀疏到绝大多数格子一辈子没被访问过。DQN 的做法是用一个神经网络 Q(s, a; θ) 去逼近这张表输入是状态输出是每个动作的 Q 值。网络本质上在做泛化见过相似的布局就能对没见过的布局给出差不多的估计。训练目标变成了最小化时序差分误差L(θ) E[(r γ max_{a} Q(s, a; θ⁻) − Q(s, a; θ))²]这里 θ⁻ 是目标网络的参数和 θ 不是同一个这正是下面要展开的稳定化设计。对愤怒的小鸟来说用网络替代表格不仅解决了状态连续的问题还让智能体有可能把高弹道打碎上层结构和低弹道直接推塔这两类策略在隐层特征里共享——桌面和 Atari 里很难看到这种迁移但在物理堆叠游戏里很常见。2.2 经验回放与目标网络DQN 稳定训练的两根支柱DQN 的两大关键改动直接决定了训练能不能收敛。第一个是经验回放Experience Replay。在线学习时相邻 step 的样本高度相关智能体在同一个场景里反复学容易把某个局部策略学到过拟合。经验回放的做法是维护一个固定大小的环形缓冲区把每次转移 (s, a, r, s, done) 存进去训练时随机采样一个小批量。愤怒的小鸟里这个设计尤其重要一次弹射产生一条很长的轨迹如果按顺序学习这一整条轨迹的样本都在同一个物理场景下网络会被这条轨迹带着跑。随机采样则打散了相关性让更新梯度近似独立同分布。第二个是目标网络Target Network。注意 2.1 的损失函数里max_{a} Q(s, a; θ⁻) 用的是目标网络的参数 θ⁻而不是正在更新的 θ。如果不分开每一步更新都在移动目标本身loss 会震荡甚至发散——这是大家常说的训练不稳定的根源之一。常见做法是每隔固定步数把 θ 复制到 θ⁻或者用软更新θ⁻ ← τθ⁻ (1−τ)θ。愤怒的小鸟这类奖励延迟的游戏中目标网络更新的频率对稳定性非常敏感我一般先用硬更新、每 1000 步同步一次起步跑起来再调。2.3 一个可运行的 DQN 核心类骨架下面这个骨架是删掉了游戏交互的纯智能体部分PyTorch 实现可以直接套到任意环境上。重点看网络结构、回放缓冲区、以及每步训练的逻辑。import torch import torch.nn as nn import torch.optim as optim import numpy as np import random from collections import deque class DQN(nn.Module): def __init__(self, state_dim, action_dim, hidden128): 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) class ReplayBuffer: def __init__(self, capacity50000): self.buffer deque(maxlencapacity) def push(self, s, a, r, s_next, done): self.buffer.append((s, a, r, s_next, done)) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) s, a, r, s_next, done zip(*batch) return (np.array(s), np.array(a), np.array(r), np.array(s_next), np.array(done)) def __len__(self): return len(self.buffer) class Agent: def __init__(self, state_dim, action_dim, lr1e-3, gamma0.99, buffer_size50000, batch_size64, target_update1000): self.action_dim action_dim self.gamma gamma self.batch_size batch_size self.target_update target_update self.train_step 0 self.eval_net DQN(state_dim, action_dim) self.target_net DQN(state_dim, action_dim) self.target_net.load_state_dict(self.eval_net.state_dict()) self.optimizer optim.Adam(self.eval_net.parameters(), lrlr) self.buffer ReplayBuffer(buffer_size) def act(self, state, epsilon): if random.random() epsilon: return random.randint(0, self.action_dim - 1) with torch.no_grad(): q self.eval_net(torch.FloatTensor(state).unsqueeze(0)) return q.argmax(dim1).item() def update(self): if len(self.buffer) self.batch_size: return 0.0 s, a, r, s_next, done self.buffer.sample(self.batch_size) s torch.FloatTensor(s) a torch.LongTensor(a).unsqueeze(1) r torch.FloatTensor(r).unsqueeze(1) s_next torch.FloatTensor(s_next) done torch.FloatTensor(done).unsqueeze(1) q_current self.eval_net(s).gather(1, a) with torch.no_grad(): q_next self.target_net(s_next).max(dim1, keepdimTrue).values q_target r self.gamma * q_next * (1 - done) loss nn.MSELoss()(q_current, q_target) self.optimizer.zero_grad() loss.backward() self.optimizer.step() self.train_step 1 if self.train_step % self.target_update 0: self.target_net.load_state_dict(self.eval_net.state_dict()) return loss.item()逻辑说明act方法实现 ε-greedy 策略以 ε 概率随机探索其余时间取 eval_net 的输出最大值。update方法从回放缓冲区采样一个小批量计算当前 Q 值和目标 Q 值之间的 MSE 损失。注意目标值计算用torch.no_grad()包住确保梯度只回传到 eval_net。done标志乘在 Q_next 上终止状态不再累加未来奖励。参数说明gamma是折扣因子愤怒的小鸟建议设 0.95 到 0.99太低会导致智能体只看眼前几步的伤害、放弃布局倒塌后的连锁收益target_update控制目标网络同步频率设大了训练慢设小了容易震荡1000 是个安全起步值buffer_size至少 5 万小鸟场景单次弹射轨迹长缓冲区太小会把早期经验挤出去。这段骨架不包含状态预处理和奖励计算那部分要看环境怎么定义下一章展开。3. 搭建愤怒的小鸟游戏环境状态、动作与奖励设计环境是 DQN 训练的地基。愤怒的小鸟没有现成的 OpenAI Gym 标准接口可用社区里有第三方包装但版本参差不齐我一般直接自己封装一个。这一章讲清楚三个决定性问题用什么环境、状态怎么给、奖励怎么定。3.1 用模拟环境替代原版游戏的三种常见做法第一种做法是玩原版游戏、截屏读像素。理论上最原汁原味实际上最坑你需要处理窗口坐标、渲染延迟、不同分辨率的适配而且原版物理引擎是个黑匣子每次加载布局的随机性也大。除非你要做纯视觉策略的研究课题否则我不推荐第一条路。第二种做法是自建一个简化物理模拟器用 PyGame 或者纯数学计算模拟弹弓、抛物线、碰撞和倒塌。常见做法是把小猪和木块简化成圆形或矩形碰撞体小鸟发射出去后按重力加速度运动碰到物体就扣血或击飞。这个方案可控性最好你能拿到每个物体的精确位置和速度奖励计算也方便。缺点是要自己写碰撞检测物理表现不像原版那么丰富。第三种做法是用社区现成的愤怒的小鸟模拟环境比如基于 PyGame 的简化克隆版或者学术开源平台。这些环境封装了 Gym 风格的接口但质量参差不齐有的动作空间定义很怪有的奖励设计不适合 DQN。我的建议是如果你要做毕设或工程实验选第二种自己封装如果只是想快速验证 DQN 代码有没有 bug选第三种拿来就跑。下面以第二种自建环境为例因为它的每个接口你都清楚出了玄学问题能自己查。3.2 状态表示怎么把画面变成 DQN 能吃的输入DQN 的输入是状态向量。愤怒的小鸟有三种层次的状态表示第一种是屏幕像素。把画面缩放成 84×84 灰度图堆叠最近 4 帧作为输入像 Atari 那样。但这里有个问题小鸟从发射到撞击往往持续几十帧而真正决定结果的是发射那一瞬间的角度和力度。堆叠帧对捕捉运动有用对决定下一次发射帮助有限而且灰度图丢失了物体类型信息——木块、冰块、猪在灰度下可能长得一样。第二种是物体级别的向量表示。把场景里每个物体的位置、类型、剩余血量、是否存活以及弹弓位置、小鸟角度和力度拼成一个固定长度的向量。比如 5 个物体每个用一个 4 元组表示加上弹弓状态 2 个值状态维度就是 5×4222。这种表示的好处是直接喂给 MLP 就行不需要卷积网络训练效率高坏处是当你换关卡布局时物体数量变了向量长度就变了需要填零补齐。第三种是迷你地图式的栅格表示把场景划分成网格每个格子填上该区域有没有物体、是什么类型的编码。这种介于像素和向量之间保留了空间结构又压缩了维度。我比较推荐第二种起步先用最少的状态维度把 DQN 跑通确认算法没问题后再升级成栅格表示。下面给出一个状态构造的代码片段class BirdState: def __init__(self, max_objects8, grid_size8): self.max_objects max_objects self.grid_size grid_size def build_vector(self, objects, slingshot_state): # objects: list of (x, y, type_id, hp, alive) # slingshot_state: (angle, power) in normalized [0, 1] vec [] for obj in objects[:self.max_objects]: vec.extend(obj) # 每个物体贡献 5 个值 while len(vec) self.max_objects * 5: vec.extend([0.0] * 5) # 物体不够则补零 vec.extend(slingshot_state) # 弹弓状态 2 个值 return np.array(vec, dtypenp.float32) def build_grid(self, objects, img_w, img_h): grid np.zeros((self.grid_size, self.grid_size, 3), dtypenp.float32) for x, y, type_id, hp, alive in objects: if not alive: continue gx int(x / img_w * self.grid_size) gy int(y / img_h * self.grid_size) gx min(gx, self.grid_size - 1) gy min(gy, self.grid_size - 1) grid[gy, gx, type_id] 1.0 # one-hot 类型编码 return grid逻辑说明build_vector把场景里的物体属性拼成一维向量物体不够时补零保证状态维度恒定。build_grid把场景投影到 8×8 的栅格上每个格子的三个通道分别对应木块、冰块、猪这三类物体命中就置 1。这个 one-hot 编码保留了物体类型的空间分布又比像素表示紧凑得多。参数说明max_objects8是场景里最多物体数超过的部分直接截断实战里 8 个对简化关卡够用grid_size8决定栅格分辨率太细会让状态维度爆炸太粗丢空间细节。两个函数返回的数据类型都强制转成 float32避免传入 PyTorch 时出现类型不匹配的报错。3.3 动作空间与奖励塑形让智能体一开始就知道往哪飞动作空间是 DQN 落地时最容易拍脑袋的地方。愤怒的小鸟的原始动作是两个连续参数角度和力度。DQN 输出的是离散动作所以常规做法是把角度和力度分别离散化角度分 10 档比如 20° 到 70°力度分 5 档比如 30% 到 100%组合成 50 个离散动作。50 个动作的 Q 值输出对 MLP 来说完全可行但要注意动作数越多探索效率越低——随机试 50 个动作找到一次有效射击的概率比试 5 个动作小得多。更聪明的做法是先固定几个经验角度比如 30°、45°、60°每个配 3 档力度总共 9 个动作。这不是偷懒而是符合人类玩这个游戏的策略——没人会去精准试 37.2°。动作空间的合理范围决定了探索效率我见过有人一上来就 100 个动作训练三天毫无起色缩小到 12 个动作以后半天就出效果。奖励设计是第二个关键点。只给是否打死猪的稀疏奖励DQN 学起来极慢因为中间过程没有任何梯度信号。常见做法是拆成多层奖励def compute_reward(info): r 0.0 # 1. 对猪造成伤害直接给正奖励 damage info[pig_hp_before] - info[pig_hp_after] r damage * 5.0 # 2. 造成任何物体位移或摧毁给少量正奖励探索信号 if info[objects_destroyed] 0: r 1.0 * info[objects_destroyed] # 3. 每发小鸟没碰到任何东西给负奖励 if info[shot_total_miss]: r - 2.0 # 4. 整关结束时按剩余小猪数量给最终奖励 if info[episode_end]: r 10.0 * info[pigs_killed_total] r - 0.5 * info[pigs_remaining] # 没打死的猪扣分 return r逻辑说明第一项是主要学习信号打掉猪的血量直接给正奖励这一步能引导智能体把动作和命中目标关联起来。第二项是辅助探索信号任何物体被摧毁都给奖励哪怕打的是木块因为木块倒塌可能砸到猪——这正是愤怒的小鸟的策略精髓。第三项惩罚打空枪。第四项在整关结束时按击杀数给一个较大的最终奖励让智能体学会追求通关而不只是单发伤害。参数说明damage * 5.0里的 5.0 是奖励缩放系数。奖励绝对值过大会让 Q 值网络输出不稳定过小会被探索噪声淹没。我的经验是让单发命中的奖励落在 0.5 到 2.0 区间观察几轮训练后 reward 的平均值再微调系数。shot_total_miss是一个布尔标志需要在仿真器里检测小鸟发射后有没有和任何物体发生碰撞取巧的做法是看小鸟速度有没有显著下降。奖励塑形是一把双刃剑加太多辅助信号智能体可能学会为了拿摧毁奖励而乱打却不会去瞄准猪。我见过一个训练结果智能体学会只打最近的一块木块因为那是每发都能稳定拿 1 分的最短路径。解决办法是把摧毁奖励的系数调低或者只在摧毁的物体是结构性关键方块比如支撑塔最底层的木块时才给奖励。这个尺度的把控基本就是看训练曲线反复调出来的没有一劳永逸的参数组合。4. 训练 DQN 打小鸟完整训练循环与关键参数环境封装好了智能体的框架在第 2 章也搭好了这一章把两者接起来完整的训练主循环、四组关键超参数的调法、以及训练曲线的读法。4.1 训练主循环的代码结构与参数说明训练主循环的逻辑是每回合重置环境——智能体根据当前状态选一个离散动作——动作映射成角度和力度——仿真器模拟弹射并返回奖励和新状态——存入回放缓冲区——每 N 步调用一次 Agent.update()。env BirdEnv() # 自建仿真环境 agent Agent(state_dim42, action_dim12, lr1e-3, gamma0.99, batch_size64, target_update1000) epsilon_start, epsilon_end, epsilon_decay 1.0, 0.05, 5000 episodes 800 step_counter 0 episode_rewards [] for ep in range(episodes): state, done env.reset(), False ep_reward 0.0 steps_this_episode 0 # 每回合最多发射 5 只鸟 while not done and steps_this_episode 5: # epsilon 按训练步数衰减 epsilon epsilon_end (epsilon_start - epsilon_end) * \ np.exp(-step_counter / epsilon_decay) action agent.act(state, epsilon) # 动作映射成 (angle, power) angle, power env.action_to_params(action) next_state, reward, done env.step(angle, power) # 存储转移step 计数递增 agent.buffer.push(state, action, reward, next_state, done) state next_state ep_reward reward step_counter 1 steps_this_episode 1 # 每 4 步训练一次提升训练频率 if step_counter % 4 0: loss agent.update() episode_rewards.append(ep_reward) if (ep 1) % 50 0: avg np.mean(episode_rewards[-50:]) print(fEpisode {ep1}, AvgReward{avg:.2f}, Epsilon{epsilon:.3f}, fLoss{loss:.4f})逻辑说明外层循环跑 800 个回合每回合发射上限 5 只鸟对应原版一关的鸟数。epsilon按指数衰减公式从 1.0 降到 0.05衰减速度由epsilon_decay控制。每 4 步调用一次agent.update()这个频率是经验值——每步都更新的话相邻更新之间的样本高度相关且训练开销大每 4 步更新能在样本利用率和训练稳定性之间取个平衡。参数说明state_dim42对应第 3 章的最大 8 个物体 × 5 维 弹弓 2 维 42如果你的向量表示不同这个数字要跟着改——改了环境忘了改维度是新手最常见的报错来源。action_dim12对应 3 个角度 × 4 档力度。angle, power env.action_to_params(action)是环境提供的映射函数比如 action 0 映射到 (30°, 50%)这个映射关系要提前定好并打印出来检查我踩过 action 索引写错导致智能体永远在试同一个角度的坑。4.2 超参数怎么调学习率、探索率衰减、批大小超参数不是一个固定组合而是跟着训练表现走。下面是四组参数的作用范围和我的调参习惯用表格列出来方便对照参数常见范围作用调参信号learning_rate1e-4 ~ 3e-3控制梯度更新步长loss 震荡剧烈则调小loss 不降则调大batch_size32 ~ 128每次更新的样本量太小梯度噪声大太大训练慢且易过拟合近期样本gamma0.9 ~ 0.99未来奖励的折扣奖励延迟明显时调大只看短期伤害时调小epsilon_decay3000 ~ 10000探索衰减速度前期学不动就放慢衰减让智能体多试学习率是最先要确认的。我在第 2 章的 Agent 里给的是 1e-3这个值配合 Adam 优化器在大多数小规模强化学习任务里能跑但如果你发现 loss 曲线像锯齿一样上下乱跳先别怀疑代码 bug把 lr 降到 3e-4 再看。愤怒的小鸟的奖励是事件驱动的只有撞击时才返回非零奖励梯度本身就稀疏过大的学习率会让网络参数在每次非零奖励时被推过头。探索率衰减是我最常调整的参数。epsilon_decay5000意味着大约 5000 步之后 epsilon 降到 1/e 附近如果一局平均 30 步就是约 170 局之后探索率只剩三分之一。如果前期智能体总是乱打你该做的是把epsilon_decay拉大到 10000让它在更长的训练时间内保持高探索。反过来如果训练后期智能体还在频繁随机动作、导致成绩不稳定就把epsilon_end从 0.05 降到 0.01。批大小的选择有个实际约束你自建的环境单步仿真很快但碰撞检测如果写得低效batch_size 越大每次等待采样数据的时间越长。64 是稳妥值32 能让训练更频繁地更新因为采集 32 条样本比 64 条快但梯度噪声更大。我一般先试 64看 GPU/CPU 利用率决定要不要调。4.3 训练过程的监控loss、奖励曲线到底在看什么训练不盯着曲线就等于瞎调参。我在第 4.1 的代码里每 50 回合打印一次平均奖励这是最基本的监控。除此之外两个信号必须看loss 的形态和奖励曲线的趋势。loss 在 DQN 里不等于模型性能。我见过很多人看到 loss 降到 0.001 以为模型学好了实际上是因为 epsilon 衰减到很低、智能体总是在重复同样的动作Q_target 和 Q_current 都很稳定loss 低只是因为不学了。反过来loss 周期性升高不一定是坏事——可能是智能体探索到了新策略Q 值需要重新调整。所以判断训练好坏的核心指标是平均奖励不是 loss。奖励曲线的读法要分段看。训练初期前 100 回合平均奖励应该在低位波动因为 epsilon 高、动作基本随机偶尔蒙中一次伤害会让点拔高。中期100 到 400 回合平均奖励应该有一个明显的上升台阶这代表智能体学会了瞄准和发射力度控制。后期400 回合以后曲线会进入平台期波动变小。如果 400 回合还没看到上升趋势问题大概率不在 DQN 算法本身而在环境接口或奖励定义——回到第 3 章去检查状态向量有没有传对、奖励有没有被 NaN 污染。额外建议在训练时把每一次弹射的角度、力度、造成的伤害记录到 CSV 文件里。这样训练完你能分析出智能体偏好哪些角度档位如果它把 90% 的弹射都集中在某一个角度说明动作坍缩了下一章的避坑指南会专门讲这个现象。5. DQN 训练愤怒的小鸟避坑指南5 个典型翻车现场跑 DQN 打小鸟最有价值的部分不是看到它学会通关而是搞清楚它为什么学不会。这一章列五个我反复踩过的坑每条按「现象 → 原因 → 解决」写清楚照着排查能省你至少两天时间。5.1 现象一loss 降了但平均奖励纹丝不动我见过最迷惑的情况每 50 回合打印的 loss 从 2.3 一路降到 0.05曲线漂亮得像教科书但平均奖励始终在 0 附近徘徊。当时第一反应是奖励计算出了 bug排查半天发现没有。原因是奖励尺度的问题。如果奖励绝对值很小比如单发命中只有 0.01Q 值网络的输出会逐渐被压到接近 0loss 当然小但梯度对策略的引导作用微乎其微。另一种情况是奖励的方差太大偶尔一次打出 50 分的暴击其余全是 0这种情况下 loss 的均值被少数大样本支配网络学会了输出一个中庸的 Q 值对应策略就是平庸地乱打。解决方法是先调奖励缩放系数让单发事件的奖励落在 0.5 到 2.0 区间其次检查奖励分布打印最近 100 次弹射的奖励值和方差如果方差比均值大一个数量级考虑对奖励做 clip 或取 log。还有个很实用的检查手段用手动策略固定 45°、80% 力度跑 20 回合如果手动策略的平均奖励比训练好的 DQN 高说明是训练问题不是环境问题。5.2 现象二训练到一半奖励突然崩盘这是一个更恼人的状况训练前 200 回合平均奖励稳定上升到了第 250 回合左右突然掉回负数之后再也上不来。第一次遇到我还以为是环境随机性重跑一次还是在类似位置崩盘。原因是 Q 值过估计overestimation积累到一定程度后的集中爆发。DQN 的 max 操作天然会高估 Q 值当某个状态的高估误差被回放缓冲区里的大量样本反复放大网络会对一些看起来好的动作给出虚高的 Q 值策略被带偏。目标网络虽然是缓解手段但如果target_update太频繁比如每 100 步同步一次目标网络和 eval_net 差异不大缓解效果就大打折扣。解决方法是先降低目标网络更新频率从 1000 步改成 2000 步甚至 5000 步如果还是崩就是 Double DQN——用 eval_net 选动作、target_net 算 Q 值把选动作和估值分开这个改动在代码里只加两行。再不行就调小学习率给网络更保守的更新步伐。我最后用的是 Double DQN 2000 步目标更新崩盘问题再没出现过。5.3 现象三智能体只会往一个方向弹训练结束后观察智能体的表现发现它 90% 的弹射都是同一个角度、同一档力度哪怕第一发已经没打中第二发还是原样重来。从奖励曲线上看它确实拿到了一些奖励说明不是完全没学只是策略塌缩到了一个次优解。原因有两层一是动作空间里某些动作的组合比如 45°、80%天然有更高的概率打中目标而其他动作初始探索阶段命中率低经验回放里这些好动作的样本占了多数网络就被带偏了二是 epsilon 衰减太快智能体还没充分探索就把探索率降得很低导致它没发现其他动作其实也能打。解决方法是先给探索留出更长的时间把epsilon_decay从 5000 提到 15000让前 400 回合都保持较高的探索率。其次检查动作映射看看是不是某些动作组合在物理上根本不可行比如 20° 配 30% 力度根本飞不到任何物体把这些废动作从动作空间里删掉。我用过一个取巧的技巧在前 100 回合强制给动作加均匀分布的随机噪声不管 epsilon 多少这样能保证每个动作在训练早期都被试够次数。5.4 现象四环境重置太慢训练一天没效果自建 PyGame 环境的帧率问题很隐蔽。弹射后的物理仿真如果每帧都要渲染画面训练速度会慢到让人怀疑人生。我跑过一个带完整渲染的环境一局要 3 秒训练 800 局就是 40 分钟还不算中间渲染的消耗。原因是把渲染和仿真耦合在一起了。DQN 训练根本不需要渲染画面除非你的状态表示是像素纯物理仿真用numpy计算每帧的位移和碰撞检测速度比 PyGame 渲染快几个数量级。解决方法是把环境改成 headless 模式没有渲染调用只有状态更新如果一定要用 PyGame把画面渲染放到独立的线程里或者只在save_screenshot True时渲染。另一个方法是向量化环境——同时跑多个简单关卡每个关卡是一个独立环境实例一次训练步同时采集多条经验样本效率直接翻倍。我用 4 个并行环境训练同样 800 局的时间缩短到了原来的四分之一效果完全一致。5.5 现象五模型在训练场景能打换个关卡就废训练时用的关卡布局是固定的几块木块加两只猪智能体练到平均 15 分但把场景里木块位置一换平均奖励直接跌到 3 分。这是典型的过拟合。原因是状态表示里没有布局的泛化信号。固定布局下每个物体的绝对坐标是固定的网络完全可以记住坐标 (120, 240) 有猪往那打就行而不需要学会理解猪在木块后面要先打支撑。你看到智能体打得好它记住的其实是坐标表不是策略。解决方法是做关卡随机化每次 reset 时随机微调物体位置、数量、类型组合。我常用的做法是把物体位置在一定范围内随机抖动并随机删掉 1 到 2 个无关木块。同时在 state 里不要用绝对坐标改成相对弹弓的偏移——(x - slingshot_x, y - slingshot_y)这样网络学到的是目标在我右上方角度应该大一些这类可迁移的关系。做完这两步换关卡的性能衰减从 80% 降到 30% 以内。6. 从打小鸟到打一切把 DQN 迁移到其他游戏的三步验证法练到你能让 DQN 稳定打通简化版愤怒的小鸟关卡这套代码就成了你自己的强化学习实验台换游戏只是换环境接口和奖励函数的事。我常用的迁移流程是三步验证每步都能快速定位问题是出在环境还是出在算法。第一步验证环境接口的合理性。新游戏的环境必须返回 (state, action_space, reward, done) 四件套且 state 不能包含未来信息否则就是作弊。我会先写一个随机策略跑 100 回合打印奖励的分布范围和回合结束条件——如果随机策略平均奖励已经是正数说明奖励设计太慷慨DQN 学出来的策略不一定比随机好多少这样的环境没有验证价值。第二步验证单步学习有效。把新环境接到第 2 章的 Agent 上关掉 epsilon 衰减固定在 0.1用固定布局训练 200 回合看平均奖励能否超过随机策略。这一步只验证你的代码链路有没有断——奖励有没有传对、done 有没有及时触发、状态维度有没有匹配。如果随机策略 50 分、训练后还是 50 分几乎肯定是环境状态里没有包含做出决策所需的信息。第三步逐步增加难度。固定布局跑通后加入关卡随机化再加连续事件的挑战比如移动目标、风场干扰每加一层难度就重跑一次完整训练对比平均奖励的变化趋势。如果新难度下奖励曲线完全不上扬优先检查是不是奖励信号被新机制稀释了而不是急着改网络结构。我自己的习惯是每次迁移前先打印一行环境摘要——状态维度、动作数量、奖励范围、回合平均步数这些数字能让我在训练前就对要不要调 epsilon_decay有个预判而不是等 200 回合跑了才发现参数不合适。这套流程我从愤怒的小鸟迁到过两个不同的物理堆叠游戏每次都是三步走成功率比我以前随缘调参高得多。最后说一个教训我最早做这个项目时花了整整两周想用卷积网络直接读像素打小鸟结果一直被训练不稳定折磨后来退回到物体级向量表示一天就把问题解决了。不是卷积网络不行而是像素级方案需要更多的调参耐心和更长的训练预算。如果你是为了理解 DQN 而不是为了发论文我强烈建议先用物体级向量跑通全流程等拿到稳定成绩再考虑升级状态表示。希望这篇笔记能帮你在 AI 玩游戏这个方向上少走几天弯路把你自己的 DQN 实验台搭起来。本文还有配套的精品资源点击获取