はじめに
Pythonと強化学習の勉強を兼ねて,ブラックジャックの戦略作りをやってみました.
ベーシックストラテジーという確率に基づいた戦略がありますが,それに追いつけるか試してみます.
こんな感じで進めていきます
- ブラックジャック実装
- OpenAI gymの環境に登録 ← 今回はここ
- 強化学習でブラックジャックの戦略を学習
OpenAIのgymとは
強化学習の研究環境として使われるプラットフォームです.
CartPoleや迷路などの環境(ゲーム)が用意されており,簡単に強化学習を試すことができます.
OpenAI Gymの環境は,エージェントからの行動を受け取り,その結果としてその次の状態と報酬を返す共通のインターフェースを持っています.
インストールは以下のように簡単にできますが,詳しい方法は他のページを参考にしてください.以下,インストールが終わってるものとして説明します.
pip install gym
今回はこのOpenAI Gymの環境に自分で作ったブラックジャックを登録して,強化学習できるようにします.
強化学習のおさらい
まず簡単に強化学習のおさらいから.
「環境」から「状態」を観測し,「エージェント」がそれに対して「行動」を起こします.「環境」は「エージェント」に更新された「状態」と「報酬」をフィードバックします.
強化学習の目的は,将来にわたって得られる「報酬」の総和を最大化する「行動」の仕方(=方策)を獲得することです.
ブラックジャックに強化学習の要素を当てはめる
今回のブラックジャックでは,次のように強化学習を考えます.
- 環境:ブラックジャック
- エージェント:Player
- 状態:Playerのカード,Dealerのカードなど
- 行動:Playerの選択.HitやStandなど
- 報酬:勝負で得られるチップ
OpenAI Gymに環境を登録する手順
次の手順で自作の環境をOpenAI Gymに登録します.
- OpenAI Gymのgym.Envを継承したブラックジャック環境のクラス「BlackJackEnv」を作成する
- 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つの行動がとれることを表しています.
self.action_space = gym.spaces.Discrete(4)
observation_spaceプロパティ
Playerの手札の合計点,Dealerの開示されている手札の点,ソフトハンド(Playerの手札にAが含まれる)を示すフラグ,PlayerがHit済みか示すフラグの4つの状態を観測します.
それぞれの最大値,最小値を決めます.
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プロパティ
報酬の範囲を決めます.ここでは獲得できるチップの最小値と最大値が含まれるように決めています.
self.reward_range = [-10000, 10000]
resetメソッド
self.doneの初期化,self.game.reset_game()でPlayer, Dealerの手札の初期化,チップを賭ける(Bet),カードの配布(Deal)を行います.
self.doneはstepメソッドで触れる通り,勝敗がついているかを示すブール値です.
self.observe()で4つの状態を観測して返します.
ただし今回は,Playerの所持チップは減らないものとして学習させることとしました.
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をした場合にはルール違反ということでペナルティを与えることとしました.
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のコード全体は次のようになります.
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
というクラスを呼び出すことを宣言します.
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
の中にあることを宣言します.
from myenv.env.blackjack_env import BlackJackEnv
強化学習させるには
強化学習のコードの中で,env = gym.make('BlackJack-v0')
とすると環境を使うことができます.
今回は環境の登録がメインなので割愛しますが,次の記事はこれを作成します.
終わりに
自作したブラックジャックのゲームをOpenAI Gymの環境に登録してみました.
自作した環境に対して,何を行動とし,何を状態として観測し,何を報酬とするのか,そして1ステップとはどこからどこまでなのかと,よくよく考えながら作らなければいけないことを実感できました.
はじめは1ステップの長さをとんでもなく長く設定してしまっていました...
次は,この環境を使ってブラックジャックの戦略を学習させてみたいと思います.