决策树

理论

什么是决策树?

决策树内部节点表示一个特征或属性,叶节点表示一个类,进行分类时,从根节点开始,对实例的某一特征进行测试,根据测试结果,将实例分配到其子节点,这时每一个子节点对应着该特征的一个取值,如此递归地对实例进行测试并分配,直至到达叶节点,最后将实例分到叶节点地类中。
可以将决策树看成一个if-then规则的集合,每一个实例都被一条路径或一条规则所覆盖,而且只被一条路径或一条规则所覆盖,互斥并且完备。同时,决策树还表示给定特征条件下类的条件概率分布,定义了特征空间的一个划分,分为互不相交的几个单元,决策树的一条路径就对应划分中的一个单元,决策树分类时将实例分到条件概率大的一类去。

决策树学习

决策树学习通常包括3个步骤:特征选择、决策树的生成和决策树的剪枝。决策树学习的算法通常是一个递归地选择最优特征,并根据该特征对训练数据进行分割,使得对各个子数据集有一个最好的分类的过程,这一过程对应着特征空间的划分,也对应着决策树的构建。为了防止过拟合,我们需要对生成的决策树自下而上进行剪枝,去掉过于细分的叶节点,使其回退到父节点,甚至更高的节点,然后将父节点或更高的节点改为新的叶节点。

前提知识

X 是一个取有限值的离散随机变量,其概率分布为
P(X=x_i) = p_i, i=1,2,\cdots,n
随机变量(X,Y),其联合概率分布为
P(X=x_i,Y=y_j) = p_{i} \cdot p_{j}, i=1,2,\cdots,n; j=1,2,\cdots,m;

我们使用熵来衡量事件所包含的信息量,事件发生的不确定性越大,包含的信息量就越多,因此熵是事件发生概率的函数h(x) = -log(p(x)。对于两个独立(不相关)事件 x 和 y,有h(x,y) = h(x) + h(y),同时有 p(x,y) = p(x)p(y),这也符合对数函数的性质。
随机变量 X 的熵是随机变量 X 中包含的平均信息量,因此随机变量的熵定义为
H(X) = -\sum_{i=1}^{n} p_i log(p_i)

条件熵H(Y|X)表示在已知X的条件下随机变量Y的不确定性,定义为在X给定的条件下,Y的条件概率分布的熵对X的数学期望
H(Y|X) = \sum_{i=1}^{n} p_iH(Y|X=x_i)
这里,p_i =P(X=x_i)
当熵和条件熵的概率由数据估计得到时,称为经验熵和经验条件熵,如果有0概率,则令 0log0=0

特征选择

特征选择在于选取对训练数据具有分类能力的特征,准则是信息增益或信息增益比。

信息增益表示得知特征X的信息而使得类Y的信息的不确定性减少的程度,将熵H(Y)与条件熵H(Y|X)之差称为互信息,决策树学习中的信息增益等价于训练数据集中类与特征的互信息。

给定训练数据集D和特征A,经验熵H(D)表示对数据集D进行分类的不确定性,经验条件熵H(D|A)表示在特征A给定的条件下对数据集D进行分类的不确定性,它们的差,即信息增益,表示由于特征A而使得对数据集D的分类的不确定性减少的程度。在进行特征选择的时候。我们选择信息增益最大的特征,因为信息增益大的特征具有更强的分类能力。
信息增益定义为
g(D,A) = H(D) - H(D|A)

设训练数据集为D|D|表示其样本容量。设有K个类C_kk=1,2,\cdots,K, |C_k|为属于类C_k的样本个数,\sum_{k=1}^{K}|C_k| = D。设特征An个不同的取值\{a_1,a_2,\cdots,a_n\},根据特征A的取值将D划分为n个子集D_1,D_2,\cdots,D_n|D_i|D_i的样本个数,\sum_{i=1}^{n}|D_i| = |D|。记子集|D_i|中属于类|C_k|的样本的集合为|D_{ik}|,即D_{ik} = D_i \bigcap C_k|D_{ik}|D_{ik}的样本个数。
计算信息增益的算法如下:

输入:训练数据D和特征A
输出:特征A对训练数据集D的信息增益g(D,A)

  1. 计算数据集的经验熵H(D)
    H(D) = -\sum_{k=1}^{K} \frac{|C_k|}{|D|}log_2\frac{|C_k|}{|D|}
  2. 计算特征A对数据集D的经验条件熵H(D|A)
    H(D|A) = \sum_{i=1}^{n} \frac{|D_i|}{|D|} H(D_i) = -\sum_{i=1}^{n} \frac{|D_i|}{|D|} \sum_{k=1}^{K} \frac{|D_{ik}|}{|D_i|}log_2\frac{|D_{ik}|}{|D_i|}
  3. 计算信息增益
    g(D,A) = H(D) - H(D|A)

使用信息增益作为划分训练数据集的特征,存在偏向于选择取值较多的特征的问题,使用信息增益比可以进行校正。
信息增益比g_R(D,A)定义为其信息增益g(D,A)与训练数据集D关于特征A的值的熵H_A(D)之比,即
g_R(D,A) = \frac{g(D,A)}{H_A(D)}
其中, H_A(D) = -\sum_{i=1}^{n} \frac{|D_i|}{|D|}log_2\frac{|D_i|}{|D|}

决策树生成

ID3算法

应用信息增益准则选择特征,递归地构建决策树。从根节点开始,对结点计算所有可能的特征的信息增益,选择信息增益最大的特征作为结点的特征,由该特征的不同取值建立子结点;再对子结点递归地调用以上方法,构建决策树;直到所有特征的信息增益均很小或没有特征可以选择为止。

输入:训练数据集D,特征集A,阈值\epsilon
输出:决策树T

  1. D中所有实例属于同一类C_k,则T为单结点树,并将类C_k作为该结点的类标记,返回T
  2. A = \emptyset,则T为单结点树,并将D中实例数最大的类C_k作为该结点的类标记,返回T
  3. 否则,计算A中各特征对D的信息增益,选择信息增益最大的特征A_g
  4. 如果A_g的信息增益小于阈值\epsilon,则置T为单结点树,并将D中实例数最大的类C_k作为该结点的类标记,返回T
  5. 否则,对A_g的每一可能值a_i,依A_g=a_iD分割为若干非空子集D_i,将D_i中实例数最大的类作为标记,构建子结点,由结点及其子结点构成树T,返回T;
  6. 对第i个子结点,以D_i为训练集,以A-\{A_g\}为特征集,递归地调用1-5步,得到子树T_i,返回T_i
C4.5算法

使用信息增益比来选择特征,其他与ID3类似。

实现

例:有一个由15个样本组成的贷款申请训练数据,数据包括4个特征:年龄,是否有工作,是否有房子,信贷情况,最后一列是类别,是否同意贷款。
数据train_data.csv如下:

ID,age,hasWork,hasOwnHouse,credit,clazz
1,青年,否,否,一般,否
2,青年,否,否,好,否
3,青年,是,否,好,是
4,青年,是,是,一般,是
5,青年,否,否,一般,否
6,中年,否,否,一般,否
7,中年,否,否,好,否
8,中年,是,是,好,是
9,中年,否,是,非常好,是
10,中年,否,是,非常好,是
11,老年,否,是,非常好,是
12,老年,否,是,好,是
13,老年,是,否,好,是
14,老年,是,否,非常好,是
15,老年,否,否,一般,否

决策树实现代码decisionTree.py如下:

# -*- coding: utf-8 -*-
import pandas as pd
import numpy as np
import queue

class Node:
    def __init__(self, label):
        self.label = label
        self.attribute = None
        self.children = None
        self.x = None
        self.y = None

    def setAttribute(self, attribute):
        self.attribute = attribute

    def setChildren(self, children):
        self.children = children

def entropy(datas, clazz):
    C = datas[clazz].value_counts().values
    D = float(datas.index.size)
    H = -np.sum(C/D * np.log2(C/D))
    return H

def conditional_entropy(datas, clazz, attr):
    sub_d = datas[attr].value_counts()
    label = sub_d.index
    Di = sub_d.values
    D = float(datas.index.size)
    H = []
    for i in range(label.size):
        H.append(entropy(datas[datas[attr]==label[i]], clazz))
    CH = np.sum(Di * H / D)
    return CH

def info_gain(datas, clazz, attr):
    H = entropy(datas, clazz)
    CH = conditional_entropy(datas, clazz, attr)
    G = H - CH
    return G

def info_gain_ratio(datas, clazz, attr):
    G = info_gain(datas, clazz, attr)
    Di = datas[attr].value_counts().values
    D = float(datas.index.size)
    HA = -np.sum(Di / D * np.log2(Di / D))
    GR = G / HA
    return GR


def decisionTree(datas, attrs, clazz, threshold, method = 'ID3'):
    clazz_value_cnts = datas[clazz].value_counts()
    label = clazz_value_cnts.index[0]
    node = Node(label)
    if clazz_value_cnts.size == 1 or attrs.size == 0:
        return node

    IG = np.zeros(attrs.size)
    for i in range(attrs.size):
        if method == 'ID3':
            IG[i] = info_gain(datas, clazz, attrs[i])
        if method == 'C4.5':
            IG[i] = info_gain_ratio(datas, clazz, attrs[i])
    index = IG.argmax()
    attr = attrs[index]
    node.setAttribute(attr)

    if IG.max() < threshold:
        return node

    attrs_value_cnts = datas[attr].value_counts()
    attrs = attrs.drop(attr)
    children = dict()

    for i in range(attrs_value_cnts.size):
        children[attrs_value_cnts.index[i]] = decisionTree(datas[datas[attr]==attrs_value_cnts.index[i]], attrs, clazz, threshold, method)

    node.setChildren(children)
    return node

"""
return tree depth
"""
def getTreeDepth(tree):
    if tree.children == None:
        return 1

    ds = [None] * len(tree.children)
    i = 0
    for node in tree.children.values():
        ds[i] = getTreeDepth(node)
        i = i + 1

    return max(ds) + 1


"""
return the tree's leaves's number
"""

def getLeavesNum(tree):
    if tree.children == None:
        return 1

    num = 0
    for node in tree.children.values():
        num += getLeavesNum(node)

    return num

df = pd.read_csv('train_data.csv', encoding='utf-8')
tree = decisionTree(df, df.columns.drop('clazz').drop('ID'), 'clazz', 0, 'ID3')
printTree(tree)

进行可视化代码plotTree.py如下:

import matplotlib.pyplot as plt
import pandas as pd
import decisionTree as dt


def xy(tree, y):
    global n
    tree.y = y

    if tree.x != None:
        return tree.x

    if tree.children == None:
        x = n * x0
        n += 1
    else:
        xSum = 0
        k = len(tree.children)
        for node in tree.children.values():
            xSum += xy(node, y - y0)
        x = float(xSum) / k

    tree.x = x
    return x


xx = 0.05
yy = 0.08


def pp(tree, ax):
    if tree == None:
        return

    if tree.children != None:
        for key in tree.children.keys():
            ax.annotate(tree.attribute,
                        xy=(tree.children[key].x + xx, tree.children[key].y + yy),
                        xytext=(tree.x, tree.y),
                        xycoords='figure fraction',
                        textcoords='figure fraction',
                        arrowprops=dict(arrowstyle='->')
                        )
            ax.text((tree.children[key].x + tree.x) / 2, (tree.children[key].y + tree.y) / 2, key,
                    transform=ax.transAxes)
            pp(tree.children[key], ax)
    else:
        ax.text(tree.x, tree.y, tree.label, transform=ax.transAxes)


df = pd.read_csv('train_data.csv', encoding='utf-8')
tree = dt.decisionTree(df, df.columns.drop('clazz').drop('ID'), 'clazz', 0, 'ID3')

fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)

h = dt.getTreeDepth(tree)
w = dt.getLeavesNum(tree)

y0 = float(1) / (h + 1)
x0 = float(1) / (w + 1)

n = 1

xy(tree, 1 - y0)
pp(tree, ax)

plt.show()

结果展示如下:


plotTree.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容

  • 看过一个很经典的比喻,喜欢跟爱是不一样的,喜欢是荡秋千可以自得其乐,不需别人的回应;爱是跷跷板需要一个人坐在对面与...
    我是憨憨你是谁阅读 78评论 0 0
  • 妈妈,我感谢您对我的关爱和照顾。我更感谢您对我的付出。 但是,妈妈我有一件重要的事要对您说:期末考试...
    子铭妈妈阅读 391评论 1 1