1 k-近鄰算法概述
- k-近鄰算法採用測量不同特徵值之間的距離方法進行分類。
- 優點:精度高、對異常值不敏感、無數據輸入假定
- 缺點:計算複雜度高、空間複雜度高
- 適用數據範圍:數值型和標稱型
1.1 k-近鄰算法工作原理:
存在一個樣本數據集合(訓練集),並且樣本集中每個數據都有對應的標籤。在輸入一個沒有標籤的新數據時,將這個新數據的每個特徵與樣本集中數據對應的特徵進行比較,然後從樣本集中提取出與新數據特徵最相似的前k個數據的標籤。在這k個標籤中,出現次數最多的標籤,就認爲是新數據的標籤。
上面說的最相似,表現在可視化中就意味着最近鄰。比如判斷電影是愛情片還是動作片的二分類問題,在打鬥和接吻鏡頭的比例圖中,我們分別標出了樣本集位於圖中的位置,問號表示未知電影的位置:
然後通過數學方法計算出每個樣本點與輸入數據的歐式距離:
計算兩個向量點和之間的距離:
例如,點(0, 0)與(1, 2)之間的距離計算爲:
如果數據集存在4個特徵值,則點(1, 0, 0, 1)與(7, 6, 9, 4)之間的距離計算爲:
利用k-近鄰算法,假設k爲3,那麼與新數據最近鄰(最相似)的3個電影爲He's Not Really into Dudes
、Beautiful Woman
和California Man
,這3個電影的標籤都是“愛情片”,所以我們判斷未知電影是“愛情片”。
1.2 k-近鄰算法一般流程:
(1)收集數據:可以使用任何方法。
(2)準備數據:距離計算所需要的數值。
(3)分析數據:可以使用任何方法。
(4)訓練算法:此步驟不適用於k-近鄰算法。
(5)測試算法:計算錯誤率。
(6)使用算法:首先需要輸入樣本數據和結構化的輸出結果,然後運行k-近鄰算法判定輸入數據分別屬於哪個分類,最後應用對計算出的分類執行後續的處理
1.3 k-近鄰的簡單實現
k-近鄰算法對未知類別屬性的數據集中的每個點依次執行以下操作:
(1) 計算已知類別數據集中的點與當前點之間的距離;
(2) 按照距離遞增次序排序;
(3) 選取與當前點距離最小的k個點;
(4) 確定前k個點所在類別的出現頻率;
(5) 返回前k個點出現頻率最高的類別作爲當前點的預測分類。
# coding:utf-8
# python3.7
from numpy import *
import operator
def createDataSet():
group = array([[1.0, 1.1],
[1.0, 1.0],
[0, 0],
[0, 0.1]])
lables = ['A', 'A', 'B', 'B']
return group, lables
def classify0(inx, dataSet, labels, k):
"""
inx: 輸入向量
dataSet: 輸入的訓練樣本集
labels: 標籤向量
k: 選擇最近鄰的數目
"""
dataSetSize = dataSet.shape[0] # 行數4
# tile():就是將原矩陣橫向、縱向地複製
# 計算歐式距離
diffMat = tile(inx, (dataSetSize, 1)) - dataSet # 對每一個特徵先取差值
sqDiffMat = diffMat ** 2 # 再平方
sqDistances = sqDiffMat.sum(axis=1) # 再相加
distances = sqDistances ** 0.5 # 開方
sortedDistIndicies = distances.argsort() # argsort()從小到大排序,返回索引位置
classCount = {}
for i in range(k):
# 提取出k個最近鄰的標籤
voteIlabel = labels[sortedDistIndicies[i]]
# 統計各個標籤出現的頻率
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
# 按照第二個元素的次序對元組進行逆序排序
sortedClassCount = sorted(classCount.items(),
key=operator.itemgetter(1), reverse=True)
# 返回頻率最高的
return sortedClassCount[0][0]
# just for test
if __name__ == "__main__":
group, labels = createDataSet()
print(classify0([0, 0], group, labels, 3))
上面代碼中的classify0()
實際上就是k-近鄰算法的簡單分類過程,輸出的結果是B
。
2 使用 k-近鄰算法改進約會網站的配對效果
步驟:
(1)手機數據:提供文本文件。
(2)準備數據:使用Python解析文本文件。
(3)分析數據:使用Matplotlib畫二維擴散圖。
(4)訓練算法:此步驟不適用於k-近鄰算法。
(5)測試算法:使用部分數據作爲測試樣本,測試樣本的類別已知,若預測類別與實際類別不同,則標記爲一個錯誤。
(6)使用算法:輸入一些特徵數據以判斷預測分類。
2.1 解析數據
訓練集datingTestSet.txt:
- 每個樣本佔一行,每行包括3個特徵和一個分類標籤,共1000行。
- 3個特徵分別爲:每年獲得的飛行常客里程數、玩視頻遊戲所耗時間百分比、每週消費的冰淇淋公升數。
上面的數據集需要進行處理,將其轉化爲分類器可以接受的格式(訓練樣本矩陣、類標籤向量等),代碼如下:
def file2matrix(filename):
fr = open(filename)
arrayOLines = fr.readlines()
numberOfLines = len(arrayOLines)
returnMat = zeros((numberOfLines, 3)) # 創建與數據大小一致的numpy矩陣
classLabelVector = []
index = 0
for line in arrayOLines:
line = line.strip()
listFromLine = line.split('\t')
# 將文本數據前3列填到創建的numpy矩陣中
returnMat[index, :] = listFromLine[0:3]
# 填入分類的標籤(最後一列)
classLabelVector.append(int(listFromLine[-1]))
index += 1
return returnMat, classLabelVector
結果如下:
2.2 分析數據
使用Matplotlib庫製作原始數據散點圖:
import matplotlib
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(datingDataMat[:, 1], datingDataMat[:, 2],
15.0*array(datingLabels), 15.0*array(datingLabels))
plt.xlabel('Percent of time spent playing video games')
plt.ylabel('Ice cream liters consumed per week')
plt.show()
2.3 歸一化數值
由前面所介紹的,如果我們要求上表中樣本3和樣本4之間的距離,計算方法如下:
可以看到上式的結果主要會被裏程數的差值所決定,而三個特徵實際上應該同等重要,造成的原因僅僅是因爲這個特徵的數值大,這樣就會使結果不準確。
爲了避免這種情況的影響,我們需要將數值歸一化,如將取值範圍都處理到0至1或者-1至1之間,可以下面的公式可以將任意取值範圍的特徵值轉化爲0到1區間內的值:
newValue = (oldValue - min) / (max - min)
其中min和max是數據集中的最小和最大特徵值。
歸一化特徵值的函數如下:
def autoNorm(dataSet):
# 參數0使得函數從列中選取最小值
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
ranges = maxVals - minVals
# 創建歸一化矩陣
normDataSet = zeros(shape(dataSet))
m = dataSet.shape[0]
normDataSet = dataSet - tile(minVals, (m, 1))
normDataSet = normDataSet / tile(ranges, (m, 1))
return normDataSet, ranges, minVals
2.3 測試算法
通常我們只提供已有數據的90%作爲訓練樣本來訓練分類器,而使用其餘的10%數據(隨機選取的)去測試分類器,檢測分類器的正確率。
可以使用錯誤率來檢測分類器的性能,對於分類器來說,錯誤率就是分類器給出錯誤結果的次數除以測試數據的總數。
該分類器的測試代碼如下:
def datingClassTest():
hoRatio = 0.10
datingDataMat, datingLabels = file2matrix('datingTestSet2.txt')
# 對數據進行歸一化
normMat, ranges, minVals = autoNorm(datingDataMat)
m = normMat.shape[0]
# 選取10%
numTestVecs = int(m * hoRatio)
errorCount = 0.0
for i in range(numTestVecs):
classifierResult = classify0(normMat[i, :], normMat[numTestVecs:m, :], \
datingLabels[numTestVecs:m], 3)
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)))
上述結果的錯誤率爲5%,通過改變hoRatio和k的值,錯誤率可能會發生變化。
2.5 使用算法
我們在前面已經寫好了分類器並進行了測試,現在要使用這個分類器,可以通過下面的預測函數:
def classifyPerson():
resultList = ['not at all', 'in small doses', 'in large doses']
percentTats = float(input("percentage of time spent playing video games?\n"))
ffMiles = float(input("frequent flier miles earned per year?\n"))
iceCream = float(input("liters of ice cream consumed per year?\n"))
datingDataMat, datingLabels = file2matrix('datingTestSet2.txt')
normMat, ranges, minVals = autoNorm(datingDataMat)
inArr = array([ffMiles, percentTats, iceCream])
classifierResult = classify0((inArr-minVals)/ranges, normMat,
datingLabels, 3)
print("You will probably like this person:",\
resultList[classifierResult - 1])
調用該函數結果如下:
3 手寫識別系統
下面構建一個使用k-近鄰分類器的手寫識別系統,這裏僅能識別數字0-9。需要識別的數字已經使用圖形處理軟件,處理成具有相同色彩和大小(32×32),並且爲了方便理解,將圖像轉換爲文本格式。
3.1 準備數據
目錄trainingDigits中包含了大約2000個例子,每個例子的內容如下圖所示(由0-1二進制矩陣組成),每個數字大約有200個樣本:
目錄testDigits中包含了大約900個測試數據。我們使用目錄trainingDigits中的數據訓練分類器,使用目錄testDigits中的數據測試分類器的效果。
手寫需要將一個32×32的二進制圖形矩陣轉換爲1×1024的向量,這樣才能使用分類器處理:
def img2vector(filename):
returnVect = zeros((1, 1024))
fr = open(filename)
for i in range(32):
linestr = fr.readlines()
for j in range(32):
returnVect[0, 32 * i + j] = int(linestr[j])
return returnVect
3.2 測試算法
測試函數如下:
- 需要使用到os庫中的listdir()函數來遍歷數據集。
- 創建一個m行1024列的訓練矩陣,m爲訓練集的數量,1024爲每個訓練數據的二進制矩陣轉換而來的特徵。
- 每個數據的標籤可以從文件名中獲取,存在hwLabels中。
- 因爲每個特徵均爲0-1二進制,所以無需歸一化。
- 最後以同樣的方式遍歷測試集來進行預測,並計算錯誤率。
import os
# ...
def handwritingClassTest():
hwLabels = []
trainingFileList = os.listdir('./digits/trainingDigits') # 加載訓練集
m = len(trainingFileList)
trainingMat = zeros((m, 1024))
for i in range(m):
# 從文件名獲取分類標籤添加到hwLabels中
fileNameStr = trainingFileList[i]
fileStr = fileNameStr.split('.')[0]
classNumStr = int(fileStr.split('_')[0])
hwLabels.append(classNumStr)
# 數據處理
trainingMat[i, :] = img2vector('./digits/trainingDigits/%s' % fileNameStr)
# 測試集
testFileList = os.listdir('./digits/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('./digits/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("\nthe total number of errors is: %d" % errorCount)
print("\nthe total error rate is: %f" % (errorCount / float(mTest)))
上述程序的錯誤率爲1.05%,實際還可以調整訓練樣本、k值等參數來對錯誤率進行優化。
2.4 總結
(1)k-近鄰算法使分類數據最簡單有效的算法,其基於實例的學習要求我們必須有接近實際數據的訓練樣本數據。
(2)對於數據集較大的情況時,k-近鄰算法就不是一個搞笑的算法。
- 其必須保存全部數據集,如果訓練數據集很大,就會消耗大量的存儲空間。
- 對數據集中的每個數據計算距離,也非常的耗時。
(3)k-近鄰算法的另一個缺陷是它無法給出任何數據的基礎結構信息,因此我們也無法知曉平均
實例樣本和典型實例樣本具有什麼特徵。