跳到主要內容

002互動

從菜鳥到鳥霸王:讓 50 隻小鳥自己學會 Flappy Bird

給五十隻小鳥各一顆只有六個權重的大腦,加上優勝劣汰,多半二十幾代之內牠們就能連過一百根管子。然後打開那顆大腦,看牠到底學到了什麼。

發布
更新
閱讀時間
3 分鐘

上一篇的網路是用梯度下降訓練的:算出誤差,再把每個權重往對的方向推一點。這一篇完全不用。這裡沒有梯度、沒有反向傳播、沒有損失函數,唯一的回饋是「這隻鳥活了多久」。

下面的五十隻小鳥現在就在你的瀏覽器裡演化。先看牠們撞牆撞個幾代,再把速度調快。

fig 01/neuroevolution / flappy
世代
1
存活
50
通過管數
0
最高紀錄
0

領頭鳥的大腦

鳥的高度開口高度拍翅膀?0.50 不拍

正權重 負權重

每一代通過的管數(100 根就畢業,換下一代)

第一代還沒結束

速度
實心的那隻是目前還活著的領頭鳥,淡淡的一團是其他同伴。右邊是領頭鳥的整顆大腦:線的顏色是權重的正負,粗細是大小,節點越亮代表當下的輸出越強。下方的折線是每一代通過了幾根管子;飛過 100 根就算畢業,換下一代上場。

一顆只有六個數字的大腦

儀器右邊那張圖就是一隻鳥的全部:

  • 輸入兩個:鳥現在的高度,和下一個開口的高度。
  • 隱藏層兩個神經元。
  • 輸出一個:超過 0.5 就拍翅膀。

這個網路沒有偏置(bias),所以連線的權重總共六個:2 × 2 + 2 × 1。把神經網路和演化放在一起的做法叫神經進化(neuroevolution):不去算「每個權重該怎麼改」,而是養一群權重各不相同的個體,讓表現好的留下後代。

一代是怎麼變成下一代的

第一代的五十隻鳥,六個權重全是亂數,大部分一開場就撞牆。

簡化自 content/posts/ai-flappy-bird/components/world.ts
const population = new Population({
  shape: [2, 2, 1], // 2 個輸入、2 個隱藏神經元、1 個輸出
  size: 50,
});

全部死光之後,依照活了多久排名,產生下一代:

lib/ml/neuroevolution.ts
elitism: 0.2,    // 成績最好的 20% 原封不動留下
randomRate: 0.2, // 20% 是全新的亂數個體,免得整群卡在同一種做法
                 // 其餘 60% 是留下來的那些鳥的後代

「後代」的意思很單純。一隻鳥的基因就是那條六個數字的陣列;兩隻鳥生一隻小鳥,就是每個位置各有一半機率拿爸爸或媽媽的數字,然後每個數字有 10% 的機率再被隨機推動最多 ±0.5:

lib/ml/neuroevolution.ts
private breed(a: Float32Array, b: Float32Array): Float32Array {
  const child = Float32Array.from(a);
  for (let i = 0; i < child.length; i++) {
    if (rng() <= 0.5) child[i] = b[i]; // 交叉:一半機率換成另一個親代的數字
    if (rng() <= mutationRate) child[i] += rng() * mutationRange * 2 - mutationRange; // 突變
  }
  return child;
}

整個演算法就是這三段程式。

打開大腦看:牠學到的是一個減法

等牠們飛得不錯之後,看儀器右邊那張圖。演化成功的鳥腦通常長得很像:至少有一個隱藏神經元,接到它的「鳥的高度」和「開口高度」兩條線顏色相反,也就是一正一負。

一正一負相加,算的就是「我的高度減掉開口的高度」,也就是「我比開口高還是低」。低了就拍。這不是巧合:兩個輸入都是 0 到 1 之間的正數,兩個權重同號的神經元只量得出「兩者加起來有多大」,分不出誰高誰低。要比較,就得一正一負。

六個數字,學到的是一個減法。這個減法不在任何一行程式裡;它只是六個數字的所有組合當中,活得最久的那一種。

演化不保證成功

我用 12 組不同的隨機種子各跑了 60 代:10 組在 22 代之內畢業,1 組拖到第 43 代,還有 1 組整個族群卡在同一種壞習慣裡,60 代下來最多只過了 3 根管子。把種子加到 40 組,有 5 組沒學會。

這是演化式方法的通病。它沒有梯度告訴它「往哪邊走會更好」,只能靠運氣碰到更好的個體;如果整群都長得差不多,又剛好都不好,那 20% 的新血要很久才翻得了盤。如果你看到分數一直上不去,按「重新開始」換一批祖先就好。

它在這裡行得通,是因為問題夠小:兩個輸入、一個動作、六個數字。要搜尋的空間小到亂槍打鳥也打得中。參數一多,亂試碰到好解的機會就小得多,那通常是梯度下降的主場。

延伸閱讀


這篇文章最初於 2025 年 3 月發表在我的舊站。搬到這本筆記時互動元件用 TypeScript 整個重寫了;2026 年 9 月又重寫了文字。