La Revolución Vectorial

S3: Visualización de Datos de Alta Dimensión — t-SNE y PCA

Prof. Francisco Suárez

Universidad Católica Boliviana

2026-03-07

Agenda de Hoy

Primera Parte

  1. 🔙 Repaso: Word embeddings de 300 dimensiones
  2. 🤔 La maldición de la dimensionalidad
  3. 📉 PCA: Análisis de Componentes Principales

Segunda Parte

  1. 🗺️ t-SNE: Preservando la estructura local
  2. 🆚 PCA vs. t-SNE: ¿Cuándo usar cada uno?
  3. 🛠️ Visualizaciones interactivas de embeddings

Bloque 1: El Problema de Visualizar

300 Dimensiones vs. 2 Ojos

El problema

Nuestros word embeddings viven en \(\mathbb{R}^{300}\):

\[\vec{v}_{\text{gato}} = [0.23, -0.11, 0.87, \ldots, 0.42] \in \mathbb{R}^{300}\]

Pero los humanos solo podemos visualizar 2D o 3D.

La pregunta

¿Cómo proyectar 300 dimensiones a 2 dimensiones sin perder la estructura importante?

Lo que queremos preservar

  • Palabras similares deben quedar cerca
  • Palabras diferentes deben quedar lejos
  • Clusters de categorías deben ser visibles
  • Relaciones (analogías) deben ser reconocibles

Lo que inevitablemente perdemos

  • Información precisa de distancia
  • Algunas relaciones complejas
  • Estructura geométrica exacta

La Maldición de la Dimensionalidad

En espacios de alta dimensión, nuestra intuición geométrica falla:

Fenómenos contraintuitivos

  1. Casi todo está “lejos”: en \(\mathbb{R}^{300}\), los puntos aleatorios tienden a estar a distancias similares entre sí

  2. El volumen se concentra en la superficie: la mayoría del volumen de una hiperesfera está cerca de su borde

  3. La similitud coseno es más útil que la distancia euclidiana en alta dimensión

Ejemplo numérico

Code
import numpy as np

np.random.seed(42)

# Distancias entre puntos aleatorios en diferentes dimensiones
for d in [2, 10, 100, 300]:
    puntos = np.random.randn(1000, d)
    distancias = np.linalg.norm(puntos[0] - puntos[1:], axis=1)
    print(f"d={d:3d}: media={distancias.mean():.2f}, "
          f"std={distancias.std():.2f}, "
          f"ratio={distancias.std()/distancias.mean():.3f}")
d=  2: media=1.32, std=0.69, ratio=0.521
d= 10: media=4.00, std=0.83, ratio=0.209
d=100: media=13.42, std=0.83, ratio=0.062
d=300: media=23.66, std=0.86, ratio=0.036

Observación clave

A medida que la dimensión crece, la variación relativa de las distancias disminuye → todos los puntos parecen estar “igual de lejos”. Por eso necesitamos técnicas especializadas.

Reducción de Dimensionalidad: Panorama General

Code
flowchart TB
    subgraph Lineales ["Métodos Lineales"]
        PCA["PCA<br>Máxima varianza"]
        SVD["SVD<br>Factorización de matrices"]
    end

    subgraph NoLineales ["Métodos No Lineales"]
        TSNE["t-SNE<br>Estructura local"]
        UMAP["UMAP<br>Estructura + velocidad"]
    end

    HD["Datos en Alta Dimensión<br>ℝ^300"] --> PCA
    HD --> SVD
    HD --> TSNE
    HD --> UMAP

    PCA --> VIS["Visualización en 2D/3D"]
    SVD --> VIS
    TSNE --> VIS
    UMAP --> VIS

flowchart TB
    subgraph Lineales ["Métodos Lineales"]
        PCA["PCA<br>Máxima varianza"]
        SVD["SVD<br>Factorización de matrices"]
    end

    subgraph NoLineales ["Métodos No Lineales"]
        TSNE["t-SNE<br>Estructura local"]
        UMAP["UMAP<br>Estructura + velocidad"]
    end

    HD["Datos en Alta Dimensión<br>ℝ^300"] --> PCA
    HD --> SVD
    HD --> TSNE
    HD --> UMAP

    PCA --> VIS["Visualización en 2D/3D"]
    SVD --> VIS
    TSNE --> VIS
    UMAP --> VIS

Hoy nos enfocaremos en PCA y t-SNE, los dos más utilizados en NLP.

Bloque 2: PCA — Análisis de Componentes Principales

PCA: La Idea Intuitiva

PCA encuentra las direcciones de máxima varianza en los datos y proyecta sobre ellas.

Analogía

Imagina una nube de puntos alargada en 3D (como un cigarro). PCA encuentra:

  1. PC1: el eje largo del cigarro (máxima varianza)
  2. PC2: el segundo eje más largo (perpendicular a PC1)
  3. PC3: el eje más corto (menor varianza)

Si proyectas sobre PC1 y PC2, capturas la mayor parte de la información.

Formalmente

  1. Centrar los datos: \(\tilde{X} = X - \bar{X}\)
  2. Calcular la matriz de covarianza: \(C = \frac{1}{n}\tilde{X}^T\tilde{X}\)
  3. Encontrar los eigenvectores de \(C\)
  4. Los eigenvectores con los eigenvalores más grandes son los componentes principales
  5. Proyectar: \(Z = \tilde{X} W_k\) donde \(W_k\) son los \(k\) eigenvectores principales

PCA: Paso a Paso Visual

Code
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

# Crear datos 2D con correlación (para demostración)
np.random.seed(42)
n = 200
x = np.random.randn(n)
y = 0.7 * x + 0.3 * np.random.randn(n)  # Correlación con ruido
datos_2d = np.column_stack([x, y])

# Aplicar PCA
pca = PCA(n_components=2)
pca.fit(datos_2d)

fig, axes = plt.subplots(1, 2, figsize=(12, 4))

# Gráfico 1: Datos originales con componentes principales
ax = axes[0]
ax.scatter(datos_2d[:, 0], datos_2d[:, 1], alpha=0.4, s=20, color='#0077b6')
origin = pca.mean_

for i, (comp, var) in enumerate(zip(pca.components_, pca.explained_variance_)):
    ax.annotate('', xy=origin + comp * np.sqrt(var) * 2,
                xytext=origin,
                arrowprops=dict(arrowstyle='->', color=['#e63946', '#2a9d8f'][i],
                               lw=3))
    ax.text(origin[0] + comp[0] * np.sqrt(var) * 2.3,
            origin[1] + comp[1] * np.sqrt(var) * 2.3,
            f'PC{i+1} ({pca.explained_variance_ratio_[i]:.1%})',
            fontsize=11, fontweight='bold',
            color=['#e63946', '#2a9d8f'][i])

ax.set_xlabel('x', fontsize=11)
ax.set_ylabel('y', fontsize=11)
ax.set_title('Datos originales + Componentes Principales', fontsize=12)
ax.set_aspect('equal')
ax.grid(True, alpha=0.3)

# Gráfico 2: Datos proyectados en PC1
datos_pca = pca.transform(datos_2d)
ax = axes[1]
ax.scatter(datos_pca[:, 0], np.zeros(n), alpha=0.4, s=20, color='#e63946')
ax.set_xlabel('PC1', fontsize=11)
ax.set_title(f'Proyección sobre PC1 ({pca.explained_variance_ratio_[0]:.1%} varianza)', fontsize=12)
ax.set_yticks([])
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

PCA Aplicado a Word Embeddings

Code
from gensim.models import Word2Vec

# Corpus con más estructura semántica
corpus = [
    ["el", "gato", "come", "pescado", "fresco"],
    ["el", "perro", "come", "carne", "roja"],
    ["mi", "gato", "duerme", "en", "el", "sofá"],
    ["mi", "perro", "duerme", "en", "la", "cama"],
    ["el", "gato", "negro", "persigue", "al", "ratón"],
    ["el", "perro", "grande", "persigue", "al", "gato"],
    ["el", "niño", "come", "una", "manzana", "roja"],
    ["la", "niña", "come", "una", "naranja", "fresca"],
    ["el", "gato", "juega", "con", "la", "pelota"],
    ["el", "perro", "juega", "con", "un", "hueso"],
    ["el", "gato", "bebe", "leche", "fresca"],
    ["el", "perro", "bebe", "agua", "fría"],
    ["un", "gato", "pequeño", "maúlla", "fuerte"],
    ["un", "perro", "pequeño", "ladra", "fuerte"],
    ["la", "niña", "acaricia", "al", "gato", "negro"],
    ["el", "niño", "acaricia", "al", "perro", "grande"],
    ["el", "pez", "nada", "en", "el", "agua"],
    ["el", "pájaro", "vuela", "en", "el", "cielo"],
    ["la", "manzana", "roja", "es", "dulce"],
    ["la", "naranja", "fresca", "es", "dulce"],
]

modelo = Word2Vec(sentences=corpus, vector_size=50, window=3,
                  min_count=1, sg=1, negative=5, epochs=300, seed=42)

# Seleccionar palabras de interés (por categorías)
categorias = {
    "🐾 Animales": ["gato", "perro", "ratón", "pez", "pájaro"],
    "🍎 Comida": ["pescado", "carne", "manzana", "naranja", "leche"],
    "👤 Personas": ["niño", "niña"],
    "🏃 Acciones": ["come", "bebe", "duerme", "juega", "persigue"],
    "📏 Adjetivos": ["grande", "pequeño", "negro", "roja", "fresca"],
}

# Obtener vectores
palabras = []
vectores = []
colores = []
color_map = {'🐾 Animales': '#e63946', '🍎 Comida': '#2a9d8f',
             '👤 Personas': '#e9c46a', '🏃 Acciones': '#264653',
             '📏 Adjetivos': '#f4a261'}

for cat, words in categorias.items():
    for w in words:
        if w in modelo.wv:
            palabras.append(w)
            vectores.append(modelo.wv[w])
            colores.append(color_map[cat])

vectores = np.array(vectores)
print(f"Proyectando {len(palabras)} palabras de {vectores.shape[1]}D a 2D con PCA...")
Proyectando 22 palabras de 50D a 2D con PCA...

PCA: Visualización de Embeddings

Code
from sklearn.decomposition import PCA

pca = PCA(n_components=2)
coords_pca = pca.fit_transform(vectores)

fig, ax = plt.subplots(figsize=(10, 6))

# Graficar puntos por categoría
for cat, color in color_map.items():
    mask = [c == color for c in colores]
    ax.scatter(coords_pca[mask, 0], coords_pca[mask, 1],
               c=color, label=cat, s=100, alpha=0.8, edgecolors='white', linewidth=0.5)

# Etiquetar cada punto
for i, palabra in enumerate(palabras):
    ax.annotate(palabra, (coords_pca[i, 0], coords_pca[i, 1]),
                fontsize=9, fontweight='bold',
                xytext=(5, 5), textcoords='offset points')

ax.set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.1%} varianza)', fontsize=12)
ax.set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.1%} varianza)', fontsize=12)
ax.set_title('Word Embeddings — Proyección PCA', fontsize=14)
ax.legend(fontsize=10, loc='best')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Interpretación

Con un corpus pequeño los clusters no serán perfectos, pero se puede observar cierta agrupación por categoría. Con embeddings pre-entrenados en corpus grandes, la separación es mucho más clara.

Varianza Explicada

Una ventaja de PCA: podemos saber cuánta información preservamos:

Code
pca_full = PCA(n_components=min(len(palabras), vectores.shape[1]))
pca_full.fit(vectores)

varianza_acum = np.cumsum(pca_full.explained_variance_ratio_)

fig, axes = plt.subplots(1, 2, figsize=(12, 4))

# Varianza por componente
ax = axes[0]
n_show = min(20, len(pca_full.explained_variance_ratio_))
ax.bar(range(1, n_show + 1), pca_full.explained_variance_ratio_[:n_show],
       color='#0077b6', alpha=0.7)
ax.set_xlabel('Componente Principal', fontsize=11)
ax.set_ylabel('Varianza Explicada', fontsize=11)
ax.set_title('Varianza por Componente', fontsize=12)
ax.grid(True, alpha=0.3, axis='y')

# Varianza acumulada
ax = axes[1]
ax.plot(range(1, len(varianza_acum) + 1), varianza_acum,
        'o-', color='#e63946', markersize=4)
ax.axhline(y=0.9, color='gray', linestyle='--', alpha=0.7, label='90%')
ax.axhline(y=0.95, color='gray', linestyle=':', alpha=0.7, label='95%')

# Encontrar componentes para 90%
n_90 = np.argmax(varianza_acum >= 0.9) + 1
ax.axvline(x=n_90, color='#2a9d8f', linestyle='--', alpha=0.7)
ax.text(n_90 + 0.5, 0.85, f'{n_90} PCs para 90%', fontsize=10, color='#2a9d8f')

ax.set_xlabel('Número de Componentes', fontsize=11)
ax.set_ylabel('Varianza Acumulada', fontsize=11)
ax.set_title('Varianza Acumulada', fontsize=12)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print(f"Con 2 componentes: {varianza_acum[1]:.1%} de varianza explicada")
print(f"Componentes para 90%: {n_90}")
Con 2 componentes: 87.1% de varianza explicada
Componentes para 90%: 4

Limitaciones de PCA

Problemas

  1. Solo captura relaciones lineales — No puede “desdoblar” variedades (manifolds) no lineales
  2. Optimiza varianza global — Puede sacrificar estructura local por preservar distancias grandes
  3. Sensible a outliers — Un punto muy lejano puede distorsionar toda la proyección

El Problema del “Swiss Roll”

Datos en 3D enrollados como un brazo de gitano. PCA los aplasta, t-SNE los desdobla:

Code
from sklearn.datasets import make_swiss_roll

X_swiss, color_swiss = make_swiss_roll(n_samples=1000, noise=0.5, random_state=42)

fig, axes = plt.subplots(1, 2, figsize=(10, 4))

# PCA
pca_swiss = PCA(n_components=2)
X_pca_swiss = pca_swiss.fit_transform(X_swiss)
axes[0].scatter(X_pca_swiss[:, 0], X_pca_swiss[:, 1],
                c=color_swiss, cmap='Spectral', s=10, alpha=0.6)
axes[0].set_title('PCA del Swiss Roll', fontsize=12)
axes[0].set_xlabel('PC1')
axes[0].set_ylabel('PC2')

# Lo que querríamos
from sklearn.manifold import TSNE
tsne_swiss = TSNE(n_components=2, perplexity=30, random_state=42)
X_tsne_swiss = tsne_swiss.fit_transform(X_swiss)
axes[1].scatter(X_tsne_swiss[:, 0], X_tsne_swiss[:, 1],
                c=color_swiss, cmap='Spectral', s=10, alpha=0.6)
axes[1].set_title('t-SNE del Swiss Roll', fontsize=12)
axes[1].set_xlabel('t-SNE 1')
axes[1].set_ylabel('t-SNE 2')

plt.tight_layout()
plt.show()

Conclusión

PCA es bueno para una primera exploración, pero para visualizar clusters no lineales necesitamos algo más potente → t-SNE.

Bloque 3: t-SNE

t-SNE: La Idea Intuitiva

t-SNE = t-distributed Stochastic Neighbor Embedding (van der Maaten & Hinton, 2008)

Filosofía

“Si dos puntos son vecinos cercanos en alta dimensión, que lo sean también en baja dimensión.”

t-SNE se enfoca en preservar la estructura local (vecindades), no la global.

Intuición

  1. En \(\mathbb{R}^{300}\): calcular qué tan “cercano” es cada par de puntos (distribución de probabilidad)
  2. En \(\mathbb{R}^2\): colocar puntos aleatoriamente
  3. Mover los puntos en 2D hasta que las vecindades se parezcan a las de 300D
  4. Minimizar la diferencia (divergencia KL)

t-SNE: La Matemática

Paso 1: Probabilidades en alta dimensión

Para cada par \((i, j)\), definir la probabilidad de que \(j\) sea “vecino” de \(i\):

\[p_{j|i} = \frac{\exp\left(-\frac{||\vec{x}_i - \vec{x}_j||^2}{2\sigma_i^2}\right)}{\sum_{k \neq i} \exp\left(-\frac{||\vec{x}_i - \vec{x}_k||^2}{2\sigma_i^2}\right)}\]

. . .

Se simetriza: \(p_{ij} = \frac{p_{j|i} + p_{i|j}}{2n}\)

. . .

Paso 2: Probabilidades en baja dimensión

Usar una distribución t de Student (con 1 grado de libertad = distribución de Cauchy):

\[q_{ij} = \frac{\left(1 + ||\vec{y}_i - \vec{y}_j||^2\right)^{-1}}{\sum_{k \neq l}\left(1 + ||\vec{y}_k - \vec{y}_l||^2\right)^{-1}}\]

Paso 3: Minimizar la divergencia KL

\[\text{KL}(P || Q) = \sum_{i \neq j} p_{ij} \log \frac{p_{ij}}{q_{ij}}\]

¿Por Qué la Distribución t?

El problema del “crowding”

Al proyectar de alta a baja dimensión, hay menos espacio disponible.

  • En \(\mathbb{R}^{300}\): un punto puede tener muchos vecinos equidistantes.
  • En \(\mathbb{R}^2\): no caben todos a la misma distancia → se “apiñan” (crowding).

Solución: La distribución t tiene colas más pesadas que la gaussiana → más espacio para separar clusters.

Gaussiana vs. t de Student

Code
x = np.linspace(-5, 5, 500)
gauss = np.exp(-x**2 / 2) / np.sqrt(2 * np.pi)
t_dist = 1 / (np.pi * (1 + x**2))  # Cauchy = t con df=1

plt.figure(figsize=(10, 4))
plt.plot(x, gauss, label='Gaussiana', linewidth=2, color='#0077b6')
plt.plot(x, t_dist, label='t de Student (df=1)', linewidth=2,
         color='#e63946', linestyle='--')
plt.fill_between(x, t_dist, alpha=0.1, color='#e63946')
plt.xlabel('Distancia', fontsize=11)
plt.ylabel('Densidad', fontsize=11)
plt.title('Colas más pesadas → más espacio para separar clusters', fontsize=12)
plt.legend(fontsize=11)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Consecuencia de las colas pesadas

Puntos moderadamente distantes en alta dimensión pueden quedar más separados en 2D. Esto evita el apiñamiento y produce clusters más claros.

El Hiperparámetro Clave: Perplexity

La perplejidad en t-SNE controla cuántos vecinos “efectivos” considera cada punto:

Definición

\[\text{Perplexity} = 2^{H(P_i)}\]

donde \(H(P_i) = -\sum_j p_{j|i} \log_2 p_{j|i}\) es la entropía de la distribución condicional.

Valores típicos

Perplexity Efecto
5-10 Estructura muy local, clusters pequeños
30 Valor por defecto, buen balance ✅
50-100 Estructura más global

Efecto visual

Ver siguiente slide →

Efecto de la Perplejidad

Code
from sklearn.manifold import TSNE
from sklearn.datasets import make_blobs

X_blobs, y_blobs = make_blobs(n_samples=300, centers=4,
                               cluster_std=1.0, random_state=42)

fig, axes = plt.subplots(1, 3, figsize=(10, 3.5))
perplexities = [5, 30, 100]

for ax, perp in zip(axes, perplexities):
    tsne = TSNE(n_components=2, perplexity=perp, random_state=42, max_iter=1000)
    coords = tsne.fit_transform(X_blobs)
    ax.scatter(coords[:, 0], coords[:, 1], c=y_blobs,
               cmap='Set1', s=20, alpha=0.7)
    ax.set_title(f'Perplexity = {perp}', fontsize=12)
    ax.set_xticks([])
    ax.set_yticks([])

plt.suptitle('Efecto de la perplejidad en t-SNE', fontsize=13, y=1.02)
plt.tight_layout()
plt.show()

Regla práctica

perplexity\(\sqrt{n}\) donde \(n\) es el número de puntos. Para nuestros embeddings con ~30 palabras, perplexity=5-10 funciona bien.

t-SNE Aplicado a Word Embeddings

Code
from sklearn.manifold import TSNE

# Usar los mismos vectores que con PCA
tsne = TSNE(n_components=2, perplexity=5, random_state=42,
            max_iter=2000, learning_rate='auto', init='pca')
coords_tsne = tsne.fit_transform(vectores)

fig, axes = plt.subplots(1, 2, figsize=(10, 5))

# PCA
ax = axes[0]
for cat, color in color_map.items():
    mask = [c == color for c in colores]
    ax.scatter(coords_pca[mask, 0], coords_pca[mask, 1],
               c=color, label=cat, s=100, alpha=0.8, edgecolors='white', linewidth=0.5)
for i, palabra in enumerate(palabras):
    ax.annotate(palabra, (coords_pca[i, 0], coords_pca[i, 1]),
                fontsize=8, fontweight='bold', xytext=(5, 5), textcoords='offset points')
ax.set_title('PCA', fontsize=14)
ax.legend(fontsize=9, loc='best')
ax.grid(True, alpha=0.3)

# t-SNE
ax = axes[1]
for cat, color in color_map.items():
    mask = [c == color for c in colores]
    ax.scatter(coords_tsne[mask, 0], coords_tsne[mask, 1],
               c=color, label=cat, s=100, alpha=0.8, edgecolors='white', linewidth=0.5)
for i, palabra in enumerate(palabras):
    ax.annotate(palabra, (coords_tsne[i, 0], coords_tsne[i, 1]),
                fontsize=8, fontweight='bold', xytext=(5, 5), textcoords='offset points')
ax.set_title('t-SNE', fontsize=14)
ax.legend(fontsize=9, loc='best')
ax.grid(True, alpha=0.3)

plt.suptitle('Word Embeddings: PCA vs. t-SNE', fontsize=15, y=1.02)
plt.tight_layout()
plt.show()

Bloque 4: Errores Comunes al Interpretar t-SNE

Cuidado con t-SNE ⚠️

t-SNE produce visualizaciones hermosas, pero hay trampas:

❌ Lo que NO puedes concluir

  1. Las distancias entre clusters no son significativas
    • Que un cluster esté lejos de otro no dice nada sobre su “distancia real”
  2. El tamaño de los clusters no es significativo
    • t-SNE puede expandir clusters densos y comprimir clusters dispersos
  3. Es no determinístico
    • Diferentes ejecuciones dan mapas diferentes (depende de random_state)

✅ Lo que SÍ puedes concluir

  1. La estructura local es confiable
    • Si dos puntos están juntos, probablemente están cerca en alta dimensión
  2. La existencia de clusters
    • Si ves clusters separados, probablemente existen
  3. Membresía en clusters
    • Qué puntos pertenecen a qué cluster es confiable

Regla de oro

t-SNE te dice quién está con quién, pero NO te dice qué tan lejos están los grupos entre sí.

Efecto de Hiperparámetros

Code
fig, axes = plt.subplots(2, 3, figsize=(10, 6))
configs = [
    (5, 500), (5, 2000), (5, 5000),
    (8, 500), (8, 2000), (8, 5000),
]

for ax, (perp, iters) in zip(axes.flat, configs):
    tsne_test = TSNE(n_components=2, perplexity=perp, random_state=42,
                     max_iter=iters, learning_rate='auto', init='pca')
    coords = tsne_test.fit_transform(vectores)

    for cat, color in color_map.items():
        mask = [c == color for c in colores]
        ax.scatter(coords[mask, 0], coords[mask, 1],
                   c=color, s=60, alpha=0.8)
    for i, palabra in enumerate(palabras):
        ax.annotate(palabra, (coords[i, 0], coords[i, 1]),
                    fontsize=6, xytext=(3, 3), textcoords='offset points')

    ax.set_title(f'perp={perp}, iter={iters}', fontsize=11)
    ax.set_xticks([])
    ax.set_yticks([])

plt.suptitle('t-SNE: Efecto de perplejidad e iteraciones', fontsize=14, y=1.01)
plt.tight_layout()
plt.show()

Consejo práctico

Siempre prueba con varios valores de perplejidad y suficientes iteraciones (≥1000). Si dos ejecuciones dan resultados muy diferentes, no confíes demasiado en ninguna.

Bloque 5: PCA vs. t-SNE

Comparación Directa

Aspecto PCA t-SNE
Tipo Lineal No lineal
Preserva Varianza global Vecindades locales
Determinístico ✅ Sí ❌ No (aleatorio)
Velocidad Muy rápido \(O(nd^2)\) Lento \(O(n^2)\)
Escalabilidad ✅ Millones de puntos ⚠️ Miles de puntos
Inversa ✅ Puede reconstruir ❌ No tiene inversa
Distancias Significativas ❌ No significativas entre clusters
Varianza explicada ✅ Cuantificable ❌ No aplica
Hiperparámetros Ninguno (solo \(k\)) Perplexity, lr, max_iter
Mejor para Exploración inicial, preproceso Visualización de clusters

¿Cuándo usar cuál?

  • PCA primero: para entender cuántas dimensiones “importan” y como preproceso
  • t-SNE después: para visualizar clusters y vecindades en 2D
  • Combinación: PCA a 50 dims → t-SNE a 2 dims (¡más rápido y estable!)

Bonus: UMAP

UMAP (Uniform Manifold Approximation and Projection) es una alternativa moderna a t-SNE.

Ventajas sobre t-SNE

  • Más rápido: \(O(n^{1.14})\) vs. \(O(n^2)\)
  • Preserva estructura global además de local
  • Determinístico (con semilla fija)
  • Escalable: funciona con millones de puntos

Uso

# pip install umap-learn
from umap import UMAP

reducer = UMAP(n_components=2,
               n_neighbors=15,
               min_dist=0.1,
               random_state=42)
coords_umap = reducer.fit_transform(vectores)

En la práctica

UMAP está reemplazando a t-SNE en muchas aplicaciones. Si trabajas con datasets grandes (>10,000 puntos), UMAP es la mejor opción. Para este curso, t-SNE es suficiente.

Bloque 6: Flujo de Trabajo Completo

Pipeline de Visualización de Embeddings

Code
# Pipeline completo: Entrenar → Seleccionar → Reducir → Visualizar

from gensim.models import Word2Vec
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
from sklearn.preprocessing import StandardScaler

# 1. Ya tenemos el modelo entrenado (modelo)
# 2. Seleccionar palabras de interés
palabras_sel = [w for w in modelo.wv.index_to_key
                if modelo.wv.get_vecattr(w, "count") >= 1][:25]

# 3. Obtener vectores
vecs = np.array([modelo.wv[w] for w in palabras_sel])

# 4. Estandarizar (recomendado antes de PCA/t-SNE)
scaler = StandardScaler()
vecs_std = scaler.fit_transform(vecs)

# 5. PCA como preproceso (reducir a 20 dims antes de t-SNE)
n_pca = min(20, vecs_std.shape[0] - 1, vecs_std.shape[1])
pca_pre = PCA(n_components=n_pca)
vecs_pca = pca_pre.fit_transform(vecs_std)
print(f"PCA: {vecs_std.shape[1]}D → {n_pca}D "
      f"({sum(pca_pre.explained_variance_ratio_):.1%} varianza)")

# 6. t-SNE final
tsne_final = TSNE(n_components=2, perplexity=min(8, len(palabras_sel)//3),
                  random_state=42, max_iter=2000, learning_rate='auto', init='pca')
coords_final = tsne_final.fit_transform(vecs_pca)

# 7. Visualizar
fig, ax = plt.subplots(figsize=(10, 7))
ax.scatter(coords_final[:, 0], coords_final[:, 1],
           c='#0077b6', s=100, alpha=0.7, edgecolors='white', linewidth=0.5)

for i, w in enumerate(palabras_sel):
    ax.annotate(w, (coords_final[i, 0], coords_final[i, 1]),
                fontsize=9, fontweight='bold',
                xytext=(6, 6), textcoords='offset points',
                bbox=dict(boxstyle='round,pad=0.2', facecolor='lightyellow',
                          alpha=0.7, edgecolor='gray', linewidth=0.5))

ax.set_title('Pipeline completo: Word2Vec → PCA → t-SNE', fontsize=14)
ax.set_xlabel('t-SNE 1', fontsize=11)
ax.set_ylabel('t-SNE 2', fontsize=11)
ax.grid(True, alpha=0.2)
plt.tight_layout()
plt.show()
PCA: 50D → 20D (99.2% varianza)

Bloque 7: Resumen y Conexiones

Resumen de la Semana 4

S1: Word2Vec

  • CBOW y Skip-gram
  • Negative Sampling
  • Propiedades (analogías, similitud)

S2: GloVe y FastText

  • GloVe: co-ocurrencia global
  • FastText: subpalabras y OOV
  • Comparación de los tres modelos

S3: Visualización (hoy)

  • PCA: reducción lineal, varianza explicada
  • t-SNE: reducción no lineal, clusters locales
  • Advertencias: no sobreinterpretar distancias entre clusters
  • Pipeline: estandarizar → PCA → t-SNE

Próxima semana: Redes Neuronales

La Semana 5 marca el inicio de las redes neuronales para NLP: perceptrones, retropropagación y clasificación de texto con MLPs.

Fórmulas Clave de Hoy

Concepto Fórmula
PCA — Covarianza \(C = \frac{1}{n}\tilde{X}^T \tilde{X}\)
PCA — Proyección \(Z = \tilde{X} W_k\)
PCA — Varianza explicada \(\frac{\lambda_i}{\sum_j \lambda_j}\)
t-SNE — Similaridad HD \(p_{j \mid i} = \frac{\exp(-\|\vec{x}_i - \vec{x}_j\|^2 / 2\sigma_i^2)}{\sum_{k \neq i} \exp(-\|\vec{x}_i - \vec{x}_k\|^2 / 2\sigma_i^2)}\)
t-SNE — Similaridad LD \(q_{ij} = \frac{(1 + \|\vec{y}_i - \vec{y}_j\|^2)^{-1}}{\sum_{k \neq l}(1 + \|\vec{y}_k - \vec{y}_l\|^2)^{-1}}\)
t-SNE — Objetivo \(\text{KL}(P \| Q) = \sum_{i \neq j} p_{ij} \log \frac{p_{ij}}{q_{ij}}\)

Para Reflexionar 🤔

Preguntas de discusión

  1. Si PCA explica el 95% de la varianza con 10 componentes, ¿significa que los otros 290 son “inútiles”?

  2. ¿Por qué no deberíamos usar t-SNE como preprocesamiento para un clasificador?

  3. Si dos ejecuciones de t-SNE producen mapas muy diferentes, ¿qué nos dice eso sobre los datos?

Lecturas recomendadas

  1. Van der Maaten & Hinton (2008). Visualizing Data using t-SNE. JMLR

  2. How to Use t-SNE Effectively — Distill.pub (interactivo, excelente)

  3. McInnes et al. (2018). UMAP: Uniform Manifold Approximation and Projection. arXiv:1802.03426

  4. Jurafsky & Martin, Ch. 6 — Visualización de embeddings

📝 Tarea 1: Analizador de Sentimiento

Esta semana se entrega la Tarea 1: Analizador de sentimiento usando TF-IDF + Regresión Logística. ¡No la dejen para el último día!

Para la Próxima Sesión 📚

Semana 5: Redes Neuronales para NLP

  • Perceptrones y retropropagación
  • Redes neuronales feedforward (MLPs)
  • Clasificación de texto con redes neuronales

Lectura:

Preparación:

  • Entregar Tarea 1 antes del viernes
  • Repasar conceptos de álgebra lineal (multiplicación de matrices, gradientes)

¿Preguntas? 🙋

¡Gracias!

📧 fsuarez@ucb.edu.bo

🔗 Materiales: github.com/fjsuarez/ucb-nlp