《Machine Learning in Action》—— Taoye给你讲讲决策树到底是支什么“鬼”
《Machine Learning in Action》—— Taoye给你讲讲决策树到底是支什么“鬼”
前面我们已经详细讲解了线性SVM以及SMO的初步优化过程,具体可看:
关于SVM非线性相关的内容,我们留着下个星期来撕
这篇文章我们先来看看决策树的内容,决策树相对于SVM来讲要简单不少,也没有多么复杂的公式。我理解的决策树,简单来说就是通过已有的数据集训练出一个树形结构的模型,以便我们能够依据该模型对不知分类结果的数据进行预测。简单的决策树就有点类似于我们在学习数据结构时候的二叉排序树。当然了,决策树除了能够进行分类之外,还能进行回归,这里主要讨论的是分类问题。
关于决策树相关的内容,可能会肝两篇文章来介绍。第一篇主要是讲解决策树先关的基本知识,及其决策训练的过程,也就是我们的这篇文章,重在理论及推导,也重在理解。而第二篇文章写的主要内容是决策树的代码实战,而代码实战是在熟悉理论基础的前提之下来进行的,所以对于决策树相关的理论知识还是有必要了解的。可能的话,还会有第三篇,主要是关羽防止决策树过拟合的剪枝操作的内容。
一、什么是决策树
关于决策树的定义,李航——《统计学习方法》(第二版)这本书中是这样描述的:
分类决策树模型是一种描述对实例进行分类的树形结构,决策树由节点(node)和有向边(directed edge)组成。节点有两种类型:内部节点(iternal node)和叶节点(leaf node)。内部节点表示一个特征或属性,叶节点表示一个类。
上述提到的内部节点可以理解成样本的属性特征,而叶节点可以理解成样本的标签,而有向边可以理解成不同的属性特征结果。一般我们在绘制决策树的时候,内部节点用方框或长方形表示,而叶节点用圆形或椭圆表示。(注意:这个并不是绝对的,在不同资料中可能有不同的表示方法,比如《统计学习方法》一书中用圆表示内部节点,用方形表示叶子节点,而在《机器学习实战》一书中表示方式正好相反)
枯燥的文字说明总是会降低读者的阅读欲望,下面我们不妨通过一个实际的例子来说明下,从而加强读者对决策树的理解。
比如说,形容女生美与丑的时候一般会存在多个指标,这个例子感觉有点危险,还是换个吧。
例子来源:李航——《统计学习方法》(第二版)
比如说,我们没Money用了,想要去银行贷款,这个时候银行就会根据你自己的个人附带特征来决定是否给你放款。假设这里的属性特征有四个,分别是年纪、是否工作、是否有房子、信用情况,这些属性特征就相当于是内部节点,标签(结果)是否放款相当于叶子节点,而不同属性特征的结果表示有向边,像年纪中有青年、中年、老年,信用情况有好、一般、非常好等表示不同的有向边。
对此,我们可以根据自己的感觉来手动绘制一个简单的决策树模型:
我们知道,上述决策树仅仅是我们自己手动来实现的,不一定能够运用于解决实际问题。而决策树的任务,则是根据已有的数据样本集训练得到这么一颗树形结构模型,以此来对未知分类的数据进行类别预测。对此,我们要向得到这么一颗尽可能理想的决策树,则不得不考虑以下几个问题:
- 决策树的指标(属性特征)选择的顺序应该是怎样的的?哪个特征应当被优先考虑呢?
- 如果构建出的决策树出现了过拟合问题,也就说我们的训练的时候还不错,但是一测试就GG,这个时候我们应当怎么解决呢?
本篇文章,主要内容是属性特征的优先选取问题。
二、属性特征的选择
对于属性特征的选择,我们应当优先选取对训练数据集具有明显分类能力的特征,这样可以提高我们决策树的学习效率。假如说一个属性特征的分类结果与随机分类基本没什么差别,则我们可以说这个属性特征基本是没有分类能力的。
比如说,我们这里有12个关于女生美与丑的数据样本,其中六个是丑女,另外六个是美女,六个丑女身高有三个是165cm、三个是185cm,而另外六个美女身高同样如此,这样的话,我们仅仅通过身高来对女生美貌的判断基本是没有分类能力的。(仅仅是个例子,不代表任何观点)
所以,我们理想情况下决策树的生成理应是这样的:随着决策过程的不断进行,我们希望决策树的分支节点所包含的样本尽可能的属于同一类别(标签相同),也就是节点的“纯度”越来越高。
所以,决策树的关键技术在于属性特征的选择。对于属性特征的选择,我们通常有三个准则:信息增益、信息增益比(增益率)和基尼指数。
2.1 信息增益
上面提到了样本浓度,比如说我这里有10个女生,10个都是美女,那就说此时的样本浓度高,假如美女和丑女五五分,那这个时候的浓度就比较低了。这里的“浓度”表达的就是这个意思,它主要针对的是标签(结果)。
而表示样本“浓度”的指标,最常用的就是“信息熵”了。假定当前样本集合\(D\)中第\(k\)类(总共\(n\)类)样本所占的比例为\(p_k\),则此时样本\(D\)的信息熵为:
通过上述公式,我们可以知道10个美女(一个类别)的信息熵为
美女丑女五五分(两类)的信息熵为:
通过上面信息熵的简单计算,我们可以知道,信息熵的值越小,则纯度越高。
既然我们已经了解了信息增益的计算公式及其所表达的意义,那么对于一个数据样本集,我们应当如何用代码来进行计算呢?下面我们来试试吧。
本次使用到的数据样本来自 李航——《统计学习方法》(第二版),数据是关于贷款申请的,具体如下:
那么现在我们来通过代码形式计算下这个数据样本集的信息熵吧:
首先创建一个establish_data
方法来建立一个二维数组用于存储上述数据样本集的相关信息(关于属性特征所对于的值0,1,2之类的,大家可以参考上述表格对号入座,这里就不做过多解释了):
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain:创建训数据集
"""
def establish_data():
data = [[0, 0, 0, 0, 'N'], # 样本数据集相关信息,前面几项代表属性特征,最后一项表示是否放款
[0, 0, 0, 1, 'N'],
[0, 1, 0, 1, 'Y'],
[0, 1, 1, 0, 'Y'],
[0, 0, 0, 0, 'N'],
[1, 0, 0, 0, 'N'],
[1, 0, 0, 1, 'N'],
[1, 1, 1, 1, 'Y'],
[1, 0, 1, 2, 'Y'],
[1, 0, 1, 2, 'Y'],
[2, 0, 1, 2, 'Y'],
[2, 0, 1, 1, 'Y'],
[2, 1, 0, 1, 'Y'],
[2, 1, 0, 2, 'Y'],
[2, 0, 0, 0, 'N']]
return np.array(data)
随后,我们创建一个calc_information_entropy
方法用于计算信息熵,计算过程的代码可结合上述的公式来看。另外,需要说一点的是,为了方便对放款结果进行分类,我们使用pandas
包进行处理,关于pandas
的使用,大伙可以参考文档,Taoye后期也会整理一份。当然了,不用pandas
模块也能实现这个功能,有兴趣的读者可自行尝试,也不难。calc_information_entropy
具体代码如下:
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain:计算信息熵
"""
def calc_information_entropy(data):
data_number, _ = data.shape
information_entropy = 0
for item in pd.DataFrame(data).groupby(_ - 1):
print(item[1])
proportion = item[1].shape[0] / data_number
information_entropy += - proportion * np.log2(proportion)
return information_entropy
我们运行上述代码,来看看具体的信息熵结果:
相比大家都了解了信息熵的概念,并能够手动计算样本集的信息熵了,现在我们再来把信息增益搬出来吧。
假设一个属性特征\(a\)有\(V\)个可能的取值\(\{a^1, a^2,.., a^V\}\),若使用\(a\)来对样本集\(D\)进行划分,这样的话就会产生\(V\)个分支节点,其中第\(v\)个 分支节点包含了\(D\)中所有在属性\(a\)上取值为\(a^V\)的样本,记为\(D^V\)。于是可计算出用属性特征\(a\)对样本集\(D\)进行划分所获得的“信息增益”(information gain)为:
以上是周志华——《机器学习》一书中对信息增益的相关说明。
枯燥的文字说明总是会降低读者的阅读欲望,上述符号也是有点多,我们不妨拿上面的贷款数据来进一步理解下上述信息增益的内容:
我们单独拿年龄这一个属性特征来计算其信息增益吧,其有青年、中年、老年三个可能的值,也就是说上述的\(a=年龄\),\(\{a^1,a^2,...,a^V\}=\{青年,中年,老年\}\),而数据样本集\(D\)为上述15个数据样本,由于年龄有三个可能的值,所以此时会产生三个分支,即\(V=3\)。之前我们已经计算得到\(Ent(D)=0.971\),现在我们只要计算出后一项\(\sum_{v=1}^V\frac{D^v}{D}Ent(D^v)\)的值即可得到该属性特征所对应的信息增益:
通过数据,我们观察得到如下信息:
- 对于年龄这个属性特征来讲,青年、中年、老年分别有5个。
- 5个青年中有2个允许贷款,有三个不允许贷款;中年中有3个允许贷款,2个不允许贷款;老年中有4个允许贷款,有1个不允许贷款。
对此,我们可以计算:
对此,我们的计算处年龄这个属性特征所对应的信息增益值为:
我们可以把后一项的内容理解成条件概率。另外,信息增益越大,则其所对应的属性特征的分类能力也就越强,也就是我们应当优先选取的特征。
同理,我们可以计算出工作、房子、信用属性特征所对应的信息增益:
我们比较哥哥属性特征的信息增益值,可以发现房子这个属性特征最大,所以它应该是我们优先选择的属性特征。
了解了信息增益的计算及其意义,下面我们来通过代码计算一下(代码含义见注释):
import numpy as np
import pandas as pd
np.__version__
pd.__version__
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain:创建训数据集
"""
def establish_data():
data = [[0, 0, 0, 0, 'N'], # 样本数据集相关信息,前面几项代表属性特征,最后一项表示是否放款
[0, 0, 0, 1, 'N'],
[0, 1, 0, 1, 'Y'],
[0, 1, 1, 0, 'Y'],
[0, 0, 0, 0, 'N'],
[1, 0, 0, 0, 'N'],
[1, 0, 0, 1, 'N'],
[1, 1, 1, 1, 'Y'],
[1, 0, 1, 2, 'Y'],
[1, 0, 1, 2, 'Y'],
[2, 0, 1, 2, 'Y'],
[2, 0, 1, 1, 'Y'],
[2, 1, 0, 1, 'Y'],
[2, 1, 0, 2, 'Y'],
[2, 0, 0, 0, 'N']]
return np.array(data)
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain:计算信息熵
"""
def calc_information_entropy(data):
data_number, _ = data.shape
information_entropy = 0
for item in pd.DataFrame(data).groupby(_ - 1):
proportion = item[1].shape[0] / data_number
information_entropy += - proportion * np.log2(proportion)
return information_entropy
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain:找出对应属性特征值的样本,比如找出所有年纪为青年的样本数据集
"""
def handle_data(data, axis, value):
result_data = list()
for item in data:
if item[axis] == value: result_data.append(item)
return result_data
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain:计算最大的信息增益,并输出其所对应的特征索引
"""
def calc_information_gain(data):
feature_number = data.shape[1] - 1 # 属性特征的数量
base_entropy = calc_information_entropy(data) # 计算总体数据集的信息熵
max_information_gain, best_feature = 0.0, -1 # 初始化最大信息增益和对应的特征索引
for index in range(feature_number):
feat_list = [item[index] for item in data]
feat_set = set(feat_list)
new_entropy = 0.0
for set_item in feat_set: # 计算属性特征划分后的信息增益
sub_data = handle_data(data, index, set_item)
proportion = len(sub_data) / float(data.shape[0]) # 计算子集的比例
new_entropy += proportion * calc_information_entropy(np.array(sub_data))
temp_information_gain = base_entropy - new_entropy # 计算信息增益
print("第%d个属性特征所对应的的增益为%.3f" % (index + 1, temp_information_gain)) # 输出每个特征的信息增益
if (temp_information_gain > max_information_gain):
max_information_gain, best_feature = temp_information_gain, index # 更新信息增益,确定的最大的信息增益对应的索引
return best_feature
if __name__ == "__main__":
data = establish_data()
print("属性特征对应的最大的信息增益对应的索引为:%d" % calc_information_gain(data))
运行上述代码,可见输出的各个属性特征的信息增益,以及最大信息增益对应的特征索引:
可以发现,和我们手动计算的如出一辙,所以此时我们应当优先选取索引为2的属性特征作为决策标准,也就是房子。
2.2 信息增益比(增益率)
我们使用信息增益作为选取特征指标的时候,存在偏向于选择取值较多的特征的问题。
比如我们将id作为上述数据集的一个分类属性,通过计算可以发现该信息增益最大,但实际上该id对类别不具有什么分类能力,所以这样得到的决策树不具有泛化能力,无法对新样本进行有效预测。
为了解决信息增益存在的这个问题,我们就引入了信息增益比(增益率)的概念,著名的C4.5算法就是用“增益率”来作为选取最优划分属性。增益率定义为:
\(IV(a)\)称为\(a\)的“固有值”(intrinsic value)。通常\(a\)的取值数目越多,则\(IV(a)\)的值会越大。其中的\(a\)的取值比如说:上述的年纪有青年、中年、老年三种取值。
对于上述的贷款数据集来说,信用情况有一般、好和非常好三种,比例分为是\(\frac{5}{15}、\frac{6}{15}、\frac{4}{15}\)。毒刺,我们可以计算信用情况的“固有值”:
所以,对于信用属性来讲,其增益率为:
同理,我们可以计算出其他属性特征的增益率:
计算增益率的具体代码可参考如下:
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain:计算增益率
"""
def calc_gain_ratio(data):
feature_number = data.shape[1] - 1 # 属性特征的数量
base_entropy = calc_information_entropy(data) # 计算总体数据集的信息熵
for index in range(feature_number):
feat_list = [item[index] for item in data]
feat_set = set(feat_list)
new_entropy, iv = 0.0, 0.0
for set_item in feat_set: # 计算属性特征划分后的信息增益
sub_data = handle_data(data, index, set_item)
proportion = len(sub_data) / float(data.shape[0]) # 计算子集的比例
new_entropy += proportion * calc_information_entropy(np.array(sub_data))
iv += -proportion * np.log2(proportion)
temp_information_gain = base_entropy - new_entropy # 计算信息增益
gain_ratio = temp_information_gain / iv
print("第%d个属性特征所对应的增益率为%.3f,信息增益为%.3f" % (index + 1, gain_ratio, temp_information_gain)) # 输出每个特征的信息增益
运行结果如下:
在C4.5算法中,并不是直接单一的使用增益率来对属性特征的选取进行决策,而是先在信息增益中选取高于平均水平的属性作为候选,然后从候选中选取增益率最高的。
关于C4.5算法,我们后期会讲到。
2.3 基尼指数
基尼指数是另外一种选取属性的指标。
前面我们提到了,信息熵是描述数据样本纯度一个标准,而在基尼指数中的基尼值同样可以体现数据样本的纯度。数据样本集\(D\)的基尼值可以用\(Gini(D)\)来表示(其中\(k\)表示有\(k\)个标签结果):
基尼值\(Gini(D)\)越小,说明数据样本纯度越高。而属性\(a\)对应的基尼指数\(Gini\_index(D,a)\)可以定义为:
我们同样来分别计算下上述贷款数据集的基尼指数。
对于信用情况这一属性特征来讲,其基尼指数的手动计算过程如下所示:
对于其他属性的基尼指数,读者可自行根据上述过程进行计算(真的很简单)。关于基尼指数的计算代码,可参考如下:
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain:计算基尼指数
"""
def calc_gini_index(data):
feature_number = data.shape[1] - 1 # 属性特征的数量
base_entropy = calc_information_entropy(data) # 计算总体数据集的信息熵
for index in range(feature_number):
feat_list = [item[index] for item in data]
feat_set = set(feat_list)
new_entropy, iv, gini_index = 0.0, 0.0, 0.0
for set_item in feat_set: # 计算属性特征划分后的信息增益
sub_data = handle_data(data, index, set_item)
temp_df = pd.DataFrame(sub_data)
yes_proportion = temp_df[temp_df[temp_df.shape[1]-1] == "Y"].shape[0] / len(sub_data)
gini_value = 1 - (yes_proportion ** 2) - ((1- yes_proportion) ** 2)
proportion = len(sub_data) / float(data.shape[0]) # 计算子集的比例
new_entropy += proportion * calc_information_entropy(np.array(sub_data))
iv += -proportion * np.log2(proportion)
gini_index += gini_value * proportion # 计算基尼指数
temp_information_gain = base_entropy - new_entropy # 计算信息增益
gain_ratio = temp_information_gain / iv
print("第%d个属性特征所对应的信息增益为%.3f,增益率为%.3f, 基尼指数为%.3f" % (index + 1, temp_information_gain, gain_ratio, gini_index))
运行结果:
以上就是基尼指数的计算及其相关代码,一般来讲,基尼指数越小就优先被划分。
上述内容的完整代码如下:
import numpy as np
import pandas as pd
np.__version__
pd.__version__
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain:创建训数据集
"""
def establish_data():
data = [[0, 0, 0, 0, 'N'], # 样本数据集相关信息,前面几项代表属性特征,最后一项表示是否放款
[0, 0, 0, 1, 'N'],
[0, 1, 0, 1, 'Y'],
[0, 1, 1, 0, 'Y'],
[0, 0, 0, 0, 'N'],
[1, 0, 0, 0, 'N'],
[1, 0, 0, 1, 'N'],
[1, 1, 1, 1, 'Y'],
[1, 0, 1, 2, 'Y'],
[1, 0, 1, 2, 'Y'],
[2, 0, 1, 2, 'Y'],
[2, 0, 1, 1, 'Y'],
[2, 1, 0, 1, 'Y'],
[2, 1, 0, 2, 'Y'],
[2, 0, 0, 0, 'N']]
return np.array(data)
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain:计算信息熵
"""
def calc_information_entropy(data):
data_number, _ = data.shape
information_entropy = 0
for item in pd.DataFrame(data).groupby(_ - 1):
proportion = item[1].shape[0] / data_number
information_entropy += - proportion * np.log2(proportion)
return information_entropy
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain:找出对应属性特征值的样本,比如找出所有年纪为青年的样本数据集
"""
def handle_data(data, axis, value):
result_data = list()
for item in data:
if item[axis] == value: result_data.append(item)
return result_data
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain:计算最大的信息增益,并输出其所对应的特征索引
"""
def calc_information_gain(data):
feature_number = data.shape[1] - 1 # 属性特征的数量
base_entropy = calc_information_entropy(data) # 计算总体数据集的信息熵
max_information_gain, best_feature = 0.0, -1 # 初始化最大信息增益和对应的特征索引
for index in range(feature_number):
feat_list = [item[index] for item in data]
feat_set = set(feat_list)
new_entropy = 0.0
for set_item in feat_set: # 计算属性特征划分后的信息增益
sub_data = handle_data(data, index, set_item)
proportion = len(sub_data) / float(data.shape[0]) # 计算子集的比例
new_entropy += proportion * calc_information_entropy(np.array(sub_data))
temp_information_gain = base_entropy - new_entropy # 计算信息增益
print("第%d个属性特征所对应的的增益为%.3f" % (index + 1, temp_information_gain)) # 输出每个特征的信息增益
if (temp_information_gain > max_information_gain):
max_information_gain, best_feature = temp_information_gain, index # 更新信息增益,确定的最大的信息增益对应的索引
return best_feature
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain:计算增益率
"""
def calc_gain_ratio(data):
feature_number = data.shape[1] - 1 # 属性特征的数量
base_entropy = calc_information_entropy(data) # 计算总体数据集的信息熵
for index in range(feature_number):
feat_list = [item[index] for item in data]
feat_set = set(feat_list)
new_entropy, iv = 0.0, 0.0
for set_item in feat_set: # 计算属性特征划分后的信息增益
sub_data = handle_data(data, index, set_item)
proportion = len(sub_data) / float(data.shape[0]) # 计算子集的比例
new_entropy += proportion * calc_information_entropy(np.array(sub_data))
iv += -proportion * np.log2(proportion)
temp_information_gain = base_entropy - new_entropy # 计算信息增益
gain_ratio = temp_information_gain / iv
print("第%d个属性特征所对应的增益率为%.3f,信息增益为%.3f" % (index + 1, gain_ratio, temp_information_gain)) # 输出每个特征的信息增益
"""
Author: Taoye
微信公众号: 玩世不恭的Coder
Explain:计算基尼指数
"""
def calc_gini_index(data):
feature_number = data.shape[1] - 1 # 属性特征的数量
base_entropy = calc_information_entropy(data) # 计算总体数据集的信息熵
for index in range(feature_number):
feat_list = [item[index] for item in data]
feat_set = set(feat_list)
new_entropy, iv, gini_index = 0.0, 0.0, 0.0
for set_item in feat_set: # 计算属性特征划分后的信息增益
sub_data = handle_data(data, index, set_item)
temp_df = pd.DataFrame(sub_data)
yes_proportion = temp_df[temp_df[temp_df.shape[1]-1] == "Y"].shape[0] / len(sub_data)
gini_value = 1 - (yes_proportion ** 2) - ((1- yes_proportion) ** 2)
proportion = len(sub_data) / float(data.shape[0]) # 计算子集的比例
new_entropy += proportion * calc_information_entropy(np.array(sub_data))
iv += -proportion * np.log2(proportion)
gini_index += gini_value * proportion
temp_information_gain = base_entropy - new_entropy # 计算信息增益
gain_ratio = temp_information_gain / iv
print("第%d个属性特征所对应的信息增益为%.3f,增益率为%.3f, 基尼指数为%.3f" % (index + 1, temp_information_gain, gain_ratio, gini_index))
if __name__ == "__main__":
data = establish_data()
calc_gini_index(data)
优先选取属性特征的指标主要有三个,分别是信息增益、增益率、基尼指数。对上述内容做个简单的总结吧:
- 属性特征的信息增益越高,按道理来讲应当被优先选取,常用于\(ID3\)算法
- 属性特征的增益率越高,按道理来讲应当被优先选取,常用与\(C4.5\)算法
- 属性特征的尼基指数低,按道理来讲应当被优先选取,常用于\(CART\)算法
本文主要是决策树的理论部分内容,介绍了什么决策树,以及生成决策树时所需要优先选取的三种决策标准。有学习的过SVM,或阅读过Taoye之前写的几篇SVM内容的文章可以发现,决策树相对于SVM来讲要简单很多,没有太多且复杂的公式推导。关于决策树的其他内容,比如决策树的生成、可视化、剪枝等,我们放在后面几篇文章来写。
我是Taoye,爱专研,爱分享,热衷于各种技术,学习之余喜欢下象棋、听音乐、聊动漫,希望借此一亩三分地记录自己的成长过程以及生活点滴,也希望能结实更多志同道合的圈内朋友,更多内容欢迎来访微信公主号:玩世不恭的Coder。