[儲からない競馬予想AI] Section 04-01 : 単純なGPによるベット予想

GPでベットをするかしないかを予測する関数を作ります。
前処理はほとんど前章や前前章と変わりありません。

学習過程は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)

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'})

学習

さて、ここからDeapを利用するコードを書いていきます。GPのサンプルコードは公式のGithub内にあるので、それらを参考にします。
まず、使うライブラリをインポートします。

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

割り算の演算関数を再定義します。pythonの素の割り算やnumpyの割り算は、0割するとエラーやwarnningがでてきますし、nanで埋められてしまいます。その値が他の関数の入力に来ると、nanがどんどん増殖してしまうため、nanを1として再定義します。

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でつかう関数群を定義します。これはdeap.gp.PrimitiveSetで定義されます。
PrimitiveSetの第2引数は、この関数の入力数を表すので、特徴量数を指定します。

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)})

creatorは新たにクラスを作るものです。もちろんclass XXとして定義してもらっても構いませんが、deapの流儀ではこのように書きます。

toolboxは関数を作り、登録するものです。creatorと同様に別にtoolboxを間に挟む理由は、殆どの場合、無いため、自分でdef xxxと定義してもらっても問題ありません。
deapの流儀ではこのように書きます。

creator.create("FitnessMax", base.Fitness, weights=(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_money = bet*(train_Y['hit']*train_Y['payout'] - 1.0)
    return np.sum(win_money),

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

toolbox.register("evaluate", eval_individual)
toolbox.register("select", tools.selTournament, tournsize=3)
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_fit = tools.Statistics(lambda ind: ind.fitness.values)
height_size = tools.Statistics(lambda ind: ind.height)
mstats = tools.MultiStatistics(fitness=stats_fit, height=height_size)
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

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
    best = toolbox.clone(max(population, key=lambda ind:ind.fitness.values[0]))
    if elite == None or elite.fitness.values[0] < best.fitness.values[0]:
        elite = best

    offspring = toolbox.select(population, len(population)-1)
    offspring.append(toolbox.clone(elite))

    offspring = [toolbox.clone(ind) for ind in population]

    # 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)

    # Replace the current population by the offspring
    population[:] = offspring

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

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

                                       fitness                                              height                     
               ------------------------------------------------------- ------------------------------------------------
gen    nevals  avg         gen max min     nevals  std     avg     gen max min nevals  std     
...
198    403     -71829.6    198 0       -138788 403     29587.9 3.178   198 11  0   403     2.10578 
199    412     -71996.9    199 0       -138788 412     30034.5 3.172   199 10  0   412     2.17403 
200    438     -72459.2    200 0       -138788 438     30367   3.154   200 10  0   438     2.14063 

一番良かった解候補の利益を算出してみます。

best_ind = toolbox.clone(max(population+[elite], key=lambda ind:ind.fitness.values[0]))

def calc_win_money(X, Y):
    inputs = {f'x_{col}':X.loc[:, col].values for col in X.columns}
    func = toolbox.compile(expr=best_ind)
    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)

print(f'train win money: {calc_win_money(train_X, train_Y)}')
print(f'train win money: {calc_win_money(valid_X, valid_Y)}')
print(f'train win money: {calc_win_money(test_X, test_Y)}')

学習データは利益がでましたが、バリデーション、テストデータは損失がでました。あまりうまくいかないようです。

train win money: 1931.200000000002
train win money: -1828.5000000000002
train win money: -2287.7999999999997
タイトルとURLをコピーしました