机器学习之决策树分类算法(ID3 and C4.5)
1 决策树算法框架
1.1 决策树主函数
决策树主函数本质是一个递归函数,主要功能是按照某种规则生长出决策树的各个分支节点,并根据终止条件结束算法。
主函数功能:
(1)输入分类的数据集和类别标签。
(2)根据某种分类规则得到最优的划分特征,并创建特征的划分节点——计算最优特征子函数。
(3)按照该特征取值划分数据集为若*分——划分数据集子函数。
(4)根据划分子函数的计算结果构建新的节点,作为树生长的新分支。
(5)检验是否符合递归的终止条件。
(6)将划分的新节点包含的数据集和类别标签作为输入,递归执行上述步骤。
1.2 计算最优特征子函数
不同标准有不同最优特征子函数,ID3→信息增益,C4.5→信息增益率,CART是节点方差大小。一般选择最优特征需要遍历整个数据集,评估每个特征,找到最优的那一个特征返回。不同标准有不同最优特征子函数,ID3→信息增益,C4.5→信息增益率,CART是节点方差大小。一般选择最优特征需要遍历整个数据集,评估每个特征,找到最优的那一个特征返回。
1.3 划分数据集函数
划分数据集函数的主要功能是分割数据集。
1.4 分类器
决策树的分类器是通过遍历整个决策树,使测试集数据找到决策树叶子节点对应的类别标签,得到返回结果。
2 信息熵
2.1 信息熵基本理论
数据特征的划分过程是一个将数据集从无序变为有序的过程。
不确定性函数I称为事件的信息量,是事件U发生概率p的单调递减函数;两个独立事件所产生的不确定性等于各自不确定性之和,即,称为可加性。
同时满足以上两个条件的函数I是对数函数,即:
若一个信源事件有n种取值:,对应概率,且相互独立。
则信息熵为单个符号不确定性的统计平均值(E),即:
2.2信息熵在决策树中的应用
决策树中,信息熵用来度量类别的不确定性,也用来度量包含不同特征的数据样本与类别的不确定性。即某个特征列向量的信息熵越大,说明该向量的不确定性程度越大,即混乱程度越大,应优先从该特征向量着手来进行划分。
第一步:使用信息熵来度量类别标签对样本整体的不确定性。
设S是s个数据样本的集合。假定类别标签具有m个不同值,定义m个不同类。设si是类Ci中的样本数,则对一个给定样本分类所需要的信息熵:
其中pi是任意样本属于Ci的概率,用估计。
第二步:使用信息熵来度量每种特征不同取值的不确定性。
设A具有v个不同值,可以用特征A将S划分为v个子集。
其中,Sj为S中在A特征上具有值aj的样本子集。如果选A作为测试特征,即最优划分特征,这些子集就是S节点中生长出来的决策树分支。设sij是子集Sj中类Cj的样本数。由A划分成子集的熵或期望信息为:
其中,是第j个子集的权,等于子集(即A值为aj)中的样本个数除以S中的样本总数。信息熵越小,子集划分的纯度越高。对于给定的子集Sj为:
其中,pij是Sj中的样本属于类Ci的概率,用估计。
第三步:用信息增益来确定决策树分支的划分依据。信息增益是决策树某个分支上整个数据集信息熵与当前节点信息熵的差值,用Gain(A)表示,那么在A上的分支将获得的信息增益:
由于知道属性A的值导致的熵的期望压缩。具有最高信息增益的特征就可选做集合S的测试属性,创建一个节点,并以该特征标记,对特征的每个值创建分支,并据此划分样本。
3 ID3算法
3.1 算法流程
(1)以信息增益最大的那个特征列作为根节点来划分。
(2)根据划分节点的不同取值来拆分数据集为多个子集,然后删去当前的特征列,再计算胜于特征列的信息熵。如果有信息增益,就重新划分直至划分结束。
(3)划分结束的标志位:子集中只有一个类别标签,停止划分。
3.2 需求分析
需求:针对一个1024个样本的人群购买新笔记本的需求,样本包含“年龄、收入、学生、信誉”4个特征,构建分类算法预测某个输入样本的购买类别。首先将特征值离散化,对于如下:年龄={0(青),1(中),2(老)},收入={0(高),1(中),2(低)},学生={0(否),1(是)},信誉={0(良),1(优)}。最后一项是类别标签,yes=“买”,no=“不买”。
数据集格式如下:
0 2 0 1 no
0 1 0 0 no
~~~~~~~1024个~~~~~~
1 0 1 0 yes
2 1 0 1 yes
3.3 算法构建
第一步:构建一个ID3Tree工具类来封装算法
# -*- coding:utf-8 -*-
from numpy import *
import math
import copy
import cPickle as pickle
class ID3Tree(object): #定义一个ID3算法工具类
def __int__(self): #构造方法
self.tree={} #生成的树
self.dataSet={} #数据集
self.labels={} #标签集(特征)
def loadDataSet(self,path,labels): #数据导入函数
fp=open(path,"rb") #读取文件内容
content=fp.read()
fp.close()
rowlist=content.splitlines() #按行转变成一维表
recordlist=[row.split("\t") for row in rowlist if row.strip()] #删除前后的空格符
self.dataSet=recordlist
self.labels=labels
def train(self):
labels=copy.deepcopy(self.labels) #深度复制
self.tree=self.buildTree(self.dataSet,labels)
#创建决策树主方法
def buildTree(self,dataSet,labels):
cateList=[data[-1] for data in dataSet] #抽取数据集的决策标签列,标签分类
#程序终止条件1:如果cateList只有一种决策标签,停止划分,返回这个决策标签
if cateList.count(cateList[0])==len(cateList):
return cateList[0]
#程序终止条件2:如果数据集的第一个决策标签只有一个,则返回这个决策标签
if len(dataSet[0])==1:
return self.maxCate(cateList)
#算法核心
bestFeat=self.getBestFeat(dataSet) #返回最优的特征轴
bestFeatLabel=labels[bestFeat] #获取最优特征轴的特征标签
tree={bestFeatLabel:{}} #最优特征轴作为根节点
del(labels[bestFeat]) #删除标签集合中根节点的特征标签列
#再次抽取最优特征轴的列向量
uniqueVals=set([data[bestFeat] for data in dataSet]) #去重
for value in uniqueVals: #决策树递归生长
subLabels=labels[:] #将删除后的特征类别集建立子类别集
#按最优特征列和值分割数据集
splitDataSet=self.splitDataSet(dataSet,bestFeat,value)
subTree=self.buildTree(splitDataSet,subLabels) #构建子树
tree[bestFeatLabel][value]=subTree #递归写入树的集合
return tree
def maxCate(self,cateList): #计算出现次数最多的类别标签
items=dict([(cateList.count(i),i) for i in cateList])
return items[max(items.keys())]
def getBestFeat(self,dataSet): #最优的信息增益
#计算特征向量维,最后一列用于类别标签,需要减去
numFeatures=len(dataSet[0])-1 #特征向量维数,行向量维度-1
baseEntropy=self.computeEntropy(dataSet) #基础熵:源数据的熵
bestInfoGain=0.0 #初始化最优的信息增益
bestFeature=-1 #初始化最优的特征轴
#外循环:遍历数据集各列,计算最优特征轴
#i为数据集列索引,取值范围为(0~(numFeatures-1)
for i in xrange(numFeatures): #抽取第i列的列向量
uniqueVals=set([data[i] for data in dataSet]) #去重:该列的唯一值集
newEntropy=0.0 #初始化该列的香农熵
for value in uniqueVals: #内循环:按列和唯一值计算香农熵
#按选定列i和唯一值分割数据集
subDataSet=self.splitDataSet(dataSet,i,value)
prob=len(subDataSet)/float(len(dataSet))
newEntropy+=prob*self.computeEntropy(subDataSet)
infoGain=baseEntropy-newEntropy #计算最大增益
if(infoGain>bestInfoGain):
bestInfoGain=infoGain
bestFeature=i #重置最优特征为当前列
return bestFeature
def computeEntropy(self,dataSet): #计算香农熵
datalen=float(len(dataSet))
cateList = [data[-1] for data in dataSet] #从数据集中得到类别标签
#得到类别为key,出现次数value的字典
items=dict([(i,cateList.count(i)) for i in cateList])
infoEntropy=0.0 #初始化香农熵
for key in items:
prob=float(items[key])/datalen
infoEntropy-=prob*math.log(prob,2)
return infoEntropy
#划分数据集:分割数据集,删除特征轴所在的数据列,返回剩余的数据集
# dataSet:数据集; axis:特征轴; value:特征轴的取值
def splitDataSet(self,dataSet,axis,value):
rtnList=[]
for featVec in dataSet:
if featVec[axis]==value:
rFeatVec=featVec[:axis] #list操作,提取0~(axis-1)的元素
rFeatVec.extend(featVec[axis+1:]) #list操作:将特征轴(列)之后的元素加回
rtnList.append(rFeatVec)
return rtnList
# 存储树到文件
def storeTree(self, inputTree, filename):
fw = open(filename, 'w')
pickle.dump(inputTree, fw)
fw.close()
# 从文件抓取树
def grabTree(self, filename):
fr = open(filename)
return pickle.load(fr)
#分类器
def predict(self,inputTree,featLables,testVec): #分类器
root=inputTree.keys()[0] #树的根节点
secondDict=inputTree[root] #value-子树结构或分类标签
featIndex=featLables.index(root) #根节点在分类标签集中的位置
key=testVec[featIndex] #测试集数组取值
valueOfFeat=secondDict[key]
if isinstance(valueOfFeat,dict):
classLabel=self.predict(valueOfFeat,featLables,testVec) #递归分类
else:classLabel=valueOfFeat
return classLabel
第二步:训练、存储、测试决策树
# -*- coding:utf-8 -*-
from numpy import *
from ID3Tree import *
import treePlotter as tp
dtree=ID3Tree()
dtree.loadDataSet("dataset.dat",["age","revenue","student","credit"])
dtree.train()
print dtree.tree
tp.createPlot(dtree.tree)
dtree.storeTree(dtree.tree,"data.tree")
mytree=dtree.grabTree("data.tree")
print mytree
lables=["age","revenue","student","credit"]
vector=['0','1','0','0']
mytree=dtree.grabTree("data.tree")
print "真实输出","no","-->","决策树输出",dtree.predict(mytree,lables,vector)
结果如下:
{'age': {'1': 'yes', '0': {'student': {'1': 'yes', '0': 'no'}}, '2': {'credit': {'1': 'no', '0': 'yes'}}}}
{'age': {'1': 'yes', '0': {'student': {'1': 'yes', '0': 'no'}}, '2': {'credit': {'1': 'no', '0': 'yes'}}}}
真实输出 no --> 决策树输出 no
3.4 算法评估
特征:ID3算法以信息熵为度量标准,划分出决策树特征节点,每次优先选取信息量最多的属性,也就是使信息熵变为最小的属性,以构造一棵信息熵下降最快的决策树。
缺点:
(1)采用的是信息增益度量标准,偏向于选取特征值个数多的特征,但是个数多不一定是最优的特征。
(2)递归过程需要依次计算每个特征值的,对于大型数据会生成比较复杂的决策树;层次和分支都很多,而其中某些分支的特征值概率很小,如果不加忽略会造成过拟合的问题。
4 C4.5 算法
4.1信息增益率
C4.5使用信息增益率来替代信息增益进行特征选择,客服了信息增益选择特征时偏向于特征值个数较多的不足。信息增益率定义如下:
其中Gain(S,A)是ID3算法中的信息增益,而划分信息SplitInfo(S,A)代表按照特征A划分样本集S的广度和均匀性。
其中Si到Sc是特征A的C个不同值构成的样本子集。
4.2 算法构建
C4.5更改了ID3的增益率最优节点划分方法,其他部分基本不变。
(1)修改最优节点方法
def getBestFeat(self, dataSet):
Num_Feats = len(dataSet[0][:-1])
totality = len(dataSet)
BaseEntropy = self.computeEntropy(dataSet)
ConditionEntropy = [] # 初始化条件熵
slpitInfo = [] # for C4.5, calculate gain ratio
allFeatVList=[]
for f in xrange(Num_Feats):
featList = [example[f] for example in dataSet]
[splitI,featureValueList] = self.computeSplitInfo(featList)
allFeatVList.append(featureValueList)
slpitInfo.append(splitI)
resultGain = 0.0
for value in featureValueList:
subSet = self.splitDataSet(dataSet, f, value)
appearNum = float(len(subSet))
subEntropy = self.computeEntropy(subSet)
resultGain += (appearNum/totality)*subEntropy
ConditionEntropy.append(resultGain) # 总条件熵
infoGainArray = BaseEntropy*ones(Num_Feats)-array(ConditionEntropy)
infoGainRatio = infoGainArray/array(slpitInfo) # c4.5, info gain ratio
bestFeatureIndex = argsort(-infoGainRatio)[0]
return bestFeatureIndex, allFeatVList[bestFeatureIndex]
(2)增加计算划分信息方法(Splitlnfo)
def computeSplitInfo(self, featureVList):
numEntries = len(featureVList)
featureVauleSetList = list(set(featureVList))
valueCounts = [featureVList.count(featVec) for featVec in featureVauleSetList]
# caclulate shannonEnt
pList = [float(item)/numEntries for item in valueCounts ]
lList = [item*math.log(item,2) for item in pList]
splitInfo = -sum(lList)
return splitInfo, featureVauleSetList
(3)修改决策树主方法
def buildTree(self,dataSet,labels):
cateList = [data[-1] for data in dataSet]
if cateList.count(cateList[0]) == len(cateList):
return cateList[0]
if len(dataSet[0]) == 1:
return self.maxCate(cateList)
bestFeat, featValueList = self.getBestFeat(dataSet)
bestFeatLabel = labels[bestFeat]
tree = {bestFeatLabel:{}}
del(labels[bestFeat])
for value in featValueList:
subLabels = labels[:]
splitDataset = self.splitDataSet(dataSet, bestFeat, value)
subTree = self.buildTree(splitDataset,subLabels)
tree[bestFeatLabel][value] = subTree
return tree
(4)输出结果
{'student': {'1': {'credit': {'1': {'age': {'1': 'yes', '0': 'yes', '2': 'no'}}, '0': 'yes'}}, '0': {'age': {'1': 'yes', '0': 'no', '2': {'credit': {'1': 'no', '0': 'yes'}}}}}}