English · Español
Lab 00 — Tiny ViT sobre Íconos de Gramática¶
Objetivo: construir un Vision Transformer de 4 bloques sobre el bloque transformer de la Fase 17, entrenarlo sobre un dataset sintético de "íconos de gramática" (cada forma verbal renderizada como una pequeña imagen coloreada), y alcanzar > 80% top-1 accuracy en el test set de holdout en ≤ 5 minutos de entrenamiento en CPU.
🇪🇸 Aplicamos el bloque transformer de la Fase 17 a una nueva modalidad — imagen — para entender que un transformer es agnóstico a la modalidad: lo único que cambia es la capa de embedding. Implementamos patchify + posición + bloque transformer + cabezal de clasificación.
Tiempo estimado: 3–4 horas.
Prerequisitos: -
docs/extension-track/X2-multimodal/theory/01-vision-transformers.mdleído. - Implementación del bloque transformer de la Fase 17 (src/minimodel/transformer/) completa. - Python 3.11 + envuvcon NumPy, Pillow.
Lo que produces¶
Un directorio experiments/X2-multimodal/lab-00-tiny-vit/ que contiene:
BLUEPRINT.md— tu diseño antes de cualquier código. Forma de patchify, config de ViT-tiny, sketch del training loop, plan de tests. Debe ser aprobado por el usuario antes de código.generate_icons.py— script de Pillow que produce 1000 íconos sintéticos.data/icons/— 1000 archivos PNG:verb_<verb>_tense_<tense>_person_<person>_<idx>.png.data/labels.json— lista plana de{filename, verb_id, tense_id, person_id, class_id}.vit.py— tu implementación ViT (importandoTransformerBlockdesrc/minimodel/transformer/).train.py— script de entrenamiento.eval.py— script de eval con salida de matriz de confusión.manifest.json— versiones, seed, config (verCLAUDE.md§0.5).README.md— 1–2 párrafos: qué, resultados, sorpresas.loss.png,accuracy.png,confusion_matrix.png— artefactos commiteados.
El dataset¶
Renderizamos un ícono sintético por combinación (verb, tense, person, sample-index). El ícono codifica:
- Color → tiempo verbal.
- Infinitivo: gris. Presente simple: azul. Pasado simple: rojo. Participio pasado: verde. Futuro: morado.
- Forma → persona.
- 1ª-sg (I): círculo. 2ª-sg (you): cuadrado. 3ª-sg (he/she/it): triángulo.
- Patrón dentro de la forma → identidad de verbo. Usa uno de 20 patrones internos distintos (rayas alternas, puntos, cuadrícula entrecruzada, etc.) atados a los 20 verbos de §A13.
La tarea del modelo: dado un ícono \(32 \times 32\), clasificar qué (verb, tense, person) representa. Multi-clase con 20 × 5 × 3 = 300 clases, pero empezamos con una tarea más simple: clasificar solo el tiempo verbal (5 clases).
Por qué este dataset¶
- Sintético. Reproducible bit-a-bit (
seed_everything). - Atado a §A13. Se queda dentro del universo de gramática — esto no es un ejemplo "gatos vs. perros".
- Suficientemente fácil para que un ViT diminuto aprenda en CPU. Solo la señal de color basta para la tarea de tiempo verbal; un ViT diminuto debería pegar 90%+. La versión "las 300 clases" es un objetivo de stretch.
- Reutilizable en lab 01. El lab 01 usa los mismos íconos pareados con fragmentos de texto para entrenamiento estilo CLIP.
Spec de generación de íconos¶
Para cada triple (verb, tense, person) en el alcance de gramática de §A13 (20 × 5 × 3 = 300 combos):
# Pseudocode — fill in details in generate_icons.py
TENSE_COLOR = {
"infinitive": (128, 128, 128),
"present_simple": (50, 100, 200),
"past_simple": (200, 50, 50),
"past_participle": (50, 200, 100),
"future": (160, 80, 200),
}
PERSON_SHAPE = {
"1sg": "circle",
"2sg": "square",
"3sg": "triangle",
}
VERB_PATTERN = {
"work": "stripes_horizontal",
"play": "stripes_vertical",
# ... 18 more
}
for verb in VERBS:
for tense in TENSES:
for person in PERSONS:
for idx in range(N_PER_COMBO): # N_PER_COMBO = 1000 / 300 ≈ 3-4
img = render_icon(verb, tense, person, idx, jitter=True)
img.save(...)
Aplica jitter aleatorio (posición, ligera rotación, ligero ruido de color) para que el modelo vea variación por (verb, tense, person), forzándolo a aprender las features subyacentes en vez de memorizar.
Total: ~1000 íconos. ~3-4 por combo. Splits: 80% train (800), 20% test (200), estratificado por combo para que cada clase esté en ambos splits.
Arquitectura: ViT diminuto¶
Un ViT de 4 bloques, escalado dramáticamente hacia abajo desde ViT-Base.
Config¶
| Hyperparam | Valor | Notas |
|---|---|---|
| image_size | 32 | \(32 \times 32\) RGB |
| patch_size | 4 | da 64 parches |
| n_classes | 5 (tiempo verbal), o 15 (tiempo × persona), o 300 (completo) | |
| d_model | 64 | diminuto |
| n_heads | 4 | \(d_k = 16\) |
| n_blocks | 4 | reusa bloque Fase 17 × 4 |
| d_ff | 256 | expansión 4× |
| dropout | 0.1 | mínimo |
Total params: ~150k. Suficientemente pequeño para que puedas sanity-check computando tamaños de capa a mano.
Forward pass¶
input image (B, 32, 32, 3)
↓ patchify (P=4)
patches (B, 64, 48) # 4·4·3 = 48
↓ linear projection W_E (48 → 64)
patch tokens (B, 64, 64)
↓ prepend [CLS] token
tokens (B, 65, 64)
↓ + learnable position embedding (65, 64)
tokens (B, 65, 64)
↓ × 4 transformer blocks (Phase 17 block, no causal mask)
output (B, 65, 64)
↓ take [CLS] (index 0): (B, 64)
↓ classification head: Linear(64 → n_classes)
logits (B, n_classes)
Qué reutilizas de la Fase 17¶
from src.minimodel.transformer.block import TransformerBlock
El bloque de la Fase 17 hace self-attention causal. Para ViT queremos self-attention bidireccional (sin máscara). Dos opciones:
- Pasa
mask=Nonesi tu bloque de la Fase 17 acepta un argumento de máscara opcional. - Añade una flag
causal: bool = Trueal bloque y override aquí.
Elige la que requiera menos cambios a la Fase 17. Si tienes que añadir la flag, commitea el cambio a la Fase 17 por separado con una nota de que es requerido por X2 lab 00. No parchees la Fase 17 silenciosamente.
TODOs¶
Bloque A — diseño (escribir BLUEPRINT.md primero)¶
En BLUEPRINT.md, antes de cualquier código:
- Sketch del grafo computacional completo con formas de tensor en cada paso.
- Lista los params entrenables + su forma + conteo total. Sanity-check contra ~150k totales.
- Sketch del training loop: optimizador (SGD con momentum sirve — no se necesita Adam para 5 minutos de entrenamiento), LR, batch size, n_epochs.
- Sketch del plan de tests: al menos 5 tests unitarios para
vit.py(forma de patchify, prepending de [CLS], forma de pos emb, forma de forward completo, sanity de gradiente). - Lista las alternativas que consideraste y por qué las rechazaste.
Para. Obtén aprobación del usuario antes de continuar.
Bloque B — generar el dataset¶
En generate_icons.py:
- Lee
src/minimodel/grammar/verbs.py(o donde viva la lista de verbos de §A13) para obtener los 20 verbos canónicos, 5 tiempos, 3 personas. - Implementa
render_icon(verb, tense, person, idx, jitter=True)→ devuelve un array NumPy(32, 32, 3)uint8. - Itera sobre todos los (verb, tense, person, idx) → guarda como
verb_<verb>_tense_<tense>_person_<person>_<idx>.png. - Escribe
labels.json. - Verifica: cuenta archivos, inspecciona visualmente 20 íconos aleatoriamente muestreados (commit a
data/icons_sample.png).
Bloque C — implementar ViT¶
En vit.py:
-
patchify(x, P)— el einsum detheory/01-vision-transformers.md. -
class TinyViT— envuelve patchify + linear-proj + [CLS] + pos-emb + 4 bloques transformer + cls-head. - Inicializa el position embedding a aleatorio pequeño (std 0.02). Inicializa [CLS] a aleatorio pequeño.
Bloque D — entrenar¶
En train.py:
- Carga íconos + labels.
- Construye train/test split (80/20 estratificado por clase).
- Entrena para ≤ 20 epochs, batch size 32, SGD con momentum 0.9, LR 1e-3 con scheduling coseno.
- Loguea train loss + test accuracy cada epoch.
- Guarda
loss.pngyaccuracy.png.
Bloque E — evaluar¶
En eval.py:
- Carga el modelo final. Computa test accuracy.
- Computa matriz de confusión. Guarda como
confusion_matrix.png. - Aceptación: test accuracy ≥ 80% en la tarea de solo-tiempo-verbal (5 clases). Si estás por debajo de 80%, debug: revisa forma de salida de patchify, entropía de salida de atención, logits de clasificación por clase.
Bloque F — objetivos stretch (opcional)¶
- Repite para la tarea de 15-clases (tiempo × persona). Debería seguir pegando ≥ 70%.
- Repite para la tarea de 300-clases (verb × tense × person). Objetivo: ≥ 30% top-1 (azar es 0.33%; incluso 30% es significativo). Si no puedes pegar esto, la tarea probablemente excede la capacidad del modelo a 150k params — anótalo en el reporte.
- Visualiza el patrón de atención del token [CLS] sobre los 64 parches para un ejemplo por clase. Guarda como
cls_attention.png. ¿Se enfoca en las regiones que codifican la respuesta?
Criterios de aceptación¶
Tu lab 00 se shippea cuando:
BLUEPRINT.mdaprobado por el usuario antes de código.- 1000 íconos generados, reproduciblemente (seed en manifest).
- La implementación ViT reutiliza el bloque transformer de la Fase 17 — sin copy-paste de código de atención.
- La tarea de solo-tiempo-verbal alcanza ≥ 80% test accuracy.
- El entrenamiento tarda ≤ 5 minutos wall-clock en la CPU i5-8250U (4 cores). Perfila y reporta si excede.
- Matriz de confusión commiteada.
README.mdescrito, 1–2 párrafos.manifest.jsonincluye: versión NumPy, versión Pillow, seed, hyperparams, test accuracy final, wall-clock de entrenamiento.
Lo que este lab intencionadamente NO es¶
- No es un experimento SOTA. 150k params sobre íconos sintéticos prueban el mecanismo, no los límites. ViT-Base real sobre ImageNet son 86M params entrenados durante días; tenemos un juguete de 150k params.
- No es un clasificador genérico de imágenes. Los íconos están deliberadamente atados a gramática §A13.
- No es un benchmark de ViT vs CNN. Esa comparación necesita órdenes de magnitud más datos que 1000 íconos sintéticos. Estamos verificando el mecanismo.
Pistas de debugging¶
Si la accuracy está atascada en azar (20% para 5 clases):
- Revisa la salida de patchify. Imprime
patches.shape. Debería ser(B, 64, 48). Si es(B, 48, 64)tienes los ejes intercambiados. - Revisa el prepending de [CLS]. Tras prepender, forma es
(B, 65, 64). Tras 4 bloques, sigue siendo(B, 65, 64). Toma el índice 0 a lo largo del eje 1, no el eje 0 (eso tomaría batch). - Revisa el position embedding. Debería ser forma
(65, 64)y hacer broadcast sobre batch. Si escribiste(64, 65)por accidente, estás sumando posición al eje equivocado. - Revisa la entrada del cabezal de clasificación. Debería ser la representación de [CLS], forma
(B, 64). No la media de todos los tokens. - Revisa que la loss decrezca. Si la train loss está plana, el gradiente probablemente sea cero en algún lado. Imprime
np.linalg.norm(grad)en los pesos de cada bloque.
Si train accuracy >> test accuracy: el overfitting sobre 800 imágenes de entrenamiento es esperable para 150k params. Añade dropout (0.1–0.3) o reduce el tamaño del modelo.
Si el entrenamiento tarda > 5 min: perfila. El cuello de botella suele ser la atención. Con 64 tokens y d=64, la atención es pequeña (\(O(T^2 d) = 64^2 \cdot 64 = 262\)k ops por head por capa). El cuello de botella podría ser el reshape de patchify (si lo escribiste como un loop en vez de como einsum).
Lo que habrás aprendido¶
Al final del lab 00:
- Un bloque transformer es agnóstico a la modalidad. La misma arquitectura funciona para texto (Fase 17) e imágenes (este lab).
- Patchify es la única diferencia arquitectónica entre un vision transformer y un transformer de lenguaje. Todo lo demás se reutiliza.
- Un ViT de 150k params sobre datos sintéticos es el experimento de vision-transformer significativo más pequeño. Corre en una CPU en 5 minutos.
- ViT-Base real es la misma arquitectura escalada hacia arriba ~500× en parámetros y ~\(10^6 \times\) en datos. La mecánica extiende; las necesidades de datos son lo que difiere.
Siguiente lab: lab/01-clip-style-grammar-image-pairing.md reutiliza este ViT (sin el cabezal de clasificación) como el encoder de imagen para un setup contrastivo estilo CLIP, pareado con un encoder de texto diminuto.