一、簡介
KNN(k-nearst neighbors,KNN)作為機器學習算法中的一種非常基本的算法,也正是因為其原理簡單,被廣泛應用於電影/音樂推薦等方面,即有些時候我們很難去建立確切的模型來描述幾種類別的具體表征特點,就可以利用天然的臨近關系來進行分類;
二、原理
KNN算法主要用於分類任務中,用於基於新樣本與已有樣本的距離來為其賦以所屬的類別,即使用一個新樣本k個近鄰的信息來對該無標記的樣本進行分類,k是KNN中最基本的參數,表示任意數目的近鄰,在k確定后,KNN算法還依賴於一個帶標注的訓練集,對沒有分類的測試集中的樣本進行分類,KNN確定訓練集中與該新樣本“距離”最近的k個訓練集樣本,並將新樣本類別判定到這k個近鄰中占比最大的那個類中,下面是一個廣泛傳播的KNN示例(圖源自網絡):
藍色與紅色的樣本點即為上述的已標注訓練樣本集,綠色樣本點為待標記的新樣本,這時k取值的重要性就體現了出來:
1.當k=3時,在圖中實線圈中包含了離新樣本最近的3個訓練樣本點,因為此時紅色樣本數目為2,藍色樣本數目為1,根據最大占比原則,綠色樣本點自然而然的被判定為紅色所屬類別;
2.當k=5時,在圖中虛線圈中包含了離新樣本最近的5個訓練樣本點,因為此時藍色樣本數目為3,紅色樣本數目為2,根據最大占比原則,綠色樣本點被判定為藍色所屬類別;
從上面的例子中可以看出,不同的k值對最終的分類結果的影響非常明顯,一般來說,k值滿足下列規律:k值越大,算法的泛化能力越強,在訓練集上的表現越差;k值越小,算法在訓練集上的誤差越小,也更有可能導致泛化能力變差;
而在距離的衡量上,一般來說,歐氏距離是最常見的,即:
有時也會用到一些特殊的距離,譬如曼哈頓距離(即絕對值距離):
KNN的過程就是找出距離最小的k個訓練樣本點的過程,而針對數據量大小的不同,有幾種不同的算法,下面一一列舉:
蠻力法(brute force)
對快速計算最近鄰的探索是機器學習中一個活躍的領域,最單純的近鄰搜索方法使用蠻力的方法,也就是直接去運算樣本集中每個點與待分類樣本間的距離,那么對於含有N個樣本維數為D的情況下,蠻力運算的時間復雜度為O[DN2],對於較小的數據集,蠻力運算是比較高效的,但隨着N的增長,蠻力運算就變得不太合實際了,想象一下,對於一個千萬級別的數據集,使用蠻力運算意味着對每一個待分類的新樣本,你都需要進行數千萬次的平方和開根號,這實在是一件很愚蠢的事,於是便有了如下幾種快速方法;
KD樹(KD-tree)
KD樹是一種基於模型的算法,它並沒有上來就對測試樣本分類,而是基於訓練集先建立模型,這個模型就稱為KD樹,通過建立起的模型對測試集進行預測。KD樹指的是具有K個特征維度的樹,與KNN的參數k不是一個概念,這里我們以大小寫來區分;
KD樹算法有如下幾個步驟:
1.建立KD樹
KD樹構造樹采用的是從樣本集中m個樣本的n個維度的特征中,分別計算這n個特征各自的方差,用其中方差最大的第k維特征nk來作為根結點,接着針對這個特征,我們選擇特征nk的中位數nkm對應的樣本點作為划分點,即對所有在nk這個特征上取值小於nkm的樣本,將其划入左子樹,對於在nk上大於等於nkm的樣本,將其划入右子樹,接着,對於左子樹和右子樹,我們采用類似的方法計算方差——挑選最大方差對應的特征——根據該特征的中位數建立左右子樹,重復這個過程,以遞歸的方式生成我們需要的KD樹,更嚴謹的流程圖如下:
下面以一個非常簡單的例子來更形象的展現這個過程:
我們構造數據集{(1,3),(2.5,4),(2,3.4),(4,5),(6.3,4),(7,7)}
Step1:分別計算x與y的方差,var(x)=5.86,var(y)=2.08,因此我們選擇x作為KD樹的根結點,此時x的中位數為3.25,構造左子樹與右子樹,將{(1,3),(2.5,4),(2,3.4)}划入左子樹,{(4,5),(6.3,4),(7,7)}划入右子樹,此時的划分情況如下圖:
Step2:接着針對左子樹中的{(1,3),(2.5,4),(2,3.4)},計算出var(x)=0.5833333,var(y)=0.2533333,因此選擇x作為划分特征,此時中位數為2,划分出左-左子樹{(1,3)},左-右子樹{(2.5,4),(2,3.4)};右子樹中的{(4,5),(6.3,4),(7,7)},計算出var(x)=2.463333,var(y)=2.333333,所以選取x作為划分特征,中位數為6.3,划分出右-左子樹{(4,5)},右-右子樹{(6.3,4),(7,7)},這一輪得到如下划分:
Step3:接下來對樣本數未達到1的左-右子樹{(2.5,4),(2,3.4)},計算得var(x)=0.125,var(y)=0.18,因此這里選擇y進行划分,中位數為3.7,這個路徑下所有樣本划分完成;對同樣樣本數未到達1的右-右子樹{(6.3,4),(7,7)},var(x)=0.245,var(y)=4.5,選擇y進行划分,中位數為5.5,這一輪,也是最終輪得到如下划分:
2.KD樹搜索最近鄰
在KD樹建立完成之后,我們可以通過它來為測試集中的樣本點進行分類,對於任意一個測試樣本點,首先我們在KD樹中找到該樣本點歸入的范圍空間,接着以該樣本點為圓心,以該樣本點與該范圍空間中的單個實例點的距離為半徑,獲得一個超球體,最近鄰的點必然屬於該超球體,接着沿着KD樹向上返回葉子節點的父節點,檢查該父節點下另一半子樹對應的范圍空間是否與前面的超球體相交,如果相交,在該半邊子樹下尋找是否有更近的最近鄰點,若有,更新最近鄰點,若無,繼續沿着KD樹向上到達父節點的父節點的另一半子樹,繼續搜索有無更近鄰,這個過程一直向上回溯到根結點時,算法結束,當前保存的最近鄰點即為最終的最近鄰。
通過KD樹的划分建模,在對新樣本進行分類時,可以極大程度減少冗余的最近鄰搜索過程,因為很多樣本點所在的矩形范圍空間與超球體不相交,即不需要計算距離,這大大減少了計算時間,下面還以前面舉例中創建的KD樹為例,對新樣本點(3.4,4.2)進行分類:
Step1:首先我們找到(3.4,4.2)應該歸入(4,5)所在的超矩形體內,它與(4,5)的歐氏距離為1,以(3.4,4.2)為圓心,1為半徑作圓,得到如下圖:
可以看出,該圓與平面x=3.25存在重疊的部分,且在該圓與其他范圍空間相交部分存在着距離新樣本點更近的實例點(2.5,4),這時將新樣本點的最近鄰更新為實例點(2.5,4),再作圓,如下圖:
此時該圓雖然與其他矩形范圍空間仍然存在着相交部分,但這些部分中已不再存在比實例點(2.5,4)更靠近新樣本點的實例點,因此得到最近鄰點;
3.基於KD樹進行預測
通過上面描述的KD樹建模——KD樹搜索的過程,我們就可以利用數量合理的訓練實例點來訓練最佳的KD樹,接着對新樣本進行預測,在設定的近鄰數k下,一,通過KD樹完成第一輪的搜索,找到最近的近鄰點;二、利用同樣的步驟,在將已搜索到的近鄰點從KD樹中移除的條件下,用遞歸的方式對余下的k-1個待尋近鄰點進行迭代搜索;三,在所需k個最近鄰點都尋得的基礎上,利用最大占比原則完成類別的預測。
球樹法(ball tree)
KD樹法雖然快捷高效,但在遇到維度過高的數據或分布不均勻的數據集時效率也不太理想,譬如,以我們上面使用過的例子:
在這一輪中,圖中X距離左邊上部矩形內的實例點已經非常之近,但因為它也與左邊下部矩形空間有些許相交部分,因此仍然需要重復對左邊下部區域內的點計算其與樣本點的距離,這在維度較高時,就成了災難,會出現數量非常龐大的冗余的范圍空間需要計算,這是由於KD樹中以平行於坐標軸的多條線段划分訓練集,形成矩形結構導致的,因為他們都有突出去的棱角,容易與圓相交;
為了優化這一情況,球樹誕生了,這種結構可以大幅度優化上面所說的問題;
球樹的算法有如下幾個步驟:
1.建立球樹
球樹得名於它利用超球體而不是超立方體來分割空間:
Step1:先構建一個超球體,這個超球體是能夠包含所有樣本點的最小超球體;
Step2:根據確定的超球體球心,先選擇超球體中距離球心最遠的那個點,再選擇超球體中距離球心次遠的那個點,用類似K-means聚類的思想,將剩余的點歸類到這兩個點中最近的那個點的聚類群中,接着計算這兩個聚類群的聚類中心(重心),以及聚類群能夠包含所有群內樣本點的最小半徑,再分別構造兩個超球體(類似KD樹中的左右子樹);
Step3:重復上面的步驟,對子超球體進一步細分,最終得到分割出每一個訓練樣本的超球體的集合;
KD樹和球樹思想類似,區別在於球樹的划分空間為超球體,KD樹得到的是超立方體,因為在半徑等於邊長的情況下,得到的球體體積必然比立方體體積要小很多,這樣在計算高維和巨量數據時就可以避免很多多余的搜索。
2.球樹搜索最近鄰
因為球樹與KD樹的划分空間形狀特點不同,它會有很多空余出來的空間(譬如一個超球體內部除去其兩個子超球體外的其他空間),這使得其無法像KD樹那樣依據范圍空間在一開始就對新樣本點進行初始定位(因為新樣本點可能會落入最底層超球體之間空余的空間內),因此球樹找出給定目標點的最近鄰方法是自上而下從根結點出發,向下逐層為新樣本點定位,並在最終確定的葉子中找到與其最為接近的點,並確定一個最近鄰距離的上限值(類似線性規划中割平面法定上限的過程),接着類似KD樹,建立起以新樣本點為球心,上限值為半徑的超球體,檢查該超球體是否與其他球樹中的超球體有相交的部分,若有,則計算所有相交超球體內部點與新樣本點的距離,若上限值得到更新,則繼續這個過程直到上限值不再收斂;否則直接將上限值對應的點標記為這一輪的最近鄰點,利用球樹預測時也是類似KD樹預測的步驟,遞歸搜索,直到找到所需的k個結點為止;
三、評價
作為一種簡單又高效的機器學習算法,其主要優缺點如下:
優點:
1、原理簡單
2、訓練過程的時間復雜度較低
3、無需對數據分布作出假設,准確度高,且魯棒性強,對異常值不敏感
4、對分類任務中類與類之間重疊區域較多的情況,KNN較為合適
5、適合各個類訓練樣本數量較多的情況
缺點:
1、對樣本嚴重不平衡的情況效果較差,即對比例處於劣勢的類別預測精度低下
2、KD樹、球樹的建模過程往往會消耗大量內存,尤其在訓練樣本集較大時
3、屬於lazy learning,導致預測時速度比不上邏輯回歸等可用表達式計算的分類器
4、可解釋性較差,所以經常用於難以解釋內部關系時的分類任務
5、計算量大,容易陷於高維災難
下面分別在Python和R中實現KNN算法;
四、Python
在Python中,我們使用sklearn.neighbors中的KNeighborsClassifier()來進行常規的KNN分類,其主要參數如下:
n_neighbors:int型,控制近鄰數k,默認是5
weights:控制KNN算法中對不同數據分布情況的不同策略,'uniform'代表所有數據都是均勻分布在樣本空間中的,這時所有近鄰權重相等;'distance'表示近鄰的權重與距離成反比,即距離越大權重越小,越近的近鄰貢獻越大。這個權重被應用於最終的近鄰代表投票的過程中,默認是'uniform'
algorithm:字符型,控制KNN具體使用的算法,'ball_tree'代表球樹法,'kd_tree'表示KD樹法,'brute'表示蠻力運算法,'auto'表示算法自動去決定使用哪一種方法最好
leaf_size:int型,默認為30,控制球樹或KD樹中葉子中的最小樣本個數,越小意味着樹的構建越精細,也意味着越費內存
p:int型,默認值為2,這個參數對應Minkowski距離中的不同情況,p取1時為絕對值距離,p取2時為歐氏距離
metric:字符型,控制構造樹時距離的類型,默認是Minkowski距離,配合p=2,即為標准的歐氏距離
n_jobs:int型,控制並行運算使用到的CPU核心數,默認是1,即單核,-1時為開啟所有核心
下面以我們喜聞樂見的鳶尾花數據為例進行演示:
from sklearn.neighbors import KNeighborsClassifier from sklearn import datasets from sklearn.metrics import confusion_matrix from sklearn.model_selection import train_test_split '''加載鳶尾花數據''' X,y = datasets.load_iris(return_X_y=True) '''留出法分割訓練集與驗證集''' X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.3) '''搭建KNN分類器''' clf = KNeighborsClassifier(algorithm='brute', leaf_size=30, metric='minkowski', metric_params=None, n_jobs=1, n_neighbors=5, p=2, weights='uniform') '''利用訓練數據對KNN進行訓練''' clf = clf.fit(X_train,y_train) '''利用訓練完成的KNN分類器對驗證集上的樣本進行分類''' pre = clf.predict(X_test) '''打印混淆矩陣''' print(confusion_matrix(y_test,pre))
運行結果:
我們將近鄰權重參數weights調為'distance':
'''搭建KNN分類器''' clf = KNeighborsClassifier(algorithm='brute', leaf_size=30, metric='minkowski', metric_params=None, n_jobs=1, n_neighbors=5, p=2, weights='distance') '''利用訓練數據對KNN進行訓練''' clf = clf.fit(X_train,y_train) '''利用訓練完成的KNN分類器對驗證集上的樣本進行分類''' pre = clf.predict(X_test) '''打印混淆矩陣''' print(confusion_matrix(y_test,pre))
運行結果:
五、R
在R中有多個包可以實現KNN算法,我們這里簡單介紹class包中的knn(),其主要參數如下:
train:訓練集的自變量部分,數據框或矩陣形式
test:待預測的新樣本,數據框或矩陣形式
cl:訓練集的特征對應的真實類別
k:整數型,控制KNN的近鄰數
prob:邏輯型參數,默認為F,設置為T時,輸出的結果里還會包含每個樣本點被歸類的概率大小
下面依然以鳶尾花數據進行演示:
> library(class) > > #載入鳶尾花數據 > data(iris) > > #留出法分割訓練集與驗證集 > sam <- sample(1:dim(iris)[1],dim(iris)[1]*0.7) > train <- iris[sam,] > test <- iris[-sam,] > > #訓練KNN分類器並輸出test的預測結果 > Kclf <- knn(train=train[,-5],test=test[,-5],cl=train[,5],k=5,prob=T) > > #打印混淆矩陣 > table(test[,5],Kclf) Kclf setosa versicolor virginica setosa 18 0 0 versicolor 0 11 3 virginica 0 0 13 > > #打印正確率 > sum(diag(prop.table(table(test[,5],Kclf)))) [1] 0.9333333
以上就是關於KNN算法的基本內容,如有筆誤,望指出。