Connect系ゲーム(N目並べ)を題材に、DQN(Deep Q-Network)による強化学習を実装・実験するプロジェクトです。
当初はNumPyのみで手書き実装していましたが、学習速度のボトルネック解消のためステージ8でPyTorchに移行しました。NumPy手書きフェーズでDQNの仕組み(順伝播・逆伝播・Adam)を一から理解した上で、実用的な速度を得ています。
- 目的: 強化学習の基礎(DQN)を、Connect系ゲームで手を動かしながら学ぶ
- 最終目標: 初心者の人間より強くなる
- 実装: PyTorch(CPU)。ステージ7まではNumPy手書き
- サブプロジェクト:
- 4moku/: Connect Four(6×7, 4目)— DQN学習11ステージの試行錯誤でDQN単体の限界を確認
- 3moku/: Connect Three(5×5, 3目)— 完了。DQNが人間に勝てるレベルに到達(4,000epで最適戦略獲得)
- alphazero/: AlphaZero方式(MCTS+NN)— 実装完了、本格学習開始
| ステージ | 内容 | 状態 |
|---|---|---|
| 1 | vs ランダムAI | 廃止(理由は後述) |
| 2 | カリキュラム学習(段階的に強い相手) | 廃止(理由は後述) |
| 3 | Self-play + 混合学習 | 廃止(理由は後述) |
| 4 | vs ルールベースAI固定(先手固定) | 廃止(理由は後述) |
| 5 | NoisyRuleBasedカリキュラム + 攻撃優先中間報酬 + プール方式Phase 4 | 廃止(中間報酬過大・next_stateバグ) |
| 6 | 遅延コールバック方式 + 小スケール中間報酬 + カリキュラム再学習 | 廃止(目標高すぎ + プール不要) |
| 7 | シンプルカリキュラム + 小スケール中間報酬 | 廃止(中間報酬の累積で停滞) |
| 8 | カリキュラム + 中間報酬なし(勝敗±1.0のみ) | 廃止(Phase 2で30000ep停滞、防御を学べず) |
| 9 | カリキュラム + 極小中間報酬(MLP) | 廃止(Phase 1突破に32000ep + Phase 2停滞) |
| 10 | CNN + カリキュラム + 中間報酬なし | 廃止(カリキュラムが「方策の上書き」と判明) |
| 11 | CNN + vs ルールベースAI固定 + 中間報酬なし | 実行中 |
# 仮想環境の作成
python -m venv .venv
# 依存パッケージのインストール
.venv\Scripts\pip install numpy flask
.venv\Scripts\pip install torch --index-url https://download.pytorch.org/whl/cpu# ゼロからカリキュラム学習(推奨)
.venv\Scripts\python train.py --episodes 30000
# チェックポイントから再開(バッファ+ε+Adam状態を引き継ぐ)
.venv\Scripts\python train.py --load-path weights/dqn_connect4 --episodes 30000
# バッファを引き継がずに再開
.venv\Scripts\python train.py --load-path weights/dqn_connect4 --no-buffer --episodes 30000完了後、weights/dqn_connect4.npz(重み)と weights/dqn_connect4_checkpoint.npz(バッファ+学習状態)が保存されます。学習ログは weights/train_log.txt にリアルタイム出力されます。
.venv\Scripts\python web/app.pyhttp://localhost:5000 をブラウザで開いてください。
- CPU(赤)が先攻、プレイヤー(橙)が後攻
- DQNはPLAYER1(先攻)として学習しているため、WebUIもそれに合わせた配置になっています
4moku/
├── env/
│ └── connect4_env.py # ゲームロジック(重力・勝利判定・状態表現・中間報酬)
├── agents/
│ ├── base_agent.py # 抽象基底クラス
│ ├── human_agent.py # 人間プレイヤー(WebUI経由)
│ ├── random_agent.py # ランダムAI
│ ├── rule_based_agent.py # ルールベースAI(勝ち手・阻止・中央優先)
│ └── dqn_agent.py # DQNエージェント(NumPy手書き実装)
├── web/
│ ├── app.py # Flask サーバー(CPU先攻・プレイヤー後攻)
│ └── templates/
│ └── index.html # ブラウザUI(アニメーション付き)
├── weights/
│ ├── dqn_connect4.npz # 学習済み重み
│ ├── train_log.txt # 学習ログ(リアルタイム書き出し)
│ ├── snapshots/ # 自己ベスト更新時のスナップショット
│ └── archive/ # 過去の学習重み(参照用)
├── game_runner.py # ターン管理(env・agent・UIを繋ぐ)
├── train.py # 学習スクリプト
└── CLAUDE.md # 設計メモ・開発ログ(詳細はこちら)
盤面を (3, 6, 7) の3次元配列で表します:
チャンネル0: 自分のコマがある場所 → 1.0、それ以外 → 0.0
チャンネル1: 相手のコマがある場所 → 1.0、それ以外 → 0.0
チャンネル2: 自分のターンなら盤面全体が 1.0、相手のターンなら 0.0
入力: (3, 6, 7) — 3チャンネルの盤面
Conv2d(3 → 32, 3×3, padding=1)
↓ ReLU
Conv2d(32 → 64, 3×3, padding=1)
↓ ReLU
flatten → 2688次元
Linear(2688 → 256)
↓ ReLU
Linear(256 → 7)
出力: 各列のQ値(7次元)
パラメータ数: 約710K
CNNの畳み込みフィルタにより、3連・4連パターンや防御位置を空間的に認識できます。旧MLP版(ステージ8〜9)ではflatten(126次元)で空間情報が失われ、パターン認識に中間報酬の「ヒント」が必要でしたが、CNNでは不要になりました。
出力の7つの値が「各列に置いたときの期待価値(Q値)」です。埋まった列は -inf でマスクし、有効な列の中から最大Q値の列を選びます。
Q値の更新式:
Q(s, a) ← r + γ * max Q(s', a')
このプロジェクトでのポイント:GameRunner が遅延コールバック方式を採用し、on_step_end を「相手が1手打った後(再び自分のターン)」に呼ぶ。これにより s' は常に「自分のターンの状態」になるため、通常のDQN更新式がそのまま使える。
旧実装では s' が「相手のターンの状態」だったため r - γ * max Q(s') と符号を反転していたが、Q値予測の入力として不適切だった(ネットワークは自分のターンの状態で学習しているため)。
ステージ5〜9で中間報酬の様々なスケール(大→中→小→極小→ゼロ)を試したが、全て問題を引き起こした:
| ステージ | スケール | 問題 |
|---|---|---|
| 5 | 大(累積200%超) | 「3連作り」に最適化、勝ちを逃す |
| 7 | 中(累積20〜25%) | 負けても報酬プラス、Phase 2停滞 |
| 9 | 極小(累積6〜9%) | Ph1突破4倍遅い + Ph2停滞、Q値不安定 |
| 8 | ゼロ | MLP: 空間パターンを認識できず停滞 |
結論: 中間報酬のチューニングではなく、ネットワーク構造(CNN)でパターン認識を解決するのが正しいアプローチ。ステージ10以降は勝敗報酬(±1.0)のみで学習。
| パラメータ | 値 | 意味 |
|---|---|---|
lr |
5e-4 | Adamオプティマイザの学習率 |
gamma |
0.99 | 割引率(未来の報酬の重み) |
epsilon_start |
1.0 | 探索率の初期値(完全ランダム) |
epsilon_end |
0.10 | 探索率の最小値(0.05では固定相手への過学習が激しかったため) |
epsilon_decay |
0.99990 | 探索率の減衰率(1ステップごと) |
batch_size |
128 | 1回の学習に使うサンプル数 |
buffer_capacity |
200,000 | 経験再生バッファの容量 |
warmup_steps |
2,000 | 学習開始前のバッファ蓄積ステップ数 |
target_update_interval |
500 | ターゲットネットの同期間隔(ステップ)(200では不安定だったため) |
各ステージの「失敗→原因分析→次の戦略」を時系列で記録しています。
ランダム相手で勝率79%を達成したDQNが、ルールベースAI相手には勝率0%。ランダムAIは攻撃を防ぐ動機を与えないため、「攻撃だけのAI」にしかならなかった。先手・後手の混在も戦略の干渉を招いた。
教訓: 戦略を持たない相手では汎用的な実力がつかない。先手固定で学習すべき。
DQN同士の対戦で vs Random 77% → 44% に崩壊(破滅的忘却)。「対DQN戦略」に特化して汎用性を失った。
教訓: Self-playは相手の多様性を確保しないと崩壊する。
学習中の評価(ε=0.05混入)では33%に見えたが、ε=0.0では**0%**だった。εの「まぐれ勝ち」が数字を水増し。ルールベースAI(vs ランダム勝率99%)は初期DQNには強すぎて、負けシグナルしか得られなかった。
教訓: いきなり強い相手と戦わせても信号が薄すぎる。段階的に強くすべき。
Phase 3(noise=0.2)でvs RuleBased 99.5%を達成。しかしPhase 4移行後に崩壊(86% → 5%)。
原因分析で構造的なバグを2つ発見:
-
next_stateが「相手のターンの状態」だった —
GameRunnerがon_step_endを自分が打った直後に呼んでいたため、next_stateは相手の番の盤面。ネットワークは「自分のターンの状態」で学習しているので、max Q(s',a')の予測が信頼できない。r - γ*maxQと符号反転する対症療法をしていたが本質的に不正確だった。Phase 3まではnoise=0.2のランダム性がこの不正確さを吸収していたが、Phase 4(noise=0.1)で致命的に。 -
中間報酬が勝敗報酬を上回りうるスケールだった — 3連作成+0.50、見逃し-0.80 vs 勝敗±1.0。1ゲーム中に中間報酬が複数回発生し、累積が勝敗を超える。「勝つ」より「3連を作り続ける」方がQ値的に得になりうる。
教訓: two-playerゲームではnext_stateの視点が重要。中間報酬は勝敗を超えてはならない。
ステージ5のバグ2つを修正(遅延コールバック + 中間報酬縮小)してゼロから再学習。Phase 1目標80%でep25500まで学習したが、vs Noisy(0.8)が73〜80%で振動し2回連続クリアに到達できず足踏み。noise=0.8(80%ランダム)相手の目標80%は運の要素が大きすぎた。
教訓: ほぼランダムな相手での目標は控えめに。早く次フェーズに進む方が学びが大きい。
Phase 1(noise=0.8)はep2000で突破したが、Phase 2(noise=0.5、目標70%)でvs Noisy 45〜55%のまま42000エピソード以上停滞。平均報酬は0.96前後で高止まり(勝率45%なのに報酬が高い)。中間報酬(3連+0.10等)が毎手蓄積され、1ゲームの累積中間報酬が+0.20〜0.25に達し、負けても中間報酬で相殺されていた。Q値が「勝つこと」より「中間報酬を稼ぐこと」に最適化された。
教訓: 中間報酬は「1回の値」ではなく「1ゲームの累積値」で勝敗報酬と比較すべき。ステージ5(大スケール)でもステージ7(小スケール)でも同じ構造の問題が発生した。
中間報酬を完全廃止。Phase 1はep8000で突破したが、Phase 2(noise=0.5)でvs Noisy 42〜58%のまま30000エピソード以上停滞。vs RuleBasedは全期間を通じて0%。勝敗報酬だけではConnect Fourのゲーム長(15〜20手)に対して信号が薄すぎ、「なぜ負けたか(防御をサボったから)」を学べなかった。
教訓: 中間報酬が「多すぎる」のも「ゼロ」も問題(MLP構造では)。
極小中間報酬(3連+0.03, 防御+0.02, 見逃し-0.05)を再導入。Phase 1突破にep32000かかり(ステージ8はep8000 → 4倍遅い)、Phase 2(noise=0.5)でvs Noisy 39〜57%のまま18000ep停滞。学習が激しく振動(69%→20%→74%→45%)し、極小スケールでもQ値の安定性を壊した。
教訓: 中間報酬は大→中→小→極小→ゼロの全スケールで失敗。問題の本質はMLPの空間認識能力の限界であり、中間報酬チューニングでは解決しない。ネットワーク構造の改善(CNN)が正しいアプローチ。
CNNの効果は明確: ep4000で vs RuleBased 53%(全ステージ最高記録)。Phase 1目標を75%→70%に引き下げ後、カリキュラム全Phase通過。しかしPhase 5(ルールベースAI固定)ではvs RuleBased 16%が最高で、その後0%に逆戻り。
最大の発見: カリキュラム学習は「段階的強化」ではなく「方策の上書き」だった。 ランダムAI向けの方策(攻撃のみ)とルールベースAI向けの方策(攻撃+防御)は根本的に異なり、DQNは1つの方策しか持てないため、noiseを下げるたびに前フェーズの方策が書き換えられるだけだった。
教訓: 対人ゲームにおけるカリキュラム学習は、相手タイプごとの最適方策が異なる場合、「段階的強化」ではなく「方策の上書き」になる。
4mokuの教訓(方策の上書き)をそのまま適用。カリキュラムは採用せず。
3,000epで vs RuleBased 100%達成。4mokuと違い、3mokuではDQNが十分に学習可能なことを確認。しかし人間が対戦すると簡単に勝ててしまう。簡易RuleBased(即勝ち/即防御/中央寄りの3ルール)は2手先を読めないため、フォーク(両端リーチ)に対応できない。
教訓: DQNの強さは対戦相手の強さに制約される。対戦相手を強化しなければDQNも強くならない。
RuleBasedにフォーク検出・トラップ回避・リーチスコアリングを追加したが、DQNはep3000で100%に到達。3moku先手はゲーム構造上有利すぎ、RuleBasedの強化だけでは天井テストとして不十分。スナップショット保存がベスト更新時のみだったため、100%到達後の汎化性能の変化を追跡できなかった。
教訓: vs RuleBased 100%でも人間に勝てる保証はない。汎化性能指標(vs Random)の併用と、100%到達後の定期スナップショットが必要。
100%時に毎回スナップショット保存、vs Random勝率を評価指標に追加。ep2000で vs RuleBased 100%到達、ep4000で vs Random 94%(汎化性能ピーク)。人間が対戦しても全く勝てないレベルに到達。
- DQNは3moku(5×5, 3目)で人間より強いAIを実現(4mokuでは未達成だった最終目標)
- DQNの適用限界: 3moku(状態空間~10^6)は通用、4moku(~10^12)は通用しない
- 対戦相手の強さがDQNの天井を決める。評価指標は複数持つべき(単一100%は汎化性能を保証しない)
4mokuで11ステージにわたりDQNの限界を体感した結果、MCTS + ニューラルネット(AlphaZero方式) に移行。
| DQNの限界 | AlphaZeroの解決 |
|---|---|
| 先読みなし — Q値一発で手を決める | MCTS — 50回のシミュレーションで数手先まで探索 |
| 信号が薄い — 勝敗報酬が最終手にしか付かない | MCTSの行動確率πが教師信号 — 各手に直接の教師がある |
| Self-playが不安定 — 弱い方策同士が退化 | MCTSが品質保証 — NNが弱くても探索で補正される |
alphazero/
├── env/connect4_env.py # ゲームロジック
├── network.py # デュアルヘッドNN(Policy + Value, 残差4ブロック)
├── mcts.py # モンテカルロ木探索
├── self_play.py # Self-playデータ生成
├── train.py # 学習スクリプト(CPU軽量版: 100イテレーション)
├── agents/alphazero_agent.py # 推論用エージェント
└── web/ # Flask WebUI(ポート5001)
# 学習(CPU環境向け、100イテレーション)
.venv\Scripts\python alphazero/train.py
# WebUI
.venv\Scripts\python alphazero/web/app.py
# ブラウザで http://localhost:5001 を開くDQNではvs RuleBased 16%が最高だった。AlphaZeroがこれを超えられるかが見どころ。
詳細は alphazero/CLAUDE.md を参照。
カリキュラム学習を廃止し、ルールベースAI固定で直接学習:
- カリキュラム学習を廃止: ランダム→ルールベースの段階的移行は「方策の上書き」に過ぎなかった
- ルールベースAI固定で直接学習: ステージ10 ep11000スナップショット(vs RuleBased 16%)から再開
- CNN-A・中間報酬なし・遅延コールバック方式は維持
ステージ4で失敗した原因(next_stateバグ、MLP構造、terminal_reward符号バグ)は全て解消済み。
- CNN-A: 3×3畳み込みフィルタで3連・4連パターン、防御位置を空間的に認識
- 中間報酬なし: 勝敗報酬(±1.0)のみ
- GameRunner 遅延コールバック方式:
next_stateが「自分のターンの状態」になり、DQN更新式が正確に機能 - チェックポイント保存: ReplayBuffer + ε + Adam状態を
_checkpoint.ptに保存 - スナップショットは2系統:
best_ep*+phaseup_ph* - eval負荷削減: eval_interval=1000、vs Noisy 200戦、vs RuleBased 100戦
このプロジェクトを通じて得られた最も本質的な知見。
10ステージの実験を通じて、一貫したジレンマが観察された:
- ランダム相手に最適化 → ルールベースAI相手の勝率が下がる
- ルールベースAI相手に最適化 → ランダム相手の勝率が下がる
同じ盤面でも、相手によって最適な手が異なる。 しかしDQNは「盤面だけ」を見て手を決めるため、相手を区別できない。
DQN(Q学習全般)はマルコフ性を前提とする ― 「現在の状態だけで最適行動が決まる」。しかし対人ゲームでは「相手がどういうプレイヤーか」という情報が重要であり、これは過去の手の履歴を見ないと分からない。
盤面という「状態」には相手の傾向が含まれない。つまり状態が不完全であり、厳密にはPOMDP(部分観測マルコフ決定過程)になっている。
格ゲーのプロが「戦いながら相手の癖を読んで刺しに行く」のは、まさにこの「相手モデル」を動的に構築している行為。現在のDQNにはこの機構がない。
| アプローチ | 概要 |
|---|---|
| 状態に履歴を追加 | 相手の直近N手を入力に含める(最も簡単) |
| RNN/Transformer | 手の系列から相手パターンを暗黙的に学習(AlphaStar等が採用) |
| Opponent Modeling | 相手の方策を明示的に推定する別モジュール |
| MCTS + NN | ゲーム木探索で相手非依存の最適戦略を目指す(AlphaZero方式) |
教科書的なDQNの「万能でない部分」を実験で体感し、なぜAlphaZeroやPOMDP系の手法が必要になるのかを動機づける経験が得られた。「DQNを実装して限界を知る → 次のステップ(MCTS、系列モデル等)の必要性を理解する」という学習の流れとして、非常に有意義だった。
→ この知見を踏まえ、alphazero/ でMCTS+NN方式を実装。 DQNの限界を超えられるか、本格学習で検証中。