[譯] 降維技術中常用的幾種降維方法

leeyang-tju發表於2019-01-21

在本文中,我將盡最大的努力來闡明降維技術中常用的三種降維方法:即 PCA、t-SNE 和自動編碼器。本文的動機是由於這些方法多被當作黑盒使用,而有時會被誤用。理解它們將可以幫助大家決定何時以及如何使用它們。

我將使用 TensorFlow 從頭開始介紹每個方法的內部結構和對應程式碼(其中不包括 t-SNE)。為什麼使用 TensorFlow?因為它主要用於深度學習,讓我們使用它做一些其它的挑戰性任務 :) 本文的程式碼可以在這個筆記中找到。


動機

在處理實際問題和實際資料時,我們通常面臨的是高達百萬維度的資料。

雖然資料在其原始高維結構中更能夠表達自己的特性,但有時候我們可能需要降低它的維度。 一般需要減少維度的通常與視覺化相關(減少到 2 到 3 個維度上,這樣我們就可以繪製它),但情況並非總是如此。

有時,在實際應用中我們可能認為效能比精度更重要,這樣我們就可以把 1000 維的資料降低到 10 維,這樣我們就能更快地處理它(比如在距離計算中)。

有時緯度降低的需求是真實存在的,並且有很多的應用。

在開始之前,如果你必須為下列情況選擇一種降維技術,那麼你會選擇哪一種呢?

  1. 你的系統使用餘弦相似度來度量距離,但是你需要把它進行視覺化,展示給一些沒有技術背景的董事會成員,他們可能根本不熟悉餘弦相似度的概念 —— 你會怎麼做呢?

  2. 你需要將資料壓縮到儘可能小的維度,並且你得到的約束條件是保持大約 80% 的資料,你又會怎麼做呢?

  3. 你有一個資料庫,其中包含經過長時間收集的某類資料,並且資料(類似的型別)不斷地新增進來。

你需要降低資料的維度,無論是目前已有的資料還是源源不斷的新資料,你會選擇哪種方法呢?

我希望本文能幫助你更好地理解降維,這樣你就能處理好類似的問題。

讓我們從 PCA 方法開始。


PCA 方法

PCA(Principal Component Analysis)方法大概是書本中最古老的降維技術了。

因此 PCA 已經得到了很好的研究,並且很多的方法也可以得到相同的解,我們將在這裡討論其中的兩種方法,即特徵分解和奇異值分解(SVD),然後我們將在 TensorFlow 中實現 SVD 方法。

從現在開始,X 為我們要處理的資料矩陣,形狀為 (n, p),其中 n 表示樣本數量,p 表示維度。

給定 X,以上這兩種方法都試圖以它們自己的方式,來處理和分解 X,之後我們可以將分解後的結果相乘,以在更小的維數中表示最多的資訊。我知道這聽起來很可怕,所以這裡我將省去大部分的數學知識,只保留有助於理解這些方法優缺點的部分。

因此特徵分解和奇異之分解是兩種不同的矩陣分解方法,讓我們看看它們在主成分分析中有什麼用,以及它們之間有什麼聯絡。

先看一眼下面的流程圖,我馬上就會解釋其中的內容。

[譯] 降維技術中常用的幾種降維方法

圖 1 PCA 工作流圖

為什麼要關心這個呢?因為這兩個過程中有一些非常基本的東西,它們可以告訴我們很多關於主成分分析(PCA)的知識。

正如你所看到的這樣,這兩種方法都是純粹的線性代數方法,這基本上告訴我們,使用 PCA 方法就是從不同的角度觀察真實資料 —— 這是 PCA 方法獨有的,而其它的方法則是將資料從低維進行隨機表示,並且試圖讓它表現得像高維度資料一樣。

另外值得注意的是,所有的操作都是線性的,所以使用 SVD 的方法速度非常快。

同樣給定相同的資料,PCA 方法總是能給出相同的答案(另外兩種方法則不是這樣)。

注意到在 SVD 方法中我們如何選擇引數 r(r 是我們想要降到的維度)對於更低的維度來說能夠保留更大的 Σ 值? 其中 Σ 有一些特別之處。

Σ 是一個對角矩陣,其中有 p(p 為維度大小)個對角值(一般又稱為奇異值),並且它們的大小表示了它們對於資訊儲存的重要性。

因此我們可以選擇將維度減少,且減少到一個要大約保留的維度數量。給定一個維度數量的百分比,然後我將在程式碼中演示它(比如在丟失最多 15% 的資料約束下,我們給出的減少維度的能力)。

正如你看到的一樣,使用 TensorFlow 實現這個功能是相當簡單的 —— 我們只需要編寫一個類,該類中包含有一個 fit 方法和 reduce 方法即可,我們需要提供的引數是需要降維到的維度。

程式碼(PCA)

讓我們來看一下 fit 方法是什麼樣的,這裡的 self.X 包含參與計算的資料,並且其型別為 self.dtype=tf.float32

def fit(self):
    self.graph = tf.Graph()
    with self.graph.as_default():
        self.X = tf.placeholder(self.dtype, shape=self.data.shape)

        # 執行 SVD 方法
        singular_values, u, _ = tf.svd(self.X)

        # 建立 sigma 矩陣
        sigma = tf.diag(singular_values)

    with tf.Session(graph=self.graph) as session:
        self.u, self.singular_values, self.sigma = session.run([u, singular_values, sigma],
                                                               feed_dict={self.X: self.data})
複製程式碼

因此方法 fit 的目的是建立我們後面會使用到的 Σ 和 U。 我們從 tf.svd 這一行開始,它給我們計算出了對應的奇異值,也就是圖 1 中表示為 Σ 的對角線的值,同時它也計算出了 U 和 V 矩陣。

然後 tf.diag 是 TensorFlow 中將一維向量轉換成對角線矩陣的方法,這裡將奇異值轉換成對角線矩陣 Σ。

fit 方法的最後我們就可以計算出奇異值、矩陣 Σ 和 U。

現在讓我們來實現 reduce 方法。

def reduce(self, n_dimensions=None, keep_info=None):
    if keep_info:
        # 奇異值規範化
        normalized_singular_values = self.singular_values / sum(self.singular_values)

        # 為每個維度建立儲存資訊的梯形累計和
        ladder = np.cumsum(normalized_singular_values)

        # 獲取超過給定資訊閾值的第一個索引
        index = next(idx for idx, value in enumerate(ladder) if value >= keep_info) + 1
        n_dimensions = index

    with self.graph.as_default():
        # 從 sigma 中刪去相關部分
        sigma = tf.slice(self.sigma, [0, 0], [self.data.shape[1], n_dimensions])

        # PCA 方法
        pca = tf.matmul(self.u, sigma)

    with tf.Session(graph=self.graph) as session:
        return session.run(pca, feed_dict={self.X: self.data})
複製程式碼

如上所示,reduce 方法接受引數 keep_info 或者 n_dimensions(這裡我沒有寫輸入檢查,輸入檢查是必須要有的呀)。
如果使用時提供引數 n_dimensions,該方法會簡單將資料維度降低到該值,但是如果我們使用時提供引數 keep_info,該引數是一個 0 到 1 的浮點數,那麼該方法就需要保留原始資料的該資料對應百分比(比如 0.9 —— 表示保留原始資料的 90%)。 在第一個 “if” 判斷語句中,我們將資料進行了歸一化,並且檢查了需要使用多少個奇異值,接下來基本上是從 keep_info 中求出 n_dimensions 的值。

在圖中,我們只是把 Σ(sigma) 矩陣進行切片以對應儘可能多的需求資料,然後我們執行矩陣乘法。

讓我們在鳶尾花(iris)資料集上試一下,這是包含有 3 種形狀為(150,4)的鳶尾花資料集。

from sklearn import datasets
import matplotlib.pyplot as plt
import seaborn as sns

tf_pca = TF_PCA(iris_dataset.data, iris_dataset.target)
tf_pca.fit()
pca = tf_pca.reduce(keep_info=0.9)  # Results in 2 dimensions

color_mapping = {0: sns.xkcd_rgb['bright purple'], 1: sns.xkcd_rgb['lime'], 2: sns.xkcd_rgb['ochre']}
colors = list(map(lambda x: color_mapping[x], tf_pca.target))

plt.scatter(pca[:, 0], pca[:, 1], c=colors)
複製程式碼

[譯] 降維技術中常用的幾種降維方法

圖 2 在 Iris 資料集上通過 PCA 方法進行二維展示

還不錯,對吧?


t-SNE 方法

t-SNE 相對於 PCA 來說是一個相對較新的方法,源自於 2008 年的一篇論文(原始論文連結)。

它比 PCA 方法理解起來要複雜,所以請耐心聽我說。
我們對 t-SNE 的表示法是這樣的,X 表示原始資料,P 是一個矩陣,它儲存著高維(原始)空間中 X 的點之間的親密程度(即距離),Q 也是一個矩陣,它儲存著低維空間中資料點之間的親密程度。如果我們有 n 個資料樣本,Q 和 P 都是 n×n 的矩陣(即從任意點到包括它自身在內的任意點之間的距離)。

現在 t-SNE 方法有它自己“獨特的方式”(我們將很快介紹)來衡量物體之間的距離,一種測量高維空間中資料點之間的距離的方法,而另外一種方法是在低維度空間中測量資料點之間的距離,還有第三種方法是度量 P 和 Q 之間的距離的方法。 從原始文獻可知,一個點 x_j 與另一個點 x_i 的相似度由 _p_j|i 給出,其中,如果在以 x_i 為中心的高斯分佈下,按其概率密度的比例選取鄰點 x_j

“什麼?”別擔心,就像我說的那樣,t-SNE 有它自己的距離測量方法,所以我們來看一下距離(親密程度)測量公式,從中找出一些解釋來理解 t-SNE 的行為。

從高階層面來說,這就是該演算法的工作原理(注意,它與 PCA 不同,它是一種迭代演算法)。

[譯] 降維技術中常用的幾種降維方法

圖 3 t-SNE 工作流

讓我們一步一步來。

該演算法接受兩個輸入,一個是資料本身,另一個是複雜度(Perp)。

複雜度簡單來講就是你想要如何平衡在優化過程中的資料區域性(關閉點)結構和全域性結構之間的焦點 —— 本文建議將其保持在 5 到 50 之間。

較高的複雜度意味著資料點會考慮更多的點作為其近鄰點,較低的值則意味著考慮較少的點。

複雜度真的會影響到視覺化效果,並且一定要小心,因為它會在低維資料的視覺化中產生一些誤導現象 —— 因此我強烈建議閱讀這篇非常好的文章如何正確使用 t-SNE,這篇文章涉及到了複雜度取值不同的影響。

這種複雜度從何而來?它是用來計算出方程 (1)中的 σ_i,因為它們有一個由二叉搜尋樹構成的單調連線。

因此 σ_i 基本上是使用我們為演算法提供的複雜度資料,以不同的方式計算出來的。

讓我們來看一下 t-SNE 中的公式能夠告訴我們什麼。

在我們研究方程(1)和方程(2)之前需要知道的是,p_ii 被設定為 0,q_ii 也被設定為 0(這只是一個給定的值,我們將它應用於兩個相似的點,方程不會輸出 0)。

所以我們來看方程(1)和方程(2),請注意,如果兩個點是接近的(在高緯度結構表示下),那麼分子將產生一個約為 1 的值,而如果它們相距很遠,那麼我們會得到一個無限小的值 —— 這在後面將有助於我們理解成本函式。

現在我們已經看到了 t-SNE 的一些性質。

一個是由於關聯關係方程的建立方式的原因,在 t-SNE 圖中解釋距離是有問題的。

這意味著叢集之間的距離和叢集大小可能會產生誤導,並且也會受到所選擇的複雜度大小的影響(再次提醒,我將參考上面文中提到的文章中的方法,以檢視這些現象的視覺化資料)。

另外一件需要注意的事情是方程(1)中我們如何計算點與點之間的歐氏距離的?這裡非常強大的地方是,我們可以將距離度量切換成其它任何我們想要使用的距離度量方法,比如餘弦距離、曼哈頓距離等等(只要保持空間度量即可),並保持低維結構下的親和力是相同的 —— 這將以歐幾里德距離方式下,導致繪製比較複雜的距離。

舉個例子,如果你是 CTO,並且有一些資料,你想要使用餘弦相似性來作為其距離度量,而你的 CEO 希望你現在來做一些展示來表現這些資料的特性,我不確定你有時間來向董事會解釋餘弦相似性以及如何操作叢集資料,你只需要簡單地繪製出餘弦相似叢集即可,這正如在 t-SNE 演算法中使用的歐幾里德距離叢集一樣 —— 這就是我想說的,這已經很好了。

在程式碼中,你可以在 scikit-learn 中通過提供一個距離矩陣來使用 TSNE 方法來實現這些。

好了,現在我們知道了,如果 x_ix_j 相距很近時,p_ij/q_ij 的值就會很大,相反地,相距很遠時其值就會很小。

讓我們通過繪製出其影象來看一下我們的損失函式(我們稱之為 Kullback–Leibler 散度)的影響,另外來檢查一下沒有求和部分時的公式(3)。

[譯] 降維技術中常用的幾種降維方法

圖 4 t-SNE 的不包括求和部分的損失函式

這很難理解,但是我確實把座標軸的名字寫在這裡了。 可以看到,損失函式是非對稱的。

當點在高維空間(p 軸)附近時,它會產生一個很大的代價值,但是卻由低維度空間下的相距較遠的點來表示,同樣地,高維度空間下較遠的點會產生較小的代價值,卻由低維度空間下相距較近的點來表示。

這進一步說明了 t-SNE 繪製圖的距離解釋能力問題。

在鳶尾花資料集上使用 t-SNE 演算法,看看在不同的複雜度下會發生什麼

model = TSNE(learning_rate=100, n_components=2, random_state=0, perplexity=5)
tsne5 = model.fit_transform(iris_dataset.data)

model = TSNE(learning_rate=100, n_components=2, random_state=0, perplexity=30)
tsne30 = model.fit_transform(iris_dataset.data)

model = TSNE(learning_rate=100, n_components=2, random_state=0, perplexity=50)
tsne50 = model.fit_transform(iris_dataset.data)

plt.figure(1)
plt.subplot(311)
plt.scatter(tsne5[:, 0], tsne5[:, 1], c=colors)

plt.subplot(312)
plt.scatter(tsne30[:, 0], tsne30[:, 1], c=colors)

plt.subplot(313)
plt.scatter(tsne50[:, 0], tsne50[:, 1], c=colors)

plt.show()
複製程式碼

[譯] 降維技術中常用的幾種降維方法

圖 5 在鳶尾花資料集上使用 t-SNE 演算法,不同的複雜度

正如我們從數學的角度理解的那樣,可以看到,如果給定一個很好的複雜度的值,資料確實能夠很好地聚類,同時請注意超引數的敏感性(如果不提供梯度下降的學習率,我們就無法找到該聚類)。

在我們繼續之前,我想說,如果你能夠正確地應用,那麼 t-SNE 就是一個非常強大的方法,不要把你所學到的知識用到消極的一面上去,只要知道如何使用它即可。

接下來是自動編碼器的內容。


自動編碼器

PCA 和 t-SNE 是一種方法,而自動編碼器則是一類方法。
自動編碼器是一種神經網路,該網路的目標是通過使用更少的隱藏節點(編碼器輸出節點)來預測輸入(訓練該網路以輸出與輸入儘可能相似的結果),通過使用更少的隱藏節點(編碼器輸出節點)來編碼與輸入節點儘可能多的資訊。

我們的 4 維鳶尾花資料集的自動編碼器基本實現如圖 6 所示,其中連線輸入層和隱藏層的連線被稱為“編碼器”,隱藏層和輸出層之間的連線被稱為“解碼器”。

[譯] 降維技術中常用的幾種降維方法

圖 6 鳶尾花資料集上基礎自動編碼器結構

那麼為什麼自動編碼器是是一個方法簇呢?因為我們唯一的限制是輸入和輸出層的相同維度,而在內部我們卻可以建立任何我們想要使用的並能夠最好地編碼高維資料即可。

自動編碼器從一些隨機的低維表示法(z)開始,通過改變連線輸入層和隱藏層,以及連線隱藏層和輸出層的權重,並且使用梯度下降方法計算出最優的解決方案。

到目前為止,我們已經可以學習一些關於自動編碼器的重要知識,因為我們控制著網路內部結構,所以我們可以設計編碼器,使其能夠選擇特徵之間非常複雜的關係。

自動編碼器的另一個優點,是在訓練結束時我們就得到了指向隱藏層的權重,我們可以對特定的輸入進行訓練,如果稍後我們遇到另外一個資料,我們可以使用這個權重來降低它的維數,而不需要重新訓練 —— 但是需要小心,這樣的方案只有在資料點與訓練資料相似時才會有效。

在這種情況下,自動編碼器的數學原理可能很簡單,研究這個並不是很有用,因為我們選擇的每個內部架構和成本函式的數學原理都是不同的。

但是如果我們花點時間想想如何優化自動編碼器的權重,這樣我們就會明白成本函式的定義有著非常重要的作用。

因為自動編碼器會使用成本函式來決定它的預測的效果,所以我們可以使用這個能力來強調我們想要的。
無論我們想要歐幾里德距離還是其它距離度量值,我們都可以通過成本函式將它反映到編碼的資料上,使用不同的距離方法,使用對稱和非對稱函式等等。

更有說服力的是,由於自動編碼器本質上是一個神經網路,我們甚至可以在訓練時對類和樣本進行加權,從而賦予資料中的某些現象更多的內涵。

這為我們壓縮資料提供了很大的靈活性。

自動編碼器非常強大,並且在某些情況下與其它方法(比如谷歌的“PCA 對自動編碼器方法”)相比顯示了一些非常好的結果,因此它們肯定是一種有效的方法。

讓我們使用 TensorFlow 來實現一個基本的自動編碼器,使用鳶尾花資料集來測試使用並繪製其影象

程式碼(自動編碼器)

又一次,我們需要實現 fitreduce 方法

def fit(self, n_dimensions):
    graph = tf.Graph()
    with graph.as_default():

        # 輸入變數
        X = tf.placeholder(self.dtype, shape=(None, self.features.shape[1]))

        # 網路引數
        encoder_weights = tf.Variable(tf.random_normal(shape=(self.features.shape[1], n_dimensions)))
        encoder_bias = tf.Variable(tf.zeros(shape=[n_dimensions]))

        decoder_weights = tf.Variable(tf.random_normal(shape=(n_dimensions, self.features.shape[1])))
        decoder_bias = tf.Variable(tf.zeros(shape=[self.features.shape[1]]))

        # 編碼部分
        encoding = tf.nn.sigmoid(tf.add(tf.matmul(X, encoder_weights), encoder_bias))

        # 解碼部分
        predicted_x = tf.nn.sigmoid(tf.add(tf.matmul(encoding, decoder_weights), decoder_bias))

        # 以最小平方誤差定義成本函式和優化器
        cost = tf.reduce_mean(tf.pow(tf.subtract(predicted_x, X), 2))
        optimizer = tf.train.AdamOptimizer().minimize(cost)

    with tf.Session(graph=graph) as session:
        # 初始化全域性的變數引數
        session.run(tf.global_variables_initializer())

        for batch_x in batch_generator(self.features):
            self.encoder['weights'], self.encoder['bias'], _ = session.run([encoder_weights, encoder_bias, optimizer],
                                                                        feed_dict={X: batch_x})
複製程式碼

這裡沒有什麼特別之處,程式碼基本可以自解釋,我們將編碼器的權重儲存在偏差中,這樣我們就可以在接下來的 reduce 方法中減少資料。

def reduce(self):
    return np.add(np.matmul(self.features, self.encoder['weights']), self.encoder['bias'])
複製程式碼

呵,就是這麼簡單 :)

讓我們看看效果如何(批量大小為 50,1000 個輪次迭代)

[譯] 降維技術中常用的幾種降維方法

圖 7 簡單的自動編碼器在鳶尾花資料集上的輸出

即使不改變內部結構,我們使用不同的批量大小、輪次數量和不同的優化器等引數,也可能會得到不同的結果 —— 這只是剛剛開始的。

注意,我只是隨機選擇了一些超引數的值,在真實的場景中,我們將通過交叉驗證或測試資料來衡量我們這麼做的效果,並找到最佳的設定。

結語

像這樣的帖文通常會以一些比較圖表、優缺點等來結尾。 但這與我想要達到的目標恰恰相反。

我的目標是揭示這些方法的內部實現,這樣讀者就能理解每個方法的優缺點了。
我希望你能享受本次閱讀,並且學到一些新的東西。

從文章的開頭,到上面這三個問題,你現在感覺舒服多了吧?

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章