决策树


决策树思想来源于程序设计中的分支结构if-else,最早的决策树就是利用这类结构分割数据的一种分类学习方法。其本质上是一颗由多个判断节点组成的树,在已知各种情况发生的概率基础上,通过生成决策树来求取净现值期望大于或等于0的概率,属于监督学习。

大致结构如下:

class Decision Tree:
	
	data; 分类数据
	rule; # 分割条件
	children; # 记录子节点栈

1. 决策树规则该如何选取?

1.1 熵

决策规则应该是能让信息分歧最大化的(只有这样的分割从贪心程度上来说才是比较正确的)。这里我们引入**熵(Entropy)**的概念。

python treeview 示例源码 python decision tree_数据

在物理上,熵是混乱程度的度量,熵值越高,系统越分散,越不稳定。而在信息论中,给出了如下描述:

  1. 从信息的完整性上进行的描述
    当系统有序状态一致时,数据越集中的地方熵值越小。
  2. 从信息的有序性上进行的描述
    当数据量一致时,系统越有序熵值越低。

1948年,香农对此进行了总结归纳,提出了信息熵的概念[即未知事件可能含有的信息量]。

假设事件A的分类划分是python treeview 示例源码 python decision tree_数据_02,每部分发生的概率是python treeview 示例源码 python decision tree_python_03,那么信息熵则定义为:
python treeview 示例源码 python decision tree_python_04
其本质是一个随机变量信息量的数学期望。

至于为什么是python treeview 示例源码 python decision tree_python_05?我们知道,一件事情内含的信息越清楚,那么他的随机可能就越少。举个例子,小明在云南上学。这句话给的信息量比较大,小明是学生还是老师?在云南哪个地方上学?上哪个阶段的学?这些我们都不知道,所以小明可能在云南大学读研究生,有可能在云师大附中教书。再看这句话:小明在云南大学地球科学学院地理信息科学系读大四,哦,原来小明是云大大四学生啊。瞬间感觉没有探索欲望了。那么反映到数据上,如何清楚一个事件呢?那就是组成该事件各个基本事件发生的概率。这个概率越高,这个事件就越无趣。那么怎么去量化?不难想到,可以用这个概率做文章。比如python treeview 示例源码 python decision tree_信息熵_06。但这样会有些问题呀,概率很小的时候倒数会特别大。我们想让曲线平滑一点可以吗,当然可以。香农发现了,python treeview 示例源码 python decision tree_python_07函数很好地满足了这一点。它是一个恒正、递减的函数,甚至还巧合地满足python treeview 示例源码 python decision tree_python_08!这能很好地分割独立事件~

于是,一个随机变量的信息熵就可以定义为:
python treeview 示例源码 python decision tree_python_09
这个对数的底选取是随意的,那为什么要选2?其实以树形结构就能很好地解释。为了方便表达,我们举一个例子,32只球队,有一个是冠军,至少要问几次才能问出这个冠军?

我们就不抠字眼了,假设一次只能问出一个条件,时间复杂度python treeview 示例源码 python decision tree_决策树_10的解法是:

for i in range(1,32):
	if check(i):
		return i
return 32

而利用二分法,可以将时间复杂度压缩到python treeview 示例源码 python decision tree_python_11

# 实际上是问是否在1~16之间,这样就分割成两个了
# 我们假设check为: [0,0,0,0,1,1,1,1,1]
# 所需找的值就是第一个1出现的位置
# 有点类似第一个错误版本
def check(i):
	if i<tar: return 0
	return 1
	
l,r=0,32
while l<r:
	mid=l+((r-l)>>1)
	if check(mid):
		r=mid
    else:
        l=mid+1
return l

于是,再对一个随机事件的各个随机变量平均后(依靠概率权重),最终的结果就是我们看到的那样啦!

python treeview 示例源码 python decision tree_python_04

import numpy as np
import pandas as pd
import collections
from math import log

class Entroy(object):

    def __init__(self,dataset:pd.DataFrame)->None:
        self.dataset=np.array(dataset)

    def ent(self,data:np.array=None)->float:
        # 计算熵
        # 用频率估计概率
        if not data:
            data=self.dataset
        size=len(data)
        label=collections.defaultdict(int)# 用于统计频率的字典
        for i in range(size):
            # 默认最后一列属性是label
            lab=data[i][-1]
            label[lab]+=1
        # 接着计算熵
        return -sum([(_/size)*log((_/size),2) for _ in label.values()])

1.2 信息增益

首先来看概念:信息增益就是以某特征划分数据集前后的熵差值。可以看做得知特征x的信息而使得类Y信息熵减少的程度。

换句话说,我现在知道了小明去上学的信息熵是8,当我明确知道小明要去坐车,此时小明去上学的信息熵是5。而当我知道小明背着书包时,此时小明去上学的信息熵为7。不难发现,小明背书包能够让数据集更加稳定(8-7=1<3),换言之,也就是以该特征划分数据集,能够让数据的纯度更高。所以我们会选择信息增益更高的特征(以该特征划分的数据更加稳定)。
python treeview 示例源码 python decision tree_决策树_13
这个值越大呢,就表示更改后的系统信息量越多。

我们给出严格定义:

特征python treeview 示例源码 python decision tree_决策树_14对于训练数据集python treeview 示例源码 python decision tree_数据_15的信息增益python treeview 示例源码 python decision tree_数据_16,定义为集合python treeview 示例源码 python decision tree_数据_15的信息熵python treeview 示例源码 python decision tree_决策树_18与特征python treeview 示例源码 python decision tree_决策树_14给定条件下python treeview 示例源码 python decision tree_数据_15的信息条件熵python treeview 示例源码 python decision tree_信息熵_21之差。
python treeview 示例源码 python decision tree_数据_22
其中,python treeview 示例源码 python decision tree_决策树_18的计算公式为:
python treeview 示例源码 python decision tree_决策树_24
python treeview 示例源码 python decision tree_数据_25表示某个类别的样本数,这是以频率作为概率的似然估计。

而条件熵的计算公式为:
python treeview 示例源码 python decision tree_数据_26
这也是ID3决策树的分式算法。但是它只能对离散属性数据集构造决策树,且偏向取值较多的属性。以下是算法实现:

def condition_ent(self,data=None,axis=0)->int:
        # 条件熵,现在是以特征axis进行划分
        # 每种axis都有对应的数据
        # 我们统计这些数据对应的label即可
        # 实际上,就是先依据特征划分,再对每个特征计算label的信息熵
        # 如此便是条件概型
        # 也能看出特征y对于目标x的影响程度
        # axis是第几个维度
        if not data:
            data=self.dataset
        size=len(data)
        Feature=collections.defaultdict(list)
        for i in range(size):
            feature=data[i][axis]
            Feature[feature].append(data[i])
        # 这时候不是负号了
        # 负号在单个特征内已经计算过了
        return sum([(len(_)/size)*self.ent(_) for _ in Feature.values()])
    
	@staticmethod
    def KLIC(a,b):
        # 信息增益
        return a-b

不难发现,这玩意的空间复杂度是python treeview 示例源码 python decision tree_数据_27级别,也就是对于连续数据难以处理,不仅是数学层次。

此时,我们已经实现了决策树的rule模块,其完整内容如下:

"""
@author:かぐらなな
The Time of Creating:23 23:10
File Name: untitled2 - Ent
"""
import numpy as np
import pandas as pd
import heapq
import collections
from math import log


class Entroy(object):

    def __init__(self,dataset:pd.DataFrame,featurename=None):
        self.dataset=np.array(dataset)
        self.featurename=featurename
        # small heap
        self.heap=[]
        self.entroy=self.ent()

    def ent(self,data:np.array=None)->float:
        # calculate the entroy
        # estimate the possibility by using frequency
        if not data:
            data=self.dataset
        size=len(data)
        label=collections.defaultdict(int)# a dictionary of statistical frequencies
        for i in range(size):
            # default the last col is label
            lab=data[i][-1]
            label[lab]+=1
        # now we can work out the ent
        return -sum([(_/size)*log((_/size),2) for _ in label.values()])

    def condition_ent(self,data=None,axis=0)->int:
        # condition entroy,which is divided by "axis-feature"
        # each axis has corresponding data

        # the following steps:
        # 1. classfication the data by feature
        # 2. statistics the frequency of label and work out the c-e of each feature
        # 3. sum and give weight
        if not data:
            data=self.dataset
        size=len(data)
        Feature=collections.defaultdict(list)
        for i in range(size):
            feature=data[i][axis]
            Feature[feature].append(data[i])
        return sum([(len(_)/size)*self.ent(_) for _ in Feature.values() ])
    
	@staticmethod
    def KLIC(a,b):
        # information divergence
        return a-b

    def Create_node(self)->None:
        # create a small heap to record c-e
        if self.dataset.size==0:
            raise Exception("None Date!")
        # features
        feature_count=len(self.dataset[0])-1
        # recreate
        self.heap=[]
        for i in range(feature_count):
            # calculate the c-e
            tem=self.condition_ent(axis=i)
            # handle c-e by small heap
            # we just need smallest one
            heapq.heappush(self.heap,(tem,i))


    def getNode(self):
        if self.heap:
            # i: c-e
            # j: index
            i,j=heapq.heappop(self.heap)
            if self.featurename:
                j=self.featurename[j]
            return (j,self.entroy-i)
        raise Exception("You haven't created a tree yet!")

用一组实例去测试:

labels = ["地区", "楼层", "装修","离学校距离","售价"]
datasets = pd.DataFrame([
    ["呈贡", "低", "毛坯", '10KM','30'],
    ["呈贡", "中", "精装", '2KM','50'],
    ["呈贡", "高", "精装", '<1KM','80'],
    ["盘龙", "高", "毛坯", '2KM','50'],
    ["西山", "低", "精装", '10KM','50'],
    ["盘龙", "中", "精装", '10KM','80'],
    ["五华", "高", "毛坯", '<1KM','80']
],columns=labels)
A=Entroy(datasets)
A.Create_node()
# simulate the steps of extracting nodes each time
while A.heap:
    print("特征 %s 的信息增益最大,为:%s,故选为当前结点"%(A.getNode()))

结果如下:

python treeview 示例源码 python decision tree_信息熵_28

通过比对我们可以发现,基本上与预期的一致。(数据是特意针对填写的)

[小根堆和收集器能够让均摊时间复杂度的步骤执行时间更短,而选用Python语言主要是为了接下来的数据分析和特征工程]

1.3 信息增益率