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_tcon 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é:
- Primera comprobación: el schedule, batch size y seed son idénticos (lo son). Elimina "el entrenamiento tuvo mala suerte" como explicación.
- 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.
- 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ó.
- 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)¶
- (Suave) "Los dos runs difieren en un único hiperparámetro del optimizer. Imprime ambas configs lado a lado."
- (Media) "Mira el panel de norma de pesos. ¿Qué está subiendo en el Run B que no está subiendo en el Run A?"
- (Directa) "El término
weight_decayde 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.