决策树(Decision Tree)是在已知各种情况发生概率的基础上,通过构成决策树来求取净现值的期望值大于等于零的概率,评价项目风险,判断其可行性的决策分析方法,是直观运用概率分析的一种图解法。
下面介绍分类决策树,分类决策树模型是一种描述对实例进行分类的树形结构。决策树由结点和有向边组成。结点有两种类型:内部节点和叶节点,内部节点表示一个特征或属性,叶节点表示一个类。
分类的时候,从根节点开始,对实例的某一个特征进行测试,根据测试结果,将实例分配到其子结点;此时,每一个子结点对应着该特征的一个取值。如此递归向下移动,直至达到叶结点,最后将实例分配到叶结点的类中。
下图是决策树的例子
决策树的构建
决策树的构建有三个步骤:特征选择、决策树生成、决策树的修建
特征选择
为什么要进行特征选择呢?很简单,不同的特征会对分类结果造成不同的影响,有的特征甚至无法对分类造成影响,比如说皮肤黑白并不能作为区分男女的特征,所以说我们需要选择更好的特征来生成决策树。但是特征选择显然是不能靠人工来判断的。在机器学习中,我们通过信息增益来作为特征选择的标准。
假设我们要针对上面的数据实现是否批准贷款申请的决策树,如果我们根据年龄和是否有工作可以得出以下两种可能的决策树
那么那个更好呢?这个时候我们计算出每个特征值划分数据集是所获得的信息增益,增益最高的特征就是最好的。
信息增益
信息熵:信息的基本作用就是消除人们对事物的不确定性。多数粒子组合之后,在它似像非像的形态上押上有价值的数码,具体地说,这就是一个在博弈对局中现象信息的混乱。
香农指出,它的准确信息量应该是-(p1*log(2,p1) + p2 * log(2,p2) + ... +p32 *log(2,p32)),
其中,p1,p2 , ...,p32 分别是这 32 个球队夺冠的概率。香农把它称为“信息熵” (Entropy),一般用符号 H 表示,单位是比特。
变量的不确定性越大,熵也就越大,把它搞清楚所需要的信息量也就越大。也就是说价值越大
香农熵公式如下
P(x):选择该分类的概率(银行贷款数据中只有两个分类,yes和no)
经验熵
如果说熵中的概率是数据估计得出来的时候,计算出的熵叫做经验熵,也可以理解为根据自己所有的数据来计算概率。
接下来是用Python代码实现上述公式
def creatDataset():
"""
创建数据集,用于练习香农公式
:return: dataSet:数据集,labels:分类属性
"""
dataSet = [
[0, 0, 0, 0, 'no'],
[0, 0, 0, 1, 'no'],
[0, 1, 0, 1, 'yes'],
[0, 1, 1, 0, 'yes'],
[0, 0, 0, 0, 'no'],
[1, 0, 0, 0, 'no'],
[1, 0, 0, 1, 'no'],
[1, 1, 1, 1, 'yes'],
[1, 0, 1, 2, 'yes'],
[1, 0, 1, 2, 'yes'],
[2, 0, 1, 2, 'yes'],
[2, 0, 1, 1, 'yes'],
[2, 1, 0, 1, 'yes'],
[2, 1, 0, 2, 'yes'],
[2, 0, 0, 0, 'no']
]
"""
年龄:0:青年、1:中年、2:老年
工作:0:否、1:是
房子:0:否、1:是
信贷情况:0:一般好、1:好、2:非常好
类别:no:否、yes:是
"""
labels = ['年龄','工作','房子','信贷情况']
return dataSet,labels
def calcShannonEntropy(dataSet):
"""
计算香农熵
:return: ShannonEntropy:香农熵
"""
numEntires = len(dataSet)
labelCounts = {}#用字典保存每个标签出现的次数
for featVec in dataSet:#遍历每组特征向量
currentLabel = featVec[-1]
if currentLabel not in labelCounts.keys():#如果标签没有加入字典则加入进行
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
print(labelCounts)
shannonEntroy = 0.0#香农熵
for key in labelCounts:
prob = float(labelCounts[key])/numEntires
shannonEntroy -= prob*log(prob,2)#log(prob,2)-->log2prob
return shannonEntroy
运行结果
由上面的结果可以得出经验熵
条件熵
条件熵是什么意思呢?H(Y|X)是指:一直随机变量X的条件下随机变量Y的不确定性,公式如下
了解了上述的概念后我们可以讲解信息增益了:
信息增益:熵 - 条件熵
下面是知乎上某位打算对于信息增益的理解:通俗地讲,X(明天下雨)是一个随机变量,X的熵可以算出来, Y(明天阴天)也是随机变量,在阴天情况下下雨的信息熵我们如果也知道的话(此处需要知道其联合概率分布或是通过数据估计)即是条件熵。两者相减就是信息增益!原来明天下雨例如信息熵是2,条件熵是0.01(因为如果是阴天就下雨的概率很大,信息就少了),这样相减后为1.99,在获得阴天这个信息后,下雨信息不确定性减少了1.99!是很多的!所以信息增益大!也就是说,阴天这个信息对下雨来说是很重要的!所以在特征选择的时候常常用信息增益,如果IG(信息增益大)的话那么这个特征对于分类来说很关键~~ 决策树就是这样来找特征的!
下面是计算信息争议的Python代码
from math import log
def creatDataset():
"""
创建数据集,用于练习香农公式
:return: dataSet:数据集,labels:分类属性
"""
dataSet = [
[0, 0, 0, 0, 'no'],
[0, 0, 0, 1, 'no'],
[0, 1, 0, 1, 'yes'],
[0, 1, 1, 0, 'yes'],
[0, 0, 0, 0, 'no'],
[1, 0, 0, 0, 'no'],
[1, 0, 0, 1, 'no'],
[1, 1, 1, 1, 'yes'],
[1, 0, 1, 2, 'yes'],
[1, 0, 1, 2, 'yes'],
[2, 0, 1, 2, 'yes'],
[2, 0, 1, 1, 'yes'],
[2, 1, 0, 1, 'yes'],
[2, 1, 0, 2, 'yes'],
[2, 0, 0, 0, 'no']
]
"""
年龄:0:青年、1:中年、2:老年
工作:0:否、1:是
房子:0:否、1:是
信贷情况:0:一般好、1:好、2:非常好
类别:no:否、yes:是
"""
labels = ['年龄','工作','房子','信贷情况']
return dataSet,labels
def calcShannonEntropy(dataSet):
"""
计算香农熵
:return: ShannonEntropy:香农熵
"""
numEntires = len(dataSet)
labelCounts = {}#用字典保存每个标签出现的次数
for featVec in dataSet:#遍历每组特征向量
currentLabel = featVec[-1]
if currentLabel not in labelCounts.keys():#如果标签没有加入字典则加入进行
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
shannonEntroy = 0.0#香农熵
for key in labelCounts:
prob = float(labelCounts[key])/numEntires
shannonEntroy -= prob*log(prob,2)#log(prob,2)-->log2prob
return shannonEntroy
def splitDataSet(dataSet, axis, value):
retDataSet = []#创建返回的数据集列表
for featVec in dataSet:#遍历数据集
#print("1",featVec)
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]#去掉axis特征
#print("2",reducedFeatVec)
reducedFeatVec.extend(featVec[axis+1:])#将符合条件的添加到返回的数据集
#print("3",reducedFeatVec)
retDataSet.append(reducedFeatVec)
#print("4", retDataSet)
return retDataSet#返回划分后的数据集
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1#特征数量
baseEntropy = calcShannonEntropy(dataSet)#计算数据集的香农熵
bestInfoGain = 0.0#信息增益
bestFeature = -1#最优特征的索引值
for i in range(numFeatures):#遍历所有特征
#获取dataSet的第i个所有特征
featList = [example[i] for example in dataSet]
#print("featList:",featList)
uniqueVals = set(featList)#创建set集合{},元素不可重复
#print(uniqueVals)
newEntropy = 0.0#经验条件熵
for value in uniqueVals:#计算信息增益
#print("i:",i,",value:",value)
subDataSet = splitDataSet(dataSet, i, value)#subDataSet划分后的子集
prob = len(subDataSet) / float(len(dataSet))#计算子集的概率
newEntropy += prob * calcShannonEntropy(subDataSet)#根据公式计算经验条件熵
infoGain = baseEntropy - newEntropy#信息增益
print("第%d个特征的增益为%.3f" % (i, infoGain)) #打印每个特征的信息增益
if (infoGain > bestInfoGain):#计算信息增益
bestInfoGain = infoGain#更新信息增益,找到最大的信息增益
bestFeature = i#记录信息增益最大的特征的索引值
return bestFeature#返回信息增益最大的特征的索引值
if __name__ == '__main__':
dataSet,labels = creatDataset()
chooseBestFeatureToSplit(dataSet)
决策树的生成和裁剪
决策树的生成算法有ID3、C4.5、CART
ID3算法
ID3算法最早是由罗斯昆(J. Ross Quinlan)于1975年在悉尼大学提出的一种分类预测算法,算法的核心是“信息熵”。ID3算法通过计算每个属性的信息增益,认为信息增益高的是好属性,每次划分选取信息增益最高的属性为划分标准,重复这个过程,直至生成一个能完美分类训练样例的决策树。
决策树是对数据进行分类,以此达到预测的目的。该决策树方法先根据训练集数据形成决策树,如果该树不能对所有对象给出正确的分类,那么选择一些例外加入到训练集数据中,重复该过程一直到形成正确的决策集。决策树代表着决策集的树形结构。
决策树由决策结点、分支和叶子组成。决策树中最上面的结点为根结点,每个分支是一个新的决策结点,或者是树的叶子。每个决策结点代表一个问题或决策,通常对应于待分类对象的属性。每一个叶子结点代表一种可能的分类结果。沿决策树从上到下遍历的过程中,在每个结点都会遇到一个测试,对每个结点上问题的不同的测试输出导致不同的分支,最后会到达一个叶子结点,这个过程就是利用决策树进行分类的过程,利用若干个变量来判断所属的类别。(来着百度百科)
ID3算法Python实现
from math import log
import operator
def creatDataset():
"""
创建数据集,用于练习香农公式
:return: dataSet:数据集,labels:分类属性
"""
dataSet = [
[0, 0, 0, 0, 'no'],
[0, 0, 0, 1, 'no'],
[0, 1, 0, 1, 'yes'],
[0, 1, 1, 0, 'yes'],
[0, 0, 0, 0, 'no'],
[1, 0, 0, 0, 'no'],
[1, 0, 0, 1, 'no'],
[1, 1, 1, 1, 'yes'],
[1, 0, 1, 2, 'yes'],
[1, 0, 1, 2, 'yes'],
[2, 0, 1, 2, 'yes'],
[2, 0, 1, 1, 'yes'],
[2, 1, 0, 1, 'yes'],
[2, 1, 0, 2, 'yes'],
[2, 0, 0, 0, 'no']
]
"""
年龄:0:青年、1:中年、2:老年
工作:0:否、1:是
房子:0:否、1:是
信贷情况:0:一般好、1:好、2:非常好
类别:no:否、yes:是
"""
labels = ['年龄','工作','房子','信贷情况']
return dataSet,labels
def calcShannonEntropy(dataSet):
"""
计算香农熵
:return: ShannonEntropy:香农熵
"""
numEntires = len(dataSet)
labelCounts = {}#用字典保存每个标签出现的次数
for featVec in dataSet:#遍历每组特征向量
currentLabel = featVec[-1]
if currentLabel not in labelCounts.keys():#如果标签没有加入字典则加入进行
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
shannonEntroy = 0.0#香农熵
for key in labelCounts:
prob = float(labelCounts[key])/numEntires
shannonEntroy -= prob*log(prob,2)#log(prob,2)-->log2prob
return shannonEntroy
def splitDataSet(dataSet, axis, value):
"""
为计算条件信息熵,划分数据集
:param dataSet: 要划分的数据集
:param axis: 根据第几个特征划分
:param value: 特征值
:return:划分完的数据集
"""
retDataSet = []#创建返回的数据集列表
for featVec in dataSet:#遍历数据集
#print("1",featVec)
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]#去掉axis特征
#print("2",reducedFeatVec)
reducedFeatVec.extend(featVec[axis+1:])#将符合条件的添加到返回的数据集
#print("3",reducedFeatVec)
retDataSet.append(reducedFeatVec)
#print("4", retDataSet)
return retDataSet#返回划分后的数据集
def chooseBestFeatureToSplit(dataSet):
"""
选择最好的特征划分
:param dataSet:
:return: 最好特征的索引值
"""
numFeatures = len(dataSet[0]) - 1#特征数量
baseEntropy = calcShannonEntropy(dataSet)#计算数据集的香农熵
bestInfoGain = 0.0#信息增益
bestFeature = -1#最优特征的索引值
for i in range(numFeatures):#遍历所有特征
#获取dataSet的第i个所有特征
featList = [example[i] for example in dataSet]
#print("featList:",featList)
uniqueVals = set(featList)#创建set集合{},元素不可重复
#print(uniqueVals)
newEntropy = 0.0#经验条件熵
for value in uniqueVals:#计算信息增益
#print("i:",i,",value:",value)
subDataSet = splitDataSet(dataSet, i, value)#subDataSet划分后的子集
prob = len(subDataSet) / float(len(dataSet))#计算子集的概率
newEntropy += prob * calcShannonEntropy(subDataSet)#根据公式计算经验条件熵
infoGain = baseEntropy - newEntropy#信息增益
#print("第%d个特征的增益为%.3f" % (i, infoGain)) #打印每个特征的信息增益
if (infoGain > bestInfoGain):#计算信息增益
bestInfoGain = infoGain#更新信息增益,找到最大的信息增益
bestFeature = i#记录信息增益最大的特征的索引值
return bestFeature#返回信息增益最大的特征的索引值
def majorityCnt(classList):
"""
统计类标签中出现最多的元素
:param classList:类标签列表
:return: 出现最多的元素
"""
classCount = {}#统计classList中每个元素出现的次数
for vote in classList:
if vote not in classCount.keys():classCount[vote] = 0
classCount += 1
#根据字典值的降序排序
sortedClassCount = sorted(classCount.items(),key = operator.itemgetter(1),reverse=True)
return sortedClassCount[0][0]
def createTree(dataSet,labels,featLabels):
"""
构造决策树
:param dataSet:训练数据集
:param labels:分类属性标签
:param featLabels:储存选择的最优特征标签
:return: 决策树
"""
classList = [example[-1] for example in dataSet]#取分类标签
print("classList:",classList)
#如果类别完全相同则停止继续划分
if classList.count(classList[0]) == len(classList):
print("类别完全相同")
return classList[0]
#遍历完所有特征时返回出现次数最多的类标签
if len(dataSet[0]) == 1:
print("遍历完所有特征")
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet)#选择最优特征
print("bestFeat:",bestFeat)
bestFeatLabel = labels[bestFeat]#最优特征的标签
print("bestFeatLabel:", bestFeatLabel)
featLabels.append(bestFeatLabel)
print("featLabels",featLabels)
myTree = {bestFeatLabel: {}}#据最优特征的标签生成树
print("myTree",myTree)
del (labels[bestFeat])#删除已经使用特征标签
print("labels:",labels)
featValues = [example[bestFeat] for example in dataSet]#得到训练集中所有最优特征的属性值
uniqueVals = set(featValues)#去掉重复的属性值
print("uniqueVals",uniqueVals)
for value in uniqueVals:#遍历特征,创建决策树。
print("value:",value)
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels)
print("myTree:",myTree)
return myTree
if __name__ == '__main__':
dataSet,labels = creatDataset()
featLabels = []
myTree = createTree(dataSet,labels,featLabels)
print(myTree)
运行结果
决策树可视化
使用matplotlib实现决策树可视化
下面介绍需要使用到的API
getNumLeafs:获取决策树叶子节点的数目
getTreeDepth:获取决策树的层数
plotNode:绘制节点
plotMidText:标注有向属性值
plotTree:绘制决策树
createPlot:创建绘制面板
绘制决策树代码(本文代码参照点击打开链接)
#绘制决策树
def getNumLeafs(myTree):
"""
获取决策树叶子节点数目
:param myTree:
:return:决策树叶子节点数目
"""
numLeafs = 0
#python3中myTree.keys()返回的是dict_keys,不在是list,所以不能使用myTree.keys()[0]的方法获取结点属性,可以使用list(myTree.keys())[0]
firstStr = next(iter(myTree))
#print("firstStr:",firstStr)
secondDict = myTree[firstStr]#获取下一组字典
#print("secondDict:",secondDict)
for key in secondDict.keys():
#测试该节点是否是字典,如果不是则说明为叶子节点
if type(secondDict[key]).__name__ == 'dict':
#print("字典")
numLeafs += getNumLeafs(secondDict[key])
else:
#print("叶子节点")
numLeafs += 1
#print("numLeafs:",numLeafs)
return numLeafs
def getTreeDepth(myTree):
"""
获取决策树的深度
:param myTree:
:return:决策树的深度
"""
maxDepth = 0
firstStr = next(iter(myTree))
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 plotNode(nodeTxt, centerPt, parentPt, nodeType):
arrow_args = dict(arrowstyle="<-")#定义箭头格式
font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)#设置中文字体
#绘制结点
createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',xytext=centerPt, textcoords='axes fraction',va="center", ha="center", bbox=nodeType, arrowprops=arrow_args, FontProperties=font)
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, va="center", ha="center", rotation=30)
def plotTree(myTree, parentPt, nodeTxt):
decisionNode = dict(boxstyle="sawtooth", fc="0.8")#设置结点格式
leafNode = dict(boxstyle="round4", fc="0.8")#设置叶结点格式
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, nodeTxt)#标注有向边属性值
plotNode(firstStr, cntrPt, parentPt, decisionNode)#绘制结点
secondDict = myTree[firstStr]#下一个字典,也就是继续绘制子结点
plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD#y偏移
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
fig.clf()#清空fig
axprops = dict(xticks=[], yticks=[])
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)#去掉x、y轴
plotTree.totalW = float(getNumLeafs(inTree))#获取决策树叶结点数目
plotTree.totalD = float(getTreeDepth(inTree))#获取决策树层数
plotTree.xOff = -0.5/plotTree.totalW
plotTree.yOff = 1.0#x偏移
plotTree(inTree, (0.5,1.0), '')#绘制决策树
plt.show()
运行结果
利用决策树进行分类
def classify(inputTree,featLabels,testVec):
"""
使用决策树分类
:param inputTree: 已经生成的决策树
:param featLabels: 存储选择的最优特征标签
:param testVec: 测试数据列表
:return: 分类结果
"""
firstStr = next(iter(inputTree))
print("firstStr:",firstStr)
secondDict = inputTree[firstStr]
print("secondDict:",secondDict)
featIndex = featLabels.index(firstStr)
print("featIndex:",featIndex)
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__':
dataSet,labels = creatDataset()
featLabels = []
myTree = createTree(dataSet,labels,featLabels)
print(myTree)
testVec = [0,1]
print("featLabels",featLabels)
result = classify(myTree,featLabels,testVec)
if result == 'yes':
print("放贷")
if result == 'no':
print("不放贷")
createPlot(myTree)
运行结果
存储决策树
如果每次使用分类器都创建一次决策树逻辑上说不通,所以需要对决策树进行存储
存储和载入决策树的Python代码
def storeTree(inputTree,filename):
"""
保存决策树
:param inputTree: 要保存的决策树
:param filename: 保存的文件名
:return: None
"""
with open(filename,'wb') as fw:
pickle.dump(inputTree,fw)
def grabTree(filename):
"""
导入决策树
:param filename: 保存的决策树文件名
:return: 决策树
"""
fr = open(filename,'rb')
return pickle.load(fr)
if __name__ == '__main__':
# dataSet,labels = creatDataset()
# featLabels = []
# myTree = createTree(dataSet,labels,featLabels)
# print(myTree)
#保存决策树
#storeTree(myTree,'classifierStorage.txt')
#导入决策树
print(grabTree('classifierStorage.txt'))