用Python从零开始实现K近邻算法

时间:2023-12-18 12:55:02

KNN算法的定义:

KNN通过测量不同样本的特征值之间的距离进行分类。它的思路是:如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。K通常是不大于20的整数。KNN算法中,所选择的邻居都是已经正确分类的对象。该方法在定类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。

下面通过一个简单的例子说明一下:如下图,绿色圆要被决定赋予哪个类,是红色三角形还是蓝色四方形?如果K=3,由于红色三角形所占比例为2/3,绿色圆将被赋予红色三角形那个类,如果K=5,由于蓝色四方形比例为3/5,因此绿色圆被赋予蓝色四方形类。


由此也说明了KNN算法的结果很大程度取决于K的选择。

KNN算法的好处在于新数据进来,分类器可以马上学习并适应,但是计算成本也是线性增长,存储也是问题。

数学中的几个距离概念:

先上图比较直观:下图中红线代表曼哈顿距离,绿色代表欧氏距离,也就是直线距离,而蓝色和黄色代表等价的曼哈顿距离。曼哈顿距离——两点在南北方向上的距离加上在东西方向上的距离,即d(i,j)=|xi-xj|+|yi-yj|。

http://www.cnblogs.com/turingbrain/p/7711387.html

1. 欧氏距离

最常见的两点之间或多点之间的距离表示法,又称之为欧几里得度量,它定义于欧几里得空间中,如点 x = (x1,...,xn) 和 y = (y1,...,yn) 之间的距离为:

(1)二维平面上两点a(x1,y1)与b(x2,y2)间的欧氏距离:

(2)三维空间两点a(x1,y1,z1)与b(x2,y2,z2)间的欧氏距离:

(3)两个n维向量a(x11,x12,…,x1n)与 b(x21,x22,…,x2n)间的欧氏距离:

也可以用表示成向量运算的形式:

2. 曼哈顿距离

我们可以定义曼哈顿距离的正式意义为L1-距离或城市区块距离,也就是在欧几里得空间的固定直角坐标系上两点所形成的线段对轴产生的投影的距离总和。例如在平面上,坐标(x1, y1)的点P1与坐标(x2, y2)的点P2的曼哈顿距离为: 要注意的是,曼哈顿距离依赖座标系统的转度,而非系统在座标轴上的平移或映射。

通俗来讲,想象你在曼哈顿要从一个十字路口开车到另外一个十字路口,驾驶距离是两点间的直线距离吗?显然不是,除非你能穿越大楼。而实际驾驶距离就是这个“曼哈顿距离”,此即曼哈顿距离名称的来源, 同时,曼哈顿距离也称为城市街区距离(City Block distance)。

(1)二维平面两点a(x1,y1)与b(x2,y2)间的曼哈顿距离

(2)两个n维向量a(x11,x12,…,x1n)与 b(x21,x22,…,x2n)间的曼哈顿距离

   

3. 切比雪夫距离

若二个向量或二个点p 、and q,其座标分别为及,则两者之间的切比雪夫距离定义如下:,

这也等于以下Lp度量的极值:,因此切比雪夫距离也称为L∞度量。

以数学的观点来看,切比雪夫距离是由一致范数(uniform norm)(或称为上确界范数)所衍生的度量,也是超凸度量(injective metric space)的一种。

在平面几何中,若二点p及q的直角坐标系坐标为及,则切比雪夫距离为:。

玩过国际象棋的朋友或许知道,国王走一步能够移动到相邻的8个方格中的任意一个。那么国王从格子(x1,y1)走到格子(x2,y2)最少需要多少步?。你会发现最少步数总是max( | x2-x1 | , | y2-y1 | ) 步 。有一种类似的一种距离度量方法叫切比雪夫距离。(此处可在草稿纸上推导)

(1)二维平面两点a(x1,y1)与b(x2,y2)间的切比雪夫距离

(2)两个n维向量a(x11,x12,…,x1n)与 b(x21,x22,…,x2n)间的切比雪夫距离   

这个公式的另一种等价形式是

(2^5=32,3^5=243……只能帮到这了)

4. 闵可夫斯基距离(Minkowski Distance)

闵氏距离不是一种距离,而是一组距离的定义。

(1) 闵氏距离的定义

两个n维变量a(x11,x12,…,x1n)与 b(x21,x22,…,x2n)间的闵可夫斯基距离定义为:

 

其中p是一个变参数。

当p=1时,就是曼哈顿距离

当p=2时,就是欧氏距离

当p→∞时,就是切比雪夫距离

根据变参数的不同,闵氏距离可以表示一类的距离。

理论基础打好了,下面演示如何使用sklearn中的KNN算法(还是万年不变的标准化后的鸢尾花数据集):

如何选择 k 是一个重点,并且需要标准化数据。例子中用到的minkowski distance (闵可夫斯基距离)是普通的 Euclidean (欧式距离)和 Manhattan distance (曼哈顿距离) 的扩展。

K近邻算法

(或简称kNN)是易于理解和实现的算法,而且是你解决问题的强大工具。

http://python.jobbole.com/87407/

在本教程中,你将基于Python(2.7)从零开始实现kNN算法。该实现主要针对分类问题,将会用鸢尾花分类问题来演示。

这篇教程主要针对Python程序员,或者你可以快速上手Python,并且对如何从零实现kNN算法感兴趣。用Python从零开始实现K近邻算法

kNN算法图片,来自Wikipedia,保留所有权利

什么是kNN

kNN算法的模型就是整个训练数据集。当需要对一个未知数据实例进行预测时,kNN算法会在训练数据集中搜寻k个最相似实例。对k个最相似实例的属性进行归纳,将其作为对未知实例的预测。

相似性度量依赖于数据类型。对于实数,可以使用欧式距离来计算。其他类型的数据,如分类数据或二进制数据,可以用汉明距离。

对于回归问题,会返回k个最相似实例属性的平均值。对于分类问题,会返回k个最相似实例属性出现最多的属性。

kNN如何工作

kNN属于基于实例算法簇的竞争学习和懒惰学习算法。

基于实例的算法运用数据实例(或数据行)对问题进行建模,进而做出预测决策。kNN算法算是基于实例方法的一种极端形式,因为其保留所有的训练集数据作为模型的一部分。

kNN是一个竞争学习算法,因为为了做出决策,模型内部元素(数据实例)需要互相竞争。 数据实例之间客观相似度的计算,促使每个数据实例都希望在竞争中“获胜”或者尽可能地与给定的未知数据实例相似,继而在预测中做出贡献。

懒惰学习是指直到需要预测时算法才建立模型。它很懒,因为它只在最后一刻才开始工作。优点是只包含了与未知数据相关的数据,称之为局部模型。缺点是,在大型训练数据集中会重复相同或相似的搜索过程,带来昂贵的计算开销。

最后,kNN的强大之处在于它对数据不进行任何假设,除了任意两个数据实例之间距离的一致计算。因此,它被称为成为无参数或者非线性的,因为它没有预设的函数模型。

使用测量值对鸢尾花分类

本教程中我们演示的是鸢尾花分类问题。

原始数据集由来自3个品种鸢尾花的150个观察结果组成。对每一朵花有四个测量值:萼片长度、萼片宽度、花瓣长度、花瓣宽度,单位都是厘米。待预测鸢尾花属于Setosa,Versicolour,Virginica三个种类之一。

这是一个标准的数据集,所有示例的种类都是已知的。因此我们可以将数据集分割成训练数据集和测试数据集,并使用预测结果来评估实现的算法。这个问题,比较好的分类算法的准确度在90%以上,通常为96%甚至更好。

你可以从iris.data上免费下载数据集,更多细节见参考资料部分。

怎样用Python实现k-Nearest Neighbors

本教程将KNN算法分为如下几步:

数据处理:打开CSV文件获取数据,将原始数据分为测试集/训练集。

相似性度量:计算每两个数据实例之间的距离。

近邻查找:找到k个与当前数据最近的邻居。

结果反馈:从近邻实例反馈结果。

精度评估:统计预测精度。

主函数:将上述过程串起来。

1. 数据处理

我们首先要做的是把文件中的数据加载进来。这些数据以CSV形式存放在文件中,不包含header行和其它任何引用。我们可以使用open函数打开这些文件,然后使用csv module中的reader函数去读取文件。

Python

1
2
3
4
5
import csv
with open('iris.data', 'rb') as csvfile:
    lines = csv.reader(csvfile)
    for row in lines:
        print ', '.join(row)

接下来,我们需要将这些数据拆分为kNN用于做预测的训练集(training dataset)和用来评估模型精度的测试集(test dataset)。

首先,我们需要将以字符串形式载入的鸢尾花测量数据转换为容易处理的数组。接下来,我们需要将数据集随机的分为训练集与测试集。通常训练集/测试集的划分比例标准为67/33。

将上述步骤合在一起,我们可以定义一个叫loadDataset的函数,该函数可以加载指定的CSV文件,并按照指定的比例随机分为训练集与测试集。

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
import csv
import random
def loadDataset(filename, split, trainingSet=[] , testSet=[]):
    with open(filename, 'rb') as csvfile:
        lines = csv.reader(csvfile)
        dataset = list(lines)
        for x in range(len(dataset)-1):
            for y in range(4):
                dataset[x][y] = float(dataset[x][y])
            if random.random() < split:
                trainingSet.append(dataset[x])
            else:
                testSet.append(dataset[x])

将鸢尾花数据集的csv文件下载到本地目录,我们可以用鸢尾花数据集按照如下方式测试这个函数:

Python

1
2
3
4
5
trainingSet=[]
testSet=[]
loadDataset('iris.data', 0.66, trainingSet, testSet)
print 'Train: ' + repr(len(trainingSet))
print 'Test: ' + repr(len(testSet))

2.相似性度量

为了进行预测我们需要计算任意两个数据实例的相似性。这是必要的,因为对于给定的每一个测试集中的数据实例,我们都可以在训练集中找出k个相似性最高的数据实例,这样就可以依次进行预测。

假定鸢尾花的4个测量数据都为数值形式且单位相同,我们可以直接采用欧氏距离(Euclidean distance)进行相似性度量。欧式距离定义为:两组向量对应元素之差的平方和再做平方根运算。

另外,我们要控制哪个字段参与欧式距离的计算。具体来讲,我们只想包括前四个属性。一种方法是采用固定长度的向量来限制欧式距离,忽略最后的维度。

将上述步骤合在一起,我们可以将euclideanDistance函数定义为:

Python

1
2
3
4
5
6
import math
def euclideanDistance(instance1, instance2, length):
    distance = 0
    for x in range(length):
        distance += pow((instance1[x] - instance2[x]), 2)
    return math.sqrt(distance)

我们可以用一些样本数据来测试这个函数,具体如下:

Python

1
2
3
4
data1 = [2, 2, 2, 'a']
data2 = [4, 4, 4, 'b']
distance = euclideanDistance(data1, data2, 3)
print 'Distance: ' + repr(distance)

3. 近邻查找

由于我们有相似性度量的方法,因此可以采用该方法寻找未知数据实例的k个相似性最高的实例。

处理过程直接计算所有样本点到给定点的欧式距离,进而筛选距离最近的样本点子集。

下面是getNeighbors函数,该函数遍历训练集并返回与测试实例距离最近的k个近邻样本点(采用已经定义好的euclideanDistance函数)。

Python

1
2
3
4
5
6
7
8
9
10
11
12
import operator
def getNeighbors(trainingSet, testInstance, k):
    distances = []
    length = len(testInstance)-1
    for x in range(len(trainingSet)):
        dist = euclideanDistance(testInstance, trainingSet[x], length)
        distances.append((trainingSet[x], dist))
    distances.sort(key=operator.itemgetter(1))
    neighbors = []
    for x in range(k):
        neighbors.append(distances[x][0])
    return neighbors

我们可以按如下方法来测试这个函数:

Python

1
2
3
4
5
trainSet = [[2, 2, 2, 'a'], [4, 4, 4, 'b']]
testInstance = [5, 5, 5]
k = 1
neighbors = getNeighbors(trainSet, testInstance, 1)
print(neighbors)

4. 结果反馈

我们已经找到了测试实例的最近的邻居,下一步就是基于这些近邻做出预测结果。

我们可以让每个邻居对测试实例的类别属性进行投票,最终以票数多者做为预测结果。

下面的函数提供了从近邻投票中反馈多数投票结果的机制。该函数假定每个邻居的最后一列为类别属性。

Python

1
2
3
4
5
6
7
8
9
10
11
import operator
def getResponse(neighbors):
    classVotes = {}
    for x in range(len(neighbors)):
        response = neighbors[x][-1]
        if response in classVotes:
            classVotes[response] += 1
        else:
            classVotes[response] = 1
    sortedVotes = sorted(classVotes.iteritems(), key=operator.itemgetter(1), reverse=True)
    return sortedVotes[0][0]

我们可以输入近邻数据测试该函数,结果如下:

Python

1
2
3
neighbors = [[1,1,1,'a'], [2,2,2,'a'], [3,3,3,'b']]
response = getResponse(neighbors)
print(response)

该方法在平局的情况下依然会有一个返回结果,但是我们可以对其特殊处理,例如返回空值或者选择一个无偏随机结果

5. 精度评估

我们已经具备了所有的kNN算法片段。还有一件事情仍需我们重点关注,那就是就是如何评估预测精度。

评估模型精度最简单的方法就是计算正确预测结果数量占全部预测结果数量的比例,称为分类精度。

下面是getAccuracy函数,该函数统计所有的正确预测并返回正确分类的百分比精度。

Python

1
2
3
4
5
6
def getAccuracy(testSet, predictions):
    correct = 0
    for x in range(len(testSet)):
        if testSet[x][-1] is predictions[x]:
            correct += 1
    return (correct/float(len(testSet))) * 100.0

我们可以采用测试集与预测结果来测试该函数,结果如下:

Python

1
2
3
4
testSet = [[1,1,1,'a'], [2,2,2,'a'], [3,3,3,'b']]
predictions = ['a', 'a', 'a']
accuracy = getAccuracy(testSet, predictions)
print(accuracy)

6. 主函数

目前为止,我们已经具备了所有的算法组成元素,下面我们将这些元素串起来,组成主函数。

下面是从零开始实现的kNN算法完整Python代码。

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# Example of kNN implemented from Scratch in Python
import csv
import random
import math
import operator
def loadDataset(filename, split, trainingSet=[] , testSet=[]):
    with open(filename, 'rb') as csvfile:
        lines = csv.reader(csvfile)
        dataset = list(lines)
        for x in range(len(dataset)-1):
            for y in range(4):
                dataset[x][y] = float(dataset[x][y])
            if random.random() < split:
                 trainingSet.append(dataset[x])
             else:
                 testSet.append(dataset[x])
def euclideanDistance(instance1, instance2, length):
    distance = 0
    for x in range(length):
        distance += pow((instance1[x] - instance2[x]), 2)
    return math.sqrt(distance)
def getNeighbors(trainingSet, testInstance, k):
    distances = []
    length = len(testInstance)-1
    for x in range(len(trainingSet)):
        dist = euclideanDistance(testInstance, trainingSet[x], length)
        distances.append((trainingSet[x], dist))
    distances.sort(key=operator.itemgetter(1))
    neighbors = []
    for x in range(k):
        neighbors.append(distances[x][0])
    return neighbors
def getResponse(neighbors):
    classVotes = {}
    for x in range(len(neighbors)):
        response = neighbors[x][-1]
        if response in classVotes:
            classVotes[response] += 1
        else:
            classVotes[response] = 1
    sortedVotes = sorted(classVotes.iteritems(), key=operator.itemgetter(1), reverse=True)
    return sortedVotes[0][0]
def getAccuracy(testSet, predictions):
    correct = 0
    for x in range(len(testSet)):
        if testSet[x][-1] == predictions[x]:
            correct += 1
    return (correct/float(len(testSet))) * 100.0
def main():
    # prepare data
    trainingSet=[]
    testSet=[]
    split = 0.67
    loadDataset('iris.data', split, trainingSet, testSet)
    print 'Train set: ' + repr(len(trainingSet))
    print 'Test set: ' + repr(len(testSet))
    # generate predictions
    predictions=[]
    k = 3
    for x in range(len(testSet)):
        neighbors = get
        Neighbors(trainingSet, testSet[x], k)
     result = getResponse(neighbors)
     predictions.append(result)
     print('> predicted=' + repr(result) + ', actual=' + repr(testSet[x][-1]))
    accuracy = getAccuracy(testSet, predictions)
    print('Accuracy: ' + repr(accuracy) + '%')
main()

运行上述实例代码,你将会看到每一项预测分类结果和与之对应的测试集的实际分类结果。在运行的结尾,你将会看到整个模型的预测精度。当前的实例的预测精度略高于98%。

Python

1
2
3
4
5
6
7
...
> predicted='Iris-virginica', actual='Iris-virginica'
> predicted='Iris-virginica', actual='Iris-virginica'
> predicted='Iris-virginica', actual='Iris-virginica'
> predicted='Iris-virginica', actual='Iris-virginica'
> predicted='Iris-virginica', actual='Iris-virginica'
Accuracy: 98.0392156862745%

思路扩展

本节向读者提供了一些思路扩展,以便大家在本教程实现代码的基础上进一步应用和探索。

  • 回归问题:你可以将本实现应用到一些回归问题(预测基于数值的属性)。对近邻实例的汇总可能涉及要预测属性的平均数或者中位数
  • 归一化:当属性之间的度量单位不同时,很容易造成某些属性在距离度量层面成为主导因素。对于这类问题,你应该在相似性度量前将属性值都放缩到0-1范围内(称为归一化)。将模型升级以支持数据归一化。
  • 多种距离度量:通常有许多距离度量方法可供选用,如果你愿意,甚至可以创造出针对特定领域的距离度量方法。实现替代的距离度量方法,例如曼哈顿距离(Manhattan distance)或向量点积(vector dot product)。

该算法还有很多扩展形式可以探索。这里给出两个扩展思路,包括基于距离权重的k-most相似性实例去预测以及更进一步的基于树形结构查找相似度去查找。

更多的学习资源

本节将向读者提供一些k-Nearest Neighbors算法的深入学习资源,从理论上阐述kNN算法的工作原理,以及代码实现中应注意的实际问题。

问题

Wikipedia的鸢尾花数据集 UCI Machine Learning Repository的鸢尾花数据集

代码

本节提供了kNN算法在主流机器学习库中的开源实现链接。如果你考虑在业务应用中自己实现新版算法,可以先检查下这些已有的开源版本是否满足需要。

scikit-learn中的kNN实现

Weka中的kNN实现(非官方)

参考书籍

你也许已经有一本或者更多有关机器学习应用方面的书籍。本节重点介绍常见的机器学习书籍中有关k-Nearest Neighbors的章节。

《模型预测应用》,159-300页

《数据挖掘:实用机器学习技术,第三版(数据管理系统的摩根考夫曼系列)》,76页,128页,235页

《黑客机器学习》,第十章

《机器学习实战》,第二章

《编程集体智慧:构建WEB2应用程序》,第二章,第八章,第293页

最流行的4个机器学习数据集

Iris

Iris也称鸢尾花卉数据集,是一类多重变量分析的数据集。通过花萼长度,花萼宽度,花瓣长度,花瓣宽度4个属性预测鸢尾花卉属于(Setosa,Versicolour,Virginica)三个种类中的哪一类。

数据集特征: 多变量 记录数: 150 领域: 生活
属性特征: 实数 属性数目: 4 捐赠日期 1988-07-01
相关应用: 分类 缺失值? 网站点击数: 563347

Adult

该数据从美国1994年人口普查数据库抽取而来,可以用来预测居民收入是否超过50K$/year。该数据集类变量为年收入是否超过50k$,属性变量包含年龄,工种,学历,职业,人种等重要信息,值得一提的是,14个属性变量中有7个类别型变量。

数据集特征: 多变量 记录数: 48842 领域: 社会
属性特征: 类别型,整数 属性数目: 14 捐赠日期 1996-05-01
相关应用: 分类 缺失值? 网站点击数: 393977

Wine

这份数据集包含来自3种不同起源的葡萄酒的共178条记录。13个属性是葡萄酒的13种化学成分。通过化学分析可以来推断葡萄酒的起源。值得一提的是所有属性变量都是连续变量。

数据集特征: 多变量 记录数: 178 领域: 物理
属性特征: 整数,实数 属性数目: 13 捐赠日期 1991-07-01
相关应用: 分类 缺失值? 网站点击数: 337319

Car Evaluation

这是一个关于汽车测评的数据集,类别变量为汽车的测评,(unacc,ACC,good,vgood)分别代表(不可接受,可接受,好,非常好),而6个属性变量分别为「买入价」,「维护费」,「车门数」,「可容纳人数」,「后备箱大小」,「安全性」。值得一提的是6个属性变量全部是有序类别变量,比如「可容纳人数」值可为「2,4,more」,「安全性」值可为「low, med, high」。

数据集特征: 多变量 记录数: 1728 领域: N/A
属性特征: 类别型 属性数目: 6 捐赠日期 1997-06-01
相关应用: 分类 缺失值? 网站点击数: 272901

小结

通过比较以上4个数据集的差异,简单地总结:当需要试验较大量的数据时,我们可以想到「Adult」;当想研究变量之间的相关性时,我们可以选择变量值只为整数或实数的「Iris」和「Wine」;当想研究logistic回归时,我们可以选择类变量值只有两种的「Adult」;当想研究类别变量转换时,我们可以选择属性变量为有序类别的「Car Evaluation」。更多的尝试还需要对这些数据集了解更多才行。以上数据集下载地址:http://archive.ics.uci.edu/ml/


作者:紫松
链接:http://www.jianshu.com/p/be23b3870d2e
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

总结

在本教程中,你了解了k近邻算法的工作原理,其中一些隐喻可以用来思考本算法,并延伸到其他算法。我们用python从零开始实现了kNN算法,你理解了每一行代码,因此你可以基于本实现去探索扩展,满足你自己的项目需求。

以下是本教程涉及的5类学习算法:

  • K近邻算法:理解和实现起来较简单的算法,非常强大的非参数算法
  • 基于实例的算法:使用数据集(观察值)对问题建模
  • 竞争算法:通过模型元素之间的内部竞争来做出预测决策
  • 懒惰学习:需要做出预测时才开始建立模型
  • 相似性计算:计算数据实例之间的客观距离是该算法的一个关键特征