本节讲解根据信息增益如何选择划分属性与算法实现,以西瓜数据集2.0为例进行讲解。
1. 理论求信息增益
(理论参考:机器学习算法——决策树1(ID3决策树算法)_Vicky_xiduoduo的博客-CSDN博客)。
编号 | 色泽 | 根蒂 | 敲声 | 纹理 | 脐部 | 触感 | 好瓜 |
---|---|---|---|---|---|---|---|
1 | 青绿 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 是 |
2 | 乌黑 | 蜷缩 | 沉闷 | 清晰 | 凹陷 | 硬滑 | 是 |
3 | 乌黑 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 是 |
4 | 青绿 | 蜷缩 | 沉闷 | 清晰 | 凹陷 | 硬滑 | 是 |
5 | 浅白 | 蜷缩 | 浊响 | 清晰 | 凹陷 | 硬滑 | 是 |
6 | 青绿 | 稍蜷 | 浊响 | 清晰 | 稍凹 | 软粘 | 是 |
7 | 乌黑 | 稍蜷 | 浊响 | 稍糊 | 稍凹 | 软粘 | 是 |
8 | 乌黑 | 稍蜷 | 浊响 | 清晰 | 稍凹 | 硬滑 | 是 |
9 | 乌黑 | 稍蜷 | 沉闷 | 稍糊 | 稍凹 | 硬滑 | 否 |
10 | 青绿 | 硬挺 | 清脆 | 清晰 | 平坦 | 软粘 | 否 |
11 | 浅白 | 硬挺 | 清脆 | 模糊 | 平坦 | 硬滑 | 否 |
12 | 浅白 | 蜷缩 | 浊响 | 模糊 | 平坦 | 软粘 | 否 |
13 | 青绿 | 稍蜷 | 浊响 | 稍糊 | 凹陷 | 硬滑 | 否 |
14 | 浅白 | 稍蜷 | 沉闷 | 稍糊 | 凹陷 | 硬滑 | 否 |
15 | 乌黑 | 稍蜷 | 浊响 | 清晰 | 稍凹 | 软粘 | 否 |
16 | 浅白 | 蜷缩 | 浊响 | 模糊 | 平坦 | 硬滑 | 否 |
17 | 青绿 | 蜷缩 | 沉闷 | 稍糊 | 稍凹 | 硬滑 | 否 |
计算出当前属性集合{色泽,根蒂,敲声,纹理,脐部,触感}中每个属性的信息增益。
信息熵公式:
信息增益公式:
首先求(集合D中第k类样本所占的比例,好瓜占
,坏瓜占
)
以“色泽”(代表属性a)为例,a可能的取值为:{青绿,乌黑,浅白}(属性a上的取值为,记为
)。若使用该属性对样本集合D进行划分,则得到
(色泽=青绿),
(色泽=乌黑),
(色泽=浅白)。接下来,求Ent(
),Ent(
),Ent(
)。
根据信息熵公式,先求(集合D中第k类样本所占的比例)。
包含样本{1,4,6,10,13,17},正例(代表集合
中好瓜所占的比例)占
,反例(代表
中坏瓜所占的比例)占
。
包含样本{2,3,7,8,9,15},正例占
,反例占
。
包含样本{5,11,12,14,16},正例占
,反例占
。
所以三者的信息熵为:
然后求“色泽”的信息增益:
同理,可以得到其他属性的信息增益:
Gain(D,根蒂)=0.143
Gain(D,敲声)=0.141
Gain(D,纹理)=0.381
Gain(D,脐部)=0.289
Gain(D,触感)=0.006
显然,属性“纹理”的信息增益最大。于是它被选为划分属性,作为根节点。
纹理的属性集合为{清晰,稍糊,模糊},以“纹理=清晰”为例,中有编号{1,2,3,4,6,8,10,15}的九个样例,可用属性{色泽,根蒂,敲声,脐部,触感}。基于
计算出各属性的信息增益:
Gain(,色泽)=0.043
Gain(,根蒂)=0.458
Gain(,敲声)=0.331
Gain(,脐部)=0.458
Gain(,触感)=0.458
根蒂、脐部、触感三个属性均取得了最大的信息增益,可任选其中之一作为划分属性。类似的,对每个分支节点进行上述操作,最终得到一棵决策树。
2. 算法实现(ID3算法)
(1)导入所需要的工具包
import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
(2)导入西瓜数据集2.0
def creat_dataset_watermelon2(): #导入西瓜数据集2.0并进行初始化
data_watermelon = pd.read_csv('D:/Machine_Learning/西瓜数据集2.0.csv', encoding='GBK')
dataset_watermelon = data_watermelon.values.tolist() #将数组转化为列表
featureNames = ['色泽', '根蒂', '敲击', '纹理', '脐部', '触感']
#获取特征的值
featureNamesSet = []
for i in range(len(dataset_watermelon[0])-1):
col = [x[i] for x in dataset_watermelon]
colSet = set(col)
featureNamesSet.append(list(colSet))
return dataset_watermelon, featureNames, featureNamesSet
(3)计算信息熵
def calEntropy(dataSet): #求信息熵
mD = len(dataSet)
dataLabelList=[x[-1] for x in dataSet]
dataLabelSet=set(dataLabelList)
ent = 0
for label in dataLabelSet:
mDv=dataLabelList.count(label)
prop=float(mDv)/mD
ent=ent-prop * np.emath.log2(prop)
return ent
(4)进行数据集分割,以便进行求每个属性的信息增益
def splitDataSet(dataSet, index, feature):
#按特征的值,划分数据集
#index要拆分的特征的下标,feature要拆分的特征
split_DataSet = []
for data in dataSet:
if (data[index] == feature):
sliceTmp = data[:index]
sliceTmp.extend(data[index+1:]) #用在列表末尾一次性追加另一个序列中的多个值(用新列表扩展原来的列表)。
split_DataSet.append(sliceTmp)
return split_DataSet
(5)选择最优属性
def chooseBestFeature(dataSet): #求最大的信息增益 return返回最合适特征的索引值i
entD = calEntropy(dataSet)
mD = len(dataSet)
featureNumber = len(dataSet[0])-1
maxGain = 0
maxIndex = -1
for i in range(1, featureNumber):
entDCopy = entD
featureI = [x[i] for x in dataSet]
featureSet = set(featureI)
for feature in featureSet:
splitedDataSet = splitDataSet(dataSet, i, feature) # 拆分数据集
mDv = len(splitedDataSet)
entDCopy = entDCopy - float(mDv) / mD * calEntropy(splitedDataSet)
if(maxIndex == -1):
maxGain = entDCopy
maxIndex = i
elif(maxGain < entDCopy):
maxGain = entDCopy
maxIndex = i
return maxIndex
(6)求解特殊情况下最优属性
def mainLabel(labelList): #返回值是标签
labelRec = labelList[0]
maxLabelCount = -1
labelSet = set(labelList)
for label in labelSet:
if (labelList.count(label) > maxLabelCount):
maxLabelCount = labelList.count(label)
labelRec = label
return labelRec
(7)构建决策树
def CreatDecisionTree(dataSet, featureNames, featureNamesSet, LabellistParent): #构造决策树 LabellistParent=featureNames
LabelList = [x[-1] for x in dataSet]
if(len(dataSet) == 0):#dataset为空集
return mainLabel(LabellistParent)
elif(len(dataSet) == 1): #没有可划分属性了
return mainLabel(LabelList)
elif (LabelList.count(LabelList[0]) == len(LabelList)):#全部属于同一个label
return LabelList[0]
bestFeatureIndex = chooseBestFeature(dataSet)#选择最优属性的索引值
bestFeatureName = featureNames.pop(bestFeatureIndex-1)#将最优属性在列表中删除,将列表中指定位置的元素移除,同时移除的元素赋值给某个变量
myTree = {bestFeatureName: {}}#构造树的字典
featureList = featureNamesSet.pop(bestFeatureIndex) #将最优属性在集合中删除掉。根据键将字典中指定的键值对删除,同时将删除的值赋值给变量
featureSet = set(featureList)
for feature in featureSet:
featureNamesNext = featureNames[:]
featureNamesSetNext = featureNamesSet[:][:]
splited_dataset=splitDataSet(dataSet, bestFeatureIndex, feature)
myTree[bestFeatureName][feature] = CreatDecisionTree(splited_dataset, featureNamesNext, featureNamesSetNext, LabelList)
return myTree
(8)画出决策树
# 能够显示中文
matplotlib.rcParams['font.sans-serif'] = ['SimHei']
matplotlib.rcParams['font.serif'] = ['SimHei']
# 分叉节点,也就是决策节点
decisionNode = dict(boxstyle="round", fc="0.5")
# 叶子节点
leafNode = dict(boxstyle="round4", fc="0.8")
# 箭头样式
arrow_args = dict(arrowstyle="<-")
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
#param nodeTxt: 描述该节点的文本信息
#param centerPt: 文本的坐标
#param parentPt: 点的坐标,这里也是指父节点的坐标
#param nodeType: 节点类型,分为叶子节点和决策节点
#return:
createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',
xytext=centerPt, textcoords='axes fraction',
va="center", ha="center", bbox=nodeType, arrowprops=arrow_args)
def getNumLeafs(myTree):
#获取叶节点的数目
numLeafs = 0
# 得到当前第一个key,也就是根节点
firstStr = list(myTree.keys())[0]
# 得到第一个key对应的内容
secondDict = myTree[firstStr]
# 递归遍历叶子节点
for key in secondDict.keys():
# 如果key对应的是一个字典,就递归调用
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]
# 得到key对应的内容
secondDic = myTree[firstStr]
# 遍历所有子节点
for key in secondDic.keys():
# 如果该节点是字典,就递归调用
if type(secondDic[key]).__name__ == 'dict':
# 子节点的深度加1
thisDepth = 1 + getTreeDepth(secondDic[key])
# 说明此时是叶子节点
else:
thisDepth = 1
# 替换最大层数
if thisDepth > maxDepth:
maxDepth = thisDepth
return maxDepth
def plotMidText(cntrPt, parentPt, txtString):
"""
计算出父节点和子节点的中间位置,填充信息
:param cntrPt: 子节点坐标
:param parentPt: 父节点坐标
:param txtString: 填充的文本信息
:return:
"""
# 计算x轴的中间位置
xMid = (parentPt[0]-cntrPt[0])/2.0 + cntrPt[0]
# 计算y轴的中间位置
yMid = (parentPt[1]-cntrPt[1])/2.0 + cntrPt[1]
# 进行绘制
createPlot.ax1.text(xMid, yMid, txtString)
def plotTree(myTree, parentPt, nodeTxt):
"""
绘制出树的所有节点,递归绘制
:param myTree: 树
:param parentPt: 父节点的坐标
:param nodeTxt: 节点的文本信息
:return:
"""
# 计算叶子节点数
numLeafs = getNumLeafs(myTree)
# 计算树的深度
depth = getTreeDepth(myTree)
# 得到根节点的信息内容
firstStr = list(myTree.keys())[0]
# 计算出当前根节点在所有子节点的中间坐标,也就是当前x轴的偏移量加上计算出来的根节点的中心位置作为x轴(
# 比如说第一次:初始的x偏移量为:-1/2W,计算出来的根节点中心位置为:(1+W)/2W,相加得到:1/2),当前y轴偏移量作为y轴
cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff)
# 绘制该节点与父节点的联系
plotMidText(cntrPt, parentPt, nodeTxt)
# 绘制该节点
plotNode(firstStr, cntrPt, parentPt, decisionNode)
# 得到当前根节点对应的子树
secondDict = myTree[firstStr]
# 计算出新的y轴偏移量,向下移动1/D,也就是下一层的绘制y轴
plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD
# 循环遍历所有的key
for key in secondDict.keys():
# 如果当前的key是字典的话,代表还有子树,则递归遍历
if isinstance(secondDict[key], dict):
plotTree(secondDict[key], cntrPt, str(key))
else:
# 计算新的x轴偏移量,也就是下个叶子绘制的x轴坐标向右移动了1/W
plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW
# 打开注释可以观察叶子节点的坐标变化
# print((plotTree.xOff, plotTree.yOff), secondDict[key])
# 绘制叶子节点
plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode)
# 绘制叶子节点和父节点的中间连线内容
plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key))
# 返回递归之前,需要将y轴的偏移量增加,向上移动1/D,也就是返回去绘制上一层的y轴
plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD
def createPlot(inTree):
"""
需要绘制的决策树
:param inTree: 决策树字典
:return:
"""
# 创建一个图像
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))
# 初始的x轴偏移量,也就是-1/2W,每次向右移动1/W,也就是第一个叶子节点绘制的x坐标为:1/2W,第二个:3/2W,第三个:5/2W,最后一个:(W-1)/2W
plotTree.xOff = -0.5/plotTree.totalW
# 初始的y轴偏移量,每次向下或者向上移动1/D
plotTree.yOff = 1.0
# 调用函数进行绘制节点图像
plotTree(inTree, (0.5, 1.0), '')
# 绘制
plt.show()
(9)调用上述函数
if __name__ == '__main__':
dataset_watermelon, featureNames, featureNamesSet = creat_dataset_watermelon2()
myTree = CreatDecisionTree(dataset_watermelon, featureNames, featureNamesSet, featureNames)
createPlot(myTree)
最后得到的结果如下
欢迎私信探讨讲解不清楚的地方!
C4.5算法与CART算法与ID3算法的实现类似,区别在于最优选择属性选择上,其余代码一致,不再赘述。
下一章将进行剪枝处理实例讲解以及代码实现。
欢迎各位关注,一起探讨学习机器学习算法!