1. 程式人生 > 其它 >K-近鄰演算法的Sklearn完整復現

K-近鄰演算法的Sklearn完整復現

1. 演算法原理

  • 核心思想:未標記樣本的類別,由距離其最近的k個鄰居投票來決定。
  • 已知條件:具有已標記資料集且知道資料集中每個樣本所屬類別。一個未標記資料樣本。
  • 目的:預測未標記資料樣本所屬類別。
  • 演算法原理虛擬碼:
    • 假設 X_test 為待標記的資料樣本,X_train為已標記的資料集
    • 遍歷 X_train 中所有樣本,計算每個樣本與 X_test 的距離,並把距離儲存在 Distance 陣列中
    • 對 Distance 陣列進行排序,取距離最近的 k 個點,記為 X_knn
    • 在 X_knn 中統計每個類別的個數
    • 待標記樣本的類別就是在 X_knn 中樣本個數最多的那個類別
  • 優缺點:
    • 優點:準確性高,對異常值和噪聲有較高容忍度。
    • 缺點:計算量較大,對記憶體需求大。(每次對未標記樣本分類都需要重新計算距離)
  • 演算法引數:演算法引數為 k ,引數選擇根據資料來確定,k 值越大,模型偏差越大,對噪聲資料越不敏感。(可能出現欠擬合)k值越小,模型方差越大。(可能出現過擬合)

2. 使用k-近鄰演算法進行分類

sklearn中使用k-近鄰進行分類處理的是sklearn.neighbors.KNeighborsClassifier

#生成已標記資料
from sklearn.datasets import make_blobs
#生成資料
centers = [[-2,2],[2,2],[0,4]]
X,y = make_blobs(n_samples = 60,centers = centers,
                 random_state = 0,cluster_std = 0.6)

說明:n_sample為訓練樣本的個數,centers指定中心點位置,cluster_std指明生成點分佈的鬆散程度(標準差)。訓練資料集放在X中,資料集類別標記放在y中。

import matplotlib.pyplot as plt
import numpy as np
#繪製資料
plt.figure(figsize = (16,10),dpi = 144)
c = np.array(centers)
plt.scatter(X[:,0],X[:,1],c = y,s = 100,cmap = 'cool') #畫樣本
plt.scatter(c[:,0],c[:,1],s = 100,marker = '^',c = 'orange') #畫中心點
<matplotlib.collections.PathCollection at 0x1f28e49d220>


使用KNeighborsClassifier來對演算法進行訓練,選擇的引數是k=5

from sklearn.neighbors import KNeighborsClassifier
#模型訓練
k = 5
clf = KNeighborsClassifier(n_neighbors = k)
clf.fit(X,y)
KNeighborsClassifier()

對一個新的樣本進行預測,需要進行預測的樣本為[0,2],使用kneighbors()方法把樣本週圍距離最近的5個點取出來,取出來的點是訓練樣本X中的索引。

#進行預測
X_sample = np.array([0,2]).reshape(1,-1)
y_sample = clf.predict(X_sample)
neighbors = clf.kneighbors(X_sample,return_distance = False)
X_sample
array([[0, 2]])
y_sample
array([0])
neighbors
array([[16, 20, 48,  6, 23]], dtype=int64)

標記最近的5個點和待預測樣本

#畫示意圖
plt.figure(figsize = (16,10),dpi = 144)
plt.scatter(X[:,0],X[:,1],c=y,s = 100,cmap = 'cool') #樣本
plt.scatter(c[:,0],c[:,1],s=100,marker = '^',c = 'k') #中心點
plt.scatter(X_sample[0][0],X_sample[0][1],marker='x',
           c = y_sample,s = 100,cmap = 'cool') #待預測點
for i in neighbors[0]:
    plt.plot([X[i][0],X_sample[0][0]],[X[i][1],X_sample[0][1]],'k--',linewidth = 0.6) #預測點與距離最近5點的連線


3. 使用k-近鄰演算法進行迴歸擬合

用k-近鄰演算法在連續區間內對數值進行預測,進行迴歸擬合。

scikit-learn中,使用k-近鄰演算法進行迴歸擬合的演算法是sklearn.neighbors.KNeighborsRegressor

#生成資料集,在餘弦曲線的基礎上加入了噪聲
import numpy as np
n_dots = 40
X = 5 * np.random.rand(n_dots,1)
y = np.cos(X).ravel()

#新增一些噪聲
y += 0.2 * np.random.rand(n_dots) - 0.1
#使用KNeighborsRegressor來訓練模型
from sklearn.neighbors import KNeighborsRegressor
k = 5
knn = KNeighborsRegressor(k)
knn.fit(X,y)
KNeighborsRegressor()

迴歸擬合的過程:在X軸上指定區間內生成足夠多的點,針對這些足夠密集的點,使用訓練出來的模型進行預測,把所有的預測點連線起來得到擬合曲線。

#生成足夠密集的點進行預測(np.newais用於插入新維度)
T = np.linspace(0,5,500)[:,np.newaxis]
y_pred = knn.predict(T)
knn.score(X,y)
0.9756908320045331
#繪製擬合曲線
plt.figure(figsize = (16,10),dpi = 144)
plt.scatter(X,y,c = 'g',label = 'data',s = 100)  #畫出訓練樣本
plt.plot(T,y_pred,c = 'k',label = 'prediction',lw = 4) #畫出擬合曲線
plt.axis('tight')
plt.title('KNeighborsRegressor (k = %i)' % k)
plt.show()


4. 糖尿病預測

使用 k-近鄰演算法及其變種,對 Pima 印第安人的糖尿病進行預測。

#載入資料
import pandas as pd

data = pd.read_csv('diabetes.csv')
print('dataset shape {}'.format(data.shape))
data.head()
dataset shape (768, 9)
Pregnancies Glucose BloodPressure SkinThickness Insulin BMI DiabetesPedigreeFunction Age Outcome
0 6 148 72 35 0 33.6 0.627 50 1
1 1 85 66 29 0 26.6 0.351 31 0
2 8 183 64 0 0 23.3 0.672 32 1
3 1 89 66 23 94 28.1 0.167 21 0
4 0 137 40 35 168 43.1 2.288 33 1
  • Outcome:0表示沒有糖尿病,1表示有糖尿病。

8個特徵分別如下:

  • Pregnancies:懷孕的次數。
  • Glucose:血漿葡萄糖濃度,採用2小時口服葡萄糖耐量試驗測得。
  • BloodPressure:舒張壓(毫米汞柱)
  • SkinThickness:肱三頭肌面板褶皺厚度(毫米)
  • Insulin:兩個小時血清胰島素(μU/毫升)
  • BMI:身體質量指數,體重除以身高的平方。
  • Diabetes Pedigree Function:糖尿病血統指數。糖尿病和家庭遺傳相關。
  • Age: 年齡
data.groupby("Outcome").size()
Outcome
0    500
1    268
dtype: int64
#將8個特徵值分離出來作為訓練資料集,把Outcome列分離出來作為目標值。
#然後把訓練集劃分為訓練資料集和測試資料集
X = data.iloc[:,0:8]
Y = data.iloc[:,8]
print('shape of X {}; shape of Y {}'.format(X.shape,Y.shape))
from sklearn.model_selection import train_test_split
X_train,X_test,Y_train,Y_test = train_test_split(X,Y,test_size = 0.2)
shape of X (768, 8); shape of Y (768,)

4.1 模型比較

使用普通的k-均值演算法、帶權重的k-均值演算法以及指定半徑的k-均值演算法分別對資料集進行擬合併計算評分:

from sklearn.neighbors import KNeighborsClassifier,RadiusNeighborsClassifier

#構造3個模型
models = []
models.append(("KNN",KNeighborsClassifier(n_neighbors = 2)))
models.append(("KNN with weights",KNeighborsClassifier(
    n_neighbors=2,weights = "distance")))
models.append(("Radius Neighbors",RadiusNeighborsClassifier(
    n_neighbors=2,radius = 500.0)))

print(models)
#分別訓練3個模型,並計算評分
results = []
for name,model in models:
    model.fit(X_train,Y_train)
    results.append((name,model.score(X_test,Y_test)))
for i in range(len(results)):
    print("name: {};score: {}".format(results[i][0],results[i][1]))
[('KNN', KNeighborsClassifier(n_neighbors=2)), ('KNN with weights', KNeighborsClassifier(n_neighbors=2, weights='distance')), ('Radius Neighbors', RadiusNeighborsClassifier(radius=500.0))]
name: KNN;score: 0.7792207792207793
name: KNN with weights;score: 0.7077922077922078
name: Radius Neighbors;score: 0.6883116883116883

說明:

  • 權重演算法,選擇了距離越近,權重越高。
  • RadiusNeighborsClassifier模型的半徑,選擇了500
  • 從輸出看出,普通的k-均值演算法效能還是最好。答案是不準確的,因為訓練樣本和測試樣本是隨機分配的,不同的訓練樣本和測試樣本組合可能導致計算出來的演算法準確性是有差異的。

如何更準確地對比演算法準確性?多次隨機分配訓練資料集和交叉驗證資料集,然後求模型準確性評分的平均值。scikit-learn提供了KFoldcross_val_score()函式來處理這種問題:

交叉驗證具體流程:https://blog.csdn.net/qq_36523839/article/details/80707678

from sklearn.model_selection import KFold
from sklearn.model_selection import cross_val_score

results = []
for name,model in models:
    kfold = KFold(n_splits = 10)
    cv_result = cross_val_score(model,X,Y,cv = kfold)
    results.append((name,cv_result))
for i in range(len(results)):
    print("name: {};cross val score: {}".format(
        results[i][0],results[i][1].mean()))
name: KNN;cross val score: 0.7147641831852358
name: KNN with weights;cross val score: 0.6770505809979495
name: Radius Neighbors;cross val score: 0.6497265892002735

上述程式碼通過KFold把資料集分成10份,其中1份會作為交叉驗證資料集來計算模型準確性,剩下9份作為訓練資料集。

cross_val_score()函式總共計算出10次不同訓練集和交叉驗證資料集組合得到的模型準確性評分。

4.2 模型訓練及分析

綜上所述,普通的k-均值演算法效能更優一些。接下來,我們就使用普通的k-均值演算法模型對資料集進行訓練,並檢視對訓練樣本的擬合情況以及對測試樣本的預測準確性情況。

knn = KNeighborsClassifier(n_neighbors = 2)
knn.fit(X_train,Y_train)
train_score = knn.score(X_train,Y_train)
test_score = knn.score(X_test,Y_test)
print("train score: {};test score: {}".format(train_score,test_score))
train score: 0.8159609120521173;test score: 0.7792207792207793

以上結果表明:

  • 演算法模型過於簡單,無法很好擬合樣本資料
  • 模型準確性欠佳,預測準確性較低,需要進一步畫學習曲線驗證。
from sklearn.model_selection import ShuffleSplit
from common.utils import plot_learning_curve

knn = KNeighborsClassifier(n_neighbors = 2)
cv = ShuffleSplit(n_splits = 10,test_size = 0.2,random_state = 0)
plt.figure(figsize = (10,6),dpi = 200)
plot_learning_curve(plt,knn,"Learn Curve for KNN Diabetes",
                X,Y,ylim = (0.0,1.01),cv = cv)
<module 'matplotlib.pyplot' from 'D:\\Anaconda3\\lib\\site-packages\\matplotlib\\pyplot.py'>


如上圖所示:訓練樣本評分較低,且測試樣本與訓練樣本距離較大,這是典型的欠擬合現象。

4.4 特徵選擇及資料視覺化

如果要用直觀方法來揭示為什麼k-均值演算法不是針對這一問題的好模型?

  • 將資料畫出來,可具有8個特徵,無法在這麼高的維度中畫出資料並直觀觀察
  • 可以採用特徵選擇,即只選擇2個與輸出值相關性最大的特徵,這樣就可以在二維平面上畫出輸入特徵值與輸出值的關係。

scikit-learnsklearn.feature_selection包中提供了豐富的特徵選擇方法,在此使用SelectKBest選擇相關性最大的兩個特徵:

from sklearn.feature_selection import SelectKBest

selector = SelectKBest(k = 2)
X_new = selector.fit_transform(X,Y)
X_new[0:5]
array([[148. ,  33.6],
       [ 85. ,  26.6],
       [183. ,  23.3],
       [ 89. ,  28.1],
       [137. ,  43.1]])

準確性效果

results = []
for name,model in models:
    kfold = KFold(n_splits = 10)
    cv_result = cross_val_score(model,X_new,Y,cv = kfold)
    results.append((name,cv_result))
for i in range(len(results)):
    print("name: {};cross val score: {}".format(
        results[i][0],results[i][1].mean()))
name: KNN;cross val score: 0.725205058099795
name: KNN with weights;cross val score: 0.6900375939849623
name: Radius Neighbors;cross val score: 0.6510252904989747

由此看出兩個特徵與所有特徵比較準確性差不多,側面體現了SelectKBest特徵選擇的準確性。

#兩個特徵畫出所有訓練樣本,觀察分佈情況
plt.figure(figsize = (10,6),dpi = 200)
plt.ylabel("BMI")
plt.xlabel("Glucose")
#畫出Y==0的陰性樣本
plt.scatter(X_new[Y==0][:,0],X_new[Y==0][:,1],c = 'r',s = 20,marker = 'o')
#畫出Y==1的陽性樣本
plt.scatter(X_new[Y==1][:,0],X_new[Y==1][:,1],c = 'g',s = 20,marker = '^')
<matplotlib.collections.PathCollection at 0x1f29282d520>


因為兩特徵對應的陰性和陽性樣本的分類並不明顯,所以很難預測糖尿病問題,無法達到很高的預測準確性。