k近邻(k-Nearest Neighbor,kNN)算法是机器学习算法中最基础、最简单的算法之一。它既能用于分类,也能用于回归。kNN通过计算不同特征值之间的距离来进行分类或回归。 具体的距离计算公式点这里
kNN分类算法
基本思想
输入没有标签的数据后,将这个没有标签的数据的每个特征与训练集中的数据对应的特征进行比较,然后提取特征最相近的数据(最近邻)的分类标签。
一般而言,只选择样本数据集中前k个最相似的数据,这就是kNN算法中k的由来,通常k是不大于20的整数。最后,选择k个最相似数据中出现次数最多的类别,作为新数据的分类。
用下图来说明下,分类标签有两类:蓝色正方形,红色三角形。我们待分类的数据是绿色圆形点。
如果k=3,那么离绿色点最近的3个点(2个红色三角形和1个蓝色正方形)(圆形实线以内)中红色三角形所占比例最大(2/3) ,于是绿色点属于红色三角形;如果k=5,那么离绿色点最近5个点(2个红色三角形和3个蓝色正方形)(圆形虚线以内)中蓝色正方形所占比例最大(3/5),于是绿色点属于蓝色正方形。同时也说明了kNN算法的结果很大程度取决于k的选择 。
步骤
对未知类别属性的数据集中的每个点依次执行以下操作:
- 计算已知类别数据集中的点与当前点之间的距离;
- 按照距离递增依次排序;
- 选取与当前点距离最小的k个点;
- 确定前k个点所在类别的出现频率;
- 返回前k个点出现频率最高的类作为当前点的预测分类。
python实现
说明:本文python版本为3.6
1. 约会网站的配对
采用海伦收集约会数据进行分类预测,海伦的样本数据共有1000行,每个样本数据占据一行,样本主要包括三个特征和一个标签,三个特征如下:
- 每年飞行常客里程数;
- 玩视频游戏所耗时间百分比;
- 每周消费的冰淇淋公升数;
三个特征之间的取值范围差异较大,需要对特征数据进行归一化,使三个特征同等重要。本文采用最大-最小归一化,将取值范围转化到0~1之间。
import numpy as np
def file2matrix(filename):
"""
将文本记录转换为numpy的解析程序
:param filename:文件路径名
:return:
"""
fr = open(filename)
arrayOLines = fr.readlines()
numOLines = len(arrayOLines)
retMat = np.zeros((numOLines, 3))
labels = []
index = 0
for line in arrayOLines:
line = line.strip() # 截取掉所有的回车字符
listFromLine = line.split('\t') # 使用TAB字符\t进行分隔
retMat[index, :] = listFromLine[:3] # 前三列是特征值
labels.append(int(listFromLine[-1])) # 最后一列表示标签
index += 1
return retMat, labels
def autoNorm(dataSet):
"""对特征值进行最大-最小归一化,将数据范围处理到0~1之间
计算公式:(x-min)/(max-min)"""
minVals = dataSet.min(0) # 从列中选出最小值
maxVals = dataSet.max(0)
rangeVals = maxVals - minVals
m = dataSet.shape[0]
normDataSet = dataSet-np.tile(minVals,(m,1))
normDataSet = normDataSet/np.tile(rangeVals,(m,1)) # 特征值相除
return normDataSet, rangeVals, minVals
def kNN_classify(X, dataSet, labels, k):
"""
k近邻算法
:param X: 需要预测类别的点
:param dataSet: 特征数据集
:param labels: 标签集
:param k: 输入的k为正整数
:return:
"""
# 计算样本X与dataSet之间的距离
m = len(dataSet)
diffMat = np.tile(X, (m, 1)) - dataSet
sqare_diffMat = diffMat ** 2
sum_sqare_diffMat = sqare_diffMat.sum(axis=1)
distances = np.sqrt(sum_sqare_diffMat)
soretedDistIndex = distances.argsort()
classCounts = {}
for i in range(k):
votelabel = labels[soretedDistIndex[i]]
classCounts[votelabel] = classCounts.get(votelabel, 0) + 1
sortedClassCount = sorted(classCounts.items(), key=lambda p: p[1],
reverse=True)
return sortedClassCount[0][0]
def datingClassTest():
"""分类器针对约会网站的测试代码"""
hoRatio = 0.1 # 测试数据占总数据的比例
# 海伦收集约会数据,前三列为特征,最后一列为标签
dataSet, labels = file2matrix('datingTestSet2.txt')
# 由于特征值之间的取值范围差异较大,故需对特征值进行归一化
normDataSet, rangeVals, minVals = autoNorm(dataSet)
m = normDataSet.shape[0]
numTestSet = int(m*hoRatio) # 测试数据量
errorCount = 0.0
# 训练数据
trainX, trainLabel = normDataSet[numTestSet:, :], labels[numTestSet:]
for i in range(numTestSet):
clssifyResult = kNN_classify(normDataSet[i,:],trainX,trainLabel,3)
# print("the classify came back with : %d, the real answer is: %d" %(clssifyResult,labels[i]))
if clssifyResult != labels[i]:
errorCount += 1
print("the total error rate is: %f" %(errorCount/numTestSet)) # 错误率
def classifyPerson():
"""约会网站预测函数,根据输入数据使用分类器获得预测分类结果"""
# 讨厌、一般喜欢、非常喜欢
resultList = ['not at all','in small doses','in large doses']
ffMiles = float(input('frequent flier miles earned per year?'))
percentTats = float(input('percentage of time spend playing video games?'))
iceCream = float(input('liters of ice cream consumed per year?'))
datingDataMat,datingLabels = file2matrix('datingTestSet2.txt')
normMat,rangeVals,minVals = autoNorm(datingDataMat)
X = [[ffMiles,percentTats,iceCream]]
normX = (X-minVals)/rangeVals
classifyResult = kNN_classify(normX,normMat,datingLabels,3)
print('You will probably like this person : ',resultList[classifyResult-1])
if __name__ == '__main__':
datingClassTest()
classifyPerson()
运行结果
the total error rate is: 0.050000
frequent flier miles earned per year?10000
percentage of time spend playing video games?10
liters of ice cream consumed per year?0.5
You will probably like this person : in small doses
改变函数datingClassTest中的变量hoRatio和变量k的值,使得错误率随着变量值的变化而减少。
2. 手写数字识别
识别的数字是0~9,需要识别的数字都是图像:宽高是32*32的黑白图像。虽然采用文本格式存储图像不能有效地利用内存空间,但本文为方便理解,还是将图像转换为文本格式。
按kNN原理实现
import numpy as np
import os
import time
import kNN_classify
def img2Vector(filename):
"""把一个32*32的二进制图像矩阵转换为1*1024的向量"""
returnVect = np.zeros((1,1024))
fr = open(filename)
for i in range(32):
lineStr = fr.readline()
for j in range(32):
returnVect[0,i*32+j] = int(lineStr[j])
return returnVect
def handWritingClassTest_by_theory():
"""手写数字识别系统的测试代码"""
hwLabels =[]
traingFileList = os.listdir('trainingDigits')
m = len(traingFileList)
trainMat = np.zeros((m,1024))
for i in range(m):
fileNameStr = traingFileList[i]
fileStr = fileNameStr.split('.')[0]
classNumStr = fileStr.split('_')[0]
hwLabels.append(classNumStr)
trainMat[i,:] = img2Vector('trainingDigits/%s' %(fileNameStr))
testFileList = os.listdir('testDigits')
m_test = len(testFileList)
error_count = 0.0
for i in range(m_test):
fileNameStr = testFileList[i]
fileStr = fileNameStr.split('.')[0]
classNumStr = int(fileStr.split('_')[0])
testVec = img2Vector('testDigits/%s' % (fileNameStr))
classifyResult = int(kNN_classify(testVec,trainMat,hwLabels,3))
# print("the classifier came back with: %d, the real answer is: %d" % (classifyResult,classNumStr))
if classifyResult != classNumStr:
error_count += 1.0
print("\nthe total number of errors is: %d" % (error_count))
print("\nthe total error rate is : %f" % (error_count/m_test))
if __name__ == '__main__':
print("按kNN原理实现手写数字识别算法")
start_time = time.time()
handWritingClassTest_by_theory()
print("\n耗时:",time.time()-start_time)
运行结果
调用sklearn中的kNN算法实现
import numpy as np
import os
import time
from sklearn.neighbors import KNeighborsClassifier as KNN
def img2Vector(filename):
"""把一个32*32的二进制图像矩阵转换为1*1024的向量"""
returnVect = np.zeros((1,1024))
fr = open(filename)
for i in range(32):
lineStr = fr.readline()
for j in range(32):
returnVect[0,i*32+j] = int(lineStr[j])
return returnVect
def handWritingClassTest_by_sklearn():
"""手写数字识别系统的测试代码"""
hwLabels =[]
traingFileList = os.listdir('trainingDigits')
m = len(traingFileList)
trainMat = np.zeros((m,1024))
for i in range(m):
fileNameStr = traingFileList[i]
fileStr = fileNameStr.split('.')[0]
classNumStr = fileStr.split('_')[0]
hwLabels.append(classNumStr)
trainMat[i,:] = img2Vector('trainingDigits/%s' %(fileNameStr))
# 构建kNN分类器
neigh = KNN(n_neighbors=3, algorithm='auto')
# 拟合模型, trainMat为训练数据集,hwLabels为对应的标签集
neigh.fit(trainMat, hwLabels)
testFileList = os.listdir('testDigits')
m_test = len(testFileList)
error_count = 0.0
for i in range(m_test):
fileNameStr = testFileList[i]
fileStr = fileNameStr.split('.')[0]
classNumStr = int(fileStr.split('_')[0])
testVec = img2Vector('testDigits/%s' % (fileNameStr))
classifyResult = int(neigh.predict(testVec))
# print("the classifier came back with: %d, the real answer is: %d" % (classifyResult,classNumStr))
if classifyResult != classNumStr:
error_count += 1.0
print("\nthe total number of errors is: %d", error_count)
print("\nthe total error rate is : %f" % (error_count/m_test))
if __name__ == '__main__':
print("调用sklearn中的kNN算法实现手写数字识别算法")
start_time = time.time()
handWritingClassTest_by_theory()
print("\n耗时:", time.time() - start_time)
运行结果
k的选择
可以从k=1开始,逐步增加,用检验数据来分析错误率,从而选择最优k。结果要均衡考虑错误率与计算量,比如k=3时,错误率为10%,而k=10时,错误率为9%,则需要考虑计算量换来的1%提升是否合算了。
小结
基于kNN原理和调用sklearn中kNN算法算法相比,调用sklearn中kNN算法明显耗时较短。
kNN回归算法
kNN算法主要用于分类预测,但也可以用于回归预测。与分类预测类似,kNN算法用于回归预测时,同样是寻找测试样本的k近邻,然后将这k个样本的目标值求均值,将该均值作为测试样本的预测值:
y
p
r
e
=
1
K
∑
i
=
1
K
y
i
y_{pre} = \frac{1}{K}\sum_{i=1}^{K}{y_i}
ypre=K1i=1∑Kyi
kNN回归算法也可以直接调用sklearn中的包
from sklearn.neighbors import KNeighborsRegressor
回归模型的评价指标
回归模型的评价指标有很多,后续专门一篇来总结。这里介绍常见的三种,分别是: 均方误差(Mean Squared Error,MSE)、 均方根误差( Root Mean Squared Error ,RMSE)或者 平均绝对误差 (Mean Absolute Error,MAE)。
- MSE是指预测值与真实值之差平方的期望值, 具体计算公式如下:
M S E = 1 N ∑ i = 1 N ( y i − y i ^ ) 2 MSE=\frac{1}{N}\sum_{i=1}^{N}{(y_i-\hat{y_i})^2} MSE=N1i=1∑N(yi−yi^)2
- RMSE是均方误差的算术平方根 ,具体计算公式如下:
R M S E = 1 N ∑ i = 1 N ( y i − y i ^ ) 2 RMSE=\sqrt{\frac{1}{N}\sum_{i=1}^{N}{(y_i-\hat{y_i})^2}} RMSE=N1i=1∑N(yi−yi^)2
- MAE是绝对误差的平均值,具体计算公式如下
M A E = 1 N ∑ i = 1 N ∣ y i − y i ^ ∣ MAE=\frac{1}{N}\sum_{i=1}^{N}{|y_i-\hat{y_i}|} MAE=N1i=1∑N∣yi−yi^∣
y i y_i yi为真实值, y ^ i \hat y_i y^i为预测值,N为测试集样本个数。这些指标越小越好。
可以按公式计算,也可直接调用sklearn中的包直接用
from sklearn.metrics import mean_squared_error # MSE
from sklearn.metrics import mean_absolute_error #MAE
# y_test:测试数据集中的真实值,y_predict:根据测试集中的x所预测到的数值
mse_predict = mean_squared_error(y_test, y_predict) # MSE
mae_predict = mean_absolute_error(y_test, y_predict) # MAE
总结
kNN算法的执行效率并不高,因为每个测试样本都要和训练样本计算距离,实际使用会非常耗时。
kNN优缺点
优点:精度高、对异常值不敏感、无数据输入假定
缺点:计算复杂度高、空间复杂度高
适用数据范围:数值型和标称型
相关链接
书籍:《机器学习实战》