《機器學習實戰》學習筆記(一): k-近鄰算法(手寫識別系統實戰)
一、k-近鄰法簡介
k近鄰法(k-nearest neighbor, k-NN)是1967年由Cover T和Hart P提出的一種基本分類與迴歸方法。它的工作原理是:存在一個樣本數據集合,
也稱作爲訓練樣本集,並且樣本集中每個數據都存在標籤,即我們知道樣本集中每一個數據與所屬分類的對應關係。輸入沒有標籤的新數據後,
將新的數據的每個特徵與樣本集中數據對應的特徵進行比較,然後算法提取樣本最相似數據(最近鄰)的分類標籤。一般來說,我們只選擇樣本數據集
中前k個最相似的數據,這就是k-近鄰算法中k的出處,通常k是不大於20的整數。最後,選擇k個最相似數據中出現次數最多的分類,作爲新數據的分類。
說白了,就是將新數據和訓練集中每個數據進行比較,找到距離新數據最近的K個數據,對這K個數據的標籤進行統計,支持數最高的標籤即可認爲
是新數據的標籤。比如K=10,其中有9的標籤是A類,1個的標籤是B類,那麼這個新數據的標籤被認爲是A。
二、手寫識別系統問題
1:背景知識
對於需要識別的數字已經使用圖形處理軟件,處理成具有相同的色彩和大小:寬高是32像素x32像素。儘管採用本文格式存儲圖像不能有效地利用內存空間,但是爲了方便理解,我們將圖片轉換爲文本格式,數字的文本格式如圖所示。
與此同時,這些文本格式存儲的數字的文件命名也很有特點,格式爲:數字的值_該數字的樣本序號
對於這樣已經整理好的文本,我們可以直接使用Python處理,進行數字預測。數據集分爲訓練集和測試集,使用K-近鄰的方法,
自己設計k-近鄰算法分類器,可以實現分類。數據集和實現代碼下載地址:https://github.com/miraitowa/Machine-Learning/tree/master/KNN/3.數字識別
2、sklearn簡介
Scikit learn 也簡稱sklearn,是機器學習領域當中最知名的python模塊之一。sklearn包含了很多機器學習的方式:
1. Classification 分類 1. Regression 迴歸 1. Clustering 非監督分類 1. Dimensionality reduction 數據降維 1. Model Selection 模型選擇 1. Preprocessing 數據與處理
使用sklearn可以很方便地讓我們實現一個機器學習算法。一個複雜度算法的實現,使用sklearn可能只需要調用幾行API即可。所以學習sklearn,可以有效減少我們特定任務的實現週期。
3、sklearn安裝
由於我使用Anaconda安裝,推薦Anaconda,因爲裏面已經內置了NumPy,SciPy等常用工具
conda install scikit-learn
具體安裝教程以及步驟可以查閱:http://blackblog.tech/2018/02/05/十分鐘上手sklearn-1/
KNneighborsClassifier參數說明:
n_neighbors:默認爲5,就是k-NN的k的值,選取最近的k個點。
weights:默認是uniform,參數可以是uniform、distance,也可以是用戶自己定義的函數。uniform是均等的權重,就說所有的鄰近點的權重都是相等的。distance是不均等的權重,距離近的點比距離遠的點的影響大。用戶自定義的函數,接收距離的數組,返回一組維數相同的權重。
algorithm:快速k近鄰搜索算法,默認參數爲auto,可以理解爲算法自己決定合適的搜索算法。除此之外,用戶也可以自己指定搜索算法ball_tree、kd_tree、brute方法進行搜索,brute是蠻力搜索,也就是線性掃描,當訓練集很大時,計算非常耗時。kd_tree,構造kd樹存儲數據以便對其進行快速檢索的樹形數據結構,kd樹也就是數據結構中的二叉樹。以中值切分構造的樹,每個結點是一個超矩形,在維數小於20時效率高。ball tree是爲了克服kd樹高緯失效而發明的,其構造過程是以質心C和半徑r分割樣本空間,每個節點是一個超球體。
leaf_size:默認是30,這個是構造的kd樹和ball樹的大小。這個值的設置會影響樹構建的速度和搜索速度,同樣也影響着存儲樹所需的內存大小。需要根據問題的性質選擇最優的大小。
metric:用於距離度量,默認度量是minkowski,也就是p=2的歐氏距離(歐幾里德度量)。
p:距離度量公式。在上小結,我們使用歐氏距離公式進行距離度量。除此之外,還有其他的度量方法,例如曼哈頓距離。這個參數默認爲2,也就是默認使用歐式距離公式進行距離度量。也可以設置爲1,使用曼哈頓距離公式進行距離度量。
metric_params:距離公式的其他關鍵參數,這個可以不管,使用默認的None即可。
n_jobs:並行處理設置。默認爲1,臨近點搜索並行工作數。如果爲-1,那麼CPU的所有cores都用於並行工作
sklearn小試牛刀
我們知道數字圖片是32x32的二進制圖像,爲了方便計算,我們可以將32x32的二進制圖像轉換爲1x1024的向量。對於sklearn的KNeighborsClassifier輸入可以是矩陣,不用一定轉換爲向量,不過爲了跟自己寫的k-近鄰算法分類器對應上,這裏也做了向量化處理。
然後構建kNN分類器,利用分類器做預測
# -*- coding: UTF-8 -*-
import numpy as np
import operator
from os import listdir
from sklearn.neighbors import KNeighborsClassifier as kNN
"""
函數說明:將32x32的二進制圖像轉換爲1x1024向量。
Parameters:
filename - 文件名
Returns:
returnVect - 返回的二進制圖像的1x1024向量
"""
def img2vector(filename):
#創建1x1024零向量
returnVect = np.zeros((1, 1024))
#打開文件
fr = open(filename)
#按行讀取
for i in range(32):
#讀一行數據
lineStr = fr.readline()
#每一行的前32個元素依次添加到returnVect中
for j in range(32):
returnVect[0, 32*i+j] = int(lineStr[j])
#返回轉換後的1x1024向量
return returnVect
"""
函數說明:手寫數字分類測試
Parameters:
無
Returns:
無
"""
def handwritingClassTest():
#測試集的Labels
hwLabels = []
#返回trainingDigits目錄下的文件名
trainingFileList = listdir('trainingDigits')
#返回文件夾下文件的個數
m = len(trainingFileList)
#初始化訓練的Mat矩陣,測試集
trainingMat = np.zeros((m, 1024))
#從文件名中解析出訓練集的類別
for i in range(m):
#獲得文件的名字
fileNameStr = trainingFileList[i]
#獲得分類的數字
classNumber = int(fileNameStr.split('_')[0])
#將獲得的類別添加到hwLabels中
hwLabels.append(classNumber)
#將每一個文件的1x1024數據存儲到trainingMat矩陣中
trainingMat[i,:] = img2vector('trainingDigits/%s' % (fileNameStr))
#構建kNN分類器
neigh = kNN(n_neighbors = 3, algorithm = 'auto')
#擬合模型, trainingMat爲測試矩陣,hwLabels爲對應的標籤
neigh.fit(trainingMat, hwLabels)
#返回testDigits目錄下的文件列表
testFileList = listdir('testDigits')
#錯誤檢測計數
errorCount = 0.0
#測試數據的數量
mTest = len(testFileList)
#從文件中解析出測試集的類別並進行分類測試
for i in range(mTest):
#獲得文件的名字
fileNameStr = testFileList[i]
#獲得分類的數字
classNumber = int(fileNameStr.split('_')[0])
#獲得測試集的1x1024向量,用於訓練
vectorUnderTest = img2vector('testDigits/%s' % (fileNameStr))
#獲得預測結果
# classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3)
classifierResult = neigh.predict(vectorUnderTest)
print("分類返回結果爲%d\t真實結果爲%d" % (classifierResult, classNumber))
if(classifierResult != classNumber):
errorCount += 1.0
print("總共錯了%d個數據\n錯誤率爲%f%%" % (errorCount, errorCount/mTest * 100))
"""
函數說明:main函數
Parameters:
無
Returns:
無
"""
if __name__ == '__main__':
handwritingClassTest()
上述代碼使用的algorithm參數是auto,更改algorithm參數爲brute,使用暴力搜索,你會發現,運行時間變長了,變爲10s+。
更改n_neighbors參數,你會發現,不同的值,檢測精度也是不同的。自己可以嘗試更改這些參數的設置,加深對其函數的理解。
而進行簡單的優化處理就會發現
# -*- coding:utf-8 -*-
from numpy import *
import operator
from os import listdir
import matplotlib.pyplot as plt
from sklearn.neighbors import KNeighborsClassifier as kNN
"""函數說明:將32x32的二進制圖像轉換爲1x1024向量。"""
def img2vector(filename):
#創建1x1024零向量
returnVect = np.zeros((1,1024))
#打開文件
fr = open(filename)
#按行讀取
for i in range(32):
#讀一行數據
lineStr = fr.readline()
#每一行的前32個元素依次添加到returnVect中
for j in range(32):
returnVect[0,32*i+j] = int(lineStr[j])
#返回轉換後的1x1024向量
return returnVect
"""函數說明:手寫數字分類測試"""
def handwritingClassTest():
#測試集的Labels
hwLabels = []
#返回trainingDigits目錄下的文件名
trainingFileList = listdir('trainingDigits')
#返回文件夾下文件的個數
m = len(trainingFileList)
#初始化訓練的Mat矩陣,測試集
trainingMat = np.zeros((m,1024))
#從文件名中解析出訓練集的類別
for i in range(m):
#獲得文件的名字
fileNameStr = trainingFileList[i]
#獲得分類的數字
fileStr = fileNameStr.split('.')[0]
classNumStr = int(fileStr.split('_')[0])
#將獲得的類別添加到hwLabels中
hwLabels.append(classNumStr)
#將每一個文件的1x1024數據存儲到trainingMat矩陣中
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]
classNumStr = int(fileStr.split('_')[0])
vectorUnderTest = img2vector('testDigits/%s' % fileNameStr)
classifierResult = classify(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)))
if __name__ == '__main__':
handwritingClassTest()
四、總結
1、kNN算法的優缺點
優點
簡單好用,容易理解,精度高,理論成熟,既可以用來做分類也可以用來做迴歸;
可用於數值型數據和離散型數據;
訓練時間複雜度爲O(n);無數據輸入假定;
對異常值不敏感
缺點
計算複雜性高;空間複雜性高;
樣本不平衡問題(即有些類別的樣本數量很多,而其它樣本的數量很少);
一般數值很大的時候不用這個,計算量太大。但是單個樣本又不能太少,否則容易發生誤分。
最大的缺點是無法給出數據的內在含義。