阅读 322

机器学习笔记(四)——决策树的构建及可视化

前文简介

上一篇文章中主要介绍了以下几方面:

  • 决策树的简介
  • 决策树的流程
  • 熵的定义及如何计算熵
  • 信息增益的定义及如何计算信息增益
  • 依据信息增益划分数据集

本文以一个新的数据集(隐形眼镜数据集)为基础实现构建决策树、决策树的保存与加载、利用决策树分类、决策树的可视化,前文的知识不在过多概述,着重介绍这四个方面。

先大致了解一下数据集: 在这里插入图片描述

这份数据源至UCI数据库,其共有4个特征分别为age(年龄)、prescript(症状)、astigmatic(闪光)、tearRate(泪液产生率)以及一个分类标签class,该分类包含硬材质、软材质和不应配带三种。

为了方便处理,对样本做以下处理:

  1. age:young—>0、pre—>1、presbyopic—>2
  2. prescript:myope—>0、hyper—>1
  3. astigmatic:no—>0、yes—>1
  4. tearRate:reduced—>0、normal—>1

四、决策树的构建

在构造决策树之前,先回顾一下前几个子模块的工作原理:先获取原始数据集,然后基于最优特征划分数据集,当数据集特征大于两个时,第一次划分之后,数据将被向下传递至树的下一个节点,在这个节点上,在此划分数据,此过程是利用递归原理处理数据集。

什么时候划分结束呢?当程序遍历完所有划分数据集的属性,或者每个分支下所有实例分类一致时代表划分数据集结束。

而构造决策树的过程就是将每一次划分出的数据填入一个字典中,当数据集划分结束时,向字典中填充数据也结束,此过程也是一个递归过程,至此决策树的构造完成。 代码如下:

def CreateTree(DataSet):
#获取所有特征标签
index_list = list(DataSet.columns)
#获取最后一列(分类标签)的类别
label_series = DataSet.iloc[:,-1].value_counts()
#判断类别标签最多一个是否等于数据样本数、或者数据集是否只有一列
if label_series[0]==DataSet.shape[0] or DataSet.shape[1] == 1:
return label_series.index[0] #返回类标签
# 获取最优特征列索引
col = ChooseBF(DataSet)
# 获取最优特征
BestFeature = index_list[col]
#将最优特征依次填入字典中
TheTree = {BestFeature:{}}
# 从标签列表中删去该特征标签
del index_list[col]
#提取最佳切分列的所有属性值
value_list = set(DataSet.iloc[:,col])
#利用递归方法建树,每次对象为当前最优特征
for value in value_list:
TheTree[BeatFeature][value] = CreateTree(splitSet(DataSet,col,value))
return TheTree
复制代码

递归函数的第一个停止条件是所有的类标签都相同,递归函数第二个停止条件是使用完数据集中所有的特征,即数据集不能继续划分;字典变量TheTree储存了树的所有信息,BestFeature则是当前最优特征。

最后代码遍历当前最优特征的所有属性值,在每个数据集划分上递归调用函数CreateTree(),并且传入的参数是每次划分之后的数据集,得到的返回值都会被插入字典TheTree中,递归结束后,字典中将会嵌套很多代表叶子节点信息的数据。

得到TheTree字典如下:

{'tearRate': {0: 'no lenses', 1: {'astigmatic': {0: {'age': {0: 'soft', 1: 'soft', 2: {'prescript': {0: 'no lenses', 1: 'soft'}}}}, 1: {'prescript': {0: 'hard', 1: {'age': {0: 'hard', 1: 'no lenses', 2: 'no lenses'}}}}}}}}
复制代码

从左边开始,第一个关键字key为tearRate,这代表在所有特征中,tearRate特征的信息增益最大,在此特征下,数据下降(划分)最快,该关键字的值也是一个字典。第二个关键字是依据tearRate特征划分的数据集,这些关键字的值就是tearRate节点的子节点。

这些值可能是类标签,也可能是另一个字典。如果值是类标签,则该子节点为叶子节点;如果值是另一个字典,则该子节点是一个判断节点,通过这类格式不断重复就构成了一棵决策树。

五、决策树的保存与加载

决策树的保存有很多种方法,但原理都是一样的,即序列化与反序列化,这里介绍以下两种方法。

#第一种方法
np.save('TheTree.npy',TheTree)
read_tree = np.load('TheTree.npy',allow_pickle=True).item()
复制代码

第一种方法是利用numpy库中的save方法,可以将字典格式的决策树保存为npy文件;当读取树时,需要在方法后加上item(),因为我们存储的数据是字典类型,若是矩阵类型则需删去。

#第二种方法
import pickle
def storeTree(inputTree, filename):
fw = open(filename,'wb')
pickle.dump(inputTree,fw)
fw.close()
def grabTree(filename):
fr = open(filename,'rb')
return pickle.load(fr)
复制代码

第二种方法是利用pickle库的dump方法对数据序列化,在读取时,用load方法即可加载数据,这里需要注意,不论写入还是读取时,都需要以二进制的格式,不然会发生报错。

六、利用决策树分类

构造决策树之后,可以将它用于实际数据的分类,在执行数据分类时,需要传入决策树、特征标签列表和用于分类的测试数据。然后程序会比较测试数据与决策树上的数值,递归执行该过程直到进入叶子节点,最后得到的分类结果就是叶子节点所属类型。 代码如下:

#传入的数据为决策树、数据集特征标签、测试数据
def classify(inputTree,labels,testVec):
#获取决策树的第一个节点
FirstStr = list(inputTree.keys())[0]
#取第一个节点外下一个字典
SecondDict = inputTree[FirstStr]
'''
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
{0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}
{0: 'no', 1: 'yes'}
'''

#取第一个节点在labels里的索引
feat_index = labels.index(FirstStr)
#遍历字典中的key
for key in SecondDict.keys():
#比较testVec里的值与树节点的值,如果达到叶子节点,返回类标签
if testVec[feat_index]==key:
#如果下一个字典中的仍包含字典,则递归继续比较
if type(SecondDict[key])==dict :
classlabel = classify(SecondDict[key],labels,testVec)
else:
#直到最后取到类标签
classlabel = SecondDict[key]
return classlabel
复制代码

其中传入特征标签列表labels的作用是帮助确定每次最优特征在数据集中的索引,利用index方法查找当前列表中第一个匹配FirstStr变量的元素,然后代码递归遍历整棵树,比较测试数据testVec变量中的值与树节点的值,直到达到叶子节点,返回当前节点的分类标签。

这里利用了上篇文章的数据构造的树做一个SecondDict举例,它的作用就是获取当前字典中最优特征(第一个关键字)的值,以达到与测试数据递归比较的效果。

classlabel = classify(inputTree,labels,[0,1,0,0])
'''
no lenses
'''

复制代码

执行该函数,可以将传入的数据与原文中的数据进行比对,得到的分类结果是一致的。

七、决策树可视化

决策树的主要优点就是直观易于理解,如果不能将其直观地显示出来,就无法发挥其优势。但通过matplotlib库绘制决策树是一个十分复杂的过程,这里偷懒介绍令一种比较简易的方法。

Graphviz是一种图形绘制工具,可以绘制出很多图形结构,但传入的数据需要的是dot格式,所以这里利用sklearn生成的决策树进行可视化。

Graphviz是需要手动下载的,并且安装结束后需要配置环境,将该文件夹的路径添加至系统变量的Path中,最后在cmd中输入dot -version若出现版本信息则代表安装配置成功。 在这里插入图片描述

决策树可视化代码如下:

from sklearn import tree
from sklearn.tree import DecisionTreeClassifier
import pydotplus
import graphviz
def pic_tree(DataSet):
#所有特征数据
feature_train = DataSet.iloc[:, :-1]
#类标签数据
the_label = DataSet.iloc[:, -1]
#unique将类标签去重
labels = the_label.unique().tolist()
#以类标签的在列表中的索引代替该标签——转化成数字
the_label = the_label.apply(lambda x: labels.index(x))
#训练数据
clf = DecisionTreeClassifier()
clf = clf.fit(feature_train, the_label)
#绘图过程
dot_data = tree.export_graphviz(clf, out_file=None, feature_names=['age','prescript','astigmatic','tearRate'],
class_names=['no lenses', 'soft','hard'], filled=True, rounded=True,
special_characters=True)
# 两种方法
# 1.利用graphviz库生成PDF图片
pic = graphviz.Source(dot_data)
pic.render('lense')
# 2.利用pydotplus库将Dot格式转成PDF
#graph = pydotplus.graph_from_dot_data(dot_data)
#return graph.write_pdf("lense.pdf")
复制代码

这里生成决策树图片时也有两种方法,第一种是利用graphviz库的Source方法生成PDF图片,第二种需要利用pydotplus库将Dot格式转成PDF,最后得到的可视化图片如下: 在这里插入图片描述

总结

综上有关决策树的相关知识介绍完毕,总体来说,这个分类算法还是易于理解的,但它是十分重要的,因为它为后面学习随机森林奠定了基础,每一个算法都有各自的适合环境,而决策树也有自己的有缺点。

决策树的优点:

  • 计算复杂度不高,输出结果易于理解。
  • 对中间缺失值不敏感。
  • 可以处理不相关特征数据。
  • 树能实现图形化。

决策树的缺点:

  • 当决策树过于复杂时,会出现过度拟合情况。
  • 比较不稳定,数据发生比较小的变化时也会导致生成不同的树。
  • 在样本不均衡时,权重不同会导致树出现偏差。

公众号【奶糖猫】后台回复“隐形眼镜”即可获取源码和数据供参考,感谢阅读。