昨天經過一個酒吧猜紅酒的故事,介紹了機器學習中最簡單的一個算法:kNN (K 近鄰算法),並用 Python 一步步實現這個算法。同時爲了對比,調用了 Sklearn 中的 kNN 算法包,僅用了 5 行代碼。兩種方法異曲同工,都正確解決了二分類問題,即新倒的紅酒屬於赤霞珠。python
文章傳送門:算法
Python 手寫機器學習最簡單的 kNN 算法 (可點擊)數組
雖然調用 Sklearn 庫算法,簡單的幾行代碼就能解決問題,感受很爽,但其實咱們時處於黑箱中的,Sklearn 背後幹了些什麼咱們其實不明白。做爲初學者,若是不搞清楚算法原理就直接調包,學的也只是表面功夫,沒什麼卵用。markdown
因此今天來咱們瞭解一下 Sklearn 是如何封裝 kNN 算法的並本身 Python 實現一下。這樣,之後咱們再調用 Sklearn 算法包時,會有更清晰的認識。機器學習
先來回顧昨天 Sklearn 中 kNN 算法的 5 行代碼:函數
from sklearn.neighbors import KNeighborsClassifier kNN_classifier = KNeighborsClassifier(n_neighbors=3) kNN_classifier.fit(X_train,y_train ) x_test = x_test.reshape(1,-1) kNN_classifier.predict(x_test)[0] 複製代碼
代碼已解釋過,今天用一張圖繼續加深理解:oop
能夠說,Sklearn 調用全部的機器學習算法幾乎都是按照這樣的套路:把訓練數據餵給選擇的算法進行 fit 擬合,能計算出一個模型,模型有了就把要預測的數據餵給模型,進行預測 predict,最後輸出結果,分類和迴歸算法都是如此。學習
值得注意的一點是,**kNN 是一個特殊算法,它不須要訓練(fit)創建模型,直接拿測試數據在訓練集上就能夠預測出結果。**這也是爲何說 kNN 算法是最簡單的機器學習算法緣由之一。測試
但在上面的 Sklearn 中爲何這裏還 fit 擬合這一步操做呢,其實是能夠不用的,不過 Sklearn 的接口很整齊統一,因此爲了跟多數算法保持一致把訓練集當成模型。spa
隨着以後咱們學習更多的算法,會發現每一個算法都有一些特色,能夠總結對比一下。
把昨天的手寫代碼整理成一個函數就能夠看到沒有訓練過程:
import numpy as np from math import sqrt from collections import Counter def kNNClassify(K, X_train, y_train, X_predict): distances = [sqrt(np.sum((x - X_predict)**2)) for x in X_train] sort = np.argsort(distances) topK = [y_train[i] for i in sort[:K]] votes = Counter(topK) y_predict = votes.most_common(1)[0][0] return y_predict 複製代碼
接下來咱們按照上圖的思路,把 Sklearn 中封裝的 kNN 算法,從底層一步步寫出那 5 行代碼是如何運行的:
import numpy as np from math import sqrt from collections import Counter class kNNClassifier: def __init__(self,k): self.k =k self._X_train = None self._y_train = None def fit(self,X_train,y_train): self._X_train = X_train self._y_train = y_train return self 複製代碼
首先,咱們須要把以前的函數改寫一個名爲 kNNClassifier 的 Class 類,由於 Sklearn 中的算法都是面向對象的,使用類更方便。
若是你對類還不熟悉能夠參考我之前的一篇文章:
Python 類 Class 的理解(可點擊)
在__init__
函數中定義三個初始變量,k 表示咱們要選擇傳進了的 k 個近鄰點。
self._X_train
和 self._y_train
前面有個 下劃線_ ,意思是把它們當成內部私有變量,只在內部運算,外部不能改動。
接着定義一個 fit 函數,這個函數就是用來擬合 kNN 模型,但 kNN 模型並不須要擬合,因此咱們就原封不動地把數據集複製一遍,最後返回兩個數據集自身。
這裏要對輸入的變量作一下約束,一個是 X_train 和 y_train 的行數要同樣,一個是咱們選的 k 近鄰點不能是非法數,好比負數或者多於樣本點的數, 否則後續計算會出錯。用什麼作約束呢,可使用 assert 斷言語句:
def fit(self,X_train,y_train): assert X_train.shape[0] == y_train.shape[0],"添加 assert 斷言是爲了確保輸入正常的數據集和k值,若是不添加一旦輸入不正常的值,難找到出錯緣由" assert self.k <= X_train.shape[0] self._X_train = X_train self._y_train = y_train return self 複製代碼
接下來咱們就要傳進待預測的樣本點,計算它跟每一個樣本點之間的距離,對應 Sklearn 中的 predict ,這是算法的核心部分。而這一步代碼就是咱們以前寫的函數,能夠直接拿過來用,加幾行斷言保證輸入的變量是合理的。
def predict(self,X_predict): assert self._X_train is not None,"要求predict 以前要先運行 fit 這樣self._X_train 就不會爲空" assert self._y_train is not None assert X_predict.shape[1] == self._X_train.shape[1],"要求測試集和預測集的特徵數量一致" distances = [sqrt(np.sum((x_train - X_predict)**2)) for x_train in self._X_train] sort = np.argsort(distances) topK = [self._y_train[i] for i in sort[:self.k]] votes = Counter(topK) y_predict = votes.most_common(1)[0][0] return y_predict 複製代碼
到這兒咱們就完成了一個簡易的 Sklearn kNN 封裝算法,保存爲kNN_sklearn.py
文件,而後在 jupyter notebook 運行測試一下:
先得到基礎數據:
# 樣本集 X_raw = [[13.23, 5.64], [13.2 , 4.38], [13.16, 4.68], [13.37, 4.8 ], [13.24, 4.32], [12.07, 2.76], [12.43, 3.94], [11.79, 3. ], [12.37, 2.12], [12.04, 2.6 ]] X_train = np.array(X_raw) # 特徵值 y_raw = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1] y_train = np.array(y_raw) # 待預測值 x_test= np.array([12.08, 3.3]) X_predict = x_test.reshape(1,-1) 複製代碼
注意:當預測變量只有一個時,必定要 reshape(1,-1) 成二維數組否則會報錯。
在 jupyter notebook 中運行程序可使用一個魔法命令 %run:
%run kNN_Euler.py
複製代碼
這樣就直接運行好了 kNN_Euler.py 程序,而後就能夠調用程序中的 kNNClassifier 類,賦予 k 參數爲 3,命名爲一個實例 kNN_classify 。
kNN_classify = kNNClassifier(3) 複製代碼
接着把樣本集 X_train,y_train 傳給實例 fit :
kNN_classify.fit(X_train,y_train)
複製代碼
fit 好後再傳入待預測樣本 X_predict 進行預測就能夠獲得分類結果了:
y_predict = kNN_classify.predict(X_predict) y_predict [out]:1 複製代碼
答案是 1 和昨天兩種方法的結果是同樣的。
是否是不難?
再進一步,若是咱們一次預測不僅一個點,而是多個點,好比要預測下面這兩個點屬於哪一類:
那能不能同時給出預測的分類結果呢?答案固然是能夠的,咱們只須要稍微修改如下上面的封裝算法就能夠了,把 predict 函數做以下修改:
def predict(self,X_predict): y_predict = [self._predict(x) for x in X_predict] # 列表生成是把分類結果都存儲到list 中而後返回 return np.array(y_predict) def _predict(self,x): # _predict私有函數 assert self._X_train is not None assert self._y_train is not None distances = [sqrt(np.sum((x_train - x)**2)) for x_train in self._X_train] sort = np.argsort(distances) topK = [self._y_train[i] for i in sort[:self.k]] votes = Counter(topK) y_predict = votes.most_common(1)[0][0] return y_predict 複製代碼
這裏定義了兩個函數,predict 用列表生成式來存儲多個預測分類值,預測值從哪裏來呢,就是利用 _predict
函數計算,_predict
前面的下劃線一樣代表它是封裝的私有函數,只在內部使用,外界不能調用,由於不須要。
算法寫好,只須要傳入多個預測樣本就能夠了,這裏咱們傳遞兩個:
X_predict = np.array([[12.08, 3.3 ], [12.8,4.1]]) 複製代碼
輸出預測結果:
y_predict = kNN_classify.predict(X_predict) y_predict [out]:array([1, 0]) 複製代碼
看,返回了兩個值,第一個樣本的分類結果是 1 即赤霞珠,第二個樣本結果是 0 即黑皮諾。和實際結果一致,很完美。
到這裏,咱們就按照 Sklearn 算法封裝方式寫出了 kNN 算法,不過 Sklearn 中的 kNN 算法要比這複雜地多,由於 kNN 算法還有不少要考慮的,好比處理 **kNN 算法的一個缺點:計算耗時。**簡單說就是 kNN 算法運行時間高度依賴樣本集有和特徵值數量的維度,當維度很高時算法運行時間就極速增長,具體緣由和改善方法咱們後續再說。
如今還有一個重要的問題,咱們在所有訓練集上實現了 kNN 算法,但它預測的效果和準確率怎麼樣,咱們並不清楚,下一篇文章,來講說怎麼衡量 kNN 算法效果的好壞。
本文的 jupyter notebook 代碼,能夠在我公衆號:「高級農民工」後臺回覆「kNN2」獲得,加油!