Machine Learning - Bayesian Optimization

Ajuste de hiperparâmetros- Python

July 09, 2022 · 12 mins read

Machine Learning - Bayesian Optimization

O ajuste de hiperpârametros em um modelo de Machine Learning é extremamente importante, já que pode elevar substancialmente o desempenho do modelo em aderência aos dados. É muito comum ver em tutoriais de Machine Learning o uso do Grid Search para ajustar os hiperparâmetros, entranto há um problema de custo de processamento alto já que o Grid Search irá executar vários modelos e testar todo o intervalo de hiperparâmetros definido pelo usuário. Em contra partida o Bayesian Optimization é um método de ajuste de hiperparâmetros que utiliza a tecnologia de Bayesian Inference para encontrar o melhor valor de hiperparâmetros para o modelo.

Estudo

Usaremos um estudo já abordado em um tutorial aqui no blog: Trading Machine - Decision Tree Onde basicamente o objetivo é classificar se o retorno do ativo será positivou ou negativo ao longo dos dias.

Bibliotecas utilizadas:

import pandas as pd
import numpy as np
import ta 
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler
from skopt import gp_minimize
from skopt import space
from functools import partial
from sklearn.model_selection import train_test_split, KFold
from sklearn.metrics import accuracy_score
from sklearn.metrics import classification_report
import matplotlib.pyplot as plt

Base de dados:

A base de dados utilizada foi coletada da plataforma Economatica e contém:

  • Fechamento do ativo: BOVA11
  • Quantidade de negócios;
  • Volume;
  • Fechamento;
  • Abertura;
  • Mínima;
  • Máxima;
  • Médio.
### Importando a base de dados
df = pd.read_excel('economatica.xlsx', skiprows=3, index_col=0, parse_dates=True)
df.rename(columns={'Volume$': 'Volume'}, inplace=True)

Features:

A biblioteca ta foi utilizada para extrair as features da base de dados.

df['Retornos'] = df.Fechamento.pct_change() ## retornos
df['Kama'] = ta.momentum.KAMAIndicator(close=df.Fechamento, window=21).kama() ## indicador Kama
df['ROC'] = ta.momentum.ROCIndicator(close=df.Fechamento, window=12).roc()
df['RSI'] = ta.momentum.RSIIndicator(close=df.Fechamento, window=14).rsi()
df['Stoch'] = ta.momentum.StochasticOscillator(close=df.Fechamento, high=df.Máximo, low=df.Mínimo, 
                                                window=14, smooth_window=3).stoch()
df['Chaikin_money'] = ta.volume.ChaikinMoneyFlowIndicator(high=df.Máximo, low=df.Mínimo, close=df.Fechamento, 
                                                          volume=df.Volume, window=20).chaikin_money_flow()
df['Force_index'] = ta.volume.ForceIndexIndicator(close=df.Fechamento, 
                                                  volume=df.Volume, window=13).force_index() 
df['Normal'] = (df.Fechamento - df.Mínimo) / (df.Máximo - df.Mínimo) 

Tratando dados:

Houve a necessidade da limpeza de dados faltantes quando calculados os indicadores, como também a criação de targets para o modelo de classificação supervisionada

df = df.dropna() ## excluindo valores nulos
X = df[[
        'Q Negs', 'Q Títs', 'Volume', 'Fechamento', 'Abertura', 'Mínimo', 
        'Máximo', 'Médio', 'Kama', 'ROC', 'RSI', 'Stoch', 'Chaikin_money', 
        'Force_index', 'Normal']] ## criando as features
y = np.where(df['Fechamento'].shift(-1) > df['Fechamento'], 1, -1) ## criando target

Através train_test_split foi criado a base de treinamento e teste Houve a normalização dos dados para que os valores fossem entre 0 e 1 através do método StandardScaler

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, shuffle=False)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

A ideia por trás do Bayesian Optimization é criar um modelo probabilistico usando um processo Gaussiano, ou seja, os valores assumidos da função seguem uma distribuição Gaussiana multivariada. Onde a covariância dos valores da função é dada por um Kernel GP entre os parâmetros.

Bayesian Optimization

### Função de minimização para a Random Forest
def optimize(params, param_names, x, y):
    params = dict(zip(param_names, params))
    model = RandomForestClassifier(**params)
    kf = KFold(n_splits=5)
    accuracies = []
    for idx in kf.split(X=x, y=y):
        train_idx, test_idx = idx[0], idx[1]
        xtrain = x[train_idx]
        ytrain = y[train_idx]

        xtest = x[test_idx]
        ytest = y[test_idx]

        model.fit(xtrain, ytrain)
        preds = model.predict(xtest)
        fold_acc = accuracy_score(ytest, preds)
        accuracies.append(fold_acc)

    return -1.0 * np.mean(accuracies)
### Escolhendo os parâmetros para a Random Forest
parameters = [
    space.Integer(3, 15, name="max_depth"),
    space.Categorical(["gini", "entropy"], name="criterion"),
    space.Integer(100, 1000, prior="uniform", name="n_estimators"),
    space.Integer(2, 10, name="min_samples_split")
    ]

param_names = ['max_depth', 'criterion', 'n_estimators', 'min_samples_split']
### Função de otimização para Bayes Optimization
optimization_function = partial(
    optimize,
    param_names=param_names,
    x=X_train,
    y=y_train
)
### Rodando o Bayes Optimization
result = gp_minimize(
    optimization_function, 
    dimensions=parameters,
    n_calls=15, 
    n_random_starts=10, 
    verbose=10
)

Conferindo hiperparâmetros do modelo:

print(dict(zip(param_names, result.x))) ## imprime os parametros

Agora que já descobrimos os hiperparâmetros que melhor se ajustam aos dados, é necessário rodar novamente o modelo com os hiperparâmetros desejados.

rf = RandomForestClassifier(**dict(zip(param_names, result.x))) ## criando o modelo
rf.fit(X_train, y_train) ### treinando o modelo

### Testando o modelo
report = classification_report(y_test, rf.predict(X_test))
print(report)

Com o modelo já treinado é possível analisar o desempenho da estratégia

df['Strategy_returns'] = df['Retornos'].shift(-1) * rf.predict(X) ### retorno da estratégia
### Calculando Drawdown
strategy = df['Strategy_returns'][X_train.shape[0]:]
bova = df['Fechamento'][X_train.shape[0]:].div(df['Fechamento'].iloc[0])
wealth = 1000*(1+strategy).cumprod()
peaks = wealth.cummax()
drawdown = (wealth-peaks)/peaks

plt.style.use('fivethirtyeight')
f, (a0, a1) = plt.subplots(2, 1, figsize=(20, 15), gridspec_kw={'height_ratios': [3, 1]})
a0.plot((df['Strategy_returns'][X_train.shape[0]:]+1).cumprod())
a0.plot(bova, color='red')
a0.set_title('Retornos')
a0.legend(['Strategy', 'BOVA11'])
a1.plot(drawdown, linewidth=0)
a1.set_title('Drawdown')
a1.fill_between(drawdown.index, drawdown, alpha=1)
f.tight_layout()

'Estratégia'