Saltar al contenido
← Álgebra lineal para LLMs

Lección 5 de 10 · Descomposiciones · 17 min

Proyecciones y bases ortogonales

I − r̂r̂ᵀ, ablación direccional y superposición

Notebook del episodio

El código de esta lección, para correrlo y modificarlo en Google Colab, gratis y con tu cuenta. Para guardar tus cambios: Archivo → Guardar una copia en Drive.


Álgebra lineal para LLMs. Episodio cinco: proyecciones y bases ortogonales. Con producto punto, producto exterior y normas ya tenemos todas las piezas para la operación favorita de la interpretabilidad: quedarse con una dirección, o borrarla.

De dónde venimos

x′=x−(x⋅r^) r^=(I−r^r^⊤) xx' = x - (x \cdot \hat r)\, \hat r = (I - \hat r \hat r^\top)\, x

Alineación, ep. 8: ablación direccional

W′=(I−r^r^⊤) WW' = (I - \hat r \hat r^\top)\, W

Alineación, ep. 8: ortogonalizar los pesos

x^=ReLU(W⊤Wx+b)\hat x = \mathrm{ReLU}(W^\top W x + b)

Alineación, ep. 9: superposición de juguete

En Alineación, episodio ocho, la ablación: a x se le resta su componente sobre la dirección r sombrero. En el dibujo, x, su sombra sobre r sombrero, y lo que queda, x prima. La misma cuenta se escribe I menos r sombrero r sombrero transpuesta, por x. Ese mismo proyector, multiplicado por una matriz de pesos, la ortogonaliza: aplica la ablación a cada columna. Y en el nueve, la superposición de juguete: ReLU, rectified linear unit, de W transpuesta W x. En el dibujo, tres features que pasan por dos dimensiones. Hoy, las tres fórmulas.

Parte 1 · Proyectar sobre una dirección

La sombra de un vector sobre otro: el producto punto del episodio uno, convertido en vector.

Sombra y resto

x es dos, uno. r es tres, cuatro: una dirección cualquiera. Primero la hacemos unitaria, dividiendo por su norma, cinco: r sombrero, cero coma seis, cero coma ocho. El producto punto con r sombrero dice cuánto de x va en esa dirección. Ese número por r sombrero es la sombra: un vector sobre la recta de r. Lo que queda es x prima. Y comprobamos que es perpendicular. x punto r sombrero da dos. La sombra, uno coma dos, uno coma seis. x prima, cero coma ocho, menos cero coma seis. Su producto con r sombrero, cero. Y sombra más resto vuelve a dar x.

python
x = np.array([2.0, 1.0])
r = np.array([3.0, 4.0])              # una dirección cualquiera

r_hat = r / np.linalg.norm(r)         # largo 1
print("r̂ =", r_hat)

c = x @ r_hat                          # cuánto de x va hacia r̂
sombra = c * r_hat                     # la componente sobre r̂
x_prima = x - sombra                   # lo que queda
print("x·r̂ =", round(c, 2), "  sombra =", sombra.round(2))
print("x' =", x_prima.round(2))
print("x'·r̂ =", round(x_prima @ r_hat, 6) + 0.0)   # 0: perpendicular
print("sombra + x' =", (sombra + x_prima).round(2))
text
r̂ = [0.6 0.8]
x·r̂ = 2.0   sombra = [1.2 1.6]
x' = [ 0.8 -0.6]
x'·r̂ = 0.0
sombra + x' = [2. 1.]

La proyección, paso a paso

r^=r∥r∥=(3, 4)5=(0,6,  0,8)\hat r = \frac{r}{\lVert r \rVert} = \frac{(3,\,4)}{5} = (0{,}6,\; 0{,}8)

unitario: largo 1

x⋅r^=2⋅0,6+1⋅0,8=2x \cdot \hat r = 2 \cdot 0{,}6 + 1 \cdot 0{,}8 = 2

cuánto de xx va hacia r^\hat r

x′=x−2 r^=(2,1)−(1,2, 1,6)=(0,8, −0,6)x' = x - 2\,\hat r = (2,1) - (1{,}2,\, 1{,}6) = (0{,}8,\, -0{,}6)

restar la sombra

x′⋅r^=0,48−0,48=0x' \cdot \hat r = 0{,}48 - 0{,}48 = 0

lo que queda es perpendicular

r sombrero es r dividido por su norma: tres al cuadrado más cuatro al cuadrado, veinticinco, raíz, cinco. En el dibujo, los dos cuadrados. x punto r sombrero: dos por cero coma seis más uno por cero coma ocho. Dos. x prima es x menos dos veces r sombrero: cero coma ocho, menos cero coma seis. En el dibujo, x en cian, r sombrero en ámbar, la sombra en rojo y x prima en verde. Mirá que x gira despacio: la sombra y el resto se recalculan, y x prima sigue perpendicular. Y la prueba: cero coma cuarenta y ocho menos cero coma cuarenta y ocho, cero.

Parte 2 · El proyector, como matriz

La sombra se puede escribir como una matriz que multiplica a x. Es el producto exterior del episodio tres.

P = r̂ r̂ᵀ

El producto exterior de r sombrero consigo mismo: una matriz de dos por dos, de rango uno. P por x da la sombra, porque se reagrupa: r sombrero por el número r sombrero transpuesta x, como en el episodio tres. I es la identidad, la matriz que no cambia nada: unos en la diagonal. I menos P da el resto: esa es la ablación del episodio ocho de Alineación. Y en dos mil trescientas cuatro dimensiones, con una dirección al azar, medimos el rango de I menos P. P: cero coma treinta y seis, cero coma cuarenta y ocho; cero coma cuarenta y ocho, cero coma sesenta y cuatro. Las dos proyecciones dan la sombra y el resto de antes. P por P es P. Y el rango, dos mil trescientos tres: una dimensión menos.

Cada línea, su término:

  • P = np.outer(r_hat, r_hat) → P=r^ r^⊤P = \hat r\, \hat r^\top — columna × fila: rango 1
  • print("P @ x → Px=r^ (r^⊤x)P x = \hat r\, (\hat r^\top x) — la sombra
  • np.eye(2) - P → (I−P) x=x′(I - P)\, x = x' — el resto: la ablación
  • matrix_rank(Q) → rango(I−r^r^⊤)=d−1\mathrm{rango}(I - \hat r \hat r^\top) = d - 1 — se pierde una sola dirección
python
r_hat = np.array([0.6, 0.8])
x = np.array([2.0, 1.0])

P = np.outer(r_hat, r_hat)             # r̂ r̂ᵀ, el producto exterior
print("P =\n", P.round(2))
print("P @ x       =", (P @ x).round(2))           # la sombra
print("(I - P) @ x =", ((np.eye(2) - P) @ x).round(2))
print("P @ P == P:", np.allclose(P @ P, P))

d = 2304                               # el tamaño de Gemma
r = torch.randn(d); r = r / r.norm()
Q = torch.eye(d) - torch.outer(r, r)
print("rango de I - r̂r̂ᵀ con d = 2304:", torch.linalg.matrix_rank(Q).item())
text
P =
 [[0.36 0.48]
 [0.48 0.64]]
P @ x       = [1.2 1.6]
(I - P) @ x = [ 0.8 -0.6]
P @ P == P: True
rango de I - r̂r̂ᵀ con d = 2304: 2303

Por qué proyectar dos veces no cambia nada

P=r^r^⊤=(0,360,480,480,64)P = \hat r \hat r^\top = \begin{pmatrix} 0{,}36 & 0{,}48 \\ 0{,}48 & 0{,}64 \end{pmatrix}

cada celda: r^i r^j\hat r_i\, \hat r_j

P2=r^ (r^⊤r^) r^⊤=r^ r^⊤=PP^2 = \hat r\, (\hat r^\top \hat r)\, \hat r^\top = \hat r\, \hat r^\top = P

porque r^⊤r^=1\hat r^\top \hat r = 1

(I−P)2=I−2P+P2=I−P(I - P)^2 = I - 2P + P^2 = I - P

borrar dos veces = borrar una

P, celda por celda: r sombrero i por r sombrero j. En el dibujo, la columna r sombrero por la fila r sombrero transpuesta. P al cuadrado: reagrupamos el medio, r sombrero transpuesta r sombrero, que es la norma al cuadrado: uno. En el dibujo, cero coma treinta y seis más cero coma sesenta y cuatro. Queda P otra vez. Una matriz que cumple esto se llama proyector. Lo mismo con I menos P: desarrollamos el cuadrado como con números, y P al cuadrado se cambia por P. En el dibujo, x prima punto r sombrero: cero. Ya no tiene sombra, así que borrarla de nuevo no hace nada.

Parte 3 · Bases ortogonales

Dos direcciones perpendiculares y de largo uno alcanzan para describir cualquier vector del plano. Y con productos punto.

Coordenadas = productos punto

q uno es nuestro r sombrero. q dos es r sombrero girado noventa grados. Las dos son unitarias y perpendiculares: una base ortonormal. Las coordenadas de x en esa base salen con dos productos punto. Y con esas coordenadas reconstruimos x. Ponemos la base como columnas de una matriz Q: Q transpuesta Q tiene que dar la identidad. Y Q transpuesta por x conserva el largo. q uno punto q dos, cero; largo, uno. Coordenadas: dos y menos uno. Reconstruye dos, uno. Q transpuesta Q, la identidad. Y los largos, iguales: raíz de cinco, dos coma dos tres seis.

python
q1 = np.array([0.6, 0.8])              # r̂
q2 = np.array([-0.8, 0.6])             # r̂ girado 90°
print("q1·q2 =", q1 @ q2, "  ‖q1‖ =", np.linalg.norm(q1))

x = np.array([2.0, 1.0])
c1, c2 = x @ q1, x @ q2                # coordenadas = productos punto
print("coordenadas:", round(c1, 2), round(c2, 2))
print("c1 q1 + c2 q2 =", (c1 * q1 + c2 * q2).round(2))

Q = np.column_stack([q1, q2])          # la base, como columnas
print("Qᵀ Q =\n", (Q.T @ Q).round(2))
print("‖Qᵀ x‖ =", round(np.linalg.norm(Q.T @ x), 3),
      "  ‖x‖ =", round(np.linalg.norm(x), 3))
text
q1·q2 = 0.0   ‖q1‖ = 1.0
coordenadas: 2.0 -1.0
c1 q1 + c2 q2 = [2. 1.]
Qᵀ Q =
 [[1. 0.]
 [0. 1.]]
‖Qᵀ x‖ = 2.236   ‖x‖ = 2.236

El mismo x, en otra base

q uno, la dirección de r. q dos, perpendicular. Dos pasos de q uno. Y un paso de q dos, hacia atrás. Llegamos a x. En la base de siempre, x es dos, uno; en esta base, dos, menos uno. El vector no cambió: cambió desde dónde lo medimos. La ablación es justamente esto: escribir x en una base donde r sombrero es un eje, y poner a cero esa coordenada.

Bases ortonormales

x=(x⋅q1) q1+(x⋅q2) q2=2 q1−1 q2x = (x \cdot q_1)\, q_1 + (x \cdot q_2)\, q_2 = 2\, q_1 - 1\, q_2

cada coordenada es un producto punto

Q⊤Q=IQ^\top Q = I

columnas unitarias y perpendiculares

∥Q⊤x∥=∥x∥\lVert Q^\top x \rVert = \lVert x \rVert

cambiar de base no estira nada

Con una base ortonormal, x es la suma de cada q por su producto punto con x. En el dibujo, dos columnas pesadas por dos y por menos uno. Que la base sea ortonormal se escribe Q transpuesta Q igual a la identidad: la celda i j es q i punto q j, uno si es la misma, cero si no. Y por eso cambiar de base no estira nada: el largo se conserva. En el dibujo, dos al cuadrado más menos uno al cuadrado: cinco, igual que dos al cuadrado más uno al cuadrado.

Parte 4 · Ablación en Gemma

Ahora con una dirección de verdad, sacada de los embeddings de Gemma.

Borrar una dirección de los embeddings

Una dirección como las de Alineación, episodio ocho, sobre los embeddings. Cinco pares en inglés que difieren en género, man y woman, he y she, y así. Restamos cada par y promediamos: una diferencia de medias. La hacemos unitaria. El proyector de la ablación, de dos mil trescientos cuatro por dos mil trescientos cuatro. Y medimos cuánto va cada palabra sobre r sombrero, antes y después. King, cero coma cero cinco; queen, menos cero coma treinta y siete; actor, casi cero; actress, menos cero coma cuarenta y cuatro. La dirección lee algo que se parece al género. Después de la ablación, las cuatro dan cero. Y el coseno entre king y queen apenas sube, de cero coma cincuenta y seis a cero coma cincuenta y ocho: es una dirección entre dos mil trescientas cuatro.

Cada línea, su término:

  • pares = [ → r=15∑k(xak−xbk)r = \tfrac{1}{5}\textstyle\sum_k (x_{a_k} - x_{b_k}) — promedio de cinco diferencias
  • r_hat = r / r.norm() → r^=r/∥r∥\hat r = r / \lVert r \rVert — unitaria
  • P = torch.eye(2304) → P=I−r^r^⊤∈R2304×2304P = I - \hat r \hat r^\top \in \mathbb{R}^{2304 \times 2304} — el proyector de la ablación
  • (P @ vec(p)) @ r_hat → (Px)⋅r^=0(P x) \cdot \hat r = 0 — después: nada sobre r^\hat r
python
pares = [(" man", " woman"), (" he", " she"), (" his", " her"),
         (" boy", " girl"), (" father", " mother")]
r = torch.stack([vec(a) - vec(b) for a, b in pares]).mean(0)
r_hat = r / r.norm()                       # la dirección, unitaria

P = torch.eye(2304) - torch.outer(r_hat, r_hat)   # I − r̂ r̂ᵀ
for p in [" king", " queen", " actor", " actress"]:
    antes = (vec(p) @ r_hat).item()
    despues = round(((P @ vec(p)) @ r_hat).item(), 2) + 0.0
    print(f"{p:9} x·r̂ = {antes:+.2f}   después: {despues:+.2f}")

cos = lambda a, b: (a @ b / (a.norm() * b.norm())).item()
k, q = vec(" king"), vec(" queen")
print("cos(king, queen):", round(cos(k, q), 2),
      "→", round(cos(P @ k, P @ q), 2))
text
 king     x·r̂ = +0.05   después: +0.00
 queen    x·r̂ = -0.37   después: +0.00
 actor    x·r̂ = -0.03   después: +0.00
 actress  x·r̂ = -0.44   después: +0.00
cos(king, queen): 0.56 → 0.58

Sobre r̂, antes y después (salida real)

En barras, el tamaño de la componente sobre r sombrero. Queen, cero coma treinta y siete, para el lado negativo. Actress, cero coma cuarenta y cuatro. Después de la ablación, queen: cero. Actress: cero. Una probe que leyera esta dirección ya no puede distinguir nada. Ojo: borrar una dirección de los embeddings no garantiza que el modelo no reconstruya el concepto en capas posteriores. Por eso en Alineación la ablación se aplicaba en todas las capas.

  • queen antes: −0,37
  • actress antes: −0,44
  • queen después: 0,00
  • actress después: 0,00

Parte 5 · Más direcciones que dimensiones

En el plano caben dos direcciones perpendiculares, no más. En dos mil trescientas cuatro dimensiones pasa algo raro.

Casi perpendiculares, de a miles

Mil direcciones al azar, unitarias, en tres tamaños: dos, sesenta y cuatro y dos mil trescientas cuatro dimensiones. V por V transpuesta da todos los cosenos de a pares en una matriz, porque son unitarias. Nos quedamos con la parte de arriba de la diagonal: cada par una vez. Y medimos el coseno promedio, en valor absoluto, y el máximo. En dos dimensiones, promedio cero coma sesenta y cuatro, y hay pares idénticos. En sesenta y cuatro, cero coma uno. En dos mil trescientas cuatro, cero coma cero diecisiete, y el peor par no llega a cero coma uno. Mil direcciones casi perpendiculares entre sí. El cero coma cero dos del episodio uno era esto: uno sobre raíz de d.

python
for d in [2, 64, 2304]:
    V = torch.randn(1000, d)
    V = V / V.norm(dim=1, keepdim=True)       # 1000 direcciones unitarias
    C = V @ V.T                               # todos los cosenos de a pares
    pares = C[torch.triu(torch.ones(1000, 1000), 1).bool()]
    print(f"d={d:5}  |cos| promedio = {pares.abs().mean():.3f}"
          f"   máximo = {pares.abs().max():.3f}")
print("1/√2304 =", round(1 / 2304 ** 0.5, 3))
text
d=    2  |cos| promedio = 0.636   máximo = 1.000
d=   64  |cos| promedio = 0.100   máximo = 0.524
d= 2304  |cos| promedio = 0.017   máximo = 0.093
1/√2304 = 0.021

Tres features en dos dimensiones

La versión de juguete, en el plano. Una feature, un concepto, apunta para arriba. Otra, a ciento veinte grados. Y una tercera, a ciento veinte de las dos. Tres direcciones en dos dimensiones: no pueden ser perpendiculares. El coseno entre cada par es menos cero coma cinco. Si una está activa, las otras dos la leen un poco, pero en negativo. Y eso, una ReLU lo puede tirar.

Leer tres features de dos números

W tiene las tres direcciones como columnas. W transpuesta W es la tabla de cosenos entre ellas. Activamos solo la feature cero y la comprimimos: dos números. Para leer, W transpuesta por x: un producto punto con cada dirección. Y la ReLU tira lo negativo. W transpuesta W: unos en la diagonal, menos cero coma cinco afuera. Leído: uno, menos cero coma cinco, menos cero coma cinco. Con la ReLU, uno, cero, cero: recuperamos la feature exacta. Pero con dos activas a la vez sale cero coma cinco, cero coma cinco: interferencia. La superposición funciona mientras pocas features se activen juntas.

Cada línea, su término:

  • W = np.stack → W∈R2×3W \in \mathbb{R}^{2 \times 3} — cada columna, una feature
  • x = W @ f → x=Wfx = W f — comprimir: 3 → 2
  • leido = W.T @ x → W⊤x=W⊤WfW^\top x = W^\top W f — leer: 3 productos punto
  • np.maximum(0, leido) → f^=ReLU(W⊤Wf)\hat f = \mathrm{ReLU}(W^\top W f) — tirar lo negativo
python
ang = np.radians([90, 210, 330])           # 3 direcciones a 120°
W = np.stack([np.cos(ang), np.sin(ang)])   # 2 × 3: 3 features en 2 dims
print("W =\n", W.round(2))
print("WᵀW =\n", (W.T @ W).round(2))       # cosenos entre features

f = np.array([1.0, 0.0, 0.0])              # solo la feature 0 activa
x = W @ f                                  # comprimir a 2 números
leido = W.T @ x                            # leer las 3 con productos punto
print("x =", x.round(2), "  leído =", leido.round(2))
print("ReLU(leído) =", np.maximum(0, leido).round(2))

f2 = np.array([1.0, 1.0, 0.0])             # dos activas a la vez
print("con dos activas:", np.maximum(0, W.T @ W @ f2).round(2))
text
W =
 [[ 0.   -0.87  0.87]
 [ 1.   -0.5  -0.5 ]]
WᵀW =
 [[ 1.  -0.5 -0.5]
 [-0.5  1.  -0.5]
 [-0.5 -0.5  1. ]]
x = [0. 1.]   leído = [ 1.  -0.5 -0.5]
ReLU(leído) = [1. 0. 0.]
con dos activas: [0.5 0.5 0. ]

Elhage et al., 2022 — Toy Models of Superposition · arXiv:2209.10652

superposición · Polysemanticity, «arising as a result of models storing additional sparse features in "superposition"».

La idea viene de este paper de Anthropic, el de Alineación, episodio nueve. Muestran un modelo de juguete donde una neurona que responde a muchos conceptos sin relación se explica entera: el modelo guarda features adicionales, dispersas, en superposición. Dispersas quiere decir pocas activas a la vez, justo la condición que vimos. Y eso es lo que un sparse autoencoder trata de desarmar.

Las tres fórmulas, leídas enteras

x′=(I−r^r^⊤) xx' = (I - \hat r \hat r^\top)\, x

restar la sombra sobre r^\hat r: ablación

W′=(I−r^r^⊤) WW' = (I - \hat r \hat r^\top)\, W

lo mismo a cada columna: W′W' ya no escribe sobre r^\hat r

f^=ReLU(W⊤Wf)\hat f = \mathrm{ReLU}(W^\top W f)

cosenos de −0,5 que la ReLU tira

De vuelta a Alineación. La ablación es I menos el producto exterior de r sombrero: quedarse con todo menos la sombra. En el dibujo, nuestro x de dos, uno. Multiplicar ese proyector por una matriz de pesos lo aplica a cada columna: después, W prima no puede escribir nada sobre r sombrero. Por eso ortogonalizar los pesos equivale a la ablación, sin hooks. Y la superposición: W transpuesta W son los cosenos entre features, la ReLU tira la interferencia negativa. En el dibujo, lo que leyó el snippet: uno, y dos menos cero coma cinco.

Resumiendo

  • Sombra de xx sobre r^\hat r: (x⋅r^) r^(x \cdot \hat r)\,\hat r; resto x′⊥r^x' \perp \hat r
  • Proyector P=r^r^⊤P = \hat r \hat r^\top, P2=PP^2 = P; ablación =I−P= I - P, rango d−1d - 1
  • Base ortonormal: Q⊤Q=IQ^\top Q = I, coordenadas = productos punto
  • En Gemma: la dirección man − woman, borrada, da 0 en todas
  • En d=2304d = 2304: miles de direcciones casi perpendiculares → superposición
  • Próximo: autovalores, SVD y rango bajo

Papers del episodio

¿Qué tal esta lección?

Sé el primero en calificarla

Entra para calificar

¿Terminaste la lección?

Márcala para sumar +50 XP y mantener tu racha.