首页 > 代码库 > 《机器学习实战》读书笔记2:K-近邻(kNN)算法

《机器学习实战》读书笔记2:K-近邻(kNN)算法

声明:文章是读书笔记,所以必然有大部分内容出自《机器学习实战》。外加个人的理解,另外修改了部分代码,并添加了注释

1、什么是K-近邻算法?

简单地说,k-近邻算法采用测量不同特征值之间距离的方法进行分类。不恰当但是形象地可以表述为近朱者赤,近墨者黑。它有如下特点:

  • 优点:精度高、对异常值不敏感、无数据输入假定
  • 缺点:计算复杂度高、空间复杂度高
  • 适用数据范围:数值型和标称型

2、K-近邻算法的工作原理:

存在一个样本数据集合,也称作训练样本集,并且样本集中的每个数据都存在标签,即我们知道样本集中每一数据与所属分类的对应关系。输入没有标签的数据后,将这个没有标签的数据的每个特征与样本集中的数据对应的特征进行比较,然后算法提取样本中特征最相似的数据(最邻近)的分类标签。一般来说,我们只选择样本数据集中前 k<script type="math/tex" id="MathJax-Element-1">k</script> 个最相似的数据,这就是 k<script type="math/tex" id="MathJax-Element-2">k</script>-近邻算法中 k<script type="math/tex" id="MathJax-Element-3">k</script> 的出处,通常 k<script type="math/tex" id="MathJax-Element-4">k</script> 是不大于 20 的整数。最后,选择 k<script type="math/tex" id="MathJax-Element-5">k</script> 个最相似数据中出现次数最多的类别,作为新数据的分类。


2.1 一个例子

例子出自《机器学习实战》,中文版第16页,英文版第19页

下面举一个书本上的例子来说明 k<script type="math/tex" id="MathJax-Element-6">k</script>-近邻算法大概的工作原理:
我们想使用 K<script type="math/tex" id="MathJax-Element-7">K</script>-近邻算法来分来爱情片和动作片。有人曾统计过很多电影的打斗镜头和接吻镜头,下图显示了 6 部电影的打斗镜头和接吻镜头数。假如有一部未看过的电影,如何确定它是爱情片还是动作片呢?(当然了,我们这里不考虑爱情动作片。邪恶的笑容)我们可以使用 kNN(k-nearest neighbors algorithm) 来解决这个问题。

技术分享

首先我们需要知道这个未知电影中存在多少个打斗镜头和接吻镜头,上图中问号的位置是该位置电影出现的镜头数的图形化展示,具体数字如下表所示:

电影名称 打斗镜头 接吻镜头 电影类型
California Man 3 104 爱情片
He’s Not Really into Dudes 2 100 爱情片
Beautiful Woman 1 81 爱情片
Kevin Longblade 101 10 动作片
Robo Slayer 3000 99 5 动作片
Amped II 98 2 动作片
? 18 90 未知


即使不知道未知电影属于哪种类型,我们也可以通过某种方法计算出来。首先要计算未知电影与样本集中其他电影的距离,计算方法很简单,即欧式空间距离(Euclidean Distance),结果如下表所示。

电影名称 与未知电影的距离
California Man 20.5
He’s Not Really into Dudes 18.7
Beautiful Woman 19.2
Kevin Longblade 115.3
Robo Slayer 3000 117.4
Amped II 118.9


现在我们得到了样本集中所有电影与未知电影的距离,按照距离递增排序,可以找到 k<script type="math/tex" id="MathJax-Element-8">k</script> 个距离最近的电影,例如 k=3<script type="math/tex" id="MathJax-Element-9">k=3</script>,则三个最靠近的电影是 He’s Not Really into Dudes、Beautiful Woman 和 California Man。K<script type="math/tex" id="MathJax-Element-10">K</script>-近邻算法按照距离最近的三部电影的类型,决定未知电影的类型,而这三部电影全是爱情片,因此我们判定未知电影是爱情片。


2.2 K-近邻算法的一般流程

k<script type="math/tex" id="MathJax-Element-11">k</script>-近邻算法的一般流程分为如下 6 个步骤:

  1. 搜集数据:可以使用任何方法。
  2. 准备数据:距离计算所需要的值,最好是结构化的数据。
  3. 分析数据:可以使用任何方法。
  4. 训练算法:此步骤不适用于k<script type="math/tex" id="MathJax-Element-12">k</script>-近邻算法。
  5. 测试算法:计算错误率。
  6. 使用算法:首先需要输入样本数据和待分类数据,然后运行k<script type="math/tex" id="MathJax-Element-13">k</script>-近邻算法判定待分类数据分别属于哪个分类,最后应用计算出的分类执行后续的处理。

3、使用python实现kNN算法

我们主要用的编程语言是python,以及一些python增强包。选用python使我们不许要考虑太多的编程细节,能够把重心都放在程序逻辑上。


3.1 使用python导入数据

机器学习最重要的就是数据,首先我们来捏造一些样本数据。将下面的代码保存到名为 kNN.py 的文本文件中:

from numpy import *

def createDataSet():
    dataSet = array([[1.0, 1.1], [1.0, 1.0], [0, 0], [0, 0.1]]) # 创建一个2x2的数组
    labels = [‘A‘, ‘A‘, ‘B‘, ‘B‘] # 创建一个长度为4的列表
    return dataSet, labels

在上面的代码中,我们导入了科学计算包 NumPy,如果你还没有安装 NumPy,请参考我的另外一篇文章《机器学习实战》读书笔记1:NumPy的安装及简单用法。

在上面的代码中,我们创建了一个大小为 2x2 的 NumPy 数组 dataSet,dataSet 的每一行是一个数据项。我们还创建了一个长度为 4 的列表 labels,labels 的每一项对应于 dataSet 的每一行,其对应关系如下表所示:

dataSet[i],即样本 特征0 特征1 labels[i],即样本对应的类别
dataSet[0] 1.0 1.1 A
dataSet[1] 1.0 1.0 A
dataSet[2] 0 0 B
dataSet[3] 0 0.1 B


现在我们打开命令提示符(我使用的是 Ubuntu),检查代码是否能够正常工作:
1. 在代码文件 kNN.py 所在目录下打开命令提示符(终端)
2. 在终端中输入python打开python(我使用的是python2.7)
3. 导入我们刚才写的代码 kNN.py:import kNN
4. 输入如下命令获得由函数 createDataSet() 捏造的数据样本并保存到变量 gropu 和 labels 中:
>>> group, labels = kNN.createDataSet()
5. 输入 group 和 labels 产看数据内容是否正确:
结果应该如下所示:

>>> group
array([[ 1. ,  1.1],
       [ 1. ,  1. ],
       [ 0. ,  0. ],
       [ 0. ,  0.1]])`
>>> labels
[‘A‘, ‘A‘, ‘B‘, ‘B‘]

上述步骤的运行截图:

技术分享

上面的四组数据和其对应的标签可以表示在如下的二维坐标系中:
技术分享

现在我们已经知道 python 如何解析数据,如何加载数据,以及 kNN 算法的工作原理,接下来我们将使用这些方法完成分类任务。


3.2 实施 kNN 算法

首先给出 kNN 算法的伪代码。对未知类别属性的数据集中的每个点一次执行以下操作:

  1. 计算已知类别数据集中的每个点与未知点的距离;
  2. 按照距离递增次序排序;
  3. 选取与未知点距离最小的 k 个点;
  4. 确定前 k 个点所在类别的出现频率;
  5. 返回前 k 个点出现频率最高的类别作为未知点的预测分类。

下面我们写一个函数 classify0() 来实现上面的步骤:

from operator import itemgetter
# inVec为待分类向量,dataSet和labels为数据集,k是最近点的个数
def classify0(inVec, dataSet, labels, k):
    numberOfLines = dataSet.shape[0] # 获得数据集样本数量

    diffMat = tile(inVec, (numberOfLines, 1)) - dataSet # 将数据集中每个点都与待分类点相减,即各个特征相减
    squareDiffMat = diffMat**2 # 求差的平方
    squareDistance = squareDiffMat.sum(axis=1) # 求差的平方的和
    distances = squareDistance**0.5 # 对平方和开方得到距离

    # 对距离进行排序,argsort()函数默认按升序排列,但只返回下标,不对原数组排序
    sortedDistIndicies = distances.argsort()
    classCount = {} # 用于保存各个类别出现的次数

    for i in range(k): # 统计最近的 k 个点的类别出现的次数
        label = labels[sortedDistIndicies[i]]
        classCount[label] = classCount.get(label, 0) + 1

    # 对类别出现的次数进行排序,sorted()函数默认升序
    sortedClassCount = sorted(classCount.iteritems(), key=itemgetter(1), reverse=True)
    return sortedClassCount[0][0] # 返回类别出现次数最多的分类名称

相信上面代码的注释已经把代码解释的很清楚了。只需要注意以下几个函数和 numpy 语法的用法:
1、shape()函数:返回数组的尺寸信息,例如:

>>> x = tile((1,2),(3,2))
>>> x.shape[0] # 返回第0维的大小,行数
3
>>> x.shape[1] # 返回第1维的大小,列数
4
>>> shape(x) # 返回尺寸
(3, 4)

2、tile()函数:将数组、列表或元组平铺,返回平铺后的数组,例如 tile([1,2],(3,2)) 将返回如下数组:

>>> tile([1,2],(3,2)) 
array([[1, 2, 1, 2],
       [1, 2, 1, 2],
       [1, 2, 1, 2]])

3、sum()函数,示例:

>>> x = tile((1,2),(3,2))
>>> x
array([[1, 2, 1, 2],
       [1, 2, 1, 2],
       [1, 2, 1, 2]])
>>> x.sum(axis=0)
array([3, 6, 3, 6])
>>> x.sum(axis=1)
array([6, 6, 6])

4、argsort()函数,返回排序后的原来位置的索引。示例:

>>> v = [1, 4, 2, 3]
>>> argsort(v)
array([0, 2, 3, 1])

5、sorted()函数,按参数 key 排序,示例(按字典的键的值降序排列):

>>> d = {‘a‘:2,‘b‘:1,‘c‘:6,‘d‘:-2}
>>> d
{‘a‘: 2, ‘c‘: 6, ‘b‘: 1, ‘d‘: -2}
>>> from operator import itemgetter
>>> sorted(d.iteritems(),key=itemgetter(1),reverse=True)
[(‘c‘, 6), (‘a‘, 2), (‘b‘, 1), (‘d‘, -2)]

具体用法,请参考文档,或百度。


另外,我们在前面提到过,距离计算使用的是欧式距离,其公式如下:

d=(x0?x1)2+(y0?y1)2???????????????????
<script type="math/tex; mode=display" id="MathJax-Element-14">d=\sqrt {(x_0-x_1)^2+(y_0-y_1)^2}</script>
更高维度的欧式距离也是如此计算。即对分量相减后差的平方求和再开方。例如点(0,0)<script type="math/tex" id="MathJax-Element-15">(0,0)</script>和点(1,2)<script type="math/tex" id="MathJax-Element-16">(1,2)</script>之间的距离为:
d=(0?1)2+(0?2)2???????????????
<script type="math/tex; mode=display" id="MathJax-Element-17">d=\sqrt {(0-1)^2+(0-2)^2}</script>
又例如有四个特征值的点(1,0,0,1)<script type="math/tex" id="MathJax-Element-18">(1,0,0,1)</script>与(7,6,9,4)<script type="math/tex" id="MathJax-Element-19">(7,6,9,4)</script>的距离为:
d=(1?7)2+(0?6)2+(0?9)2+(1?4)2????????????????????????????????
<script type="math/tex; mode=display" id="MathJax-Element-20">d=\sqrt {(1-7)^2+(0-6)^2+(0-9)^2+(1-4)^2}</script>


好了,现在我们来试一试 k<script type="math/tex" id="MathJax-Element-21">k</script>-近邻(kNN)算法。
1、首先回到刚才的 python 窗口,输入reload(kNN)重新加载我们的代码。
2、输入kNN.classify([0,0], group, labels, 3)来对未知数据点(0,0)<script type="math/tex" id="MathJax-Element-22">(0,0)</script>进行分类,其中 group 和 labels 是我们用函数 createDataSet() 得到的样本数据集,3 是 k<script type="math/tex" id="MathJax-Element-23">k</script> 的值,结果应该为B:

技术分享


3.3 测试分类器

分类器也有可能出错,即把属于某一类的分到另一类。

=
<script type="math/tex; mode=display" id="MathJax-Element-24">分类器的错误率=\frac{错误分类次数}{分类测试总次数}</script>
我们将在下面的实例中来测试分类器。


4、示例:使用 K-近邻法改进约会网站的配对效果

此示例出自《机器学习实战》,中文版第20页,英文版第24页

下面是问题的原文:

我的朋友海伦一直使用在线约会网站寻找合适自己的约会对象。尽管约会网站会推荐不同的人选,但她并不是喜欢每一个人。经过一番总结,她发现曾交往过三种类型的人:
(1)不喜欢的人;
(2)魅力一般的人;
(3)极具魅力的人;
尽管发现了上述规律,但海伦依然无法将约会网站推荐的匹配对象归入恰当的分类,她觉得可以在周一到周五约会那些魅力一般的人,而周末则更喜欢与那些极具魅力的人为伴。海伦希望我们的分类软件可以更好地帮助她将匹配对象划分到确切的分类中。此外,海伦还收集了一些约会网站未曾记录的数据信息,她认为这些数据更助于匹配对象的归类。

在约会网站上使用 k<script type="math/tex" id="MathJax-Element-25">k</script>-近邻算法的步骤如下:

  1. 收集数据:提供文本文件;
  2. 准备数据:使用 Python 解析文本文件;
  3. 分析数据:使用 Matplotlib 画二维扩散图;
  4. 训练算法:此步骤不适用于K-近邻算法;
  5. 测试算法:使用海伦提供的部分数据作为测试样本,
    测试样本和非测试样本的区别在于:测试样本是已经完成分类的数据,如果预测分类与实际类别不同,则标记为一个错误。
  6. 使用算法:产生简单的命令行程序,然后海伦可以输入一些特征数据以判断对方是否为自己喜欢的类型。

4.1 准备数据:从文本文件中解析数据

海伦收集约会数据已经有了一段时间,她把这些数据存放在文本文件datingTestSet2.txt中,每个样本数据占据一行,总共有1000行。海伦的样本主要包括以下3种特征:

  1. 每年获得的飞行常客里程数;
  2. 玩视频游戏所耗时间百分比;
  3. 每周消费的冰淇淋公升数;

部分数据截图如下(第一列为飞行里程,第二列为游戏时间百分比,第三列为冰淇淋公升数):

技术分享

在将上述特征数据输入到分类器之前,必须将待处理数据的格式改变为分类器可以接受的格式。在 kNN.py 中创建名为 file2matrix 的函数,以此来处理输入格式问题。该函数的输入为文本文件名字符串,输出为训练样本矩阵和类标签向量。

将下面的代码增加到kNN.py中:

def file2matrix(filename):
    f = open(filename) # 打开文件
    dataSet = f.readlines() # 读取文件的全部内容
    numberOfLines = len(dataSet) # 获得数据集的行数
    returnMat = zeros((numberOfLines, 3)) # 创建一个初始值为0,大小为 numberOfLines x 3 的数组
    classLabelVector = [] # 用于保存没个数据的类别标签
    index = 0
    for line in dataSet: # 处理每一行数据
        line = line.strip() # 去掉行首尾的空白字符,(包括‘\n‘, ‘\r‘,  ‘\t‘,  ‘ ‘)
        listFromLine = line.split() # 分割每行数据,保存到一个列表中
        returnMat[index, :] = listFromLine[0:3] # 将列表中的特征保存到reurnMat中
        classLabelVector.append(int(listFromLine[-1])) # 保存分类标签
        index += 1
    return returnMat, classLabelVector

好了,现在我们回到 python 命令窗口中,输入如下两行命令:

 >>> reload(kNN)
 >>> datingDataMat, datingLabels = kNN.file2matrix(‘datingTestSet2.txt‘)

reload(kNN)重新加载了代码。第二行得到了处理好的样本数据,其中 datingDataMat 保存了每一个样本的特征,datingLabels 保存了每一个样本的分类标签。现在我们可以查看一下它们的内容:

>>> datingDataMat
array([[  4.09200000e+04,   8.32697600e+00,   9.53952000e-01],
       [  1.44880000e+04,   7.15346900e+00,   1.67390400e+00],
       [  2.60520000e+04,   1.44187100e+00,   8.05124000e-01],
       ..., 
       [  2.65750000e+04,   1.06501020e+01,   8.66627000e-01],
       [  4.81110000e+04,   9.13452800e+00,   7.28045000e-01],
       [  4.37570000e+04,   7.88260100e+00,   1.33244600e+00]])
>>> datingLabels[:20]
[3, 2, 1, 1, 1, 1, 3, 3, 1, 3, 1, 1, 2, 1, 1, 1, 1, 1, 2, 3]

由于 datingLabels 内容有1000个,所以只查看了前20个。现在数据已经准备好了,接下来我们应该对数据进行分析。


4.2 分析数据:使用 Matplotlib 创建散点图

关于 Matplotlib 的安装,请参见我的另一篇文章:

在python命令行环境中输入下列命令:

>>> import matplotlib
>>> import matplotlib.pyplot as plt
>>> fig = plt.figure()
>>> ax = fig.add_subplot(111)
>>> ax.scatter(datingDataMat[:,1], datingDataMat[:,2])
>>> plt.show()

输出效果图如下图所示(x轴为玩视频游戏所耗时间百分比,y轴为每周消费的冰淇淋公升数):

技术分享

由于没有使用样本分类的特征值,我们很难从上图中看出任何有用的数据模式信息。一般我们会使用彩色或者不同的记号来表示不同的样本分类,以更好地理解数据信息。Matplotlib 库提供的 scatter 函数支持个性化标记散点图上的点。重新输入上面的代码,调用scatter函数:

>>> from numpy import *
>>> fig = plt.figure()
>>> ax = fig.add_subplot(111)
>>> ax.scatter(datingDataMat[:,1],datingDataMat[:,2],15.0*array(datingLabels),15.0*array(datingLabels))
>>> plt.show()

输出效果图如下图所示:

技术分享

上图中 x 轴为玩视频游戏所耗时间百分比,y 轴为每周消费的冰淇淋公升数,我们好像从中看不出明显的数据模式。

下面我们将每年获得的飞行常客里程数作为 x 轴,玩视频游戏所耗时间百分比作为 y 轴。看看样本点的分布有何不同。

>>> fig = plt.figure()
>>> ax = fig.add_subplot(111)
>>> ax.scatter(datingDataMat[:,0],datingDataMat[:,1],15.0*array(datingLabels),15.0*array(datingLabels))
>>> plt.show()

输出效果图如下图所示:

技术分享

从上面的图中,我们可以看出明显的数据模式。由上面两幅图对比可知,采用列0和列1的特征数据可以更好的区分不同的类别。


另外,为了方便画图,不用每次都输入上面几行代码,我们可以写一个函数 showPlots 来完成同样的功能(将代码添加到 kNN.py 中):

from numpy import *
import matplotlib.pyplot as plt

def showPlots(x, y, labels): # x:x轴数据,y:轴数据,labels:分类标签数据
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.scatter(x ,y, 15.0*array(labels), 15*array(labels))
    plt.show()

重新载入 kNN.py ,输入如下代码即可画图:

>>> reload(kNN)
>>> kNN.showPlots(datingDataMat[:,0], datingDataMat[:,1], datingLabels)

4.3 归一化数据

下表给出了四组数据:

样本 每年获得的飞行常客里程数 玩视频游戏所耗时间百分比 每周消费的冰淇淋公升数 样本对应的类别
1 400 0.8 0.5 1
2 134,000 12 0.9 3
3 20,000 0 1.1 2
4 32,000 67 0.1 2


其中 1 代表不喜欢,2 代表魅力一般,3 代表极具魅力

假如我们要计算样本 3 和样本 4 之间的距离,可以用下面的方法:

d=(20000?32000)2+(0?67)2+(1.1?0.1)2??????????????????????????????????
<script type="math/tex; mode=display" id="MathJax-Element-26">d=\sqrt {(20000-32000)^2+(0-67)^2+(1.1-0.1)^2}</script>
很容易发现,上面数字差值最大的特征对计算结果的影响最大,也就是说,飞行里程数对计算结果的影响远远大于其他两个特征的影响。但是海伦认为这三种特征是同等重要的,因此作为三个等权重的特征之一,飞行里程数不应该如此严重的影响计算结果。

在处理这种不同取值范围的特征值时,我们需要对数据进行归一化处理。将取值范围处理为 0 到 1 或者 -1 到 1 之间。下面的公式可以将任意取值范围的特征值转化为 0 到 1 区间内的值:

newValue=(oldValue?min)/(max?min)
<script type="math/tex; mode=display" id="MathJax-Element-27">newValue=http://www.mamicode.com/(oldValue-min)/(max-min)</script>
其中 min 和 max 分别是数据集中某个特征的最小值和最大值。下面我们就将从文本中加载的数据进行归一化处理,将下面的函数 autoNorm 添加到 kNN.py 中:

def autoNorm(dataSet):
    minVals = dataSet.min(0) # minVals保存每列最小值
    maxVals = dataSet.max(0) # maxVals保存每列最大值
    ranges = maxVals - minVals # ranges保存每列的取值范围
    normedDataSet = zeros(shape(dataSet))
    numberOfLines = dataSet.shape[0]
    normedDataSet = dataSet - tile(minVals, (numberOfLines, 1))
    normedDataSet = normedDataSet / tile(ranges, (numberOfLines, 1))
    return normedDataSet, ranges, minVals

dataSet.min(0) 函数可以从列中选取最小值,dataSet.max(0) 同理。

下面在 python 命令行中,重新加载 kNN.py 模块,执行 autoNorm 函数,检查函数的执行结果:

>>> reload(kNN)
>>> normMat, ranges, minVals = kNN.autoNorm(datingDataMat)
>>> normMat
array([[ 0.44832535,  0.39805139,  0.56233353],
       [ 0.15873259,  0.34195467,  0.98724416],
       [ 0.28542943,  0.06892523,  0.47449629],
       ..., 
       [ 0.29115949,  0.50910294,  0.51079493],
       [ 0.52711097,  0.43665451,  0.4290048 ],
       [ 0.47940793,  0.3768091 ,  0.78571804]])
>>> ranges
array([  9.12730000e+04,   2.09193490e+01,   1.69436100e+00])
>>> minVals
array([ 0.      ,  0.      ,  0.001156])

可以看到,特征数据集已经被归一化了。


4.4 测试分类器

分类器的错误率公式如下:

=
<script type="math/tex; mode=display" id="MathJax-Element-28">分类器的错误率=\frac{错误分类次数}{分类测试总次数}</script>
我们使用数据集的 90% 作为样本,10%作为测试集
下面的函数可以测试分类器的错误率:

def datingClassTest():
    testRatio = 0.10 # 测试比例
    datingDataMat, datingLabels = file2matrix(‘datingTestSet2.txt‘) # 获得原始数据
    normedMat, ranges, minVals = autoNorm(datingDataMat) # 归一化
    m = normedMat.shape[0] # 原始数据行数
    numTestVecs = int(m*testRatio) # 测试数据行数
    errorCount = 0 # 错误分类计数器
    for i in range(numTestVecs): # 测试
        classifierResult = classify0(normedMat[i,:], 
            normedMat[numTestVecs:m,:], datingLabels[numTestVecs:m], 4)
        print "The classifier came back with: %d, the real answer is: %d"            % (classifierResult, datingLabels[i])
        if(classifierResult != datingLabels[i]):
            errorCount += 1
    print "The total error rate is: %f" % (errorCount/float(numTestVecs))

在 python 命令行中重新加载 kNN.py 模块,并执行上面的函数,下面我执行的结果:

>>> reload(kNN)
>>> kNN.datingClassTest()
The classifier came back with: 3, the real answer is: 3
The classifier came back with: 2, the real answer is: 2
.
.
The classifier came back with: 1, the real answer is: 1
The classifier came back with: 3, the real answer is: 3
The classifier came back with: 3, the real answer is: 3
The classifier came back with: 2, the real answer is: 2
The classifier came back with: 1, the real answer is: 1
The classifier came back with: 1, the real answer is: 1
The total error rate is: 0.030000

错误率为 3%,应该算不错了。


4.5 构建完整可用的系统

下面我们就来构建一个可以使用的分类系统,只需添加一个简单的函数即可实现:

def classifyPerson():
    resultList = [‘not at all‘, ‘in small doses‘, ‘in large doses‘]
    percentTime = float(raw_input("percentage of time spent playing video games: "))
    ffMiles = float(raw_input("frequent flier miles earned per year: "))
    iceCream = float(raw_input("liters of ice cream consumed per year: "))
    datingDataMat, datingLabels = file2matrix(‘datingTestSet2.txt‘)
    normedMat, ranges, minVals = autoNorm(datingDataMat)
    inVec = array([ffMiles, percentTime, iceCream])
    classifierResult = classify0((inVec-minVals)/ranges, normedMat, datingLabels, 4)
    print "You will probably like this person", resultList[classifierResult-1]

上面的代码很简单,就不要注释了吧=。=,下面我们来试一试:

>>> reload(kNN)
>>> kNN.classifyPerson()
percentage of time spent playing video games?10
frequent flier miles earned per year?10000
liters of ice cream consumed per year?0.5
You will probably like this person: in small doses

Perfect.下面我们用 kNN 来做一个手写识别系统


5、手写识别系统

此示例出自《机器学习实战》,中文版第28页,英文版第33页

为了简单起见,这里构造的系统只能识别数字 0 到 9。数字是由 0 和 1 表示的文本形式。如下图分别是 9、2、7:

技术分享

每个数字的尺寸均为 32行 x 32列。


5.1 准备数据

书本配备了两个数据集,一个是存储在文件夹 trainingDigits 内的训练集,大约2000个样本;另一个是存储在 testDigits 文件夹内的测试集,大约900个样本。如下图所示。两组数据没有重叠,尺寸均为 32行 x 32列。

技术分享

文件夹 trainingDigits 和文件夹 testDigits 的内容都是如下形式的样列:
技术分享

为了使用我们之前已经编写好的分类器,我们必须将 32x32 的文本处理为一个尺寸为 1x1024 向量,这样就能使用前面的分类器了。下面的函数 img2vector 能够将图像转换为向量:

def img2vector(filename):
    returnVec = zeros((1,1024)) # 用于保存1x1024的向量
    f = open(filename) # 打开文件
    for i in range(32): # 读取每一行并转换为1x1024的向量
        lineStr = f.readline()
        for j in range(32): # 处理第i行j列的一个字符
            returnVec[0,32*i+j] = int(lineStr[j]) # 字符需要强制类型转换成整数
    return returnVec

现在输入如下命令,检查函数是否正常工作:

>>> reload(kNN)
>>> testVec = kNN.img2vector(‘testDigits/0_13.txt‘)
>>> testVec
array([[ 0.,  0.,  0., ...,  0.,  0.,  0.]])
>>> testVec[0,0:32]
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.,  0.])
>>> testVec[0,32:64]
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.,  0.])

打开 testDigits 文件夹下的 0_13.txt 与上面的结果对比,说明函数 img2vector 工作正常。注意: 0_13.txt 表示数据集中数字 0 的第 13 样本。下面我们测试一下算法。


5.2 测试算法: 使用 K-近邻法识别手写数字

上节已经数字的图像处理成分类器可以识别的格式,我们现在就来测试分类去的执行效果。下面的函数 handwritingClassTest() 是测试分类器的代码:

from os import listdir
def handwritingClassTest():
    print "Loading data..."
    hwLabels = [] # 保存手写数字的分类标签
    trainingFileList = listdir(‘trainingDigits‘) # 得到文件夹trainingDigits下的所有文件名
    m = len(trainingFileList) # 训练集样本的个数
    trainingMat = zeros((m, 1024)) # 保存训练集
    for i in range(m):
        filenameStr = trainingFileList[i] # 得到文件名
        classNum = int(filenameStr.split(‘_‘)[0]) # 得到样本的分类标签
        hwLabels.append(classNum)
        trainingMat[i,:] = img2vector(‘trainingDigits/%s‘ % filenameStr)
    testFileList = listdir(‘testDigits‘) # 得到文件夹testDigits下的所有文件名
    errorCount = 0 # 错误分类计数器
    mTest = len(testFileList) # 测试集样本个数
    for i in range(mTest): # 开始测试
        filenameStr = testFileList[i]
        classNum = int(filenameStr.split(‘_‘)[0])
        testVect = img2vector(‘trainingDigits/%s‘ % filenameStr)
        classifierResult = classify0(testVect, trainingMat, hwLabels, 3)
        print "The classifier came back with: %d, the real answer is: %d"            % (classifierResult, classNum)
        if(classifierResult != classNum):
            errorCount += 1
    print "The total number of errors is: %d" % errorCount
    print "The total error rate is: %f" % (errorCount/float(mTest))

上面的注释已经足够清楚了。主要说一下这行代码:

classNum = int(filenameStr.split(‘_‘)[0]) # 得到样本的分类标签

5.1 节中提到了 0_13.txt 表示数据集中数字 0 的第 13 样本。即 0 是样本0_13.txt的标签,其内容如下:

技术分享

接着,我们输入下面的命令对分类器进行测试(k=3):

>>> reload(kNN)
>>> kNN.handwritingClassTest()
Loading data...
The classifier came back with: 6, the real answer is: 6
The classifier came back with: 0, the real answer is: 0
.
.
The classifier came back with: 8, the real answer is: 8
The classifier came back with: 8, the real answer is: 8
The classifier came back with: 2, the real answer is: 2
The classifier came back with: 9, the real answer is: 9
The classifier came back with: 5, the real answer is: 5
The total number of errors is: 13
The total error rate is: 0.013742

错误率为 1.3%,看来分类器工作得还不错。

当我把 k 设为1时,发现错误率为0,我想原因大概是每个数字的形状差别都比较大,如果选择最近的一个作为分类标签,那么准确率会非常的高:

>>> reload(kNN)
<module ‘kNN‘ from ‘kNN.pyc‘>
>>> kNN.handwritingClassTest()
Loading data...
The classifier came back with: 6, the real answer is: 6
The classifier came back with: 0, the real answer is: 0
.
.
The classifier came back with: 8, the real answer is: 8
The classifier came back with: 8, the real answer is: 8
The classifier came back with: 2, the real answer is: 2
The classifier came back with: 9, the real answer is: 9
The classifier came back with: 5, the real answer is: 5
The total number of errors is: 0
The total error rate is: 0.000000

6、总结

kNN算法简单而且准确率高,但是最大的缺点就是既占空间速度又慢。例如上面的手写数字识别系统只是 0-9 十个数字,测试向量就占用了大约 2MB 的空间。而且计算复杂度高,算法要为每个测试向量执行 2000 次距离计算,每次距离计算又包括了 900 次 1024 个维度的浮点运算。除此之外,每次距离计算还需要进行排序等耗时的工作。所以 kNN 的缺点很大。应该算是一种比较不实用的算法,但优点是结果准确。

kNN 算法的另一个缺陷是无法给出任何数据的基础结构信息。而决策树能够解决这个问题,并且速度很快。

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    《机器学习实战》读书笔记2:K-近邻(kNN)算法