Simple gadget life programming diary

Simple gadget life の中の人によるプログラミングメモ

機械学習で競馬・フェブラリーステークスを予想してみた。

f:id:jtaka1012:20190222084740p:plain
何度かチャレンジして、ようやく形らしいものになったので、備忘録的に残しておこうと思います。
機械学習の初心者のやってみた記事です。

1着から5着までの馬が当たりました!(着順通りではないです)

問題の設定

1着から3着までに入りそうな馬を当てるというものです。
過去レースを用いて、3着以内にゴールしたものを答えとして学習し、それを用いて予想を行います。

問題設定時に考えたこと

競馬は出走頭数がレースによりバラバラです。
扱い方次第ですが、欠損値が多く発生します。そこで、タイムのような特性だと線形データとしてうまく扱えなそうだなと思い、単純な分類問題として扱うことにしました。

取得データ

学習に用いるデータはnetkeiba.comさんのデータを用いて学習を行いました。
また、クローリングの際は以下のコードを参考にすすめていきました。 github.com

ライブラリはBeautifulSoupを使います。

今回の取得対象はフェブラリーステークスと同一競馬場、同一距離のコース(東京競馬場、ダート1600m)かつ、1000万円以上のレースを300件分取得します。
このような条件にしたのは、賞金が低いと馬の過去の出走数が低かったりとデータの欠損値が多くなりそうだと推測したためです。(これならデータが割と取れそう) この条件の場合、10年前のレースも対象となってきます。

最終的に取得したデータは以下になります。 このあたりは、元となるプログラムを参考に少し改良したもので、これからも改良していきたい部分です。

①レースデータ

f:id:jtaka1012:20190222073450p:plain 対象レースで取得するデータが赤枠の内容です。 出走した馬と着順を教師データとして使用。斤量、馬場状態については、学習時に使用します。

②レースに出場した馬の過去5戦分の戦績

f:id:jtaka1012:20190222073533p:plain ①を元に各馬の過去5戦分のデータを取得します。

③ジョッキーの該当レースの年の成績

f:id:jtaka1012:20190222073705p:plain こちらも①からジョッキーを割り出し、その年の勝利数を取得していきます。

最終的なデータは、

学習データ:
②+③を各レース毎に1行にしたデータ

教師データ:
①を元に、馬番順に並び替え、1着〜3着の馬番に「1」、残りを「0」としたデータです。

前処理

東京ダート1600Mは最大16頭のレースなので、16頭に満たないときはデータを0埋め。 その他、学習データで欠損値がある場合も0埋めしています。 さらに、各データが0〜1の範囲となるように正規化しています。

MLP

Keras (TensorFlow) を用いて学習を行いました。

MLP実装

ほぼ、こちらの参考記事と同様になります。 傾向をみていると、やはりエポック数が30あたりから過学習をしているように見えたので、30で止めています。

# -*- coding: utf-8 -*-
import keras
from keras.models import Sequential
from keras.layers import Dense, Dropout
from keras.optimizers import Adam
from keras.layers.normalization import BatchNormalization
from keras.callbacks import EarlyStopping, TensorBoard, ModelCheckpoint
import matplotlib.pyplot as plt
import horse_data  # テストデータ、教師データの取得を行うプログラム
import pandas as pd

TRAIN_FILE = "train.txt"  # 教師データ名
TEST_FILE = "test.txt" # 学習データ名
ELEMENT_COUNT = 2417

# 学習履歴を表示
def plot_history(history):
    print(history.history.keys())

    # 精度の履歴をプロット
    plt.plot(history.history['acc'])
    plt.plot(history.history['val_acc'])
    plt.title('model accuracy')
    plt.xlabel('epoch')
    plt.ylabel('accuracy')
    plt.legend(['acc', 'val_acc'], loc='lower right')
    plt.show()

    # 損失の履歴をプロット
    # plt.plot(history.history['loss'])
    # plt.plot(history.history['val_loss'])
    # plt.title('model loss')
    # plt.xlabel('epoch')
    # plt.ylabel('loss')
    # plt.legend(['loss', 'val_loss'], loc='lower right')
    # plt.show()

def pred(X):
    score = list(model.predict(X.reshape(1,ELEMENT_COUNT))[0])
    result = pd.DataFrame([], columns=['num','score'])
    result['score'] = score
    result['num'] = list(range(1,17))
    print(result.sort_values(by='score', ascending=False))

train_horse, train_label = horse_data.get_horse_and_label(TRAIN_FILE)

model = Sequential()
model.add(Dense(ELEMENT_COUNT, activation='relu', input_dim=ELEMENT_COUNT))
model.add(Dropout(0.8))
model.add(BatchNormalization())
model.add(Dense(1280, activation='relu'))
model.add(Dropout(0.5))
model.add(BatchNormalization())
model.add(Dense(16, activation='softmax'))

model.compile(loss='categorical_crossentropy',
                            optimizer=Adam(),
                            metrics=['accuracy'])

model.summary()

# 過学習に対する早期切り上げの設定
early_stopping = EarlyStopping(patience=0, verbose=1)

# バッチサイズ
batch_size = 5

# エポック数の定義
epoches = 30

# 学習
history = model.fit(train_horse, train_label,
                                        epochs=epoches,
                                        batch_size=batch_size,
                                        validation_split=0.1)

# 評価
test_horse, test_label = horse_data.get_horse_and_label(TEST_FILE)
pred(test_horse)

score = model.evaluate(test_horse, test_label, verbose=0)
print('Test loss:', score[0])
print('Test accuracy:', score[1])


# 学習履歴をプロット
plot_history(history)

出力結果とレース結果

出力結果

    num     score
7     8  0.120262
1     2  0.111644
5     6  0.092470
2     3  0.090559
10   11  0.083663
4     5  0.067724
3     4  0.059051
8     9  0.057948
11   12  0.056728
6     7  0.054262
9    10  0.046802
13   14  0.042381
12   13  0.041633
0     1  0.035678
15   16  0.023597
14   15  0.015599
Test loss: 7.9274187088012695
Test accuracy: 0.0

レース結果

f:id:jtaka1012:20190222075442p:plain

1〜4着までは当たっているので、3連複で4つの馬を買っていれば、当たったことになります。 15、16番の馬は存在していないため、傾向的にも正しそうです。(工夫の余地は多分にありますが。)

まだまだ勉強不足な中での結果で、まぐれ当たりなことは十分理解していますが、それでも結果が出たことはとても嬉しいものでしたし、可能性はあるのだなということがわかりました。 もう少し学習を深めていって、どういうことなら精度がでるのかなどを探ってみたいと思います。

参考にした記事

qiita.com