决策树定义
决策树是一种树形结构,其中每个内部节点表示一个属性上的测试,每个分支代表一个测试输出,每个叶节点代表一种类别。决策树是一种十分常用的分类回归方法。
决策树是附加概率结果的一个树状的决策图,是直观的运用统计概率分析的图法。机器学习中决策树是一个预测模型,它表示对象属性和对象值之间的一种映射,树中的每一个节点表示对象属性的判断条件,其分支表示符合节点条件的对象。树的叶子节点表示对象所属的预测结果。
决策树学习通常包括 3 个步骤:特征选择、决策树的生成和剪枝处理。
特征选择
熵
熵是表示随机变量不确定性的度量
Ent(D)的值越小,则D的纯度越高
计算信息熵时约定:若p = 0,则plog2p=0
Ent(D)的最小值为0,最大值为log2|y|
信息增益(ID3)
在划分数据集前后信息发生的变化称为信息增益。
对于决策树节点最合适的特征选择,就是信息增益最大的特征。
ID3算法优点:方法简单、计算量小、理论清晰、学习能力较强、比较适用于处理规模较大的学习问题。
ID3算法缺点:倾向于选择那些属性取值比较多的属性,在实际的应用中往往取值比较多的属性对分类没有太大价值、不能对连续属性进行处理、对噪声数据比较敏感、需计算每一个属性的信息增益值、计算代价较高。
增益率(C4.5)
C4.5适用:比较适用于处理规模较小的学习问题。
其中IV(a)称为属性a的“固有值”,属性a的可能取值数目越多(即V越大),则IV(a)的值通常就越大;
基尼指数(CART)
分类问题中,假设D有K个类,样本点属于第k类的概率为p_k,则概率分布的基尼值定义为:
Gini(D)越小,数据集D的纯度越高;
例子:给定数据集D,属性a的基尼指数定义为:
在候选属性集合A中,选择那个使得划分后基尼指数最小的属性作为最有划分属性。
剪枝处理
剪枝:在决策树学习中将已生成的树进行简化的过程。
目的:“剪枝”是决策树学习算法对付“过拟合”的主要手段。
可通过“剪枝”来一定程度避免因决策分支过多,以致于把训练集自身的一些特点当做所有数据都具有的一般性质而导致的过拟合;
好处:减少时间、空间复杂度;减少过拟合,提高算法泛化能力
剪枝策略
1.预剪枝
预剪枝:边建立决策树边进行剪枝操作(通过限制深度、叶子节点个数、叶子节点样本数、信息增益量等)
预剪枝优点:降低过拟合风险,减少训练、测试时间;
预剪枝缺点:欠拟合风险,有些分支的当前划分虽然不能提升泛化性能,但在其基础上进行的后续划分却有可能显著提高性能。预剪枝基于“贪心”本质禁止这些分支展开,带来了欠拟合风险。
2.后剪枝
后剪枝:建立完决策树后来剪枝(通过一定的标准衡量,叶子节点越多,损失越大)
后剪枝优点:后剪枝比预剪枝保留了更多的分支,欠拟合风险小;泛化性能往往优于预剪枝决策树
后剪枝缺点:训练时间开销大:后剪枝过程是在生成完全决策树之后进行的,需要自底向上对所有非叶结点逐一计算
决策树构成
基本流程
(1)收集数据
(2)准备数据:树构造算法只适用于标称型数据,因此数值型数据必须离散化。
(3)分析数据:可以使用任何方法,构造树完成之后,我们应该检查图形是否符合预期。
(4)训练算法:构造树的数据结构。
(5)测试算法:使用经验树计算错误率。
(6)使用算法:此步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义。
代码
import numpy as np
import matplotlib.pylab as plt
import matplotlib
# 能够显示中文
matplotlib.rcParams['font.sans-serif'] = ['SimHei']
matplotlib.rcParams['font.serif'] = ['SimHei']
# 分叉节点,也就是决策节点
decisionNode = dict(boxstyle="sawtooth", fc="0.8")
# 叶子节点
leafNode = dict(boxstyle="round4", fc="0.8")
# 箭头样式
arrow_args = dict(arrowstyle="<-")
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
"""
绘制一个节点
:param nodeTxt: 描述该节点的文本信息
:param centerPt: 文本的坐标
:param parentPt: 点的坐标,这里也是指父节点的坐标
:param nodeType: 节点类型,分为叶子节点和决策节点
:return:
"""
createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',
xytext=centerPt, textcoords='axes fraction',
va="center", ha="center", bbox=nodeType, arrowprops=arrow_args)
def getNumLeafs(myTree):
"""
获取叶节点的数目
:param myTree:
:return:
"""
# 统计叶子节点的总数
numLeafs = 0
# 得到当前第一个key,也就是根节点
firstStr = list(myTree.keys())[0]
# 得到第一个key对应的内容
secondDict = myTree[firstStr]
# 递归遍历叶子节点
for key in secondDict.keys():
# 如果key对应的是一个字典,就递归调用
if type(secondDict[key]).__name__ == 'dict':
numLeafs += getNumLeafs(secondDict[key])
# 不是的话,说明此时是一个叶子节点
else:
numLeafs += 1
return numLeafs
def getTreeDepth(myTree):
"""
得到数的深度层数
:param myTree:
:return:
"""
# 用来保存最大层数
maxDepth = 0
# 得到根节点
firstStr = list(myTree.keys())[0]
# 得到key对应的内容
secondDic = myTree[firstStr]
# 遍历所有子节点
for key in secondDic.keys():
# 如果该节点是字典,就递归调用
if type(secondDic[key]).__name__ == 'dict':
# 子节点的深度加1
thisDepth = 1 + getTreeDepth(secondDic[key])
# 说明此时是叶子节点
else:
thisDepth = 1
# 替换最大层数
if thisDepth > maxDepth:
maxDepth = thisDepth
return maxDepth
def plotMidText(cntrPt, parentPt, txtString):
"""
计算出父节点和子节点的中间位置,填充信息
:param cntrPt: 子节点坐标
:param parentPt: 父节点坐标
:param txtString: 填充的文本信息
:return:
"""
# 计算x轴的中间位置
xMid = (parentPt[0]-cntrPt[0])/2.0 + cntrPt[0]
# 计算y轴的中间位置
yMid = (parentPt[1]-cntrPt[1])/2.0 + cntrPt[1]
# 进行绘制
createPlot.ax1.text(xMid, yMid, txtString)
def plotTree(myTree, parentPt, nodeTxt):
"""
绘制出树的所有节点,递归绘制
:param myTree: 树
:param parentPt: 父节点的坐标
:param nodeTxt: 节点的文本信息
:return:
"""
# 计算叶子节点数
numLeafs = getNumLeafs(myTree=myTree)
# 计算树的深度
depth = getTreeDepth(myTree=myTree)
# 得到根节点的信息内容
firstStr = list(myTree.keys())[0]
# 计算出当前根节点在所有子节点的中间坐标,也就是当前x轴的偏移量加上计算出来的根节点的中心位置作为x轴(比如说第一次:初始的x偏移量为:-1/2W,计算出来的根节点中心位置为:(1+W)/2W,相加得到:1/2),当前y轴偏移量作为y轴
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轴偏移量,向下移动1/D,也就是下一层的绘制y轴
plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD
# 循环遍历所有的key
for key in secondDict.keys():
# 如果当前的key是字典的话,代表还有子树,则递归遍历
if isinstance(secondDict[key], dict):
plotTree(secondDict[key], cntrPt, str(key))
else:
# 计算新的x轴偏移量,也就是下个叶子绘制的x轴坐标向右移动了1/W
plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW
# 打开注释可以观察叶子节点的坐标变化
# print((plotTree.xOff, plotTree.yOff), secondDict[key])
# 绘制叶子节点
plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode)
# 绘制叶子节点和父节点的中间连线内容
plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key))
# 返回递归之前,需要将y轴的偏移量增加,向上移动1/D,也就是返回去绘制上一层的y轴
plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD
def createPlot(inTree):
"""
需要绘制的决策树
:param inTree: 决策树字典
:return:
"""
# 创建一个图像
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))
# 初始的x轴偏移量,也就是-1/2W,每次向右移动1/W,也就是第一个叶子节点绘制的x坐标为:1/2W,第二个:3/2W,第三个:5/2W,最后一个:(W-1)/2W
plotTree.xOff = -0.5/plotTree.totalW
# 初始的y轴偏移量,每次向下或者向上移动1/D
plotTree.yOff = 1.0
# 调用函数进行绘制节点图像
plotTree(inTree, (0.5, 1.0), '')
# 绘制
plt.show()
class DecisionTree():
def fit(self, x, y, x_label):
self.label = x_label # x数据集对应的 特征标签
numpy_data = np.c_[x, y] # 把x,y拼接成一个numpy矩阵,方便后面操作
self.tree = self.build_tree(numpy_data) # 构建树
def build_tree(self, numpy_data): # numpy_data n行m列的矩阵,最后一列是结果集。返回字典
finish_columns = np.unique(numpy_data[:, -1])
if len(finish_columns) == 1:
return finish_columns[0] # 返回ng与ok,表面这个分支已经分到叶子了
rows, columns = numpy_data.shape # numpy行数与列数
root_numpy = np.zeros(columns - 1) # 构造全为0的numpy数组,保存信息熵数据
for i in range(columns - 1):
sum = 0
unique_data = np.unique(numpy_data[:, i])
for j in unique_data:
len_whole = len(numpy_data[(numpy_data[:, i] == j)])
len_ng = len(numpy_data[((numpy_data[:,i] == j) & (numpy_data[:, -1] == "ng"))])
len_ok = len_whole - len_ng
if len_ng != 0 and len_ok != 0: # 有一个为0就不用参与计算
x = len_ok / len_whole
sum = sum + round(len_whole / rows * (x * np.log2(x) + (1-x) * np.log2(1-x)), 3)
root_numpy[i] = sum # 保存各个特征的信息熵
root = root_numpy.argsort()[-1] # -1 选取最大值索引。np.argsort():排序后的索引
tree_dict = {self.label[root]: {}}
for values in np.unique(numpy_data[:, root]):
numpy_root = numpy_data[(numpy_data[:, root] == values)]
tree_dict[self.label[root]][values] = self.build_tree(numpy_root)
return tree_dict
if __name__ == "__main__":
data2 = ["青绿", "乌黑", "乌黑", "青绿", "浅白", "青绿", "乌黑", "乌黑",
"乌黑", "青绿", "浅白", "浅白", "青绿", "浅白", "乌黑", "浅白", "青绿"]
data6 = ["蜷缩", "蜷缩", "蜷缩", "蜷缩", "蜷缩", "稍蜷", "稍蜷", "稍蜷",
"稍蜷", "硬挺", "硬挺", "蜷缩", "稍蜷", "稍蜷", "稍蜷", "蜷缩", "蜷缩"]
data3 = ["浊响", "沉闷", "浊响", "沉闷", "浊响", "浊响", "浊响", "浊响",
"沉闷", "清脆", "清脆", "浊响", "浊响", "沉闷", "浊响", "浊响", "沉闷"]
data4 = ["清晰", "清晰", "清晰", "清晰", "清晰", "清晰", "稍糊", "清晰",
"稍糊", "清晰", "模糊", "模糊", "稍糊", "稍糊", "清晰", "模糊", "稍糊"]
data5 = ["凹陷", "凹陷", "凹陷", "凹陷", "凹陷", "稍凹", "稍凹", "稍凹",
"稍凹", "平坦", "平坦", "平坦", "凹陷", "凹陷", "稍凹", "平坦", "稍凹"]
data1 = ["硬滑", "硬滑", "硬滑", "硬滑", "硬滑", "软粘", "软粘", "硬滑",
"硬滑", "软粘", "硬滑", "软粘", "硬滑", "硬滑", "软粘", "硬滑", "硬滑"]
result = ["ok", "ok", "ok", "ok", "ok", "ok", "ok", "ok",
"ng", "ng", "ng", "ng", "ng", "ng", "ng", "ng", "ng"]
x_label = ["触感", "色泽", "敲声", "纹理", "脐部", "根蒂"]
x = np.array([data1, data2, data3, data4, data5, data6]).transpose()
y = np.array(result)
decisionTree = DecisionTree()
decisionTree.fit(x, y, x_label)
print(decisionTree.tree)
createPlot(decisionTree.tree)
结果:
分析:
构建树的时候只需要计算信息熵就可以了,而且不用移除出之前的特征。
原因: 代码实际计算sum = sum + x * np.log2(x) + (1-x) * np.log2(1-x)
函数y = x * np.log2(x) + (1-x) * np.log2(1-x)。这个函数关于x=1/2对称。当x属于(0,1)时连续,求导,计算出(0,1/2)递减,(1/2,1)递增。
当x = 1/2最小,y越小说明信息增益越小。
总结:决策树优缺点
优点:
- 易于理解和解释,决策树可以可视化。
- 几乎不需要数据预处理。其他方法经常需要数据标准化,创建虚拟变量和删除缺失值。决策树还不支持缺失值。
- 使用树的花费(例如预测数据)是训练数据点(data points)数量的对数。
- 可以同时处理数值变量和分类变量。其他方法大都适用于分析一种变量的集合。
- 可以处理多值输出变量问题。
- 使用白盒模型。如果一个情况被观察到,使用逻辑判断容易表示这种规则。相反,如果是黑盒模型(例如人工神经网络),结果会非常难解释。
- 即使对真实模型来说,假设无效的情况下,也可以较好的适用。
缺点:
- 决策树学习可能创建一个过于复杂的树,并不能很好的预测数据。也就是过拟合。修剪机制(现在不支持),设置一个叶子节点需要的最小样本数量,或者数的最大深度,可以避免过拟合。
- 决策树可能是不稳定的,因为即使非常小的变异,可能会产生一颗完全不同的树。
- 学习一颗最优的决策树是一个NP-完全问题。因此,传统决策树算法基于启发式算法,例如贪婪算法,即每个节点创建最优决策。这些算法不能产生一个全家最优的决策树。对样本和特征随机抽样可以降低整体效果偏差。
- 如果某些分类占优势,决策树将会创建一棵有偏差的树。因此,建议在训练之前,先抽样使样本均衡。