はじめに
不労所得での生活は全人類の夢です。そんな夢を叶えるためにグラフニューラルネットワークを使って競馬の予測AIにチャレンジしてみました。なお、私自身もそんなにGNNに詳しいわけではないので間違えがあれば教えていただけると助かります。
GNN(グラフニューラルネットワーク)とは
そもそもGNNって何よって人のために簡単な説明をすると
この画像のようにノード(まる)とエッジ(線)からなる構造のことをグラフ構造といいます。
グラフニューラルネットワークとはこの各ノードがもつ情報を伝言ゲームのように隣のノードに伝えていくことで、各ノードの情報を伝搬し、学習するモデルになります。
わかりにくい場合はCNNのグラフ版と思っていてくれればそれで大丈夫です。
詳しい解説を知りたい人はこちら
が非常にわかりやすいです。
GNNを使う目的
競馬のレースをグラフとして考えた時に
このような馬をノード、馬同士の関係性をエッジとしたようなグラフとすることで、全ての馬の情報を考慮した上での勝つ馬が予測できるのではないかと考えてGNNを採用しました。
アプローチ方法
今回GNNでの予測方法としてのアプローチ方法としてグラフ分類を考えました。
最大の馬数を18頭と仮定したときに、正解ラベルとして一位になった馬の番号を与えることで、18クラスのグラフ分類問題として設定しました。
ひとまず期待値などは考えず、一位の馬を予測することだけを目的としました。
全体の流れ
- レース情報と馬の情報をnetkeiba.comからスクレイピング
- 全体のデータを前処理
- 各レースごとにグラフを作成
- GCNを使って学習を行いモデルを作成
- 最終的な予測精度を確認する
スクレイピング
qiitaには優秀な人たちがすでに素晴らしいスクレイピングのコードを上げてくださっているのでここでは割愛します。
スクレイピングの範囲は2008〜2021の13年間としました。
利用する特徴量
特徴量名 | 概要 | エンコード方法 |
---|---|---|
number | 枠番 | 数値 |
horse_num | 馬番 | 数値 |
horse_name | 馬名 | ordinary encoding |
jockey | 騎手 | ordinary encoding |
trainer | 調教師 | ordinary encoding |
sex | 性別 | onehot encoding |
old | 年齢 | 数値 |
weight | 斤量 | 数値 |
horse_weight | 馬体重 | 数値 |
horse_weight_diff | 馬体重の変化量 | 数値 |
win_rate | 単勝オッズ | 数値 |
popularity | 人気 | 数値 |
sub_title | 何歳以上何百万以下など | onehot encoding |
round | 何ラウンド目か | 数値 |
race_type | 芝かダートか | onehot encoding |
condition | 馬場の状態 | onehot encoding |
weather | 天気 | onehot encoding |
race_line | inかoutか | onehot encoding |
distance | 距離 | onehot encoding |
location | どこ開催か | onehot encoding |
leg | 脚質 | 数値 |
total winning | その時点での獲得賞金 | 数値 |
last day | 前回から何日経過したか | 数値 |
前処理
基本的にはエンコード方法の通りに行っています。
グラフの作り方
基本的にはレースごとに前処理をした後networkxを使ってグラフに変換します。
前処理にはcategory_encodersを利用しました。
後で考えると全体に前処理してからグラフにしたほうが良かった気がする。
使用モデル
グラフニューラルネットワークのライブラリであるDeep Graph Libraryを使用します。
訓練データを2020年までのデータ、テストデータを2021年のデータとしました。
モデルのコードはDGLのグラフ分類のサンプルコードをそのまま流用しました。
import dgl
import dgl.nn.pytorch as dglnn
import torch.nn as nn
import torch.nn.functional as F
class GNNClassifier(nn.Module):
# 馬の最大頭数は18頭なので分類は18classとする
def __init__(self, in_feat=in_feat, hidden_feat=hidden_feat, n_classifier=18):
super(GATClassifier, self).__init__()
self.conv1 = dglnn.GraphConv(in_feat, hidden_feat)
self.conv2 = dglnn.GraphConv(hidden_feat, hidden_feat)
self.fc1 = nn.Linear(hidden_feat, n_classifier)
def forward(self, g, h):
# Apply graph convolution and activation.
h = F.relu(self.conv1(g, h))
h = F.relu(self.conv2(g, h))
with g.local_scope():
g.ndata['h'] = h
# Calculate graph representation by average readout.
hg = dgl.mean_nodes(g, 'h')
x = self.fc1(hg)
return x
また、訓練のコードは
import torch
import torch.nn as nn
import torch.nn.functional as F
import dgl
from dgl.dataloading import GraphDataLoader
def train(dataset, model, criterion, optim, batch_size, epochs, device):
train_dataloader = GraphDataLoader(dataset, batch_size=batch_size, drop_last=True, shuffle=True)
model = model.to(device)
criterion = criterion
optim = optim
min_loss = 1e9
model.train()
for epoch in tqdm(range(epochs)):
running_loss = 0
total = 0
correct = 0
for batched_graph, labels in train_dataloader:
optim.zero_grad()
feats = batched_graph.ndata['feat'].to(device)
outputs = model(batched_graph, feats)
labels = labels.to(device)
loss = criterion(outputs, labels)
running_loss += loss.item()
loss.backward()
optim.step()
print('epoch :{}'.format(epoch+1))
print('loss :{}'.format(running_loss))
if min_loss >= running_loss:
model_path = './data/weight'
torch.save(model.state_dict(), model_path)
動かした結果
最終的な予測精度は7%程度ととても使い物にならない精度を叩き出しました。
原因として考えられるものとしては
- グラフの作り方が悪い
- レースごとに前処理してからグラフにするのではなく全体を前処理してからグラフにしてみる
- 前処理に問題がある
- めんどくさくて正規化標準化してないのでおそらくしたほうがいい
- 正解率の計算に問題がある
- GNNの正解率の計算ぶっちゃけよくわからない
この辺が原因じゃないかなと思ってます。
今後試してみること
- 正規化標準化する
- グラフの作り方の見直し
- グラフ分類するのではなくノード分類としてノードごとの順位を予測してみる
- 18頭のレースのみのデータのみに絞ってみる
終わりに
それでもGNNはいつか最強の精度を叩き出してくれると信じています。
この記事はまだ途中なので今後も更新していきます。
よろしければ引き続き見守ってくださると嬉しいです。
更新情報
- 2021/12/23 記事をアップ