1.概述
-
简介:k-近邻算法(k-Nearest Neighbors),是一种基本的分类和回归算法。
-
分类原理:通过测量不同特征值之间的距离进行分类。具体为:将新数据的信息与样本集中数据的特征进行比对,然后选择k个最近邻个数据中出现次数最多的标签作为新数据的标签。前面提到的距离通过欧式距离公式可得,如式1所示
d = ( x A 0 − x B 0 ) 2 + ( x A 1 − x B 1 ) 2 (1) d = \sqrt{(xA_0-xB_0)^2+(xA_1-xB1)^2}\tag1 d=(xA0−xB0)2+(xA1−xB1)2(1)
直接理解为计算出两点间的距离 -
举例:假设测定一块牛排是否为合格品
import numpy as np import matplotlib.pyplot as plt # 定义牛排样品的特征以及是否合格,y轴为1表示合格,为0表不合格 X_train = np.array( [[2, 5], [3, 4], [4, 7], [6, 1], [7, 2], [8, 4], [1, 3], [2, 6], [3, 8], [5, 2], [6, 3], [7, 5], [8, 6], [9, 3], [10, 4], [11, 6], [12, 2], [13, 3], [14, 5], [15, 4]]) y_train = np.array([1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1]) # 定义k-近邻算法 def knn(X_train, y_train, x, k): distances = np.sqrt(np.sum((X_train - x)**2, axis=1)) # 计算欧式距离 nearest = np.argsort(distances)[:k] # 找到距离最近的k个样本的索引 y = np.bincount(y_train[nearest]).argmax() # 统计k个样本中出现最多的标签 return y # 绘制散点图 def showPic(x, y): plt.scatter(x[:, 0], x[:, 1], c=y) plt.xlabel('Feature 1') plt.ylabel('Feature 2') plt.savefig('./2.1.png', dpi=300) plt.show() if __name__ == '__main__': # 输入牛排的属性值 x = np.array([10, 5]) # 使用k-近邻算法判断牛排是否为合格品 y = knn(X_train, y_train, x, k=3) if y == 1: print("这是一块合格的牛排") else: print("这是一块不合格的牛排") showPic(X_train, y_train)
二维离散点图
三维点图
分析:用以训练的数据集有20个具有两个特征的样本数据(y值为0表示不合格,为1表合格),k值设置为3(表示选取与测试样本距离最近的3个样本),若距离最近的这3个样本中有至少2个合格品,则测试样本也被判定为合格品,反之不合格。
knn
函数中,计算出对应的欧氏距离,用argsort
函数找出最近的k个点的索引,bincount
函数计算出这k个点中每个类别出现的次数,返回出现次数最多的那个样本。最后的结果显示,y
的值为0,测试的牛排是不合格品。
算法实现过程:计算距离,从小到大排序,选取前k的点,将点与其类别一一对应,取频数最大者为预测结果
准备数据:因为算法用到“欧氏距离”,如果样本存在特征的值特别大或特别小,将导致计算结果有很大出入,因此需要考虑是否要对数据做归一化处理,量子力学里常用的归一化方法之一如式2所示
n
e
w
D
a
t
a
=
(
o
l
d
D
a
t
a
−
m
i
n
)
/
(
m
a
x
−
m
i
n
)
(2)
newData = (oldData-min)/(max-min)\tag2
newData=(oldData−min)/(max−min)(2)
这种方法对数据做归一化处理后能将数值转换为[0,1]
间的值,虽然提高了算法复杂度,但是能很大程度地提高准确性
优点:容错率高,不易受坏值影响,简单易懂好实现,能有效实现分类
缺点:计算量非常大,且需要因地制宜设置k值
2.识别手写数字
使用到书本提供的数据集,内含约2千个例子,形成的数字为0-9,每个数字样本约200个
先将一副32x32
的二进制图像转为1x1024
的numpy数组,打开给定的文件,循环读出文件的前32行,将每行的前32个字符转换为一个长度为32的一维数组中,并返回这个数组。
def img2vector(filename):
#创建1x1024零向量
returnVect = np.zeros((1, 1024))
#打开文件
fr = open(filename)
#按行读取
for i in range(32):
#读一行数据
lineStr = fr.readline()
#每一行的前32个元素依次添加到returnVect中
for j in range(32):
returnVect[0, 32*i+j] = int(lineStr[j])
#返回转换后的1x1024向量
return returnVect
实例:手写数字识别算法
先设置一个空的测试集hwLabels
,用listdir
读取指定文件夹下所有文件名,记录下文件数量m
,将测试集矩阵的所有元素置为0。创建一个for循环,读取所有文件之名称,并将其拆分得到样本所表征的数字,所有的文件均是诸如0_0.txt
,5_99.txt
格式命名,他们分别表示:字符为0的第0个样本,字符为5的第99个样本(我们主要是要获得这些文件所表征的字符而非顺序),将这些字符逐一导入测试集hwLabels
,再依次把各个文件里的数据存储进测试集矩阵。自此,测试集初始化完毕。
下一步要用到k-近邻算法,构建一个k-NN分类器,创建一个实例对象neigh
,‘k值’设置为3,表示每次找距离最近的三个样本;使用fit()
拟合训练数据,参数是刚才所有填充好的测试矩阵和他们对应的字符;然后就可以开始测试算法的准确性,通过一个测试样本集分类后得到的预测结果,计算出算法出错的概率。
def handwritingClassTest():
# 测试集的Labels
hwLabels = []
# 返回trainingDigits目录下的文件名
trainingFileList = listdir('train')
# 返回文件夹下文件的个数
m = len(trainingFileList)
# 初始化训练的Mat矩阵,测试集
trainingMat = np.zeros((m, 1024))
# 从文件名中解析出训练集的类别
for i in range(m):
# 获得文件的名字
fileNameStr = trainingFileList[i]
# 获得分类的数字
classNumber = int(fileNameStr.split('_')[0])
# 将获得的类别添加到hwLabels中
hwLabels.append(classNumber)
# 将每一个文件的1x1024数据存储到trainingMat矩阵中
trainingMat[i, :] = img2vector('train/%s' % (fileNameStr))
# 构建kNN分类器
neigh = kNN(n_neighbors=3, algorithm='auto')
# 拟合模型, trainingMat为测试矩阵,hwLabels为对应的标签
neigh.fit(trainingMat, hwLabels)
# 返回testDigits目录下的文件列表
testFileList = listdir('test')
# 错误检测计数
errorCount = 0.0
# 测试数据的数量
mTest = len(testFileList)
# 从文件中解析出测试集的类别并进行分类测试
for i in range(mTest):
# 获得文件的名字
fileNameStr = testFileList[i]
# 获得分类的数字
classNumber = int(fileNameStr.split('_')[0])
# 获得测试集的1x1024向量,用于训练
vectorUnderTest = img2vector('test/%s' % (fileNameStr))
# 获得预测结果
# classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3)
classifierResult = neigh.predict(vectorUnderTest)
# print("分类返回结果为%d\t真实结果为%d" % (classifierResult, classNumber))
if(classifierResult != classNumber):
errorCount += 1.0
print("总共错了%d个数据\n错误率为%f%%" % (errorCount, errorCount / mTest * 100))
3.小结
k-NN的优点是容易理解,训练原理简单,但存在计算复杂度过高,存储开销大等问题,它需要计算每个测试样本与所有训练样本之间的距离,当样本集很大时,计算量也会变得很大;并且,训练样本需要存储各自的“距离”,“邻居”。书上有一个“约会系统”的例子,里面存在如下两个参数,“飞行里程数”,“每周消费的冰淇淋公升数”,前者数值至少为几百,后者的值在[0,1.5]
这个区间,k-NN默认样本的所有特征对欧氏距离计算的贡献相同,如果没有对数据做归一化处理的话,会导致“冰淇淋消耗”这一特征几乎被抹去,样本的判断基本取决于“飞行里程数”,导致算法测试样本的准确率降低