机器学习实战:k-近邻算法解决海伦约会预测问题

目录

一、算法简介

二、算法流程

三、算法实战

问题引入

问题分析

问题解决

加载数据集

划分数据集

归一化特征值

实现 kNN分类器

评估模型

测试模型

​编辑

研究k值对准确率的影响

完整代码

 

一、算法简介

k-近邻(kNN,k Nearest Neignbor)算法是数据挖掘分类技术中最简单的方法之一,最初由 Cover和Hart于1968年提出,是一个理论上比较成熟的方法,也是最简单的机器学习算法之一。

该算法的思想非常简单直观:如果一个样本在特征空间中的k个最近邻的样本大多数属于某一个类别,则该样本也属于这个类别。

二、算法流程

  1. 计算已知训练集中的点与当前点之间的距离

  2. 按照距离大小递增排序

  3. 选取与当前点距离最近的k个点

  4. 统计这k个点对应类别出现的频率

  5. 选取频率最高的类别作为预测结果

三、算法实战

我们用一个经典的海伦约会预测问题来详细了解实施kNN算法的基本过程。

问题引入

海伦一直使用在线约会网站寻找适合自己的约会对象。她曾交往过三种类型的人:

  • 不喜欢的人
  • 一般喜欢的人
  • 非常喜欢的人

这些人包含以下三种特征

  1. 每年获得的飞行常客里程数

  2. 玩视频游戏所耗时间百分比

  3. 每周消费的冰淇淋公升数

该网站现在需要尽可能向海伦推荐她喜欢的人,需要我们设计一个分类器,根据用户的以上三种特征,识别出是否该向海伦推荐。

问题分析

根据问题,我们可知,样本特征个数为3,分别为飞行里程数、玩游戏耗时百分比、消费冰淇淋公升数,样本标签为三类,分别为不喜欢、一般喜欢、非常喜欢。现需要使用k近邻算法实现将一个待分类样本的三个特征值输入程序后,能够识别预测该样本的类别,并且将该类别输出。

问题解决

我们先来看一下数据集,第一二三列依次表示飞行里程数、玩游戏耗时百分比、消费冰淇淋公升数这三个特征值,第四列是这一组特征值对应的标签,didntLike、smallDoses、largeDoses依次表示不喜欢、一般喜欢、非常喜欢

加载数据集

首先,我们要先把数据集读入内存,以遍后续操作。我们定义一个函数load_data(filepath),其中filepath为数据集的文件路径,在函数中,我们遍历该文件的每一行,依次将特征向量加入data,标签加入labels,最后返回结果。

def load_data(filepath):
    data=[]
    labels=[]
    with open(filepath,'r') as f:
        for line in f:
            items=line.strip().split('\t')
            data.append(list(map(float,items[:3])))
            labels.append(items[-1])
    return np.array(data),np.array(labels)

划分数据集

然后,我们需要把数据集按照一定比例随机划分为训练集和测试集。我们定义一个函数split_dataset(data,labels,test_radio),其中data为特征向量集,labels为标签集,test_radio为测试集比例。先获取数据集的长度为num,然后使用np.random.permutation函数获得0~num-1的随机序列indexes,而后计算测试集长度test_size,我们把indexes的前text_size项作为测试集对应的索引,其余部分作为训练集对应的索引,最后依次返回训练集的特征向量集、训练集的标签集、测试集的特征向量集、测试集的标签集

def split_dataset(data,labels,test_radio):
    num=data.shape[0]
    indexes=np.random.permutation(num)
    test_size=int(num*test_radio)
    test_indexes=indexes[:test_size]
    train_indexes=indexes[test_size:]
    return data[train_indexes],labels[train_indexes],data[test_indexes],labels[test_indexes]

归一化特征值

我们应当认为每个特征对结果的影响力是相同的。但由于每个特征的数据范围各不相同,会导致各特征的影响力各不相同、差值较大的数据影响力较大,例如该问题中的每年飞行里程数,因此我们要对特征值进行归一化。什么是归一化?归一化就是通过一定的方法把每个特征值映射到区间[0,1]。归一化的方法有很多,在本文中,我们使用最小最大值归一化法。

我们设特征向量min(x)的每一项表示对应特征列的最小值,max(x)的每一项表示对应特征列的最大值x_{old}x_{new}分别表示归一化前后的特征向量,那么有x_{new}=\frac{x_{old}-min(x)}{max(x)-min(x)}

根据以上即可编写出归一化函数normalize(train_data,test_data),返回归一化后的数据集。

def normalize(train_data,test_data):
    min_vals=train_data.min(axis=0)
    max_vals=train_data.max(axis=0)
    ranges=max_vals-min_vals
    ranges[ranges==0]=1
    train_norm=(train_data-min_vals)/ranges
    test_norm=(test_data-min_vals)/ranges
    return train_norm,test_norm

实现 kNN分类器

万事俱备后,我们可以开始实现kNN算法了。按照算法过程,我们只需计算训练集中所有点到当前点的距离,取距离最小的k个,即k近邻,然后统计这k个点中哪个类别的频率最高,并作为预测结果返回。首先我们要确定使用距离度量方法,例如欧氏距离、曼哈顿距离、闵可夫斯基距离。在本文中,我们采用欧氏距离,设特征向量x=(x_1,x_2,...,x_n),则欧氏距离distance(x)=\sqrt{x_1^2+x_2^2+...+x_n^2}。根据以上即可编写出classify函数,返回预测结果。

def classify(train_data,train_labels,test_sample,k):
    distances=np.sqrt(np.sum((train_data-test_sample)**2,axis=1))
    distance_labels=list(zip(distances,train_labels))
    distance_labels.sort(key=lambda x:x[0])
    k_nearest=distance_labels[:k]
    label_counts={}
    for distance,label in k_nearest:
        label_counts[label]=label_counts.get(label,0)+1
    sorted_labels=sorted(label_counts.items(),key=lambda x:x[1],reverse=True)
    return sorted_labels[0][0]

评估模型

完成以上操作后,我们便可以开始使用测试集评估模型预测的准确率。具体的,对测试集中的每个样本,使用classify函数得到预测结果,与实际的所属类别进行对比,统计预测正确的频率。

def evaluate(train_data,train_labels,test_data,test_labels,k):
    right_count=0
    for i in range(len(test_data)):
        if(classify(train_data,train_labels,test_data[i],k)==test_labels[i]):
            right_count+=1
    accuracy=100*right_count/len(test_data)
    return accuracy

测试模型

最后,我们进行模型的测试。不妨取k=3,测试集比例test_radio=0.1。

if __name__=="__main__":
    k=3
    test_radio=0.1
    data,labels=load_data("datingTestSet.txt")
    train_data,train_labels,test_data,test_labels=split_dataset(data,labels,test_radio)
    train_norm,test_norm=normalize(train_data,test_data)
    label_map={"didntLike":"不喜欢","smallDoses":"一般喜欢","largeDoses":"非常喜欢"}
    print(f"准确率:{evaluate(train_norm,train_labels,test_norm,test_labels,k)}%",)
    while True:
        print("请输入特征值(飞行里程 游戏时间% 冰淇淋消耗量),用空格分隔,空输入退出:")
        inputs=input()
        if(inputs==""):
            break
        features=np.array(list(map(float,inputs.split())))
        if(len(features)!=3):
            print("格式错误")
            continue
        print(f"预测结果:{label_map[classify(train_data,train_labels,features,k)]}")

运行效果如图:

研究k值对准确率的影响

我们遍历所有可能的k值,依次得到它们对应的准确率,并使用matplotlib.pyplot绘制出准确率关于k的图像。由于每次划分数据集都是随机的,得到的数据仅供参考。

def try_all_k(train_data,train_labels,test_data,test_labels):
    num=len(train_data)
    accuracies=np.zeros(shape=num)
    for k in range(num):
        accuracies[k]=evaluate(train_data,train_labels,test_data,test_labels,k+1)
    ks=list(range(1,num+1))
    plt.plot(ks,accuracies,marker='o',linestyle='-')
    plt.xlabel("k",fontsize=12)
    plt.ylabel("Accuracy",fontsize=12)
    plt.title("kNN Accuracy")
    plt.show()

 当k值较大时,模型准确率逐步下降;当k值接近训练集总数900时,模型准确率大幅下降。

完整代码

import numpy as np
import matplotlib.pyplot as plt

def load_data(filepath):
    data=[]
    labels=[]
    with open(filepath,'r') as f:
        for line in f:
            items=line.strip().split('\t')
            data.append(list(map(float,items[:3])))
            labels.append(items[-1])
    return np.array(data),np.array(labels)

def split_dataset(data,labels,test_radio):
    num=data.shape[0]
    indexes=np.random.permutation(num)
    test_size=int(num*test_radio)
    test_indexes=indexes[:test_size]
    train_indexes=indexes[test_size:]
    return data[train_indexes],labels[train_indexes],data[test_indexes],labels[test_indexes]

def normalize(train_data,test_data):
    min_vals=train_data.min(axis=0)
    max_vals=train_data.max(axis=0)
    ranges=max_vals-min_vals
    ranges[ranges==0]=1
    train_norm=(train_data-min_vals)/ranges
    test_norm=(test_data-min_vals)/ranges
    return train_norm,test_norm

def classify(train_data,train_labels,test_sample,k):
    distances=np.sqrt(np.sum((train_data-test_sample)**2,axis=1))
    distance_labels=list(zip(distances,train_labels))
    distance_labels.sort(key=lambda x:x[0])
    k_nearest=distance_labels[:k]
    label_counts={}
    for distance,label in k_nearest:
        label_counts[label]=label_counts.get(label,0)+1
    sorted_labels=sorted(label_counts.items(),key=lambda x:x[1],reverse=True)
    return sorted_labels[0][0]

def evaluate(train_data,train_labels,test_data,test_labels,k):
    right_count=0
    for i in range(len(test_data)):
        if(classify(train_data,train_labels,test_data[i],k)==test_labels[i]):
            right_count+=1
    accuracy=100*right_count/len(test_data)
    return accuracy

def try_all_k(train_data,train_labels,test_data,test_labels):
    num=len(train_data)
    accuracies=np.zeros(shape=num)
    for k in range(num):
        accuracies[k]=evaluate(train_data,train_labels,test_data,test_labels,k+1)
    ks=list(range(1,num+1))
    plt.plot(ks,accuracies,marker='o',linestyle='-')
    plt.xlabel("k",fontsize=12)
    plt.ylabel("Accuracy",fontsize=12)
    plt.title("kNN Accuracy")
    plt.show()
    
if __name__=="__main__":
    k=3
    test_radio=0.1
    data,labels=load_data("datingTestSet.txt")
    train_data,train_labels,test_data,test_labels=split_dataset(data,labels,test_radio)
    train_norm,test_norm=normalize(train_data,test_data)
    # try_all_k(train_norm,train_labels,test_norm,test_labels)
    label_map={"didntLike":"不喜欢","smallDoses":"一般喜欢","largeDoses":"非常喜欢"}
    print(f"准确率:{evaluate(train_norm,train_labels,test_norm,test_labels,k)}%",)
    while True:
        print("请输入特征值(飞行里程 游戏时间% 冰淇淋消耗量),用空格分隔,空输入退出:")
        inputs=input()
        if(inputs==""):
            break
        features=np.array(list(map(float,inputs.split())))
        if(len(features)!=3):
            print("格式错误")
            continue
        print(f"预测结果:{label_map[classify(train_data,train_labels,features,k)]}")

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值