本文首發於:行者AI
Qmix是多智能體強化學習中比較經典的算法之一,在VDN的基礎上做了一些改進,與VDN相比,在各個agent之間有着較大差異的環境中,表現的更好。
1. IQL與VDN
IQL(Independent Q_Learning),是一種比較暴力的解決問題的方法,每個agent都各自為政,自己學習自己的,沒有一個共同的目標。導致算法最終很難收斂。但是在實際一些問題中有不錯的表現。
VDN(Value-Decomposition Networks For CooperativeMulti-Agent Learning),每個agent都有自己的動作價值函數\(Q_a\),通過各自的價值函數的\(argmaxQ_a\),選取動作。\(Q_{tot}=\sum_{i=1}^nQ_i\),主要將系統的聯合\(Q_{tot}\)近似成為多個單智能體的\(Q_a\)函數的和。因為VDN的聯合函數的求和形式表現力有限,在有復雜組合的環境中,表現的很差。例如非線性環境。
2. Qmix
2.1 Qmix的算法思想
QMIX的主要目的:找到一個完全去中心化的策略,並沒有像VDN一樣完全分解,為了保持策略一致性,我們只需要是全局的\(Q_{tot}\)的執行與\(argmaxQa\)上的執行的結果相同:
要達到這個效果只需要滿足\(Q_{tot}\)對於任何一個\(Q_a\)都是單調遞增的:
不難看出,當\(\frac {\partial Q_{tot}}{\partial Q_a}=1\)的時候就是VDN,VDN是QMIX的一種特殊情況。
2.2 Qmix的網絡結構
QMIX的模型由兩大部分組成(三個網絡組成),一個是agent network,輸出單智能體的\(Q_i\)的函數,mixing network則是以\(Q_i\)作為輸入,輸出為聯合\(Q_{tot}\)。為了保證單調性,mixing network的網絡參數權重和偏置通過hypernetworks網絡計算得出,並且hypernetworks輸出的網絡權重都必須大於0,對偏置沒有要求。
3. 算法流程
-
初始化網絡eval_agent_network,eval_mixing_network這兩個網絡,分別將這兩個網絡的參數復制給target_agent_network,targent_mixing_network.初始化buffer \(D\),容量為\(M\),總迭代輪數\(T\),target_agent_network,targent_mixing_network兩個網絡參數更新頻率\(p\)。
-
\(for\) \(t=1\) $to $ \(T\) $ do$
1)初始化環境
2)獲取環境的\(S\),每個agent的觀察值\(O\),每個agent的\(avail\) \(action\),獎勵\(R\)。
3)\(for\) \(step=1\) \(to\) \(episode\)_\(limit\)
a)每個agent通過eval_agent_network獲取每個動作的\(Q\)值,eval_agent_network中有GRU循環層,需要記錄每個agnet的隱藏層,作為下次GRU隱藏的輸入。(一一對應)
b)通過計算出的Q值選擇動作。(1,通過最大的\(Q\)值進行選取動作,有小幾率采取隨機動作。2,將\(Q\)值再進行一次softmax,隨機采樣(sample)動作)
c)將\(S\),\(S_{next}\),每個agent的觀察值\(O\),每個agent的\(avail\) \(action\),每個agent的\(next\) \(avail\) \(action\),獎勵\(R\),選擇的動作\(u\),env是否結束\(terminated\),存入經驗池\(D\)。
d)\(if\) \(len(D)\) \(>=\) \(M\)
e)隨機從\(D\)中采樣一些數據,但是數據必須是不同的episode中的相同transition。因為在選動作時不僅需要輸入當前的inputs,還要給神經網絡輸入hidden_state,hidden_state和之前的經驗相關,因此就不能隨機抽取經驗進行學習。所以這里一次抽取多個episode,然后一次給神經網絡傳入每個episode的同一個位置的transition。
f)通過DQN相同的方式更新參數:
\(L(\theta)=\sum_{i=1}^b[(y_i^{tot}-Q_{tot}(\tau,u,s;\theta))^2]\)
g)\(if\) \(terminated\) == \(True\) \(and\) \(step\) \(<=\) \(episode\)_\(limit\)
h)\(for\) \(k=step\) \(to\) \(episode\)_\(limit\)
i)將不足的數據用0進行填充,保證數據的一致性。
j)\(S,avail\space\space action = S_{next},next \space avail \space action\)
k)\(if\) \(t\) % \(p==0\)
l)將eval_agent_network,eval_mixing_network網絡參數復制給target_agent_network,targent_mixing_network
4. 結果分析
關於QMIX的實驗結果,paper中先用一個比較簡單的tabular的游戲two-step game來證明了QMIX相較於VDN,更容易找到最優解,而VDN則會陷入局部最優解(具體內容有興趣的讀者可以查閱論文第5節)。 作者在星際爭霸2的多個任務下也進行了實驗測試,如下圖所示:
在paper中還提到了QMIX要比VDN更好的使聯合動作的優勢更加突出,下圖中,a表示VDN,b表示QMIX,agent1和agent2在學習之后,VDN中A和B的聯合最優動作的價值為6.51,而QMIX的聯合最優動作的價值為8.0。可以看出QMIX體現出的優勢聯合動作的價值更大。
5. 關鍵代碼
5.1 網絡結構
agent_network,采用循環神經網絡GRU,上一回合輸出的隱藏做為當前回合的輸入。
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))
# print(hidden_state.shape,"xxxxx")
h_in = hidden_state.reshape(-1, self.args.rnn_hidden_dim)
# print(h_in.shape,"uuu")
h = self.rnn(x, h_in)
q = self.fc2(h)
print(q)
print(h)
return q, h
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
5.2 動作選擇
這里采用的是epsilon的方式更新,有一定的幾率隨機選取動作。另一種方式,將Q值再進行一次softmax,然后采樣獲取動作。
def choose_action(self, obs, last_action, agent_num, avail_actions, epsilon, maven_z=None, evaluate=False):
inputs = obs.copy()
avail_actions_ind = np.nonzero(avail_actions)[0] # index of actions which can be choose
# transform agent_num to onehot vector
agent_id = np.zeros(self.n_agents)
agent_id[agent_num] = 1.
if self.args.last_action:
inputs = np.hstack((inputs, last_action))
if self.args.reuse_network:
inputs = np.hstack((inputs, agent_id))
# print("input:", inputs, last_action, agent_id)
# print("hidden:", self.policy.eval_hidden.shape)
hidden_state = self.policy.eval_hidden[:, agent_num, :]
# transform the shape of inputs from (42,) to (1,42)
inputs = torch.tensor(inputs, dtype=torch.float32).unsqueeze(0)
avail_actions = torch.tensor(avail_actions, dtype=torch.float32).unsqueeze(0)
if self.args.cuda:
inputs = inputs.cuda()
hidden_state = hidden_state.cuda()
# get q value
q_value, self.policy.eval_hidden[:, agent_num, :] = self.policy.eval_rnn(inputs, hidden_state)
# choose action from q value
q_value[avail_actions == 0.0] = - float("inf")
if np.random.uniform() < epsilon:
action = np.random.choice(avail_actions_ind) # action是一個整數
else:
action = torch.argmax(q_value)
return action
5.3 learn更新網絡參數
在learn的時候,抽取到的數據是四維的,四個維度分別為 1——第幾個episode 2——episode中第幾個transition 3——第幾個agent的數據 4——具體obs維度。因為在選動作時不僅需要輸入當前的inputs,還要給神經網絡輸入hidden_state,hidden_state和之前的經驗相關,因此就不能隨機抽取經驗進行學習。所以這里一次抽取多個episode,然后一次給神經網絡傳入每個episode的同一個位置的transition。
def learn(self, batch, max_episode_len, train_step, epsilon=None): # train_step表示是第幾次學習,用來控制更新target_net網絡的參數
episode_num = batch['o'].shape[0]
self.init_hidden(episode_num)
for key in batch.keys(): # 把batch里的數據轉化成tensor
if key == 'u':
batch[key] = torch.tensor(batch[key], dtype=torch.long)
else:
batch[key] = torch.tensor(batch[key], dtype=torch.float32)
s, s_next, u, r, avail_u, avail_u_next, terminated = batch['s'], batch['s_next'], batch['u'], \
batch['r'], batch['avail_u'], batch['avail_u_next'],\
batch['terminated']
mask = 1 - batch["padded"].float() # 用來把那些填充的經驗的TD-error置0,從而不讓它們影響到學習
# 得到每個agent對應的Q值,維度為(episode個數,max_episode_len, n_agents,n_actions)
q_evals, q_targets = self.get_q_values(batch, max_episode_len)
if self.args.cuda:
s = s.cuda()
u = u.cuda()
r = r.cuda()
s_next = s_next.cuda()
terminated = terminated.cuda()
mask = mask.cuda()
# 取每個agent動作對應的Q值,並且把最后不需要的一維去掉,因為最后一維只有一個值了
q_evals = torch.gather(q_evals, dim=3, index=u).squeeze(3)
# 得到target_q
q_targets[avail_u_next == 0.0] = - 9999999
q_targets = q_targets.max(dim=3)[0]
q_total_eval = self.eval_qmix_net(q_evals, s)
q_total_target = self.target_qmix_net(q_targets, s_next)
targets = r + self.args.gamma * q_total_target * (1 - terminated)
td_error = (q_total_eval - targets.detach())
masked_td_error = mask * td_error # 抹掉填充的經驗的td_error
# 不能直接用mean,因為還有許多經驗是沒用的,所以要求和再比真實的經驗數,才是真正的均值
loss = (masked_td_error ** 2).sum() / mask.sum()
self.optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(self.eval_parameters, self.args.grad_norm_clip)
self.optimizer.step()
if train_step > 0 and train_step % self.args.target_update_cycle == 0:
self.target_rnn.load_state_dict(self.eval_rnn.state_dict())
self.target_qmix_net.load_state_dict(self.eval_qmix_net.state_dict())
def _get_inputs(self, batch, transition_idx):
# 取出所有episode上該transition_idx的經驗,u_onehot要取出所有,因為要用到上一條
obs, obs_next, u_onehot = batch['o'][:, transition_idx], \
batch['o_next'][:, transition_idx], batch['u_onehot'][:]
episode_num = obs.shape[0]
inputs, inputs_next = [], []
inputs.append(obs)
inputs_next.append(obs_next)
# 給obs添加上一個動作、agent編號
if self.args.last_action:
if transition_idx == 0: # 如果是第一條經驗,就讓前一個動作為0向量
inputs.append(torch.zeros_like(u_onehot[:, transition_idx]))
else:
inputs.append(u_onehot[:, transition_idx - 1])
inputs_next.append(u_onehot[:, transition_idx])
if self.args.reuse_network:
# 因為當前的obs三維的數據,每一維分別代表(episode編號,agent編號,obs維度),直接在dim_1上添加對應的向量
# 即可,比如給agent_0后面加(1, 0, 0, 0, 0),表示5個agent中的0號。而agent_0的數據正好在第0行,那么需要加的
# agent編號恰好就是一個單位矩陣,即對角線為1,其余為0
inputs.append(torch.eye(self.args.n_agents).unsqueeze(0).expand(episode_num, -1, -1))
inputs_next.append(torch.eye(self.args.n_agents).unsqueeze(0).expand(episode_num, -1, -1))
# 要把obs中的三個拼起來,並且要把episode_num個episode、self.args.n_agents個agent的數據拼成40條(40,96)的數據,
# 因為這里所有agent共享一個神經網絡,每條數據中帶上了自己的編號,所以還是自己的數據
inputs = torch.cat([x.reshape(episode_num * self.args.n_agents, -1) for x in inputs], dim=1)
inputs_next = torch.cat([x.reshape(episode_num * self.args.n_agents, -1) for x in inputs_next], dim=1)
return inputs, inputs_next
def get_q_values(self, batch, max_episode_len):
episode_num = batch['o'].shape[0]
q_evals, q_targets = [], []
for transition_idx in range(max_episode_len):
inputs, inputs_next = self._get_inputs(batch, transition_idx) # 給obs加last_action、agent_id
if self.args.cuda:
inputs = inputs.cuda()
inputs_next = inputs_next.cuda()
self.eval_hidden = self.eval_hidden.cuda()
self.target_hidden = self.target_hidden.cuda()
q_eval, self.eval_hidden = self.eval_rnn(inputs, self.eval_hidden) # inputs維度為(40,96),得到的q_eval維度為(40,n_actions)
q_target, self.target_hidden = self.target_rnn(inputs_next, self.target_hidden)
# 把q_eval維度重新變回(8, 5,n_actions)
q_eval = q_eval.view(episode_num, self.n_agents, -1)
q_target = q_target.view(episode_num, self.n_agents, -1)
q_evals.append(q_eval)
q_targets.append(q_target)
# 得的q_eval和q_target是一個列表,列表里裝着max_episode_len個數組,數組的維度是(episode個數, n_agents,n_actions)
# 把該列表轉化成(episode個數,max_episode_len,n_agents,n_actions)的數組
q_evals = torch.stack(q_evals, dim=1)
q_targets = torch.stack(q_targets, dim=1)
return q_evals, q_targets
5.4 代碼總結
MARL的代碼相對來說要比single RL的代碼要復雜的多,筆者還是建議讀者看懂原理之后,自己手敲一遍,敲一遍之后會對一個算法的理解程度大大的提升。
6. 資料
QMIX: Monotonic Value Function Factorisation for Deep Multi-Agent Reinforcement Learning
PS:更多技術干貨,快關注【公眾號 | xingzhe_ai】,與行者一起討論吧!