Skip to content

English · Español

02 — Leakage, dedup y splits estratificados

🇪🇸 Si la división train/val/test no es limpia, la métrica de generalización miente. Dedup por huella normalizada y división estratificada por (verbo, tiempo) son los dos mecanismos. Sin ellos, el tutor de la Fase 32 puede memorizar la matriz y parecer brillante mientras sigue siendo inútil.


Cómo se ve el leakage en una tarea de morfología

Leakage es cuando información que debería estar en el "desconocido" del conjunto de test aparece en el conjunto de entrenamiento. Un modelo puede entonces memorizar la información filtrada en lugar de aprender el patrón subyacente. La accuracy del test-set se ve genial; el rendimiento del mundo real es pobre.

Para un corpus enumerado de conjugación verbal, los modos de leakage son diferentes a los de un corpus de texto libre. Específicamente:

  1. Duplicados exactos. El generador emite la misma fila dos veces. Improbable dada la enumeración, pero posible si los generadores de mis-conjugación colisionan.
  2. Leakage a nivel de persona dentro de un (verbo, tiempo). Train ve I work y you work; test ve he works. Un modelo que aprende "<pronoun> work + agreement morpheme" puede predecir la fila de test trivialmente sin aprender nada sobre el verbo work que no supiera ya por las filas de train. Este es el mayor riesgo de leakage para nuestro corpus.
  3. Leakage del par mis-conjugación ↔ forma-correcta. Una fila mis-conjugada he work está emparejada con he works (vía el campo correct_form). Si he work está en test y he works está en train, el modelo ha visto la respuesta.

Cada uno tiene un arreglo.

Arreglo 1: dedup por fingerprint

Define el fingerprint de una fila como sha256(normalize(text)), donde normalize realiza:

  1. Normalización Unicode NFC (según teoría 03).
  2. Eliminar espacios en blanco al principio/final.
  3. Minúsculas, excepto preservar la capitalización canónica de I (así I work e i work difieren — no deberían existir ambos, pero si lo hacen, esto los detecta).

Tras la normalización, dos filas con el mismo text tienen el mismo fingerprint, y el dedup elimina el duplicado.

Nota: no renombramos pronombres o stems verbales durante la normalización. A diferencia de la Fase 12 en el viejo encuadre A1 (donde se necesitaba normalización de identificadores C), aquí el texto es corto y la morfología es la señal. Queremos que I work y he works tengan fingerprints diferentes porque son filas diferentes.

Tras el dedup:

  • Esperado: 360 filas correctas + 100–300 mis-conjugaciones = ~460–660 filas únicas.
  • Si el dedup elimina más del 5% de las filas, una plantilla del generador está colapsando — investiga.

Arreglo 2: split estratificado por (verbo, tiempo)

División ingenua: barajar todas las filas y tomar 80/10/10. Mal.

Por qué: con 360 celdas correctas y 3 personas por (verbo, tiempo), si I work aterriza en train y he works en test, el modelo puede resolver he works desde "aprendí work del entrenamiento; el test me pide aplicar la -s de 3rd-person a un stem que ya he visto". Esa no es la generalización que queremos medir aquí.

Pero un momento: esa es la generalización que queremos. El punto entero es aprender -s de una celda y aplicarlo a otra. ¿Entonces es leakage o no?

La respuesta depende de qué afirmación se supone que el split de test valida:

  • Afirmación A: "El modelo ha aprendido la regla de morfología 3rd-sg present-simple takes -s." → testear sobre una nueva tripla (verbo, tiempo, persona), dado que hemos visto otras triplas (verbo, tiempo, persona), valida esto. (El split a nivel de persona está OK.)
  • Afirmación B: "El modelo puede producir una tripla (verbo, tiempo, persona) de la cual nunca ha visto ninguna forma." → requiere apartar un (verbo, tiempo) entero, para que los verbos de test en ese tiempo sean completamente no vistos.

Nuestra afirmación es B, porque para el tutor §A13, el test más significativo es: dada una mis-conjugación que involucra celdas (verbo, tiempo) que el modelo no memorizó, ¿puede aún producir la corrección aplicando generalización morfológica? Por tanto: dividir por (verbo, tiempo).

For each of 120 (verb, tense) pairs (20 verbs × 6 tense surface-forms):
  - 80% → train
  - 10% → val
  - 10% → test
Each (verb, tense) sends all 3 persons to the same split. Mis-conjugations
of that (verb, tense) also go to the same split.

Esto da:

  • ~96 pares (verbo, tiempo) en train × 3 personas = ~288 filas correctas en train, más mis-conjugaciones.
  • ~12 en val × 3 personas = ~36 filas correctas en val, más mis-conjugaciones.
  • ~12 en test × 3 personas = ~36 filas correctas en test, más mis-conjugaciones.

12 pares (verbo, tiempo) en test es pequeño — solo 36 filas correctas. Las estimaciones val/test tienen alta varianza. Lo compensamos:

  1. Reportando accuracy por (verbo, tiempo) no solo agregada.
  2. Añadiendo pruebas generadas en tiempo de evaluación de la Fase 32 (nuevas mis-conjugaciones de los verbos+tiempos de test).
  3. Considerando subir el corpus a múltiples filas por celda en un v1.5 (con envoltorio de contexto de frase).

Implementación:

def stratified_split_by_verb_tense(rows, ratios=(0.8, 0.1, 0.1), seed=42):
    rng = random.Random(seed)
    # bucket by (verb, tense)
    buckets = defaultdict(list)
    for r in rows:
        buckets[(r.verb_lemma, r.tense)].append(r)
    keys = sorted(buckets.keys())   # deterministic order
    rng.shuffle(keys)
    n = len(keys)
    n_train = int(n * ratios[0])
    n_val   = int(n * ratios[1])
    train_keys = set(keys[:n_train])
    val_keys   = set(keys[n_train:n_train + n_val])
    test_keys  = set(keys[n_train + n_val:])
    train, val, test = [], [], []
    for r in rows:
        k = (r.verb_lemma, r.tense)
        if k in train_keys: train.append(r)
        elif k in val_keys: val.append(r)
        else:               test.append(r)
    return train, val, test

La misma semilla produce el mismo split en cada ejecución — requerido por la invariante de reproducibilidad.

Arreglo 3: contención del par mis-conjugación ↔ forma-correcta

Una fila mis-conjugada text="he work", correct_form="he works" es potencialmente leaky si la forma correcta (he works) de esa celda exacta está en train pero la fila mis-conjugada está en test. El modelo ha visto efectivamente la respuesta.

Mitigación: el split estratificado por (verbo, tiempo) garantiza que todas las filas de una celda (verbo, tiempo) — correctas y mis-conjugadas — van al mismo split. Así que he work (mis) y he works (correcto), ambos pertenecientes a (work, present_simple), están siempre en el mismo split. Sin leakage de pares.

Esta es una de las razones por las que (verbo, tiempo) es la granularidad correcta. Una granularidad más fina (por-fila) dividiría las mis de las correctas.

¿Y las "pruebas de robustez"?

Una prueba de robustez es un conjunto de test apartado que el modelo nunca ha visto durante el entrenamiento, diseñado para testear propiedades específicas de generalización. Ejemplos para nuestra tarea:

  • Verbo no visto enteramente — entrenar sin write, testear solo en write (en cualquier tiempo, cualquier persona).
  • Tiempo no visto para un verbo conocido — train ve todos los 6 tiempos de work excepto past_participle, test solo el past participle.
  • Prueba cross-lingüística — presentar la forma en español y pedir el inglés (o viceversa). Testea la alineación bilingüe.

Para v1 de la Fase 12, no apartamos un conjunto de pruebas. Las 12 celdas (verbo, tiempo) de test son el único test. Si la Fase 32 necesita más, añadirlas como un corpus v2.

(Una pregunta abierta que discutimos en PHASE_12_PLAN.md §7d: si apartar un verbo entero para testear generalización cross-verbo. Default: no para v1.)

El trabajo del validador

scripts/validate_corpus.py se ejecuta tras la generación y asegura:

  • Validez del esquema. Cada fila pasa el JSONSchema.
  • Sin duplicados exactos. Cada fingerprint es único.
  • Cobertura de celda. Las 360 celdas (verbo, superficie de tiempo, persona) tienen ≥ 1 fila correct. (Comprobación dura; según §A13.)
  • Los 20 verbos presentes. set(r.verb_lemma for r in rows) == {20 in-scope verbs}.
  • Las 6 superficies de tiempo presentes por verbo. Cada verbo tiene ≥ 1 fila en cada uno de los 6 buckets de tiempo.
  • Las 3 personas presentes por (verbo, tiempo). Cada (verbo, tiempo) tiene ≥ 1 fila en cada persona.
  • Tipos de mis-conjugación de la taxonomía canónica. Sin errores tipográficos, sin códigos rebeldes.
  • Las filas de mis-conjugación tienen correct_form poblado. correct_form vacío es inválido.
  • Las filas correctas tienen mis_conjugation_type = null y correct_form = null.
  • Cada fila tiene un campo spanish. No vacío.
  • Normalización NFC. Cada campo text y spanish está en NFC. (Re-NFC y asegura igualdad.)
  • Longitud de texto en rango. 2 ≤ len(text) ≤ 30 bytes para inglés. 2 ≤ len(spanish) ≤ 40 bytes para español (los acentos añaden bytes).
  • scripts/split_corpus.py posteriormente produce train/val/test sin solapamiento de fingerprint y sin solapamiento cross-split de (verbo, tiempo).

Estas son 12 verificaciones separadas. El validador escribe un reporte resumen + sale con código non-zero ante cualquier fallo.

Por qué esto importa para la Fase 32

El tutor de gramática de la Fase 32 se evalúa así:

  1. Toma el split de test del corpus.
  2. Para cada fila de test mis-conjugada, pregunta al agente: "¿Está correcto esto? Si no, ¿cuál es la forma correcta y cuál fue el error?"
  3. Puntúa las respuestas del agente contra correct_form y mis_conjugation_type.

Si el split de test está filtrado (especialmente vía leakage cross-persona dentro de un (verbo, tiempo)), la puntuación del agente carece de sentido — podría estar haciendo pattern-matching en lugar de aprender la regla. La prevención de leakage de la Fase 12 es la base de la validez de evaluación de la Fase 32.

Problemas de práctica

Soluciones en solutions/02-leakage-and-splits-ref.md (apertura de fase).

  1. El split estratificado por (verbo, tiempo) pone las 3 personas de un (verbo, tiempo) en el mismo split. Argumenta por qué esto no es "tirar señal" — ¿no obtendríamos un modelo mejor entrenado si entrenáramos en más personas y testeáramos en menos?
  2. Con 120 pares (verbo, tiempo) y un split val/test 12/12, los conjuntos val y test contienen solo 12 combinaciones distintas de (verbo, tiempo). ¿Es suficiente? Estima la varianza del estimador de accuracy en val.
  3. Supón que Borja accidentalmente re-usa la misma semilla para gen_corpus.py y split_corpus.py. ¿Causa esto leakage? (Pista: piensa en lo que el RNG de cada script controla.)

Recapitulación de un párrafo

Tres canales de leakage: duplicados exactos (arreglo: dedup por sha256(NFC-normalize(text))), leakage de persona dentro de (verbo, tiempo) (arreglo: split estratificado por (verbo, tiempo) — las 3 personas + todas las mis-conjugaciones de una celda van al mismo split), y leakage del par mis-conjugación/forma-correcta (arreglo: resuelto automáticamente por el split (verbo, tiempo)). El validador ejecuta 12 verificaciones antes de declarar el corpus hecho; la más crítica es la verificación de cobertura de los-20-verbos × 6-superficies-de-tiempo × 3-personas de §A13.


Siguiente: theory/03-reproducibility-and-versioning.md.