说明:python小白我用了修改自《Machine Laerning in Action》的代码和学习了这篇和这篇博文对于离散和连续两种不同特征类型的处理,十分感谢。ID3算法本身这里不赘述,本文主要以一个初学者的角度详细讲解算法的python实现过程。希望和我一样看别人代码需要理解好久的新手看我啰嗦的注释可以理解地快一点~所用数据是周志华《机器学习》p84页的西瓜数据集3.0,请点击这里下载数据csv文件,占版面我就不在正文附数据了。是个很小却包括了离散和连续两种类型的数据集~数据对应的中文点击这里查看。
实现:为了方便测试,我把本文讲解的几段代码整合放到一个.py文件里了,请点击这里进行下载。将下载好的数据文件"watermelon3_0_En.csv"和代码.py文件放在一个目录下,直接运行代码即可看到绘出的决策树图像。
详解:代码主要分为以下五个部分
1)定义生成树过程中要用到的函数;
2)定义主函数:调用之前写好的函数吗,通过递归实现决策树生成(这部分会写伪代码详解思路)
3)读取数据+调用主函数生成决策树;
4)定义绘图函数;
5)调用函数绘图。
下文会分步骤详细解释代码。代码每一步的含义都在注释中尽可能详细地说明的,至于每个函数的用处,函数之间关系和整体代码思路框架,我会在文章里面说明。
1. 定义生成树所用函数
首先说明:对于连续和离散两种类型的特征,经常需要分别处理,这个问题贯穿着整个题目的求解。
1) 我们知道ID3是基于信息熵增益 (Infomation Gain)来选取下一步的划分特征的,故这部分需要定义一个函数可以计算不同划分方式(特征)的信息熵增益:calcInfoEnt()
计算信息熵增益时,当前数据集已经被划分成好几个subDataSet了,所以信息熵是几个子集上信息熵加权之和(总之就是ID3算法本身讲的,这里注意一下就好)。
2) 对给定数据集,需根据求得的熵增选取当前最佳(熵增最大)用于划分的特征:chooseBestFeatureToSplit()
若是离散特征,只需要选择返回根据哪一个离散取值划分即可;
若是连续特征,则需返回最佳划分点的值。这些划分点时我们定义的函数中计算出来的。
还有一点十分重要的:可以看到这个函数定义的最后那十行左右,在对连续特征进行二值化处理。这是由于我们之后在递归生成决策树createTree()时候会调对每一个特征进行一次dataSet的划分(我在程序注释里标记了),那里直接调用了SplitDiscreteDataSet()函数,因为这里二值化后已经将continuous特征的取值变成0和1的离散值了。(注:其实这个操作也不是多精巧……比如在createTree()划分dataSet的时候,我完全可以判定一下是离散or连续,然后调用相应的Split...DatsSet()就可以)
3) 选择好划分特征后,需要对数据集进行划分。这时候应有两个函数对离散和连续分别处理:SplitDiscreteDataSet() 和 SplitContinuousDataSet()
注意到,这两个函数其实先是为chooseBestFeatureToSplit()服务的,之后才是用来做这一步即真的对数据集进行操作划分。之前都是在chooseBestFeatureToSplit()的选择过程中,给每个特征的每个取值(或划分点)试一下“这样划分后怎么样?”,这才选出了最佳划分特征。
4) 以上这样划分数据集就完成了?但实际上编程实现需要考虑更周全一点。若出现噪声数据怎么办(或者说数据本身就这样,不一定噪声)?即如果所
有的特征都已经在划分中用过了,但节点下样本分类还是没有统一。
这时候,就需要
投票处理
,少数服从多数了:
majorityCnt()
。
有了这些函数,下一步就可以着手递归生成决策树了。
# -*- coding: utf-8 -*-
'''生成树的函数'''###############################################################
from numpy import *
import numpy as np
import pandas as pd
from math import log
import operator
### 计算数据集的信息熵(Information Gain)增益函数(机器学习实战中信息熵叫香农熵)
def calcInfoEnt(dataSet):#本题中Label即好or坏瓜 #dataSet每一列是一个属性(列末是Label)
numEntries = len(dataSet) # 每一行是一个样本
labelCounts = {} #给所有可能的分类创建字典labelCounts
for featVec in dataSet: #按行循环:即rowVev取遍了数据集中的每一行
currentLabel = featVec[-1] #故featVec[-1]取遍每行最后一个值即Label
if currentLabel not in labelCounts.keys(): #如果当前的Label在字典中还没有
labelCounts[currentLabel] = 0 #则先赋值0来创建这个词
labelCounts[currentLabel] += 1 #计数, 统计每类Label数量(这行不受if限制)
InfoEnt = 0.0
for key in labelCounts: #遍历每类Label
prob = float(labelCounts[key])/numEntries #各类Label熵累加
InfoEnt -= prob * log(prob,2) #ID3用的信息熵增益公式
return InfoEnt
### 对于离散特征: 取出该特征取值为value的所有样本
def splitDiscreteDataSet(dataSet, axis, value): #dataSet是当前结点(待划分)集合
#axis指示划分所依据的属性
#value该属性用于划分的取值
retDataSet = [] #为return Data Set分配一个列表用来储存
for featVec in dataSet:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis] #该特征之前的特征仍保留在样本dataSet中
reducedFeatVec.extend(featVec[axis+1:]) #该特征之后的特征仍保留在样本dataSet中
retDataSet.append(reducedFeatVec) #把这个样本加到list中
return retDataSet
### 对于连续特征: 返回特征取值大于value的所有样本(以value为阈值将集合分成两部分)
def splitContinuousDataSet(dataSet, axis, value):
retDataSetG=[] #将储存取值大于value的样本
retDataSetL=[] #将储存取值小于value的样本
for featVec in dataSet:
if featVec[axis]>value:
reducedFeatVecG=featVec[:axis]
reducedFeatVecG.extend(featVec[axis+1:])
retDataSetG.append(reducedFeatVecG)
else:
reducedFeatVecL=featVec[:axis]
reducedFeatVecL.extend(featVec[axis+1:])
retDataSetL.append(reducedFeatVecL)
return retDataSetG,retDataSetL #返回两个集合, 是含2个元素的tuple形式
### 根据InfoGain选择当前最好的划分特征(以及对于连续变量还要选择以什么值划分)
def chooseBestFeatureToSplit(dataSet,labels):
numFeatures=len(dataSet[0])-1
baseEntropy=calcInfoEnt(dataSet)
bestInfoGain=0.0; bestFeature=-1
bestSplitDict={}
for i in range(numFeatures):
#遍历所有特征:下面这句是取每一行的第i个, 即得当前集合所有样本第i个feature的值
featList=[example[i] for example in dataSet]
#判断是否为离散特征
if not (type(featList[0]).__name__=='float' or type(featList[0]).__name__=='int'):
### 对于离散特征:求若以该特征划分的熵增
uniqueVals = set(featList) #从列表中创建集合set(得列表唯一元素值)
newEntropy = 0.0
for value in uniqueVals: #遍历该离散特征每个取值
subDataSet = splitDiscreteDataSet(dataSet, i, value)#计算每个取值的信息熵
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calcInfoEnt(subDataSet)#各取值的熵累加
infoGain = baseEntropy - newEntropy #得到以该特征划分的熵增
### 对于连续特征:求若以该特征划分的熵增(区别:n个数据则需添n-1个候选划分点, 并选最佳划分点)
else:
#产生n-1个候选划分点
sortfeatList=sorted(featList)
splitList=[]
for j in range(len(sortfeatList)-1): #产生n-1个候选划分点
splitList.append((sortfeatList[j]+sortfeatList[j+1])/2.0)
bestSplitEntropy=10000 #设定一个很大的熵值(之后用)
#遍历n-1个候选划分点: 求选第j个候选划分点划分时的熵增, 并选出最佳划分点
for j in range(len(splitList)):
value=splitList[j]
newEntropy=0.0
DataSet = splitContinuousDataSet(dataSet, i, value)
subDataSetG=DataSet[0]
subDataSetL=DataSet[1]
probG = len(subDataSetG) / float(len(dataSet))
newEntropy += probG * calcInfoEnt(subDataSetG)
probL = len(subDataSetL) / float(len(dataSet))
newEntropy += probL * calcInfoEnt(subDataSetL)
if newEntropy < bestSplitEntropy:
bestSplitEntropy=newEntropy
bestSplit=j
bestSplitDict[labels[i]] = splitList[bestSplit]#字典记录当前连续属性的最佳划分点
infoGain = baseEntropy - bestSplitEntropy #计算以该节点划分的熵增
### 在所有属性(包括连续和离散)中选择可以获得最大熵增的属性
if infoGain>bestInfoGain:
bestInfoGain=infoGain
bestFeature=i
#若当前节点的最佳划分特征为连续特征,则需根据“是否小于等于其最佳划分点”进行二值化处理
#即将该特征改为“是否小于等于bestSplitValue”, 例如将“密度”变为“密度<=0.3815”
#注意:以下这段直接操作了原dataSet数据, 之前的那些float型的值相应变为0和1
#【为何这样做?】在函数createTree()末尾将看到解释
if type(dataSet[0][bestFeature]).__name__=='float' or \
type(dataSet[0][bestFeature]).__name__=='int':
bestSplitValue=bestSplitDict[labels[bestFeature]]
labels[bestFeature]=labels[bestFeature]+'<='+str(bestSplitValue)
for i in range(shape(dataSet)[0]):
if dataSet[i][bestFeature]<=bestSplitValue:
dataSet[i][bestFeature]=1
else:
dataSet[i][bestFeature]=0
return bestFeature
### 若特征已经划分完,节点下的样本还没有统一取值,则需要进行投票:计算每类Label个数, 取max者
def majorityCnt(classList):
classCount={} #将创建键值为Label类型的字典
for vote in classList:
if vote not in classCount.keys():
classCount[vote]=0 #第一次出现的Label加入字典
classCount[vote]+=1 #计数
return max(classCount)
2. 定义递归的主函数
生成决策树用了编程中常见且巧妙的“递归”方法,先来看一下伪代码?也许会有一点好的理解。