Índice
- O Que São Transformers em Deep Learning?
- Temporal Fusion Transformers
- Como Instalar a NeuralForecast Com e Sem Suporte a GPU
- Como Preparar a Série Temporal Para o TFT
- Hiperparâmetros do Temporal Fusion Transformer
- Como Treinar Temporal Fusion Transformers em Python
- Como Adicionar Variáveis Externas ao TFT
- Baseline Simples Com Sazonalidade
- Função Objetivo WMAPE no PyTorch
O Que São Transformers em Deep Learning?
Transformers são uma classe de modelos de deep learning projetados para processar dados sequenciais de forma mais eficiente que as redes neurais recorrentes.
O exemplo mais conhecido de Transformer é o GPT-3, que é um modelo de linguagem natural que consegue gerar textos com uma qualidade impressionante.
A característica principal desses modelos é o mecanismo de “atenção”, que permite atribuir importâncias diferentes para cada elemento da sequência de acordo com as características dela.
Para entender o mecanismo de atenção, vamos comparar com o processo de fazer pães.
Vários tipos de pães usam os mesmos ingredientes (farinha, sal, água, óleo, fermento) e vão ao forno, mas dependendo da quantidade de cada ingrediente e do tempo de forno, o pão terá uma textura diferente.
Em vez de ter que sempre usar a mesma quantidade de cada ingrediente e o mesmo tempo de forno, de acordo com o pão que você quer fazer, você pode aumentar ou diminuir a quantidade de cada um.
O mecanismo de atenção faz algo similar a isso, mas com os dados da sequência.
Na modelagem de séries temporais com uma rede neural comum, os pesos que são multiplicados por cada observação são os mesmos, não importa a sequência.
Já com o mecanismo de atenção o modelo pode atribuir pesos diferentes para cada observação dependendo de características obtidas olhando para a sequência toda.
Ah, então porque não usar Transformers sempre?
Porque eles são “famintos por dados”, ou seja, eles precisam de mais dados para conseguir usar todo esse maquinário com boa performance.
Mas isso não é uma regra, o mais importante é testá-los em seus dados e comparar a performance com outros modelos nos dados de validação.
Temporal Fusion Transformers
Fonte: Temporal Fusion Transformers
Temporal Fusion Transformers (TFT) são uma adaptação dos Transformers especialmente projetada para lidar com dados temporais.
Sua arquitetura é bastante complexa.
O bloco GRN (Gated Residual Network) é responsável pela primeira transformação da sequência original, mas ele possui um mecanismo que só aplica esta transformação se ela for útil (por isso o “Gated”).
Dentro desse bloco, existem GLUs (Gated Linear Units), que definem quanto da transformação será somada à sequência original antes de passar para a próxima camada.
O “residual” vem do fato de que a transformação é somada à sequência original, formato popular usado em redes neurais residuais.
Segundo os autores, isso é importante porque reconhece que algumas séries temporais vão se beneficiar de uma modelagem mais complexa, enquanto outras precisam de modelos mais simples para evitar overfitting.
Desta maneira o modelo se torna mais robusto e pode ser generalizado a diferentes casos de uso.
Além disso, o modelo transforma os valores da sequência original em embeddings antes de passá-los para a próxima camada.
Embeddings são vetores que buscam projetar os dados em um espaço vetorial onde sejam tenham representações mais informativas para o objetivo do modelo.
Este modelo também usa blocos LSTM para criar representações internas que levem em conta as informações preservando a ordem das observações.
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 TFT
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
weekday = pd.get_dummies(data2['date'].dt.weekday)
weekday.columns = ['weekday_' + str(i) for i in range(weekday.shape[1])]
data2 = pd.concat([data2, weekday], axis=1)
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 Temporal Fusion Transformer
A biblioteca que vamos usar tem objetos para fazer todo o processo de busca de hiperparâmetros e treinamento do modelo.
Por padrão esta é uma busca aleatória.
Durante a busca automática de hiperparâmetros, estes são os hiperparâmetros que serão testados e otimizados.
Recomendo que você mantenha os intervalos de busca padrão, principalmente se você não tem muita experiência com o modelo.
Lembrando que esses intervalos não cobrem, necessariamente, todos os valores válidos para esses hiperparâmetros, mas são apenas números sensatos para testar.
input_size_multiplier
Este hiperparâmetro determina quantas observações serão passadas para o TFT como vetor de entrada (features).
Ele é multiplicado pelo horizonte h
para determinar o tamanho final.
Por exemplo, se o input_size_multiplier
for 5 e o h
for 90, o vetor de entrada terá as 450 observações imediatamente anteriores ao período alvo para aquela amostra.
Quanto maior o input_size_multiplier
, mais informações o modelo poderá considerar para fazer a previsão, mas também pode levar a overfitting, por isso é importante testar valores diferentes.
O intervalo padrão é de números inteiros de 1 a 5.
hidden_size
Este hiperparâmetro determina o número de unidades (neurônios) das camadas ocultas do modelo.
Quanto maior, mais complexo o modelo, mas novamente pode levar a overfitting.
Este número é válido para as camadas ocultas de todas as partes do TFT (GRN, Atenção, LSTM, MLP).
Nós poderíamos ajustar o valor de cada parte separadamente, mas isso aumentaria muito as combinações de hiperparâmetros, então os criadores da biblioteca optaram por usar o mesmo valor para todas as partes para simplificar.
O intervalo padrão da busca escolhe um valor entre 64, 128 e 256.
n_head
O mecanismo de atenção é dividido em “cabeças” que aprendem coeficientes diferentes.
Este hiperparâmetro determina o número de cabeças que serão usadas.
Assim como os hiperparâmetros anteriores, quanto maior o número de cabeças, mais complexo o modelo é.
O intervalo padrão da busca escolhe entre dois valores: 4 e 8.
learning_rate
Este hiperparâmetro regula a quantidade de ajuste que o modelo fará em seus pesos a cada iteração de treinamento.
É um dos hiperparâmetros mais importantes.
Valores muito altos podem fazer com que o modelo fique oscilando em volta de um ponto ótimo, enquanto valores muito baixos podem fazer com que o modelo nunca chegue a ele.
Por padrão, a busca escolhe valores entre 0.0001 e 0.1, fazendo amostragem de uma distribuição log uniforme.
scaler_type
Este hiperparâmetro determina o tipo de normalização dos dados de entrada.
Em boa parte dos casos as redes neurais convergem mais rapidamente se os dados estiverem normalizados, pois isso remodela a superfície de otimização e facilita a busca pelo ponto ótimo.
Aqui a busca escolherá entre não usar normalização (None
), normalização padrão (standard
) e normalização robusta (robust
).
A normalização padrão é a popular “subtração da média e divisão pelo desvio padrão”, enquanto a normalização robusta usa a mediana e o desvio absoluto.
max_steps
Este hiperparâmetro está intimamente ligado à learning_rate
.
Ele é o número máximo de iterações para ajustes dos pesos que o modelo fará durante o treinamento.
Quanto menor a learning_rate
, maior o max_steps
deve ser para que o modelo consiga convergir.
Por padrão, a busca escolhe entre os valores 500, 1000 e 2000.
batch_size
Este hiperparâmetro determina quantas séries temporais (unique_ids
) serão usadas para cada iteração do treinamento.
Valores maiores oferecem uma estimativa mais confiável do gradiente (a direção oposta do passo que devemos dar para ajustar os pesos do modelo).
Por padrão, a busca escolhe entre os valores 32, 64, 128 e 256.
windows_batch_size
Dentro do valor acima, este hiperparâmetro determina quantas janelas de cada série temporal serão usadas para cada iteração do treinamento.
Essas janelas são séries derivadas da série original, que são agregadas para criar as amostras que são passadas ao modelo para treinamento.
Os valores padrão são 128, 256, 512 e 1024.
Como Treinar Temporal Fusion Transformers em Python
Treinar um TFT com a biblioteca NeuralForecast é muito simples.
from neuralforecast import NeuralForecast
from neuralforecast.auto import AutoTFT
models = [AutoTFT(h=h,
num_samples=30,
loss=WMAPE())]
model = NeuralForecast(models=models, freq='D')
model.fit(train)
Primeiro criamos uma lista com o objeto AutoTFT
, que engloba a busca de hiperparâmetros e o treinamento do modelo.
Ele precisa estar numa lista porque o NeuralForecast
pode receber mais de um modelo para treinamento.
Por ser um tutorial, vamos usar apenas um modelo, mas na prática você pode usar este mesmo objeto para treinar vários modelos diferentes.
O hiperparâmetro h
determina o horizonte de previsão.
O num_samples
determina quantas combinações de hiperparâmetros serão testadas na busca automática.
Por experiência, 30 combinações te dão uma solução boa sem demorar muito.
O loss
é a função de perda que será usada para otimizar os pesos do modelo.
Criei uma função personalizada para calcular o WMAPE e compartilho o código no fim do artigo.
Depois passamos esta lista para o argumento models
do NeuralForecast
, que é o objeto que vai gerenciar o treinamento.
O argumento freq
é a frequência da série temporal, que no nosso caso é diária.
Agora basta chamar o método fit
para treinar o modelo, passando o dataframe de treinamento como argumento.
p = model.predict().reset_index()
p = p.merge(valid[['ds','unique_id', 'y']], on=['ds', 'unique_id'], how='left')
Terminada a busca e o treinamento, podemos chamar o método predict
para gerar as previsões com o melhor modelo encontrado.
Juntei os valores previstos com os valores reais da validação para facilitar a visualização das previsões e o cálculo do WMAPE.
unique_id | ds | AutoTFT | y |
---|---|---|---|
MEATS | 2017-01-01 00:00:00 | 143.9 | 0 |
PERSONAL CARE | 2017-01-01 00:00:00 | 134.479 | 0 |
PERSONAL CARE | 2017-01-02 00:00:00 | 139.566 | 81 |
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', 'AutoTFT']].plot(ax=ax[ax_i], linewidth=2, title=unique_id)
ax[ax_i].grid()
print(wmape(p['y'], p['AutoTFT']))
Este bloco faz o gráfico para vermos se o modelo está fazendo boas previsões.
Surpreendentemente o TFT não conseguiu capturar o padrão sazonal de vendas usando apenas a série temporal bruta como dados de entrada.
Por isso é importante ter várias maneiras de visualizar os dados em vez de confiar cegamente em apenas uma métrica.
Nosso WMAPE ficou em 49,91%, que você verá que é um resultado muito ruim comparado a modelos mais simples.
Para ver todas as combinações de hiperparâmetros testadas, basta chamar o método get_dataframe
do atributo results
do objeto AutoTFT
.
results_df = models[0].results.get_dataframe().sort_values('loss')
results_df
loss | config/hidden_size | config/n_head | config/learning_rate |
---|---|---|---|
0.344812 | 128 | 4 | 0.00396742 |
0.345202 | 128 | 4 | 0.0008987 |
0.345444 | 128 | 4 | 0.0102537 |
0.428944 | 256 | 4 | 0.000417848 |
0.439017 | 256 | 8 | 0.00596673 |
E para ver a combinação que obteve o melhor resultado, basta chamar o método get_best_result
do mesmo atributo.
best_config = models[0].results.get_best_result().metrics['config']
best_config
{'h': 90,
'hidden_size': 128,
'n_head': 4,
'learning_rate': 0.003967419559112194,
'scaler_type': None,
'max_steps': 2000,
'batch_size': 256,
'windows_batch_size': 1024,
'loss': WMAPE(),
'check_val_every_n_epoch': 100,
'random_seed': 7,
'input_size': 90,
'step_size': 1}
Por curiosidade eu testei retreinar o modelo com estes hiperparâmetros mas com o scaler standard
.
Apesar de melhorar o WMAPE, ele ainda não conseguiu capturar o padrão sazonal.
Vamos tentar melhorá-lo adicionando as variáveis sazonais explicitamente como variáveis externas.
Como Adicionar Variáveis Externas ao TFT
Adaptar o código para adicionar as variáveis externas é simples.
from neuralforecast import NeuralForecast
from neuralforecast.models import TFT
models = [TFT(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', 'TFT']].plot(ax=ax[ax_i], linewidth=2, title=unique_id)
ax[ax_i].grid()
print(wmape(p['y'], p['TFT']))
As diferenças aqui são:
- Usamos o objeto
TFT
em vez doAutoTFT
. - Passamos a lista com os nomes das variáveis externas no argumento
futr_exog_list
. - Passamos o dicionário com os hiperparâmetros como argumentos nomeados
**best_config
. - No método
predict
passamos o dataframe com as variáveis externas para cada observação futura no argumentofutr_df
. Neste caso são dias da semana e dias com promoções, então temos essa informação no momento da previsão.
Caso você queira adicionar variáveis externas estáticas (por exemplo, o código do produto), você pode usar o argumento stat_exog_list
.
Com essas variáveis externas, o WMAPE caiu para 22%, um resultado muito melhor.
Dá pra ver no gráfico que agora o modelo foi capaz de capturar o padrão sazonal.
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%.
Neste caso, nosso TFT sem variáveis externas é muito pior do que essa baseline simples e jamais seria colocado em produção.
Com as variáveis externas, o WMAPE caiu para 22%, o que já justificaria testá-lo em produção.
Ainda assim é importante compará-lo a modelos mais simples e rápidos de treinar, como redes neurais tradicionais e redes neurais convolucionais.
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.
Seja o primeiro a saber das novidades em Machine Learning. Me siga no LinkedIn.