算法概述
自我感觉K-近邻(k-NearestNeighbor)算法是最简单最易理解的分类算法了。怎么个简单法呢?简单到没有一个训练分类器的过程,仅仅根据需要分类的样本到已知类别的样本之间的距离来进行分类。
简单来讲,如果存在一个训练样本集,并且训练集中每个样本对应的类别也已知;那么对于未知类别的新样本,我们计算它到每个训练样本的距离,然后选择距离最近的k个样本,这k个样本中计数最多的一类就是待分类样本的类别。
k-NN算法的步骤如下:
- 根据特征值计算待分类样本到训练集每个样本的距离;
- 按照距离递增次序排序;
- 选出与待分类样本距离最近的前 k 个样本;
- 计算前 k 个样本中每一类别的数量;
- 将前 k 个样本中计数最多的类别数作为待分类样本的类别。
注意
-
距离
K-近邻算法计算距离时可以使用常用的距离度量方法,一般可以选择欧式距离,计算方式如下:
d = ∑ i = 0 n ( x i − y i ) 2 d=\sqrt{\sum_{i=0}^{n}\left(x_{i}-y_{i}\right)^{2}} d=i=0∑n(xi−yi)2
x x x 和 y y y 分别表示两个样本, n n n 表示特征维度。 -
k的取值
k的取值很重要,k值太小的话算法对于一些噪声成分会比较敏感,k太大的话离待分类样本距离较远的点也会对分类结果产生影响,这都不是想要的结果。一般建议k取小于20的整数。
利用k-NN实现手写数字识别
k-NN算法的原理比较容易理解,接下来通过手写数字识别的例子来看看怎么实现一个k-近邻分类器。
数据集
这里用到的手写数字数据集不是MNIST数据集,而是采用文本格式存储的数字图像,如下图所示。每个文本文件存储着 32 ∗ 32 32*32 32∗32 个黑白像素点,用来表示一个手写数字。数据集中包含了2000个训练样本和900个测试样本。
代码实现
像素文本转换为特征向量
上面提到,每个样本都是一个
32
∗
32
32*32
32∗32的二进制图像,因此我们首先将每个样本读取为一个向量的表示方式:
def img2vector(fileName):
returnVect = np.zeros((1, 1024))
with open(fileName) as fr:
i = 0
for lineStr in fr: # 按行读取文件
for j in range(32):
returnVect[0, 32*i+j] = int(lineStr[j])
i += 1
return returnVect
传入参数为文件名,最后返回样本的向量表示。
分类函数
k-NN没有一个显式的训练过程,对于每个测试样本,对其进行分类的时候都需要计算到每一个训练样本的距离,进而根据距离最近的k个样本的类别进行分类。
def classify(testX, trainingSet, labels, k):
'''
testX: 测试样本的特征向量
trainingSet: 训练集特征
labels: 训练集类别标签
k: 最邻近样本个数
'''
m = trainingSet.shape[0] # 训练样本的数量
distance = getDistance(testX, trainingSet) # 计算测试样本到每个训练样本的距离
nearestIndices = np.argsort(distance)[:k] # 距离最小的k个样本的索引
# 计算前k个样本中每一类的数量
classCount = {}
maxCount = 0
nearestClass = None
for i in range(k):
voteIlabel = labels[nearestIndices[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
if classCount[voteIlabel] > maxCount: # 更新最多的计数和其对应的标签
maxCount = classCount[voteIlabel]
nearestClass = voteIlabel
return nearestClass
完整的代码实现如下:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
'''
@Date : 2019/9/27
@Author : Rezero
'''
import numpy as np
import os
def img2vector(fileName):
returnVect = np.zeros((1, 1024))
with open(fileName) as fr:
i = 0
for lineStr in fr: # 按行读取文件
for j in range(32):
returnVect[0, 32*i+j] = int(lineStr[j])
i += 1
return returnVect
def getDistance(testX, trainingSet):
return np.sqrt(np.sum((trainingSet - testX)**2, axis=1)) # 欧式距离
def classify(testX, trainingSet, labels, k):
'''
testX: 测试样本的特征向量
trainingSet: 训练集特征
labels: 训练集类别标签
k: 最邻近样本个数
'''
m = trainingSet.shape[0] # 训练样本的数量
distance = getDistance(testX, trainingSet) # 计算测试样本到每个训练样本的距离
nearestIndices = np.argsort(distance)[:k] # 距离最小的k个样本的索引
# 计算前k个样本中每一类的数量
classCount = {}
maxCount = 0
nearestClass = None
for i in range(k):
voteIlabel = labels[nearestIndices[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
if classCount[voteIlabel] > maxCount: # 更新最多的计数和其对应的标签
maxCount = classCount[voteIlabel]
nearestClass = voteIlabel
return nearestClass
def main():
labels = []
trainingFiles = os.listdir('data/trainingDigits') # 训练集路径
trainNum = len(trainingFiles) # 训练集样本数
trainingMat = np.zeros((trainNum, 1024))
for i in range(trainNum):
fileName = trainingFiles[i]
clas = int(fileName.split('_')[0]) # 当前样本的类别
labels.append(clas)
trainingMat[i, :] = img2vector('data/trainingDigits/' + fileName) # 把每个样本所表示的数字读取为一个1*1024的向量
testFiles = os.listdir('data/testDigits') # 测试集路径
errorCount = 0 # 分类错误计数
testNum = len(testFiles)
for i in range(testNum):
fileName = testFiles[i]
clas = int(fileName.split('_')[0]) # 样本的真实类别
vectorUnderTest = img2vector('data/testDigits/' + fileName) # 当前测试样本的向量
# 使用kNN分类器对当前样本进行分类,设置k=3
classifierResult = classify(vectorUnderTest, trainingMat, labels, 3)
print("The classifier came back with: %d, the real answer is: %d" % (classifierResult, clas))
if clas != classifierResult:
errorCount += 1
print("The total number of errors is: %d" % errorCount)
print("the total error rate is %f: " %(errorCount/testNum))
if __name__ == "__main__":
main()
k-NN算法的优缺点
-
优点
简单易懂、精度高、对异常值不敏感、无数据输入假定,可以用于数值型和离散型数据 -
缺点
计算复杂度高,单个样本分类需要计算到所有训练样本(已知样本)的距离;空间复杂度高,需要存储所有的训练样本,空间开销大。
参考资料
《机器学习实战》第二章:k-近邻算法