一、K-近邻算法概念
1、书上概念:
K-近邻算法采用不同特征值之间的距离的方法进行分类
2、算法介绍:
- 计算测试数据与各个训练数据之间的距离;
- 按照距离的递增关系进行排序;
- 选取距离最小的K个点;
- 确定前K个点所在类别的出现的频率;
- 返回钱K个点中出现频率最高的类别作为测试数据的预测分类。
3、怎么算:
比如:如下这张图, 图中有两种点,一种是蓝色的正方形,一种是红色的三角,在图片的中间有一个未知的绿色圆点,请问这个绿色远点应该是蓝色的正方形,还是红色的三角形?
在这里:
- 首先将绿色远点与整个图中所有的蓝色正方形以及红色三角形进行距离的计算;
- 按照绿点与每个点的距离进行递增排序;
- 选取距离最小的K个点;
- 在这K个点中,确定到底是蓝色还是红色出现的频率,也就是次数更多;
- 如果红色多,则返回红色,反之返回红色。
注意:
- 在K决定的过程中,需要根据实际情况进行确定,比如这里的K如果取3,则在这个范围内的红色点多,那么这个未知点就应该是红色的三角,如果K取5,则这个范围中的蓝色点居多,则未知点应该是蓝色的正方形。
- 在K进行取值判断的时候,K应当取一个奇数值,即如果取偶数值,有可能会发生两个的比例相同的情况,如这个图中,如果K=4,则红色和蓝色的比例将相等,则无法进行有效判断。
4、距离如何计算:
在K-临近算法中,距离通常使用欧氏距离:
L 2 ( a , b ) = ( ∑ i = 1 n ∣ a i − b i ∣ 2 ) 1 2 L_2(a, b)=(\sum_{i=1}^n \ |a_i-b_i|^2)^\frac{1}{2} \ L2(a,b)=(i=1∑n ∣ai−bi∣2)21
通俗来讲,就是计算两个点的直线距离,比如
求二维坐标轴中点a(1, 4)和点 b(4, 8)的欧氏距离,则:
s = ∣ 1 − 4 ∣ 2 + ∣ 4 − 8 ∣ 2 = 5 s = \sqrt{|1-4|^2+|4-8|^2} \ = 5 s=∣1−4∣2+∣4−8∣2 =5
所以,a、b两点的直线距离为5,同理,高维的点的计算方法依旧是各个对应点的差的平方和,最后再开方。
二、数据的归一化处理
1、什么是数据归一化
在《机器学习实战》书中,K-近邻算法为我们提供了一个改进约会网站的配对效果的案例,在书中,作者为我们提供了相应的数据:
数据含义:
- 每年获得的飞行常客里程数
- 玩视频游戏所耗时间百分比
- 每周消费的冰淇淋公升数
- 最右一列表示:1-不喜欢的人, 2-魅力一般的人, 3-极具魅力的人
在这里,如果有一个需要测试的数据为:
里程数 | 时间百分比 | 公升数 |
---|---|---|
1234 | 6.24134 | 1.12345 |
在计算过程中,首先我们需要将该数据与提供的训练数据中的每个数据进行欧氏距离计算,在这个过程中,我们就能发现,每一个特征值的单位以及数据的大小几乎完全不一样,所以最终的结果可能会因为某一个数据过大或过小导致最终的结果出现偏差。因此,我们需要引入数据归一化的概念。
2、怎么进行数据归一化处理
我们在处理不同取值范围的特征值时,我们通常讲取值范围处理为0到1或者-1到1,具体公式如下:
n
e
w
V
a
l
u
e
(
归
一
化
后
的
数
据
)
=
o
l
d
V
a
l
u
e
(
原
数
据
)
−
m
i
n
(
该
列
数
据
最
小
值
)
m
a
x
(
该
列
数
据
最
大
值
)
−
m
i
n
(
该
列
数
据
最
小
值
)
newValue(归一化后的数据)=\frac{oldValue(原数据)-min(该列数据最小值)}{max(该列数据最大值)-min(该列数据最小值)} \
newValue(归一化后的数据)=max(该列数据最大值)−min(该列数据最小值)oldValue(原数据)−min(该列数据最小值)
在该例中,比如求饿数据的处理:
里 程 数 = 1234 − 5569 77372 − 5569 = − 0.06037352199768812 里程数=\frac{1234-5569}{77372-5569} \ =-0.06037352199768812 里程数=77372−55691234−5569 =−0.06037352199768812
时 间 百 分 比 = 6.24134 − 0 15.299570 − 0 = 0.40794218399602084 时间百分比=\frac{6.24134-0}{15.299570-0} \ =0.40794218399602084 时间百分比=15.299570−06.24134−0 =0.40794218399602084
公 升 数 = 1.12345 − 0.134296 1.686209 − 0.134296 = 0.6373772241098566 公升数=\frac{1.12345-0.134296}{1.686209-0.134296} \ =0.6373772241098566 公升数=1.686209−0.1342961.12345−0.134296 =0.6373772241098566
此时,可以发现三个特征值都处于-1到1之间,同时将所有的数据进行相应的处理后,将会去除因为大小导致的偏差。此时,在进行欧式距离计算将会使数据在同等的单位下进行大小运算,得到的结果将会比原结果更加准确。
3、归一化代码的实现
在这个地方,归一化的方法已经给出了,那么就是针对代码对数据进行归一化实现,具体代码书中已经给出,如下:
def autoNorm(dataSet):
minVals = dataSet.min(0) # 求数据中各行的最小值
maxVals = dataSet.max(0) # 求数据中各行的最大值
ranges = maxVals - minVals # 取最大值与最小值的差值,也就是求分母
normDataSet = zeros(shape(dataSet)) # 创建一个全为0的维度和dataSet一样的矩阵
m = dataSet.shape[0] # 输出dataSet的第一维的个数
normDataSet = dataSet - tile(minVals, (m, 1)) # 生成数据的差值,也就是分子
normDataSet = normDataSet / tile(ranges, (m, 1)) # 使用分子对应除以分母,并将结果保存到normDataSet中
return normDataSet, ranges, minVals
当然,不同情况下的数据,在处理的时候使用的方法也不一样,不过大致思路都会是一样的,后续中在学习到相应位置时,我还会补充其他的归一化的方法。
三、实例:手写识别系统
我会在本节同大家一起学习《机器学习实战》中的K-近邻算法中的示例:手写识别系统。
1、什么是手写识别系统
在《机器学习实战》中为我们提供了已经经过归一化后的数据集以及测试集,具体的样式如下图:
为了简单起见,该构造的系统只能识别数字0~9,并且每个数字是由32像素*32像素的黑白图像,为了方便,使用文本进行存储,即相当于一个32*32的矩阵,里面由0和1构成,其中由1组成数字的图像。
点击此处下载书中相关的数据集以及代码,同时里面也带有《机器学习实战》英文版和中文版PDF电子书(侵权请告知)。
在书中提供的数据集里,大约提供了2000个例子,每个数字大约200个,因为作者对我们的数据进行了处理,所以测试集以及训练集中的例子都不会出现重叠。
我们需要对获取训练集的数据进行学习,最后对测试集进行测试并判断错误率,以此对该分类器的准确度进行判断。
2、怎么做
在前面的内容中,我们讲到了K-近邻算法的计算方法,在这里,因为每一个数据都是由32*32的矩阵组成,所以我们首先需要将矩阵转化为一个1*1024的向量,这样可以方便我们的分类器对数字图像信息的处理。
下面一段代码img2vector,将图像转换为向量,过程如下:
- 创建1*1024的NumPy数组
- 打开相应数据的文件
- 循环读取文件的前32行,并将每行的头32个字符值存储在已经创建好的NumPy数组中
- 返回数组
def img2vector(filename):
returnVect = zeros((1, 1024))
fr = open(filename)
for i in range(32):
lineStr = fr.readline()
for j in range(32):
returnVect[0, 32*i + j] = int(lineStr[j])
return returnVect
我们可以对代码进行一些简单的测试:
>>> testVector = img2vector(‘testDigits/0_13.txt’)
>>> testVector[0, 0:31]
array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1.,
1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
>>> testVector[0, 32:63]
array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1.,
1., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
接下来就是对分类器进行编写,上面的img2vector()函数已经帮我们将数据进行了一些前置的处理,所以我们只需要编写分类器,并且进行测试即可,具体代码如下:
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]
def handwritingClassTest():
hwLabels = []
trainingFileList = listdir('trainingDigits')
m = len(trainingFileList)
trainingMat = zeros((m, 1024))
for i in range(m):
fileNameStr = trainingFileList[i]
fileStr = fileNameStr.split('.')[0]
classNumStr = int(fileStr.split('_')[0])
hwLabels.append(classNumStr)
trainingMat[i, :] = img2vector('trainingDigits/' + fileNameStr)
testFileList = listdir('testDigits')
errorCount = 0.0
mTest = len(testFileList)
for i in range(mTest):
fileNameStr = testFileList[i]
fileStr = fileNameStr.split('.')[0]
classNumStr = int(fileStr.split('_')[0])
vectorUnderTest = img2vector('testDigits/' + fileNameStr)
classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3)
print("the classifier came back with: " + str(classifierResult) + ",the real answer is: " + str(classNumStr))
if classifierResult != classNumStr:
errorCount += 1.0
print("\nthe total number of error is: ", str(errorCount))
print("\nthe total error rate is: ", str(errorCount/float(mTest)))
测试结果如下:
>>> handwritingClassTest()
the classifier came back with: 4,the real answer is: 4
the classifier came back with: 4,the real answer is: 4
the classifier came back with: 3,the real answer is: 3
the classifier came back with: 9,the real answer is: 9
the classifier came back with: 0,the real answer is: 0
the classifier came back with: 0,the real answer is: 0
the classifier came back with: 9,the real answer is: 9
.
.
the classifier came back with: 5,the real answer is: 5
the classifier came back with: 4,the real answer is: 4
the classifier came back with: 3,the real answer is: 3
the classifier came back with: 3,the real answer is: 3
the total number of error is: 11.0
the total error rate is: 0.011627906976744186
可明显看出,K-近邻算法识别手写数字机的错误率约为1.2%。当我们对变量K值进行改变,或者修改训练样本、随机样本数目时,都会对该算法的错误率产生影响。
四、总结
K-近邻算法在使用的时候相当于一边训练一边测试,所以每一个测试都需要一次训练,这就导致如果数据量特别大的情况下,与其他算法相比会花费大量的时间并且会占用大量的内存进行运算,同时,在计算每个数据的距离值时,会消耗大量的时间,所以该算法更多适用于小数据量的样本进行计算。
优点:
简单、易于理解、易于实现、无需估计参数、无需提前训练。
缺点:
懒惰算法、对测试样本分类时的计算量大、内存开销大,并且必须指定K值,K值选择不当则分类精度不能保证。