引き続きKaggleのイントロをみます。ほとんどコピペですが。。。
ヒューリスティックを使用してエージェントと知識を共有する方法を学びます。
ゲームツリー
人間のプレイヤーとして、ゲームをプレイする方法についてどのように考えますか?代替手をどのように評価しますか?
あなたは少し予測をしている可能性があります。各潜在的な手について、相手が応答としてどのように反応すると予測されるか、その後どのように反応するか、その後の相手の反応はどうなるか、などを予測します。そして、勝利の可能性が最も高いと思われる手を選びます。
このアイディアを使ってすべての可能な結果を(完全な)ゲームツリーで表現することができます。
ゲームツリー
ゲームツリーは、空のボードから始まる各可能な手(エージェントと対戦相手の両方)を表現します。最初の行はエージェント(赤いプレイヤー)が行えるすべての可能な手を示しています。次に、対戦相手(黄色いプレイヤー)が応答として行える各手を記録し、各枝がゲームの終わりに達するまで続きます。(Connect Fourのゲームツリーは非常に大きいため、上の画像ではプレビューのみを表示しています。)
ゲームが可能な終了のすべての方法を見ることができれば、勝利の可能性が最も高い手を選ぶのに役立ちます。
ヒューリスティック
Connect Fourの完全なゲームツリーには4兆以上の異なるボードがあります!実際には、エージェントは手を計画する際に小さなサブセットだけを使用します。
不完全なツリーがエージェントにとって依然として有用であることを確認するために、ヒューリスティック(またはヒューリスティック関数)を使用します。ヒューリスティックは、ゲームのボードに異なるスコアを割り当てます。スコアが高いボードは、エージェントがゲームに勝つ可能性が高いと推定されます。ゲームの知識に基づいてヒューリスティックを設計します。
例えば、Connect Fourに合理的にうまく機能する可能性があるヒューリスティックは、各(水平、垂直、または対角線上の)線上の4つの隣接する場所を見て、以下のように点を割り当てます:
1000000(1e6)ポイントを、エージェントが4つのディスクを並べて持っている場合(エージェントが勝った場合)、
1ポイントを、エージェントが3つのスポットを埋め、残りのスポットが空の場合(エージェントが空のスポットに入れると勝ちます),
-100ポイントを、対戦相手が3つのスポットを埋め、残りのスポットが空の場合(対戦相手が空のスポットに入れると勝ちます)。
これは下の画像にも表示されています。
エージェントは具体的にヒューリスティックをどのように使用するのでしょうか?エージェントのターンであり、下の図の上部に示されているゲームボードの手を計画しようとしていると仮定します。7つの可能な手があります(各列に1つずつ)。各手に対して、結果としてのゲームボードを記録します。
それでは、ヒューリスティックを使用して各ボードにスコアを割り当てます。これを行うには、グリッドを検索してヒューリスティックのパターンのすべての出現を探し、ワードサーチパズルのように動作します。各出現はスコアを変更します。例えば、
1. エージェントが列0でプレイする最初のボードのスコアは2です。これは、ボードにスコアに1点を追加する2つの異なるパターンが含まれているためです(上の画像で両方とも囲まれています)。
2. 第二のボードには1のスコアが割り当てられます。
3. エージェントが列2でプレイする第三のボードのスコアは0です。これは、ヒューリスティックのパターンがボードに表示されないためです。
最初のボードは最高のスコアを受け取るので、エージェントはこの手を選択します。これは、もう1手で確実に勝つため、エージェントにとっても最良の結果です。これがあなたに意味をなすかどうか、今すぐ図で確認してください!
この特定の例に対してヒューリスティックは非常にうまく機能します。なぜなら、それは最高の手と最高のスコアを一致させるからです。これはConnect Fourエージェントを作成するためのうまく機能する多くのヒューリスティックのうちの1つに過ぎません。より効果的なヒューリスティックを設計できるかもしれません!
一般的に、ヒューリスティックをどのように設計するかわからない場合(つまり、異なるゲーム状態にどのようにスコアを付けるか、または異なる条件にどのスコアを割り当てるか)、しばしば最良の方法は単に初期の推測を取ることです。そして、エージェントに対してプレイします。これにより、ヒューリスティックを変更して修正できる、エージェントが悪い手を使う特定のケースを特定できます。
コード
私たちの一歩先読みエージェントは:
1. ヒューリスティックを使用して、各可能な有効な手にスコアを割り当てる
2. 最高のスコアを取得する手を選択します(複数の手が最高スコアを取得する場合、ランダムに1つを選択します)。
「一歩先読み」とは、エージェントがゲームツリーの深い部分ではなく、未来の1ステップ(または手)だけを見ることを指します。
このエージェントを定義するには、以下のコードセルの関数を使用します。これらの関数は、それらを使用してエージェントを指定するときにもっと意味があります。
import random
import numpy as np
In [2]:
# Calculates score if agent drops piece in selected column
def score_move(grid, col, mark, config):
next_grid = drop_piece(grid, col, mark, config)
score = get_heuristic(next_grid, mark, config)
return score
Helper function for score_move: gets board at next step if agent drops piece in selected column
def drop_piece(grid, col, mark, config):
next_grid = grid.copy()
for row in range(config.rows-1, -1, -1):
if next_grid[row][col] == 0:
break
next_grid[row][col] = mark
return next_grid
Helper function for score_move: calculates value of heuristic for grid
def get_heuristic(grid, mark, config):
num_threes = count_windows(grid, 3, mark, config)
num_fours = count_windows(grid, 4, mark, config)
num_threes_opp = count_windows(grid, 3, mark%2+1, config)
score = num_threes - 1e2*num_threes_opp + 1e6*num_fours
return score
Helper function for get_heuristic: checks if window satisfies heuristic conditions
def check_window(window, num_discs, piece, config):
return (window.count(piece) == num_discs and window.count(0) == config.inarow-num_discs)
Helper function for get_heuristic: counts number of windows satisfying specified heuristic conditions
def count_windows(grid, num_discs, piece, config):
num_windows = 0
# horizontal
for row in range(config.rows):
for col in range(config.columns-(config.inarow-1)):
window = list(grid[row, col:col+config.inarow])
if check_window(window, num_discs, piece, config):
num_windows += 1
# vertical
for row in range(config.rows-(config.inarow-1)):
for col in range(config.columns):
window = list(grid[row:row+config.inarow, col])
if check_window(window, num_discs, piece, config):
num_windows += 1
# positive diagonal
for row in range(config.rows-(config.inarow-1)):
for col in range(config.columns-(config.inarow-1)):
window = list(grid[range(row, row+config.inarow), range(col, col+config.inarow)])
if check_window(window, num_discs, piece, config):
num_windows += 1
# negative diagonal
for row in range(config.inarow-1, config.rows):
for col in range(config.columns-(config.inarow-1)):
window = list(grid[range(row, row-config.inarow, -1), range(col, col+config.inarow)])
if check_window(window, num_discs, piece, config):
num_windows += 1
return num_windows
次のコードセルで一歩先読みエージェントが定義されています。
# The agent is always implemented as a Python function that accepts two arguments: obs and config
def agent(obs, config):
# Get list of valid moves
valid_moves = [c for c in range(config.columns) if obs.board[c] == 0]
# Convert the board to a 2D grid
grid = np.asarray(obs.board).reshape(config.rows, config.columns)
# Use the heuristic to assign a score to each possible board in the next turn
scores = dict(zip(valid_moves, [score_move(grid, col, obs.mark, config) for col in valid_moves]))
# Get a list of columns (moves) that maximize the heuristic
max_cols = [key for key in scores.keys() if scores[key] == max(scores.values())]
# Select at random from the maximizing columns
return random.choice(max_cols)
エージェントのコードでは、まず有効な手のリストを取得します。これは前のチュートリアルで使用したのと同じコード行です!
次に、ゲームボードを2Dのnumpy配列に変換します。Connect Fourの場合、gridは6行7列の配列です。
次に、score_move()関数は各有効な手のヒューリスティックの値を計算します。この関数はいくつかのヘルパー関数を使用します:
drop_piece()は、選択した列にディスクをドロップしたときの結果としてのグリッドを返します。
get_heuristic()は、供給されたボード(grid)のヒューリスティックの値を計算します。この関数は、count_windows()関数を使用します。これは、ヒューリスティックの特定の条件を満たすウィンドウ(行、列、または対角線の4つの隣接する位置)の数を数える関数です。具体的には、count_windows(grid, num_discs, piece, config)は、ゲームボード(grid)内のウィンドウで、num_discsピースがマークpieceのプレイヤー(エージェントまたは対戦相手)にあり、ウィンドウの残りの位置が空であるウィンドウの数を返します。例として、
num_discs=4およびpiece=obs.markを設定すると、エージェントが4つのディスクを一列に並べた回数をカウントします。
num_discs=3およびpiece=obs.mark%2+1を設定すると、対戦相手が3つのディスクを持ち、残りの位置が空であるウィンドウの数をカウントします(対戦相手は空のスポットに入れることで勝ちます)。
最後に、ヒューリスティックを最大化する列のリストを取得し、その中からランダムに1つを選択します。
(注:このコースでは、フォローしやすい比較的遅いコードを提供することにしました。上記のコードを理解する時間を取った後、それをかなり速く実行する方法で再記述する方法がわかりますか?ヒントとして、count_windows()関数がゲームボードの位置で何度もループするために使用されることに注意してください。)
次のコードセルでは、ランダムエージェントとの1ゲームの結果を見ることができます。
from kaggle_environments import make, evaluate
Create the game environment
env = make("connectx")
Two random agents play one game round
env.run([agent, "random"])
Show the game
env.render(mode="ipython")
We use the get_win_percentage() function from the previous tutorial to check how we can expect it to perform on average.
unfold_lessHide code
In [5]:
def get_win_percentages(agent1, agent2, n_rounds=100):
# Use default Connect Four setup
config = {'rows': 6, 'columns': 7, 'inarow': 4}
# Agent 1 goes first (roughly) half the time
outcomes = evaluate("connectx", [agent1, agent2], config, [], n_rounds//2)
# Agent 2 goes first (roughly) half the time
outcomes += [[b,a] for [a,b] in evaluate("connectx", [agent2, agent1], config, [], n_rounds-n_rounds//2)]
print("Agent 1 Win Percentage:", np.round(outcomes.count([1,-1])/len(outcomes), 2))
print("Agent 2 Win Percentage:", np.round(outcomes.count([-1,1])/len(outcomes), 2))
print("Number of Invalid Plays by Agent 1:", outcomes.count([None, 0]))
print("Number of Invalid Plays by Agent 2:", outcomes.count([0, None]))
In [6]:
get_win_percentages(agent1=agent, agent2="random")
Agent 1 Win Percentage: 0.96
Agent 2 Win Percentage: 0.04
Number of Invalid Plays by Agent 1: 0
Number of Invalid Plays by Agent 2: 0
Agent 1 Win Percentage: 0.96
Agent 2 Win Percentage: 0.04
Number of Invalid Plays by Agent 1: 0
Number of Invalid Plays by Agent 2: 0
このエージェントはランダムエージェントよりもはるかに優れています!