1. 程式人生 > >【機器學習演算法-python實現】KNN-k近鄰演算法的實現(附原始碼)

【機器學習演算法-python實現】KNN-k近鄰演算法的實現(附原始碼)

 下載地址

kNN演算法及例項原始碼實現
#coding=utf-8

'''
Created on Sep 16, 2010
kNN: k Nearest Neighbors

Input:      inX: vector to compare to existing dataset (1xN)
            dataSet: size m data set of known vectors (NxM)
            labels: data set labels (1xM vector)
            k: number of neighbors to use for comparison (should be an odd number)
            
Output:     the most popular class label

@author: pbharrin

k-近鄰演算法
k-近鄰演算法採用測量不同特徵值之間的距離方法進行分類

k-近鄰演算法的優缺點
優點 精度高 對異常值不敏感 無資料輸入假定
缺點 計算複雜度高 空間複雜度高
適用資料範圍: 數值型和標稱型

標稱型目標變數的結果只在有限目標集中取值,如真與假、動物分類集合{ 爬行類、魚類、哺乳類、兩棲類} ;數值型目標變數則可以從無限的數值集合中取值,如0.100、42.001、1000.743 等。

kNN演算法的工作原理:
存在一個樣本資料集合,也稱作訓練樣本集,並且樣本集中每個資料都存在標籤。即我們知道樣本集中每一資料與所屬分類的對應關係
輸入沒有標籤的新資料後,將新資料的每個特徵和樣本集中資料對應的特徵進行比較,然後演算法提取樣本集中特徵最相似的資料(最近鄰)
和標籤分類。一般來說,我們只選擇樣本資料集中前k個最相似的資料,這就是k-近鄰演算法的出處,通常k是不大於20的整數。最後,選擇k
個最相似資料中出現次數最多的分類,作為新資料的分類




                  k-近鄰演算法的一般流程
(1)收集資料:可以使用任何方法。
(2)準備資料:距離計算所需要的數值,最好是結構化的資料格式。
(3)分析資料:可以使用任何方法。
(4)訓練演算法:此步驟不適用於k-近部演算法。
(5)測試演算法:計算錯誤率。
(6)使用演算法:首先需要輸入樣本資料和結構化的輸出結果,然後執行k-近鄰演算法判定輸
  入資料分別屬於哪個分類,最後應用對計算出的分類執行後續的處理。



從文字檔案中解析資料
虛擬碼如下:
1、計算已知類別資料集中的點與當前點之間的距離
2、按照距離遞增次序排列
3、選取與當前點距離最小的k個點
4、確定前k個點所在類別的出現頻率
5、返回前k個點出現頻率最高的類別作為當前點的預測分類


k-近鄰演算法是分類資料最簡單有效的演算法 k-近鄰演算法基於例項的學習,使用演算法時,必須有接近實際資料的訓練樣本資料
k-近鄰演算法必須儲存全部資料集,這樣訓練資料集很大的話,必須使用大量的儲存空間。由於必須對資料集中每個資料計算距離值,實際使用時可能非常耗時
k-近鄰演算法的另一個缺陷是無法給出任何資料的基礎結構資訊,因此無法知曉平均例項樣本和典型例項樣本具有什麼特徵

numpy科學計算包
運算子模組
'''
from numpy import *
import operator
from os import listdir
'''距離的計算
classify0函式有4個輸入引數:
用於分類的輸入向量inX,輸入的訓練樣本集為dataSet,標籤向量labels,最後的引數k表示用於選擇最近鄰居的數目
其中標籤向量的元素數目和矩陣dataSet的行數相同,使用歐氏距離公式,計算兩個想亮點xA和xB之間的距離

計算兩個向量點xA xB之間的距離
歐氏距離公式:
d=sqrt((xA0-xB0)^2+(xA1-xB1)^2)

計算完所有點之間的距離後,可以對資料按照從小到大的次序排列。然後,確定前K個距離最小元素所在的主要分類,輸入K總是正整數;最後
將classCount()字典分解為元祖列表,然後使用程式第二行導數運算子模組的itermgetter方法,按照第二個元素
的次序對元祖進行排序。此處的排序為逆序,即按照從最大到最小次序排序,最後返回發生頻率最好的元素標籤
'''
def classify0(inX, dataSet, labels, k):
    dataSetSize = dataSet.shape[0] #陣列的大小
    diffMat = tile(inX, (dataSetSize,1)) - dataSet #函式的形式是tile(A,reps),參看部落格
    sqDiffMat = diffMat**2  #**平方的意思
    sqDistances = sqDiffMat.sum(axis=1)
    distances = sqDistances**0.5 #開平方
    #按照距離遞增次序排列  計算完所有點之間的距離後,可以對資料按照從小到大的次序進行排序,然後確定前k個距離最小元素所在的主要分類,輸入k總是正整數;最後,將classCount字典分解為元祖列表,然後使用程式第二行匯入運算子模組的itemgetter方法,按照第二個元素的次序對元祖進行排序  
    sortedDistIndicies = distances.argsort()
    
    classCount={}          
    for i in range(k):
        voteIlabel = labels[sortedDistIndicies[i]]
        classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
    sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]

'''
有四組數,魅族資料有兩個已知的屬性或特徵值,group矩陣每行包含一個不同的資料,可以把它想象成某個日誌檔案中
不同的測量點或者入口。因為人腦的限制,通常只能視覺化處理三維以下的事務。因此為了實現資料視覺化,對於每個
資料點通常只使用兩個特徵。
向量label包含每個資料點的標籤資訊,label包含的元素個數等於group矩陣行數
這裡(1.0,1.1)定義為A (0,0.1)定義為B
'''
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

'''
在約會網站上使用k-近鄰演算法
1、收集資料:提供樣本檔案
2、準備資料:使用Python解析文字檔案
3、分析資料:使用matplotlib畫二維擴散圖
4、訓練演算法:此步驟不適合k-近鄰演算法
5、測試演算法:測試樣本和非測試樣本區別在於:測試樣本已經完成分類的資料,如果預測分類與實際類別不同,則標為error
6、使用演算法:產生簡單的命令列程式,然後可以輸入一些特徵資料以判斷對方是否為自己喜歡的型別
'''
#確保樣本檔案和py檔案在同一目錄下,樣本資料存放在datingTestSet.txt檔案中
'''
樣本主要包含了一下內容
1、每年獲得的飛行常客里程數
2、玩視訊遊戲所耗時間百分比
3、每週消費的冰激凌公升數

>>> 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 0x03EF6690>
>>> plt.show()
由於沒有使用樣本分類的特徵值,很難看到任何有用的資料模式資訊,一般來說
採用色彩或者其他記號來標記不同樣本分類,以便更好地理解資料資訊
>>> ax.scatter(datingDataMat[:,1],datingDataMat[:,2,15*array(datingLabels),15*datingLabels])  暫時有誤,需要解決
利用顏色以及尺寸標識了資料點的屬性類別,帶有養病呢分類標籤的約會資料散點圖,雖然能夠比較容易的區分資料點從屬類別,但依然很難根據這張圖給出結論性的資訊

'''



def file2matrix(filename):
    fr = open(filename)
    f_lines = fr.readlines()
    numberOfLines = len(f_lines)         #get the number of lines in the file 得到檔案的行數
    
    returnMat = zeros((numberOfLines,3))        #prepare matrix to return  建立以0填充的矩陣numpy,為了簡化處理,將該矩陣的另一維度設定為固定值3,可以根據自己的需求增加相應的程式碼以適應變化的輸入值
    classLabelVector = []                       #prepare labels return   
    #fr = open(filename)
    
    index = 0
    for line in f_lines:   #迴圈處理檔案中的每行資料,首先使用line.strip擷取掉所有的回車字元,然後使用tab字元\t將上一步得到的整行資料分割成一個元素列表
        line = line.strip()
        
        listFromLine = line.split('\t')
        
        returnMat[index,:] = listFromLine[0:3]  #選取前3個元素,將其儲存到特徵矩陣中
        classLabelVector.append(listFromLine[-1]) #Python語言可以使用索引值-1表示列表中的最後一列元素,利用這種負索引,可以將列表的最後一列儲存到向量classLabelVector中。注意:必須明確的通知直譯器,告訴它列表中儲存的元素值為整形,否則Python語言會將這些元素當做字串來處理  listFromLine前不能加int否則報錯
        index += 1
    return returnMat,classLabelVector

    
'''
歸一化數值
多種特徵同等重要時(等權重),處理不同取值範圍的特徵值時,通常採用數值歸一化,將取值範圍處理為0~1或者-1~1之間
newValue = {oldValue-min}/(max-min)
min和max分別是資料及資料集中的最小特徵值和最大特徵值。雖然改變數值取值範圍增加了分類器的複雜度,但為了得到精確結果,必須這樣做
autoNorm將數字特徵值轉換為0~1
>>> reload(kNN)
<module 'kNN' from 'C:\Users\kernel\Documents\python\kNN.py'>
>>> normMat,ranges,minVals = kNN.autoNorm(datingDataMat)

函式autoNorm()中,將每列的最小值放在變數minValue中,將最大值放在變數maxValue中。其中
dataSet.min(0)中的引數0使得函式可以從列中選取最小值,而不是選取當前行的最小值。然後,
函式計算可能的取值範圍,並建立新的矩陣。

為了歸一化特徵值,必須使用當前值減去最小值,除以取值範圍。需要注意的是:特徵值矩陣有1000*3
個值,而minVals和range的值都為1*3.使用Numpy庫中tile()函式將變數內容複製成輸入矩陣大
小的矩陣,具體特徵值相除,而對於某些數值處理軟體包,/可能意味著矩陣除法在Numpy同樣庫中,
矩陣除法需要使用函式linalg.solve(matA,matB)

'''
    
def autoNorm(dataSet):
    minVals = dataSet.min(0)  #每列的最小值  引數0可以從列中選取最小值而不是選取當前行的最小值
    maxVals = dataSet.max(0)  
    ranges = maxVals - minVals  #函式計算可能的取值範圍,並建立新的返回矩陣,為了歸一化特徵值,必須使用當前值減去最小值,然後除以取值範圍
    normDataSet = zeros(shape(dataSet))  #注意事項:特徵值矩陣有1000*3個值。而minVals和range的值都為1*3.為了解決這個問題使用numpy中tile函式將變數內容複製成輸入矩陣同樣大小的矩陣
    m = dataSet.shape[0]
    normDataSet = dataSet - tile(minVals, (m,1))
    normDataSet = normDataSet/tile(ranges, (m,1))   #element wise divide
    return normDataSet, ranges, minVals
'''
對於分類器而言錯誤率就是分類器給出錯誤結果的次數除以測試資料的總數,完美分類器錯誤率為0,錯誤率為1的分類器不會給出任何正確的分類結果
在程式碼中設定一個計數器變數,每次分類器錯誤的分類資料,計數器就+1,程式執行完成後計算器的結果除以資料點總數即為錯誤率
>>> kNN.datingClassTest()
NameError: global name 'datingDataMat' is not defined  懸而未決
'''

   
def datingClassTest():
    hoRatio = 0;10

    datingDataMat,datingLables = 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))
    print errorCount

'''
該方法有問題需要改正 (已作更正)

約會網站預測函式
'''
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 = int(classify0((inArr-\
                                  minVals)/ranges,normMat,datingLabels,3))
    print "You will probably like this person:",\
          resultList[classifierResult - 1]


'''
手寫識別系統
構造的系統只能識別數字0~9,需要是別的數字已經使用影象處理軟體,處理成具有相同的色彩和大小:
寬高是32*32的黑白影象
1、收集資料 提供文字檔案
2、準備資料 編寫函式classify0(),將影象格式轉換成分類器使用的list格式
3、分析資料 在Python命令提示符中檢查資料,確保它符合要求
4、訓練演算法 此步驟不適合k-近鄰演算法
5、測試演算法 測試樣本和非測試樣本區別在於:測試樣本已經完成分類的資料,如果預測分類與實際類別不同,則標為error
6、使用演算法 未實現
'''    
def img2vector(filename):
    returnVect = zeros((1,1024))
    fr = open(filename)
    for i in range(32):
        lineStr = fr.readline()
        for j in range(32):
            returnVect[0,32*i+j] = int(lineStr[j])
    return returnVect

'''

手寫數字識別系統的測試程式碼

testDigits目錄中的檔案內容儲存在列表中,然後可以得到目錄中有多少檔案,便將其儲存到變數m中
建立一個m*1024的訓練矩陣,該矩陣的每行資料儲存一個影象,可以從檔名中解析出分類數字
該目錄下的檔案按照規則命名,如檔案9_45.txt的分類是9,它是數字9的第45個例項
將類程式碼儲存在hwLabels向量中,使用img2vector載入影象
對testDigits目錄中的檔案執行相似的操作,不同之處在於我們並不將這個目錄下的檔案載入矩陣中
而是利用classify0()函式測試該目錄下每個檔案,由於檔案中的值已在0~1之間,所以不需要autoNorm()函式
該演算法執行效率不高,因為演算法需要為每個測試向量做2000詞距離計算,每個距離計算包括了1024個維度浮點計算,總計執行900次
此外還需要為向量準備2M的儲存空間  k決策樹是k-近鄰演算法的改進版


'''
def handwritingClassTest():
    hwLabels = []
    trainingFileList = listdir('trainingDigits')           #load the training set
    m = len(trainingFileList)
    trainingMat = zeros((m,1024))
    for i in range(m):
        fileNameStr = trainingFileList[i]
        fileStr = fileNameStr.split('.')[0]     #take off .txt
        classNumStr = int(fileStr.split('_')[0])
        hwLabels.append(classNumStr)
        trainingMat[i,:] = img2vector('trainingDigits/%s' % fileNameStr)
    testFileList = listdir('testDigits')        #iterate through the test set
    errorCount = 0.0
    mTest = len(testFileList)
    for i in range(mTest):
        fileNameStr = testFileList[i]
        fileStr = fileNameStr.split('.')[0]     #take off .txt
        classNumStr = int(fileStr.split('_')[0])
        vectorUnderTest = img2vector('testDigits/%s' % fileNameStr)
        classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3)
        print "the classifier came back with: %d, the real answer is: %d" % (classifierResult, classNumStr)
        if (classifierResult != classNumStr): errorCount += 1.0
    print "\nthe total number of errors is: %d" % errorCount
    print "\nthe total error rate is: %f" % (errorCount/float(mTest))