1. 程式人生 > >機器學習實戰(3)—— kNN實戰約會網站

機器學習實戰(3)—— kNN實戰約會網站

機器學習實戰(3)—— kNN實戰約會網站

老闆:小韓啊,別忘了去改進一下約會網站的配對效果。

我:好嘞好嘞!馬上工作!!

好了,又要開始一天的工作啦。接著上篇文章老闆佈置的任務,我們來看一下這次實戰的相關資訊。

前言

老闆的朋友,卡特琳娜一直在使用約會網站尋找適合自己的約會物件。儘管約會網站會給她推薦不同的人選,但她並不是喜歡每一個人。經過一番總結,她發現她曾經交往過三種類型的人:

  1. 不喜歡的人
  2. 魅力一般的人
  3. 極具魅力的人

儘管發現了這樣的規律,但是卡特琳娜還是沒辦法及那個網站推薦的人分類,這可把她給愁壞了!所以,她找到我們,希望我們可以寫一個分類軟體

,來幫助她將匹配物件劃分到確切的分類中。

此外,她還手機了一些約會網站未曾記錄的資訊,她認為這能過夠幫到我們!

示例:在約會網站上使用k-近鄰演算法

(1) 收集資料:卡特琳娜提供的文字檔案。

(2) 準備資料:使用Python解析文字檔案。

(3) 分析資料:使用Matplotlib畫二維擴散圖。

(4) 訓練演算法:此步驟不適用於k-近鄰演算法。

(5) 測試演算法:使用卡特琳娜提供的部分資料作為測試樣本。 測試樣本和非測試樣本的區別在於:測試樣本是已經完成分類的資料,如果預測分類與實際類別不同,則標記為一個錯誤。

(6) 使用演算法:產生簡單的命令列程式,然後卡特琳娜可以輸入一些特徵資料以判斷對方是否為自己喜歡的型別。

2.2.1 準備資料:從文字檔案中解析資料

我們需要進行的第一步就是資料解析。我們必須弄明白我們的資料以什麼形式儲存的,資料中的每一項都代表什麼含義。

卡特琳娜提供給我們一個txt檔案,叫做datingSet.txt,其中每個樣本佔一行,總共有1000行。每個樣本主要包括以下三個特徵:

  1. 每年獲得的飛行常客里程數
  2. 玩視訊遊戲所耗時間百分比
  3. 每週消費的冰淇淋公升數

也就是下面這樣的形式:

在將上述特徵資料輸入到分類器之前,必須先進行資料處理,將待處理資料的格式改變為分類器可以接受的格式。接著我們上次寫過的kNN.py檔案,我們在這個檔案中建立名為file2matrix的函式,以此來處理輸入格式問題。

該函式的輸入為檔名字串,輸出為訓練樣本矩陣和類標籤向量。

def file2matrix(filename):
    # 1.得到檔案行數
    fr = open(filename)
    arrayOLines = fr.readlines()
    numberOfLines = len(arrayOLines)
    # 2.建立返回的NumPy矩陣
    returnMat = zeros((numberOfLines, 3))
    classLabelVector = []
    index = 0
    # 3.解析檔案資料到列表
    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命令提示符下輸入下面的命令:

>>> reload(kNN)
>>> datingDataMat, datingLabels = kNN.file2matrix('datingSet.txt')

注意:因為書上使用的python2,所以在這裡直接使用reload即可。

但是對於 python版本 <= Python 3.3,則需要使用以下命令:

import imp
imp.reload(kNN)

對於python版本 >= Python 3.4,則需要使用以下命令:

import importlib
importlib.reload(sys)

好了,在輸入命令的時候,還有下面兩點需要注意:

  1. 這樣讀檔案的時候,我們要注意:datingSet.txt要儲存在我們的工作目錄中。
  2. 在執行這個函式之前,我們需要重新載入kNN.py模組,以確保新的內容可以生效。

在不報錯的情況下,我們看一下我們生成的資料:

>>> datingDataMat
array([[4.0920000e+04, 8.3269760e+00, 9.5395200e-01],
       [1.4488000e+04, 7.1534690e+00, 1.6739040e+00],
       [2.6052000e+04, 1.4418710e+00, 8.0512400e-01],
       ...,
       [2.6575000e+04, 1.0650102e+01, 8.6662700e-01],
       [4.8111000e+04, 9.1345280e+00, 7.2804500e-01],
       [4.3757000e+04, 7.8826010e+00, 1.3324460e+00]])
>>> datingLabels
[3, 2, 1, 1, 1, 1, 3, 3, 1, 3, 1, 1, 2, 1, ......]

我們現在已經從文字檔案中匯入了資料,並將其轉換成了我們需要的格式。但是這樣有一個問題,我們沒辦法直觀地看出資料的含義,所以我們接下來就使用Python工具來圖形化展示資料內容。

注意:在分析資料的時候,圖形化是一個非常好用的方法。Python中的Matplotlib模組為我們提供了這樣的功能。

2.2.2 分析資料:使用Matplotlib建立散點圖

這裡我們使用Matplotlib來製作原始資料的散點圖,我本人也是剛入手機器學習和Python語言,所以對於這個庫的使用還不是很瞭解,後期我也會慢慢出一個Matplotlib的教程,最好能夠以通俗的語言來為大家講明白。

(給自己立的flag,哭著也要讓它站穩)

好了,我們繼續在上面的Python命令列環境中,輸入以下命令:

>>> import matplotlib
>>> import matplotlib
>>> import matplotlib.pyplot as plt
>>> fig = plt.figure()
>>> ax = fig.add_subplot(111)
>>> ax.scatter(datingDataMat[:,1], datingDataMat[:, 2])
<matplotlib.collections.PathCollection object at 0x00000111E23668D0>
>>> plt.show()

輸出效果如下圖所示(我是使用Pycharm下的終端輸出的,輸入命令的時候有些粗心,大家千萬不要向我這樣):

這樣看,生成的圖片雖然能看,但是我們不懂它是什麼含義,這我們就得從原始碼入手了。

datingDataMat[:, 1] 這個代表的是datingDataMat矩陣的第二列資料,代表特徵‘玩視訊遊戲所耗時間百分比’
datingDataMat[:, 2] 這個代表的是datingDataMat矩陣的第三列資料,代表特徵‘每週所消費的冰淇淋公升數’

然後,我們看看ax.scatter()這個函式,其原型是:

scatter(x, y, s=None, c=None, marker=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, verts=None, edgecolors=None, hold=None, data=None, **kwargs)

暫且不看後面的引數(後面的教程會詳細介紹),我們看到,一個x代表橫軸,一個y代表縱軸。

好了,這樣我們就可以知道這個圖的基本含義了,用書上的圖表示如下:

由於我們沒有使用樣本分類的特徵值,我們很難從上面的圖中看出有用的資訊來。一般來說,我們會採用色彩或其他的記號來標記不同樣本分類,以便更好地理解資料資訊。

Matplotlib 庫提供的scatter函式支援個性化標記散點圖上的點。重新輸入上面的程式碼,呼叫scatter函式 時使用下列引數:

>>> fig = plt.figure()
>>> ax = fig.add_subplot(111)
>>> ax.scatter(datingDataMat[:,1], datingDataMat[:, 2], 15.0 * array(datingLabels), 15.0 * array(datingLabels))
<matplotlib.collections.PathCollection object at 0x00000111E41CC9B0>
>>> plt.show()

我們就是利用了變數datingLabels儲存的類標籤屬性,在散點圖上繪製了色彩不等,尺寸不同的點,輸出效果如下:

這樣,我們就可以基本上看到資料點所屬三個樣本分類的區域輪廓。

2.2.3 準備資料:歸一化數值

為什麼要歸一化數值?這個問題我們慢慢來解決!

我們先來看一組資料:

玩視訊遊戲所耗時間百分比 每年獲得的飛行常客里程數 每週消費的冰淇淋公升數 樣本分類
1 0.8 400 0.5 1
2 12 134000 0.9 3
3 0 20000 1.1 2
4 67 32000 1 2

計算以下樣本3和樣本4之間的距離,如下:
$$
d = \sqrt{(0-67)^2 + (20000 - 32000)^2 + (1.1 - 1.0)^2}
$$
我們很容易發現,上面方程中數字差值最大的屬性對計算結果的影響最大。也就是說,每年獲取的飛行常客里程數對於計算結果的影響將遠遠大於其他兩個特徵——玩視訊遊戲所耗時間百分比和每週消費冰淇淋公升數——的影響。而產生這種現象的唯一原因,僅僅是因為飛行常客里程數遠大於其他特徵值。

但卡特琳娜認為這三種特徵是同等重要的,因此作為三個等權重的特徵之一,飛行常客里程數並不應該如此嚴重地影響到計算結果。

所以,在處理這種不同取值範圍的特徵值時,我們通常就需要採用歸一化,如將取值範圍處理為0到1或者-1到1之間。下面的公式可以將任意取值範圍的特徵值轉化為0-到1區間的值:
$$
newValue = (oldValue - min) / (max - min)
$$
其中min和max分別是資料集中當前特徵的最小值和最大值。雖然改變數值取值範圍增加了分類器的複雜度,但為了得到準確結果,我們必須這樣做。

好了,理論講了一大堆,我們還是來寫程式碼。繼續在kNN.py中編寫程式碼,建立一個函式autoNorm()。

def autoNum(dataSet):
    minVals = dataSet.min(0)  # 儲存每列的最小值,引數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

上面的程式碼,有一點需要我們注意:特徵值矩陣有1000×3個值,而minVals和ranges的值都為1×3。為了解決這個問題,我們使用NumPy庫中tile()函式將變數內容複製成輸入矩陣同樣大小的矩陣,注意這是具體特徵值相除 ,而 對於某些數值處理軟體包,/可能意味著矩陣除法。

好了,寫完之後,我們繼續在Pyhon命令提示符下,重新載入kNN.py模組,執行autoNorm函式:

>>> reload(kNN)
>>> normMat, ranges, minVals = kNN.autoNorm(datingDataMat)
>>> normMat
array([[0.44832535, 0.39805139, 0.56233353],
       [0.15873259, 0.34195467, 0.98724416],
       [0.28542943, 0.06892523, 0.47449629],
       ...,
       [0.29115949, 0.50910294, 0.51079493],
       [0.52711097, 0.43665451, 0.4290048 ],
       [0.47940793, 0.3768091 , 0.78571804]])
>>> ranges
array([9.1273000e+04, 2.0919349e+01, 1.6943610e+00])
>>> minVals
array([0.      , 0.      , 0.001156])

這裡我們也可以只返回normMat矩陣,但是下一節我們需要將取值範圍和最小值歸一化測試資料。

2.2.4 測試演算法:作為完整程式驗證分類器

老闆:小韓啊,寫的怎麼樣了啊?

我:資料視覺化,資料處理以及資料歸一化我都做完了,接下來,把資料丟到昨天寫的分類器就能看到效果了!!

老闆:不錯不錯!繼續努力啊!年終獎到時候少不了你的!

我:(美滋滋)沒問題,你就放100個心吧。

好了,總算是處理完資料了。接下來我們就可以去看一下我們的分類器效果怎麼樣了,想想就很開心呢!,如果分類器的正確率滿足要求,卡特琳娜就可以使用這個軟體來處理約會網站提供的約會名單了!!!

機器學習演算法一個很重要的工作就是評估演算法的正確率,通常我們只提供已有資料的90%作為訓練樣本來訓練分類器,而使用其餘的10%資料去測試分類器,檢測分類器的正確率。需要注意的是,10%的測試資料應該是隨機選擇的,由於卡特琳娜提供的資料並沒有按照特定目的來排序,所以我們可以隨意選擇10%資料而不影響其隨機性。

話不多說,直接上程式碼,我們還是在kNN.py中繼續編寫:

def datingClassTest():
    hoRatio = 0.10
    datingDataMat, datingLabels = file2matrix('datingTestSet2.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)))

其實,程式碼流程還是很簡單的。

  1. 首先使用了file2matrix和autoNorm()函式從檔案中讀取資料並將其轉換為歸一化特徵值;
  2. 接著計算測試向量的數量,此步決定了normMat向量中哪些資料用於測試,哪些資料用於分類器的訓練樣本;
  3. 然後將這兩部分資料輸入到原始kNN分類器函式classify0。最後,函式計算錯誤率並輸出結果。

寫完之後,我們直接在我們的Python命令提示符下從在kNN.py模組,然後輸入kNN.datingClassTest(),執行分類測試程式,我們可以得到以下結果:

Yes Yes Yes!!! 效果還是不錯的,只有5%的錯誤率!!!當然,我們也可以改變函式 datingClassTest內變數hoRatio和變數k的值,檢測錯誤率是否隨著變數值的變化而增加。不同的引數,分類器的效能會有很大不同的。

老闆:哎呦,不錯哦!只有5%的錯誤率

我:那是必須的!

老闆:那你在改改,我們就可以給卡特琳娜用了!!

我:沒問題,您就等著給我加雞腿吧!!

2.2.5 使用演算法:構建完整可用系統

好了,程式設計師測試完了,準備釋出吧!!!

這裡我們就使用這個簡答的分類器來幫助卡特琳娜對網站給出的候選人進行分類!!!

def classifyPerson():
    resultList = ['not at all', 'in small doses', 'in large doses']
    percentTats = float(input('percentage of time spent playing video games?'))
    ffMiles = float(input('frequent flier miles earned per year?'))
    iceCream = float(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(),這是Python 2的函式,Python3就直接使用input就行。

我們直接看結果:

總算是寫完了,雖然都是黑乎乎的控制檯程式,但是演算法的核心及應用方法我們都會了,前端的包裝就不是我們的鍋了!!!hiahiahia!!!


最後,還是熟悉的配方!

歡迎大家關注我的公眾號,有什麼問題也可以給我留言哦!