latentSource

Visualizando word embeddings

Explora la matemática detrás del lenguaje: observa cómo 'King - Man + Woman = Queen' se representa visualmente en un espacio vectorial.

·5 min read
Compartir
Visualizando word embeddings

Visualizando word embeddings

Antes de 2013, la mayoría de los sistemas de NLP trataban las palabras como símbolos atómicos. En una representación one-hot, la palabra "dog" estaba tan lejos de "cat" como de "quantum". Los word embeddings rompieron con eso. Permiten aprender vectores densos donde las palabras con significados similares terminan cerca en un espacio geométrico, y esa idea sigue siendo la base de cada modelo de lenguaje con el que trabajo hoy en día.

Orígenes: Word2Vec y GloVe

Word2Vec (2013), de Tomas Mikolov, se lanzó con dos objetivos de entrenamiento: Skip-gram y CBOW. Skip-gram predice el contexto circundante dada una palabra objetivo. CBOW hace lo contrario: predice el objetivo a partir de su contexto. Ambos producen una matriz de pesos donde cada fila es un vector de palabra, usualmente de 100 a 300 dimensiones.

El truco es simple. Las palabras que aparecen en contextos similares terminan con vectores similares. "Dog" y "cat" aparecen junto a "pet", "food" y "vet", por lo que se desplazan juntas en el espacio vectorial. Sin etiquetas ni anotaciones, solo texto puro y una ventana deslizante (sliding window).

GloVe (Pennington et al., 2014) tomó un camino diferente. En lugar de ventanas de contexto local, factoriza la matriz de co-ocurrencia global de palabras. El objetivo: aprender vectores cuyo producto punto sea igual al logaritmo de la frecuencia con la que dos palabras aparecen juntas. GloVe captura mejor las estadísticas globales. Word2Vec es más fuerte en patrones sintácticos locales. En la práctica, he usado ambos y son aproximadamente intercambiables para tareas downstream.

Qué capturan los embeddings

El truco famoso es la aritmética de vectores. "king - man + woman = queen" funciona porque el modelo ha aprendido implícitamente una dirección de "género" en el espacio. Si restas "man" a "king", eliminas el componente masculino, dejando algo como "realeza". Al sumar "woman", devuelves el género con un valor diferente.

El género no es el único eje. También se ven cosas como:

  • Paris - France + Italy = Rome (capital de país)
  • bigger - big + small = smaller (forma comparativa)
  • walking - walked + swam = swimming (tiempo verbal)

Esto surge de los patrones de co-ocurrencia. Nadie supervisa nada de esto. El espacio termina codificando relaciones como direcciones, no solo como ubicaciones.

Para comparar embeddings, la similitud de coseno (cosine similarity) es el estándar. Dos vectores que apuntan en la misma dirección obtienen una puntuación cercana a 1, independientemente de su magnitud. Eso coincide con la intuición: lo que importa es el patrón de asociaciones, no el tamaño absoluto del vector.

Reducción de dimensionalidad para visualización

Los vectores de embedding viven en 100 a 300 dimensiones. Para graficarlos, necesitas proyectarlos a 2 o 3 dimensiones manteniendo suficiente estructura para que sean útiles. Tres técnicas realizan la mayor parte del trabajo.

PCA encuentra las direcciones de varianza máxima y las proyecta en las dos o tres principales. Es rápido, determinista y lineal. Pierde la estructura no lineal, pero es un buen primer paso y es útil cuando quieres saber cuáles son realmente los ejes principales de variación.

t-SNE preserva los vecindarios locales. Los puntos que están cerca en el espacio de alta dimensión se mantienen cerca en la proyección. Es excelente para revelar clusters, pero distorsiona las distancias globales. El parámetro de perplejidad (perplexity) equilibra la estructura local frente a la global, y los resultados pueden variar entre ejecuciones.

La proyección no es una transformación de un solo paso; es una optimización que se ejecuta durante cientos de iteraciones. Este es un descenso de gradiente real de t-SNE sobre 90 vectores de palabras sintéticos de 50 dimensiones (cinco grupos semánticos). Presiona play y observa cómo los clusters emergen de una mancha aleatoria:

Invalid viz:anim block: expected JSON like {"name":"my_frames","height":360}.

Cada punto es un vector de 50-D; t-SNE minimiza el desajuste entre las probabilidades de vecindad en 50-D y en 2-D. Comienza como una mancha aleatoria en el centro, y la fase de "early exaggeration" (primeras ~60 iteraciones) separa los grupos antes de que el diseño se relaje en cinco clusters limpios. Los colores son los grupos reales; el algoritmo nunca los ve; recupera la estructura solo a partir de las distancias.

UMAP es más reciente y es lo que suelo utilizar ahora. Mantiene mejor tanto la estructura local como la global que t-SNE, se ejecuta más rápido en datasets grandes y se basa en una base matemática más sólida (geometría de Riemann, topología difusa). Los dos parámetros que importan son n_neighbors (equilibrio local vs. global) y min_dist (qué tan apretados se agrupan los puntos).

Interpretando clusters

Una vez que proyectas los embeddings con t-SNE o UMAP, los clusters aparecen por sí solos. Típicamente verás clusters semánticos (animales juntos, países juntos, profesiones juntas), clusters sintácticos (verbos en pasado en su propia región, plurales separados de singulares) y una estructura jerárquica donde "dog" se sitúa más cerca de "cat" que de "eagle", y "eagle" más cerca de "sparrow".

Dicho esto, no hay que sobreinterpretar la imagen. t-SNE y UMAP son proyecciones no lineales que pueden inventar artefactos visuales. Dos clusters que parecen adyacentes en 2D podrían no ser vecinos en absoluto en el espacio original. Cada vez que veo algo que parece significativo en un gráfico, vuelvo y lo compruebo con la similitud de coseno en el espacio dimensional completo antes de confiar en ello.

Embeddings modernos: de palabras a oraciones

Word2Vec y GloVe asignan a cada palabra un único vector independientemente del contexto. "Bank" recibe un vector ya sea que se trate de una institución financiera o de la orilla de un río. Ese es un techo difícil de superar.

BERT (Devlin et al., 2018) produce embeddings contextuales. Cada aparición de una palabra recibe un vector diferente según su contexto. "Bank" en "river bank" se ve diferente de "bank" en "bank account". BERT utiliza el codificador Transformer (12 capas en el modelo base), y cada capa te ofrece un embedding diferente. Concatenar o promediar las últimas cuatro capas tiende a funcionar mejor para la mayoría de las tareas downstream.

Sentence-BERT (Reimers y Gurevych, 2019) ajusta (fine-tunes) BERT con una configuración siamesa para que la salida sea un embedding a nivel de oración utilizable. El BERT original no es ideal para la similitud de oraciones porque el embedding del token [CLS] no captura de manera confiable el significado de toda la oración. Sentence-BERT soluciona eso y hace que sea económico comparar miles de oraciones con similitud de coseno, que es lo que se busca para el retrieval.

Ejemplo práctico: explorando embeddings con Python

Aquí hay un ejemplo completo que carga embeddings preentrenados y los grafica:

from sklearn.decomposition import PCA from sklearn.manifold import TSNE import matplotlib.pyplot as plt import numpy as np from gensim.models import KeyedVectors # Load pretrained Word2Vec (Google News, 300d) model = KeyedVectors.load_word2vec_format( 'GoogleNews-vectors-negative300.bin', binary=True ) # Select words to visualize categories = {{ 'animals': ['dog', 'cat', 'horse', 'eagle', 'shark', 'whale'], 'countries': ['france', 'germany', 'japan', 'brazil', 'egypt', 'canada'], 'tech': ['python', 'javascript', 'algorithm', 'database', 'server', 'cloud'], }} words = [w for group in categories.values() for w in group] vectors = np.array([model[w] for w in words]) # Reduce to 2D with UMAP (or t-SNE as fallback) try: import umap reducer = umap.UMAP(n_neighbors=5, min_dist=0.3, random_state=42) coords = reducer.fit_transform(vectors) except ImportError: reducer = TSNE(n_components=2, perplexity=5, random_state=42) coords = reducer.fit_transform(vectors) # Plot with color coding colors = {{'animals': 'green', 'countries': 'blue', 'tech': 'red'}} fig, ax = plt.subplots(figsize=(10, 8)) idx = 0 for category, word_list in categories.items(): n = len(word_list) ax.scatter(coords[idx:idx+n, 0], coords[idx:idx+n, 1], c=colors[category], label=category, s=100) for i, word in enumerate(word_list): ax.annotate(word, (coords[idx+i, 0], coords[idx+i, 1]), fontsize=9, ha='center', va='bottom') idx += n ax.legend() ax.set_title('Word Embeddings Projected to 2D') plt.tight_layout() plt.savefig('embeddings_visualization.png', dpi=150)

Incluso con esta pequeña muestra, las categorías se separan limpiamente. Los animales se agrupan, los países se agrupan y los términos tecnológicos también.

Conclusiones prácticas

Los embeddings no son solo un paso de preprocesamiento. Son una lente para ver lo que el modelo realmente ha aprendido sobre el lenguaje. Graficarlos revela sesgos (asociaciones de género con ciertas profesiones), brechas de dominio (un modelo médico ajustado agrupa nombres de medicamentos de manera diferente al modelo base) y problemas de calidad de datos (si "asdf" aparece junto a palabras reales, tus datos de entrenamiento están sucios).

Para sistemas en producción, el modelo de embedding que elijas importa más que la visualización. Sentence-BERT y sus sucesores (E5, GTE, BGE) son los que utilizo para búsqueda semántica y retrieval. Los word embeddings a nivel de palabra todavía tienen su valor para tareas a nivel de token, como el reconocimiento de entidades nombradas, y para indagar en la geometría del significado cuando realmente quieres entender qué piensa el modelo.