遇到了t分布。git
这学期正巧选了一门数据可视化的课程,不过彷佛老师并无打算讲数学原理方面的东西,而纯粹是以工程的角度秀本身以前的成果……所以也不指望从课上学到什么。但仍是在老师的PPT上看到了一些酷炫的数据可视化的图片。一直好奇一些论文中漂亮的散点图是怎么绘制的,好比像下面这种将Fashion MNSIT种的10种衣物一齐展现在一张图上,而且物以类聚,简洁明了。github
以前猜想这种图多是用到了PCA或者Kmeans这些方式,好比先用某个“黑盒子”提取出几种特征,而后再使用PCA筛选最终展现效果规定的维度(好比上面这个例子就是2维),若是使用神经网络的话,彷佛直接用全链接层控制输出的维度就行了。不过关于这种猜想并无真正实践(狗头)。后来在看CS224N的时候,Christopher Manning老师正好提到了t-SNE,因而就开始了这趟递归学习……算法
SNE的全称是stochastic neighbor embedding,并且是Hinton提出的。看到“stochastic”和“embedding”的第一感受就是这个算法必定是某种迭代优化的算法,而不是一种直接映射的方式。网络
算法的目的是将高维数据分布的特色迁移到低维数据上,若是两个点在高维空间靠近的话,那么在映射到低维空间时,这两个点的距离也应当是靠近的。app
先是如何衡量高维上的靠近。最简单的是算出每一对点的距离,不过Hinton并无这么作,而是使用另外一种衡量方式。假设高维上有两个点和点
,定义点i靠近点j的程度为dom
表示以
为中心的高斯分布的方差。 从这个式子能够看出这个式子定义的靠近是几率意义上的,而且这种所谓的靠近并不对称,由于
,就好像KL散度的非对称性同样。实际上以后的损失函数的确用到了KL散度。函数
而后,高维点集X要被映射到低维上要进行可视化了,假设映射关系为。而且定义新点集距离分布为学习
由于这里的分布是将要计算的,所以能够自定义分布的方差,简单起见就为好了,这样式子就不出现
了。优化
将视角切换到点上,点
到其余点的距离分布最终转化为了几率分布,不妨假设其为
;同理,低维空间的点
也拥有一个分布
。如何衡量两个分布的类似程度?KL散度出现了。spa
定义损失函数为
t-SNE是对SNE的改进。首先是 对称问题。由于KL散度是非对称的,KL(P|Q)通常不等于KL(Q|P),为了作到对称,简单的方法就是二者兼顾,即
n表示点的数量。这是高维空间的几率,低维空间的改进是考虑全局距离
和SNE的对比一下,能够发现分母计算了每一对点的距离。
损失函数变为
其次是 数据拥挤 的问题。考虑在n维空间上随机撒点,那么这些点距离原点的距离分布的问题。
import numpy as np
import matplotlib.pyplot as plt
from numpy.linalg import norm
plt.figure(figsize=(30, 4))
for i, dim in enumerate((1, 2, 4, 8, 16, 32)):
# 在dim维空间随机撒点
pts = [np.random.rand(dim) for j in range(1000)]
dst = list(map(lambda x: norm(x), pts))
# 绘图
ax = plt.subplot(1, 6, i + 1)
ax.set_xlabel('distance')
if i == 0:
ax.set_ylabel('count')
ax.hist(dst, bins=np.linspace(0, np.sqrt(dim), 32))
ax.set_title('m={0}'.format(str(dim)), loc='left')
plt.show()
复制代码
能够看出,随机点离原点的距离变化随着维度升高,是趋向于敏感的,高维空间点的分布更加集中。这会给直接降维带来麻烦,若不加处理直接将高维空间距离分布映射到低维空间,那么点会聚成一团;即便使用正则化“拉伸”这些点的距离,效果也不会很明显。
须要注意的是,点是随机撒的,那么不管是高维空间仍是低维空间,空间中点的分布都是大体均匀的,并不存在挤成一团的问题。计算距离也算是一种降维处理,这里就能体现出简单降维处理存在的拥挤问题。
比较理想的处理方式是使用一种映射方式,让高维距离分布拥有低维距离分布“矮胖”的特色,这就用到了t-分布。
t-分布的自由度越低,曲线越矮。若是把高维空间的分布视为正态分布,将要映射的分布视为自由度为1的t-分布,就能够缓解数据拥挤的问题!这样,高维数据距离公式不须要变更,只须要将低维距离公式改变,让其知足t-分布的特色便可。
t-分布这种“矮胖”的身材还能很好的消除 小样本 中存在离群点的缺点。
的公式贴在下面。之因此使用自由度为1的t-分布,是由于这种形式符合“平方反比定律”,可以在低维空间中很好地表示巨多的点对之间的关系(做者说的:))。
衡量两个距离分布类似性仍是使用KL散度。计算能够直接使用梯度计算
最后说说t-SNE算法的缺点吧,其实缺点已经很明显了……和训练词向量同样,当数据量很大的时候,会算得很慢,而且每次训练的结果都是不同的。为了解决速度的问题,有不少人尝试使用“树”结构来加速(好比kd-tree?),还有人使用“负采样”来加速,说到底也是想用小样本估计整体样本,虽然精度可能会出现误差,不过反正训练出来的结果是给人看的,又何须在乎一两个点的偏移呢?
既然本篇是在学习CS224N的过程当中深挖的产物,那么实验环节用词向量降维是再合适不过的了。一般使用的词向量动辄上百维,很是不适合碳基生物的理解,若是须要查看训练以后词向量表达意思的能力,至少须要将其降维到3维空间。
以GLOVE为例,glove.6B.50d
为高维数据,而后要将其投射到3维平面。该数据集有40w条预训练的词向量,每条词向量拥有50个维度。为了能比较清楚地看出词和词之间的关系,使用2维空间可视化这些向量(3维能显示更多的信息,可是显示效果容易受视角的影响)。
训练代码附在后面,训练结果以下:
import copy
import math
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm
# 读取GLOVE预训练词向量
filename = r"D:\data\trained_net\glove.6B.50d.txt"
glove = {}
with open(filename, encoding="utf8") as f:
for line in f.readlines():
line = line.split()
glove[line[0]] = list(map(lambda w: float(w), line[1:]))
# !注意words本身从百度上找,我是用的是四级高频词汇,一共700个!
words = [...]
# 筛选存在GLOVE的词
selected_vecs = {word: glove[word] for word in words if word in glove.keys()}
selected_words = list(selected_vecs.keys())
sigma = [np.var(selected_vecs[w]) for w in selected_words]
n = len(selected_vecs)
# 距离矩阵
_p = np.zeros((n, n))
def vec_distance(word1, word2):
# 计算两个词向量差的距离
vec1 = np.array(selected_vecs[word1])
vec2 = np.array(selected_vecs[word2])
vec = vec1 - vec2
return np.dot(vec, vec)
# 计算高维向量分布矩阵
for i in tqdm(range(n)):
dominator = 0.0 # 计算分母
for k in range(n):
if i == k:
continue
d = vec_distance(selected_words[i], selected_words[k])
dominator += np.exp(-d / (2 * sigma[i]**2))
# 计算p矩阵的项
for j in range(n):
if i == j:
continue
d = vec_distance(selected_words[i], selected_words[j])
_p[i][j] = np.exp(-d / (2 * sigma[i]**2)) / dominator
pass
p = (_p.T + _p[j][i]) / (2 * n)
# 训练低维空间距离
EPOCH = 100
DIM = 2
lr = 1e3
pre_kl = []
window = 5
y = np.random.randn(n, DIM) # 默认是服从N(0,1)分布
def KL(P, Q, epsilon=1e-10):
_P = P + epsilon
return (_P * np.log2(_P / (Q + epsilon))).sum()
for epoch in range(EPOCH):
# 计算分母
dy = y.reshape(n, 1, DIM) - y.reshape(1, n, DIM) # n * n * 3
dominator = (dy**2).sum(axis=2) + 1 # n * n * 1
dominator = 1 / dominator # n * n * 1
dominator_sum = dominator.sum()
# 低维距离矩阵
q = dominator / dominator_sum
# 计算loss
kl = KL(p, q)
print(f"epoch {epoch + 1}/{EPOCH}\tKL = {kl:.6f}\tLR = {lr}")
# 动态调整学习率
if len(pre_kl) > 0:
kl_mean = sum(pre_kl) / len(pre_kl)
if kl_mean > kl:
lr *= 1.2
else:
lr *= 0.5
pre_kl.append(kl)
if len(pre_kl) > window:
pre_kl.pop(0)
d_pq = p - q
# 计算梯度
grad = np.zeros((n, DIM))
for i in range(n):
_grad = 0
for j in range(n):
_grad += d_pq[i][j] * dy[i][j] * dominator[i][j]
grad[i] = _grad * 4
# 更新低维向量
y = y - grad * lr
y -= y.mean(axis=0)
# 词向量展现
SHOW = 150 # 为了防止太过密集,选取部分词向量展现
if DIM == 3:
from mpl_toolkits.mplot3d import Axes3D # 须要有Axes3D才能绘画
fig = plt.figure()
ax = fig.gca(projection='3d')
ax.scatter(y[:SHOW, 0], y[:SHOW, 1], y[:SHOW, 2], s=1)
for i in range(SHOW):
word = selected_words[i]
ax.text(y[i, 0], y[i, 1], y[i, 2], word, fontsize=10)
else:
plt.scatter(y[:SHOW, 0], y[:SHOW, 1], s=1)
for i in range(SHOW):
word = selected_words[i]
plt.text(y[i, 0], y[i, 1], word, fontsize=10)
plt.show()
复制代码