Skip to content

English · Español

Lab 01 — Renderizar el dashboard como un fichero HTML autocontenido

Objetivo: convertir las estadísticas en streaming en un único fichero HTML estático con siete paneles, sin assets externos.

Tiempo estimado: 90-120 minutos.

Prerrequisito: Lab 00 (Inspector) commiteado.


Lo que produces

Un fichero nuevo:

  • src/minitrain/dashboard.py — el renderer.

Un primer dashboard generado:

  • experiments/19-healthy/dashboard.html — entrenamiento sano de la Fase 18, esta vez con el Inspector activo.

Además un diagrama de ejemplo:

  • docs/phase-19-training-dynamics/diagrams/dashboard-layout.png — captura anotada de cómo deben verse los paneles (para el sitio de docs).

TODOs

Bloque A — definir la estructura de datos del dashboard

src/minitrain/dashboard.py consume un "log file" escrito por el Inspector — un JSONL donde cada línea es:

{
  "step": 0,
  "train_loss": 6.234,
  "val_loss": null,
  "loss_regular": 6.21,
  "loss_irregular": 6.34,
  "lr": 0.0,
  "grad_norm_pre": 5.21,
  "grad_norm_post": 1.0,
  "activations": {"layer_0": {"l2": 0.95, "mean": 0.01, ...}, ...},
  "spectral": {"layer_0.W_q": 1.23, ...},
  "dead_neurons": {"layer_0": 3, "layer_1": 8, ...},
  "dead_heads": {"layer_0": 0, "layer_1": 0, ...}
}

Este log file lo escribe src/minitrain/loop.py con el Inspector activo. Los campos loss_regular / loss_irregular vienen de partition_batch_loss en src/minitrain/per_class_loss.py (Lab 00, Bloque D). Si un batch tiene cero ejemplos de una clase, el campo correspondiente es null y el Panel 7 del dashboard interpola sobre él.

Bloque B — renderizar siete paneles con matplotlib

En src/minitrain/dashboard.py:

def render(log_path: Path, out_html: Path, config: dict, manifest: dict) -> None:
    log = read_jsonl(log_path)
    panels = []
    panels.append(render_panel_loss(log, config))           # Panel 1
    panels.append(render_panel_lr(log, config))             # Panel 2
    panels.append(render_panel_grad_norm(log, config))      # Panel 3
    panels.append(render_panel_activations(log))            # Panel 4
    panels.append(render_panel_spectral(log))               # Panel 5
    panels.append(render_panel_dead(log))                   # Panel 6
    panels.append(render_panel_reg_vs_irr(log))             # Panel 7

    # Each render_panel_* returns a base64-encoded PNG bytes blob.
    html = build_html(panels, config, manifest)
    out_html.write_text(html, encoding='utf-8')

El Panel 7 dibuja loss_regular y loss_irregular como dos líneas en los mismos ejes (escala y logarítmica), con el gap τ = loss_irregular − loss_regular sombreado entre ambas. Anota el gap del último paso en la leyenda.

Cada render_panel_* llama a matplotlib, guarda a un PNG en memoria vía io.BytesIO, devuelve los bytes codificados en base64:

def fig_to_b64(fig) -> str:
    buf = io.BytesIO()
    fig.savefig(buf, format='png', bbox_inches='tight', dpi=100)
    plt.close(fig)
    return base64.b64encode(buf.getvalue()).decode('ascii')

Bloque C — ensamblar el HTML

La plantilla HTML debe ser un único fichero con:

  • Un bloque <style> que defina una rejilla responsive (p. ej., 2 columnas × 4 filas, con la última celda vacía o albergando el resumen del run). CSS Grid, sin framework externo.
  • Siete etiquetas <img src="data:image/png;base64,...">, una por panel.
  • Una caja de cabecera con el nombre del run, el hash de config, el resumen del manifest (semilla, hardware, versiones), la perplejidad final de val.
  • Un footer con un enlace al manifest.json (ruta relativa).
  • Sin CDN. Sin CSS externo. Sin JavaScript. HTML puro + CSS inline + PNGs embebidos.
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Run: {{ run_name }}</title>
  <style>
    body { font-family: -apple-system, sans-serif; max-width: 1400px; margin: 1rem auto; }
    .panels { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; }
    .panel img { width: 100%; }
    /* etc. */
  </style>
</head>
<body>
  <header>...</header>
  <div class="panels">
    <div class="panel"><h3>1 Loss</h3><img src="data:image/png;base64,{{ panel_1 }}"></div>
    <!-- ... -->
    <div class="panel"><h3>7 Regular vs Irregular</h3><img src="data:image/png;base64,{{ panel_7 }}"></div>
  </div>
  <footer>...</footer>
</body>
</html>

Usa formateo de strings de Python o string.Templatesin Jinja2 (dependencia extra).

Bloque D — generar el dashboard sano

En experiments/19-healthy/:

  1. Re-ejecuta el entrenamiento de la Fase 18 con --inspector enabled.
  2. El bucle de entrenamiento escribe inspector.log.jsonl.
  3. Tras el entrenamiento, llama a dashboard.render('inspector.log.jsonl', 'dashboard.html', config, manifest).
  4. Abre el dashboard.html resultante en Firefox Y Chromium. Verifica que los siete paneles son visibles y están correctamente etiquetados. El Panel 7 debe mostrar la línea regular claramente por debajo de la irregular hacia el paso ~500, con el gap estrechándose hacia el final del entrenamiento (el patrón esperado de §A13).

Bloque E — anotar el diagrama

Saca una captura de dashboard.html y guárdala en docs/phase-19-training-dynamics/diagrams/dashboard-layout.png. Añade anotaciones con flechas identificando los seis paneles (usa GIMP, Inkscape, o simplemente el annotate de matplotlib para dibujar encima).

Restricciones

  • HTML autocontenido. Abrir desde un USB sin red. Pruébalo literalmente con firefox file:///path/to/dashboard.html.
  • Sin CSS/JS externo. Sin Bootstrap, sin Plotly, sin Tailwind, sin jQuery, nada alojado en CDN.
  • Renderiza tanto en Firefox como en Chromium. El CSS específico de navegador está prohibido.

Condiciones de parada

Hecho cuando:

  1. experiments/19-healthy/dashboard.html se abre tanto en Firefox como en Chromium con los siete paneles visibles.
  2. Desconectar la red no cambia el render.
  3. La cabecera muestra el nombre del run + hash de config + perplejidad final de val.

Trampas

  • base64 hace que el fichero HTML sea grande. Un dashboard típico es de 200-500 KB. Aceptable. Si pasa de 2 MB, los PNG de tus paneles tienen demasiada resolución; baja el dpi a 80.
  • Problemas de fuentes en matplotlib si se usa una fuente no por defecto. Quédate con la default (DejaVu Sans en Linux).
  • CSS Grid en navegadores antiguos. Bien para Firefox/Chromium modernos; si necesitas soportar navegadores más viejos, usa flexbox. No es algo de la Fase 19.
  • Velocidad de parseo de JSONL. Si los logs crecen mucho (p. ej., para el run de overfit con 8000 pasos y muchos campos de logging), un ingenuo for line in file: json.loads(line) está bien, pero verifica que parsea en < 2 s.

Cuándo consultar solutions/

Tras renderizar correctamente el dashboard sano en ambos navegadores. La solución en solutions/01-build-dashboard-ref.md (escrita al abrir la fase) discute la elección entre matplotlib, Plotly y Bokeh y por qué aterrizamos en matplotlib.


Siguiente lab: lab/02-break-it.md.