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


笔记使用jupyter notebook完成,之后导出为markdown格式调整。环境:Anaconda3

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

数据样本特征

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

流程

  • 收集数据:提供文本数据
  • 准备数据:使用Python解析文本文件
  • 分析数据:使用matplotlib画二维扩散图
  • 训练算法:k-近邻算法中测试数据每一次都要与全量的训练数据进行比较,此步骤不适用该算法
  • 测试数据:使用提供的部分数据作为测试样本。测试样本是已经完成分类的数据。如果预测分类和实际分类不同,则标记为一个错误
  • 使用算法:参数简单的命令行程序,输入一个特征数据以判断是否是喜欢的类型

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

数据文件:datingTestSet2.txt

每个样本数据占据一行,样本数据包含以下三种特征:

  1. 每年获得的飞行常客里程数
  2. 玩视频游戏所消耗时间百分比
  3. 每周消费的冰淇淋公升数

定义文件打开:

def open_file(filename):
    """
    打开文件
    :param filename: 文件名称
    :return: 文件内容list
    """
    with open(filename) as fr:
        return fr.readlines()

将文本记录转换为Numpy:

def file2matrix(filename):
    """
    将文本记录转换为 NumPy
    :param filename: 文件名称
    :return: numpy矩阵
    """
    fr = open_file(filename)
    # 获取文件中行数
    numberOfLines = len(fr)
    # 创建返回的numpy矩阵
    returnMat = zeros((numberOfLines, 3))
    # 解析文件数据保存到列表中
    classLabelVector = []
    index = 0
    for line in fr:
        # str.strip([chars]) --返回已移除字符串头尾指定字符(默认为空格或换行符)所生成的新字符串
        line = line.strip()
        # 以 '\t' 切割字符串
        listFromLine = line.split('\t')
        # 每列的属性数据
        returnMat[index, :] = listFromLine[0:3]
        # 每列的类别数据,就是 label 标签数据
        classLabelVector.append(int(listFromLine[-1]))
        index += 1
    # 返回数据矩阵returnMat和对应的类别classLabelVector
    return returnMat, classLabelVector
from numpy import *
filename = 'data/datingTestSet2.txt'
datingDataMat,datingLabels = file2matrix(filename)
print(datingDataMat,datingLabels)
[[4.0920000e+04 8.3269760e+00 9.5395200e-01]
 [1.4488000e+04 7.1534690e+00 1.6739040e+00]
 [2.6052000e+04 1.4418710e+00 8.0512400e-01]
 ...
 [2.6575000e+04 1.0650102e+01 8.6662700e-01]
 [4.8111000e+04 9.1345280e+00 7.2804500e-01]
 [4.3757000e+04 7.8826010e+00 1.3324460e+00]] [3, 2, 1, 1, 1, 1, 3, 3, 1, 3, 1, 1, 2, 1, 1, 1, 1, 1, 2, 3, 2, 1, 2, 3, 2, 3, 2, 3, 2, 1, 3, 1, 3, 1, 2, 1, 1, 2, 3, 3, 1, 2, 3, 3, 3, 1, 1, 1, 1, 2, 2, 1, 3, 2, 2, 2, 2, 3, 1, 2, 1, 2, 2, 2, 2, 2, 3, 2, 3, 1, 2, 3, 2, 2, 1, 3, 1, 1, 3, 3, 1, 2, 3, 1, 3, 1, 2, 2, 1, 1, 3, 3, 1, 2, 1, 3, 3, 2, 1, 1, 3, 1, 2, 3, 3, 2, 3, 3, 1, 2, 3, 2, 1, 3, 1, 2, 1, 1, 2, 3, 2, 3, 2, 3, 2, 1, 3, 3, 3, 1, 3, 2, 2, 3, 1, 3, 3, 3, 1, 3, 1, 1, 3, 3, 2, 3, 3, 1, 2, 3, 2, 2, 3, 3, 3, 1, 2, 2, 1, 1, 3, 2, 3, 3, 1, 2, 1, 3, 1, 2, 3, 2, 3, 1, 1, 1, 3, 2, 3, 1, 3, 2, 1, 3, 2, 2, 3, 2, 3, 2, 1, 1, 3, 1, 3, 2, 2, 2, 3, 2, 2, 1, 2, 2, 3, 1, 3, 3, 2, 1, 1, 1, 2, 1, 3, 3, 3, 3, 2, 1, 1, 1, 2, 3, 2, 1, 3, 1, 3, 2, 2, 3, 1, 3, 1, 1, 2, 1, 2, 2, 1, 3, 1, 3, 2, 3, 1, 2, 3, 1, 1, 1, 1, 2, 3, 2, 2, 3, 1, 2, 1, 1, 1, 3, 3, 2, 1, 1, 1, 2, 2, 3, 1, 1, 1, 2, 1, 1, 2, 1, 1, 1, 2, 2, 3, 2, 3, 3, 3, 3, 1, 2, 3, 1, 1, 1, 3, 1, 3, 2, 2, 1, 3, 1, 3, 2, 2, 1, 2, 2, 3, 1, 3, 2, 1, 1, 3, 3, 2, 3, 3, 2, 3, 1, 3, 1, 3, 3, 1, 3, 2, 1, 3, 1, 3, 2, 1, 2, 2, 1, 3, 1, 1, 3, 3, 2, 2, 3, 1, 2, 3, 3, 2, 2, 1, 1, 1, 1, 3, 2, 1, 1, 3, 2, 1, 1, 3, 3, 3, 2, 3, 2, 1, 1, 1, 1, 1, 3, 2, 2, 1, 2, 1, 3, 2, 1, 3, 2, 1, 3, 1, 1, 3, 3, 3, 3, 2, 1, 1, 2, 1, 3, 3, 2, 1, 2, 3, 2, 1, 2, 2, 2, 1, 1, 3, 1, 1, 2, 3, 1, 1, 2, 3, 1, 3, 1, 1, 2, 2, 1, 2, 2, 2, 3, 1, 1, 1, 3, 1, 3, 1, 3, 3, 1, 1, 1, 3, 2, 3, 3, 2, 2, 1, 1, 1, 2, 1, 2, 2, 3, 3, 3, 1, 1, 3, 3, 2, 3, 3, 2, 3, 3, 3, 2, 3, 3, 1, 2, 3, 2, 1, 1, 1, 1, 3, 3, 3, 3, 2, 1, 1, 1, 1, 3, 1, 1, 2, 1, 1, 2, 3, 2, 1, 2, 2, 2, 3, 2, 1, 3, 2, 3, 2, 3, 2, 1, 1, 2, 3, 1, 3, 3, 3, 1, 2, 1, 2, 2, 1, 2, 2, 2, 2, 2, 3, 2, 1, 3, 3, 2, 2, 2, 3, 1, 2, 1, 1, 3, 2, 3, 2, 3, 2, 3, 3, 2, 2, 1, 3, 1, 2, 1, 3, 1, 1, 1, 3, 1, 1, 3, 3, 2, 2, 1, 3, 1, 1, 3, 2, 3, 1, 1, 3, 1, 3, 3, 1, 2, 3, 1, 3, 1, 1, 2, 1, 3, 1, 1, 1, 1, 2, 1, 3, 1, 2, 1, 3, 1, 3, 1, 1, 2, 2, 2, 3, 2, 2, 1, 2, 3, 3, 2, 3, 3, 3, 2, 3, 3, 1, 3, 2, 3, 2, 1, 2, 1, 1, 1, 2, 3, 2, 2, 1, 2, 2, 1, 3, 1, 3, 3, 3, 2, 2, 3, 3, 1, 2, 2, 2, 3, 1, 2, 1, 3, 1, 2, 3, 1, 1, 1, 2, 2, 3, 1, 3, 1, 1, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 2, 2, 2, 3, 1, 3, 1, 2, 3, 2, 2, 3, 1, 2, 3, 2, 3, 1, 2, 2, 3, 1, 1, 1, 2, 2, 1, 1, 2, 1, 2, 1, 2, 3, 2, 1, 3, 3, 3, 1, 1, 3, 1, 2, 3, 3, 2, 2, 2, 1, 2, 3, 2, 2, 3, 2, 2, 2, 3, 3, 2, 1, 3, 2, 1, 3, 3, 1, 2, 3, 2, 1, 3, 3, 3, 1, 2, 2, 2, 3, 2, 3, 3, 1, 2, 1, 1, 2, 1, 3, 1, 2, 2, 1, 3, 2, 1, 3, 3, 2, 2, 2, 1, 2, 2, 1, 3, 1, 3, 1, 3, 3, 1, 1, 2, 3, 2, 2, 3, 1, 1, 1, 1, 3, 2, 2, 1, 3, 1, 2, 3, 1, 3, 1, 3, 1, 1, 3, 2, 3, 1, 1, 3, 3, 3, 3, 1, 3, 2, 2, 1, 1, 3, 3, 2, 2, 2, 1, 2, 1, 2, 1, 3, 2, 1, 2, 2, 3, 1, 2, 2, 2, 3, 2, 1, 2, 1, 2, 3, 3, 2, 3, 1, 1, 3, 3, 1, 2, 2, 2, 2, 2, 2, 1, 3, 3, 3, 3, 3, 1, 1, 3, 2, 1, 2, 1, 2, 2, 3, 2, 2, 2, 3, 1, 2, 1, 2, 2, 1, 1, 2, 3, 3, 1, 1, 1, 1, 3, 3, 3, 3, 3, 3, 1, 3, 3, 2, 3, 2, 3, 3, 2, 2, 1, 1, 1, 3, 3, 1, 1, 1, 3, 3, 2, 1, 2, 1, 1, 2, 2, 1, 1, 1, 3, 1, 1, 2, 3, 2, 2, 1, 3, 1, 2, 3, 1, 2, 2, 2, 2, 3, 2, 3, 3, 1, 2, 1, 2, 3, 1, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 1, 3, 3, 3]

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

import matplotlib
import matplotlib.pyplot as plt
# 显示中文
plt.rcParams['font.sans-serif'] = 'SimHei'
# 设置正常显示符号,解决保存图像是符号’-‘显示方块
plt.rcParams['axes.unicode_minus'] = False

fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_title('散点分析图:没有样本类别标签的约会数据散点图,难以辨别图中点究竟属于哪个样本分类')
ax.set_xlabel('玩视频游戏所耗时间百分比')
ax.set_ylabel('每周消费的冰淇淋公升数')
ax.scatter(datingDataMat[:, 1], datingDataMat[:, 2])
plt.show()

没有样本类别标签的约会数据散点图,难以辨别图中点究竟属于哪个样本分类

采用色彩或其他的记号来标记不同的样本分类,以便更好的理解数据信息

# 显示中文
plt.rcParams['font.sans-serif'] = 'SimHei'
# 设置正常显示符号,解决保存图像是符号’-‘显示方块
plt.rcParams['axes.unicode_minus'] = False

fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_title('散点分析图:利用颜色和尺寸标识数据点的属性类别,基本可看出数据点所属三个样本分类的区域轮廓')
ax.set_xlabel('玩视频游戏所耗时间百分比')
ax.set_ylabel('每周消费的冰淇淋公升数')
ax.scatter(datingDataMat[:, 1], datingDataMat[:, 2], 15.0*array(datingLabels), 15.0*array(datingLabels))
plt.show()

利用颜色和尺寸标识数据点的属性类别,基本可看出数据点所属三个样本分类的区域轮廓

使用矩阵属性列0和1展示数据,可清晰的标识三种不同样本分类区域

import numpy as np
# 显示中文
plt.rcParams['font.sans-serif'] = 'SimHei'
# 设置正常显示符号,解决保存图像是符号’-‘显示方块
plt.rcParams['axes.unicode_minus'] = False

fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_title('飞行常客里程数和玩视频游戏所占百分比的散点分析图,区分数据点从属的类别')
ax.set_xlabel('每年获得的飞行常客里程数')
ax.set_ylabel('玩视频游戏所耗时间百分比')
datingLabels = np.array(datingLabels)
# datingLabels为1的索引
index_1 = np.where(datingLabels==1)
type_1 = ax.scatter(datingDataMat[index_1,0],datingDataMat[index_1,1],color='r')
index_2 = np.where(datingLabels==2)
type_2 = ax.scatter(datingDataMat[index_2,0],datingDataMat[index_2,1],color='g')
index_3 = np.where(datingLabels==3)
type_3 = ax.scatter(datingDataMat[index_3,0],datingDataMat[index_3,1],color='b')
ax.legend([type_1,type_2,type_3], ['不喜欢', '魅力一般', '极具魅力'],loc=1)
plt.show()

飞行常客里程数和玩视频游戏所占百分比的散点分析图,区分数据点从属的类别

3.准备数据:归一化数值

以下四组数据中:

序号玩视频游戏所耗时间百分比每年获得的飞行常客里程数每周消费的冰淇淋公升数样本分类
10.84000.51
212134 0000.93
3020 0001.12
46732 0000.12

样本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之间。
归一化特征值,消除特征之间量级不同导致的影响.
归一化可以采用的方法有很多,这里使用线性函数转化:
newValue=(oldValue-MinValue)/(MaxValue-MinValue)  
其中:oldValue、newValue分别为转换前、后的值,MaxValue、MinValue分别为数据集中的最大特征值和最小特征值。

归一化特征值:

def autoNorm(dataSet):
    """
     归一化特征值,消除特征之间量级不同导致的影响
    :param dataSet: 数据集
    :return:归一化后的数据集 normDataSet. ranges和minVals即范围和最小值
    """
    # 计算每列的最小值和最大值,dataSet.min(0)参数0使得函数从列中选取最小值而不是选取当前行的最小值
    minVals = dataSet.min(0)
    maxVals = dataSet.max(0)
    # 计算可能的取值范围 1*3
    ranges = maxVals - minVals
    # 1000*3
    normDataSet = zeros(shape(dataSet))
    # shape[0]读取矩阵的第一维长度
    m = dataSet.shape[0]
    # 生成与最小值之差组成的矩阵。tile()将变量内容复制成输入矩阵同大小的矩阵
    normDataSet = dataSet - tile(minVals, (m, 1))
    # 将最小值之差除以范围组成矩阵
    normDataSet = normDataSet / tile(ranges, (m, 1))  # 特征值相除
    return normDataSet, ranges, minVals
normMat, ranges, minVals = autoNorm(datingDataMat)
normMat
array([[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]])

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

评估算法的正确率,选取已有数据的90%作为训练分类器,随机选取10%数据测试分类器,用来检测分类器的正确率。
该示例中,提供的数据并没有按照特定目的来排序,所以可以随意选择10%数据而不影响随机性。

分类器对约会网站的测试代码:如果预测分类与实际类别不同,则出错次数加1。

def classify0(inX, dataSet, labels, k):
    """
    k-近邻算法
    :param inX: 用于分类的输入向量
    :param dataSet: 输入的训练样本集
    :param labels: 标签向量
    :param k: 表示用于选择最近邻居的数目
    :return: 前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个最短距离, 选取这K个中最多的分类类别
    classCount={}
    for i in range(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]

def datingClassTest():
    """
    对约会网站的测试
    :return: 错误率和错误数
    """
    # 设置测试数据的的一个比例(训练数据集比例=1-hoRatio)
    hoRatio = 0.1  # 测试范围,一部分测试一部分作为样本
    # 从文件中加载数据
    datingDataMat, datingLabels = file2matrix('data/datingTestSet2.txt')
    # 归一化数据
    normMat, ranges, minVals = autoNorm(datingDataMat)
    # m 表示数据的行数,即矩阵的第一维
    m = normMat.shape[0]
    # 设置测试的样本数量, numTestVecs:m表示训练样本的数量
    numTestVecs = int(m * hoRatio)
    print('测试的样本数量numTestVecs=', numTestVecs)
    errorCount = 0.0
    for i in range(numTestVecs):
        # 对数据测试
        classifierResult = classify0(normMat[i, :], normMat[numTestVecs:m, :], datingLabels[numTestVecs:m], 3)
        if (classifierResult != datingLabels[i]): 
            errorCount += 1.0
            print(f"预测分类为: {classifierResult},真正的分类为: {datingLabels[i]}")
    print(f"错误率为: {errorCount / float(numTestVecs)}")
    print(errorCount)
    return errorCount,errorCount / float(numTestVecs)
import operator
datingClassTest()
测试的样本数量numTestVecs= 100
预测分类为: 3,真正的分类为: 2
预测分类为: 3,真正的分类为: 1
预测分类为: 3,真正的分类为: 1
预测分类为: 2,真正的分类为: 3
预测分类为: 3,真正的分类为: 1
错误率为: 0.05
5.0

(5.0, 0.05)

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

产生简单的程序,可以通过输入一些特征数据给出对对方喜欢程度的预测值

def classifyPerson():
    """
    约会网站预测函数
    :return: 预测结果
    """
    resultList = ['not at all不喜欢的人', 'in small doses魅力一般的人', 'in large doses极具魅力的人']
    ffMiles = float(input("每年获得的飞行常客里程数?"))
    percentTats = float(input("玩视频游戏所耗费时间百分比?"))
    iceCream = float(input("每周消费的冰淇淋公升数?"))
    datingDataMat, datingLabels = file2matrix('data/datingTestSet2.txt')
    normMat, ranges, minVals = autoNorm(datingDataMat)
    inArr = np.array([ffMiles, percentTats, iceCream, ])
    classifierResult = classify0((inArr - minVals) / ranges, normMat, datingLabels, 3)
    print(f"在你看来,对方很可能是: {resultList[classifierResult - 1]}")

实际运行效果如下:

classifyPerson()
每年获得的飞行常客里程数? 10000
玩视频游戏所耗费时间百分比? 10
每周消费的冰淇淋公升数? 0.5


在你看来,对方很可能是: in small doses魅力一般的人
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值