第五章 實現你的第一個學習代理-解決山地車的問題

海里的羊發表於2021-01-04

前言

幹得好,走到這一步!在前面的章節中,我們很好地介紹了OpenAI Gym,它的特性,以及如何在你自己的程式中安裝、配置和使用它。我們還討論了強化學習的基礎知識以及什麼是深度強化學習,並建立了PyTorch深度學習庫來開發深度強化學習應用程式。在本章中,您將開始開發您的第一個學習代理!你將開發一個智慧代理,它將學習如何解決山地車的問題。逐漸在接下來的章節中,我們將解決日益具有挑戰性的問題得到更舒適OpenAI健身發展中強化學習演算法解決問題。我們將開始本章通過了解山車的問題,這已成為一種流行在強化學習和社會最優控制問題。我們將開發從頭學習代理,然後訓練它解決山車問題使用Gym的山車環境。我們將終於看到代理的進展並簡要如何看待的方式我們可以改善劑用它來解決更復雜的問題。我們將在這一章討論的主題如下:

  • 瞭解山車問題
  • 實現基於代理的強化學習來解決山車問題
  • 在Gym中訓練強化學習代理
  • 測試代理的效能

理解山車問題

對於任何強化學習問題,無論我們使用何種學習演算法,有關該問題的兩個基本定義都是重要的。它們是狀態空間和動作空間的定義。我們在本書前面提到過,狀態和動作空間可以是離散的,也可以是連續的。通常,在大多數問題中,狀態空間由連續值組成,並表示為向量、矩陣或張量(多維矩陣)。與連續值問題和環境相比,具有離散動作空間的問題和環境相對容易。在這本書中,我們將為一些問題和環境開發混合了狀態空間和動作空間組合的學習演算法,這樣當你開始自己開發智慧代理和演算法時,你就可以輕鬆地處理任何此類變化。

讓我們首先通過高層次的描述來理解山地車問題,然後再看看山地車環境的狀態和行動空間。

山車問題和環境

在山地車Gym的環境中,一輛車是在一個一維的軌道上,位於兩座山之間。我們的目標是讓車靠右行駛上山;然而,即使以最高速度行駛,汽車的引擎也不夠強勁,無法上山。因此,要想取得成功,就必須反覆推波助瀾。簡而言之,山地車的問題就是把動力不足的車開到山頂。

在你實現你的代理演算法之前,它將極大地幫助你理解環境、問題、狀態和行動空間。我們如何找出運動室內山地車環境的狀態和活動空間?我們已經從第四章瞭解瞭如何做到這一點,探索健身房及其特點。我們編寫了一個名為get_observation_action_space的指令碼。它將列印環境的狀態、觀察和操作空間,其名稱將作為第一個引數傳遞給指令碼。讓我們用以下命令要求它列印MountainCar-v0環境的空格:

:~/rl_gym_book/ch4$ python get_observation_action_space.py 'MountainCar-v0'

上述命令將產生如下輸出:
在這裡插入圖片描述
從這個輸出,我們可以看到狀態和觀測空間是一個二維的盒子,而行動空間是三維的和離散的。

如果你想複習一下盒子和離散空間的含義,你可以快速翻到第4章,探索Gym及其特徵,我們在Gym部分討論了這些空間及其含義。理解它們是很重要的。

下表總結了狀態和動作空間型別、描述和允許值的範圍,供參考:
在這裡插入圖片描述
舉個例子,汽車從-0.6到-0.4的隨機位置出發,速度為零,目標是到達右邊的山頂,也就是0。5的位置。(從技術上講,汽車可以超過0.5,達到0.6,這也是考慮的。)在到達目標位置(0.5)之前,環境將在每一次步驟中傳送-1作為獎勵。環境會終止這一插曲。當汽車到達0.5位置或所走的步數達到200時,done變數將等於True。

從零開始實現Q-learning

在本節中,我們將逐步開始實現我們的智慧代理。我們將使用NumPy庫和OpenAI Gym庫中的MountainCar-V0環境來實現著名的Q-learning演算法。
讓我們回顧一下我們在第4章中使用的強化學習Gym鍋板程式碼,探索Gym及其特點,如下:

import gym
env = gym.make("Qbert-v0")
MAX_NUM_EPISODES = 10
MAX_STEPS_PER_EPISODE = 500
for episode in range(MAX_NUM_EPISODES):
    obs = env.reset()
    for step in range(MAX_STEPS_PER_EPISODE):
        env.render()
        action = env.action_space.sample()# Sample random action. This will be replaced by our agent's action when we start developing the agent algorithms
        next_state, reward, done, info = env.step(action) # Send the action to the environment and receive the next_state, reward and whether done or not
        obs = next_state

        if done is True:
            print("\n Episode #{} ended in {} steps.".format(episode, step+1))
            break

這段程式碼是開發我們的強化學習代理的一個很好的起點(也就是樣板!)。我們首先將環境名從Qbert-v0更改為MountainCar-v0。注意,在前面的指令碼中,我們設定了MAX_STEPS_PER_EPISODE。這是代理在情節結束之前可以採取的步驟或操作的數量。這在持續的、永久的或迴圈的環境中可能很有用,因為環境本身不會結束情節。這裡,我們為代理設定了一個限制,以避免無限迴圈。然而,OpenAI Gym中定義的大多數環境都有一個插曲終止條件,一旦滿足其中任何一個條件,env.step(…)函式返回的done變數將被設定為True。我們在上一節中看到,對於我們感興趣的山地車問題,如果汽車到達目標位置(0.5)或所走的步數達到200,環境將終止插曲。因此,我們可以進一步簡化這個樣板程式碼,使其像下面這樣用於Mountain Car環境:

import gym
env = gym.make("MountainCar-v0")
MAX_NUM_EPISODES = 5000

for episode in range(MAX_NUM_EPISODES):
    done = False
    obs = env.reset()
    total_reward = 0.0 # To keep track of the total reward obtained in each episode
    step = 0
    while not done:
        env.render()
        action = env.action_space.sample()# Sample random action. This will be replaced by our agent's action when we start developing the agent algorithms
        next_state, reward, done, info = env.step(action) # Send the action to the environment and receive the next_state, reward and whether done or not
        total_reward += reward
        step += 1
        obs = next_state

    print("\n Episode #{} ended in {} steps. total_reward={}".format(episode, step+1, total_reward))
env.close()

如果您執行前面的指令碼,您將看到山地車環境出現在一個新的視窗中,汽車隨機左右移動1000集。你還會在每個章節的末尾看到章節編號、所採取的步驟以及所獲得的總獎勵,如下圖所示:
在這裡插入圖片描述
示例輸出應該類似於下面的截圖:
在這裡插入圖片描述
您應該記得,在我們前面的小節中,代理每走一步就得到-1的獎勵,而MountainCar-v0環境將在200步後終止插曲;這就是為什麼你的代理有時會得到-200的總獎勵!畢竟,行為人沒有思考或從之前的行為中學習就採取了隨機行動。理想情況下,我們希望代理確定如何以最少的步數到達山頂(靠近旗子、接近、位於或超過位置0.5)。別擔心——我們將在本章的最後構建這樣一個智慧代理!
讓我們看一下Q-learning部分。

回顧Q-learning

在第二章,強化學習和深度強化學習中,我們討論了SARSA和q學習演算法。這兩種演算法都提供了一種更新動作值函式估計的系統方法。特別地,我們看到Q-learning是一種off-policy學習演算法,它根據agent的策略,將當前狀態和動作的動作值估計更新到後續狀態中可獲得的最大動作值。我們也看到Q-learning更新由以下公式給出:
在這裡插入圖片描述
在下一節中,我們將在Python中實現一個Q_Learner類,它實現這個學習更新規則以及其他必要的函式和方法。

使用Python和Numpy來實現Q-learning代理

讓我們通過實現Q_Learner類來開始實現Q-learning代理。這個類的主要方法如下:

  • init(self, env)
  • discretize(self, obs)
  • get_action(self, obs)
  • learn(self, obs, action, reward, next_obs)

稍後您將發現,這裡的方法是常見的,並且存在於我們將在本書中實現的幾乎所有代理中。這使得您很容易掌握它們,因為這些方法將會被反覆使用(經過一些修改)。

對於一般的代理實現來說,discretize()函式不是必需的,但是當狀態空間很大且連續時,最好將該空間離散為可計數的容器或值範圍,以簡化表示。這也減少了Q-learning演算法需要學習的值的數量,因為它現在只需要學習有限的值集,可以用表格格式或使用n維陣列來簡潔地表示,而不是複雜的函式。此外,用於最優控制的Q-learnig演算法對q值的表表示也能保證收斂。

定義超引數

在Q_Learner類宣告之前,我們將初始化幾個有用的超引數。下面是我們將在Q_Learner實現中使用的超引數:

  • EPSILON_MIN: 這是我們希望代理在執行貪心策略時使用的最小值。
  • MAX_NUM_EPISODES: 我們希望代理與環境互動的最大集數。
  • STEPS_PER_EPISODE: 這是每一集的步數。這可能是環境中每集允許的最大步驟數,也可能是我們想要基於時間預算限制的自定義值。允許每個插曲有更多的步驟意味著每個插曲可能需要更長的時間才能完成,在非終止的環境中,直到達到這個限制,環境才會被重置,即使代理被困在相同的位置。
  • ALPHA: 這是我們希望agent使用的學習速率。這是前一節列出的q學習更新方程中的alpha值。有些演算法會隨著訓練的進行而改變學習速率。
  • GAMMA: 這是代理人將用來計入未來獎勵的折扣係數。這個值對應於上一節Q-learning更新方程中的gamma值。
  • NUM_DISCRETE_BINS: 這是狀態空間將被離散到的值的箱數。對於山地車環境,我們將狀態空間離散化為30個箱子。你可以使用更高或更低的值。

請注意,MAX_NUM_EPISODES和STEPS_PER_EPISODE已經在本章前面的某個章節中介紹的樣板程式碼中定義了。

這些超引數在Python程式碼中像這樣定義,帶有一些初始值:

EPSILON_MIN = 0.005 
max_num_steps = MAX_NUM_EPISODES * STEPS_PER_EPISODE 
EPSILON_DECAY = 500 * EPSILON_MIN / max_num_steps 
ALPHA = 0.05 # Learning rate
GAMMA = 0.98 # Discount factor 
NUM_DISCRETE_BINS = 30 # Number of bins to Discretize each observation dim

實現Q_learner類的__init__方法

接下來,讓我們看看Q_Learner類的成員函式定義。init(self, env)函式接受環境的例項,env,作為輸入引數,初始化的尺寸/形狀觀測空間和操作空間,同時也決定了引數離散化基於NUM_DISCRETE_BINS我們組的觀測空間。init(self, env)函式也初始化函式Q NumPy陣列,基於離散觀測空間的形狀和動作空間維度。init(self, env)的實現很簡單,因為我們只初始化代理所需的值。這是我們的實現:

class Q_Learner(object):
    def __init__(self, env):
        self.obs_shape = env.observation_space.shape
        self.obs_high = env.observation_space.high
        self.obs_low = env.observation_space.low
        self.obs_bins = NUM_DISCRETE_BINS  # Number of bins to Discretize each observation dim
        self.bin_width = (self.obs_high - self.obs_low) / self.obs_bins
        self.action_shape = env.action_space.n
        # Create a multi-dimensional array (aka. Table) to represent the
        # Q-values
        self.Q = np.zeros((self.obs_bins + 1, self.obs_bins + 1,
                           self.action_shape))  # (51 x 51 x 3)
        self.alpha = ALPHA  # Learning rate
        self.gamma = GAMMA  # Discount factor
        self.epsilon = 1.0

實現Q_learner類的discretize方法

讓我們花點時間來理解我們是如何離散化觀測空間的。離散觀察空間(通常是度量空間)的最簡單且有效的方法是將值的範圍劃分為一組稱為bins的有限值。值的跨度/範圍由空間中每個維度的最大可能值和最小可能值之間的差給出。一旦我們計算跨度,我們可以用它除以我們決定的NUM_DISCRETE_BINS來獲得箱子的寬度。我們在__init__函式中計算了bin寬度,因為它不會隨著每一次新的觀察而改變。離散化(self, obs)函式接收每一個新的函式,應用離散化步驟在離散化空間中尋找觀測值所屬的物件。就像這樣做一樣簡單:

(obs - self.obs_low) / self.bin_width

我們希望它屬於任何一個bins(而不是介於兩者之間的某個位置);因此,我們將前面的程式碼轉換為一個integer:

((obs - self.obs_low) / self.bin_width).astype(int)

最後,我們將這個離散的觀察結果返回為一個元組。所有這些操作都可以在一行Python程式碼中編寫,像這樣:

def discretize(self, obs): 
	return tuple(((obs - self.obs_low) / self.bin_width).astype(int))

實現Q_learner的get_action方法

我們希望探員在觀察後採取行動。get_action(self, obs)是我們定義的函式,用於在obs中給定一個觀察結果來生成一個動作。最廣泛使用的動作選擇策略是貪心策略,它根據agent的估計以(高)概率為1-的方式採取最佳動作,並以(小)概率由epsilon給出的方式採取隨機動作。我們使用NumPy的random模組中的random()方法來實現epsilon-greedy策略,如下所示:

 def get_action(self, obs):
        discretized_obs = self.discretize(obs)
        # Epsilon-Greedy action selection
        if self.epsilon > EPSILON_MIN:
            self.epsilon -= EPSILON_DECAY
        if np.random.random() > self.epsilon:
            return np.argmax(self.Q[discretized_obs])
        else:  # Choose a random action
            return np.random.choice([a for a in range(self.action_shape)])

實現Q_learner類的學習方法

正如您可能已經猜到的,這是Q_Learner類最重要的方法,它可以神奇地學習q值,從而使代理能夠在一段時間內採取智慧行動!最好的部分是它實現起來並不複雜!它僅僅是我們之前看到的Q-learning更新方程的實現。當我說它很容易實現時,不要相信我?!好的,下面是學習函式的實現:

    def learn(self, obs, action, reward, next_obs):
        discretized_obs = self.discretize(obs)
        discretized_next_obs = self.discretize(next_obs)
        td_target = reward + self.gamma * np.max(self.Q[discretized_next_obs])
        td_error = td_target - self.Q[discretized_obs][action]
        self.Q[discretized_obs][action] += self.alpha * td_error

我們本可以在一行程式碼中編寫Q學習更新規則,像這樣:

self.Q[discretized_obs][action] += self.alpha * (reward + self.gamma * np.max(self.Q[discretized_next_obs]) - self.Q[discretized_obs][action])

但是,在單獨的一行中計算每一項會使它更容易閱讀和理解。

完整實現Q_learner類

如果我們把所有的方法實現放在一起,我們會得到這樣的程式碼片段:

import gym
import numpy as np

MAX_NUM_EPISODES = 50000
STEPS_PER_EPISODE = 200 #  This is specific to MountainCar. May change with env
EPSILON_MIN = 0.005
max_num_steps = MAX_NUM_EPISODES * STEPS_PER_EPISODE
EPSILON_DECAY = 500 * EPSILON_MIN / max_num_steps
ALPHA = 0.05  # Learning rate
GAMMA = 0.98  # Discount factor
NUM_DISCRETE_BINS = 30  # Number of bins to Discretize each observation dim


class Q_Learner(object):
    def __init__(self, env):
        self.obs_shape = env.observation_space.shape
        self.obs_high = env.observation_space.high
        self.obs_low = env.observation_space.low
        self.obs_bins = NUM_DISCRETE_BINS  # Number of bins to Discretize each observation dim
        self.bin_width = (self.obs_high - self.obs_low) / self.obs_bins
        self.action_shape = env.action_space.n
        # Create a multi-dimensional array (aka. Table) to represent the
        # Q-values
        self.Q = np.zeros((self.obs_bins + 1, self.obs_bins + 1,
                           self.action_shape))  # (51 x 51 x 3)
        self.alpha = ALPHA  # Learning rate
        self.gamma = GAMMA  # Discount factor
        self.epsilon = 1.0

    def discretize(self, obs):
        return tuple(((obs - self.obs_low) / self.bin_width).astype(int))

    def get_action(self, obs):
        discretized_obs = self.discretize(obs)
        # Epsilon-Greedy action selection
        if self.epsilon > EPSILON_MIN:
            self.epsilon -= EPSILON_DECAY
        if np.random.random() > self.epsilon:
            return np.argmax(self.Q[discretized_obs])
        else:  # Choose a random action
            return np.random.choice([a for a in range(self.action_shape)])

    def learn(self, obs, action, reward, next_obs):
        discretized_obs = self.discretize(obs)
        discretized_next_obs = self.discretize(next_obs)
        td_target = reward + self.gamma * np.max(self.Q[discretized_next_obs])
        td_error = td_target - self.Q[discretized_obs][action]
        self.Q[discretized_obs][action] += self.alpha * td_error

我們已經準備好智慧體了。你可能會問,我們下一步該做什麼?我們應該在Gym裡訓練智慧體!在下一節中,我們將介紹訓練過程。

在Gym中訓練強化學習代理

培訓Q-learning代理的過程對您來說可能已經很熟悉了,因為它有許多與我們之前使用的樣板程式碼相同的程式碼行,也有類似的結構。我們現在不再從環境的操作空間中選擇隨機操作,而是使用agent.get_action(obs)方法從代理中獲取操作。我們也會打電話給代理。將agent的動作傳送到環境並收到反饋後,學習(obs, action, reward, next_obs)方法。培訓功能如下:

def train(agent, env):
    best_reward = -float('inf')
    for episode in range(MAX_NUM_EPISODES):
        done = False
        obs = env.reset()
        total_reward = 0.0
        while not done:
            action = agent.get_action(obs)
            next_obs, reward, done, info = env.step(action)
            agent.learn(obs, action, reward, next_obs)
            obs = next_obs
            total_reward += reward
        if total_reward > best_reward:
            best_reward = total_reward
        print("Episode#:{} reward:{} best_reward:{} eps:{}".format(episode,
                                     total_reward, best_reward, agent.epsilon))
    # Return the trained policy
    return np.argmax(agent.Q, axis=2)

測試和記錄智慧體的效能

一旦我們讓智慧體在Gym訓練,我們希望能夠衡量他的學習情況。為了做到這一點,我們讓智慧體做了一個測試。就像在學校一樣!test(agent、env、policy)獲取代理物件、環境例項和代理的策略,以測試代理在環境中的效能,並返回一個完整插曲的總報酬。它類似於我們前面看到的train(agent, env)函式,但它不讓agent學習或更新其Q值估計:

def test(agent, env, policy):
    done = False
    obs = env.reset()
    total_reward = 0.0
    while not done:
        action = policy[agent.discretize(obs)]
        next_obs, reward, done, info = env.step(action)
        obs = next_obs
        total_reward += reward
    return total_reward

請注意,test(agent, env, policy)函式評估代理在一集中的表現,並返回代理在該集中獲得的總獎勵。我們想要衡量代理在幾集中的表現如何,以得到一個很好的衡量代理的實際表現。此外,Gym提供了一個稱為monitor的方便包裝函式,以視訊檔案的形式記錄agent的進度。下面的程式碼片段演示瞭如何測試和記錄代理在1,000集上的效能,並將記錄的代理的動作作為視訊檔案儲存在環境中的gym_monitor_path目錄中:

if __name__ == "__main__":
    env = gym.make('MountainCar-v0')
    agent = Q_Learner(env)
    learned_policy = train(agent, env)
    # Use the Gym Monitor wrapper to evalaute the agent and record video
    gym_monitor_path = "./gym_monitor_output"
    env = gym.wrappers.Monitor(env, gym_monitor_path, force=True)
    for _ in range(1000):
        test(agent, env, learned_policy)
    env.close()

簡單完整的Q-learner實現用來解決山車問題

在這一節中,我們將把整個程式碼放到一個Python指令碼中,以初始化環境,啟動代理的培訓過程,獲取訓練好的策略,測試代理的效能,並記錄它在環境中的行為!

#!/usr/bin/env/ python
"""
q_learner.py
An easy-to-follow script to train, test and evaluate a Q-learning agent on the Mountain Car
problem using the OpenAI Gym. |Praveen Palanisamy
# Chapter 5, Hands-on Intelligent Agents with OpenAI Gym, 2018
"""
import gym
import numpy as np

MAX_NUM_EPISODES = 2000
STEPS_PER_EPISODE = 200 #  This is specific to MountainCar. May change with env
EPSILON_MIN = 0.005
max_num_steps = MAX_NUM_EPISODES * STEPS_PER_EPISODE
EPSILON_DECAY = 500 * EPSILON_MIN / max_num_steps
ALPHA = 0.05  # Learning rate
GAMMA = 0.98  # Discount factor
NUM_DISCRETE_BINS = 30  # Number of bins to Discretize each observation dim


class Q_Learner(object):
    def __init__(self, env):
        self.obs_shape = env.observation_space.shape
        self.obs_high = env.observation_space.high
        self.obs_low = env.observation_space.low
        self.obs_bins = NUM_DISCRETE_BINS  # Number of bins to Discretize each observation dim
        self.bin_width = (self.obs_high - self.obs_low) / self.obs_bins
        self.action_shape = env.action_space.n
        # Create a multi-dimensional array (aka. Table) to represent the
        # Q-values
        self.Q = np.zeros((self.obs_bins + 1, self.obs_bins + 1,
                           self.action_shape))  # (51 x 51 x 3)
        self.alpha = ALPHA  # Learning rate
        self.gamma = GAMMA  # Discount factor
        self.epsilon = 1.0

    def discretize(self, obs):
        return tuple(((obs - self.obs_low) / self.bin_width).astype(int))

    def get_action(self, obs):
        discretized_obs = self.discretize(obs)
        # Epsilon-Greedy action selection
        if self.epsilon > EPSILON_MIN:
            self.epsilon -= EPSILON_DECAY
        if np.random.random() > self.epsilon:
            return np.argmax(self.Q[discretized_obs])
        else:  # Choose a random action
            return np.random.choice([a for a in range(self.action_shape)])

    def learn(self, obs, action, reward, next_obs):
        discretized_obs = self.discretize(obs)
        discretized_next_obs = self.discretize(next_obs)
        td_target = reward + self.gamma * np.max(self.Q[discretized_next_obs])
        td_error = td_target - self.Q[discretized_obs][action]
        self.Q[discretized_obs][action] += self.alpha * td_error

def train(agent, env):
    best_reward = -float('inf')
    for episode in range(MAX_NUM_EPISODES):
        done = False
        obs = env.reset()
        total_reward = 0.0
        while not done:
            action = agent.get_action(obs)
            next_obs, reward, done, info = env.step(action)
            agent.learn(obs, action, reward, next_obs)
            obs = next_obs
            total_reward += reward
        if total_reward > best_reward:
            best_reward = total_reward
        print("Episode#:{} reward:{} best_reward:{} eps:{}".format(episode,
                                     total_reward, best_reward, agent.epsilon))
    # Return the trained policy
    return np.argmax(agent.Q, axis=2)


def test(agent, env, policy):
    done = False
    obs = env.reset()
    total_reward = 0.0
    while not done:
        action = policy[agent.discretize(obs)]
        next_obs, reward, done, info = env.step(action)
        obs = next_obs
        total_reward += reward
    return total_reward


if __name__ == "__main__":
    env = gym.make('MountainCar-v0')
    agent = Q_Learner(env)
    learned_policy = train(agent, env)
    # Use the Gym Monitor wrapper to evalaute the agent and record video
    gym_monitor_path = "./gym_monitor_output"
    env = gym.wrappers.Monitor(env, gym_monitor_path, force=True)
    for _ in range(1000):
        test(agent, env, learned_policy)
    env.close()


如果你讓代理學習的時間足夠長,你就會看到代理不斷改進和學習,以越來越少的步驟到達山頂。

總結

在這一章裡我們學到了很多。更重要的是,我們實現了一個agent,它能在7分鐘左右的時間裡聰明地解決山地車的問題!

我們從瞭解著名的山地車問題開始,並觀察環境、觀察空間、狀態空間和獎勵是如何在健身房的山地車v0環境中設計的。我們重新訪問了上一章中使用的reinforcement learning Gym樣板程式碼,並對其進行了一些改進,這些改進也可以在本書的程式碼儲存庫中獲得。

然後我們為我們的Q-learning代理定義了超引數,並開始從頭開始實現Q-learning演算法。我們首先實現了代理的初始化函式來初始化代理的內部狀態變數,包括使用NumPy n維陣列的Q值表示。然後採用離散化方法對狀態空間進行離散化;get_action(…)方法基於貪心策略選擇操作;最後是learn(…)函式,它實現了Q-learning更新規則,形成了agent的核心。我們看到了它有多簡單。

我希望您在實現這個代理並觀看它在Gym解決山地車問題時玩得很開心!我們將在下一章進入高階方法來解決各種更有挑戰性的問題。

相關文章