《机器学习实战》——k近邻算法

一、前言

如何区分一部片是爱情片还是动作片,爱情片中也有动作,动作片中也有亲吻的镜头。但是,动作片中打斗的场景更多,爱情片中亲吻的镜头更多。所以,可以引入“统计概率”的概念,基于某类场景出现的次数来分类。

二、k近邻算法概述

(1)原理:

给定一堆数据集,对于新输入的测试样例,通过计算其与数据集不同样本特征值之间的距离,从中找出与测试样例最相近的k个邻居。通过判断这k个邻居中哪种类别占多数,则预测该测试样例也属于该类。

(2)优点:

精度高,对异常值不敏感(只与相近的k个值相关),无数据输入假定

(3)缺点:

计算复杂度高,空间复杂度高,学习效率低(算不上学习,只是统计)

(4)特别注意:

其中,最重要的参数是K的选择。

如下图所示,输入绿色圆圈为测试样例。当K=3时,根据原理,最近的3个邻居中,红色三角占多数,则预测绿色圆圈属于红色三角类;当K=5时,同理,5个邻居中,蓝色方块占多数,则预测绿色圆圈属于蓝色方块类。

因此,K的选择将直接影响了预测的结果。如果K值过小,则对于邻近的训练数据特别敏感,容易学习到噪点数据,而造成过拟合;但是如果K值过大,把所有数据都当做邻居来预测,那就减弱了学习能力,只是简单地统计数值而已。

因此,根据李航的《统计学习方法》书中所说,K的选择,应该先以一个小值开始,利用交叉验证的方法,通过不断调参来选取最优值。通常k是不大于20的整数。

在这里插入图片描述
其次,对于距离公式的选择,一般是选择欧式距离,若p(p1,p2,…,pn)和q(q1,q2,…,qn)是空间的两点,则他们的距离为:
在这里插入图片描述

三、实践出真知

(1)核心算法

创建一个kNN.py文件,根据以上原理,写出核心算法代码。

def kNN_classify(test_datas, train_data_set, labels, k):
    """ k近邻分类算法 """
    data_set_size = train_data_set.shape[0] # 数据集个数,行数
    # 1、计算新数据与训练集的欧氏距离
    # tile是将测试集复制成与训练集同行数,一列
    diff_mat = tile(test_datas, (data_set_size, 1)) - train_data_set #(x1-x2)
    sq_diff_mat = diff_mat ** 2 #(x1-x2)平方
    sq_distances = sq_diff_mat.sum(axis=1) # 按行累加
    distances = sq_distances ** 0.5 # 开根号算出欧式距离
    # 2、按照距离递增排序
    sorted_dist_indicies = distances.argsort()
    # 3、选取距离最近的前 k个,计算类别个数
    class_count = {} # key是类别,value是个数
    for i in range(k):
        vote_label = labels[sorted_dist_indicies[i]]
        # 这里字典的get函数是将找key为vote_label的值,若找不到,则用0
        class_count[vote_label] = class_count.get(vote_label, 0) + 1 # 计算类别个数
    # 4、排序找出类别个数最高的作为预测结果
    sorted_class_count = sorted(class_count.items(), key=operator.itemgetter(1), 
                        reverse=True) # 按照value(类的个数)逆序排序,找出类别最多的
    return sorted_class_count[0][0] # [0][0]是预测的标签,[0][1]为k中最多的类别数

完整的验证kNN算法的程序文件为:

from numpy import *
import operator

def create_dataset():
    """ 生成数据集 """
    train_datas = array([[1.0, 1.1], [1.0, 1.0], [0, 0], [0, 0.1]])
    labels = ['动作片', '动作片', '爱情片', '爱情片']
    return train_datas, labels

def kNN_classify(test_datas, train_data_set, labels, k):
    """ k近邻分类算法 """
    data_set_size = train_data_set.shape[0] # 数据集个数,行数
    # 1、计算新数据与训练集的欧氏距离
    # tile是将测试集复制成与训练集同行数,一列
    diff_mat = tile(test_datas, (data_set_size, 1)) - train_data_set #(x1-x2)
    sq_diff_mat = diff_mat ** 2 #(x1-x2)平方
    sq_distances = sq_diff_mat.sum(axis=1) # 按行累加
    distances = sq_distances ** 0.5 # 开根号算出欧式距离
    # 2、按照距离递增排序
    sorted_dist_indicies = distances.argsort()
    # 3、选取距离最近的前 k个,计算类别个数
    class_count = {} # key是类别,value是个数
    for i in range(k):
        vote_label = labels[sorted_dist_indicies[i]]
        # 这里字典的get函数是将找key为vote_label的值,若找不到,则用0
        class_count[vote_label] = class_count.get(vote_label, 0) + 1 # 计算类别个数
    # 4、排序找出类别个数最高的作为预测结果
    sorted_class_count = sorted(class_count.items(), key=operator.itemgetter(1), 
                        reverse=True) # 按照value(类的个数)逆序排序,找出类别最多的
    return sorted_class_count[0][0] # [0][0]是预测的标签,[0][1]为k中最多的类别数

if __name__ == '__main__':
    train_datas, labels = create_dataset()
    test_1 = [1.0, 0.9]
    pred_class = kNN_classify(test_1, train_datas, labels, 3)
    print("test_1预测为:", pred_class)
    test_2 = [0.2, 0.2]
    pred_class = kNN_classify(test_2, train_datas, labels, 3)
    print("test_2预测为:", pred_class)

结果为:

test_1预测为: 动作片
test_2预测为: 爱情片
(2)约会人员喜好预测

注释已经很详细,所以简单粗暴上代码:

from numpy import *
from kNN import kNN_classify
import matplotlib.pyplot as plt
import sys

def file_to_dataset(filename):
    """ 将文件的数据集转换成分类器的输入格式 """
    try:
        with open(filename) as f_obj:
            # 一次性读整个文件,自动将内容分析成一个行的字符串列表
            list_of_lines = f_obj.readlines()
    except FileNotFoundError:
        print("Sorry, the file " + filename + " does not exist.")
        sys.exit()
    numbers_of_lines = len(list_of_lines) # 获取文件行数
    dataset = zeros((numbers_of_lines, 3))
    labels = []
    index = 0
    for line in list_of_lines:
        line = line.strip() # 去掉头尾的指定字符,默认是空格或换行符
        list_from_line = line.split('\t') # 以'\t'分割行,形成列表 
        # 取前三列放进矩阵中, matrix[index, :]表示index行所有列
        dataset[index, :] = list_from_line[0:3]
        labels.append(int(list_from_line[-1])) # 取最后一列作为标签放进标签列表
        index += 1
    return dataset, labels

def show_dataset(dataset, labels):
    """ 可视化数据 """
    fig = plt.figure() # 创建一个画板对象
    ax = fig.add_subplot(111) # 创建子画板,位于第一行第一列第一个位置
    colors = []
    for i in labels:
        if i == 1:
            colors.append('red')
        elif i== 2:
            colors.append('green')
        else :
            colors.append('blue')
    ax.scatter(x=dataset[:, 0], y=dataset[:, 1], 
            c=colors, s=15) # 取第2、3列作为x、y画散列图
    plt.show()

def normalization(dataset):
    """ 
    将数据集归一化到(0, 1)之间 
    new_value = (old_value-min)/(max-min)
    """
    min_values = dataset.min(0) # 参数0确保取该矩阵每一列的最小值,1是每一行的最小值
    max_values = dataset.max(0)
    ranges = max_values - min_values
    norm_dateset = zeros(shape(dataset))
    num_of_lines = dataset.shape[0]
    # tile是复制功能
    norm_dateset = dataset - tile(min_values, (num_of_lines, 1)) 
    norm_dateset = norm_dateset / tile(ranges, (num_of_lines, 1))
    return norm_dateset, ranges, min_values

def dating_test():
    """ 测试分类器效率 """
    ratio = 0.10 # 测试集占 10%
    train_datas, labels = file_to_dataset('ch02_kNN\datingTestSet2.txt')
    norm_dataset, ranges, min_values = normalization(train_datas)
    num_of_lines = norm_dataset.shape[0] # 总行数
    num_test_data = int(num_of_lines * ratio) # 测试行数
    error_count = 0.0
    for i in range(num_test_data):
        res = kNN_classify(norm_dataset[i, :], 
            norm_dataset[num_test_data: num_of_lines, :],
            labels[num_test_data: num_of_lines], 4)
        print("the classifier prediction is: " + str(res) + 
            ", the real answer is:" + str(labels[i]))
        if res != labels[i] :
            error_count += 1.0
    print("Error rate : " + str((error_count/float(num_test_data)*100)) + "%")
    print("Accuracy rate : "+str((1-(error_count/float(num_test_data)))*100)+"%")

def classify_person():
    """ 输出完整结果 """
    res_lists = ['not at all', 'in small doses', 'in large doses']
    percent_tats = float(input("percentage of time spend playing vodeo games?")) 
    ff_miles = float(input("frequent flier miles earned per year?"))
    ice_cream = float(input("liters of ice cream consumed per year?"))
    train_datas, labels = file_to_dataset('ch02_kNN\datingTestSet2.txt')
    norm_dataset, ranges, min_values = normalization(train_datas)
    new_test = array([ff_miles, percent_tats, ice_cream])
    new_test = (new_test-min_values)/ranges # 对新数据归一化
    res = kNN_classify(new_test, norm_dataset, labels, 4)
    print("You will probably like this person: ", res_lists[res-1])

if __name__ == '__main__':
    '''
    # 准备数据
    train_datas, labels = file_to_dataset('ch02_kNN\datingTestSet2.txt')
    print("train_datas:\n", train_datas)
    print("labels:\n", labels[0:20])
    # 可视化
    show_dataset(train_datas, labels)
    # 归一化
    norm_dataset, ranges, min_values = normalization(train_datas)
    print("归一化后数据集:\n", norm_dataset)
    print("每列的数值范围:\n", ranges)
    print("每列最小值:\n", min_values)
    # 测试
    dating_test()
    '''
    # 使用
    classify_person()
    

结果为:

percentage of time spend playing vodeo games?10
frequent flier miles earned per year?10000
liters of ice cream consumed per year?0.5
You will probably like this person:  in small doses
(3)手写数字识别

简单粗暴上代码:

from numpy import *
from kNN import kNN_classify
import sys, os

def img_to_vector(filename):
    """ 将一张数字图像转成向量格式 """
    digit_vector = zeros((1, 1024))
    try:
        with open(filename) as f_obj:
            for i in range(32):
                list_of_line = f_obj.readline()
                for j in range(32):
                    digit_vector[0, 32*i+j] = int(list_of_line[j])
            return digit_vector
    except FileNotFoundError:
        print("Sorry, the file " + filename + " does not exist.")
        sys.exit() 

def create_train_set():
    """ 将文件数据转为输入格式 """
    labels = [] # 存放训练集的标签
    train_file_list = os.listdir(r"ch02_KNN\digits\trainingDigits") # 得到所有文件名
    num_train_set = len(train_file_list) # 训练的图片个数
    train_datas = zeros((num_train_set, 1024)) # 创建训练集矩阵
    # 遍历处理每一张图片
    for i in range(num_train_set):
        full_file_name = train_file_list[i] # 0_13.txt
        file_name = full_file_name.split('.')[0] # 以‘。’分割,取第一个,即:0_13
        label = int(file_name.split('_')[0]) # 以‘_’分割,取第一个,0就是标签
        labels.append(label)
        train_datas[i,:] = img_to_vector(
            r"ch02_KNN\digits\trainingDigits\%s" % full_file_name 
        )
    return train_datas, labels

def hand_writing_test():
    """ 使用kNN算法识别手写数字 """
    # 获取训练集
    train_datas, labels = create_train_set()
    # 处理测试集
    test_file_list = os.listdir(r"ch02_KNN\digits\testDigits")
    error_count = 0.0 # 统计错误率
    num_test_set = len(test_file_list)
    for i in range(num_test_set):
        full_file_name = test_file_list[i]
        file_name = full_file_name.split('.')[0]
        label = int(file_name.split('_')[0])
        test_data = img_to_vector(
            r"ch02_KNN\digits\testDigits\%s" % full_file_name
        )
        # 使用 kNN分类器分类
        pred_res = kNN_classify(test_data, train_datas, labels, 3)
        print("the classifier prediction is: " + str(pred_res) + 
            ", the real answer is:" + str(label))
        if pred_res != label:
            error_count += 1.0
    print("Error rate : {:.2f}%".format(error_count/float(num_test_set)*100))
    print("Accuracy rate : {:.2f}%".format((1-(error_count/float(num_test_set)))
                                            *100))

if __name__ == "__main__":
    hand_writing_test()

结果为:

the classifier prediction is: 0, the real answer is:0
the classifier prediction is: 0, the real answer is:0
the classifier prediction is: 0, the real answer is:0
。。。
the classifier prediction is: 9, the real answer is:9
the classifier prediction is: 9, the real answer is:9
the classifier prediction is: 9, the real answer is:9
the classifier prediction is: 9, the real answer is:9
Error rate : 1.06%
Accuracy rate : 98.94%

以上,就是这个算法的介绍和实践,有错误的希望留言指正,有疑问的也欢迎留言一起交流学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JackkoLing

感谢你的支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值