リアルタイム盤面解析から行動提案まで
※ 本記事では、特定のゲーム名・固有IPは避け、
**「盤面を持つリアルタイム対戦ゲーム」**として一般化して記述します。
1. 何を作っているのか
目標はシンプルです。
-
入力: プレイ動画(mp4)やリアルタイム画面
-
出力:
- 「今は何もしない(NOOP)」
- 「どの行動を」「どの位置(グリッド)に」行うか
を 人間が操作できる“少し先”のタイミングで提案する AI を作ること。
そのために、次の 2 段構成を採用しました。
- State → Action の教師あり学習
- 強化学習ではなく、まずは 人間プレイの模倣 から始める
2. 全体アーキテクチャ
mp4 / 録画画面
↓
[盤面ROI抽出]
↓
[状態画像(256x256)]
↓
[CNNエンコーダ]
↓
[2ヘッド出力]
├─ action_id(何をするか / NOOP含む)
└─ grid_id(どこにするか)
特徴は次の通りです。
- 盤面は 6×9 グリッド(粗くして学習を安定させる)
- NOOP を明示的なクラスとして扱う
- 後述の理由から 2フレーム + diff(9ch)入力を採用
3. 状態表現:2フレーム + diff
3.1 diffチャンネルとは何か(補足)
ここで言う diffチャンネル とは、
現在フレーム − 過去フレーム
を計算した差分画像を、追加の入力チャンネルとして使うことを指します。
具体的には、入力は次の3種類に分かれます。
- current frame(現在の盤面・3ch)
- past frame(少し前の盤面・3ch)
- diff = current − past(変化量・3ch)
👉 合計 9チャンネル になります。
なぜ diff が必要か?
盤面ゲームにおいて重要なのは、
- 「そこに何が ある か」よりも
- 「何が 変わった か」
であることが非常に多いです。
人間は無意識に
- 新しく出てきたユニット
- 消えたオブジェクト
- 動いた塊
といった 差分 に注目して判断しています。
CNN に現在フレームだけを与えると、
- 静止した背景
- 常に存在する建物/UI
も同じ重みで処理されてしまいます。
diffチャンネルを与えることで、
「今フレームで 新しく意味のある変化 が起きた場所」
を強く示すヒントをモデルに渡せます。
2フレームだけでは不十分?
「current と past の 2 枚があれば、diff は不要では?」と思うかもしれません。
理論的には可能ですが、実務では次の差が出ました。
-
2フレームのみ
- モデル自身が差分を 内部で学習 する必要がある
- データ量が少ないと学習が安定しない
-
2フレーム + diff
-
差分が明示される
-
少量データでも
- 出現
- 消滅
- 移動
が学習しやすい
-
つまり diff は
モデルにとっての「下書き済み特徴量」
のような役割を果たします。
diffが特に効くケース
- ユニットの 出現エフェクト
- 小さくて一瞬のオブジェクト
- 複数要素が重なった盤面
- 背景が派手・常時アニメーションしている場合
こうした状況では、
- current だけ → ノイズに埋もれる
- diff あり → 変化部分が浮き上がる
という違いがはっきり出ます。
フォールバック設計
実装上は、
-
過去フレームが存在しない場合
- past = current
- diff = 0
とすることで、
- 入力形状を常に固定
- 学習・推論コードを単純化
しています。
まとめ(diffチャンネルの嬉しさ)
diffチャンネルを使うことで、
- 盤面の「変化」に強くなる
- 少量データでも学習が安定
- 人間の認知に近い入力表現になる
というメリットが得られます。
なぜ1フレームでは足りないか
盤面ゲームでは、
- 「今ある」よりも
- 「何が動いたか / 何が増えたか」
が重要です。
そこで、入力を次のように拡張しました。
- current frame(3ch)
- past frame(3ch)
- diff = current - past(3ch)
👉 合計 9ch, [9, 256, 256]
過去フレームが存在しない場合はフォールバックして zero-diff になります。
4. データセット設計(JSONL)
学習データは 行動イベント列として持ちます。
{"t": 1.23, "x": 100.0, "y": 200.0, "action_id": "action_0"}
{"t": 1.56, "x": -1.0, "y": -1.0, "action_id": "__NOOP__"}
ポイント:
- 1行 = 1イベント
- NOOP は特別扱い(x,y = -1)
- 時刻
tから「少し前(lead-sec)」の状態画像を生成
5. NOOP 問題とデバッグ設計
初期データが少ないと、ほぼ必ずこの現象が起きます。
推論結果がすべて NOOP になる
原因
- grid 確率がほぼ一様
- combined_score = card_prob × grid_prob
- → NOOP が常に勝つ
対策(predict のデバッグ拡張)
以下を predict CLI に追加しました。
--exclude-noop--score-mode {mul, add, logadd}--dataset-path--device {auto,cpu,cuda,mps}
これにより、
- 必ず overlay を表示して位置を確認
- CPU/GPU を切り替えて快適に推論
が可能になりました。
6. 学習パイプライン
6.1 2ヘッド出力とは何か(補足)
本プロジェクトでは、モデルの出力を 「2ヘッド」 に分けています。
ここで言う ヘッド とは、
同じ特徴量(CNNの出力)を入力として、別々の目的を持つ出力層
のことです。
なぜ分けるのか?
盤面ゲームの行動は、次の2つを同時に決める必要があります。
- 何をするか(行動の種類 / カード / アクション)
- どこにするか(位置・座標・グリッド)
人間も無意識に
「今はこの行動を、この辺に出す」
と 2段階で判断しています。
これをそのままモデル構造に反映したのが 2ヘッド出力です。
構造イメージ
[状態画像]
│
CNN Encoder
│ ← 盤面の特徴ベクトル
┌───────┴────────┐
│ │
Action Head Grid Head
(分類) (分類)
│ │
action_id grid_id
-
Action Head
- 出力: action_id(NOOP 含む)
- 例:
__NOOP__,action_0,action_1, ...
-
Grid Head
- 出力: grid_id(6×9 グリッド = 54 クラス)
- 例:
(gx=3, gy=6)
推論時はどう使う?
推論では、それぞれの確率を組み合わせます。
- action_prob = 行動の確率
- grid_prob = 位置の確率
combined_score = f(action_prob, grid_prob)
本プロジェクトでは、デバッグや学習段階に応じて
action_prob × grid_probaction_prob + grid_problog(action_prob) + log(grid_prob)
を切り替えられるようにしています。
なぜ1ヘッド(全部組み合わせ)にしないのか?
例えば
- 行動 10 種類
- グリッド 54 マス
を1ヘッドで表すと 540クラス分類になります。
これは、
- データが大量に必要
- NOOP が支配的になりやすい
- 「行動は合っているが位置が少し違う」学習ができない
という問題を生みます。
2ヘッドに分けることで、
-
行動と位置を 独立に学習できる
-
データが少なくても安定
-
デバッグ時に
- 行動は合ってる?
- 位置は合ってる?
を分解して確認できる
という大きなメリットがあります。
モデル
-
最小構成 CNN
-
2ヘッド出力
- action head(分類)
- grid head(分類)
学習時の工夫
- NOOP augmentation(間隙から自動生成)
- epoch 毎に
last.ptを必ず保存 - val がある場合のみ
best.pt
7. ラベリングツール(最大の山場)
学習が進まない最大の原因は データ不足 です。
そこで、mp4 を見ながら events を量産するツールを作りました。
機能
- mp4 再生(OpenCV)
- クリックで (x,y)
- 数字キーで action_id
-
Nで NOOP -
Uで undo -
A/D/J/Lでシーク - ROI 指定対応(座標は元フレームへ復元)
設計ポイント
- GUI 部分と IO/座標ロジックを分離
- IO/ROI は pytest でテスト可能
これで 1 試合から 数百イベントを現実的に作れます。
8. ここまでで得られたこと
- 強化学習に行く前に
- 教師あり + 良いデバッグ体験が圧倒的に重要
- NOOP は逃げずに正面から設計すべき
- 「必ず可視化できる」predict が開発速度を決める
9. 次にやること
- ラベルデータを 200〜500 件まで増やす
- 連続 idx 推論(動画スキャン)
- 予測ヒートマップ化
- 提案タイミングの最適化(人間操作に間に合う)
9.5 入力から出力までの具体例(イメージしやすい補足)
ここまでの説明を、具体的な数字つきの例で整理します。
入力例:モデルに渡る状態画像
※ ここで重要な補足です。
このプロジェクトでは、
ユーザー入力(動画・画面)から
[9,256,256]の入力テンソルを作る部分は、学習ではありません。
この変換はすべて 通常のロジック(前処理) で行っています。
前処理と学習の役割分担(補足)
処理全体を分けて考えると、役割は明確です。
前処理(非学習・ルールベース)
- mp4 / 画面キャプチャを取得
- ROI を切り出す
- リサイズ(256×256)
- 正規化(0–255 → 0–1)
- current / past フレームを用意
- diff = current − past を計算
- チャンネル方向に結合
結果として得られるのが:
入力テンソル: [9, 256, 256]
この段階では 学習済モデルは一切使っていません。
学習済モデル(CNN)の役割
CNN は、上記の入力テンソルを受け取り、
-
盤面の重要な特徴を抽出(=埋め込みを生成)
-
その埋め込みから
- 行動(action)
- 位置(grid)
を予測します。
つまり、
[9,256,256]を作るのは人間が書いたロジック
そこから「何が重要か」を学ぶのが CNN
という役割分担になっています。
なぜこの分離が重要か
この設計により、
- 学習を安定させやすい
- 推論時の挙動が分かりやすい
- デバッグがしやすい(前処理 or モデル、どちらが原因か切り分け可能)
という実務上のメリットがあります。
以下は、具体的な入出力例です。
前処理後、モデルに渡る 1 サンプルは次の形です。
入力テンソル shape: [9, 256, 256]
中身は次の 9 チャンネルで構成されています。
| チャンネル | 内容 |
|---|---|
| 0-2 | current frame(RGB) |
| 3-5 | past frame(RGB) |
| 6-8 | diff = current − past(RGB) |
直感的には:
「今の盤面」「少し前の盤面」「その差分」を
重ねて1枚の入力として渡している
と考えると分かりやすいです。
出力例①:モデルの生出力(logits)
CNN を通した直後、モデルは次のような 生のスコア を出します。
action_logits shape: [10]
[ -1.2, 0.3, 2.1, -0.5, ... ]
grid_logits shape: [54]
[ -0.8, -1.1, 0.2, 1.9, ... ]
この段階では:
- 値は実数
- 合計は 1 ではない
- 0/1 ではない
点に注意してください。
出力例②:確率分布(softmax 後)
それぞれに softmax を適用すると、次のようになります。
action_prob shape: [10]
[ 0.03, 0.07, 0.52, 0.04, ... ] ← 合計 = 1.0
grid_prob shape: [54]
[ 0.01, 0.00, 0.02, 0.18, ... ] ← 合計 = 1.0
ここで初めて:
- 「action_2 が一番ありそう」
- 「grid_3 が一番ありそう」
と 確率として解釈できるようになります。
出力例③:最終的な行動決定
最終的な提案は、次のように決まります。
action_id = argmax(action_prob) = 2
grid_id = argmax(grid_prob) = 3
または、確率を組み合わせてランキングします。
combined_score = action_prob × grid_prob
この結果、
「action_2 を grid_3 に出す」
という 1つの提案が得られます。
NOOP の場合
NOOP も同じ action head の 1 クラスです。
action_prob = [ ..., __NOOP__: 0.35, ... ]
- NOOP が最大 → 何もしない提案
- exclude-noop を使うと次候補を可視化
重要なポイントまとめ
-
入力は 9ch の連続値テンソル
-
出力は
- 10 個の行動確率
- 54 個の位置確率
-
64 個の 0/1 が出るわけではない
-
0/1 は学習時の正解ラベル側にのみ存在
9.6 なぜ前処理を学習に任せなかったのか(設計上の判断)
ここまで読んで、次の疑問を持った方も多いと思います。
「なぜ
[9,256,256]への前処理を、すべて学習に任せなかったのか?」
これは偶然ではなく、明確な設計判断です。
前処理を学習に任せなかった理由
本プロジェクトでは、前処理(ROI切り出し・解像度固定・diff計算など)を
学習ではなくルールベースで固定しています。
理由は主に次の4点です。
1. 入力の意味を固定したかった
モデルに対して、
- ここが盤面
- このスケールで判断すればよい
- 重要なのは「変化(diff)」
という 設計意図を明示的に渡すためです。
少量データの段階で、
- UI要素に引っ張られる
- 無意味な細部に過適合する
といった挙動を防ぐ狙いがあります。
2. データ量との現実的なバランス
前処理を学習に任せる設計(フルフレーム入力・end-to-end学習)は、
- 非常に大量のデータ
- 長い試行錯誤
を前提とします。
本プロジェクトでは、
- 個人でのラベリング
- 数百〜数千イベントが現実的な上限
という制約があるため、
学習に任せる設計はスケールが合わないと判断しました。
3. デバッグ可能性を保つため
前処理を固定することで、
- 入力が原因か
- モデルが原因か
を切り分けて考えられます。
実際に本記事で紹介した
- diffチャンネル
- NOOP除外
- overlayによる可視化
といったデバッグは、
前処理が固定されているからこそ成立しています。
4. 人間の認知構造に近づけるため
人間は盤面ゲームを、
- 画面全体を等しく見る
- すべてをピクセルレベルで処理する
わけではありません。
実際には、
- 盤面だけを見る
- 変化した場所に注目する
- 粗い空間分割で判断する
という認知を行っています。
今回の前処理は、
この人間の認知を近似した入力表現になっています。
前処理を学習に任せる設計とのトレードオフ
前処理を固定する設計と、学習に任せる設計には
それぞれ明確なトレードオフがあります。
| 観点 | 前処理固定(本記事) | 学習に任せる設計 |
|---|---|---|
| データ量 | 少量でも可 | 大量に必要 |
| 学習安定性 | 高い | 不安定になりやすい |
| デバッグ | 容易 | 困難 |
| 表現力 | 人間の設計に依存 | 非常に高い |
| 汎用性 | ゲーム依存 | 高い |
どちらが正しい、という話ではなく、
目的・フェーズ・制約によって選択が変わります。
将来的な発展(ハイブリッド設計)
実務では、
- ROI検出のみを学習に任せる
- diffの重み付けだけを学習する
- 埋め込みを別タスクと共有する
といった 中間的な設計 も有効です。
本プロジェクトの設計は、
そうした発展へ進むための
安全で理解可能な第一段階と位置づけています。
9.7 大量データがあれば end-to-end 学習の方が性能は良くなるのか?
ここまでの設計を見て、次の疑問を持つ方もいるでしょう。
「大量のデータがあれば、最終的には end-to-end 学習の方が性能が良くなるのでは?」
この問いに対する答えは、Yes でもあり No でもあります。
理論的な答え
理論上は、
- 十分に大量のデータ
- 十分に大きなモデル
- 安定した学習環境
が揃えば、
人間が設計した前処理を超える特徴表現を
モデルが自ら発見できる
可能性があります。
実際に、大規模な成功例(大規模対戦AIなど)では、
ほぼ end-to-end に近い構成が採用されています。
それでも「必ず良くなる」とは言えない理由
1. 必要なデータ量の桁が違う
end-to-end 学習が安定して優位になるのは、
- 数百万〜数億規模のサンプル
がある場合です。
本プロジェクトのように、
- 個人でのラベリング
- 数百〜数千イベント
が現実的な上限となる状況では
スケールが大きく合いません。
2. ノイズ耐性と学習安定性
end-to-end 学習では、
- UI
- エフェクト
- 背景装飾
といった、
本質的でない情報もすべて入力に含まれます。
大量データがない状態では、
- 偶然の相関
- 無意味な特徴への過適合
が起きやすくなります。
前処理固定は、
これらを 人間が先に取り除く制約 として機能します。
3. 性能は「精度」だけではない
実務で重要なのは、
- 精度やスコア
- 勝率
だけではありません。
- 予測が安定しているか
- なぜそう判断したか説明できるか
- デバッグできるか
といった点も同じくらい重要です。
end-to-end 学習は、
これらの点で不利になることがあります。
実務でよく選ばれる最終形:ハイブリッド設計
多くの実システムでは、
- 完全な前処理固定
- 完全な end-to-end
のどちらかではなく、
前処理の一部だけを学習に任せるハイブリッド設計
が採用されます。
例:
- ROI 検出のみを学習
- diff の重み付けのみを学習
- 埋め込み表現を複数タスクで共有
本プロジェクトの設計は、
こうした発展へ進むための
安全で理解可能な第一段階です。
まとめ
- 理論上は、大量データがあれば end-to-end 学習は高い上限性能を持つ
- しかし、少量データ・個人開発・デバッグ重視のフェーズでは
前処理を固定した方が実務的な性能は高くなりやすい - end-to-end はゴールではなく、フェーズに応じた選択肢の一つ
10. まとめ
本記事では、
- 盤面ゲームの状態表現
- NOOP を含む行動提案モデル
- 実用的なデータ収集とデバッグ設計
を 実装ベースで紹介しました。
同じように
- 動画解析
- 行動予測
- 人間支援AI
を作っている方の参考になれば幸いです。
続編では、連続推論・ヒートマップ・提案UI側の話を書く予定です。
