算法概述
自我感覺K-近鄰(k-NearestNeighbor)算法是最簡單最易理解的分類算法了。怎麼個簡單法呢?簡單到沒有一個訓練分類器的過程,僅僅根據需要分類的樣本到已知類別的樣本之間的距離來進行分類。
簡單來講,如果存在一個訓練樣本集,並且訓練集中每個樣本對應的類別也已知;那麼對於未知類別的新樣本,我們計算它到每個訓練樣本的距離,然後選擇距離最近的k個樣本,這k個樣本中計數最多的一類就是待分類樣本的類別。
k-NN算法的步驟如下:
- 根據特徵值計算待分類樣本到訓練集每個樣本的距離;
- 按照距離遞增次序排序;
- 選出與待分類樣本距離最近的前 k 個樣本;
- 計算前 k 個樣本中每一類別的數量;
- 將前 k 個樣本中計數最多的類別數作爲待分類樣本的類別。
注意
-
距離
K-近鄰算法計算距離時可以使用常用的距離度量方法,一般可以選擇歐式距離,計算方式如下:
和 分別表示兩個樣本, 表示特徵維度。 -
k的取值
k的取值很重要,k值太小的話算法對於一些噪聲成分會比較敏感,k太大的話離待分類樣本距離較遠的點也會對分類結果產生影響,這都不是想要的結果。一般建議k取小於20的整數。
利用k-NN實現手寫數字識別
k-NN算法的原理比較容易理解,接下來通過手寫數字識別的例子來看看怎麼實現一個k-近鄰分類器。
數據集
這裏用到的手寫數字數據集不是MNIST數據集,而是採用文本格式存儲的數字圖像,如下圖所示。每個文本文件存儲着 個黑白像素點,用來表示一個手寫數字。數據集中包含了2000個訓練樣本和900個測試樣本。
代碼實現
像素文本轉換爲特徵向量
上面提到,每個樣本都是一個的二進制圖像,因此我們首先將每個樣本讀取爲一個向量的表示方式:
def img2vector(fileName):
returnVect = np.zeros((1, 1024))
with open(fileName) as fr:
i = 0
for lineStr in fr: # 按行讀取文件
for j in range(32):
returnVect[0, 32*i+j] = int(lineStr[j])
i += 1
return returnVect
傳入參數爲文件名,最後返回樣本的向量表示。
分類函數
k-NN沒有一個顯式的訓練過程,對於每個測試樣本,對其進行分類的時候都需要計算到每一個訓練樣本的距離,進而根據距離最近的k個樣本的類別進行分類。
def classify(testX, trainingSet, labels, k):
'''
testX: 測試樣本的特徵向量
trainingSet: 訓練集特徵
labels: 訓練集類別標籤
k: 最鄰近樣本個數
'''
m = trainingSet.shape[0] # 訓練樣本的數量
distance = getDistance(testX, trainingSet) # 計算測試樣本到每個訓練樣本的距離
nearestIndices = np.argsort(distance)[:k] # 距離最小的k個樣本的索引
# 計算前k個樣本中每一類的數量
classCount = {}
maxCount = 0
nearestClass = None
for i in range(k):
voteIlabel = labels[nearestIndices[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
if classCount[voteIlabel] > maxCount: # 更新最多的計數和其對應的標籤
maxCount = classCount[voteIlabel]
nearestClass = voteIlabel
return nearestClass
完整的代碼實現如下:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
'''
@Date : 2019/9/27
@Author : Rezero
'''
import numpy as np
import os
def img2vector(fileName):
returnVect = np.zeros((1, 1024))
with open(fileName) as fr:
i = 0
for lineStr in fr: # 按行讀取文件
for j in range(32):
returnVect[0, 32*i+j] = int(lineStr[j])
i += 1
return returnVect
def getDistance(testX, trainingSet):
return np.sqrt(np.sum((trainingSet - testX)**2, axis=1)) # 歐式距離
def classify(testX, trainingSet, labels, k):
'''
testX: 測試樣本的特徵向量
trainingSet: 訓練集特徵
labels: 訓練集類別標籤
k: 最鄰近樣本個數
'''
m = trainingSet.shape[0] # 訓練樣本的數量
distance = getDistance(testX, trainingSet) # 計算測試樣本到每個訓練樣本的距離
nearestIndices = np.argsort(distance)[:k] # 距離最小的k個樣本的索引
# 計算前k個樣本中每一類的數量
classCount = {}
maxCount = 0
nearestClass = None
for i in range(k):
voteIlabel = labels[nearestIndices[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
if classCount[voteIlabel] > maxCount: # 更新最多的計數和其對應的標籤
maxCount = classCount[voteIlabel]
nearestClass = voteIlabel
return nearestClass
def main():
labels = []
trainingFiles = os.listdir('data/trainingDigits') # 訓練集路徑
trainNum = len(trainingFiles) # 訓練集樣本數
trainingMat = np.zeros((trainNum, 1024))
for i in range(trainNum):
fileName = trainingFiles[i]
clas = int(fileName.split('_')[0]) # 當前樣本的類別
labels.append(clas)
trainingMat[i, :] = img2vector('data/trainingDigits/' + fileName) # 把每個樣本所表示的數字讀取爲一個1*1024的向量
testFiles = os.listdir('data/testDigits') # 測試集路徑
errorCount = 0 # 分類錯誤計數
testNum = len(testFiles)
for i in range(testNum):
fileName = testFiles[i]
clas = int(fileName.split('_')[0]) # 樣本的真實類別
vectorUnderTest = img2vector('data/testDigits/' + fileName) # 當前測試樣本的向量
# 使用kNN分類器對當前樣本進行分類,設置k=3
classifierResult = classify(vectorUnderTest, trainingMat, labels, 3)
print("The classifier came back with: %d, the real answer is: %d" % (classifierResult, clas))
if clas != classifierResult:
errorCount += 1
print("The total number of errors is: %d" % errorCount)
print("the total error rate is %f: " %(errorCount/testNum))
if __name__ == "__main__":
main()
k-NN算法的優缺點
-
優點
簡單易懂、精度高、對異常值不敏感、無數據輸入假定,可以用於數值型和離散型數據 -
缺點
計算複雜度高,單個樣本分類需要計算到所有訓練樣本(已知樣本)的距離;空間複雜度高,需要存儲所有的訓練樣本,空間開銷大。
參考資料
《機器學習實戰》第二章:k-近鄰算法