目錄
工作原理
存在一個樣本資料集合,也稱作訓練樣本集,並且樣本集中每個資料都存在標籤,即我們知道樣本集中每一資料與所屬分類的對應關係。輸入沒有標籤的新資料後,將新資料的每個特徵與樣本集中資料對應的特徵進行比較,然後演算法提取樣本集中特徵最相似資料(最近鄰)的分類特徵。一般來說,我們只選擇樣本資料集中前k個最相似的資料,這就是k-近鄰演算法中k的出處,通常k是不大於20的整數。最後選擇k個最相似資料中出現次數最多的分類,作為新資料的分類。
舉個例子,現在我們用k-近鄰演算法來分類一部電影,判斷它屬於愛情片還是動作片。現在已知六部電影的打鬥鏡頭、接吻鏡頭以及電影評估型別,如下圖所示。
現在我們有一部電影,它有18個打鬥鏡頭、90個接吻鏡頭,想知道這部電影屬於什麼型別。根據k-近鄰演算法,我們可以這麼算。首先計算未知電影與樣本集中其他電影的距離(先不管這個距離如何算,後面會提到)。現在我們得到了樣本集中所有電影與未知電影的距離。按照距離遞增排序,可以找到k個距離最近的電影。
現在假定k=3
,則三個最靠近的電影依次是He's Not Really into Dudes、Beautiful Woman、California Man。
k-近鄰演算法按照距離最近的三部電影的型別,決定未知電影的型別,而這三部電影全是愛情片,因此我們判定未知電影是愛情片。
python實現
首先編寫一個用於建立資料集和標籤的函式,要注意的是該函式在實際用途上沒有多大意義,僅用於測試程式碼。
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
然後是函式classify0()
,該函式的功能是使用k-近鄰演算法將每組資料劃分到某個類中,其虛擬碼如下:
對未知類別屬性的資料集中的每個點依次執行以下操作:
(1)計算已知類別資料集中的點與當前點之間的距離;
(2)按照距離遞增次序排序;
(3)選取與當前點距離最小的k個點;
(4)確定前k個點所在類別的出現頻率;
(5)返回前k個點出現頻率最高的類別作為當前點的預測分類。
Python程式碼如下:
def classify0(inX, dataSet, labels, k):
dataSetSize = dataSet.shape[0] # shape[0]表示矩陣有多少行 shape[1]表示矩陣有多少列
diffMat = tile(inX, (dataSetSize,1)) - dataSet # 計算Ai-Bi
sqDiffMat = diffMat**2 #計算(Ai-Bi)^2
sqDistances = sqDiffMat.sum(axis=1) # 計算(A0-B0)^2+...+(Ai-Bi)^2
distances = sqDistances**0.5 # 計算((A0-B0)^2+...+(Ai-Bi)^2)^0.5 也就是歐式距離
sortedDistIndicies = distances.argsort() # 得到陣列的值按遞增排序的索引
classCount = {}
for i in range (k): #距離最近的k個點
voteIlabel = labels[sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0)+1 # 如果voteIlabels的key不存在就返回0
sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
該函式具有4個輸入引數,分別是待分類的輸入向量inX、輸入的訓練樣本集dataSet、標籤向量labels、選擇距離最近的k個點。其中距離使用歐式距離,計算公式如下:
例如,點(0,0)與(1,2)之間的歐式距離計算為:
如果資料集存在4個特徵值,則點(1,0,0,1)與(7,6,9,4)之間的歐式距離計算為:
計算完所有點之間的距離後,可以對資料按照從小到大的次序排序。然後,確定前k個距離最小元素所在的主要分類。輸入k總是正整數;最後,將classCount字典分解為元組列表,然後按照從大到小的次序進行排序,最後返回頻率最高的元素標籤。
執行程式後得到如下結果應該是B
演算法實戰
舉兩個例子,一個是約會物件的好感度預測,一個是手寫識別系統。
約會物件好感度預測
故事背景
海倫小姐是一個大齡單身女青年,她一直通過網路尋找適合自己的另一半。儘管網上會遇到不一樣的約會物件,但是她並不是喜歡每一個人。經過一番總結,她發現她曾和三種型別的人約會過:
- [ ] 不喜歡的人
- [ ] 魅力一般的人
- [ ] 極具魅力的人
她還發現當她歸類約會物件時主要考慮以下三個特徵:
- [ ] 月收入
- [ ] 顏值
- [ ] 每週跑步的公里數
她將這些資料儲存在文字檔案datingTestSet2.txt
中。
準備資料:從文字檔案中解析資料
首先要將待處理資料的格式改變為分類器可以接受的格式。建立名為file2matrix()
的函式,以此來處理輸入格式問題。該函式的輸入為檔名字串,輸出為訓練樣本矩陣和類標籤向量。
def file2matrix(filename):
fr = open(filename,encoding = 'utf-8')
arrayOfLines = fr.readlines() #讀取檔案的每一行
numberOfLines = len(arrayOfLines) #獲得檔案行數
returnMat = zeros((numberOfLines,3))
classLabelVector = []
index = 0
for line in arrayOfLines:
line = line.strip() #去除首尾空格和回車
listFromLine = line.split() #按照tab鍵分割資料
returnMat[index,:] = listFromLine[0:3]
classLabelVector.append(int(listFromLine[-1]))
index += 1
return returnMat,classLabelVector
開啟檔案,得到檔案的行數。然後建立以零填充的矩陣。迴圈處理檔案中的每行資料,首先使用函式line.strip()
擷取掉所有的回車字元,然後使用tab字元\t將上一步得到的整行資料分割成一個元素列表。接著,選取前3個元素,將它們存到特徵矩陣中。利用負索引將列表的最後一列儲存到向量classLabelVector
中。
分析資料:使用Matplotlib建立散點圖
這一步不過多解釋,建立視覺化資料圖。
def drawFig(datingDataMat,datingLabels):
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(datingDataMat[:, 1], datingDataMat[:, 2],15.0*array(datingLabels), 15.0*array(datingLabels))
plt.show()
準備資料:歸一化數值
因為月收入的數值和其他兩個特徵相比大很多,因此對於計算距離的影響遠大於其他兩個特徵。但是在海倫看來這是三個等權重的特徵,月收入不應該如此嚴重地影響到計算結果。
因此我們需要進行數值歸一化。採用公式newValue = (oldValue-min)/(max-min)
可以將任意取值範圍的特徵值轉化為0到1的區間。其中min和max分別是資料集中最小特徵值和最大特徵值。
def autoNorm(dataSet):
minVals = dataSet.min(0) #引數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
測試演算法:作為完整程式驗證分類器
在資料集中選取10%的資料作為測試資料。
def datingClassTest():
hoRatio = 0.10 # 10%的資料作為測試集
datingDataMat, datingLabels = file2matrix("datingTestSet2.txt") # load data setfrom file
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], 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)))
得到結果如下:
the classifier came back with: 3, the real answer is: 3
the classifier came back with: 2, the real answer is: 2
the classifier came back with: 1, the real answer is: 1
the classifier came back with: 1, the real answer is: 1
...
the classifier came back with: 3, the real answer is: 3
the classifier came back with: 3, the real answer is: 3
the classifier came back with: 2, the real answer is: 2
the classifier came back with: 1, the real answer is: 1
the classifier came back with: 3, the real answer is: 1
the total error rate is: 0.050000
錯誤率僅為5%左右,基本上可以正確的分類。
使用演算法:構建完整可用的系統
def classifyPerson():
resultList = ["not at all", "in small doses", "in large doses"]
percentTats = float(input("monthly income?"))
ffMiles = float(input("level of appearance?"))
iceCream = float(input("running miles per month?"))
datingDataMat, datingLabels = file2matrix("datingTestSet2.txt") # load data setfrom file
normMat, ranges, minVals = autoNorm(datingDataMat)
inArr = array([ffMiles, percentTats, iceCream])
classifierResult = classify0(inArr, datingDataMat, datingLabels, 3)
print("You will probably like this person:",resultList[classifierResult-1])
海倫可以將她要約會的物件資訊輸入程式,程式會給出她對對方的喜歡誠度的預測值。例如輸入一個月收入為20000、顏值為5、每週運動量為1公里的資料,得到的結果是:
monthly income?20000
level of appearance?5
running miles per month?1
You will probably like this person: in small doses
手寫識別系統
為了簡單起見,這裡只識別數字0-9。資料集分為訓練集和測試集分別存放在兩個資料夾下。
準備資料:將影象轉換為測試向量
和之前一個例子不一樣的地方在於資料的處理上。我們必須將影象格式處理為一個向量。我們將32x32的二進位制影象矩陣轉換為1x1024的向量。
編寫函式img2vector
,將影象轉換為向量。
def img2vector(filename):
returnVector = zeros((1,1024))
fr = open(filename)
for i in range(32):
lineStr = fr.readline()
for j in range(32):
returnVector[0,32*i+j] = int(lineStr[j])
return returnVector
測試演算法:使用k-近鄰演算法識別手寫數字
def handwritingClassTest():
hwLabels = []
trainingFileList = listdir("trainingDigits")
mTrain = len(trainingFileList)
trainingMat = zeros((mTrain,1024))
for i in range(mTrain):
filenameStr = trainingFileList[i]
fileStr = filenameStr.split('.')[0]
classNum = int(fileStr.split('_')[0])
hwLabels.append(classNum)
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]
classNum = int(fileStr.split('_')[0])
testVector = img2vector("testDigits/%s"%filenameStr)
classifierResult = classify0(testVector, trainingMat, hwLabels, 4)
print("the classifier came back with: %d, the real answer is: %d" %(classifierResult, classNum))
if(classifierResult != classNum):
errorCount += 1.0
print("\nthe total number of errors is: %d" % errorCount)
print("\nthe total error rate is: %f" % (errorCount / float(mTest)))
得到結果如下:
the classifier came back with: 0, the real answer is: 0
the classifier came back with: 0, the real answer is: 0
the classifier came back with: 0, the real answer is: 0
the classifier came back with: 0, the real answer is: 0
the classifier came back with: 0, the real answer is: 0
...
the classifier came back with: 9, the real answer is: 9
the classifier came back with: 9, the real answer is: 9
the classifier came back with: 9, the real answer is: 9
the classifier came back with: 9, the real answer is: 9
the total number of errors is: 11
the total error rate is: 0.011628
小結
k-近鄰演算法是分類資料最簡單最有效的演算法。k-近鄰是基於例項的學習,使用演算法時必須有大量接近實際資料的訓練樣本資料。k-近鄰演算法必須儲存全部資料集,如果訓練的資料集很大,必須使用大量的儲存空間。此外,由於必須對資料集中的每個資料計算距離值,實際使用時可能非常耗時。
附錄
文中程式碼及資料集:https://github.com/Professorchen/Machine-Learning/tree/master/kNN