Chapter2 K-近邻算法案例

发布时间 2023-04-08 08:33:54作者: gao79138

案例1:使用K-近邻算法分类爱情片和动作片


1. 案例要求

    创建一个应用,应用K-近邻算法,将样本分到以下三种类别。
        1. 不喜欢的人
        2. 魅力一般的人
        3. 极具魅力的人

2. 案例的执行流程

    示例:在约会网站上使用k-近邻算法
        (1)收集数据:提供文本文件。
        (2)准备数据:使用Python解析文本文件。这一步的作用:必须将待处理数据的格式改变为分类器可以接受的格式。(使数据的格式符合要求)
        (3)分析数据:使用Matplotlib画二维扩散图。
        (4)训练算法:此步骤不适用于k-近邻算法。
        (5)测试算法:使用海伦提供的部分数据作为测试样本。测试样本和非测试样本的区别在于:测试样本是已经完成分类的数据,如果预测分类与实际类别不同,则标记为一个错误。
        (6)使用算法:产生简单的命令行程序,然后海伦可以输入一些特征数据以判断对方是否为自己喜欢的类型。

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

    准备工作:数据存放在datingTestSet2.txt中,每个样本数据占据一行,共1000行。每个样本都有三个特征。
        1. 每年获得的飞行常客里程数
        2. 玩视频游戏所耗时间百分比
        3. 每周消费的冰淇淋公升数
# 该代码的作用是将文本文件中的数据格式改变为分类器可以接受的格式
# 该代码位于kNN.py中

def file2matrix(filename):
    love_dictionary = {'largeDoses':3, 'smallDoses':2, 'didntLike':1}       # 将三种类别与数字对应起来
    fr = open(filename)                         # 打开文件
    arrayOLines = fr.readlines()                # 读取文件的每一行并以列表的形式返回
    numberOfLines = len(arrayOLines)            # 读取该列表长度,即为行数
    returnMat = np.zeros((numberOfLines, 3))    # 数据(训练)集矩阵 numpy.zero代表生成指定格式的数组,元素用0填充
    classLabelVector = []                       # 标签向量
    index = 0                                   # index是代表行的游标
    for line in arrayOLines:                    # 取出文件的每一行
        line = line.strip()                     # strip()代表去除首尾空格或回车
        listFromLine = line.split('\t')         # 将每一行元素分隔开,以\t为标志,返回的是一个列表
        returnMat[index, :] = listFromLine[0:3] # 填充数据集
        # 该if...else语句的作用是填充标签向量
        if(listFromLine[-1].isdigit()):         # isdigit()检测字符串是否只包含数字,是为True,否为False。listFromLine[-1]为标签(字符串形式)
            classLabelVector.append(int(listFromLine[-1]))
        else:
            classLabelVector.append(love_dictionary.get(listFromLine[-1]))
        index += 1
    return returnMat, classLabelVector
# 该代码的作用用于测试
# 该代码位于personalTest.py中

print(kNN.file2matrix('datingTestSet.txt'))
'''
array([[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,...]
'''

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

# 该代码用于绘制散点图
# 该代码位于personalTest.py文件中

from numpy import *
import matplotlib
import matplotlib.pyplot as plt

from Ch02 import kNN

fig = plt.figure()           # 创建一个figure
ax = fig.add_subplot(111)    # 在figure的基础上添加子图。参数为行列第几个图 111 代表1行1列第一个子图(共有一个子图)
                             # 224 代表2行2列第4个子图。(共有四个子图)
datingDataMat,datingLabels = kNN.file2matrix('datingTestSet.txt') # 提取数据矩阵和标签向量
# ax.scatter(datingDataMat[:,1], datingDataMat[:,2])  # scatter代表绘制散点图,其中第一个和第二个参数代表长度相同的数组序列。(分别代表x和y)
ax.scatter(datingDataMat[:,1], datingDataMat[:,2], 15.0*array(datingLabels), array(datingLabels))
# 上述函数的第三个参数代表点的大小,根据不同的类别,大小也不同
# 上述函数的第四个参数代表点的颜色,根据不同的类别,颜色也不同
ax.axis([-2,25,-0.2,2.0])                                     # 配置坐标轴,其中参数分别为xmin xmax ymin ymax
plt.xlabel('Percentage of Time Spent Playing Video Games')    # 设置在x轴上的标签
plt.ylabel('Liters of Ice Cream Consumed Per Week')           # 设置在y轴上的标签
plt.show()                                                    # 显示figure

img

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

img
img

    为什么要归一化数值?
        根据上表,如果我们想要计算样本3和样本4之间的欧氏距离,可以使用上面的公式。
        我们很容易发现:上面公式的大小很大程度上取决于数值差值最大的属性(特征)。也就是说,每年获取的飞行常客里程数对于计算结果的影响将远远大于其他两个特征的影响。而产生这种现象的原因仅仅是因为每年获取的飞行常客里程数远大于其他特征值。但是这三种特征应该是同样重要的。(权重是相等的),飞行常客里程数不应该大程度地决定整个距离的大小。所以我们要归一化数值,使这三种特征的取值范围同步,这样的话每个特征的数值差值就同步了。
        因此,在处理这种不同取值范围的特征值时,我们通常采用的方法就是数值归一化,把取值范围限制在0到1或-1到1之间。这里我们将取值范围限制在0到1之间,采用下面的公式。

img

    其中min和max分别为:每个特征的最小特征值和最大特征值。
# 此函数的功能是要把每一个特征都要归一化,使其范围在0-1之间
# 该函数位于kNN.py文件中

def autoNorm(dataSet):
    minVals = dataSet.min(0)                            # min方法,如果无参,代表这个array数组中所有元素的最小值。如果参数为0,则代表返回array数组中每一列(每一个特征)的最小值,如果参数为1,就是行
    maxVals = dataSet.max(0)                            # max方法同上,只是最大值
    ranges = maxVals - minVals
    normDataSet = np.zeros(np.shape(dataSet))           # 归一化之后的数据集,元素初始化为0
    m = dataSet.shape[0]                                # 取行数
    normDataSet = dataSet - np.tile(minVals, (m, 1))    # 相减,对应公式中的oldValue-min
    normDataSet = normDataSet/np.tile(ranges, (m, 1))   # 相除(这里不是矩阵除法,只是将矩阵对应元素之间进行相除),对应公式中的oldValue-min/max-min
    return normDataSet, ranges, minVals                 # 返回归一化之后的数据集,每个特征的最大值和最小值之间的距离,每个特征的最小值
# 本代码的功能是用于测试
# 代码位于personalTest.py文件中

datingDataMat,datingLabels = kNN.file2matrix('datingTestSet.txt') # 提取数据矩阵和标签向量

print(kNN.autoNorm(datingDataMat))

'''
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]])
'''
'''
array([9.1273000e+04, 2.0919349e+01, 1.6943610e+00])
'''
'''
array([0.      , 0.      , 0.001156])
'''

6. 测试分类器

# 此函数的作用就是用于测试分类器的错误率
# 该函数位于kNN.py文件中

def datingClassTest():
    hoRatio = 0.10      # 代表控制测试数据的比率,测试数据应该为整体数据的10%,剩余的90%应为训练数据。
    datingDataMat, datingLabels = file2matrix('datingTestSet.txt')       # 从文件中加载数据集
    normMat, ranges, minVals = autoNorm(datingDataMat)                    # 归一化数据集
    m = normMat.shape[0]                                                  # 行数
    numTestVecs = int(m*hoRatio)                                          # 测试数据的数量,由比率控制
    errorCount = 0.0                                                      # 每当测试器错误分类数据,则数量+1
    for i in range(numTestVecs):
        #numpy中的冒号有两层意思:默认全部选择、指定范围(左闭右开)
        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          # 如果分类器预测的类别和真实的类别有误,则计数器+1
    print("the total error rate is: %f" % (errorCount / float(numTestVecs))) # 错误率
    print(errorCount)                                                        # 输出错误数量
# 此代码的作用是运行上述的测试程序
# 该代码在哪里都能执行,只要导入了kNN.py文件就行

kNN.datingClassTest()
'''
the classifier came back with: 3, the real answer is: 3
the classifier came back with: 2, the real answer is: 2
the classifier came back with: 1, the real answer is: 1
the classifier came back with: 1, the real answer is: 1
the classifier came back with: 1, the real answer is: 1
the classifier came back with: 1, the real answer is: 1
the classifier came back with: 3, the real answer is: 3
the classifier came back with: 3, the real answer is: 3
...
the total error rate is: 0.050000 
5.0
'''
# 错误率在5%,还是可以的。

7. 构建完整可用系统

# 本代码实现一个小程序,用于对输入的样本进行分类
# 该代码位于kNN.py文件中

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.txt')
    normMat, ranges, minVals = autoNorm(datingDataMat)
    inArr = np.array([ffMiles, percentTats, iceCream, ])
    classifierResult = classify0((inArr - \
                                  minVals)/ranges, normMat, datingLabels, 3)        # 训练集和测试集需要归一化,预测样本也需要归一化。
    print("You will probably like this person: %s" % resultList[classifierResult - 1]) # 输出结果
# 本代码用于测试该程序
# 该代码可以随时运行,只要导入kNN.py文件

kNN.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
2
You will probably like this person: in small doses
'''