Índice

O Que São Redes Neurais Convolucionais?

Redes Neurais Convolucionais (CNN) são redes neurais pensadas para processar dados com alguma estrutura espacial.

Os exemplos mais famosos estão na área de visão computacional, onde as imagens são representadas como matrizes e as CNNs são usadas para extrair características relevantes para prever um alvo.

As convoluções são como filtros que são aplicados sobre os dados.

A diferença entre aplicar uma rede convolucional ou um filtro pré-definido é que a rede aprende os filtros mais relevantes para os dados apresentados.

Uma média móvel pode ser vista como um filtro convolucional: ela multiplica cada valor da série por um peso e soma os resultados.

Redes Convolucionais Temporais

A WaveNet foi um dos primeiros sucessos de redes neurais convolucionais aplicadas a séries temporais.

Ela ficou conhecida, de forma mais geral, como Rede Convolucional Temporal.

arquitetura da rede convolucional temporal Fonte: WaveNet: A Generative Model for Raw Audio

O ingrediente principal que a torna diferente de outras redes convolucionais é que ela usa “convoluções causais e dilatadas”.

As convoluções causais forçam o modelo a aprender a dependência entre os passos sem violar a ordem natural do tempo.

Isso é diferente de outras redes convolucionais que consideram todos os dados disponíveis numa sequência para fazer a modelagem.

A técnica de dilatação faz com que ela considere uma porção cada vez maior de passos da série temporal conforme avança nas camadas mais profundas.

Em geral, como você pode ver na imagem, o fator de dilatação é dobrado a cada camada.

A unidade 2 na primeira camada oculta processa as informações das observações 1 e 2.

Já a unidade 4 da segunda camada oculta processa as observações 1, 2, 3 e 4 através do processamento das unidades ocultas 2 e 4 da primeira camada oculta.

E assim vai.

Isso resulta em um crescimento exponencial do campo receptivo conforme a rede vai avançando às camadas mais profundas.

Essas redes são geralmente mais rápidas de treinar do que as redes recorrentes.

Com a biblioteca NeuralForecast ficou muito fácil treinar essa rede rapidamente em seus dados e é isso que você vai aprender agora.

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 A Rede Neural Convolucional

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 e Arquitetura da TCN

A implementação da TCN nesta biblioteca segue o padrão encoder-decoder.

A ideia é usar uma TCN para aprender uma representação numérica otimizada das observações passadas (encoder) e depois passar essa representação para uma rede neural comum (decoder) que vai gerar as previsões.

Existem vários hiperparâmetros que podem ser ajustados, mas a biblioteca nos dá uma classe que fará a busca automática aleatória pela melhor combinação com base no erro de validação.

Ainda assim é importante entender quais são eles e o intervalo de valores padrão que a biblioteca usa para otimizá-los.

Recomendo que você faça a busca usando o intervalo padrão, principalmente se você ainda não tem muita experiência com redes neurais.

Os números mostrados aqui como opções padrão não são necessariamente os únicos possíveis, mas apenas intervalos escolhidos pelo criador da biblioteca como sensatos para otimizar.

kernel_size

Este é o tamanho de cada filtro usado nas camadas de convolução da TCN.

Um filtro é simplesmente um vetor de pesos que é aplicado deslizando-o sobre a série temporal para gerar uma nova sequência de valores.

Podemos pensar nessa nova sequência como uma transformação da série temporal.

Se temos uma série temporal com 10 observações e um filtro com tamanho 2, primeiro multiplicamos seus dois pesos pelos dois primeiros valores da série temporal e somamos o resultado.

Isso nos dá o valor do primeiro elemento da sequência transformada.

No segundo passo, multiplicamos os dois pesos pelos valor 2 e 3 da série temporal e somamos o resultado para obter o segundo elemento da sequência transformada.

E assim vai até o último.

Aplicamos vários filtros diferentes para gerar várias sequências transformadas.

A ideia é que essas transformações representem melhor os padrões que precisamos para prever as próximas observações.

Este hiperparâmetro não é otimizado durante a busca automática e recebe o valor padrão de 2, igual ao diagrama original da TCN.

dilations

Este é o valor do intervalo de dilatação dos filtros, quantas unidades de tempo eles devem pular ao aplicar a transformação.

Também são valores fixos, múltiplos de 2, conforme o diagrama acima.

input_size_multiplier

O primeiro valor otimizado na busca automática.

Ele define o número de observações que serão usadas como entrada para a TCN.

Os valores possíveis são -1, 4, 16 e 64.

Mas preste atenção, esse valor é multiplicado pelo horizonte!

Ou seja, um valor igual a 4 significa que serão usadas 4 vezes o horizonte como entrada (4 * 90 = 360 dias no nosso exemplo).

Caso o valor seja -1, o objeto passará todas as observações passadas como entrada.

encoder_hidden_size

Este é o tamanho da representação (oculta) otimizada que será retornada pela TCN, que também é o número de filtros aplicados.

O número de unidades desta representação afeta diretamente a capacidade da rede de aprender padrões complexos.

Quanto maior, mais complexos os padrões que ela pode aprender, mas também maior a chance de overfitting.

Este hiperparâmetro é otimizado durante a busca automática, seus valores possíveis são 50, 100, 200 ou 300.

context_size

Antes de passar os valores de saída da TCN para o decoder, eles são transformados novamente para representar o contexto geral das informações da série temporal.

Podemos pensar nisso como um resumo das informações mais importantes que precisamos para fazer as previsões das próximas observações.

Ele é um vetor de tamanho definido pelo context_size.

Os valores possíveis para este hiperparâmetro são 5, 10 e 50.

decoder_hidden_size

Este é o número de unidades nas camadas ocultas da rede neural MLP que será usada como decoder.

Ela tem duas camadas ocultas por padrão, e este é o número de unidades em cada uma delas, não o total entre elas.

Dois valores são testados durante a busca: 64 e 128.

learning_rate

Um dos hiperparâmetros mais impactantes, ela define quanto cada passo da otimização irá modificar os pesos da rede.

Quanto menor a learning_rate, mais lenta a otimização, mas também mais estável.

Durante a busca o valor é escolhido através da amostragem de uma distribuição log uniforme e pode variar entre 0.0001 e 0.1.

max_steps

O número máximo de vezes que a rede neural vai atualizar os pesos durante a otimização.

Ele é intimamente ligado à learning_rate e tende a ser maior quanto menor for o valor dela.

Dois valores são testados: 500 e 1000.

batch_size

O número de exemplos que serão usados para calcular o gradiente para atualizar os pesos da rede neural em cada passo da otimização.

Cada exemplo é uma série derivada da série original, com um tamanho definido pelo input_size_multiplier.

A busca testa dois valores: 16 e 32.

Treinamento da Rede Neural

Com os dados prontos, fazer a busca automática é bem simples.

from neuralforecast import NeuralForecast
from neuralforecast.auto import AutoTCN

models = [AutoTCN(h=h, 
                   num_samples=30,
                   loss=WMAPE())]

model = NeuralForecast(models=models, freq='D')
model.fit(train)

Após importarmos os objetos AutoTCN e NeuralForecast, criamos uma lista com um único objeto AutoTCN e passamos para o objeto NeuralForecast.

A biblioteca permite treinar vários modelos diferentes num mesmo objeto, mas por ser um tutorial, vamos usar apenas o objeto da TCN.

O objeto AutoTCN recebe os seguintes argumentos:

  • h: o horizonte de previsão (quantos passos no futuro queremos prever)
  • num_samples: o número de combinações de hiperparâmetros que serão testadas durante a busca automática

Por padrão, a busca é aleatória, mas também poderíamos usar otimização bayesiana.

Na prática, testar 30 combinações acha uma boa solução em um tempo razoável.

No objeto NeuralForecast passamos o argumento freq que define a frequência da série temporal. No nosso caso, é diária.

Agora, basta chamar o método fit e passar os dados de treino para iniciar o treinamento.

p = model.predict().reset_index()
p = p.merge(valid[['ds','unique_id', 'y']], on=['ds', 'unique_id'], how='left')

Com o modelo treinado, podemos fazer as previsões usando o método predict.

Juntei os dados de previsão com os de validação para facilitar a visualização e o cálculo do erro.

unique_id ds AutoTCN y
MEATS 2017-01-01 00:00:00 159.179 0
PERSONAL CARE 2017-01-01 00:00:00 147.123 0
PERSONAL CARE 2017-01-02 00:00:00 82.5419 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', 'AutoTCN']].plot(ax=ax[ax_i], linewidth=2, title=unique_id)
    

    ax[ax_i].grid()  

grafico da previsao da TCN sem variaveis exogenas

Por fim verificamos o WMAPE da melhor combinação sobre nossos dados de validação, que ficou em 31,53%.

print(wmape(p['y'], p['AutoTCN']))

Se você quiser ver todas as combinações testadas, basta usar o método get_dataframe.

results_df = models[0].results.get_dataframe().sort_values('loss')
results_df

Ele retorna um DataFrame com os valores usados em cada hiperparâmetro e o erro daquela combinação na validação interna da busca.

Isso é muito útil para entender quais hiperparâmetros estão influenciando mais o resultado e guiar seus próximos experimentos.

loss config/encoder_hidden_size config/decoder_hidden_size config/max_steps
0.685285 200 64 500
0.695898 50 128 1000
0.69598 100 64 1000
0.697506 200 128 500
0.702176 100 64 1000

Além disso, usando o método get_best_result podemos ver qual foi a melhor combinação e usá-la para treinar modelos futuros.

best_config = models[0].results.get_best_result().metrics['config']
best_config

{'h': 90,
 'encoder_hidden_size': 200,
 'context_size': 10,
 'decoder_hidden_size': 64,
 'learning_rate': 0.0001565744688888989,
 'max_steps': 500,
 'batch_size': 16,
 'loss': WMAPE(),
 'check_val_every_n_epoch': 100,
 'random_seed': 9,
 'input_size': -90}

Vamos usar essa configuração para treinar um modelo usando variáveis externas além da própria série temporal e ver se conseguimos melhorar o resultado.

Como Adicionar Variáveis Externas à Rede Neural Convolucional

Este é o código completo para treinar a TCN usando variáveis externas e a melhor configuração encontrada pelo AutoTCN.

from neuralforecast import NeuralForecast
from neuralforecast.models import TCN

models = [TCN(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', 'TCN']].plot(ax=ax[ax_i], linewidth=2, title=unique_id)
    

    ax[ax_i].grid()

print(wmape(p['y'], p['TCN']))

Precisamos fazer poucas modificações:

  • Em vez de usar o objeto AutoTCN, usamos o objeto TCN para treinar o modelo. Esse objeto treina uma TCN sem fazer a busca de hiperparâmetros.
  • Passamos a lista com os nomes das variáveis externas que queremos usar como argumento futr_exog_list do objeto TCN. Caso você queira adicionar variáveis externas estáticas (por exemplo, o código do produto), você pode usar o argumento stat_exog_list.
  • Passamos o dicionário best_config como argumentos nomeados do objeto TCN. Assim ele treinará a rede usando essa configuração.
  • No método predict, passamos o DataFrame com os valores futuros para cada período que queremos prever. Por exemplo, se naquele dia planejamos ter uma promoção, passamos o valor 1 para a variável onpromotion e 0 caso contrário.

Este foi o resultado:

gráfico das previsões da TCN com variáveis externas

O WMAPE caiu para 25,91%.

Muito bom! Indicar o dia da semana e se haverá promoção naquele dia ajudou a melhorar o resultado.

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 modelo de TCN com variáveis externas é melhor que a baseline e temos mais confiança em colocá-lo em produção.

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.