
简介这套名为“人工智能玩游戏之-愤怒的小鸟 DQN”的工程包聚焦深度强化学习中的经典DQN算法以《愤怒的小鸟》为环境面向具备一定Python基础、希望动手实践强化学习项目的开发者。压缩包共54个文件约23.54MB主体包括4个Python源文件如网络定义与训练脚本、保存的预训练模型checkpoint、meta等、5张XML配置、18张PNG图片素材、5个OGG与5个WAV音效以及gif演示文件和说明文档结构完整可直接加载运行。当前已有1645人学习下载。项目实现了从游戏画面预处理、Q网络设计到经验回放、目标网络同步的完整流程并附带了预训练权重读者可快速观察AI通过调整角度与力度击倒猪堡的过程也能在此基础上调整超参数或网络结构进行二次实验是理解DQN与视觉决策任务的良好范例。 前几天整理硬盘翻出了当年做的那个强化学习项目——用DQN训练AI打愤怒的小鸟。这个项目最早是我研究生阶段《人工智能导论》课程的大作业后来自己又迭代了好几版从一开始的“纯随机乱射”到后来能两三发通关中间踩过的坑几乎能写满一本笔记。趁着今天有空我把整套做法的思考路径、实现细节和调参经验整理出来想拿游戏当深度强化学习练手项目的人应该能省下不少试错时间。你可能觉得愤怒的小鸟只是个弹弓小游戏和AI能有什么关系其实它非常适合做DQN的入门场景画面简单但信息完整、物理效果直观、每发小鸟的胜负反馈非常清楚。而且它和CartPole、Flappy Bird这类经典测试环境不同动作空间不是左/右这种抽象指令而是包含角度和力度的连续射击参数更接近真实世界的决策问题。这篇文章会从环境设计、网络搭建、奖励函数到训练调参完整过一遍即使你之前没碰过强化学习照着手搓一个能玩的Agent也不是难事。1. 为什么选择「愤怒的小鸟」作为DQN练手项目1.1 这是一个典型的强化学习问题强化学习解决的是“序列决策”问题智能体观察当前状态做出动作环境给出奖励然后进入下一个状态。愤怒的小鸟恰好完美符合这个设定——AI看到屏幕上的关卡结构决定拉弓的角度和力度发射后小鸟飞出击中建筑或者打不中然后进入下一回合。这里要特别说一下很多人以为AI只在发射那一刻做一次决策之后就是抛物线自由飞行根本算不上序列决策。但实际上每一关有多只小鸟每射击一次关卡状态就变了某些建筑塌了某些猪没了剩余小鸟数量也减少了。所以完整的一局可以看作一个多步MDP过程每一步的“状态”都在更新“决策”就是射击参数“奖励”就是这次射击造成的得分变化或关卡进展。这种设定比单纯的“一次预测”更有强化学习味道也更适合DQN训练。1.2 选择这个游戏的具体理由我在选型时其实对比了好几个候选CartPole太简单、Atari Pong还是像素操作、王者荣耀那种动作空间太大不现实。愤怒的小鸟处在中间位置刚刚好。动作空间可离散化角度从20°到70°力度从30%到100%拆成网格后大概几十个动作既不复杂又能看出策略。奖励信号非常明确每一发小鸟打出去屏幕右上角的分数就会变动游戏结束还能看到是否通关。奖励天然存在不需要自己造。视觉信息丰富但可控整个游戏区域是2D场景摄像镜头固定在侧面没有复杂的3D视角旋转AI能直接从截图里提取有效特征。物理引擎本身就是老师小鸟飞行会受到重力撞到木头、冰块、玻璃会有不同的反馈。AI不需要懂物理公式只要不断尝试就能从像素观察里学会“瞄准”这种抽象能力。失败成本低一局打不好随时重启训练时间可控非常适合个人电脑上跑。1.3 在这个场景里AI到底要学什么抛开“打猪”这个表面目标AI要学的其实分三层第一层是弹道直觉。给定一个目标位置AI要能大概估计出应该用什么角度、什么力度这需要大量试错来建立像素坐标和飞行轨迹之间映射。第二层是结构策略。不是所有目标都值得直接打。有些关卡里猪躲在木头棚子下面直接命中的效果远不如打掉支撑柱让屋顶塌下来砸到猪。DQN能不能学会这种“间接伤害”策略实测下来看到AI慢慢会打支撑柱了这比单纯瞄准要高级得多。第三层是资源分配。一只鸟没打中要不要调整策略用什么类型的鸟这个简化版里所有鸟都一样这些细微选择会在奖励函数里被隐式编码。2. DQN的核心机制与“为什么有效”2.1 从Q表格到深度网络DQN的全称是Deep Q-Network本质上是把Q-learning里的Q值表换成了深度神经网络。经典Q-learning的做法是维护一个表格记录“在状态s下执行动作a能获得多少长期回报”然后不断用贝尔曼方程去更新这个表格。问题是游戏画面经过像素化之后状态空间是天文数字级别的表格根本存不下。DQN的关键思路是用CNN从原始像素里提取特征然后输出每个动作的Q值。也就是说神经网络要拟合的函数是Q(s, a)输入是84x84x4的堆叠灰度图输出是动作数量的实数向量。这里有个新手常问的问题为什么不把连续的状态输入直接回归出一个最佳动作因为动作之间不是简单的数值关系角度60°力度50%和角度60°力度70%之间的“差距”并不能用线性方式度量。输出每个动作的Q值然后用argmax选最大更适合离散控制。2.2 两个稳定训练的关键机制DQN里的状态是前后连贯的直接拿连续几帧数据训练神经网络会导致严重的不稳定。主要问题有两个样本相关性太强、优化目标在不停变动。经验回放Experience Replay的解决办法是把每次转移数据(s, a, r, s)存进一个固定容量的队列训练时随机抽取一个batch来更新参数。随机采样打破了轨迹之间的时间相关性让模型不会“刚学完自己上一秒的操作就忘了更早的经验”。目标网络Target Network解决的是另一个问题。如果只有一个网络更新参数的瞬间计算Q目标值和预测Q值用的是同一套权重模型会像追自己尾巴的小狗一样绕圈。DQN的做法是保留一份参数滞后更新的“目标网络”每隔若干步复制主网络权重过去这样在一段时间里目标是固定的训练方向就不会乱。2.3 关于DQN的局限性动手之前得先有心理准备基础DQN有两个臭名昭著的毛病。一是Q值过估计。因为max操作天然会放大噪声DQN对动作的价值估计往往偏高实际效果可能比理论要差。这可以通过Double DQN解决后面的常见问题部分我会展开说。二是样本利用率低。一次episode产生不了几条有效经验要学习一个像样的策略经常需要几十万步。愤怒的小鸟还好动作结果在一个爆发式事件后就能看出来如果你换到长流程游戏这个缺点会被无限放大。3. 把游戏变成训练环境状态、动作、奖励三件套3.1 状态空间怎么设计我先试过直接用RGB三通道截图输入结果训练速度慢到无法忍受。后来参考了DQN在Atari上的经典做法把游戏画面转成灰度图、缩放到84x84像素、连续堆叠4帧作为输入。堆叠4帧是为了让网络感知运动趋势。只看单帧画面AI无法判断小鸟是在飞向目标还是已经飞过了也不能感受弹弓的拉伸变化。4帧灰度图相当于给网络提供了“短视频片段”能捕捉到关键动量信息。具体操作里有个容易被忽略的细节截图区域要固定窗口位置不要动否则同一个游戏关卡在不同截图位置下会被识别成完全不同的状态。我在脚本里直接用PyGetWindow锁定窗口坐标每次截图前都会校准一下。3.2 动作空间怎么离散化原版游戏里角度和力度是连续变化的但DQN本身是离散动作算法强行输出连续值会引入额外复杂度。我直接把二维参数空间网格化角度20°到70°每10°一个档共6档力度30%到100%每10%一个档共8档两两组合之后得到48个离散动作。这个数量对全连接层输出来说完全OK但如果角度步长减到5°、力度步长减到5%动作数会膨胀到144个训练时间会明显变长。我发现对于这类简单关卡10°的精度已经够用AI打不到猪主要不是精度问题而是策略问题。3.3 奖励函数的设计细节奖励设计直接决定AI会往什么方向优化这也是整个项目我花时间最多的地方。初始版本我用了最朴素的奖励每次射击后把得分增量作为奖励值。但实测效果不理想因为大多数射击的得分增量是0AI根本不知道哪个动作“好了一点点”奖励信号太稀疏。后来改成混合奖励方案分数增量奖励r_score (score_after - score_before) * 0.01这个值通常在0到5之间碰撞有效奖励如果小鸟在飞行中碰到任何物体可以通过开源模拟器的碰撞事件反馈判断额外给0.2通关奖励每关结束时如果剩余小鸟还有盈余给5分空枪惩罚如果小鸟什么都没碰到就坠地给-0.1的微小惩罚这个方案的核心思想是“把稀疏的大奖励拆成密集的小奖励”。AI不需要等到一关打完才知道这发打得好不好它可以从碰撞事件里直接获得即时反馈学起来快很多。3.4 一局游戏的Episode怎么界定训练时我把“一局游戏”作为Episode每局有5只小鸟所以每个Episode包含5个决策步。状态在每发小鸟落地后更新Done标志在关卡结束或小鸟用完后置位。这个设定和常见的CartPole有很大区别CartPole每个step都会立刻得到反馈而愤怒的小鸟必须等小鸟飞完大概2到5秒才能看到结果。为了模拟这种延迟反馈我在模拟器里做完一次射击后不会立刻取下一个状态而是等到物理仿真结束再继续采集下一步状态。刚开始写代码时很容易忽略这个等待过程导致状态采样全部错乱。4. 网络结构与超参数的选择逻辑4.1 网络结构设计网络结构我直接沿用了Atari DQN的经典配置在愤怒的小鸟场景下实测效果不错层类型卷积核步长输出尺寸输入层--84x84x4Conv18x8420x20x32Conv24x429x9x64Conv33x317x7x64全连接层--512输出层--48三个卷积层的作用是逐级抽取空间特征第一层提取边缘和轮廓第二层提取局部结构如木头块儿、冰块形状第三层综合出全局建筑布局。全连接层把空间特征映射到动作价值上。有人问为什么不用更大更深的网络深度太深在样本量不够时反而容易过拟合把训练集里某个特定角度识别成“神角度”换个关卡就不会玩了。浅层网络结构表达力虽然弱一点但泛化能力更强训练速度也快用在这个小规模游戏上更合适。4.2 超参数推荐表超参数是我调了一周之后的最终版直接给出表格照着用就行参数取值说明优化器Adam比SGD收敛更快适合小规模项目学习率1e-4一开始用1e-3会梯度爆炸折扣因子γ0.95延迟奖励只有几步不需要太高的折扣经验池容量50000差不多是一万局的样本量Batch Size32经验采样批次大小目标网络更新频率每500步训练步数不是episode数ε初始值1.0前1000步全随机探索ε终值0.05后期保底探索程度ε衰减步数5000从1.0线性衰减到0.054.3 为什么这些超参数这么选学习率先从小数值开始是我的血泪教训DQN的损失函数天然比较剧烈刚开始训练时Q值变化会很大1e-3的学习率很容易让损失直接变成NaN。1e-4到3e-4是DQN的“安全区间”。γ取0.95是因为愤怒的小鸟每个episode只有5步奖励最多滞后几帧画面。γ太大没有意义太小又会让AI变成纯粹的“眼前主义”只看单发鸟打出的即时奖励忽略后续小鸟的配合。至于目标网络更新频率500步我试过100步和1000步100步太频繁导致目标一直在变稳定性差1000步太久则会让目标网络完全跟不上主网络的变化训练速度急剧下降。500步是个不错的平衡点。5. 训练实操流程与现场观察记录5.1 训练主循环伪代码核心训练逻辑其实也就几十行代码关键步骤写在下面# 伪代码实际项目里用TensorFlow 2.x env AngryBirdsEnv() agent DQNAgent( state_shape(84, 84, 4), num_actions48, lr1e-4, gamma0.95, memory_size50000, batch_size32, target_update_freq500 ) for episode in range(5000): state env.reset() ep_reward 0 for bird_idx in range(5): epsilon max(0.05, 1.0 - episode / 5000) action agent.choose_action(state, epsilon) next_state, reward, done env.step(action) agent.store_transition(state, action, reward, next_state, done) if len(agent.memory) 32: loss agent.train_once() state next_state ep_reward reward if done: break if episode % 100 0: eval_score evaluate(agent, eval_episodes5) print(fEpisode {episode}, EvalScore {eval_score:.2f})这段代码里有三个位置必须小心。第一是choose_action里的ε值很多人在训练后期忘记把ε降下来导致模型始终在乱试评估分数始终上不去。第二是store_transition一定要在train_once之前执行否则第一次更新时经验池里是空的。第三是evaluate时要把ε强制设成0只用贪心策略跑这样评价结果才是真实水平。5.2 训练过程中能观察到的三个阶段训练过程不是一蹴而就的我记录到的分数曲线大概可以分成三个阶段。阶段一纯随机乱射0-400局。这个阶段Agent完全不懂物理规律角度和力度基本随机经常打出空枪。损失值却挺高因为Q值的更新方向乱七八糟。不用慌这是正常现象说明Agent正在用碰撞反馈建立最基础的物理感知。阶段二学会“瞄准”400-1200局。这里是最兴奋的阶段。AI开始大量选择中等角度和中等力度偶尔能打中地图边缘的小猪平均单局得分开始明显上升。我观察到这个阶段AI特别喜欢用45°角因为抛物线最远打中目标的概率最高但还不大会拆建筑结构。阶段三稳定策略1200局以后。AI学会了瞄准上方悬空的木箱子来砸底下的猪也会优先攻击薄弱支撑点。平均得分趋于平稳损失值小幅波动不再大幅下降。如果继续训练过久反而可能过拟合到特定关卡配置这时候就应该停下来了。5.3 提升训练效率的几个手段如果你不想等训练跑一整晚可以试三个提速技巧用灰度图而不是RGB图输入内存占用和计算量直接降到原来的三分之一用多进程并行跑多个模拟器同时采样我的双核笔记本挂4个模拟器后采集速度提升了接近3倍但注意游戏模拟器本身要轻量化把游戏窗口缩小但保持比例不变这样截图像素数减小网络输入尺寸虽然不变但每次预处理截图的时间会减少6. 常见问题与排查技巧实录6.1 训练了2000局分数还是平的这是最常见的情况。如果你看到奖励曲线一直贴地飞行多半不是模型坏了而是“环境—奖励—动作”这个链路里某个环节出了问题。现象可能原因排查方向损失值很小但分数低动作输出基本收敛到了一个固定值看ε是否衰减到接近0检查经验池是否被同策略样本污染损失值震荡剧烈学习率过高或奖励没有缩放降到1e-4检查奖励值是否过大空枪率极高动作空间太大或奖励里没有碰撞惩罚减小力度档位检查经验池里碰撞样本比例分数偶尔暴增但平均很低环境随机性大一次好结果被放大评估时多跑几个episode取平均去掉最高和最低我遇到过的坑是环境返回的状态坐标偶尔是0导致截图全黑训练曲线瞬间崩坏。排查方法是在训练循环里随机打印几个state的像素值均值如果突然变成0或很小说明采样环节出了问题。6.2 DQN的Q值过估计问题基础DQN对动作Q值的估计常常偏高特别是30°角配合高力度这种偶尔能打出惊人伤害的动作。这个动作明明是运气好才成功网络却记住了这个Q值后续不断给它更高的评价形成一种虚假的局部最优。我的解决方案是换成Double DQN改动很小用主网络选择最优动作用目标网络计算这个动作的Q值两个网络分开用可以有效降低过估计的影响。代码上只改一行把target_q self.target_net(next_state).max(axis1)改成best_action self.policy_net(next_state).argmax(axis1) target_q self.target_net(next_state).gather(1, best_action.unsqueeze(1)).squeeze(1)换完之后训练稳定性明显好了一个档次同等的学习率下震荡更小。6.3 经验池“脏数据”问题经验回放池里存了各种时期的数据早期完全随机的动作和经验后期精致的策略混在一起模型可能会被早期垃圾数据带偏。我一开始经验池容量设成20万太大了到后面采样的还是早期完全乱弹的数据训练效果很差。解决办法不是单纯缩小容量而是做分层采样最近的5000条经验权重翻倍让模型优先学习最近状态下的经验远期的经验则作为泛化补充。这个改动让收敛速度提升了大概20%。6.4 训练崩溃与输出NaN训练到中途损失变成NaN是最让人头大的问题。原因基本出在梯度爆炸上。解决办法有两招第一是奖励值统一做归一化缩放把大几千的得分除以固定系数压到个位数级别第二是在优化器上加梯度裁剪optimizer tf.keras.optimizers.Adam(learning_rate1e-4, clipnorm1.0)加完clipnorm之后损失变成NaN的概率几乎降为零这个参数在训练稳定时不会有负面影响建议直接加上别犹豫。7. 写在最后的个人体会这个项目我前前后后改了三版最大的感触是强化学习的门道基本都在环境构建和奖励设计上模型结构反而是最标准化的部分。很多人一上来就研究最新论文里的SOTA架构但在愤怒的小鸟这种小场景里经典DQN加一点Double DQN的改进就完全够用真正决定成败的是你把游戏变成“可训练环境”时做的每一个小决策。后来我把这个项目改造成了一个入门强化学习的教学demo带着几个师弟师妹跑了一遍。每次看它从乱弹到学会看角度、算抛物线还是会生出一种“老父亲看孩子长大”的感觉。如果你也正在折腾类似的强化学习项目建议从简化版开始先把整个闭环跑通再慢慢加地图复杂度。训练过程中什么奇怪的bug都可能遇到常回来翻翻这套调参经验能少走不少弯路。本文还有配套的精品资源点击获取