You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ML-For-Beginners/translations/zh-HK/8-Reinforcement/2-Gym
localizeflow[bot] e4c1af4385
[zh-MO,zh-HK,zh-TW] chore(i18n): sync translations
1 month ago
..
solution chore(i18n): sync translations with latest source changes (chunk 5/6, 259 changes) 7 months ago
README.md [zh-MO,zh-HK,zh-TW] chore(i18n): sync translations 1 month ago
assignment.md chore(i18n): sync translations with latest source changes (chunk 5/6, 259 changes) 7 months ago
notebook.ipynb chore(i18n): sync translations with latest source changes (chunk 5/6, 259 changes) 7 months ago

README.md

賽車桿滑冰

我們在上一課解決的問題可能看起來像個玩具問題,似乎不太適用於現實生活場景。但事實並非如此,因為許多現實世界的問題也有類似的場景——包括下棋或圍棋。它們相似,因為我們也有一個有既定規則的棋盤和一個離散狀態

課前測驗

介紹

本課中,我們將把相同的 Q-Learning 原則應用於一個具有連續狀態的問題,也就是由一個或多個實數表示的狀態。我們將處理以下問題:

問題:如果 Peter 想要逃離狼群,他需要能夠更快移動。我們將看看 Peter 如何通過 Q-Learning 學會滑冰,尤其是如何保持平衡。

偉大的逃脫!

Peter 和朋友們發揮創意以逃離狼群!圖片來自 Jen Looper

我們將使用一個簡化版本的平衡問題,稱為 賽車桿CartPole 問題。在賽車桿世界中,我們有一個可左右移動的水平滑軌,目標是在滑軌上平衡一根垂直桿。

a cartpole

前置知識

本課中,我們將使用一個叫做 OpenAI Gym 的庫來模擬不同的環境。你可以在本地執行本課代碼(例如,從 Visual Studio Code 中),此時模擬會在新視窗中打開。若在網上執行代碼,你可能需要對代碼做一些調整,具體說明見這裡

OpenAI Gym

在上一課中,遊戲規則和狀態由我們自定義的 Board 類給出。這裡我們會使用一個特殊的模擬環境模擬平衡桿背後的物理。OpenAI 維護的一個非常流行的強化學習訓練模擬環境叫做 Gym,使用該 Gym我們可以創建從賽車桿模擬到街機遊戲等不同的環境

注意:你可以在 這裡 查看 OpenAI Gym 的其他可用環境。

首先,讓我們安裝 gym 並導入所需庫(代碼塊 1

import sys
!{sys.executable} -m pip install gym 

import gym
import matplotlib.pyplot as plt
import numpy as np
import random

練習 - 初始化賽車桿環境

若要處理賽車桿平衡問題,我們需要初始化相應環境。每個環境都關聯著:

  • 觀察空間,定義了我們從環境中接收到的信息結構。對賽車桿問題而言,我們獲取桿的位置、速度及其他一些數值。

  • 動作空間,定義可能的動作。在我們的例子中,動作空間是離散的,由兩個動作組成——。(代碼塊 2

  1. 初始化,輸入如下代碼:

    env = gym.make("CartPole-v1")
    print(env.action_space)
    print(env.observation_space)
    print(env.action_space.sample())
    

看看環境如何工作我們運行100步簡短模擬。每一步我們提供要採取的一個動作——這個模擬中我們只是從 action_space 隨機選擇一個動作。

  1. 執行下面的代碼,看看結果如何。

    請記住,最好在本地 Python 環境運行此代碼!(代碼塊 3

    env.reset()
    
    for i in range(100):
       env.render()
       env.step(env.action_space.sample())
    env.close()
    

    你應該會看到類似下圖的畫面:

    未平衡的賽車桿

  2. 在模擬過程中,我們需要獲取觀察值以決定如何行動。實際上,step 函數返回當前觀察、獎勵函數值以及完成標誌,用於指示是否應繼續模擬:(代碼塊 4

    env.reset()
    
    done = False
    while not done:
       env.render()
       obs, rew, done, info = env.step(env.action_space.sample())
       print(f"{obs} -> {rew}")
    env.close()
    

    你最終會在筆記本輸出中看到如下內容:

    [ 0.03403272 -0.24301182  0.02669811  0.2895829 ] -> 1.0
    [ 0.02917248 -0.04828055  0.03248977  0.00543839] -> 1.0
    [ 0.02820687  0.14636075  0.03259854 -0.27681916] -> 1.0
    [ 0.03113408  0.34100283  0.02706215 -0.55904489] -> 1.0
    [ 0.03795414  0.53573468  0.01588125 -0.84308041] -> 1.0
    ...
    [ 0.17299878  0.15868546 -0.20754175 -0.55975453] -> 1.0
    [ 0.17617249  0.35602306 -0.21873684 -0.90998894] -> 1.0
    

    每步模擬返回的觀察向量包含以下數值:

    • 賽車位置
    • 賽車速度
    • 桿的角度
    • 桿的旋轉速率
  3. 獲取這些數值的最小和最大值:(代碼塊 5

    print(env.observation_space.low)
    print(env.observation_space.high)
    

    你可能還會注意到每步模擬的獎勵值總是1。這是因為我們的目標是在最長時間內保持桿子相對垂直也就是盡可能長時間生存。

    事實上當我們在100次連續試驗中平均獲得195分時CartPole 模擬被認為已經解決。

狀態離散化

在 Q-Learning 中,我們需要構建 Q-Table定義在每個狀態下該怎麼做。為此我們需要狀態是離散的,更精確說,應包含有限數量的離散值。因此,我們需要將觀察值離散化,映射到一個有限的狀態集合。

有幾種方法可以做到這點:

  • 分箱。若知道某數值的區間,我們可以將該區間分成多個箱子,然後用該數值所在的箱位來替代原數值。這可以用 numpy 的 digitize 方法實現。如此一來,我們確切知道狀態大小,因為它取決於分箱數量。

另一種方法是利用線性插值將數值映射到有限區間(例如從 -20 到 20然後通過四捨五入轉換為整數。這種方法對狀態大小的控制較少尤其當我們不知道輸入數據的確切範圍時。比如本例中4個數值中有2個沒有明確上限或下限可能導致無限狀態數。

在本例中,我們會採用第二種方法。如你稍後可能會注意到,儘管沒有明確的界限,這些值很少超出某個有限區間,因此極端值的狀態會非常罕見。

  1. 這是根據模擬觀察結果返回4個整數元組的函數代碼塊 6

    def discretize(x):
        return tuple((x/np.array([0.25, 0.25, 0.01, 0.1])).astype(np.int))
    
  2. 我們也來看一下利用分箱的另一種離散化方法:(代碼塊 7

    def create_bins(i,num):
        return np.arange(num+1)*(i[1]-i[0])/num+i[0]
    
    print("Sample bins for interval (-5,5) with 10 bins\n",create_bins((-5,5),10))
    
    ints = [(-5,5),(-2,2),(-0.5,0.5),(-2,2)] # 每個參數的數值區間
    nbins = [20,20,10,10] # 每個參數的分箱數量
    bins = [create_bins(ints[i],nbins[i]) for i in range(4)]
    
    def discretize_bins(x):
        return tuple(np.digitize(x[i],bins[i]) for i in range(4))
    
  3. 現在運行一段短模擬,觀察這些離散化的環境值。你可以嘗試 discretizediscretize_bins,看看有何不同。

    discretize_bins 返回的是箱號從0開始。因此對接近0的輸入值它返回中間箱號10。而 discretize 不限制輸出範圍允許負值狀態值未偏移0 對應於 0。代碼塊 8

    env.reset()
    
    done = False
    while not done:
       #env.render()
       obs, rew, done, info = env.step(env.action_space.sample())
       #print(discretize_bins(obs))
       print(discretize(obs))
    env.close()
    

    如果你想看到環境如何執行,可取消註釋以 env.render 開頭的行。否則你也可以讓它在後台執行,速度更快。我們在 Q-Learning 過程中將使用這種“隱形”執行方式。

Q-Table 結構

在上一課中狀態是從0到8的簡單數字對因此我們用形狀為 8x8x2 的 numpy 張量方便地表示 Q-Table。如果我們使用分箱離散化狀態向量大小也是已知的因此我們可以用形狀為 20x20x10x10x2 的陣列表示狀態(其中 2 是動作空間維度,前幾維是對應每個觀察空間參數選定的分箱數)。

但有時候,觀察空間確切維度不明。對 discretize 函數而言,我們始終無法保證狀態會保持在一定限制內,因為原始值中有些沒有限定上下限。因此,我們將採用稍微不同的方法,用字典來表示 Q-Table。

  1. 使用 (state,action) 對作為字典的鍵,對應值即 Q-Table 的條目值。(代碼塊 9

    Q = {}
    actions = (0,1)
    
    def qvalues(state):
        return [Q.get((state,a),0) for a in actions]
    

    這裡我們還定義了 qvalues() 函數,返回給定狀態在所有可能動作下的 Q-Table 值列表。若 Q-Table 中沒有該條目,預設回傳 0。

開始 Q-Learning

現在我們準備教 Peter 平衡了!

  1. 首先,設置一些超參數:(代碼塊 10

    # 超參數
    alpha = 0.3
    gamma = 0.9
    epsilon = 0.90
    

    這裡,alpha學習率,定義每步調整 Q-Table 當前值的幅度。上一課我們從1開始然後在訓練過程中將 alpha 降到較低值。這裡為簡便起見,我們保持其恒定,後續你可以試試調整 alpha

    gamma折扣因子,表示我們對未來獎勵相比於當前獎勵的重視程度。

    epsilon探索/利用因子,決定我們應該優先探索還是利用。在算法中,我們會在 epsilon 百分比情況下根據 Q-Table 選擇下一步動作,剩餘情況隨機選擇動作。這可以讓我們探索搜索空間中未曾見過的區域。

    對平衡問題而言,隨機選擇動作(探索)就像隨機向錯誤方向用力,桿子必須學會如何從這些“錯誤”中恢復平衡。

優化算法

我們可以對上一課的算法作兩個改進:

  • 計算平均累積獎勵統計多次模擬。每5000次迭代我們輸出一次進度並對累積獎勵取平均值。若平均超過 195 分,我們可認為問題已被解決,甚至優於要求質量。

  • 記錄最大平均累積結果 Qmax,存儲對應該結果的 Q-Table。在訓練中你會發現平均累積結果有時會下降我們希望保留訓練中表現最好的 Q-Table。

  1. 收集每次模擬的累積獎勵到 rewards 向量,便於繪圖分析。(代碼塊 11

    def probs(v,eps=1e-4):
        v = v-v.min()+eps
        v = v/v.sum()
        return v
    
    Qmax = 0
    cum_rewards = []
    rewards = []
    for epoch in range(100000):
        obs = env.reset()
        done = False
        cum_reward=0
        # == 進行模擬 ==
        while not done:
            s = discretize(obs)
            if random.random()<epsilon:
                # 利用 - 根據 Q 表的機率選擇行動
                v = probs(np.array(qvalues(s)))
                a = random.choices(actions,weights=v)[0]
            else:
                # 探索 - 隨機選擇行動
                a = np.random.randint(env.action_space.n)
    
            obs, rew, done, info = env.step(a)
            cum_reward+=rew
            ns = discretize(obs)
            Q[(s,a)] = (1 - alpha) * Q.get((s,a),0) + alpha * (rew + gamma * max(qvalues(ns)))
        cum_rewards.append(cum_reward)
        rewards.append(cum_reward)
        # == 定期列印結果及計算平均獎賞 ==
        if epoch%5000==0:
            print(f"{epoch}: {np.average(cum_rewards)}, alpha={alpha}, epsilon={epsilon}")
            if np.average(cum_rewards) > Qmax:
                Qmax = np.average(cum_rewards)
                Qbest = Q
            cum_rewards=[]
    

你可能會注意到:

  • 接近目標。我們很接近在100次連續模擬中獲得195累積獎勵的目標甚至可能已經達成即使數字較小我們也無法判斷因為取的是5000次的平均值而正式判定只需100次。

  • 獎勵開始下降。有時獎勵開始下降,表示我們的 Q-Table 中的值開始被更差的值覆蓋,導致模型退化。

這一現象在繪製訓練過程中更為明顯。

繪製訓練進度

在訓練中,我們將每次迭代的累積獎勵值收集到了 rewards 向量中。當將其對迭代次數繪圖時:

plt.plot(rewards)

原始進度

從該圖無法看出什麼,因為隨機訓練過程的長度大不相同。為讓圖形更有意義,我們計算一系列實驗的移動平均,譬如 100 次。可使用 np.convolve 方便地實現:(代碼塊 12

def running_average(x,window):
    return np.convolve(x,np.ones(window)/window,mode='valid')

plt.plot(running_average(rewards,100))

訓練進度

變化超參數

為使學習更穩定,調整訓練過程中的部分超參數是合理的。特別是:

  • 學習率 alpha我們可以先設為接近1的值然後不斷減小。隨著時間推移Q-Table 中概率值會越來越好,我們只需稍作調整而非完全更新。

  • 增加 epsilon。我們可能想慢慢增大 epsilon,逐步減少探索、增多利用。大概先用較低 epsilon逐漸增大到接近1較為合理。

任務1: 嘗試調整超參數值看是否能達到更高累積獎勵。你能超過195嗎

任務 2:正式解決此問題,您需要在連續 100 次運行中取得平均獎勵 195。訓練期間測量該值並確保您已正式解決此問題

看到結果的實際表現

實際看看訓練好的模型如何表現會很有趣。讓我們運行模擬,並且遵循與訓練時相同的動作選擇策略,根據 Q-Table 中的機率分布進行抽樣:(程式碼區塊 13)

obs = env.reset()
done = False
while not done:
   s = discretize(obs)
   env.render()
   v = probs(np.array(qvalues(s)))
   a = random.choices(actions,weights=v)[0]
   obs,_,done,_ = env.step(a)
env.close()

你應該會見到類似以下的畫面:

一個平衡的倒立擺小車


🚀挑戰

任務 3:這裡我們使用的是最終版的 Q-Table但這可能不是最佳版本。請記得我們已將表現最佳的 Q-Table 儲存在 Qbest 變數中!嘗試用這個表現最佳的 Q-Table 來做同樣的範例,把 Qbest 複製到 Q,並看看你是否能注意到不同。

任務 4:這裡我們並非在每一步都選擇最佳動作,而是依照對應的機率分布進行抽樣。如果總是選擇 Q-Table 值最高的最佳動作會不會更合理?這可以透過 np.argmax 函數找出對應最高 Q-Table 值的動作編號來實現。請實作此策略,看看是否能改善平衡效果。

課後小測驗

作業

訓練一輛山地車

結論

我們現已學會如何透過提供定義遊戲目標狀態的獎勵函數,並讓智能體有機會聰明地探索搜尋空間,來訓練智能代理達成良好表現。我們成功地在離散及連續環境(但動作為離散)情境中應用了 Q-Learning 演算法。

同時也很重要去研究動作狀態也是連續的情況,還有當觀測空間更加複雜,例如來自 Atari 遊戲螢幕的影像。這些問題通常需要使用更強大的機器學習技術,如神經網絡,才能達到良好表現。這些更進階的主題將會是我們即將開設的進階 AI 課程內容。


免責聲明 本文件由 AI 翻譯服務 Co-op Translator 翻譯而成。雖然我們致力於確保準確性,但請注意,機器自動翻譯可能包含錯誤或不準確之處。原始文件的母語版本應被視為權威來源。對於重要資訊,建議進行專業人工翻譯。我們不對因使用本翻譯而產生的任何誤解或誤釋承擔責任。