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

Обыгрываем казино, с блэкджеком и стратегиями

Обыгрываем казино, с блэкджеком и стратегиями - 1

В данной статье будет рассмотрена работа с библиотекой 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 (полторы ставки).

Формализация среды блэкджека

Обыгрываем казино, с блэкджеком и стратегиями - 2

Если описывать правила в терминах машинного обучения с подкреплением, то доступно два действия: 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) leftarrow Q(s, a) + alpha cdot left[ r + gamma cdot max_{a'} Q(s', a') - Q(s, a) right]

Где:

  • 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)

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

Обыгрываем казино, с блэкджеком и стратегиями - 4

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

Ещё больше интересного в телеграм канале [8].

Автор: 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

www.BrainTools.ru

Rambler's Top100