1. 程式人生 > >用50行Python程式碼從零開始實現一個AI平衡小遊戲!

用50行Python程式碼從零開始實現一個AI平衡小遊戲!

用50行Python程式碼從零開始實現一個AI平衡小遊戲!

集智導讀:

本文會為大家展示機器學習專家 Mike Shi 如何用 50 行 Python 程式碼建立一個 AI,使用增強學習技術,玩耍一個保持杆子平衡的小遊戲。所用環境為標準的 OpenAI Gym,只使用 Numpy 來建立 agent。

學習Python中有不明白推薦加入交流群號:
    前面548中間377後面875
        群裡有志同道合的小夥伴,互幫互助,
    群裡有不錯的學習教程!

各位看官好,我(作者 Mike Shi——譯者注)將在本文教大家如何用 50 行 Python 程式碼,教會 AI 玩一個簡單的平衡遊戲。我們會用到標準的 OpenAI Gym 作為測試環境,僅用 Numpy 建立我們的 AI,別的不用。

這個小遊戲就是經典的 Cart Pole 任務,它是 OpenAI Gym 中一個經典的傳統增強學習任務。遊戲玩法如下方動圖所示,就是盡力保持這根杆子始終豎直向上。杆子由於重力原因,會出現傾斜,到了一定程度就會倒下,AI 的任務就是在此時向左或向右移動杆子,不讓它倒下。這就跟我們在手指尖上樹立一支鉛筆玩“金雞獨立”一樣,只不過我們這裡是個一維的簡單遊戲(但是還是很有挑戰性的)。

你可能好奇最終實現怎樣的結果,可以在檢視 demo:

	https:// repl.it/@MikeShi42/Cart Pole

用50行Python程式碼從零開始實現一個AI平衡小遊戲!

 

增強學習速覽

如果這是你第一次接觸機器學習或增強學習,別擔心,我下面介紹一些基礎知識,這樣你就可以瞭解本文使用的術語了:)。如果已經熟悉了,大可跳過這部分,直接看看編寫 AI 的部分。

增強學習(RL)是一個研究領域:教 agent(我們的演算法/機器)執行某些任務/動作,但明確告訴它該怎樣做。把它想象成一個嬰兒,以隨機的方式伸腿,如果寶寶偶然間走運站立起來,我們會給它一個糖果作為獎勵。同樣,Agent 的目標就是在其生命週期內得到最多的獎勵,而且我們會根據是否和要完成的任務相符來決定獎勵的型別。對於嬰兒站立的例子,站立時獎勵 1,否則為0。

增強學習 agent 的一個著名例子是 AlphaGo,其中的 agent 已經學會了如何玩圍棋以最大化其獎勵(贏得遊戲)。在本教程中,我們將建立一個 agent,或者說 AI,可以向左或向右移動小車,讓杆子保持平衡。

狀態

狀態是目前遊戲的樣子。我們通常處理遊戲的多種數字表示。在乒乓球比賽中,它可能是每個球拍的垂直位置和 x,y 座標和球的速度。在我們這個遊戲中,我們的狀態由 4 個數字組成:底部小車的位置,小車的速度,杆的位置(以角度表示)和杆的角速度。這 4 個數字都是給定的陣列(或向量)。這個很重要,理解狀態是一個數字陣列意味著我們可以對它進行一些數學運算來決定我們根據狀態採取什麼行動。

用50行Python程式碼從零開始實現一個AI平衡小遊戲!

 

策略

策略是一種函式,其輸入是遊戲的狀態(例如棋盤的位置,或小車和杆的位置),輸出 agent應該在該位置採取的動作(例如,將小車向左邊移動)。在 agent 採取我們選擇的操作後,遊戲將使用下一個狀態進行更新,我們會再次將其納入策略以做出決策。這種情況一直持續到遊戲結束。策略非常重要,也是我們一直追求的,因為代表了 agent 背後的決策能力。

點積

兩個陣列(向量)之間的點積簡單地將第一個陣列的每個元素乘以第二個陣列的對應元素,並將它們全部加在一起。假設我們想找到陣列 A 和 B 的點積,只需計算是 A [0] * B [0] + A [1] * B [1] ......我們將使用這種運算將狀態(一個數組)乘以另一個數組(我們的策略)。

建立我們的策略

為了完成這個推車平衡遊戲,我們希望讓我們的 agent(或者說 AI)學習策略贏得比賽或獲得最大獎勵。

對於我們今天要開發的 agent,我們將策略表示為 4 個數字的陣列,分別代表狀態的各個部分的“重要性”(小車位置,杆子的位置等)然後我們會計算狀態和策略陣列的點積,得到一個數字。根據數字是正數還是負數,我們將向左或向右推動小車。

如果這聽起來有點抽象,那麼我們選擇一個具體的例子,看看會發生什麼。

假設小車在遊戲中居中並且靜止,杆子向右傾斜且可能倒向右邊。它看起來像這樣:

用50行Python程式碼從零開始實現一個AI平衡小遊戲!

 

相關狀態可能如下所示:

用50行Python程式碼從零開始實現一個AI平衡小遊戲!

 

那麼狀態陣列將是 [0,0,0.2,0.05]。

從直覺上,我們要把小車推向右邊,將支桿拉直。我從訓練中得到了一個很好的策略,其策略資料如下:[ - 0.116,0.332,0.207 0.352]。我們快速計算一下,看看該策略會輸出怎樣的動作。

這裡,我們將狀態陣列 [0,0,0.2,0.05] 和上述策略陣列結合計算點積。如果數字是正數,我們將車推向右邊,如果數字是負數,我們向左推。

用50行Python程式碼從零開始實現一個AI平衡小遊戲!

 

結果為正,意味著策略會向右推動小車,符合我們的預期。

現在比較明顯了,我們需要 4 個像上面這樣的神奇數字來幫我們解決問題。那麼我們該如何獲得這些數字?如果我們只是隨機挑選它們會怎樣?AI 的效果會怎樣?我們來一起看程式碼!

啟動你的編輯器!

首先在repl.it 上開啟一個 Python 例項。Repl.it 能讓我們快速啟動大量不同程式設計環境的雲實例,並在任何地方都能訪問的強大雲 IDE 中編輯程式碼!

用50行Python程式碼從零開始實現一個AI平衡小遊戲!

 

安裝軟體包

我們首先安裝這個專案所需的兩個軟體包:numpy 幫助進行數值計算;OpenAI Gym 作為我們代理的模擬器。

用50行Python程式碼從零開始實現一個AI平衡小遊戲!

 

只需在編輯器左側的包搜尋工具中輸入 gym 和 numpy,然後單擊加號按鈕即可安裝包。

建立基礎框架

我們首先將我們剛剛安裝的兩個依賴項匯入到main.py 指令碼中,並設定一個新的 gym 環境:

import gym
import numpy as np
env = gym.make('CartPole-v1')

接下來,我們定義一個名為“play”的函式,為該函式提供一個環境和一個策略陣列,在環境中計算策略陣列並返回分數,以及每個時步的遊戲快照(用於觀察)。我們將使用分數來判斷策略的效果以及檢視每個時步的遊戲快照來判斷策略的表現。這樣我們就可以測試不同的策略,看看它們在遊戲中的表現如何!

首先我們理解函式的定義,然後將遊戲重置為開始狀態。

def play(env, policy):
 observation = env.reset()

接下來,我們將初始化一些變數以跟蹤遊戲是否已經結束,包括策略的總分以及遊戲中每個步驟的快照(供觀察)。

done = False
 score = 0
 observations = []

現在我們多次運行遊戲,直到 gym 告訴我們遊戲已經完成。

for _ in range(5000):
 observations += [observation.tolist()] # 記錄用於正則化的觀察值,並回放
 
 if done: # 如果模擬在最後一次迭代中結束,則退出迴圈
 break
 
 # 根據策略矩陣選擇一種行為
 outcome = np.dot(policy, observation)
 action = 1 if outcome > 0 else 0
 
 # 建立行為,記錄反饋
 observation, reward, done, info = env.step(action)
 score += reward
 return score, observations

上面的大部分程式碼主要是玩遊戲的過程以及記錄的結果。實際上,我們的策略程式碼只需要兩行:

outcome = np.dot(policy, observation)
 action = 1 if outcome > 0 else 0

我們在這裡所做的只是策略陣列和狀態陣列之間的點積運算,就像我們之前在具體例子中所示的那樣。然後我們根據結果是正還是負,選擇 1 或 0(左或右)的動作。

到目前為止,我們的main.py 應如下所示:

	import gym
import numpy as np
env = gym.make('CartPole-v1')
def play(env, policy):
 observation = env.reset()
 
 done = False
 score = 0
 observations = []
 
 for _ in range(5000):
 observations += [observation.tolist()] # 如果模擬在最後一次迭代中結束,則退出迴圈
 
 if done: # 如果模擬在最後一次迭代中結束,則退出迴圈
 break
 
 # 根據策略矩陣選擇一種行為
 outcome = np.dot(policy, observation)
 action = 1 if outcome > 0 else 0
 
 # 建立行為,記錄反饋
 observation, reward, done, info = env.step(action)
 score += reward
 return score, observations

現在,我們開始玩遊戲,尋找我們的最佳策略!

第一局遊戲

由於我們有了能夠玩遊戲的函式,並且能告訴我們的策略有多好,那麼下面就建立一些策略,看看它們的效果怎樣。

如果我們首先只想嘗試隨機策略呢?能達到怎樣的效果?我們使用 numpy 來生成我們的策略,它是一個 4 元素陣列或 1x4 矩陣。它會選擇 0 到 1 之間的 4 個數字作為我們的策略。

policy = np.random.rand(1,4)

根據該策略和我們上面建立的環境,我們可以用它們來玩遊戲,獲得一個分數。

score, observations = play(env, policy)
print('Policy Score', score)

點選執行,執行我們的指令碼,然後會輸出我們的策略得分:

用50行Python程式碼從零開始實現一個AI平衡小遊戲!

 

遊戲的最大得分是 500 分,你的策略有可能達不到這個水平。如果達到了,恭喜你!絕對是你的大日子!只是看一個數字並沒有特別大的意義。如果能看到我們的 agent 是如何玩遊戲的,那就太好了,下一步我們就會設定它!

檢視我們的agent

要檢視我們的 agent,我們會使用 Flask 設定一個輕量級伺服器,以便我們可以在瀏覽器中檢視代理的效能。Flask 是一個輕量級的 Python HTTP 伺服器框架,可以為我們的 HTML UI 和資料伺服。這部分我就一筆帶過了,因為渲染和 HTTP 伺服器背後的細節對訓練我們的 agent 並不重要。

我們首先將 Flask 安裝為 Python 包,就像我們在前面安裝 gym 和 Numpy 一樣。

用50行Python程式碼從零開始實現一個AI平衡小遊戲!

 

接著,在我們指令碼的底部,我們將建立一個 Flask 伺服器。它將在端點 / data 上顯示遊戲的每一幀的記錄,並在/上託管UI。

from flask import Flask
import json
app = Flask(__name__, static_folder='.')
@app.route("/data")
def data():
 return json.dumps(observations)
@app.route('/')
def root():
 return app.send_static_file('./index.html')
 
app.run(host='0.0.0.0', port='3000')

另外,我們需要新增兩個檔案。一個是專案的空白 Python 檔案。這是repl.it 如何檢測 repl 是處於 eval 模式還是專案模式的專用術語。只需使用新檔案按鈕新增空白 Python 指令碼即可。

之後我們還想建立一個用於渲染 UI 的 index.html。這裡不再深入講解,只需將此 index.html 上傳到你的repl.it 專案即可。

現在你應該有一個如下所示的專案目錄:

用50行Python程式碼從零開始實現一個AI平衡小遊戲!

 

現在有了這兩個新檔案,當我們執行 repl 時,它應該能演示我們的策略。有了這個,我們嘗試找到最佳策略!

用50行Python程式碼從零開始實現一個AI平衡小遊戲!

 

策略搜尋

在我們的第一局遊戲中,我們只是隨機選擇了一個策略,但是如果我們選擇了一批策略,並且只保留那個表現最好的策略呢?

我們回到釋出策略的部分,這次不是僅生成一個,而是編寫一個迴圈來生成多個策略,並跟蹤每個策略的執行情況,最終僅儲存最佳策略。

首先我們建立一個名為 max 的元組,它將儲存我們迄今為止看到的最佳策略的得分,觀察值和策略陣列。

max = (0, [], [])

接著我們會生成和評估 10 個策略,並將最優策略儲存在 max 中。

for _ in range(10):
 policy = np.random.rand(1,4)
 score, observations = play(env, policy)
 
 if score > max[0]:
 max = (score, observations, policy)
print('Max Score', max[0])

我們還要讓 /data 端點返回最優策略的回放。

該端點:

@app.route("/data")
def data():
return json.dumps(observations)

應該改為:

@app.route("/data")
def data():
return json.dumps(max[1])

你的main.py 應該如下所示:

	import gym
import numpy as np
env = gym.make('CartPole-v1')
def play(env, policy):
 observation = env.reset()
 
 done = False
 score = 0
 observations = []
 
 for _ in range(5000):
 observations += [observation.tolist()] 
 
 if done: 
 break
 
 outcome = np.dot(policy, observation)
 action = 1 if outcome > 0 else 0
 
 observation, reward, done, info = env.step(action)
 score += reward
 return score, observations
max = (0, [], [])
for _ in range(10):
 policy = np.random.rand(1,4)
 score, observations = play(env, policy)
 
 if score > max[0]:
 max = (score, observations, policy)
print('Max Score', max[0])
from flask import Flask
import json
app = Flask(__name__, static_folder='.')
@app.route("/data")
def data():
 return json.dumps(max[1])
@app.route('/')
def root():
 return app.send_static_file('./index.html')
 
app.run(host='0.0.0.0', port='3000')

如果我們現在執行 repl,應該會得到最多為 500 分的分數,如果沒有達到這個結果,那就再執行 repl 一遍。另外我們可以看到策略幾乎完美地讓推車上的杆子保持平衡。

不是那麼快

不過實際上或許沒有這麼好,因為我們在第一部分稍微有一點作弊。首先,我們只是在 0 到 1 的範圍內隨機建立了策略陣列。這恰好可行,但是如果我們修改一下運算子,就會看到 agent 出現災難性的失敗。你自己可以試試將 action = 1 if outcome > 0 else 0 改成 action = 1 if outcome < 0 else 0。

但是效果仍然不穩定,因為如果我們恰好選擇少於而不是大於 0,我們永遠找不到最優的策略。為了解決這個問題,我們實際上應該生成對負數同樣適用的策略。雖然這為我們的工作增加了難度,但我們再也不必通過將我們的特定演算法擬合特定遊戲來“作弊”了。不然,如果我們試圖在 OpenAIgym 以外的其他環境中執行演算法時,演算法肯定會失敗。

要做到這一點,我們不再使用 policy = np.random.rand(1,4),而是改為 policy = np.random.rand(1,4) - 0.5。這樣我們策略中的每個數字都在 -0.5 到 0.5 之間,而不是 0 到 1。但是因為這樣難度更高,我們還想搜尋更多的策略。在上面的 for 迴圈中,不是迭代 10 個策略,而是通過讓程式碼改為讀取 for _ in range(100): 來嘗試 100 個策略。此外也鼓勵大家嘗試首先只迭代 10 個策略,看看現在用負數來獲得好的策略的難度如何。

現在我們的main.py 應該如下所示:

import gym
import numpy as np
env = gym.make('CartPole-v1')
def play(env, policy):
 observation = env.reset()
 
 done = False
 score = 0
 observations = []
 
 for _ in range(5000):
 observations += [observation.tolist()] 
 
 if done: 
 break
 
 outcome = np.dot(policy, observation)
 action = 1 if outcome > 0 else 0
 
 observation, reward, done, info = env.step(action)
 score += reward
 return score, observations
max = (0, [], [])
# 修改接下來兩行!
for _ in range(100):
 policy = np.random.rand(1,4) - 0.5
 score, observations = play(env, policy)
 
 if score > max[0]:
 max = (score, observations, policy)
print('Max Score', max[0])
from flask import Flask
import json
app = Flask(__name__, static_folder='.')
@app.route("/data")
def data():
 return json.dumps(max[1])
@app.route('/')
def root():
 return app.send_static_file('./index.html')
 
app.run(host='0.0.0.0', port='3000')

如果現在執行 repl,無論我們使用的值是否大於或小於 0,我們仍然可以為遊戲找到一個好的策略。

但是等等,這還沒完!即使我們的策略可以執行一次就達到最高分 500,但每次都能做到嗎?當我們生成 100 個策略,並選擇出在單一執行中表現最佳的策略時,該策略可能只是走運而已,甚至它可能是一個非常糟糕的策略,只是恰好執行效果很好。這是因為遊戲本身具有隨機性因素(起始位置每次都不同),因此策略可能只適用於一個起始位置,換成其他起始位置就不行了。

因此,為了解決這個問題,我們需要評估策略在多次試驗中的表現。現在,我們使用之前找到的最優策略,看看它在 100 次試驗中的表現如何。

scores = []
for _ in range(100):
 score, _ = play(env, max[2])
 scores += [score]
 
print('Average Score (100 trials)', np.mean(scores))

這裡我們將該策略執行 100次,並且每次都記錄它的得分。然後我們使用 numpy 計算平均分數並將其列印到我們的終端。沒有嚴格的已釋出的“已解決”定義,但它應該只有少數幾個點。你可能會注意到最好的政策實際上可能實際上是低於平均水平。但是,我會把解決方案留給你決定!

當然,對於何為“最優”並沒有嚴格的定義,但是至少比最高分 500 來說不應太差。你可能注意到最優策略有時是低於平均水平的,但是最終的最優策略如何,還是要靠大家根據自己的實際情況來定奪。

結語

恭喜!至此我們成功建立了一個 AI,能夠很好地玩耍這個簡單的平衡遊戲。不過,仍然有很多需要改進的地方:

  • 找到一個“真正的”最優策略(每局遊戲都能表現良好)
  • 減少我們尋找最優策略的搜尋次數
  • 研究怎樣找到正確的策略,而不是隨機選擇它們
  • 嘗試其它開發環境