一、简介
KNN算法,全称为K Nearest Neighbor算法,也叫K临近算法,是一种懒惰学习的有监督分类算法。(懒惰学习是指训练后并不建立确定的模型,而是根据输入的数据与训练集的关系即时进行分类。)KNN算法作为机器学习中较基础的算法,其分类的准确性与变量K的取值有很大关系。
二、原理
先举一个经典的例子,根据电影中接吻和打斗镜头出现的次数判断电影类型:
电影名称 | 打斗次数 | 接吻次数 | 电影类型 |
California Man | 3 | 104 | Romance |
He’s Not Really into Dudes | 2 | 100 | Romance |
Beautiful Woman | 1 | 81 | Romance |
Kevin Longblade | 101 | 10 | Action |
Robo Slayer 3000 | 99 | 5 | Action |
Amped II | 98 | 2 | Action |
Test | 18 | 90 | Unknown |
将上述表格中的前六组数据作为训练集,最后一组数据作为测试集,将打斗次数和接吻次数作为特征,将电影类型作为标记,则可以将这六个实例看作平面上的六个点,打斗次数为x值,接吻次数为y值(若特征数大于2,则可由平面拓展至体或超平面),可得到下表:
点 | x值 | y值 | 类型 |
A | 3 | 104 | Romance |
B | 2 | 100 | Romance |
C | 1 | 81 | Romance |
D | 101 | 10 | Action |
E | 99 | 5 | Action |
F | 98 | 2 | Action |
G | 18 | 90 | Unknown |
为便于理解,将这些点绘制在平面直角坐标系中,如图:
在KNN算法中,若要确定Test(红点)的类型,只需在坐标系中找到与点G距离最近的K个点,就可以根据这K个点的类型判断出点G的类型:若这K个点对应的类型均为Romance/Action,则点G的类型也为Romance/Action;若这K个点对应的类型中既有Romance也有Action,则根据投票法则判断点G的类型,因此常取K的值为3、5、7等奇数。
对于点G到这K个点的距离有多种定义,如欧几里得度量(Euclidean Distance)、余弦值(cos)、相关度(correlation)、曼哈顿距离(Manhattan Distance)等,这里根据欧几里得度量计算,公式为:
注意:训练后,进行测试时会先计算测试实例对应点到训练集中所有点的距离并存储,再根据K的取值找出与测试点距离最近的K个点。
但当出现下图中的情况时,判断的准确率将大大降低:
对于上图中的Y,虽然属于ω1,但因为ω2在这一侧的分布过于密集,导致极有可能将Y分类为ω2。为避免这种情况的出现,我们可以对上述算法进行改进,即在计算训练集中各点到测试点的距离后,先将每段距离乘以权重再存储,如乘以(d为距离)。
三、应用
环境:Python 3.6
这里使用IRIS数据集测试,以txt文件的形式存储在本地,格式如下:
5.1,3.5,1.4,0.2,Iris-setosa
7.0,3.2,4.7,1.4,Iris-versicolor
6.3,3.3,6.0,2.5,Iris-virginica
......
具体实现如下:
import csv
import math
import operator
import random
def loadDataset(filename, split, trainingSet = [], testSet = []):
with open(filename, 'rt') 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 i in range(k):
neighbors.append(distances[i][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.items(), 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
if __name__ == '__main__':
trainingSet = []
testSet = []
split = 0.25
loadDataset(r'D:\XXX\iris.txt', split, trainingSet, testSet)
predictions = []
k = 3
for x in range(len(testSet)):
neighbors = getNeighbors(trainingSet, testSet[x], k)
result = getResponse(neighbors)
predictions.append(result)
accuracy = getAccuracy(testSet, predictions)
print(accuracy, "%")
loadDataset:从本地文件载入使用的数据集。 先用csv
模块从本地文件中读取数据集中的所有实例,再将数据集转化为二维数组,形如[['5.1', '3.5', '1.4', '0.2', 'Iris-setosa'], ['4.9', '3.0', '1.4', '0.2', 'Iris-setosa'], ['4.7', '3.2', '1.3', '0.2', 'Iris-setosa'], ...]
。变量split
的作用是将原数据集按比例分为训练集和测试集,如当split == 0.75
时,表示将原数据集的作为训练集,作为测试集。
euclideanDistance:根据欧几里得度量计算某测试实例对应的点与训练集中点的距离。 由于该数据集中有四个特征变量,故根据公式计算两点间的距离。
Tips:这里的x、y不表示横坐标和纵坐标,而是表示instance1和instance2。
getNeighbors:得到与某测试实例距离最近的k个点。 先调用euclideanDistance()
计算该实例对应的点与训练集中各点的距离,再将所有距离排序,取出与测试点距离最近的k个点。
Tips:这里返回的是实例,不是对应的类型。
getResponse:得到对某测试实例分类结果。 遍历在getNeighbors()
中得到的与测试实例距离最近的k个点并排序,取第0位的值,即最近的k个点中最多的类型作为分类结果。通过在主函数中循环调用来得到整个测试集的分类结果。
Tips:这里的len(neighbors)
即为k。
getAccuracy:计算预测结果的准群率。 比较测试集的最后一位与分类结果,用正确的个数除以总数再乘以100.0,得到正确的百分比,即准确率。