Flappy Bird with AI

1. 用class將所有遊戲物件獨立包裝

首先第一步是要修改遊戲, 將bird用class包裝起來,可以一次過在同一遊戲畫面,有後多鳥在遊玩。

例如下面的遊戲,我就修改成雙打,可以用spacebarw鍵,分別控制兩隻鳥。

flappy_bird.pyde:

stuffs.py:

2. 加入大腦和作簡單測試

Toy Neural NetworkMatrix.pynn.py複製到這個項目中,像下圖,你的項目中應該有4個頁面。

螢幕截圖 2024-03-31 下午4.29.00

下面的程式,是參考The Coding Train的Neuroevolution Flappy Bird,我將其轉成了Processing for Python版本,你可以參考一下他的影片。

Stuffs.py:

flappy_bird.pyde:

加入後,首先在stuffs.py中,一開始導入from nn import NeuralNetwork

之後在birdclass中,加入self.brain = NeuralNetwork(4, 4, 1),做一個4個輸入, 4個隱藏層和1個輸出的神經網路。

之後,在bird中加入:

首先要測試一下,NeuralNetwork的class是否真的能導入到這個program中,所以先固定開4個inputs給它,再在無訓練的情況下,用predict()產生output,只要output大於0.5jump()

最後,在主程式的draw()中,

在所有birds中,加係bird.think()。運行後,兩隻鳥會隨機不停上升或不停下降。

3. 輸入有意義的inputs

Stuffs.py:

在這段program中,首先在Bird class中,加入birdIsPass():

來判斷鳥是否通過了第一條水管。

再在上面的think()中,加入有意思的參數。

這裡加入了鳥本身的y座標,鳥前方水管的頂和底水管的座標,還有水管的x座標。

運行後,你可以看到,鳥是否可以通過了第一條水管,和上面幾個inputs的參數。

4. 製作一個世代

flappy_bird.pyde:

Stuffs.py:

GeneticAlgorithm.py:

 

第一步,先將原本在Stuff.py中,列印inputs的值都刪去,免得一次過列印太多東西。

之後,回到主程式,

在最上方加入:

導入新的一個叫GeneticAlgorithm的python檔案和一次過製作500隻鳥。

setup()中,將原本的birds = [Bird(), Bird()],轉成:

在主程中draw()中,將所有鳥的顯示和更新等,轉成:

加入指令,只要撞到水管或跌出畫面,就將這隻鳥,從birds中刪除,最後再觀察現時有多少隻鳥。在一次過500隻的情況下,總會有幾隻能夠捱得到不出畫面外。

如果全部鳥都被刪除,就重生下一個世代。

開一個叫GeneticAlgorithm.py的新tag,加入:

這個動作,只是重新製作新一代的人口。

5. 基因演算法(初始化、評估和選擇)

遺傳演算法的運作方式類似於生物進化的過程。它以一個稱為"基因型"的編碼來表示問題的解,並使用一組稱為"個體"的基因型來形成一個"族群"。每個個體都對應於問題的一個可能解。

遺傳演算法的運行過程通常包括以下步驟:

  1. 初始化:隨機生成一個初始個體族群。

  2. 適應度評估:對於每個個體,根據問題的目標函數計算其適應度,評估其解的品質。

  3. 選擇:根據適應度的大小,選擇一些優秀的個體作為下一代的父母。

  4. 交叉:將選擇的父母進行交叉操作,生成子代個體。

  5. 變異:對子代進行突變操作,引入一些隨機性,以增加搜索空間的探索能力。

  6. 更新族群:將子代個體與父代個體結合,形成新的族群。

  7. 重複執行步驟2至6,直到滿足停止條件(例如達到最大迭代次數或找到足夠好的解)。

flappy_bird.pyde:

Stuffs.py:

GeneticAlgorithm.py:

以上的程式碼,做了基因演算法的前三步。我們建立了一個500隻鳥的群組,為每一隻鳥計分,紀錄鳥生存了多久,然後我們根據每隻鳥的fitness去抽選這隻鳥是否能遺傳下去有下一代。

當我們運行這個程式後,會發現過了幾代後,幾乎所有的鳥都只會有同一個行為,500隻鳥會疊在一起,這是因為這些鳥只有根據分數決定下一代,但沒有做交叉基因,所以幾代後,所有的鳥只會有同一個單一基因,所以行為完全一模一樣。

6. 基因演算法(交叉基因)

為方便展示效果,程式做了較多修改,我將全部都貼上來。共有5個檔案。

flappy_bird_AI.pyde:

GeneticAlgorithm.py:

Matrix.py:

Stuffs.py:

nn.py:

螢幕截圖 2024-04-01 下午3.47.41

為方便和調試,我將遊戲的難度降底了,可以看到,去到第12代後,在沒有設定timeout的情況下,最終有20隻鳥有40000分以上。

這裡,我主要在Matrixclass中加入了crossover功能,讓矩陣能夠交換內容,之後也在GeneticAlgorithm.py中,將鳥交換基因,令到每一代都能跟隨上一代的父母交換基因特徵。

但是否每一次都能成功呢?當然不是,就算生物在進化時,有些物種不能適應環境,就算勉強繁演下去,也會全族滅亡,舉個例子,如果這500隻鳥也沒有跳過水管的能力,那麼它們的基因傳下去,也沒有這個能力的。就像近親繁殖一樣,如果找不到新的基因,太相似的基因一路繁演下去,就會變得很單一,一個可能是對這個水管十分熟識,整個族群都能拿到高分,一個可能是對環境十分不熟識,全部群眾都很低分。但就算是前者, 也會因為對環境十分熟悉,這時如果將水管的開口變窄,就會完全不適應而全族滅亡。為了應該這個問題,便需要在遺傳中,加入「基因變異(mutation)」。

7. 基因演算法(變異)

GeneticAlgorithm.py:

今次只有一個細節位有改變。

在這裡,我們加入了一個mutation_func,用來令神經網路改變。之後在原本的generate中,在基因交換後,加入child.brain.mutate(mutation_func)令基因變異。

這裡有2個數值得們我們關注:

  1. mutation_rate = 0.002: 是變異率,0.002大約是500隻當中,有一隻會有變異,這個其實已經相當高,人類的變異率,是108的級數,而細菌和病毒則較高,是106的級數。太高的變異率,會令好的基因很難維持下去。

  2. x + random.gauss(0, 0.05): 另一個是我們的變異函數,在發生變異時,有多大程度影響這個基因,如果太大的話,會令整個基因產生翻天覆地的變化,由貓變異做狗,但如果太少的話,對效果又不明顯。

8. 考考你

來到這裡,這個程式已經完成,但也有不少改良空間。

  1. 鳥的大腦,中間隱藏層只有4個神經源,在The Coding Train的例子中,神經網路不是我們的NeuralNetwork(4, 4, 1),即4個輸入變量,4層隱藏層和1個輸出,而是NeuralNetwork(5, 8, 2),他的例子有2個輸出,如果輸出1少於輸出2,則鳥就會跳,令神經網路的複雜度增加,你試試先將神經網路轉成NeuralNetwork(4, 16, 1),看看鳥有沒有更聰明。之後再試著變成NeuralNetwork(4, 8, 2)

  2. 在原例子中,神經網路是NeuralNetwork(5, 8, 2),因為輸入的考慮因素,還有一個是birdVec,試著將birdVec.y加入變為考慮: inputs.append( map(this.birdVec.y, -5, 5, 0, 1))

  3. 鳥的生命沒有限制,令成功生存的鳥一直生存下去,沒有機會變下一世代,不利基因遺傳,試設定一個liftcount,鳥在經過2000個frameCount後就會滅亡,還入下一個世代。