強化學習的發展歷經了從單智慧體(single agent)到多智慧體(multi-agent)的蛻變,其中2019年是MARL演算法爆發的一年,這一年先後誕生了AlphaStar、OpenAI Five、Pluribus Poker等標誌性的成果。這些標誌性的成果是由一個個基礎演算法逐步發展而來的,今天我們將分別介紹MADDPG演算法(
2017,Ryan Lowe et al。
)和QMIX演算法(
2018,Tabish Rashid et al。
),這兩種演算法對多智慧體的解決思路都是基於一個普遍的框架:集中式訓練,分散式執行。
背景
在多智慧體情形,每個智慧體同單智慧體情形下類似,也是透過與環境互動的試錯過程來進行序貫決策,一個很大的不同之處則是在於智慧體之間會相互影響,因此環境狀態和回報函式其實是取決於所有智慧體的聯合動作(joint actions)。一個直觀的idea是對於其中某個智慧體而言,將其他智慧體的狀態動作等也建模成環境,然後運用單智慧體領域的演算法。然而,將單智慧體系統較為成功的演算法直接引入多智慧體系統時存在很多問題,並不適用,一個較為突出的問題則是,隨著訓練的進行,每個智慧體的策略都在變化,從任何一個獨立的智慧體看來環境都是非靜態的(non-stastionary),這與MDP的靜態性相違背。
多智慧體強化學習問題的一般建模方式是隨機博弈(Stochastic Game),或稱馬爾科夫博弈(Markov Game),用數學語言描述是
,其中
表示智慧體數目
表示所有智慧體共享的環境狀態集
表示第
個智慧體的動作集
表示狀態轉移機率,
表示第
個智慧體的立即回報
MADDPG
MADDPG,全稱是Multi-Agent Deep Deterministic Policy Gradient,顧名思義,它是DDPG演算法的多智慧體版,其與DDPG的區別,也即它的獨到核心思想之處在於利用全域性資訊學習Critic,利用區域性資訊學習Actor,具體到如下的示意圖:
MADDPG示意圖
對於每個智慧體而言,都需要維護自身的Actor網路
和Critic網路
,所謂的區域性資訊是指
的輸入只需要智慧體自身周圍觀察到的資訊
,而全域性資訊是指
的輸入除了包括環境所有的狀態
以外,還要輸入所有智慧體的動作
。反映到梯度公式,更新Actor網路時:
其中,
是多智慧體情形下的replay buffer,格式為
。
更新Critic網路時,
其中,
是一系列引數為
的目標網路。
除此之外,在MADDPG原文中還提出了推斷其他策略網路(Inferring Policies of Other Agents)和策略整合(Agents withPolicy Ensemble)兩種技巧,消融實驗結果表明,這兩種技巧對於模型效果的提升都有一定貢獻。
總結起來,演算法偽碼大致如下:
演算法的一系列實驗是在OpenAI自己開發的multi-agent particle環境下進行的,該環境中既有合作環境,也有競爭環境。以競爭環境Predator-Prey為例,紅色點是三個速度較慢的追捕者,它們需要透過合作抓到更快的綠色小球,每當紅色小球碰到綠色小球,紅色小球就會獲得獎勵,而綠色小球獲得懲罰。演算法最終實現的納什均衡狀態如下:
跑程式碼的話推薦該演算法一個比較簡潔的pytorch版本倉庫:
QMIX
同MADDPG一樣,QMIX也是集中式訓練,分散式執行的典範,不同之處在於MADDPG是基於DDPG的,不需要考慮如何從總的Q函式中提取分散式策略,而QMIX是基於Q-learning的。QMIX的提出主要是針對星際爭霸II遊戲,由於對一個聯合動作-狀態只有一個總獎勵值,而不是每個智慧體得到一個自己的獎勵值,因此只能用於合作環境,而不能用於競爭對抗環境。
具體來看,在部分可觀測的馬爾科夫環境中,定義單個智慧體的動作-觀測軌跡為
,聯合的動作-觀測軌跡為
,對於單個智慧體的區域性值函式為
,總的聯合動作值函式為
。
QMIX是對VDN(Value Decomposition Network)方法的改進,在VDN方法中,認為聯合動作值函式是區域性值函式的線性相加,即
,這樣簡單的線性相加很好地滿足了聯合值函式與區域性值函式之間的單調性,從而可以提取分散式策略,即:
但線性結構的值函式表達能力有限,在有的任務上表現也不盡人意。於是QMIX的作者提出用非線性結構來表徵聯合值函式,把上述的連加符號換成多層感知機,
,多層感知機的具體結構如圖(a):狀態向量
透過超網路(hybernetwork)和絕對值啟用函式生成非負的權重向量和偏重向量,從而保證單調性。在Mixing網路的輸入Q值向量是由DRQN網路輸出的(如圖c),這種帶LSTM/GRU的網路結構在處理部分可觀的馬爾科夫問題具有一定的優勢。
總的來說,QMIX演算法的創新性在於設計了特定的神經網路結構來保證聯合值函式與區域性值函式之間的單調性,我們再從程式碼的角度來看看每個部分的結構。
Mixing Network:
import
torch。nn
as
nn
import
torch
import
torch。nn。functional
as
F
class
QMixNet
(
nn
。
Module
):
def
__init__
(
self
,
args
):
super
(
QMixNet
,
self
)
。
__init__
()
self
。
args
=
args
# 因為生成的hyper_w1需要是一個矩陣,而pytorch神經網路只能輸出一個向量,
# 所以就先輸出長度為需要的 矩陣行*矩陣列 的向量,然後再轉化成矩陣
# args。n_agents是使用hyper_w1作為引數的網路的輸入維度,args。qmix_hidden_dim是網路隱藏層引數個數
# 從而經過hyper_w1得到(經驗條數,args。n_agents * args。qmix_hidden_dim)的矩陣
if
args
。
two_hyper_layers
:
self
。
hyper_w1
=
nn
。
Sequential
(
nn
。
Linear
(
args
。
state_shape
,
args
。
hyper_hidden_dim
),
nn
。
ReLU
(),
nn
。
Linear
(
args
。
hyper_hidden_dim
,
args
。
n_agents
*
args
。
qmix_hidden_dim
))
# 經過hyper_w2得到(經驗條數, 1)的矩陣
self
。
hyper_w2
=
nn
。
Sequential
(
nn
。
Linear
(
args
。
state_shape
,
args
。
hyper_hidden_dim
),
nn
。
ReLU
(),
nn
。
Linear
(
args
。
hyper_hidden_dim
,
args
。
qmix_hidden_dim
))
else
:
self
。
hyper_w1
=
nn
。
Linear
(
args
。
state_shape
,
args
。
n_agents
*
args
。
qmix_hidden_dim
)
# 經過hyper_w2得到(經驗條數, 1)的矩陣
self
。
hyper_w2
=
nn
。
Linear
(
args
。
state_shape
,
args
。
qmix_hidden_dim
*
1
)
# hyper_w1得到的(經驗條數,args。qmix_hidden_dim)矩陣需要同樣維度的hyper_b1
self
。
hyper_b1
=
nn
。
Linear
(
args
。
state_shape
,
args
。
qmix_hidden_dim
)
# hyper_w2得到的(經驗條數,1)的矩陣需要同樣維度的hyper_b1
self
。
hyper_b2
=
nn
。
Sequential
(
nn
。
Linear
(
args
。
state_shape
,
args
。
qmix_hidden_dim
),
nn
。
ReLU
(),
nn
。
Linear
(
args
。
qmix_hidden_dim
,
1
)
)
def
forward
(
self
,
q_values
,
states
):
# states的shape為(episode_num, max_episode_len, state_shape)
# 傳入的q_values是三維的,shape為(episode_num, max_episode_len, n_agents)
episode_num
=
q_values
。
size
(
0
)
q_values
=
q_values
。
view
(
-
1
,
1
,
self
。
args
。
n_agents
)
# (episode_num * max_episode_len, 1, n_agents) = (1920,1,5)
states
=
states
。
reshape
(
-
1
,
self
。
args
。
state_shape
)
# (episode_num * max_episode_len, state_shape)
w1
=
torch
。
abs
(
self
。
hyper_w1
(
states
))
# (1920, 160)
b1
=
self
。
hyper_b1
(
states
)
# (1920, 32)
w1
=
w1
。
view
(
-
1
,
self
。
args
。
n_agents
,
self
。
args
。
qmix_hidden_dim
)
# (1920, 5, 32)
b1
=
b1
。
view
(
-
1
,
1
,
self
。
args
。
qmix_hidden_dim
)
# (1920, 1, 32)
hidden
=
F
。
elu
(
torch
。
bmm
(
q_values
,
w1
)
+
b1
)
# (1920, 1, 32)
w2
=
torch
。
abs
(
self
。
hyper_w2
(
states
))
# (1920, 32)
b2
=
self
。
hyper_b2
(
states
)
# (1920, 1)
w2
=
w2
。
view
(
-
1
,
self
。
args
。
qmix_hidden_dim
,
1
)
# (1920, 32, 1)
b2
=
b2
。
view
(
-
1
,
1
,
1
)
# (1920, 1, 1)
q_total
=
torch
。
bmm
(
hidden
,
w2
)
+
b2
# (1920, 1, 1)
q_total
=
q_total
。
view
(
episode_num
,
-
1
,
1
)
# (32, 60, 1)
return
q_total
Agent Network:
import
torch。nn
as
nn
import
torch。nn。functional
as
f
class
RNN
(
nn
。
Module
):
# Because all the agents share the same network, input_shape=obs_shape+n_actions+n_agents
def
__init__
(
self
,
input_shape
,
args
):
super
(
RNN
,
self
)
。
__init__
()
self
。
args
=
args
self
。
fc1
=
nn
。
Linear
(
input_shape
,
args
。
rnn_hidden_dim
)
self
。
rnn
=
nn
。
GRUCell
(
args
。
rnn_hidden_dim
,
args
。
rnn_hidden_dim
)
self
。
fc2
=
nn
。
Linear
(
args
。
rnn_hidden_dim
,
args
。
n_actions
)
def
forward
(
self
,
obs
,
hidden_state
):
x
=
f
。
relu
(
self
。
fc1
(
obs
))
h_in
=
hidden_state
。
reshape
(
-
1
,
self
。
args
。
rnn_hidden_dim
)
h
=
self
。
rnn
(
x
,
h_in
)
q
=
self
。
fc2
(
h
)
return
q
,
h
程式碼是來自這個倉庫: