ai_improvement1-1

競馬予想AIを改良する1~出走馬の成績~

2025/02/24
2025/02/24
関連ワード:AIpython競馬

競馬予想AIの改良がある程度進んだので、ぼちぼち解説していきます。

量が多くなりそうなので

  1. 出走馬の成績
  2. 親馬の成績
  3. 騎手の成績
  4. 予想AI

の4回に分けて解説しようかなと思います。1~3回までは前処理部分で、AI本体の改良については最後でのみになると思います。(もしかしたらAI本体は手を加えないかも)

今回は出走馬の成績部分の改良になります。

ソースはこちら

変更点

主な変更点は

  • データの次元圧縮に使用するモデルを変更
  • AE内で使用するモデルを変更
  • モデル変更に合わせて学習データを調整

の3つです。

次元圧縮モデルの変更

出走馬に限らず、親馬と騎手についてもそうですがデータの前処理では

  • データを全て数値に変換
  • データの特徴量を維持しつつデータのサイズを縮小

の2つをやっています。このうちのサイズ縮小の処理を変更しました。具体的にはVAEからAEに変えました。軽くVAEとAEについて触れると

AE(Autoencoder)

エンコーダとデコーダを持ち、

  1. 入力データを低次元のデータにエンコード
  2. エンコードしたデータをデコード
  3. 復元されたデータと入力データの誤差が最小になるように学習

を行うモデル。

主にデータの圧縮や特徴量の抽出に使用されます。

VAE(Variational Autoencoder)

AEを拡張したモデル。基本的な仕組みはAEと変わらないが

エンコードしたデータを確率分布(基本的にはガウス分布)として扱っています。これにより確率分布から新たなデータを生成することができます。

データ圧縮に加えて、画像生成や異常検知にも使用されます。


といった感じになっています。VAEでもデータ圧縮はできるのですがより直接的に圧縮ができるAEに変更しました。

AE内の使用モデル変更

AEのエンコーダ・デコーダに使用するモデルをTransformerから双方向GRUに変えました。この2つのモデルも簡単に説明すると

Transformer

主に自然言語処理に使われているモデル。

大きく分けると

  • 入力データの特徴を抽出するエンコーダ
  • エンコーダからの情報をもとにデータを生成するデコーダ
  • 入力データの順序を保持するための位置エンコーディング

の構造を持ち、

  • エンコードとデコーダを同時に行えるため学習が速い
  • エンコーダ内のセルフアテンション機構により離れているデータ同氏の関係も効率的に学習できる

といった特徴がある。

文章生成といった自然言語処理以外にも画像関連の処理や時系列データの解析等にも使われる

GRU(Gated Recurrent Unit)

主に時系列データに用いられるモデル。

同じような用途で用いられているRNNの課題を改善しつつ、LSTMよりも軽量で同等の性能を持つ。

  • 過去のデータをどれくらい保持するかを決める更新ゲート
  • 不要なデータを破棄するリセットゲート

の2つを使用して過去のデータを上手に利用しながら学習を行う


といった感じです。

今回はモデルを軽くしたかったのと、そこまでデータ長が長くなく離れたデータ間の関連をとらえる必要性がないと思ったのでGRUを採用しました。

また、精度をより高めるために過去→未来と未来→過去の双方向で入力を与えています。

AE全体のコード

python:src/pedigree-results.ipynb
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

入力データの次元数のほか

  • 隠れ層のユニット数
  • 潜在空間(圧縮後データ)の次元数
  • GRUの層数
  • ドロップアウト率

を引数で渡すようにして調節できるようにしています。

学習データの調整

今までは固定長のデータとして最新10回分のレースの成績を使用し、それより古い成績は削除していました。

今回のモデル変更にあたって可変データ長でも学習できるようになったため古い成績を削除する処理を無くしました。

後になって気づいたことですが、以前のtransformerを使ったモデルでも可変データ長で学習できるそうなので機会があればこれも試そうかなと思います。

学習結果

AEのモデルのパラメータと学習率はoptunaを使って調整し

入力データサイズ67
隠れ層のユニット数56
潜在空間の次元数37
GRUの層数8
ドロップアウト率0.25954105524890514
学習率4.6214697747393665e-05

で設定しました。

エポックごとの誤差の推移は下の画像の通りです。

学習の推移

最終的に誤差は

訓練データ:6215

バリデーションデータ:6702

テストデータ: 6680

になりました。そもそものデータの形もモデルのパラメータも変わっているので単純比較できないですが、前のモデルでは誤差が12000程度だったので、おおよそ2倍の精度で圧縮出来ています。

試しにデータを一つ圧縮してみます。

python
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())
shell
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になっています。

ひとまずデータ圧縮の精度を上げることはできたので、出走馬の成績データの前処理はこれくらいにして次は親馬の成績データに移ろうと思います。