创建分支的伪代码函数createBranch()
if so return 类标签
else
寻找划分数据集的最好特征
划分数据集
创建分支节点
for 每个划分的子集
调用函数createBranch并增加返回结果到分支节点中
return 分支节点
决策树的一般流程
- 收集数据:可以使用任何方法。
- 准备数据:树构造算法只适用于标称型数据,因此数值型数据必须离散化。
- 分析数据:可以使用任何方法,构造树完成之后,我们应该检查图形是否符合预期。
- 训练算法:构造树的数据结构。
- 测试算法:使用经验树计算错误率。
- 使用算法:此步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义。
这里将采用ID3算法划分数据集。
信息增益
- 划分数据集的大原则是:将无序的数据变得更加有序。
- 在划分数据集之前之后信息发生的变化称之为信息增益。
- 获得信息增益最高的特征就是最好的选择。
- 集合信息的度量方式称为香农熵或者简称为熵。
- 熵定义为信息的期望值,在明晰这个概念之前,我们必须知道信息的定义。如果待分类的事务可能划分在多个分类之中,则符号
xi
x
i
的信息定义为:
l(xi)=−log2p(xi) l ( x i ) = − log 2 p ( x i ) 。 其中 p(xi) p ( x i ) 是选择该分类的概率。 - 为了计算熵,我们需要计算所有类别所有可能值包含的信息期望值,通过下面的公式得到:
H=−∑ni=1p(xi)log2p(xi) H = − ∑ i = 1 n p ( x i ) log 2 p ( x i ) 其中n是分类的数目。
程序清单1 计算给定数据集的香农熵
from math import log
def calcShannonEnt(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
首先,计算数据集中实例的总数。我们也可以在需要时再计算这个值,但是由于代码中多次用到这个值,为了提高代码效率,我们显示地声明一个变量保存实例总数。然后,创建一个数据字典,它的键值是最后一列的数值。如果当前键值不存在,则扩展字典并将当前键值加入字典。每个键值都记录了当前类别出现的次数。最后,使用所有类标签的发生频率计算类别出现的概率。我们将用这儿概率计算香农熵,统计所有类标签发生的次数。
程序清单2 按照给定特征划分数据集
def split_data_set(data_set, axis, value):
ret_data_set = []
for feat_vec in data_set:
if feat_vec[axis] == value:
reduced_feat_vec = feat_vec[:axis]
reduced_feat_vec.extend(feat_vec[axis + 1:])
ret_data_set.append(reduced_feat_vec)
return ret_data_set
代码使用了3个参数:待划分的数据集、划分数据集的特征、需要返回的特征的值。需要注意的是,python不用考虑内存分配问题。python语言在函数中传递的是列表的引用,在函数内部对列表对象的修改,将会影响该列表对象的整个生存周期。为了消除这个不良影响,我们需要在函数的开始声明一个新列表对象。因为该函数代码在同一数据集上被调用多次,为了不修改原始数据集,创建一个新的列表对象。数据集这个列表中的各个元素也是列表,我们要遍历数据集中的每一个元素,一旦发现符合要求的值,则将其添加到新创建的列表中。在if语句中,程序将符合特征的数据取出来。后面我们这样理解:当我们按照某个特征划分数据集时,就需要将所有符合要求的元素抽取出来。代码中使用了python语言列表类型自带的extend()和append()方法。
程序清单3 选择最好的数据集划分方式
def choose_best_feature_to_split(data_set):
num_features = len(data_set[0] - 1)
base_entropy = calc_shannon_ent(data_set)
best_info_gain = 0.0
best_feature = -1
for i in range(num_features):
feat_list = [example[i] for example in data_set]
unique_vals = set(feat_list)
new_entropy = 0.0
for value in unique_vals:
sub_data_set = split_data_set(data_set, i, value)
prob = len(sub_data_set) / float(len(data_set))
new_entropy += prob * calc_shannon_ent(sub_data_set)
info_gain = base_entropy - new_entropy
if info_gain > best_info_gain:
best_info_gain = info_gain
best_feature = i
return best_feature
这个函数使用了程序清单1和2中的函数。在函数中调用的数据需要满足一定的要求:第一个要求是,数据必须是一种由列表元素组成的列表,而且所有的列表元素都要具有相同的数据长度;第二个要求是,数据的最后一列或者每个实例的最后一个元素是当前实例的类别标签。数据集一旦满足上述要求,我们就可以在函数的第一行判定当前数据集包含多少特征属性。我们无需限定list中的数据类型。
划分数据之前,第三行的代码计算了整个数据集的原始香农熵,我们保存最初的无序度量值,用于与划分完之后的数据集计算的熵值进行比较。第一个for循环遍历数据集中的所有特征。使用列表推导(List Comprehension)来创建新的列表,将数据集中所有第i个特征值或者所有可能存在的值写入这个新list中。然后使用python语言原生的集合(set)数据类型。
遍历当前特征中的所有唯一属性值,对每个唯一属性值划分一次数据集,然后计算数据集的新熵值,并对所有唯一特征值得到的熵求和。信息增益是熵的减少或者是数据无序度的减少,大家肯定对于将熵用于度量数据无序度的减少更容易理解。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。
如果数据集已经处理了所有属性,但是类标签依然不是唯一的,此时我们需要决定如何定义该叶子结点,在这种情况下,我们通常会采用多数表决的方法决定该叶子结点的分类。
多数表决:
首部添加:
import operator
内容添加:
def majority_cnt(class_list):
class_count = {}
for vote in class_list:
if vote not in class_count.keys():
class_count[vote] = 0
class_count[vote] += 1
sorted_class_count = sorted(class_count.items(),
key=operator.itemgetter(1), reverse=True)
return sorted_class_count[0][0]
程序清单4 创建树的函数代码
def create_tree(data_set, labels):
class_list = [example[-1] for example in data_set]
if class_list.count(class_list[0]) == len(class_list): # 类别完全相同则停止继续划分
return class_list[0]
if len(data_set[0]) == 1:
return majority_cnt(class_list) # 遍历完所有特征时返回出现次数最多的类别
best_feat = choose_best_feature_to_split(data_set)
best_feat_label = labels[best_feat]
my_tree = {best_feat_label: {}}
del(labels[best_feat])
feat_values = [example[best_feat] for example in data_set] # 得到列表包含的所有属性值
unique_values = set(feat_values)
for value in unique_values:
sub_labels = labels[:]
my_tree[best_feat_label][value] = create_tree(split_data_set(data_set, best_feat, value),
sub_labels)
return my_tree
上述代码使用两个输入参数:数据集和标签列表。标签列表包含了数据集中所有特征的标签,算法本身并不需要这个变量,但是为了给出数据明确的含义,我们将它作为一个输入参数提供。
上述代码首先创建了名为class_list的列表变量,其中包含了数据集的所有类标签。递归函数的第一个停止条件是所有的类标签完全相同,则直接返回该类标签。递归函数的第二个停止条件是使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组。由于第二个条件无法简单地返回唯一的类标签,这里使用前面介绍的 majority_cnt()函数挑选出现次数最多的类别作为返回值。
这里字典变量my_tree存储了树的所有信息,这对于其后绘制树形图非常重要。当前数据集选取的最好特征存储在变量best_feat中,得到列表包含的所有属性值。
最后代码遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数create_tree(),得到的返回值将被插入到字典变量my_tree中,因此函数中止执行时,字典中将会嵌套很多代表叶子节点信息的字典数据。在解释这个嵌套数据之前,我们先看一下循环的第一行sub_labels = labels[:],这行代码复制了类标签,并将其存储在新列表变量sub_labels中。之所以这样做,是因为在py中函数参数是列表类型时,参数是按照引用方式传递的。为了保证每次调用函数create_tree()时不改变原始列表的内容,使用新变量sub_labels代替原始列表。
在Python中使用Matplotlib注解绘制树形图
程序清单5 使用文本注解绘制树节点
# -*- coding:utf-8 -*-
from pylab import *
mpl.rcParams['font.sans-serif'] = ['SimHei']
decision_node = dict(boxstyle="sawtooth", fc="0.8")
leaf_node = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")
def plot_node(node_txt, center_pt, parent_pt, node_type):
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 create_plot():
fig = plt.figure(1,facecolor='white')
fig.clf()
create_plot.ax1 = plt.subplot(111,frameon=False)
plot_node('决策节点', (0.5, 0.1), (0.1, 0.5), decision_node)
plot_node('叶节点', (0.8, 0.1), (0.3, 0.8), leaf_node)
plt.show()
注意中文乱码的问题。首部改为这两句:
from pylab import *
mpl.rcParams['font.sans-serif'] = ['SimHei']
构造注解树
程序清单6 获取叶节点的数目和树的层数
定义两个新函数,get_num_leafs()和get_tree_depth()
def get_num_leafs(my_tree):
num_leafs = 0
first_str = my_tree.keys()[0]
second_dict = my_tree[first_str]
for key in second_dict.keys():
if type(second_dict[key]).__name__ == 'dict':
num_leafs += get_num_leafs(second_dict[key])
else:
num_leafs += 1
return num_leafs
def get_tree_depth(my_tree):
max_depth = 0
first_str = my_tree.keys()[0]
second_dict = my_tree[first_str]
for key in second_dict.keys():
if type(second_dict[key]).__name__ == 'dict':
this_depth = 1 + get_tree_depth(second_dict[key])
else:
this_depth = 1
if this_depth > max_depth:
max_depth = this_depth
return max_depth
在python2中,找到key所对应的第一个元素为:first_str = my_tree.keys()[0],这在3中运行会报错:‘dict_keys‘ object does not support indexing,这是因为python3改变了dict.keys,返回的是dict_keys对象,支持iterable 但不支持indexable,我们可以将其明确的转化成list
程序清单7 plot_tree函数
def plot_mid_text(cntr_pt, parent_pt, txt_string):
x_mid = (parent_pt[0] - cntr_pt[0])/2.0 + cntr_pt[0]
y_mid = (parent_pt[1] - cntr_pt[1])/2.0 + cntr_pt[1]
create_plot.ax1.text(x_mid, y_mid, txt_string)
def plot_tree(my_tree, parent_pt, node_txt):
num_leafs = get_num_leafs(my_tree) # 计算宽与高
depth = get_tree_depth(my_tree)
first_str = list(my_tree.keys())[0]
cntr_pt = (plot_tree.xOff + (1.0 + float(num_leafs))/2.0/plot_tree.totalW, plot_tree.yOff)
plot_mid_text(cntr_pt, parent_pt, node_txt) # 标记子节点属性值
plot_node(first_str, cntr_pt, parent_pt, decision_node)
second_dict = my_tree[first_str]
plot_tree.yOff = plot_tree.yOff - 1.0/plot_tree.totalD # 减少y偏移
for key in second_dict.keys():
if type(second_dict[key]).__name__ == 'dict':
plot_tree(second_dict[key], cntr_pt, str(key))
else:
plot_tree.xOff = plot_tree.xOff + 1.0/plot_tree.totalW
plot_node(second_dict[key], (plot_tree.xOff, plot_tree.yOff), cntr_pt, leaf_node)
plot_mid_text((plot_tree.xOff, plot_tree.yOff), cntr_pt, str(key))
plot_tree.yOff = plot_tree.yOff + 1.0/plot_tree.totalD
测试和存储分类器
测试算法:使用决策树执行分类
在执行数据分类时,需要使用决策树以及用于构造决策树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行该过程直到进入叶子节点;最后将测试数据定义为叶子节点所属的类型。
程序清单8 使用决策树的分类函数
def classify(input_tree, feat_labels, test_vec):
first_str = list(input_tree.keys())[0]
second_dict = input_tree[first_str]
feat_index = feat_labels.index(first_str)
for key in second_dict.keys():
if test_vec[feat_index] == key:
if type(second_dict[key]).__name__ == 'dict':
class_label = classify(second_dict[key], feat_labels, test_vec)
else:
class_label = second_dict[key]
return class_label
程序清单9 使用pickle模块存储决策树
# 使用算法:决策树的存储
# 使用pickle模块存储决策树
def store_tree(input_tree, filename):
import pickle
fw = open(filename, 'wb+')
pickle.dump(input_tree, fw)
fw.close()
def grab_tree(filename):
import pickle
fr = open(filename, 'rb')
return pickle.load(fr)
注意这里py3用的是’wb+”以及’rb’。
使用决策树预测隐形眼镜类型
- 收集数据:提供的文本文件。
- 准备数据:解析tab键分隔的数据行
- 分析数据:快速检查数据,确保正确地解析数据内容,使用create_plot()函数绘制最终的树形图。
- 训练算法:使用3.1节的create_tree()函数。
- 测试算法:编写测试函数验证决策树可以正确分类给定的数据实例。
- 使用算法:存储树的数据结构,以便下次使用时无需重新构造树。
测试程序:
test.py
import trees
import treePlotter
fr = open('lenses.txt')
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
lenses_labels = ['age', 'prescript', 'astigmatic', 'tear_rate'] # 年龄,约定,矫正的,
lenses_tree = trees.create_tree(lenses, lenses_labels)
treePlotter.create_plot(lenses_tree)
lenses_labels = ['age', 'prescript', 'astigmatic', 'tear_rate']
print(trees.classify(lenses_tree, lenses_labels, ['young', 'hyper', 'no', 'normal']))
最后奉上总代码:
trees.py
import operator
from math import log
def majority_cnt(class_list):
class_count = {}
for vote in class_list:
if vote not in class_count.keys():
class_count[vote] = 0
class_count[vote] += 1
sorted_class_count = sorted(class_count.items(),
key=operator.itemgetter(1), reverse=True)
return sorted_class_count[0][0]
# 计算给定数据的香农熵
def calc_shannon_ent(data_set):
num_entries = len(data_set)
label_counts = {}
for feat_vec in data_set:
current_label = feat_vec[-1]
if current_label not in label_counts.keys():
label_counts[current_label] = 0
label_counts[current_label] += 1
shannon_ent = 0.0
for key in label_counts:
prob = float(label_counts[key]) / num_entries
shannon_ent -= prob * log(prob, 2)
return shannon_ent
# 创建数据
def create_data_set():
data_set = [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
labels = ['no surfacing', 'flippers']
return data_set, labels
# 按照给定特征划分数据集
def split_data_set(data_set, axis, value):
ret_data_set = []
for feat_vec in data_set:
if feat_vec[axis] == value:
reduced_feat_vec = feat_vec[:axis]
reduced_feat_vec.extend(feat_vec[axis + 1:])
ret_data_set.append(reduced_feat_vec)
return ret_data_set
# 选择最好的数据集划分方式
def choose_best_feature_to_split(data_set):
num_features = len(data_set[0]) - 1
base_entropy = calc_shannon_ent(data_set)
best_info_gain = 0.0
best_feature = -1
for i in range(num_features):
feat_list = [example[i] for example in data_set]
unique_vals = set(feat_list)
new_entropy = 0.0
for value in unique_vals:
sub_data_set = split_data_set(data_set, i, value)
prob = len(sub_data_set) / float(len(data_set))
new_entropy += prob * calc_shannon_ent(sub_data_set)
info_gain = base_entropy - new_entropy
if info_gain > best_info_gain:
best_info_gain = info_gain
best_feature = i
return best_feature
# 创建树的函数代码
def create_tree(data_set, labels):
class_list = [example[-1] for example in data_set]
if class_list.count(class_list[0]) == len(class_list): # 类别完全相同则停止继续划分
return class_list[0]
if len(data_set[0]) == 1:
return majority_cnt(class_list) # 遍历完所有特征时返回出现次数最多的类别
best_feat = choose_best_feature_to_split(data_set)
best_feat_label = labels[best_feat]
my_tree = {best_feat_label: {}}
del(labels[best_feat])
feat_values = [example[best_feat] for example in data_set] # 得到列表包含的所有属性值
unique_values = set(feat_values)
for value in unique_values:
sub_labels = labels[:]
my_tree[best_feat_label][value] = create_tree(split_data_set(data_set, best_feat, value),
sub_labels)
return my_tree
# 使用决策树的分类函数
def classify(input_tree, feat_labels, test_vec):
first_str = list(input_tree.keys())[0]
second_dict = input_tree[first_str]
feat_index = feat_labels.index(first_str)
for key in second_dict.keys():
if test_vec[feat_index] == key:
if type(second_dict[key]).__name__ == 'dict':
class_label = classify(second_dict[key], feat_labels, test_vec)
else:
class_label = second_dict[key]
return class_label
# 使用算法:决策树的存储
# 使用pickle模块存储决策树
def store_tree(input_tree, filename):
import pickle
fw = open(filename, 'wb+')
pickle.dump(input_tree, fw)
fw.close()
def grab_tree(filename):
import pickle
fr = open(filename, 'rb')
return pickle.load(fr)
treePlotter.py
# -*- coding:utf-8 -*-
from pylab import *
mpl.rcParams['font.sans-serif'] = ['SimHei']
decision_node = dict(boxstyle="sawtooth", fc="0.8")
leaf_node = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")
def plot_node(node_txt, center_pt, parent_pt, node_type):
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 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)
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 get_num_leafs(my_tree):
num_leafs = 0
first_str = list(my_tree.keys())[0]
second_dict = my_tree[first_str]
for key in second_dict.keys():
if type(second_dict[key]).__name__ == 'dict':
num_leafs += get_num_leafs(second_dict[key])
else:
num_leafs += 1
return num_leafs
def get_tree_depth(my_tree):
max_depth = 0
first_str = list(my_tree.keys())[0]
second_dict = my_tree[first_str]
for key in second_dict.keys():
if type(second_dict[key]).__name__ == 'dict':
this_depth = 1 + get_tree_depth(second_dict[key])
else:
this_depth = 1
if this_depth > max_depth:
max_depth = this_depth
return max_depth
def retrieve_tree(i):
list_of_trees = [{'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 list_of_trees[i]
# plot_tree函数
# 在父子节点间填充文本信息
def plot_mid_text(cntr_pt, parent_pt, txt_string):
x_mid = (parent_pt[0] - cntr_pt[0])/2.0 + cntr_pt[0]
y_mid = (parent_pt[1] - cntr_pt[1])/2.0 + cntr_pt[1]
create_plot.ax1.text(x_mid, y_mid, txt_string)
def plot_tree(my_tree, parent_pt, node_txt):
num_leafs = get_num_leafs(my_tree) # 计算宽与高
depth = get_tree_depth(my_tree)
first_str = list(my_tree.keys())[0]
cntr_pt = (plot_tree.xOff + (1.0 + float(num_leafs))/2.0/plot_tree.totalW, plot_tree.yOff)
plot_mid_text(cntr_pt, parent_pt, node_txt) # 标记子节点属性值
plot_node(first_str, cntr_pt, parent_pt, decision_node)
second_dict = my_tree[first_str]
plot_tree.yOff = plot_tree.yOff - 1.0/plot_tree.totalD # 减少y偏移
for key in second_dict.keys():
if type(second_dict[key]).__name__ == 'dict':
plot_tree(second_dict[key], cntr_pt, str(key))
else:
plot_tree.xOff = plot_tree.xOff + 1.0/plot_tree.totalW
plot_node(second_dict[key], (plot_tree.xOff, plot_tree.yOff), cntr_pt, leaf_node)
plot_mid_text((plot_tree.xOff, plot_tree.yOff), cntr_pt, str(key))
plot_tree.yOff = plot_tree.yOff + 1.0/plot_tree.totalD
本人用的是python3,可能与实战书上略有不同。
奉上机器学习实战源码地址:
机器学习实战源码