世上最生動的PCA:直觀理解並應用主成分分析 - LeeMeng
文章推薦指數: 80 %
主成分分析(Principal Component Analysis, 後簡稱為PCA)在100 年前由英國數學家卡爾·皮爾森發明,是一個至今仍在機器學習與統計學領域中被廣泛用來 ...
世上最生動的PCA:直觀理解並應用主成分分析一些你需先具備的基礎知識世上最簡單的降維:給我一個數字就好!世上最簡單的去關聯:數據原來的共變異去哪了?踏入荒野:實際應用PCA來解析真實數據在萬物皆向量的時代,如何瞭解事物本質?
PCA
主成分分析
機器學習
線性代數
Python
世上最生動的PCA:直觀理解並應用主成分分析
2020-01-06(Mon)
87,653views
在這個萬物皆向量的時代,能夠了解事物本質的數據處理能力變得前所未有地重要。
主成分分析(PrincipalComponentAnalysis,後簡稱為PCA)在100年前由英國數學家卡爾·皮爾森發明,是一個至今仍在機器學習與統計學領域中被廣泛用來分析資料、降低數據維度以及去關聯的線性降維方法。
因為其歷史悠久且相較其他降維手法簡單,網路上已有不少優質的機器學習課程以及部落格探討其概念。
在這篇文章裡,我則將透過Manim動畫、NumPy以及scikit-learn,跟你一起用這世上最直觀的角度重新體會PCA之美以及其背後關鍵的線性代數(LinearAlgrbra)與統計(Statistic)精神。
您的瀏覽器不支援影片標籤,請留言通知我:S
我們後面會看到將共變異數矩陣$\mathbf{K}$視為一線性轉換並套用到數據$\mathbf{X}$上會增強其變異趨勢
閱讀完本文,我相信你將能夠:
直觀且正確地理解PCA並欣賞其背後美麗的數學概念
了解如何運用PCA分析個人遇到的問題或是企業數據
具備能夠深入了解相關機器學習領域的基礎
希望你跟我一樣迫不及待地想要開始了!在正式踏上這趟旅途之前,我想說明一下你需要事先預習(或複習)的知識。
另外因為本文內的動畫皆為黑底,我強烈推薦點擊左下按鈕以暗色模式繼續閱讀。
一些你需先具備的基礎知識¶
鑽研學問的這條道路之上,就連帝王也沒有捷徑可循。
如同以往文章,為了讓美麗的知識能夠散播到最遠的地方,我會盡可能地平鋪直述PCA,以期能讓閱讀門檻被降到最低。
我會用不少動畫帶你直觀理解PCA的本質,而不只是丟個公式給你,或是教你怎麼用機器學習函式庫的API。
話雖這麼說,你仍需具備高中或大一程度的線性代數基礎。
如果你完全沒有修過任何線代課程,我會強烈建議你先去觀看3Blue1Brown的線性代數本質。
這是世上最好的線代入門教學,能讓你對以下概念有最直觀的理解:
線性組合LinearCombination
線性轉換LinearTransformation
點積DotProduct
基底變更ChangeofBasis
特徵向量/值Eigenvectors與Eigenvalues
本文則可以幫助你把基礎的線代知識無縫接軌地與PCA連結,並學會如何將PCA運用在真實世界的數據。
如果你要更扎實、更正式的線性代數基礎課程,我會大力推薦MathTheBeautiful以及MathematicsForMachineLearning。
一個好的教學者會引導你去找到最好的學習資源。
我的上一篇文章,給所有人的深度學習入門:直觀理解神經網路與線性代數也用了大量動畫說明矩陣相乘(MatrixMultiplication)、線性轉換以及神經網路(
NeuralNetwork)
與線代之間的緊密關係,建議事先閱讀。
本文會聚焦在PCA身上。
未來有時間的話,我會撰文說明PCA跟深度學習領域中的Autoencoder之間的美妙對應關係。
想要先睹為快的讀者稍後可以觀看台大電機李宏毅教授的PCA課程以及圖靈獎得主GeoffreyHinton的FromPCAtoautoencoders。
您的瀏覽器不支援影片標籤,請留言通知我:S
《給所有人的深度學習入門》一文展示了神經網路解決二元分類的過程
我接著會假設你已將上篇文章以及3Blue1Brown的影片看過一遍,或是至少已了解剛剛提到的幾個基本線代概念。
當然,我懂你想要「省時間」直接閱讀的心情,而你也完全可以這樣做!不過如果你等等發現自己的線代基礎不是那麼穩固,我會建議回到本節打好底子,或是點擊我在當下附的連結複習相關概念。
另外,如果你只是被文章封面吸引過來,想要馬上看到用PCA分析線上遊戲《英雄聯盟》的案例,可以先跳到踏入荒野:實際應用PCA來解析真實數據一節。
萬丈高樓平地起,打好地基真的很重要。
但現在假設你已經準備完畢,讓我們開始這趟PCA的深度探索之旅吧!
世上最簡單的降維:給我一個數字就好!¶
我們前面提到PCA可以用來有效地降低數據維度。
跟上篇文章討論過的二元分類不同,降維(DimensionalityReduction)是一種無監督學習,其最主要的目的是「化繁為簡」:將原本高維的數據(比方說$N$維)重新以一個相較低維的形式表達(比方說$K$維,且$K
透過投影,我們以$\vec{pc_{1}}$為基準,重新描述本來處在二維空間$\Re^{2}$裡的所有樣本$x$,得到其新的一維成分表徵$l$。
儘管只會產生一個數字,這個基底向量$\vec{pc_{1}}$可比我們從小習慣使用的$\hat{i}$與$\hat{j}$還能夠清楚地描述數據$\mathbf{X}$的本質。
有了$\vec{pc_{1}}$,你就只需要看一維特徵$\mathbf{L}$而不再需要用$\mathbf{B}_{standard}$所描述的二維數據$\mathbf{X}$了:
X[:,:4]
array([[2.89,0.32,5.8,-6.52],
[1.52,0.91,1.52,-0.88]])
這正呼應到我們前面提過的重要概念:
線性降維的核心精神是將原始數據拆解成更具代表性的主成分,並以其作為新的基準,由此獲得更能描述數據本質的新成分表徵。
而這正是透過PCA來達到「化繁為簡」的至高精神,希望你能有所體悟。
我稍後還會展示如何用PCA來對真實世界中的高維數據降維,但從下節開始,讓我們先看看PCA是如何透過主成分以及座標轉換來將多個特徵去關聯(Decorrelation)的。
我們將會看到更多振奮人心的動畫,但在那之前你得確保自己已熟悉以下線代概念:
基底變更ChangeofBasis
特徵向量/值Eigenvectors/Eigenvalues
記住,欲速則不達。
你的目標應該放在「真正地理解並掌握PCA」,而不是「在最短的時間閱讀完本文」。
世上最簡單的去關聯:數據原來的共變異去哪了?¶
除了可以降低數據維度,PCA也常被用來去除多個特徵之間的關聯。
去關聯(Decorrelation)在機器學習領域裡有不少應用情境,比方說你可能會想要有一組能夠用來獨立解釋數據特質的特徵,或是想保證餵進ML模型的多個特徵彼此無關,據此簡化問題以幫助模型泛化(generalize)。
這時PCA就可以被視為一種數據前處理手法,將多個特徵之間的關聯「拿掉」。
知己知彼。
要理解「去關聯」,你得先對「關聯」與「共變異」有透徹的理解。
在統計以及機率論的世界裡,我們時常使用樣本共變異數(SampleCovariance,後簡稱為Cov)來估計多個特徵兩兩之間的「共同變化程度」。
你常聽到的相關係數則是其正規化後的結果。
以數據$\mathbf{X}$裡頭的兩特徵$f_1$與$f_2$為例,你可以把$\operatorname{Cov}(f_1,f_2)$視為一個具有兩運算元的運算,其公式如下:
$$
\begin{align}
\operatorname{Cov}(f_1,f_2)=\frac{1}{N-1}\sum_{i=1}^{N}\left(f_{i1}-\bar{f}_1\right)\left(f_{i2}-\bar{f}_2\right)
\end{align}
$$其中$f_{i1}$與$f_{i2}$分別代表$\mathbf{X}$裡第$i$個樣本$x_i$的$f_1$與$f_2$的值;$\bar{f}_1$為我們觀測到的所有$f_1$值的平均($f_2$同理);$N$則為樣本數目。
以數據$\mathbf{X}$而言,$N=20$。
值得一提的是,當$f_2=f_1$,共變異數就代表著特徵$f_1$本身的變異(Variance)。
因此共變異數只是你所熟悉的變異的通用版本。
有了上式以後,我們可以輕易地計算出$\operatorname{Cov}(f_1,f_2)$:
#別忘記X維度是(n_features,n_samples)
print("X.shape:",X.shape,"\n")
print(X,"\n")
#分別計算兩特徵的樣本平均
f1_bar=X[0].mean()
f2_bar=X[1].mean()
#因為我們當初已經減去各特徵平均,兩者事實上已經為0
#也就是說f1_bar與f2_bar分別跟y,x軸重疊
print("f1_bar:",f1_bar)
print("f2_bar:",f2_bar)
assert_almost_equal(0,f1_bar)
assert_almost_equal(0,f2_bar)
#樣本共變異估計平均每個樣本裡特徵1跟特徵2的共同變化程度
#為了幫助你理解,我用世界上最沒效率,但跟公式最相近的方式計算
cov=0
forxinX.T:#這時就能看出(n_samples,)維度在前的好處
f1,f2=x
cov+=(f1-f1_bar)*(f2-f2_bar)
n=X.shape[1]
cov/=n-1
print("\nCov(f1,f2):{:.2f}".format(cov))
X.shape:(2,20)
[[2.890.325.8-6.523.94-4.210.452.141.3-4.98-2.4-3.1
0.69-1.59-3.64-0.246.814.63-2.24-0.06]
[1.520.911.52-0.88-0.03-1.26-0.250.96-0.89-0.45-0.88-1.12
-0.860.13-1.530.512.661.28-0.14-1.19]]
f1_bar:-5.551115123125783e-18
f2_bar:-2.2204460492503132e-17
Cov(f1,f2):3.27
碰!利用20筆樣本數據,我們估計出特徵$f_1$與$f_2$的樣本共變異數$\operatorname{Cov}(f_1,f_2)\cong3.27$。
減去各特徵的平均很合理,因為我們關心的是各特徵之間相對(於平均)的變化關係,而非其絕對值變化。
我不曉得你的統計基礎如何,但如果這是你第一次聽到共變異數,我會推薦StatQuest的教學影片。
當然,要直觀地理解共變異數也沒那麼困難。
不過比起實際數值$3.27$,我會建議你關注在$\operatorname{Cov}(f_1,f_2)$的正負號並問自己以下3個問題:
怎樣的情況會讓$\operatorname{Cov}(f_1,f_2)>0$?
怎樣的情況會讓$\operatorname{Cov}(f_1,f_2)=0$?
怎樣的情況會讓$\operatorname{Cov}(f_1,f_2)<0$?
如果你能先自己想通這件事情,我相信會非常有成就感。
讓我再次列出樣本共變異數的公式供你參考:
$$
\operatorname{Cov}(f_1,f_2)=\frac{1}{N-1}\sum_{i=1}^{N}\left(f_{i1}-\bar{f}_1\right)\left(f_{i2}-\bar{f}_2\right)
$$以純代數的角度來看,當第$i$個樣本$x_i$裡的這兩項結果:
$f_{i1}-\bar{f}_1$
$f_{i2}-\bar{f}_2$
為同號(即皆為正或皆為負)時,兩者相乘會得到正值。
而如果多數樣本的相乘結果皆為正,將其加總並平均的結果自然仍為正值;反之,當兩者異號(一正一負)的情況較多時,最後的平均結果是負的機會就會比較大。
換句話說:
$\operatorname{Cov}(f_1,f_2)>0$時代表兩特徵的變化具有相同傾向,時常一起變大或一起變小
$\operatorname{Cov}(f_1,f_2)=0$時代表兩特徵的變化沒有明顯關係
$\operatorname{Cov}(f_1,f_2)<0$時代表兩特徵的變化具有相反傾向,時常一個變大,一個變小
當然,統計做的事情是估計,當我們手中有越多觀測結果,估計出來的樣本共變異數自然就會越準,降低過適(overfit)的可能性。
為了讓你能夠更直觀地了解共變異數,底下我以數據$\mathbf{X}$為例,用幾何的觀點展示$\operatorname{Cov}(f_1,f_2)$的計算過程:
您的瀏覽器不支援影片標籤,請留言通知我:S
我想這應該是你這輩子看過最直觀的共變異數計算過程。
在計算過程中值為正的物件皆被以藍色所表示;紅色則代表其值為負。
每個數據點$x_i$的顏色依照其$(f_{i1}-\bar{f}_1)(f_{i2}-\bar{f}_2)$的相乘結果有所變化:
藍點:$(f_{i1}-\bar{f}_1)(f_{i2}-\bar{f}_2)>0$
紅點:$(f_{i1}-\bar{f}_1)(f_{i2}-\bar{f}_2)<0$
透過這個動畫,你可以清楚地觀察到以下結果:
因為兩特徵值的平均皆為$0$,$\bar{f}_1$與$\bar{f}_2$正好分別跟y、x軸重合(圖中黃線)
在第一與第三象限的數據點因為$(f_{i1}-\bar{f}_1)$跟$(f_{i2}-\bar{f}_2)$同號(距離線皆為紅或藍),相乘結果為正(顯示為藍點)
在第二及第四象限則因為異號(距離線一紅一藍),相乘結果為負(數據點呈紅色)
因為藍點的結果加總較大,數據$\mathbf{X}$的共變異數為正,代表平均來說,兩特徵存在著正向關聯
我們也可以做個簡單歸納,當一三或是二四象限的數據點較其他象限多時,我們更容易觀察到兩特徵呈現線性關係,且共變異數不太可能為零。
這些都是不錯的觀察,不過現在問你自己:
如果數據$\mathbf{X}$的共變異數為正,跟它最像但共變異數為零的成分表徵$\mathbf{L}$長什麼樣子?如果的確存在一個共變異數為零的成分表徵$\mathbf{L}$的話,怎樣才能在不改變$\mathbf{X}$本質的情況下轉變到$\mathbf{L}$?
如果你能想通這件事情,用PCA去關聯就完全是小事一樁。
我馬上就會揭曉答案,但我希望你能先自己動腦想想。
提示:選擇一組適當的基底$\mathbf{B}$並旋轉$\mathbf{X}$到適當的位置。
如果你完全沒有想法,我相信底下的動畫可以派上用場。
觀察:
什麼時候共變異數最小
$\mathbf{X}$的兩主成分$\vec{pc_1}$與$\vec{pc_2}$的位置
$\mathbf{X}$的變異趨向
您的瀏覽器不支援影片標籤,請留言通知我:S
怎麼樣?有點感覺了嗎?我想你會同意,要在不改變數據$\mathbf{X}$本身的情況下將特徵$f_1$、$f_2$之間的關聯消除,最簡單且有效率的作法就是旋轉座標了。
你可以想像在旋轉的過程中,我們不斷地將當下跟水平x軸以及垂直y軸重疊的2個單位向量視為基底向量$b_1$與$b_2$,並將數據$\mathbf{X}$裡頭的每個樣本$\vec{x_i}$重新表達成這兩向量的線性組合,即:
$$
\begin{align}
\vec{x_i}=l_1\vec{b_1}+l_2\vec{b_2},l_1,l_2\in\Re
\end{align}
$$
每組$b_1$與$b_2$都是一個合法的$\Re^2$基底$B$,所以上面動畫實際上無時無刻都在進行基底變更(ChangeofBasis)。
在旋轉開始之前,初始基底自然就是我們一開始表達數據$\mathbf{X}$的$\mathbf{B}_{standard}$,因此每個樣本$x$的成分表徵,也就是$f_1$以及$f_2$的值並沒有被改變。
但當基底$B$對應到$\{\vec{pc_1},\vec{pc_2}\}$,也就是文章開頭劇透過的$\mathbf{B}_{pc}$時,美好的結果就誕生了:
$\mathbf{X}$的變異傾向不再是由左下至右上的斜線,而是被限制在水平或是垂直軸上,新的兩特徵看似不再相關
$\mathbf{B}_{pc}$自帶的特徵關聯信息分別被編碼在新的x、y軸上,這使得新的兩特徵之間不再相關
透過投影到$\mathbf{B}_{pc}$,每個樣本$x$獲得一組可以互相獨立解釋的成分表徵$l_1$與$l_2$
新特徵的共變異數$\operatorname{Cov}(l_1,l_2)$被最小化至零
這些敘述都是等價的。
而你現在知道去關聯時選擇對的基底有多重要了。
你也可以想像這動畫是前面旋轉向量$\vec{v}$並計算一維重建錯誤$RE_1$的2維版本。
當時為了降維,我們只投影到$\vec{pc_1}$;但現在為了去關聯,我們選擇投影到$\vec{pc_1}$以及$\vec{pc_2}$,獲得另外一組2維成分表徵。
我們甚至可以說用PCA對數據$\mathbf{X}$去關聯就是將主成分當作新的基底$B_{pc}$並進行基底變更。
我們用一組更具代表性的基底$\{\vec{pc_1},\vec{pc_2}\}$來重新表述數據$\mathbf{X}$,由此獲得一組彼此沒有關聯的全新特徵$l_1$與$l_2$。
您的瀏覽器不支援影片標籤,請留言通知我:S
我們剛剛看過的1維投影與重建錯誤
透過剛剛的動畫以及解說,我想你已經能夠直觀地理解PCA是怎麼將數據$\mathbf{X}$去關聯的。
如果數據只有2維,那麼就只要將2個主成分作為新的基底,將數據投影到上面並得到新的特徵$l_1$及$l_2$即可。
現在只剩一個謎團還沒解開:怎麼實際找出主成分?數據$\mathbf{X}$的主成分究竟從哪冒出來的?
我在降維時已經透露過,數據$\mathbf{X}$的共變異數矩陣(CovarianceMatrix)裡頭的2個Eigenvectors事實上就是我們要找的$\vec{pc_1}$以及$\vec{pc_2}$。
我等等會給你個更直觀的解釋,但現在先讓我們算出該共變異數矩陣:
"""
透過NumPy計算X的共變異數矩陣K。
跟多數PythonMLAPI不同,NumPyAPI預期的輸入維度大都是
(n_features,n_samples)。
事實上這比較符合線性代數直覺,
每個column是一個樣本x。
這也是我喜歡NumPy的原因
"""
#注意不像scikit-learn,我並沒有先將X轉置才傳入
K=np.cov(X)
#共變異數矩陣diagonal方向上為各特徵自己的變異
K
array([[13.35,3.27],
[3.27,1.33]])
不只是兩特徵的共變異數,共變異數矩陣事實上也包含了兩特徵自身的變異。
如果你的線代神經夠敏銳,應該可以理解為何共變異數矩陣$\mathbf{K}_{f_1f_2}$是對稱矩陣(SymmetricMatrix)。
這是因為共變異數的計算跟$f_1$與$f_2$的輸入順序無關,自然$\mathbf{K}_{f_1f_2}=\mathbf{K}_{f_1f_2}^{T}$。
我們稍後將利用這個性質直探共變異數矩陣的本質。
我在這裡不會說明如何計算一個矩陣的Eigenvalues以及Eigenvectors,但我們可以透過NumPy來將它們找出來:
"""
透過NumPy我們能輕易地找出共變異數矩陣的Eigenvectors
不像scikit-learn,通常我們得自己依照eigenvalue大小排序eigenvectors
但在此例中,正好第一行為Eigenvalue最大的Eigenvector
"""
#透過特徵拆解取得共變異數矩陣的Eigenvectors/values
eig_vals,eig_vecs=np.linalg.eig(K)
#eig_vecs的每一行(column)為一個Eigenvectors
print(f"eig_vecs.shape:",eig_vecs.shape)
print(eig_vecs)
print('-'*40)
#eig_vals裡則是對應的Eigenvalues
print("eig_vals.shape:",eig_vals.shape)
print(eig_vals)
eig_vecs.shape:(2,2)
[[0.97-0.25]
[0.250.97]]
----------------------------------------
eig_vals.shape:(2,)
[14.180.5]
沒錯,$\mathbf{K}_{f_1f_2}$的第一個Eigenvector$\vec{v_1}$就是我們降維$\mathbf{X}$時用的第一主成分$\vec{pc_1}$:
#兩者完全一樣,毫不意外
pc1=pca_1d.components_.T
assert_almost_equal(eig_vecs[:,:1],pc1)
#降維時我們用的第一主成分
pc1
array([[0.97],
[0.25]])
你現在也知道數據$\mathbf{X}$的第二主成分$\vec{pc_2}$就是$\mathbf{K}_{f_1f_2}$的第二大Eigenvector$\vec{v_2}$。
我們也清楚$\{\vec{v_1},\vec{v_2}\}$就等同於之前提過的$\mathbf{B}_{pc}$,其跟標準基底$\mathbf{B}_{standard}$一樣可以作為$\Re^2$基底。
跟$\mathbf{B}_{standard}$不同的是,用$\mathbf{B}_{pc}$為基底獲得的2維成分表徵能最有效率地表達數據$\mathbf{X}$的特性。
且由於這兩個向量同時為$\mathbf{K}_{f_1f_2}$的Eigenvectors,我們一般也稱這種基底為Eigenbasis$\mathbf{B}_{eigen}$。
如果你有依照建議事先觀看3Blue1Brown的基底變更介紹,就能明白我們剛剛得出的eig_vecs事實上就代表著一個基底變更矩陣(ChangeofBasisMatrix,後簡稱為COB矩陣)。
這個COB矩陣做的事情是將:
$C_{eigen}$:以$\mathbf{B}_{eigen}$為基準的座標系統,即以$\mathbf{B}_{eigen}$所表示的成分表徵$l_1,l_2$
轉換成
$C_{standard}$:以$\mathbf{B}_{standard}$為基準的座標系統,以$\mathbf{B}_{standard}$所表示的成分表徵$f_1,f_2$
這也是為何COB矩陣常被說是在進行座標轉換,就像是你在之前的旋轉動畫看到的那樣。
為了方便起見,我將這個COB矩陣以$\mathbf{Q}$表示。
而因為$\mathbf{Q}$裡頭的每個eigenvectors$\vec{v_i}$長度皆為1且互相正交,$\mathbf{Q}$是一個正交矩陣(OrthogonalMatrix)。
其美妙的性質值得特別空一行出來表示:
$$
\begin{align}
\mathbf{Q}^{-1}=\mathbf{Q}^\mathsf{T}
\end{align}
$$
別忘了我們最一開始得到的$(x,y)$座標是$\mathbf{B}_{standard}$的成分表徵,因此是被以$C_{standard}$的形式表示。
我們實際想要做的事情是$\mathbf{Q}$的逆運算$\mathbf{Q}^{-1}$:將$C_{standard}$轉換成$C_{eigen}$。
幸運的是,你已經知道$\mathbf{Q^{-1}}=\mathbf{Q}^\mathsf{T}$!水到渠成,最令人期待的時刻到來了,我們現在可以用$\mathbf{Q}^\mathsf{T}$將數據$\mathbf{X}$重新表示成$\mathbf{B}_{eigen}$的成分表徵以去關聯:
您的瀏覽器不支援影片標籤,請留言通知我:S
簡單而美麗。
這動畫總結了所有我們在這章節談到的概念。
為了把$\mathbf{X}$去關聯,我們將$\mathbf{K}_{f_1f_2}$的Eigenvectors作為新的基底並建構出COB矩陣,成功地將彼此關聯的兩特徵$f_1$與$f_2$重新表示成互相獨立的隱藏表徵$l_1$與$l_2$。
換句話說,我們是透過PCA找出數據$\mathbf{X}$中的主成分,並以此為基底$\mathbf{B_{eigen}}$算出$\mathbf{X}$的主成分表徵(PrincipalComponentRepresentation)。
我們在前面也已經看過,如果只投影到第一主成分$\vec{pc_1}$,那就等同於最有效的一維線性降維。
這組主成分可以從$\mathbf{X}$的共變異數矩陣$\mathbf{K}_{f_1f_2}$中找出且專屬於$\mathbf{X}$;而我們一開始依照直覺用來表達$\mathbf{X}$的成分$\{\hat{i},\hat{j}\}$是一組通用成分(GeneralComponents):非常地泛用,但因為沒有針對$\mathbf{X}$客製化,在對$\mathbf{X}$去關聯及降維的情境中並不是最好的基底。
花了不少時間,但現在你應該已經能夠深刻地體會我在文章開頭所陳述的重要概念:
所有基底生而平等。
但最後,你的應用情境與目的決定了哪組基底比其他的選擇好。
如果今天我們只是想要簡單地視覺化數據$\mathbf{X}$,則$\mathbf{B}_{standard}$是我們的第一人選;但如果我們的目的是將$\mathbf{X}$降維或者是去關聯,則$\mathbf{B}_{pc}$是你最好的選擇;而如果今天的數據不再是$\mathbf{X}$而是另組數據$\mathbf{Y}$,$\mathbf{B}_{pc}$也就不再是最好的選擇,需要重新尋找。
掌握了這些道理,我們要自己用PCA為$\mathbf{X}$去關聯可說是蛋糕一片:
#Q代表將Eigenbasis的空間轉回標準的笛卡爾空間
#因此我們要做的是Q-1=Q.T
Q=eig_vecs
X_trans=Q.T@X
X_trans
array([[3.18,0.53,5.99,-6.53,3.81,-4.39,0.37,2.31,1.04,
-4.93,-2.54,-3.28,0.46,-1.51,-3.9,-0.11,7.26,4.81,
-2.2,-0.35],
[0.76,0.81,0.04,0.75,-1.,-0.18,-0.35,0.4,-1.18,
0.79,-0.27,-0.33,-1.,0.52,-0.59,0.55,0.9,0.09,
0.42,-1.13]])
真的,關鍵就一行矩陣相乘而已,樸實無華。
你也可以確認$\mathbf{X}_{trans}$的共變異數已經為$=0$:
K1=np.cov(X_trans)
print(K1)
assert_almost_equal(0,K1-np.diag(np.diagonal(K1)))
[[1.42e+018.42e-16]
[8.42e-165.02e-01]]
你也可以將去關聯前後的數據都畫出來比較一下:
plt.scatter(X[0],X[1],alpha=0.2)
plt.scatter(X_trans[0],X_trans[1])
plt.axis('equal');
很直覺的,在我們的例子裡PCA實際上就是稍微旋轉數據,使得讓兩特徵的共變異數$=0$。
而且旋轉後的數據呈現水平分布,的確沒有相關,你說是吧?去關聯任務,大成功!到此為止,我想你已經能夠直觀地理解PCA怎麼將數據去關聯,並能實際將這個數據處理技巧應用在自己的數據上了,恭喜!
我在這章節用了不少篇幅闡述PCA的去關聯應用,如果你到目前為止都有好好跟上,那你的PCA基礎就已經勝過大多數人了。
不過,如果你內心還是覺得好像少了點什麼,可能是因為你還無法給這個問題一個非常直觀的解釋:
我們透過PCA將數據去關聯了,但原來特徵之間的共變異去了哪裡?
事實上這是本章標題丟出的提問。
再繼續閱讀之前,用一分鐘想像你迫不及待地想跟別人解釋PCA是怎麼將數據去關聯時,他/她問了這個問題。
你要怎麼回答?
喔,拜託別直接往下拉!這是一個複習本章概念以及所有你學過的線代概念的最佳時機,也是你唯一一次有機會用自己的話解釋PCA概念。
我保證,在觀看接下來幾個動畫之後,你就永遠沒有這個機會了。
你可以快速地重新瀏覽本章內容或是Google,尋找此題解答的蛛絲馬跡。
希望你自己有想出一些合理的解釋,因為這對內化PCA概念有很大的幫助。
你的解釋應該跟數據$\mathbf{X}$的共變異數矩陣$\mathbf{K}_{f_1f_2}$有關,畢竟我們已經知道PCA找出的主成分就是該矩陣的Eigenvectors。
如果讓我回答剛剛的問題,我會說:
數據的共變異一直都在,是去關聯後數據的成分表徵改變了。
去關聯後,數據的共變異性質體現在其主成分本身之上,而共變異的程度對應到主成分的變異。
再換句話說,共變異數矩陣的Eigenvectors事實上就隱含了特徵之間的共變異性質,而該共變異的程度則由對應的Eigenvalues所表示。
如果你有類似的回答,很好!你可以直接跳到下一章,看看如何將PCA套用到真實世界的數據。
但如果你對這個概念還是有點模糊,我會再花一點篇幅讓你多點直觀感受。
關鍵在於意識到$X$的共變異數矩陣$\mathbf{K}_{f_1f_2}$完整地儲存了$\mathbf{X}$的變異資訊,同時本身也是一個線性轉換的事實:
#K是X的宇宙魔方,隱含了X的關鍵資訊
K
array([[13.35,3.27],
[3.27,1.33]])
如同之前神經網路的文章以及前面對一維投影矩陣$\mathbf{P}_1$的解釋,你可以想像如果我們將$\mathbf{K}_{f_1f_2}$套用(apply)到數據$\mathbf{X}$上,它會增強其$\mathbf{X}$本來的變異傾向,使得每個樣本$x$依照兩特徵$f_1$與$f_2$的線性關係做延伸:
您的瀏覽器不支援影片標籤,請留言通知我:S
你等等可以數數格子,確認轉換後的$\hat{i}$與$\hat{j}$是否的確移動到$\mathbf{K}_{f_1f_2}$所定義的位置。
這邊最重要的是,因為共變異數矩陣$\mathbf{K}_{f_1f_2}$本身就代表著數據$\mathbf{X}$的變異以及特徵$f_1$與$f_2$之間的線性關係,其所對應的線性轉換自然能把最吻合該變異、特徵之間相對關係的向量等倍縮放。
而什麼向量能被$\mathbf{K}_{f_1f_2}$轉換後維持等倍縮放而不改變自己的方向?自然是該矩陣的Eigenvectors!你也可以清楚地看到,第一大的Eigenvector$\vec{v_1}$直接地反映了整體數據的變異傾向。
這是為何$\mathbf{K}_{f_1f_2}$的Eigenvectors可以作為數據$\mathbf{X}$的主成分的原因。
而儘管$\lambda_2$不大,第二個Eigenvector$\vec{v_2}$則解釋了跟$\vec{v_1}$正交方向的數據變異。
把兩者解釋的變異放在一起,我們就能還原數據$X$的原貌。
我們剛剛用NumPy得到的Eigenvalues$\lambda_1,\lambda_2$就是這兩個Eigenvectors套用$\mathbf{K}_{f_1f_2}$後的伸縮倍率:
#K@pc1=14.2*pc1
#K@pc2=0.5*pc2
eig_vals
array([14.18,0.5])
另外你可以看到$\lambda_{1}$已經解釋了大部分$\mathbf{X}$的變異,這是為何投影到$\vec{v_1}$是個好的線性降維。
更美妙的是,因為共變異數矩陣$\mathbf{K}_{f_1f_2}$是一個實數對稱矩陣(RealSymmetricMatrix),我們可以對其進行特徵分解(EigenDecomposition):
$$
\begin{align}
\mathbf{K}_{f_1f_2}=\mathbf{Q}\mathbf{\Lambda}\mathbf{Q}^\mathsf{T}
\end{align}
$$
如果你腦筋動得夠快,且還記得我們前面是怎麼用$\mathbf{Q}^\mathsf{T}$將數據$\mathbf{X}$旋轉並去關聯的,看到上面這個式子你應該跟當初的我一樣激動地跳起來並撞到天花板才對!不不不,並不是因為右項長得很像表情符號,而是因為$\mathbf{K}_{f_1f_2}$所代表的線性轉換可以被拆解成3個更簡單的線性轉換:旋轉(rotate)、伸縮(stretch)、再旋轉!
$\mathbf{Q}$正是我們前面去關聯時交手過的那個正交矩陣(OrthogonalMatrix),裡頭的每個column都對應到一個Eigenvector$\vec{v_i}$;$\mathbf{\Lambda}$則是一個對角矩陣(Diagonalmatrix),其對角線皆為Eigenvalues$\lambda_i$,分別對應到$\mathbf{Q}$裡頭的Eigenvectors使得:
$$
\begin{align}
\mathbf{K}_{f_1f_2}\vec{v_i}=\lambda_i\vec{v_i}
\end{align}
$$
而我們之前是透過$\mathbf{Q}^\mathsf{T}$旋轉座標來獲得共變異數$=0$的$\mathbf{X}_{trans}$:
$$
\begin{align}
\mathbf{X}_{trans}=\mathbf{Q}^\mathsf{T}\mathbf{X}
\end{align}
$$
透過以上幾個式子,我們也可以將$\mathbf{K}_{f_1f_2}$套用到$\mathbf{X}$的過程重新表示成以下形式:
$$
\begin{align}
\mathbf{K}_{f_1f_2}\mathbf{X}&=\mathbf{Q}\mathbf{\Lambda}\mathbf{Q}^\mathsf{T}\mathbf{X}\\
&=\mathbf{Q}\mathbf{\Lambda}\mathbf{X}_{trans}
\end{align}
$$
把你從本文學到的所有概念拿出來,你會知道$\mathbf{K}_{f_1f_2}$隱含的3個簡單轉換分別為:
旋轉:透過$\mathbf{Q}^\mathsf{T}$將$\mathbf{X}$旋轉到以$\mathbf{B}_{eigen}$為基底的座標$C_{eigen}$
伸縮:透過$\mathbf{\Lambda}$將轉換到$C_{eigen}$上的$\mathbf{X}_{trans}$依照Eigenvector方向伸縮Eigenvalue倍
旋轉:透過$\mathbf{Q}$將伸縮完的$\mathbf{X}_{trans}$旋轉回原來座標$C_{standard}$
沒錯,共變異數矩陣$\mathbf{K}_{f_1f_2}$本身代表著3個連續的簡單線性轉換:旋轉、伸縮、再旋轉。
而在共變異的情境下,代表著將數據中各特徵$f_i$的共變異傾向重新表達成主成分$\vec{pc_j}$自己的變異。
子曰:『朝聞道,夕死可矣。
』
─《論語.里仁》
我承認這用詞有點浮誇,但這的確是我當初想通這道理時腦中浮現的第一個句子。
如果你平常沒有接觸線性代數,那從代數的角度看來可能只是一些符號的相乘轉換;但如果你從幾何的觀點觀察,這拆解結果實在是美到不要不要的:
您的瀏覽器不支援影片標籤,請留言通知我:S
你可以拉回去跟上個$\mathbf{K}_{f_1f_2}$的轉換動畫做比較,但我可以直接告訴你兩者代表一模一樣的轉換。
從幾何角度我們可以清楚地看到兩個旋轉$\mathbf{Q}$與$\mathbf{Q}^\mathsf{T}$都只是在進行基底變更(改變觀察視角),並不會改變任何$\mathbf{X}$本身的變異。
因此事實上$\mathbf{X}$的兩特徵的共變異趨勢(Covariance)都被以$\mathbf{K}_{f_1f_2}$裡頭的Eigenvectors$\vec{v_i}$所指的方向所表達,而Eigenvalue$\lambda_i$則代表了該變異的大小(Variance)。
現在你能夠直觀地回答本章的大哉問了:「數據的共變異去了哪裡?」
數據的共變異一直都在,是去關聯後數據的成分表徵改變了。
去關聯後,數據的共變異性質體現在其主成分本身之上,而共變異的程度對應到主成分的變異。
就跟剛剛我們利用$\mathbf{Q}^\mathsf{T}$改變基底一樣,在你理解對稱矩陣的特徵拆解以及其所代表的幾何意義以後,要用程式碼實現這些運算真的是簡單到不行,多虧了無數開發者的功勞:
"""
利用NumPy取得原始數據X的共變異數矩陣K
並實現EigenDecomposition
"""
print("原始數據X:")
print(X)
#計算共變異數矩陣
K=np.cov(X)
print("\n共變異數矩陣K:")
print(K)
#實現對稱矩陣的EigenDecomposition
eig_vals,Q=np.linalg.eig(K)
La=np.diag(eig_vals)
print("\nQ:")
print(Q)
print("\nLambda:")
print(La)
print("\nQ.T:")
print(Q.T)
#我們可以確認共變異數矩陣的確等於這三個矩陣相乘
assert_almost_equal(K,Q@[email protected])
print("\nK=Q@[email protected]!!")
原始數據X:
[[2.890.325.8-6.523.94-4.210.452.141.3-4.98-2.4-3.1
0.69-1.59-3.64-0.246.814.63-2.24-0.06]
[1.520.911.52-0.88-0.03-1.26-0.250.96-0.89-0.45-0.88-1.12
-0.860.13-1.530.512.661.28-0.14-1.19]]
共變異數矩陣K:
[[13.353.27]
[3.271.33]]
Q:
[[0.97-0.25]
[0.250.97]]
Lambda:
[[14.180.]
[0.0.5]]
Q.T:
[[0.970.25]
[-0.250.97]]
K=Q@[email protected]!!
好啦!遮掩住PCA本質的最後一塊迷霧也已經散去。
你現在能夠直觀地理解為何人們總是說共變異數矩陣$\mathbf{K}_{f_1f_2}$的Eigenvectors是數據$\mathbf{X}$的主成分了(因為這些向量直接解釋了$\mathbf{X}$中特徵間的共變異傾向)。
你也懂得如何利用PCA找出的主成分來對數據去關聯,這可是一個不小的成就!
從下一節開始,我們將從美麗的理論基礎走向實際的PCA應用。
我將簡單分享2個透過PCA解析真實數據的例子。
閱讀完該節後,你也能用最有效率的方式分析自己感興趣的數據並獲得無數有趣的洞見。
踏入荒野:實際應用PCA來解析真實數據¶
閱讀完前面兩節,你現在已經能夠直觀且正確地理解PCA了。
你也學會如何利用NumPy找出數據$\mathbf{X}$的共變異數矩陣$\mathbf{K}$並對其進行特徵拆解、建立基底變更矩陣$\mathbf{Q}$、$\mathbf{Q}^\mathsf{T}$並將數據線性降維以及去關聯。
這些是PCA最核心的概念與技巧。
在掌握這些基礎以後,讓我們用前面已經展示過的scikit-learn來提升分析數據的效率。
以下是幾個網路上常被用來展示PCA概念的數據集:
鳶尾花瓣TheIrisDataset
真實人臉TheOlivettifaces
手寫數字HandwrittenDigits
這些數據集非常有名,而它們最棒的地方在於取得容易,且網路上也有大量的相關教學。
我鼓勵有興趣的讀者稍後自行點擊以上連結,深入了解如何透過PCA來解析這些數據。
但我強烈建議:
要內化並掌握某個數據分析技巧,將其應用在自己感興趣的數據與問題之上。
而這也是我在本章打算展示給你看的事情。
閱讀過我其他文章的讀者們想必都清楚,任何機器學習模型或是Python函式庫說到底都只是一種工具;最重要的不是學習如何使用某個工具,而是了解該工具如何幫助你達成你想要的目的。
以下是一些過往案例:
直觀理解GPT-2語言模型並生成金庸武俠小說
淺談神經機器翻譯&用Transformer英翻中
用CartoonGAN及TensorFlow生成新海誠動畫
用BERT:NLP界的巨人之力進行假新聞分類
除了使用的「工具」以外,你可以看到我的文章充滿著「目的性」。
畢竟,真正有用的是那些能被實際用來解決你手邊問題的工具。
秉持著這個想法並冒著被視為宅宅的風險,
與其使用隨處可見的數據集然後展示乏善可陳的結果,在這節我將PCA套用在自己喜歡的一款多人線上遊戲:英雄聯盟(LeagueofLegends,常被簡稱為LOL)的公開數據之上。
這應該是你最想不到會被拿來當作PCA案例的數據。
以下就是該遊戲目前所有可供使用的147名英雄角色(champions):
您的瀏覽器不支援影片標籤,請留言通知我:S
#使用方便的資料分析函式庫pandas處理CSV數據
importpandasaspd
fromIPython.displayimportdisplay
#將事先預處理過的英雄數據讀取為pandas的DataFrame
#你可以從同樣的url獲得本文demo的數據
df=pd.read_csv("https://bit.ly/2FkIaTv",sep="\t",index_col="名稱")
print("df.shape:",df.shape)
#展示前5rows
print("前五名英雄數據:")
display(df.head(5))
#顯示各特徵的平均與標準差
print("各特徵平均與標準差:")
df_stats=df.describe().loc[['mean','std']]
df_stats.style.format("{:.2f}")
df.shape:(147,12)
前五名英雄數據:
類型
攻擊距離
魔力
魔力回復
魔力提升
生命提升
生命
生命回復
移動速度
物理攻擊
物理防禦
魔法防禦
名稱
厄薩斯
鬥士
175
0
0.0
0
90
580.0
3.0
345
60.0
38.0
32.1
阿璃
法師
550
418
0.8
25
92
526.0
6.5
330
53.0
20.9
30.0
阿卡莉
刺客
125
200
0.0
0
95
575.0
8.0
345
62.4
23.0
37.0
亞歷斯塔
坦克
125
350
0.8
40
106
573.4
8.5
330
61.1
44.0
32.1
阿姆姆
坦克
125
287
0.5
40
84
613.1
9.0
335
53.4
33.0
32.1
各特徵平均與標準差:
攻擊距離魔力魔力回復魔力提升生命提升生命生命回復移動速度物理攻擊物理防禦魔法防禦
mean
326.39
309.66
0.57
33.03
88.52
555.83
6.60
336.31
59.37
30.06
31.13
std
196.52
115.40
0.28
16.62
6.65
37.33
1.78
7.56
6.17
6.72
1.71
對熟悉此遊戲的玩家們(players)而言,我相信這數據集本身就顯得十分有趣並值得深入探索了。
不過在這篇文章裡,我將聚焦在PCA身上而不會進行探索性數據分析EDA。
另外,我會用資料科學家的pandas實戰手冊裡頭闡述過的技巧來處理這些英雄數據。
如果你想要用最短的時間上手pandas,稍後可以自行前往閱讀該篇文章。
現在我們手上握有每位英雄的詳細資料,包含了英雄名稱、所屬類型以及11種屬性值(特徵值)。
除非有什麼領域知識(DomainKnowledge)讓你確信某個特徵$f_i$的單位變異比另個特徵$f_j$的單位變異要來得重要,在套用PCA或是任何機器學習模型之前,你都應該將數據正規化(Normalization),也就是將每個數據點$x$(此例中為一名名英雄向量)減去平均後除以標準差(StandardDeviation):
$$
\begin{align}
z&=\operatorname{normalize}(x)\\
&=(x-\mu)/\sigma
\end{align}
$$數據正規化的實作也十分容易:
"""
將英雄數據正規化。
使用對的API能幫我們省下不少時間。
這邊為了教學目的,同時呼叫scikit-learnAPI並手動計算比較結果
"""
fromsklearn.preprocessingimportStandardScaler
fromnumpy.testingimportassert_almost_equal
#將類型以外的11個特徵全取出
X=df.iloc[:,1:]#(n_samples,n_features)
#使用scikit-learn內建的API正規化
scaler=StandardScaler()
Z_sk=scaler.fit_transform(X)#注意維度
#手動正規化當然也能得到跟scikit-learnAPI相同的結果
#注意我們有所有英雄數據(母體)而非抽樣,自由度=0
Z=(X-X.mean(axis=0))/X.std(axis=0,ddof=0)
assert_almost_equal(Z,Z_sk)
#更新我們的DataFrame
df.iloc[:,1:]=Z
#展示前5rows
print("正規化後前五名英雄數據:")
display(df.head(5).style\
.format("{:.2f}",subset=df.columns[1:]))
#顯示各特徵的平均與標準差
print("各特徵平均與標準差:")
df_stats=df.describe().loc[['mean','std']]
df_stats.style.format("{:.2f}")
正規化後前五名英雄數據:
類型攻擊距離魔力魔力回復魔力提升生命提升生命生命回復移動速度物理攻擊物理防禦魔法防禦名稱
厄薩斯
鬥士
-0.77
-2.69
-2.02
-1.99
0.22
0.65
-2.03
1.15
0.10
1.19
0.57
阿璃
法師
1.14
0.94
0.84
-0.48
0.52
-0.80
-0.06
-0.84
-1.04
-1.37
-0.66
阿卡莉
刺客
-1.03
-0.95
-2.02
-1.99
0.98
0.52
0.79
1.15
0.49
-1.05
3.45
亞歷斯塔
坦克
-1.03
0.35
0.84
0.42
2.64
0.47
1.07
-0.84
0.28
2.08
0.57
阿姆姆
坦克
-1.03
-0.20
-0.23
0.42
-0.68
1.54
1.35
-0.17
-0.97
0.44
0.57
各特徵平均與標準差:
攻擊距離魔力魔力回復魔力提升生命提升生命生命回復移動速度物理攻擊物理防禦魔法防禦
mean
-0.00
-0.00
0.00
-0.00
0.00
-0.00
0.00
0.00
0.00
0.00
0.00
std
1.00
1.00
1.00
1.00
1.00
1.00
1.00
1.00
1.00
1.00
1.00
稍微有接觸過電玩的讀者們應該都能夠直觀地解讀這些特徵各自代表的涵義。
注意正規化前後的數據$\mathbf{X}$與$\mathbf{Z}$維度皆為(n_samples,n_features),這也是實務上你最常遇到的情境。
你也可以看到正規化後$\mathbf{Z}$的各特徵平均皆為$0$、標準差為$1$。
這節最重要的概念是這裡的每位英雄都對應到11維特徵空間$\Re^{11}$裡頭的一個特定向量(Vector)。
儘管生活在$\Re^3$的我們無法描繪出$\Re^{11}$,你可以想像這是一個非常龐大的向量空間。
這空間大到一個被隨機初始化的$\Re^{11}$向量$\vec{v}$會長得像這些英雄向量的機率近似於$0$。
因此:
我們可以合理地假設這些英雄數據存在於一個被嵌入在高維空間$\Re^{11}$裡頭的低維空間$\Re^{k}$,且$k$遠小於$11$。
再換句話說,我們並不需要整整11個數字來形容一個英雄,只需要$k$個具有代表性的數字就好。
這正是機器學習、尤其是近年深度學習(DeepLearning)領域一直信奉著的流形假設(ManifoldHypothesis)。
而既然我們不需要那麼多個數字來表示一個英雄的特性,我們可以將數據$\mathbf{X}$降維來取得這些英雄的新$k$維表徵。
現在假設這些英雄所對應的向量存在於$\Re^2$,即$k=2$。
在學會PCA以後,你的第一反應應該是設法找出數據前兩大主成分$\{\vec{pc_1},\vec{pc_2}\}$並將數據投影到上去以得到新的2維(主)成分表徵$\mathbf{L}$。
透過scikit-learn的API,線性降維是件輕鬆寫意的差事:
"""
透過scikit-learn將11維的LOL英雄數據降到2維
"""
fromsklearn.decompositionimportPCA
#我們只要最大的兩個主成分。
scikit-learn會自動幫我們
#依照eigenvalue的大小排序共變異數矩陣的eigenvectors
n_components=2
random_state=9527
pca=PCA(n_components=n_components,
random_state=random_state)
#注意我們是對正規化後的特徵Z做PCA
L=pca.fit_transform(Z)#(n_samples,n_components)
#將投影到第一主成分的repr.顯示在x軸,第二主成分在y軸
plt.scatter(L[:,0],L[:,1])
plt.axis('equal');
透過幾行非常簡單的程式碼,我們成功地將11維的英雄數據降維並表達成更具代表性的2維主成分表徵了。
讓我們利用過去幾節所學,解讀一下這兩個主成分所代表的潛在意涵:
"""
解析英雄數據的前兩大主成份所代表的意涵。
顏色越突出代表其絕對值越大
"""
pcs=np.array(pca.components_)#(n_comp,n_features)
df_pc=pd.DataFrame(pcs,columns=df.columns[1:])
df_pc.index=[f"第{c}主成分"forcin['一','二']]
df_pc.style\
.background_gradient(cmap='bwr_r',axis=None)\
.format("{:.2}")
攻擊距離魔力魔力回復魔力提升生命提升生命生命回復移動速度物理攻擊物理防禦魔法防禦
第一主成分
0.43
0.28
0.19
0.14
-0.14
-0.32
-0.31
-0.33
-0.35
-0.34
-0.34
第二主成分
0.072
-0.43
-0.56
-0.5
-0.32
-0.24
-0.21
0.014
-0.021
-0.08
-0.16
我們之前就說過主成分本身的方向就解釋了原始數據中多個特徵之間的共變異傾向。
透過簡單的觀察,你可以發現:
第一主成分代表著「遠攻」或是「魔力型」英雄。
這是因為x值越大會讓攻擊距離越長、魔力相關屬性皆有所提升;其代價是生命與防禦相關的數值降低。
這些是被俗稱為「脆皮」的血少攻高類型英雄。
第二主成分則是以降低魔力相關屬性的數值以換取更遠的攻擊距離以及更高跑速的物理英雄。
從第一主成分我們可以看出攻擊距離以及魔力造就了英雄聯盟裡最大的整體英雄數值差異,而這也或多或少反映了設計該遊戲的公司以及設計師理念。
這可是個非常有趣且實用的洞見!不過雖然我們已經知道第一主成分解釋了最多的數據變異,但實際上它到底解釋(explain)了多少?有沒有什麼辦法可以量化這件事情?
變異解釋率(ExplainedVarianceRatio)是一個常被用來量化某個主成分$\vec{v_i}$解釋數據變異程度的指標:
$$
\begin{align}
\frac{\lambda_i}{\sum\lambda_i}\|\\mathbf{K}\vec{v_i}=\lambda_i\vec{v_i}
\end{align}
$$別忘了$\mathbf{K}$是數據$\mathbf{X}$的共變異數矩陣,而$\lambda_i$是其Eigenvector$\vec{v_i}$所對應的Eigenvalue。
將某個主成分$\vec{v_i}$的$\lambda_i$拿去除以所有Eigenvalues的總和$\sum\lambda_i$就能得到該主成分的變異解釋率。
如果你還記得我們前面說過主成分的Eigenvalue解釋了主成分變異趨勢的大小,那麼這個公式對你來說就十分直覺了。
我們可以看看前10大主成分各自解釋了多少英雄數值的變異:
pca_10d=PCA(10,random_state=random_state)
pca_10d.fit(Z)
np.round(pca_10d.explained_variance_ratio_,2)
array([0.41,0.2,0.09,0.08,0.05,0.05,0.04,0.03,0.02,0.02])
端看你的應用情境,累加的變異解釋率也可以幫助我們決定該把原始的$N$維數據降到幾維(即決定$K$)以維持足夠的數據變異,進而讓降維後的表徵具有足夠的代表性。
在我們的例子裡頭,前兩個主成分就已經解釋了100多位英雄數值中近$6$成的差異($0.41+0.2$),是一個相當不錯的降維結果。
這可是你把任意兩個原始屬性(比方說生命與物理攻擊)作為數據的2維成分表徵無法得到的美麗結果。
如果我們將PCA降維後的結果結合每個英雄所屬的主要類型的話,一切會變得更加有趣。
每位英雄都可以被歸類在以下6個類別:
df['類型'].unique()
array(['鬥士','法師','刺客','坦克','射手','輔助'],dtype=object)
你可以猜一猜,這是我們剛剛對第一主成分的解讀:
第一主成分代表著「遠攻」或是「魔力型」英雄。
x軸的值越大會讓攻擊距離越長、魔力相關屬性皆有所提升;其代價是生命與防禦相關的數值降低。
這些是被俗稱為「脆皮」的血少攻高類型的英雄。
依照這個敘述,假設我們將每位英雄投影到第一主成分所得到的特徵值$l_1$描繪在水平x軸上,你覺得哪種類型的英雄會有比較大的x值?如果你有些電玩遊戲經驗,應該可以想像以下這兩類型的英雄相當符合第一主成分的描述:
射手(Marksman):定位為遠程攻擊,生命值少,玻璃大砲
法師(Mage):大多也為遠程攻擊,魔力為主要傷害來源
我們可以看看這個猜想有多準確。
下面依序展示每個英雄類型的2維主成分表徵分布。
前兩類型即為射手與法師:
您的瀏覽器不支援影片標籤,請留言通知我:S
我們可以看到跟其他類型的英雄相比,射手以及法師英雄的確普遍具有較大的x值,代表它們相當符合第一主成分的特性:遠程攻擊、魔法傷害高。
透過幾行程式碼,在沒有介紹任何英雄的情況下我們就能有效率地發掘出顯著且有趣的英雄特性,這正是PCA的強大之處!跟這兩類型英雄相反,你也可以發現動畫中第四個類型:鬥士(Fighter)普遍擁有較小的x值。
這代表它們魔力較低但擁有較高的生命以及防禦力。
如果你是此遊戲的玩家,也能透過英雄頭像感受一下結果。
我相信你會同意從PCA得到的發現不只相當有趣,還符合我們對這些英雄特性的理解。
這例子也告訴我們領域知識(DomainKnowledge)的重要。
以這邊的例子而言,所謂的領域知識自然是你對此遊戲以及英雄特性的理解。
如果你完全沒玩過此遊戲,PCA能幫助你快速地了解這些英雄屬性的本質;但領域知識能幫助你驗證數據得出的結果是否符合常理並讓你找到更多有趣的洞見。
這也是我們在如何用30秒了解台灣發展與全球趨勢就已經看過的重要數據概念:
不靠數據無法了解世界,但光靠數據也無法了解世界。
─漢斯・羅斯林,《真確》
我鼓勵你認真思考如何將PCA應用到自己感興趣或是熟悉的數據之上,並嘗試利用自己的世界觀以及領域知識,解讀PCA帶給你的分析結果。
相信我,只要結合領域知識以及數據分析能力,你將獲得專屬於自己的全新洞見。
讓我再簡單介紹一個PCA案例。
這次直接用PCA解析你剛剛已經看過的英雄頭像圖片:
您的瀏覽器不支援影片標籤,請留言通知我:S
你可以自行前往英雄聯盟的開發者頁面或是右鍵下載我為你準備好的lol_champion_images.npy。
下載後可以這樣將所有的英雄頭像讀取為4維NumPy陣列:
"""
包含所有英雄頭像圖片的陣列。
維度分別為(n_champions,height,width,channel)
"""
images=np.load("lol_champion_images.npy")
print("images.shape:",images.shape)
flatten_images=np.reshape(images,(images.shape[0],-1))
print("flatten_images.shape:",flatten_images.shape)
images.shape:(147,120,120,3)
flatten_images.shape:(147,43200)
你可以看到一張小小的英雄頭像就有整整43200個像素(維度),剛剛的11維英雄數據跟它比完全是小巫見大巫。
但我們完全可以依樣畫葫蘆,用PCA將這些處在超高維空間的圖片向量維降並得到不錯的結果,而這是代數觀點上的一大勝利。
著有線代經典《IntroductiontoLinearAlgebra》的著名數學家GilbertStrang曾說過:
線性代數真的是一門很棒的學問。
我的意思是,如果所有東西都是線性的,還有什麼事情會出錯呢?
─GilbertStrang
現在馬上讓我們用PCA將高維的英雄頭像做線性降維:
fromsklearn.pipelineimportmake_pipeline
fromsklearn.preprocessingimportStandardScaler
fromsklearn.decompositionimportPCA
#將有43,200像素的圖片降到108維度
random_state=9527
pca=PCA(n_components=108,
random_state=random_state)
#我們可以建構一個標準化->降維的pipeline
pipe=make_pipeline(StandardScaler(),pca)
transformed_images=pca.fit_transform(flatten_images)
print("transformed_images.shape:",transformed_images.shape)
transformed_images.shape:(147,108)
程式碼非常直覺,且圖片的維度一下子減少了許多。
你可以想像我們將這些高維圖片中最重要的資訊投影到了低維空間,類似的降維有很多應用,比方說提升數據傳輸效率、萃取關鍵資訊等等。
我們也可以看看這108個主成分解釋了多少數據的變異:
"""
計算主成分累積的疊加變異解釋率
"""
cum_explained_var_ratio=np.cumsum(pca.explained_variance_ratio_)
plt.plot(cum_explained_var_ratio)
plt.xlabel('#principalcomponents')
plt.ylabel('cumulativeexplainedvariance');
你可以看到,我們只用了原本的0.25%的維度數目,就幾乎完整地解釋了這些英雄頭像的變異:
108./43200
0.0025
這可是很驚人的結果!而前30個主成分就幾乎解釋了70%的數據變異。
且因為每個主成分自身就代表著一個在43200維度裡頭的高維向量,我們可以把它們也視為一個個的圖片視覺化出來:
"""
將前30個英雄頭像圖片的主成分rescale並繪出
"""
#為了方便視覺解讀rescale主成分
fromsklearn.preprocessingimportminmax_scale
scaled_comps=minmax_scale(pca.components_,axis=1)
#繪製前30個主成分
fig,axes=plt.subplots(3,10,figsize=(12,4),
subplot_kw={'xticks':[],'yticks':[]},
gridspec_kw=dict(hspace=0.1,wspace=0.1))
fori,axinenumerate(axes.flat):
c=scaled_comps[i]
ax.imshow(c.reshape(120,120,3))
你可以看到這裡的每個主成分都捕捉到了某種形式的頭像資訊。
這也是你隨機選擇30個像素無法做到的事情。
你也可以看到第一主成分包含了所有英雄頭像皆有的黑色邊框;而我個人覺得第一排從左數來第二與第六主成分都相當貼近英雄頭像,你覺得呢?
我們也可以將新得到的108維主成分表徵重新投影回原來43200維的像素空間看看重建成果:
"""
將新得到的108維成分表徵重新投影回原高維空間並比較前後結果
"""
reconstructed_images=pca.inverse_transform(transformed_images)
reconstructed_images=minmax_scale(reconstructed_images,axis=1)
shape=(120,120,3)
foriinrange(3):
fig,ax=plt.subplots(2,10,figsize=(12,6),
subplot_kw={'xticks':[],'yticks':[]},
gridspec_kw=dict(hspace=-0.75,wspace=0.1))
forjinrange(10):
idx=i*10+j
ax[0,j].imshow(images[idx].reshape(shape))
ax[1,j].imshow(reconstructed_images[idx].reshape(shape))
ax[0,0].set_ylabel('43,200-Dim\nImage')
ax[1,0].set_ylabel('108-Dim\nReconstruction');
投影後的結果雖不完美,但也已經相當不錯了。
畢竟我們只用了0.25%的維度數目來重新表達這些圖片。
原圖跟投影後的圖片之間的差距,就是我們之前講過的重建錯誤。
在這個例子中為$RE_{108}$。
好啦,透過英雄聯盟的案例分析,我想你現在也能如法炮製並實際應用PCA在你自己感興趣的數據上面啦!我們這趟漫長的PCA之旅也即將邁入尾聲。
下節讓我簡單總結一下我們一路走來學了些什麼。
在萬物皆向量的時代,如何瞭解事物本質?¶
首先,由衷地感謝你一路跟隨我走到這裡。
我得承認,我在本文開頭要求你須先具備的基礎知識並不算少,但如果你有好好聽從我的建議奠定好基礎並一步步跟上來,我相信這趟PCA旅程也會讓你收穫滿滿。
閱讀完本文,你現在應該已經能夠:
用PCA降維並直觀理解為何將數據投影到主成分上能最小化重建錯誤
用PCA去關聯並了解如何進行基底變更以將數據重新表示成主成分表徵
直觀理解共變異數的物理意義並了解如何透過座標轉換使共變異數為零
了解為何數據的共變異數矩陣的Eigenvectors為PCA找出的主成分
理解為何$\mathbf{B}_{standard}$並非萬能以及如何找出適當的基底$\mathbf{B}_{pc}$重新表述數據
透過NumPy以及scikit-learn將PCA應用在自己想分析的數據之上
直觀理解並能欣賞PCA背後的線性代數與統計概念
我希望你也能感受到現在自己的腦袋裡多了不少有用的知識。
PCA的核心精神是為手邊的數據選擇一個最好的觀察視角,給予數據全新且最具意義的表徵(representation)。
在這個萬物皆能被以一個數值向量表示並解析的時代,這個核心精神能幫助你解構任何數據,瞭解事物的本質。
這也是我撰寫此文希望能讓更多人掌握此精神的動力之一。
您的瀏覽器不支援影片標籤,請留言通知我:S
t-SNE也可以用來視覺化高維數據,但需小心解讀
雖然本文篇幅有限無法詳述,在熟悉PCA這個線性降維技巧之後,你已經可以開始了解其他(非)線性的降維技術了。
比如知名的t-SNE、UMAP、NMF以及Autoencoder。
希望你離去之後能夠實際嘗試應用PCA來分析自己或是企業的數據,並將得到的洞見與我分享。
另外如果這篇文章有幫助到你,還請不吝花個幾秒鐘分享給對機器學習或是數據分析有興趣的朋友閱讀,幫我將這些知識傳播給更多人。
那就這樣啦!我們下次見。
或許當年發明PCA的皮爾森也沒料想到,在一百年後AI滿天飛的年代,如此簡單的主成分分析仍然佔有它自己的一席之地。
PostTags
PCA
主成分分析
機器學習
線性代數
Python
GetLatestArrivals
訂閱最新文章
跟資料科學相關的最新文章直接送到家。
只要加入訂閱名單,當新文章出爐時,
你將能馬上收到通知
Subscribe
PreviousPost
監控資本主義時代下的資料科學、AI與你我的數位未來
NextPost
給所有人的深度學習入門:直觀理解神經網路與線性代數
ViewAllPost
延伸文章資訊
- 1第81 章主成分分析Principal Component Analysis | 醫學統計學
第81 章 主成分分析Principal Component Analysis. A big computer, a complex algorithm and a long time does...
- 2主成分分析- SAS Taiwan
在多變量分析中,主成分分析(Principal components analysis, PCA)是一種分析、簡化數據集的技術。利用原有的變數組合成新的變數,以達到資料縮減的 ...
- 3R 主成分分析PCA 教學:使用ade4 套件 - Office 指南
主成分分析(principal component analysis,簡稱PCA)是多變量分析中的一種降維(dimensionality reduction)方法,可降低資料的維度,同時保留資料...
- 4主成分分析PCA
主成分分析(Principal Component Analysis, PCA)最主要是利用數學的方法,將複雜的事情簡化之。主成分分析在100 年前由英國數學家卡爾·皮爾森發明, ...
- 5主成份分析(Principal Component Analysis, PCA)
主成份分析(Principal. Component Analysis, PCA). 它是對多個變數決定各變數權重而成. 加權平均,依此訂出總指標. 經由線性組合而得的主成份,能保有原.