La "otra mitad" de cada bloque Transformer. Después de que self-attention mezcla la información entre tokens, cada token pasa por una pequeña red neuronal que procesa esa información — independientemente y con no-linealidad.
El FFN procesa la información de cada token individualmente, agregando la no-linealidad que la atención no puede dar.
Procesar la información de cada token por separado, después de que la atención la mezcló entre tokens. Es la fase de "pensar individualmente".
No-linealidad (vía ReLU) + capacidad de cómputo (vía la expansión). De hecho, la mayoría de los parámetros del modelo (60-70%) viven en los FFN.
Sin FFN, todo el modelo sería lineal (solo aprendería líneas rectas) y apilar muchas capas no agregaría nada — se colapsarían matemáticamente a una sola. Sin FFN, no hay LLM posible.
↓ A continuación, el detalle con ejemplos y visualizaciones · ¿solo querés la intuición? Andá al paso 6b ↓
Comparado con self-attention y multi-head, este paso es mucho más simple. Si entendiste lo de multiplicar por matrices, ya entendiste el 80% de FFN. Lo único realmente nuevo es la función no-lineal entre las dos capas, que vamos a explicar desde cero.
Para entender por qué FFN existe, tenemos que notar algo que se nos puede haber escapado: todas las operaciones del paso 4 y 5 son lineales.
Una operación es lineal si cumple dos propiedades:
Multiplicar por una matriz es lineal. Sumar vectores es lineal. Multiplicar por un número es lineal.
Y aquí está el problema: si encadenás solo operaciones lineales, todo lo que podés hacer sigue siendo... una sola operación lineal. Por más capas que apiles, equivalen a una sola matriz grande.
La "no-linealidad" se introduce con una función llamada función de activación. La más simple y popular es ReLU (Rectified Linear Unit). Suena complicado pero es ridículamente simple:
"Si x es positivo, devolvé x. Si es negativo o cero, devolvé 0."
ReLU(3) = max(0, 3) = 3 ReLU(-2) = max(0, -2) = 0 ReLU(0) = max(0, 0) = 0 ReLU(5.7) = max(0, 5.7) = 5.7 ReLU(-0.1) = max(0, -0.1) = 0 Aplicada a un vector: se aplica elemento por elemento. ReLU([3, -1, 5, -2, 0, 7]) = [3, 0, 5, 0, 0, 7]
La izquierda (x negativo) se "aplasta" a cero. La derecha (x positivo) queda igual. El "doblez" en x=0 es exactamente lo que hace a ReLU una función no-lineal.
Ingresá 6 números y observá cómo ReLU los transforma:
Sin ReLU (u otra función no-lineal), la red solo puede aprender líneas rectas en el espacio. Con ReLU, puede aprender funciones quebradas, y combinando muchas ReLU encadenadas puede aproximar cualquier función que quieras (esto se llama universal approximation theorem).
El FFN tiene 3 operaciones encadenadas, aplicadas a cada token por separado:
Pregunta natural: ¿no sería más simple multiplicar por una matriz 4×4 y aplicar ReLU? ¿Para qué pasar por dim 8 en el medio?
La razón es capacidad expresiva: con más dimensiones intermedias, ReLU tiene más "lugar" para "doblar" la función y aprender patrones complejos. Es como darle al modelo un espacio temporal más grande para hacer sus cálculos.
Analogía: imaginá que tenés que resolver un problema matemático complicado y solo te dan media hoja para hacer las cuentas. Si te dan 4 hojas, podés desarrollar el problema con más pasos intermedios y llegar a una solución mejor. Después escribís solo la respuesta final en la hoja original.
| Modelo | d_model | d_ff | Factor expansión |
|---|---|---|---|
| GPT-2 small | 768 | 3072 | 4x |
| GPT-3 | 12288 | 49152 | 4x |
| LLaMA-2 7B | 4096 | 11008 | ~2.7x |
| Nuestro ejemplo | 4 | 8 | 2x |
El factor más común es 4x. Acá usamos 2x para que los números sean más manejables.
Dato curioso: en un Transformer típico, el FFN tiene más parámetros que el bloque de atención. La mayor parte del modelo (en cantidad de pesos) está en los FFN, no en attention.
x: el vector input (un token), de dimensión d_model = 4.W₁: matriz de pesos aprendida, shape [d_model, d_ff] = [4, 8]. La "expandidora".b₁: vector de bias aprendido, dim 8. Se suma al resultado.ReLU(...): aplica la función ReLU elemento por elemento.W₂: segunda matriz de pesos, shape [d_ff, d_model] = [8, 4]. La "contraedora".b₂: segundo bias, dim 4.El bias (en español "sesgo") es simplemente un vector que se suma al resultado de una multiplicación de matrices. Su propósito: dar más flexibilidad al modelo.
Sin bias: output = x · W Con bias: output = x · W + b Ejemplo: x · W = [2.5, -1.3, 0.8] b = [0.1, 0.5, -0.2] output = [2.5+0.1, -1.3+0.5, 0.8-0.2] = [2.6, -0.8, 0.6]
Sin bias, la transformación lineal tiene que pasar siempre por el origen (cuando x=0, output=0). Con bias, podés "desplazar" la salida. Es como la "b" en y = m·x + b de la línea recta.
A diferencia de self-attention (donde los tokens "se mezclan"), en FFN cada token se procesa por separado:
Los inputs son los 3 vectores que salieron del paso 5 (multi-head attention). Vamos a calcular el FFN para uno de ellos, "perro", paso a paso.
x_perro = [+0.71, +0.55, +0.22, +0.78] (dim 4)
Multiplicamos el vector de dim 4 por W₁ (matriz 4×8) y sumamos el bias b₁ (dim 8). Resultado: vector de dim 8.
Tomamos el vector del paso A y aplicamos ReLU elemento por elemento. Los valores negativos se vuelven 0; los positivos quedan igual.
Multiplicamos el vector "activado" (dim 8) por W₂ (matriz 8×4) y sumamos b₂ (dim 4). Resultado: vector de dim 4 — igual tamaño que el input original.
Click "Siguiente paso" para ver cada fase del FFN aplicada a las 3 palabras. Cada paso muestra qué entra, qué hacemos y qué sale.
En el Paso 7: Residual Connections + LayerNorm vamos a ver cómo se "conectan" attention y FFN entre sí dentro de un bloque, y cómo se evita que las señales se distorsionen al apilar muchos bloques.