機械学習 (Python) を使って競馬で稼ぎたい

今回、競馬について素人が機械学習を使ってどれぐらい予想できるかをやってみました。適当な予想プラス競馬について全く知識を持ち合わせてないので、遊び感覚としてみてもらえたらなと思います。

今回は、CMでよく流れていた有馬記念を予測してみました。

実行環境

  • macOS バージョン 10.13.5
  • Python 3.6.7
  • jupiter

大まかな流れ

まずは、有馬記念の出走馬がわかるページから出走馬のurlをスクレイピングします。下の赤く囲っているところからスクレイピングします。

次に各馬のレースのURLを取得します。今回は、2018年のデータだけにしたいので水色の所を取得します。

一番最初みたいなページに飛ぶので、各馬のホームページに飛べるように、さっきと同じ作業をします。

各馬のページに飛んだら、ここでやっと、馬の特徴量を取得します。馬の特徴量は、年や性別、体重、順位、コースの適正など、様々考えられますが、自分は競馬について全く知識がないので、netkeibaさんのホームページの馬の評価を参考に馬のデータを取得しました。下の囲っているところを馬の特徴量としました。

またレースについての特徴量もとってきたい所です。そこで、さきほどレースのページに飛んだときの囲った所をレースの特徴量にしようと思います。そこで今回の特徴量は、芝かダート、右か左、距離、天候、馬場の状態とします。

これらの特徴量をうまく連結したいと思います。
そこで、簡単に3頭のレースの時の連結を考えます。

この時、データは [ [馬1+馬2+レース], [馬1+馬3+レース], [馬2+馬3+レース], [馬3+馬2+レース], [馬3+馬1+レース], [馬2+馬1+レース] ]と出力するようにします。この規則性で各レースからデータを抽出します (分かりにくかったらすいません) 。

以上の流れを最初の有馬記念のページの16頭分全てします。
またKerasにかける時、以上のデータをX_listとしY_listは、馬+馬+レースの特徴量の並びの最初の馬が勝った場合1と出力、負けた場合0と出力するようにします。

データの取得

今回、以上の流れをAidemyでやったBeautifulSoupでスクレイピングしようと思います。
まずは、馬のURLを取得します。

import requests
from bs4 import BeautifulSoup
import re
import pandas as pd

def horse_page_url(url):
    req = requests.get(url)
    BeautifulSoup(req.content, 'html.parser')
    soup = BeautifulSoup(req.content, 'html.parser')
    table1 = soup.find('table', attrs={'class':'race_table_01 nk_tb_common'})
    table2 = soup.findAll('td', attrs={'class':'txt_l'})
    all_data_keiba=[]
    for a in table2:
        b=a.find('a')
        if b != None:
            c=b.get('href')
            if "horse" in c:
                print(c)
                data=race_page_url(c)
                for i in range(len(data)):
                    all_data_keiba.append(data[i])
    return all_data_keiba

各馬のページからレースのURLを取得します。

def race_page_url(url):
    req = requests.get(url)
    BeautifulSoup(req.content, 'html.parser')
    soup = BeautifulSoup(req.content, 'html.parser')
    race_table = soup.findAll("table")[4]
    race_table1 = race_table.find_all("a")
    all_data=[]
    if race_table1==[]:
        race_table = soup.findAll("table")[3]
        race_table1=race_table.find_all("a")
    for a in race_table1:
        b=a.get('href')
        if "race" in b
            if "2018" in b:
                if "list" in b or "sum" in b or "movie" in b:
                    b=b
                else:
                    if b.islower() == True:
                        b='https://db.netkeiba.com' + b
                        race=race_data(b)
                        horse=horse_page_url2(b)
                        for i in range(len(horse)):
                            horse[i]=horse[i]+race
                        for i in range(len(horse)):
                            all_data.append(horse[i])
    return all_data

レースのページからレースの特徴量を得ます。

def race_data(url):
    req = requests.get(url)
    BeautifulSoup(req.content, 'html.parser')
    soup = BeautifulSoup(req.content, 'html.parser')
    race_data=[]
    race_name = soup.find("p",attrs={"class":"smalltxt"})
    print(race_name)
    race_table = soup.findAll("table")[0]
    race_table2= soup.findAll("span")[6]
    text=str(race_table2.text)
    post_code = re.findall(r'\d+', text)
    code=int(post_code[0])
    race_data.append(code)
    race=[]
    if "芝" in race_table2.text:
        race_data.append(0)
    elif "ダ" in race_table2.text:
        race_data.append(1)

    if "右" in race_table2.text:
        race_data.append(0)
    elif "左" in race_table2.text:
        race_data.append(1)
    if "障" in race_table2.text:
        race_data.append(2)

    if "晴" in race_table2.text:
        race_data.append(0)
    elif "曇" in race_table2.text:
        race_data.append(1)
    elif "雨" in race_table2.text:
        race_data.append(2)

    if "良" in race_table2.text:
        race_data.append(0)
    elif "稍" in race_table2.text:
        race_data.append(1)
    elif "重" in race_table2.text:
        race_data.append(2)
    
    if "中山" in race_table2.text:
        race_data.append(0)
    else:
        race_data.append(1)

    return race_data

レースのページから各馬のURLを取得し、馬の特徴量を取得します。

def horse_page_url2(url):
    req = requests.get(url)
    BeautifulSoup(req.content, 'html.parser')
    soup = BeautifulSoup(req.content, 'html.parser')
    table1 = soup.find('table', attrs={'class':'race_table_01 nk_tb_common'})
    table2 = soup.findAll('td', attrs={'class':'txt_l'})
    all_data=[]
    data=[]
    i=0
    for a in table2:
        b=a.find('a')
        if b != None:
            c=b.get('href')
            if "horse" in c:
                c='https://db.netkeiba.com' + c
                all_data.append(horse_data(c))
                
    for i in range(len(all_data)-1):
        for k in range(i+1,len(all_data)):
            data.append(all_data[i]+all_data[k])
    
    all_data2=all_data[::-1]
    
    for i in range(len(all_data2)-1):
        for k in range(I+1,len(all_data2)):
            data.append(all_data2[i]+all_data2[k])
    return data

馬の特徴量としている部分が書かれていない馬のウェブページもあるため、その馬は [0, 58, 58, 58, 58, 58] と出力する。

def horse_data(url):
    req = requests.get(url)
    BeautifulSoup(req.content, 'html.parser')
    soup = BeautifulSoup(req.content, 'html.parser')
    race_table2=soup.findAll("table")[0]
    horse_name=soup.find("div",attrs={'class':'horse_title'})
       race_table4=soup.find("p",attrs={'class':'rate'})
        a=float(race_table4.text)
        data=[]
        all_data=[]
        data.append(a)
        for i in range(25):
            if i % 5 == 1:
                race_table3=str(race_table2.findAll("img")[i])
                data.append(int(re.findall(r'\d+', race_table3)[1]))
    return data

抽出した特徴量を大まかな流れで言ったように連結するようにデータを出力するようにします。これでデータの出力は完成です。

Kerasを使って予想

MLPでKerasを使って予想。

X_testは有馬記念の出走馬の特徴量とレースの特徴量を先ほどと同じ要領で連結した値とする。
活性化関数として、ReLUとソフトマックス関数を使用した。ReLuは、入力した値が0以下の場合0、0以上の場合その値を出力する関数であり、ソフトマックス関数は、出力層で使われる関数である。

from keras.models import Sequential
from keras.layers import Activation, Dense
from keras.utils.np_utils import to_categorical
from keras.models import Sequential
from keras.layers import Dense, Dropout
from keras.optimizers import Adam
from keras.layers.normalization import BatchNormalization
import numpy as np
import keras

X = np.array(X_list)
# Yは0と1からなるリスト
Y = to_categorical(Y_list)

# モデル
model = Sequential()
# 全結合層(18を500に)
model.add(Dense(input_dim=18, output_dim=500))
# 活性化関数(ReLu関数) 
model.add(BatchNormalization())
model.add(Activation("relu"))
# 全結合層(500を2に) 
model.add(Dense(output_dim=2))
# softmax関数
model.add(Activation("softmax"))
# コンパイル
optimizer = Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=None, decay=0.0, amsgrad=False)
model.compile(loss="categorical_crossentropy", optimizer=optimizer, metrics=["accuracy"])
# 実行
model.fit(X, Y, nb_epoch=300, batch_size=50,validation_split=0.1)

# 予測
results = model.predict_proba(np.array(X_test))
# 結果
print("Predict:\n", results)

lossが低くならなかった。また振れ幅が大きくなった。そこで、Dropoutを入れてみる。

# モデル
model = Sequential()
# 全結合層(18を500に)
model.add(Dense(input_dim=18, output_dim=500))
# 活性化関数(ReLu関数) ドロップアウトを0.4にする
model.add(BatchNormalization())
model.add(Activation("relu"))
model.add(Dropout(0.4))
# 全結合層(500を2に) ドロップアウトを0.2にする
model.add(Dense(output_dim=2))
model.add(Dropout(0.2))
# softmax関数
model.add(Activation("softmax"))
# コンパイル
optimizer = Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=None, decay=0.0, amsgrad=False)
model.compile(loss="categorical_crossentropy", optimizer=optimizer, metrics=["accuracy"])
# 実行
model.fit(X, Y, nb_epoch=300, batch_size=50,validation_split=0.1)

# 予測
results = model.predict_proba(np.array(X_test))
# 結果
print("Predict:\n", results)

ドロップアウトを入れることによって、平均的にaccuracyとlossの値がよくなった。また、振れ幅も小さくなり、良い予想ができると期待し採用。

ドロップアウトを入れることにより、深層学習の場合、前の層の答えを0にして伝えることで、過学習が緩和され、精度が良くなる。また、上記のモデルは、accuracyの値がよくなるように間の層の出力数やドロップアウトの数値を試行錯誤して決定した。

結果は

という感じになる。ここでPredictのリストの値は例の[馬1+馬2+レース]の予想である。右の数字は馬1の値とし、左の数字は馬2の値とする。今、有馬記念は16頭で、馬番に合わせて値を足してみる。

lossがあまり低くならなかったが、目をつむることにする。

結果

l=16
q=[]
p=[]
results2=results[::-1]
for i in range(l):
    s1=0
    b1=0
    c1=0
    n1=i
    b1=(i)*(l-1)-sum(range(1, n1))
    c1=(i+1)*(l-1)-sum(range(1, n1 + 1))
    a1=l-1
    for k in range(b1,c1):
        s1+=results[k][1]
    for k in range(n1):
        s1+=results[n1-1][0]
        a1-=1
        n1+=a1
    q.append(s1)
print(q)

for i in range(l):
    s2=0
    b2=0
    c2=0
    n2=i
    b2=(i)*(l-1)-sum(range(1, n2))
    c2=(i+1)*(l-1)-sum(range(1, n2 + 1))
    a2=l-1
    for k in range(b2,c2):
        s2+=results2[k][1]
    for k in range(n2):
        s2+=results2[n2-1][0]
        a2-=1
        n2+=a2
    p.append(s2)
p=p[::-1]
print(p)
for i in range(l):
    print(i+1,q[i]+p[i])

馬番ごとに出した値が上のようになる。これをみると、1のオジュチョウサン、5のパフォーマプロミス、10のミッキースワローが上位にくると予想。14と15も良い成績を残すことも考えられる。

また、元のデータを天皇賞(秋)にして同じように機械学習すると

12のレイデオロ、15のシュヴァルグランが上位にくると予想しており、5 ,8 ,9 ,14 ,16も良い成績を残すと考えられる。順位で当てるのは難しいと考え、3連複で買うことを考える。実際は、3連複1-5-10 ,5-12-15 ,12-14-15 ,12-15-16を買いました。

しかし、結果は8-12-15で全て外れました。そんなに甘くないと痛感。ただ天皇賞(秋)のデータの予想はまだ良い予想だったんじゃないかと。。。競馬素人にとってはなかなかのできだったのではないだろうか。

考察

今回の結果を踏まえて、すべての特徴量は絶対値で行ったところをデータの列ごとに平均0, 分散1となるように正規化して機械学習してみる。ドロップアウトを書いているモデルのX_train、X_testを正規化してみた。

from sklearn import preprocessing
model = preprocessing.StandardScaler()
X = model.fit_transform(X_train)
from sklearn import preprocessing
model = preprocessing.StandardScaler()
X_test = model.fit_transform(X_test)

上のソースコードを追加してみた。すると、、、

AccuracyとLossの値がよくなった。また、振れ幅も小さくなり良さげ。
ちなみにこの時の結果は、、、

8のブラストワンピースが上位に!!!
1のオジュチョウサンがまだ高い。障害のレースを抜けば良い感じになったのかも。。

今後

先ほど少し言いましたが、オジュチョウサンは障害馬であり、障害のレースデータを抜くべきだったのではと思います。天皇賞(秋)のデータには障害のレースデータが含まれておらず、良い予想ができたのではないかと。今後、考察や反省を踏まえれば良い予想ができる予感。上手くいかないと思いますが、、、

今後の課題は、以下のようなことが考えられた。

  1. 騎手のデータを入れる。今回、馬の特徴量とレースの特徴量で予測したため、騎手のデータを入れることを考える。
  2. 馬の特徴量を増やす。今回のものに馬の性別、年、体重、血統を入れる。ただ、血統が重要だとは聞くが、どのような形で入れるか難しいところである。
  3. 過去の同じタイトルでどのような馬が勝つか考える。今回は、出走馬の過去レースで機械学習を行ったが、同じタイトルでどのような馬が勝つかを予想し、似た馬を探すという別の視点からも考える。

競馬素人の奮闘ぶりはいかがだったでしょうか。楽しんで見てくだされば幸いです。

参考文献

参考: Qlita 「大井競馬で帝王賞を機械学習で当てた話」
最後までご覧くださりありがとうございました。

プログラミング未経験からでもAIスキルが身につくAidemy Premium




PythonやAIプログラミングを学ぶなら、オンライン制スクールのAidemy Premiumがおすすめです。
「機械学習・ディープラーニングに興味がある」
「AIをどのように活用するのだろう?」
「文系の私でもプログラミング学習を続けられるだろうか?」
少しでも気になることがございましたら、ぜひお気軽にAidemy Premiumの【オンライン無料相談会】にご参加いただき、お悩みをお聞かせください!