目录node
让咱们来玩一个游戏,你如今在你的脑海里想好某个事物,你的同桌向你提问,可是只容许问你20个问题,你的回答只能是对或错,若是你的同桌在问你20个问题以前说出了你脑海里的那个事物,则他赢,不然你赢。惩罚,你本身定咯。python
决策树的工做原理就相似与咱们这个游戏,用户输入一些列数据,而后给出游戏的答案。算法
决策树听起来很高大善,其实它的概念很简单。经过简单的流程就能理解其工做原理。数据库
图3-1 决策树流程图数据结构
从图3-1咱们能够看到一个流程图,他就是一个决策树。长方形表明判断模块,椭圆形表明终止模块,从判断模块引出的左右箭头称做分支。该流程图构造了一个假想的邮件分类系统,它首先检测发送邮件域名地址。若是地址为 myEmployer.com,则将其放在分类“无聊时须要阅读的邮件”;若是邮件不是来自这个域名,则检查邮件内容里是否包含单词 曲棍球 ,若是包含则将邮件归类到“须要及时处理的朋友邮件”;若是不包含则将邮件归类到“无需阅读的垃圾邮件”。app
第二章咱们已经介绍了 k-近邻算法完成不少分类问题,可是它没法给出数据的内在含义,而决策树的主要优点就在此,它的数据形式很是容易理解。函数
那如何去理解决策树的数据中所蕴含的信息呢?接下来咱们就将学习如何从一堆原始数据中构造决策树。首先咱们讨论构造决策树的方法,以及如何比那些构造树的 Python 代码;接着提出一些度量算法成功率的方法;最后使用递归创建分类器,而且使用 Matplotlib 绘制决策树图。构造完成决策树分类器以后,咱们将输入一些隐形眼睛的处方数据,并由决策树分类器预测须要的镜片类型。工具
优势: 计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,能够处理不相关特征数据。 缺点: 可能会产生过分匹配问题。
在构造决策树的时候,咱们将会遇到的第一个问题就是找到决定性的特征,划分出最好的结果,所以咱们必须评估每一个特征。学习
# 构建分支的伪代码函数 create_branch 检测数据集中的每一个子项是否属于同一分类: if yes return 类标记 else 寻找划分数据集的最好特征 划分数据集 建立分直节点 for 每一个划分的子集 调用函数 create_branch 并增长返回结果到分支节点 return 分支节点
在构造 create_branch 方法以前,咱们先来了解一下决策树的通常流程:测试
1. 收集数据: 可使用任何方法 2. 准备数据: 树构造算法只适用于标称型数据,所以数值型数据必须离散化 3. 分析数据: 可使用任何方法,构造树完成以后,咱们应该检查图形是否符合预期 4. 训练算法: 构造树的数据结构 5. 测试算法: 使用经验树计算错误率 6. 使用算法: 此步骤能够适用于任何监督学习算法,而使用决策树能够更好的理解数据的内在含义
一些决策树采用二分法划分数据,本书并不采用这种方法。若是依据某个属性划分数据将会产生4个可能的值,咱们将把数据划分红四块,并建立四个不一样的分支。本书将采用 ID3 算法划分数据集。关于 ID3 算法的详细解释。
表3-1 海洋生物数据
不浮出水面是否能够生存 | 是否有脚蹼 | 属于鱼类 | |
---|---|---|---|
1 | 是 | 是 | 是 |
2 | 是 | 是 | 是 |
3 | 是 | 否 | 否 |
4 | 否 | 是 | 否 |
5 | 否 | 是 | 否 |
划分大数据的大原则是:将无序的数据变得更加有序。其中组织杂乱无章数据的一种方法就是使用信息论度量信息。信息论是量化处理信息的分支科学。咱们能够在划分数据以前或以后使用信息论量化度量信息的内容。其中在划分数据以前以后信息发生的变化称为信息增益,得到信息增益最高的特征就是最好的选择。
在数据划分以前,咱们先介绍熵也称做香农熵。熵定义为信息的指望值,它是信息的一种度量方式。
信息——若是待分类的事务可能划分在多个分类之中,则符号\(x_i\)的信息定义为:
\(l(x_i)=-log_2p(x_i)\) \(p(x_i)\)是选择该分类的几率
全部类别全部可能包含的信息指望值:
\(H=-\sum_{i=1}^np(x_i)log_2p(x_i)\)n是分类的数目
对公式有必定的了解以后咱们在 trees.py 文件中定义一个 calc_shannon_ent 方法来计算信息熵。
# trees.py from math import log 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 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 shannon_run(): data_set, labels = create_data_set() shannon_ent = calc_shannon_ent(data_set) print(shannon_ent) # 0.9709505944546686 if __name__ == '__main__': shannon_run()
混合的数据越多,则熵越高,有兴趣的同窗能够试试增长一个 ’maybe’ 的标记。
若是还有兴趣的能够自行了解另外一个度量集合无序程度的方法是基尼不纯度,简单地讲就是从一个数据集中随机选取子项,度量其被错误分类到其余分组里的几率。
咱们已经测量了信息熵,可是咱们还须要划分数据集,度量划分数据集的熵,所以判断当前是否正确地划分数据集。你能够想象一个二维空间,空间上随机分布的数据,咱们如今须要在数据之间划一条线把它们分红两部分,那咱们应该按照 x 轴仍是 y 轴划线呢?
所以咱们能够定义一个 split_data_set 方法来解决上述所说的问题:
# trees.py 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 split_data_run(): my_data, labels = create_data_set() ret_data_set = split_data_set(my_data, 0, 1) print(ret_data_set) # [[1, 'yes'], [1, 'yes'], [0, 'no']] if __name__ == '__main__': # shannon_run() split_data_run()
接下来咱们把上述两个方法结合起来遍历整个数据集,找到最好的特征划分方式。
所以咱们须要定义一个 choose_best_feature_to_split 方法来解决上述所说的问题:
# trees.py 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 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 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 choose_best_feature_run(): data_set, _ = create_data_set() best_feature = choose_best_feature_to_split(data_set) print(best_feature) # 0 if __name__ == '__main__': # shannon_run() # split_data_run() choose_best_feature_run
咱们发现第0个即第一个特征是最好的用于划分数据集的特征。对照表3-1,咱们能够发现若是咱们以第一个特征划分,特征值为1的海洋生物分组将有两个属于鱼类,一个属于非鱼类;另外一个分组则所有属于非鱼类。若是按照第二个特征划分……能够看出以第一个特征划分效果是较好于以第二个特征划分的。同窗们也能够用 calc_shannon_entropy 方法测试不一样特征分组的输出结果。
咱们已经介绍了构造决策树所须要的子功能模块,其工做原理以下:获得原始数据集,而后基于最好的属性值划分数据集,因为特征值可能多于两个,所以可能存在大于两个分支的数据集划分。第一次划分后,数据将被向下传递到树分支的下一个节点,在这个节点上,咱们能够再次划分数据。所以咱们采用递归的原则处理数据集。
递归结束的条件是:程序遍历完全部划分数据集的属性,或者每一个分支下的全部实例都具备相同的分类。若是全部实例具备相同的分类,则获得一个叶子节点或者终止块。任何到达叶子节点的数据必然属于叶子节点的分类。如图3-2 所示:
图3-2 划分数据集时的数据路径
第一个结束条件使得算法能够终止,咱们甚至能够设置算法能够划分的最大分组数目。后续章节会陆续介绍其余决策树算法,如 C4.5和 CART,这些算法在运行时并不老是在每次划分分组时都会消耗特征。因为特征数目并非在每次划分数据分组时都减小,所以这些算法在实际使用时可能引发必定的问题。但目前咱们并不须要考虑这个问题,咱们只须要查看算法是否使用了全部属性便可。若是数据集已经处理了全部特征,可是该特征下的类标记依然不是惟一的,可能类标记为是,也可能为否,此时咱们一般会采用多数表决的方法决定该叶子节点的分类。
所以咱们能够定义一个 maority_cnt 方法来决定如何定义叶子节点:
# trees.py def majority_cnt(class_list): """对 class_list 作分类处理,相似于 k-近邻算法的 classify0 方法""" import operator 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]
在定义处理叶子节点的方法以后,咱们就开始用代码 create_tree 方法实现咱们的整个流程:
# trees.py 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] # TODO 补充解释 # 若是最后使用了全部的特征没法将数据集划分红仅包含惟一类别的分组 # 所以咱们遍历完全部特征时返回出现次数最多的特征 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_vals = set(feat_values) # 遍历最适合特征对应的全部可能值,而且对这些可能值继续生成子树 # 相似于 choose_best_feature_to_split 方法 for value in unique_vals: 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 create_tree_run(): my_dat, labels = create_data_set() my_tree = create_tree(my_dat, labels) print(my_tree) # {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}} if __name__ == '__main__': # shannon_run() # split_data_run() # choose_best_feature_run() create_tree_run()
咱们已经手动为咱们的流程生成了一个树结构的字典,第一个关键字 ‘no surfacing’ 是第一个划分数据集的特征名字,该关键字的值也是另外一个数据字典。第二个关键字是 ‘no surfacing’ 特征划分的数据集,这些关键字的值是 ‘no surfacing’ 节点的子节点。若是值是类标签,则盖子节点是叶子节点;若是值是另外一个数据字典,则子节点是一个判断节点,这种格式结构不断重复就狗策很难过了整棵树。
咱们已经用代码实现了从数据集中建立树,可是返回的结果是字典,并不容易让人理解,可是决策树的主要优势是易于理解,所以咱们将使用 Matplotlib 库建立树形图。
Matplotlib 库提供了一个注解工具annotations,他能够在数据图形上添加文本注释。而且工具内嵌支持带箭头的划线工具。
所以咱们建立一个 tree_plotter.py 的文件经过注解和箭头绘制树形图。
# tree_plotter.py import matplotlib.pyplot as plt from chinese_font import font # 定义文本框和箭头格式 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, fontproperties=font) 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() if __name__ == '__main__': create_plot()
图3-3 plot_node例子
咱们顺利已经构造了一个能够生成树节点的方法,输出结果如图3-3 所示。
咱们已经能够本身构造一个树节点了,可是咱们的最终目标是构造一个树来展现咱们的整个流程。构造树不像构造树节点同样随意,咱们须要知道树有多少层,有多少个树节点,每一个树节点的位置。
所以咱们定义两个新方法 get_num_leafs 和 get_tree_depth 来获取叶节点的数目和树的层度,而且为了以后不用再去本身生成树,咱们定义 retrieve_tree 方法生成树。
# tree_plotter.py 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 isinstance(second_dict[key], 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 isinstance(second_dict[key], 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] def get_num_and_get_depth(): my_tree = retrieve_tree(0) print(my_tree) # {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}} num_leafs = get_num_leafs(my_tree) max_depth = get_tree_depth(my_tree) print(num_leafs) # 3 print(max_depth) # 2 if __name__ == '__main__': # create_plot() get_num_and_get_depth()
全部准备工做都已经齐全了,目前咱们只须要作的就是生成一颗完整的树了。
让咱们自定义 create_plot 方法来生成咱们的第一棵树吧!
# treePlotter.py # TODO 补充解释 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_2.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.total_w, 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] # 减小 y 偏移 plot_tree.yOff = plot_tree.yOff - 1.0 / plot_tree.total_d for key in list(second_dict.keys()): if isinstance(second_dict[key], dict): plot_tree(second_dict[key], cntr_pt, str(key)) else: plot_tree.xOff = plot_tree.xOff + 1.0 / plot_tree.total_w 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.total_d def create_plot_2(in_tree): """""" # 建立并清空画布 fig = plt.figure(1, facecolor='white') fig.clf() # axprops = dict(xticks=[], yticks=[]) create_plot_2.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.xOff = -0.5 / plot_tree.total_w plot_tree.yOff = 1.0 plot_tree(in_tree, (0.5, 1.0), '') plt.show() def create_plot_2_run(): """create_plot_2的运行函数""" my_tree = retrieve_tree(0) create_plot_2(my_tree) if __name__ == '__main__': # create_plot() # get_num_and_get_depth() create_plot_2_run()
最终结果如图3-4
图3-4 超过两个分支的树
咱们已经学习了如何从原始数据中建立决策树,而且可以使用 Python 绘制树形图,可是为了咱们了解数据的真实含义,下面咱们将学习若是用决策树执行数据分类。
# trees.py 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 = None for key in list(second_dict.keys()): if test_vec[feat_index] == key: if isinstance(second_dict[key], dict): class_label = classify(second_dict[key], feat_labels, test_vec) else: class_label = second_dict[key] return class_label def classify_run(): from 第三章.code.tree_plotter import retrieve_tree my_dat, labels = create_data_set() my_tree = retrieve_tree(0) class_label = classify(my_tree, labels, [1, 0]) print(class_label) # 'no'
index 函数解决在存储带有特征的数据会面临一个问题:程序没法肯定特征在数据集中的位置。
每次使用分类器时,都必须从新构造决策树,而且构造决策树是很耗时的任务。
所以咱们构造 store_tree 和 grab_tree 方法来建立好的决策树。
# trees.py def store_tree(input_tree, filename): import pickle with open(filename, 'wb') as fw: pickle.dump(input_tree, fw) def grab_tree(filename): import pickle with open(filename, 'rb') as fr: return pickle.load(fr) def store_grab_tree_run(): import os from 第三章.code.tree_plotter import retrieve_tree my_tree = retrieve_tree(0) classifier_storage_filename = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'classifierStorage.txt') store_tree(my_tree, classifier_storage_filename) my_tree = grab_tree(classifier_storage_filename) print(my_tree) # {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}} if __name__ == '__main__': # shannon_run() # split_data_run() # choose_best_feature_run() # create_tree_run() # classify_run() store_grab_tree_run()
决策树经过一个小数据集技能学到不少知识。下面咱们经过一颗决策树来帮助人们判断须要佩戴的镜片类型。
1. 收集数据: 提供的文本文件 2. 准备数据: 解析 tab 键分隔的数据行 3. 分析数据: 快速检查数据,确保正确地解析数据内容,使用 create_plot 函数绘制最终的树形图。 4. 训练算法: 使用以前的 create_tree 函数 5. 测试算法: 编写测试函数验证决策树能够正确分类给定的数据实例 6. 使用算法: 存储输的数据结构,以便下次使用时无需从新构造树
加载源自 UCI 数据库的隐形眼镜数据集。
# trees.py def store_grab_tree_run(): import os from 第三章.code.tree_plotter import retrieve_tree my_tree = retrieve_tree(0) classifier_storage_filename = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'classifierStorage.txt') store_tree(my_tree, classifier_storage_filename) my_tree = grab_tree(classifier_storage_filename) print(my_tree) # {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}} def create_lenses_tree(): import os lenses_filename = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'lenses.txt') with open(lenses_filename, 'rt', encoding='utf-8') as fr: lenses = [inst.strip().split('\t') for inst in fr.readlines()] lenses_labels = ['age', 'prescript', 'astigmatic', 'tearRate'] lenses_tree = create_tree(lenses, lenses_labels) return lenses_tree def plot_lenses_tree(): from 第三章.code import tree_plotter lenses_tree = create_lenses_tree() tree_plotter.create_plot(lenses_tree) if __name__ == '__main__': # shannon_run() # split_data_run() # choose_best_feature_run() # create_tree_run() # classify_run() # store_grab_tree_run() plot_lenses_tree()
图3-5 由 ID3 算法产生的决策树
由图3-4咱们能够看出咱们只须要问4个问题就能肯定患者须要佩戴哪一种类型的眼镜。
可是细心的同窗应该发现了咱们的决策树很好的匹配了实验数据,然而这些匹配选项可能太多了,咱们称之为过分匹配,为了解决过分匹配的问题,咱们能够裁剪决策树,去掉不必的叶子节点。若是叶子节点只能增长少量信息,则能够删除该节点,并将它传入到其余叶子节点中。不过这个问题咱们须要之后使用决策树构造算法 CART 来解决。而且若是存在太多的特征划分,ID3 算法仍然会面临其余问题。
决策树分类器就像带有终止块的流程图,终止块表示分类结果。开始处理数据时,咱们首先须要测量集合中数据的不一致性,也就是熵,而后寻找最优方案划分数据集,知道数据集中的全部数据属于统一分类。
对数据进行分类后,咱们通常使用 Python 语言内嵌的数据结构字典存储树节点信息。
以后咱们使用 Matplotlib 的注解功能,咱们将存储的树结构转化为容易理解的图像。可是隐形眼镜的例子代表决策树可能会产生过多的数据集,从而产生过分匹配数据集的问题。之后咱们能够经过裁剪决策树,合并相邻的没法产生大量信息增益的叶节点,消除过分匹配问题。