Esta é a postagem ultimate de uma introdução em quatro partes à previsão de séries temporais com torch
. Essas postagens têm sido a história de uma busca por previsão em múltiplas etapas e, até agora, vimos três abordagens diferentes: previsão em loop, incorporação de um perceptron multicamadas (MLP) e modelos sequência a sequência. Aqui está uma rápida recapitulação.
Como deveria ser quando se parte para uma viagem de aventura, começamos com um estudo aprofundado das ferramentas à nossa disposição: redes neurais recorrentes (RNNs). Treinamos um modelo para prever a próxima observação na fila e, então, pensamos em um hack inteligente: que tal usarmos isso para previsão em várias etapas, realimentando previsões individuais em um loop? O resultado, no fim das contas, foi bastante aceitável.
Então, a aventura realmente começou. Construímos nosso primeiro modelo “nativamente” para previsão em várias etapas, aliviando um pouco a carga de trabalho da RNN e envolvendo um segundo jogador, um minúsculo MLP. Agora, a tarefa do MLP period projetar a saída da RNN para vários pontos no tempo no futuro. Embora os resultados tenham sido bastante satisfatórios, não paramos por aí.
Em vez disso, aplicamos a séries temporais numéricas uma técnica comumente usada em processamento de linguagem pure (PNL): sequência a sequência (seq2seq) previsão. Embora o desempenho da previsão não tenha sido muito diferente do caso anterior, achamos a técnica mais intuitivamente atraente, uma vez que reflete o causal relação entre previsões sucessivas.
Hoje enriqueceremos a abordagem seq2seq adicionando um novo componente: o atenção módulo. Introduzidos originalmente por volta de 2014, os mecanismos de atenção ganharam enorme força, tanto que o título de um artigo recente começa com “Atenção não é tudo que você precisa”.
A ideia é a seguinte.
Na configuração clássica do codificador-decodificador, o decodificador é “preparado” com um resumo do codificador apenas uma única vez: o momento em que inicia seu ciclo de previsão. A partir daí, é por conta própria. Com atenção, porém, ele consegue ver novamente a sequência completa das saídas do codificador sempre que prevê um novo valor. Além do mais, sempre é possível ampliar aqueles resultados que parecem relevante para a etapa de previsão atual.
Esta é uma estratégia particularmente útil na tradução: ao gerar a próxima palavra, um modelo precisará saber em que parte da frase authentic focar. O quanto a técnica ajuda com sequências numéricas, por outro lado, provavelmente dependerá das características da série em questão.
Como antes, trabalhamos com vic_elec
mas desta vez nos desviamos parcialmente da maneira como costumávamos empregá-lo. Com o conjunto de dados authentic, a cada duas horas, o treinamento do modelo atual leva muito tempo, mais tempo do que os leitores gostariam de esperar ao experimentar. Então, em vez disso, agregamos observações por dia. Para termos dados suficientes, treinamos nos anos de 2012 e 2013, reservando 2014 para validação e também para inspeção pós-treinamento.
Tentaremos prever a demanda com até quatorze dias de antecedência. Quanto tempo, então, devem durar as sequências de entrada? Esta é uma questão de experimentação; ainda mais agora que estamos adicionando o mecanismo de atenção. (Suspeito que ele possa não lidar tão bem com sequências muito longas).
Abaixo, também escolhemos quatorze dias para a duração da entrada, mas essa pode não ser necessariamente a melhor escolha possível para esta série.
n_timesteps <- 7 * 2
n_forecast <- 7 * 2
elec_dataset <- dataset(
title = "elec_dataset",
initialize = perform(x, n_timesteps, sample_frac = 1) {
self$n_timesteps <- n_timesteps
self$x <- torch_tensor((x - train_mean) / train_sd)
n <- size(self$x) - self$n_timesteps - 1
self$begins <- type(pattern.int(
n = n,
measurement = n * sample_frac
))
},
.getitem = perform(i) {
begin <- self$begins(i)
finish <- begin + self$n_timesteps - 1
lag <- 1
checklist(
x = self$x(begin:finish),
y = self$x((begin+lag):(finish+lag))$squeeze(2)
)
},
.size = perform() {
size(self$begins)
}
)
batch_size <- 32
train_ds <- elec_dataset(elec_train, n_timesteps)
train_dl <- train_ds %>% dataloader(batch_size = batch_size, shuffle = TRUE)
valid_ds <- elec_dataset(elec_valid, n_timesteps)
valid_dl <- valid_ds %>% dataloader(batch_size = batch_size)
test_ds <- elec_dataset(elec_test, n_timesteps)
test_dl <- test_ds %>% dataloader(batch_size = 1)
Em termos de modelo, encontramos novamente os três módulos acquainted da postagem anterior: codificador, decodificador e módulo seq2seq de nível superior. Contudo, há um componente adicional: o atenção módulo, usado pelo decodificador para obter pesos de atenção.
Codificador
O codificador ainda funciona da mesma maneira. Ele envolve um RNN e retorna o estado ultimate.
encoder_module <- nn_module(
initialize = perform(sort, input_size, hidden_size, num_layers = 1, dropout = 0) {
self$sort <- sort
self$rnn <- if (self$sort == "gru") {
nn_gru(
input_size = input_size,
hidden_size = hidden_size,
num_layers = num_layers,
dropout = dropout,
batch_first = TRUE
)
} else {
nn_lstm(
input_size = input_size,
hidden_size = hidden_size,
num_layers = num_layers,
dropout = dropout,
batch_first = TRUE
)
}
},
ahead = perform(x) {
# return outputs for all timesteps, in addition to last-timestep states for all layers
x %>% self$rnn()
}
)
Módulo de atenção
No seq2seq básico, sempre que period necessário gerar um novo valor, o decodificador levava em consideração duas coisas: seu estado anterior e a saída anterior gerada. Em uma configuração enriquecida com atenção, o decodificador recebe adicionalmente a saída completa do codificador. Ao decidir qual subconjunto dessa saída deve ser importante, ela recebe a ajuda de um novo agente, o módulo de atenção.
Esta é, então, a razão de ser do módulo de atenção: dado o estado atual do decodificador e também as saídas completas do codificador, obtenha uma ponderação dessas saídas indicativa de quão relevantes elas são para o que o decodificador está fazendo atualmente. Este procedimento resulta no chamado pesos de atenção: uma pontuação normalizada, para cada etapa de tempo da codificação, que quantifica sua respectiva importância.
A atenção pode ser implementada de várias maneiras diferentes. Aqui, mostramos duas opções de implementação, uma aditiva e uma multiplicativa.
Atenção aditiva
Na atenção aditiva, as saídas do codificador e o estado do decodificador são comumente adicionados ou concatenados (optamos por fazer o último, abaixo). O tensor resultante é executado através de uma camada linear e um softmax é aplicado para normalização.
attention_module_additive <- nn_module(
initialize = perform(hidden_dim, attention_size) {
self$consideration <- nn_linear(2 * hidden_dim, attention_size)
},
ahead = perform(state, encoder_outputs) {
# perform argument shapes
# encoder_outputs: (bs, timesteps, hidden_dim)
# state: (1, bs, hidden_dim)
# multiplex state to permit for concatenation (dimensions 1 and a couple of should agree)
seq_len <- dim(encoder_outputs)(2)
# ensuing form: (bs, timesteps, hidden_dim)
state_rep <- state$permute(c(2, 1, 3))$repeat_interleave(seq_len, 2)
# concatenate alongside function dimension
concat <- torch_cat(checklist(state_rep, encoder_outputs), dim = 3)
# run by means of linear layer with tanh
# ensuing form: (bs, timesteps, attention_size)
scores <- self$consideration(concat) %>%
torch_tanh()
# sum over consideration dimension and normalize
# ensuing form: (bs, timesteps)
attention_weights <- scores %>%
torch_sum(dim = 3) %>%
nnf_softmax(dim = 2)
# a normalized rating for each supply token
attention_weights
}
)
Atenção multiplicativa
Na atenção multiplicativa, as pontuações são obtidas calculando produtos escalares entre o estado do decodificador e todas as saídas do codificador. Aqui também, um softmax é usado para normalização.
attention_module_multiplicative <- nn_module(
initialize = perform() {
NULL
},
ahead = perform(state, encoder_outputs) {
# perform argument shapes
# encoder_outputs: (bs, timesteps, hidden_dim)
# state: (1, bs, hidden_dim)
# enable for matrix multiplication with encoder_outputs
state <- state$permute(c(2, 3, 1))
# put together for scaling by variety of options
d <- torch_tensor(dim(encoder_outputs)(3), dtype = torch_float())
# scaled dot merchandise between state and outputs
# ensuing form: (bs, timesteps, 1)
scores <- torch_bmm(encoder_outputs, state) %>%
torch_div(torch_sqrt(d))
# normalize
# ensuing form: (bs, timesteps)
attention_weights <- scores$squeeze(3) %>%
nnf_softmax(dim = 2)
# a normalized rating for each supply token
attention_weights
}
)
Decodificador
Uma vez calculados os pesos de atenção, sua aplicação actual é tratada pelo decodificador. Concretamente, o método em questão, weighted_encoder_outputs()
calcula um produto de pesos e saídas do codificador, garantindo que cada saída terá o impacto apropriado.
O resto da ação acontece em ahead()
. Uma concatenação de saídas ponderadas do codificador (geralmente chamada de “contexto”) e entrada atual é executada por meio de um RNN. Então, um conjunto de saída, contexto e entrada RNN é passado para um MLP. Finalmente, tanto o estado RNN quanto a previsão atual são retornados.
decoder_module <- nn_module(
initialize = perform(sort, input_size, hidden_size, attention_type, attention_size = 8, num_layers = 1) {
self$sort <- sort
self$rnn <- if (self$sort == "gru") {
nn_gru(
input_size = input_size,
hidden_size = hidden_size,
num_layers = num_layers,
batch_first = TRUE
)
} else {
nn_lstm(
input_size = input_size,
hidden_size = hidden_size,
num_layers = num_layers,
batch_first = TRUE
)
}
self$linear <- nn_linear(2 * hidden_size + 1, 1)
self$consideration <- if (attention_type == "multiplicative") attention_module_multiplicative()
else attention_module_additive(hidden_size, attention_size)
},
weighted_encoder_outputs = perform(state, encoder_outputs) {
# encoder_outputs is (bs, timesteps, hidden_dim)
# state is (1, bs, hidden_dim)
# ensuing form: (bs * timesteps)
attention_weights <- self$consideration(state, encoder_outputs)
# ensuing form: (bs, 1, seq_len)
attention_weights <- attention_weights$unsqueeze(2)
# ensuing form: (bs, 1, hidden_size)
weighted_encoder_outputs <- torch_bmm(attention_weights, encoder_outputs)
weighted_encoder_outputs
},
ahead = perform(x, state, encoder_outputs) {
# encoder_outputs is (bs, timesteps, hidden_dim)
# state is (1, bs, hidden_dim)
# ensuing form: (bs, 1, hidden_size)
context <- self$weighted_encoder_outputs(state, encoder_outputs)
# concatenate enter and context
# NOTE: this repeating is finished to compensate for the absence of an embedding module
# that, in NLP, would give x a better proportion within the concatenation
x_rep <- x$repeat_interleave(dim(context)(3), 3)
rnn_input <- torch_cat(checklist(x_rep, context), dim = 3)
# ensuing shapes: (bs, 1, hidden_size) and (1, bs, hidden_size)
rnn_out <- self$rnn(rnn_input, state)
rnn_output <- rnn_out((1))
next_hidden <- rnn_out((2))
mlp_input <- torch_cat(checklist(rnn_output$squeeze(2), context$squeeze(2), x$squeeze(2)), dim = 2)
output <- self$linear(mlp_input)
# shapes: (bs, 1) and (1, bs, hidden_size)
checklist(output, next_hidden)
}
)
seq2seq
módulo
O seq2seq
O módulo permanece basicamente inalterado (além do fato de que agora permite a configuração do módulo de atenção). Para uma explicação detalhada do que acontece aqui, consulte o postagem anterior.
seq2seq_module <- nn_module(
initialize = perform(sort, input_size, hidden_size, attention_type, attention_size, n_forecast,
num_layers = 1, encoder_dropout = 0) {
self$encoder <- encoder_module(sort = sort, input_size = input_size, hidden_size = hidden_size,
num_layers, encoder_dropout)
self$decoder <- decoder_module(sort = sort, input_size = 2 * hidden_size, hidden_size = hidden_size,
attention_type = attention_type, attention_size = attention_size, num_layers)
self$n_forecast <- n_forecast
},
ahead = perform(x, y, teacher_forcing_ratio) {
outputs <- torch_zeros(dim(x)(1), self$n_forecast)
encoded <- self$encoder(x)
encoder_outputs <- encoded((1))
hidden <- encoded((2))
# checklist of (batch_size, 1), (1, batch_size, hidden_size)
out <- self$decoder(x( , n_timesteps, , drop = FALSE), hidden, encoder_outputs)
# (batch_size, 1)
pred <- out((1))
# (1, batch_size, hidden_size)
state <- out((2))
outputs( , 1) <- pred$squeeze(2)
for (t in 2:self$n_forecast) {
teacher_forcing <- runif(1) < teacher_forcing_ratio
enter <- if (teacher_forcing == TRUE) y( , t - 1, drop = FALSE) else pred
enter <- enter$unsqueeze(3)
out <- self$decoder(enter, state, encoder_outputs)
pred <- out((1))
state <- out((2))
outputs( , t) <- pred$squeeze(2)
}
outputs
}
)
Ao instanciar o modelo de nível superior, temos agora uma escolha adicional: entre atenção aditiva e multiplicativa. No sentido de “precisão” de desempenho, meus testes não mostraram diferenças. No entanto, a variante multiplicativa é muito mais rápida.
web <- seq2seq_module("gru", input_size = 1, hidden_size = 32, attention_type = "multiplicative",
attention_size = 8, n_forecast = n_forecast)
Assim como da última vez, no treinamento de modelo, podemos escolher o grau de forçamento do professor. Abaixo, vamos com uma fração de 0,0, ou seja, sem forçar nada.
optimizer <- optim_adam(web$parameters, lr = 0.001)
num_epochs <- 1000
train_batch <- perform(b, teacher_forcing_ratio) {
optimizer$zero_grad()
output <- web(b$x, b$y, teacher_forcing_ratio)
goal <- b$y
loss <- nnf_mse_loss(output, goal( , 1:(dim(output)(2))))
loss$backward()
optimizer$step()
loss$merchandise()
}
valid_batch <- perform(b, teacher_forcing_ratio = 0) {
output <- web(b$x, b$y, teacher_forcing_ratio)
goal <- b$y
loss <- nnf_mse_loss(output, goal( , 1:(dim(output)(2))))
loss$merchandise()
}
for (epoch in 1:num_epochs) {
web$practice()
train_loss <- c()
coro::loop(for (b in train_dl) {
loss <-train_batch(b, teacher_forcing_ratio = 0.0)
train_loss <- c(train_loss, loss)
})
cat(sprintf("nEpoch %d, coaching: loss: %3.5f n", epoch, imply(train_loss)))
web$eval()
valid_loss <- c()
coro::loop(for (b in valid_dl) {
loss <- valid_batch(b)
valid_loss <- c(valid_loss, loss)
})
cat(sprintf("nEpoch %d, validation: loss: %3.5f n", epoch, imply(valid_loss)))
}
# Epoch 1, coaching: loss: 0.83752
# Epoch 1, validation: loss: 0.83167
# Epoch 2, coaching: loss: 0.72803
# Epoch 2, validation: loss: 0.80804
# ...
# ...
# Epoch 99, coaching: loss: 0.10385
# Epoch 99, validation: loss: 0.21259
# Epoch 100, coaching: loss: 0.10396
# Epoch 100, validation: loss: 0.20975
Para inspeção visible, escolhemos algumas previsões do conjunto de testes.
web$eval()
test_preds <- vector(mode = "checklist", size = size(test_dl))
i <- 1
vic_elec_test <- vic_elec_daily %>%
filter(12 months(Date) == 2014, month(Date) %in% 1:4)
coro::loop(for (b in test_dl) {
output <- web(b$x, b$y, teacher_forcing_ratio = 0)
preds <- as.numeric(output)
test_preds((i)) <- preds
i <<- i + 1
})
test_pred1 <- test_preds((1))
test_pred1 <- c(rep(NA, n_timesteps), test_pred1, rep(NA, nrow(vic_elec_test) - n_timesteps - n_forecast))
test_pred2 <- test_preds((21))
test_pred2 <- c(rep(NA, n_timesteps + 20), test_pred2, rep(NA, nrow(vic_elec_test) - 20 - n_timesteps - n_forecast))
test_pred3 <- test_preds((41))
test_pred3 <- c(rep(NA, n_timesteps + 40), test_pred3, rep(NA, nrow(vic_elec_test) - 40 - n_timesteps - n_forecast))
test_pred4 <- test_preds((61))
test_pred4 <- c(rep(NA, n_timesteps + 60), test_pred4, rep(NA, nrow(vic_elec_test) - 60 - n_timesteps - n_forecast))
test_pred5 <- test_preds((81))
test_pred5 <- c(rep(NA, n_timesteps + 80), test_pred5, rep(NA, nrow(vic_elec_test) - 80 - n_timesteps - n_forecast))
preds_ts <- vic_elec_test %>%
choose(Demand, Date) %>%
add_column(
ex_1 = test_pred1 * train_sd + train_mean,
ex_2 = test_pred2 * train_sd + train_mean,
ex_3 = test_pred3 * train_sd + train_mean,
ex_4 = test_pred4 * train_sd + train_mean,
ex_5 = test_pred5 * train_sd + train_mean) %>%
pivot_longer(-Date) %>%
update_tsibble(key = title)
preds_ts %>%
autoplot() +
scale_color_hue(h = c(80, 300), l = 70) +
theme_minimal()

Figura 1: Uma amostra de previsões com duas semanas de antecedência para o conjunto de testes, 2014.
Não podemos comparar diretamente o desempenho aqui com o dos modelos anteriores da nossa série, pois redefinimos a tarefa de forma pragmática. O objetivo principal, entretanto, foi introduzir o conceito de atenção. Especificamente, como manualmente implementar a técnica – algo que, depois de entender o conceito, talvez você nunca exact fazer na prática. Em vez disso, você provavelmente usaria as ferramentas existentes que vêm com torch
(módulos transformadores e de atenção multi-cabeças), ferramentas que poderemos apresentar em uma futura “temporada” desta série.
Obrigado por ler!
Foto de David Clode sobre Remover respingo
Bahdanau, Dzmitry, Kyunghyun Cho e Yoshua Bengio. 2014. “Tradução automática neural por aprendizagem conjunta para alinhar e traduzir.” CoRR abs/1409.0473. http://arxiv.org/abs/1409.0473.
Dong, Yihe, Jean-Baptiste Cordonnier e Andreas Loukas. 2021. “Atenção não é tudo que você precisa: a atenção pura perde classificação duplamente exponencial com a profundidade.” impressões eletrônicas arXivmarço, arXiv:2103.03404. https://arxiv.org/abs/2103.03404.
Vaswani, Ashish, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Lukasz Kaiser e Illia Polosukhin. 2017. “Atenção é tudo que você precisa.” impressões eletrônicas arXivjunho, arXiv:1706.03762. https://arxiv.org/abs/1706.03762.
Vinyals, Oriol, Lukasz Kaiser, Terry Koo, Slav Petrov, Ilya Sutskever e Geoffrey E. Hinton. 2014. “Gramática como língua estrangeira”. CoRR abs/1412.7449. http://arxiv.org/abs/1412.7449.
Xu, Kelvin, Jimmy Ba, Ryan Kiros, Kyunghyun Cho, Aaron C. Courville, Ruslan Salakhutdinov, Richard S. Zemel e Yoshua Bengio. 2015. “Mostre, participe e conte: geração de legendas de imagens neurais com atenção visible.” CoRR abs/1502.03044. http://arxiv.org/abs/1502.03044.