Apilar sin perder lo de abajo
28 min de lectura
A la atención ya no le falta nada: tiene sus tres proyecciones, su divisor, sus cabezas y, desde la lección anterior sobre la codificación posicional, unas filas que saben dónde están. Lo que no existe todavía es una capa que se pueda poner encima de otra. Y el artículo pone seis, con lo que sale de cada una haciendo de entrada de la siguiente.
Repetir seis veces una operación que ya funciona suena a no hacer nada, y es justo donde se estrelló el bloque 3. Allí lo que se repetía era un paso de la recurrencia, y componer una función consigo misma multiplica derivadas: un producto de muchos factores parecidos se va a cero o se dispara, y lo que venía de abajo no llega arriba. La profundidad de un Transformer no la pone la frase —la ponen las capas, y son seis se alargue el texto lo que se alargue—, pero seis productos siguen siendo seis productos. Esta lección es lo que hay que ponerle alrededor a una subcapa para que esa cuenta salga distinta, y son dos cosas.
Vuelve al diagrama de la lección sobre el adiós a la recurrencia y mira las cajas que dejó sin explicar. Cinco de las quince se llaman «Suma y layer norm», y de cada una sale una línea que baja por el lado de fuera de su columna y vuelve a entrar por debajo de la subcapa que esa caja cierra. Pincha una: esa línea es la mitad de lo que la caja suma.
Ninguna subcapa reemplaza lo que recibe
La conexión residual es la línea del dibujo escrita en símbolos, y no tiene más que esto:
donde es un hueco —como el del bloque 2 o el del bloque anterior— que ocupa la atención multi-head en la primera subcapa y el perceptrón por posiciones en la segunda. Lo interesante está en su derivada. Pon las filas de una debajo de otra, como en la lección sobre por qué falla el MLP (multilayer perceptron) en secuencias, y llama a esa columna. Derivar una suma es derivar cada sumando:
Ahí está todo. El jacobiano de la subcapa depende de sus pesos y puede valer lo que sea, incluso cero; el otro sumando es la identidad y no depende de nada. Y como la regla de la cadena de su lección convierte una composición en un producto de jacobianos, apilar bloques multiplica paréntesis así, cada uno con su identidad dentro.
Dos bloques, multiplicados
Con dos bloques —y subíndices para distinguir la subcapa de cada uno— el producto se abre así:
Cuatro sumandos: uno que no atraviesa ninguna subcapa, dos que atraviesan una y uno que atraviesa las dos. Con bloques salen , uno por cada manera de elegir en cada paréntesis entre la identidad y la subcapa, y entre ellos hay siempre exactamente uno que no atraviesa nada.
En la lección sobre el gradiente que se desvanece no había paréntesis: cada factor era un producto pelado y el único camino entre el paso y el paso los atravesaba todos. La LSTM (long short-term memory) respondió abriendo una vía aditiva al lado. La conexión residual es esa misma idea con la profundidad en el sitio del tiempo: aquí lo que se apila no son posiciones, son capas.
Con eso queda pagada la deuda de la lección anterior. La codificación posicional se suma una vez, abajo del todo, y ninguna caja vuelve a recordar después dónde estaba cada token: el sumando que vale es lo que la lleva hasta arriba.
Una sola anchura de abajo arriba
La suma trae una restricción escondida: y se suman casilla a casilla, así que la subcapa tiene prohibido devolver otra forma que no sea . Ninguna de las dos la respeta por dentro y las dos hacen el viaje de vuelta: la atención multi-head baja a por cabeza y la devuelve —para esto lo dejó señalado la lección sobre la atención multi-head—, y el perceptrón por posiciones sube a y vuelve a bajar.
Eso convierte a en el único número que hay que elegir para todo el modelo. En el perceptrón multicapa del bloque 2 cada capa tenía su anchura y podía estrecharse a voluntad; aquí, si una capa cambiara de anchura, la siguiente no tendría a qué sumarle nada. Los del artículo son los mismos en la primera capa y en la sexta, y esa rigidez es el precio de apilar.
Cada fila se normaliza con sus propias coordenadas
La suma crea un problema ella sola: si cada subcapa añade sin quitar, lo que circula va creciendo capa a capa, y la lección sobre el producto interno escalado ya midió lo que le pasa a un softmax cuando le llegan números grandes. Hay que devolver la escala a su sitio después de cada suma, y eso es el layer norm.
Fijemos la fila de la posición , con sus coordenadas . Su media y su desviación típica son las de esos números y de ningún otro:
Con ellas se centra y se escala la fila, y después entran dos vectores que se aprenden, y , los dos de coordenadas:
es una constante pequeña — en casi todas las implementaciones— que evita dividir entre cero si una fila tiene sus coordenadas iguales. Los otros dos parecen deshacer el trabajo que se acaba de hacer, y en parte lo hacen: dejar cada fila con media y desviación es una condición que nadie ha pedido, y y devuelven esa libertad convertida en parámetros. Del todo no pueden deshacerla: y cambian de fila en fila, y estos dos vectores son los mismos en todas.
Por qué la fila y no el batch
Sumar a lo ancho de la fila es una decisión, y la alternativa es la contraria: normalizar cada coordenada a lo largo de la columna, que es la normalización por batch. Sobre secuencias falla por tres sitios. Lo que saliera en la posición dependería de qué otras frases hubieran caído en el mismo batch; dependería de y de lo que hubiera en las demás posiciones, incluidas las de padding, que no dicen nada; y habría que guardar estadísticas del entrenamiento para responder después a una frase suelta.
Con la fila no aparece ninguna de las tres, porque y salen de la fila y de nada más. Guarda la tercera: la caja «Suma y layer norm» no mezcla posiciones, y por eso la afirmación de la lección sobre el adiós a la recurrencia —tres de las quince cajas miran más allá de su propia fila— sigue en pie ahora que sabemos qué hacen las otras doce.
La subcapa que no mira a ninguna otra posición
Falta la segunda subcapa: la que el artículo llama FFN (feed-forward network) y esta lección llama el perceptrón por posiciones —los dos nombres son la misma caja—. Por dentro es el perceptrón multicapa del bloque 2 con dos capas y ReLU en medio, aplicado a una fila cada vez:
con y , y con el subíndice nombrando cuál de las dos capas es. es la columna de unos del bloque 2, que aquí repite el sesgo una vez por posición en lugar de una vez por ejemplo, y la ReLU es el del artículo.
Las dos matrices son las mismas para todas las posiciones: la fila y la fila pasan por los mismos pesos, así que la subcapa no puede enterarse de cuál está transformando. Eso es lo que significa «por posiciones» y lo que la deja fuera de las cajas que mezclan. Su papel sale de ahí por descarte: la atención no transforma, mezcla, y éste es el único sitio donde el contenido de una posición pasa por una no linealidad propia.
El reparto no es el que sugiere el dibujo. Con y :
A la izquierda las dos matrices del perceptrón por posiciones, a la derecha las cuatro de la atención sumadas: la caja que parece fontanería lleva el doble de pesos que la que le da nombre a la arquitectura. Ninguno de los dos números menciona a .
El bloque completo, en dos líneas
Con las tres piezas puestas, un bloque del encoder es la misma línea dos veces:
con en el hueco la primera vez y la segunda, y con la salida de la primera haciendo de de la segunda. Sesgos y vectores de normalización incluidos, son parámetros por bloque, y el encoder del artículo apila : unos millones, ninguno de ellos dependiente de la longitud del texto.
Y ahora la concesión. El artículo normaliza después de sumar, y eso deja el layer norm encima del camino de la identidad: el sumando que vale está dentro del paréntesis, pero lo que sale de la caja ha pasado además por una normalización, así que el camino limpio de la primera sección no es tan limpio. Casi todas las implementaciones de hoy escriben —normalizar lo que entra en la subcapa y dejar la suma sin tocar—, que es lo que se conoce como pre-norm. Cuál entrena mejor queda fuera de este curso; que son dos operaciones distintas, no.
Las dos piezas en NumPy
La primera celda es el layer norm sobre cinco posiciones. Mira tres cosas: la media y la desviación típica de cada fila, qué devuelven y , y qué le pasa a la fila cuando se toca la fila .
rng = np.random.default_rng(7)
X = rng.normal(loc=3.0, scale=2.0, size=(5, 8)) # 5 posiciones, d_model = 8
def layer_norm(X, gamma, beta, eps=1e-5):
mu = X.mean(axis=1, keepdims=True) # una media por fila
s = X.std(axis=1, keepdims=True) # una desviacion por fila
return gamma * (X - mu) / np.sqrt(s**2 + eps) + beta
gamma, beta = np.ones(8), np.zeros(8)
Y = layer_norm(X, gamma, beta)
print("entra media por fila:", np.round(X.mean(axis=1), 3))
print(" desviacion :", np.round(X.std(axis=1), 3))
print("sale media por fila:", np.round(Y.mean(axis=1), 3))
print(" desviacion :", np.round(Y.std(axis=1), 3))
Z = layer_norm(X, np.full(8, 4.0), np.full(8, -1.0))
print("\ncon gamma = 4 y beta = -1:", np.round(Z.mean(axis=1), 3), np.round(Z.std(axis=1), 3))
# Toco SOLO la fila 4 y miro que le pasa a la fila 0.
X2 = X.copy()
X2[4] += 10.0
por_columnas = lambda M: (M - M.mean(axis=0)) / M.std(axis=0)
print("\ntoco la fila 4; cuanto se mueve la fila 0")
print(" normalizando por filas :",
round(float(np.abs(layer_norm(X2, gamma, beta)[0] - Y[0]).max()), 12))
print(" normalizando por columnas:",
round(float(np.abs(por_columnas(X2)[0] - por_columnas(X)[0]).max()), 3))
La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador.
Las cinco filas entran con medias entre y y salen con media y desviación ; con y salen con media y desviación , que es lo que esos dos vectores significan. Las dos últimas líneas son la sección anterior comprobada: tocar la fila mueve la fila en por filas y en por columnas.
La segunda mide lo que hace la suma, y para verla sola le quito al bloque el layer norm y la atención: quedan perceptrones por posiciones, uno encima de otro, con y sin la línea que los rodea. La escala de los pesos la he puesto baja a propósito, : al arrancar, una subcapa es una corrección pequeña.
rng = np.random.default_rng(0)
T, d_model, d_ff, N = 8, 16, 32, 12
pesos = [(rng.normal(size=(d_model, d_ff)) * 0.15,
rng.normal(size=(d_ff, d_model)) * 0.15) for _ in range(N)]
def apilar(X, n, con_suma):
for W1, W2 in pesos[:n]:
sub = np.maximum(0.0, X @ W1) @ W2 # el perceptron por posiciones
X = X + sub if con_suma else sub
return X
X = rng.normal(size=(T, d_model))
h = 1e-5
Xh = X.copy()
Xh[0, 0] += h # muevo UNA coordenada de la fila 0
print("capas con la suma sin la suma")
for n in (1, 2, 4, 6, 8, 12):
con = np.linalg.norm((apilar(Xh, n, True) - apilar(X, n, True)) / h)
sin = np.linalg.norm((apilar(Xh, n, False) - apilar(X, n, False)) / h)
print(f"{n:5d} {con:11.4f} {sin:11.3e}")
La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador.
Cada número es una derivada medida por sondeo numérico: cuánto se mueve la salida entera al empujar una sola coordenada de la entrada. Sin la suma cae de con una capa a con doce —siete órdenes de magnitud, y con doce capas, no con doce mil—, porque es un producto de doce factores pequeños. Con la suma se queda entre y todo el camino: el sumando que no atraviesa ningún peso vale , y los demás sólo lo corrigen.
Comprueba tu intuición
Cinco preguntas: qué añade la suma a la derivada, qué le exige a la subcapa, qué números entran al normalizar una fila, dónde están los parámetros y qué cambia al mover el layer norm de sitio.
Cada subcapa se envuelve en . Al componer bloques y derivar, ¿qué aparece que en la cuenta del bloque 3 no había?
¿Qué le exige a una subcapa el hecho de que su salida se le sume a su propia entrada?
El layer norm del bloque normaliza cada fila con sus propias coordenadas. Marca lo que es cierto de lo que sale en la posición .
Marca todas las opciones correctas. Se corrige todo o nada: no hay puntuación parcial.
Con y , ¿cuántos pesos tienen las dos matrices del perceptrón por posiciones, sin contar los sesgos?
Se acepta un margen de ±0.
Normalizar la salida de la subcapa y sumarle la entrada después, , es una operación distinta de la del artículo.
Monta el bloque entero con las dos piezas de la lección.
layer_norm(X, gamma, beta, eps=1e-5) recibe X de forma y devuelve
otra igual: cada fila centrada con su propia media y dividida por
, con la desviación típica de esa misma fila, y después
multiplicada coordenada a coordenada por gamma y sumada a beta.
bloque(X, atencion, p) aplica las dos subcapas. atencion es una función que recibe X y
devuelve algo de la misma forma; p es un diccionario con W1 de forma
, b1 de , W2 de
, b2 de y los cuatro vectores
gamma1, beta1, gamma2, beta2, todos de .
Cada subcapa va envuelta igual: se suma lo que entró a lo que salió y se normaliza el resultado. La segunda subcapa es el perceptrón por posiciones, .
La primera comprobación descarga el intérprete de Python (~15 MB); después queda en la caché del navegador. Este desafío se resuelve mejor con un teclado físico: en el móvil puedes leerlo y volver luego.
El bloque está entero y es apilable, y con eso está terminada una de las dos columnas del dibujo. Vuelve a mirarlo: la de la derecha tiene tres subcapas donde la izquierda tiene dos, la de en medio recibe una flecha que cruza desde la otra columna, y la de abajo se llama «auto-atención enmascarada», que es una palabra que no ha salido en estas seis lecciones.
Las tres cosas son la misma cosa, y es la que separa leer un texto de escribirlo. Un encoder puede mirar la frase entera porque la tiene delante; un decoder produce la salida token a token, y al escribir el cuarto no puede mirar el quinto, que todavía no existe. Eso es la lección siguiente, sobre el encoder, el decoder y las máscaras, y lo bueno es que no toca ni una línea de lo que llevamos: tachar el futuro se hace sumándole a la rejilla de puntuaciones una matriz de ceros y de menos infinitos antes del softmax.
Para profundizar4 fuentes · 4 papers
De dónde sale lo de esta lección, y dónde seguir si quieres más. Nada de aquí hace falta para continuar el curso.
- Attention Is All You Need
Su §3.1 da la envoltura LayerNorm(x + Sublayer(x)) y el apilado N = 6; su §3.3, el perceptrón por posiciones con d_ff = 2048. Cada una en una frase: por qué la suma salva el apilado que hundió al bloque 3 es cosa tuya.
- Layer Normalization
Normaliza cada caso con sus propias unidades en vez de con la columna del batch, y por tus mismas razones: vale en recurrentes y no guarda estadísticas para responder a una frase suelta. Lo ensaya en RNN, no en un Transformer.
- Identity Mappings in Deep Residual Networks
Analiza lo que aquí desarrollas: con la conexión identidad el gradiente lleva un sumando que baja directo entre bloques y no se cancela. Es sobre redes de imagen y pide la identidad también tras la suma, algo que el Transformer no hace.
- On Layer Normalization in the Transformer Architecture
Contesta lo que la lección deja abierto: normalizar antes de la subcapa (pre-norm) y no después deja el gradiente sano al arrancar y quita el calentamiento de la tasa. El post-norm del artículo lo necesita. Medido, no demostrado.