社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
目录
正文
---------------------------------------------------------------------------------------
本系列文章为《机器学习实战》学习笔记,内容整理自书本,网络以及自己的理解,如有错误欢迎指正。
源码在Python3.5上测试均通过,代码及数据 --> https://github.com/Wellat/MLaction
https://github.com/apachecn/AiLearning/tree/master
简单地说,k-近邻算法采用测量不同特征值之间的距离方法进行分类。
优点:精度高、对异常值不敏感、无数据输入假定
缺点:计算复杂度高、空间复杂度高
适用数据范围:数值型和标称型
存在一个训练样本集,并且每个样本都存在标签(有监督学习)。输入没有标签的新样本数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取出与样本集中特征最相似的数据(最近邻)的分类标签。一般来说,我们只选择样本数据集中前k个最相似的数据,这就是k-近邻算法中k的出处,而且k通常不大于20。最后选择k个最相似数据中出现次数最多的分类,作为新数据的分类。
以电影分类为例子,使用k-近邻算法分类爱情片和动作片。有人曾经统计过很多电影的打斗镜头和接吻镜头,下图显示了6部电影的打斗和接吻镜头数。 假如有一部未看过的电影,如何确定它是爱情片还是动作片呢?
①首先需要统计这个未知电影存在多少个打斗镜头和接吻镜头,下图中问号位置是该未知电影出现的镜头数
②之后计算未知电影与样本集中其他电影的距离(相似度),具体算法先忽略,结果如下表所示:
③将相似度列表排序,选出前k个最相似的样本。此处我们假设k=3,将上表中的相似度进行排序后前3分别是:He’s Not Really into Dudes,Beautiful Woman,California Man。
④统计最相似样本的分类。此处很容易知道这3个样本均为爱情片。
⑤将分类最多的类别作为未知电影的分类。那么我们就得出结论,未知电影属于爱情片。
再次解释:
KNN分类算法(K-Nearest-Neighbors Classification),又叫K近邻算法,是一个概念极其简单,而分类效果又很优秀的分类算法。
他的核心思想就是,要确定测试样本属于哪一类,就寻找所有训练样本中与该测试样本“距离”最近的前K个样本,然后看这K个样本大部分属于哪一类,那么就认为这个测试样本也属于哪一类。简单的说就是让最相似的K个样本来投票决定。
这里所说的距离,一般最常用的就是多维空间的欧式距离。这里的维度指特征维度,即样本有几个特征就属于几维。
KNN示意图如下所示。(图片来源:百度百科http://baike.baidu.com/view/1485833.htm?from_id=3479559&type=syn&fromtitle=knn&fr=aladdin)
上图中要确定测试样本绿色属于蓝色还是红色。
显然,当K=3时,将以1:2的投票结果分类于红色;而K=5时,将以3:2的投票结果分类于蓝色。
KNN算法简单有效,但没有优化的暴力法效率容易达到瓶颈。如样本个数为N,特征维度为D的时候,该算法时间复杂度呈O(DN)增长。
所以通常KNN的实现会把训练数据构建成K-D Tree(K-dimensional tree),构建过程很快,甚至不用计算D维欧氏距离,而搜索速度高达O(D*log(N))。
不过当D维度过高,会产生所谓的”维度灾难“,最终效率会降低到与暴力法一样。
因此通常D>20以后,最好使用更高效率的Ball-Tree,其时间复杂度为O(D*log(N))。
人们经过长期的实践发现KNN算法虽然简单,但能处理大规模的数据分类,尤其适用于样本分类边界不规则的情况。最重要的是该算法是很多高级机器学习算法的基础。
当然,KNN算法也存在一切问题。比如如果训练数据大部分都属于某一类,投票算法就有很大问题了。这时候就需要考虑设计每个投票者票的权重了。
2.1.1 算法一般流程
2.1.2 Python实现代码及注释
1 #coding=UTF8
2 from numpy import *
3 import operator
4
5 def createDataSet():
6 """
7 函数作用:构建一组训练数据(训练样本),共4个样本
8 同时给出了这4个样本的标签,及labels
9 """
10 group = array([
11 [1.0, 1.1],
12 [1.0, 1.0],
13 [0. , 0. ],
14 [0. , 0.1]
15 ])
16 labels = ['A', 'A', 'B', 'B']
17 return group, labels
18
19 def classify0(inX, dataset, labels, k):
20 """
21 inX 是输入的测试样本,是一个[x, y]样式的
22 dataset 是训练样本集
23 labels 是训练样本标签
24 k 是top k最相近的
25 """
26 # shape返回矩阵的[行数,列数],
27 # 那么shape[0]获取数据集的行数,
28 # 行数就是样本的数量
29 dataSetSize = dataset.shape[0]
30
31 """
32 下面的求距离过程就是按照欧氏距离的公式计算的。
33 即 根号(x^2+y^2)
34 """
35 # tile属于numpy模块下边的函数
36 # tile(A, reps)返回一个shape=reps的矩阵,矩阵的每个元素是A
37 # 比如 A=[0,1,2] 那么,tile(A, 2)= [0, 1, 2, 0, 1, 2]
38 # tile(A,(2,2)) = [[0, 1, 2, 0, 1, 2],
39 # [0, 1, 2, 0, 1, 2]]
40 # tile(A,(2,1,2)) = [[[0, 1, 2, 0, 1, 2]],
41 # [[0, 1, 2, 0, 1, 2]]]
42 # 上边那个结果的分开理解就是:
43 # 最外层是2个元素,即最外边的[]中包含2个元素,类似于[C,D],而此处的C=D,因为是复制出来的
44 # 然后C包含1个元素,即C=[E],同理D=[E]
45 # 最后E包含2个元素,即E=[F,G],此处F=G,因为是复制出来的
46 # F就是A了,基础元素
47 # 综合起来就是(2,1,2)= [C, C] = [[E], [E]] = [[[F, F]], [[F, F]]] = [[[A, A]], [[A, A]]]
48 # 这个地方就是为了把输入的测试样本扩展为和dataset的shape一样,然后就可以直接做矩阵减法了。
49 # 比如,dataset有4个样本,就是4*2的矩阵,输入测试样本肯定是一个了,就是1*2,为了计算输入样本与训练样本的距离
50 # 那么,需要对这个数据进行作差。这是一次比较,因为训练样本有n个,那么就要进行n次比较;
51 # 为了方便计算,把输入样本复制n次,然后直接与训练样本作矩阵差运算,就可以一次性比较了n个样本。
52 # 比如inX = [0,1],dataset就用函数返回的结果,那么
53 # tile(inX, (4,1))= [[ 0.0, 1.0],
54 # [ 0.0, 1.0],
55 # [ 0.0, 1.0],
56 # [ 0.0, 1.0]]
57 # 作差之后
58 # diffMat = [[-1.0,-0.1],
59 # [-1.0, 0.0],
60 # [ 0.0, 1.0],
61 # [ 0.0, 0.9]]
62 diffMat = tile(inX, (dataSetSize, 1)) - dataset
63
64 # diffMat就是输入样本与每个训练样本的差值,然后对其每个x和y的差值进行平方运算。
65 # diffMat是一个矩阵,矩阵**2表示对矩阵中的每个元素进行**2操作,即平方。
66 # sqDiffMat = [[1.0, 0.01],
67 # [1.0, 0.0 ],
68 # [0.0, 1.0 ],
69 # [0.0, 0.81]]
70 sqDiffMat = diffMat ** 2
71
72 # axis=1表示按照横轴,sum表示累加,即按照行进行累加。
73 # sqDistance = [[1.01],
74 # [1.0 ],
75 # [1.0 ],
76 # [0.81]]
77 sqDistance = sqDiffMat.sum(axis=1)
78
79 # 对平方和进行开根号
80 distance = sqDistance ** 0.5
81
82 # 按照升序进行快速排序,返回的是原数组的下标。
83 # 比如,x = [30, 10, 20, 40]
84 # 升序排序后应该是[10,20,30,40],他们的原下标是[1,2,0,3]
85 # 那么,numpy.argsort(x) = [1, 2, 0, 3]
86 sortedDistIndicies = distance.argsort()
87
88 # 存放最终的分类结果及相应的结果投票数
89 classCount = {}
90
91 # 投票过程,就是统计前k个最近的样本所属类别包含的样本个数
92 for i in range(k):
93 # index = sortedDistIndicies[i]是第i个最相近的样本下标
94 # voteIlabel = labels[index]是样本index对应的分类结果('A' or 'B')
95 voteIlabel = labels[sortedDistIndicies[i]]
96 # classCount.get(voteIlabel, 0)返回voteIlabel的值,如果不存在,则返回0
97 # 然后将票数增1
98 classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
99
100 # 把分类结果进行排序,然后返回得票数最多的分类结果
101 sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
102 return sortedClassCount[0][0]
103
104 if __name__== "__main__":
105 # 导入数据
106 dataset, labels = createDataSet()
107 inX = [0.1, 0.1]
108 # 简单分类
109 className = classify0(inX, dataset, labels, 3)
110 print('the class of test sample is %s' %className)
2.2.1 算法一般流程
2.2.2 Python实现代码
datingTestSet.txt 文件中有1000行的约会数据,样本主要包括以下3种特征:
将上述特征数据输人到分类器之前,必须将待处理数据的格式改变为分类器可以接受的格式 。在kNN.py中创建名为 file2matrix 的函数,以此来处理输人格式问题。该函数的输人为文件名字符串,输出为训练样本矩阵和类标签向量。autoNorm 为数值归一化函数,将任意取值范围的特征值转化为0到1区间内的值。最后,datingClassTest 函数是测试代码。
将下面的代码增加到 kNN.py 中。
1 def file2matrix(filename):
2 """
3 从文件中读入训练数据,并存储为矩阵
4 """
5 fr = open(filename)
6 arrayOlines = fr.readlines()
7 numberOfLines = len(arrayOlines) #获取 n=样本的行数
8 returnMat = zeros((numberOfLines,3)) #创建一个2维矩阵用于存放训练样本数据,一共有n行,每一行存放3个数据
9 classLabelVector = [] #创建一个1维数组用于存放训练样本标签。
10 index = 0
11 for line in arrayOlines:
12 # 把回车符号给去掉
13 line = line.strip()
14 # 把每一行数据用t分割
15 listFromLine = line.split('t')
16 # 把分割好的数据放至数据集,其中index是该样本数据的下标,就是放到第几行
17 returnMat[index,:] = listFromLine[0:3]
18 # 把该样本对应的标签放至标签集,顺序与样本集对应。
19 classLabelVector.append(int(listFromLine[-1]))
20 index += 1
21 return returnMat,classLabelVector
22
23 def autoNorm(dataSet):
24 """
25 训练数据归一化
26 """
27 # 获取数据集中每一列的最小数值
28 # 以createDataSet()中的数据为例,group.min(0)=[0,0]
29 minVals = dataSet.min(0)
30 # 获取数据集中每一列的最大数值
31 # group.max(0)=[1, 1.1]
32 maxVals = dataSet.max(0)
33 # 最大值与最小的差值
34 ranges = maxVals - minVals
35 # 创建一个与dataSet同shape的全0矩阵,用于存放归一化后的数据
36 normDataSet = zeros(shape(dataSet))
37 m = dataSet.shape[0]
38 # 把最小值扩充为与dataSet同shape,然后作差,具体tile请翻看 第三节 代码中的tile
39 normDataSet = dataSet - tile(minVals, (m,1))
40 # 把最大最小差值扩充为dataSet同shape,然后作商,是指对应元素进行除法运算,而不是矩阵除法。
41 # 矩阵除法在numpy中要用linalg.solve(A,B)
42 normDataSet = normDataSet/tile(ranges, (m,1))
43 return normDataSet, ranges, minVals
44
45 def datingClassTest():
46 # 将数据集中10%的数据留作测试用,其余的90%用于训练
47 hoRatio = 0.10
48 datingDataMat,datingLabels = file2matrix('datingTestSet2.txt') #load data setfrom file
49 normMat, ranges, minVals = autoNorm(datingDataMat)
50 m = normMat.shape[0]
51 numTestVecs = int(m*hoRatio)
52 errorCount = 0.0
53 for i in range(numTestVecs):
54 classifierResult = classify0(normMat[i,:],normMat[numTestVecs:m,:],datingLabels[numTestVecs:m],3)
55 print("the classifier came back with: %d, the real answer is: %d, result is :%s" % (classifierResult, datingLabels[i],classifierResult==datingLabels[i]))
56 if (classifierResult != datingLabels[i]): errorCount += 1.0
57 print("the total error rate is: %f" % (errorCount/float(numTestVecs)))
58 print(errorCount)
2.3.1 实例数据
为了简单起见,这里构造的系统只能识别数字0到9。需要识别的数字已经使用图形处理软件,处理成具有相同的色彩和大小 : 宽髙是32像素x 32像素的黑白图像。尽管采用文本格式存储图像不能有效地利用内存空间,但是为了方便理解,我们还是将图像转换为文本格式。
trainingDigits是2000个训练样本,testDigits是900个测试样本。
2.3.2 算法的流程
2.3.3 Python实现代码
将下面的代码增加到 kNN.py 中,img2vector 为图片转换成向量的方法,handwritingClassTest 为测试方法:
1 from os import listdir
2 def img2vector(filename):
3 """
4 将图片数据转换为01矩阵。
5 每张图片是32*32像素,也就是一共1024个字节。
6 因此转换的时候,每行表示一个样本,每个样本含1024个字节。
7 """
8 # 每个样本数据是1024=32*32个字节
9 returnVect = zeros((1,1024))
10 fr = open(filename)
11 # 循环读取32行,32列。
12 for i in range(32):
13 lineStr = fr.readline()
14 for j in range(32):
15 returnVect[0,32*i+j] = int(lineStr[j])
16 return returnVect
17
18 def handwritingClassTest():
19 hwLabels = []
20 # 加载训练数据
21 trainingFileList = listdir('trainingDigits')
22 m = len(trainingFileList)
23 trainingMat = zeros((m,1024))
24 for i in range(m):
25 # 从文件名中解析出当前图像的标签,也就是数字是几
26 # 文件名格式为 0_3.txt 表示图片数字是 0
27 fileNameStr = trainingFileList[i]
28 fileStr = fileNameStr.split('.')[0] #take off .txt
29 classNumStr = int(fileStr.split('_')[0])
30 hwLabels.append(classNumStr)
31 trainingMat[i,:] = img2vector('trainingDigits/%s' % fileNameStr)
32 # 加载测试数据
33 testFileList = listdir('testDigits') #iterate through the test set
34 errorCount = 0.0
35 mTest = len(testFileList)
36 for i in range(mTest):
37 fileNameStr = testFileList[i]
38 fileStr = fileNameStr.split('.')[0] #take off .txt
39 classNumStr = int(fileStr.split('_')[0])
40 vectorUnderTest = img2vector('testDigits/%s' % fileNameStr)
41 classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3)
42 print("the classifier came back with: %d, the real answer is: %d, The predict result is: %s" % (classifierResult, classNumStr, classifierResult==classNumStr))
43 if (classifierResult != classNumStr): errorCount += 1.0
44 print("nthe total number of errors is: %d / %d" %(errorCount, mTest))
45 print("nthe total error rate is: %f" % (errorCount/float(mTest)))
k-近邻算法识别手写数字数据集,错误率为1. 2%。改变变量k的值、修改函数 handwritingClassTest 随机选取训练样本、改变训练样本的数目,都会对k-近邻算法的错误率产生影响,感兴趣的话可以改变这些变量值,观察错误率的变化。
k-近邻算法是分类数据最简单最有效的算法。它必须保存全部数据集,如果训练数据集很大,必须使用大量的存储空间。此外,由于必须对数据集中的每个数据计算距离值,实际使用时可能非常耗时。其另一个缺陷是它无法给出任何数据的基础结构信息,因此我们也无法知晓平均实例样本和典型实例样本具有什么特征。
#!/usr/bin/python
# -*- coding: UTF-8 -*-
"""
Created on 2017-06-28
Updated on 2017-06-28
KNN:k近邻算法
@author: 小瑶
《机器学习实战》更新地址:https://github.com/apachecn/AiLearning
"""
print(__doc__)
import numpy as np
import matplotlib.pyplot as plt
from numpy import *
from matplotlib.colors import ListedColormap
from sklearn import neighbors, datasets
n_neighbors = 3
# 导入一些要玩的数据
iris = datasets.load_iris()
X = iris.data[:, :2] # 我们只采用前两个feature. 我们可以使用二维数据集避免这个丑陋的切片
y = iris.target
# print 'X=', type(X), X
# print 'y=', type(y), y
# X = array([[-1.0, -1.1], [-1.0, -1.0], [0, 0], [1.0, 1.1], [2.0, 2.0], [2.0, 2.1]])
# y = array([0, 0, 0, 1, 1, 1])
# print 'X=', type(X), X
# print 'y=', type(y), y
h = .02 # 网格中的步长
# 创建彩色的图
cmap_light = ListedColormap(['#FFAAAA', '#AAFFAA', '#AAAAFF'])
cmap_bold = ListedColormap(['#FF0000', '#00FF00', '#0000FF'])
# cmap_light = ListedColormap(['#FFAAAA', '#AAFFAA'])
# cmap_bold = ListedColormap(['#FF0000', '#00FF00'])
for weights in ['uniform', 'distance']:
# 我们创建了一个knn分类器的实例,并拟合数据。
clf = neighbors.KNeighborsClassifier(n_neighbors, weights=weights)
clf.fit(X, y)
#训练准确率:
score = clf.score(X, y)
print("score:%fn" % score)
#score = clf.score(iris.data, iris.target)
#预测
# 预测
predict = clf.predict([[0.1, 0.2]])
# 预测,返回概率数组
predict2 = clf.predict_proba([[0.1, 0.2]])
print(predict)
print(iris.target_names[predict])
# 绘制决策边界。为此,我们将为每个分配一个颜色
# 来绘制网格中的点 [x_min, x_max]x[y_min, y_max].
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
np.arange(y_min, y_max, h))
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
# 将结果放入一个彩色图中
Z = Z.reshape(xx.shape)
plt.figure()
plt.pcolormesh(xx, yy, Z, cmap=cmap_light)
# 绘制训练点
plt.scatter(X[:, 0], X[:, 1], c=y, cmap=cmap_bold)
plt.xlim(xx.min(), xx.max())
plt.ylim(yy.min(), yy.max())
plt.title("3-Class classification (k = %i, weights = '%s')"
% (n_neighbors, weights))
plt.show()
https://blog.csdn.net/qq_36330643/article/details/77532161
四、KNN的优缺点
(1)优点
①简单,易于理解,易于实现,无需参数估计,无需训练;
②精度高,对异常值不敏感(个别噪音数据对结果的影响不是很大);
③适合对稀有事件进行分类;
④特别适合于多分类问题(multi-modal,对象具有多个类别标签),KNN要比SVM表现要好.
(2)缺点
①对测试样本分类时的计算量大,空间开销大,因为对每一个待分类的文本都要计算它到全体已知样本的距离,才能求得它的K个最近邻点。目前常用的解决方法是事先对已知样本点进行剪辑,事先去除对分类作用不大的样本;
②可解释性差,无法给出决策树那样的规则;
③最大的缺点是当样本不平衡时,如一个类的样本容量很大,而其他类样本容量很小时,有可能导致当输入一个新样本时,该样本的K个邻居中大容量类的样本占多数。该算法只计算“最近的”邻居样本,某一类的样本数量很大,那么或者这类样本并不接近目标样本,或者这类样本很靠近目标样本。无论怎样,数量并不能影响运行结果。可以采用权值的方法(和该样本距离小的邻居权值大)来改进;
④消极学习方法。
五、对k-近邻算法的说明
按距离加权的k-近邻算法是一种非常有效的归纳推理方法。它对训练数据中的噪声有很好的鲁棒性,而且当给定足够大的训练集合时它也非常有效。注意通过取k个近邻的加权平均,可以消除孤立的噪声样例的影响。
问题一:近邻间的距离会被大量的不相关属性所支配。
应用k-近邻算法的一个实践问题是,实例间的距离是根据实例的所有属性(也就是包含实例的欧氏空间的所有坐标轴)计算的。这与那些只选择全部实例属性的一个子集的方法不同,例如决策树学习系统。
比如这样一个问题:每个实例由20个属性描述,但在这些属性中仅有2个与它的分类是有关。在这种情况下,这两个相关属性的值一致的实例可能在这个20维的实例空间中相距很远。结果,依赖这20个属性的相似性度量会误导k-近邻算法的分类。近邻间的距离会被大量的不相关属性所支配。这种由于存在很多不相关属性所导致的难题,有时被称为维度灾难(curse of dimensionality)。最近邻方法对这个问题特别敏感。
解决方法:当计算两个实例间的距离时对每个属性加权。
这相当于按比例缩放欧氏空间中的坐标轴,缩短对应于不太相关属性的坐标轴,拉长对应于更相关的属性的坐标轴。每个坐标轴应伸展的数量可以通过交叉验证的方法自动决定。
问题二:应用k-近邻算法的另外一个实践问题是如何建立高效的索引。因为这个算法推迟所有的处理,直到接收到一个新的查询,所以处理每个新查询可能需要大量的计算。
解决方法:目前已经开发了很多方法用来对存储的训练样例进行索引,以便在增加一定存储开销情况下更高效地确定最近邻。一种索引方法是kd-tree(Bentley 1975;Friedman et al. 1977),它把实例存储在树的叶结点内,邻近的实例存储在同一个或附近的结点内。通过测试新查询xq的选定属性,树的内部结点把查询xq排列到相关的叶结点。
问题三:当样本不平衡时,如一个类的样本容量很大,而其他类样本容量很小时,有可能导致当输入一个新样本时,该样本的K个邻居中大容量类的样本占多数。该算法只计算“最近的”邻居样本,某一类的样本数量很大,那么或者这类样本并不接近目标样本,或者这类样本很靠近目标样本。
解决方法:无论怎样,数量并不能影响运行结果。可以采用权值的方法(和该样本距离小的邻居权值大)来改进,距离加权。
比较
典型算法
机器学习包sklearn中有最近邻 from sklearn import neighbors, datasets
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!