Введение в многоагентное обучение с подкреплением Qmix

обучение с подкреплением

Эта статья была впервые опубликована на:Уокер ИИ

Qmix — один из наиболее классических алгоритмов мультиагентного обучения с подкреплением.В нем сделаны некоторые улучшения на основе VDN.По сравнению с VDN он лучше работает в средах с большими различиями между агентами.

1. IQL и VDN

IQL (Independent Q_Learning) — относительно насильственный метод решения проблем: каждый агент действует сам по себе, учится самостоятельно и не имеет общей цели. Это затрудняет сходимость алгоритма в конце. Но он имеет хорошую производительность в некоторых практических задачах.

VDN (сети декомпозиции ценности для совместного многоагентного обучения), каждый агент имеет свою собственную функцию ценности действия.QaQ_a, через соответствующие функции ценыargmaxQaargmaxQ_a, выберите действие.Qtot=i=1nQiQ_{tot}=\sum_{i=1}^nQ_i, которая в основном объединяет системуQtotQ_{tot}Приблизительно как несколько отдельных агентовQaQ_aсумма функций. Из-за ограниченной выраженности сумматорной формы суставной функции ВДН она плохо работает в условиях сложных сочетаний. например, нелинейные среды.

2. Qmix

2.1 Идея алгоритма Qmix

Основная цель QMIX: найти полностью децентрализованную стратегию, которая не является полностью декомпозированной, как VDN, чтобы сохранить согласованность стратегии, нам нужно только быть глобальнымQtotQ_{tot}реализация иargmaxQaargmaxQaРезультат выполнения такой же:

Для достижения этого эффекта необходимо только удовлетворитьQtotQ_{tot}для всехQaQ_aмонотонно возрастают:

Нетрудно заметить, что когдаQtotQa=1\frac {\partial Q_{tot}}{\partial Q_a}=1Когда это VDN, VDN является частным случаем QMIX.

2.2 Сетевая структура Qmix

Модель QMIX состоит из двух частей (трех сетей), одна из которых является сетью агентов, которая выводитQiQ_iФункция смесительной сети основана наQiQ_iна входе, на выходе есть объединениеQtotQ_{tot}. Для обеспечения монотонности веса параметров сети и смещения смешивающей сети рассчитываются сетью гиперсетей, а веса сети, выдаваемые гиперсетями, должны бытьБольше 0, нет требований к предвзятости.

3. Алгоритм процесса

  • Инициализируйте сеть eval_agent_network, eval_mixing_network и скопируйте параметры этих двух сетей в target_agent_network, target_mixing_network.DD, вместимостьMM, общее количество итерацийTT, target_agent_network, target_mixing_network частота обновления двух сетевых параметровpp.

  • forfor t=1t=1 toto TT do do

    ​ 1) Инициализировать среду

    ​ 2) Получить окружающую средуSS, наблюдения для каждого агентаOO, каждый агентavailavail actionaction,наградаRR.

    3)forfor step=1step=1 toto episodeepisode_limitlimit

    ​ а) Каждый агент получает значение каждого действия через eval_agent_networkQQзначение, в eval_agent_network есть рекуррентный слой GRU, и скрытый слой каждого agnet должен быть записан как вход для следующего скрытого GRU. (индивидуальная переписка)

    б) Действие выбирается по рассчитанному значению Q. (1, через самый большойQQзначение для выбора действий, есть небольшой шанс совершить случайные действия. 2, будетQQЗначение снова softmax, случайная выборка (выборка) действие)

    в) будетSS,SnextS_{next}, наблюдения для каждого агентаOO, каждый агентavailavail actionaction, каждый агентnextnext availavail actionaction,наградаRR, выбранное действиеuu, заканчивается ли envterminatedterminated, в пул опытаDD.

    г)ifif len(D)len(D) >=>= MM

    д) случайно изDDВыборка некоторых данных, но данные должны быть одним и тем же переходом в разных эпизодах. Потому что при выборе действия в нейросеть должны быть введены не только текущие входы, но и hidden_state, Hidden_state связан с предыдущим опытом, поэтому опыт не может быть выбран случайным образом для обучения. Итак, здесь одновременно извлекается несколько эпизодов, а затем переход одной и той же позиции каждого эпизода передается в нейронную сеть за раз.

    f) Обновите параметры так же, как DQN:

    L(θ)=i=1b[(yitotQtot(т,u,s;θ))2]L(\theta)=\sum_{i=1}^b[(y_i^{tot}-Q_{tot}(\tau,u,s;\theta))^2]

    г)ifif terminatedterminated == TrueTrue andand stepstep <=<= episodeepisode_limitlimit

    ч)forfor k=stepk=step toto episodeepisode_limitlimit

    i) Заполните недостающие данные 0, чтобы обеспечить согласованность данных.

    к)S,avail action=Snext,next avail actionS,avail\space\space action = S_{next},next \space avail \space action

    к)ifif tt % p==0p==0

    l) Скопируйте сетевые параметры eval_agent_network, eval_mixing_network в target_agent_network, target_mixing_network

4. Анализ результатов

Что касается экспериментальных результатов QMIX, то в статье сначала используется относительно простая табличная двухшаговая игра, чтобы доказать, что в QMIX легче найти оптимальное решение, чем в VDN, при этом VDN попадет в локальное оптимальное решение (конкретное содержание — Заинтересованные читатели). можно обратиться к разделу 5 документа). Автор также провел экспериментальные тесты под несколькими задачами StarCraft 2, как показано на следующем рисунке:

В документе также упоминается, что QMIX лучше, чем VDN, чтобы сделать преимущества совместного действия более заметными.На следующем рисунке a представляет VDN, b представляет QMIX, после того как агент 1 и агент 2 узнают, совместное оптимальное действие A и B в VDN равен 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 Выбор действия

Метод обновления здесь эпсилон, и есть определенная вероятность случайного выбора действий. Другой способ: снова софтмаксить значение Q, а затем семплировать, чтобы получить действие.

    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 научиться обновлять сетевые параметры

При обучении извлекаемые данные являются четырехмерными, и четыре измерения: 1 — первый эпизод 2 — первый переход в эпизоде ​​3 — данные первого агента 4 — конкретное измерение наблюдения. Потому что при выборе действия в нейросеть должны быть введены не только текущие входы, но и hidden_state, Hidden_state связан с предыдущим опытом, поэтому опыт не может быть выбран случайным образом для обучения. Итак, здесь одновременно извлекается несколько эпизодов, а затем переход одной и той же позиции каждого эпизода передается в нейронную сеть за раз.

 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 относительно сложнее, чем код одиночного RL. Автор по-прежнему рекомендует читателям набирать его вручную после понимания принципа.После повторного набора, понимание алгоритма значительно улучшится.

6. Информация

QMIX: Monotonic Value Function Factorisation for Deep Multi-Agent Reinforcement Learning


PS: Для получения дополнительной технической галантереи, пожалуйста, обратите внимание на [Публичный аккаунт | xingzhe_ai] и обсудите с ходоками!