編集:AIのパフォーマンスを向上させることができるかどうかを確認したい場合は、完全なソースコードをアップロードしました: https://www.dropbox.com/s/ous72hidygbnqv6/MCTS_TTT.rar
編集:検索スペースが検索され、損失をもたらす移動が検出されます。しかし、UCTアルゴリズムのため、損失をもたらす動きはあまり頻繁に訪れません。
MCTS(モンテカルロ木探索)について学ぶために、私はアルゴリズムを使用して、三目並べの古典的なゲーム用のAIを作成しました。次の設計を使用してアルゴリズムを実装しました。
ツリーポリシーはUCTに基づいており、デフォルトのポリシーはゲームが終了するまでランダムな移動を実行することです。私の実装で観察したことは、特定の移動が直接損失につながることを「認識」できないために、コンピューターが誤った移動を行うことがあるということです。
例えば: アクション6(赤い四角)が青い四角よりもわずかに高く評価されているため、コンピューターがこのスポットをマークしていることに注目してください。これは、ゲームのポリシーがランダムな動きに基づいているため、人間が青いボックスに「2」を入れない可能性が高いためだと思います。そして、プレイヤーが青いボックスに2を入れなかった場合、コンピューターは勝利を保証されます。
私の質問
1)これはMCTSの既知の問題ですか、それとも実装の失敗の結果ですか?
2)可能な解決策は何ですか?選択フェーズで動きを制限することを考えていますが、よくわかりません:-)
コアMCTSのコード:
//THE EXECUTING FUNCTION
public unsafe byte GetBestMove(Game game, int player, TreeView tv)
{
//Setup root and initial variables
Node root = new Node(null, 0, Opponent(player));
int startPlayer = player;
helper.CopyBytes(root.state, game.board);
//four phases: descent, roll-out, update and growth done iteratively X times
//-----------------------------------------------------------------------------------------------------
for (int iteration = 0; iteration < 1000; iteration++)
{
Node current = Selection(root, game);
int value = Rollout(current, game, startPlayer);
Update(current, value);
}
//Restore game state and return move with highest value
helper.CopyBytes(game.board, root.state);
//Draw tree
DrawTree(tv, root);
//return root.children.Aggregate((i1, i2) => i1.visits > i2.visits ? i1 : i2).action;
return BestChildUCB(root, 0).action;
}
//#1. Select a node if 1: we have more valid feasible moves or 2: it is terminal
public Node Selection(Node current, Game game)
{
while (!game.IsTerminal(current.state))
{
List<byte> validMoves = game.GetValidMoves(current.state);
if (validMoves.Count > current.children.Count)
return Expand(current, game);
else
current = BestChildUCB(current, 1.44);
}
return current;
}
//#1. Helper
public Node BestChildUCB(Node current, double C)
{
Node bestChild = null;
double best = double.NegativeInfinity;
foreach (Node child in current.children)
{
double UCB1 = ((double)child.value / (double)child.visits) + C * Math.Sqrt((2.0 * Math.Log((double)current.visits)) / (double)child.visits);
if (UCB1 > best)
{
bestChild = child;
best = UCB1;
}
}
return bestChild;
}
//#2. Expand a node by creating a new move and returning the node
public Node Expand(Node current, Game game)
{
//Copy current state to the game
helper.CopyBytes(game.board, current.state);
List<byte> validMoves = game.GetValidMoves(current.state);
for (int i = 0; i < validMoves.Count; i++)
{
//We already have evaluated this move
if (current.children.Exists(a => a.action == validMoves[i]))
continue;
int playerActing = Opponent(current.PlayerTookAction);
Node node = new Node(current, validMoves[i], playerActing);
current.children.Add(node);
//Do the move in the game and save it to the child node
game.Mark(playerActing, validMoves[i]);
helper.CopyBytes(node.state, game.board);
//Return to the previous game state
helper.CopyBytes(game.board, current.state);
return node;
}
throw new Exception("Error");
}
//#3. Roll-out. Simulate a game with a given policy and return the value
public int Rollout(Node current, Game game, int startPlayer)
{
Random r = new Random(1337);
helper.CopyBytes(game.board, current.state);
int player = Opponent(current.PlayerTookAction);
//Do the policy until a winner is found for the first (change?) node added
while (game.GetWinner() == 0)
{
//Random
List<byte> moves = game.GetValidMoves();
byte move = moves[r.Next(0, moves.Count)];
game.Mark(player, move);
player = Opponent(player);
}
if (game.GetWinner() == startPlayer)
return 1;
return 0;
}
//#4. Update
public unsafe void Update(Node current, int value)
{
do
{
current.visits++;
current.value += value;
current = current.parent;
}
while (current != null);
}
わかりました。コードを追加して問題を解決しました。
//If this move is terminal and the opponent wins, this means we have
//previously made a move where the opponent can always find a move to win.. not good
if (game.GetWinner() == Opponent(startPlayer))
{
current.parent.value = int.MinValue;
return 0;
}
問題は、検索スペースが小さすぎることだと思います。これにより、選択によって実際に終了する移動が選択された場合でも、この移動が選択されることはなく、代わりにリソースが他の移動の探索に使用されます:)。
現在、AI対AIは常に同点であり、Aiは人間として打ち負かすことは不可能です:-)
あなたの答えは受け入れられたものとしてマークされるべきではないと思います。 Tic-Tac-Toeの場合、検索スペースは比較的小さく、妥当な反復回数内で最適なアクションを見つける必要があります。
更新関数(バックプロパゲーション)は、異なるツリーレベルのノードに同じ量の報酬を追加するようです。現在のプレイヤーはツリーレベルによって異なるため、これは正しくありません。
この例のUCTメソッドのバックプロパゲーションを確認することをお勧めします: http://mcts.ai/code/python.html
特定のレベルで前のプレーヤーによって計算された報酬に基づいて、ノードの合計報酬を更新する必要があります(例ではnode.playerJustMoved)。
私の最初の推測は、アルゴリズムが機能する方法で、試合に勝つ可能性が最も高い(エンドノードで最も勝つ)ステップを選択することです。
したがって、AIの「失敗」を示す例は、私が正しければ「バグ」ではありません。動きを評価するこの方法は、敵のランダムな動きから始まります。このロジックは失敗します。これは、試合に勝つためにどの1ステップを実行するかがプレーヤーにとって明らかであるためです。
したがって、プレーヤーの勝利で次のノードを含むすべてのノードを消去する必要があります。
多分私は間違っている、ただの最初の推測でした...
したがって、ランダムベースのヒューリスティックでは、ゲーム空間の代表的なサンプルを検索しない可能性があります。例えば。理論的には、失われる隣接するブランチを完全に無視して、まったく同じシーケンスを100回ランダムにサンプリングする可能性があります。これは、すべての動きを見つけようとするより一般的な検索アルゴリズムとは一線を画しています。
ただし、これは実装の失敗である可能性がはるかに高くなります。ティックタックのゲームツリーはそれほど大きくなく、約9です!移動1で、急速に縮小するため、ツリー検索がすべての移動で妥当な反復回数を検索するわけではないため、最適な移動を見つける必要があります。
あなたのコードがなければ、私はそれ以上のコメントを提供することはできません。
私が推測するつもりなら、おそらくあなたは勝利の最大の分数ではなく、最大の勝利の数に基づいて動きを選択していると言うでしょう、そしてそれ故に一般的に検索された動きに向かって選択を偏らせますほとんどの時間。