Aidemy Tech Blog

機械学習・ディープラーニング関連技術の活用事例や実装方法をまとめる、株式会社アイデミーの技術ブログです。

自分で強くなるAI「DQN」で3色オセロ「トリコロール」の学習に挑戦

どーも! まじすけです🎉
今回は最近話題の強化学習、DQNに挑戦してみました。

以前「AINOW」というAIのキュレーションメディアにてDQNについての記事を書いたので、よろしければ見てみてくださいm(_ _)m
ainow.ai

今回はこのDQNを使って3色オセロのトリコロールを学習しました!

トリコロールとは?

f:id:majisuke:20170805195611p:plain
トリコロールは青・赤・白の3色で行うボードゲームです。

青と赤それぞれの裏面が白になっています。

f:id:majisuke:20170805195628p:plain
ひっくり返るルールはオセロと同じで、縦・横・斜めで自分の色のコマ同士で挟むことでひっくり返ります。

ただ記憶力が高くなければ白がひっくり返った時に何色になるかを覚えることができません。特に本プログラムでは初期配置の白コマの裏面はランダムになっています。なので、最初の時点ですでに運ゲー要素があります。

DQNの学習

今回はこちらのサイトを参考に実装しました。
機械学習の理論を理解せずに tensorflow で オセロ AI を作ってみた 〜導入編〜 - Qiita

まずはオセロとDQNのプログラムをこちらからダウンロードしてみてください。

$ git clone https://github.com/sasaco/tf-dqn-reversi

DQNはAI(エージェント)が状況を把握して行動します。行動に対する報酬"Q値"をエージェントに与えることで、より適した行動を取れるようになります。初めはランダムに行動した結果を保存しますが、勝った時に得られる報酬を参考にそれぞれの一手に対するQ値が定まります。

本プログラムではDQNのエージェント同士を戦わせて学習していきます。
まずは黒ターンと白ターンのAIを用意します

#train.py
#もろもろインポート
import copy
from Reversi import Reversi
from dqn_agent import DQNAgent

#オセロ開始の合図
if __name__ == "__main__":
    # 繰り返しの学習回数の設定
    n_epochs = 100
    # オセロの環境を構築
    env = Reversi()
    # playerID    
    playerID = [env.Black, env.White, env.Black]
    players = []
    # player[0]= 黒のターン
    players.append(DQNAgent(env.enable_actions, env.name, env.screen_n_rows, env.screen_n_cols))
    # player[1]= 白のターン
    players.append(DQNAgent(env.enable_actions, env.name, env.screen_n_rows, env.screen_n_cols))

ここから学習の記述になります。コマを置く、勝敗が決まる、というオセロの一連の流れをn_epochs回リピートして行動のモデルを保存します。
後にAIと戦う際に学習したAIが後攻になるので、後攻の勝敗の結果のみ保存します。

#train.py
    for e in range(n_epochs):
        env.reset() # 最初は初期化
        terminal = False
        while terminal == False: # 1エピソードが終わるまでループ
            for i in range(0, len(players)): 
                state = env.screen
                targets = env.get_enables(playerID[i])
                if len(targets) > 0: # どこかに置く場所がある場合
                    for tr in targets: #すべての手をトレーニングする
                        tmp = copy.deepcopy(env)
                        tmp.update(tr, playerID[i])
                        #終了判定
                        win = tmp.winner()
                        end = tmp.isEnd()
                        #次の状態
                        state_X = tmp.screen
                        target_X = tmp.get_enables(playerID[i+1])
                        if len(target_X) == 0:
                            target_X = tmp.get_enables(playerID[i])
                        # 両者トレーニング
                        for j in range(0, len(players)):
                            reword = 0
                            if end == True:
                                if win == playerID[j]:
                                    # 勝ったら報酬1を得る
                                    reword = 1
                            # 勝敗を保存する    
                            players[j].store_experience(state, targets, tr, reword, state_X, target_X, end)
                            players[j].experience_replay()

                    # 行動を選択  
                    action = players[i].select_action(state, targets, players[i].exploration)
                    # 行動を実行
                    env.update(action, playerID[i])
                    # ログの記述
                    loss = players[i].current_loss
                    Q_max, Q_action = players[i].select_enable_action(state, targets)
                    print("player:{:1d} | pos:{:2d} | LOSS: {:.4f} | Q_MAX: {:.4f}".format(
                             playerID[i], action, loss, Q_max))

                # 行動を実行した結果
                terminal = env.isEnd()     
        # 試合ごとの勝敗を記述               
        w = env.winner()                    
        print("EPOCH: {:03d}/{:03d} | WIN: player{:1d}".format(
                         e, n_epochs, w))
    # 後攻のplayer2のデータのみを保存する。
    players[1].save_model()

f:id:majisuke:20170806113353p:plain
実行するとこんな感じにどんどん学習していきます。(結構時間かかります)

DQNとの戦いの実装

次は実際に学習したAIと戦うプログラムです。コマンドライン上でコマンドを書きながらAIとオセロをします。argparseを使用することで、保存しているモデルをエージェントに追加します。

#FightWithAI.py
#もろもろインポート
import argparse
from Reversi import Reversi
from dqn_agent import DQNAgent

if __name__ == "__main__":
    # argparseでコマンドラインの引数を解釈
    parser = argparse.ArgumentParser()
    parser.add_argument("-m", "--model_path")
    parser.add_argument("-s", "--save", dest="save", action="store_true")
    parser.set_defaults(save=False)
    args = parser.parse_args()
    # オセロ環境を構築してmodelsのフォルダから学習したモデルをロードする
    env = Reversi()
    agent = DQNAgent(env.enable_actions, env.name, env.screen_n_rows, env.screen_n_cols)
    agent.load_model(args.model_path)

これ以下のほとんどのコードがオセロの実行プログラムReversi.pyと記述が同じなので、AIの実装部分のみ紹介します。保存されている学習結果を参考に次の手を決めています。

#FightWithAI.py
 print("*** AIターン● ***")
        env.print_screen()
        enables = env.get_enables(2)
        if len(enables) > 0:
            # 学習結果の中で最も報酬が高い一手を選択
            qvalue, action_t = agent.select_enable_action(env.screen, enables) 
            print('>>>  {:}'.format(action_t))              
            env.update(action_t, 2)
        else:
            print("パス")

f:id:majisuke:20170806113536p:plain
実行すると、このようにAIと戦えます。(画像はトリコロールでの実行結果)

オセロをトリコロールに書き換え

最後にオセロのプログラムReversi.pyをトリコロールに書き換えます。

まずは初期のマスが8*8と大きく時間がかかってしまうので、5*5に変えます。
そしてトリコロール独特のコマである「黒の裏」と「白の裏」を作っておきます。

import os
import numpy as np
import random # 初期の白コマにランダムを付与するため

class Reversi:
    def __init__(self):
        # parameters
        self.name = os.path.splitext(os.path.basename(__file__))[0]
        self.Blank = 0
        self.Black = 1
        self.White = 2
        self.Blackrev = 3 # 黒の裏のコマを追加
        self.Whiterev = 4 # 白の裏のコマを追加
        # 学習時間を早めるために5*5の25マスでプレイ #
        self.screen_n_rows = 5 
        self.screen_n_cols = 5 
        self.enable_actions = np.arange(self.screen_n_rows*self.screen_n_cols)
        # variables
        self.reset()

初期のコマの配置の際、センターのコマの色をランダムで決めます。
20~25行目

    def reset(self):
        """ 盤面の初期化 """
        # reset ball position
        self.screen = np.zeros((self.screen_n_rows, self.screen_n_cols))
        self.set_cells(7, self.White)
        self.set_cells(11, self.Black)
        self.set_cells(17, self.Black)
        self.set_cells(13, self.White)
        self.set_cells(12, random.randint(3,4)) # 初期の白コマはランダムで決まる

黒と白以外のコマは「裏」と表記することにします。
40~50行目

    def print_screen(self):
        """ 盤面の出力 """
        i = 0
        for r in range(self.screen_n_rows):
            s1 = ''
            for c in range(self.screen_n_cols):
                s2 = ''
                if self.screen[r][c] == self.Blank:
                    s2 = '{0:2d}'.format(self.enable_actions[i])
                elif self.screen[r][c] == self.Black:
                    s2 = '●'
                elif self.screen[r][c] == self.White:
                    s2 = '○'
                else:
                    s2 = '裏' # 出力が白黒なので、ひっくり返った時は「裏」にする
                s1 = s1 + ' ' + s2
                i += 1
            print(s1)

マスの数を変えたので、コマをおける場所の定義も変更します。
60~80行目

    def put_piece(self, action, color, puton=True):
        
        """ ---------------------------------------------------------"""

        t, x, y, l = 0, action%5, action//5, [] # 盤面の変更にともないコマの動きを変更
        for di, fi in zip([-1, 0, 1], [x, 4, 4-x]):
            for dj, fj in zip([-5, 0, 5], [y, 4, 4-y]):

裏に返す処理の記述です。黒と白だけであれば塗り替えるだけで良いのですが、反転するという動作に変更します。
110~120行目

if puton:
            """ ひっくり返す石を場合分けする """
            for i in l:
                if self.get_cells(i) == 3:
                    self.set_cells(i, 1)
                elif self.get_cells(i) ==4:
                    self.set_cells(i, 2)
                elif self.get_cells(i) ==1:
                    self.set_cells(i, 3)
                elif self.get_cells(i) ==2:
                    self.set_cells(i, 4)

これでトリコロールに書き換え完了です。

実戦!!

それでは学習数100回のAIと戦ってみました!

f:id:majisuke:20170806113717p:plain

負けちゃいましたね...

もうちょっと自分の腕を磨いて再チャレンジします。