让小鸟开始在意输赢:Flappy Bird AI 的奖励函数与试错机制

这是 Flappy Bird 强化学习系列的第二期配套博客。

第一期里,我们解决的是一个更基础的问题:小鸟如何把自己看到的世界表示成状态,并把这些状态映射到一张可查询的 Q-Table 里。但只做到这一步,AI 其实还不会“学习”。因为它虽然已经有了观察能力和一张空表,却仍然不知道什么结果值得追求,什么结果必须避免。

所以第二期真正要补上的,是强化学习里最关键的一根骨架:奖励(Reward)

读完这一篇,你应该能回答下面几个问题:

  • 为什么光有状态和动作,还不足以让 AI 变聪明?
  • 为什么奖励不是“可有可无的加分项”,而是学习本身的方向盘?
  • 当前项目里的奖励设计为什么不是单一的一条规则,而是分层的?
  • Python 里的 learn 函数,到底是怎么把“现实反馈”写进 Q-Table 的?

先看视频,再对照博客拆解

如果你还没看这期视频,建议先花几分钟把完整演示看一遍,再回来读这篇文章。视频负责建立直觉,博客负责把直觉拆成可复现的技术结构,两者配合起来,最适合真正把第二期的奖励机制吃透。


一、第一期解决了“看见什么”,第二期解决“为什么要这么做”

从建模顺序上说,第一期和第二期分别对应两个完全不同的问题。

第一期讨论的是:

  • 当前局面应该用哪些信息来表示?
  • 这些信息如何离散化成状态键?
  • 在某个状态下,智能体有哪些动作可选?

而第二期讨论的是:

  • 小鸟做完动作之后,环境要给它什么反馈?
  • 这个反馈怎样才能推动它朝正确方向收敛?
  • 反馈强弱不同,会把它训练成什么样子?

这两个层次经常被混在一起,但其实最好分开理解。状态解决的是“AI 看见了什么”,奖励解决的是“AI 应该在意什么”。

如果一个智能体能看懂世界,却没有反馈,它就像一个拿着地图却不知道目的地的人。它当然可以移动,但谈不上学习。


二、奖励到底是什么:它不是情绪,而是数值化的反馈信号

在视频里,我们会用“甜头”和“苦头”来类比奖励系统,这个比喻对于建立直觉很有效。但到了代码层面,你要更精确地理解它:

奖励不是情绪,不是表扬,也不是惩罚本身,而是环境在每一步之后返回给智能体的数值信号。

这个数值的作用只有一个:告诉智能体,刚刚那个动作从结果上看是更接近目标,还是更远离目标。

对于 Flappy Bird 这个项目来说,目标很明确:

  • 不要撞墙
  • 尽量活久一点
  • 尽量穿过更多水管
  • 飞行姿态尽量稳定,不要在离空隙很远的位置乱晃

所以奖励系统必须把这些目标转译成可计算的数字。

你可以把它理解成一套“价值记分板”:

  • 环境不是告诉 AI “你做错了”
  • 而是告诉它 “你刚才这个动作值 +300、值 +1、还是值 -100

智能体不理解语言,但它理解数值的高低差异。强化学习的本质,就是让智能体学会偏好那些长期累计回报更高的动作序列。


三、当前项目里的奖励设计,不是一条规则,而是三层反馈

如果你去看 Scratch 端的实现,会发现这套项目并不是简单地只在“过水管”时给一次奖励。它其实是一个分层结构。

核心逻辑大致是这样:

如果 < <碰到 (水管 v) ? > 或 < < (y 坐标) < [-170] > 或 < (y 坐标) > [170] > > > 那么
  将 [_奖励分 v] 设为 [-100]
否则
  如果 < (_奖励分) = [0] > 那么
    将 [_高度差 v] 设为 ([绝对值 v] ( (y 坐标) - (最近水管空隙Y) ))
    如果 < (_高度差) < [30] > 那么
      将 [_奖励分 v] 增加 [1]
    否则
      将 [_奖励分 v] 增加 ( (0) - ( 四舍五入 ( (_高度差) / [25] ) ) )

再配合 水管数据更新 里的通过奖励:

如果 <(_临时X) = [-145]> 那么
  将 [_奖励分 v] 设为 [300]

这意味着当前项目至少包含三层奖励信号。

1. 终止惩罚:撞墙或出界时给 -100

这是最强烈的负反馈。它的意义不是“让小鸟伤心”,而是把死亡这件事明确地标成高成本行为。

如果没有这条规则,智能体很难迅速形成“避障优先”的倾向。因为对它来说,撞墙和乱飞可能都只是普通状态转换,没有足够显著的代价差异。

2. 里程碑奖励:成功穿过水管时给 +300

这是主要目标的正反馈。它负责告诉智能体:

你刚才做的一整串动作,不只是没死,而且真正推进了任务进度。

这类奖励通常比较大,因为它对应的是任务中的关键事件,而不是过程中的细小调整。

3. 过程奖励:根据离空隙中心的高度差给局部反馈

这一层最容易被忽视,但在工程上非常重要。

  • 如果高度差小于 30,给 +1
  • 如果高度差太大,就按距离大小给负分

它的作用是:即使还没到“穿过水管”这个时刻,环境也能提前告诉小鸟,它当前姿态是更合理还是更偏离目标。

这就是为什么这套奖励系统不是简单粗暴的“过了加分,死了扣分”,而是增加了一层对飞行姿态的连续反馈。


四、为什么不能只在过水管时奖励:这涉及“稀疏反馈”问题

很多初学者第一次写强化学习时,会很自然地想到一种最朴素的奖励方案:

  • 过水管时 +1
  • 死亡时 -1
  • 其他时刻 0

这并不是完全错误,但在 Flappy Bird 这样的项目里,它会让学习变得非常慢。

原因在于:反馈太稀疏了。

如果智能体大部分时间拿到的都是 0,那它很难区分“刚才那个动作略微更好”还是“其实完全没差”。尤其在训练初期,小鸟往往还没撑到第一个有效里程碑就已经撞死了。这时 Q-Table 能收到的信息太少,更新会很迟钝。

所以工程上常常会做一件事,叫奖励塑形(Reward Shaping)

奖励塑形的核心思想是:

在不改变最终目标的前提下,为中间过程加入一些合理的辅助反馈,让学习信号更密集。

在这个项目里,“离空隙近就略微加分,离空隙远就略微扣分”就是典型的奖励塑形。

它的意义不是替代最终目标,而是让智能体在还没过关之前,也能逐步感知“我是不是在朝正确方向靠近”。

这是第二期最值得真正理解的工程思想之一。很多强化学习项目是否能训起来,不取决于公式本身,而取决于奖励信号有没有设计得足够可学。


五、Python 里的 Q 值更新,到底在改什么

Scratch 负责给出奖励,真正把奖励写进记忆的是 Python 里的 learn 函数:

def learn(self, last_state, action, reward, next_state):
    if last_state is None or last_state == 'ERROR':
        return

    old_q = self.get_q_value(last_state, action)
    next_max_q = max([self.get_q_value(next_state, a) for a in self.action_space])
    new_q = old_q + self.alpha * (reward + self.gamma * next_max_q - old_q)
    self.q_table[last_state][action] = new_q

如果把它翻译成自然语言,这段代码在做的事其实很朴素:

  1. 先看一眼旧记忆里,这个动作原本值多少分。
  2. 再看一眼这次动作真正带来的即时奖励是多少。
  3. 顺便把“下一步看起来还有多大潜力”也估进去。
  4. 最后不要全盘推翻旧结论,而是按学习率缓慢修正。

用公式表示就是:

Q(s,a)Q(s,a)+α[r+γmaxaQ(s,a)Q(s,a)]Q(s, a) \leftarrow Q(s, a) + \alpha [r + \gamma \max_a Q(s', a) - Q(s, a)]

这里面最值得拆开理解的是中括号里的那一项:

r+γmaxaQ(s,a)Q(s,a)r + \gamma \max_a Q(s', a) - Q(s, a)

它表示的是:现实结果与旧预测之间的差值。

如果现实比预期好,这一项就是正的,Q 值会上涨;如果现实比预期差,这一项就是负的,Q 值会下降。

所以 Q-Learning 的本质不是“直接记住结果”,而是反复修正自己对结果的预测。


六、为什么奖励不会一次性把 Q 值改满:学习率决定了修正速度

当前实现里:

self.alpha = 0.2

这意味着,新的经验不会 100% 覆盖旧记忆,而只会按 20% 的强度去修正它。

举一个适合直觉理解的例子。

假设某个状态下:

  • 旧的 Q 值是 0
  • 这次拿到即时奖励 300
  • 下一状态的未来价值暂时先按 0 处理

那么一次更新之后:

new_q=0+0.2×(3000)=60new\_q = 0 + 0.2 \times (300 - 0) = 60

也就是说,小鸟不会因为一次好运气就马上断定“这里一定值 300 分”。它只会把判断往上修一点,从 0 提到 60

这种设计非常重要,因为训练早期有很多偶然性。

  • 一次成功可能只是误打误撞
  • 一次失败也可能只是前面累积误差的结果

如果学习率太大,Q 值会大起大落,很不稳定;如果学习率太小,学习速度又会很慢。当前项目把它设成 0.2,是一个比较稳妥的折中。


七、试错并不是乱撞,而是持续缩小“预期”和“现实”的偏差

视频里我们会用“被现实打脸”来解释试错,这个说法其实非常接近强化学习的本质。

因为所谓试错,不是无意义地胡乱尝试,而是每走一步都把“原来的判断”和“真实结果”做一次比较。

可以把这个过程理解成下面这条循环:

  1. 小鸟先根据当前 Q-Table 形成一个预期。
  2. 它做出动作。
  3. 环境返回奖励和下一个状态。
  4. 它发现现实与预期有偏差。
  5. 它修正表格中的对应值。

如果这种循环反复发生,Q-Table 里的值就会越来越接近真实环境下的长期收益。

这也是为什么强化学习看起来像“从错误中成长”。真正被不断修正的,并不是动作本身,而是动作背后的价值判断。


八、奖励设计错了,AI 会学歪:这叫 Reward Hacking

只要你真正开始做强化学习项目,就一定会遇到一个现实问题:AI 会严格优化你写下来的奖励,而不是你脑子里想象的目标。

这意味着,如果奖励函数写歪了,它会非常认真地学歪。

在这个 Flappy Bird 项目里,你完全可以做几个非常有教育意义的反例实验:

1. 把死亡惩罚从 -100 改成 0

结果通常会是:小鸟对死亡的厌恶显著降低,策略会变得更随意,因为“撞死”不再是一个明显坏结果。

2. 把死亡惩罚改成正数

那就更极端了。你实际上是在告诉智能体:死亡也是有利可图的。它很可能会演化出匪夷所思的“寻死策略”。

3. 把过水管奖励去掉

这会让智能体失去最重要的里程碑目标。哪怕它偶尔飞得还行,也很难稳定形成“推进进度”的偏好。

4. 只保留姿态奖励,不保留关键事件奖励

这容易把策略训练成“局部看起来合理,但整体目标感很弱”的样子。它也许会努力贴近空隙,却不一定真的学会持续过关。

这就是为什么奖励函数设计不是调味料,而是整个训练目标的数值化定义。写得不好,模型不是“学不会”,而是“学得非常认真,但方向完全错了”。


九、从第二期往第三期:为什么它明明会飞了,却还会突然犯傻

到这里,你已经能理解为什么小鸟会在奖励和惩罚中逐渐形成稳定策略。

但第二期讲完之后,通常会冒出一个新的问题:

如果它已经知道某些动作更值钱,为什么有时候还是会突然做出看起来很蠢的选择?

这就把我们自然引向第三期要解决的主题:探索(Exploration)与利用(Exploitation)

也就是说,智能体并不会永远死守当前最优动作。为了避免错过更好的路径,它会保留一部分“故意试试看”的空间。这也是为什么一个看起来已经很稳的小鸟,偶尔还会做出让人费解的动作。

所以从系列结构上看:

  • 第一期解决“如何表示世界”
  • 第二期解决“如何定义好坏”
  • 第三期解决“为什么它还会故意试错”

这三期合在一起,才构成一个比较完整的 Q-Learning 入门闭环。


十、你现在应该真正理解了什么

如果说第一期让小鸟拥有了“眼睛”和“笔记本”,那么第二期真正赋予它的是价值判断能力。

你现在应该能把训练链路理解成这样:

  1. Scratch 先根据当前局面计算奖励。
  2. Python 拿到这个奖励后,对上一步动作的 Q 值做修正。
  3. 修正的方向取决于现实反馈比旧预测更好还是更差。
  4. 多次重复之后,Q-Table 会逐步沉淀出更可靠的动作偏好。

所以强化学习并不是“AI 自己莫名其妙变聪明了”,而是你通过奖励函数,把任务目标翻译成了一个它能优化的数字系统。

这一步做对了,学习才真正开始。


建议你立刻动手做的三个实验:

  • 把死亡惩罚从 -100 改成 0,观察训练是否明显变慢。
  • 把通过水管的奖励从 300 改成 50,比较它对主目标的重视程度是否下降。
  • 保留死亡惩罚,但去掉“靠近空隙”的过程奖励,体会稀疏反馈会怎样拖慢学习。

🧠 知识自测:你真的理解奖励机制了吗?

为什么当前项目不只是设置‘过水管加分、死亡扣分’,还额外加入了根据高度差变化的过程奖励?

解析

过程奖励本质上是一种奖励塑形。它让小鸟在还没过水管之前,也能根据离空隙远近收到正负反馈,从而加快学习。

learn 函数里,Q 值更新的核心思想是什么?

解析

Q-Learning 不是直接覆盖旧值,而是根据‘现实结果与旧预测的偏差’做增量修正,这样更稳定,也更符合试错学习的过程。

如果你把死亡惩罚改成正数,最可能发生什么?

解析

智能体优化的是你写下来的奖励,而不是你心里默认的目标。把死亡写成正收益,本质上是在鼓励它去送命。

分享