不需要藉助GPU的力量,用樹莓派也能實時訓練agent玩Atari

機器之心發表於2020-04-03
不需要藉助GPU的力量,用樹莓派也能實時訓練agent玩Atari
自從 DeepMind 團隊提出 DQN,在 Atari 遊戲中表現出超人技巧,已經過去很長一段時間了。在此期間持續有新的方法被提出,不斷創造出 Deep RL 領域新 SOTA。然而,目前不論是同策略或異策略強化學習方法(此處僅比較無模型 RL),仍然需要強大的算力予以支撐。即便研究者已將 Atari 遊戲的解析度降低到 84x84,一般情況下仍然需要使用 GPU 進行策略的訓練。

如今,來自 Ogma Intelligent Systems Corp. 的研究人員突破了這一限制。他們在稀疏預測性階層機制(Sparse Predictive Hierarchies)的基礎上,提出一種不需要反傳機制的策略搜尋框架,使得實時在樹莓派上訓練 Atari 遊戲的控制策略成為可能。下圖展示了使用該演算法在樹莓派上進行實時訓練的情形。

可以看到,agent 學會了如何正確調整滑塊位置來接住小球,並發動進攻的策略。值得注意的是,觀測輸入為每一時刻產生的圖片。

不需要藉助GPU的力量,用樹莓派也能實時訓練agent玩Atari

也就是說,該演算法做到了在樹莓派這樣算力較小的邊緣裝置上,實時學習從畫素到策略的對映關係。

研究者開源了他們的 SPH 機制實現程式碼,並提供了相應 Python API。這是一個結合了動態系統應用數學、計算神經科學以及機器學習的擴充套件庫。他們的方法曾經還被 MIT 科技評論列為「Best of the Physics arXiv」。

專案地址:
https://github.com/ogmacorp/OgmaNeo2

OgmaNeo2

研究者所提出的 SPH 機制不僅在 Pong 中表現良好,在連續策略領域也有不錯的表現。下圖分別是使用該演算法在 OpenAI gym 中 Lunar Lander 環境與 PyBullet 中四足機器人環境的訓練結果。

不需要藉助GPU的力量,用樹莓派也能實時訓練agent玩Atari

不需要藉助GPU的力量,用樹莓派也能實時訓練agent玩Atari


在 Lunar Lander 環境中,訓練 1000 代之後,每個 episode 下 agent 取得了平均 100 分左右的 reward。如果訓練時間更長(3000 代以上),agent 的平均 reward 甚至能達到 200。在 PyBullet 的 Minitaur 環境中,agent 的訓練目標是在其自身能量限制條件下,跑得越快越好。從圖中可以看到,經過一段時間訓練,這個四足機器人學會了保持身體平衡與快速奔跑(雖然它的步態看起來不是那麼地自然)。看起來效果還是很棒的,機器之心也上手測試了一番。

演算法框架

OgmaNeo2 用來學習 Pong 控制策略的整體框架如下圖所示。影像觀測值通過影像編碼器輸入兩層 exponential memory 結構中,計算結果輸出到之後的 RL 層產生相應動作策略。

不需要藉助GPU的力量,用樹莓派也能實時訓練agent玩Atari

專案實測

在安裝 PyOgmaNeo2 之前,我們需要先編譯安裝其對應的 C++庫。將 OgmaNeo2 克隆到本地:

!git clone https://github.com/ogmacorp/OgmaNeo2.git

之後將工作目錄切換到 OgmaNeo2 下,並在其中建立一個名為 build 的資料夾,用於存放編譯過程產生的檔案。

import os
os.chdir('OgmaNeo2')
!mkdir build
os.chdir('build')

接下來我們對 OgmaNeo2 進行編譯。這裡值得注意的是,我們需要將-DBUILD_SHARED_LIBS=ON 命令傳入 cmake 中,這樣我們才能在之後的 PyOgmaNeo2 擴充套件庫裡使用它。


!cmake .. -DBUILD_SHARED_LIBS=ON
!make
!make install

當 OgmaNeo2 安裝成功後,安裝 SWIG v3 及 OgmaNeo2 的相應 Python 擴充套件庫:

!apt-get install swig3.0
os.chdir('/content')
!git clone https://github.com/ogmacorp/PyOgmaNeo2
os.chdir('PyOgmaNeo2')
!python3 setup.py install --user
接下來輸入 import pyogmaneo,如果沒有錯誤提示就說明已經成功安裝了 PyOgmaNeo2。

我們先用一個官方提供的時間序列迴歸來測試一下,在 notebook 中輸入:

import numpy as np
import pyogmaneo
import matplotlib.pyplot as plt
# Set the number of threads
pyogmaneo.ComputeSystem.setNumThreads(4)
# Create the compute system
cs = pyogmaneo.ComputeSystem()
# This defines the resolution of the input encoding - we are using a simple single column that represents a bounded scalar through a one-hot encoding. This value is the number of "bins"
inputColumnSize = 64
# The bounds of the scalar we are encoding (low, high)
bounds = (-1.0, 1.0)
# Define layer descriptors: Parameters of each layer upon creation
lds = []
for i in range(5): # Layers with exponential memory
    ld = pyogmaneo.LayerDesc()
    # Set the hidden (encoder) layer size: width x height x columnSize
    ld.hiddenSize = pyogmaneo.Int3(4, 4, 16)
    ld.ffRadius = 2 # Sparse coder radius onto visible layers
    ld.pRadius = 2 # Predictor radius onto sparse coder hidden layer (and feed back)
    ld.ticksPerUpdate = 2 # How many ticks before a layer updates (compared to previous layer) - clock speed for exponential memory
    ld.temporalHorizon = 2 # Memory horizon of the layer. Must be greater or equal to ticksPerUpdate, usually equal (minimum required)

    lds.append(ld)
# Create the hierarchy: Provided with input layer sizes (a single column in this case), and input types (a single predicted layer)
h = pyogmaneo.Hierarchy(cs, [ pyogmaneo.Int3(1, 1, inputColumnSize) ], [ pyogmaneo.inputTypePrediction ], lds)
# Present the wave sequence for some timesteps
iters = 2000
for t in range(iters):
    # The value to encode into the input column
    valueToEncode = np.sin(t * 0.02 * 2.0 * np.pi) * np.sin(t * 0.035 * 2.0 * np.pi + 0.45) # Some wavy line
    valueToEncodeBinned = int((valueToEncode - bounds[0]) / (bounds[1] - bounds[0]) * (inputColumnSize - 1) + 0.5)
    # Step the hierarchy given the inputs (just one here)
    h.step(cs, [ [ valueToEncodeBinned ] ], True) # True for enabling learning
    # Print progress
    if t % 100 == 0:
        print(t)
# Recall the sequence
ts = [] # Time step
vs = [] # Predicted value
trgs = [] # True value
for t2 in range(300):
    t = t2 + iters # Continue where previous sequence left off
    # New, continued value for comparison to what the hierarchy predicts
    valueToEncode = np.sin(t * 0.02 * 2.0 * np.pi) * np.sin(t * 0.035 * 2.0 * np.pi + 0.45) # Some wavy line
    # Bin the value into the column and write into the input buffer. We are simply rounding to the nearest integer location to "bin" the scalar into the column
    valueToEncodeBinned = int((valueToEncode - bounds[0]) / (bounds[1] - bounds[0]) * (inputColumnSize - 1) + 0.5)
    # Run off of own predictions with learning disabled
    h.step(cs, [ [ valueToEncodeBinned ] ], False) # Learning disabled
    predIndex = h.getPredictionCs(0)[0] # First (only in this case) input layer prediction

    # Decode value (de-bin)
    value = predIndex / float(inputColumnSize - 1) * (bounds[1] - bounds[0]) + bounds[0]
    # Append to plot data
    ts.append(t2)
    vs.append(value)
    trgs.append(valueToEncode)
    # Show predicted value
    print(value)
# Show plot
plt.plot(ts, vs, ts, trgs)

可得到如下結果。圖中橙色曲線為真實值,藍色曲線為預測值。可以看到,該方法以極小的誤差擬合了真實曲線。

不需要藉助GPU的力量,用樹莓派也能實時訓練agent玩Atari

最後是該專案在 CartPole 任務中的表現。執行!python3 ./examples/CartPole.py,得到如下訓練結果。可以看到,其僅用 150 個 episode 左右即解決了 CartPole 任務。

不需要藉助GPU的力量,用樹莓派也能實時訓練agent玩Atari

相關文章