《算法图解》学习笔记(十):K 最近邻算法(附代码)

欢迎关注WX公众号:【程序员管小亮】

python学习之路 - 从入门到精通到大师

一、橙子还是柚子

请看下边的水果,是橙子还是柚子呢?
在这里插入图片描述
你肯定会说,柚子通常比橙子更大、更红。但是思维过程类似于这样:脑子里有个图表。
在这里插入图片描述
一般而言,柚子更大、更红。这个水果又大又红,因此很可能是柚子。但下面这样的水果呢?
在这里插入图片描述
如果判断这个水果是橙子还是柚子呢?一种办法是看它的邻居。来看看离它最近的三个邻居。
在这里插入图片描述
在这三个邻居中,橙子比柚子多,因此这个水果很可能是橙子。祝贺你,刚才就是使用 K最近邻(k-nearest neighbours,KNN) 算法进行了分类!这个算法非常简单。
在这里插入图片描述
KNN算法虽然简单却很有用!要对东西进行分类时,可首先尝试这种算法。下面来看一个更真实的例子。

二、创建推荐系统

假设你是Netflix,要为用户创建一个电影推荐系统。从本质上说,这类似于前面的水果问题!你可以将所有用户都放入一个图表中。
在这里插入图片描述
这些用户在图表中的位置取决于其喜好,因此喜好相似的用户距离较近。假设你要向Priyanka推荐电影,可以找出五位与他最接近的用户。
在这里插入图片描述
假设在对电影的喜好方面,Justin、JC、Joey、Lance和Chris都与Priyanka差不多,因此他们喜欢的电影很可能Priyanka也喜欢!

有了这样的图表以后,创建推荐系统就将易如反掌:只要是Justin喜欢的电影,就将其推荐给Priyanka。
在这里插入图片描述
但还有一个重要的问题没有解决。在前面的图表中,相似的用户相距较近,但如何确定两位用户的相似程度呢?

1)特征抽取

在前面的水果示例中,你根据个头和颜色来比较水果,换言之,你比较的特征是个头和颜色。现在假设有三个水果,你可抽取它们的特征。
在这里插入图片描述
再根据这些特征绘图。
在这里插入图片描述
从上图可知,水果 A 和 B 比较像。下面来度量它们有多像。要计算两点的距离,可使用毕达哥拉斯公式。
在这里插入图片描述
例如,A和B的距离如下。
在这里插入图片描述
A 和 B 的距离为1。你还可计算其他水果之间的距离。
在这里插入图片描述
这个距离公式印证了你的直觉:A 和 B 很像。

假设你要比较的是Netflix用户,就需要以某种方式将他们放到图表中。因此,你需要将每位用户都转换为一组坐标,就像前面对水果所做的那样。
在这里插入图片描述
在能够将用户放入图表后,你就可以计算他们之间的距离了。

下面是一种将用户转换为一组数字的方式。用户注册时,要求他们指出对各种电影的喜欢程度。这样,对于每位用户,都将获得一组数字!
在这里插入图片描述
Priyanka和Justin都喜欢爱情片且都讨厌恐怖片。Morpheus喜欢动作片,但讨厌爱情片(他讨厌好好的动作电影毁于浪漫的桥段)。前面判断水果是橙子还是柚子时,每种水果都用2个数字表示,你还记得吗?在这里,每位用户都用5个数字表示。
在这里插入图片描述
在数学家看来,这里计算的是五维(而不是二维)空间中的距离,但计算公式不变。
在这里插入图片描述
这个公式包含5个而不是2个数字。

这个距离公式很灵活,即便涉及很多个数字,依然可以使用它来计算距离。你可能会问,涉及5个数字时,距离意味着什么呢?这种距离指出了两组数字之间的相似程度。
在这里插入图片描述
这是Priyanka和Justin的距离。可以看出Priyanka和Justin很像。而Priyanka和Morpheus的距离为24,上述距离表明,Priyanka的喜好更接近于Justin而不是Morpheus。

太好了!现在要向Priyanka推荐电影将易如反掌:只要是Justin喜欢的电影,就将其推荐给Priyanka,反之亦然。你这就创建了一个电影推荐系统!

如果你是Netflix用户,Netflix将不断提醒你:多给电影评分吧,你评论的电影越多,给你的推荐就越准确。现在你明白了其中的原因:你评论的电影越多,Netflix就越能准确地判断出你与哪些用户类似。

2)回归

假设你不仅要向Priyanka推荐电影,还要预测她将给这部电影打多少分。为此,先找出与她最近的5个人。

顺便说一句,我老说最近的5个人,其实并非一定要选择5个最近的邻居,也可选择2个、10个或10 000个。这就是这种算法名为K最近邻而不是5最近邻的原因!
在这里插入图片描述
假设你要预测Priyanka会给电影Pitch Perfect打多少分。Justin、JC、Joey、Lance和Chris都给它打了多少分呢?
在这里插入图片描述
求这些人打的分的平均值,结果为4.2。这就是 回归(regression)。你将使用KNN来做两项基本工作——分类回归

  • 分类就是编组;
  • 回归就是预测结果(如一个数字)。

回归很有用。假设你在伯克利开个小小的面包店,每天都做新鲜面包,需要根据如下一组特征预测当天该烤多少条面包:

  • 天气指数1~5(1表示天气很糟,5表示天气非常好);
  • 是不是周末或节假日(周末或节假日为1,否则为0);
  • 有没有活动(1表示有,0表示没有)。

你还有一些历史数据,记录了在各种不同的日子里售出的面包数量。
在这里插入图片描述
今天是周末,天气不错。根据这些数据,预测今天能售出多少条面包呢?我们来使用KNN算法,其中的K为4。

首先,找出与今天最接近的4个邻居。
在这里插入图片描述
距离如下,因此最近的邻居为A、B、D和E。
在这里插入图片描述
将这些天售出的面包数平均,结果为218.75。这就是你今天要烤的面包数!

上面举例中在计算两位用户的距离时,使用的都是距离公式。还有更合适的公式吗?在实际工作中,经常使用余弦相似度(cosine similarity)。
余弦相似度,又称为余弦相似性,是通过计算两个向量的夹角余弦值来评估他们的相似度。余弦相似度将向量根据坐标值,绘制到向量空间中,如最常见的二维空间。
假设有两位品味类似的用户,但其中一位打分时更保守。他们都很喜欢Manmohan Desai的电影Amar Akbar Anthony,但Paul给了5星,而Rowan只给4星。如果你使用距离公式,这两位用户可能不是邻居,虽然他们的品味非常接近。余弦相似度不计算两个矢量的距离,而比较它们的角度,因此更适合处理前面所说的情况。
如果你要使用KNN,就一定要研究研究它!

3)挑选合适的特征

为了推荐电影,让用户指出他对各类电影的喜好程度。如果是让用户给一系列小猫图片打分呢?在这种情况下,你找出的是对小猫图片的欣赏品味类似的用户。对电影推荐系统来说,这很可能是一个糟糕的推荐引擎,因为选择的特征与电影欣赏品味没多大关系。

又假设你只让用户给《玩具总动员》《玩具总动员2》和《玩具总动员3》打分。这将难以让用户的电影欣赏品味显现出来!使用KNN时,挑选合适的特征进行比较至关重要。所谓合适的特征,就是:

  • 与要推荐的电影紧密相关的特征;
  • 不偏不倚的特征(例如,如果只让用户给喜剧片打分,就无法判断他们是否喜欢动作片)。

你认为评分是不错的电影推荐指标吗?我给The Wire的评分可能比House Hunters高,但实际上观看House Hunters的时间更长。该如何改进Netflix的推荐系统呢?

回到面包店的例子:对于面包店,你能找出两个不错和糟糕的特征吗?在报纸上打广告后,你可能需要烤制更多的面包;或者每周一你都需要烤制更多的面包。在挑选合适的特征方面,没有放之四海皆准的法则,你必须考虑到各种需要考虑的因素。

三、机器学习简介

KNN算法真的是很有用,堪称你进入神奇的机器学习领域的领路人!机器学习旨在让计算机更聪明。你见过一个机器学习的例子:创建推荐系统。下面再来看看其他一些例子。
在这里插入图片描述

1)OCR

OCR指的是 光学字符识别(optical character recognition),这意味着你可拍摄印刷页面的照片,计算机将自动识别出其中的文字。Google使用OCR来实现图书数字化。OCR是如何工作的呢?来看一个例子,请看下面的数字。
在这里插入图片描述
如何自动识别出这个数字是什么呢?可使用KNN。

  1. 浏览大量的数字图像,将这些数字的特征提取出来。
  2. 遇到新图像时,提取该图像的特征,再找出它最近的邻居都是谁!

这与前面判断水果是橙子还是柚子时一样。一般而言,OCR算法提取线段、点和曲线等特征。
在这里插入图片描述
遇到新字符时,可从中提取同样的特征。

与前面的水果示例相比,OCR中的特征提取要复杂得多,但再复杂的技术也是基于KNN等简单理念的。这些理念也可用于语音识别和人脸识别。你将照片上传到Facebook时,它有时候能够自动标出照片中的人物,这是机器学习在发挥作用!

OCR的第一步是查看大量的数字图像并提取特征,这被称为 训练(training)。大多数机器学习算法都包含训练的步骤:要让计算机完成任务,必须先训练它。下一个示例是垃圾邮件过滤器,其中也包含训练的步骤。

python版本代码如下,数据集是手写数字识别,仅供参考:

from os import listdir
from numpy import *
import numpy as np
import operator
import datetime

def KNN(test_data,train_data,train_label,k):
    #已知分类的数据集(训练集)的行数
    dataSetSize = train_data.shape[0]
    #求所有距离:先tile函数将输入点拓展成与训练集相同维数的矩阵,计算测试样本与每一个训练样本的距离
    all_distances = np.sqrt(np.sum(np.square(tile(test_data,(dataSetSize,1))-train_data),axis=1))
    #print("所有距离:",all_distances)
    #按all_distances中元素进行升序排序后得到其对应索引的列表
    sort_distance_index = all_distances.argsort()
    #print("文件索引排序:",sort_distance_index)
    #选择距离最小的k个点
    classCount = {}
    for i in range(k):
        #返回最小距离的训练集的索引(预测值)
        voteIlabel = train_label[sort_distance_index[i]]
        #print('第',i+1,'次预测值',voteIlabel)
        classCount[voteIlabel] = classCount.get(voteIlabel,0)+1
    #求众数:按classCount字典的第2个元素(即类别出现的次数)从大到小排序
    sortedClassCount = sorted(classCount.items(), key = operator.itemgetter(1), reverse = True)
    return sortedClassCount[0][0]

#文本向量化 32x32 -> 1x1024
def img2vector(filename):
    returnVect = []
    fr = open(filename)
    for i in range(28):
        lineStr = fr.readline()
        for j in range(28):
            returnVect.append(int(lineStr[j]))
    return returnVect

#从文件名中解析分类数字
def classnumCut(fileName):
    #参考文件名格式为:0_3.txt
    fileStr = fileName.split('.')[0]
    classNumStr = int(fileStr.split('_')[0])
    return classNumStr

#构建训练集数据向量,及对应分类标签向量
def trainingDataSet():
    train_label = []
    trainingFileList = listdir('trainingDigits')
    m = len(trainingFileList)
    train_data = zeros((m,1024))
    #获取训练集的标签
    for i in range(m):
        # fileNameStr:所有训练集文件名
        fileNameStr = trainingFileList[i]
        # 得到训练集索引
        train_label.append(classnumCut(fileNameStr))
        train_data[i,:] = img2vector('trainingDigits/%s' % fileNameStr)
    return train_label,train_data

#测试函数
def main():
    t1 = datetime.datetime.now()  # 计时开始
    Nearest_Neighbor_number = int(input('选取最邻近的K个值,K='))
    train_label,train_data = trainingDataSet()
    testFileList = listdir('testDigits')
    error_sum = 0
    test_number = len(testFileList)
    for i in range(test_number):
        #测试集文件名
        fileNameStr = testFileList[i]
        #切片后得到测试集索引
        classNumStr = classnumCut(fileNameStr)
        test_data = img2vector('testDigits/%s' % fileNameStr)
        #调用knn算法进行测试
        classifierResult = KNN(test_data, train_data, train_label, Nearest_Neighbor_number)
        print ("第",i+1,"组:","预测值:",classifierResult,"真实值:",classNumStr)
        if (classifierResult != classNumStr):
            error_sum += 1.0
    print ("
测试集总数为:",test_number)
    print ("测试出错总数:",error_sum)
    print ("
错误率:",error_sum/float(test_number)*100,'%')
    t2 = datetime.datetime.now()
    print('耗 时 = ', t2 - t1)

if __name__ == "__main__":
    main()

MATLAB版本代码如下:

function foo = main()
    addpath('mnistHelper');
    train_images = loadMNISTImages('train-images-idx3-ubyte');
    train_labels = loadMNISTLabels('train-labels-idx1-ubyte');

    test_images = loadMNISTImages('t10k-images-idx3-ubyte');
    test_labels = loadMNISTLabels('t10k-labels-idx1-ubyte');

    % showData(train_images, 100, 100);
    guesses(20, 3, train_images, train_labels, test_images, test_labels);
end

function foo = showData(images, rows, cols)
    grid = [];

    i = 0;
    for x = 1:rows
        imgs = [];
        for y = 1:cols
           i = i + 1;
           imgs = [imgs reshape(images(:, i), 28, 28)];
        end
        grid = [grid; imgs];
    end
    imshow(grid);
end

function d = distance(train_image, test_image)
    v = train_image - test_image;
    v = double(v);
    d = sqrt(v * v');
end

function result = border(image, value)
  image = reshape(image, 28, 28);
  result = zeros(28, 28);
  result(:, :) = value;
  result(2:27, 2:27) = image(2:27, 2:27);
  result = reshape(result, 784, 1);
end

function foo = guesses(count, k, train_images, train_labels, test_images, test_labels)
    [foo num_train_images] = size(train_images);
    [foo num_test_images] = size(test_images);

    correct = 0;

    grid = [];
    for x_ = 1:count
        x = floor(rand() * num_test_images);
        test_image = test_images(:, x);        
        correct_label = test_labels(x);
        dist = [];
        num_train_images = 50000;
        for i = 1:num_train_images

            train_image = train_images(:, i);
            d = distance(train_image', test_image');
            dist = [dist; [d i]];
        end
        sorted_ = (sortrows(dist, 1));
        sorted = sorted_(1:k, :);
        labels = [];
        for i = 1:k
          grid = [grid train_images(:, sorted(i, 2))];
          labels = [labels train_labels(sorted(i, 2))];
        end
        guess_label = mode(labels);
        if guess_label == correct_label
            correct = correct + 1;
            grid = [grid test_image];
        else
            grid = [grid border(test_image, 255)];
        end
    end
    correct
    
    showData(grid, count, k+1);
end

2)创建垃圾邮件过滤器

垃圾邮件过滤器使用一种简单算法——朴素贝叶斯分类器(Naive Bayes classifier)《机器学习实战》学习笔记(四):基于概率论的分类方法 - 朴素贝叶斯),你首先需要使用一些数据对这个分类器进行训练。
在这里插入图片描述
假设你收到一封主题为“collect your million dollars now!”的邮件,这是垃圾邮件吗?你可研究这个句子中的每个单词,看看它在垃圾邮件中出现的概率是多少。例如,使用这个非常简单的模型时,发现只有单词million在垃圾邮件中出现过。朴素贝叶斯分类器能计算出邮件为垃圾邮件的概率,其应用领域与KNN相似。

例如,你可使用朴素贝叶斯分类器来对水果进行分类:假设有一个又大又红的水果,它是柚子的概率是多少呢?朴素贝叶斯分类器也是一种简单而极其有效的算法。我们钟爱这样的算法!

3)预测股票市场

使用机器学习来预测股票市场的涨跌真的很难。对于股票市场,如何挑选合适的特征呢?股票昨天涨了,今天也会涨,这样的特征合适吗?又或者每年五月份股票市场都以绿盘报收,这样的预测可行吗?在根据以往的数据来预测未来方面,没有万无一失的方法。未来很难预测,由于涉及的变数太多,这几乎是不可能完成的任务。

四、总结

  • KNN用于分类和回归,需要考虑最近的邻居。
  • 分类就是编组。
  • 回归就是预测结果(如数字)。
  • 特征抽取意味着将物品(如水果或用户)转换为一系列可比较的数字。
  • 能否挑选合适的特征事关KNN算法的成败。

参考文章

  • 《算法图解》
原文地址:https://www.cnblogs.com/hzcya1995/p/13302685.html