Cada posición, un puñado de relojes
29 min read
Queda una casilla por rellenar y no está en la capa, sino en lo que se le entrega. La atención multi-head cerró todo lo demás — maneras de mirar la frase, concatenadas y proyectadas de vuelta al precio de una—, y dejó dicho que ninguna cantidad de cabezas consigue que tenga una fila primera y una última: eso hay que escribirlo en cada fila antes de que la capa la lea. De qué se escribe ahí trata esta lección.
Las respuestas fáciles fallan de tres maneras, y conviene mirarlas porque entre las tres describen lo que hace falta. Escribir el número de la posición en una coordenada rompe la escala: en la posición esa coordenada vale mientras el resto del vector se mueve entre y , y se lleva por delante cualquier producto escalar en el que entre. Dividirlo entre la longitud de la frase, , arregla la escala y estropea otra cosa: el de una frase de ocho tokens señalaría el cuarto token y el de un texto de trescientos señalaría el ciento cincuenta, y sería la misma coordenada con el mismo valor. Y aprender un vector por posición, una tabla como la de los embeddings, funciona —hay modelos que lo hacen— a cambio de que la tabla tenga exactamente tantas filas como posiciones se hayan entrenado, y de que la posición y la sean dos filas sin ninguna relación entre ellas: nada dice que sean vecinas.
Ese último defecto es el que apunta a lo que se quiere de verdad. Piensa en un cuentakilómetros: la rueda de la derecha gira deprisa y distingue perfectamente dos posiciones seguidas, pero se repite cada diez; la de la izquierda casi no se mueve, así que no distingue vecinas, pero sí el principio del recorrido del final. Ninguna rueda sola sirve y todas juntas dan un número que no se repite —y donde, además, dos lecturas separadas por la misma cantidad se parecen igual en cualquier tramo, porque cada rueda ha avanzado lo mismo—. Los senos y cosenos son esa idea sin escalones: una pareja de coordenadas por «rueda», cada pareja girando a su propia velocidad.
Una escalera de longitudes de onda
El artículo define la codificación posicional entrada por entrada, con como índice de la posición —es el del resto del curso, escrito con la letra del artículo— y con de positional encoding:
para . Léelo despacio, porque la estructura está en los índices: las coordenadas van por parejas, la pareja ocupa las coordenadas y , y las dos comparten el mismo argumento. Llamemos a ese divisor invertido, que es lo que la pareja gira por cada posición que se avanza:
A la derecha está la longitud de onda: cuántas posiciones tarda esa pareja en volver al mismo sitio. La pareja tiene y vuelve cada posiciones; la última se queda justo por debajo de , unas posiciones, que sobre cualquier texto es una franja que apenas se mueve. Entre las dos, las longitudes de onda crecen multiplicándose siempre por el mismo factor: son una progresión geométrica, y eso es la escalera que el explorable dibuja de izquierda a derecha.
Lo que sale de ahí es una matriz cuya fila es , exactamente la forma de la de las tres lecciones anteriores. Y se junta con ella sumando:
Fíjate en que la primera pega ha desaparecido sola: seno y coseno viven en pase lo que pase con , así que la codificación de la posición y la de la posición son del mismo tamaño. La segunda también, y por una razón distinta: no aparece en ninguna parte de la fórmula, de modo que la posición se codifica igual en una frase corta que en un texto largo.
Sumar en lugar de concatenar es una decisión, y merece decirse en voz alta lo que cuesta. Concatenar mantendría separadas por construcción las coordenadas del significado y las de la posición, y se pagaría en anchura: reservar de las para la posición deja para todo lo demás. Sumando no se paga anchura ninguna, y a cambio las dos señales comparten las mismas coordenadas y nada garantiza que se puedan volver a separar. Lo que hay a favor es que y se aprenden y pueden quedarse con unas direcciones y no con otras, y que la codificación tiene una estructura muy suya que un embedding entrenado no tiene por qué imitar. Plausible, entonces; no demostrado.
Desplazarse una distancia fija es girar un ángulo fijo
De las tres pegas del principio queda la tercera, que era la que de verdad pedía algo: que dos posiciones vecinas se parezcan, y que se parezcan igual estén donde estén. Aquí se puede demostrar, y ocupa tres líneas.
Fija un desplazamiento y quédate con una sola pareja. Las fórmulas de la suma de ángulos dicen qué le pasa a esa pareja al avanzar posiciones:
—y las he escrito con los factores en ese orden a propósito, porque así las dos líneas son ya un producto de matrices. Las coordenadas nuevas son una combinación de las dos viejas, y los coeficientes de esa combinación son cuatro números que sólo miran a y a :
Mira lo que no está en esa matriz: . La posición de partida se ha quedado entera dentro del vector de la derecha, y el desarrollo la ha dejado ahí. Como cada pareja se transforma sin tocar a las demás, apilar las matrices de a lo largo de la diagonal da una sola matriz para el vector entero:
Esa es la propiedad, y conviene leer bien lo que dice: hay una matriz por cada distancia , y esa matriz vale para todas las posiciones de la frase. Avanzar tres posiciones es aplicar , se avance desde la primera palabra o desde la última. Cada bloque es además un giro —sus dos columnas son perpendiculares y de longitud —, así que no estira ni encoge nada: todas las filas de miden lo mismo.
Lo que de todo esto puede ver una puntuación
es una propiedad de la codificación, y la capa de atención no mira matrices: mira productos escalares. Merece la pena hacer uno y ver qué queda. Con aplicado pareja a pareja:
ha vuelto a desaparecer. Dos posiciones separadas por puntúan entre ellas el mismo número al principio de la frase y en la mitad, y ese número sólo depende de la distancia. Con cada término vale y la suma da , que es lo máximo: ninguna posición puntúa contra otra tanto como contra sí misma.
Y ahora la concesión, porque esto es menos de lo que parece. La capa no calcula : calcula con las dos proyectadas desde , así que al desarrollar el producto salen cuatro términos y sólo uno de ellos es puramente posicional. Lo que la forma sinusoidal garantiza es que ese término existe y que depende sólo de la distancia; una cabeza que quiera aprender «el determinante que tengo justo delante» —el la de la casa amarilla— tiene con qué construirlo, porque la señal de «una posición atrás» es la misma en toda la frase. Si lo construye o no lo decide el entrenamiento, y no esta lección.
La escalera en NumPy
La primera celda monta con dos rodajas y sin un solo bucle, y después imprime la longitud de onda de unas cuantas parejas.
T, d_model = 24, 32
i = np.arange(d_model // 2) # una pareja de coordenadas por cada i
omega = 1.0 / (10000 ** (2 * i / d_model))
pos = np.arange(T)[:, None] # (T, 1) contra (d_model/2,)
PE = np.zeros((T, d_model))
PE[:, 0::2] = np.sin(pos * omega) # coordenadas pares: el seno
PE[:, 1::2] = np.cos(pos * omega) # impares: el coseno de ese mismo angulo
print("PE", PE.shape, " valores entre", round(PE.min(), 3), "y", round(PE.max(), 3))
print("fila 0 :", np.round(PE[0, :8], 3))
print("fila 1 :", np.round(PE[1, :8], 3))
print("fila 12:", np.round(PE[12, :8], 3))
print("\npareja omega longitud de onda")
for j in (0, 1, 2, 8, 15):
print(f"{j:6d} {omega[j]:.6f} {2 * np.pi / omega[j]:14.1f}")
norma = np.linalg.norm(PE, axis=1)
print("\ntodas las filas miden lo mismo:",
bool(np.allclose(norma, np.sqrt(d_model / 2))), "->", round(float(norma[0]), 3))
La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador.
La fila sale , que es lo que tiene que salir: en la posición todos los ángulos valen . Las tres primeras filas de la tabla son la escalera: la pareja vuelve cada posiciones, la cada , la cada —cada una veces la anterior— y la , la última de este , cada , que sobre posiciones es una recta. La última línea es la observación del final de la sección anterior comprobada: todas las filas miden , que es .
La segunda celda comprueba las dos propiedades, que es donde está el contenido de la lección.
T, d_model, k = 24, 32, 3
i = np.arange(d_model // 2)
omega = 1.0 / (10000 ** (2 * i / d_model))
pos = np.arange(T + k)[:, None]
PE = np.zeros((T + k, d_model))
PE[:, 0::2] = np.sin(pos * omega)
PE[:, 1::2] = np.cos(pos * omega)
c, s = np.cos(k * omega), np.sin(k * omega) # los cuatro numeros de cada bloque
M = np.zeros((d_model, d_model))
M[0::2, 0::2] = np.diag(c)
M[0::2, 1::2] = np.diag(s)
M[1::2, 0::2] = np.diag(-s)
M[1::2, 1::2] = np.diag(c)
movidas = PE[:T] @ M.T # M por columna == fila por M traspuesta
print("una sola M mueve las", T, "filas a la vez; error maximo",
f"{np.abs(movidas - PE[k:T + k]).max():.2e}")
print("\nPE_pos . PE_pos+k, desde cuatro posiciones distintas:")
print(" ", np.round([float(PE[p] @ PE[p + k]) for p in (0, 5, 11, 19)], 6))
print(" suma de cosenos:", round(float(np.cos(k * omega).sum()), 6))
print("\ny como cambia con la distancia:")
for kk in (0, 1, 2, 3, 8, 20):
print(f" k = {kk:2d} -> {np.cos(kk * omega).sum():7.3f}")
La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador.
La primera línea es movida a través de las veinticuatro posiciones de una vez, y el error máximo se queda en el orden de : la misma matriz, todas las filas. La segunda es la otra propiedad —cuatro posiciones de partida y cuatro veces , que es exactamente la suma de cosenos— y la tabla del final dice cómo se comporta esa cantidad al alejarse: contra sí misma, a un paso, a tres, y luego baja despacio y sin ser monótona, porque es una suma de cosenos y no una distancia.
Comprueba tu intuición
Cinco preguntas: por qué no vale el número de la posición, de qué depende , cuánto puntúa una posición contra sí misma, qué se rompe al quitar los cosenos, y qué garantiza sumar en vez de concatenar.
¿Qué rompe escribir el número de la posición —, , , …— en una coordenada de ?
La matriz cumple . ¿De qué depende?
Con , ¿cuánto vale ?
A margin of ±0 is accepted.
Te quedas sólo con los senos: una coordenada por pareja, sin su coseno. Marca lo que sigue siendo cierto.
Select every correct option. This is graded all-or-nothing: there is no partial credit.
Sumar la codificación al embedding, en vez de concatenarla, garantiza que el modelo pueda volver a separar significado y posición.
Escribe las dos piezas de la lección.
codificacion_posicional(T, d_model) devuelve de forma ,
con la fila llevando en las coordenadas pares
y en las impares, donde
y la pareja ocupa las coordenadas
y .
desplazamiento(k, d_model) devuelve de forma
: ceros salvo un bloque por pareja,
,
colocado en las filas y columnas y .
Las dos tienen que encajar: aplicada a la fila da la fila , en toda la frase. Supón par.
The first run downloads the Python interpreter (~15 MB); after that it stays in the browser cache. This challenge is much easier to solve on a physical keyboard: on a phone, read it and come back later.
Con esto la entrada está completa: cada fila de trae lo que dice el token y dónde estaba, y la capa de atención de las cuatro lecciones anteriores ya se puede calcular entera sobre ella. Lo que queda es apilarla, que es lo que convierte una capa en un modelo. Y ahí aparece algo que esta lección deja pendiente sin decirlo: la posición se suma una vez, abajo del todo, y tiene que llegar viva hasta la última capa.
Lo que el artículo dibuja alrededor de la atención —una conexión que se salta la capa entera, una normalización, un perceptrón por posiciones— parece fontanería alrededor de lo importante y no lo es. Es lo que decide si una señal metida abajo sigue ahí arriba, y el bloque 3 ya midió por qué al seguir un gradiente a lo largo de una cadena de productos y verlo desvanecerse. Eso es la lección siguiente, sobre el bloque completo con residuales y layer norm.
Further reading2 sources · 1 paper, 1 article
Where this lesson comes from, and where to go next. None of it is needed to carry on with the course.
- Attention Is All You Need
Su §3.5 define los senos y cosenos y afirma —sin probarlo— que desplazarse k posiciones es una función lineal de PE_pos. Esa prueba, la matriz M_k, es el centro de esta lección; el artículo solo dice que existe.
- Transformer Architecture: The Positional Encoding
Hace la cuenta que el artículo se salta: monta la matriz de giro M_k pareja a pareja y comprueba que no depende de la posición, tu misma demostración. Estira algo más el tema hacia la distancia relativa entre posiciones.