决策树,这里的树不是我们日常看到路边的树,是指一种数据结构——树。
如上图就是一个二叉树,你可以把决策树看做二叉树。
二叉树上的每个圈就是节点,可以看做一个if
判断,将数据进行二分。
最低的叶节点就是对应的分类。
决策树算法
用决策树分类,从根结点开始,对实例的某一特征进行测试,根据测试结果,将实例分配到其子结点;这时,每一个子结点对应着该特征的一个取值。如此递归地对实例进行测试并分配,直至达到叶结点,最后将实例分到叶结点对应的类中。
决策树的路径或其对应的if-then
规则集合具有一个重要的性质:互斥并且完备。
这句话的意思是,每一个实例都能够从根节点开始依次访问内结点直到叶结点,每一个实例都有一条路径,并且只有一条路径。
决策树的NP完全问题
在决策树算法中,寻找最优决策树是一个NP完全问题,因而我们无法在多项式时间内找到全局最优,或者近似全局最优解。
因此,大多数决策树算法都采用启发法,如贪心算法,来指导对假设空间的搜索。在这个过程中,将在每一个节点上做局部最优解,并在最后得到一个决策树,但这样获得的决策树无法保证它是全局最优的。
决策树的构建
-
构建根结点: 选择一个最优特征,按照这一特征将训练数据集分成子集,使得各个子集有一个在当前条件下最好的分类。
-
构建叶结点或内部结点:
如果这些子集已经能够被基本正确分类,那么构建叶结点,并将这些子集分到所对应的叶结点中去。
如果还有子集不能被基本正确分类,那么就对这些子集选择新的最优特征,继续对其进行分割,构建相应的内部结点。
-
递归:按照第二步,如此递归地进行下去,直至所有训练数据子集被基本正确分类,或者没有合适的特征为止。最后每个子集都被分到叶结点上,即都有了明确的类。
此时生成的决策树模型可能是过拟合的,因此在构建决策树后,我们还需要自下而上地对决策树进行剪枝,使决策树模型变得更简单,并具有更好的泛化能力。
因为决策树在生成中,对于每一个结点而言我们只考虑一个特征,因此决策树的生成对应于模型的局部选择,只考虑局部最优(贪心算法中对小问题的求解)。而在剪枝过程中,我们需要考虑决策树的整体泛化能力,因此剪枝对应于模型的全局选择,需要考虑全局最优(贪心算法无法保证全局最优)。
特征选择
书中给出的方法是,依据信息增益或信息增益比来选择最优特征。
信息增益
信息增益表示得知特征X的信息而使得类Y的信息的不确定性减少的程度。
g
(
D
,
A
)
=
H
(
D
)
−
H
(
D
∣
A
)
g(D,A)=H(D)-H(D|A)
g(D,A)=H(D)−H(D∣A)
熵 H ( Y ) H(Y) H(Y)与条件熵 H ( Y ∣ X ) H(Y|X) H(Y∣X)之差称为互信息。在决策树中,计算得到的信息增益就是训练数据集中类 ( Y ) (Y) (Y)与特征 ( X ) (X) (X)的互信息,即当我们得知特征 X X X后 Y Y Y的信息的不确定性减少的程度。
针对各个特征,可以对类求得不同的信息增益,信息增益大的特征具有更强的分类能力,因为该特征对Y的不确定性减少的程度最大。
但信息增益存在一个问题,以信息增益作为划分训练数据集的特征,存在偏向于选择取值较多的特征的问题,造成过拟合。
上面提到了熵和条件熵,下面简单说明一下。
熵
熵表示随机变量不确定性的度量。熵值越低,不确定性越低。熵值越高,不确定性越高。比如骰子,那么当每个值出现的概率相等,即1/6时,其不确定性最高,因为6个可选值将随机出现。
随机变量X的熵定义为:
H
(
X
)
=
−
∑
i
=
1
n
p
i
log
p
i
H(X)=-\sum_{i=1}^np_i\log{p_i}
H(X)=−i=1∑npilogpi
若pi=0,则定义0log0 = 0。通常,式中对数以2或者e为底,这时熵的单位分别称作比特或纳特。
观察熵的公式,可以发现公式中仅出现p而未出现X,说明熵的大小与X的值无关,而与X值的分布有关,因此也可以将熵记作H§。回想之前说到的骰子问题,骰子的可取值为1-6,但是熵仅与这6个可取值的出现的概率有关,也就是与X无关,但与X值的分布有关。
条件熵
H
(
Y
∣
H
)
=
∑
i
=
1
n
p
i
H
(
Y
∣
X
=
x
i
)
=
−
∑
x
∈
X
p
(
x
)
∑
y
∈
Y
p
(
y
∣
x
)
l
o
g
p
(
y
∣
x
)
H(Y|H)=\sum_{i=1}^np_iH(Y|X=x_i)=-\sum_{x\in X}p(x)\sum_{y\in Y}p(y|x)logp(y|x)
H(Y∣H)=i=1∑npiH(Y∣X=xi)=−x∈X∑p(x)y∈Y∑p(y∣x)logp(y∣x)
这里
p
i
=
P
(
X
=
x
i
)
,
i
=
1
,
2
,
…
,
n
p_i = P(X = x_i), i = 1,2,…,n
pi=P(X=xi),i=1,2,…,n。需要注意的是,条件熵中需要对每个x可取值进行计算并加和。
当熵和条件熵中的概率是通过训练集得到时,所对应的熵与条件熵分别称为经验熵和经验条件熵。
信息增益比
可以使用信息增益比解决信息增益的过拟合问题。
信息增益比的公式:
g
R
(
D
,
A
)
=
g
(
D
,
A
)
H
A
(
D
)
g_R(D,A)=\frac{g(D,A)}{H_A(D)}
gR(D,A)=HA(D)g(D,A)
H
A
(
D
)
H_A(D)
HA(D)代表训练数据集D关于特征A的值的熵。即此处将特征A作为变量计算A的熵。
H
A
(
D
)
=
−
∑
i
=
1
n
∣
D
i
∣
∣
D
∣
l
o
g
∣
D
i
∣
∣
D
∣
H_A(D)=-\sum_{i=1}^n\frac{|D_i|}{|D|}log\frac{|D_i|}{|D|}
HA(D)=−i=1∑n∣D∣∣Di∣log∣D∣∣Di∣
上式中,n是特征A的取值个数,
D
i
D_i
Di即在A取值为
a
i
a_i
ai时的个数,D为训练集中记录的个数。
信息增益比的本质就是在信息增益中添加了一个惩罚项 H A ( D ) H_A(D) HA(D),当特征的可取值个数较大时, H A ( D ) H_A(D) HA(D) 较大,那么信息增益比较小,因此,信息增益比倾向于选择可取值个数较小的特征,解决了采用信息增益进行特征选择时面临的过拟合问题。
现在我们了解了如何通过信息增益或信息增益比来对根结点和内部结点选择特征,现在,因为分别采用信息增益和信息增益比。
决策树生成算法
ID3决策树生成算法
ID3算法的核心是在决策树各个结点上应用信息增益选择特征。
具体方法是:从根结点开始,对结点计算所有可能的特征的信息增益,选择信息增益最大的特征作为结点的特征,由该特征的不同取值建立子结点,并递归地构建决策树,直到所有特征的信息增益均很小或没有特征可以选择为止。
C4.5决策树生成算法
C4.5算法对ID3算法进行了改进,用信息增益比来选择特征。
决策树的剪枝
在决策树学习中将已生成的树进行简化的过程称为剪枝,剪枝的目的是使决策树模型变得更简单,防止过拟合,增加泛化能力。剪枝可以通过极小化决策树整体的损失函数来实现。
损失函数
C
a
(
T
)
=
∑
t
=
1
∣
T
∣
N
t
H
t
(
T
)
+
a
(
T
)
C_a(T)=\sum_{t=1}^{|T|}N_tH_t(T)+a(T)
Ca(T)=t=1∑∣T∣NtHt(T)+a(T)
H t ( T ) = − ∑ k = 1 K N t k N t l o g N t k N t H_t(T)=-\sum_{k=1}^K\frac{N_{tk}}{N_t}log\frac{N_{tk}}{N_t} Ht(T)=−k=1∑KNtNtklogNtNtk
其中树T的叶结点个数为 ∣ T ∣ |T| ∣T∣,t是树T的叶结点,该叶结点有 N t N_t Nt个样本点,其中k类的样本点有 N t k N_{tk} Ntk个, k = 1 , 2 , … , K k=1, 2, …, K k=1,2,…,K, H t ( T ) H_t(T) Ht(T)为叶结点t上的经验熵, α ≥ 0 α≥0 α≥0为参数,较大的α促使选择较简单的模型,较小的α促使选择较复杂的模型。
将两式合并后可得:
C
(
T
)
=
∑
t
=
1
∣
T
∣
N
t
H
t
(
T
)
=
−
∑
t
=
1
∣
T
∣
∑
k
=
1
K
N
t
k
l
o
g
N
t
k
N
t
C(T)=\sum_{t=1}^{|T|}N_tH_t(T)=-\sum_{t=1}^{|T|}\sum_{k=1}^KN_{tk}log\frac{N_{tk}}{N_t}
C(T)=t=1∑∣T∣NtHt(T)=−t=1∑∣T∣k=1∑KNtklogNtNtk
C a ( T ) = C ( T ) + a ∣ T ∣ C_a(T)=C(T)+a|T| Ca(T)=C(T)+a∣T∣
C ( T ) C(T) C(T)表示模型对训练数据的预测误差,即模型与训练数据的拟合程度。它对每个叶结点进行熵值的计算,其实是在评判叶结点中对训练集的分类的正确程度。
乘以 N t k N_{tk} Ntk,相当于每个叶结点的熵的权重,个数越大,则权重越高,那么对于该叶结点,不管它的分类结果拟合较好还是较差,都因为其权重大而被放大了,因而对整体决策树而言更加重要。
∣ T ∣ |T| ∣T∣表示模型复杂度,参数α≥0控制两者之间的影响。决策树的叶结点个数越多说明决策树越复杂,如果过于复杂,即叶节点个数过多,则会造成过拟合。而α则代表了叶节点个数的重要程度,α越大,说明叶节点个数对整体而言越重要。
剪枝流程
-
计算每个结点的经验熵
-
递归地从树的叶结点向上回缩
设一组叶结点回缩到其父结点之前与之后的整体树分别为 T b T_b Tb与 T a T_a Ta,其对应的损失函数值分别是 C α ( T b ) C_α(T_b) Cα(Tb)与 C α ( T a ) C_α(T_a) Cα(Ta)。如果:
KaTeX parse error: Can't use function '$' in math mode at position 15: C_α(T_b)\leq $̲C_α(T_a)则进行剪枝,即将父结点变为新的叶结点。
直到不能继续为止,得到损失函数最小的子树Tα。
代码实现
ID3
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from collections import Counter
import math
from math import log
def create_data():
datasets = [['青年', '否', '否', '一般', '否'],
['青年', '否', '否', '好', '否'],
['青年', '是', '否', '好', '是'],
['青年', '是', '是', '一般', '是'],
['青年', '否', '否', '一般', '否'],
['中年', '否', '否', '一般', '否'],
['中年', '否', '否', '好', '否'],
['中年', '是', '是', '好', '是'],
['中年', '否', '是', '非常好', '是'],
['中年', '否', '是', '非常好', '是'],
['老年', '否', '是', '非常好', '是'],
['老年', '否', '是', '好', '是'],
['老年', '是', '否', '好', '是'],
['老年', '是', '否', '非常好', '是'],
['老年', '否', '否', '一般', '否'],
]
labels = [u'年龄', u'有工作', u'有自己的房子', u'信贷情况', u'类别']
# 返回数据集和每个维度的名称
return datasets, labels
class Node:
def __init__(self, root=True, label=None, feature_name=None, feature=None):
self.root = root
self.label = label
self.feature_name = feature_name
self.feature = feature
self.tree = {}
self.result = {'label:': self.label, 'feature': self.feature, 'tree': self.tree}
def __repr__(self):
return '{}'.format(self.result)
def add_node(self, val, node):
self.tree[val] = node
def predict(self, features):
if self.root is True:
return self.label
return self.tree[features[self.feature]].predict(features)
class DTree:
def __init__(self, epsilon=0.1):
self.epsilon = epsilon
self._tree = {}
@staticmethod
def calc_ent(datasets):
data_length = len(datasets)
label_count = {}
for i in range(data_length):
label = datasets[i][-1]
if label not in label_count:
label_count[label] = 0
label_count[label] += 1
ent = -sum([(p/data_length)*log(p/data_length, 2) for p in label_count.values()])
return ent
# 经验条件熵
def cond_ent(self, datasets, axis=0):
data_length = len(datasets)
feature_sets = {}
for i in range(data_length):
feature = datasets[i][axis]
if feature not in feature_sets:
feature_sets[feature] = []
feature_sets[feature].append(datasets[i])
cond_ent = sum([(len(p)/data_length)*self.calc_ent(p) for p in feature_sets.values()])
return cond_ent
# 信息增益
@staticmethod
def info_gain(ent, cond_ent):
return ent - cond_ent
def info_gain_train(self, datasets):
count = len(datasets[0]) - 1
ent = self.calc_ent(datasets)
best_feature = []
for c in range(count):
c_info_gain = self.info_gain(ent, self.cond_ent(datasets, axis=c))
best_feature.append((c, c_info_gain))
# 比较大小
best_ = max(best_feature, key=lambda x: x[-1])
return best_
def train(self, train_data):
"""
input:数据集D(DataFrame格式),特征集A,阈值eta
output:决策树T
"""
_, y_train, features = train_data.iloc[:, :-1], train_data.iloc[:, -1], train_data.columns[:-1]
# 1,若D中实例属于同一类Ck,则T为单节点树,并将类Ck作为结点的类标记,返回T
if len(y_train.value_counts()) == 1:
return Node(root=True,
label=y_train.iloc[0])
# 2, 若A为空,则T为单节点树,将D中实例树最大的类Ck作为该节点的类标记,返回T
if len(features) == 0:
return Node(root=True, label=y_train.value_counts().sort_values(ascending=False).index[0])
# 3,计算最大信息增益 同5.1,Ag为信息增益最大的特征
max_feature, max_info_gain = self.info_gain_train(np.array(train_data))
max_feature_name = features[max_feature]
# 4,Ag的信息增益小于阈值eta,则置T为单节点树,并将D中是实例数最大的类Ck作为该节点的类标记,返回T
if max_info_gain < self.epsilon:
return Node(root=True, label=y_train.value_counts().sort_values(ascending=False).index[0])
# 5,构建Ag子集
node_tree = Node(root=False, feature_name=max_feature_name, feature=max_feature)
feature_list = train_data[max_feature_name].value_counts().index
for f in feature_list:
sub_train_df = train_data.loc[train_data[max_feature_name] == f].drop([max_feature_name], axis=1)
# 6, 递归生成树
sub_tree = self.train(sub_train_df)
node_tree.add_node(f, sub_tree)
# pprint.pprint(node_tree.tree)
return node_tree
def fit(self, train_data):
self._tree = self.train(train_data)
return self._tree
def predict(self, X_test):
return self._tree.predict(X_test)
datasets, labels = create_data()
data_df = pd.DataFrame(datasets, columns=labels)
dt = DTree()
tree = dt.fit(data_df)
dt.predict(['老年', '否', '否', '一般'])