第15章降維算法
如果拿到的數據特徵過於龐大,一方面會使得計算任務變得繁重;另一方面,如果數據特徵還有問題,可能會對結果造成不利的影響。降維是機器學習領域中經常使用的數據處理方法,一般通過某種映射方法,將原始高維空間中的數據點映射到低維度的空間中,本章將從原理和實踐的角度介紹兩種經典的降維算法——線性判別分析和主成分分析。
15.1線性判別分析
線性判別式分析(Linear Discriminant Analysis,LDA),也叫作Fisher線性判別(Fisher Linear Discriminant,FLD),最開始用於處理機器學習中的分類任務,但是由於其對數據特徵進行了降維投影,使其成爲一種經典的降維方法。
15.1.1降維原理概述
線性判別分析屬於有監督學習算法,也就是數據中必須要有明確的類別標籤,它不僅能用來降維,還可以處理分類任務,不過,更多用於降維。下面通過一個小例子來感受下降維的作用。這個遊戲需要通過不斷地尋找最合適的投影面,來觀察原始物體的形狀,如圖15-1所示。降維任務與之非常相似,也是通過找到最合適的投影方向,使得原始數據更容易被計算機理解並利用。
圖15-1 投影的意義
接下來,通過實例說明降維的過程。假設有兩類數據點,如圖15-2所示。由於數據點都是二維特徵,需要將其降到一維,也就是需要找到一個最合適的投影方向把這些數據點全部映射過去。圖15-2(a)、(b)分別是選擇兩個投影方向後的結果,那麼,究竟哪一個更好呢?
圖15-2 降維的目的
從投影結果上觀察,圖15-2(a)中的數據點經過投影后依舊有一部分混在一起,區別效果有待提高。圖15-2(b)中的數據點經過投影后,沒有混合,區別效果比圖15-2(a)更好一些。因此,我們當然會選擇圖15-2(b)所示的降維方法,由此可見,降維不僅要壓縮數據的特徵,還需要尋找最合適的方向,使得壓縮後的數據更有利用價值。
由圖15-2可知,線性判別分析的原理可以這樣理解:任務目標就是要找到最合適的投影方向,這個方向可以是多維的。
爲了把降維任務做得更圓滿,提出了兩個目標。
1.對於不同類別的數據點,希望其經過投影后能離得越遠越好,也就是兩類數據點區別得越明顯越好,不要混在一起。
2.對於同類別的數據點,希望它們能更集中,離組織的中心越近越好。
接下來的任務就是完成這兩個目標,這也是線性判別分析的核心優化目標,降維任務就是找到能同時滿足這兩個目標的投影方向。
15.1.2優化的目標
投影就是通過矩陣變換的方式把數據映射到最適合做分類的方向上:
其中,x表示當前數據所在空間,也就是原始數據;y表示降維後的數據。最終的目標也很明顯,就是找到最合適的變換方向,即求解出參數W。除了降維任務中提出的兩個目標,還需要定義一下距離這個概念,例如“扎堆”該怎麼體現呢?這裏用數據點的均值來表示其中心位置,如果每一個數據點都離中心很近,它們就“扎堆”在一起了。中心點位置計算方法如下:
對於多分類問題,也可以得到各自類別的中心點,不同類別需要各自計算,這就是爲什麼要強調線性判別分析是一個有監督問題,因爲需要各個類別分別進行計算,所以每一個數據點是什麼類別,必須在標籤中給出。
在降維算法中,其實我們更關心的並不是原始數據集中數據點的扎堆情況,而是降維後的結果,因此,可知投影后中心點位置爲:
由式(15.3)可以得到投影后的中心點計算方法,按照之前制定的目標,對於一個二分類任務來說,應當使得這兩類數據點的中心離得越遠越好,這樣才能更好地區分它們:
現在可以把當作目標函數,目標是希望其值能夠越大越好,但是隻讓不同類別投影后的中心點越遠可以達到我們期望的結果嗎?
對於圖15-3所示的樣本數據,假設只能在x1和x2兩方向進行投影,如果按照之前定義的J(w),顯然x1方向更合適,但是投影后兩類數據點依舊有很多重合在一起,而x2方向上的投影結果是兩類數據點重合較少;因此,x2方向更好。
這個問題就涉及要優化的另一個目標,不僅要考慮不同類別之間要區分開,還要考慮同類樣本點應當儘可能聚集在一起。顯然在圖15-3中,x1方向不滿足這個條件,因爲在x1方向上,同類樣本變得更分散,不夠集中。
圖15-3 投影方向選擇
我們還可以使用另一個度量指標——散列值(scatter),表示同類數據樣本點的離散程度,定義如下:
其中,y表示經過投影后的數據點,從式(15.5)中可以看出,散列值表示樣本點的密集程度,其值越大,表示越分散;反之,則越集中。定義好要優化的兩個目標後,接下來就是求解了。
15.1.3線性判別分析求解
上一小節已經介紹了降維後想要得到的目標,現在把它們綜合在一起,但是優化的目標有兩個,那麼如何才能整合它們呢?
既然要最大化不同類別之間的距離,那就把它當作分子;最小化同類樣本之間的離散程度,那就把它當作分母,最終整體的J(W)依舊求其極大值即可。
在公式推導過程中,牢記最終的要求依舊是尋找最合適的投影方向,先把散列值公式展開:
爲了化簡方便,則令:
式(15.8)稱爲散佈矩陣(scatter matrices)。
由此可以定義類內散佈矩陣爲:
Sw=S1+S2 (15.9)
將式(15.9)代入式(15.7),可得:
對式(15.6)分子進行展開可得:
其中,SB爲類間散佈矩陣。
目標函數J(w)最終可以表示爲:
對於散列矩陣SB和SW,只要有數據和標籤即可求解。但是如何求解最終的結果呢?如果對分子和分母同時求解,就會有無窮多解,通用的解決方案是先固定分母,經過放縮變換後,將其值限定爲1,則令:
在此條件下求WTSBW的極大值點,利用拉格朗日乘子法可得:
既然目標是找投影方向,也就是w,將式(15.14)左右兩邊同時乘以Sw-1可得:
觀察一下式(15.15),它與線性代數中的特徵向量有點像,如果把Sw-1當作一個整體,那麼w就是其特徵向量,問題到此迎刃而解。在線性判別分析中,其實只需要得到類內和類間散佈矩陣,然後求其特徵向量,就可以得到投影方向,然後,只需要對數據執行相應的矩陣變換,就完成全部降維操作。
15.1.4Python實現線性判別分析降維
下面要在非常經典的“鳶尾花”數據集上使用線性判別分析完成降維任務。鳶尾花數據集可從該鏈接下載:https://archive.ics.uci.edu/ml/datasets/Iris,也可以從sklearn庫內置的數據集中獲取。數據集中含有3類、共150條鳶尾花基本數據,其中山鳶尾、變色鳶尾、維吉尼亞鳶尾各有50條數據,每條數據包括萼片長度(單位:釐米)、萼片寬度、花瓣長度、花瓣寬度4種特徵。
首先讀取數據集,代碼如下:
1 # 自己來定義列名 2 feature_dict = {i:label for i,label in zip( 3 range(4), 4 ('sepal length in cm', 5 'sepal width in cm', 6 'petal length in cm', 7 'petal width in cm', ))} 8 9 label_dict = {i:label for i,label in zip( 10 range(1,4), 11 ('Setosa', 12 'Versicolor', 13 'Virginica' 14 ))} 15 import pandas as pd 16 # 數據讀取,大家也可以先下載下來直接讀取 17 df = pd.io.parsers.read_csv( 18 filepath_or_buffer='https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data', 19 header=None, 20 sep=',', 21 ) 22 # 指定列名 23 df.columns = [l for i,l in sorted(feature_dict.items())] + ['class label'] 24 25 df.head()
數據集共有150條數據,每條數據有4個特徵,現在需要將四維特徵降成二維。觀察輸出結果可以發現,其特徵已經是數值數據,不需要做額外處理,但是需要轉換一下標籤:
1 from sklearn.preprocessing import LabelEncoder 2 3 X = df[['sepal length in cm','sepal width in cm','petal length in cm','petal width in cm']].values 4 y = df['class label'].values 5 6 # 製作標籤{1: 'Setosa', 2: 'Versicolor', 3:'Virginica'} 7 enc = LabelEncoder() 8 label_encoder = enc.fit(y) 9 y = label_encoder.transform(y) + 1
上述代碼使用了sklearn工具包中的LabelEncoder用於快速完成標籤轉換,可以發現基本上所有sklearn中的數據處理操作都是分兩步走,先fit再transform,變換結果如圖15-4所示。
在計算過程中需要基於均值來判斷距離,因此先要對數據中各個特徵求均值,但是隻求4個特徵的均值能滿足要求嗎?不要忘記任務中還有3種花,相當於3個類別,所以也要對每種花分別求其各個特徵的均值:
圖15-4 標籤轉換
1 import numpy as np
2 #設置小數點的位數
3 np.set_printoptions(precision=4)
4 #這裏會保存所有的均值
5 mean_vectors = []
6 # 要計算3個類別
7 for cl in range(1,4):
8 # 求當前類別各個特徵均值
9 mean_vectors.append(np.mean(X[y==cl], axis=0))
10 print('均值類別 %s: %s\n' %(cl, mean_vectors[cl-1]))
均值類別 1: [5.006 3.418 1.464 0.244]
均值類別 2: [5.936 2.77 4.26 1.326]
均值類別 3: [6.588 2.974 5.552 2.026]
接下來計算類內散佈矩陣:
1 # 原始數據中有4個特徵
2 S_W = np.zeros((4,4))
3 # 要考慮不同類別,自己算自己的
4 for cl,mv in zip(range(1,4), mean_vectors):
5 class_sc_mat = np.zeros((4,4))
6 # 選中屬於當前類別的數據
7 for row in X[y == cl]:
8 # 這裏相當於對各個特徵分別進行計算,用矩陣的形式
9 row, mv = row.reshape(4,1), mv.reshape(4,1)
10 # 跟公式一樣
11 class_sc_mat += (row-mv).dot((row-mv).T)
12 S_W += class_sc_mat
13 print('類內散佈矩陣:\n', S_W)
類內散佈矩陣:
[[38.9562 13.683 24.614 5.6556]
[13.683 17.035 8.12 4.9132]
[24.614 8.12 27.22 6.2536]
[ 5.6556 4.9132 6.2536 6.1756]]
繼續計算類間散佈矩陣:
式中,m爲全局均值;mi爲各個類別的均值;Ni爲樣本個數。
1 # 全局均值 2 overall_mean = np.mean(X, axis=0) 3 # 構建類間散佈矩陣 4 S_B = np.zeros((4,4)) 5 # 對各個類別進行計算 6 for i,mean_vec in enumerate(mean_vectors): 7 #當前類別的樣本數 8 n = X[y==i+1,:].shape[0] 9 mean_vec = mean_vec.reshape(4,1) 10 overall_mean = overall_mean.reshape(4,1) 11 # 如上述公式進行計算 12 S_B += n * (mean_vec - overall_mean).dot((mean_vec - overall_mean).T) 13 14 print('類間散佈矩陣:\n', S_B)
類間散佈矩陣: [[ 63.2121 -19.534 165.1647 71.3631] [-19.534 10.9776 -56.0552 -22.4924] [165.1647 -56.0552 436.6437 186.9081] [ 71.3631 -22.4924 186.9081 80.6041]]
得到類內和類間散佈矩陣後,還需將它們組合在一起,然後求解矩陣的特徵向量:
1 #求解矩陣特徵值,特徵向量 2 eig_vals, eig_vecs = np.linalg.eig(np.linalg.inv(S_W).dot(S_B)) 3 # 拿到每一個特徵值和其所對應的特徵向量 4 for i in range(len(eig_vals)): 5 eigvec_sc = eig_vecs[:,i].reshape(4,1) 6 print('\n特徵向量 {}: \n{}'.format(i+1, eigvec_sc.real)) 7 print('特徵值 {:}: {:.2e}'.format(i+1, eig_vals[i].real))
特徵向量 1: [[ 0.2049] [ 0.3871] [-0.5465] [-0.7138]] 特徵值 1: 3.23e+01 特徵向量 2: [[-0.009 ] [-0.589 ] [ 0.2543] [-0.767 ]] 特徵值 2: 2.78e-01 特徵向量 3: [[-0.8379] [ 0.1696] [ 0.1229] [ 0.5041]] 特徵值 3: -4.13e-15 特徵向量 4: [[ 0.2 ] [-0.3949] [-0.4567] [ 0.7717]] 特徵值 4: 1.20e-14
輸出結果得到4個特徵值和其所對應的特徵向量。特徵向量直接觀察起來比較麻煩,因爲投影方向在高維上很難理解;特徵值還是比較直觀的,這裏可以認爲特徵值代表的是其所對應特徵向量的重要程度,也就是特徵值越大,其所對應的特徵向量就越重要,所以接下來可以對特徵值按大小進行排序,排在前面的越重要,排在後面的就沒那麼重要了。
1 #特徵值和特徵向量配對 2 eig_pairs = [(np.abs(eig_vals[i]), eig_vecs[:,i]) for i in range(len(eig_vals))] 3 4 # 按特徵值大小進行排序 5 eig_pairs = sorted(eig_pairs, key=lambda k: k[0], reverse=True) 6 7 print('特徵值排序結果:\n') 8 for i in eig_pairs: 9 print(i[0])
特徵值排序結果:
32.27195779972981
0.27756686384004264
1.1953730364935478e-14
4.1311796919088535e-15
1 print('特徵值佔總體百分比:\n') 2 eigv_sum = sum(eig_vals) 3 for i,j in enumerate(eig_pairs): 4 print('特徵值 {0:}: {1:.2%}'.format(i+1, (j[0]/eigv_sum).real))
特徵值佔總體百分比: 特徵值 1: 99.15% 特徵值 2: 0.85% 特徵值 3: 0.00% 特徵值 4: 0.00%
可以看出,打印出來的結果差異很大,第一個特徵值佔據總體的99.15%,第二個特徵值只佔0.85%,第三和第四個特徵值,看起來微不足道。這表示對鳶尾花數據進行降維時,可以把特徵數據降到二維甚至一維,但沒必要降到三維。
既然已經有結論,選擇把數據降到二維,只需選擇特徵值1、特徵值2所對應的特徵向量即可:
1 W = np.hstack((eig_pairs[0][1].reshape(4,1), eig_pairs[1][1].reshape(4,1))) 2 print('矩陣W:\n', W.real)
矩陣W: [[ 0.2049 -0.009 ] [ 0.3871 -0.589 ] [-0.5465 0.2543] [-0.7138 -0.767 ]]
這也是最終所需的投影方向,只需和原始數據組合,就可以得到降維結果:
1 # 執行降維操作 2 X_lda = X.dot(W) 3 X_lda.shape
(150, 2)
現在可以看到數據維度從原始的(150,4)降到(150,2),到此就完成全部的降維工作。接下來對比分析一下降維後結果,爲了方便可視化展示,在原始四維數據集中隨機選擇兩維進行繪圖展示:
1 from matplotlib import pyplot as plt 2 # 可視化展示 3 def plot_step_lda(): 4 5 ax = plt.subplot(111) 6 for label,marker,color in zip( 7 range(1,4),('^', 's', 'o'),('blue', 'red', 'green')): 8 9 plt.scatter(x=X[:,0].real[y == label], 10 y=X[:,1].real[y == label], 11 marker=marker, 12 color=color, 13 alpha=0.5, 14 label=label_dict[label] 15 ) 16 17 plt.xlabel('X[0]') 18 plt.ylabel('X[1]') 19 20 leg = plt.legend(loc='upper right', fancybox=True) 21 leg.get_frame().set_alpha(0.5) 22 plt.title('Original data') 23 24 # 把邊邊角角隱藏起來 25 plt.tick_params(axis="both", which="both", bottom="off", top="off", 26 labelbottom="on", left="off", right="off", labelleft="on") 27 28 # 爲了看的清晰些,儘量簡潔 29 ax.spines["top"].set_visible(False) 30 ax.spines["right"].set_visible(False) 31 ax.spines["bottom"].set_visible(False) 32 ax.spines["left"].set_visible(False) 33 34 plt.grid() 35 plt.tight_layout 36 plt.show() 37 38 plot_step_lda()
從上述輸出結果可以發現,如果對原始數據集隨機取兩維數據,數據集並不能按類別劃分開,很多數據都堆疊在一起(尤其是圖中方塊和圓形數據點)。再來看看降維後的數據點分佈,繪圖代碼保持不變,只需要傳入降維後的兩維數據即可,可以生成圖15-5的輸出。
1 from matplotlib import pyplot as plt 2 # 可視化展示 3 def plot_step_lda(): 4 5 ax = plt.subplot(111) 6 for label,marker,color in zip( 7 range(1,4),('^', 's', 'o'),('blue', 'red', 'green')): 8 9 plt.scatter(x=X_lda[:,0].real[y == label], 10 y=X_lda[:,1].real[y == label], 11 marker=marker, 12 color=color, 13 alpha=0.5, 14 label=label_dict[label] 15 ) 16 17 plt.xlabel('LD1') 18 plt.ylabel('LD2') 19 20 leg = plt.legend(loc='upper right', fancybox=True) 21 leg.get_frame().set_alpha(0.5) 22 plt.title('LDA on iris') 23 24 # 把邊邊角角隱藏起來 25 plt.tick_params(axis="both", which="both", bottom="off", top="off", 26 labelbottom="on", left="off", right="off", labelleft="on") 27 28 # 爲了看的清晰些,儘量簡潔 29 ax.spines["top"].set_visible(False) 30 ax.spines["right"].set_visible(False) 31 ax.spines["bottom"].set_visible(False) 32 ax.spines["left"].set_visible(False) 33 34 plt.grid() 35 plt.tight_layout 36 plt.show() 37 38 plot_step_lda()
圖15-5 線性判別分析降維後數據點分佈
可以明顯看到,座標軸變成LD1與LD2,這就是降維後的結果,從數據點的分佈來看,混雜在一起的數據不多,劃分起來就更容易。這就是經過一步步計算得到的最終降維結果。
當拿到一份規模較大的數據集時,如何選定降維的維數呢?一方面可以通過觀察特徵值排序結果來決定,另一方面還是要通過實驗來進行交叉驗證。
我們肯定希望用更高效、穩定的方法來完成一個實際任務,再來看看sklearn工具包中如何調用線性判別分析進行降維:
1 from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA 2 3 # LDA 4 sklearn_lda = LDA(n_components=2) 5 X_lda_sklearn = sklearn_lda.fit_transform(X, y)
1 def plot_scikit_lda(X, title): 2 3 ax = plt.subplot(111) 4 for label,marker,color in zip( 5 range(1,4),('^', 's', 'o'),('blue', 'red', 'green')): 6 7 plt.scatter(x=X[:,0][y == label], 8 y=X[:,1][y == label] * -1, # flip the figure 9 marker=marker, 10 color=color, 11 alpha=0.5, 12 label=label_dict[label]) 13 14 plt.xlabel('LD1') 15 plt.ylabel('LD2') 16 17 leg = plt.legend(loc='upper right', fancybox=True) 18 leg.get_frame().set_alpha(0.5) 19 plt.title(title) 20 21 # hide axis ticks 22 plt.tick_params(axis="both", which="both", bottom="off", top="off", 23 labelbottom="on", left="off", right="off", labelleft="on") 24 25 # remove axis spines 26 ax.spines["top"].set_visible(False) 27 ax.spines["right"].set_visible(False) 28 ax.spines["bottom"].set_visible(False) 29 ax.spines["left"].set_visible(False) 30 31 plt.grid() 32 plt.tight_layout 33 plt.show()
1 plot_scikit_lda(X_lda_sklearn, title='Default LDA via scikit-learn')
很簡單,僅僅兩步外加一個指定降維到兩維的參數即可,上述代碼可以生成圖15-6的輸出。
可以發現,使用sklearn工具包降維後的結果與自己一步步計算的結果完全一致。
圖15-6 sklearn工具包降維結果
15.2主成分分析
主成分分析(Principal Component Analysis,PCA)是在降維中使用特別廣泛的算法。在使用主成分分析降維的時候沒有束縛,不像線性判別分析,必須要有數據標籤,只要拿到數據,沒有標籤也可以用主成分分析進行降維。所以應該先有一個直觀的認識,主成分分析本質上屬於無監督算法,這也是它流行的主要原因。
15.2.1PCA降維基本知識點
既然不需要標籤,就很難去分析類間與類內距離因素,那麼該怎麼辦呢?PCA的基本思想就是方差,可以想象一下哪些特徵更有價值?應當是那些區別能力更強的特徵。例如我們想比較兩個遊戲玩家的戰鬥力水平。第一個特徵是其所在幫派等級:A玩家,5級幫派;B玩家,4級幫派,A、B玩家的幫派等級看起來差別不大。第二個特徵是其充值金額:A玩家,10000;B玩家,100。A、B玩家的充值金額的差距好像有些大。通過這兩個特徵就可以預估一下哪個玩家戰鬥力更強,答案肯定是A玩家。
現在再來觀察一下這兩個特徵,幫派等級似乎相差不大,不能拉開差距,但是充值金額的差異卻很大。我們希望得到充值金額這種能把不同玩家區分開的特徵。在數學上可以用方差來描述這種數據的離散程度,所以在主成分析中主要依靠方差。
爲了讓大家更好地理解主成分分析,下面介紹一些基本概念。
(1)向量的表示。假設有向量(3,2),如圖15-7所示。爲什麼向量可以表示爲(3,2),這是在直角座標系中的表示,如果座標系變了,向量的表示形式也要發生變換。
實際上該向量可以表示成線性組合3·(1,0)T+2·(0,1)T,其中(1,0)和(0,1)就稱爲二維空間中的一組基。
(3)基變換。大家常見的座標系都是正交的,即內積爲0,兩兩相互垂直,並且線性無關。爲什麼基都是這樣的呢?如果不垂直,那麼肯定線性相關,能用一個表示另一個,此時基就會失去意義,所以基的出發點就是要正交。
圖15-7 向量的組成
基也可以進行變換,將一個向量從一組基變換到另一組基中。例如新的座標系的兩個基分別是(),因此向量(3,2)映射到這個新的座標系中,可以通過下面變換實現:
(3)方差和協方差。方差(variance)相當於特徵辨識度,其值越大越好。協方差(covariance)就是不同特徵之間的相關程度,協方差的計算式式爲:
如果兩個變量的變化趨勢相同,例如隨着身高的增長,體重也增長,此時它們的協方差值就會比較大,表示正相關。而方差又描述了各自的辨識能力,接下來就要把這些知識點穿插在一起。
15.2.2PCA優化目標求解
對於降維任務,無非就是將原始數據特徵投影到一個更合適的空間,結合基的概念,這就相當於由一組基變換到另一組基,變換的過程要求特徵變得更有價值,也就是方差能夠更大。所以現在已經明確基本目標了:找到一組基,使得變換後的特徵方差越大越好。
假設找到了第一個合適的投影方向,這個方向能夠使得方差最大,對於降維任務來說,一般情況下並不是降到一維,接下來肯定要找方差第二大的方向。方差第二大的方向理論上應該與第一方向非常接近,甚至重合,這樣才能保證方差最大,如圖15-8所示。
在這種情況下,看似可以得到無數多個方差非常大的方向,但是想一想它們能組成想要的基嗎?不能,因爲沒有滿足基的最基本要求——線性無關,也就是相互垂直正交。所以在尋找方差最大的方向的同時,還要使得各個投影方向能夠正交,即協方差應當等於0,表示完全獨立無關。所以在選擇基的時候,一方面要儘可能地找方差的最大方向,另一方面要在其正交方向上繼續尋找方差第二大的方向,以此類推。
圖15-8 方差方向選擇
解釋PCA中要求解的目標後,接下來就是在數學上將它表達出來。先來看一下協方差矩陣,爲了簡便,可以把數據中各個特徵的均值默認爲0,也可以認爲數據已經進行過標準化處理。其計算式如下:
其中,X爲實際的數據。包含2個特徵a和b,一共有m個樣本。
此時協方差矩陣爲:
先觀察一下協方差矩陣結果,其主對角線上的元素就是各個特徵的方差(均值爲0時),而非主對角線的上元素恰好是特徵之間的協方差。按照目標函數的要求,首先應當使得方差越大越好,並且確保協方差爲0,這就需要對協方差矩陣做對角化。
從一個n行n列的實對稱矩陣中一定可以找到n個單位正交特徵向量E=(e1,e2 ,… ,en),以完成對角化的操作:
式(15.21)中的協方差矩陣恰好滿足上述要求。假設需要將一組N維向量降爲K維(K大於0,小於N),目標是選擇K個單位正交基,使原始數據變換到這組基上後,各字段兩兩間協方差爲0,各字段本身的方差儘可能大。當得到其協方差矩陣後,對其進行對角化操作,即可使得除主對角線上元素之外都爲0。
其中對角線上的元素就是矩陣的特徵值,這與線性判別分析很像,還是先把特徵值按從大到小的順序進行排列,找到前K個最大特徵值對應的特徵向量,接下來就是進行投影變換。
按照給定PCA優化目標,基本流程如下。
- 第①步:數據預處理,只有數值數據纔可以進行PCA降維。
- 第②步:計算樣本數據的協方差矩陣。
- 第③步:求解協方差矩陣的特徵值和特徵向量。
- 第④步:將特徵值按照從大到小的順序排列,選擇其中較大的K個,然後將其對應的K個特徵向量組成投影矩陣。
- 第⑤步:將樣本點投影計算,完成PCA降維任務。
15.2.3Python實現PCA降維
接下來通過一個實例介紹如何使用PCA處理實際問題,同樣使用鳶尾花數據集,目的依舊是完成降維任務,下面就來看一下PCA是怎麼實現的。
第①步:導入數據。
1 import numpy as np 2 import pandas as pd 3 4 # 讀取數據集 5 df = pd.read_csv('iris.data') 6 # 原始數據沒有給定列名的時候需要我們自己加上 7 df.columns=['sepal_len', 'sepal_wid', 'petal_len', 'petal_wid', 'class'] 8 df.head()
sepal_len sepal_wid petal_len petal_wid class 0 4.9 3.0 1.4 0.2 Iris-setosa 1 4.7 3.2 1.3 0.2 Iris-setosa 2 4.6 3.1 1.5 0.2 Iris-setosa 3 5.0 3.6 1.4 0.2 Iris-setosa 4 5.4 3.9 1.7 0.4 Iris-setosa
第②步:展示數據特徵。
1 # 把數據分成特徵和標籤 2 3 X = df.iloc[:,0:4].values 4 y = df.iloc[:,4].values 5 6 from matplotlib import pyplot as plt 7 8 # 展示我們標籤用的 9 label_dict = {1: 'Iris-Setosa', 10 2: 'Iris-Versicolor', 11 3: 'Iris-Virgnica'} 12 13 # 展示特徵用的 14 feature_dict = {0: 'sepal length [cm]', 15 1: 'sepal width [cm]', 16 2: 'petal length [cm]', 17 3: 'petal width [cm]'} 18 19 # 指定繪圖區域大小 20 plt.figure(figsize=(8, 6)) 21 for cnt in range(4): 22 # 這裏用子圖來呈現4個特徵 23 plt.subplot(2, 2, cnt+1) 24 for lab in ('Iris-setosa', 'Iris-versicolor', 'Iris-virginica'): 25 plt.hist(X[y==lab, cnt], 26 label=lab, 27 bins=10, 28 alpha=0.3,) 29 plt.xlabel(feature_dict[cnt]) 30 plt.legend(loc='upper right', fancybox=True, fontsize=8) 31 32 plt.tight_layout() 33 plt.show()
上述代碼可以生成圖15-9的輸出。可以看出,有些特徵區別能力較強,能把3種花各自呈現出來;有些特徵區別能力較弱,部分特徵數據樣本混雜在一起。
第③步:數據的標準化。一般情況下,在進行訓練前,數據經常需要進行標準化處理,可以使用sklearn庫中的StandardScaler方法進行標準化處理,代碼如下:
圖15-9 鳶尾花數據集特徵
1 from sklearn.preprocessing import StandardScaler
2 X_std = StandardScaler().fit_transform(X)
第④步:計算協方差矩陣。按照式(15.19)定義的協方差矩陣公式計算:
1 mean_vec = np.mean(X_std, axis=0) 2 cov_mat = (X_std - mean_vec).T.dot((X_std - mean_vec)) / (X_std.shape[0]-1) 3 print('協方差矩陣 \n%s' %cov_mat)
協方差矩陣 [[ 1.00675676 -0.10448539 0.87716999 0.82249094] [-0.10448539 1.00675676 -0.41802325 -0.35310295] [ 0.87716999 -0.41802325 1.00675676 0.96881642] [ 0.82249094 -0.35310295 0.96881642 1.00675676]]
或者可以直接使用Numpy工具包來計算協方差,結果是一樣的:
1 print('NumPy 計算協方差矩陣: \n%s' %np.cov(X_std.T))
NumPy 計算協方差矩陣: [[ 1.00675676 -0.10448539 0.87716999 0.82249094] [-0.10448539 1.00675676 -0.41802325 -0.35310295] [ 0.87716999 -0.41802325 1.00675676 0.96881642] [ 0.82249094 -0.35310295 0.96881642 1.00675676]]
第⑤步:求特徵值與特徵向量。
1 cov_mat = np.cov(X_std.T) 2 3 eig_vals, eig_vecs = np.linalg.eig(cov_mat) 4 5 print('特徵向量 \n%s' %eig_vecs) 6 print('\n特徵值 \n%s' %eig_vals)
特徵向量 [[ 0.52308496 -0.36956962 -0.72154279 0.26301409] [-0.25956935 -0.92681168 0.2411952 -0.12437342] [ 0.58184289 -0.01912775 0.13962963 -0.80099722] [ 0.56609604 -0.06381646 0.63380158 0.52321917]] 特徵值 [2.92442837 0.93215233 0.14946373 0.02098259]
第⑥步:按照特徵值大小進行排序。
1 # 把特徵值和特徵向量對應起來 2 eig_pairs = [(np.abs(eig_vals[i]), eig_vecs[:,i]) for i in range(len(eig_vals))] 3 print (eig_pairs) 4 print ('----------') 5 # 把它們按照特徵值大小進行排序 6 eig_pairs.sort(key=lambda x: x[0], reverse=True) 7 8 # 打印排序結果 9 print('特徵值又大到小排序結果:') 10 for i in eig_pairs: 11 print(i[0])
[(2.9244283691111135, array([ 0.52308496, -0.25956935, 0.58184289, 0.56609604])),
(0.9321523302535066, array([-0.36956962, -0.92681168, -0.01912775, -0.06381646])),
(0.1494637348981336, array([-0.72154279, 0.2411952 , 0.13962963, 0.63380158])),
(0.02098259276427038, array([ 0.26301409, -0.12437342, -0.80099722, 0.52321917]))] ---------- 特徵值又大到小排序結果: 2.9244283691111135 0.9321523302535066 0.1494637348981336 0.02098259276427038
第⑦步:計算累加貢獻率。同樣可以用累加的方法,將特徵向量累加起來,當其超過一定百分比時,就選擇其爲降維後的維度大小:
1 # 計算累加結果 2 tot = sum(eig_vals) 3 var_exp = [(i / tot)*100 for i in sorted(eig_vals, reverse=True)] 4 print (var_exp) 5 cum_var_exp = np.cumsum(var_exp) 6 cum_var_exp
[72.6200333269203, 23.14740685864414, 3.7115155645845284, 0.5210442498510098]
array([ 72.62003333, 95.76744019, 99.47895575, 100. ])
可以發現,使用前兩個特徵值時,其對應的累計貢獻率已經超過95%,所以選擇降到二維。也可以通過畫圖的形式,這樣更直接:
1 a = np.array([1,2,3,4]) 2 print (a) 3 print ('-----------') 4 print (np.cumsum(a)) 5 6 plt.figure(figsize=(6, 4)) 7 8 plt.bar(range(4), var_exp, alpha=0.5, align='center', 9 label='individual explained variance') 10 plt.step(range(4), cum_var_exp, where='mid', 11 label='cumulative explained variance') 12 plt.ylabel('Explained variance ratio') 13 plt.xlabel('Principal components') 14 plt.legend(loc='best') 15 plt.tight_layout() 16 plt.show()
上述代碼可以生成圖15-10的輸出。
圖15-10 累加特徵值
第⑧步:完成PCA降維。接下來把特徵向量組合起來完成降維工作:
1 matrix_w = np.hstack((eig_pairs[0][1].reshape(4,1), 2 eig_pairs[1][1].reshape(4,1))) 3 4 print('Matrix W:\n', matrix_w)
Matrix W: [[ 0.52308496 -0.36956962] [-0.25956935 -0.92681168] [ 0.58184289 -0.01912775] [ 0.56609604 -0.06381646]]
1 Y = X_std.dot(matrix_w) 2 Y
array([[-2.10795032, 0.64427554], [-2.38797131, 0.30583307], [-2.32487909, 0.56292316], [-2.40508635, -0.687591 ], [-2.08320351, -1.53025171], [-2.4636848 , -0.08795413], [-2.25174963, -0.25964365], [-2.3645813 , 1.08255676], [-2.20946338, 0.43707676], [-2.17862017, -1.08221046], [-2.34525657, -0.17122946], [-2.24590315, 0.6974389 ], [-2.66214582, 0.92447316], [-2.2050227 , -1.90150522], [-2.25993023, -2.73492274], [-2.21591283, -1.52588897], [-2.20705382, -0.52623535], [-1.9077081 , -1.4415791 ], [-2.35411558, -1.17088308], [-1.93202643, -0.44083479], [-2.21942518, -0.96477499], [-2.79116421, -0.50421849], [-1.83814105, -0.11729122], [-2.24572458, -0.17450151], [-1.97825353, 0.59734172], [-2.06935091, -0.27755619], [-2.18514506, -0.56366755], [-2.15824269, -0.34805785], [-2.28843932, 0.30256102], [-2.16501749, 0.47232759], [-1.8491597 , -0.45547527], [-2.62023392, -1.84237072], [-2.44885384, -2.1984673 ], [-2.20946338, 0.43707676], [-2.23112223, 0.17266644], [-2.06147331, -0.6957435 ], [-2.20946338, 0.43707676], [-2.45783833, 0.86912843], [-2.1884075 , -0.30439609], [-2.30357329, -0.48039222], [-1.89932763, 2.31759817], [-2.57799771, 0.4400904 ], [-1.98020921, -0.50889705], [-2.14679556, -1.18365675], [-2.09668176, 0.68061705], [-2.39554894, -1.16356284], [-2.41813611, 0.34949483], [-2.24196231, -1.03745802], [-2.22484727, -0.04403395], [ 1.09225538, -0.86148748], [ 0.72045861, -0.59920238], [ 1.2299583 , -0.61280832], [ 0.37598859, 1.756516 ], [ 1.05729685, 0.21303055], [ 0.36816104, 0.58896262], [ 0.73800214, -0.77956125], [-0.52021731, 1.84337921], [ 0.9113379 , -0.02941906], [-0.01292322, 1.02537703], [-0.15020174, 2.65452146], [ 0.42437533, 0.05686991], [ 0.52894687, 1.77250558], [ 0.70241525, 0.18484154], [-0.05385675, 0.42901221], [ 0.86277668, -0.50943908], [ 0.33388091, 0.18785518], [ 0.13504146, 0.7883247 ], [ 1.19457128, 1.63549265], [ 0.13677262, 1.30063807], [ 0.72711201, -0.40394501], [ 0.45564294, 0.41540628], [ 1.21038365, 0.94282042], [ 0.61327355, 0.4161824 ], [ 0.68512164, 0.06335788], [ 0.85951424, -0.25016762], [ 1.23906722, 0.08500278], [ 1.34575245, -0.32669695], [ 0.64732915, 0.22336443], [-0.06728496, 1.05414028], [ 0.10033285, 1.56100021], [-0.00745518, 1.57050182], [ 0.2179082 , 0.77368423], [ 1.04116321, 0.63744742], [ 0.20719664, 0.27736006], [ 0.42154138, -0.85764157], [ 1.03691937, -0.52112206], [ 1.015435 , 1.39413373], [ 0.0519502 , 0.20903977], [ 0.25582921, 1.32747797], [ 0.25384813, 1.11700714], [ 0.60915822, -0.02858679], [ 0.31116522, 0.98711256], [-0.39679548, 2.01314578], [ 0.26536661, 0.85150613], [ 0.07385897, 0.17160757], [ 0.20854936, 0.37771566], [ 0.55843737, 0.15286277], [-0.47853403, 1.53421644], [ 0.23545172, 0.59332536], [ 1.8408037 , -0.86943848], [ 1.13831104, 0.70171953], [ 2.19615974, -0.54916658], [ 1.42613827, 0.05187679], [ 1.8575403 , -0.28797217], [ 2.74511173, -0.78056359], [ 0.34010583, 1.5568955 ], [ 2.29180093, -0.40328242], [ 1.98618025, 0.72876171], [ 2.26382116, -1.91685818], [ 1.35591821, -0.69255356], [ 1.58471851, 0.43102351], [ 1.87342402, -0.41054652], [ 1.23656166, 1.16818977], [ 1.45128483, 0.4451459 ], [ 1.58276283, -0.67521526], [ 1.45956552, -0.25105642], [ 2.43560434, -2.55096977], [ 3.29752602, 0.01266612], [ 1.23377366, 1.71954411], [ 2.03218282, -0.90334021], [ 0.95980311, 0.57047585], [ 2.88717988, -0.38895776], [ 1.31405636, 0.48854962], [ 1.69619746, -1.01153249], [ 1.94868773, -0.99881497], [ 1.1574572 , 0.31987373], [ 1.007133 , -0.06550254], [ 1.7733922 , 0.19641059], [ 1.85327106, -0.55077372], [ 2.4234788 , -0.2397454 ], [ 2.31353522, -2.62038074], [ 1.84800289, 0.18799967], [ 1.09649923, 0.29708201], [ 1.1812503 , 0.81858241], [ 2.79178861, -0.83668445], [ 1.57340399, -1.07118383], [ 1.33614369, -0.420823 ], [ 0.91061354, -0.01965942], [ 1.84350913, -0.66872729], [ 2.00701161, -0.60663655], [ 1.89319854, -0.68227708], [ 1.13831104, 0.70171953], [ 2.03519535, -0.86076914], [ 1.99464025, -1.04517619], [ 1.85977129, -0.37934387], [ 1.54200377, 0.90808604], [ 1.50925493, -0.26460621], [ 1.3690965 , -1.01583909], [ 0.94680339, 0.02182097]])
輸出結果顯示,使用PCA降維算法把原數據矩陣從150×4降到150×2。
第⑨步:可視化對比降維前後數據的分佈。由於數據具有4個特徵,無法在平面圖中顯示,因此只使用兩維特徵顯示數據,代碼如下:
1 plt.figure(figsize=(6, 4)) 2 for lab, col in zip(('Iris-setosa', 'Iris-versicolor', 'Iris-virginica'), 3 ('blue', 'red', 'green')): 4 plt.scatter(X[y==lab, 0], 5 X[y==lab, 1], 6 label=lab, 7 c=col) 8 plt.xlabel('sepal_len') 9 plt.ylabel('sepal_wid') 10 plt.legend(loc='best') 11 plt.tight_layout() 12 plt.show()
上面代碼只使用前兩個特徵顯示3類數據,如圖15-11所示,看起來3類鳶尾花相互交疊在一起,不容易區分開。
圖15-11 原始數據集數據樣本分佈
下面看看使用PCA降維後的情況,代碼如下:
1 plt.figure(figsize=(6, 4)) 2 for lab, col in zip(('Iris-setosa', 'Iris-versicolor', 'Iris-virginica'), 3 ('blue', 'red', 'green')): 4 plt.scatter(Y[y==lab, 0], 5 Y[y==lab, 1], 6 label=lab, 7 c=col) 8 plt.xlabel('Principal Component 1') 9 plt.ylabel('Principal Component 2') 10 plt.legend(loc='lower center') 11 plt.tight_layout() 12 plt.show()
上述代碼使用降維以後的二維特徵作爲x,y軸,顯示如圖15-12所示,對比這兩個結果,可以看出經過PCA降維後的結果更容易區別。
圖15-12 PCA降維結果
本章小結:
本章介紹了兩種非常實用的降維方法:線性判別分析和主成分分析。其中線性判別分析是有監督算法,需要有標籤才能計算;而主成分析是無監督算法,無須標籤,直接就可以對數據進行分析。那麼,在實際建模任務中,到底用哪種降維方法呢?沒有固定的模式,需要大家通過實驗對比確定,例如,取得一份數據後可以分多步走,以對比不同策略的結果。
降維可以大大減少算法的計算量,加快程序執行的速度,遇到特徵非常多的數據時就可以大顯身手。但是降維算法本身最大的問題就是降維後得到結果的物理含義很難解釋,只能說這就是計算機看來最好的降維特徵,而不能具體化其含義。此時如果想進一步對結果進行分析就有些麻煩,因爲其中每一個特徵指標的含義都只是數值,而沒有具體指代。
Python案例中使用非常簡單的鳶尾花數據集,按照原理推導的流程一步步完成了整個任務,大家在練習的時候也可以選用稍微複雜一點的數據集來複現算法。
第15章完。
該書資源下載,請至異步社區:https://www.epubit.com