决策树:ID3、C4.5、CART算法与Python实现

一、决策树的基本概念

决策树(Decision Tree)算法是一类常用的机器学习算法,在分类问题中,决策树算法通过样本中某一些属性的值,将样本划分到不同的类别中。
决策树跟人在做决策的思考方式很想像,先考虑重点选项,不符合则可最优先做出决策。

长得帅
不帅
才华横溢
会认字
教练级
小白
热爱
早鸭子
相亲对象
有才吗?
不去
喜欢羽毛球吗?
不去
喜欢游泳吗?
不去

1. 划分标准

在决定是否去见相亲对象时,该女提出了4个特征来做决策,这些特征在做决策的过程中是存在一定顺序的,首先选择了相貌,可能是因为她是外貌协会,这个通不过就不打算再考虑后续的特征了,决策性较强。
在决策树的算法中,通常用标准来确定特征的先后顺序:信息增益(Information Gain)、增益率(Gain Ratio)和基尼指数(Gini Index)。
首先介绍一下 熵(Entropy) 的概念,熵是度量样本集合纯度最常用的指标,对于包含m个训练样本的数据集:D{(X(1),y(1)),…,(X(m),y(m))}, 在数据集D中,第k类的样本所占的比例为pk,则数据集D的信息熵为:
E n t r o p y ( D ) = k = 1 k p k   l o g 2   p k Entropy(D) =- \sum_{k=1}^k p_k\space log_2\space p_k
其中,k表示数据集D中类别的个数,pk为类别k的数量占数据集总数量的比例。

假定有10个相亲对象,各特征值如下:

相关对象 相貌 才华 羽毛球 游泳 是否去见
1 会识字 小白 热爱 不去
2 不帅 会识字 教练级 热爱 不去
3 不帅 会识字 小白 早鸭子 不去
4 不帅 才华横溢 小白 热爱 不去
5 不帅 会识字 小白 热爱 不去
6 不帅 会识字 小白 早鸭子 不去
7 才华横溢 教练级 热爱
8 不帅 会识字 小白 早鸭子 不去
9 不帅 会识字 小白 热爱 不去
10 不帅 会识字 教练级 早鸭子 不去

以如上数据为例,其信息熵为:
E n t r o p y ( D ) = k = 1 2 p k   l o g 2   p k = ( 1 10 l o g 2 1 10 + 9 10 l o g 2 9 10 ) = 0.469 Entropy(D) =- \sum_{k=1}^2 p_k\space log_2\space p_k=-({1 \over 10} log_2{1 \over 10} + {9 \over 10} log_2{9 \over 10}) =0.469

from math import log
from collections import defaultdict

def calculate_entropy(data):
	total_sample = len(data)
	if len(data) == 0:
		return 0
	label_counts = label_unique_cnt(data)   #统计数据集中不同标签的个数

	# 计算数据集中的Entropy
	entropy = 0
	for label in label_counts.keys():
		class_ratio = label_counts[label]/total_sample
		entropy -=  class_ratio * log(class_ratio,2)
	return entropy

def label_unique_cnt(data):
	label_unique_cnt = defaultdict(int)
	
	for x in data:
		label = x[len(x) -1]   # 取得每一个样本的类标签
		label_unique_cnt[label] += 1
	return label_unique_label

当样本按照特征“相貌”划分成两个子数据集D1和D2时,此时整个数据集D的熵为两个独立数据集D1和D2的熵的加权和,即:
E n t r o p y ( D ) = [ 2 10 ( 1 2 l o g 2 1 2 + 1 2 l o g 2 1 2 ) + 8 10 ( l o g 2 1 ) ] = 0.2 Entropy(D) =-[{2 \over 10}({1 \over 2} log_2{1 \over 2} + {1 \over 2} log_2{1 \over 2})+{8 \over 10}( log_2{1}) ] =0.2
由上述的划分可知,在用“相貌”划分后数据集D的信息熵减小了,对于给定的数据集,划分前后数据集信息熵的减少量称为__信息增益(Information Gain)__,即:
I G ( D , ) = 0.469 0.2 = 0.269 IG(D, 相貌) = 0.469-0.2 = 0.269
在选择数据集划分的标准时,通过选择能够使得信息增益最大的划分。
ID3决策树算法就是利用信息增益作为划分数据集的一种方法。

增益率(Gain Ratio) 是可以作为最优划分属性的方法,
G a i n R a t i o ( D , A ) = I G ( D , A ) I V ( A ) GainRatio(D, A) = { IG(D, A) \over IV(A)}
其中,IV(A)是特征A的"固有值(Intrinsic Value)",即
I V ( A ) = p = 1 p D   p   D   l o g 2   D   p   D IV(A) = - \sum_{p=1}^p {\vert D~p~\vert \over \vert D\vert}\space log_2\space {\vert D~p~\vert \over \vert D\vert}
意思是指按特征A划分成两个子数据集的信息熵,不考虑类别的情况,举例说明:
I V ( ) = 2 10 l o g 2 2 10 8 10 l o g 2 8 10 = 0.72 IV(相貌) = -{2 \over 10}log_2{2 \over 10} - {8 \over 10} log_2{8 \over 10} =0.72
G a i n R a t i o ( D , ) = I G ( D , A ) I V ( A ) = 0.269 0.72 = 0.374 GainRatio(D, 相貌) = { IG(D, A) \over IV(A)} = {0.269 \over 0.72 }= 0.374
在著名的C4.5决策树算法中就是利用增益率作为划分数据集的方法。

基尼指数(Gini Index) 也可以选择最优的划分属性,Gini值越小表示纯度越高。对于数据集D,假设有n个分类,则样本属于第k个分类的概率为pk,则此概率分布的基尼指数为:
G i n i ( D ) = 1 k = 1 k ( C k D ) 2 Gini(D) = 1 - \sum_{k=1}^k ({ {\vert C_k \vert} \over {\vert D \vert} })^2
其中,Ck表示数据集D中属于类别k的样本个数。
还是利用相亲对象的数据为例,数据集的基尼指数为:
G i n i ( D ) = 1 k = 1 2 ( p k ) 2 = 1 [ ( 1 10 ) 2 + ( 9 10 ) 2 ] = 0.18 Gini(D) = 1 - \sum_{k=1}^2 (p_k)^2 = 1 - [({1 \over 10})^2 + ({9 \over 10} )^2] = 0.18
利用特征“相貌”将数据集划分后,基尼指数为:
G i n i ( D , ) = 2 10 [ 1 ( ( 1 2 ) 2 + ( 1 2 ) 2 ) ] + 8 10 [ 1 1 ] = 0.1 Gini(D,相貌) = {2 \over 10}[1 - \Big( ({1 \over 2})^2 + ({1 \over 2})^2 \Big)] + {8 \over 10}[1-1] = 0.1
在CART决策树算法中利用Gini指数作为划分数据集的方法。


下面用Python来计算Gini指数:

from math import pow

def calculate_gini_index(data):
	total_sample = len(data)
	if len(data) == 0:
		return 0
	label_counts = label_unique_cnt(data)   #统计数据集中不同标签的个数

	# 计算数据集中的gini指数
	gini = 0
	for label in label_counts.keys():
		gini += pow(label_counts[label],2)
	
	gini = 1- float(gini)/pow(total_sample,2)
	return gini

2.停止划分的标准

在按照特征对数据集进行进行划分时,需设置划分的终止条件。通常在算法过程中设置终止条件的方法有:

  1. 结点中的样本数小于给定阈值
  2. 样本集的Gini指数小于给定值
  3. 没有更多特征

在sklearn的决策树模型中,就有相关的参数用来设置终止划分的条件:
DecisionTreeClassifier
class sklearn.tree.DecisionTreeClassifier(criterion=’gini’, splitter=’best’, max_depth=None, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features=None, random_state=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, class_weight=None, presort=False)
max_depth - 最大深度,因为有些特征可能被重复选择用来划分,导致可能出深度相对特征会大得多的情况;
min_samples_split - 被划分时,该结点所需含的最少数量;
min_samples_leaf - 被划分后的叶结点中最少应包含的数量;
min_weight_fraction_leaf - 叶结点中,相对总数据集的最小加权分数
max_leaf_nodes - 叶结点数上限
min_impurity_decrease - 不纯度减少的阈值,若该结点被划分后减少的不纯度>=此值,则将被划分
min_impurity_split - 不纯度阈值,只有该结点的不纯度>=此值,该结点才会被划分,否则为叶结点


二、ID3算法

Iterative Dichotomizer, 迭代二分类器。
算法核心:在决策树各个子结点上应用信息增益准则来选择特征,递归地构建决策树,已经用来划分过的特征将不再重复使用。
只有树的生成,容易过拟合,分得太细,考虑条件太多。

缺点:1)用信息增益选择特征时,偏向于选择分枝比较多的属性值;
2)不能处理连续属性

三、C4.5算法

对ID3算法的改进,主要为:

  1. 用信息增益率来选择属性
  2. 能处理非离散数据
  3. 能处理不完整数据(抛弃有特征值缺失的样本)
  4. 决策树构造后会对树进行剪枝

连续型变量计算信息增益:由小->大递增排序,取相邻两个值的中点作为分裂点,然后按离散型来计算IG,取最大IG的点为分裂点,如连续数据列(10.2, 12, 14, 16, 18, …):
10.2 12 1 1.1   12 14 1 3   14 16 1 5   16 18 1 7   . . . \underbrace{10.2 | 12}_{\text 11.1} \space \underbrace{12 | 14}_{\text 13}\space \underbrace{14 | 16}_{\text 15}\space \underbrace{16 | 18}_{\text 17} \space ...
以分裂点11.1, 13, 15, 17, …分别计算IG,假如在数值13的IG是最大的,则以13来将数据划分成: <13 和 >13两个子数据集。

四、CART算法

Classification And Regression Tree, 分类与回归树,属于二叉树,可创建分类树和回归树,既能处理分类问题也可以处理回归问题。

  1. CART作为分类树
    分类属性是离散类型,特征属性可以是离散型或连续型。
    在节点分裂时,使用Gini指数作为划分的指标,遍历所有属性及其可能的分裂点,寻找最佳切分属性及其最佳分裂点,使得切分后的Gini指数最小,利用该属性及其最佳分裂点将训练数据划分成两个子数据集,重复该划分步骤,直到子数据集均为同一类别或者用尽特征。

对于取值数N>=3的特征属性,因只能有两个分支,需人为创建二取值序列并取使切分之后的Gini指数最小的点作为树分叉决策点。
例如:size取值为[“S”,“M”,“L”],二分序列有三种情况:

  • [“S", (“M”,“L”)]
  • [“M”,(“S”,“L”)]
  • [“L”,(“S”,“M”)]

对于连续属性,需转换成离散属性:

  1. 先排序(升序)
  2. 取中点作为可能分裂点,计算那些使分类属性发生改变的特征值的Gini指数
  3. 选择Gini指数最小的点作为该特征的最佳分裂点

注意:离散特征分支划分数据集时,子数据集中不再包含该特征;而连续特征分支时,各子数据集依旧包含该特征,在接下来的树分支过程中可能被再次用来树分支。

class node:
    '''树的节点的类
    '''
    def __init__(self, fea=-1, value=None, results=None, right=None, left=None):
        self.fea = fea  # 用于切分数据集的属性的列索引值
        self.value = value  # 设置划分的值
        self.results = results  # 存储叶节点所属的类别
        self.right = right  # 右子树
        self.left = left  # 左子树
        
def split_tree(data, fea, value):
    '''根据特征fea中的值value将数据集data划分成左右子树
    input:  data(list):数据集
            fea(int):待分割特征的索引
            value(float):待分割的特征的具体值
    output: (set1,set2)(tuple):分割后的左右子树
    '''
    set_1 = []
    set_2 = []
    for x in data:
        if x[fea] >= value:
            set_1.append(x)
        else:
            set_2.append(x)
    return (set_1, set_2)

def build_tree(data):
    '''构建树
    input:  data(list):训练样本
    output: node:树的根结点
    '''
    # 构建决策树,函数返回该决策树的根节点
    if len(data) == 0:
        return node()
    
    # 1、计算当前的Gini指数
    currentGini = calculate_gini_index(data)
    
    bestGain = 0.0
    bestCriteria = None  # 存储最佳切分属性以及最佳切分点
    bestSets = None  # 存储切分后的两个数据集
    
    feature_num = len(data[0]) - 1  # 样本中特征的个数
    # 2、找到最好的划分
    for fea in range(0, feature_num):
        # 2.1、取得fea特征处所有可能的取值
        feature_values = {}  # 在fea位置处可能的取值
        for sample in data:  # 对每一个样本
            feature_values[sample[fea]] = 1  # 存储特征fea处所有可能的取值
        
        # 2.2、针对每一个可能的取值,尝试将数据集划分,并计算Gini指数
        for value in feature_values.keys():  # 遍历该属性的所有切分点
            # 2.2.1、 根据fea特征中的值value将数据集划分成左右子树
            (set_1, set_2) = split_tree(data, fea, value)
            # 2.2.2、计算当前的Gini指数
            nowGini = float(len(set_1) * calculate_gini_index(set_1) +\
            len(set_2) * calculate_gini_index(set_2)) / len(data)
            # 2.2.3、计算Gini指数的增加量
            gain = currentGini - nowGini
            # 2.2.4、判断此划分是否比当前的划分更好
            if gain > bestGain and len(set_1) > 0 and len(set_2) > 0:
                bestGain = gain
                bestCriteria = (fea, value)
                bestSets = (set_1, set_2)
    
    # 3、判断划分是否结束
    if bestGain > 0:
        right = build_tree(bestSets[0])
        left = build_tree(bestSets[1])
        return node(fea=bestCriteria[0], value=bestCriteria[1],\
        right=right, left=left)
    else:
        return node(results=label_unique_cnt(data))  
        # 返回当前的类别标签作为最终的类别标签
        
def predict(sample, tree):
    '''对每一个样本sample进行预测
    input:  sample(list):需要预测的样本
            tree(类):构建好的分类树
    output: tree.results:所属的类别
    '''
    # 1、只是树根
    if tree.results != None:
        return tree.results
    else:
    # 2、有左右子树
        val_sample = sample[tree.fea]
        branch = None
        if val_sample >= tree.value:
            branch = tree.right
        else:
            branch = tree.left
        return predict(sample, branch)
  1. CART作为回归树
    <待补充>

参考的文献
——————————————————————————————————————
[1] 赵志勇. Python机器学习算法. 北京: 电子工业出版社. 2017.
[2] Wikipedia. ID3算法,https://en.wikipedia.org/wiki/ID3_algorithm
[3] Wikipedia. C4.5算法,https://en.wikipedia.org/wiki/C4.5_algorithm

猜你喜欢

转载自blog.csdn.net/sinat_41939868/article/details/88391494
今日推荐