Skip to content

English · Español

Break — Evaluar sobre el training set en vez de held-out; mostrar cómo miente el score

🇪🇸 Cambiamos un solo path en el harness — el split de evaluación pasa de val a train. Reportamos 99% de accuracy con un modelo que en realidad solo memorizó. Es la trampa más vieja y más común en evaluación; verla en frío evita caerla en caliente.


Síntoma que verá Borja

Dos informes de evaluación:

  • Informe A (control): el harness lee data/eval/val.jsonl (60 frases held-out). Reporta CCR-regular = 82%, CCR-irregular = 64%, ConjAcc = 76%.
  • Informe B (break): el harness lee data/eval/train.jsonl (240 frases de entrenamiento). Reporta CCR-regular = 95%, CCR-irregular = 87%, ConjAcc = 88%.

El mismo checkpoint del modelo. El mismo código de métricas. Conclusión distinta.

Si Borja ve el Informe B y pega "ConjAcc 88%, listo para shippear" en el journal, acaba de cometer el error de reporting más común en ML.

El break, mecánicamente

En src/minieval/harness.py:

# Run A (control)
def run_eval(model, split: str = "val") -> Report:
    ...

# Run B (break) — one line in the config
eval_split: train   # was: val

O directamente en código: hardcodea split = "train" y entierra el cambio en un commit de refactor. (Así es exactamente como aparece el bug en repos del mundo real. Un refactor renombra una clave de config, el default cae a "train", y nadie revisa el diff.)

El break entero es un intercambio de un identificador.

Por qué esto enseña el concepto

A escala §A13 con 103k parámetros y 240 frases de entrenamiento, el modelo tiene aproximadamente 430 parámetros por ejemplo de entrenamiento. Eso no está hambriento de parámetros — el modelo tiene capacidad de memorizar frases enteras si quiere.

El training set tiene la propiedad de que el modelo ha visto cada forma en él, a menudo varias veces. Predecir wrote tras He es, para el modelo entrenado, un lookup casi determinista. El modelo no está generalizando; está recuperando.

Cuando evalúas en train:

  • La PPL sobre train está sesgada hacia abajo por la memorización que el modelo hizo durante el entrenamiento. No refleja el comportamiento del modelo en inputs novedosos.
  • El CCR sobre train está sesgado hacia arriba por la misma memorización. El alto CCR-irregular del modelo (87%) es "memoricé las formas irregulares de estos 8 verbos", no "aprendí la estructura de la conjugación irregular".
  • La alineación bilingüe sobre train está sesgada hacia arriba por la misma razón.

Cada métrica está inflada. Ninguna te dice nada sobre generalización.

Este es el propósito entero del split train/val. La infraestructura del portal §A12 / §A14 (Fase 41) va a forzar el split a nivel del data-loader, pero en la Fase 20 el harness sigue funcionando por honor.

Escalera diagnóstica que Borja debería recorrer

  1. Primera comprobación: lee el manifest. ¿Cuál es eval_split?
  2. Segunda comprobación: compara las tablas por slice. Si train y val son parecidas, o el modelo super-generaliza (raro a escala §A13) o estás evaluando sobre el split equivocado.
  3. Tercera comprobación: el test canónico de sanity — toma 5 frases del eval set y comprueba si aparecen en el train set. Si sí, el split está roto.
  4. Diagnóstico: eval_split es train, no val. O bien los archivos train y val fueron concatenados por un mal paso de preprocesado.

Reproductor

# Control
just phase-20-eval split=val

# Break (looks like "wow we trained well")
just phase-20-eval split=train

# Compare
just phase-20-eval-compare experiments/20-eval-val experiments/20-eval-train

El script de comparación imprime ambas tablas de métricas lado a lado y calcula el sesgo (train − val) por slice. Los sesgos son el tamaño de "lo que la memorización te compró".

Cascada de pistas

  1. (Suave) "Los dos informes usan el mismo modelo. ¿Qué más podría diferir?"
  2. (Media) "¿Qué dice manifest.json sobre los datos de eval?"
  3. (Directa) "El eval está leyendo el training set. ¿Dónde decide el harness qué archivo abrir?"

Arreglo

Cambia eval_split: train de vuelta a eval_split: val. O, estructuralmente, refactoriza el harness para que la API de eval tome una ruta y train no sea un valor válido en producción. El portal de la Fase 41 va más allá: los datos held-out viven en un mount de filesystem separado al que el bucle de entrenamiento físicamente no puede llegar.

Una versión más sutil del mismo bug

Una variante más insidiosa: train y val están correctamente separados, pero la construcción de probes usa prompts que aparecieron en train. Por ejemplo, el probe "He went to the ___" se pregunta en eval, pero el prompt exacto "He went to the school" aparece en train. La continuación del modelo es memorización.

Defensa: asegúrate de que los prompts en data/eval/probes.jsonl sean novedosos — sus prefijos no aparecen en data/train/*.jsonl. El corpus §A13 es lo bastante pequeño como para que esto sea sencillo de garantizar. El probe-schema.md de la Fase 20 (lab/00) exige este chequeo.

Lo que este break NO es

  • No es un bug de modelo.
  • No es un bug matemático de métrica.
  • No es un leak por preprocesado de datos inadvertido.

Es el error de evaluación más simple posible: leer el archivo equivocado. La lección es que los errores de evaluación más comunes no son exóticos — son "la ruta está mal" o "el slice está mal". Un harness ruidoso y simple con rutas explícitas y separadas es la defensa.

Referencias cruzadas

  • theory/04-perplexity-pitfalls-tiny-corpus.md — incluso sobre un split val limpio, la PPL sola engaña.
  • theory/03-probe-construction.md — cómo se construyen los probes para evitar leaks.
  • Fase 41 §A14 — aislamiento de datos a nivel de portal.