Phase 05 - Lesson 03

Word Embeddings — Word2Vec desde cero

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

Una palabra es la compania que mantiene. Entrena una red poco profunda con esa idea y la geometria surge sola.

Tipo: Build Lenguajes: Python Prerrequisitos: Fase 5 · 02 (BoW + TF-IDF), Fase 3 · 03 (Backpropagation desde cero) Tiempo: ~75 minutos

El Problema

TF-IDF sabe que dog y puppy son palabras distintas. No sabe que significan casi lo mismo. Un clasificador entrenado con dog no puede generalizar a una resena sobre puppy. Puedes tapar esto enumerando sinonimos, pero eso falla con terminos raros, jerga de dominio y todo idioma que no anticipaste.

Quieres una representacion donde dog y puppy queden cerca uno del otro en el espacio. Donde king - man + woman caiga cerca de queen. Donde un modelo entrenado con dog transfiera algo de senal gratis a puppy.

Word2Vec nos dio ese espacio. Red neuronal de dos capas, ejecuciones de entrenamiento de billones de tokens, publicado en 2013. La arquitectura es casi vergonzosamente simple. Los resultados remodelaron el NLP durante una decada.

El Concepto

Ventana skip-gram y espacio de embeddings

Hipotesis distribucional (Firth, 1957): "Conoceras una palabra por la compania que mantiene." Si dos palabras aparecen en contextos similares, probablemente significan cosas similares.

Word2Vec viene en dos sabores, ambos explotando esa idea.

  • Skip-gram. Dada una palabra central, predice las palabras circundantes. cat -> (the, sat, on) con ventana de tamano 2.
  • CBOW (continuous bag of words). Dadas las palabras circundantes, predice la central. (the, sat, on) -> cat.

Skip-gram es mas lento de entrenar pero maneja mejor las palabras raras. Se convirtio en el predeterminado.

La red tiene una capa oculta sin no linealidad. La entrada es un vector one-hot sobre el vocabulario. La salida es un softmax sobre el vocabulario. Despues del entrenamiento, descartas la capa de salida. Los pesos de la capa oculta son los embeddings.

one-hot(center) ── W ──▶ hidden (d-dim) ── W' ──▶ softmax(vocab)
                          ^
                          this is the embedding

El truco: un softmax sobre 100k palabras es prohibitivamente costoso. Word2Vec usa muestreo negativo (negative sampling) para convertirlo en una tarea de clasificacion binaria. Predice "esta palabra de contexto aparecio cerca de esta palabra central, si o no". Muestrea un punado de palabras negativas (que no co-ocurren) por par de entrenamiento en lugar de calcular el softmax sobre todo el vocabulario.

Construyelo

Paso 1: pares de entrenamiento a partir de un corpus

def skipgram_pairs(docs, window=2):
    pairs = []
    for doc in docs:
        for i, center in enumerate(doc):
            for j in range(max(0, i - window), min(len(doc), i + window + 1)):
                if i == j:
                    continue
                pairs.append((center, doc[j]))
    return pairs
>>> skipgram_pairs([["the", "cat", "sat", "on", "mat"]], window=2)
[('the', 'cat'), ('the', 'sat'),
 ('cat', 'the'), ('cat', 'sat'), ('cat', 'on'),
 ('sat', 'the'), ('sat', 'cat'), ('sat', 'on'), ('sat', 'mat'),
 ...]

Cada par (central, contexto) dentro de una ventana es un ejemplo de entrenamiento positivo.

Paso 2: tablas de embedding

Dos matrices. W es la tabla de embeddings de la palabra central (la que conservas). W' es la tabla de la palabra de contexto (a menudo descartada, a veces promediada con W).

import numpy as np


def init_embeddings(vocab_size, dim, seed=0):
    rng = np.random.default_rng(seed)
    W = rng.normal(0, 0.1, size=(vocab_size, dim))
    W_prime = rng.normal(0, 0.1, size=(vocab_size, dim))
    return W, W_prime

Inicializacion aleatoria pequena. Un vocabulario de 10k y dim 100 es realista; para ensenanza, 50 de vocabulario x 16 de dim basta para ver la geometria.

Paso 3: objetivo de muestreo negativo

Para cada par positivo (center, context), muestrea k palabras aleatorias del vocabulario como negativas. Entrena el modelo para que el producto punto W[center] · W'[context] sea alto para los positivos y bajo para los negativos.

def sigmoid(x):
    return 1.0 / (1.0 + np.exp(-np.clip(x, -20, 20)))


def train_pair(W, W_prime, center_idx, context_idx, negative_indices, lr):
    v_c = W[center_idx]
    u_pos = W_prime[context_idx]
    u_negs = W_prime[negative_indices]

    pos_score = sigmoid(v_c @ u_pos)
    neg_scores = sigmoid(u_negs @ v_c)

    grad_center = (pos_score - 1) * u_pos
    for i, u in enumerate(u_negs):
        grad_center += neg_scores[i] * u

    W[context_idx] = W[context_idx]
    W_prime[context_idx] -= lr * (pos_score - 1) * v_c
    for i, neg_idx in enumerate(negative_indices):
        W_prime[neg_idx] -= lr * neg_scores[i] * v_c
    W[center_idx] -= lr * grad_center

La formula magica: perdida logistica en el par positivo (queremos sigmoide cerca de 1) mas perdida logistica en los pares negativos (queremos sigmoide cerca de 0). Los gradientes fluyen a ambas tablas. La derivacion completa esta en el articulo original; recorrela una vez con lapiz y papel si quieres que se te fije.

Paso 4: entrena en un corpus de juguete

def train(docs, dim=16, window=2, k_neg=5, epochs=100, lr=0.05, seed=0):
    vocab = build_vocab(docs)
    vocab_size = len(vocab)
    rng = np.random.default_rng(seed)
    W, W_prime = init_embeddings(vocab_size, dim, seed=seed)
    pairs = skipgram_pairs(docs, window=window)

    for epoch in range(epochs):
        rng.shuffle(pairs)
        for center, context in pairs:
            c_idx = vocab[center]
            ctx_idx = vocab[context]
            negs = rng.integers(0, vocab_size, size=k_neg)
            negs = [n for n in negs if n != ctx_idx and n != c_idx]
            train_pair(W, W_prime, c_idx, ctx_idx, negs, lr)
    return vocab, W

Tras suficientes epocas en un corpus grande, las palabras que comparten contextos tienen embeddings centrales similares. En un corpus de juguete, ves el efecto debilmente. En miles de millones de tokens, lo ves de forma dramatica.

Paso 5: el truco de la analogia

def nearest(vocab, W, target_vec, topk=5, exclude=None):
    exclude = exclude or set()
    inv_vocab = {i: w for w, i in vocab.items()}
    norms = np.linalg.norm(W, axis=1, keepdims=True) + 1e-9
    W_norm = W / norms
    target = target_vec / (np.linalg.norm(target_vec) + 1e-9)
    sims = W_norm @ target
    order = np.argsort(-sims)
    out = []
    for i in order:
        if i in exclude:
            continue
        out.append((inv_vocab[i], float(sims[i])))
        if len(out) == topk:
            break
    return out


def analogy(vocab, W, a, b, c, topk=5):
    v = W[vocab[b]] - W[vocab[a]] + W[vocab[c]]
    return nearest(vocab, W, v, topk=topk, exclude={vocab[a], vocab[b], vocab[c]})

Sobre los vectores preentrenados Google News de 300d:

>>> analogy(vocab, W, "man", "king", "woman")
[('queen', 0.71), ('monarch', 0.62), ('princess', 0.59), ...]

king - man + woman = queen. No porque el modelo sepa que es la realeza. Porque el vector (king - man) captura algo como "real", y sumarlo a woman cae cerca de la region real-femenina.

Usalo

Escribir Word2Vec desde cero es didactico. El NLP de produccion usa gensim.

from gensim.models import Word2Vec

sentences = [
    ["the", "cat", "sat", "on", "the", "mat"],
    ["the", "dog", "ran", "across", "the", "room"],
]

model = Word2Vec(
    sentences,
    vector_size=100,
    window=5,
    min_count=1,
    sg=1,
    negative=5,
    workers=4,
    epochs=30,
)

print(model.wv["cat"])
print(model.wv.most_similar("cat", topn=3))

Para trabajo real, casi nunca entrenas Word2Vec por tu cuenta. Descargas vectores preentrenados.

  • GloVe — enfoque de factorizacion de matriz de co-ocurrencia de Stanford. Checkpoints de 50d, 100d, 200d, 300d. Buena cobertura general. La leccion 04 cubre GloVe especificamente.
  • fastText — extension de Word2Vec de Facebook que embebe n-gramas de caracteres. Maneja palabras fuera del vocabulario componiendo subpalabras. Leccion 04.
  • Word2Vec preentrenado en Google News — 300d, vocabulario de 3M de palabras, publicado en 2013. Aun se descarga a diario.

Cuando Word2Vec todavia gana en 2026

  • Recuperacion ligera y especifica de dominio. Entrena con resumenes medicos en una hora en una laptop y obten vectores especializados que ningun modelo general captura.
  • Ingenieria de features estilo analogia. gender_vector = mean(man - woman pairs). Restalo de otras palabras para obtener un eje de genero neutro. Aun se usa en investigacion de equidad (fairness).
  • Interpretabilidad. 100d es lo bastante pequeno como para graficar via PCA o t-SNE y ver realmente como se forman los clusters.
  • Donde sea que la inferencia tenga que correr en el dispositivo sin GPU. La busqueda de Word2Vec es la lectura de una sola fila.

Donde Word2Vec falla

El muro de la polisemia. bank tiene un solo vector. river bank y financial bank lo comparten. table (hoja de calculo vs. mueble) lo comparte. Un clasificador aguas abajo no puede distinguir los sentidos a partir del vector.

Los embeddings contextuales (ELMo, BERT, todo transformer desde entonces) resolvieron esto produciendo un vector distinto para cada aparicion de la palabra segun el contexto circundante. Ese es el salto de Word2Vec a BERT: de lo estatico a lo contextual. La Fase 7 cubre la mitad del transformer.

El problema de fuera del vocabulario (out-of-vocabulary) es el otro fallo. Word2Vec nunca ha visto Zoomer-approved si no estaba en los datos de entrenamiento. Sin fallback. fastText lo arregla con composicion de subpalabras (leccion 04).

Entregalo

Guarda como outputs/skill-embedding-probe.md:

---
name: embedding-probe
description: Inspect a word2vec model. Run analogies, find neighbors, diagnose quality.
version: 1.0.0
phase: 5
lesson: 03
tags: [nlp, embeddings, debugging]
---

You probe trained word embeddings to verify they are working. Given a `gensim.models.KeyedVectors` object and a vocabulary, you run:

1. Three canonical analogy tests. `king : man :: queen : woman`. `paris : france :: tokyo : japan`. `walking : walked :: swimming : ?`. Report the top-1 result and its cosine.
2. Five nearest-neighbor tests on domain-specific words the user supplies. Print top-5 neighbors with cosines.
3. One symmetry check. `similarity(a, b) == similarity(b, a)` to within float precision.
4. One degenerate check. If any embedding has a norm below 0.01 or above 100, the model has a training bug. Flag it.

Refuse to declare a model good on analogy accuracy alone. Analogy benchmarks are gameable and do not transfer to downstream tasks. Recommend intrinsic + downstream evaluation together.

Ejercicios

  1. Facil. Ejecuta el bucle de entrenamiento en un corpus diminuto (20 oraciones sobre gatos y perros). Tras 200 epocas, verifica que nearest(vocab, W, W[vocab["cat"]]) devuelva dog en su top 3. Si no, aumenta las epocas o el vocabulario.
  2. Medio. Agrega submuestreo de palabras frecuentes. Las palabras con frecuencia superior a 10^-5 se descartan de los pares de entrenamiento con probabilidad proporcional a su frecuencia. Mide el efecto en la similitud de palabras raras.
  3. Dificil. Entrena un modelo en el corpus 20 Newsgroups. Calcula dos ejes de sesgo: he - she y doctor - nurse. Proyecta palabras de ocupacion sobre ambos ejes. Reporta que ocupaciones tienen la mayor brecha de sesgo. Este es el tipo de sondeo que usan los investigadores de equidad.

Terminos Clave

Termino Lo que la gente dice Lo que realmente significa
Word embedding Palabra como vector Una representacion densa, de baja dimension (tipicamente 100-300) aprendida a partir del contexto.
Skip-gram Truco de Word2Vec Predecir palabras de contexto a partir de la palabra central. Mas lento que CBOW, mejor para palabras raras.
Muestreo negativo Atajo de entrenamiento Reemplazar el softmax sobre todo el vocabulario por clasificacion binaria contra k palabras aleatorias.
Embedding estatico Un vector por palabra El mismo vector sin importar el contexto. Falla en la polisemia.
Embedding contextual Vector sensible al contexto Vector distinto para cada aparicion segun las palabras circundantes. Lo que producen los transformers.
OOV Fuera del vocabulario Palabra no vista en el entrenamiento. Word2Vec no puede producir un vector para ellas.

Lecturas Adicionales

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).