[儲からない競馬予想AI] Section 05-07 : Multi-objective GPによるベット予想

「Section 04-03: Multi-objective GPによるベット予想」の事前オッズを使う場合です。

特徴量として、前回(Chapter05: 事前オッズを使った予測)求めた事前オッズを追加するだけで、あとは同じです。

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

前処理

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

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_data02.pkl')

次に学習データとテストデータ、バリデーションデータの切り分け、および入力データと教師データの切り分けをします。データの正規化もします。

教師データは単勝が当たったかどうか(tansyo_hit)と単勝が当たった時の支払い金額(tansyo_payout)を使います。

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

not_use_columns = [
    'race_date', 'race_id', 'race_grade', 'name', 'jocky_name', 'odds', 'popular', 'rank', 'time', 'prize', 'tansyo_hit', 'tansyo_payout', 'hukusyo_hit', 'hukusyo_payout',
]

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

lower_noise = np.random.normal(0.0, 0.5, len(df))
upper_noise = np.random.normal(0.8, 0.3, len(df))
p_func = lambda x : -np.exp((-x+1)*0.02) + 1
p = p_func(not_use_df['odds'])
noise = lower_noise*(1-p) + upper_noise*p
not_use_df['noised_odds'] = not_use_df['odds']/np.exp(noise)

target_df['noised_odds_log'] = np.log(not_use_df['noised_odds'])

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 =  pd.DataFrame(data=scaler.fit_transform(train_df), columns=train_df.columns)
valid_X =  pd.DataFrame(data=scaler.transform(valid_df), columns=train_df.columns)
test_X  =  pd.DataFrame(data=scaler.transform(test_df), columns=train_df.columns)

train_X = train_X.fillna(0)
valid_X = valid_X.fillna(0)
test_X = test_X.fillna(0)

Y_columns = ['tansyo_hit', 'tansyo_payout']
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.rename(columns={'tansyo_hit':'hit', 'tansyo_payout':'payout'})
valid_Y = valid_Y.rename(columns={'tansyo_hit':'hit', 'tansyo_payout':'payout'})
test_Y = test_Y.rename(columns={'tansyo_hit':'hit', 'tansyo_payout':'payout'})

学習

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

import operator
from deap import base
from deap import creator
from deap import tools
from deap import gp
import random
from functools import partial

割り算の演算関数を再定義します。

def protectedDiv(left, right):
    with np.errstate(divide='ignore',invalid='ignore'):
        x = np.divide(left, right)
        if isinstance(x, np.ndarray):
            x[np.isinf(x)] = 1
            x[np.isnan(x)] = 1
        elif np.isinf(x) or np.isnan(x):
            x = 1
    return x

次にGPでつかう関数群を定義します。

pset = gp.PrimitiveSet("MAIN", len(train_X.columns))
pset.addPrimitive(np.add, 2, name="add")
pset.addPrimitive(np.subtract, 2, name="sub")
pset.addPrimitive(np.multiply, 2, name="mul")
pset.addPrimitive(protectedDiv, 2, name='div')
pset.addPrimitive(np.negative, 1, name="neg")
pset.addPrimitive(np.cos, 1, name="cos")
pset.addPrimitive(np.sin, 1, name="sin")
pset.addEphemeralConstant("rand101", partial(random.randint, -1, 1))

pset.renameArguments(**{f'ARG{i}':f'x_{col}' for i, col in enumerate(train_X.columns)})

評価クラスを多目的用に設定します。FittnessMultiとしました。
weightsは複数の評価値の重みです。3つ目の評価値として負けた賭け数を与えるため、それは最小化が目的なので-1.0を設定。それ以外の2つは最大化がもくてきなので1.0を設定。

creator.create("FitnessMulti", base.Fitness, weights=(1.0, 1.0, -1.0))
creator.create("Individual", gp.PrimitiveTree, fitness=creator.FitnessMax)

toolbox = base.Toolbox()
toolbox.register("expr", gp.genHalfAndHalf, pset=pset, min_=1, max_=2)
toolbox.register("individual", tools.initIterate, creator.Individual, toolbox.expr)
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
toolbox.register("compile", gp.compile, pset=pset)

評価関数を定義します。個体(解候補)が与えられたときに、その評価値を返せばよいです。
toolbox.compileによって解候補から合成関数を作り出します。その後、train_inputsを与えて、出力値を得ます。
出力値が0以上のときに賭け、そうでないときは賭けないようにして、利益を算出します。
その総利益を評価値として返します。
また、勝った数や負けた数を数えてそれらも評価値として返します。

train_inputs = {f'x_{col}':train_X.loc[:, col].values for col in train_X.columns}
def eval_individual(individual):
    func = toolbox.compile(expr=individual)
    ret = func(**train_inputs)
    bet = np.zeros_like(ret)
    bet[ret > 0] = 1
    win_num = np.sum(win_money > 0)
    lose_num = np.sum(win_money < 0)
    return win_score, win_num, lose_num

定義した評価関数や、deapに内蔵されている選択、交叉、突然変異の関数をtoolboxに登録します。また、個体の深さ(合成関数の処理規模だと思ってください)に制限を設けます。

選択方法をtools.selNSGA2に設定します。多目的最適化の手法はたくさんありますが、今回は評価値も3つなのでクラシックなNSGA2にしたいと思います。

toolbox.register("evaluate", eval_individual)
toolbox.register("select", tools.selNSGA2)
toolbox.register("mate", gp.cxOnePoint)
toolbox.register("expr_mut", gp.genFull, min_=0, max_=2)
toolbox.register('mutate', gp.mutUniform, expr=toolbox.expr_mut, pset=pset)

toolbox.decorate("mate", gp.staticLimit(key=operator.attrgetter("height"), max_value=12))
toolbox.decorate("mutate", gp.staticLimit(key=operator.attrgetter("height"), max_value=12))

準備が揃ったので、学習を始めます。
個体数500、世代数200で学習します。

crossover_rate = 0.8
mutate_rate = 0.2
max_generation = 200
population_size = 500

random.seed(318)

population = toolbox.population(n=population_size)
halloffame = tools.HallOfFame(1)
stats_fit0 = tools.Statistics(lambda ind: ind.fitness.values[0])
stats_fit1 = tools.Statistics(lambda ind: ind.fitness.values[1])
stats_fit2 = tools.Statistics(lambda ind: ind.fitness.values[2])
mstats = tools.MultiStatistics(win_score=stats_fit0, win_num=stats_fit1, lose_num=stats_fit2)
mstats.register("avg", np.mean)
mstats.register("std", np.std)
mstats.register("min", np.min)
mstats.register("max", np.max)

logbook = tools.Logbook()
logbook.header = ['gen', 'nevals'] + mstats.fields

# Evaluate the individuals with an invalid fitness
invalid_ind = [ind for ind in population if not ind.fitness.valid]
fitnesses = toolbox.map(toolbox.evaluate, invalid_ind)
for ind, fit in zip(invalid_ind, fitnesses):
    ind.fitness.values = fit
pop = toolbox.select(population, len(population))

if halloffame is not None:
    halloffame.update(population)

record = mstats.compile(population)
logbook.record(gen=0, nevals=len(invalid_ind), **record)
print(logbook.stream)

elite = None
# Begin the generational process
for gen in range(1, max_generation + 1):
    # Select the next generation individuals
    offspring = tools.selTournamentDCD(population, len(population))
    offspring = [toolbox.clone(ind) for ind in offspring]

    # Apply crossover and mutation on the offspring
    for i in range(1, len(offspring), 2):
        if random.random() < crossover_rate:
            offspring[i - 1], offspring[i] = toolbox.mate(offspring[i - 1], offspring[i])
            del offspring[i - 1].fitness.values, offspring[i].fitness.values

    for i in range(len(offspring)):
        if random.random() < mutate_rate:
            offspring[i], = toolbox.mutate(offspring[i])
            del offspring[i].fitness.values

    # Evaluate the individuals with an invalid fitness
    invalid_ind = [ind for ind in offspring if not ind.fitness.valid]
    fitnesses = toolbox.map(toolbox.evaluate, invalid_ind)
    for ind, fit in zip(invalid_ind, fitnesses):
        ind.fitness.values = fit

    # Update the hall of fame with the generated individuals
    if halloffame is not None:
        halloffame.update(offspring)

    population = toolbox.select(population + offspring, population_size)

    # Append the current generation statistics to the logbook
    record = mstats.compile(population)
    logbook.record(gen=gen, nevals=len(invalid_ind), **record)
    print(logbook.stream)

実行するとログが流れます。終わるまで待ちます。

                                  lose_num                                         win_num                                            win_score                       
               ----------------------------------------------  ----------------------------------------------- -------------------------------------------------------
gen    nevals  avg     gen max     min nevals  std     avg     gen max     min nevals  std     avg         gen max min     nevals  std   
...
198    434     193102  198 469102  0   434     149272  25425.8 198 35862   0   434     10926   -44040.6    198 2908.5  -135522 434     35718.2
199    411     195974  199 469102  0   411     149588  25577.1 199 35862   0   411     10982.9 -44667.3    199 2908.5  -135522 411     35885.5
200    421     192074  200 469102  0   421     148693  25363.9 200 35862   0   421     10952.6 -43729      200 2908.5  -135522 421     35631.6

パレート解を見てみます。
多目的最適化では複数の評価基準があるため、一番良い解候補というのをアルゴリズムは選べません。複数の回候補を見て、人が選ぶ必要があります。

fits = np.array([ind.fitness.values for ind in population])
#win_score, bet_num, lose_score
plt.figure(tight_layout=True, figsize=(12, 6))
plt.subplot(131)
plt.scatter(fits[:, 0], fits[:, 1])
plt.title('win_score - win_num')
plt.xlabel('win_score')
plt.ylabel('win_num')

plt.subplot(132)
plt.scatter(fits[:, 1], fits[:, 2])
plt.title('win_num - lose_num')
plt.xlabel('win_num')
plt.ylabel('lose_num')

plt.subplot(133)
plt.scatter(fits[:, 0], fits[:, 2])
plt.title('win_score - lose_num')
plt.xlabel('win_score')
plt.ylabel('lose_num')


カーブが描けているので、アルゴリズムは正常に動作しているようです。
オッズを特徴量に入れても、大した変化は見られませんでした。

学習データで利益がでたものだけ、ピックアップしてバリデーションとテストデータの利益を見てみます。

def calc_win_money(individual, X, Y):
    inputs = {f'x_{col}':X.loc[:, col].values for col in X.columns}
    func = toolbox.compile(expr=individual)
    ret = func(**inputs)
    bet = np.zeros_like(ret)
    bet[ret > 0] = 1
    win_money = bet*(Y['hit']*Y['payout'] - 1.0)
    return np.sum(win_money)

for i, ind in enumerate(population):
    if ind.fitness.values[0] > 0:
        print(f'--------{i}--------')
        print(ind.fitness.values)
        print(f'train win money: {calc_win_money(ind, train_X, train_Y)}')
        print(f'valid win money: {calc_win_money(ind, valid_X, valid_Y)}')
        print(f'test  win money: {calc_win_money(ind, test_X, test_Y)}')

学習データの利益がでる解候補はありましたが、バリデーションデータ、テストデータで利益がでる解候補は見つかりませんでした。

--------3--------
(2908.4999999999995, 58.0, 560.0)
train win money: 2908.4999999999995
valid win money: -24.39999999999999
test  win money: -53.29999999999998
--------28--------
(1245.7000000000007, 2593.0, 4262.0)
train win money: 1245.7000000000007
valid win money: -679.3
test  win money: -921.6999999999999
--------38--------
(2368.4000000000005, 349.0, 1005.0)
train win money: 2368.4000000000005
valid win money: -410.0999999999999
test  win money: -791.2
--------40--------
(711.7000000000007, 3325.0, 5755.0)
train win money: 711.7000000000007
valid win money: -815.9
test  win money: -918.9
--------98--------
(2873.1, 68.0, 625.0)
train win money: 2873.1
valid win money: -72.89999999999998
test  win money: -77.69999999999999
--------200--------
(1604.1000000000006, 1010.0, 3473.0)
train win money: 1604.1000000000006
valid win money: -943.4
test  win money: -802.3999999999999
--------283--------
(1456.2, 1821.0, 3962.0)
train win money: 1456.2
valid win money: -588.1999999999999
test  win money: -778.9
--------309--------
(1753.4, 883.0, 3094.0)
train win money: 1753.4
valid win money: -862.3
test  win money: -679.8
--------330--------
(374.8999999999991, 3761.0, 6610.0)
train win money: 374.8999999999991
valid win money: -657.1999999999999
test  win money: -996.4000000000001
--------406--------
(40.600000000000804, 4084.0, 7364.0)
train win money: 40.600000000000804
valid win money: -801.4
test  win money: -1050.3999999999999
--------411--------
(1455.0999999999995, 1997.0, 3514.0)
train win money: 1455.0999999999995
valid win money: -669.5
test  win money: -322.00000000000006
--------461--------
(1245.7000000000007, 2593.0, 4262.0)
train win money: 1245.7000000000007
valid win money: -679.3
test  win money: -921.6999999999999
--------463--------
(2208.5, 536.0, 1367.0)
train win money: 2208.5
valid win money: -493.30000000000007
test  win money: -823.0
--------475--------
(2208.5, 536.0, 1367.0)
train win money: 2208.5
valid win money: -493.30000000000007
test  win money: -823.0
タイトルとURLをコピーしました