【數據可視化】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()
複製代碼

參考

相關文章
相關標籤/搜索