Обучение с подкреплением и имитация поведения человека с помощью MineRL

В случайно сгенерированном мире Minecraft найдём алмазы с помощью ИИ. Как обученный с подкреплением агент проявит себя в одной из самых сложных задач игры? Подробностями делимся к старту флагманского курса по Data Science.
Minecraft — масштабная игра с большим количеством механик и сложных последовательностей действий. Чтобы просто научить людей играть в эту игру, написали целую энциклопедию на 8000 страниц.

Обсуждаемое не ограничивается только Minecraft; подход можно применять в таких же сложных средах. Строго говоря, мы реализуем два разных метода, которые станут о��новой нашего интеллектуального агента.
Но, прежде чем обучать агента, необходимо понять, как взаимодействовать со средой. Начнём со скриптового бота и познакомимся с синтаксисом. Работать мы будем с MineRL — потрясающей библиотекой для создания приложений искусственного интеллекта в Minecraft.
Код из статьи доступен в Google Colab. Это упрощённая и доработанная версия потрясающих блокнотов, написанных организаторами турнира MineRL 2021 (лицензия MIT).
I. Скриптовый бот
MineRL позволяет запускать Minecraft в Python и взаимодействовать с игрой. Взаимодействие реализовано через популярную библиотеку gym:
env = gym.make('MineRLObtainDiamond-v0')
env.seed(21)
Мы стоим перед деревом. Как вы видите, разрешение довольно низкое. В низком разрешении используется меньше пикселей, что ускоряет работу. К счастью для нас, нейронным сетям не нужно разрешение 4К, чтобы понять происходящее.
Мы хотим взаимодействовать с игрой. Что может делать наш агент? Вот список возможных действий:

Первый шаг при поиске алмазов — добыть древесину для изготовления верстака и деревянной кирки.
Давайте постараемся подойти к дереву поближе. То есть нам нужно удерживать кнопку Forward меньше секунды. В MineRL обрабатывается по 20 действий в секунду: нам не нужна целая секунда, поэтому давайте повторим действие Forward 5 раз и подождём ещё 40 тактов:

# Define the sequence of actions
script = ['forward'] * 5 + [''] * 40
env = gym.make('MineRLObtainDiamond-v0')
env = Recorder(env, './video', fps=60)
env.seed(21)
obs = env.reset()
for action in script:
# Get the action space (dict of possible actions)
action_space = env.action_space.noop()
# Activate the selected action in the script
action_space[action] = 1
# Update the environment with the new action space
obs, reward, done, _ = env.step(action_space)
env.release()
env.play()
Отлично, а сейчас срубим дерево. В общей сложности от нас требуется 4 действия:
Forward — встать перед деревом;
Attack — срубить дерево;
Camera — посмотреть вверх или вниз;
Jump — получить готовый кусок древесины.

Управляться с камерой может быть трудновато. Для упрощения синтаксиса мы задействуем функцию str_to_act из этого GitHub-репозитория (лицензия MIT). Новый скрипт теперь выглядит так:
script = []
script += [''] * 20
script += ['forward'] * 5
script += ['attack'] * 61
script += ['camera:[-10,0]'] * 7 # Look up
script += ['attack'] * 240
script += ['jump']
script += ['forward'] * 10 # Jump forward
script += ['camera:[-10,0]'] * 2 # Look up
script += ['attack'] * 150
script += ['camera:[10,0]'] * 7 # Look down
script += [''] * 40
for action in tqdm(script):
obs, reward, done, _ = env.step(str_to_act(env, action))
env.release()
env.play()Агент успешно срубил целое дерево. Неплохо для начала, но от ИИ хотелось бы побольше самостоятельности…
II. Глубокое обучение
Наш бот хорошо работает в фиксированной среде. Но что случится, если мы поменяем затравку или её начальную точку? Всё прописано в скрипте, так что агент, скорее всего, попытается срубить несуществующее дерево.
Такой подход слишком статичен для наших целей: нам нужно что-то, что могло бы адаптироваться к новым средам. И вместо заданных в скрипте команд нам нужен искусственный интеллект, который бы знал, как рубить деревья. Конечно же, подходящей основой для обучения такого агента служит обучение с подкреплением. Если точнее, то подходящим вариантом будет глубокое обучение с подкреплением, поскольку мы обрабатываем изображения, из которых выбираются правильные действия.
Существует два способа реализации глубокого обучения с подкреплением:
Истинное глубокое обучение с подкреплением: агент обучается с нуля, взаимодействуя со средой. Он поощряется каждый раз, когда срубает дерево.
Имитационное обучение: агент учится, как рубить деревья, исходя из набора данных. Здесь данные — это последовательность действий по рубке деревьев, выполняемая человеком.
Оба подхода дают один и то же результат, но они не одинаковы. Авторы турнира MineRL 2021 пишут, что одинаковый уровень производительности достигается в обучении с подкреплением за 8 часов, а в имитационном обучении — за 15 минут.
У нас нет столько времени, поэтому что мы останавливаемся на варианте с имитационным обучением. Эта техника также называется клонированием поведения, то есть простейшей формой имитации.
Обратите внимание, что имитационное обучение не всегда бывает эффективнее обучения с подкреплением. Если хотите почитать подробнее, то Кумар с соавторами написал об этом отличную статью в блоге.

Проблема сводится к задаче классификации с несколькими классами данных. Набор данных состоит из видео в mp4, так что мы воспользуемся свёрточной нейронной сетью (CNN), чтобы перевести данные изображения в соответствующие действия. Кроме того, наша цель — ограничить количество возможных действий (классов). Таким образом, у CNN останется меньше доступных вариантов, и обучение пройдёт эффективнее:
class CNN(nn.Module):
def __init__(self, input_shape, output_dim):
super().__init__()
n_input_channels = input_shape[0]
self.cnn = nn.Sequential(
nn.Conv2d(n_input_channels, 32, kernel_size=8, stride=4),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.Conv2d(32, 64, kernel_size=4, stride=2),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64, 64, kernel_size=3, stride=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Flatten(),
nn.Linear(1024, 512),
nn.ReLU(),
nn.Linear(512, output_dim)
)
def forward(self, observations):
return self.cnn(observations)
def dataset_action_batch_to_actions(dataset_actions, camera_margin=5):
...
class ActionShaping(gym.ActionWrapper):
...В этом примере мы вручную определили 7 подходящих действий: Attack — атака, Forward — движение вперёд, Jump (прыжок) и движение камеры влево, вправо, вверх и вниз. Ещё один популярный подход — использовать метод k-средних для автоматического подбора релевантных действий человека. В любом случае смысл в том, чтобы убрать самые бесполезные для решения задачи действия. Наша задача — изготовление (крафтинг) вещей.
Давайте обучим нашу CNN на наборе данных MineRLTreechop-v0. Другие наборы данных можно найти здесь. Мы выбрали скорость обучения 0,0001 и 6 эпох с размером пакетов в 32:
# Get data
minerl.data.download(directory='data', environment='MineRLTreechop-v0')
data = minerl.data.make("MineRLTreechop-v0", data_dir='data', num_workers=2)
# Model
model = CNN((3, 64, 64), 7).cuda()
optimizer = torch.optim.Adam(model.parameters(), lr=0.0001)
criterion = nn.CrossEntropyLoss()
# Training loop
step = 0
losses = []
for state, action, _, _, _ \
in tqdm(data.batch_iter(num_epochs=6, batch_size=32, seq_len=1)):
# Get pov observations
obs = state['pov'].squeeze().astype(np.float32)
# Transpose and normalize
obs = obs.transpose(0, 3, 1, 2) / 255.0
# Translate batch of actions for the ActionShaping wrapper
actions = dataset_action_batch_to_actions(action)
# Remove samples with no corresponding action
mask = actions != -1
obs = obs[mask]
actions = actions[mask]
# Update weights with backprop
logits = model(torch.from_numpy(obs).float().cuda())
loss = criterion(logits, torch.from_numpy(actions).long().cuda())
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Print loss
step += 1
losses.append(loss.item())
if (step % 4000) == 0:
mean_loss = sum(losses) / len(losses)
tqdm.write(f'Step {step:>5} | Training loss = {mean_loss:.3f}')
losses.clear()Step 4000 | Training loss = 0.878
Step 8000 | Training loss = 0.826
Step 12000 | Training loss = 0.805
Step 16000 | Training loss = 0.773
Step 20000 | Training loss = 0.789
Step 24000 | Training loss = 0.816
Step 28000 | Training loss = 0.769
Step 32000 | Training loss = 0.777
Step 36000 | Training loss = 0.738
Step 40000 | Training loss = 0.751
Step 44000 | Training loss = 0.764
Step 48000 | Training loss = 0.732
Step 52000 | Training loss = 0.748
Step 56000 | Training loss = 0.765
Step 60000 | Training loss = 0.735
Step 64000 | Training loss = 0.716
Step 68000 | Training loss = 0.710
Step 72000 | Training loss = 0.693
Step 76000 | Training loss = 0.695Модель обучена. Теперь мы можем создать экземпляр объекта среды и посмотреть, как поведёт себя модель. Если обучение прошло успешно, модель должна безостановочно рубить все деревья в зоне видимости.
В этот раз мы воспользуемся обёрткой ActionShaping. Она нужна для сопоставления массива чисел, созданного с помощью dataset_action_batch_to_actions, с дискретными действиями из MineRL.
Нашей модели нужно наблюдение от первого лица в корректном формате, а на выходе она выводит логиты. С помощью функции softmax эти логиты можно перевести в распределение вероятностей в наборе из 7 действий. Далее случайным образом выбираем действие по его вероятности. Выбранное действие реализуется в MineRL через env.step(action).
Этот процесс можно повторять сколько угодно раз. Повторим его 1 000 раз и посмотрим, что получится:
model = CNN((3, 64, 64), 7).cuda()
model.load_state_dict(torch.load('model.pth'))
env = gym.make('MineRLObtainDiamond-v0')
env1 = Recorder(env, './video', fps=60)
env = ActionShaping(env1)
action_list = np.arange(env.action_space.n)
obs = env.reset()
for step in tqdm(range(1000)):
# Get input in the correct format
obs = torch.from_numpy(obs['pov'].transpose(2, 0, 1)[None].astype(np.float32) / 255).cuda()
# Turn logits into probabilities
probabilities = torch.softmax(model(obs), dim=1)[0].detach().cpu().numpy()
# Sample action according to the probabilities
action = np.random.choice(action_list, p=probabilities)
obs, reward, _, _ = env.step(action)
env1.release()
env1.play()Наш агент довольно хаотичный, но в этой новой, незнакомой среде у него всё-таки получается рубить деревья. А теперь… как же найти алмазы?
III. Скрипт + имитационное обучение
Простой, но действенный подход — комбинировать скриптовые действия с действиями искусственного интеллекта. Учим всему скучному и записываем знания в скрипт.
В этой парадигме нам нужна свёрточная сеть, которая позволит получить большой объём древесины (3 000 шагов). Затем в скрипте прописывается последовательность действий для изготовления досок, палок, верстака, деревянной кирки и приступаем к добыче камня (он должен быть у нас под ногами). Этот камень можно использовать для изготовления каменной кирки, которой можно добывать железную руду.

Вот здесь всё и начинает усложняться: железная руда встречается довольно редко, поэтому для поиска её залежей придётся на какое-то время запустить игру. Далее нужно создать печь и расплавить руду, чтобы получить железную кирку. И, наконец, придётся спуститься ещё глубже и суметь получить алмаз, не упав в лаву.
Как видите, это выполнимо, но результат — непредсказуем. Мы могли бы обучить второго агента искать алмазы, а третьего — создавать железную кирку. Если вам интересны более сложные подходы, то можете почитать про результаты турнира MineRL Diamond 2021. Канервисто с соавторами описал несколько решений на основе разных хитроумных методов, включая архитектуры сквозного глубокого обучения. Надо сказать, что это сложная задача, и ни одной команде не удавалось находить алмазы постоянно… если они вообще хоть что-то находили.
По этой причине в примере ниже ограничимся созданием каменной кирки, но код можно изменить и пойти дальше:
obs = env_script.reset()
done = False
# 1. Get wood with the CNN
for i in tqdm(range(3000)):
obs = torch.from_numpy(obs['pov'].transpose(2, 0, 1)[None].astype(np.float32) / 255).cuda()
probabilities = torch.softmax(model(obs), dim=1)[0].detach().cpu().numpy()
action = np.random.choice(action_list, p=probabilities)
obs, reward, done, _ = env_script.step(action)
# 2. Craft stone pickaxe with scripted actions
for action in tqdm(script):
obs, reward, done, _ = env_cnn.step(str_to_act(env_cnn, action))
env_cnn.release()
env_cnn.play()Видно, что в первые 3 000 шагов агент рубит деревья как сумасшедший, а затем срабатывает скрипт, и выполняется задача. Возможно, это не так очевидно, но команда print(obs.inventory) показывает каменную кирку. Обратите внимание, что это избранный пример; большинство запусков завершаются не так успешно.
Существует несколько причин, почему агент терпит неудачу: он может попасть во враждебную среду (вода, лава и т. д.), в область без леса или даже упасть и погибнуть. Если вы поэкспериментируете с разными затравками, то сможете лучше понять всю сложность проблемы и, надеюсь, получите идеи по созданию ещё более «талантливых» агентов.
Заключение
Надеюсь, вам понравилось это небольшое руководство по обучению с подкреплением в Minecraft. Это не просто популярная игра, но и интересная среда для проверки агентов ИИ, обученных с подкреплением. Здесь, как и в NetHack, нужно очень хорошо разбираться в игровой механике, чтобы спланировать точную последовательность действий в процедурно-сгенерированном мире. В данной статье мы:
узнали, как пользоваться MineRL;
рассмотрели 2 подхода (скрипт, клонирование поведения) и их комбинацию;
показали действия агента в коротких роликах.
Основная проблема среды — низкая скорость её обработки. Minecraft — не такая лёгкая игра, как NetHack или Pong, поэтому агентам нужно много времени на обучение. Если для вас это проблема, рекомендую присмотреться к средам полегче, например Gym Retro.
Спасибо за внимание! Если вам интересно, как использовать ИИ в видеоиграх, то подписывайтесь на меня в Twitter. А мы поможем прокачать ваши навыки или с самого начала освоить профессию, актуальную в любое время:
Выбрать другую востребованную профессию.

