遇到了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()
複製代碼