雑魚ボードゲームAIを作ってしまった...
この記事はSoftbank AI部 Advent Calender2019 その2の16日目の記事です.
11月,学内イベント用にボードゲームの対戦AIを作りました.
今回はその対戦AIについて書いていきます.
SLIPE(スライプ)
今回使用したボードゲームはSLIPEです.
オーイシ加藤のピザラジオさんの動画(https://youtu.be/OAgw9-p4Rik)を一度を見てもらうとSLIPEがどういったものかイメージしやすいと思います.
SLIPEとは別にゴブレットゴブラーズも候補にあがったのですが,個人的に難易度が高い(どうやってAIを実装して良いか想像できなかった)と判断し,SLIPEを選びました.
SLIPEのルールは下図のようになっていて,盤面の中心に印の付いた駒を置くと勝利です.ルールも簡単で小学生向けのイベントでも問題なく扱えました.
対戦AI
対戦AIと言えば「強化学習でしょ!!」という天の声を信じた私はQ-Learningでの実装に取り掛かりました.
強化学習を簡単に説明すると,ある状態で環境に対してエージェントが何かしらの行動をした結果,次の状態が良くなったか悪くなったかを判断して,良い行動は何だろうかと学んでいくものです.(雑でごめんなさい)
SLIPEの環境
同じ研究室のM1が作ってくれました.
先攻の駒を2,4(印の付いた駒)で,後攻の駒を1,3(印の付いた駒)で,中央は5,その他は0としています.外枠に-1を入れて7×7マスにしていますが,意味あるのかな?と思います 理由は知りません.
import numpy as np import random import copy class Board(): # 盤面の初期化 def __init__(self): self.board = np.zeros((7,7)) - 1 #盤面をすべて-1にする self.board[2:5, 1:6] = 0 # インデクスは文字と文字の間を指しており,最初の文字の左端が0になっている self.board[1, 1:6] = 1 # △ self.board[5, 1:6] = 2 # ○ self.board[1, 3] = 3 # ▲ self.board[5, 3] = 4 # ● self.board[3, 3] = 5 # 真ん中 self.moves = 0 # 手数 self.winner = -1 # 勝敗 self.turn = 2 # ○が先攻 self.turn2 = 0 # どっちのターンか self.pie_sort = [] # 駒の種類 self.Piece = {'1':'△', '2':'○', '3':'▲', '4':'●', '0':' ', '5':'×'} # 駒の表示 self.game_end = False # ゲームの終わりを表す self.available_pie = [] self.int_tostr = ['a', 'b', 'c', 'd', 'e'] self.str_toint = {'a':'1', 'b':'2', 'c':'3', 'd':'4', 'e':'5'} # 駒を動かす def move_piece(self, pos): self.board[pos[0], pos[1]] = self.pie_sort # ゲーム終了チェック def end_check(self): if self.board[3, 3] == 3 or self.board[3, 3] == 4: self.winner = 1 if self.board[3, 3] == 3 else 2 self.game_end = True if self.moves == 100: # 100手かかった場合は引き分け self.winner = 0 self.game_end = True # ターンチェンジ def change_turn(self): self.turn = 1 if self.turn == 2 else 2 self.turn2 = 1 if self.turn2 == 2 else 2 self.moves += 1 # 手数を一つ増やす # 動かせる駒の場所を探し出す def search_pieces(self): pie = [] # piece 駒 (駒の場所) star = np.where(self.board == self.turn+2) # 自分の星の駒の場所を取得 est_mask = self.board == self.turn # 自分の駒の場所を取得 est_mask[star[0][0], star[1][0]] = True est = np.where(est_mask) for i in range(est[0].size): # 自分の駒の場所を一つずつ取得 p = [est[0][i], est[1][i]] # (i,j)表記にする x, y = est[0][i], est[1][i] if self.board[x+1, y] == 0 or self.board[x+1, y] == 5 or \ self.board[x-1, y] == 0 or self.board[x-1, y] == 5 or \ self.board[x, y+1] == 0 or self.board[x, y+1] == 5 or \ self.board[x, y-1] == 0 or self.board[x, y-1] == 5: pie.append(p) self.available_pie = pie # 動かせる駒(駒の場所を表す) # リスト型 return pie # リスト型 # [[x, y], [x,y]] # ランダムに動かす駒を選ぶ (ε-greedy用) def random_piece(self): if len(self.available_pie) > 0: pie = random.choice(self.available_pie) # 動かす駒をランダムに選ぶ return pie # リスト型 # [x, y] return False # 駒の種類を取得 def piece_sort(self, pie): self.pie_sort = self.board[pie[0], pie[1]] # ランダムに駒を動かす場所を選ぶ def random_position(self, pie): if len(pie) > 0: pos = random.choice(pie) # 動かす場所をランダムに選ぶ return pos # リスト型 # [x, y] return False # 選んだ駒の移動可能な場所を探す def move_available(self, pie): # posには選んだ駒の場所を入れる x = pie[0] y = pie[1] up_move = [] down_move = [] right_move = [] left_move = [] moves = [] # 上方向の動ける位置 if self.board[x+1][y] == 0 or self.board[x+1][y] == 5: up_move = [x+1, y] if self.board[x+2][y] == 0 or self.board[x+2][y] == 5: up_move = [x+2, y] if self.board[x+3][y] == 0 or self.board[x+3][y] == 5: up_move = [x+3, y] if self.board[x+4][y] == 0 or self.board[x+4][y] == 5: up_move = [x+4, y] # 選んだ駒がノーマル駒で進む場所が真ん中の時は追加しない if self.pie_sort == self.turn+2: moves.append(up_move) elif up_move != [3, 3]: moves.append(up_move) # 下方向の動ける位置 if self.board[x-1][y] == 0 or self.board[x-1][y] == 5: down_move = [x-1, y] if self.board[x-2][y] == 0 or self.board[x-2][y] == 5: down_move = [x-2, y] if self.board[x-3][y] == 0 or self.board[x-3][y] == 5: down_move = [x-3, y] if self.board[x-4][y] == 0 or self.board[x-4][y] == 5: down_move = [x-4, y] # 選んだ駒がノーマル駒で進む場所が真ん中の時は追加しない if self.pie_sort == self.turn+2: moves.append(down_move) elif down_move != [3, 3]: moves.append(down_move) # 右方向の動ける位置 if self.board[x][y+1] == 0 or self.board[x][y+1] == 5: right_move = [x, y+1] if self.board[x][y+2] == 0 or self.board[x][y+2] == 5: right_move = [x, y+2] if self.board[x][y+3] == 0 or self.board[x][y+3] == 5: right_move = [x, y+3] if self.board[x][y+4] == 0 or self.board[x][y+4] == 5: right_move = [x, y+4] # 選んだ駒がノーマル駒で進む場所が真ん中の時は追加しない if self.pie_sort == self.turn+2: moves.append(right_move) elif right_move != [3, 3]: moves.append(right_move) # 左方向の動ける位置 if self.board[x][y-1] == 0 or self.board[x][y-1] == 5: left_move = [x, y-1] if self.board[x][y-2] == 0 or self.board[x][y-2] == 5: left_move = [x, y-2] if self.board[x][y-3] == 0 or self.board[x][y-3] == 5: left_move = [x, y-3] if self.board[x][y-4] == 0 or self.board[x][y-4] == 5: left_move = [x, y-4] # 選んだ駒がノーマル駒で進む場所が真ん中の時は追加しない if self.pie_sort == self.turn+2: moves.append(left_move) elif left_move != [3, 3]: moves.append(left_move) return moves # リスト型 #[[x, y], [x+1, y]]こんな感じで入ってる # ボード表示 def show_board(self): print(' ', end='') #end=''で改行しない # [0, 0]に空白を入れる for i in range(1, 6): # 0行目に番号を振る print(' {}'.format(i), end='') print('') for i in range(1, 6): # 0列目に番号を振る print('{} '.format(self.int_tostr[i-1]), end='') for j in range(1, 6): # 駒を配置 print('{} '.format(self.Piece[str(int(self.board[i][j]))]), end='') print('') # キーボード入力する座標を配列にする def convert_coordinate(self, pos): pos = pos.split(' ') # ' 'を区切りとして,リストにする ii = self.str_toint[pos[0]] # aを1に i = int(ii) # 文字列を整数に j = int(pos[1]) return [i, j] # ゲーム終了 def judge(self, a, you): if self.winner == a: # コンピュータの勝ち print('Game over. You lose!') elif self.winner == you: # あなたの勝ち print('You win! Congratulations!') else: print('Time is up. Draw!') # ゲーム終了(Q_learning) def judge_Q(self, a, you): if self.winner == a: print('===WIN=== (Q2)') reward = -1 elif self.winner == you: print('===WIN=== (Q1)') reward = 1 else: print('Draw') reward = 0 return reward
Q-Learningで実装
Q-Learnigは行動価値をテーブルで用意してあげて,状態のときに価値が一番高い行動を選択します.拙いコードですが載せておきます.
Q_learning.pyでゲームを進行,Q_learning_function.pyで値を更新しています.
行動は駒を選ぶ(座標)と動かす方向(上下左右)の組み合わせで100通りあります.そして,状態 (5×5マスを一次元に直したもの)のときに選択肢にならない(座標が違う,方向が違う)ものは-50の価値を与えて使用不可にしています.一応,探索のために-greedy法を使い,一定の確率で探索を行います.
import numpy as np import copy import random import pickle import csv import train import Q_learning_function as Q_learn ### ----- main ----- ### # ゲームスタート print('===SLIPE===') you = int(2) # ここは別にいらないらしい a = int(1) q_table = Q_learn.q_table_initFunc() # q_tableを初期化 ACT = Q_learn.act_list() Total_reward = 0 # 先攻Q-learningの合計報酬用 Total_reward_2 = 0 # 後攻Q-learningの合計報酬用 win = 0 lose = 0 num_episodes = 10000 # トータルの試合数 for episode in range(num_episodes): board = train.Board() print('episode', episode) s_t = [] # 状態の保存用 s_t_2 = [] action = [] # 行動の保存用 action_2 = [] next_s = [] # 次の状態用 next_s_2 = [] # 2000回ごとに保存 if ((episode+1) % 2000) == 0: with open('Q_TABLE'+str(episode+1)+'.csv', 'w') as file: np.savetxt(file, q_table, fmt='%.4e', delimiter=',') with open('STATE'+str(episode+1)+'.csv', 'w') as file_state: np.savetxt(file_state, Q_learn.STATE, fmt='%d', delimiter=',') while not board.game_end: # 終了判定があるまではFalseが入る # Q_learning (先攻)--- state = board.board[1:6, 1:6] # 状態を取得 move_pos = [] moveable_pices = board.search_pieces() # 動かせる駒を探す # 動かせる駒の移動可能場所 for i in range(len(moveable_pices)): board.piece_sort(moveable_pices[i]) move = board.move_available(moveable_pices[i]) move_pos.append(move) # 移動可能場所がないときは負け if len(move_pos) == 0: board.game_end = True board.winner = a break while True: # 中央[3, 3]に移動可能(次で勝てる時) if [[3, 3]] in move_pos: num = move_pos.index([[3,3]]) moveable_pices = [moveable_pices[num]] move_pos = [move_pos[num]] # 駒を選ぶ,動く方向,移動場所をQ値によって判断 pie, action_number, move_pos = Q_learn.select_pie(moveable_pices, state, q_table, move_pos, episode) # 駒を選び,行動と移動先を返す board.piece_sort(pie) # 駒の種類を受け取る(番号2,4のどれか) moveable_pos = board.move_available(pie) # 移動可能な場所を確認(今思えばいらない) if moveable_pos == []: continue break s_t.extend([Q_learn.make_state_re(state)]) # 状態を保存>>>新規状態は保存していき,テーブルを作っていく action.extend([Q_learn.search_act_index(ACT, pie, action_number)]) # 駒を動かし,元の場所を0にする board.move_piece(move_pos) board.board[pie[0], pie[1]] = 0 # 次の状態を保存 next_s.extend([Q_learn.make_state_re(board.board[1:6, 1:6])]) #board.show_board() # 盤面の表示 board.end_check() if board.game_end: break board.change_turn() # Q_learning 2 (後攻)--- state = board.board[1:6, 1:6] # 状態を取得 move_pos_2 = [] moveable_pices_2 = board.search_pieces() # 動かせる駒を探す for i in range(len(moveable_pices_2)): board.piece_sort(moveable_pices_2[i]) move2 = board.move_available(moveable_pices_2[i]) move_pos_2.append(move2) if len(move_pos_2) == 0: board.game_end = True board.winner = you break while True: if [[3, 3]] in move_pos_2: num2 = move_pos_2.index([[3, 3]]) moveable_pices_2 = [moveable_pices_2[num2]] move_pos_2 = [move_pos_2[num2]] pie2, action_number, move_pos_2 = Q_learn.select_pie(moveable_pices_2, state, q_table, move_pos_2, episode) # 駒を選び,行動と移動先を返す board.piece_sort(pie2) moveable_pos_2 = board.move_available(pie2) if moveable_pos_2 == []: continue break s_t_2.extend([Q_learn.make_state_re(state)]) action_2.extend([Q_learn.search_act_index(ACT, pie2, action_number)]) board.move_piece(move_pos_2) board.board[pie2[0], pie2[1]] = 0 next_s_2.extend([Q_learn.make_state_re(board.board[1:6, 1:6])]) #board.show_board() board.end_check() board.change_turn() # 終了状態になるとgame_endがTrueになる if board.game_end: # 報酬の受け取り reward = board.judge_Q(a, you) reward_2 = - reward if reward > 0: # 先攻勝利時 先攻:全ての行動に報酬1 後攻:最後から三手目までに報酬-1 q_table = Q_learn.update_Qtable(q_table, s_t, action, reward, next_s) q_table = Q_learn.update_Qtable(q_table, s_t_2[-3:], action_2[-3:], reward_2, next_s_2[-3:]) elif reward < 0: # 後攻勝利時 先攻:最後から三手目までに報酬-1 後攻:全ての行動に報酬1 q_table = Q_learn.update_Qtable(q_table, s_t[-3:], action[-3:], reward, next_s[-3:]) q_table = Q_learn.update_Qtable(q_table, s_t_2, action_2, reward_2, next_s_2) Total_reward += reward Total_reward_2 += reward_2 if reward == 1: win += 1 elif reward == -1: lose += 1 continue print('Total reward', Total_reward) print('WIN Q1', win) print('WIN Q2', lose) print(q_table[0]) with open('Q_TABLE.csv', 'w') as file: np.savetxt(file, q_table, fmt='%.4e', delimiter=',') with open('STATE.csv', 'w') as file_state: np.savetxt(file_state, Q_learn.STATE, fmt='%d', delimiter=',')
import numpy as np import copy import random import csv import train global STATE STATE = [] STATE = np.genfromtxt('./STATE.csv', delimiter=',') STATE = STATE.tolist() #board = train.Board() ### ----- ACTION ----- ### # create action number --- def act_list(): # 駒の座標と動く方向の組み合わせを作る act = [] #空行列 for i in range(1,6): for j in range(1,6): for k in range(4): act.append([i, j, k]) return act # search for act index function --- def search_act_index(ACT, pie, action_number): # インデックスを調べる # argument: ACT;行動 pie;選んだ駒 number_action;駒の動き # return: 数値(int型)) [x, y] = pie LIST = [x, y, action_number] INDEX = ACT.index(LIST) return INDEX # convert action list --- def convert_action(pices, move_pos): # arugument: pices;動かせるコマの座標 # return: act_List;行動パターンのリスト act_List = [] # actionの取得用 pos = [] for i in range(len(pices)): action_number = able_action(pices[i], move_pos[i]) pos.extend(move_pos[i]) for j in range(len(action_number)): # 行動数:able_action pices[i].append(action_number[j]) a = copy.deepcopy(pices[i]) act_List.append(a) pices[i].pop() return act_List, pos # search able action --- def able_action(pie, move_pos): # argument: pie;移動する駒の座標, move_pos;移動可能な座標 # return: action_number;行動パターン(0~3までの整数値) #move_pos = board.move_available(pie) action_number = [0] * len(move_pos) # move_posの長さ分の配列を作成 [x_t, y_t] = pie # 元々の座標 for i in range(len(move_pos)): [x, y] = move_pos[i] if (x_t - x) > 0 and y_t == y: action_number[i] = 0 elif x_t == x and (y_t - y) < 0: action_number[i] = 1 elif (x_t - x) < 0 and y_t == y: action_number[i] = 2 else: action_number[i] = 3 return action_number # select action --- def select_pie(pices, state, q_table, move_pos, episode): # argument: pices;動かせる駒の座標,state;盤面の座標,q_table;Qテーブル # return: pie;動かす駒(どの座標の駒か), action_numer(行動の番号) s_t = make_state_re(state) # 状態を数値に変換 action_list, pos = convert_action(pices, move_pos) # 動かせる駒の行動リスト[o,o,o] ACT = act_list() #epsilon = 0.5 * (1 / (episode + 1)) epsilon = (1 / ((episode/100) + 1)) if epsilon <= np.random.uniform(0, 1): action_index = [ACT.index(action_list[i]) for i in range(len(action_list))] # actionのインデックス q_table = insert_zero(s_t, action_index, q_table) q_table_index = [i for i, x in enumerate(q_table[s_t]) if x == max(q_table[s_t])] maxindex = [action_index[i] for i in range(len(action_index)) if action_index[i] in q_table_index] # 複数ある場合はランダムで選ぶ if len(maxindex) == 1: chooseindex = maxindex[0] elif len(maxindex) > 1: chooseindex = maxindex[random.randrange(len(maxindex))] else: print('action_list', action_list) chooseindex = action_index[random.randrange(len(action_list))] [x, y, z] = ACT[chooseindex] pie = [x, y] action_number = z index = action_list.index([x, y, z]) pos = pos[index] else: action_index = [ACT.index(action_list[i]) for i in range(len(action_list))] q_table = insert_zero(s_t, action_index, q_table) chooseindex = random.randrange(len(action_list)) [x, y, z] = action_list[chooseindex] pie = [x, y] action_number = z pos = pos[chooseindex] return pie, action_number, pos ### ----- Q Value ----- ### # q_table --- def q_table_Func(): num_STATE = 500 # 状態空間の大きさ(適当) num_ACTION = 5*5*4 # 行動の種類(座標[5*5]],上下左右に動かす) q_table_init = np.zeros((num_STATE, num_ACTION)) return q_table_init def q_table_initFunc(): num_STATE = int(10e+5) num_ACTION = 5*5*4 q_table_init = [[0.0 for i in range(num_ACTION)] for j in range(num_STATE)] return q_table_init # insert -50 q_table --- def insert_zero(s_t, action_index, q_table): List = list(range(100)) for i in range(len(action_index)): List.remove(action_index[i]) for j in range(len(List)): q_table[s_t][List[j]] = -50.0 return q_table # state for q_table function --- def make_state_re(state): state = np.reshape(state, (1, 25)) # (1, 25)に変換 state = np.array(state, dtype='int') state = state.tolist() # ndarray --> list 変換 if state[0] in STATE: s_t = STATE.index(state[0]) else: STATE.append(state[0]) s_t = STATE.index(state[0]) return s_t # update Qtable function --- def update_Qtable(q_table, state, action, reward, next_state): # argument; Qテーブル,状態,行動,報酬,次の状態 # return; 更新したQテーブル for i in range(len(state)): gamma = 0.9 alpha = 0.5 next_maxQ = max(q_table[next_state[i]]) if q_table[state[i]][action[i]] >= -25: q_table[state[i]][action[i]] = (1 - alpha) * q_table[state[i]][action[i]] +\ alpha * (reward + gamma * next_maxQ) return q_table
実際に学習を回したのですが,意外と状態が多く,メモリをどんどん圧迫して計算も遅くなり全く学習ができませんでした.(状態が100万以上になったのは想定外でした.)
「5×5マスだから状態もそんなに多くないだろうし,q_tableを学習させたら終わりだな.」と安直な考えをしていた私を殴ってやりたい...
戦えないことはないのですが,ありえないほど弱い.そこで,DQNで実装することにしました.
DQNで実装
DQNはざっくり説明すると,Q-learningの行動価値をニューラルネットで表現したものです.行動はQ-learningと同じく100通りで,DQN同士で対戦させて学習させており,フレームワークにChainerを用いています.
自身のミス(移動不能な行動を選んだ場合)のときはそのエージェントだけに報酬-1を,中央に駒を置いたときは勝者に報酬1,敗者に報酬-1を与え,NNのパラメータを更新します.また,引き分けのときは両者に報酬0を与え更新します.
コードは以下のようです.
import chainer import chainer.functions as F import chainer.links as L import chainerrl import numpy as np import sys import random import re import copy import DQN_function as dqn import train ### Qfunction ### class QFunction(chainer.Chain): def __init__(self, obs_size, n_actions, n_nodes): w = chainer.initializers.HeNormal(scale=1.0) # 重みの初期化 super(QFunction, self).__init__() with self.init_scope(): self.l1 = L.Linear(obs_size, n_nodes, initialW=w) self.l2 = L.Linear(n_nodes, n_nodes, initialW=w) self.l3 = L.Linear(n_nodes, n_nodes, initialW=w) self.l4 = L.Linear(n_nodes, n_actions, initialW=w) def __call__(self, x): h = F.relu(self.l1(x)) h = F.relu(self.l2(h)) h = F.relu(self.l3(h)) return chainerrl.action_value.DiscreteActionValue(self.l4(h)) # SEtting NN --- obs_size = 5 * 5 # ボードサイズ n_actions = 100 # 行動数 n_nodes = 256 # 中間層のノード数 q_func = QFunction(obs_size, n_actions, n_nodes) # Setting optimizer --- optimizer = chainer.optimizers.Adam(eps=1e-2) optimizer.setup(q_func) # Setting explorer --- gamma = 0.99 explorer = chainerrl.explorers.LinearDecayEpsilonGreedy(start_epsilon=1.0, end_epsilon=0.1, decay_steps=50000, random_action_func=dqn.random_action) # Experience Replay用のbuffer replay_buffer_f = chainerrl.replay_buffer.ReplayBuffer(capacity=10 ** 6) replay_buffer_l = chainerrl.replay_buffer.ReplayBuffer(capacity=10 ** 6) # エージェントを別々に学習する agent_f = chainerrl.agents.DQN(q_func, optimizer, replay_buffer_f, gamma, explorer, replay_start_size=1000, minibatch_size=128, update_interval=1, target_update_interval=1000) agent_l = chainerrl.agents.DQN(q_func, optimizer, replay_buffer_l, gamma, explorer, replay_start_size=1000, minibatch_size=128, update_interval=1, target_update_interval=1000) agents = ['', agent_f, agent_l] # reward REWARD_WIN = 1 REWARD_LOSE = -1 all_f_win = 0 all_f_lose = 0 all_l_win = 0 all_l_lose = 0 all_draw = 0 f_win = 0 f_lose = 0 l_win = 0 l_lose = 0 draw = 0 f = int(2) f_m = 1.5 l = int(1) l_m = 2.5 num_episode = 1000000 # ゲーム数 # ゲームスタート print('===SLIPE===') for episode in range(1, num_episode + 1): board = train.Board() # ボードを初期化 rewards = [0, 0, 0] rewards = np.array(rewards, dtype='float32') turn = 0 if episode % 500 == 0: print('episode', episode, '~~~') # ここからが試合 while not board.game_end: turn += 1 # 先攻のDQN --- state = board.board[1:6, 1:6] state = np.reshape(state, (-1,)) state = np.array(state, dtype='float32') move_pos = [] # 移動可能な座標 moveable_pices = board.search_pieces() # 動かせる駒の座標を取得 for i in range(len(moveable_pices)): board.piece_sort(moveable_pices[i]) move = board.move_available(moveable_pices[i]) move_pos.append(move) # 動かせる駒の分だけ移動可能な場所を取得 if len(move_pos) == 0: board.game_end = True board.winner = l # 後攻の純粋な勝ち # NNで行動を決定及び学習 action_index = agents[1].act_and_train(state, rewards[1]) # 駒の座標[x, y], 方向directionを受け取る [x, y], direction = dqn.search_action(action_index) pie = [x, y] # action_indexが実際に使用可能なものかどうか判断し,ゲーム続行か判断 # 使用不能時は終了 if pie in moveable_pices: # 選んだ駒が動かせる駒の中にあるとき ab = moveable_pices.index(pie) # その駒がどのインデックスにあるか if move_pos[ab] != []: # 印なし駒が[3,3]しか移動できない場合は空が返ってくるため # 方向directionに実際移動可能か判断(可能だとBOOL==True) BOOL, pos = dqn.check_action_number(direction, pie, move_pos[ab]) if BOOL: # 駒を動かす board.piece_sort(pie) board.move_piece(pos) board.board[pie[0], pie[1]] = 0 #board.show_board() else: board.game_end = True board.winner = f_m else: board.game_end = True board.winner = f_m else: board.game_end = True board.winner = f_m #board.show_board() board.end_check() if board.game_end: break board.change_turn() # 後攻のDQN --- turn += 1 state = board.board[1:6, 1:6] state = np.reshape(state, (-1,)) state = np.array(state, dtype='float32') move_pos = [] moveable_pices = board.search_pieces() for i in range(len(moveable_pices)): board.piece_sort(moveable_pices[i]) move = board.move_available(moveable_pices[i]) move_pos.append(move) if len(move_pos) == 0: board.game_end = True board.winner = f action_index = agents[2].act_and_train(state, rewards[1]) [x, y], direction = dqn.search_action(action_index) pie = [x, y] if pie in moveable_pices: ab = moveable_pices.index(pie) if move_pos[ab] != []: BOOL, pos = dqn.check_action_number(direction, pie, move_pos[ab]) if BOOL: board.piece_sort(pie) board.move_piece(pos) board.board[pie[0], pie[1]] = 0 #board.show_board() else: board.game_end = True board.winner = l_m else: board.game_end = True board.winner = l_m else: board.game_end = True board.winner = l_m #board.show_board() board.end_check() if board.game_end: break board.change_turn() # 試合終了 if board.game_end: # 今の盤面をコピー boardcopy = np.reshape(board.board[1:6, 1:6], (-1,)) boardcopy = np.array(boardcopy, dtype='float32') if board.winner == f: # 純粋な先攻の勝利 f_win += 1 rewards[1] = REWARD_WIN l_lose += 1 rewards[2] = REWARD_LOSE # エピソード終了後の学習 agents[1].stop_episode_and_train(boardcopy, rewards[1], True) agents[2].stop_episode_and_train(boardcopy, rewards[2], True) elif board.winner == l: # 純粋な後攻の勝利 l_win += 1 rewards[2] = REWARD_WIN f_lose += 1 rewards[1] = REWARD_LOSE # エピソード終了後の学習 agents[1].stop_episode_and_train(boardcopy, rewards[1], True) agents[2].stop_episode_and_train(boardcopy, rewards[2], True) elif board.winner == f_m: # 先攻のミスで決着 rewards[1] = REWARD_LOSE agents[1].stop_episode_and_train(boardcopy, rewards[1], True) if turn != 1: agents[2].stop_episode_and_train(boardcopy, rewards[2], True) elif board.winner == l_m: # 後攻のミスで決着 rewards[2] = REWARD_LOSE agents[2].stop_episode_and_train(boardcopy, rewards[2], True) agents[1].stop_episode_and_train(boardcopy, rewards[1], True) else: draw += 1 agents[1].stop_episode_and_train(boardcopy, rewards[1], True) agents[2].stop_episode_and_train(boardcopy, rewards[2], True) if episode % 100000 == 0: agent_f.save('agent_f_' + str(episode/10000)) agent_l.save('agent_l_' + str(episode/10000)) print('DQN_f', f_win, f_lose) print('DQN_l', l_win, l_lose) print('draw', draw) print('turn:',turn) all_f_win += f_win all_f_lose += f_lose all_l_win += l_win all_l_lose += l_lose all_draw += draw f_win = 0 f_lose = 0 l_win = 0 l_lose = 0 draw = 0 print('WIN DQN_F:', all_f_win) print('WIN DQN_L:', all_l_win) print('DRAW:', all_draw)
脳筋なので100万回対戦させて,学習しています.
学習後のエージェントを使ってvsHuman.pyで対戦し,実力を測りました.
import chainer import chainer.functions as F import chainer.links as L import chainerrl import numpy as np import sys import re import random import copy import train import DQN_function as dqn ### Qfunction define ### class QFunction(chainer.Chain): def __init__(self, obs_size, n_actions, n_nodes): w = chainer.initializers.HeNormal(scale=1.0) # 重みの初期化 super(QFunction, self).__init__() with self.init_scope(): self.l1 = L.Linear(obs_size, n_nodes, initialW=w) self.l2 = L.Linear(n_nodes, n_nodes, initialW=w) self.l3 = L.Linear(n_nodes, n_nodes, initialW=w) self.l4 = L.Linear(n_nodes, n_actions, initialW=w) def __call__(self, x): h = F.relu(self.l1(x)) h = F.relu(self.l2(h)) h = F.relu(self.l3(h)) return chainerrl.action_value.DiscreteActionValue(self.l4(h)) # SEtting NN --- obs_size = 5 * 5 # ボードサイズ n_actions = 100 n_nodes = 256 # 中間層のノード数 q_func = QFunction(obs_size, n_actions, n_nodes) # Setting optimizer --- optimizer = chainer.optimizers.Adam(eps=1e-2) optimizer.setup(q_func) # Setting explorer --- gamma = 0.99 explorer = chainerrl.explorers.LinearDecayEpsilonGreedy(start_epsilon=1.0, end_epsilon=0.1, decay_steps=50000, random_action_func=dqn.random_action) # 未設定 replay_buffer = chainerrl.replay_buffer.ReplayBuffer(capacity=10 ** 6) agent = chainerrl.agents.DQN(q_func, optimizer, replay_buffer, gamma, explorer, replay_start_size=1000, minibatch_size=128, update_interval=1, target_update_interval=1000) charenge = True while charenge: board = train.Board() ### ここからゲームスタート ### print('=== SLIPE ===') you = input('先攻 (○, 2) or 後攻 (△, 1) を選択してください : ') you = int(you) a = 1 if you == 2 else 2 board.turn2 = you if board.turn2 == 2: s = '「○」(先攻)' agent.load('agent_l') elif board.turn2 == 1: s = '「△」(後攻)' agent.load('agent_f') print('あなたは{}です。ゲームスタート!'.format(s)) board.show_board() # ゲーム開始 while not board.game_end: while board.turn2 == 2: memory = board.pre_memory() # 駒を動かす前の状態を保持 ava_pie = board.search_pieces() print('あなたの番です。') if len(board.available_pie) == 0: # 動かせる駒が無いとき print('パスします。') else: # 動かせる駒があるとき pie = input('どの駒を動かしますか? (行列で指定。例 "a 1"):') if not re.match(r'[a-e] [1-5]', pie): # 盤面から外れた場所を指定した時 print('正しく駒の場所を選んでください。') continue # ループ内の処理をスキップする pie1 = board.convert_coordinate(pie) count = 0 for i in ava_pie: # 選んだ駒が自分のではない,または動かせない時 if pie1 == i: break count += 1 if count == len(ava_pie): print('この駒は動かせません') continue board.piece_sort(pie1) # 決めた駒の分類をする pos = input('どの場所に動かしますか? (行列で指定。例 "a 1"):') if not re.match(r'[a-e] [1-5]', pos): # 盤面から外れた場所を指定した時 print('正しく場所を選んでください。') continue # ループ内の処理をスキップする pos1 = board.convert_coordinate(pos) # 移動する場所を配列になおす moves = board.move_available(pie1) # 選んだ駒の移動可能な場所 count = 0 # 選んだ駒の移動可能な範囲と違うとき for i in moves: if pos1 == i: break count += 1 if count == len(moves): print('ここへは動かせません') continue board.move_piece(pos1) # 駒を動かす board.board[pie1[0], pie1[1]] = 0 # 動かした後の駒の部分を0に board.show_board() # 盤面を表示 rere = input('やっぱやり直す??? (続ける:''(Enter押す) やり直す:1):') if rere == '1': board.reset(memory) board.show_board() # 盤面を表示 continue board.end_check() # ゲームの終了をチェック board.change_turn() if board.game_end: board.judge(a, you) continue while board.turn2 == 1: print('コンピュータの番です。') #DQN --- state = board.board[1:6, 1:6] state = np.reshape(state, (-1,)) boardcopy = np.array(state, dtype='float32') move_pos = [] moveable_pieces = board.search_pieces() print('moveable_pieces:', moveable_pieces) for i in range(len(moveable_pieces)): board.piece_sort(moveable_pieces[i]) move = board.move_available(moveable_pieces[i]) move_pos.append(move) if len(move_pos) == 0: board.game_end = True board.winner = you break action_index = agent.act(boardcopy) #print('action_index:', action_index) [x, y], direction = dqn.search_action(action_index) pie = [x, y] print(pie, direction) if pie in moveable_pieces: ab = moveable_pieces.index(pie) if move_pos[ab] != []: BOOL, pos = dqn.check_action_number(direction, pie, move_pos[ab]) print(BOOL) if BOOL: board.piece_sort(pie) board.move_piece(pos) board.board[pie[0], pie[1]] = 0 print('NN output') else: pie = board.random_piece() board.piece_sort(pie) move_ava_pos = board.move_available(pie) if move_ava_pos == []: continue pos = board.random_position(move_ava_pos) board.move_piece(pos) board.board[pie[0], pie[1]] = 0 else: pie = board.random_piece() board.piece_sort(pie) move_ava_pos = board.move_available(pie) if move_ava_pos == []: continue pos = board.random_position(move_ava_pos) board.move_piece(pos) board.board[pie[0], pie[1]] = 0 else: pie = board.random_piece() board.piece_sort(pie) move_ava_pos = board.move_available(pie) if move_ava_pos == []: continue pos = board.random_position(move_ava_pos) board.move_piece(pos) board.board[pie[0], pie[1]] = 0 board.show_board() board.end_check() if board.game_end: break board.change_turn() if board.game_end: board.judge(a, you) continue charenge = input('再度チャレンジ>>>1,終了>>>2:') charenge = int(charenge) charenge = bool(charenge == 1) print(charenge)
結果はこれまた弱い...
ゲーム序盤はしっかりと駒を動かしてくるが,途中から高確率でミスをしてくる.
これは経験の差が大きいと感じていました.序盤の盤面は多く経験できているが,ゲームが進むとどうしても経験が少なくなる.また,の確率で探索を入れてくるので基本的に探索でエージェントがミスをして,ゲームが終了してしまい,長期戦ができていない.
しかし,ここで自分の過ちに気付きました.
探索のやり方がどう考えてもおかしいのです.
移動可能な駒と方向の中からランダムで行動を選択すれば良いものを,100個の行動パターンからランダムに選択していました.どうして気付かなかったのか...
100万回の対戦を無に帰す己の愚かさを呪います.
最後に
イベントはM1が作成したmin-max法の対戦AIで乗り切りました(研究室としては化血研賞というものを頂きました).
個人的には上手くいかなかったことが非常に悔しいので,探索を変更して再度学習させます.
ポスターセッションの質問に対するまとめ
2019年11月30日,学会に行ってきました.
ここでは,ポスターセッションで受けた質問に対して自分なりの考えをまとめていきます.
研究の概要
既に安定化された閉ループ系に対してフィードフォワード側から過渡応答を改善することを試みています.応答改善のためにニューラルネットワーク(NN)を用いて制御対象の入力を作成し,パラメータの学習には強化学習のDDPGを用いています.ただし,内部の数式モデルは未知のものとして扱っています.
質問
- 学習が終わったかどうかの判断をどうするのか
- ニューラルネットワークで微分器のような表現ができるのか
- NNの入力がよくないのではないか
- どうして強化学習を用いるのか
回答及び考え
1. 学習が終わったかどうかの判断をどうするのか
強化学習では行動価値関数で表現されるQ値が存在する.このQ値が更新されなくなったときに学習が収束したと判断できる.確か,こんな感じだったと思う.ただ,収束しないこともある(私の研究ではしていない).仮に収束したとしてそれが制御系での最適解なのかどうかは判断できないよね.という質問だったみたい.
正直,確かに!!と思ってしまった.内部の数式モデルは未知としてやっているがどこまでが良いのかどうかの判断は難しい.今は収益(強化学習でよく使う言葉)が一定ラインを越えた時に学習が終了したとしている(正しくは終了と勝手に見なしている).
2.ニューラルネットワークで微分器のような表現ができるのか
これに関してはお恥ずかしい話ですが,全く回答ができませんでした.
シミュレーションで用いている制御対象を最適解に持っていくためにはゲインをいじる必要があるらしく(制御的視点),それをNNで表現できるかは正直分かりませんでした.
3.NNの入力がよくないのではないか
私自身も入力が悪いのではないかと考えていました.ほとんどが0で一個だけ1が存在する.そこに時刻tを含めたものを入力としていました.これでは,発火する場所が少なく,NNを使うメリットが存在しない.そして,時刻tに関しても最大の時刻t_maxで割ることをしていなかったため,大きく発火している.などの指摘を受けました.また,時系列データに対して学習経験を用いていることと単純なNNを用いていることも指摘を受けました.
NNをリカレントニューラルネットワークに変更するか,入力に前の情報を与えてあげるなどの手法をとることで改善が見込めるかもしれません.あとは,NNに与える入力を極端な0,1にするのではなく,もう少し幅を持たせることで発火する場所を増やしてあげる.入力に関しては再度検討していくべきだと感じています.
4.どうして強化学習を用いるのか
この質問だけは意図が本当に読めず,回答に苦労しました.
恐らくですが,教師あり学習でいけるのではないかという質問だったと思います.
NNで作成した入力に対して,制御対象から出力が返ってきます.この出力が求めているものと一致しているのであれば,それを正解ラベルとして学習ができるかもしれません.しかし,未知の制御対象に対して正解が分かっているのであれば,それは未知ではなく既知です.こんな出力にしたいという目標をそのまま正解におけるのであれば強化学習は使いません.
最後に
卒業するためにも,今回頂いたアドバイスを活かします.
あと,初めて知ったカオスニューラルネットワークについても後日調べたいと思います.