网上有很多《机器学习实战》的资料,大多只讲具体操作,内部的原理并没有分析,这篇博文先讲kNN的具体实现,然后在推导一下算法背后的数学原理,注意:(代码是在Python3下实现的,有一点小改动)。
一、算法的工作原理
首先我们需要一个数据集,数据集中包括数据和标签(数据和标签之间的关系一一对应)--该数据集我们称之为样本集,当我们输入没有标签的新数据时,算法将新数据的每个特征和已有样本集对应的特征进行对比,然后提取提取样本集 中与新数据最邻近的特征作为输出。所以我们可以明确算法的输入是一组没有标签的数据,输出是算法针对该组数据给出的标签。
二、代码实现
1. 分类函数实现的步骤:
程序清单如下:
函数输入: (测试向量,样本集,标签机,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。