30 KiB
Катание на CartPole
Проблема, которую мы решали на предыдущем уроке, может показаться игрушечной, не совсем применимой к реальным ситуациям. Это не так, потому что многие реальные задачи имеют похожий сценарий – включая игру в шахматы или го. Они похожи тем, что у нас есть доска с установленными правилами и дискретное состояние.
Тест перед лекцией
Введение
В этом уроке мы применим те же принципы Q-обучения к задаче с непрерывным состоянием, то есть состоянием, заданным одним или несколькими действительными числами. Мы рассмотрим следующую задачу:
Задача: Если Пётр хочет убежать от волка, ему нужно уметь двигаться быстрее. Мы увидим, как Пётр может научиться кататься на коньках, в частности, сохранять равновесие, используя Q-обучение.
Пётр и его друзья проявляют изобретательность, чтобы убежать от волка! Изображение от Jen Looper
Мы будем использовать упрощённую версию балансирования, известную как задача CartPole. В мире cartpole у нас есть горизонтальный ползунок, который может двигаться влево или вправо, а цель — удержать вертикальный стержень на ползунке в равновесии.
Предварительные знания
В этом уроке мы будем использовать библиотеку OpenAI Gym для симуляции различных окружений. Код этого урока можно запускать локально (например, из Visual Studio Code), в этом случае симуляция откроется в новом окне. При запуске кода онлайн может потребоваться внести некоторые изменения в код, как описано здесь.
OpenAI Gym
На прошлом уроке правила игры и состояние задавались классом Board, который мы определили сами. Здесь мы будем использовать специальное симуляционное окружение, которое будет моделировать физику балансирующего стержня. Одно из самых популярных симуляционных окружений для обучения алгоритмов обучения с подкреплением называется Gym, поддерживаемое OpenAI. Используя этот gym, мы можем создавать разные окружения — от симуляции cartpole до игр Atari.
Примечание: Другие доступные окружения OpenAI Gym можно посмотреть здесь.
Сначала установим gym и импортируем необходимые библиотеки (блок кода 1):
import sys
!{sys.executable} -m pip install gym
import gym
import matplotlib.pyplot as plt
import numpy as np
import random
Упражнение - инициализировать окружение cartpole
Чтобы работать с задачей балансирования cartpole, нам нужно инициализировать соответствующее окружение. Каждое окружение связано с:
-
Пространством наблюдений (Observation space), которое определяет структуру информации, которую мы получаем из окружения. Для задачи cartpole мы получаем позицию стержня, скорость и некоторые другие значения.
-
Пространством действий (Action space), которое определяет возможные действия. В нашем случае пространство действий дискретное и состоит из двух действий — влево и вправо. (блок кода 2)
-
Для инициализации введите следующий код:
env = gym.make("CartPole-v1") print(env.action_space) print(env.observation_space) print(env.action_space.sample())
Чтобы понять, как работает окружение, давайте запустим короткую симуляцию на 100 шагов. На каждом шаге мы будем задавать одно из возможных действий — в этой симуляции мы просто случайным образом выбираем действие из action_space.
-
Запустите код ниже и посмотрите, к чему это приведёт.
✅ Помните, что предпочтительно выполнять этот код на локальной установке Python! (блок кода 3)
env.reset() for i in range(100): env.render() env.step(env.action_space.sample()) env.close()Вы должны увидеть нечто подобное на это изображение:
-
Во время симуляции нам нужно получать наблюдения, чтобы решить, как действовать. По сути, функция step возвращает текущие наблюдения, функцию вознаграждения и флаг done, который показывает, есть ли смысл продолжать симуляцию: (блок кода 4)
env.reset() done = False while not done: env.render() obs, rew, done, info = env.step(env.action_space.sample()) print(f"{obs} -> {rew}") env.close()В результате в выводе ноутбука вы увидите следующее:
[ 0.03403272 -0.24301182 0.02669811 0.2895829 ] -> 1.0 [ 0.02917248 -0.04828055 0.03248977 0.00543839] -> 1.0 [ 0.02820687 0.14636075 0.03259854 -0.27681916] -> 1.0 [ 0.03113408 0.34100283 0.02706215 -0.55904489] -> 1.0 [ 0.03795414 0.53573468 0.01588125 -0.84308041] -> 1.0 ... [ 0.17299878 0.15868546 -0.20754175 -0.55975453] -> 1.0 [ 0.17617249 0.35602306 -0.21873684 -0.90998894] -> 1.0Вектор наблюдений, который возвращается на каждом шаге симуляции, содержит следующие значения:
- Позиция тележки
- Скорость тележки
- Угол стержня
- Скорость вращения стержня
-
Получите минимальные и максимальные значения этих чисел: (блок кода 5)
print(env.observation_space.low) print(env.observation_space.high)Также вы можете заметить, что значение вознаграждения на каждом шаге симуляции всегда равно 1. Это потому, что наша цель — выживать как можно дольше, то есть удерживать стержень в достаточно вертикальном положении максимально долго.
✅ Фактически, симуляция CartPole считается решённой, если мы достигаем среднего вознаграждения 195 за 100 последовательных испытаний.
Дискретизация состояния
В Q-обучении нам нужно построить Q-таблицу, которая определяет, что делать в каждом состоянии. Чтобы это сделать, наше состояние должно быть дискретным, точнее, должно содержать конечное число дискретных значений. Значит, нам нужно как-то дискретизировать наши наблюдения, отображая их в конечный набор состояний.
Есть несколько способов это сделать:
- Разбиение на интервалы (bins). Если мы знаем интервал некоторого значения, мы можем разделить этот интервал на несколько корзин (bins) и заменить значение номером корзины, к которой оно принадлежит. Это можно сделать с помощью функции numpy
digitize. В этом случае мы точно знаем размерность состояния, так как она будет зависеть от количества выбранных корзин для дискретизации.
✅ Можно использовать линейную интерполяцию, чтобы привести значения к некоторому конечному интервалу (скажем, от -20 до 20), а затем преобразовать числа в целые, округляя их. Это даёт меньше контроля над размером состояния, особенно если вы не знаете точные диапазоны входных значений. Например, в нашем случае 2 из 4 значений не имеют верхних или нижних границ, что может привести к бесконечному количеству состояний.
В нашем примере мы выберем второй подход. Как вы увидите позже, несмотря на неопределённые границы, эти значения редко выходят за пределы некоторых конечных интервалов, значит состояния с экстремальными значениями будут очень редки.
-
Вот функция, которая принимает наблюдение из нашей модели и выдаёт кортеж из 4 целых чисел: (блок кода 6)
def discretize(x): return tuple((x/np.array([0.25, 0.25, 0.01, 0.1])).astype(np.int)) -
Рассмотрим также другой метод дискретизации с помощью корзин: (блок кода 7)
def create_bins(i,num): return np.arange(num+1)*(i[1]-i[0])/num+i[0] print("Sample bins for interval (-5,5) with 10 bins\n",create_bins((-5,5),10)) ints = [(-5,5),(-2,2),(-0.5,0.5),(-2,2)] # интервалы значений для каждого параметра nbins = [20,20,10,10] # количество корзин для каждого параметра bins = [create_bins(ints[i],nbins[i]) for i in range(4)] def discretize_bins(x): return tuple(np.digitize(x[i],bins[i]) for i in range(4)) -
Теперь запустим короткую симуляцию и посмотрим на эти дискретизированные значения окружения. Попробуйте оба варианта —
discretizeиdiscretize_bins— и посмотрите, есть ли разница.✅ функция discretize_bins возвращает номер корзины, нумерация с 0. Значит, для значений входной переменной около 0 она возвращает число из середины интервала (10). В функции discretize мы не учитывали диапазон выходных значений, позволяя им быть отрицательными, поэтому значения состояний не сдвинуты, и 0 соответствует 0. (блок кода 8)
env.reset() done = False while not done: #env.render() obs, rew, done, info = env.step(env.action_space.sample()) #print(discretize_bins(obs)) print(discretize(obs)) env.close()✅ Раскомментируйте строку, начинающуюся с env.render, если хотите видеть выполнение окружения. В противном случае можно выполнять его в фоновом режиме — это быстрее. Мы будем использовать такое «невидимое» выполнение во время процесса Q-обучения.
Структура Q-таблицы
В предыдущем уроке состояние было простой парой чисел от 0 до 8, и поэтому было удобно представлять Q-таблицу как numpy-тензор размером 8x8x2. Если мы используем дискретизацию корзинами, размерность нашего вектора состояния тоже известна, поэтому мы можем использовать тот же подход и представить состояние массивом формы 20x20x10x10x2 (где 2 — размерность пространства действий, а первые размеры соответствуют количеству корзин для каждого из параметров в пространстве наблюдений).
Однако иногда точный размер пространства наблюдений неизвестен. В случае функции discretize мы никогда не можем быть уверены, что наше состояние останется в определённых пределах, так как некоторые из исходных значений не ограничены. Поэтому мы используем немного другой подход и представим Q-таблицу словарём.
-
Используйте пару (state,action) как ключ словаря, а значением будет соответствующая запись Q-таблицы. (блок кода 9)
Q = {} actions = (0,1) def qvalues(state): return [Q.get((state,a),0) for a in actions]Здесь мы также определяем функцию
qvalues(), которая возвращает список значений Q-таблицы для данного состояния, соответствующих всем возможным действиям. Если запись отсутствует в Q-таблице, возвращаем по умолчанию 0.
Начинаем Q-обучение
Теперь мы готовы научить Петра балансировать!
-
Сначала зададим некоторые гиперпараметры: (блок кода 10)
# гиперпараметры alpha = 0.3 gamma = 0.9 epsilon = 0.90Здесь
alpha— это скорость обучения, которая определяет, насколько сильно мы должны корректировать текущие значения Q-таблицы на каждом шаге. В предыдущем уроке мы начинали с 1, а затем уменьшалиalphaво время обучения. В этом примере мы оставим её постоянной для простоты, хотя вы можете потом поэкспериментировать и изменить значениеalpha.gamma— это коэффициент дисконтирования, показывающий, насколько важно учитывать будущее вознаграждение по сравнению с текущим.epsilon— это фактор исследования/использования, который определяет, стоит ли предпочесть исследование или использование. В нашем алгоритме вepsilonпроцентов случаев следующее действие выбирается на основе значений Q-таблицы, а в оставшихся случаях выполняется случайное действие. Это позволяет исследовать области пространства поиска, которые мы ранее не видели.✅ Что касается балансировки — выбор случайного действия (исследование) действует как случайный толчок в неправильном направлении, и стержень должен научиться восстанавливаться после этих «ошибок».
Улучшаем алгоритм
Мы также можем внести два улучшения в наш алгоритм с предыдущего урока:
-
Вычислять среднюю накопленную награду за несколько симуляций. Мы будем выводить прогресс каждые 5000 итераций и усреднять накопленную награду за этот период. Это значит, что если мы получаем более 195 очков — мы можем считать задачу решённой с ещё более высоким качеством, чем требуется.
-
Вычислять максимальный средний накопленный результат,
Qmax, и сохранять Q-таблицу для этой лучшей модели. При запуске обучения вы заметите, что средний накопленный результат иногда начинает падать, и мы хотим сохранить значения Q-таблицы, соответствующие лучшей модели, найденной во время тренировки.
-
Собираем все накопленные награды каждой симуляции в вектор
rewardsдля последующего построения графиков. (блок кода 11)def probs(v,eps=1e-4): v = v-v.min()+eps v = v/v.sum() return v Qmax = 0 cum_rewards = [] rewards = [] for epoch in range(100000): obs = env.reset() done = False cum_reward=0 # == выполнить симуляцию == while not done: s = discretize(obs) if random.random()<epsilon: # эксплуатация - выбрать действие согласно вероятностям в Q-таблице v = probs(np.array(qvalues(s))) a = random.choices(actions,weights=v)[0] else: # исследование - случайно выбрать действие a = np.random.randint(env.action_space.n) obs, rew, done, info = env.step(a) cum_reward+=rew ns = discretize(obs) Q[(s,a)] = (1 - alpha) * Q.get((s,a),0) + alpha * (rew + gamma * max(qvalues(ns))) cum_rewards.append(cum_reward) rewards.append(cum_reward) # == Периодически выводить результаты и вычислять среднее вознаграждение == if epoch%5000==0: print(f"{epoch}: {np.average(cum_rewards)}, alpha={alpha}, epsilon={epsilon}") if np.average(cum_rewards) > Qmax: Qmax = np.average(cum_rewards) Qbest = Q cum_rewards=[]
Что можно заметить по этим результатам:
-
Близко к цели. Мы очень близки к достижению цели — получить 195 накопленных очков за 100 и более последовательных запусков симуляции, и, возможно, уже достигли её! Даже если получаются меньшие числа, мы всё равно не можем точно сказать, так как усредняем по 5000 прогонов, а формальный критерий требует всего 100.
-
Награда начинает падать. Иногда награда начинает снижаться, что означает, что мы можем «разрушить» уже выученные значения в Q-таблице новыми, ухудшающими ситуацию.
Это наблюдение лучше видно на графике прогресса обучения.
График прогресса обучения
Во время обучения мы собирали значение накопленной награды на каждой итерации в вектор rewards. Вот как это выглядит при построении графика зависимости от номера итерации:
plt.plot(rewards)
По этой диаграмме ничего нельзя сказать, потому что из-за природы стохастического процесса обучения длина сессий сильно варьируется. Чтобы этот график имел смысл, можно вычислить скользящее среднее по серии экспериментальных данных, скажем, по 100. Это удобно сделать с помощью np.convolve: (блок кода 12)
def running_average(x,window):
return np.convolve(x,np.ones(window)/window,mode='valid')
plt.plot(running_average(rewards,100))
Изменение гиперпараметров
Чтобы обучение было более стабильным, имеет смысл изменять некоторые гиперпараметры во время тренировки. В частности:
-
По коэффициенту обучения
alphaможно начать с значения близкого к 1, а затем постепенно снижать этот параметр. Со временем мы будем получать надежные значения вероятностей в Q-таблице, и нужно лишь небольшое корректирование, а не полное переписывание значений. -
Увеличивать epsilon. Можно увеличивать
epsilonпостепенно, чтобы уменьшать исследование и увеличивать использование. Вероятно, имеет смысл начать с низкогоepsilonи подниматься до почти 1.
Задание 1: Поиграйте с гиперпараметрами и попробуйте добиться большей накопленной награды. Получается ли у вас больше 195?
Задача 2: Для формального решения задачи необходимо получить среднюю награду 195 за 100 последовательных запусков. Измеряйте это во время обучения и убедитесь, что вы формально решили задачу!
Наблюдение результата в действии
Было бы интересно увидеть, как ведет себя обученная модель. Запустим симуляцию и выберем действия по той же стратегии выбора, что и во время обучения, с выборкой согласно вероятностному распределению в Q-таблице: (кодовый блок 13)
obs = env.reset()
done = False
while not done:
s = discretize(obs)
env.render()
v = probs(np.array(qvalues(s)))
a = random.choices(actions,weights=v)[0]
obs,_,done,_ = env.step(a)
env.close()
Вы должны увидеть что-то вроде этого:
🚀Вызов
Задача 3: Здесь мы использовали финальную копию Q-таблицы, которая может быть не лучшей. Помните, что мы сохранили лучшую Q-таблицу в переменную
Qbest! Попробуйте тот же пример, заменивQнаQbestи посмотрите, заметите ли вы разницу.
Задача 4: Здесь мы не выбирали лучшее действие на каждом шаге, а выбирали с вероятностью, соответствующей распределению. Будет ли более разумно всегда выбирать лучшее действие с наивысшим значением в Q-таблице? Это можно сделать с помощью функции
np.argmax, чтобы определить номер действия с максимальным значением в Q-таблице. Реализуйте эту стратегию и посмотрите, улучшится ли балансирование.
Викторина после лекции
Задание
Заключение
Теперь мы научились тренировать агентов добиваться хороших результатов, просто предоставляя функцию награды, которая определяет желаемое состояние игры, и давая им возможность интеллектуально исследовать пространство поиска. Мы успешно применили алгоритм Q-обучения как в дискретных, так и в непрерывных средах, но с дискретными действиями.
Важно также изучать ситуации, когда пространство действий тоже непрерывно, а пространство наблюдений намного сложнее, например изображение с экрана игры Atari. В таких задачах часто необходимо использовать более мощные методы машинного обучения, такие как нейронные сети, чтобы добиться хороших результатов. Эти более продвинутые темы будут предметом нашего следующего более продвинутого курса по ИИ.
Отказ от ответственности: Этот документ был переведен с использованием сервиса машинного перевода Co-op Translator. Несмотря на наши усилия по обеспечению точности, имейте в виду, что автоматический перевод может содержать ошибки или неточности. Оригинальный документ на его исходном языке следует считать авторитетным источником. Для получения критически важной информации рекомендуется обратиться к профессиональному человеческому переводу. Мы не несем ответственности за любые недоразумения или неправильные толкования, возникшие в результате использования этого перевода.




