【LightGBM】予測確率を補正して回収率100%超えのモデルを作成|Python

競馬AI

本記事の概要

本サイトでは、競馬AIの作成方法について解説しており、過去に作成した競馬AIの回収率は80%台に留まっています。

過去モデルの回収率

※横軸が「1着になる確率」、縦軸が「y_pred_probがある値を超えた競争馬に単勝を賭けた時の回収率」を表しています。

この回収率では競馬AIとしての利用は困難なので、回収率の向上方法を調査してみたところ「分類モデルの確率補正」という記事を発見しました。

この記事によると、「scikit-learn」の

CalibratedClassifierCV

を使用することで、予測値を真の確率に近づけるような補正を行えるようです。

早速試してみたいと思います。

前提条件

続いて、競馬AI作成における前提条件を説明します。

前提条件
  • 目的変数
  • 特徴量
  • 使用する機械学習アルゴリズム

①目的変数

今回モデルでは、

着順

を目的変数としています。

ただし、着順をそのまま使用しているのではなく

  • 1着 → 1
  • 2着以降 → 0

というような変換を行った結果を、目的変数としています。

②特徴量

大きく分けて、「レース情報」「競争馬情報(絶対情報)」「競走馬情報(相対情報)」の3つを特徴量としています。

レース情報
  • レースID
  • 開催日
  • 回数
  • 日数
  • レース数
  • レース長
  • 天候
  • 馬場状態
  • レースクラス
  • 競走馬数
競争馬情報(絶対情報)
  • 競走馬ID
  • 性別
  • 年齢
  • 馬体重
  • 馬体重の増減
  • 馬番
  • 人気
  • 単勝オッズ
  • スピード指数
  • 騎手ID
  • 母馬ID
  • 父馬ID
  • 母父馬ID
  • 同じ母馬を持つ競争馬の勝率・連帯率・複勝率
  • 同じ父馬を持つ競走馬の勝率・連帯率・複勝率
  • 同じ母父馬を持つ競走馬の勝率・連帯率・複勝率
  • 過去5Rの通算賞金
  • 過去5Rの勝率・連帯率・複勝率
  • 過去5Rの第一コーナーから着順までの順位変化
  • 過去5Rの第一コーナーから最終コーナーまでの順位変化
  • 過去5Rの最終コーナーから着順までの順位変化
競走馬情報
(レース内の相対情報)
  • 過去5Rの通算賞金の偏差値
  • 過去5Rの勝率・連帯率・複勝率の偏差値
  • スピード指数の偏差値
  • 同じ母馬を持つ競走馬の勝率・連帯率・複勝率の偏差値
  • 同じ父馬を持つ競走馬の勝率・連帯率・複勝率の偏差値
  • 同じ母父馬を持つ競走馬の勝率・連帯率・複勝率の偏差値

特徴量の入手・加工方法

上記特徴量は、「netkeiba.com」から入手したデータをそのまま、もしくは加工して使用しています。

入手・加工を行うソースコードについては、本記事で解説していないので過去記事を参照してください。

過去記事
カテゴリ 対象データ URL
データ入手
(スクレイピング)
レース結果のURL https://ryohei22.com/scraping-get_url-central/
レース結果 https://ryohei22.com/scraping-race_result/
競走馬の競争成績 https://ryohei22.com/scraping-horse_result/
データ加工 大部分の特徴量 https://ryohei22.com/scraping-edit_data/
血統情報 https://ryohei22.com/win-prediction4/
スピード指数 https://ryohei22.com/win-prediction3/

③使用する機械学習アルゴリズム

今回使用する機械学習アルゴリズムは、

LightGBM

です。

LightGBMとは、Microsoftが開発した分散型勾配ブースティングフレームワークのことで、詳細な解説は「最近流行りのLightGBMとは」を参照してください。

LightGBMには下記の特徴があり、実装の容易性と高い回収率が期待できると考え採用しました。

LightGBMの特徴
  • 予測精度が高く、計算時間が短い
  • 決定木をベースとするモデルのため、正規化等の前処理が必要ない
  • 特徴量の重要度を容易に確認できる

ソースコード解説

以降では、「scikit-learn」の

CalibratedClassifierCV

を使用することで、回収率がどのようになるのかをソースコードを交えて解説していきます。

①モデル作成

まずは、確率補正する前のLightGBMモデルの作成方法を解説します。

""" 2. モデル作成 """
# ライブラリ
import optuna.integration.lightgbm as lgb
from lightgbm import LGBMClassifier
from sklearn.calibration import CalibratedClassifierCV, calibration_curve
import matplotlib.pyplot as plt
import numpy as np
import pickle
import math

# LightGBM用のデータセットに変換
lgb_train = lgb.Dataset(x_train, y_train)
lgb_valid = lgb.Dataset(x_valid, y_valid, reference=lgb_train)

# ハイパーパラメータ
params = {'task': 'train',
          'boosting_type': 'gbdt',
          'objective': 'binary',
          'is_unbalance': True,
          'metric': 'binary_logloss',
          'learning_rate': 0.01}

# モデル作成(Optuna)
start = datetime.datetime.now()
model_o = lgb.train(params,
                    lgb_train,
                    valid_sets=(lgb_train, lgb_valid),
                    early_stopping_rounds=100,
                    verbose_eval=100,
                    optuna_seed=100)
end = datetime.datetime.now()
print(str(end - start))

# 学習
best_params = model_o.params
del best_params['early_stopping_round']
lgb_clf = LGBMClassifier(**best_params)
lgb_clf.fit(x_train, y_train)

以降で、各要素について解説していきます。

学習用のデータセットを作成

今回作成するLightGBMのモデルは、Python APIを使用します。

そのため、「lgb.Dataset()」で専用のデータセットを事前に作成します。

# ライブラリ
import optuna.integration.lightgbm as lgb

# LightGBM用のデータセットに変換
lgb_train = lgb.Dataset(x_train, y_train)
lgb_valid = lgb.Dataset(x_valid, y_valid, reference=lgb_train)

モデル作成

続いて、ハイパーパラメータを指定してモデルを作成します。

# ハイパーパラメータ
params = {'boosting_type': 'gbdt',
          'objective': 'binary',
          'is_unbalance': True,
          'metric': 'binary_logloss',
          'learning_rate': 0.01}

# モデル作成(Optuna)
start = datetime.datetime.now()
model_o = lgb.train(params,
                    lgb_train,
                    valid_sets=(lgb_train, lgb_valid),
                    early_stopping_rounds=100,
                    verbose_eval=100,
                    optuna_seed=100)

今回は「Optuna」を利用して、LightGBMモデルを作成しています。

Optunaを利用する理由

自動でパラメータチューニングしてくれるため

なお、自動チューニングしてくれるパラメータは下記の7つです。

  • lambda_la
  • lambda_l2
  • num_leaves
  • feature_fraction
  • bagging_fraction
  • bagging_freq
  • min_child_samples

しかしながら、上記以外のパラメータは手動で設定する必要があるので、「params」という変数で指定しています。

以降で、今回設定したパラメータを解説していきます。

boosting_type

ブースティングのアルゴリズムを選択できます。
基本は勾配ブースティングを行うことが多いので、デフォルトの ‘gbdt’ でよいと思います。

objective

目的関数を選択できます。
今回は2値分類モデルを作成するため、’binary’ を指定しています。

metric

評価指標を選択できます。
2値分類の場合は ‘binary_logloss’、’auc’ が代表的ですが、今回は’binary_logloss’を採用しています。

learning_rate

学習率を指定できます。
値を大きくするほど処理時間は短くなりますが、精度は落ちます。
一般的に、0.01を設定することが多いです。

再学習

先ほどの「Optuna」で自動チューニングしたパラメータを使用して、「scikit-learn API」のLightGBMモデルを作成します。

先ほどの「model_o」でもテストデータの予測自体は可能なのですが、今回の目玉である「CalibratedClassifierCV(予測確率の補正)」は「Python API」のLightGBMモデルに適用できません。

なので二度手間ではありますが、「Optuna」で自動チューニングしたモデルを作成してからハイパーパラメータを取得し、そのパラメータを基に「scikit-learn API」のLightGBMモデルを作成します。

# 再学習
best_params = model_o.params
del best_params['early_stopping_round']
lgb_clf = LGBMClassifier(**best_params)
lgb_clf.fit(x_train, y_train)

確率補正(Calibration)

この箇所が、本記事のメインとなる箇所で、「scikit-learn」の

CalibratedClassifierCV

を使用することで、予測確率の補正を行います。

ソースコードは簡単で、下記変数を「CalibratedClassifierCV」関数の引数に設定するだけです。

  • 学習済みモデル
  • csv = ‘prefit
     → 第一引数が学習済みで、補正モデルに渡すデータは全量補正用であることを指定。
  • method = ‘isotonic’
     →補正方法を指定。詳細な解説は割愛します。

その後に、補正用のデータを指定して「fit」関数を適用させれば、確率補正が完了します。

# Calibration
calib = CalibratedClassifierCV(lgb_clf, cv="prefit", method="isotonic")
calib.fit(x_valid, y_valid)

# Calibrationの結果を出力
plot_calibration_curve(dict(LGBM=lgbm, CalibratedLGBM=calib), x_test, y_test)

また、補正した結果を「plot_calibration_curve」という関数で出力していますが、これは下記記事で紹介されているメソッドをそのまま流用しています。

出力結果は以下のとおりで、きれいに補正されていることが分かりますね。

②モデルの評価

続いて、先ほど補正したモデルでテストデータの予測を行い、その回収率を計算・プロットします。

# テストデータについて、1着になる確率を予測
y_pred_prob = pd.DataFrame(calib.predict_proba(x_test))

# 予測結果を整理
df_pred = pd.DataFrame(data={'race_ID': x_test['race_ID'],
                             'y_pred_prob': y_pred_prob.iloc[:, 1],
                             'win_odds': odds_df_test['win_odds'],
                             'e_value': y_pred_prob.iloc[:, 1] * odds_df_test['win_odds'],
                             'y_test': y_test[0]})

# 回収率を計算
step_list = [x * 0.02 for x in range(0, 31)]
return_rate_list = []

for cnt in range(0, len(step_list)):
    step = step_list[cnt]
    df_pred['y_pred_class'] = [1 if x >= step else 0 for x in df_pred['y_pred_prob']] 
    input_amount = len(df_pred.loc[df_pred['y_pred_class'] == 1, 'y_pred_prob']) 
    hit_odds_list = list(df_pred.loc[(df_pred['y_pred_class'] == df_pred['y_test']) &\
                                     (df_pred['y_pred_class'] == 1),
                                     'win_odds'])
    return_amount = sum(hit_odds_list)
    if input_amount > 0:
        return_rate_list.append(return_amount / input_amount)
    elif input_amount == 0:
        return_rate_list.append(float('nan'))

# 回収率を描画
x = step_list
y = return_rate_list
fig, ax = plt.subplots(1, 1, figsize=(10, 8))
ax.plot(x, y, linestyle='solid', color='k', marker='^', label='race_cnt')
plt.title('Return_Rate', fontsize=20)

# 軸ラベルを追加
ax.set_ylabel('return_rate', fontsize=15)

# y軸設定
ax.set_ylim(0.5, 3)

# 値ラベルを追加
for i, k in zip(x, y):
    if math.isnan(k):
        continue
    elif k < 0.5 :
        continue
    else:
        plt.text(i, k + 0.05, str(round(k * 100)) + '%', fontsize=10)  

以降で、各要素の詳細を解説します。

テストデータを予測

テストデータの予測はとても簡単で、

LightGBMモデル.predict_proba()

と記述することで計算できます。

# テストデータについて、1着になる確率を予測
y_pred_prob = pd.DataFrame(calib.predict_proba(x_test))

回収率を計算

下記ロジックで賭けると仮定して、回収率を計算します。

予測確率がxx%を超えたら、その競争馬に単勝を賭ける

本処理では、上記xxの値を0%~60%の間で2%刻みで計算しています。

# 回収率を計算
step_list = [x * 0.02 for x in range(0, 31)]
return_rate_list = []

for cnt in range(0, len(step_list)):
    step = step_list[cnt]
    df_pred['y_pred_class'] = [1 if x >= step else 0 for x in df_pred['y_pred_prob']] 
    input_amount = len(df_pred.loc[df_pred['y_pred_class'] == 1, 'y_pred_prob']) 
    hit_odds_list = list(df_pred.loc[(df_pred['y_pred_class'] == df_pred['y_test']) &\
                                     (df_pred['y_pred_class'] == 1),
                                     'win_odds'])
    return_amount = sum(hit_odds_list)
    if input_amount > 0:
        return_rate_list.append(return_amount / input_amount)
    elif input_amount == 0:
        return_rate_list.append(float('nan'))

回収率を描画

計算した回収率をグラフ化します。

  • 横軸:予測確率(1着になる確率)
  • 縦軸:回収率
# 回収率を描画
x = step_list
y = return_rate_list
fig, ax = plt.subplots(1, 1, figsize=(10, 8))
ax.plot(x, y, linestyle='solid', color='k', marker='^', label='race_cnt')
plt.title('Return_Rate', fontsize=20)

# 軸ラベルを追加
ax.set_ylabel('return_rate', fontsize=15)

# y軸設定
ax.set_ylim(0.5, 3)

# 値ラベルを追加
for i, k in zip(x, y):
    if math.isnan(k):
        continue
    elif k < 0.5 :
        continue
    else:
        plt.text(i, k + 0.05, str(round(k * 100)) + '%', fontsize=10)  

出力されたグラフは下記のとおりで、予測確率が40%を超える競争馬に賭けることで回収率が140%を超える結果になりました!

しかしながら、予測確率が40%を超える競走馬の数は少ないので、まだ改善の余地がありそうです。。

また過去モデルの回収率と比較してみると、全体的に回収率は向上しているので今回行った確率の補正は、効果が大きいですね!

過去モデルの回収率

※横軸が「1着になる確率」、縦軸が「y_pred_probがある値を超えた競争馬に単勝を賭けた時の回収率」を表しています。

コメント

タイトルとURLをコピーしました