机器学习实战笔记(Python)- 决策树
决策树算法概述
优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据
缺点:可能会产生过度匹配问题
适用数据范围:数值型和标称型
决策树的一般流程
信息增益
实例:计算数据集的香农熵
from math import log
def createDataSet():
dataSet = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
labels = ['no surfacing','flippers']
#change to discrete values
return dataSet, labels
#计算给定数据集的信息熵
def calcShannonEnt(dataSet):
"""
计算给定数据集的香农熵
"""
numEntries = len(dataSet)
labelCounts = {} #记录每一类标签的数量
"""
Python中的花括号{}:
代表dict字典数据类型,字典是Python中唯一内建的映射类型。
字典中的值没有特殊的顺序,但都是存储在一个特定的键(key)下。键可以是数字、字符串甚至是元祖。
"""
#定义特征向量featVec
for featVec in dataSet:
currentLabel = featVec[-1]
"""
featVec[-1]= yes
featVec[-1]= yes
featVec[-1]= no
featVec[-1]= no
featVec[-1]= no
"""
#print("featVec[-1]=",featVec[-1]) #观察语句
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0 #为所有可能分类创建字典
labelCounts[currentLabel] += 1 #标签currentLabel出现的次数
shannonEnt = 0.0
"""
该项目中labelCounts= {'yes': 2, 'no': 3}
"""
#print("labelCounts=",labelCounts) #观察语句
for key in labelCounts:
prob = float(labelCounts[key])/numEntries
shannonEnt -= prob * log(prob,2) #以2为底数求对数
return shannonEnt
#熵越高,混合的数据也越多。得到熵之后我们就可以按照获取最大信息增益的方法划分数据集
划分数据集
实例:按照给定特征划分数据集
香农熵度量数据集的无序程度,分类算法除了需要测量信息熵,还需要划分数据集,度量花费数据集的熵,以便判断当前是否正确地划分了数据集。我们将对每个特征划分数据集的结果计算一次信息熵,然后判断按照哪个特征划分数据集是最好的划分方式。想象一个分布在二维空间的数据散点图,需要在数据之间划条线,将它们分成两部分,我们应该按照x轴还是轴划线呢?
def splitDataSet(dataSet,axis,value):
"""
按照给定特征划分数据集,返回的是axis为value值的dataset
待划分的数据集
划分数据集的特征
需要返回的特征的值
"""
retDataSet = []
for featVec in dataSet: #dataset中各元素是列表,遍历每个列表
"""
print("featVec[axis]=",featVec[axis])
myDat = [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
featVec[axis]= 1
featVec[axis]= 1
featVec[axis]= 1
featVec[axis]= 0
featVec[axis]= 0
"""
if featVec[axis] == value: #找出第axis元素为value的行
reducedFeatVec = featVec[:axis] #抽取符合特征的数据
"""
reducedFeatVec= []
reducedFeatVec= []
reducedFeatVec= []
"""
reducedFeatVec.extend(featVec[axis+1:]) #把抽取出该特征以后的所有特征组成一个列表
"""
reducedFeatVec= [1, 'yes']
reducedFeatVec= [1, 'yes']
reducedFeatVec= [0, 'no']
"""
retDataSet.append(reducedFeatVec) #创建抽取该特征以后的dataset
return retDataSet
>>> myDat, labels = tree0.createDataSet()
>>> myDat
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
>>> tree0.splitDataSet(myDat,0,1)
[[1, 'yes'], [1, 'yes'], [0, 'no']]
>>> tree0.splitDataSet(myDat,0,0)
[[1, 'no'], [1, 'no']]
实例:选择最好的数据集划分方式
遍历当前特征中的所有唯一属性值,对每个特征划分一次数据集,然后计算数据集的新熵值,并对所有唯一特征值得到的熵求和。信息增益是熵的减少或者是数据无序度的减少,大家肯定对于将熵用于度量数据无序度的减少更容易理解。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。
def chooseBestFeatureToSplit(dataSet):
"""
选取特征
划分数据集
计算出最好的划分数据集的特征
"""
numFeature = len(dataSet[0]) - 1 #获取属性个数,最后一列为label
baseEntropy = calcShannonEnt(dataSet) #计算数据集中的原始香农熵
bestInfoGain = 0.0
bestFeature = -1
#迭代所有属性
for i in range(numFeature):
#featList,获取某一列属性
featList = [example[i] for example in dataSet] #遍历所有属性
#获取属性的值
#集合元素中各个值互不相同,从列表中创建集合是得到唯一元素值最快的方法
uniqueVals = set(featList)
#python的set是一个无序不重复元素集
newEntropy = 0.0
#下面是计算每种划分方式的信息熵,特征i个,每个特征value个值
for value in uniqueVals:
subDataSet = splitDataSet(dataSet,i,value)
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
infoGain = baseEntropy - newEntropy #计算i个特征的信息熵
if(infoGain > bestInfoGain):
#获得最大信息增益的为最好的划分属性
bestInfoGain = infoGain
bestFeature = i
return bestFeature
>>> myDat,labels = tree0.createDataSet()
>>> tree0.chooseBestFeatureToSplit(myDat)
>>> 0
>>> myDat
>>> [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
递归构建决策树
得到原始的数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据将向下传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据。因此我们可以采用递归的原则处理数据集。
实例:多数表决
如C45和CART,这些算法在运行时并不总是在每次划分分组时都会消耗特征。由于特征数目并不是在每次划分数据分组时都减少,因此这些算法在实际使用时可能引起一定的问题。目前我们并不需要考虑这个问题,只需要在算法开始运行前计算列的数目查看算法是否使用了所有属性即可。如果数据集已经处理了所有属性,但是类标签依然不是唯的,此时我们需要决定如何定义该叶子节点,在这种情况下,我们通常会采用多数表决的方法决定该叶子节点的分类。
def majorityCnt(classList):
"""
通过多数表决的方法决定该叶子节点的分类
"""
classCount={} #创建字典,返回出现频率最高label
for vote in classList:
if vote not in classCount.keys():
classCount[vote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
#operator.itemgetter(1),获取对象的第1个域的值 (即第二个数)
return sortedClassCount[0][0]
实例:创建树的函数代码
递归函数的第一个停止条件是所有的类标签完全相同,则直接返回该类标签。
递归函数的第二个停止条件是使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组。由于第二个条件无法简单地返回唯一的类标签,这里使用多数表决函数挑选出现次数最多的类别作为返回值。
def createTree(dataSet,labels):
"""
创建树
两个参数: 数据集和标签列表
"""
classList = [example[-1] for example in dataSet] #classList变量包含了数据集的所有类标签
#print("dataSet = ",dataSet)
"""
dataSet = [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
dataSet = [[1, 'no'], [1, 'no']]
dataSet = [[1, 'yes'], [1, 'yes'], [0, 'no']]
dataSet = [['no']]
dataSet = [['yes'], ['yes']]
"""
#print("classList = ",classList)
"""
classList = ['yes', 'yes', 'no', 'no', 'no']
classList = ['no', 'no']
classList = ['yes', 'yes', 'no']
classList = ['no']
classList = ['yes', 'yes']
"""
#print("classList.count(classList[0]) = ",classList.count(classList[0])) #查看有多少个classList[0]标签
"""
classList.count(classList[0]) = 2
classList.count(classList[0]) = 2
classList.count(classList[0]) = 2
classList.count(classList[0]) = 1
classList.count(classList[0]) = 2
"""
#print("dataSet[0] = ",dataSet[0])
"""
dataSet[0] = [1, 1, 'yes']
dataSet[0] = [1, 'no']
dataSet[0] = [1, 'yes']
dataSet[0] = ['no']
dataSet[0] = ['yes']
"""
if classList.count(classList[0]) == len(classList): #
return classList[0] #类标签完全相同就停止继续划分
if len(dataSet[0]) == 1: #如果遍历完数据的属性,数据集只剩下一个属性,则停止遍历
return majorityCnt(classList) #遍历完所有特征时返回出现次数最多的。
bestFeat = chooseBestFeatureToSplit(dataSet) #选取数据集中的最好特征存储在bestFeat中
#print("bestFeat = ",bestFeat)
"""
bestFeat = 0
bestFeat = 0
"""
bestFeatLabel = labels[bestFeat] #这里的labels表示属性列表,并不是类标签
#print("bestFeatLabel = ",bestFeatLabel)
"""
bestFeatLabel = no surfacing
bestFeatLabel = flippers
"""
myTree = {bestFeatLabel:{}} #存储树的所有信息
del(labels[bestFeat]) #除去已分的特征
featValues = [example[bestFeat] for example in dataSet] #得到划分属性列中包含的所有属性值
#print("featValues = ",featValues)
"""
featValues = [1, 1, 1, 0, 0]
featValues = [1, 1, 0]
"""
uniqueVals = set(featValues)
#print("uniqueVals = ",uniqueVals)
"""
uniqueVals = {0, 1}
uniqueVals = {0, 1}
"""
"""
set() 函数创建一个无序不重复元素集
>>>x = set('runoob')
>>> x
(set(['b', 'r', 'u', 'o', 'n'])
"""
for value in uniqueVals:
subLabels = labels[:] #复制类标签,并将其存储在新列表变量subLabels中
#print("subLabels = ",subLabels)
"""
subLabels = ['flippers']
subLabels = ['flippers']
subLabels = []
subLabels = []
"""
#print("value = ",value)
"""
value = 0
value = 1
value = 0
value = 1
"""
#这样做的原因是:python语言中函数参数是列表类型时,参数是按照引用方式传递的,
#为了保证每次调用函数createTree()时不改变原始列表的内容,使用新变量subLabels代替原始列表。
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
#print("myTree[bestFeatLabel][value] = ",myTree[bestFeatLabel][value])
"""
myTree[bestFeatLabel][value] = no
myTree[bestFeatLabel][value] = no
myTree[bestFeatLabel][value] = yes
myTree[bestFeatLabel][value] = {'flippers': {0: 'no', 1: 'yes'}}
"""
#递归调用createTree()函数,得到的返回值插入到字典变量myTree中
return myTree
myDat,labels = tree0.createDataSet()
tree0.chooseBestFeatureToSplit(myDat)
Out[32]: 0
myDat
Out[33]: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
myTree = tree0.createTree(myDat,labels)
myTree
Out[35]: {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
本章小结
决策树分类器就像带有终止块的流程图,终止块表示分类结果。开始处理数据集时,我们首先需要测量集合中数据的不一致性,也就是嫡,然后寻找最优方案划分数据集,直到数据集中的所有数据属于同一分类。ID3算法可以用于划分标称型数据集。构建决策树时,我们通常采用递归的方法将数据集转化为决策树。一般我们并不构造新的数据结构,而是使用 Python语言内嵌的数据结构字典存储树节点信息
Python语言的pickle模块可用于存储决策树的结构。
决策树可能会产生过多的数据集划分,从而产生过度匹配数据集的问题。我们可以通过裁剪决策树,合并相邻的无法产生大量信息增益的叶节点,消除过度匹配问题,还有其他的决策树的构造算法最流行的是C45和CART。