根据《机器学习实战》中国工信出版集团 人民邮电出版社 学习得到的笔记
(二)K-近邻算法 KNN
一、K-近邻算法概述
K—近邻算法采用测量不同特征值之间的距离方法进行分类
- 优点:精度高、对异常值不敏感、无数据输入假定
- 缺点:计算复杂度高、空间复杂度高
- 适用数据范围:数值型和标称型
k-近邻算法称为kNN,它的工作原理是:存在一个样本数据集合,也称作训练样本集,并且样本集中每个数据都存在标签,即我们知道样本集中每一数据与所属分类的对应关系。输入没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本集中特征最相似数据(最近邻)的分类标签。一般来说,我们只选择样本数据集中前κ个最相似的数据,这就是k-近邻算法中κ的出处。通常κ是不大于20的整数。最后,选择κ个最相似数据出现次数最多的分类,作为新数据的分类。
二、python中的数据操作
1、使用python导入数据
新建一个kNN.py文件,导入预设的数据(注意缩进)
from numpy import * #导入科学计算包Numpy
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
接着在python shell下 导入kNN模块(关于导入模块可能会遇到无法导入的问题,可以先查看python shell的工作路径,将kNN.py文件放到python shell的工作环境下)
查看python shell的工作路径需要导入os包,再调用os包下的get.cwd()函数
>>> import os
>>> os.getcwd()
'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python37'
查到了工作环境之后拷贝py文件到工作环境下,进行import导入kNN模块。导入了之后可以输入变量的名字以检验是够正确的定义变量。
>>> import kNN
>>> group,labels = kNN.createDataSet()
>>> group
array([[1. , 1.1],
[1. , 1. ],
[0. , 0. ],
[0. , 0.1]])
>>> labels
['A', 'A', 'B', 'B']
得到了4组数据,每组数据有两个我们已知的属性或者特征值。向量labels包含了每个数据点的标签信息,labels包含的元素个数等于group矩阵行数。这里将数据点(1,1.1)定义为类A,数据点(0,0.1)定义为类B。为了说明方便,例子中的数值是任意选择的,并没有给出轴标签。
k-近邻算法:带有4个数据点的简单例子
2、使用kNN算法进行数据分类(从文本文件中解析数据)
伪代码如下:
计算已知类别数据集中的每个点依次执行以下操作
- 计算已知类别数据集中的点与当前点之间的距离
- 按照距离递增次序排序
- 选择与当前点距离最小的κ个点
- 确定前κ个点所在类别的出现概率
- 返回前κ个点出现频率最高的类别作为当前点的预测分类
python函数classify0()
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 = {}
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)
#Python3中不再支持iteritems(),将iteritems()改成items()
return sortedClassCount[0][0]
classify0()函数有4个参数 :
- inX:用于分类的输入向量
- dataSet:输入的训练样本集
- labels:标签向量
- k:用于选择最近邻居的数目
其中标签向量的元素数目和矩阵dataSet的行数相同。程序使用的是欧氏距离公式,计算向量xA与xB之间的距离:
(---------------欧式公式----------------)
计算完所有点的距离后,对数据按照从小到大的次序排序,确认前k个距离最小元素所在的主要分类。输入k总是正整数;最后,将classCount字典分解为元组列表,然后使用程序第二行导入运算符模块的itemgetter方法,按照第二个元素的次序对元组进行排序。此处的排序为逆序,即按照从最大到最小次序排序。最后返回发生频率最高的元素标签。
下面测试了一下结果,预测的结果也是‘B’
>>> group,labels = kNN.createDataSet()
>>> kNN.classify0([0,0],group,labels,3)
'B'
3、如何测试分类器
分类器并不会得到百分百的正确的结果,我们可以使用多种方法检验分类器的正确率。错误率是评估常用方法,完美的错误率为0,最差错误率是1.0。
实例:使用k-近邻算法改进约会网站的配对效果
1、准备数据:从文本文件中解析数据 将文本记录到转换Numpy的解析数据
将代码添加增加到kNN.py中
def file2matrix(filename):
fr = open(filename)
numberOfLines = len(fr.readlines())
returnMat = zeros((numberOfLines,3))
classLabelVector = []
fr = open(filename)
index = 0
for line in fr.readlines():
line = line.strip()
listFromLine = line.split('\t')
returnMat[index,:] = listFromLine[0:3]
classLabelVector.append(int(listFromLine[-1]))
index += 1
return returnMat,classLabelVector
接着开始重载kNN模块,进行数据读取。在python命令提示符下输入下面的命令
>>> reload(kNN)
执行以下命令,导入datingTestSet2.txt中的数据(在此之前必须保证datingTestSet2.txt文件在python的工作目录下,才可用函数filematrix读取文件数据)这里勘误一下:书籍中用的数据是datingTestSet.txt,导入会报错。应该导入datingTestSet2.txt中的函数,
datingDataMat,datingLabels = kNN.file2matrix('datingTestSet2.txt')
导入数据后,可以检查一下数据内容,在python shell 中输出
>>> datingDataMat
array([[4.0920000e+04, 8.3269760e+00, 9.5395200e-01],
[1.4488000e+04, 7.1534690e+00, 1.6739040e+00],
[2.6052000e+04, 1.4418710e+00, 8.0512400e-01],
...,
[2.6575000e+04, 1.0650102e+01, 8.6662700e-01],
[4.8111000e+04, 9.1345280e+00, 7.2804500e-01],
[4.3757000e+04, 7.8826010e+00, 1.3324460e+00]])
>>> datingLabels[0:20]
[3, 2, 1, 1, 1, 1, 3, 3, 1, 3, 1, 1, 2, 1, 1, 1, 1, 1, 2, 3]
2、分析数据:使用Matplotlib创建散点图
准备工作:为python安装matplotlib模块
在cmd下执行命令
python -m pip install -U pip setuptools
接着执行命令进行自动安装
python -m pip install matplotlib
安装完后执行命令 查看本机的安装的所有模块,确保matplotlib已经安装成功
python -m pip list
开始使用Matplotlib制作原始数据的散点图,在python shell 中执行命令。
>>> import matplotlib
>>> import matplotlib.pyplot as plt
>>> fig = plt.figure()
>>> ax = fig.add_subplot(111)
>>> ax.scatter(datingDataMat[:,1], datingDataMat[:,2])
>>> plt.show()
(三)决策树
一、决策树
决策树(Decision Tree)是在已知各种情况发生概率的基础上,通过构成决策树来求取净现值的期望值大于等于零的概率,评价项目迎合,判断其可行性的决策分析方法,是直观运行概率分析的一种图解法。由于这种决策分支画成图形很像一棵树的树干,故称决策树。
在机器学习 ,决策树是一个预测模型,它代表的是对象属性与对象值之间的一种映射关系。Entropy = 系统的混乱程度,使用算法ID3,C4.5和C5.0生成树算法使用熵。这一度量是基于信息学理论中熵的概念。
决策树是一种树形结果,其中每个内部节点表示一个属性上的测试,每个分支代表一个测试输出,每个叶节点代表一种类别。
决策树(Decision Tree)算法主要用来处理分类问题,是最经常使用的数据挖掘算法之一,是一种监督学习。
一个决策树包含三种类型的节点:
- 决策节点:通常用矩形框来表示
- 机会节点:通常和圆圈来表示
- 终结束:通常用三角形来表示
1、决策树的构造
在构造决策树前,我们先讨论下数学上如何信息论划分数据集,然后再将理论用代码实现到具体的数据集上,最后构建决策树。
在构建决策树时,我们需要解决的第一个问题是:当前数据集哪个特征在划分数据分类时起决定性作用,即我们要如何找出最优的分类特征。为了找到决定性的特征,划分出最好的结果,我们必须评估每个特征。完成数据划分后,原始数据集就被划分为几个数据子集,这些数据子集会分布在第一个决策点的所有分支上。如果某个分支下的数据属于同一类型,即数据已正确分类,无需进一步分割。如果数据子集内的数据不属于同个类型,则需要重复划分数据子集的过程。划分数据子集的算法和划分原始数据集的方法相同(因此可用递归函数继续划分子集),直到所有具有相同类型的数据都在一个数据子集内。
构建决策树的伪代码函数createTree() 如下所示:
检测数据集中的每个子集是否属于同一分类:
If so return 类标签
Else
寻找划分数据集的最好特征
划分数据集
创建分支节点
For 每个划分的子集:
调用函数createTree()并增加返回结果到分支节点中
Return 分支节点
上面是个的伪代码是个递归函数,递归的结束条件是遍历完所有数据集的属性或每个分支下的所有实例都具有相同分类
- 在原始数据集上基于最好的特征进行划分
- 划分后得到新的数据集
- 将新的数据集后放到分支节点上去
- 接着再调用1、2、3直到不能划分为止
二、如何划分数据集?(如何采用量化的方法判断如何划分数据)
划分数据集的大原则是:将无序的数据变得更加有序。在划分数据集前后,信息发生的变化称为信息增益。获得信息增益最高的特征就是最好的选择。
1、信息增益(香农熵&熵)
集合信息的度量方式称为香农熵或者简称为熵。熵定义为信息的期望值。关于信息的概念:
如果待分类的事物可能会出现多个结果x,则第i个结果xi发生的概率为p(xi),那么我们可以由此计算出xi的信息熵为:
那么,对于所有可能出现的结果,事物所包含的信息希望值(信息熵)就为:
其中n为分类的数目
下面开始用python计算信息熵,创建一个trees.py 文件 写入下列代码(计算给定的数据集的香农熵):
代码来自于apachCN,感谢代码中的注释。求数据集中每个实例标签出现的频率,然后用这个频率 计算香农熵:
from math import log
import operator
def calcShannonEnt(dataSet):
"""calcShannonEnt(calculate Shannon entropy 计算给定数据集的香农熵)
Args:
dataSet 数据集
Returns:
返回 每一组feature下的某个分类下,香农熵的信息期望
"""
# -----------计算香农熵的第一种实现方式start--------------------------------------------------------------------------------
# 求list的长度,表示计算参与训练的数据量
numEntries = len(dataSet)
# 下面输出我们测试的数据集的一些信息
# 例如:<type 'list'> numEntries: 5 是下面的代码的输出
# print type(dataSet), 'numEntries: ', numEntries
# 计算分类标签label出现的次数
labelCounts = {}
# the the number of unique elements and their occurance
for featVec in dataSet:
# 将当前实例的标签存储,即每一行数据的最后一个数据代表的是标签
currentLabel = featVec[-1]
# 为所有可能的分类创建字典,如果当前的键值不存在,则扩展字典并将当前键值加入字典。每个键值都记录了当前类别出现的次数。
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
# print '-----', featVec, labelCounts
# 对于label标签的占比,求出label标签的香农熵
shannonEnt = 0.0
for key in labelCounts:
# 使用所有类标签的发生频率计算类别出现的概率。
prob = float(labelCounts[key])/numEntries
# log base 2
# 计算香农熵,以 2 为底求对数
shannonEnt -= prob * log(prob, 2)
# print '---', prob, prob * log(prob, 2), shannonEnt
# -----------计算香农熵的第一种实现方式end--------------------------------------------------------------------------------
# # -----------计算香农熵的第二种实现方式start--------------------------------------------------------------------------------
# # 统计标签出现的次数
# label_count = Counter(data[-1] for data in dataSet)
# # 计算概率
# probs = [p[1] / len(dataSet) for p in label_count.items()]
# # 计算香农熵
# shannonEnt = sum([-p * log(p, 2) for p in probs])
# # -----------计算香农熵的第二种实现方式end--------------------------------------------------------------------------------
return shannonEnt
接着构建一个测试用的测试数据集
def createDataSet():
"""DateSet 基础数据集
Args:
无需传入参数
Returns:
返回数据集和对应的label标签
"""
dataSet = [[1, 1, 'yes'],#最后一列出现不同标签的数量越高,则熵越大,代表无序程序越高,我们在数据集中添加的分类就越多
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
# dataSet = [['yes'],
# ['yes'],
# ['no'],
# ['no'],
# ['no']]
# labels 露出水面 脚蹼
labels = ['no surfacing', 'flippers']#在这里,数据集是针对标签的,第一个数据对应第一个标签,最后一个数据代表判断标签
# change to discrete values
return dataSet, labels
在Python命令提示符下输入下列命令,得到这份数据的香农熵:
>>> reload(trees)
>>> myDat,labels = trees.createDataSet()
>>> myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> labels
['no surfacing', 'flippers']
>>> trees.calcShannonBnt(myDat)
>>> trees.calcShannonEnt(myDat)
0.9709505944546686
熵越高,则混合的数据也越多,数据也越混乱,例如在数据集中添加更多的分类,观察熵的变化情况。这里增加第三个名为maybe的分类,测试香农熵的变化:
>>> myDat[0][-1] = 'maybe'
>>> myDat
[[1, 1, 'maybe'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> trees.calcShannonEnt(myDat)
1.3709505944546687
得到熵之后,我们可以按周获取最大信息增量的方法划分数据集。
2、划分数据集
学习了如何度量数据集的无序程度,分类算除了需要测量信息熵,还需要划分数据集,度量划分数据集的熵,以便判断当前是否正确地划分了数据集。我们将对每个特征划分数据集的结果计算一次信息熵,然后判断按照哪个特征划分数据集是最好的划分方式。
在trees.py文件中添加下列的代码(按照给定的特征划分数据集):
"""
Function:
按给定特征划分数据集
Parameters:
dataSet——待划分的数据集
axis——划分数据集的特征
value——特征的返回值
Return:
retDataSet——划分好后的数据集列表
Modify:
2017-11-29
"""
def splitDataSet(dataSet, axis, value):
retDataSet = []
#创建返回数据列表对象
for featVec in dataSet:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]
#去掉axis特征
reducedFeatVec.extend(featVec[axis+1:])
#将符合条件的添加到返回的数据里表中
retDataSet.append(reducedFeatVec)
return retDataSet
#返回划分后的数据集
这里要特别提醒下python语言列表类型知道的extend()和append()方法,这两个方法功能类似,但是在处理多个列表时,这两个方法处理得到的结果是完全不一样的。这里借用apachCN中的内容解释两者的区别
extend和append的区别
list.append(object) 向列表中添加一个对象object
list.extend(sequence) 把一个序列seq的内容添加到列表中
1、使用append的时候,是将new_media看作一个对象,整体打包添加到music_media对象中。
2、使用extend的时候,是将new_media看作一个序列,将这个序列和music_media序列合并,并放在其后面。
result = []
result.extend([1,2,3])
print result
result.append([4,5,6])
print result
result.extend([7,8,9])
print result
结果:
[1, 2, 3]
[1, 2, 3, [4, 5, 6]]
[1, 2, 3, [4, 5, 6], 7, 8, 9]
我们可以在前面的简单样本数据上测试函数splitDataSe(),在python shell 中输入命令:
>>> reload(trees)
<module 'trees' from 'trees.py'>
>>> myDat,labels = trees.createDataSet()
>>> myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> trees.splitDataSet(myDat,0,1)
[[1, 'yes'], [1, 'yes'], [0, 'no']]
>>> trees.splitDataSet(myDat,0,0)
[[1, 'no'], [1, 'no']]
接下来我们将开始遍历整个数据集,循环计算香农熵和splitDataSet()函数,找到最好的特征划分方式。在trees.py文件中输入下列带代码(选择最好的数据划分方式)
"""
Function:
选择最优特征
Parameters:
dataSet——数据集
Return:
bestFeature——信息增益最大的(最优)特征的索引值
Modify:
2017-12-14
"""
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1
#特征数量
baseEntropy = calcShannonEnt(dataSet)
#计算数据集的最初香农熵
bestInfoGain = 0.0;
#信息增益
bestFeature = -1
#最优特征的索引值
for i in range(numFeatures):
#iterate over all the features 开始遍历数据集中的所有特征
featList = [example[i] for example in dataSet]
#获取dataSet的第i个所有特征
#create a list of all the examples of this feature 使用列表推导来创建一个新的列表,将数据集中的所有第i个特征值或者可能存在的值写入这个新list中
uniqueVals = set(featList)
#创建set集合{},元素不可重复
#get a set of unique values
newEntropy = 0.0
#经验条件熵
for value in uniqueVals:
#计算信息增益
subDataSet = splitDataSet(dataSet, i, value)
#subDataSet划分后的子集
prob = len(subDataSet)/float(len(dataSet))
#计算子集的概率
newEntropy += prob * calcShannonEnt(subDataSet)
#根据公式计算经验条件熵
infoGain = baseEntropy - newEntropy
#信息增益
#calculate the info gain; ie reduction in entropy
if (infoGain > bestInfoGain):
#compare this to the best gain so far
#计算信息增益
bestInfoGain = infoGain
#更新信息增益,找到最大的信息增益
#if better than current best, set to best
bestFeature = i
#记录信息增益最大的特征的索引值
return bestFeature
#返回信息增益最大的特征的索引值
list和set的不同之处在于集合类型中的每个值互不相同。从列表中创建set集合是python语言得到list列表中唯一元素值的最快方法。遍历当前特征中的所有唯一属性值,对每个特征值划分一次数据集,然后计算数据集的新熵值,并对所有唯一特征值得到的熵求和。信息增益是香农熵减少或者是无序度的减少。比较所有特征中的信息增益,返回最好的特征划分索引值。
下面开始测试以上代码的输出结果,在python shell 中输入下列代码:
>> reload(trees)
<module 'trees' from 'trees.py'>
>>> myDat,labels = trees.createDataSet()
>>> trees.chooseBestFeatureToSplit(myDat)
0
>>> myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
代码运行结果显示,第0个特征为最好用于划分数据集的特征。为了检验这个结果的正确性和实际意义,我们对应前面列出的数据表和myDat中的数据。
如果我们按照第一个特征属性划分数据,也就是说第一个特征是1的放在一个组,第一个特征是0的放在一个组,按照这个方法划分数据集:第一个特征为1的海洋生物分组将有两哥属于鱼类,一个属于非鱼类;另一个分组则全部属于非鱼类。如果按照第二个特征分组:第一个海洋动物分组将有两个属于鱼类,两个属于非鱼类;另一个分组则只有一个非鱼类。不难看出,按照第一个特征分组的正确率高。
3、递归构建决策树
回顾一下工作原理:从数据集构造决策树算法所需要的子功能模块,其工作原理如下:得到原始数据集,然后基于最好的属性值划分数据集,由于特增值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据将被向下传递到树分支的下一个节点,在这个节点上,我们可以在此划分数据。因此,可以采用递归的原则处理数据集。
递归结束的条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。下图展示划分数据集是的数据路径,如果所有实例具有相同的分类,则得到一个叶子节点或者终止块。
图1 划分数据集是的数据路径