[数据挖掘笔记02] 决策树ID3算法
1.原理【问题】故事发生在100年前,一个还没有手机的时代,小明的女朋友小红想去找小明玩,但不知道小明在不在家,因为小明可能出去打球了。现在小红想知道小明的去向,她手里有过去14次去找小明玩时的数据,请帮她判断一下小明到底是在家还是去打球了。[现在的情况是[‘rainy’,‘hot’,‘high’,‘false’]]这里引入两个概念:熵:表示随机变量不确定性的度量,物体内部的混乱程度。比...
1.原理
【问题】故事发生在100年前,一个还没有手机的时代,小明的女朋友小红想去找小明玩,但不知道小明在不在家,因为小明可能出去打球了。现在小红想知道小明的去向,她手里有过去14次去找小明玩时的数据,请帮她判断一下小明到底是在家还是去打球了。[现在的情况是[‘rainy’,‘hot’,‘high’,‘false’]]
这里引入两个概念:
- 熵:表示随机变量不确定性的度量,物体内部的混乱程度。比如说学校里的选修课,可能有来自学校各个不同专业的人选修,混乱程度很高,不易分类;而专业课可能都是同一个专业的同学在上,很容易分类,纯度高,随表一块板砖拍下去砸到的就是你们专业的。计算公式为:
其中p为取集合中某个属性取到的概率。
原始数据中一共14天,9天打球,5天不打球,所以原始数据的熵为:
- 信息增益:特征X使得类Y的不确定性减少程度。信息增益越大,越能减少不确定性。
ID3算法就是根据最大信息增益来选取决策树的节点。
该数据集一共有4种划分方式,如下:
接下来分别计算每种划分方式的熵,首先,对于outlook属性,有
outlook取sunny,overcast,rainy的概率分别是5/14,4/14,5/14,则分类后的熵值为:
outlook的信息增益:Gain(D, outlook) = Ent(D) - 0.693 = 0.247
同理可以计算出:
Gain(D, temperature) = 0.029
Gain(D, humility) = 0.152
Gain(D, windy) = 0.048
outlook的信息增益最大,因此选取outlook作为根节点,此时的决策树为:
可以看到overcast已经确定,只要outlook为overcast,小明就去打球。这样就可以排除含overcast的数据了,现在的数据集如下:
现在来计算sunny,sunny的数据集如下:
我们基于这个数据集来进行计算各种划分方式的熵,划分方式有如下几种:
对于temperature属性,计算熵:
temperature取hot,mild,cool的概率分别是2/5, 2/5,1/5,则分类后的熵值为:
根据前面的计算,我们知道Ent(outlook=sunny) = 0.971,则temperature的信息增益为:
Gain(outlook=sunny, temperature) = Ent(outlook=sunny) - 0.4= 0.971 - 0.4 = 0.571
同理可得humility和windy的信息增益:
Gain(outlook=sunny, humility) = 0.971
Gain(outlook=sunny, windy) = 0.971 - 0.951=0.02
humility的信息增益最大,因此选择humility作为叶子节点,这时候可以看到humility为high的决策全为no,humility为normal的决策全为yes,因此可以确定决策,此时的决策树为:
继续计算rainy,rainy的数据集如下:
我们基于这个数据集来进行计算各种划分方式的熵,划分方式有如下几种:
这里可以直接看出windy为false的决策全为yes,windy为true的决策全为no,因此可以确定决策,选择windy作为叶子节点,此时的决策树为。
至此,就完成了决策树的构建,其实这就是一个不断递归的过程。我们把现在的数据带进决策树中计算,[‘rainy’,‘hot’,‘high’,‘false’],从outlook开始,outlook=rainy,走右边的节点,到达windy,windy=false,走左边的节点,结果为yes。因此,如果今天去小明的家,有很大可能见不到小明,因为他打球去了。
ID3算法的问题:
如果选用id特征来分类,每个id都是唯一的,因此每个类别里只有一类,纯洁度是最高的,信息增益最大,根据ID3算法的思想,我们要选用信息增益最大的节点来划分,那我们是不是要选择id呢?但我们都知道id属性并没有什么意义,因此在ID3算法的基础上,产生了C4.5算法,它使用信息增益率来进行计算,考虑了自身的熵。
C4.5算法
拿outlook特征和id特征来说,基于outlook的信息增益是0.247,基于id的信息增益是0.940
outlook自身的熵值为:
id自身的熵值为:
outlook的信息增益率:0.247 / 1.557 = 0.157
id的信息增益率:0.940 / 3.81 = 0.247
id 的信息增益率还是比outlook大,这是因为样本的数据量不够大,如果我们把数据数量乘以100,也就是说有1400个样本,各属性比例保持不变,基于id的信息增益也是不变的,这时id自身的熵值为:
id的信息增益率:0.940 / 10.45 = 0.090,信息增益率马上降了下来。
CART算法
CART算法使用GINI系数作为衡量指标,GINI系数的计算公式为:
2.代码实现
首先在项目文件夹建立一个名为treePlotter的包,我们要用它来绘制决策树。
在__init__.py文件里放入如下代码:
import matplotlib.pyplot as plt
"""绘制决策树的函数"""
decisionNode = dict(boxstyle="sawtooth", fc="0.8") # 定义分支点的样式
leafNode = dict(boxstyle="round4", fc="0.8") # 定义叶节点的样式
arrow_args = dict(arrowstyle="<-") # 定义箭头标识样式
# 计算树的叶子节点数量
def getNumLeafs(myTree):
numLeafs = 0
firstStr = list(myTree.keys())[0]
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]).__name__ == 'dict':
numLeafs += getNumLeafs(secondDict[key])
else:
numLeafs += 1
return numLeafs
# 计算树的最大深度
def getTreeDepth(myTree):
maxDepth = 0
firstStr = list(myTree.keys())[0]
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]).__name__ == 'dict':
thisDepth = 1 + getTreeDepth(secondDict[key])
else:
thisDepth = 1
if thisDepth > maxDepth:
maxDepth = thisDepth
return maxDepth
# 画出节点
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction', \
xytext=centerPt, textcoords='axes fraction', va="center", ha="center", \
bbox=nodeType, arrowprops=arrow_args)
# 标箭头上的文字
def plotMidText(cntrPt, parentPt, txtString):
lens = len(txtString)
xMid = (parentPt[0] + cntrPt[0]) / 2.0 - lens * 0.002
yMid = (parentPt[1] + cntrPt[1]) / 2.0
createPlot.ax1.text(xMid, yMid, txtString)
def plotTree(myTree, parentPt, nodeTxt):
numLeafs = getNumLeafs(myTree)
firstStr = list(myTree.keys())[0]
cntrPt = (plotTree.x0ff + \
(1.0 + float(numLeafs)) / 2.0 / plotTree.totalW, plotTree.y0ff)
plotMidText(cntrPt, parentPt, nodeTxt)
plotNode(firstStr, cntrPt, parentPt, decisionNode)
secondDict = myTree[firstStr]
plotTree.y0ff = plotTree.y0ff - 1.0 / plotTree.totalD
for key in secondDict.keys():
if type(secondDict[key]).__name__ == 'dict':
plotTree(secondDict[key], cntrPt, str(key))
else:
plotTree.x0ff = plotTree.x0ff + 1.0 / plotTree.totalW
plotNode(secondDict[key], \
(plotTree.x0ff, plotTree.y0ff), cntrPt, leafNode)
plotMidText((plotTree.x0ff, plotTree.y0ff) \
, cntrPt, str(key))
plotTree.y0ff = plotTree.y0ff + 1.0 / plotTree.totalD
def createPlot(inTree):
fig = plt.figure(1, facecolor='white')
fig.clf()
axprops = dict(xticks=[], yticks=[])
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)
plotTree.totalW = float(getNumLeafs(inTree))
plotTree.totalD = float(getTreeDepth(inTree))
plotTree.x0ff = -0.5 / plotTree.totalW
plotTree.y0ff = 1.0
plotTree(inTree, (0.5, 1.0), '')
plt.show()
if __name__=='__main__':
createPlot()
建立一个test.csv文件,放入测试数据。
接下来就可以写主程序了,代码如下。
from math import log
import operator
import treePlotter #画出决策树的包
'''计算信息熵'''
def calcShannonEnt(dataSet):
numEntries = len(dataSet) #计算数据集中的项目数
labelCounts = {}
#为所有可能的分类创建字典
for featVec in dataSet:
currentLabel = featVec[-1] #取出数据集的最后一列
if currentLabel not in labelCounts.keys(): #如果属性不在字典中
labelCounts[currentLabel] = 0 #加入字典
labelCounts[currentLabel] += 1 #当前属性出现次数加1
#计算当前数据集的香农熵并返回
shannonEnt = 0.0
for key in labelCounts: #遍历字典labelCounts的键值对
prob = float(labelCounts[key]) / numEntries #计算每个属性出现的概率
shannonEnt = -prob * log(prob, 2) #熵计算公式
return shannonEnt
'''划分数据集'''
def splitDataSet(dataSet, axis, value):
retDataSet = []
for featVec in dataSet:
if featVec[axis] == value: #如果数据集的目标特征值等于value
reducedFeatVec = featVec[:axis] #抽取数据集中的目标特征值列之前的所有列
reducedFeatVec.extend(featVec[axis+1:]) #抽取数据集中的目标特征值列之后的所有列
retDataSet.append(reducedFeatVec)
return retDataSet #返回不包含第axis列且第axis列值与value相等的数据集
'''以信息增益最大为原则,选择最好的数据集划分方式'''
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #获取总特征数量
baseEntropy = calcShannonEnt(dataSet) #计算原始数据集熵
bestInfoGain = 0.0 #记录信息增益
bestFeature = -1 #和原始数据集熵差最大的划分对应的属性的索引
#创建唯一的分类列表
for i in range(numFeatures):
featList = [example[i] for example in dataSet]
uniqueVals = set(featList) #获取该特征所有不同的值
#计算每种划分方式的信息熵
newEntropy = 0.0
for value in uniqueVals: #计算该属性划分下所有划分子集的信息熵,并叠加
subDataSet = splitDataSet(dataSet, i, value) #划分数据子集
prob = len(subDataSet) / float(len(dataSet)) #计算第i列中,特征值为value的概率
newEntropy += prob * calcShannonEnt(subDataSet) #计算以第i个属性为划分方式得到的信息熵
infoGain = baseEntropy - newEntropy #计算信息增益
#选择具有最大信息增益的属性作为测试属性
if infoGain > bestInfoGain:
bestInfoGain = infoGain
bestFeature = i
return bestFeature #返回特征的索引
'''若已经遍历了所有属性,但类别标签不唯一,采用多数表决法决定叶子节点分类'''
def majorityCnt(classList):
classCount = {}
for vote in classList:
if vote not in classCount.keys():
classCount[vote] = 0
classCount[vote] += 1
#根据第1列的键值进行倒序排列
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
'''构建决策树'''
def createTree(dataSet, labels):
classList = [example[-1] for example in dataSet] #保存数据集列表的最后一列
#递归终止条件一:如果数据集内所有分类一致
if classList.count(classList[0]) == len(classList):
return classList[0]
#递归终止条件二:如果所有属性都划分完毕
if len(dataSet[0]) == 1:
return majorityCnt(classList) #将它们都归为一类
bestFeat = chooseBestFeatureToSplit(dataSet) #选择具有最大信息增益的属性作为测试属性
bestFeatLabel = labels[bestFeat] #测试属性对应的类别
myTree = {bestFeatLabel:{}} #创建字典,用于记录最佳分类标签对应的类别和出现次数
del labels[bestFeat] #在属性列表中剔除最佳属性(删掉名字)
featValues = [example[bestFeat] for example in dataSet] #获取最佳划分标签下的所有变量取值
uniqueVals = set(featValues) #唯一化
for value in uniqueVals: #逐行遍历变量取值集合
subLabels = labels[:] #复制类标记,并存储在新的列表变量中
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
return myTree
'''使用决策树进行分类'''
def classify(inputTree, featLabels, testVec):
firstStr = list(inputTree.keys())[0] #当前树的根节点的属性名称
secondDict = inputTree[firstStr] #根节点的所有子节点
featIndex = featLabels.index(firstStr) #找到根节点特征对应的下标
for key in secondDict.keys():
if testVec[featIndex] == key: #获取待分类对象中当前分类的特征值
if type(secondDict[key]).__name__ == 'dict': #如果节点不是叶子节点,则递归
classLabel = classify(secondDict[key], featLabels, testVec) #调用函数本身,直到得到叶子节点
else:
classLabel = secondDict[key] #如果节点是叶子节点,返回当前节点分类标签
return classLabel
def main():
fr = open('test.csv') #打开测试数据集文件
test = [inst.strip().split(',')[0:] for inst in fr.readlines()] #读取数据
testLabels = ['outlook','temperature','humidity','windy','play'] #输入所有属性变量
testTree = createTree(test, testLabels) #生成此数据集的决策树
#createTree函数对属性做了改变,因此需要再次输入
testLabels = ['outlook','temperature','humidity','windy','play']
#调用treePlotter文件的CreatePlot函数绘制决策树
treePlotter.createPlot(testTree)
decision = classify(testTree, testLabels, ['rainy','hot','high','false'])
print("\n" * 5,"决策为:", decision)
main()
运行结果
可以看到,运行结果与我们手工计算的结果一致。
更多推荐
所有评论(0)