相手が賢くなると、環境が変わってしまう — 強化学習を対戦ゲームに持ち込むとき

要約

強化学習の基礎(TD法、モンテカルロ法、DQN)を学んだ直後に、対戦ゲームを作ってみようとして ぶつかった問いの記録。

教科書で学んだ世界観は「エージェントと環境の2者」だった。エージェントが行動すると、環境が報酬と 次の状態を返す。FrozenLake も CartPole も、登場人物は自分だけ。ところが五目並べのような対戦ゲームでは 相手がいる。相手の動きを環境の一部とみなせばよいのか? しかしそれだと、環境の振る舞いが 「相手の賢さ」に依存してしまう。相手が学習して強くなれば、環境そのものが変わってしまう—— これは全然別のパラダイムが要るのではないか、と考えた。

答えは「その直感は正しい」だった。相手込みで環境とみなすのが最もシンプルで、エージェントから見た インターフェースは FrozenLake と全く同じになる。ただし相手が成長すると、同じ状態・行動に対する 報酬の分布が時間とともに変わる。これは非定常環境問題と呼ばれる、名前のついた問題だった。 対処は難易度順に、相手を固定する/カリキュラム学習(段階的に相手を強化)/Self-play(AlphaGo 方式)。

そして実際に実装して動かしたら、学習が収束しなかった。勝率は 5000 エピソード回しても 37% 前後で 頭打ち。「そんなものでしょうか?」と聞いたら、「そんなものではなく、設計上の問題が複数重なっている」 という診断が返ってきた。ε の減衰が速すぎる、報酬が sparse すぎる、ネットワークが浅い、 バッファが小さい。修正したら 500 エピソードで勝率 80% を超えた。

理論を学ぶ → 実装する → 失敗する → 診断する → 直す、という一周が丸ごと入っている。

要点

1. 教科書の世界観と、対戦ゲームのズレ

  • 教科書(FrozenLake、CartPole)の世界観:登場人物は「エージェント」と「環境」の2者。 行動すると、環境が報酬と次の状態を返す
  • 対戦ゲームには相手がいる。相手を環境の一部とみなせばよいのか?
  • しかしそれだと、環境の振る舞いが「相手の賢さ」に依存する。相手が学習して強くなれば、 環境そのものが変わってしまう

2. 答え:相手込みで環境とみなしてよい。ただしそれは「非定常環境問題」という名前を持つ

  • エージェントから見たインターフェースは FrozenLake と全く同じ。 環境の step 処理の中に相手の着手が入るだけで、DQN の本体は変えなくていい
  • ただし相手が成長すると、同じ状態・行動に対する報酬の分布が時間とともに変わる。 通常の DQN は環境が定常であることを前提にしている

3. 非定常性への対処(難易度順)

手法 中身 特徴
相手を固定 ランダムAI や固定強度のルールベース 環境が定常になり、通常のDQNがそのまま使える。ただし「ランダム相手に強くなっても人間には弱い」
カリキュラム学習 前フェーズで学習したAIを次の相手にする 相手の強さが徐々に上がる。実装の現実解
Self-play 常に「現在の自分自身」と対戦(AlphaGo 方式) 理論的には最強だが、相手が同時に変わり続けるので学習が不安定

4. 実装して失敗した — 収束しない原因は4つ重なっていた

5000 エピソード回して勝率 37% で頭打ち。「そんなもの」ではなく設計上の問題だった。

問題 中身 修正
ε の減衰が速すぎる 500ep で ε=0.05(探索ほぼゼロ)。9×9 の状態空間に対して探索不足 0.9995 → 0.99985
報酬が sparse すぎる 1ゲーム数十手なのに、報酬が最後の1手にしかつかない。Q値の伝播が遅い 中間報酬(4連を作る +0.3/相手の4連を阻止 +0.2)
ネットワークが浅い 盤面を flatten して全結合のみ。空間的パターン(石の並び)を捉えられない 本来は CNN 向き(今回は後回し)
バッファ・バッチが小さい capacity 10000, batch 64 50000, 128 に拡大

→ 修正後、500 エピソードで勝率 80% 超(修正前は 5000ep で 37%)

5. 学んだこと

  • 報酬が最後にしか来ない設計は、それだけで学習を殺す。中間報酬で「この手は良かった」という信号を 毎ステップ届ける必要がある
  • 探索(ε)を早く切り上げると、状態空間が広い問題では致命的
  • 「収束しない」を「そんなもの」で済ませてはいけない。診断すれば原因は特定できる

軌跡(対話そのまま)

問い — 何を題材に強化学習を試すか

私は強化学習を勉強しています。今、TD法、モンテカルロ法、とかを勉強したところです。自分で何かAIを強化学習させてみたいのですが、何か良い題材はありますかね。

強化学習の実践題材、いくつかのレベル感で紹介しますね。

初級(環境構築が簡単)

OpenAI Gymnasium(旧Gym)の古典的な問題

  • CartPole — 棒を倒さないようにカートを動かす。報酬設計がシンプルで、TD法やモンテカルロ法との対応が見やすい。最初の一歩として最もおすすめ
  • FrozenLake — 格子世界での経路探索。Q学習の理解に最適で、状態・行動空間が小さく可視化しやすい
  • MountainCar — 谷から車を脱出させる。報酬がスパースで「探索」の難しさを体感できる

中級(少し歯ごたえあり)

  • Blackjack(ブラックジャック) — モンテカルロ法との相性が特に良い。エピソードが自然に区切られる
  • Taxi-v3 — 乗客の送迎タスク。Q-tableで解ける規模感でありながら、状態の設計が学べる
  • LunarLander — 月面着陸船の制御。DQNへのステップアップに丁度いい

個人的におすすめの流れ

FrozenLake でQ学習を実装

CartPole でTD法・方策勾配を比較

Blackjack でモンテカルロ法を丁寧に実装

環境は pip install gymnasium で入りますし、Pythonで数十行から実装できます。

今どのあたりの実装レベルを想定していますか?(たとえば「Q-tableを手書きしたい」なのか「ライブラリを使ってサクッと動かしたい」なのかで、最適な題材が変わります)

オセロとか、5目並べとか、対戦できるタイプのって作れますか?強化学習、もしくはDQNで、AIが学習して強くなったことを実感したいと思っています

作れます!ただ、題材選びが重要で、難易度差がかなりあります。

難易度比較

題材状態空間実装難易度学習で強くなるのを実感しやすいか
三目並べ(○×)約5,000★☆☆△(すぐ完璧になりすぎる)
五目並べ(小盤面)中程度★★☆
オセロ(6×6)大きい★★☆
オセロ(8×8)非常に大きい★★★△(学習が重すぎる)
将棋・チェス膨大★★★✗(DQNでは無理)

最もおすすめは「6×6オセロ」か「小盤面の五目並べ(9×9)」です。


なぜDQNで対戦ゲームが難しいか

通常のDQNは「環境が固定」を前提にしていますが、対戦ゲームは相手も変化するので工夫が必要です。

主な手法:

  • Self-play — 自分自身と対戦させて学習(AlphaGoもこれ)
  • Fixed opponent — ランダムAIや一定強さの相手と対戦
  • Experience Replay — 過去の自分と対戦

具体的な構成イメージ(6×6オセロの場合)

# 状態:6×6の盤面 → 36次元の入力
# 行動:置ける場所を選ぶ(最大36択)
# 報酬:勝ち +1 / 負け -1 / 引き分け 0

# ネットワーク構成例
Input(36) → Dense(128) → Dense(128) → Output(36)

学習の流れ:

  1. まずランダムAI相手に学習
  2. ある程度強くなったらSelf-playに切り替え
  3. 人間(自分)と対戦して強さを実感

実際に作ってみますか?どちらの題材が好みか教えてもらえれば、コードの雛形を作ります。五目並べの方がルールがシンプルで実装しやすく、オセロの方が「AIらしい戦略」を学習しやすいです。

核心の問い — 対戦ゲームでは、相手を環境とみなすのか

ほどほどの学習規模であり、かつ実装が簡単ということで、

「五目並べ(小盤面)」 を作ってみたいと思います。

ただ、私は強化学習の基礎を勉強しただけで、

単にfrozenlakeのような「自分が盤面を移動するだけ」であったり、

cartpoleのような、「自分が動くだけ」のような、

登場人物が自分だけのテーマについて、

TD法、モンテカルロ法、DQNなどを勉強した程度でした。

つまり、登場人物としてエージェントと環境の2つがあり、

今の状態に応じて、登場人物が行動を起こすと、

環境がそれに対する報酬と、次の状態を返却する、という世界観でした。

一方で、こうした五目並べなどの対戦ゲームのような形式のタスクは実装したことがないのですが、

どのような方針で実装する形になるでしょうか。

「相手の動き」を環境側としてみなすような考え方になりますか?

その場合、環境側の動きは必ずしも固定されず、「相手の賢さ」にも依存するように思いますが、

この問題をどう処理するでしょうか。

もしくは全然別のパラダイムでとらえる必要があるでしょうか。

コードを生成する前に、この前提知識について教えてください。

非常に良い着眼点です。順番に整理しましょう。


基本的な答え:「相手込みで環境とみなす」

あなたの直感は正しいです。相手の動きを環境の一部として吸収するのが最もシンプルな考え方です。

つまり、エージェントから見た世界観はこうなります:

自分が行動を選ぶ

環境(=盤面 + 相手の応手)が次の状態と報酬を返す

自分が次の行動を選ぶ…

FrozenLakeと構造は全く同じです。ただし「環境のステップ処理」の中に相手の着手が含まれているというだけです。


「相手が賢くなると環境が変わる」問題

ここがご指摘の核心ですね。これは非定常環境問題と呼ばれます。

通常のDQNは環境が固定(定常)であることを前提にしています。ところが相手が強化学習で成長していくと、同じ状態・行動に対する報酬の分布が時間とともに変わってしまいます。

この問題への対処法がいくつかあり、難易度順に以下のようになります。

① 相手を固定する(最も簡単)

相手はランダムAI、もしくは強さが固定のルールベースAIにする。環境が定常になるので、通常のDQNがそのまま使えます。

欠点として「ランダム相手に強くなっても人間には弱い」ということが起きます。

② カリキュラム学習(中程度)

段階的に相手を強化していきます。

Phase 1: ランダム相手で学習
Phase 2: Phase1で学習したAIを相手に学習
Phase 3: Phase2で学習したAIを相手に学習…

相手の強さが徐々に上がるので、エージェントも着実に成長します。今回の実装にはこれが最もおすすめです。

③ Self-play(本格的)

エージェントが常に「現在の自分自身」と対戦します。AlphaGoがこの方式です。

理論的には最も強くなりますが、学習が不安定になりやすいという問題があります。同じ学習率・探索率でも、相手が同時に変わり続けるため、うまく収束しないことがあります。Experience Replay(過去の自分)と組み合わせることで安定しやすくなります。


もう一つの考え方:Two-agent視点

上記はすべて「片方だけ学習する」前提でした。もう一つのパラダイムとして、両方のエージェントを同時に学習させる方法(Multi-Agent Reinforcement Learning, MARL)があります。

ただしこれは理論的に複雑になるので、今回は「片方だけ学習、相手は固定 or 段階的強化」で十分です。


今回の実装方針まとめ

盤面環境クラス
├── step(action) → 次の状態、報酬、終了フラグを返す
│     ├── エージェントの着手を盤面に反映
│     ├── 勝敗チェック
│     ├── 相手(ランダムAI)の着手を盤面に反映  ← ここが追加点
│     └── 勝敗チェック
└── reset() → 初期盤面を返す

DQNエージェント
└── FrozenLakeと同じ構造でOK

つまりDQNの本体は変えなくていいです。step()の中に相手の手が入るだけで、エージェントから見たインターフェースはFrozenLakeと同一です。


この方針で理解できましたか?問題なければ、コードを一緒に作っていきましょう。最初は「相手=ランダムAI」から始めて、動作確認後にカリキュラム学習に発展させる流れがおすすめです。

実装してみる

わかりました。では、その通り、

最初は「① 相手を固定する(最も簡単)」で作って、

その後「② カリキュラム学習(中程度)」と発展させましょう。

コードの内容は、「ゼロから作るdeeplearning④」の表記をベースとしてください。

まずは「① 相手を固定する(最も簡単)」からお願いします。

「ゼロから作るDeepLearning④」のスタイルを確認してからコードを作ります。 「ゼロから作るDeepLearning④」のスタイルを把握した上で、コードを作成します。同書のスタイルの特徴(NumPyベース、クラス設計、レイヤー抽象化など)に合わせて実装します。 動作確認をします。 問題なく動きました。 動作確認OKです。コードの構成を説明します。


コードの全体構成

1. GomokuEnv       環境クラス
2. QNetwork        ニューラルネット(NumPy手書き)
3. ReplayBuffer    経験再生バッファ
4. DQNAgent        エージェント
5. train()         学習ループ
6. play_vs_human() 対戦モード

重要な設計ポイント

step() の中に相手の手が入る

def step(self, action):
    # エージェントの着手 → 勝敗チェック
    # 相手(ランダム)の着手 → 勝敗チェック
    # next_state, reward, done を返す

エージェントから見ると「行動したら次の状態が返ってくる」だけで、FrozenLakeと全く同じインターフェースです。

② 無効手へのペナルティ すでに石がある場所に置こうとすると reward=-1 でエピソード終了。これがないとε-greedy の探索中に無効手を選び続けてしまいます。

③ 有効手マスク

masked_q[valid_actions] = q_values[valid_actions]
return np.argmax(masked_q)

Q値が最大でも無効手は選べないようにマスクしています。


実行方法

pip install numpy
python3 gomoku_dqn.py

5000エピソード学習後に対戦モードが起動します。学習の進み具合は500エピソードごとに勝率が表示されます。相手がランダムAIなので、学習が進むにつれて勝率が上がっていくのが実感できます(最終的に70〜80%台になるはずです)。

動作確認できたら、次のカリキュラム学習へのステップアップも作ります。

動かしたら収束しなかった — そして診断

学習開始(相手: ランダムAI固定) Episode | 勝率(直近100) | 平均報酬 | ε -------------------------------------------------- 500 | 29.0% | -0.400 | 0.0500 1000 | 17.0% | -0.640 | 0.0500 1500 | 12.0% | -0.760 | 0.0500 2000 | 31.0% | -0.380 | 0.0500 2500 | 28.0% | -0.420 | 0.0500 3000 | 36.0% | -0.230 | 0.0500 3500 | 40.0% | -0.130 | 0.0500 4000 | 47.0% | 0.010 | 0.0500 4500 | 40.0% | -0.150 | 0.0500 5000 | 37.0% | -0.110 | 0.0500

実行してみました。学習が収束しないのですが、そんなものでしょうか?

結果を見ると、いくつか問題があります。「そんなもの」ではなく、設計上の問題が複数重なっています。順番に説明します。


問題の診断

問題① εが早く下がりすぎている

500エピソード時点でε=0.05になっています。9×9の盤面は状態空間が膨大なので、探索が全然足りていません。

現在: epsilon_decay=0.9995 → 500ep付近でε≒0.05に到達
理想: 2000〜3000ep付近でε≒0.05に到達すべき

問題② 報酬が sparse すぎる

現在の報酬設計:

勝ち  → +1
負け  → -1
途中  →  0  ← ゲーム中ずっとこれ

1ゲームが数十手あるのに、報酬が最後の1手にしかつかないため、Q値の伝播が非常に遅いです。

問題③ ネットワークが9×9盤面に対して浅い

現在は盤面をflattenして全結合のみです。盤面の空間的なパターン(連続した石の並び)を学習するには、本来CNNが向いています。ただしNumPy手書きで実装できます。

問題④ バッファ容量が小さい・バッチサイズも小さい

現在: capacity=10000, batch_size=64
改善: capacity=50000, batch_size=128

修正方針

今回は③CNNは後回しにして、①②④の修正で大きく改善できるはずです。

具体的には:

パラメータ変更前変更後
epsilon_decay0.99950.99985
中間報酬なし4連追加で+0.3、相手の4連阻止で+0.2
buffer_capacity1000050000
batch_size64128
num_episodes500010000
修正版を作成します。
動作確認します。
大幅に改善されました。500エピソードで既に勝率70〜90%が出ています。
動作確認の結果を見ると、改善が非常に効いています。
Ep 100:  勝率 80%
Ep 300:  勝率 90%
Ep 500:  勝率 79%

前回の5000ep末尾で37%だったのが、500epで80%超になりました。


今回の修正まとめ

① εの減衰を遅くした(0.9995 → 0.99985

前回は500ep時点でε=0.05(探索ほぼゼロ)になっていました。9×9盤面で状態空間が膨大なのに、ランダム探索が早く終わりすぎていたのが主な原因でした。

② 中間報酬を追加した

自分が4連を作った     → +0.3
相手の4連を阻止した   → +0.2

勝敗だけが報酬だと、1ゲーム数十手の中で最後の1手にしか信号がなく、Q値の伝播がひどく遅くなります。中間報酬を入れることで、「この手は良い手だった」という信号が毎ステップ届くようになります。

③ バッファ・バッチサイズを拡大(10000→50000, 64→128

サンプルの多様性と学習の安定性が上がります。


10000ep回すと最終的に80〜90%台に落ち着くはずです。確認できたら、次はカリキュラム学習(フェーズ学習済みAIを相手にして段階的強化)への発展を作りましょう。

← 一覧へ