目录
5.5 统计classList中出现次数最多的元素(类标签)
一、算法概述
1.1决策树模型简介
决策树(Decision Tree):一种分类和回归方法,即基于各种情况发生的所需条件构成决策树,以实现期望最大化的一种图解法。由于这种决策分支画成图形很像一棵树的枝干,故称决策树。它的运行机制非常通俗易懂,因此被誉为机器学习中,最“友好”的算法。在分类问题中,表示基于特征对实例进行分类的过程,可以认为是if-then的集合,也可以认为是定义在特征空间与类空间上的条件概率分布。例如下图是对于西瓜问题的一个决策树:
一般,一棵决策树包含一个根结点、若干个内部结点和若干个叶结点;叶结点对应于决策结果,其他每个结点则对应于一个属性测试;每个结点包含的样本集合根据集合测试的结果被划分到子结点中;根结点包含样本全集。决策树学习的目的是为了产生一棵泛化能力强,即处理未见示例能力强的决策树,其基本流程遵循“分而治之”策略。
1.2决策树的构造
构造决策树通常有三个步骤:特征选择、决策树的生成、决策树的修剪。
决策树学习的算法通常是一个递归地选择最优特征,并根据该特征对训练数据进行分割,使得各个子数据集有一个最好的分类的过程。这一过程对应着对特征空间的划分,也对应着决策树的构建。
1) 开始:构建根节点,将所有训练数据都放在根节点,选择一个最优特征,按着这一特征将训练数据集分割成子集,使得各个子集有一个在当前条件下最好的分类。
2) 若这些子集已经能够被基本正确分类,则构建叶节点,并将这些子集分到所对应的叶节点去。
3)若还有子集不能够被正确的分类,那么就对这些子集选择新的最优特征,继续对其进行分割,构建相应的节点,如果递归进行,直至所有训练数据子集被基本正确的分类,或者没有合适的特征为止。
4)每个子集都被分到叶节点上,即都有了明确的类,这样就生成了一棵决策树。
决策树的生成是一个递归过程。在决策树算法中,有三种情形会导致递归返回:(1)当前结点包含的样本全属于同一类别,无需划分;(2)当前属性集为空,或所有样本在属性值上取值相同,无法划分;(3)当前结点包含的样本集合为空,不能划分。
过程:
首先,确定当前数据集上的决定性特征,为了得到该决定性特征,必须评估每个特征,完成测试之后,原始数据集就被划分为几个数据子集,这些数据子集会分布在第一个决策点的所有分支上,如果某个分支下的数据属于同一类型,则当前无序阅读的垃圾邮件已经正确的划分数据分类,无需进一步对数据集进行分割,如果不属于同一类,则要重复划分数据子集,直到所有相同类型的数据均在一个数据子集内。
创建分支的伪代码 createBranch() 如下图所示:
检测数据集中每个子项是否属于同一类:
If so return 类标签:
Else
寻找划分数据集的最好特征
划分数据集
创建分支节点
for 每个划分的子集
调用函数createBranch()并增加返回结果到分支节点中
return 分支节点
二、信息熵
2.1信息熵的介绍
信息熵(Information Entropy)是表示随机变量不确定性的度量,即物体内部的混乱程度。
2.2信息熵的计算
假设当前样本集合D中第k个样本所占的比例为(k=1,2,...,|y|),则D的信息熵定义为
Ent(D)的值越小,则D的纯度越高。
三、划分选择
3..1 信息增益(ID3算法)
假定离散属性a有V个可能的取值{},若使用a来对样本集D进行划分,则会产生V个分支结点,其中第v个分支结点包含了D中所有在a上取值为
的样本,记为
。根据信息熵公式可以计算出
的信息熵,再考虑到不同的分支结点所包含的样本数不同,给分支结点赋予权重|
|/|D|,即样本数越多的分支结点的影响越大,于是可以计算出用属性a对样本集D进行划分所获得的“信息增益”
一般而言,信息增益越大,意味着使用属性a来进行划分所获得的“纯度提升”越大。著名的ID3决策树学习算法[Quinlan,1986]就是以信息增益作为准则来选择划分属性。但是ID3 仅仅适用于二分类问题。
3.2信息增益率(C4.5算法)
C4.5 克服了 ID3 仅仅能够处理离散属性的问题,以及信息增益偏向选择取值较多特征的问题。信息增益准则对可取数目较多的属性有所偏好,为减少这种偏好可能带来的不利影响,C4.5决策树算法[Quinlan,1993]使用“增益率”来选择最优划分属性。增益率定义为
其中
IV(a)称为属性a的“固有值”。属性a的可能取值数目越多(即V越大),则IV(a)的值通常会越大。
但是,增益率准则对可取值数目较少的属性有所偏好。
3.3基尼系数(CART算法)
CART决策树使用“基尼指数”来选择划分属性,数据集D的纯度可用基尼值来度量:
CART 与 ID3,C4.5 不同之处在于 CART 生成的树必须是二叉树。直观来说,Gini(D)反映了从数据集D中随机抽取两个样本,其类别标记不一致的概率。因此,Gini(D)越小,则数据集D的纯度越高。基尼指数越大,集合不确定性越高,不纯度也越大。属性a的基尼指数定义为
于是,在候选属性集合A中,选择那个使得划分后基尼指数最小的属性作为最优划分属性,即
四、对决策树的剪枝
4.1剪枝
剪枝(pruning):从已经生成的树上裁掉一些子树或叶节点,并将其根节点或父节点作为新的叶子节点,从而简化分类树模型。
实现方式:极小化决策树整体的损失函数或代价函数来实现
决策树学习的损失函数定义为:
其中,T表示这棵子树的叶子结点,表示第t 个叶子的熵,
表示该叶子所含的训练样例的个数,|T|表示子树的叶子结点的个数,α表示惩罚系数。
剪枝就是当α 确定时,选择损失函数最小的模型,即损失函数最小的子树。
当α值确定时,子树越大,往往与训练数据的拟合越好,但是模型的复杂度越高;子树越小,模型的复杂度就越低,但是往往与训练数据的拟合不好
损失函数正好表示了对两者的平衡。
决策树算法很容易过拟合(overfitting),剪枝算法就是用来防止决策树过拟合,提高泛化性能的方法。
4.2预剪枝处理
预剪枝是在决策树生成过程中,对每个结点在划分前先进行估计,若当前结点的划分不能带来决策树泛化性能提升,则停止划分并将当前结点标记为叶结点。
4.3后剪枝处理
后剪枝是从训练集生成一棵完整的决策树,然后自底向上地对非叶结点进行考察,若将该结点对应的子树替换为叶结点能带来决策树泛化性能提升,则将该子树替换为叶结点。
五、算法实现(ID3)
5.1准备数据集,并进行属性标注
![](https://img-blog.csdnimg.cn/direct/f5e7dfa3e84541d5ac752d9987e0d12b.png)
在编写代码之前,我们先对数据集进行属性标注。
- 年龄:0代表青年,1代表中年,2代表老年;
- 有工作:0代表否,1代表是;
- 有自己的房子:0代表否,1代表是;
- 信贷情况:0代表一般,1代表好,2代表非常好;
- 类别(是否给贷款):no代表否,yes代表是。
5.2 计算信息熵
首先计算数据集中实例的总数。接着创建数据字典,它的键值是最后一列的数值。若当前键值不存在,则拓展字典并将当前键值加入字典。每个键值记录了当前类别出现的次数。最后,使用所有类标签的发生频率计算类别出现的概率。用这个概率计算出熵。
def calcShannonEnt(dataSet):
#返回数据集行数
numEntries=len(dataSet)
#保存每个标签(label)出现次数的字典
labelCounts={}
#对每组特征向量进行统计
for featVec in dataSet:
currentLabel=featVec[-1] #提取标签信息
if currentLabel not in labelCounts.keys(): #如果标签没有放入统计次数的字典,添加进去
labelCounts[currentLabel]=0
labelCounts[currentLabel]+=1 #label计数
shannonEnt=0.0 #经验熵
#计算经验熵
for key in labelCounts:
prob=float(labelCounts[key])/numEntries #选择该标签的概率
shannonEnt-=prob*log(prob,2) #利用公式计算
return shannonEnt #返回经验熵
5.3按照给定的特征划分数据集
分类算法除了需要测量信息熵,还需要划分数据集,度量划分数据集的熵,以便判断当前是否正确地划分了数据集。我们将对每个特征划分数据集的结果计算一次信息熵,然后判断按照哪个特征划分数据集是最好的划分方式。
def splitDataSet(dataSet,axis,value):
retDataSet=[]
for featVec in dataSet:
if featVec[axis]==value:
reducedFeatVec=featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
5.4选择最好的数据集划分方式
计算整个数据集的原始信息熵,保存最初的无序度量值,用于与划分完之后的数据集计算的熵值进行比较。遍历当前特征的所有唯一属性值,对每个唯一属性值划分一次数据集,然后计算数据集的新熵值,并对所有唯一特征值得到的熵求和。最后比较所有特征中的信息增益,返回最好特征划分的索引值。
#选择信息增益最大特征的索引值
def chooseBestFeatureToSplit(dataSet):
#特征数量
numFeatures = len(dataSet[0]) - 1
#计数数据集的香农熵
baseEntropy = calcShannonEnt(dataSet)
#信息增益
bestInfoGain = 0.0
#最优特征的索引值
bestFeature = -1
#遍历所有特征
for i in range(numFeatures):
# 获取dataSet的第i个所有特征
featList = [example[i] for example in dataSet]
#创建set集合{},元素不可重复
uniqueVals = set(featList)
#经验条件熵
newEntropy = 0.0
#计算信息增益
for value in uniqueVals:
#subDataSet划分后的子集
subDataSet = splitDataSet(dataSet, i, value)
#计算子集的概率
prob = len(subDataSet) / float(len(dataSet))
#根据公式计算经验条件熵
newEntropy += prob * calcShannonEnt((subDataSet))
#信息增益
infoGain = baseEntropy - newEntropy
#打印每个特征的信息增益
print("第%d个特征的增益为%.3f" % (i, infoGain))
#计算信息增益
if (infoGain > bestInfoGain):
#更新信息增益,找到最大的信息增益
bestInfoGain = infoGain
#记录信息增益最大的特征的索引值
bestFeature = i
#返回信息增益最大特征的索引值
return bestFeature
5.5 统计classList中出现次数最多的元素(类标签)
通过分类名称的列表,创建键值为classList中唯一值的数据字典,字典对象存储了classList中每个类标签出现的频率,利用operator操作键值排序字典,并返回出现次数最多的分类名称。
def majorityCnt(classList):
classCount={}
#统计classList中每个元素出现的次数
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]
5.6创建决策树
递归函数的第一个停止条件是当所有的类标签完全相同,则直接返回该类标签。第二个停止条件是使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组。代码中字典变量myTree存储了树的所有信息,当前数据集选取的最好特征存储在变量bestFeat中,得到列表包含的所有属性值。
def createTree(dataSet,labels,featLabels):
#取分类标签(是否放贷:yes or no)
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]
featLabels.append(bestFeatLabel)
#根据最优特征的标签生成树
myTree={bestFeatLabel:{}}
#删除已经使用的特征标签
del(labels[bestFeat])
#得到训练集中所有最优特征的属性值
featValues=[example[bestFeat] for example in dataSet]
#去掉重复的属性值
uniqueVls=set(featValues)
#遍历特征,创建决策树
for value in uniqueVls:
myTree[bestFeatLabel][value]=createTree(splitDataSet(dataSet,bestFeat,value),labels,featLabels)
return myTree
5.7整体代码
from math import log
import operator
#创建测试数据集(dataSet:数据集;labels:分类属性)
def createDataSet():
# 数据集
dataSet=[[0, 0, 0, 0, 'no'],
[0, 0, 0, 1, 'no'],
[0, 1, 0, 1, 'yes'],
[0, 1, 1, 0, 'yes'],
[0, 0, 0, 0, 'no'],
[1, 0, 0, 0, 'no'],
[1, 0, 0, 1, 'no'],
[1, 1, 1, 1, 'yes'],
[1, 0, 1, 2, 'yes'],
[1, 0, 1, 2, 'yes'],
[2, 0, 1, 2, 'yes'],
[2, 0, 1, 1, 'yes'],
[2, 1, 0, 1, 'yes'],
[2, 1, 0, 2, 'yes'],
[2, 0, 0, 0, 'no']]
#分类属性
labels=['年龄','有工作','有自己的房子','信贷情况']
#返回数据集和分类属性
return dataSet,labels
#计算数据集的经验熵
def calcShannonEnt(dataSet):
#返回数据集行数
numEntries=len(dataSet)
#保存每个标签(label)出现次数的字典
labelCounts={}
#对每组特征向量进行统计
for featVec in dataSet:
currentLabel=featVec[-1] #提取标签信息
if currentLabel not in labelCounts.keys(): #如果标签没有放入统计次数的字典,添加进去
labelCounts[currentLabel]=0
labelCounts[currentLabel]+=1 #label计数
shannonEnt=0.0 #经验熵
#计算经验熵
for key in labelCounts:
prob=float(labelCounts[key])/numEntries #选择该标签的概率
shannonEnt-=prob*log(prob,2) #利用公式计算
return shannonEnt #返回经验熵
#根据给定特征划分数据集
def splitDataSet(dataSet,axis,value):
retDataSet=[]
for featVec in dataSet:
if featVec[axis]==value:
reducedFeatVec=featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
#选择信息增益最大特征的索引值
def chooseBestFeatureToSplit(dataSet):
#特征数量
numFeatures = len(dataSet[0]) - 1
#计数数据集的香农熵
baseEntropy = calcShannonEnt(dataSet)
#信息增益
bestInfoGain = 0.0
#最优特征的索引值
bestFeature = -1
#遍历所有特征
for i in range(numFeatures):
# 获取dataSet的第i个所有特征
featList = [example[i] for example in dataSet]
#创建set集合{},元素不可重复
uniqueVals = set(featList)
#经验条件熵
newEntropy = 0.0
#计算信息增益
for value in uniqueVals:
#subDataSet划分后的子集
subDataSet = splitDataSet(dataSet, i, value)
#计算子集的概率
prob = len(subDataSet) / float(len(dataSet))
#根据公式计算经验条件熵
newEntropy += prob * calcShannonEnt((subDataSet))
#信息增益
infoGain = baseEntropy - newEntropy
#打印每个特征的信息增益
print("第%d个特征的增益为%.3f" % (i, infoGain))
#计算信息增益
if (infoGain > bestInfoGain):
#更新信息增益,找到最大的信息增益
bestInfoGain = infoGain
#记录信息增益最大的特征的索引值
bestFeature = i
#返回信息增益最大特征的索引值
return bestFeature
#统计classList中出现次数最多的元素(类标签)
def majorityCnt(classList):
classCount={}
#统计classList中每个元素出现的次数
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,featLabels):
#取分类标签(是否放贷:yes or no)
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]
featLabels.append(bestFeatLabel)
#根据最优特征的标签生成树
myTree={bestFeatLabel:{}}
#删除已经使用的特征标签
del(labels[bestFeat])
#得到训练集中所有最优特征的属性值
featValues=[example[bestFeat] for example in dataSet]
#去掉重复的属性值
uniqueVls=set(featValues)
#遍历特征,创建决策树
for value in uniqueVls:
myTree[bestFeatLabel][value]=createTree(splitDataSet(dataSet,bestFeat,value),labels,featLabels)
return myTree
"""
使用决策树进行分类
Parameters:
inputTree;已经生成的决策树
featLabels:存储选择的最优特征标签
testVec:测试数据列表,顺序对应最优特征标签
Returns:
classLabel:分类结果
"""
def classify(inputTree,featLabels,testVec):
#获取决策树节点
firstStr=next(iter(inputTree))
#下一个字典
secondDict=inputTree[firstStr]
featIndex=featLabels.index(firstStr)
for key in secondDict.keys():
if testVec[featIndex]==key:
if type(secondDict[key]).__name__=='dict':
classLabel=classify(secondDict[key],featLabels,testVec)
else: classLabel=secondDict[key]
return classLabel
if __name__=='__main__':
print('使用ID3算法')
dataSet,labels=createDataSet()
featLabels=[]
myTree=createTree(dataSet,labels,featLabels)
print(myTree)
#测试数据
testVec=[0,1]
result=classify(myTree,featLabels,testVec)
if result=='yes':
print('测试数据的预测结果:放贷')
if result=='no':
print('测试数据的预测结果:不放贷')
六、以C4.5实现构建决策树
与ID3类似,只不过此时需要将信息增益改为信息增益率,修改calcShannonEnt
函数以计算信息增益率,其余过程与ID3一致。
from math import log
import operator
def createDataSet():
dataSet = [[0, 0, 0, 0, 'no'],
[0, 0, 0, 1, 'no'],
[0, 1, 0, 1, 'yes'],
[0, 1, 1, 0, 'yes'],
[0, 0, 0, 0, 'no'],
[1, 0, 0, 0, 'no'],
[1, 0, 0, 1, 'no'],
[1, 1, 1, 1, 'yes'],
[1, 0, 1, 2, 'yes'],
[1, 0, 1, 2, 'yes'],
[2, 0, 1, 2, 'yes'],
[2, 0, 1, 1, 'yes'],
[2, 1, 0, 1, 'yes'],
[2, 1, 0, 2, 'yes'],
[2, 0, 0, 0, 'no']]
labels = ['年龄', '有工作', '有自己的房子', '信贷情况']
return dataSet, labels
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
def splitDataSet(dataSet, axis, value):
retDataSet = []
for featVec in dataSet:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis + 1:])
retDataSet.append(reducedFeatVec)
return retDataSet
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1
baseEntropy = calcShannonEnt(dataSet)
bestInfoGainRatio = 0.0
bestFeature = -1
for i in range(numFeatures):
featList = [example[i] for example in dataSet]
uniqueVals = set(featList)
newEntropy = 0.0
IV = 0.0
for value in uniqueVals:
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet) / float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
IV -= prob * log(prob, 2)
infoGain = baseEntropy - newEntropy
if (IV == 0): # 防止除数为0的情况
IV = 1
infoGainRatio = infoGain / IV
print("第%d个特征的增益率为%.3f" % (i, infoGainRatio))
if (infoGainRatio > bestInfoGainRatio):
bestInfoGainRatio = infoGainRatio
bestFeature = i
return bestFeature
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)
return sortedClassCount[0][0]
def createTree(dataSet, labels, featLabels):
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]
featLabels.append(bestFeatLabel)
myTree = {bestFeatLabel: {}}
del (labels[bestFeat])
featValues = [example[bestFeat] for example in dataSet]
uniqueVls = set(featValues)
for value in uniqueVls:
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels)
return myTree
def classify(inputTree, featLabels, testVec):
firstStr = next(iter(inputTree))
secondDict = inputTree[firstStr]
featIndex = featLabels.index(firstStr)
for key in secondDict.keys():
if testVec[featIndex] == key:
if type(secondDict[key]).__name__ == 'dict':
classLabel = classify(secondDict[key], featLabels, testVec)
else:
classLabel = secondDict[key]
return classLabel
if __name__ == '__main__':
print('使用C4.5算法')
dataSet, labels = createDataSet()
featLabels = []
myTree = createTree(dataSet, labels, featLabels)
print(myTree)
#测试数据
testVec=[0,1]
result=classify(myTree,featLabels,testVec)
if result=='yes':
print('测试数据的预测结果:放贷')
if result=='no':
print('测试数据的预测结果:不放贷')
七、实验小结
7.1实验结果截图
以ID3实现的结果为:
根据计算,选择信息增益最大的A2作为节点的特征,由于其有两个取值可能,所以引出两个子节点:
①对应“是”(有工作),包含三个样本,属于同一类,所以是一个叶子节点,类标记为“是”
②对应“否”(无工作),包含六个样本,输入同一类,所以是一个叶子节点,类标记为“否”
这样就生成一个决策树,该树只用了两个特征(有两个内部节点),生成的决策树如下图所示:
以C4.5实现的结果为:
7.2实验总结
通过本次实验学习了对决策树的创建,决策树算法主要包括三个部分:特征选择、树的生成、树的剪枝。常用算法有 ID3、C4.5、CART。其中,特征选择的关键是准则:信息增益、信息增益比、Gini 指数;决策树的生成通常是利用信息增益最大、信息增益比最大、Gini 指数最小作为特征选择的准则。从根节点开始,递归的生成决策树。相当于是不断选取局部最优特征,或将训练集分割为基本能够正确分类的子集;决策树的剪枝是为了防止树的过拟合,增强其泛化能力。包括预剪枝和后剪枝。
7.3参考书籍
《机器学习》 周志华
《机器学习实战》 Peter Harrington