说明
本博客为本人学习过程中的笔记记录,加上一些思路总结,可能比较琐碎,希望阅读者见谅。
简介
决策树,作为众多树模型的基础,以它来作为另一个的开篇最合适不过。其核心思想就是从数据中学出一套分类规则,这样的分类规则有多种(意味着决策树不唯一),我们的目的,是找到那个在局部和全局上都表现不错的:局部(从数据中学到的分类效果很好,误差小),全局(未知数据中分类效果也很好,鲁棒性好)。
俯视图
决策树,顾名思义,就是 决策/ 树。但实际过程中却是反过来的:
- 从数据中学 出一颗树。
- 剪枝(树模型都很容易过拟和,减枝可以缓和这种现象)
- 用学到的树分类。
一些概念
上面这张图可以视作一个决策树,它一共有两个比较重要的元素:
1. 非叶子节点(包含根节点,也叫决策点,每个节点都表示**选择的不同特征**,这个特征用来将样本分类)
2. 叶子节点(每个节点表示我分出来的类,当然,不同节点可以表示同一类,但是每个节点里的样本是互斥的)
特征选择
这小节就是针对上面1中加粗的部分进行解释,如何进行特征选择?
熵
严格来说,属于信息论的东西,举一个实际例子,假设我原始样本的标签是A[0,0,0,1,1,1,0,0,1,1],经过我样本的某个特征,我的样本可以分成B[0,0,0,1], C[1,1,0,0,1,1],我怎样以数学的方式衡量这个选择的好坏??
化学里叫混乱程度,在这里,表面理解:属于同一类样本占比越多,混乱程度越低,熵越小。我可以用熵的计算公式得到熵,那上面的分布可以变为: 熵A,熵B,熵C,而怎样衡量选择的好坏,做差就好了 熵A - (熵B + 熵C),这个式子的结果还有个名字,信息增益。所以综上,熵充当了什么成分不言而喻。
下面是它的计算公式,其中的概率均通过统计得到,如A中0的概率,怎么算应该知道吧…:
目标明确
希望您知道目标函数的含义,对于一颗决策树,当然也是要有个目标函数来衡量的,而衡量的思想:随着树深度的增加,熵在快速减小。换句人听的懂的,就是从根节点开始,从上往下的非叶子节点,我都尽可能的让那些使熵迅速减少的特征位于前面,即如果用信息增益衡量的话,使信息增益大的特征排在前面。
最后我怎样衡量构建的树呢?(只考虑局部最优)
其中t是叶子节点,Nt是当前叶子节点里的样本个数,H(T)是当前叶子节点的熵。
特征选择举例
假如我们有如下的数据:前四列是 特征,最后一列是标签。
最开始的根节点处的特征选哪个?可以通过如下操作:
分别计算每一个特征分类后的熵,计算信息增益,选择最大的特征,作为根节点的特征。此过程递归,就是剩下特征的操作。
不同衡量标准
- ID3(一个算法):信息增益。
- C4.5:信息增益率。
- CART: (分类)Gini系数 (回归)MSE
以上是版本的更迭下,不同的衡量标准,其实目的用法一样,只是计算公式不一样,当然,考虑的细节也有差异。
递归算法
谈谈递归
1. 递归程序更加简洁,更容易理清意思/也不容易理清意思。
2. 递归程序本质可以理解为栈,先进后出。
3. 递归并不是一个为性能优化而生的程序思想,相反,在很多情况下,其对性能的消耗是巨大的。
4. 递归思想的本质是大问题化为数个解决方案相同的小问题。
5. 学递归有这么一个现象,你可能看得懂,但是有时候自己就是写不出来,恭喜你,你其实还没有理解递归的本质,练的不够多。巧了——我也是.....
递归算法要求。递归算法所体现的“重复”一般有三个要求:
(1) 是每次调用在规模上都有所缩小(通常是减半);
(2) 是相邻两次重复之间有紧密的联系,前一次要为后一次做准备(通常前一次的输出就作为后一次的输入);
(3) 是在问题的规模极小时必须用直接给出解答而不再进行递归调用,因而每次递归调用都是有条件的(以规模未达到直接解答的大小为条件),无条件递归调用将会成为死循环而不能正常结束。
斐波纳契数列
代码
if (n <= 1) /* base case */
return n;
else /* recursive caseS */
return f(n-1) + f(n-2);
代码结果可视化
见到的递归最好的可视化是树形结构的呈现形式,所有的叶子节点表示走不下去了,也就是所谓的出口,所有的非叶子节点都对最后的结果计算没有意义。上面那个可视化的图,真的好美妙。 具体可视化网址见参考小节。
决策树生成
决策树的生成对应最开始说的“局部”,也是上面说过的,这是一个递归过程。
IE3
减枝
预减枝
预剪枝就是在树的构建过程(只用到训练集),设置一个阈值(样本个数小于预定阈值或GINI指数小于预定阈值),使得当在当前分裂节点中分裂前和分裂后的误差超过这个阈值则分列,否则不进行分裂操作。
看了看预减枝的代码,但是有个问题,测试集这样验证有问题,因为没有用到树的结构信息,只是单纯用了这个节点,测试集也只是单纯用了两列。想象一下,我从根节点到中间某个节点,一定是经过好几个节点特征分过的,此时到这个节点的样本数量肯定少了,但是预剪枝里,却是没有这个过程噢,只是单纯用测试集合所有样本去验证这个节点,感觉并不好。。。
当然,本来预剪枝就没后剪枝好,这里只是单纯看了代码吐槽下,下面给出看到代码的关键逻辑代码。
感谢大佬提供代码参考:
https://github.com/appleyuchi/Decision_Tree_Prune/tree/master/ID3-REP-post_prune-Python-draw
# 筛选条件
# 如果 继续分后的错误率 < 不分的错误率
if testing_feat(bestFeatLabel, dataSet, test_data, labels_copy) < testingMajor(majorityCnt(classList),test_data)
#这个用于预剪枝,目的:测试当前分了节点后在测试集上的错误率
def testing_feat(feat, train_data, test_data, labels):
print"train_data=",json.dumps(train_data,ensure_ascii=False)
class_list = [example[-1] for example in train_data]
bestFeatIndex = labels.index(feat)
train_data = [example[bestFeatIndex] for example in train_data]
test_data = [(example[bestFeatIndex], example[-1]) for example in test_data]
all_feat = set(train_data)
error = 0.0
for value in all_feat:
class_feat = [class_list[i] for i in range(len(class_list)) if train_data[i] == value]
major = majorityCnt(class_feat)
for data in test_data:
if data[0] == value and data[1] != major:
error += 1.0
# print 'myTree %d' % error
return error
后减枝
文字参考:https://blog.csdn.net/t15600624671/article/details/78895267
代码参考:https://github.com/appleyuchi/Decision_Tree_Prune/tree/master/ID3-REP-post_prune-Python-draw
(1)后剪枝是在用训练集构建好一颗决策树后,利用测试集进行的操作。
(2)在回归树一般用总方差计算误差(即用叶子节点的值减去所有叶子节点的均值)。
后剪枝的算法包括:
Reduced-Error Pruning (REP,错误率降低剪枝)
这个思路很直接,完全的决策树不是过度拟合么,我再搞一个测试数据集来纠正它。对于完全决策树中的每一个非叶子节点的子树,我们尝试着把它替换成一个叶子节点,该叶子节点的类别我们用子树所覆盖训练样本中存在最多的那个类来代替,这样就产生了一个简化决策树,然后比较这两个决策树在测试数据集中的表现,如果简化决策树在测试数据集中的错误比较少,那么该子树就可以替换成叶子节点。
该算法以bottom-up的方式遍历所有的子树,直至没有任何子树可以替换使得测试数据集的表现得以改进时,算法就可以终止。
实现
本来以为要动态规划了,但是找到的代码看完让我惊了一下,还可以这样????有点新奇啊,而且个人感觉这种做法行的通。
本人将原有的后减枝代码融到了自己的代码里,预减枝由于觉得不好,就没加,下面展示关键代码:
if is_post:
# 继续分错误的损失 - 部分错误的损失 > 阈值
if self.testing(myTree, test_data,test_feature_name) - self.testingMajor(self.majorityCnt(labels), test_data) > theta:
return self.majorityCnt(labels)
# 实现后剪枝操作
# 无视当前的myThree,直接返回一个叶子节点,等效于实现了REP后剪枝
return myTree
- 这里将原代码的比较改为做差,与阈值比较,更加合理点。
- 此代码位于生成树的代码内,位于最后,是不是很奇怪,不是有了完整的树再减枝叶,其实这里这种写法个人感觉也似等价的,因为它的参数里有树结构,也就是这里它把树的结构考虑进去了。个人感觉很秒啊…
由于粘贴代码格式太乱,而且本身其实这里想让看看上面那个函数的位置,所以截一个全局的图出来,宏观看看。
离散化和连续化
其实本来以为决策树觉得两天基本就完事儿了,但是由于想到所谓做戏做全套,就一直在考虑各种问题,上面所有的代码是一个不断优化的过程(当然本人由于自带bug体,改bug用的时间也很长…)
本人参考代码尝试加入支持连续化,但是实际情况下倒不是难,而是很难全面,自己实操过程中把代码改的太过冗余复杂,而且容错性太低太低了,当然参考代码写的说实话也不怎么样,甚至本身逻辑在这里就有问题,所以最后本人删除了连续化操作。
可视化
这里直接使用的原代码函数,没有去去研究画图函数。
CART
分类树
代码refer: https://github.com/RRdmlearning/Decision-Tree
参考代码作者对应知乎:https://zhuanlan.zhihu.com/p/32164933
可能你会碰到需要额外安装的包,本人Ubuntu16碰到了Bug,用于以下命令安装正常:
pip install pydot>=1.2.4 sudo apt-get install graphviz
其实分类还是那个味道,基本换汤不换药,这里由于换了参考代码,其实最大的变化是数据结构上的变化,原本是字典存储,下面这种数据结构在针对小数据集时更方便,因为很多信息都存了下来,比单纯用字典好很多。下面是其代码:
# 树的结构由它存储
class Tree:
def __init__(self, value=None, trueBranch=None, falseBranch=None, results=None, col=-1, summary=None, data=None):
# 最优分割列
self.col = col
# 当前列内选择的分割点
self.value = value
# 左右(是否为此值) 分支
self.trueBranch = trueBranch
self.falseBranch = falseBranch
# 此节点上的分类结果(叶子节点才会设置)
self.results = results
# 存储叶子节点上的数据信息
self.data = data
# 类似于日志信息,画图时需要
self.summary = summary
———— 1.这里个人同样没有深究画图函数,有兴趣的朋友可以自己细读。
————2.参考代码减枝操作与李航树上的伪代码并不相同,也算是减枝的一种,如下面介绍
减枝
Critical Value Pruning:
该方法由Mingers1987年发明。
树的生成过程中,会得到选择属性及分裂值的评估值,设定一个阈值,所有小于此阈值的节点都剪掉
回归树
在处理回归问题时,CART算法使用均方误差MSE(Mean Squared Error)来衡量节点的不纯度;MSE越大,不纯度越高。
这里没有找到好的参考代码,本人直接参考分类树进行了修改,只改了两部分:
- MSE替换gini筛选特征
- 叶子节点取均值表示改叶子节点的回归值。
个人理解的回归树只需修改这两点,若不全面请指教
上代码
注意: 该代码内数据无实际参考意义,只为跑通代码使用。
参考
[1] 李航 《统计机器学习》
[2] B站 唐宇迪——决策树
视频: https://www.bilibili.com/video/av26086646?p=3
对应代码:https://pan.baidu.com/s/19kfm9F68MNmAz-QshadFtg 密码:l4l7
[3] 递归算法
算法讲解:https://chenqx.github.io/2014/09/29/Algorithm-Recursive-Programming/
可视化网址:https://visualgo.net/zh
*注:其余参考代码内或文中间已经指明,感谢这些大佬的分享。