問い — 何を題材に強化学習を試すか
私は強化学習を勉強しています。今、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)
学習の流れ:
- まずランダムAI相手に学習
- ある程度強くなったらSelf-playに切り替え
- 人間(自分)と対戦して強さを実感
実際に作ってみますか?どちらの題材が好みか教えてもらえれば、コードの雛形を作ります。五目並べの方がルールがシンプルで実装しやすく、オセロの方が「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_decay | 0.9995 | 0.99985 |
| 中間報酬 | なし | 4連追加で+0.3、相手の4連阻止で+0.2 |
buffer_capacity | 10000 | 50000 |
batch_size | 64 | 128 |
num_episodes | 5000 | 10000 |
| 修正版を作成します。 | ||
| 動作確認します。 | ||
| 大幅に改善されました。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を相手にして段階的強化)への発展を作りましょう。