看过我数学建模专栏的读者应该知道,我从去年开始就计划写这专栏,但由于各种原因,一直无暇实施计划。而从本篇开始,这个专栏将开始填坑。每篇博文会采用
理论 实践
的形式,试图用sklearn这个强大的工具包来实现机器学习中的一些经典算法。 本篇的核心算法是决策树。
决策树理论
首先看决策树的相关理论,在我看过的一些资料中,李航老师的《统计机器学习》这部分写得最全面,因此下面的内容主要参考了这本书,但顺序我做了一些更改,改成了决策树理论建立的顺序,以便读者能够更容易看懂。
决策树基本概念
说到“树”,可能会联系到数据结构中的树概念。没错,决策树基本结构就是数据结构中的树状图。看下图两个简单例子:
一个根节点可根据条件分裂成不同的子节点,银行业就常用这个模型来根据用户信用来判断是否贷款。
决策树既可以做分类,又可以做回归。但通常来说,做分类的情况更多。
基本模型有了,那么这些指标如何进行排布?又依据什么来分类?这就要引出下面的数学指标。
经验熵
先看一个数理概念——经验熵。
熵原本是物理上的一个概念,用来描述事物的混乱程度。而经验熵(在信息论里也叫信息熵),用来衡量事物的不确定程度。
我们假设X为一维随机变量,它的概率分布满足
则X的经验熵为
为了探究经验熵的实际意义,我们考虑一种最简单的概率分布,即伯努利分布(0-1)分布。
设X=1的概率为p,则X的分布为
经验熵H§为
绘制经验熵H§随概率p变化曲线如下图所示。
当p=0或p=1时,H§=0,此时X的取值是确定的,没有随机性。
当p=0.5时,H§=1,此时X的取值最随机,X=0或X=1的概率各占一半。
因此,我们可以认识到经验熵的实际意义是衡量不确定性,熵越大则不确定性越大。
这里顺便提一下熵概念的引出和信息论中的信息量有关,熵的定义式实际上就是 概率*信息量。
条件熵
上面是最简单的一种情况,如果我们把随机变量的个数增加成两个,就可以引出条件熵的概念。
设随机变量(X,Y)的联合概率分布为
则条件熵H(Y|X)为
条件熵H(Y|X)表示在X的条件下Y的不确定性。
注:对于概率为0的情况,规定0log0=0。
信息增益
有了上面两个概念之后,我们再将其进行相减,这样就得到了新的概念——信息增益。
信息增益g(D, A)表示因特征A而导致数据集D分类不确定性减少的程度。
听起来很绕?那翻译一下。我们的目标是让数据集D尽可能分开,即让分类不确定性最小,那应该怎么挑选特征最好,自然是先挑那些最“显著”的特征,这个显著的衡量标准就是信息增益。
下面看了例子,以便对公式的理解更深刻。
首先给定数据集D,如下图所示。
这里有年龄、有工作、有自己的房子、信贷情况四个特征,我们需要根据信息增益来选择最优的一个特征。
套用上面的公式,可以计算出下列数据。
比较四个特征的信息增益,发现g(D,A3)最大,因此有自己的房子是最优特征。
ID3算法
通过上面的例子,我们已经能够通过信息增益来筛选出最优特征。那么,如果不断得进行特征筛选,那就能构成一棵决策树。这就是ID3算法的思想。
ID3算法描述: 从根节点开始,对节点计算所有可能的特征的信息增益,选择信息增益值最大的特征作为节点的划分特征; 由该特征的不同取值建立子节点; 再对子节点递归地调用以上方法,构建决策树;直到所有特征的信息增益都很小或者没有特征可以选择为止,得到最终的决策树。
还是拿上面的房贷例子来说,上面我们只进行了一步划分,根据是否有房子特征来划分出有房者能够发放贷款,但对于无房者来说,都全部拒绝吗?显然不是,我们还需要根据其它的特征进行进一步划分。而这部分需要划分的数据集记为D2。在这个数据集中,就可以根据剩下的特征再次进行划分,同样计算信息增益:
g(D2,A2)最大,因此选择A2特征进行下一步分裂,这样就可以得到决策树。
但ID3算法存在一个缺点是,当数据量过大时,它依旧会把每个叶子节点分出来,而不存在剪枝策略,这就导致运算缓慢。而剪枝策略就是下一个算法C4.5对ID3的优化之一。
信息增益比
在进入C4.5算法之前,需要引入一个新的概念:信息增益比。
理解起来也很简单,就是把信息增益和信息熵相除。
为什么要引入这个概念呢?因为之前我们使用信息增益来进行特征选择时,经验熵越大,信息增益值也会偏大,两者进行相比,则可以进行校正,有了这个概念之后,就可以自然得进入到C4.5算法。
C4.5算法
看名字也知道,这个算法一定是在ID3之后的。相比于ID3,C4.5做了两个改进:
1、划分标准改为信息增益比
2、引入剪枝策略
下面介绍剪枝策略
这张图能明确得表示了剪枝概念,即需要通过剪除一些分支来降低整体的复杂度,这里还需要引入一个新的概念–损失函数。
式中,|T|表示树T的叶结点数量,
表示叶结点t上的经验熵
如果我们把第一项记作C(T),代入经验熵的定义式,有
这样,损失函数就可以简写为
如果学过机器学习,就会发现这个式子和正则化的损失函数非常类似。
C(T)表示模型对训练数据的预测误差,即模型和训练数据的拟合程度,|T|表示模型复杂度,参数
则控制了拟合程度和复杂度的均衡。通常来说,模型拟合程度越高,复杂度也越高,这就产生了过拟合现象,这时候需要正则化来进行缓解。这里的
就控制了正则化的程度。
有了这个概念后,就可以判断什么时候应该采用剪枝。
剪枝是个不断从叶结点向上递归迭代的过程,当全部剪完后,就能得到最终的决策树。
CART算法
上面的ID3和C4.5都只适用于分类的场景。CART算法则进行了进一步的拓展,既可以回归也可以分类。同样,CART算法和C4.5算法过程类似,包含决策树生成,决策树剪枝。
回归树的生成
回归树的生成采用平方误差最小化准则,因此回归树通常称为最小二乘回归树。
算法流程见图,不作详细分析。
分类树的生成/基尼指数
上面提到ID3的分类指标用了信息增益,C4.5用了信息增益率,这里的CART也做了修改,用了基尼指数。
基尼指数的定义:
假设有K个类,样本点属于第k类的概率为
,则概率分布的基尼指数为
对于给定的样本集合D,基尼指数为
基尼指数的意义和经验熵类似,均表示样本集合的不确定程度。基尼指数越大,样本集合的不确定性也越大。
CART剪枝
CART的剪枝过程和C4.5的剪枝过程类似,计算指标略有区别,这里也不展开详细分析,具体流程见图。
决策树实践
有了上面的决策树理论知识储备后,下面利用python的sklearn工具包来进行实践。
下面部分主要参考的资料为《菜菜的机器学习sklearn课堂》
分类树实践
首先来尝试用决策树来做分类,所使用的数据集是sklearn自带的wine数据集。
1.导入需要的算法库和模块
代码语言:javascript复制from sklearn import tree
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
import pandas as pd
import graphviz
2.查看数据
这里使用的是sklearn自带的wine数据集。
代码语言:javascript复制wine = load_wine()
print(wine.data.shape)
print(pd.concat([pd.DataFrame(wine.data), pd.DataFrame(wine.target)], axis=1))
print(wine.feature_names)
print(wine.target_names)
共178条数据,13个特征,3个类别。
3.划分训练集和测试集
代码语言:javascript复制Xtrain, Xtest, Ytrain, Ytest = train_test_split(wine.data,wine.target,test_size=0.3)
print(Xtrain.shape)
print(Xtest.shape)
test_size=0.3表示测试集占样本数量的30%
划分之后,训练集为124条数据,测试集为54条数据。
4.模型建立
代码语言:javascript复制clf = tree.DecisionTreeClassifier(criterion="entropy")
clf = clf.fit(Xtrain, Ytrain)
score = clf.score(Xtest, Ytest) # 返回预测的准确度
print(score)
criterion参数即特征划分标准,我们使用的是entropy(信息熵),即前面理论提到的经验熵,此外还可以使用gini(基尼指数),如果不填,默认使用基尼指数。
score代表准确度
由于决策树的建立包含随机变量,每次运行结果都不一样。
这里我运行几次平均准确率在90%以上。
5.决策树可视化
代码语言:javascript复制feature_name = ['酒精','苹果酸','灰','灰的碱性','镁','总酚','类黄酮','非黄烷类酚类','花青素','颜色强度','色调','od280/od315稀释葡萄酒','脯氨酸']
dot_data = tree.export_graphviz(clf
,feature_names= feature_name
,class_names=["类型一","类型二","类型三"]
,filled=True #控制颜色填充
,rounded=True #控制图片为圆角
)
graph = graphviz.Source(dot_data.replace('helvetica','"Microsoft YaHei"'), encoding='utf-8')
graph.view()
在Jupyter环境中,运行该代码,会以pdf的形式生成可视化结果,结果如下。
这就是分类决策树,每一个分支节点上第一行代表分支的依据。
颜色代表不纯度,颜色越深代表代表不纯度越小,叶子节点不纯度为0。
如果运行报错,可能是由于没安装graphviz插件,安装方式如下:
插件下载地址https://graphviz.gitlab.io/download/
windows选择:
在安装时,勾选将graphviz添加到环境变量
6.特征重要性显示
上图的决策树分支是根据特征重要性(信息增益)来进行分支,通过下面的程序可以打印出各个特征的重要性。
代码语言:javascript复制print([*zip(feature_name,clf.feature_importances_)])
得到结果:
代码语言:javascript复制[('酒精', 0.0), ('苹果酸', 0.0), ('灰', 0.0), ('灰的碱性', 0.03448006546085971), ('镁', 0.0), ('总酚', 0.0), ('类黄酮', 0.4207777417026953), ('非黄烷类酚类', 0.0), ('花青素', 0.0), ('颜色强度', 0.1444829682905809), ('色调', 0.03408453152321241), ('od280/od315稀释葡萄酒', 0.0), ('脯氨酸', 0.3661746930226517)]
有些特征的重要性为0,说明这些指标在决策树中没有被利用。
更多参数拓展
回顾上面分类树的构建过程,核心代码是tree.DecisionTreeClassifier(criterion="entropy")
这里我们只使用了criterion
来选择划分指标,当然还有更多的参数可以自定义。
随机参数
在上面的例子中,每次运行结果都会有些不同,原因在于使用sklearn自带的决策树时,它会默认“栽种”好几棵不同的决策树,从中返回出效果最好的那一棵。
random_state
:用来设置分枝中的随机模式的参数,默认None,输入任意整数,会一直长出同一棵树,让模型稳定下来。
splitter
:用来控制决策树中的随机选项的,有两种输入值:
- 输入”best",决策树在分枝时虽然随机,但是还是会优先选择更重要的特征进行分枝(重要性可以通过属性feature_importances_查看)
- 输入“random",决策树在分枝时会更加随机,树会因为含有更多的不必要信息而更深更大,并因这些不必要信息而降低对训练集的拟合。这也是防止过拟合的一种方式。
调用示例:
代码语言:javascript复制clf = tree.DecisionTreeClassifier(criterion="entropy"
,random_state=30
,splitter="random"
)
剪枝策略
max_depth
: 用来限制树的最大深度,超过设定深度的树枝全部剪掉。(建议从=3开始使用)
min_samples_leaf
:限定一个节点在分枝后的每个子节点都必须包含至少min_samples_leaf个训练样本,否则分枝就不会发生。(建议从=5开始使用)
min_samples_split
:限定一个节点必须要包含至少min_samples_split个训练样本,这个节点才允许被分枝,否则分枝就不会发生。
max_features
:限制分枝时考虑的特征个数,超过限制个数的特征都会被舍弃。
min_impurity_decrease
:限制信息增益的大小,信息增益小于设定数值的分枝不会发生。
调用示例:
代码语言:javascript复制clf = tree.DecisionTreeClassifier(criterion="entropy"
,random_state=30
,splitter="random"
,max_depth=3
,min_samples_leaf=10
,min_samples_split=10
)
参数选择
上面列举了那么多参数,如何选择最合适的呢?这里有两种方法,一种是用循环来进行尝试,另一种使用参数的网格搜索。
关于网格搜索,比较适用于参数多且相互制约的情况,在后续SVM章节中会进行详解,这里先采用循环的方式。
比如,要知道剪枝深度max_depth
取值多少合适,可以使用循环,看看什么时候测试集的score最大。
import matplotlib.pyplot as plt
test = []
for i in range(10):
clf = tree.DecisionTreeClassifier(max_depth=i 1
,criterion="entropy"
,random_state=30
,splitter="random"
)
clf = clf.fit(Xtrain, Ytrain)
score = clf.score(Xtest, Ytest)
test.append(score)
plt.plot(range(1,11),test,color="red",label="max_depth")
plt.legend()
plt.show()
绘制结果如图所示
说明max_depth取4时,效果最好。
回归树实践
回归树的属性接口和分类树大部分类似,直接来上手。
1.导入需要的算法库和模块
代码语言:javascript复制import numpy as np
from sklearn.tree import DecisionTreeRegressor
import matplotlib.pyplot as plt
2. 创建一条含有噪声的正弦曲线
这里所用的数据由numpy进行生成。
先创建一组随机的,分布在0~5上的横坐标轴的取值(x),然后将这一组值放到sin函数中去生成纵坐标的值(y),接着再到y上去添加噪声。
代码语言:javascript复制rng = np.random.RandomState(1)
X = np.sort(5 * rng.rand(80,1), axis=0)
y = np.sin(X).ravel()
y[::5] = 3 * (0.5 - rng.rand(16))
3.模型建立
代码语言:javascript复制regr_1 = DecisionTreeRegressor(criterion='mse',max_depth=3)
regr_1.fit(X, y)
这里的criterion
和分类树不同,分类树是两个分类标准可选,这里可选的参数有三种:
- 输入"mse":使用均方误差mean squared error(MSE),父节点和叶子节点之间的均方误差的差额将被用来作为特征选择的标准,这种方法通过使用叶子节点的均值来最小化L2损失。
- 输入“friedman_mse”使用费尔德曼均方误差,这种指标使用弗里德曼针对潜在分枝中的问题改进后的均方误差。
- 输入"mae"使用绝对平均误差MAE(mean absolute error),这种指标使用叶节点的中值来最小化L1损失。
4.测试集导入模型,预测结果
代码语言:javascript复制X_test = np.arange(0.0, 5.0, 0.01)[:, np.newaxis]
y_1 = regr_1.predict(X_test)
5.结果可视化
代码语言:javascript复制plt.figure()
plt.scatter(X, y, s=20, edgecolor="black",c="darkorange", label="data")
plt.plot(X_test, y_1, color="cornflowerblue",label="predict", linewidth=2)
plt.xlabel("data")
plt.ylabel("target")
plt.title("Decision Tree Regression")
plt.legend()
plt.show()
如图所示,蓝色线段为回归树的预测结果。
更多实例
上面是两个简单的使用情景,如果想进一步了解,可以看一个更复杂的情况分类决策树实践——Titanic数据集