k-近邻算法采用测量不同特征值之间的距离方法进行分类,它的简单工作原理是:存在一个样本数据集合,也称作训练样本集,并且样本集合中每个数据都存在标签,即我们知道样本集中每一数据所属分类的对应关系。输入没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本集中特征最相似数据(最近邻)的分类标签。一般来说,我们只选择样本数据集中前k个最相似的数据,k通常是不大于20的整数。最后,选择k个最相似数据中出现次数最多的分类,作为新数据的分类。
优点:精度高、对异常值不敏感、无数据输入假定。
缺点:计算复杂度高、空间复杂度高。
适用数据范围:数值型和标称型。
使用k-近邻算法将每组数据划分到某个类中,伪代码如下。
对未知类别属性的数据集中的每个点依次执行以下操作:
(1)计算已知类别数据集中点与当前点之间的距离;
(2)按照距离递增次序排序;
(3)选择与当前点距离最小的k个点;
(4)确定前k个点所在类别的出现频率;
(5)返回前k个点出现频率最高的类别作为当前点的预测分类。
对应的代码如下:
def classify0(intX, dataSet, labels, k):
dataSetSize = dataSet.shape[0]
#计算距离,tile得到(dataSetSize行重复inxX, 列重复1次intX)
diffMat = tile(intX, (dataSetSize, 1)) - dataSet
sqDiffMat = diffMat ** 2
#axis = 1 表示对行求和
sqDistances = sqDiffMat.sum(axis = 1)
distances = sqDistances ** 0.5
#argsort返回数值从小到大的索引值
sortedDistIndicies = distances.argsort()
#选择距离最小的k个点, classCount字典保存标签出现频率
classCount = {}
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
#排序,返回k邻近中出现频次最大的标签
sortedClassCount = sorted(classCount.items(), key = operator.itemgetter(1), reverse = True)
return sortedClassCount[0][0]
其中,
inX
是用于分类的输入向量,dataSet
是输入的训练样本集,labels
为标签向量,参数k
表示用于选择最近邻居的数目。代码中求的是欧氏距离。如求两个向量点xA和xB之间的欧式距离有如下公式
d = ( x A 0 − x B 0 ) 2 + ( x A 1 − x B 1 ) 2 d=\sqrt{(xA_0 - xB_0)^2+(xA_1-xB_1)^2} d=(xA0−xB0)2+(xA1−xB1)2
from numpy import *
import operator
def classify0(intX, dataSet, labels, k):
#···
if __name__ == '__main__':
group = array([[1.0, 1.1], [1.0, 1.0], [0.5, 0.3], [0.3, 0.1]])
labels = ['A', 'A', 'B', 'B']
res = classify0([0, 0], group, labels, 3)
print(res)
调用上述代码,可知输出结果,与点**[0, 0]**最相近的标签为B。
数据集见https://github.com/pbharrin/machinelearninginaction中第二章节Ch02
海伦在约会网站上与交往过的人通过总结,她发现曾经交往过三种类型的人:
此外海伦还收集了网站未曾记录的数据信息,她认为这些数据更有助于匹配对象的归类。海伦收集的约会样本主要包含以下3种特征(datingTestSet.txt)
def file2matrix(filename):
"""
约会网站数据导入矩阵、标签向量中
:param filename:
:return:
"""
love_dictionary = {'largeDoses':3, 'smallDoses':2, 'didntLike':1}
fr = open(filename)
arrayOLines = fr.readlines()
#得到文件行数,即数据条数
numberOfLines = len(arrayOLines)
returnMat = zeros((numberOfLines, 3))
classLabelVector = []
index = 0
for line in arrayOLines:
line = line.strip()
listFromLine = line.split('\t')
returnMat[index, :] = listFromLine[0:3]
if(listFromLine[-1].isdigit()):
classLabelVector.append(int(listFromLine[-1]))
else:
classLabelVector.append(love_dictionary.get(listFromLine[-1]))
index += 1
return returnMat, classLabelVector
使用matplotlib绘图,导入包并在主程序中进行数据处理的调用
import matplotlib.pyplot as plt
if __name__ == '__main__':
datingDataMat, datingLabels = file2matrix('datingTestSet.txt')
fig = plt.figure()
ax = fig.add_subplot(111)
#ax.scatter(datingDataMat[:, 1], datingDataMat[:, 2])
ax.scatter(datingDataMat[:, 1], datingDataMat[:, 2], 15.0 * array(datingLabels), 15.0 * array(datingLabels))
plt.xlabel('Percentage of Time Spent Playing Video Games')
plt.ylabel('Liters of Ice Cream Consumed Per Week')
plt.show()
根据==”玩游戏所耗时间百分比“和“每周消费的冰淇淋公升数”==这两个维度,以及向量所对应的标签绘制出的散点图如下所示。虽然能够比较容易地区分数据点从属类别,但依然很难根据这张图得出结论性信息。
根据==”每年获得的飞行常客里程数“和“玩视频游戏所耗时间百分比”==这两个维度,以及向量所对应的标签绘制出的散点图如下所示。这两个特征更容易区分数据点从属的类别。
ax.scatter(datingDataMat[:, 0], datingDataMat[:, 1], 15.0 * array(datingLabels), 15.0 * array(datingLabels))
plt.xlabel('Number of flight mileage obtained per year')
plt.ylabel('Percentage of Time Spent Playing Video Games')
数值归一化使得每个特征享有相同的权重,若不归一化,则里程数的大小会直接影响欧式距离的大小,数据小的影响就特别微小了。
def autoNorm(dataSet):
#min、max中参数0使得函数可以从列中选取最大或最小值(三个维度,取每一列的最大最小值)
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
ranges = maxVals - minVals
normDataSet = zeros(shape(dataSet))
m = dataSet.shape[0]
normDataSet = dataSet - tile(minVals, (m, 1))
normDataSet = normDataSet / tile(ranges, (m, 1))
return normDataSet, ranges, minVals
分类器针对约会网站的测试代码,选择10%的测试数据
def datingClassTest():
#选择10%的测试数据
hoRatio = 0.10
datingDataMat, datingLabels = file2matrix('datingTestSet.txt')
normMat, ranges, minVals = autoNorm(datingDataMat)
m = normMat.shape[0]
numTestVecs = int(m * hoRatio)
errorCount = 0.0
for i in range(numTestVecs):
classifierResult = classify0(normMat[i, :], normMat[numTestVecs : m, :], datingLabels[numTestVecs : m], 3)
print("the classifier came back with: %d, the real answer is: %d" % (classifierResult, datingLabels[i]))
if (classifierResult != datingLabels[i]):
errorCount += 1.0
print("the total error rate is: %f" % (errorCount / float(numTestVecs)))
调用此函数,得到的输出结果如下所示:
the classifier came back with: 3, the real answer is: 3
the classifier came back with: 2, the real answer is: 2
the classifier came back with: 1, the real answer is: 1
·····
the classifier came back with: 3, the real answer is: 1
the classifier came back with: 3, the real answer is: 3
the total error rate is: 0.050000
分类器处理约会数据集的错误率是5%。可以改变函数datingClassTest内变量hoRatio和变量k的值,检测错误率是否随着变量值的变化而增加。依赖于分类算法、数据集合程序设置,分类器的输出结果可能有很大的不同。
通过海伦在约会网站上找到的某个人并输入他的信息。程序会给出她对对方喜欢程度的预测值。
约会网站测试函数
def classifyPerson():
resultList = ['not at all', 'in small doses', 'in large doses']
percentTats = float(input("percentage of time spent playing video games?"))
ffMiles = float(input("frequent flier miles earned per year?"))
iceCream = float(input("liters of ice cream consumed per year?"))
datingDataMat, datingLabels = file2matrix('datingTestSet.txt')
normMat, ranges, minVals = autoNorm(datingDataMat)
inArr = array([ffMiles, percentTats, iceCream])
classifierResult = classify0((inArr - minVals) / ranges, normMat, datingLabels, 3)
print("You will probably like this person: ", resultList[classifierResult - 1])
调用并测试结果如下
手写数字数据集的例子如下图
为了简单起见,这里构造的系统只能识别数字0到9。数据集是采用二进制的文本数据来存储图像,这样便于理解和处理。
目录trainingDigits中包含了大约2000个例子,每个数字大约有200个样本;目录testDigits中包含了大约900个测试数据。拿出其中一个为例,其二进制文本表示如下(数字4):
00000000000000011110000000000000
00000000000000011111000000000000
00000000000000011111000000000000
00000000000000111110000000000000
00000000000000111110000000000000
00000000000001111100000001100000
00000000000011111000000111100000
00000000000011111000000111100000
00000000000011111000001111110000
00000000000111100000011111000000
00000000001111100000011111000000
00000000001111100000011111000000
00000000011111000000111110000000
00000000111110000000111110000000
00000000111110000000111110000000
00000011111000000001111101000000
00000011111000000001111111100000
00000011111000001111111111100000
00001111111111111111111111000000
00001111111111111111111111000000
00001111111111111111111111000000
00001111111111111111111100000000
00001111111111111111111000000000
00000111111111111111100000000000
00000111111000011111000000000000
00000000000000011111000000000000
00000000000000011111000000000000
00000000000000111110000000000000
00000000000000011111000000000000
00000000000000011111000000000000
00000000000000111110000000000000
00000000000000111100000000000000
为了使用前面两个例子的分类器,必须将图像格式化处理为一个向量。我们将把一个32x32的二进制图像矩阵转换成1x1024的向量,这样前两节使用的分类器就可以处理数字图像信息了。
def img2vector(filename):
returnVect = zeros((1, 1024))
fr = open(filename)
for i in range(32):
lineStr = fr.readline()
for j in range(32):
returnVect[0, 32 * i + j] = int(lineStr[j])
return returnVect
导入模块添加代码:from os import listdir
,函数listdir可以列出给定目录的文件名
def handwritingClassTest():
hwLabels = []
trainingFileList = listdir('trainingDigits')
m = len(trainingFileList)
trainingMat = zeros((m, 1024))
#从每一个文件中解析数字
for i in range(m):
fileNameStr = trainingFileList[i]
fileStr = fileNameStr.split('.')[0]
#获取数字标签
classNumStr = int(fileStr.split('_')[0])
hwLabels.append(classNumStr)
trainingMat[i, :] = img2vector('trainingDigits/%s' % fileNameStr)
testFileList = listdir('testDigits')
errCount = 0.0
mTest = len(testFileList)
for i in range(mTest):
fileNameStr = testFileList[i]
fileStr = fileNameStr.split('.')[0]
classNumStr = int(fileStr.split('_')[0])
vectorUnderTest = img2vector('testDigits/%s' % fileNameStr)
classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3)
print("the classifier came back with: %d, the real answer is: %d" % (classifierResult, classNumStr))
if (classifierResult != classNumStr):
errCount += 1.0
print("the total number of errors is: %d" % errCount)
print("the total error rate os: %f" % (errCount / float(mTest)))
调用输出结果如下:
the classifier came back with: 0, the real answer is: 0
the classifier came back with: 0, the real answer is: 0
···
the classifier came back with: 5, the real answer is: 5
the classifier came back with: 3, the real answer is: 5
the classifier came back with: 6, the real answer is: 5
···
the classifier came back with: 9, the real answer is: 9
the classifier came back with: 9, the real answer is: 9
the total number of errors is: 10
the total error rate os: 0.010571
k-近邻法识别手写数字数据集,错误率为1.056%,改变遍历k的值,修改函数handwritingClassTest随机选取训练样本、改变训练样本的数目,都会对k-近邻算法的错误率产生影响,感兴趣的话可以改变这些变量值,观察错误率的变化。
实际使用这个算法时,算法的执行效率并不高。因为算法要为每个测试向量做2000次距离计算,每个距离计算包括了1024个维度浮点运算,总计要执行900次,此外,我们还需要为向量准备2MB的存储空间。k决策树是k-近邻算法的优化版,可以节省大量的计算开销。
k-近邻算法是分类数据最有效的算法。k-近邻算法是基于实例的学习,使用算法时我们必须有接近实际数据的训练样本数据。k-近邻算法必须保存全部数据集,如果训练的数据集很大,必须使用大量的存储空间。此外,由于必须对数据集中的每个数据计算距离值,实际使用时可能非常耗时。
k-近邻算法的另一个缺陷是它无法给出任何数据的基础结构信息,因此我们也无法知晓平均示例样本和典型实例样本具有什么特征。
数据集见—路径指南>Ch02
《机器学习实战》[美]Peter Harrington,李锐、李鹏等译
numpy.argsort()函数详解
NumPy教程
Python numpy函数:zeros()、ones()、empty()