Краткое руководство по определению проблемы с несколькими агентами и ее решению с помощью мощных библиотек обучения с подкреплением

Мультиагентные системы есть везде. От летающей стаи птиц и волчьей стаи, охотящейся на оленей, до людей, управляющих автомобилями и торгующих акциями. Эти реальные проблемы познания связаны с взаимодействием нескольких интеллектуальных агентов друг с другом. Но что побуждает нас их изучать? Может, любопытство?

Если бы только мы могли имитировать сложное групповое поведение с помощью искусственного интеллекта.

И здесь на помощь приходит многоагентное обучение с подкреплением (MARL). В то время как в настройке обучения с подкреплением (RL) с одним агентом состояние среды изменяется из-за действия одного агента, в MARL переход состояния происходит на основе совместное действие нескольких агентов. Формально это расширение классической структуры Марковского процесса принятия решений (MDP) для включения нескольких агентов и представлено с помощью стохастической игры (SG). Обычно многоагентная система на основе MARL разрабатывается с учетом следующих характеристик:

  1. Автономность: агенты должны быть хотя бы частично независимыми и самосознательными.
  2. Локальный обзор: принятие решений зависит только от локальной наблюдаемости системы.
  3. Децентрализация: ни один агент не может контролировать всю систему

Такой дизайн отличается от более традиционного монолитного подхода к решению проблем, который может быть сильно ограничен с точки зрения масштабируемости и принятия сложных решений.

Ладно, хватит теории. Давайте запачкаем руки каким-нибудь кодом.

Игрушечная проблема с несколькими агентами

Рассмотрим пять фермеров. Каждый из них хочет набрать воду из ручья, чтобы орошать свои поля (выращивать больше мозолей!). Однако проблема в том, что если фермер, расположенный выше по течению, заберет слишком много воды, у фермеров, расположенных ниже по течению, возникнет нехватка воды. Давайте теперь попытаемся сформулировать проблему MARL, которая побуждает фермеров не быть жадными и сотрудничать.

Смотровая площадка

Каждый агент наблюдает количество воды, текущей в ручье в конкретный месяц. Для простоты мы случайным образом выбираем воду, текущую в ручье в определенный день, в диапазоне от 200 до 800 единиц объема. (Обратите внимание, что здесь мы позволяем агентам иметь глобальную наблюдаемость, потому что наблюдение довольно простое)

Пространство действий

Каждый агент выбирает долю воды для удаления из потока. Это соотношение составляет от 0 до 1. Обратите внимание, что выбор действия происходит в начале каждого эпизода. Вы можете рассматривать это как объявление, сделанное каждым фермером в начале серии. (Немного нереально, но это игрушечный пример!)

В этом месяце в ручье потечет 380 литров воды. Хм, думаю, для моего кукурузного поля хватит 110 литров. Заявляю, что в этом месяце я заберу 30% воды из ручья.

Что, если фактическое количество воды в потоке меньше, чем требуется (потому что вышестоящие агенты уже забрали воду!). В этом случае фермер забирает все, что осталось в потоке, и на систему налагается штраф (описанный в следующем разделе).

Награда

Больше воды означает лучший полив. Но вода сверх лимита может повредить посевы. Более того, у каждого фермера есть минимальная потребность в воде. Сначала поставим простую границу забора воды w для каждого агента как 0 ‹w‹ 200. Затем мы определяем вознаграждение от урожая как квадратичную функцию от забора воды с положительным вознаграждением от 0 до 200.

R(w) = -w² + 200w

Далее мы определяем штраф, пропорциональный количеству дефицита воды для каждого агента.

дефицит воды = потребность в воде - забор воды

Штраф = 100 * (дефицит воды)

Чтобы способствовать сотрудничеству, мы даем глобальное вознаграждение всем агентам, которое равно сумме индивидуальных вознаграждений и штрафов для каждого агента.

Реализация с использованием RLlib

Во-первых, нам нужно реализовать настраиваемую среду. В RLlib это делается путем наследования от aMultiAgentEnvclass. Краткое описание дизайна пользовательской среды выглядит следующим образом:

  1. В пользовательской среде должны быть определены resetи step функции. Если хотите, добавьте другие вспомогательные функции.
  2. Функция reset возвращает словарь наблюдений с ключами, являющимися идентификаторами агентов. Идентификаторы агентов могут быть любыми, уникальными для агентов (да!), Но должны быть согласованными для всех функций в определении среды.
  3. step функция принимает в качестве входных данных словарь действий с ключами, являющимися идентификаторами агентов (как и выше). Функция должна возвращать словарь observations, rewards, dones (логическое значение независимо от того, завершен эпизод или нет) и любые дополнительные info. Опять же, ключи для всех этих словарей - это идентификаторы агентов.
  4. dones словарь имеет дополнительный ключ __all__, который должен иметь значение True только тогда, когда все агенты завершили эпизод.
  5. Наконец, мощный, но довольно запутанный выбор дизайна. Не все агенты должны присутствовать в игре на любом временном шаге. Это требует, чтобы
  • action_dict, переданный в функциюstep, всегда содержит действия для наблюдений, возвращенных на предыдущем временном шаге.
  • observations, возвращаемый на любом временном шаге, не обязательно должен относиться к тем же агентам, для которых были получены действия.
  • Ключи для observations, rewards, dones и info должны быть одинаковыми.

Этот выбор конструкции позволяет разделить действие агента и вознаграждение, что полезно во многих сценариях с несколькими агентами.

А теперь, возвращаясь к нашей игре счастливых фермеров, настраиваемую среду можно определить примерно так.

Машинист поезда

RLlib нужна некоторая информация перед началом тяжелой тренировки. Это включает

  1. Регистрация пользовательской среды
def env_creator(_):
    return IrrigationEnv()
single_env = IrrigationEnv()
env_name = "IrrigationEnv"
register_env(env_name, env_creator)

2. Определение соответствия между агентами и политиками

obs_space = single_env.observation_space
act_space = single_env.action_space
num_agents = single_env.num_agents
def gen_policy():
    return (None, obs_space, act_space, {})
policy_graphs = {}
for i in range(num_agents):
    policy_graphs['agent-' + str(i)] = gen_policy()
def policy_mapping_fn(agent_id):
        return 'agent-' + str(agent_id)

3. Гиперпараметры и подробности конфигурации тренировки (для моей скромной настройки тренировки)

config={
    "log_level": "WARN",
    "num_workers": 3,
    "num_cpus_for_driver": 1,
    "num_cpus_per_worker": 1,
    "lr": 5e-3,
    "model":{"fcnet_hiddens": [8, 8]},
    "multiagent": {
        "policies": policy_graphs,
        "policy_mapping_fn": policy_mapping_fn,
    },
    "env": "IrrigationEnv"
}

4. Наконец, код обучающего водителя.

exp_name = 'more_corns_yey'
exp_dict = {
        'name': exp_name,
        'run_or_experiment': 'PG',
        "stop": {
            "training_iteration": 100
        },
        'checkpoint_freq': 20,
        "config": config,
}
ray.init()
tune.run(**exp_dict)

Вот и все, мощный Policy Gradient (PG) оптимизирует вашу систему в направлении социально оптимального и кооперативного поведения. Вы можете использовать Proximal Policy Optimization PPO вместо PG, чтобы улучшить результаты. Дополнительные варианты выбора алгоритма и подробности гиперпараметров доступны в документации RLlib.

Реализация с использованием Tensorforce

В отличие от RLlib, Tensorforce изначально не поддерживает многоагентный RL. Почему тогда мы хотим попробовать? Что ж, по моему личному опыту, если вы хотите реализовать сложные сетевые архитектуры для функций политик или вам нужен очень эффективный конвейер обучения для нескольких кластеров, RLlib действительно сияет. Но это кажется излишним, когда вам просто нужна простая многоагентная система, которая имеет несколько сложных межагентных взаимодействий. И здесь Tensorforce может быть очень кстати.

Как и tune.run RLlib, у Tensorforce тоже есть похожий API. Но мы не будем это обсуждать. Вместо этого мы сосредоточимся на act and observe рабочем процессе. Он очень гибкий и дает вам свободу разделять действия агентов, выполнение шагов среды и обновление внутренней модели. Достичь подобной автономности в RLlib относительно сложно. Хорошо, теперь вернемся к фрагментам кода.

Во-первых, нам нужна среда. Здесь нам не нужен какой-то конкретный формат. Единственное требование - мы должны иметь возможность получать начальные наблюдения при вызове reset и получать new observations, rewards, terminal states и любые дополнительные info при выполнении шага среды. Итак, для простоты давайте просто повторно используем ранее определенную среду.

Во-вторых, нам нужны агенты. Мы можем создать их с необходимыми спецификациями, используя метод Agent.from_spec. Создадим 5 таких. (Обратите внимание, что мы используем state_space и action_space из определения среды)

env = IrrigationEnv()
num_agents = env.num_agents
state_space = dict(type='float', shape=(1,))
action_space = dict(type='float', shape=(1,), min_value=0., max_value=1.)
config = dict(
            states=state_space,
            actions=action_space,
            network=[
                        dict(type='dense', size=8),
                        dict(type='dense', size=8),
                    ]
)
agent_list = []
for i in range(num_agents):
    agent_list.append(Agent.from_spec(spec='ppo_agent', kwargs=config))

Конфигурация агента может быть предоставлена ​​несколькими способами. Это минимальный пример, но читатель может обратиться к документации Tensorforce для получения более подробной информации.

И, наконец, нам понадобится обучающий код. Должен признаться, я написал это очень запутанно. Итак, вот основная схема рабочего процесса.

  1. Создайте пакет сред
env_batch = []
for i in range(batch_size):
    env_batch.append(IrrigationEnv())

2. Выполните цикл обучающих итераций и на каждой итерации сбрасывайте пакет сред, чтобы получить начальные наблюдения.

for _ in range(training_iterations):
    for b in range(batch_size):
        obs = env_batch[b].reset()

3. Переберите идентификаторы агента, для которых возвращаются наблюдения, и вызовите Agent.act для пакета наблюдений.

    for agent_id in obs:
        actions = agent_list[agent_id].act(states =       obs_batch[agent_id])

4. Переберите все среды в пакете и примените действия к каждой среде. Мы получаем новые наблюдения, награды, состояния терминала и дополнительную информацию.

    for b in range(batch_size):
        new_obs, rew, dones, info = env_batch[b].step(action_batch[b])

5. Наконец, для каждого агента, которому мы вызвали Agent.act, вызов Agent.model.observe с наградами и конечными состояниями, чтобы усвоить траектории опыта.

    for agent_id in new_obs:
        agent_list[agent_id].model.observe(reward = rew_batch[agent_id], terminal = done_batch[agent_id])

Остальное - всего лишь некоторая гимнастика кода для возможности удобного доступа к значениям для каждого агента и каждого элемента в пакете. Полный код для обучения

env_batch = []
for i in range(batch_size):
    env_batch.append(IrrigationEnv())
for _ in range(training_iterations):
    obs_batch = {i:[] for i in range(num_agents)}
    rew_batch = {i:[] for i in range(num_agents)}
    done_batch = {i:[] for i in range(num_agents)}
    action_batch = {b:{} for b in range(batch_size)}
    for b in range(batch_size):
        obs = env_batch[b].reset()
        for agent_id in range(num_agents):
            obs_batch[agent_id].append(obs[agent_id])
    for agent_id in obs:
        actions = agent_list[agent_id].act(states = obs_batch[agent_id])
        for b in range(batch_size):
            action_batch[b][agent_id] = actions[b]
    for b in range(batch_size):
        new_obs, rew, dones, info = env_batch[b].step(action_batch[b])
        for agent_id in obs:
            rew_batch[agent_id].append(rew[agent_id])
            done_batch[agent_id].append(dones[agent_id])
    for agent_id in new_obs:
        agent_list[agent_id].model.observe(reward = rew_batch[agent_id], terminal = done_batch[agent_id])
    
    print(np.mean(rew_batch[0]))

Теперь я должен сказать, что это не очень масштабируемый код. Конечно, некоторые улучшения эффективности могут быть достигнуты за счет распараллеливания циклов for и использования потоков. Однако я хотел продемонстрировать здесь легкость, с которой можно быстро прототипировать взаимодействия агентов в многоагентных системах. Более сложные механизмы, такие как межагентское обучение, консультирование или многоагентная связь, также легко реализовать с использованием этого рабочего процесса.

Заключение и загадки будущего

На данный момент Tensorforce и RLlib - замечательные библиотеки для обучения агентов RL. Однако они страдают от трудночитаемой и часто хаотичной документации. Также существует серьезная нехватка примеров передовых сценариев использования. Более того, помощь в Интернете ограничена, поскольку сообщество все еще невелико. Поэтому я решил написать серию блогов, в которых будет рассказано, как можно создавать интересные многоагентные системы с использованием этих библиотек. Я надеюсь, что это будет особенно полезно для тех, кто целыми днями пытается исправить то, что у них на уме. Дайте мне знать, если у вас есть предложения, комментарии или забавные идеи! Я также открыт для сотрудничества.

Да и весь код доступен в репозитории здесь. Cheerio!