前置き: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月の成績が特に弱かった件)の追加分析