English · Español
Lab 02 — Entrenar un MLP escalar en una tarea diminuta de identidad de tiempo verbal usando solo minigrad.scalar¶
Objetivo: construir un MLP de 2 capas desde neuronas
Value, entrenarlo en una tarea microscópica de tiempo verbal, y ver descender una curva de pérdida. La ejecución de entrenamiento ML extremo a extremo más pequeña y pedagógicamente más pura que existe, anclada en el dominio §A13 de gramática verbal.Tiempo estimado: 90–120 minutos.
Prerrequisitos: lab 00, lab 01 (todas las operaciones implementadas y testeadas).
La tarea (el anclaje §A13)¶
Elige un verbo — digamos work. Sus 5 tiempos son:
| index | tense | English form | Spanish |
|---|---|---|---|
| 0 | infinitive | (to) work |
trabajar |
| 1 | present (3rd sg) | works |
trabaja |
| 2 | past simple | worked |
trabajó |
| 3 | past participle | worked |
trabajado |
| 4 | future (will) | will work |
trabajará |
La tarea es el mapeo de identidad de tiempo verbal de 5 vías: dado un input one-hot 5-dim que codifica "qué tiempo es este", producir una salida 5-dim cuyo argmax sea igual al argmax del input. Es artificialmente sencilla — un autoencoder perfecto para un one-hot — pero ese es precisamente el punto: cada componente no trivial del modelo (autograd, parámetros, pérdida, bucle de entrenamiento) debe ser correcto para que la red aprenda esto. Cualquier fallo es un bug de Fase 7, no un bug de problema difícil.
Lo que produces¶
Un directorio experiments/07-train-tense-logits/ conteniendo:
model.py— clasesNeuron,Layer,MLPconstruidas desdeValue. ~60 líneas.train.py— el bucle de entrenamiento. ~40 líneas.loss.png— curva de pérdida sobre el entrenamiento.predictions.json— las salidas del MLP entrenado en los 5 inputs.manifest.json— esquema estándar.README.md— qué entrenaste, qué pérdida alcanzaste, cuánto tardó, qué mejorarías.
Más un directorio separado experiments/07-visualize-graph/:
viz.py— construye una pequeña expresión, renderiza el DAG vía graphviz, guarda como SVG.graph.svg— el grafo renderizado, nodos etiquetados con data del forward y grad del backward.manifest.json.
TODOs (experimento 1: identidad de tiempo verbal)¶
Bloque A — Neuron, Layer, MLP¶
En model.py:
-
Neuron: toman_inentradas. Poseew: list[Value]de longitudn_in(inicializado aleatoriamente a valores pequeños, p. ej., desderandom.uniform(-1, 1)) yb: Value(inicializado a 0).__call__(self, xs: list[Value])devuelve(sum(wᵢ · xᵢ) + b).tanh(). -
Layer: toman_in, n_out. Poseeneurons: list[Neuron].__call__(self, xs)devuelve la lista de la salida de cada neurona. -
MLP: toman_in, layer_sizes: list[int]. Poseelayers: list[Layer].__call__(self, xs)los encadena. Una última capa multi-output (el caso aquí — 5 salidas) devuelve la lista, no un únicoValue. - Método
parameters(self)en cada uno (devuelve lista deValuepara todos los pesos y sesgos). La Fase 9 introduceParameter; para la Fase 7 simplemente recolectaValues manualmente.
Bloque B — Dataset de tiempos verbales¶
Los 5 pares input/target son los 5 vectores one-hot de tiempo verbal para work:
input target
(1, 0, 0, 0, 0) → (1, -1, -1, -1, -1) # infinitive / to work
(0, 1, 0, 0, 0) → (-1, 1, -1, -1, -1) # present 3sg / works
(0, 0, 1, 0, 0) → (-1, -1, 1, -1, -1) # past simple / worked
(0, 0, 0, 1, 0) → (-1, -1, -1, 1, -1) # participle / worked
(0, 0, 0, 0, 1) → (-1, -1, -1, -1, 1) # future / will work
- Codifica como
xs: list[list[Value]](5 inputs, cada uno un 5-vector deValues) eys: list[list[Value]](5 targets). - Usa activaciones
tanhen el modelo. Contanh, la "etiqueta 0" se codifica mejor como -1 (ya que las salidas detanhestán en (-1, 1)). Usa codificación {-1, 1} para los targets.
🇪🇸 La tarea es deliberadamente trivial: dado un one-hot de "qué tiempo verbal", devuelve ese mismo one-hot. La gracia no está en aprender gramática — eso lo hace la fase 9 con el grid completo — sino en confirmar que tu autograd, tu loss y tu loop de entrenamiento funcionan extremo a extremo.
Bloque C — bucle de entrenamiento¶
En train.py:
- Instancia
model = MLP(5, [4, 5])— 5 entradas (one-hot de tiempo), una capa oculta de 4 neuronas, 5 salidas. - Hiperparámetros:
lr = 0.05,n_epochs = 300. - Para cada epoch:
- Calcula predicciones:
preds = [model(x) for x in xs]. Cadapredes una lista de 5Values. - Calcula la pérdida:
loss = sum((p - y)**2 for pred, target in zip(preds, ys) for p, y in zip(pred, target)). (Suma de errores cuadráticos sobre los 25 logits = 5 inputs × 5 outputs.) La pérdida es unValue. - Pon a cero los gradientes en todos los parámetros:
for p in model.parameters(): p.grad = 0.0. loss.backward().- Actualiza parámetros:
for p in model.parameters(): p.data -= lr * p.grad. - Loguea
epoch, loss.dataa un logger estructurado (usando elget_loggerde la Fase 6). - Grafica pérdida vs epoch con matplotlib. Guarda como
loss.png. - Tras entrenar, ejecuta
model(x)para cada one-hot de tiempo. Guarda las salidas comopredictions.json.
Bloque D — asegurar éxito¶
En tu train.py (o un verify.py aparte):
- Comprueba con assert que la pérdida final < 0.5 (5 outputs × 5 ejemplos = 25 logits; objetivo holgado por-logit ~0.02).
- Comprueba con assert que
argmax(model(x))iguala el argmax del input para los 5 inputs.
Si alguna comprobación falla, tu entrenamiento no convergió. Diagnostica:
- ¿Pérdida no decrece? Probable bug en backward. Re-ejecuta los tests unitarios.
- ¿Pérdida decrece y luego explota? lr demasiado alto. Prueba 0.01.
- ¿Pérdida decrece lentamente? lr demasiado bajo o capa oculta demasiado pequeña.
TODOs (experimento 2: visualizar)¶
Bloque E — visualización del grafo¶
En experiments/07-visualize-graph/viz.py:
- Elige una expresión pequeña: p. ej., el ejemplo del diamante de
theory/03:L = (a*b + c)*(a - c)cona=2, b=3, c=4. - Constrúyela con
minigrad.scalar. LlamaL.backward(). - Usa
graphviz(binding de Python) para construir unDigraph. Para cada nodoValue, añade un nodo etiquetado con{op | data | grad}. Añade aristas desde cada_preval nodo. - Renderiza como SVG:
dot.render('graph', format='svg', cleanup=True). - Guarda
graph.svg. - Imprime la ruta. Abre en navegador. Confirma:
- Los nodos muestran data y grad.
- Forma de diamante visible:
atiene dos aristas salientes.
Esta es la visualización que la especificación pide en §4 FASE 7.
Bloque F — manifest para ambos¶
Esquema estándar según el lab 00 de la Fase 6. Incluye en config:
- Para identidad de tiempo verbal: hiperparámetros (lr, epochs, layer_sizes, seed, rango de init, verbo elegido).
- Para viz: qué expresión se renderizó, versión de graphviz.
Restricciones¶
- Solo
minigrad.scalar. Sin NumPy en el modelo o el bucle de entrenamiento. Solo listas deValue. - Usa utilidades de la Fase 6.
seed_everything(42),get_logger(__name__)— sinprint. graphvizdebe estar instalado. En Fedora:dnf install graphvizpara el paquete del sistema +pip install graphvizpara el binding de Python.- Reproducible. La misma semilla debe producir la misma pérdida final (con ~1% por ruido de coma flotante).
Resultados esperados¶
- La pérdida final debe alcanzar ~0.05–0.5 dentro de 300 epochs (25 logits, objetivo por-logit ~0.01-0.02).
- Las 5 predicciones deben tener
argmaxigual al argmax del input. graph.svgdebe mostrar claramente el patrón de diamante (ateniendo dos flechas salientes).
Condiciones de parada¶
Hecho cuando:
loss.pngmuestra pérdida monótona decreciente hasta por debajo de 0.5.- Las 5 predicciones de identidad de tiempo correctas (argmax coincide).
graph.svgrenderizado, se abre en navegador, visualmente correcto.- Ambos ficheros
manifest.jsonexisten con el esquema esperado.
Escollos¶
- Olvidar poner a cero los gradientes. La pérdida explota tras el primer epoch. Añade
p.grad = 0.0antes de cadabackward(). - Pesos inicializados a cero. Todas las neuronas computan la misma salida; sin ruptura de simetría; el modelo no aprende. Inicializa con
random.uniform(-1, 1)o similar. - Usar etiquetas
0/1con salidastanh.tanhsaca valores en(-1, 1); con targets(0, 1), el modelo quiere empujar salidas a 0 (mitad del rango), los gradientes son diminutos, el entrenamiento es lento. Usa etiquetas(-1, 1). - Sin sembrar. La varianza ejecución-a-ejecución es enorme para modelos diminutos. Llama
seed_everything(42)al inicio detrain.py. - Saturación de
tanh.tanh(muy_grande)está bien (satura a ±1), pero el gradiente(1 - tanh²)en saturación es ≈0 — gradientes desvanecientes. Con una capa oculta de 4 neuronas ylr=0.05esto raramente muerde; si lo hace, baja el rango de init arandom.uniform(-0.5, 0.5). - Graphviz no instalado. Dos capas: paquete del sistema (comando
dot) y binding de Python (pip install graphviz). Ambos deben estar presentes. Testea condot -Vdesde el shell. - Tratar las 5 salidas como un escalar. Cada
model(x)devuelve una lista de 5Values, no unValue. Suma sobre los ejes de ejemplo y salida al computar la pérdida; un bucle olvidado aquí es el bug más común de este lab.
Cuándo consultar solutions/¶
Después de que tu experimento de tiempos verbales converja y exista graph.svg. Luego solutions/02-train-tense-logits-ref.md (en la apertura de fase) proporciona la curva de pérdida de referencia y la comparación de visualización.
Fin de los labs de la Fase 7. Siguiente: escribir PHASE_07_REPORT.md y learners/borja/phase-07/reflections.md.