《机器学习实战》读书笔记——k-近邻算法

1 k-近邻算法概述

  • k-近邻算法采用测量不同特征值之间的距离方法进行分类。
  • 优点:精度高、对异常值不敏感、无数据输入假定
  • 缺点:计算复杂度高、空间复杂度高
  • 适用数据范围:数值型和标称型
1.1 k-近邻算法工作原理:

存在一个样本数据集合(训练集),并且样本集中每个数据都有对应的标签。在输入一个没有标签的新数据时,将这个新数据的每个特征与样本集中数据对应的特征进行比较,然后从样本集中提取出与新数据特征最相似的前k个数据的标签。在这k个标签中,出现次数最多的标签,就认为是新数据的标签。

上面说的最相似,表现在可视化中就意味着最近邻。比如判断电影是爱情片还是动作片的二分类问题,在打斗和接吻镜头的比例图中,我们分别标出了样本集位于图中的位置,问号表示未知电影的位置:
在这里插入图片描述
在这里插入图片描述
然后通过数学方法计算出每个样本点与输入数据的欧式距离:

计算两个向量点 x A x_A xA x B x_B xB之间的距离:
d = ( x A 0 − x B 0 ) 2 + ( x A 1 − x B 1 ) 2 d=\sqrt{\left(x_{A_{0}}-x_{B_{0}}\right)^{2}+\left(x_{A_{1}}-x_{B_{1}}\right)^{2}} d=(xA0xB0)2+(xA1xB1)2
例如,点(0, 0)与(1, 2)之间的距离计算为:
( 1 − 0 ) 2 + ( 2 − 0 ) 2 \sqrt{(1-0)^{2}+(2-0)^{2}} (10)2+(20)2
如果数据集存在4个特征值,则点(1, 0, 0, 1)与(7, 6, 9, 4)之间的距离计算为:
( 7 − 1 ) 2 + ( 6 − 0 ) 2 + ( 9 − 0 ) 2 + ( 4 − 1 ) 2 \sqrt{(7-1)^{2}+(6-0)^{2}+(9-0)^{2}+(4-1)^{2}} (71)2+(60)2+(90)2+(41)2

在这里插入图片描述
利用k-近邻算法,假设k为3,那么与新数据最近邻(最相似)的3个电影为He's Not Really into DudesBeautiful WomanCalifornia Man,这3个电影的标签都是“爱情片”,所以我们判断未知电影是“爱情片”。

1.2 k-近邻算法一般流程:

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


1.3 k-近邻的简单实现

k-近邻算法对未知类别属性的数据集中的每个点依次执行以下操作:
(1) 计算已知类别数据集中的点与当前点之间的距离;
(2) 按照距离递增次序排序;
(3) 选取与当前点距离最小的k个点;
(4) 确定前k个点所在类别的出现频率;
(5) 返回前k个点出现频率最高的类别作为当前点的预测分类。

# coding:utf-8
# python3.7
from numpy import *
import operator


def createDataSet():
    group = array([[1.0, 1.1],
                   [1.0, 1.0],
                   [0, 0],
                   [0, 0.1]])
    lables = ['A', 'A', 'B', 'B']
    return group, lables


def classify0(inx, dataSet, labels, k):
    """
    inx: 输入向量
    dataSet: 输入的训练样本集
    labels: 标签向量
    k: 选择最近邻的数目
    """
    dataSetSize = dataSet.shape[0]  # 行数4
    # tile():就是将原矩阵横向、纵向地复制
    # 计算欧式距离
    diffMat = tile(inx, (dataSetSize, 1)) - dataSet # 对每一个特征先取差值
    sqDiffMat = diffMat ** 2  # 再平方
    sqDistances = sqDiffMat.sum(axis=1)  # 再相加
    distances = sqDistances ** 0.5 # 开方
    sortedDistIndicies = distances.argsort()  # argsort()从小到大排序,返回索引位置
    classCount = {}
    for i in range(k):
        # 提取出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]

# just for test
if __name__ == "__main__":
    group, labels = createDataSet()
    print(classify0([0, 0], group, labels, 3))

上面代码中的classify0()实际上就是k-近邻算法的简单分类过程,输出的结果是B


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

步骤:
(1)手机数据:提供文本文件。
(2)准备数据:使用Python解析文本文件。
(3)分析数据:使用Matplotlib画二维扩散图。
(4)训练算法:此步骤不适用于k-近邻算法。
(5)测试算法:使用部分数据作为测试样本,测试样本的类别已知,若预测类别与实际类别不同,则标记为一个错误。
(6)使用算法:输入一些特征数据以判断预测分类。

2.1 解析数据

训练集datingTestSet.txt:

  • 每个样本占一行,每行包括3个特征和一个分类标签,共1000行。
  • 3个特征分别为:每年获得的飞行常客里程数、玩视频游戏所耗时间百分比、每周消费的冰淇淋公升数。

上面的数据集需要进行处理,将其转化为分类器可以接受的格式(训练样本矩阵、类标签向量等),代码如下:

def file2matrix(filename):
    fr = open(filename)
    arrayOLines = fr.readlines()
    numberOfLines = len(arrayOLines)
    returnMat = zeros((numberOfLines, 3))  # 创建与数据大小一致的numpy矩阵
    classLabelVector = []
    index = 0
    for line in arrayOLines:
        line = line.strip()
        listFromLine = line.split('\t')
        # 将文本数据前3列填到创建的numpy矩阵中
        returnMat[index, :] = listFromLine[0:3]
        # 填入分类的标签(最后一列)
        classLabelVector.append(int(listFromLine[-1]))
        index += 1
    return returnMat, classLabelVector

结果如下:
在这里插入图片描述

2.2 分析数据

使用Matplotlib库制作原始数据散点图:

import matplotlib
import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(datingDataMat[:, 1], datingDataMat[:, 2],
           15.0*array(datingLabels), 15.0*array(datingLabels))
plt.xlabel('Percent of time spent playing video games')
plt.ylabel('Ice cream liters consumed per week')
plt.show()

在这里插入图片描述

2.3 归一化数值

在这里插入图片描述
由前面所介绍的,如果我们要求上表中样本3和样本4之间的距离,计算方法如下:

( 0 − 67 ) 2 + ( 20000 − 32000 ) 2 + ( 1.1 − 0.1 ) 2 \sqrt{(0-67)^{2}+(20000-32000)^{2}+(1.1-0.1)^{2}} (067)2+(2000032000)2+(1.10.1)2

可以看到上式的结果主要会被里程数的差值所决定,而三个特征实际上应该同等重要,造成的原因仅仅是因为这个特征的数值大,这样就会使结果不准确。

为了避免这种情况的影响,我们需要将数值归一化,如将取值范围都处理到0至1或者-1至1之间,可以下面的公式可以将任意取值范围的特征值转化为0到1区间内的值:

newValue = (oldValue - min) / (max - min)

其中min和max是数据集中的最小和最大特征值。

归一化特征值的函数如下:

def autoNorm(dataSet):
	# 参数0使得函数从列中选取最小值
    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 测试算法

通常我们只提供已有数据的90%作为训练样本来训练分类器,而使用其余的10%数据(随机选取的)去测试分类器,检测分类器的正确率。

可以使用错误率来检测分类器的性能,对于分类器来说,错误率就是分类器给出错误结果的次数除以测试数据的总数。

该分类器的测试代码如下:

def datingClassTest():
    hoRatio = 0.10
    datingDataMat, datingLabels = file2matrix('datingTestSet2.txt')
    # 对数据进行归一化
    normMat, ranges, minVals = autoNorm(datingDataMat)
    m = normMat.shape[0]
    # 选取10%
    numTestVecs = int(m * hoRatio)
    errorCount = 0.0
    for i in range(numTestVecs):
        classifierResult = classify0(normMat[i, :], normMat[numTestVecs:m, :], \
                                     datingLabels[numTestVecs:m], 3)
        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)))

在这里插入图片描述
上述结果的错误率为5%,通过改变hoRatio和k的值,错误率可能会发生变化。

2.5 使用算法

我们在前面已经写好了分类器并进行了测试,现在要使用这个分类器,可以通过下面的预测函数:

def classifyPerson():
    resultList = ['not at all', 'in small doses', 'in large doses']
    percentTats = float(input("percentage of time spent playing video games?\n"))
    ffMiles = float(input("frequent flier miles earned per year?\n"))
    iceCream = float(input("liters of ice cream consumed per year?\n"))
    datingDataMat, datingLabels = file2matrix('datingTestSet2.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])

调用该函数结果如下:
在这里插入图片描述


3 手写识别系统

下面构建一个使用k-近邻分类器的手写识别系统,这里仅能识别数字0-9。需要识别的数字已经使用图形处理软件,处理成具有相同色彩和大小(32×32),并且为了方便理解,将图像转换为文本格式。

3.1 准备数据

目录trainingDigits中包含了大约2000个例子,每个例子的内容如下图所示(由0-1二进制矩阵组成),每个数字大约有200个样本:
在这里插入图片描述
目录testDigits中包含了大约900个测试数据。我们使用目录trainingDigits中的数据训练分类器,使用目录testDigits中的数据测试分类器的效果。

手写需要将一个32×32的二进制图形矩阵转换为1×1024的向量,这样才能使用分类器处理:

def img2vector(filename):
    returnVect = zeros((1, 1024))
    fr = open(filename)
    for i in range(32):
        linestr = fr.readlines()
        for j in range(32):
            returnVect[0, 32 * i + j] = int(linestr[j])
    return returnVect
3.2 测试算法

测试函数如下:

  • 需要使用到os库中的listdir()函数来遍历数据集。
  • 创建一个m行1024列的训练矩阵,m为训练集的数量,1024为每个训练数据的二进制矩阵转换而来的特征。
  • 每个数据的标签可以从文件名中获取,存在hwLabels中。
  • 因为每个特征均为0-1二进制,所以无需归一化。
  • 最后以同样的方式遍历测试集来进行预测,并计算错误率。
import os 
# ...
def handwritingClassTest():
    hwLabels = []
    trainingFileList = os.listdir('./digits/trainingDigits')  # 加载训练集
    m = len(trainingFileList)
    trainingMat = zeros((m, 1024))
    for i in range(m):
        # 从文件名获取分类标签添加到hwLabels中
        fileNameStr = trainingFileList[i]
        fileStr = fileNameStr.split('.')[0]
        classNumStr = int(fileStr.split('_')[0])
        hwLabels.append(classNumStr)
        # 数据处理
        trainingMat[i, :] = img2vector('./digits/trainingDigits/%s' % fileNameStr)
    # 测试集
    testFileList = os.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)
        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)))

在这里插入图片描述
上述程序的错误率为1.05%,实际还可以调整训练样本、k值等参数来对错误率进行优化。

2.4 总结

(1)k-近邻算法使分类数据最简单有效的算法,其基于实例的学习要求我们必须有接近实际数据的训练样本数据。

(2)对于数据集较大的情况时,k-近邻算法就不是一个搞笑的算法。

  • 其必须保存全部数据集,如果训练数据集很大,就会消耗大量的存储空间。
  • 对数据集中的每个数据计算距离,也非常的耗时。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值