K 近邻算法实现手写数字识别系统

k近邻算法

k近邻算法通过测量不同特征值之间的距离来分类,具有如下优缺点

优点:精度高,对异常值不敏感,无数据输入假定

缺点:计算复杂度高,空间复杂度高

K 近邻算法的工作原理是:存在一个样本数据集合,也称作训练样本集,并且样本集中每个数据都存在标签,即我们知道样本集中每一数据与所属分类的对应关系。

输入没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本集中特征最相似数据(最近邻)的分类标签。一般来说,我们只选择样本数据集中前 k个最相似的数据,这就是 K 近邻算法中 k的出处,通常 k 是不大于 20 的整数。最后,选择k个最相似数据中出现次数最多的分类,作为新数据的分类。

k近邻算法的一般流程

1. 收集数据:可以使用任何方法。   
2. 准备数据:距离计算所需要的数值,最好是结构化的数据格式。  
3. 分析数据:可以使用任何方法。  
4. 训练算法:此步骤不适用于 K 近邻算法。  
5. 测试算法:计算错误率。
6. 使用算法:首先需要输入样本数据和结构化的输出结果,然后运行K 近邻算法判定输入数据分别属于哪个分类,最后应用对计算出的分类执行后续的处理。

首先,导入数据

import numpy as np

def create_dataset():
    group=np.array([[1.0, 1.1], [1.0, 1.0], [0, 0], [0, 0.1]])
    labels=['A', 'A', 'B', 'B']
    return group, labels

这里有 4 组数据,每组数据有两个我们已知的属性或者特征值。上面的 `group` 矩阵每行包含一个不同的数据,我们可以把它想象为某个日志文件中不同的测量点或者入口。由于人类大脑的限制,我们通常只能可视化处理三维以下的事务。因此为了简单地实现数据可视化,对于每个数据点我们通常只使用两个特征。

向量 `labels` 包含了每个数据点的标签信息,`labels` 包含的元素个数等于 `group` 矩阵行数。这里我们将数据点 `(1, 1.1)` 定义为类 A,数据点 `(0, 0.1)` 定义为类 B。为了说明方便,例子中的数值是任意选择的,并没有给出轴标签。

如何测试分类器

在上文中我们提到使用 K 近邻 算法能够判断出一个电影是动作片还是爱情片,即我们使用 K 近邻 算法能够实现一个分类器。我们需要检验分类器给出的答案是否符合我们的预期。读者可能会问:「分类器何种情况下会出错?」或者「答案是否总是正确的?」

答案是否定的,分类器并不会得到百分百正确的结果,我们可以使用多种方法检测分类器的正确率。此外分类器的性能也会受到多种因素的影响,如分类器设置和数据集等。不同的算法在不同数据集上的表现可能完全不同,这也是本部分的 6 章都在讨论分类算法的原因所在。

为了测试分类器的效果,我们可以使用已知答案的数据,当然答案不能告诉分类器,检验分类器给出的结果是否符合预期结果。通过大量的测试数据,我们可以得到分类器的错误率:分类器给出错误结果的次数除以测试执行的总数。

错误率是常用的评估方法,主要用于评估分类器在某个数据集上的执行效果。完美分类器的错误率为 0,最差分类器的错误率是 1.0,在这种情况下,分类器根本就无法找到一个正确答案。读者可以在后面章节看到实际的数据例子。

开始正文

# 在 Jupyter Notebook 单元格中执行,下载并解压数据。
!wget "http://labfile.oss.aliyuncs.com/courses/777/digits.zip"
# 解压缩
!unzip digits.zip

步骤一:将图像转化为测试向量

为了使用前面两个例子的分类器,我们必须将图像格式化处理为一个向量。我们将把一个 `32x32` 的二进制图像矩阵转换为 `1x1024` 的向量,这样前两节使用的分类器就可以处理数字图像信息了。

我们首先编写一段函数 `img2vector`,将图像转换为向量:该函数创建 `1x1024` 的 NumPy 数组,然后打开给定的文件,循环读出文件的前 `32` 行,并将每行的头 `32` 个字符值存储在 NumPy 数组中,最后返回数组。

def img2vector(filename):
    returnVect=np.zeros((1, 1024))
    fr=open(filename)
    for i in range(32):
        line_str=fr.readline()
        for j in range(32):
            returnVect[0, 32*i+j] = int(line_str[j])
    return returnVect

分析数据

K 近邻算法我们在理论学习部分已经有所了解,本节内容将实现这个算法的核心部分:计算「距离」。

当我们有一定的样本数据和这些数据所属的分类后,输入一个测试数据,就可以根据算法得出该测试数据属于哪个类别,此处的类别为 0-9 十个数字,就是十个类别。

算法实现过程:

1.计算已知类别数据集中的点与当前点直接的距离

2.按照距离递增排序

3.选取与当前点距离小的k个点

4.确定k个点所在类别的出现频率

5.返回k个点中频率最高的类别作为预测点的分类

import operator


def classify0(inX, dataSet, labels, k):
    
    """
    参数: 
    - inX: 用于分类的输入向量
    - dataSet: 输入的训练样本集
    - labels: 样本数据的c类标签向量
    - k: 用于选择最近邻居的数目
    """
    
    # 获取样本数据数量
    dataSetSize = dataSet.shape[0]

    # 矩阵运算,计算测试数据与每个样本数据对应数据项的差值
    diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet

    # sqDistances 上一步骤结果平方和
    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]

这里,使用欧氏距离公式,计算两个向量点a和 b之间的距离, (0, 0),(1,1)之间的距离为((1-0)**2+(1-0)**2 )**0.5

计算完所有点之间的距离后,可以对数据按照从小到大的次序排序。然后,确定前 k个距离最小元素所在的主要分类,输入k总是正整数;最后,将 classCount 字典分解为元组列表,然后使用程序第二行导入运算符模块的 `itemgetter` 方法,按照第二个元素的次序对元组进行排序。

此处的排序为逆序,即按照从最大到最小次序排序,最后返回发生频率最高的元素标签。

group, labels = createDataSet()
classify0([0, 0], group, labels, 3)

测试算法:使用k近邻算法识别手写数字

我们已经将数据处理成分类器可以识别的格式。接下来,我们将这些数据输入到分类器,检测分类器的执行效果。在写入这些代码之前,我们必须确保将 `from os import listdir` 写入文件的起始部分,这段代码的主要功能是从 os 模块中导入函数 `listdir`,它可以列出给定目录的文件名。

1. 读取训练数据到向量(手写图片数据),从数据文件名中提取类别标签列表(每个向量对应的真实的数字)
1. 读取测试数据到向量,从数据文件名中提取类别标签
3. 执行K 近邻算法对测试数据进行测试,得到分类结果
4. 与实际的类别标签进行对比,记录分类错误率
5. 打印每个数据文件的分类数据及错误率作为最终的结果

from os import listdir


def handwritingClassTest():
    # 样本数据的类标签列表
    hwLabels = []

    # 样本数据文件列表
    trainingFileList = listdir('digits/trainingDigits')
    m = len(trainingFileList)

    # 初始化样本数据矩阵(M*1024)
    trainingMat = np.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(
            'digits/trainingDigits/%s' % fileNameStr)

    # 循环读取测试数据
    testFileList = listdir('digits/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('digits/testDigits/%s' % fileNameStr)

        # 对数据文件进行分类
        classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3)

        # 打印 K 近邻算法分类结果和真实的分类
        print("测试样本 %d, 分类器预测: %d, 真实类别: %d" %
              (i+1, classifierResult, classNumStr))

        # 判断K 近邻算法结果是否准确
        if (classifierResult != classNumStr):
            errorCount += 1.0

    # 打印错误率
    print("\n错误分类计数: %d" % errorCount)
    print("\n错误分类比例: %f" % (errorCount/float(mTest)))

上面的代码中,将 trainingDigits 目录中的文件内容存储在列表中,然后可以得到目录中有多少文件,并将其存储在变量 `m` 中。接着,代码创建一个 `m` 行 1024 列的训练矩阵,该矩阵的每行数据存储一个图像。

我们可以从文件名中解析出分类数字。该目录下的文件按照规则命名,如文件 `9_45.txt` 的分类是 `9`,它是数字 `9` 的第 `45` 个实例。然后我们可以将类代码存储在 `hwLabels` 向量中,使用前面讨论的 `img2vector` 函数载入图像。

在下一步中,我们对 `testDigits` 目录中的文件执行相似的操作,不同之处是我们并不将这个目录下的文件载入矩阵中,而是使用 `classify0()` 函数测试该目录下的每个文件。

最后,我们输入 `handwritingClassTest()`,测试该函数的输出结果。

K 近邻算法识别手写数字数据集,错误率为 `1.05%`。改变变量 `k` 的值、修改函数 `handwritingClassTest` 随机选取训练样本、改变训练样本的数目,都会对 K 近邻算法的错误率产生影响,感兴趣的话可以改变这些变量值,观察错误率的变化。

猜你喜欢

转载自blog.csdn.net/weixin_42307828/article/details/86499016