本文将尽量使用通俗易懂的方式,尽可能不涉及数学公式,而是从整体的思路上来看,运用感性直觉的思考来解释 集成学习。并且用水浒传为例学习。并且从名著中延伸了具体应用场景来帮助大家深入这个概念。
[白话解析] 通俗解析集成学习之bagging,boosting & 随机森林
0x00 摘要
本文将尽量使用通俗易懂的方式,尽可能不涉及数学公式,而是从整体的思路上来看,运用感性直觉的思考来解释 集成学习。并且从名著中延伸了具体应用场景来帮助大家深入这个概念。
在机器学习过程中,会遇到很多晦涩的概念,相关数学公式很多,大家理解起来很有困难。遇到类似情况,我们应该多从直觉角度入手思考,用类比或者举例来附会,这样往往会有更好的效果。
我在讲解论述过程中给自己的要求是:在生活中或者名著中找一个例子,然后用自己的话语阐述出来。
0x01 相关概念
1. 偏置( bias ) vs 方差( variance )
- 平方偏置( bias ),表示所有数据集的平均预测与预期的回归函数之间的差异。
- 方差( variance ),度量了对于单独的数据集,模型所给出的解在平均值附近波动的情况,
方差 度量了同样大小的训练集的变动所导致的学习性能的变化,度量了在面对同样规模的不同训练集时,学习算法的估计结果发生变动的程度(相关于观测样本的误差,刻画了一个学习算法的精确性和特定性:一个高的方差意味着一个弱的匹配),即刻画了数据扰动所造成的影响。
偏置 度量了学习算法期望预测与真实结果的偏离程度,度量了某种学习算法的平均估计结果所能逼近学习目标的程度(独立于训练样本的误差,刻画了匹配的准确性和质量:一个高的偏置意味着一个坏的匹配),即刻画了学习算法本身的拟合能力。
网上的比较好的例子是那个标靶的例子,大家可以去看看
- High Variance, low bias 对应就是点都打在靶心附近,但是散乱,所以瞄的是准的,但手不一定稳。
- Low variance, high bias 对应就是点都打的很集中,但不一定是靶心附近,手很稳,但是瞄的不准。
这里试图用水浒传来解释下这两个概念。
2. 用梁山为例通俗说方差
比如说五虎将,武力平均值就是呼延灼,其下是秦明,董平;其上是林冲,关胜。 假设武力值是 秦明 95,董平 96,呼延灼 97,关胜 98,林冲 99 针对这个样本,其方差就是秦明,董平,林冲,关胜这四人的武力值和呼延灼差距多少。 如果都和呼延灼需要大战八百回合才能分出胜负,那么说明方差小。如果大战三百合就能分出胜负,那说明方差大。
3. 用梁山为例通俗说偏置
假设马军八骠骑的武力值如下:徐宁 92、索超 90、张清 86、朱仝 90、史进 88、穆弘 85,花荣 90、杨志 95。 如果有一个模型,用八骠骑来拟合五虎将。这个模型是:任意选取一个八骠骑来拟合。( 实际工作中偏置应该是取八骠骑平均值,这里偏置是任选一个,是为了说明方便 ) 如果用杨志来拟合五虎将,则最能逼近五虎将,这时候偏置小。如果选出的是穆弘,则偏置最大。 对于没遮拦穆弘,大家估计都是只知其名不知其事,可见其武力值最低。 当然穆弘的故事可能是在古本《水浒》有,但是后来世代相传中从今本《水浒》中被删除,所以被低视。 水浒故事有系统的文字记录首见《宣和遗事》。该书讲述先后落草太行山梁山、后来《水浒》说成是入伙鲁西梁山泊的人物共三十八名。其中的没遮拦穆横无疑就是《水浒》中的穆弘。 《宣和遗事》中的穆横是押运花石纲的十二指使之一。这十二人因公务而结义,后又因营救杨志而同往太行山落草,遂成为开创山寨的基本成员。《水浒传》处理这十二指使,有八人是和穆横完全一样待遇的(林冲、花荣、张清、徐宁、李应、关胜、孙立、杨志)。 这八人全都在《水浒》里成了独当一面的人物。如果一切顺应传统去发展,穆弘总不致变成一个有名无实的空白人物。
下面是《宣和遗事》
先是朱勔运花石纲时分,差着杨志、李进义、林冲、王雄,花荣、柴进、张青、徐宁、李应、穆横、关胜、孙立十二人为指使,前往太湖等处,押人夫搬运花石。
那十二人领了文字,结义为兄弟,誓有灾厄,各相救援。李进义等十名,运花石已到京城;只有杨志在颍州等候孙立不来,在彼处雪阻。那雪景如何却是:乱飘僧舍茶烟湿,密洒歌楼酒力微。
那杨志为等孙立不来,又值雪天,旅涂贫困,缺少果足,未免将一口宝刀出市货卖。终日价无人商量。行至日哺,遇一个恶少后生要卖宝刀,两个交口厮争,那后生被杨志挥刀一斫,只见颈随刀落。杨志上了枷,取了招状,送狱推勘。结案申奏文字回来,太守判道:“杨志事体虽大,情实可悯。将杨志诰札出身尽行烧毁,配卫州军城。”断罢,差两人防送往卫州交管。正行次,撞着一汉,高叫:“杨指使!”杨志抬头一觑,却认得孙立指使。孙立惊怪:“哥怎恁地犯罪”。杨志把那卖刀杀人的事,一一说与孙立。道罢,各人自去。那孙立心中思忖:“杨志因等候我了,犯着这罪。当初结义之时,誓在厄难相救。”只得星夜奔归京师,报与李进义等知道杨志犯罪因由。这李进义同孙立商议,兄弟十一人往黄河岸上,等待杨志过来,将防送军人杀了,同往太行山落草为寇去也。
0x02 集成学习(ensemble learning)
1. 为什么要集成
在集成学习理论中,我们将弱学习器(或基础模型)称为「模型」,这些模型可用作设计更复杂模型的构件。在大多数情况下,这些基本模型本身的性能并不是非常好,这要么是因为它们具有较高的偏置(例如,低*度模型),要么是因为他们的方差太大导致鲁棒性不强(例如,高*度模型)。
比如各弱分类器间具有一定差异性(如不同的算法,或相同算法不同参数配置),这会导致生成的分类决策边界不同,也就是说它们在决策时会犯不同的错误。
集成方法的思想是通过将这些弱学习器的偏置和/或方差结合起来,从而创建一个「强学习器」(或「集成模型」),从而获得更好的性能。
集成学习是一种机器学习范式。其基本思想是:「三个臭皮匠顶个诸葛亮」。「团结就是力量」。「博采众长」。
在集成学习中,我们会训练多个模型(通常称为「弱学习器」)解决相同的问题,并将它们结合起来以获得更好的结果。最重要的假设是:当弱模型被正确组合时,我们可以得到更精确和/或更鲁棒的模型。
我们可以对集成学习的思想做一个概括。对于训练集数据,我们通过训练若干个个体学习器,通过一定的结合策略,就可以最终形成一个强学习器,以达到博采众长的目的。
集成学习法由训练数据构建一组基分类器,然后通过对每个基分类器的预测进行投票来进行分类
严格来说,集成学习并不算是一种分类器,而是一种分类器结合的方法。
如果把单个分类器比作一个决策者的话,集成学习的方法就相当于多个决策者共同进行一项决策。
2. 分类
对于集成学习的分类,常见两种分类方法:
分类1
个体学习器按照个体学习器之间是否存在依赖关系可以分为两类:
- 个体学习器之间存在强依赖关系,一系列个体学习器基本都需要串行生成,代表算法是boosting系列算法;
- 个体学习器之间不存在强依赖关系,一系列个体学习器可以并行生成,代表算法是bagging和随机森林(Random Forest)系列算法。
分类2
集成学习按照基本分类器之间的关系可以分为异态集成学习和同态集成学习。
异态集成学习是指弱分类器之间本身不同;
而同态集成学习是指弱分类器之间本身相同只是参数不同。
3. 主要问题
集成学习有两个主要的问题需要解决:
1)怎么训练每个算法?即如何得到若干个个体学习器。
2)怎么融合每个算法?即如何选择一种结合策略,将这些个体学习器集合成一个强学习器。
如何得到个体学习器?
主要从以下5个方面得到:
- 基本分类器本身的种类,即其构成算法不同;
- 对数据进行处理不同,比如说boosting,bagging,stacking,cross-validation,hold-out test;
- 对输入特征进行处理和选择;
- 对输出结果进行处理,比如说有的学者提出的纠错码;
- 引入随机扰动;
很重要的一点是:我们对弱学习器的选择应该和我们聚合这些模型的方式相一致。如果我们选择具有低偏置高方差的基础模型,我们应该使用一种倾向于减小方差的聚合方法;而如果我们选择具有低方差高偏置的基础模型,我们应该使用一种倾向于减小偏置的聚合方法。
如何选择一种结合策略
一旦选定了弱学习器,我们仍需要定义它们的拟合方式(在拟合当前模型时,要考虑之前模型的哪些信息?)和聚合方式(如何将当前的模型聚合到之前的模型中?)
这就引出了如何组合这些模型的问题。我们可以用三种主要的旨在组合弱学习器的「元算法」:
bagging,该方法通常考虑的是同质弱学习器,相互独立地并行学习这些弱学习器,并按照某种确定性的平均过程将它们组合起来。
boosting,该方法通常考虑的也是同质弱学习器。它以一种高度自适应的方法顺序地学习这些弱学习器(每个基础模型都依赖于前面的模型),并按照某种确定性的策略将它们组合起来。
stacking,该方法通常考虑的是异质弱学习器,并行地学习它们,并通过训练一个「元模型」将它们组合起来,根据不同弱模型的预测结果输出一个最终的预测结果。
非常粗略地说,我们可以说 bagging 的重点在于获得一个方差比其组成部分更小的集成模型,而 boosting 和 stacking 则将主要生成偏置比其组成部分更低的强模型(即使方差也可以被减小)。
我们可以看看在水浒传如何应用结合策略
梁山要打某座州府,究竟用什么办法呢? 听吴学究的嘛? 吴学究在生辰纲的表现实在不行,漏洞太多。打大名府等也基本就是一招:事先混进城里,然后里应外合。 所以还是应该集思广益,采用集成学习。 可以采用两个办法 办法1 神机军师朱武,混江龙李俊,智多星吴用,入云龙公孙胜,混世魔王樊瑞五个人都分别出一个主意,然后投票选出一个综合方案。 方法2 吴用先出一个主意,然后朱武针对这个主意做些调整补漏,给出了一个新主意。李俊再针对朱武的主意进行修补,给出了第三个主意...... 最后给出一个最终主意。 办法1 就是bagging方式的近似模拟 办法2 就是boosting方式的近似模拟
0x03 Bootstrap
首先需要介绍下Bootstrap,这个其实不属于集成学习,而是统计学的一种方法,属于集成学习的先驱。
Boostrap是靴子的带子的意思,名字来源于“pull up your own boostraps”,意思是通过拉靴子提高自己,本来的意思是不可能发生的事情,但后来发展成通过自己的努力让事情变得更好。放在组合分类器这里,意思就是通过分类器自己提高分类的性能。
Boostrap是一种抽样方法。
人们希望获取整体的全部信息,因为这样就可以做到“运筹帷幄”——整体都知道了,还有什么样本是我们不能掌握的吗?而实际上获取整体的信息是很难的,甚至是不可能的,所以才有了统计学中的“抽样”。也就是说,我们只能获取整体中的某些样本的信息,人们期望可以通过这些有限的样本信息来尽可能准确地估计总体,从而为决策提供依据。而Bootstrap方法认为,既然得到的样本是从总体中“抽取”的,那么为什么不可以把这些样本当做一个整体,从中进行有放回地再抽取呢?这种方法看似简单,而实际上却是十分有效的。
具体的方法是:
(1)采用重复抽样的方法每次从n个原始样本中抽取m个样本(m自己设定) (2)对于m个样本计算统计量 (3)重复步骤(1)(2)N次(N一般大于1000),这样就可以算出N个统计量 (4)计算这N个统计量的方差 比如说,我现在要对一些未知样本做分类,分类算法选取一种,比如SVM。我要估计的总体参数是准确率(accuracy)。对于n个原始样本,从步骤(1)开始,每次对抽取出的样本用SVM训练出一个模型,然后用这个模型对未知样本做分类,得到一个准确率。重复N次,可以得到N个准确率,然后对计算出的N个准确率做方差。 为什么要计算这N个统计量的方差而不是期望或者均值。方差表示的是一组数据与其平均水平的偏离程度,如果计算的方差值在一定范围之内,则表示这些数据的波动不是很大,那么他们的均值就可以用来估计总体的参数,而如果方差很大,这些样本统计量波动很大,那么总体的参数估计就不太准确?
看起来太简单了,所以提出者Efron给统计学*期刊投稿的时候被拒绝的理由--"太简单"。
Bootstrap只是提供了一种组合方法的思想,就是将基分类器的训练结果进行综合分析,而其它的名称如Bagging。Boosting是对组合方法的具体演绎。所以由Bootstrap方法开始,我们将导入到集成学习。
0x04 bagging方法
Bagging是boostrap aggregation的缩写,又叫自助聚集,是一种根据均匀概率分布从数据中重复抽样(有放回)的技术。至于为什么叫bootstrap aggregation,因为它抽取训练样本的时候采用的就是bootstrap的方法!
子训练样本集的大小和原始数据集相同。在构造每一个子分类器的训练样本时,由于是对原始数据集的有放回抽样,因此同一个训练样本集中可能出现多次同一个样本数据。
1. Bagging策略过程
- 从样本集中用Bootstrap采样选出n个训练样本(放回,因为别的分类器抽训练样本的时候也要用)
- 在所有属性上,用这n个样本训练分类器(CART or SVM or ...)
- 重复以上两步m次,就可以得到m个分类器(CART or SVM or ...)
- 将数据放在这m个分类器上跑,最后投票机制(多数服从少数)看到底分到哪一类(分类问题)
- 对于分类问题:由投票表决产生分类结果;对于回归问题:由n个模型预测结果的均值作为最后预测结果。(所有模型的重要性相同)
2. 总结一下bagging方法
- Bagging通过降低基分类器的方差,改善了泛化误差
- 其性能依赖于基分类器的稳定性;如果基分类器不稳定,bagging有助于降低训练数据的随机波动导致的误差;如果稳定,则集成分类器的误差主要由基分类器的偏倚引起
- 由于每个样本被选中的概率相同,因此bagging并不侧重于训练数据集中的任何特定实例
0x05 随机森林
随机森林是结合 Breimans 的 "Bootstrap aggregating" 想法和 Ho 的"random subspace method"以建造决策树的集合。即 Bagging + 决策树 = 随机森林。这是在共拥有m个特征的决策树中随机选择k个特征组成n棵决策树,再选择预测结果模式(如果是回归问题,选择平均值)。
1. Bagging特点如何应用
假设共有N个样本,M个特征,让我们结合Bagging的各个特点看看。
用Bootstrap采样 (Bagging特点应用) : 每棵树都有放回的随机抽取训练样本,这里抽取随机抽取 2/3 的样本作为训练集,再有放回的随机选取 m 个特征作为这棵树的分枝的依据。
随机:一个是随机选取样本 (Bagging特点应用) ,一个是随机选取特征。这样就构建出了一棵树。之后再在随机选取的特征中选取最优的特征。这样能够使得随机森林中的决策树都能够彼此不同,提升系统的多样性,从而提升分类性能。
选出优秀特征:随机森林真正厉害的地方不在于它通过多棵树进行综合得出最终结果,而是在于通过迭代使得森林中的树不断变得优秀 (森林中的树选用更好的特征进行分枝)。
迭代得到若干分类器 (Bagging特点应用):按照上面的步骤迭代多次,逐步去除相对较差的特征,每次都会生成新的森林,直到剩余的特征数为 m 为止。假设迭代 x 次,得到 x 个森林。
投票表决 (Bagging特点应用):用另外 1/3 样本 (也叫做袋外样本) 做为测试集,对迭代出的 x 个森林进行预测。预测出所有样本的结果之后与真实值进行比较,选择套外误差率最小的森林作为最终的随机森林模型。
2. 选出优秀特征
对于选出优秀特征这个,需要再做一下解释。
随机森林的思想是构建出优秀的树,优秀的树需要优秀的特征。那我们需要知道各个特征的重要程度。对于每一棵树都有个特征,要知道某个特征在这个树中是否起到了作用,可以随机改变这个特征的值,使得“这棵树中有没有这个特征都无所谓”,之后比较改变前后的测试集误差率,误差率的差距作为该特征在该树中的重要程度,测试集即为该树抽取样本之后剩余的样本(袋外样本)(由袋外样本做测试集造成的误差称为袋外误差)。
在一棵树中对于个特征都计算一次,就可以算法个特征在该树中的重要程度。我们可以计算出所有树中的特征在各自树中的重要程度。但这只能代表这些特征在树中的重要程度不能代表特征在整个森林中的重要程度。那我们怎么计算各特征在森林中的重要程度呢?每个特征在多棵数中出现,取这个特征值在多棵树中的重要程度的均值即为该特征在森林中的重要程度。这样就得到了所有特征在森林中的重要程度。将所有的特征按照重要程度排序,去除森林中重要程度低的部分特征,得到新的特征集。这时相当于我们回到了原点,这算是真正意义上完成了一次迭代。
0x06 Boosting 方法
"Boosting"的基本思想是通过某种方式使得每一轮基学习器在训练过程中更加关注上一轮学习错误的样本。
Boosting 方法和bagging 方法的工作思路是一样的:我们构建一系列模型,将它们聚合起来得到一个性能更好的强学习器。然而,与重点在于减小方差的 bagging 不同,boosting 着眼于以一种适应性很强的方式顺序拟合多个弱学习器:序列中每个模型在拟合的过程中,会更加重视那些序列中之前的模型处理地很糟糕的观测数据。
Boosting是一种迭代算法,针对同一个训练集训练不同的分类器(弱分类器),然后进行分类,对于分类正确的样本权值低,分类错误的样本权值高(通常是边界附近的样本),最后的分类器是很多弱分类器的线性叠加(加权组合),分类器相当简单。实际上就是一个简单的弱分类算法提升(boost)的过程。
每次样本加入的过程中,通常根据它们的上一轮的分类准确率给予不同的权重。数据通常会被重新加权,来强化对之前分类错误数据点的分类。
当boost运行每个模型时,它会跟踪哪些数据样本是最成功的,哪些不是。输出分类错误最多的数据集被赋予更重的权重。这些数据被认为更复杂,需要更多的迭代来正确地训练模型。
在实际分类阶段,boosting处理模型的方式也有所不同。在boosting中,模型的错误率被跟踪,因为更好的模型被赋予更好的权重。即 误差越小的弱分类器,权值越大。这样,当“投票”发生时,就像bagging一样,结果更好的模型对最终的输出有更的强拉动力。
0x07 AdaBoost
由于采用的损失函数不同,Boosting算法也因此有了不同的类型,AdaBoost就是损失函数为指数损失的Boosting算法。采用指数损失的原因是:每一轮最小化指数损失其实是在训练一个logistic regression模型,这样就逼近对数几率 (log odds)。
1. 两个主要问题
Boosting算法是将“弱学习算法“提升为“强学习算法”的过程。采用Boosting思想实现的算法,需要解决两个主要问题。
- 如何选择一组有不同优缺点的弱学习器,使得它们可以相互弥补不足。
- 如何组合弱学习器的输出以获得整体的更好的决策表现。
和这两个问题对应的是加法模型和前向分步算法。
- 加法模型就是说强分类器由一系列弱分类器线性相加而成。
- 前向分步就是说在训练过程中,下一轮迭代产生的分类器是在上一轮的基础上训练得来的。
前向分布算法说:“我可以提供一套框架,不管基函数和损失函数是什么形式,只要你的模型是加法模型,就可以按照我的框架的指导,去求解。” 也就是说,前向分步算法提供了一种学习加法模型的普遍性方法,不同形式的基函数、不同形式的损失函数都可以用这种普遍性方法去求出加法模型的最优化参数,它是一种元算法。
前向分步算法的思路是:加法模型中一共有M个基函数以及与之相应的M个系数,可以从前往后,每次学习一个基函数及其系数。
2. AdaBoost的解决方案
选择弱学习器
为了解决第一个问题,AdaBoost的办法是:每轮结束时改变样本的权值。这样AdaBoost让每一个新加入的弱学习器都体现出一些新的数据中的模式。
为了实现这一点,AdaBoost为每一个训练样本维护一个权重分布。即对任意一个样本 xi 都有一个分布 D(i) 与之对应,以表示这个样本的重要性。
当衡量弱学习器的表现时,AdaBoost会考虑每个样本的权重。权重较大的误分类样本会比权重较小的误分类样本贡献更大的训练错误率。为了获得更小的加权错误率,弱分类器必须更多的聚焦于高权重的样本,保证对它们准确的预测。
也可以这么理解提高错误点的权值的意义:当"下一次分类器"再次分错了这些点之后,会提高"下一次分类器"整体的错误率,这样就导致 "下一次分类器" 自己的 权重 变的很小,最终导致这个分类器在整个混合分类器的权值变低。
通过修改样本的权重 D(i) ,也就改变了样本的概率分布,将关注点放在被错误分类的样本上,减小上一轮被正确分类的样本权值,提高那些被错误分类的样本权值。这样就可以引导弱学习器学习训练样本的不同部分。
训练时候,我们是利用前一轮迭代弱学习器的误差率来更新训练集的权重,这样一轮轮的迭代下去。
组合弱学习器
现在,我们获得了一组已训练的拥有不同优缺点的弱学习器,如何有效的组合它们,使得相互优势互补来产生更准确的整体预测效果?这就是第二个问题。
对于第二个问题,AdaBoost采用加权多数表决的方法,加大分类误差率小的弱分类器的权重,减小分类误差率大的弱分类器的权重。这个很好理解,正确率高分得好的弱分类器在强分类器中当然应该有较大的发言权。
每一个弱学习器是用不同的权重分布训练出来的,我们可以看做给不同的弱学习器分配了不同的任务,每个弱学习器都尽力完成给定的任务。直觉上看,当我们要把每个弱学习器的判断组合到最终的预测结果中时,如果弱学习器在之前的任务中表现优异,我们会更多的相信它,相反,如果弱学习器在之前的任务中表现较差,我们就更少的相信它。
换句话说,我们会加权地组合弱学习器,给每个弱学习器赋予一个表示可信程度的值 wi ,这个值取决于它在被分配的任务中的表现,表现越好 wi 越大,反之越小。
0x08 从水浒传中派生一个例子看如何使用集成学习
我们找一个例子来详细看看如何使用集成学习这个概念。现在梁山需要投票来决定是否接受招安。宋江说了,我们要科学*的进行决策,所以我们采用最新科技:集成学习。
1. Bagging
首先考虑的是bagging方法
如果采用了bagging方法。样本有放回,而且投票可以并行。 每次抽取 5 个人投票是否接受。 第一次抽出 徐宁 、索超 、朱仝 、花荣、杨志。则 5 票都是 接受招安 第二次抽出 鲁智深,武松,朱贵,刘唐,李逵。则 5 票都是 反对招安 第三次抽出 徐宁 、索超,鲁智深,武松,阮小二。则 2 票接受,3 票反对,则这次样本是 反对招安。 最后综合三次的结果是:反对招安。 现在情况已经对招安不利了,如果再并行进行投票,那么对结果就更无法估计。 这样的话,对于是否招安就真的是梁山群体*评议,公明哥哥和吴学究没办法后台黑箱操控了。
如果宋江想黑箱操作,他就不能采用bagging方法,而是需要选择boosting,因为这个算法的训练集的选择不是独立的,每一次选择的训练集都依赖于上一次学习的结果,所以利于宋江暗箱操纵。
2. Boosting
于是宋江决定采取Boosting方法,大家可以看看宋江如何使用boosting一步一步调整bias以达到最好拟合 “接受招安” 这个最终期望的。
"Boosting"的基本思想是通过某种方式使得每一轮基学习器在训练过程中更加关注上一轮学习错误的样本 所以我们有两套方案。 方案一:每次每次剔除异常数值 方案二:每次调整异常数值权重 ----------------------------------------------------------------- 方案一:每次剔除异常数值,这种适合宋江的样本全部无法控制的情况 迭代1 样本:鲁智深,武松,朱贵,刘唐,李逵。则 5 票都是 反对招安 宋江说:出家人与世无争,就不要参与投票了,改换为徐宁,索超两位兄弟。 宋江跟踪错误率,因为本次弱分类器误差太大,所以本次权重降低。 迭代2 样本:徐宁,索超,朱贵,刘唐,李逵。则 2 票接受,3 票反对,则这次样本是 反对招安 宋江说:铁牛兄弟不懂事,乱投票,来人乱棒打出去,换成杨志兄弟。 宋江跟踪错误率,因为本次弱分类器误差太大,所以本次权重降低。 迭代3 样本:徐宁,索超,朱贵,刘唐,杨志。则 3 票接受,2 票反对,则这次样本是 接受招安 宋江说:朱贵兄弟平时总是打点酒店,对于时政缺少认知,换成关胜兄弟。 宋江跟踪错误率,因为本次弱分类器误差较小,所以本次权重增加。 迭代4 样本:徐宁,索超,关胜,刘唐,杨志。则 4 票接受,1 票反对,则这次样本是 接受招安 宋江说:刘唐兄弟头发染色了,不利用梁山形象,换成花荣兄弟。 宋江跟踪错误率,因为本次弱分类器误差较小,所以本次权重增加。 迭代5 样本:徐宁,索超,关胜,花荣,杨志。则 5 票接受,则这次样本是 接受招安。 宋江跟踪错误率,因为本次弱分类器没有误差,所以本次权重增加。 即误差率小的分类器,在最终分类器中的重要程度大。 最后综合 5 次选举结果,梁山最后决定接受招安。 ----------------------------------------------------------------- 方案二:每次降低异常数值权重,这种适合样本中有宋江可以控制的头领 迭代1 样本:武松,花荣,朱贵,杨志,李逵。则 2 票接受,3 票反对,则本次结论是 反对招安 宋江说:出家人与世无争,降低武松权重为 1/2 。 宋江跟踪错误率,因为本次弱分类器误差太大,所以本次权重降低。 迭代2 样本:武松(权重1/2),花荣,朱贵,杨志,李逵。则 2 票接受,2又1/2 票反对,则这次结论是 反对招安 宋江说:铁牛兄弟不懂事,乱投票,降低李逵权重为 1/2。 宋江跟踪错误率,因为本次弱分类器误差太大,所以本次权重降低。 迭代3 样本:武松(权重1/2),花荣,朱贵,杨志,李逵(权重1/2)。则 2 票接受,2 票反对,则这次结论是 无结论 宋江说:朱贵兄弟平时打点酒店,对于时政缺少认知,降低朱贵权重为 1/2。 宋江跟踪错误率,因为本次弱分类器无结论,所以本次权重为零。 迭代4 样本:武松(权重1/2),花荣,朱贵(权重1/2),杨志,李逵(权重1/2)。则 2 票接受,1又1/2 票反对,则这次结论是 接受招安 宋江说:这次好,花荣做过知寨,有见地,增加花荣权重。继续投票。 宋江跟踪错误率,因为本次弱分类器误差较小,所以本次权重增加。 迭代5 样本:武松(权重1/2),花荣(权重2),朱贵(权重1/2),杨志,李逵(权重1/2)。则 3 票接受,1又1/2 票反对,则这次结论是 接受招安 宋江说:这次好,杨志做过制使,有见地,增加杨志权重。继续投票。 宋江跟踪错误率,因为本次弱分类器误差较小,所以本次权重增加。 迭代6 样本:武松(权重1/2),花荣(权重2),朱贵(权重1/2),杨志(权重2),李逵(权重1/2)。则 4 票接受,1又1/2 票反对,则这次结论是 接受招安 宋江跟踪错误率,因为本次弱分类器误差较小,所以本次权重增加。 最后综合 6 次选举结果,梁山最后决定接受招安。 ----------------------------------------------------------------- 这里能看出来,Boosting算法对于样本的异常值十分敏感,因为Boosting算法中每个分类器的输入都依赖于前一个分类器的分类结果,会导致误差呈指数级累积。 宋江每一次选择的训练集都依赖于上一次学习的结果。每次剔除异常数值 或者 调整异常数值权重。(在实际boosting算法中是增加异常数值的权重)。 宋江也根据每一次训练的训练误差得到该次预测函数的权重。
3. 为什么说bagging是减少variance,而boosting是减少bias?
bias描述的是根据样本拟合出的模型的输出预测结果的期望与样本真实结果的差距,简单讲,就是在样本上拟合的好不好。要想在bias上表现好,low bias,就得复杂化模型,增加模型的参数,但这样容易过拟合 (overfitting)。
varience描述的是样本上训练出来的模型在测试集上的表现,要想在variance上表现好,low varience,就要简化模型,减少模型的参数,但这样容易欠拟合(unfitting)。
从我们例子能看出来。 1. Bagging bagging没有针对性的对分类器进行调整,只是单纯的增加样本数量和采样次数,以此来让平均值逼近结果。 所以bagging的基模型应该本身就是强模型(偏差低方差高)。 所以,bagging应该是对许多强(甚至过强)的分类器求平均。在这里,每个单独的分类器的bias都是低的,平均之后bias依然低;而每个单独的分类器都强到可能产生overfitting的程度,也就是variance高,求平均的操作起到的作用就是降低这个variance。 2. Boosting boosting就是为了让每一次分类器的结果逐渐接近期望目标。即宋江期望一步一步的boosting到最终接受招安这个结果。这样才能在样本上最好拟合“招安”这个期望结果,从最初的“拒绝招安”这个high bias过渡到“接受招安”这个期望结果。 boosting是把许多弱的分类器组合成一个强的分类器。弱的分类器bias高,而强的分类器bias低,所以说boosting起到了降低bias的作用。variance不是boosting的主要考虑因素。 Boosting 是迭代算法,每一次迭代都根据上一次迭代的预测结果对样本进行加权,所以随着迭代不断进行,误差会越来越小,所以模型的 bias 会不断降低。这种算法无法并行,例子比如Adaptive Boosting。
4. Bagging vs Boosting
由此我们可以对比Bagging和Boosting:
- 样本选择上:Bagging采用的是Bootstrap随机有放回抽样,各训练集是独立的;而boosting训练集的选择不是独立的,每一次选择的训练集都依赖于上一次学习的结果。如果训练集不变,那么改变的只是每一个样本的权重。
- 样本权重:Bagging使用的是均匀取样,每个样本权重相等;Boosting根据错误率调整样本权重,错误率越大的样本权重越大。
- 预测函数:Bagging所有的预测函数的权重相等;Boosting根据每一次训练的训练误差得到该次预测函数的权重,误差越小的预测函数其权重越大。
- 并行计算:Bagging各个预测函数可以并行生成;Boosting各个预测函数必须按顺序迭代生成。
0x09 随机森林代码
有兴趣的同学可以用代码来论证下Bagging。这里给出两份代码。
其一出自 https://blog.csdn.net/colourful_sky/article/details/82082854
# -*- coding: utf-8 -*- """ Created on Thu Jul 26 16:38:18 2018 @author: aoanng """ import csv from random import seed from random import randrange from math import sqrt def loadCSV(filename):#加载数据,一行行的存入列表 dataSet = [] with open(filename, 'r') as file: csvReader = csv.reader(file) for line in csvReader: dataSet.append(line) return dataSet # 除了标签列,其他列都转换为float类型 def column_to_float(dataSet): featLen = len(dataSet[0]) - 1 for data in dataSet: for column in range(featLen): data[column] = float(data[column].strip()) # 将数据集随机分成N块,方便交叉验证,其中一块是测试集,其他四块是训练集 def spiltDataSet(dataSet, n_folds): fold_size = int(len(dataSet) / n_folds) dataSet_copy = list(dataSet) dataSet_spilt = [] for i in range(n_folds): fold = [] while len(fold) < fold_size: # 这里不能用if,if只是在第一次判断时起作用,while执行循环,直到条件不成立 index = randrange(len(dataSet_copy)) fold.append(dataSet_copy.pop(index)) # pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。 dataSet_spilt.append(fold) return dataSet_spilt # 构造数据子集 def get_subsample(dataSet, ratio): subdataSet = [] lenSubdata = round(len(dataSet) * ratio)#返回浮点数 while len(subdataSet) < lenSubdata: index = randrange(len(dataSet) - 1) subdataSet.append(dataSet[index]) # print len(subdataSet) return subdataSet # 分割数据集 def data_spilt(dataSet, index, value): left = [] right = [] for row in dataSet: if row[index] < value: left.append(row) else: right.append(row) return left, right # 计算分割代价 def spilt_loss(left, right, class_values): loss = 0.0 for class_value in class_values: left_size = len(left) if left_size != 0: # 防止除数为零 prop = [row[-1] for row in left].count(class_value) / float(left_size) loss += (prop * (1.0 - prop)) right_size = len(right) if right_size != 0: prop = [row[-1] for row in right].count(class_value) / float(right_size) loss += (prop * (1.0 - prop)) return loss # 选取任意的n个特征,在这n个特征中,选取分割时的最优特征 def get_best_spilt(dataSet, n_features): features = [] class_values = list(set(row[-1] for row in dataSet)) b_index, b_value, b_loss, b_left, b_right = 999, 999, 999, None, None while len(features) < n_features: index = randrange(len(dataSet[0]) - 1) if index not in features: features.append(index) # print 'features:',features for index in features:#找到列的最适合做节点的索引,(损失最小) for row in dataSet: left, right = data_spilt(dataSet, index, row[index])#以它为节点的,左右分支 loss = spilt_loss(left, right, class_values) if loss < b_loss:#寻找最小分割代价 b_index, b_value, b_loss, b_left, b_right = index, row[index], loss, left, right # print b_loss # print type(b_index) return {'index': b_index, 'value': b_value, 'left': b_left, 'right': b_right} # 决定输出标签 def decide_label(data): output = [row[-1] for row in data] return max(set(output), key=output.count) # 子分割,不断地构建叶节点的过程 def sub_spilt(root, n_features, max_depth, min_size, depth): left = root['left'] # print left right = root['right'] del (root['left']) del (root['right']) # print depth if not left or not right: root['left'] = root['right'] = decide_label(left + right) # print 'testing' return if depth > max_depth: root['left'] = decide_label(left) root['right'] = decide_label(right) return if len(left) < min_size: root['left'] = decide_label(left) else: root['left'] = get_best_spilt(left, n_features) # print 'testing_left' sub_spilt(root['left'], n_features, max_depth, min_size, depth + 1) if len(right) < min_size: root['right'] = decide_label(right) else: root['right'] = get_best_spilt(right, n_features) # print 'testing_right' sub_spilt(root['right'], n_features, max_depth, min_size, depth + 1) # 构造决策树 def build_tree(dataSet, n_features, max_depth, min_size): root = get_best_spilt(dataSet, n_features) sub_spilt(root, n_features, max_depth, min_size, 1) return root # 预测测试集结果 def predict(tree, row): predictions = [] if row[tree['index']] < tree['value']: if isinstance(tree['left'], dict): return predict(tree['left'], row) else: return tree['left'] else: if isinstance(tree['right'], dict): return predict(tree['right'], row) else: return tree['right'] # predictions=set(predictions) def bagging_predict(trees, row): predictions = [predict(tree, row) for tree in trees] return max(set(predictions), key=predictions.count) # 创建随机森林 def random_forest(train, test, ratio, n_feature, max_depth, min_size, n_trees): trees = [] for i in range(n_trees): train = get_subsample(train, ratio)#从切割的数据集中选取子集 tree = build_tree(train, n_features, max_depth, min_size) # print 'tree %d: '%i,tree trees.append(tree) # predict_values = [predict(trees,row) for row in test] predict_values = [bagging_predict(trees, row) for row in test] return predict_values # 计算准确率 def accuracy(predict_values, actual): correct = 0 for i in range(len(actual)): if actual[i] == predict_values[i]: correct += 1 return correct / float(len(actual)) if __name__ == '__main__': seed(1) dataSet = loadCSV('sonar-all-data.csv') column_to_float(dataSet)#dataSet n_folds = 5 max_depth = 15 min_size = 1 ratio = 1.0 # n_features=sqrt(len(dataSet)-1) n_features = 15 n_trees = 10 folds = spiltDataSet(dataSet, n_folds)#先是切割数据集 scores = [] for fold in folds: train_set = folds[ :] # 此处不能简单地用train_set=folds,这样用属于引用,那么当train_set的值改变的时候,folds的值也会改变,所以要用复制的形式。(L[:])能够复制序列,D.copy() 能够复制字典,list能够生成拷贝 list(L) train_set.remove(fold)#选好训练集 # print len(folds) train_set = sum(train_set, []) # 将多个fold列表组合成一个train_set列表 # print len(train_set) test_set = [] for row in fold: row_copy = list(row) row_copy[-1] = None test_set.append(row_copy) # for row in test_set: # print row[-1] actual = [row[-1] for row in fold] predict_values = random_forest(train_set, test_set, ratio, n_features, max_depth, min_size, n_trees) accur = accuracy(predict_values, actual) scores.append(accur) print ('Trees is %d' % n_trees) print ('scores:%s' % scores) print ('mean score:%s' % (sum(scores) / float(len(scores))))
其二出自https://github.com/zhaoxingfeng/RandomForest
# -*- coding: utf-8 -*- """ @Env: Python2.7 @Time: 2019/10/24 13:31 @Author: zhaoxingfeng @Function:Random Forest(RF),随机森林二分类 @Version: V1.2 参考文献: [1] UCI. wine[DB/OL].https://archive.ics.uci.edu/ml/machine-learning-databases/wine. """ import pandas as pd import numpy as np import random import math import collections from sklearn.externals.joblib import Parallel, delayed class Tree(object): """定义一棵决策树""" def __init__(self): self.split_feature = None self.split_value = None self.leaf_value = None self.tree_left = None self.tree_right = None def calc_predict_value(self, dataset): """通过递归决策树找到样本所属叶子节点""" if self.leaf_value is not None: return self.leaf_value elif dataset[self.split_feature] <= self.split_value: return self.tree_left.calc_predict_value(dataset) else: return self.tree_right.calc_predict_value(dataset) def describe_tree(self): """以json形式打印决策树,方便查看树结构""" if not self.tree_left and not self.tree_right: leaf_info = "{leaf_value:" + str(self.leaf_value) + "}" return leaf_info left_info = self.tree_left.describe_tree() right_info = self.tree_right.describe_tree() tree_structure = "{split_feature:" + str(self.split_feature) + \ ",split_value:" + str(self.split_value) + \ ",left_tree:" + left_info + \ ",right_tree:" + right_info + "}" return tree_structure class RandomForestClassifier(object): def __init__(self, n_estimators=10, max_depth=-1, min_samples_split=2, min_samples_leaf=1, min_split_gain=0.0, colsample_bytree=None, subsample=0.8, random_state=None): """ 随机森林参数 ---------- n_estimators: 树数量 max_depth: 树深度,-1表示不限制深度 min_samples_split: 节点分裂所需的最小样本数量,小于该值节点终止分裂 min_samples_leaf: 叶子节点最少样本数量,小于该值叶子被合并 min_split_gain: 分裂所需的最小增益,小于该值节点终止分裂 colsample_bytree: 列采样设置,可取[sqrt、log2]。sqrt表示随机选择sqrt(n_features)个特征, log2表示随机选择log(n_features)个特征,设置为其他则不进行列采样 subsample: 行采样比例 random_state: 随机种子,设置之后每次生成的n_estimators个样本集不会变,确保实验可重复 """ self.n_estimators = n_estimators self.max_depth = max_depth if max_depth != -1 else float('inf') self.min_samples_split = min_samples_split self.min_samples_leaf = min_samples_leaf self.min_split_gain = min_split_gain self.colsample_bytree = colsample_bytree self.subsample = subsample self.random_state = random_state self.trees = None self.feature_importances_ = dict() def fit(self, dataset, targets): """模型训练入口""" assert targets.unique().__len__() == 2, "There must be two class for targets!" targets = targets.to_frame(name='label') if self.random_state: random.seed(self.random_state) random_state_stages = random.sample(range(self.n_estimators), self.n_estimators) # 两种列采样方式 if self.colsample_bytree == "sqrt": self.colsample_bytree = int(len(dataset.columns) ** 0.5) elif self.colsample_bytree == "log2": self.colsample_bytree = int(math.log(len(dataset.columns))) else: self.colsample_bytree = len(dataset.columns) # 并行建立多棵决策树 self.trees = Parallel(n_jobs=-1, verbose=0, backend="threading")( delayed(self._parallel_build_trees)(dataset, targets, random_state) for random_state in random_state_stages) def _parallel_build_trees(self, dataset, targets, random_state): """bootstrap有放回抽样生成训练样本集,建立决策树""" subcol_index = random.sample(dataset.columns.tolist(), self.colsample_bytree) dataset_stage = dataset.sample(n=int(self.subsample * len(dataset)), replace=True, random_state=random_state).reset_index(drop=True) dataset_stage = dataset_stage.loc[:, subcol_index] targets_stage = targets.sample(n=int(self.subsample * len(dataset)), replace=True, random_state=random_state).reset_index(drop=True) tree = self._build_single_tree(dataset_stage, targets_stage, depth=0) print(tree.describe_tree()) return tree def _build_single_tree(self, dataset, targets, depth): """递归建立决策树""" # 如果该节点的类别全都一样/样本小于分裂所需最小样本数量,则选取出现次数最多的类别。终止分裂 if len(targets['label'].unique()) <= 1 or dataset.__len__() <= self.min_samples_split: tree = Tree() tree.leaf_value = self.calc_leaf_value(targets['label']) return tree if depth < self.max_depth: best_split_feature, best_split_value, best_split_gain = self.choose_best_feature(dataset, targets) left_dataset, right_dataset, left_targets, right_targets = \ self.split_dataset(dataset, targets, best_split_feature, best_split_value) tree = Tree() # 如果父节点分裂后,左叶子节点/右叶子节点样本小于设置的叶子节点最小样本数量,则该父节点终止分裂 if left_dataset.__len__() <= self.min_samples_leaf or \ right_dataset.__len__() <= self.min_samples_leaf or \ best_split_gain <= self.min_split_gain: tree.leaf_value = self.calc_leaf_value(targets['label']) return tree else: # 如果分裂的时候用到该特征,则该特征的importance加1 self.feature_importances_[best_split_feature] = \ self.feature_importances_.get(best_split_feature, 0) + 1 tree.split_feature = best_split_feature tree.split_value = best_split_value tree.tree_left = self._build_single_tree(left_dataset, left_targets, depth+1) tree.tree_right = self._build_single_tree(right_dataset, right_targets, depth+1) return tree # 如果树的深度超过预设值,则终止分裂 else: tree = Tree() tree.leaf_value = self.calc_leaf_value(targets['label']) return tree def choose_best_feature(self, dataset, targets): """寻找最好的数据集划分方式,找到最优分裂特征、分裂阈值、分裂增益""" best_split_gain = 1 best_split_feature = None best_split_value = None for feature in dataset.columns: if dataset[feature].unique().__len__() <= 100: unique_values = sorted(dataset[feature].unique().tolist()) # 如果该维度特征取值太多,则选择100个百分位值作为待选分裂阈值 else: unique_values = np.unique([np.percentile(dataset[feature], x) for x in np.linspace(0, 100, 100)]) # 对可能的分裂阈值求分裂增益,选取增益最大的阈值 for split_value in unique_values: left_targets = targets[dataset[feature] split_value] split_gain = self.calc_gini(left_targets['label'], right_targets['label']) if split_gain < best_split_gain: best_split_feature = feature best_split_value = split_value best_split_gain = split_gain return best_split_feature, best_split_value, best_split_gain @staticmethod def calc_leaf_value(targets): """选择样本中出现次数最多的类别作为叶子节点取值""" label_counts = collections.Counter(targets) major_label = max(zip(label_counts.values(), label_counts.keys())) return major_label[1] @staticmethod def calc_gini(left_targets, right_targets): """分类树采用基尼指数作为指标来选择最优分裂点""" split_gain = 0 for targets in [left_targets, right_targets]: gini = 1 # 统计每个类别有多少样本,然后计算gini label_counts = collections.Counter(targets) for key in label_counts: prob = label_counts[key] * 1.0 / len(targets) gini -= prob ** 2 split_gain += len(targets) * 1.0 / (len(left_targets) + len(right_targets)) * gini return split_gain @staticmethod def split_dataset(dataset, targets, split_feature, split_value): """根据特征和阈值将样本划分成左右两份,左边小于等于阈值,右边大于阈值""" left_dataset = dataset[dataset[split_feature] <= split_value] left_targets = targets[dataset[split_feature] split_value] right_targets = targets[dataset[split_feature] > split_value] return left_dataset, right_dataset, left_targets, right_targets def predict(self, dataset): """输入样本,预测所属类别""" res = [] for _, row in dataset.iterrows(): pred_list = [] # 统计每棵树的预测结果,选取出现次数最多的结果作为最终类别 for tree in self.trees: pred_list.append(tree.calc_predict_value(row)) pred_label_counts = collections.Counter(pred_list) pred_label = max(zip(pred_label_counts.values(), pred_label_counts.keys())) res.append(pred_label[1]) return np.array(res) if __name__ == '__main__': df = pd.read_csv("source/wine.txt") df = df[df['label'].isin([1, 2])].sample(frac=1, random_state=66).reset_index(drop=True) clf = RandomForestClassifier(n_estimators=5, max_depth=5, min_samples_split=6, min_samples_leaf=2, min_split_gain=0.0, colsample_bytree="sqrt", subsample=0.8, random_state=66) train_count = int(0.7 * len(df)) feature_list = ["Alcohol", "Malic acid", "Ash", "Alcalinity of ash", "Magnesium", "Total phenols", "Flavanoids", "Nonflavanoid phenols", "Proanthocyanins", "Color intensity", "Hue", "OD280/OD315 of diluted wines", "Proline"] clf.fit(df.loc[:train_count, feature_list], df.loc[:train_count, 'label']) from sklearn import metrics print(metrics.accuracy_score(df.loc[:train_count, 'label'], clf.predict(df.loc[:train_count, feature_list]))) print(metrics.accuracy_score(df.loc[train_count:, 'label'], clf.predict(df.loc[train_count:, feature_list])))
0x10 参考
统计学中“最简单”的Bootstrap方法介绍及其应用 https://blog.csdn.net/SunJW_2017/article/details/79160369
分类器组合方法Bootstrap, Boosting, Bagging, 随机森林(一)https://blog.csdn.net/zjsghww/article/details/51591009
集成学习算法与Boosting算法原理 https://baijiahao.baidu.com/s?id=1619984901377076475
xgboost的原理没你想像的那么难 https://www.jianshu.com/p/7467e616f227
为什么没有人把 boosting 的思路应用在深度学习上? https://www.zhihu.com/question/53257850
集成学习算法与Boosting算法原理 https://baijiahao.baidu.com/s?id=1619984901377076475
集成学习法之bagging方法和boosting方法 https://blog.csdn.net/qq_30189255/article/details/51532442
总结:Bootstrap(自助法),Bagging,Boosting(提升) https://www.jianshu.com/p/708dff71df3a
https://blog.csdn.net/zjsghww/article/details/51591009
集成学习(Ensemble Learning) https://blog.csdn.net/wydbyxr/article/details/82259728
Adaboost入门教程——最通俗易懂的原理介绍 http://www.uml.org.cn/sjjmwj/2019030721.asp
Boosting和Bagging: 如何开发一个鲁棒的机器学习算法 https://ai.51cto.com/art/201906/598160.htm
为什么bagging降低方差,boosting降低偏差?https://blog.csdn.net/sinat_25394043/article/details/104119469
bagging与boosting两种集成模型的偏差bias以及方差variance 的理解 https://blog.csdn.net/shenxiaoming77/article/details/53894973
用通俗易懂的方式剖析随机森林 https://blog.csdn.net/cg896406166/article/details/83796557
python实现随机森林 https://blog.csdn.net/colourful_sky/article/details/82082854
https://github.com/zhaoxingfeng/RandomForest
数据分析(工具篇)——Boosting https://zhuanlan.zhihu.com/p/26215100
知识篇——基于AdaBoost的分类问题 https://www.jianshu.com/p/a6426f4c4e64
数据挖掘面试题之梯度提升树 https://www.jianshu.com/p/0e5ccc88d2cb