作者:張張張張
github地址:https://github.com/zhanghekai
【转载请注明出处,谢谢!】
ID3决策树 西瓜数据集2.0案例
项目概述:
根据以下6个特征,将西瓜分成两类:好瓜和坏瓜
特征:
1.西瓜的颜色
2.西瓜的根卷曲程度
3.敲打西瓜发出的声音
4.西瓜表面的纹理
5.西瓜脐部凹陷情况
6.抚摸西瓜时的触感
开发流程:
收集数据集:可以使用任何方法.
准备数据:树构造算法(这里使用ID3算法,因此数值型数据必须离散化。)
分析数据:可以使用任何方法,构造书完成之后,我们可以将树画出来。
训练算法:构造树结构。
测试算法:使用习得的决策树执行分类。
使用算法:此步骤可以适用于任何监督学习任务,而使用决策树可以更好地理解数据的内在含义。
import math
import operator
import numpy as np
一、收集数据:利用createDataSet()函数输入数据
def createDataSet():
# dataSet中最后一列记录的是类别,其余列记录的是特征值
dataSet = [
['青绿','蜷缩','浊响','清晰','凹陷','硬滑','是'],
['乌黑','蜷缩','沉闷','清晰','凹陷','硬滑','是'],
['乌黑','蜷缩','浊响','清晰','凹陷','硬滑','是'],
['青绿','蜷缩','沉闷','清晰','凹陷','硬滑','是'],
['浅白','蜷缩','浊响','清晰','凹陷','硬滑','是'],
['青绿','稍蜷','浊响','清晰','稍凹','软粘','是'],
['乌黑','稍蜷','浊响','稍糊','稍凹','软粘','是'],
['乌黑','稍蜷','浊响','清晰','稍凹','硬滑','是'],
['乌黑','稍蜷','沉闷','稍糊','稍凹','硬滑','否'],
['青绿','硬挺','清脆','清晰','平坦','软粘','否'],
['浅白','硬挺','清脆','模糊','平坦','硬滑','否'],
['浅白','蜷缩','浊响','模糊','平坦','软粘','否'],
['青绿','稍蜷','浊响','稍糊','凹陷','硬滑','否'],
['浅白','稍蜷','沉闷','稍糊','凹陷','硬滑','否'],
['乌黑','稍蜷','浊响','清晰','稍凹','软粘','否'],
['浅白','蜷缩','浊响','模糊','平坦','硬滑','否'],
['青绿','蜷缩','沉闷','稍糊','稍凹','硬滑','否']
]
# label中记录的是特征的名称
labels = ['色泽','根蒂','敲声','纹理','脐部','触感']
return dataSet, labels
二、准备数据
由于我们输入的数据本身就是离散化数据,所以这一步就省略了。
三、分析数据
使用信息增益的方法:
四、算法分析
1.对于每一个结点,首先会将其视为叶节点,计算它的信息熵,所以把“结点计算信息熵”写做一个函数。
2.对于每一个节点都有一个数据集,该数据集是根据其“特征”父节点下的属性划分出来的,所以把“数据集划分”写做一个函数。
3.对每一个结点都会选择最优特征,对每一个“特征”都会计算其信息增益,所以把“计算信息增益,并返回最优特征”写做一个函数。
4.停止划分结点的三种情况:a)所有的类标签完全相同,则直接返回该类标签;b)使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组,那么出现相同label多的一类,作为结果;c)如果该结点为空集,则将其设为叶子结点,节点类型为其父节点中类别最多的类。
1.计算给定数据集的信息熵的函数
由于每需要确定一个节点,就需要计算一次该节点的信息熵,于是我们将它写成函数,方便调用。
def calcShannonEnt(dataSet):
# 计算参与训练的数据量,即训练集中共有多少条数据;用作信息熵概率 p 的分母
numEntries = len(dataSet)
# 计算每个分类标签label在当前节点出现的次数,即每个类别在当前节点下别分别出现多少次,用作信息熵概率 p 的分子
labelCounts = {}
# 每次读入一条样本数据
for featVec in dataSet:
# 将当前实例的类别存储,即每一行数据的最后一个数据代表的是类别
currentLabel = featVec[-1]
#为所有分类创建字典,每个键值对都记录了当前类别出现的次数
if currentLabel not in labelCounts:
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
# 使用循环方法,依次求出公式求和部分每个类别所占的比例及相应的计算
shannonEnt = 0 #记录信息熵的值
for key in labelCounts:
# 算出该类别在该节点数据集中所占的比例
prob = labelCounts[key] / numEntries
shannonEnt -= prob * math.log(prob, 2)
return shannonEnt
上述巧妙的将信息熵的公式写为:
2.按照给定的特征划分子数据集
通过遍历dataSet数据集,求出去除父节点特征后的其余特征组合成的数据集。
'''
1.dataSet为该节点的父节点使用的数据集
2.retDataSet为某特征下去除特定属性后其余的数据
3.value为某个特征下的其中一个属性
4.index为当前操作的是哪一个特征
'''
def splitDataSet(dataSet, index, value):
# 存储划分好的数据集
retDataSet = []
for featVec in dataSet:
if featVec[index] == value:
reducedFeatVec = featVec[:index]
reducedFeatVec.extend(featVec[index+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
以特征“色泽”下的属性“乌黑”为例:
dataSet, _ ,_,_,_= createDataSet()
splitDataSet(dataSet, 0, "乌黑")
输出结果为:
[['蜷缩', '沉闷', '清晰', '凹陷', '硬滑', '是'],
['蜷缩', '浊响', '清晰', '凹陷', '硬滑', '是'],
['稍蜷', '浊响', '稍糊', '稍凹', '软粘', '是'],
['稍蜷', '浊响', '清晰', '稍凹', '硬滑', '是'],
['稍蜷', '沉闷', '稍糊', '稍凹', '硬滑', '否'],
['稍蜷', '浊响', '清晰', '稍凹', '软粘', '否']]
3.选择最好的特征划分数据集
目的是找到用来划分该节点的特征。
def chooseBestFeatureToSplit(dataSet):
# 求该数据集中共有多少特征(由于最后一列为label标签,所以减1)
numFeatures = len(dataSet[0]) - 1
# 将该结点视为叶子节点,计算该节点的信息熵
baseEntropy = calcShannonEnt(dataSet)
# 初始化最优信息增益值,和最优的特征索引
bestInfoGain, bestFeature = 0, -1
# 依次遍历每一个特征,计算其信息增益
# 当前数据集中有几列就说明有几个特征
for i in range(numFeatures):
# 将数据集中每一个特征下的所有属性拿出来生成一个列表
featList = [example[i] for example in dataSet]
# 使用集合对列表进行去重,获得去重后的集合,即:此时集合中每个属性只出现一次
uniqueVals = set(featList)
# 创建一个临时信息熵
newEntropy = 0
# 依次遍历该特征下的所有属性
for value in uniqueVals:
# 依据每一个属性划分数据集
subDataSet = splitDataSet(dataSet,i,value) # 详情见splitDataSet函数
# 计算该属性包含的数据占该特征包含数据的比例
pro = len(subDataSet) / len(dataSet)
# 计算每个属性结点的信息熵,并乘以相应的权重,再求他们的和,即为该特征节点的信息熵
newEntropy += pro * calcShannonEnt(subDataSet)
# 计算信息增益
infoGain = baseEntropy - newEntropy
# 比较所有特征中的信息增益,返回最好特征划分的索引值
if (infoGain > bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
# 返回最优的特征索引
return bestFeature
4.返回该节点数据集中类别出现次数最多的类别
'''classList:为标签列的列表形式'''
def majorityCnt(classList):
classCount = {}
for vote in classList:
if vote not in classCount.keys():
classCount[vote] = 0
classCount[vote] += 1 # classCount以字典形式记录每个类别出现的次数
# 倒叙排列classCount得到一个字典集合,然后取出第一个就是结果(好瓜/坏瓜),即出现次数最多的结果
# sortedClassCount的形式是[(),(),(),...],每个键值对变为元组并以列表形式返回
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1),reverse=True)
return sortedClassCount[0][0] # 返回的是出现类别次数最多的“类别”
例:
a ={'a':5,'b':8,'c':10}
a.items()
输出结果为:
dict_items([('a', 5), ('b', 8), ('c', 10)])
operator.itemgetter(1):表示按元组中 1 号位索引位置进行排序,即:按字典中value值进行排序,也就是按照每个类别出现的次数排序。
5.训练算法:构造树的数据结构
def createTree(dataSet, labels,FutherClassList,inidata,inilabels):
# 如果该结点为空集,则将其设为叶子结点,节点类型为其父节点中类别最多的类。
if len(dataSet) == 0:
return majorityCnt(FutherClassList)
# classList中记录了该节点数据中“类别”一列,保存为列表形式
classList = [example[-1] for example in dataSet]
# 第一个停止条件:所有的类标签完全相同,则直接返回该类标签
# 如果数据集到最后一列的第一个值出现的次数=整个集合的数量,也就说只有一个类别,直接返回结果即可
if classList.count(classList[0]) == len(classList): # count()函数是统计括号中的值在list中出现的次数
return classList[0]
# 第二个停止条件:使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组
# 如果最后只剩一个特征,那么出现相同label多的一类,作为结果
if len(dataSet[0]) == 1: # 所有的特征都用完了,只剩下最后的标签列了
return majorityCnt(classList)
# 选择最优的特征
bestFeat = chooseBestFeatureToSplit(dataSet)# 返回的是最优特征的索引
# 获取最优特征
bestFeatLabel = labels[bestFeat]
# 初始化决策树
myTree = {bestFeatLabel:{}}
#将使用过的特征数据删除
del(labels[bestFeat])
# 在数据集中去除最优特征列,然后用最优特征的分支继续生成决策树
indexF = inilabels.index(bestFeatLabel)
featValues = [example[indexF] for example in inidata]
uniqueVals = set(featValues)
# 遍历该特征下每个属性节点,继续生成决策树
for value in uniqueVals:
# 求出剩余的可用的特征
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),subLabels,classList,inidata,inilabels)
return myTree
构建好的决策树文本输出格式如下:
dataSet,labels, classList, inidata, inilabels = createDataSet()
myTree = createTree(dataSet, labels,classList,inidata,inilabels)
print(myTree)
输出结果如下:
{'纹理': {'稍糊': {'触感': {'硬滑': '否', '软粘': '是'}}, '清晰': {'根蒂': {'稍蜷': {'色泽': {'浅白': '是', '乌黑': {'触感': {'硬滑': '是', '软粘': '否'}}, '青绿': '是'}}, '硬挺': '否', '蜷缩': '是'}}, '模糊': '否'}}
6.最终生成的决策树图形形式如下:
7.测试算法:使用决策树执行分类
inputTree:是构建好的决策树模型
featLabels:是特征的名称
testVec:测试输入的数据,是对应featLabels中特征下的属性名称
def classify(inputTree, featLabels, testVec):
# 拿到当前树模型的根节点
firstStr = list(inputTree.keys())[0]
# 拿到当前树模型去除根节点后的分支树
secondDict = inputTree[firstStr]
# 返回根节点特征在特征列表中的索引
featIndex = featLabels.index(firstStr)
# 在测试集中找到该特征下的属性
key = testVec[featIndex]
# 根据该属性找到下一层的特征节点
valueOfFeat = secondDict[key]
# 输出查看类别判定过程:根据特征,选择属性
print('+++',firstStr,'xxx',secondDict,'---',key,'>>>',valueOfFeat)
# 判断分支是否结束;判断valueOfFeat是否是dict类型
# 若是dict类型,则表示该节点非叶子节点,继续递归;
# 若不是dict类型,则表示该节点是叶子节点,已经分类完成,可以打印类别
if isinstance(valueOfFeat,dict):
classLabel = classify(valueOfFeat,featLabels,testVec)
else:
classLabel= valueOfFeat
return classLabel
此处使用的是训练集中的数据,意在了解决策树的分类过程,实际应用中这里输入的应该是模型没有见过的测试集。
featLabels=['色泽','根蒂','敲声','纹理','脐部','触感']
testVec = ['青绿','蜷缩','沉闷','稍糊','稍凹','硬滑']
classify(myTree,featLabels,testVec)
输出结果如下:
+++ 纹理 xxx {'稍糊': {'触感': {'硬滑': '否', '软粘': '是'}}, '清晰': {'根蒂': {'稍蜷': {'色泽': {'浅白': '是', '乌黑': {'触感': {'硬滑': '是', '软粘': '否'}}, '青绿': '是'}}, '硬挺': '否', '蜷缩': '是'}}, '模糊': '否'} --- 稍糊 >>> {'触感': {'硬滑': '否', '软粘': '是'}}
+++ 触感 xxx {'硬滑': '否', '软粘': '是'} --- 硬滑 >>> 否
'否'
=============================================
C4.5决策树 西瓜数据集2.0案例
C4.5大致思路与ID3相同,唯一的差别是最优特征选择的标准使用的是信息增益率。
信息增益率选取规则:先从候选划分特征中找出信息增益率高于平均水平的特征,再从中选择增益率最高的。
def C45_chooseBestFeatureToSplit(dataSet):
# 求该数据集中共有多少特征(由于最后一列为label标签,所以减1)
numFeatures = len(dataSet[0]) - 1
# 将该结点视为叶子节点,计算该节点的信息熵
baseEntropy = calcShannonEnt(dataSet)
# 初始化最优增益值率,和最优的特征索引
bestInfoGainRatio, bestFeature = 0, -1
# 记录每个特征的信息增益,以列表形式存储
eachEntropy = []
# 记录每个特征下的属性数据集占特征数据集的比例
eachFeaturesPro = []
# 依次遍历每一个特征,计算其信息增益
# 当前数据集中有几列就说明有几个特征
for i in range(numFeatures):
# 将数据集中每一个特征下的所有属性拿出来生成一个列表
featList = [example[i] for example in dataSet]
# 使用集合对列表进行去重,获得去重后的集合,即:此时集合中每个属性只出现一次
uniqueVals = set(featList)
# 创建一个临时信息熵
newEntropy = 0
# 记录该特征下的属性数据集占特征数据集的比例
eachpro = []
# 依次遍历该特征下的所有属性
for value in uniqueVals:
# 依据每一个属性划分数据集
subDataSet = splitDataSet(dataSet,i,value) # 详情见splitDataSet函数
# 计算该属性包含的数据占该特征包含数据的比例
pro = len(subDataSet) / len(dataSet)
# 计算每个属性结点的信息熵,并乘以相应的权重,再求他们的和,即为该特征节点的信息熵
newEntropy += pro * calcShannonEnt(subDataSet)
# 将该属性数据集数量占特征数据集比例加入到字典
eachpro += [pro]
eachFeaturesPro.append(eachpro)
# 计算该特征信息增益
infoGain = baseEntropy - newEntropy
'''1.把每个特征的信息增益保存起来
2.求所有特征信息增益的平均值
3.对信息增益大于平均值的特征求增益率
4.选择增益率最大的特征'''
eachEntropy += [infoGain]
# 对所有特征信息增益取平均值
aveEntropy = np.mean(eachEntropy)
# 对信息增益大于平均值的特征求增益率
for j in range(numFeatures):
# 之所以有等于号,是因为可能会出现所有值相等的情况,这样平均值也与他们的值相等
if eachEntropy[j] >= aveEntropy:
IVa = 0
for prob in eachFeaturesPro[j]:
IVa -= prob * math.log(prob, 2)
newGainRatio = eachEntropy[j] / IVa
if newGainRatio >= bestInfoGainRatio:
bestInfoGainRatio = newGainRatio
bestFeature = j
# 返回最优的特征索引
return bestFeature
def C45_createTree(dataSet, labels,FutherClassList,inidata,inilabels):
# 如果该结点为空集,则将其设为叶子结点,节点类型为其父节点中类别最多的类。
if len(dataSet) == 0:
return majorityCnt(FutherClassList)
# classList中记录了该节点数据中“类别”一列,保存为列表形式
classList = [example[-1] for example in dataSet]
# 第一个停止条件:所有的类标签完全相同,则直接返回该类标签
# 如果数据集到最后一列的第一个值出现的次数=整个集合的数量,也就说只有一个类别,直接返回结果即可
if classList.count(classList[0]) == len(classList): # count()函数是统计括号中的值在list中出现的次数
return classList[0]
# 第二个停止条件:使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组
# 如果最后只剩一个特征,那么出现相同label多的一类,作为结果
if len(dataSet[0]) == 1: # 所有的特征都用完了,只剩下最后的标签列了
return majorityCnt(classList)
# 选择最优的特征
bestFeat = C45_chooseBestFeatureToSplit(dataSet)# 返回的是最优特征的索引
# 获取最优特征
bestFeatLabel = labels[bestFeat]
# 初始化决策树
C45_myTree = {bestFeatLabel:{}}
#将使用过的特征数据删除
del(labels[bestFeat])
# 在数据集中去除最优特征列,然后用最优特征的分支继续生成决策树
indexF = inilabels.index(bestFeatLabel)
featValues = [example[indexF] for example in inidata]
uniqueVals = set(featValues)
# 遍历该特征下每个属性节点,继续生成决策树
for value in uniqueVals:
# 求出剩余的可用的特征
subLabels = labels[:]
C45_myTree[bestFeatLabel][value] = C45_createTree(splitDataSet(dataSet,bestFeat,value),subLabels,classList,inidata,inilabels)
# print('myTree',value,myTree)
return C45_myTree
dataSet,labels, classList, inidata, inilabels = createDataSet()
C45_myTree = C45_createTree(dataSet, labels,classList,inidata,inilabels)
print(C45_myTree)
输出结果如下:
{'纹理': {'稍糊': {'触感': {'硬滑': '否', '软粘': '是'}}, '清晰': {'触感': {'硬滑': '是', '软粘': {'脐部': {'平坦': '否', '稍凹': {'色泽': {'浅白': '是', '乌黑': '否', '青绿': '是'}}, '凹陷': '否'}}}}, '模糊': '否'}}
最终生成的决策树如下:
=============================================
CART 决策树 西瓜数据集2.0案例
修改部分函数内容如下。
每一个特征下的属性节点都需要求基尼值
def CART_Gini(dataSet):
# 计算参与训练的数据量,即训练集中共有多少条数据;
numEntries = len(dataSet)
# 计算每个分类标签label在当前节点出现的次数,即每个类别在当前节点下别分别出现多少次,用作信息熵概率 p 的分子
labelCounts = {}
# 每次读入一条样本数据
for featVec in dataSet:
# 将当前实例的类别存储,即每一行数据的最后一个数据代表的是类别
currentLabel = featVec[-1]
#为所有分类创建字典,每个键值对都记录了当前类别出现的次数
if currentLabel not in labelCounts:
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
# 使用循环方法,依次求出公式求和部分每个类别所占的比例及相应的计算
Gini = 1 #记录基尼值
for key in labelCounts:
# 算出该类别在该节点数据集中所占的比例
prob = labelCounts[key] / numEntries
Gini -= prob * prob
return Gini
选择最优的特征进行划分数据集
def CART_chooseBestFeatureToSplit(dataSet):
# 求该数据集中共有多少特征(由于最后一列为label标签,所以减1)
numFeatures = len(dataSet[0]) - 1
# 初始化最优基尼指数,和最优的特征索引
bestGiniIndex, bestFeature = 1, -1
# 依次遍历每一个特征,计算其基尼指数
# 当前数据集中有几列就说明有几个特征
for i in range(numFeatures):
# 将数据集中每一个特征下的所有属性拿出来生成一个列表
featList = [example[i] for example in dataSet]
# 使用集合对列表进行去重,获得去重后的集合,即:此时集合中每个属性只出现一次
uniqueVals = set(featList)
# 创建一个临时基尼指数
newGini_index = 0
# 依次遍历该特征下的所有属性
for value in uniqueVals:
# 依据每一个属性划分数据集
subDataSet = splitDataSet(dataSet,i,value) # 详情见splitDataSet函数
# 计算该属性包含的数据占该特征包含数据的比例
pro = len(subDataSet) / len(dataSet)
# 计算每个属性结点的基尼值,并乘以相应的权重,再求他们的和,即为该特征节点的基尼指数
newGini_index += pro * CART_Gini(subDataSet)
# 比较所有特征中的基尼指数,返回最好特征划分的索引值,注意:基尼指数越小越好
if (newGini_index < bestGiniIndex):
bestGiniIndex = newGini_index
bestFeature = i
# 返回最优的特征索引
return bestFeature
创建CART决策树
def CART_createTree(dataSet, labels,FutherClassList,inidata,inilabels):
# 如果该结点为空集,则将其设为叶子结点,节点类型为其父节点中类别最多的类。
if len(dataSet) == 0:
return majorityCnt(FutherClassList)
# classList中记录了该节点数据中“类别”一列,保存为列表形式
classList = [example[-1] for example in dataSet]
# 第一个停止条件:所有的类标签完全相同,则直接返回该类标签
# 如果数据集到最后一列的第一个值出现的次数=整个集合的数量,也就说只有一个类别,直接返回结果即可
if classList.count(classList[0]) == len(classList): # count()函数是统计括号中的值在list中出现的次数
return classList[0]
# 第二个停止条件:使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组
# 如果最后只剩一个特征,那么出现相同label多的一类,作为结果
if len(dataSet[0]) == 1: # 所有的特征都用完了,只剩下最后的标签列了
return majorityCnt(classList)
# 选择最优的特征
bestFeat = CART_chooseBestFeatureToSplit(dataSet)# 返回的是最优特征的索引
# 获取最优特征
bestFeatLabel = labels[bestFeat]
# 初始化决策树
CART_myTree = {bestFeatLabel:{}}
#将使用过的特征数据删除
del(labels[bestFeat])
# 在数据集中去除最优特征列,然后用最优特征的分支继续生成决策树
indexF = inilabels.index(bestFeatLabel)
featValues = [example[indexF] for example in inidata]
uniqueVals = set(featValues)
# 遍历该特征下每个属性节点,继续生成决策树
for value in uniqueVals:
# 求出剩余的可用的特征
subLabels = labels[:]
CART_myTree[bestFeatLabel][value] = CART_createTree(splitDataSet(dataSet,bestFeat,value),subLabels,classList,inidata,inilabels)
return CART_myTree
dataSet,labels, classList, inidata, inilabels = createDataSet()
CART_myTree = CART_createTree(dataSet, labels,classList,inidata,inilabels)
print(CART_myTree)
输出结果如下:
{'纹理': {'稍糊': {'触感': {'硬滑': '否', '软粘': '是'}}, '清晰': {'根蒂': {'稍蜷': {'色泽': {'浅白': '是', '乌黑': {'触感': {'硬滑': '是', '软粘': '否'}}, '青绿': '是'}}, '硬挺': '否', '蜷缩': '是'}}, '模糊': '否'}}
最终生成的决策树如下:
【参考文献】
- apache github主页:https://github.com/apachecn/AiLearning
- 周志华 《机器学习》