k-近邻算法
所谓近朱者赤近墨者黑,有这么一种说法,我们只需要看一个人的朋友就能判断这个人怎么样。
k-近邻算法的思想与此类似,我们只需要计算出给定点附近哪一种标签最多,即可据此判断该点属于哪个类。
核心代码
上代码:
from numpy import *
import operator
# 创建数据集和标签
def createDataSet():
group = array([[1.0, 1.1], [1.0, 1.0], [0., 0.], [0., 0.1]])
labels = ['A', 'A', 'B', 'B']
return group, labels
def classify0(inX, dataSet, labels, k):
# 获取数据集的行数
dataSetSize = dataSet.shape[0]
# 将输入向量扩展成与数据集相同的维度
tmp = tile(inX, (dataSetSize, 1))
diffMat = tmp - dataSet
# 计算距离
sqDiffMat = diffMat ** 2
# 每一行的距离相加
sqDistances = sqDiffMat.sum(axis=1)
# 开方
distances = sqDistances ** 0.5
# 按照升序排序
sortedDistIndicies = distances.argsort()
# 创建一个字典
classCount = {}
# 选择距离最小的k个点
for i in range(k):
# 获取标签
voteIlabel = labels[sortedDistIndicies[i]]
# 统计标签的数量
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
# 按照标签数量降序排序
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
# 返回数量最多的标签
return sortedClassCount[0][0]
group, labels = createDataSet()
print(classify0([0.0, 0.0], group, labels, 3))
在这里,我们的classify0()接收了四个参数:[0.0, 0.0], group, labels, 3。其中group和labels是训练用的数据,由createDataSet()给出。我们想要知道[0.0, 0.0]属于哪个类,然后呢,我们就看这个点附近的3个坐标属于哪个类,这就是[0.0, 0.0]和3的由来。输出结果如是B,此处不再展示,如果我们修改坐标为[1.0, 1.0]可以得到结果A。这是由于我们所求的坐标点改变了所导致的。
预处理以及示例代码
此处只是最简单的示例,如果要处理真正的数据的话,我们还需要经过一些必要的预处理:
- 读取数据并转换为合适的格式
- 归一化处理
第一步很好理解,没有合适的格式我们就无法处理。第二步的原因则是为了保证每一种特征对最后结果的影响都是一致的。比方说每个人每年可能只能吃几公斤冰激凌,但是却有可能飞几百公里飞机。这种差距有可能会导致飞行里程占主导因素,这是我们不希望看到的。
下面是这两步的示例代码,数据集采用了随书的数据集。
代码如下:
# 将文本记录转换为NumPy的解析程序
def file2matrix(filename):
# 打开文件
fr = open(filename)
# 读取文件所有内容
arrayOLines = fr.readlines()
# 获取文件行数
numberOfLines = len(arrayOLines)
# 创建一个numberOfLines行,3列的零矩阵
returnMat = zeros((numberOfLines, 3))
# 创建一个长度为numberOfLines的空列表
classLabelVector = []
# 行索引
index = 0
# 遍历文件中的每一行
for line in arrayOLines:
# 去掉每一行的空格
line = line.strip()
# 将每一行的内容以\t分割
listFromLine = line.split('\t')
# 将前三列的数据放到returnMat矩阵中
returnMat[index, :] = listFromLine[0:3]
# 将最后一列的数据放到classLabelVector列表中
classLabelVector.append(int(listFromLine[-1]))
# 行索引加1
index += 1
# 返回数据矩阵和标签列表
return returnMat, classLabelVector
# 归一化特征值
def autoNorm(dataSet):
# 获取每一列的最小值
minVals = dataSet.min(0)
# 获取每一列的最大值
maxVals = dataSet.max(0)
# 最大值和最小值的范围
ranges = maxVals - minVals
# 创建一个与dataSet同shape的零矩阵
normDataSet = zeros(shape(dataSet))
# 获取dataSet的行数
m = dataSet.shape[0]
# 将最小值扩展成与dataSet同shape的矩阵
normDataSet = dataSet - tile(minVals, (m, 1))
# 将最小值扩展成与dataSet同shape的矩阵
normDataSet = normDataSet / tile(ranges, (m, 1))
# 返回归一化后的矩阵,数据范围,最小值
return normDataSet, ranges, minVals
此时,我们所需要的组件已经足够全面了,剩下的就是调用组件来获取最后结果,此处我们写一个测试函数:
# 测试分类器
def datingClassTest():
# 测试集比例
hoRatio = 0.10
# 从文件中读取数据
datingDataMat, datingLabels = file2matrix('datingTestSet2.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]))
# 如果预测结果不等于真实结果,错误计数加1
if classifierResult != datingLabels[i]:
errorCount += 1.0
# 打印错误率
print("the total error rate is: %f" % (errorCount / float(numTestVecs)))
最终输出结果为5%的错误率,结果还不错。有错误的原因是,总有一些特殊的数据不符合分布,而这种错误只能尽量降低而无法完全避免。
手写识别
书上采用了由01文本组成的数字,此处同样采取一样的方案,数据集为随书附赠的数据集。
由于均是k-近邻算法,因此此处的核心代码与前面的相同,只是预处理这里有些变化:
def image2vector(filename):
# 创建一个1行1024列的零矩阵
returnVect = zeros((1, 1024))
# 打开文件
fr = open(filename)
# 遍历文件中的每一行
for i in range(32):
# 读取文件的一行
lineStr = fr.readline()
# 遍历每一行的每一列
for j in range(32):
# 将每一行的前32个字符依次放到returnVect中
returnVect[0, 32 * i + j] = int(lineStr[j])
# 返回returnVect
return returnVect
由于数字是3232的大小,但是核心代码只能处理一维的数据,因此此处我们要先将3232的矩阵转化为1*1024的格式。然后再丢到模型里去跑,示例代码如下:
def handwritingClassTest():
hwLabels = []
# 获取目录内容
trainingFileList = listdir('trainingDigits')
# 获取目录内容的长度
m = len(trainingFileList)
# 创建一个m行1024列的零矩阵
trainingMat = zeros((m, 1024))
# 遍历目录内容
for i in range(m):
# 获取文件名
fileNameStr = trainingFileList[i]
# 获取文件名中的数字
classNumber = int(fileNameStr.split('_')[0])
# 将数字添加到hwLabels中
hwLabels.append(classNumber)
# 将文件内容转换为向量
trainingMat[i, :] = image2vector('trainingDigits/%s' % fileNameStr)
# 获取测试目录内容
testFileList = listdir('testDigits')
# 错误数
errorCount = 0.0
# 测试数据的数量
mTest = len(testFileList)
# 遍历测试目录内容
for i in range(mTest):
# 获取文件名
fileNameStr = testFileList[i]
# 获取文件名中的数字
classNumber = int(fileNameStr.split('_')[0])
# 将文件内容转换为向量
vectorUnderTest = image2vector('testDigits/%s' % fileNameStr)
# 获取分类结果
classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 7)
# 打印KNN算法分类结果和真实的分类
print("the classifier came back with: %d, the real answer is: %d" % (classifierResult, classNumber))
# 如果不相等,错误数加1
if classifierResult != classNumber:
errorCount += 1.0
# 打印错误率
print("\nthe total number of errors is: %d" % errorCount)
print("\nthe total error rate is: %f" % (errorCount / float(mTest)))
那么,我们要如何理解这段代码呢?与前面的方法相同,根据距离最近的几个点的分类来判断当前点是属于哪一类。虽然我们只有01两个值,但是我们有1024个维度,因此还是能比较好的进行分类的。运行结果如下:
错误率为2%,可见效果还是很不错的。
上述代码我们选择了附近7个点最多的类,但是当我们将值改为3个之后,准确率反而有所提升(如下图所示),针对这个问题,我猜测是因为我们的数据量不够所造成的,如果数据量足够多的话,那么最近的7个点应该会使得结果更准确才对。为了验证这个猜想,我们将数据量减少一半,并仍然选择最近3个点重新进行了测试
下图是将数据量改为原本一半的运行结果,可以看到错误率有着显著提高,因此猜想正确。要想提高手写识别准确率可以考虑增加样本数量来实现。
小结
其实思路非常朴素,就是看跟哪个最相似。但是不得不说这本书写的还是蛮好的,初学者不需要对概念了解的太清晰。精准的定义应该再学完一遍后回头再来记住,这样子学习效率会高很多。