预备知识
决策树顾名思义是一种树形结构。
决策树在进行训练的时候,会使用某种策略(如ID3)进行最优属性选择,按照最优属性的取值将原始数据集划分为几个数据子集,然后递归的生成一棵决策树。
最优属性选择
显然,我们希望划分以后的数据子集尽可能地属于同一类别。因此,我们需要有一种衡量集合“纯度”的标准。以不同标准进行划分,也就有了各种版本的决策树,著名的有ID3,C4.5,Gini等。在此仅介绍ID3和Gini划分标准。
信息增益
首先,我们定义一个集合 D 的“信息熵”:
其中
k
取遍所有的类别。
假定离散属性a可以有V个可能的值
一般认为信息增益越大,划分效果越好,即该策略会选择信息增益最大的那个属性去划分数据集。
利用这种策略去选取属性的叫做ID3决策树。
基尼指数
同样地,我们先定义一个集合 D 的“基尼值”:
直观的理解就是,基尼值衡量了从集合 D 中随机取出两个样本,这两个样本不同的概率有多大。
显然,基尼值的取值越小,表示这两个样本不同的概率越小,也即是数据集
类似地,我们定义离散属性a的“基尼指数”:
Gini_index(D,a)=∑v|Dv||D|Gini(Dv)
这里,我们会选取划分后基尼指数最小的那个属性最为最优划分属性。
利用这种策略取选取最有划分属性的叫做CART决策树。
算法流程
数据集D={(x1,y1),(x2,y2),...(xm,ym)}
属性集A={a1,a2,...ad}
build-tree(D, A)
- if D中的样本全属于同一类别C then
- return C
- if 属性集A为空 then
- return D中样本数最多的类标签
- 以某种策略选择最优划分属性 ak
- 得到以属性 ak 划分的数据子集subsets {D1,D2,...,Dv}
- 新建根节点tree
- for Di in subsets:
- subtree = build-tree( Di , A−{ak} )
- 将subtree置为tree的孩子节点
- return tree
实现
说了那么多,我们现在可以尝试一下用Python语言实现决策树算法。
首先导入必要的包
import math import utility from collections import Counter
生成一些人工数据
def create_data(): data = [[1, 1], [1, 1], [1, 0], [0, 1], [0, 1]] label = ['yes', 'yes', 'maybe', 'no', 'no'] # 标签 feature_names = ['no surfacing','flippers'] # 属性集合 return data, label, feature_names
然后我们定义一个函数用以计算集合的信息熵
def cal_entropy(y): cnter = Counter(y) n_examples = len(y) probs = [val*1.0/n_examples for val in cnter.itervalues()] # 先计算每个类别占的比例 ent = -sum(p*math.log(p, 2) for p in probs) # 然后根据信息熵的定义即可求得信息熵 return ent
现在我们定义一个划分函数,这个函数可以指定要划分的属性,由参数axis决定
def split_data(X, y, axis): ans = [] all_vals = set(item[axis] for item in X) data_dict = {key:[] for key in all_vals} label_dict = {key:[] for key in all_vals} n_examples = len(y) for vec, lb in zip(X, y): # 下面两行代码删除(略去)了axis位置的值 reduced_vec = vec[:axis] reduced_vec.extend(vec[axis+1:]) data_dict[vec[axis]].append(reduced_vec) label_dict[vec[axis]].append(lb) for key in all_vals: ans.append((key, data_dict[key], label_dict[key])) return ans
我们来细看一下这个函数在做什么:
首先我们获得属性axis的所有可能的取值all_vals
然后迭代每一个属性值去生成两个字典,字典的key是属性值,value是一个空的链表。data_dict是用来存储特征的,而label_dict是用来存储标签的
接着我们迭代每一个训练样本,把它们添加到特定的属性值的链表中
接着我们从字典中取出划分后的每一个子集,每一个子集包含三个东西(在axis上的属性值,数据,标签),即代码中的(key,data_dict[key],label_dict[key])
可以尝试调用一下,输出如下的结果:
data_sets = split_data(data, label, 0) # [(0, [[1], [1]], ['no', 'no']), (1, [[1], [1], [0]], ['yes', 'yes', 'maybe'])] # 这里有两个个子集,分别为 # (0, [[1], [1]], ['no', 'no'])和(1, [[1], [1], [0]], ['yes', 'yes', 'maybe'])
有了上面这个函数,我们就可以利用它来计算信息增益以选取最优划分属性了
def choose_feature_to_split(X, y): n_features = len(X[0]) n_examples = len(y) best_ent_gain = -(1<<21) # 设置成一个很小的值 best_axis = -1 best_data_sets = None # 迭代每个属性,对它进行划分,求信息增益 for i in range(n_features): data_sets = split_data(X, y, i) cur_ent = 0.0 for key, xx, yy in data_sets: part_ent = cal_entropy(yy) p = len(yy)*1.0 / n_examples cur_ent += p*part_ent # 这里不用去计算原始数据的信息熵,我们只需要比较一下相对大小就行,所以可以取0.0 cur_ent_gain = 0.0 - cur_ent # 记录最好的属性以及相应的数据划分 if cur_ent_gain > best_ent_gain: best_ent_gain = cur_ent_gain best_axis = i best_data_sets = data_sets return best_axis, best_data_sets
在真正建立决策树前,我们需要两个辅助函数
- 投票函数
def vote(y): cnter = Counter(y) ans = cnter.most_common(1)[0] return ans[0]
- 检查集合是否属于同一类别
def check_if_one_class(y): return y.count(y[0]) == len(y)
最后我们把上面的子模块整合在一起,建立一棵决策树,差不多就是前面的伪代码
def build_tree(X, y, feature_names): # 只剩一个类别 if check_if_one_class(y): return y[0] # 没有属性可供我们划分了 if len(X[0]) == 0: return vote(y) # 否则选取最优划分属性,获得划分后的数据子集 axis, data_sets = choose_feature_to_split(X, y) best_label = feature_names[axis] # 记得把用过的属性删除掉 del(feature_names[axis]) subtree = {} for key, xx, yy in data_sets: # 记得复制,不能直接赋值 feature_names_copy = feature_names[:] # 递归建立子树 subtree[key] = build_tree(xx, yy, feature_names_copy) tree = {best_label:subtree} return tree
有一点要说明的是上面的递归树是存储在字典中的,python的字典数据结构允许我们很容创建一棵树。
它的结构具体如下{属性:{属性值1:子树1,属性值2:字树2,…,属性值n:子树n}}
tree = build_tree(data, label, labelnames) print tree # 输出如下: # {'no surfacing': {0: 'no', 1: {'flippers': {0: 'maybe', 1: 'yes'}}}}
然后我们就可以利用工具可视化这么一棵决策树了,具体代码看这里,效果如下
使用sklearn中的决策树
是不是觉得自己实现有点繁琐啊,那我们就来看一下python的机器学习包sklearn中决策树的使用(下面的代码是在jupyter notebook里写的)
导入相应的包
from sklearn.datasets import load_iris from sklearn.tree import DecisionTreeClassifier import matplotlib.pyplot as plt %matplotlib inline
加载数据
data = load_iris()
将数据分为训练集和验证集
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test= train_test_split(data.data, data.target, test_size=.25)
训练一个决策树分类器
# 这里criterion就是我们说的划分策略,可以自己去试一下其他的标准 clf = DecisionTreeClassifier(criterion='entropy') clf.fit(X_train, y_train)
看一下效果怎么样
print 'Training accuracy: ', clf.score(X_train, y_train) print 'Testing accuracy: ', clf.score(X_test, y_test) # Training accuracy: 1.0 # Testing accuracy: 0.921052631579 # 恩,还不错
最后,我们来看一下这棵决策树长什么样子的
- 这小段代码估计得先pip install pydotplus
- 然后再到这个网站下载安装相应系统下的graphviz
- 然后,应该就可以了
from IPython.display import Image from sklearn import tree import pydotplus dot_data = tree.export_graphviz(clf, out_file=None, feature_names=data.feature_names, class_names=data.target_names, filled=True, rounded=True, special_characters=True) graph = pydotplus.graph_from_dot_data(dot_data) Image(graph.create_png())
(是不是比我们自己画的好看多了==)
小结
决策树有个很好的特性叫:highly interpretable,就是极强的可解释性,也就是它学习到的知识不像其他学习器(如神经网络)那么抽象,而是很具体清晰的一些规则,这也让决策树广泛应用于很多专家系统中。
决策树中还有相当多的知识值得探讨,比如缺失值和连续值的处理,怎样通过剪枝(预剪枝和后剪枝)来对付过拟合,还有如何通过多变量决策树解决复杂决策便捷问题等。