今日も窓辺でプログラム

外資系企業勤めのエンジニアが勉強した内容をまとめておくブログ

TensorFlowでボートレースの予想しようとして失敗した話

はじめに

以前は日経平均が上がるか下がるかを予測していましたが、少し気分を変えてボートレースの結果を予測してみようと思い立って実装しました。
結論からいうと、今回は非常に単純なモデルを使い、訓練データに対しては完璧な精度を出したのですがテストデータに対してはどのような賭け方をしても赤字、というような結果になっています。

もし何かの参考になればと思い、記事にしておきます。

やりたいこと

ボートレースのレース結果をニューラルネットを使って予測して小遣いを稼ぐ!
以前投稿した株価予測シリーズと同様、なんとかお小遣いを増やせないか、という取り組みの一環です。
ボートレースの舟券はネット経由で購入できるので、よさそうなモデルができたら券を即購入できるわけです。

詳細は後述しますが、ボートレースの1~3位の結果を予測し、その結果に基づいて舟券を買った場合の損益についてバックテストをして実験します。

ボートレースの基礎知識

私自身、数日前までボートレースの知識は皆無でした。ネットで軽くは調べましたが、現時点でもボートレース公式サイトの解説ページの知識のみです。私は抑えた点は、ボートレースは6艇で行われるという点と、舟券の買い方です。
ボートレースでは、次のような券の買い方があるようです。

単勝 1着の艇を当てる
複勝 2着までに入る艇当てる
2連単 1・2着の艇を着順通り当てる
2連複 1・2着の艇を当てる
3連単 1・2・3着の艇を着順通り当てる
3連複 1・2・3着の艇を当てる
拡連複 1~3着までの2艇を当てる

他にもながしボックスという買い方もあるようですが、今回はこの2つの買い方は無視してモデルの評価を行います。

要するに、ボートレースは6艇で行われる競技で、そのうち上位3位に入る艇をうまく予想できれば儲かりそう、という点が大事です。

入手可能なデータ

公式サイトで様々なデータが公開されていますが、今回はその中でもレースの成績をダウンロードして使用します。
レース結果には、参加した選手の選手番号、着順、タイム、レース会場などの情報が保存されています。
試しにサイトにアクセスしてデータをダウンロードすると分かりますが、結果はテキストファイルで保存されています。なのでそれを気合でパースします。

また、今回の記事では、2016年1月1日~2016年12月27日までのレース結果(約5万レース)を4:1に分割してそれぞれ学習データとテストデータとして使用します。
参考までに、汚いコードですが私の書いたものはこちらです。

単純なモデルを構築する

今回はまずは非常にシンプルなモデルを試します。
まず、選手一人は選手番号をone-hot表現で保持したベクトルで保持します。1レース6艇なので、そのベクトルを6選手分つなげたベクトルが入力のベクトルとなります。
今回の私の使用した範囲のデータだと、選手が1627人だったので、入力は9762次元となります。

出力は、1位~3位さえ予測できればよかったです。1位の予測には6次元のベクトルで、それぞれの要素が1番艇~6番艇が1位となる確率を保持しているものとします。2位と3位についても同様の出力を用意し、合計で18次元のベクトルが出力となります。
損失関数は、1~3位のそれぞれの予想に対してクロスエントロピーを計算し、その和を損失としました。

入力と出力をつなぐ隠れ層は1層で隠れ層の数は1000個とました。今回は隠れ層の数の調整は行っていないので、これが適切な数かはよくわかりません。

モデルの実装

以上のモデルをTensorFlowで実装しました。ソースコードはこちらにアップしてあります。
ちょっと長くて読みにくいソースコードですが、極力以前と同様にinference, loss, trainingの3部分に分割して書くようにしています。
inferenceの部分が層をつないでいる部分、lossが損失関数を定義しています。実際の学習はtrainを走らせることで実行されます。

実装で少し注意した点としては、one-hot表現はnumpyの配列などを使って表現したりせずにtf.one_hotというクラスを使用した点です。
最初は入力をnumpy.zerosなどを使って自分で作っていたのですが、この場合1万個近い要素を持つ配列を保持するため、メモリが結構かつかつになります。というか、私の手元のPCだと1年分~2年分のデータに対して走らせただけでメモリ不足で学習が落ちるという状況でした。
それを解消するために、入力データは選手番号の配列(例えば[1000, 1001, 1002, 1003, 1004, 1005]など)を保持しておいて、実際にTensorFlowのsessionを走らせるときにこれをone-hot表現に変換します。具体的なコードはこんな感じです。

def convert_input(self, input_data):
    onehots = tf.map_fn(lambda x: tf.one_hot(x, self.race_results.get_input_length(), dtype=tf.int32), input_data)
    return tf.cast(tf.reshape(onehots, [-1, self.race_results.get_input_length() * 6]), "float")

input_dataには[1000, 1001, 1002, 1003, 1004, 1005]というような選手番号を6つ持った配列が渡されます。コード中2行目のself.race_results.get_input_length()は選手数を返してくる関数で、この2つの情報をtf.one_hotに渡すことでone-hotベクトルを生成しています。
3行目では6つの異なるone-hotベクトルを1つのベクトルにまとめて、その後の処理のためにfloatにキャストしています。

以上の実装を使い、バッチサイズは128として30エポック学習を回した際の損失関数の変化は次のようになりました。なにやらものすごくよい感じで学習が進んでいるように見えます。ちなみに、手元のマシンで1時間ほどかかりました。
f:id:kanohk:20161230211929p:plain

モデルの精度の評価

次にモデルの精度を評価します。予測は1位~3位の艇番号を出力しているので、その結果が特定の舟券の買い方のもと買ったか負けたかを評価します。たとえば、
予測が1-3-2の順で、実際の結果が1-3-4の場合は、「単勝、複勝、2連単、2連複、拡連複」の場合は正解ですが、「3連単、3連複」の場合は不正解、といった感じです。
このような感じで、舟券の買い方ごとに精度を算出します。

そのため、ソースコードにはaccuracy_*という形の関数がたくさん定義してあります。
例えば、単勝用の精度評価はaccuracy_winで、複勝の精度はaccuracy_placeで行っています。

最も単純な単勝の場合だけソースコードを紹介しておきます。ほかの実装も興味がある方は、GitHubのほうを参照してください。

    # 単勝
    def accuracy_win(self, output, actual_labels):
        # 出力の最初の3分の1(つまり、1位の部分)を取り出す
        osize = self.output_layer_size // 3
        first = tf.slice(output, [0, 0], [self.batch_size, osize])

        # 正解データの最初の3分の1(1位部分)も同様に取り出す
        label1 = tf.slice(actual_labels, [0, 0], [self.batch_size, osize])

        # 単勝の場合は、上記2つが一致していれば正解
        correct = tf.equal(tf.argmax(first, 1), tf.argmax(label1, 1))
        accuracy = tf.reduce_mean(tf.cast(correct, "float"))
        return accuracy

以上のような形で精度の評価を行ったら、次のような結果が得られました。

#train: 40192 #test: 10112
*** Evaluation on train ***
単勝: 1.0
複勝: 1.0
2連単: 0.992188
2連複: 0.992188
3連単: 0.992188
3連複: 0.992188
拡連複: 0.992188

*** Evaluation on test ***
単勝: 0.414063
複勝: 0.609375
2連単: 0.132813
2連複: 0.164063
3連単: 0.0546875
3連複: 0.101563
拡連複: 0.304688

学習データに対してはほぼ完ぺきな予測をしているが、テストデータに対しては全然だめ、という結果です。

念のためバックテストも…

全然だめとはいっても、単勝や複勝では4割や6割の正解率なので、念のためバックテストを行ってみました。
それぞれの舟券の買い方でテストデータの対象レースの舟券をすべて100円で買った場合の損益がこちらになります。…全敗ですね。
対象のレース数が10112レース、購入金額が101万円なので、単勝や複勝で買い続けた場合は購入金額に対して約1割程度負けている計算になります。

単勝: -130440
複勝: -86040
2連単: -268020
2連複: -13830
拡連複: -155790
3連単: -352400
3連複: -286990

実装はbacktestという関数を参照してください。
中身は単純で、それぞれのレースに対して不正解だったら100円マイナス、正解だったら最初頑張ってパースした払戻金をもとにいくら買ったのかを計算して、その合計値を求めています。

まとめ

今回はボートレースの1着~3着の艇をを下記の単純なニューラルネットワークで予測しようと試みました。

  • 入力は選手番号のone-hotベクトルを6個つなげたもの
  • 出力は各艇が1着~3着になる確率を意味する18次元のベクトル
  • 隠れ層は1層で1000個

結果は、学習データに対してはほぼ100%の精度で予測できたが、テストデータではボロボロ(1位~3位を正確に当てたのは約5%)、バックテストでも赤字という結果でした。
改善点はいくつかあり、

  • モーターとボートの情報も使用する
  • 選手を保持するベクトルを工夫する。たとえば、選手別のデータを使用して、より密で低次元なベクトルに何とか落とし込む、など。
  • 今回はレース場やレースのグレードなどの情報は使用していないので、それらの情報も学習時に使用する

などが考えられます。モチベーションが続いたらそれらについても試して結果を共有できればと思っています。

(2017/1/1追記) レース場、モーター、ボートの情報も追加して同様の方法で学習させてみましたが、結果は同じような形でした。対象とするレース結果も2016年1年分から2015~2016年の2年分に広げましたが、結果は変わらずでした。。

ソースコード

今回のソースコードはGitHubで公開しています。raceresults.pyがレース情報のダウンロード・パース部分で、model.pyがtensorflowを使用した実装部分です。
github.com