無監督學習(2) 數據降維簡述與Python實現

爲什麼要數據降維

大數據時代面臨的最大問題是"維度災難",度量上的不平衡和高維空間的學習複雜度都讓機器學習算法在高維數據上很多時候行不通。而且,如果數據超過三維,它們也很難被可視化。不能被可視化的數據是很難理解的。
如果我們的數據比較高維,不適合直接進行監督或聚類學習,則我們可以先用一些其他的無監督或有監督的技巧把數據進行降維。這一系列的方法也常常被叫做度量學習。
通過有監督或者無監督的方法,學習在原始的座標距之上的距離評估方法,就是度量學習。度量學習可以爲KNN和聚類服務,有時這些方法也可以被直接用在數據預處理上,而且一般表現都不錯。我們首先介紹的是PCA,主成分分析;

PCA

PCA使用一個線性變換投影來得到新的座標空間,也就是把原向量空間裏的數據x乘上一個W矩陣,變換到另一個向量空間。PCA的目的就是確定W應該是怎樣的W;考慮我們希望的是讓被處理後的數據被分的儘可能開,也就是讓投影后的數據點的方差最大化,問題就簡化爲了最大化協方差矩陣的跡。如果投影后的新向量是
WTx W^Tx
那麼協方差矩陣就是
WT(xx^)(xx^)TW W^T(x-\hat{x})(x-\hat{x})^TW
因爲W^T是投影矩陣,新的座標基應該滿足兩兩正交條件,還需要有約束條件
WTW=I W^TW = I
PCA有意思就有意思在下面的步驟,我們知道如果是約束優化問題,可以用拉格朗日乘子法來解。給上面的約束條件使用拉格朗日乘子法添上一個λ\lambda,就變成
WT(xx^)(xx^)TW=λ(WTWI) W^T(x-\hat{x})(x-\hat{x})^TW=\lambda (W^TW - I)
再計算偏導等於零,原式變爲
(xx^)(xx^)TW=λW (x-\hat{x})(x-\hat{x})^TW=\lambda W
這是特徵值分解的形式!特徵值分解找到的特徵值和特徵向量對有多個,也就是滿足約束條件的解有多個。對W的一個向量w,我們希望最大化的目標函數是
wT(xx^).(xx^)T.w=wTwλ=λ w^T(x-\hat{x}).(x-\hat{x})^T.w=w^Tw\lambda=\lambda
就等於λ\lambda,最大的λ\lambda對應最好的約束優化問題的解;好了,現在拿到數據,我們把數據做個標準化,讓x^\hat x=0,然後令S = XTXX^TX,對S做特徵值分解,在得到的λ\lambda中選k個最大的對應的特徵向量,就得到W矩陣,做W點乘X就能把數據降維到k維。算法就結束啦。下面的代碼也可以看到,只需要幾行就可以實現。

def PCA(X,dim):
    #中心化
    xmean = np.mean(X,axis=0)
    X=deepcopy(X-xmean)
    #協方差矩陣
    Covs = X.T.dot(X)
    lamda,V=np.linalg.eigh(Covs)
    #取前dim個最大的特徵值對應的特徵向量
    index=np.argsort(-lamda)[:dim]
    V_selected=V[:,index]
    return V_selected

我們可以試試在Iris上的效果

X,y = datasets.load_iris(return_X_y=True)

W = PCA(X,2)
X_ = X.dot(W)
plt.scatter(X_[:,0], X_[:,1],edgecolors='black',c=y)

在這裏插入圖片描述

LDA

PCA並不總能把事情做的很好,因爲PCA盲目地把數據映射到了最能"平鋪"的空間。如果我們想完成二分類任務,我們的數據集像油條的兩根那樣平行地排布在一起,而且又被拉長,則PCA只會把油條平放在桌子上,而我們希望油條被豎直地立在桌子上,這樣才能更好地區分兩個類別。
爲此需要引入有標籤的線性降維學習方法LDA,其實思想是和PCA完全一致,但現在我們希望最小化類內方差,最大化類間方差。事實上這部分內容在講線性模型時已經講過了,我們這裏複習一下。
首先定義類間距離和類內距離,類間距就是兩個類中心的距離,類內距就是所有數據點到類中心的距離均值
J0=((μ1μ0)W)T((μ1μ0)W)=WT(μ1μ0)T(μ1μ0)WJ_0=((\mu_1-\mu_0)W)^T((\mu_1-\mu_0)W)=W^T(\mu_1-\mu_0)^T(\mu_1-\mu_0)W
J1=((Xμ)W)T((Xμ)W)=WT(Xμ)T(Xμ)WJ_1=((X-\mu)W)^T((X-\mu)W)=W^T(X-\mu)^T(X-\mu)W
我們設S1=(Xμ)T(Xμ),S0=(μ1μ0)T(μ1μ0)S_1=(X-\mu)^T(X-\mu),S_0 = (\mu_1-\mu_0)^T(\mu_1-\mu_0)
有了這兩個量就可以自己定義損失函數了,一種能保證數據規模不會影響loss的方法是設J1=1,最大化J0。即J=WTS0Ws.t.WTS1W=1J=W^TS_0W \quad s.t.\quad W^TS_1W=1
這個問題直接用拉格朗日乘子法就能求解,寫出拉格朗日函數
L(W,λ)=WTS0Wλ(WTS1W1) L(W,\lambda)=W^TS_0W-\lambda (W^TS_1W-1)
計算偏導並讓它等於0,就得到極值的必要條件
LW=2WTS0W2λWTS1=0 \frac{\partial{L}} {\partial{W}}=2W^TS_0W-2\lambda W^TS_1=0
S0W=λS1W S_0W=\lambda S_1W
S11S0W=λW S_1^{-1}S_0W=\lambda W
即W是最優解時上式一定成立,從上式我們能逐步推導出
S0W=λS1W S_0 W = \lambda S_1 W
WTS0W=λWTS1W=λ=J W^TS_0W = \lambda W^TS_1W = \lambda = J
目標函數和λ\lambda相等。我們發現還是類似PCA的特徵值分解。因爲我們要最大化目標函數,我們取S11S0S_1^{-1}S_0最大的特徵向量,就得到了最優解W。如果我們取前d個最大的特徵向量,就能實現從原數據域降維到d維的線性變換矩陣。

def LDA(X,y,dim):
    '''
    接收數據特徵X和標籤y,需要X爲NxM的二維numpy array
    y爲數值爲0-1的一維numpy array
    '''
    # 分爲正負樣本
    X_1 = X[np.where(y==1)]
    X_0 = X[np.where(y==0)]
    # 計算均值
    mu1 = np.mean(X_1,axis = 0)
    mu0 = np.mean(X_0,axis = 0)
    # 類內散度
    S1 = (X_1-mu1).T.dot((X_1-mu1))
    # 類間散度
    S0 = (mu1-mu0).reshape(-1,1).dot((mu1-mu0).reshape(1,-1))
    # 特徵值分解
    S = np.linalg.inv(S1).dot(S0)
    S += np.eye(len(S))*0.001
    lamda,V=np.linalg.eigh(S)
    #取前dim個最大的特徵值對應的特徵向量
    index=np.argsort(-lamda)[:dim]
    V_selected=V[:,index]
    return V_selected,S0,S1

按照我們上面說的"油條數據集",我們自己定義一個三維的數據集來驗證LDA和PCA的區別。

n = 30

X0 = 0.1*np.ones(n)
X1 = X0*2 + 0.2
X0 = np.concatenate((X0+np.random.rand(n),X0+np.random.rand(n)))
X1 = np.concatenate((X1+np.random.rand(n),X1+np.random.rand(n)))

X2_1 = X0[:n] * 0.9 + X1[:n] ** (-0.3) + 0.2
X2_2 = X0[n:] * 0.9 + X1[n:] ** (-0.3) - 0.2


X2 = np.concatenate((X2_1,X2_2))

X = np.concatenate((X0[None],X1[None],X2[None]),axis = 0).T
y = np.concatenate((np.zeros(n),np.ones(n)))

W = PCA(X,2)
X_ = X.dot(W)
plt.scatter(X_[:,0], X_[:,1],edgecolors='black',c=y)

W,S0,S1 = LDA(X,y,2)
X_ = X.dot(W)
plt.scatter(X_[:,0], X_[:,1],edgecolors='black',c=y)

在這裏插入圖片描述

在這裏插入圖片描述
LDA能做到PCA做不到的事情。

Auto Encoder

自編碼器是基於PCA和神經網絡的思想,我們把PCA的線性變化矩陣和神經網絡的非線性激勵函數疊起來,就能實現非線性降維任務。自編碼器把輸入當作神經網絡的輸出,中間的隱層需要至少一層的神經元個數少於輸入層,這樣就能在這一層得到神經網絡自動降維後的結果。
事實上,今天自編碼器擔任的不僅僅是降維的角色,相當多的研究者在使用這種模型做更多有意思的事情。比如用特殊結構的AE做字典學習,用變分的AE做生成器等等。這裏我們拿pytorch的神經網絡模型來實現一個自編碼器。

import torch
import torch.nn as nn

class AutoEncoder(nn.Module):
    def __init__(self):
        super(AutoEncoder, self).__init__()

        # 壓縮
        self.encoder = nn.Sequential(
            nn.Linear(4,3),
            nn.Tanh(),
            nn.Linear(3,2),
            nn.Tanh(),
        )
        # 解壓
        self.decoder = nn.Sequential(
            nn.Linear(2,3),
            nn.Tanh(),
            nn.Linear(3,4)
        )

    def forward(self, x):
        encoded = self.encoder(x)
        decoded = self.decoder(encoded)
        return encoded, decoded

autoencoder = AutoEncoder()

from torch.utils.data import DataLoader,TensorDataset

X,y = datasets.load_iris(return_X_y=True)
X_train = torch.Tensor(X)
y_train = torch.Tensor(y)

myset = TensorDataset(X_train,y_train)
myloader = DataLoader(dataset=myset, batch_size=10, shuffle=True)

optimizer = torch.optim.Adam(autoencoder.parameters(), lr=0.002)
loss_func = nn.MSELoss()
 
for epoch in range(1000):
    Loss = 0
    for step, (X_, y_) in enumerate(myloader):
        encoded, decoded = autoencoder(X_)
        loss = loss_func(decoded, X_)
        optimizer.zero_grad()
        loss.backward()
        Loss += loss.item()
        optimizer.step()
    Loss /= (step+1)
    if (epoch+1)%100==0:
        print("epoch %d, loss %.2f"%(epoch+1,Loss))



epoch 100, loss 0.44
epoch 200, loss 0.14
epoch 300, loss 0.11
epoch 400, loss 0.10
epoch 500, loss 0.09
epoch 600, loss 0.09
epoch 700, loss 0.04
epoch 800, loss 0.04
epoch 900, loss 0.03
epoch 1000, loss 0.03
# 看一看結果如何
X_red,_ = autoencoder(X_train)
X_red = X_red.detach().numpy()

plt.scatter(X_red[:,0], X_red[:,1],edgecolors='black',c=y)

在這裏插入圖片描述
如果不設置激活函數,只設置兩個線性變化矩陣的話,會發現得到的結果實際上和PCA得到的結果非常類似。PCA比起單隱層線性自編碼器,只是多了一個正交的限制條件,而且輸入的線性變化矩陣和輸出的矩陣互爲轉置。這也就決定了線性自編碼器得到的結果會和PCA很相似,但是並不會比PCA更好。

更好的降維算法

上面的算法是比較快速,比較簡單的降維算法。但是在實際使用時,我們經常發現,因爲LDA和PCA不能實現非線性降維,它們在實際場景中不總能取得好的效果。而AE雖然能實現非線性,但它需要一段可能不短的訓練時間開銷,而且它給出的解並不穩定(會根據初始化的值變化)。我們也許需要更好的,既能實現非線性,又能學習流形,還能保證運行速度的算法。

LLE

首先講一下基於線性近鄰的降維學習方法,這個方法叫Locally Linear的Embeddding。雖然它的名字是linear,但其實實現的是非線性的降維。爲什麼叫Locally Linear呢?這個名字就是算法的精髓,我們對原數據降維後,保有的是樣本點和它附近的k個樣本點的固有線性關係。如果原空間的x0附近的3近鄰是x1,x2,x3,而且原空間中有x0 = 0.5x1+0.25x2+0.25x3的線性關係,那麼我們希望降維後,這個關係仍然存在。
那麼,算法最首要的任務就是求解每個數據點的k近鄰,並計算上面的線性組合權重向量w。事實上,k近鄰的線性組合並不保證能完全等於數據點x,所以我們實質上要解的是一個優化問題。寫出優化問題,如下式(x默認爲行向量)
minimizei=1MxijQiwijxj2 minimize \qquad \sum_{i=1}^M ||x_i-\sum_{j\in Q_i}w_{ij}x_j||^2
如果我們把所有k近鄰的xj行向量在列方向上排列成矩陣,wi是樣本點xi對於的權重向量,則上式有矩陣代數形式
minimizei=1MxiwiXQi2 minimize \qquad \sum_{i=1}^M ||x_i-w_i X_{Q_i}||^2
求和號中的每項都是獨立的優化問題,我們對其中任意i的一項求偏導爲0
(wiXQixi)XQiT=0 (w_i X_{Q_i}-x_i)X_{Q_i}^T = 0
wi=xiXQiT(XQiXQiT)1 w_i = x_i X_{Q_i}^T(X_{Q_i}X_{Q_i}^T)^{-1}
按照上式可以解出所有的wijw_{ij}(不屬於k近鄰的jj權重wijw_{ij}就設爲0),然後我們的問題就變成了從W矩陣重構一個低維的數據集矩陣Z,其中ziz_{i}是Z矩陣的第i行,表示原數據點xix_i降維後的結果。我們的設降維後的Z高爲n,寬爲d,即降維的目標維度。優化問題和上面的形式實質上是類似的,不過我們現在希望求解Z
minimizei=1MziwiZ2 minimize \qquad \sum_{i=1}^M ||z_i-w_i Z||^2
同樣可以寫成更代數的形式
minimizetr[(ZWZ)T(ZWZ)]=tr[ZT(IW)T(IW)Z] minimize \qquad tr[(Z- WZ)^T(Z- WZ)] = tr[Z^T(I- W)^T(I- W)Z]
如果不加約束,上式解出來的Z只會非常非常小,甚至等於0.爲了得到有效的解我們還要加一些約束
s.t.ZTZ=I s.t.\qquad Z^TZ = I
然後這個形式就和PCA類似了,我們對M=(IW)T(IW)M=(I- W)^T(I- W)做特徵值分解,並取最小的特徵值,就得到了Z,降維後的數據。
L(Z)=tr[ZTMZ]λ(ZTZI) L(Z) = tr[Z^TMZ]-\lambda (Z^TZ - I)
LZ=MZλZ=0\frac{\partial{L}}{\partial{Z}} = MZ-\lambda Z = 0
Mzi=λzi M z_i = \lambda z_i
又是特徵值分解,特徵值分解無處不在。

def dist(x1,x2):
    '''
    x1,x2: numpy array, shape1 = (m,), shape2 = (m,)
    表示維度都爲m的兩個向量
    return: D,int, 表示數據點差異的L2範數
    '''
    return np.sum((x1-x2)**2)

def LLE(X, k, d):
    '''
    X: numpy array, shape = (n,m),表示n個m維的數據點組成的數據集
    k: LLE使用k近鄰,不得大於n-1
    d: 目標降維維度,小於m大於0
    return: Z,numpy array, shape = (n,d),表示n個d維的數據點組成的數據集
    '''
    n,m = X.shape
    D = np.zeros((n,n))
    for i in range(n):
        for j in range(n):
            D[i][j] = dist(X[i],X[j])     #得到L2的距離矩陣
    
    knn = []
    for i in range(n):
        knn.append(np.argsort(D[i])[1:k+1]) #得到每個點的k近鄰
    
    W = np.zeros((n,n))
    for i in range(n):
        XQ = X[knn[i]]                # K近鄰矩陣
        A = XQ.dot(XQ.T)
        A+=np.eye(k)*1e-3*np.trace(XQ)  #magic,保證正定
        W[i][knn[i]] = X[i].dot(XQ.T).dot(np.linalg.pinv(A))
        # 得到局部線性嵌入權重矩陣
    
    M = (np.eye(n)-W).T.dot((np.eye(n)-W))  # 計算M矩陣
    lamda,V=np.linalg.eigh(M)
    # 取前dim個最小的特徵值對應的特徵向量
    index=np.argsort(lamda)[:d]
    V_selected=V[:,index]
    return V_selected

我們在Iris和瑞士捲數據上測試這個算法。

X,y = datasets.load_iris(return_X_y=True)

Z = LLE(X,25,2)
plt.scatter(Z[:,0], Z[:,1],edgecolors='black',c=y)

在這裏插入圖片描述

import matplotlib as mpl
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import proj3d
#Generate mainfold data set
from sklearn.datasets import make_swiss_roll
X, t = make_swiss_roll(n_samples=1000, noise=0, random_state=0)
axes = [-11.5, 14, -2, 23, -12, 15]
#plot figure
fig = plt.figure(figsize=(6, 5))
plt.title("old_data", fontsize=14)
ax = fig.add_subplot(111, projection='3d')
ax.scatter(X[:, 0], X[:, 1], X[:, 2], c=t, cmap=plt.cm.hot)
ax.view_init(10, -70)
ax.set_xlabel("$x_1$", fontsize=18)
ax.set_ylabel("$x_2$", fontsize=18)
ax.set_zlabel("$x_3$", fontsize=18)
ax.set_xlim(axes[0:2])
ax.set_ylim(axes[2:4])
ax.set_zlim(axes[4:6])
plt.show()

在這裏插入圖片描述

Z = LLE(X,k=6,d=2)
plt.scatter(Z[:, 0], Z[:, 1], c=t, cmap=plt.cm.hot)
plt.show()

在這裏插入圖片描述
LLE對參數非常敏感,在使用時要進行細緻的調參。

Isomap

上面的LLE算法,劣勢在於我們用於重建Z的W矩陣,只包含近鄰點的信息;很直覺地可以想到,如果上級算法不提供給我們它與其他非近鄰點的關係信息,算法很顯然不能穩定地給出近鄰的點仍然近鄰,遠離的點仍然遠離的結果。
以上面的流形問題爲例,我們在這種模型中希望用測地線距離替換原空間的歐拉距離。修過圖論或者數據結構的都知道,如果我們把數據點看做圖中的頂點,則我們是有比較高效的算法計算任意兩點間在圖中的最短距離的(Floyd or dijkstra)。這樣我們就得到了一種能近似測地線距離的新距離矩陣。這個矩陣提供了哪些點離得近,哪些點離得遠的全部信息。

MDS

從距離矩陣重構數據點信息需要MDS算法。首先我們認爲這個距離矩陣在重構後的空間Z中表現出的是兩點間的歐拉距離,如果我們設bij=ziTzjb_{ij} = z_i^Tz_j,限制Z,認爲Z的均值爲0。就有distij2=zi2+zj22ziTzj=bii+bjj2bijdist_{ij}^2 = ||z_i||^2+||z_j||^2-2z_i^Tz_j = b_{ii}+b_{jj}-2b_{ij}i=1nbij=j=1nbij=0\sum_{i=1}^n b_{ij} = \sum_{j=1}^n b_{ij} = 0。則如果對上面的式子做類似"邊緣積分"的運算,能得到一些有用的結論。
i=1ndistij2=tr(B)+nbjj \sum_{i=1}^ndist_{ij}^2 = tr(B)+nb_{jj}
j=1ndistij2=tr(B)+nbii \sum_{j=1}^ndist_{ij}^2 = tr(B)+nb_{ii}
i=1ndistij2j=1n=2ntr(B) \sum_{i=1}^ndist_{ij}^2\sum_{j=1}^n = 2ntr(B)
從這三個結論我們可以反推出,具有中心化性質的Z矩陣對應的內積矩陣B
bij=12(distij21nj=1ndistij21ni=1ndistij2+1n2i=1nj=1ndistij2 b_{ij} = -\frac{1}{2}(dist_{ij}^2-\frac{1}{n}\sum_{j=1}^n dist_{ij}^2-\frac{1}{n}\sum_{i=1}^n dist_{ij}^2+\frac{1}{n^2}\sum_{i=1}^n\sum_{j=1}^n dist_{ij}^2
內積矩陣是B=ZZTB = ZZ^T,可以用特徵值分解來解Z。B=VΛVTB = V\Lambda V^T,則Z=VΛ12Z = V \Lambda^{\frac{1}{2}}。我們取前d個特徵向量和特徵值,就得到了降維後的Z矩陣。

import heapq

def MDS(D,dim):
    m = len(D)
    disti_ = np.zeros(m)
    dist_j = np.zeros(m)
    dist__ = 0
    for i in range(m):
        disti_[i]=np.mean(D[i,:])
    for j in range(m):
        dist_j[j]=np.mean(D[:,j])
    dist__ = np.mean(D)
    B = np.copy(D)
    for i in range(m):
        for j in range(m):
            B[i][j] += (-disti_[i]-dist_j[j]+dist__)
            B[i][j] *= -0.5
    vals, vecs = np.linalg.eig(B)
    lamda,V=np.linalg.eigh(B)
    index=np.argsort(-lamda)[:dim]
    diag_lamda=np.sqrt(np.diag(-np.sort(-lamda)[:dim]))
    V_selected=V[:,index]
    Z=V_selected.dot(diag_lamda)
    return Z

def Isomap(X, k, dim):
    N = len(X)
    D = np.zeros((N,N))
    for i in range(N):
        for j in range(i+1,N):
            D[i][j] = dist(X[i],X[j])**0.5
            D[j][i] = D[i][j]
    
    bound = D.max()
    Map = np.ones((N,N))*1000
    n,m = X.shape
    
    for i in range(n):
        knn = np.argsort(D[i])[1:k+1] #得到每個點的k近鄰
        Map[i,knn] = D[i,knn]
        Map[knn,i] = D[knn,i]
    #Floyd算法
    for k in range(N):
        for i in range(N):
            for j in range(N):
                Map[i][j] = min(Map[i][k]+Map[k][j],Map[i][j])
    
    
    Map[np.where(Map==1000)] = bound
    return MDS(Map,dim)
X, t = make_swiss_roll(n_samples=300, noise=0, random_state=0)
Z = Isomap(X,k=4,dim=2)
plt.scatter(Z[:, 0], Z[:, 1], c=t, cmap=plt.cm.hot)
plt.show()

在這裏插入圖片描述
雖然Isomap一般能比較完美地學習流形,但Isomap的劣勢也非常明顯,它必須花費O(n3)O(n^3)的時間開銷去計算距離矩陣,這比一般算法的O(n2)O(n^2)要可怕很多。即使是在上面的1000級別的瑞士捲樣本上跑一次都要花費相當多的時間。儘管我們可以用一些近似算法來做一些加速,但是這仍然不是一個高效的算法。

SNE

stochastic neighbor embedding,事實上我們從上面的例子可以看出來,LLE解決的問題是"把原本就靠近的數據點相互靠近",這可以在原始數據分佈性質比較好,分佈比較開的數據集上表現得不錯。但是如果原始數據集的分佈性質一般,像是上面的流形,算法的確會把近似的點都放在一起,但是並不保證不相似的點能分開。雖然LLE經過細緻的調參可以在上面的流形跑出還不錯的結果,但是這並非我們想要的。
TSNE一定程度上解決的就是這個問題,它使用了概率和近似的方法,一般能得到高維到低維的比較好的效果。它也是當前最常被用作可視化工具的一種方法。
算法
這個算法基於距離,給出樣本點的相似度概念;這個相似度還會被歸一化成概率形式。
P(xjxi)=S(xi,xj)kiS(xi,xk) P(x_j|x_i) = \frac{S(x_i,x_j)}{\sum_{k\neq i}S(x_i,x_k)}
P(zjzi)=S(zi,zj)kiS(zi,zk) P(z_j|z_i) = \frac{S'(z_i,z_j)}{\sum_{k\neq i}S'(z_i,z_k)}
算法的優化目標是兩個概率分佈的KL散度
L=iKL(P(xi)Q(zi))=ijP(xjxi)logP(xjxi)Q(zjzi) L = \sum_i KL(P(*|x_i)||Q(*|z_i)) = \sum_i\sum_j P(x_j|x_i)log\frac{P(x_j|x_i)}{Q(z_j|z_i)}
SNE在相似度上使用的分佈函數是高斯函數。
P(xjxi)=exp(xixj2/2σi2)kiexp(xixk2/2σi2) P(x_j|x_i) = \frac{exp(-||x_i-x_j||^2/2\sigma_i^2)}{\sum_{k\neq i}exp(-||x_i-x_k||^2/2\sigma_i^2)}
想執行算法可能還需要先確定每個i對應的方差(高斯核帶寬),這個過程也是有方法做的。原始論文用的是perplexity的定義。我們用二分法找一個困惑度最小的方差
Perp(Pi)=2H(Pi)Perp(P_i) = 2^{H(P_i)}
H(Pi)=jpjilog2pji H(P_i) = -\sum_j p_{j|i}log_2 p_{j|i}
但是實際使用的時候,一般會在轉換前的高維空間進行搜索,對低維直接用常數。確定了方差,一切就都可以計算了,我們可以用梯度下降來解這個問題
Lyi=2j(pjiqji+pijqij)(yiyj) \frac{\partial L} {\partial y_i} = 2\sum_j(p_{j|i}-q_{j|i}+p_{i|j}-q{i|j})(y_i-y_j)

t-SNE

SNE的缺點是不容易優化,Hinton等人在08年提出了t-SNE這種變形。主要改動了以下兩點。

  1. t-SNE在相似度上使用的分佈函數是高斯函數和學生氏分佈(t分佈)。
    pij=exp(xixj2/2σi2)kiexp(xkxi2/2σi2) p_{i|j} = \frac{exp(-||x_i-x_j||^2/2\sigma_i^2)}{\sum_{k\neq i}exp(-||x_k-x_i||^2/2\sigma_i^2)}
    qij=(1+xixj2)1ki(1+xkxi2)1 q_{i|j} = \frac{(1+||x_i-x_j||^{2})^{-1}}{\sum_{k\neq i}(1+||x_k-x_i||^{2})^{-1}}
    因爲t分佈更有長期性,在新空間中可以表現得更好。
  2. 這裏的分佈不再是條件概率分佈,而是聯合概率分佈。同時提出假設,假設pij=pji。另外,爲了柔和化異常點的影響,我們使用修正的概率計算梯度。
    pij=(pij+pji)/2 p_{ij} = (p_{i|j}+p_{j|i})/2
    這時的梯度也有更爲簡單的形式。
    Lyi=4j(pijqij)(yiyj)(1+yiyj2)1 \frac{\partial L} {\partial y_i} = 4\sum_j(p_{ij}-q_{ij})(y_i-y_j)(1+||y_i-y_j||^2)^{-1}
    這裏貼一篇講的比較好的博客:click.
def cal_pairwise_dist(x):
    '''計算pairwise 距離, x是matrix
      (a-b)^2 = a^2 + b^2 - 2*a*b
    '''
    sum_x = np.sum(np.square(x), 1)
    dist = np.add(np.add(-2 * np.dot(x, x.T), sum_x).T, sum_x)
    #返回任意兩個點之間距離的平方
    return dist

# 計算困惑度,最終會選擇合適的beta,也就是每個點的方差啦
def cal_perplexity(dist, idx=0, beta=1.0):
    # '''計算perplexity, D是距離向量,
    # idx指dist中自己與自己距離的位置,beta是高斯分佈參數
    # 這裏的perp僅計算了熵,方便計算
    # '''
    prob = np.exp(-dist * beta)
    # 設置自身prob爲0
    prob[idx] = 0
    sum_prob = np.sum(prob)
    if sum_prob == 0:
        prob = np.maximum(prob, 1e-12)
        perp = -12
    else:
        prob /= sum_prob
        perp = 0
        for pj in prob:
            if pj != 0:
                perp += -pj*np.log(pj)
    # 困惑度和pi\j的概率分佈
    return perp, prob


def seach_prob(x, tol=1e-5, perplexity=30.0):
    # '''二分搜索尋找beta,並計算pairwise的prob
    # '''
    # 初始化參數
    print("Computing pairwise distances...")
    (n, d) = x.shape
    dist = cal_pairwise_dist(x)
    pair_prob = np.zeros((n, n))
    beta = np.ones((n, 1))
    # 取log,方便後續計算
    base_perp = np.log(perplexity)

    for i in range(n):
        if i % 500 == 0:
            print("Computing pair_prob for point %s of %s ..." %(i,n))

        betamin = -np.inf
        betamax = np.inf
        #dist[i]需要換不能是所有點
        perp, this_prob = cal_perplexity(dist[i], i, beta[i])

        # 二分搜索,尋找最佳sigma下的prob
        perp_diff = perp - base_perp
        tries = 0
        while np.abs(perp_diff) > tol and tries < 50:
            if perp_diff > 0:
                betamin = beta[i].copy()
                if betamax == np.inf or betamax == -np.inf:
                    beta[i] = beta[i] * 2
                else:
                    beta[i] = (beta[i] + betamax) / 2
            else:
                betamax = beta[i].copy()
                if betamin == np.inf or betamin == -np.inf:
                    beta[i] = beta[i] / 2
                else:
                    beta[i] = (beta[i] + betamin) / 2

            # 更新perb,prob值
            perp, this_prob = cal_perplexity(dist[i], i, beta[i])
            perp_diff = perp - base_perp
            tries = tries + 1
        # 記錄prob值
        pair_prob[i,] = this_prob
    print("Mean value of sigma: ", np.mean(np.sqrt(1 / beta)))
    #每個點對其他點的條件概率分佈pi\j
    return pair_prob

def tsne(x, no_dims=2, initial_dims=50, perplexity=30.0, max_iter=800):
    """Runs t-SNE on the dataset in the NxD array x
    to reduce its dimensionality to no_dims dimensions.
    The syntaxis of the function is Y = tsne.tsne(x, no_dims, perplexity),
    where x is an NxD NumPy array.
    """

    # Check inputs
    if isinstance(no_dims, float):
        print("Error: array x should have type float.")
        return -1
    if round(no_dims) != no_dims:
        print("Error: number of dimensions should be an integer.")
        return -1

    (n, d) = x.shape
    print (x.shape)

    #動量
    lr = 500
    # 隨機初始化Y
    y = np.random.randn(n, no_dims)
    # dy梯度
    dy = np.zeros((n, no_dims))
    # 對稱化
    P = seach_prob(x, 1e-5, perplexity)
    P = P + np.transpose(P)
    P = P / np.sum(P)   #pij
    # early exaggeration
    # pi\j
    P = P * 4
    P = np.maximum(P, 1e-12)

    # Run iterations
    for iter in range(max_iter):
        # Compute pairwise affinities
        sum_y = np.sum(np.square(y), 1)
        num = 1 / (1 + np.add(np.add(-2 * np.dot(y, y.T), sum_y).T, sum_y))
        num[range(n), range(n)] = 0
        Q = num / np.sum(num)   #qij
        Q = np.maximum(Q, 1e-12)    #X與Y逐位比較取其大者

        # Compute gradient
        #pij-qij
        PQ = P - Q
        #梯度dy
        for i in range(n):
            dy[i,:] = np.sum(np.tile(PQ[:,i] * num[:,i], (no_dims, 1)).T * (y[i,:] - y), 0)
        
        # 更新y
        y = y - lr*dy

        # 減去均值
        y = y - np.tile(np.mean(y, 0), (n, 1))
        # Compute current value of cost function
        if (iter + 1) % 50 == 0:
            if iter > 100:
                C = np.sum(P * np.log(P / Q))
            else:
                C = np.sum( P/4 * np.log( P/4 / Q))
            print("Iteration ", (iter + 1), ": error is ", C)
        # Stop lying about P-values
        if iter == 100:
            P = P / 4
    print("finished training!")
    return y
X, t = make_swiss_roll(n_samples=1000, noise=0, random_state=0)
Y = tsne(X, 2, 50, 20.0)
plt.scatter(Y[:, 0], Y[:, 1], 20, c = t, cmap=plt.cm.hot)
plt.show()

在這裏插入圖片描述

小結

上面的算法是比較常見的降維算法,它們有不同的應用場景。PCA在無標籤的大量數據降維時比較有效,而如果是爲了分析值域和可視化,t-SNE是最常用的方法。此外,isomap可以很好的學習流形,auto-encoder可以做生成式模型和其他有意思的無監督學習任務。所有的算法沒有絕對的優劣,都是視應用場景而定。
還有就是學好線性代數太重要了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章