Skip to content

English · Español

Lab 01 — Entrena embeddings CBOW sobre el corpus de gramática verbal

Lee theory/02-cbow-skipgram.md. No consultes solutions/.

Objetivo

Entrena embeddings CBOW minúsculas sobre el corpus de gramática verbal de la Fase 12. Tras 20 epochs con \(d = 32, k = 2\) (ventana 4 en total), la pérdida debería caer de ~\(\log V\) a ~2,0 y la matriz de embeddings resultante debería codificar suficiente estructura para que la visualización del Lab 02 muestre clustering por tiempo / verbo / idioma.

Setup

  • src/minimodel/embedding.py del Lab 00.
  • El corpus y el tokenizer de la Fase 12. Codifica el corpus en un array plano de ids de token.
  • Un script de entrenamiento nuevo: scripts/phase13_train_cbow.py.

Tareas

Tarea 1 — construir el dataset CBOW

A partir del corpus codificado, produce pares (context, center):

def make_cbow_pairs(tokens: NDArray[np.int64], window: int = 2) -> tuple[NDArray, NDArray]:
    """
    For each position t in tokens, take the 2*window tokens around it as context
    and the token at t as the center. Skip positions where the full context window
    doesn't fit (i.e., start and end of the corpus).

    Returns:
      contexts: (N, 2*window) int64
      centers:  (N,)           int64
    """

Elección pad-o-skip: skip es más limpio para nuestro corpus (no necesitamos que cada token sea un centro).

Para un corpus de \(L\) tokens con ventana 2: \(N = L - 4\) pares.

Tarea 2 — modelo

class CBOWModel:
    def __init__(self, vocab_size: int, embedding_dim: int):
        self.embed = Embedding(vocab_size, embedding_dim)
        # Output projection: separate matrix W_out, not tied to E (Word2Vec convention).
        self.W_out = Parameter(np.random.randn(vocab_size, embedding_dim) * 0.02)
        self.b_out = Parameter(np.zeros(vocab_size))

    def __call__(self, contexts: NDArray[np.int64]) -> Tensor:
        """contexts: (B, 2*window) → logits: (B, vocab_size)"""
        h = self.embed(contexts).mean(axis=1)         # (B, d)
        logits = h @ self.W_out.T + self.b_out        # (B, vocab_size)
        return logits

Nota: la matriz de salida W_out está separada de la embedding de entrada E. Word2Vec no las ata. (Los transformers de la Fase 17 sí atan la embedding de entrada a la cabeza LM, pero eso es distinto.)

Tarea 3 — bucle de entrenamiento

Usa cross_entropy_from_logits de la Fase 05 (o fusionada con softmax en el código de entrenamiento de la Fase 18 si está disponible; por ahora escríbela inline si hace falta).

def train(model, contexts, centers, epochs=20, batch_size=64, lr=0.01, momentum=0.9):
    """SGD with momentum. Log per-epoch loss. Return per-epoch loss array."""
    n = len(contexts)
    losses = []
    for epoch in range(epochs):
        perm = np.random.permutation(n)
        epoch_loss = 0.0
        for batch_start in range(0, n, batch_size):
            idx = perm[batch_start:batch_start + batch_size]
            logits = model(contexts[idx])
            loss = cross_entropy_from_logits_batch(logits, centers[idx]).mean()
            loss.backward()
            sgd_step(model.parameters(), lr=lr, momentum=momentum)
            epoch_loss += float(loss.value) * len(idx)
        epoch_loss /= n
        losses.append(epoch_loss)
        print(f"epoch {epoch:3d}: loss = {epoch_loss:.4f}")
    return losses

Restricciones:

  • Siembra todo vía src/utils/seeding.py.
  • Manifiesta la ejecución: hiperparámetros, ruta del corpus, tamaño de vocabulario, dimensión de embedding, pérdida final. Guarda en experiments/<date>-phase-13-cbow/manifest.json.
  • Guarda el Embedding entrenado (vía Embedding.save) al final.

Tarea 4 — tests de sanity sobre las embeddings entrenadas

Antes del lab de visualización, verifica que el entrenamiento hizo algo:

  1. Sanity de la curva de pérdida. Plotea la pérdida por epoch. Debería ser monótona decreciente (quizá ruidosa). Guarda como experiments/<date>-phase-13-cbow/loss_curve.png.
  2. Normas de tokens frecuentes vs raros. Calcula \(\|E[i]\|\) para cada token; ordena por frecuencia. Los tokens más frecuentes deberían tener norma mayor que los menos frecuentes. (Esta es la correlación norma-frecuencia que mitigaremos con similitud coseno en el Lab 02.)
  3. Top-5 vecinos coseno más cercanos de work. Deberían incluir verbos en contextos similares (walk, talk) y no deberían estar dominados por puntuación. Si lo están, aumenta epochs o revisa la tokenización.

Tarea 5 — registrar las métricas destacadas

Guarda en experiments/<date>-phase-13-cbow/summary.json:

{
  "epochs": 20,
  "embedding_dim": 32,
  "window": 2,
  "vocab_size": 64,
  "corpus_size_tokens": 2400,
  "final_loss": 2.07,
  "initial_loss": 4.16,
  "loss_drop": 2.09,
  "top5_nearest_work_cosine": ["walk", "talk", "study", "play", "watch"],
  "top5_nearest_work_euclidean": [".", ",", "the", "a", "is"]
}

(Estos números son ilustrativos; tus resultados reales variarán.)

Aceptación

  • Dataset CBOW construido: \(N\) pares de forma \((2k,)\) context + centro escalar.
  • El modelo CBOW entrena 20 epochs sin error.
  • La pérdida cae de ~\(\log V = 4{,}16\) a menos de 2,5.
  • Plot de la curva de pérdida guardado.
  • Los top-5 vecinos coseno más cercanos de work son mayoritariamente verbos (no puntuación).
  • Embedding entrenada guardada vía Embedding.save.
  • Manifiesto comiteado.

Escollos esperables

  • Olvidar mean(axis=1) sobre los contextos. Si sumas sin promediar, las magnitudes derivan con el tamaño de ventana y la pérdida es inestable.
  • W_out atado a E por accidente. Si escribes self.W_out = self.embed.E, los has atado. CBOW convencionalmente no los ata; atarlos cambia la superficie de pérdida. Mantenlos desatados en este lab.
  • Tasa de aprendizaje demasiado alta. Con \(V = 64, d = 32\), la superficie de pérdida es benévola, pero lr=1.0 aún puede divergir en el primer epoch. Empieza en lr=0.01; ajusta si hace falta.
  • Saltar posiciones en vez de pad-skip. Cuando window = 2, tienes que saltar las 2 primeras y 2 últimas posiciones del corpus. Si no, indexarás fuera de rango.
  • Confusión en el log: pérdida en nats vs bits. Usamos nats en todo (según la Fase 05). No coles np.log2 aquí.

Siguiente: 02-visualize-and-probe.md