着手学习机器学习了,打算根据《机器学习实战》这本书从头到尾把这书里的代码撸个遍,想必就应该能入门了吧。写笔记目的一是记录,二是加深记忆,三是分享。cd尽量减少摘抄性文字。
书中的所有代码来源可到官网下载:https://www.manning.com/books/machine-learning-in-action
这里博客的代码及相关数据放在了cd的github上:https://github.com/ChenDdon/mechine_learning_practice.git
k-近邻算法
文章目录
一 定义、优缺点及工作原理
定义:k-近邻算法采用测量不同特征值的距离方法进行分类。
优点:进度稿、对异常值不敏感、无数据输入假定。
缺点:计算复杂度高、空间复杂度高、
适用数据范围:数值型和标称型。
工作原理:
存在一个样本数据集合,也称作为训练样本集,并且样本集中每个数据都存在标签,即我们知道样本集中每一个数据与所属分类的对应关系。
输入没有标签的新数据后,将新的数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本最相似数据(最近邻)的分类标签。一般来说,我们只选择样本数据集中前k个最相似的数据,这就是k-近邻算法中k的出处,通常k是不大于20的整数,且最好为奇数(涉及到判定分类时的投票过程)。
最后,选择k个最相似数据中出现次数最多的分类,作为新数据的分类。
二 k-近邻算法实践
2.1 k-近邻算法的一般流程
1、收集数据:任意方法。
2、准备数据:距离计算所需要的数值,最好是格式化的数据格式。
3、分析数据:可以使用任何方法。
4、测试算法:计算错误率。
5、使用算法:首先输入样本数据和结构化的输出结果,然后进行k-近邻算法判定输入数据分别属于哪个分类,最后应用对计算出的分类执行后续处理。
2.2 示例说明(任务说明)
这里cd主要基于书本中给的使用k-近邻算法改进约会网站的配对结果这个示例来写的。
示例背景: 海伦一直用一个约会平台在找对象。海伦将约会的众多对象分成了三类:
- didntLike;
- smallDoses;
- largeDoses
然后,海伦将之前每个约会对象数据记录下来,每个人主要是三个数据:
- 玩视频游戏所耗时间百分比
- 每年获取的飞行常客里程数
- 每周消费的冰淇淋公升数
海伦将每个人的数据以及分类整理在了datingTestSet.txt这个文件里了。希望能有个分类程序,每当她拿到一个没约会过的人的前述三个数据,就能知道这个人她会不会喜欢。
给出的数据:datingTestSet.txt与datingTestSet2.txt这两个文件。其中我们要用的是datingTestSet2.txt这个文件与datingTestSet.txt的区别就在于,数据文件中的第四列数值做了如下转换:
第四列数值意义 |
---|
largeDoses ——> 3 |
smallDoses ——> 2 |
didntLike ——> 1 |
任务:得到一个基于k-近邻算法的分类器,对一行新的向量能进行正确的分类判定。
分类器需要达到的效果,即:input为1*4的向量;output为相应的分类结果。
2.3 代码实践
首先,创建名为kNN.py的python文件。
然后,要清楚我们要做哪些事情,需要哪些功能,从而确定框架。
#1收集数据
#这里提供的datingTestSet2.txt文件就是收集到的数据
#准备数据:将收集的到的数据转换成格式化的数据格式
def img2matrix():
pass
#分析数据,可以用matplotlib创建可视化的图形
def analyseShow():
pass
#分类器构建
def classify0():
pass
#测试算法部分
def datingClassTest():
pass
#使用算法部分
def classifyPerson():
pass
2.3.1 收集数据
因为这个例子里的数据是准备好了的,所以直接拿来用就行了。代码和数据都同意放在文末的连接里吧。
2.3.2 准备数据
准备数据其实也是为了后续处理方便,我们首先看一下这个例子中的数据特征:
40920 8.326976 0.953952 3
14488 7.153469 1.673904 2
26052 1.441871 0.805124 1
75136 13.147394 0.428964 1
38344 1.669788 0.134296 1
72993 10.141740 1.032955 1
35948 6.830792 1.213192 3
... ... ... ...
这里罗列的数据前几行,前三列是三个特征分别是:每年获得的飞行常客里程数;玩视频游戏所耗时间百分比;每周消费的冰淇淋公升数。
第四列是分类结果,用数字1,2,3表示三种结果。
那么可以将特征数据,与分类结果数据分开来,代码如下:
import numpy as np
def file2matrix(filename):
with open(filename) as fr:
lines = fr.readlines()
numberOfLines = len(lines) #获取文件行数
returnMat = np.zeros((numberOfLines,3)) #准备回传的矩阵-零矩阵
classLabelVector = [] #准备返回的结果数值-空list
index = 0
for line in lines:
line = line.strip()
listFromLine = line.split('\t')
returnMat[index,:] = listFromLine[0:3]
classLabelVector.append(int(listFromLine[-1]))
index += 1
return returnMat,classLabelVector
结果如以下截图所示:
此外,我们可以看到三个特征属性的数值存在巨大差异,那么在计算两个样本之间的差距时,数值大的特征将占到主导地位,而对于这个示例而言,三种特征是同等重要的。因此,在准备数据阶段,我们还需要将特征值的数据进行归一化处理,如将数值取值均归一化到0到1,或者-1到1之间。(不归一化容易导致欠拟合的情况)
下面公式是将特征值转化为0到1区间的值:
newValue = (oldValue-min)/(max-min)
因此,在准备数据这个模块里我们还需要加进去归一化处理的功能:
#准备数据:将收集的到的数据转换成格式化的数据格式
import numpy as np
def file2matrix(filename):
with open(filename) as fr:
lines = fr.readlines()
numberOfLines = len(lines) #获取文件行数
returnMat = np.zeros((numberOfLines,3)) #准备回传的矩阵-零矩阵
classLabelVector = [] #准备返回的结果数值-空list
index = 0
for line in lines:
line = line.strip()
listFromLine = line.split('\t')
returnMat[index,:] = listFromLine[0:3]
classLabelVector.append(int(listFromLine[-1]))
index += 1
#数据归一化的部分
dataSet = returnMat
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
ranges = maxVals - minVals
normDataSet = np.zeros(np.shape(dataSet))
m = dataSet.shape[0]
normDataSet = dataSet - np.tile(minVals, (m,1))
normDataSet = normDataSet/np.tile(ranges, (m,1)) #element wise divide
# return returnMat,classLabelVector
return normDataSet,classLabelVector,ranges, minVals
这样一来,输出的normDataSet就是经过归一化之后的数据了。
由于后续会用到每列的数值范围,因此也一起返回了取值范围(ranges)和最小值(minVals)。
2.3.3 分析数据
这里我们使用matplotlib制作原始数据的散点图:
#分析数据,可以用matplotlib创建可视化的图形
'''
Parameters:
datingDataMat: 数据特征矩阵(包含n个样本的数据集合)
datingLabels: 样本标签集合(各个样本的结果的集合)
'''
def analyseShow(datingDataMat, datingLabels):
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif']=['Microsoft YaHei'] #用来正常显示中文标签
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号
fig = plt.figure(figsize=[8,8],tight_layout=True)
ax1 = fig.add_subplot(221)
ax1.scatter(datingDataMat[:,1],datingDataMat[:,2],
15*np.array(datingLabels),15*np.array(datingLabels),
alpha=0.7)
ax1.set_xlabel(r'玩视频游戏所耗时间百分比')
ax1.set_ylabel(r'每周消费的冰淇淋公升数')
ax2 = fig.add_subplot(222)
ax2.scatter(datingDataMat[:,0],datingDataMat[:,1],
15*np.array(datingLabels),15*np.array(datingLabels),
alpha = 0.7)
ax2.set_xlabel(r'每年获取的飞行常客里程数')
ax2.set_ylabel(r'玩视频游戏所耗时间百分比')
ax3 = fig.add_subplot(223)
ax3.scatter(datingDataMat[:,2],datingDataMat[:,0],
15*np.array(datingLabels),15*np.array(datingLabels),
alpha = 0.7)
ax3.set_xlabel(r'每周消费的冰淇淋公升数')
ax3.set_ylabel(r'每年获取的飞行常客里程数')
#画图legend,这里不是重点,不用细看,就是说明一下图例
from matplotlib.legend_handler import HandlerLine2D
ax4 = fig.add_subplot(224)
dot1, = ax4.plot([3], marker='o', label='didntLike',color='purple')
dot2, = ax4.plot([3], marker='o', label='smallDoses',color='c')
dot3, = ax4.plot([3], marker='o', label='largeDoses',color='yellow')
ax4.tick_params(axis='both',labelsize=0,length=0,width=0)
ax4.spines['top'].set_color('none') #设置颜色
ax4.spines['bottom'].set_color('none')
ax4.spines['left'].set_color('none')
ax4.spines['right'].set_color('none')
plt.legend(loc='center',handler_map={dot1: HandlerLine2D(numpoints=4),
dot2: HandlerLine2D(numpoints=4),
dot3: HandlerLine2D(numpoints=4)})
plt.savefig('analyseShow.png')
plt.show()
输出如图:
通过简单的图像展示,图一很难辨识图中的点究竟属于哪个样本分类;而从图二中相对容易的得看出“每年赢得的飞行常客里程数”与“玩视频游戏所占百分比的约会数据散点图”这两个特征从属的类别;图三虽然能够比较容易得区分的确分数据点从属类别,但依然很难根据这张图得出结论性的信息。
将数据可视化是一个很重要的分析数据的方法。尤其对于未知数据,如果能通过合适的方式进行可视化,那么对于后续分类器的构建等步骤都有很好的指导作用。
2.3.4 分类器的构建
分类器的功能是使用k-近邻算法将每组数据划分到某个类中,
对未知类别属性的数据集中的每个点依次执行以下操作:
(1)计算已知类别数据集中的点与当前点之间的距离;
(2)按照距离距离递增次序排列;
(3)选取与当前点距离最小的k个点;
(4)确定前k个点所在地类别的出现频率;
(5)返回前k个点出现频率最高的类别作为当前点的预测分类;(这也就是为什么k最好选取奇数)
#分类器
'''
Parameters:
inX : 用于分类的输入向量inX
dataSet : 输入的训练样本集为dataSet
labels : 标签向量为labels
k : 表示用于选择最近邻居的数目
Return:
sortedClassCount[0][0] : 发生频率最高的元素标签,即分类结果。
'''
def classify0(inX, dataSet, labels, k):
dataSetSize = dataSet.shape[0]
diffMat = np.tile(inX, (dataSetSize,1)) - dataSet
sqDiffMat = diffMat**2
sqDistances = sqDiffMat.sum(axis=1)
distances = sqDistances**0.5
sortedDistIndicies = distances.argsort()
classCount={}
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
这里,用于衡量两特征向量的“距离”所用的方式是欧氏距离,
d
=
(
x
0
−
x
1
)
2
+
(
y
0
−
y
1
)
2
+
(
z
0
−
z
1
)
2
.
.
.
d=\sqrt{(x_0-x_1)^2+(y_0-y_1)^2+(z_0-z_1)^2...}
d=(x0−x1)2+(y0−y1)2+(z0−z1)2...
但这个衡量标准并不是唯一的。
常用的向量距离度量准则 | |
---|---|
1 | 欧式距离 |
2 | 曼哈顿距离 |
3 | 切比雪夫距离 |
4 | 马氏距离 |
5 | 巴氏距离 |
6 | 汉明距离 |
7 | 皮尔逊系数 |
8 | 信息熵 |
甚至在一些时候我们可以自定义一个函数,用于特殊的情况。
2.3.5 测试算法
在数据准备阶段我们已经将数据格式化成方便处理的格式了,已经归一化处理了。此外还构建好了分类器了,那么接下来我们就需要测试一下算法:
#测试算法
def datingClassTest():
hoRatio = 0.10 #抽取所有数据的百分之10
#导入数据,并格式化,归一化
normMat,datingLabels,ranges, minVals = file2matrix('datingTestSet2.txt')
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("分类结果: %d, 实际结果: %d" % (classifierResult, datingLabels[i]))
if (classifierResult != datingLabels[i]):
errorCount += 1.0
print("总错误率: %f" % (errorCount/float(numTestVecs)))
print("错误的样本数: %d"%errorCount)
测试结果如下:
分类器处理从约会数据集采样出来的10%的数据的错误率是5%,其实也不赖。
2.3.6 使用算法:构建完整可用系统
基于上述的分类器,在实际应用中,主人公海伦只要拿到一个人的前述三个特征数据,然后在约会之前通过这个分类器给出的分类结果(讨厌,一般喜欢,非常喜欢),就能有95%的概率判断海伦对这个人的喜欢程度。
其实所有的工作前述都已经完成了,这里就是整合调用前述代码的过程:
#使用算法部分
def classifyPerson():
resultList = ['didntLike','smallDoses','largeDoses']
#获取输入的数据:
percentTats = float(input("玩视频游戏所耗时间百分比:"))
ffMiles = float(input("每年获取的飞行常客里程数:"))
iceCream = float(input("每周消费的冰淇淋公升数:"))
normMat,datingLabels,ranges, minVals = file2matrix('datingTestSet2.txt')
inArr = np.array([percentTats,ffMiles,iceCream])
#这里用到了范围minVals和ranges
classifierResult =classify0((inArr-minVals)/ranges,normMat,datingLabels,3)
#给出分类结果
print("你将会对这个人产生感觉是:",resultList[classifierResult-1])
if __name__ == '__main__':
classifyPerson()
输入的例子及结果:
到这里为止,各个模块的函数就写完了。
综合的代码如下:
# -*- coding: utf-8 -*-
"""
@author: ChenD
Modify: 2019-3-7
"""
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#收集数据
#这里提供的datingTestSet2.txt文件就是收集到的数据
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#准备数据:将收集的到的数据转换成格式化的数据格式
import numpy as np
def file2matrix(filename):
with open(filename) as fr:
lines = fr.readlines()
numberOfLines = len(lines) #获取文件行数
returnMat = np.zeros((numberOfLines,3)) #准备回传的矩阵-零矩阵
classLabelVector = [] #准备返回的结果数值-空list
index = 0
for line in lines:
line = line.strip()
listFromLine = line.split('\t')
returnMat[index,:] = listFromLine[0:3]
classLabelVector.append(int(listFromLine[-1]))
index += 1
#数据归一化的部分
dataSet = returnMat
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
ranges = maxVals - minVals
normDataSet = np.zeros(np.shape(dataSet))
m = dataSet.shape[0]
normDataSet = dataSet - np.tile(minVals, (m,1))
normDataSet = normDataSet/np.tile(ranges, (m,1)) #element wise divide
# return returnMat,classLabelVector
return normDataSet,classLabelVector,ranges, minVals
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#分析数据,可以用matplotlib创建可视化的图形
def analyseShow(datingDataMat, datingLabels):
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif']=['Microsoft YaHei'] #用来正常显示中文标签
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号
fig = plt.figure(figsize=[8,8],tight_layout=True)
ax1 = fig.add_subplot(221)
ax1.scatter(datingDataMat[:,1],datingDataMat[:,2],
15*np.array(datingLabels),15*np.array(datingLabels),
alpha=0.7)
ax1.set_xlabel(r'玩视频游戏所耗时间百分比')
ax1.set_ylabel(r'每周消费的冰淇淋公升数')
ax2 = fig.add_subplot(222)
ax2.scatter(datingDataMat[:,0],datingDataMat[:,1],
15*np.array(datingLabels),15*np.array(datingLabels),
alpha = 0.7)
ax2.set_xlabel(r'每年获取的飞行常客里程数')
ax2.set_ylabel(r'玩视频游戏所耗时间百分比')
ax3 = fig.add_subplot(223)
ax3.scatter(datingDataMat[:,2],datingDataMat[:,0],
15*np.array(datingLabels),15*np.array(datingLabels),
alpha = 0.7)
ax3.set_xlabel(r'每周消费的冰淇淋公升数')
ax3.set_ylabel(r'每年获取的飞行常客里程数')
#画图legend,这里不是重点,不用细看,就是说明一下图例
from matplotlib.legend_handler import HandlerLine2D
ax4 = fig.add_subplot(224)
dot1, = ax4.plot([3], marker='o', label='didntLike',color='purple')
dot2, = ax4.plot([3], marker='o', label='smallDoses',color='c')
dot3, = ax4.plot([3], marker='o', label='largeDoses',color='yellow')
ax4.tick_params(axis='both',labelsize=0,length=0,width=0)
ax4.spines['top'].set_color('none') #设置颜色
ax4.spines['bottom'].set_color('none')
ax4.spines['left'].set_color('none')
ax4.spines['right'].set_color('none')
plt.legend(loc='center',handler_map={dot1: HandlerLine2D(numpoints=4),
dot2: HandlerLine2D(numpoints=4),
dot3: HandlerLine2D(numpoints=4)})
plt.savefig('analyseShow.png')
plt.show()
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#分类器的构建
import operator
def classify0(inX, dataSet, labels, k):
dataSetSize = dataSet.shape[0]
diffMat = np.tile(inX, (dataSetSize,1)) - dataSet
sqDiffMat = diffMat**2
sqDistances = sqDiffMat.sum(axis=1)
distances = sqDistances**0.5
sortedDistIndicies = distances.argsort() #计算距离
classCount={}
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
#python3中用items()替换python2中的iteritems()
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#测试算法
def datingClassTest():
hoRatio = 0.1 #抽取所有数据的百分之10
#导入数据,并格式化,归一化
normMat,datingLabels,ranges, minVals = file2matrix('datingTestSet2.txt')
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("分类结果: %d, 实际结果: %d" % (classifierResult, datingLabels[i]))
if (classifierResult != datingLabels[i]):
errorCount += 1.0
print("总错误率: %f" % (errorCount/float(numTestVecs)))
print("错误的样本数: %d"%errorCount)
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#使用算法部分
def classifyPerson():
resultList = ['didntLike','smallDoses','largeDoses']
#获取输入的数据:
percentTats = float(input("玩视频游戏所耗时间百分比:"))
ffMiles = float(input("每年获取的飞行常客里程数:"))
iceCream = float(input("每周消费的冰淇淋公升数:"))
normMat,datingLabels,ranges, minVals = file2matrix('datingTestSet2.txt')
inArr = np.array([percentTats,ffMiles,iceCream])
#这里用到了范围minVals和ranges
classifierResult =classify0((inArr-minVals)/ranges,normMat,datingLabels,3)
#给出分类结果
print("你将会对这个人产生感觉是:",resultList[classifierResult-1])
if __name__ == '__main__':
classifyPerson()
(欢迎关注微信公众号,cd的实验报告:cdsreport)