概述
决策树decision tree是功能强大且相当受欢迎的分类和预测方法。他是基于特征对实例进行分类,他是一种树状结构。
本章中,训练集用
D
D
D,
T
T
T表示一颗决策树。
##决策树
决策树原理
决策树是一个贪心算法,即在特性空间上执行递归的二元分割,决策树有节点和有向边组成。内部节点表示一个特征或者属性;叶子节点表示一个分类。
决策树可以表示给定特征条件下,类别的条件概率分布。将特征空间划分成互不相交的单元
S
1
,
S
2
,
.
.
.
,
S
m
S_1,S_2,...,S_m
S1,S2,...,Sm。设某个单元
S
i
S_i
Si内部有
N
i
N_i
Ni个样本点。他定义一个条件概率分布
P
(
y
=
c
k
/
X
)
,
X
∈
S
i
,
c
k
,
k
=
1
,
2
,
.
.
.
,
K
P(y=c_k/X),X\in S_i, c_k,k=1,2,...,K
P(y=ck/X),X∈Si,ck,k=1,2,...,K为第
k
k
k个分类。
- 每个单元对应于决策树的一条路径
- 所有单元的条件概率分布构成了决策树所代表的条件概率分布
- 在单元 S i S_i Si内部有 N i N_i Ni个样本,但是整个单元都属于类 c k ^ \hat{c_k} ck^。其中 c k ^ = a r g c k m a x P ( y = c k / X ) , X ∈ S i \hat{c_k} = arg_ {c_k} \quad maxP(y=c_k/X),X\in S_i ck^=argckmaxP(y=ck/X),X∈Si。即单元 S i S_i Si内部的 N i N_i Ni个样本点,那个分类占优,则那个单元都属于那个分类。
构建决策树3个步骤
- 特征选择
- 决策树生成
- 决策树剪枝
假定给定训练集
D
=
{
(
x
⃗
1
,
y
1
)
,
(
x
⃗
2
,
y
2
)
,
.
.
.
,
(
x
⃗
N
,
y
N
)
}
D = \{(\vec x_1,y_1),(\vec x_2,y_2),...,(\vec x_N,y_N)\}
D={(x1,y1),(x2,y2),...,(xN,yN)},其中
x
⃗
i
=
(
x
i
(
1
)
,
x
i
(
2
)
,
.
.
.
,
x
i
(
n
)
)
\vec x_i = (x_i^{(1)},x_i^{(2)},...,x_i^{(n)})
xi=(xi(1),xi(2),...,xi(n))为输入实例,n为特征个数;
y
i
∈
{
1
,
2
,
.
.
.
K
}
y_i \in \{1,2,...K\}
yi∈{1,2,...K}为类标记,
i
=
1
,
2
,
.
.
.
N
;
N
i=1,2,...N;N
i=1,2,...N;N为样本容量。构建决策树的目标是,根据给定的训练数据集学习一个决策树模型。
构建决策树时通常是将正则化的极大似然函数作为损失函数,其学习的目标是损失函数为目标函数的最小化。构建决策树的算法通常是递归的选择最优特征,并根据该特征分割。步骤如下:
- 建立根节点,
- 选择一个最优特征,通过该特征将训练数据分割成子集,确保每个子集能够有最好的分类,但要考虑下列两种情况。
- 如果子集很好的分类,建立叶子节点
- 如果子集不能很好的分类,对子集继续划分
- 递归直到所有的训练样本都很好的分类,或者没有合适的特征为止
特征选择
就是选择较强分类能力的特征,分类能力通过信息增益和信息增益比来刻画
先给熵的定义。设
X
X
X是一个离散型的随机变量,其概率分布为:
P
(
X
=
x
⃗
i
)
=
p
i
,
i
=
1
,
2
,
.
.
.
,
n
P(X= \vec x _i) = pi, i=1,2,...,n
P(X=xi)=pi,i=1,2,...,n
随机变量的熵为:
H
(
X
)
=
−
∑
i
=
1
n
p
i
l
o
g
p
i
H(X) = -\sum_{i=1}^n p_i log{p_i}
H(X)=−i=1∑npilogpi
直观的理解:如果x属于某个分类的值越大(即数据越有序),H的值越小;极端情况下,p(xi)=1时,H=0,此时分类最准确。所以我们要使H的值尽可能小。
- 计算信息熵
def calcShannonEnt(dataSet):
"""calcShannonEnt(calculate Shannon entropy 计算给定数据集的香农熵)
Args: dataSet 数据集
Returns: 返回 每一组feature下的某个分类下,香农熵的信息期望
"""
# -----------计算香农熵的第一种实现方式start--------------------------------------------------------------------------------
# 求list的长度,表示计算参与训练的数据量
numEntries = len(dataSet)
# 下面输出我们测试的数据集的一些信息
# 例如:<type 'list'> numEntries: 5 是下面的代码的输出
# print type(dataSet), 'numEntries: ', numEntries
# 计算分类标签label出现的次数
labelCounts = {}
# the the number of unique elements and their occurance
for featVec in dataSet:
# 将当前实例的标签存储,即每一行数据的最后一个数据代表的是标签
print(featVec)
currentLabel = featVec[-1]
print("currentLabel", currentLabel)
# 为所有可能的分类创建字典,如果当前的键值不存在,则扩展字典并将当前键值加入字典。每个键值都记录了当前类别出现的次数。
if currentLabel not in list(labelCounts.keys()):
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
# print '-----', featVec, labelCounts
print(labelCounts)
# 对于label标签的占比,求出label标签的香农熵
shannonEnt = 0.0
for key in labelCounts:
# 使用所有类标签的发生频率计算类别出现的概率。
prob = float(labelCounts[key])/numEntries
# log base 2
# 计算香农熵,以 2 为底求对数
shannonEnt -= prob * log(prob, 2)
# print '---', prob, prob * log(prob, 2), shannonEnt
# -----------计算香农熵的第一种实现方式end--------------------------------------------------------------------------------
# # -----------计算香农熵的第二种实现方式start--------------------------------------------------------------------------------
# # 统计标签出现的次数
# label_count = Counter(data[-1] for data in dataSet)
# # 计算概率
# probs = [p[1] / len(dataSet) for p in label_count.items()]
# # 计算香农熵
# shannonEnt = sum([-p * log(p, 2) for p in probs])
# # -----------计算香农熵的第二种实现方式end--------------------------------------------------------------------------------
return shannonEnt
- 计算完信息熵后,我们便可以得到数据集的无序程度。我们将对每个特征划分数据集的结果计算一次信息熵,然后判断哪个特征划分数据集是最好的划分方式(根据信息熵判断,信息熵越小,说明划分效果越好)。
按照给定特征划分数据集的代码如下:
def splitDataSet(dataSet, index, value):
"""splitDataSet(通过遍历dataSet数据集,求出index对应的colnum列的值为value的行)
就是依据index列进行分类,如果index列的数据等于 value的时候,就要将 index 划分到我们创建的新的数据集中
Args:
dataSet 数据集 待划分的数据集
index 表示每一行的index列 划分数据集的特征
value 表示index列对应的value值 需要返回的特征的值。
Returns:
index列为value的数据集【该数据集需要排除index列】
"""
# -----------切分数据集的第一种方式 start------------------------------------
retDataSet = []
for featVec in dataSet:
# index列为value的数据集【该数据集需要排除index列】
# 判断index列的值是否为value
if featVec[index] == value:
# chop out index used for splitting
# [:index]表示前index行,即若 index 为2,就是取 featVec 的前 index 行
reducedFeatVec = featVec[:index]
'''
请百度查询一下: extend和append的区别
list.append(object) 向列表中添加一个对象object
list.extend(sequence) 把一个序列seq的内容添加到列表中
1、使用append的时候,是将new_media看作一个对象,整体打包添加到music_media对象中。
2、使用extend的时候,是将new_media看作一个序列,将这个序列和music_media序列合并,并放在其后面。
result = []
result.extend([1,2,3])
print result
result.append([4,5,6])
print result
result.extend([7,8,9])
print result
结果:
[1, 2, 3]
[1, 2, 3, [4, 5, 6]]
[1, 2, 3, [4, 5, 6], 7, 8, 9]
'''
reducedFeatVec.extend(featVec[index+1:])
# [index+1:]表示从跳过 index 的 index+1行,取接下来的数据
# 收集结果值 index列为value的行【该行需要排除index列】
retDataSet.append(reducedFeatVec)
# -----------切分数据集的第一种方式 end------------------------------------
# # -----------切分数据集的第二种方式 start------------------------------------
# retDataSet = [data for data in dataSet for i, v in enumerate(data) if i == axis and v == value]
# # -----------切分数据集的第二种方式 end------------------------------------
return retDataSet
- 选择最好的数据集划分方式,代码如下:
def chooseBestFeatureToSplit(dataSet):
"""chooseBestFeatureToSplit(选择最好的特征)
Args:
dataSet 数据集
Returns:
bestFeature 最优的特征列
"""
# -----------选择最优特征的第一种方式 start------------------------------------
# 求第一行有多少列的 Feature, 最后一列是label列嘛
numFeatures = len(dataSet[0]) - 1
# label的信息熵
baseEntropy = calcShannonEnt(dataSet)
# 最优的信息增益值, 和最优的Featurn编号
bestInfoGain, bestFeature = 0.0, -1
# iterate over all the features
for i in range(numFeatures):
# create a list of all the examples of this feature
# 获取每一个实例的第i+1个feature,组成list集合
featList = [example[i] for example in dataSet]
# get a set of unique values
# 获取剔重后的集合,使用set对list数据进行去重
uniqueVals = set(featList)
# 创建一个临时的信息熵
newEntropy = 0.0
# 遍历某一列的value集合,计算该列的信息熵
# 遍历当前特征中的所有唯一属性值,对每个唯一属性值划分一次数据集,计算数据集的新熵值,并对所有唯一特征值得到的熵求和。
for value in uniqueVals:
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
# gain[信息增益]: 划分数据集前后的信息变化, 获取信息熵最大的值
# 信息增益是熵的减少或者是数据无序度的减少。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。
infoGain = baseEntropy - newEntropy
print(('infoGain=', infoGain, 'bestFeature=', i, baseEntropy, newEntropy))
if (infoGain > bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
return bestFeature
# -----------选择最优特征的第一种方式 start------------------------------------
# # -----------选择最优特征的第二种方式 start------------------------------------
# # 计算初始香农熵
# base_entropy = calcShannonEnt(dataSet)
# best_info_gain = 0
# best_feature = -1
# # 遍历每一个特征
# for i in range(len(dataSet[0]) - 1):
# # 对当前特征进行统计
# feature_count = Counter([data[i] for data in dataSet])
# # 计算分割后的香农熵
# new_entropy = sum(feature[1] / float(len(dataSet)) * calcShannonEnt(splitDataSet(dataSet, i, feature[0])) \
# for feature in feature_count.items())
# # 更新值
# info_gain = base_entropy - new_entropy
# print('No. {0} feature info gain is {1:.3f}'.format(i, info_gain))
# if info_gain > best_info_gain:
# best_info_gain = info_gain
# best_feature = i
# return best_feature
# # -----------选择最优特征的第二种方式 end------------------------------------
- 选择好最好的划分特征后,接下来,可以开始创建决策树了。其工作原理如下:得到原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分后,数据将被向下传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据。因此我们可以使用递归的原则处理数据集。递归结束的条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。
具体实现代码如下:
def createTree(dataSet, labels):
classList = [example[-1] for example in dataSet]
# 如果数据集的最后一列的第一个值出现的次数=整个集合的数量,也就说只有一个类别,就只直接返回结果就行
# 第一个停止条件:所有的类标签完全相同,则直接返回该类标签。
# count() 函数是统计括号中的值在list中出现的次数
if classList.count(classList[0]) == len(classList):
return classList[0]
# 如果数据集只有1列,那么最初出现label次数最多的一类,作为结果
# 第二个停止条件:使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组。
if len(dataSet[0]) == 1:
return majorityCnt(classList)
# 选择最优的列,得到最优列对应的label含义
bestFeat = chooseBestFeatureToSplit(dataSet)
# 获取label的名称
bestFeatLabel = labels[bestFeat]
# 初始化myTree
myTree = {bestFeatLabel: {}}
# 注:labels列表是可变对象,在PYTHON函数中作为参数时传址引用,能够被全局修改
# 所以这行代码导致函数外的同名变量被删除了元素,造成例句无法执行,提示'no surfacing' is not in list
del(labels[bestFeat])
# 取出最优列,然后它的branch做分类
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
# 求出剩余的标签label
subLabels = labels[:]
# 遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createTree()
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
# print 'myTree', value, myTree
return myTree
- 依靠训练数据构造了决策树之后,我们可以将它用于实际数据的分类。在执行数据分类时,需要使用决策树以及用于构造树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行该过程直到进入叶子节点;最后将测试数据定义为叶子节点所属的类型。
def classify(inputTree, featLabels, testVec):
"""classify(给输入的节点,进行分类)
Args:
inputTree 决策树模型
featLabels Feature标签对应的名称
testVec 测试输入的数据
Returns:
classLabel 分类的结果值,需要映射label才能知道名称
"""
# 获取tree的根节点对于的key值
firstStr = list(inputTree.keys())[0]
# 通过key得到根节点对应的value
secondDict = inputTree[firstStr]
# 判断根节点名称获取根节点在label中的先后顺序,这样就知道输入的testVec怎么开始对照树来做分类
featIndex = featLabels.index(firstStr)
# 测试数据,找到根节点对应的label位置,也就知道从输入的数据的第几位来开始分类
key = testVec[featIndex]
valueOfFeat = secondDict[key]
print(('+++', firstStr, 'xxx', secondDict, '---', key, '>>>', valueOfFeat))
# 判断分枝是否结束: 判断valueOfFeat是否是dict类型
if isinstance(valueOfFeat, dict):
classLabel = classify(valueOfFeat, featLabels, testVec)
else:
classLabel = valueOfFeat
return classLabel