[儲からない競馬予想AI] Section 03-04 : rankを予想するRegression

LightGBMと同様に順位を予測するRegressionを行います

学習過程はipynbファイルで行いました。以下はその記録です。

前処理

基本的にChapter03の前処理はほぼ同じです。教師データとなる列名によって、すこしコードをいじる程度の使いまわしです。

まず、使いそうなライブラリをインポートします。

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from IPython.display import display, Math

学習データの読み込み

df = pd.read_pickle('/work/formatted_source_data/analysis_data03.pkl')

次に学習データとテストデータ、バリデーションデータの切り分け、および入力データと教師データの切り分けをします。データの正規化もします。
処理の説明はLightGBMと大差ないので割愛します。

pytorchの学習はデフォルトがfloat32型なので、numpyのfloat32型へ変えておきます。

df = df.sort_values(['race_date']).reset_index(drop=True)

not_use_columns = [
    'race_date', 'race_id', 'race_grade'
]
for n in range(1, 19):
    for col_keyword in ['name', 'jocky_name', 'odds', 'popular', 'rank', 'time', 'prize', 'tansyo_hit', 'tansyo_payout', 'hukusyo_hit', 'hukusyo_payout']:
        not_use_columns.append(f'horse_{n}_{col_keyword}')
not_use_columns += [col for col in df.columns if 'ped' in col]

not_use_df = df[not_use_columns]
target_df = df.drop(columns=not_use_columns)

for n in range(1, 19):
    not_use_df[f'horse_{n}_relative_rank'] = (not_use_df[f'horse_{n}_rank'] - 1.0) / (target_df['horse_count'] - 1.0)

train_df = target_df.loc[not_use_df['race_date'] < '2018-01-01', :].reset_index(drop=True)
valid_df = target_df.loc[('2018-01-01' <= not_use_df['race_date']) & (not_use_df['race_date'] < '2022-01-01'), :].reset_index(drop=True)
test_df = target_df.loc['2022-01-01' <= not_use_df['race_date'], :].reset_index(drop=True)

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
train_X =  scaler.fit_transform(train_df).astype(np.float32)
valid_X =  scaler.transform(valid_df).astype(np.float32)
test_X =  scaler.transform(test_df).astype(np.float32)

target_col_keyward = 'relative_rank'

Y_columns = [f'horse_{n}_{target_col_keyward}' for n in range(1, 19)]
train_Y = not_use_df.loc[not_use_df['race_date'] < '2018-01-01', Y_columns].reset_index(drop=True)
valid_Y = not_use_df.loc[('2018-01-01' <= not_use_df['race_date']) & (not_use_df['race_date'] < '2022-01-01'), Y_columns].reset_index(drop=True)
test_Y = not_use_df.loc['2022-01-01' <= not_use_df['race_date'], Y_columns].reset_index(drop=True)

train_Y = train_Y.to_numpy().astype(np.float32)
valid_Y = valid_Y.to_numpy().astype(np.float32)
test_Y = test_Y.to_numpy().astype(np.float32)

学習

pytorchのライブラリ群をインポートします

import torch
from torch import nn
from torch.autograd import Variable

学習パラーメターを設定します。

batch_size = 1024
learning_rate = 0.001
max_epoch = 30

numpyデータからtorchのTensor型、およびそれらをミニバッチで切り出すDataLoaderへと変換します。valid_loaderはこの程度のバッチ数とモデルサイズだったら、一度で処理できるので必要ありませんが、モデルサイズが大きくなると切り出さないと処理できなくなるため、一応書きました。

Y_maskデータは、出力データが存在しない場合をマスキングして取り除くために使います。

train_X = np.nan_to_num(train_X)
valid_X = np.nan_to_num(valid_X)
test_X = np.nan_to_num(test_X)

train_Y_mask = np.isfinite(train_Y)
valid_Y_mask = np.isfinite(valid_Y)
test_Y_mask = np.isfinite(test_Y)

train_Y = np.nan_to_num(train_Y)
valid_Y = np.nan_to_num(valid_Y)
test_Y = np.nan_to_num(test_Y)

train_dataset = torch.utils.data.TensorDataset(torch.from_numpy(train_X), torch.from_numpy(train_Y), torch.from_numpy(train_Y_mask))
valid_dataset = torch.utils.data.TensorDataset(torch.from_numpy(valid_X), torch.from_numpy(valid_Y), torch.from_numpy(valid_Y_mask))
#test_dataset = torch.utils.data.TensorDataset(torch.from_numpy(test_X), torch.from_numpy(test_Y))

train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
valid_loader = torch.utils.data.DataLoader(valid_dataset, batch_size=batch_size, shuffle=True)

ネットワークとオプティマイザーを作ります。損失関数はMSELossを使いました。オプティマイザーは汎用的に使えるAdamWを使いました。
最終層はRegressionなのでSigmoidは外します。

また、Y_maskをモデルに入れて、処理することにしました。Section03-01,02と処理結果は変わらないので、好きな方を実装してください。

class NeuralNetwork(nn.Module):
    def __init__(self, input_dim, output_dim):
        super(NeuralNetwork, self).__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, 512),
            nn.ReLU(),
            nn.BatchNorm1d(512),
            nn.Dropout(0.2),
            nn.Linear(512, 1024),
            nn.ReLU(),
            nn.BatchNorm1d(1024),
            nn.Dropout(0.2),
            nn.Linear(1024, 128),
            nn.ReLU(),
            nn.BatchNorm1d(128),
            nn.Dropout(0.2),
            nn.Linear(128, output_dim)
        )
    def forward(self, x, mask):
        return self.net(x) * mask

device = torch.device('cuda')
loss_func = nn.MSELoss()
model = NeuralNetwork(train_X.shape[1], train_Y.shape[1])
model.to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)

では、学習させていきます。バリデーションデータが最も良い地点でモデルを保存して、最後にそれを読み込んでいます。

best_valid = {
    'score':None,
    'epoch':0,
    'model_path':None
}
logging = []

for epoch in range(max_epoch):
    model.train()
    train_loss = 0

    model.train()
    for X, Y, Y_mask in train_loader:
        X = Variable(X.to(device), requires_grad=True)
        Y = Variable(Y.to(device))
        Y_mask = Variable(Y_mask.to(device), requires_grad=True)
        optimizer.zero_grad()

        out = model(X, Y_mask)
        loss = loss_func(out, Y)
        train_loss += loss.item() * X.shape[0]

        loss.backward()
        optimizer.step()
    train_loss = train_loss / len(train_X)
    model.eval()
    with torch.no_grad():
        valid_loss = 0
        for X, Y, Y_mask in valid_loader:
            X = Variable(X.to(device))
            Y = Variable(Y.to(device))
            Y_mask = Variable(Y_mask.to(device))
            out = model(X, Y_mask)
            loss = loss_func(out, Y)
            valid_loss += loss.item() * X.shape[0]
        valid_loss = valid_loss / len(valid_X)
    print(f'-------epoch : {epoch} ----------')
    print(f'loss')
    print(f'  train loss : {train_loss}')
    print(f'  valid loss : {valid_loss}')

    logging.append({
        'train_loss': train_loss,
        'valid_loss': valid_loss,
    })

    save_path = f'/work/models/chapter3_section3.model'
    if best_valid['score'] == None or best_valid['score'] > valid_loss:
        best_valid['score'] = valid_loss
        best_valid['epoch'] = epoch
        best_valid['model_path'] = save_path
        torch.save(model.state_dict(), save_path)

model.load_state_dict(torch.load(best_valid['model_path']))

実行すると、ログが流れます。

...
-------epoch : 26 ----------
loss
  train loss : 0.06000224745187103
  valid loss : 0.06567983069236565
-------epoch : 27 ----------
loss
  train loss : 0.05942976576458881
  valid loss : 0.06601396415901535
-------epoch : 28 ----------
loss
  train loss : 0.058922663524839027
  valid loss : 0.06604306565019047
-------epoch : 29 ----------
loss
  train loss : 0.0585510833349187
  valid loss : 0.06634774451701625

学習結果の解析

学習曲線を見てみます

logging_df = pd.DataFrame(logging)
plt.plot(logging_df['train_loss'], label='train loss')
plt.plot(logging_df['valid_loss'], label='valid loss')
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend()

まぁ妥当な感じがします。

次に、出力の分布を見てみます。一度、モデルの出力値を吐き出し、numpy型へと変換します。

X = torch.from_numpy(train_X).to(device)
Y_mask = torch.from_numpy(train_Y_mask).to(device)
train_pred = model(X, Y_mask).cpu().detach().numpy()
X = torch.from_numpy(valid_X).to(device)
Y_mask = torch.from_numpy(valid_Y_mask).to(device)
valid_pred = model(X, Y_mask).cpu().detach().numpy()
X = torch.from_numpy(test_X).to(device)
Y_mask = torch.from_numpy(test_Y_mask).to(device)
test_pred = model(X, Y_mask).cpu().detach().numpy()

その後、各データをマスクして、分布を見てみます。

train_pred_flat =train_pred[train_Y_mask==1]
valid_pred_flat = valid_pred[valid_Y_mask==1]
test_pred_flat = test_pred[test_Y_mask==1]

train_Y_flat = train_Y[train_Y_mask==1]
valid_Y_flat = valid_Y[valid_Y_mask==1]
test_Y_flat = test_Y[test_Y_mask==1]

plt.figure(tight_layout=True, figsize=(12, 6))
plt.subplot(131)
plt.scatter(train_pred_flat, train_Y_flat)
plt.title('train')
plt.xlabel('pred')
plt.ylabel('y')

plt.subplot(132)
plt.scatter(valid_pred_flat, valid_Y_flat)
plt.title('valid')
plt.xlabel('pred')
plt.ylabel('y')

plt.subplot(133)
plt.scatter(test_pred_flat, test_Y_flat)
plt.title('test')
plt.xlabel('pred')
plt.ylabel('y')

横のラインが目立つ分布がでました。LightGBMと同じですね。テストの分布が若干荒いきがします。

利益の解析

利益を計算してみます。

target_col_keyward = 'tansyo_hit'
Y_columns = [f'horse_{n}_{target_col_keyward}' for n in range(1, 19)]
train_hit_Y = not_use_df.loc[not_use_df['race_date'] < '2018-01-01', Y_columns].reset_index(drop=True)
valid_hit_Y = not_use_df.loc[('2018-01-01' <= not_use_df['race_date']) & (not_use_df['race_date'] < '2022-01-01'), Y_columns].reset_index(drop=True)
test_hit_Y = not_use_df.loc['2022-01-01' <= not_use_df['race_date'], Y_columns].reset_index(drop=True)

train_hit_Y_flat = train_hit_Y.values[train_Y_mask==1]
valid_hit_Y_flat = valid_hit_Y.values[valid_Y_mask==1]
test_hit_Y_flat = test_hit_Y.values[test_Y_mask==1]

train_hit_Y_flat = np.nan_to_num(train_hit_Y_flat)
valid_hit_Y_flat = np.nan_to_num(valid_hit_Y_flat)
test_hit_Y_flat = np.nan_to_num(test_hit_Y_flat)

target_col_keyward = 'tansyo_payout'
Y_columns = [f'horse_{n}_{target_col_keyward}' for n in range(1, 19)]
train_payout_Y = not_use_df.loc[not_use_df['race_date'] < '2018-01-01', Y_columns].reset_index(drop=True)
valid_payout_Y = not_use_df.loc[('2018-01-01' <= not_use_df['race_date']) & (not_use_df['race_date'] < '2022-01-01'), Y_columns].reset_index(drop=True)
test_payout_Y = not_use_df.loc['2022-01-01' <= not_use_df['race_date'], Y_columns].reset_index(drop=True)

train_payout_Y_flat = train_payout_Y.values[train_Y_mask==1]
valid_payout_Y_flat = valid_payout_Y.values[valid_Y_mask==1]
test_payout_Y_flat = test_payout_Y.values[test_Y_mask==1]

train_payout_Y_flat = np.nan_to_num(train_payout_Y_flat)
valid_payout_Y_flat = np.nan_to_num(valid_payout_Y_flat)
test_payout_Y_flat = np.nan_to_num(test_payout_Y_flat)

def calc_win_money(pred, hit, payout, th):
    bet = (pred < th).astype(int)
    return_money = np.sum(bet * hit * payout)
    bet_money = np.sum(bet)
    win_money = return_money - bet_money
    return win_money

print(f'train win money :{calc_win_money(train_pred_flat, train_hit_Y_flat, train_payout_Y_flat, 0.1)}')
print(f'valid win money :{calc_win_money(valid_pred_flat, valid_hit_Y_flat, valid_payout_Y_flat, 0.1)}')
print(f'test  win money :{calc_win_money(test_pred_flat, test_hit_Y_flat, test_payout_Y_flat, 0.1)}')

学習、バリデーション、テスト、すべてのデータでマイナスの利益でした。

train win money :-43.59999999999991
valid win money :-53.60000000000002
test  win money :-7.6000000000000085

しきい値を変えたときの利益の推移を見てみます。

train_win_list = []
valid_win_list = []
test_win_list = []
th_list = np.linspace(-1.0, 1.0, 100)
for th in th_list:
    train_win_list.append(calc_win_money(train_pred_flat, train_hit_Y_flat, train_payout_Y_flat, th))
    valid_win_list.append(calc_win_money(valid_pred_flat, valid_hit_Y_flat, valid_payout_Y_flat, th))
    test_win_list.append(calc_win_money(test_pred_flat, test_hit_Y_flat, test_payout_Y_flat, th))

plt.plot(th_list, train_win_list, label='train')
plt.plot(th_list, valid_win_list, label='valid')
plt.plot(th_list, test_win_list, label='test')
plt.legend()
plt.xlabel('th')
plt.ylabel('win money')

バリデーションが変な飛び方をしていますが、総じてマイナスの利益であることには代わりありません。サチる部分を拡大してみて、利益が少しでもないか確認します。

plt.plot(th_list, train_win_list, label='train')
plt.plot(th_list, valid_win_list, label='valid')
plt.plot(th_list, test_win_list, label='test')
plt.legend()
plt.xlabel('th')
plt.ylabel('win money')
plt.xlim([0.0, 0.5])

1円も儲かりそうにないことが分かります。

タイトルとURLをコピーしました