0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

リアルタイム盤面解析から行動提案まで

Last updated at Posted at 2026-01-14

リアルタイム盤面解析から行動提案まで

※ 本記事では、特定のゲーム名・固有IPは避け、
**「盤面を持つリアルタイム対戦ゲーム」**として一般化して記述します。


1. 何を作っているのか

目標はシンプルです。

  • 入力: プレイ動画(mp4)やリアルタイム画面

  • 出力:

    • 「今は何もしない(NOOP)」
    • 「どの行動を」「どの位置(グリッド)に」行うか

人間が操作できる“少し先”のタイミングで提案する AI を作ること。

そのために、次の 2 段構成を採用しました。

  1. State → Action の教師あり学習
  2. 強化学習ではなく、まずは 人間プレイの模倣 から始める

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つを同時に決める必要があります。

  1. 何をするか(行動の種類 / カード / アクション)
  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_prob
  • action_prob + grid_prob
  • log(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枚の入力として渡している

と考えると分かりやすいです。

参考イメージ
image.png


出力例①:モデルの生出力(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側の話を書く予定です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?