【参考书籍】机器学习实战(Machine Learning in Action)
本章内容
决策树分类器就像带有终止块(表示分类结果)的流程图。处理数据集的流程,1、测量集合中数据的不一致性,也就是熵;2、寻找最优化方案划分数据集,直到数据集中的所有数据属于同一分类。ID3算法可以用于划分标称型数据集。构建决策树时,通常采用递归方法将数据集转化为决策树。在Python中,采用内嵌的数据结构字典存储树的节点信息。
使用Matplotlib可以将字典决策树转化为图形。使用Python的pickle模块可以序列化对象,用于存储决策树结构。其他决策树算法:C4.5,CART。
构造决策树时,要解决的第一个问题是:当前数据集上哪个特征在划分数据分类时起决定作用,这需要评估每个特征。
一般决策树算法采用二分法划分数据,但文中采用ID3算法划分数据集。每次划分数据集只选取一个特征属性,在多个特征属性中,第一次选择哪个特征作为划分的参考属性?首先,需要采用量化的方法判断如何划分数据,信息论是量化处理信息的分支科学。
信息增益
划分数据集的原则:将无需的数据变得更加有序。
信息增益:在划分数据集之前信息发生的变化称为信息增益。计算每个特征值划分数据集获得的信息增益,获得信息增益最高的特征是最好的选择。
熵(香农熵):集合信息的度量方式称为熵。熵定义为信息的期望值。熵越高,则混合的数据也越多。待分类的事务可能划分在多个分类中,则符号x2i的信息定义为:
l(xi)=−log2p(xi)其中p(xi)是选择该分类的概率。 熵就是所有类别所有可能值包含的信息期望值,公式:
H=−∑i=1np(xi)log2p(xi)基尼不纯度:从一个数据集中随机选取子项,度量其被错误分类到其他分组里的概率。这是另一个度量集合无序程度的方法。
递归创建决策树
由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。
递归创建决策树的结束条件是:遍历完所划分的数据集的属性,或者每个分支下的所有实例都具有相同的分类。可以使用Python语言中的字典类型存储树的信息。
myTree = {bestFeatLabel : {}} # Python定义
>>> myTree # 命令行中输出
遇到的问题:使用Matplotlib输出中文的问题。
使用的函数
函数 | 功能 |
---|---|
arr[-1] | 数组的最后一个元素 |
len(arr) | 数组的长度 |
dict.keys() | 获取字典的key列表 |
log(prob, n) | n为底的prob的对数 |
featVec[:axis] | 获取featVec从0到axis为索引的元素 |
arr1.extend(arr2) | 将arr2中元素逐个添加到arr1中 |
arr1.append(arr2) | 将arr2作为一个元素添加到arr1中 |
set(list) | 对list求集合操作,集合中的所有元素不相同,且包含list中的所有元素 |
列表推导式 | list = [item[i] for item in dataSet],将数据集合dataSet的第i列赋给集合list |
sorted(,,) | 排序函数 |
{label : {}} | 嵌套字典类型,可用于存储树的信息 |
del(list[i]) | 删除list中的第i个元素 |
dict(boxstyle="sawtooth", fc=0.8 ) | 定义文本框,boxstyle="round4" 另一种边框线条 |
dict(arrowstyle="<-" | 定义箭头格式 |
plt.annotate() | 绘制注解,可用于绘制点箭头的注释,详见代码 |
plt.text(x,y,txt) | 在位置(x,y)处填充文本 |
type(var).\__name__=='vartype' | 判断变量var的类型是否是vartype类型 |
d=dic(key1='value1', key2='value2') | 与d={'key1':'value1', 'key2':'value2'} 功能一样,在代码中不能把key1当成变量使用 |
plt.subplot() | plt.subplot(111, frameon=False, **axprops),frameon控制是否显示边框,**axprops控制边框上显示的数字刻度 |
fw=open(filename, ‘w’) | 以写的方式打开文件 |
fr=open(filename) | 以读的方式打开文件 |
pickle.dump(obj, fw) | 将对象obj序列化进fw文件中 |
pickle.load(fr) | 将文件fr中的序列化对象读取出来 |
问题:有些字符串不能作为python文件名(如test.py)
程序代码
# coding=utf-8
# The File Name is trees.py
from math import log
# 计算给定数据集的香农熵
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
shannonEnt = 0.0
for key in labelCounts :
prob = float(labelCounts[key]) / numEntries
shannonEnt -= prob * log(prob, 2)
return shannonEnt
# 简单的鱼鉴定数据集(不浮出水面是否可以生存, 是否有脚蹼, 属于鱼类)
def createDataSet() :
dataSet = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
# labels意思:不浮出水面, 脚蹼
labels = ['no surfacing', 'flippers']
return dataSet, labels
# 按照给定特征划分数据集
# dataSet: 待划分的数据集
# axis: 划分数据集的特征
# value: 需要返回的特征的值
def splitDataSet(dataSet, axis, value) :
retDataSet = []
for featVec in dataSet :
# 如果数据条目的特征的值与value相等
if featVec[axis] == value :
# 后边两条语句将向量featVec的axis维去掉后,剩余部分存储在reducedFeatVec
reducedFeatVec = featVec[ : axis] # featVec[ : axis],返回数组featVec的第0-axis元素
reducedFeatVec.extend(featVec[axis+1 : ]) # featVec[axis+1 : ],返回数组featVec的第axis-末尾元素
retDataSet.append(reducedFeatVec)
return retDataSet
# 选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet) :
numFeatures = len(dataSet[0]) - 1
baseEntropy = calcShannonEnt(dataSet)
# info gain: 信息增益
bestInfoGain = 0.0; bestFeature = -1
for i in range(numFeatures) :
# 将dataSet中的第i列元素存储在featList中
featList = [example[i] for example in dataSet]
# 特征值featList列表中各不相同的元素组成一个集合
uniqueVals = set(featList)
newEntropy = 0.0
# 对第i特征划分一次数据集,计算数据的新熵值,并对所有唯一特征值得到的熵求和
for value in uniqueVals :
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet) / float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
# 信息增益是熵的减少或者数据的无序度的减少
infoGain = baseEntropy - newEntropy
# 取信息增益最大,即熵减少最多的特征
if infoGain > bestInfoGain :
bestInfoGain = infoGain
bestFeature = i
return bestFeature
# 创建树的函数代码
# dataSet: 数据集
# labels: 标签
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 :
# 复制类标签,并将其存储在新列表变量subLabels中
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
return myTree
# 使用文本注释绘制树节点
import matplotlib.pyplot as plt
# 设置中文字体,不设置无法显示中文
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)
# 定义文本框和箭头样式
decisionNode = dict(boxstyle="sawtooth", fc="0.8")
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")
# 绘制带箭头注解,\后边不能有空格
def plotNode(nodeTxt, centerPt, parentPt, nodeType) :
createPlot.axl.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction', xytext=centerPt, \
textcoords='axes fraction', va='center', ha='center', bbox=nodeType, \
arrowprops=arrow_args, fontproperties=font) # fontproperties=font,设置显示中文字体
# 后边有同样名字的函数更新此函数
def createPlot() :
fig = plt.figure(1, facecolor='white')
fig.clf()
createPlot.axl = plt.subplot(111, frameon=False)
plotNode(u'决策节点', (0.5, 0.1), (0.1, 0.5), decisionNode);
plotNode(u'叶节点', (0.8, 0.1), (0.3, 0.8), decisionNode);
plt.show();
# 获取叶节点的数目
def getNumLeafs(myTree) :
numLeafs = 0
firstStr = myTree.keys()[0]
secondDict = myTree[firstStr]
for key in secondDict.keys() :
# 判断节点的数据类型是否为字典
if type(secondDict[key]).__name__=='dict' :
numLeafs += getNumLeafs(secondDict[key])
else :
numLeafs += 1
return numLeafs
# 获取树的层数
def getTreeDepth(myTree) :
maxDepth = 0
firstStr = myTree.keys()[0]
secondDict = myTree[firstStr]
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
# 输出预先存储树的信息,避免每次测试代码都要从数据创建树的麻烦
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]
# 创建最终决策树的代码段
# 在父子节点间填充文本信息(子节点具有的特征值)
def plotMidText(cntrPt, parentPt, txtString) :
xMid = (parentPt[0] - cntrPt[0])/2.0 + cntrPt[0]
yMid = (parentPt[1] - cntrPt[1])/2.0 + cntrPt[1]
createPlot.axl.text(xMid, yMid, txtString)
def plotTree(myTree, parentPt, nodeTxt) :
# 计算树的宽和高
numLeafs = getNumLeafs(myTree)
depth = getTreeDepth(myTree)
firstStr = myTree.keys()[0]
cntrPt = (plotTree.xOff + (1.0+float(numLeafs))/2.0/plotTree.totalW, \
plotTree.yOff)
# 绘制子节点具有的特征值,或者沿此分支向下的数据实例必须具有的特征值
plotMidText(cntrPt, parentPt, nodeTxt)
plotNode(firstStr, cntrPt, parentPt, decisionNode)
secondDict = myTree[firstStr]
# 按比例减少全局变量plotTree.yOff,并标注此处将要绘制子节点
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))
# 绘制所有子节点后,增加全局Y的偏移
plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD
# 绘制图形是按照比例绘制树形图,好处是无需关心实际输出的图形大小
# x轴和y轴的有效范围是0.0-1.0
def createPlot(inTree) :
fig = plt.figure(1, facecolor='white')
fig.clf()
axprops = dict(xticks=[], yticks=[])
createPlot.axl = plt.subplot(111, frameon=False, **axprops)
# plotTree.totalW: 存储树的宽度;plotTree.totalD: 存储树的深度,
# 使用这两个变量可以计算树的节点的摆放位置,这样可以将树绘制在水平方向和垂直方向的中心位置
plotTree.totalW = float(getNumLeafs(inTree))
plotTree.totalD = float(getTreeDepth(inTree))
# plotTree.xOff和plotTree.yOff追踪已经绘制的节点位置,以及放置下一个节点的恰当位置
plotTree.xOff = -0.5/plotTree.totalW
plotTree.yOff = 1.0
plotTree(inTree, (0.5,1.0), '')
plt.show()
# 使用决策树的分类函数
def classify(inputTree, featLabels, testVec) :
firstStr = inputTree.keys()[0]
secondDict = inputTree[firstStr]
# 将标签字符串转换为索引,使用index()方法查找当前列表中第一个匹配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
# 使用pickle模块存储决策树,使用pickle序列化对象,序列化对象可以在磁盘保存对象,并在需要时读取出来
def storeTree(inputTree, filename) :
import pickle
fw = open(filename, 'w')
pickle.dump(inputTree, fw)
fw.close()
# 读取序列化对象
def grabTree(filename) :
import pickle
fr = open(filename)
return pickle.load(fr)
# 使用决策树预测隐形眼镜类型,这直接可以在命令行中找到这部分
在命令行中执行
>>> import 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.calcShannonEnt(myDat)
0.9709505944546686
>>> trees.splitDataSet(myDat, 0, 1)
[[1, 'yes'], [1, 'yes'], [0, 'no']]
>>> trees.splitDataSet(myDat, 0, 0)
[[1, 'no'], [1, 'no']]
>>> reload(trees)
<module 'trees' from 'C:\Python27\trees.py'>
>>> myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> trees.chooseBestFeatureToSplit(myDat)
0
>>> reload(trees)
<module 'trees' from 'C:\Python27\trees.pyc'>
>>> myDat, labels = trees.createDataSet()
>>> myTree = trees.createTree(myDat, labels)
>>> myTree
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
>>> reload(trees)
<module 'trees' from 'C:\Python27\trees.py'>
>>> trees.createPlot() # 生成的图,见末尾图1
>>> reload(trees)
<module 'trees' from 'C:\Python27\trees.py'>
>>> trees.retrieveTree(0)
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
>>> trees.retrieveTree(1)
{'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1:
'no'}}}}
>>> myTree = trees.retrieveTree(0)
>>> trees.getNumLeafs(myTree)
3
>>> trees.getTreeDepth(myTree)
2
>>> myTree = trees.retrieveTree(1)
>>> trees.getTreeDepth(myTree)
3
>>> trees.getNumLeafs(myTree)
4
>>> reload(trees)
<module 'trees' from 'C:\Python27\trees.py'>
>>>
trees.createPlot(myTree) # 生成的图见末尾图2
>>> reload(trees)
<module 'trees' from 'C:\Python27\trees.py'>
>>> myDat, labels=trees.createDataSet()
>>> trees.createPlot(myTree)
>>> myTree=trees.retrieveTree(0)
>>> myTree
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
>>> trees.classify(myTree, labels, [1,0])
'no'
>>> trees.classify(myTree, labels, [1,1])
'yes'
>>> reload(trees)
<module 'trees' from 'C:\Python27\trees.py'>
>>> trees.storeTree(myTree, 'c:\python27\ml\classifierStorage.txt')
>>> trees.grabTree('c:\python27\ml\classifierStorage.txt')
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
>>> fr=open('c:\python27\ml\ch03\lenses.txt') # 使用决策树预测隐形眼镜类型
>>> lenses=[inst.strip().split('\t') for inst in fr.readlines()]
>>> lenseslabels=['age','prescript','astigmatic','tearRate']
>>> lensesTree=trees.createTree(lenses, lenseslabels)
>>> lensesTree
{'tearRate': {'reduced': 'no lenses', 'normal': {'astigmatic': {'yes': {'prescri
pt': {'hyper': {'age': {'pre': 'no lenses', 'presbyopic': 'no lenses', 'young':
'hard'}}, 'myope': 'hard'}}, 'no': {'age': {'pre': 'soft', 'presbyopic': {'presc
ript': {'hyper': 'soft', 'myope': 'no lenses'}}, 'young': 'soft'}}}}}}
>>>
trees.createPlot(lensesTree) # 生成的图见末尾图3
图1 绘制的决策树决策节点和叶节点
图2 超过两个分支的树形图
图3 使用决策树预测隐形眼镜类型