系列文章:《機器學習實戰》學習筆記html
本章介紹了《機器學習實戰》這本書中的第一個機器學習算法:k-近鄰算法,它很是有效並且易於掌握。首先,咱們將探討k-近鄰算法的基本理論,以及如何使用距離測量的方法分類物品;其次咱們將使用Python從文本文件中導入並解析數據;再次,本文討論了當存在許多數據來源時,如何避免計算距離時可能碰到的一些常見錯誤;最後,利用實際的例子講解如何使用k-近鄰算法改進約會網站和手寫數字識別系統。算法
簡單地說,k-近鄰算法採用測量不一樣特徵值之間的距離方法進行分類。sql
k-近鄰算法shell
優勢:精度高、對異常值不敏感、無數據輸入假定。
缺點:計算複雜度高、空間複雜度高
適用數據範圍:數值型和標稱型app
k-近鄰算法(kNN)的工做原理是:存在一個樣本數據集合,也稱做訓練樣本集,而且樣本集中每一個數據都存在標籤,即咱們知道樣本集中每一數據與所屬分類的對應關係。輸入沒有標籤的新數據後,將新數據的每一個特徵與樣本集中數據對應的特徵進行比較,而後算法提取樣本集中特徵最類似數據(最近鄰)的分類標籤。通常來講,咱們只選擇樣本數據集中前k個最類似的數據,這就是k-近鄰算法中k的出處,一般k是不大於20的整數。最後,選擇k個最類似數據中出現次數最多的分類,做爲新數據的分類。機器學習
如今咱們回到前面電影分類的例子,使用k-近鄰算法分類愛情片和動做片。有人曾經統計過不少電影的打鬥鏡頭和接吻鏡頭,圖1顯示了6部電影的打鬥和接吻鏡頭數。假若有一部未看過的電影,如何肯定它是愛情片仍是動做片呢?咱們可使用kNN來解決這個問題。ide
圖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-近鄰算法斷定輸入數據分別屬於哪一個分類。
建立名爲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()
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。
上文咱們已經使用k-近鄰算法構造了第一個分類器,也能夠檢驗分類器給出的答案是否符合咱們的預期。然而分類器並不會獲得百分百正確的結果,咱們可使用多種方法檢測分類器的正確率。
爲了測試分類器的效果,咱們可使用已知答案的數據,固然答案不能告訴分類器,檢驗分類器給出的結果是否符合預期結果。經過大量的測試數據,咱們能夠獲得分類器的錯誤率——分類器給出錯誤結果的次數除以測試執行的總數。錯誤率是經常使用的評估方法,主要用於評估分類器在某個數據集上的執行效果。完美分類器的錯誤率爲0,最差分類器的錯誤率是1.0,在這種狀況下,分類器根本就沒法找到一個正確答案。然而錯誤率幾乎不會達到1.0,由於即便是隨機猜想,也會有必定機率猜對的。所以,錯誤率通常存在一個上限,且具體的值會與各種型之間的比例關係直接相關。
個人朋友海倫一直使用在線約會網站尋找適合本身的約會對象。儘管約會網站會推薦不一樣的人選,但她並非喜歡每個人。通過一番總結,她發現曾交往過三種類型的人:
海倫但願咱們的分類軟件能夠更好地幫助她將匹配對象劃分到確切的分類中。此外海倫還收集了一些約會網站不曾記錄的數據信息,她認爲這些數據更有助於匹配對象的歸類。
數據存放在文本文件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處理文本文件很是容易——
測試代碼:
>>> datingDataMat, datingLabels = kNN.file2matrix('datingTestSet.txt') >>> datingDataMat >>> datingLabels
咱們使用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矩陣的第2、第三列數據,分別表示特徵值「玩視頻遊戲所耗時間百分比」和「每週所消費的冰淇淋公升數」。
圖3 沒有樣本類別標籤的約會數據散點圖
從新輸入上面的代碼,在調用scatter函數時使用下列參數:
>>> ax.scatter(datingDataMat[:,1], datingDataMat[:,2], 15.0 * array(datingLabels), 15.0 * array(datingLabels))
上述代碼利用變量datingLabels存儲的類標籤屬性,在散點圖上繪製了色彩不等、尺寸不一樣的點。
利用顏色及尺寸標識了數據點的屬性類別,於是咱們基本上能夠從圖4中看到數據點所屬三個樣本分類的區域輪廓。
圖4 帶有樣本分類標籤的約會數據散點圖
而下圖採用列1和2的屬性值能夠獲得更好的效果:
圖5 每一年贏得的飛行常客里程數與玩視頻遊戲所佔百分比的約會數據散點圖
不一樣特徵值有不一樣的均值和取值範圍,若是直接使用特徵值計算距離,取值範圍較大的特徵將對距離計算的結果產生絕對得影響,而使較小的特徵值幾乎沒有做用,近乎沒有用到該屬性。如兩組特徵:{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
機器學習算法一個很重要的工做就是評估算法的正確率,一般咱們只提供已有數據的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%。海倫徹底能夠輸入未知對象的屬性信息,由分類軟件來幫助她斷定某一對象的可交往程度:討厭、通常喜歡、很是喜歡。
綜合上述代碼,咱們能夠構建完整的約會網站預測函數:
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
其中的粗體字是用戶的輸入。
目前爲止,咱們已經看到如何在數據上構建分類器。
k-近鄰算法是分類數據最簡單最有效的算法,本章經過兩個例子講述瞭如何使用k-近鄰算法構造分類器。k-近鄰算法是基於實例的學習,使用算法時咱們必須有接近實際數據的訓練樣本數據。k-近鄰算法必須保存所有數據集,若是訓練數據集的很大,必須使用大量的存儲空間。此外,因爲必須對數據集中的每一個數據計算距離值,實際使用時可能很是耗時。
k-近鄰算法的另外一個缺陷是它沒法給出任何數據的基礎結構信息,所以咱們也沒法知曉平均實例樣本和典型實例樣本具備什麼特徵。下一章咱們將使用機率測量方法處理分類問題,該算法能夠解決這個問題。