機器學習經典分類演算法 —— k-近鄰演算法(附python實現程式碼及資料集)

閩A2436發表於2019-07-29

工作原理

存在一個樣本資料集合,也稱作訓練樣本集,並且樣本集中每個資料都存在標籤,即我們知道樣本集中每一資料與所屬分類的對應關係。輸入沒有標籤的新資料後,將新資料的每個特徵與樣本集中資料對應的特徵進行比較,然後演算法提取樣本集中特徵最相似資料(最近鄰)的分類特徵。一般來說,我們只選擇樣本資料集中前k個最相似的資料,這就是k-近鄰演算法中k的出處,通常k是不大於20的整數。最後選擇k個最相似資料中出現次數最多的分類,作為新資料的分類。

舉個例子,現在我們用k-近鄰演算法來分類一部電影,判斷它屬於愛情片還是動作片。現在已知六部電影的打鬥鏡頭、接吻鏡頭以及電影評估型別,如下圖所示。

機器學習經典分類演算法 —— k-近鄰演算法(附python實現程式碼及資料集)

現在我們有一部電影,它有18個打鬥鏡頭、90個接吻鏡頭,想知道這部電影屬於什麼型別。根據k-近鄰演算法,我們可以這麼算。首先計算未知電影與樣本集中其他電影的距離(先不管這個距離如何算,後面會提到)。現在我們得到了樣本集中所有電影與未知電影的距離。按照距離遞增排序,可以找到k個距離最近的電影。

機器學習經典分類演算法 —— k-近鄰演算法(附python實現程式碼及資料集)

現在假定k=3,則三個最靠近的電影依次是He's Not Really into DudesBeautiful WomanCalifornia Man

機器學習經典分類演算法 —— k-近鄰演算法(附python實現程式碼及資料集)

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個點。其中距離使用歐式距離,計算公式如下:

機器學習經典分類演算法 —— k-近鄰演算法(附python實現程式碼及資料集)

例如,點(0,0)與(1,2)之間的歐式距離計算為:
機器學習經典分類演算法 —— k-近鄰演算法(附python實現程式碼及資料集)

如果資料集存在4個特徵值,則點(1,0,0,1)與(7,6,9,4)之間的歐式距離計算為:
機器學習經典分類演算法 —— k-近鄰演算法(附python實現程式碼及資料集)

計算完所有點之間的距離後,可以對資料按照從小到大的次序排序。然後,確定前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()

機器學習經典分類演算法 —— k-近鄰演算法(附python實現程式碼及資料集)

準備資料:歸一化數值

因為月收入的數值和其他兩個特徵相比大很多,因此對於計算距離的影響遠大於其他兩個特徵。但是在海倫看來這是三個等權重的特徵,月收入不應該如此嚴重地影響到計算結果。

因此我們需要進行數值歸一化。採用公式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

相關文章