机器学习实战 -- kNN分类算法

    网上有很多《机器学习实战》的资料,大多只讲具体操作,内部的原理并没有分析,这篇博文先讲kNN的具体实现,然后在推导一下算法背后的数学原理,注意:(代码是在Python3下实现的,有一点小改动)。

一、算法的工作原理

    首先我们需要一个数据集,数据集中包括数据和标签(数据和标签之间的关系一一对应)--该数据集我们称之为样本集,当我们输入没有标签的新数据时,算法将新数据的每个特征和已有样本集对应的特征进行对比,然后提取提取样本集 中与新数据最邻近的特征作为输出。所以我们可以明确算法的输入是一组没有标签的数据,输出是算法针对该组数据给出的标签。 


二、代码实现
    1. 分类函数实现的步骤:

        (1)计算样本集上的点与当前点之间的距离;
        (2)按照距离递增依次排序;
        (3)选取与当前点距离最小的K个点;
        (4)确定前k个点所在类别的出现频率;
        (5)返回前k个点出现频率最高的类别作为当前点的预测分类
程序清单如下:
函数输入: (测试向量,样本集,标签机,k)
函数输出: 预测标签
from numpy import *
import operator
from os import listdir
def classify0(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()     
    classCount={}          
    for i in range(k):
        voteIlabel = labels[sortedDistIndicies[i]]
        classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
    sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]

     classify0()作为分类函数,一共有四个输入,inX 是我们输入的待分类数据集,dataSet是样本集,labels是标签向量,k是选择的最邻近的个数。函数首先读取样本集的行数,然后将输入数据和样本集做差,平方,求行和,再开平方。所得结果是两点之间的欧氏距离。按照计算的结果由小到大排列索引号,然后统计最邻近的k个值中出现频率最高的标签,作出预测。

     2. 数据处理函数

    由于输入的样本集是一个.txt文档,所以要将其转化为能够处理的数据形式,做这项工作的是file2matrix()函数。

程序清单如下:

函数输入: 文本文档

函数输出: 样本集矩阵,标签向量

def file2matrix(filename):
    love_dictionary={'largeDoses':3, 'smallDoses':2, 'didntLike':1}
    fr = open(filename)
    arrayOLines = fr.readlines()
    numberOfLines = len(arrayOLines)            #get the number of lines in the file
    returnMat = zeros((numberOfLines,3))        #prepare matrix to return
    classLabelVector = []                       #prepare labels return   
    index = 0
    for line in arrayOLines:
        line = line.strip()
        listFromLine = line.split('\t')
        returnMat[index,:] = listFromLine[0:3]
        if(listFromLine[-1].isdigit()):
            classLabelVector.append(int(listFromLine[-1]))
        else:
            classLabelVector.append(love_dictionary.get(listFromLine[-1]))
        index += 1
    return returnMat,classLabelVector

     首先读取样本集行数,创建一个列数为3的矩阵(列数为3是因为这里样本集的特征维度是3),创建一个空的标签向量。然后进入循环,先去掉样本集中前后两锻的字符,在用Tab键将各元素分开,得到的一个4列的数组,前三行读取到样本集矩阵中,最后一行读取到标签向量中。

     3.归一化函数

     将样本集的数值进行归一化处理。

函数输入: 样本集

函数输出: 归一化的样本集,取值范围,最小值

函数清单如下:

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))   #element wise divide
    return normDataSet, ranges, minVals

     函数首先取列的最大值和最小值,求两者之差,创建归一化样本集,再减去最小值,除以range。最终得到归一化的样本集。

     4. 测试函数

     有了上述几个函数之后,我们可以构建测试函数,对数据集进行分类。

     函数清单如下:

def datingClassTest():
    hoRatio = 0.10      #hold out 10%
    datingDataMat,datingLabels = file2matrix('datingTestSet2.txt')       #load data setfrom file
    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)

     运行测试函数,首先用file2matrix()函数转化数据,然后读取。将读取到的样本集矩阵数据归一化,读取归一化样本集的行数,取其中10%作为测试集。然后进入循环,用10%的样本集作为测试集,用剩余的90%作为样本集,输入到分类函数classify0()中,最终输出分类结果,真实结果,错误个数,和错误率。

     至此,整个kNN算法运行结束。

三、kNN背后的数学意义

    kNN这个算法运行起来并不难,但是问题是我们为什么要这样做呢,为什么要去找到测试样本最邻近的k个点?然后再把其中出现频率最高的标签作为输出呢?接下来我们来探讨一下其中的数学意义。我们首先要默认一点,就是相似的输入总会有相似的输出,更具这一点开始后面的推导。

    假设有一个样本集n,则其中一个样本x落入R内的概率为:,其中p(x)是x的概率密度函数。设这n个样本中,有k个都落在了范围R内,按照二项式分布,我们可以写出k的期望是k=nP。这时,我们将范围R无限的缩小至一点,则下面的公式可以推出


     根据上述公式,我们就能得到:


     为了获得p(x),需要构造一系列的包含x的区域R,R1,R2,R3,R4,R5,R6.....,也就是要构造不同的V,得到


     n个p(x)的收敛值就是x点处的概率估值。

     对于k-邻近分类,其概率密度的估值为


     这里的2d(x)在一维空间中可以理解为一条线段的长度,二维空间中是一个圆的面积,在三维空间中是一个球体的体积,四维空间中是四维球体的体积。所以可以将其写为:


     最后根据贝叶斯规则:





     上述四个公式分别是,贝叶斯规则,类似然项,先验条件,证据(看不懂的可以下去看一下贝叶斯分类),将后三个公式代入第一个公式,就可以发现p(ci | x)=ki / k,ki是在体积V内属于ci类的样本个数,k是体积V内的所有样本个数。这就是我们为什么要在测试点周围选择k个点,再将出现频率最高的标签作为输出的原因。

     你可能疑惑为什么类似然项中的体积V不是Vi而是V呢?这是因为先验条件是x在ci已经发生的条件下发生的概率,这是ci已经发生了,而ci是包含着c1,c2,c3,c4...等所有情况的这些情况又全部包含在样本n中,所以这里的体积使用V。

猜你喜欢

转载自blog.csdn.net/weixin_39749553/article/details/79702334