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

1、C4.5决策树概述html

  C4.5决策树是ID3决策树的改进算法,它解决了ID3决策树没法处理连续型数据的问题以及ID3决策树在使用信息增益划分数据集的时候倾向于选择属性分支更多的属性的问题。它的大部分流程和ID3决策树是相同的或者类似的,能够参考个人上一篇博客:http://www.javashuo.com/article/p-rqvlfpnq-dq.htmlgit

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

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

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

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

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

 

 

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

复制代码
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]#这里不是减法,而是在数据集中去除该列
复制代码

  由上面的伪代码,也能够理解到“信息增益最大的时候,熵减最多”。这里的数学理解就是:信息增益的公式能够看做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所获得的信息增益率/信息增益比为:学习

 

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

  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是训练数据数目)

 

2、准备数据集

  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, Taiwan, Haiti, Columbia, Hungary, Guatemala, Nicaragua, Scotland, Thailand, Yugoslavia, El-Salvador, Trinadad&Tobago, Peru, Hong, Holand-Netherlands.

  

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

  

下一步是分析数据:

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

二、数据清洗:

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

 
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)很是快速,可是这种修改迭代器的方式会引发编译器的报错,对于这种报错能够选择修改编译器使其忽略,可是不建议使用这种回退迭代器的写法。

 

三、数据归一化:

  决策树这样的概念模型不须要进行数据归一化,由于它关心的是向量的分布状况和向量之间的条件几率而不是变量的值,进行数据归一化更难以进行划分数据集,由于Double类型的判等很是难作且不许确。

四、数据集读入:

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

  

 1 #读取数据集
 2 def createDateset(filename):
 3     with open(filename, 'r')as csvfile:
 4         dataset= [line.strip().split(', ') for line in csvfile.readlines()]     #读取文件中的每一行
 5         dataset=[[int(i) if i.isdigit() else i for i in row] for row in dataset]    #对于每一行中的每个元素,将行列式数字化而且去除空白保证匹配的正确完成
 6         cleanoutdata(dataset)   #清洗数据
 7         del (dataset[-1])       #去除最后一行的空行
 8         #precondition(dataset)   #预处理数据
 9         labels=['age','workclass','fnlwgt','education','education-num',
10                'marital-status','occupation',
11                 'relationship','race','sex','capital-gain','capital-loss','hours-per-week',
12                 'native-country']
13         labelType = ['continuous', 'uncontinuous', 'continuous',
14                      'uncontinuous',
15                      'continuous', 'uncontinuous',
16                      'uncontinuous', 'uncontinuous', 'uncontinuous',
17                      'uncontinuous', 'continuous', 'continuous',
18                      'continuous', 'uncontinuous']
19 
20         return dataset,labels,labelType
21 
22 def cleanoutdata(dataset):#数据清洗
23     for row in dataset:
24         for column in row:
25             if column == '?' or column=='':
26                 dataset.remove(row)
27                 break

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

3、训练算法

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

  用伪代码表示以下:

  if 数据集中全部的向量属于同一分类:

    return 分类标签

  else:

    if 属性特征已经使用完:

      进行投票决策

      return 票数最多的分类标签

    else:

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

      根据属性特征T建立分支

      if 属性为连续型属性:

        读入取值序列并升序排列

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

      else:

        for 属性特征T的每一个取值

          成为当前树分支的子树

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

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

  

  根据上面的伪代码,能够获得下面一步一部的训练算法流程,其中不少的过程和在ID3决策树中的过程是类似的甚至如出一辙的。

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

  1 #计算香农熵/指望信息
  2 def calculateEntropy(dataSet):
  3     ClassifyCount = {}#分类标签统计字典,用来统计每一个分类标签的几率
  4     for vector in dataSet:
  5         clasification = vector[-1]  #获取分类
  6         if not clasification in ClassifyCount.keys():#若是分类暂时不在字典中,在字典中添加对应的值对
  7             ClassifyCount[clasification] = 0
  8         ClassifyCount[clasification] += 1         #计算出现次数
  9     shannonEntropy=0.0
 10     for key in ClassifyCount:
 11         probability = float(ClassifyCount[key]) / len(dataSet)      #计算几率
 12         shannonEntropy -= probability * log(probability,2)   #香农熵的每个子项都是负的
 13     return shannonEntropy
 14 
 15 #连续型属性不须要将训练集中有,测试集中没有的值补全,离散性属性须要
 16 # def addFeatureValue(featureListOfValue,feature):
 17 #     feat = [[ 'Private', 'Self-emp-not-inc', 'Self-emp-inc',
 18 #               'Federal-gov', 'Local-gov', 'State-gov', 'Without-pay', 'Never-worked'],
 19 #             [],[],[],[],[]]
 20 #     for featureValue in feat[feature]: #feat保存的是全部属性特征的全部可能的取值,其结构为feat = [ [val1,val2,val3,…,valn], [], [], [], … ,[] ]
 21 #         featureListOfValue.append(featureValue)
 22 
 23 #选择最好的数据集划分方式
 24 def chooseBestSplitWay(dataSet,labelType):
 25     isContinuous = -1 #判断是不是连续值,是为1,不是为0
 26     HC = calculateEntropy(dataSet)#计算整个数据集的香农熵(指望信息),即H(C),用来和每一个feature的香农熵进行比较
 27     bestfeatureIndex = -1                   #最好的划分方式的索引值,由于0也是索引值,因此应该设置为负数
 28     gainRatioMax=0.0                        #信息增益率=(指望信息-熵)/分割得到的信息增益,即为GR = IG / split = ( HC - HTC )/ split , gainRatioMax为最好的信息增益率,IG为各类划分方式的信息增益
 29 
 30     continuousValue = -1 #设置若是是连续值的属性返回的最好的划分方式的最好分割点的值
 31     for feature in range(len(dataSet[0]) -1 ): #计算feature的个数,因为dataset中是包含有类别的,因此要减去类别
 32         featureListOfValue=[vector[feature] for vector in dataSet] #对于dataset中每个feature,建立单独的列表list保存其取值,其中是不重复的
 33         addFeatureValue(featureListOfValue,feature) #增长在训练集中有,测试集中没有的属性特征的取值
 34         if labelType[feature] == 'uncontinuous':
 35             unique=set(featureListOfValue)
 36             HTC=0.0 #保存HTC,即H(T|C)
 37             split = 0.0 #保存split(T)
 38             for value in unique:
 39                 subDataSet = splitDataSet(dataSet,feature,value) #划分数据集
 40                 probability = len(subDataSet) / float(len(dataSet)) #求得当前类别的几率
 41                 split -= probability * log(probability,2) #计算split(T)
 42                 HTC += probability * calculateEntropy(subDataSet) #计算当前类别的香农熵,并和HTC想加,即H(T|C) = H(T1|C)+ H(T2|C) + … + H(TN|C)
 43             IG=HC-HTC #计算对于该种划分方式的信息增益
 44             if split == 0:
 45                 split = 1
 46             gainRatio = float(IG)/float(split) #计算对于该种划分方式的信息增益率
 47             if gainRatio > gainRatioMax :
 48                 isContinuous = 0
 49                 gainRatioMax = gainRatio
 50                 bestfeatureIndex = feature
 51         else: #若是feature是连续型的
 52             featureListOfValue  = set(featureListOfValue)
 53             sortedValue = sorted(featureListOfValue)
 54             splitPoint = []
 55             for i in range(len(sortedValue)-1):#n个value,应该有n-1个分割点splitPoint
 56                 splitPoint.append((float(sortedValue[i])+float(sortedValue[i+1]))/2.0)
 57 
 58             #C4.5修正,再也不使用信息增益率来选择最佳分割点
 59             # for i in range(len(splitPoint)): #对于n-1个分割点,计算每一个分割点的信息增益率,来选择最佳分割点
 60             #     HTC = 0.0
 61             #     split = 0.0
 62             #     gainRatio = 0.0
 63             #     biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i])
 64             #     print(i)
 65             #     probabilityBig = len(biggerDataSet)/len(dataSet)
 66             #     probabilitySmall = len(smallerDataSet)/len(dataSet)
 67             #     HTC += probabilityBig * calculateEntropy(biggerDataSet)
 68             #     HTC += probabilityBig * calculateEntropy(smallerDataSet)
 69             #     if probabilityBig != 0:
 70             #         split -= probabilityBig * log(probabilityBig,2)
 71             #     if probabilitySmall != 0:
 72             #         split -= probabilitySmall *log(probabilitySmall,2)
 73             #     IG = HC - HTC
 74             #     if split == 0:
 75             #         split = 1;
 76             #     gainRatio = IG/split
 77             #     if gainRatio>gainRatioMax:
 78             #         isContinuous = 1
 79             #         gainRatioMax = gainRatio
 80             #         bestfeatureIndex = feature
 81             #         continuousValue = splitPoint[i]
 82             IGMAX = 0.0
 83             for i in range(len(splitPoint)):
 84                 HTC = 0.0
 85                 split = 0.0
 86 
 87                 biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i])
 88                 probabilityBig = len(biggerDataSet) / len(dataSet)
 89                 probabilitySmall = len(smallerDataSet) / len(dataSet)
 90                 HTC += probabilityBig * calculateEntropy(biggerDataSet)
 91                 HTC += probabilityBig * calculateEntropy(smallerDataSet)
 92                 if probabilityBig != 0:
 93                     split -= probabilityBig * log(probabilityBig, 2)
 94                 if probabilitySmall != 0:
 95                     split -= probabilitySmall * log(probabilitySmall, 2)
 96                 IG = HC - HTC
 97                 if IG>IGMAX:
 98                     IGMAX = IG
 99                     continuousValue = splitPoint[i]
100                     N = len(splitPoint)
101                     D = len(dataSet)
102                     IG -= log(featureListOfValue - 1, 2) / abs(D)
103                     GR = float(IG) / float(split)
104                     if GR > gainRatioMax:
105                         isContinuous = 1
106                         gainRatioMax = GR
107                         bestfeatureIndex = feature
108 
109         return bestfeatureIndex,continuousValue,isContinuous

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

  1)信息增益的计算:

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

  因此咱们能够反复调用calculateEntropy()函数,而后对于每一次计算结果进行累加,这就能够获得HTC。

  2)addFeatureValue()函数

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

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

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

  

 1         else: #若是feature是连续型的
 2             featureListOfValue  = set(featureListOfValue)
 3             sortedValue = sorted(featureListOfValue)
 4             splitPoint = []
 5             for i in range(len(sortedValue)-1):#n个value,应该有n-1个分割点splitPoint
 6                 splitPoint.append((float(sortedValue[i])+float(sortedValue[i+1]))/2.0)
 7 
 8             #C4.5修正,再也不使用信息增益率来选择最佳分割点
 9             for i in range(len(splitPoint)): #对于n-1个分割点,计算每一个分割点的信息增益率,来选择最佳分割点
10                 HTC = 0.0
11                 split = 0.0
12                 gainRatio = 0.0
13                 biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i])
14                 print(i)
15                 probabilityBig = len(biggerDataSet)/len(dataSet)
16                 probabilitySmall = len(smallerDataSet)/len(dataSet)
17                 HTC += probabilityBig * calculateEntropy(biggerDataSet)
18                 HTC += probabilityBig * calculateEntropy(smallerDataSet)
19                 if probabilityBig != 0:
20                     split -= probabilityBig * log(probabilityBig,2)
21                 if probabilitySmall != 0:
22                     split -= probabilitySmall *log(probabilitySmall,2)
23                 IG = HC - HTC
24                 if split == 0:
25                     split = 1;
26                 gainRatio = IG/split
27                 if gainRatio>gainRatioMax:
28                     isContinuous = 1
29                     gainRatioMax = gainRatio
30                     bestfeatureIndex = feature
31                     continuousValue = splitPoint[i]
32         return bestfeatureIndex,continuousValue,isContinuous

  这段代码自己是没有错误的,它也能根据连续型属性的非修正算法来进行划分,可是它的问题在于,它老是优先选择连续型属性来做为划分描述属性,以下所示:

  

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

  因此根据上述的“修正”过程,获得下面的修正代码:

  

 1         else: #若是feature是连续型的
 2             featureListOfValue  = set(featureListOfValue)
 3             sortedValue = sorted(featureListOfValue)
 4             splitPoint = []
 5             for i in range(len(sortedValue)-1):#n个value,应该有n-1个分割点splitPoint
 6                 splitPoint.append((float(sortedValue[i])+float(sortedValue[i+1]))/2.0)
 7 
 8             #C4.5修正,再也不使用信息增益率来选择最佳分割点
 9             IGMAX = 0.0
10             for i in range(len(splitPoint)):
11                 HTC = 0.0
12                 split = 0.0
13 
14                 biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i])
15                 probabilityBig = len(biggerDataSet) / len(dataSet)
16                 probabilitySmall = len(smallerDataSet) / len(dataSet)
17                 HTC += probabilityBig * calculateEntropy(biggerDataSet)
18                 HTC += probabilityBig * calculateEntropy(smallerDataSet)
19                 if probabilityBig != 0:
20                     split -= probabilityBig * log(probabilityBig, 2)
21                 if probabilitySmall != 0:
22                     split -= probabilitySmall * log(probabilitySmall, 2)
23                 IG = HC - HTC
24                 if IG>IGMAX:
25                     IGMAX = IG
26                     continuousValue = splitPoint[i]
27                     N = len(featureListOfValue)
28                     D = len(dataSet)
29                     IG -= log(N - 1, 2) / abs(D)
30                     GR = float(IG) / float(split)
31                     if GR > gainRatioMax:
32                         isContinuous = 1
33                         gainRatioMax = GR
34                         bestfeatureIndex = feature
35 
36 
37         return bestfeatureIndex,continuousValue,isContinuous

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

 

  因为 将连续性数据和离散型数据的处理方式统一的放在同一个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的计算是针对不去重列表的。并且对于去重后的分割点列表,对于每一个取值,划分数据集能够保证不会出现空值,这就极大程度地下降了程序的运行效率

  二、划分数据集

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

  

 1 #划分数据集
 2 def splitDataSet(dataSet,featureIndex,value):#根据离散值划分数据集,方法同ID3决策树
 3     newDataSet=[]
 4     for vec in dataSet:#将选定的feature的列从数据集中去除
 5         if vec[featureIndex] == value:
 6             rest = vec[:featureIndex]
 7             rest.extend(vec[featureIndex + 1:])
 8             newDataSet.append(rest)
 9     return newDataSet
10 
11 
12 def splitContinuousDataSet(dataSet,feature,continuousValue):#根据连续值来划分数据集
13     biggerDataSet = []
14     smallerDataSet = []
15     for vec in dataSet:
16         rest = vec[:feature]
17         rest.extend(vec[feature + 1:])#将当前列中的feature列去除,其余的数据保留
18         if vec[feature]>continuousValue:#若是feature列的值比最佳分割点的大,则放在biggerDataSet中,不然放在smallerDataSet中
19             biggerDataSet.append(rest)
20         else:
21             smallerDataSet.append(rest)
22     return biggerDataSet,smallerDataSet

 

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

  三、投票表决:

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

  

 1 #返回出现次数最多的类别,避免产生全部特征所有用完没法判断类别的状况
 2 def majority(classList):
 3     classificationCount = {}
 4     for i in classList:
 5         if not i in classificationCount.keys():
 6             classificationCount[i] = 0
 7         classificationCount[i] += 1
 8     sortedClassification = sorted(dict2list(classificationCount),key = operator.itemgetter(1),reverse = True)
 9     return sortedClassification[0][0]
10 
11 #dict字典转换为list列表
12 def dict2list(dic:dict):
13     keys=dic.keys()
14     values=dic.values()
15     lst=[(key,value)for key,value in zip(keys,values)]
16     return lst

 

这里惟一须要注意的是排序过程:由于dict没法进行排序,因此代码dict应该转换为list来进行排序,见dict2list()函数

 

四、树建立:

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

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

 

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

  1)是否余下全部的取值都是同类?

  2)是否已经用完了全部的属性特征?

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

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

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

  如同通常的树建立算法同样,咱们的算法能够简单表示为

1 def createTree():
2     Tree = {}
3     Tree[left] = createTree()
4     Tree[right] = createTree()
5     return Tree

 

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

   看看构造出来的C4.5决策树:

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

  五、保存树(读取树):

1 def storeTree(inputree,filename):
2     fw = open(filename, 'wb')
3     pickle.dump(inputree, fw)
4     fw.close()
5 
6 def grabTree(filename):
7     fr = open(filename, 'rb')
8     return pickle.load(fr)

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

 

4、测试算法

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

  一、预处理、清洗测试集

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

  二、测试过程

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

  

 1 #测试算法
 2 def classify(inputTree,featLabels,testVector,labelType):
 3     root = list(inputTree.keys())[0] #取出树的第一个标签,即树的根节点
 4     dictionary = inputTree[root] #取出树的第一个标签下的字典
 5     featIndex = featLabels.index(root)
 6     classLabel = '<=50K'
 7     if labelType[featIndex] == 'uncontinuous':#若是是离散型的标签,则按照ID3决策树的方法来测试算法
 8         for key in dictionary.keys():#对于这个字典
 9             if testVector[featIndex] == key:
10                 if type(dictionary[key]).__name__ == 'dict': #若是还有一个新的字典
11                     classLabel = classify(dictionary[key],featLabels,testVector,labelType)#递归向下寻找到非字典的状况,此时是叶子节点,叶子节点保存的确定是类别
12                 else:
13                     classLabel = dictionary[key]#叶子节点,返回类别
14     else:#若是是连续型的标签,则在取出子树的每个分支的时候,还须要判断是>n仍是<=n的状况,只有这两种状况,因此只须要判断是不是其中一种
15         firstBranch = list(dictionary.keys())[0] #取出第一个分支,来肯定这个double值
16         if str(firstBranch).startswith('>'): #若是第一个分支是">n"的状况,则取出n,为1:n
17             number = firstBranch[1:]
18         else: #若是第一个分支是“<=n”的状况,则取出n,为2:n
19             number = firstBranch[2:]
20         if float(testVector[featIndex])>float(number):#若是测试向量是>n的状况
21             string = '>'+str(number) #设置一个判断string,它是firstBranch的还原,可是为了节省判断branch是哪种,直接使用字符串链接的方式连接
22         else:
23             string = "<="+str(number) #设置一个判断string,它是firstBranch的还原,可是为了节省判断branch是哪种,直接使用字符串链接的方式连接
24         for key in dictionary.keys():
25             if string == key:
26                 if type(dictionary[key]).__name__ == 'dict':#若是还有一个新的字典
27                     classLabel = classify(dictionary[key],featLabels,testVector,labelType)
28                 else:
29                     classLabel = dictionary[key]
30     return classLabel
31 
32 def test(mytree,labels,filename,labelType,mydate):
33     with open(filename, 'r')as csvfile:
34         dataset=[line.strip().split(', ') for line in csvfile.readlines()]     #读取文件中的每一行
35         dataset=[[int(i) if i.isdigit() else i for i in row] for row in dataset]    #对于每一行中的每个元素,将行列式数字化而且去除空白保证匹配的正确完成
36         cleanoutdata(dataset)   #数据清洗
37         del(dataset[0])         #删除第一行和最后一行的空白数据
38         del(dataset[-1])
39         #precondition(dataset)       #预处理数据集
40         clean(dataset,mydate)          #把测试集中的,不存在于训练集中的离散数据清洗掉
41         total = len(dataset)
42         correct = 0
43         error = 0
44     for line in dataset:
45         result=classify(mytree,labels,line,labelType=labelType)+'.'
46         if result==line[14]:     #若是测试结果和类别相同
47             correct = correct + 1
48         else :
49             error = error + 1
50 
51     return total,correct,error

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

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

  测试结果:

    未通过修正的算法:

    

    通过修正后的算法:

    

  官方数据:

 

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

5、完整代码

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

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

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

  1 #encoding=utf-8
  2 from math import log
  3 import operator
  4 import pickle
  5 
  6 #读取数据集
  7 def createDateset(filename):
  8     with open(filename, 'r')as csvfile:
  9         dataset= [line.strip().split(', ') for line in csvfile.readlines()]     #读取文件中的每一行
 10         dataset=[[int(i) if i.isdigit() else i for i in row] for row in dataset]    #对于每一行中的每个元素,将行列式数字化而且去除空白保证匹配的正确完成
 11         cleanoutdata(dataset)   #清洗数据
 12         del (dataset[-1])       #去除最后一行的空行
 13         #precondition(dataset)   #预处理数据
 14         labels=['age','workclass','fnlwgt','education','education-num',
 15                'marital-status','occupation',
 16                 'relationship','race','sex','capital-gain','capital-loss','hours-per-week',
 17                 'native-country']
 18         labelType = ['continuous', 'uncontinuous', 'continuous',
 19                      'uncontinuous',
 20                      'continuous', 'uncontinuous',
 21                      'uncontinuous', 'uncontinuous', 'uncontinuous',
 22                      'uncontinuous', 'continuous', 'continuous',
 23                      'continuous', 'uncontinuous']
 24 
 25         return dataset,labels,labelType
 26 
 27 def cleanoutdata(dataset):#数据清洗
 28     for row in dataset:
 29         for column in row:
 30             if column == '?' or column=='':
 31                 dataset.remove(row)
 32                 break
 33 
 34 #计算香农熵/指望信息
 35 def calculateEntropy(dataSet):
 36     ClassifyCount = {}#分类标签统计字典,用来统计每一个分类标签的几率
 37     for vector in dataSet:
 38         clasification = vector[-1]  #获取分类
 39         if not clasification in ClassifyCount.keys():#若是分类暂时不在字典中,在字典中添加对应的值对
 40             ClassifyCount[clasification] = 0
 41         ClassifyCount[clasification] += 1         #计算出现次数
 42     shannonEntropy=0.0
 43     for key in ClassifyCount:
 44         probability = float(ClassifyCount[key]) / len(dataSet)      #计算几率
 45         shannonEntropy -= probability * log(probability,2)   #香农熵的每个子项都是负的
 46     return shannonEntropy
 47 
 48 # def addFetureValue(feature):
 49 
 50 #划分数据集
 51 def splitDataSet(dataSet,featureIndex,value):#根据离散值划分数据集,方法同ID3决策树
 52     newDataSet=[]
 53     for vec in dataSet:#将选定的feature的列从数据集中去除
 54         if vec[featureIndex] == value:
 55             rest = vec[:featureIndex]
 56             rest.extend(vec[featureIndex + 1:])
 57             newDataSet.append(rest)
 58     return newDataSet
 59 
 60 
 61 def splitContinuousDataSet(dataSet,feature,continuousValue):#根据连续值来划分数据集
 62     biggerDataSet = []
 63     smallerDataSet = []
 64     for vec in dataSet:
 65         rest = vec[:feature]
 66         rest.extend(vec[feature + 1:])#将当前列中的feature列去除,其余的数据保留
 67         if vec[feature]>continuousValue:#若是feature列的值比最佳分割点的大,则放在biggerDataSet中,不然放在smallerDataSet中
 68             biggerDataSet.append(rest)
 69         else:
 70             smallerDataSet.append(rest)
 71     return biggerDataSet,smallerDataSet
 72 
 73 
 74 #连续型属性不须要将训练集中有,测试集中没有的值补全,离散性属性须要
 75 def addFeatureValue(featureListOfValue,feature):
 76     feat = [[ 'Private', 'Self-emp-not-inc', 'Self-emp-inc',
 77               'Federal-gov', 'Local-gov', 'State-gov', 'Without-pay', 'Never-worked'],
 78             [],[],[],[],[]]
 79     for featureValue in feat[feature]: #feat保存的是全部属性特征的全部可能的取值,其结构为feat = [ [val1,val2,val3,…,valn], [], [], [], … ,[] ]
 80         featureListOfValue.append(featureValue)
 81 
 82 def calGainRatioUnContinuous(dataSet,feature,HC):
 83     # addFeatureValue(featureListOfValue,feature) #增长在训练集中有,测试集中没有的属性特征的取值
 84     featureListOfValue = [vector[feature] for vector in dataSet]  # 对于dataset中每个feature,建立单独的列表list保存其取值,其中是不重复的
 85     unique = set(featureListOfValue)
 86     HTC = 0.0  # 保存HTC,即H(T|C)
 87     split = 0.0  # 保存split(T)
 88     for value in unique:
 89         subDataSet = splitDataSet(dataSet, feature, value)  # 划分数据集
 90         probability = len(subDataSet) / float(len(dataSet))  # 求得当前类别的几率
 91         split -= probability * log(probability, 2)  # 计算split(T)
 92         HTC += probability * calculateEntropy(subDataSet)  # 计算当前类别的香农熵,并和HTC想加,即H(T|C) = H(T1|C)+ H(T2|C) + … + H(TN|C)
 93     IG = HC - HTC  # 计算对于该种划分方式的信息增益
 94     if split == 0:
 95         split = 1
 96     gainRatio = float(IG) / float(split)  # 计算对于该种划分方式的信息增益率
 97     return gainRatio
 98 
 99 def calGainRatioContinuous(dataSet,feature,HC):
100     featureListOfValue = [vector[feature] for vector in dataSet]  # 对于dataset中每个feature,建立单独的列表list保存其取值,其中是不重复的
101     featureListOfValue  = set(featureListOfValue)
102     sortedValue = sorted(featureListOfValue)
103     splitPoint = []
104     IGMAX = 0.0
105     GR = 0.0
106     continuousValue = 0.0
107     for i in range(len(sortedValue)-1):#n个value,应该有n-1个分割点splitPoint
108         splitPoint.append((float(sortedValue[i])+float(sortedValue[i+1]))/2.0)
109     for i in range(len(splitPoint)):
110         HTC = 0.0
111         split = 0.0
112         biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i])
113         probabilityBig = len(biggerDataSet) / len(dataSet)
114         probabilitySmall = len(smallerDataSet) / len(dataSet)
115         HTC += probabilityBig * calculateEntropy(biggerDataSet)
116         HTC += probabilitySmall * calculateEntropy(smallerDataSet)
117         IG = HC - HTC
118         if IG>IGMAX:
119             IGMAX = IG
120             split -= probabilityBig * log(probabilityBig, 2)
121             split -= probabilitySmall * log(probabilitySmall, 2)
122             continuousValue = splitPoint[i]
123             N = len(featureListOfValue)
124             D = len(dataSet)
125             IG -= log(N - 1, 2) / abs(D)
126             GR = float(IG) / float(split)
127     return GR,continuousValue
128 
129 #选择最好的数据集划分方式
130 def chooseBestSplitWay(dataSet,labelType):
131     isContinuous = -1 #判断是不是连续值,是为1,不是为0
132     HC = calculateEntropy(dataSet)#计算整个数据集的香农熵(指望信息),即H(C),用来和每一个feature的香农熵进行比较
133     bestfeatureIndex = -1                   #最好的划分方式的索引值,由于0也是索引值,因此应该设置为负数
134     GRMAX=0.0                        #信息增益率=(指望信息-熵)/分割得到的信息增益,即为GR = IG / split = ( HC - HTC )/ split , gainRatioMax为最好的信息增益率,IG为各类划分方式的信息增益
135     continuousValue = -1 #设置若是是连续值的属性返回的最好的划分方式的最好分割点的值
136     for feature in range(len(dataSet[0]) -1 ): #计算feature的个数,因为dataset中是包含有类别的,因此要减去类别
137         if labelType[feature] == 'uncontinuous':
138             GR = calGainRatioUnContinuous(dataSet,feature,HC)
139             if GR>GRMAX:
140                 GRMAX = GR
141                 bestfeatureIndex = feature
142                 isContinuous = 0
143         else: #若是feature是连续型的
144             GR ,bestSplitPoint = calGainRatioContinuous(dataSet,feature,HC)
145             if GR>GRMAX:
146                 GRMAX = GR
147                 continuousValue = bestSplitPoint
148                 isContinuous = 1
149                 bestfeatureIndex = feature
150     return bestfeatureIndex,continuousValue,isContinuous
151             # featureListOfValue  = set(featureListOfValue)
152             # sortedValue = sorted(featureListOfValue)
153             # splitPoint = []
154             # for i in range(len(sortedValue)-1):#n个value,应该有n-1个分割点splitPoint
155             #     splitPoint.append((float(sortedValue[i])+float(sortedValue[i+1]))/2.0)
156 
157             #C4.5修正,再也不使用信息增益率来选择最佳分割点
158             # for i in range(len(splitPoint)): #对于n-1个分割点,计算每一个分割点的信息增益率,来选择最佳分割点
159             #     HTC = 0.0
160             #     split = 0.0
161             #     gainRatio = 0.0
162             #     biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i])
163             #     probabilityBig = len(biggerDataSet)/len(dataSet)
164             #     probabilitySmall = len(smallerDataSet)/len(dataSet)
165             #     HTC += probabilityBig * calculateEntropy(biggerDataSet)
166             #     HTC += probabilityBig * calculateEntropy(smallerDataSet)
167             #     if probabilityBig != 0:
168             #         split -= probabilityBig * log(probabilityBig,2)
169             #     if probabilitySmall != 0:
170             #         split -= probabilitySmall *log(probabilitySmall,2)
171             #     IG = HC - HTC
172             #     if split == 0:
173             #         split = 1;
174             #     gainRatio = IG/split
175             #     if gainRatio>gainRatioMax:
176             #         isContinuous = 1
177             #         gainRatioMax = gainRatio
178             #         bestfeatureIndex = feature
179             #         continuousValue = splitPoint[i]
180             # IGMAX = 0.0
181             # for i in range(len(splitPoint)):
182             #     HTC = 0.0
183             #     split = 0.0
184             #     biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i])
185             #     probabilityBig = len(biggerDataSet) / len(dataSet)
186             #     probabilitySmall = len(smallerDataSet) / len(dataSet)
187             #     HTC += probabilityBig * calculateEntropy(biggerDataSet)
188             #     HTC += probabilitySmall * calculateEntropy(smallerDataSet)
189             #     IG = HC - HTC
190             #     if IG>IGMAX:
191             #         split -= probabilityBig * log(probabilityBig, 2)
192             #         split -= probabilitySmall * log(probabilitySmall, 2)
193             #         IGMAX = IG
194             #         continuousValue = splitPoint[i]
195             #         N = len(splitPoint)
196             #         D = len(dataSet)
197             #         IG -= log(N - 1, 2) / abs(D)
198             #         GR = float(IG) / float(split)
199             #         if GR > GRMAX:
200             #             isContinuous = 1
201             #             GRMAX = GR
202             #             bestfeatureIndex = feature
203         # return bestfeatureIndex,continuousValue,isContinuous
204 
205 #返回出现次数最多的类别,避免产生全部特征所有用完没法判断类别的状况
206 def majority(classList):
207     classificationCount = {}
208     for i in classList:
209         if not i in classificationCount.keys():
210             classificationCount[i] = 0
211         classificationCount[i] += 1
212     sortedClassification = sorted(dict2list(classificationCount),key = operator.itemgetter(1),reverse = True)
213     return sortedClassification[0][0]
214 
215 #dict字典转换为list列表
216 def dict2list(dic:dict):
217     keys=dic.keys()
218     values=dic.values()
219     lst=[(key,value)for key,value in zip(keys,values)]
220     return lst
221 
222 #建立树
223 def createTree(dataSet,labels,labelType):
224     classificationList = [feature[-1] for feature in dataSet] #产生数据集中的分类列表,保存的是每一行的分类
225     if classificationList.count(classificationList[0]) == len(classificationList): #若是分类别表中的全部分类都是同样的,则直接返回当前的分类
226         return classificationList[0]
227     if len(dataSet[0]) == 1: #若是划分数据集已经到了没法继续划分的程度,即已经使用完了所有的feature,则进行决策
228         return majority(classificationList)
229     bestFeature,continuousValue,isContinuous = chooseBestSplitWay(dataSet,labelType) #计算香农熵和信息增益来返回最佳的划分方案,bestFeature保存最佳的划分的feature的索引,在C4.5中要判断该feature是连续型仍是离散型的,continuousValue是当前的返回feature是continuous的的时候,选择的“最好的”分割点
230     bestFeatureLabel = labels[bestFeature] #取出上述的bestfeature的具体值
231     print(bestFeatureLabel)
232     Tree = {bestFeatureLabel:{}}
233     del(labels[bestFeature]) #删除当前进行划分是使用的feature避免下次继续使用到这个feature来划分
234     del(labelType[bestFeature])#删除labelType中的feature类型,保持和labels同步
235     if isContinuous == 1 :#若是要划分的feature是连续的
236         #构造以当前的feature做为root节点,它的连续序列中的分割点为叶子的子树
237         biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,bestFeature,continuousValue)#根据最佳分割点将数据集划分为两部分,一个是大于最佳分割值,一个是小于等于最佳分割值
238         subLabels = labels[:]
239         subLabelType = labelType[:]
240         Tree[bestFeatureLabel]['>'+str(continuousValue)] = createTree(biggerDataSet,subLabels,subLabelType)#将大于分割值的数据集加入到树中,而且递归建立这颗子树
241         subLabels = labels[:]
242         subLabelType = labelType[:]
243         Tree[bestFeatureLabel]['<'+str(continuousValue)] = createTree(smallerDataSet,subLabels,subLabelType)#将小于等于分割值的数据集加入到树中,而且递归建立这颗子树
244     else:#若是要划分的feature是非连续的,下面的步骤和ID3决策树相同
245         #构造以当前的feature做为root节点,它的全部存在的feature取值为叶子的子树
246         featureValueList = [feature[bestFeature]for feature in dataSet] #对于上述取出的bestFeature,取出数据集中属于当前feature的列的全部的值
247         uniqueValue = set(featureValueList) #去重
248         for value in uniqueValue: #对于每个feature标签的value值,进行递归构造决策树
249             subLabels = labels[:]
250             subLabelType = labelType[:]
251             Tree[bestFeatureLabel][value] = createTree(splitDataSet(dataSet,bestFeature,value),subLabels,subLabelType)
252     return Tree
253 
254 def storeTree(inputree,filename):
255     fw = open(filename, 'wb')
256     pickle.dump(inputree, fw)
257     fw.close()
258 
259 def grabTree(filename):
260     fr = open(filename, 'rb')
261     return pickle.load(fr)
262 
263 #测试算法
264 def classify(inputTree,featLabels,testVector,labelType):
265     root = list(inputTree.keys())[0] #取出树的第一个标签,即树的根节点
266     dictionary = inputTree[root] #取出树的第一个标签下的字典
267     featIndex = featLabels.index(root)
268     classLabel = '<=50K'
269     if labelType[featIndex] == 'uncontinuous':#若是是离散型的标签,则按照ID3决策树的方法来测试算法
270         for key in dictionary.keys():#对于这个字典
271             if testVector[featIndex] == key:
272                 if type(dictionary[key]).__name__ == 'dict': #若是还有一个新的字典
273                     classLabel = classify(dictionary[key],featLabels,testVector,labelType)#递归向下寻找到非字典的状况,此时是叶子节点,叶子节点保存的确定是类别
274                 else:
275                     classLabel = dictionary[key]#叶子节点,返回类别
276     else:#若是是连续型的标签,则在取出子树的每个分支的时候,还须要判断是>n仍是<=n的状况,只有这两种状况,因此只须要判断是不是其中一种
277         firstBranch = list(dictionary.keys())[0] #取出第一个分支,来肯定这个double值
278         if str(firstBranch).startswith('>'): #若是第一个分支是">n"的状况,则取出n,为1:n
279             number = firstBranch[1:]
280         else: #若是第一个分支是“<=n”的状况,则取出n,为2:n
281             number = firstBranch[2:]
282         if float(testVector[featIndex])>float(number):#若是测试向量是>n的状况
283             string = '>'+str(number) #设置一个判断string,它是firstBranch的还原,可是为了节省判断branch是哪种,直接使用字符串链接的方式连接
284         else:
285             string = "<="+str(number) #设置一个判断string,它是firstBranch的还原,可是为了节省判断branch是哪种,直接使用字符串链接的方式连接
286         for key in dictionary.keys():
287             if string == key:
288                 if type(dictionary[key]).__name__ == 'dict':#若是还有一个新的字典
289                     classLabel = classify(dictionary[key],featLabels,testVector,labelType)
290                 else:
291                     classLabel = dictionary[key]
292     return classLabel
293 
294 def test(mytree,labels,filename,labelType,mydate):
295     with open(filename, 'r')as csvfile:
296         dataset=[line.strip().split(', ') for line in csvfile.readlines()]     #读取文件中的每一行
297         dataset=[[int(i) if i.isdigit() else i for i in row] for row in dataset]    #对于每一行中的每个元素,将行列式数字化而且去除空白保证匹配的正确完成
298         cleanoutdata(dataset)   #数据清洗
299         del(dataset[0])         #删除第一行和最后一行的空白数据
300         del(dataset[-1])
301         #precondition(dataset)       #预处理数据集
302         clean(dataset,mydate)          #把测试集中的,不存在于训练集中的离散数据清洗掉
303         total = len(dataset)
304         correct = 0
305         error = 0
306     for line in dataset:
307         result=classify(mytree,labels,line,labelType=labelType)+'.'
308         if result==line[14]:     #若是测试结果和类别相同
309             correct = correct + 1
310         else :
311             error = error + 1
312 
313     return total,correct,error
314 
315 #C4.5决策树不须要清洗掉连续性数据
316 # def precondition(mydate):#清洗连续型数据
317 #     #continuous:0,2,4,10,11,12
318 #     for each in mydate:
319 #         del(each[0])
320 #         del(each[1])
321 #         del(each[2])
322 #         del(each[7])
323 #         del(each[7])
324 #         del(each[7])
325 
326 #C4.5决策树不须要清洗掉测试集中连续值出现了训练集中没有的值的状况,可是离散数据集中仍是须要清洗的
327 def clean(dataset,mydate):#清洗掉测试集中出现了训练集中没有的值的状况
328     for i in [1,3,5,6,7,8,9,13]:
329         set1=set()
330         for row1 in mydate:
331             set1.add(row1[i])
332         for row2 in dataset:
333             if row2[i] not in set1:
334                dataset.remove(row2)
335         set1.clear()
336 
337 def main():
338     dataSetName = r"C:\Users\yang\Desktop\adult.data"
339     mydate, label ,labelType= createDateset(dataSetName)
340     labelList = label[:]
341     labelTypeList = labelType[:]
342     Tree = createTree(mydate, labelList,labelType=labelTypeList)
343     print(Tree)
344     storeTree(Tree, r'C:\Users\yang\Desktop\tree.txt')  # 保存决策树,避免下次再生成决策树
345 
346     #Tree=grabTree(r'C:\Users\yang\Desktop\tree.txt')#读取决策树,若是已经存在tree.txt能够直接使用决策树不须要再次生成决策树
347     total, correct, error  = test(Tree, label, r'C:\Users\yang\Desktop\adult.test',labelType,mydate)
348     # with open(r'C:\Users\yang\Desktop\trees.txt', 'w')as f:
349     #     f.write(str(Tree))
350     accuracy = float(correct)/float(total)
351     print("准确率:%f" % accuracy)
352 
353 if __name__ == '__main__':
354     main()

 

6、总结

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

  C4.5是真的很是很是很是低效!低效!低效!

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

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

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

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

相关文章
相关标签/搜索