4. t-SNE 核心参数:learning_rate
learning_rate,说白了就是梯度下降的「步长」。你想想看,t-SNE 的优化过程就像在山上找最低点——每一步迈多大,直接决定了你能不能走到真正的谷底,还是卡在半山腰,或者直接滚下山去。
我个人刚接触 t-SNE 时,觉得这参数不就是个步长嘛,调大调小能有多大区别?后来被现实狠狠教育了一次,才发现这东西远比想象中敏感。
4.1 learning_rate 到底是什么?
t-SNE 通过梯度下降最小化 KL 散度,每次迭代都在调整高维点对的相似度与低维嵌入之间的差距。learning_rate 控制的就是每次沿着梯度方向更新的幅度。
数学上很简单:
new_position = old_position - learning_rate × gradient
这个式子看着平平无奇,但实际跑起来,learning_rate 一不对劲,整个嵌入结果就完全变样了。
核心直觉:learning_rate 决定了 t-SNE 在「保留局部结构」和「发现全局结构」之间的平衡路径。
4.2 默认值与推荐范围
在 scikit-learn 中,默认值是 200。这个数字不是拍脑袋定的,Barnes-Hut 实现里大量实验证明 200 对中等规模数据集(几千个点)表现稳定。但「稳定」不等于「最优」,你用默认值就跑,很可能吃亏。
推荐范围是 10 ~ 1000。少于 10,优化基本动不了;超过 1000,结果开始放飞自我。
我整理了一张不同取值的效果对照表,方便你快速判断:
| learning_rate | 优化状态 | 嵌入效果 | 适用场景 |
|---|---|---|---|
| < 10 | 几乎不动 | 点聚成一团,啥结构都看不出 | 基本不用 |
| 10 ~ 50 | 收敛极慢 | 局部聚得紧,全局拉不开 | 极少量数据(< 500 点) |
| 100 ~ 500 | 收敛平稳 | 局部与全局结构都较清晰 | 大多数场景的首选区间 |
| 500 ~ 1000 | 收敛偏快 | 簇间距离增大,整体更分散 | 数据量较大(> 5000 点) |
| > 1000 | 剧烈震荡 | 结构扭曲,常出现均匀「气泡」 | 极少使用,需谨慎 |
提示:默认值 200 是一个安全的「起跑线」。我建议你先用 200 跑一次,观察损失曲线和嵌入形状,再根据效果上下调整。
4.3 不同取值到底怎么影响结果?
这里我结合自己的踩坑经历来说。
学习率太小(< 50)
我在项目中遇到过一批基因表达数据,样本量只有 300 左右。当时图省事用了默认 200,结果还行。但有一次我故意把 learning_rate 降到 20,想看看到底会怎样——跑了 1000 轮,损失值还在缓慢下降,嵌入图上所有点挤成一团,连基本的亚群都分不开。
说白了就是步长太小,迭代了半天还在原地踏步。你增加迭代次数也许能缓解,但效率实在太低。
学习率太大(> 1000)
另一个极端更坑。我曾经处理 8000 个细胞的单细胞 RNA-seq 数据,心想「数据点多,步子迈大点」,直接把 learning_rate 设到 3000。结果跑完一看,所有细胞均匀分布在二维平面上,形成一个「气泡状」结构,完全看不出任何生物学意义的分群。
为什么会这样?因为步长过大,每次更新都直接跳过最优解,损失函数在谷底附近来回震荡,最终嵌入变成了一盘散沙。
警告:学习率过大时,t-SNE 会倾向于产生「均匀分布」的假象——看起来每个簇都分开了,其实是算法失效的产物。千万别把这种结果当真!
学习率适中(100 ~ 500)
这才是 t-SNE 最舒服的工作区间。在这个范围内,梯度更新既不会卡死,也不会跳过关键结构。局部邻域关系被保留,全局分布也相对真实。
4.4 如何选择合适的 learning_rate?
这里我分享一套自己总结的「三步法」:
- 按数据量级定起点:数据点 N < 1000,从 100 开始试;1000 ≤ N ≤ 5000,用默认 200;N > 5000,从 500 开始。
- 看损失曲线调方向:如果损失下降太慢,果断调大;如果损失震荡剧烈,果断调小。
- 对比多次结果:同一个 learning_rate 跑 3 次(设不同 random_state),看嵌入结构是否稳定。结构一致才可信。
下面这段代码展示了如何用不同 learning_rate 做对比实验:
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
learning_rates = [50, 200, 500, 1500]
fig, axes = plt.subplots(1, 4, figsize=(20, 5))
for i, lr in enumerate(learning_rates):
tsne = TSNE(
n_components=2,
learning_rate=lr,
max_iter=1000,
random_state=42
)
X_emb = tsne.fit_transform(X)
axes[i].scatter(X_emb[:, 0], X_emb[:, 1], s=10, alpha=0.6)
axes[i].set_title(f"learning_rate = {lr}")
axes[i].set_xticks([])
axes[i].set_yticks([])
plt.tight_layout()
plt.show()
跑完你就能直观看到:lr=50 时所有点挤在一起;lr=200 和 500 结构清晰且接近;lr=1500 则出现明显的「气泡化」——这基本就是学习率过大的典型特征。
提示:对于大数据集(超过 10000 点),我习惯先对 learning_rate 做一次网格搜索,候选值设为 [200, 500, 800, 1000]。跑一轮下来,哪个值损失下降最平稳就选哪个。
4.5 避坑指南——我踩过的几个雷
踩坑一:以为学习率越大收敛越快
我曾经有一批 5000 点的用户行为数据,图省事把 learning_rate 设到 2000,结果损失曲线像心电图一样狂跳,嵌入结果每跑一次都不一样。后来老老实实降到 500,跑了 3 次,结构就稳定了。
踩坑二:小数据集用了小学习率还不加迭代
有一次处理 200 个样本的测试数据,learning_rate 设成 30,max_iter 没改(默认 1000)。结果跑完一看,损失还在下降,嵌入图完全没成熟。后来把 max_iter 提到 3000 才好一些。记住:小学习率 = 需要更多迭代次数。
踩坑三:用默认值跑所有数据
这是最常见的坑。默认 200 确实普适,但遇到极度稀疏或极度高维的数据,直接套默认值很容易得到一团混沌。我建议每次换新数据集,至少试 3 个不同的 learning_rate,别偷懒。
关键提醒:t-SNE 对 learning_rate 的敏感度,远高于对 perplexity 的敏感度。很多人花大量时间调 perplexity,却直接用默认 learning_rate——这个习惯真的该改改。
看完这张图你应该能直观感受到:learning_rate 不是越大越好,也不是越小越安全。它像一个「油门」,踩得轻了车不动,踩得重了要翻车。找到那个平衡点,t-SNE 才能给你真正有意义的低维结构。
嗯,这一节的内容就到这里。下回你用 t-SNE 的时候,不妨多花几分钟试试不同的 learning_rate——我保证这点时间投入,比你在 perplexity 上折腾半天划算得多。