这里以机器学习_周志华 第四章决策树的课后习题4.3为例
试编程实现基于信息熵进行划分选择的决策树算法,并为表4.3中的数据生成一颗决策树
数据集为西瓜数据集3.0α:
注释应该已经写的非常非常详细并且明了了,如果已经理解ID3算法的话看一眼注释应该直接就懂了
如果还在犯迷糊的话可以回过头加深一下对于该算法的理解,明白每一个符号代表的具体意义,清楚算法走下来的整个流程,这样效果最好,毕竟最终目的不就是掌握这个算法吗?
话不多说,直接上代码:
from math import log #提供方法log()来计算对数
import collections #提供方法Counter来统计列表中各个元素出现的个数
#创建西瓜数据集3.0α
data = [['青绿', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', 0.697, 0.460, '是'],
['乌黑', '蜷缩', '沉闷', '清晰', '凹陷', '硬滑', 0.774, 0.376, '是'],
['乌黑', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', 0.634, 0.264, '是'],
['青绿', '蜷缩', '沉闷', '清晰', '凹陷', '硬滑', 0.608, 0.318, '是'],
['浅白', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', 0.556, 0.215, '是'],
['青绿', '稍蜷', '浊响', '清晰', '稍凹', '软粘', 0.403, 0.237, '是'],
['乌黑', '稍蜷', '浊响', '稍糊', '稍凹', '软粘', 0.481, 0.149, '是'],
['乌黑', '稍蜷', '浊响', '清晰', '稍凹', '硬滑', 0.437, 0.211, '是'],
['乌黑', '稍蜷', '沉闷', '稍糊', '稍凹', '硬滑', 0.666, 0.091, '否'],
['青绿', '硬挺', '清脆', '清晰', '平坦', '软粘', 0.243, 0.267, '否'],
['浅白', '硬挺', '清脆', '模糊', '平坦', '硬滑', 0.245, 0.057, '否'],
['浅白', '蜷缩', '浊响', '模糊', '平坦', '软粘', 0.343, 0.099, '否'],
['青绿', '稍蜷', '浊响', '稍糊', '凹陷', '硬滑', 0.639, 0.161, '否'],
['浅白', '稍蜷', '沉闷', '稍糊', '凹陷', '硬滑', 0.657, 0.198, '否'],
['乌黑', '稍蜷', '浊响', '清晰', '稍凹', '软粘', 0.360, 0.370, '否'],
['浅白', '蜷缩', '浊响', '模糊', '平坦', '硬滑', 0.593, 0.042, '否'],
['青绿', '蜷缩', '沉闷', '稍糊', '稍凹', '硬滑', 0.719, 0.103, '否']]
#各个维度对应的属性名称
features = ['色泽', '根蒂', '敲声', '纹理', '脐部', '触感', '密度', '含糖率', '好瓜']
#分割离散数据集函数(本程序最重要的函数,用来离散进行数据划分)
def dividDisData(data,dimension,value):
#输入参数:
#data: 当前数据集,不一定是初始创建的数据集,随着数据集的分割会发生变化
#dimension: 选择的维度,也就是对应的属性
#value: 当前选择的维度中的特征值
#返回值:
#dividedData:函数返回值,在当前数据集data中,所有维度为dimension,特征值为value并且删除了dimension这一维特征值的实例
dividedData = []
for vector in data: #遍历每个实例,也就是每一个list元素 eg:vector[0] = ['青绿', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', '是']
if vector[dimension] == value: #如果dimension=0 value='青绿' ,现在先选出所有维度为0特征值为青绿的list元素
vector1 = vector[:dimension] #通过对list进行切片操作来删除这一维的特征值,需要定义vector1和vector2来实现,不能直接对vector进行.pop(dimension)
vector2 = vector[dimension+1:] #否则会出现indexError
vector1.extend(vector2) #vector1与vector2合并,此时这一list元素已删掉了dimension这一维特征值
dividedData.append(vector1)
return dividedData
#分割连续数据集函数(本程序最重要的函数,用来进行连续数据划分)
def dividConData(data,dimension,value,direction):
#输入参数:
#data: 当前数据集,不一定是初始创建的数据集,随着数据集的分割会发生变化
#dimension: 选择的维度,也就是对应的属性
#value: 当前选择的维度中的特征值
#direction: 0:大于value; 1:小于等于value
#返回值:
#dividedDataGreater:函数返回值,在当前数据集data中,所有维度为dimension,特征值为大于value并且删除了dimension这一维特征值的实例
#dividedDataLess: 函数返回值,在当前数据集data中,所有维度为dimension,特征值为小于等于value并且删除了dimension这一维特征值的实例
dividedDataGreater = []
dividedDataLess = []
dimension = int(dimension)
for vector in data: #遍历每个实例,也就是每一个list元素
if vector[dimension] > value: #如果dimension=0 value>0.403 ,现在先选出所有维度为0特征值大于0.403的list元素
vector1 = vector[:dimension] #通过对list进行切片操作来删除这一维的特征值,需要定义vector1和vector2来实现,不能直接对vector进行vector.remove(dimension)
vector2 = vector[dimension+1:] #否则会出现indexError
vector1.extend(vector2) #vector1与vector2合并,此时这一list元素已删掉了dimension这一维特征值
dividedDataGreater.append(vector1)
if vector[dimension] <= value: #如果dimension=0 value>0.403 ,现在先选出所有维度为0特征值大于0.403的list元素
vector1 = vector[:dimension] #通过对list进行切片操作来删除这一维的特征值,需要定义vector1和vector2来实现,不能直接对vector进行vector.remove(dimension)
vector2 = vector[dimension+1:] #否则会出现indexError
vector1.extend(vector2) #vector1与vector2合并,此时这一list元素已删掉了dimension这一维特征值
dividedDataLess.append(vector1)
if direction == 0:
return dividedDataGreater
if direction == 1:
return dividedDataLess
#计算信息熵函数
def calEnt(data):
#输入参数:
#data:当前数据集
#返回值:
#Ent: 函数返回值,当前数据集的信息熵
Ent = 0.0
num = len(data) #当前数据集中所有实例的个数
res = []
for vector in data: #遍历每个实例,将每个实例的最后一个元素存入res中
lastElem = vector[-1]
res.append(lastElem)
resultLabel = collections.Counter(res) #通过Counter方法统计各个元素的频次 eg:Counter({'否': 9, '是': 8})
for value in resultLabel:
frequency = resultLabel[value] #通过键值对,可以获得当特征值为value时此特征值在当前数据集出现的频次
p = float(frequency) / num #计算出概率
Ent -= p * log(p,2) #求和,计算出Ent
return Ent
#计算信息增益函数
def calGain(data,dimension):
#输入参数:
#data: 当前数据集
#dimension:选择的维度,也就是对应的属性
#返回值:
#Gain: 函数返回值,在当前数据集data中,维度为dimension的属性的信息增益
Ent = calEnt(data) #当前数据集data的信息熵
allValuesEnt = 0.0
res=[]
for vector in data:
element = vector[dimension]
res.append(element) #将该维度在当前数据集data中的每个实例对应的特征值放入res中
test = data[0] #获取当前数据集data的第一个实例,用来检查该维度是离散还是连续
if isinstance(test[dimension] , str): #如果是str类型,那么说明是离散变量
resultLabel = collections.Counter(res) #通过Counter方法统计特征值种类
for value in resultLabel: #vaule是当前data内的某一属性dimension的某一特征值
splitedData = dividDisData(data,dimension,value) #分割数据集,获得在当前数据集data中,所有维度为dimension,特征值为value并且删除了dimension这一维特征值的实例
p = len(splitedData) / float(len(data)) #将数据集分割后,通过实例个数来计算当前维度为dimension的集合中每种特征的信息增益
allValuesEnt += p * calEnt(splitedData) #求和,计算出当前维度为dimension的集合中每种特征的信息增益之和
Gain = Ent - allValuesEnt #信息增益 = 当前数据集data的信息熵 - 当前维度为dimension的集合中每种特征的信息增益之和
return Gain
if isinstance(test[dimension] , float): #如果是float类型,那么说明是连续变量
bestGain = 0
t=len(res)
candidatePartitionPoints = [] #用来存放所有候选值
for i in range(t-1): #循环16次,产生16个候选值 range(16):0~15 循环16次,最后一次 res[15] + res[16]
tmp= float((res[i] + res[i+1])/2)
candidatePartitionPoints.append(tmp)
candidatePartitionPoints.sort() #将所有候选值从小到大进行排序
for i in range(t-1): #循环16次,从16个候选值中选出对应信息增益最大的划分点,并返回其划分点的值
dataGreater = dividConData(data,dimension,candidatePartitionPoints[i],0)
dataLess = dividConData(data,dimension,candidatePartitionPoints[i],1)
lenDataGreater = len(dataGreater) #通过dividConData函数可以分别得到比划分点大的Dt+和不大于划分点的Dt-
lenDataLess = len(dataLess)
allValuesEnt = (lenDataGreater / float(len(data))) * calEnt(dataGreater) + (lenDataLess / float(len(data))) * calEnt(dataLess)
Gain = Ent - allValuesEnt
if Gain > bestGain:
bestGain = Gain
return bestGain
#当dimension对应的维度的特征值是连续的的时候时,求出该维度的划分点
def calDivisonPoint(data,dimension):
#输入参数:
#data: 当前数据集
#dimension: 选择的维度,也就是对应的属性
#返回值:
#divisionPoint:函数返回值,在当前数据集data中,维度为dimension的连续属性的划分点
Ent = calEnt(data) #当前数据集data的信息熵
allValuesEnt = 0.0
res=[]
for vector in data:
element = vector[dimension]
res.append(element) #将该维度在当前数据集data中的每个实例对应的特征值放入res中
bestGain = 0
t=len(res)
candidatePartitionPoints = [] #用来存放所有候选值
for i in range(t-1): #循环16次,产生16个候选值 range(16):0~15 循环16次,最后一次 res[15] + res[16]
tmp= float((res[i] + res[i+1])/2)
candidatePartitionPoints.append(tmp)
candidatePartitionPoints.sort() #将所有候选值从小到大进行排序
for i in range(t-1): #循环16次,从16个候选值中选出对应信息增益最大的划分点,并返回其划分点的值
dataGreater = dividConData(data,dimension,candidatePartitionPoints[i],0)
dataLess = dividConData(data,dimension,candidatePartitionPoints[i],1)
lenDataGreater = len(dataGreater) #通过dividConData函数可以分别得到比划分点大的Dt+和不大于划分点的Dt-
lenDataLess = len(dataLess)
allValuesEnt = (lenDataGreater / float(len(data))) * calEnt(dataGreater) + (lenDataLess / float(len(data))) * calEnt(dataLess)
Gain = Ent - allValuesEnt
if Gain > bestGain:
bestGain = Gain
divisionPoint = candidatePartitionPoints[i]
return divisionPoint
#选出划分属性
def chooseMappingAttribute(data):
#输入参数:
#data: 当前数据集
#返回值
#mappingAttribute:函数返回值,当前数据集data的划分属性,但是这是一个整形的变量int,得到的其实是划分属性对应的维度
curGain = 0.0
times = len(data[0]) -1 #得到当前数据集data的属性个数
for i in range(times): #遍历每一个维度特征,并与上一个维度特征进行比较
Gain = calGain(data,i) #返回具体特征的信息增益
if Gain >= curGain: #更新划分属性,直到得到对应信息增益最大的属性,即最终的划分属性
curGain = Gain
mappingAttribute = i
mappingAttribute = int(mappingAttribute) #mappingAttribute就是当前数据集data的划分属性
return mappingAttribute
#生成决策树(运用了递归的思想)
def createTree(data,features):
#输入参数:
#data: 当前数据集
#features:各个维度对应的属性名称
#返回值
#tree: 函数返回值,是一个dict类型的变量,通过对其进行解析便可以得到决策树
res = []
for vector in data: #遍历每个实例,将每个实例中最后一个元素,存入res中
lastElement = vector[-1]
res.append(lastElement) #获得所有实例的最后一个元素
#在不断递归中,数据集被不断分割,此时可能出现两种特殊情况,要特殊处理:
#1.当出现这种情况时,意味着当前数据集data经过了n此分割以后,所有实例中属性为'好瓜'的元素的结果全部相等,也就是类别全部相同,那么意味着这时可以停止划分,返回第一个实例的结果即可
if res.count(res[0]) == len(res):
return res[0]
#2.当出现这种情况时,意味着当前数据集data经过了n此分割以后,所有实例只剩下属性为'好瓜'的元素,直接返回所有实例中出现最多的结果即可
if len(data[0]) == 1:
res = collections.Counter(res)
return res.most_common(1)[0][0]
mappingAttribute = chooseMappingAttribute(data) #得到当前数据集的划分属性
mappingAttributeLabel = features[mappingAttribute] #得到划分属性对应的属性名称
tree = {mappingAttributeLabel:{}} #tree为dict结构,使用此数据结在递归时将结构写入tree时非常方便,通过结果画决策树时也十分容易解析
del (features[mappingAttribute])
#这一点非常重要:每次递归都要删除当前feaature中维度为mappingAttribute的属性。解释如下:
#这和数据集分割函数和chooseMappingAttribute(data)函数有关,此data是经过数据集分割的data,它的返回值mappingAttribute是当前数据集的划分属性,
#而下一次递归时的数据集为dividData(data,mappingAttribute,value),这时的data又经过数据集划分,已经不再是上一步的data,是没有维度为mappingAttribute这一属性的,
#也就是说下一次递归得到mappingAttribute后并不能通过mappingAttribute和初始声明的features中的属性的对应的维度来确认该划分属性的属性名称,
#而是要和去掉维度为mappingAttribute这一属性的数据集去对应才能得到正确的属性名称,否则会出现不对应的错误,比如'触感'的两个分支变成了'>0.420' '<=0.420'这样的情况,
#所以每次递归在得到mappingAttribute后features也要“与时俱进”,删掉维度为mappingAttribute这一属性的属性名称,这样才能保证下一次递归时mappingAttribute对应的属性名称是正确的!
test = data[0]
#两种情况要用不同的方法生成决策树:离散 & 连续:
#1.如果是str类型,那么说明是离散变量
if isinstance(test[mappingAttribute] , str):
result = []
for vector in data: #遍历每个实例,将每个实例中维度为dimension的元素,存入res中
element = vector[mappingAttribute]
result.append(element) #获得所有实例的维度为dimension的特征值
resultLabel = collections.Counter(result) #通过Counter方法获得维度为dimension特征值的所有种类
for value in resultLabel:
delFeatures = features.copy() #通过copy方法复制当前的features,如果下一条语句直接使用features的话会出现indexError
tree[mappingAttributeLabel][value] = createTree(dividDisData(data,mappingAttribute,value),delFeatures)
return tree
#2.如果是float类型,那么说明是连续变量,连续变量只有两种情况,'是'和'否'
if isinstance(test[mappingAttribute] , float):
divPoint = calDivisonPoint(data,mappingAttribute) #得到划分点
point = divPoint #将float型的结果保存在point中,递归时还需要用到这一结果
divPoint = str(divPoint) #转换成str格式
than = []
resultLabel = []
a = ['>', divPoint]
than.append(a[0] + a[1]) #将str(划分点)与'>'合并
b = ['<=', divPoint]
than.append(b[0] + b[1]) #将str(划分点)与'<='合并
resLabel = [than[0], than[1]]
for value in resLabel:
delFeatures = features.copy() #通过copy方法复制当前的features,如果下一条语句直接使用features的话会出现indexError
if value == resLabel[0]: #大于划分点
tree[mappingAttributeLabel][value] = createTree(dividConData(data,mappingAttribute,point,0),delFeatures)
else: #小于划分点
tree[mappingAttributeLabel][value] = createTree(dividConData(data,mappingAttribute,point,1),delFeatures)
return tree
#主函数
print(createTree(data,features))
要注意的有以下几点:
1.数据集中同时包含离散和连续变量,处理连续变量在理解算法的时候并不难,但是在编程的时候显然比离散变量的处理要复杂一些,我在编程的时候可能算法不是很成熟,造成了一些代码冗余,比如calDivisonPoint(data,dimension)这个函数,基本都是一些和calGain(data,dimension)重复的代码
2.对于离散值,每一维度的所有特征值很好统计并且可以直接在决策树中表示出来,但是对于连续值,在决策树中要表示的不是选出的最佳划分点,而是‘>最佳划分点’,‘<=最佳划分点’这样的str类型的值,这里就需要另想办法了:
divPoint = calDivisonPoint(data,mappingAttribute) #得到划分点
point = divPoint #将float型的结果保存在point中,递归时还需要用到这一结果
divPoint = str(divPoint) #转换成str格式
than = []
resultLabel = []
a = ['>', divPoint]
than.append(a[0] + a[1]) #将str(划分点)与'>'合并
b = ['<=', divPoint]
than.append(b[0] + b[1]) #将str(划分点)与'<='合并
resLabel = [than[0], than[1]]
将‘>最佳划分点’,‘<=最佳划分点’依次放入than这一列表中,这样就将这一维度的两个特征值用以str的格式存储在了列表中
3.del (features[mappingAttribute])
这一语句非常重要,如果已经看懂了这个算法肯定能直接理解为什么每次递归都要删除当前feaature中维度为mappingAttribute的属性:
这和数据集分割函数和chooseMappingAttribute(data)函数有关,此data是经过数据集分割的data,它的返回值mappingAttribute是当前数据集的划分属性,而下一次递归时的数据集为数据集分割函数的返回值,这时的data又经过数据集划分,已经不再是上一步的data,是没有维度为mappingAttribute这一属性的,也就是说下一次递归得到mappingAttribute后并不能通过mappingAttribute和初始声明的features中的属性的对应的维度来确认该划分属性的属性名称,而是要和去掉维度为mappingAttribute这一属性的数据集去对应才能得到正确的属性名称,否则会出现不对应的错误,比如’触感’的两个分支变成了’>0.420’ '<=0.420’这样的情况,所以每次递归在得到mappingAttribute后features也要“与时俱进”,删掉维度为mappingAttribute这一属性的属性名称,这样才能保证下一次递归时mappingAttribute对应的属性名称是正确的!
运行的结果为:
{‘纹理’: {‘清晰’: {‘密度’: {’>0.420’: ‘是’,’<=0.420’: {‘含糖率’: {’>0.252’: ‘否’, ‘<=0.252’: ‘是’}}}},‘稍糊’: {‘密度’: {’>0.5735’: ‘否’, ‘<=0.5735’: ‘是’}}, ‘模糊’: ‘否’}}
根据这一结果可以画出决策树:
这个结果和书中的结果不太相同,这是书中给出的结果:
我在刚开始对自己的结果充满了怀疑,认为是哪里出错了,并把各个函数都进行了测试,发现问题出在了这里:calGain(data,dimension) 计算信息增益Gain的函数,当维度为离散维度时结果没有问题,但是当维度为连续维度时就出现问题了
print(calGain(data,6)) #也就是计算表4.3密度这一属性的信息增益
结果为:0.1179805181500242
然而书中给出的结果是0.262,差的非常多,肯定是哪里不一样导致的,经过排查,居然源头在选取最优的划分点进行样本集合的划分这里!
这是书中给出的集合的划分:
这里是我按照式(4.7)计算的:
[0.244, 0.294, 0.442, 0.455, 0.459, 0.477, 0.480, 0.491, 0.509, 0.552, 0.582, 0.621, 0.648, 0.656, 0.704, 0.736]
和书中的划分基本都是不一样的,我完全是按照式(4.7)从(a[i]+a[i+1])/2,i从0到15这样计算的,最后得到16个划分点,然而书中给出的集合的划分貌似并不是按照式(4.7)来计算的,它不是一个挨着一个算,是跳着算,这里的不同就是一切问题的根源了。
我问了问助教这个问题,助教表示这本书上可能会有错误,按照自己理解的来就可以了。
最后,我是个编程菜鸡,这个其实更多是写给自己的,如果哪里出现错误的话欢迎提醒和交流
补充内容:如果不感兴趣或者不需要的话无视即可:
在以上基础上进行剪枝: 训练集:1-5,9-15;验证集:6-8,16-17.
我选择的是预剪枝,其实仅仅需要补充一个函数,其他地方修修补补就足够了:
#判断某一属性是否需要预剪枝
def prePruning(trainData,verifData,dimension):
#输入参数:
#trainData:训练集
#verifData: 测试集
#dimension: 判断是否需要剪枝的属性对应的维度
#返回值
#True: 需要剪枝
#False: 接着划分
t = len(verifData) #验证集的长度
correct = 0
for vector in verifData:
if vector[-1] == '√':
correct += 1
beforeAcc = float(correct / t) #求出该维度对应的属性验证前的精度
test = trainData[0]
if isinstance(test[dimension] , str): #当该维度的特征值为离散值的时候
res=[]
for vector in trainData:
element = vector[dimension]
res.append(element)
resultLabel = collections.Counter(res) #通过Counter方法统计特征值种类
Verification = {} #该字典用来存放经过训练集训练该属性的各个特征值对应的结果
for value in resultLabel:
splitedData = dividDisData(trainData,dimension,value)
res = []
for vector in splitedData:
lastElement = vector[-1]
res.append(lastElement)
res = collections.Counter(res)
result = res.most_common(1)[0][0]
Verification[value] = result #获得经过训练集训练该属性的各个特征值对应的结果
afterCorrect = 0 #接下来开始测试
for vector in verifData:
tmp = vector[dimension]
for value in resultLabel:
if value == tmp:
verifRes = Verification[value]
if verifRes == vector[-2]:
afterCorrect += 1
else:
if vector[-1] == '√': #验证集是需要更新的
vector[-1] = '×'
else:
vector[-1] = '√'
afterAcc = float(afterCorrect / t) #求出该维度对应的属性验证后的精度
if afterAcc > beforeAcc: #如果划分后的验证集的精度大于划分前验证集的精度,那么保留划分,不剪枝
return False
else:
return True
if isinstance(test[dimension] , float): #当该维度的特征值为连续值的时候
divPoint = calDivisonPoint(trainData,dimension) #得到划分点,但这是训练集对应的划分点
point = divPoint #将float型的结果保存在point中,递归时还需要用到这一结果
divPoint = str(divPoint)
than = []
resLabel = []
a = ['>', divPoint]
than.append(a[0] + a[1]) #将str(划分点)与'>'合并
b = ['<=', divPoint]
than.append(b[0] + b[1]) #将str(划分点)与'<='合并
resLabel = [than[0], than[1]]
Verification = {} #该字典用来存放经过训练集训练该属性大于 和 小于等于对应的结果
#大于划分点时:
splitedDataGreater = dividConData(trainData,dimension,point,0)
res = []
for vector in splitedDataGreater:
lastElement = vector[-1]
res.append(lastElement)
res = collections.Counter(res)
result = res.most_common(1)[0][0]
Verification[resLabel[0]] = result
#不大于划分点时:
splitedDateLess = dividConData(trainData,dimension,point,1)
res = []
for vector in splitedDateLess:
lastElement = vector[-1]
res.append(lastElement)
res = collections.Counter(res)
result = res.most_common(1)[0][0]
Verification[resLabel[1]] = result
afterCorrect = 0 #接下来开始测试
for vector in verifData:
tmp = vector[dimension]
if tmp > point:
verifRes = Verification[resLabel[0]]
else:
verifRes = Verification[resLabel[1]]
if verifRes == vector[-2]:
afterCorrect += 1
else:
if vector[-1] == '√':
vector[-1] = '×'
else:
vector[-1] = '√'
afterAcc = float(afterCorrect / t)
if afterAcc > beforeAcc: #如果划分后的验证集的精度大于划分前验证集的精度,那么保留划分,不剪枝
return False
else:
return True
这是全部代码:
from math import log #提供方法log()来计算对数
import collections #提供方法Counter来统计列表中各个元素出现的个数
#创建西瓜数据集3.0α
data = [['青绿', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', 0.697, 0.460, '是'],
['乌黑', '蜷缩', '沉闷', '清晰', '凹陷', '硬滑', 0.774, 0.376, '是'],
['乌黑', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', 0.634, 0.264, '是'],
['青绿', '蜷缩', '沉闷', '清晰', '凹陷', '硬滑', 0.608, 0.318, '是'],
['浅白', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', 0.556, 0.215, '是'],
['青绿', '稍蜷', '浊响', '清晰', '稍凹', '软粘', 0.403, 0.237, '是'],
['乌黑', '稍蜷', '浊响', '稍糊', '稍凹', '软粘', 0.481, 0.149, '是'],
['乌黑', '稍蜷', '浊响', '清晰', '稍凹', '硬滑', 0.437, 0.211, '是'],
['乌黑', '稍蜷', '沉闷', '稍糊', '稍凹', '硬滑', 0.666, 0.091, '否'],
['青绿', '硬挺', '清脆', '清晰', '平坦', '软粘', 0.243, 0.267, '否'],
['浅白', '硬挺', '清脆', '模糊', '平坦', '硬滑', 0.245, 0.057, '否'],
['浅白', '蜷缩', '浊响', '模糊', '平坦', '软粘', 0.343, 0.099, '否'],
['青绿', '稍蜷', '浊响', '稍糊', '凹陷', '硬滑', 0.639, 0.161, '否'],
['浅白', '稍蜷', '沉闷', '稍糊', '凹陷', '硬滑', 0.657, 0.198, '否'],
['乌黑', '稍蜷', '浊响', '清晰', '稍凹', '软粘', 0.360, 0.370, '否'],
['浅白', '蜷缩', '浊响', '模糊', '平坦', '硬滑', 0.593, 0.042, '否'],
['青绿', '蜷缩', '沉闷', '稍糊', '稍凹', '硬滑', 0.719, 0.103, '否']]
#创建训练集
trainData=[['青绿', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', 0.697, 0.460, '是'],
['乌黑', '蜷缩', '沉闷', '清晰', '凹陷', '硬滑', 0.774, 0.376, '是'],
['乌黑', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', 0.634, 0.264, '是'],
['青绿', '蜷缩', '沉闷', '清晰', '凹陷', '硬滑', 0.608, 0.318, '是'],
['浅白', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', 0.556, 0.215, '是'],
['乌黑', '稍蜷', '沉闷', '稍糊', '稍凹', '硬滑', 0.666, 0.091, '否'],
['青绿', '硬挺', '清脆', '清晰', '平坦', '软粘', 0.243, 0.267, '否'],
['浅白', '硬挺', '清脆', '模糊', '平坦', '硬滑', 0.245, 0.057, '否'],
['浅白', '蜷缩', '浊响', '模糊', '平坦', '软粘', 0.343, 0.099, '否'],
['青绿', '稍蜷', '浊响', '稍糊', '凹陷', '硬滑', 0.639, 0.161, '否'],
['浅白', '稍蜷', '沉闷', '稍糊', '凹陷', '硬滑', 0.657, 0.198, '否'],
['乌黑', '稍蜷', '浊响', '清晰', '稍凹', '软粘', 0.360, 0.370, '否']]
#创建验证集
verifData=[['青绿', '稍蜷', '浊响', '清晰', '稍凹', '软粘', 0.403, 0.237, '是', '√'],
['乌黑', '稍蜷', '浊响', '稍糊', '稍凹', '软粘', 0.481, 0.149, '是', '√'],
['乌黑', '稍蜷', '浊响', '清晰', '稍凹', '硬滑', 0.437, 0.211, '是', '√'],
['浅白', '蜷缩', '浊响', '模糊', '平坦', '硬滑', 0.593, 0.042, '否', '×'],
['青绿', '蜷缩', '沉闷', '稍糊', '稍凹', '硬滑', 0.719, 0.103, '否', '×']]
#各个维度对应的属性名称
features = ['色泽', '根蒂', '敲声', '纹理', '脐部', '触感', '密度', '含糖率', '好瓜']
#分割离散数据集函数(本程序最重要的函数,用来离散进行数据划分)
def dividDisData(data,dimension,value):
#输入参数:
#data: 当前数据集,不一定是初始创建的数据集,随着数据集的分割会发生变化
#dimension: 选择的维度,也就是对应的属性
#value: 当前选择的维度中的特征值
#返回值:
#dividedData:函数返回值,在当前数据集data中,所有维度为dimension,特征值为value并且删除了dimension这一维特征值的实例
dividedData = []
for vector in data: #遍历每个实例,也就是每一个list元素 eg:vector[0] = ['青绿', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', '是']
if vector[dimension] == value: #如果dimension=0 value='青绿' ,现在先选出所有维度为0特征值为青绿的list元素
vector1 = vector[:dimension] #通过对list进行切片操作来删除这一维的特征值,需要定义vector1和vector2来实现,不能直接对vector进行.pop(dimension)
vector2 = vector[dimension+1:] #否则会出现indexError
vector1.extend(vector2) #vector1与vector2合并,此时这一list元素已删掉了dimension这一维特征值
dividedData.append(vector1)
return dividedData
#分割连续数据集函数(本程序最重要的函数,用来进行连续数据划分)
def dividConData(data,dimension,value,direction):
#输入参数:
#data: 当前数据集,不一定是初始创建的数据集,随着数据集的分割会发生变化
#dimension: 选择的维度,也就是对应的属性
#value: 当前选择的维度中的特征值
#direction: 0:大于value; 1:小于等于value
#返回值:
#dividedDataGreater:函数返回值,在当前数据集data中,所有维度为dimension,特征值为大于value并且删除了dimension这一维特征值的实例
#dividedDataLess: 函数返回值,在当前数据集data中,所有维度为dimension,特征值为小于等于value并且删除了dimension这一维特征值的实例
dividedDataGreater = []
dividedDataLess = []
dimension = int(dimension)
for vector in data: #遍历每个实例,也就是每一个list元素
if vector[dimension] > value: #如果dimension=0 value>0.403 ,现在先选出所有维度为0特征值大于0.403的list元素
vector1 = vector[:dimension] #通过对list进行切片操作来删除这一维的特征值,需要定义vector1和vector2来实现,不能直接对vector进行vector.remove(dimension)
vector2 = vector[dimension+1:] #否则会出现indexError
vector1.extend(vector2) #vector1与vector2合并,此时这一list元素已删掉了dimension这一维特征值
dividedDataGreater.append(vector1)
if vector[dimension] <= value: #如果dimension=0 value>0.403 ,现在先选出所有维度为0特征值大于0.403的list元素
vector1 = vector[:dimension] #通过对list进行切片操作来删除这一维的特征值,需要定义vector1和vector2来实现,不能直接对vector进行vector.remove(dimension)
vector2 = vector[dimension+1:] #否则会出现indexError
vector1.extend(vector2) #vector1与vector2合并,此时这一list元素已删掉了dimension这一维特征值
dividedDataLess.append(vector1)
if direction == 0:
return dividedDataGreater
if direction == 1:
return dividedDataLess
#计算信息熵函数
def calEnt(data):
#输入参数:
#data:当前数据集
#返回值:
#Ent: 函数返回值,当前数据集的信息熵
Ent = 0.0
num = len(data) #当前数据集中所有实例的个数
res = []
for vector in data: #遍历每个实例,将每个实例的最后一个元素存入res中
lastElem = vector[-1]
res.append(lastElem)
resultLabel = collections.Counter(res) #通过Counter方法统计各个元素的频次 eg:Counter({'否': 9, '是': 8})
for value in resultLabel:
frequency = resultLabel[value] #通过键值对,可以获得当特征值为value时此特征值在当前数据集出现的频次
p = float(frequency) / num #计算出概率
Ent -= p * log(p,2) #求和,计算出Ent
return Ent
#计算信息增益函数
def calGain(data,dimension):
#输入参数:
#data: 当前数据集
#dimension:选择的维度,也就是对应的属性
#返回值:
#Gain: 函数返回值,在当前数据集data中,维度为dimension的属性的信息增益
Ent = calEnt(data) #当前数据集data的信息熵
allValuesEnt = 0.0
res=[]
for vector in data:
element = vector[dimension]
res.append(element) #将该维度在当前数据集data中的每个实例对应的特征值放入res中
test = data[0] #获取当前数据集data的第一个实例,用来检查该维度是离散还是连续
if isinstance(test[dimension] , str): #如果是str类型,那么说明是离散变量
resultLabel = collections.Counter(res) #通过Counter方法统计特征值种类
for value in resultLabel: #vaule是当前data内的某一属性dimension的某一特征值
splitedData = dividDisData(data,dimension,value) #分割数据集,获得在当前数据集data中,所有维度为dimension,特征值为value并且删除了dimension这一维特征值的实例
p = len(splitedData) / float(len(data)) #将数据集分割后,通过实例个数来计算当前维度为dimension的集合中每种特征的信息增益
allValuesEnt += p * calEnt(splitedData) #求和,计算出当前维度为dimension的集合中每种特征的信息增益之和
Gain = Ent - allValuesEnt #信息增益 = 当前数据集data的信息熵 - 当前维度为dimension的集合中每种特征的信息增益之和
return Gain
if isinstance(test[dimension] , float): #如果是float类型,那么说明是连续变量
bestGain = 0
t=len(res)
candidatePartitionPoints = [] #用来存放所有候选值
for i in range(t-1): #循环16次,产生16个候选值 range(16):0~15 循环16次,最后一次 res[15] + res[16]
tmp= float((res[i] + res[i+1])/2)
candidatePartitionPoints.append(tmp)
candidatePartitionPoints.sort() #将所有候选值从小到大进行排序
for i in range(t-1): #循环16次,从16个候选值中选出对应信息增益最大的划分点,并返回其划分点的值
dataGreater = dividConData(data,dimension,candidatePartitionPoints[i],0)
dataLess = dividConData(data,dimension,candidatePartitionPoints[i],1)
lenDataGreater = len(dataGreater) #通过dividConData函数可以分别得到比划分点大的Dt+和不大于划分点的Dt-
lenDataLess = len(dataLess)
allValuesEnt = (lenDataGreater / float(len(data))) * calEnt(dataGreater) + (lenDataLess / float(len(data))) * calEnt(dataLess)
Gain = Ent - allValuesEnt
if Gain > bestGain:
bestGain = Gain
return bestGain
#当dimension对应的维度的特征值是连续的的时候时,求出该维度的划分点
def calDivisonPoint(data,dimension):
#输入参数:
#data: 当前数据集
#dimension: 选择的维度,也就是对应的属性
#返回值:
#divisionPoint:函数返回值,在当前数据集data中,维度为dimension的连续属性的划分点
Ent = calEnt(data) #当前数据集data的信息熵
allValuesEnt = 0.0
res=[]
for vector in data:
element = vector[dimension]
res.append(element) #将该维度在当前数据集data中的每个实例对应的特征值放入res中
bestGain = 0
t=len(res)
candidatePartitionPoints = [] #用来存放所有候选值
for i in range(t-1): #循环16次,产生16个候选值 range(16):0~15 循环16次,最后一次 res[15] + res[16]
tmp= float((res[i] + res[i+1])/2)
candidatePartitionPoints.append(tmp)
candidatePartitionPoints.sort() #将所有候选值从小到大进行排序
for i in range(t-1): #循环16次,从16个候选值中选出对应信息增益最大的划分点,并返回其划分点的值
dataGreater = dividConData(data,dimension,candidatePartitionPoints[i],0)
dataLess = dividConData(data,dimension,candidatePartitionPoints[i],1)
lenDataGreater = len(dataGreater) #通过dividConData函数可以分别得到比划分点大的Dt+和不大于划分点的Dt-
lenDataLess = len(dataLess)
allValuesEnt = (lenDataGreater / float(len(data))) * calEnt(dataGreater) + (lenDataLess / float(len(data))) * calEnt(dataLess)
Gain = Ent - allValuesEnt
if Gain > bestGain:
bestGain = Gain
divisionPoint = candidatePartitionPoints[i]
return divisionPoint
#选出划分属性
def chooseMappingAttribute(data):
#输入参数:
#data: 当前数据集
#返回值
#mappingAttribute:函数返回值,当前数据集data的划分属性,但是这是一个整形的变量int,得到的其实是划分属性对应的维度
curGain = 0.0
times = len(data[0]) -1 #得到当前数据集data的属性个数
for i in range(times): #遍历每一个维度特征,并与上一个维度特征进行比较
Gain = calGain(data,i) #返回具体特征的信息增益
if Gain >= curGain: #更新划分属性,直到得到对应信息增益最大的属性,即最终的划分属性
curGain = Gain
mappingAttribute = i
mappingAttribute = int(mappingAttribute) #mappingAttribute就是当前数据集data的划分属性
return mappingAttribute
#判断某一属性是否需要预剪枝
def prePruning(trainData,verifData,dimension):
#输入参数:
#trainData:训练集
#verifData: 测试集
#dimension: 判断是否需要剪枝的属性对应的维度
#返回值
#True: 需要剪枝
#False: 接着划分
t = len(verifData) #验证集的长度
correct = 0
for vector in verifData:
if vector[-1] == '√':
correct += 1
beforeAcc = float(correct / t) #求出该维度对应的属性验证前的精度
test = trainData[0]
if isinstance(test[dimension] , str): #当该维度的特征值为离散值的时候
res=[]
for vector in trainData:
element = vector[dimension]
res.append(element)
resultLabel = collections.Counter(res) #通过Counter方法统计特征值种类
Verification = {} #该字典用来存放经过训练集训练该属性的各个特征值对应的结果
for value in resultLabel:
splitedData = dividDisData(trainData,dimension,value)
res = []
for vector in splitedData:
lastElement = vector[-1]
res.append(lastElement)
res = collections.Counter(res)
result = res.most_common(1)[0][0]
Verification[value] = result #获得经过训练集训练该属性的各个特征值对应的结果
afterCorrect = 0 #接下来开始测试
for vector in verifData:
tmp = vector[dimension]
for value in resultLabel:
if value == tmp:
verifRes = Verification[value]
if verifRes == vector[-2]:
afterCorrect += 1
else:
if vector[-1] == '√': #验证集是需要更新的
vector[-1] = '×'
else:
vector[-1] = '√'
afterAcc = float(afterCorrect / t) #求出该维度对应的属性验证后的精度
if afterAcc > beforeAcc: #如果划分后的验证集的精度大于划分前验证集的精度,那么保留划分,不剪枝
return False
else:
return True
if isinstance(test[dimension] , float): #当该维度的特征值为连续值的时候
divPoint = calDivisonPoint(trainData,dimension) #得到划分点,但这是训练集对应的划分点
point = divPoint #将float型的结果保存在point中,递归时还需要用到这一结果
divPoint = str(divPoint)
than = []
resLabel = []
a = ['>', divPoint]
than.append(a[0] + a[1]) #将str(划分点)与'>'合并
b = ['<=', divPoint]
than.append(b[0] + b[1]) #将str(划分点)与'<='合并
resLabel = [than[0], than[1]]
Verification = {} #该字典用来存放经过训练集训练该属性大于 和 小于等于对应的结果
#大于划分点时:
splitedDataGreater = dividConData(trainData,dimension,point,0)
res = []
for vector in splitedDataGreater:
lastElement = vector[-1]
res.append(lastElement)
res = collections.Counter(res)
result = res.most_common(1)[0][0]
Verification[resLabel[0]] = result
#不大于划分点时:
splitedDateLess = dividConData(trainData,dimension,point,1)
res = []
for vector in splitedDateLess:
lastElement = vector[-1]
res.append(lastElement)
res = collections.Counter(res)
result = res.most_common(1)[0][0]
Verification[resLabel[1]] = result
afterCorrect = 0 #接下来开始测试
for vector in verifData:
tmp = vector[dimension]
if tmp > point:
verifRes = Verification[resLabel[0]]
else:
verifRes = Verification[resLabel[1]]
if verifRes == vector[-2]:
afterCorrect += 1
else:
if vector[-1] == '√':
vector[-1] = '×'
else:
vector[-1] = '√'
afterAcc = float(afterCorrect / t)
if afterAcc > beforeAcc: #如果划分后的验证集的精度大于划分前验证集的精度,那么保留划分,不剪枝
return False
else:
return True
#生成决策树(运用了递归的思想)
def createTree(data,features):
#输入参数:
#data: 当前数据集
#features: 各个维度对应的属性名称
#返回值
#tree: 函数返回值,是一个dict类型的变量,通过对其进行解析便可以得到决策树
res = []
for vector in data: #遍历每个实例,将每个实例中最后一个元素,存入res中
lastElement = vector[-1]
res.append(lastElement) #获得所有实例的最后一个元素
#在不断递归中,数据集被不断分割,此时可能出现两种特殊情况,要特殊处理:
#1.当出现这种情况时,意味着当前数据集data经过了n此分割以后,所有实例中属性为'好瓜'的元素的结果全部相等,也就是类别全部相同,那么意味着这时可以停止划分,返回第一个实例的结果即可
if res.count(res[0]) == len(res):
return res[0]
#2.当出现这种情况时,意味着当前数据集data经过了n此分割以后,所有实例只剩下属性为'好瓜'的元素,直接返回所有实例中出现最多的结果即可
if len(data[0]) == 1:
res = collections.Counter(res)
return res.most_common(1)[0][0]
mappingAttribute = chooseMappingAttribute(data) #得到当前数据集的划分属性
if(prePruning(trainData,verifData,mappingAttribute)): #说明该划分属性需要被剪枝
res = []
for vector in data: #遍历每个实例,将每个实例中最后一个元素,存入res中
lastElement = vector[-1]
res.append(lastElement) #获得所有实例的最后一个元素
res = collections.Counter(res)
return res.most_common(1)[0][0] #即使被剪枝,也不能什么都不返回,避免在最后的结果中出现'none'
mappingAttributeLabel = features[mappingAttribute] #得到划分属性对应的属性名称
tree = {mappingAttributeLabel:{}} #tree为dict结构,使用此数据结在递归时将结构写入tree时非常方便,通过结果画决策树时也十分容易解析
del (features[mappingAttribute])
#这一点非常重要:每次递归都要删除当前feaature中维度为mappingAttribute的属性。解释如下:
#这和数据集分割函数和chooseMappingAttribute(data)函数有关,此data是经过数据集分割的data,它的返回值mappingAttribute是当前数据集的划分属性,
#而下一次递归时的数据集为dividData(data,mappingAttribute,value),这时的data又经过数据集划分,已经不再是上一步的data,是没有维度为mappingAttribute这一属性的,
#也就是说下一次递归得到mappingAttribute后并不能通过mappingAttribute和初始声明的features中的属性的对应的维度来确认该划分属性的属性名称,
#而是要和去掉维度为mappingAttribute这一属性的数据集去对应才能得到正确的属性名称,否则会出现不对应的错误,比如'触感'的两个分支变成了'>0.420' '<=0.420'这样的情况,
#所以每次递归在得到mappingAttribute后features也要“与时俱进”,删掉维度为mappingAttribute这一属性的属性名称,这样才能保证下一次递归时mappingAttribute对应的属性名称是正确的!
test = data[0]
#两种情况要用不同的方法生成决策树:离散 & 连续:
#1.如果是str类型,那么说明是离散变量
if isinstance(test[mappingAttribute] , str):
result = []
for vector in data: #遍历每个实例,将每个实例中维度为dimension的元素,存入res中
element = vector[mappingAttribute]
result.append(element) #获得所有实例的维度为dimension的特征值
resultLabel = collections.Counter(result) #通过Counter方法获得维度为dimension特征值的所有种类
for value in resultLabel:
delFeatures = features.copy() #通过copy方法复制当前的features,如果下一条语句直接使用features的话会出现indexError
tree[mappingAttributeLabel][value] = createTree(dividDisData(data,mappingAttribute,value),delFeatures)
return tree
#2.如果是float类型,那么说明是连续变量,连续变量只有两种情况,'是'和'否'
if isinstance(test[mappingAttribute] , float):
divPoint = calDivisonPoint(data,mappingAttribute) #得到划分点
point = divPoint #将float型的结果保存在point中,递归时还需要用到这一结果
divPoint = str(divPoint) #转换成str格式
than = []
resultLabel = []
a = ['>', divPoint]
than.append(a[0] + a[1]) #将str(划分点)与'>'合并
b = ['<=', divPoint]
than.append(b[0] + b[1]) #将str(划分点)与'<='合并
resLabel = [than[0], than[1]]
for value in resLabel:
delFeatures = features.copy() #通过copy方法复制当前的features,如果下一条语句直接使用features的话会出现indexError
if value == resLabel[0]: #大于划分点
tree[mappingAttributeLabel][value] = createTree(dividConData(data,mappingAttribute,point,0),delFeatures)
else: #小于划分点
tree[mappingAttributeLabel][value] = createTree(dividConData(data,mappingAttribute,point,1),delFeatures)
return tree
#主函数
print(createTree(data,features))
#如果需要多次运行要删去注释,因为verifData是随着程序运行发生变化的,下一次运行前需要进行复位
'''
stand = [['青绿', '稍蜷', '浊响', '清晰', '稍凹', '软粘', 0.403, 0.237, '是', '√'],
['乌黑', '稍蜷', '浊响', '稍糊', '稍凹', '软粘', 0.481, 0.149, '是', '√'],
['乌黑', '稍蜷', '浊响', '清晰', '稍凹', '硬滑', 0.437, 0.211, '是', '√'],
['浅白', '蜷缩', '浊响', '模糊', '平坦', '硬滑', 0.593, 0.042, '否', '×'],
['青绿', '蜷缩', '沉闷', '稍糊', '稍凹', '硬滑', 0.719, 0.103, '否', '×']]
verifData = stand
'''
运行结果:{‘纹理’: {‘清晰’: ‘是’, ‘稍糊’: ‘否’, ‘模糊’: ‘否’}}
画出决策树:
经过剪枝后决策树果然简洁明了了很多