《机器学习实战》知识总结及代码解析(1)K-近邻算法

@《机器学习实战》
@K-近邻算法

1 K-近邻算法(k-Nearest Neighbor,KNN

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

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

适用数据范围:数值型和标称型

1.1 工作原理

存在一个样本数据集合(训练样本集),并且样本集中每个数据都存在标签,即我们知道样本集中每一数据与所属分类的对应关系。

输入没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本集中特征最相似数据的分类标签,即最近邻的分类标签。

一般来说,我们只选取样本数据集中前K个最相似的数据,通常K是不大于20的整数。

最后,选择K个最相似数据中出现次数最多的分类,作为数据的分类。

1.2 一般流程

(1)收集数据:数据是数值型,并带有分类标签
(2)准备数据:通过python从文本文件中解析数据,并归一化特征值
(3)分析数据:使用matplotlib画二维散点图
(4)测试算法:作为对分类器的验证,计算它的错误率和错误个数
(5)使用算法:输入特征数值,来对未知类别的数据进行分类

1.3 注意事项

(1)KNN分类算法的classify0函数有4个参数,其中的inX需要注意,它是输入向量,即待测试数据。它用到了tile函数,这个我在下文的代码解析会提到。

(2)标签向量labels的元素数目和矩阵dataset的行数相同

(3)读取文件数据,必须确保txt等文档存储在当前的工作目录

(4)创建散点图,需要确保环境有matplotlib(这点针对零基础的学习者)

(5)归一化特征值是很有必要的,因为这样能避免较大的特征数据对计算的影响,使得每个特征值对计算结果的影响是一样的。

(6)可以通过改变datingClassTest函数的hoRatio和K值,使得错误率发生变化,错误率是随着它们的变化而增加。

(7)书中“2-5约会网站预测函数”的raw_input函数是python2中的,python3环境会报错,直接换成input函数即可。

(8)同样,书中“2-1 k-近邻算法”,用到的iteritems也是python2中的,在python3环境下,应该换成items。



2 代码解析

这一章节书中共有9个函数代码,下面我会逐个展示代码及注释,并做一些相关的解析。

2.1 KNN分类算法

这里,我将对classify0函数和createDataSet函数进行解析。

首先,createDataSet函数是一个很基础的函数,它用来创建数据集和标签,并能返回数据集和标签的值。

而classify0函数,则是这章的精髓。它的功能是将每组数据划分到某个类中。

函数包含4个参数,其中inX是用于分类的输入向量,dataSet是输入的训练样本集,labels是标签向量,K是用于选择最近邻居的数目。

而函数的大体思路可以概括为5个步骤:
(1)计算已知数据集的点与当前点之间的距离
(2)按照距离递增次序排序
(3)选取与当前点距离最小的K个点
(4)确定前K个点所在类别的出现频率
(5)返回前K个点出现频率最高的类别
而这些步骤的解释,我在下面的代码片里有所提及,可以详见注释。

相关代码及注释:

#KNN分类算法,inX是用于分类的输入向量,dataSet是输入的训练样本集,labels是标签向量,K是用于选择最近邻居的数目
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()     #取得输入向量和数据集中各向量欧式距离的从小到大排序的下标值,返回排序后的索引值
    #  以下是第三步:选取与当前点距离最小的K个点
    classCount={}          #定义一个空字典
    for i in range(k):     #取计算欧氏距离排序后最小的前k个值
        voteIlabel = labels[sortedDistIndicies[i]]
        classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1   #若字典中存在label键,则对应的值+1,如果不存在,则添加这个键设置为0并+1
    #  以下是第四步:确定前K个点所在类别的出现频率
    sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)  #按照字典中的值对字典进行降序排列
    #  最后是第五步,返回前K个点出现频率最高的类别
    return sortedClassCount[0][0]

#创建数据集和标签
def createDataSet():
    group = array([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]])
    labels = ['A','A','B','B']
    return group, labels

补充:
因为函数里有用到tile函数,这个函数将inX向量进行相应的变换,使得它与dataSet矩阵为相似矩阵,并进行相应的操作。

而tile函数的形式是: tile(A,rep),

  • A: Array类的都可以

  • rep:A沿着各个维度重复的次数(由外至内)

它的功能是:重复A的各个维度

如果对tile函数感兴趣,可以参照这个Python tile函数详细解释

2.2 准备数据

这里,我将对“准备数据”的相关代码函数进行解析,即file2matrix函数和 autoNorm函数

file2matrix函数的功能是从文本文件中解析数据,输入的参数为文件名字,返回的结果为训练样本矩阵和类标签向量。

file2matrix函数的大体思路可以概况为3个步骤:
(1)得到文件行数
(2)创建返回的Numpy矩阵
(3)解析文件数据到列表
同样,这些步骤的解释,我在下面的代码片里有所提及,可以详见注释。

autoNorm函数的功能是归一化特征值,输入的参数为数据集数据,返回的结果为归一化矩阵,取值范围以及最小值。

至于归一化特征值的作用,我在上文有所提及,可以详见“1.3 注意事项”的第5点。

相关代码及注释:

#从文本文件中解析数据,输入为文件名字,输出为训练样本矩阵和类标签向量
def file2matrix(filename):
    #  以下是第一步:得到文件行数
    fr = open(filename)     #打开文件
    numberOfLines = len(fr.readlines())         #计算该文件共有多少行(即共有多少个样本)
    #  以下是第二步:创建返回的Numpy矩阵
    returnMat = zeros((numberOfLines,3))        #创建返回的Numpy矩阵
    classLabelVector = []                       #定义下面需要返回的标签
    fr = open(filename)     #需要再次打开文件,否则会报错
    index = 0
    #  以下是第三步:解析文件数据到列表
    for line in fr.readlines():          #解析文件数据到列表
        line = line.strip()              #去掉首尾空白符
        listFromLine = line.split('\t')  #利用空格符分离字符串
        returnMat[index,:] = listFromLine[0:3]    #将每行样本数据的前3个数据输入返回样本矩阵中
        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 = zeros(shape(dataSet))   #初始化归一化数据集
    m = dataSet.shape[0]      #行
    normDataSet = dataSet - tile(minVals, (m,1))
    normDataSet = normDataSet/tile(ranges, (m,1))   #特征值相除
    return normDataSet, ranges, minVals       #返回归一化矩阵,取值范围以及最小值

2.3 分析数据

这里,我将对创建散点图的相关代码进行解析。
散点图使用矩阵的第二、第三列数据,分别表示特征值。
而它的scatter函数个性化标记散点图上的点。它利用变量datingLabels存储的类标签属性,在散点图上绘制了色彩不等、尺寸不同的点。它还利用datingDatamat矩阵的第二列和第三列属性来展示数据。

相关代码:

  # 创建散点图,散点图使用矩阵的第二、第三列数据,分别表示特征值
     fig = plt.figure()
     ax = fig.add_subplot(111)
     ax.scatter(datingDatamat[:,1], datingDatamat[:,2], 15.0*array(datingLabels), 15.0*array(datingLabels))
     matplotlib.pyplot.show()

2.4 测试算法及使用算法

“测试算法”对应datingClassTest函数,“使用算法”对应classifyperson函数。这里我将对它们的相关代码进行解析。

datingClassTest函数的功能是计算分类器的错误率和错误个数,它输出计算得到的错误率和错误个数。

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

同样,这些函数的解释,我在下面的代码片里有所提及,可以详见注释。

相关代码及注释:

#计算分类器的错误率和错误个数
def datingClassTest():
    hoRatio = 0.50      #取测试样本占数据集样本的10%
    datingDataMat,datingLabels = file2matrix('datingTestSet2(1).txt')       #得到样本集,样本标签
    normMat, ranges, minVals = autoNorm(datingDataMat)       #得到归一化样本集,取值范围以及最小值
    m = normMat.shape[0]         #样本集行数
    numTestVecs = int(m*hoRatio)         #测试样本集数量
    errorCount = 0.0                     #初始化错误率
    for i in range(numTestVecs):         #循环,计算测试样本集错误数量
        classifierResult = classify0(normMat[i,:],normMat[numTestVecs:m,:],datingLabels[numTestVecs:m],3)  #通过k-近邻算法进行分类,并记录结果
        print ("the classifier came back with: %d, the real answer is: %d" % (classifierResult, datingLabels[i]))
        if (classifierResult != datingLabels[i]): errorCount += 1.0           #算法结果与样本的实际分类做对比
    print("the total error rate is: %f" % (errorCount/float(numTestVecs)))   #计算错误率,并输出
    print(errorCount)         #输出错误的个数

#约会网站的对象判断
def classifyperson():
    resultList = ['not at all', 'in small doses', 'in large doses']   #喜欢程度的预测值列表
    #输入约会对象的一些特征
    percentTats = float(input("percentage of time spent playing video games?"))
    ffMiles = float(input("frequent flier miles earned per year?"))
    iceCream = float(input("liters of ice cream consumed per year?"))
    datingDatamat,datingLabels = file2matrix('datingTestSet2(1).txt')   # 从文本文件中解析数据
    normMat, ranges, minVals = autoNorm(datingDatamat)    #特征归一化处理
    inArr = array([ffMiles, percentTats, iceCream])       #输入的约会对象的特征
    classifierResult = classify0((inArr - minVals) / ranges, normMat, datingLabels, 3)  #判断约会对象的类型
    print("You will probably like this person: ", resultList[classifierResult - 1])

2.5 手写识别系统

手写识别系统包含2个部分,即“准备数据”和“测试算法”。
“准备数据”对应img2vector函数,“测试算法”对应handwritingClassTest函数。这里我将对它们的相关代码进行解析。

img2vector函数的功能是将图像转换为测试向量,它最后返回测试向量,这个向量本身也完全是由01构成,相当于将原来的矩阵每一行首尾相连。

handwritingClassTest函数的功能是使用K-近邻算法识别手写数字,并计算错误率和错误个数。

同样,这些函数的解释,我在下面的代码片里有所提及,可以详见注释。

相关代码及注释:

#将图像转换为测试向量
def img2vector(filename):
    returnVect = zeros((1,1024))   #返回一个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])   #这个向量本身也完全是由01构成,相当于将原来的矩阵每一行首尾相连
    return returnVect

#识别手写数字
def handwritingClassTest():
    hwLabels = []
    trainingFileList = listdir('trainingDigits')           #导入训练集,‘trainingDigits’是一个文件夹
    m = len(trainingFileList)                       #计算训练样本个数
    trainingMat = zeros((m,1024))                   #初始化数据集,将所有训练数据用一个m行,1024列的矩阵表示
    for i in range(m):
        fileNameStr = trainingFileList[i]   #获得所有文件名,文件名格式‘x_y.txt’,x表示这个手写数字实际表示的数字(label)
        fileStr = fileNameStr.split('.')[0]     #去除 .txt
        classNumStr = int(fileStr.split('_')[0])   #classnumber为每个样本的分类,用‘_’分割,取得label
        hwLabels.append(classNumStr)         #将所有标签都存进hwLables[]
        trainingMat[i,:] = img2vector('trainingDigits/%s' % fileNameStr)     #将文件转化为向量后存入trainingMat[],这里展现了灵活的文件操作
    testFileList = listdir('testDigits')        #迭代测试集
    errorCount = 0.0
    mTest = len(testFileList)
    for i in range(mTest):
        fileNameStr = testFileList[i]
        fileStr = fileNameStr.split('.')[0]     #去除 .txt
        classNumStr = int(fileStr.split('_')[0])
        vectorUnderTest = img2vector('testDigits/%s' % fileNameStr)  #这部分针对测试集的预处理和前面基本相同
        classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3)    #使用算法预测样本所属类别,调用了前面写的classify0()函数
        print ("the classifier came back with: %d, the real answer is: %d" % (classifierResult, classNumStr))
        if (classifierResult != classNumStr): errorCount += 1.0       #算法结果与样本的实际分类做对比
    print ("\nthe total number of errors is: %d" % errorCount)
    print ("\nthe total error rate is: %f" % (errorCount/float(mTest)))

2.6 完整代码

from numpy import *   #导入科学计算包numpy模块
import operator       #导入运算符模块
from os import listdir   #用于获取目录中的文件名
import matplotlib    #用于创建散点图
import matplotlib.pyplot as plt  #将pyplot命名为plt

#KNN分类算法,inX是用于分类的输入向量,dataSet是输入的训练样本集,labels是标签向量,K是用于选择最近邻居的数目
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()     #取得输入向量和数据集中各向量欧式距离的从小到大排序的下标值,返回排序后的索引值
    #  以下是第三步:选取与当前点距离最小的K个点
    classCount={}          #定义一个空字典
    for i in range(k):     #取计算欧氏距离排序后最小的前k个值
        voteIlabel = labels[sortedDistIndicies[i]]
        classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1   #若字典中存在label键,则对应的值+1,如果不存在,则添加这个键设置为0并+1
    #  以下是第四步:确定前K个点所在类别的出现频率
    sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)  #按照字典中的值对字典进行降序排列
    #  最后是第五步,返回前K个点出现频率最高的类别
    return sortedClassCount[0][0]

#创建数据集和标签
def createDataSet():
    group = array([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]])
    labels = ['A','A','B','B']
    return group, labels

#从文本文件中解析数据,输入为文件名字,输出为训练样本矩阵和类标签向量
def file2matrix(filename):
    #  以下是第一步:得到文件行数
    fr = open(filename)     #打开文件
    numberOfLines = len(fr.readlines())         #计算该文件共有多少行(即共有多少个样本)
    #  以下是第二步:创建返回的Numpy矩阵
    returnMat = zeros((numberOfLines,3))        #创建返回的Numpy矩阵
    classLabelVector = []                       #定义下面需要返回的标签
    fr = open(filename)     #需要再次打开文件,否则会报错
    index = 0
    #  以下是第三步:解析文件数据到列表
    for line in fr.readlines():          #解析文件数据到列表
        line = line.strip()              #去掉首尾空白符
        listFromLine = line.split('\t')  #利用空格符分离字符串
        returnMat[index,:] = listFromLine[0:3]    #将每行样本数据的前3个数据输入返回样本矩阵中
        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 = zeros(shape(dataSet))   #初始化归一化数据集
    m = dataSet.shape[0]      #行
    normDataSet = dataSet - tile(minVals, (m,1))
    normDataSet = normDataSet/tile(ranges, (m,1))   #特征值相除
    return normDataSet, ranges, minVals       #返回归一化矩阵,取值范围以及最小值

#计算分类器的错误率和错误个数
def datingClassTest():
    hoRatio = 0.50      #取测试样本占数据集样本的10%
    datingDataMat,datingLabels = file2matrix('datingTestSet2(1).txt')       #得到样本集,样本标签
    normMat, ranges, minVals = autoNorm(datingDataMat)       #得到归一化样本集,取值范围以及最小值
    m = normMat.shape[0]         #样本集行数
    numTestVecs = int(m*hoRatio)         #测试样本集数量
    errorCount = 0.0                     #初始化错误率
    for i in range(numTestVecs):         #循环,计算测试样本集错误数量
        classifierResult = classify0(normMat[i,:],normMat[numTestVecs:m,:],datingLabels[numTestVecs:m],3)  #通过k-近邻算法进行分类,并记录结果
        print ("the classifier came back with: %d, the real answer is: %d" % (classifierResult, datingLabels[i]))
        if (classifierResult != datingLabels[i]): errorCount += 1.0           #算法结果与样本的实际分类做对比
    print("the total error rate is: %f" % (errorCount/float(numTestVecs)))   #计算错误率,并输出
    print(errorCount)         #输出错误的个数

#约会网站的对象判断
def classifyperson():
    resultList = ['not at all', 'in small doses', 'in large doses']   #喜欢程度的预测值列表
    #输入约会对象的一些特征
    percentTats = float(input("percentage of time spent playing video games?"))
    ffMiles = float(input("frequent flier miles earned per year?"))
    iceCream = float(input("liters of ice cream consumed per year?"))
    datingDatamat,datingLabels = file2matrix('datingTestSet2(1).txt')   # 从文本文件中解析数据
    normMat, ranges, minVals = autoNorm(datingDatamat)    #特征归一化处理
    inArr = array([ffMiles, percentTats, iceCream])       #输入的约会对象的特征
    classifierResult = classify0((inArr - minVals) / ranges, normMat, datingLabels, 3)  #判断约会对象的类型
    print("You will probably like this person: ", resultList[classifierResult - 1])



#将图像转换为测试向量
def img2vector(filename):
    returnVect = zeros((1,1024))   #返回一个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])   #这个向量本身也完全是由01构成,相当于将原来的矩阵每一行首尾相连
    return returnVect

#识别手写数字
def handwritingClassTest():
    hwLabels = []
    trainingFileList = listdir('trainingDigits')           #导入训练集,‘trainingDigits’是一个文件夹
    m = len(trainingFileList)                       #计算训练样本个数
    trainingMat = zeros((m,1024))                   #初始化数据集,将所有训练数据用一个m行,1024列的矩阵表示
    for i in range(m):
        fileNameStr = trainingFileList[i]   #获得所有文件名,文件名格式‘x_y.txt’,x表示这个手写数字实际表示的数字(label)
        fileStr = fileNameStr.split('.')[0]     #去除 .txt
        classNumStr = int(fileStr.split('_')[0])   #classnumber为每个样本的分类,用‘_’分割,取得label
        hwLabels.append(classNumStr)         #将所有标签都存进hwLables[]
        trainingMat[i,:] = img2vector('trainingDigits/%s' % fileNameStr)     #将文件转化为向量后存入trainingMat[],这里展现了灵活的文件操作
    testFileList = listdir('testDigits')        #迭代测试集
    errorCount = 0.0
    mTest = len(testFileList)
    for i in range(mTest):
        fileNameStr = testFileList[i]
        fileStr = fileNameStr.split('.')[0]     #去除 .txt
        classNumStr = int(fileStr.split('_')[0])
        vectorUnderTest = img2vector('testDigits/%s' % fileNameStr)  #这部分针对测试集的预处理和前面基本相同
        classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3)    #使用算法预测样本所属类别,调用了前面写的classify0()函数
        print ("the classifier came back with: %d, the real answer is: %d" % (classifierResult, classNumStr))
        if (classifierResult != classNumStr): errorCount += 1.0       #算法结果与样本的实际分类做对比
    print ("\nthe total number of errors is: %d" % errorCount)
    print ("\nthe total error rate is: %f" % (errorCount/float(mTest)))

if __name__ == "__main__":  # 当程序执行时
    # 调用基础函数
     group,labels = createDataSet()
     a = classify0([0,0], group, labels, 3)
     print(a)
    # 从文件中解析数据
     f = open("datingTestSet2(1).txt", "r")
     datingDatamat,datingLabels = file2matrix('datingTestSet2(1).txt')
     print(datingDatamat)
     print(datingLabels[0:20])
    # 创建散点图,散点图使用矩阵的第二、第三列数据,分别表示特征值
     fig = plt.figure()
     ax = fig.add_subplot(111)
     ax.scatter(datingDatamat[:,1], datingDatamat[:,2], 15.0*array(datingLabels), 15.0*array(datingLabels))
     matplotlib.pyplot.show()
    # 归一化特征值
     normMat, ranges, minVals = autoNorm(datingDatamat)
     print(normMat)
     print(ranges)
     print(minVals)
    # 计算分类器的错误率和错误个数
     datingClassTest()
    # 约会网站的对象判断
     classifyperson()
    # 将图像转换为测试向量
     testVector = img2vector('testDigits/0_13.txt')
     print(testVector[0,0:31])
     print(testVector[0,32:63])
    # 计算分类器的错误率和错误个数
     handwritingClassTest()


3 实战心得

以上是我整理的k-近邻算法的相关知识和代码解析,望对大家有用。由于初写,如有纰漏或者错误,欢迎指出。

以下是对K-近邻算法的评价:
1.算法执行效率低:算法需要为每个测试向量做大约2000次距离计算,每个距离计算包括了1024个维度浮点计算,总共要执行900次。此外,还需要为测试向量准备一定的存储空间。所以这个算法不仅耗时,而且需要不少的存储空间。

2.k-临近算法的另一个缺陷就是它无法给出任何数据的基础结构信息,因此我们无法得知平均实例样本和典型实例样本具体有什么特征。

3.k-近邻算法是分类数据最简单最有效的算法,并且精度高、对异常值不敏感、无数据输入假定。

4.K决策树就是k-近邻算法的优化版,可以节省大量的计算开销。

[1]: 《机器学习实战》[美] Peter Harrington 著 李锐 李鹏 曲亚东 王斌 译

你可能感兴趣的:(机器学习实战,机器学习,人工智能,近邻算法)