前言
机器学习实战系列之学习笔记主要是本人进行学习机器学习的整理。本系列所有代码是用python3编写,并使用IDE Pycharm在Windows平台上编译通过。本系列所涉及的所有代码和资料可在我的github或者码云上下载到,gitbub地址:https://github.com/mcyJacky/MachineLearning,码云地址:https://gitee.com/mcyHome/MachineLearning,如有问题,欢迎指出~。
一、ID3算法构造决策树
1.1 背景
上一篇内容已经较详尽的介绍决策树理论,包括特征的选择、决策树的生产和决策树的剪枝。现在我们就对表1.1海洋生物数据表数据集运用ID3算法使用信息增益准则选择特征,构造决策树,表中数据包含5个海洋动物,特征包括:no surfacing、flippers(结果是用1表示、结果否用0表示),将这些动物分为两类:鱼类和非鱼类(鱼类用yes表示、非鱼类用no表示)。
序号 | 不浮出水面是否可以生存(no surfacing) | 是否有脚蹼(flippers) | 属于鱼类 |
---|---|---|---|
1 | 是 | 是 | 是 |
2 | 是 | 是 | 是 |
3 | 是 | 否 | 否 |
4 | 否 | 是 | 否 |
5 | 否 | 是 | 否 |
决策树的一般流程:
- (1)收集数据:可以使用任何方法
- (2)准备数据:树构造算法只适用于标称型数据,因此数值型数据必须离散化
- (3)分析数据:可以使用任何方法,构造树完成之后,我们应该检查图形是否符合预期
- (4)训练算法:构造树的数据结构
- (5)测试算法:使用经验树计算错误率
- (6)使用算法:此不步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义
1.2 信息增益计算
1)熵(香农熵)计算
首先,我们对表1.1海洋生物数据表创建数据集,如下:
'''
@function:创建一个训练数据集
@#param: None
@return:
dataSet [list] 训练数据集
labels [list] 特征标签
'''
def createDataSet():
dataSet = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
labels = ['no surfacing', 'flippers']
return dataSet, labels
if __name__ == '__main__':
myDat, labels = createDataSet()
print(myDat)
print(labels)
'''输出结果
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
['no surfacing', 'flippers']
'''
接着,我们根据给定的数据集来计算香农熵(Entropy):
import time
import math
'''
@function:创建一个训练数据集
@#param: None
@return:
dataSet [list] 训练数据集
labels [list] 特征标签
'''
def createDataSet():
dataSet = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
labels = ['no surfacing', 'flippers']
return dataSet, labels
'''
@function:给定数据集熵的计算
@#param: dataSet [list] 训练数据集列表
@return:
shannonEnt [num] 熵(香农熵)
'''
def calcShannonEnt(dataSet):
numEntries = len(dataSet)
labelCounts = {} #定义类标签数量字典
for featVec in dataSet:
currentLabel = featVec[-1]
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
shannoEnt = 0.0
for key in labelCounts: #进行熵计算
prob = float(labelCounts[key])/numEntries
shannoEnt -= prob * math.log(prob, 2)
return shannoEnt
if __name__ == '__main__':
start = time.clock()
myDat, labels = createDataSet()
print(myDat)
entropy = calcShannonEnt(myDat)
print("entropy: ", entropy)
end = time.clock()
print(end - start)
'''输出结果
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
entropy: 0.9709505944546686
0.0011534813068524764
'''
2)划分数据集
如果我们已经确定了某个特征,再接下来的计算时,我们就要对数据集进行划分,我们将对每个特征划分数据集的结果计算一次信息熵,然后判断按照哪个特征划分数据集是最好的划分方式。下面就是按照给定特征划分数据集的方法:
'''
@function:划分数据集
@#param:
dataSet [list] 待划分训练数据集
axis 划分数据集的特征(索引)
value 需要返回特征的值
@return:
retDataSet [list] 划分后的数据集
'''
def splitDataSet(dataSet, axis, value):
retDataSet = []
for featVec in dataSet:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
if __name__ == '__main__':
start = time.clock()
myDat, labels = createDataSet()
print(myDat)
retData = splitDataSet(myDat, 0, 1)
print(retData)
end = time.clock()
print(end - start)
'''输出结果
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
[[1, 'yes'], [1, 'yes'], [0, 'no']]
4.8949925410577366e-05
'''
3)用信息增益法选择最优特征
现在,我们就可以进行选择最好的数据集划分方式来确定最优特征,当然我们要注意在下面函数调用需要满足一定的要求:①数据必须是一种由列表元素组成的列表,而且所有的列表元素都要具有相同的数据长度;②数据的最后一列或者每个实例的最后一个元素是当前实例的类别标签。
'''
@function:通过信息增益选择最优特征
@#param:
dataSet [list] 待划分训练数据集
@return:
bestFeature [num] 最优特征位于列表的索引
'''
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #特征个数
baseEntropy = calcShannonEnt(dataSet) #熵的结果
bestInfoGain = 0.0; bestFeature = -1 #初始化信息增益和最优特征
for i in range(numFeatures):
featList = [example[i] for example in dataSet] #某一特征所有取值列表
uniqueValues =set(featList) #用集合去取列表中相同部分
newEntropy = 0.0 #初始化特征熵
for value in uniqueValues:
subDataSet = splitDataSet(dataSet, i, value) #H划分特征数据集
prob = len(subDataSet) / float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
infoGain = baseEntropy - newEntropy
if infoGain > bestInfoGain:
bestInfoGain = infoGain
bestFeature = i #最优特征索引
return bestFeature
if __name__ == '__main__':
start = time.clock()
myDat, labels = createDataSet()
print(myDat)
bestFeat = chooseBestFeatureToSplit(myDat)
print('bestFeat', bestFeat)
end = time.clock()
print(end - start)
'''输出结果
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
bestFeat 0
0.00010855830232184497
'''
通过函数chooseBestFeatureToSplit()可以计算出某个训练数据集最优特征的索引位置。运行结果为0,即我们将按照将表1.1中先按第一个特征属性进行划分(序号1,2,3为一组;序号4,5为一组)。按照上述方法划分数据集,第一个特征为1的海洋生物分组将有两个属于鱼类,一个属于非鱼类;另一个分组则全部属于非鱼类。如果按照第二个特征分组呢?第一个海洋动物分组将有两个属于鱼类、两个属于非鱼类;另一个分组则只有一个非鱼类。因此,比较可知第一种划分很好地处理了相关数据,这也正是我们的计算结果。
1.3 递归生成决策树
目前我们已经学习了从数据集构造决策树算法所需要的子功能模块,其工作原理如下:得到原始数据集,然后基于最后的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据将向下传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据。因此可以采用递归的原则处理数据集。
递归结束的条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。如果所有实例具有相同的分类,则得到一个叶子节点或者终止块。第一个结束条件使得算法可以终止,我们甚至可以设置算法可以划分的最大分组数目。(如果数据集已经处理所有属性,但是类标签依然不是唯一的,此时我们需要决定如何定义该叶子节点,在这种情况下,我们通常会采用多数表决的方法决定该叶子节点的分类)。下面函数majorityCnt()就是返回数据集中出现次数最多的分类:
import operator
'''
@function:通过多数表决返回次数分类最多的名称
@#param:
classList [list] 类标签
@return:
sortedClassCount[0][0] 次数最多的类标签的名称
'''
def majorityCnt(classList):
classCount = {} #定义初始化分类标签字典存储变量
for vote in classList:
if vote not in classCount.keys():
classCount[vote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.items(), key = operator.itemgetter(1), reverse=True) #从大到小排序
return sortedClassCount[0][0]
if __name__ == '__main__':
start = time.clock()
classList = ['yes', 'yes', 'yse', 'no']
maxLabel = majorityCnt(classList)
print(maxLabel)
end = time.clock()
print(end - start)
'''输出结果
yes
9.079421648736125e-05
'''
最后,我们根据训练数据集合特征标签来创建决策树:
'''
@function:构建决策树
@#param:
dataSet [list] 待划分训练数据集
labels [] 特征标签
@return:
myTree [dict] 决策树
'''
def createTree(dataSet, labels):
classList = [example[-1] for example in dataSet] #分类标签列表
if classList.count(classList[0]) == len(classList): #类别完全相同则停止继续划分
return classList[0]
if len(dataSet[0]) == 1: #使用完所有特征,仍然不能将数据集划分成仅包含唯一类别的分组,返回出现次数最多的类别
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet) #最优特征索引
bestFeatLabel = labels[bestFeat] #最优特征的名称
myTree = {bestFeatLabel:{}} #初始化决策树
del(labels[bestFeat] )
featValues = [example[bestFeat] for example in dataSet] #最优特征中特征值列表
uniqueVals = set(featValues) #特征值列表去重
for value in uniqueVals:
subLables = labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLables) #递归
return myTree
if __name__ == '__main__':
start = time.clock()
myDat, labels = createDataSet()
print(myDat)
myTree = createTree(myDat, labels)
print(myTree)
end = time.clock()
print(end - start)
'''输出结果
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
0.00015513968295449118
'''
如上变量myTree包含了很多代表树结构信息的嵌套字典,从左边开始,第一个关键字no surfacing是第一个特征划分的数据集,该关键字的值也是另一个数据字典。第二个关键字是flippers特征划分数据集,这些关键字是值no surfacing节点的子节点。这些值可能是类标签,也可能是另一个数据字典。如果值是类标签,则该子节点是叶子节点;如果值是另一个数据字典,则子节点是一个判断节点,这种格式结构不断重复就构成了整课树。本例子子中,这棵树包含了3个叶子节点和2个判断节点。
二、使用Matplotlib注解绘制树形图
上述我们已经学习了如何从数据集中创建树,然而字典的表示形式非常不易于理解,而且直接绘制图形也比较困难。决策树的主要优点是直观易于理解,如果不能将其直观显示出来,就无法发挥优势,本节我们将使用matplotlib库创建树形图。
2.1 Matplotlib注解
Matplotlib提供了一个非常有用的注解工具annotations,我们创建treePlotter.py文件,它可以在数据图形上添加文本标注。简单使用方法示例如下:
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号
decisionNode = dict(boxstyle='sawtooth', fc='0.8') #pad=0.3,tooth_size=None
leafNode = dict(boxstyle='round4', fc='0.8')
arrow_args = dict(arrowstyle='<-') #head_length=0.4,head_width=0.2
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
# xy 标注点
# xytext 对标注点进行注释的点
# axes fraction: fraction of axes from lower left
createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',
xytext=centerPt, textcoords='axes fraction',
va="center", ha="center", bbox=nodeType, arrowprops=arrow_args)
'''
@function:绘制结点标注
@#param:
nodeTxt [str] 标注文字
centerPt [xy] 注释文字位置的坐标
parentPt [xy] 标注对象点坐标
nodeType [dic] 标注字体框的格式
@return: 标注后的图
'''
def createPlot():
fig = plt.figure(1, facecolor='white') #背景色
fig.clf() #Clear the current figure
createPlot.ax1 = plt.subplot(111, frameon=False) ##用函数属性createPlot.ax1定义全局变量
plotNode('决策结点', (0.5, 0.1), (0.1, 0.5), decisionNode)
plotNode('叶结点', (0.8, 0.1), (0.3, 0.8), leafNode)
plt.show()
if __name__ == '__main__':
createPlot()
使用createPlot()会输出图2.1注解示例结果:
图2.1 使用matplotlib注解示例
2.2 构造注解树
1)获取叶节点数目和树层数
绘制一颗树需要一些技巧,我们虽然有x,y坐标,但是如何放置所有的树节点却是一个问题。我们要知道有多少个节点,以便正确确定x轴的长度;我们还需要知道树由多少层,以便可以正确确定y轴的高度。下面我们用getNumLeafs()和getTreeDepth()来获取叶节点的数目和树的层数:
'''
@function:获得叶子结点个数
@#param:
myTree [dict] 决策树
@return:
numLeafs [nums] 叶子结点个数
'''
def getNumLeafs(myTree):
numLeafs = 0 #初始化叶子节点树
firstStr = next(iter(myTree)) #当前树的第一个key
secondDict = myTree[firstStr] #当前树的第一个key值
for key in secondDict.keys():
if type(secondDict[key]).__name__ == 'dict': #如果该节点是判断节点,使用递归
numLeafs += getNumLeafs(secondDict[key])
else:
numLeafs += 1 #如果是叶子节点,则叶子结点数+1
return numLeafs
'''
@function:获得决策树层数
@#param:
myTree [dict] 决策树
@return:
maxDepth [nums] 树的层数
'''
def getTreeDepth(myTree):
maxDepth = 0
firstStr = next(iter(myTree)) #当前树的第一个key
secondDict = myTree[firstStr] ##当前树的第一个key值
for key in secondDict.keys():
if type(secondDict[key]).__name__ == 'dict': #如果该节点是判断节点,使用递归
thisDepth = 1 + getTreeDepth(secondDict[key])
else:
thisDepth = 1
if thisDepth > maxDepth:
maxDepth = thisDepth
return maxDepth
'''
@function:返回预定义的树结构(用于测试)
@#param:
i [num] 列表的索引
@return:
listOfTrees[i] [dict] 某决策树
'''
def retrieveTree(i):
listOfTrees = [{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}},
{'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}}]
return listOfTrees[i]
if __name__ == '__main__':
# createPlot()
myTree = retrieveTree(0)
print(myTree)
numLeafs = getNumLeafs(myTree)
depth = getTreeDepth(myTree)
print('numLeafs: ', numLeafs)
print('depth: ', depth)
'''输出结果
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
numLeafs: 3
depth: 2
'''
2)绘制注解树
下面使用plotMidText()函数绘制父子节点间的填充文本信息,用plotTree()函数来绘制树:
'''
@function:标注结点之间的判断文字
@#param:
cntrPt [xy] 标注文字的位置
parentPt [xy] 标注对象的位置
txtString [str] 标注文字
@return: 结点之间的判断文字
'''
def plotMidText(cntrPt, parentPt, txtString):
xMid = (parentPt[0] - cntrPt[0])/2.0 + cntrPt[0]
yMid = (parentPt[1] - cntrPt[1])/2.0 + cntrPt[1]
createPlot.ax1.text(xMid, yMid, txtString)
'''
@function:绘制决策树方法
@#param:
myTree [dict] 待绘制的决策树对象
parentPt [xy] 标注文字的位置
nodeText [str] 标注文字
@return: 决策树图
'''
def plotTree(myTree, parentPt, nodeText):
numLeafs = getNumLeafs(myTree)
# depth = getTreeDepth(myTree)
firstStr = next(iter(myTree))
cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff) #标注点的中心位置
plotMidText(cntrPt, parentPt, nodeText)
plotNode(firstStr, cntrPt, parentPt, decisionNode)
secondDict = myTree[firstStr]
plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD
for key in secondDict.keys():
if type(secondDict[key]).__name__ == 'dict':
plotTree(secondDict[key], cntrPt, str(key))
else:
plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW
plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode)
plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key))
plotTree.yOff = plotTree.yOff + 1.0 / plotTree.totalD
def createPlot(inTree):
fig = plt.figure(1, facecolor='white')
fig.clf()
axprops = dict(xticks=[], yticks=[])
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops) #无坐标轴的图
plotTree.totalW = float(getNumLeafs(inTree)) #全局变量定义树的宽度
plotTree.totalD = float(getTreeDepth(inTree)) #全局变量定义树的高度
plotTree.xOff = -0.5/plotTree.totalW;plotTree.yOff = 1.0; #x,y偏移量
plotTree(inTree, (0.5, 1.0), '')
plt.show()
if __name__ == '__main__':
myTree = retrieveTree(0)
createPlot(myTree)
现在我们就绘制出树形图,如下图2.2决策树形图:
图2.2 决策树形图
三、测试和存储分类器
3.1 测试算法:使用决策树执行分类
依靠训练数据构造了决策树之后,我们可以将它用于实际数据的分类。在执行数据分类时,需要使用决策树以及用于构造决策树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行该过程直到进入叶子结点;最后将测试数据定义为叶子节点所属的类型。
'''
@function:决策树的分类函数
@#param:
inputTree [dict] 决策树
featLabels [list] 特征标签
testVec [list] 测试对象
@return:
classLabel 分类标签
'''
def classify(inputTree, featLabels, testVec):
firstStr = next(iter(inputTree))
secondDict = inputTree[firstStr]
featIndex = featLabels.index(firstStr)
for key in secondDict.keys():
if testVec[featIndex] == key:
if type(secondDict[key]).__name__ == 'dict':
classLabel = classify(secondDict[key], featLabels, testVec)
else: classLabel = secondDict[key]
return classLabel
if __name__ == '__main__':
start = time.clock()
import treePlotter as tp
myTree = tp.retrieveTree(0)
result = classify(myTree, labels, [1,0])
print('result: ', result)
end = time.clock()
print(end - start)
'''输出结果
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
result: no
0.9584667778040511
'''
3.2 使用算法:决策树的存储
我们构造决策树是很耗时的任务,因此,为了节省计算时间,最后能够在每次执行分类时调用已经构造好的决策树,为了解决这个问题,我们用模块pickle序列化对象。序列化对象可以在磁盘上保存对象,并在需要的时候读取出来。(任何对象都能序列化,字典对象也不例外)
'''
@function:序列化对象存储决策树
@#param:
inputTree [dict] 决策树
filename 存储后的文件
@return: None
'''
def storeTree(inputTree, filename):
import pickle #导入pickle模块
fw = open(filename, 'wb')
pickle.dump(inputTree, fw) #序列化
fw.close()
'''
@function:将对象进行反序列化(读取决策树对象)
@#param:
filename 存储后的文件
@return: 决策树模型
'''
def grabTree(filename):
import pickle
fr = open(filename, 'rb')
return pickle.load(fr) #反序列化
if __name__ == '__main__':
start = time.clock()
import treePlotter as tp
myTree = tp.retrieveTree(0)
filename = './storageTree1.txt' #当前目录下的文件
storeTree(myTree, filename)
rTree = grabTree(filename)
print('rTree', rTree)
end = time.clock()
print(end - start)
'''输出结果
rTree {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
0.7381443478034314
'''
通过执行storeTree()函数会将存储在当前文件夹下的txt文件中,当你用记事本打开文件时是乱码的格式,因为序列化是已二进制格式进行存储的。而当执行gragTree()函数我重新加载原理树的格式。
四、示例:使用决策树预测隐形眼镜类型
下面我们通过一个例子讲解决策树如何预测患者需要佩戴的隐形眼镜类型。它的一般流程是:
- (1)收集数据:提供的文本文件
- (2)准备数据:解析tab键分隔的数据行
- (3)分析数据:快速检查数据,确保正确地解析数据内容,绘制树形图
- (4)训练算法:使用createTree()函数
- (5)测试算法:编写测试函数验证决策树可以正确分类给定的数据实例
- (6)使用算法:存储树的数据结构,以便下次使用时无需重新构造树
下面是训练数据集的部分文本:
young myope no reduced no lenses
young myope no normal soft
young myope yes reduced no lenses
young myope yes normal hard
young hyper no reduced no lenses
该数据的特征标签分别是:age(年龄)、prescript(症状)、astigmatic(是否散光)、tearRate(眼泪数量)以及类标签。下面我们使用决策树ID3算法来生成隐形眼镜数据集决策树:
if __name__ == '__main__':
import treePlotter as tp
fr = open('lenses.txt')
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate'] # 年龄, 散光,
lensesTree = createTree(lenses, lensesLabels)
print(lensesTree)
tp.createPlot(lensesTree)
'''输出结果
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
{'tearRate': {'reduced': 'no lenses', 'normal': {'astigmatic': {'yes': {'prescript': {'hyper': {'age': {'young': 'hard', 'pre': 'no lenses', 'presbyopic': 'no lenses'}}, 'myope': 'hard'}}, 'no': {'age': {'young': 'soft', 'pre': 'soft', 'presbyopic': {'prescript': {'hyper': 'soft', 'myope': 'no lenses'}}}}}}}}
'''
输出的决策树图如下:
图4.1 由ID3算法生成的隐形眼镜分类决策树图
至目前为止,我们已经完成了决策树的匹配,但是可能这些匹配选项可能太多了,过造成过度拟合问题,之后我们会介绍如果减少过度匹配问题,通过裁剪决策树,这边就不多详述。总体来说,使用决策树的优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据;缺点:可能会产生过度匹配问题;适用数据类型:数值型和标称型。
【参考】:
1. 《统计学习方法》作者:李航 第5章 决策树
2. 《机器学习实战》作者:Peter Harrington 第3章 决策树
转载声明:
版权声明:非商用自由转载-保持署名-注明出处
署名 :mcyJacky
文章出处:https://blog.csdn.net/mcyJacky