决策树 (decision tree) 是一种基本的分类与回归方法。本章主要讨论用于分类的决策树。决策树模型呈树形结构,在分类问题中,表示基于特征对实例进行分类的过程。它可以认为是 if-then 规则的集合,也可以认为是定义在特征空间与类空间上的条件概率分布。其主要优点是模型具有可读性,分类速度快。学习时,利用训练数据,根据损失函数最小化的原则建立决策树模型。预测时,对新的数据,利用决策树模型进行分类。决策树学习通常包括 3 个步骤:特征选择、决策树的生成 和决策树的修剪。
5.1 决策树模型与学习
5.1.1 决策树模型
定义(决策树)
分类决策树模型是一种描述对实例进行分类的树形结构。决策树由结点和有向边组成。结点有两种类型:内部结点和叶结点。内部结点表示一个特征或属性,叶结点表 示一个类。
用决策树分类,从根结点开始,对实例的某一特征进行测试,根据测试结果,将实例分配到其子结点:这时,每一个子结点对应着该特征的一个取值。如此递归地对实例进行测试并分配,直至达到叶结点。最后将实例分到叶结点的类中。
5.1.2 决策树与 if-then 规则
可以将决策树看成一个 if-then 规则的集合。将决策树转换成 if-then 规则的过程是这样的:由决策树的根结点到叶结点的每一条路径构建一条规则;路径上内部结点的特征对应着规则的条件,而叶结点的类对应着规则的结论。决策树的路径或其对应的 if-then 规则集合具有一个重要的性质:互斥并且完备。这就是说,每一个实例都被一条路径或一条规则所覆盖,而且只被一条路径或一条规则所覆盖。这里所谓覆盖是指实例的特征与路径上的特征一致或实例满足规则的条件。
5.1.3 决策树与条件概率分布
决策树还表示给定特征条件下类的条件概率分布。 这一条件概率分布定义在特征空间的一个划分上。 将特征空间划分为互不相交的单元或区域,并在每个单元定义一个类的概率分布就构成了一个条件概率分布。决策树的一条路径对应于划分中的一个单元。决策树所表示的条件概率分布由各个单元给定条件下类的条件概率分布组成。
5.1.4 决策树学习
假设给定训练数据集
D = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , … , ( x N , y N ) } D=\{(x_1,y_1),(x_2,y_2),\dots,(x_N,y_N)\} D={(x1,y1),(x2,y2),…,(xN,yN)}
其中, x i = ( x i ( 1 ) , x i ( 2 ) , … , x i ( n ) ) T x_i=(x^{(1)}_{i},x^{(2)}_{i},\dots,x^{(n)}_{i})^{T} xi=(xi(1),xi(2),…,xi(n))T为输入实例(特征向量), n n n为特征个数, y i ∈ { 1 , 2 , … , K } y_i \in \{1,2,\dots,K\} yi∈{1,2,…,K}为类标记, N N N为样本容量。决策树学习的目标是根据给定的训练数据集构建一个决策树模型,使它能够对实例进行正确的分类。
决策树学习本质上是从训练数据集中归纳出一组分类规则。与训练数据集不相矛盾的决策树(即能对训练数据进行正确分类的决策树)可能有多个,也可能一个都没有。我们需要的是一个与训练数据矛盾较小的决策树,同时具有很好的泛化能力。从另一个角度看,决策树学习是由训练数据集估计条件概率模型。基于特征空间划分的类的条件概率模型有无穷多个。我们选择的条件概率模型应该不仅对训练数据有很好的拟合,而且对未知数据有很好的预测。决策树学习用损失函数表示这一目标。
5.2 特征选择
5.2.1 特征选择问题
特征选择在于选取对训练数据具有分类能力的特征。这样可以提高决策树学习的效率。如果利用一个特征进行分类的结果与随机分类的结果没有很大差别,则称这个特征是没有分类能力的。经验上扔掉这样的特征对决策树学习的精度影响不大。通常特征选择的准则是信息增益或信息增益比。
5.2.2 信息增益
在信息论与概率统计中,熵(entropy)是表示随机变量不确定性的度量。设 X X X 是一个取有限个值的离散随机变量,其概率分布为
P ( X = x i ) = p i , i = 1 , 2 , … , n P(X=x_i)=p_i,\quad i=1,2,\dots,n P(X=xi)=pi,i=1,2,…,n
则随机变量 X X X的熵定义为
H ( X ) = − ∑ i = 1 n p i log p i H(X)=-\sum^{n}_{i=1}p_i\log p_i H(X)=−i=1∑npilogpi
通常,上式中的对数以 2 为底或以 e 为底,这时熵的单位分别称作比特或纳特,熵越大,随机变量的不确定性就越大。从定义可验证
0 ≤ H ( p ) ≤ log n 0 \le H(p) \le \log n 0≤H(p)≤logn
随机变量 X X X 给定的条件下随机变量 Y Y Y 的条件熵 H ( Y ∣ X ) H(Y|X) H(Y∣X) , 定义为 X X X 给定条件下 Y Y Y 的条件概率分布的熵对 X X X 的数学期望
H ( Y ∣ X ) = ∑ i = 1 n p i H ( Y ∣ X = x i ) H(Y|X)=\sum^{n}_{i=1}p_iH(Y|X=x_i) H(Y∣X)=i=1∑npiH(Y∣X=xi)
信息增益表示得知特征 X X X 的信息而使得类 Y Y Y 的信息的不确定性减少的程度。
定义(信息增益)
特征 A A A 对训练数据集 D D D 的信息增益 g ( D , A ) g(D,A) g(D,A) , 定义为集合 D D D 的经验熵 H ( D ) H(D) H(D) 与特征 A A A 给定条件下 D D D 的经验条件熵 H ( D ∣ A ) H(D|A) H(D∣A) 之差,即
g ( D , A ) = H ( D ) − H ( D ∣ A ) g(D,A)=H(D)-H(D|A) g(D,A)=H(D)−H(D∣A)
根据信息增益准则的特征选择方法是:对训练数据集 D D D, 计算其每个特征的信息增益,井比较它们的大小,选择信息增益最大的特征。
5.2.3 信息增益比
以信息增益作为划分训练数据集的特征,存在偏向于选择取值较多的特征的问题。使用信息增益比可以对这一问题进行校正。这是特征选择的另一准则。
5.3 决策树的生成
5.3.1 ID3算法
ID3 算法的核心是在决策树各个结点上应用信息增益准则选择特征,递归地构建决策树。
具体方法是:从根结点开始,对结点计算所有可能的特征的信息增益,选择信息增益最大的特征作为结点的特征,由该特征的不同取值建立子结点;再对子结点递归地调用以上方法,构建决策树;直到所有特征的信息增益均很小或没有特征可以选择为止。 最后得到一棵决策树。 ID3 相当于用极大似然法进行概率模型的选择。
算法(ID3算法)
输入:训练数据集 D D D , 特征集 A A A 阈值 ε \varepsilon ε;
输出:决策树 T T T。
(1)若
D
D
D 中所有实例属于同一类
C
k
C_k
Ck , 则
T
T
T 为单结点树,并将类
C
k
C_k
Ck 作为该结点
的类标记,返回
T
T
T;
(2)若
A
=
∅
A= \varnothing
A=∅,则
T
T
T 为单结点树,并将
D
D
D 中实例数最大的类
C
k
C_k
Ck 作为该结点的类
标记,返回
T
T
T;
(3)否则,按算法 5.1 计算
A
A
A 中各特征对
D
D
D 的信息增益,选择信息增益最大的特
征
A
g
A_g
Ag;
(4)如果
A
g
A_g
Ag 的信息增益小于阔值
ε
\varepsilon
ε,则置
T
T
T 为单结点树,并将
D
D
D 中实例数最大
的类
C
k
C_k
Ck 作为该结点的类标记,返回
T
T
T;
(5)否则,对
A
g
A_g
Ag 的每一可能值
a
i
a_i
ai,依
A
g
=
a
i
A_g =a_i
Ag=ai 将
D
D
D 分割为若干非空子集
D
i
D_i
Di, 将
D
i
D_i
Di 中实例数最大的类作为标记,构建子结点,由结点及其子结点构成树
T
T
T , 返回
T
T
T;
(6)对第
i
i
i 个子结点,以
D
i
D_i
Di为训练集,以
A
−
{
A
g
}
A-\{A_g\}
A−{Ag} 为特征集,递归地调用步 (1)~
步 (5) ,得到子树
T
i
T_i
Ti,返回
T
i
T_i
Ti。
ID3 算法只有树的生成,所以该算法生成的树容易产生过拟合。
from math import log
def createDataSet():
dataSet = [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
labels = ['no surfacing', 'flippers']
return dataSet, labels
myDat, myLab = createDataSet()
# 计算信息熵
def calcShannonEnt(dataSet):
numEntries = len(dataSet)
# 为所有的分类类目创建字典
labelCounts = {}
for featVec in dataSet:
currentLabel = featVec[-1] # 取得最后一列数据
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
# 计算香农熵
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key]) / numEntries
shannonEnt -= prob * log(prob, 2)
return shannonEnt
# print(calcShannonEnt(myDat))
# 按照最大信息增益划分数据集
# 定义按照某个特征进行划分的函数splitDataSet
# 输入三个变量(待划分的数据集,特征,分类值)
def splitDataSet(dataSet, axis, value):
retDataSet = []
for featVec in dataSet:
if featVec[axis] == value:
reduceFeatVec = featVec[:axis]
reduceFeatVec.extend(featVec[axis + 1:])
retDataSet.append(reduceFeatVec)
return retDataSet # 返回不含划分特征的子集
# 定义按照最大信息增益划分数据的函数
def chooseBestFeatureToSplit(dataSet):
numFeature = len(dataSet[0]) - 1
baseEntropy = calcShannonEnt(dataSet) # 香农熵
bestInformGain = 0
bestFeature = -1
for i in range(numFeature):
featList = [number[i] for number in dataSet] # 得到某个特征下所有值(某列)
uniquelyVales = set(featList) # set无重复的属性特征值
newEntropy = 0
for value in uniquelyVales:
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet) / float(len(dataSet)) # 即p(t)
newEntropy += prob * calcShannonEnt(subDataSet) # 对各子集香农熵求和
infoGain = baseEntropy - newEntropy # 计算信息增益
# 最大信息增益
if infoGain > bestInformGain:
bestInformGain = infoGain
bestFeature = i
return bestFeature # 返回特征值
5.3.2 C4.5的生成算法
C4.5 算法与 ID3 算法相似, C4.5 算法对 ID3 算法进行了改进。 C4.5 在生成的过 程中,用信息增益比来选择特征。
5.4 决策树的剪枝
决策树生成算法递归地产生决策树,直到不能继续下去为止。这样产生的树往往对训练数据的分类很准确,但对未知的测试数据的分类却没有那么准确,即出现过拟合现象。过拟合的原因在于学习时过多地考虑如何提高对训练数据的正确分类,从而构建出过于复杂的决策树。 解决这个问题的办法是考虑决策树的复杂度,对己生成的决策树进行简化。
在决策树学习中将己生成的树进行简化的过程称为剪枝 (pruning) 。具体地,剪枝从己生成的树上裁掉一些子树或叶结点 , 并将其根结点或父结点作为新的叶结点,从而简化分类树模型。
决策树的剪枝往往通过极小化决策树整体的损失函数或代价函数来实现。 设树 T T T 的叶结点个数为 ∣ T ∣ |T| ∣T∣ , t t t 是树 T T T 的叶结点,该叶结点有 N t N_t Nt个样本点,其中 k k k 类的样本点有 N t k N_{tk} Ntk 个, k = 1 , 2 , ⋯ , K k = 1,2,\cdots ,K k=1,2,⋯,K , H t ( T ) H_t(T) Ht(T) 为叶结点 t t t 上的经验熵, α ≥ 0 \alpha \ge 0 α≥0为参数,则决策树学习的损失函数可以定义为
C α ( T ) = ∑ t = 1 ∣ T ∣ N t H t ( T ) + α ∣ T ∣ C_\alpha(T)=\sum^{|T|}_{t=1}N_tH_t(T)+\alpha|T| Cα(T)=t=1∑∣T∣NtHt(T)+α∣T∣
H t ( T ) = − ∑ k N t k N t log N t k N t H_{t}(T)=-\sum_{k} \frac{N_{t k}}{N_{t}} \log \frac{N_{t k}}{N_{t}} Ht(T)=−k∑NtNtklogNtNtk
在损失函数中,将式右端的第 1 项记作
C ( T ) = ∑ t = 1 ∣ T ∣ N t H t ( T ) = − ∑ t = 1 ∣ T ∣ ∑ k = 1 K N t k log N t k N t C(T)=\sum_{t=1}^{|T|} N_{t} H_{t}(T)=-\sum_{t=1}^{|T|} \sum_{k=1}^{K} N_{t k} \log \frac{N_{t k}}{N_{t}} C(T)=t=1∑∣T∣NtHt(T)=−t=1∑∣T∣k=1∑KNtklogNtNtk
这时有
C α ( T ) = C ( T ) + α ∣ T ∣ C_\alpha(T)=C(T)+\alpha|T| Cα(T)=C(T)+α∣T∣
C ( T ) C(T) C(T) 表示模型对训练数据的预测误差,即模型与训练数据的拟合程度 , ∣ T ∣ |T| ∣T∣ 表示模型复杂度,参数 α ≥ 0 \alpha \ge0 α≥0 控制两者之间的影响。较大的 α \alpha α 促使选择较简单的模型(树),较小的 α \alpha α 促使选择较复杂的模型(树)。 α = 0 \alpha=0 α=0 意味着只考虑模型与训练数据的拟合程度,不考虑模型的复杂度。
剪枝,就是当 α \alpha α 确定时,选择损失函数最小的模型,即损失函数最小的子树。当 α \alpha α 值确定时,子树越大,往往与训练数据的拟合越好,但是模型的复杂度就越高;相反,子树越小,模型的复杂度就越低,但是往往与训练数据的拟合不好。损失函数正好表示了对两者的平衡。
算法(树的剪枝算法)
输入:生成算法产生的整个树 T T T,参数 α \alpha α;
输出:修建后的子树 T α T_\alpha Tα。
(1)计算每个结点的经验熵。
(2)递归地从树地叶结点向上回缩。
如果有 C α ( T A ) ≤ C α ( T B ) C_\alpha(T_A) \le C_\alpha(T_B) Cα(TA)≤Cα(TB),则进行剪枝,即将父结点变为新的叶结点
注意,上式只需考虑两个树的损失函数的差,其计算可以在局部进行。所 以,决策树的剪枝算法可以由一种动态规划的算法实现。
(3)返回(2),直到不能继续位置,得到损失函数最小的子树 T α T_\alpha Tα。
5.5 CART算法
分类与回归树 (classification and regression tree, CART)模型是应用广泛的决策树学习方法。CART 同样由特征选择、树的生成及剪枝组成,既可以用于分类也可以用于回归。以下将用于分类与回归的树统称为决策树。
CART 是在给定输入随机变量 X X X 条件下输出随机变量 Y Y Y 的条件概率分布的学习方法。 CART 假设决策树是二叉树,内部结点特征的取值为"是"和"否",左分支是取值为"是"的分支,右分支是取值为"否"的分支。这样的决策树等价于递归地二分每个特征,将输入空间即特征空间划分为有限个单元,并在这些单元上确定预测的概率分布,也就是在输入给定的条件下输出的条件概率分布。
CART 算法由以下两步组成:
(1)决策树生成:基于训练数据集生成决策树,生成的决策树要尽量大
(2)决策树剪枝:用验证数据集对己生成的树进行剪枝并选择最优子树,这时用损失函数最小作为剪技的标准。
import operator
def createDataSet1():
"""
创造示例数据/读取数据
@return dataSet labels:数据集 特征集
"""
# 数据集
originDataSet = [('青年', '否', '否', '一般', '不同意'),
('青年', '否', '否', '好', '不同意'),
('青年', '是', '否', '好', '同意'),
('青年', '是', '是', '一般', '同意'),
('青年', '否', '否', '一般', '不同意'),
('中年', '否', '否', '一般', '不同意'),
('中年', '否', '否', '好', '不同意'),
('中年', '是', '是', '好', '同意'),
('中年', '否', '是', '非常好', '同意'),
('中年', '否', '是', '非常好', '同意'),
('老年', '否', '是', '非常好', '同意'),
('老年', '否', '是', '好', '同意'),
('老年', '是', '否', '好', '同意'),
('老年', '是', '否', '非常好', '同意'),
('老年', '否', '否', '一般', '不同意')]
# 特征集
originLabels = ['年龄', '有工作', '有房子', '信贷情况']
return originDataSet, originLabels
def calcProbabilityEnt(entDataSet):
"""
样本点属于第1个类的概率p,即计算2p(1-p)中的p
@param entDataSet: 数据集
@return probabilityEnt: 数据集的概率
"""
numEntries = len(entDataSet) # 数据条数
feaCounts = 0
fea1 = entDataSet[0][len(entDataSet[0]) - 1]
for featVec in entDataSet: # 每行数据
if featVec[-1] == fea1:
feaCounts += 1
probabilityEnt = float(feaCounts) / numEntries
return probabilityEnt
def splitDataSet(splitData, index, value):
"""
划分数据集,提取含有某个特征的某个属性的所有数据
@param splitData: 数据集
@param index: 属性值所对应的特征列
@param value: 某个属性值
@return retDataSet: 含有某个特征的某个属性的数据集
"""
retDataSet = []
for featVec in splitData:
# 如果该样本该特征的属性值等于传入的属性值,则去掉该属性然后放入数据集中
if featVec[index] == value:
reducedFeatVec = featVec[:index] + featVec[index + 1:] # 去掉该属性的当前样本
retDataSet.append(reducedFeatVec) # append向末尾追加一个新元素,新元素在元素中格式不变,如数组作为一个值在元素中存在
return retDataSet
def chooseBestFeatureToSplit(chooseDataSet):
"""
选择最优特征
@param chooseDataSet: 数据集
@return bestFeature: 最优特征所在列
"""
numFeatures = len(chooseDataSet[0]) - 1 # 特征总数
if numFeatures == 1: # 当只有一个特征时
return 0
bestGini = 1 # 最佳基尼系数
bestFeature = -1 # 最优特征
for i in range(numFeatures):
uniqueVales = set(example[i] for example in chooseDataSet) # 去重,每个属性值唯一
feaGini = 0 # 定义特征的值的基尼系数
# 依次计算每个特征的值的熵
for value in uniqueVales:
subDataSet = splitDataSet(chooseDataSet, i, value) # 根据该特征属性值分的类
# 参数:原数据、循环次数(当前属性值所在列)、当前属性值
prob = len(subDataSet) / float(len(chooseDataSet))
probabilityEnt = calcProbabilityEnt(subDataSet)
feaGini += prob * (2 * probabilityEnt * (1 - probabilityEnt))
if feaGini < bestGini: # 基尼系数越小越好
bestGini = feaGini
bestFeature = i
return bestFeature
def majorityCnt(classList):
"""
对最后一个特征分类,出现次数最多的类即为该属性类别,比如:最后分类为2男1女,则判定为男
@param classList: 数据集,也是类别集
@return sortedClassCount[0][0]: 该属性的类别
"""
classCount = {}
# 计算每个类别出现次数
for vote in classList:
try:
classCount[vote] += 1
except KeyError:
classCount[vote] = 1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True) # 出现次数最多的类别在首位
# 对第1个参数,按照参数的第1个域来进行排序(第2个参数),然后反序(第3个参数)
return sortedClassCount[0][0] # 该属性的类别
def createTree(treeDataSet, treeLabels):
"""
对最后一个特征分类,按分类后类别数量排序,比如:最后分类为2同意1不同意,则判定为同意
@param treeDataSet: 数据集
@param treeLabels: 特征集
@return myTree: 决策树
"""
classList = [example[-1] for example in treeDataSet] # 获取每行数据的最后一个值,即每行数据的类别
# 当数据集只有一个类别
if classList.count(classList[0]) == len(classList):
return classList[0]
# 当数据集只剩一列(即类别),即根据最后一个特征分类
if len(treeDataSet[0]) == 1:
return majorityCnt(classList)
# 其他情况
bestFeat = chooseBestFeatureToSplit(treeDataSet) # 选择最优特征(所在列)
bestFeatLabel = treeLabels[bestFeat] # 最优特征
del (treeLabels[bestFeat]) # 从特征集中删除当前最优特征
uniqueVales = set(example[bestFeat] for example in treeDataSet) # 选出最优特征对应属性的唯一值
myTree = {bestFeatLabel: {}} # 分类结果以字典形式保存
for value in uniqueVales:
subLabels = treeLabels[:] # 深拷贝,拷贝后的值与原值无关(普通复制为浅拷贝,对原值或拷贝后的值的改变互相影响)
myTree[bestFeatLabel][value] = createTree(splitDataSet(treeDataSet, bestFeat, value), subLabels) # 递归调用创建决策树
return myTree
if __name__ == '__main__':
dataSet, labels = createDataSet1() # 创造示列数据
print(createTree(dataSet, labels)) # 输出决策树模型结果