Skip to content

English · Español

Break — AdamW con weight_decay=0 vs el valor correcto

🇪🇸 Apagamos el weight decay y observamos cómo el modelo memoriza el corpus §A13 más rápido y generaliza peor. Es la prueba más limpia de que el decay no es "regularización opcional" — es lo que mantiene los pesos en el régimen donde el optimizer estima v_t con utilidad.


Síntoma que verá Borja

Dos runs de entrenamiento con seed, schedule, batch size y arquitectura idénticos. Solo difiere una config:

  • Run A (control): weight_decay = 0.1 (el valor recomendado §A13).
  • Run B (break): weight_decay = 0.0.

Tras 2000 steps:

  • Run A: train loss \(\approx 1.85\), val loss \(\approx 2.05\), gap \(\approx 0.20\).
  • Run B: train loss \(\approx 1.55\), val loss \(\approx 2.40\), gap \(\approx 0.85\).

El panel "train vs val loss" del dashboard muestra las dos curvas divergiendo a partir del step ~600 en el Run B; en el Run A se siguen mutuamente dentro de 0.25 a lo largo de todo el run. El panel de norma de pesos del Run B muestra la norma de Frobenius de la tabla de embeddings subiendo monótonamente — en el step 2000 es 2.5× el valor inicial. La del Run A se mantiene dentro de 1.2× del init.

El break, mecánicamente

En experiments/18-break-weight-decay/config.yaml:

# Run A (control)
optimizer:
  name: adamw
  weight_decay: 0.1

# Run B (the break)
optimizer:
  name: adamw
  weight_decay: 0.0   # <-- THIS LINE

O equivalentemente en código: pasa weight_decay=0.0 al constructor de AdamW en src/minitrain/loop.py.

Sin otros cambios. Todo el break es un solo número.

Por qué esto enseña el concepto

A escala §A13, el train set son 240 frases de formas verbales. El modelo tiene ~103k parámetros. Sin ninguna regularización, el paisaje de optimización contiene muchos mínimos de baja loss en train que no generalizan — el modelo puede memorizar formas superficiales específicas (p. ej., la cadena literal he goes) en vez de aprender la regla de conjugación (-s en presente simple, 3ª singular de go).

Weight decay con \(\lambda = 0.1\) ejerce un pequeño tirón constante sobre cada peso hacia cero. Cada step resta \(\eta_t \lambda \theta\) de \(\theta\). En el régimen donde los gradients de la tarea sobre verbos raros son pequeños, este tirón es la fuerza dominante para esos parámetros — y evita que la tabla de embeddings derive hacia el régimen de norma alta de memorización.

El punto pedagógico: weight decay no es "magia anti-overfitting". Es una fuerza que mantiene al optimizer en la región bien condicionada donde las estimaciones de los momentos de AdamW están calibradas a la escala de gradient de la tarea, no a la escala de deriva del parámetro.

Escalera de diagnóstico que Borja debería recorrer

Si Borja ve el gap val/train en el Run B y se pregunta por qué:

  1. Primera comprobación: el schedule, batch size y seed son idénticos (lo son). Elimina "el entrenamiento tuvo mala suerte" como explicación.
  2. Segunda comprobación: el panel de norma de pesos. Que la norma del embedding del Run B suba 2.5× es la pistola humeante. Que la del Run A se mantenga estable es el control.
  3. Tercera comprobación: la eval por slices (Fase 20). La precisión de train del Run B sobre los 12 verbos regulares es ~99%, sobre los 8 verbos irregulares es ~95%. La precisión de val es 78% y 62% respectivamente. La del Run A: train 92% / 88%, val 84% / 75%. El Run B memorizó; el Run A aprendió.
  4. Confirma por ablación: fija weight_decay=0.5 (el caso sobre-corregido) y observa el fallo opuesto — tanto la loss de train como la de val se estancan más alto, las normas de pesos colapsan hacia cero. Esto muestra que el régimen está acotado por ambos lados; el sweet spot \(\lambda = 0.1\) no es arbitrario.

Reproductor

# Control
seed=42 weight_decay=0.1 just phase-18-train

# Break
seed=42 weight_decay=0.0 just phase-18-train

# Compare
just phase-18-compare experiments/18-control experiments/18-break-wd0

El script de comparación produce dashboard-compare.html superponiendo los dos runs.

Cascada de pistas (si Borja se queda atascado)

  1. (Suave) "Los dos runs difieren en un único hiperparámetro del optimizer. Imprime ambas configs lado a lado."
  2. (Media) "Mira el panel de norma de pesos. ¿Qué está subiendo en el Run B que no está subiendo en el Run A?"
  3. (Directa) "El término weight_decay de AdamW resta \(\eta_t \lambda \theta\) en cada step. ¿Qué implica si \(\lambda = 0\)?"

Arreglo

Restaura weight_decay: 0.1 en la config del Run B. Re-ejecuta. Confirma que las curvas del Run B ahora coinciden con las del Run A.

Lo que este break NO es

  • No es un break de precisión numérica (sin fp16, sin NaN).
  • No es un break de fuga de datos (los splits de val y train están limpios).
  • No es un break de capacidad del modelo (la arquitectura no cambia).

Es un break de regularización quitada, y el fallo emerge solo tras los steps suficientes para que la tabla de embeddings derive. Ese retraso (~600 steps antes de la divergencia) es en sí una lección: los fallos de regularización son lentos, no catastróficos.

Referencias cruzadas

  • theory/05-adamw-vs-adam-decoupling.md — la razón algebraica.
  • Fase 19 theory/03-three-failure-modes.md — fallos hermanos (init, warmup, mask).
  • Fase 20 theory/01-metrics-catalog.md — la precisión por slices es cómo se cuantifica la memorización, no solo se visualiza.