决策树不确定性的度量方法
用熵和基尼指数去衡量数据集的不确定性
在信息论和概率论统计中,熵是表示随机变量不确定性的度量,基尼指数越大,数据集的不确定性越大。
整个决策树的学习过程就是一个递归地选择最优特征,并根据该特征对数据集进行划分,使得各个样本都得到一个最好的分类的过程。
传统的经典决策树算法包括ID3算法、C4.5算法以及GBDT的基分类器CART算法。
主要的区别在于其特征选择准则的不同。ID3算法选择特征的依据是信息增益、C4.5是信息增益比,而CART则是Gini指数。作为一种基础的分类和回归方法,决策树可以有如下两种理解方式。一种是我们可以将决策树看作是一组if-then规则的集合,另一种则是给定特征条件下类的条件概率分布。
决策树以树状结构构建分类或回归模型。它通过将数据集不断拆分为更小的子集来使决策树不断生长。最终长成具有决策节点(包括根节点和内部节点)和叶节点的树。
1.Iterative Dichotomiser 3(ID3)
信息熵和信息增益用于被用来构建决策树。
直观上说地理解,信息熵表示一个事件的确定性程度。
信息熵度量样本的同一性,如果样本全部属于同一类,则信息熵为0;如果样本等分成不同的类别,则信息熵为1。
若离散随机变量X的概率分布为:
则随机变量X的熵定义为:
同理,对于连续型随机变量Y,其熵可定义为:
当给定随机变量X的条件下随机变量Y的熵可定义为条件熵H(Y|X):
所谓信息增益就是数据在得到特征X的信息时使得类Y的信息不确定性减少的程度。假设数据集D的信息熵为H(D),给定特征A之后的条件熵为H(D|A),则特征A对于数据集的信息增益g(D,A)可表示为:
信息增益越大,则该特征对数据集确定性贡献越大,表示该特征对数据有较强的分类能力。
信息增益的计算示例如下:
1).计算目标特征的信息熵。
2).计算加入某个特征之后的条件熵。
3).计算信息增益。
### 导入需要用到的python库
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.datasets import load_iris
iris = load_iris()
y = iris.target
X = iris.data
print(y.shape, X.shape)
### 将数据集拆分为训练集和测试集
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=0)
### 特征缩放
from sklearn.preprocessing import StandardScaler
sc = StandardScaler()
X_train = sc.fit_transform(X_train)
X_test = sc.transform(X_test)
### 对测试集进行决策树分类拟合
from sklearn.tree import DecisionTreeClassifier
classifier = DecisionTreeClassifier(criterion='entropy', random_state=0)
classifier.fit(X_train, y_train)
### 预测测试集的结果
y_pred = classifier.predict(X_test)
print(y_test, y_pred)
numpy
计算熵值。
# 计算熵值。
def entropy(ele):
'''
input: A list contain categorical value.
output: Entropy value.
entropy = - sum(p * log(p)), p is a prob value.
'''
# 计算列表值的概率分布yes 1/2 no 1/2
probs = [ele.count(i)/len(ele) for i in set(ele)]
# 计算熵值
entropy = -sum([prob*log(prob, 2) for prob in probs])
return entropy
print(set(df['play'].tolist()))
entropy(df['play'].tolist())
{'no', 'yes'}
0.9402859586706309
根据特征和特征值进行数据划分的方法:
# 根据特征和特征值进行数据划分的方法:
def split_dataframe(data, col):
'''
input: dataframe, 根据那一列分.
output: a dict of splited dataframe.
'''
# 这列中唯一的值
unique_values = data[col].unique()
# 装dataframe的字典,键是上面唯一的值
result_dict = {elem : pd.DataFrame for elem in unique_values}
# 根据列值分dataframe
for key in result_dict.keys():
# 各行如果这列等于该键则添加
result_dict[key] = data[:][data[col] == key]
return result_dict
split_example = split_dataframe(df, 'temp')
split_example
{'hot': humility outlook play temp windy
0 high sunny no hot False
1 high sunny no hot True
2 high overcast yes hot False
12 normal overcast yes hot False,
'mild': humility outlook play temp windy
3 high rainy yes mild False
7 high sunny no mild False
9 normal rainy yes mild False
10 normal sunny yes mild True
11 high overcast yes mild True
13 high rainy no mild True,
'cool': humility outlook play temp windy
4 normal rainy yes cool False
5 normal rainy no cool True
6 normal overcast yes cool True
8 normal sunny yes cool False}
根据熵计算公式和数据集划分方法计算信息增益来选择最佳特征
# 根据熵计算公式和数据集划分方法计算信息增益来选择最佳特征
def choose_best_col(df, label):
'''
input: datafram, 目标特征
output: max infomation gain, best column,
splited dataframe dict based on best column.
'''
# 目标特征的信息熵
entropy_D = entropy(df[label].tolist())
# 除了目标特征的列表
cols = [col for col in df.columns if col not in [label]]
# ['humility', 'outlook', 'temp', 'windy']
# 初始化最大信息增益、最佳列和最佳分割dict
max_value, best_col = -999, None
max_splited = None
# 根据不同的列拆分数据
for col in cols:
splited_set = split_dataframe(df, col)
# {'high': humility outlook play temp windy
# 0 high sunny no hot False
# 1 high sunny no hot True
# 2 high overcast yes hot False
# 3 high rainy yes mild False
# 7 high sunny no mild False
# 11 high overcast yes mild True
# 13 high rainy no mild True,
# 'normal': humility outlook play temp windy
# 4 normal rainy yes cool False
# 5 normal rainy no cool True
# 6 normal overcast yes cool True
# 8 normal sunny yes cool False
# 9 normal rainy yes mild False
# 10 normal sunny yes mild True
# 12 normal overcast yes hot False}
entropy_DA = 0
for subset_col, subset in splited_set.items():
# 计算拆分后的dataframe中目的特征的熵
entropy_Di = entropy(subset[label].tolist())
# 计算当前特征的熵。加入某个特征之后的条件熵。
entropy_DA += len(subset)/len(df) * entropy_Di
# 计算当前特征的信息增益
info_gain = entropy_D - entropy_DA
if info_gain > max_value:
max_value, best_col = info_gain, col
max_splited = splited_set
return max_value, best_col, max_splited
choose_best_col(df, 'play')
(0.2467498197744391,
'outlook',
{'sunny': humility outlook play temp windy
0 high sunny no hot False
1 high sunny no hot True
7 high sunny no mild False
8 normal sunny yes cool False
10 normal sunny yes mild True,
'overcast': humility outlook play temp windy
2 high overcast yes hot False
6 normal overcast yes cool True
11 high overcast yes mild True
12 normal overcast yes hot False,
'rainy': humility outlook play temp windy
3 high rainy yes mild False
4 normal rainy yes cool False
5 normal rainy no cool True
9 normal rainy yes mild False
13 high rainy no mild True})
根据上述代码和示例数据集构造一个ID3决策树:
class ID3Tree:
# define a Node class
class Node:
def __init__(self, name):
self.name = name
self.connections = {}
def connect(self, label, node):
self.connections[label] = node
def __init__(self, data, label):
self.columns = data.columns
self.data = data
self.label = label
self.root = self.Node("Root")
# print tree method
def print_tree(self, node, tabs):
print(tabs + node.name)
for connection, child_node in node.connections.items():
print(tabs + "\t" + "(" + connection + ")")
self.print_tree(child_node, tabs + "\t\t")
def construct_tree(self):
self.construct(self.root, "", self.data, self.columns)
# construct tree
def construct(self, parent_node, parent_connection_label, input_data, columns):
max_value, best_col, max_splited = choose_best_col(input_data[columns], self.label)
if not best_col:
node = self.Node(input_data[self.label].iloc[0])
parent_node.connect(parent_connection_label, node)
return
node = self.Node(best_col)
parent_node.connect(parent_connection_label, node)
new_columns = [col for col in columns if col != best_col]
# Recursively constructing decision trees
for splited_value, splited_data in max_splited.items():
self.construct(node, splited_value, splited_data, new_columns)
tree1 = ID3Tree(df, 'play')
tree1.construct_tree()
tree1.print_tree(tree1.root, "")
2.CART
所谓CART算法,全名叫Classification and Regression Tree,即分类与回归树。顾名思义,相较于此前的ID3算法和C4.5算法,CART除了可以用于分类任务外,还可以完成回归分析。
回归树用于目标变量为连续型的建模任务,其特征选择准则用的是平方误差最小准则。分类树用于目标变量为离散型的的建模任务,其特征选择准则用的是基尼指数(Gini Index),这也有别于此前ID3的信息增益准则和C4.5的信息增益比准则。无论是回归树还是分类树,其算法核心都在于递归地选择最优特征构建决策树。
除了选择最优特征构建决策树之外,CART算法还包括另外一个重要的部分:剪枝。剪枝可以视为决策树算法的一种正则化手段,作为一种基于规则的非参数监督学习方法,决策树在训练很容易过拟合,导致最后生成的决策树泛化性能不高。
另外,CART作为一种单模型,也是GBDT的基模型。当很多棵CART分类树或者回归树集成起来的时候,就形成了GBDT模型。
回归树
给定输入特征向量X和输出连续型变量Y,一个回归树的生成就对应着输入空间的一个划分以及在划分的单元上的输出值。假设输入空间被划分为M个单元,在每一个单元上都有一个固定的输出值,所以回归树模型可以表示为
在输入空间划分确定时,回归树算法使用最小平方误差准则来选择最优特征和最优且切分点。具体来说就是对全部特征进行遍历,按照最小平方误差准则来求解最优切分变量和切分点。即求解如下公式
这种按照最小平方误差准则来递归地寻找最佳特征和最优切分点构造决策树的过程就是最小二乘回归树算法。
完整的最小二乘回归树生成算法如下:
回归树的树深度越大的情况下,模型复杂度越高,对数据的拟合程度就越好,但相应的泛化能力就得不到保证。
分类树
CART分类树跟回归树大不相同,但与此前的ID3和C4.5基本套路相同。ID3和C4.5分别采用信息增益和信息增益比来选择最优特征,但CART分类树采用Gini指数来进行特征选择。先来看Gini指数的定义。
Gini指数是针对概率分布而言的。假设在一个分类问题中有K个类,样本属于第k个类的概率为Pk,则该样本概率分布的基尼指数为
具体到实际的分类计算中,给定样本集合D的Gini指数计算如下
相应的条件Gini指数,也即给定特征A的条件下集合D的Gini指数计算如下
实际构造分类树时,选择条件Gini指数最小的特征作为最优特征构造决策树。完整的分类树构造算法如下:
一棵基于Gini指数准则选择特征的分类树构造:
剪枝
基于最小平方误差准则和Gini指数准则构造好决策树只能算完成的模型的一半。为了构造好的决策树能够具备更好的泛化性能,通过我们需要对其进行剪枝(pruning)。在特征选择算法效果趋于一致的情况下,剪枝逐渐成为决策树更为重要的一部分。
所谓剪枝,就是将构造好的决策树进行简化的过程。具体而言就是从已生成的树上裁掉一些子树或者叶结点,并将其根结点或父结点作为新的叶结点。
通常来说,有两种剪枝方法。一种是在决策树生成过程中进行剪枝,也叫预剪枝(pre-pruning)。另一种就是前面说的基于生成好的决策树自底向上的进行剪枝,又叫后剪枝(post-pruning)。
先来看预剪枝。预剪枝是在树生成过程中进行剪枝的方法,其核心思想在树中结点进行扩展之前,先计算当前的特征划分能否带来决策树泛化性能的提升,如果不能的话则决策树不再进行生长。预剪枝比较直接,算法也简单,效率高,适合大规模问题计算,但预剪枝可能会有一种”早停”的风险,可能会导致模型欠拟合。
后剪枝则是等树完全生长完毕之后再从最底端的叶子结点进行剪枝。CART剪枝正是一种后剪枝方法。简单来说,就是自底向上对完全树进行逐结点剪枝,每剪一次就形成一个子树,一直到根结点,这样就形成一个子树序列。然后在独立的验证集数据上对全部子树进行交叉验证,哪个子树误差最小,哪个就是最优子树。
lst = ['a', 'b', 'c', 'd', 'b', 'c', 'a', 'b', 'c', 'd', 'a']
# Gini指数的计算函数
def gini(nums):
probs = [nums.count(i)/len(nums) for i in set(nums)]
gini = sum([p*(1-p) for p in probs])
return gini
gini(lst)
# 根据特征和特征值进行数据划分的方法:
def split_dataframe(data, col):
'''
input: dataframe, 根据那一列分.
output: a dict of splited dataframe.
'''
# 这列中唯一的值
unique_values = data[col].unique()
# 装dataframe的字典,键是上面唯一的值
result_dict = {elem : pd.DataFrame for elem in unique_values}
# 根据列值分dataframe
for key in result_dict.keys():
# 各行如果这列等于该键则添加
result_dict[key] = data[:][data[col] == key]
return result_dict
split_example = split_dataframe(df, 'temp')
split_example
# 根据熵计算公式和数据集划分方法计算信息增益来选择最佳特征
def choose_best_col(df, label):
'''
input: datafram, 目标特征
output: max infomation gain, best column,
splited dataframe dict based on best column.
'''
gini_D = gini(df[label].tolist())
# 除了目标特征的列表
cols = [col for col in df.columns if col not in [label]]
# ['humility', 'outlook', 'temp', 'windy']
# 初始化最大信息增益、最佳列和最佳分割dict
min_value, best_col = 999, None
min_splited = None
# 根据不同的列拆分数据
for col in cols:
splited_set = split_dataframe(df, col)
# {'high': humility outlook play temp windy
# 0 high sunny no hot False
# 1 high sunny no hot True
# 2 high overcast yes hot False
# 3 high rainy yes mild False
# 7 high sunny no mild False
# 11 high overcast yes mild True
# 13 high rainy no mild True,
# 'normal': humility outlook play temp windy
# 4 normal rainy yes cool False
# 5 normal rainy no cool True
# 6 normal overcast yes cool True
# 8 normal sunny yes cool False
# 9 normal rainy yes mild False
# 10 normal sunny yes mild True
# 12 normal overcast yes hot False}
gini_DA = 0
for subset_col, subset in splited_set.items():
# 计算拆分后的dataframe中目的特征的gini index
gini_Di = gini(subset[label].tolist())
# 计算当前特征的gini index。加入某个特征之后的条件熵。
gini_DA += len(subset)/len(df) * gini_Di
if gini_DA < min_value:
min_value, best_col = gini_DA, col
min_splited = splited_set
return min_value, best_col, min_splited
choose_best_col(df, 'play')
class CartTree:
# define a Node class
class Node:
def __init__(self, name):
self.name = name
self.connections = {}
def connect(self, label, node):
self.connections[label] = node
def __init__(self, data, label):
self.columns = data.columns
self.data = data
self.label = label
self.root = self.Node("Root")
# print tree method
def print_tree(self, node, tabs):
print(tabs + node.name)
for connection, child_node in node.connections.items():
print(tabs + "\t" + "(" + connection + ")")
self.print_tree(child_node, tabs + "\t\t")
def construct_tree(self):
self.construct(self.root, "", self.data, self.columns)
# construct tree
def construct(self, parent_node, parent_connection_label, input_data, columns):
min_value, best_col, min_splited = choose_best_col(input_data[columns], self.label)
if not best_col:
node = self.Node(input_data[self.label].iloc[0])
parent_node.connect(parent_connection_label, node)
return
node = self.Node(best_col)
parent_node.connect(parent_connection_label, node)
new_columns = [col for col in columns if col != best_col]
# Recursively constructing decision trees
for splited_value, splited_data in min_splited.items():
self.construct(node, splited_value, splited_data, new_columns)
tree1 = CartTree(df, 'play')
tree1.construct_tree()
tree1.print_tree(tree1.root, "")
优缺点
优点:
算法简单,模型具有很强的解释性
可以用于分类和回归问题
缺点:
决策树模型很容易出现过拟合现象,即训练数据集的训练误差很小,测试数据集的测试误差很大,且
不同的训练数据集构建的模型相差也很大。实际项目中,我们往往不是单独使用决策树模型,为了避
免决策树的过拟合,需对决策树结合集成算法使用,如bagging算法和boosting算法。
参数 | DecisionTreeClassifier | DecisionTreeRegressor |
---|---|---|
特征选择标准criterion | 可"gini"或者"entropy",默认的"gini"就可以,即CART算法。除非你更喜欢类似ID3, C4.5的最优特征选择方法。 | 可以使用"mse"或者"mae"推荐使用默认的"mse"。一般来说"mse"比"mae"更加精确。 |
特征划分点选择标准splitter | 可"best"或者"random"。前者在特征的所有划分点中找出最优的划分点。后者是随机的在部分划分点中找局部最优的划分点。默认的"best"适合样本量不大的时候,而**如果样本数据量非常大,此时决策树构建推荐"random" ** | |
划分时考虑的最大特征数max_features | 默认是"None",意味着划分时考虑所有的特征数;如果是"log2"意味着划分时最多考虑个特征;如果是"sqrt"或者"auto"意味着划分时最多考虑个特征。如果是整数,代表考虑的特征绝对数。如果是浮点数,代表考虑特征百分比,即考虑(百分比xN)取整后的特征数。其中N为样本总特征数。一般来说,如果样本特征数不多,比如小于50,我们用默认的"None"就可以了,如果特征数非常多,我们可以灵活使用刚才描述的其他取值来控制划分时考虑的最大特征数,以控制决策树的生成时间。 | |
决策树最大深max_depth | 默认可以不输入,如果不输入的话,决策树在建立子树的时候不会限制子树的深度。一般来说,数据少或者特征少的时候可以不管这个值。如果模型样本量多,特征也多的情况下,推荐限制这个最大深度,具体的取值取决于数据的分布。常用的可以取值10-100之间。 | |
内部节点再划分所需最小样本数min_samples_split | 这个值限制了子树继续划分的条件,如果某节点的样本数少于min_samples_split,则不会继续再尝试选择最优特征来进行划分。 默认是2.如果样本量不大,不需要管这个值。如果样本量数量级非常大,则推荐增大这个值。我之前的一个项目例子,有大概10万样本,建立决策树时,我选择了min_samples_split=10。可以作为参考。 | |
叶子节点最少样本数min_samples_leaf | 这个值限制了叶子节点最少的样本数,如果某叶子节点数目小于样本数,则会和兄弟节点一起被剪枝。 默认是1,可以输入最少的样本数的整数,或者最少样本数占样本总数的百分比。如果样本量不大,不需要管这个值。如果样本量数量级非常大,则推荐增大这个值。之前的10万样本项目使用min_samples_leaf的值为5,仅供参考。 | |
叶子节点最小的样本权重和min_weight_fraction_leaf | 这个值限制了叶子节点所有样本权重和的最小值,如果小于这个值,则会和兄弟节点一起被剪枝。 默认是0,就是不考虑权重问题。一般来说,如果我们有较多样本有缺失值,或者分类树样本的分布类别偏差很大,就会引入样本权重,这时我们就要注意这个值了。 | |
最大叶子节点数max_leaf_nodes | 通过限制最大叶子节点数,可以防止过拟合,默认是"None”,即不限制最大的叶子节点数。如果加了限制,算法会建立在最大叶子节点数内最优的决策树。如果特征不多,可以不考虑这个值,但是如果特征分成多的话,可以加以限制,具体的值可以通过交叉验证得到。 | |
类别权重class_weight | 指定样本各类别的的权重,主要是为了防止训练集某些类别的样本过多,导致训练的决策树过于偏向这些类别。这里可以自己指定各个样本的权重,或者用“balanced”,如果使用“balanced”,则算法会自己计算权重,样本量少的类别所对应的样本权重会高。当然,如果你的样本类别分布没有明显的偏倚,则可以不管这个参数,选择默认的"None" | 不适用于回归树 |
节点划分最小不纯度min_impurity_split | 这个值限制了决策树的增长,如果某节点的不纯度(基尼系数,信息增益,均方差,绝对差)小于这个阈值,则该节点不再生成子节点。即为叶子节点 。 | |
数据是否预排序presort | 这个值是布尔值,默认是False不排序。一般来说,如果样本量少或者限制了一个深度很小的决策树,设置为true可以让划分点选择更加快,决策树建立的更加快。如果样本量太大的话,反而没有什么好处。问题是样本量少的时候,我速度本来就不慢。所以这个值一般懒得理它就可以了。 |
除了这些参数要注意以外,其他在调参时的注意点有:
1)当样本少数量但是样本特征非常多的时候,决策树很容易过拟合,一般来说,样本数比特征数多一些会比较容易建立健壮的模型
2)如果样本数量少但是样本特征非常多,在拟合决策树模型前,推荐先做维度规约,比如主成分分析(PCA),特征选择(Losso)或者独立成分分析(ICA)。这样特征的维度会大大减小。再来拟合决策树模型效果会好。
3)推荐多用决策树的可视化(下节会讲),同时先限制决策树的深度(比如最多3层),这样可以先观察下生成的决策树里数据的初步拟合情况,然后再决定是否要增加深度。
4)在训练模型先,注意观察样本的类别情况(主要指分类树),如果类别分布非常不均匀,就要考虑用class_weight来限制模型过于偏向样本多的类别。
5)决策树的数组使用的是numpy的float32类型,如果训练数据不是这样的格式,算法会先做copy再运行。
6)如果输入的样本矩阵是稀疏的,推荐在拟合前调用csc_matrix稀疏化,在预测前调用csr_matrix稀疏化。