Python3实现机器学习经典算法(四)C4.5决策树

时间:2022-03-08 09:02:25

一、C4.5决策树概述

  C4.5决策树是ID3决策树的改进算法,它解决了ID3决策树无法处理连续型数据的问题以及ID3决策树在使用信息增益划分数据集的时候倾向于选择属性分支更多的属性的问题。它的大部分流程和ID3决策树是相同的或者相似的,可以参考我的上一篇博客:https://www.cnblogs.com/DawnSwallow/p/9452586.html

  C4.5决策树和ID3决策树相同,也可以产生一个离线的“决策树”,而且对于连续属性组成的C4.5决策树数据集,C4.5算法可以避开“测试集中的取值不存在于训练集”这种情况,所以不需要像ID3决策树那样,预先将测试集中不存在于训练集中的属性的取值,“手动地”加入到决策树中的问题。但是对于同时有离散属性和连续属性的数据集,离散属性部分仍旧是需要进行将存在于测试集,不存在于训练集中的取值(注意,是取值不是向量!)给删除或者加入到树的构造过程中。

  C4.5不是一个简单的决策树构造算法,它是一组算法,包括C4.5的构造和C4.5剪枝的问题,剪枝问题在ID3决策树、C4.5决策树和CART树都实现完的时候再统一实现。

  流行的C4.5决策树构造算法是没有进行修正的过程的,这会导致一个很严重的问题:C4.5决策树在构造树的时候倾向于选择连续属性作为最佳分割属性。所以C4.5需要一个修正的过程,在进行连续属性的信息增益率的计算的时候,要进行修正。

  C4.5使用的是信息增益率来划分“最好的”属性,这个“信息增益率”和ID3决策树使用的“信息增益”有什么区别呢?

  信息增益(Information Gain):对于某一种划分的信息增益可以表示为“期望信息 - 该种划分的香农熵”。它的公式可以表示为:IG(T)=H(C)-H(C|T)。其中C代表的是分类或者聚类C,T代表的是则是当前选择进行划分的特征。这条公式表示了:选择特征T进行划分,则其信息增益为数据集的期望信息减去选择该特征T进行划分后的期望信息。这里要明确的是:期望信息就是香农熵。熵是信息的期望,所以熵的表示应该为所有信息出现的概率和其期望的总和,即:

Python3实现机器学习经典算法(四)C4.5决策树

  当我们把这条熵公式转换为一个函数:calculateEntropy(dataSet,feature = NULL)的时候,上面这个计算过程可以变成以下的伪代码:

Python3实现机器学习经典算法(四)C4.5决策树
1 while dataSet != NULL:
2 feat = -1
3 for i in range(featureNum):
4 IG = calculateEntropy(dataSet) - calculateEntropy(dataSet,feature[i])
5 if IG > IGMAX:
6 IGMAX = IG
7 feat = feature[i]
8 #IGMAX此时保存的即为最大的信息增益,feat保存的即为最大的信息增益所对应的特征
9 dataSet = dataSet - feature[i]#这里不是减法,而是在数据集中去除该列
Python3实现机器学习经典算法(四)C4.5决策树

  由上面的伪代码,也可以理解到“信息增益最大的时候,熵减最多”。这里的数学理解就是:信息增益的公式可以看作A - B,其中B是改变的,A是一个常量,那么B越小A - B的值就会越大,B越小则代表熵越小,当B达到最小的时候,A - B最大,此时熵最小,也即是熵减最多。

  信息增益率/信息增益比(GainRatio):在选择决策树中某个结点上的分支属性时,假设该结点上的数据集为DataSet,其中包含Feature个描述属性,样本总数为len(DataSet)或者DataSet.shape[0],设描述属性feature(不同于Feature,Feature是属性的个数,取值为DataSet.shape[1],feature是某一个具体的属性)总共有M个不同的取值,则利用描述属性feature可以将数据集DataSet划分为M个子集,设这些子集为{DataSet1,DataSet2,…,DataSetN,…,DataSetM},并且这些子集中样本在同一个子集中对应的feature属性的取值应该是相同的(若feature属性为离散属性,则取值为某一个离散值,若为连续属性,则取值为<=num,>num之一),用{len1,len2,…,lenN,…,lenM}表示每个对应的子集的样本的数量,则用描述属性feature来划分给定的数据集DataSet所得到的信息增益率/信息增益比为:

Python3实现机器学习经典算法(四)C4.5决策树

Gain(feature)的算法和上面ID3决策树计算信息增益的算法是一样的,事实上,求GainRatio的过程就是在上述的计算信息增益的过程中加上一个对其“稀释”的作用,使得取值多的feature不会占据主导地位,由于在计算信息增益的时候,是一个累加的公式,log(p)一定是一个负值,这样就导致Gain(feature)会一直地往上增大,即使增大幅度很小,而除以划分属性的熵公式(如下)则可以尽量的把这种微小的累加所带来的影响降到最低。

Python3实现机器学习经典算法(四)C4.5决策树

  C4.5处理连续属性的数据的过程:假设当前正在处理的属性feature为一个连续型属性,当前正在划分的数据集的样本数量为total,则:

  ①将该节点上的所有数据样本按照连续型描述属性的具体取值,由小到大进行排序,得到属性值的取值序列{A1,A2,…,AN,…,Atotal};

  ②在获得的取值序列{A1,A2,…,AN,…,Atotal}中生成total - 1个分割点,其中,第n(1<= n <= total - 1)个分割点的取值为(An + An+1 )/ 2,获得的这个分割点,可以将数据集DataSet划分为两个子集,即描述属性feature的取值在[ A1,(An + An+1 )/ 2 ],((An + An+1 )/ 2 ,Atotal]这两个区间的数据样本。

  ③从total - 1个分割点中选择当前描述属性feature的最佳分割点,这个分割点可以得到最大的信息增益。(注意,信息增益,而非信息增益率,这是对C4.5的修正)

  ④计算当前描述属性feature的信息增益率,如果它的信息增益率是所有的描述属性中最大的,则选择其作为当前结点的划分描述属性。

  下面举例说明C4.5算法对于连续型描述属性的处理方法:假设一个连续型属性的取值序列为{32,25,46,56,60,52,42,36,23,51,38,43,41,65}。

  ①对连续序列进行升序排序,产生一个新的有序连续序列:{23,25,32,36,38,41,42,43,46,51,52,56,60,65};

  ②对新的有序连续序列产生分割点,共产生13个分割点:{24,23.5,34,37,39.5,41.5,42.5,44.5,48.5,51.5,54,58,62.5};

  ③选择最佳分割点。对于第一个分割点,计算取值在[23,24]的数量和在(24,65]中的数量,然后计算其信息增益IG1,而后对于第二个分割点,计算取值在[23,25]的数量和在(25,65]的数量,计算其信息增益IG2,以此类推,最后选择最大的信息增益IGMAX,此时对应的分割点为最大分割点。

  ④选择最大分割点后,对于这个分割点,计算信息增益率GainRatio,则这个GainRatio则代表了这个描述属性feature的GainRatio。

  C4.5修正:C4.5的修正在上面的处理连续属性的数据的过程③中体现了出来,它选择的并不是能获得最大的信息增益率的分割点,而是选择能获得最大的信息增益的分割点。这样做的原因是,当我们选择信息增益率来作为C4.5的连续型属性的数据集划分的依据时,它会倾向于选择连续型属性来作为划分的描述属性具体算法流程如下:

  ①将该节点上的所有数据样本按照连续型描述属性的具体取值,由小到大进行排序,得到属性值的取值序列{A1,A2,…,AN,…,Atotal};

  ②在获得的取值序列{A1,A2,…,AN,…,Atotal}中生成total - 1个分割点,其中,第n(1<= n <= total - 1)个分割点的取值为(An + An+1 )/ 2,获得的这个分割点,可以将数据集DataSet划分为两个子集,即描述属性feature的取值在[ A1,(An + An+1 )/ 2 ],((An + An+1 )/ 2 ,Atotal]这两个区间的数据样本,计算这个分割点的信息增益。

  ③选择信息增益最大的分割点作为该描述属性feature的最佳分割点。

  ④计算最佳分割点的信息增益率作为当前的描述属性的信息增益率,对最佳分割点的信息增益进行修正,减去log2(N-1)/|D|(N是连续特征的取值个数,D是训练数据数目)。

二、准备数据集

  Python3实现机器学习经典算法的数据集都采用了著名的机器学习仓库UCI(http://archive.ics.uci.edu/ml/datasets.html),其中分类系列算法采用的是Adult数据集(http://archive.ics.uci.edu/ml/datasets/Adult),测试数据所在网址:http://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data,训练数据所在网址:http://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.test。

  Adult数据集通过收集14个特征来判断一个人的收入是否超过50K,14个特征及其取值分别是:

  age: continuous.

  workclass: Private, Self-emp-not-inc, Self-emp-inc, Federal-gov, Local-gov, State-gov, Without-pay, Never-worked.

  fnlwgt: continuous.

  education: Bachelors, Some-college, 11th, HS-grad, Prof-school, Assoc-acdm, Assoc-voc, 9th, 7th-8th, 12th, Masters, 1st-4th, 10th, Doctorate, 5th-6th, Preschool.

  education-num: continuous.

  marital-status: Married-civ-spouse, Divorced, Never-married, Separated, Widowed, Married-spouse-absent, Married-AF-spouse.

  occupation: Tech-support, Craft-repair, Other-service, Sales, Exec-managerial, Prof-specialty, Handlers-cleaners, Machine-op-inspct, Adm-clerical, Farming-fishing, Transport-moving, Priv-house-serv, Protective-serv, Armed-Forces.

  relationship: Wife, Own-child, Husband, Not-in-family, Other-relative, Unmarried.

  race: White, Asian-Pac-Islander, Amer-Indian-Eskimo, Other, Black.

  sex: Female, Male.

  capital-gain: continuous.

  capital-loss: continuous.

  hours-per-week: continuous.

  native-country: United-States, Cambodia, England, Puerto-Rico, Canada, Germany, Outlying-US(Guam-USVI-etc), India, Japan, Greece, South, China, Cuba, Iran, Honduras, Philippines, Italy, Poland, Jamaica, Vietnam, Mexico, Portugal, Ireland, France, Dominican-Republic, Laos, Ecuador, *, Haiti, Columbia, Hungary, Guatemala, Nicaragua, Scotland, Thailand, Yugoslavia, El-Salvador, Trinadad&Tobago, Peru, Hong, Holand-Netherlands.

  

  最终的分类标签有两个:>50K, <=50K.

  

下一步是分析数据:

1、数据预处理:C4.5算法能处理连续属性和连续属性,所以这里不需要数据预处理的过程,整个原生的数据集就是训练集/测试集。

2、数据清洗:

  数据中含有大量的不确定数据,这些数据在数据集中已经被转换为‘?’,但是它仍旧是无法使用的,数据挖掘对于这类数据进行数据清洗的要求规定,如果是可推算数据,应该推算后填入;或者应该通过数据处理填入一个平滑的值,然而这里的数据大部分没有相关性,所以无法推算出一个合理的平滑值;所以所有的‘?’数据都应该被剔除而不应该继续使用。为此我们要用一段代码来进行数据的清洗:


1 def cleanOutData(dataSet):#数据清洗
2 for row in dataSet:
3 for column in row:
4 if column == '?' or column=='':
5 dataSet.remove(row

  这段代码只是示例,它有它不能处理的数据集!比如上述这段代码是无法处理相邻两个向量都存在‘?’的情况的!修改思路有多种,一种是循环上述代码N次直到没有'?'的情况,这种算法简单易实现,只是给上述代码加了一层循环,然而其复杂度为O(N*len(dataset));另外一种实现是每次找到存在'?'的列,回退迭代器一个距离,大致的伪代码为:

1 def cleanOutData(dataSet):
2 for i in range(len(dataSet)):
3 if dataSet[i].contain('?'):
4 dataSet.remove(dataSet[i]) ( dataSet.drop(i) )
5 i-=1

  上述代码的复杂度为O(n)非常快速,但是这种修改迭代器的方式会引起编译器的报错,对于这种报错可以选择修改编译器使其忽略,但是不建议使用这种回退迭代器的写法。

3、数据归一化:

  决策树这样的概念模型不需要进行数据归一化,因为它关心的是向量的分布情况和向量之间的条件概率而不是变量的值,进行数据归一化更难以进行划分数据集,因为Double类型的判等非常难做且不准确。

4、数据集读入:

  综合上诉的预处理和数据清洗的过程,数据集读入的过程为:

  

 #读取数据集
def createDateset(filename):
with open(filename, 'r')as csvfile:
dataset= [line.strip().split(', ') for line in csvfile.readlines()] #读取文件中的每一行
dataset=[[int(i) if i.isdigit() else i for i in row] for row in dataset] #对于每一行中的每一个元素,将行列式数字化并且去除空白保证匹配的正确完成
cleanoutdata(dataset) #清洗数据
del (dataset[-1]) #去除最后一行的空行
#precondition(dataset) #预处理数据
labels=['age','workclass','fnlwgt','education','education-num',
'marital-status','occupation',
'relationship','race','sex','capital-gain','capital-loss','hours-per-week',
'native-country']
labelType = ['continuous', 'uncontinuous', 'continuous',
'uncontinuous',
'continuous', 'uncontinuous',
'uncontinuous', 'uncontinuous', 'uncontinuous',
'uncontinuous', 'continuous', 'continuous',
'continuous', 'uncontinuous'] return dataset,labels,labelType def cleanoutdata(dataset):#数据清洗
for row in dataset:
for column in row:
if column == '?' or column=='':
dataset.remove(row)
break

  对比ID3的读入过程,少了一个对于连续型属性的清洗过程,增加了一个labelType的列表来表示当前的属性是连续型属性还是离散型属性。

三、训练算法

  训练算法既是构造C4.5决策树的过程,构造结束的原则为:如果某个树分支下的数据全部属于同一类型,则已经正确的为该分支以下的所有数据划分分类,无需进一步对数据集进行分割,如果数据集内的数据不属于同一类型,则需要继续划分数据子集,该数据子集划分后作为一个分支继续进行当前的判断。

  用伪代码表示如下:

  if 数据集中所有的向量属于同一分类:

    return 分类标签

  else:

    if 属性特征已经使用完:

      进行投票决策

      return 票数最多的分类标签

    else:

      寻找信息增益率最大的数据集划分方式(找到要分割的属性特征T)

      根据属性特征T创建分支

      if 属性为连续型属性:

        读入取值序列并升序排列

        选择信息增益最大的分割点作为子树的划分依据

      else:

        for 属性特征T的每个取值

          成为当前树分支的子树

        划分数据集(将T属性特征的列丢弃或屏蔽)

      return 分支(新的数据集,递归)

  

  根据上面的伪代码,可以得到下面一步一部的训练算法流程,其中很多的过程和在ID3决策树中的过程是相似的甚至一模一样的。

  1、寻找信息增益率最大的数据集划分方式(找到要分割的属性特征T):

 #计算香农熵/期望信息
def calculateEntropy(dataSet):
ClassifyCount = {}#分类标签统计字典,用来统计每个分类标签的概率
for vector in dataSet:
clasification = vector[-1] #获取分类
if not clasification in ClassifyCount.keys():#如果分类暂时不在字典中,在字典中添加对应的值对
ClassifyCount[clasification] = 0
ClassifyCount[clasification] += 1 #计算出现次数
shannonEntropy=0.0
for key in ClassifyCount:
probability = float(ClassifyCount[key]) / len(dataSet) #计算概率
shannonEntropy -= probability * log(probability,2) #香农熵的每一个子项都是负的
return shannonEntropy #连续型属性不需要将训练集中有,测试集中没有的值补全,离散性属性需要
# def addFeatureValue(featureListOfValue,feature):
# feat = [[ 'Private', 'Self-emp-not-inc', 'Self-emp-inc',
# 'Federal-gov', 'Local-gov', 'State-gov', 'Without-pay', 'Never-worked'],
# [],[],[],[],[]]
# for featureValue in feat[feature]: #feat保存的是所有属性特征的所有可能的取值,其结构为feat = [ [val1,val2,val3,…,valn], [], [], [], … ,[] ]
# featureListOfValue.append(featureValue) #选择最好的数据集划分方式
def chooseBestSplitWay(dataSet,labelType):
isContinuous = -1 #判断是否是连续值,是为1,不是为0
HC = calculateEntropy(dataSet)#计算整个数据集的香农熵(期望信息),即H(C),用来和每个feature的香农熵进行比较
bestfeatureIndex = -1 #最好的划分方式的索引值,因为0也是索引值,所以应该设置为负数
gainRatioMax=0.0 #信息增益率=(期望信息-熵)/分割获得的信息增益,即为GR = IG / split = ( HC - HTC )/ split , gainRatioMax为最好的信息增益率,IG为各种划分方式的信息增益 continuousValue = -1 #设置如果是连续值的属性返回的最好的划分方式的最好分割点的值
for feature in range(len(dataSet[0]) -1 ): #计算feature的个数,由于dataset中是包含有类别的,所以要减去类别
featureListOfValue=[vector[feature] for vector in dataSet] #对于dataset中每一个feature,创建单独的列表list保存其取值,其中是不重复的
addFeatureValue(featureListOfValue,feature) #增加在训练集中有,测试集中没有的属性特征的取值
if labelType[feature] == 'uncontinuous':
unique=set(featureListOfValue)
HTC=0.0 #保存HTC,即H(T|C)
split = 0.0 #保存split(T)
for value in unique:
subDataSet = splitDataSet(dataSet,feature,value) #划分数据集
probability = len(subDataSet) / float(len(dataSet)) #求得当前类别的概率
split -= probability * log(probability,2) #计算split(T)
HTC += probability * calculateEntropy(subDataSet) #计算当前类别的香农熵,并和HTC想加,即H(T|C) = H(T1|C)+ H(T2|C) + … + H(TN|C)
IG=HC-HTC #计算对于该种划分方式的信息增益
if split == 0:
split = 1
gainRatio = float(IG)/float(split) #计算对于该种划分方式的信息增益率
if gainRatio > gainRatioMax :
isContinuous = 0
gainRatioMax = gainRatio
bestfeatureIndex = feature
else: #如果feature是连续型的
featureListOfValue = set(featureListOfValue)
sortedValue = sorted(featureListOfValue)
splitPoint = []
for i in range(len(sortedValue)-1):#n个value,应该有n-1个分割点splitPoint
splitPoint.append((float(sortedValue[i])+float(sortedValue[i+1]))/2.0) #C4.5修正,不再使用信息增益率来选择最佳分割点
# for i in range(len(splitPoint)): #对于n-1个分割点,计算每个分割点的信息增益率,来选择最佳分割点
# HTC = 0.0
# split = 0.0
# gainRatio = 0.0
# biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i])
# print(i)
# probabilityBig = len(biggerDataSet)/len(dataSet)
# probabilitySmall = len(smallerDataSet)/len(dataSet)
# HTC += probabilityBig * calculateEntropy(biggerDataSet)
# HTC += probabilityBig * calculateEntropy(smallerDataSet)
# if probabilityBig != 0:
# split -= probabilityBig * log(probabilityBig,2)
# if probabilitySmall != 0:
# split -= probabilitySmall *log(probabilitySmall,2)
# IG = HC - HTC
# if split == 0:
# split = 1;
# gainRatio = IG/split
# if gainRatio>gainRatioMax:
# isContinuous = 1
# gainRatioMax = gainRatio
# bestfeatureIndex = feature
# continuousValue = splitPoint[i]
IGMAX = 0.0
for i in range(len(splitPoint)):
HTC = 0.0
split = 0.0 biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i])
probabilityBig = len(biggerDataSet) / len(dataSet)
probabilitySmall = len(smallerDataSet) / len(dataSet)
HTC += probabilityBig * calculateEntropy(biggerDataSet)
HTC += probabilityBig * calculateEntropy(smallerDataSet)
if probabilityBig != 0:
split -= probabilityBig * log(probabilityBig, 2)
if probabilitySmall != 0:
split -= probabilitySmall * log(probabilitySmall, 2)
IG = HC - HTC
if IG>IGMAX:
IGMAX = IG
continuousValue = splitPoint[i]
N = len(splitPoint)
D = len(dataSet)
IG -= log(featureListOfValue - 1, 2) / abs(D)
GR = float(IG) / float(split)
if GR > gainRatioMax:
isContinuous = 1
gainRatioMax = GR
bestfeatureIndex = feature return bestfeatureIndex,continuousValue,isContinuous

  这里需要解释的地方有几个:

  1)信息增益的计算:

    经过前面对信息增益的计算,来到这里应该很容易能看得懂这段代码了。IG表示的是对于某一种划分方式的信息增益,由上面公式可知:IG = HC - HTC,HC和HTC的计算基于相同的函数calculateEntropy(),唯一不同的是,HC的计算相对简单,因为它是针对整个数据集(子集)的;HTC的计算则相对复杂,由条件概率得知HTC可以这样计算:

Python3实现机器学习经典算法(四)C4.5决策树

  所以我们可以反复调用calculateEntropy()函数,然后对于每一次计算结果进行累加,这就可以得到HTC。

  2)addFeatureValue()函数

    增加这一个函数的主要原因是:在测试集中可能出现训练集中没有的特征的取值的情况,这在我所使用的adlut数据集中是存在的。庆幸的是,adult数据集官方给出了每种属性特征可能出现的所有的取值,这就创造了解决这个机会的条件。如上所示,在第二部分准备数据集中,每个属性特征的取值已经给出,那我们就可以在创建保存某一属性特征的所有不重复取值的时候加上没有存在的,但是可能出现在测试集中的取值。这就是addFeatureValue()的功用了。

  3)chooseBestSplitWay()函数中的修正部分:

    这是C4.5修正和不修正的区别之处,下面是不修正的代码:

  

         else: #如果feature是连续型的
featureListOfValue = set(featureListOfValue)
sortedValue = sorted(featureListOfValue)
splitPoint = []
for i in range(len(sortedValue)-1):#n个value,应该有n-1个分割点splitPoint
splitPoint.append((float(sortedValue[i])+float(sortedValue[i+1]))/2.0) #C4.5修正,不再使用信息增益率来选择最佳分割点
for i in range(len(splitPoint)): #对于n-1个分割点,计算每个分割点的信息增益率,来选择最佳分割点
HTC = 0.0
split = 0.0
gainRatio = 0.0
biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i])
print(i)
probabilityBig = len(biggerDataSet)/len(dataSet)
probabilitySmall = len(smallerDataSet)/len(dataSet)
HTC += probabilityBig * calculateEntropy(biggerDataSet)
HTC += probabilityBig * calculateEntropy(smallerDataSet)
if probabilityBig != 0:
split -= probabilityBig * log(probabilityBig,2)
if probabilitySmall != 0:
split -= probabilitySmall *log(probabilitySmall,2)
IG = HC - HTC
if split == 0:
split = 1;
gainRatio = IG/split
if gainRatio>gainRatioMax:
isContinuous = 1
gainRatioMax = gainRatio
bestfeatureIndex = feature
continuousValue = splitPoint[i]
return bestfeatureIndex,continuousValue,isContinuous

  这段代码本身是没有错误的,它也能根据连续型属性的非修正算法来进行划分,但是它的问题在于,它总是优先选择连续型属性来作为划分描述属性,如下所示:

  Python3实现机器学习经典算法(四)C4.5决策树

  这些属性大多数都是连续型属性,这就使得我们原本解决“ID3决策树倾向于选择取值多的属性”转变为“C4.5决策树倾向于选择连续型属性”的问题。

  所以根据上述的“修正”过程,得到下面的修正代码:

  

         else: #如果feature是连续型的
featureListOfValue = set(featureListOfValue)
sortedValue = sorted(featureListOfValue)
splitPoint = []
for i in range(len(sortedValue)-1):#n个value,应该有n-1个分割点splitPoint
splitPoint.append((float(sortedValue[i])+float(sortedValue[i+1]))/2.0) #C4.5修正,不再使用信息增益率来选择最佳分割点
IGMAX = 0.0
for i in range(len(splitPoint)):
HTC = 0.0
split = 0.0 biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i])
probabilityBig = len(biggerDataSet) / len(dataSet)
probabilitySmall = len(smallerDataSet) / len(dataSet)
HTC += probabilityBig * calculateEntropy(biggerDataSet)
HTC += probabilityBig * calculateEntropy(smallerDataSet)
if probabilityBig != 0:
split -= probabilityBig * log(probabilityBig, 2)
if probabilitySmall != 0:
split -= probabilitySmall * log(probabilitySmall, 2)
IG = HC - HTC
if IG>IGMAX:
IGMAX = IG
continuousValue = splitPoint[i]
N = len(featureListOfValue)
D = len(dataSet)
IG -= log(N - 1, 2) / abs(D)
GR = float(IG) / float(split)
if GR > gainRatioMax:
isContinuous = 1
gainRatioMax = GR
bestfeatureIndex = feature return bestfeatureIndex,continuousValue,isContinuous

这种算法所运行的结果比较倾向于平均化:

Python3实现机器学习经典算法(四)C4.5决策树

  由于 将连续性数据和离散型数据的处理方式统一的放在同一个chooseBestSplitWay中会导致这个函数非常的臃肿混乱,所以后面将它解析了,具体看完整代码。

  4)在处理连续型属性的时候,属性取值序列进行了一次去重操作。

  这个操作可以没有,但是测试结果表示,进行一次去重操作反而可以提高程序的运行效率和正确率。

  为什么要进行这个去重操作?

  考虑下面一种连续性属性的取值序列:{A1,A2,…,An},其中An-m,An-m+1,…,An (0 <= m < n )是相等的,这种数据序列出现的概率非常大,比如age序列(已经升序):{1,2,3,4,…,80,80,80,80}

  ①如果按照上述的连续值处理的操作来做的话,那么对于这个没有去重的取值序列来说,将有多次取值为某一个数,那么这多次取同一个数来进行数据集划分的操作将是一模一样的,加上C4.5本身就是一个低效的算法,如果重复值非常多,会导致算法更加的低效。

  ②另外一个情况就是,考虑它的分割点情况,在最后的{…,80,80,80,…,80}的序列中,显然分割点为(80+80)/ 2 = 80,那么在进行划分数据集的时候,将会划分为[min,80]以及(80,max]的情况,这样又会遇到一个问题,即划分的数据子集中,很有可能出现空集的情况。这样就会导致我们计算出来的概率probability的取值为0,这样又要对log(p)的计算进行0值的检查。如果对数据集进行去重,本身是不会影响到信息熵和最佳分割点的,因为在划分数据集的时候,probability的计算是针对不去重列表的。而且对于去重后的分割点列表,对于每个取值,划分数据集可以保证不会出现空值,这就极大程度地降低了程序的运行效率。

  2、划分数据集

    其实在上一步就已经使用到了划分数据集了,它没有像我上面给到的流程那样,在创建子树后才划分数据集,而是先进行划分,然后再进行创建子树,原因在于划分数据集后计算信息增益会变的更加通用,可以仅仅使用calculateEntropy()这个函数,而不需要在calculateEntropy()函数的前面增加一个划分条件,所以我们应该将“划分数据集”提前到“寻找最好的属性特征之后”立刻进行:

  

 #划分数据集
def splitDataSet(dataSet,featureIndex,value):#根据离散值划分数据集,方法同ID3决策树
newDataSet=[]
for vec in dataSet:#将选定的feature的列从数据集中去除
if vec[featureIndex] == value:
rest = vec[:featureIndex]
rest.extend(vec[featureIndex + 1:])
newDataSet.append(rest)
return newDataSet def splitContinuousDataSet(dataSet,feature,continuousValue):#根据连续值来划分数据集
biggerDataSet = []
smallerDataSet = []
for vec in dataSet:
rest = vec[:feature]
rest.extend(vec[feature + 1:])#将当前列中的feature列去除,其他的数据保留
if vec[feature]>continuousValue:#如果feature列的值比最佳分割点的大,则放在biggerDataSet中,否则放在smallerDataSet中
biggerDataSet.append(rest)
else:
smallerDataSet.append(rest)
return biggerDataSet,smallerDataSet

  划分数据集的算法被分割为离散型属性的分割和连续型属性的分割,他们也可以如上述的chooseBestSplitWay()一样写在一起,但是解构出来会显得更加明了。

  3、投票表决:

    增加投票表决这个过程主要是因为:创建分支的过程就是创建树的过程,而这个过程无论是原始数据集,还是数据集的子集,都应该是基于相同的依据来进行创建的,所以这里采用的递归的方式来创建树,这就存在一个递归的结束条件。这个算法的递归结束条件应该是:使用完所有的数据集的属性,并且已经根据所有的属性的取值构建了其所有的子树,所有的子树下都达到所有的分类。但是存在这样一种情况:已经处理了数据集的所有属性特征,但是分类标签并不是唯一的,比如孪生兄弟性格不一样,他们的所有属性特征可能相同,可是分类标签并不一样,这就需要一个算法来保证在这里能得到一个表决结果,它代表了依据这些属性特征,所能达到的分类结果中,“最有可能”出现的一个,所以采用的是投票表决的算法:

  

 #返回出现次数最多的类别,避免产生所有特征全部用完无法判断类别的情况
def majority(classList):
classificationCount = {}
for i in classList:
if not i in classificationCount.keys():
classificationCount[i] = 0
classificationCount[i] += 1
sortedClassification = sorted(dict2list(classificationCount),key = operator.itemgetter(1),reverse = True)
return sortedClassification[0][0] #dict字典转换为list列表
def dict2list(dic:dict):
keys=dic.keys()
values=dic.values()
lst=[(key,value)for key,value in zip(keys,values)]
return lst

这里唯一需要注意的是排序过程:因为dict无法进行排序,所以代码dict应该转换为list来进行排序,见dict2list()函数

4、树创建:

    树创建的过程就是将上面的局部串接成为整体的过程,它也是上面的创建分支过程的实现:

 #创建树
def createTree(dataSet,labels,labelType):
classificationList = [feature[-1] for feature in dataSet] #产生数据集中的分类列表,保存的是每一行的分类
if classificationList.count(classificationList[0]) == len(classificationList): #如果分类别表中的所有分类都是一样的,则直接返回当前的分类
return classificationList[0]
if len(dataSet[0]) == 1: #如果划分数据集已经到了无法继续划分的程度,即已经使用完了全部的feature,则进行决策
return majority(classificationList)
bestFeature,continuousValue,isContinuous = chooseBestSplitWay(dataSet,labelType) #计算香农熵和信息增益来返回最佳的划分方案,bestFeature保存最佳的划分的feature的索引,在C4.5中要判断该feature是连续型还是离散型的,continuousValue是当前的返回feature是continuous的的时候,选择的“最好的”分割点
bestFeatureLabel = labels[bestFeature] #取出上述的bestfeature的具体值
print(bestFeatureLabel)
Tree = {bestFeatureLabel:{}}
del(labels[bestFeature]) #删除当前进行划分是使用的feature避免下次继续使用到这个feature来划分
del(labelType[bestFeature])#删除labelType中的feature类型,保持和labels同步
if isContinuous == 1 :#如果要划分的feature是连续的
#构造以当前的feature作为root节点,它的连续序列中的分割点为叶子的子树
biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,bestFeature,continuousValue)#根据最佳分割点将数据集划分为两部分,一个是大于最佳分割值,一个是小于等于最佳分割值
subLabels = labels[:]
subLabelType = labelType[:]
Tree[bestFeatureLabel]['>'+str(continuousValue)] = createTree(biggerDataSet,subLabels,subLabelType)#将大于分割值的数据集加入到树中,并且递归创建这颗子树
subLabels = labels[:]
subLabelType = labelType[:]
Tree[bestFeatureLabel]['<'+str(continuousValue)] = createTree(smallerDataSet,subLabels,subLabelType)#将小于等于分割值的数据集加入到树中,并且递归创建这颗子树
else:#如果要划分的feature是非连续的,下面的步骤和ID3决策树相同
#构造以当前的feature作为root节点,它的所有存在的feature取值为叶子的子树
featureValueList = [feature[bestFeature]for feature in dataSet] #对于上述取出的bestFeature,取出数据集中属于当前feature的列的所有的值
uniqueValue = set(featureValueList) #去重
for value in uniqueValue: #对于每一个feature标签的value值,进行递归构造决策树
subLabels = labels[:]
subLabelType = labelType[:]
Tree[bestFeatureLabel][value] = createTree(splitDataSet(dataSet,bestFeature,value),subLabels,subLabelType)
return Tree

  算法同我上面所写出来的流程一样,先进行两次判断:

  1)是否余下所有的取值都是同类?

  2)是否已经用完了所有的属性特征?

  这两个判断都是终结这个递归算法的根本。而后就是取得对于“原始数据集”的最佳分割方案,然后对于这个分割方案,构建出分支,把这个方案所得到的bestFeature的所有可能的取值构建新的下属分支即子树,自此,“原始数据集”的操作就结束了,下面都是对于这个数据集进行一次或多次划分的子集的分支构建方案了。而在进行递归调用创建子树的时候,传入的labels应该是已经复制过的labels,否则,由于Python不是值传递而是引用传递的原因,在子树创建中将影响到父节点的labels。

  在创建树的过程中,也是应该分为当前所选择的最佳分割方案是连续型属性还是离散性属性,如果是离散性属性的话,操作的流程和ID3决策树应该是一样的,而如果它是连续型属性的话,创建的子树结点的储存值应该是一个二元组(属性,分割点)。

  在创建连续型属性的子树的时候有一个很致命的问题:递归。

  如同一般的树创建算法一样,我们的算法可以简单表示为

 def createTree():
Tree = {}
Tree[left] = createTree()
Tree[right] = createTree()
return Tree

  这样看来递归对于创建树算法没什么影响,然而,真正致命的问题在于参数的传递。如果没有进行参数复制的话,在创建左子树的时候,将会修改参数的值,而这些参数传递给创建右子树的函数的时候,将是“脏数据”和“错误数据”。所以在两次递归的前面,需要将参数值进行保存,并传递它的一个副本给这个递归算法,保证回溯的时候能传递正确的参数给下一个递归函数,对于不是尾递归的函数,这个问题总是会遇到的。

  看看构造出来的C4.5决策树:Python3实现机器学习经典算法(四)C4.5决策树

  这只是一部分……事实上,运行完成这棵树的耗时非常长,因为数据集非常大,在没有使用分布式的计算的前提下,我们最好要把这棵树保存在本地上,然后下次进行测试算法的时候读取离线的树,而不是再次生成,《机器学习实战》中给我们提供了这样一种保存树的方式:

  5、保存树(读取树):

 def storeTree(inputree,filename):
fw = open(filename, 'wb')
pickle.dump(inputree, fw)
fw.close() def grabTree(filename):
fr = open(filename, 'rb')
return pickle.load(fr)

  它借用pickle模块来直接将树保存下来,但是这个保存下来的树不是可视化的。

四、测试算法

  树已经构造完成了,下一步就是使用这棵树的过程了,这也是测试算法的过程。我们的树是一个字典,所以我们测试算法的过程应该是循着这个字典查值的过程:  

  1、预处理、清洗测试集

    预处理和清洗过程和上面对训练集的过程是一样的。

  2、测试过程

    测试过程需要一个classify()函数和一个count()函数。classify()函数负责将上面构造树的代码所构造出来的树接受,并且根据传入的向量进行分类,然后返回预测的分类标签,count()函数负责计算这个数据集的正确率:

  

 #测试算法
def classify(inputTree,featLabels,testVector,labelType):
root = list(inputTree.keys())[0] #取出树的第一个标签,即树的根节点
dictionary = inputTree[root] #取出树的第一个标签下的字典
featIndex = featLabels.index(root)
classLabel = '<=50K'
if labelType[featIndex] == 'uncontinuous':#如果是离散型的标签,则按照ID3决策树的方法来测试算法
for key in dictionary.keys():#对于这个字典
if testVector[featIndex] == key:
if type(dictionary[key]).__name__ == 'dict': #如果还有一个新的字典
classLabel = classify(dictionary[key],featLabels,testVector,labelType)#递归向下寻找到非字典的情况,此时是叶子节点,叶子节点保存的肯定是类别
else:
classLabel = dictionary[key]#叶子节点,返回类别
else:#如果是连续型的标签,则在取出子树的每一个分支的时候,还需要判断是>n还是<=n的情况,只有这两种情况,所以只需要判断是否是其中一种
firstBranch = list(dictionary.keys())[0] #取出第一个分支,来确定这个double值
if str(firstBranch).startswith('>'): #如果第一个分支是">n"的情况,则取出n,为1:n
number = firstBranch[1:]
else: #如果第一个分支是“<=n”的情况,则取出n,为2:n
number = firstBranch[2:]
if float(testVector[featIndex])>float(number):#如果测试向量是>n的情况
string = '>'+str(number) #设置一个判断string,它是firstBranch的还原,但是为了节省判断branch是哪一种,直接使用字符串连接的方式链接
else:
string = "<="+str(number) #设置一个判断string,它是firstBranch的还原,但是为了节省判断branch是哪一种,直接使用字符串连接的方式链接
for key in dictionary.keys():
if string == key:
if type(dictionary[key]).__name__ == 'dict':#如果还有一个新的字典
classLabel = classify(dictionary[key],featLabels,testVector,labelType)
else:
classLabel = dictionary[key]
return classLabel def test(mytree,labels,filename,labelType,mydate):
with open(filename, 'r')as csvfile:
dataset=[line.strip().split(', ') for line in csvfile.readlines()] #读取文件中的每一行
dataset=[[int(i) if i.isdigit() else i for i in row] for row in dataset] #对于每一行中的每一个元素,将行列式数字化并且去除空白保证匹配的正确完成
cleanoutdata(dataset) #数据清洗
del(dataset[0]) #删除第一行和最后一行的空白数据
del(dataset[-1])
#precondition(dataset) #预处理数据集
clean(dataset,mydate) #把测试集中的,不存在于训练集中的离散数据清洗掉
total = len(dataset)
correct = 0
error = 0
for line in dataset:
result=classify(mytree,labels,line,labelType=labelType)+'.'
if result==line[14]: #如果测试结果和类别相同
correct = correct + 1
else :
error = error + 1 return total,correct,error

  由于构建树的时候,我们采用的是字典包含字典的过程,所以当我们找到一个字典的键(Key),可以直接判断它的值(Value)是否仍旧是一个字典,如果是,则说明它下面还有分支,还有子树,否则说明这已经到达了叶子节点,可直接获取到分类标签。这个classify()也是一个递归向下查找的过程,它通过第一个参数,将树不断地进行剪枝,最后达到只剩下一个叶子节点的目的。

  创建树的时候,对于连续型属性的保存方式是(属性,分割点)的二元组,所以在测试算法的时候应该将其拆开来,读取分割点,然后进行判断来进入左子树或者右子树。

  测试结果:

    未经过修正的算法:

    Python3实现机器学习经典算法(四)C4.5决策树

    经过修正后的算法:

    Python3实现机器学习经典算法(四)C4.5决策树

  官方数据:

Python3实现机器学习经典算法(四)C4.5决策树

  在后面回过头来对这个算法进行剪枝操作兴许能提高点正确率:)

五、完整代码

  长注释部分是非修正的算法,没有将它从程序中移除,保留了另外一种实现思路。其中addFeatureValue()函数的实现我没有放上来,因为我没有将离散属性中测试集的所有取值在训练过程中加入,而是将测试集中出现了训练集中没有的取值的时候,直接将其去除。如果采用前者的方式,将会出现一条完全拟合的从根到叶子的路径,属于这一条唯一的向量。addFeatureValue()的实现思路如下:

 def addFeatureValue(featureListOfValue,feature):
for featureValue in feat[feature]: #feat保存的是所有属性特征的所有可能的取值,其结构为feat = [ [val1,val2,val3,…,valn], [], [], [], … ,[] ]
featureListOfValue.append(featureValue)

  下面是针对adult数据集的可运行完整代码:

 #encoding=utf-8
from math import log
import operator
import pickle #读取数据集
def createDateset(filename):
with open(filename, 'r')as csvfile:
dataset= [line.strip().split(', ') for line in csvfile.readlines()] #读取文件中的每一行
dataset=[[int(i) if i.isdigit() else i for i in row] for row in dataset] #对于每一行中的每一个元素,将行列式数字化并且去除空白保证匹配的正确完成
cleanoutdata(dataset) #清洗数据
del (dataset[-1]) #去除最后一行的空行
#precondition(dataset) #预处理数据
labels=['age','workclass','fnlwgt','education','education-num',
'marital-status','occupation',
'relationship','race','sex','capital-gain','capital-loss','hours-per-week',
'native-country']
labelType = ['continuous', 'uncontinuous', 'continuous',
'uncontinuous',
'continuous', 'uncontinuous',
'uncontinuous', 'uncontinuous', 'uncontinuous',
'uncontinuous', 'continuous', 'continuous',
'continuous', 'uncontinuous'] return dataset,labels,labelType def cleanoutdata(dataset):#数据清洗
for row in dataset:
for column in row:
if column == '?' or column=='':
dataset.remove(row)
break #计算香农熵/期望信息
def calculateEntropy(dataSet):
ClassifyCount = {}#分类标签统计字典,用来统计每个分类标签的概率
for vector in dataSet:
clasification = vector[-1] #获取分类
if not clasification in ClassifyCount.keys():#如果分类暂时不在字典中,在字典中添加对应的值对
ClassifyCount[clasification] = 0
ClassifyCount[clasification] += 1 #计算出现次数
shannonEntropy=0.0
for key in ClassifyCount:
probability = float(ClassifyCount[key]) / len(dataSet) #计算概率
shannonEntropy -= probability * log(probability,2) #香农熵的每一个子项都是负的
return shannonEntropy # def addFetureValue(feature): #划分数据集
def splitDataSet(dataSet,featureIndex,value):#根据离散值划分数据集,方法同ID3决策树
newDataSet=[]
for vec in dataSet:#将选定的feature的列从数据集中去除
if vec[featureIndex] == value:
rest = vec[:featureIndex]
rest.extend(vec[featureIndex + 1:])
newDataSet.append(rest)
return newDataSet def splitContinuousDataSet(dataSet,feature,continuousValue):#根据连续值来划分数据集
biggerDataSet = []
smallerDataSet = []
for vec in dataSet:
rest = vec[:feature]
rest.extend(vec[feature + 1:])#将当前列中的feature列去除,其他的数据保留
if vec[feature]>continuousValue:#如果feature列的值比最佳分割点的大,则放在biggerDataSet中,否则放在smallerDataSet中
biggerDataSet.append(rest)
else:
smallerDataSet.append(rest)
return biggerDataSet,smallerDataSet #连续型属性不需要将训练集中有,测试集中没有的值补全,离散性属性需要
def addFeatureValue(featureListOfValue,feature):
feat = [[ 'Private', 'Self-emp-not-inc', 'Self-emp-inc',
'Federal-gov', 'Local-gov', 'State-gov', 'Without-pay', 'Never-worked'],
[],[],[],[],[]]
for featureValue in feat[feature]: #feat保存的是所有属性特征的所有可能的取值,其结构为feat = [ [val1,val2,val3,…,valn], [], [], [], … ,[] ]
featureListOfValue.append(featureValue) def calGainRatioUnContinuous(dataSet,feature,HC):
# addFeatureValue(featureListOfValue,feature) #增加在训练集中有,测试集中没有的属性特征的取值
featureListOfValue = [vector[feature] for vector in dataSet] # 对于dataset中每一个feature,创建单独的列表list保存其取值,其中是不重复的
unique = set(featureListOfValue)
HTC = 0.0 # 保存HTC,即H(T|C)
split = 0.0 # 保存split(T)
for value in unique:
subDataSet = splitDataSet(dataSet, feature, value) # 划分数据集
probability = len(subDataSet) / float(len(dataSet)) # 求得当前类别的概率
split -= probability * log(probability, 2) # 计算split(T)
HTC += probability * calculateEntropy(subDataSet) # 计算当前类别的香农熵,并和HTC想加,即H(T|C) = H(T1|C)+ H(T2|C) + … + H(TN|C)
IG = HC - HTC # 计算对于该种划分方式的信息增益
if split == 0:
split = 1
gainRatio = float(IG) / float(split) # 计算对于该种划分方式的信息增益率
return gainRatio def calGainRatioContinuous(dataSet,feature,HC):
featureListOfValue = [vector[feature] for vector in dataSet] # 对于dataset中每一个feature,创建单独的列表list保存其取值,其中是不重复的
featureListOfValue = set(featureListOfValue)
sortedValue = sorted(featureListOfValue)
splitPoint = []
IGMAX = 0.0
GR = 0.0
continuousValue = 0.0
for i in range(len(sortedValue)-1):#n个value,应该有n-1个分割点splitPoint
splitPoint.append((float(sortedValue[i])+float(sortedValue[i+1]))/2.0)
for i in range(len(splitPoint)):
HTC = 0.0
split = 0.0
biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i])
probabilityBig = len(biggerDataSet) / len(dataSet)
probabilitySmall = len(smallerDataSet) / len(dataSet)
HTC += probabilityBig * calculateEntropy(biggerDataSet)
HTC += probabilitySmall * calculateEntropy(smallerDataSet)
IG = HC - HTC
if IG>IGMAX:
IGMAX = IG
split -= probabilityBig * log(probabilityBig, 2)
split -= probabilitySmall * log(probabilitySmall, 2)
continuousValue = splitPoint[i]
N = len(featureListOfValue)
D = len(dataSet)
IG -= log(N - 1, 2) / abs(D)
GR = float(IG) / float(split)
return GR,continuousValue #选择最好的数据集划分方式
def chooseBestSplitWay(dataSet,labelType):
isContinuous = -1 #判断是否是连续值,是为1,不是为0
HC = calculateEntropy(dataSet)#计算整个数据集的香农熵(期望信息),即H(C),用来和每个feature的香农熵进行比较
bestfeatureIndex = -1 #最好的划分方式的索引值,因为0也是索引值,所以应该设置为负数
GRMAX=0.0 #信息增益率=(期望信息-熵)/分割获得的信息增益,即为GR = IG / split = ( HC - HTC )/ split , gainRatioMax为最好的信息增益率,IG为各种划分方式的信息增益
continuousValue = -1 #设置如果是连续值的属性返回的最好的划分方式的最好分割点的值
for feature in range(len(dataSet[0]) -1 ): #计算feature的个数,由于dataset中是包含有类别的,所以要减去类别
if labelType[feature] == 'uncontinuous':
GR = calGainRatioUnContinuous(dataSet,feature,HC)
if GR>GRMAX:
GRMAX = GR
bestfeatureIndex = feature
isContinuous = 0
else: #如果feature是连续型的
GR ,bestSplitPoint = calGainRatioContinuous(dataSet,feature,HC)
if GR>GRMAX:
GRMAX = GR
continuousValue = bestSplitPoint
isContinuous = 1
bestfeatureIndex = feature
return bestfeatureIndex,continuousValue,isContinuous
# featureListOfValue = set(featureListOfValue)
# sortedValue = sorted(featureListOfValue)
# splitPoint = []
# for i in range(len(sortedValue)-1):#n个value,应该有n-1个分割点splitPoint
# splitPoint.append((float(sortedValue[i])+float(sortedValue[i+1]))/2.0) #C4.5修正,不再使用信息增益率来选择最佳分割点
# for i in range(len(splitPoint)): #对于n-1个分割点,计算每个分割点的信息增益率,来选择最佳分割点
# HTC = 0.0
# split = 0.0
# gainRatio = 0.0
# biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i])
# probabilityBig = len(biggerDataSet)/len(dataSet)
# probabilitySmall = len(smallerDataSet)/len(dataSet)
# HTC += probabilityBig * calculateEntropy(biggerDataSet)
# HTC += probabilityBig * calculateEntropy(smallerDataSet)
# if probabilityBig != 0:
# split -= probabilityBig * log(probabilityBig,2)
# if probabilitySmall != 0:
# split -= probabilitySmall *log(probabilitySmall,2)
# IG = HC - HTC
# if split == 0:
# split = 1;
# gainRatio = IG/split
# if gainRatio>gainRatioMax:
# isContinuous = 1
# gainRatioMax = gainRatio
# bestfeatureIndex = feature
# continuousValue = splitPoint[i]
# IGMAX = 0.0
# for i in range(len(splitPoint)):
# HTC = 0.0
# split = 0.0
# biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i])
# probabilityBig = len(biggerDataSet) / len(dataSet)
# probabilitySmall = len(smallerDataSet) / len(dataSet)
# HTC += probabilityBig * calculateEntropy(biggerDataSet)
# HTC += probabilitySmall * calculateEntropy(smallerDataSet)
# IG = HC - HTC
# if IG>IGMAX:
# split -= probabilityBig * log(probabilityBig, 2)
# split -= probabilitySmall * log(probabilitySmall, 2)
# IGMAX = IG
# continuousValue = splitPoint[i]
# N = len(splitPoint)
# D = len(dataSet)
# IG -= log(N - 1, 2) / abs(D)
# GR = float(IG) / float(split)
# if GR > GRMAX:
# isContinuous = 1
# GRMAX = GR
# bestfeatureIndex = feature
# return bestfeatureIndex,continuousValue,isContinuous #返回出现次数最多的类别,避免产生所有特征全部用完无法判断类别的情况
def majority(classList):
classificationCount = {}
for i in classList:
if not i in classificationCount.keys():
classificationCount[i] = 0
classificationCount[i] += 1
sortedClassification = sorted(dict2list(classificationCount),key = operator.itemgetter(1),reverse = True)
return sortedClassification[0][0] #dict字典转换为list列表
def dict2list(dic:dict):
keys=dic.keys()
values=dic.values()
lst=[(key,value)for key,value in zip(keys,values)]
return lst #创建树
def createTree(dataSet,labels,labelType):
classificationList = [feature[-1] for feature in dataSet] #产生数据集中的分类列表,保存的是每一行的分类
if classificationList.count(classificationList[0]) == len(classificationList): #如果分类别表中的所有分类都是一样的,则直接返回当前的分类
return classificationList[0]
if len(dataSet[0]) == 1: #如果划分数据集已经到了无法继续划分的程度,即已经使用完了全部的feature,则进行决策
return majority(classificationList)
bestFeature,continuousValue,isContinuous = chooseBestSplitWay(dataSet,labelType) #计算香农熵和信息增益来返回最佳的划分方案,bestFeature保存最佳的划分的feature的索引,在C4.5中要判断该feature是连续型还是离散型的,continuousValue是当前的返回feature是continuous的的时候,选择的“最好的”分割点
bestFeatureLabel = labels[bestFeature] #取出上述的bestfeature的具体值
print(bestFeatureLabel)
Tree = {bestFeatureLabel:{}}
del(labels[bestFeature]) #删除当前进行划分是使用的feature避免下次继续使用到这个feature来划分
del(labelType[bestFeature])#删除labelType中的feature类型,保持和labels同步
if isContinuous == 1 :#如果要划分的feature是连续的
#构造以当前的feature作为root节点,它的连续序列中的分割点为叶子的子树
biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,bestFeature,continuousValue)#根据最佳分割点将数据集划分为两部分,一个是大于最佳分割值,一个是小于等于最佳分割值
subLabels = labels[:]
subLabelType = labelType[:]
Tree[bestFeatureLabel]['>'+str(continuousValue)] = createTree(biggerDataSet,subLabels,subLabelType)#将大于分割值的数据集加入到树中,并且递归创建这颗子树
subLabels = labels[:]
subLabelType = labelType[:]
Tree[bestFeatureLabel]['<'+str(continuousValue)] = createTree(smallerDataSet,subLabels,subLabelType)#将小于等于分割值的数据集加入到树中,并且递归创建这颗子树
else:#如果要划分的feature是非连续的,下面的步骤和ID3决策树相同
#构造以当前的feature作为root节点,它的所有存在的feature取值为叶子的子树
featureValueList = [feature[bestFeature]for feature in dataSet] #对于上述取出的bestFeature,取出数据集中属于当前feature的列的所有的值
uniqueValue = set(featureValueList) #去重
for value in uniqueValue: #对于每一个feature标签的value值,进行递归构造决策树
subLabels = labels[:]
subLabelType = labelType[:]
Tree[bestFeatureLabel][value] = createTree(splitDataSet(dataSet,bestFeature,value),subLabels,subLabelType)
return Tree def storeTree(inputree,filename):
fw = open(filename, 'wb')
pickle.dump(inputree, fw)
fw.close() def grabTree(filename):
fr = open(filename, 'rb')
return pickle.load(fr) #测试算法
def classify(inputTree,featLabels,testVector,labelType):
root = list(inputTree.keys())[0] #取出树的第一个标签,即树的根节点
dictionary = inputTree[root] #取出树的第一个标签下的字典
featIndex = featLabels.index(root)
classLabel = '<=50K'
if labelType[featIndex] == 'uncontinuous':#如果是离散型的标签,则按照ID3决策树的方法来测试算法
for key in dictionary.keys():#对于这个字典
if testVector[featIndex] == key:
if type(dictionary[key]).__name__ == 'dict': #如果还有一个新的字典
classLabel = classify(dictionary[key],featLabels,testVector,labelType)#递归向下寻找到非字典的情况,此时是叶子节点,叶子节点保存的肯定是类别
else:
classLabel = dictionary[key]#叶子节点,返回类别
else:#如果是连续型的标签,则在取出子树的每一个分支的时候,还需要判断是>n还是<=n的情况,只有这两种情况,所以只需要判断是否是其中一种
firstBranch = list(dictionary.keys())[0] #取出第一个分支,来确定这个double值
if str(firstBranch).startswith('>'): #如果第一个分支是">n"的情况,则取出n,为1:n
number = firstBranch[1:]
else: #如果第一个分支是“<=n”的情况,则取出n,为2:n
number = firstBranch[2:]
if float(testVector[featIndex])>float(number):#如果测试向量是>n的情况
string = '>'+str(number) #设置一个判断string,它是firstBranch的还原,但是为了节省判断branch是哪一种,直接使用字符串连接的方式链接
else:
string = "<="+str(number) #设置一个判断string,它是firstBranch的还原,但是为了节省判断branch是哪一种,直接使用字符串连接的方式链接
for key in dictionary.keys():
if string == key:
if type(dictionary[key]).__name__ == 'dict':#如果还有一个新的字典
classLabel = classify(dictionary[key],featLabels,testVector,labelType)
else:
classLabel = dictionary[key]
return classLabel def test(mytree,labels,filename,labelType,mydate):
with open(filename, 'r')as csvfile:
dataset=[line.strip().split(', ') for line in csvfile.readlines()] #读取文件中的每一行
dataset=[[int(i) if i.isdigit() else i for i in row] for row in dataset] #对于每一行中的每一个元素,将行列式数字化并且去除空白保证匹配的正确完成
cleanoutdata(dataset) #数据清洗
del(dataset[0]) #删除第一行和最后一行的空白数据
del(dataset[-1])
#precondition(dataset) #预处理数据集
clean(dataset,mydate) #把测试集中的,不存在于训练集中的离散数据清洗掉
total = len(dataset)
correct = 0
error = 0
for line in dataset:
result=classify(mytree,labels,line,labelType=labelType)+'.'
if result==line[14]: #如果测试结果和类别相同
correct = correct + 1
else :
error = error + 1 return total,correct,error #C4.5决策树不需要清洗掉连续性数据
# def precondition(mydate):#清洗连续型数据
# #continuous:0,2,4,10,11,12
# for each in mydate:
# del(each[0])
# del(each[1])
# del(each[2])
# del(each[7])
# del(each[7])
# del(each[7]) #C4.5决策树不需要清洗掉测试集中连续值出现了训练集中没有的值的情况,但是离散数据集中还是需要清洗的
def clean(dataset,mydate):#清洗掉测试集中出现了训练集中没有的值的情况
for i in [1,3,5,6,7,8,9,13]:
set1=set()
for row1 in mydate:
set1.add(row1[i])
for row2 in dataset:
if row2[i] not in set1:
dataset.remove(row2)
set1.clear() def main():
dataSetName = r"C:\Users\yang\Desktop\adult.data"
mydate, label ,labelType= createDateset(dataSetName)
labelList = label[:]
labelTypeList = labelType[:]
Tree = createTree(mydate, labelList,labelType=labelTypeList)
print(Tree)
storeTree(Tree, r'C:\Users\yang\Desktop\tree.txt') # 保存决策树,避免下次再生成决策树 #Tree=grabTree(r'C:\Users\yang\Desktop\tree.txt')#读取决策树,如果已经存在tree.txt可以直接使用决策树不需要再次生成决策树
total, correct, error = test(Tree, label, r'C:\Users\yang\Desktop\adult.test',labelType,mydate)
# with open(r'C:\Users\yang\Desktop\trees.txt', 'w')as f:
# f.write(str(Tree))
accuracy = float(correct)/float(total)
print("准确率:%f" % accuracy) if __name__ == '__main__':
main()

六、总结

  太慢了!!!太慢了!!!太慢了!!!

  C4.5是真的非常非常非常低效!低效!低效!

  不过对比ID3的优点还是非常显著的,尤其在能处理既有离散型属性又有连续型属性的数据集的能力上,很强。

  树的深度也比ID3那种单纯的快速分割数据集的增长不同,不会产生“过拟合”的情况,不过ID3决策树是数据集划分得太快,C4.5是划分的太慢。

  C4.5代码放在GitHub:https://github.com/hahahaha1997/C4.5DecisionTree

  转载注明出处:https://www.cnblogs.com/DawnSwallow/p/9622398.html