1.决策树模型
什么是决策树呢?开门见山,首先抛出数据和模型吧。
假如有以下数据集:
经过训练后,我们得到下面这样的决策树模型:
模型是一棵树,但并不是二叉树,上图只是巧合而已。从图上看,模型的内部结点是特征的属性,叶子结点是分类类别。每一条分支上都附带一个条件值,此值是分支父结点的特征值。如上图模型,可以总结为:
if feature(‘有房子’) == ‘是’:
Class = ‘是’
elif feature(‘有工作’) == ‘是’:
Class = ‘是’
else:
Class = ‘否’
所以,决策树可以看成是一个if-then的规则集合。将决策树转化成if-then的规则过程是这样的:由决策树的根结点到叶结点的每一条路径构建一条规则,路径上内部节点的特征对应着规则的条件,叶节点对应规则的结论,一条路径上的多个内部节点对应的多条件之间是‘并’的关系。务必要确保每一个实例都能被且只能被一条规则覆盖。
学习决策树模型的目标就是将数据集整理成确保每一个实例都能被且只能被一条规则覆盖的if-then规则集合,并将这种集合存储成树形结构。
2.特征选择
从数据集来看,提供的特征有(‘年龄’、‘有工作’、‘有房子’、‘信贷情况’)这4维,但是决策树的内部节点只有’有房子’、’有工作’。仔细看数据会发现:
feature(‘有房子’) == ‘否’ and feature(‘有工作’) == ‘否’这个条件下的所有的数据都是’否’,树不需要再有分支了。因此,决策树的一个重点——特征选择。
特征选择的依据是“信息增益”或“信息增益比”。什么是信息增益?什么是信息增益比呢?
首先,信息熵表示随机变量不确定性的度量,设X是一个取有限个数值的离散随机变量,其概率分布为
随机变量X的熵定义为
同理,条件熵指输出Y在随机变量X的概率分布P(Y|X)的熵,表示已知变量xi后,变量Y的不确定性,即
其中,
其中,表示条件概率
。
根据数据统计的概率而非模型预测的概率计算的熵和条件熵称为经验熵和经验条件熵。
信息增益的定义:
这样定义的原因在于:H(D)表示整个特征集合的信息量,而H(D|A)表示除A以外的特征的信息量,两者相减后得到的就是A的信息量。
A的信息量越大,这一维特征对分类越重要,越小越不重要,如果信息量为0了,那这一维特征对分类没有实质意义。因此,根据信息增益的大小来选择特征。
以信息增益选择特征的算法是ID3算法,C4.5算法以信息增益比来选择特征。
信息增益比的定义:
其中,,
表示特征A取值为第i个值在数据集中的数据个数,n是特征A的取值个数。
3.决策树的生成
如上一节所说,ID3算法以信息增益选择特征,C4.5算法以信息增益比选择特征,在构建决策树时,越重要的特征对应的内部节点越靠近根结点,且如第1小节所说,每一维有效特征是一个内部节点。因此,在训练过程中,已经被选出,构建到树模型中的结点的特征要及时从数据集中删除,已经贝分类的叶结点对应的的数据条目也需要及时删除。
ID3具体算法如下:
C4.5的具体算法如下:
4. 决策树剪枝
决策树的生成(训练)过程是对整个数据集遍历,只要是信息增益非零的特征都会进入树模型中,这样的好处是对训练集分类很准确,但是太过细节化,过拟合的风险很高。因此,需要做相应的正则化。决策树的正则化实际操作是把树模型中太细节的子树或叶节点删掉,并将父结点替换成新的叶节点。
剪枝的方法通过极小化损失函数了来实现的,损失函数由叶节点的经验熵之和和模型复杂度决定的。
其中,表示训练集的预测误差,α是正则参数,越大,树的剪枝程度越高,模型越简单,反之,模型越复杂。
具体算法过程:
5. 程序
ID3算法的程序如下,其中信息增益和信息增益比是自己编写的,决策树骨架和可视化是参考《机器学习实战》决策树那一章的程序。
#coding:utf-8
import numpy as np
import math
import operator
import sys
#from tree import *
reload(sys)
sys.setdefaultencoding('utf-8')
from pylab import *
def createDataSet():
"""
创建数据集
:return:总数据集、特征标签列表、特征值map、类别值列表
"""
dataSet = [[u'青年', u'否', u'否', u'一般', u'拒绝'],
[u'青年', u'否', u'否', u'好', u'拒绝'],
[u'青年', u'是', u'否', u'好', u'同意'],
[u'青年', u'是', u'是', u'一般', u'同意'],
[u'青年', u'否', u'否', u'一般', u'拒绝'],
[u'中年', u'否', u'否', u'一般', u'拒绝'],
[u'中年', u'否', u'否', u'好', u'拒绝'],
[u'中年', u'是', u'是', u'好', u'同意'],
[u'中年', u'否', u'是', u'非常好', u'同意'],
[u'中年', u'否', u'是', u'非常好', u'同意'],
[u'老年', u'否', u'是', u'非常好', u'同意'],
[u'老年', u'否', u'是', u'好', u'同意'],
[u'老年', u'是', u'否', u'好', u'同意'],
[u'老年', u'是', u'否', u'非常好', u'同意'],
[u'老年', u'否', u'否', u'一般', u'拒绝'],
]
labels = [u'年龄', u'有工作', u'有房子', u'信贷情况']
label_value_map = {u'年龄': [u'青年', u'中年', u'老年'],
u'有工作': [u'是', u'否'],
u'有房子': [u'是', u'否'],
u'信贷情况': [u'一般', u'好', u'非常好']
}
class_value_list = [u'同意', u'拒绝']
# 返回数据集和每个维度的名称
return dataSet, labels, label_value_map, class_value_list
"""
H(p) = -∑ (pi * logpi)
计算各分类(y)的经验熵
feature_class_map: 按类别存储的特征map
sample_num:样本总数
base:对数的底,书上对二值分类用2为底,其他是e为底。缺省为2
return:经验熵
"""
def compute_empirical_entropy(class_map, sample_num, base=2):
entropy = 0
for class_x in class_map:
prob_x = float(class_map[class_x]) / sample_num
entropy -= prob_x * math.log(prob_x, base)
return entropy
"""
计算基本的经验熵,H(D)
dataSet:总体数据集
class_list:类别列表
return:基本的经验熵
"""
def compute_base_entroy(dataSet, class_list):
feature_class_map = {}
for class_i in class_list:
feature_class_map[class_i] = 0
sample_num = 0
for features in dataSet:
class_x = features[-1]
feature_class_map[class_x] += 1
sample_num += 1
base_entropy = compute_empirical_entropy(feature_class_map, sample_num)
return base_entropy
"""
计算条件熵
H(Y|X) = ∑ (pi * H(Y|X=xi)
dataSet:总的数据集
i:给定的特征列
labels:特征维度标签
label_value_map:特征值map,存储了各特征标签下的值
return: 条件熵
"""
def comput_condition_entropy(dataSet, i, value_list):
condition_entropy = 0
for value in value_list:
value_num = 0
sample_num = len(dataSet)
subdataSet_map = {}
for j in range(sample_num):
if value == dataSet[j, i]:
value_num += 1
class_x = dataSet[j, -1]
if class_x not in subdataSet_map.keys():
subdataSet_map[class_x] = 1
else:
subdataSet_map[class_x] += 1
prob_value = float(value_num) / sample_num
entropy = compute_empirical_entropy(subdataSet_map, value_num)
condition_entropy += prob_value * entropy
return condition_entropy
"""
计算信息增益
g(D,A) = H(D) - H(D|A)
base_entropy:数据集的经验熵
dataSet:总的数据集
i:给定的特征列
value_list:给定特征的特征值列表
return: 信息增益
"""
def compute_info_gain(base_entropy, dataSet, i, value_list):
#value_list = label_value_map[labels[i]]
condition_entropy = comput_condition_entropy(dataSet, i,value_list)
info_gain = base_entropy - condition_entropy
return info_gain
"""
计算信息增益比
g(D,A)
gR(D,A) = -----------
HA(D)
info_gain:信息增益
dataSet:总的数据集
i:给定的特征列
return: 信息增益比
"""
def compute_info_gain_rato(info_fain, dataSet, i):
value_map = {}
sample_num = len(dataSet)
entropy_base_i = 0
for value in dataSet[:, i]:
if value not in value_map.keys():
value_map[value] = 1
value_map[value] += 1
info_gain_rato = compute_empirical_entropy(value_map, sample_num)
return info_gain_rato
"""**********************************以下代码基本上是参考别人的**************************************"""
def splitDataSet(dataSet, axis, value):
"""
按照给定特征划分数据集
:param dataSet: 待划分的数据集
:param axis: 划分数据集的特征的维度
:param value: 特征的值
:return: 符合该特征的所有实例(并且自动移除掉这维特征)
"""
retDataSet = []
for featVec in dataSet:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis] # 删掉这一维特征
reducedFeatVec.extend(featVec[axis + 1:])
retDataSet.append(reducedFeatVec)
return retDataSet
def chooseBestFeatureToSplitByID3(dataSet, labels, label_value_map, class_list):
"""
选择最好的数据集划分方式
:param dataSet:
:return:
"""
numFeatures = len(dataSet[0]) - 1 # 最后一列是分类
base_entropy = compute_base_entroy(dataSet, class_list)
bestInfoGain = 0.0
bestFeature = -1
for i in range(numFeatures): # 遍历所有维度特征
print i
value_list = label_value_map[labels[i]]
infoGain = compute_info_gain(base_entropy, dataSet, i, value_list)
if (infoGain > bestInfoGain): # 选择最大的信息增益
bestInfoGain = infoGain
bestFeature = i
return bestFeature # 返回最佳特征对应的维度
"""
返回出现次数最多的分类名称
:param classList: 类列表
:return: 出现次数最多的类名称
"""
def majorityCnt(classList):
classCount = {} # 这是一个字典
for vote in classList:
if vote not in classCount.keys(): classCount[vote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
"""
创建决策树
:param dataSet:数据集
:param labels:数据集每一维的名称
:return:决策树
"""
def createTree(dataSet, labels, label_value_map, calss_value_list, chooseBestFeatureToSplitFunc=chooseBestFeatureToSplitByID3):
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)
dataSet_array = np.array(dataSet)
bestFeat = chooseBestFeatureToSplitFunc(dataSet_array, labels, label_value_map, class_value_list)
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel: {}}
del (labels[bestFeat])
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels[:] # 复制操作
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels, label_value_map, class_value_list)
return myTree
mpl.rcParams['font.sans-serif'] = ['SimHei'] # 指定默认字体
mpl.rcParams['axes.unicode_minus'] = False # 解决保存图像时负号'-'显示为方块的问题
##################################
# 测试决策树的构建
myDat, labels,label_value_map, class_value_list = createDataSet()
#myDat = np.array(myDat)
myTree = createTree(myDat, labels, label_value_map, class_value_list)
# 绘制决策树
import treeplotter
treeplotter.createPlot(myTree)
执行后的效果如第一小节中的决策树模型图