Skip to content

English · Español

Lab 01 — Pareo Imagen-Texto de Gramática Estilo CLIP

Objetivo: construir un trainer contrastivo mínimo estilo CLIP. Reutilizar el ViT del lab 00 (sin cabezal de clasificación) como el encoder de imagen; construir un encoder de texto diminuto (transformer de la Fase 17 en modo encoder) para snippets de texto de formas verbales; entrenar con InfoNCE simétrico sobre pares (ícono, texto); benchmarkear retrieval top-k.

🇪🇸 Implementamos el bucle de entrenamiento contrastivo de CLIP sobre nuestro dominio de gramática. La métrica clave: dada una imagen de ícono, ¿se recupera el snippet de texto correcto en top-k? Y viceversa. Esto verifica que las dos modalidades se alinean en el mismo espacio de embedding.

Tiempo estimado: 4–5 horas.

Prerequisitos: - Lab 00 completo (tienes un ViT funcionando). - docs/extension-track/X2-multimodal/theory/01-vision-transformers.md §"CLIP" entendido — específicamente, la pérdida InfoNCE simétrica.


Lo que produces

Un directorio experiments/X2-multimodal/lab-01-clip-grammar/ que contiene:

  • BLUEPRINT.md
  • dataset.py — parea cada ícono del lab 00 con un snippet de texto (la forma verbal que representa).
  • image_encoder.py — ViT del lab 00, cabezal de clasificación eliminado; emite la representación de [CLS] (64-dim).
  • text_encoder.py — transformer diminuto sobre tokens BPE/word del snippet de forma verbal; emite la representación de [EOS] (64-dim).
  • clip_model.py — envuelve ambos encoders + L2-normalize + escalar de temperatura.
  • loss.pysymmetric_infonce.
  • train.py.
  • eval.py — top-1, top-5 retrieval en ambas direcciones.
  • manifest.json.
  • README.md.
  • loss.png, retrieval_curves.png, confusion_matrix_top1.png.

El dataset

Reutilizamos los 1000 íconos del lab 00. Para cada ícono, generamos un pequeño conjunto de snippets de texto equivalentes que describen el mismo triple (verb, tense, person). Ejemplos para el ícono que codifica (verb=work, tense=present_simple, person=3sg):

"he works"
"she works"
"it works"
"3rd person singular present simple of work"
"works"

Para cada ícono, muestrea 1 snippet de texto por step de entrenamiento (aumento de datos). Pares totales por epoch ≈ 1000.

Vocabulario de texto

No uses BPE aquí — overkill. Construye un pequeño vocabulario a nivel de palabra desde el corpus:

  • 20 verbos (work, play, walk, ...) → 20 tokens.
  • Formas flexionadas (works, worked, working, ...) → ~80 tokens (4-5 formas × 20 verbos).
  • Pronombres: I, you, he, she, it → 5 tokens.
  • Palabras de tiempo: present, past, simple, participle, future → 5 tokens.
  • Auxiliares: will, going, to → 3 tokens.
  • Glue: of, the, person, singular, 3rd, 2nd, 1st → 7 tokens.
  • Especiales: <BOS>, <EOS>, <PAD>, <UNK> → 4 tokens.

Total: ~124 tokens. Vocabulario diminuto, tabla de embedding tratable.

Tokeniza snippets: minúsculas, split en whitespace, mapea cada palabra a su ID de vocabulario. Padding a longitud 8. Añade <BOS> y <EOS>.

Por qué estos snippets

  • Múltiples snippets por (verb, tense, person) → augmentación. El modelo no puede memorizar "ícono X = snippet Y"; debe aprender la estructura (color → tiempo verbal, forma → persona, patrón → verbo).
  • La variedad de snippets imita el setting del mundo real de CLIP donde los captions varían en estilo.

Arquitectura: el modelo CLIP

┌───────── image branch ─────────┐    ┌──────── text branch ────────┐
icon (32, 32, 3)                       tokens (T,) — e.g. 8 tokens
  ↓ patchify (P=4)                       ↓ embedding lookup
patches (64, 48)                         token emb (8, 64)
  ↓ linear proj                          ↓ + learned pos emb
patch tokens (64, 64)                  embedded (8, 64)
  ↓ prepend [CLS]                        ↓ × 2 transformer blocks
tokens (65, 64)                        output (8, 64)
  ↓ + pos emb                            ↓ take [EOS] (last position)
  ↓ × 4 transformer blocks                z_T (64,)
output (65, 64)                          ↓ proj_T (Linear 64 → 64)
  ↓ take [CLS]                          z_T (64,)
z_I (64,)                                ↓ L2 normalize
  ↓ proj_I (Linear 64 → 64)            z_T_norm (64,)
z_I (64,)
  ↓ L2 normalize
z_I_norm (64,)
└─────────────────────────────┘    └────────────────────────────┘
                  ↓                                 ↓
                   logits = z_I_norm @ z_T_norm.T / tau    (N × N)
                  Symmetric InfoNCE — cross-entropy on diagonal, both directions

Nota la L2-normalización + temperatura: esto es lo que hace que el producto punto sea una similitud coseno y da gradientes bien definidos a la pérdida InfoNCE.

Conteos de params

Componente Params (aprox)
Encoder de imagen (ViT lab 00) 150k
Cabezal de proyección de imagen 4k
Embedding de tokens de texto (124 × 64) 8k
Position embedding de texto (8 × 64) 512
Transformer de texto (2 bloques) 60k
Cabezal de proyección de texto 4k
Temperatura τ (escalar) 1
Total ~230k

Entrenable en CPU. ~1 minuto por epoch en la i5-8250U a batch size 32.


La pérdida: InfoNCE simétrica

La implementación completa en NumPy (o tu autograd de la Fase 08, dependiendo de dónde esté el currículo):

def symmetric_infonce(z_I, z_T, tau):
    """
    z_I, z_T: (N, d), each row L2-normalized.
    tau: float, temperature.
    Returns scalar loss.
    """
    N = z_I.shape[0]
    logits = z_I @ z_T.T / tau          # (N, N)
    labels = np.arange(N)
    # cross_entropy reduces with mean
    loss_i2t = cross_entropy(logits, labels)        # i-th row → label i
    loss_t2i = cross_entropy(logits.T, labels)      # i-th col → label i
    return (loss_i2t + loss_t2i) / 2

Dos interpretaciones de la misma matriz:

  • Fila \(i\): ¿qué texto matchea con la imagen \(i\)? Softmax sobre los \(N\) textos del batch. Ground truth es el índice \(i\).
  • Columna \(j\): ¿qué imagen matchea con el texto \(j\)? Softmax sobre las \(N\) imágenes del batch. Ground truth es el índice \(j\).

Pérdida simétrica = media de ambas.

Por qué importa el batch size

Los "negativos" para la imagen \(i\) son todos los textos \(j \ne i\) del batch. Con batch 32, tienes 31 negativos. CLIP-el-paper usó batch 32\,768 → 32\,767 negativos.

Para nuestro problema juguete (dataset pequeño, tarea fácil), 31 negativos es suficiente. No intentes hacer que este lab iguale la escala de CLIP.

La temperatura

CLIP usa un parámetro de temperatura aprendible, inicializado a \(1/0.07 \approx 14.29\) (así los logits se escalan hacia arriba por ~14, después el softmax produce distribuciones más afiladas). La temperatura es aprendible pero clamped en CLIP — no puede exceder \(\exp(4.6) \approx 100\) para prevenir que la pérdida diverja.

Para nuestro problema juguete, fija \(\tau = 0.1\) (así \(1/\tau = 10\)) y no la hagas aprendible. Un hyperparam menos que debuggear.


TODOs

Bloque A — diseño

  • BLUEPRINT.md primero, antes de código. Incluye:
  • Diagrama de arquitectura con formas.
  • Conteo de params hasta dentro del 10% del estimado.
  • Política de split train/test. Crítico: hold out de 4 de los 20 verbos enteramente del entrenamiento, así el test set tiene verbos no vistos. Esto fuerza al modelo a aprender estructura composicional (color = tiempo, forma = persona, patrón = verbo) en vez de memorizar.
  • Hyperparámetros: batch size 32, LR 1e-3 con coseno, n_epochs 30, tau 0.1.
  • Plan de tests: top-1 y top-5 retrieval en ambas direcciones, sobre verbos en holdout.

Pausa para aprobación.

Bloque B — implementar encoders

  • image_encoder.py. Subclase / envuelve tu ViT del lab 00. Quita el cabezal de clasificación; expón forward(image) → z_I (B, d).
  • text_encoder.py. Nuevo transformer diminuto (2 bloques, d_model=64, 4 heads). Embedding + posición + bloques + toma [EOS]. Reusa el bloque transformer de la Fase 17.

Bloque C — modelo CLIP + pérdida

  • clip_model.py. Envuelve ambos encoders + cabezales de proyección + L2-normalize.
  • loss.py. symmetric_infonce con helper cross_entropy.
  • Test unitario de la pérdida:
  • Con \(z_I = z_T\) (alineamiento perfecto), batch 4, \(\tau = 0.1\): la pérdida debería igualar \(-\log(\text{softmax-de-diag})\) ≈ 0. (No exactamente 0 porque la diagonal no es \(+\infty\); pero muy pequeña.)
  • Con \(z_I\) aleatorio y \(z_T\) aleatorio, batch 4: la pérdida debería ser alrededor de \(\log(4) \approx 1.39\) (uniforme sobre 4 clases).

Bloque D — bucle de entrenamiento

  • train.py. Bucle estándar. Loguea loss + retrieval-accuracy-en-batch cada step.
  • Guarda la curva de loss como loss.png.

Bloque E — eval

  • eval.py. Para cada ícono en holdout, computa su embedding + embebe todos los snippets de texto en holdout. Ranquea por coseno; reporta top-1 y top-5 accuracy.
  • Repite en la dirección texto → imagen.
  • Aceptación:
  • Top-1 retrieval (imagen → texto) ≥ 50% sobre verbos en holdout. (Azar sería ~5%.)
  • Top-5 retrieval ≥ 80%.
  • Si pegas estos, has verificado que el mecanismo contrastivo funciona en el dominio de gramática.
  • Guarda retrieval_curves.png (top-k vs k) y confusion_matrix_top1.png (predicho vs clase verdadera).

Bloque F — objetivos stretch

  • Reemplaza el InfoNCE softmax simétrico con pérdida sigmoide estilo SigLIP. Compara velocidad de convergencia y retrieval final a batch 32. SigLIP debería ganar a este batch size pequeño.
  • Sondea el modality gap de theory/00-motivation.md: computa el centroide de todos los embeddings \(z_I\), centroide de todos los embeddings \(z_T\), mide la distancia coseno. Reporta. Compara antes y después del entrenamiento. (Expectativa: en init está cerca de 0.5–1.0; tras el entrenamiento sigue siendo > 0 pero más pequeño. Confirma que el modality gap es real en un problema juguete también.)
  • Visualiza los embeddings aprendidos vía PCA-a-2D. Guarda embeddings_pca.png. Colorea embeddings de imagen en azul, texto en rojo. ¿Forman dos clusters (modality gap) o se intercalan (sin gap)?

Criterios de aceptación

  1. BLUEPRINT.md aprobado.
  2. Encoder de imagen reutilizado del lab 00 (sin copy-paste).
  3. Encoder de texto reutiliza el bloque transformer de la Fase 17.
  4. symmetric_infonce casa con la spec; tests unitarios pasan.
  5. Top-1 retrieval ≥ 50%, top-5 ≥ 80% sobre verbos en holdout.
  6. Entrenamiento ≤ 30 minutos wall-clock en CPU.
  7. README.md, manifest.json, todos los artefactos commiteados.

Lo que este lab intencionadamente NO es

  • No es una reproducción de CLIP. CLIP real necesita órdenes de magnitud más datos y batch size. Verificamos el mecanismo.
  • No es clasificación zero-shot de ImageNet. Eso requiere un modelo que haya visto imágenes del mundo real, que no tenemos.
  • No es una comparación con pérdidas no-contrastivas (p. ej. cross-entropy de generación de caption). Podría ser un objetivo stretch.

Pistas de debugging

Si el retrieval top-1 está en azar (~5%) tras varios epochs:

  1. Revisa L2-normalización. Tras normalizar, z_I.norm(axis=1) debería ser todo 1. Si no, no estás en una esfera unidad; la interpretación coseno falla.
  2. Revisa que la loss decrezca. Si está plana, el gradiente podría no fluir. Imprime la norma de gradiente de la primera capa de cada encoder. Debería ser de magnitud similar. Si la norma de gradiente de imagen es 100× la de texto, tienes un problema de escala — probablemente la L2-normalize no es diferenciable / olvidaste hacer backprop a través de ella.
  3. Revisa la diagonal. Imprime logits antes del softmax para un batch pequeño. La diagonal debería crecer más positiva sobre el entrenamiento; la fuera-de-diagonal debería crecer más negativa.
  4. Los verbos en holdout son demasiado difíciles. Prueba la eval primero sobre el split de test in-distribution (íconos de verbos de entrenamiento pero muestras no vistas). Si pegas > 80% ahí pero fallas en holdout, el modelo está memorizando los patrones de los verbos de entrenamiento y no generalizando. Eso sigue siendo un hallazgo válido — anótalo en el reporte.

Si el modality gap (objetivo stretch) es enorme tras el entrenamiento (> 0.5 distancia coseno entre centroides):

  • Prueba entrenar más tiempo.
  • Prueba un \(\tau\) más pequeño (softmax más afilado, más presión contrastiva).
  • Prueba proyección compartida (la misma capa lineal proyecta ambas modalidades). Algunas variantes de CLIP hacen esto; fuerza alineamiento en el espacio de proyección.

Lo que habrás aprendido

  • InfoNCE simétrico en la práctica: el softmax simétrico sobre una matriz de similitud.
  • La dependencia del batch size de la pérdida contrastiva: más negativos = pérdida más dura = mejores representaciones. A batch pequeño, el aprendizaje contrastivo es débil.
  • El modality gap como un fenómeno medible, no solo una afirmación de paper.
  • Que construir un "modelo estilo CLIP" mecánicamente es fácil — tres componentes (encoder, encoder, pérdida contrastiva). La parte difícil son los datos y la escala.

Siguiente lab: lab/02-whisper-inference-walkthrough.md cambia a audio — carga Whisper-tiny, corre inferencia sobre clips pre-grabados de formas verbales en inglés, inspecciona la atención interna y los logits de timestamp.