机器学习之决策树

138 阅读11分钟

本文主要讲述了决策树的概念和代码实现,理论部分来自《统计学习方法》,实践部分来自《动手学机器学习》

决策树

组成与性质

  • 分类型决策树模型是对实例进行 分类的树形结构。结点分为 内部节点(表示特征或属性)叶节点(类别)

条件概率分布(比较抽象)

  • 决策树表示 特征条件下,类的条件概率分布。假设XX是表示 特征的随机变量,YY为表示类的集合,该条件概率表示为P(YX)P(Y|X)。实例看后面的例子.
  • 5b5e195a28293d3618828efb7d8d229.jpg
  • 上边的例子是二分类,当类别扩大的时候,我们取 条件概率最高的类别,认为x是这个类别

if-then规则

  • 可以把决策树看成一个 if-then规则的集合——由 根节点到叶节点的每一条路径 就对应一个规则。路径上的内部节点就是规则的条件(if),而叶节点就是 结论
  • if-then规则是互斥且完备的。互斥体现在树根节点到叶子节点都是互斥的,完备体现在每个类别(叶子)在集合中。

训练过程

  1. 构建根结点
  2. 选择 最优特征,以此来分割数据集
  3. 若子集被正确分类,构建叶结点,否则转2
  4. 重复2,3,直到所有训练数据子集被正确分类

特征选择

信息增益

  • 特征选择在于选取对训练数据具有分类能力,如果一个特征对分类 影响结果不大,说明这个特征没有分类能力。 评价特征好坏的准则是 信息增益/信息增益比
  • 特征AA对训练数据集DD的信息增益表示为 g(D,A)g(D,A) ,定义为经验熵 H(D)H(D)与特征A给定条件下D的经验条件熵之差 —— g(D,A)=H(D)H(DA)g(D,A)=H(D)-H(D|A)
  • H(D)H(D)表示的是数据集DD进行分类的不确定性,而H(DA)H(D|A)表示在 特征A条件下信息分类的不确定性,二者之差(即信息增益)表示 由于AD分类不确定性减少的程度,信息增益越大,分类能力越好。然而使用 信息增益作为划分,存在偏向 取值较多的特征问题,可以使用 信息增益比作为校正.

具体计算

  • 1712804183907.png
  1. 首先计算 H(D)H(D),D是指输出分类(**类别 是/否)——上图中的否有6个,是有9个。H(D)=915log2915615log2615=0.971H(D)=-\frac{9}{15}*\log_2{\frac{9}{15}}-\frac{6}{15}\log_2\frac{6}{15}=0.971
  2. 计算各个 特征信息增益,用A1,A2,A3,A4分别表示年龄,有工作,有自己的房子,信贷情况A_1,A_2,A_3,A_4分别表示年龄,有工作,有自己的房子,信贷情况 四个特征.
  3. 计算年龄的信息增益g(D,A1)=H(D)[p1H(D1)+p2H(D2)+p3H(D3)]g(D,A_1)=H(D)-[p_1*H(D_1)+p_2*H(D_2)+p_3*H(D_3)] ,年龄有青年(5个),中年(5个),老年(5个),需要分别计算该条件下的 信息熵H(D1),H(D2),H(D3)H(D_1),H(D_2),H(D_3)
    • 青年中,2是3否,故:H(D1)=1log225log235H(D_1)=-1*\log_2\frac{2}{5}-\log_2\frac{3}{5}
    • 中年中, 3是2否,故:H(D2)=1log235log225H(D_2)=-1*\log_2\frac{3}{5}-\log_2\frac{2}{5}
    • 老年中, 4是1否, 故:H(D3)=1log245log215H(D_3)=-1*\log_2\frac{4}{5}-\log_2\frac{1}{5}
  4. g(D,A1)=H(D)[515H(D1)+515H(D2)+515H(D3)]=0.083g(D,A_1)=H(D)-[\frac{5}{15}*H(D_1)+\frac{5}{15}*H(D_2)+\frac{5}{15}*H(D_3)]=0.083 ,同理,去计算g(D,A2)=0.324,g(D,A3)=0.420,g(D,A4)=0.363g(D,A_2)=0.324,g(D,A_3)=0.420,g(D,A_4)=0.363
  5. 比较可知,A3A_3的信息增益最大,所以选择 A3A_3 作为最优特征

  • 表示 随机变量不确定性的度量。 XX是一个有限个值的离散随机变量,概率分布是 P(X=xi)=pi,i=1,2,..,nP(X=x_i)=p_i,i=1,2,..,n,则熵为 H(x)=i=1npilog(pi)H(x)=-\sum_{i=1}^{n}p_i*log(p_i)
  • 随机变量的取值 等概率分布的时候,相应的熵最大0<=H(p)<=log2(n)0<=H(p)<=log_2{(n)}
条件熵
  • 条件熵 中的概率 是通过 数据估计 得到时,则为 经验熵经验条件熵。数学表示为H(YX)=1i=1npiH(YX=xi)H(Y|X)=-1*\sum_{i=1}^n{p_i * H(Y|X=x_i)}

信息增益比

  • 在[[#信息增益]]中提到,单纯的增益存在偏向 取值较多的情况,我们要除以一个 训练集D关于特征A的熵之比,称为HA(D)H_A(D),HA(D)=1i=1n,DiDlog2DiD,n为特征数目H_A(D)=-1*\sum_{i=1}^n,\frac{D_i}{D}\log_2\frac{D_i}{D},n为特征数目 ,
  • 信息增益比写为gR(D,A)g_R(D,A),gR(D,A)=g(D,A)HA(D)g_R(D,A)=\frac{g(D,A)}{H_A(D)},以A1A_1(年龄)为例子,HA1(D)=H_{A_1}(D)= 515log2515515log2515515log2515-\frac{5}{15}*\log_2\frac{5}{15}-\frac{5}{15}*\log_2\frac{5}{15}-\frac{5}{15}*\log_2\frac{5}{15}

决策树的生成

ID3算法

输入与输出

  • 输入:训练集DD,特征集AA,阈值ϵ\epsilon.
  • 输出:决策树T

算法

  1. 判断T是否需要选择特征生成决策树(特征集A的大小<=1)
    • 若D中的实例都是 同一类,记录实例类别CKC_K,返回T
    • 若实例没有 特征,则T为单节点树,记录D中实例个数最多的类别CKC_K,返回T
  2. 否则计算AA中各个特征的 [[#信息增益]],并且选择 信息增益最大的特征AgA_g
    • Ag<=ϵA_g<=\epsilon,则T为单节点树,返回 实例个数最多的类别CkC_k,然后返回T
    • Ag>ϵA_g>\epsilon,按照AgA_g 的取值,把DD划分为若干的非空子集DiD_i,将DiD_i中实例个数最多的类别作为 标记,构建 子节点,并且和其他子集DiD_i 构成一颗树,递归地构建,最后返回T.
    • 对于第ii个结点,以DiD_i作为训练集,AAgA-A_g作为特征集合,重复以上步骤

C4.5算法

  • 与[[#ID3算法]]唯一不同的是,把 信息增益替换成 信息增益比
  • 能够处理 连续变量的情况,而 ID3算法不行

决策树的实现(代码部分)

  • 以下代码手动实现了一颗C4.5的决策树,请注意,这个决策树限制了是 二叉树,并没有实现 多叉树。它的构造是这样的,对于每个特征,选择xbx_b 为信息增益最大的点,分为了两部分,一部分是小于xbx_b的部分,另一部分是 大于xbx_b部分。
  
class Node:  
    def __init__(self):  
        # 如果是内部结点,表示特征编号  
        # 叶子结点表示分类结果  
        self.feat = None  
        # 分类值的列表  
        self.split = None  
        # 儿子结点  
        self.child = []  
#%%  
  
class DecisionTree:  
    def __init__(self,X,Y,feat_ranges,lbd):  
        self.root = Node()  
        self.X = X   
        self.Y = Y  
        self.feat_ranges = feat_ranges # 特征值的取值集合  
        self.lbd = lbd # 正则化约束强度  
        self.eps = 1e-8  
        self.T = 0 #记录叶子结点的个数  
        self.ID3(self.root,self.X,self.Y)  
    def aloga(self,a):  
        # 自定义的一个log2,防止出现log2(0)的情况  
        return a*np.log2(self.eps + a)  
    def entropy(self,Y):  
        cnt = np.unique(Y,return_counts=True)[1] # 统计每个类别的出现次数  
        N = len(Y)  
        # 要返回的熵是 每个类别-1* { log2(cnt_i/N) }  
        return -1 * np.sum([self.aloga(Ni/N) for Ni in cnt])  
    # 递归地分裂节点,构造一颗决策树  
    def info_gain(self,X,Y,feat,val):  
        # 信息增益等于 H(D) - sum { pi * log2(pi) }        # 这里简单地分为两类,一类是 信息熵 <=val ,另一类是 信息熵 >= val        # 通过枚举val,保存最佳的一个划分方法  
        N = len(Y)  
        if(N==0):  
            return 0  
        HX = self.entropy(Y) # 计算Y的信息熵  
        HXY = 0   
Y_l = Y[X[:,feat]<=val]  
        HXY += len(Y_l) / len(Y) * self.entropy(Y_l)  
        Y_r = Y[X[:,feat]>val]   
        HXY += len(Y_r) / len(Y) * self.entropy(Y_r)  
        return HX - HXY  
      
    def entropy_YX(self,X,Y,feat,val):  
        HXY = 0  
        N = len(Y)  
        if N==0:  
            return 0  
        # 此处是计算信息增益比的分母部分  
        # 分母是要求一个H_D(A) = - sum{ log2(cnt_i / N)}  
        Y_l = Y[X[:,feat]<=val]  
        HXY += len(Y_l)/ N * -self.aloga(len(Y_l) / N)  
        Y_r = Y[X[:,feat]>val]  
        HXY += len(Y_r) / N * -self.aloga(len(Y_r) / N)  
        return HXY  
      
    def info_gain_ratio(self,X,Y,feat,val):  
        IG = self.info_gain(X,Y,feat,val) # 计算信息增益 Information gain        HYX = self.entropy_YX(X,Y,feat,val) # 计算要除的 训练集D 除以 特征A的 熵  
        return IG / HYX  
    def ID3(self,node,X,Y):  
        # 对应步骤1,判断特征集中的数目是否<=1,若是,停止分裂  
        if len(np.unique(Y)) == 1:  
            node.feat = Y[0]  
            self.T +=1  
            return   
          
best_IGR = 0  
        best_feat = None  
        best_val = None  
        # feat_names是特征的名字集合  
        # feat_ranges是 对应特征的取值集合  
        for feat in range(len(feat_names)):  
            for val in feat_ranges[feat_names[feat]]:  
                # 找到最优的信息增益比  
                IGR = self.info_gain_ratio(X,Y,feat,val)  
                if IGR > best_IGR:  
                    best_IGR = IGR  
                    best_feat = feat  
                    best_val = val  
          
        cur_cost = len(Y) * self.entropy(Y) + self.lbd  
        if best_feat is None:  
            # 说明最优的信息增益为0,再分类也没办法增加信息了  
            new_cost = np.inf  
        else:  
            new_cost = 0  
            x_feat = X[:,best_feat]  
            new_Y_l = Y[x_feat<=best_val]  
            new_cost += len(new_Y_l) * self.entropy(new_Y_l)  
            new_Y_R = Y[x_feat>best_val]  
            new_cost += len(new_Y_R) * self.entropy(new_Y_R)  
            new_cost += 2 * self.lbd  
          
        if new_cost <= cur_cost:  
            # 如果分裂的代价更小,就执行  
            node.feat = best_feat  
            node.split = best_val  
            l_child = Node()  
            l_X = X[x_feat<=best_val]  
            l_Y = Y[x_feat<=best_val]  
            self.ID3(l_child,l_X,l_Y) # 递归构造左子树  
            r_child = Node()  
            r_X = X[x_feat>best_val]  
            r_Y = Y[x_feat>best_val]  
            self.ID3(r_child,r_X,r_Y)  
            node.child = [l_child,r_child]  
        else:  
            # 直接返回数目最多的类别  
            vals,cnt = np.unique(Y,return_counts=True)  
            node.feat = vals[np.argmax(cnt)]  
            self.T +=1  
    def predict(self,X):  
        node = self.root#从根节点往下找  
        while node.split is not None:  
            if X[node.feat] <= node.split:  
                node = node.child[0] # 往左子树走  
            else:  
                node = node.child[1]  
        # 到达叶结点,返回类别  
        return node.feat  
    def accuracy(self,X,Y):  
        correct = 0  
        for x,y in zip(X,Y):  
            pred = self.predict(x)  
            if pred == y:  
                correct+=1  
        return correct/len(Y)

下面拿这份代码跑一份什么泰坦尼克号游客生还的代码(源于书籍《动手学机器学习》)

import numpy as np  
import matplotlib.pyplot as plt  
import pandas as pd  
# 读取数据  
data = pd.read_csv('titanic/train.csv')  
# 查看数据集信息和前5行具体内容,其中NaN代表数据缺失  
print(data.info())  
print(data[:5])  
  
# 删去编号、姓名、船票编号3列  
data.drop(columns=['PassengerId', 'Name', 'Ticket'], inplace=True)  
feat_ranges = {}  
cont_feat = ['Age', 'Fare']  # 连续特征  
bins = 10  # 分类点数  
  
for feat in cont_feat:  
    # 数据集中存在缺省值nan,需要用np.nanmin和np.nanmax  
    min_val = np.nanmin(data[feat])  
    max_val = np.nanmax(data[feat])  
    feat_ranges[feat] = np.linspace(min_val, max_val, bins).tolist()  
    print(feat, ':')  # 查看分类点  
    for spt in feat_ranges[feat]:  
        print(f'{spt:.4f}')  
# 只有有限取值的离散特征  
cat_feat = ['Sex', 'Pclass', 'SibSp', 'Parch', 'Cabin', 'Embarked']  
for feat in cat_feat:  
    data[feat] = data[feat].astype('category')  # 数据格式转为分类格式  
    print(f'{feat}{data[feat].cat.categories}')  # 查看类别  
    data[feat] = data[feat].cat.codes.to_list()  # 将类别按顺序转换为整数  
    ranges = list(set(data[feat]))  
    ranges.sort()  
    feat_ranges[feat] = ranges  
# 将所有缺省值替换为-1  
data.fillna(-1, inplace=True)  
for feat in feat_ranges.keys():  
    feat_ranges[feat] = [-1] + feat_ranges[feat]  
# 划分训练集与测试集  
np.random.seed(0)  
feat_names = data.columns[1:]  
label_name = data.columns[0]  
data = data.reindex(np.random.permutation(data.index))  
ratio = 0.8  
split = int(ratio * len(data))  
train_x = data[:split].drop(columns=['Survived']).to_numpy()  
train_y = data['Survived'][:split].to_numpy()  
test_x = data[split:].drop(columns=['Survived']).to_numpy()  
test_y = data['Survived'][split:].to_numpy()  
print('训练集大小:', len(train_x))  
print('测试集大小:', len(test_x))  
print('特征数:', train_x.shape[1])  
#%%  
class Node:  
    def __init__(self):  
        # 如果是内部结点,表示特征编号  
        # 叶子结点表示分类结果  
        self.feat = None  
        # 分类值的列表  
        self.split = None  
        # 儿子结点  
        self.child = []  
#%%  
  
class DecisionTree:  
    def __init__(self,X,Y,feat_ranges,lbd):  
        self.root = Node()  
        self.X = X   
        self.Y = Y  
        self.feat_ranges = feat_ranges # 特征值的取值集合  
        self.lbd = lbd # 正则化约束强度  
        self.eps = 1e-8  
        self.T = 0 #记录叶子结点的个数  
        self.ID3(self.root,self.X,self.Y)  
    def aloga(self,a):  
        # 自定义的一个log2,防止出现log2(0)的情况  
        return a*np.log2(self.eps + a)  
    def entropy(self,Y):  
        cnt = np.unique(Y,return_counts=True)[1] # 统计每个类别的出现次数  
        N = len(Y)  
        # 要返回的熵是 每个类别-1* { log2(cnt_i/N) }  
        return -1 * np.sum([self.aloga(Ni/N) for Ni in cnt])  
    # 递归地分裂节点,构造一颗决策树  
    def info_gain(self,X,Y,feat,val):  
        # 信息增益等于 H(D) - sum { pi * log2(pi) }        # 这里简单地分为两类,一类是 信息熵 <=val ,另一类是 信息熵 >= val        # 通过枚举val,保存最佳的一个划分方法  
        N = len(Y)  
        if(N==0):  
            return 0  
        HX = self.entropy(Y) # 计算Y的信息熵  
        HXY = 0   
Y_l = Y[X[:,feat]<=val]  
        HXY += len(Y_l) / len(Y) * self.entropy(Y_l)  
        Y_r = Y[X[:,feat]>val]   
        HXY += len(Y_r) / len(Y) * self.entropy(Y_r)  
        return HX - HXY  
      
    def entropy_YX(self,X,Y,feat,val):  
        HXY = 0  
        N = len(Y)  
        if N==0:  
            return 0  
        # 此处是计算信息增益比的分母部分  
        # 分母是要求一个H_D(A) = - sum{ log2(cnt_i / N)}  
        Y_l = Y[X[:,feat]<=val]  
        HXY += len(Y_l)/ N * -self.aloga(len(Y_l) / N)  
        Y_r = Y[X[:,feat]>val]  
        HXY += len(Y_r) / N * -self.aloga(len(Y_r) / N)  
        return HXY  
      
    def info_gain_ratio(self,X,Y,feat,val):  
        IG = self.info_gain(X,Y,feat,val) # 计算信息增益 Information gain        HYX = self.entropy_YX(X,Y,feat,val) # 计算要除的 训练集D 除以 特征A的 熵  
        return IG / HYX  
    def ID3(self,node,X,Y):  
        # 对应步骤1,判断特征集中的数目是否<=1,若是,停止分裂  
        if len(np.unique(Y)) == 1:  
            node.feat = Y[0]  
            self.T +=1  
            return   
          
best_IGR = 0  
        best_feat = None  
        best_val = None  
        # feat_names是特征的名字集合  
        # feat_ranges是 对应特征的取值集合  
        for feat in range(len(feat_names)):  
            for val in feat_ranges[feat_names[feat]]:  
                # 找到最优的信息增益比  
                IGR = self.info_gain_ratio(X,Y,feat,val)  
                if IGR > best_IGR:  
                    best_IGR = IGR  
                    best_feat = feat  
                    best_val = val  
          
        cur_cost = len(Y) * self.entropy(Y) + self.lbd  
        if best_feat is None:  
            # 说明最优的信息增益为0,再分类也没办法增加信息了  
            new_cost = np.inf  
        else:  
            new_cost = 0  
            x_feat = X[:,best_feat]  
            new_Y_l = Y[x_feat<=best_val]  
            new_cost += len(new_Y_l) * self.entropy(new_Y_l)  
            new_Y_R = Y[x_feat>best_val]  
            new_cost += len(new_Y_R) * self.entropy(new_Y_R)  
            new_cost += 2 * self.lbd  
          
        if new_cost <= cur_cost:  
            # 如果分裂的代价更小,就执行  
            node.feat = best_feat  
            node.split = best_val  
            l_child = Node()  
            l_X = X[x_feat<=best_val]  
            l_Y = Y[x_feat<=best_val]  
            self.ID3(l_child,l_X,l_Y) # 递归构造左子树  
            r_child = Node()  
            r_X = X[x_feat>best_val]  
            r_Y = Y[x_feat>best_val]  
            self.ID3(r_child,r_X,r_Y)  
            node.child = [l_child,r_child]  
        else:  
            # 直接返回数目最多的类别  
            vals,cnt = np.unique(Y,return_counts=True)  
            node.feat = vals[np.argmax(cnt)]  
            self.T +=1  
    def predict(self,X):  
        node = self.root#从根节点往下找  
        while node.split is not None:  
            if X[node.feat] <= node.split:  
                node = node.child[0] # 往左子树走  
            else:  
                node = node.child[1]  
        # 到达叶结点,返回类别  
        return node.feat  
    def accuracy(self,X,Y):  
        correct = 0  
        for x,y in zip(X,Y):  
            pred = self.predict(x)  
            if pred == y:  
                correct+=1  
        return correct/len(Y)  
#%%  
DT = DecisionTree(train_x,train_y,feat_ranges,lbd=1.0)  
print('叶结点数量: ',DT.T)  
print('训练集准确率: ',DT.accuracy(train_x,train_y))  
print('测试集准确率: ',DT.accuracy(test_x,test_y))
  • 在测试集上表现只有70多,非常感人