Дневник разработки RL-агента
Отчет о экспериментах в wandb
Реализация среды
Работа началась с реализации исходной среды. Особых проблем с этим не возникло, однако я допустил критическую ошибку, которая всплыла только в процессе обучения. Я не предусмотрел возможность создавать идентичные среды для повторяемости экспериментов, а также для тестирования агента на той же карте, на которой происходил процесс обучения. В результате, так как агент по сути учится запоминать карту, это полностью ломало процесс тестирования.
Чтобы исправить это, я ввел seed при инициализации среды для полной повторяемости генерации карт. Случайное зерно генерации препятствий я привязал к параметрам карты, таким как размер, целевая позиция и процент препятствий. Кроме того, после генерации препятствий теперь проходит проверка наличия прохода между начальной и целевой позицией. Это необходимо, чтобы избежать ситуаций, где препятствия полностью отделяют старт от цели, ставя агента в безвыходное положение.
В первых реализациях на карте вероятностей начальной позиции я задавал всё поле, за исключением цели и препятствий. Это приводило к сильному разбросу по длине эпизодов: иногда агент доходил за 2–3 шага, а иногда требовалось несколько десятков. Такая вариативность делала обучение нестабильным, поэтому для тестов было решено фиксировать стартовую позицию в первых двух столбцах карты, а целевую — в правом нижнем углу.
Алгоритмы обучения
Следующей трудностью, с которой я столкнулся при реализации алгоритма обучения, стала частичная наблюдаемость среды. Так как агент не знает свои координаты x и y, он не может определить свое положение относительно цели. Изначально я планировал использовать подход, применявшийся в старых играх Atari: брать не один кадр (одно наблюдение), а подавать на вход 4–5 последовательных кадров за раз. Но в этом случае возникал вопрос выбора размера карты наблюдения. Если брать её по размеру истинной карты, то к агенту могли утечь размеры поля, и он мог бы находить целевую позицию (особенно в углу) просто на основе этой информации.
Немного изучив тему, я натолкнулся на статью Deep Recurrent Q-Learning for Partially Observable MDPs, описывающую применение рекуррентных слоев в RL. Это внесло два основных изменения в мой алгоритм DQN. Во-первых, линейный слой перед выходом был заменен на рекуррентный слой LSTM, так как, судя по результатам из статьи, он показывал лучшие результаты. Во-вторых, скрытое состояние из слоя LSTM от предыдущего шага теперь передается в новое в качестве входа (для первого шага скрытое состояние принимается нулевым).
Проведенные эксперименты показали, что данный вариант позволял агенту достигать цели в среднем в 80% случаев при ста тестах в наблюдаемой среде (где видны препятствия), но крайне плохо показывал себя в среде, где агент препятствий не видит. Изучив связанные статьи, я внес еще две модификации.
Я отошел от стандартной реализации DQN, где и выбор действия, и его оценка делались по сохраненной сети. Теперь оценка делается по target network, а выбор действия — по online network. В оригинальной статье утверждалось, что это уменьшает влияние шума в процессе обучения. Кроме того, я изменил архитектуру так, чтобы сеть выдавала не просто функцию ценности, а V и A — насколько хорошо текущее состояние и насколько выбранное действие лучше среднего в этом состоянии соответственно. На основе этих значений уже высчитывается Q. Внесение этих модификаций (Dueling Double DQN) позволило агенту, который не видит препятствий, доходить до цели в небольшой среде в 70% случаев.
Вторым алгоритмом я выбрал A2C. Здесь я также решил добавить скрытый слой LSTM для хранения информации с предыдущих шагов. Благодаря этому алгоритм успешно доходит до цели в средах, где агент видит препятствия, но, к сожалению, ломается в противном случае.
Интеграция наблюдений MNIST
Теоретически, так как цифры являются двумерным объектом, для извлечения максимального количества информации следовало бы использовать сверточные сети. Однако для этого пришлось бы значительно модифицировать исходный код обучения и вместо последовательности принимать двумерный массив.
Поэтому было принято решение протестировать вариант с входом flatten. Для этого достаточно было изменить размеры входного вектора наблюдений на размер сжатого представления MNIST. Оказалось, что этого более чем достаточно: агент обучается крайне точно определять цифры. Вероятно, это связано с относительной простотой датасета MNIST.
Касательно вопроса о том, стоит ли изменять имеющуюся среду или создавать новую, я считаю, что лучше не трогать исходную среду из первого задания. Правильнее будет скопировать первую версию и вносить изменения уже в копию. Это позволит сохранить работоспособный эталон для сравнения результатов и избежать поломки уже отлаженного кода, если эксперименты с MNIST пойдут не по плану.
Данное изменение среды (замена простых наблюдений на MNIST) влияет на сложность восприятия. По сути, это тестирует способность агента работать с высокоразмерным и зашумленным входом, извлекая из него полезные признаки, при этом сама логика навигации и динамика среды остаются прежними.
Векторизированные среды
Для реализации среды с использованием доступного инструментария я изучил документацию Gymnasium Vector API и постарался максимально перенести функционал на основе исходного кода. Для истинной векторной среды я использовал трехмерный массив для хранения состояний всех агентов, где каждый срез соответствует отдельному агенту.
В методе step для индексации клеток я использовал доступные в numpy векторы координат. Формат индексации выглядит следующим образом:
env_idx = np.arange(self.n_envs, dtype=int) # [0, 1, 2, ... N-1]
cell_vals = self.grid[env_idx, tr, tc]
Применять условные операторы для векторного варианта не получится, поэтому для проверки коллизий я использую битовые маски. Так как некоторые агенты доходят до цели раньше других, была введена отдельная битовая маска для отслеживания активных агентов:
Это позволяет корректно обновлять состояние только для тех агентов, которые еще не завершили эпизод.