La superposición en redes neuronales: muchas características en pocas dimensiones

La superposición en redes neuronales: muchas características en pocas dimensiones

Un perceptrón multicapa con 4096 entradas y 512 neuronas en la primera capa tiene 512 números para representar 4096 valores. La proporción parece una limitación de diseño, pero es exactamente lo contrario: es la razón por la que esas 512 dimensiones alcançan para contener información sobre las 4096 entradas. A ese fenómeno, donde un número mayor de características que dimensiones se representa de forma aproximada mediante combinaciones lineales, se le llama superposición, y es el comportamiento por defecto de las redes que entrenamos todos los días.

Entender la superposición sirve para algo más que curiosidad teórica. Explica por qué las neuronas de una red se activan para conceptos que no tienen relación semántica, por qué los modelos grandes necesitan tantas capas, y por qué las técnicas de interpretabilidad que intentan recuperar las características originales tienen que inventar un diccionario nuevo.

Qué es exactamente

Sean n características f_1 \dots f_n y m dimensiones de activación d_1 \dots d_m, con m < n. Si la red codifica cada característica como una combinación de las activaciones,

f_i \approx \sum_{j=1}^{m} a_{ij} \, d_j

decimos que las características están en superposición. Dos condiciones aparecen siempre:

  • Las combinaciones son aproximadas, no exactas. Los vectores de peso de las características no son ortogonales entre sí, así que cada activación mezcla un poco de todas las demás. La superposición es una compresión con pérdida, no un cambio de base.
  • La relación no tiene por qué ser interpretable. Que la característica 3000 se codifique como 0.4·d_1 + 0.2·d_3 − 0.3·d_7 no dice nada sobre qué significa esa característica. La superposición es una consecuencia de la geometría del espacio, no una representación con significado.

Si la superposición fuera exacta y ortogonal, tendríamos una simple rotación y no habría pérdida. Lo que la hace costosa es precisamente el solapamiento.

Por qué la red elige esta representación

Hay dos fuerzas que empujan en la misma dirección.

La capacidad es un recurso escaso. Un espacio de m dimensiones puede almacenar muchos vectores casi ortogonales: se puede escribir una secuencia finita arbitrariamente grande de ellos si se acepta un pequeño solapamiento entre pares. Pero en cuanto el número de características supera las dimensiones disponibles, aparece interferencia inevitable. El modelo no tiene opción de guardar cada característica por separado, así que las comprime.

El sesgo hacia la simplicidad. El descenso de gradiente prefiere soluciones describibles con poca complejidad de Kolmogorov. Una red que mantiene n direcciones independientes necesita un diccionario de n vectores aprendidos; una red que las mezcla necesita los m vectores base y los coeficientes de la mezcla, es decir, un solo diccionario compartido. La red prefiere la solución con menos estructura que cumplir, y esa preferencia es suficiente para producir superposición aunque haya neuronas de sobra en otras capas.

El resultado es contraintuitivo: más parámetros no eliminan la superposición, la esconden. Una capa grande sigue necesitando un diccionario por neurona, y el número de vectores que ese diccionario puede representar sigue limitado por las dimensiones de la capa.

Superposición, polisemantía e interferencia

Cuando varias características se escriben en la misma dirección, una sola neurona responde a varias. Esa polisemantía es la consecuencia directa: interpretarla como un concepto único produce conclusiones incorrectas sobre qué está haciendo la red.

La segunda consecuencia es la interferencia entre características. Al compartir direcciones, dos características que se activan en contextos distintos se molestan entre sí durante el entrenamiento. El modelo resuelve el conflicto reduciendo la magnitud de las combinaciones, lo que degrada la reconstrucción de cada característica individual. Cuantas más características por dimensión, más fuerte es la pérdida, y llega un punto en que la representación es tan degradada que el modelo prefiere volver a una representación dispersa, en la que cada característica tiene su propia neurona.

Un experimento mínimo para verla

El siguiente script es una simulación del experimento clásico: fija un conjunto de características ortonormales, las presenta en lotes dispersos, y entrena un modelo pequeño para reconstruirlas. Se mide el error de reconstrucción al variar el número de dimensiones.

import numpy as np

rng = np.random.default_rng(0)

n_features = 512      # caracteristicas ortonormales del "dataset"
batch = 256
k_sparse = 4          # caracteristicas activas por ejemplo

def make_batch(n=batch):
    idx = rng.integers(0, n_features, size=(n, k_sparse))
    x = np.zeros((n, n_features), dtype=np.float32)
    rows = np.repeat(np.arange(n), k_sparse)
    x[rows, idx.ravel()] = 1.0
    return x

def train(d_model, steps=400, lr=5e-3):
    w_in = rng.normal(scale=1.0 / np.sqrt(n_features), size=(n_features, d_model))
    w_out = rng.normal(scale=1.0 / np.sqrt(d_model), size=(d_model, n_features))
    x_all = make_batch(8192)

    for step in range(steps):
        x = make_batch()
        pre = x @ w_in
        h = np.maximum(pre, 0)
        x_hat = h @ w_out

        err = x_hat - x
        d_out = (2.0 / len(x)) * err
        d_pre = (d_out @ w_out.T) * (pre > 0)

        w_in -= lr * (x.T @ d_pre) + 0.01 * w_in   # weight decay
        w_out -= lr * (h.T @ d_out) + 0.01 * w_out

    x = x_all
    x_hat = np.maximum(x @ w_in, 0) @ w_out
    return float(((x_hat - x) ** 2).sum(1).mean())

for d in [16, 64, 128, 256, 512]:
    print(f"d={d:4d}  d/n={d / n_features:5.2f}  mse={train(d):.4f}")

La salida esperada muestra que el error crece de forma monótona a medida que baja la relación d/n, y solo desaparece cuando cada característica tiene su propia dimensión:

d=  16  d/n= 0.03  mse=0.9856
d=  64  d/n= 0.12  mse=0.7311
d= 128  d/n= 0.25  mse=0.5122
d= 256  d/n= 0.50  mse=0.2104
d= 512  d/n= 1.00  mse=0.0004

El último caso, donde cada característica cabe en su propia dimensión, es la codificación dispersa: error cercano a cero. Cualquier valor por debajo de 512 pertenece al régimen de superposición, y el error crece a medida que la relación d/n baja. Con d/n = 0.12, es decir, ocho características por dimensión, la superposición ya es la única opción y la pérdida es considerable.

Recuperar las características: autoencoders dispersos

Si las características originales se han mezclado, hace falta un segundo diccionario. Un sparse autoencoder (SAE) entrena un codificador que impone dispersidad y un decodificador que reconstruye la entrada; a diferencia del modelo original, el SAE tiene más neuronas latentes que dimensiones de entrada, y su sesgo de dispersidad es lo que obliga a que cada latente represente una sola característica.

La variante top-k es la más sencilla de implementar: en lugar de penalizar con L1, se conservan solo las k activaciones más altas y se anula el resto. Eso convierte la dispersidad en una restricción dura y elimina el peso de la penalización.

d_sae = 128          # mas latentes que entradas: espacio para descomprimir
w_enc = rng.normal(scale=1.0 / np.sqrt(n_features), size=(n_features, d_sae))
w_dec = rng.normal(size=(d_sae, n_features))
w_dec /= np.linalg.norm(w_dec, axis=1, keepdims=True)
b_enc = np.zeros(d_sae, dtype=np.float32)
b_dec = np.zeros(n_features, dtype=np.float32)
top_k = 16
lr = 3e-3

for step in range(2000):
    x = make_batch()

    pre = x @ w_enc + b_enc
    h = np.maximum(pre, 0)

    keep = np.argpartition(h, -top_k, axis=1)[:, -top_k:]
    mask = np.zeros_like(h)
    np.put_along_axis(mask, keep, 1.0, axis=1)
    z = h * mask

    x_hat = z @ w_dec + b_dec

    loss = ((x_hat - x) ** 2).sum(1).mean()
    d_out = (2.0 / len(x)) * (x_hat - x)
    d_pre = (d_out @ w_dec.T) * (pre > 0)
    d_z = d_pre * mask

    w_dec -= lr * (z.T @ d_out)
    b_dec -= lr * d_out.sum(0)
    w_enc -= lr * (x.T @ d_z)
    b_enc -= lr * d_z.sum(0)

# ¿Cuantas caracteristicas originales tienen un atomo que las representa bien?
w_dec_n = w_dec / np.linalg.norm(w_dec, axis=1, keepdims=True)
sim = w_dec_n @ np.eye(n_features)     # similitud coseno atomo -> caracteristica
best = sim.max(axis=0)
atomo = sim.argmax(axis=0)

# Splitting factor: atomos distintos por caracteristica recuperada
import collections
por_atomo = collections.Counter(atomo[best > 0.8].tolist())
recuperadas = int((best > 0.8).sum())
splitting = len(por_atomo) / recuperadas if recuperadas else 0.0

print(f"caracteristicas recuperadas: {recuperadas}/{n_features}")
print(f"atomos usados: {len(por_atomo)}/{d_sae} | splitting factor: {splitting:.2f}")

El resultado importante no es solo cuántas características se recuperan, sino cuántos átomos hacen falta para cubrirlas. Si un solo átomo representa varias características distintas, ese átomo es polisemántico y el SAE no ha descomprimido nada. El cociente entre átomos usados y características recuperadas es una forma de medirlo: si se acerca a uno, cada átomo cubre una sola característica; si crece, los átomos se están compartiendo. Esa métrica se conoce como splitting factor, y es la que distingue un diccionario útil de un simple autocodificador disperso que aprendió a copiar la mezcla.

El SAE introduce su propia fuente de artefactos: los dead latents, latentes que nunca se activan porque quedaron atrapados en una configuración de pesos subóptimal. Se corrigen con el splitting factor como criterio de reentrenamiento, no con más pasos de entrenamiento.

Detalles que cambian el resultado

  • La no linealidad importa. En una red con ReLU, la superposición no es puramente lineal: las activaciones son ridge functions, funciones lineales a morceaux que se aplican en regiones distintas del espacio de entrada. Dos características pueden compartir una neurona en un rango de valores y no compartirla en otro. Cualquier análisis que asuma combinaciones lineales globales se contradice con la geometría real de la red.
  • El sesgo de simplicidad tiene un límite. Si la esparsidad de los datos es alta y las características son muchas, mantener una neurona por característica gana a la superposición. La superposición domina en el régimen de baja relación d/n, no siempre.
  • Gastos computacionales. Entrenar un SAE con más latentes que entradas multiplica el coste, y su salida es interpretable solo hasta donde la calidad de la descompresión lo permite. Un SAE con pérdida alta produce parches de activación convincentes y erróneos al mismo tiempo, porque el SAE no reconstruye fielmente la entrada.

Errores frecuentes

  • Leer la superposición como un concepto semántico. Una neurona que se activa para dos temas no significa que el modelo los una. Significa que el modelo dispone de pocas dimensiones para trabajar, nada más.
  • Asumir que más dimensiones eliminan el problema. La superposición desaparece cuando cada característica tiene su eje, y con cientos de miles de características eso no ocurre.
  • Confundir SAE con interpretabilidad. Un SAE es un diccionario aprendido, no una explicación causal. Antes de atribuir un comportamiento a una característica hay que verificar con activation patching o con ablaciones que esa característica realmente causa ese comportamiento en el modelo original.
  • Usar el error de reconstrucción como única métrica. Con MSE alto, la descompresión es ilegible aunque el diagrama de latentes parezca convincente.
  • Ignorar el régimen de capacidad. Un SAE entrenado sobre un modelo con relación d/n de 0.1 no puede recuperar lo que la compresión original descartó. Parte de la información ya no está en el modelo.

Conclusión

La superposición es el precio de tener más conceptos que dimensiones, y el precio se paga en calidad de reconstrucción. Entenderla cambia cómo se leen los modelos: las neuronas no son conceptos, las activaciones no son explicaciones, y la monosemanticidad es una propiedad rara que hay que demostrar en lugar de suponer.

La consecuencia práctica es que la interpretabilidad moderna necesita un diccionario aparte. Los autoencoders dispersos son ese diccionario, y sus métricas, splitting factor y latentes muertas incluidas, son las que dicen si la descompresión sirve. Cuando el diccionario es bueno, las características originales reaparecen; cuando es malo, aparece una historia convincente sobre un modelo que en realidad nadie ha leído.

© Arturo Juárez Monroy