[Перевод] Выстраиваем стабильное соединение для обучения с подкреплением на Python на моделях AnyLogic

Введение

Стремительное развитие глубокого обучения с подкреплением (deep reinforcement learning — DRL), представляющего собой комбинацию глубокого обучения (DL) и обучения с подкреплением (RL), привлекает все больше исследователей из самых разных сфер науки к применению DRL для решения задач в своих областях исследований. Благодаря способности глубокого обучения работать с непрерывным или сложным пространством состояний и способности обучения с подкреплением учиться методом проб и ошибок в сложной среде, DRL особенно хорошо подходит для решения задач, для которых не хватает хороших точных или эвристических методов в сложных средах. Поскольку решение большинства задач обучения с подкреплением требует чрезвычайно большого количества данных, большинство DRL (или RL)-агентов обучаются в симулированной среде. Основным выбором для обучения DRL-моделей, благодаря своей внушительной библиотеке инструментов машинного обучения, стал Python. Однако использовать Python как язык программирования для создания крупномасштабных симуляций, имитирующих сложные среды, довольно сложно.

AnyLogic — идеальная платформа для создания симуляционных моделей для обучения DRL-агентов в сложных средах. Недавно разработанная библиотека Alpyne — это библиотека Python, которая позволяет пользователям обучать DRL-агентов на Python, взаимодействуя с моделью AnyLogic. К сожалению, она все еще недостаточно стабильна для работы со сложными симуляционными моделями.

В этой статье мы представляем новый способ взаимодействия DRL с симуляционными моделям в AnyLogic с помощью библиотеки Pypeline. Этот метод также может быть использован для (не глубокого) обучения с подкреплением, но благодаря своей простоте большинство сред, для которых хватает простого RL, могут быть смоделированы непосредственно в самих языках программирования, таких как Python.

Стандартным способом обучения DRL-агента является взаимодействие с симуляционными моделями из Python. В рамках этого метода DRL-агент вызывается из симуляционной модели, чтобы наблюдать и взаимодействовать с моделью через определенные временные интервалы, и сохраняет все свои критические компоненты, например, буфер воспроизведения и нейронные сети, в локальной памяти в конце каждого эпизода. Такой подход обеспечивает стабильный способ реализации DRL в моделях AnyLogic.

Начнем же мы с краткого обзора основных компонентов, задействованных в этом методе. В частности, для демонстрации мы здесь используем реализацию Deep Q-Learning, но этот метод может быть применен к различным RL-алгоритмам. Затем мы рассмотрим простой пример (упрощенный OpenAI Gym Taxi-v3), чтобы продемонстрировать реализацию этого метода.

Краткий обзор основных компонентов

Компоненты на стороне AnyLogic (среда)

Чтобы взаимодействовать с Python, сначала нужно установить в модель AnyLogic библиотеку Pypeline. Поскольку в данной статье речь идет не о библиотеке Pypeline, детальные инструкции по установке и использованию библиотеки вы можете найти на сайте.

После установки библиотеки Pypeline нам нужно импортировать модуль Python для обучения DRL и создать инстанс класса обучения DRL в секции On Startup главного агента. В дальнейшем этот инстанс класса обучения DRL будет вызываться во время работы симуляции на каждом шаге действия для получения информации о состоянии, совершения какого-либо действия на ее основе и получения вознаграждения от симуляционной среды.

Для обучения RL-агента симуляционная среда должна обладать четырьмя важными возможностями:

  • выводить информацию о состоянии из среды (1),

  • выдавать вознаграждение от среды (2),

  • получать и выполнять действия от RL-агента (3), и

  • сообщать RL-агенту о завершении эпизода (4).

Таким образом, в симуляции должны быть предусмотрены функции для всех этих четырех возможностей. Специально для нашей реализации были созданы функция под пункты (1) и (2), и еще одна функция для пунктов (3) и (4). Функция для (1) просто возвращает информацию о текущем состоянии в виде вещественного или целочисленного списка. Функция для (2) просто возвращает текущее вознаграждение в вещественном или целочисленном виде. Функция для (3) и (4) принимает от RL-агента входные данные о действиях в среде и возвращает, будет ли симуляция закончена после выполнения действия.

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

Компоненты на стороне Python (RL-агент)

Как говорилось выше, в начале каждого эпизода инициализируется новый инстанс RL-агента. Поскольку в каждом эпизоде инициализируется новый RL-агент, очень важно реализовать какой-нибудь способ локальной записи важной информации об обучении, чтобы эта информация не была потеряна в конце каждого эпизода обучения. Здесь мы используем JSON и функции сохранения из библиотек наподобие PyTorch для сохранения информации в конце каждого эпизода обучения и загрузки ее при инициализации. На примере Deep Q-Learning, важная информация включает, но не ограничивается буфером воспроизведения, сетью политик, целевой сетью, количеством пройденных шагов, буфером вознаграждения, историей потерь и оптимизатором (если используется оптимизатор импульса, например ADAM). Более подробно об алгоритме Deep Q-Learning можно узнать из [1].

Логирование важной информации позволяет нам непрерывно обучать RL-агента между эпизодами. Однако необходимо решить еще одну задачу: симуляционная модель выводит только текущее состояние, вознаграждение и информацию о том, закончен ли эпизод (в дальнейшем мы будем говорить об этом, как о флаге DONE), но RL-агенту необходимо предыдущее состояние, чтобы сформировать переход для отправки в буфер воспроизведения. Эта проблема решается путем инициализации предыдущего состояния и значений действий в null. После получения информации о состоянии, вознаграждении и DONE от симуляции состояние станет новым предыдущим состоянием, а действие на основе состояния — новым предыдущим действием. Если значения предыдущего состояния и предыдущего действия не равны null, то в буфер воспроизведения будет добавлен новый переход, состоящий из предыдущего состояния, предыдущего действия, текущего состояния, вознаграждения и DONE.

Простая демонстрация — Упрощенное Taxi-v3

Давайте перейдем непосредственно к реализации. Модель AnyLogic с файлами Python, созданными для этой демонстрации, доступны в этом репозитории.

Среда

В демонстрационных целях мы показываем наш метод на примере упрощенной среды OpenAI Gym Taxi-v3, реплицированной в AnyLogic. Тем не менее, этот метод достаточно стабилен, чтобы его можно было применять к крупномасштабным и гораздо более сложным средам. Возможно, он даже лучше подходит для более сложных сред, потому что в более сложных средах накладными расходами на коммуникацию между AnyLogic и Python по сути можно пренебречь.

Эта среда состоит из сетки 4×4, такси, управляемого RL, и пассажира. Визуализация сетки показана на рисунке 1. Зеленые линии обозначают стены, через которые такси не может проехать. Начальное местоположение пассажира — G, а пункт назначения — Y. Такси будет инициализировано в произвольном месте, отличном от местоположения пассажира. Задача такси — сначала забрать пассажира, а затем высадить его в пункте назначения. Как только пассажир будет высажен или будет сделано более 200 шагов, эпизод завершится. Пространство действий в этой среде: 0 — движение вверх, 1 — движение вниз, 2 — движение влево, 3 — движение вправо, 4 — подобрать пассажира и 5 — высадить. Пространство состояний — это положение такси по оси X, положение такси по оси Y, а также был ли подобран пассажир (0 или 1). Если такси неудачно подобрало или высадило пассажира, оно получает вознаграждение -10. Когда такси успешно высаживает пассажира, оно получает награду +20. Такси получает награду -1, если не срабатывает одно из вышеупомянутых вознаграждений.

Рисунок 1: Визуализация сетки

Рисунок 1: Визуализация сетки

Реализация в AnyLogic

В этой модели есть несколько важных функций, которые позволяют обучать RL-агента. Функция f_State возвращает целочисленный список, представляющий текущее состояние. Функция f_Reward возвращает вознаграждение, полученное в результате выполнения действия. Функция f_TaxiAction реализует действие от RL-агента и возвращает, завершился ли эпизод после выполнения этого действия. Если параметр модели deploy установлен в true, функция f_TaxiAction изменит визуализацию в соответствии с действием. Функция f_RLAction вызывает RL-агента для выбора действия в соответствии с текущим состоянием и предоставляет RL-агенту необходимую для обучения информацию с помощью вышеупомянутых трех функций. Во время выполнения симуляции функция вызывается в цикле f_RLAction с интервалом в 0.1 секунду.

Реализация на Python

Для реализации Deep Q-Learning на Python мы используем библиотеку глубокого обучения PyTorch. За исключением нескольких дополнительных строк кода для сохранения и загрузки важной информации для обучения, эта реализация ничем не отличается от других стандартных реализаций Deep Q-Learning. Поскольку основной темой этой статьи не являются алгоритмы обучения с подкреплением, и чтобы не утомлять вас техническими подробностями, в этом разделе будут рассмотрены только те части кода, которые имеют отношение к применению RL в AnyLogic. В данной реализации для обучения с подкреплением выделено два файла Python: Train.py и DQNModel.py. Поскольку задача DQNModel.py заключается только в построении нейронной сети, он не рассматривается в этом блоге.

Здесь следует отметить, что, учитывая, что мы создаем связь между AnyLogic и Python, код Python лучше модулировать, чтобы сделать эту связь проще и чище. Здесь мы создали класс для обучающего агента Deep Q-Learning под названием DQN_Main.

Чтобы инициализировать инстанс класса DQN_Main (это происходит в начале каждого эпизода), нам нужно сначала загрузить необходимую информацию с локального диска с помощью JSON и функции load из PyTorch, затем установить значение предыдущего состояния и предыдущего действия в null, а вознаграждение эпизода — в 0. Информация, необходимая для этого инстанса, выделена красным на рисунке 2.

Рисунок 2: Файлы в папке Model (красный: информация, необходимая для обучения, желтый: графики для мониторинга обучения)

Рисунок 2: Файлы в папке Model (красный: информация, необходимая для обучения, желтый: графики для мониторинга обучения)

Затем на каждом шаге действий определяется функция act, которая вызывается из AnyLogic, чтобы поместить опыт в буфер воспроизведения, вызвать функцию train для обучения нейронной сети и, если эпизод завершен, сохранить важную информацию на локальном диске.

После вызова функции act функция train обучает нейронную сеть в течение одной эпохи и сохраняет на локальном диске важную информацию, которая изменилась в процессе обучения, если эпизод завершен.

При желании вы можете добавить функции для генерации графиков вознаграждения и потерь на локальный диск, чтобы вы могли наблюдать за прогрессом вашего RL-агента. Сгенерированные графики для данного инстанса отмечены желтым цветом на рисунке 2.

Ниже прилагается полный код Train.py:

import os.path
import os
import torch
import json
from DQNModel import DQN
import random
import numpy as np
import torch.nn.functional as F
import matplotlib.pyplot as plt

class DQN_Main:
    def __init__(self):
        self.BUFFER_SIZE = 200000
        self.MIN_REPLAY_SIZE = 50000
        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        self.GAMMA = 0.99
        self.BATCH_SIZE = 128
        self.EPSILON_START = 0.99
        self.EPSILON_END = 0.1
        self.EPSILON_DECAY = 0.000025
        self.TARGET_UPDATE_FREQ = 10000
        self.LR = 0.00025
        os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
        if os.path.exists('replay_buffer.json'):
            with open("replay_buffer.json", "r") as read_content:
                self.replay_buffer = json.load(read_content)
        else:
                    self.replay_buffer = []

        if os.path.exists('reward_buffer.json'):
            with open("reward_buffer.json", "r") as read_content:
                self.reward_buffer = json.load(read_content)
        else:
            self.reward_buffer = []

        if os.path.exists('step.json'):
            with open("step.json", "r") as read_content:
                self.step = json.load(read_content)
        else:
            self.step = 0

        self.policy_net = DQN(device=self.device).to(self.device)
        self.target_net = DQN(device=self.device).to(self.device)

        if os.path.exists('policy_net.pth'):
            self.policy_net.load_state_dict(torch.load('policy_net.pth'))
            self.target_net.load_state_dict(torch.load('target_net.pth'))
        else:
            self.target_net.load_state_dict(self.policy_net.state_dict())

        self.optimizer = torch.optim.Adam(self.policy_net.parameters(), lr=self.LR)
        if os.path.exists('policy_net_adam.pth'):
            self.optimizer.load_state_dict(torch.load('policy_net_adam.pth'))

        if os.path.exists('loss_hist.json'):
            with open("loss_hist.json", "r") as read_content:
                self.loss_hist = json.load(read_content)
        else:
            self.loss_hist = []

        if os.path.exists('loss_hist_show.json'):
            with open("loss_hist_show.json", "r") as read_content:
                self.loss_hist_show = json.load(read_content)
        else:
            self.loss_hist_show = []

        self.episode_reward = 0
        self.prev_state = None
        self.prev_action = None

    def save_hyperparams(self):
        hyperparams_dict = {
            'BUFFER SIZE': self.BUFFER_SIZE,
            'MIN REPLAY SIZE': self.MIN_REPLAY_SIZE,
            'GAMMA': self.GAMMA,
            'BATCH SIZE': self.BATCH_SIZE,
            'EPSILON START': self.EPSILON_START,
            'EPSILON END': self.EPSILON_END,
            'EPSILON DECAY': self.EPSILON_DECAY,
            'TARGET UPDATE FREQ': self.TARGET_UPDATE_FREQ,
            'LR': self.LR,
        }
        with open("hyperparameters.json", "w") as write:
            json.dump(hyperparams_dict, write)

    def train(self, done):
        # добавьте сюда шаг обучения
        transitions = random.sample(self.replay_buffer, self.BATCH_SIZE)

        states = np.asarray([t[0] for t in transitions])
        actions = np.asarray([t[1] for t in transitions])
        rewards = np.asarray([t[2] for t in transitions])
        dones = np.asarray([t[3] for t in transitions])
        next_states = np.asarray([t[4] for t in transitions])

        states_t = torch.as_tensor(states, dtype=torch.float32).to(self.device)
        actions_t = torch.as_tensor(actions, dtype=torch.int64).unsqueeze(-1).to(self.device)
        rewards_t = torch.as_tensor(rewards, dtype=torch.float32).unsqueeze(-1).to(self.device)
        dones_t = torch.as_tensor(dones, dtype=torch.float32).unsqueeze(-1).to(self.device)
        next_states_t = torch.as_tensor(next_states, dtype=torch.float32).to(self.device)

        # вычисление цели
        _, actions_target = self.policy_net(next_states_t).max(dim=1, keepdim=True)
        target_q_values_1 = self.target_net(next_states_t).gather(dim=1, index=actions_target)
        targets_1 = rewards_t + self.GAMMA * (1 - dones_t) * target_q_values_1

        # вычисление потери
        q_values = self.policy_net(states_t)
        action_q_values = torch.gather(input=q_values, dim=1, index=actions_t)

        # градиентный спуск
        loss = F.mse_loss(action_q_values, targets_1)
        self.loss_hist.append(loss.item())
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()
        if self.step % 200 == 0:
            self.loss_hist_show.append(sum(self.loss_hist[-300:])/300)
            self.plot_loss_hist()

        # обновление целевой сети
        if self.step % self.TARGET_UPDATE_FREQ == 0:
            self.target_net.load_state_dict(self.policy_net.state_dict())

        # нам нужен параметр done, так как нам нужно сохранить нейросети, если эпизод завершен
        if done:
            torch.save(self.policy_net.state_dict(), 'policy_net.pth')
            torch.save(self.target_net.state_dict(), 'target_net.pth')
            torch.save(self.optimizer.state_dict(), 'policy_net_adam.pth')
            with open("loss_hist.json", "w") as write:
                json.dump(self.loss_hist, write)
            with open("loss_hist_show.json", "w") as write:
                json.dump(self.loss_hist_show, write)

    def random_action(self):
        return random.choice(self.policy_net.action_space)

    def act(self, state, reward, done, deploy):
        if deploy:
            with torch.no_grad():
                state_t = torch.tensor(state)
                action = self.policy_net.act(state_t)
            return action
        if len(self.replay_buffer) >= self.MIN_REPLAY_SIZE:
            rnd = random.random()
            epsilon = self.EPSILON_START - self.EPSILON_DECAY * self.step
            self.step += 1
            if epsilon < self.EPSILON_END:
                epsilon = self.EPSILON_END
            if rnd <= epsilon:
                action = self.random_action()
            else:
                with torch.no_grad():
                    state_t = torch.tensor(state)
                    action = self.policy_net.act(state_t)
        else:
            # заполнение буфера воспроизведения
            action = self.random_action()

        if self.prev_state is None:
            # начало эпизода, мы просто берем действие, ничего не добавляем в буфер воспроизведения
            self.prev_state = state.copy()
            self.prev_action = action

            # здесь нам все еще нужно обучить нашу нейронную сеть
            if len(self.replay_buffer) >= self.MIN_REPLAY_SIZE:
                # if done neural nets will be saved in the train function
                self.train(done)
            return action
        else:
            # здесь мы добавляем переходы в буфер воспроизведения
            self.episode_reward += reward
            transition = (self.prev_state, self.prev_action, reward, done, state)
            self.replay_buffer.append(transition)
            if len(self.replay_buffer) > self.BUFFER_SIZE:
                self.replay_buffer.pop(0)

            # корректировка предыдущего состояния и действия
            self.prev_state = state.copy()
            self.prev_action = action

        if done:
            self.reward_buffer.append(self.episode_reward)
            # поскольку мы подключаемся к AnyLogic, нам приходится сохранять все в каждом эпизоде
            with open("reward_buffer.json", "w") as write:
                json.dump(self.reward_buffer, write)
            with open("replay_buffer.json", "w") as write:
                json.dump(self.replay_buffer, write)
            with open("step.json", "w") as write:
                json.dump(self.step, write)
            if len(self.reward_buffer)%100 == 0:
                self.plot_reward_buffer()

        if len(self.replay_buffer) >= self.MIN_REPLAY_SIZE:
            # если done, нейронные сети будут сохранены в функции train

            self.train(done)

        return action

    def plot_reward_buffer(self):
        plt.plot(self.reward_buffer)
        plt.xlabel('Episodes')
        plt.ylabel('Rewards')
        plt.savefig('reward buffer.jpg')
        plt.close()

    def plot_loss_hist(self):
        plt.plot(self.loss_hist_show[10:])
        plt.xlabel('100 Epoch')
        plt.ylabel('Loss')
        plt.savefig('Loss History.jpg')
        plt.close()

Обучение с помощью эксперимента AnyLogic

Обучение RL-агента происходит с помощью эксперимента Монте-Карло от AnyLogic. В эксперименте Монте-Карло мы задаем количество эпизодов, на которых мы хотим обучить RL-агента, в разделе Replications, после чего нам остается только сидеть и наблюдать, как RL-агент развивается, получая опыт от симуляции!

Если вы хотите прервать текущее обучение и заново обучить нового RL-агента, вы можете сделать это, удалив все файлы, отмеченные желтым или красным цветом на рисунке 2.

Результат обучения

Результат обучения подтверждает, что наш метод работает не хуже любого другого метода обучения RL-моделей. На рисунке 3 показано, что вознаграждение постоянно улучшается, и модель успешно сходится.

Рисунок 3: История вознаграждений

Рисунок 3: История вознаграждений

Успех обучения подтверждается визуализацией эксперимента AnyLogic (желтый: такси, красный: пассажир, зеленый: пункт назначения):

Спасибо за внимание! Если у вас возникнут дополнительные вопросы, не стесняйтесь посетить мою страницу на GitHub и оставить свои вопросы в разделе обсуждений! :)

Ссылки:

  1. Mnih, V., Kavukcuoglu, K., Silver, D. et al. Human-level control through deep reinforcement learning. Nature 518, 529–533 (2015).

Научиться создавать имитационные модели в ПО AnyLogic и применять их для анализа проектов можно под руководством экспертов области на онлайн-курсе «Имитационное моделирование на базе AnyLogic».

Habrahabr.ru прочитано 5749 раз