目录
引入:
什么是决策树?
决策树相较KNN的优势?
决策树的运作方式?
决策树模型的优缺?
决策树的构造:
构造思路
信息增益
划分数据集
递归构造决策树
绘制决策树树形图
Matplotlib注解
构造注解树
测试和存储分类器:
使用决策树执行分类:
存储建立好的决策树:
实例:使用决策树预测隐形眼镜类型:
总结
开坑前言:大一在读新生开启了自己的机器学习之旅,从接触到现在已经有快两个月了,现在回过头为夯实基础,从开始看起《机器学习实战》这本书,即使是最简单的KNN算法,详细看过作者用python实现的代码后还能感觉到不少收获。开这个坑希望能分享一些经验给到更多一同在学习这本书的朋友,共同进步。
若本文有漏洞或者希望交流的朋友可以私信我,欢迎大家指正。
如下的流程图就是一个决策树,是数据结构中经典的树状结构,长方形代表判断模块,椭圆形代表终止模块,即已得出结论。决策树模型通过不停地二分类或多分类,最终达到全分类的目的,如下图,就已经把邮件分成了“无聊时需要阅读的邮件”,“需要及时处理的朋友邮件”,“无需阅读的垃圾邮件”。决策树极其简单的概念,对于一点不了解机器学习的同学,也很容易上手。
与上一章的KNN多分类器相对比,KNN无法给出数据的内在含义,决策树的主要优势就在于数据形式非常容易理解。
决策树的一个重要任务是理解数据中所蕴含的知识信息,因此决策树可以使用不熟悉的数据集合,并从中提取一系列规则,这些机器根据数据集创建规则的过程,就是机器学习的过程。
优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据
缺点:可能会产生过度匹配问题
适用数据类型:数值型和标称型(离散型)
我们首先讨论数学上如何用信息论划分数据集,然后编写代码将理论应用到具体的数据集上,最后编写代码构建决策树。(前提知识:对信息熵有一定的理解)
构造决策树时,需要解决的第一个问题就是:当前数据集上哪个特征在划分数据分类时,起到决定性作用。为了找到决定性的特征,划分出最好的是结果,我们必须评估每个特征。完成第一阶段的特征评估后,原始数据集就被划分为几个数据子集,而这些数据子集会分布在第一阶段决策点的所有分支上。如果某个分支下的数据属于同一类型,如图1,我们在第一个分支已经将“无聊时需要阅读的文件”分成了一个单独的数据子集,则不需要对这个数据子集进行再次分类。而如果一个数据子集中仍存在不同的类型,如第一阶段决策点的右分支,我们用“是否包含单词‘曲棍球’”进行第二次划分数据。不断地划分,直到所有相同类型的数据在各自的数据子集中。
因此我们可以有一个递归函数去实现以上的构造思路:(前提知识:对递归了解透彻。倒数第二行调用了自身)
def create_branch()
If 数据集中的每个子项属于同一分类
return 类标签
else
寻找划分数据集的最好特征
划分数据集
创建分支节点
for 每个划分的子集
调用create_branch并增加返回结果到分支节点中
return 分支节点
之后我们需要解决的就是如何应用信息论找到最好特征并划分数据集,找到这个方法并将其转换为python代码我们就成功了。
让我们再举一个例子来强调我们现在的问题,如下图所示,这里包含了五个海洋动物,特征包括:不浮出水面是否可以生存,以及是否有脚蹼,这些动物被分为两类:鱼类和非鱼类。现在我们想要决定依据第一个特征还是第二个特征来划分数据。在回答这个问题之前,我们必须采用量化的方法判断如何划分数据
从这里需要引用信息论的知识,不难理解,没有相关理论的可以继续往下看,在这里会引入一些相关概念保证萌新可以上手
划分数据集的大原则是:将无序的数据变为有序。
这一句话如何理解?对于表3-1,假设我们只知道5个样本是否属于鱼类,此时并不知道他们的两个特征(不浮出水面是否可以生存,是否有脚蹼),现在的数据是完全无序的。当我们引入一个特征来划分数据集时,能将这些从一个大类划分为多个小类,我们也就有了一定的根据去划分是否为鱼类。此时的数据相比之前的数据更加有序。而划分数据集过程前后信息发生的变化,就称为信息增益。
而信息增益是可以被计算的,这就需要学习信息论的知识。
第一个概念称作信息量,这个表征事件的不确定性:如果一件事情的发生的概率越低,那么它的信息量越大,如果一件事情发生的概率越高,那么它的信息量越低,因此我们有以下公式:
第二个概念是信息熵,这个表征信息量的数学期望,对于当前的信息体系来说,信息熵代表着信息量的期望之和:
信息熵代表着当前体系的不确定性,熵越大不确定性越高,熵越小则代表答案的不确定性降低。我们的决策树就是在不断地降低这个体系的信息熵,每一次决策选取降低信息熵最大的特征。
计算信息熵是我们建立决策树贯穿始终的步骤,我们敲一个函数来计算给定数据集的熵。
from math import log
def calc_ent(dataset):
num_entries = len(dataset)
# 保存数据集中的实类总数
labelcounts = {}
# labelcounts是字典,用于装载每个标签出现的次数
for feat_vec in dataset:
current_label = feat_vec[-1]
# 取标签每个样本的标签,样本的标签在最后一位
if current_label not in labelcounts.keys():
labelcounts[current_label] = 0
# 若字典中之前没有该标签,则在字典里新增一条对应关系
labelcounts[current_label] += 1
# 对于某样本的标签的出现次数+1
ent = 0.0
# ent为信息熵
for key in labelcounts:
prob = float(labelcounts[key])/num_entries
ent -= prob*log(prob,2)
# 这整个for循环,计算信息熵
return ent
我们这个函数用数据集中每个样本的标签计算了系统的信息熵。具体思路是:先声明一个变量保存实例总数,然后创造一个字典数组,它的键值记录了当前类别出现的次数。最后,使用所有类标签的发生频率计算类别出现的概率。我们将用这个概率计算整个体系的信息熵,再统计所有类标签发生的次数。
得到熵以后,我们就可以按照获得最大信息增益的方法来划分数据集。下一节我们实现如何划分数据集和度量信息增益。
另一个度量集合无需程度的方法是基尼不纯度(Gini impurity),简单地说就是从一个数据集中随机选取子项,度量其被错误分类到其他分组的概率,作者没有学习,之后会更新本篇博客进行一个引入介绍。
序号 | 不浮出水面是否可以生存(特征1) | 是否有脚蹼(特征2) | 属于鱼类 |
1 | 1 | 1 | Yes |
2 | 1 | 1 | Yes |
3 | 1 | 0 | No |
4 | 0 | 1 | No |
5 | 0 | 1 | No |
我们将表3-1数值化为以上表格以便于更好地输入样本。在判断以什么特征作为每次决策的参照前,我们需要先介绍划分数据集的方法。假设我们当前要以特征1对数据集进行划分,则我们特征1的值为1的划分为第一类,特征1的值为2的划分为第二类。分好类后,原数据集中特征1的项就被去除,如下面的表格所示。
序号 | 是否有脚蹼(特征2) | 属于鱼类 | 第一次分支所属类别 |
1 | 1 | Yes | 第一类 |
2 | 1 | Yes | 第一类 |
3 | 0 | No | 第一类 |
4 | 1 | No | 第二类 |
5 | 1 | No | 第二类 |
按照其他特征的原理也如此,当我们以某个特征决策时,将该特征下值相同的样本分为一类,分好类的同时抽取出这些样本的该类特征,保证下一次决策时不会再以该特征做分类。
我们把划分数据集的代码敲出来:
def split_dataset(dataset, axis, value):
ret_dataset = []
# 新建一个列表作为去掉axis类特征的新数据集
for featvec in dataset:
if featvec[axis] == value:
# 分出axis类特征值为value的样例
reduced_featvec = featvec[:axis]
reduced_featvec.expend(featvec[axis+1:])
# 以上的两行是去除原样例中的axis类特征
ret_dataset.append(reduced_featvec)
# 将去除后的样例数据添加到新数据集中
return ret_dataset
# 这里要注意expend和append的区别,expend(x)是将x拆开成多个元素加入列表中,append(x)是将x原封不动作为一个整体,作为列表中的一个元素
我们用数值化后的海洋生物数据对于特征一进行划分数据集,一共使用split_dataset函数两次,若第一次value==1,第二次value==0,我们就获得了如下的两个列表:
第一次:[1, Yes; 1, Yes; 0, No],第二次:[1, No; 1 No]。
接下来我们将遍历整个数据集,循环计算信息熵和splitdataset()函数,找到当前阶段用以决策的最好特征:
def choose_best_feature_to_split(dataset):
num_features = len(dataset[0]) - 1
# num_features为dataset原数据集的特征数量
base_entropy = calc_ent(dataset)
# 计算原数据集的信息熵
best_infogain = 0.0
# 储存最高的信息增益
best_feature = -1
# 储存信息增益最高的特征
for i in range(num_features):
featlist = [example[i] for example in dataset]
# 这里是取dataset中的第i个特征,可以理解为取了dataset中的第i列
uniquevals = set(featlist)
# 使用set集合去除featlist中的相同变量
new_entropy = 0.0
# 声明新子集的信息熵之和
for value in uniquevals:
sub_dataset = split_dataset(dataset, i, value)
# 得出对于特征i中值为value的子集
prob = len(sub_dataset)/float(len(dataset))
# prob为该子集占总集的比例
new_entropy += prob * calc_ent(sub_dataset)
# 计算子集的信息熵之和
infogain = base_entropy - new_entropy
# 计算信息增益
if infogain > best_feature:
# 若信息增益更大则更换选取的特征(事实上也可以比较子集的信息熵之和最小)
best_infogain = infogain
best_feature = i
return best_feature
# 返回信息增益最大的特征
函数中调用的数据需要满足一定的需求:
1.数据必须是一种由列表元素组成的列表,并且所有的列表元素都必须具有相同的数据长度
2.数据的最后一列或者每个实例的最后一个元素是当前实例的类别标签
看代码+完全的注释很容易理解整个函数的思路,我还是在这里做一下梗概:
枚举数据集里的每个特征,假设以该特征做决策,找出这一特征下总共有几种值,对这几种值求子集,将子集的信息熵相加,找到子集信息熵之和最小的特征。
我们现在已经有了计算信息熵、分割数据集、找出最适合特征的子模块,还记得我们在构造思路的小节中的递归函数伪代码吗?这就是我们构造出决策树的关键。
其详细的工作原理如下:
得到原始数据集后,基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分后,数据将被向下传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据,因此我们可以采用递归的原则处理数据集。
递归结束有以下条件:
1、子集中所有样例都是相同的分类。
2、程序遍历完所有划分数据集的属性。
在第一个条件下,我们可以判断任何到达子节点的数据必然属于叶子节点的分类。但是在第二个条件下,子集中类标签不唯一,这个时候采取多数表决的方法来决定该叶子节点的分类。
因而我们需要写一个函数来在二种条件的情况下进行分类:
def majority_cnt(class_list):
class_count = {}
# class_count收集子集中每种标签的数量
for vote in class_list:
if vote not in class_count.keys():
class_count[vote] = 0
# 若字典中没有这样的映射,则添加vote的映射
class_count[vote] += 1
# vote映射的键值+1
sorted_class_count = sorted(class_count.iteritems(), key=operator.itemgetter(1), reverse=True)
# 对vote映射进行降序排序,iteritems分解class_count中的元素为一个个集合,key的意思是以iteritems的第二个元素(即键值)为参照排序
return sorted_class_count[0][0]
# 返回子集中键值最高的元素的标签
上面的代码类似于knn中的投票表决,该函数使用分类名称的列表,然后创建键值为classlist中唯一值的数据字典,字典对象存储了classlist中每个类标签出现的频率,最后利用operator操作键值排序字典,并返回出现次数最多的分类名称。
有了最后一个子模块后我们便可以用递归来构建决策树了
def create_tree(dataset, labels):
classlist = [example[-1] for example in dataset]
# 取数据集中每个样例的标签
if classlist.count(classlist[0]) == len(classlist):
# 判断标签列表中的字符是否全部相同
return classlist[0]
if len(dataset[0]) == 1:
# 判断是否数据集中的每个样例仅仅剩下一个元素,若是只剩下一个元素,说明程序已经遍历完所有划分数据集的属性
return majority_cnt(classlist)
best_feat = choose_best_feature_to_split(dataset)
# 选取最好的特征
best_feat_label = labels[best_feat]
# 获取最好的特征的特征名
mytree = {best_feat_label: {}}
# 用字典中元素对应字典的嵌套方式来建立树
del(labels[best_feat])
# 删除特征名列表中的最好特征的标签
feat_values = [example[best_feat] for example in dataset]
uniquevals = set(feat_values)
# 对最好的特征查询存在的所有值,并去重
for value in uniquevals:
sublabels = labels[:]
# sublabels赋值了类标签,并将其存储在新列表变量sublabels中,这样做是因为python语言中函数参数是列表类型时,参数是按照引用方式传递的,防止改变原列表的内容
mytree[best_feat_label][value] = create_tree(split_dataset(dataset, best_feat, value), sublabels)
# 根据最好特征所有的特征值创建子树
return mytree
直接读带注释的代码很容易了解该函数的思路,再说一下从头到尾概括一下思路:
输入参数——判断数据集是否满足停止条件(满足则以类标签创建叶子节点并停止)——以特征名创造树节点——枚举最好特征的每个特征值并调用函数自身创建子树
决策树构建成功后,我们先不急着代入测试集,由于决策树还可以方便我们正确地理解信息的内在含义,我们将绘制图形来观察我们的决策树。
我们这里将发挥决策树的优势,绘制一个如下图所示的决策树。
在学会建立一整个可视的决策树时,先学会如何使用Python中的Matplotlib进行注解。
Matplotlib提供了一个非常有用的注解工具(函数)annotations,它可以在数据图形上添加文本注解,如下图所示,整个函数可以实现:在坐标(0.2,0.1)的位置有一个点,我们将对该点的描述信息放在(0.35,0.3)的位置,并用箭头指向(0.2,0.1).
下边我们就先试试简单地实现一个注释功能,在写函数之前,我们需要导入matplotlib,并将叶子节点和决策节点的样式分别定义好,以及箭头格式。
import matplotlib.pyplot as plt
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)
# annotate用于在图形上给数据添加文本注解,支持带箭头的划线工具,方便我们在合适的位置添加描述信息
# s:注释文本的内容
# xy:被注释的坐标点,二维元组形如(x,y)
# xytext:注释文本的坐标点,也是二维元组,默认与xy相同
# xycoords:被注释点的坐标系属性,允许输入的值如下
# textcoords:注释文本的坐标系属性
# arrowprops:箭头的样式
# va="center", ha="center"表示注释的坐标以注释框的正中心为准,而不是注释框的左下角(v代表垂直方向,h代表水平方向)
# bbox:注释框的风格和颜色深度,fc越小,注释框的颜色越深,支持输入一个字典
为测试注释功能,我们先写一个缩水版的节点创建函数,只创建两个节点,一个叶子节点和一个决策节点,用来支持我们的注释函数:
def create_plot():
fig = plt.figure(1, facecolor='white')
fig.clf()
# clear figure清除所有轴,但是窗口打开,这样它就可以被重复使用
create_plot.ax1 = plt.subplot(111, frameon=False)
# 新建对象create_plot.ax1,将其定义为plt的子图
plot_node('a decision node', (0.5, 0.1), (0.1, 0.5), decision_node)
# 绘制一个决策点,该点是第一个(x,y)
plot_node('a leaf node', (0.8, 0.1), (0.3, 0.8), leaf_node)
# 绘制一个叶子节点,该点是第一个(x,y)
plt.show()
# 展示当前plt中的图片
直接调用create_plot()函数,运行PyCharm即可直接测试我们两个函数的功能是否正常运行。
create_plot()
出现如下图结果表明结果正常:
现在我们已经掌握了如何绘制树节点,下面将学习如何绘制整棵树。
在正式绘制树的主体前,我们需要得知一些树的信息来确定我们表格的规格,我们必须知道有多少个叶节点,以便正确确定x轴的长度,我们还需要知道树有多少层,以便可以正确确定y轴的高度。这里我们定义两个新函数get_num_leafs()和get_tree_depth()来获取上述信息。
def get_num_leafs(mytree):
num_leafs = 0
# 该变量为叶子节点的数目
first_str = list(mytree.key())[0]
# 返回一个列表,这个列表只有一个元素,即为mytree的根,因此我们调用[0]
second_dict = mytree[first_str]
# 相当于将根节点跑去,second_list作为新字典少去了根节点的那一层,可以直接对根的子节点进行遍历
for key in second_dict.key():
if type(second_dict[key]).__name__ == 'dict':
# 判断是否为决策节点
num_leafs += get_num_leafs(second_dict[key])
# 若是则调用自身进行递归
else:
num_leafs += 1
# 若为叶子节点,则直接在叶子节点的总数上+1
return num_leafs
def get_tree_depth(mytree):
max_depth = 0
first_str = list(mytree.key())[0]
second_dict = mytree[first_str]
# 以上基本与上一个函数相同
for key in second_dict.key():
if type(second_dict[key]).__name__ == 'dict':
this_depth = 1 + get_tree_depth(second_dict[key])
# 若为决策节点,则调用自身进行递归,并+1层数
else:
this_depth = 1
# 若为叶子节点,则说明递归到了树的底端,直接返回1
if this_depth > max_depth:
max_depth = this_depth
# 取当前根下能递归到的最大深度
return max_depth
注意,对字典的keys()在Python2.0和Python3.0中的功能并不相同,这里采用Python3.0的写法获取列表。
这两个函数的思路均是建立在递归的基础上,通过遍历树的子节点,根据它们是否为子树(即是否为字典,是否有子节点),来确定如何进行数据处理,叶子节点的总数和深度的数值处理比较简单,在这里不展开,看代码有较为详细的中文注释。为测试我们的两个函数是否正常,这里提供一个测试用的函数:
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]
测试用语句
mytree = retrieve_tree(0)
print(get_num_leafs(mytree))
print(get_tree_depth(mytree))
mytree = retrieve_tree(1)
print(get_num_leafs(mytree))
print(get_tree_depth(mytree))
按顺序输出的结果为: 3 2 4 3,我们的两个函数是正常运行的。
接下来就是用递归绘制图了(重点):
我们定义一个create_plot()函数,用于在最开始绘制图形,并进入绘树的递归函数。我们把这个递归函数定义为plot_tree(),功能是绘制当前的根节点并连接它和它的祖先节点,若在这个根节点下遇到叶子节点则直接绘制叶子节点,若遇到子树的根节点,则对其调用自身函数进行递归。值得注意的细节是如何找到每个节点在图像上的位置,下面给出思路:
1、为便于绘图,我们x轴与y轴的有效范围是(0,1)。我们按照树的先序遍历来构建节点图。
2、对于每个节点的Y值来说,按照从上到下的原则,我们按照树的深度来安放节点。如果整棵树的深度一共为3,则第一层的Y值为1,第二层的Y值为2/3,第三层的Y值为1/3,递归很容易做到这一点,待会在代码中会有这里的中文注释。
3、对于每个节点的X值来说,我们按照子节点从左到右,或者说是先序遍历的原则。假设有n个叶子节点,我们计划在这n个叶子节点排布时,是将图横分成(n+1)块。为便于表达,这几个节点分别是1/n,2/n,3/n……n/n,这是偏离正中心的。因此我们在初始化时,x轴的偏移量设为 -1 / (总叶子节点数量*2),每次计算节点的x时,加上该偏移量,即可获得x的位置。又根据我们对树的先序遍历的顺序,我们每次画出一个叶子节点时,在x轴的偏移量加上1/n,即可以将图从左向右正确绘出。
下面给出代码(略长,注释略多,下次考虑削减注释量)
def plot_mid_text(cntr_pt, parent_pt, txt_string):
# 这个函数用于对两个点之间的路线进行绘制
x_mid = (parent_pt[0]+cntr_pt[0])/2.0
y_mid = (parent_pt[1]+cntr_pt[1])/2.0
create_plot.ax1.text(x_mid, y_mid, txt_string)
def plot_tree(mytree, parent_pt, node_txt):
# mytree当前的子集 parent_pt父节点 node_txt选取的最佳特征相应的值
num_leafs = get_num_leafs(mytree)
# 计算当前的树上叶节点的数量
depth = get_tree_depth(mytree)
# 计算当前树的深度
first_str = list(mytree.keys())[0]
# 提出当前树的根节点
cntr_pt = (plot_tree.x_off + (1.0+float(num_leafs))/2.0/plot_tree.total_w, plot_tree.y_off)
# 计算当前根节点应当处在的位置,将x的表达式拆开会发现其正好在当前所属叶子节点的中心位置
plot_mid_text(cntr_pt, parent_pt, node_txt)
# 绘出当前根节点,并绘出其与其它父节点的路线值(即最佳特征的选择值)
plot_node(first_str, cntr_pt, parent_pt, decision_node)
# 当前根节点与其父节点连路线
second_dict = mytree[first_str]
# 获取当前根节点下属的节点到second_dict上,便于直接遍历
plot_tree.y_off = plot_tree.y_off - 1.0/plot_tree.total_d
# 遍历到下一层前,调整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.x_off = plot_tree.x_off + 1.0/plot_tree.total_w
# 若是子节点,则在当前的偏移量加上1.0/plot_tree.total_w,这里会对之后的偏移量根据递归序产生影响
plot_node(second_dict[key], (plot_tree.x_off, plot_tree.y_off), cntr_pt, leaf_node)
# 绘制该叶子节点,并绘制与当前根节点的路线
plot_mid_text((plot_tree.x_off, plot_tree.y_off), cntr_pt, str(key))
# 绘制当前子节点与根节点的路线值
plot_tree.y_off = plot_tree.y_off + 1.0/plot_tree.total_d
# 遍历结束后将y偏移量回调
def create_plot(in_tree):
fig = plt.figure(1, facecolor='white')
# 返回一个图形实例,相当于背景板
fig.clf()
axprops = dict(xticks=[], yticks=[])
# 设置一个字典建立x、y轴,代入之后的子图中
create_plot.ax1 = plt.subplot(111, frameon=False, **axprops)
plot_tree.total_w = float(get_num_leafs(in_tree))
# 计算叶子节点的数量
plot_tree.total_d = float(get_tree_depth(in_tree))
# 计算树的层数
plot_tree.x_off = -0.5/plot_tree.total_w
# 设置x轴的偏移量。我们知道叶子的x=1/totalw,2/totalw,3/totalw...1时,叶子节点是偏右的,若加上-0.5/plot_tree.total_w则能偏移到正中
plot_tree.y_off = 1.0
# 设置y轴的偏移量,根节点一定在最上方,因此我们每下一层就给y的偏移量减1/total_d
plot_tree(in_tree, (0.5, 1.0), '')
# in_tree是总数据集,(0.5,1.0)是根节点自身的位置,我们需要最先绘制根节点,而根节点没有祖先,因此我们不需要注释它与祖先之间的值
plt.show()
# 显示图像
还记得我们之前弄进来的retrieve_tree样例函数吗?我们验证一下这个函数的效果吧:
mytree=retrieve_tree(0)
create_plot(mytree)
mytree=retrieve_tree(1)
create_plot(mytree)
嗯,看上去对劲的呢。
我们利用训练集构造决策树后,需要用测试集来检测该决策树的准确性,并且存储该分类器便于以后使用:
我们将对该决策树用于对实际数据的分类,在执行数据分类时,需要使用决策树以及用于构造决策树的标签向量。用该程序比较测试数据与决策树上的数值,递归执行该过程直到进入叶子节点;最后将测试数据定义为叶子节点所属的类型。
我们用以下的代码进行分类:
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)
# 根据我们刚刚构建的树形,每个决策节点对应了一个标签,我们需要索引测试样本中的这个标签。
class_label = -1
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
我们利用之前建立好的create_dataset()函数进行测试:
mydat, labels = create_dataset()
mytree = retrieve_tree(0)
print(classify(mytree, labels, [1, 0]))
print(classify(mytree, labels, [1, 1]))
得到结果:no yes,这个结果是正确的,函数可正常使用
构建决策树十分耗时,利用创建好的决策树解决分类问题可以很迅速。因此为了节省时间,我们最好存储每个模型已经建立好的决策树,需要时调用。我们用python中的pickle模块序列化对象,它可以在硬盘中保存对象,并在需要的时候读取出来。任何对象都可以执行序列化操作,字典也不例外。
下面给出代码:
def store_tree(input_tree, filename):
# 输入中的filename为保存决策树input_tree的文件名
import pickle
fw = open(filename, 'w')
pickle.dump(input_tree, fw)
fw.close()
def grab_tree(filename):
# 引入filename文件中的决策树
import pickle
fr = open(filename)
return pickle.load(fr)
套用函数即可,不多写了,代码的一些难理解的地方我注释一下:
lenses数据集(以及书本全部的数据)在这里下载:https://www.manning.com/books/machine-learning-in-action
fr = open('lenses.txt')
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
# 读取所有行并返回列表,每个行作为一个列表,split对制表符进行分割,分成列表中不同的元素
print(lenses)
lenses_labels = ['age', 'prescript', 'astigmatic', 'tear rate']
lenses_tree = create_tree(lenses, lenses_labels)
create_plot(lenses_tree)
最值得细品的还是第二行代码,python小白算是长见识了。
决策树非常好地匹配了实验数据,然而这些匹配选项可能太多了,我们将这种问题称为过度匹配。为了减少过度匹配问题,我们可以裁剪决策树,去掉一些不必要的叶子节点。如果叶子节点只能增加少许信息,则可以删除该节点。
本章学习应用的是ID3算法,很明显它只能处理离散型的变量。第9章会接触cart来处理数值型的变量,之后应该会更新到。
稍微分享一下这周末的学习感受吧:这篇大概有200行的代码量,加上注释超300行了,巨多的函数对于我这种入门的大一真的不太友好,下次考虑大致记一下每个函数的输入,输出,功能。
第四章可能要过半个月这样更,导师布置任务了,还有一个高数期中考要突击复习一下。
如果导师允许的话,下一篇博客可能会更一篇英文论文的大致解读(也可能会因为能力不足更不出来)。
Measuring relevance between discrete and continuous features based on neighborhood mutual information
走过路过,关注一个吧!