欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-03-19 16:43:10
...

在上一文中:初识K-近邻算法。已经介绍了kNN(k-近邻算法)的工作原理和代码实现,这次将讲述《机器学习实战》中的一个案例,使用kNN算法来改进越会网站的配对效果。


案例的描述及kNN流程。

海伦一直使用在线约会网站寻找适合自己的约会对象。尽管约会网站会推荐不同的人选,但是她没有从中找到喜欢的人。经过一番总结,她发现曾交往过三中类型的人:

  • 不喜欢的人。
  • 魅力一般的人。
  • 极具魅力的人。

尽管发现了上述规律,但海伦依然无法将约会网站推荐的匹配对象归入恰当的分类。她觉得可以在周一到周五约会那些魅力一般的人,而周末更喜欢与那些极具魅力的人为伴。海伦希望我们的分类软件可以更好地帮助她将匹配对象划分到确切的分类中。此外海伦还收集了一些约会网站未曾记录的数据信息,她认为这些数据更有助于匹配对象的归类。

海伦的情况就是这样,想让我们利用她给的数据,然后帮助她分析一下对某人的感觉会是什么。虽然不明白海伦为什么会和不喜欢的人交往,但是确实发生了。

问题出现了就需要解决,使用kNN的一般流程上一文也说明了,在本次的案例中kNN的流程如下:

  1. 收集数据:提供文本文件。
  2. 准备数据:使用Python解析文本文件。
  3. 分析数据:使用Matplotlib画二维扩散图。
  4. 训练算法:此步骤不适用于k-近邻算法。
  5. 测试算法:使用海伦提供的部分数据作为测试样本。测试样本和非测试样本的区别在于:测试样本是已经完成分类的数据,如果预测分类与实际类别不同,则标记为一个错误。
  6. 使用算法:产生简单的命令行程序,然后海伦可以输入一些特征数据以判断对方是否为自己喜欢的类型。

那么流程已经说完了,下面就是分步骤完成本次程序。


准备数据:从文本文件中解析数据。

海伦收集约会数据已经有了一段时间,她把这些数据存放在文本文件datingTestSet.txt中,每个样本数据占据一行,总共有1000行。海伦的样本主要包含以下3种特征:

  • 每年获得的飞行客场里程数。
  • 玩视频游戏所耗时间百分比。
  • 每周消费的冰淇淋公斤数。

数据集长这个样子:

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

数据一共有四列,第一列是每年获得的飞行常客里程数,第二列是玩视频游戏所耗时间百分比,第三列是每周消费的冰淇淋公斤数,第四列是对这个人的喜欢程度:largeDoses(极具魅力)、smallDoses(魅力一般)、didntLike(不喜欢)。

看到这些数据集,依据到kNN的工作原理,我们就知道了我们需要计算新数据和这些样本数据的距离,然后得到距离最近的数据的分类标签。

也就是我们最后需要输入:
特征1:每年获得的飞行客场里程数;
特征2:玩视频游戏所耗时间百分比。
特征3:每周消费的冰淇淋公斤数。

然后kNN算法会计算出一个分类,这个分类共有三种情况:largeDoses(极具魅力)、smallDoses(魅力一般)、didntLike(不喜欢)。

我们在将上述特征数据输入到分类器(也就是上一文中创建的classify0方法)之前,必须将待处理数据的格式改变为分类器可以接受的格式,也就是一个numpy.ndarray类型的数组。所以我们创建一个file2matrix的函数,用来处理输入格式的问题,该函数的输入为文本文件名称,输出为训练样本矩阵和分类标签向量,分类向量的话我们将不喜欢的人使用1代替,魅力一般的人用2代替,极具魅力的人用3代替。

file2matrix函数:

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from KNN import kNN


def file2matrix(filename):
    """
    将文件转换为特征矩阵和对应的标签
    :param filename:
    :return:
    """
    # 打开文件
    fr = open(filename)
    # 按照行进行读取
    arrayOLines = fr.readlines()
    # 计算出总行数,为1000行
    numbserOfLines = len(arrayOLines)
    # 创建一个1000行的0矩阵
    returnMat = np.zeros((numbserOfLines, 3))
    classLabelVector = []
    index = 0

    for line in arrayOLines:
        # 去除前后空格
        line = line.strip()
        # 按照\t将每行内容分割开来
        listFormLine = line.split('\t')
        # 将得到前三项的内容填充到对应的行数矩阵上
        returnMat[index, :] = listFormLine[0:3]
        # 按照喜欢程度进行添加标签
        # classLabelVector.append(int(listFormLine[-1]))
        if listFormLine[-1] == 'largeDoses':
            classLabelVector.append(3)
        if listFormLine[-1] == 'smallDoses':
            classLabelVector.append(2)
        if listFormLine[-1] == 'didntLike':
            classLabelVector.append(1)
        # 行数索引加一
        index += 1

    return returnMat, classLabelVector


if __name__ == '__main__':
    # 拿到数据
    datingDataMat, datingLabels = file2matrix('datingTestSet.txt')
    print(datingDataMat)
    print(datingLabels)

输出内容就是文本中的数据和分类标签:

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


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

Matplotlib是一个非常好用的可视化绘图库,我们将创建两个散点图:

  • 第一个散点图:x轴代表:每年获得飞行客场里程数。y轴代表:玩视频游戏所消耗时间百分比。
  • 第二个散点图:x轴代表:玩视频游戏所消耗时间百分比。y轴代表:每周消耗冰淇淋公斤数。

分别使用两种不同的作图思路进行绘画。蓝色的小点代表不喜欢,黄色的中等点代表魅力一般,红色的大点代表极具魅力。

将用来画图的dataShow函数添加到我们的代码中:

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from KNN import kNN


def file2matrix(filename):
    """
    将文件转换为特征矩阵和对应的标签
    :param filename:
    :return:
    """
    # 打开文件
    fr = open(filename)
    # 按照行进行读取
    arrayOLines = fr.readlines()
    # 计算出总行数,为1000行
    numbserOfLines = len(arrayOLines)
    # 创建一个1000行的0矩阵
    returnMat = np.zeros((numbserOfLines, 3))
    classLabelVector = []
    index = 0

    for line in arrayOLines:
        # 去除前后空格
        line = line.strip()
        # 按照\t将每行内容分割开来
        listFormLine = line.split('\t')
        # 将得到前三项的内容填充到对应的行数矩阵上
        returnMat[index, :] = listFormLine[0:3]
        # 按照喜欢程度进行添加标签
        # classLabelVector.append(int(listFormLine[-1]))
        if listFormLine[-1] == 'largeDoses':
            classLabelVector.append(3)
        if listFormLine[-1] == 'smallDoses':
            classLabelVector.append(2)
        if listFormLine[-1] == 'didntLike':
            classLabelVector.append(1)
        # 行数索引加一
        index += 1

    return returnMat, classLabelVector


def dataShow(datingDataMat, datingLabels):
    """
    将矩阵可视化,画出每年获得的飞行常客里程数和玩视频游戏所消耗时间百分比的图示
    :param datingDataMat:矩阵
    :param datingLabels:矩阵标签
    :return:
    """

    # 将对应的标签转换为颜色
    LabelsColors = []
    for i in datingLabels:
        if i == 1:
            LabelsColors.append("b")  # 黑色不喜欢
        if i == 2:
            LabelsColors.append("y")  # 黄色一般喜欢
        if i == 3:
            LabelsColors.append("r")  # 红色很喜欢

    # 能够显示中文
    matplotlib.rcParams['font.sans-serif'] = ['SimHei']
    matplotlib.rcParams['font.serif'] = ['SimHei']

    # matplotlib的图像都位于Figure对象中,设置大小和分辨率
    fig = plt.figure(figsize=(12, 7), dpi=80)

    # 第一个图,用来显示每年获得的飞行常客里程数和玩视频游戏所消耗时间百分比
    ax1 = fig.add_subplot(1, 2, 1)
    # 设置x标签
    ax1.set_xlabel('每年获得飞行常客里程数')
    # 设置y标签
    ax1.set_ylabel('玩视频游戏所消耗时间百分比')
    # 添加数据,第一列就是飞行里程数,第二列是玩视频游戏的时间
    ax1.scatter(x=datingDataMat[:, 0], y=datingDataMat[:, 1], color=LabelsColors,
                s=(15.0 * np.array(datingLabels), 15.0 * np.array(datingLabels)), marker='o')

    didnt_Like = ax1.scatter([], [], c='b', s=15)
    smallDoses = ax1.scatter([], [], c='y', s=30)
    largeDoses = ax1.scatter([], [], c='r', s=45)
    # 添加标签
    ax1.legend((didnt_Like, smallDoses, largeDoses), ('不喜欢', '魅力一般', '极具魅力'), loc='best')

    # 另一种作图思路,作出玩视频游戏所消耗百分比(x)和每周消耗冰淇淋公斤数(y)的图
    ax2 = fig.add_subplot(1, 2, 2)
    # 设置x标签
    ax2.set_xlabel('玩视频游戏所消耗百分比')
    # 设置y标签
    ax2.set_ylabel('每周消耗冰淇淋公斤数')

    # 遍历标签,划分出三种类别的坐标
    ice_didntLike = []  # 不喜欢类型的冰淇淋公斤数
    game_didntLike = []  # 不喜欢类型的玩游戏时间

    ice_smallDoses = []  # 一般魅力的冰淇淋公斤数
    game_smallDoses = []  # 一般魅力你的玩游戏时间

    ice_largeDoses = []  # 极大魅力的冰淇淋公斤数
    game_largeDoses = []  # 极大魅力的玩游戏时间

    num = 0  # 索引

    for i in datingLabels:
        if i == 1:  # 不喜欢
            ice_didntLike.append(datingDataMat[num, 2])
            game_didntLike.append(datingDataMat[num, 1])
        if i == 2:  # 一般魅力
            ice_smallDoses.append(datingDataMat[num, 2])
            game_smallDoses.append(datingDataMat[num, 1])
        if i == 3:  # 极大魅力
            ice_largeDoses.append(datingDataMat[num, 2])
            game_largeDoses.append(datingDataMat[num, 1])

        num += 1

    # 极大魅力绘制
    largeDoses = ax2.scatter(x=game_largeDoses, y=ice_largeDoses, s=45, c='r')
    # 一般魅力绘制
    smallDoses = ax2.scatter(x=game_smallDoses, y=ice_smallDoses, s=30, c='y')
    # 不喜欢绘制
    didntLike = ax2.scatter(x=game_didntLike, y=ice_didntLike, s=15, c='b')

    # 添加标签
    ax2.legend((didntLike, smallDoses, largeDoses), ('不喜欢', '魅力一般', '极具魅力'), loc='best')

    # 自动调整间距防止标签超过范围
    plt.tight_layout()
    plt.show()


if __name__ == '__main__':
    # 拿到数据
    datingDataMat, datingLabels = file2matrix('datingTestSet.txt')
    # print(datingDataMat)
    # print(datingLabels)

    # 数据可视化
    dataShow(datingDataMat, datingLabels)

运行结果,将散点图显示出来:

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

为什么要绘图呢?

因为我们可以从图中大概的看出不同类别的人对应的特征值的分布范围,比如说从第一张图就能够看出魅力一般的人每年飞行客场里程数大概分布在0到20000这个范围内。而从第二张图又能够看出,极具魅力的人玩视频游戏所消耗时间百分比大概分布在10左右。同样也能够从第二张图看出,貌似每周所消耗的冰淇淋公斤数三种类别的人分布都没有固定的聚集范围,都是比较分散的,这样就能够大概猜出其实喜欢不喜欢和每周所消耗的冰淇淋公斤数没有多大关系。

而且绘图的时候需要注意,不同的类别的点用不同颜色和大小进行绘制,这样绘制出来的图比较直观明了。不然绘画出来都是一样的黑点也看不出来什么东西。


准备数据:归一化数值。

在开始使用分类器之前,我们获得的数据还存在一些问题。

我们现在拿到的样本数据如下所示:

# 玩视频游戏所耗时间百分比 每年获得的飞行常客里程数 每周消费的冰淇淋公斤数 样本分类
1 0.8 400 0.5 1
2 12 134000 039 3
3 0 20000 1.1 2
4 67 32000 0.1 2

如果现在计算样本3和样本4之间的距离,按照kNN的工作原理我们使用的是下面的公式:

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

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

在处理这种不同取值范围的特征值时,我们通常采用的方法是将数据归一化,如将取值范围处理为0到1或者-1到1之间。这样的再次使用公式计算起来的话,就不会存在个别数值的差非常大的情况。我们可以使用下面的公式将任意取值范围的特征值转化为0到1区间内的值:

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

其中min和max分别是数据集中的最小特征值和最大特征值。虽然改变数值取值范围增加了分类器的复杂程度,但为了得到准确结果,我们必须这样做。编写一个autoNorm函数,该函数会自动的将数字特征值转化为0到1的区间。

现在我们的代码是这样的:

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from KNN import kNN


def file2matrix(filename):
    """
    将文件转换为特征矩阵和对应的标签
    :param filename:
    :return:
    """
    # 打开文件
    fr = open(filename)
    # 按照行进行读取
    arrayOLines = fr.readlines()
    # 计算出总行数,为1000行
    numbserOfLines = len(arrayOLines)
    # 创建一个1000行的0矩阵
    returnMat = np.zeros((numbserOfLines, 3))
    classLabelVector = []
    index = 0

    for line in arrayOLines:
        # 去除前后空格
        line = line.strip()
        # 按照\t将每行内容分割开来
        listFormLine = line.split('\t')
        # 将得到前三项的内容填充到对应的行数矩阵上
        returnMat[index, :] = listFormLine[0:3]
        # 按照喜欢程度进行添加标签
        # classLabelVector.append(int(listFormLine[-1]))
        if listFormLine[-1] == 'largeDoses':
            classLabelVector.append(3)
        if listFormLine[-1] == 'smallDoses':
            classLabelVector.append(2)
        if listFormLine[-1] == 'didntLike':
            classLabelVector.append(1)
        # 行数索引加一
        index += 1

    return returnMat, classLabelVector


def dataShow(datingDataMat, datingLabels):
    """
    将矩阵可视化,画出每年获得的飞行常客里程数和玩视频游戏所消耗时间百分比的图示
    :param datingDataMat:矩阵
    :param datingLabels:矩阵标签
    :return:
    """

    # 将对应的标签转换为颜色
    LabelsColors = []
    for i in datingLabels:
        if i == 1:
            LabelsColors.append("b")  # 黑色不喜欢
        if i == 2:
            LabelsColors.append("y")  # 黄色一般喜欢
        if i == 3:
            LabelsColors.append("r")  # 红色很喜欢

    # 能够显示中文
    matplotlib.rcParams['font.sans-serif'] = ['SimHei']
    matplotlib.rcParams['font.serif'] = ['SimHei']

    # matplotlib的图像都位于Figure对象中,设置大小和分辨率
    fig = plt.figure(figsize=(12, 7), dpi=80)

    # 第一个图,用来显示每年获得的飞行常客里程数和玩视频游戏所消耗时间百分比
    ax1 = fig.add_subplot(1, 2, 1)
    # 设置x标签
    ax1.set_xlabel('每年获得飞行常客里程数')
    # 设置y标签
    ax1.set_ylabel('玩视频游戏所消耗时间百分比')
    # 添加数据,第一列就是飞行里程数,第二列是玩视频游戏的时间
    ax1.scatter(x=datingDataMat[:, 0], y=datingDataMat[:, 1], color=LabelsColors,
                s=(15.0 * np.array(datingLabels), 15.0 * np.array(datingLabels)), marker='o')

    didnt_Like = ax1.scatter([], [], c='b', s=15)
    smallDoses = ax1.scatter([], [], c='y', s=30)
    largeDoses = ax1.scatter([], [], c='r', s=45)
    # 添加标签
    ax1.legend((didnt_Like, smallDoses, largeDoses), ('不喜欢', '魅力一般', '极具魅力'), loc='best')

    # 另一种作图思路,作出玩视频游戏所消耗百分比(x)和每周消耗冰淇淋公斤数(y)的图
    ax2 = fig.add_subplot(1, 2, 2)
    # 设置x标签
    ax2.set_xlabel('玩视频游戏所消耗百分比')
    # 设置y标签
    ax2.set_ylabel('每周消耗冰淇淋公斤数')

    # 遍历标签,划分出三种类别的坐标
    ice_didntLike = []  # 不喜欢类型的冰淇淋公斤数
    game_didntLike = []  # 不喜欢类型的玩游戏时间

    ice_smallDoses = []  # 一般魅力的冰淇淋公斤数
    game_smallDoses = []  # 一般魅力你的玩游戏时间

    ice_largeDoses = []  # 极大魅力的冰淇淋公斤数
    game_largeDoses = []  # 极大魅力的玩游戏时间

    num = 0  # 索引

    for i in datingLabels:
        if i == 1:  # 不喜欢
            ice_didntLike.append(datingDataMat[num, 2])
            game_didntLike.append(datingDataMat[num, 1])
        if i == 2:  # 一般魅力
            ice_smallDoses.append(datingDataMat[num, 2])
            game_smallDoses.append(datingDataMat[num, 1])
        if i == 3:  # 极大魅力
            ice_largeDoses.append(datingDataMat[num, 2])
            game_largeDoses.append(datingDataMat[num, 1])

        num += 1

    # 极大魅力绘制
    largeDoses = ax2.scatter(x=game_largeDoses, y=ice_largeDoses, s=45, c='r')
    # 一般魅力绘制
    smallDoses = ax2.scatter(x=game_smallDoses, y=ice_smallDoses, s=30, c='y')
    # 不喜欢绘制
    didntLike = ax2.scatter(x=game_didntLike, y=ice_didntLike, s=15, c='b')

    # 添加标签
    ax2.legend((didntLike, smallDoses, largeDoses), ('不喜欢', '魅力一般', '极具魅力'), loc='best')

    # 自动调整间距防止标签超过范围
    plt.tight_layout()
    plt.show()

def autoNorm(dataSet):
    """
    数值归一化,将所有的特征值控制在0到1之间
    :param dataSet:
    :return:
    """
    # 按照axis=0的方向计算出最大值(也就是找到每列中的最大值)/ 或最小值
    minVals = dataSet.min(0)
    maxVals = dataSet.max(0)

    # 计算出差值
    ranges = maxVals - minVals

    # 创建一个用0填充的数组
    normDataSet = np.zeros(shape=(np.shape(dataSet)))

    # 得到总行数
    m = dataSet.shape[0]

    # 将最小值扩充到与原矩阵相等的样子,也就是1000*3的样子,然后相减
    normDataSet = dataSet - np.tile(minVals, (m, 1))

    # 再次相除
    normDataSet = normDataSet / np.tile(ranges, (m, 1))

    return normDataSet, ranges, minVals


if __name__ == '__main__':
    # 拿到数据
    datingDataMat, datingLabels = file2matrix('datingTestSet.txt')
    # print(datingDataMat)
    # print(datingLabels)

    # 数据可视化
    # dataShow(datingDataMat, datingLabels)

    normMat, ranges, minVals = autoNorm(datingDataMat)
    print(normMat)

运行结果:

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

我们不仅仅拿到了归一化之后的矩阵数据,还将最大值和最小值的差值以及最小值都得到了,这是为了后面的程序做准备。


测试算法:作为完整程序验证分类器。

上面的工作中我们已经将数据进行了足够的处理,在使用之前我们还应该测试一下分类器的效果,看看分类器的正确率是否能够让我们满意。如果分类器的正确率能够满足要求的话,海伦就可以使用这个软件来处理约会网站提供的约会名单了。

机器学习算法一个很重要的工作就是评估算法的正确率,通常我们只提供已有数据的90%作为训练样本来训练分类器,而使用其余的10%数据去测试分类器,检测分类器的正确率。需要注意的是,10%的测试数据应该是随机选择的,由于海伦提供的数据并没有按照特定目的来排序,所以我们可以随意选择10%数据而不影响其随机性。这里我们就选择前100条数据进行测试。

给我们现有的代码添加datingClassTest函数:

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from KNN import kNN


def file2matrix(filename):
    """
    将文件转换为特征矩阵和对应的标签
    :param filename:
    :return:
    """
    # 打开文件
    fr = open(filename)
    # 按照行进行读取
    arrayOLines = fr.readlines()
    # 计算出总行数,为1000行
    numbserOfLines = len(arrayOLines)
    # 创建一个1000行的0矩阵
    returnMat = np.zeros((numbserOfLines, 3))
    classLabelVector = []
    index = 0

    for line in arrayOLines:
        # 去除前后空格
        line = line.strip()
        # 按照\t将每行内容分割开来
        listFormLine = line.split('\t')
        # 将得到前三项的内容填充到对应的行数矩阵上
        returnMat[index, :] = listFormLine[0:3]
        # 按照喜欢程度进行添加标签
        # classLabelVector.append(int(listFormLine[-1]))
        if listFormLine[-1] == 'largeDoses':
            classLabelVector.append(3)
        if listFormLine[-1] == 'smallDoses':
            classLabelVector.append(2)
        if listFormLine[-1] == 'didntLike':
            classLabelVector.append(1)
        # 行数索引加一
        index += 1

    return returnMat, classLabelVector


def dataShow(datingDataMat, datingLabels):
    """
    将矩阵可视化,画出每年获得的飞行常客里程数和玩视频游戏所消耗时间百分比的图示
    :param datingDataMat:矩阵
    :param datingLabels:矩阵标签
    :return:
    """

    # 将对应的标签转换为颜色
    LabelsColors = []
    for i in datingLabels:
        if i == 1:
            LabelsColors.append("b")  # 黑色不喜欢
        if i == 2:
            LabelsColors.append("y")  # 黄色一般喜欢
        if i == 3:
            LabelsColors.append("r")  # 红色很喜欢

    # 能够显示中文
    matplotlib.rcParams['font.sans-serif'] = ['SimHei']
    matplotlib.rcParams['font.serif'] = ['SimHei']

    # matplotlib的图像都位于Figure对象中,设置大小和分辨率
    fig = plt.figure(figsize=(12, 7), dpi=80)

    # 第一个图,用来显示每年获得的飞行常客里程数和玩视频游戏所消耗时间百分比
    ax1 = fig.add_subplot(1, 2, 1)
    # 设置x标签
    ax1.set_xlabel('每年获得飞行常客里程数')
    # 设置y标签
    ax1.set_ylabel('玩视频游戏所消耗时间百分比')
    # 添加数据,第一列就是飞行里程数,第二列是玩视频游戏的时间
    ax1.scatter(x=datingDataMat[:, 0], y=datingDataMat[:, 1], color=LabelsColors,
                s=(15.0 * np.array(datingLabels), 15.0 * np.array(datingLabels)), marker='o')

    didnt_Like = ax1.scatter([], [], c='b', s=15)
    smallDoses = ax1.scatter([], [], c='y', s=30)
    largeDoses = ax1.scatter([], [], c='r', s=45)
    # 添加标签
    ax1.legend((didnt_Like, smallDoses, largeDoses), ('不喜欢', '魅力一般', '极具魅力'), loc='best')

    # 另一种作图思路,作出玩视频游戏所消耗百分比(x)和每周消耗冰淇淋公斤数(y)的图
    ax2 = fig.add_subplot(1, 2, 2)
    # 设置x标签
    ax2.set_xlabel('玩视频游戏所消耗百分比')
    # 设置y标签
    ax2.set_ylabel('每周消耗冰淇淋公斤数')

    # 遍历标签,划分出三种类别的坐标
    ice_didntLike = []  # 不喜欢类型的冰淇淋公斤数
    game_didntLike = []  # 不喜欢类型的玩游戏时间

    ice_smallDoses = []  # 一般魅力的冰淇淋公斤数
    game_smallDoses = []  # 一般魅力你的玩游戏时间

    ice_largeDoses = []  # 极大魅力的冰淇淋公斤数
    game_largeDoses = []  # 极大魅力的玩游戏时间

    num = 0  # 索引

    for i in datingLabels:
        if i == 1:  # 不喜欢
            ice_didntLike.append(datingDataMat[num, 2])
            game_didntLike.append(datingDataMat[num, 1])
        if i == 2:  # 一般魅力
            ice_smallDoses.append(datingDataMat[num, 2])
            game_smallDoses.append(datingDataMat[num, 1])
        if i == 3:  # 极大魅力
            ice_largeDoses.append(datingDataMat[num, 2])
            game_largeDoses.append(datingDataMat[num, 1])

        num += 1

    # 极大魅力绘制
    largeDoses = ax2.scatter(x=game_largeDoses, y=ice_largeDoses, s=45, c='r')
    # 一般魅力绘制
    smallDoses = ax2.scatter(x=game_smallDoses, y=ice_smallDoses, s=30, c='y')
    # 不喜欢绘制
    didntLike = ax2.scatter(x=game_didntLike, y=ice_didntLike, s=15, c='b')

    # 添加标签
    ax2.legend((didntLike, smallDoses, largeDoses), ('不喜欢', '魅力一般', '极具魅力'), loc='best')

    # 自动调整间距防止标签超过范围
    plt.tight_layout()
    plt.show()

def autoNorm(dataSet):
    """
    数值归一化,将所有的特征值控制在0到1之间
    :param dataSet:
    :return:
    """
    # 按照axis=0的方向计算出最大值(也就是找到每列中的最大值)/ 或最小值
    minVals = dataSet.min(0)
    maxVals = dataSet.max(0)

    # 计算出差值
    ranges = maxVals - minVals

    # 创建一个用0填充的数组
    normDataSet = np.zeros(shape=(np.shape(dataSet)))

    # 得到总行数
    m = dataSet.shape[0]

    # 将最小值扩充到与原矩阵相等的样子,也就是1000*3的样子,然后相减
    normDataSet = dataSet - np.tile(minVals, (m, 1))

    # 再次相除
    normDataSet = normDataSet / np.tile(ranges, (m, 1))

    return normDataSet, ranges, minVals


def datingClassTest():
    """
    测试分类器的准确率
    :return:
    """
    # 一个比率,与总数据相乘应该得到100,就是我们需要测试的100条数据
    hoRatio = 0.10

    # 拿到数据
    datingDataMat, datingLabels = file2matrix('datingTestSet.txt')

    # 数据归一化
    normMat, ranges, minVals = autoNorm(datingDataMat)

    # 计算出总行数
    m = normMat.shape[0]

    # 得到总的100
    numTestVecs = int(m*hoRatio)

    # 错误率初始化为0
    errorCount = 0.0

    # 使用kNN进行分类验证前一百个数据
    for i in range(numTestVecs):
        # 使用原来的分类器
        classifierResult = kNN.classify0(normMat[i, :], normMat[numTestVecs:m, :], datingLabels[numTestVecs:m], 3)

        # 打印出预测结果和真实结果
        print('使用分类器分类得到的结果为:%d ,当前数据的真实结果为:%d 。' % (classifierResult, datingLabels[i]))

        # 如果不对的话错误值加一
        if classifierResult != datingLabels[i]:
            errorCount += 1.0

    # 打印出错误率
    print('分类器的错误率为:' + str(errorCount/float(numTestVecs)))


if __name__ == '__main__':
    # 拿到数据
    datingDataMat, datingLabels = file2matrix('datingTestSet.txt')
    # print(datingDataMat)
    # print(datingLabels)

    # 数据可视化
    # dataShow(datingDataMat, datingLabels)

    # normMat, ranges, minVals = autoNorm(datingDataMat)
    # print(normMat)

    # 检测分类器的准确性
    datingClassTest()

运行结果:

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

可以看到,我们将前100条数据分别使用分类器去进行分类,然后与数据的真实类别进行比较,最后得到错误率为:0.05,也就是5%,那么正确率就是95%,已经能够满足要求了。


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

通过上面的种种步骤,我们的分类器终于接近尾声了。现在我们将整个系统补充完成,通过该程序海伦会在约会网站上找到某个人并输入他的信息,程序会给出她对对方喜欢程度的预测值。

我们最后的代码如下:

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from KNN import kNN


def file2matrix(filename):
    """
    将文件转换为特征矩阵和对应的标签
    :param filename:
    :return:
    """
    # 打开文件
    fr = open(filename)
    # 按照行进行读取
    arrayOLines = fr.readlines()
    # 计算出总行数,为1000行
    numbserOfLines = len(arrayOLines)
    # 创建一个1000行的0矩阵
    returnMat = np.zeros((numbserOfLines, 3))
    classLabelVector = []
    index = 0

    for line in arrayOLines:
        # 去除前后空格
        line = line.strip()
        # 按照\t将每行内容分割开来
        listFormLine = line.split('\t')
        # 将得到前三项的内容填充到对应的行数矩阵上
        returnMat[index, :] = listFormLine[0:3]
        # 按照喜欢程度进行添加标签
        # classLabelVector.append(int(listFormLine[-1]))
        if listFormLine[-1] == 'largeDoses':
            classLabelVector.append(3)
        if listFormLine[-1] == 'smallDoses':
            classLabelVector.append(2)
        if listFormLine[-1] == 'didntLike':
            classLabelVector.append(1)
        # 行数索引加一
        index += 1

    return returnMat, classLabelVector


def dataShow(datingDataMat, datingLabels):
    """
    将矩阵可视化,画出每年获得的飞行常客里程数和玩视频游戏所消耗时间百分比的图示
    :param datingDataMat:
    :param datingLabels:
    :return:
    """

    # 将对应的标签转换为颜色
    LabelsColors = []
    for i in datingLabels:
        if i == 1:
            LabelsColors.append("b")  # 黑色不喜欢
        if i == 2:
            LabelsColors.append("y")  # 黄色一般喜欢
        if i == 3:
            LabelsColors.append("r")  # 红色很喜欢

    # 能够显示中文
    matplotlib.rcParams['font.sans-serif'] = ['SimHei']
    matplotlib.rcParams['font.serif'] = ['SimHei']

    # matplotlib的图像都位于Figure对象中,设置大小和分辨率
    fig = plt.figure(figsize=(12, 7), dpi=80)

    # 第一个图,用来显示每年获得的飞行常客里程数和玩视频游戏所消耗时间百分比
    ax1 = fig.add_subplot(1, 2, 1)
    # 设置x标签
    ax1.set_xlabel('每年获得飞行常客里程数')
    # 设置y标签
    ax1.set_ylabel('玩视频游戏所消耗时间百分比')
    # 添加数据,第一列就是飞行里程数,第二列是玩视频游戏的时间
    ax1.scatter(x=datingDataMat[:, 0], y=datingDataMat[:, 1], color=LabelsColors,
                s=(15.0 * np.array(datingLabels), 15.0 * np.array(datingLabels)), marker='o')

    didnt_Like = ax1.scatter([], [], c='b', s=15)
    smallDoses = ax1.scatter([], [], c='y', s=30)
    largeDoses = ax1.scatter([], [], c='r', s=45)
    # 添加标签
    ax1.legend((didnt_Like, smallDoses, largeDoses), ('不喜欢', '魅力一般', '极具魅力'), loc='best')

    # 另一种作图思路,作出玩视频游戏所消耗百分比(x)和每周消耗冰淇淋公斤数(y)的图
    ax2 = fig.add_subplot(1, 2, 2)
    # 设置x标签
    ax2.set_xlabel('玩视频游戏所消耗百分比')
    # 设置y标签
    ax2.set_ylabel('每周消耗冰淇淋公斤数')

    # 遍历标签,划分出三种类别的坐标
    ice_didntLike = []  # 不喜欢类型的冰淇淋公斤数
    game_didntLike = []  # 不喜欢类型的玩游戏时间

    ice_smallDoses = []  # 一般魅力的冰淇淋公斤数
    game_smallDoses = []  # 一般魅力你的玩游戏时间

    ice_largeDoses = []  # 极大魅力的冰淇淋公斤数
    game_largeDoses = []  # 极大魅力的玩游戏时间

    num = 0  # 索引

    for i in datingLabels:
        if i == 1:  # 不喜欢
            ice_didntLike.append(datingDataMat[num, 2])
            game_didntLike.append(datingDataMat[num, 1])
        if i == 2:  # 一般魅力
            ice_smallDoses.append(datingDataMat[num, 2])
            game_smallDoses.append(datingDataMat[num, 1])
        if i == 3:  # 极大魅力
            ice_largeDoses.append(datingDataMat[num, 2])
            game_largeDoses.append(datingDataMat[num, 1])

        num += 1

    # 极大魅力绘制
    largeDoses = ax2.scatter(x=game_largeDoses, y=ice_largeDoses, s=45, c='r')
    # 一般魅力绘制
    smallDoses = ax2.scatter(x=game_smallDoses, y=ice_smallDoses, s=30, c='y')
    # 不喜欢绘制
    didntLike = ax2.scatter(x=game_didntLike, y=ice_didntLike, s=15, c='b')

    # 添加标签
    ax2.legend((didntLike, smallDoses, largeDoses), ('不喜欢', '魅力一般', '极具魅力'), loc='best')

    # 自动调整间距防止标签超过范围
    plt.tight_layout()
    plt.show()


def autoNorm(dataSet):
    """
    数值归一化,将所有的特征值控制在0到1之间
    :param dataSet:
    :return:
    """
    # 按照axis=0的方向计算出最大值(也就是找到每列中的最大值)/ 或最小值
    minVals = dataSet.min(0)
    maxVals = dataSet.max(0)

    # 计算出差值
    ranges = maxVals - minVals

    # 创建一个用0填充的数组
    normDataSet = np.zeros(shape=(np.shape(dataSet)))

    # 得到总行数
    m = dataSet.shape[0]

    # 将最小值扩充到与原矩阵相等的样子,也就是1000*3的样子,然后相减
    normDataSet = dataSet - np.tile(minVals, (m, 1))

    # 再次相除
    normDataSet = normDataSet/np.tile(ranges, (m, 1))

    return normDataSet, ranges, minVals


def datingClassTest():
    """
    测试分类器的准确率
    :return:
    """
    # 一个比率,与总数据相乘应该得到100,就是我们需要测试的100条数据
    hoRatio = 0.10

    # 拿到数据
    datingDataMat, datingLabels = file2matrix('datingTestSet.txt')

    # 数据归一化
    normMat, ranges, minVals = autoNorm(datingDataMat)

    # 计算出总行数
    m = normMat.shape[0]

    # 得到总的100
    numTestVecs = int(m*hoRatio)

    # 错误率初始化为0
    errorCount = 0.0

    # 使用kNN进行分类验证前一百个数据
    for i in range(numTestVecs):
        # 使用原来的分类器
        classifierResult = kNN.classify0(normMat[i, :], normMat[numTestVecs:m, :], datingLabels[numTestVecs:m], 3)

        # 打印出预测结果和真实结果
        print('使用分类器分类得到的结果为:%d ,当前数据的真实结果为:%d 。' % (classifierResult, datingLabels[i]))

        # 如果不对的话错误值加一
        if classifierResult != datingLabels[i]:
            errorCount += 1.0

    # 打印出错误率
    print('分类器的错误率为:' + str(errorCount/float(numTestVecs)))


def classifyPerson():
    """
    输入内容进行预测
    :return:
    """
    # 结果列表,不喜欢、一般魅力、极具魅力
    resultList = ['not at all', 'in small doses', 'in large doses']

    # 输入玩视频游戏所消耗时间的百分比
    percentTats = float(input('玩视频游戏所消耗时间的百分比:'))

    # 输入每年获得的飞行常客里程数
    ffMiles = float(input('每年获得的飞行常客里程数:'))

    # 输入每周消费的冰淇淋公斤数
    iceCream = float(input('每周消费的冰淇淋公斤数:'))

    # 拿到数据
    datingDataMat, datingLabels = file2matrix('datingTestSet.txt')

    # 数据归一化
    normMat, ranges, minVals = autoNorm(datingDataMat)

    # 将输入的数据转换为数组
    inArr = np.array([ffMiles, percentTats, iceCream])

    # 测试数据归一化
    normInArr = (inArr - minVals) / ranges

    # 进行分类
    classifierResult = kNN.classify0(normInArr, normMat, datingLabels, 3)

    print('你对此人的感觉可能是:' + resultList[classifierResult - 1])


if __name__ == '__main__':
    # 拿到数据
    datingDataMat, datingLabels = file2matrix('datingTestSet.txt')

    # 数据可视化
    # dataShow(datingDataMat, datingLabels)

    # 检测分类器的准确性
    # datingClassTest()

    # 进行分类
    classifyPerson()

来运行测试一下:

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

还记得之前绘图的时候有提到过:从图中看来每周冰淇淋公斤数的影响并不大么。

我们现在就可以测试一下了:

其他条件不变,当每周消耗的冰淇淋很少的时候也是这样的结果:

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

至此,我们就完成了kNN的第一个案例,代码及数据:约会网站配对。