前言
看过《Python机器学习》的都知道,逻辑回归后先讲了支持向量机,然后是核支持向量机,然后才是决策树。那为什么我先写决策树呢?因为前面那俩玩意一时半会儿没看懂 。
此外,本篇内容也参考了西瓜书《机器学习》的相关内容
决策树
决策树的逻辑基于问答,每一个节点针对某个特征进行询问,并根据答案走对应的分支;最终的叶子节点就是此次查询的预测值。而模型要做的就是构建一个最优的决策树
信息熵
显然,能在有限的时间,空间内准确率更高,决策树就更优。那么,在构建整个决策树的过程中,如何量化当前状态下的决策树的优秀程度呢?很直接的想法:在同一个节点下的样本标签不同种类的数量越低,这个分支就越好。
所以,类似线性分类的损失函数,我们为决策树引入了信息熵的概念。假定当前节点上的样本集合D中第k类样本所占的比例为 p k ( k = 1 , 2 , . . . , ∣ γ ∣ ) p_{k}(k=1, 2, ..., |\gamma|) pk(k=1,2,...,∣γ∣),则D的信息熵定义为:
E n t ( D ) = − ∑ k = 1 ∣ γ ∣ p k log 2 p k Ent(D)=-\sum_{k=1}^{|\gamma|} p_{k}\log_{2}{p_{k}} Ent(D)=−∑k=1∣γ∣pklog2pk
规定当 p = 0 , p l o g 2 p = 0 p=0, plog_{2}{p}=0 p=0,plog2p=0
可以发现Ent(D)的值越小,我们认为D的纯度越高
接下来就是寻找一个策略来分离样本,减小信息熵。假定样本有一个属性a,它有
V
V
V个可能的取值
{
a
1
,
a
2
,
.
.
.
,
a
V
}
\left \{a^{1}, a^{2}, ..., a^{V}\right \}
{a1,a2,...,aV},则一般会产生
V
V
V个分支节点,其中第
v
v
v个节点上包含所有属性
a
a
a的值为
a
v
a^{v}
av的样本,记为
D
v
D^{v}
Dv,则每一个子节点的信息熵为
E
n
t
(
D
v
)
Ent(D^{v})
Ent(Dv)。
考虑到节点的样本数量会干扰信息熵大小的比较,我们通过赋予权重来消除样本数量的影响,然后计算前后信息熵的变化来得到这一步划分的信息增益:
G a i n ( D , a ) = E n t ( D ) − ∑ v = 1 V ∣ D v ∣ ∣ D ∣ E n t ( D v ) Gain(D,a)=Ent(D)-\sum_{v=1}^{V}\frac{|D^{v}|}{|D|}Ent(D^{v}) Gain(D,a)=Ent(D)−∑v=1V∣D∣∣Dv∣Ent(Dv)
我们可以简单地认为,如果根据这个属性来划分,信息增益最大,我们就更倾向于用这个属性来划分当前节点。记这个最好的划分属性为 a ∗ a_{*} a∗
操作
决策树的生成是个递归的过程,我们记当前待处理的节点为node,样本集为 D D D,属性集为 A A A,每个样本的标签记为 C i ( i = 1 , 2 , . . . , γ ) C_{i}(i=1, 2, ..., \gamma) Ci(i=1,2,...,γ),,则每一步有以下情况:
记以下过程为函数
G
e
n
e
r
a
t
e
(
D
,
A
)
Generate(D, A)
Generate(D,A)
1.
D
D
D中样本全部属于同一标签
C
C
C,则将node标记为
C
C
C类叶子节点,直接return
2.
A
=
ϕ
A=\phi
A=ϕ 或者
D
D
D中样本的所有属性值均相同,则无法划分,node标记为
D
D
D中样本数量最多的类别,直接return
3. 开始划分:
(1)从
A
A
A中找到最好的划分属性
a
∗
a_{*}
a∗
(2)遍历每个样本在属性
a
∗
a_{*}
a∗下的值,记
D
v
D_{v}
Dv为取值为
a
∗
v
a_{*}^{v}
a∗v的样本子集
(3)如果
D
v
=
ϕ
D_{v}=\phi
Dv=ϕ,则当前node为叶节点,标记为
D
D
D中样本最多的标签。
(4)否则,将
G
e
n
e
r
a
t
e
(
D
v
,
A
∖
{
a
∗
}
)
Generate(D_{v}, A \setminus \left \{a_{*}\right \})
Generate(Dv,A∖{a∗}) 记为子树。
4. return
优化:增益率
我们考虑给样本编号,如果将编号当作属性加入候选划分属性,我们会发现它的信息增益远大于其他的属性,划分后每个节点只包含一个节点,熵降到最低。这显然不是我们想要的。
分析这一现象,我们发现上述的决策会偏向可取值数目较多的属性。所以,我们引入增益率替代信息增益,来减少不利的影响:
G a i n ‾ r a d i o ( D , a ) = G a i n ( D , a ) I V ( a ) Gain\underline{~~}radio(D,a)=\frac{Gain(D,a)}{IV(a)} Gain radio(D,a)=IV(a)Gain(D,a)
其中 I V ( A ) IV(A) IV(A)称为属性a的固有值
I V ( a ) = − ∑ v = 1 V ∣ D v ∣ ∣ D ∣ log 2 ∣ D v ∣ ∣ D ∣ IV(a)=-\sum_{v=1}^{V}\frac{|D^{v}|}{|D|}\log_{2}{\frac{|D^{v}|}{|D|}} IV(a)=−∑v=1V∣D∣∣Dv∣log2∣D∣∣Dv∣
可以发现,属性a的取值的可能取值数目越多(即 V V V越大),则 I V ( a ) IV(a) IV(a)通常会越大。
此外需要注意的是,增益率准则会偏好数目较少的属性,所以我们采取启发式算法,先选出信息增益高于平均水平的属性,再从中选取增益率最高的。
基尼指数
决策树有很多种,区别就在于策略不同。CART决策树使用基尼指数来划分属性,数据集 D D D的纯度可以用基尼值来衡量:
G i n i ( D ) = ∑ k = 1 ∣ γ ∣ ∑ k ′ ≠ k p k p k ′ = 1 − ∑ k = 1 ∣ γ ∣ p k 2 Gini(D)=\sum_{k=1}^{|\gamma|}\sum_{k'\ne k}^{}p_{k}p_{k'}=1-\sum_{k=1}^{|\gamma|}p_{k}^{2} Gini(D)=∑k=1∣γ∣∑k′=kpkpk′=1−∑k=1∣γ∣pk2
其中, p k p_{k} pk表示该节点属于k类样本的概率, p k ′ = 1 − p k p_{k'}=1-p_{k} pk′=1−pk
类似信息增益,某属性a的基尼指数定义为
G i n i ‾ i n d e x ( D , a ) = ∑ v = 1 V ∣ D v ∣ ∣ D ∣ G i n i ( D v ) Gini\underline{~~}index(D,a)=\sum_{v=1}^{V}\frac{|D^{v}|}{|D|}Gini(D^{v}) Gini index(D,a)=∑v=1V∣D∣∣Dv∣Gini(Dv)
于是,我们可以用基尼指数来作为划分的指标,基尼指数越小,划分越优秀
剪枝
过拟合永远是模型逃不掉的问题,显然如果树过于复杂,分类过于具体,会导致过拟合,所以我们需要去掉一些树枝。
预剪枝
即在树生长过程中中断递归过程,我们只需要设置一些停止条件,如:
(1)如果当前节点数据集的信息增益低于预设的阈值,停止向下分裂。
(2)如果当前节点的数据集已经达到足够纯度(例如,所有数据属于同一类),则停止分裂。
(3)如果当前节点包含的数据量少于预设的最小数据量,停止分裂。
(4)限制决策树的最大深度,当达到预设的深度时,停止分裂。
(5)如果向下分裂后准确率降低,则不分裂。注意,这种方式用贪心的思想抑制决策树分裂,有导致欠拟合的风险。
后剪枝
即先生成一个完整的决策树,再根据需求删减节点。
(1)根据需求,回溯时将子树退化成叶子节点,标记为该标签的样本数量最多的。
(2)如果模型准确率提高,则更改,否则保留原来的子树。
注意,后剪枝也有欠拟合的风险,但风险比预剪枝小;代价是时间消耗变大。
连续值的处理
如果属性a的取值为一段连续值 ( l , r ) (l, r) (l,r),节点上的样本在属性a上的取值分别为 { a 1 , a 2 , . . . a n } \left \{ a^{1},a^{2},...a_{n} \right \} {a1,a2,...an}。我们选取一个划分点 t t t,将 D D D划分为子集 D − D^{-} D−和 D + D^{+} D+,其中 D − D^{-} D−包含了那些在属性a上取值小于等于 t t t的样本, D + D^{+} D+包含了那些在属性a上取值大于 t t t的样本。
显然,对于相邻的两个属性取值 a i , a i + 1 a^{i}, a^{i+1} ai,ai+1, t t t取区间 [ a i , a i + 1 ) [a^{i}, a^{i+1}) [ai,ai+1)中的任意值产生的划分相同。所以,我们另该节点的划分点 t t t在以下区间 T a T_{a} Ta中取值:
T a = { a i + a i + 1 2 ∣ 1 ≤ i ≤ n − 1 } T_{a}=\left \{ \frac{a^{i}+a^{i+1}}{2}|1\le i\le n-1 \right \} Ta={2ai+ai+1∣1≤i≤n−1}
然后我们考察并选取最优的划分点,满足:
G a i n ( D , a ) = max t ∈ T a G a i n ( D , a , t ) Gain(D,a)=\max_{t\in T_{a}}Gain(D,a,t) Gain(D,a)=maxt∈TaGain(D,a,t)
其中 G a i n ( D , a , t ) Gain(D,a,t) Gain(D,a,t)是样本集 D D D基于划分点 t t t二分后的信息增益,然后我们选择使 G a i n ( D , a , t ) Gain(D,a,t) Gain(D,a,t)最大的划分点。
特别要注意的是,连续属性 a a a在划分后在之后的选择中还可以当作候选属性(也就是可以划分多次)
多变量决策树
当属性种类很多时,往往会使决策树变得庞大,预测时间开销很大。我们可以采用多变量决策树来解决这个问题。
我们将每一个非叶子节点进行修改,让它不再只依据一个属性进行划分,而是变成一个形如
∑
i
=
1
d
w
i
a
i
=
t
\sum_{i=1}^{d}w_{i}a_{i}=t
∑i=1dwiai=t的分类器,其中
w
i
w_{i}
wi为属性
a
i
a_{i}
ai的权重,训练时可以从该节点上的样本集和测试集来学习这些
w
i
w_{i}
wi和
t
t
t。
简单的决策树的代码实现
构建决策树:
# 导入决策树分类器
from sklearn.tree import DecisionTreeClassifier
# 创建决策树分类器实例,使用'gini'不纯度作为节点分裂的标准,最大深度限制为4,随机种子设置为1
tree_model = DecisionTreeClassifier(criterion='gini',
max_depth=4,
random_state=1)
# 使用训练数据集X_train和对应的标签y_train对决策树分类器进行拟合
tree_model.fit(X_train, y_train)
# 将训练集和测试集的特征向量堆叠在一起,形成一个组合数据集
X_combined = np.vstack((X_train, X_test))
# 将训练集和测试集的标签堆叠在一起,形成一个组合的标签集(为了可视化)
y_combined = np.hstack((y_train, y_test))
# 使用plot_decision_regions函数来可视化决策区域,传入组合后的特征和标签数据
# classifier参数指定了要可视化的分类器,test_idx参数指定了测试数据在组合数据中的索引范围
plot_decision_regions(X_combined, y_combined,
classifier=tree_model,
test_idx=range(105, 150))
# 设置X轴的标签
plt.xlabel('petal length [cm]')
# 设置Y轴的标签
plt.ylabel('petal width [cm]')
# 在图上添加图例,并设置其位置为左上角
plt.legend(loc='upper left')
# 调整布局以适应坐标轴标签和图例
plt.tight_layout()
# 保存图像
# plt.savefig('images/03_20.png', dpi=300)
# 显示图像
plt.show()
可视化决策树:
from sklearn import tree
tree.plot_tree(tree_model)
#plt.savefig('images/03_21_1.pdf')
plt.show()
更好地呈现图像:
# 导入用于从DOT数据创建图形的函数
from pydotplus import graph_from_dot_data
# 导入用于从决策树模型导出DOT格式数据的函数
from sklearn.tree import export_graphviz
# 使用export_graphviz函数从决策树模型导出DOT数据
# filled=True, rounded=True选项使节点填充颜色并显示圆角,以增加可读性
# class_names参数指定了类别名称,用于节点的标签
# feature_names参数指定了特征名称,用于描述树中的分裂条件
# out_file=None表示不直接写入文件,而是返回DOT数据的字符串形式
dot_data = export_graphviz(tree_model,
filled=True,
rounded=True,
class_names=['Setosa',
'Versicolor',
'Virginica'],
feature_names=['petal length',
'petal width'],
out_file=None)
# 使用graph_from_dot_data函数将DOT数据转换为Graphviz图形对象
graph = graph_from_dot_data(dot_data)
# 使用write_png方法将Graphviz图形对象导出为PNG图像文件
graph.write_png('tree.png')
决策森林
当数据集足够大时(相比起属性种类而言),单颗决策树的性能提升会越来越小,浪费了大量数据。我们可以使用随机森林算法来更好地处理数据和减少过拟合的概率。
步骤
(1)从训练集
D
D
D中有放回选取大小为
d
d
d的子集
D
i
D_{i}
Di
(2)基于样本
D
i
D_{i}
Di生成决策树
T
r
e
e
i
Tree_{i}
Treei,在每个节点下做以下任务:
(a)不放回随机选择
k
k
k个特征。
(b)选取最佳特征分裂节点。
(3)聚合每棵树的预测结果,以多数投票制来确定标签分类。
PS: 步骤1中“有放回”的原因是为了让每棵树的训练集保持独立;步骤2中“无放回”的原因暂未找到解答。
优缺点
优势:不必关心超参数的选择(即学习率,迭代次数,正则化等参数),我们唯一需要关心的是森林中树的数量(通常选取
m
\sqrt{m}
m,其中
m
m
m为训练集的大小);
此外我们通常不需要修剪随机森林,因为集成模型对单个决策树的噪声具有较强的抵抗力。
缺点:计算成本随树的增多而增大;可解释性不如普通决策树
ski-learn上实现
现成的模型RandomForestClassifier
# 导入随机森林分类器类
from sklearn.ensemble import RandomForestClassifier
# 初始化随机森林分类器
# 使用基尼不纯度作为质量度量划分决策树节点
# 设置25棵树,随机状态为1以确保实验可复现,n_jobs=2使用2个CPU核心并行训练
forest = RandomForestClassifier(criterion='gini',
n_estimators=25,
random_state=1,
n_jobs=2)
# 使用训练数据拟合模型
# X_train是特征数据,y_train是对应的标签数据
forest.fit(X_train, y_train)
# 绘制决策区域边界
# X_combined和y_combined是组合了训练和测试数据的特征和标签
# classifier参数是已经训练好的随机森林模型
# test_idx参数标识出测试数据点的索引范围
# 这将可视化出训练和测试数据点,并展示出决策边界
plot_decision_regions(X_combined, y_combined,
classifier=forest, test_idx=range(105, 150))
# 设置标签
plt.xlabel('petal length [cm]')
plt.ylabel('petal width [cm]')
# 添加图例,位置在左上角
plt.legend(loc='upper left')
# 调整图形布局,使得子图和标签更紧凑
plt.tight_layout()
# 保存图形到本地文件
# plt.savefig('images/03_22.png', dpi=300)
# 显示图形
plt.show()