简介:这套名为“人工智能玩游戏之-愤怒的小鸟 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会往什么方向优化,这也是整个项目我花时间最多的地方。
初始版本我用了最朴素的奖励:每次射击后,把得分增量作为奖励值。但实测效果不理想,因为大多数射击的得分增量是0,AI根本不知道哪个动作“好了一点点”,奖励信号太稀疏。
后来改成混合奖励方案:
- 分数增量奖励: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的经典配置,在愤怒的小鸟场景下实测效果不错:
| 层类型 | 卷积核 | 步长 | 输出尺寸 |
|---|---|---|---|
| 输入层 | - | - | 84x84x4 |
| Conv1 | 8x8 | 4 | 20x20x32 |
| Conv2 | 4x4 | 2 | 9x9x64 |
| Conv3 | 3x3 | 1 | 7x7x64 |
| 全连接层 | - | - | 512 |
| 输出层 | - | - | 48 |
三个卷积层的作用是逐级抽取空间特征:第一层提取边缘和轮廓,第二层提取局部结构如木头块儿、冰块形状,第三层综合出全局建筑布局。全连接层把空间特征映射到动作价值上。
有人问为什么不用更大更深的网络?深度太深在样本量不够时反而容易过拟合,把训练集里某个特定角度识别成“神角度”,换个关卡就不会玩了。浅层网络结构表达力虽然弱一点,但泛化能力更强,训练速度也快,用在这个小规模游戏上更合适。
4.2 超参数推荐表
超参数是我调了一周之后的最终版,直接给出表格照着用就行:
| 参数 | 取值 | 说明 |
|---|---|---|
| 优化器 | Adam | 比SGD收敛更快,适合小规模项目 |
| 学习率 | 1e-4 | 一开始用1e-3会梯度爆炸 |
| 折扣因子γ | 0.95 | 延迟奖励只有几步,不需要太高的折扣 |
| 经验池容量 | 50000 | 差不多是一万局的样本量 |
| Batch Size | 32 | 经验采样批次大小 |
| 目标网络更新频率 | 每500步 | 训练步数,不是episode数 |
| ε初始值 | 1.0 | 前1000步全随机探索 |
| ε终值 | 0.05 | 后期保底探索程度 |
| ε衰减步数 | 5000 | 从1.0线性衰减到0.05 |
4.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_actions=48, lr=1e-4, gamma=0.95, memory_size=50000, batch_size=32, target_update_freq=500 ) 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_episodes=5) print(f"Episode {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(axis=1)改成:
best_action = self.policy_net(next_state).argmax(axis=1) 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_rate=1e-4, clipnorm=1.0)加完clipnorm之后,损失变成NaN的概率几乎降为零,这个参数在训练稳定时不会有负面影响,建议直接加上别犹豫。
7. 写在最后的个人体会
这个项目我前前后后改了三版,最大的感触是强化学习的门道基本都在环境构建和奖励设计上,模型结构反而是最标准化的部分。很多人一上来就研究最新论文里的SOTA架构,但在愤怒的小鸟这种小场景里,经典DQN加一点Double DQN的改进就完全够用,真正决定成败的是你把游戏变成“可训练环境”时做的每一个小决策。
后来我把这个项目改造成了一个入门强化学习的教学demo,带着几个师弟师妹跑了一遍。每次看它从乱弹到学会看角度、算抛物线,还是会生出一种“老父亲看孩子长大”的感觉。如果你也正在折腾类似的强化学习项目,建议从简化版开始,先把整个闭环跑通,再慢慢加地图复杂度。训练过程中什么奇怪的bug都可能遇到,常回来翻翻这套调参经验,能少走不少弯路。
本文还有配套的精品资源,点击获取