《ML in Action》笔记(1) —— kNN分类器
-
本章有三个案例,分别是:
(1) 根据打斗镜头和接吻镜头数量评估电影类型;
(2) 根据飞行常客里程数等3个参数预判约会网站的配对效果。
(3) 利用训练数据集的手写数字识别 -
kNN(k-近邻)算法的实现相对简单:
- 计算当前点与训练数据集中每个点的距离
- 选取与当前点最近的k个点;
- 统计前k个点的类别频率(选举)
- 取统计频率最高的那个类别,作为对当前点的分类判别。
-
以下是代码笔记,原书代码为PYTHON2,经学习消化,改为PYTHON3.5可运行版本。
【kNN.py: part1 —— 引入库文件】
from numpy import *
import operator
from os import listdir
- 从这第一个最简单的案例起,就严重依赖numpy数组的特性,盖因其便捷的矢量化运算(操作符默认对数组内每一个元素进行批量运算)。
【kNN.py: part2 —— 分类器核心函数classify0()
】
classify0()
函数是实现kNN算法的核心代码。输入参数inX
是待分类的当前值,dataSet
是训练数据集,labels
是训练集的分类标签,k
是k近邻的取值。函数输出对inX
的分类判定结果。
def classify0(inX, dataSet, labels, k):
# 训练集记录数
dataSetSize = dataSet.shape[0]
# 计算数据与训练集的差值
diffMat = tile(inX, (dataSetSize,1)) - dataSet
# 计算差值的平方
sqDiffMat = diffMat**2
# 按记录行求和,得到平方差
sqDistances = sqDiffMat.sum(axis=1)
# 开根求得欧式距离
distances = sqDistances**0.5
# 按距离升序排列
sortedDistIndicies = distances.argsort()
classCount={}
# 选取TOP K个近邻样本
for i in range(k):
# TOP K对应样本的分类标签
voteIlabel = labels[sortedDistIndicies[i]]
# 针对某个样本分类标签的投票计数
classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
# 原书代码此处用iteritems(),PYTHON3的DICT已不支持,改用items()和lambda
#sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
sortedClassCount = sorted(classCount.items(), key=lambda item:item[1], reverse=True)
return sortedClassCount[0][0]
items()
表示取出DICT的键值对(keys()
,values()
)。lambda表示取数模式:lambda item:item[1]
按数值排序,lambda item:item[0]
按键值排序,常用于sorted()
函数。
【kNN.py: part3 —— 构建案例一的简单训练集】
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
【IPYTHON —— 运行案例一】
# 生成训练集数据
group,labels = kNN.createDataSet()
# 运行分类器,对样本点[0.3,0.4]进行判定
rst = kNN.classify0([0.3,0.4],group,labels,3)
【kNN.py: part4 —— 案例二用到的其他函数】
# 加载包含训练集数据的文本文件
def file2matrix(filename):
# 打开文件
fr = open(filename)
# 读取文件内容(一次读所有行)
arrayOLines = fr.readlines()
# 获取记录行数
numberOfLines = len(arrayOLines)
# 初始化用于存储训练集数据的数组
returnMat = zeros((numberOfLines,3))
classLabelVector = []
index = 0
# 遍历行
for line in arrayOLines:
# 去除行末回车符
line = line.strip()
# 按tab分列
listFromLine = line.split('\t')
# 存入该行记录的3个参数。
returnMat[index,:] = listFromLine[0:3]
# 将该行记录对应的标签结果存入标签数组
classLabelVector.append(int(listFromLine[-1]))
index += 1
# 返回训练集参数数组和标签数组
return returnMat,classLabelVector
# 数据归一化 (x-min)/(max-min)
def autoNorm(dataSet):
# 按列取最小值
minVals = dataSet.min(0)
# 按列取最大值
maxVals = dataSet.max(0)
ranges = maxVals - minVals
normDataSet = zeros(shape(dataSet))
# 取记录数
m = dataSet.shape[0]
# 分子 (x-min)
normDataSet = dataSet - tile(minVals, (m,1))
# 计算归一化数值
normDataSet = normDataSet/tile(ranges, (m,1))
return normDataSet, ranges, minVals
# 算法准确率测试
def datingClassTest():
# 训练样本比例
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)))
print (errorCount)
- 第一个函数
file2matrix
主要演示了如何加载文本数据到Numpy数组。其中倒数4行,原代码使用了listFromLine[0:3]
,Numpy数组的下表是“左方右圆”括号,实际只取出3个数。 - 第二个函数
autoNorm
对数据进行了归一化。参数点的“尺度”对距离的计算结果影响很大,从而影响分类器的结果。本案例采用了最简单的一种归一化方法。在实际运用中,归一化的方法应该是与样本数据本身的分布密切相关的。 - 第三个函数
datingClassTest()
演示了用训练数据测试分类器准确性的过程。
【kNN.py: part5—— 案例二的实际运用】
# 根据输入的样本参数,输出分类器结果
def classifyPerson():
resultList = ['not at all','in small doses','in large doses']
# 以此读取用户输入的三个参数。
# 原代码此处使用raw_input(),Python3 不再支持,改作input()
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])
【kNN.py: part6—— 案例三的函数代码】
# 将二进制图形向量转为一维向量
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
# 批量运行手写数字识别的程序函数
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')
errorCount = 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): errorCount += 1.0
print ("\nthe total number of errors is: %d" % errorCount)
print ("\nthe total error rate is: %f" % (errorCount/float(mTest)))
- 本案例省略了将数字图形转化为二进制向量的过程,算法的模型输入是1024位长的2进制数组。随后可直接运用
classify0()
函数进行分类。
算法小结
- 这两个案例调用了同一个kNN函数
classify0()
。kNN算法不存在一个经训练生成后可以直接“调用”的参数模型,而是每次运行时都要计算当前点与所有点的距离,并在k-近邻中进行分类选举。 - 距离的定义和数据的归一化方法是与业务和参数本身高度相关的,不同的选择肯定会影响分类器的结果和准确率,后续可以亲自测试一下。
PYTHON小结
- numPy数组的基本用法
- 读取文本文件到numPy的方法
- sorted排序函数的用法。
sorted(classCount.items(), key=lambda item:item[1], reverse=True)