《機器學習實戰》讀書筆記——k-近鄰算法

1 k-近鄰算法概述

  • k-近鄰算法採用測量不同特徵值之間的距離方法進行分類。
  • 優點:精度高、對異常值不敏感、無數據輸入假定
  • 缺點:計算複雜度高、空間複雜度高
  • 適用數據範圍:數值型和標稱型
1.1 k-近鄰算法工作原理:

存在一個樣本數據集合(訓練集),並且樣本集中每個數據都有對應的標籤。在輸入一個沒有標籤的新數據時,將這個新數據的每個特徵與樣本集中數據對應的特徵進行比較,然後從樣本集中提取出與新數據特徵最相似的前k個數據的標籤。在這k個標籤中,出現次數最多的標籤,就認爲是新數據的標籤。

上面說的最相似,表現在可視化中就意味着最近鄰。比如判斷電影是愛情片還是動作片的二分類問題,在打鬥和接吻鏡頭的比例圖中,我們分別標出了樣本集位於圖中的位置,問號表示未知電影的位置:
在這裏插入圖片描述
在這裏插入圖片描述
然後通過數學方法計算出每個樣本點與輸入數據的歐式距離:

計算兩個向量點xAx_AxBx_B之間的距離:
d=(xA0xB0)2+(xA1xB1)2 d=\sqrt{\left(x_{A_{0}}-x_{B_{0}}\right)^{2}+\left(x_{A_{1}}-x_{B_{1}}\right)^{2}}
例如,點(0, 0)與(1, 2)之間的距離計算爲:
(10)2+(20)2 \sqrt{(1-0)^{2}+(2-0)^{2}}
如果數據集存在4個特徵值,則點(1, 0, 0, 1)與(7, 6, 9, 4)之間的距離計算爲:
(71)2+(60)2+(90)2+(41)2 \sqrt{(7-1)^{2}+(6-0)^{2}+(9-0)^{2}+(4-1)^{2}}

在這裏插入圖片描述
利用k-近鄰算法,假設k爲3,那麼與新數據最近鄰(最相似)的3個電影爲He's Not Really into DudesBeautiful WomanCalifornia 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之間的距離,計算方法如下:

(067)2+(2000032000)2+(1.10.1)2 \sqrt{(0-67)^{2}+(20000-32000)^{2}+(1.1-0.1)^{2}}

可以看到上式的結果主要會被裏程數的差值所決定,而三個特徵實際上應該同等重要,造成的原因僅僅是因爲這個特徵的數值大,這樣就會使結果不準確。

爲了避免這種情況的影響,我們需要將數值歸一化,如將取值範圍都處理到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-近鄰算法的另一個缺陷是它無法給出任何數據的基礎結構信息,因此我們也無法知曉平均
實例樣本和典型實例樣本具有什麼特徵。

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