1 - 决策树原理
决策树(Decision Tree)是一种基本的分类与回归方法,本文主要讨论分类决策树。决策树模型呈树形结构,在分类问题中,表示基于特征对数据进行分类的过程。它可以认为是if-then规则的集合。每个内部节点表示在属性上的一个测试,每个分支代表一个测试输出,每个叶节点代表一种类别。
决策树的优点:
- 1)可以自学习。在学习过程中不需要使用者了解过多的背景知识,只需要对训练数据进行较好的标注,就能进行学习。
- 2)决策树模型可读性好,具有描述性,有助于人工分析;
- 3)效率高,决策树只需要一次构建,就可以反复使用,每一次预测的最大计算次数不超过决策树的深度。
简单介绍完毕,让我们来通过一个例子让决策树“原形毕露”。
一天,老师问了个问题,只根据头发和声音怎么判断一位同学的性别。
为了解决这个问题,同学们马上简单的统计了7位同学的相关特征,数据如下:
头发 | 声音 | 性别 |
---|---|---|
长 | 粗 | 男 |
短 | 粗 | 男 |
短 | 粗 | 男 |
长 | 细 | 女 |
短 | 细 | 女 |
短 | 粗 | 女 |
长 | 粗 | 女 |
长 | 粗 | 女 |
机智的同学A想了想,先根据头发判断,若判断不出,再根据声音判断,于是画了一幅图,如下:
于是,一个简单、直观的决策树就这么出来了。头发长、声音粗就是男生;头发长、声音细就是女生;头发短、声音粗是男生;头发短、声音细是女生。
原来机器学习中决策树就这玩意,这也太简单了吧。。。
这时又蹦出个同学B,想先根据声音判断,然后再根据头发来判断,如是大手一挥也画了个决策树:
同学B的决策树:首先判断声音,声音细,就是女生;声音粗、头发长是男生;声音粗、头发长是女生。
那么问题来了:同学A和同学B谁的决策树好些?计算机做决策树的时候,面对多个特征,该如何选哪个特征为最佳的划分特征?
如何评估分裂点的好坏?如果一个分裂点可以将当前的所有节点分为两类,使得每一类都很“纯”,也就是同一类的记录较多,那么就是一个好分裂点。一般而言,随着划分不断进行,我们希望决策树的分支节点所包含的样本尽可能属于同一类别,即节点的“纯度”(purity)越来越高
具体实践中,到底选择哪个特征作为当前分裂特征,常用的有下面三种算法:
- ID3:使用信息增益g(D,A)进行特征选择
- C4.5:信息增益率 =g(D,A)/H(A)
- CART:基尼系数
一个特征的信息增益(或信息增益率,或基尼系数)越大,表明特征对样本的熵的减少能力更强,这个特征使得数据由不确定性到确定性的能力越强。
2 - 使用ID3算法构建决策树
ID3算法是通过信息增益来判断选取哪个特征进行决策的,要了解信息增益这个概念,我们需要先知道什么是“信息熵”
2.1 - 信息熵
“信息熵”(information entropy)是度量样本集合纯度最常用的一种指标,假定当前样本集合
D
D
D中第
k
k
k类样本所占的比例为
p
k
(
k
=
1
,
2
,
…
,
∣
y
∣
)
p_k(k=1,2,\dots,|y|)
pk(k=1,2,…,∣y∣),则
D
D
D的信息熵定义为:
E
n
t
(
D
)
=
−
∑
k
=
1
∣
y
∣
p
k
l
o
g
2
p
k
Ent(D)=-\sum_{k=1}^{|y|}p_klog_2p_k
Ent(D)=−k=1∑∣y∣pklog2pk
E n t ( D ) Ent(D) Ent(D)的值越小,则D的纯度越高
那么这个式子 E n t ( D ) = − ∑ k = 1 ∣ y ∣ p k l o g 2 p k Ent(D)=-\sum_{k=1}^{|y|}p_klog_2p_k Ent(D)=−∑k=1∣y∣pklog2pk又是怎么来的呢?
实际上,熵的概念首先在热力学中引入,用于表述热力学第二定律。波尔兹曼研究得到,热力学熵与微观状态数目的对数之间存在联系,并给出了公式:
S
=
k
l
n
W
S = klnW
S=klnW
信息熵的定义与上述这个热力学的熵,虽然不是一个东西,但是有一定的联系。熵在信息论中代表随机变量不确定度的度量。一个离散型随机变量 X X X的熵 H ( X ) H(X) H(X)定义为:
H
(
X
)
=
−
∑
x
∈
X
p
(
x
)
l
o
g
p
(
x
)
H(X)=-\sum_{x\in {X}}p(x)logp(x)
H(X)=−x∈X∑p(x)logp(x)
这个定义的特点是,有明确定义的科学名词且与内容无关,而且不随信息的具体表达式的变化而变化。是独立于形式,反映了信息表达式中统计方面的性质。是统计学上的抽象概念。
所以这个定义如题主提到的可能有点抽象和晦涩,不易理解。那么下面让我们从直觉出发,以生活中的一些例子来阐述信息熵是什么,以及有什么用处。
直觉上,信息量等于传输该信息所用的代价,这个也是通信中考虑最多的问题。比如说:赌马比赛里,有4匹马 A , B , C , D {A,B,C,D} A,B,C,D,获胜概率分别为 1 2 , 1 4 , 1 8 , 1 8 {\frac{1}{2},\frac{1}{4},\frac{1}{8},\frac{1}{8}} 21,41,81,81
接下来,让我们将哪一匹马获胜视为一个随机变量 X ∈ A , B , C , D X\in {A,B,C,D} X∈A,B,C,D假定我们需要用尽可能少的二元问题来确定随机变量 X X X的取值。
例如:问题1:A获胜了吗?问题2:B获胜了吗?问题3:C获胜了吗?最后我们可以通过最多3个二元问题,来确定 X X X的取值,即哪一匹马赢了比赛。
如果 X = A X=A X=A,那么需要问1次(问题1:是不是A?)概率为 1 2 \frac{1}{2} 21
如果 X = B X=B X=B,那么需要问2次(问题1:是不是A?问题2:是不是B?)概率为 1 4 \frac{1}{4} 41
如果 X = C X=C X=C,那么需要问3次(问题1,问题2,问题3)概率为 1 8 \frac{1}{8} 81
如果 X = D X=D X=D,那么需要问3次(问题1,问题2,问题3)概率为 1 8 \frac{1}{8} 81
那么很容易计算,在这种问法下,为确定
X
X
X取值的二元问题数量为:
E
(
N
)
=
1
2
⋅
1
+
1
4
⋅
2
+
1
8
⋅
3
+
1
8
⋅
3
=
7
4
E(N)=\frac{1}{2}\cdot 1+\frac{1}{4}\cdot 2+\frac{1}{8}\cdot 3+\frac{1}{8}\cdot 3=\frac{7}{4}
E(N)=21⋅1+41⋅2+81⋅3+81⋅3=47
那么我们回到信息熵的定义,会发现通过之前的信息熵公式,神奇地得到了:
H
(
X
)
=
1
2
l
o
g
2
(
2
)
+
1
4
l
o
g
2
(
4
)
+
1
8
l
o
g
2
(
8
)
+
1
8
l
o
g
2
(
8
)
=
7
4
H(X)=\frac{1}{2}log_2(2)+\frac{1}{4}log_2(4)+\frac{1}{8}log_2(8)+\frac{1}{8}log_2(8)=\frac{7}{4}
H(X)=21log2(2)+41log2(4)+81log2(8)+81log2(8)=47
从而验证了上式(当然这只是便于理解而不是推导过程)。
2.2 - 信息增益
我们计算出
D
v
D^v
Dv的信息熵,再考虑到不同的分支结点所包含的样本数不同,给分支结点赋予权重
∣
D
V
∣
/
∣
D
∣
|D^V|/|D|
∣DV∣/∣D∣,即样本数越多的分支结点的影响越大,于是可计算“信息增益”(information gain)
G
a
i
n
(
D
,
a
)
=
E
n
t
(
D
)
−
∑
v
=
1
V
∣
D
v
∣
∣
D
∣
E
n
t
(
D
v
)
Gain(D,a)=Ent(D)-\sum_{v=1}^V\frac{|D^v|}{|D|}Ent(D^v)
Gain(D,a)=Ent(D)−v=1∑V∣D∣∣Dv∣Ent(Dv)
一般而言,信息增益越大,则意味着使用属性来进行划分所得的“纯度提升”越大,因此,我们可用信息增益进行决策树的划分属性选择。
以上述为例:
首先计算未分类前的熵,总共有8位同学,男生3位,女生5位。
E n t ( 总 ) = − 3 8 ⋅ l o g 2 ( 3 8 ) − 5 8 ⋅ l o g 2 ( 5 8 ) = 0.9544 Ent(总)=-\frac{3}{8}\cdot log_2(\frac{3}{8})-\frac{5}{8}\cdot log_2(\frac{5}{8})=0.9544 Ent(总)=−83⋅log2(83)−85⋅log2(85)=0.9544
接着分别计算同学A和同学B分类后信息熵。
同学A首先按头发分类,分类结果为:长头发中有1男3女,短头发中有2男2女
D
=
(
所
有
样
例
)
D=(所有样例)
D=(所有样例)
D
1
=
(
长
发
)
D^1=(长发)
D1=(长发)
D
2
=
(
短
发
)
D^2=(短发)
D2=(短发)
E
n
t
(
D
1
)
=
−
1
4
⋅
l
o
g
2
(
1
4
)
−
3
4
⋅
l
o
g
2
(
3
4
)
=
0.8113
Ent(D^1)=-\frac{1}{4}\cdot log_2(\frac{1}{4})-\frac{3}{4}\cdot log_2(\frac{3}{4})=0.8113
Ent(D1)=−41⋅log2(41)−43⋅log2(43)=0.8113
E
n
t
(
D
2
)
=
−
2
4
⋅
l
o
g
2
(
2
4
)
−
2
4
⋅
l
o
g
2
(
2
4
)
=
1
Ent(D^2)=-\frac{2}{4}\cdot log_2(\frac{2}{4})-\frac{2}{4}\cdot log_2(\frac{2}{4})=1
Ent(D2)=−42⋅log2(42)−42⋅log2(42)=1
∑
v
=
1
2
∣
D
v
∣
∣
D
∣
E
n
t
(
D
v
)
=
4
8
⋅
0.08113
+
4
8
⋅
1
=
0.9057
\sum_{v=1}^2\frac{|D^v|}{|D|}Ent(D^v)= \frac{4}{8}\cdot 0.08113+\frac{4}{8}\cdot 1 = 0.9057
v=1∑2∣D∣∣Dv∣Ent(Dv)=84⋅0.08113+84⋅1=0.9057
G
a
i
n
(
D
,
头
发
)
=
E
n
t
(
总
)
−
∑
v
=
1
2
∣
D
v
∣
∣
D
∣
E
n
t
(
D
v
)
=
0.9544
−
0.9057
=
0.0487
Gain(D,头发)=Ent(总)-\sum_{v=1}^2\frac{|D^v|}{|D|}Ent(D^v)=0.9544-0.9057=0.0487
Gain(D,头发)=Ent(总)−v=1∑2∣D∣∣Dv∣Ent(Dv)=0.9544−0.9057=0.0487
同理,按同学B的方法,首先按声音特征来分,分类后的结果为:声音粗中有3男3女,声音细中有0男2女
D
1
=
(
声
音
粗
)
D^1=(声音粗)
D1=(声音粗)
D
2
=
(
声
音
细
)
D^2=(声音细)
D2=(声音细)
E
n
t
(
D
1
)
=
−
3
6
⋅
l
o
g
2
(
3
6
)
−
3
6
⋅
l
o
g
2
(
3
6
)
=
1
Ent(D^1)=-\frac{3}{6}\cdot log_2(\frac{3}{6})-\frac{3}{6}\cdot log_2(\frac{3}{6})=1
Ent(D1)=−63⋅log2(63)−63⋅log2(63)=1
E
n
t
(
D
2
)
=
−
2
2
⋅
l
o
g
2
(
2
2
)
=
0
Ent(D^2)=-\frac{2}{2}\cdot log_2(\frac{2}{2})=0
Ent(D2)=−22⋅log2(22)=0
∑
v
=
1
2
∣
D
v
∣
∣
D
∣
E
n
t
(
D
v
)
=
6
8
⋅
1
+
2
8
⋅
0
=
0.75
\sum_{v=1}^2\frac{|D^v|}{|D|}Ent(D^v)= \frac{6}{8}\cdot 1+\frac{2}{8}\cdot 0=0.75
v=1∑2∣D∣∣Dv∣Ent(Dv)=86⋅1+82⋅0=0.75
G
a
i
n
(
D
,
声
音
)
=
E
n
t
(
总
)
−
∑
v
=
1
2
∣
D
v
∣
∣
D
∣
E
n
t
(
D
v
)
=
0.9544
−
0.75
=
0.2087
Gain(D,声音)=Ent(总)-\sum_{v=1}^2\frac{|D^v|}{|D|}Ent(D^v)=0.9544-0.75=0.2087
Gain(D,声音)=Ent(总)−v=1∑2∣D∣∣Dv∣Ent(Dv)=0.9544−0.75=0.2087
按同学B的方法,先按声音特征分类,信息增益更大,区分样本的能力更强,更具有代表性。
以上就是决策树ID3算法的核心思想。
2.3 - 接下来用python代码来实现ID3算法:
#构建数据集
from math import log
import operator
def createDataSet1(): # 创造示例数据
dataSet = [['长', '粗', '男'],
['短', '粗', '男'],
['短', '粗', '男'],
['长', '细', '女'],
['短', '细', '女'],
['短', '粗', '女'],
['长', '粗', '女'],
['长', '粗', '女']]
labels = ['头发','声音'] #两个特征
return dataSet,labels
#计算数据信息熵
def calcShannonEnt(dataSet): # 计算数据的熵(entropy)
numEntries=len(dataSet) # 数据条数
labelCounts={} #构造一个空的字典类型
for featVec in dataSet: # 统计有多少个类以及每个类的数量,也就是分别统计男女的数量
currentLabel=featVec[-1] # 每行数据的最后一个字(类别)
if currentLabel not in labelCounts.keys(): #如果currentLabel没有在labelCounts中,则在labelCounts创建一个这个类别,并记为0
labelCounts[currentLabel]=0
labelCounts[currentLabel]+=1 #否则,这个类别的数量+1
shannonEnt=0
for key in labelCounts:
prob=float(labelCounts[key])/numEntries # 计算单个类的熵值
shannonEnt-=prob*log(prob,2) # 累加每个类的熵值
return shannonEnt
我们来测试一下,可以看到数据集的信息熵与上文我们手算的结果一致
#测试calcShannonEnt函数
dataSet,labels=createDataSet1()
shannonEnt=calcShannonEnt(dataSet)
print(shannonEnt)
0.9544340029249649
#定义按照某个特征进行划分的函数splitDataSet
#输入三个变量(待划分的数据集,特征,分类值)
def splitDataSet(dataSet,axis,value):
retDataSet=[] #创建一个空List
for featVec in dataSet: #遍历数据集
if featVec[axis]==value:
reducedFeatVec =featVec[:axis] # 取划分特征值前面一部分,但不包括特征值
reducedFeatVec.extend(featVec[axis+1:]) # 取划分特征值后面一部分,但不包括特征值
"""
以上两行获得的数据集将不包括特征值的记录,
以达到每一次划分都将大数据集划分为小数据集的目的
"""
# 以每条记录为单位追加到空列表中
retDataSet.append(reducedFeatVec)
return retDataSet
我们来测试一下,可以看到按照所选的特征划分数据集
#测试splitDataSet函数
retDataSet=splitDataSet(dataSet,0,"长")
print(retDataSet)
[[‘粗’, ‘男’], [‘细’, ‘女’], [‘粗’, ‘女’], [‘粗’, ‘女’]]
def chooseBestFeatureToSplit(dataSet): # 选择最优的分类特征
numFeatures = len(dataSet[0])-1
baseEntropy = calcShannonEnt(dataSet) # 原始的熵
bestInfoGain = 0
bestFeature = -1
for i in range(numFeatures):
featList = [example[i] for example in dataSet]
uniqueVals = set(featList)
newEntropy = 0
for value in uniqueVals:
subDataSet = splitDataSet(dataSet,i,value)
prob =len(subDataSet)/float(len(dataSet))
newEntropy +=prob*calcShannonEnt(subDataSet) # 按特征分类后的熵
infoGain = baseEntropy - newEntropy # 原始熵与按特征分类后的熵的差值
if (infoGain>bestInfoGain): # 若按某特征划分后,熵值减少的最大,则次特征为最优分类特征
bestInfoGain=infoGain
bestFeature = i
return bestFeature #返回值对应标签取值,若为0则为头发长短,若为1则为声音粗细
测试一下,返回值对应标签取值,若为0则为头发长短,若为1则为声音粗细,可以看到也与我们手动计算信息增益所选出的特征一致
#测试chooseBestFeatureToSplit函数
bestFeature=chooseBestFeatureToSplit(dataSet)
print(labels[bestFeature])
声音
#功能:多数表决决定叶子结点
#使用分类名称的列表,创建键值为classList中唯一值的数据字典,字典对象存储了classList每个类标签出现的频率
#返回:出现次数最多的分类名称
def majorityCnt(classList):
classCount={}
for vote in classList:
if vote not in classCount.keys():
classCount[vote]=0
classCount[vote]+=1
sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
"""
classCount.iteritems()将classCount字典分解为元组列表,
operator.itemgetter(1)按照第二个元素的次序对元组进行排序,
reverse=True是逆序,即按照从大到小的顺序排列
"""
return sortedClassCount[0][0]
我们用初始的数据集进行测试,该函数会返回最多的类标签
#测试majorityCnt
classList=[example[-1] for example in dataSet]
sortedClassCount=majorityCnt(classList)
print(sortedClassCount)
女
#构造决策树,采用递归的方法构建
def createTree(dataSet,labels):
classList=[example[-1] for example in dataSet] # 类别:男或女
if classList.count(classList[0])==len(classList): #当所有的类都相等时停止分裂
return classList[0]
if len(dataSet[0])==1: #当使用完所有的特征进行分类后仍然仍然不能将数据集划分成仅包含唯一类别的分组,则将子叶定为数量最多类
return majorityCnt(classList) # 采用多数多数原则选出分组
bestFeat=chooseBestFeatureToSplit(dataSet) #选择最优特征
bestFeatLabel=labels[bestFeat]
# 这里直接使用字典变量来存储树信息,这对于绘制树形图很重要。
myTree={bestFeatLabel:{}} #分类结果以字典形式保存
del(labels[bestFeat]) ## 删除已经在选取的特征
featValues=[example[bestFeat] for example in dataSet]
uniqueVals=set(featValues)
for value in uniqueVals:
subLabels=labels[:] # 复制所有标签,这样树就不会破坏现有的标签
myTree[bestFeatLabel][value]=createTree(splitDataSet\
(dataSet,bestFeat,value),subLabels)
return myTree
通过递归的方法构造决策树,最后得出一个由ID3算法选取分类特征的已分好类的树形结构(字典型数据)
#测试createTree
createTree(dataSet,labels)
{‘声音’: {‘粗’: {‘头发’: {‘短’: ‘男’, ‘长’: ‘女’}}, ‘细’: ‘女’}}
3 - 使用C4.5算法构建决策树:
实际上,ID3算法对可取值数目较多的属性有所偏好,为减少这种偏好可能带来的不利影响,著名的C4.5决策树算法不直接使用信息增益,而是使用“增益率”(gain ratio)来选择最优划分属性。那么什么是增益率呢?
3.1 - 增益率
增益率定义为:
G
a
i
n
r
a
t
i
o
(
D
,
a
)
=
G
a
i
n
(
D
,
a
)
I
V
(
a
)
Gain_ratio(D,a)=\frac{Gain(D,a)}{IV(a)}
Gainratio(D,a)=IV(a)Gain(D,a)
其中:
I
V
(
a
)
=
−
∑
v
=
1
V
∣
D
V
∣
∣
D
∣
l
o
g
2
(
∣
D
V
∣
∣
D
∣
)
IV(a)=-\sum_{v=1}^V\frac{|D^V|}{|D|}log_2(\frac{|D^V|}{|D|})
IV(a)=−v=1∑V∣D∣∣DV∣log2(∣D∣∣DV∣)
成为属性a的“固有值”,属性a的可能取值数目越多(即V越大),则IV(a)的值通常会越大,从而改善了由于取值数目较多属性的影响。
同学A首先按头发分类,分类结果为:长头发中有1男3女,短头发中有2男2女
D
=
(
所
有
样
例
)
D=(所有样例)
D=(所有样例)
D
1
=
(
长
发
)
D^1=(长发)
D1=(长发)
D
2
=
(
短
发
)
D^2=(短发)
D2=(短发)
I
V
(
a
)
=
−
∑
v
=
1
V
∣
D
v
∣
D
l
o
g
2
∣
D
v
∣
D
=
−
4
8
l
o
g
2
4
8
−
4
8
l
o
g
2
4
8
=
1
IV(a)=-\sum_{v=1}^V\frac{|D^v|}{D}log_2\frac{|D^v|}{D}=-\frac{4}{8}log_2\frac{4}{8}-\frac{4}{8}log_2\frac{4}{8}=1
IV(a)=−v=1∑VD∣Dv∣log2D∣Dv∣=−84log284−84log284=1
G
a
i
n
r
a
t
i
o
(
D
,
a
)
=
G
a
i
n
(
D
,
a
)
I
V
(
a
)
=
0.0487
/
1
=
0.0487
Gain_ratio(D,a)=\frac{Gain(D,a)}{IV(a)}=0.0487/1=0.0487
Gainratio(D,a)=IV(a)Gain(D,a)=0.0487/1=0.0487
同理,按同学B的方法,首先按声音特征来分,分类后的结果为:声音粗中有3男3女,声音细中有0男2女
D
1
=
(
声
音
粗
)
D^1=(声音粗)
D1=(声音粗)
D
2
=
(
声
音
细
)
D^2=(声音细)
D2=(声音细)
I
V
(
a
)
=
−
∑
v
=
1
V
∣
D
v
∣
D
l
o
g
2
∣
D
v
∣
D
=
−
2
8
l
o
g
2
2
8
−
6
8
l
o
g
2
6
8
=
0.8112
IV(a)=-\sum_{v=1}^V\frac{|D^v|}{D}log_2\frac{|D^v|}{D}=-\frac{2}{8}log_2\frac{2}{8}-\frac{6}{8}log_2\frac{6}{8}=0.8112
IV(a)=−v=1∑VD∣Dv∣log2D∣Dv∣=−82log282−86log286=0.8112
G
a
i
n
r
a
t
i
o
(
D
,
a
)
=
G
a
i
n
(
D
,
a
)
I
V
(
a
)
=
0.2087
/
0.8112
=
0.2572
Gain_ratio(D,a)=\frac{Gain(D,a)}{IV(a)}=0.2087/0.8112=0.2572
Gainratio(D,a)=IV(a)Gain(D,a)=0.2087/0.8112=0.2572
按同学B的方法,先按声音特征分类,增益率更大,区分样本的能力更强,更具有代表性。
3.2 - 使用Python实现C4.5决策树:
C4.5决策树的实现代码和ID3的代码基本一致,只是在选取最优特征的代码中,将信息增益改为了增益率,其他都没有变化。
import math
import operator
#功能:导入数据表
#功能:计算熵
def createDataSet1(): # 创造示例数据
dataSet = [['长', '粗', '男'],
['短', '粗', '男'],
['短', '粗', '男'],
['长', '细', '女'],
['短', '细', '女'],
['短', '粗', '女'],
['长', '粗', '女'],
['长', '粗', '女']]
labels = ['头发','声音'] #两个特征
return dataSet,labels
def calcShannonEnt (dataSet):
num = len(dataSet)#实例的个数
labelCounts = {}#类标签
for featVec in dataSet:
currentLabel = featVec[-1]#最后一列的数值
if currentLabel not in labelCounts.keys():#如果在labelCounts中没出现
labelCounts[currentLabel] = 0#就把currentLabel键加入labelCounts中,值为0
labelCounts[currentLabel] += 1#labelCounts对应的值就加一
#计算香农熵H(D)
ShannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key]) / num#计算p(Xi)概率
ShannonEnt -= prob * math.log(prob, 2)
return ShannonEnt
#功能:按照给定特征划分数据集
#输入:数据集、划分数据集的特征、需要返回的特征的值
#返回:划分后的数据集
def splitDataSet(dataSet, axis, value):
newdataSet = []
for featVec in dataSet:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:])
newdataSet.append(reducedFeatVec)
return newdataSet
#功能:选取最好的数据集划分方式
#返回:最佳特征下标(增益率最大)
def chooseBestFeatureTosplit(dataSet):
numFeatures = len(dataSet[0]) - 1#特征个数
baseEntropy = calcShannonEnt(dataSet)#原始香农熵H(D)
bestInfoGainrate = 0.0; bestFeature = -1#信息增益和最好的特征
#遍历特征
for i in range(numFeatures):
featureSet = set([example[i] for example in dataSet])#第i个特征取值集合
newEntropy = 0.0
splitinfo = 0.0
for value in featureSet:
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)#经验条件熵H(D|A)
splitinfo -= prob * math.log(prob,2)#经验熵HA(D)
#当概率为1或者0时(因为经验熵要做被除数):
if not splitinfo:
splitinfo = -0.99 * math.log(0.99,2) - 0.01 * math.log(0.01,2)
infoGain = baseEntropy - newEntropy#信息增益=H(D)-H(D|A)
infoGainrate = float(infoGain) / float(splitinfo)#增益率=信息增益/经验熵
if infoGainrate > bestInfoGainrate:
bestInfoGainrate = infoGainrate
bestFeature = i
return bestFeature
#功能:多数表决决定叶子结点
#使用分类名称的列表,创建键值为classList中唯一值的数据字典,字典对象存储了classList每个类标签出现的频率
#返回:出现次数最多的分类名称
def majorityCnt(classList): #按分类后类别数量排序,比如:最后分类为2男1女,则判定为男;
classCount={}
for vote in classList:
if vote not in classCount.keys():
classCount[vote]=0
classCount[vote]+=1
sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
return sortedClassCount[0][0]
#功能:创建树
#输入数据集和类标签
#返回字典树
def createTree(dataSet, labels):
classList = [example[-1] for example in dataSet]#数据集的所有类标签
if classList.count(classList[0]) == len(classList):#停止条件是所有的类标签完全相同
return classList[0]
if len(dataSet[0]) == 1:#当使用完了所有特征,还不能将数据集划分成仅包含唯一类别的分组
return majorityCnt(classList)#挑选出出现次数最多的类别作为返回值
#开始创建树
bestFeat = chooseBestFeatureTosplit(dataSet)#当前数据集选取的最好特征
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel:{}}
#得到列表包含的所有属性值
#赋值当前特征标签列表,防止改变原始列表的内容
subLabels = labels[:]
#删除属性列表中当前分类数据集特征
del(subLabels[bestFeat])
#获取数据集中最优特征所在列
featValues = [example[bestFeat] for example in dataSet]
#采用set集合性质,获取特征的所有的唯一取值
uniqueVals = set(featValues)
for value in uniqueVals:
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
return myTree
if __name__=='__main__':
dataSet, labels=createDataSet1() # 创造示列数据
print(createTree(dataSet, labels)) # 输出决策树模型结果
{‘声音’: {‘粗’: {‘头发’: {‘短’: ‘男’, ‘长’: ‘女’}}, ‘细’: ‘女’}}
4 - CART决策树
CART决策树使用“基尼指数”来选择划分属性,所以我们先来介绍一下基尼指数
4 - 1 基尼指数
基尼指数由基尼值构成,而基尼值定义为:
G
i
n
i
(
D
)
=
1
−
∑
k
=
1
∣
y
∣
p
k
2
Gini(D)=1-\sum_{k=1}^{|y|}p_k^2
Gini(D)=1−k=1∑∣y∣pk2
直观来说, G i n i ( D ) Gini(D) Gini(D)反映了从数据集D中随机抽取两个样本,其类别标记不一致的概率,因此, G i n i ( D ) Gini(D) Gini(D)越小,则数据集 D D D的纯度越高。
则属性a的基尼指数定义为:
G i n i − i n d e x ( D , a ) = ∑ v = 1 V ∣ D v ∣ ∣ D ∣ G i n i ( D v ) Gini_{-}index(D,a)=\sum_{v=1}^V\frac{|D^v|}{|D|}Gini(D^v) Gini−index(D,a)=v=1∑V∣D∣∣Dv∣Gini(Dv)
于是,我们在候选属性集合A中,选择哪个使用划分后基尼指数最小的属性作为划分属性。
同学A首先按头发分类,分类结果为:长头发中有1男3女,短头发中有2男2女
D
=
(
所
有
样
例
)
D=(所有样例)
D=(所有样例)
D
1
=
(
长
发
)
D^1=(长发)
D1=(长发)
D
2
=
(
短
发
)
D^2=(短发)
D2=(短发)
G i n i ( D 1 ) = 1 − ( 1 4 ) 2 − ( 3 4 ) 2 = 0.375 Gini(D^1)=1-(\frac{1}{4})^2-(\frac{3}{4})^2=0.375 Gini(D1)=1−(41)2−(43)2=0.375
G i n i ( D 2 ) = 1 − ( 2 4 ) 2 − ( 2 4 ) 2 = 0.5 Gini(D^2)=1-(\frac{2}{4})^2-(\frac{2}{4})^2=0.5 Gini(D2)=1−(42)2−(42)2=0.5
G i n i − g a i n ( 头 发 ) = 4 8 G i n i ( D 1 ) + 4 8 G i n i ( D 2 ) = 0.435 Gini_{-}gain(头发)=\frac{4}{8}Gini(D^1)+\frac{4}{8}Gini(D^2)=0.435 Gini−gain(头发)=84Gini(D1)+84Gini(D2)=0.435
同理,按同学B的方法,首先按声音特征来分,分类后的结果为:声音粗中有3男3女,声音细中有0男2女
D
1
=
(
声
音
粗
)
D^1=(声音粗)
D1=(声音粗)
D
2
=
(
声
音
细
)
D^2=(声音细)
D2=(声音细)
G i n i ( D 1 ) = 1 − ( 3 6 ) 2 − ( 3 6 ) 2 = 0.5 Gini(D^1)=1-(\frac{3}{6})^2-(\frac{3}{6})^2=0.5 Gini(D1)=1−(63)2−(63)2=0.5
G i n i ( D 2 ) = 1 − ( 0 2 ) 2 − ( 2 2 ) 2 = 0 Gini(D^2)=1-(\frac{0}{2})^2-(\frac{2}{2})^2=0 Gini(D2)=1−(20)2−(22)2=0
G i n i − g a i n ( 声 音 ) = 6 8 G i n i ( D 1 ) + 2 8 G i n i ( D 2 ) = 0.375 Gini_{-}gain(声音)=\frac{6}{8}Gini(D^1)+\frac{2}{8}Gini(D^2)=0.375 Gini−gain(声音)=86Gini(D1)+82Gini(D2)=0.375
按同学B的方法,先按声音特征分类,基尼指数更小,区分样本的能力更强,更具有代表性。
4.2 - Python实现CART决策树
CART决策树的代码也与ID3和C4.5算法类似,主要区别在于选取最优节点的函数chooseBestFeatureTosplit算法不同
import math
import operator
#功能:导入数据表
#功能:计算熵
def createDataSet1(): # 创造示例数据
dataSet = [['长', '粗', '男'],
['短', '粗', '男'],
['短', '粗', '男'],
['长', '细', '女'],
['短', '细', '女'],
['短', '粗', '女'],
['长', '粗', '女'],
['长', '粗', '女']]
labels = ['头发','声音'] #两个特征
return dataSet,labels
def calcShannonEnt (dataSet):
num = len(dataSet)#实例的个数
labelCounts = {}#类标签
for featVec in dataSet:
currentLabel = featVec[-1]#最后一列的数值
if currentLabel not in labelCounts.keys():#如果在labelCounts中没出现
labelCounts[currentLabel] = 0#就把currentLabel键加入labelCounts中,值为0
labelCounts[currentLabel] += 1#labelCounts对应的值就加一
#计算香农熵H(D)
ShannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key]) / num#计算p(Xi)概率
ShannonEnt -= prob * math.log(prob, 2)
return ShannonEnt
#功能:按照给定特征划分数据集
#输入:数据集、划分数据集的特征、需要返回的特征的值
#返回:划分后的数据集
def splitDataSet(dataSet, axis, value):
newdataSet = []
for featVec in dataSet:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:])
newdataSet.append(reducedFeatVec)
return newdataSet
#功能:选取最好的数据集划分方式
#返回:最佳特征下标(基尼指数最小)
def chooseBestFeatureTosplit(dataSet):
"""
输入:数据集
输出:最好的划分维度
描述:选择最好的数据集划分维度
"""
numFeatures = len(dataSet[0]) - 1
bestGini = 999999.0
bestFeature = -1
for i in range(numFeatures):
featList = [example[i] for example in dataSet]
uniqueVals = set(featList)
gini = 0.0
for value in uniqueVals:
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet)/float(len(dataSet))
subProb = len(splitDataSet(subDataSet, -1, '男')) / float(len(subDataSet))
gini += prob * (1.0 - pow(subProb, 2) - pow(1 - subProb, 2))
if (gini < bestGini):
bestGini = gini
bestFeature = i
return bestFeature
#功能:多数表决决定叶子结点
#使用分类名称的列表,创建键值为classList中唯一值的数据字典,字典对象存储了classList每个类标签出现的频率
#返回:出现次数最多的分类名称
def majorityCnt(classList): #按分类后类别数量排序,比如:最后分类为2男1女,则判定为男;
classCount={}
for vote in classList:
if vote not in classCount.keys():
classCount[vote]=0
classCount[vote]+=1
sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
return sortedClassCount[0][0]
#功能:创建树
#输入数据集和类标签
#返回字典树
def createTree(dataSet, labels):
classList = [example[-1] for example in dataSet]#数据集的所有类标签
if classList.count(classList[0]) == len(classList):#停止条件是所有的类标签完全相同
return classList[0]
if len(dataSet[0]) == 1:#当使用完了所有特征,还不能将数据集划分成仅包含唯一类别的分组
return majorityCnt(classList)#挑选出出现次数最多的类别作为返回值
#开始创建树
bestFeat = chooseBestFeatureTosplit(dataSet)#当前数据集选取的最好特征
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel:{}}
#得到列表包含的所有属性值
#赋值当前特征标签列表,防止改变原始列表的内容
subLabels = labels[:]
#删除属性列表中当前分类数据集特征
del(subLabels[bestFeat])
#获取数据集中最优特征所在列
featValues = [example[bestFeat] for example in dataSet]
#采用set集合性质,获取特征的所有的唯一取值
uniqueVals = set(featValues)
for value in uniqueVals:
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
return myTree
if __name__=='__main__':
dataSet, labels=createDataSet1() # 创造示列数据
print(createTree(dataSet, labels)) # 输出决策树模型结果
{‘声音’: {‘粗’: {‘头发’: {‘长’: ‘女’, ‘短’: ‘男’}}, ‘细’: ‘女’}}