第二章 K-近邻算法(机器学习实战)

第二章 K-近邻算法

一个简单的直观的K-近邻法分类算法的理解

  • 电影题材分类: 基于电影中出现的亲吻、打斗出现的次数,使用k-近邻算法自动划分电影的题材类型。

K-近邻法概述

  • k-近邻算法采用测量不同特征值之间的距离方法进行分类。

  • 工作原理:

    存在一个样本数据集合,样本集中每个数据都存在标签,输入没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本集中特征最相似数据(最近邻)的分类标签。一般来说,我们只选择样本数据集中前k个最相似的数据,这就是k近邻算法中k的出处,通常k是不大于20的整数。 现在我们得到了样本集中所有电影与未知电影的距离,按照距离递增排序,可以找到k个距 离最近的电影。假定k=3,则三个最靠近的电影依次是He s Not Really into Dudes、Beauiful Woman和California Man。k-近邻算法按照距离最近的三部电影的类型,决定未知电影的类型,而这三部电影全是爱情片,因此我们判定未知电影是爱情片。

  • 假如有一部未看过的电影, 如何确定它是爱情片还是动作片呢?

    image-20190319112830361

    我们可以统计知道的电影,打斗镜头和接吻镜头的出现次数,以及对应的电影类型。

    image-20190319112923158

    然后计算未知电影与已知电影的距离,然后判断它是什么类型片。

    image-20190319113008062

    按照距离递增排序,可以找到K个距离最近的电影,假定k=3,最近的三部电影全是爱情片,由此我们判断未知电影是爱情片。

使用Python导入数据

  • createDataSet()函数,创建数据集和标签

    from numpy import *
    import operator
    
    def createDataSet():
      	'''建立了一个数据集,在这个数据集里,group代表了其具体坐标位置,labels代表了其标签(属性)'''
        group = array([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]])
        labels = ['A','A','B','B']
        return group, labels
    
    group,labels = createDataSet()
    print(group)
    print(labels)
    
    [[1.  1.1]
     [1.  1. ]
     [0.  0. ]
     [0.  0.1]]
    ['A', 'A', 'B', 'B']
    

    向量label包含了每个数据点的标签信息,label包含的元素个数等于group矩阵行数。

    image-20190319114037065

从文本文件中解析数据

  • 伪代码

    对未知类别属性的数据集中的每个点依次执行以下操作:

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

    # inX是被分类的向量,dataSet是数据集,labels是数据集标签,k是参数
    def classify0(inX, dataSet, labels, k): 
        '''基于上面的数据集建立了一个基本分类器,该分类器將通过坐标,对其可能的属性是‘A’还是‘B’进行预测。'''
        # 首先计算已知类别数据集与当前点的距离
        dataSetSize = dataSet.shape[0] #读取数据集的行数,并把行数放到dataSetsize里,shape[]用来读取矩阵的行列数,shape[1]表示读取列数
        # 1. 距离计算 将矩阵作为分块子阵,创建dataSetSize行,1列的矩阵,并且整个矩阵减去数据集,整个矩阵变成了坐标差
        # tile(inX,(dataSetSize,1))复制比较向量inX,tile的功能是告诉inX需要复制多少遍
        # 这里复制成(dataSetSize行,一列)目的是把inX转化成与数据集相同大小,再与数据集矩阵相减,形成的差值矩阵存放在diffMat里
        diffMat = tile(inX, (dataSetSize, 1)) - dataSet 
        # 注意这里是把矩阵里的各个元素依次平方
        sqDiffMat = diffMat ** 2 
        # 将所有列向量求和
        # axis就是沿着第二个轴的意思,也就是列 
        # 实现计算计算结果,axis表矩阵每一行元素相加,即行范数
        # 现在得到列一个1行dataSet列的矩阵,代表的是每个数据和这个点的距离 
        sqDistances = sqDiffMat.sum(axis=1) 
        # 开根号
        distances = sqDistances ** 0.5 
        # 按照距离递增次序排序
        # 利用argsort()方法得到排序后的下标
        # 使用argsort排序,返回从小到大的"顺序值"
        sortedDisIndicies = distances.argsort() 
    
        # 对k个邻居进行投票
        # 新建一个字典,用于计数
        classCount = {} 
        # 2. 选取与当前点距离最小的k个点
        # 按顺序对标签进行统计
        for i in range(k): 
          	# 按照之前排序值依次对标签进行计数
            voteIlabel = labels[sortedDisIndicies[i]] 
            # 这一步就可以直接利用map.get()中的默认值,创建不存在的key
            # 对字典进行抓取,此时字典是空的
            classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1 
        # 3. 排序
        # 所以没有标签,现在将一个标签作为key,value就是label出现次数,因为数组从0开始,但计数从1开始,所以需要加1
        sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1),reverse=True)
        # 返回一个列表按照第二个元素降序排列
        # 返回出现次数最多但label值,即为当前点但预测分类
        return sortedClassCount[0][0] 
    
    • classify0()函数有4个输入参数:用于分类的输入向量是inX,输入的训练样本集为dataSet,标签向量为labels,最后的参数k表示用于选择最近邻居的数目,其中标签向量的元素数目和矩阵dataSet的行数相同。

    • 计算两个向量点之间的距离为欧氏距离公式。

      image-20190319134054673

      如果数据集存在4个特征值,则点(1,0,0,1)与(7,6,9,4)之间的距离计算为:

      image-20190319134145936

    • 计算完所有点之间的距离后,可以对数据按照从小到大的次序排序。然后,确定前k个距离最小元素所在的主要分类❷,;最后,将classCount字典分解为元组列表,然后使用程序第二行导人运算符模块的itemgetter方法,按照第二个元素的次序对元组进行排序❸。此处的排序为逆序,即按照从最大到最小次序排序,最后返回发生频率最高的元素标签。

    • 为了预测数据所在分类,在Python提示符中输入下列命令:

      classify0([0,0],group,labels,3)
      
      # 'B'
      

使用k-近邻算法改进约会网站的配对效果

image-20190319185420583

  • file2matrix()函数用来处理输入格式问题,将文本记录转化为Numpy矩阵

    # 该函数的输入为文件名字符串,输出为训练样本矩阵和类标签向量
    # 从文件中读入训练数据,并存储为矩阵 
    # 将文本记录转化为Numpy矩阵
    def file2matrix(filename): 
      	# 打开文件,存到fr里
        fr = open(filename) 
        # 按行读取,并存到arrOlines里
        arrayOLines = fr.readlines() 
        # 1. 得到文件行数 # 获取 n=样本的行数 # 读取其行数
        numberOfLines = len(arrayOLines) 
        # 建立文本文件行数,3列到矩阵,以后整理到文件存在这里面
        # 2. 创建返回的NumPy矩阵 # 创建一个2维矩阵用于存放训练样本数据,一共有n行,每一行存放3个数据
        returnMat = zeros((numberOfLines,3)) 
        # 创建一个1维数组用于存放训练样本标签。
        classLabelVector = [] 
        # 索引值先清0
        index = 0 
        # 3. 按行读取文本,并依次给其贴标签,解析文件数据到列表
        for line in arrayOLines:
          	# 把回车符号给去掉 # 将文本每一行首尾的空格去掉
            line = line.strip() 
            # 把每一行数据用\t分割 # 矩阵中,每遇到一个'\t',便依次将这一部分赋给一个元素
            listFromLine = line.split('\t') 
            # 将每一行的前三个元素依次赋予之前预留矩阵空间
            returnMat[index, :] = listFromLine[0:3] # 把分割好的数据放至数据集,其中index是该样本数据的下标,就是放到第几行-
            #labels = {'didntLike': 1, 'smallDoses': 2, 'largeDoses': 3}  # 新增
            #classLabelVector.append(listFromLine[-1]) # 去掉了int
            # 把该样本对应的标签放至标签集,顺序与样本集对应。 python语言中可以使用-1表示列表中的最后一列元素
            # 对于每行最后一列,按照其值的不同,来给单列矩阵赋值
            if(listFromLine[-1]=='largeDoses'):
                classLabelVector.append(3)
            elif listFromLine[-1]=='smallDoses':
                classLabelVector.append(2)
            elif listFromLine[-1]=='didntLike':
                classLabelVector.append(1)
            # 每执行一次,便向下一行再循环
            index += 1 
        # 返回两个矩阵,一个是三个特征组成的特征矩阵,另一个为类矩阵
        return returnMat, classLabelVector 
    

    从上面的代码可以看到, Python处理文本文件非常容易。首先我们需要知道文本文件包含多少行。打开文件,得到文件的行数➊。然后创建以零填充的矩阵NumPy@ (实际上, NumPy是一个二维数组,这里暂时不用考虑其用途)。为了简化处理,我们将该矩阵的另一维度设置为固定值3,你可以按照自己的实际需求增加相应的代码以适应变化的输入值。循环处理文件中的每行数据❸,首先使用函数line. strip()截取掉所有的回车字符,然后使用tab字符\t将上一步得到的整行数据分割成一个元素列表。接着,我们选取前3个元素,将它们存储到特征矩阵中。Python语言可以使用索引值-1表示列表中的最后-列元素,利用这种负索引,我们可以很方便地将列表的最后一列存储到向量classLabelVector中。需要注意的是,我们必须明确地通知解释器,告
    诉它列表中存储的元素值为整型,否则Python语 言会将这些元素当作字符串处理。以前我们必须自已处理这些变量值类型问题,现在这些细节问题完全可以交给NumPy函数库来处理。

  • 查看将四行的文本记录,转换为三行+1类的矩阵。

    datingDataMat, datingLabels = file2matrix('第二章 k-近邻法//datingTestSet.txt')
    print(datingDataMat)
    print(datingLabels)
    

    image-20190319191818537

  • 现在已经从文本文件中导入了数据,并将其格式化为想要的格式,接着我们需要了解数据的真实含义。当然我们可以直接浏览文本文件,但是这种方法非常不友好,一般来说,我们会采用图形化的方式直观地展示数据。下面就用Python工具来图形化展示数据内容,以便辨识出一些数据模式。

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

  • 输出datingDataMat矩阵的第二、第三列数据,分别表示特征值"玩视频游戏所耗时间百分比"和"每周所消费的冰淇淋公升数"。

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

    image-20190319192538410

  • sactter()函数支持个性化标记散点图上的点

    import matplotlib.pyplot as plt
    fig = plt.figure()
    ax = fig.add_subplot(111)
    # 只在scatter函数里添加了两个参数
    ax.scatter(datingDataMat[:,1], datingDataMat[:,2],15.0*array(datingLabels),15.0*array(datingLabels))
    print(plt.show())
    

    image-20190319192836616

准备数据:归一化数值

image-20190319193139045

我们很容易发现,上面方程中数字差值最大的属性对计算结果的影响最大,也就是说,每年获取的飞行常客里程数对于计算结果的影响将远远大于表2-3中其他两个特征——玩视频游戏的和每周消费冰淇淋公升数——的影响。 而产生这种现象的唯- -原因,仅仅是因为飞行常客里程数远大于其他特征值。但海伦认为这三种特征是同等重要的,因此作为三个等权重的特征之一,飞行常客里程数并不应该如此严重地影响到计算结果。

在处理这种不同取值范围的特征值时,我们通常采用的方法是将数值归一化,如将取值范围处理为0-1或-1到1之间。

下面的公式可以将任意取值范围的特征值转化为0-1区间的值。

newValue = (oldValue-min)/(max-min)
  • 归一化特征值

    # 对每个特征进行归一化处理
    def autoNorm(dataSet): 
      	# 取数据集最小值
        minVals = dataSet.min(0) 
        # 取数据集最大值
        maxVals = dataSet.max(0) 
        # 取差值即为范围
        ranges = maxVals - minVals
        # 建立一个新0矩阵,其行数列数与数据集一致,处理后数据存这里
        normDataSet = zeros(shape(dataSet)) 
        # 读取数据集行数
        m = dataSet.shape[0] 
        # 现有数据集减去最小值矩阵
        normDataSet = dataSet - tile(minVals, (m,1)) 
        # 特征值相除 # 归一化处理
        normDataSet = normDataSet/tile(ranges, (m,1)) 
        return  normDataSet, ranges, minVals
    

    特征值矩阵有1000*3个值,而minVals和range的值都为1 * 3,为了解决这个问题,我们使用Numpy库中的tile()函数将变量内容复制成输入矩阵同样大小的矩阵。

  • 在Numpy库中,矩阵除法需要使用函数linalg.solve(matA,matB)

  • 检测函数的执行结果

    normMat, ranges, minVals = autoNorm(datingDataMat)
    print(normMat)
    print(ranges)
    print(minVals)
    
    [[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]]
    [9.1273000e+04 2.0919349e+01 1.6943610e+00]
    [0.       0.       0.001156]
    
  • 测试算法:作为完整程序验证分类器

  • 分类器针对约会网站的测试代码

    # 对数据进行测试
    def datingClassTest(): 
      	# 载入数据
        filename = "第二章 k-近邻法//datingTestSet.txt" 
        # 提取数据为10%
        hoRatio = 0.10  
        # 将文本数据转化为numpy矩阵
        datingDataMat, datingLabels = file2matrix('第二章 k-近邻法//datingTestSet.txt') 
        # 数据归一化
        normMat, ranges, minVals = autoNorm(datingDataMat)
        # 获取数据矩阵行数
        m = normMat.shape[0] 
        # 获取测试集数据行数
        numTestVecs = int(m*hoRatio) 
        # 错误数先清0
        errorCount = 0.0 
        # 获取测试集对错误率
        for i in range(numTestVecs):
            # 执行之前对分类器,并将分类结果方法classifierResult里
            classiferResult = classify0(normMat[i, :], normMat[numTestVecs:m,:],datingLabels[numTestVecs:m], 3)
            print ("the classifier came back with: %d, the real answer is: %d" % (classiferResult, datingLabels[i]))
            # 如果分类器的结果与标签值不一致,则将错误数加一
            if (classiferResult != datingLabels[i]) :
                errorCount += 1.0
        #输出错误率
        print("the total error rate is: %f" % (errorCount/float(numTestVecs)*100)) 
    

    image-20190319195626911

使用算法:构建完整可用系统

  • 约会网站预测函数

    # 约会对象评价预测
    def classifyPerson(): 
      	# 输出结果可能列表
        resultList = ['not at all', 'in small doses', 'in large doses'] 
        # 首先需要键入花在游戏上时间比重
        percenTats = 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, datingLables = file2matrix('datingTestSet2.txt') 
        # 几个关键数据量归一化,便于处理
        normMat, ranges, minVals = autoNorm(datingDataMat) 
        # 将刚刚键入的数组成预测集,便于带入现有模型预测
        inArr = array([ffMiles, percenTats, iceCream]) 
        # 将预测结果存储
        classifierResult= classify0((inArr-minVals)/ranges,normMat,datingLables,3) 
        # 给出预测结果
        print("You will probably like this person: ",resultList[classifierResult - 1]) 
    

    image-20190319200443580

手写识别系统

  • 需要识别的数字已经使用图形处理软件,处理成具有相同的色彩和大小;宽高是32像素*32像素的黑白图像。

  • trainingDigits中包含了大约2000个例子,每个数字大约有200个样本。testDigits中包含了大约900个测试数据。

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

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

    # 将图像转化为向量
    def img2vector(filename): 
      	# 构建预存一维向量,大小之所以为1024是因为图片大小为32*32=1*1024
        returnVect = zeros((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]) 
        # 返回处理好的一维向量
        return returnVect 
    

    测试:

    testVector = img2vector('第二章 k-近邻法//digits//testDigits/0_13.txt')
    print(testVector[0,0:31])
    print(testVector[0,32:63])
    
    [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. 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.]
    
  • 测试算法:使用k-近邻算法识别手写数字

    import os
    def handwritingClassTest():
      	# 测试集的标签矩阵
        hwLabels = [] 
        # 获取目录内容 # 返回trainingDigits目录下的文件名
        trainingFileList = os.listdir('第二章 k-近邻法//digits//trainingDigits') 
        # 返回文件夹下文件的个数
        m = len(trainingFileList) 
        # 初始化训练的mat矩阵,测试集向量大小为训练数据个数*1024,即多少张图像,就有多少行,一行存一个图像
        traingMat = zeros((m,1024)) 
        # 从文件名解析分类数字
        for i in range(m): # 从文件名中解析出训练集的类别标签
          	# 获得文件的名字
            fileNameStr = trainingFileList[i] 
            fileStr = fileNameStr.split('.')[0]
            # 第一个字符串存储标签,故取分离后的第一个元素,即相当于获取了该图像类别标签
            classNumStr = int(fileStr.split('_')[0]) 
            # 将获得的类别标签添加到hwLabels中
            hwLabels.append(classNumStr) 
            # 将每一个文件的1*1024数据存储到trainingMat中
            traingMat[i,:] = img2vector('第二章 k-近邻法//digits//trainingDigits//%s' % fileNameStr) 
        # 返回testDigits目录下的文件列表
        testFileList = os.listdir('第二章 k-近邻法//digits//trainingDigits') 
        # 错误检测计数,初始值为0
        errorCount = 0.0 
        # 测试数据的数量
        mTest = len(testFileList) 
        # 从文件中解析出测试集的类别并进行分类测试
        for i in range(mTest): 
          	# 获得文件的名字
            fileNameStr = testFileList[i] 
            fileStr = fileNameStr.split('.')[0]
            # 获得分类的数字标签
            classNumStr = int(fileStr.split('_')[0]) 
            # 获得测试集的1*1024向量,用于训练
            vetorUnderTest = img2vector('第二章 k-近邻法//digits//trainingDigits//%s' % fileNameStr) 
            # 获得预测结果
            classifierResult = classify0(vetorUnderTest, traingMat, hwLabels, 3) 
            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))) 
    

    image-20190319202548661

k-近邻算法识别手写数字数据集,错误率为1.1%。改变变量k的值、修改函数handwxiting-ClassTest随机选取训练样本.改变训练样本的数目,都会对k近邻算法的错误率产生影响,感兴趣的话可以改变这些变量值,观察错误率的变化。
实际使用这个算法时,算法的执行效率并不高。因为算法需要为每个测试向量做2000次距离计算,每个距离计算包括了1024个维度浮点运算,总计要执行900次,此外,我们还需要为测试向量准备2MB的存储空间。是否存在-种算法减少存储空间和计算时间的开销呢? k决策树就是k-近邻算法的优化版,可以节省大量的计算开销。

###总结

k-近邻算法是分类数据最简单最有效的算法 k-近邻算法是基于实例的学习,使用算法时我们必须有接近实际数据的训练样本数据。 k-近邻算法必须保存全部数据集,如果训练数据集的很大,必须使用大量的存储空间。此外, 由于必须对数据集中的每个数据计算距离值,实际使用时可能非常耗时。 k-近邻算法的另一个缺陷是它无法给出任何数据的基础结构信息,因此我们也无法知晓平均 实例样本和典型实例样本具有什么特征。 下一章我们将使用概率测量方法处理分类问题,该算法可以解决这个问题。

猜你喜欢

转载自blog.csdn.net/qq_39362996/article/details/88672623