Разработайте своего первого AI-агента глубокое Q-обучение

Создайте своего первого AI-агента с помощью глубокого Q-обучения

Окунитесь в мир искусственного интеллекта – создайте глубинный усилительный тренировочный зал с нуля.

Создайте свой собственный глубинный тренировочный зал усилителя обратного распространения — изображение автора

Содержание

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

Вступление

Почему усиленное обучение?Что вы получитеЧто такое усиленное обучениеГлубокое обучение Q-обучением

Пошаговое руководство

1. Начальная настройка2. Общая картина3. Среда: Начальные основы4. Реализация агента: Нейронная архитектура и политика5. Влияние на среду: Завершение6. Извлечение опыта: Многократное повторение7. Определение процесса обучения агента: Подгонка НС8. Выполнение цикла обучения: Сбор всех этих вместе9. Завершение10. Бонус: Оптимизация представления состояния

Почему усиленное обучение?

Широкое распространение передовых систем искусственного интеллекта, таких как ChatGPT, Bard, Midjourney, Stable Diffusion и многих других, привлекло интерес к области искусственного интеллекта, машинного обучения и нейронных сетей, который часто остается неудовлетворенным из-за технической природы реализации таких систем.

Для тех, кто желает начать свой путь в области искусственного интеллекта (или продолжить его), создание тренировочного зала усиления обратного распространения с использованием глубокого обучения Q-обучением – это отличный старт, так как он не требует продвинутых знаний для реализации, может быть легко расширен для решения сложных задач и позволяет сразу получить практические знания о том, как искусственный интеллект становится “интеллектуальным”.

Что вы получите

Предполагая, что у вас есть базовое понимание Python, к концу этого введения в глубокое усиленное обучение без использования высокоуровневых фреймворков для усиленного обучения вы разработаете свой собственный тренировочный зал для обучения агента решать простую проблему – переместиться от начальной точки к цели!

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

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

Изображение автора, использующего среду LunarLander-v2 Gymnasium

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

Что такое обучение с подкреплением?

Обучение с подкреплением (Reinforcement Learning, RL) – это подотрасль машинного обучения (ML), которая специально направлена на то, как агенты (сущности, принимающие решения) совершают действия в среде для достижения цели.

В его реализации включены:

  • Игры
  • Автономные транспортные средства
  • Робототехника
  • Финансы (алгоритмическая торговля)
  • Обработка естественного языка
  • и многое другое…

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

Перед тем как продолжить, важно понять некоторые часто используемые термины:

  • Среда: Это мир – место, где действует агент. Она устанавливает правила, границы и вознаграждения, с которыми агент должен взаимодействовать.
  • Агент: Принимающий решения внутри среды. Агент совершает действия на основе своего понимания текущего состояния.
  • Состояние: Подробный снимок текущей ситуации агента в среде, включая соответствующие метрики или сенсорную информацию, используемую для принятия решений.
  • Действие: Конкретное мероприятие, которое агент предпринимает для взаимодействия с окружающей средой, такое как движение, сбор предмета или инициирование взаимодействия.
  • Вознаграждение: Обратная связь, получаемая от окружающей среды в результате действий агента, которая может быть положительной, отрицательной или нейтральной и направляющей процесс обучения.
  • Пространство состояний/действий: Комбинация всех возможных состояний, с которыми агент может столкнуться, и всех действий, которые он может совершить в среде. Это определяет сферу принятия решений и ситуаций, с которыми агент должен научиться справляться.

В основном, на каждом шаге программы агент получает состояние от среды, выбирает действие, получает вознаграждение или наказание, и среда обновляется или эпизод завершается. Полученная информация после каждого шага сохраняется в качестве “опыта” для последующего обучения.

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

На первый взгляд это может показаться множеством информации, но по мере того, как вы сами это строите, эти термины становятся естественными.

Глубокое обучение Q-Learning

Q-Learning – это алгоритм, используемый в ML, где «Q» означает “Quality” (качество), то есть значение действий, которые может предпринять агент. Он работает путем создания таблицы значений Q, действий и их связанного качества, которые оценивают ожидаемое будущее вознаграждение за выполнение действия в заданном состоянии.

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

Последовательный поток Q-Learning: от оценки состояния до вознаграждения и обновления таблицы Q. — Изображение автора

Однако у Q-Learning есть несколько недостатков. Необходимо исследовать каждую пару состояние-действие, чтобы достичь хороших результатов. Если пространства состояний и действий (набор всех возможных состояний и действий) слишком велики, то невозможно хранить их все в таблице.

Вот где появляется Deep Q-Learning (DQL), эволюция Q-Learning. DQL использует глубокую нейронную сеть (NN), чтобы приблизительно вычислять функцию Q-значений, а не сохранять их в таблице. Это позволяет работать с окружениями, где есть высокоразмерные пространства состояний, такие как изображения с камеры, что было бы непрактично для традиционного Q-Learning.

Deep Q-Learning является пересечением Q-Learning и глубоких нейронных сетей - Изображение автора

Затем нейронная сеть может обобщаться на похожие состояния и действия, выбирая желаемое действие, даже если она не была обучена на точной ситуации, что устраняет необходимость в большой таблице.

Каким образом нейронная сеть это делает, не входит в рамки этого руководства. К счастью, для эффективной реализации Deep Q-Learning не требуется глубокого понимания.

Построение обучения с подкреплением в гимнастическом зале

1. Начальная настройка

Прежде чем мы начнем писать код для нашего AI агента, рекомендуется иметь хорошее понимание основ принципов объектно-ориентированного программирования (OOP) на Python.

Если у вас еще нет установленного Python, ниже приведено простое руководство от Bhargav Bachina, чтобы вы начали. Версия, которую я буду использовать, – 3.11.6.

Как установить и начать работать с Python

Руководство для начинающих и всех, кто хочет начать изучать Python

VoAGI.com

Вам понадобится только TensorFlow, библиотека машинного обучения с открытым исходным кодом от Google, которую мы будем использовать для создания и обучения нашей нейронной сети. Его можно установить с помощью pip в терминале. Моя версия – 2.14.0.

pip install tensorflow

Или если это не работает:

pip3 install tensorflow

Вам также понадобится пакет NumPy, но он должен быть включен в TensorFlow. Если у вас возникнут проблемы, выполните pip install numpy.

Также рекомендуется создавать новый файл для каждого класса (например, environment.py). Это поможет избежать перегруженности и упростит устранение любых ошибок, с которыми вы можете столкнуться.

Для вашего удобства вот репозиторий GitHub с завершенным кодом: https://github.com/HestonCV/rl-gym-from-scratch. Вы можете клонировать, исследовать и использовать его в качестве исходной точки!

2. Общая картина

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

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

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

from environment import Environmentfrom agent import Agentfrom experience_replay import ExperienceReplayimport timeif __name__ == '__main__':    grid_size = 5    environment = Environment(grid_size=grid_size, render_on=True)    agent = Agent(grid_size=grid_size, epsilon=1, epsilon_decay=0.998, epsilon_end=0.01)    # agent.load(f'models/model_{grid_size}.h5')    experience_replay = ExperienceReplay(capacity=10000, batch_size=32)        # Количество эпизодов до остановки обучения    episodes = 5000    # Максимальное количество шагов в каждом эпизоде    max_steps = 200    for episode in range(episodes):        # Получаем начальное состояние окружения и устанавливаем done в False        state = environment.reset()        # Цикл до окончания эпизода        for step in range(max_steps):            print('Эпизод:', episode)            print('Шаг:', step)            print('Epsilon:', agent.epsilon)            # Получаем выбор действия из политики агента            action = agent.get_action(state)            # Переходим к следующему состоянию и сохраняем опыт            reward, next_state, done = environment.step(action)            experience_replay.add_experience(state, action, reward, next_state, done)            # Если опыта достаточно для обучения агента, обучаем его            if experience_replay.can_provide_sample():                experiences = experience_replay.sample_batch()                agent.learn(experiences)            # Устанавливаем состояние в следующее состояние            state = next_state                        if done:                break            # time.sleep(0.5)        agent.save(f'models/model_{grid_size}.h5')

Каждая внутренняя петля считается одним шагом.

Процесс обучения через взаимодействие Агента-Окружения — Изображение автора

На каждом шаге:

  • Состояние извлекается из окружения.
  • Агент выбирает действие на основе этого состояния.
  • Действует на окружение, возвращая вознаграждение, результат состояния после выполнения действия и указывает, закончен ли эпизод.
  • Начальное состояние, действие, вознаграждение, следующее состояние и флаг окончания затем сохраняются в experience_replay в качестве долговременной памяти (опыта).
  • Затем агент обучается на случайной выборке из этих опытов.

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

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

Как указано во введении, наша проблема для агента довольно простая: добраться из начальной позиции в сетке до целевой позиции.

3. Окружение: Первоначальные основы

Самое очевидное место, с которого следует начать разработку этой системы, – это окружение.

Чтобы иметь функционирующую среду обучения с подкреплением, окружению необходимо выполнить несколько действий:

  • Сохранять текущее состояние мира.
  • Отслеживать цель и агента.
  • Позволять агенту вносить изменения в мир.
  • Возвращать состояние в форме, понятной модели.
  • Отображать его так, чтобы мы могли наблюдать за агентом.

Именно здесь агент проводит всю свою жизнь. Мы определим окружение как простую квадратную матрицу/2D массив или список списков в Python.

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

DQL специально разработан для дискретного пространства действий (конечное число действий) — на этом мы и сосредоточимся. Другие методы используются для непрерывного пространства действий.

В сетке пустое пространство будет представлено нулями, агент будет представлен единицей, а цель будет представлена -1. Размер окружения может быть любым, но по мере увеличения размера окружения множество возможных состояний (пространство состояний) растет экспоненциально. Это может существенно замедлить время обучения.

Сетка будет выглядеть примерно так при рендеринге:

[0, 1, 0, 0, 0][0, 0, 0, 0, 0][0, 0, 0, 0, 0][0, 0, 0, -1, 0][0, 0, 0, 0, 0]

Создание класса Environment и метода resetНачнем с реализации класса Environment и способа инициализации окружения. На данный момент он будет принимать целое число grid_size, но мы вскоре расширим его.

import numpy as npclass Environment:    def __init__(self, grid_size):        self.grid_size = grid_size        self.grid = []    def reset(self):        # Инициализация пустой сетки в виде 2D списка из 0s        self.grid = np.zeros((self.grid_size, self.grid_size))

При создании нового экземпляра Environment сохраняет grid_size и инициализирует пустую сетку.

Метод reset заполняет сетку, используя np.zeros((self.grid_size, self.grid_size)), который принимает кортеж, форму и возвращает 2D массив NumPy этой формы, состоящий только из нулей.

A NumPy-массив – это структура данных в виде сетки, которая ведет себя, подобно списку в Python, за исключением того, что он позволяет нам эффективно хранить и обрабатывать числовые данные. Он позволяет выполнять векторизованные операции, что означает, что операции автоматически применяются ко всем элементам массива без необходимости явных циклов.

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

Почему имя reset? Что ж, этот метод будет вызываться для сброса окружения и, в конечном итоге, вернет начальное состояние сетки.

Добавление агента и целиЗатем мы создадим методы для добавления агента и цели на сетку.

import randomdef add_agent(self):    # Выберите случайное местоположение    location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))        # Агент представлен числом 1    self.grid[location[0]][location[1]] = 1        return locationdef add_goal(self):    # Выберите случайное местоположение    location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))    # Получайте случайное местоположение, пока оно не занято    while self.grid[location[0]][location[1]] == 1:        location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))        # Цель представлена числом -1    self.grid[location[0]][location[1]] = -1    return location

Местоположения агента и цели будут представлены кортежем (x, y). Оба метода выбирают случайные значения в пределах границы сетки и возвращают местоположение. Основное отличие в том, что add_goal гарантирует, что она не выберет местоположение, уже занятое агентом.

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

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

def render(self):        # Преобразовать в список целых чисел для улучшения форматирования        grid = self.grid.astype(int).tolist()        for row in grid:            print(row)        print('') # Добавить немного пространства между отображениями для каждого шага

render выполняет три действия: приводит элементы self.grid к типу int, преобразует его в список Python и печатает каждую строку.

Единственная причина, по которой мы не печатаем каждую строку из массива NumPy напрямую, заключается в том, что это просто не выглядит так хорошо.

Связываем все вместе..

import numpy as npimport randomclass Environment:    def __init__(self, grid_size):        self.grid_size = grid_size        self.grid = []    def reset(self):        # Инициализируем пустую сетку в виде двумерного массива из 0        self.grid = np.zeros((self.grid_size, self.grid_size))          def add_agent(self):        # Выберите случайное местоположение        location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))                # Агент представлен числом 1        self.grid[location[0]][location[1]] = 1                return location    def add_goal(self):        # Выберите случайное местоположение        location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))            # Получайте случайное местоположение, пока оно не занято        while self.grid[location[0]][location[1]] == 1:            location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))                # Цель представлена числом -1        self.grid[location[0]][location[1]] = -1            return location          def render(self):        # Преобразовать в список целых чисел для улучшения форматирования        grid = self.grid.astype(int).tolist()        for row in grid:            print(row)        print('') # Добавить немного пространства между отрисовками для каждого шага# Тест окруженияenv = Environment(5)env.reset()agent_location = env.add_agent()goal_location = env.add_goal()env.render()print(f'Местоположение агента: {agent_location}')print(f'Местоположение цели: {goal_location}')

>>>[0, 0, 0, 0, 0][0, 0, -1, 0, 0][0, 0, 0, 0, 0][0, 0, 0, 1, 0][0, 0, 0, 0, 0]Местоположение агента: (3, 3)Местоположение цели: (1, 2)

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

Хорошо, окружение определено. Что дальше?

Углубление в resetДавайте отредактируем метод reset, чтобы он рассматривал размещение агента и цели за нас. А заодно автоматизируем также и render.

класс Среда:    def __init__ (self, размер_сетки, render_on=False):        self.grid_size = размер_сетки        self.grid = []        # Убедитесь, что добавлены новые атрибуты        self.render_on = render_on        self.agent_location = None        self.goal_location = None    def reset (self):        # Инициализируйте пустую сетку в виде двумерного массива из 0        self.grid = np.zeros((self.grid_size, self.grid_size))        # Добавьте агента и цель на сетку        self.agent_location = self.add_agent()        self.goal_location = self.add_goal()        if self.render_on:            self.render()

Теперь, когда вызывается reset, агент и цель добавляются на сетку, их начальные позиции сохраняются, и если render_on установлен в true, сетка будет отображаться.

...# Тест Среды
env = Environment(5, render_on=True)
env.reset()
# Теперь для доступа к позиции агента и цели вы можете использовать атрибуты Среды
print(f'Местоположение агента: {env.agent_location}')
print(f'Местоположение цели: {env.goal_location}')

>>>[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, -1]
[1, 0, 0, 0, 0]
Местоположение агента: (4, 0)
Местоположение цели: (3, 4)

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

Нейронные сети обычно требуют одномерного входа, а не двумерной формы, которой в настоящее время представлена сетка. Мы можем исправить это, выравнивая сетку с помощью встроенного метода flatten в NumPy. Это поместит каждую строку в один массив.

def get_state(self):    # Преобразование сетки из двумерной в одномерную    state = self.grid.flatten()    return state

Это преобразует:

[0, 0, 0, 0, 0]
[0, 0, 0, 1, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, -1]
[0, 0, 0, 0, 0]

В:

[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0]

Как видите, не сразу очевидно, какая ячейка является какой, но для глубокой нейронной сети это не проблема.

Теперь мы можем обновить reset, чтобы он возвращал состояние сразу после заполнения grid. Ничего больше не изменится.

def reset (self):    ...    # Вернуть начальное состояние сетки    return self.get_state()

Полный код до этого момента:

import randomclass Environment:    def __init__ (self, размер_сетки, render_on=False):        self.grid_size = размер_сетки        self.grid = []        self.render_on = render_on        self.agent_location = None        self.goal_location = None    def reset (self):        # Инициализируйте пустую сетку в виде двумерного массива из 0        self.grid = np.zeros((self.grid_size, self.grid_size))        # Добавьте агента и цель на сетку        self.agent_location = self.add_agent()        self.goal_location = self.add_goal()        if self.render_on:            self.render()        # Вернуть начальное состояние сетки        return self.get_state()    def add_agent (self):        # Выберите случайное местоположение        location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))        # Агент представлен 1        self.grid[location[0]][location[1]] = 1        return location    def add_goal (self):        # Выберите случайное местоположение        location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))        # Получите случайное местоположение, пока оно не будет занято        while self.grid[location[0]][location[1]] == 1:            location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))                   # Цель представлена -1        self.grid[location[0]][location[1]] = -1        return location          def render (self):        # Преобразование в список целых чисел для улучшения форматирования        grid = self.grid.astype(int).tolist()        for row in grid:            print(row)        print('') # Чтобы добавить немного пространства между рендерами для каждого шага          def get_state (self):        # Преобразование сетки из двумерной в одномерную        state = self.grid.flatten()        return state

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

Мы вернемся к этой проблеме позже, когда класс Agent будет написан для более точного контекста.

4. Реализация нейронной архитектуры и стратегии агента

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

Пространство действий – это набор всех возможных действий, в данном случае агент может двигаться вверх, вниз, влево и вправо, поэтому размер пространства действий равен 4.

Пространство состояний – это набор всех возможных состояний. Это может быть огромное количество в зависимости от окружающей среды и точки зрения агента. В нашем случае, если мир имеет размер 5х5, то возможных состояний будет 600, а если мир имеет размер 25х25, то возможных состояний будет 390,000, что значительно увеличивает время обучения.

Чтобы агент эффективно научился достигать цели, ему нужны несколько компонентов:

  • Нейронная сеть для приближенного определения Q-значений (оценочного общего количества будущей награды за действие) в случае DQL.
  • Стратегия, по которой агент выбирает действие, или политика.
  • Сигналы вознаграждения от окружающей среды, указывающие агенту, насколько хорошо он справляется.
  • Возможность тренироваться на прошлых опытах.

Существуют две различных стратегии, которые можно реализовать:

  • Жадная стратегия: выбирается действие с наибольшим Q-значением в текущем состоянии.
  • Жадная стратегия с эпсилоном: выбирается действие с наибольшим Q-значением в текущем состоянии, но существует небольшой шанс, эпсилон (обычно обозначается как ϵ), выбрать случайное действие. Если эпсилон равен 0.02, то существует 2% вероятность выбора случайного действия.

Мы реализуем жадную стратегию с эпсилоном.

Зачем случайные действия помогают агенту учиться? Эксплорация.

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

Начиная с большого значения эпсилон и медленно уменьшая его, агент может полностью исследовать окружающую среду, обновляя свои Q-значения, прежде чем эксплуатировать изученные стратегии. Количество уменьшения эпсилон со временем называется затуханием эпсилон, что станет понятнее вскоре.

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

Теперь, перед тем, как реализовать стратегию, нам нужен способ получения Q-значений. В этом нам помогает мозг нашего агента – или нейронная сеть.

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

Идея заключается в том, чтобы дать NN большое количество размеченных данных, например, “вот входные данные и вот, что вы должны выдать на выходе”. Она медленно корректирует значения между нейронами на каждом этапе обучения, пытаясь приблизиться к данным выходам, находить паттерны в данных и, надеюсь, помогать нам предсказывать для входных данных, которые сеть никогда ранее не видела.

Преобразование состояния в Q-значения с помощью нейронной сети - Картинка от автора

Класс Agent и определение нейронной архитектуры В данном случае мы определим нейронную архитектуру с помощью TensorFlow и сосредоточимся на “прямом проходе” данных.

from tensorflow.keras.layers import Densefrom tensorflow.keras.models import Sequentialclass Agent:    def __init__(self, grid_size):        self.grid_size = grid_size        self.model = self.build_model()    def build_model(self):        # Создание последовательной модели с 3 слоями        model = Sequential([            # Входной слой ожидает разглаженную сетку, поэтому форма ввода - квадрат размера grid_size            Dense(128, activation='relu', input_shape=(self.grid_size**2,)),            Dense(64, activation='relu'),            # Выходной слой с 4 блоками для возможных действий (вверх, вниз, влево, вправо)            Dense(4, activation='linear')        ])        model.compile(optimizer='adam', loss='mse')        return model

Если вы не знакомы с нейронными сетями, не зацикливайтесь на этом разделе. Хотя мы используем активации, такие как «relu» и «линейная», в нашей модели, подробное исследование функций активации выходит за рамки этой статьи.

Все, что вам действительно нужно знать, – это то, что модель принимает состояние в качестве входных данных, значения преобразуются на каждом слое в модели, и выводятся четыре значения Q, соответствующих каждому действию.

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

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

  1. Первый скрытый слой имеет 128 нейронов, Dense(128, activation='relu'), и принимает сглаженную сетку в качестве входных данных.
  2. Второй скрытый слой состоит из 64 нейронов, Dense(64, activation='relu') и дополнительно обрабатывает информацию.

Наконец, выходной слой, Dense(4, activation='linear'), состоит из 4 нейронов, соответствующих четырем возможным действиям (вверх, вниз, влево, вправо). Этот слой выводит значения Q – оценки будущей награды для каждого действия.

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

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

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

import numpy as np   def get_action(self, state):    # Добавить дополнительное измерение к состоянию, чтобы создать пакет с одним экземпляром    state = np.expand_dims(state, axis=0)        # Используйте модель для прогнозирования Q-значений (значений действия) для данного состояния    q_values = self.model.predict(state, verbose=0)        # Выберите и верните действие с наибольшим Q-значением    action = np.argmax(q_values[0]) # Взять действие из первой (и единственной) записи        return action

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

state = np.expand_dims(state, axis=0)

Мы можем исправить это, используя метод expand_dims из NumPy, указав axis=0. Это просто превращает его в пакет из одного ввода. Например, состояние сетки размером 5×5:

[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0]

Становится:

[[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0]]

При обучении модели обычно используют пакеты размером от 32 и более. Он будет выглядеть примерно так:

[[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0], ... [0, 0, 0, 0, 0, 

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

...# Используйте модель для предсказания значений Q (значений действий) для заданного состоянияq_values = self.model.predict(state, verbose=0)# Выберите и верните действие с наивысшим значением Qaction = np.argmax(q_values[0]) # Возьмите действие из первой (и единственной) записи...

Мы просто передаем модели состояние, и она выводит пакет предсказаний. Помните, что поскольку мы подаем сети пакет одного элемента, она вернет пакет из одного элемента. Кроме того, verbose=0 гарантирует, что консоль остается чистой от обычных отладочных сообщений при каждом вызове функции predict.

Наконец, мы выбираем и возвращаем индекс действия с наибольшим значением, используя np.argmax на первую и единственную запись в пакете.

В нашем случае индексы 0, 1, 2 и 3 будут соответствовать вверху, внизу, влево и вправо соответственно.

Жадная политика всегда выбирает действие с наивысшей наградой согласно текущим значениям Q, что не всегда приводит к наилучшим долгосрочным результатам.

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

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

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

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

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

Инициализациja значений:epsilon = 1epsilon_decay = 0.998-----------------Шаг 1:epsilon = 1epsilon = 1 * 0.998 = 0.998-----------------Шаг 2:epsilon = 0.998epsilon = 0.998 * 0.998 = 0.996-----------------Шаг 3:epsilon = 0.996epsilon = 0.996 * 0.998 = 0.994-----------------Шаг 4:epsilon = 0.994epsilon = 0.994 * 0.998 = 0.992-----------------...-----------------Шаг 1000:epsilon = 1 * (0.998)^1000 = 0.135-----------------...и так далее

Как видно, эпсилон медленно приближается к нулю с каждым шагом. На шаге 1000 существует вероятность 13,5%, что будет выбрано случайное действие. Затухание эпсилон - это значение, которое нужно настроить в зависимости от пространства состояний. При большом пространстве состояний может потребоваться больше исследований или большое затухание эпсилон.

Распад эпсилон с течением времени — Изображение от автора

Даже когда агент обучен хорошо, полезно сохранять небольшое значение эпсилон. Мы должны определить точку остановки, где эпсилон не будет уменьшаться, окончание эпсилон. Это может быть 0.1, 0.01 или даже 0.001 в зависимости от конкретного случая и сложности задачи.

На рисунке выше вы заметите, что эпсилон перестает уменьшаться при 0.1, предопределенном окончании эпсилон.

Давайте обновим наш класс Agent, чтобы включить эпсилон.

import numpy as npclass Agent:    def __init__(self, grid_size, epsilon=1, epsilon_decay=0.998, epsilon_end=0.01):        self.grid_size = grid_size        self.epsilon = epsilon        self.epsilon_decay = epsilon_decay        self.epsilon_end = epsilon_end        ...    ...    def get_action(self, state):        # rand() возвращает случайное значение между 0 и 1        if np.random.rand() <= self.epsilon:            # Исследование: случайное действие            action = np.random.randint(0, 4)        else:            # Добавьте дополнительное измерение для состояния, чтобы создать пакет с одним экземпляром            state = np.expand_dims(state, axis=0)            # Используйте модель для предсказания значений Q (значений действий) для заданного состояния            q_values = self.model.predict(state, verbose=0)            # Выберите и верните действие с наивысшим значением Q            action = np.argmax(q_values[0]) # Возьмите действие из первой (и единственной) записи                # Уменьшение значения эпсилон для уменьшения исследования со временем        if self.epsilon > self.epsilon_end:            self.epsilon *= self.epsilon_decay        return action

Мы присвоили значения по умолчанию epsilon, epsilon_decay и epsilon_end равные 1, 0.998 и 0.01 соответственно.

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

Метод get_action был обновлен для интеграции эпсилона. Если случайное значение, полученное с помощью np.random.rand, меньше или равно epsilon, выбирается случайное действие. В противном случае, процесс остается прежним.

Наконец, если значение epsilon еще не достигло значения epsilon_end, мы обновляем его, умножая на epsilon_decay следующим образом - self.epsilon *= self.epsilon_decay.

Агент до этого момента:

from tensorflow.keras.layers import Densefrom tensorflow.keras.models import Sequentialimport numpy as npclass Agent:    def __init__(self, grid_size, epsilon=1, epsilon_decay=0.998, epsilon_end=0.01):        self.grid_size = grid_size        self.epsilon = epsilon        self.epsilon_decay = epsilon_decay        self.epsilon_end = epsilon_end        self.model = self.build_model()    def build_model(self):        # Создаем последовательную модель с тремя слоями        model = Sequential([            # Входной слой ожидает развернутую сетку, поэтому форма ввода - квадрат grid_size            Dense(128, activation='relu', input_shape=(self.grid_size**2,)),            Dense(64, activation='relu'),            # Выходной слой с 4 элементами для возможных действий (вверх, вниз, влево, вправо)            Dense(4, activation='linear')        ])        model.compile(optimizer='adam', loss='mse')        return model    def get_action(self, state):        # rand() возвращает случайное значение между 0 и 1        if np.random.rand() <= self.epsilon:            # Исследование: случайное действие            action = np.random.randint(0, 4)        else:            # Добавляем дополнительное измерение к состоянию, чтобы создать пакет с одним экземпляром            state = np.expand_dims(state, axis=0)            # Используем модель для предсказания Q-значений (значений действия) для данного состояния            q_values = self.model.predict(state, verbose=0)            # Выбираем и возвращаем действие с наибольшим Q-значением            action = np.argmax(q_values[0]) # Берем действие из первой (и единственной) записи                # Уменьшаем значение эпсилона, чтобы уменьшить исследование со временем        if self.epsilon > self.epsilon_end:            self.epsilon *= self.epsilon_decay        return action

Мы успешно реализовали политику эпсилон-жадного выбора и почти готовы позволить агенту учиться!

5. Влияние на окружающую среду: Завершение

Окружение в настоящее время имеет методы для сброса сетки, добавления агента и цели, предоставления текущего состояния и вывода сетки в консоль.

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

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

Цель вознаграждения - поощрять определенное поведение. В нашем случае мы хотим направить агента к целевой ячейке, заданной значением -1.

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

Два основных типа структур вознаграждения:

  • Сжатая: Когда вознаграждения даются только в нескольких состояниях.
  • Плотная: Когда вознаграждения распространены по всему пространству состояний.

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

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

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

Плотные структуры вознаграждения либо

  • имеют более одной цели.
  • дают подсказки на протяжении эпизода.

Тогда агент получает больше возможностей для изучения желаемого поведения.

Например, представьте, что вы обучаете агента использовать тело для ходьбы, и единственное вознаграждение, которое вы даёте ему, - это достижение цели. Агент может научиться добираться до неё, просто карабкаясь или катаясь по земле, или даже не научиться вообще.

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

Разрешение взаимодействия агента с окружающей средойЧтобы вообще были возможны вознаграждения, необходимо позволить агенту взаимодействовать со своим окружением. Давайте вернемся к классу Environment для определения этого взаимодействия.

...def move_agent(self, action):    # Сопоставление действия агента с соответствующими перемещениями    moves = {        0: (-1, 0), # Вверх        1: (1, 0),  # Вниз        2: (0, -1), # Влево        3: (0, 1)   # Вправо    }        previous_location = self.agent_location        # Определение новой позиции после выполнения хода    move = moves[action]    new_location = (previous_location[0] + move[0], previous_location[1] + move[1])        # Проверка на валидность хода    if self.is_valid_location(new_location):        # Удаляем агента из предыдущей позиции        self.grid[previous_location[0]][previous_location[1]] = 0                # Добавляем агента в новую позицию        self.grid[new_location[0]][new_location[1]] = 1                 # Обновляем позицию агента        self.agent_location = new_location            def is_valid_location(self, location):    # Проверка, находится ли позиция в пределах сетки    if (0 <= location[0] < self.grid_size) and (0 <= location[1] < self.grid_size):        return True    else:        return False

Приведенный выше код сначала определяет изменение координат, связанное с каждым значением действия. Если выбрано действие 0, то координаты изменяются на (-1, 0).

Помните, в данном сценарии координаты интерпретируются как (строка, столбец). Если строка уменьшается на единицу, агент перемещается вверх на одну ячейку, и если столбец уменьшается на единицу, агент перемещается влево на одну ячейку.

Затем вычисляется новая позиция на основе перемещения. Если новая позиция является валидной, обновляется agent_location. В противном случае agent_location остается прежним.

Также, is_valid_location просто проверяет, находится ли новая позиция в пределах границ сетки.

Это довольно просто, но что нам здесь не хватает? Обратная связь!

Предоставление обратной связиОкружение должно предоставлять соответствующее вознаграждение и информацию о завершении эпизода.

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

...def move_agent(self, action):    ...    done = False  # По умолчанию эпизод не завершен              # Проверка на валидность хода    if self.is_valid_location(new_location):        # Удаляем агента из предыдущей позиции        self.grid[previous_location[0]][previous_location[1]] = 0                # Добавляем агента в новую позицию        self.grid[new_location[0]][new_location[1]] = 1                 # Обновляем позицию агента        self.agent_location = new_location                # Проверка, является ли новая позиция местом вознаграждения        if self.agent_location == self.goal_location:            # Эпизод завершен            done = True        return done...

Мы установили done в значение false по умолчанию. Если новая позиция agent_location совпадает с goal_location, то done устанавливается в значение true. Наконец, мы возвращаем это значение.

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

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

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

...def move_agent(self, action):    ...    done = False # Эпизод по умолчанию не завершен    reward = 0   # Инициализируем награду              # Проверяем на допустимость действие    if self.is_valid_location(new_location):        # Удаляем агента из старого местоположения        self.grid[previous_location[0]][previous_location[1]] = 0                # Добавляем агента в новое местоположение        self.grid[new_location[0]][new_location[1]] = 1                 # Обновляем местоположение агента        self.agent_location = new_location                # Проверяем, является ли новое местоположение местом вознаграждения        if self.agent_location == self.goal_location:            # Награда за достижение цели            reward = 100                          # Эпизод завершен            done = True        else:            # Незначительное наказание за допустимое действие, которое не достигло цели            reward = -1    else:        # Несколько более серьезное наказание за недопустимое действие        reward = -3        return reward, done...

Убедитесь, что инициализируется переменная reward, чтобы к ней можно было получить доступ после условных блоков if. Также тщательно проверьте каждый случай: допустимое действие и достижение цели, допустимое действие и недостижение цели, и недопустимое действие.

Плотная система вознагражденийВоплотить нашу плотную систему вознаграждений на практике все еще довольно просто, просто требуется чаще предоставлять обратную связь.

Каким был бы хороший способ вознаграждать агента за движение в сторону цели более инкрементальным образом?

Первый способ - вернуть отрицание манхэттенского расстояния. Манхэттенское расстояние - это расстояние в стороне строки плюс расстояние в стороне столбца, а не путь в прямой линии. Вот как это выглядит в коде:

reward = -(np.abs(self.goal_location[0] - new_location[0]) + \           np.abs(self.goal_location[1] - new_location[1]))

Таким образом, количество шагов в стороне строки плюс количество шагов в стороне столбца со знаком минус.

Другой способ - предоставить вознаграждение на основе направления движения агента: если он отходит от цели, предоставить отрицательное вознаграждение, а если движется к ней, предоставить положительное вознаграждение.

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

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

Код для этого варианта:

...def move_agent(self, action):    ...        if self.agent_location == self.goal_location:            ...        else:            # Рассчитываем расстояние до перемещения            previous_distance = np.abs(self.goal_location[0] - previous_location[0]) + \                                np.abs(self.goal_location[1] - previous_location[1])                                # Рассчитываем расстояние после перемещения            new_distance = np.abs(self.goal_location[0] - new_location[0]) + \                           np.abs(self.goal_location[1] - new_location[1])                        # Если новое местоположение ближе к цели, награда = 1, если дальше, награда = -1            reward = (previous_distance - new_distance)    ...

Как видите, если агент не достиг цели, мы рассчитываем previous_distance, new_distance, а затем определяем reward как разницу между ними.

В зависимости от производительности может быть целесообразно масштабировать его или любую вознаграждение в системе. Вы можете сделать это, просто умножив на число (например, 0,01, 2, 100), если нужно увеличить его. Их пропорции должны эффективно направлять агента к цели. Например, награда 1 за приближение к цели и награда 0,1 за саму цель не имели бы особого смысла.

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

В итоге, если агент находится в 10 шагах от цели и переместился на клетку на 11 шагов, то reward будет равно -1.

Вот обновленный move_agent.

def move_agent(self, action):    # Сопоставляем действие агента с правильным движением    moves = {        0: (-1, 0), # Вверх        1: (1, 0),  # Вниз        2: (0, -1), # Влево        3: (0, 1)   # Вправо    }        previous_location = self.agent_location        # Определяем новое местоположение после применения действия    move = moves[action]    new_location = (previous_location[0] + move[0], previous_location[1] + move[1])        done = False # Эпизод по умолчанию не завершен    reward = 0   # Инициализируем награду              # Проверяем на допустимость действие    if self.is_valid_location(new_location):        # Удаляем агента из старого местоположения        self.grid[previous_location[0]][previous_location[1]] = 0                # Добавляем агента в новое местоположение        self.grid[new_location[0]][new_location[1]] = 1                 # Обновляем местоположение агента        self.agent_location = new_location                # Проверяем, является ли новое местоположение местом вознаграждения        if self.agent_location == self.goal_location:            # Награда за достижение цели            reward = 100                          # Эпизод завершен            done = True        else:            # Рассчитываем расстояние до перемещения            previous_distance = np.abs(self.goal_location[0] - previous_location[0]) + \                                np.abs(self.goal_location[1] - previous_location[1])                        # Рассчитываем расстояние после перемещения            new_distance = np.abs(self.goal_location[0] - new_location[0]) + \                           np.abs(self.goal_location[1] - new_location[1])                        # Если новое местоположение ближе к цели, награда = 1, если дальше, награда = -1            reward = (previous_distance - new_distance)    else:        # Несколько более серьезное наказание за недопустимое действие        reward = -3        return reward, done

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

Штраф за ходЕсть только одна вещь, которая нам не хватает.

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

Пути вознаграждения с и без штрафа за шаг — Изображение автора

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

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

Попробуйте представить себе петлю в каждом случае и посмотрите, накапливается ли штраф (или нет).

Давайте это реализуем.

...# Если new_location ближе к цели, reward = 0.9, если дальше, reward = -1.1reward = (previous_distance - new_distance) - 0.1...

Вот и все. Агент теперь будет поощряться выбирать кратчайший путь, что предотвращает поведение петель.

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

И вы будете правы.

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

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

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

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

Окончательный метод окружающей среды —step.С каждым компонентом Environment на месте мы теперь можем определить главное взаимодействие между агентом и окружающей средой.

К счастью, это довольно просто.

def step(self, action):    # Применить действие к окружающей среде, записать наблюдения    reward, done = self.move_agent(action)    next_state = self.get_state()      # Отображение сетки на каждом шаге    if self.render_on:        self.render()      return reward, next_state, done

step сначала перемещает агента в окружающей среде и записывает значение reward и done. Затем получает состояние, немедленно следующее за этим взаимодействием, next_state. Затем, если render_on установлено в значение true, сетка отображается.

Наконец, step возвращает записанные значения, reward, next_state и done.

Это будет существенным для построения опыта, на котором будет учиться наш агент.

Поздравляю! Вы официально завершили создание окружающей среды для вашей DRL тренажерной площадки.

Ниже представлен полный класс Environment .

import randomimport numpy as npclass Environment:    def __init__(self, grid_size, render_on=False):        self.grid_size = grid_size        self.render_on = render_on        self.grid = []        self.agent_location = None        self.goal_location = None    def reset(self):        # Инициализация пустой сетки в виде двумерного массива из 0s        self.grid = np.zeros((self.grid_size, self.grid_size))        # Добавление агента и цели в сетку        self.agent_location = self.add_agent()        self.goal_location = self.add_goal()        # Отображение начальной сетки        if self.render_on:            self.render()        # Возвращение начального состояния        return self.get_state()    def add_agent(self):        # Выбор случайной локации        location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))                # Агент представлен 1        self.grid[location[0]][location[1]] = 1        return location    def add_goal(self):        # Выбор случайной локации        location = (random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1))        # Получение случайной локации, пока она не будет свободной       

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

6. Извлекайте уроки из опыта: Replay опыта

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

Это можно сделать, сохранив опыт.

Каждый опыт состоит из нескольких вещей:

  • Состояние: Состояние перед выполнением действия.
  • Действие: Какое действие было выполнено в этом состоянии.
  • Вознаграждение: Положительная или отрицательная реакция, полученная агентом от окружения на основе его действия.
  • Следующее состояние: Состояние, непосредственно следующее за действием, позволяющее агенту действовать, не только основываясь на последствиях текущего состояния, но и на нескольких состояниях впереди.
  • Завершено: Указывает на конец опыта, сообщая агенту, было ли задание выполнено или нет. Он может быть либо true, либо false на каждом шаге.

Эти термины вам не должны быть новыми, но их всегда полезно видеть снова!

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

Класс ExperienceReplayЧтобы отслеживать и предоставлять эти опыты по мере необходимости, мы определим последний класс ExperienceReplay.

from collections import deque, namedtupleclass ExperienceReplay:    def __init__(self, capacity, batch_size):        # Память хранит опыт в виде двусторонней очереди, поэтому если превышается вместимость, удаляется        # самый старый элемент максимальной эффективностью        self.memory = deque(maxlen=capacity)        # Размер пакета указывает количество опытов, которые будут отбираться сразу        self.batch_size = batch_size        # Опыт - это именованный кортеж, который хранит соответствующую информацию для обучения        self.Experience = namedtuple('Experience', ['state', 'action', 'reward', 'next_state', 'done'])

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

Формирование опытов в пакетыЕсли вы помните, нейронная сеть в классе Agent принимает пакеты вводных данных. Хотя мы использовали только пакет размером один для предсказания, это было бы очень неэффективно для обучения. Обычно используют пакеты размером 32 или больше.

Формирование пакетов для обучения делает две вещи:

  • Повышает эффективность, потому что позволяет параллельную обработку нескольких точек данных, снижая вычислительные затраты и лучшим образом используя ресурсы GPU или CPU.
  • Помогает модели учиться более последовательно, поскольку она учится на разнообразных примерах одновременно, что может сделать ее лучше в обработке новых, не виданных данных.

Памятьmemory будет представлять собой очередь с двумя концами (deque). Это позволяет добавлять новые опыты в начало, и когда достигнута максимальная длина, опыты будут удаляться без необходимости сдвигать каждый элемент, как это происходит со списком Python. Это может значительно увеличить скорость при установке capacity на 10 000 и более.

ОпытКаждый опыт будет определен в виде namedtuple. Хотя многие другие структуры данных также подойдут, это улучшит читабельность, так как мы извлекаем каждую часть по мере надобности во время обучения.

add_experience и реализация sample_batchДобавление нового опыта и отбор пакета довольно просты.

import randomdef add_experience(self, state, action, reward, next_state, done):    # Создание нового опыта и сохранение его в памяти    experience = self.Experience(state, action, reward, next_state, done)    self.memory.append(experience)def sample_batch(self):    # Пакет будет случайной выборкой опытов из памяти размером batch_size    batch = random.sample(self.memory, self.batch_size)    return batch

Метод add_experience создает namedtuple с каждой частью опыта - state, action, reward, next_state и done, и добавляет его в memory.

sample_batch также прост в реализации. Он получает и возвращает случайную выборку из memory размером batch_size.

Хранение опыта повторного воспроизведения опыта для агента для формирования батчей и обучения — Картинка автора

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

def can_provide_sample(self):    # Определяет, превысило ли количество элементов в memory значение batch_size    return len(self.memory) >= self.batch_size

Завершенный класс ExperienceReplay...

import randomfrom collections import deque, namedtupleclass ExperienceReplay:    def __init__(self, capacity, batch_size):        # Memory хранит опыт в deque, поэтому если он превысит capacity, то самый старый элемент будет удален        self.memory = deque(maxlen=capacity)        # Batch size задает количество опыта, которое будет выбрано за один раз        self.batch_size = batch_size        # Experience - это именованный кортеж, который хранит необходимую информацию для обучения        self.Experience = namedtuple('Experience', ['state', 'action', 'reward', 'next_state', 'done'])    def add_experience(self, state, action, reward, next_state, done):        # Создаем новый опыт и сохраняем его в memory        experience = self.Experience(state, action, reward, next_state, done)        self.memory.append(experience)    def sample_batch(self):        # Batch - это случайный выбор опыта из memory размером batch_size        batch = random.sample(self.memory, self.batch_size)        return batch    def can_provide_sample(self):        # Определяет, превысило ли количество элементов в memory значение batch_size        return len(self.memory) >= self.batch_size

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

7. Определение процесса обучения агента: обучение нейронной сети

Цель при обучении нейронной сети - сделать так, чтобы производимые ею Q-значения точно отражали будущую награду, которую принесет каждый выбор.

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

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

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

Метод learn

import numpy as npdef learn(self, experiences):    states = np.array([experience.state for experience in experiences])    actions = np.array([experience.action for experience in experiences])    rewards = np.array([experience.reward for experience in experiences])    next_states = np.array([experience.next_state for experience in experiences])    dones = np.array([experience.done for experience in experiences])    # Предсказываем Q-значения (значения действий) для данного набора состояний    current_q_values = self.model.predict(states, verbose=0)    # Предсказываем Q-значения для набора следующих состояний    next_q_values = self.model.predict(next_states, verbose=0)    ...

Используя предоставленный набор данных experiences, мы извлечем каждую часть опыта с использованием генератора списка и значений namedtuple, которые мы определили ранее в ExperienceReplay. Затем мы преобразуем каждое значение в массив NumPy, чтобы улучшить эффективность и соответствовать ожиданиям модели, как было объяснено ранее.

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

Перед продолжением метода learn, мне нужно объяснить что-то, называемое коэффициентом дисконтирования.

Дисконтирование будущих вознаграждений — роль гаммаИнтуитивно мы знаем, что немедленные вознаграждения обычно имеют более высокий приоритет при равных условиях. (Вы хотите получить свою зарплату сегодня или на следующей неделе?)

Математическое представление этого может показаться менее интуитивным. Если мы рассматриваем будущее, мы не хотим, чтобы оно было так же важно (взвешено), как настоящее. Насколько мы уменьшаем влияние будущего, или насколько его влияние на каждое решение снижается, определяется коэффициентом гамма (обычно обозначается греческой буквой γ).

Гамма может быть настроена, при более высоких значениях гамма поощряется планирование, а при более низких значениях гамма поощряется более кратковременное поведение. Мы будем использовать значение 0.99 по умолчанию.

Коэффициент дисконтирования обычно находится в диапазоне от 0 до 1. Коэффициент дисконтирования больше 1, приоритезирующий будущее над настоящим, приведет к нестабильному поведению и имеет мало или вообще нет практических применений.

Реализация гамма и определение целевых Q-значенийНапомним, что в контексте обучения нейронной сети процесс зависит от двух ключевых элементов: входных данных, которые мы обеспечиваем, и соответствующих выходных данных, которые мы хотим, чтобы сеть научилась предсказывать.

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

Я знаю, что это много информации, но лучше всего она объясняется через реализацию и примеры.

import numpy as np...class Agent:    def __init__(self, grid_size, epsilon=1, epsilon_decay=0.995, epsilon_end=0.01, gamma=0.99):        ...        self.gamma = gamma        ...    ...    def learn(self, experiences):        ...        # Инициализируем целевые Q-значения текущими Q-значениями        target_q_values = current_q_values.copy()        # Проходимся по каждому опыту в пакете        for i in range(len(experiences)):            if dones[i]:                # Если эпизод закончен, нет следующего Q-значения                # [i, actions[i]] это numpy аналог [i][actions[i]]                target_q_values[i, actions[i]] = rewards[i]            else:                # Обновленное Q-значение - это вознаграждение плюс дисконтированное максимальное Q-значение для следующего состояния                # [i, actions[i]] это numpy аналог [i][actions[i]]                target_q_values[i, actions[i]] = rewards[i] + self.gamma * np.max(next_q_values[i])        ...

Мы определили атрибут класса gamma со значением по умолчанию 0.99.

Затем, после получения предсказания для state и next_state, что мы реализовали выше, мы инициализируем target_q_values текущими Q-значениями. Они будут обновлены в следующем цикле.

Обновление target_q_values Мы проходимся по каждому experience в пакете с двумя случаями обновления значения:

  • Если эпизод закончен, то target_q_value для этого действия просто равно полученному вознаграждению, потому что связанное next_q_value не имеет релевантности.
  • В противном случае эпизод не закончен, и target_q_value для этого действия становится равным полученному вознаграждению, плюс дисконтированное Q-значение предсказанного следующего действия в next_q_values.

Обновляем, если done истина:

target_q_values[i, actions[i]] = rewards[i]

Обновляем, если done ложно:

target_q_values[i, actions[i]] = rewards[i] + self.gamma * np.max(next_q_values[i])

Синтаксис здесь, target_q_values[i, actions[i]], может показаться запутанным, но это просто Q-значение i-го опыта для действия actions[i].

       Опыт в пакете   Вознаграждение от среды                v                    vtarget_q_values[i, actions[i]] = rewards[i]                       ^           Индекс выбранного действия

Это эквивалент NumPy к [i][actions[i]] в списках Python. Помните, что каждое действие - это индекс (от 0 до 3).

Как обновляются target_q_values

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

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

Предположим, что значения actions, rewards, dones, current_q_values и next_q_values следующие.

gamma = 0.99actions = [1, 2, 2]  # (down, left, left)rewards = [1, -1, 100] # Награды, получаемые средой за действиедones = [False, False, True] # Обозначает, завершена ли эпизодcurrent_q_values = [    [2, 5, -2, -3],  # В этом состоянии действие 2 (индекс 1) наиболее подходит до сих пор    [1, 3, 4, -1],   # Здесь действие 3 (индекс 2) в настоящее время предпочтительно    [-3, 2, 6, 1]    # Действие 3 (индекс 2) имеет самое высокое значение Q-функции в этом состоянии]next_q_values = [    [1, 4, -1, -2],  # Будущие значения Q-функций после выбора каждого действия из первого состояния    [2, 2, 5, 0],    # Будущие значения Q-функций из второго состояния    [-2, 3, 7, 2]    # Будущие значения Q-функций из третьего состояния]

Затем мы копируем current_q_values в target_q_values для обновления.

target_q_values = current_q_values

Затем, для каждого опыта в пакете, мы можем показать связанные значения.

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

Запись 1

i = 0 # Это первая запись в пакете (первая итерация)# Первые значения связанных значенийactions[i] = 1rewards[i] = 1dones[i] = Falsetarget_q_values[i] = [2, 5, -2, -3]next_q_values[i] = [1, 4, -1, -2]

Поскольку dones[i] равно false для этого опыта, нам нужно рассмотреть next_q_values и применить gamma (0.99).

target_q_values[i, actions[i]] = rewards[i] + 0.99 * max(next_q_values[i])

Почему нам нужно выбирать наибольшее значение из next_q_values[i]? Потому что это будет следующее выбранное действие, и нам нужно оценить награду (Q-значение).

Затем мы обновляем i-ое target_q_values в индексе, соответствующем actions[i], до награды за это состояние/действие плюс скидка на награду за следующее состояние/действие.

Вот целевые значения в этом опыте после обновления.

# Обновленные target_q_values[i]target_q_values[i] = [2, 4.96, -2, -3]                ^          ^              i = 0    actions[i] = 1

Как видите, для текущего состояния выбор 1 (down) теперь еще более желателен, потому что значение выше, и такое поведение было укреплено.

Возможно, вам поможет самому посчитать эти значения, чтобы прояснить все.

Запись 2

i = 1 # Это вторая запись в пакете# Вторые значения связанных значенийactions[i] = 2rewards[i] = -1dones[i] = Falsetarget_q_values[i] = [1, 3, 4, -1]next_q_values[i] = [2, 2, 5, 0]

dones[i] здесь также ложное значение, поэтому нам необходимо принять во внимание next_q_values.

target_q_values[i, actions[i]] = rewards[i] + 0.99 * max(next_q_values[i])

Опять же, обновляем target_q_values i-го опыта по индексу actions[i].

# Обновленные target_q_values[i]target_q_values[i] = [1, 3, 3.95, -1]                ^             ^              i = 1      action[i] = 2

Выбор 2 (влево) теперь менее желателен, поскольку значение Q стало ниже, и такое поведение не рекомендуется.

Элемент 3

Наконец, последний элемент в серии.

i = 2 # Это третий и последний элемент в серии# Вторые элементы связанных значенийactions[i] = 2rewards[i] = 100dones[i] = True target_q_values[i] = [-3, 2, 6, 1]next_q_values[i] = [-2, 3, 7, 2]

dones[i] для данного элемента равно true, что указывает на то, что эпизод завершен, и дальнейших действий не будет. Это означает, что мы не учитываем next_q_values в нашем обновлении.

target_q_values[i, actions[i]] = rewards[i]

Обратите внимание, что мы просто устанавливаем target_q_values[i, action[i]] в значение rewards[i], потому что больше действий не будет — нет будущего для рассмотрения.

# Обновленные target_q_values[i]target_q_values[i] = [-3, 2, 100, 1]                ^             ^              i = 2       action[i] = 2

Выбор 2 (влево) в этом и похожих состояниях теперь будет намного более желательным.

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

Хотя это может показаться довольно запутанным, идея состоит просто в том, чтобы создать обновленные значения Q, которые точно представляют награды, предоставленные окружением, для предоставления нейронной сети. Вот что должна приближать НС.

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

Эффект распространения наград по пространству состояний — изображение от автора

Выше представлена упрощенная версия значений Q и эффекта коэффициента дисконтирования, учитывающая только награду за цель, а не поэтапные награды или штрафы.

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

Этот эффект не наступает мгновенно. Агенту требуется исследование пространства состояний и действий, чтобы постепенно изучать и корректировать свою стратегию, создавая понимание того, как разные действия ведут к различным наградам со временем.

Если структура вознаграждения была тщательно разработана, это постепенно будет направлять нашего агента на принятие более выгодных действий.

Достроение нейронной сетиДля метода learn остается последний шаг — предоставить нейронной сети агента states и их связанные target_q_values. Затем TensorFlow обновит веса, чтобы более точно предсказывать эти значения для аналогичных состояний.

...def learn(self, experiences):    states = np.array([experience.state for experience in experiences])    actions = np.array([experience.action for experience in experiences])    rewards = np.array([experience.reward for experience in experiences])    next_states = np.array([experience.next_state for experience in experiences])    dones = np.array([experience.done for experience in experiences])    # Предсказываем Q-значения (значения действий) для данной партии состояний    current_q_values = self.model.predict(states, verbose=0)    # Предсказываем Q-значения для партии следующих состояний    next_q_values = self.model.predict(next_states, verbose=0)    # Инициализируем target Q-значения текущими Q-значениями    target_q_values = current_q_values.copy()    # Проходимся по каждому опыту в партии    for i in range(len(experiences)):        if dones[i]:            # Если эпизод завершен, нет следующего Q-значения            target_q_values[i, actions[i]] = rewards[i]        else:            # Обновленное Q-значение — награда плюс дисконтированное максимальное Q-значение для следующего состояния            # [i, actions[i]] — это numpy-эквивалент [i][actions[i]]            target_q_values[i, actions[i]] = rewards[i] + self.gamma * np.max(next_q_values[i])    # Обучаем модель    self.model.fit(states, target_q_values, epochs=1, verbose=0)

Единственная новая часть - self.model.fit(states, target_q_values, epochs=1, verbose=0). fit принимает два основных аргумента: входные данные и целевые значения, которых мы хотим достичь. В данном случае наш входной набор данных - пакет states, а целевые значения - обновленные Q-значения для каждого состояния.

epochs=1 просто задает количество попыток сети приспособиться к данным. Одной достаточно, потому что мы хотим, чтобы она хорошо обобщалась, а не приспосабливалась к этому конкретному пакету данных. verbose=0 просто указывает TensorFlow не печатать отладочные сообщения, такие как индикаторы прогресса.

Класс Agent теперь обладает способностью учиться на основе опыта, но для этого ему нужно два простых метода - save и load.

Сохранение и загрузка обученных моделейСохранение и загрузка модели позволяют нам избежать полной повторной тренировки при необходимости. Мы можем использовать простые методы TensorFlow, которые принимают только один аргумент, file_path.

from tensorflow.keras.models import load_modeldef load(self, file_path):    self.model = load_model(file_path)def save(self, file_path):    self.model.save(file_path)

Создайте каталог с именем models или любым другим и затем вы сможете сохранять обученную модель с заданным интервалом. Эти файлы имеют расширение .h5. Поэтому, когда вы хотите сохранить свою модель, просто вызываете agent.save(‘models/model_name.h5’). То же самое делается при загрузке.

Полный класс Agent

from tensorflow.keras.layers import Densefrom tensorflow.keras.models import Sequential, load_modelimport numpy as npclass Agent:    def __init__(self, grid_size, epsilon=1, epsilon_decay=0.998, epsilon_end=0.01, gamma=0.99):        self.grid_size = grid_size        self.epsilon = epsilon        self.epsilon_decay = epsilon_decay        self.epsilon_end = epsilon_end        self.gamma = gamma    def build_model(self):        # Создаем последовательную модель с 3 слоями        model = Sequential([            # Входной слой ожидает сглаженную сетку, поэтому форма входных данных - это квадрат grid_size            Dense(128, activation='relu', input_shape=(self.grid_size**2,)),            Dense(64, activation='relu'),            # Выходной слой с 4 блоками для возможных действий (вверх, вниз, влево, вправо)            Dense(4, activation='linear')        ])        model.compile(optimizer='adam', loss='mse')        return model        def get_action(self, state):        # rand() возвращает случайное значение от 0 до 1        if np.random.rand() <= self.epsilon:            # Исследование: случайное действие            action = np.random.randint(0, 4)        else:            # Добавляем дополнительное измерение к состоянию для создания пакета с одним экземпляром            state = np.expand_dims(state, axis=0)            # Используем модель для предсказания Q-значений (значений действий) для данного состояния            q_values = self.model.predict(state, verbose=0)            # Выбираем и возвращаем действие с наибольшим Q-значением            action = np.argmax(q_values[0]) # Берем действие из первой (и единственной) записи                # Уменьшаем значение epsilon для уменьшения исследования со временем        if self.epsilon > self.epsilon_end:            self.epsilon *= self.epsilon_decay        return action        def learn(self, experiences):        states = np.array([experience.state for experience in experiences])        actions = np.array([experience.action for experience in experiences])        rewards = np.array([experience.reward for experience in experiences])        next_states = np.array([experience.next_state for experience in experiences])        dones = np.array([experience.done for experience in experiences])        # Предсказываем Q-значения (значения действий) для данного пакета состояний        current_q_values = self.model.predict(states, verbose=0)        # Предсказываем Q-значения для пакета следующих состояний        next_q_values = self.model.predict(next_states, verbose=0)        # Инициализируем целевые Q-значения текущими Q-значениями        target_q_values = current_q_values.copy()        # Проходимся по каждому опыту в пакете        for i in range(len(experiences)):            if dones[i]:                # Если эпизод завершен, то следующего Q-значения нет                target_q_values[i, actions[i]] = rewards[i]            else:                # Обновленное Q-значение - награда плюс сниженное ожидаемое максимальное Q-значение для следующего состояния                # [i, actions[i]] является эквивалентом [i][actions[i]] в numpy                target_q_values[i, actions[i]] = rewards[i] + self.gamma * np.max(next_q_values[i])        # Обучаем модель        self.model.fit(states, target_q_values, epochs=1, verbose=0)        def load(self, file_path):        self.model = load_model(file_path)    def save(self, file_path):        self.model.save(file_path)

Каждый класс вашей глубокой обучающейся гимнастики по усилению теперь завершен! Вы успешно написали Agent, Environment и ExperienceReplay. Остался только основной цикл обучения.

8. Выполнение цикла обучения: все вместе

Мы находимся на финальном этапе проекта! Каждому модулю, которые мы написали - Agent, Environment и ExperienceReplay, нужен способ взаимодействия.

Это будет основная программа, где выполняется каждый эпизод, и где мы определяем наши гиперпараметры, такие как epsilon.

Хотя это довольно просто, я разобью каждую часть по мере ее написания, чтобы сделать все более ясным.

Инициализация каждой частиСначала мы устанавливаем grid_size и используем созданные нами классы для инициализации каждого экземпляра.

from environment import Environmentfrom agent import Agentfrom experience_replay import ExperienceReplayif __name__ == '__main__':    grid_size = 5    environment = Environment(grid_size=grid_size, render_on=True)    agent = Agent(grid_size=grid_size, epsilon=1, epsilon_decay=0.998, epsilon_end=0.01)    experience_replay = ExperienceReplay(capacity=10000, batch_size=32)    ...

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

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

Ограничение количества шагов помогает гарантировать, что наш агент не застревает в цикле и поощряет более короткие пути. Мы будем довольно щедрыми и для сетки 5х5 установим максимальное значение в 200. Для больших сред это значение нужно увеличить.

from environment import Environmentfrom agent import Agentfrom experience_replay import ExperienceReplayif __name__ == '__main__':    grid_size = 5    environment = Environment(grid_size=grid_size, render_on=True)    agent = Agent(grid_size=grid_size, epsilon=1, epsilon_decay=0.998, epsilon_end=0.01)    experience_replay = ExperienceReplay(capacity=10000, batch_size=32)        # Количество эпизодов до остановки тренировки    episodes = 5000    # Максимальное количество шагов в каждом эпизоде    max_steps = 200    ...

Цикл эпизодовВ каждом эпизоде мы сбрасываем среду и сохраняем начальное состояние. Затем мы выполняем каждый шаг до тех пор, пока не выполнены условия done или достигнут предел max_steps. Наконец, мы сохраняем модель. Логика для каждого шага еще не реализована.

from environment import Environmentfrom agent import Agentfrom experience_replay import ExperienceReplayif __name__ == '__main__':    grid_size = 5    environment = Environment(grid_size=grid_size, render_on=True)    agent = Agent(grid_size=grid_size, epsilon=1, epsilon_decay=0.998, epsilon_end=0.01)    experience_replay = ExperienceReplay(capacity=10000, batch_size=32)        # Количество эпизодов до остановки тренировки    episodes = 5000    # Максимальное количество шагов в каждом эпизоде    max_steps = 200    for episode in range(episodes):        # Получаем начальное состояние среды и устанавливаем done как False        state = environment.reset()        # Запускаем цикл до завершения эпизода        for step in range(max_steps):            # Логика для каждого шага            ...            if done:                break            agent.save(f'models/model_{grid_size}.h5')

Обратите внимание, что мы называем модель, используя grid_size, потому что архитектура НС будет отличаться для каждого размера входных данных. Попытка загрузить модель размером 5х5 в архитектуру 10х10 вызовет ошибку.

Логика шагаНаконец, внутри цикла шагов мы расставим взаимодействие между каждой частью, как обсуждалось ранее.

from environment import Environmentfrom agent import Agentfrom experience_replay import ExperienceReplayif __name__ == '__main__':    grid_size = 5    environment = Environment(grid_size=grid_size, render_on=True)    agent = Agent(grid_size=grid_size, epsilon=1, epsilon_decay=0.998, epsilon_end=0.01)    experience_replay = ExperienceReplay(capacity=10000, batch_size=32)        # Количество эпизодов до остановки тренировки    episodes = 5000    # Максимальное количество шагов в каждом эпизоде    max_steps = 200    for episode in range(episodes):        # Получаем начальное состояние среды и устанавливаем done как False        state = environment.reset()        # Запускаем цикл до завершения эпизода        for step in range(max_steps):            print('Episode:', episode)            print('Step:', step)            print('Epsilon:', agent.epsilon)            # Получаем выбор действия из политики агента            action = agent.get_action(state)            # Производим шаг в среде и сохраняем опыт            reward, next_state, done = environment.step(action)            experience_replay.add_experience(state, action, reward, next_state, done)            # Если память опыта имеет достаточно записей, чтобы предоставить выборку, обучаем агента            if experience_replay.can_provide_sample():                experiences = experience_replay.sample_batch()                agent.learn(experiences)            # Устанавливаем состояние как следующее состояние            state = next_state                        if done:                break            agent.save(f'models/model_{grid_size}.h5')

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

После печати информации мы используем политику агента agent, чтобы получить действие action на основе текущего state и выполнить шаг в environment, записывая возвращаемые значения.

Затем мы сохраняем state, action, reward, next_state и done как опыт. Если у experience_replay есть достаточно памяти, мы тренируем агента на случайной выборке experiences.

Наконец, мы устанавливаем state равным next_state и проверяем, закончен ли эпизод.

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

После инициализации agent просто используйте его метод загрузки по аналогии с тем, как мы сохраняли — agent.load(f'models/model_{grid_size}.h5')

Вы также можете добавить небольшую задержку на каждом шаге при оценке модели с помощью команды time.sleep(0.5). Это приводит к паузе каждого шага на полсекунды. Убедитесь, что вы импортируете import time.

Завершенный цикл тренировки

from environment import Environmentfrom agent import Agentfrom experience_replay import ExperienceReplayimport timeif __name__ == '__main__':    grid_size = 5    environment = Environment(grid_size=grid_size, render_on=True)    agent = Agent(grid_size=grid_size, epsilon=1, epsilon_decay=0.998, epsilon_end=0.01)    # agent.load(f'models/model_{grid_size}.h5')    experience_replay = ExperienceReplay(capacity=10000, batch_size=32)        # Количество эпизодов перед окончанием тренировки    episodes = 5000    # Максимальное количество шагов в каждом эпизоде    max_steps = 200    for episode in range(episodes):        # Получаем начальное состояние окружения и устанавливаем done в False        state = environment.reset()        # Продолжаем цикл, пока эпизод не завершится        for step in range(max_steps):            print('Эпизод:', episode)            print('Шаг:', step)            print('Epsilon:', agent.epsilon)            # Получаем выбор действия из политики агента            action = agent.get_action(state)            # Совершаем шаг в окружении и сохраняем опыт            reward, next_state, done = environment.step(action)            experience_replay.add_experience(state, action, reward, next_state, done)            # Если опытное воспроизведение имеет достаточно памяти для предоставления выборки, обучаем агента            if experience_replay.can_provide_sample():                experiences = experience_replay.sample_batch()                agent.learn(experiences)            # Устанавливаем следующее состояние как текущее            state = next_state                        if done:                break                        # По желанию, делаем паузу в полсекунды для оценки модели            # time.sleep(0.5)        agent.save(f'models/model_{grid_size}.h5')

Когда вам нужны time.sleep или agent.load, вы можете просто закомментировать их.

Запуск программыЗапустите ее! Вы должны суметь успешно тренировать агента для достижения цели до среды сетки размером примерно 8х8. При увеличении размера сетки больше этого тренировка начинает затрудняться.

Попробуйте увеличить размер сетки. Вы можете сделать несколько вещей, таких как добавление слоев и нейронов в нейронную сеть, изменение epsilon_decay или увеличение времени для тренировки. Это поможет закрепить ваше понимание каждой части.

Например, вы можете заметить, что epsilon достигает значения epsilon_end достаточно быстро. Не бойтесь изменить значение epsilon_decay, например, на 0.9998 или 0.99998, если хотите.

При росте размера сетки состояние, передаваемое в сеть, становится экспоненциально больше.

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

9. Заключение

Поздравляю с успешным прохождением этого обширного путешествия через мир Усиления и Глубокого Q-обучения!

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

В этом руководстве вы:

  • Познакомились с основными концепциями обучения с подкреплением и почему это критическая область в ИИ.
  • Создали простое окружение, закладывая основу для взаимодействия агента и обучения.
  • Определили архитектуру нейронной сети агента для использования с Глубоким Q-обучением, позволяя вашему агенту принимать решения в более сложных средах, чем в традиционном Q-обучении.
  • Поняли, почему исследование важно перед использованием выученной стратегии и реализовали политику Epsilon-Greedy.
  • Реализовали систему вознаграждений для направления агента к цели и узнали разницу между разреженными и плотными вознаграждениями.
  • Разработали механизм повторного проигрывания опыта, позволяющий агенту учиться на основе прошлых событий.
  • Получили практический опыт в обучении нейронной сети, критическом процессе, при котором агент улучшает свою производительность на основе обратной связи от окружающей среды.
  • Собрали все эти компоненты в цикле обучения, наблюдая за процессом обучения агента в действии и настраивая его для достижения оптимальной производительности.

К настоящему моменту вы должны чувствовать себя уверенно в своем понимании Обучения с подкреплением и Глубокого Q-обучения. Вы построили прочную основу не только в теории, но и в практическом применении, создавая DRL-площадку с нуля.

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

Agar.io inspired game where agents are encouraged to eat one another to win — GIF by author

Выше показана сетевая игра, вдохновленная Agar.io, где агентам стимулируется рост за счет поедания друг друга. На каждом шаге окружение было изображено на графике с использованием библиотеки Python Matplotlib. Квадраты вокруг агентов - это их поле зрения. Это передается им в качестве их состояния из окружающей среды в виде сплющенной сетки, так же, как мы сделали в нашей системе.

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

Тем не менее, Глубокое Q-обучение подходит только для дискретного пространства действий — такого, в котором есть конечное число различных действий. Для непрерывного пространства действий, например, в физической среде, вам потребуются другие методы в мире DRL.

10. Бонус: Оптимизация представления состояния

Верите или нет, способ, которым мы сейчас представляем состояние, не является оптимальным для этого использования.

На самом деле, он крайне неэффективен.

Для сетки размером 100x100 существует 99,990,000 возможных состояний. Модель не только должна быть довольно большой, учитывая размер входного значения — 10,000 значений, но и требует значительного объема обучающих данных. В зависимости от вычислительных ресурсов, которыми вы располагаете, это может занять дни или недели.

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

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

Лучший способХотя есть несколько способов сделать это, самым простым и, вероятно, наиболее эффективным является использование относительного расстояния до цели.

Вместо того, чтобы состояние для сетки 5х5 выглядело так:

[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0]

Это можно представить всего двумя значениями:

[-2, -1]

Использование этого метода позволит сократить пространство состояний сетки размером 100x100 с 99,990,000 до 39,601!

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

Это позволяет модели исследовать только часть пространства состояний.

25x25 карты принятия решений агента в каждой ячейке с целью в центре — Анимация автора

На рисунке представлен прогресс обучения модели на сетке 25x25. Он показывает цветовую кодировку выбора агента в каждой ячейке с целью в центре.

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

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

Это также применимо для целей в любом месте.

Четыре 25x25 карты принятия решений модели, примененные к разным целям — Изображение автора

И, наконец, модель невероятно хорошо обобщает свои знания.

201x201 тепловая карта решений модели 25x25, отображающая обобщение — Изображение автора

Эта модель видела только сетку размером 25x25, но она может использовать свою стратегию в среде гораздо большего размера — 201x201. При этом имеется 1,632,200,400 комбинаций агента и цели!

Давайте обновим наш код с этим революционным улучшением.

РеализацияНа самом деле, нам не нужно сделать многое, чтобы это заработало, к счастью.

Первое, что нужно сделать, это обновить get_state в Environment.

def get_state(self):    # Вычисляем расстояние по строкам и столбцам    relative_distance = (self.agent_location[0] - self.goal_location[0],                         self.agent_location[1] - self.goal_location[1])        # Распаковываем кортежи в массив numpy    state = np.array([*relative_distance])    return state

Вместо сплющенной версии сетки мы вычисляем расстояние до цели и возвращаем его как массив NumPy. Оператор * просто распаковывает кортежи в отдельные компоненты. Это имеет такой же эффект, как и вот это - state = np.array([relative_distance[0], relative_distance[1]).

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

def move_agent(self, action):    ...    else:        # Тот же штраф за неправильное движение        reward = -1.1            return reward, done

Обновление нейронной архитектурыВ данный момент наша модель TensorFlow выглядит следующим образом. Я опустил все остальное для простоты.

class Agent:    def __init__(self, grid_size, ...):        self.grid_size = grid_size        ...        self.model = self.build_model()    def build_model(self):        # Создаем последовательную модель с 3 слоями        model = Sequential([            # Входной слой ожидает сплющенную сетку, поэтому форма ввода - квадрат размером grid_size            Dense(128, activation='relu', input_shape=(self.grid_size**2,)),            Dense(64, activation='relu'),            # Выходной слой с 4 блоками для возможных действий (вверх, вниз, влево, вправо)            Dense(4, activation='linear')        ])        model.compile(optimizer='adam', loss='mse')        return model    ...

Если вы помните, нашей модели архитектуры нужен постоянный вход. В этом случае размер входа зависит от grid_size.

С обновленным представлением состояния каждое состояние будет иметь всего два значения, независимо от значения grid_size. Мы можем обновить модель, чтобы она ожидала это. Кроме того, мы можем вообще удалить self.grid_size, потому что класс Agent уже не полагается на него.

class Agent:    def __init__(self, ...):        ...        self.model = self.build_model()    def build_model(self):        # Создаем последовательную модель с 3 слоями        model = Sequential([            # Входной слой ожидает выравненную сетку, поэтому форма ввода - квадратный grid_size.            Dense(64, activation='relu', input_shape=(2,)),            Dense(32, activation='relu'),            # Выходной слой с четырьмя единицами для возможных действий (вверх, вниз, влево, вправо)            Dense(4, activation='linear')        ])        model.compile(optimizer='adam', loss='mse')        return model    ...

Параметр input_shape ожидает кортеж, представляющий состояние входа.

(2,) указывает на одномерный массив с двумя значениями. Примерно так:

[-2, 0]

А (2,1), двумерный массив, указывает на две строки и один столбец. Примерно так:

[[-2], [0]]

Наконец, мы уменьшили количество нейронов в наших скрытых слоях до 64 и 32 соответственно. С этим простым представлением состояния это, возможно, все еще избыточно, но должно работать достаточно быстро.

Когда вы начинаете тренировку, попробуйте увидеть, сколько нейронов вам нужно, чтобы модель эффективно обучалась. Вы даже можете попробовать удалить второй слой, если хотите.

Исправление основного цикла обученияОсновной цикл обучения требует очень небольших изменений. Давайте обновим его в соответствии с нашими изменениями.

from environment import Environmentfrom agent import Agentfrom experience_replay import ExperienceReplayimport timeif __name__ == '__main__':    grid_size = 5    environment = Environment(grid_size=grid_size, render_on=True)    agent = Agent(epsilon=1, epsilon_decay=0.998, epsilon_end=0.01)    # agent.load(f'models/model.h5')    experience_replay = ExperienceReplay(capacity=10000, batch_size=32)        # Количество эпизодов, прежде чем обучение прекратится    episodes = 5000    # Максимальное количество шагов в каждом эпизоде    max_steps = 200    for episode in range(episodes):        # Получаем начальное состояние окружающей среды и устанавливаем done в False        state = environment.reset()        # Цикл до завершения эпизода        for step in range(max_steps):            print('Эпизод:', episode)            print('Шаг:', step)            print('Epsilon:', agent.epsilon)            # Получаем выбор действия из политики агента            action = agent.get_action(state)            # Делаем шаг в окружающей среде и сохраняем опыт            reward, next_state, done = environment.step(action)            experience_replay.add_experience(state, action, reward, next_state, done)            # Если опытное воспроизведение имеет достаточно памяти, чтобы предоставить выборку, обучаем агента            if experience_replay.can_provide_sample():                experiences = experience_replay.sample_batch()                agent.learn(experiences)            # Устанавливаем состояние в следующее состояние            state = next_state                        if done:                break            # При желании, приостанавливаемся на полсекунды, чтобы оценить модель            # time.sleep(0.5)        agent.save(f'models/model.h5')

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

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

Если вам интересно ExperienceReplay, оно останется без изменений.

Обратите внимание, что нет универсального представления состояния. В некоторых случаях может быть разумным предоставить полную сетку, как мы сделали, или ее подразделение, как я сделал с многоразовой системой в разделе 9. Цель состоит в том, чтобы найти баланс между упрощением пространства состояний и предоставлением достаточной информации для обучения агента.

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

Каждый из них, о котором мы говорили, включает:

  • epsilon, epsilon_decay, epsilon_end (исследование/использование)
  • gamma (коэффициент дисконтирования)
  • количество нейронов и слоев
  • batch_size, capacity (воспроизведение опыта)
  • max_steps

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

Скорость обучения Скорость обучения (LR) является гипер-параметром модели нейронной сети.

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

Значения LR обычно находятся в диапазоне от 1 до 0,0000001, причем наиболее распространенными являются значения, такие как 0,01, 0,001 и 0,0001.

Под-оптимальная скорость обучения, которая может никогда не сходиться к оптимальной стратегии — Картинка автора

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

Хотя эти диаграммы по скорости обучения являются сильно упрощенными, они должны передать основную идею.

Под-оптимальная скорость обучения, вызывающая экспоненциальный рост Q-значений — Картинка автора

С другой стороны, скорость обучения, которая слишком высокая, может вызвать «взрыв» значений или их увеличение. Корректировки, которые модель производит, слишком велики, что приводит к расхождению — или ухудшению со временем.

Какая является идеальной скоростью обучения? Какова длина спички?

Во многих случаях вам просто приходится использовать простую методику проб и ошибок. Хороший способ определить, является ли скорость обучения проблемой, - это проверить вывод модели.

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

Мне это не давало смысла, поэтому я решил посмотреть на выходные значения Q-значений модели в методе Agent get_action.

Шаг 10[[ 0.29763165 0.28393078 -0.01633328 -0.45749056]]Шаг 50[[ 7.173178 6.3558702 -0.48632553 -3.1968129 ]]Шаг 100[[ 33.015953 32.89661 33.11674 -14.883122]]Шаг 200[[573.52844 590.95685 592.3647 531.27576]]...Шаг 5000[[37862352. 34156752. 35527612. 37821140.]]

Это пример взрывных значений.

В TensorFlow для настройки весов мы используем оптимизатор Adam с изначальной скоростью обучения 0,001. Для этого конкретного случая она оказалась слишком высокой.

Сбалансированная скорость обучения, в конечном итоге сходящаяся к оптимальной стратегии — Картинка автора

После тестирования различных значений, наилучшим вариантом кажется значение 0.00001.

Давайте внедрим это.

from tensorflow.keras.optimizers import Adam
def build_model(self):    
    # Создаем последовательную модель с 3 слоями    
    model = Sequential([        
        # Входной слой ожидает выровненную сетку, поэтому форма входных данных - это квадрат размером grid_size        
        Dense(64, activation='relu', input_shape=(2,)),       
        Dense(32, activation='relu'),        
        # Выходной слой с 4 единицами для возможных действий (вверх, вниз, влево, вправо)        
        Dense(4, activation='linear')    
    ])         
    # Обновляем скорость обучения    
    optimizer = Adam(learning_rate=0.00001)    
    # Компилируем модель с пользовательским оптимизатором    
    model.compile(optimizer=optimizer, loss='mse')    
    return model

Не стесняйтесь изменять это и наблюдать, как это влияет на значения Q. Также убедитесь, что вы импортируете модуль Adam.

Наконец, вы можете снова начать обучение!

Код тепловой картыНиже приведен код для построения собственной тепловой карты, как показано ранее.

import matplotlib.pyplot as plt
import numpy as np
from tensorflow.keras.models import load_model

def generate_heatmap(episode, grid_size, model_path):    
    # Загрузить модель    
    model = load_model(model_path)        
    goal_location = (grid_size // 2, grid_size // 2)  # Центр сетки    
    # Инициализация массива для хранения интенсивности цвета    
    heatmap_data = np.zeros((grid_size, grid_size, 3))    
    # Определение цветов для каждого действия    
    colors = {        
        0: np.array([0, 0, 1]),  # Синий для вверха        
        1: np.array([1, 0, 0]),  # Красный для вниза        
        2: np.array([0, 1, 0]),  # Зеленый для влево        
        3: np.array([1, 1, 0])   # Желтый для вправо    
    }    
    # Вычисление значений Q для каждого состояния и определение интенсивности цвета    
    for x in range(grid_size):        
        for y in range(grid_size):            
            relative_distance = (x - goal_location[0], y - goal_location[1])            
            state = np.array([*relative_distance]).reshape(1, -1)            
            q_values = model.predict(state)            
            best_action = np.argmax(q_values)            
            if (x, y) == goal_location:                
                heatmap_data[x, y] = np.array([1, 1, 1])            
            else:                
                heatmap_data[x, y] = colors[best_action]    
    # Построение тепловой карты    
    plt.imshow(heatmap_data, interpolation='nearest')    
    plt.xlabel(f'Episode: {episode}')    
    plt.axis('off')    
    plt.tight_layout(pad=0)    
    plt.savefig(f'./figures/heatmap_{grid_size}_{episode}', bbox_inches='tight')

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

Следующие шагиПосле того, как вы успешно обучили модель и экспериментировали с гиперпараметрами, я рекомендую вам на самом деле сделать ее собственной.

Некоторые идеи для расширения системы:

  • Добавить препятствия между агентом и целью
  • Создать более разнообразную среду, возможно, с случайно генерируемыми комнатами и путями
  • Реализовать систему многих агентов совместного сотрудничества/соревнования - прятки
  • Создать игру вдохновленную Pong
  • Реализовать управление ресурсами, такое как система голода или энергии, где агент должен собирать еду по пути к цели

Вот пример, выходящий за рамки нашей простой сетки:

Игра, вдохновленная Flappy Bird, где агент должен избегать труб, чтобы выжить — GIF авторства

Используя Pygame, популярную библиотеку Python для создания 2D-игр, я создал клон Flappy Bird. Затем я определил взаимодействия, ограничения и структуру вознаграждения в нашем предварительно созданном классе Environment.

Я представил состояние как текущую скорость и местоположение агента, расстояние до ближайшей трубы и положение открытия.

Для класса Agent я просто изменил размер входных данных на (4,), добавил несколько слоев в НС и обновил сеть, чтобы она выводила только два значения - прыгать или не прыгать.

Вы можете найти и запустить это в директории flappy_bird на репозитории GitHub repo. Обязательно выполните установку pip install pygame.

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

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

Надеюсь, создание собственного DRL-гимна открыло вам глаза на красоту и великолепие искусственного интеллекта и вдохновило вас к погружению еще глубже.

Эта статья была вдохновлена книгой Создание нейронных сетей с нуля на Python и серией на YouTube от Харрисона Кинсли (sentdex) и Даниэля Кукел. Беседующий стиль и код, написанный с нуля, действительно укрепили мое понимание нейронных сетей.