Глубокое обучение с подкреплением для сборки кубика Рубика
Введение
Кубик Рубика — известная трехмерная головоломка. У обычного кубика Рубика шесть граней, на каждой из которых по девять цветных стикеров, и головоломка решена, когда каждая грань имеет единый цвет. Если считать один поворот на четверть (90°) за один ход и два поворота на четверть (поворот лицом) за два хода, то лучшие алгоритмы, изобретенные человеком, могут решить любой экземпляр куба за 26 ходов.
Моя цель — дать компьютеру возможность научиться собирать кубик Рубика, не снабжая его никакими человеческими знаниями, такими как симметрия кубика. Самая сложная часть — Кубик Рубика имеет 43 252 003 274 489 856 000 возможных перестановок. Прежде чем мы обсудим, как подойти к этой проблеме, сначала давайте сформулируем проблему в стиле обучения с подкреплением.
Постановка проблемы
Государственное пространство
Кубик Рубика состоит из 27 кубиков, но центральный никогда не двигается. Мы можем рассматривать перестановку как тензор 3 × 3 × 3, представляющий положение всех этих 27 кубиков. Например, конечное состояние, т. е. решенное состояние, может быть представлено следующим тензором:
tensor([[[ 0., 1., 2.],
[ 3., 4., 5.],
[ 6., 7., 8.]],
[[ 9., 10., 11.],
[12., 13., 14.],
[15., 16., 17.]],
[[18., 19., 20.],
[21., 22., 23.],
[24., 25., 26.]]])
Пространство действий
В этом отчете я использую «обозначение Singmaster» для обозначения возможных действий. Во-первых, давайте определим шесть граней кубика Рубика с помощью шести букв: F (спереди), B (сзади), U (вверху), D (внизу), L (слева), R (справа), как показано на следующем рисунке. .
Теперь мы можем использовать эти буквы для обозначения поворота этих граней по часовой стрелке, а когда за буквой следует символ штриха (‘), он обозначает поворот этих граней против часовой стрелки. Например, «F» означает поворот лицевой стороны на 90° по часовой стрелке, а «F’» означает поворот лицевой стороны на -90°.
Очевидно, что для каждого состояния есть 12 возможных действий: {F, F’, B, B’, U, U’, D, D’, L, L’, R, R’}.
Динамика перехода
Из каждого состояния каждое действие приводит к новому детерминированному состоянию. Например, из следующего состояния:
tensor([[[ 6., 3., 0.],
[ 7., 4., 1.],
[ 8., 5., 2.]],
[[ 9., 10., 11.],
[12., 13., 14.],
[15., 16., 17.]],
[[18., 19., 20.],
[21., 22., 23.],
[24., 25., 26.]]])
Если мы предпримем действие «F», следующим состоянием должно быть конечное состояние.
Цель и награда
Задача агента — собрать кубик Рубика за как можно меньше шагов. Я установил простой механизм вознаграждения: -1 за каждое действие. Поэтому мы определяем цель агента как максимизацию накопленного вознаграждения.
Классификация проблем
Это проблема конечных марковских процессов принятия решений (MDP) с детерминированной динамикой перехода, о которой мы полностью знаем.
Метод решения
Во-первых, поскольку эта проблема MDP имеет такое огромное пространство состояний, мы не можем использовать метод табличного решения, а подход глубокого обучения.
Как создавать эпизоды
Проблема заключается в том, что если агент начинает с произвольной политики и генерирует с ее помощью эпизоды, завершение одного эпизода может занять целую вечность. Однако мы не можем вводить какие-либо человеческие знания, поэтому мы не можем позволить им начать с разработанного человеком алгоритма. Мы по-прежнему хотим, чтобы агент учился с нуля. Поэтому ключ в том, как генерировать эпизоды, которые гарантированно заканчиваются в разумные сроки.
В статье Решение кубика Рубика без участия человека была предложена идея решения этой задачи: начиная с решенного состояния, мы перебираем кубик несколько раз, записываем все промежуточные состояния и скармливаем их обратно агенту как эпизод. . На следующем изображении показан процесс создания последовательности состояний и обратной передачи ее агенту.
В отличие от статьи, я решил позволить агенту узнать оптимальные значения состояния вместо значений состояния-действия. После того, как агент узнает оптимальные значения состояния, я бы предложил агенту использовать простую жадную политику. Причина этого дизайна в том, что я хочу, чтобы входные функции для нейронной сети были как можно более простыми.
Как оценить стоимость состояния
Интуитивно, если для сборки куба из состояния A требуется 1 ход, то оно имеет более высокое значение, чем состояние B, в котором для сборки куба требуется 2 хода. Следовательно, если состояние появляется в эпизоде несколько раз, расстояние между его последним появлением и конечным состоянием является более разумной оценкой значения его состояния.
Например, на следующем рисунке state_2 и state_0 — это одни и те же состояния, которые находятся не более чем в 3 шагах от конечного состояния вместо 5 шагов. Все состояния между state_2 и state_0 являются простыми «обходными путями».
Поэтому я использую метод последнего визита Монте-Карло (MC) для оценки оптимального значения состояния.
Реализация и результат
В этом разделе я расскажу о своей реализации с использованием PyTorch в следующем порядке:
- Подготовка данных
- Построение нейронной сети
- Оптимизация модели
- Политика, основанная на обученной модели
- Результат эксперимента
Подготовка данных
Государственное представительство
Я использую тензор PyTorch для представления каждой перестановки, то есть состояния куба. Есть два преимущества:
- Тензор PyTorch может использовать мощность графического процессора для достижения более быстрых вычислений, чем на процессоре.
- С помощью
torch.rot90
можно легко имитировать физический поворот на 90° на каждой грани куба. Я научился этому трюку из реализации NumPy.
Генератор эпизодов
Учитывая длину эпизода l, определим e_l как эпизод, в котором мы случайным образом перемешиваем кубик l раз. Думайте о последовательности состояний как о массиве, индекс каждого состояния является естественным представлением его значения. Нам просто нужно взять отрицательное значение индекса.
Чтобы сделать его более гибким, я добавил коэффициент, чтобы можно было экспериментировать с различными схемами вознаграждения.
estimated_state_value.append(float(-index*c))
Хотя концептуально мы передаем эпизоды обратно агенту, поскольку мы принимаем метод MC последнего посещения, мы собираем значение первого появления состояний в нашем сгенерированном массиве.
Другие приемы работы с GPU
- Вместо того, чтобы подавать в модель ванильный массив, я использую
torch.stack
для объединения выборок в тензор более высокой размерности, чтобы использовать параллельные вычисления GPU. - Поместите данные на GPU с помощью
X.to('cuda')
. - Используйте
DataLoader
и позвольте PyTorch обработать пакетную обработку.
Построение нейронной сети
Чтобы добавить нелинейности модели, я использую простую функцию ReLu
.
NeuralNetwork(
(flatten): Flatten(start_dim=1, end_dim=-1)
(linear_relu_stack): Sequential(
(0): Linear(in_features=27, out_features=512, bias=True)
(1): ReLU()
(2): Linear(in_features=512, out_features=512, bias=True)
(3): ReLU()
(4): Linear(in_features=512, out_features=1, bias=True)
)
)
Оптимизация модели
Функция потерь
Это проблема регрессии, потому что агент изучает значение состояния. Поэтому я использую среднеквадратичную ошибку (квадрат нормы L2) в качестве функции потерь.
loss_fn = nn.MSELoss()
Алгоритм оптимизации
Что касается оптимизаторов, я попробовал стохастический градиентный спуск (SGD) и Адама со скоростью обучения 1e-3
и 1e-5
.
Политика, основанная на обученной модели
С обученной моделью мы можем использовать простую жадную политику, которая всегда выбирает действие, ведущее к состоянию с наибольшим значением.
Результат
Для эксперимента я создал 17 642 842 эпизода в диапазоне от e_1 до e_19.
Эксперимент 1:
Интуитивно понятно, что кубик, перетасованный один раз, легче собрать, чем кубик, перетасованный дважды. Поэтому моя первая мысль — сначала накормить агента набором, а затем масштабировать сложность, накормив его набором, а затем и так далее. Как оказалось, функция потерь сходится медленнее, чем случайная выборка, что приводит к следующему эксперименту.
Эксперимент 2:
В этом примере я использую SGD и Adam, а PyTorch DataLoader
позаботится о пакетной обработке. Однако после 15 эпох обучения функция потерь колеблется между 3 ~ 7. Я пробовал меньшую скорость обучения в более поздние эпохи, но это не помогло.
Учитывая, что функция потерь является среднеквадратичной ошибкой, реальная разница между прогнозом и «истиной» составляет около 1,7–2,5. Это может показаться немного, однако, если куб совершит одну ошибку, он уйдет еще на один шаг от конечного состояния, и ситуация станет намного сложнее.
Отражение
Я думаю, что основная причина, по которой агенту не удается узнать оптимальное значение состояния, заключается в том, что моя модель недостаточно выразительна. Я пробовал с одной парой «Linear-ReLU», двумя парами и тремя парами, и результат почти одинаков. Я предполагаю, что мне нужно использовать слой свертки 3D. Тем не менее, у меня нет достаточно времени, чтобы заставить его работать.
Кроме того, поскольку модель не научилась решать даже головоломку, я не могу начать обобщенную итерацию политики (GPI) с политикой, которая могла бы завершить один эпизод. Поэтому агент может учиться только на случайно сгенерированных эпизодах.
Заключение
Несмотря на то, что кубик Рубика является эпизодической задачей MDP, и у нас есть полное представление о его детерминированной динамике перехода, он по-прежнему очень сложен из-за огромного пространства состояний. Во время проекта я прохожу весь процесс от формулирования проблемы до реализации и экспериментирования. Попутно я изучаю основы PyTorch, с которыми обязательно поиграюсь в будущем. Если бы у меня было больше времени, я бы попробовал слои свертки 3D и использовал графический процессор. В целом, я получил большой опыт и многому научился, реализовав проект на промышленном уровне.