27.3 C
Colombia
martes, julio 8, 2025

Serie del tiempo de la antorcha, episodio closing: Atención


Esta es la última publicación de una introducción de cuatro partes al pronóstico de sequence de tiempo con torch. Estas publicaciones han sido la historia de una búsqueda de predicción de múltiples pasos y, hasta ahora, hemos visto tres enfoques diferentes: pronóstico en un bucle, incorporación de un perceptrón multicapa (MLP) y modelos de secuencia a secuencia. Aquí hay un resumen rápido.

  • Como debe hacerse cuando uno se embarca en un viaje de aventuras, comenzamos con una estudio en profundidad de las herramientas a nuestra disposición: redes neuronales recurrentes (RNN). Entrenamos un modelo para predecir la siguiente observación en línea y luego pensamos en un truco inteligente: ¿qué tal si usamos esto para una predicción de varios pasos, retroalimentando predicciones individuales en un bucle? El resultado resultó ser bastante aceptable.

  • Entonces empezó realmente la aventura. Construimos nuestro primer modelo. “de forma nativa” para la predicción de varios pasos, aliviando un poco la carga de trabajo del RNN e involucrando a un segundo jugador, un MLP pequeño. Ahora, la tarea del MLP period proyectar la salida de RNN en varios puntos temporales en el futuro. Aunque los resultados fueron bastante satisfactorios, no nos quedamos ahí.

  • En cambio, aplicamos a sequence de tiempo numéricas una técnica comúnmente utilizada en el procesamiento del lenguaje pure (PLN): secuencia a secuencia (seq2seq) predicción. Si bien el desempeño del pronóstico no fue muy diferente del caso anterior, encontramos que la técnica es más intuitivamente atractiva, ya que refleja la causal relación entre pronósticos sucesivos.

Hoy enriqueceremos el enfoque seq2seq agregando un nuevo componente: el atención módulo. Introducidos originalmente alrededor de 2014, los mecanismos de atención han ganado enorme fuerza, hasta el punto de que el título de un artículo reciente comienza con “La atención no es todo lo que necesitas”.

La concept es la siguiente.

En la configuración clásica de codificador-decodificador, el decodificador se “prepara” con un resumen del codificador solo una vez: el momento en que inicia su ciclo de pronóstico. A partir de entonces, todo va solo. Sin embargo, con atención, vuelve a ver la secuencia completa de salidas del codificador cada vez que pronostica un nuevo valor. Es más, cada vez se acerca aquellos resultados que parecen relevante para el paso de predicción precise.

Esta es una estrategia particularmente útil en la traducción: al generar la siguiente palabra, un modelo necesitará saber en qué parte de la oración unique enfocarse. Por el contrario, la ayuda de la técnica con las secuencias numéricas probablemente dependerá de las características de la serie en cuestión.

Como antes, trabajamos con vic_elecpero esta vez nos desviamos en parte de la forma en que solíamos utilizarlo. Con el conjunto de datos unique cada dos horas, entrenar el modelo precise lleva mucho tiempo, más de lo que los lectores querrán esperar cuando experimenten. Entonces, agregamos las observaciones por día. Para tener suficientes datos, entrenamos en los años 2012 y 2013, reservando el 2014 para la validación así como la inspección post-entrenamiento.

Intentaremos pronosticar la demanda con hasta catorce días de anticipación. Entonces, ¿cuán largas deben ser las secuencias de entrada? Esta es una cuestión de experimentación; más aún ahora que estamos agregando el mecanismo de atención. (Sospecho que es posible que no maneje tan bien secuencias muy largas).

A continuación, también utilizamos catorce días para la duración de la entrada, pero puede que esa no sea necesariamente la mejor opción posible para esta serie.

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 <- kind(pattern.int(
      n = n,
      dimension = n * sample_frac
    ))
    
  },
  
  .getitem = perform(i) {
    
    begin <- self$begins[i]
    finish <- begin + self$n_timesteps - 1
    lag <- 1
    
    record(
      x = self$x[start:end],
      y = self$x[(start+lag):(end+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)

En cuanto al modelo, nos encontramos nuevamente con los tres módulos acquainted de la publicación anterior: codificador, decodificador y módulo seq2seq de nivel superior. Sin embargo, hay un componente adicional: el atención módulo, utilizado por el decodificador para obtener pesas de atención.

Codificador

El codificador sigue funcionando de la misma manera. Envuelve un RNN y devuelve el estado closing.

encoder_module <- nn_module(
  
  initialize = perform(kind, input_size, hidden_size, num_layers = 1, dropout = 0) {
    
    self$kind <- kind
    
    self$rnn <- if (self$kind == "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 atención

En seq2seq básico, cada vez que tenía que generar un nuevo valor, el decodificador tenía en cuenta dos cosas: su estado anterior y la salida anterior generada. En una configuración enriquecida con atención, el decodificador recibe además la salida completa del codificador. Para decidir qué subconjunto de esa salida debería importar, recibe ayuda de un nuevo agente, el módulo de atención.

Ésta, entonces, es la razón de ser del módulo de atención: dado el estado precise del decodificador y además de las salidas completas del codificador, obtener una ponderación de esas salidas indicativa de cuán relevantes son para lo que el decodificador está haciendo actualmente. Este procedimiento da como resultado el llamado pesas de atención: una puntuación normalizada, para cada paso de tiempo en la codificación, que cuantifica su importancia respectiva.

La atención se puede implementar de varias maneras diferentes. Aquí mostramos dos opciones de implementación, una aditiva y otra multiplicativa.

Atención aditiva

En atención aditiva, las salidas del codificador y el estado del decodificador comúnmente se agregan o concatenan (elegimos hacer lo último, a continuación). El tensor resultante se pasa a través de una capa lineal y se aplica un softmax para la normalización.

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 characteristic dimension
    concat <- torch_cat(record(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
  }
)

Atención multiplicativa

En atención multiplicativa, las puntuaciones se obtienen calculando productos escalares entre el estado del decodificador y todas las salidas del codificador. También en este caso se utiliza un softmax para la normalización.

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)

    # permit 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
  }
)

Descifrador

Una vez que se han calculado los pesos de atención, el decodificador maneja su aplicación actual. En concreto, el método en cuestión, weighted_encoder_outputs()calcula un producto de pesos y salidas del codificador, asegurándose de que cada salida tenga el impacto adecuado.

El resto de la acción luego ocurre en ahead(). Una concatenación de salidas ponderadas del codificador (a menudo denominadas “contexto”) y la entrada precise se ejecuta a través de un RNN. Luego, un conjunto de salida, contexto y entrada RNN se pasa a un MLP. Finalmente, se devuelven tanto el estado RNN como la predicción precise.

decoder_module <- nn_module(
  
  initialize = perform(kind, input_size, hidden_size, attention_type, attention_size = 8, num_layers = 1) {
    
    self$kind <- kind
    
    self$rnn <- if (self$kind == "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 completed to compensate for the absence of an embedding module
    # that, in NLP, would give x the next proportion within the concatenation
    x_rep <- x$repeat_interleave(dim(context)[3], 3) 
    rnn_input <- torch_cat(record(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(record(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)
    record(output, next_hidden)
  }
  
)

seq2seq módulo

El seq2seq El módulo básicamente no ha cambiado (aparte del hecho de que ahora permite la configuración del módulo de atención). Para obtener una explicación detallada de lo que sucede aquí, consulte el publicación anterior.

seq2seq_module <- nn_module(
  
  initialize = perform(kind, input_size, hidden_size, attention_type, attention_size, n_forecast, 
                        num_layers = 1, encoder_dropout = 0) {
    
    self$encoder <- encoder_module(kind = kind, input_size = input_size, hidden_size = hidden_size,
                                   num_layers, encoder_dropout)
    self$decoder <- decoder_module(kind = kind, 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]]
    # record 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
  }
  
)

Al crear una instancia del modelo de nivel superior, ahora tenemos una opción adicional: entre atención aditiva y multiplicativa. En el sentido de “precisión” del rendimiento, mis pruebas no mostraron ninguna diferencia. Sin embargo, la variante multiplicativa es mucho más rápida.

internet <- seq2seq_module("gru", input_size = 1, hidden_size = 32, attention_type = "multiplicative",
                      attention_size = 8, n_forecast = n_forecast)

Al igual que la última vez, en la formación modelo, podemos elegir el grado de obligatoriedad del profesor. A continuación, vamos con una fracción de 0,0, es decir, sin ningún tipo de forzamiento.

optimizer <- optim_adam(internet$parameters, lr = 0.001)

num_epochs <- 1000

train_batch <- perform(b, teacher_forcing_ratio) {
  
  optimizer$zero_grad()
  output <- internet(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 <- internet(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) {
  
  internet$prepare()
  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)))
  
  internet$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 la inspección visible, seleccionamos algunos pronósticos del conjunto de prueba.

internet$eval()

test_preds <- vector(mode = "record", 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 <- internet(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()

Una muestra de predicciones con dos semanas de antelación para el conjunto de prueba, 2014.

Figura 1: Una muestra de predicciones con dos semanas de antelación para el conjunto de prueba, 2014.

No podemos comparar directamente el rendimiento aquí con el de los modelos anteriores de nuestra serie, ya que hemos redefinido pragmáticamente la tarea. El objetivo principal, sin embargo, ha sido introducir el concepto de atención. Específicamente, ¿cómo a mano Implemente la técnica, algo que, una vez que haya entendido el concepto, es posible que nunca tenga que hacer en la práctica. En su lugar, probablemente haría uso de las herramientas existentes que vienen con torch (atención de cabezales múltiples y módulos transformadores), herramientas que podemos presentar en una futura “temporada” de esta serie.

¡Gracias por leer!

Foto por David Clode en desempaquetar

Bahdanau, Dzmitry, Kyunghyun Cho y Yoshua Bengio. 2014. “Traducción automática neuronal mediante el aprendizaje conjunto de alinear y traducir”. CORR abs/1409.0473. http://arxiv.org/abs/1409.0473.

Dong, Yihe, Jean-Baptiste Cordonnier y Andreas Loukas. 2021. La atención no es todo lo que necesita: la atención pura pierde rango doblemente exponencialmente con la profundidad.” impresiones electrónicas de arXivmarzo, 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. Atención es todo lo que necesitas.” impresiones electrónicas de arXivjunio, arXiv:1706.03762. https://arxiv.org/abs/1706.03762.

Vinyals, Oriol, Lukasz Kaiser, Terry Koo, Slav Petrov, Ilya Sutskever y Geoffrey E. Hinton. 2014. “Gramática como lengua extranjera”. 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 y Yoshua Bengio. 2015. “Mostrar, asistir y contar: generación de subtítulos de imágenes neuronales con atención visible”. CORR abs/1502.03044. http://arxiv.org/abs/1502.03044.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles