競馬予想AIの改良がある程度進んだので、ぼちぼち解説していきます。
量が多くなりそうなので
- 出走馬の成績
- 親馬の成績
- 騎手の成績
- 予想AI
の4回に分けて解説しようかなと思います。1~3回までは前処理部分で、AI本体の改良については最後でのみになると思います。(もしかしたらAI本体は手を加えないかも)
今回は出走馬の成績部分の改良になります。
ソースはこちら

競馬予想AIの改良がある程度進んだので、ぼちぼち解説していきます。
量が多くなりそうなので
の4回に分けて解説しようかなと思います。1~3回までは前処理部分で、AI本体の改良については最後でのみになると思います。(もしかしたらAI本体は手を加えないかも)
今回は出走馬の成績部分の改良になります。
ソースはこちら
主な変更点は
の3つです。
出走馬に限らず、親馬と騎手についてもそうですがデータの前処理では
の2つをやっています。このうちのサイズ縮小の処理を変更しました。具体的にはVAEからAEに変えました。軽くVAEとAEについて触れると
エンコーダとデコーダを持ち、
を行うモデル。
主にデータの圧縮や特徴量の抽出に使用されます。
AEを拡張したモデル。基本的な仕組みはAEと変わらないが
エンコードしたデータを確率分布(基本的にはガウス分布)として扱っています。これにより確率分布から新たなデータを生成することができます。
データ圧縮に加えて、画像生成や異常検知にも使用されます。
といった感じになっています。VAEでもデータ圧縮はできるのですがより直接的に圧縮ができるAEに変更しました。
AEのエンコーダ・デコーダに使用するモデルをTransformerから双方向GRUに変えました。この2つのモデルも簡単に説明すると
主に自然言語処理に使われているモデル。
大きく分けると
の構造を持ち、
といった特徴がある。
文章生成といった自然言語処理以外にも画像関連の処理や時系列データの解析等にも使われる
主に時系列データに用いられるモデル。
同じような用途で用いられているRNNの課題を改善しつつ、LSTMよりも軽量で同等の性能を持つ。
の2つを使用して過去のデータを上手に利用しながら学習を行う
といった感じです。
今回はモデルを軽くしたかったのと、そこまでデータ長が長くなく離れたデータ間の関連をとらえる必要性がないと思ったのでGRUを採用しました。
また、精度をより高めるために過去→未来と未来→過去の双方向で入力を与えています。
1class AutoEncoder(nn.Module):
2 def __init__(self, input_dim, hidden_dim, latent_dim, num_layers=2, dropout=0.1):
3 """
4 Args:
5 input_dim (int): 入力データの次元数(特徴量の数)
6 hidden_dim (int): 隠れ層のユニット数
7 latent_dim (int): 潜在空間の次元数(圧縮後の次元数)
8 num_layers (int): GRUの層数
9 dropout (float): ドロップアウト率
10 """
11 super(AutoEncoder, self).__init__()
12
13 self.encoder_gru = nn.GRU(
14 input_size=input_dim,
15 hidden_size=hidden_dim,
16 num_layers=num_layers,
17 batch_first=True,
18 dropout=dropout,
19 bidirectional=True,
20 )
21 self.fc_mu = nn.Linear(hidden_dim * 2, latent_dim)
22
23 self.decoder_fc = nn.Linear(latent_dim, hidden_dim * 2)
24 self.decoder_gru = nn.GRU(
25 input_size=hidden_dim * 2,
26 hidden_size=hidden_dim,
27 num_layers=num_layers,
28 batch_first=True,
29 dropout=dropout,
30 bidirectional=True,
31 )
32 self.output_layer = nn.Linear(hidden_dim * 2, input_dim)
33
34 def encode(self, x):
35 _, h_n = self.encoder_gru(x)
36 h_n = h_n[-2:].transpose(0, 1).contiguous()
37 h_n = h_n.view(h_n.size(0), -1)
38 z = self.fc_mu(h_n)
39 return z
40
41 def decode(self, z, seq_len):
42 h = self.decoder_fc(z)
43 h = h.unsqueeze(1).repeat(1, seq_len, 1)
44 output, _ = self.decoder_gru(h)
45 output = self.output_layer(output)
46 return output
47
48 def forward(self, x):
49 z = self.encode(x)
50 output = self.decode(z, x.size(1))
51 return output入力データの次元数のほか
を引数で渡すようにして調節できるようにしています。
今までは固定長のデータとして最新10回分のレースの成績を使用し、それより古い成績は削除していました。
今回のモデル変更にあたって可変データ長でも学習できるようになったため古い成績を削除する処理を無くしました。
後になって気づいたことですが、以前のtransformerを使ったモデルでも可変データ長で学習できるそうなので機会があればこれも試そうかなと思います。
AEのモデルのパラメータと学習率はoptunaを使って調整し
| 入力データサイズ | 67 |
| 隠れ層のユニット数 | 56 |
| 潜在空間の次元数 | 37 |
| GRUの層数 | 8 |
| ドロップアウト率 | 0.25954105524890514 |
| 学習率 | 4.6214697747393665e-05 |
で設定しました。
エポックごとの誤差の推移は下の画像の通りです。

学習の推移
最終的に誤差は
訓練データ:6215
バリデーションデータ:6702
テストデータ: 6680
になりました。そもそものデータの形もモデルのパラメータも変わっているので単純比較できないですが、前のモデルでは誤差が12000程度だったので、おおよそ2倍の精度で圧縮出来ています。
試しにデータを一つ圧縮してみます。
1model.eval()
2test_df = pd.read_pickle("../Processed-Data/Horse-Results/2011101814.pkl")
3test_df = test_df.iloc[:].astype("float32")
4# データフレームをテンソルに変換してGPUに転送
5data = torch.tensor(test_df.values, dtype=torch.float32).unsqueeze(0).to(device)
6
7# 潜在変数の取得
8with torch.no_grad():
9 encoded = model.get_latent_val(data)
10
11print(f"入力データサイズ: {data.size()}")
12print(f"潜在変数サイズ: {encoded.size()}")
13print("\n潜在変数:")
14print(encoded.cpu().numpy())1入力データサイズ: torch.Size([1, 44, 67])
2潜在変数サイズ: torch.Size([1, 37])
3
4潜在変数:
5[[-0.6031361 -0.41373312 -0.07094935 1.0170487 -0.07658499 0.03957762
6 -0.40517583 -0.4399286 -0.7719066 -0.05634143 0.51706153 1.2585212
7 1.1883372 0.89504516 -0.55131894 -1.0807186 1.4251518 -1.3181621
8 0.26893717 -0.8097326 -0.7210528 -0.541371 -1.4630766 -0.9171215
9 -0.32020655 1.138465 0.16784808 0.89805824 -0.62369126 0.29323277
10 -0.06836507 -0.23196848 0.597075 -0.21393116 -0.8040468 -0.7157359
11 -0.01877082]]入力データのサイズ44*67=2948で圧縮後が37になっています。
ひとまずデータ圧縮の精度を上げることはできたので、出走馬の成績データの前処理はこれくらいにして次は親馬の成績データに移ろうと思います。