English · Español
Lab 01 — Entrena embeddings CBOW sobre el corpus de gramática verbal¶
Lee
theory/02-cbow-skipgram.md. No consultessolutions/.
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.pydel 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
Embeddingentrenado (víaEmbedding.save) al final.
Tarea 4 — tests de sanity sobre las embeddings entrenadas¶
Antes del lab de visualización, verifica que el entrenamiento hizo algo:
- 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. - 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.)
- 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
workson 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_outatado aEpor accidente. Si escribesself.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.0aún puede divergir en el primer epoch. Empieza enlr=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.log2aquí.
Siguiente: 02-visualize-and-probe.md