利用八周时间,完成以下任务:
决策树模型是一种描述对实例进行分类的树形结构。其由结点(node)和有向(directed edge)组成。结点有两种类型:内部结点(internal node)和叶结点(leaf node)。内部结点表示一个特征或属性(features),叶结点表示一个类(labels)。
用决策树对需要测试的实例进行分类:从根节点开始,对实例的某一特征进行测试,根据测试结果,将实例分配到其子结点;这时,每一个子结点对应着该特征的一个取值。如此递归地对实例进行测试并分配,直至达到叶结点。最后将实例分配到叶结点的类中。
决策树模型可以认为是 if-then 规则的集合,也可以认为是定义在特征空间与类空间上的条件概率分布。
决策树学习通常包括 3 个步骤:特征选择、决策树的生成和决策树的修剪。
信息熵
划分数据集的原则是:将无序的数据变得更加有序。选择信息增益最大的特征来划分数据集。所用到的公式如下:
数据集的信息熵:
H ( D ) = − ∑ i = 0 N ∣ C k ∣ ∣ D ∣ l o g 2 ∣ C k ∣ ∣ D ∣ H(D)=-\sum_{i=0}^N{\frac {|C_k|}{|D|}log_2{{\frac {|C_k|} {|D|} } }} H(D)=−i=0∑N∣D∣∣Ck∣log2∣D∣∣Ck∣
其中 C k C_k Ck表示集合 D D D中属于第 k k k类样本的样本子集。
针对某个特征 A A A ,对于数据集 D D D 的条件熵 H ( D ∣ A ) H(D|A) H(D∣A) 为:
H ( D ∣ A ) = ∑ i = 1 N ∣ D i ∣ ∣ D ∣ H ( D i ) H(D|A)=\sum_{i=1}^N{\frac{|D_i|}{|D|}}H(D_i) H(D∣A)=i=1∑N∣D∣∣Di∣H(Di)
其中 D i D_i Di 表示 D D D 中特征 A A A 取第 i i i 个值的样本子集。
信息增益 = 信息熵 - 条件熵:
G a i n ( D , A ) = H ( D ) − H ( D ∣ A ) Gain(D,A)=H(D)-H(D|A) Gain(D,A)=H(D)−H(D∣A)
信息增益越大表示使用特征 A A A 来划分所获得的“纯度提升越大”。
如何构造一个决策树?
我们使用 createBranch() 方法,如下所示:
def createBranch():
'''
此处运用了迭代的思想。
'''
检测数据集中的所有数据的分类标签是否相同:
If so return 类标签
Else:
寻找划分数据集的最好特征(划分之后信息熵最小,也就是信息增益最大的特征)
划分数据集
创建分支节点
for 每个划分的子集
调用函数 createBranch (创建分支的函数)并增加返回结果到分支节点中
return 分支节点
项目概述
根据以下 2 个特征,将动物分成两类:鱼类和非鱼类。
开发流程
Step1:收集数据
Step2:准备数据
不浮出水面是否可以生存: 1 代表 ”是“ ; 0 代表 ”否“
是否有脚蹼:1 代表 ”是“ ; 0 代表 ”否“
属于鱼类: yes 代表 ”是“ ; no 代表 ”否“
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
Step3:分析数据
可以使用任何方法,构造树完成之后,我们可以将树画出来。
计算给定数据集的香农熵的函数如下:
import math
def calcShannonEnt(dataSet):
# 求list的长度,表示计算参与训练的数据量
numEntries=len(dataSet)
# 计算分类标签label出现的次数
labelCounts={}
# 对每组特征向量进行统计
for featVec in dataSet:
# 将当前实例的标签存储,即每一行数据的最后一个数据代表的是标签
currentLabel=featVec[-1]
# 为所有可能的分类创建字典,如果当前的键值不存在,则扩展字典并将当前键值加入字典。每个键值都记录了当前类别出现的次数。
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel]=0
labelCounts[currentLabel]+=1
# 对于 label 标签的占比,求出 label 标签的香农熵
shannoEnt=0.0
for key in labelCounts:
# 使用所有类标签的发生频率计算类别出现的概率。
prob=float(labelCounts[key])/numEntries
# 计算香农熵,以 2 为底求对数
shannoEnt-=prob*math.log(prob,2)
return shannoEnt
dataSet,label=creatDataSet()
print(calcShannonEnt(dataSet))
划分数据集:
按照给定特征划分数据集
将指定特征的特征值等于 value 的行剩下列作为子数据集。
#划分数据集
def spilitDataSet(dataSet,index,value):
"""
splitDataSet(通过遍历dataSet数据集,求出index对应的colnum列的值为value的行)
就是依据index列进行分类,如果index列的数据等于 value的时候,就要将 index 划分到我们创建的新的数据集中
Args:
dataSet 数据集 待划分的数据集
index 表示每一行的index列 划分数据集的特征
value 表示index列对应的value值 需要返回的特征的值。
Returns:
index列为value的数据集【该数据集需要排除index列】
"""
# 创建返回的数据集列表
retDataSet=[]
# 遍历数据集
for featVec in dataSet:
# 判断index列的值是否为value
if featVec[index]==value:
# 去掉axis特征
reduceFeatVec=featVec[:,index]
# 将符合条件的添加到返回的数据集
reduceFeatVec.extend(featVec[index+1,:])
接下来我们将遍历整个数据集,循环计算香农熵和splitDataSet()函数,找到最好的特征划分方式。熵计算将会告诉我们如何划分数据集是最好的数据组织方式。
# 选择最好的数据集划分方式
def chooseBeastFeatureToSpilit(dataSet):
"""
chooseBestFeatureToSplit(选择最好的特征)
Args:
dataSet 数据集
Returns:
bestFeature 最优的特征列
"""
# 求第一行有多少列的 Feature, 最后一列是label列
numFeatures=len(dataSet[0])-1
# 数据集的香农熵
baseEntropy=calcShannonEnt(dataSet)
# 最优的信息增益值, 和最优的Featurn编号
bestInfoGain, bestFeature = 0.0, -1
# 记录信息增益和最优特征的索引值
for i in range(numFeatures):
# 获取对应的feature下的所有数据
feaList=[example[i] for example in dataSet]
# 获取剔重后的集合,使用set对list数据进行去重
uniqleVals=set(feaList)
# 创建一个临时的信息熵
newEntropy=0.0
# 遍历某一列的value集合,计算该列的信息熵
# 遍历当前特征中的所有唯一属性值,对每个唯一属性值划分一次数据集,计算数据集的新熵值,并对所有唯一特征值得到的熵求和。
for value in uniqleVals:
subDataSet=spilitDataSet(dataSet,i,value)
#计算概率
prob=len(subDataSet)/float(len(dataSet))
# 计算条件熵
newEntropy+=prob*calcShannonEnt(subDataSet)
# gain[信息增益]: 划分数据集前后的信息变化, 获取信息熵最大的值
# 信息增益是熵的减少或者是数据无序度的减少。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。
infoGain=baseEntropy-newEntropy
print('infoGain=', infoGain, ' bestFeature=', i," ", baseEntropy, ' ',newEntropy)
if infoGain>bestFeature:
bestInfoGain=infoGain
bestFeature=i
return bestFeature
dataSet,label=creatDataSet()
print(chooseBeastFeatureToSpilit(dataSet))
在上述chooseBestFeatureToSplit()函数中调用的dataSet需要满足一定的要求:
Step4:训练算法
采用多数表决的方法决定该叶子节点的分类
#多数表决法
def majorityCnt(classList):
"""
majorityCnt(选择出现次数最多的一个结果)
Args:
classList label列的集合
Returns:
bestFeature 最优的特征列
"""
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)
# 返回classList中出现次数最多的元素
return sortedClassCount[0][0]
该方法与第2章classify0部分的投票表决代码非常类似,该函数使用分类名称的列表,然后创建键值为classList中唯一值的数据字典,字典对象存储了classList中每个类标签出现的频率,最后利用operator操作键值排序字典,并返回出现次数最多的分类名称。
由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据将被向下传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据。
因此我们可以采用递归的原则处理数据集。
递归结束的条件是:
#创建决策树
def creatTree(dataSet,labels):
"""
创建决策树
- - - -
dataSet - 数据集
labels - 标签列表
"""
# 取分类标签(是否属于鱼类)
classList=[example[-1] for example in dataSet]
# 如果数据集的最后一列的第一个值出现的次数=整个集合的数量,也就说只有一个类别,就只直接返回结果就行
# 第一个停止条件:所有的类标签完全相同,则直接返回该类标签。
# count() 函数是统计括号中的值在list中出现的次数
if classList.count(classList[0])==len(classList):
return classList[0]
# 如果数据集只有1列,那么最初出现label次数最多的一类,作为结果
# 第二个停止条件:使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组。
if len(dataSet[0])==1:
return majorityCnt(classList)
# 选择最优特征
bestFeat=chooseBeastFeatureToSpilit(dataSet)
# 最优特征的标签
bestFeatLabel=labels[bestFeat]
# 初始化myTree
myTree={bestFeatLabel:{}}
# 注:labels列表是可变对象,在PYTHON函数中作为参数时传址引用,能够被全局修改
# 所以这行代码导致函数外的同名变量被删除了元素,造成例句无法执行,提示'no surfacing' is not in list
# 删除已经使用特征标签
del(labels[bestFeat])
# 得到训练集中所有最优特征的属性值
featValues = [example[bestFeat] for example in dataSet]
# 去掉重复的属性值
uniqueVals=set(featValues)
# 遍历特征,创建决策树
for value in uniqueVals:
#复制类标签(保证每次调用函数createTree()时不改变原始列表的内容)
subLabels = labels[:]
# 遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createTree()
myTree[bestFeatLabel][value] = creatTree(spilitDataSet(dataSet, bestFeat, value), subLabels)
return myTree
myDat,labels=creatDataSet()
print(creatTree(myDat,labels))
#{'flippers': {0: 'no', 1: {'no surfacing': {0: 'no', 1: 'yes'}}}}
使用Matplotlib库创建树形图
# author:answer time:2019/11/23
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
from 决策树 import jc1
#获取决策树叶子结点的数目
def getNumLeafs(myTree):
"""
获取叶节点的数目
- - - -
myTree - 决策树
"""
# 初始化叶子
numleafs=0
# 获取结点属性
firstStr=list(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):
"""
获取叶节点的深度
- - - -
myTree - 决策树
"""
# 初始化决策树深度
maxDepth=0
firstStr=list(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 plotNode(nodeTxt,centerPt,parentPt,nodeType):
"""
绘制结点
- - - -
nodeTxt - 结点名
centerPt - 文本位置
parentPt - 标注的箭头位置
nodeType - 结点格式
"""
# 定义箭头格式
arrow_args=dict(arrowstyle="<-")
# 设置中文字体
font = FontProperties(fname=r'C:/Windows/Fonts/simsun.ttc', size=16)
# 绘制节点
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):
"""
计算在父子节点间填充文本信息的位置
- - - -
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):
"""
递归绘制决策树
- - - -
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]
# y偏移
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
fig = plt.figure(1, facecolor='white')
# 清空fig
fig.clf()
axprops = dict(xticks=[], yticks=[])
# 去掉x、y轴
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)
# 获取决策树叶结点数目
plotTree.totalW = float(getNumLeafs(inTree))
# 获取决策树层数
plotTree.totalD = float(getTreeDepth(inTree))
# x偏移
plotTree.xOff = -0.5/plotTree.totalW;
# y偏移
plotTree.yOff = 1.0;
# 绘制决策树
plotTree(inTree, (0.5,1.0), '')
plt.show()
if __name__ == "__main__":
dataSet,labels=jc1.creatDataSet()
myTree=jc1.creatTree(dataSet,labels)
print(myTree)
createPlot(myTree)
Step5:测试算法
存储决策树
#使用pickle模块存储决策树
def storeTree(inputTree,filaname):
import pickle
fw=open(filaname,'wb')
pickle.dump(inputTree,fw)
fw.close()
def grabTree(filename):
import pickle
fr=open(filename,'rb')
return pickle.load(fr)
if __name__=='__main__':
dataSet,labels=creatDataSet()
labelsCopy=labels[:]
myTree=creatTree(dataSet,labels)
print(myTree)
storeTree(myTree,'classifierStorage.pkl')
tree=grabTree('classifierStorage.pkl')
print(tree)
输出结果:
使用决策树执行分类
#使用决策树的分类函数
def classify(inputTree,featLabels,testVec):
# 获取tree的根节点对于的key值
firstStr=list(inputTree.keys())[0]
# 通过key得到根节点对应的value
secondDict=inputTree[firstStr]
# 判断根节点名称获取根节点在label中的先后顺序,这样就知道输入的testVec怎么开始对照树来做分类
featIndx=featLabels.index(firstStr)
for key in secondDict.keys():
if testVec[featIndx]==key:
if type(secondDict[key]).__name__=='dict':
classLabel=classify(secondDict[key],featLabels,testVec)
else:classLabel=secondDict[key]
return classLabel
Step6:使用算法
if __name__=='__main__':
dataSet,labels=creatDataSet()
labelsCopy=labels[:]
myTree=creatTree(dataSet,labels)
print("[0,0] ",classify(myTree,labelsCopy,[0,0]))
print("[0,1] ",classify(myTree,labelsCopy,[0,1]))
print("[1,0] ",classify(myTree,labelsCopy,[1,0]))
print("[1,1] ",classify(myTree,labelsCopy,[1,1]))
输出结果:
项目概述
隐形眼镜类型包括硬材质、软材质以及不适合佩戴隐形眼镜。我们需要使用决策树预测患者需要佩戴的隐形眼镜类型。
开发流程
Step1:收集数据
提供的文本文件:
文本文件数据格式如下:
Step2:准备数据
解析tab键分隔的数据行
if __name__=='__main__':
# 加载隐形眼镜相关的 文本文件 数据
fr=open('lenses.txt')
# 解析数据,获得 features 数据
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
# 得到数据的对应的 Labels
lensesLabels=['age', 'prescript', 'astigmatic', 'tearRate']
Step3:分析数据
快速检查数据,确保正确地解析数据内容,使用createPlot()函数绘制
最终的树形图
同案例1,计算香农熵、按照给定的特征划分数据集、选择最好的数据集划分方式。
Step4:训练算法
使用createTree()函数
# 使用上面的创建决策树的代码,构造预测隐形眼镜的决策树
lensesTree = creatTree(lenses, lensesLabels)
storeTree(lensesTree, 'classifierLenes.pkl')
lensesTree = grabTree('classifierLenes.pkl')
print(lensesTree)
输出结果:
Step5:测试算法
编写测试函数验证决策树可以正确分类给定的数据实例
Step6:使用算法
存储树的数据结构,以便下次使用时无需重新构造树
if __name__=='__main__':
# 加载隐形眼镜相关的 文本文件 数据
fr=open('lenses.txt')
# 解析数据,获得 features 数据
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
# 得到数据的对应的 Labels
lensesLabels=['age', 'prescript', 'astigmatic', 'tearRate']
# 使用上面的创建决策树的代码,构造预测隐形眼镜的决策树
lensesTree = creatTree(lenses, lensesLabels)
#储存决策树
storeTree(lensesTree, 'classifierLenes.pkl')
print(lensesTree)
jc2.createPlot(lensesTree)
输出结果:
决策树的一些优点:
决策树的一些缺点:
参考文章
https://blog.csdn.net/lsgo_myp/article/details/103079044
https://blog.csdn.net/c406495762/article/details/76262487
https://github.com/apachecn/AiLearning/tree/master/docs/ml