Índice

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

arquitetura do TFT 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 registro
  • family: categoria do produto
  • sales: número de vendas
  • onpromotion: 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 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 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.

grafico das previsoes do TFT sem variaveis externas

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 do AutoTFT.
  • 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 argumento futr_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.

grafico das previsoes do TFT com variaveis externas

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.

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

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.