强化学习实战:用DQN训练AI玩愤怒的小鸟从环境搭建到调参避坑

发布时间:2026/10/11 16:12:38
强化学习实战:用DQN训练AI玩愤怒的小鸟从环境搭建到调参避坑
简介围绕DQN算法实现《愤怒的小鸟》自动游玩的完整工程包面向对深度强化学习感兴趣、希望从零搭建游戏AI的Python开发者。项目基于Python与TensorFlow包含深度Q网络的定义、训练与决策代码以及预训练权重bird-dqn-2920000和训练日志可直接加载运行观察AI逐步学会调整角度和力度击倒猪堡。资源共54个文件涵盖py源码、png图片说明、ogg/wav音频素材、xml配置及gif演示动画等压缩包整体23.54MB目录将代码、模型、素材分模块存放便于对照学习。目前已服务1646名学习者通过实际案例可深入掌握DQN经验回放、目标网络、ε-贪婪策略等核心机制并能迁移到其他游戏或控制场景中对探索-利用平衡、奖励函数设计也有直观参考。另外包内附带gif演示动画和png示意图可直观展示训练过程与网络结构下载后可配合代码快速梳理状态预处理和模型设计思路。1. 让 AI 玩愤怒的小鸟 DQN这个方向真正值得复现的点在哪里把强化学习放回到一个具体游戏里最容易出现两种极端一种是环境太简单训练一万步就收敛什么都看不出来另一种是环境太复杂模型跑了几天还在原地打转。愤怒的小鸟恰好卡在中间——它既有明确的物理判定又有离散的动作选择还保留了“差一点就命中”的直观反馈。用 DQN 来打愤怒的小鸟不只是给弹弓算抛物线而是让你在一个可视化、可量化的场景里把经验回放、目标网络、奖励塑形这些抽象概念一个一个抠清楚。这个方向适合谁适合已经把 DQN 的理论看了一遍、但还没真刀真枪跑过一个完整项目的从业者。也适合想从 CartPole 那种玩具环境跳出来、感受“真实动作空间设计”带来难度跃迁的爱好者。你最终得到的不是一个能稳定通关的 AI而是一套能复用、能调参、能解释的训练闭环。这篇文章从环境搭建讲到训练排坑全程可复现用的都是一台普通机器就能跑动的配置。2. 先搭环境还是先写网络愤怒的小鸟 DQN 的最小技术栈与选型理由2.1 环境选择为什么常见做法是先做一套简化物理模拟器很多人拿到这个题目第一反应是去找现成的游戏引擎或者模拟器但实际上更常见的做法是自己写一套简化物理环境。原因很简单官方游戏环境的接口不够可控训练速度也慢你很难拿到每一帧的状态和精确的碰撞信息。自己做环境你可以随时打印观测、改重力系数、调整命中半径哪怕环境写得糙一点只要物理行为大致符合直觉DQN 就能在这个环境里学出策略。我一般会先定义一个坐标系小鸟从原点或某个固定点出发猪的位置随机生成在弹弓右侧的某个区域。每次射击智能体输出一个角度和一个力度环境根据抛物线公式逐步更新小鸟的位置直到小鸟落地、命中或者飞出边界。这样一大轮 episode 的耗时在毫秒级训练 3000 回合也就几分钟到十几分钟的事。这里的关键取舍是把物理简化到什么程度。如果你只做弹道模拟不考虑木块、冰块和二次碰撞那么环境就是纯粹的抛物线运动。这样做的好处是问题清晰、可复现坏处是策略泛化到真实游戏时有偏差。对于入门 DQN这个偏差可以接受因为你要验证的是算法本身而不是做一个物理引擎。2.2 观测与动作接口设计画面、距离还是两者的组合下一个要决定的点是智能体看到什么。最直觉的做法是截取游戏画面像人一样看屏幕然后让 CNN 处理图像但这样会引入不小的训练开销而且观测里的很多像素对决策没有帮助。更实用的方案是使用向量观测把归一化后的角度、力度、小鸟当前位置、猪的位置、两者水平距离和垂直距离拼成一个向量。这个选择会直接影响网络结构和训练难度。向量观测的好处是状态空间维度低一个两到三层的全连接网络就能处理缺点是智能体能依赖的信息有限如果观测里漏掉了关键量比如当前小鸟的 y 坐标模型可能根本学不会“先飞到最高点再下落”的直觉。动作空间的离散化也是容易拍脑袋决定的部分。常见做法是把角度分成若干档力度分成若干档然后把所有组合展开成一个一维动作索引。比如角度取 -60 到 60 度、间隔 15 度一共 9 档力度取 0.2 到 1.0、间隔 0.2一共 5 档组合起来就是 45 个离散动作。45 个动作不算多DQN 的输出层直接输出 45 个 Q 值即可。为什么不用连续动作因为标准 DQN 只能处理离散动作空间如果要用连续角度和力度就得换成 DDPG、SAC 这类 actor-critic 算法。这个标题写的是 DQN那么离散化就是最快打通整个流程的方式。等到你后面把 DQN 跑通了再去对比连续动作算法会更有获得感。2.3 DQN 网络结构选型从输入到输出的形状推导网络结构不需要复杂。输入维度就是观测向量的长度假设是 9 个特征角度、力度、鸟 x、鸟 y、猪 x、猪 y、dx、dy、距离输出维度是 45。中间先接一个 128 个神经元的全连接层再接一个 64 个神经元激活函数统一用 ReLU最后一层是线性输出。你可能会问为什么不用更深的网络在这个任务里从观测到 Q 值的映射并没有那么强的非线性加深网络只会增加训练时间和过拟合风险。我实际调过的配置里512 宽的效果和 128 宽的差不多但训练稳定性反而更差。更关键的网络设计点在于是否要拆分输入有些实现会把“当前小鸟位置”和“猪的位置”分开编码再拼接起来这样在语义上是更清晰的不过对于这个任务没必要。等模型不收敛的时候先看看观测里有没有包含决策所需的全部信息再考虑结构问题。经验回放缓冲区和目标网络是整个训练稳定性的两块基石。缓冲区存的是 (state, action, reward, next_state, done) 五元组每次训练从缓冲区随机抽一批样本打破数据之间的时间相关性目标网络则提供一个冻结的 Q 值参考避免训练过程中目标值一直跟着当前网络漂移。这两个组件在下一章会直接落到代码里先从原理上理解它们的作用后面调参时才不会乱。3. 把愤怒的小鸟环境跑起来状态、动作与奖励的代码实现3.1 用 Python 实现一个可以训练的最小物理环境我不建议用复杂的游戏库来做这一步直接用 Python 写一个类就能满足训练需求。这个环境类的核心方法是 reset 和 step。reset 负责随机生成一个猪的位置返回初始观测step 接收动作索引解算出角度和力度然后模拟弹道直到落地或命中返回下一个观测、奖励和结束标志。import numpy as np class SimpleAngryBirdEnv: def __init__(self, step_dt0.05, g9.8, hit_radius0.8): self.dt step_dt self.g g self.hit_radius hit_radius self.angle_grid np.arange(-60, 61, 15) # 角度-60~60度步长15度 self.power_grid np.arange(0.2, 1.01, 0.2) # 力度0.2~1.0步长0.2 self.state None def reset(self): # 猪的位置随机出现在弹弓右前方x范围5~12y范围0~4 pig_x np.random.uniform(5, 12) pig_y np.random.uniform(0, 4) self.bird_x, self.bird_y 0.0, 0.0 self.pig_x, self.pig_y pig_x, pig_y self.done False return self._get_obs() def _get_obs(self): dx self.pig_x - self.bird_x dy self.pig_y - self.bird_y dist np.sqrt(dx**2 dy**2) return np.array([ dx / 15.0, dy / 8.0, dist / 15.0, self.bird_x / 15.0, self.bird_y / 8.0, self.pig_x / 15.0, self.pig_y / 8.0, dx, dy ], dtypenp.float32) def step(self, action): angle_deg self.angle_grid[action // len(self.power_grid)] power self.power_grid[action % len(self.power_grid)] angle_rad np.deg2rad(angle_deg) vx power * np.cos(angle_rad) vy power * np.sin(angle_rad) while not self.done: self.bird_x vx * self.dt self.bird_y vy * self.dt vy - self.g * self.dt dist np.sqrt((self.bird_x - self.pig_x)**2 (self.bird_y - self.pig_y)**2) if dist self.hit_radius: return self._get_obs(), 10.0, True, {hit: True} if self.bird_y 0 or self.bird_x 16 or self.bird_y 10: return self._get_obs(), -0.5, True, {hit: False} return self._get_obs(), 0.0, True, {hit: False}这段代码的逻辑说明reset 每回合随机生成猪的位置保证训练时能看到不同距离和不同高度的目标。step 先把动作索引拆回角度和力度然后用一个 while 循环逐步更新小鸟位置每步模拟一个微小的时间间隔直到发生命中、落地或出界三种终止条件之一。这个 while 循环的粒度由 step_dt 控制dt 越小时序越精确但每一回合计算量也越大。参数说明g 是重力加速度这里直接用 9.8不区分像素单位和米制单位因为整个坐标系是抽象出来的。hit_radius 是命中半径0.8 表示小鸟和猪的距离小于 0.8 就算命中。角度档位和力度档位定义在 angle_grid 和 power_grid 里修改这两个数组就能改变动作空间大小。观测向量里把 dx、dy 等原始值也放进去了归一化的部分和控制量叠加在一起是为了让网络更容易学习但保留原始值可以在调试时直接打印检查。3.2 动作空间离散化角度与力度的组合表离散化动作空间看似只是简单的一维展开但展开方式决定了网络输出层的组织方式。这里把动作索引按照“先角度后力度”的规则展开也就是每个动作先固定一个角度再遍历该角度下的所有力度档位。这样的好处是当你需要分析某个动作的具体含义时可以直接通过整除和取余还原出角度和力度。参数取值说明角度范围-60° ~ 60°以水平方向为 0°负值代表向下拉弓角度步长15°共 9 档过小会让动作空间爆炸力度范围0.2 ~ 1.0归一化的弹弓力度系数力度步长0.2共 5 档总动作数 9×545重力系数9.8控制抛物线弯曲程度数值越大飞行越平这个表格就是动作空间的“契约”环境、网络、训练代码全部围绕它来写。在实际项目中你会想调力度步长或者角度范围这时候唯一需要同步修改的就是 angle_grid 和 power_grid 两个数组以及注释里的动作数量。我有一个习惯把动作总数写成一个 ACTION_DIM 常量并在环境初始化时断言 len(angle_grid) * len(power_grid) 等于 ACTION_DIM防止后面网络输出维度和环境动作数对不上。一个容易忽略的点是力度归一化。0.2 到 1.0 的力度不是直接用来做速度而是在计算初速度时作为系数乘以 cos 和 sin。这意味着力度为 1.0 时水平分量最大为 1.0 米/秒在重力 9.8 的作用下飞行距离大约在 0.1 米量级。你可能会觉得这个飞行距离太短其实这里的单位全是抽象的只要命中半径、猪的位置范围和力度系数三者匹配让猪出现在“能打到的距离区间”内就行。不要盲目照搬别人的数值先打印几组动作的落点确认覆盖面合理再开始训练。3.3 奖励塑形的三个档位命中、接近与容错奖励设计直接决定 DQN 学出来的策略品味。最基本的档位是稀疏奖励命中给 10其他情况一律 0 分回合结束也不额外给惩罚。这种设计最简单但问题也很明显——如果猪的位置随机且命中半径很小随机动作的命中率可能只有百分之几模型很难在有限的探索里碰到正样本训练就会停滞。第二档是加落地惩罚无论命中还是没命中回合结束都返回一个负值比如 -0.5。这样一来智能体至少会倾向于缩短回合避免无意义的拖延。注意这里的“缩短回合”很可能导致策略变成“随便打一发就结束”所以还要配合正奖励来引导。第三档是命中奖励加距离惩罚在回合结束时如果没命中按最终落点与猪的距离给出一个负分距离越近扣得越少距离越远扣得越多。我用的是 -min(2.0, dist / 3.0) 这种封顶写法避免距离特别远时奖励值过度拉大。这个档位下模型能学到“把小鸟打到猪附近”这个中间目标训练曲线的上升也会更平滑。在代码里可以把挡位做成可配置的class RewardConfig: def __init__(self, hit_reward10.0, miss_penalty0.5, use_distance_rewardFalse): self.hit_reward hit_reward self.miss_penalty miss_penalty self.use_distance_reward use_distance_reward def compute_reward(env, hit, config): if hit: return config.hit_reward if not config.use_distance_reward: return -config.miss_penalty dist np.sqrt((env.bird_x - env.pig_x)**2 (env.bird_y - env.pig_y)**2) return -min(2.0, dist / 3.0)这样 switch 奖励档位时不需要改动环境主循环只要在训练代码里替换 compute_reward 的调用即可。我通常先不开距离奖励用稀疏奖励加落地惩罚跑 1000 回合观察曲线是否有任何抬升信号如果有就说明随机探索能碰到命中样本再考虑是否加距离奖励。如果没有再打开距离奖励引导探索。这个调试顺序比一开始就堆叠奖励要容易定位问题。4. DQN 训练主循环记忆库、目标网络与 epsilon 策略的实现细节4.1 经验回放与目标网络为什么这两个组件缺一不可如果去掉经验回放DQN 就退化成一个在线学习的 Q 学习变体相邻状态之间高度相关更新时会反复往同一个方向推导致损失剧烈波动。经验回放的核心是用一个固定容量的队列存下过去的所有转移训练时随机采样打破时间相关性。目标网络解决的是另一个问题Q 学习的目标值里包含对下一个状态的最大 Q 估计如果这个估计来自当前正在更新的网络那目标值会跟着网络一起漂移训练过程容易发散。常规做法是维护一个参数滞后于在线网络的 target_net每隔 C 步把在线网络的参数复制给它让目标值在 C 步之内保持稳定。这两个组件在 PyTorch 里实现起来很直接。记忆库用 deque 就行目标是 actions 和 rewards 的队列容量设成 50000 已经足够。4.2 训练循环代码与超参数说明下面给出一个可以跑通的完整训练片段。环境采用上面的 Simplified Bird 环境网络用两层全连接优化器选 Adamloss 用 MSE。重点看记忆库怎么存、目标网络怎么同步、epsilon 怎么衰减。import torch import torch.nn as nn import numpy as np from collections import deque class DQN(nn.Module): def __init__(self, state_dim, action_dim): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, 128), nn.ReLU(), nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, action_dim) ) def forward(self, x): return self.net(x) state_dim 9 action_dim 45 gamma 0.99 lr 0.001 batch_size 128 replay_size 50000 target_update_freq 200 epsilon_start 1.0 epsilon_end 0.05 epsilon_decay_steps 2000 train_steps 5000 online_net DQN(state_dim, action_dim) target_net DQN(state_dim, action_dim) target_net.load_state_dict(online_net.state_dict()) optimizer torch.optim.Adam(online_net.parameters(), lrlr) replay_buffer deque(maxlenreplay_size) env SimpleAngryBirdEnv() step_count 0 for episode in range(1000): state env.reset() done False total_reward 0.0 while not done: # epsilon-greedy 选择动作 epsilon max(epsilon_end, epsilon_start - step_count * (epsilon_start - epsilon_end) / epsilon_decay_steps) if np.random.rand() epsilon: action np.random.randint(action_dim) else: with torch.no_grad(): q_vals online_net(torch.FloatTensor(state).unsqueeze(0)) action q_vals.argmax(dim1).item() next_state, reward, done, info env.step(action) replay_buffer.append((state, action, reward, next_state, done)) state next_state total_reward reward # 缓冲区的样本足够后开始训练 if len(replay_buffer) batch_size: batch np.random.choice(len(replay_buffer), batch_size, replaceFalse) states torch.FloatTensor([replay_buffer[i][0] for i in batch]) actions torch.LongTensor([replay_buffer[i][1] for i in batch]).unsqueeze(1) rewards torch.FloatTensor([replay_buffer[i][2] for i in batch]).unsqueeze(1) next_states torch.FloatTensor([replay_buffer[i][3] for i in batch]) dones torch.FloatTensor([replay_buffer[i][4] for i in batch]).unsqueeze(1) current_q online_net(states).gather(1, actions) with torch.no_grad(): max_next_q target_net(next_states).max(dim1, keepdimTrue)[0] target_q rewards gamma * max_next_q * (1 - dones) loss nn.MSELoss()(current_q, target_q) optimizer.zero_grad() loss.backward() optimizer.step() step_count 1 if step_count % target_update_freq 0: target_net.load_state_dict(online_net.state_dict())这段代码里值得注意的地方有几个。经验回放抽样用的是 np.random.choice从 buffer 里随机抽 128 条每条都是独立的五元组。这样做比按顺序取最近的 batch 要稳定因为遇到连续几帧都是同一局比赛时在线更新的方差会大很多。target_q 的计算里乘了 (1 - dones)这是让 done 状态的样本不再把未来奖励算进目标。如果不做这个 mask已经结束的回合会被模型当作“还能继续拿奖励”导致起飞后的终止状态估值偏高。epsilon 的衰减用的是线性公式从 1.0 降到 0.05衰减步数设成 2000。这个数值意味着前 2000 个训练步里智能体用很高的概率做随机探索之后逐步转为利用已学到的策略。如果你的环境动作空间更大衰减步数要相应拉长否则探索不够充分。loss 没有做任何梯度裁剪。在 DQN 里Q 值偶尔会出现尖刺如果不加 clip一次异常的大 loss 就可能把网络权重推出正常范围。常见做法是给 loss 计算加上 clip_value1.0 的 Huber 损失也就是 SmoothL1Loss。如果你发现训练过程中 loss 突然跳到几百再回来就收敛不回去了优先检查减 target_q 后的差是否过大然后换成 Huber loss。4.3 日志与评估训练多久才敢说“学会”了训练过程中光看 loss 曲线是不够的因为 loss 下降不代表策略变好它只说明当前 Q 值和目标 Q 值的差距在缩小。你需要一个独立的评估函数固定生成 20 个猪的位置或者直接使用同一批随机种子把 epsilon 设为 0让模型用贪心策略各打一次统计命中率和平均回合奖励。def evaluate(env, online_net, episodes20, seed42): rng np.random.RandomState(seed) hit_count 0 total_rewards [] for _ in range(episodes): # 用固定种子生成位置保证评估可对比 state env.reset() done False ep_reward 0.0 while not done: with torch.no_grad(): q_vals online_net(torch.FloatTensor(state).unsqueeze(0)) action q_vals.argmax(dim1).item() next_state, reward, done, info env.step(action) state next_state ep_reward reward if done: if info[hit]: hit_count 1 total_rewards.append(ep_reward) return hit_count / episodes, float(np.mean(total_rewards))评估时最关键的是固定种子。同一批评估关卡在训练前后各跑一次才能看到真实的提升。如果每次评估都随机生成位置那么命中率会在 0% 到 30% 之间大幅波动你根本分不清是模型变好了还是关卡变简单了。把评估函数挂到训练主循环里每 500 回合跑一次同时打印 epsilon 和最近的 loss 均值。当你看到命中率从个位数稳步爬到 40% 以上同时平均回合奖励从 -0.5 附近升到正值就可以认为模型已经在这个简化环境里学到了基本策略。真实游戏里还涉及更复杂的物理碰撞但作为 DQN 项目这个验收标准已经足够说明问题。5. 愤怒的小鸟 DQN 训练避坑指南五个翻车现场与修法5.1 奖励全为 0稀疏奖励导致的训练停滞现象训练跑了几千步loss 一直在降但回合奖励始终在 -0.5 附近徘徊命中率纹丝不动。原因这是稀疏奖励的典型表现。当命中概率很低时缓冲区里几乎没有正奖励样本网络学到的只是“无论做什么都拿 -0.5”Q 值被压成一片平坦的负值梯度也提供不了有效的策略方向。解决分两步走。第一步确认随机策略的命中率到底有多少如果低于 5%就不要指望纯探索。第二步打开距离奖励让没命中的回合也能按最终落点距离拿到有区分度的负分比如 -min(2.0, dist / 3.0)。这样哪怕打不中模型也能学会“落在猪旁边比落得远更好”。同时可以调大 epsilon 的初始值或延长衰减步数但不能只靠这一招。5.2 网络不收敛目标网络更新频率与学习率失衡现象loss 曲线像锯齿一样上下剧烈震荡甚至中出现几十到上百的尖峰之后模型彻底失效。原因常见的原因是目标网络更新太频繁或者学习率过高。如果每 50 步就同步一次 target_net目标值一直在追着在线网络跑DQN 很容易在局部振荡。学习率 0.01 以上的 Adam 在这个小网络上也会放大 Q 值的微小扰动。解决先做一次“抄作业式的设置”目标网络更新频率设为 200 步学习率降到 0.001损失函数换成 Huber。如果还震荡把更新频率提到 400 步再看。不要同时改多个超参数一次只动一个否则你根本不知道是哪一步修好了问题。一个排查技巧是打印 Q 值的量级如果某次更新的 max_Q 突然比上一轮大 10 倍以上基本可以锁定是目标值计算出了异常。5.3 小鸟每次都飞出屏幕动作空间过大的边界问题现象训练结束之后用贪心策略评估发现小鸟的落点总是超出猪的位置很远甚至直接飞出屏幕。原因这是典型的动作空间设计问题。力度档位上限太大或者角度范围覆盖了太多无用区域导致大部分动作的落点都集中在远距离区域。DQN 学习到的策略可能选择了“看起来合理”的中间档位但实际物理模拟里这个中间档位还是打不中目标范围。解决回到环境代码里把每种力度和角度组合的落点打印出来画一张散布图看猪的位置是否能被动作空间有效覆盖。如果猪普遍出现在 x5~12 的范围而动作落点大多在 x15 以上说明力度上限过大了。把 power_grid 改成 0.15 到 0.6或者调整猪的位置范围让动作空间和目标任务匹配。记住一个原则动作空间越大不代表越先进覆盖率高才意味着模型有机会找到正解。5.4 训练曲线看起来很漂亮但实际打不中评估环节的隐性陷阱现象训练过程中平均回合奖励一路上升从 -0.5 升到 1.2但拿固定关卡测试命中率却只有 10%。原因评估和训练状态不一致。最常见的情况是训练时用了随机生成的猪的位置而模型其实记住了训练后期大量出现的某种位置分布碰到评估里的固定关卡反而表现不好。另一个隐蔽原因是评估时没关掉 epsilon模型有 5% 的概率随机乱飞命中率被拖低。解决评估函数里必须把 epsilon 设为 0并且固定 seed。同时把训练期间生成的猪的位置分布打印出来看看是均匀分布还是偏向某块区域。如果是环境 bug 导致位置集中在某个角落模型学到的是“朝那个角落打”而不是“根据猪的位置调整弹道”。修复方法是确保 reset 里用 np.random.uniform 并且范围足够宽再用分布直方图验证。5.5 玄学波动随机种子和 epsilon 衰减的相互影响现象同一个超参数配置只是换了随机种子训练结果差异巨大。一次跑到命中率 60%另一次只有 15%。原因这是强化学习的正常现象不是 bug。随机种子决定了初始网络权重、猪的位置序列、探索动作等多项随机性。epsilon 衰减如果过快早期探索不足后期策略被锁定在一个次优解上衰减过慢则模型浪费大量时间在低效探索。解决接受“多跑几个种子看分布”这个观念同时把 epsilon 衰减策略做得更平滑。我喜欢用分段衰减前 20% 的训练步数让 epsilon 从 1.0 线性降到 0.2之后保持 0.2 很长时间最后 20% 再降到 0.05。这种退火方式在探索和利用之间留出了更充足的缓冲。记录时先跑 3 个种子报告命中率的中位数和范围而不是某一个种子的最优值。6. 把训练好的 DQN 用到新关卡泛化验证与参数微调技巧训练收敛后你要面对一个问题这个 AI 换个关卡还能打吗验证方法很简单把猪的位置范围扩大或者在评估集合里加入训练时从未出现过的猪的位置组合。如果命中率明显下降先检查是不是过拟合到了训练分布的某个中心点。一个实用技巧是每隔一段训练步数把当前模型的评估结果记录到 CSV 里和训练损失画在同一张图上这样能直观看到“loss 还在降泛化指标已经不再提升”的过拟合信号。参数微调的优先级也很明确。第一优先是奖励函数其次是动作空间覆盖最后才考虑网络结构。我想要调整命中奖励从 10 提到 20可以减少“满足于接近目标”的倾向但注意太多正奖励会让 Q 值整体偏大梯度更新可能变得急躁。力度步长从 0.2 缩到 0.1会生成 90 个动作训练时间明显变长但命中精度会有提升。如果你的目标只是验证 DQN 流程不建议一开始就追求高精度先跑通一个命中率 30% 的版本再逐步收紧。我最终保留的一套配置是这样的角度 9 档、力度 5 档命中奖励 10 分、落地惩罚 0.5 分不加距离奖励但靠随机探索保证正样本比例在 5% 以上目标网络每 200 步同步Adam 学习率 0.001批量大小 128。这个配置不是最优的但它足够稳定任何一个刚接触 DQN 的人复现都不会翻车。这里有一个我重复过很多次的教训不要一边改奖励一边换网络结构自以为能加速收敛结果只会让你连“为什么这次没跑好”都说不清楚。先把环境、动作、奖励全部钉死跑出一个可接受的 baseline再单独动一个变量。调参的过程本质上是做对照实验不是靠感觉玄学碰运气。这套方案做完之后你手里有了一个完整的强化学习闭环从物理模拟器的编写到动作空间的离散化到 DQN 训练与评估再到踩坑排查。把游戏换成别的二维弹道类场景只需要改环境和奖励函数训练循环可以直接复用这就是做这个方向最大的收获。希望帮到你。本文还有配套的精品资源点击获取