前言
文章为《机器学习实战》摘录笔记。
简介
你是否玩过二十个问题的游戏,游戏的规则很简单:参与游戏的一方在脑海里想某个事物,其他参与者向他提问题,只允许提20个问题,问题的答案也只能用对或错回答。问问题的人通过推断分解,逐步缩小待猜测事物的范围。决策树的工作原理与20个问题类似,用户输入一系列数据,然后给出游戏的答案。
决策论中(如风险管理),决策树经常在运筹学中使用,特别是在决策分析中,它帮助确定一个能最可能达到目标的策略。如果在实际中,决策不得不在没有完备知识的情况下被在线采用,一个决策树应该平行概率模型作为最佳的选择模型或在线选择模型算法。决策树的另一个使用是作为计算条件概率的描述性手段。
在机器学习中,决策树是一个预测模型;他代表的是对象属性之间的一种映射关系。树中的每个节点表示某个对象,而每个交叉路径则代表某个可能的属性值,而每个叶节点则对应从根节点到该叶节点所经历的路径所表示的对象的值。决策树仅有单一输出,若欲有复数输出,可以建立独立的决策树以处理不同的输出。数据挖掘中决策树是一种经常要用到的技术,可以用于分析数据,同样也可以用来做预测。
一个决策树包含三种类型的节点:1.决策节点,通常用矩形框来表示;2.机会节点,通常用圆圈来表示;3.终结点,通常用三角形来表示。
详情请见:决策树
决策树的构造
决策树
优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据。
缺点:可能会产生过度匹配问题。
适用数据类型:数值型和标称型。
信息增益
划分数据集的大原则是:将无序的数据变得更加有序。组织杂乱无章数据的一种方法就是使用信息论度量信息,信息论是量化处理信息的分支科学。
在划分数据集之前之后信息发生的变化称为信息增益,知道如何计算信息增益,我们就可以计算每个特征值划分数据集获得的信息增益,获得信息增益最高的特征就是最好的选择。
香农熵
集合信息的度量方式称为香农熵或者简称为熵。
熵定义为信息的期望值,在明晰这个概念之前,我们必须知道信息的定义。如果待分类的事务可能划分在多个分类之中,则符号xi 的信息定义为:
其中p(xi)是选择该分类的概率。
为了计算熵,我们需要计算所有类别所有可能值包含的信息期望值,通过下面的公式得到:
其中n是分类的数目。
基尼不纯度
一个度量集合无序程度的方法是基尼不纯度2 (Gini impurity),简单地说就是从一个数据集中随机选取子项,度量其被错误分类到其他分组里的概率。
基于香农熵的决策树实现
import operator
from math import log
def create_data_set():
"""
创建数据集合
:return: 返回创建好的数据集合
"""
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 calc_shannon_ent(data_set):
"""
计算给定数据集合的香农熵
:param data_set:数据集
:return:返回香农熵
"""
numEntries = len(data_set)
labelCounts = {}
# 为所有可能分类创建字典
for featVec in data_set:
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
# 以2为底数求对数
shannonEnt -= prob * log(prob, 2)
return shannonEnt
def split_data_set(data_set, axis, value):
"""
划分数据集合
:param data_set:待划分的数据集合
:param axis:按某个轴划分,划分数据集合的特征
:param value:需要返回特征的值
:return:
"""
retDataSet = []
for featVec in data_set:
if featVec[axis] == value:
# 抽取数据
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis + 1:])
retDataSet.append(reducedFeatVec)
return retDataSet
def choose_best_feature_to_split(data_set):
"""
选择最好的数据集合划分方式
:param data_set: 待划分的数据集合
:return: 返回最好的用于划分数据集合的特征
"""
# 数据的最后一列或者每个实例的最后一个元素是当前实例的类别标签。
numFeatures = len(data_set[0]) - 1
baseEntropy = calc_shannon_ent(data_set)
bestInfoGain = 0.0
bestFeature = -1
# 遍历所有的属性
for i in range(numFeatures):
# 创建唯一的分类标签列表
featList = [example[i] for example in data_set]
uniqueVals = set(featList)
# 初始化香农熵
newEntropy = 0.0
# 计算每种划分方式的信息熵
# 信息增益是熵的减少或者是数据无序度的减少
for value in uniqueVals:
subDataSet = split_data_set(data_set, i, value)
prob = len(subDataSet) / float(len(data_set))
newEntropy += prob * calc_shannon_ent(subDataSet)
infoGain = baseEntropy - newEntropy
if infoGain > bestInfoGain:
# 计算最好的信息熵
bestInfoGain = infoGain
bestFeature = i
return bestFeature # 返回最好的划分数据集合的特征
def majority_cnt(class_list):
"""
如果数据集已经处理了所有属性,但是类标签依然不是唯一的,
此时我们需要决定如何定义该叶子节点,在这种情况下,我们
通常会采用多数表决的方法决定该叶子节点的分类。
:param class_list:分类列表
:return:返回可能性最大的一个类别
"""
classCount = {}
for vote in class_list:
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]
def create_tree(data_set, labels):
"""
创建树的函数代码
:param data_set:数据集合
:param labels:标签列表,标签列表包含了数据集中所有特征的标签
:return:返回树
"""
classList = [example[-1] for example in data_set]
# 类别完全相同则停止继续划分
if classList.count(classList[0]) == len(classList):
return classList[0]
# 遍历完所有特征时返回出现次数最多的
if len(data_set[0]) == 1:
return majority_cnt(classList)
bestFeat = choose_best_feature_to_split(data_set)
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel: {}}
# 得到包含的所属属性值
del (labels[bestFeat])
featValues = [example[bestFeat] for example in data_set]
uniqueVals = set(featValues)
for value in uniqueVals:
# copy all of labels, so trees don't mess up existing labels
subLabels = labels[:]
myTree[bestFeatLabel][value] = create_tree(
split_data_set(data_set, bestFeat, value),
subLabels)
# 字典变量myTree存储了树的所有信息
return myTree
def classify(input_tree, feat_labels, test_vec):
"""
使用决策树的分类函数
:param input_tree: 输入树
:param feat_labels:特征标签
:param test_vec:测试向量集合
:return:分类后的标签
"""
firstSides = list(input_tree.keys())
firstStr = firstSides[0] # 找到输入的第一个元素
secondDict = input_tree[firstStr]
# 将标签字符串转换为索引
featIndex = feat_labels.index(firstStr)
key = test_vec[featIndex]
valueOfFeat = secondDict[key]
if isinstance(valueOfFeat, dict):
# 递归遍历整棵树,比较test_vec变量中的值与树节点中的值,
# 如果到达叶子节点,则返回当前节点的分类标签
classLabel = classify(valueOfFeat, feat_labels, test_vec)
else:
classLabel = valueOfFeat
return classLabel
def store_tree(input_tree, filename):
"""
使用决策树存储
:param input_tree:要存储的树
:param filename:存储的文件的名称
:return:无
"""
# 序列化对象存储
import pickle
fw = open(filename, 'w')
pickle.dump(input_tree, fw)
fw.close()
def grab_tree(filename):
"""
使用pickle获取存储的树
:param filename:
:return:
"""
import pickle
fr = open(filename)
return pickle.load(fr)
决策树显示
import matplotlib.pyplot as plt
# 定义文本框和箭头格式
decisionNode = dict(boxstyle="sawtooth", fc="0.8")
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")
def get_num_leafs(my_tree):
"""
获取叶节点的数目和树的层数
:param my_tree: 树
:return:返回叶节点的数目与和树的层数
"""
numLeafs = 0
firstSides = list(my_tree.keys())
firstStr = firstSides[0]
secondDict = my_tree[firstStr]
for key in secondDict.keys():
# 测试节点的数据类型是否为字典
if type(secondDict[
key]).__name__ == 'dict':
numLeafs += get_num_leafs(secondDict[key])
else:
numLeafs += 1
return numLeafs
def get_tree_depth(my_tree):
"""
计算树的深度
:param my_tree:树
:return:返回深度
"""
maxDepth = 0
firstSides = list(my_tree.keys())
firstStr = firstSides[0]
secondDict = my_tree[firstStr]
for key in secondDict.keys():
# test to see if the nodes are dictonaires, if not they are leaf nodes
if type(secondDict[key]).__name__ == 'dict':
thisDepth = 1 + get_tree_depth(secondDict[key])
else:
thisDepth = 1
if thisDepth > maxDepth: maxDepth = thisDepth
return maxDepth
def plot_node(node_txt, center_pt, parent_pt, node_type):
"""
绘制带箭头的注解
:param node_txt:
:param center_pt:
:param parent_pt:
:param node_type:
:return:
"""
create_plot.ax1.annotate(node_txt,
xy=parent_pt,
xycoords='axes fraction',
xytext=center_pt,
textcoords='axes fraction',
va="center",
ha="center",
bbox=node_type,
arrowprops=arrow_args)
def plot_mid_text(cntr_pt, parent_pt, txt_string):
"""
通过计算父节点和子节点的中间位置,并在此处添加简单的文本信息
:param cntr_pt:子节点
:param parent_pt:父节点
:param txt_string:填充的文本内容
:return:无
"""
xMid = (parent_pt[0] - cntr_pt[0]) / 2.0 + cntr_pt[0]
yMid = (parent_pt[1] - cntr_pt[1]) / 2.0 + cntr_pt[1]
create_plot.ax1.text(xMid,
yMid,
txt_string,
va="center",
ha="center",
rotation=30)
def plot_tree(my_tree, parent_pt, node_txt): # if the first key tells you what feat was split on
"""
绘制树
:param my_tree:树
:param parent_pt:父节点
:param node_txt:节点信息
:return:无
"""
# 计算叶子的数量
numLeafs = get_num_leafs(my_tree)
depth = get_tree_depth(my_tree)
firstSides = list(my_tree.keys())
firstStr = firstSides[0]
cntrPt = (plot_tree.xOff + (1.0 + float(numLeafs)) / 2.0 / plot_tree.totalW, plot_tree.yOff)
# 标记子节点属性值
plot_mid_text(cntrPt, parent_pt, node_txt)
plot_node(firstStr, cntrPt, parent_pt, decisionNode)
secondDict = my_tree[firstStr]
# 减少y偏移
plot_tree.yOff = plot_tree.yOff - 1.0 / plot_tree.totalD
for key in secondDict.keys():
# test to see if the nodes are dictonaires, if not they are leaf nodes
if type(secondDict[key]).__name__ == 'dict':
plot_tree(secondDict[key], cntrPt, str(key)) # recursion
else: # it's a leaf node print the leaf node
plot_tree.xOff = plot_tree.xOff + 1.0 / plot_tree.totalW
plot_node(secondDict[key], (plot_tree.xOff, plot_tree.yOff), cntrPt, leafNode)
plot_mid_text((plot_tree.xOff, plot_tree.yOff), cntrPt, str(key))
plot_tree.yOff = plot_tree.yOff + 1.0 / plot_tree.totalD
def create_plot(in_tree):
fig = plt.figure(1, facecolor='white')
fig.clf()
axprops = dict(xticks=[], yticks=[])
create_plot.ax1 = plt.subplot(111, frameon=False, **axprops) # no ticks
# createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses
# 树的宽度
plot_tree.totalW = float(get_num_leafs(in_tree))
# 树的深度
plot_tree.totalD = float(get_tree_depth(in_tree))
plot_tree.xOff = -0.5 / plot_tree.totalW
plot_tree.yOff = 1.0
plot_tree(in_tree, (0.5, 1.0), '')
plt.show()
# def createPlot():
# fig = plt.figure(1, facecolor='white')
# fig.clf()
# createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses
# plotNode('决策节点', (0.5, 0.1), (0.1, 0.5), decisionNode)
# plotNode('叶子节点', (0.8, 0.1), (0.3, 0.8), leafNode)
# plt.show()
def retrieve_tree(i):
"""
为了节省大家的时间,函数retrieveTree输出预先存储的树信息,避免了每次测试代码时都要从数据中创建树的麻烦。
:param i:树的下标
:return:树
"""
listOfTrees = [{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}},
{'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}}
]
return listOfTrees[i]
显示效果:
测试使用
from Ch03 import treePlotter
from Ch03 import trees
# 创建数据集合
data, label = trees.create_data_set()
print("数据集合:", data)
print("数据标签:", label)
# 计算香农熵
print("香农熵:", trees.calc_shannon_ent(data))
# 香农熵越高,则混合的数据也越多
data[0][-1] = '香农熵'
print("修改后的数据:", data)
print("香农熵:", trees.calc_shannon_ent(data))
# 数据划分测试
print("划分1:", trees.split_data_set(data, 0, 1))
print("划分2:", trees.split_data_set(data, 0, 0))
# 选择最好的数据集合划分方式
print("第", trees.choose_best_feature_to_split(data),
"个特征是最好的用于划分数据集合的特征。")
# 创建决策树
myTree = trees.create_tree(data, label)
print(myTree)
# 显示决策树
# treePlotter.create_plot(myTree)
print("树的深度:", treePlotter.get_tree_depth(myTree))
print("树的叶子节点数:", treePlotter.get_num_leafs(myTree))
_, labels = trees.create_data_set()
myTree = treePlotter.retrieve_tree(0)
print(trees.classify(myTree, labels, [1, 0]),
trees.classify(myTree, labels, [1, 1]))
结果输出:
数据集合: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
数据标签: ['no surfacing', 'flippers']
香农熵: 0.9709505944546686
修改后的数据: [[1, 1, '香农熵'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
香农熵: 1.3709505944546687
划分1: [[1, '香农熵'], [1, 'yes'], [0, 'no']]
划分2: [[1, 'no'], [1, 'no']]
第 0 个特征是最好的用于划分数据集合的特征。
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: '香农熵'}}}}
树的深度: 2
树的叶子节点数: 3
no yes
使用决策树预测隐形眼睛类型
# 使用决策树预测隐形眼睛类型
fr = open('lenses.txt')
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate']
lensesTree = trees.create_tree(lenses, lensesLabels)
treePlotter.create_plot(lensesTree)
显示效果:
决策树非常好地匹配了实验数据,然而这些匹配选项可能太多了。我们将这种问题称之为过度匹配(overfitting)。为了减少过度匹配问题,我们可以裁剪决策树,去掉一些不必要的叶子节点。如果叶子节点只能增加少许信息,则可以删除该节点,将它并入到其他叶子节点中。