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.Template — sin Jinja2 (dependencia extra).
Bloque D — generar el dashboard sano¶
En experiments/19-healthy/:
- Re-ejecuta el entrenamiento de la Fase 18 con
--inspector enabled. - El bucle de entrenamiento escribe
inspector.log.jsonl. - Tras el entrenamiento, llama a
dashboard.render('inspector.log.jsonl', 'dashboard.html', config, manifest). - Abre el
dashboard.htmlresultante 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:
experiments/19-healthy/dashboard.htmlse abre tanto en Firefox como en Chromium con los siete paneles visibles.- Desconectar la red no cambia el render.
- 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 Sansen 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.