《機器學習實戰》——kNN(k近鄰演算法)
原作者寫的太好了,包括排版都特別整齊(其中有一個錯誤之處就是在約會網站配對效果判定的時候,列表順序不對,導致結果有誤,這裡我已做出修改)
執行平臺: Windows
Python版本: Python3.x
IDE: Sublime text3
一 簡單k-近鄰演算法
本文將從k-鄰近演算法的思想開始講起,使用python3一步一步編寫程式碼進行實戰訓練。並且,我也提供了相應的資料集,對程式碼進行了詳細的註釋。除此之外,本文也對sklearn實現k-鄰近演算法的方法進行了講解。實戰例項:電影類別分類、約會網站配對效果判定、手寫數字識別。
如果對於程式碼理解不夠的,可以結合本文,觀看由南京航空航天大學碩士:深度眸,為大家免費錄製的視訊:
1.1 k-近鄰法簡介
k近鄰法(k-nearest neighbor, k-NN)是1967年由Cover T和Hart P提出的一種基本分類與迴歸方法。它的工作原理是:存在一個樣本資料集合,也稱作為訓練樣本集,並且樣本集中每個資料都存在標籤,即我們知道樣本集中每一個數據與所屬分類的對應關係。輸入沒有標籤的新資料後,將新的資料的每個特徵與樣本集中資料對應的特徵進行比較,然後演算法提取樣本最相似資料(最近鄰)的分類標籤。一般來說,我們只選擇樣本資料集中前k個最相似的資料,這就是k-近鄰演算法中k的出處,通常k是不大於20的整數。最後,選擇k個最相似資料中出現次數最多的分類,作為新資料的分類。
舉個簡單的例子,我們可以使用k-近鄰演算法分類一個電影是愛情片還是動作片。
電影名稱 | 打鬥鏡頭 | 接吻鏡頭 | 電影型別 |
---|---|---|---|
電影1 | 1 | 101 | 愛情片 |
電影2 | 5 | 89 | 愛情片 |
電影3 | 108 | 5 | 動作片 |
電影4 | 115 | 8 | 動作片 |
表1.1就是我們已有的資料集合,也就是訓練樣本集。這個資料集有兩個特徵,即打鬥鏡頭數和接吻鏡頭數。除此之外,我們也知道每個電影的所屬型別,即分類標籤。用肉眼粗略地觀察,接吻鏡頭多的,是愛情片。打鬥鏡頭多的,是動作片。以我們多年的看片經驗,這個分類還算合理。如果現在給我一部電影,你告訴我這個電影打鬥鏡頭數和接吻鏡頭數。不告訴我這個電影型別,我可以根據你給我的資訊進行判斷,這個電影是屬於愛情片還是動作片。而k-近鄰演算法也可以像我們人一樣做到這一點,不同的地方在於,我們的經驗更”牛逼”,而k-鄰近演算法是靠已有的資料。比如,你告訴我這個電影打鬥鏡頭數為2,接吻鏡頭數為102,我的經驗會告訴你這個是愛情片,k-近鄰演算法也會告訴你這個是愛情片。你又告訴我另一個電影打鬥鏡頭數為49,接吻鏡頭數為51,我”邪惡”的經驗可能會告訴你,這有可能是個”愛情動作片”,畫面太美,我不敢想象。 (如果說,你不知道”愛情動作片”是什麼?請評論留言與我聯絡,我需要你這樣像我一樣純潔的朋友。) 但是k-近鄰演算法不會告訴你這些,因為在它的眼裡,電影型別只有愛情片和動作片,它會提取樣本集中特徵最相似資料(最鄰近)的分類標籤,得到的結果可能是愛情片,也可能是動作片,但絕不會是”愛情動作片”。當然,這些取決於資料集的大小以及最近鄰的判斷標準等因素。
1.2 距離度量
我們已經知道k-近鄰演算法根據特徵比較,然後提取樣本集中特徵最相似資料(最鄰近)的分類標籤。那麼,如何進行比較呢?比如,我們還是以表1.1為例,怎麼判斷紅色圓點標記的電影所屬的類別呢?如圖1.1所示。
圖1.1 電影分類我們可以從散點圖大致推斷,這個紅色圓點標記的電影可能屬於動作片,因為距離已知的那兩個動作片的圓點更近。k-近鄰演算法用什麼方法進行判斷呢?沒錯,就是距離度量。這個電影分類的例子有2個特徵,也就是在2維實數向量空間,可以使用我們高中學過的兩點距離公式計算距離,如圖1.2所示。
圖1.2 兩點距離公式通過計算,我們可以得到如下結果:
- (101,20)->動作片(108,5)的距離約為16.55
- (101,20)->動作片(115,8)的距離約為18.44
- (101,20)->愛情片(5,89)的距離約為118.22
- (101,20)->愛情片(1,101)的距離約為128.69
通過計算可知,紅色圓點標記的電影到動作片 (108,5)的距離最近,為16.55。如果演算法直接根據這個結果,判斷該紅色圓點標記的電影為動作片,這個演算法就是最近鄰演算法,而非k-近鄰演算法。那麼k-鄰近演算法是什麼呢?k-近鄰演算法步驟如下:
- 計算已知類別資料集中的點與當前點之間的距離;
- 按照距離遞增次序排序;
- 選取與當前點距離最小的k個點;
- 確定前k個點所在類別的出現頻率;
- 返回前k個點所出現頻率最高的類別作為當前點的預測分類。
比如,現在我這個k值取3,那麼在電影例子中,按距離依次排序的三個點分別是動作片(108,5)、動作片(115,8)、愛情片(5,89)。在這三個點中,動作片出現的頻率為三分之二,愛情片出現的頻率為三分之一,所以該紅色圓點標記的電影為動作片。這個判別過程就是k-近鄰演算法。
1.3 Python3程式碼實現
我們已經知道了k-近鄰演算法的原理,那麼接下來就是使用Python3實現該演算法,依然以電影分類為例。
1.3.1 準備資料集
對於表1.1中的資料,我們可以使用numpy直接建立,程式碼如下:
# -*- coding: UTF-8 -*-
import numpy as np
"""
函式說明:建立資料集
Parameters:
無
Returns:
group - 資料集
labels - 分類標籤
Modify:
2017-07-13
"""
def createDataSet():
#四組二維特徵
group = np.array([[1,101],[5,89],[108,5],[115,8]])
#四組特徵的標籤
labels = ['愛情片','愛情片','動作片','動作片']
return group, labels
if __name__ == '__main__':
#建立資料集
group, labels = createDataSet()
#列印資料集
print(group)
print(labels)
執行結果,如圖1.3所示:圖1.3 執行結果
1.3.2 k-近鄰演算法
根據兩點距離公式,計算距離,選擇距離最小的前k個點,並返回分類結果。
# -*- coding: UTF-8 -*-
import numpy as np
import operator
"""
函式說明:kNN演算法,分類器
Parameters:
inX - 用於分類的資料(測試集)
dataSet - 用於訓練的資料(訓練集)
labes - 分類標籤
k - kNN演算法引數,選擇距離最小的k個點
Returns:
sortedClassCount[0][0] - 分類結果
Modify:
2017-07-13
"""
def classify0(inX, dataSet, labels, k):
#numpy函式shape[0]返回dataSet的行數
dataSetSize = dataSet.shape[0]
#在列向量方向上重複inX共1次(橫向),行向量方向上重複inX共dataSetSize次(縱向)
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
#二維特徵相減後平方
sqDiffMat = diffMat**2
#sum()所有元素相加,sum(0)列相加,sum(1)行相加
sqDistances = sqDiffMat.sum(axis=1)
#開方,計算出距離
distances = sqDistances**0.5
#返回distances中元素從小到大排序後的索引值
sortedDistIndices = distances.argsort()
#定一個記錄類別次數的字典
classCount = {}
for i in range(k):
#取出前k個元素的類別
voteIlabel = labels[sortedDistIndices[i]]
#dict.get(key,default=None),字典的get()方法,返回指定鍵的值,如果值不在字典中返回預設值。
#計算類別次數
classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
#python3中用items()替換python2中的iteritems()
#key=operator.itemgetter(1)根據字典的值進行排序
#key=operator.itemgetter(0)根據字典的鍵進行排序
#reverse降序排序字典
sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
#返回次數最多的類別,即所要分類的類別
return sortedClassCount[0][0]
1.3.3 整體程式碼
這裡預測紅色圓點標記的電影(101,20)的類別,K-NN的k值為3。建立kNN_test01.py檔案,編寫程式碼如下:
# -*- coding: UTF-8 -*-
import numpy as np
import operator
"""
函式說明:建立資料集
Parameters:
無
Returns:
group - 資料集
labels - 分類標籤
Modify:
2017-07-13
"""
def createDataSet():
#四組二維特徵
group = np.array([[1,101],[5,89],[108,5],[115,8]])
#四組特徵的標籤
labels = ['愛情片','愛情片','動作片','動作片']
return group, labels
"""
函式說明:kNN演算法,分類器
Parameters:
inX - 用於分類的資料(測試集)
dataSet - 用於訓練的資料(訓練集)
labes - 分類標籤
k - kNN演算法引數,選擇距離最小的k個點
Returns:
sortedClassCount[0][0] - 分類結果
Modify:
2017-07-13
"""
def classify0(inX, dataSet, labels, k):
#numpy函式shape[0]返回dataSet的行數
dataSetSize = dataSet.shape[0]
#在列向量方向上重複inX共1次(橫向),行向量方向上重複inX共dataSetSize次(縱向)
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
#二維特徵相減後平方
sqDiffMat = diffMat**2
#sum()所有元素相加,sum(0)列相加,sum(1)行相加
sqDistances = sqDiffMat.sum(axis=1)
#開方,計算出距離
distances = sqDistances**0.5
#返回distances中元素從小到大排序後的索引值
sortedDistIndices = distances.argsort()
#定一個記錄類別次數的字典
classCount = {}
for i in range(k):
#取出前k個元素的類別
voteIlabel = labels[sortedDistIndices[i]]
#dict.get(key,default=None),字典的get()方法,返回指定鍵的值,如果值不在字典中返回預設值。
#計算類別次數
classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
#python3中用items()替換python2中的iteritems()
#key=operator.itemgetter(1)根據字典的值進行排序
#key=operator.itemgetter(0)根據字典的鍵進行排序
#reverse降序排序字典
sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
#返回次數最多的類別,即所要分類的類別
return sortedClassCount[0][0]
if __name__ == '__main__':
#建立資料集
group, labels = createDataSet()
#測試集
test = [101,20]
#kNN分類
test_class = classify0(test, group, labels, 3)
#列印分類結果
print(test_class)
執行結果,如圖1.4所示:圖1.4 執行結果可以看到,分類結果根據我們的”經驗”,是正確的,儘管這種分類比較耗時,用時1.4s。
到這裡,也許有人早已經發現,電影例子中的特徵是2維的,這樣的距離度量可以用兩 點距離公式計算,但是如果是更高維的呢?對,沒錯。我們可以用歐氏距離(也稱歐幾里德度量),如圖1.5所示。我們高中所學的兩點距離公式就是歐氏距離在二維空間上的公式,也就是歐氏距離的n的值為2的情況。
圖1.5 歐氏距離公式看到這裡,有人可能會問:“分類器何種情況下會出錯?”或者“答案是否總是正確的?”答案是否定的,分類器並不會得到百分百正確的結果,我們可以使用多種方法檢測分類器的正確率。此外分類器的效能也會受到多種因素的影響,如分類器設定和資料集等。不同的演算法在不同資料集上的表現可能完全不同。為了測試分類器的效果,我們可以使用已知答案的資料,當然答案不能告訴分類器,檢驗分類器給出的結果是否符合預期結果。通過大量的測試資料,我們可以得到分類器的錯誤率-分類器給出錯誤結果的次數除以測試執行的總數。錯誤率是常用的評估方法,主要用於評估分類器在某個資料集上的執行效果。完美分類器的錯誤率為0,最差分類器的錯誤率是1.0。同時,我們也不難發現,k-近鄰演算法沒有進行資料的訓練,直接使用未知的資料與已知的資料進行比較,得到結果。因此,可以說k-鄰近演算法不具有顯式的學習過程。
二 k-近鄰演算法實戰之約會網站配對效果判定
上一小結學習了簡單的k-近鄰演算法的實現方法,但是這並不是完整的k-近鄰演算法流程,k-近鄰演算法的一般流程:
- 收集資料:可以使用爬蟲進行資料的收集,也可以使用第三方提供的免費或收費的資料。一般來講,資料放在txt文字檔案中,按照一定的格式進行儲存,便於解析及處理。
- 準備資料:使用Python解析、預處理資料。
- 分析資料:可以使用很多方法對資料進行分析,例如使用Matplotlib將資料視覺化。
- 測試演算法:計算錯誤率。
- 使用演算法:錯誤率在可接受範圍內,就可以執行k-近鄰演算法進行分類。
已經瞭解了k-近鄰演算法的一般流程,下面開始進入實戰內容。
2.1 實戰背景
海倫女士一直使用線上約會網站尋找適合自己的約會物件。儘管約會網站會推薦不同的任選,但她並不是喜歡每一個人。經過一番總結,她發現自己交往過的人可以進行如下分類:
- 不喜歡的人
- 魅力一般的人
- 極具魅力的人
海倫收集約會資料已經有了一段時間,她把這些資料存放在文字檔案datingTestSet.txt中,每個樣本資料佔據一行,總共有1000行。
海倫收集的樣本資料主要包含以下3種特徵:
- 每年獲得的飛行常客里程數
- 玩視訊遊戲所消耗時間百分比
- 每週消費的冰淇淋公升數
2.2 準備資料:資料解析
在將上述特徵資料輸入到分類器前,必須將待處理的資料的格式改變為分類器可以接收的格式。分類器接收的資料是什麼格式的?從上小結已經知道,要將資料分類兩部分,即特徵矩陣和對應的分類標籤向量。在kNN_test02.py檔案中建立名為file2matrix的函式,以此來處理輸入格式問題。 將datingTestSet.txt放到與kNN_test02.py相同目錄下,編寫程式碼如下:
# -*- coding: UTF-8 -*-
import numpy as np
"""
函式說明:開啟並解析檔案,對資料進行分類:1代表不喜歡,2代表魅力一般,3代表極具魅力
Parameters:
filename - 檔名
Returns:
returnMat - 特徵矩陣
classLabelVector - 分類Label向量
Modify:
2017-03-24
"""
def file2matrix(filename):
#開啟檔案
fr = open(filename)
#讀取檔案所有內容
arrayOLines = fr.readlines()
#得到檔案行數
numberOfLines = len(arrayOLines)
#返回的NumPy矩陣,解析完成的資料:numberOfLines行,3列
returnMat = np.zeros((numberOfLines,3))
#返回的分類標籤向量
classLabelVector = []
#行的索引值
index = 0
for line in arrayOLines:
#s.strip(rm),當rm空時,預設刪除空白符(包括'\n','\r','\t',' ')
line = line.strip()
#使用s.split(str="",num=string,cout(str))將字串根據'\t'分隔符進行切片。
listFromLine = line.split('\t')
#將資料前三列提取出來,存放到returnMat的NumPy矩陣中,也就是特徵矩陣
returnMat[index,:] = listFromLine[0:3]
#根據文字中標記的喜歡的程度進行分類,1代表不喜歡,2代表魅力一般,3代表極具魅力
if listFromLine[-1] == 'didntLike':
classLabelVector.append(1)
elif listFromLine[-1] == 'smallDoses':
classLabelVector.append(2)
elif listFromLine[-1] == 'largeDoses':
classLabelVector.append(3)
index += 1
return returnMat, classLabelVector
"""
函式說明:main函式
Parameters:
無
Returns:
無
Modify:
2017-03-24
"""
if __name__ == '__main__':
#開啟的檔名
filename = "datingTestSet.txt"
#開啟並處理資料
datingDataMat, datingLabels = file2matrix(filename)
print(datingDataMat)
print(datingLabels)
執行上述程式碼,得到的資料解析結果如圖2.2所示。圖2.2 資料解析結果可以看到,我們已經順利匯入資料,並對資料進行解析,格式化為分類器需要的資料格式。接著我們需要了解資料的真正含義。可以通過友好、直觀的圖形化的方式觀察資料。
2.3 分析資料:資料視覺化
在kNN_test02.py檔案中編寫名為showdatas的函式,用來將資料視覺化。編寫程式碼如下:
# -*- coding: UTF-8 -*-
from matplotlib.font_manager import FontProperties
import matplotlib.lines as mlines
import matplotlib.pyplot as plt
import numpy as np
"""
函式說明:開啟並解析檔案,對資料進行分類:1代表不喜歡,2代表魅力一般,3代表極具魅力
Parameters:
filename - 檔名
Returns:
returnMat - 特徵矩陣
classLabelVector - 分類Label向量
Modify:
2017-03-24
"""
def file2matrix(filename):
#開啟檔案
fr = open(filename)
#讀取檔案所有內容
arrayOLines = fr.readlines()
#得到檔案行數
numberOfLines = len(arrayOLines)
#返回的NumPy矩陣,解析完成的資料:numberOfLines行,3列
returnMat = np.zeros((numberOfLines,3))
#返回的分類標籤向量
classLabelVector = []
#行的索引值
index = 0
for line in arrayOLines:
#s.strip(rm),當rm空時,預設刪除空白符(包括'\n','\r','\t',' ')
line = line.strip()
#使用s.split(str="",num=string,cout(str))將字串根據'\t'分隔符進行切片。
listFromLine = line.split('\t')
#將資料前三列提取出來,存放到returnMat的NumPy矩陣中,也就是特徵矩陣
returnMat[index,:] = listFromLine[0:3]
#根據文字中標記的喜歡的程度進行分類,1代表不喜歡,2代表魅力一般,3代表極具魅力
if listFromLine[-1] == 'didntLike':
classLabelVector.append(1)
elif listFromLine[-1] == 'smallDoses':
classLabelVector.append(2)
elif listFromLine[-1] == 'largeDoses':
classLabelVector.append(3)
index += 1
return returnMat, classLabelVector
"""
函式說明:視覺化資料
Parameters:
datingDataMat - 特徵矩陣
datingLabels - 分類Label
Returns:
無
Modify:
2017-03-24
"""
def showdatas(datingDataMat, datingLabels):
#設定漢字格式
font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)
#將fig畫布分隔成1行1列,不共享x軸和y軸,fig畫布的大小為(13,8)
#當nrow=2,nclos=2時,代表fig畫布被分為四個區域,axs[0][0]表示第一行第一個區域
fig, axs = plt.subplots(nrows=2, ncols=2,sharex=False, sharey=False, figsize=(13,8))
numberOfLabels = len(datingLabels)
LabelsColors = []
for i in datingLabels:
if i == 1:
LabelsColors.append('black')
if i == 2:
LabelsColors.append('orange')
if i == 3:
LabelsColors.append('red')
#畫出散點圖,以datingDataMat矩陣的第一(飛行常客例程)、第二列(玩遊戲)資料畫散點資料,散點大小為15,透明度為0.5
axs[0][0].scatter(x=datingDataMat[:,0], y=datingDataMat[:,1], color=LabelsColors,s=15, alpha=.5)
#設定標題,x軸label,y軸label
axs0_title_text = axs[0][0].set_title(u'每年獲得的飛行常客里程數與玩視訊遊戲所消耗時間佔比',FontProperties=font)
axs0_xlabel_text = axs[0][0].set_xlabel(u'每年獲得的飛行常客里程數',FontProperties=font)
axs0_ylabel_text = axs[0][0].set_ylabel(u'玩視訊遊戲所消耗時間佔',FontProperties=font)
plt.setp(axs0_title_text, size=9, weight='bold', color='red')
plt.setp(axs0_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs0_ylabel_text, size=7, weight='bold', color='black')
#畫出散點圖,以datingDataMat矩陣的第一(飛行常客例程)、第三列(冰激凌)資料畫散點資料,散點大小為15,透明度為0.5
axs[0][1].scatter(x=datingDataMat[:,0], y=datingDataMat[:,2], color=LabelsColors,s=15, alpha=.5)
#設定標題,x軸label,y軸label
axs1_title_text = axs[0][1].set_title(u'每年獲得的飛行常客里程數與每週消費的冰激淋公升數',FontProperties=font)
axs1_xlabel_text = axs[0][1].set_xlabel(u'每年獲得的飛行常客里程數',FontProperties=font)
axs1_ylabel_text = axs[0][1].set_ylabel(u'每週消費的冰激淋公升數',FontProperties=font)
plt.setp(axs1_title_text, size=9, weight='bold', color='red')
plt.setp(axs1_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs1_ylabel_text, size=7, weight='bold', color='black')
#畫出散點圖,以datingDataMat矩陣的第二(玩遊戲)、第三列(冰激凌)資料畫散點資料,散點大小為15,透明度為0.5
axs[1][0].scatter(x=datingDataMat[:,1], y=datingDataMat[:,2], color=LabelsColors,s=15, alpha=.5)
#設定標題,x軸label,y軸label
axs2_title_text = axs[1][0].set_title(u'玩視訊遊戲所消耗時間佔比與每週消費的冰激淋公升數',FontProperties=font)
axs2_xlabel_text = axs[1][0].set_xlabel(u'玩視訊遊戲所消耗時間佔比',FontProperties=font)
axs2_ylabel_text = axs[1][0].set_ylabel(u'每週消費的冰激淋公升數',FontProperties=font)
plt.setp(axs2_title_text, size=9, weight='bold', color='red')
plt.setp(axs2_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs2_ylabel_text, size=7, weight='bold', color='black')
#設定圖例
didntLike = mlines.Line2D([], [], color='black', marker='.',
markersize=6, label='didntLike')
smallDoses = mlines.Line2D([], [], color='orange', marker='.',
markersize=6, label='smallDoses')
largeDoses = mlines.Line2D([], [], color='red', marker='.',
markersize=6, label='largeDoses')
#新增圖例
axs[0][0].legend(handles=[didntLike,smallDoses,largeDoses])
axs[0][1].legend(handles=[didntLike,smallDoses,largeDoses])
axs[1][0].legend(handles=[didntLike,smallDoses,largeDoses])
#顯示圖片
plt.show()
"""
函式說明:main函式
Parameters:
無
Returns:
無
Modify:
2017-03-24
"""
if __name__ == '__main__':
#開啟的檔名
filename = "datingTestSet.txt"
#開啟並處理資料
datingDataMat, datingLabels = file2matrix(filename)
showdatas(datingDataMat, datingLabels)
執行上述程式碼,可以看到視覺化結果如圖2.3所示。圖2.3 資料視覺化結果 點選檢視大圖
通過資料可以很直觀的發現數據的規律,比如以玩遊戲所消耗時間佔比與每年獲得的飛行常客里程數,只考慮這二維的特徵資訊,給我的感覺就是海倫喜歡有生活質量的男人。為什麼這麼說呢?每年獲得的飛行常客里程數表明,海倫喜歡能享受飛行常客獎勵計劃的男人,但是不能經常坐飛機,疲於奔波,滿世界飛。同時,這個男人也要玩視訊遊戲,並且佔一定時間比例。能到處飛,又能經常玩遊戲的男人是什麼樣的男人?很顯然,有生活質量,並且生活悠閒的人。我的分析,僅僅是通過視覺化的資料總結的個人看法。我想,每個人的感受應該也是不盡相同。
2.4 準備資料:資料歸一化
表2.1給出了四組樣本,如果想要計算樣本3和樣本4之間的距離,可以使用尤拉公式計算。
樣本 | 玩遊戲所耗時間百分比 | 每年獲得的飛行常用里程數 | 每週消費的冰淇淋公升數 | 樣本分類 |
---|---|---|---|---|
1 | 0.8 | 400 | 0.5 | 1 |
2 | 12 | 134000 | 0.9 | 3 |
3 | 0 | 20000 | 1.1 | 2 |
4 | 67 | 32000 | 0.1 | 2 |
計算方法如圖2.4所示。
圖2.4 計算公式我們很容易發現,上面方程中數字差值最大的屬性對計算結果的影響最大,也就是說,每年獲取的飛行常客里程數對於計算結果的影響將遠遠大於表2.1中其他兩個特徵-玩視訊遊戲所耗時間佔比和每週消費冰淇淋公斤數的影響。而產生這種現象的唯一原因,僅僅是因為飛行常客里程數遠大於其他特徵值。但海倫認為這三種特徵是同等重要的,因此作為三個等權重的特徵之一,飛行常客里程數並不應該如此嚴重地影響到計算結果。
在處理這種不同取值範圍的特徵值時,我們通常採用的方法是將數值歸一化,如將取值範圍處理為0到1或者-1到1之間。下面的公式可以將任意取值範圍的特徵值轉化為0到1區間內的值:
newValue = (oldValue - min) / (max - min)
其中min和max分別是資料集中的最小特徵值和最大特徵值。雖然改變數值取值範圍增加了分類器的複雜度,但為了得到準確結果,我們必須這樣做。在kNN_test02.py檔案中編寫名為autoNorm的函式,用該函式自動將資料歸一化。程式碼如下:
# -*- coding: UTF-8 -*-
import numpy as np
"""
函式說明:開啟並解析檔案,對資料進行分類:1代表不喜歡,2代表魅力一般,3代表極具魅力
Parameters:
filename - 檔名
Returns:
returnMat - 特徵矩陣
classLabelVector - 分類Label向量
Modify:
2017-03-24
"""
def file2matrix(filename):
#開啟檔案
fr = open(filename)
#讀取檔案所有內容
arrayOLines = fr.readlines()
#得到檔案行數
numberOfLines = len(arrayOLines)
#返回的NumPy矩陣,解析完成的資料:numberOfLines行,3列
returnMat = np.zeros((numberOfLines,3))
#返回的分類標籤向量
classLabelVector = []
#行的索引值
index = 0
for line in arrayOLines:
#s.strip(rm),當rm空時,預設刪除空白符(包括'\n','\r','\t',' ')
line = line.strip()
#使用s.split(str="",num=string,cout(str))將字串根據'\t'分隔符進行切片。
listFromLine = line.split('\t')
#將資料前三列提取出來,存放到returnMat的NumPy矩陣中,也就是特徵矩陣
returnMat[index,:] = listFromLine[0:3]
#根據文字中標記的喜歡的程度進行分類,1代表不喜歡,2代表魅力一般,3代表極具魅力
if listFromLine[-1] == 'didntLike':
classLabelVector.append(1)
elif listFromLine[-1] == 'smallDoses':
classLabelVector.append(2)
elif listFromLine[-1] == 'largeDoses':
classLabelVector.append(3)
index += 1
return returnMat, classLabelVector
"""
函式說明:對資料進行歸一化
Parameters:
dataSet - 特徵矩陣
Returns:
normDataSet - 歸一化後的特徵矩陣
ranges - 資料範圍
minVals - 資料最小值
Modify:
2017-03-24
"""
def autoNorm(dataSet):
#獲得資料的最小值
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
#最大值和最小值的範圍
ranges = maxVals - minVals
#shape(dataSet)返回dataSet的矩陣行列數
normDataSet = np.zeros(np.shape(dataSet))
#返回dataSet的行數
m = dataSet.shape[0]
#原始值減去最小值
normDataSet = dataSet - np.tile(minVals, (m, 1))
#除以最大和最小值的差,得到歸一化資料
normDataSet = normDataSet / np.tile(ranges, (m, 1))
#返回歸一化資料結果,資料範圍,最小值
return normDataSet, ranges, minVals
"""
函式說明:main函式
Parameters:
無
Returns:
無
Modify:
2017-03-24
"""
if __name__ == '__main__':
#開啟的檔名
filename = "datingTestSet.txt"
#開啟並處理資料
datingDataMat, datingLabels = file2matrix(filename)
normDataSet, ranges, minVals = autoNorm(datingDataMat)
print(normDataSet)
print(ranges)
print(minVals)
執行上述程式碼,得到結果如圖2.5所示。圖2.5 歸一化函式執行結果從圖2.5的執行結果可以看到,我們已經順利將資料歸一化了,並且求出了資料的取值範圍和資料的最小值,這兩個值是在分類的時候需要用到的,直接先求解出來,也算是對資料預處理了。
2.5 測試演算法:驗證分類器
機器學習演算法一個很重要的工作就是評估演算法的正確率,通常我們只提供已有資料的90%作為訓練樣本來訓練分類器,而使用其餘的10%資料去測試分類器,檢測分類器的正確率。需要注意的是,10%的測試資料應該是隨機選擇的,由於海倫提供的資料並沒有按照特定目的來排序,所以我麼你可以隨意選擇10%資料而不影響其隨機性。
為了測試分類器效果,在kNN_test02.py檔案中建立函式datingClassTest,編寫程式碼如下:
# -*- coding: UTF-8 -*-
import numpy as np
import operator
"""
函式說明:kNN演算法,分類器
Parameters:
inX - 用於分類的資料(測試集)
dataSet - 用於訓練的資料(訓練集)
labes - 分類標籤
k - kNN演算法引數,選擇距離最小的k個點
Returns:
sortedClassCount[0][0] - 分類結果
Modify:
2017-03-24
"""
def classify0(inX, dataSet, labels, k):
#numpy函式shape[0]返回dataSet的行數
dataSetSize = dataSet.shape[0]
#在列向量方向上重複inX共1次(橫向),行向量方向上重複inX共dataSetSize次(縱向)
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
#二維特徵相減後平方
sqDiffMat = diffMat**2
#sum()所有元素相加,sum(0)列相加,sum(1)行相加
sqDistances = sqDiffMat.sum(axis=1)
#開方,計算出距離
distances = sqDistances**0.5
#返回distances中元素從小到大排序後的索引值
sortedDistIndices = distances.argsort()
#定一個記錄類別次數的字典
classCount = {}
for i in range(k):
#取出前k個元素的類別
voteIlabel = labels[sortedDistIndices[i]]
#dict.get(key,default=None),字典的get()方法,返回指定鍵的值,如果值不在字典中返回預設值。
#計算類別次數
classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
#python3中用items()替換python2中的iteritems()
#key=operator.itemgetter(1)根據字典的值進行排序
#key=operator.itemgetter(0)根據字典的鍵進行排序
#reverse降序排序字典
sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
#返回次數最多的類別,即所要分類的類別
return sortedClassCount[0][0]
"""
函式說明:開啟並解析檔案,對資料進行分類:1代表不喜歡,2代表魅力一般,3代表極具魅力
Parameters:
filename - 檔名
Returns:
returnMat - 特徵矩陣
classLabelVector - 分類Label向量
Modify:
2017-03-24
"""
def file2matrix(filename):
#開啟檔案
fr = open(filename)
#讀取檔案所有內容
arrayOLines = fr.readlines()
#得到檔案行數
numberOfLines = len(arrayOLines)
#返回的NumPy矩陣,解析完成的資料:numberOfLines行,3列
returnMat = np.zeros((numberOfLines,3))
#返回的分類標籤向量
classLabelVector = []
#行的索引值
index = 0
for line in arrayOLines:
#s.strip(rm),當rm空時,預設刪除空白符(包括'\n','\r','\t',' ')
line = line.strip()
#使用s.split(str="",num=string,cout(str))將字串根據'\t'分隔符進行切片。
listFromLine = line.split('\t')
#將資料前三列提取出來,存放到returnMat的NumPy矩陣中,也就是特徵矩陣
returnMat[index,:] = listFromLine[0:3]
#根據文字中標記的喜歡的程度進行分類,1代表不喜歡,2代表魅力一般,3代表極具魅力
if listFromLine[-1] == 'didntLike':
classLabelVector.append(1)
elif listFromLine[-1] == 'smallDoses':
classLabelVector.append(2)
elif listFromLine[-1] == 'largeDoses':
classLabelVector.append(3)
index += 1
return returnMat, classLabelVector
"""
函式說明:對資料進行歸一化
Parameters:
dataSet - 特徵矩陣
Returns:
normDataSet - 歸一化後的特徵矩陣
ranges - 資料範圍
minVals - 資料最小值
Modify:
2017-03-24
"""
def autoNorm(dataSet):
#獲得資料的最小值
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
#最大值和最小值的範圍
ranges = maxVals - minVals
#shape(dataSet)返回dataSet的矩陣行列數
normDataSet = np.zeros(np.shape(dataSet))
#返回dataSet的行數
m = dataSet.shape[0]
#原始值減去最小值
normDataSet = dataSet - np.tile(minVals, (m, 1))
#除以最大和最小值的差,得到歸一化資料
normDataSet = normDataSet / np.tile(ranges, (m, 1))
#返回歸一化資料結果,資料範圍,最小值
return normDataSet, ranges, minVals
"""
函式說明:分類器測試函式
Parameters:
無
Returns:
normDataSet - 歸一化後的特徵矩陣
ranges - 資料範圍
minVals - 資料最小值
Modify:
2017-03-24
"""
def datingClassTest():
#開啟的檔名
filename = "datingTestSet.txt"
#將返回的特徵矩陣和分類向量分別儲存到datingDataMat和datingLabels中
datingDataMat, datingLabels = file2matrix(filename)
#取所有資料的百分之十
hoRatio = 0.10
#資料歸一化,返回歸一化後的矩陣,資料範圍,資料最小值
normMat, ranges, minVals = autoNorm(datingDataMat)
#獲得normMat的行數
m = normMat.shape[0]
#百分之十的測試資料的個數
numTestVecs = int(m * hoRatio)
#分類錯誤計數
errorCount = 0.0
for i in range(numTestVecs):
#前numTestVecs個數據作為測試集,後m-numTestVecs個數據作為訓練集
classifierResult = classify0(normMat[i,:], normMat[numTestVecs:m,:],
datingLabels[numTestVecs:m], 4)
print("分類結果:%d\t真實類別:%d" % (classifierResult, datingLabels[i]))
if classifierResult != datingLabels[i]:
errorCount += 1.0
print("錯誤率:%f%%" %(errorCount/float(numTestVecs)*100))
"""
函式說明:main函式
Parameters:
無
Returns:
無
Modify:
2017-03-24
"""
if __name__ == '__main__':
datingClassTest()
執行上述程式碼,得到結果如圖2.6所示。圖2.6 驗證分類器結果從圖2.6驗證分類器結果中可以看出,錯誤率是3%,這是一個想當不錯的結果。我們可以改變函式datingClassTest內變數hoRatio和分類器k的值,檢測錯誤率是否隨著變數值的變化而增加。依賴於分類演算法、資料集和程式設定,分類器的輸出結果可能有很大的不同。
2.6 使用演算法:構建完整可用系統
我們可以給海倫一個小段程式,通過該程式海倫會在約會網站上找到某個人並輸入他的資訊。程式會給出她對男方喜歡程度的預測值。
在kNN_test02.py檔案中建立函式classifyPerson,程式碼如下:
# -*- coding:UTF-8 -*-
from matplotlib.font_manager import FontProperties
import matplotlib.lines as mlines
import matplotlib.pyplot as plt
import numpy as np
import operator
"""
函式說明:kNN演算法,分類器------------------------約會網路配對效果判定----------------------------------------
Parameters:inX - 用於分類的資料(測試集)
dataSet - 用於訓練的資料 (訓練集)
labels - 分類標籤
k - kNN演算法引數,選擇距離最小的k個點
Returns: sortedClassCount[0][0] -分類結果
"""
# 分類
def classify0(inX, dataSet, labels, k):
# numpy函式shape[0]返回dataSet的行數
dataSetSize = dataSet.shape[0]
# 在列向量方向上重複inX共1次(橫向),行向量方向上重複inX共dataSetSize次(縱向)
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
# 二維特徵相減後平方
sqDiffMat = diffMat ** 2
# sum()所有元素相加,sum(0)列相加,sum(1)行相加
sqDistances = sqDiffMat.sum(axis=1)
# 開方,計算出距離
distances = sqDistances ** 0.5
# 返回distances中元素從小到大排序後的索引值
sortedDistIndices = distances.argsort()
# 定一個記錄類別次數的字典
classCount = {}
for i in range(k):
# 取出前k個元素的類別
voteIlabel = labels[sortedDistIndices[i]]
# dict.get(key,default=None),字典的get()方法,返回指定鍵的值,如果值不在字典中返回預設值。
# 計算類別次數
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
# python3中用items()替換python2中的iteritems()
# key=operator.itemgetter(1)根據字典的值進行排序
# key=operator.itemgetter(0)根據字典的鍵進行排序
# reverse降序排序字典
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
# 返回次數最多的類別,即所要分類的類別
return sortedClassCount[0][0]
"""
函式說明:開啟解析檔案,對資料進行分類,1代表喜歡,2代表魅力一般,3代表極具魅力
Parameters:filename - 檔名
Returns: returnMat - 矩陣特徵
classLabelVector - 分類Label向量
"""
def file2matrix(filename):
# 開啟檔案
fr = open(filename)
# 讀取檔案所有內容
arrayOLines = fr.readlines()
# 得到檔案行數
numberOfLines = len(arrayOLines)
# 返回的NumPy矩陣,解析完成的資料:numberOfLines行,3列
returnMat = np.zeros((numberOfLines, 3))
# 返回的分類標籤向量
classLabelVector = []
# 行的索引值
index = 0
for line in arrayOLines:
# s.strip(rm),當rm空時,預設刪除空白符(包括'\n','\r','\t',' ')
line = line.strip()
# 使用s.split(str="",num=string,cout(str))將字串根據'\t'分隔符進行切片。
listFromLine = line.split('\t')
# 將資料前三列提取出來,存放到returnMat的NumPy矩陣中,也就是特徵矩陣
returnMat[index, :] = listFromLine[0:3]
# 根據文字中標記的喜歡的程度進行分類,1代表不喜歡,2代表魅力一般,3代表極具魅力
if listFromLine[-1] == 'didntLike':
classLabelVector.append(1)
elif listFromLine[-1] == 'smallDoses':
classLabelVector.append(2)
elif listFromLine[-1] == 'largeDoses':
classLabelVector.append(3)
index += 1
return returnMat, classLabelVector
"""
函式說明:視覺化資料
Parameters: datingDataMat - 特徵矩陣
datingLabels - 分類Label
Returns: 無
"""
def showdatas(datingDataMat,datingLabels):
#設定漢字格式
font = FontProperties(fname=r"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",size=14)
#將fig畫布分割成1行1列,不共享x軸和y軸,fig畫布的大小為(13,8)
#當nrow=2,nclos=2時,代表fig畫布被分為四個區域,axs[0][0]表示第一行第一個區域
fig,axs = plt.subplots(nrows=2,ncols=2,sharex=False,sharey=False,figsize=(13,8))
numberOfLabels = len(datingLabels)
LabelsColors=[]
for i in datingLabels:
if i==1:
LabelsColors.append('black')
if i==2:
LabelsColors.append('orange')
if i==3:
LabelsColors.append('red')
#畫出散點圖,以datingDataMat矩陣的第一(飛行常客例程)、第二列(玩遊戲)資料畫散點資料,散點大小為15,透明度為0.5
axs[0][0].scatter(x=datingDataMat[:,0],y=datingDataMat[:,1],color=LabelsColors,s=15,alpha=.5)
#設定標題,x軸label,y軸label
axs0_title_text=axs[0][0].set_title(u'每年獲得的飛行常客里程數與玩遊戲視訊所消耗時間佔比',FontProperties=font)
axs0_xlabel_text=axs[0][1].set_xlabel(u'每年獲得的飛行常客里程數',FontProperties=font)
axs0_ylabel_text = axs[0][1].set_ylabel(u'玩遊戲視訊所消耗時間佔比', FontProperties=font)
plt.setp(axs0_title_text,size=9, weight='bold',color='red')
plt.setp(axs0_xlabel_text,size=7, weight='bold',color='black')
plt.setp(axs0_ylabel_text,size=7, weight='bold',color='black')
# 畫出散點圖,以datingDataMat矩陣的第一(飛行常客例程)、第三列(冰激凌)資料畫散點資料,散點大小為15,透明度為0.5
axs[0][1].scatter(x=datingDataMat[:, 0], y=datingDataMat[:, 2], color=LabelsColors, s=15, alpha=.5)
# 設定標題,x軸label,y軸label
axs1_title_text = axs[0][0].set_title(u'每年獲得的飛行常客里程數與每週消費的冰激凌公升數', FontProperties=font)
axs1_xlabel_text = axs[0][1].set_xlabel(u'每年獲得的飛行常客里程數', FontProperties=font)
axs1_ylabel_text = axs[0][1].set_ylabel(u'每週消費的冰激凌公升數', FontProperties=font)
plt.setp(axs1_title_text, size=9, weight='bold', color='red')
plt.setp(axs1_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs1_ylabel_text, size=7, weight='bold', color='black')
# 畫出散點圖,以datingDataMat矩陣的第二(玩遊戲)、第三列(冰激凌)資料畫散點資料,散點大小為15,透明度為0.5
axs[1][0].scatter(x=datingDataMat[:, 0], y=datingDataMat[:, 2], color=LabelsColors, s=15, alpha=.5)
# 設定標題,x軸label,y軸label
axs2_title_text = axs[0][0].set_title(u'玩遊戲視訊所消耗時間佔比與每週消費的冰激凌公升數', FontProperties=font)
axs2_xlabel_text = axs[0][1].set_xlabel(u'玩遊戲視訊所消耗時間佔比', FontProperties=font)
axs2_ylabel_text = axs[0][1].set_ylabel(u'每週消費的冰激凌公升數', FontProperties=font)
plt.setp(axs2_title_text, size=9, weight='bold', color='red')
plt.setp(axs2_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs2_ylabel_text, size=7, weight='bold', color='black')
#設定圖例
didntLike = mlines.Line2D([],[],color='black',marker='.',markersize=6,label='didntLike')
smallDoses = mlines.Line2D([], [], color='orange', marker='.', markersize=6, label='smallDoses')
largeDoses = mlines.Line2D([], [], color='red', marker='.', markersize=6, label='largeDoses')
#新增圖例
axs[0][0].legend(handles=[didntLike,smallDoses,largeDoses])
axs[0][1].legend(handles=[didntLike, smallDoses, largeDoses])
axs[1][0].legend(handles=[didntLike, smallDoses, largeDoses])
#顯示圖片
plt.show()
"""
函式說明:對資料進行歸一化
Parameter:dataSet - 特徵矩陣
Returns: normDataSet - 歸一化後的特徵矩陣
ranges - 資料範圍
minVals - 資料最小值
"""
def autoNorm(dataSet):
#獲得資料的最小值,--min(0)表示返回的是每一列的最小值,min(1)是每一行的最小值
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
#最大值和最小值範圍
ranges = maxVals-minVals
#shape(dataSet)返回dataSet的矩陣行列數
normDataSet = np.zeros(np.shape(dataSet))
#返回dataSet的行數
m = dataSet.shape[0]
#原始值減去最小值
normDataSet = dataSet - np.tile(minVals,(m,1))
#除以最大和最小值的差,得到歸一化資料
normDataSet = normDataSet / np.tile(ranges, (m, 1))
#返回歸一化資料結果,資料範圍,最小值
return normDataSet,ranges,minVals
"""
函式說明:分類器測試函式
Parameters:無
Returns: normDataSet - 歸一化後的特徵矩陣
ranges - 資料範圍
minVals - 資料最小值
"""
def datingClassTest():
# 開啟的檔名
filename = "datingTestSet.txt"
# 將返回的特徵矩陣和分類向量分別儲存到datingDataMat和datingLabels中
datingDataMat, datingLabels = file2matrix(filename)
# 取所有資料的百分之十
hoRatio = 0.10
# 資料歸一化,返回歸一化後的矩陣,資料範圍,資料最小值
normMat, ranges, minVals = autoNorm(datingDataMat)
# 獲得normMat的行數
m = normMat.shape[0]
# 百分之十的測試資料的個數
numTestVecs = int(m * hoRatio)
# 分類錯誤計數
errorCount = 0.0
for i in range(numTestVecs):
# 前numTestVecs個數據作為測試集,後m-numTestVecs個數據作為訓練集
classifierResult = classify0(normMat[i, :], normMat[numTestVecs:m, :],
datingLabels[numTestVecs:m], 4)
print("分類結果:%d\t真實類別:%d" % (classifierResult, datingLabels[i]))
if classifierResult != datingLabels[i]:
errorCount += 1.0
print("錯誤率:%f%%" % (errorCount / float(numTestVecs) * 100))
"""
函式說明:通過輸入一個人的三維特徵,進行分類輸出
Parameters:無
Returns:
"""
def classifyPerson():
# 輸出結果
resultList = ['討厭', '有些喜歡', '非常喜歡']
# 三維特徵使用者輸入
ffMiles = float(input("每年獲得的飛行常客里程數:"))
precentTats = float(input("玩視訊遊戲所耗時間百分比:"))
iceCream = float(input("每週消費的冰激淋公升數:"))
# 開啟的檔名
filename = "datingTestSet.txt"
# 開啟並處理資料
datingDataMat, datingLabels = file2matrix(filename)
# 訓練集歸一化
normMat, ranges, minVals = autoNorm(datingDataMat)
# 生成NumPy陣列,測試集
inArr = np.array([ffMiles, precentTats, iceCream])
# 測試集歸一化
norminArr = (inArr - minVals) / ranges
# 返回分類結果
classifierResult = classify0(norminArr, normMat, datingLabels, 3)
print(classifierResult)
# 列印結果
print("你可能%s這個人" % (resultList[classifierResult-1]))
"""
函式說明:main函式
Parameters:無
returnd:無
"""
if __name__=='__main__':
#1、測試程式碼
filename="datingTestSet.txt"
#開啟並處理資料
datingDataMat,datingLabels=file2matrix(filename)
# showdatas(datingDataMat,datingLabels)
#2、測試程式碼
datingClassTest()
#3、正式程式碼
classifyPerson()
在cmd中,執行程式,並輸入資料(12,44000,0.5),預測結果是”你可能有些喜歡這個人”,也就是這個人魅力一般。一共有三個檔次:討厭、有些喜歡、非常喜歡,對應著不喜歡的人、魅力一般的人、極具魅力的人。結果如圖2.7所示。圖2.7 預測結果
三 k-近鄰演算法實戰之sklearn手寫數字識別
3.1 實戰背景
對於需要識別的數字已經使用圖形處理軟體,處理成具有相同的色彩和大小:寬高是32畫素x32畫素。儘管採用本文格式儲存影象不能有效地利用記憶體空間,但是為了方便理解,我們將圖片轉換為文字格式,數字的文字格式如圖3.1所示。
圖3.1 數字的文字格式與此同時,這些文字格式儲存的數字的檔案命名也很有特點,格式為:數字的值_該數字的樣本序號,如圖3.2所示。
圖3.2 文字數字的儲存格式對於這樣已經整理好的文字,我們可以直接使用Python處理,進行數字預測。資料集分為訓練集和測試集,使用上小結的方法,自己設計k-近鄰演算法分類器,可以實現分類。
這裡不再講解自己用Python寫的k-鄰域分類器的方法,因為這不是本小節的重點。接下來,我們將使用強大的第三方Python科學計算庫Sklearn構建手寫數字系統。
3.2 Sklearn簡介
Scikit learn 也簡稱sklearn,是機器學習領域當中最知名的python模組之一。sklearn包含了很多機器學習的方式:
- Classification 分類
- Regression 迴歸
- Clustering 非監督分類
- Dimensionality reduction 資料降維
- Model Selection 模型選擇
- Preprocessing 資料與處理
使用sklearn可以很方便地讓我們實現一個機器學習演算法。一個複雜度演算法的實現,使用sklearn可能只需要呼叫幾行API即可。所以學習sklearn,可以有效減少我們特定任務的實現週期。
3.3 Sklearn安裝
在安裝sklearn之前,需要安裝兩個庫,即numpy+mkl和scipy。不要使用pip3直接進行安裝,因為pip3默安裝的是numpy,而不是numpy+mkl。第三方庫下載地址:http://www.lfd.uci.edu/~gohlke/pythonlibs/
找到對應python版本的numpy+mkl和scipy,下載安裝即可,如圖3.1和圖3.2所示。
圖3.1 numpy+mkl圖3.2 scipy使用pip3安裝好這兩個whl檔案後,使用如下指令安裝sklearn。
pip3 install -U scikit-learn
- 1
3.4 Sklearn實現k-近鄰演算法簡介
sklearn.neighbors模組實現了k-近鄰演算法,內容如圖3.3所示。
圖3.3 sklearn.neighbors我們使用sklearn.neighbors.KNeighborsClassifier就可以是實現上小結,我們實現的k-近鄰演算法。KNeighborsClassifier函式一共有8個引數,如圖3.4所示。
圖3.4 KNeighborsClassifierKNneighborsClassifier引數說明:
- 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都用於並行工作。
KNeighborsClassifier提供了以一些方法供我們使用,如圖3.5所示。
圖3.5 KNeighborsClassifier的方法由於篇幅原因,每個函式的怎麼用,就不具體講解了。官方手冊已經講解的很詳細了,各位可以檢視這個手冊進行學習,我們直接講手寫數字識別系統的實現。
3.5 Sklearn小試牛刀
我們知道數字圖片是32x32的二進位制影象,為了方便計算,我們可以將32x32的二進位制影象轉換為1x1024的向量。對於sklearn的KNeighborsClassifier輸入可以是矩陣,不用一定轉換為向量,不過為了跟自己寫的k-近鄰演算法分類器對應上,這裡也做了向量化處理。然後構建kNN分類器,利用分類器做預測。建立kNN_test04.py檔案,編寫程式碼如下:
# -*- 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向量
Modify:
2017-07-15
"""
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:
無
Modify:
2017-07-15
"""
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:
無
Modify:
2017-07-15
"""
if __name__ == '__main__':
handwritingClassTest()
執行上述程式碼,得到如圖3.6所示的結果。圖3.6 sklearn執行結果上述程式碼使用的algorithm引數是auto,更改algorithm引數為brute,使用暴力搜尋,你會發現,執行時間變長了,變為10s+。更改n_neighbors引數,你會發現,不同的值,檢測精度也是不同的。自己可以嘗試更改這些引數的設定,加深對其函式的理解。
四 總結
4.1 kNN演算法的優缺點
優點
- 簡單好用,容易理解,精度高,理論成熟,既