《统计学习方法》—— 信息增益、决策树算法(ID3,C4.5)以及python3实现

前言

在这篇博客中,我们将主要介绍决策树算法。决策树算法主要由三个部分构成:特征的选择,决策树的构建以及决策树的剪枝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=1npilog2pi

这里,我们强制令 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)=1log21j=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 0H(X)log2n

下面我们介绍条件熵 H ( Y ∣ X ) H(Y|X) H(YX)。假设 P ( X , Y ) P(X, Y) P(X,Y) 为联合概率分布,则条件熵 H ( Y ∣ X ) H(Y|X) H(YX)
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(YX)=i=1nP(X=xi)H(YX=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=DiCk,即类别为 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=1KDCklog2DCk
(2)条件熵 H ( D ∣ A ) H(D|A) H(DA)
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(DA)===i=1nP(X=xi)H(DX=xi)i=1nDDiH(DX=xi)i=1nDDik=1KDiDiklog2DiDik
(3)计算信息增益
g ( D , A ) = H ( D ) − H ( D ∣ A ) g(D, A)=H(D)-H(D|A) g(D,A)=H(D)H(DA)

根据上述算法,我们可以写出代码

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=1TNtHt(T)+αTt=1TNtk=1KNtNtklog2NtNtk+αTt=1Tk=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 (Fs children L1)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) (Fs children LNLHL(T))NFHF(T)

只要上述两个值的和大于0,就可以更新父节点为叶子结点。

抱歉,具体算法目前给不了,后面会加上!抱拳!
还有CART算法后面也会加上!

数据:数据来源于《统计学习方法》第二版 表5.1。可以直接复制粘贴为.txt文件。

ID	年龄	有工作	有自己的房子	信贷情况	类别
1	青年	否	否	一般	否
2	青年	否	否	好	否
3	青年	是	否	好	是
4	青年	是	是	一般	是
5	青年	否	否	一般	否
6	中年	否	否	一般	否
7	中年	否	否	好	否
8	中年	是	是	好	是
9	中年	否	是	非常好	是
10	中年	否	是	非常好	是
11	老年	否	是	非常好	是
12	老年	否	是	好	是
13	老年	是	否	好	是
14	老年	是	否	非常好	是
15	老年	否	否	一般	否
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值