t-SNE算法的核心实现涉及几个关键步骤,主要包括概率分布的构建、梯度计算和优化。以下是这些步骤的简要说明:
1. **概率分布的构建**:
- 在高维空间中,t-SNE使用高斯分布(Gaussian distribution)来构建概率分布,表示样本点之间的相似度。在低维空间中,使用学生t分布(Student's t-distribution)。
- 通过计算高维空间中所有点对之间的欧氏距离,然后使用这些距离来计算高斯概率分布,即联合概率矩阵P。
2. **KL散度(Kullback-Leibler divergence)**:
- t-SNE的目标是最小化高维空间和低维空间概率分布之间的KL散度,以此来保持高维空间中的局部结构。
- KL散度是衡量两个概率分布差异的指标,t-SNE通过最小化这个值来优化低维空间中点的布局。
3. **梯度计算**:
- 为了最小化KL散度,t-SNE需要计算梯度(gradient),这是通过反向传播算法完成的。
- 梯度计算涉及到对高维和低维概率分布的微分,以及对嵌入空间坐标的微分。
4. **优化(梯度下降)**:
- 使用梯度下降算法来更新低维空间中点的位置,以最小化KL散度。
- t-SNE有两种优化阶段:一种是带有早期夸张(early exaggeration)的优化,以鼓励簇之间的更大分离;另一种是标准的梯度下降,用于细化结果。
5. **动量和学习率调整**:
- 在优化过程中,t-SNE使用动量(momentum)来帮助逃离局部最小值,并调整学习率以控制优化步长。
6. **早期夸张和学习率衰减**:
- 在早期夸张阶段,学习率会逐渐减小,以允许在优化的后期阶段进行更细致的调整。
高斯分布就是正态分布,这里我就不讲了。
我们来看一下学生分布:
学生t分布(Student's t-distribution)的原理基于以下统计学概念和假设:
-
正态分布样本的样本均值: 当从一个正态分布的总体中随机抽取样本时,样本均值的分布也是正态分布的。这是中心极限定理的一个特例。
-
样本标准差: 样本的标准差(S)是总体标准差(σ)的一个估计。当总体标准差未知时,我们使用样本标准差来估计它。
-
自由度: 在统计学中,自由度(df)是指在估计统计量时,数据中可以自由变动的值的数量。对于样本方差,自由度通常是样本大小减一(n-1),因为样本均值的计算限制了数据的一个自由度。
-
t统计量: t统计量是通过将样本均值与总体均值的差除以样本标准差(除以样本大小的平方根)来计算的。数学上,如果X1, X2, ..., Xn是从均值为μ、方差为σ^2的正态分布中抽取的样本,则t统计量定义为: t= 其中,Xˉ是样本均值,μ是总体均值,S是样本标准差,n是样本大小。
-
t分布的形成: 当总体标准差σ未知且用样本标准差S代替时,t统计量的分布就是学生t分布。这个分布的形状取决于自由度(n-1),随着自由度的增加,t分布越来越接近标准正态分布。
-
t分布的特性:
- t分布是对称的,并且以0为中心。
- 与标准正态分布相比,t分布的尾部更重,这意味着它更有可能产生极端值。
- 当自由度增加时,t分布的形状越来越接近标准正态分布。
接下来我们来解析一下简化后的t-SNE源代码
import numpy as np
from scipy.spatial.distance import pdist, squareform
from sklearn.decomposition import PCA
from sklearn.utils import check_random_state
from sklearn.neighbors import NearestNeighbors
def _joint_probabilities(distances, desired_perplexity, verbose):
distances = distances.astype(np.float32)
conditional_P = np.exp(-distances ** 2 / (2 * desired_perplexity ** 2))
sum_P = np.sum(conditional_P)
P = conditional_P / sum_P
return P
def _kl_divergence(P, X_embedded, degrees_of_freedom):
dist = pdist(X_embedded, "sqeuclidean")
Q = np.maximum(1 + dist, 1e-15) ** (-1 * dist)
kl_divergence = np.sum(P * np.log(P / Q))
return kl_divergence
def _gradient_descent(kl_divergence_grad, X_embedded, learning_rate, momentum):
update = momentum * update - learning_rate * kl_divergence_grad
X_embedded += update
return X_embedded
class TSNE:
def __init__(self, n_components=2, perplexity=30.0, learning_rate=200.0):
self.n_components = n_components
self.perplexity = perplexity
self.learning_rate = learning_rate
def fit_transform(self, X):
n_samples = X.shape[0]
distances = pdist(X, "euclidean")
P = _joint_probabilities(distances, self.perplexity, 0)
pca = PCA(n_components=self.n_components)
X_embedded = pca.fit_transform(X)
degrees_of_freedom = self.n_components - 1
kl_divergence_grad = None # Initialize gradient
for _ in range(1000): # Max iterations
dist = pdist(X_embedded, "sqeuclidean")
Q = np.maximum(1 + dist, 1e-15) ** (-1 * dist)
kl_divergence_grad = 4 * (P - Q) * (1 + dist) ** (-1)
kl_divergence_grad = kl_divergence_grad[:, np.newaxis] * (X_embedded[:, np.newaxis] - X_embedded[np.newaxis, :])
kl_divergence_grad = kl_divergence_grad.ravel()
X_embedded = _gradient_descent(kl_divergence_grad, X_embedded, self.learning_rate, 0.8)
kl_divergence = _kl_divergence(P, X_embedded, degrees_of_freedom)
return X_embedded
# Example usage
X = np.array([[0, 0, 0], [0, 1, 1], [1, 0, 1], [1, 1, 1]])
tsne = TSNE(n_components=2, perplexity=3)
X_embedded = tsne.fit_transform(X)
print(X_embedded.shape)
1.导入依赖
import numpy as np
from scipy.spatial.distance import pdist, squareform
from sklearn.decomposition import PCA
from sklearn.utils import check_random_state
from sklearn.neighbors import NearestNeighbors
可以看见这边导入了PCA,主成分分析,我们先来了解一下24/11/7 算法笔记 PCA主成分分析-CSDN博客
2.定义计算联合函数
def _joint_probabilities(distances, desired_perplexity, verbose):
distances = distances.astype(np.float32)
conditional_P = np.exp(-distances ** 2 / (2 * desired_perplexity ** 2))
sum_P = np.sum(conditional_P)
P = conditional_P / sum_P
return P
3.定义计算KL散度的函数_kl_divergence
:
def _kl_divergence(P, X_embedded, degrees_of_freedom):
dist = pdist(X_embedded, "sqeuclidean")
Q = np.maximum(1 + dist, 1e-15) ** (-1 * dist)
kl_divergence = np.sum(P * np.log(P / Q))
return kl_divergence
这行代码使用了scipy.spatial.distance.pdist
函数来计算X_embedded
数组中所有点之间的平方欧几里得距离。对于两个点 x 和 y,它们的平方欧几里得距离 d计算公式为:
其中 n 是点的维度数,xi 和 yi 分别是点 x 和 y 在第 i 维的坐标。
在t-SNE算法中,这个距离度量用于计算低维空间中点之间的相似度,进而构建概率分布Q,这是优化过程的一部分,目的是使得低维空间中的概率分布 Q 尽可能地接近高维空间中的概率分布 P。通过最小化 P 和 Q 之间的KL散度来实现这一目标。
4.定义梯度下降的函数_gradient_descent
def _gradient_descent(kl_divergence_grad,X_embedded, learning_rate, momentum):
update = momentum * update - learning_rate * kl_divergence_grad
X_embedded += update
return X_embedded
5.定义t-SNE类的TSNE
:
class TSNE:
def __init__(self, n_components=2, perplexity=30.0, learning_rate=200.0):
self.n_components = n_components
self.perplexity = perplexity
self.learning_rate = learning_rate
这个类初始化t-SNE算法的参数,包括降维后的成分数、困惑度(perplexity)和学习率。
6. 定义fit_transform
方法:
def fit_transform(self, X):
n_samples = X.shape[0]
distances = pdist(X, "euclidean")
P = _joint_probabilities(distances, self.perplexity, 0)
pca = PCA(n_components=self.n_components)
X_embedded = pca.fit_transform(X)
degrees_of_freedom = self.n_components - 1
kl_divergence_grad = None # Initialize gradient
for _ in range(1000): # Max iterations
dist = pdist(X_embedded, "sqeuclidean")
Q = np.maximum(1 + dist, 1e-15) ** (-1 * dist)
#计算KL散度的梯度,并将其展平为一维数组,以便用于梯度下降。
kl_divergence_grad = 4 * (P - Q) * (1 + dist) ** (-1)
kl_divergence_grad = kl_divergence_grad[:, np.newaxis] * (X_embedded[:, np.newaxis] - X_embedded[np.newaxis, :])
kl_divergence_grad = kl_divergence_grad.ravel()
#执行梯度下降
X_embedded = _gradient_descent(kl_divergence_grad, X_embedded, self.learning_rate, 0.8)
#计算KL散度:
kl_divergence = _kl_divergence(P, X_embedded, degrees_of_freedom)
return X_embedded
这个方法执行t-SNE算法的主要步骤:计算高维空间中的联合概率、执行PCA降维、初始化梯度、执行梯度下降优化,并返回最终的低维嵌入结果。