Help us understand the problem. What is going on with this article?

ブラックジャックの戦略を強化学習で作ってみる(②gymに環境を登録)

はじめに

Pythonと強化学習の勉強を兼ねて,ブラックジャックの戦略作りをやってみました.
ベーシックストラテジーという確率に基づいた戦略がありますが,それに追いつけるか試してみます.

こんな感じで進めていきます
1. ブラックジャック実装 
2. OpenAI gymの環境に登録 ← 今回はここ
3. 強化学習でブラックジャックの戦略を学習

OpenAIのgymとは

強化学習の研究環境として使われるプラットフォームです.
CartPoleや迷路などの環境(ゲーム)が用意されており,簡単に強化学習を試すことができます.
OpenAI Gymの環境は,エージェントからの行動を受け取り,その結果としてその次の状態と報酬を返す共通のインターフェースを持っています.
インストールは以下のように簡単にできますが,詳しい方法は他のページを参考にしてください.以下,インストールが終わってるものとして説明します.

pip install gym

今回はこのOpenAI Gymの環境に自分で作ったブラックジャックを登録して,強化学習できるようにします.

強化学習のおさらい

まず簡単に強化学習のおさらいから.
「環境」から「状態」を観測し,「エージェント」がそれに対して「行動」を起こします.「環境」は「エージェント」に更新された「状態」と「報酬」をフィードバックします.
強化学習の目的は,将来にわたって得られる「報酬」の総和を最大化する「行動」の仕方(=方策)を獲得することです.

ブラックジャックに強化学習の要素を当てはめる

今回のブラックジャックでは,次のように強化学習を考えます.

  • 環境:ブラックジャック
  • エージェント:Player
  • 状態:Playerのカード,Dealerのカードなど
  • 行動:Playerの選択.HitやStandなど
  • 報酬:勝負で得られるチップ

image.png

OpenAI Gymに環境を登録する手順

次の手順で自作の環境をOpenAI Gymに登録します.

  1. OpenAI Gymのgym.Envを継承したブラックジャック環境のクラス「BlackJackEnv」を作成する
  2. gym.envs.registration.register 関数を使って環境を登録し,BlackJack-v0というIDで呼び出せるようにする

開発環境

  • Windows 10
  • Python 3.6.9
  • Anaconda 4.3.0 (64-bit)
  • gym 0.15.4

ファイル構成

ファイル構成は以下のようにします.
__init__.pyという名前のファイルが2つあるので注意してください.

└─ myenv
    ├─ __init__.py  ---> BlacJackEnvを呼び出す
    └─env
       ├─ __init__.py  ---> BlackJackEnvのある場所を示す
       ├─ blackjack.py  ---> BlacJackのゲーム自体
       └─ blackjack_env.py  ---> OpenAI Gymのgym.Envを継承したBlackJackEnvクラスを作る

それでは手順に沿って,環境を登録します.

OpenAI Gymのgym.Envを継承したブラックジャック環境のクラス「BlackJackEnv」を作成する

myenv/env/blackjack.py

前回作成したブラックジャックのコードをそのまま置きます.
下のblackjack_env.pyでインポートして使います.

myenv/env/blackjack_env.py

OpenAI Gymに登録したいBlackJackのゲーム環境「BlackJackEnv」クラスを作ります.
gym.Envを継承して,以下の3つのプロパティと5つのメソッドを実装します.

プロパティ

  • action_space:Player(エージェント)がどんな行動を選択できるかを表す.
  • observation_space:Player(エージェント)が観測できるゲーム環境の情報
  • reward_range:報酬の最小値から最大値の範囲

メソッド

  • reset:環境をリセットするためのメソッド.
  • step: 環境においてアクションを実行し,その結果を返すメソッド.
  • render:環境を可視化するメソッド.
  • close:環境を閉じるためのメソッド.学習終了時に使います.
  • seed:ランダムシードを固定するメソッド.
action_spaceプロパティ

Stand, Hit, Double Down, Surrenderの4つの行動がとれることを表しています.

action_space
self.action_space = gym.spaces.Discrete(4)
observation_spaceプロパティ

Playerの手札の合計点,Dealerの開示されている手札の点,ソフトハンド(Playerの手札にAが含まれる)を示すフラグ,PlayerがHit済みか示すフラグの4つの状態を観測します.
それぞれの最大値,最小値を決めます.

observation_space
high = np.array([
            30,  # player max
            30,  # dealer max
            1,   # is_soft_hand
            1,   # hit flag true
        ])
        low = np.array([
            2,  # player min
            1,  # dealer min
            0,  # is_soft_hand false
            0,  # hit flag false
        ])
        self.observation_space = gym.spaces.Box(low=low, high=high)
reward_rangeプロパティ

報酬の範囲を決めます.ここでは獲得できるチップの最小値と最大値が含まれるように決めています.

reward_range
        self.reward_range = [-10000, 10000]
resetメソッド

self.doneの初期化,self.game.reset_game()でPlayer, Dealerの手札の初期化,チップを賭ける(Bet),カードの配布(Deal)を行います.
self.doneはstepメソッドで触れる通り,勝敗がついているかを示すブール値です.
self.observe()で4つの状態を観測して返します.
ただし今回は,Playerの所持チップは減らないものとして学習させることとしました.

reset()
    def reset(self):
        # 状態を初期化し,初期の観測値を返す
        # 諸々の変数を初期化する
        self.done = False

        self.game.reset_game()
        self.game.bet(bet=100)
        self.game.player.chip.balance = 1000  # 学習中は所持金がゼロになることはないとする
        self.game.deal()
        # self.bet_done = True

        return self.observe()
stepメソッド

Playerは環境に対してStand, Hit, Double down, Surrenderいずれかの行動をとります.プレーヤーのターンが終了していればチップの精算を行います.最後に以下の4つの情報を返します.

  • obserbation:観測した環境の状態.
  • reward:アクションによって獲得した報酬の量.
  • done:もう一度環境をリセットすべきかどうかを示すブール値.BlackJackでは勝敗がついたかどうかを示すブール値.
  • info:デバッグに役立つ情報を設定できるdictionary.

またこの学習環境においては,Hitした後にDouble downやSurrenderをした場合にはルール違反ということでペナルティを与えることとしました.

step()
    def step(self, action):
        # action を実行し,結果を返す
        # 1ステップ進める処理を記述.戻り値はobservation, reward, done(ゲーム終了したか), info(追加の情報の辞書)

        if action == 0:
            action_str = 's'  # Stand
        elif action == 1:
            action_str = 'h'  # Hit
        elif action == 2:
            action_str = 'd'  # Double down
        elif action == 3:
            action_str = 'r'  # Surrender
        else:
            print(action)
            print("未定義のActionです")
            print(self.observe())

        hit_flag_before_step = self.game.player.hit_flag
        self.game.player_step(action=action_str)

        if self.game.player.done:
            # プレーヤーのターンが終了したとき
            self.game.dealer_turn()
            self.game.judge()
            reward = self.get_reward()
            self.game.check_deck()
            print(str(self.game.judgment) + " : " + str(reward))


        elif action >= 2 and hit_flag_before_step is True:
            reward = -1e3  # ルールに反する場合はペナルティを与える

        else:
            # プレーヤーのターンを継続するとき
            reward = 0

        observation = self.observe()
        self.done = self.is_done()
        return observation, reward, self.done, {}

なお今回,render, close, seedメソッドは使いません.

blackjack_env.pyのコード全体は次のようになります.

myenv/env/blackjack_env.py
import gym
import gym.spaces
import numpy as np

from myenv.env.blackjack import Game


class BlackJackEnv(gym.Env):
    metadata = {'render.mode': ['human', 'ansi']}

    def __init__(self):
        super().__init__()

        self.game = Game()
        self.game.start()

        # action_space, observation_space, reward_range を設定する
        self.action_space = gym.spaces.Discrete(4)  # hit, stand, double down, surrender

        high = np.array([
            30,  # player max
            30,  # dealer max
            1,   # is_soft_hand
            1,   # hit flag true
        ])
        low = np.array([
            2,  # player min
            1,  # dealer min
            0,  # is_soft_hand false
            0,  # hit flag false
        ])
        self.observation_space = gym.spaces.Box(low=low, high=high)
        self.reward_range = [-10000, 10000]  # 報酬の最小値と最大値のリスト

        self.done = False
        self.reset()

    def reset(self):
        # 状態を初期化し,初期の観測値を返す
        # 諸々の変数を初期化する
        self.done = False

        self.game.reset_game()
        self.game.bet(bet=100)
        self.game.player.chip.balance = 1000  # 学習中は所持金がゼロになることはないとする
        self.game.deal()
        # self.bet_done = True

        return self.observe()

    def step(self, action):
        # action を実行し,結果を返す
        # 1ステップ進める処理を記述.戻り値はobservation, reward, done(ゲーム終了したか), info(追加の情報の辞書)

        if action == 0:
            action_str = 's'  # Stand
        elif action == 1:
            action_str = 'h'  # Hit
        elif action == 2:
            action_str = 'd'  # Double down
        elif action == 3:
            action_str = 'r'  # Surrender
        else:
            print(action)
            print("未定義のActionです")
            print(self.observe())

        hit_flag_before_step = self.game.player.hit_flag
        self.game.player_step(action=action_str)

        if self.game.player.done:
            # プレーヤーのターンが終了したとき
            self.game.dealer_turn()
            self.game.judge()
            reward = self.get_reward()
            self.game.check_deck()
            print(str(self.game.judgment) + " : " + str(reward))


        elif action >= 2 and hit_flag_before_step is True:
            reward = -1e3  # ルールに反する場合はペナルティを与える

        else:
            # プレーヤーのターンを継続するとき
            reward = 0

        observation = self.observe()
        self.done = self.is_done()
        return observation, reward, self.done, {}

    def render(self, mode='human', close=False):
        # 環境を可視化する
        # human の場合はコンソールに出力.ansi の場合は StringIO を返す
        pass

    def close(self):
        # 環境を閉じて,後処理をする
        pass

    def seed(self, seed=None):
        # ランダムシードを固定する
        pass

    def get_reward(self):
        # 報酬を返す
        reward = self.game.pay_chip() - self.game.player.chip.bet
        return reward

    def is_done(self):
        if self.game.player.done:
            return True
        else:
            return False

    def observe(self):
        if self.game.player.done:
            observation = tuple([
                self.game.player.hand.calc_final_point(),
                self.game.dealer.hand.calc_final_point(),  # Dealerのカードの合計点
                int(self.game.player.hand.is_soft_hand),
                int(self.game.player.hit_flag)])
        else:
            observation = tuple([
                self.game.player.hand.calc_final_point(),
                self.game.dealer.hand.hand[0].point,  # Dealerのアップカードのみ
                int(self.game.player.hand.is_soft_hand),
                int(self.game.player.hit_flag)])

        return observation

gym.envs.registration.register 関数を使って環境を登録し,BlackJack-v0というIDで呼び出せるようにする

myenv/__init__.py

gym.envs.registration.register関数を使ってBlackJackEnvをgymに登録します.
ここでBlackJack-v0というIDでmyenvディレクトリの下のenvディレクトリの下にあるBlackJackEnvというクラスを呼び出すことを宣言します.

myenv/__init__.py
from gym.envs.registration import register

register(
    id='BlackJack-v0',
    entry_point='myenv.env:BlackJackEnv',
)

myenv/env/__init__.py

ここでBlakcJackEnvクラスがmyenvディレクトリの下のenvディレクトリの下にあるblackjack_env.pyの中にあることを宣言します.

myenv/env/__init__.py
from myenv.env.blackjack_env import BlackJackEnv

強化学習させるには

強化学習のコードの中で,env = gym.make('BlackJack-v0')とすると環境を使うことができます.

今回は環境の登録がメインなので割愛しますが,次の記事はこれを作成します.

終わりに

自作したブラックジャックのゲームをOpenAI Gymの環境に登録してみました.
自作した環境に対して,何を行動とし,何を状態として観測し,何を報酬とするのか,そして1ステップとはどこからどこまでなのかと,よくよく考えながら作らなければいけないことを実感できました.
はじめは1ステップの長さをとんでもなく長く設定してしまっていました...

次は,この環境を使ってブラックジャックの戦略を学習させてみたいと思います.

参考にさせていただいたサイト/書籍

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away