[機器學習]K-近鄰算法

算法概述

  自我感覺K-近鄰(k-NearestNeighbor)算法是最簡單最易理解的分類算法了。怎麼個簡單法呢?簡單到沒有一個訓練分類器的過程,僅僅根據需要分類的樣本到已知類別的樣本之間的距離來進行分類。
  簡單來講,如果存在一個訓練樣本集,並且訓練集中每個樣本對應的類別也已知;那麼對於未知類別的新樣本,我們計算它到每個訓練樣本的距離,然後選擇距離最近的k個樣本,這k個樣本中計數最多的一類就是待分類樣本的類別。

k-NN算法的步驟如下:

  1. 根據特徵值計算待分類樣本到訓練集每個樣本的距離;
  2. 按照距離遞增次序排序;
  3. 選出與待分類樣本距離最近的前 k 個樣本;
  4. 計算前 k 個樣本中每一類別的數量;
  5. 將前 k 個樣本中計數最多的類別數作爲待分類樣本的類別。

注意

  1. 距離
    K-近鄰算法計算距離時可以使用常用的距離度量方法,一般可以選擇歐式距離,計算方式如下:
    d=i=0n(xiyi)2d=\sqrt{\sum_{i=0}^{n}\left(x_{i}-y_{i}\right)^{2}}
    xxyy 分別表示兩個樣本,nn 表示特徵維度。

  2. k的取值

    k的取值很重要,k值太小的話算法對於一些噪聲成分會比較敏感,k太大的話離待分類樣本距離較遠的點也會對分類結果產生影響,這都不是想要的結果。一般建議k取小於20的整數。

利用k-NN實現手寫數字識別

  k-NN算法的原理比較容易理解,接下來通過手寫數字識別的例子來看看怎麼實現一個k-近鄰分類器。

數據集

  這裏用到的手寫數字數據集不是MNIST數據集,而是採用文本格式存儲的數字圖像,如下圖所示。每個文本文件存儲着 323232*32 個黑白像素點,用來表示一個手寫數字。數據集中包含了2000個訓練樣本和900個測試樣本。

代碼實現

像素文本轉換爲特徵向量
  上面提到,每個樣本都是一個323232*32的二進制圖像,因此我們首先將每個樣本讀取爲一個向量的表示方式:

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-近鄰算法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章