决策树=树+决策 也就是先构造一棵树,在这棵树上进行一系列的决策 (是分类算法但也可以做回归)
那他做了什么事呢?请看下图:
我们的目标是分出图中谁爱篮球。 图中对应有五个样本以及右图中的两个特征(age、male)。这个分类的过程是:对五个样本从上往下走得出来最终的结果(输入样本数等于输出样本数)。因此决策树的分类过程比较简单。但是,我们要怎么构造一棵决策树呢?
再举一个例子:
假如现在要去相亲,但是相亲之前有一些指标。首先来看,年龄小于30去见,大于30不见;其次长相帅去见,不帅不见;最后收入高见,低不见;那么你为什么要年龄当成第一个节点,长相第二节点,收入第三个节点呢?所以我们在构造一棵树的时候,首先要做的就是决定每个节点的顺序。
在说决策树之前,先解释一个概念——熵
听起来好像很难理解,但是通俗的讲就好理解了。熵在我们高中的化学里面表示的是分子内部的混乱程度,其实也就是表示的当前这个东西纯不纯。
定义:X和Y两个事件相互独立,P(X,Y) = P(X)*P(Y)
H(X),H(Y) 表示事件发生的不确定性。
P(事情发生的概率越小)------> H(x)值越大 如:今天买的彩票中了。
P(事情发生的概率越大)------> H(Y)值越小 如:我们今天要上班。
在明白了上面的概念只有,我们就来讲决策树。
构造决策树的基本想法是:随着树深度的增加,节点的熵迅速地降低。熵降低的速度越快越好,这样我们就可以得到一棵高度最矮的决策树。这个怎么理解呢?比如公司让你做一件事,我们如果三步能做完为什么要用五步呢,当然是希望越快做完越好啊。
接下来给出一个例子说明如何构造决策树:
(图中有14行数据,每个数据4个特征 :天气,温度,湿度,风)
上面我们说过我们可以基于特征划分,现在有四个候选条件,那选择那个节点呢?
首先:我们要先计算在没有给定任何天气信息时,根据历史数据,我们知道新的一天出去约会的概率是9/14,不出去约会的概率是5/14,此时的熵为:
其次,从候选的划分节点中选择根节点。
对每项指标分别统计:在不同的取值下出去约会和不出去约会的次数。例如我们计算已知变量天气时,信息熵是多少:
- 天气 = 晴 :2/5的概率出去约会,3/5的概率不出去约会。entropy = 0.971
- 天气 = 多云 :1的概率去约会,所以 entropy = 0
- 天气 = 下雨 :3/5的概率出去约会,2/5的概率不出去约会,entropy = 0.971
天气取值为晴、多云、下雨的概率分别是5/14、4/14、5/14
信息熵为:5/14 x 0.971 + 4/14 x 0 + 5/14 x 0.971 = 0.693
这样的话系统的信息熵就从0.940下降到了0.693,信息增益gain(天气) = 0.940 - 0.693 = 0.247
同理可以计算出gain(温度) = 0.029,gain(湿度) = 0.152, gain(风) = 0.048.
gain(天气)最大(即在第一步中是系统的信息熵下降的最快),所以决策树的根节点就取 天气。
那么问题来了,刚刚计算里面提到的信息增益是什么鬼?在定义信息增益之前,还要明确一个概念,条件熵。
上面讲了熵我们知道是什么,那条件熵是个什么东西???条件熵H(Y|X)表示在已知随机变量X的条件下随机变量Y的不确定性,随机变量X给定的条件下随机变量Y的条件熵(conditional entropy) H(Y|X),定义X给定条件下Y的条件概率分布的熵对X的数学期望:
这里:
接下来,让我们说说信息增益。其实,相信聪明的你从上面的计算就可以看出,信息增益什么了。它定义为集合D的经验熵H(D)与特征A给定条件下D的经验条件熵H(D|A)之差,即
信息增益越大越好。越大表示选择当前特征我们信息熵下降的越快,使得样本划分的越好。
根节点选完了,第二个节点选谁呢?跟之前选择根节点的过程是一样的,直到全部选完。
以上的算法就是ID3算法(信息增益)。
但是ID3算法有缺点:
举例:在上面的特征中加入ID特征,根据尝试我们知道,去不去约会,跟id值无关,但是我们在计算的时候也会把它当成一个候选特征。而且通过计算发现,他会使我们的信息增益值最大。
因此,如果当我们的集合中特征属性很多的时候,一些无关的特征会影响我们最后的划分,因此引入了信息增益比(C4.5)
定义:特征A对集合D的信息增益比gr(D,A)定义为其信息增益g(D,A)与训练集D关于特征A的值得熵Ha(D)之比,即:
其中,
CART:Gini 系数
课本例题5.1实战代码(ID3,C45,CART):
"""
函数说明:创建数据集
dataSet:数据集
labels : 标签
"""
from math import log
import operator
def loadDataSet():
dataSet = [[0, 0, 0, 0, 'no'], #数据集
[0, 0, 0, 1, 'no'],
[0, 1, 0, 1, 'yes'],
[0, 1, 1, 0, 'yes'],
[0, 0, 0, 0, 'no'],
[1, 0, 0, 0, 'no'],
[1, 0, 0, 1, 'no'],
[1, 1, 1, 1, 'yes'],
[1, 0, 1, 2, 'yes'],
[1, 0, 1, 2, 'yes'],
[2, 0, 1, 2, 'yes'],
[2, 0, 1, 1, 'yes'],
[2, 1, 0, 1, 'yes'],
[2, 1, 0, 2, 'yes'],
[2, 0, 0, 0, 'no']]
labels = ['年龄', '有工作', '有自己的房子', '信贷情况'] #分类属性
return dataSet, labels #返回数据集和分类属性
"""
计算数据集的熵
dataSet:数据集
shannonEnt :熵
"""
def calcShannonEnt(dataSet):
numEntries = len(dataSet)
labelCounts = {}
for feat in dataSet:
curlabel = feat[-1]
if curlabel not in labelCounts.keys():
labelCounts[curlabel] = 0
labelCounts[curlabel] += 1
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key])/numEntries
shannonEnt -= prob * log(prob,2)
return shannonEnt
"""
函数说明:按照给定特征划分数据集
dataSet:待划分数据集
axis : 划分数据集的特征
value : 需要返回的特征的值
"""
def splitDataSet(dataSet,axis,value):
retDataSet = []
for feat in dataSet:
if feat[axis] == value:
reducedFeatVec = feat[:axis]
reducedFeatVec.extend(feat[axis+1:]) #这两步合起来相当于去掉了axis特征
retDataSet.append(reducedFeatVec)
return retDataSet
"""
函数说明:ID3、C4.5、CART算法选择最优特征
dataSet:数据集
bestFeature:信息增益最大的特征的索引值
"""
def ID3_chooseBestFeatureToSplit(dataSet):
numFeature = len(dataSet[0])-1 #特征的数量
Entropy = calcShannonEnt(dataSet) #数据集的熵
bestInfoGain = 0.0
bestFeature = -1
for i in range(numFeature):
featList = [example[i] for example in dataSet] #获取第i个特征的所有取值
uniqueVals = set(featList)
newEntropy = 0.0
for value in uniqueVals:
subDataSet = splitDataSet(dataSet,i,value)
prob = len(subDataSet)/float(len(dataSet)) #计算子集的概率
newEntropy += prob*calcShannonEnt(subDataSet) #根据公式计算经验熵
infoGain = Entropy - newEntropy #计算信息增益
print(u"ID3中第%d个特征的信息增益率为:%.3f"%(i,infoGain))
if (infoGain > bestInfoGain): #计算信息增益
bestInfoGain = infoGain #更新信息增益,找到最大的信息增益
bestFeature = i #记录信息增益最大的特征的索引值
return bestFeature
def C45_chooseBestFeatureToSplit(dataSet):
numFeature = len(dataSet[0])-1 #特征的数量
Entropy = calcShannonEnt(dataSet) #数据集的熵
bestInfoGain_ratio = 0.0
bestFeature = -1
for i in range(numFeature):
featList = [example[i] for example in dataSet] #获取第i个特征的所有取值
uniqueVals = set(featList)
newEntropy = 0.0
iv = 0.0
for value in uniqueVals:
subDataSet = splitDataSet(dataSet,i,value)
prob = len(subDataSet)/float(len(dataSet)) #计算子集的概率
newEntropy += prob*calcShannonEnt(subDataSet) #根据公式计算经验熵
iv = iv - prob*log(prob,2)
infoGain = Entropy - newEntropy #计算信息增益
if iv == 0:
continue
infoGain_ratio = infoGain / iv #计算信息增益率
print(u"C4.5中第%d个特征的信息增益率为:%.3f"%(i,infoGain_ratio))
if (infoGain_ratio > bestInfoGain_ratio):
bestInfoGain_ratio = infoGain #更新信息增益率,找到最大的信息增益率
bestFeature = i #记录信息增益率最大的特征的索引值
return bestFeature
def CART_chooseBestFeatureToSplit(dataSet):
numFeature = len(dataSet[0])-1 #特征的数量
bestGini = 999999.0
bestFeature = -1
for i in range(numFeature):
featList = [example[i] for example in dataSet] #获取第i个特征的所有取值
uniqueVals = set(featList)
gini = 0.0
for value in uniqueVals:
subdataset = splitDataSet(dataSet,i,value)
p = len(subdataset)/float(len(dataSet))
subp = len(splitDataSet(subdataset,-1,'no'))/float(len(subdataset))
gini += p*(1.0-pow(subp,2)-pow(1-subp,2)) #G(D,A)
print(u"CART中第%d个特征的基尼值为:%.3f"%(i,gini))
if gini < bestGini:
bestGini = gini
bestFeature = i
return bestFeature
"""
函数说明:统计出现次数最多的类标签
classList : 类标签列表
"""
def majorityCnt(classList):
classCount = {}
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) #根据字典的值降序排序
return sortedClassCount[0][0]
"""
函数说明:创建决策树
dataSet:训练数据集
labels :标签
featLabels :最优的特征标签
"""
def ID3_createTree(dataSet,labels,featLabels):
classList = [example[-1] for example in dataSet]
if classList.count(classList[0]) == len(classList): #如果类别全部相同,停止划分
return classList[0]
if len(dataSet[0]) == 1: #遍历完所有特征,返回出现次数最多的
return majorityCnt(classList)
bestFeat = ID3_chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeat] #最优特征对应的标签
featLabels.append(bestFeatLabel)
myTree = {bestFeatLabel:{}}
del(labels[bestFeat]) #加入树中的特征删除
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
myTree[bestFeatLabel][value] = ID3_createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels)
return myTree
def C45_createTree(dataSet,labels,featLabels):
classList = [example[-1] for example in dataSet]
if classList.count(classList[0]) == len(classList): #如果类别全部相同,停止划分
return classList[0]
if len(dataSet[0]) == 1: #遍历完所有特征,返回出现次数最多的
return majorityCnt(classList)
bestFeat = C45_chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeat] #最优特征对应的标签
featLabels.append(bestFeatLabel)
myTree = {bestFeatLabel:{}}
del(labels[bestFeat]) #加入树中的特征删除
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
myTree[bestFeatLabel][value] = C45_createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels)
return myTree
def CART_createTree(dataSet,labels,featLabels):
classList = [example[-1] for example in dataSet]
if classList.count(classList[0]) == len(classList): #如果类别全部相同,停止划分
return classList[0]
if len(dataSet[0]) == 1: #遍历完所有特征,返回出现次数最多的
return majorityCnt(classList)
bestFeat = CART_chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeat] #最优特征对应的标签
featLabels.append(bestFeatLabel)
myTree = {bestFeatLabel:{}}
del(labels[bestFeat]) #加入树中的特征删除
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
myTree[bestFeatLabel][value] = CART_createTree(splitDataSet(dataSet, bestFeat, value), labels, featLabels)
return myTree
"""
函数说明:对每一个测试数据分类
inputTree:决策树
featLabels:分类标签
testVec:每一个测试数据
返回 :决策结果
"""
def classify(inputTree,featLabels,testVec):
firstStr = list(inputTree.keys())[0] #决策树的根节点
secondDict = inputTree[firstStr] #根节点下面的树
featIndex = featLabels.index(firstStr) #根节点对应数据的下标
classLabel = 'no'
for key in secondDict.keys(): #遍历根节点下的子树
if testVec[featIndex] == key:
if type(secondDict[key]).__name__ == 'dict': #如果当前分支下的结果是字典,则继续往下遍历知道叶子节点为止
classLabel = classify(secondDict[key], featLabels, testVec)
else:
classLabel = secondDict[key] #找到对应的标签
return classLabel
"""
函数说明:对测试数据分类
inputTree:决策树
featLabels:分类标签
testDataSet:测试数据集
返回 :决策结果
"""
def classifytest(inputTree, featLabels, testDataSet):
classLabelAll = []
for testVec in testDataSet:
classLabelAll.append(classify(inputTree, featLabels, testVec))
return classLabelAll
if __name__=="__main__":
dataSet,features = loadDataSet()
#print(dataSet)
#print(calcShannonEnt(dataSet))
test_data = [[0, 0, 0, 1], #测试数据集
[0, 1, 0, 1],
[1, 0, 1, 2],
[1, 0, 0, 1],
[2, 1, 0, 2],
[2, 0, 0, 0],
[2, 0, 0, 2]]
while(True):
dec_Tree = str(input("请选择决策树:->(1:ID3; 2:C4.5; 3:CART)|('enter q to quit!')|:"))
#ID3
if dec_Tree == '1':
featLabels = []
labels_tmp = features[:] # 拷贝,createTree会改变labels
ID3desicionTree = ID3_createTree(dataSet,labels_tmp,featLabels)
print('ID3desicionTree:\n', ID3desicionTree)
testSet = test_data
print("下面为测试数据集结果:")
print('ID3_TestSet_classifyResult:\n', classifytest(ID3desicionTree, features, testSet))
print("---------------------------------------------")
#C4.5决策树
if dec_Tree =='2':
featLabels = []
labels_tmp = features[:] # 拷贝,createTree会改变labels
C45desicionTree =C45_createTree(dataSet,labels_tmp,featLabels)
print('C45desicionTree:\n', C45desicionTree)
testSet = test_data
print("下面为测试数据集结果:")
print('C4.5_TestSet_classifyResult:\n', classifytest(C45desicionTree, features, testSet))
print("---------------------------------------------")
#CART决策树
if dec_Tree =='3':
featLabels = []
labels_tmp = features[:] # 拷贝,createTree会改变labels
CARTdesicionTree = CART_createTree(dataSet,labels_tmp,featLabels)
print('CARTdesicionTree:\n', CARTdesicionTree)
testSet = test_data
print("下面为测试数据集结果:")
print('CART_TestSet_classifyResult:\n', classifytest(CARTdesicionTree, features, testSet))
if dec_Tree=='q':
break
结果:
ID3desicionTree:
{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}
ID3_TestSet_classifyResult:
['no', 'yes', 'yes', 'no', 'yes', 'no', 'no']
C45desicionTree:
{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}
下面为测试数据集结果:
C4.5_TestSet_classifyResult:
['no', 'yes', 'yes', 'no', 'yes', 'no', 'no']
CARTdesicionTree:
{'有工作': {0: {'有自己的房子': {0: 'no', 1: 'yes'}}, 1: 'yes'}}
下面为测试数据集结果:
CART_TestSet_classifyResult:
['no', 'yes', 'yes', 'no', 'yes', 'no', 'no']