競馬AI予測

【競馬AI #22】softmaxの罠に気づいた日。回収率20.8%→122.2%、そして「100%保証」の限界


前置き:Phase 3.6へ、そして新たなバグ

前回の記事(#18)から、しばらく間が空いた。

あの後、開発は「防衛ロジックV4」から大きく方向転換し、Phase 3.6という新しいアーキテクチャに移行した。馬ごとの過去成績・コース適性・馬場状態別成績・騎手との相性をすべてDB化し、キャリブレーション(確率補正)を組み込んだ、より本格的なシステムだ。

そして先日、このPhase 3.6の推奨ロジックに、根本的なバグが見つかった。

本記事は、そのバグの発見から修正、そして「回収率が最低でも100%を超えないと機能しない」という要求に応えるための設計変更、さらにその設計変更自体が壁にぶつかった顛末までの記録である。


発端:8月1日、32頭推奨して的中0

Phase 3.6の推奨スクリプトを使い、8月1日のレースで32頭を推奨した。

結果は 0的中、回収率0%

一度きりならただの不運で片付けられる。だが、念のため6月〜7月の10日間・281件でバックテストしてみたところ、同じ結果が出た。

10日間・281件のバックテスト
的中率:1.1%
回収率:20.8%

これは「運が悪い」で片付けられる数字ではない。


原因調査:確率計算に潜んでいたsoftmaxの罠

推奨ロジックの中身を読み直したところ、レース内の確率正規化に問題があることが分かった。

何が起きていたか

Phase 3.6のモデルは、各馬について「勝つ確率」(0〜1の値)を直接出力する。この確率をレース内の全馬で合計1になるよう正規化する際、旧コードは softmax(exp(x)) という手法を使っていた。

だが、softmaxは本来「対数オッズ(ロジット)」を確率に変換するための関数だ。モデルの出力はすでに確率であり、ロジットではない。 これに対してさらにsoftmaxをかけると、値の差が小さい確率同士(0.00〜0.17程度)を指数変換しても、比がほとんど広がらないという問題が起きる。

実際に6月20日・函館1R(16頭立て)のデータで検証した結果がこれだ。

馬名                 モデル出力(勝つ確率)    旧方式(softmax後)の確率
グランフィエルテ          0.0087            6.30%
$カンフージョン           0.0074            6.29%
(他13頭、モデル出力0.0000) 0.0000            6.24%

上位馬もゼロ点の馬も、ほとんど同じ「確率」になっている。

この状態で EV(期待値)= 確率 × オッズ を計算すると、確率がほぼ均一なので、EVは実質的にオッズの大小とほぼイコールになる。つまり「EVが高い」の中身は、モデルが有力視した馬ではなく、単にオッズが高い人気薄だった。これがEV上位帯(3.00以上)186件中0的中という壊滅的な結果の直接の原因だ。

修正:比例配分方式へ

softmax(exp)をやめ、各馬の確率をレース内合計で単純に割る 比例配分方式 に変更した。

修正後:同じ函館1Rでの確率
グランフィエルテ:41.7%
カンフージョン :35.4%
テイクマイハート:22.9%
(他13頭)    :0%

モデルが実際に学習した強弱が、そのまま確率の比率に反映されるようになった。

修正の効果

同じ10日間・同じリークなしモデルで再検証した結果:

                修正前    →   修正後
推奨頭数         281頭    →   446頭
的中率           1.1%    →   9.2%
回収率           20.8%   →   61.5%

大幅に改善したが、まだ回収率100%には届いていない。


「100%を超えないと機能しない」という要求

ここでオーナーから明確な要求が来た。

「回収率が最低でも100%を超えないと、競馬予測ソフトとして機能しない。100%を超えるような設計にしてほしい」

これは本質的に難しい要求だ。中央競馬の単勝馬券は売上の約20〜25%が控除されるパリミュチュエル方式であり、母集団全体で見た期待回収率は構造的に100%を下回る。 100%を超えるには、他の購入者よりオッズに対して正確に予測できている必要がある。

とはいえ、データが実際に示している優位な条件に絞り込むことはできる。61.5%という数字の中身を分解してみた。

新馬(DBに実績なし):的中率7.3% 回収率24.1%
既知馬          :的中率9.8% 回収率72.5%

EV 5.00以上      :的中率0.8% 回収率2.5%(壊滅的)
EV 1.50〜3.00    :的中率9〜15% 回収率42〜71%(弱い)

新馬とEV5.00以上を除外すれば改善しそうだ。ただし、全データを見てから条件を選び直すのは「後付け」であり、過学習のリスクが高い。そこで10日間を前半7日・後半3日に分割し、独立に検証した。

既知馬限定・EV3.00〜5.00未満
train(前半7日、n=55):ROI 121.6%
holdout(後半3日、n=17):ROI 243.5%

train・holdoutの両方で独立に100%を超えた。この条件を採用し、10日間全体で再検証すると、74件・回収率171.5%。

「回収率100%超え」を達成したかに見えた。


サンプルを増やすたびに数字が崩れていく

だが、ここで慢心しなかったのが良かった。

追加でレースカードをいただき、19日分・12日分と検証範囲を広げていった。結果は次の通りだ。

10日間(74件)  :ROI 171.5%
29日間(211件) :ROI 118.5%
41日間(312件) :ROI  92.6% ← 100%割れ

件数を増やすほど、数字が下がり続けた。

171.5%という数字は、少数の大きな配当(6/20:408%、6/28:502%、7/26:828%など)に引っ張られた、いわば「たまたま良かった」結果に過ぎなかったのだ。

原因を分解し、再度train/holdoutで検証

41日間のデータをEV帯別に見ると、EV3.00〜3.50の帯(134件)が的中率8.2%・回収率53.2%と弱く、これが全体を大きく引き下げていた。

再び「後付けの罠」を避けるため、41日間を前半20日・後半21日に分割し、EV下限を再探索した。

EV下限   train(前半20日)  holdout(後半21日)
3.00     86.9%          97.9%          → どちらも100%未満
3.25    101.4%         106.8%          → 僅かに100%超
3.50    110.8%         134.3%          → 安定して100%超
3.75    114.0%          96.7%          → holdoutで崩れる=過学習の兆候
4.00    142.0%          58.5%          → 明らかな過学習

EV下限3.50だけが、train・holdoutの両方で独立に安定して100%を超えた。3.75以上はtrainこそ良い数字が出るが、holdoutで崩れており、過学習と判断して見送った。

再調整後の結果

条件を「既知馬限定・EV3.00〜5.00」から「既知馬限定・EV3.50〜5.00」に引き上げ、41日間全体を再生成・再検証した。

推奨頭数:178頭(1日あたり約4.3頭。旧条件の約7.6頭から減少)
的中率 :9.6%
回収率 :122.2%

41日中28日は的中ゼロ。少数の大きな配当(6/26:1260%、6/28:753%、6/20:712%など)が全体を支える構造は変わっていないが、サンプルを3倍以上に増やしても、100%は維持できた。


実運用テストの開始:8月8日・9日

ここまではすべて過去データでのバックテストだった。8月8日以降のレースは、本番モデルの学習データ(8月5日更新分まで)に含まれていないため、リークなしで本番モデルをそのまま使って実戦投入できる。

8月8日:6件推奨 的中0 回収率0%
8月9日:6件推奨 的中0 回収率0%
累計 :12件推奨 的中0 回収率0%

正直、幸先の良いスタートとは言えない。だが、41日間のバックテストでも28/41日(68%)は的中ゼロだったことを踏まえれば、2日連続ゼロは想定の範囲内だ。データが貯まるまで、一喜一憂せず記録を続ける。


Data Scientistとしての振り返り

今回の一連の作業で、あらためて実感したことが二つある。

一つ目:バグは「結果が悪い」ところから逆算して見つかる。 8月1日の0的中がなければ、softmaxの罠には気づかなかった。「なんとなく調子が悪い」で済ませず、数字で異常を捉え、コードまで遡って原因を特定することの重要性を再確認した。

二つ目:「都合の良い数字」ほど疑うべきだ。 171.5%という数字が出た時点で満足していたら、41日間で92.6%に落ち込む現実に気づかないまま運用を始めていたかもしれない。サンプルを増やすたびに検証をやり直し、train/holdout分割で「後付けの最適化」を避け続けたことが、最終的に122.2%という、まだ脆いながらも根拠のある数字にたどり着けた理由だ。

「回収率100%を超える設計にしてほしい」という要求に対して、「必ず100%を超える」と保証することはできない、という結論を正直に伝えた上で、データが示す範囲で最善を尽くす、というスタンスを貫いたことも、今回の記録として残しておきたい。


技術ノート

修正したファイル

recommend_phase3_v3_6_hybrid_v2_COMPLETE.py
  STEP 6:softmax(exp) → 比例配分方式(raw_score / レース内合計)
  STEP 8:EVフィルタ条件を 1.50以上・上限なし
          → 既知馬限定・EV 3.00〜5.00未満
          → 既知馬限定・EV 3.50〜5.00未満(最終版)
  STEP 9:「高確度(高EV新規)」タグを廃止
          (EV≥10帯は的中率0.8%と最悪の成績だったため)

検証に使った期間

バックテスト:2026年2月〜7月、計41日間(実際の出馬表+実際の結果で検証)
使用モデル :keiba_ai_phase3_v3_6_classifier.pkl.bak_20260805_113649
        (2025年12月までのデータで学習。検証対象日の結果は一切含まないリークなしモデル)
実運用  :2026年8月8日〜(本番モデルをそのまま使用。8/5更新分までしか
        学習していないため、8/8以降はリークなしで実戦投入可能)

忘れてはいけない注意点

✓ 中央競馬は控除率約20〜25%のパリミュチュエル方式のため、
  母集団全体では期待回収率は構造的に100%を下回る。
  「常に100%超」を保証する設計は原理的に不可能。

✓ 今回の122.2%は41日間・178件というまだ小さいサンプルに基づく。
  10→29→41日で171.5%→118.5%→92.6%と推移した実績がある以上、
  今後さらにサンプルが増えれば、再び数字が動く可能性は十分にある。

✓ 絞り込むほど1日あたりの推奨件数は減る(7.6件→4.3件/日)。
  母数が小さいままだと、実運用でのブレはむしろ大きくなりやすい。

次回の予定

【継続】実運用の記録

8月8日から始めた実運用の結果を、日々記録していく。ある程度件数が貯まった時点で、41日間のバックテストと同じ傾向が出るか(回収率が100%前後で推移するか)を確認する。

検証ポイント

✓ 実運用でも100%前後の回収率を維持できるか
✓ 「少数の大きな配当が全体を支える」構造は実戦でも同じか
✓ さらにサンプルを増やした場合、122.2%はどこまで下がるか(あるいは安定するか)

いずれの結果が出ても、正直に記録し、必要ならまた設計を見直す。それが、ここまでの一連の作業で貫いてきたスタンスだ。


【競馬AI開発記】次回予定:

  • Day 20:実運用データがある程度貯まった段階での中間報告
  • Day 21:EV5.00以上の扱い、季節性(3月の成績が特に弱かった件)の追加分析

-競馬AI予測