在这篇博客中,我们将主要介绍决策树算法。决策树算法主要由三个部分构成:特征的选择,决策树的构建以及决策树的剪枝X。
我们往往期望选择最有分类能力的特征。比如找对象这件事情上,女生面对众多追求者,往往会将经济能力、教育背景等作为区分的特征,因为这些特征最有分类能力;而很少女生将鞋码或者发型作为第一优考虑的特征。
经过特征选择,我们可以将原数据集分成若干子数据集,对于这些子数据集,再次进行特征选择,直到数据集里面类别基本相同或者已经没有特征可分。
上面的构建过程可能会造成过拟合。一个比较极端的情况是,决策树的叶子结点实际对应着一个或者少数几个数据,这就导致决策树对测试集的拟合特别好,但是对于未知数据,则可能表现很差。
为了选出最有分类能力的特征,我们首先介绍熵和信息增益。
熵是用来衡量随机变量的不确定程度的。假设离散随机变量 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)
一个特征所带来的信息增益为 当得知该特征的信息使得数据集的信息不确定性降低的程度。
设数据集 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 是一致的。
决策树的生成需要选取最优特征,根据上述过程选取的最优特征的生成算法,被称为 ID3。
具体生成算法如下:
(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
可以看到,是对的。
经过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作为新的叶子节点,那么损失函数的变化是
只要上述两个值的和大于0,就可以更新父节点为叶子结点。
抱歉,具体算法目前给不了,后面会加上!抱拳!
还有CART算法后面也会加上!
数据:数据来源于《统计学习方法》第二版 表5.1。可以直接复制粘贴为.txt文件。
ID 年龄 有工作 有自己的房子 信贷情况 类别
1 青年 否 否 一般 否
2 青年 否 否 好 否
3 青年 是 否 好 是
4 青年 是 是 一般 是
5 青年 否 否 一般 否
6 中年 否 否 一般 否
7 中年 否 否 好 否
8 中年 是 是 好 是
9 中年 否 是 非常好 是
10 中年 否 是 非常好 是
11 老年 否 是 非常好 是
12 老年 否 是 好 是
13 老年 是 否 好 是
14 老年 是 否 非常好 是
15 老年 否 否 一般 否