系列文章:《機器學習實戰》學習筆記
本章介紹了《機器學習實戰》這本書中的第一個機器學習算法:k-近鄰算法,它非常有效而且易於掌握。首先,我們將探討k-近鄰算法的基本理論,以及如何使用距離測量的方法分類物品;其次我們將使用Python從文本文件中導入並解析數據;再次,本文討論了當存在許多數據來源時,如何避免計算距離時可能碰到的一些常見錯誤;最后,利用實際的例子講解如何使用k-近鄰算法改進約會網站和手寫數字識別系統。
1. k-近鄰算法概述
簡單地說,k-近鄰算法采用測量不同特征值之間的距離方法進行分類。
k-近鄰算法
優點:精度高、對異常值不敏感、無數據輸入假定。
缺點:計算復雜度高、空間復雜度高
適用數據范圍:數值型和標稱型
k-近鄰算法(kNN)的工作原理是:存在一個樣本數據集合,也稱作訓練樣本集,並且樣本集中每個數據都存在標簽,即我們知道樣本集中每一數據與所屬分類的對應關系。輸入沒有標簽的新數據后,將新數據的每個特征與樣本集中數據對應的特征進行比較,然后算法提取樣本集中特征最相似數據(最近鄰)的分類標簽。一般來說,我們只選擇樣本數據集中前k個最相似的數據,這就是k-近鄰算法中k的出處,通常k是不大於20的整數。最后,選擇k個最相似數據中出現次數最多的分類,作為新數據的分類。
現在我們回到前面電影分類的例子,使用k-近鄰算法分類愛情片和動作片。有人曾經統計過很多電影的打斗鏡頭和接吻鏡頭,圖1顯示了6部電影的打斗和接吻鏡頭數。假如有一部未看過的電影,如何確定它是愛情片還是動作片呢?我們可以使用kNN來解決這個問題。
圖1 使用打斗和接吻鏡頭數分類電影
首先我們需要知道這個未知電影存在多少個打斗鏡頭和接吻鏡頭,圖1中問號位置是該未知電影出現的鏡頭數圖形化展示,具體數字參見下表。
表1 每部電影的打斗鏡頭數、接吻鏡頭數以及電影評估類型
電影名稱 | 打斗鏡頭 | 接吻鏡頭 | 電影類型 |
California Man | 3 | 104 | 愛情片 |
He’s Not Really into Dudes | 2 | 100 | 愛情片 |
Beautiful Woman | 1 | 81 | 愛情片 |
Kevin Longblade | 101 | 10 | 動作片 |
Robo Slayer 3000 | 99 | 5 | 動作片 |
Amped II | 98 | 2 | 動作片 |
? | 18 | 90 | 未知 |
計算未知電影與樣本集中其他電影的距離,我們可以比較其相似度:
表2 已知電影與未知電影的距離
電影名稱 | 與未知電影的距離 |
California Man | 20.5 |
He’s Not Really into Dudes | 18.7 |
Beautiful Woman | 19.2 |
Kevin Longblade | 115.3 |
Robo Slayer 3000 | 117.4 |
Amped II | 118.9 |
現在我們得到了樣本集中所有電影與未知電影的距離,按照距離遞增排序,可以找到k個距離最近的電影。假定k=3,則三個最靠近的電影依次是He’s Not Really into Dudes、Beautiful Woman和California Man。k-近鄰算法按照距離最近的三部電影的類型,決定未知電影的類型,而這三部電影全是愛情片,因此我們判定未知電影是愛情片。
k-近鄰算法的一般流程
收集數據:可以使用任何方法。
准備數據:距離計算所需要的數值,最好是結構化的數據格式。
分析數據:可以使用任何方法。
測試算法:計算錯誤率。
使用算法:首先需要輸入樣本數據和結構化的輸出結果,然后運行k-近鄰算法判定輸入數據分別屬於哪個分類。
1.1 准備:使用Python導入數據
創建名為kNN.py的Python模塊,在kNN.py文件中增加下面的代碼:
from numpy import * import operator def createDataSet(): group = array([[1.0, 1.1], [1.0, 1.0], [0, 0], [0, 0.1]]) labels = ['A', 'A', 'B', 'B'] return group, labels
這個函數創建了我們將要使用的樣例數據集。
在Python shell中輸入下列命令測試上面的函數:
>>> import kNN >>> group, labels = kNN.createDataSet()
1.2 實施kNN算法
k-近鄰算法的偽代碼
對未知類型屬性的數據集中的每個點依次執行以下操作:
(1) 計算已知類別數據集中的點與當前點之間的距離;
(2) 按照距離增序排序;
(3) 選取與當前點距離最近的k個點;
(4) 決定這k個點所屬類別的出現頻率;
(5) 返回前k個點出現頻率最高的類別作為當前點的預測分類。
函數實現如下:
def classify(inX, dataSet, labels, k): dataSetSize = dataSet.shape[0] # 數據集大小 # 計算距離 diffMat = tile(inX, (dataSetSize, 1)) - dataSet sqDiffMat = diffMat**2 sqDistances = sqDiffMat.sum(axis=1) distances = sqDistances**0.5 # 按距離排序 sortedDistIndicies = distances.argsort() # 統計前k個點所屬的類別 classCount = {} for i in range(k): votaIlabel = labels[sortedDistIndicies[i]] classCount[votaIlabel] = classCount.get(votaIlabel, 0) + 1 sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True) # 返回前k個點中頻率最高的類別 return sortedClassCount[0][0]
計算距離時直接使用了歐式距離公式,計算兩個向量點之間的距離:
$ d=\sqrt{(xA_0-xB_0)^2+(xA_1-xB_1)^2} $
計算完所有點之間的距離后,可以對數據按照從小到大的次序排序。然后,確定前k個距離最小元素所在的主要分類,輸入k總是正整數;最后,將classCount字典分解為元組列表,然后按照第二個元素的次序對元組進行排序,最后返回發生頻率最高的元素標簽。
預測數據所在分類:
>>> kNN.classify([0, 0], group, labels, 3)
輸出結果應該是B。
1.3 如何測試分類器
上文我們已經使用k-近鄰算法構造了第一個分類器,也可以檢驗分類器給出的答案是否符合我們的預期。然而分類器並不會得到百分百正確的結果,我們可以使用多種方法檢測分類器的正確率。
為了測試分類器的效果,我們可以使用已知答案的數據,當然答案不能告訴分類器,檢驗分類器給出的結果是否符合預期結果。通過大量的測試數據,我們可以得到分類器的錯誤率——分類器給出錯誤結果的次數除以測試執行的總數。錯誤率是常用的評估方法,主要用於評估分類器在某個數據集上的執行效果。完美分類器的錯誤率為0,最差分類器的錯誤率是1.0,在這種情況下,分類器根本就無法找到一個正確答案。然而錯誤率幾乎不會達到1.0,因為即使是隨機猜測,也會有一定概率猜對的。因此,錯誤率一般存在一個上限,且具體的值會與各類型之間的比例關系直接相關。
2. 示例:使用k-近鄰算法改進約會網站的配對效果
我的朋友海倫一直使用在線約會網站尋找適合自己的約會對象。盡管約會網站會推薦不同的人選,但她並不是喜歡每一個人。經過一番總結,她發現曾交往過三種類型的人:
- 不喜歡的人
- 魅力一般的人
- 極具魅力的人
海倫希望我們的分類軟件可以更好地幫助她將匹配對象划分到確切的分類中。此外海倫還收集了一些約會網站未曾記錄的數據信息,她認為這些數據更有助於匹配對象的歸類。
2.1 准備數據:從文本文件中解析數據
數據存放在文本文件datingTestSet.txt中,每個樣本數據占據一行,總共有1000行。
海倫的樣本主要包含以下3種特征:
- 每年獲得的飛行常客里程數
- 玩視頻游戲所耗時間百分比
- 每周消費的冰淇淋公升數
在將上述特征數據輸入到分類器之前,必須將待處理數據的格式改變為分類器可以接受的格式。在kNN.py中創建名為file2matrix的函數,以此來處理輸入格式問題。該函數的輸入為文件名字符串,輸出為訓練樣本矩陣和類標簽向量。
def file2matrix(filename): fr = open(filename) arrayOLines = fr.readlines() numberOfLines = len(arrayOLines) returnMat = zeros((numberOfLines, 3)) 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處理文本文件非常容易——
- 首先我們需要知道文本文件包含多少行。打開文件,得到文件的行數。
- 然后創建以零填充的矩陣。
- 循環處理文件中的每行數據,首先使用函數line.strip()截取掉所有的回車字符,然后使用tab字符\t將上一步得到的整行數據分割成一個元素列表。
- 接着,我們選取前3個元素,將它們存儲到特征矩陣中。
- Python語言可以使用索引值-1表示列表中的最后一列元素,利用這種負索引,我們可以很方便地將列表的最后一列存儲到向量classLabelVector中。
測試代碼:
>>> datingDataMat, datingLabels = kNN.file2matrix('datingTestSet.txt') >>> datingDataMat >>> datingLabels
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()
散點圖使用datingDataMat矩陣的第二、第三列數據,分別表示特征值“玩視頻游戲所耗時間百分比”和“每周所消費的冰淇淋公升數”。
圖3 沒有樣本類別標簽的約會數據散點圖
重新輸入上面的代碼,在調用scatter函數時使用下列參數:
>>> ax.scatter(datingDataMat[:,1], datingDataMat[:,2], 15.0 * array(datingLabels), 15.0 * array(datingLabels))
上述代碼利用變量datingLabels存儲的類標簽屬性,在散點圖上繪制了色彩不等、尺寸不同的點。
利用顏色及尺寸標識了數據點的屬性類別,因而我們基本上可以從圖4中看到數據點所屬三個樣本分類的區域輪廓。
圖4 帶有樣本分類標簽的約會數據散點圖
而下圖采用列1和2的屬性值可以得到更好的效果:
圖5 每年贏得的飛行常客里程數與玩視頻游戲所占百分比的約會數據散點圖
2.3 准備數據:歸一化數值
不同特征值有不同的均值和取值范圍,如果直接使用特征值計算距離,取值范圍較大的特征將對距離計算的結果產生絕對得影響,而使較小的特征值幾乎沒有作用,近乎沒有用到該屬性。如兩組特征:{0, 20000, 1.1}和{67, 32000, 0.1},計算距離的算式為:
$ \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分別是數據集中的最小特征值和最大特征值。
添加autoNorm()函數,用於將數字特征值歸一化:
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
對這個函數,要注意返回結果除了歸一化好的數據,還包括用來歸一化的范圍值ranges和最小值minVals,這將用於對測試數據的歸一化。
注意,對測試數據集的歸一化過程必須使用和訓練數據集相同的參數(ranges和minVals),不能針對測試數據單獨計算ranges和minVals,否則將造成同一組數據在訓練數據集和測試數據集中的不一致。
查看經過歸一化后的數據:
>>> normMat, ranges, minVals = kNN.autoNorm(datingDataMat) >>> normMat >>> ranges >>> minVals
2.4 測試算法:作為完整程序驗證分類器
機器學習算法一個很重要的工作就是評估算法的正確率,通常我們只提供已有數據的90%作為訓練樣本來訓練分類器,而使用其余的10%數據去測試分類器,檢測分類器的正確率。需要注意的是,10%的測試數據應該是隨機選擇的。由於海倫提供的數據並沒有按照特定目的來排序,所以我們可以隨意選擇10%數據而不影響其隨機性。
創建分類器針對約會網站的測試代碼:
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 = classify(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))
執行分類器測試程序:
>>> kNN.datingClassTest()
分類器處理約會數據集的錯誤率是2.4%,這是一個相當不錯的結果。我們可以改變函數datingClassTest內變量hoRatio和變量k的值,檢測錯誤率是否隨着變量值的變化而增加。
這個例子表明我們可以正確地預測分類,錯誤率僅僅是2.4%。海倫完全可以輸入未知對象的屬性信息,由分類軟件來幫助她判定某一對象的可交往程度:討厭、一般喜歡、非常喜歡。
2.5 使用算法:構建完整可用系統
綜合上述代碼,我們可以構建完整的約會網站預測函數:
def classifyPerson(): resultList = ['not at all', 'in small doses', 'in large doses'] percentTats = float(raw_input("Percentage of time spent playing video game?")) ffMiles = float(raw_input("Frequent flier miles earned per year?")) iceCream = float(raw_input("Liters of ice cream consumed per year?")) datingDataMat, datingLabels = file2matrix('datingTestSet.txt') normMat, ranges, minVals = autoNorm(datingDataMat) inArr = array([ffMiles, percentTats, iceCream]) classifierResult = classify((inArr - minVals) / ranges, normMat, datingLabels, 3) print "You will probably like this person: ", resultList[classifierResult - 1]
為了解程序的實際運行效果,輸入如下命令:
>>> 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
其中的粗體字是用戶的輸入。
目前為止,我們已經看到如何在數據上構建分類器。
3. 示例:手寫識別系統
4. 小結
k-近鄰算法是分類數據最簡單最有效的算法,本章通過兩個例子講述了如何使用k-近鄰算法構造分類器。k-近鄰算法是基於實例的學習,使用算法時我們必須有接近實際數據的訓練樣本數據。k-近鄰算法必須保存全部數據集,如果訓練數據集的很大,必須使用大量的存儲空間。此外,由於必須對數據集中的每個數據計算距離值,實際使用時可能非常耗時。
k-近鄰算法的另一個缺陷是它無法給出任何數據的基礎結構信息,因此我們也無法知曉平均實例樣本和典型實例樣本具有什么特征。下一章我們將使用概率測量方法處理分類問題,該算法可以解決這個問題。