📝 Pseudocódigo del Algoritmo Word2Vec
Word2Vec, introducido por Mikolov et al. en Google en 2013, aprende representaciones vectoriales densas (embeddings) de palabras a partir de grandes corpus de texto. Captura relaciones semánticas de modo que palabras similares tienen vectores similares, permitiendo aritmética como: rey - hombre + mujer ≈ reina.
El Algoritmo
Visión General de las Arquitecturas Word2Vec
WORD2VEC: Dos Arquitecturas
─────────────────────────────────────────────────────────────────
1. SKIP-GRAM:
Dada una palabra objetivo, predecir las palabras de contexto circundantes.
"El rápido zorro marrón salta"
↓
Objetivo: "zorro"
Predecir: ["rápido", "marrón"] (tamaño de ventana 1)
Mejor para: Palabras infrecuentes, conjuntos de datos más pequeños
2. CBOW (Bolsa Continua de Palabras):
Dadas las palabras de contexto circundantes, predecir la palabra objetivo.
"El rápido ___ marrón salta"
↓
Contexto: ["rápido", "marrón"]
Predecir: "zorro"
Mejor para: Palabras frecuentes, conjuntos de datos más grandesModelo Skip-Gram
ALGORITMO: Entrenamiento Skip-Gram (Versión Básica)
─────────────────────────────────────────────────────────────────
ENTRADA:
corpus: Lista de oraciones (listas de tokens de palabras)
V: Tamaño del vocabulario
d: Dimensión del embedding (típicamente 100-300)
ventana: Tamaño de la ventana de contexto
tasa_aprendizaje: η
SALIDA:
W_in: Matriz de embedding de entrada (V × d) - estos son los vectores de palabras
W_out: Matriz de embedding de salida (d × V)
PROCEDIMIENTO:
1. // Inicializar embeddings aleatoriamente
W_in ← uniforme_aleatorio(-0.5/d, 0.5/d, forma=(V, d))
W_out ← ceros(d, V)
2. // Construir vocabulario y mapeo palabra-a-índice
palabra_a_idx ← {palabra: i para i, palabra en enumerar(vocabulario)}
3. // Bucle de entrenamiento
PARA cada oración en corpus:
PARA t = 0 HASTA longitud(oración) - 1:
palabra_objetivo ← oración[t]
idx_objetivo ← palabra_a_idx[palabra_objetivo]
// Obtener palabras de contexto dentro de la ventana
PARA j = max(0, t - ventana) HASTA min(len(oración), t + ventana):
SI j ≠ t:
palabra_contexto ← oración[j]
idx_contexto ← palabra_a_idx[palabra_contexto]
// Paso adelante: predecir contexto desde objetivo
// Obtener vector de palabra objetivo
v_objetivo ← W_in[idx_objetivo] // (d,)
// Calcular puntuaciones para todas las palabras
puntuaciones ← W_out.T · v_objetivo // (V,)
// Softmax para obtener probabilidades
probs ← softmax(puntuaciones) // (V,)
// Paso atrás: descenso del gradiente
// Gradiente de pérdida de entropía cruzada
error ← probs.copiar()
error[idx_contexto] -= 1 // y - ŷ
// Actualizar pesos de salida
W_out ← W_out - η × producto_externo(v_objetivo, error)
// Actualizar embedding de entrada
grad_entrada ← W_out · error
W_in[idx_objetivo] ← W_in[idx_objetivo] - η × grad_entrada
4. DEVOLVER W_in, W_out // Usualmente solo se usa W_inModelo CBOW
ALGORITMO: Entrenamiento CBOW (Versión Básica)
─────────────────────────────────────────────────────────────────
ENTRADA:
corpus: Lista de oraciones
V: Tamaño del vocabulario
d: Dimensión del embedding
ventana: Tamaño de la ventana de contexto
tasa_aprendizaje: η
SALIDA:
W_in: Matriz de embedding de entrada (V × d)
W_out: Matriz de embedding de salida (d × V)
PROCEDIMIENTO:
1. // Inicializar embeddings
W_in ← uniforme_aleatorio(-0.5/d, 0.5/d, forma=(V, d))
W_out ← ceros(d, V)
2. // Bucle de entrenamiento
PARA cada oración en corpus:
PARA t = 0 HASTA longitud(oración) - 1:
palabra_objetivo ← oración[t]
idx_objetivo ← palabra_a_idx[palabra_objetivo]
// Recopilar índices de palabras de contexto
indices_contexto ← []
PARA j = max(0, t - ventana) HASTA min(len(oración), t + ventana):
SI j ≠ t:
indices_contexto.añadir(palabra_a_idx[oración[j]])
SI indices_contexto está vacío:
CONTINUAR
// Paso adelante: predecir objetivo desde contexto
// Promediar vectores de palabras de contexto (CBOW)
vectores_contexto ← [W_in[idx] para idx en indices_contexto]
v_contexto ← promedio(vectores_contexto, eje=0) // (d,)
// Calcular puntuaciones y probabilidades
puntuaciones ← W_out.T · v_contexto // (V,)
probs ← softmax(puntuaciones)
// Paso atrás
error ← probs.copiar()
error[idx_objetivo] -= 1
// Actualizar pesos de salida
W_out ← W_out - η × producto_externo(v_contexto, error)
// Actualizar embeddings de entrada (distribuir gradiente a todas las palabras de contexto)
grad_entrada ← W_out · error
PARA idx en indices_contexto:
W_in[idx] ← W_in[idx] - η × (grad_entrada / len(indices_contexto))
3. DEVOLVER W_in, W_outMuestreo Negativo (Entrenamiento Eficiente)
ALGORITMO: Skip-Gram con Muestreo Negativo (SGNS)
─────────────────────────────────────────────────────────────────
// El softmax completo sobre V palabras es costoso (O(V) por ejemplo de entrenamiento)
// El muestreo negativo lo aproxima con O(k+1) operaciones
ENTRADA:
corpus: Corpus de texto
V, d, ventana: Como antes
k: Número de muestras negativas (típicamente 5-20)
tasa_aprendizaje: η
PROCEDIMIENTO:
1. // Construir distribución de ruido para muestreo negativo
// P(w) ∝ conteo(w)^0.75 (submuestrear palabras frecuentes)
conteos_palabras ← contar ocurrencias de cada palabra
dist_ruido ← (conteos_palabras ** 0.75) / suma(conteos_palabras ** 0.75)
2. // Inicializar embeddings
W_objetivo ← uniforme_aleatorio(-0.5/d, 0.5/d, forma=(V, d))
W_contexto ← ceros(V, d)
3. // Bucle de entrenamiento
PARA cada oración en corpus:
PARA cada par (objetivo, contexto) de la oración:
idx_objetivo ← palabra_a_idx[objetivo]
idx_contexto ← palabra_a_idx[contexto]
// Muestrear k palabras negativas
muestras_neg ← []
MIENTRAS len(muestras_neg) < k:
idx_neg ← muestrear de dist_ruido
SI idx_neg ≠ idx_contexto:
muestras_neg.añadir(idx_neg)
// Obtener vectores
v_objetivo ← W_objetivo[idx_objetivo]
v_contexto ← W_contexto[idx_contexto]
// ===== EJEMPLO POSITIVO =====
// Maximizar log σ(v_objetivo · v_contexto)
puntuación_pos ← punto(v_objetivo, v_contexto)
sigmoid_pos ← σ(puntuación_pos)
// Gradientes para par positivo
grad_objetivo ← (sigmoid_pos - 1) × v_contexto
grad_contexto ← (sigmoid_pos - 1) × v_objetivo
// ===== EJEMPLOS NEGATIVOS =====
// Maximizar log σ(-v_objetivo · v_negativo)
PARA idx_neg en muestras_neg:
v_neg ← W_contexto[idx_neg]
puntuación_neg ← punto(v_objetivo, v_neg)
sigmoid_neg ← σ(puntuación_neg)
// Gradientes para par negativo
grad_objetivo += sigmoid_neg × v_neg
W_contexto[idx_neg] -= η × sigmoid_neg × v_objetivo
// Actualizar embeddings
W_objetivo[idx_objetivo] -= η × grad_objetivo
W_contexto[idx_contexto] -= η × grad_contexto
4. // Embeddings finales: a menudo promediar o concatenar W_objetivo y W_contexto
DEVOLVER W_objetivo // o (W_objetivo + W_contexto) / 2Submuestreo de Palabras Frecuentes
ALGORITMO: Submuestreo
─────────────────────────────────────────────────────────────────
// Palabras muy frecuentes como "el", "un", "es" proveen menos información
// y aparecen en demasiados contextos. Submuestrearlas.
ENTRADA:
oración: Lista de palabras
freqs_palabras: Frecuencia de cada palabra en el corpus
umbral: t (típicamente 10⁻⁵)
SALIDA:
oración_submuestreada: Oración con algunas palabras frecuentes eliminadas
PROCEDIMIENTO:
oración_submuestreada ← []
PARA palabra en oración:
freq ← freqs_palabras[palabra]
// Probabilidad de mantener la palabra
// Mayor frecuencia → menor probabilidad de mantener
p_mantener ← (√(freq / t) + 1) × (t / freq)
p_mantener ← min(p_mantener, 1.0)
SI aleatorio() < p_mantener:
oración_submuestreada.añadir(palabra)
DEVOLVER oración_submuestreadaSoftmax Jerárquico (Alternativa al Muestreo Negativo)
ALGORITMO: Softmax Jerárquico
─────────────────────────────────────────────────────────────────
// Usa un árbol binario (codificación Huffman) para reducir softmax de O(V) a O(log V)
ESTRUCTURA:
- Construir árbol Huffman desde frecuencias de palabras
- Cada palabra es una hoja
- Camino desde raíz a palabra = secuencia de decisiones binarias
- Cada nodo interno tiene un vector de parámetros
PREDICCIÓN:
P(palabra | contexto) = ∏ σ(±n_i · v_contexto)
donde el camino desde la raíz a la palabra pasa por nodos n₁, n₂, ..., nₗ
y ± depende de si vamos a la izquierda (-) o derecha (+) en cada nodo
COMPLEJIDAD:
O(log V) por ejemplo de entrenamiento (vs O(V) para softmax completo)
COMPROMISO:
- Más complejo de implementar
- Funciona bien para palabras frecuentes
- El muestreo negativo a menudo se prefiere en la prácticaFundamento Matemático
Objetivo Skip-Gram
Para un corpus de palabras $w_1, w_2, \ldots, w_T$:
$$J(\theta) = \frac{1}{T} \sum_{t=1}^{T} \sum_{-c \leq j \leq c, j \neq 0} \log P(w_{t+j} | w_t)$$donde $c$ es el tamaño de la ventana de contexto.
Probabilidad Softmax
$$P(w_O | w_I) = \frac{\exp(v'_{w_O} \cdot v_{w_I})}{\sum_{w=1}^{V} \exp(v'_w \cdot v_{w_I})}$$Objetivo del Muestreo Negativo
En lugar del softmax completo, maximizar:
$$\log \sigma(v'_{w_O} \cdot v_{w_I}) + \sum_{i=1}^{k} \mathbb{E}_{w_i \sim P_n(w)} [\log \sigma(-v'_{w_i} \cdot v_{w_I})]$$donde $P_n(w) \propto \text{conteo}(w)^{0.75}$ es la distribución de ruido.
Análisis de Complejidad
| Variante | Tiempo por Ejemplo | Espacio |
|---|---|---|
| Softmax Completo | $O(V \cdot d)$ | $O(V \cdot d)$ |
| Muestreo Negativo | $O((k+1) \cdot d)$ | $O(V \cdot d)$ |
| Softmax Jerárquico | $O(\log V \cdot d)$ | $O(V \cdot d)$ |
Donde $V$ = tamaño del vocabulario, $d$ = dimensión del embedding, $k$ = muestras negativas.
Analogías de Palabras
ANALOGÍAS DE PALABRAS: El Famoso Resultado
─────────────────────────────────────────────────────────────────
Los embeddings aprendidos capturan relaciones semánticas:
vec("rey") - vec("hombre") + vec("mujer") ≈ vec("reina")
vec("París") - vec("Francia") + vec("Alemania") ≈ vec("Berlín")
vec("caminando") - vec("caminar") + vec("nadar") ≈ vec("nadando")
RESOLVER ANALOGÍAS:
Dado: a es a b como c es a ?
Encontrar: argmax_d similitud_coseno(vec(b) - vec(a) + vec(c), vec(d))
donde d ∉ {a, b, c}Puntos Clave
Hipótesis Distribucional: Palabras que aparecen en contextos similares tienen significados similares—Word2Vec operacionaliza esto.
Denso vs Disperso: Word2Vec produce vectores densos de 100-300 dimensiones vs dispersos one-hot (V dimensiones).
Aprendizaje por Transferencia: Los embeddings pre-entrenados pueden mejorar el rendimiento en tareas de PLN posteriores.
Muestreo Negativo: Un truco computacional brillante que hace el entrenamiento manejable en vocabularios grandes.
Submuestreo: Reducir las ocurrencias de palabras frecuentes mejora la calidad de los embeddings para palabras raras.
Estructura Lineal: Las relaciones semánticas se capturan como translaciones lineales en el espacio vectorial.
Referencias
- Mikolov, T., Chen, K., Corrado, G., & Dean, J. (2013). Efficient Estimation of Word Representations in Vector Space. ICLR Workshop. arXiv:1301.3781 🇬🇧
- Mikolov, T., Sutskever, I., Chen, K., Corrado, G., & Dean, J. (2013). Distributed Representations of Words and Phrases and their Compositionality. NeurIPS. 🇬🇧
- Goldberg, Y., & Levy, O. (2014). word2vec Explained: Deriving Mikolov et al.'s Negative-Sampling Word-Embedding Method. arXiv:1402.3722 🇬🇧
- Rong, X. (2014). word2vec Parameter Learning Explained. arXiv:1411.2738 🇬🇧
- Jurafsky, D., & Martin, J. H. (2023). Speech and Language Processing, Capítulo 6. https://web.stanford.edu/~jurafsky/slp3/ 🇬🇧