【数据可视化】SNE

遇到了t分布。git

这学期正巧选了一门数据可视化的课程,不过彷佛老师并无打算讲数学原理方面的东西,而纯粹是以工程的角度秀本身以前的成果……所以也不指望从课上学到什么。但仍是在老师的PPT上看到了一些酷炫的数据可视化的图片。一直好奇一些论文中漂亮的散点图是怎么绘制的,好比像下面这种将Fashion MNSIT种的10种衣物一齐展现在一张图上,而且物以类聚,简洁明了。github

FashonMNIST

以前猜想这种图多是用到了PCA或者Kmeans这些方式,好比先用某个“黑盒子”提取出几种特征,而后再使用PCA筛选最终展现效果规定的维度(好比上面这个例子就是2维),若是使用神经网络的话,彷佛直接用全链接层控制输出的维度就行了。不过关于这种猜想并无真正实践(狗头)。后来在看CS224N的时候,Christopher Manning老师正好提到了t-SNE,因而就开始了这趟递归学习……算法

SNE

SNE的全称是stochastic neighbor embedding,并且是Hinton提出的。看到“stochastic”和“embedding”的第一感受就是这个算法必定是某种迭代优化的算法,而不是一种直接映射的方式。网络

算法的目的是将高维数据分布的特色迁移到低维数据上,若是两个点在高维空间靠近的话,那么在映射到低维空间时,这两个点的距离也应当是靠近的。app

先是如何衡量高维上的靠近。最简单的是算出每一对点的距离,不过Hinton并无这么作,而是使用另外一种衡量方式。假设高维上有两个点x_i和点x_j,定义点i靠近点j的程度为dom

p_{j|i} = \frac{exp(-\left\|x_i-x_j\right\|^2/2\sigma_i^2)}{\sum_{k \neq i}{exp(-\left\|x_i-x_k\right\|^2/2\sigma_i^2)}}

\sigma_i表示以x_i为中心的高斯分布的方差。 从这个式子能够看出这个式子定义的靠近是几率意义上的,而且这种所谓的靠近并不对称,由于p_{j|i} \neq p_{i|j},就好像KL散度的非对称性同样。实际上以后的损失函数的确用到了KL散度。函数

而后,高维点集X要被映射到低维上要进行可视化了,假设映射关系为x_i \rightarrow y_i。而且定义新点集距离分布为学习

q_{j|i} = \frac{exp(-\left\|y_i-y_j\right\|^2)}{\sum_{k \neq i}{exp(-\left\|y_i-y_k\right\|^2)}}

由于这里的分布是将要计算的,所以能够自定义分布的方差,简单起见就为1/\sqrt{2}好了,这样式子就不出现\sigma了。优化

将视角切换到点x_i上,点x_i到其余点的距离分布最终转化为了几率分布,不妨假设其为P_i;同理,低维空间的点y_i也拥有一个分布Q_i。如何衡量两个分布的类似程度?KL散度出现了。spa

定义损失函数为

C = \sum_{i}KL(P_i|Q_i) = \sum_{i}{\sum_{j}{p_{j|i}\log{\frac{p_{j|i}}{q_{j|i}}}}}

t-SNE

t-SNE是对SNE的改进。首先是 对称问题。由于KL散度是非对称的,KL(P|Q)通常不等于KL(Q|P),为了作到对称,简单的方法就是二者兼顾,即

p_{ij} = \frac{p_{j|i} + p_{i|j}}{2n}

n表示点的数量。这是高维空间的几率,低维空间的改进是考虑全局距离

q_{ij} = \frac{exp(-\left\|y_i-y_j\right\|^2)}{\sum_{k \neq l}{exp(-\left\|y_k-y_l\right\|^2)}}

和SNE的q_{j|i}对比一下,能够发现分母计算了每一对点的距离。

损失函数变为

C = KL(P|Q) = \sum_{i}{\sum_{j}{p_{ij}\log{\frac{p_{ij}}{q_{ij}}}}}

其次是 数据拥挤 的问题。考虑在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()
复制代码

distribution

能够看出,随机点离原点的距离变化随着维度升高,是趋向于敏感的,高维空间点的分布更加集中。这会给直接降维带来麻烦,若不加处理直接将高维空间距离分布映射到低维空间,那么点会聚成一团;即便使用正则化“拉伸”这些点的距离,效果也不会很明显。

须要注意的是,点是随机撒的,那么不管是高维空间仍是低维空间,空间中点的分布都是大体均匀的,并不存在挤成一团的问题。计算距离也算是一种降维处理,这里就能体现出简单降维处理存在的拥挤问题。

比较理想的处理方式是使用一种映射方式,让高维距离分布拥有低维距离分布“矮胖”的特色,这就用到了t-分布。

t-分布的自由度越低,曲线越矮。若是把高维空间的分布视为正态分布,将要映射的分布视为自由度为1的t-分布,就能够缓解数据拥挤的问题!这样,高维数据距离公式不须要变更,只须要将低维距离公式改变,让其知足t-分布的特色便可。

t-分布这种“矮胖”的身材还能很好的消除 小样本 中存在离群点的缺点。

q_{ij}的公式贴在下面。之因此使用自由度为1的t-分布,是由于这种形式符合“平方反比定律”,可以在低维空间中很好地表示巨多的点对之间的关系(做者说的:))。

q_{ij} = \frac{(1 + \left\|y_i-y_j \right\|^2)^{-1}}{\sum_{k \neq l}{(1 + \left\|y_k-y_l \right\|^2)^{-1}}}

衡量两个距离分布类似性仍是使用KL散度。计算能够直接使用梯度计算

\frac{\partial C}{\partial y_i} = 4 \sum_{j}{(p_{ij} - q_{ij})(y_i - y_j)(1 + \left\|y_i - y_j\right\|^2)^{-1}}

最后说说t-SNE算法的缺点吧,其实缺点已经很明显了……和训练词向量同样,当数据量很大的时候,会算得很慢,而且每次训练的结果都是不同的。为了解决速度的问题,有不少人尝试使用“树”结构来加速(好比kd-tree?),还有人使用“负采样”来加速,说到底也是想用小样本估计整体样本,虽然精度可能会出现误差,不过反正训练出来的结果是给人看的,又何须在乎一两个点的偏移呢?

词向量降维可视化

既然本篇是在学习CS224N的过程当中深挖的产物,那么实验环节用词向量降维是再合适不过的了。一般使用的词向量动辄上百维,很是不适合碳基生物的理解,若是须要查看训练以后词向量表达意思的能力,至少须要将其降维到3维空间。

以GLOVE为例,glove.6B.50d为高维数据,而后要将其投射到3维平面。该数据集有40w条预训练的词向量,每条词向量拥有50个维度。为了能比较清楚地看出词和词之间的关系,使用2维空间可视化这些向量(3维能显示更多的信息,可是显示效果容易受视角的影响)。

训练代码附在后面,训练结果以下:

t_SNE

代码

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()
复制代码

参考

相关文章
相关标签/搜索