- BrainTools - https://www.braintools.ru -

В данной статье будет рассмотрена работа с библиотекой gymnasium [1] для изучения машинного обучения [2] с подкреплением [3]. Реализация агента, который использует метод машинного обучения q-learning для максимизации выигрыша в карточной игре blackjack и сравним средний выигрыш за 100 000 игр при различных реализациях игры blackjack.
Подробно с правилами игры в блэкджек можно ознакомиться на википедии [4]. Рассмотрим окружение, которое реализовано в gymnasium [5]:
численные значения карт равны от 2 до 10 – для карт от двойки до десятки соответственно, и 10 – для валетов, дам и королей;
туз считается за 11 очков, если общая сумма карт на руке игрока не превосходит 21 (по-английски, в этом случае, говорят, что на руке есть usable ace), и за 1 очко, если превосходит 21;
игроку раздаются две карты, дилеру — одна в открытую и одна в закрытую;
игрок может совершать одно из двух действий:
hit — взять ещё одну карту;
stand — не брать больше карт;
если сумма очков у игрока на руках больше 21, он проигрывает (bust);
если игрок выбирает stand с суммой не больше 21, дилер добирает карты, пока сумма карт в его руке меньше 17;
после этого игрок выигрывает, если дилер либо превышает 21, либо получает сумму очков меньше, чем сумма очков у игрока; при равенстве очков объявляется ничья (ставка возвращается);
в исходных правилах есть ещё дополнительный бонус за natural blackjack: если игрок набирает 21 очко с раздачи двумя картами, он выигрывает не +1, а +1.5 (полторы ставки).

Если описывать правила в терминах машинного обучения с подкреплением, то доступно два действия: hit и stand.
Пространство наблюдений – это 3 дискретных состояния:
сумма карт игрока (2 – 32);
открытая карта дилера (1 – 10);
флаг, который показывает туз игрока, считается за 1 или за 11.
Награда: 1 – при победе, 1.5 – если сразу раздали блэджек, -1 – при проигрыше и 0 – при исходе “ничья”.
В качестве baseline реализуем простую стратегию – делать stand, если у нас на руках комбинация в 19, 20 или 21 очко. Во всех остальных случаях – говорить hit.
def play_basic_strategy(env):
observation, info = env.reset()
player_score, deler_score, usable_ace = observation
while player_score < 19:
observation, reward, terminated, truncated, info = env.step(STAND)
player_score, deler_score, usable_ace = observation
observation, reward, terminated, truncated, info = env.step(HIT)
return reward
В результате, получили мат. ожидание выигрыша около -0.06%.
Для игр с небольшим количеством состояний отлично подходит метод машинного обучения с подкреплением q-learning. Идея q-learning следующая – на основе награды, получаемой от среды, агент формирует q_policy, где для каждого состояния и для каждого возможного действия из этого состояния записывается ожидаемый выигрыш, полученный на основе опыта [6] в процессе обучения. И уже после обучения выбирается действие, которое даёт максимально ожидаемый выигрыш.
Формула обновления Q-значений в алгоритме Q-learning:
Где:
Q(s, a) – текущее значение функции полезности (Q-value) для состояния s и действия a
α – скорость обучения (learning rate), определяет степень влияния новой информации на существующую оценку
r – награда, полученная после выполнения действия a в состоянии s
γ – коэффициент дисконтирования (discount factor), показывает важность будущих наград
s′ – новое состояние, в которое переходит агент после выполнения действия
maxₐ′ Q(s′, a′) – максимальная оценка полезности в следующем состоянии s′ по всем возможным действиям a′
Реализация агента, который будет максимизировать выигрыш при игре в блэкджек, используя q-learning.
class BlackJackAgent:
def __init__(self, env, count_action: int, get_action_func):
self.states = self.get_all_states_from_env(env)
self.q_policy = dict(zip(self.states, np.zeros((len(self.states), count_action))))
self.env = env
self.get_action_func = get_action_func
def get_all_states_from_env(self, env):
states = [get_observation_range_from_discrete(obseration) for obseration in env.observation_space]
return list(itertools.product(*states))
def q_learning(self, num_episodes: int,epsilon: float, alpha: float, gamma: float):
for _ in range(num_episodes):
state, info = self.env.reset()
terminated = False
while not terminated:
action = self.get_action_func(self.q_policy, state, epsilon, self.env)
observation, reward, terminated, truncated, info = self.env.step(action)
# Q[s,a] = Q[s,a] + alpha (r + gamma maxₐ′ Q[s′,a′] − Q[s,a]).
self.q_policy[state][action] = self.q_policy[state][action] + alpha * (reward + gamma * max(self.q_policy[observation]) - self.q_policy[state][action])
state = observation
Сравним средний выигрыш за 100 000 игр, получаемый агентом, в различных вариациях игры блэкджек:
Базовое окружение из пакета gymnasium;
Базовое окружение из пакета gymnasium с добавленным действием dobule. В окружение добавляется ещё одно действие double — удвоить ставку. После выбора действия doubleделать другие действия нельзя. Игроку выдаётся ровно одна дополнительная карта, а выигрыш или проигрыш удваивается;
Базовое окружение из пакета gymnasium + double + подсчёт карт по системе половинки. Для реализации этого окружения необходимо переопределить колоду, так как в gymnasium реализована бесконечная колода. В нашем случае, будет шуз на 6 колод, и, когда будет оставаться меньше 30 карт, шуз будет заново замешиваться. Система подсчёта Половинки описана на википедии [4]. Если коротко, каждой карте присвоен вес и, при появлении карты на столе, счёт изменяется согласно весу. Детали реализации – игрок может считать карты в диапазоне от -35 до 35 с шагом 0.5, и, если нам так “повезло”, что было очень много картинок подряд, и мы получили счёт -36, то выбирается ближайшее состояние, соответствующее ближайшему счёту (в нашем примере выбирается состояние, соответствующие счёту -35);
Базовое окружение из пакета gymnasium + double + подсчёт карт по системе плюс-минус. Аналогично окружению, реализующему подсчёт карт по системе половинки, только веса у карт [-1, 0, 1] и шаг при подсчёте равен 1;
Базовое окружение из пакета gymnasium + double + подсчёт карт по системе плюс-минус + split. В случае, когда игроку пришли две одинаковые карты(либо две картинки), он может разбить руку на две, внести ещё одну ставку и продолжать играть в две руки сразу (как-будто за двоих игроков);
Базовое окружение из пакета gymnasium + double + подсчёт карт по системе половинки + split.
Рассмотрим реализацию окружения, которое даёт возможность удваивать ставку и делать подсчёт карт. Реализацию остальных окружений можно посмотреть подробнее в репозитории [7].
class BlackjackDoubleCardsCountEnv(BlackjackEnv): # наследуемся от базового окружения из gymnasium. https://github.com/openai/gym/blob/master/gym/envs/toy_text/blackjack.py
def __init__(self, render_mode: Optional[str] = None, natural=False, sab=False, ...):
super().__init__(render_mode, natural, sab)
...
self.action_space = spaces.Discrete(3) # переопределим пространство действий
...
def step(self, action):
...
elif action == 3:
# дилер набирает карты
# даём игроку ещё одну карту
...
# домножаем награду на 2 и завершаем игру
done = True
reward = cmp(score(self.player), score(self.dealer)) * 2
return self._get_obs(), reward, done, False, {}
Для реализации подсчёта карт в конструкторе переопределим колоду, добавим аргумент для изменения значений весов mathing и аргумент для выбора диапазона, в котором считаются карты cards_count_range. Также в пространство наблюдений нужно добавить состояние, показывающее текущий счёт. А в вызове функции step, при каждом действии, считаем выданную карту. Также переопределим функцию _get_obs и функцию reset, в которой будем замешивать колоду только в том случае, если карт в колоде осталось меньше 30.
class BlackjackDoubleCardsCountEnv(BlackjackEnv):
def __init__(self, render_mode: Optional[str] = None, natural=False, sab=False, mathing=None, cards_count_range=np.arange(-35, 35, 0.5)):
self.standart_deck = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10] * 4
# шуз на 6 колод
self.deck = self.standart_deck * 6
# 'счет' карт
self.cards_count = 0
# числовые значения для карт
if mathing == None:
self.mathing = { # веса по схеме "Половинки"
1: -1,
2: 0.5,
3: 1,
4: 1,
5: 1.5,
6: 1,
7: 0.5,
8: 0,
9: -0.5,
10: -1,
}
else:
self.mathing = mathing
avilable_count_card_values = cards_count_range
self.map_avilable_count_card_to_state = dict(zip(avilable_count_card_values, range(len(avilable_count_card_values))))
self.observation_space = spaces.Tuple(
(spaces.Discrete(32), spaces.Discrete(11), spaces.Discrete(2), spaces.Discrete(len(avilable_count_card_values)))
)
def step(self, action):
...
if action == any:
self.count(card) # при каждом действии, считаем выданную карту
# не забываем, что у дилера нужно считать только закрытые карты, в конце игры
...
def _get_obs(self):
# считаем, что возможности агента по счёту не бесконечны
# и после того как счёт выходит за доступный диапазон
# выбираем ближайшее крайнее состояние
if self.cards_count in self.map_avilable_count_card_to_state:
cards_count_state = self.map_avilable_count_card_to_state[self.cards_count]
elif self.cards_count < 0:
cards_count_state = 0
else:
cards_count_state = len(self.map_avilable_count_card_to_state) - 1
return (... , cards_count_state)
def reset(self):
if len(self.deck) < 30: # заново замешиваем колоды
self.deck = self.standart_deck * 6
self.cards_count = 0
cards = self.draw_hand(self.np_random)
self.dealer = cards
self.count(self.dealer[0])
cards = self.draw_hand(self.np_random)
self.player = cards
for card in cards:
self.count(card)
return self._get_obs(), {}
Агент для каждого окружения обучается на 3 500 000 играх.
После 100 000 игр будем уменьшать exploration уменьшая вероятность выбора случайного действия до 0.08.
def fit_agent(agent, env, alpha = 0.0009, epsilon = 0.77, gamma = 0.91):
rewards = []
for i in tqdm(range(0, COUNT_GAMES, STEP)):
agent.q_learning(STEP, epsilon, alpha, gamma)
mean_reward = np.mean([play(env, agent) for _ in range(0, 100_000)])
rewards.append(mean_reward)
if i % 100_000 == 0:
epsilon = min(0.92, epsilon + 0.05)
return rewards
env = gym.make('Blackjack-v1', natural=True)
agent = BlackJackAgent(env, env.action_space.n, get_action)
rewards_default_env = fit_agent(agent, env)
В результате, получаются следующие зависимости значений наград от количества сыгранных игр.

Как видно из графиков, обыграть казино получается только в том случае, когда мы считаем карты и умеем делать split и double, причём метод подсчёта карт “плюс-минус” показывает лучший результат, чем метод “половинки”. Так же при базовом окружении и окружении с возможностью делать double оценочные Q-значения не сходятся к устойчивой стратегии.
Автор: monkey_llm
Источник [9]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/16152
URLs in this post:
[1] gymnasium: https://gymnasium.farama.org/#
[2] обучения: http://www.braintools.ru/article/5125
[3] подкреплением: http://www.braintools.ru/article/5528
[4] википедии: https://ru.wikipedia.org/wiki/%D0%91%D0%BB%D1%8D%D0%BA%D0%B4%D0%B6%D0%B5%D0%BA
[5] gymnasium: https://gymnasium.farama.org/environments/toy_text/blackjack/
[6] опыта: http://www.braintools.ru/article/6952
[7] репозитории: https://github.com/lIkesimba9/blackjack_q_learning
[8] Ещё больше интересного в телеграм канале: https://t.me/+9fl51jd750A3MTIy
[9] Источник: https://habr.com/ru/articles/917924/?utm_campaign=917924&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.