El mecanismo de atención es una técnica que permite al modelo enfocarse en diferentes partes de la entrada para cada paso de la salida. Esto es particularmente útil en tareas como la traducción automática, donde el modelo necesita alinear segmentos de la entrada con la salida correspondiente.
En un modelo seq2seq tradicional sin atención, se utiliza un codificador para transformar toda la entrada en un vector de características fijo, que luego se pasa a un decodificador para generar la salida. Este enfoque tiene una limitación importante: el vector fijo debe contener toda la información necesaria de la secuencia de entrada, lo cual es difícil cuando las secuencias son largas, ya que puede llevar a la pérdida de información y hacer que el aprendizaje y la traducción sean ineficientes.
El mecanismo de atención aborda este problema permitiendo que el decodificador tenga acceso a toda la secuencia de entrada en cada paso de tiempo. En lugar de depender de un único vector de contexto para toda la secuencia de salida, el modelo puede aprender a atender (es decir, dar más peso o importancia) a diferentes partes de la entrada para cada palabra o elemento de la secuencia de salida que está generando.
El mecanismo de atención ha sido fundamental para mejorar el rendimiento de los modelos seq2seq en muchas tareas de procesamiento de lenguaje natural (NLP), y la idea se ha extendido y refinado en modelos más avanzados como Transformer, que utiliza lo que se llama "auto-atención" (self-attention) para procesar toda la entrada de una vez, en lugar de secuencialmente. Esto permite que los modelos sean aún más eficientes y efectivos en capturar relaciones complejas en los datos.
Se introdujo en 2016 en el artículo: Neural Machine Translation by Jointly Learning to Align and Translate
Un mecanismo de atención es una parte de una red neuronal. En cada paso del decodificador, decide qué partes de la fuente son más importantes. En este contexto, el codificador no tiene que comprimir toda la fuente en un solo vector
En cada paso del decodificador, la atención:
El esquema general del cálculo sería:
En el proceso general descrito arriba, no hemos especificado cómo se calculan exactamente los scores de atención. Puedes aplicar cualquier función que desees, incluso una muy complicada. Sin embargo, usualmente no es necesario hacerlo; hay varias variantes populares y sencillas que funcionan bastante bien.
Las formas más populares de calcular los scores de atención son:
Cuando se habla de los primeros modelos de atención, es muy probable que veas estas variantes:
Atención de Bahdanau - del artículo "Neural Machine Translation by Jointly Learning to Align and Translate" de Dzmitry Bahdanau, KyungHyun Cho y Yoshua Bengio (este es el artículo que introdujo por primera vez el mecanismo de atención).
Atención de Luong - del artículo "Effective Approaches to Attention-based Neural Machine Translation" de Minh-Thang Luong, Hieu Pham y Christopher D. Manning.
Estos pueden referirse tanto a las funciones de score como a los modelos completos utilizados en estos artículos. En esta parte, examinaremos más de cerca estas dos variantes de modelo.
Codificador bidireccional: Para codificar mejor cada palabra fuente, el codificador tiene dos RNN, hacia adelante y hacia atrás, que leen la entrada en direcciones opuestas. Para cada token, se concatenan los estados de los dos RNN.
Score de atención: perceptrón multicapa. Para obtener un score de atención, se aplica un perceptrón multicapa (MLP) a un estado del codificador y un estado del decodificador.
Atención aplicada entre pasos del decodificador: La atención se utiliza entre pasos del decodificador: el estado st se utiliza para calcular la atención y su salida at, y ambos st y at se pasan al decodificador en el paso t.
Mientras que el artículo considera varias variantes del modelo, la que usualmente se llama "atención de Luong" es la siguiente:
Codificador: unidireccional (simple)
Score de atención: función bilineal
Atención aplicada: entre el estado RNN del decodificador st y la predicción para este paso
La atención se utiliza después del paso del RNN decodificador st antes de hacer una predicción. El estado st se utiliza para calcular la atención y su salida at. Luego at se combina con st para obtener una representación actualizada ht, que se utiliza para hacer una predicción.
¿Recuerdas la motivación de la atención? En diferentes pasos, el decodificador puede necesitar enfocarse en diferentes tokens fuente, aquellos que son más relevantes en este paso. Veamos los pesos de atención: ¿qué palabras fuente utiliza el decodificador?
Los ejemplos son del artículo "Traducción Automática Neuronal mediante el Aprendizaje Conjunto de Alineación y Traducción".
De los ejemplos, vemos que la atención aprendió una alineación (suave) entre las palabras fuente y objetivo: el decodificador mira aquellos tokens fuente en los que está traduciendo en el paso actual.
Partiendo del código del modelo seq2seq con feedback para tareas de Traducción Automática Neuronal (NMT) del notebook anterior, se debe implementar el modelo de atención de Bahdanau o Luong.
Aquí tienes algunas operaciones con tensores que te pueden ser útiles para la práctica.
El producto escalar de dos vectores es la suma de los productos de sus componentes. Por ejemplo, el producto escalar de los vectores a y b es:
a⋅b=a1b1+a2b2+a3b3Si tenemos dos vectores en PyTorch, podemos calcular su producto escalar usando la función torch.dot().
import torch
a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])
c = torch.dot(a, b)
# 1*4 + 2*5 + 3*6 = 32
print("Producto escalar:", c.item())
Producto escalar: 32
Si tenemos un grupo de vectores almacenados en una matriz, podemos calcular el producto escalar de todos los vectores en la matriz con un vector dado.
A = torch.tensor([[1, 2, 3], [4, 5, 6]])
b = torch.tensor([7, 8, 9])
C = torch.matmul(A, b)
# 1*7 + 2*8 + 3*9 = 50
# 4*7 + 5*8 + 6*9 = 122
print("Producto matricial:\n", C)
Producto matricial: tensor([ 50, 122])
Si ahora tenemos un tensor correspondiente a un batch de matrices de 2×3×2 (batch, secuencia, embedding) y un batch de vectores de 2×2, podemos calcular el producto escalar de cada matriz con su vector correspondiente en el batch. La figura siguiente muestras los tres primeros casos. La matriz amarilla de 2×3 correspondería al resultado.
import torch
A = torch.arange(1, 13) # vector de 12 elementos
A = A.view(2, 3, 2) # vector reconvertido en una matriz de 2x3x2
B = torch.arange(1, 5)
B = B.view(2, 2)
print(A)
print(B)
tensor([[[ 1, 2], [ 3, 4], [ 5, 6]], [[ 7, 8], [ 9, 10], [11, 12]]]) tensor([[1, 2], [3, 4]])
Para hacer el producto escalar de un batch de matrices con un batch de vectores, podemos usar la función torch.bmm() (batch matrix multiplication).
C = torch.bmm(A, B.unsqueeze(1).transpose(1, 2))
print(C.squeeze())
tensor([[ 5, 11, 17], [53, 67, 81]])