我的朋友海倫一直使用在線約會網站尋找適合自己的約會對象。儘管約會網站會推薦不同的人選,但她沒有從中找到喜歡的人。經過一番總結,她發現曾交往過三種類型的人:
- 不喜歡的人
- 魅力一般的人
- 極具魅力的人
儘管發現了上述規律,但海倫依然無法將約會網站推薦的匹配對像歸入恰當的類別。她覺得可以在週一到週五約會那些魅力一般的人,而週末則更喜歡與那些極具魅力的人為伴。海倫希望我們的分類軟件可以更好地幫助她將匹配對像劃分到確切的分類中。此外海倫還收集了一些約會網站未曾記錄的數據信息,她認為這些數據更有助於匹配對象的歸類。
示例:在約會網站上使用k近鄰算法
- 收集數據:提供文本文件。
- 準備數據:使用Python解析文本文件。
- 分析數據:使用Matplotlib畫二維擴散圖。
- 訓練算法:此步驟不適用於k近鄰算法。
- 測試算法:使用海倫提供的部分數據作為測試樣本。 測試樣本和非測試樣本的區別在於:測試樣本是已經完成分類的數據,如果預測分類與實際類別不同,則標記為一個錯誤。
- 使用算法:產生簡單的命令行程序,然後海倫可以輸入一些特徵數據以判斷對方是否為自己喜歡的類型。
2.2.1 準備數據:從文本文件中解析數據
海倫收集約會數據已經有了一段時間,她把這些數據存放在文本文件datingTestSet.txt中,每個樣本數據佔據一行,總共有1000行。海倫的樣本主要包含以下3種特徵:
- 每年獲得的飛行常客里程數
- 玩視頻遊戲所耗時間百分比
- 每週消費的冰琪淋公升數
在將上述特徵數據輸入到分類器之前,必須將待處理數據的格式改變為分類器可以接受的格式。在kNN.py中創建名為file2matrix
的函數,以此來處理輸入格式問題。該函數的輸入為文件名字符串,輸出為訓練樣本矩陣和類標籤向量。
將下面的代碼增加到kNN.py中。
程序清單2-2 將文本記錄轉換到NumPy的解析程序
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\')
returnMat[index,:] = listFromLine[0:3]
classLabelVector.append(int(listFromLine[-1]))
index += 1
return returnMat,classLabelVector
從上面的代碼可以看到,Python處理文本文件非常容易。首先我們需要知道文本文件包含多少行。打開文件,得到文件的行數❶。然後創建以零填充的矩陣NumPy❷(實際上,NumPy是一個二維數組,這裡暫時不用考慮其用途)。為了簡化處理,我們將該矩陣的另一維度設置為固定值3
,你可以按照自己的實際需求增加相應的代碼以適應變化的輸入值。循環處理文件中的每行數據❸,首先使用函數line.strip
截取掉所有的回車字符,然後使用tab字符t
將上一步得到的整行數據分割成一個元素列表。接著,我們選取前3個元素,將它們存儲到特徵矩陣中。Python語言可以使用索引值-1表示列表中的最後一列元素,利用這種負索引,我們可以很方便地將列表的最後一列存儲到向量classLabelVector
中。需要注意的是,我們必須明確地通知解釋器,告訴它列表中存儲的元素值為整型,否則Python語言會將這些元素當作字符串處理。以前我們必須自己處理這些變量值類型問題,現在這些細節問題完全可以交給NumPy函數庫來處理。
在Python命令提示符下輸入下面命令:
>>> reload(kNN)
>>> datingDataMat, datingLabels = kNN.file2matrix(\'datingTestSet2.txt\')
使用函數file2matrix
讀取文件數據,必須確保文件datingTestSet.txt存儲在我們的工作目錄中。此外在執行這個函數之前,我們重新加載了kNN.py模塊,以確保更新的內容可以生效,否則Python將繼續使用上次加載的kNN模塊。
成功導入datingTestSet.txt文件中的數據之後,可以簡單檢查一下數據內容。Python的輸出結果大致如下:
>>> datingDataMat
array([[ 7.29170000e+04, 7.10627300e+00, 2.23600000e-01],
[ 1.42830000e+04, 2.44186700e+00, 1.90838000e-01],
[ 7.34750000e+04, 8.31018900e+00, 8.52795000e-01],
...,
[ 1.24290000e+04, 4.43233100e+00, 9.24649000e-01],
[ 2.52880000e+04, 1.31899030e+01, 1.05013800e+00],
[ 4.91800000e+03, 3.01112400e+00, 1.90663000e-01]])
>>> datingLabels[0:20]
[3, 2, 1, 1, 1, 1, 3, 3, 1, 3, 1, 1, 2, 1, 1, 1, 1, 1, 2, 3]
現在已經從文本文件中導入了數據,並將其格式化為想要的格式,接著我們需要瞭解數據的真實含義。當然我們可以直接瀏覽文本文件,但是這種方法非常不友好,一般來說,我們會採用圖形化的方式直觀地展示數據。下面就用Python工具來圖形化展示數據內容,以便辨識出一些數據模式。
NumPy數組和Python數組
本書將大量使用NumPy數組,你既可以直接在Python命令行環境中輸入
from numpy import array
將其導入,也可以通過直接導入所有NumPy庫內容來將其導入。由於NumPy庫提供的數組操作並不支持Python自帶的數組類型,因此在編寫代碼時要注意不要使用錯誤的數組類型。
2.2.2 分析數據:使用Matplotlib創建散點圖
首先我們使用Matplotlib製作原始數據的散點圖,在Python命令行環境中,輸入下列命令:
>>> import matplotlib
>>> import matplotlib.pyplot as plt
>>> fig = plt.figure
>>> ax = fig.add_subplot(111)
>>> ax.scatter(datingDataMat[:,1], datingDataMat[:,2])
>>> plt.show
輸出效果如圖2-3所示。散點圖使用datingDataMat矩陣的第二、第三列數據,分別表示特徵值「玩視頻遊戲所耗時間百分比」和「每週所消費的冰淇淋公升數」。
圖2-3 沒有樣本類別標籤的約會數據散點圖。難以辨識圖中的點究竟屬於哪個樣本分類
由於沒有使用樣本分類的特徵值,我們很難從圖2-3中看到任何有用的數據模式信息。一般來說,我們會採用色彩或其他的記號來標記不同樣本分類,以便更好地理解數據信息。Matplotlib庫提供的scatter
函數支持個性化標記散點圖上的點。重新輸入上面的代碼,調用scatter
函數時使用下列參數:
>>> ax.scatter(datingDataMat[:,1], datingDataMat[:,2],
15.0*array(datingLabels), 15.0*array(datingLabels))
上述代碼利用變量datingLabels存儲的類標籤屬性,在散點圖上繪製了色彩不等、尺寸不同的點。你可以看到一個與圖2-3類似的散點圖。從圖2-3中,我們很難看到任何有用的信息,然而由於圖2-4利用顏色及尺寸標識了數據點的屬性類別,因而我們基本上可以從圖2-4中看到數據點所屬三個樣本分類的區域輪廓。
圖2-4 帶有樣本分類標籤的約會數據散點圖。雖然能夠比較容易地區分數據點從屬類別,但依然很難根據這張圖得出結論性信息
本節我們學習了如何使用Matplotlib庫圖形化展示數據,圖2-4使用了datingDataMat矩陣的第二和第三列屬性來展示數據,雖然也可以區分,但圖2-5採用矩陣第一和第二列屬性卻可以得到更好的展示效果,圖中清晰地標識了三個不同的樣本分類區域,具有不同愛好的人其類別區域也不同。
圖2-5 每年贏得的飛行常客里程數與玩視頻遊戲所佔百分比的約會數據散點圖。約會數據有三個特徵,通過圖中展示的兩個特徵更容易區分數據點從屬的類別
2.2.3 準備數據:歸一化數值
表2-3給出了提取的四組數據,如果想要計算樣本3和樣本4之間的距離,可以使用下面的方法:
我們很容易發現,上面方程中數字差值最大的屬性對計算結果的影響最大,也就是說,每年獲取的飛行常客里程數對於計算結果的影響將遠遠大於表2-3中其他兩個特徵——玩視頻遊戲的和每週消費冰淇淋公升數——的影響。而產生這種現象的唯一原因,僅僅是因為飛行常客里程數遠大於其他特徵值。但海倫認為這三種特徵是同等重要的,因此作為三個等權重的特徵之一,飛行常客里程數並不應該如此嚴重地影響到計算結果。
表2-3 約會網站原始數據改進之後的樣本數據
在處理這種不同取值範圍的特徵值時,我們通常採用的方法是將數值歸一化,如將取值範圍處理為0到1或者-1到1之間。下面的公式可以將任意取值範圍的特徵值轉化為0到1區間內的值:
newValue = (oldValue-min)/(max-min)
其中min
和max
分別是數據集中的最小特徵值和最大特徵值。雖然改變數值取值範圍增加了分類器的複雜度,但為了得到準確結果,我們必須這樣做。我們需要在文件kNN.py中增加一個新函數autoNorm
,該函數可以自動將數字特徵值轉化為0到1的區間。
程序清單2-3 提供了函數autoNorm
的代碼。
程序清單2-3 歸一化特徵值
def autoNorm(dataSet):
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
在函數autoNorm
中,我們將每列的最小值放在變量minVals
中,將最大值放在變量maxVals
中,其中dataSet.min(0)
中的參數0使得函數可以從列中選取最小值,而不是選取當前行的最小值。然後,函數計算可能的取值範圍,並創建新的返回矩陣。正如前面給出的公式,為了歸一化特徵值,我們必須使用當前值減去最小值,然後除以取值範圍。需要注意的是,特徵值矩陣有1000 x 3個值,而minVals
和range
的值都為1 x 3。為了解決這個問題,我們使用NumPy庫中tile
函數將變量內容複製成輸入矩陣同樣大小的矩陣,注意這是具體特徵值相除❶,而對於某些數值處理軟件包,/
可能意味著矩陣除法,但在NumPy庫中,矩陣除法需要使用函數linalg.solve(matA,matB)
。
在Python命令提示符下,重新加載kNN.py模塊,執行autoNorm
函數,檢測函數的執行結果:
>>> reload(kNN)
>>> normMat, ranges, minVals = kNN.autoNorm(datingDataMat)
>>> normMat
array([[ 0.33060119, 0.58918886, 0.69043973],
[ 0.49199139, 0.50262471, 0.13468257],
[ 0.34858782, 0.68886842, 0.59540619],
...,
[ 0.93077422, 0.52696233, 0.58885466],
[ 0.76626481, 0.44109859, 0.88192528],
[ 0.0975718 , 0.02096883, 0.02443895]])
>>> ranges
array([ 8.78430000e+04, 2.02823930e+01, 1.69197100e+00])
>>> minVals
array([ 0. , 0. , 0.001818])
這裡我們也可以只返回normMat
矩陣,但是下一節我們將需要取值範圍和最小值歸一化測試數據。
2.2.4 測試算法:作為完整程序驗證分類器
上節我們已經將數據按照需求做了處理,本節我們將測試分類器的效果,如果分類器的正確率滿足要求,海倫就可以使用這個軟件來處理約會網站提供的約會名單了。機器學習算法一個很重要的工作就是評估算法的正確率,通常我們只提供已有數據的90%作為訓練樣本來訓練分類器,而使用其餘的10%數據去測試分類器,檢測分類器的正確率。本書後續章節還會介紹一些高級方法完成同樣的任務,這裡我們還是採用最原始的做法。需要注意的是,10%的測試數據應該是隨機選擇的,由於海倫提供的數據並沒有按照特定目的來排序,所以我們可以隨意選擇10%數據而不影響其隨機性。
前面我們已經提到可以使用錯誤率來檢測分類器的性能。對於分類器來說,錯誤率就是分類器給出錯誤結果的次數除以測試數據的總數,完美分類器的錯誤率為0,而錯誤率為1.0的分類器不會給出任何正確的分類結果。代碼裡我們定義一個計數器變量,每次分類器錯誤地分類數據,計數器就加1,程序執行完成之後計數器的結果除以數據點總數即是錯誤率。
為了測試分類器效果,在kNN.py文件中創建函數datingClassTest
,該函數是自包含的,你可以在任何時候在Python運行環境中使用該函數測試分類器效果。在kNN.py文件中輸入下面的程序代碼。
程序清單2-4 分類器針對約會網站的測試代碼
def datingClassTest:
hoRatio = 0.10
datingDataMat,datingLabels = file2matrix(\'datingTestSet.txt\')
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))
函數datingClassTest
如程序清單2.4所示,它首先使用了file2matrix
和autoNorm
函數從文件中讀取數據並將其轉換為歸一化特徵值。接著計算測試向量的數量,此步決定了normMat
向量中哪些數據用於測試,哪些數據用於分類器的訓練樣本;然後將這兩部分數據輸入到原始kNN分類器函數classify0
。最後,函數計算錯誤率並輸出結果。注意此處我們使用原始分類器,本章花費了大量的篇幅在講解如何處理數據,如何將數據改造為分類器可以使用的特徵值。得到可靠的數據同樣重要,本書後續的章節將介紹這個主題。
在Python命令提示符下重新加載kNN模塊,並輸入kNN.datingClassTest
,執行分類器測試程序,我們將得到下面的輸出結果:
>>> kNN.datingClassTest
the classifier came back with: 1, the real answer is: 1
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: 2, the real answer is: 2
the classifier came back with: 3, the real answer is: 3
the classifier came back with: 3, the real answer is: 1
the classifier came back with: 2, the real answer is: 2
the total error rate is: 0.024000
分類器處理約會數據集的錯誤率是2.4%,這是一個相當不錯的結果。我們可以改變函數datingClassTest
內變量hoRatio
和變量k
的值,檢測錯誤率是否隨著變量值的變化而增加。依賴於分類算法、數據集和程序設置,分類器的輸出結果可能有很大的不同。
這個例子表明我們可以正確地預測分類,錯誤率僅僅是2.4%。海倫完全可以輸入未知對象的屬性信息,由分類軟件來幫助她判定某一對象的可交往程度:討厭、一般喜歡、非常喜歡。
2.2.5 使用算法:構建完整可用系統
上面我們已經在數據上對分類器進行了測試,現在終於可以使用這個分類器為海倫來對人們分類。我們會給海倫一小段程序,通過該程序海倫會在約會網站上找到某個人並輸入他的信息。程序會給出她對對方喜歡程度的預測值。
將下列代碼加入到kNN.py並重新載入kNN。
程序清單2-5 約會網站預測函數
def classifyPerson:
resultList = [\'not at all\',\'in small doses\', \'in large doses\']
percentTats = float(raw_input(
\"percentage of time spent playing video games?\"))
ffMiles = float(raw_input(\"frequent flier miles earned per year?\"))
iceCream = float(raw_input(\"liters of ice cream consumed per year?\"))
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]
上述程序清單中的大部分代碼我們在前面都見過。唯一新加入的代碼是函數raw_input
。該函數允許用戶輸入文本行命令並返回用戶所輸入的內容。為瞭解程序的實際運行效果,輸入如下命令:
>>> kNN.classifyPerson
percentage of time spent playing video games?10
frequent flier miles earned per year?10000
liters of ice cream consumed per year?0.5
You will probably like this person: in small doses
目前為止,我們已經看到如何在數據上構建分類器。這裡所有的數據讓人看起來都很容易,但是如何在人不太容易看懂的數據上使用分類器呢?從下一節的例子中,我們會看到如何在二進制存儲的圖像數據上使用kNN。