本文以经典的西瓜问题为例,决策树原理详见文章 ML学习笔记——决策树
ID3
代码详解
代码来源:https://blog.csdn.net/leaf_zizi/article/details/82848682
数据集
青绿 蜷缩 浊响 清晰 凹陷 硬滑 是
乌黑 蜷缩 沉闷 清晰 凹陷 硬滑 是
乌黑 蜷缩 浊响 清晰 凹陷 硬滑 是
青绿 蜷缩 沉闷 清晰 凹陷 硬滑 是
浅白 蜷缩 浊响 清晰 凹陷 硬滑 是
青绿 稍蜷 浊响 清晰 稍凹 软粘 是
乌黑 稍蜷 浊响 稍糊 稍凹 软粘 是
乌黑 稍蜷 浊响 清晰 稍凹 硬滑 是
乌黑 稍蜷 沉闷 稍糊 稍凹 硬滑 否
青绿 硬挺 清脆 清晰 平坦 软粘 否
浅白 硬挺 清脆 模糊 平坦 硬滑 否
浅白 蜷缩 浊响 模糊 平坦 软粘 否
青绿 稍蜷 浊响 稍糊 凹陷 硬滑 否
浅白 稍蜷 沉闷 稍糊 凹陷 硬滑 否
乌黑 稍蜷 浊响 清晰 稍凹 软粘 否
浅白 蜷缩 浊响 模糊 平坦 硬滑 否
青绿 蜷缩 沉闷 稍糊 稍凹 硬滑 否
ID3主代码
源代码基于python2,在python3环境下会报错,因此以下代码在源代码的基础上稍有修改并添加了详细的注释。
import numpy as np
import pandas as pd
import sklearn.tree as st
import math
import matplotlib
import os
import matplotlib.pyplot as plt
# 以下注释主要针对西瓜数据集
# 计算信息熵
# 参数 - 数据集(二维list)
# 返回值 - 传入的数据集的信息熵(浮点数)
def calcEntropy(dataSet):
mD = len(dataSet) # 数据集行数
dataLabelList = [x[-1] for x in dataSet] # 类别list(是/否)
dataLabelSet = set(dataLabelList) # 类别list -> 类别set(元素不重复,只有‘是’和‘否’)
ent = 0
for label in dataLabelSet: # 遍历两次,一次遍历‘是’类别,一次遍历‘否’类别,遍历结束得到信息熵ent
mDv = dataLabelList.count(label)
prop = float(mDv) / mD
ent = ent - prop * np.math.log(prop, 2)
return ent
# 删除数据集包含某一特征的所有列数据,并返回经过删除的曾包含该特征的list的数据集(二维list)
# 参数
# index - 要拆分的特征的下标(整型数),代表属性
# feature - 要拆分的特征(字符串)
# 返回值 - dataSet中所有特征是feature的那一行数据中去掉该feature的数据集(二维list)
def splitDataSet(dataSet, index, feature):
splitedDataSet = [] # 初始化已拆分的数据集splitedDataSet,类型为list
for data in dataSet: # 遍历数据集的每一行
if (data[index] == feature): # 如果是我们要拆分出来的特征feature
sliceTmp = data[:index] # 新建一个列表sliceTmp,先取这一行feature前面的所有数据
sliceTmp.extend(data[index + 1:]) # 再取这一行feature后面的所有数据
splitedDataSet.append(sliceTmp) # 将sliceTmp加到splitedDataSet中去
return splitedDataSet
# 选择最优划分属性(信息增益法)
# 参数 - 数据集(二维list)
# 返回值 - 最好的特征的下标(整型数)
def chooseBestFeature(dataSet):
entD = calcEntropy(dataSet) # 计算信息熵
mD = len(dataSet) # 数据集的行数
featureNumber = len(dataSet[0]) - 1 # 数据集的特征数,减1是因为数据集最后一列是类型而不是特征
maxGain = -100 # 初始化最大信息增益
maxIndex = -1 # 初始化最大信息增益对应的列号(属性)
for i in range(featureNumber): # 遍历数据集的所有列(属性),i:0,1,...,featureNumber-1
entDCopy = entD
featureI = [x[i] for x in dataSet] # 特征向量(第i列,即第i个属性)
featureSet = set(featureI) # 不含重复元素的特征集合(第i列,即第i个属性)
for feature in featureSet: # 遍历该特征集合中的特征feature
splitedDataSet = splitDataSet(dataSet, i, feature) # 含feature的数据集,但不包含feature的一列
mDv = len(splitedDataSet) # 行数,即含feature的数据个数
entDCopy = entDCopy - float(mDv) / mD * calcEntropy(splitedDataSet)
# 为什么这里可以直接计算splitedDataSet的信息熵?
# 因为计算信息熵时只需要数据集中最后一列的数据(类型),有无含feature的一列对计算结果无影响
if (maxIndex == -1):
maxGain = entDCopy
maxIndex = i
elif (maxGain < entDCopy): # 找到了更大的信息增益,即找到了更优的划分属性
maxGain = entDCopy
maxIndex = i
return maxIndex
# 返回数量最多的类别
# 参数 - 类别向量(一维list)
# 返回值 - 类别(字符串)
def mainLabel(labelList):
labelRec = labelList[0]
maxLabelCount = -1 # 数量最多的类别的类别数
labelSet = set(labelList) # 无重复元素的类别集合
for label in labelSet: # 遍历该集合中的类别
if (labelList.count(label) > maxLabelCount): # 如果传入的类别向量中当前类别的数量比maxLabelCount大,更新数量最多的类别
maxLabelCount = labelList.count(label)
labelRec = label
return labelRec
# 构建决策树
def createFullDecisionTree(dataSet, featureNames, featureNamesSet, labelListParent):
labelList = [x[-1] for x in dataSet] # 取数据集最后一列为类别向量
if (len(dataSet) == 0): # 数据集为空集
return mainLabel(labelListParent)
if (len(dataSet[0]) == 1): # 没有可划分的属性了
return mainLabel(labelList) # 选出最多的类别作为该数据集的标签
if (labelList.count(labelList[0]) == len(labelList)): # 全部都属于同一个类别
return labelList[0]
bestFeatureIndex = chooseBestFeature(dataSet) # 最好的属性的下标
bestFeatureName = featureNames.pop(bestFeatureIndex) # 从featureNames中删去bestFeature,并返回bestFeature
myTree = {
bestFeatureName: {
}} # 用字典构建决策树
featureList = featureNamesSet.pop(bestFeatureIndex) # 从featureNamesSet中删去bestFeature对应的子属性集,并返回这个最好的属性的子属性集
featureSet = set(featureList) # 转换成set类型
for feature in featureSet:
featureNamesNext = featureNames[:] # 这里的featureNames已经删去了最好的属性
featureNamesSetNext = featureNamesSet[:][:] # 这里的featureNamesSet也已经删去了最好的属性
splitedDataSet = splitDataSet(dataSet, bestFeatureIndex, feature)
# 递归建树
myTree[bestFeatureName][feature] = createFullDecisionTree(splitedDataSet, featureNamesNext, featureNamesSetNext, labelList)
return myTree
# 返回值
# dataSet 数据集
# featureNames 属性集(一维list)
# featureNamesSet 一个二维list,其中每一个元素是对应的属性的所有子属性的集合list
def readWatermelonDataSet():
fr = open(r'C:\Users\静如止水\Desktop\ID3\data.txt', encoding='utf-8')
dataSet = [inst.strip().split(' ') for inst in fr.readlines()]
# print("dataSet =")
# print(dataSet)
# print('\n')
featureNames = ['色泽', '根蒂', '敲击', '纹理', '脐部', '触感']
# 获取featureNamesSet
featureNamesSet = []
for i in range(len(dataSet[0]) - 1):
col = [x[i] for x in dataSet]
colSet = set(col)
featureNamesSet.append(list(colSet))
return dataSet, featureNames, featureNamesSet
基于matplotlib的可视化代码
以下代码与源代码一致,无删改。
# 能够显示中文
matplotlib.rcParams['font.sans-serif'] = ['SimHei']
matplotlib.rcParams['font.serif'] = ['SimHei']
# 分叉节点,也就是决策节点
decisionNode = dict(boxstyle="sawtooth", fc="0.8")
# 叶子节点
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):
"""
获取叶节点的数目
:param myTree:
:return:
"""
# 统计叶子节点的总数
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):
"""
得到数的深度层数
:param myTree:
:return:
"""
# 用来保存最大层数
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=myTree)
# 计算树的深度
depth = getTreeDepth(myTree=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是字典的话,代表还有子树,则递归遍历