Skip to content

English · Español

Lab 01 — Primer run de entrenamiento real; superar la baseline n-gram

Objetivo: entrenar MiniGPT (Fase 17) sobre el corpus de conjugaciones verbales de la Fase 12 hasta que la perplejidad de validación en el split held-out-4-verbs supere a la baseline n-gram de la Fase 14.

Tiempo estimado: 3–5 horas, incluyendo un run de entrenamiento de 60–90 minutos en el i5-8250U.

Requisito previo: lab 00 hecho; PPL de la baseline de la Fase 14 commiteada en experiments/14-ngram-baseline/results.json.


Lo que produces

  • src/minitrain/loop.py — el step de entrenamiento + el bucle de épocas.
  • scripts/train_mini.py — entrypoint CLI: toma una config YAML, ejecuta el entrenamiento, escribe checkpoints + manifest.
  • experiments/18-train-mini/:
  • manifest.json{seed, versions, config, git_sha, data_manifest_hash, hardware}
  • config.yaml — hiperparámetros resueltos
  • train_log.jsonl — métricas por step (loss, lr, grad_norm)
  • loss_curve.png — loss de train + val, con la baseline n-gram como línea horizontal
  • results.json — PPL final de train, PPL de val, mejor step de val, comparación con la baseline
  • models/minigpt-phase18-<hash>.safetensors — checkpoint final (el lab 02 verifica el round-trip)

Antecedentes que debes haber leído

  • theory/00-motivation.md — las cinco máquinas de estado.
  • theory/02-optimizer-and-schedule.md — AdamW + warmup + cosine + clipping.

TODOs

Bloque A — escribe src/minitrain/loop.py

La estructura canónica:

def train(
    model, optimizer, scheduler, train_iter, val_iter,
    total_steps, val_every, log_every,
    rng_seed, manifest_path,
) -> dict:
    """Returns final metrics dict; writes log_file along the way."""
    set_seed(rng_seed)  # training-control RNG, NOT the data RNG
    write_manifest_pre_train(manifest_path, ...)

    for step in range(total_steps):
        batch = next(train_iter)
        logits, loss = model.forward(batch.input, batch.target, batch.attn_pad_mask, batch.loss_mask)
        grads = model.backward()
        g_norm = global_grad_norm(grads)
        optimizer.step(model.params, grads)
        scheduler.step()

        if step % log_every == 0:
            log({"step": step, "loss": loss, "lr": scheduler.lr, "g_norm": g_norm})

        if step % val_every == 0:
            val_loss, val_ppl = evaluate(model, val_iter)
            log({"step": step, "val_loss": val_loss, "val_ppl": val_ppl})

    return final_metrics
  • Implementa evaluate(model, val_iter): ejecuta todo el set de val en modo eval (sin gradients), devuelve la loss media por token y exp(mean_loss) como PPL.
  • Implementa global_grad_norm(grads): devuelve sqrt(sum((g*g).sum() for g in grads)).
  • Inserta el clipping antes de optimizer.step. Usa la implementación de theory/02 §"Gradient clipping".
  • Registra cada step en train_log.jsonl como un objeto JSON por línea.

Bloque B — escribe scripts/train_mini.py

Un CLI fino:

python scripts/train_mini.py \
    --config experiments/18-train-mini/config.yaml \
    --out-dir experiments/18-train-mini/ \
    --seed 42
  • Carga la config YAML.
  • Resuelve la info de hardware (modelo de CPU, RAM) para el manifest.
  • Fija las seeds (Python, NumPy) y registra la seed resuelta del RNG.
  • Construye el tokenizer, el cargador de corpus, el batcher (lab 00), MiniGPT (Fase 17), AdamW (Fase 9), el scheduler.
  • Llama a train(...).
  • Guarda el checkpoint final vía src/minitrain/checkpoint.py (el lab 02 lo implementa).
  • Guarda el gráfico, results.json.

Bloque C — config

experiments/18-train-mini/config.yaml — valores por defecto de partida:

model:
  n_layer: 2       # Phase 17 locked config
  n_head: 4
  d_model: 64
  d_ff: 256
  vocab_size: 64   # Phase 17 locked config
  max_seq_len: 32  # verb sentences are short

optimizer:
  type: adamw
  lr_max: 3e-4
  lr_min: 3e-5
  beta1: 0.9
  beta2: 0.95
  eps: 1e-8
  weight_decay: 0.1
  clip: 1.0

scheduler:
  warmup_steps: 100
  total_steps: 2000

training:
  batch_size: 8
  val_every: 50
  log_every: 10
  loss_reduction: token_mean
  rng_seed: 42
  data_seed: 7

data:
  train_path: data/processed/train.jsonl
  val_path:   data/processed/val.jsonl
  heldout_policy: verbs_4

Los números son puntos de partida. Ajusta si el entrenamiento es inestable, pero documenta el cambio en experiments/18-train-mini/README.md.

Bloque D — ejecuta el entrenamiento

  • Ejecuta python scripts/train_mini.py .... Tiempo de pared esperado en el i5-8250U: 60–90 minutos para 2000 steps.
  • Monitoriza train_log.jsonl durante el run. La loss debería bajar de ~6 en el step 0 a ~1.5–2.5 en el step 2000.
  • Si la loss se estanca por encima de 4.0, detén el run — algo está mal. Depura antes de escalar.
  • Si la loss se va a NaN: detén, guarda el manifest, documenta en experiments/18-train-mini/README.md, depura. (La Fase 19 sistematizará esto con el dashboard.)

Bloque E — gráfico e informe

  • Genera loss_curve.png con dos líneas (train, val), una línea horizontal discontinua en la PPL de la baseline n-gram de la Fase 14.
  • Computa las métricas finales: train_ppl, val_ppl, best_val_step, best_val_ppl, n_gram_baseline_val_ppl.
  • Escribe results.json:
{
  "train_ppl": 4.5,
  "val_ppl": 9.0,
  "best_val_step": 1850,
  "best_val_ppl": 8.8,
  "ngram_baseline_val_ppl": 13.0,
  "beats_baseline": true,
  "ppl_improvement_pct": 30.8
}

La puerta de DoD es val_ppl < ngram_baseline_val_ppl.

Bloque F — narrativa

En experiments/18-train-mini/README.md, escribe 200–400 palabras cubriendo:

  • Los números cabecera de PPL frente a la baseline.
  • La forma de la curva de loss (descenso suave, mesetas, picos).
  • Cualquier desviación de la config por defecto y por qué.
  • El gap train-vs-val al final (que sugeriría sobreajuste (overfitting) — explorado más en la Fase 19).
  • Tiempo de pared y RAM pico.

Restricciones

  • NumPy puro + minitorch hecho a mano. Sin PyTorch.
  • La seed del train RNG y la seed del data RNG son distintas. Una recarga debe restaurar ambas.
  • Sin early stopping basado en PPL de val durante este lab. Ejecuta los 2000 steps completos para que la forma de la curva sea totalmente visible. El early stopping llega en las Fases 19/20.

Condiciones de parada

Hecho cuando:

  1. experiments/18-train-mini/manifest.json está commiteado y completo.
  2. loss_curve.png está commiteado; muestra visualmente la PPL de val cayendo por debajo de la línea de la baseline.
  3. results.json tiene beats_baseline: true.
  4. Puedes responder: "¿qué habría empeorado mi PPL de val — y por qué?" (Al menos dos respuestas específicas.)

Escollos

  • Curva de loss perfectamente plana. Comprueba: ¿está el optimizer haciendo step realmente? Añade un log de np.linalg.norm(params['layer0.W']) cada step; debería cambiar.
  • La loss explota tras el step ~100. Probablemente el warmup terminó y el LR completo es demasiado grande. Reduce lr_max a la mitad.
  • PPL de val plana, PPL de train baja. Fuga en la mask. Comprueba que la loss mask está enmascarando el target, no el input.
  • PPL de train plana, PPL de val baja. Probablemente tus splits de train y val están intercambiados. Verifica el test de split del lab 00.
  • La RAM se dispara. Un autograd en NumPy puro mantiene las activaciones del forward para el backward. Con n_layer=2, d_model=64, batch_size=8, seq_len=32 deberías estar por debajo de 4 GB pico. Si está por encima, reduce el batch size.

Cuándo consultar solutions/

Después de que results.json muestre beats_baseline: true. Solución en solutions/01-train-mini-ref.md (escrita al abrir la fase). La solución discute cómo son las curvas de loss "buenas" para este corpus concreto y los tipos de patrones de meseta a esperar.


Siguiente lab: lab/02-checkpoint-roundtrip.md.