K-近鄰演算法—基本原理與實戰
概述
k-近鄰演算法(k-Nearest Neighbor, KNN),是一個理論上比較成熟的方法,也是最簡單的機器學習演算法之一,用於預測資料的類別,以及對資料進行分類。該方法的簡要思路就是採用測量不同特徵值之間的距離來進行分類。
特點
工作原理
存在一個樣本資料集合,也稱作訓練樣本集,並且樣本集中的每個資料都存在標籤,即我們知道樣本集中每一個數據與所屬分類的對應關係。當輸入一個沒有包含標籤(也就是對應的類別)的新資料後,將新資料的每個特徵與樣本集中資料對應的特徵進行比較,然後用演算法提取樣本集中特徵與新資料最相似的資料(最近鄰)的分類標籤。一般來說,只取樣本資料集中前k個最相似的資料,這就是k-近鄰演算法中k的出處,通常k是不大於20的整數。最後選擇k個最相似資料
使用歐式距離公式,計算兩個向量點xA和xB的距離:
kNN演算法的一般流程
(1)收集資料:可以使用任何方法。
(2)準備資料:距離計算所需要的數值,最好是結構化的資料格式。
(3)分析資料:可以使用任何方法。
(4)訓練演算法:k-近鄰演算法不需要訓練演算法。
(5)測試演算法:計算錯誤率。
(6)使用演算法:首先需要輸入樣本資料和結構化的輸出結果,然後執行k-近鄰演算法判定輸入資料分別屬於哪個分類,最後應用對計算出的分類執行後續的處理。
實施kNN分類演算法
#kNN演算法的虛擬碼 #功能:使用k-近鄰演算法將資料劃分到某個類中 #對未知類別屬性的資料集中的每個點依次執行以下操作: (1)計算已知類別資料集中的點與當前點之間的距離; (2)按照距離遞增次序對資料集中的點進行排序; (3)選取與當前點距離最小的k個點; (4)確定前k個點所在類別的出現頻率; (5)返回前k個點出現頻率最高的類別作為當前點的預測分類;
簡單分類示例
from numpy import *
import operator
#createDataSet():產生樣本集資料和樣本集中每個資料的型別資訊
def createDataSet():
group = array([[1.0, 1.1], [1.0, 1.0], [0, 0], [0, 0.1]])
labels = ['A', 'A', 'B', 'B']
return group, labels
函式createDataSet()初始化了四個樣本資料,每個樣本資料含有兩個特徵值,並且還初始化了標籤資訊,其中第一個資料的標籤是A,第二個是A,第三個是B,第四個是B。產生的樣本資料用於對新資料進行分類預測。
#classify0:k-近鄰演算法的基本實現,返回資料的類別預測資訊
#inX:用於分類的輸入向量
#dataSet:訓練樣本集
#labels:訓練樣本集對應的標籤向量
#k:選擇最近鄰居的數目
from numpy import *
def classify0(inX, dataSet, labels, k):
dataSetSize = dataSet.shape[0] #獲取樣本資料的行數
diffMat = tile(inX, (dataSetSize, 1)) - dataSet #將輸入向量擴充套件到和樣本資料同樣大小,然後與樣本資料相減
sqDiffMat = diffMat ** 2; #對相減後的陣列平方(每個元素平方);如果是矩陣,表示矩陣平方(矩陣乘法)
sqDistances = sqDiffMat.sum(axis=1) #axis=0表示將矩陣或陣列的每個元素相加;
#axis=1表示將矩陣或陣列的每行的每個元素相加,相加的結果(數量和行數相同)組成一個一維的陣列
distances = sqDistances ** 0.5 #對陣列中的每個元素求平方根
sortedDistIndicies = distances.argsort() #對distances陣列中的元素進行排序,返回從小到大的索引值
classCount = {} #建立一個空字典
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]] #根據距離從小到大或取樣本資料的類別
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
#將類別作為key,出現次數作為value,類別每出現一次,key值加1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
#根據字典中value值的大小進行排序,預設從小到大,新增reverse=True表示從大到小
return sortedClassCount[0][0] #返回出現次數最高的類別
執行classify0(),傳入上面生成的資料,返回B。
分類器的測試
為了檢驗分類器給出的答案是否符合預期,就需要對分類器進行測試。通過大量的測試資料,我們可以得到分類器的錯誤率——分類器給出錯誤結果的次數/測試執行總次數。完美的分類器錯誤率為0,最差的分類器錯誤率為1.0。
歸一化特徵值
對於大多數資料來說,其每個特徵的特徵值可能數值差距很大。例如某個人,身高屬性1.7m,而體重屬性70KG,這兩個屬性數值差距比較大,如果直接用尤拉距離公式計算,體重對分類結果的影響會大大提升,而真正意願則是體重和身高對分類結果的影響相同,因此需要對這些資料進行歸一化。
使用下列公式可以將任意取值範圍的特徵值轉化為0到1區間內的值:
其中oldValue是需要進行歸一化的特徵值,min是所有同類特徵值最小的特徵值,max是所有同類特徵值最大的特徵值。
程式碼示例:
def autoNorm(dataSet):
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
ranges = maxVals - minVals
normDataSet = zeros(shape(dataSet)) #建立一個和dataSet大小相同的陣列
m = dataSet.shape[0] #dataSet的行數
normDataSet = dataSet - tile(minVals, (m, 1)) #minVals擴充套件:行*m,列*n
normDataSet = normDataSet/tile(ranges, (m, 1)) #特徵值相除
return normDataSet, ranges, minVals
示例一:使用kNN演算法改進約會網站的配對效果
這裡使用的示例都來自書籍《機器學習實戰》,原始碼下載
(1)收集資料:提供文字檔案。
(2)準備資料:使用Python解析文字檔案。
(3)分析資料:使用Matplotlib畫二維擴散圖。
(4)訓練演算法:此步驟不適用於k-近鄰演算法。
(5)測試演算法:使用訓練資料中10%的資料進行測試。
(6)使用演算法:產生簡單的命令列程式,然後輸入特徵值進行判斷。
準備資料:將文字記錄轉換成NumPy的解析程式:
def file2matrix(filename):
fr = open(filename)
arrayOLines = fr.readlines()
numberOfLines = len(arrayOLines)
#獲取列數
cols = len(arrayOLines[0].split('\t'))
returnMat = zeros((numberOfLines, cols - 1))
classLabelVector = []
index = 0
for line in arrayOLines:
line = line.strip()
listFromLine = line.split('\t')
returnMat[index, :] = listFromLine[0:cols-1]
classLabelVector.append(int(listFromLine[-1])) #特徵值為int型
index += 1
return returnMat, classLabelVector
分析資料:使用Matplotlib建立散點圖
import matplotlib
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(datingDataMat[:, 1], datingDataMat[:, 2])
plt.show()
準備資料:歸一化數值
採用上述方法進行歸一化
測試演算法:作為完整程式驗證分類器
#測試分類器的效果
def datingClassTest():
hoRatio = 0.10
datingDataMat, datingLabels = file2matrix('datingTestSet2.txt')
normMat, ranges, minVals = autoNorm(datingDataMat)
m = normMat.shape[0]
numTestVecs = int(m*hoRatio)
errorCount = 0.0
for i in range(numTestVecs):
classifierResult = classify0(normMat[i, :], normMat[numTestVecs:m, :], datingLabels[numTestVecs:m], 10)
print("The classifier came back with: %d, the real answer is: %d" % (classifierResult, datingLabels[i]))
if classifierResult != datingLabels[i]:
errorCount += 1.0
print("The total error rate is: %f", errorCount/float(numTestVecs))
使用演算法:構建完整可用系統
def classifyPerson():
resultList = ['not at all', 'in small doses', 'in large doses']
percentTats = float(input("percentage of time spent palying video games?"))
ffMiles = float(input("frequent flier miles earned per year?"))
iceCream = float(input("liters of ice cream consumed per year?"))
datingDataMat, datingLables = file2matrix('datingTestSet2.txt')
normMat, ranges, minVals = autoNorm(datingDataMat)
inArr = array([ffMiles, percentTats, iceCream])
classifierResult = classify0((inArr - minVals)/ranges, normMat, datingLables, 3)
print("You will like the person: ", resultList[classifierResult - 1])
示例二:手寫識別系統
原理:將影象經過圖形處理軟體轉為為二進位制的形式,然後將二進位制圖片轉化為一個向量,每一位二進位制都是向量的一個元素,將大量向量再加上對應的類別作為訓練資料集。使用演算法時,使用沒有類別的向量,計算其與訓練資料集向量的距離,通過kNN演算法預測出類別資訊。
準備資料:將影象(已經是二進位制格式)轉換為測試向量
def img2vector(filename):
returnVect = zeros((1, 1024))
fr = open(filename)
for i in range(32):
lineStr = fr.readline()
for j in range(32):
returnVect[0, 32*i + j] = int(lineStr[j])
return returnVect
測試演算法:使用k-近鄰演算法識別手寫數字
def handwritingClassTest():
hwLabels = []
trainingFileList = listdir('trainingDigits') #listdir需要使用 from os import listdir
m = len(trainingFileList)
trainingMat = zeros((m, 1024))
for i in range(m):
fileNameStr = trainingFileList[i] #每個檔案的檔名,如:0_1.txt
fileStr = fileNameStr.split('.')[0] #獲取除字尾的檔名,如:0_1
classNumStr = int(fileStr.split('_')[0]) #獲取檔案的標籤,如0_1表示數字0的第一個訓練資料,則該資料的的標籤是1
hwLabels.append(classNumStr)
trainingMat[i, :] = img2vector('trainingDigits/%s' % fileNameStr)
testFileList = listdir('testDigits')
errorCount = 0.0
mTest = len(testFileList)
for i in range(mTest):
fileNameStr = testFileList[i]
fileStr = fileNameStr.split('.')[0]
classNumStr = int(fileStr.split('_')[0])
vectorUnderTest = img2vector('testDigits/%s' % fileNameStr)
classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3)
print("The classifier came back with: %d, the real answer is: %d" % (classifierResult, classNumStr))
if classifierResult != classNumStr:
errorCount += 1.0;
print("The total number of errors is: %d" % errorCount)
print("The total error rate is: %f" % (errorCount / float(mTest)))