K近邻(KNN,k-NearestNeighbor)分类算法是数据挖掘分类技术中最简单的方法之一。 所谓K最近邻,就是K个最近的邻居的意思,说的是每个样本都可以用它最接近的k个邻居来代表。KNN算法的核心思想是如果一个样本在特征空间中的K个最相邻的样本中的大多数属于某一个类别,则该样本也属于这个类别,并具有这个类别上样本的特征。该方法在确定分类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。KNN方法在类别决策时,只与极少数的相邻样本有关。由于kNN方法主要靠周围有限的邻近的样本,而不是靠判别类域的方法来确定所属类别的,因此对于类域的交叉或者重叠较多的待分样本集来说,kNN方法较其他方法更为适合。
KNN算法描述:
(1):计算测试数据与各个训练数据之间的距离
(2):按照距离的递增关系进行排序
(3):选取距离最小的K个点
(4):确定前K个点所在类别的出现频率
(5):返回前K个点中出现频率最高的类别作为测试数据的预测分类
K临近算法优缺点:
优点:
- 简单,易于理解,易于实现,无需参数估计,无需训练
- 对异常值不敏感(个别噪音数据对结果的影响不是很大)
- 适合对稀有事件进行分类
- 适合于多分类问题(multi-modal,对象具有多个类别标签),KNN要比SVM表现好
缺点:
- 对测试样本分类时的计算量大,内存开销大,因为对每个待分类的文本都要计算它到全体已知样本的距离,才能求得它的K个最近邻点,目前常用的解决方法是对已知的样本点进行剪辑,事先要去除对分类作用不大的样本
- 可解析性差,无法告诉你哪个变量更重要,无法给出决策树那样的规则
- k值的选择:最大的缺点是当样本不平衡时,如一个类的样本容量很大,而其他样本容量很小时候,有可能导致当输入一个新样本时,该样本的K个邻居中大容量类的样本占多数。该算法只计算“最近的”邻居样本,某一类的样本数量很大的时候,那么或者这类样本并不接近目标样本,或者这类样本很靠近目标样本。无论如何,数量并不影响运行结果,可以采用权值的方法(和该样本距离小的邻居权重大)来改进
- KNN是一种消极学习方法,懒惰算法
K临近算法实战–电影分类:
我们从散点图大致推断,这个红色圆点标记的电影可能属于动作片,因为距离已知的那两个动作片的圆点更近,那么k-近邻算法用什么方法进行判断呢?没错,就是距离度量,这个电影分类的例子有两个特征,也就是二维实数向量空间,可以使用我们学过的两点距离公式计算距离(欧式距离),如下图:
Python代码实现:
1,使用Python导入数据
from numpy import *
import operator
def creataDataSet():
group = array([[1,101],[5,89],[108,5],[115,8]])
labels = ['A','A','B','B'] #labels中 A代表爱情片 B代表动作片
return group,labels
if __name__ == '__main__':
group,labels = creataDataSet()
print(group)
print(labels)
测试输出结果如下:
[[ 1 101]
[ 5 89]
[108 5]
[115 8]]
['A', 'A', 'B', 'B']
2,使用k-近邻算法解析数据
import numpy as np
def classify0(inX,dataSet,labels,k):
dataSetSize = dataSet.shape[0] # shape读取数据矩阵第一维度的长度
diffMat = np.tile(inX,(dataSetSize,1)) - dataSet # tile重复数组inX,有dataSet行 1个dataSet列,减法计算差值
sqDiffMat = diffMat ** 2 # 进行幂运算
sqDistances = sqDiffMat.sum(axis=1) # 普通sum默认参数为axis=0为普通相加,axis=1为一行的行向量相加
distances = sqDistances ** 0.5 #开方得欧氏距离
sortedDistIndicies = distances.argsort() # argsort返回数值从小到大的索引值(数组索引0,1,2,3)
classCount = {}
for i in range(k): # 选择距离最小的k个点
voteLabel = labels[sortedDistIndicies[i]] # 根据排序结果的索引值返回靠近的前k个标签
classCount[voteLabel] = classCount.get(voteLabel,0) +1 # 统计各个标签出现频率
sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True) # reverse默认升序,此时按逆序, key关键字排序itemgetter(1)按第一个域的值排序,即频数
return sortedClassCount[0][0]
classify0()函数有4个输入参数:用于分类的输入向量是inX,输入的训练样本集为dataSet,标签向量为labels,最后的参数k表示用于选择最近邻居的数目,其中标签向量的元素数目和矩阵dataSet的行数相同,上面程式使用欧式距离公式。
计算完所有点之间的距离后,可以对数据按照从小到大的次序排序。然后,确定前k个距离最小元素所在的主要分类,输入k总是正整数;最后,将classCount字典分解为元组列表,然后使用程序第二行导入运算符模块的itemgetter方法,按照第二个元素的次序对元组进行排序。此处的排序是逆序,即按照从最大到最小次序排序,最后返回发生频率最高的元素标签。
3,进行预测
if __name__ == '__main__':
group,labels = creataDataSet()
knn = classify0([101,20],group,labels,3)
print(knn)
结果:
B
K临近算法实战进阶—约会网站匹配
James一直使用在线约会网站寻找适合自己的约会对象。尽管约会网站会推荐不同的人选,但是他从来没有选中喜欢的人,经过一番总结,他发现曾交往过三种类型的人:
- 不喜欢的人
- 魅力一般的人
- 极具魅力的人
1.在约会网站上使用k-近邻算法流程
(1)收集数据:提供文本文件
(2)准备数据:使用Python解析文本文件
(3)分析数据:使用Matplotlib画二维扩散图
(4)训练算法:此步骤不适用于k-近邻算法
(5)测试算法:使用James提供的部分数据作为测试样本
测试样本和非测试样本的区别在于:测试样本是已经完成分类的数据,如果预测分类与实际类别不同,则标记为一个错误。
(6)使用算法:产生简单的命令程序,然后James可以输入一些特征数据以判断对方是否为自己喜欢的类型。
1,准备数据:从文本文件中解析数据
James收集的数据有一段时间了,她将这些数据放在文本文件datingTestSet.txt中,每个样本数据占据一行,总共有1000行,James的样本主要包括以下三种特征:
- 每年获得的飞行常客里程数
- 玩视频游戏所消耗时间百分比
- 每周消费的冰淇淋公升数
在将数据特征数据输入到分类器之前,必须将待处理数据的格式改变为分类器可以接受的格式,我们首先处理输入格式问题,该函数的输入问文件名字符串,输出为训练样本矩阵和类标签向量。
将文本记录到转换Numpy的解析程序代码如下:
def file2Matrix(filename):
fr = open(filename)
arrayOLines = fr.readlines()
numberOfLines = len(arrayOLines) #得到文件行数
returnMat = zeros((numberOfLines,3)) #创建返回的numpy矩阵
classLabelVector = []
index = 0
for line in arrayOLines: #解析文件数据到列表
line = line.strip() #删除首尾空格
listFromLine = line.split('\t') #提取数据
returnMat[index,:] = listFromLine[0:3] #将数据储存在返回矩阵中
classLabelVector.append(int(listFromLine[-1])) #将最后一列储存的类别按序添加到标签
index+=1
return returnMat,classLabelVector #返回样本矩阵和标签
2,分析数据:使用Matplotlib创建散点图
import matplotlib.pyplot as plt
from pylab import mpl
from matplotlib.font_manager import FontProperties
import numpy as np
if __name__ == '__main__':
filename = 'datingTestSet.txt'
datingDataMat,datingLabels = file2Matrix(filename)
mpl.rcParams['font.sans-serif'] = ['FangSong'] # 指定默认字体
mpl.rcParams['axes.unicode_minus'] = False # 解决保存图像是负号- 显示为方块的问题
fig = plt.figure()
ax = fig.add_subplot(111) #画布分成一行一列,第一个位置
zhfont = FontProperties(fname='C:/Windows/Fonts/simsun.ttc',size=12)
datingLabels = np.array(datingLabels)
idx_1 = np.where(datingLabels==1)
p1 = ax.scatter(datingDataMat[idx_1,0],datingDataMat[idx_1,1],marker = '*',color = 'r',label='1',s=10)
idx_2 = np.where(datingLabels==2)
p2 = ax.scatter(datingDataMat[idx_2,0],datingDataMat[idx_2,1],marker = 'o',color ='g',label='2',s=20)
idx_3 = np.where(datingLabels==3)
p3 = ax.scatter(datingDataMat[idx_3,0],datingDataMat[idx_3,1],marker = '+',color ='b',label='3',s=30)
plt.xlabel(u'每年获取的飞行里程数', fontproperties=zhfont)
plt.ylabel(u'玩视频游戏所消耗的事件百分比', fontproperties=zhfont)
ax.legend((p1, p2, p3), (u'不喜欢', u'魅力一般', u'极具魅力'), loc=2, prop=zhfont)
plt.show()
3,归一化数值
表给出了提取的四组数据,如果想要计算样本3和样本4之间的距离,可以使用下面的方法:
计算公式:
我们很容易发现,上面方程中数字差值最大的属性对计算结果的影响最大,也就是说,每年获取的飞行常客里程数对于计算结果的影响远远大于下标中其他两个特征——玩视频游戏的和每周消费冰淇淋公升数的影响。而产生这种现象的唯一原因仅仅是飞行常客里程数远大于其他特征数值。但是James认为这三种特征是同等重要的,因此作为三个等权重的特征之一,飞行常客里程数并不应该如此严重的影响到计算结果
在处理这种不同取值范围的特征值时,我们通常采用的方法是将数值归一化,如将取值范围处理为0到1或者-1到1之间。下面的公式可以将任意取值范围的特征值转化到0到1区间内的值:
newValue = (oldValue - min)/(max-min)
其中min和max分别是数据集中的最小特征值和最大特征值。虽然改变数值取值范围增加了分类器的复杂度,但是为了得到准确结果,我们必须这样做,所以我们新增一个函数autoNorm() ,该函数可以自动将数字特征转化为0到1的区间。
归一化特征值代码如下:
# 归一化特征值
#归一化公式 : (当前值 - 最小值) / range
def autoNorm(dataSet):
minVals = dataSet.min(0) # 存放每列最小值,参数0使得可以从列中选取最小值,而不是当前行
maxVals = dataSet.max(0) # 存放每列最大值
ranges = maxVals - minVals
normDataSet = zeros(shape(dataSet)) # 初始化归一化矩阵为读取的dataSet
m = dataSet.shape[0] # 保留第一行
normDataSet = dataSet - tile(minVals , (m,1)) # 因此采用tile将变量内容复制成输入矩阵同大小
normDataSet = normDataSet/tile(ranges,(m,1)) # 特征值相除,特征矩阵是3*1000 min max range是1*3
return normDataSet,ranges,minVals
4,测试算法
上面我们已经将数据按照需求做了处理,本节我们将测试分类器的效果,如果分类器的正确率满足要求,James就可以使用这个软件来处理约会网站提供的约会名单了。机器学习算法一个很重要的工作就是评估算法的正确率,通常我们只提供已有数据的90%作为训练样本来训练分类器,而使用其余的10%数据来测试分类器,检测分类器的正确率。值得注意的是,10%的测试数据应该是随机选择的,由于James提供的数据并没有按照特定目的来排序,所以我们可以随意选择10%数据而不影响其随机性。
为了测试分类器效果,我们创建了datingClassTest,该函数时自包含的,代码如下:
分类器针对约会网站的测试代码:
# 分类器针对约会网站的测试代码
def datingClassTest():
hoRatio = 0.10
datingDataMat,datingLabels = file2Matrix('datingTestSet2.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 read answer is :%d '
%(classifierResult,datingLabels[i]))
if (classifierResult != datingLabels[i]):
errorCount +=1.0
print("the total error rate is :%f"%(errorCount/float(numTestVecs)))
测试结果:
the total error rate is :0.050000
分类器处理越活数据集的错误率是5%,这是一个相当不错的结果
使用算法
上面我们已经在数据上对分类器进行了测试,现在我们可以使用这个分类器为James来对人们分类,我们会给James给一小段程序,通过该程序会在约会网站上找到某个人并输入他的信息,程序会给出他对对方喜欢程序的预测值。
约会网站预测函数代码:
约会网站预测函数
def classifyPerson():
resultList = ['not at all','in small','in large doses']
percentTats = float(input("percentage of time spent playing video games ?"))
ffMiles = float(input("frequent flier miles earned per year?"))
iceCream = float(input("liters od ice cream consumed per year?"))
dataingDataMat,datingLabels = file2Matrix('datingTestSet2.txt')
normMat,ranges,minVals = autoNorm(datingDataMat)
inArr = array([ffMiles,percentTats,iceCream])
classifierResult = classify0((inArr-minVals)/ranges,normMat,datingLabels,3)
print("You will probably like this person ",resultList[classifierResult-1])
测试结果如下:
if __name__ == '__main__':
# datingClassTest()
filename = 'datingTestSet2.txt'
datingDataMat,datingLabels = file2Matrix(filename)
classifyPerson()
'''
percentage of time spent playing video games ?10
frequent flier miles earned per year?10000
liters od ice cream consumed per year?0.5
You will probably like this person in small
'''