3. t-SNE 数学推导:高维空间高斯分布、低维空间t分布、梯度计算

这一章我们来啃t-SNE的数学内核。别怕,虽然看着公式多,但核心逻辑其实很直观。

说白了,t-SNE要做的事情就是:在高维空间里,用高斯分布衡量点与点之间的相似度;然后在低维空间里,用t分布模仿这种相似度。最后通过梯度下降,让两个空间的分布尽量一致。

我个人习惯把这个问题拆成三步来看:

  1. 高维空间里,邻居关系怎么定义?
  2. 低维空间里,邻居关系怎么定义?
  3. 怎么让它们俩长得像?

3.1 高维空间:用高斯分布衡量相似度

假设我们有一堆高维数据点x₁, x₂, ... , xₙ。对于任意两个点xⱼ和xₖ,它们到底算不算“邻居”?t-SNE的做法不是简单算欧氏距离,而是用了一个条件概率的思路。

它把点xⱼ周围的距离分布,看成是一个以xⱼ为中心的高斯分布。然后问:如果我在这个分布里采样,采到xₖ的概率有多大?

这个条件概率p_{j|k}的公式长这样:

p_{j|k} = exp(-||xⱼ - xₖ||² / 2σₖ²) / Σ_{l≠k} exp(-||xₖ - xₗ||² / 2σₖ²)

其中σₖ是点xₖ周围高斯分布的方差。这个σₖ不是固定的——它通过一个叫“困惑度”(Perplexity)的参数来调节。困惑度大致相当于你期望每个点有多少个有效邻居。

我刚开始接触时也觉得这个σₖ很玄乎。后来想通了:它就是个“感知半径”。困惑度设小了,每个点只看几个最近的邻居;设大了,远处的点也会被拉进来。

有了条件概率之后,我们通常会把它们对称化,得到联合概率分布pⱼₖ:

pⱼₖ = (p_{j|k} + p_{k|j}) / 2n

这个对称化的目的,我个人理解是为了让距离关系更稳定,避免出现“A觉得B是邻居,但B不觉得A是邻居”这种尴尬情况。

核心要点:高维空间用高斯分布建模邻居关系,通过困惑度控制邻居数量。

3.2 低维空间:用t分布搞定“拥挤问题”

现在我们把数据映射到低维空间,得到了y₁, y₂, ... , yₙ。同样地,我们需要定义低维空间里点与点之间的相似度。

这里t-SNE做了一个关键选择——它没有继续用高斯分布,而是换成了t分布(自由度为1,也就是柯西分布)。

低维空间的联合概率qⱼₖ公式如下:

qⱼₖ = (1 + ||yⱼ - yₖ||²)⁻¹ / Σ_{l≠m} (1 + ||yₗ - yₘ||²)⁻¹

为什么要换成t分布?我遇到过不少初学者问这个问题。其实原因很直观:

  • 高维空间里,数据点通常比较稀疏,远距离的点之间相似度自然很低。
  • 但在低维空间里,如果还用高斯分布,远处的点会被“推”得很远,造成所谓的“拥挤问题”(Crowding Problem)。
  • t分布有长尾特性——它允许远处的点在低维空间里稍微近一点,而不会过分惩罚它们。

需要注意的是:t分布的尾部比高斯分布厚重得多,这会导致低维空间中的距离关系被“拉伸”。这也是为什么t-SNE的可视化结果中,簇之间的距离往往不能直接解读为“真实差距”。

3.3 梯度计算:让两个分布越来越像

接下来就是关键步骤了——我们要让高维空间的p分布和低维空间的q分布尽量接近。衡量两个分布差异,最常用的就是KL散度(Kullback-Leibler divergence)。

目标函数C长这样:

C = Σⱼ Σₖ pⱼₖ * log(pⱼₖ / qⱼₖ)

我们的目标就是最小化这个C。怎么最小化?梯度下降。

对每个低维点yᵢ求梯度,经过一番推导(嗯,我当初推这个符号的时候也费了不少劲),最终得到的梯度公式是:

∂C/∂yᵢ = 4 * Σⱼ (pⱼᵢ - qⱼᵢ) * (yᵢ - yⱼ) * (1 + ||yᵢ - yⱼ||²)⁻¹

这个公式看着复杂,但拆开来看其实很清晰:

  • pⱼᵢ - qⱼᵢ:高维和低维的相似度差异。如果高维认为它们该更近(p > q),就让它们靠近;反之则远离。
  • yᵢ - yⱼ:移动的方向向量。
  • (1 + ||yᵢ - yⱼ||²)⁻¹:t分布的权重,防止远距离点被过度拉扯。

我曾经在实现t-SNE时,一直觉得梯度计算这部分最容易出bug。特别是符号没搞对,结果可视化时发现所有点都挤成一团。嗯,后来检查发现是梯度里的负号写反了。

小技巧:实际实现时,通常会在梯度上加上一个“动量项”(Momentum),帮助算法跳出局部最优。一般动量值设为0.8左右,后期可以调高到0.9。

3.4 整体流程与核心逻辑

现在我们把这三步串起来,看看t-SNE的完整流程。下面这张图梳理了整个逻辑链条:

t-SNE 核心流程图 高维数据点 x₁...xₙ 高维空间:高斯分布 计算条件概率 pⱼₖ 计算 KL 散度 C = Σp·log(p/q) 低维空间:t 分布(柯西) 计算联合概率 qⱼₖ 梯度下降更新 yᵢ ∂C/∂yᵢ = 4Σ(pᵢⱼ - qᵢⱼ)·(...) 迭代至收敛 → 降维结果 迭代 学习率 + 动量 加速收敛、避免局部最优

整体流程就是:高维→高斯分布→计算p→KL散度→梯度下降→更新低维坐标→用t分布重新计算q→再算KL散度→再梯度下降……如此循环,直到收敛。

3.5 实现中的一些坑(我踩过的)

最后分享几个实际编码时容易翻车的地方:

  • 对称化公式写错:我曾经因为pⱼₖ对称化时忘了除以2n,结果所有点都挤到一块。调了一下午才发现。
  • 困惑度太小:建议设置在5-50之间。我一般从30开始调,效果不好再往两边试。
  • 学习率太激进:学习率设太大,梯度会震荡,结果不稳定。我习惯用200-500这个范围。
  • 迭代次数不够:别指望几百步就能收敛。t-SNE通常需要1000步以上才能稳定。

记住一句话:t-SNE的数学不复杂,但参数调校和工程实现才是真正考验耐心的环节。

关于数学推导我们就聊到这里。理解了这些,后面看代码实现时会轻松很多。