《机器学习实战》学习笔记(二)决策树
1. 定义
- 决策树的结构和数据结构中的树结构类似,但是可以有多个分支。决策树从根节点出发,每个非叶子节点为一个判断条件,每一个叶子节点是结论,从根节点出发,经过多次判断最终得到结论。在我理解就是一堆信息的逻辑组合。
- 专家系统中经常使用决策树,而且决策树给出结果往往可以匹敌在当前领域具有几十年工作经验的人类专家。
2. 决策树的构造
(1) 收集数据:可以使用任何方法。
(2) 准备数据:树构造算法只适用于标称型数据,因此数值型数据必须离散化。
(3) 分析数据:可以使用任何方法,构造树完成之后,我们应该检查图形是否符合预期。
(4) 训练算法:构造树的数据结构。
(5) 测试算法:使用经验树计算错误率。
(6) 使用算法:此步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义
3. 数据集划分原则
- 划分数据集的大原则是:将无序的数据变得更加有序。这里采用信息熵作为划分依据。
- 是使用信息论度量信息,用信息熵表示数据的状态 ,信息熵可以作为一个系统复杂程度的度量,如果系统越复杂,出现不同情况的种类越多,那么它的信息熵是比较大的。
- 信息的定义如下:
h ( x i ) = − l o g 2 p ( x i ) h(x_i) = -log_2 p(x_i) h(xi)=−log2p(xi)
其中:其中p(xi)是某一类的样本数量占种样本数量的比例 - 信息熵的定义:所有信息的期望值
H = − ∑ i − 1 n p ( x i ) l o g 2 p ( x i ) H=- \sum_{i-1}^{n} {p(x_i)log_2 p(x_i)} H=−i−1∑np(xi)log2p(xi)
- 信息的定义如下:
- 另一个度量集合无序程度的方法是基尼不纯度。
- 是使用信息论度量信息,用信息熵表示数据的状态 ,信息熵可以作为一个系统复杂程度的度量,如果系统越复杂,出现不同情况的种类越多,那么它的信息熵是比较大的。
4. 算法实现
- 这里采用了ID3算法:
- 原理:首先检测所有属性,选择信息增益最大的属性产生决策树节点,由该属性的不同取值建立分支,再对各分支的子集递归调用该方法建立决策树节点的分支,直到所有子集仅包含同一类别的数据为止。
递归结束条件:
1. 该分支的所有样本属于同一类;此时返回该类
2. 所有特征已经使用完毕,不能继续进行分裂;此时返回样本出现最多的类。此处也可以自定义结束条件,所剩余的特征为n时结束。
3. 遍历所有特征,选择使信息熵增加最多的特征为依据建立分支,然后递归此方法,直到条件结束。
- 原理:首先检测所有属性,选择信息增益最大的属性产生决策树节点,由该属性的不同取值建立分支,再对各分支的子集递归调用该方法建立决策树节点的分支,直到所有子集仅包含同一类别的数据为止。
- 算法实现:
# -*- coding: utf-8 -*-
"""
Created on Tue Feb 26 17:15:43 2019
@author: xinglin
"""
from math import log
class Trees:
def __init__(self):
pass
# 计算信息熵
def calcShannonEnt(self,dataSet):
numEntries = len(dataSet)
labelCounts = {}
for featVec in dataSet:
currentLabel = featVec[-1]
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key])/numEntries
shannonEnt -= prob*log(prob,2)
return shannonEnt
# 按照特征划分数据集
def splitDataSet(self,dataSet,axis,value):
retDataSet = []
for i in dataSet:
if i[axis] == value:
ret = i[:axis]
ret += i[axis+1:]
retDataSet.append(ret)
return retDataSet
# 选择最佳划分依据,依据:信息熵减少最多的特征
def chooseBestFeatureToSplit(self,dataSet):
numFeatures = len(dataSet[0]) - 1
baseEntropy = self.calcShannonEnt(dataSet)
bestInfoGain = 0.0
bestFeature = -1
for i in range(numFeatures):
#列出特征i的所有可能取值 val
featValList = [example[i] for example in dataSet]
featValSet = set(featValList)
newEntropy = 0.0
for value in featValSet: #求出每种val的信息熵的和
splitData = self.splitDataSet(dataSet,i,value)
prob = len(splitData)/float(len(dataSet))
newEntropy += prob*self.calcShannonEnt(splitData)
infoGain = baseEntropy - newEntropy
if infoGain >= bestInfoGain:
bestInfoGain = infoGain
bestFeature = i
return bestFeature
# 返回出现次数最多的类别,原函数采用了operator模块的一些方法,本人对该模块不怎么了解
#这里稍作改动,直接遍历列表求出现次数最多的
def majorityCnt(self,classList):
temp = 0
for i in set(classList):
if classList.count(i) > temp:
maxClassList = i
temp = classList.count(i)
return maxClassList
# 资料的原函数
# {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
def createTree(self,dataSet,labels):
classList = [i[-1] for i in dataSet]
if classList.count(classList[0]) == len(classList):
return classList[0]
if len(dataSet[0]) == 1:
return self.majorityCnt(classList)
bestFeat = self.chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel:{}}
del(labels[bestFeat])
featValues = [i[bestFeat] for i in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels[:]
myTree[bestFeatLabel][value] = \
self.createTree(self.splitDataSet(dataSet,bestFeat,value),subLabels)
return myTree
# 每一分支节点改为键值feat记录当前节点划分依据,featVal记录子节点,以feat为依据遍历featVal可以直接得到结果
# {'feat': 0, 'featVal': {0: 'no', 1: {'feat': 0, 'featVal': {0: 'no', 1: 'yes'}}}}
def myCreateTree(self,data): #输入数据最后一列为标签
label = [i[-1] for i in data]
if label.count(label[0]) == len(label):
return label[0]
if len(data[0]) == 1:
return self.majorityCnt(label)
bestFeat = self.chooseBestFeatureToSplit(data)
myTree = {'feat':bestFeat,'featVal':{}}
featValues = [i[bestFeat] for i in data]
uniqueVals = set(featValues)
for val in uniqueVals:
myTree['featVal'][val] = self.myCreateTree(self.splitDataSet(data,bestFeat,val))
return myTree
# 训练,返回训练好的模型
def fit(self,x_train,y_train):
'''
x_train数据格式:每一列表示一个属性,每一行表示一个样本
y_train数据格式:一维数组,表示标签,与X_train相对应
'''
lence = len(y_train)
for i in range(lence):
x_train[i].append(y_train[i])
return self.myCreateTree(x_train)
# 预测predict,
def predict(self,inputTree,test_data):
if inputTree is None:
return 'Decision tree not created !please run fit()'
index = inputTree['feat']
valueKey = test_data[index]
nextTree = inputTree['featVal'][valueKey]
if type(nextTree) is dict:
del(test_data[index])
return self.predict(nextTree,test_data)
else:
return nextTree
# 示例数据
def creatDataSet(self):
dataSet = [
[1,1,'yes'],
[1,1,'yes'],
[1,0,'no'],
[0,1,'no'],
[0,1,'no'],
]
labels = ['no surfacing','flippers']
return dataSet,labels
if __name__=='__main__':
Model = Trees()
myData,labels = Model.creatDataSet()
print('myData的信息熵:',Model.calcShannonEnt(myData))
# print(Model.myCreateTree(myData))
data_x = [
[1,1],
[1,1],
[1,0],
[0,1],
[0,1]
]
data_y = ['yes','yes','no','no','no']
itree = Model.fit(data_x,data_y)
print('决策树结构:',itree)
print('[1,0]预测结果:',Model.predict(itree,[1,0]))
5. 总结
- 决策树分类器就像带有终止块的流程图,终止块表示分类结果;
- 决策树可能会过度匹配训练数据,从而导致过拟合。这里可以裁剪决策树,去掉一些不必要的叶子节点。如果叶子节点只能增加少许信息,则可以删除该节点,将它并入到其他叶子节点中。