一、前言
KNN 的英文叫 K-Nearest Neighbor,應該算是資料探勘演算法中最簡單的一種。
先用一個例子體會下。
/*請尊重作者勞動成果,轉載請標明原文連結:*/
/* https://www.cnblogs.com/jpcflyer/p/11111817.html * /
假設,我們想對電影的型別進行分類,統計了電影中打鬥次數、接吻次數,當然還有其他的指標也可以被統計到,如下表所示。
我們很容易理解《戰狼》《紅海行動》《碟中諜 6》是動作片,《前任 3》《春嬌救志明》《泰坦尼克號》是愛情片,但是有沒有一種方法讓機器也可以掌握這個分類的規則,當有一部新電影的時候,也可以對它的型別自動分類呢?
我們可以把打鬥次數看成 X 軸,接吻次數看成 Y 軸,然後在二維的座標軸上,對這幾部電影進行標記,如下圖所示。對於未知的電影 A,座標為 (x,y),我們需要看下離電影 A 最近的都有哪些電影,這些電影中的大多數屬於哪個分類,那麼電影 A 就屬於哪個分類。實際操作中,我們還需要確定一個 K 值,也就是我們要觀察離電影 A 最近的電影有多少個。
二、KNN 的工作原理
“近朱者赤,近墨者黑”可以說是 KNN 的工作原理。整個計算過程分為三步:
1. 計算待分類物體與其他物體之間的距離;
2. 統計距離最近的 K 個鄰居;
3.對於 K 個最近的鄰居,它們屬於哪個分類最多,待分類物體就屬於哪一類。
K 值如何選擇
你能看出整個 KNN 的分類過程,K 值的選擇還是很重要的。那麼問題來了,K 值選擇多少是適合的呢?
如果 K 值比較小,就相當於未分類物體與它的鄰居非常接近才行。這樣產生的一個問題就是,如果鄰居點是個噪聲點,那麼未分類物體的分類也會產生誤差,這樣 KNN 分類就會產生過擬合。
如果 K 值比較大,相當於距離過遠的點也會對未知物體的分類產生影響,雖然這種情況的好處是魯棒性強,但是不足也很明顯,會產生欠擬合情況,也就是沒有把未分類物體真正分類出來。
所以 K 值應該是個實踐出來的結果,並不是我們事先而定的。在工程上,我們一般採用交叉驗證的方式選取 K 值。
交叉驗證的思路就是,把樣本集中的大部分樣本作為訓練集,剩餘的小部分樣本用於預測,來驗證分類模型的準確性。所以在 KNN 演算法中,我們一般會把 K 值選取在較小的範圍內,同時在驗證集上準確率最高的那一個最終確定作為 K 值。
距離如何計算
在 KNN 演算法中,還有一個重要的計算就是關於距離的度量。兩個樣本點之間的距離代表了這兩個樣本之間的相似度。距離越大,差異性越大;距離越小,相似度越大。
關於距離的計算方式有下面五種方式:
歐氏距離;
曼哈頓距離;
閔可夫斯基距離;
切比雪夫距離;
餘弦距離。
其中前三種距離是 KNN 中最常用的距離,現在分別講解下。
歐氏距離是我們最常用的距離公式,也叫做歐幾里得距離。在二維空間中,兩點的歐式距離就是:
同理,我們也可以求得兩點在 n 維空間中的距離:
曼哈頓距離
在幾何空間中用的比較多。曼哈頓距離等於兩個點在座標系上絕對軸距總和。用公式表示就是:
閔可夫斯基距離
不是一個距離,而是一組距離的定義。對於 n 維空間中的兩個點 x(x1,x2,…,xn) 和 y(y1,y2,…,yn) , x 和 y 兩點之間的閔可夫斯基距離為:
其中 p 代表空間的維數,當 p=1 時,就是曼哈頓距離;當 p=2 時,就是歐氏距離;當 p→∞時,就是切比雪夫距離。
那麼切比雪夫距離 怎麼計算呢?二個點之間的切比雪夫距離就是這兩個點座標數值差的絕對值的最大值,用數學表示就是:max(|x1-y1|,|x2-y2|)。
餘弦距離 實際上計算的是兩個向量的夾角,是在方向上計算兩者之間的差異,對絕對數值不敏感。在興趣相關性比較上,角度關係比距離的絕對值更重要,因此餘弦距離可以用於衡量使用者對內容興趣的區分度。比如我們用搜尋引擎搜尋某個關鍵詞,它還會給你推薦其他的相關搜尋,這些推薦的關鍵詞就是採用餘弦距離計算得出的。
KD 樹
其實從上文你也能看出來,KNN 的計算過程是大量計算樣本點之間的距離。為了減少計算距離次數,提升 KNN 的搜尋效率,人們提出了 KD 樹(K-Dimensional 的縮寫)。KD 樹是對資料點在 K 維空間中劃分的一種資料結構。在 KD 樹的構造中,每個節點都是 k 維數值點的二叉樹。既然是二叉樹,就可以採用二叉樹的增刪改查操作,這樣就大大提升了搜尋效率。
在這裡,我們不需要對 KD 樹的數學原理了解太多,你只需要知道它是一個二叉樹的資料結構,方便儲存 K 維空間的資料就可以了。而且在 sklearn 中,我們直接可以呼叫 KD 樹,很方便。
用 KNN 做迴歸
KNN 不僅可以做分類,還可以做迴歸。首先講下什麼是迴歸。在開頭電影這個案例中,如果想要對未知電影進行型別劃分,這是一個分類問題。首先看一下要分類的未知電影,離它最近的 K 部電影大多數屬於哪個分類,這部電影就屬於哪個分類。
如果是一部新電影,已知它是愛情片,想要知道它的打鬥次數、接吻次數可能是多少,這就是一個迴歸問題。
那麼 KNN 如何做迴歸呢?
對於一個新點,我們需要找出這個點的 K 個最近鄰居,然後將這些鄰居的屬性的平均值賦給該點,就可以得到該點的屬性。當然不同鄰居的影響力權重可以設定成不同的。舉個例子,比如一部電影 A,已知它是動作片,當 K=3 時,最近的 3 部電影是《戰狼》,《紅海行動》和《碟中諜 6》,那麼它的打鬥次數和接吻次數的預估值分別為 (100+95+105)/3=100 次、(5+3+31)/3=13 次。
三、 如何在 sklearn 中使用 KNN
接下來,我們先看下如何在 sklearn 中使用 KNN 演算法,然後通過 sklearn 中自帶的手寫數字資料集來進行實戰。
在 Python 的 sklearn 工具包中有 KNN 演算法。KNN 既可以做分類器,也可以做迴歸。如果是做分類,你需要引用:
1 from sklearn.neighbors import KNeighborsClassifier
如果是做迴歸,你需要引用:
1 from sklearn.neighbors import KNeighborsRegressor
從名字上你也能看出來 Classifier 對應的是分類,Regressor 對應的是迴歸。一般來說如果一個演算法有 Classifier 類,都能找到相應的 Regressor 類。比如在決策樹分類中,你可以使用 DecisionTreeClassifier,也可以使用決策樹來做迴歸 DecisionTreeRegressor。
好了,我們看下如何在 sklearn 中建立 KNN 分類器。
這裡,我們使用建構函式 KNeighborsClassifier(n_neighbors=5, weights=‘uniform’, algorithm=‘auto’, leaf_size=30),這裡有幾個比較主要的引數,我分別來講解下:
1.n_neighbors:即 KNN 中的 K 值,代表的是鄰居的數量。K 值如果比較小,會造成過擬合。如果 K 值比較大,無法將未知物體分類出來。一般我們使用預設值 5。
2.weights:是用來確定鄰居的權重,有三種方式:
weights=uniform,代表所有鄰居的權重相同;
weights=distance,代表權重是距離的倒數,即與距離成反比;
自定義函式,你可以自定義不同距離所對應的權重。大部分情況下不需要自己定義函式。
3.algorithm:用來規定計算鄰居的方法,它有四種方式:
algorithm=auto,根據資料的情況自動選擇適合的演算法,預設情況選擇 auto;
algorithm=kd_tree,也叫作 KD 樹,是多維空間的資料結構,方便對關鍵資料進行檢索,不過 KD 樹適用於維度少的情況,一般維數不超過 20,如果維數大於 20 之後,效率反而會下降;
algorithm=ball_tree,也叫作球樹,它和 KD 樹一樣都是多維空間的資料結果,不同於 KD 樹,球樹更適用於維度大的情況;
algorithm=brute,也叫作暴力搜尋,它和 KD 樹不同的地方是在於採用的是線性掃描,而不是通過構造樹結構進行快速檢索。當訓練集大的時候,效率很低。
4.leaf_size:代表構造 KD 樹或球樹時的葉子數,預設是 30,調整 leaf_size 會影響到樹的構造和搜尋速度。
建立完 KNN 分類器之後,我們就可以輸入訓練集對它進行訓練,這裡我們使用 fit() 函式,傳入訓練集中的樣本特徵矩陣和分類標識,會自動得到訓練好的 KNN 分類器。然後可以使用 predict() 函式來對結果進行預測,這裡傳入測試集的特徵矩陣,可以得到測試集的預測分類結果。
四、 如何用 KNN 對手寫數字進行識別分類
手寫數字資料集是個非常有名的用於影象識別的資料集。數字識別的過程就是將這些圖片與分類結果 0-9 一一對應起來。完整的手寫數字資料集 MNIST 裡面包括了 60000 個訓練樣本,以及 10000 個測試樣本。如果你學習深度學習的話,MNIST 基本上是你接觸的第一個資料集。
今天我們用 sklearn 自帶的手寫數字資料集做 KNN 分類,你可以把這個資料集理解成一個簡版的 MNIST 資料集,它只包括了 1797 幅數字影象,每幅影象大小是 8*8 畫素。
我們先來規劃下整個 KNN 分類的流程:
資料載入:我們可以直接從 sklearn 中載入自帶的手寫數字資料集;
準備階段:在這個階段中,我們需要對資料集有個初步的瞭解,比如樣本的個數、影象長什麼樣、識別結果是怎樣的。你可以通過視覺化的方式來檢視影象的呈現。通過資料規範化可以讓資料都在同一個數量級的維度。另外,因為訓練集是影象,每幅影象是個 8*8 的矩陣,我們不需要對它進行特徵選擇,將全部的影象資料作為特徵值矩陣即可;
分類階段:通過訓練可以得到分類器,然後用測試集進行準確率的計算。
好了,按照上面的步驟,我們一起來實現下這個專案。
首先是載入資料和對資料的探索:
1 # 載入資料 2 digits = load_digits() 3 data = digits.data 4 # 資料探索 5 print(data.shape) 6 # 檢視第一幅影象 7 print(digits.images[0]) 8 # 第一幅影象代表的數字含義 9 print(digits.target[0]) 10 # 將第一幅影象顯示出來 11 plt.gray() 12 plt.imshow(digits.images[0]) 13 plt.show()
執行結果:
1 (1797, 64) 2 [[ 0. 0. 5. 13. 9. 1. 0. 0.] 3 [ 0. 0. 13. 15. 10. 15. 5. 0.] 4 [ 0. 3. 15. 2. 0. 11. 8. 0.] 5 [ 0. 4. 12. 0. 0. 8. 8. 0.] 6 [ 0. 5. 8. 0. 0. 9. 8. 0.] 7 [ 0. 4. 11. 0. 1. 12. 7. 0.] 8 [ 0. 2. 14. 5. 10. 12. 0. 0.] 9 [ 0. 0. 6. 13. 10. 0. 0. 0.]] 10 0
我們對原始資料集中的第一幅進行資料視覺化,可以看到影象是個 8*8 的畫素矩陣,上面這幅影象是一個“0”,從訓練集的分類標註中我們也可以看到分類標註為“0”。
sklearn 自帶的手寫數字資料集一共包括了 1797 個樣本,每幅影象都是 8*8 畫素的矩陣。因為並沒有專門的測試集,所以我們需要對資料集做劃分,劃分成訓練集和測試集。因為 KNN 演算法和距離定義相關,我們需要對資料進行規範化處理,採用 Z-Score 規範化,程式碼如下:
1 # 分割資料,將 25% 的資料作為測試集,其餘作為訓練集(你也可以指定其他比例的資料作為訓練集) 2 train_x, test_x, train_y, test_y = train_test_split(data, digits.target, test_size=0.25, random_state=33) 3 # 採用 Z-Score 規範化 4 ss = preprocessing.StandardScaler() 5 train_ss_x = ss.fit_transform(train_x) 6 test_ss_x = ss.transform(test_x) 7 然後我們構造一個 KNN 分類器 knn,把訓練集的資料傳入構造好的 knn,並通過測試集進行結果預測,與測試集的結果進行對比,得到 KNN 分類器準確率,程式碼如下: 8 # 建立 KNN 分類器 9 knn = KNeighborsClassifier() 10 knn.fit(train_ss_x, train_y) 11 predict_y = knn.predict(test_ss_x) 12 print("KNN 準確率: %.4lf" % accuracy_score(predict_y, test_y))
執行結果:
1 KNN 準確率: 0.975
好了,這樣我們就構造好了一個 KNN 分類器。之前我們還講過 SVM、樸素貝葉斯和決策樹分類。我們用手寫數字資料集一起來訓練下這些分類器,然後對比下哪個分類器的效果更好。程式碼如下:
1 # 建立 SVM 分類器 2 svm = SVC() 3 svm.fit(train_ss_x, train_y) 4 predict_y=svm.predict(test_ss_x) 5 print('SVM 準確率: %0.4lf' % accuracy_score(predict_y, test_y)) 6 # 採用 Min-Max 規範化 7 mm = preprocessing.MinMaxScaler() 8 train_mm_x = mm.fit_transform(train_x) 9 test_mm_x = mm.transform(test_x) 10 # 建立 Naive Bayes 分類器 11 mnb = MultinomialNB() 12 mnb.fit(train_mm_x, train_y) 13 predict_y = mnb.predict(test_mm_x) 14 print(" 多項式樸素貝葉斯準確率: %.4lf" % accuracy_score(predict_y, test_y)) 15 # 建立 CART 決策樹分類器 16 dtc = DecisionTreeClassifier() 17 dtc.fit(train_mm_x, train_y) 18 predict_y = dtc.predict(test_mm_x) 19 print("CART 決策樹準確率: %.4lf" % accuracy_score(predict_y, test_y))
執行結果如下:
1 SVM 準確率: 0.9867 2 多項式樸素貝葉斯準確率: 0.8844 3 CART 決策樹準確率: 0.8556
這裡需要注意的是,我們在做多項式樸素貝葉斯分類的時候,傳入的資料不能有負數。因為 Z-Score 會將數值規範化為一個標準的正態分佈,即均值為 0,方差為 1,數值會包含負數。因此我們需要採用 Min-Max 規範化,將資料規範化到 [0,1] 範圍內。
好了,我們整理下這 4 個分類器的結果。
你能看出來 KNN 的準確率還是不錯的,和 SVM 不相上下。
你可以自己跑一遍整個程式碼,在執行前還需要 import 相關的工具包(下面的這些工具包你都會用到,所以都需要引用):
1 from sklearn.model_selection import train_test_split 2 from sklearn import preprocessing 3 from sklearn.metrics import accuracy_score 4 from sklearn.datasets import load_digits 5 from sklearn.neighbors import KNeighborsClassifier 6 from sklearn.svm import SVC 7 from sklearn.naive_bayes import MultinomialNB 8 from sklearn.tree import DecisionTreeClassifier 9 import matplotlib.pyplot as plt
程式碼中,我使用了 train_test_split 做資料集的拆分,使用 matplotlib.pyplot 工具包顯示影象,使用 accuracy_score 進行分類器準確率的計算,使用 preprocessing 中的 StandardScaler 和 MinMaxScaler 做資料的規範化。