序言
之前复习了好几次k-NN算法,总想找个时间将这段内容记录下来,这次借着整理机器学习内容的机会,将它完整的梳理一遍,希望能够让自己日后温习更加方便。
这篇笔记是按照李航老师的统计学习方法这本书梳理下来的,其中夹杂的一些例子是在学习过程中为了更好的理解算法所想出的,可能不是很恰当。另外通过参考其他资料,例如机器学习实战和周志华老师的机器学习,还加入了一些别的内容,所以可能形式上有点散乱。
k-NN算法算是机器学习最基础的一部分内容。这篇笔记将详细的介绍k-NN算法的原理和python代码实现。
k近邻算法简介
k近邻法(k-nearest neighbor)是一种基本分类与回归方法,也是一种很基本的机器学习方法,具有简单直观的特点。
这里我们借用李航博士在书中对k-NN的描述:给定一个训练数据集,对新的输入实例,在训练数据集中找到与该实例最邻近的k个实例,这k个实例的多数属于某个类,就把该输入实例分为这个类。
k近邻法
k近邻分类算法代码实现(python版)
下面我们来通过一些实际问题,来更好的理解k近邻的使用
先来看一个简单的例子,假设我们买了个西瓜,现在要从以往的购买经历中,来判断我们买的这个瓜是好瓜还是坏瓜。
以往购买瓜的经历绘制成的表格
瓜的长度 | 瓜的宽度 | 瓜的质量 |
---|---|---|
30 | 20 | 坏瓜 |
35 | 30 | 坏瓜 |
40 | 30 | 好瓜 |
43 | 31 | 好瓜 |
我们新买到的瓜的数据
瓜的长度 | 瓜的宽度 | 瓜的质量 |
---|---|---|
41 | 30 | ? |
将数据绘制到图中
瓜的长度为41,宽度为30,通过k近邻来求瓜的质量
我们来分析问题,实现k近邻,首先应该获取训练数据集
import numpy as np
def createData():
group = np.array([[30,20],[35,30],[40,30],[43,31]])
labels = ["坏瓜", "坏瓜", "好瓜", "好瓜"]
return group,labels
获取数据后下一步就应该实现k-NN算法了
import numpy as np
import operator
def createData():
group = np.array([[30,20],[35,30],[40,30],[43,31]])
labels = ["坏瓜", "坏瓜", "好瓜", "好瓜"]
return group,labels
//inX表示输入实例,group表示训练数据集中属性,labels表示训练数据集中类别,k为近邻点的个数
def kNN(inX,group,labels,k):
//先获取训练数据集数据的个数
dataSize = group.shape[0]
dataDiff = np.tile(inX, (dataSize, 1)) - group
//以下几步计算训练数据集中各个点到输入实例的欧式距离
distance = dataDiff ** 2
distance = distance.sum(axis=1)
distance = distance ** 0.5
//将距离从小到大进行排列
sortDistance = distance.argsort()
classCount = {}
for i in range(k):
//选出第i个点所属的类别
klabel = labels[sortDistance[i]]
//为该类别加一
classCount[klabel] = classCount.get(klabel, 0) + 1
//选择出k个近邻点所属类别最多的那个类
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
group, labels = createData()
print(kNN([41,30],group,labels,3))
运行结果为:
好瓜
以上实例只是一个较为简单的例子,下面我们来探讨这样一个问题
上面这个例题中我们选取的两个西瓜属性是瓜的长度和宽度,这两个属性对于瓜的好坏的影响我们在设计程序时认为是等价的,原因是这两个属性差值的范围相差不大。那么如果我们现在选取的属性为瓜的长度和瓜的重量,经过统计后绘制成表格
瓜的长度 | 瓜的重量 | 瓜的质量 |
---|---|---|
30 | 6 .5 | 坏瓜 |
35 | 7.6 | 坏瓜 |
40 | 8 | 好瓜 |
43 | 8.5 | 好瓜 |
我们买的瓜的数据为
瓜的长度 | 瓜的重量 | 瓜的质量 |
---|---|---|
41 | 8 | ? |
将表格中的数据绘制成图像
我们可以看出,对于瓜的长度,训练数据集与输入实例之间的数字差值为,分别为11,6,1,2
对于瓜的重量,训练数据集与输入实例之间的数字差值为1.5,0.4,0,0.5
结合图像来看,如果我们采用欧式距离,由于瓜的长度这一属性数字差值要大于瓜的重量这一属性的数字差值,这就导致了瓜的长度对于计算结果的影响将大于瓜的质量。但我们在判断是否为好瓜时,希望这两个属性对于判断瓜的好坏时处于同等地位的,所以我们要进行归一化。
归一化将所有属性的取值范围处理为0到1之间。使用以下公式:
新的属性值=(原属性值-该属性的最小值)/(该属性的最大值-该属性的最小值)
接下来我们用代码实现
import numpy as np
import operator
def createData():
group = np.array([[30,6.5],[35,7.6],[40,8],[43,8.5]])
labels = ["坏瓜", "坏瓜", "好瓜", "好瓜"]
return group,labels
def kNN(inX,group,labels,k):
dataSize = group.shape[0]
dataDiff = np.tile(inX, (dataSize, 1)) - group
distance = dataDiff ** 2
distance = distance.sum(axis=1)
distance = distance ** 0.5
sortDistance = distance.argsort()
classCount = {}
for i in range(k):
klabel = labels[sortDistance[i]]
classCount[klabel] = classCount.get(klabel, 0) + 1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
//归一化代码
def autoNorm(group):
minValues = group.min(0)
maxValues = group.max(0)
ranges = maxValues - minValues
normDataSet = np.zeros(np.shape(group))
dataSize = group.shape[0]
normDataSet = group - np.tile(minValues,(dataSize,1))
normDataSet = normDataSet / np.tile(ranges, (dataSize, 1))
return normDataSet
group, labels = createData()
group = autoNorm(group)
print(kNN([41, 8], group, labels, 3))
运行结果:
好瓜
距离度量与k值的选择
距离度量
特征空间中两个实例点的距离是两个实例点相似程度的反应。
在以上两个问题中,我们采用的都是欧式距离。除了欧式距离外,常用的还有曼哈顿距离,切比雪夫距离等
明白了各个距离计算方法,我们就用python代码来实现一下,我们先看前面问题中计算欧式距离的代码
dataDiff = np.tile(inX, (dataSize, 1)) - group
distance = dataDiff ** 2
distance = distance.sum(axis=1)
distance = distance ** 0.5
同理,曼哈顿距离应该为
dataDiff = np.tile(inX,(dataSize,1)) - group
distance = math.sqrt(dataDiff ** 2)
distance = distance.sum(axis=1)
切比雪夫距离为
dataDiff = np.tile(inX,(dataSize,1)) - group
distance = distance.max(axis=1)
对于不同的问题我们采取不同的距离度量,具体哪种情况下使用,先不做过多介绍。
k值的选择
k值的选择对于k近邻算法来说至关重要,若果k值过小,预测结果将会对邻近的实例点非常敏感,如果邻近的实例点恰好是噪声,那么预测结果就非常容易出错。
下面我们从图中理解一下这个问题
在这个问题中,如果k取1,距离小黑点最近点是小蓝点,所以算法将判定输入实例为坏瓜。
但是我们从图中根据黑点周围的其他点可以判断出,输入实例应当是好瓜,但是由于k取值过小,将噪声学习了进来。所以k的取值不应当过小。
那如果我们将k的取值调的大一点呢,我们不妨极端一点,将k的值设为11,也就是说包含了所有的训练数据集中的点,这样的话,整个模型变得十分简单,与输入实例距离非常远的点也会被计算在内,误差就会增大。
在应用中,k值应该案取一个较小的数值,通常采用交叉验证法来选取最优k值
下面来看什么是交叉验证法
交叉验证法现将数据集划分为k个大小相似的互斥子集,每个子集使用分层抽样取得,这样就可以尽可能保持数据分布的一致性,然后每次用k-1个子集的并集作为训练集,剩下的那个子集作为测试集,然后进行k次测试。
下面是代码实现
import operator
import numpy as np
from sklearn.model_selection import StratifiedKFold
def createDataSet():
group = np.array([[30, 10], [31, 20], [32, 30], [20, 10], [20, 20], [24, 30],
[50,14],[52,19],[53,36],[54,33],[24,24]])
labels = ['good', 'good', 'good', 'good', 'good', 'good', 'bad', 'bad',
'bad', 'bad','bad']
return group, labels
def classify0(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):
//获取第i个近邻点的类别
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]
/*
本函数用来测试确定训练集,测试集和k值后经测试的错误率
trainData:训练集属性
testData:测试集属性
trainlabels:训练集结果
testlabels:测试集结果
k:选取的近邻点个数
*/
def comapre(trainData, testData, trainLabels, testLabels, k):
falseNum = 0
//测试集中错误的个数
for i in range(len(testData)):
lLabel = classify0(testData[i], trainData, trainLabels, k)
if lLabel!=testLabels[i]:
falseNum += 1
//返回的是错误率
return falseNum/len(testLabels)
/*
dataSet:属性集
labels:结果集
*/
def crossVal(dataSet, labels):
//四折折交叉验证
kf = StratifiedKFold(n_splits=4)
kf.get_n_splits(labels)
smallFalse = 10000
//设置不同k值时的错误率之和
best_k = 1
//设置错误率之和最低时的k值
//k值得我们假设从1到7里选择
for k in range(1,7):
print(k)
avgFalse = 0
//错误率之和
for train,test in kf.split(dataSet,labels):
falseNum = comapre(np.array(dataSet)[train], np.array(dataSet[test]), np.array(labels)[train], np.array(labels)[test], k)
avgFalse += falseNum
print(avgFalse)
if smallFalse > avgFalse:
smallFalse = avgFalse
best_k = k
return best_k
group, labels = createDataSet()
//输出结果
print(crossVal(group,labels))
运行结果为:
3
分类决策规则
k近邻方法中的分类决策规则往往是多数表决,即由输入实例的k个邻近的训练实例中的多数类决定输入实例的类。这个知识点不过多的展开
k近邻回归算法代码实现(python版)
下面我们开始用k近邻算法处理回归问题。
我们先来看看什么是分类?什么是回归?
如果我们预测的是离散值,那么此类学习任务就是分类,例如知道了瓜的长度和宽度,来判断瓜的好坏,只有好瓜和坏瓜两种可能。
如果我们预测的是连续值,那么此类学习任务就是回归,例如知道了瓜的长度和宽度,来判断瓜的价格,瓜的价格可以是20元,21元,22元
理解了两者的区别,我们来看这样一个问题,从以往购买瓜的数据中,来判断一个西瓜的价格。
这是以往购买瓜的信息绘制成的表格。
瓜的长度 | 瓜的宽度 | 瓜的价格 |
---|---|---|
30 | 20 | 6 |
30 | 25 | 6.5 |
30 | 27 | 6.8 |
34 | 23 | 7 |
37 | 22 | 7.5 |
40 | 27 | 8 |
40 | 31 | 8.5 |
绘制到图中:
import numpy as np
import operator
def createData():
group = np.array([[30,20],[30,25],[30,27],[34,23],[37,22],[40,27],[40,31]])
labels = [6,6.5,6.8,7,7.5,8,8.5]
return group,labels
def kNN(inX,group,labels,k):
dataSize = group.shape[0]
dataDiff = np.tile(inX, (dataSize, 1)) - group
distance = dataDiff ** 2
distance = distance.sum(axis=1)
distance = distance ** 0.5
sortDistance = distance.argsort()
classCount = []
for i in range(k):
klabel = labels[sortDistance[i]]
classCount.append(klabel)
moneySum = 0
for i in range(0, len(classCount)):
moneySum += classCount[i]
return moneySum/len(classCount)
group, labels = createData()
print(kNN([24,24],group,labels,3))
在代码中我们选取的k值为3,我们将距离输入示例最近的三个点转化为绿色
将这三个点的价格进行平均,即得到了输入实例的价格
运算结果为:
6.433333333333334
该算法预测输入实例的价格为6.43元
参考
1.李航,统计学习方法,清华大学出版社
2.Peter Harrington 机器学习实战,人民邮电出版社
3.周志华,机器学习,清华大学出版社