一、什么是决策树
决策树(Decision Tree)是有监督学习中的一种算法,并且是一种节本的分类与回归的方法。即决策树有两种:分类树和回归树。
那什么事决策树了? 简单点说就是二元判定,从头到尾逐次判定其归属类型。
从上述案例,我们很容易理解:决策树算法的本质就是二元判定的属性结构,我们可以通过一些静心设计的问题,对数据进行分类。以下是关于决策树需要理解的几个概念:
节点 | 说明 |
根节点 | 没有进边,有出边 |
中间节点 | 既有进边也有出边,但进边有且仅有一条,出边也可以有很多条 |
叶节点 | 只有进边,没有出边,进边有且仅有一条。每个叶节点都是一类类别标签 |
*父节点和子节点 | 在两个相连的节点中,更靠近根节点的是父节点,另一个则是子节点。两者是相对的 |
决策树看作是一个if-then规则的集合。将决策树转换成if-then规则的过程是这样的:
- 由决策树的根节点到叶节点的每一条路径构建一条规则
- 路径上中间节点的特征对应着规则的条件,叶节点的类标签对应着规则的结论
决策树的路径或者其对应的if-then规则集合有一个重要的性质:互斥并且完备。也就是说,每一个实例都被有且仅有一条路径或者规则所覆盖。这里的覆盖是指实例的特征与路径上的特征一致,或实例满足规则的条件。
二、决策树的构建准备工作
使用决策树做分类的每一步步骤都很重要,首先我们要手机足够多的数据,如果数据收集不到位,将会导致没有足够的特征去构建错误率低的决策树。数据特征充足,但是不知道用哪些特征好,也会导致最终无法构建出分类效果好的决策树。从算法方面来看的话,决策树的构建就是我们的核心内容。
决策树如何构建呢?通常,这一过程可以概括为3个步骤:特征选择、决策树的生成和决策树的剪枝。
1. 特征选择
特征选择就是决定用哪个特征来划分特征空间,其目的在于选取对训练数据具有分类能力的特征。这样可以提高决策树学习的效率。如果利用一个特征进行分类的结果与随机分类的结果没有很大的差别,则称这个特征是没有分类能力的,经验上扔掉这些特征对决策树学习的精度影响不会很大。
那如何来选择最优的特征进行划分呢?一般而言,随着划分过程不断进行,我们希望决策树的分支节点所包含的样本仅可能属于同一类别,也就是节点的纯度(purity)越来越高。
下面是三个图表示的是纯度越来越低的过程,最后一个表示的是纯度最低的状态。
在实际使用中,我们衡量的常常是不纯度。度量不纯度的指标有很多种,如:熵、增益率、基尼指数。
这里我们使用的是熵,即香农熵,这个名字来源于信息论之父 克劳德-香农。
①香农熵及计算函数
熵定义为信息的期望值。在信息论与概率统计中,熵是表示随机变量不确定性的度量。
假定当前样本集合D中一共有n类样本,第i类样本为xi,那么xi的信息定义为:l(xi) = -log2p(xi)
其中p(xi) 是选择该分类的概率。
通过上式,我们可以得到所有类别的信息。为了计算熵,我们需要计算所有类别所有可能值包含的信息期望值(数学期望),通过下面的公式得到:
Ent(D)的值越小,则D的不纯度就越低。
香农熵的python代码如下:
def calEnt(dataSet): """ 函数功能:计算香农熵 参数说明: dataSet:原始数据集 返回: ent:香农熵的值 """ n = dataSet.shape[0] # 数据集的总行数 iset = dataSet.iloc[:, -1].calue_counts() # 标签的所有类别 p = iset/n # 每一类标签的占比 ent = (-p*np.log2(p)).sum() # 信息熵(此函数应该要导入numpy库, from numpy as np) return ent
下面以海洋生物数据为例来构建数据集,并计算香农熵。
首先创建海洋生物数据集
import pandas as pd import numpy as np def createdataset(): """ 根据已知数据创建数据集 """ row_data = {'no surfacing': [1, 1, 1, 0, 0], 'flippers': [1, 1, 0, 1, 1], 'fish': ['yes', 'yes', 'no', 'no', 'no']} dataSet = pd.DataFrame(row_data) return dataSet
带入数据集后,可以看到其熵结果97.1%。熵越高,信息的不纯度就越高。也就是混合的数据就越多。
②信息增益
信息增益(information Gain)的计算公式其实就是父节点的信息熵与其下所有子节点总信息熵之差。但是这里要注意的是,此时计算子节点的总信息熵不能简单求和,而要求在求和汇总之前进行修正。假设离散属性a有V个可能的取值{a1,a2,......,aV},若使用a对样本数据集D进行划分,则会产生V个分支节点,其中第V个分支节点包含了D找那个所有在属性a上取值为au的样本,即为Du。我们可根据信息熵的计算公式计算出Du的信息熵,再考虑到不同的分支节点所包含的样本数不同,给分支节点赋予权重|Du| / |D|,这就是所谓的修正。
所以信息增益的计算公式为:
那我们手动计算一下,海洋生物数据集中第0列的信息增益:
用同样的方法,可以把第1列的信息增益也算出来,结果为0.17。
2. 数据集最佳切分函数
划分数据集的最大准则是选择最大信息增益,也就是信息下降最快的方向。
# 选择最优的列进行切分 def bestSplit(dataSet): """ 函数功能:根据信息增益选择出最佳数据集切分的列 参数说明: dataSet:原始数据集 返回: axis:数据集最佳切分列的索引 """ baseEnt = calEnt(dataSet) # 计算原始数据集的信息熵 bestGain = 0 # 初始化信息增益 axis = -1 # 初始化最佳切分列,标签 # 列循环 for i in range(dataSet.shape[1] -1): # 对特征的每一列进行循环 levels = dataSet.iloc[:, i].value_counts().index # 提取出当前列的所有取值 ents = 0 # 初始化子节点的信息熵 for j in lecvels: # 对房钱列的每一个取值进行循环 childSet = dataSet[dataSet.iloc[:, i] == j] # 某一个子节点的dataframe ent = calEnt(childSet) # 计算某一个子节点的信息熵 ents += (childSet.shape[0]/dataSet.shape[0]*ent # 计算当前列的信息熵 print(f'第{i}列的信息熵为{ents}') infoGain = baseEnt - ents # 计算当前列的信息增益 if (infoGain > baseGain): bestGain = infoGain # 选择最大信息增益 axis = i # 最大信息增益所在列的索引 return axis
通过上面手动计算,我们知道:第0列的信息增益为0.42,第1列的信息增益为0.17,所以我们应该选择第0列进行切分数据集。
3. 按照给定列切分数据集
通过最佳切分函数返回最佳切分列的索引,我们就可以根据这个索引,构建一个按照给定列切分数据集的函数
def muSplit(dataSet, axis, value): """ 函数功能:按照给定的列划分数据集 参数说明: dataSet:原始数据集 axis:指定的列索引 value:指定的属性值 返回: redataSet:按照指定列索引和属性值切分后的数据集 """ col = dataSet.columns[axis] redataSet = dataSet.loc[dataSet[col]==value, :].drop(col, axis=1) return redataSet
验证函数,以axis=0, value=1为例
mySplit(dataSet, 0, 1)
三、递增构建决策树
目前我们学习了从数据集构造决策树算法所需要的子功能模块,其工作原理如下:得到原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据集被向下传递到树的分支的下一个结点。在这个结点上,我们可以再次划分数据。因此我们可以采用递归的原则处理数据集。
决策树生成算法递归地产生决策树,知道不能继续下去为止。这样产生的树往往对训练数据的分类很准确,但对未知的测试数据的分类却没有那么准确,即出现过拟合现象。过拟合的原因在于学习时过多地考虑如何提高对训练数据的正确分类,从而构建出过于复杂的决策树。解决这个问题的办法是考虑决策树的复杂度,对已生成的决策树进行简化,也就是常说的剪枝处理。
1. ID3算法
构建决策树的算法有很多,比如ID3、C4.5和CART,基于《机器学习实战》这本书,我们选择ID3算法。
ID3算法的核心是在决策树各个节点上对应信息增益最大准则选择特征,递归地构建决策树。具体方法是:从根节点开始,对节点计算所有可能的特征的信息增益,选择信息增益最大的特征作为节点的特征,由该特征的不同取值建立子节点;再对子节点递归地调用以上方法,构建决策树;直到所有特征的信息增益均很小或没有没有特征可以选择为止。最后得到一个决策树。
递归结束的条件是:程序遍历完所有的特征列,或者每个分支下的所有实例都具有相同的分类。如果所有实例具有相同分类,则得到一个叶节点。任何到达叶节点的数据必然属于叶节点的分离,即叶节点里面必须是标签。
2. 编写发代码构建决策树
def createTree(dataSet): """ 函数功能:基于最大信息增益切分数据集,递归构建决策树 参数说明: dataSet:原始数据集(最后一列是标签) 返回: myTree:字典形式的树 """ featlist = list(dataSet.cloumns) # 提取出数据集所有的列 classlist = dataSet.iloc[:, -1].value_counts() # 获取最后一列类标签 # 判断最多标签数据是否等于数据集行数,或者数据集是否只有一列 if classlist[0] == dataSet.shape[0] or dataSet.shape[1] == 1: return classlist.index[0] # 如何是,返回类标签 axis = bestSplit(dataSet) # 确定出当前最佳列切分列的索引 bestfeat = featlist[axis] # 获取该索引对应的特征 myTree = {bestfeat:{}} # 采用字典嵌套的方式存储树信息 del featlist[axis] # 删除当前特征 valuelist = set(dataSet.iloc[:, axis]) # 提取最佳切分列所有属性值 forvalue in valuelist: #对每一个属性值递归建树 myTree[bestfeat][value] = createTree(mySplit(dataSet, axis, value)) return myTree
查看函数运行结果:
createTree(dataSet)
四、决策树的存储
构造决策树是很耗时的任务,即使处理很小的数据集,也要花费几秒的时间,如何数据量很大,将会耗费很多计算时间。因此为了节省时间,建好树之后立马将其保存,后续使用直接调用即可。
可直接采用numpy中的save()函数,它可以直接把字典形式的数据保存为.npy文件,调用的时候直接使用load()函数调用。
五、使用决策树执行分类
def classify(inputTree,labels, testVec):
"""
函数功能:对一个测试实例进行分类
参数说明:
inputTree:已经生成的决策树
labels:存储选择的最优特征标签
testVec:测试数据列表,顺序对应原数据集
返回:
classLabel:分类结果
"""
firstStr = next(iter(inputTree)) #获取决策树第一个节点
secondDict = inputTree[firstStr] #下一个字典
featIndex = labels.index(firstStr) #第一个节点所在列的索引
for key in secondDict.keys():
if testVec[featIndex] == key:
if type(secondDict[key]) == dict :
classLabel = classify(secondDict[key], labels, testVec)
else:
classLabel = secondDict[key]
return classLabel
注意点:
1. python3中myTree.keys()返回的是dict_keys,不再是list,所以不能使用myTree.keys()[0]的方法获取节点属性,可以使用list(myTree.keys())[0];
def acc_classify(train,test):
"""
函数功能:对测试集进行预测,并返回预测后的结果
参数说明:
train:训练集
test:测试集
返回:
test:预测好分类的测试集
"""
inputTree = createTree(train) #根据测试集生成一棵树
labels = list(train.columns) #数据集所有的列名称
result = []
for i in range(test.shape[0]): #对测试集中每一条数据进行循环
testVec = test.iloc[i,:-1] #测试集中的一个实例
classLabel = classify(inputTree,labels,testVec) #预测该实例的分类
result.append(classLabel) #将分类结果追加到result列表中
test['predict']=result #将预测结果追加到测试集最后一列
acc = (test.iloc[:,-1]==test.iloc[:,-2]).mean() #计算准确率
print(f'模型预测准确率为{acc}')
return test
测试函数
train = dataSet
test = dataSet.iloc[:3,:]
acc_classify(train,test)
使用sklearn中graphivz包实现决策树的绘制
#导入相应的包
from sklearn import tree
from sklearn.tree import DecisionTreeClassifier
import graphviz
#特征
Xtrain = dataSet.iloc[:,:-1]
#标签
Ytrain = dataSet.iloc[:,-1]
labels = Ytrain.unique().tolist()
Ytrain = Ytrain.apply(lambda x: labels.index(x)) #将本文转换为数字
#绘制树模型
clf = DecisionTreeClassifier()
clf = clf.fit(Xtrain, Ytrain)
tree.export_graphviz(clf)
dot_data = tree.export_graphviz(clf, out_file=None)
graphviz.Source(dot_data)
#给图形增加标签和颜色
dot_data = tree.export_graphviz(clf, out_file=None,
feature_names=['no surfacing', 'flippers'],
class_names=['fish', 'not fish'],
filled=True, rounded=True,
special_characters=True)
graphviz.Source(dot_data)
#利用render方法生成图形
graph = graphviz.Source(dot_data)
graph.render("fish")
生成pdf文件格式,存储决策树图形。
六、决策树可视化
决策树的主要优点就是直观易于理解,如果不能将其直观地显示出来,就无法发挥其优势。此处介绍了手动实现建树方法。
此处采用的是Matplotlib中一个非常有用的注解工具annotation,它可以在数据图形上添加文本注解。参考:https://matplotlib.org/users/annotations.html
可视化需要用到的函数:
getNumLeafs:获取决策树叶子节点的数目
getTreeDepth:获取决策树的层数
plotNode:绘制节点
plotMidText:标注有向边属性值
plotTree:绘制决策树
createPlot:创建绘制面板(主函数)
1. 计算叶子节点数目
def getNumLeafs(myTree):
"""
函数功能:递归计算叶子节点的数目
参数说明:
myTree:字典形式的树
返回:
numLeafs:叶节点数目
"""
numLeafs = 0 #初始化叶节点数目
firstStr = next(iter(myTree)) #获得树的第一个键值,即第一个特征
secondDict = myTree[firstStr] #获取下一组字典
for key in secondDict.keys():
if type(secondDict[key]) == dict: #测试该节点是否为字典
numLeafs += getNumLeafs(secondDict[key]) #是字典,递归,循环计算新分支叶节点数
else:
numLeafs +=1 #不是字典,代表此结点为叶子结点
return numLeafs
2. 计数树深度
数深度计算方式与叶节点计算方式基本上相同
def getTreeDepth(myTree):
"""
函数功能:递归计算树的深度
参数说明:
myTree:字典形式的树
返回:
maxDepth:树的最大深度
"""
maxDepth = 0
firstStr = next(iter(myTree))
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]) == dict:
thisDepth = 1+getTreeDepth(secondDict[key])
else:
thisDepth = 1
if thisDepth>maxDepth:
maxDepth = thisDepth
return maxDepth
3. 绘制节点
import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文
def plotNode(nodeTxt, cntrPt, parentPt, nodeType):
"""
函数功能:绘制节点
参数说明:
nodeTxt:节点名
centerPt:文本位置
parentPt:标注的箭头位置
nodeType:节点格式
"""
arrow_args = dict(arrowstyle="<-") #定义箭头格式
createPlot.ax1.annotate(nodeTxt,
xy=parentPt,xycoords='axes fraction',
xytext=cntrPt, textcoords='axes fraction',
va="center", ha="center",
bbox=nodeType,
arrowprops=arrow_args)
4. 编著有向边属性值
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=45)
5. 绘制决策树
三个步骤:①绘制自身; ②判断子节点非叶子节点,递归; ③ 判断子节点为叶子节点,绘制。
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] #下一个字典,也就是继续绘制子结点
plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD #y偏移
for key in secondDict.keys():
if type(secondDict[key])== dict: #测试该结点是否为字典
plotTree(secondDict[key],cntrPt,str(key)) #是字典则不是叶结点,递归调用继续绘制
else:
plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW #x偏移
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
6. 创建绘制面板
def createPlot(inTree):
"""
函数功能:创建绘制面板
参数说明:
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 #x偏移
plotTree.yOff = 1.0 #y偏移
plotTree(inTree, (0.5,1.0), '') #绘制决策树
plt.show()
首先由于整个画布根据叶子节点数和深度进行平均切分,并且x轴的总长度为1,即如同下图:
其中方形为非叶子节点的位置,@是叶子节点的位置,因此每份即上图的一个表格的长度应该为1/plotTree.toatlW,但是叶子节点的位置为@所在位置,则在开始的时候plotTree.xOff的赋值为-0.5/plotTree.totalW,即意为开始x位置为第一个表格左边的半个表格距离位置,这样作的好处为:在以后确定@位置时候可以直接加整数倍的1/plotTree.totalW。
对于plotTree函数中:
cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff)
plotTree.xOff即为最近绘制的一个叶子节点的x坐标,在确定当前节点位置时每次只需要确定当前节点有几个叶子节点,因此其叶子节点所占的总距离就确定了即为float(numLeafs)/plotTree.toatlW*1(因为总长度为1),因此当前节点的位置即为其所有叶子节点所占距离的中间即一半为float(numLeafs)/2.0/plotTree.totalW*1,但是由于开始plotTree.xOff赋值并非从0开始,而是左移了半个表格,因此还需要加上半个表格距离即为1/2/plotTree.totalW*1,则加起来变为(1.0+ float(numLeafs))/2.0/plotTree.totalW*1,因此偏移量确定,则x位置变为plottREE.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW。
对于plotTree函数参数赋值为(0.5, 1.0)
因此开始的根节点并不用划线,因此父节点和当前节点的位置需要重合,为(0.5, 1.0)
总结:利用这样的逐渐增加x的坐标,以及逐渐降低y的坐标能够很好的将数的叶子节点数和深度考虑进去,因此图的逻辑比例就很好的确定了,这样不用去关心输出图形的大小,一旦图形发生变化,函数会重新绘制,但是假如利用像素为单位来绘制图形,这样缩放图形就比较有难度了。
绘制树图形
createPlot(myTree)
七、使用决策树预测隐形眼镜类型
隐形眼镜数据集是非常著名的数据集,它包含很对患者眼部状况的观察条件以及医生推荐的隐形眼镜的类型。隐形眼镜类型包含三类:硬材质、软材质以及不适合佩戴隐形眼镜。数据来源于UCI数据库:https://archive.ics.uci.edu/ml/datasets/lenses
此处采用的是简单修改过的数据。
1. 导入数据集
lenses = pd.read_table('lenses.txt',header = None)
lenses.columns =['age','prescript','astigmatic','tearRate','class']
lenses
2. 划分训练集和测试集
import random
def randSplit(dataSet, rate):
"""
函数功能:切分训练集和测试集
参数说明:
dataSet:输入的数据集
rate:训练集所占比例
返回:
train,test:切分好的训练集和测试集
"""
l = list(dataSet.index) #提取出索引
random.shuffle(l) #随机打乱索引
dataSet.index = l #将打乱后的索引重新赋值给原数据集
n = dataSet.shape[0] #总行数
m = int(n * rate) #训练集的数量
train = dataSet.loc[range(m), :] #提取前m个记录作为训练集
test = dataSet.loc[range(m, n), :] #剩下的作为测试集
dataSet.index = range(dataSet.shape[0]) #更新原数据集的索引
test.index = range(test.shape[0]) #更新测试集的索引
return train, test
train1,test1 =randSplit(lenses, 0.8)
3. 生成决策树并构造注解树
4. 使用决策树进行分类
八、 算法总结
1. 决策树的优点
- 决策树可以可视化,易于理解和解释;
- 数据准备工作很少。其他很多算法通常狗需要数据规范化,需要创建虚拟变量并删除空值等;
- 能够同时处理数值和分类数据,既可以做回归又可以做分类。其他技术通过专门用于分析仅具有一种变量类型的数据集;
- 效率高,决策树只需要一次构建,反复使用,每一此预测的最大计算次数不超过决策树的深度;
- 能够处理多输出问题,即含有多个标签的问题,注意与一个标签中含有多种标签分类的问题区别开;
- 是一个白盒模型,结果很容易能够被解释。如果在模型中可以观察到给定的情况,则可以通过布尔逻辑轻松解释条件,相反,在黑盒模型中(例如人工神经网络),结果可能更难以解释。
2. 决策树的缺点
- 递归生成树的方法很容易出现过拟合;
- 决策树可能是不稳定的,因为即使非常小的变异,可能会产生一颗完全不同的树;
- 如果某些分类占优势,决策树将会创建一颗有偏差的树。因此,建议在拟合决策树之前平衡数据集。
九、 同样采用sklearn库进行一个决策树案例分享
1. 数据库导入
常规需求数据库导入
2. 导入数据
os.chdir() 指定数据路径
flag:代表该样本是否有申请欺诈
net_size:用户的社交圈规模
degree:代表联系人数量
average_neighbor_degree:相邻用户的联系人数据的均值
percentage:用户所在社交圈的预期客户百分比
3. 导入sklearn库,并建立训练集和测试集
从flag的实际数据来看,数据并不是平衡状态的。
4. 决策树建模
5. 将决策树的图形绘制出来
6. 用决策树模型进行预测,并生成决策类评估指标报告
自定义绘制预测ROC/AUC函数
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理