Índice

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 registro
  • family: categoria do produto
  • sales: número de vendas
  • onpromotion: 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 registro
  • y: 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']))

grafico das previsoes do NBEATS sem as variáveis externas

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

grafico das previsoes do NBEATS com as variáveis externas

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.

gráfico das previsões da baseline simples com sazonalidade

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.