书籍:机器学习实战
作者:Peter Harrington

K近邻算法的优缺点

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

K近邻算法一般流程

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

代码分析

第一段
# from numpy import *
import numpy as np
import operator

def createDataSet():
    # group = array([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]]) # 数据
    group = np.array([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]])
    labels = ['A','A','B','B'] # 对应的标签
    return group, labels
  • 原书的第一行代码为from numpy import *,我这里改为了import numpy as np,原因是如果采用了原书的代码,我们在不熟悉numpy的情况下,很可能把numpy下的array方法错认为是Python内置的函数,而np.array就很明显知道arraynumpy下的方法了。import numpy as np表示导入numpy模块并将np作为numpy的替代词,以简化书写。
  • group = np.array([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]])创建了一个二维(python K近邻插补法 k近邻算法代码cpp_机器学习)的numpy数组。
第二段
def classify0(inX, dataSet, labels, k):
    dataSetSize = dataSet.shape[0]
    diffMat = np.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.iteritems(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]
  • classify0函数的四个输入参数:
  • inX:用于分类的输入向量
  • dataSet:输入的训练样本,是一个二维矩阵,行数为参与的训练样本数目,列数为特征数目
  • labels标签向量,数目和dataSet行数相同
  • k:最近邻居的数目,标量
  • dataSetSize = dataSet.shape[0]表示将训练样本数目赋给变量dataSetSize
  • .tile(array,[i,j])numpy模块下的一个方法,表示数组array在行方向上重复i次,在列方向上重复j次。如果[i,j]处是一个标量j,表示列方向上重复j次,行方向上默认重复1次。
>>> import numpy
>>> numpy.tile([0,0],5)
array([0, 0, 0, 0, 0, 0])
>>> numpy.tile([0,0],(1,1))
array([[0, 0]])
>>> numpy.tile([0,0],(3,1))
array([[0, 0],
   		[0, 0],
   		[0, 0]])
>>> numpy.tile([0,0],(1,3))
array([[0, 0, 0, 0, 0, 0]])
>>> numpy.tile([0,0],(2,2))
array([[0, 0, 0, 0,],
   [0, 0, 0, 0]])
  • diffMat = np.tile(inX, (dataSetSize,1)) - dataSet到底做了一件什么事呢?通过两点间的欧式距离公式python K近邻插补法 k近邻算法代码cpp_python_02可以看出这行代码执行的就是求待分类的点到每个训练样本点的欧式距离公式中的python K近邻插补法 k近邻算法代码cpp_python_03,我们可以举个例子来解释:

假设有三个训练样本(1,1),(2,2),(3,3),待分类样本为(5,2),则代码先将(5,2)在行方向上重复三次,使它变成和dataSet同维度的3行2列矩阵,再进行相减,即:
python K近邻插补法 k近邻算法代码cpp_python_04

  • diffMat**2表示矩阵diffMat的每个元素求平方,而非数学意义上两个相同矩阵做乘法。
  • 经过代码sqDiffMat = diffMat**2,矩阵变为了:python K近邻插补法 k近邻算法代码cpp_数据_05
  • distances = sqDistances**0.5表示对矩阵元素求合后开根号,就得到了我们想要的欧式距离,即:python K近邻插补法 k近邻算法代码cpp_python_06
  • .argsort(array)numpy下的一个方法,返回的是数组array的值从小到大的索引值;若是多维数组可以通过制定轴参数axis来排序;如果在参数array前面加上一个负号,表示从大到小排序返回对应的索引值。
>>> x = np.array([3, 1, 2])
  >>> np.argsort(x)
  array([1, 2, 0])
  >>> x = np.array([[0, 1], [2, 2]])  # 相等数据下默认前面一个排在前面
  >>> x
  array([[0, 1],
         [2, 2]])
  >>> np.argsort(x, axis=0)  # 按列排序
  array([[0, 1],
         [1, 0]])
  
  >>> np.argsort(x, axis=1)  # 按行排序
  array([[0, 1],
         [0, 1]])
  >>> x = np.array([3, 1, 2])
  >>> np.argsort(x)  # 按升序排列
  array([1, 2, 0])
  >>> np.argsort(-x)  # 按降序排列
  array([0, 2, 1])
  >>> x[np.argsort(x)]
  array([1, 2, 3])
  >>> x[np.argsort(-x)]
  array([3, 2, 1])
  • classCount={}定义了一个字典classCount,字典的键存的是最近的k个邻居对应的的非重合标签值,字典的值存的是对应这个标签在k个里面出现的次数。例如最近的8个邻居对应的标签为['A','A','A','A','A','B','B','C'],则classCount = {'A':5, 'B':2, 'C':1}
  • 后面的循环代码实现的就是得到上面classCount的过程,然后通过字典的值进行降序排序,因为K近邻算法需要得到的就是k个邻居中对应标签中出现次数最多的那个标签。排序后,通过最大的字典值得到它对应的标签值作为函数的返回值即可。这个循环程序段中有几点值得注意:
  • 字典的.get(key,default=None)方法返回指定键的值,如果键不在字典中返回默认值 None 或者设置的默认值。这里将默认值设为0是非常妙的,它相当于如果字典不存在该键,那么创建该键,并赋值为0。
  • sorted(iterable,cmp=None,key=None,reverse=False)参数介绍:
  • iterable:可迭代对象
  • key:主要是用来进行比较的元素,只有一个参数,具体的函数的参数就是取自于可迭代对象中,指定可迭代对象中的一个元素来进行排序
  • reverse:排序规则,reverse = True降序,reverse = False升序(默认)
  • itemgetteroperator下的方法,表示获取对象指定域中的值
>>> from operator import itemgetter
  >>> a = [1,2,3,4,5]
  >>> b = itemgetter(0)
  >>> b(a)
  1
  >>> c = itemgetter(0,1,2)
  >>> c(a)
  (1, 2, 3)
  • itemgetterkey参数连用可以对字典进行排序,排序的标准是itemgetter括号内对应的键或值
  • .items()以列表的形式返回字典的键值对,.iteritems()以迭代器的形式返回字典的键值对
  • sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)表示通过字典的值对返回的键值对进行降序排列。
第三段
def file2matrix(filename):
    fr = open(filename)  # 打开文本(这个文本共有4列,前三列为样本特征,第四列为标签)
    numberOfLines = len(fr.readlines())  # 确定该文本的行数
    returnMat = np.zeros((numberOfLines,3))  # 创建一个行数为文本行数,列数为3的全0矩阵
    classLabelVector = []  # 创建一个标签列表,用于存储样本的标签
    index = 0  # 设置初始索引值为0
    for line in fr.readlines():
        line = line.strip()  # 删除行两边的空白符
        listFromLine = line.split('\t')  # 以制表符为分隔符分割行上的文本数据
        returnMat[index,:] = listFromLine[0:3]  # 将该样本的特征赋给矩阵的index行
        classLabelVector.append(int(listFromLine[-1]))  # 将该样本的特征值加到标签列表中
        index += 1
    return returnMat,classLabelVector
第四段
def autoNorm(dataSet):
    minVals = dataSet.min(0)  # 求数据集每一列的最小值
    maxVals = dataSet.max(0)  # 求数据集每一列的最大值
    ranges = maxVals - minVals  # 求数据集每一列的极差
    normDataSet = np.zeros(shape(dataSet))  # 创建一个和数据集相同形状的全0矩阵
    m = dataSet.shape[0]  # 将数据集中的样本数(行数)赋给m
    normDataSet = dataSet - np.tile(minVals, (m,1))  # 数据集每个元素的值减去它对应列的最小值
    normDataSet = normDataSet/np.tile(ranges, (m,1))  # 最大最小值归一化,公式如下
    return normDataSet, ranges, minVals
  • min(0)max(0)在矩阵中的用法:
  • min(0)返回该矩阵中每一列的最小值
  • min(1)返回该矩阵中每一行的最小值
  • max(0)返回该矩阵中每一列的最大值
  • max(1)返回该矩阵中每一行的最大值
  • 最大最小值归一化公式:python K近邻插补法 k近邻算法代码cpp_数据_07
第五段
def datingClassTest():
    hoRatio = 0.10  # 测试数据的比例
    datingDataMat,datingLabels = file2matrix('datingTestSet2.txt')  # 调用第三段代码,读取文本,将文本前三列数据赋给datingDataMat,最后一行数据赋给datingLabels
    normMat, ranges, minVals = autoNorm(datingDataMat)  # 最大最小值归一化,调用第四段代码,返回归一化后的矩阵、每列的极差和最小值
    m = normMat.shape[0]  # 将数据集中的样本数(行数)赋给m
    numTestVecs = int(m*hoRatio)  # 将样本数目的10%赋给numTestVecs
    errorCount = 0.0  # 初始化预测错误的次数
    for i in range(numTestVecs):
        classifierResult = classify0(normMat[i,:],normMat[numTestVecs:m,:],datingLabels[numTestVecs:m],3)  # 调用第二段代码,返回分类结果
        print(classifierResult, datingLabels[i])  # 展示分类结果和真实结果
        if (classifierResult != datingLabels[i]): errorCount += 1.0  # 如果两者不等,错误次数加1
    print(errorCount/float(numTestVecs))  # 返回分类错误率

raw_input()函数可以从用户那里读取一行。此函数将通过剥离尾随换行符来返回一个字符串。它在Python 3.0及更高版本中被重命名为input()函数。
raw_inputinput的基本区别在于raw_input总是返回一个字符串值,而input函数不一定返回一个字符串,因为当用户输入的是数字时,它会将其作为一个整数。

第六段
def img2vector(filename):
    returnVect = np.zeros((1,1024))  # 创建一个维度为1024的列向量(数组)
    fr = open(filename)
    for i in range(32):  # 读取文件的前32行
        lineStr = fr.readline()
        for j in range(32):  # 将每行的前32个字符值存储在列向量中 32 * 32 = 1024
            returnVect[0,32*i+j] = int(lineStr[j])
    return returnVect
第七段
def handwritingClassTest():
    hwLabels = []  # 创建标签列表
    trainingFileList = os.listdir('trainingDigits')  # 返回trainingDigits文件夹下的文件(或文件夹)的名字的列表
    m = len(trainingFileList)  # 把trainingDigits文件夹下的文件(或文件夹)的个数赋给m
    trainingMat = zeros((m,1024))  # 创建一个m行1024列的全0矩阵,该矩阵后面每行存储一个图像
    for i in range(m):  # 遍历训练文件夹下的m个文件
        fileNameStr = trainingFileList[i]  # 将文件名赋给fileNameStr
        fileStr = fileNameStr.split('.')[0]  # 去掉文件名后面的.txt后缀名,并赋给fileStr
        classNumStr = int(fileStr.split('_')[0])  # 以"_"为分隔符进行分割,并返回分割后的第一个值并转换为整数后作为分类标签,如"9_45"返回"9"-->9
        hwLabels.append(classNumStr)  # 将标签加入到标签列表中
        trainingMat[i,:] = img2vector('trainingDigits/%s' % fileNameStr)  # 返回第i+1个文件的前32行的前32个字符(共32*32=1024)给trainingMat的第i行
    testFileList = os.listdir('testDigits')  # 返回testDigits文件夹下的文件(或文件夹)的名字的列表
    errorCount = 0.0  # 初始化预测错误的次数
    mTest = len(testFileList)  # 把testDigits文件夹下的文件(或文件夹)的个数赋给mTest
    for i in range(mTest):  # 遍历测试文件夹下的m个文件
        fileNameStr = testFileList[i]
        fileStr = fileNameStr.split('.')[0]
        classNumStr = int(fileStr.split('_')[0])
        vectorUnderTest = img2vector('testDigits/%s' % fileNameStr)
        classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3)  # 返回分类结果
        print(classifierResult, classNumStr)  # 展示分类结果和真实结果
        if (classifierResult != classNumStr): errorCount += 1.0  # 如果两者不等,错误次数加1
    print(errorCount)  # 返回分类错误的次数
    print(errorCount/float(mTest))  # 返回分类错误率
  • .listdir(path)os模块下的方法,用于返回指定路径下包含的文件或文件夹的名字的列表。