K-近鄰算法介紹與代碼實現

聲明:如需轉載請先聯繫我。python

最近學習了k近鄰算法,在這裏進行了總結。git

KNN介紹

k近鄰法(k-nearest neighbors)是由Cover和Hart於1968年提出的,它是懶惰學習(lazy learning)的著名錶明。
它的工做機制比較簡單:github

  • 給定一個測試樣本
  • 計算它到訓練樣本的距離
  • 取離測試樣本最近的k個訓練樣本
  • 「投票法」選出在這k個樣本中出現最多的類別,就是預測的結果

距離衡量的標準有不少,常見的有:$L_p$距離、切比雪夫距離、馬氏距離、巴氏距離、餘弦值等。算法

什麼意思呢?先來看這張圖

2個類機器學習

咱們對應上面的流程來講ide

  • 1.給定了紅色和藍色的訓練樣本,綠色爲測試樣本
  • 2.計算綠色點到其餘點的距離
  • 3.選取離綠點最近的k個點
  • 4.選取k個點中,同種顏色最多的類。例如:k=1時,k個點全是藍色,那預測結果就是Class 1;k=3時,k個點中兩個紅色一個藍色,那預測結果就是Class 2

舉例

這裏用歐氏距離做爲距離的衡量標準,用鳶尾花數據集舉例說明。
鳶尾花數據集有三個類別,每一個類有150個樣本,每一個樣本有4個特徵。
先來回顧一下歐氏距離的定義(摘自維基百科):
函數

在歐幾里得空間中,點 x = (x1,...,xn) 和 y = (y1,...,yn) 之間的歐氏距離爲

$d(x,y):={\sqrt {(x_{1}-y_{1})^{2}+(x_{2}-y_{2})^{2}+\cdots +(x_{n}-y_{n})^{2}}}={\sqrt {\sum {{i=1}}^{n}(x{i}-y_{i})^{2}}}$
oop

向量 ${\displaystyle {\vec {x}}}$的天然長度,即該點到原點的距離爲

$|{\vec {x}}|{2}={\sqrt {|x{1}|^{2}+\cdots +|x_{n}|^{2}}}$
學習

它是一個純數值。在歐幾里得度量下,兩點之間線段最短。
如今給出六個訓練樣本,分爲三類,每一個樣本有4個特徵,編號爲7的名稱是咱們要預測的。
|編號|花萼長度(cm)|花萼寬度(cm)|花瓣長度(cm)|花瓣寬度(cm)|名稱|
|----|----------|-----------|-----------|----------|----|
|1|4.9|3.1|1.5|0.1|Iris setosa|
|2|5.4|3.7|1.5|0.2|Iris setosa|
|3|5.2|2.7|3.9|1.4|Iris versicolor|
|4|5.0|2.0|3.5|1.0|Iris versicolor|
|5|6.3|2.7|4.9|1.8|Iris virginica|
|6|6.7|3.3|5.7|2.1|Iris virginica|
|7|5.5|2.5|4.0|1.3| ? |測試

按照以前說的步驟,咱們來計算測試樣本到各個訓練樣本的距離。例如到第一個樣本的距離:
$d_{1}=\sqrt{(4.9 - 5.5)^2 + (3.1 - 2.5)^2 + (1.5 - 4.0)^2 + (0.1 - 1.3)^2} = 2.9$

寫一個函數來執行這個操做吧

import numpy as np
def calc_distance(iA,iB):
   temp = np.subtract(iA, iB)      # 對應元素相減
   temp = np.power(temp, 2)        # 元素分別平方
   distance = np.sqrt(temp.sum())  # 先求和再開方
   return distance

testSample = np.array([5.5, 2.5, 4.0, 1.3])
print("Distance to 1:", calc_distance(np.array([4.9, 3.1, 1.5, 0.1]), testSample))
print("Distance to 2:", calc_distance(np.array([5.4, 3.7, 1.5, 0.2]), testSample))
print("Distance to 3:", calc_distance(np.array([5.2, 2.7, 3.9, 1.4]), testSample))
print("Distance to 4:", calc_distance(np.array([5.0, 2.0, 3.5, 1.0]), testSample))
print("Distance to 5:", calc_distance(np.array([6.3, 2.7, 4.9, 1.8]), testSample))
print("Distance to 6:", calc_distance(np.array([6.7, 3.3, 5.7, 2.1]), testSample))

Distance to 1: 2.9

Distance to 2: 2.98496231132

Distance to 3: 0.387298334621

Distance to 4: 0.916515138991

Distance to 5: 1.31909059583

Distance to 6: 2.36854385647

若是咱們把k定爲3,那麼離測試樣本最近3個依次是:

編號 名稱
3 Iris versicolor
4 Iris versicolor
5 Iris virginica

顯然測試樣本屬於Iris versicolor類的「票數」多一點,事實上它的確屬於這個類。

優/缺點

這裏參考了CSDN蘆金宇博客上的總結

優勢

  • 簡單好用,容易理解,精度高,理論成熟,既能夠用來作分類也能夠用來作迴歸;
  • 可用於數值型數據和離散型數據;
  • 訓練時間複雜度爲O(n);無數據輸入假定;
  • 對異常值不敏感。

缺點

  • 計算複雜性高;空間複雜性高;
  • 樣本不平衡問題(即有些類別的樣本數量不少,而其它樣本的數量不多);
  • 通常數值很大的時候不用這個,計算量太大。可是單個樣本又不能太少,不然容易發生誤分。
  • 最大的缺點是沒法給出數據的內在含義。

補充一點:因爲它屬於懶惰學習,所以須要大量的空間來存儲訓練實例,在預測時它還須要與已知全部實例進行比較,增大了計算量。

這裏介紹一下,當樣本不平衡時的影響。

不平衡

從直觀上能夠看出X應該屬於$\omega_{1}$,這是理所應當的。對於Y看起來應該屬於$\omega_{1}$,但事實上在k範圍內,更多的點屬於$\omega_{2}$,這就形成了錯誤分類。

一個結論

在周志華編著的《機器學習》中證實了最近鄰學習器的泛化錯誤率不超過貝葉斯最優分類器的錯誤率的兩倍,在原書的226頁,這裏就不摘抄了。

代碼實現

知道KNN的原理後,應該能夠很輕易的寫出代碼了,這裏介紹一下在距離計算上的優化,在結尾給上完整代碼(代碼比較亂,知道思想就好)。

函數的輸入:train_Xtest_X是numpy array,假設它們的shape分別爲(n, 4)、(m, 4);要求輸出的是它們兩點間的距離矩陣,shape爲(n, m)。

不就是計算兩點之間的距離,再存起來嗎,直接暴力上啊︿( ̄︶ ̄)︿,因而就有了下面的

雙循環暴力實現

def euclideanDistance_two_loops(train_X, test_X):
   num_test = test_X.shape[0]
   num_train = train_X.shape[0]
   dists = np.zeros((num_test, num_train))
   for i in range(num_test):
       for j in range(num_train):
           test_line = test_X[i]
           train_line = train_X[j]
           temp = np.subtract(test_line, train_line)
           temp = np.power(temp, 2)
           dists[i][j] = np.sqrt(temp.sum())
   return dists

不知道你有沒有想過,這裏的樣本數只有100多個,因此時間上感受沒有等過久,可是當樣本量很是大的時候呢,雙循環計算的弊端就顯露出來了。解決方案是把它轉換爲兩個矩陣之間的運算,這樣就能避免使用循環了。

轉化爲矩陣運算實現

L2 Distance

此處參考CSDNfrankzd的博客

記測試集矩陣P的大小爲$MD$,訓練集矩陣C的大小爲$ND$(測試集中共有M個點,每一個點爲D維特徵向量。訓練集中共有N個點,每一個點爲D維特徵向量)


記$P_{i}$是P的第i行$P_i = [ P_{i1}\quad P_{i2} \cdots P_{iD}]$,記$C_{j}$是C的第j行$C_j = [ C_{j1} C_{j2} \cdots \quad C_{jD}]$

  • 首先計算Pi和Cj之間的距離dist(i,j)


    image


  • 咱們能夠推廣到距離矩陣的第i行的計算公式


    image


  • 繼續將公式推廣爲整個距離矩陣(也就是徹底平方公式)


    image

知道距離矩陣怎麼算出來的以後,在代碼上只須要套公式帶入就能實現了。

def euclideanDistance_no_loops(train_X, test_X):
   num_test = test_X.shape[0]
   num_train = train_X.shape[0]
   sum_train = np.power(train_X, 2)
   sum_train = sum_train.sum(axis=1)
   sum_train = sum_train * np.ones((num_test, num_train))
   sum_test = np.power(test_X, 2)
   sum_test = sum_test.sum(axis=1)
   sum_test = sum_test * np.ones((1, sum_train.shape[0]))
   sum_test = sum_test.T
   sum = sum_train + sum_test - 2 * np.dot(test_X, train_X.T)
   dists = np.sqrt(sum)
   return dists

是否是很簡單,這裏兩種方法咱們衡量兩點間距離的標準是歐氏距離。若是想用其餘的標準呢,好比$L_{1}$距離該怎麼實現呢,這裏我參照上面推導公式的思想得出了計算$L_{1}$距離的矩陣運算。

L1 Distance

記測試集矩陣P的大小爲$MD$,訓練集矩陣C的大小爲$ND$(測試集中共有M個點,每一個點爲D維特徵向量。訓練集中共有N個點,每一個點爲D維特徵向量)


記$P_{i}$是P的第i行$P_i = [ P_{i1}\quad P_{i2} \cdots P_{iD}]$,記$C_{j}$是C的第j行$C_j = [ C_{j1} C_{j2} \cdots \quad C_{jD}]$

  • 首先計算Pi和Cj之間的距離dist(i,j)


    $d(P_{i}, C_{j}) = |P_{i1} - C_{j1}| + |P_{i2} - C_{j2}| + \cdots + |P_{iD} - C_{jD}|$

  • 咱們能夠推廣到距離矩陣的第i行的計算公式


    $dist[i] = \left | [P_{i}\quad P_{i} \cdots P_{i}] - [C_{1}\quad C_{2}\cdots C_{N}] \right|=[|P_{i} - C_{1}|\quad |P_{i} - C_{2}| \cdots |P_{i} - C_{N}|]$

  • 繼續將公式推廣爲整個距離矩陣


    image

    其中$P_i = [ P_{i1}\quad P_{i2} \cdots P_{iD}]$、$C_j = [ C_{j1} C_{j2} \cdots \quad C_{jD}]$

def l1_distance_no_loops(train_X, test_X):
   num_test = test_X.shape[0]
   num_train = train_X.shape[0]
   test = np.tile(test_X, (num_train, 1, 1))
   train = np.tile(train_X, (num_test, 1, 1))
   train = np.transpose(train, axes=(1, 0, 2))
   sum = np.subtract(test, train)
   sum = np.abs(sum)
   sum = np.sum(sum, axis=2)
   dists = sum.T
   return dists

因爲測試集樣本數量有限,兩種距離衡量標準下的準確率分別是0.94和0.98。

完整代碼

近期文章

wechat.jpg

相關文章
相關標籤/搜索