Python3实现机器学习经典算法(三)ID3决策树

1、ID3决策树概述html

  ID3决策树是另外一种很是重要的用来处理分类问题的结构,它形似一个嵌套N层的IF…ELSE结构,可是它的判断标准再也不是一个关系表达式,而是对应的模块的信息增益。它经过信息增益的大小,从根节点开始,选择一个分支,如同进入一个IF结构的statement,经过属性值的取值不一样进入新的IF结构的statement,直到到达叶子节点,找到它所属的“分类”标签。git

  它的流程图是一课没法保证平衡的多叉树,每个父节点都是一个判断模块,经过判断,当前的向量会进入它的某一个子节点中,这个子节点是判断模块或者终止模块(叶子节点),当且仅当这个向量到达叶子节点,它也就找到了它的“分类”标签。github

  ID3决策树和KNN的区别不一样,它经过一个固定的训练集是能够造成一颗永久的“树”的,这课树能够进行保存而且运用到不一样的测试集中,惟一的要求就是测试集和训练集须要是结构等价的。这个训练过程就是根据训练集建立规则的过程,这也是机器学习的过程。算法

  ID3决策树的一个巨大缺陷是:它将产生过分匹配问题。这里在不讨论信息增益的前提下,有这样一个例子:人的属性中有性别和年龄两个属性,因为人的性别只有男和女两种,年龄有不少种分支,当它有超过两个分支的时候,在用信息增益选择新的属性的时候,会选择年龄而不是性别,由于ID3决策树在使用信息增益来划分数据集的时候会倾向于选择属性分支更多的一个;另一个缺陷是,人的年龄假定为1~100,若是不进行离散化,即区间的划分,那么在选择年龄这个属性的时候,这棵决策树会产生最多100个分支,这是很是可怕并且浪费空间和效率的,考虑这 样一种状况:两我的的其余全部属性彻底相同,他们的分类都是"A",然而在年龄这一个树节点中分支了,而这个年龄下有一个跟这两我的很像,却不属于“A”类别的人,因为ID3决策树没法处理连续性数据,那么这两我的颇有可能被划分到两个分类中,这是不合理的,这也是下一节的C4.5决策树考虑的问题。api

  前面提到了信息增益,这是ID3决策树划分数据集的根本。这里在理论上解释一下信息增益和香农熵,下面会在训练算法的时候,经过算法和数据来解释信息增益和香农熵。app

  首先解释一个熵的概念:熵指的是一个系统“内在的混乱程度”,在这里也就是表明信息的“有序程度”。熵增的方向就是信息混乱度越大的方向,熵减的方向就是信息趋于“有序”的方向,因此说咱们要划分数据集来使得数据集局部愈发趋于“有序化”。之因此是说数据集局部,是由于ID3在进行数据集划分的时候,选择一个使信息增益最大,即熵减最多的特征进行划分,然后该属性在后续的划分中将再也不被考虑,因此这是一个递归的过程,也是一个不断局部化数据集的过程。机器学习

  信息增益(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最大,此时熵最小,也便是熵减最多。学习

 

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.

  

下一步是分析数据:

一、数据预处理:

  上面提到了,ID3是没法处理连续型数据的,因此连续型数据应该在数据预处理这一步进行清理,处理方法有两种:

  一、直接清洗掉:这也是所采用的方法,由于转换离散数据的前提是,对于连续型数据的划分要足够好,好比年龄、身高等的划分,5划分和10划分之间的差距是很是大的,无论哪种划分都会破坏数据本来的结构,因此这里采用的是直接清洗掉数据的方法,对于连续数据的使用延迟到C4.5和CART的实现中:

1 def precondition(mydate):#清洗连续型数据
2     #continuous:0,2,4,10,11,12
3     for each in mydate:
4         del(each[0])
5         del(each[1])
6         del(each[2])
7         del(each[7])
8         del(each[7])
9         del(each[7])

  这里要注意在Python中用del清洗数据的时候,某一个数据被del了,它的索引为i,那么del执行完成后 i+1 的值的索引会变为i,如上所示连续型数据所在的列为0,2,4,10,11,12,可是须要清除的列应该是0,1,2,7,7,7。

  二、将连续型数据转换为离散数据:

  这里的实现方法能够根据本身的划分构造一个和KNN同样的字典,而后扫描一次数据集,将数据集中的连续数据转换为离散的数据。

二、数据清洗:

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

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

  这里是先进行预处理仍是先进行数据清洗取决于所使用的数据集中,连续型数据和脏数据哪一种更多,先处理更少的那一种能有效地减小处理量。

 

3、训练算法

 

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

  用伪代码表示以下:

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

    return 分类标签

  else:

    if 属性特征已经使用完:

      进行投票决策

      return 票数最多的分类标签

    else:

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

      根据属性特征T建立分支

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

        成为当前树分支的子树

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

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

 

  根据上面的伪代码,就能够一步一步地完善代码:

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

 

 1 #计算香农熵/指望信息
 2 def calculateEntropy(dataSet):
 3     ClassifyCount = {}#分类标签统计字典,用来统计每一个分类标签的几率
 4     for vector in dataSet:
 5         clasification = vector[-1]  #获取分类
 6         if not clasification not in ClassifyCount.keys():#若是分类暂时不在字典中,在字典中添加对应的值对
 7             ClassifyCount[clasification] = 0
 8         ClassifyCount[clasification] += 1         #计算出现次数
10     shannonEntropy=0.0
11     for key in ClassifyCount:
12         probability=float(ClassifyCount[key]) / dataSet.shape[0]      #计算几率
13         shannonEntropy -= probability * log(probability,2)   #香农熵的每个子项都是负的
14     return shannonEntropy
15 
16 #选择最好的数据集划分方式
17 def chooseBestSplitWay(dataSet):
18     HC = calculateEntropy(dataSet)#计算整个数据集的香农熵(指望信息),即H(C),用来和每一个feature的香农熵进行比较
19     bestfeatureIndex = -1                   #最好的划分方式的索引值,由于0也是索引值,因此应该设置为负数
20     gain=0.0                        #信息增益=指望信息-熵,gain为最好的信息增益,IG为各类划分方式的信息增益
21     for feature in range(len(dataSet[0]) -1 ): #计算feature的个数,因为dataset中是包含有类别的,因此要减去类别
22         featureListOfValue=[vector[feature] for vector in dataSet] #对于dataset中每个feature,建立单独的列表list保存其取值,其中是不重复的
23         addFeatureValue(featureListOfValue,feature) #增长在训练集中有,测试集中没有的属性特征的取值
24         unique=set(featureListOfValue)
25         HTC=0.0         #保存HTC,即H(T|C)
26         for value in unique:
27             subDataSet = splitDataset(dataSet,feature,value)  #划分数据集
28             probability = len(subDataSet) / float(len(dataSet))  #求得当前类别的几率
29             HTC += probability * calculateEntropy(subDataSet)      #计算当前类别的香农熵,并和HTC想加,即H(T|C) = H(T1|C)+ H(T2|C) + … + H(TN|C)
30         IG=HC-HTC        #计算对于该种划分方式的信息增益
31         if(IG > gain):
32             gain = IG
33             bestfeatureIndex = feature
34     return bestfeatureIndex
35 
36 
37 def addFeatureValue(featureListOfValue,feature):
38     for featureValue in feat[feature]: #feat保存的是全部属性特征的全部可能的取值,其结构为feat = [ [val1,val2,val3,…,valn], [], [], [], … ,[] ]
39         featureListOfValue.append(featureValue)

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

  1)信息增益的计算:

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

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

  2)addFeatureValue()函数

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

  二、划分数据集

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

1 #划分数据集
2 def splitDataSet(dataSet,featureIndex,value):
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

  划分数据集的方式就是将0~传入的featureIndex的全部的列复制到新的rest列表中,而后跳过这一列,从下一列开始到最后一列extend到列表的末尾中,而后再将这个rest列表做为新的数据集传回。

   

  三、投票表决:

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

  

 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]

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

 

1 #dict字典转换为list列表
2 def dict2list(dic:dict):
3     keys = dic.keys()
4     values = dic.values()
5     lst = [(key,value)for key,value in zip(keys,values)]
6     return lst

  四、树建立:

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

  

 1 #建立树
 2 def createTree(dataSet,labels):
 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 = chooseBestSplitWay(dataSet) #计算香农熵和信息增益来返回最佳的划分方案,bestFeature保存最佳的划分的feature的索引
 9     bestFeatureLabel = labels[bestFeature] #取出上述的bestfeature的具体值
10     Tree = {bestFeatureLabel:{}}
11     del(labels[bestFeature]) #删除当前进行划分是使用的feature避免下次继续使用到这个feature来划分
12     featureValueList = [feature[bestFeature]for feature in dataSet] #对于上述取出的bestFeature,取出数据集中属于当前feature的列的全部的值
13     uniqueValue = set(featureValueList) #去重
14     for value in uniqueValue: #对于每个feature标签的value值,进行递归构造决策树
15         subLabels = labels[:]
16         Tree[bestFeatureLabel][value] = createTree(splitDataSet(dataSet,bestFeature,value),subLabels)
17     return Tree

  

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

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

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

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

  自此,咱们的ID3决策树就已经构建完成,如今咱们彻底能够获得一棵独立的决策树,它是离线的。看看咱们的树长什么样:

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

  五、保存树(读取树):

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):
 3     root = list(inputTree.keys())[0] #取出树的第一个标签,即树的根节点
 4     dictionary = inputTree[root] #取出树的第一个标签下的字典
 5     featIndex = featLabels.index(root)
 6     for key in dictionary.keys(): #对于这个字典
 7         if testVector[featIndex] == key:
 8             if type(dictionary[key]).__name__ == 'dict': #若是还有一个新的字典
 9                 classLabel = classify(dictionary[key],featLabels,testVector) #递归向下寻找到非字典的状况,此时是叶子节点,叶子节点保存的确定是类别
10             else:
11                 classLabel=dictionary[key] #叶子节点,返回类别
12     return classLabel
13 
14 def test(myTree,labels,filename,sum,correct,error):
15     for line in dataSet:
16         result=classify(myTree,labels,line)+'.'
17         if result==line[8]: #若是测试结果和类别相同
18             correct = correct + 1
19         else :
20             error = error + 1
21     print("准确率:%f"% correct / sum )
22     return sum,correct,error

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

  看看结果 :

  

  跟官方的数据进行对比(官方的是错误率):

  

 

5、完整代码

  

  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=['workclass','education',
 15                'marital-status','occupation',
 16                 'relationship','race','sex',
 17                 'native-country']
 18         return dataset,labels
 19 
 20 def cleanoutdata(dataset):#数据清洗
 21     for row in dataset:
 22         for column in row:
 23             if column == '?' or column=='':
 24                 dataset.remove(row)
 25                 break
 26 
 27 #计算香农熵/指望信息
 28 def calculateEntropy(dataSet):
 29     ClassifyCount = {}#分类标签统计字典,用来统计每一个分类标签的几率
 30     for vector in dataSet:
 31         clasification = vector[-1]  #获取分类
 32         if not clasification not in ClassifyCount.keys():#若是分类暂时不在字典中,在字典中添加对应的值对
 33             ClassifyCount[clasification] = 0
 34         ClassifyCount[clasification] += 1         #计算出现次数
 35     shannonEntropy=0.0
 36     for key in ClassifyCount:
 37         probability=float(ClassifyCount[key]) / dataSet.shape[0]      #计算几率
 38         shannonEntropy -= probability * log(probability,2)   #香农熵的每个子项都是负的
 39     return shannonEntropy
 40 
 41 # def addFetureValue(feature):
 42 
 43 #划分数据集
 44 def splitDataSet(dataSet,featureIndex,value):
 45     newDataSet=[]
 46     for vec in dataSet:#将选定的feature的列从数据集中去除
 47         if vec[featureIndex] == value:
 48             rest = vec[:featureIndex]
 49             rest.extend(vec[featureIndex + 1:])
 50             newDataSet.append(rest)
 51     return newDataSet
 52 
 53 
 54 def addFeatureValue(featureListOfValue,feature):
 55     feat = [[ 'Private', 'Self-emp-not-inc', 'Self-emp-inc',
 56               'Federal-gov', 'Local-gov', 'State-gov', 'Without-pay', 'Never-worked'],
 57             [],[],[],[],[]]
 58     for featureValue in feat[feature]: #feat保存的是全部属性特征的全部可能的取值,其结构为feat = [ [val1,val2,val3,…,valn], [], [], [], … ,[] ]
 59         featureListOfValue.append(featureValue)
 60 
 61 #选择最好的数据集划分方式
 62 def chooseBestSplitWay(dataSet):
 63     HC = calculateEntropy(dataSet)#计算整个数据集的香农熵(指望信息),即H(C),用来和每一个feature的香农熵进行比较
 64     bestfeatureIndex = -1                   #最好的划分方式的索引值,由于0也是索引值,因此应该设置为负数
 65     gain=0.0                        #信息增益=指望信息-熵,gain为最好的信息增益,IG为各类划分方式的信息增益
 66     for feature in range(len(dataSet[0]) -1 ): #计算feature的个数,因为dataset中是包含有类别的,因此要减去类别
 67         featureListOfValue=[vector[feature] for vector in dataSet] #对于dataset中每个feature,建立单独的列表list保存其取值,其中是不重复的
 68         addFeatureValue(featureListOfValue,feature) #增长在训练集中有,测试集中没有的属性特征的取值
 69         unique=set(featureListOfValue)
 70         HTC=0.0         #保存HTC,即H(T|C)
 71         for value in unique:
 72             subDataSet = splitDataSet(dataSet,feature,value)  #划分数据集
 73             probability = len(subDataSet) / float(len(dataSet))  #求得当前类别的几率
 74             HTC += probability * calculateEntropy(subDataSet)      #计算当前类别的香农熵,并和HTC想加,即H(T|C) = H(T1|C)+ H(T2|C) + … + H(TN|C)
 75         IG=HC-HTC        #计算对于该种划分方式的信息增益
 76         if(IG > gain):
 77             gain = IG
 78             bestfeatureIndex = feature
 79     return bestfeatureIndex
 80 
 81 #返回出现次数最多的类别,避免产生全部特征所有用完没法判断类别的状况
 82 def majority(classList):
 83     classificationCount = {}
 84     for i in classList:
 85         if not i in classificationCount.keys():
 86             classificationCount[i] = 0
 87         classificationCount[i] += 1
 88     sortedClassification = sorted(dict2list(classificationCount),key = operator.itemgetter(1),reverse = True)
 89     return sortedClassification[0][0]
 90 
 91 #dict字典转换为list列表
 92 def dict2list(dic:dict):
 93     keys=dic.keys()
 94     values=dic.values()
 95     lst=[(key,value)for key,value in zip(keys,values)]
 96     return lst
 97 
 98 #建立树
 99 def createTree(dataSet,labels):
100     classificationList = [feature[-1] for feature in dataSet] #产生数据集中的分类列表,保存的是每一行的分类
101     if classificationList.count(classificationList[0]) == len(classificationList): #若是分类别表中的全部分类都是同样的,则直接返回当前的分类
102         return classificationList[0]
103     if len(dataSet[0]) == 1: #若是划分数据集已经到了没法继续划分的程度,即已经使用完了所有的feature,则进行决策
104         return majority(classificationList)
105     bestFeature = chooseBestSplitWay(dataSet) #计算香农熵和信息增益来返回最佳的划分方案,bestFeature保存最佳的划分的feature的索引
106     bestFeatureLabel = labels[bestFeature] #取出上述的bestfeature的具体值
107     Tree = {bestFeatureLabel:{}}
108     del(labels[bestFeature]) #删除当前进行划分是使用的feature避免下次继续使用到这个feature来划分
109     featureValueList = [feature[bestFeature]for feature in dataSet] #对于上述取出的bestFeature,取出数据集中属于当前feature的列的全部的值
110     uniqueValue = set(featureValueList) #去重
111     for value in uniqueValue: #对于每个feature标签的value值,进行递归构造决策树
112         subLabels = labels[:]
113         Tree[bestFeatureLabel][value] = createTree(splitDataSet(dataSet,bestFeature,value),subLabels)
114     return Tree
115 
116 def storeTree(inputree,filename):
117     fw = open(filename, 'wb')
118     pickle.dump(inputree, fw)
119     fw.close()
120 
121 def grabTree(filename):
122     fr = open(filename, 'rb')
123     return pickle.load(fr)
124 
125 #测试算法
126 def classify(inputTree,featLabels,testVector):
127     root = list(inputTree.keys())[0] #取出树的第一个标签,即树的根节点
128     dictionary = inputTree[root] #取出树的第一个标签下的字典
129     featIndex = featLabels.index(root)
130     for key in dictionary.keys():#对于这个字典
131         if testVector[featIndex] == key:
132             if type(dictionary[key]).__name__ == 'dict': #若是还有一个新的字典
133                 classLabel = classify(dictionary[key],featLabels,testVector)#递归向下寻找到非字典的状况,此时是叶子节点,叶子节点保存的确定是类别
134             else:
135                 classLabel=dictionary[key]#叶子节点,返回类别
136     return classLabel
137 
138 def test(mytree,labels,filename,sum,correct,error):
139     with open(filename, 'r')as csvfile:
140         dataset=[line.strip().split(', ') for line in csvfile.readlines()]     #读取文件中的每一行
141         dataset=[[int(i) if i.isdigit() else i for i in row] for row in dataset]    #对于每一行中的每个元素,将行列式数字化而且去除空白保证匹配的正确完成
142         cleanoutdata(dataset)   #数据清洗
143         del(dataset[0])         #删除第一行和最后一行的空白数据
144         del(dataset[-1])
145         precondition(dataset)       #预处理数据集
146         # clean(dataset)          #把测试集中的,不存在于训练集中的数据清洗掉
147         sum = len(dataset)
148     for line in dataset:
149         result=classify(mytree,labels,line)+'.'
150         if result==line[8]:     #若是测试结果和类别相同
151             correct = correct + 1
152         else :
153             error = error + 1
154     
155     return sum,correct,error
156 
157 def precondition(mydate):#清洗连续型数据
158     #continuous:0,2,4,10,11,12
159     for each in mydate:
160         del(each[0])
161         del(each[1])
162         del(each[2])
163         del(each[7])
164         del(each[7])
165         del(each[7])
166 
167 # def clean(dataset):#清洗掉测试集中出现了训练集中没有的值的状况
168 #     global mydate
169 #     for i in range(8):
170 #         set1=set()
171 #         for row1 in mydate:
172 #             set1.add(row1[i])
173 #         for row2 in dataset:
174 #             if row2[i] not in set1:
175 #                dataset.remove(row2)
176 #         set1.clear()
177 
178 dataSetName=r"C:\Users\yang\Desktop\adult.data"
179 mydate,label=createDateset(dataSetName)
180 labelList=label[:]
181 
182 Tree=createTree(mydate,labelList)
183 
184 sum = 0
185 correct = 0
186 error = 0
187 
188 storeTree(Tree,r'C:\Users\yang\Desktop\tree.txt') #保存决策树,避免下次再生成决策树
189 
190 # Tree=grabTree(r'C:\Users\yang\Desktop\tree.txt')#读取决策树,若是已经存在tree.txt能够直接使用决策树不须要再次生成决策树
191 sum,current,unreco=test(Tree,label,r'C:\Users\yang\Desktop\adult.test',sum,correct,error)
192 # with open(r'C:\Users\yang\Desktop\trees.txt', 'w')as f:
193 #     f.write(str(Tree))
194 print("准确率:%f" % correct / sum)

6、总结

  因为ID3决策树仍是存在着两个巨大的缺陷,下一节将是实现C4.5决策树,下下节是CART分类回归树,这两种树将弥补这种缺点。另外是使用的Adult数据集的问题,所获得的结果(正确率)超过官方所给的数据,究其缘由应该是数据清洗的时候,我把大多数的噪声数据清洗掉了,这对数据集的破坏很是大,其实若是能够的话,仍是应该进行填补和填充的。另外就是使用Iris数据集应该可使得正确率很是高,由于其属性特征的数目很少,取值也很少,ID3决策树在这方面仍是趋于一个弱势,因此才会有C4.5,C5.0和CART的出现。C4.5和CART会继续用Python3实现,C5.0试一下哈哈。

  原创博客,码字不易,转载注明出处:https://www.cnblogs.com/DawnSwallow/p/9452586.html  

  github:https://github.com/hahahaha1997/DecisionTree

相关文章
相关标签/搜索