[机器学习]K-近邻算法

算法概述

  自我感觉K-近邻(k-NearestNeighbor)算法是最简单最易理解的分类算法了。怎么个简单法呢?简单到没有一个训练分类器的过程,仅仅根据需要分类的样本到已知类别的样本之间的距离来进行分类。
  简单来讲,如果存在一个训练样本集,并且训练集中每个样本对应的类别也已知;那么对于未知类别的新样本,我们计算它到每个训练样本的距离,然后选择距离最近的k个样本,这k个样本中计数最多的一类就是待分类样本的类别。

k-NN算法的步骤如下:

  1. 根据特征值计算待分类样本到训练集每个样本的距离;
  2. 按照距离递增次序排序;
  3. 选出与待分类样本距离最近的前 k 个样本;
  4. 计算前 k 个样本中每一类别的数量;
  5. 将前 k 个样本中计数最多的类别数作为待分类样本的类别。

注意

  1. 距离
    K-近邻算法计算距离时可以使用常用的距离度量方法,一般可以选择欧式距离,计算方式如下:
    d=i=0n(xiyi)2d=\sqrt{\sum_{i=0}^{n}\left(x_{i}-y_{i}\right)^{2}}
    xxyy 分别表示两个样本,nn 表示特征维度。

  2. k的取值

    k的取值很重要,k值太小的话算法对于一些噪声成分会比较敏感,k太大的话离待分类样本距离较远的点也会对分类结果产生影响,这都不是想要的结果。一般建议k取小于20的整数。

利用k-NN实现手写数字识别

  k-NN算法的原理比较容易理解,接下来通过手写数字识别的例子来看看怎么实现一个k-近邻分类器。

数据集

  这里用到的手写数字数据集不是MNIST数据集,而是采用文本格式存储的数字图像,如下图所示。每个文本文件存储着 323232*32 个黑白像素点,用来表示一个手写数字。数据集中包含了2000个训练样本和900个测试样本。

代码实现

像素文本转换为特征向量
  上面提到,每个样本都是一个323232*32的二进制图像,因此我们首先将每个样本读取为一个向量的表示方式:

def img2vector(fileName):
    returnVect = np.zeros((1, 1024))
    with open(fileName) as fr:
        i = 0
        for lineStr in fr:   # 按行读取文件
            for j in range(32):
                returnVect[0, 32*i+j] = int(lineStr[j])
            i += 1
    return returnVect

传入参数为文件名,最后返回样本的向量表示。

分类函数
  k-NN没有一个显式的训练过程,对于每个测试样本,对其进行分类的时候都需要计算到每一个训练样本的距离,进而根据距离最近的k个样本的类别进行分类。

def classify(testX, trainingSet, labels, k):
    '''
    testX: 测试样本的特征向量
    trainingSet: 训练集特征
    labels: 训练集类别标签
    k: 最邻近样本个数
    '''
    m = trainingSet.shape[0]  # 训练样本的数量
    distance = getDistance(testX, trainingSet)  # 计算测试样本到每个训练样本的距离
    nearestIndices = np.argsort(distance)[:k]  # 距离最小的k个样本的索引
    # 计算前k个样本中每一类的数量
    classCount = {}
    maxCount = 0
    nearestClass = None
    for i in range(k):
        voteIlabel = labels[nearestIndices[i]]
        classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
        if classCount[voteIlabel] > maxCount:   # 更新最多的计数和其对应的标签
            maxCount = classCount[voteIlabel]
            nearestClass = voteIlabel
    return nearestClass

完整的代码实现如下:

#!/usr/bin/python3
# -*- coding: utf-8 -*-

'''
@Date    : 2019/9/27
@Author  : Rezero
'''

import numpy as np
import os

def img2vector(fileName):
    returnVect = np.zeros((1, 1024))
    with open(fileName) as fr:
        i = 0
        for lineStr in fr:   # 按行读取文件
            for j in range(32):
                returnVect[0, 32*i+j] = int(lineStr[j])
            i += 1
    return returnVect

def getDistance(testX, trainingSet):
    return np.sqrt(np.sum((trainingSet - testX)**2, axis=1))  # 欧式距离

def classify(testX, trainingSet, labels, k):
    '''
    testX: 测试样本的特征向量
    trainingSet: 训练集特征
    labels: 训练集类别标签
    k: 最邻近样本个数
    '''
    m = trainingSet.shape[0]  # 训练样本的数量
    distance = getDistance(testX, trainingSet)  # 计算测试样本到每个训练样本的距离
    nearestIndices = np.argsort(distance)[:k]  # 距离最小的k个样本的索引
    # 计算前k个样本中每一类的数量
    classCount = {}
    maxCount = 0
    nearestClass = None
    for i in range(k):
        voteIlabel = labels[nearestIndices[i]]
        classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
        if classCount[voteIlabel] > maxCount:   # 更新最多的计数和其对应的标签
            maxCount = classCount[voteIlabel]
            nearestClass = voteIlabel
    return nearestClass

def main():
    labels = []
    trainingFiles = os.listdir('data/trainingDigits')  # 训练集路径
    trainNum = len(trainingFiles)  # 训练集样本数
    trainingMat = np.zeros((trainNum, 1024))
    for i in range(trainNum):
        fileName = trainingFiles[i]
        clas = int(fileName.split('_')[0])   # 当前样本的类别
        labels.append(clas)
        trainingMat[i, :] = img2vector('data/trainingDigits/' + fileName) # 把每个样本所表示的数字读取为一个1*1024的向量


    testFiles = os.listdir('data/testDigits')  # 测试集路径
    errorCount = 0   # 分类错误计数
    testNum = len(testFiles)
    for i in range(testNum):
        fileName = testFiles[i]
        clas = int(fileName.split('_')[0])  # 样本的真实类别
        vectorUnderTest = img2vector('data/testDigits/' + fileName)  # 当前测试样本的向量
        # 使用kNN分类器对当前样本进行分类,设置k=3
        classifierResult = classify(vectorUnderTest, trainingMat, labels, 3)
        print("The classifier came back with: %d, the real answer is: %d" % (classifierResult, clas))
        if clas != classifierResult:
            errorCount += 1

    print("The total number of errors is: %d" % errorCount)
    print("the total error rate is %f: " %(errorCount/testNum))


if __name__ == "__main__":
    main()

k-NN算法的优缺点

  • 优点
    简单易懂、精度高、对异常值不敏感、无数据输入假定,可以用于数值型和离散型数据

  • 缺点
    计算复杂度高,单个样本分类需要计算到所有训练样本(已知样本)的距离;空间复杂度高,需要存储所有的训练样本,空间开销大。

参考资料

《机器学习实战》第二章:k-近邻算法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章