前言
在这篇博客中,我们将主要介绍决策树算法。决策树算法主要由三个部分构成:特征的选择,决策树的构建以及决策树的剪枝X。
我们往往期望选择最有分类能力的特征。比如找对象这件事情上,女生面对众多追求者,往往会将经济能力、教育背景等作为区分的特征,因为这些特征最有分类能力;而很少女生将鞋码或者发型作为第一优考虑的特征。
经过特征选择,我们可以将原数据集分成若干子数据集,对于这些子数据集,再次进行特征选择,直到数据集里面类别基本相同或者已经没有特征可分。
上面的构建过程可能会造成过拟合。一个比较极端的情况是,决策树的叶子结点实际对应着一个或者少数几个数据,这就导致决策树对测试集的拟合特别好,但是对于未知数据,则可能表现很差。
1. 特征选择
为了选出最有分类能力的特征,我们首先介绍熵和信息增益。
1.1 熵
熵是用来衡量随机变量的不确定程度的。假设离散随机变量
X
X
X 满足
P
(
X
=
x
i
)
=
p
i
,
i
=
1
,
2
,
.
.
.
,
n
P(X=x_i)=p_i, i=1, 2, ..., n
P(X=xi)=pi,i=1,2,...,n
则随机变量
X
X
X 的熵
H
(
X
)
H(X)
H(X) 为
H
(
X
)
=
−
∑
i
=
1
n
p
i
l
o
g
2
p
i
H(X)=-\sum_{i=1}^np_ilog_2p_i
H(X)=−i=1∑npilog2pi
这里,我们强制令 0 l o g 2 0 = 0 0log_20=0 0log20=0。
熵衡量随机变量的不确定性。因此,当随机变量是确定性的,也就是对于某个 i i i, p i = 1 p_i=1 pi=1,这时,熵最小,为 H ( X ) = − 1 l o g 2 1 − ∑ j ≠ i p j l o g 2 p j = 0 H(X)=-1log_21-\sum_{j\neq i}p_jlog_2p_j=0 H(X)=−1log21−∑j=ipjlog2pj=0;而当随机变量是均匀分布的,也就是对任意的 i i i, p i = 1 n p_i=\frac{1}{n} pi=n1,相当于对于随机变量 X X X 一无所知,这时,熵最大,为 H ( X ) = − ∑ i = 1 n p i l o g 2 p i = − ∑ i = 1 n 1 n l o g 2 1 n = l o g 2 n H(X)=-\sum_{i=1}^np_ilog_2p_i=-\sum_{i=1}^n\frac{1}{n}log_2\frac{1}{n}=log_2n H(X)=−∑i=1npilog2pi=−∑i=1nn1log2n1=log2n。
因此,下式成立
0
≤
H
(
X
)
≤
l
o
g
2
n
0\le H(X)\le log_2n
0≤H(X)≤log2n
下面我们介绍条件熵
H
(
Y
∣
X
)
H(Y|X)
H(Y∣X)。假设
P
(
X
,
Y
)
P(X, Y)
P(X,Y) 为联合概率分布,则条件熵
H
(
Y
∣
X
)
H(Y|X)
H(Y∣X) 为
H
(
Y
∣
X
)
=
∑
i
=
1
n
P
(
X
=
x
i
)
H
(
Y
∣
X
=
x
i
)
H(Y|X)=\sum_{i=1}^nP(X=x_i)H(Y|X=x_i)
H(Y∣X)=i=1∑nP(X=xi)H(Y∣X=xi)
1.2 信息增益
一个特征所带来的信息增益为 当得知该特征的信息使得数据集的信息不确定性降低的程度。
设数据集
D
D
D 中的类别分别为
{
C
1
,
C
2
,
.
.
.
,
C
K
}
\{C_1, C_2, ..., C_K\}
{C1,C2,...,CK},
∣
C
k
∣
|C_k|
∣Ck∣ 为类别
C
k
C_k
Ck 下的样本数量;特征
A
A
A 有
n
n
n 个可能的值,分别为
{
a
1
,
a
2
,
.
.
.
,
a
n
}
\{a_1, a_2, ..., a_n\}
{a1,a2,...,an},
D
i
D_i
Di 为 数据集中 特征
A
A
A 取
a
i
a_i
ai 的数据子集。进一步的,我们记
D
i
k
=
D
i
∩
C
k
D_{ik}=D_i\cap C_k
Dik=Di∩Ck,即类别为
C
k
C_k
Ck 且 特征
A
A
A 取
a
i
a_i
ai 的数据子集。这样,我们有
(1)数据集
D
D
D 的熵
H
(
D
)
H(D)
H(D) 为
H
(
D
)
=
−
∑
k
=
1
K
∣
C
k
∣
∣
D
∣
⋅
l
o
g
2
∣
C
k
∣
∣
D
∣
H(D)=-\sum_{k=1}^K\frac{|C_k|}{|D|}\cdot log_2\frac{|C_k|}{|D|}
H(D)=−k=1∑K∣D∣∣Ck∣⋅log2∣D∣∣Ck∣
(2)条件熵
H
(
D
∣
A
)
H(D|A)
H(D∣A)
H
(
D
∣
A
)
=
−
∑
i
=
1
n
P
(
X
=
x
i
)
H
(
D
∣
X
=
x
i
)
=
−
∑
i
=
1
n
∣
D
i
∣
∣
D
∣
⋅
H
(
D
∣
X
=
x
i
)
=
−
∑
i
=
1
n
∣
D
i
∣
∣
D
∣
⋅
∑
k
=
1
K
∣
D
i
k
∣
∣
D
i
∣
l
o
g
2
∣
D
i
k
∣
∣
D
i
∣
\begin{array}{lll} H(D|A)&=&-\sum_{i=1}^nP(X=x_i)H(D|X=x_i)\\ &=&-\sum_{i=1}^n\frac{|D_i|}{|D|}\cdot H(D|X=x_i)\\ &=&-\sum_{i=1}^n\frac{|D_i|}{|D|}\cdot\sum_{k=1}^K\frac{|D_{ik}|}{|D_i|}log_2\frac{|D_{ik}|}{|D_i|} \end{array}
H(D∣A)===−∑i=1nP(X=xi)H(D∣X=xi)−∑i=1n∣D∣∣Di∣⋅H(D∣X=xi)−∑i=1n∣D∣∣Di∣⋅∑k=1K∣Di∣∣Dik∣log2∣Di∣∣Dik∣
(3)计算信息增益
g
(
D
,
A
)
=
H
(
D
)
−
H
(
D
∣
A
)
g(D, A)=H(D)-H(D|A)
g(D,A)=H(D)−H(D∣A)
根据上述算法,我们可以写出代码
import pandas as pd
import numpy as np
# 载入数据,并转化数据
# 是:1,否:-1
# 青年:0,中年:1,老年:2
# 一般:0,好:1,非常好:2
dic = {'是': 1, '否': -1, '青年': 0, '中年': 1, '老年': 2, '一般': 0, '好': 1, '非常好': 2}
# 转换函数
def convert(v):
return dic[v]
def load_data(file):
data = pd.read_csv(file, sep='\t', header=0, names=['id', 'age', 'work', 'house', 'credit', 'label'], index_col='id')
data = data.applymap(convert)
return data
x_y_data = load_data('D:/pycharm/myproject/tree.txt')
#print(x_y_data)
# 数据集熵 H(D)
def entropyOfData(D):
nums = len(D)
cate_data = D['label'].value_counts()
cate_data = cate_data / nums # 类别及其概率
# 计算数据集的熵
return sum(- cate_data * np.log2(cate_data))
# 条件熵H(D|A)
def conditional_entropy(D, A):
# 特征A的不同取值的概率
nums = len(D) # 数据总数
A_data = D[A].value_counts() # 特征A的取值及个数
A_data = A_data / nums # 特征A的取值及其对应的概率
#print(A_data)
# 特征A=a的情况下,数据集的熵
D_ik = D.groupby([A, 'label']).count().iloc[:, 0].reset_index() # Dik的大小
D_i = D.groupby(A).count().iloc[:, 0] # Di的大小
#print(D_ik)
#print(D_i)
# 给定特征A=a的情况下,数据集类别的概率
D_ik['possibilities'] = D_ik.apply(lambda x: x[2]/D_i[x[0]], axis=1)
#print(D_ik)
# 给定特征A=a的情况下,数据集的熵
entropy = D_ik.groupby(A)['possibilities'].apply(lambda x: sum(- x * np.log2(x)))
#print(entropy)
# 计算条件熵H(D|A)
return sum(A_data * entropy)
#print(conditional_entropy(x_y_data, 'work'))
# 信息增益
def info_gain(D, A):
return entropyOfData(D) - conditional_entropy(D, A)
#print(info_gain(x_y_data, 'credit'))
# 获得最优特征
def getOptiFeature(D):
Max = -1
Opti = ''
for A in list(D)[:-1]:
cur = info_gain(D, A)
if Max < cur:
Opti = A
Max = cur
return Opti, Max
# 选择最优特征
print(getOptiFeature(x_y_data))
('house', 0.4199730940219749)
上面的结果与《统计学习方法》第二版 例5.2 是一致的。
2. 决策树的生成
决策树的生成需要选取最优特征,根据上述过程选取的最优特征的生成算法,被称为 ID3。
具体生成算法如下:
- 输入:数据集 D D D、特征集 A A A的阈值 ϵ \epsilon ϵ
- 输出:决策树 T T T
(1)数据集
D
D
D 中所有数据属于同一类
C
k
C_k
Ck,则
T
T
T 设为单节点树,类别为
C
k
C_k
Ck,返回
T
T
T;
(2)特征集
A
=
∅
A=\empty
A=∅,则
T
T
T将数据集
D
D
D 实例数最大的类别
C
k
C_k
Ck 作为该节点的类别,返回该节点;
(3)选择最优特征
A
g
A_g
Ag
(4)如果最优特征的信息增益小于阈值
ϵ
\epsilon
ϵ,则设置
T
T
T 为单节点树,将
D
D
D 中实例数最大的类
C
k
C_k
Ck 作为该节点的类别,返回该节点;
(5)否则,根据
A
g
A_g
Ag 所有可能的取值
a
i
a_i
ai,根据
A
g
=
a
i
A_g=a_i
Ag=ai 将数据集
D
D
D 分割为若干非空子集
D
i
D_i
Di,以该子集
D
i
D_i
Di 为训练集,以
A
−
{
A
g
}
A-\{A_g\}
A−{Ag} 为特征集,返回步骤(1)构建子树
根据上述算法,可以写出如下代码:
# 决策树节点包括 内部节点与叶子结点
# 内部节点 Ag=最优特征,classification=None, rule=[a1, a2, ..., an], child=[child1, child2..., childn]
# 叶子结点 Ag=None, classification=实例数最大的类别,rule=[], child = []
class Node:
def __init__(self, Ag, rule=[], child=[], classification=None):
self.Ag = Ag
self.child = child
self.rule = rule
self.classification = classification
# 构建决策树
def ID3(D, epsilon=1e-5):
#print(D)
# 当前数据集的类别和数量
cate = D['label'].value_counts()
#print(cate)
# 特征集
A = list(D)[:-1]
#print(A)
# 数据集D中只有一个分类
if len(cate) == 1:
classification = list(cate.index)[0]
leaf_node = Node(None, [], [], classification)
return leaf_node
# 当前特征集A为空集
if len(A) == 0:
# 选择实例数最多的类别为最终类别
max_cate = cate.argmax()
leaf_node = Node(None, [], [], max_cate)
return leaf_node
# 寻找最优特征
optiFeature, infoGain = getOptiFeature(D)
print('最优特征为', optiFeature)
# 如果信息增益小于epsilon,则将当前节点定义为叶节点
if infoGain <= epsilon:
max_cate = cate.argmax()
leaf_node = Node(None, [], [], max_cate)
return leaf_node
# 建立当前节点
node = Node(optiFeature, [], [], None)
# 将数据集根据最优特征进行分割
for ai, Di in D.groupby(optiFeature):
new_Di = Di.drop(columns=[optiFeature])
#print(ai)
#print(new_Di)
#print(Di)
node.child.append(ID3(new_Di, epsilon))
node.rule.append(ai)
return node
# 生成决策树
root = ID3(x_y_data)
#输出
最优特征为 house
最优特征为 work
上面的输出意思为,有两次特征分割,第一次是 是否有房子,第二次是 是否有工作。可以查看《统计学习方法》第二版 例5.3,结果是对的。
为了进一步检查创建的决策树的正确性,我们可以写一个利用决策树预测的程序,如下:
# 验证决策树的正确性
# 数据x=[年龄, 工作, 房子,信贷情况]
features_to_index = {'age': 0, 'work': 1, 'house': 2, 'credit': 3}
def predict(node, x): # 决策树root给出x的类别
# 已经到达叶节点
if node.classification is not None:
return node.classification
# 当前节点的最优特征
cur_feature = node.Ag
feature_values = node.rule
childs = node.child
# 当前特征下x的值
x_feature = x[features_to_index[cur_feature]]
# 对应的子节点
index = feature_values.index(x_feature)
child = childs[index]
return predict(child, x)
# 输入x=[0, -1, -1, 0],其实对应着《统计学习方法》第二版 表5.1的第一行数据,类别是否,也就是-1
# 看看决策树能否给出正确类别
x = [0, -1, -1, 0]
print('数据x=', x, '对应的类别为-1')
print('决策树给出的类别为', predict(root, x))
数据x= [0, -1, -1, 0] 对应的类别为-1
决策树给出的类别为 -1
可以看到,是对的。
3. 决策树的剪枝
经过ID3生成算法生成的决策树可能过拟合,因此,我们需要进行剪枝操作,简化决策树,提高对未知数据的泛化能力。
决策树的剪枝是通过最小化损失函数来进行的。假设决策树
T
T
T 有
∣
T
∣
|T|
∣T∣ 个叶子结点,每个叶子结点
t
t
t 对应着
N
t
N_t
Nt 个样本点,其中,类别为
k
k
k的样本点有
N
t
k
N_{tk}
Ntk个,
H
t
(
T
)
H_t(T)
Ht(T)为叶子点
t
t
t上的经验熵,
α
≥
0
\alpha\ge0
α≥0为参数,则决策树的损失函数为
C
α
(
T
)
=
∑
t
=
1
∣
T
∣
N
t
H
t
(
T
)
+
α
∣
T
∣
=
−
∑
t
=
1
∣
T
∣
N
t
∑
k
=
1
K
N
t
k
N
t
l
o
g
2
N
t
k
N
t
+
α
∣
T
∣
=
−
∑
t
=
1
∣
T
∣
∑
k
=
1
K
N
t
k
l
o
g
2
N
t
k
N
t
+
α
∣
T
∣
\begin{array}{lll} C_{\alpha}(T)&=&\sum_{t=1}^{|T|}N_tH_t(T)+\alpha |T|\\ &=&-\sum_{t=1}^{|T|}N_t\sum_{k=1}^K\frac{N_{tk}}{N_t}log_2\frac{N_{tk}}{N_t}+\alpha |T|\\ &=&-\sum_{t=1}^{|T|}\sum_{k=1}^KN_{tk}log_2\frac{N_{tk}}{N_t}+\alpha |T| \end{array}
Cα(T)===∑t=1∣T∣NtHt(T)+α∣T∣−∑t=1∣T∣Nt∑k=1KNtNtklog2NtNtk+α∣T∣−∑t=1∣T∣∑k=1KNtklog2NtNtk+α∣T∣
这里要求每个叶节点处计算经验熵以及样本点个数,因此,我们可以将节点类型中加入经验熵和样本点个数,具体为
class Node:
def __init__(self, Ag, rule=[], child=[], classification=None, entropy=0, nums=0):
self.Ag = Ag
self.child = child
self.rule = rule
self.classification = classification
# 增加经验熵和样本点个数属性
self.entropy = entropy
self.nums = nums
我们在构建决策树的时候,要在每个节点计算该节点处的经验熵和样本点个数。
# 构建决策树
def ID3(D, epsilon=1e-5):
#print(D)
# 当前数据集的样本点个数
nums = len(D)
# 当前数据集的类别和数量
cate = D['label'].value_counts()
#print(cate)
# 特征集
A = list(D)[:-1]
#print(A)
# 数据集D中只有一个分类,因此,经验熵为0
if len(cate) == 1:
classification = list(cate.index)[0]
leaf_node = Node(None, [], [], classification, 0, nums)
return leaf_node
# 计算当前数据集的经验熵
entropy = entropyOfData(D)
# 当前特征集A为空集
if len(A) == 0:
# 选择实例数最多的类别为最终类别
max_cate = cate.argmax()
leaf_node = Node(None, [], [], max_cate, entropy, nums)
return leaf_node
# 寻找最优特征
optiFeature, infoGain = getOptiFeature(D)
print('最优特征为', optiFeature)
# 如果信息增益小于epsilon,则将当前节点定义为叶节点
if infoGain <= epsilon:
max_cate = cate.argmax()
leaf_node = Node(None, [], [], max_cate, entropy, nums)
return leaf_node
# 建立当前节点
node = Node(optiFeature, [], [], None, entropy, nums)
# 将数据集根据最优特征进行分割
for ai, Di in D.groupby(optiFeature):
new_Di = Di.drop(columns=[optiFeature])
#print(ai)
#print(new_Di)
#print(Di)
node.child.append(ID3(new_Di, epsilon))
node.rule.append(ai)
return node
决策树剪枝的具体算法为:
输入:决策树
T
T
T 和参数
α
\alpha
α
输出:剪枝后的决策树
T
T
T
(1) 计算每个叶子结点的经验熵
(2) 递归地从叶子结点往上缩,将其父节点当做叶子结点之前和之后的整体树分别记为
T
B
T_B
TB 和
T
A
T_A
TA,其对应的损失函数为
C
α
(
T
B
)
C_{\alpha}(T_B)
Cα(TB) 和
C
α
(
T
A
)
C_{\alpha}(T_A)
Cα(TA),如果
C
α
(
T
A
)
≤
C
α
(
T
B
)
C_\alpha(T_A)\le C_\alpha(T_B)
Cα(TA)≤Cα(TB)
则进行剪枝,将父节点更新为新的叶子结点
(3) 返回步骤(2),直到不能继续为止
实际上,这种剪枝可以局部进行的。假设当前节点是叶子结点 L L L,且它的父节点下的所有子节点均为叶子结点,如果将父节点 F F F作为新的叶子节点,那么损失函数的变化是
- 叶子结点数的变化: ( ∑ F ′ s c h i l d r e n L 1 ) − 1 \left(\sum_{F's~ children~ L}1\right)-1 (F′s children L∑1)−1
- 叶子结点处熵的变化: ( ∑ F ′ s c h i l d r e n L N L H L ( T ) ) − N F H F ( T ) \left(\sum_{F's~ children~ L}N_LH_L(T)\right)-N_FH_F(T) (F′s children L∑NLHL(T))−NFHF(T)
只要上述两个值的和大于0,就可以更新父节点为叶子结点。
抱歉,具体算法目前给不了,后面会加上!抱拳!
还有CART算法后面也会加上!
数据:数据来源于《统计学习方法》第二版 表5.1。可以直接复制粘贴为.txt文件。
ID 年龄 有工作 有自己的房子 信贷情况 类别
1 青年 否 否 一般 否
2 青年 否 否 好 否
3 青年 是 否 好 是
4 青年 是 是 一般 是
5 青年 否 否 一般 否
6 中年 否 否 一般 否
7 中年 否 否 好 否
8 中年 是 是 好 是
9 中年 否 是 非常好 是
10 中年 否 是 非常好 是
11 老年 否 是 非常好 是
12 老年 否 是 好 是
13 老年 是 否 好 是
14 老年 是 否 非常好 是
15 老年 否 否 一般 否