自從 DeepMind 團隊提出 DQN,在 Atari 遊戲中表現出超人技巧,已經過去很長一段時間了。在此期間持續有新的方法被提出,不斷創造出 Deep RL 領域新 SOTA。然而,目前不論是同策略或異策略強化學習方法(此處僅比較無模型 RL),仍然需要強大的算力予以支撐。即便研究者已將 Atari 遊戲的解析度降低到 84x84,一般情況下仍然需要使用 GPU 進行策略的訓練。如今,來自 Ogma Intelligent Systems Corp. 的研究人員突破了這一限制。他們在稀疏預測性階層機制(Sparse Predictive Hierarchies)的基礎上,提出一種不需要反傳機制的策略搜尋框架,使得實時在樹莓派上訓練 Atari 遊戲的控制策略成為可能。下圖展示了使用該演算法在樹莓派上進行實時訓練的情形。可以看到,agent 學會了如何正確調整滑塊位置來接住小球,並發動進攻的策略。值得注意的是,觀測輸入為每一時刻產生的圖片。也就是說,該演算法做到了在樹莓派這樣算力較小的邊緣裝置上,實時學習從畫素到策略的對映關係。研究者開源了他們的 SPH 機制實現程式碼,並提供了相應 Python API。這是一個結合了動態系統應用數學、計算神經科學以及機器學習的擴充套件庫。他們的方法曾經還被 MIT 科技評論列為「Best of the Physics arXiv」。https://github.com/ogmacorp/OgmaNeo2研究者所提出的 SPH 機制不僅在 Pong 中表現良好,在連續策略領域也有不錯的表現。下圖分別是使用該演算法在 OpenAI gym 中 Lunar Lander 環境與 PyBullet 中四足機器人環境的訓練結果。在 Lunar Lander 環境中,訓練 1000 代之後,每個 episode 下 agent 取得了平均 100 分左右的 reward。如果訓練時間更長(3000 代以上),agent 的平均 reward 甚至能達到 200。在 PyBullet 的 Minitaur 環境中,agent 的訓練目標是在其自身能量限制條件下,跑得越快越好。從圖中可以看到,經過一段時間訓練,這個四足機器人學會了保持身體平衡與快速奔跑(雖然它的步態看起來不是那麼地自然)。看起來效果還是很棒的,機器之心也上手測試了一番。OgmaNeo2 用來學習 Pong 控制策略的整體框架如下圖所示。影像觀測值通過影像編碼器輸入兩層 exponential memory 結構中,計算結果輸出到之後的 RL 層產生相應動作策略。在安裝 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)
可得到如下結果。圖中橙色曲線為真實值,藍色曲線為預測值。可以看到,該方法以極小的誤差擬合了真實曲線。最後是該專案在 CartPole 任務中的表現。執行!python3 ./examples/CartPole.py,得到如下訓練結果。可以看到,其僅用 150 個 episode 左右即解決了 CartPole 任務。