Reducción de Dimensionalidad: t-SNE

Modelos de Machine Learning No-Supervisados


📌 Dr. Cristian Candia

🎓 Universidad del Desarrollo (UDD), Chile

🎓 Northwestern University, United States

🚀 Founder


¿Qué significa t-SNE?

t-SNE significa t-distributed Stochastic Neighbor Embedding.

  • t-distributed: usa una distribución t de Student en el espacio de baja dimensión.
  • Stochastic: modela las relaciones entre puntos usando probabilidades.
  • Neighbor: se enfoca en preservar la estructura de vecindad local.
  • Embedding: construye una representación de los datos en un espacio de menor dimensión.

En términos simples, t-SNE es una técnica de reducción de dimensionalidad que proyecta datos de alta dimensión en 2D o 3D intentando conservar quiénes son vecinos cercanos en el espacio original.

Ejemplo

t-SNE — Intuición, Matemática y Motivación

1. ¿Por qué necesitamos t-SNE?

PCA es una técnica poderosa cuando los datos tienen una estructura lineal. Pero en muchos casos reales, los datos:

  • Viven en espacios de alta dimensión (10, 50, 1000...).
  • Se organizan en formas complejas: espirales, anillos, variedades curvas (manifolds).
  • Tienen estructuras locales importantes que se pierden al proyectar linealmente (como en PCA).

Problema:

PCA busca direcciones globales de varianza, pero ignora la forma local de los datos.

Solución:

t-SNE fue diseñado para capturar y preservar vecindades locales durante la reducción de dimensionalidad, permitiendo visualizaciones fieles en 2D o 3D.


2. Intuición del algoritmo paso a paso

Paso 1: Medir similitud en el espacio original (alta dimensión)

Para cada punto $ \mathbf{x}_i $, queremos saber quiénes son sus vecinos más cercanos y cuán cercanos son.

  • Lo hacemos construyendo una distribución de probabilidad alrededor de cada punto usando una Gaussiana centrada en $ \mathbf{x}_i $:

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

  • Esto se puede interpretar como:
    “¿Con qué probabilidad el punto $ x_j $ sería elegido como vecino de $ x_i $?"

    Para hacer esta relación simétrica (ver nota 1 al final):

$p_{ij} = \frac{p_{j|i} + p_{i|j}}{2n}$

Nota: Esta construcción pone mayor probabilidad a los puntos más cercanos, y menor a los lejanos → define la estructura local.


¿Cómo se lee esta fórmula?

“La probabilidad condicional de que el punto $ x_j $ sea vecino del punto $ x_i $, dada la distribución local alrededor de $ x_i $.”


¿Qué significa cada parte?

Numerador:

$\exp\left(-\frac{\|\mathbf{x}_i - \mathbf{x}_j\|^2}{2\sigma_i^2}\right)$

  • Es una medida de similitud entre $ x_i $ y $ x_j $, usando una distribución Gaussiana centrada en $ x_i $.
  • Si $ x_j $ está cerca de $ x_i $, la distancia será pequeña → el valor exponencial será grande (alta probabilidad).
  • Si $ x_j $ está lejos, la distancia será grande → el valor será cercano a 0 (baja probabilidad).

Denominador:

$\sum_{k \neq i} \exp\left(-\frac{\|\mathbf{x}_i - \mathbf{x}_k\|^2}{2\sigma_i^2}\right)$

  • Es la normalización: suma todas las similitudes posibles de $ x_i $ con el resto de los puntos.
  • Asegura que las probabilidades condicionales suman 1:

    $\sum_{j \neq i} p_{j|i} = 1$


¿Por qué usamos una Gaussiana?

  • Porque es una forma natural de capturar vecindades locales: puntos más cercanos tienen mayor peso.
  • La varianza $ \sigma_i^2 $ puede adaptarse a la densidad local del dato (esto se controla con el parámetro perplexity).
  • t-SNE no quiere preservar distancias exactas, sino las relaciones de vecindad.

¿Qué es eso de $ p_{ij} $ simétrico?

La fórmula:

$p_{ij} = \frac{p_{j|i} + p_{i|j}}{2n}$

sirve para convertir las probabilidades condicionales asimétricas (de $ i $ a $ j $) en una probabilidad simétrica entre pares:

  • $ p_{j|i} $ y $ p_{i|j} $ no son iguales porque las Gaussinas están centradas en distintos puntos.
  • Para poder comparar esas relaciones con el espacio reducido (que sí será simétrico), se hace el promedio.
  • El factor $ \frac{1}{2n} $ normaliza todo el sistema para que las probabilidades simétricas también sumen 1.

Intuición

Esta fórmula responde a la pregunta: ¿qué tan probable es que $ x_j $ sea vecino de $ x_i $, si lanzáramos una moneda Gaussiana centrada en $ x_i $?. Y como esa probabilidad depende de quién está mirando a quién, t-SNE simmetriza esas relaciones para poder trasladarlas al espacio de 2D.


Paso 2: Medir similitud en el espacio proyectado (2D o 3D)

Queremos encontrar un conjunto de puntos $ \mathbf{y}_1, \ldots, \mathbf{y}_n $ en baja dimensión que preserven las relaciones de vecindad definidas por los $ p_{ij} $.

  • Definimos una nueva distribución entre pares $ \mathbf{y}_i, \mathbf{y}_j $, pero esta vez con una distribución t de Student (grado de libertad = 1):

    $q_{ij} = \frac{(1 + \|\mathbf{y}_i - \mathbf{y}_j\|^2)^{-1}}{\sum_{k \neq l}(1 + \|\mathbf{y}_k - \mathbf{y}_l\|^2)^{-1}}$

    ¿Por qué una t-Student y no una Gaussiana como antes?

  • Porque la t tiene colas más largas, lo que ayuda a:
    • Evitar el crowding problem (cuando todo termina apiñado en 2D).
    • Separar bien puntos lejanos sin colapsar los cercanos.

Ecuación de similitud en el espacio proyectado

$q_{ij} = \frac{(1 + \|\mathbf{y}_i - \mathbf{y}_j\|^2)^{-1}}{\sum_{k \ne l} (1 + \|\mathbf{y}_k - \mathbf{y}_l\|^2)^{-1}}$

Esta fórmula define la probabilidad de similitud entre dos puntos $ \mathbf{y}_i $ y $ \mathbf{y}_j $ en el espacio de baja dimensión (por ejemplo, 2D), usando una distribución t de Student en lugar de una Gaussiana.


¿Qué significa esta fórmula?

Intuición general:

Queremos asignar altas probabilidades a pares de puntos que están cerca en 2D y bajas probabilidades a los que están lejos.

¿Cómo se logra?

  • La distancia $ \|\mathbf{y}_i - \mathbf{y}_j\|^2 $ nos dice qué tan separados están los puntos proyectados.
  • Usamos la función: $(1 + \|\mathbf{y}_i - \mathbf{y}_j\|^2)^{-1}$ que disminuye suavemente con la distancia, pero más lento que una Gaussiana. Esto crea una distribución heavy-tailed (colas largas).

¿Por qué usamos la t de Student?

En PCA o en el paso 1 de t-SNE usamos Gaussiana, pero aquí eso no funciona bien porque…

Si usamos una Gaussiana en 2D:

  • Los puntos lejanos se vuelven casi indistinguibles → todos los valores tienden a cero.
  • Se genera el crowding problem: los puntos no caben en el plano 2D sin solaparse.

Con una t de Student:

  • Las colas más largas permiten que puntos lejanos sigan teniendo peso.
  • Esto evita que los puntos cercanos se apiñen demasiado.
  • Se separan mejor las estructuras reales (clusters, vecinos).

¿Qué representa $ q_{ij} $?

Es la versión proyectada del $ p_{ij} $ original (calculado en alta dimensión).

En resumen:

  • $ p_{ij} $: qué tan cerca están $ x_i $ y $ x_j $ en el espacio original.
  • $ q_{ij} $: qué tan cerca están $ y_i $ y $ y_j $ en el espacio 2D.
  • t-SNE busca que $ q_{ij} $ se parezca lo más posible a $ p_{ij} $ → así se preservan las relaciones locales.

Intuición

En el espacio de proyección, usamos una t de Student porque queremos permitir que algunos puntos estén más lejos, sin que desaparezcan estadísticamente. Eso le da a t-SNE la capacidad de abrir el espacio y mostrar grupos de datos separados de forma clara y limpia.

In [9]:
import numpy as np
import matplotlib.pyplot as plt

# Distancias
d = np.linspace(0, 15, 600)

# Parámetros
sigma = 1.0
nu = 1  # grados de libertad de la t-Student

# Funciones de similitud sin constante de normalización
gaussian = np.exp(-(d**2) / (2 * sigma**2))
student_t = (1 + d**2 / nu) ** (-(nu + 1) / 2)

# =========================
# Gráfico 1: escala normal
# =========================
plt.figure(figsize=(8, 5))
plt.plot(d, gaussian, label="Gaussiana", linewidth=2)
plt.plot(d, student_t, label="t-Student", linewidth=2)
plt.xlabel("Distancia")
plt.ylabel("Similitud relativa")
plt.title("Gaussiana vs t-Student (escala lineal)")
plt.legend()
plt.grid(True)
plt.show()

# =========================
# Gráfico 2: escala logarítmica
# =========================
plt.figure(figsize=(8, 5))
plt.semilogy(d, gaussian, label="Gaussiana", linewidth=2)
plt.semilogy(d, student_t, label="t-Student", linewidth=2)
plt.xlabel("Distancia")
plt.ylabel("Similitud relativa (escala log)")
plt.title("Gaussiana vs t-Student (escala logarítmica)")
plt.legend()
plt.grid(True, which="both")
plt.show()
In [ ]:
# !pip install ipywidgets seaborn matplotlib scikit-learn

# !jupyter nbextension enable --py widgetsnbextension --sys-prefix
In [ ]:
import numpy as np
import matplotlib.pyplot as plt
from ipywidgets import interact, FloatSlider, IntSlider, Checkbox

def comparar_colas(sigma=1.0, nu=1, dmax=15, log_scale=True, mostrar_valor_ref=True, d_ref=3.0):
    d = np.linspace(0, dmax, 800)
    
    # Gaussiana
    gaussian = np.exp(-(d**2) / (2 * sigma**2))
    
    # t-Student con nu grados de libertad
    student_t = (1 + d**2 / nu) ** (-(nu + 1) / 2)
    
    plt.figure(figsize=(9, 5))
    
    if log_scale:
        plt.semilogy(d, gaussian, label=fr"Gaussiana ($\sigma={sigma:.2f}$)", linewidth=2)
        plt.semilogy(d, student_t, label=fr"t-Student ($\nu={nu}$)", linewidth=2)
        plt.ylabel("Similitud relativa (escala log)")
        plt.title("Gaussiana vs t-Student (escala logarítmica)")
    else:
        plt.plot(d, gaussian, label=fr"Gaussiana ($\sigma={sigma:.2f}$)", linewidth=2)
        plt.plot(d, student_t, label=fr"t-Student ($\nu={nu}$)", linewidth=2)
        plt.ylabel("Similitud relativa")
        plt.title("Gaussiana vs t-Student (escala lineal)")
    
    if mostrar_valor_ref:
        g_ref = np.exp(-(d_ref**2) / (2 * sigma**2))
        t_ref = (1 + d_ref**2 / nu) ** (-(nu + 1) / 2)
        
        plt.axvline(d_ref, linestyle="--", alpha=0.7)
        plt.scatter([d_ref], [g_ref], s=60)
        plt.scatter([d_ref], [t_ref], s=60)
        
        if log_scale:
            plt.text(d_ref + 0.2, g_ref, f"G={g_ref:.4e}")
            plt.text(d_ref + 0.2, t_ref, f"T={t_ref:.4e}")
        else:
            plt.text(d_ref + 0.2, g_ref, f"G={g_ref:.4f}")
            plt.text(d_ref + 0.2, t_ref, f"T={t_ref:.4f}")
    
    plt.xlabel("Distancia")
    plt.legend()
    plt.grid(True, which="both")
    plt.show()

interact(
    comparar_colas,
    sigma=FloatSlider(value=1.0, min=0.2, max=3.0, step=0.1, description="sigma"),
    nu=IntSlider(value=1, min=1, max=30, step=1, description="nu"),
    dmax=IntSlider(value=15, min=5, max=30, step=1, description="d max"),
    log_scale=Checkbox(value=False, description="escala log"),
    mostrar_valor_ref=Checkbox(value=True, description="mostrar punto"),
    d_ref=FloatSlider(value=3.0, min=0.5, max=15.0, step=0.5, description="d ref")
);

Paso 3: Medir qué tan bien coinciden las distribuciones $ P $ y $ Q $

  • Usamos la divergencia de Kullback-Leibler (KL) para medir la diferencia entre ambas distribuciones:

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

Intuición:

  • Queremos que si dos puntos eran cercanos en el espacio original (alto $ p_{ij} $), también lo sean en el espacio proyectado (alto $ q_{ij} $).
  • Si no lo son, la función de pérdida penaliza duramente la discrepancia.

Paso 4: Minimizar la KL-divergence

  • t-SNE encuentra la configuración de puntos $ \{ \mathbf{y}_i \} $ en 2D/3D que minimiza la divergencia KL entre $ P $ y $ Q $.
  • Esto se hace con gradiente descendente estocástico (SGD).
  • La magia ocurre aquí! El algoritmo mueve los puntos en 2D/3D hasta que la distribución se parezca lo más posible a la original.

La KL divergence en t-SNE está diseñada para castigar sobre todo cuando rompemos vecindades reales. Si dos puntos eran cercanos en alta dimensión y en 2D los separo, eso duele mucho en la función objetivo. Por eso t-SNE preserva bien estructura local y genera clusters compactos.

Para más detalles ver Nota 2.


3. Parámetros importantes

Parámetro Descripción Intuición
perplexity ≈ número efectivo de vecinos considerados entre 5 y 50 usualmente
learning_rate velocidad de ajuste muy bajo: lento, muy alto: distorsión
n_iter número de iteraciones más = más refinado
random_state semilla de aleatoriedad garantiza reproducibilidad

4. Ventajas y Limitaciones

Ventajas Limitaciones
Captura relaciones no lineales No preserva distancias globales
Preserva vecindad local Sensible al parámetro perplexity
Visualizaciones muy intuitivas y limpias No sirve para compresión real ni reconstrucción inversa
Funciona bien en datos complejos (biología, texto) Lento en datasets muy grandes

In [4]:
# t-SNE con sklearn en Iris y Digits

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

from sklearn.datasets import load_iris, load_digits
from sklearn.preprocessing import StandardScaler
from sklearn.manifold import TSNE

#  1. Dataset IRIS
iris = load_iris()
X_iris = StandardScaler().fit_transform(iris.data)
y_iris = iris.target
target_names_iris = iris.target_names

# Aplicar t-SNE
tsne_iris = TSNE(n_components=2, perplexity=30, learning_rate=200, n_iter=500, random_state=42)
X_iris_tsne = tsne_iris.fit_transform(X_iris)

# Graficar resultados
plt.figure(figsize=(6,5))
sns.scatterplot(x=X_iris_tsne[:,0], y=X_iris_tsne[:,1], hue=target_names_iris[y_iris], palette='Set2', s=60)
plt.title("t-SNE - Iris Dataset")
plt.xlabel("Dim 1")
plt.ylabel("Dim 2")
plt.legend(title="Especie")
plt.grid(True)
plt.tight_layout()
plt.show()

# 2. Dataset DIGITS
digits = load_digits()
X_digits = StandardScaler().fit_transform(digits.data)
y_digits = digits.target

# Aplicar t-SNE (usa menos iteraciones si es necesario)
tsne_digits = TSNE(n_components=2, perplexity=30, learning_rate=200, n_iter=500, random_state=42)
X_digits_tsne = tsne_digits.fit_transform(X_digits)

# Graficar resultados
plt.figure(figsize=(7,6))
sns.scatterplot(x=X_digits_tsne[:,0], y=X_digits_tsne[:,1], hue=y_digits.astype(str), palette='tab10', s=40, alpha=0.7, legend=False)
plt.title("t-SNE - Digits Dataset")
plt.xlabel("Dim 1")
plt.ylabel("Dim 2")
plt.grid(True)
plt.tight_layout()
plt.show()
/Users/crcandia/anaconda3/envs/candialab2/lib/python3.10/site-packages/sklearn/manifold/_t_sne.py:1164: FutureWarning: 'n_iter' was renamed to 'max_iter' in version 1.5 and will be removed in 1.7.
  warnings.warn(
/Users/crcandia/anaconda3/envs/candialab2/lib/python3.10/site-packages/sklearn/manifold/_t_sne.py:1164: FutureWarning: 'n_iter' was renamed to 'max_iter' in version 1.5 and will be removed in 1.7.
  warnings.warn(
In [18]:
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

from sklearn.datasets import load_digits
from sklearn.preprocessing import StandardScaler
from sklearn.manifold import TSNE
from matplotlib.offsetbox import OffsetImage, AnnotationBbox

# =========================
# 1. Cargar datos
# =========================
digits = load_digits()
X_digits_raw = digits.data          # datos originales (64 pixeles)
X_digits = StandardScaler().fit_transform(digits.data)
y_digits = digits.target

# =========================
# 2. Aplicar t-SNE
# =========================
tsne_digits = TSNE(
    n_components=2,
    perplexity=30,
    learning_rate=200,
    max_iter=500,
    random_state=42
)
X_digits_tsne = tsne_digits.fit_transform(X_digits)

# =========================
# 3. Scatter base
# =========================
fig, ax = plt.subplots(figsize=(10, 8))
sns.scatterplot(
    x=X_digits_tsne[:, 0],
    y=X_digits_tsne[:, 1],
    hue=y_digits.astype(str),
    palette='tab10',
    s=18,
    alpha=0.35,
    legend=False,
    ax=ax
)

ax.set_title("t-SNE - Digits Dataset con miniaturas")
ax.set_xlabel("Dim 1")
ax.set_ylabel("Dim 2")
ax.grid(True)

# =========================
# 4. Agregar algunas miniaturas
#    (submuestreo para no saturar la figura)
# =========================
shown_points = np.array([[1e9, 1e9]])  # para controlar superposición
min_dist = 5.0  # aumenta o reduce según cuánto quieras espaciar las imágenes

for i in range(len(X_digits_tsne)):
    point = X_digits_tsne[i]
    
    # evitar poner miniaturas demasiado juntas
    dist = np.sum((shown_points - point) ** 2, axis=1)
    if np.min(dist) < min_dist**2:
        continue

    shown_points = np.vstack([shown_points, point])

    image = digits.images[i]  # imagen 8x8 original
    imagebox = OffsetImage(image, cmap="gray_r", zoom=0.8)
    ab = AnnotationBbox(imagebox, point, frameon=True, pad=0.2)
    ax.add_artist(ab)

plt.tight_layout()
plt.show()

Con t-SNE no reconstruimos imágenes, porque t-SNE no aprende una transformación invertible desde 2D al espacio original. Lo que sí podemos hacer es visualizar dónde cae cada imagen original en el embedding. Si queremos compresión con reconstrucción, PCA sí permite hacerlo.

In [ ]:
#  Visualización interactiva de t-SNE sobre Iris con sliders
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.manifold import TSNE
import ipywidgets as widgets
from ipywidgets import interact

# Cargar y estandarizar los datos
iris = load_iris()
X = StandardScaler().fit_transform(iris.data)
y = iris.target
target_names = iris.target_names

# Función que aplica t-SNE con parámetros interactivos
def plot_tsne(perplexity=30, learning_rate=200, n_iter=5, random_state=42):
    tsne = TSNE(n_components=2, perplexity=perplexity,
                learning_rate=learning_rate, n_iter=n_iter,
                random_state=random_state)
    X_embedded = tsne.fit_transform(X)

    # Visualización
    plt.figure(figsize=(6, 5))
    sns.scatterplot(x=X_embedded[:, 0], y=X_embedded[:, 1],
                    hue=target_names[y], palette='Set2', s=70, alpha=0.8)
    plt.title(f"t-SNE sobre IRIS\n(perplexity={perplexity}, learning_rate={learning_rate})")
    plt.xlabel("Dim 1")
    plt.ylabel("Dim 2")
    plt.legend(title="Especie", loc="best")
    plt.grid(True)
    plt.tight_layout()
    plt.show()

# Sliders interactivos
interact(plot_tsne,
         perplexity=widgets.IntSlider(min=5, max=50, step=5, value=30),
         learning_rate=widgets.IntSlider(min=10, max=500, step=10, value=200),
         n_iter=widgets.IntSlider(min=250, max=1000, step=250, value=500),
         random_state=widgets.fixed(42))
Out[ ]:
<function __main__.plot_tsne(perplexity=30, learning_rate=200, n_iter=5, random_state=42)>
In [3]:
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_iris, load_digits
from sklearn.preprocessing import StandardScaler
from sklearn.manifold import TSNE
from sklearn.decomposition import PCA
from ipywidgets import interact, widgets

# Cargar datasets
iris = load_iris()
X_iris = StandardScaler().fit_transform(iris.data)
y_iris = iris.target
labels_iris = iris.target_names

digits = load_digits()
X_digits = StandardScaler().fit_transform(digits.data)
y_digits = digits.target
labels_digits = y_digits.astype(str)

# Función de comparación
def compare_tsne_pca(dataset='iris', method='t-SNE', perplexity=30, learning_rate=200, n_iter=5):
    if dataset == 'iris':
        X = X_iris
        y = y_iris
        labels = labels_iris[y]
        palette = 'Set2'
    else:
        X = X_digits
        y = y_digits
        labels = labels_digits
        palette = 'tab10'

    if method == 't-SNE':
        reducer = TSNE(n_components=2, perplexity=perplexity,
                       learning_rate=learning_rate, n_iter=n_iter, random_state=42)
        X_reduced = reducer.fit_transform(X)
    else:
        reducer = PCA(n_components=2)
        X_reduced = reducer.fit_transform(X)

    # Graficar resultados
    plt.figure(figsize=(7, 6))
    sns.scatterplot(x=X_reduced[:, 0], y=X_reduced[:, 1],
                    hue=labels, palette=palette, s=50, alpha=0.8, legend=False)
    plt.title(f"{method} en dataset {dataset.upper()}")
    plt.xlabel("Componente 1")
    plt.ylabel("Componente 2")
    plt.grid(True)
    plt.tight_layout()
    plt.show()

# Interfaz interactiva
interact(compare_tsne_pca,
         dataset=widgets.RadioButtons(options=['iris', 'digits'], description='Dataset:'),
         method=widgets.RadioButtons(options=['t-SNE', 'PCA'], description='Método:'),
         perplexity=widgets.IntSlider(min=5, max=50, step=5, value=30),
         learning_rate=widgets.IntSlider(min=10, max=500, step=10, value=200),
         n_iter=widgets.IntSlider(min=250, max=1000, step=250, value=500));

Notas

Nota 1

¿Por qué la relación de similitud no es simétrica?

La cantidad $ p_{j|i} $ se define como una probabilidad condicional:

$ p_{j|i} = \frac{\exp\left( -\frac{|\mathbf{x}_i - \mathbf{x}*j|^2}{2\sigma_i^2} \right)}{\sum{k \neq i} \exp\left( -\frac{|\mathbf{x}_i - \mathbf{x}_k|^2}{2\sigma_i^2} \right)} $

y se interpreta como:

“Dado el punto $ x_i $, ¿con qué probabilidad elegiría a $ x_j $ como vecino?”

Esta relación no es simétrica porque está construida desde la perspectiva de $ x_i $. En particular, hay dos razones clave:

1. Es una probabilidad condicional dirigida

$ p_{j|i} $ y $ p_{i|j} $ responden a preguntas distintas:

  • $ p_{j|i} $: “¿Qué tan vecino es $ x_j $ para $ x_i $?”
  • $ p_{i|j} $: “¿Qué tan vecino es $ x_i $ para $ x_j $?”

Aunque la distancia euclidiana es simétrica, es decir,

$ |\mathbf{x}_i - \mathbf{x}_j| = |\mathbf{x}_j - \mathbf{x}_i| $

las probabilidades no tienen por qué serlo, porque cada una se normaliza con respecto a un punto distinto.

2. Cada punto tiene su propia escala local $ \sigma_i $

En t-SNE, la Gaussiana alrededor de cada punto usa una varianza propia $ \sigma_i $, ajustada para reflejar la densidad local alrededor de ese punto.

Entonces, en general:

  • alrededor de $ x_i $ usamos $ \sigma_i $
  • alrededor de $ x_j $ usamos $ \sigma_j $

y típicamente:

$ \sigma_i \neq \sigma_j $

Eso significa que dos puntos a la misma distancia pueden tener probabilidades distintas según desde qué punto miremos la relación.


Intuición simple

Imagina que:

  • $ x_i $ está en una zona muy densa, rodeado de muchos vecinos cercanos
  • $ x_j $ está en una zona más aislada, con pocos vecinos cercanos

Entonces, aunque $ x_i $ y $ x_j $ estén relativamente cerca entre sí:

  • para $ x_i $, $ x_j $ puede no ser tan especial, porque tiene muchos otros vecinos cerca
  • para $ x_j $, en cambio, $ x_i $ puede ser uno de sus vecinos más importantes

En ese caso:

$ p_{j|i} $ puede ser pequeño, mientras que $ p_{i|j} $ puede ser grande


Ejemplo conceptual

Supón que en el vecindario de $ x_i $ hay 20 puntos muy cercanos. Entonces la masa de probabilidad de $ p_{j|i} $ se reparte entre muchos vecinos.

Pero si en el vecindario de $ x_j $ hay solo 3 puntos cercanos, entonces $ x_i $ puede recibir una fracción mucho mayor de la probabilidad.

Por eso, aunque la distancia entre $ x_i $ y $ x_j $ sea la misma en ambos sentidos, la relación de vecindad probabilística no lo es.


¿Por qué luego se simetriza?

Como t-SNE quiere trabajar con una noción de similitud mutua entre pares de puntos, transforma estas probabilidades condicionales en una relación simétrica:

$ p_{ij} = \frac{p_{j|i} + p_{i|j}}{2n} $

Esto combina ambas perspectivas:

  • cuánto considera $ x_i $ a $ x_j $ como vecino
  • y cuánto considera $ x_j $ a $ x_i $ como vecino

Así obtenemos una medida de afinidad más equilibrada entre los puntos.


Entonces

La relación no es simétrica porque $ p_{j|i} $ no mide una similitud absoluta entre dos puntos, sino una probabilidad de vecindad vista desde $ x_i $. Como cada punto tiene su propio contexto local y su propia normalización, en general $ p_{j|i} \neq p_{i|j} $.

Nota 2:

¿Qué mide la divergencia KL en t-SNE?

En t-SNE construimos dos distribuciones de probabilidad sobre pares de puntos:

  • $ P = \{p_{ij}\} $, que representa la similitud entre puntos en el espacio original.
  • $ Q = \{q_{ij}\} $, que representa la similitud entre puntos en el espacio proyectado en 2D o 3D.

La idea central del algoritmo es encontrar una representación en baja dimensión tal que $ Q $ se parezca lo más posible a $ P $.

Para medir esa discrepancia usamos la divergencia de Kullback-Leibler:

$ \mathrm{KL}(P \| Q) = \sum_{i \ne j} p_{ij} \log\left(\frac{p_{ij}}{q_{ij}}\right) $

Intuición principal

Esta expresión penaliza especialmente los casos en que dos puntos que eran vecinos importantes en el espacio original dejan de ser vecinos en la proyección.

Eso ocurre cuando:

  • $ p_{ij} $ es alto,
  • pero $ q_{ij} $ es bajo.

En ese caso, el cociente $ \frac{p_{ij}}{q_{ij}} $ se vuelve grande y la penalización aumenta.

¿Por qué esto favorece la estructura local?

Cada término está multiplicado por $ p_{ij} $, así que los pares más importantes en el espacio original pesan más en la pérdida total.

Esto significa que t-SNE no intenta preservar todas las distancias por igual. En cambio, prioriza mantener cercanos a los puntos que realmente eran vecinos en alta dimensión.

En otras palabras, t-SNE se concentra en preservar vecindades locales, no en reproducir perfectamente la geometría global.

¿Por qué importa el orden en $ \mathrm{KL}(P \| Q) $?

La divergencia KL no es simétrica. En general:

$ \mathrm{KL}(P \| Q) \neq \mathrm{KL}(Q \| P) $

Minimizar $ \mathrm{KL}(P \| Q) $ implica que es mucho más grave separar puntos que debían estar juntos que juntar puntos que no eran tan cercanos originalmente.

Por eso t-SNE tiende a producir clusters compactos y bien definidos visualmente.

Consecuencia metodológica importante

Como t-SNE prioriza la estructura local:

  • suele representar bien quién es vecino de quién,
  • pero no garantiza que las distancias entre clusters tengan una interpretación geométrica precisa.

Por eso, en una visualización t-SNE, la cercanía dentro de grupos suele ser más confiable que la distancia exacta entre grupos distintos.