本文は、Sunrise:AlphaZero、MCTS、自己対戦、UCB を理解するための基礎などの文章、ビデオチュートリアル、コード実装の消化と理解についてです。
本文は、AlphaGo の設計原理から出発し、MCTS と自己対戦という二つのコアメカニズムを深く理解することで、人間を超える AI の五目並べ(Gomoku)システムを構築する方法を段階的に明らかにします。
AlphaGo:模倣から超越へ#
AlphaGo の進化の道筋#
囲碁の可能な局面数は宇宙の原子の数を超えており、従来の網羅的探索法は完全に無効です。AlphaGo は段階的なアプローチでこの問題を解決しました:まず人間の知識を学び、その後自己対戦を通じて進化を続けます。
この進化プロセスは三つの層に分けられます:
- 人間の高手を模倣する
- 自己対戦による向上
- 状況評価を学ぶ
コアコンポーネント#
クイックムーブ戦略(Rollout Policy)#
最左の軽量な戦略ネットワークで、迅速な評価に使用され、精度は低いが計算効率が高いです。
SL 戦略ネットワーク#
監視学習戦略ネットワーク は人間の棋譜を模倣して学習します:
- 入力:盤面状態
- 出力:人間の専門家の次の手の確率分布
- トレーニングデータ:16 万局、約 3000 万の手順
RL 戦略ネットワーク#
あなたの棋力が一定のレベルに達したときに、自分で復盤し、自分と対戦し、新しい戦術やより深い戦略を発見するのに似ています。RL ネットワーク は SL ネットワークから初期化され、このネットワークは人間を超えており、多くの人間が発見していない強力な戦略を見つけることができます。
- この段階では大量の自己対戦データが生成され、入力は依然として盤面状態で、出力は強化学習によって改善された手の戦略です。
- 歴史的な戦略ネットワーク と自己対戦を行い、ネットワークは「勝利」の信号を通じてパラメータを調整し、勝利をもたらす手を強化します:
ここで:
- は対局のシーケンス
- は勝敗ラベル(勝ちが正、負けが負)
価値ネットワーク#
価値ネットワーク は状況評価を学習し、CNN である可能性があります:
- トレーニング目標:平均二乗誤差を最小化
- :最終的な勝敗結果
- :予測された勝利確率
補足説明#
と の関係について:
- 戦略ネットワーク は具体的な手の確率を提供し、探索を指導します。
- 価値ネットワーク は局面評価を提供し、探索木の中で不必要なシミュレーションを減少させます。
- 両者の組み合わせにより、MCTS は高勝率のパスをより迅速に探索できるだけでなく、全体の棋力を大幅に向上させます。
AlphaGo の MCTS 実装#
Selection 段階#
探索と利用を組み合わせます:
ここで:
- :長期的なリターンの推定
- :探索報酬
- :戦略ネットワーク出力の事前確率
- :訪問回数
シミュレーションと評価#
元の MCTS アルゴリズムでは、シミュレーション段階の役割は、迅速なロールアウト戦略を通じて葉ノード(拡張された新しいノード)からランダムに対局を行い、ゲームが終了するまで続け、その結果に基づいて報酬を得ることです。この報酬は検索ツリー内のノードに戻され、これらのノードの値推定(すなわち )を更新するために使用されます。
このような実装はシンプルで直接的ですが、ロールアウト戦略は通常ランダムまたは単純なルールであり、シミュレーションの質が低い可能性があります。また、短期的な情報しか提供できず、全体的な戦略評価をうまく結びつけることができません。
AlphaGo はシミュレーション段階で価値ネットワーク とロールアウト戦略を組み合わせました。価値ネットワークはより効率的な葉ノードの推定と全体的な能力評価を提供し、ロールアウト戦略は迅速なシミュレーションを通じて局所的な短期効果を捉えます。
超パラメータ を使用して と のバランスを取り、局所的なシミュレーションと全体的な評価を両立させます。評価関数:
バックプロパゲーション#
n 回の MCTS でノードの訪問回数を更新します(は指示関数で、訪問すると 1 になります):
Q 値を更新し、ノード に対する長期的な期待リターンを実行します:
まとめ#
-
構造の革新:
- 戦略ネットワークは事前知識を提供
- 価値ネットワークは全体評価を提供
- MCTS は戦術検証を提供
-
トレーニングの革新:
- 監視学習からスタート
- 強化学習を通じて教師を超える
- 自己対戦によって新しい知識を生成
-
MCTS の改善:
- ニューラルネットワークを使用して探索を指導
- ポリシーネットワークは探索方向の事前確率を提供し、価値ネットワークは葉ノードの評価精度を向上させました。
- 価値ネットワークとロールアウトの評価を組み合わせることで、探索の幅と深さを減少させ、全体的な性能を大幅に向上させました。
- 効率的な探索 - 利用のバランス
この設計により、AlphaGo は広大な探索空間の中で効率的な解決策を見つけ、最終的に人間のレベルを超えることができました。
ヒューリスティック探索と MCTS#
MCTS は、意思決定ツリーの中で最適なパスを探す進化し続ける探索者のようです。
コアアイデア#
MCTS の本質は何でしょうか?簡単に言えば、それは「遊びながら学ぶ」プロセスです。全く新しいボードゲームをプレイしていると想像してみてください:
- 最初は、さまざまな可能な手を試みます(探索)
- 徐々に、特定の手がより良い結果をもたらすことに気づきます(利用)
- 新しい戦略を探索し、既知の良い戦略を利用する間でバランスを取ります
これが MCTS が行うことです。ただし、数学的な方法でこのプロセスを体系化しています。これはロールアウトアルゴリズムであり、モンテカルロシミュレーションの価値推定を蓄積して戦略選択を導きます。
アルゴリズムの流れ#
Monte Carlo Tree Search - YouTube この先生は MCTS の流れを非常に良く説明しています。
MCTS の優雅さは、その四つのシンプルで強力なステップにあります。ここでは A と B の二つの理解方法を用いて紹介します:
-
選択(Selection)
- A:子供がどうやって学ぶか知っていますか?彼らは常に既知と未知の間をさまよっています。MCTS も同様です:根ノードから始め、UCB(Upper Confidence Bound)公式を使用して、既知の良いパスを選ぶか、新しい可能性を探索するかを評価します。
- B:根ノードから出発し、特定の戦略に基づいて現在のノードから次のノードを選択します。この戦略は通常、ツリーの探索履歴に基づいており、最も潜在能力のあるパスを選択します。例えば、各ノードで現在の戦略 に基づいてアクション を実行し、探索と利用のバランスを取りながら徐々に深く進みます。
-
拡張(Expansion)
- A:探検家が地図上で新しい領域を開拓するように、葉ノードに到達したとき、下に拡張し、新しい可能性を作成します。
- B:選択されたノードに未探索の子ノードがある場合、そのノードの下でアクションセットに基づいて新しいノードを拡張します。このプロセスの目的は、意思決定ツリーの幅を増やし、可能な意思決定パスを徐々に生成することです。この拡張操作を通じて、検索がより多くの可能な状態アクションペア をカバーすることを保証します。
-
シミュレーション(Simulation)
- A:これは最も面白い部分です。新しいノードから始め、ゲームが終了するまで「仮想的な」ゲームを行います。これは、チェスをプレイしているときに「もし私がこう動いたら、相手がああ動く...」と頭の中で推測するようなものです。
- B:現在の拡張ノードから出発し、ランダムシミュレーション(ロールアウト)を実行し、MDP 環境内で現在の戦略 に沿ってサンプリングを行い、終了状態まで進みます。このプロセスは、現在のノードから終点までの報酬推定を提供し、パスの優劣に数値的な根拠を与えます。
-
バックプロパゲーション(Backpropagation)
- A:最後に、得られた結果をパスに沿って戻し、各ノードの統計情報を更新します。これは「ねえ、私はこの道を試したけど、うまくいった(またはあまり良くなかった)」と言っているようなものです。
- B:シミュレーションが完了した後、そのシミュレーションの推定報酬を通過したすべてのノードに戻し、これらのノードの価値を更新します。このプロセスは、歴史的な報酬情報を蓄積し、将来の選択がより正確に高いリターンのパスに向かうようにします。
UCB1:探索と利用の完璧なバランス#
ここで特に UCB1 公式について言及する必要があります。これは MCTS の魂です:
これを分解してみましょう:
- は平均リターン(利用項)
- は不確実性の測定(探索項)
- は探索パラメータ
優れたポートフォリオのように、既知の良い機会に注目するだけでなく、新しい機会に対してもオープンでいる必要があります(探索 - 利用のバランス)。
Best Multi-Armed Bandit Strategy? (feat: UCB Method) このビデオは Multi-Armed Bandit と UCB Method について非常に良く説明しています。ここでこの先生が使った例を借用します:
食いしん坊が UCB を理解しようとする#
あなたが新しい都市に到着し、100 軒のレストランが選べると想像してみてください。あなたには 300 日があります。毎日、あなたはレストランを選んで食事をし、300 日間で最高の料理を平均して食べることを望んでいます。
ε-greedy 戦略:シンプルだが十分ではない#
これはサイコロを振るようなものです:
- 90%の時間 (ε=0.1):既知の最高のレストランに行く(利用)
- 10%の時間:ランダムに新しいレストランを試す(探索)
def epsilon_greedy(restaurants, ratings, epsilon=0.1):
if random.random() < epsilon:
return random.choice(restaurants) # 探索
else:
return restaurants[np.argmax(ratings)] # 利用
このような効果は:
- 探索は完全にランダムで、既知の非常に悪いレストランを繰り返し訪問する可能性があります
- 探索の割合は固定されており、時間とともに調整されません
- 訪問回数の影響を考慮していません
UCB 戦略:よりスマートな選択バランス#
UCB 公式がレストラン選択において意味することは次のとおりです:
例えば、100 日目の二つのレストランの状況を考えてみましょう:
A レストラン:
- 50 回訪問、平均スコア 4.5
- UCB スコア =
B レストラン:
- 5 回訪問、平均スコア 4.0
- UCB スコア =
B レストランは平均スコアが低いですが、訪問回数が少ないため不確実性が高く、UCB はより高い探索報酬を与えます。
コード実装:
class Restaurant:
def __init__(self, name):
self.name = name
self.total_rating = 0
self.visits = 0
def ucb_choice(restaurants, total_days, c=2):
# 各レストランを少なくとも一度訪問することを保証
for r in restaurants:
if r.visits == 0:
return r
# UCB公式を使用してレストランを選択
scores = []
for r in restaurants:
avg_rating = r.total_rating / r.visits
exploration_term = c * math.sqrt(math.log(total_days) / r.visits)
ucb_score = avg_rating + exploration_term
scores.append(ucb_score)
return restaurants[np.argmax(scores)]
なぜ UCB が優れているのか?#
-
適応的探索
- 訪問回数が少ないレストランはより高い探索報酬を得ます
- 総日数が増えるにつれて、探索項は徐々に減少し、より良い利用を行います
-
時間投資のバランス
- 明らかに悪いレストランにあまり多くの時間を浪費しません
- 潜在能力が近いレストラン間で訪問回数を合理的に分配します
-
理論的保証
- Regret Bound(最適選択とのギャップ)は時間とともに対数的に増加します
- 300 日の探索は、最高のレストランを見つけるのに十分です
MCTS に戻りましょう:
なぜ MCTS はこれほど強力なのか?#
- 組み合わせ爆発の効率的な処理: MCTS は Minimax のようにすべての可能性を網羅的に探索する必要はなく、最も有望な枝に集中することで、分岐因子が巨大な問題を効率的に処理できます。
- 適応的検索: MCTS はその検索戦略を動的に調整し、より有望な領域により多くのリソースを割り当てることで、より迅速に良い解決策を見つけます。
- 探索と利用のバランス: UCB 公式を通じて、MCTS は新しい可能性を探索することと、既知の良い選択を利用することの間でバランスを取ります。局所最適に陥ることを避けます。
- 特定の領域知識を必要としない: MCTS は特定の領域の専門知識に依存せず、ゲームのルールとシミュレーション結果に基づいて学習するため、広範な適用性を持ちます。
- いつでも停止可能: MCTS は「いつでも」アルゴリズムであり、いつでも中断して現在の最良の解決策を返すことができ、リアルタイムアプリケーションにとって重要です。
AlphaZero:MCTS から自己進化へ#
AlphaZero は DeepMind が AlphaGo の後に発表した汎用強化学習アルゴリズムで、人間の棋譜を使用せずに自己対戦(Self-Play)を通じてゼロから学び、最終的に専門レベルを超えることができます。
AlphaZero は従来の MCTS を改良し、探索を指導するために神経ネットワークを導入しました:
- 戦略事前:神経ネットワークを使用して各アクションの事前確率を予測し、探索をより効率的にします。
- 価値評価:葉ノードで、神経ネットワークの価値予測を使用してランダムシミュレーションを置き換え、計算コストを削減します。
以下では、五目並べを例に、こうした自己学習が可能な AI を実装します。ここでは、schinger/alphazero.pyの優れた実装を学び、参考にします。
ゲーム環境の設計#
AlphaZero を実装する前に、まずゲーム環境を定義する必要があります。
ゲームインターフェースの定義#
AlphaZero では、神経ネットワークが現在の盤面状態を受け取り、各アクションの確率を表す戦略ベクトル と、現在のプレイヤーの勝率予測を表すスカラー値 を出力します。
MCTS と神経ネットワークがゲームと一般的に相互作用できるように、一貫したゲームインターフェースを定義する必要があります。
class GomokuGame:
def __init__(self, n=15):
self.n = n # 盤面サイズ、デフォルトは15x15
def getInitBoard(self):
"""
初期の盤面状態を返します。すべての位置が空です。
"""
b = Board(self.n)
return np.array(b.pieces)
def getBoardSize(self):
"""
盤面のサイズを返します。すなわち (n, n)。
"""
return (self.n, self.n)
def getActionSize(self):
"""
アクションの総数を返します。ここでは n * n です。各マスがアクションとなる可能性があります。
"""
return self.n * self.n
def getNextState(self, board, player, action):
"""
アクションを実行し、次の盤面状態と次のプレイヤーを返します。
パラメータ:
- board: 現在の盤面状態
- player: 現在のプレイヤー(1または-1)
- action: 現在のアクション、0 ~ n*n-1
戻り値:
- (next_board, next_player): アクション実行後の盤面と次のプレイヤー
"""
b = Board(self.n)
b.pieces = np.copy(board)
# アクションを座標 (x, y) に変換
move = (action // self.n, action % self.n)
b.execute_move(move, player)
return (b.pieces, -player)
- 統一されたインターフェース:これにより、MCTS と神経ネットワークが異なるゲームで再利用でき、ゲームの具体的なロジックを実装するだけで済みます。
- 盤面表現:二次元配列を使用して盤面を表現し、処理と可視化を容易にします。
コアアルゴリズムの実装#
双頭神経ネットワーク#
ネットワーク構造#
AlphaZero では、戦略(policy)と価値(value)を同時に予測するために、統一された双頭神経ネットワークを使用します。この神経ネットワークは、現在の盤面状態を受け取り、出力を計算します:
• :戦略ヘッドの出力確率分布で、状態 における各可能アクションの選択確率を示します。この戦略ヘッドは MCTS 検索と組み合わせて、より強力な意思決定を生成します。
• :価値ヘッドは、現在の盤面状態 の予測値(最終的な勝利の確率)を出力します。
import torch
import torch.nn as nn
import torch.nn.functional as F
class AlphaZeroNNet(nn.Module):
def __init__(self, game, args):
"""
パラメータ:
- game: ゲームオブジェクト、盤面サイズやアクション空間サイズなどの情報を提供します。
- args: ネットワーク構造のパラメータ(チャンネル数、ドロップアウト確率など)を含みます。
"""
super(AlphaZeroNNet, self).__init__()
self.board_x, self.board_y = game.getBoardSize() # 盤面サイズ
self.action_size = game.getActionSize() # アクション空間サイズ
self.args = args
# 畳み込み層ブロック
self.conv_layers = nn.Sequential(
nn.Conv2d(1, args.num_channels, kernel_size=3, padding=1),
nn.BatchNorm2d(args.num_channels),
nn.ReLU(),
nn.Conv2d(args.num_channels, args.num_channels, kernel_size=3, padding=1),
nn.BatchNorm2d(args.num_channels),
nn.ReLU(),
nn.Conv2d(args.num_channels, args.num_channels, kernel_size=3),
nn.BatchNorm2d(args.num_channels),
nn.ReLU(),
nn.Conv2d(args.num_channels, args.num_channels, kernel_size=3),
nn.BatchNorm2d(args.num_channels),
nn.ReLU(),
)
# 畳み込み層出力のサイズを計算
conv_output_size = self._get_conv_output_size()
# 全結合層ブロック
self.fc_layers = nn.Sequential(
nn.Linear(conv_output_size, 1024),
nn.BatchNorm1d(1024),
nn.ReLU(),
nn.Dropout(args.dropout),
nn.Linear(1024, 512),
nn.BatchNorm1d(512),
nn.ReLU(),
nn.Dropout(args.dropout),
)
# 戦略ヘッド:各アクションの対数確率を出力
self.policy_head = nn.Linear(512, self.action_size)
# 価値ヘッド:現在の状態の価値推定を出力
self.value_head = nn.Linear(512, 1)
def _get_conv_output_size(self):
"""
畳み込み層出力の特徴数を計算します。
"""
dummy_input = torch.zeros(1, 1, self.board_x, self.board_y)
output_feat = self.conv_layers(dummy_input)
n_size = output_feat.view(1, -1).size(1)
return n_size
def forward(self, s):
"""
前向き伝播関数。
パラメータ:
- s: 入力の盤面状態、形状は (batch_size, board_x, board_y)。
戻り値:
- log_policies: 戦略の対数確率、形状は (batch_size, action_size)。
- values: 価値推定、形状は (batch_size, 1)。
"""
# 入力形状調整
s = s.view(-1, 1, self.board_x, self.board_y) # (batch_size, 1, board_x, board_y)
# 畳み込み層で特徴を抽出
s = self.conv_layers(s) # (batch_size, num_channels, new_board_x, new_board_y)
# 畳み込み層出力を平坦化
s = s.view(s.size(0), -1) # (batch_size, conv_output_size)
# 全結合層で高次特徴を抽出
s = self.fc_layers(s) # (batch_size, 512)
# 戦略ヘッド出力
policies = self.policy_head(s) # (batch_size, action_size)
log_policies = F.log_softmax(policies, dim=1)
# 価値ヘッド出力
values = self.value_head(s) # (batch_size, 1)
values = torch.tanh(values) # 価値を [-1, 1] に制限
return log_policies, values
- 畳み込み層で特徴を抽出:複数の畳み込み層を通じて、盤面の空間的特徴を抽出します。
- 全結合層出力:特徴を平坦化し、全結合層を通じて、戦略と価値をそれぞれ出力します。
- 活性化関数:
- 戦略出力は
log_softmax
を使用し、後続の交差エントロピー損失計算を容易にします。 - 価値出力は
tanh
を使用し、値を の範囲に制限します。
- 戦略出力は
損失関数#
トレーニング目標は、以下の損失関数を通じてネットワークの戦略と価値評価能力を同時にトレーニングすることによって定義されます:
• :価値損失で、ネットワーク出力の が対局結果 (勝利は +1、敗北は -1、引き分けは 0)にできるだけ近くなるようにします。
• :戦略損失で、交差エントロピーを通じて、ネットワーク出力の戦略 が MCTS から得られた最終戦略 にできるだけ一致するようにします。
• :L2 正則化項で、過学習を防ぎ、モデルパラメータ の重みスケールを適切に保ちます。
MCTS クラス#
class MCTS:
def __init__(self, game, nnet, args):
self.game = game # ゲーム環境
self.nnet = nnet # 神経ネットワーク
self.args = args # パラメータ
self.Qsa = {} # Q値を保存:Q(s,a)
self.Nsa = {} # 辺の訪問回数を保存:N(s,a)
self.Ns = {} # ノードの訪問回数を保存:N(s)
self.Ps = {} # 戦略事前を保存:P(s,a)
self.Es = {} # ゲーム終了情報を保存:E(s)
self.Vs = {} # 合法アクションを保存:V(s)
ここでの一つのハイライトはキャッシュメカニズムを使用していることです:辞書を使用して計算結果をキャッシュし、重複計算を避け、効率を向上させます。
有効アクションマスク#
多くのゲームでは、特定の状態でいくつかのアクションが無効です。例えば、五目並べでは、特定の位置がすでに占有されている場合、その位置に置くことは無効です。したがって、合法アクションのみを考慮することは、アルゴリズムの正確性と効率にとって重要です。これは ** 有効アクションマスク(valid mask)** を使用して実現されます。
search
メソッド内で、葉ノードに到達し、神経ネットワークを使用して予測を行う必要があるとき、神経ネットワーク出力の戦略に対して処理を行い、有効マスクを使用して無効アクションを遮断します。
if s not in self.Ps: # 戦略事前保存 P(s,a) に存在しない場合
# 葉ノード、神経ネットワーク出力の戦略に基づいて拡張
self.Ps[s], v = self.nnet.predict(canonicalBoard)
valids = self.game.getValidMoves(canonicalBoard, 1) # 合法アクションのマスクを取得、1 は合法
self.Ps[s] = self.Ps[s] * valids # 無効アクションをマスク
sum_Ps_s = np.sum(self.Ps[s])
if sum_Ps_s > 0:
self.Ps[s] /= sum_Ps_s # 戦略確率を正規化
else:
# すべてのアクションが遮断された場合、均等分布を行う
self.Ps[s] = self.Ps[s] + valids
self.Ps[s] /= np.sum(self.Ps[s])
self.Vs[s] = valids # 合法アクションのマスクを保存
self.Ns[s] = 0 # 状態訪問回数を初期化
return v
PUCT#
UCB 公式の一種で、MCTS 専用です。
AlphaZero では、MCTS は神経ネットワークの出力を使用して探索方向を指導します。検索プロセス中、神経ネットワークは改良された上信頼限界公式PUCT(Polynomial Upper Confidence Trees for Trees)を使用してアクションを選択します。事前確率 を導入します。これにより、MCTS は外部知識(戦略ネットワーク)を利用して探索を指導できます:
ここで:
- は状態 でアクション を選択したときの平均価値です。
- は神経ネットワークによって提供される事前確率です。
- と はそれぞれ状態 と辺 が訪問された回数です。
- は探索の程度を制御する定数です。後の探索項と組み合わせることで、訪問回数が少なく、事前確率が高いアクションを選択することを奨励します。
cur_best = -float('inf') # 現在の最大U値を記録
best_act = -1 # 最大U値を持つアクションを記録
# すべての可能なアクションを遍歴
for a in range(self.game.getActionSize()):
if valids[a]:
# 合法なアクションに対して、PUCT値を計算
if (s, a) in self.Qsa:
# 以前に訪問した (s, a) の場合、計算済みのQ値と訪問回数を使用
u = self.Qsa[(s, a)] + self.args.cpuct * self.Ps[s][a] * \
math.sqrt(self.Ns[s]) / (1 + self.Nsa[(s, a)])
else:
# 未訪問のノードの場合、Q値は0で、探索を奨励
u = self.args.cpuct * self.Ps[s][a] * math.sqrt(self.Ns[s] + 1e-8) # 未訪問のアクションにより高いU値を与え、ゼロ除算を避けます
# 最大U値を持つアクションを選択
if u > cur_best:
cur_best = u
best_act = a
探索と利用のバランス#
-
未訪問のノードは
N(s, a) = 0
であるため、分母が小さくなり、探索項が大きくなり、新しいアクションを探索することを奨励します。 -
Q (s, a) はアクションの価値に対する現在の推定を反映し、利用を表します。
価値の反転#
プレイヤー の視点から見ると、より高い価値を得ることはプレイヤー にとってはより低い価値を意味します。その逆もまた然りです。したがって、検索と価値評価の全プロセスにおいて、価値が現在のプレイヤーの利益最大化を反映することを保証し、視点の一貫性を保ちます。
コード内では、この実装は次の呼び出しに体現されています:
v = -self.search(next_s)
Search 関数#
def search(self, canonicalBoard):
"""
一回のMCTS検索を実行します。
パラメータ:
- canonicalBoard: 現在のプレイヤーの盤面状態(標準形式)
戻り値:
- v: 現在のノードの価値評価
"""
s = self.game.stringRepresentation(canonicalBoard)
if s not in self.Es:
self.Es[s] = self.game.getGameEnded(canonicalBoard, 1)
if self.Es[s] is not None:
# ゲームが終了した場合、結果を返す
return self.Es[s]
'''
有効マスク & 葉ノード拡張
'''
valids = self.Vs[s]
'''
PUCT公式を使用して最適アクションを選択
'''
a = best_act
next_s, next_player = self.game.getNextState(canonicalBoard, 1, a)
next_s = self.game.getCanonicalForm(next_s, next_player)
# 次の状態の価値を反転
v = -self.search(next_s)
# Q値と訪問回数を更新
if (s, a) in self.Qsa:
self.Qsa[(s, a)] = (self.Nsa[(s, a)] * self.Qsa[(s, a)] + v) / (self.Nsa[(s, a)] + 1)
self.Nsa[(s, a)] += 1
else:
self.Qsa[(s, a)] = v
self.Nsa[(s, a)] = 1
self.Ns[s] += 1
return v
- 再帰的に深く検索し、将来の可能性をシミュレートします。
- 葉ノードで神経ネットワークを使用して予測し、検索速度を向上させます。
- プレイヤーが交互に行動するため、次の状態の価値を反転させます。
自己対戦#
人間の対局データを必要としない囲碁アルゴリズムは、自己対戦のみでデータを取得してトレーニングします。
自己対戦は、各手を打つたびに、指定された回数の MCTS を実行して戦略 を選択します。黒と白のプレイヤーが交互に行動し、ゲームが終了するまで続けます。自己対戦は大量のトレーニングデータを生成し、神経ネットワークがより良い戦略と価値推定を学ぶのを助けます。
「自己対戦を実行する」実装#
まず、自己対戦のコア関数 executeEpisode
を見てみましょう。これは、ゲーム全体を実行し、終了までトレーニングデータを収集します。
def executeEpisode(self):
"""
一局の自己対戦を実行し、トレーニングサンプルのリストを返します。
戻り値:
trainExamples: 複数の (canonicalBoard, pi, v) タプルを含むリスト。
"""
trainExamples = []
board = self.game.getInitBoard() # 盤面を初期化
self.curPlayer = 1 # 現在のプレイヤー(1または-1)
episodeStep = 0 # ステップ数を記録
while True: # ゲームが終了するまで
episodeStep += 1
canonicalBoard = self.game.getCanonicalForm(board, self.curPlayer)
temp = int(episodeStep < self.args.tempThreshold) # 温度パラメータ
pi = self.mcts.getActionProb(canonicalBoard, temp=temp) # MCTSを使用してアクションの確率分布を取得(戦略)
sym = self.game.getSymmetries(canonicalBoard, pi) # データ拡張(対称性)
for b, p in sym:
trainExamples.append([b, self.curPlayer, p, None])
action = np.random.choice(len(pi), p=pi) # 戦略確率に従ってアクションを選択
board, self.curPlayer = self.game.getNextState(board, self.curPlayer, action) # アクションを実行し、盤面と現在のプレイヤーを更新します。
r = self.game.getGameEnded(board, self.curPlayer) # ゲームが終了したかどうかを確認
if r is not None:
# 各トレーニングサンプルに最終的な価値を割り当てます。勝利したプレイヤーには +1、敗北したプレイヤーには -1、引き分けには 0
return [(x[0], x[2], r * ((-1) (x[1] != self.curPlayer))) for x in trainExamples]
-
温度パラメータ:戦略の探索性を制御します。初期は 1 に設定し、探索を奨励します。後期は 0 に設定し、最適戦略を選択する傾向があります。
-
データ拡張:五目並べでは、盤面の回転や反転がゲームの本質を変えません。これらの対称性を利用して、等価な盤面と戦略を生成し、トレーニングデータを増やしてモデルの汎化能力を向上させます。
ゲームが終了した場合、対局記録に基づいてトレーニングサンプルを生成し、trainExamples
を返します。
自己対戦のメインループ#
自己対戦は単に一局のゲームを実行するだけではなく、何度も繰り返し自己対戦を行い、モデルを更新する必要があります。
def learn(self):
"""
複数回の自己対戦とモデル更新を行います。
"""
for i in range(1, self.args.numIters + 1):
log.info(f"Starting Iter #{i} ...")
# 今回の反復のトレーニングサンプルを保存
iterationTrainExamples = deque([], maxlen=self.args.maxlenOfQueue)
# 指定回数の自己対戦を行う
for _ in tqdm(range(self.args.numEps), desc="Self Play"):
self.mcts = MCTS(self.game, self.nnet, self.args) # MCTSをリセット
iterationTrainExamples += self.executeEpisode()
# トレーニングサンプルを保存
self.trainExamplesHistory.append(iterationTrainExamples)
# 固定長のトレーニングサンプル履歴を維持
if len(self.trainExamplesHistory) > self.args.numItersForTrainExamplesHistory:
log.warning(f"Removing the oldest entry in trainExamples.")
self.trainExamplesHistory.pop(0)
# すべてのトレーニングサンプルを統合し、シャッフルします
trainExamples = []
for e in self.trainExamplesHistory:
trainExamples.extend(e)
shuffle(trainExamples)
# 神経ネットワークをトレーニング
self.nnet.save_checkpoint(folder=self.args.checkpoint, filename="temp.pth.tar")
self.pnet.load_checkpoint(folder=self.args.checkpoint, filename="temp.pth.tar")
pmcts = MCTS(self.game, self.pnet, self.args)
self.nnet.train(trainExamples)
nmcts = MCTS(self.game, self.nnet, self.args)
- 履歴の長さが設定された閾値を超えた場合、最古のサンプルを削除し、モデルが古いデータに過剰適合しないようにします。
- 十分なトレーニングデータが収集されたら、
self.nnet.train(trainExamples)
を使用してモデルをトレーニングします。
フィルタリングメカニズム#
モデルの棋力が常に向上することを保証するために、AlphaGo Zero は新旧モデル対局のフィルタリングメカニズムを導入しました:
- 各トレーニングラウンドの後、新モデルと旧モデルを対局させます。
- 新モデルの勝率が設定された閾値(例えば 55%)を超えた場合、新モデルを受け入れ、次の自己対戦ラウンドに進みます。そうでない場合、旧モデルの重みを復元し、旧モデルを基にデータを再生成してトレーニングを続け、無意味な退化や過剰適合の問題を避けます。
# 新モデルを評価
log.info("PITTING AGAINST PREVIOUS VERSION")
arena = game.Arena(lambda x: np.argmax(pmcts.getActionProb(x, temp=0)),
lambda x: np.argmax(nmcts.getActionProb(x, temp=0)),
self.game)
pwins, nwins, draws = arena.playGames(self.args.arenaCompare)
log.info(f"NEW/PREV WINS : {nwins} / {pwins} ; DRAWS : {draws}")
if pwins + nwins == 0 or float(nwins) / (pwins + nwins) < self.args.updateThreshold:
log.info("REJECTING NEW MODEL")
self.nnet.load_checkpoint(folder=self.args.checkpoint, filename="temp.pth.tar")
else:
log.info("ACCEPTING NEW MODEL")
self.nnet.save_checkpoint(folder=self.args.checkpoint, filename="best.pth.tar")
MCTS から戦略を取得#
自己対戦の過程で、MCTS を使用してアクションを選択します。getActionProb
関数は、MCTS から戦略確率分布を取得します。
def getActionProb(self, canonicalBoard, temp=1):
"""
指定回数のMCTS検索を実行し、アクションの確率分布を返します。
パラメータ:
canonicalBoard: 現在の標準化された盤面状態
temp: 温度パラメータ
戻り値:
probs: アクションの確率分布
"""
# 指定回数のMCTS検索を実行
for _ in range(self.args.numMCTSSims):
self.search(canonicalBoard)
s = self.game.stringRepresentation(canonicalBoard)
# 各アクションが訪問された回数 N(s, a) を記録
counts = [self.Nsa[(s, a)] if (s, a) in self.Nsa else 0
for a in range(self.game.getActionSize())]
if temp == 0:
bestAs = np.array(np.argwhere(counts == np.max(counts))).flatten()
bestA = np.random.choice(bestAs)
probs = [0] * len(counts)
probs[bestA] = 1
return probs
counts = [x (1. / temp) for x in counts]
counts_sum = float(sum(counts))
probs = [x / counts_sum for x in counts]
return probs
訪問回数 N(s, a)
は、MCTS がアクションに対して持つ信頼を反映します。
戦略確率を計算:
temp = 0
の場合:- 最も訪問回数が多いアクションを選択し、確率を 1 にします。
- 戦略の決定性を保証し、通常はゲームの後期または評価段階で使用されます。
temp > 0
の場合:- 訪問回数に温度変換を適用します:
counts = [x (1. / temp) for x in counts]
。 - 温度が高いほど、戦略は均等分布に近づき、探索性が強くなります。
- 訪問回数に温度変換を適用します:
トレーニングの経過#
MCTS シミュレーション回数:400、cpuct:1.0、トレーニング 22 ラウンド:
この時点で、モデルは五目並べのルールを学び、初級レベルの AI に達しました。盤面の端でこっそり勝つことができます。
1cycle 学習率調整の追加#
動的に学習率を調整することで、モデルの収束速度と汎化能力を向上させることができます。
- 第一段階(前半サイクル):学習率を最小値()から最大値()に徐々に増加させます。
- 第二段階(後半サイクル):学習率を最大値から徐々に最小値に減少させます。
class NNetWrapper:
def __init__(self, game, args):
...
self.optimizer = optim.Adam(self.nnet.parameters(), lr=args.max_lr)
# 1cycle学習率パラメータ
self.total_steps = args.numIters * args.epochs * (args.maxlenOfQueue // args.batch_size)
self.current_step = 0
def get_learning_rate(self):
"""1cycle学習率戦略を実装します"""
if self.current_step >= self.total_steps:
return self.args.min_lr
half_cycle = self.total_steps // 2
if self.current_step <= half_cycle:
# 第一段階:min_lrからmax_lrに増加
phase = self.current_step / half_cycle
lr = self.args.min_lr + (self.args.max_lr - self.args.min_lr) * phase
else:
# 第二段階:max_lrからmin_lrに減少
phase = (self.current_step - half_cycle) / half_cycle
lr = self.args.max_lr - (self.args.max_lr - self.args.min_lr) * phase
return lr
勾配クリッピングの追加#
PPO を復習できます
勾配爆発を防ぎ、トレーニングプロセスを安定させるために、逆伝播後に勾配クリッピングを追加します。
あるパラメータの勾配ベクトルを とし、その現在の L2 ノルムを とします:
- の場合、 はそのまま保持されます。
- の場合、 は にスケーリングされます。
これにより:
def train(self, examples):
...
for epoch in range(self.args.epochs):
...
for _ in t:
...
# 損失を計算し、逆伝播
total_loss.backward()
# 勾配クリッピング
if self.args.grad_clip:
torch.nn.utils.clip_grad_norm_(self.nnet.parameters(), self.args.grad_clip)
# パラメータを更新
self.optimizer.step()
...
勾配クリッピングは、勾配のノルムを指定された閾値内に制限し、勾配が大きすぎてトレーニングが不安定になるのを防ぎます。各パラメータ更新の前に勾配をクリッピングすることで、トレーニングプロセスの堅牢性を確保し、モデルの収束を加速します。
結語#
最終的なコードとデモは、以下のリポジトリで見つけることができます。モデルの重みは、ドキュメント内の huggingface リンクリポジトリで見つけることができます。
これらの改善を行った後、MCTS シミュレーションを 2000 回に増やし、cput を 4.0 にして探索傾向を増し、22 ラウンドトレーニングを行いました(単一の 3090 Ti で 4 日間実行)。この時点で、モデルは徐々に普通の難易度にアップグレードされました(50 回の MCTS シミュレーション設定では、前期の圧迫感はまだ良好ですが、中後期には相手の勝ち点に気づかないことがあります)。
もし推論時の MCTS シミュレーション回数を増やすことができれば、困難な AI のレベルに達することができます。興味があれば、勝てるかどうか試してみてください。