Building & Improving a K-Nearest Neighbors Algorithm in Python
K-Nearest Neighbors算法,簡稱K-NN,是一種經(jīng)典的機(jī)器學(xué)習(xí)算法,但在深度學(xué)習(xí)大熱的現(xiàn)今經(jīng)常被忽略。在本教程中,我們將在Scikit-Learn中構(gòu)建K-NN算法并在MNIST數(shù)據(jù)集上運(yùn)行它。從那里開始,我們將構(gòu)建我們自己的K-NN算法,以期開發(fā)出比Scikit-Learn K-NN有更高準(zhǔn)確度和分類速度的分類器。
K-最近鄰分類模型

K-Nearest Neighbors算法是一種監(jiān)督機(jī)器學(xué)習(xí)算法,易于實(shí)現(xiàn),但能夠進(jìn)行強(qiáng)大的分類。K-NN最大的優(yōu)勢(shì)之一就是它是一個(gè)懶惰的學(xué)習(xí)者 。這意味著該模型不需要訓(xùn)練,并且可以正確分類數(shù)據(jù),這與其他ML兄弟,如SVM,回歸(regression)和多層感知(multi-layer perception)不同。
K-NN如何運(yùn)作
為了對(duì)某些給定數(shù)據(jù)點(diǎn)p進(jìn)行分類,K-NN模型將首先使用一些距離度量(distance metric)將p與其數(shù)據(jù)庫中可用的每個(gè)其他點(diǎn)進(jìn)行比較。距離度量是諸如歐幾里德距離之類的東西,一個(gè)取兩個(gè)點(diǎn)作為參數(shù)的簡單函數(shù),并返回這兩個(gè)點(diǎn)之間的距離。因此,可以假設(shè)它們之間具有較小距離的兩個(gè)點(diǎn)比它們之間具有較大距離的兩個(gè)點(diǎn)更相似 。這是K-NN背后的核心理念。
此過程將返回?zé)o序數(shù)組,其中數(shù)組中的每個(gè)條目保持p與模型數(shù)據(jù)庫中n個(gè)數(shù)據(jù)點(diǎn)之一之間的距離。因此返回的數(shù)組大小為n 。這是K-NN的K部分的來由:k是一些任意值,它告訴模型,分類p時(shí),其應(yīng)考慮選擇多少個(gè)(通常是3-11之間) 最 相似于 p點(diǎn)的點(diǎn)。然后,該模型將采用那些k個(gè)最相似的值,并使用投票技術(shù)來決定如何對(duì)p進(jìn)行分類,如下圖所示。

圖像中的K-NN模型的k值為3,中心箭頭指向的點(diǎn)是p,是需要進(jìn)行分類的點(diǎn)。如你所見,圓中的三個(gè)點(diǎn)是最接近或最類似p的三個(gè)點(diǎn)。因此,使用簡單的投票技術(shù), p將被歸類為“白色”,因?yàn)榘咨珮?gòu)成k個(gè)最相似值的大部分。
太酷了!令人驚訝的是,這種簡單的算法可以在某些情況下實(shí)現(xiàn)超乎想象的結(jié)果,并且可以應(yīng)用于各種各樣的問題,我們將在下面看到。
在Scikit中實(shí)現(xiàn)K-NN算法 - 學(xué)習(xí)對(duì)MNIST圖像進(jìn)行分類
數(shù)據(jù):
對(duì)于此示例,我們將使用無處不在的MNIST數(shù)據(jù)集。MNIST數(shù)據(jù)集是機(jī)器學(xué)習(xí)中最常用的數(shù)據(jù)集之一,因?yàn)樗子趯?shí)現(xiàn),且是作為證明模型的可靠方法。

MNIST是70,000個(gè)手寫數(shù)字的數(shù)據(jù)集,編號(hào)為0-9。沒有兩個(gè)手寫數(shù)字是相同的,有些可能很難正確分類。對(duì)MNIST進(jìn)行分類的人類基準(zhǔn)測(cè)試精度約為97.5%,因此我們的目標(biāo)是擊敗它!
算法:
我們將使用Scikit-Learn Python庫中的KNeighborsClassifier()來開始。這個(gè)函數(shù)需要很多參數(shù),但在這個(gè)例子中我們只需要擔(dān)心一些。具體來說,我們只傳遞n_neighbors參數(shù)的值(這是k值)。weights參數(shù)給出了模型使用的投票系統(tǒng)的類型,其中默認(rèn)值是uniform,這意味著在對(duì)p進(jìn)行分類時(shí),每個(gè)k點(diǎn)的權(quán)重相等。algorithm參數(shù)也將保留其默認(rèn)值auto,因?yàn)槲覀兿M鸖cikit-Learn找到用于對(duì)MNIST數(shù)據(jù)本身進(jìn)行分類的最佳算法。
下面,我嵌入了一個(gè)Jupyter筆記本,用Scikit-Learn構(gòu)建K-NN分類器。開始了!
用基于Scikit-Learn的K-NN分類器來分類MNIST
讓我們通過導(dǎo)入所需的庫直接進(jìn)入它。
- 輸入【1】
import numpy as np
from sklearn import datasets, model_selection
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import classification_report
mnist = datasets.fetch_mldata('MNIST original')
data, target = mnist.data, mnist.target
# 確保所有內(nèi)容都已正確導(dǎo)入
data.shape, target.shape
- 輸出【1】
((70000, 784), (70000,))((70000
構(gòu)建數(shù)據(jù)集
讓我們通過制作一些我們可以使用的不同數(shù)據(jù)集來開始構(gòu)建K-NN模型。 我們將創(chuàng)建一個(gè)可以獲取我們想要的數(shù)據(jù)集大小的函數(shù)并將其返回。 模型將使用這些數(shù)據(jù)集對(duì)我們的測(cè)試數(shù)據(jù)進(jìn)行分類。
讓我們?cè)谙旅鏄?gòu)建一些模型的存儲(chǔ)數(shù)據(jù)集。
- 輸入【2】
# 創(chuàng)建一個(gè)與MNIST大小一致的索引數(shù)組,用于制作數(shù)據(jù)集。
# 該數(shù)組是隨機(jī)排列的,因此我們可以用它來加擾MNIST數(shù)據(jù)。
indx = np.random.choice(len(target), 70000, replace=False)
# method for building datasets to test with
def mk_dataset(size):
"""
生成“size”大小的數(shù)據(jù)集,并返回該數(shù)據(jù)集圖像和目標(biāo)。
這用于生成將由模型存儲(chǔ)的數(shù)據(jù)集,并用于試驗(yàn)不同的存儲(chǔ)數(shù)據(jù)集大小。
"""
train_img = [data[i] for i in indx[:size]]
train_img = np.array(train_img)
train_target = [target[i] for i in indx[:size]]
train_target = np.array(train_target)
return train_img, train_target
很好。 現(xiàn)在,我們將使用此函數(shù)構(gòu)建兩個(gè)不同大小的數(shù)據(jù)集,我們可以使用這些數(shù)據(jù)集來查看模型在進(jìn)行分類時(shí)具有不同數(shù)據(jù)量時(shí)的性能。提示:制作一個(gè)較小的數(shù)據(jù)集就像在教程中的圖像中取走一些點(diǎn)一樣,您仍然可以進(jìn)行分類,但模型使用的點(diǎn)更少了,從而使得更難進(jìn)行正確的分類。
- 輸入【3】
# 讓我們創(chuàng)建一個(gè)大小為50,000的數(shù)據(jù)集,這意味著該模型將有50,000個(gè)數(shù)據(jù)點(diǎn)來比較每個(gè)數(shù)據(jù)點(diǎn)。
# 新點(diǎn)是分類的點(diǎn)
fifty_x, fifty_y = mk_dataset(50000)
fifty_x.shape, fifty_y.shape
- 輸出【3】
((50000, 784), (50000,))
- 輸入【4】
# 讓我們?cè)僮鲆粋€(gè)20000大小的數(shù)據(jù)集,看看當(dāng)我們使用那個(gè)時(shí)分類準(zhǔn)確度如何降低。
twenty_x, twenty_y = mk_dataset(20000)
twenty_x.shape, twenty_y.shape
- 輸出【4】
((20000, 784), (20000,))
注意這些數(shù)據(jù)集如何使模型需要標(biāo)簽。 模型需要這些標(biāo)簽來理解每個(gè)點(diǎn)代表什么,并且因此可以將我們嘗試分類的點(diǎn)(p)放入特定的類中,而不是說“這就是這一點(diǎn)最相似的”,你做不了多少。
現(xiàn)在我們將構(gòu)建一個(gè)大小為10,000的測(cè)試數(shù)據(jù)集。 這是我們將在模型中運(yùn)行的數(shù)據(jù)集,并查看模型在對(duì)測(cè)試數(shù)據(jù)集中的每個(gè)點(diǎn)進(jìn)行分類時(shí)的作用。
- 輸入【5】
# 構(gòu)建模型測(cè)試數(shù)據(jù)集。
test_img = [data[i] for i in indx[60000:70000]]
test_img1 = np.array(test_img)
test_target = [target[i] for i in indx[60000:70000]]
test_target1 = np.array(test_target)
test_img1.shape, test_target1.shape
- 輸出【5】
((10000, 784), (10000,))
很好! 現(xiàn)在我們已經(jīng)完成了所有數(shù)據(jù)設(shè)置,我們可以開始使用K-NN模型了!
建立模型
我們將首先將Scikit-Learn K-NN模型放入一個(gè)函數(shù)中,以便我們可以輕松調(diào)用它并進(jìn)行調(diào)整。
- 輸入【6】
def skl_knn(k, test_data, test_target, stored_data, stored_target):
"""
k: 在分類中使用的鄰居數(shù)量
test_data: 用于測(cè)試分類器的數(shù)據(jù)/目標(biāo)
stored_data: 用于對(duì)test_data進(jìn)行分類的數(shù)據(jù)/目標(biāo)
"""
classifier = KNeighborsClassifier(n_neighbors=k)
classifier.fit(stored_data, stored_target)
y_pred = classifier.predict(test_data)
print(classification_report(test_target, y_pred))
測(cè)試
現(xiàn)在讓我們看看這個(gè)模型如何在兩個(gè)不同的測(cè)試集上執(zhí)行。
- 輸入【7】
%%time
# 存儲(chǔ)的數(shù)據(jù)集大小為50,000
skl_knn(5, test_img1, test_target1, fifty_x, fifty_y)
precision recall f1-score support
0.0 0.98 0.99 0.99 997
1.0 0.96 1.00 0.98 1118
2.0 0.98 0.96 0.97 1041
3.0 0.96 0.98 0.97 1036
4.0 0.98 0.97 0.97 966
5.0 0.97 0.97 0.97 924
6.0 0.98 0.99 0.99 918
7.0 0.96 0.98 0.97 1053
8.0 0.99 0.92 0.96 977
9.0 0.96 0.96 0.96 970
avg / total 0.97 0.97 0.97 10000
CPU times: user 8min, sys: 244 ms, total: 8min 1s
Wall time: 8min 1s
- 輸入【8】
%%time
# 存儲(chǔ)的數(shù)據(jù)集大小為20,000
skl_knn(5, test_img1, test_target1, twenty_x, twenty_y)
precision recall f1-score support
0.0 0.98 0.99 0.98 997
1.0 0.95 0.99 0.97 1118
2.0 0.98 0.94 0.96 1041
3.0 0.94 0.97 0.95 1036
4.0 0.97 0.95 0.96 966
5.0 0.96 0.96 0.96 924
6.0 0.98 0.99 0.99 918
7.0 0.95 0.97 0.96 1053
8.0 0.99 0.90 0.94 977
9.0 0.93 0.95 0.94 970
avg / total 0.96 0.96 0.96 10000
CPU times: user 3min 24s, sys: 240 ms, total: 3min 24s
Wall time: 3min 24s
甜美! 我們的模型與人類相匹配! 如您所見,當(dāng)模型有更多數(shù)據(jù)可用時(shí)(50,000而不是20,000點(diǎn)),它表現(xiàn)更好。 關(guān)于這個(gè)模型的一個(gè)更顯著的事情是,它是如此簡單,但卻可以捕捉人類層面上獨(dú)特圖像之間的復(fù)雜關(guān)系。
要查看更深入的分析,請(qǐng)?jiān)L問此GitHub存儲(chǔ)庫。
太棒了!我們使用Scikit-Learn構(gòu)建了一個(gè)非常簡單的K近鄰模型,它在MNIST數(shù)據(jù)集上獲得了非凡的性能。
問題是啥呢?嗯,它花了很長時(shí)間才對(duì)這些點(diǎn)進(jìn)行分類(兩個(gè)數(shù)據(jù)集分別為8分鐘和近4分鐘),具有諷刺意味的是,K-NN仍然是最快的分類方法之一。必須有一個(gè)更快的方式......
建立更快的模型
大多數(shù)K-NN模型使用歐幾里德或曼哈頓距離作為首選距離度量。這些指標(biāo)很簡單,在各種情況下都表現(xiàn)良好。
很少使用的一個(gè)距離度量是余弦相似度 。余弦相似度通常不是首選距離度量,因?yàn)樗`反了三角不等式,并且不適用于負(fù)數(shù)據(jù)。然而,余弦相似性對(duì)于MNIST來說是完美的。它快速,簡單,并且比MNIST上的其他距離指標(biāo)具有稍微更好的準(zhǔn)確性。但要真正實(shí)現(xiàn)最佳性能,我們必須編寫自己的K-NN模型。在我們自己制作K-NN模型之后,我們應(yīng)該獲得比Scikit-Learn模型更好的性能,甚至可能更好的準(zhǔn)確性。讓我們看看下面的筆記本,我們建立自己的K-NN模型。
建立更快的KNN分類器
在這個(gè)筆記本中,我們將構(gòu)建一個(gè)簡約的K-NN模型,該模型使用余弦相似度作為距離度量來對(duì)MNIST圖像進(jìn)行分類,以試圖找到比Scikit-Learn K-NN模型更快的速度和/或精度。
首先導(dǎo)入所需的庫,并構(gòu)建與Scikit-Learn K-NN筆記本中相同的數(shù)據(jù)集。
- 輸入【1】
import numpy as np
import heapq
from collections import Counter
from sklearn.metrics.pairwise import cosine_similarity
from sklearn import datasets, model_selection
from sklearn.metrics import classification_report
mnist = datasets.fetch_mldata('MNIST original')
data, target = mnist.data, mnist.target
# make sure everything was correctly imported
data.shape, target.shape
- 輸出【1】
((70000, 784), (70000,))
使用與Scikit-Learn K-NN筆記本中相同的方法設(shè)置完全相同的數(shù)據(jù)集。
- 輸入【2】
# 創(chuàng)建一個(gè)與MNIST大小一致的索引數(shù)組,用于制作數(shù)據(jù)集。
# 該數(shù)組是隨機(jī)排列的,因此我們可以用它來加擾MNIST數(shù)據(jù)。
indx = np.random.choice(len(target), 70000, replace=False)
# method for building datasets to test with
def mk_dataset(size):
"""makes a dataset of size "size", and returns that datasets images and targets
This is used to make the dataset that will be stored by a model and used in
experimenting with different stored dataset sizes
"""
train_img = [data[i] for i in indx[:size]]
train_img = np.array(train_img)
train_target = [target[i] for i in indx[:size]]
train_target = np.array(train_target)
return train_img, train_target
- 輸入【3】
# 讓我們創(chuàng)建一個(gè)大小為50,000的數(shù)據(jù)集,這意味著該模型將有50,000個(gè)數(shù)據(jù)點(diǎn)來比較每個(gè)數(shù)據(jù)點(diǎn).
# 新點(diǎn)是要分類的點(diǎn)。
fifty_x, fifty_y = mk_dataset(50000)
fifty_x.shape, fifty_y.shape
- 輸出【3】
((50000, 784), (50000,))
- 輸入【4】
# 讓我們?cè)僮鲆粋€(gè)20000大小的數(shù)據(jù)集,看看當(dāng)我們使用此數(shù)據(jù)集時(shí)分類準(zhǔn)確度如何降低。
twenty_x, twenty_y = mk_dataset(20000)
twenty_x.shape, twenty_y.shape
- 輸出【4】
((20000, 784), (20000,))((20000
- 輸入【5】
# 構(gòu)建模型測(cè)試數(shù)據(jù)集
test_img = [data[i] for i in indx[60000:70000]]
test_img1 = np.array(test_img)
test_target = [target[i] for i in indx[60000:70000]]
test_target1 = np.array(test_target)
test_img1.shape, test_target1.shape
- 輸出【5】
((10000, 784), (10000,))
構(gòu)建模型
下面我們將創(chuàng)建函數(shù)cos_knn(),它將作為我們最新和最好的MNIST K-NN分類器。 請(qǐng)按照函數(shù)中的注釋獲取有關(guān)其工作原理的詳細(xì)信息。
- 輸入【6】
def cos_knn(k, test_data, test_target, stored_data, stored_target):
"""
k: 用于投票的鄰居數(shù)量
test_data: 一組未觀察到的圖像進(jìn)行分類
test_target: test_data的標(biāo)簽(用于計(jì)算精度)
stored_data: 已經(jīng)觀察到并可用于模型的圖像
stored_target: stored_data的標(biāo)簽
"""
# 找到test_data中每個(gè)點(diǎn)與stored_data中每個(gè)其他點(diǎn)之間的余弦相似度。
cosim = cosine_similarity(test_data, stored_data)
# 獲取stored_data中與任何給定test_data點(diǎn)最相似的圖像的前k個(gè)索引。
top = [(heapq.nlargest((k), range(len(i)), i.take)) for i in cosim]
# 使用存儲(chǔ)的目標(biāo)值將索引轉(zhuǎn)換為數(shù)字。
top = [[stored_target[j] for j in i[:k]] for i in top]
# 投票,并返回test_data中每個(gè)圖像的預(yù)測(cè)
pred = [max(set(i), key=i.count) for i in top]
pred = np.array(pred)
# 打印表,使用test_target給出分類器準(zhǔn)確性。
print(classification_report(test_target, pred))
測(cè)試模型
現(xiàn)在,就像Scikit-Learn K-NN模型一樣,我們將在兩個(gè)數(shù)據(jù)集上測(cè)試cos_knn()模型,看看它如何比Scikit-Learn K-NN模型更好。
- 輸入【7】
%%time
# 存儲(chǔ)的數(shù)據(jù)集大小為50,000
cos_knn(5, test_img1, test_target1, fifty_x, fifty_y)
precision recall f1-score support
0.0 0.97 0.99 0.98 992
1.0 0.98 0.99 0.98 1123
2.0 0.98 0.98 0.98 984
3.0 0.98 0.97 0.97 1089
4.0 0.99 0.97 0.98 1016
5.0 0.99 0.96 0.97 857
6.0 0.98 0.99 0.98 979
7.0 0.97 0.96 0.97 1001
8.0 0.96 0.96 0.96 993
9.0 0.95 0.97 0.96 966
avg / total 0.97 0.97 0.97 10000
CPU times: user 5min 17s, sys: 1.21 s, total: 5min 18s
Wall time: 4min 59s
- 輸入【8】
%%time
# 存儲(chǔ)的數(shù)據(jù)集大小為20,000
cos_knn(5, test_img1, test_target1, twenty_x, twenty_y)
precision recall f1-score support
0.0 0.96 0.99 0.98 992
1.0 0.96 0.98 0.97 1123
2.0 0.97 0.97 0.97 984
3.0 0.97 0.95 0.96 1089
4.0 0.98 0.95 0.97 1016
5.0 0.97 0.94 0.96 857
6.0 0.97 0.99 0.98 979
7.0 0.96 0.96 0.96 1001
8.0 0.96 0.95 0.95 993
9.0 0.94 0.96 0.95 966
avg / total 0.97 0.97 0.97 10000
CPU times: user 2min 9s, sys: 528 ms, total: 2min 9s
Wall time: 2min 1s
太棒了! 我們自己構(gòu)建的余弦相似度模型優(yōu)于Scikit-Learn K-NN! 值得注意的是,該模型在分類速度(相當(dāng)大的幅度)和準(zhǔn)確性方面均優(yōu)于Scikit-Learn K-NN,而且模型非常簡單!
有關(guān)模型如何工作以及如何在許多不同情況下與Scikit-Learn K-NN比較的進(jìn)一步分析,請(qǐng)參閱此GitHub存儲(chǔ)庫。
如筆記本所示,我們自己的K-NN模型在分類速度(相當(dāng)大的幅度)和準(zhǔn)確性(一個(gè)數(shù)據(jù)集上提高1%)方面優(yōu)于Scikit-Learn K-NN!現(xiàn)在,我們可以在實(shí)踐中繼續(xù)實(shí)施這個(gè)模型,因?yàn)槲覀円呀?jīng)開發(fā)出了一種真正快速的算法。
結(jié)論
說了很多,但我們學(xué)到了幾個(gè)寶貴的經(jīng)驗(yàn)教訓(xùn)。首先,我們了解了K-NN的工作原理,以及如何輕松實(shí)現(xiàn)它。但最重要的是,我們了解到,始終考慮你要解決的問題以及可用于解決該問題的工具非常重要。有時(shí),最好在解決問題時(shí)花時(shí)間嘗試 - 并且是的,建立自己的模型。正如在筆記本中證明的那樣,它可以帶來巨大的收益:我們的第二個(gè)專有模型使用了1.5-2倍的加速,節(jié)省了使用該模型的實(shí)體很多時(shí)間。
如果你想了解更多信息,我建議你查看這個(gè)GitHub存儲(chǔ)庫 ,在這里你可以找到兩個(gè)模型之間更全面的分析,以及一些關(guān)于我們更快的K-NN模型的更有趣的功能!