代码基本上每一行都写上了注释,帮助理解记忆。
k-近邻算法
章节内容
- k-近邻算法的基本理论;
- 如何使用距离测量的方法分类物品;
- 使用Python从文本文件中导入并解析数据;
- 当存在许多数据来源时,如何避免计算距离时可能碰到的一些常见错误;
- 使用k-近邻算法改进约会网站和手写数字识别系统。
k-近邻算法概述工作原理
训练样本集中每个数据都存在标签,即我们知道样本集中每一数据与所属分类的对应关系。输入没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本集中特征最相似数据(最近邻)的分类标签。一般来说,只选择样本数据集中前k个最相似的数据,通常k是不大于20的整数。最后,选择k个最相似数据中出现次数最多的分类,作为新数据的分类。
例如电影分类,使用kNN确定它是爱情片还是动作片。
表2-1 每部电影的打斗镜头数、接吻镜头数以及电影评估类型
开发机器学习应用的通用步骤
1. 收集数据
2. 准备输入数据, 确保数据格式符合要求,本书采用的格式是Python语言的List。
3. 分析输入数据,确保数据中没有垃圾数据。
4. 训练算法(kNN不需要此步)
5. 测试算法,评估算法,计算错误率。
6. 使用算法
使用k-近邻算法将每组数据划分到某个类中,伪代码如下:
对未知类别属性的数据集中的每个点依次执行以下操作:
(1) 计算已知类别数据集中的点与当前点之间的距离;
(2) 按照距离递增次序排序;
(3) 选取与当前点距离最小的k个点;
(4) 确定前k个点所在类别的出现频率;
(5) 返回前k个点出现频率最高的类别作为当前点的预测分类。
构建分类器——
def classify0(inX, dataSet, labels, k):
#用于分类的输入向量是inX,输入的训练样本集为dataSet,标签向量为labels,最后的参数k表示用于选择最近邻居的数目
dataSetSize = dataSet.shape[0] # shape[0]是读取矩阵第一维度的长度,即数据的条数
# 计算距离
diffMat = tile(inX, (dataSetSize,1)) - dataSet #将目标复制成n行,计算得目标与每个训练数值之间的数值之差。
sqDiffMat = diffMat**2 #各个元素分别平方
sqDistances = sqDiffMat.sum(axis=1) #对应行的平方相加,即得到了距离的平方和
distances = sqDistances**0.5 #开根号,得到距离
#排序,确定前k个距离最小元素所在的主要分类
sortedDistIndicies = distances.argsort() #argsort函数返回的是数组值从小到大的索引值
classCount={}
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]
错误率是常用的评估方法。错误率 = 分类器给出错误结果的次数除以测试执行的总数。
以下测试代码可直接运行——
'''
构造kNN分类器
'''
from numpy import *
import operator #运算符模块
# 创建数据集和标签
group = array([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]])
labels = ['A','A','B','B']
def classify0(inX, dataSet, labels, k):
#用于分类的输入向量是inX,输入的训练样本集为dataSet,标签向量为labels,最后的参数k表示用于选择最近邻居的数目
dataSetSize = dataSet.shape[0] # shape[0]是读取矩阵第一维度的长度,即数据的条数
# 计算距离
diffMat = tile(inX, (dataSetSize,1)) - dataSet #将目标复制成n行,计算得目标与每个训练数值之间的数值之差。
sqDiffMat = diffMat**2 #各个元素分别平方
sqDistances = sqDiffMat.sum(axis=1) #对应行的平方相加,即得到了距离的平方和
distances = sqDistances**0.5 #开根号,得到距离
#排序,确定前k个距离最小元素所在的主要分类
sortedDistIndicies = distances.argsort() #argsort函数返回的是数组值从小到大的索引值
classCount={}
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]
print("该数据的类别是" + classify0([0, 0],group, labels, 3))
以下是本代码中使用的一些(可能小白都知道的)基础函数——
.pyc文件
py文件作为模块被import时,python编译后产生pyc文件,目的是提高解析速度。当有别的程序再次import此模块时,python读入pyc文件即可,无需重新解析py文件。
shape函数
是numpy中的函数,功能是读取矩阵的长度。
shape[0] 读取矩阵第一维度的长度
tile函数
是numpy中的函数,将原矩阵横向、纵向地复制。tile 是瓷砖的意思,顾名思义,这个函数就是把数组像瓷砖一样铺展开来。
tile(mat, (3, 4)) 将矩阵mat复制成3*4块
** 代表乘方,开根号就是 **0.5
axis=1
第0轴沿着行的垂直往下,第1轴沿着列的方向水平延伸。
a = np.sum([[1, 1, 1], [2, 2, 2]], axis=0)
输出[3 3 3],每列之和
a = np.sum([[1, 1, 1], [2, 2, 2]], axis=1)
输出[3 6],每行之和
argsort()
argsort函数返回的是数组值从小到大的索引值。
range()
range()是python的内置函数, 语法:range(start, stop[, step])
range(5)即前5个数, range(0,5)实际上就是索引从0开始到4结束的5个整数。step是步长,默认步长为1。
itemgetter函数
根据某个或某几个字典字段来排序Python列表。
报错 ‘dict’ object has no attribute 'iteritems’
在python3报错,将iteritems改为items即可。iteritems是为python2环境中dict的函数。
示例:使用 k-近邻算法改进约会网站的配对效果**
目标:将约会网站推荐的匹配对象划分到确切的分类中。分类有3种:
• 不喜欢的人
• 魅力一般的人
• 极具魅力的人
数据存放在文本文件datingTestSet.txt中,每个样本数据占据一行,总共有1000行。海伦的样本主要包含以下3种特征:
• 每年获得的飞行常客里程数
• 玩视频游戏所耗时间百分比
• 每周消费的冰淇淋公升数
1. 准备数据:从文本文件中解析数据
from numpy import *
#解析文本文件为Numpy
def file2matrix(filename):
fr = open(filename)
arrayOLines = fr.readlines()
numberOfLines = len(arrayOLines)
# 创建以0填充的numpy矩阵
returnMat = zeros((numberOfLines, 3)) # 生成一个 n*3(n行3列的)的矩阵,各个位置上全是 0
classLabelVector = [] # 返回的分类标签向量
index = 0 # 行的索引值
# 解析文件数据到列表
for line in arrayOLines:
# 按行读取数据,strip()去除首尾的空白符(包括'\n','\r','\t',' ')
line = line.strip()
# 将每一行的字符串根据'\t'分隔符进行切片,获得元素列表
listFromLine = line.split('\t')
# 选取前3个元素,存放到returnMat的NumPy矩阵中,也就是特征矩阵
returnMat[index, :] = listFromLine[0:3]
# -1表示最后一列,最后一列是类别,将其存储到向量classLabelVector中。
# 要告诉解释器存储的元素值为整型,否则会当作字符串处理
classLabelVector.append(int(listFromLine[-1]))
index += 1
# 返回特征矩阵和分类标签向量
return returnMat, classLabelVector
datingDataMat,datingLabels=file2matrix('datingTestSet2.txt')
print(datingLabels[0:20])
期间报错:invalid literal for int() with base 10: ‘largeDoses’
错误语句:classLabelVector.append(int(listFromLine[-1]))
使用 int 对一个字符类型的数据进行强制类型转换时,要求输入的字符类型只能为整数,不能为浮点数或其他。
把数据文件datingTestSet.tx改为datingTestSet2.txt即可, 这两个文件除了最后一列其他数据完全一致。前者最末列是字符串, 后者最末列是整数。
2. 分析数据:使用 Matplotlib 创建散点图
ax=fig.add_subplot(111)
这些是作为单个整数编码的子绘图网格参数。例如,“111”表示“1×1网格,第一子图”,“234”表示“2×3网格,第四子图”。
fig = plt.figure()
ax = fig.add_subplot(111)
#以datingDataMat矩阵的第一(飞行常客例程)、第二列(玩游戏)数据画散点数据
#ax.scatter(datingDataMat[:,1],datingDataMat[:,2])
#上一行画的图无颜色区分,
#可利用变量datingLabels存储的类标签属性,在散点图上绘制了色彩不等、尺寸不同的点
ax.scatter(datingDataMat[:,1],datingDataMat[:,2],15.0*array(datingLabels), 15.0*array(datingLabels))
plt.show()
使用matplotlib的字体管理器指定字体文件
plt.rcParams['font.sans-serif']=['SimHei'] #指定默认字体 SimHei为黑体
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号
常用字体:
黑体 SimHei 微软雅黑 Microsoft YaHei
微软正黑体 Microsoft JhengHei 新宋体 NSimSun
新细明体 PMingLiU 细明体 MingLiU
标楷体 DFKai-SB 仿宋 FangSong
楷体 KaiTi 仿宋_GB2312 FangSong_GB2312
楷体_GB2312 KaiTi_GB2312
设置散点颜色
可以利用变量datingLabels存储的类标签属性,在散点图上绘制了色彩不等、尺寸不同的点
ax.scatter(datingDataMat[:,1],datingDataMat[:,2],15.0*array(datingLabels), 15.0*array(datingLabels))
也可以自定义颜色
LabelsColors = []
for i in datingLabels:
# 为每个类别分别设置颜色
if i == 1:
LabelsColors.append('black')
if i == 2:
LabelsColors.append('orange')
if i == 3:
LabelsColors.append('red')
#散点大小为15,透明度为0.5
ax.scatter(datingDataMat[:,1], datingDataMat[:,2], color=LabelsColors, s=15, alpha=.5)
3. 准备数据:归一化数值
由于提取的特征数值范围大小不一,比如有的数值是1~100,有的是10000以上,那么在计算距离的时候,前者的影响作用远远小于后者。但每一种特征同等重要,因此我们需要将数值归一化,
如将取值范围处理为0到1或者-1到1之间。
# 数值归一化
def autoNorm(dataSet):
# 归一化公式:Y = (X-Xmin)/(Xmax-Xmin)
# min和max是1*3的矩阵,存着每一个特征的最值
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
4. 测试算法:验证分类器
计算错误率。
#验证分类器,计算错误率
def datingClassTest():
hoRatio = 0.10 # 取数据集中的10%
datingDataMat, datingLabels = file2matrix('datingTestSet2.txt')
drawing(datingLabels, datingDataMat)
# 数据归一化,返回归一化后的矩阵,数据范围,数据最小值
normMat, ranges, minVals = autoNorm(datingDataMat)
m = normMat.shape[0] # 获得normMat的行数
numTestVecs = int(m*hoRatio) #10%的数量
errorCount = 0.0
for i in range(numTestVecs):
# 前numTestVecs个数据作为测试集,后m-numTestVecs个数据作为训练集
classifierResult = classify0(normMat[i, :], normMat[numTestVecs:m, :],
datingLabels[numTestVecs:m], 3)
print("预测数值为 %d, 实际数值为: %d"
% (classifierResult, datingLabels[i]))
if classifierResult != datingLabels[i]:
errorCount += 1.0
print("错误率为 %f" % (errorCount/float(numTestVecs)))
5. 构建完整可用系统
写一段交互程序,使海伦可以输入某人的信息,程序输出她对对方喜欢程度的预测值。
#预测
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('datingTestSet2.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分类器识别出数字0到9,需要识别的数字已经使用图形处理软件,处理成具有相同的色彩和大小:宽高是32像素×32像素的黑白图像。
目录trainingDigits中包含了大约2000个例子,每个例子的内容如图2-6所示,每个数字大约有200个样本;目录testDigits中包含了大约900个测试数据。
尽管采用文本格式存储图像不能有效地利用内存空间,但是为了方便理解,图像将转换为文本格式。
使用的kNN分类器未改变,增加了二进制矩阵转化为向量的函数以及导入数据并验证的函数。
# 将图像文本数据转换为向量
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():
# 1. 导入训练数据
hwLabels = []
trainingFileList = listdir('trainingDigits') # load the training set
m = len(trainingFileList)
trainingMat = zeros((m, 1024))
# hwLabels存储0~9对应的index位置, trainingMat存放的每个位置对应的图片向量
for i in range(m):
fileNameStr = trainingFileList[i]
fileStr = fileNameStr.split('.')[0] # take off .txt
classNumStr = int(fileStr.split('_')[0])
hwLabels.append(classNumStr)
# 将 32*32的矩阵->1*1024的矩阵
trainingMat[i, :] = img2vector('trainingDigits/%s' % fileNameStr)
# 2. 导入测试数据
testFileList = listdir('testDigits') # iterate through the test set
errorCount = 0.0
mTest = len(testFileList)
for i in range(mTest):
fileNameStr = testFileList[i]
fileStr = fileNameStr.split('.')[0] # take off .txt
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)))
总结
k-近邻算法是分类数据最简单最有效的算法。它必须保存全部数据集,如果训练数据集的很大,会占用大量的存储空间。此外,由于必须对数据集中的每个数据计算距离值,实际使用时可能非常耗时。
k-近邻算法的另一个缺陷是它无法给出任何数据的基础结构信息,因此我们也无法知晓平均
实例样本和典型实例样本具有什么特征。下一章(第3章 决策树)我们将使用概率测量方法处理分类问题,该算法可以解决这个问题。