
【競馬AI開発#20】競馬予想AIの最適な馬券購入戦略・評価方法とは?
この【競馬AI開発】シリーズでは、競馬予想AIを作ることを通して、機械学習・データサイエンスの勉強になるコンテンツの発信や、筆者が行った実験の共有などを行っていきます。
■今回やること
今作成している競馬予想AIでは、netkeiba.comからデータを取得して「一着に入るかどうか」の予測値を確率として出力する機械学習モデルを作っています。
このモデルの「精度を上げる」と一口に言っても、何をもって「精度が高い」とするのかは、実は簡単ではありません。例えばある一つのレースに対して、二つの機械学習モデルが以下のような予測値を出力している場合、どちらが「良いモデル」なのでしょうか?

今回はこのように、これまで漠然と「精度が高い」と言っていたモデルの評価方法を、より具体的に考えていく回になります。
そしてそこを深掘っていくと、今の学習方法に対応した最適な馬券購入戦略も見えてきて、その戦略を取ると回収率126.8%に到達することが分かります。

今回の内容は、機械学習モデルを実際のビジネスで活用する際にも重要になってくる考え方なので、競馬AI以外の文脈でも参考になるはずです。
■動画(概要編)
■筆者のプロフィール
東京大学大学院卒業後、データサイエンティストとしてWEBマーケティング調査会社でWEB上の消費者行動ログ分析などを経験。
現在は、大手IT系事業会社で、転職サイトのレコメンドシステムの開発を行っています。
「良い機械学習モデル」ってどんなモデル?
ここからの内容は、動画(概要編)の2:30〜でも解説しています。
機械学習モデルの良し悪しを考える上で、まずは具体例を見ていきましょう。ここで、話を単純にするために、以下の前提を置くことにします。
馬券の種類:今回は単勝馬券のみを考えます。他の馬券種の評価は、また別の機会に検討します。
オッズ情報:本来は「直前オッズ」と「確定オッズ」で変動がありますが、今回は「確定オッズ」が使えるものとして議論を進めます。(実運用では直前オッズの取得方法も重要な課題になります。)
この設定のもと、例えばある1レースにおいて、2つのモデルが以下のような予測結果を出力しているとします:

この2つの予測値だけを見ると、モデル1の方が「良さそう」に見えます。
なぜなら:
1着馬に対して、より高い確率(0.23)を予測
8着馬に対して、より低い確率(0.00)を予測
しているためです。次に、このレースの全ての出走馬に対する予測を見てみましょう:

この場合、どちらの方が「良いモデル」と言えるでしょうか?
モデル2は同じ値が並んでおり、一見するとちゃんと予測できていないように見えますが…
モデル1:2着馬に最も高い確率(0.31)を予測しており、1着馬は2番目に高い確率(0.23)を予測している
モデル2:1着馬に最も高い確率(0.13)を予測している
となっており、もし「各レースで予測値が一番高い馬に賭ける」という選択を取ると、モデル2の方が的中して「良いモデル」と判定されることになります。
実はここに、現在の精度評価方法に関する問題が隠れています。今の機械学習モデルでは次のようなbinary loglossという指標を最小化するように学習しているのでした:
これが「ある一つのデータに対するbinary logloss」を表しており、
:実際の値(0 or 1)
:予測確率(0~1の値)
です。この損失関数は以下のような性質を持ちます:
(1着の馬)の場合:
が1に近いほど損失は小さくなる
が0に近いほど損失は大きくなる
(その他の着順)の場合:
が0に近いほど損失は小さくなる
が1に近いほど損失は大きくなる
これを全データに渡って平均を取ったものが、そのモデルの「予測誤差」として計算されるわけです。
実際に、この指標を二つのモデルで計算してみると、
モデル1のlogloss平均:0.10484
モデル2のlogloss平均:0.13075
となり、これは今の目的変数の設計では、学習の結果はモデル1の方に近づいていくことを意味しています。

一方、先ほどみたように「loglossの最適化」は必ずしも「最適な並べ替え」を意味しません。
しかし、今のモデルの評価方法である「的中率・回収率シミュレーション」は、「予測スコア上位n件を見た時に、的中・回収できているか」を見ています。

的中率・回収率シミュレーションについて、具体的には以下の記事で実装しています。
このシミュレーションは「全てのレースについて予測スコア上位に賭ける」という意思決定を前提においた評価指標になっているので、目的変数も本来はその評価指標を最大化するような設計にしなければならないのです。
つまり、「意思決定方法(=予測スコアをどう使うか)・評価指標 ↔︎ 目的変数」の間でミスマッチが起きてしまっている、ということです。
では、このミスマッチを解消するためにどのような方向性が考えられるでしょうか?
大きく分けて2つのアプローチがあります:
目的変数を今の意思決定方法・評価指標に合わせるアプローチ
「レース内での並べ替え」が最適化されるように目的変数を改善します。パッと思いつくアプローチだと、ランキング学習の導入などがあるかもしれません。意思決定方法・評価指標を今の目的変数に合わせるアプローチ
逆に今の学習方法(=loglossの最小化)と整合性が取れるような意思決定方法と評価指標を考えます。
本記事では、まず2つ目の「意思決定方法・評価指標を目的変数に合わせるアプローチ」を検討していきます。
では、今の学習方法と整合性が取れるのは、どのような意思決定方法なのでしょうか?
「整合性が取れる」というのはつまり「loglossが最小化されればされるほど、正確な意思決定が可能になる状態(=より高い回収率が達成できる状態)」ということです。
ここで、loglossの最小化は「勝率」を確率値としてより正確な値にチューニングしている、ということに着目します。
もし正確な「勝率」が計算できていれば、それをオッズと掛け合わせることで「期待値」が正確に計算できるはずです。
「この値が1を超えた馬券を賭ける」という意思決定を取れば、理論上回収率が100%を超えることになり、loglossが最小化されるほどその意思決定は正確なものになるはずです。
この仮説を検証してみましょう。
期待値型モデルのシミュレーション
ここからの内容は、動画(概要編)の14:58〜でも解説しています。
前回作成した競馬AI v3.0.7の学習コードを回すと、v3_0_7/data/03_train/evaluation.csvに予測結果が格納されるので、まずはこれを読み込んでいきます。
▼v3_0_7/src/dev.ipynb
import pandas as pd
# 評価用データの読み込み
df = pd.read_csv("../data/03_train/evaluation.csv", sep="\t")
このデータは、2018年から2022年の5年間のデータで学習した機械学習モデルを使って、2023年の1年間のレースを予測したものになります。
pred列が予測確率を表すので、これと単勝オッズ(tansho_odds列)を掛けて期待値を計算し、期待値が1を超える馬に賭けるシミュレーションをしてみましょう。
# 期待値を計算
df["expect_return"] = df["tansho_odds"] * df["pred"]
# 期待値が1.0を超えるものを抽出
bet_df = df.query("expect_return > 1.0").copy()
# 払戻金額の計算(1円賭けた場合)
bet_df["payoff"] = bet_df["target"] * bet_df["tansho_odds"]
# 回収率の計算
total_payoff = bet_df["payoff"].sum() # 総払戻金額
total_bet = len(bet_df) # 総購入枚数
return_rate = total_payoff / total_bet # 回収率
print(f"総払戻金額: {total_payoff}円")
print(f"総購入枚数: {total_bet}枚")
print(f"回収率: {return_rate:.3f}")総払戻金額: 2450.4円
総購入枚数: 3013枚
回収率: 0.813期待値が1を超える馬全てに賭けた場合、回収率は約81%となりました。
次に、この「期待値の閾値」を1から上げていくとどうなるか検証してみます。
from collections import defaultdict
import numpy as np
result = defaultdict(list)
# 0.8から2.0まで100分割した閾値を用意
for exp in np.linspace(0.8, 2, 100):
bet_df = df.query(f"expect_return > {exp}")
total_bet = len(bet_df)
total_payoff = bet_df["payoff"].sum()
return_rate = total_payoff / total_bet
result["expect_return"].append(exp)
result["total_bet"].append(total_bet)
result["total_payoff"].append(total_payoff)
result["return_rate"].append(return_rate)
result_df = pd.DataFrame(result)このデータをプロットしてみます。
import seaborn as sns
import matplotlib.pyplot as plt
sns.set()
sns.lineplot(result_df, x="expect_return", y="return_rate")
回収率100%を超える部分が存在し、期待値1.78あたりで最大127%程度に到達していることが分かります。
これはつまり、期待値の閾値を1.78に設定し、それ以上の単勝馬券に絞って賭けると127%程度の回収率になるということを意味しています。
この結果は大変喜ばしいですが、このシリーズではもう少し踏み込んで考えてみましょう。
例えば、以下の二つのモデルがあった時に、実際に賭けたいのはどちらでしょうか?
モデル1: 1年間に3レースだけ賭けて回収率200%
モデル2: 1年間全てのレースに賭けて回収率105%
直感的にモデル1は回収率が高いが「たまたま当たっただけ」の可能性がありそうだ、と思うと思います。
一方、モデル2は全レースで安定して回収率105%を達成しているように思えます。しかし、例えば以下のようなケースを考えてみましょう:
3000レース中2999レースは全て外れ
最後の1レースで超高配当馬券を的中し、結果として回収率105%(実際のところはそんな高配当馬券はないかもしれませんが)
このように内訳を見てみると、「本当に安定している」とは言えない可能性が出てきます。「どちらが賭けるべきモデルなのか」の判断を定量的に行う方法はないのでしょうか?
そう考えていくと、我々がモデル選択の上で最終目的にする指標としては、実は単なる「回収率」ではなく、投資理論でよく使われる「ある指標」を使う方が適切であることが分かってきます。
定期購読をすると、今月更新される記事に加えて#1〜#4の記事が980円で全て読めるので、単品購入より圧倒的にお得です。
※#20の単体購入だと、前回までのソースコードは含まれませんのでご注意ください。