Clase 10: Transformers
Ventajas
Desventajas
Objetivo
Codificar y comprimir información en Logits que puedan ser usados para clasificar o para ser utilizados por un Decoder.
Forward Pass en el Encoder
⚠️ Con el tiempo han aparecido distintas variantes del Transformer (pre y post normalización, distintas variantes de la atención y del positional Encodeing), pero la idea general se mantiene.
Objetivo
Tomar información de entrada y generar una salida fijándose sólo en tokens pasados.
Forward Pass en el Decoder
Supongamos que tenemos la siguiente frase:
Me gusta la pizza de Pepperoni
Suponiendo cada palabra como un token, tenemos una secuencia de largo \(L=6\). Su tokenización podría ser algo como:
[105,6587,5475,301,708,358]
Dependiendo del modelo también aplica la adición de tokens especiales según corresponda.
Luego cada token es transformado en un Embedding. En el caso del paper original, el embedding tiene \(d_{model}=512\) dimensiones.
En la sección 3.4 del paper se menciona que los parámetros de los embeddings son multiplicados por \(\sqrt{d_{model}}\).
Un potencial problema que puede tener un transformer es reconocer el orden de las frases.
No es lo mismo decir “El perro del papá mordió al niño” que “El perro del niño mordió al papá”. Las palabras usadas en ambas frases son exactamente las mismas, pero en un orden distinto implican desenlaces distintos. ¿Cómo podemos entender el concepto de orden si no tenemos recurrencia?
Incluso algunos órdenes no tienen tanto sentido lógico: “El niño del perro mordió al papá”.
Corresponden a una manera en la que se pueden generar un vector único que representa el orden en el que aparece cada token.
\[PE_{(pos,2i)} = sin\left(\frac{pos}{10000^{2i/d_{model}}}\right)\] \[PE_{(pos,2i+1)} = cos\left(\frac{pos}{10000^{2i/d_{model}}}\right)\]
Una forma más clara de ver esto es que la posición está definida por sinusoidales de periodo \(2\pi \cdot 10000^{i/d_{model}/2}\).
El positional encoder debe tener el mismo tamaño que el Embedding para que se puedan sumar.
Estabilidad numérica
Por temas de estabilidad el argumento del \(sin(\cdot)\) y \(cos(\cdot)\) se suele implementar como: \[pos \cdot exp\left[-\frac{2i}{d_{model}} log(10000)\right]\]
Regularización
La sección 5.4 menciona que se aplica Dropout posterior a sumar los Embeddings con el Positional Encoding. Se utilizo un \(P_{drop}=0.1\).
Supongamos nuestra secuencia de tokens: [me, gusta, la, pizza, de, pepperoni]
Para simplificar el problema consideremos que nuestros embeddings, y por ende nuestro positional encodings, tienen sólo 4 dimensiones (\(d_{model}=4\)).
\[PE_{pos=0} = [sin(0), cos(0), sin(0), cos(0)] = [0, 1, 0, 1]\] \[PE_{pos=1} = [sin(\frac{1}{10000^{0/4}}), cos(\frac{1}{10000^{0/4}}), sin(\frac{1}{10000^{2/4}}), cos(\frac{1}{10000^{2/4}})] = [0.8415, 0.5403, 0.0099, 1]\]
\[PE_{pos=2} = [sin(\frac{2}{10000^{0/4}}), cos(\frac{2}{10000^{0/4}}), sin(\frac{2}{10000^{2/4}}), cos(\frac{2}{10000^{2/4}})] = [0.9093, -0.4161, 0.02, 0.9998]\]
\[PE_{pos=3} = [sin(\frac{3}{10000^{0/4}}), cos(\frac{3}{10000^{0/4}}), sin(\frac{3}{10000^{2/4}}), cos(\frac{3}{10000^{2/4}})] = [0.1411, -0.99, 0.03, 1]\]
\[PE_{pos=4} = [sin(\frac{4}{10000^{0/4}}), cos(\frac{4}{10000^{0/4}}), sin(\frac{4}{10000^{2/4}}), cos(\frac{4}{10000^{2/4}})] = [-0.7568, -0.6536, 0.04, 0.9992]\]
\[PE_{pos=5} = [sin(\frac{5}{10000^{0/4}}), cos(\frac{5}{10000^{0/4}}), sin(\frac{5}{10000^{2/4}}), cos(\frac{5}{10000^{2/4}})] = [-0.9589, 0.2837, 0.05, 0.99875]\]
Por ejemplo, la palabra Pizza quedará definida como:
\[\begin{align} Input = Embedding(Pizza) + PE_{pos=3} &= [0.3, -0.2, 0.7, 0.1] + [0.1411, -0.99, 0.03, 1] \\ &= [0.4411, -1.19, 0.73, 1.1] \end{align}\]
Este input ahora almacena la información tanto de la palabra como de su posición relativa en la secuencia.
Todos los Positional Encoding son idénticos para cada posición, y se encargan de realzar o atenuar ciertas dimensiones del embedding según la posición del token en la secuencia. En la implementación original estos vectores son fijos y no entrenables.
Implementación Eficiente
En otras implementaciones modernas como los modelos GPT, estos son Embeddings y son entrenables. Cada fila es un embedding de \(d_{model}\) dimensiones asociado a cada posición.
\[Attention(Q,K,V) = Softmax\left(\frac{Q \cdot K^T}{\sqrt{d_k}}\right) V\]
Ejemplo
“La sopa se cocinó en la olla y estaba rica”. Rica podría estar refiriéndose a olla o a sopa. Sabemos que se refiere a la sopa.
El Scaled Dot-Product, más conocido como Self-Attention, es el mecanismo clave en las redes neuronales modernas. Permite determinar la atención/relación que existe entre palabras de una misma secuencia.
Self-Attention dan pie al Multihead Attention.Self-Attention tiene la capacidad de acceder a toda la secuencia, por ende modelar relaciones a larga distancia.Causal Self-Attention, una variante que se utiliza en el Decoder sólo puede ver la relación con tokens pasados.Multihead Attention es paralelizable y no secuencial como las RNN.\[ \begin{array}{c c} X = \left[ \begin{array}{c c c} [a_1,...,a_{256}]\\ [b_1,...,b_{256}]\\ [c_1,...,c_{256}]\\ [d_1,...,d_{256}]\\ [e_1,...,e_{256}]\\ [f_1,...,f_{256}]\\ \end{array} \right] \begin{array}{c c c}Me\\gusta\\la\\pizza\\de\\pepperoni \end{array} & \end{array} \]
👀 Ojito
Esto se debe aplicar a cada secuencia. Por lo tanto se debe agregar una dimensión (como unsqueeze(0)) que contabilice el número de secuencias para \(Q\), \(K\), y \(V\).
Matrices de Proyección
Definiremos 3 matrices de Proyección. Una matriz de proyección permite llevar transportar un vector \(X\) a otro espacio (es decir, son entrenables). En este caso crearemos matrices que puedan multiplicarse con \(X\). Por lo tanto irán desde \(d_{model}\) hasta \(d_q=d_k\) y \(d_v\) respectivamente.
Dimensiones de Q,K y V
:::
Siguiendo nuestro ejemplo: \(d_k = d_v = 64\)
Query (6,64)
\[ \begin{array}{c c} Q = \left[ \begin{array}{c c c} [qa_1,...,qa_{64}]\\ [qb_1,...,qb_{64}]\\ [qc_1,...,qc_{64}]\\ [qd_1,...,qd_{64}]\\ [qe_1,...,qe_{64}]\\ [qf_1,...,qf_{64}]\\ \end{array} \right] \end{array} \]
Key (6,64)
\[ \begin{array}{c c} K = \left[ \begin{array}{c c c} [ka_1,...,ka_{64}]\\ [kb_1,...,kb_{64}]\\ [kc_1,...,kc_{64}]\\ [kd_1,...,kd_{64}]\\ [ke_1,...,ke_{64}]\\ [kf_1,...,kf_{64}]\\ \end{array} \right] \end{array} \]
Value (6,64)
\[ \begin{array}{c c} V = \left[ \begin{array}{c c c} [va_1,...,va_{64}]\\ [vb_1,...,vb_{64}]\\ [vc_1,...,vc_{64}]\\ [vd_1,...,vd_{64}]\\ [ve_1,...,ve_{64}]\\ [vf_1,...,vf_{64}]\\ \end{array} \right] \end{array} \]
👀 Ojito
Como esto se aplica a una sola secuencia, la dimensión real de estos tensores debería ser \((1,6,64)\). Ese 1 cambiará si tenemos más secuencias. Pero, todas las secuencias deben ser del mismo largo.
\[\frac{Q \cdot K^T}{\sqrt{d_k}}\]
Similaridad
Control de Gradientes
\(\sqrt{d_k}\) es un factor que reduce la escala de los valores para el control de los gradientes. Recordar que esta matriz es de parámetros entrenables.
Attention
Dado que el rango de estos valores van de \(-\infty\) a \(\infty\), es más común aplicar una softmax para poder garantizar que la suma de las atenciones para cada palabra “query” sume 1.
Attention Weights
\[Attention(Q,K,V) = Softmax\left(\frac{Q \cdot K^T}{\sqrt{d_k}}\right) V\]
OJO
¿Y, estamos seguros que las atenciones/relaciones obtenidas por este algoritmos son (las) únicas/más correctas?
Multihead Attention
Es una extensión del Self-Attention. En lugar de calcular sólo “una atención” sobre el input, genera distintas “atenciones” en múltiples “cabezas” independientes. Cada Attention Head se encarga de aprender relaciones diferentes, lo que mejora la capacidad del modelo de captar patrones cada vez más complejos.
Normalmente se calculan entre \(h=8\) y \(h=12\) attention heads, las cuales se concatenan para luego pasar por una proyección lineal.
Si queremos calcular \(h=8\) attention heads. Necesitamos 8 \(Q\), 8 \(K\) y 8 \(V\). Por lo tanto, necesitamos 8 matrices de proyección. ¿Cómo lo paralelizamos?
Implementación en paralelo
Podemos definir en realidad todas las matrices de manera análoga rescribiendo las matrices de proyección para \(Q\), \(K\) y \(V\) como una subdivisión de cada embedding en \(h\) cabezas.
Dimensiones de Q, K y V
Independent Heads
Es importante mencionar que cada cabeza debe ser independiente una de otra para que se pueda paralelizar. Para ello basta con transponer las dos primeras dimensiones.
Query/Key/Value (6, 8, 64) (Previo a Transponer)
\[ \begin{array}{c c} Q/K/V = \left[ \begin{array}{c c c} \overbrace{[ia_{1},...,ia_{64}]}^{Head 1}, \overbrace{[ia_{65},...,ia_{128}]}^{Head 2}, ..., \overbrace{[ia_{449},...,ia_{512}]}^{Head 8}\\ [ib_{1},...,ib_{64}],[ib_{65},..., ib_{128}],...[ib_{449},...,ib_{512}]\\ [ic_{1},...,ic_{64}],[ic_{65},..., ic_{128}],...[ic_{449},...,ic_{512}]\\ [id_{1},...,id_{64}],[id_{65},..., id_{128}],...[id_{449},...,id_{512}]\\ [ie_{1},...,ie_{64}],[ie_{65},..., ie_{128}],...[ie_{449},...,ie_{512}]\\ [if_{1},...,if_{64}],[if_{65},..., if_{128}],...[if_{449},...,if_{512}]\\ \end{array} \right] \end{array} \]
OJO
Esto permite calcular cada Head en paralelo. Este procedimiento se aplica a cada secuencia. Por lo tanto, un Multihead Attention recibe Tensores de dimensión \((N,L,h \cdot d_i)\) con \(i=k,v\).
Query/Key/Value (8,6, 64) (Luego de Transponer)
\[ Q/K/V = \begin{array}{c c} \left[ \begin{array}{c c c} \text{Head1}\left\{ \begin{array}{c c c} \begin{bmatrix} [ia_{1},...,ia_{64}]\\ [ib_{1},...,ib_{64}]\\ [ic_{1},...,ic_{64}]\\ [id_{1},...,id_{64}]\\ [ie_{1},...,ie_{64}]\\ [if_{1},...,if_{64}]\\ \end{bmatrix} \end{array} \right. \\ \vdots \\ \vdots \\ \text{Head8}\left\{ \begin{array}{c c c} \begin{bmatrix} [ia_{449},...,ia_{512}]\\ [ib_{449},...,ib_{512}]\\ [ic_{449},...,ic_{512}]\\ [id_{449},...,id_{512}]\\ [ie_{449},...,ie_{512}]\\ [if_{449},...,if_{512}]\\ \end{bmatrix} \end{array} \right. \\ \end{array} \right] \end{array} \]
Luego de Transponer, entonces Q/K/V tienen dimensiones \((N, h, L, d_i)\). Luego se puede aplicar el Self-Attention en paralelo para cada cabeza y para cada secuencia.
Self-Attentions (aka Multihead Attention) (8,6,64)
\[ MSA = \begin{array}{c c} \left[ \begin{array}{c c c} \text{Head1}\left\{ \begin{array}{c c c} \begin{bmatrix} [SAa_1,...,SAa_{64}]\\ [SAb_1,...,SAb_{64}]\\ [SAc_1,...,SAc_{64}]\\ [SAd_1,...,SAd_{64}]\\ [SAe_1,...,SAe_{64}]\\ [SAf_1,...,SAf_{64}]\\ \end{bmatrix} \end{array} \right. \\ \vdots \\ \vdots \\ \text{Head8}\left\{ \begin{array}{c c c} \begin{bmatrix} [SAa_{449},...,SAa_{512}]\\ [SAb_{449},...,SAb_{512}]\\ [SAc_{449},...,SAc_{512}]\\ [SAd_{449},...,SAd_{512}]\\ [SAe_{449},...,SAe_{512}]\\ [SAf_{449},...,SAf_{512}]\\ \end{bmatrix} \end{array} \right. \\ \end{array} \right] \end{array} \]
Self-Attention Transpuesto (6,8,64)
\[ MSA = \begin{array}{c c} \left[ \begin{array}{c c c} \overbrace{[SAa_1,...,SAa_{64}]}^{Head 1}, \overbrace{[SAa_{65},...,SAa_{128}}^{Head 2}, ..., \overbrace{[SAa_{449},...,SAa_{512}]}^{Head 8}\\ [SAb_1,...,SAb_{128}],[SAb_{65},..., SAb_{128}],...[SAb_{449},...,SAb_{512}]\\ [SAc_1,...,SAc_{128}],[SAc_{65},..., SAc_{128}],...[SAc_{449},...,SAc_{512}]\\ [SAd_1,...,SAd_{128}],[SAd_{65},..., SAd_{128}],...[SAd_{449},...,SAd_{512}]\\ [SAe_1,...,SAe_{128}],[SAe_{65},..., SAe_{128}],...[SAe_{449},...,SAe_{512}]\\ [SAf_1,...,SAf_{128}],[SAf_{65},..., SAf_{128}],...[SAf_{449},...,SAf_{512}]\\ \end{array} \right] \end{array} \]
Self-Attention Concatenado (6,512)
Hint: La concatenación en este caso equivale a un reshape de las dos últimas dimensiones. \[ MSA = \begin{array}{c c} \left[ \begin{array}{c c c} [SAa_1,.....,SAa_{512}]\\ [SAb_1,.....,SAb_{512}]\\ [SAc_1,.....,SAc_{512}]\\ [SAd_1,.....,SAd_{512}]\\ [SAe_1,.....,SAe_{512}]\\ [SAf_1,.....,SAf_{512}]\\ \end{array} \right] \end{array} \]
Head Mixing
Los outputs de cada cabeza ahora están uno al lado del otro. Por lo tanto, si aplicamos una capa lineal $W^O de dimensión \((d_v \times h, d_{model}\)), estos parámetros entrenables se encargarán de aprender una combinación lineal que mezcle la información aprendida por cada Attention Head de manera óptima.
Multihead Attention Output (6,512)
\[ Multihead(Q,K,V) = \begin{array}{c c} \left[ \begin{array}{c c c} [SAa_1,.....,SAa_{512}]\\ [SAb_1,.....,SAb_{512}]\\ [SAc_1,.....,SAc_{512}]\\ [SAd_1,.....,SAd_{512}]\\ [SAe_1,.....,SAe_{512}]\\ [SAf_1,.....,SAf_{512}]\\ \end{array} \right] \end{array} \cdot W^O \]
El Multihead Attention Output tiene dimensiones \((N, L, d_{model})\).
Residual Connection (Add&Norm)
Corresponde a una conexión residual. Combina la información de entrada al Multihead y su salida para luego aplicar LayerNorm.
\[Add\&Norm = LayerNorm(X + Multihead(Q,K,V))\]
LayerNorm
El LayerNorm calcula el promedio y la varianza por token normalizando las dimensiones del embedding de cada token.
\[X_{norm} = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}}\cdot \gamma + \beta\]
Regularización
La sección 5.4 menciona que se aplica Dropout posterior a cada sublayer del Encoder (y el Decoder) con \(P_{drop}=0.1\).
La sección 3.3 del paper define el bloque Feed Forward de la siguiente manera:
\[FFN(x) = max(0,x \cdot W_1+b_1)W_2 + b_2\]
Donde \(W_1 \in \mathbb{R}^{d_{model} \times {d_{ff}}}\) y \(W_2 \in \mathbb{R}^{d_{ff} \times {d_{model}}}\). Implementaciones como GPT consideran \(d_{ff}=4 \cdot d_{model}\).
Arquitectura
Residual Connection (Add&Norm)
Al igual que en el Multihead Attention, se la salida de esta capa se une con una conexión residual y se pasa por un LayerNorm.
Encoder Layers
👀 Ojito
Arquitectura Encoder Only
Utilizan el output como input de una prediction Head para tareas de clasificación o regresión.
Arquitectura Encoder-Decoder
En el caso de estas arquitecturas, entonces el output del Encoder sirve como Keys y Values para el proceso de Cross Attention.
\[Attention(Q,K,V) = Softmax\left(\frac{Q \cdot K^T + Mask}{\sqrt{d_k}}\right) V\]
Corresponde a una variante del Self-Attention en el cuál sólo se presta atención a Tokens pasados, esto para preservar las propiedades auto-regresivas.
Implementaciones como GPT utilizan una máscara triangular superior de -\(\infty\) y 1s en el resto. Luego esta máscara se multiplica antes de aplicar la Softmax.
Cross Attention
Este mecanismo permite generar relaciones/atenciones entre dos secuencias de datos distintos. En este caso se relaciona una secuencia “query” con elementos “key” y “values” de otra secuencia. Además limita la generación del Decoder.
Dimensiones de Q, K y V
\[Attention(Q_{decoder},K_{encoder},V_{encoder}) = Softmax\left(\frac{Q_{decoder} \cdot K_{encoder}^T + Mask}{\sqrt{d_k}}\right) V_{encoder}\]
Arquitectura
Corresponde a una capa Feed Forward que proyecta desde \(d_{model}\) hasta \(vocab\_size\) seguida de una Softmax.
¿Por qué es necesaria?
Por que la salida del Decoder tiene dimensiones \((N, L, d_{model})\). Es decir, tenemos \(N\) secuencias de largo \(L\), donde cada token está representado como un embedding de \(d_{model}\) dimensiones, lo cuál no es interpretable por humanos.
Esta capa tiene el objetivo de estimar la probabilidad de que ocurra el siguiente token, de este modo predecir de manera autoregresiva.
Luego la salida del transformer tendrá dimensiones \((N, L, vocab\_size)\), pero la secuencia queda desplazada.
Implementaciones como GPT agregan weight sharing o weighting tying entre la capa de Embedding y la capa final de proyección.
