机器学习实战之 k近邻算法 原理及代码实现

来源:互联网 发布:知几是什么意思 编辑:程序博客网 时间:2024/05/19 05:40
基本概念:

近邻学习是一种监督学习算法,在给定的训练样本集中,基于某种距离度量,找出与训练集最靠近的k个训练样本,然后基于这k个邻居信息来进行预测。 
投票法:通常在分类任务中使用,判别方法是选择这k个样本中出现最多的雷冰标记作为预测结果。 
平均法:通常在回归任务中使用,判别方法是将这k个样本的实值输出标记的平均值最为预测结果。 
加权平均或加权投票:根据距离远近来决定权重,距离越近,权重越大

KNN算法没有显式的学习过程,事实上,它是“懒惰学习”的代表:在训练阶段仅仅是把样本保存起来,训练时间开销为0
待收到测试样本后再进行处理, 相应的,那些在训练阶段就对样本进行学习处理的方法,称为“急切学习”

定理:最近邻分类器(k=1),泛化错误率不超过贝叶斯分类器错误率的两倍




代码实现:

(平均法):
import numpy as np

def knnclassify(inX, dataSet, labels, k):
    dataSetSize = dataSet.shape[0]
    diffMat = np.tile(inX, (dataSetSize,1)) - dataSet
    sqDiffMat = diffMat**2
    sqDistances = sqDiffMat.sum(axis=1)
    distances = sqDistances**0.5
    sortedDistIndicies = distances.argsort()
    classCount = {}
    for i in range(k):
        voteIlabel = labels[sortedDistIndicies[i]]
        classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
        sortedClassCount = sorted(classCount.items(), key=lambda classCount : classCount[1], reverse=True)
    return sortedClassCount[0][0]

(加权投票法)
设权值 从 1.5 等比 降到 0.5
例如:1.5 ,1.39 ,1.28 ,1.17 ,1.06 ,0.94 ,0.83 ,0.72 ,0.61 ,0.5
classCount[votelabel] = classCount.get(votelabel, 0) + round((1.5 - i / (k-1)), 2)

在实例3中,分别用两种方法测试

k
错误总数(平均投票)
错误总数(加权投票)
1
13
13
2
13
13
3
10
13
4
11
10
5
17
11
6
17
14
7
21
18
8
18
16
9
21
19

可以看出:
  1. 并不是 K 越大,分类准确率越高。
  2. 当 K 越大时,加权投票的分类 优势越明显



参考实例:

例1:

import numpy as np
from knn.KNNclassifier import knnclassify
# 导入当前文件夹下的文件,(如果不加入"knn.",则会出现红色波浪线,但可正常运行)
def creatrDateSet():
    group = np.array([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]])
    labels = ['A','A','B','B']
    return group, labels

group, labels = creatrDateSet()
print(knnclassify([0, 0], group, labels, 3))  # 结果B
print(knnclassify([1, 1], group, labels, 4))  # 结果A
print(knnclassify([0.5, 0.5], group, labels, 4))  # 结果B

代码解释:
def classify0(inX, dataSet, labels, k):
    dataSetSize = dataSet.shape[0] #4
    diffMat = np.tile(inX, (dataSetSize,1)) - dataSet
    #  np.tile(inX, (dataSetSize,1) 结果为 [[0,0],[0,0],[0,0],[0,0]]
    # 减去dataSet 后每行为每个点的距离差
    sqDiffMat = diffMat**2
    # 将每个距离差平方
    sqDistances = sqDiffMat.sum(axis=1)
    # #axis=0表述列 ,axis=1表述行, 横项相加
    distances = sqDistances**0.5

    # 开根号得到距离:[ 1.48660687  1.41421356  0.          0.1      ]
    sortedDistIndicies = distances.argsort() # 返回结果从头到尾为非降序排序的index:[2 3 1 0]

    classCount={}
    for i in range(k):
        voteIlabel = labels[sortedDistIndicies[i]]
        # 循环取出排序从近到远的labels  : B B A
        classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1 # 构建字典,最终得到频次{'B': 2, 'A': 1}

    sortedClassCount = sorted(classCount.items(), key= lambda classCount:classCount[1], reverse=True)
    # operator模块提供的itemgetter函数用于获取对象的哪些维的数据
    # sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
    '''在Python2.x中,items( )用于 返回一个字典的拷贝列表【Returns a copy of the list of all items (key/value pairs) in D】,占额外的内存。
      iteritems() 用于返回本身字典列表操作后的迭代【Returns an iterator on all items(key/value pairs) in D】,不占用额外的内存。
    Python 3.x 里面,iteritems() 和 viewitems() 这两个方法都已经废除了,而 items() 得到的结果是和 2.x 里面 viewitems() 一致的。
    在3.x 里 用 items()替换iteritems() ,以列表返回可遍历的(键, 值) 元组数组'''

    print(sortedClassCount)
    return sortedClassCount[0][0]
    # sortedClassCount 得到的值为列表,列表元素为每个标签和频次,由频次从高到底排序 ,本例中为[('B', 2), ('A', 1)]

group, labels = creatrDateSet()
print(classify0([0, 0], group, labels, 3))  # 结果B
print(classify0([1, 1], group, labels, 4))  # 结果A
print(classify0([0.5, 0.5], group, labels, 4))  # 结果B

例2:约会对象是否合适
样本特征:1. 玩游戏消耗时间百分比   2. 飞行里程数    3.每周吃冰激凌升数
标签分类:1.不喜欢  2.有一点喜欢  3.比较喜欢

import numpy as np
from knn.KNNclassifier import knnclassify

# 将 文本记录 转换为 NumPy
def file2matrix(filename, dim):
    fr = open(filename)
    numberOfLines = len(fr.readlines())        #get the number of lines in the file
    returnMat = np.zeros((numberOfLines,dim))        #prepare matrix to return
    classLabelVector = []                      #prepare labels return
    fr = open(filename)
    index = 0
    for line in fr.readlines():
        line = line.strip()
        listFromLine = line.split('\t')
        returnMat[index,:] = listFromLine[0:dim]
        classLabelVector.append(int(listFromLine[-1]))
        index += 1
    return returnMat,classLabelVector

def normalize(dataSet):
    minVals = dataSet.min(0) # 每列最小值
    maxVals = dataSet.max(0)
    ranges = maxVals - minVals
    normDataSet = np.zeros(np.shape(dataSet))
    m = dataSet.shape[0]
    normDataSet = dataSet - np.tile(minVals, (m, 1))
    normDataSet = normDataSet/np.tile(ranges, (m, 1))
    return normDataSet , ranges, minVals

def dataplot(dataSet, labels):
    import  matplotlib.pyplot as plt
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.scatter(dataSet[:,1], dataSet[:,2], 15.0*np.array(labels), np.array(labels))
    ax.set_ylabel('Ice Cream Consumption')
    ax.set_xlabel('Time of Playing Games')
    # 后面两个参数分别控制散点大小和颜色
    plt.show()

def classifytest(dataset, datalabel, k, testRatio):
    m = dataset.shape[0] # 数据列数
    numTestVecs = int(m * testRatio) #此次测试所用的样本数目
    errorCount = 0.0
    for i in range(numTestVecs):
        classifierResult = knnclassify(dataset[i, :], dataset[numTestVecs:m, :], datalabel[numTestVecs:m], k)
        #print("the classifier came back with: %d, the real answer is: %d" % (classifierResult, datalabel[i]))
        # dataset 前 测试率*数据总数目 用于测试,  使用 余下的作为分类依据
        if (classifierResult != datalabel[i]):
            errorCount += 1.0
    print("the total error rate is: %f" % (errorCount / float(numTestVecs)))

def main():

    datingDataMat, datingLabels = file2matrix('datingTestSet2.txt',3)
    normMat, ranges, minVals = normalize(datingDataMat)
    dataplot(normMat, datingLabels)
    #classifytest(normMat,datingLabels,3,0.1) # 第三个参数为分类器的k值
    # 第四个系数为测试样本比例,系数越高 分类准确率越低(用于测试的越多,用于分类的越少)

    resultList = ['not at all', 'in small dose', 'in large doses']
    timeofgame = float(input("percentage of time spend playing video games:"))
    flymiles = float(input("frequent flier miles earner per year:"))
    icecream = float(input("liters of ice cream consumed per year:"))
    inarr = np.array([timeofgame, flymiles, icecream])
    norminput = (inarr - minVals) / ranges
    result = knnclassify(norminput, normMat, datingLabels, 3)
    print("you will probable like this person", resultList[result - 1])

if __name__=="__main__":
    main()


例3

import numpy as np
from os import listdir
from knn.KNNclassifier import knnclassify

# 将 32*32 的数组,转化为一维 nd.array 矩阵
def img2vector(filename):
    returnVect = np.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

def handwritingClassTest():
    hwLabels = []
    trainingFileList = listdir('digits/trainingDigits')          #load the training set
    m = len(trainingFileList)  # 文件数目,同时也是训练样本数目 本例中为1934
    trainingMat = np.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('digits/trainingDigits/%s' % fileNameStr)
    # 对每个文件进行 向量化处理,得到 m * 1024 的矩阵

    testFileList = listdir('digits/testDigits')        #iterate through the test set
    errorCount = 0.0
    mTest = len(testFileList)
    for i in range(mTest):
        fileNameStr = testFileList[i]
        fileStr = fileNameStr.split('.')[0]    # 去掉 .txt 的后缀
        classNumStr = int(fileStr.split('_')[0])  # _ 前为该数字的正确值
        vectorUnderTest = img2vector('digits/testDigits/%s' % fileNameStr)
        classifierResult = knnclassify(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)))

handwritingClassTest()
# 约会选择中,将一份数据一分为2位训练集和测试集,而此例中为分开好的
# 由于数据只能为 0 和 1, 因此无需归一化处理