Índice
- O Que é NBEATS?
- Como Instalar a NeuralForecast Com e Sem Suporte a GPU
- Como Preparar a Série Temporal Para o NBEATS
- Hiperparâmetros do NBEATS
- Como Treinar NBEATS em Python
- Como Adicionar Variáveis Externas ao NBEATS
- Baseline Simples Com Sazonalidade
- Função Objetivo WMAPE no PyTorch
O Que é NBEATS?
A NBEATS é uma arquitetura de rede neural capaz de prever séries temporais sem depender da criação de features específicas por um especialista.
Composto por vários blocos de camadas, o NBEATS recebe uma sequência de observações como entrada e produz, em cada bloco, dois grupos de coeficientes de expansão.
Em vez de prevermos a série diretamente, prevemos coeficientes.
A rede mapeia estes coeficientes a funções que podem ser previamente especificadas ou aprendidas junto com o restante dos pesos.
Por exemplo, no paper original o autor fala sobre usar polinômios como funções de base para modelar a tendência da série.
Digamos que vamos prever apenas um valor futuro e temos um polinômio de grau 2, então a saída será:
y = w0*t^0 + w1*t^1 + w2*t^2
Onde w0
, w1
e w2
são os coeficientes de expansão gerados pela rede neural e t
é a posição do passo que queremos prever.
Um grupo de coeficientes é usado para reconstruir a sequência de entrada e outro é usado para prever os próximos valores.
Estes blocos são combinados em “stacks” (um bloco de blocos), cada uma responsável por aprender características específicas da série temporal como tendência e sazonalidade.
Esta estrutura lembra muito a estratégia de boosting, pois cada bloco recebe os resíduos das previsões anteriores.
A previsão final é a soma de todas as previsões parciais geradas por cada bloco, forçando cada stack a aprender padrões hierárquicos diferentes.
Vamos ver como criar um modelo NBEATS para prever séries temporais usando a biblioteca NeuralForecast.
Como Instalar a NeuralForecast Com e Sem Suporte a GPU
Por usar métodos de redes neurais, se você tiver uma GPU, é importante ter o CUDA instalado para que os modelos rodem mais rápido.
Para verificar se você tem uma GPU instalada e configurada corretamente com PyTorch (backend da biblioteca), execute o código abaixo:
import torch
print(torch.cuda.is_available())
Esta função retorna True
se você tem uma GPU instalada e configurada corretamente, e False
caso contrário.
Caso você tenha uma GPU, mas não tenha o PyTorch instalado com suporte a ela, veja no site oficial como instalar a versão correta.
Recomendo que você instale o PyTorch primeiro!
O comando que usei para instalar o PyTorch com suporte a GPU foi:
conda install pytorch pytorch-cuda=11.7 -c pytorch -c nvidia
Se você não tem uma GPU, não se preocupe, a biblioteca funciona normalmente, só não fica tão rápida.
Instalar essa biblioteca é muito simples, basta executar o comando abaixo:
pip install neuralforecast
ou se você usa o Anaconda:
conda install -c conda-forge neuralforecast
Como Preparar a Série Temporal Para o NBEATS
Vamos usar dados reais de vendas da rede de lojas Favorita, do Equador.
Temos dados de vendas de 2013 a 2017 para diversas lojas e categorias de produtos.
Nossos dados de treino cobrirão os anos 2013 a 2016 e os dados de validação serão os 3 primeiros meses de 2017.
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
def wmape(y_true, y_pred):
return np.abs(y_true - y_pred).sum() / np.abs(y_true).sum()
Para medir o desempenho do modelo, vamos usar a métrica WMAPE (Weighted Mean Absolute Percentage Error).
Ela é uma adaptação do erro percentual que resolve o problema de dividir por zero.
No nosso caso, vamos considerar que o peso de cada observação é o valor absoluto dela, simplificando a fórmula.
data = pd.read_csv('train.csv', index_col='id', parse_dates=['date'])
data2 = data.loc[(data['store_nbr'] == 1) & (data['family'].isin(['MEATS', 'PERSONAL CARE'])), ['date', 'family', 'sales', 'onpromotion']]
Para simplificar, vamos usar apenas os dados de uma loja e duas categorias de produto.
As colunas são:
date
: data do registrofamily
: categoria do produtosales
: número de vendasonpromotion
: a quantidade de produtos daquela categoria que estavam em promoção naquele dia
Além das vendas e indicador de promoções, vamos criar variáveis adicionais do dia da semana.
O dia da semana pode ser tratado como uma variável ordinal ou categórica, mas aqui vou usar a abordagem categórica que é mais comum.
Em geral, usar informações adicionais que sejam relevantes para o problema pode melhorar o desempenho do modelo.
Variáveis específicas da estrutura temporal, como dias da semana, meses e dias do mês são importantes para capturar padrões sazonais.
Existe uma infinidade de outras informações que poderíamos adicionar, como a temperatura, chuva, feriados, etc.
Nem sempre essas informações vão melhorar o modelo, e podem até piorar o desempenho, então sempre verifique se elas melhoram o erro nos dados de validação.
data2 = data2.rename(columns={'date': 'ds', 'sales': 'y', 'family': 'unique_id'})
A biblioteca neuralforecast
espera que as colunas sejam nomeadas dessa forma:
ds
: data do registroy
: variável alvo (número de vendas)unique_id
: identificador único da série temporal (categoria do produto)
O unique_id
pode ser qualquer identificador que separe suas séries temporais.
Se quiséssemos modelar a previsão de vendas de todas as lojas, poderíamos usar o store_nbr
junto com a family
como identificadores.
train = data2.loc[data2['ds'] < '2017-01-01']
valid = data2.loc[(data2['ds'] >= '2017-01-01') & (data2['ds'] < '2017-04-01')]
h = valid['ds'].nunique()
Este é o formato final da tabela:
ds | unique_id | y | onpromotion | weekday_0 | weekday_1 | weekday_2 | weekday_3 | weekday_4 | weekday_5 | weekday_6 |
---|---|---|---|---|---|---|---|---|---|---|
2013-01-01 00:00:00 | MEATS | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
2013-01-01 00:00:00 | PERSONAL CARE | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
2013-01-02 00:00:00 | MEATS | 369.101 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
Separamos os dados em treino e validação com uma divisão temporal simples entre passado e futuro.
A variável h
é o horizonte, o número de períodos que queremos prever no futuro.
Neste caso, é o número de datas únicas na validação (90).
Vamos para a modelagem.
Hiperparâmetros do NBEATS
Os hiperparâmetros do NBEATS são muito similares aos de uma rede neural comum.
Vamos entender alguns dos mais importantes e como eles afetam o desempenho do modelo.
n_harmonics
O hiperparâmetro n_harmonics
define a quantidade de termos harmônicos que serão usados para modelar a sazonalidade da série temporal.
Quanto mais termos, maior o poder de expressão do modelo, mas também maior o risco de overfitting.
Por padrão o valor é 2.
n_polynomials
O hiperparâmetro n_polynomials
define o grau do polinômio para modelagem da tendência na série temporal.
Mais uma vez, a expressividade do modelo aumenta quanto maior for o grau do polinômio, mas também aumenta o risco de overfitting.
O valor padrão também é 2.
stack_types
Este hiperparâmetro define quantas e quais stacks vamos usar na rede neural.
Por padrão são 3 stacks, uma para processar a série original, outra para a tendência e outra para a sazonalidade.
input_size_multiplier
Este hiperparâmetro é multiplicado pelo horizonte h
para determinar o número de observações que serão usadas como entrada (features) para prever os próximos períodos.
Por exemplo, no nosso caso de previsão diária, se o horizonte for 90 e o input_size_multiplier
for 2, as vendas para os últimos 180 dias serão usadas como features para prever os próximos 90 dias.
O intervalo padrão é de números inteiros de 1 a 5.
learning_rate
Este é o tamanho do passo que o algoritmo de otimização vai dar para atualizar os pesos da rede neural.
É um dos hiperparâmetros mais importantes e tem um grande impacto no desempenho do modelo.
Um valor muito alto pode fazer com que o modelo nunca estabilize, enquanto um valor muito baixo pode fazer com que o modelo demore muito para convergir.
O intervalo padrão é uma distribuição log uniforme entre 0.0001 e 0.1.
scaler_type
Este é o tipo de transformação que será aplicada nos dados antes de treinar o modelo.
Deixar os dados na mesma escala remodela a superfície de erro e permite que o modelo aprenda mais rápido e chegue a um resultado melhor.
A busca pode escolher entre None
, standard
e robust
.
standard
é a transformação mais comum que subtrai a média e divide pelo desvio padrão.
robust
é uma transformação mais robusta que usa a mediana e o desvio absoluto da mediana
max_steps
Este é o número máximo de iterações que o algoritmo de otimização vai fazer para atualizar os pesos da rede neural.
Geralmente dar mais passos com uma learning_rate
baixa te dá resultados mais estáveis, mas também leva mais tempo para treinar.
Os valores padrão são 500 e 1000.
Como Treinar NBEATS em Python
Para treinar nosso modelo vamos usar a classe AutoNBEATS
do pacote neuralforecast
.
Ela será responsável por fazer uma busca automática pelos melhores hiperparâmetros e retornar o modelo com o melhor desempenho em dados internos de validação.
from neuralforecast import NeuralForecast
from neuralforecast.auto import AutoNBEATS
models = [AutoNBEATS(h=h,
loss=WMAPE(),
num_samples=30)]
model = NeuralForecast(models=models, freq='D')
model.fit(train)
Criamos a lista de modelos que queremos treinar, no nosso caso apenas um modelo AutoNBEATS
.
O argumento h
é o horizonte, o número de períodos que queremos prever no futuro.
A loss
é a função de erro que queremos minimizar. Criei uma função personalizada para minimizar o WMAPE diretamente. Compartilho o código no fim do artigo.
O num_samples
é o número de combinações de hiperparâmetros que queremos testar. 30 combinações são um bom equilíbrio entre tempo de treinamento e qualidade dos resultados.
Depois, passamos a lista de modelos para a classe NeuralForecast
e chamamos o método fit
passando o dataframe de treino.
O argumento freq
é a frequência da série temporal, no nosso caso é diária.
p = model.predict().reset_index()
p = p.merge(valid[['ds','unique_id', 'y']], on=['ds', 'unique_id'], how='left')
Depois de fazer a busca automática, podemos usar o método predict
para fazer as previsões com o melhor modelo encontrado.
Fiz o join com os dados de validação para ficar mais fácil de comparar os resultados.
unique_id | ds | AutoNBEATS | y |
---|---|---|---|
MEATS | 2017-01-01 00:00:00 | 115.232 | 0 |
PERSONAL CARE | 2017-01-01 00:00:00 | 107.544 | 0 |
PERSONAL CARE | 2017-01-02 00:00:00 | 141.685 | 81 |
Por fim vamos plotar os resultados e calcular o WMAPE nos dados de validação que separamos no início.
fig, ax = plt.subplots(2, 1, figsize = (1280/96, 720/96))
fig.tight_layout(pad=7.0)
for ax_i, unique_id in enumerate(['MEATS', 'PERSONAL CARE']):
plot_df = pd.concat([train.loc[train['unique_id'] == unique_id].tail(30),
p.loc[p['unique_id'] == unique_id]]).set_index('ds')
plot_df[['y', 'AutoNBEATS']].plot(ax=ax[ax_i], linewidth=2, title=unique_id)
ax[ax_i].grid()
print(wmape(p['y'], p['AutoNBEATS']))
O erro do modelo ficou em 22,95%.
Para entender melhor a interação dos hiperparâmetros com os dados, podemos examinar uma tabela com os resultados de todas as combinações testadas.
Basta usar o método get_dataframe
do objeto results
do modelo.
results_df = models[0].results.get_dataframe().sort_values('loss')
results_df.head(5)
loss | config/max_steps | config/input_size | config/learning_rate |
---|---|---|---|
0.261276 | 1000 | 450 | 0.00212358 |
0.266041 | 500 | 360 | 0.000222619 |
0.294368 | 1000 | 270 | 0.00266582 |
0.302672 | 500 | 270 | 0.00035389 |
0.320332 | 1000 | 450 | 0.00317887 |
Também podemos ver os hiperparâmetros do melhor modelo encontrado com o método get_best_result
.
best_config = models[0].results.get_best_result().metrics['config']
best_config
{'h': 90,
'learning_rate': 0.0021235793975551286,
'scaler_type': None,
'max_steps': 1000,
'batch_size': 32,
'windows_batch_size': 256,
'loss': WMAPE(),
'check_val_every_n_epoch': 100,
'random_seed': 17,
'input_size': 450,
'step_size': 1}
Como Adicionar Variáveis Externas ao NBEATS
Originalmente o NBEATS não suporta variáveis externas, mas há uma modificação chamada NBEATSx que integrou esse recurso.
Vamos aproveitar os hiperparâmetros encontrados no passo anterior e treinar um modelo com variáveis externas.
As classes de busca automática da neuralforecast
ainda não suportam modelos com variáveis externas, por isso não as usei no passo anterior.
Vamos fazer algumas modificações simples no código:
from neuralforecast import NeuralForecast
from neuralforecast.models import NBEATSx
models = [NBEATSx(futr_exog_list=['onpromotion', 'weekday_0',
'weekday_1', 'weekday_2', 'weekday_3', 'weekday_4', 'weekday_5',
'weekday_6'],
**best_config)]
model = NeuralForecast(models=models, freq='D')
model.fit(train)
p = model.predict(futr_df=valid).reset_index()
p = p.merge(valid[['ds','unique_id', 'y']], on=['ds', 'unique_id'], how='left')
fig, ax = plt.subplots(2, 1, figsize = (1280/96, 720/96))
fig.tight_layout(pad=7.0)
for ax_i, unique_id in enumerate(['MEATS', 'PERSONAL CARE']):
plot_df = pd.concat([train.loc[train['unique_id'] == unique_id].tail(30),
p.loc[p['unique_id'] == unique_id]]).set_index('ds') # Concatenate the train and forecast dataframes
plot_df[['y', 'NBEATSx']].plot(ax=ax[ax_i], linewidth=2, title=unique_id)
ax[ax_i].grid()
print(wmape(p['y'], p['NBEATSx']))
Em vez de usar a classe AutoNBEATS
, usamos a classe NBEATSx
e passamos a lista com os nomes das variáveis externas no argumento futr_exog_list
.
Além disso passamos os hiperparâmetros encontrados no passo anterior como argumentos nomeados a partir do próprio dicionário best_config
.
Na hora da previsão passamos os valores das variáveis externas para as datas futuras no argumento futr_df
.
Como temos apenas a variável onpromotion
e os dias da semana, fica fácil saber quais são os valores futuros.
Se fosse uma variável mais complexa, como preços de commodities ou temperatura, seria necessário fazer uma estimativa desses valores futuros.
O WMAPE deste modelo ficou em 23,31%.
Baseline Simples Com Sazonalidade
Para saber se vale a pena colocar um modelo mais complexo em produção, é importante ter uma baseline simples para comparar.
Ela pode ser a solução atual usada em sua empresa ou uma solução simples como a média dos valores passados no mesmo período.
Vamos usar a baseline de sazonalidade, que é uma técnica simples que usa a média dos valores passados no mesmo período do ano.
Esta baseline possui um WMAPE de 31,53%.
Isso significa que tanto o NBEATS sem as variáveis externas quanto o NBEATSx com as variáveis externas são melhores que a baseline.
Visite este artigo para ver como calcular essa baseline.
Função Objetivo WMAPE no PyTorch
Veja aqui o código completo da implementação da função objetivo WMAPE no PyTorch.
Fonte da imagem da capa: https://nixtla.github.io/neuralforecast/models.nbeats.html
Seja o primeiro a saber das novidades em Machine Learning. Me siga no LinkedIn.