t-SNE significa t-distributed Stochastic Neighbor Embedding.
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.

PCA es una técnica poderosa cuando los datos tienen una estructura lineal. Pero en muchos casos reales, los datos:
PCA busca direcciones globales de varianza, pero ignora la forma local de los datos.
t-SNE fue diseñado para capturar y preservar vecindades locales durante la reducción de dimensionalidad, permitiendo visualizaciones fieles en 2D o 3D.
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.
“La probabilidad condicional de que el punto $ x_j $ sea vecino del punto $ x_i $, dada la distribución local alrededor de $ x_i $.”
$\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)$
Asegura que las probabilidades condicionales suman 1:
$\sum_{j \neq i} p_{j|i} = 1$
perplexity).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:
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.
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?
$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.
Queremos asignar altas probabilidades a pares de puntos que están cerca en 2D y bajas probabilidades a los que están lejos.
En PCA o en el paso 1 de t-SNE usamos Gaussiana, pero aquí eso no funciona bien porque…
Es la versión proyectada del $ p_{ij} $ original (calculado en alta dimensión).
En resumen:
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.
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()
# !pip install ipywidgets seaborn matplotlib scikit-learn
# !jupyter nbextension enable --py widgetsnbextension --sys-prefix
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")
);
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)$
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.
| 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 |
| 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 |
# 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()
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.
# 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))
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));
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:
$ p_{j|i} $ y $ p_{i|j} $ responden a preguntas distintas:
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.
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:
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.
Imagina que:
Entonces, aunque $ x_i $ y $ x_j $ estén relativamente cerca entre sí:
En ese caso:
$ p_{j|i} $ puede ser pequeño, mientras que $ p_{i|j} $ puede ser grande
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.
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:
Así obtenemos una medida de afinidad más equilibrada entre los puntos.
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} $.
En t-SNE construimos dos distribuciones de probabilidad sobre pares de puntos:
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) $
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:
En ese caso, el cociente $ \frac{p_{ij}}{q_{ij}} $ se vuelve grande y la penalización aumenta.
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.
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.
Como t-SNE prioriza la estructura local:
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.