实用机器学习

978-7-115-44646-6
作者: 孙亮 黄倩
译者:
编辑: 杨海玲

图书目录:

详情

本书围绕实际数据分析的流程展开,着重介绍数据探索、数据预处理和常用的机器学习算法模型。本书从解决实际问题的角度出发,介绍回归算法、分类算法、推荐算法、排序算法和集成学习算法。本书的最大特色就是贴近工程实践。首先,本书仅侧重介绍当前工业界最常用的机器学习算法,而不追求知识本身的覆盖面;其次,本书在介绍每类机器学习算法时,力求通俗易懂地阐述算法思想,而不追求理论的深度,让读者借助代码获得直观的体验。

图书摘要

版权信息

书名:实用机器学习

ISBN:978-7-115-44646-6

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著     孙 亮 黄 倩

  责任编辑 杨海玲

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


大数据时代为机器学习的应用提供了广阔的空间,各行各业涉及数据分析的工作都需要使用机器学习算法。本书围绕实际数据分析的流程展开,着重介绍数据探索、数据预处理和常用的机器学习算法模型。本书从解决实际问题的角度出发,介绍回归算法、分类算法、推荐算法、排序算法和集成学习算法。在介绍每种机器学习算法模型时,书中不但阐述基本原理,而且讨论模型的评价与选择。为方便读者学习各种算法,本书介绍了R语言中相应的软件包并给出了示例程序。

本书的最大特色就是贴近工程实践。首先,本书仅侧重介绍当前工业界最常用的机器学习算法,而不追求知识内容的覆盖面;其次,本书在介绍每类机器学习算法时,力求通俗易懂地阐述算法思想,而不追求理论的深度,让读者借助代码获得直观的体验。

本书适合需要应用机器学习算法解决实际问题的工程技术人员阅读,也可作为相关专业高年级本科生或研究生的入门教材或课外读物。


机器学习是人工智能领域最成功、发展最迅速的分支之一。然而多年来,除了人机对弈之外,成功的机器学习应用对普通百姓来说似乎始终显得遥不可及。大数据时代的来临对机器学习提出了新的挑战,同时也为机器学习打开了一扇通向实用化舞台的大门。近年来,源于实际需求的海量数据集不断出现,一方面为技术交流和算法验证提供了有力的基础数据支撑,另一方面也有助于促进相关科学研究更贴近我们的日常生活。例如,在滴滴,我们正致力于结合机器学习技术和大数据技术来预测各地用户需求,并进行实时的运力调度和订单分配,不断为广大司机和用户提供更满意的服务。可以说,机器学习技术和大数据技术的结合正在显著地拉近科研人员和普通百姓的距离,正在显著地改变我们的生产方式和生活方式。

2016年8月,我在中国人工智能大会上担任“机器学习的明天”专题论坛联席主席时曾指出,人类很多难题的解决都离不开大数据和人工智能的结合。今天,我欣喜地看到,本书两位作者的合作恰恰体现了实用机器学习的这一重要发展趋势。两位作者长期在学术界和工业界工作,其中孙亮博士是我的学生,他从2006年开始就一直从事降维算法、稀疏学习、数值最优化算法等方向的机器学习研究,其快速降维算法研究曾获得机器学习领域顶级会议SIGKDD的最佳论文提名奖;黄倩博士是高文院士的学生,他从2004年开始就一直从事视频大数据的压缩和增强显示研究,其研究成果曾在视频处理领域旗舰期刊IEEE T-CSVT发表并在工业界获得实际应用。两位作者博士毕业后均有在万人以上规模企业的相关研发经历,接触过各类实际问题和数据,对机器学习如何在实际中应用有着深刻的理解和成功的经验。

这本书不厚,但却覆盖了用机器学习技术解决实际问题的主要步骤和常用算法。两位作者从实际应用出发,介绍了数据探索、数据预处理、算法应用、性能评价等具体内容,并深入浅出地介绍了模型复杂度、损失函数等机器学习领域的基本概念。由于集成学习在实际中应用较为广泛,因此本书专列一章加以讨论。考虑到实践中大家更关注的是如何选择和使用算法,两位作者还使用R语言软件包来引导读者实际操作。与市面上对机器学习作一般性介绍的书籍相比,本书介绍的算法稍稍复杂一些,但也更加实用,书中讨论的内容正是实际应用机器学习解决问题时所需要掌握的内容。对于广大业界爱好者和相关专业研究生来说,这是一本理想的入门读物和参考书,因此我非常乐意向大家推荐本书。

叶杰平

滴滴研究院副院长,密歇根大学终身教授

2016年12月


2016年10月3日,我从宿州匆匆赶回上海,试图从连日飨宴中恢复精力,等待5日凌晨谷歌公司的Pixel发布会。业界预测这将是一个划时代的发布会,标志着“Mobile First”向“AI First”的转型。这一场发布会,让我难掩激动地在朋友圈中留下了洋洋洒洒但未必成熟的千字技术点评。

我为什么会如此期待这场盛会,要从十多年前说起。那时我还是一个学生,学着和IT八竿子打不着的天文学。在本科结束后,响应高中读《时间简史》时内心深处的召唤,我继续学了天文学。经历短暂的欢愉之后,我终于意识到这条路的艰辛,我是不可能做这行了。我开始思考,有没有一条路能让我既接近最前沿的理论,又有足够好的实践环境呢?我在看了《AI》(《人工智能》)这部电影之后被震撼得天旋地转——人工智能可以创造出如此的美好,人工智能可以引发无穷的深思。

我开始去选修《机器人》的课程,开始去听张学工老师的《模式识别》课,开始把自己的知识往这个方向靠。所幸因为玻尔兹曼,这两个专业还是有些联系的。在一次选假期小班课程的时候,我选了一个SVM的讲座,后来当孙亮和我说他也听过这堂课的时候,我才意识到原来高人就在身边,只是我一直不曾留意。我曾看到过孙亮床位下一箱一箱的计算机书,大多与机器学习有关,只是人的神经系统是很奇妙的,当你不关注的时候,这些事情会自动在你的世界被屏蔽;当你关注的时候,这个概念会在极短的时间以各种方式在你面前展现。不久后我认识了同样以机器学习见长的黄倩,那时在周末大家都会找时间一起聚聚,当然话题大多是关于计算机的发展方向和算法的,然后我就作为旁听者被他们带进了门。

孙亮硕士毕业后去美国继续读机器学习的博士,最后到微软公司从事他擅长的机器学习和数据科学的研究;黄倩在高文院士指导下硕博毕业,最终回到高校任教,带领学子探索最前沿的算法。这些年他们一直躬耕一线,从未中断,我也在辗转中断断续续地做着数据相关的工作,最终转到Hadoop/Spark/TensorFlow开源平台。这几年大数据如火如荼,机器学习热浪汹涌,AlphaGo的成功进一步激励了业界,TensorFlow的开源、“AI First”的概念终于在坎坎坷坷10年之后开始盛行,高校的学子和一线的工程师开始被这个全新的世界吸引。同事和实习生不断地要我给他们推荐一些大数据和机器学习方面的书,我也曾给同事买过一些我看过的书。遗憾的是,这些书大多要么是纯理论的“屠龙术”,离实际应用还有一段距离,要么就是针对算法模块搞些例子,使学习者只知其然不知其所以然,还有的就是与目前业界普遍应用的算法不吻合。

对于一线的IT从业者和想要实践算法的学生来说,一本理论与实践相结合、能展现目前业界通用算法使用技巧的书,应该是大家最喜闻乐见的。这要求作者要有极深厚的理论功底,能够把算法娓娓道来,还需要有足够的实践经验,娴熟业界各领域现在通用的算法。《实用机器学习》正是这样的一部佳作。本书详细讨论了机器学习中回归、分类、推荐、排序4类经典问题,详述了每一类问题中常用算法的理论来源,以及在R环境中如何去使用、评测和可视化展现。作为一线的实践者,书中对数据预处理也做了独立讲解,就理论过渡、全面性、实用性、易读性来说,本书都做了充分的考量。R语言作为一个机器学习的平台工具,具有使用简单、分析方便、可用库完备以及可视化容易等特点,为进行问题分析和寻找原因提供了足够的便捷性。即使部分算法没有分布式的解决方案,本书讲授的实用机器学习算法,也极易在Mahout或Spark MLLib上平移对应的接口。在算法和实践平台上,本书倡导的方法和环境,几乎都可以和工业界做到无缝对接。

人工智能的世界来了,浩浩汤汤,开启了IT业者的另外一个世界。或许,我们只是需要一个开始,而《实用机器学习》来得适逢其时,你的一小步将来难说不是人类机器学习世界的一大步!

Fantasy(裴少芳)

威比网络科技(上海)有限公司大数据部总监

2016年12月于上海


本书侧重于数据分析和机器学习的实践,涉及从原始数据搜集到建立模型解决问题再到算法性能评估的全过程。书中主要介绍实践中最常用的4类算法,包括回归算法、分类算法、推荐算法和排序算法。此外,书中还会介绍集成学习。集成学习是一类通过综合多个模型取长补短以取得更好效果的方法,对于回归、分类、推荐和排序问题都适用。在实践中,充分掌握这4类算法和集成学习即可解决相当多的实际问题。由于篇幅所限,聚类分析、关联规则等其他相关内容书中并没有一一介绍。

对于每种算法,本书首先介绍算法的原理。在理解算法原理和算法优缺点的基础上,读者在实践中就可以根据数据的特点和问题的具体需求选用合适的算法。为了突出算法的实践性,本书使用R语言中的软件包来介绍机器学习算法,特别是介绍了如何使用各种算法。R语言是一种开源和免费的解释型语言,其最大的优点是提供了各种软件包,实现了各种不同的算法。机器学习中很多强大的算法在R中都有相应的程序包。我们在讲解各种机器学习算法时,都介绍了R中相应的软件包,并提供了相应的R程序来帮助读者学习这些软件包的使用。这样读者就可以通过R来直接使用相应的算法,获得数据分析的第一手建模经验。

除了介绍这4类机器学习算法之外,本书涵盖了使用机器学习解决实际问题的整个流程,包括数据探索、数据预处理、使用机器学习算法所构建的模型的评价和选择等。在实际使用机器学习处理数据的过程中,数据的探索和预处理是非常重要的步骤,在很多场合甚至比建立模型本身更加重要,从原始数据中提取出一个好的特征在很多时候能够显著地提高模型的性能。得到构建的模型后,我们还需要评价和选择模型。本书还会介绍不同类型算法对应的评价标准以及如何进行模型选择,并介绍R中的相关工具(如caret包),以帮助读者直接上手。

我们尽量使用简单通俗的语言来介绍机器学习中的基本概念和各种常用算法,并通过介绍R中对应的软件包来帮助读者迅速了解和掌握各种算法的使用。为了准确地介绍各类算法,不可避免地要用到一些数学知识,本书在第3章特别介绍了一些相关的数学知识。

本书的所有R代码(包括生成书中图的大部分R代码)都可以从人民邮电出版社异步社区(www.epubit.com.cn)网站上获得。

本书的出版得到了国家自然科学基金(61300122、61502145)的支持,得到了人民邮电出版社编辑杨海玲女士的支持和帮助,在此表示诚挚的谢意。成稿的关键时期适逢我们各自的女儿降生,在此衷心感谢双方家人的理解与支持。因水平和时间所限,书中难免有错误或不当之处,恳请广大读者不吝指正。读者若有任何问题或建议,可发送电子邮件至sun.liang@outlook.com或huangqian@gmail.com。

孙 亮 黄 倩

2016年12月分别于华盛顿雷德蒙和南京


随着计算机和互联网越来越深入到生活中的方方面面,人们搜集到的数据也呈指数级的增长。在这种情况下,大数据(big data)应运而生。大数据通常体量特别大,而且数据比较复杂,使得无法直接使用传统的数据库工具对其进行存储和管理。大数据带来了很多挑战,如数据的搜集、整理、存储、共享、分析和可视化等。广义的大数据处理涵盖了上述所有领域;狭义的大数据更多是指如何使用机器学习来分析大数据,从海量的数据中分析出有用的信息。

大数据分析的核心是机器学习算法。很多时候,我们有足够的数据,但是对如何利用这些数据缺乏理解。同时,实际问题往往比较复杂,并不能直接套用机器学习算法,我们需要对实际问题进行一些转化,使得机器学习算法可以应用。虽然实际问题表现形式各异,但是在将它们转化为机器学习能够处理的问题时,一般转化为如下4类问题:(1)回归问题;(2)分类问题;(3)推荐问题;(4)排序问题。这4类问题是实际应用中最主要的类型,覆盖了大部分实际问题。在1.3节,我们将详细介绍每类问题的具体例子。

机器学习(machine learning)是计算机科学的一个分支,也可以认为是模式识别(pattern recognition)、人工智能(artificial intelligence)、统计学(statistics)、数据挖掘(data mining)等多个学科的交叉学科。机器学习与数值优化(numerical optimization)也有很高的重合度。

机器学习研究如何从数据中学习出有效的模型,进而能对未来作出预测。例如,如果商店能够预测某一件商品在未来一段时间的销售量,就可以提前预订相应数量的商品,这样既可以避免缺货,又可以避免进太多货而造成积压。与传统的决策算法不同的是,机器学习算法依赖于数据。在前面的例子中,我们要从历史数据中学习出相应的模型以对未来进行预测。这样做有两个好处:第一,由于算法依赖于数据,可以使用新的数据来不停地更新模型,使得模型能够自适应地处理新的数据;第二,对人的介入要求少。在使用机器学习的过程中,虽然也会尽量利用人的经验,但更多地强调如何利用人的经验知识从数据中训练得到更好的模型。

目前,机器学习已成为研究和应用的热点之一。一些能够使用机器学习解决的实际问题包括:

虽然这些问题的具体形式不同,但是均可转化成机器学习可以解答的问题形式。

从概念上讲,在机器学习中,我们的目标是从给定的数据集中学习出一个模型f,使得它能够有效地从输入数据中预测我们感兴趣的量。根据问题的不同,我们感兴趣的量(或者叫目标值)可以有不同的形式。例如,在分类问题中,目标值就是若干类别之一;在排序问题中,目标值就是关于文档的一个序列。

在机器学习中,通常我们解决问题的流程如下:

(1)搜集足够多的数据;

(2)通过分析问题本身或者分析数据,我们认为模型f是可以从数据中学习出来的;

(3)选择合适的模型和算法,从数据中学习出模型f

(4)评价模型f,并将其利用在实际中处理新的数据。

在实际中,还需要根据应用的实际情况及时更新模型f。例如,若数据发生了显著变化,则需要更新模型f。因此,在实际部署机器学习模型时,上面的第3步和第4步是一个循环反复的过程。

一个经常与机器学习同时提起的相关领域是数据挖掘(data mining)。数据挖掘和机器学习在很多时候都被(不严格地)混用,因为这两者有很多重叠的地方。传统意义上,机器学习更加注重于算法和理论方面,而数据挖掘更加注重实践方面。数据挖掘中的很多算法都来自于机器学习或者相关领域,少数来自于数据挖掘领域,如关联规则(association rule)。

另一个与机器学习关联很深的领域是统计学。在统计学中,我们学习了很多传统的处理数据的方法,包括数据统计量的计算、模型的参数估计、假设检验等。但在实际问题中,很多情况下我们并不能直接使用统计学中的方法来解决问题。一方面,随着数据规模的扩大,统计学中很多传统的数据分析方法需要通过大量的计算才能得到结果,时效性不高;另一方面,传统的统计学方法更多地考虑了算法在数学上的性质,而忽略了如何在实际中更好地应用这些算法。

在机器学习中,常用的算法可以分为监督型学习(supervised learning)和非监督型学习(unsupervised learning)

在监督型学习中,输出y一般称为目标变量(target variable)或者因变量(dependent variable),而输入x称为解释变量(explanatory variable)或者自变量(independent variable)。

在实际中,在条件允许的情况下,我们偏好监督型学习。因为我们知道相应的目标变量的值,所以能够更加准确地构建模型,取得更好的效果。对于非监督型学习,在实际中,我们可以直接将其结果作为输出,但更多地是将其结果作为新的特征,再应用到监督型学习的算法中。例如,对于一组数据,可以先使用k均值算法对数据进行聚类分析,然后将聚类分析的结果作为新的特征。本书将主要讨论监督型学习。

在监督型学习中,一般将整个数据集分为训练集(training set)和测试集(test set)。利用训练集中的数据,可以构建相应的模型(model)或者学习器(learner)。利用测试集,可以估计所构建模型的性能高低。在数据集中,我们使用样本(sample)、数据点(data point)或实例(instance)来称呼其中的每个点。监督型学习可以进一步分为回归问题、分类问题等。我们将在1.3节利用具体的例子来介绍监督型学习。

在本节中,我们将会介绍一些可用机器学习解决的实际问题,包括病人住院时间预测、信用分数估计、Netflix上的影片推荐和酒店推荐。每个例子都对应一类不同的机器学习问题。通过这些不同类型的机器学习问题,读者对机器学习可以有更多直观的感受。

机器学习在医疗行业有着广泛的应用。我们以Heritage Health Prize竞赛作为例子以说明如何使用机器学习来预测病人未来的住院时间。

在美国每年都有超过7000万人次住院。根据相关统计,2006年在护理病人住院上所花的无关费用就已经超过了300亿美元。如果我们能够根据病人的病历提前预测病人将来的住院时间,那么就可以根据病人的具体情况提前做好相关准备从而减少那些无谓的开销。同时,医院可以提前向病人发出预警,这样就能在降低医疗成本的同时提高服务质量。在从2011年开始的Heritage Health Prize竞赛(HHP)中,竞争者成功地使用机器学习的方法,由病人的历史记录预测了病人在未来一年的住院时间。图1-1显示了竞赛中使用的病历数据的一部分样本。

图1-1 病历数据示例

在现实生活中,向银行申请贷款是比较常见的,如房屋贷款、汽车贷款等。银行在办理个人贷款业务时,会根据申请人的经济情况来估计申请人的还款能力,并根据不同还款能力确定安全的借款金额和相应的条款(如不同的利率)。在美国,每个成年人都有相应的信用分数(credit score),用来衡量和评估借款者的还款能力和风险。

在估计申请者的还款能力时,需要搜集用户的多个方面的信息,包括:

如何将这些因素综合考虑从而决定借贷者的信用分数呢?直观地讲,可以使用一些简单的规则来确定信用分数。例如,某申请者的当前借款金额很高但收入一定,则进一步借款的风险很高,信用分数将会较低;又如,某申请者的某张信用卡在过去经常没有按时还款,则其信用分数也会较低。虽然使用简单的规则能够大致解决信用分数估计的问题,但是这个办法最大的问题是不能自适应地处理大量数据。随着时间的变化,申请者不还款的风险模型可能会发生变化,因此,相应的规则也需要修改。

银行通常可以得到海量的申请者数据和对应的历史数据。利用机器学习的方法,我们希望可以从这些申请者过去的还款记录中自适应地学习出相应的模型,从而能够“智能”地计算申请者的信用分数以了解贷款的风险。具体地讲,在机器学习模型中,将申请者的信息作为输入,我们可以计算申请者在未来能够按时还款的概率。作为一个典型的例子,FICO分数就是美国FICO公司利用机器学习模型开发出来的一个信用分数模型。

Netflix是美国的一家网络视频点播公司,成立于1997年,到2015年该公司已经有了近7000万的订阅者,并且在世界上超过40个国家或地区提供服务。Netflix上的一项很重要的功能是根据用户的历史观看信息和喜好推荐相应的影片,如图1-2所示。2006年10月至2009年9月,Netflix公司举办了Netflix Prize比赛,要求参赛者根据用户对于一些电影的评价(1星~5星),推测用户对另外一些没有看过电影的评价。如果能够准确地预测用户对于那些没有看过的电影的评价,就可以相应地向这些用户推荐他们感兴趣的电影,从而显著提高推荐系统的性能和Netflix公司的盈利水平。

图1-2 Netflix上的电影推荐

在Netflix Prize比赛中,获胜的标准是将Netflix现有推荐系统的性能提高10%。在2009年,BellKor's Pragmatic Chaos队赢得了比赛。其主要方法是基于矩阵分解的推荐算法,并使用集成学习的方法综合了多种模型。Netflix Prize比赛显著地推动了推荐算法的研究,特别是基于矩阵分解的推荐算法的研究。在本书中,我们也将详细介绍这些推荐算法。

Expedia是目前世界上最大的在线旅行代理(online travel agency,OTA)之一。它的一项很重要的业务是向用户提供酒店预订,作为用户和大量酒店之间的桥梁。对于用户的每个查询,Expedia需要根据用户的喜好,提供最优的排序结果,这样用户能够方便地从中选出最合适的酒店。

Expedia于2013年年底与国际数据挖掘大会(International Conference on Data Mining,ICDM)联合举办了酒店推荐比赛。在该项比赛中,Expedia提供了实际数据,包括用户的查询以及其对所推荐结果点击或者购买的记录。在进行酒店推荐时,Expedia考虑了如下因素:

根据用户的查询及用户的背景信息,Expedia返回推荐的酒店序列。在Expedia.com上,典型的酒店搜索界面如图1-3所示。根据返回的推荐结果,用户有3种选择:(1)付款预定推荐的酒店;(2)点击推荐的酒店但没有预订;(3)既没有点击也没有预订。显然,根据用户的反应,我们希望在理想的酒店推荐结果中,对应于第一种选择的酒店能够排在最前面,并且对应于第二种选择的酒店排在对应于第三种选择的酒店前面。

图1-3 在Expedia.com上搜索酒店

上文中的4个例子分别对应于机器学习中的4类典型问题:

在第一类问题中,首先需要为每个病人构建一个特征向量x,然后构建一个函数f,使得可以用f(x)来预测病人的住院时间y。注意,这里要预测的量(病人的住院时间y)的范围是0~365(或者366),我们可以将其转化为回归问题。在回归问题中,目标变量是一个连续值。

在第二类问题中,需要为每个申请者构建一个特征向量x,而输出y是0或者1,代表批准贷款或者不批准贷款。事实上,输出y也可以是批准的概率。这是机器学习中典型的分类问题。在分类问题中,目标变量y是一个离散变量。与回归问题类似,我们的目标是构建一个函数f,使得f(x)可以预测真实的y。在典型的两类分类(binary classification)问题中,目标变量的取值为0或者1(有时是−1或者1)。在多类分类(multi-class classification)问题中,我们有多个类,而目标变量的取值是其中之一。

在第三类问题中,需要根据用户过去的历史为每个用户推荐相应的商品,这是一个典型的推荐问题。与回归和分类问题相比,我们需要为每个用户返回一个感兴趣的商品序列。

在第四类问题中,需要根据用户的输入(在上文的例子中是用户对于酒店的查询),从一系列对象(在这个例子中是酒店)中根据用户的需要返回一个对象的序列,使得该序列最前面的对象是用户最想要的。这类问题称为排序(ranking)问题。同前面的回归问题和分类问题相比,排序问题需要考虑整个返回序列。与前面的影片推荐例子相比,在排序问题中我们需要明确的用户输入,而在影片推荐中我们只是根据用户过去的历史信息来进行推荐,用户没有进行明确的输入。

在实际应用中,机器学习的应用远远超出上面的几个例子。例如,近期非常热门的AlphaGo,谷歌公司在其中使用了深度学习(deep learning)来学习围棋对弈;德国的蒂森克虏伯(ThyssenKrupp)集团作为电梯的主要制造商之一,应用机器学习来预测电梯发生故障的时间从而提前维修,降低电梯的综合运营成本;美国的很多大型零售商在开设新店时,都要搜集各个地区的各种信息和历史销售数据,通过建立机器学习模型的形式选择最优的店址。

本书主要从解决实际问题的角度来介绍常用的机器学习算法。在1.3节中我们讨论了机器学习中常见的4类典型问题,基本上覆盖了目前实际中可以使用机器学习算法来解决的主要问题类型。在本书中,我们将主要讨论对应的4类算法,包括:

其中回归算法和分类算法是两类最常用的算法,也是其他很多算法的基础,因此我们首先予以介绍。推荐系统在目前有了越来越多的应用,而排序算法在搜索引擎等领域也获得了广泛的应用,因此我们也会对常用的推荐算法和排序算法进行介绍。

在上面的4个例子中,我们可以构建多个不同的模型,希望它们之间能够取长补短,使得综合它们之后的模型的性能能够进一步提升。集成学习(ensemble learning)是一类通过综合多个模型以得到更好性能的方法,对于回归问题、分类问题、推荐问题、排序问题都适用,因此我们会专门用一章来介绍集成学习。

本书的目标是尽量介绍实用的算法。读者在掌握我们讨论的机器学习算法后就可以实际使用R中的软件包来解决实际问题了。对于每种算法,我们首先介绍其基本原理。理解算法的基本原理非常重要,它是我们实际使用算法的基础。只有理解了不同算法的特点,才能在实际中根据不同数据的特点合理选择处理的算法。在本书中,我们使用R语言来介绍这些算法的实际使用。机器学习中的各种常用算法在R中都有一种甚至多种实现,而这些都是免费和开放的。用户可以直接使用R中对应的软件包来处理数据,构建相应的机器学习模型。实际使用算法是学习机器学习算法最有效的方式之一。我们希望读者阅读完算法的理论部分后,尽可能地使用R去实际处理数据和建立模型,以获得用机器学习解决问题的第一手经验。

这里我们需要强调,很多实用、有效的算法并不简单,甚至比较复杂。以决策树为例,其原理简单,在很多关于机器学习的书籍中都是予以重点介绍的,但是,由于决策树对于噪声比较敏感,在实际中很容易出现过拟合的现象,因此很少直接使用。然而,基于决策树的随机森林和提升树在实际中应用极为广泛。在本书中,我们也会介绍决策树,但主要目的是作为介绍随机森林和提升树的基础。

由于篇幅有限,本书将主要集中介绍那些最实用的算法。例如,我们没有介绍聚类分析(cluster analysis)和关联规则(association rule)。此外,我们也省略了一些常用算法。例如,在分类算法中,朴素贝叶斯分类器(Naïve Bayesian Classifier)就没有介绍。感兴趣的读者可以参考相关读物了解有关内容。

算法的介绍只是本书的一部分。在使用机器学习算法处理实际问题之前,还需要进行如下步骤:

(1)数据探索(data exploration);

(2)数据预处理(data preprocessing);

(3)从原始数据中构建相应的特征,即特征工程(feature engineering)。

在实际使用机器学习处理数据的过程中,数据探索和数据预处理是非常重要的步骤。通过数据探索,我们可以了解数据的特性,选用合理的预处理方法,如处理缺失数据等。此外,在很多情况下直接对原始数据使用机器学习算法并不能取得良好的效果,我们需要对数据进行一些变换以生成新的特征,这个过程称为特征工程。在实际使用机器学习算法解决问题时,特征工程是非常重要的一步。良好的特征是成功应用机器学习的关键点之一。

在得到算法构建的模型后,还需要评价和选择模型,包括:

注意,不同的算法有不同的评价标准,如分类算法和回归算法的评价标准就不同。因此,我们在介绍一类算法时,通常会介绍此类算法的评价标准。例如,介绍分类算法时,我们介绍了准确率、AUC等评价分类算法的标准以及在R中的计算方法,同时也介绍了交叉检验和R中的caret包以帮助用户在R中进行模型选择。

此外,我们还介绍了R语言和必要的数学基础。本书广泛使用R语言介绍如何使用算法。R语言具有免费、开放、简单易学的特点,在工业界的使用越来越广泛;同时,R语言有大量免费的软件包可以使用,基本涵盖了机器学习的各个领域,本书所介绍的各个领域都能找到相应的R软件包。本书还介绍了常用的数学基础知识,包括概率统计、矩阵计算等,这样读者能够更加容易地理解和掌握算法的原理。

本书大致可以分为两部分。前半部分介绍一些相关的基础知识,后半部分着重介绍各类算法。

由于我们在全书中都使用R来介绍如何使用各种机器学习算法,因此首先在第2章介绍R语言的基础知识。在第3章中,我们介绍相关的数学基础知识,包括概率统计的基础知识和矩阵计算的基础知识等。

第4章介绍数据的探索和预处理,包括数据类型、数据探索、数据预处理及数据可视化。

从第5章开始,我们着重介绍各类算法。我们首先从最基本的回归算法和分类算法开始讨论,然后介绍推荐算法和排序算法,最后介绍如何使用集成学习来综合多个模型以进一步提高模型的性能。

第5章介绍回归分析,包括常用的回归算法以及回归算法的评价和选取。我们从最基本的线性回归和最小二乘法开始讨论,然后讨论更加复杂的回归算法,包括岭回归、Lasso和Elastic Net。在介绍完多种回归算法之后,我们讨论如何评价和选取不同的回归算法。另外,我们讨论偏差-方差权衡(bias-variance tradeoff),并讨论模型复杂度(model complexity)的概念。最后,我们使用实际的案例分析来说明如何在R中使用和选取回归算法。

第6章讨论基本的分类算法,包括决策树、支持向量机和逻辑回归。我们引入了不同的损失函数(loss function),并讨论不同的分类算法如何对应不同的损失函数,以及如何使用正则化项(regularization)来控制模型的复杂度。与回归算法的讨论类似,我们也会讨论如何评价和选取不同的分类算法。为了解决实际中更复杂的分类问题,我们将会详细讨论如何解决不平衡分类(imbalanced classification)问题。本章还会介绍R中对应的软件包,这样读者可以直接尝试使用各种分类算法。另外,我们会着重介绍caret包,这样读者能够简单地使用交叉检验(cross validation)来为各种算法选取最优参数。

第7章介绍常用的推荐算法,主要包括基于相似度的推荐算法和基于矩阵分解的算法。我们还会介绍推荐算法的评价和选取,以帮助用户合理地选取算法。其中,基于相似度的算法包括基于内容的算法和基于邻域的算法;基于矩阵分解的算法包括:

基于内容的推荐算法不是第7章的重点。对于基于矩阵分解的诸算法,我们推导了对应的随机梯度下降(stochastic gradient descent)算法。对于基于邻域的推荐算法,我们将讨论基于用户的邻域推荐算法和基于商品的邻域推荐算法,并详细讨论基于邻域的推荐算法的核心部分:如何计算相似度和邻域。

第8章介绍排序算法,包括逐点排序(pointwise ranking)算法、逐对排序(pairwise ranking)算法和逐列排序(listwise ranking)算法。我们将着重介绍较实用的LambdaMART算法。

集成学习可以显著地提升多种算法的性能。在本书的最后一章,我们将介绍集成学习,包括基本思想和3类不同的集成学习方法:

同时,我们还会介绍R实现中流行的软件包,包括randomForestgbm

这里我们介绍机器学习及相关领域的一些教材和相关资源。首先介绍一下目前流行的多种关于机器学习的教材。

参考文献[1]是一本早期的关于机器学习的教材。该书是一本入门读物,介绍了机器学习的很多基本概念和算法。参考文献[2]是机器学习领域影响很大的一本教材,讨论了很多比较实用的算法,但该书对于读者的数学基础要求较高。参考文献[3]是参考文献[2]的简化版,内容和讲解上都更基础一些。参考文献[4]和参考文献[5]是两本从贝叶斯统计角度讨论机器学习的流行教材,阅读的难度稍大。参考文献[5]介绍的内容稍微前沿一些,很多都是从近年的论文中直接总结的。

参考文献[6]从使用R进行实际建模的角度介绍了机器学习,覆盖了R中的很多软件包和具体用法,讨论了实际建模中的各个步骤。但该书对很多算法没有具体讲解原理和步骤,仅适用于各种模型的入门。参考文献[7]也是近期出版的一本使用R来介绍机器学习的著作,该书以应用为主,所介绍的算法过于基础。

关于模式识别方面的教材,我们推荐参考文献[8]。该书是关于模式识别的经典教材,难度适中。

关于数据挖掘方面的教材,我们推荐参考文献[9]。该书讲解了基本的分类算法、关联规则和聚类算法。其他具有代表性的教材包括参考文献[10]和参考文献[11]。参考文献[10]是一本较早从数据库角度讨论数据挖掘的教材,其中介绍的算法比较基础。参考文献[11]则是一本以WEKA为核心介绍数据挖掘的教材。此外,参考文献[12]的优点是贴近应用实际,使用实际中的一些应用例子来介绍算法。

在掌握机器学习算法时,要注意深度和广度的问题。作为一名机器学习的实践者,熟知多种机器学习算法是必需的;同时,对于那些最常用的算法,要知道其基本思想和底层实现。本书将讨论机器学习中的一些常用算法,但限于篇幅,很难做到面面俱到。读者可以根据自己的实际需要选择不同教材的相关章节阅读。

最前沿的关于机器学习的研究文章通常会在机器学习领域的顶级会议和期刊上发布或发表。此类顶级会议主要包括:

机器学习领域的顶级期刊主要包括:

这里我们只列出了部分顶级的会议和期刊。感兴趣的读者可以在互联网上找到更多的此类会议和期刊。注意,中国计算机学会在其官网上给出了各领域的推荐会议和期刊目录,我们给出的列表主要涉及其中的人工智能、数据挖掘两个领域。

对于机器学习算法的广大应用者来说,最重要的问题是如何利用机器学习算法来解决实际问题。例如,如何将实际问题转化为一个能够直接应用机器学习算法的问题。网站kaggle.com有很多关于机器学习的竞赛,是一个提高这方面能力的优秀媒介。例如,前面讨论的Heritage Health Prize就是由kaggle.com组织的。kaggle.com还提供了过去竞赛的很多获胜方案。在解决具体实际问题时,如果问题和kaggle.com中的某个竞赛类似,则可以借鉴获胜方案。互联网上也有关于kaggle.com竞赛的介绍,例如,SlideShare网站就给出了一个很好的关于kaggle.com的介绍。

关于机器学习的实际应用工具,除了R之外,比较常用的还有Python中的scikit-learn。该软件包涵盖了机器学习中的很多实用算法,包括分类、回归、聚类、数据降维和预处理等。该软件包的文档极为完备。如果读者经常使用Python,scikit-learn是一个极好的机器学习库。WEKA也是数据挖据领域使用较多的一个软件包,该软件包集成了很多常用的机器学习算法,同时也提供了调用的API。当数据规模较大时,很多时候我们需要使用Hadoop平台上的Mahout库和Spark平台上的MLlib库。在本书中,为了方便读者简单地使用各种算法,我们以R为基础来介绍各种算法的使用。

在很多资料中还有第三类称为强化学习(reinforcement learning),近年来还有半监督型学习(semi-supervised learning)提出。本书主要涉及监督型学习和非监督型学习,不讨论强化学习和半监督型学习。

http://www.heritagehealthprize.com/c/hhp

http://www.myfico.com/CreditEducation/WhatsInYourScore.aspx

http://www.netflixprize.com/

http://www.cs.waikato.ac.nz/ml/weka

http://www.ccf.org.cn/sites/ccf/paiming.jsp

http://www.slideshare.net/ksankar/oscon-kaggle20

http://scikit-learn.org/stable/

http://mahout.apache.org/

https://spark.apache.org/docs/1.1.0/mllib-guide.html


作为本书的最后一章,我们主要介绍集成学习。首先我们介绍集成学习的基本思想,之后讨论几种常用的集成学习的方法,包括bagging、boostingstacking。对于bagging和boosting,我们进一步介绍几种在实际中非常有效的算法,包括随机森林、AdaBoost及提升决策树。

集成学习的核心是构建多个不同的模型,并将这些模型聚合起来从而提高模型的性能。集成学习适用于机器学习的几乎所有领域,包括我们前面讨论过的回归、分类、推荐和排序等。在集成学习中,构建的一系列模型称为基学习器。通过使用不同的策略,我们可将这些基学习器聚合起来。在集成学习中,通常并不要求每个基学习器的性能特别好。在很多典型的集成学习算法中,我们只要求每个基学习器要好于随机猜测(random guess)。例如,在两类分类问题中,我们要求基学习器的准确率大于0.5即可。一般而言,这种基学习器也称为弱学习器(weak learner)。在集成学习中,重点是使训练得到的基学习器满足多样性(diversity)的要求,这样将多个基学习器聚合起来时,我们能够有效地提高性能。从直观上看,虽然每个基学习器都犯错,但是如果它们在犯不同的错误,那么将它们聚合起来之后犯错的可能性会很低。反之,如果基学习器比较相似,则通过集成学习提高性能的幅度较小,甚至可能带来过拟合(over-fitting)的问题而导致性能降低。一个极端的例子是,如果我们得到多个完全一样的模型,那么将它们聚合起来不会得到任何提升。

由于集成学习有效地考虑了多个不同的模型,一般而言能够获得较好的性能,因此在很多注重算法性能的场合,集成学习一般是首选。例如,在很多数据挖掘的竞赛中,获胜的算法一般都是使用集成学习将多个模型聚合而成。

根据基学习器的生成策略,集成学习的方法可以分为两类:(1)并行方法(parallel method),以bagging为主要代表;(2)顺序方法(sequential method),以boosting为主要代表。

在并行方法中,我们同时构建多个基学习器,并利用这些基学习器的独立性以提高最后模型的性能。在集成学习中,我们要尽量构建独立的学习器,以使得它们犯错也相互独立。当然,在实际中我们很难得到完全相互独立的基学习器。此外,我们一般只有一个训练集。如果都从同一训练集得到多个不同的模型的话,它们之间很难做到相互独立。因此,在实践中,我们通常通过引入随机性来尽量构造相互独立的模型。并行方法的一大优点是利于并行计算,这样我们可以显著地降低训练模型所需要的时间。

在顺序方法中,我们顺次构建多个学习器。当我们构建后面的学习器时,希望后面的学习器能够避免前面学习器的错误,从而提高聚合后的性能。因此,在顺序方法中,我们要利用基学习器的相关性。与并行方法不同,在顺序方法中,我们需要逐个建立新的学习器,较难利用并行性以缩短训练时间。

与单个模型相比,集成学习的缺点包括:(1)计算复杂度较大。因为在集成学习中需要训练多个模型,所以计算复杂度会有较大程度的提高;(2)一般而言,所得的模型很难解释。如单个决策树模型很容易解释,而由多个决策树组成的随机森林(random forest)却不大容易解释。

在本章中,我们首先介绍顺序方法中的bagging,并介绍bagging的一个典型应用:随机森林;然后我们介绍顺序方法中的boosting,并介绍boosting的两个典型应用:AdaBoost和梯度提升算法;最后,我们介绍stacking的基本思想及应用。

在下面的讨论中,我们一般以分类问题为例进行讨论。本章所讨论的集成学习方法都可以简单修改使之能适用于更广泛的问题,如回归问题、排序和推荐问题等。这也是我们将集成学习放到最后一章讨论的原因之一。

本章中我们将训练集表示为,这里。在集成学习中,我们通常构建m个弱学习器,记为。一般而言,我们通常考虑多个弱学习器的线性组合作为最终的模型。在聚合m个弱学习器时,将的权重记为。这样,最终的模型记为:

(9-1)

在分类问题中,假设输出是{−1,1},则可将最终的模型记为:

(9-2)

这里sign是符号函数。在集成学习中,一般而言,我们需要从训练集中学习出m个学习器,同时也需要学习

在本节中,我们讨论并行集成学习算法的一个典型例子——bagging。在下一节中我们介绍最常用的bagging算法随机森林。

bagging是Bootstrap AGGregatING的缩写。简而言之,就是通过bootstrap取样(可重复取样)的方法构造多个不同的训练集。之后在每个训练集上训练相应的基学习器,最后将这些基学习器聚合起来得到最终的模型。从定义来看,bagging中有两个重要的部分:(1)bootstrap取样;(2)模型聚合(aggregation)。下面我们分别予以介绍。

首先我们讨论bootstrap取样。在前面的讨论中,我们强调了基学习器相互独立的重要性:虽然每个基学习器的性能较弱,但是如果它们都是从不同的角度犯错,那么将它们聚合起来的性能也许会有较大提升。在实际中,在给定一个训练集的情况下,如何尽可能地建立相对独立的基学习器呢?基本上我们只有两种选择:(1)在训练每个基学习器的时候使用不同的训练集;(2)在训练集相同或者相近的情况下,使用不同的学习算法来训练不同的基学习器。如果我们选择使用不同的训练集,一种可能性是将整个训练集划分为多个不同的子集,然后在每个子集上训练不同的基学习器。这个方法虽然保证了这些基学习器是从不同的训练集上得到的,但是由于每个子集都显著小于原来的训练集,使得构建的基学习器可能遗漏了原始训练集中的一些关键信息。同时,每个子集和整个训练集的分布可能存在差异。因此,这样得到的基学习器的性能会受到影响。

因此,这里的问题是,既要利用训练集中更多的样本,又要同时尽量保证不同的训练集的独立性。为了解决这个问题,人们提出了bootstrap取样(bootstrap sampling)。简而言之,bootstrap取样就是使用可重复取样(sampling with replacement)的方法,从样本数为n的数据集中取出n个样本。除了一种极端情况(就是bootstrap取样得到了整个原来的数据集),使用bootstrap取样得到的n个样本中都有重复样本,同时也有原来数据集中的样本没有被取到。注意,在可重复取样中,我们假设每个样本被选中的概率是一样的。在bagging中,对于原始的训练集,我们使用bootstrap取样m次,选取出m个样本集。在每个样本集上,我们构建相应的基学习器。这样就得到了m个不同的学习器。

bagging的另一个重要步骤是模型聚合。在bagging中,我们通常采用比较简单的方法来聚合多个模型。对于分类问题,我们采用投票(voting)的方法将m个基学习器的分类结果中出现最多的一个作为最终的分类结果;对于回归问题,我们直接取m个基学习器的输出的平均值。注意,使用bagging还可以处理多类分类问题,只需要基学习器能够处理多类问题即可。

算法9-1给出了bagging在分类和回归问题中的基本流程。其中mode函数计算统计中的众数(mode),表示从中选取出现频率最高的类标作为最终的分类结果。

算法9-1 bagging的基本流程

 

1. for j=1:m

1.1 产生一个bootstrap取样的样本集

1.2 在上训练一个基学习器

2. 聚合m个模型:

2.1 对于分类问题:

(9-3)

2.2 对于回归问题:

(9-4)

bagging的另一个优点:对于每个基学习器,我们可以利用不在训练集中的样本(out-of-bag sample,OOB样本)来估计基学习器的性能。在训练模型的过程中,由于我们从未接触过OOB样本,因此它们是天然的检验算法性能的数据。

这里简单介绍如何利用OOB样本来估计bagging在分类问题中的正确率。在后面讨论bagging的具体例子随机森林时,我们将进一步讨论如何使用OOB样本来估计变量的重要性。

对于每个样本,我们首先可以找出所有没有利用训练的基学习器,然后利用这些基学习器计算,并使用投票的方法聚合这些得到最终的输出。对于所有的n个样本,我们都可重复这一过程。最后,将这些样本的预测值和真实类别相比较即可算出相应的正确率。

虽然一次bootstrap取样能够得到n个样本(这里n是原始训练集中的样本数目),但是在理想情况下,只能得到大约63.2%的原始样本。下面我们详细解释为什么只能得到大约63.2%的原始样本。这一段介绍不影响读者学习bagging,不感兴趣的读者可以跳过。

考虑样本,其在n次取样中都没有被选中的概率为:

(9-5)

因此,被选中至少一次的概率为:

(9-6)

n趋近于无穷大时,有:

(9-7)

因此

(9-8)

因此,如果我们考虑期望值的话,当n很大时,就会有大约63.2%的原始样本在一次bootstrap取样中被选中。

讨论 

在回归分析中,我们讨论了偏差-方差权衡(bias-variance tradeoff)。我们知道,模型(model)也有偏差(bias)和方差(variance)。从直观上看,对于一组给定的训练数据,如果某一算法能够很好地拟合这组训练数据,则该模型的偏差较低;但是如果训练数据发生了一些变动,而导致算法产生的模型也发生了较大变动的话,则该模型的方差较高。典型的例子就是决策树和人工神经网络。虽然它们都能很好地拟合训练集,但是对于训练集过于敏感。下面我们首先以随机变量为例讨论多个变量平均之后的方差,然后将其推广到模型以说明模型多样性的重要性。

我们知道,假设我们有m个独立同分布的随机变量,它们的方差是,则平均之后的方差为。简而言之,取平均值可以降低方差。注意,在bagging中,实际上不可能构造出完全独立的基学习器。如果我们将bagging中构造的每个模型视为一个随机变量的话,则这些随机变量都不是相互独立的。

下面我们考虑稍微一般的情况:考虑m个随机变量同分布但不相互独立,并假设两两之间的相关系数为 >0,则平均后的方差为:

(9-9)

从式(9-9)可以看出,当m增加时,式(9-9)中的第二项减少,但是第一项不受m的影响。为了清楚地了解ρ对平均后的方差的影响,可将式(9-9)写为:

(9-10)

从式(9-10)可以看出,ρ越小,平均之后的方差就越小。回到模型的讨论,如果所构建的模型相互越独立,则平均之后的模型对应的方差就越小。

这里我们给出随机变量平均后的方差的具体推导过程。不感兴趣的读者可以直接跳过这部分的推导。假设我们有未知变量满足,并且当时,对应的相关系数,则协方差可写为:

(9-11)

的平均值的方差可计算如下:

(9-12)

在集成学习中,可以将模型的方差类比为随机变量的方差。通过平均多个模型,也能降低模型的方差,从而得到更好的模型。bagging通过人为的办法,构建了多个模型,最后通过平均的方法,降低模型的方差。通过上面的讨论可知,要尽量发挥bagging的功效,在构造基学习器时要尽量构造相互独立的基学习器,这样最后平均得到的模型的方差就会小。另外,bagging对于那些高方差低偏差(high-variance,low-bias)的基学习器非常有效。基本上基学习器对训练数据越敏感,bagging之后的效果就越好。但是如果基学习器比较稳定,换言之就是方差较小,在这种情况下,bagging所能取得的提升则非常有限。此外,bagging的优点是非常适合并行训练多个算法,可有效地处理大数据。

在本节中,我们讨论bagging的一个实际应用算法——随机森林(random forest)。随机森林使用了bagging的基本思想来训练一系列决策树。但是随机森林又根据决策树的特点做了很多改进,使得所构建的决策树尽量没有相关性,从而显著提高最终的性能。在很多实际问题中,随机森林都能取得很好的效果,同时由于其控制参数易于选择,因此使得随机森林成为实际中非常受欢迎的算法,广泛应用于各类实际问题中。

在讨论随机森林之前,我们先回顾一下决策树。决策树能够有效地获取多个变量之间的相互关系。同时,如果决策树足够深,那么它能够有效地降低模型的偏差。通过前面章节关于决策树的讨论我们知道,较深的决策树虽然能够降低模型的偏差,但是很容易导致过拟合。对于不同的数据集,得到的决策树可以存在较大差异。换言之,决策树的方差较大,因此是使用bagging技巧的好对象。

在本节中,我们介绍训练随机森林的基本思想和流程。随机森林的基本思想是利用bagging构建很多决策树。通过前一节的讨论我们知道,为了提高bagging的效果,我们需要构建尽量独立的基学习器。与简单的bagging相比,随机森林在构建每棵决策树时,通过引入一些随机信息,有效地降低了各个基学习器的相关度。

具体来说,在构建决策树时,我们需要在每步选择最优的变量。在标准的构建决策树的算法中,我们需要考虑当前所有可选的变量;但是在随机森林中,我们从所有可选的d个变量中随机地取出个变量,然后从中选取出最优的变量。从直观上讲,降低的值虽然降低了单个决策树在其对应的训练集上的表现,但是它能够降低所构建的不同的决策树的相关度,从而能够降低最终平均后的模型的方差。训练随机森林的具体步骤参见算法9-2。

算法9-2 训练随机森林的具体步骤

 

1. for j=1:m

1.1 产生一个bootstrap取样的样本集

1.2 在上训练一个决策树。在生成决策树的过程中,对于每个对应的样本数大于的叶结点做如下处理:

1.2.1 从所有可选的d个变量中随机选择个变量

1.2.2 从这个变量中选择导致最优划分的变量

1.2.3 将该结点根据选择的最优变量划分为两个子结点

1.2.4 重复该过程,直到所有的叶结点对应的样本数都小于或等于

2. 聚合m棵决策树

2.1 对于分类问题:

(9-13)

2.2 对于回归问题:

(9-14)

在随机森林中,我们要构建一系列决策树。在构建每个决策树时,首先使用bootstrap取样得到一个样本数为n的可重复样本。然后利用这个样本集,构建一棵决策树。在构建决策树的每一步,我们都是按照上面所讨论的,先随机取出d1个变量,再选取出最优的变量。最后我们将所有模型的输出取平均(回归问题)或者取出最多的类别(分类问题)作为最终的输出。

在实际使用机器学习算法时,通常数据中都含有噪声或者冗余数据。因此,如果模型能够直接告诉我们各个变量的重要性,就可以剔除这些噪声和冗余数据。这样一方面能够提高模型的性能,另一方面能够降低计算复杂度。

在随机森林中,有两种方法可以用来确定每个变量的重要性:第一种方法是利用决策树的性质来计算变量的重要性;第二种方法是利用OOB样本来估计变量的重要性。在实际使用中,第二种方法的应用更为广泛。

在第一种方法中,在构建每棵决策树时,可以计算每个结点对应的变量所导致的信息增益。把所有的决策树都聚合起来,就可以计算每个变量所导致的信息增益。利用这个信息增益,可计算每个变量的重要性:信息增益越大,变量的重要性越高。

在第二种方法中,可以使用OOB样本来确定每个变量的重要性。在考虑第j棵树时,我们将所有没有包含在第j次bootstrap取样集中的OOB样本集记为。我们可将作为检验集(validation set),计算上的性能,如分类中的准确率或者回归中的RMSE。当考虑第k个变量的重要性时,我们将中所有样本的第k个变量的值打乱以得到一个随机排列。这样我们得到一个新的数据集,而且的区别在于,中第k个变量的值是随机赋予的,并没有明确的意义。我们将作为一个新的检验数据集,并计算上的性能。一般而言,要好于,我们将的差作为第k个变量的重要性。注意,在随机森林中,我们有m棵树,将所有m棵树的差平均起来,就可以作为第k个变量的重要性的度量。以分类问题为例,假设我们选定性能度量为准确率,则一般而言有,那么第k个变量的重要性可以使用式(9-15)来计算:

(9-15)

这种通过随机排列变量值来估计变量重要性的方式,目前也有了很多应用。在微软公司最新推出的Azure Machine Learning中就有相应的模块来通过此法估计变量的重要性

在实际使用随机森林时,一般需要确定如下参数。

这里我们首先讨论选取这些参数的一般原则,然后通过介绍R中的randomForest包来具体介绍随机森林的使用。

对于的选取和决策树的大小,随机森林的发明者Leo Breiman提出了如下建议。

对于的值,Breiman提出可以试验默认值、默认值的一半以及默认值的两倍,并从中挑选最优值。在很多数据中,随着的变化,随机森林的性能变化并不是非常剧烈。在一些数据集中,甚至将设为1都能取得良好的效果。然而,如果数据集的维数d较大但是其中很多是无用噪声的话,在建模中只有少数的特征是真正有用的。在这种情况下,我们需要选取较大的值,这样那些有用的特征才能够在构建决策树时被选到。

在实际中,如果不是数据集特别小或者问题特别简单,那么上百棵决策树是必需的。一般而言,随着变量的增加,若要取得较好的性能,就需要增加随机森林中决策树的数目。具体来说,我们可以简单测试当前决策树的数目是否足够:比较当前已经得到的所有决策树及其一个部分。比如,目前有500棵决策树,我们可以选取前面的450棵决策树,比较500棵决策树组成的随机森林模型和450棵决策树组成的随机森林模型。换言之,我们利用两个不同的随机森林模型来看它们的预测值是否有较大的区别。如果区别较大,则意味着我们要继续增加决策树的数目。我们的目标是保证决策树的数目足够多,直到随机森林的性能比较稳定为止。

R程序以及介绍

下面介绍R中非常流行的软件包randomForest。我们提供了程序文件randomForest_ classification_example.R 和 randomForest_regression_example.R用来介绍如何应用随机森林解决分类问题和回归问题。randomForest包中的核心函数是randomForest函数。使用该函数,可以构建一个随机森林模型。该函数的重要参数包括以下几个。

其他一些函数如下。

注意,randomForest包的randomForest函数中有一个参数是importance,同时randomForest包中也有一个函数为importance

在下面的例子中,我们着重讲解randomForest_regression_example.R,以说明如何使用randomForest包。在该文件中,我们利用randomForest包,完成以下任务:

下面是具体的R代码。注意,我们首先要检查randomForest包有没有安装,如果没有安装,则首先安装该包。

# Check required package is installed or not. If not, install it.
randomForest.installed <- 'randomForest' %in% rownames(installed.packages())
if (randomForest.installed) {
 print("the randomForest package is already installed, let's load it...")
}else {
 print("let's install the randomForest package first...")
 install.packages('randomForest', dependencies=T)
}
library('randomForest')

#========================
# Step 1. Load the data and simple exploration
# load the mtcars data
data(mtcars)
D <- mtcars

# show the type for each col
for(i in 1:ncol(D)) {
 msg <- paste('col ', i, ' and its type is ', class(D[,i]))
 print(msg)
}

# Step 2. Split the data into training and test sets
# Randomly split the whole data set into a training and a test data set
# After spliting, we have the training set: (X_train, y_train)
# and the test data set: (X_test, y_test)
train_ratio <- 0.7
n_total <- nrow(D)
n_train <- round(train_ratio * n_total)
n_test <- n_total - n_train
set.seed(42)
list_train <- sample(n_total, n_train)
D_train <- D[list_train,]
D_test <- D[-list_train,]

y_train <- D_train$mpg
y_test <- D_test$mpg

# Step 3. Benchmark: train a single decision tree using rpart
library('rpart')
M_rpart1 <- rpart(mpg~., data = D_train)
print('show the summary of the trained model')
summary(M_rpart1)

# Compute the performance on the training and test data sets
y_test_pred_rpart1 <- predict(M_rpart1, D_test)
rmse_test_rpart1 <- sqrt(sum((y_test - y_test_pred_rpart1)^2) / n_test)
msg <- paste0('rmse_test_rpart1 = ', rmse_test_rpart1)
print(msg)

y_train_pred_rpart1 <- predict(M_rpart1, D_train)
rmse_train_rpart1 <- sqrt(sum((y_train - y_train_pred_rpart1)^2) / n_train)
msg <- paste0('rmse_train_rpart1 = ', rmse_train_rpart1)
print(msg)

# Step 4. train 2 randome forest models using different parameters
# Step 4.1 Train a random forest model using default parameters
# the default number of trees is 500
M_randomForest1 <- randomForest(mpg~., data = D_train)
print('show the summary of the trained model')
summary(M_randomForest1)
# We can get mean of squared residuals using the print function directlty
print(M_randomForest1)
# We can check the number of trees of the trained model
ntree1 <- M_randomForest1$ntree
msg <- paste0('number of trees in M_randomForest1 = ', ntree1)
print(msg)

# Get the prediction on the test set and compute the RMSE
y_test_pred_rf1 <- predict(M_randomForest1, D_test)
rmse_test_rf1 <- sqrt(sum((y_test - y_test_pred_rf1)^2) / n_test)
msg <- paste0('rmse_test_rf1 = ', rmse_test_rf1)
print(msg)

y_train_pred_rf1 <- predict(M_randomForest1, D_train)
rmse_train_rf1 <- sqrt(sum((y_train - y_train_pred_rf1)^2) / n_train)
msg <- paste0('rmse_train_rf1 = ', rmse_train_rf1)
print(msg)

# We train a second model using fewer decision trees, and control the 
# complexity of each tree.
M_randomForest2 <- randomForest(mpg~., data = D_train, ntree=50, mtry=3)
print('show the summary of the trained model')
summary(M_randomForest2)
print(M_randomForest2)
# We can check the number of trees of the trained model
ntree2 <- M_randomForest2$ntree
msg <- paste0('number of trees in M_randomForest2 = ', ntree2)
print(msg)

# Get the prediction on the test set and compute the RMSE
y_test_pred_rf2 <- predict(M_randomForest2, D_test)
rmse_test_rf2 <- sqrt(sum((y_test - y_test_pred_rf2)^2) / n_test)
msg <- paste0('rmse_test_rf2 = ', rmse_test_rf2)
print(msg)

y_train_pred_rf2 <- predict(M_randomForest2, D_train)
rmse_train_rf2 <- sqrt(sum((y_train - y_train_pred_rf2)^2) / n_train)
msg <- paste0('rmse_train_rf2 = ', rmse_train_rf2)
print(msg)

# Step 5. Combine 3 random forest models together using the combine function
# Train 3 random forest models
M_rf_base1 <- randomForest(mpg~., data = D_train, ntree = 15)
M_rf_base2 <- randomForest(mpg~., data = D_train, ntree = 20)
M_rf_base3 <- randomForest(mpg~., data = D_train, ntree = 10)
# Combine these 3 models just trained
M_rf_comb <- combine(M_rf_base1, M_rf_base2, M_rf_base3)
print(M_rf_comb)

# compute the performance on the test data set
y_test_pred_rf_base1 <- predict(M_rf_base1, D_test)
rmse_test_rf_base1 <- sqrt(sum((y_test - y_test_pred_rf_base1)^2) / n_test)
msg <- paste0('rmse_test_rf_base1 = ', rmse_test_rf_base1)
print(msg)

y_test_pred_rf_base2 <- predict(M_rf_base2, D_test)
rmse_test_rf_base2 <- sqrt(sum((y_test - y_test_pred_rf_base2)^2) / n_test)
msg <- paste0('rmse_test_rf_base2 = ', rmse_test_rf_base2)
print(msg)

y_test_pred_rf_base3 <- predict(M_rf_base3, D_test)
rmse_test_rf_base3 <- sqrt(sum((y_test - y_test_pred_rf_base3)^2) / n_test)
msg <- paste0('rmse_test_rf_base3 = ', rmse_test_rf_base3)
print(msg)

y_test_pred_rf_comb <- predict(M_rf_comb, D_test)
rmse_test_rf_comb <- sqrt(sum((y_test - y_test_pred_rf_comb)^2) / n_test)
msg <- paste0('rmse_test_rf_comb = ', rmse_test_rf_comb)
print(msg)

# Step 6. Continuously grow a random forest by training more trees, and
# plot the RMSE of training and test as the number of trees increases.

ntree_list <- 1:200
ntree_length <- length(ntree_list)
rmse_train_list <- rep(0, ntree_length)
rmse_test_list <- rep(0, ntree_length)
for (i in 1:ntree_length) {
 # Build the random forest model based on the existing random forest model
 if (i==1) {
  M_rf_base <- randomForest(mpg~., data = D_train, ntree = ntree_list[1])
 }else {
  ntree_delta <- ntree_list[i] - ntree_list[i-1]
  M_rf_base <- grow(M_rf_base, ntree_delta)
 }
 # Compute rmse on training and test data set
 y_train_pred_rfi <- predict(M_rf_base, D_train)
 y_test_pred_rfi <- predict(M_rf_base, D_test) 
 rmse_train_rfi <- sqrt(sum((y_train - y_train_pred_rfi)^2) / n_train)
 rmse_test_rfi <- sqrt(sum((y_test - y_test_pred_rfi)^2) / n_test)
 rmse_train_list[i] <- rmse_train_rfi
 rmse_test_list[i] <- rmse_test_rfi
}
# Plot the training and test RMSE
y_min <- min(min(rmse_test_list), min(rmse_train_list)) - 0.1
y_max <- max(max(rmse_test_list), max(rmse_train_list)) + 0.1
plot(range(ntree_list), c(y_min, y_max), type='n', xlab='ntree', ylab='RMSE')
lines(ntree_list, rmse_train_list, type='l', lty=1, col='black')
lines(ntree_list, rmse_test_list, type='l', lty=2, col='red')
legend_char_list <- c('training data', 'test data')
# We can use locator(1) to specify the legend position by clicking the mouse
#legend(locator(1), legend_char_list, cex=1.2, col=c('red', 'black'), lty=c(1,2))
# Or we can specify the legend position directly
legend("topright", legend_char_list, cex=1.2, col=c('red', 'black'), lty=c(1,2))

# Step 7. Check the importance of all variables
# We train a new random forest model consisting of 100 trees
M_rf_imp <- randomForest(mpg~., data = D_train, ntree = 100, importance = T)
print('we show the variable importance using OOB samples')
var_imp1 <- importance(M_rf_imp, type=1)
print(var_imp1)

print('we show the variable importance using tree node impurity/MSE descrease')
var_imp2 <- importance(M_rf_imp, type=2)
print(var_imp2)

# Plot the importance of all variables
varImpPlot(M_rf_imp, main = 'Variable importance of M_rf_imp')

然后对数据做了一个简单的检查,即检查每列的类型,其输出如下:

[1] "col 1 and its type is numeric"
[1] "col 2 and its type is numeric"
[1] "col 3 and its type is numeric"
[1] "col 4 and its type is numeric"
[1] "col 5 and its type is numeric"
[1] "col 6 and its type is numeric"
[1] "col 7 and its type is numeric"
[1] "col 8 and its type is numeric"
[1] "col 9 and its type is numeric"
[1] "col 10 and its type is numeric"
[1] "col 11 and its type is numeric"

我们将数据按照70%和30%的比例分为训练集和测试集,并分别保存在数据框D_trainD_test中。

接着我们使用rpart软件包构建了一个决策树的回归模型,其输出如下:

[1] "show the summary of the trained model"
Call:
rpart(formula = mpg ~ ., data = D_train)
 n= 22 

     CP nsplit rel error  xerror   xstd
1 0.6332989   0 1.0000000 1.1516657 0.3243608
2 0.0100000   1 0.3667011 0.7476655 0.1917382

Variable importance
 hp  wt cyl disp drat qsec 
 20  18  16  16  14  14 

Node number 1: 22 observations,  complexity param=0.6332989
 mean=19.97273, MSE=40.49198 
 left son=2 (12 obs) right son=3 (10 obs)
 Primary splits:
   hp  < 116.5 to the right, improve=0.6332989, (0 missing)
   wt  < 3.325 to the right, improve=0.6219046, (0 missing)
   cyl < 5   to the right, improve=0.6198157, (0 missing)
   disp < 163.8 to the right, improve=0.5950779, (0 missing)
   drat < 3.91 to the left, improve=0.4739783, (0 missing)
 Surrogate splits:
   wt  < 3.325 to the right, agree=0.955, adj=0.9, (0 split)
   cyl < 5   to the right, agree=0.909, adj=0.8, (0 split)
   disp < 163.8 to the right, agree=0.909, adj=0.8, (0 split)
   drat < 3.655 to the left, agree=0.864, adj=0.7, (0 split)
   qsec < 18.25 to the left, agree=0.864, adj=0.7, (0 split)

Node number 2: 12 observations
 mean=15.35, MSE=8.8225 

Node number 3: 10 observations
 mean=25.52, MSE=22.0796 

[1] "rmse_test_rpart1 = 3.60306119848109"
[1] "rmse_train_rpart1 = 3.85336924592681"

可以看出,所得的决策树模型在测试集和训练集上的RMSE分别约为3.60和3.85。

之后在第4步中我们构建决策树模型。在第一个随机森林模型中,全部采用默认的参数值:

M_randomForest1 <- randomForest(mpg~., data = D_train)

随后我们使用M_randomForest1$ntree检查一共构建了多少决策树。在第二个随机森林模型中,我们指定构建50棵决策树,并将mtry设为3:

M_randomForest2 <- randomForest(mpg~., data = D_train, ntree=50, mtry=3)

我们使用print函数输出了这两个模型的一些信息:

[1] "show the summary of the trained model"

Call:
 randomForest(formula = mpg ~ ., data = D_train) 
       Type of random forest: regression
           Number of trees: 500
No. of variables tried at each split: 3

    Mean of squared residuals: 6.648723
         % Var explained: 83.58
[1] "number of trees in M_randomForest1 = 500"
[1] "rmse_test_rf1 = 1.94172203933111"
[1] "rmse_train_rf1 = 1.29780945437018"
[1] "show the summary of the trained model"

Call:
 randomForest(formula = mpg ~ ., data = D_train, ntree = 50, mtry = 3) 
       Type of random forest: regression
          Number of trees: 50
No. of variables tried at each split: 3

     Mean of squared residuals: 8.865426
         % Var explained: 78.11
[1] "number of trees in M_randomForest2 = 50"
[1] "rmse_test_rf2 = 1.86589255022898"
[1] "rmse_train_rf2 = 1.47045984929335"

在第5步中,我们构建了M_rf_base1M_rf_base2M_rf_base3随机森林模型,然后调用combine函数将这3个模型聚合起来得到一个随机森林模型。

M_rf_comb <- combine(M_rf_base1, M_rf_base2, M_rf_base3)

在第6步中,我们首先构建只有一棵决策树的随机森林模型,然后使用grow函数向该随机森林模型中不断添加新的决策树。

M_rf_base <- grow(M_rf_base, ntree_delta)

图9-1显示了随着决策树数目的增加,训练集和测试集上的RMSE的变化。从图9-1中可以看出,当决策树刚开始增加时,训练集和测试集上的RMSE都开始减小。但是当决策树的数目继续增加时,训练集和测试集上的RMSE都在波动一下后保持平稳,说明进一步增加决策树的数量不会进一步提高模型的性能。

图9-1 在随机森林模型中增加决策树的数目导致的训练集和测试集上RMSE的变化情况

在第7步中我们考虑了随机森林模型中变量的重要程度。在构建随机森林模型时,首先必须将参数importance设为TRUE

M_rf_imp <- randomForest(mpg~., data = D_train, ntree = 100, importance = T)

然后可以使用importance函数得到该模型中变量的重要程度。通过将参数type设为1(使用OOB样本来估计)或者2(使用决策树中MSE或者不纯洁程度来度量),我们能够得到不同的重要程度度量。

var_imp1 <- importance(M_rf_imp, type=1)
var_imp2 <- importance(M_rf_imp, type=2)

其输出如下:

[1] "we show the variable importance using OOB samples"
   %IncMSE
cyl 4.596421
disp 4.250417
hp  6.674625
drat 1.616390
wt  6.409588
qsec 2.719904
vs  1.652735
am  1.483702
gear 1.306683
carb 1.862388
[1] "we show the variable importance using tree node impurity/MSE descrease"
   IncNodePurity
cyl   96.819302
disp  160.206895
hp   221.200911
drat   59.407580
wt   196.157244
qsec   21.761652
vs    17.399410
am    9.937528
gear   7.189293
carb   21.664176

最后我们使用函数varImpPlot直接绘出变量的重要程度,如图9-2所示。

varImpPlot(M_rf_imp, main = 'Variable importance of M_rf_imp')

图9-2 随机森林模型M_rf_imp中使用两种不同的方法所得的变量的重要程度

boosting是近年来机器学习领域比较热门的一个方向。boosting的本意是“提高”。与bagging中并行构建很多基学习器相比,boosting顺次建立一系列基学习器。在构建新的基学习器时,boosting通过分析当前已经建立的基学习器,如分析以前的基学习器错误分类的训练样本,寻找改进的方向,来构建新的基学习器。利用boosting,可以显著地提高传统算法的性能。

boosting的基本思想虽然简单,但是事实上涉及的数学基础较多。在本章中,我们首先以AdaBoost为例来介绍boosting。在下一节我们将介绍广泛应用的提升决策树(boosted tree)和对应的梯度提升(gradient boosting)算法。

在很多文献中,AdaBoost都是作为boosting的一个典型例子来介绍的。在下面的讨论中,我们以分类问题为例介绍AdaBoost的基本思想,其在回归问题中的使用可以类似地推导出。

AdaBoost的基本思想:首先构造一个弱分类器(weak classifier)。根据该分类器在训练集上的分类结果,我们给训练集中的每个样本赋予一个权值(weight):如果该样本被正确分类,则权值较小;如果该样本被错误分类,则权值很大。之后我们考虑每个样本的权值以构建第二个弱分类器,使得权值较大的样本能够尽量被正确分类。换言之,在构建新的分类器时,我们尽量多考虑那些在前面被错分的样本。根据前面两个弱分类器的结果,我们可以更新每个样本的权值,从而构建第三个弱分类器。通过重复该过程,我们可以构建多个弱分类器,从而最终得到较好的分类效果。从直观上讲,后面的弱分类器集中处理前面被错分的样本,这样使得分类器犯的错误各不相同,因此聚合之后能够得到较好的效果。

在介绍AdaBoost算法之前,我们先介绍boosting的基本思想和指数损失函数(exponential loss function)。在此基础上,我们给出AdaBoost的具体步骤,最后介绍如何在R中使用AdaBoost。

在boosting中,我们所得的模型是顺次构建的多个模型的聚合:

(9-16)

在式(9-16)中,我们有m个基学习器,这里x表示输入,表示第j个模型对应的参数,是模型对应的权重。注意,在第步时,我们所得的模型为:

(9-17)

在boosting中,我们顺次构建这些模型,同时一般是一类函数,且比较简单。例如,在我们接下来要讨论的提升决策树中,每个都是由一棵决策树来确定的。

在机器学习中,我们通常最小化模型在训练集上的损失函数来学习出模型的参数,这里。换言之,我们要求解如下的优化问题:

(9-18)

这里L是一个损失函数,用来衡量拟合yi的好坏程度。在这个优化问题中,我们要同时求解所有的wj。从求解优化问题的角度讲,同时求解所有的参数难度太大,因此我们可以采用顺序求解的方法来求解这个优化问题。

(1)求解第1个学习器的权重和参数

(2)固定权重和参数,求解第2个学习器的权重和参数

(3)重复该过程,直到求解第m个学习器的权重和参数

严格地讲,在第j步我们求解如下最优化问题:

(9-19)

作为第j个弱学习器的权值和参数。换言之,在第j步时,固定住已求解出的,然后优化。简而言之,就是由于同时求解所有的难度太大,因此我们采用了类似贪婪算法的方法来顺次求解该优化问题。

接下来我们讨论指数损失函数,并以此函数引入AdaBoost。事实上,AdaBoost先被提出。通过分析AdaBoost算法,人们发现其对应于指数损失函数。但是在本书中为了方便读者理解AdaBoost,我们先从指数损失函数开始讨论,进而推导出AdaBoost算法。

指数函数适用于分类问题。在本章中我们假设类标,则指数损失函数的定义为:

(9-20)

图9-3给出了指数函数的图像,同时也给出了其他一些常用损失函数的图像。

下面我们解释为何指数损失函数的定义是合理的。由于我们假设,因此,当时,我们希望越大越好;当时,我们希望越小越好,最好是负数。综合起来,就是希望越大越好。与在SVM中的讨论类似,当时,分类是正确的,我们不应该引入损失或者引入极小的损失。而当<0时,我们应引入损失,而且的值越小,引入的损失越大。因此,这里我们引入指数函数来处理这两种不同的情况。注意,当时,我们仍然引入了损失,但是很小。指数函数的最大优点是它是连续可导的,而且导数非常容易计算,使得我们在求解相关的优化问题时很容易计算。

图9-3 分类算法中的常用损失函数

事实上,当使用指数损失函数时,如果我们在boosting中使用顺次求解(forward stage-wise additive modeling)的方式来导出权重和分类器,就得到了著名的AdaBoost(Adaptive Boosting)算法。下面我们进行推导并给出AdaBoost算法的具体流程。

假设我们已经推导出前面的j-1个学习器的权重和参数,并记前j-1个学习器的加权和所得的模型为:

(9-21)

在使用指数损失函数的情况下,在第j步我们需要求解如下问题:

(9-22)

这里我们将目标值简写为:

(9-23)

注意,在这里只依赖于,因此在求解第j步时为定值,可以将其视为在第j步时对于样本的权值。我们将该权值记为:

(9-24)

注意,该权值既与样本的真实类别有关,也与已经构建的分类器对该样本的当前分类结果有关。事实上,就是我们使用作为的分类结果所引起的指数损失函数值。因此,目标函数可以简写为:

(9-25)

下面我们讨论在第j步如何具体求解权值和函数

在分类问题中,我们首先注意到>0。当时,,当时,

因此,可以将目标函数表示为:

(9-26)

如果假定权重w>0,则>0, exp (-w)>0。注意,此时F中只有前一项中考虑了函数。更进一步,对于任何一个给定的w>0,使得F最小的必然使得最小。换言之,函数可通过求解如下问题得到:

(9-27)

这里函数I(x)的定义为:

简单地讲,函数就是使得使用权重之后的错误率最低的分类器。

下面我们推导在规定函数的情况下如何得到最优的。如果记A=,那么可以将目标函数表示为:

(9-28)

我们计算Fw的偏导数并设为0即可得到最优的w

(9-29)

如果对于模型,我们引入加权的错误率

(9-30)

则最优的可表示为:

(9-31)

那么在构建第j个分类器后,前j个分类器的和为:

(9-32)

注意,,那么每个样本对应的权重在构建第j个分类器后可以利用如下公式更新:

(9-33)

在式(9-33)中的倒数第二步,我们利用了性质。验证该式子很简单,只需要考虑(即)和(即)两种不同的情况即可。

这样我们就得到了著名的AdaBoost算法。算法9-3给出了AdaBoost算法的具体步骤。

算法9-3 AdaBoost算法

 

1. 将训练集中每个样本的权值定为

2. for j=1:m

2.1 构建一个分类器(考虑训练集中每个样本的权值

2.2 计算该分类器加权的错误率

(9-34)

2.3 计算该分类器的权值

(9-35)

2.4 更新每个样本的权值

(9-36)

3.输出最终的分类器

(9-37)

在该算法中,我们顺次构建m个分类器,且每个分类器能够处理样本的权重(如果分类器不能在训练过程中处理样本的权重,则我们不能直接应用该分类器)。在第j步,得到该分类器后,我们计算该分类器对应的错误率并据此计算对应的权重。然后根据更新每个样本的权重。与前面推导的公式相比,在更新时我们省略了,原因是所有的权重都要乘以,因此可以直接省略该项。在更新样本的权重时,我们可以看到:如果,则不变;如果,则要乘以权重,这样在构建下一个分类器时会更加重视这些被错分的样本。因此,在实际使用AdaBoost时,我们要为每个样本记录它们当前的权重,同时也要记录每个分类器对应的权重

在本节,我们讨论如何使用R中的adabag来调用AdaBoost算法。adabag软件包实现了适用于分类问题的boosting和bagging程序。我们提供了 AdaBoost_classification_example.R文件来介绍如何使用adabag软件包调用AdaBoost算法。

adabag包中,需要使用boosting函数调用AdaBoost算法。该函数实现了AdaBoost算法,能够解决两类分类问题和多类分类问题。在使用boosting函数时,需要指定数据,包括具体的输入数据和对应的公式(指定哪个变量是目标变量,哪些变量是自变量)。下面我们简要介绍boosting函数中的几个重要参数。

与前面讨论的标准AdaBoost算法相比,这里的参数boos指定了在构建每个分类器时利用权值的方法。boosting函数返回一个boosting对象(也称boosting,但是这个是对象,前一个是函数),包括所涉及的决策树的数目、每棵决策树的权重,以及所得的所有决策树的具体参数。我们可以逐一检查返回的决策树。

与随机森林类似,一棵决策树很容易理解,但是很多决策树的聚合却不容易理解。在这种情况下,如果我们能够计算每个变量的重要程度,将会帮助我们理解所得的模型。利用返回的boosting对象和errorevol函数,我们可以计算每个变量的重要程度。这里变量的重要程度是利用每棵决策树中每个变量带来的基尼指数的减小,并综合考虑每棵决策树的权重所得到的。

下面我们通过讲解AdaBoost_classification_example.R文件中的代码来说明如何使用adabag包。

首先我们检查adabag包是否已经安装。如果没有安装,则首先安装该软件包。在第1步,我们载入Sonar数据。注意,mlbench包自带该数据集,而adabag依赖于mlbench包。当我们使用library(adabag)载入adabag包时,mlbench包也自动载入,因此,这里可以使用data(Sonar)来直接载入该数据集。该数据集的最后一列是类标,我们将其列名改为'classLabel'。在第2步,我们将数据按照70%和30%的比例分为训练集和测试集,并分别保存在数据框D_trainD_test中。

# Step 0. Check required package is installed or not. If not, install first.
# 1. adabag
adabag.installed <- 'adabag' %in% rownames(installed.packages())
if (adabag.installed) {
 print("the adabag package is already installed, let's load it...")
}else {
 print("let's install the adabag package first...")
 install.packages('adabag', dependencies=T)
}
library('adabag')
library('rpart')

# Step 1. Load the Sonar data in the mlbench library
data(Sonar)
D <- Sonar
colnames(D)[ncol(D)] <- 'classLabel'

# Step 2. Split the data into training and test sets
# Randomly split the whole data set into a training and a test data set
# After spliting, we have the training set: (X_train, y_train)
# and the test data set: (X_test, y_test)
train_ratio <- 0.7
n_total <- nrow(D)
n_train <- round(train_ratio * n_total)
n_test <- n_total - n_train
set.seed(42)
list_train <- sample(n_total, n_train)
D_train <- D[list_train,]
D_test <- D[-list_train,]
y_train <- D_train$classLabel
y_test <- D_test$classLabel

# Step 3. Benchmark: train a single decision tree using rpart
M_rpart1 <- rpart(classLabel~., data = D_train)
print('show the summary of the trained model')
summary(M_rpart1)

# Compute the performance on the training and test data sets
y_test_pred_rpart1 <- predict(M_rpart1, D_test, type='class')
accuracy_test_rpart1 <- sum(y_test==y_test_pred_rpart1) / n_test
msg <- paste0('accuracy_test_rpart1 = ', accuracy_test_rpart1)
print(msg)

y_train_pred_rpart1 <- predict(M_rpart1, D_train, type='class')
accuracy_train_rpart1 <- sum(y_train==y_train_pred_rpart1) / n_train
msg <- paste0('accuracy_train_rpart1 = ', accuracy_train_rpart1)
print(msg)

# Step 4. Train a simple AdaBoost model
maxdepth <- 4
mfinal <- 60
M_AdaBoost1 <- boosting(classLabel~., data = D_train, 
            boos = FALSE, mfinal = mfinal, coeflearn = 'Breiman',
            control=rpart.control(maxdepth=maxdepth))

# Check the summary of the trained AdaBoost model
summary(M_AdaBoost1)
# We get all trees
M_AdaBoost1$trees
# print the 1st tree
M_AdaBoost1$trees[[1]]
# We get the weights for all trees
M_AdaBoost1$weights
# We get the variable importance
M_AdaBoost1$importance
# We get the evolution of the error
errorevol(M_AdaBoost1, D_train)
# Check the first tree trained in AdaBoost
t1 <- M_AdaBoost1$trees[[1]]
# Plot the tree
plot(t1, uniform=T, branch=0, margin=0.1, main = 'classification tree')
text(t1, splits=T, all = T, fancy = T)

# Compute the accuracy of AdaBoost model on the training and test data sets
y_test_pred_AdaBoost1 <- predict(M_AdaBoost1, D_test)
accuracy_test_AdaBoost1 <- sum(y_test==y_test_pred_AdaBoost1$class) / n_test
msg <- paste0('accuracy_test_AdaBoost1 = ', accuracy_test_AdaBoost1)
print(msg)

y_train_pred_AdaBoost1 <- predict(M_AdaBoost1, D_train)
accuracy_train_AdaBoost1 <- sum(y_train==y_train_pred_AdaBoost1$class) / n_train
msg <- paste0('accuracy_train_AdaBoost1 = ', accuracy_train_AdaBoost1)
print(msg)

在第3步,我们使用rpart包构建一个决策树的分类模型以与AdaBoost相比较。

在第4步,我们构建了一个AdaBoost模型。

M_AdaBoost1 <- boosting(classLabel~., data = D_train, 
            boos = FALSE, mfinal = mfinal, coeflearn = 'Breiman',
            control=rpart.control(maxdepth=maxdepth))

在该步骤中,指定参数boosFALSE,表示我们要使用所有的样本;将参数mfinal设为60,表示我们将构建60棵决策树。此外,我们还将control设为rpart.control(maxdepth=maxdepth),表示我们使用rpart包构建决策树时将rpart函数中的控制参数maxdepth设为4。

这样我们就得到了一个称为M_AdaBoost1boosting对象。我们使用summary函数来输出该AdaBoost模型的主要信息。此外,我们可以使用M_AdaBoost1中的如下成员来查看该boosting对象的相关信息。

在上面的程序中,我们使用t1来保存M_AdaBoost1中的第一棵决策树,并使用plottext函数绘出t1所对应的决策树的图像,如图9-4所示。

图9-4 AdaBoost模型中所得的第一棵树

程序的输出如下:

[1] "accuracy_test_rpart1 = 0.693548387096774"
[1] "accuracy_train_rpart1 = 0.876712328767123"
[1] "accuracy_test_AdaBoost1 = 0.854838709677419"
[1] "accuracy_train_AdaBoost1 = 1"

> summary(M_AdaBoost1)
      Length Class  Mode   
formula   3  formula call   
trees    60  -none- list   
weights   60  -none- numeric 
votes   292  -none- numeric 
prob    292  -none- numeric 
class   146  -none- character
importance 60  -none- numeric 
terms    3  terms  call   
call     7  -none- call   
> M_AdaBoost1$trees[[1]]
n= 146 

node), split, n, loss, yval, (yprob)
   * denotes terminal node

 1) root 146 0.486301400 M (0.52737671 0.47262329) 
  2) V11>=0.17885 85 0.143835600 M (0.76299475 0.23700525) 
   4) V17< 0.6618 61 0.047945210 M (0.89069716 0.10930284) 
    8) V6< 0.17975 54 0.020547950 M (0.94725111 0.05274889) *
    9) V6>=0.17975 7 0.020547950 R (0.44204322 0.55795678) *
   5) V17>=0.6618 24 0.068493150 R (0.43004587 0.56995413) 
   10) V51>=0.0131 12 0.020547950 M (0.76013514 0.23986486) *
   11) V51< 0.0131 12 0.006849315 R (0.08761682 0.91238318) *
  3) V11< 0.17885 61 0.075342470 R (0.18857143 0.81142857) 
   6) V5>=0.0696 15 0.041095890 M (0.61307902 0.38692098) *
   7) V5< 0.0696 46 0.013698630 R (0.04581552 0.95418448) *
> M_AdaBoost1$weights
 [1] 0.9808293 1.3074799 0.9044111 0.9944090 0.9292653 0.7900282 1.1807839 1.0509344
 [9] 0.9620091 0.8103219 0.8708190 0.8113213 0.7464254 1.3198593 1.1971926 0.9290395
[17] 1.0168932 0.9753769 0.9764947 1.1010859 1.0100822 1.0342562 1.0733267 0.7857949
[25] 1.2606899 0.8798757 0.9931865 0.9910637 1.0070627 1.0809842 1.2443802 1.0938129
[33] 0.8468769 1.0964881 1.2147754 1.1274905 1.0866019 1.1412484 0.9799360 1.0320253
[41] 1.1127173 0.9088232 0.9389669 0.9261423 0.9555823 0.9837087 0.9924242 0.8690865
[49] 0.9102623 0.9291862 0.9977138 1.1003268 0.8939846 0.9973063 0.8033537 1.1148003
[57] 0.9812458 0.9828331 0.9007920 1.3148560
> M_AdaBoost1$importance
    V1    V10    V11    V12    V13    V14    V15    V16
1.4812561 1.5426775 7.8247186 5.3938873 1.5469844 1.1158881 0.4165955 0.8154501
   V17    V18    V19    V2    V20    V21    V22    V23
2.1841351 0.0000000 0.2783136 0.3078752 0.9507150 1.3128143 1.1018555 2.7719996
   V24    V25    V26    V27    V28    V29    V3    V30
0.1851123 0.0000000 1.0870610 4.5926235 2.0182482 0.0000000 0.2588854 0.0000000
   V31    V32    V33    V34    V35    V36    V37    V38
0.8016263 0.4935591 0.5347451 3.7400880 1.3330086 6.4139607 1.1847926 0.2944026
   V39    V4    V40    V41    V42    V43    V44    V45
0.4898486 2.7445508 0.0000000 0.4289211 1.9320352 3.1744688 1.3742040 5.4004112
   V46    V47    V48    V49    V5    V50    V51    V52
3.5877499 0.3224826 3.2149998 1.7710574 0.9377694 0.2963930 2.1314830 2.6108785
   V53    V54    V55    V56    V57    V58    V59    V6
0.7701156 1.0362824 2.5357311 0.6760309 1.2456797 1.7136457 1.6977423 1.6227034
   V60    V7    V8    V9 
1.5132980 1.1750007 1.3802089 2.2330285

在上面的程序中,我们比较了rpart所构建的决策树模型和AdaBoost所构建的分类模型在测试集和训练集上的准确率。从上面的输出可以看出,与rpart所构建的单个决策树相比,AdaBoost所构建的模型在训练集上的准确率从0.877提高到了1.000;同时,在测试集上的准确率从0.694提高到了0.855。这里我们为了显示结果的方便,在使用AdaBoost时只构建了60棵决策树。当我们把决策树的数目提高到100时,AdaBoost所得的模型在测试集上的准确率会进一步提高到0.887,当决策树数目提高到200时,测试集上的准确率会提高到0.903。

与bagging相比,boosting更加激进地从训练集中提取信息来训练模型。因此,boosting更容易受到噪声的影响,更容易导致过拟合。举一个简单的例子,如果我们在训练集中将某一样本的类标标错了,那么该样本有可能成为训练集中较难分类的样本,导致我们在使用boosting的过程中增大了该样本的权重,从而使得训练所得的分类器受到了错误的影响。此外,由于在boosting中我们要顺次构建模型,在算法的实现中不如容易并行处理的bagging速度快。

在本节中,我们着重介绍提升决策树以及对应的梯度提升算法,并介绍R中相应的gbm包。

在boosting中构建每个新的分类器时,我们希望它能够弥补以前模型的“弱点”。这里的“弱点”可以以多种形式表现出来。在前面讨论的AdaBoost算法中,我们给每个样本赋予一个权值,而那些用现有模型难以分类的样本则被赋予较高的权值,从而突出前面分类器的“弱点”。在本节介绍的梯度提升算法中,我们每次根据已有的分类情况,构建新的目标值以更好地分类那些“难分类”的样本。具体来说,对于样本,在构建第j个学习器时,我们用来表示对应的目标值(注意,开始时)。通过不停地改变,使得聚合后的分类器能够更好地分类样本

在提升决策树中,我们假设每个弱学习器都是一棵决策树。我们知道AdaBoost算法对应指数损失函数,而在提升决策树中,我们可以采用不同的损失函数。与机器学习中的很多算法类似,在提升决策树中,给定训练集,我们最小化模型对应的损失函数L(y, f )来求解最优的模型:

(9-38)

在提升决策树中,我们假设表示为一系列决策树的和:

(9-39)

这里我们把第j棵决策树对应的函数记为。将第步所得的模型记为

梯度提升算法中,我们顺次建立模型。该算法的核心思想是在第步构建模型时,采用负梯度作为的新目标值来训练模型。从数值最优化的角度讲,这种策略近似于数值最优化中的梯度下降算法。下面我们从平方和损失函数对应的残差和数值最优化的角度阐述使用负梯度值作为新目标值的合理性。

我们首先讨论平方和损失函数对应的残差。假设在第j步,我们已得到模型,可以计算残差(residual)。残差表示了真实值和当前的预测值之间的差异。如果能使得差异为0,则意味着能得到极好的模型。因此,一种朴素的想法是不妨将残差作为下一个模型中对应的新目标值。

下面我们从数值优化的角度来说明使用残差作为新目标值的合理性。考虑使用平方和损失函数来度量训练集上的损失:

(9-40)

我们要优化使得损失函数最小。注意,这里要优化的对象是一个函数。换言之,我们需要在函数空间中求解该优化问题。如果只考虑训练集,为了简化讨论,可以将考虑为一个n维向量。这样的话,我们需要在n维向量空间中寻找使得损失函数最小的向量f

这样我们就可以直接使用数值最优化中的结果。在数值优化中,假设要优化的参数是f(这里,为n维向量),要最小化的目标函数是。在梯度下降算法的每一步,假设f的当前估计为,我们计算其对应的梯度,然后使用如下公式来更新f

(9-41)

当使用平方和损失函数时,可以计算对于f中每个分量的偏导数:

(9-42)

写成向量的形式,负梯度可以表示为:

(9-43)

在第j步更新模型时,的当前值是。此时的负梯度值为:

(9-44)

这里我们把第j步的负梯度记为。可以看出,在使用平方和损失函数的情况下,负梯度等于我们前面讨论的第j步的残差,也是在第j步训练新模型时的目标值。

我们可以将以上的讨论推广到一般情况。对于不同的问题,我们需要选择不同的损失函数(而不局限于平方和损失函数)。对于不同的损失函数,可以计算相应的负梯度,并使用负梯度作为新的目标值来训练新的学习器。在介绍R中的gbm包时,会涉及不同的损失函数。

注意,在梯度提升算法中,采用负导数作为新的目标来构建模型,我们只用到了训练集中的数据来计算导数,并最小化训练集上的损失函数。但是在机器学习中,我们希望得到的模型不但要在训练集上表现优秀,更重要的是能够很好地处理训练集之外的数据(即我们希望所得模型的泛化能力要好)。因此,在实际中,我们要尽量避免在训练集上的过拟合现象。通常我们采用一类函数(在提升决策树中是回归决策树)来拟合负导数,并不要求完美拟合。

算法9-4 梯度提升算法

 

1.得到初始函数,这里

2.for j=1:m

2.1 计算负梯度:

(9-45)

2.2 以作为新的目标,在训练集上构建一个新模型

2.3 求解步长

(9-46)

2.4 将函数f更新为:

(9-47)

3.输出最终的模型:

(9-48)

算法9-4给出了梯度提升算法的具体步骤。在第一步中,我们求解一个恒量c来最小化损失函数作为初始模型

在算法9-4的2.3步,我们还进一步求解最优的步长以更快地最小化损失函数。我们可以把看成一个关于s的函数F(s),找出使得F(s)最小的s。简单的实现包括找出使得导数s。在第8章介绍LambdaMART算法时,我们使用牛顿近似(即二阶泰勒展式)来逼近,这里是函数关于s的一阶导数和二阶导数。注意,是关于s的二次函数,使得其最小的s

注意,如果我们选择的每个模型是决策树的话,则在2.3步求解步长时,可以进一步对新决策树的每个叶结点求解最优的步长以进一步最小化损失函数。我们知道,当使用训练集来训练得到一棵决策树时,每个叶结点都对应了一组训练样本。假设我们的决策树个叶结点,将第l个叶结点所对应的训练样本的集合记为,则可以求出其对应的权重

(9-49)

与之相对应的是,在2.4步更新模型时,我们要为不同的样本赋予不同的权重。具体来说,更新公式如下:

(9-50)

这里函数的定义为:

(9-51)

下面我们讨论实际使用梯度提升算法时的一些技巧。利用这些技巧,可以显著地提高算法的性能,同时能有效地避免过拟合。

从理论上讲,在梯度提升算法中,我们可以无限制地构建新的决策树以优化分类模型的性能。与机器学习中的很多算法类似,如果对模型的复杂度不加以控制的话,则很容易导致过拟合。在梯度提升算法中,主要通过如下途径避免过拟合。

(1)控制学习率;

(2)控制决策树的总数;

(3)控制每棵决策树的大小;

(4)子取样。

下面逐一讨论这些防止过拟合的措施。

1.学习率和决策树的数目

在提升决策树中,我们构建了多棵决策树。从直观上讲,最开始构造的决策树更多地描述了最后所得模型的主要框架;而后面的决策树更多地是考虑那些难分类的样本。因此,我们可以认为应该给前面的决策树更大的权重。而随着更多的决策树的建立,我们应该逐渐降低其权重。具体来说,在建立第j个模型时,我们使用如下的公式来更新模型:

(9-52)

这里参数v满足0<v<1,是新的决策树的权重。如果我们把这个公式和数值最优化中的更新公式相比较,可以把ν看成是沿着负梯度方向移动的步长,因此ν也可以认为是控制了boosting算法的学习率。另外,我们可以将学习率与我们前面反复讨论的模型的正则化相联系。事实上,梯度提升算法的发明者Jerome Friedman在这方面就有相关的讨论,这里我们就不深入讨论了。

在实际使用梯度提升算法时,决策树数目和学习率是相互依赖的。一般而言,较小的ν意味着我们要以较慢的速度去拟合目标函数,需要构建较多的决策树;较大的ν则意味着可以构建较少的决策树。在实际中,较小的学习率一般能够得到更好的模型。举一个简单的例子,将v设置为0.005,在OOB样本中的表现一般来说要显著强于将v设置为0.05时的情况。但是为了得到较好的性能,较小的v值就会要求较多的决策树,这就要求更多的存储空间和计算时间。如果将v从0.05降到0.005,基本上需要多近10倍的计算时间。

在实际使用R中的gbm包时,两个最重要的参数是决策树的数目和学习率,它们在gbm包的gbm函数中分别对应于参数n.treesshrinkage。图9-5显示了在一个回归问题中,当使用不同的shrinkage值时,模型在OOB样本上的RMSE随着n.trees的增长而发生的变化。从图9-5中可以看出,在保证n.trees足够大的情况下,降低shrinkage的值能够提高模型的性能。当shrinkage降低到一定程度之后,模型性能的提升就不明显了。从图9-5中还可以看出,当我们将shrinkage从0.01降到0.005时,最优的n.trees的值也差不多增加了一倍,但是最优的RMSE只是稍稍减少。因此,一般的经验:在计算时间允许的条件下,设置尽量小的shrinkage值,再设置合适的n.trees的值。我们一般倾向于较小的ν值(v<0.1)。

图9-5 gbm中学习率(shrinkage)和决策树数目(n.trees)的关系(每条曲线对应于一个学习率的值,横坐标是决策树的数目,纵坐标是OOB样本上的RMSE值)

2.决策树的大小

控制决策树的大小可以通过控制决策树的深度或者叶结点的数目来实现。由于在梯度提升算法中我们构建了多棵决策树,因此我们一般倾向于每棵决策树的复杂度不宜太高。例如,我们可以限制每棵决策树的叶结点的数为一恒定值,或者每棵决策树的深度为一恒定值。而这些都可以作为梯度提升算法的参数,可根据实际的数据予以调节。

3.子取样

子取样(subsampling)的思想与bootstrap取样类似。在随机森林中,通过bootstrap取样显著降低了生成的决策树之间的相关性。在梯度提升算法中,也可以采用类似的方法。

具体来说,在使用子取样时,我们从所有的样本中随机取出一部分(但不是重复取样)的样本。这样,每次构建新的决策树时,只通过子取样得到一部分训练样本来构建新的模型。通过使用子取样,能够在构建决策树的时候引入一些随机性,从而能够增强各个模型的多样性,进而提高聚合之后的模型的性能。在R的gbm包中,子取样是通过参数bag.fraction来控制的。在实践中,每次子取样时我们使用一半左右的样本,这在很多时候都被证明是一个行之有效的推荐值。当样本量大时,我们还可以进一步降低每次取样时所得样本的数目。

4.更加完备和实用的梯度提升算法

综合考虑多种防止过拟合的措施,我们将得到更加完备和实用的梯度提升算法。算法9-5列出了在R中常用的gbm包中实现的梯度提升算法。事实上,在实际使用gbm包时,还有更多的输入参数,在算法9-5中我们仅列出了最主要的控制参数及具体步骤。

算法9-5 gbm包中实现的梯度提升算法

 

输入:

  • 损失函数的形式(参数distribution

  • 决策树的数目m(参数n.trees

  • 决策树的叶结点数目K(由参数interaction.depth决定)

  • 学习率v(参数shrinkage

  • 抽样比例p(参数bag.fraction

算法的具体步骤如下。

1.得到初始函数 这里

2.for j=1:m

2.1 计算负梯度:

(9-53)

2.2 从原始数据集中随机选出pn个样本(总样本数为n

2.3 以作为新的目标,使用2.2步中选出的训练集,训练得到一个新的决策树,且该决策树的叶结点数是K

2.4 使用当前选出的训练集,为的每个叶结点求出对应的权重。将第l个叶结点所对应的训练样本的集合记为,则为:

(9-54)

2.5 将函数f更新为:

(9-55)

3.输出最终的模型:

(9-56)

这里我们介绍R中最流行的实现了梯度提升算法的gbm。在gbm包中,采用的是决策树作为基本的弱学习器。利用gbm包中的gbm函数,可以使用梯度提升算法来构建分类、回归和排序模型。gbm函数的主要参数设置如下:

下面我们具体介绍这些参数并给出一些参数设置的建议,并使用实际数据来说明gbm函数的用法。

首先我们要根据问题的类型来决定损失函数的形式。损失函数是我们首先需要确定的参数。在上面的讨论中,我们使用了平方和损失函数来进行推导。事实上,很多其他的损失函数都可以在梯度提升算法中使用。gbm包中提供的适用于分类问题的常用损失函数有:

其中'bernoulli'使用交叉熵损失函数,'adaboost'是前面讨论过的指数损失函数,'huberized'是huberized Hinge损失函数(Hinge损失函数的一种变体)。前面3个都适用于两类分类问题。而'multinomial'对应于多类分类问题的损失函数。一般而言,对于两类分类问题,我们使用'bernoulli''adaboost'就足够了。

对于回归问题,可以将损失函数设为:

其中,'gaussian'表示损失函数是平方和损失函数,'laplace'表示损失函数是预测值与真实值的差的绝对值之和:

(9-57)

这里假设我们的预测值是,而真实值是。当选用'quantile'时,我们使用分位数回归(quantile regression)所对应的损失函数。一般而言,在回归问题中,我们采用'gaussian'可适用于大部分问题。

gbm包还能处理生存分析(survival analysis)。在这种情况下,需要将distribution设为'coxph'。此外,我们也可以使用gbm包来解决排序问题。在排序相关章节我们已经作了详细介绍,这里就不再讨论了。

在默认情况下,如果不指定distribution参数的值,gbm会根据数据的特征来确定distribution的值。如果输入数据的目标变量只有两个不同的值,则认为问题是分类问题,并将distribution的值设为'bernoulli'。如果输入数据的目标变量有多于两个不同的值,gbm再检查输入数据的目标变量的类型:如果是因子类型,就将distribution的值设为'multinomial';如果不是因子类型,gbm自动检查问题是否是生存分析问题;如果都不是的话,将默认问题为回归问题并将distribution的值设为'gaussian'

在决定损失函数后,接下来最重要的参数是学习率(shrinkage)和决策树的数目(n.trees)。一般的经验为:在计算时间允许的条件下,设置尽量小的shrinkage值,再设置合适的n.trees的值。一般来讲,可以将shrinkage的参数设置在0.01~0.001。对应地,可以将n.trees设置在3000至10000之间。

在设定了shrinkage的值后,如何设置决策树的数目?gbm包给出了3种估计最优的决策树的数目的方法,分别为:

核心思想都是利用某一个数据集来估计决策树的数目。具体来说,在gbm包中,我们可以使用gbm.perf函数来计算估计决策树的数目。

对于每棵决策树的大小,gbm中使用参数interaction.depth来控制。该参数指定了每棵决策树中内结点的数目。根据内结点的数目,可以相应地控制叶结点的数目。此外,利用gbm包中的函数pretty.gbm.tree,可以提取gbm所构建的决策树中的某棵决策树的对应信息。

我们提供了文件gbm_classification_example.R,用它介绍如何使用梯度提升算法来解决分类问题。具体的代码如下。

首先我们检查gbm包有没有安装,如果没有安装,那么首先安装它。这里我们使用mlbench包中的PimaIndiansDiabetes数据集,所以也要安装mlbench包。

然后我们导入数据,并检查每列的类型。之后我们将数据按照70%和30%的比例分为训练集和测试集,并分别保存在数据框D_trainD_test中。

# Step 0. Check required package is installed or not. If not, install first.
# 1. gbm
gbm.installed <- 'gbm' %in% rownames(installed.packages())
if (gbm.installed) {
 print("the gbm package is already installed, let's load it...")
}else {
 print("let's install the gbm package first...")
 install.packages('gbm', dependencies=T)
}
library(gbm)
# 2. mlbench
mlbench.installed <- 'mlbench' %in% rownames(installed.packages())
if (mlbench.installed) {
 print("the mlbench package is already installed, let's load it...")
}else {
 print("let's install the mlbench package first...")
 install.packages('mlbench', dependencies=T)
}
library(mlbench)

# Step 1. Load the data
data(PimaIndiansDiabetes2,package='mlbench')
D <- PimaIndiansDiabetes2
y <- D[[ncol(D)]]
y <- as.integer(y) - 1
D[[ncol(D)]] <- NULL
D$classLabel <- y

# Show the type for each col
for(i in 1:ncol(D)) {
 msg <- paste('col ', i, ' and its type is ', class(D[,i]))
 print(msg)
}

# Step 2. Split the data into training and test sets
# Randomly split the whole data set into a training and a test data set
# After spliting, we have the training set: (X_train, y_train)
# and the test data set: (X_test, y_test)
train_ratio <- 0.7
n_total <- nrow(D)
n_train <- round(train_ratio * n_total)
n_test <- n_total - n_train
set.seed(42)
list_train <- sample(n_total, n_train)
D_train <- D[list_train,]
D_test <- D[-list_train,]

y_train <- D_train$classLabel
y_test <- D_test$classLabel

# Step 3. Train several gbm models
# Train a simple gbm model
M_gbm1 <- gbm(classLabel~.,
       data = D_train, 
       distribution = 'bernoulli', 
       shrinkage = 0.01, 
       interaction.depth = 1,
       n.trees = 300, 
       verbose = T)
print('the summary of M_gbm1 is')
print(M_gbm1)

# Expand the gbm model by adding more trees
M_gbm1.1 <- gbm.more(M_gbm1, n.new.trees = 100)
print('the summary of M_gbm1.1 is')
print(M_gbm1.1)

# Train a gbm model using cross-validation
set.seed(1)
M_gbm2 <- gbm(classLabel~.,
       data = D_train, 
       distribution='bernoulli', 
       shrinkage = 0.01,
       n.trees=3000,
       cv.folds = 5,
       verbose=F)
print('the summary of M_gbm2 is')
print(M_gbm2)

# We use gbm.perf to get the estimate of the optimal number of trees using 
# cross-validation
best.iter2 <- gbm.perf(M_gbm2,method = 'cv')
msg <- paste0('the best n.tree is ', best.iter2)
print(msg)
# Show the variable importance using the first best.iter2 trees
varImp2 <- summary(M_gbm2, best.iter2, main = 'variable importance of M_gbm2')
# Show the marginal effect of the selected variables by "integrating" out the 
# other variables
# Here we select the 3rd variable.
plot.gbm(M_gbm2, 3, best.iter2)

# compactly print the first and last trees for curiosity
print('the structure of the 1st tree')
print(pretty.gbm.tree(M_gbm2,1))
print('the structure of the last tree')
print(pretty.gbm.tree(M_gbm2, M_gbm2$n.trees))

# predict the new data using the "best" number of trees
y_test_pred_gbm2 <- predict(M_gbm2, D_test, best.iter2, type = 'response')
print('we are going to print the first few predictions of y_test_pred_gbm2')
print(head(y_test_pred_gbm2))
y_test_pred_gbm2_raw <- predict(M_gbm2, D_test, best.iter2, type = 'link')
print('we are going to print the first few predictions of y_test_pred_gbm2_raw')
print(head(y_test_pred_gbm2_raw))

# Train a more complicated model
M_gbm3 <- gbm(classLabel~.,
       data = D_train, 
       distribution='bernoulli', 
       shrinkage = 0.005, 
       bag.fraction = 0.4, 
       cv.folds = 5,
       interaction.depth = 2,
       n.cores = 4,
       n.trees=3000, 
       verbose=F)
print('the summary of M_gbm3 is')
print(M_gbm3)

# Step 4. Use gbm to do multi-class classification
data(iris)
M_iris <- gbm(Species ~ ., 
       distribution="multinomial", 
       data=iris,
       n.trees=2000, 
       shrinkage=0.01, 
       cv.folds=5,
       verbose=F, 
       n.cores=1)
print('the summary of M_iris is')
print(M_iris)
print('the variable importance of M_iris')
summary(M_iris, main = 'variable importance of M_iris')

在第3步,我们构建了多个gbm模型。由于这里使用的数据对应两类分类问题,因此将distribution设为'bernoulli'。注意,当distribution'bernoulli'时,我们必须保证对应于类标的列的值必须是0或者1。这也是我们在第一步对y进行预处理的原因。使用如下代码我们构建了第一个gbm模型M_gbm1

M_gbm1 <- gbm(classLabel~., data = D_train, distribution = 'bernoulli', 
shrinkage = 0.01, interaction.depth = 1, n.trees = 300, verbose = T)

上述参数的含义我们在前面已经解释过了。我们在这里将参数verbose设为真,表示R将会打印模型构建中的一些信息。然后,我们使用print(M_gbm1)打印出该模型的主要信息,则对应的输出为:

Iter  TrainDeviance  ValidDeviance  StepSize  Improve
   1    1.2772       nan   0.0100  0.0025
   2    1.2722       nan   0.0100  0.0024
   3    1.2675       nan   0.0100  0.0022
   4    1.2624       nan   0.0100  0.0022
   5    1.2580       nan   0.0100  0.0021
   6    1.2534       nan   0.0100  0.0020
   7    1.2480       nan   0.0100  0.0020
   8    1.2439       nan   0.0100  0.0021
   9    1.2401       nan   0.0100  0.0020
  10    1.2361       nan   0.0100  0.0020
  20    1.1983       nan   0.0100  0.0015
  40    1.1375       nan   0.0100  0.0012
  60    1.0962       nan   0.0100  0.0008
  80    1.0626       nan   0.0100  0.0006
  100    1.0338       nan   0.0100  0.0005
  120    1.0106       nan   0.0100  0.0005
  140    0.9901       nan   0.0100  0.0003
  160    0.9720       nan   0.0100  0.0004
  180    0.9552       nan   0.0100  0.0002
  200    0.9410       nan   0.0100  0.0002
  220    0.9279       nan   0.0100  0.0000
  240    0.9169       nan   0.0100  0.0002
  260    0.9069       nan   0.0100  0.0002
  280    0.8969       nan   0.0100  -0.0000
  300    0.8877       nan   0.0100  0.0001

[1] "the summary of M_gbm1 is"
gbm(formula = classLabel ~ ., distribution = "bernoulli", data = D_train, 
  n.trees = 300, interaction.depth = 1, shrinkage = 0.01, verbose = T)
A gradient boosted model with bernoulli loss function.
300 iterations were performed.
There were 8 predictors of which 8 had non-zero influence.
Iter  TrainDeviance  ValidDeviance  StepSize  Improve
  301    0.8874       nan   0.0100  -0.0001
  302    0.8871       nan   0.0100  -0.0001
  303    0.8868       nan   0.0100  -0.0001
  304    0.8863       nan   0.0100  0.0000
  305    0.8859       nan   0.0100  0.0001
  306    0.8855       nan   0.0100  -0.0001
  307    0.8853       nan   0.0100  -0.0001
  308    0.8848       nan   0.0100  0.0001
  309    0.8843       nan   0.0100  0.0000
  310    0.8838       nan   0.0100  0.0000
  320    0.8799       nan   0.0100  0.0001
  340    0.8722       nan   0.0100  -0.0000
  360    0.8658       nan   0.0100  -0.0000
  380    0.8588       nan   0.0100  0.0001
  400    0.8528       nan   0.0100  0.0000

在实际中,如果当前的gbm模型不理想,那么可能要进一步增加决策树的数目。在gbm模型中,我们不必从头开始重新构建gbm模型。我们可以使用gbm.more函数增加当前gbm模型中决策树的数目。

M_gbm1.1 <- gbm.more(M_gbm1, n.new.trees = 100)

这里我们又增加了100棵决策树,而且将新的决策树模型记为M_gbm1.1。同样地,我们也可以使用print函数打印新M_gbm1.1的主要信息。其输出如下:

[1] "the summary of M_gbm1.1 is"
gbm.more(object = M_gbm1, n.new.trees = 100)
A gradient boosted model with bernoulli loss function.
400 iterations were performed.
There were 8 predictors of which 8 had non-zero influence.

接下来我们介绍如何使用gbm.perf函数来估计最优的决策树的数目。首先我们使用交叉检验得到模型M_gbm2

M_gbm2 <- gbm(classLabel~., data = D_train, distribution='bernoulli', shrinkage = 0.01, n.trees=3000, cv.folds = 5, verbose=F)

与前面的不同是,我们将cv.folds设为5,表示在训练中采用5重交叉检验。接下来我们调用gbm.perf就可以得到利用交叉检验得到的最优决策树的数目。

best.iter2 <- gbm.perf(M_gbm2,method = 'cv')

这段代码的输出为:

[1] "the summary of M_gbm2 is"
gbm(formula = classLabel ~ ., distribution = "bernoulli", data = D_train, 
  n.trees = 3000, shrinkage = 0.01, cv.folds = 5, verbose = F)
A gradient boosted model with bernoulli loss function.
3000 iterations were performed.
The best cross-validation iteration was 774.
There were 8 predictors of which 8 had non-zero influence.
[1] "the best n.tree is 774"

可以看出,最优的决策树的数目是774。注意,gbm.perf函数同时也绘出了损失函数在训练集和检验集上随着决策树增加而引起的变化,如图9-6 所示。从图9-6中可以看出,当增加决策树的数目时,训练集对应的损失函数一直在单调降低,而检验集上对应的损失函数先降低再上升。

接下来我们使用summary函数得到gbm模型对应的参数的重要程度。

varImp2 <- summary(M_gbm2, best.iter2, main = 'variable importance of M_gbm2')

该函数除了返回一个数据框外,还绘图显示了各个变量的重要程度,如图9-7所示。

图9-6 gbm中所使用的损失函数随着决策树数目的增加而引起的变化(下面的曲线对应于训练集,上面的曲线对应于检验集,竖直的虚线代表我们选取的最优的决策树的数目的位置)

图9-7 M_gbm2模型中变量的重要程度

接下来我们使用pretty.gbm.tree(M_gbm2, i)来取得gbm模型M_gbm2中第i棵树中变量重要性的信息。下面是第一棵和最后一棵树对应的变量重要信息:

[1] "the structure of the 1st tree"
 SplitVar SplitCodePred LeftNode RightNode MissingNode ErrorReduction Weight
0    1 1.390000e+02    1     2      3    15.45783     269
1    -1 -6.290815e-03    -1    -1     -1    0.00000    191
2    -1 1.780270e-02    -1    -1     -1    0.00000     73
3    -1 3.714074e-04    -1    -1     -1    0.00000     5
   Prediction
0 0.0003714074
1 -0.0062908155
2 0.0178027030
3 0.0003714074
[1] "the structure of the last tree"
 SplitVar SplitCodePred LeftNode RightNode MissingNode ErrorReduction Weight
0    2 87.0000000000    1     2      3   0.3832303    269
1    -1 0.0004975395    -1    -1     -1   0.0000000    229
2    -1 -0.0072377243    -1    -1     -1   0.0000000   27
3    -1 0.0019612510    -1    -1     -1   0.0000000    13
   Prediction
0 -0.0002081254
1 0.0004975395
2 -0.0072377243
3 0.0019612510

函数pretty.gbm.tree返回一个数据框以显示gbm生成的某一棵树的具体结构。在返回的数据框中,每行对应生成的决策树中的一个结点,且结点的编号从0开始。在上面显示的结果中,每行的开头显示了结点的编号。返回的数据框中主要包括8列。

在上面第一个棵树的输出中,我们可以看出有4个结点,标号为0~3,其中第一个结点是根结点(其左子结点为结点1,右子结点为结点2),后面3个结点都是叶结点(SplitVar为−1)。

这里我们要特别说明,在使用predict函数(实质上我们调用的是predict.gbm函数)处理gbm对象时,需要指定以下参数type的值。

R程序中关于这段的输出为:

[1] "we are going to print the first few predictions of y_test_pred_gbm2"
[1] 0.03375870 0.66036940 0.05968122 0.26782885 0.50472148 0.49354606
[1] "we are going to print the first few predictions of y_test_pred_gbm2_raw"
[1] -3.35417558 0.66494083 -2.75720155 -1.00566613 0.01888648 -0.02581718

最后,对于这个两类分类问题,我们构建了一个比较复杂的模型M_gbm3

在第4步中,我们使用gbmiris数据集上为多类分类问题构建了一个模型,主要区别在于将参数distribution设为了"multinomial"。我们也使用summary函数打印变量的重要程度信息,结果如图9-8所示。

图9-8  M_iris模型中变量的重要程度

与AdaBoost相比,在梯度提升算法中,我们通过使用负梯度为新的目标值来构建后继的模型从而处理那些难以分类的样本;而在AdaBoost中,我们直接给每个样本赋予了一个权值,而权值的大小直接反映了下面构建的模型对每个点的重视程度。一般而言,对于一般的数据集,在随机森林中,我们都要构建数百棵决策树。而对于boosting,要取得同样的性能,所需的决策树的数目更多。但是如果仔细选择参数,一般而言,当达到boosting所需的决策树数目后,boosting的性能能够优于随机森林。我们的经验为:对于随机森林的使用,简单的参数设置就能得到较好的性能;而对于boosting,对于很多数据集,我们都需要仔细调节参数才能得到较好的性能。我们推荐读者使用R中的randomForest包和gbm包来学习如何使用随机森林和梯度提升算法,从而获得第一手的使用经验。

在本节中,我们讨论如何将多个学习器的结果聚合以得到更好的结果。在前面的讨论中,我们假设每个基学习器为弱学习器,如在随机森林和提升决策树中的基学习器都是决策树。在本节的讨论中,则不限于弱学习器。特别是在一些注重算法性能的场合,通常我们会构建多个不同类型的学习器。例如,对于一个给定的分类问题,可以构建多个模型,如随机森林、SVM和逻辑回归;然后再将这3个模型的结果聚合以得到最终的模型。在本节中,我们根据方法的复杂程度,从易到难地介绍简单平均、加权平均,并着重讨论stacking。与简单平均和加权平均相比,stacking构建了新的模型来聚合多个模型。在实践中,stacking是一种行之有效的聚合多个模型的方法,如在很多数据挖掘的竞赛中,最终获胜的方案几乎都是使用stacking聚合多个模型得到的。

简单平均是一种看似简单但非常有效的方法。假设我们有m个模型。在回归中,我们可以计算m个模型的平均作为最后的输出:

(9-58)

在分类问题,如果类标为±1,则聚合m个模型之后最后的输出可以表示为:

(9-59)

这里sign是符号函数。在前面的讨论中,随机森林就是使用简单平均来聚合多个弱学习器的。

该方法简单易行。在实际中,很多场合通过使用简单平均就能够显著提高性能。因此,简单平均是最常用的综合多种算法的方法。在很多实际应用中,简单平均是聚合多个模型的第一选择,也是其他复杂聚合方法的第一比较对象。

当然,也存在一些极端情况使得简单平均不能取得较好的性能。例如,如果各个模型相互关联且关联度很高,则简单平均不易取得较好的性能。一个简单的极端例子:

(9-60)

在这种情况下,使用简单平均后所得的模型与原来的m个模型相比,性能没有任何提高。

加权平均是简单平均的进一步扩展。在加权平均中,我们使用如下公式来聚合多个分类器:

(9-61)

这里是第j个模型的权重。事实上,简单平均是加权平均的特例,只需要将权重设为:

(9-62)

在一些应用中,我们还可以对添加新的约束条件。例如,可以要求满足如下约束条件:

(9-63)

在前面的讨论中,在boosting中一般使用加权平均。例如,在AdaBoost中,我们根据每个学习器的错误率来决定其相应的权重。在梯度提升算法中,权重则是由学习率决定的。

加权平均的表达能力更强,但是更容易导致过拟合。很多实验结果都证实加权平均并不一定优于简单平均。一般而言,对于相似的模型或者性能(如准确率)相近的模型,我们倾向于简单平均;如果模型间的差异较大(.包括性能差异较大的情况),我们更倾向于使用加权平均。

与前面讨论的简单平均和加权平均相比,stacking是更加通用的聚合多个学习器的方法。

与前面介绍的简单平均和加权平均类似,在stacking中,我们也采用两层的结构来将多个模型聚合。其具体结构如图9-9所示。在stacking的第一层中,通常构建多个学习器,并将这一层的学习器称为第一层学习器(first-level learner)。在得到第一层学习器后,我们将这一层的输出作为新的“特征”,重新训练一个学习器,称为第二层学习器(second-level learner)。简而言之,stacking就是将已有的学习器的输出作为新的特征,再训练一个学习器以进一步优化性能。在stacking的第二层中,我们可以选择多种不同的算法,如一些非线性算法(如人工神经网络等)。但在stacking的实践中,在第二层采用复杂的模型很容易引起过拟合。因此,很多时候,在第二层中我们一般倾向于比较简单的模型,如线性回归。

在这里,我们要着重指出,在stacking中第一层的学习器并不局限于弱学习器。事实上,在采用stacking时,第一层学习器一般都是性能较好的模型,如随机森林等。为了尽量提高第一层学习器的性能,我们要求第一层学习器之间存在多样性。

图9-9 stacking中的第一层学习器和第二层学习器

在使用stacking时,特别需要注意的是,在训练第二层学习器的时候不要使用第一层学习器已经使用过的训练数据。如图9-10所示,假设我们使用训练集S1来训练第一层学习器。在得到第一层的模型后,我们计算这些模型在新的训练集S2上的输出,并将这些输出作为新的特征。利用新的训练集S2,我们训练第二层学习器。得到所有的第一层学习器和第二层学习器后,我们可以得到最后的模型在测试集T上的输出,并作为最终输出。注意,如果S1S2是同一数据集的话,那么在实际中很难避免过拟合。

图9-10 训练数据和测试数据的划分

为了充分利用训练集中的信息,我们还可以在stacking中使用交叉检验。具体来说,我们可将整个训练集S等分成k个子集:S1S2,…,Sk,并记的补,如图9-11所示。

图9-11 使用交叉检验来应用stacking时训练数据和测试数据的划分

这里我们考虑建立m个不同的模型。对于每个模型,我们使用如下流程来得到在整个训练集上的输出。对于,我们使用作为训练集,使用作为测试集。我们将这样训练得到的模型记为。这样,对于测试集中的每个样本,我们可以计算对应的输出。具体来说,对于,我们记。这样,当p值从1增长到kj从1增长到m后,交叉检验结束,我们将得到m个模型在整个训练集上的输出,记为

(9-64)

这里是样本对应的类标或者目标值。

利用这个新的训练集,我们可以构建第二层学习器h。当得到第二层学习器后,我们重新回到第一层学习器:利用整个训练集S重新训练所有的第一层学习器,然后利用刚才得到的第二层学习器h得到最终的结果。

在回归问题中,我们可以直接将第一层模型的输出作为特征。在分类问题中,我们既可以将第一层模型的输出作为特征,也可以将分类对应的概率作为特征(如果第一层模型可以输出概率的话,如逻辑回归)。

为何stacking能够提高第一层学习器的性能?与前面的bagging类似,stacking也利用不同学习器之间存在的多样性。这样第一层学习器之间就能够取长补短,有效提高学习器的综合性能。因此,我们一般在第一层构建不同类型的模型。或者,在构建每个第一层学习器时,我们每次都是用不同的数据集,如使用不同样本的子集,或者不同特征的子集。

但是与bagging和boosting相比,在实际使用中stacking更容易导致过拟合。当stacking使用不当时,其性能甚至低于简单平均。在大多数情况下,在stacking中我们一般不推荐使用特别复杂的第二层模型。事实上,线性回归在很多时候就能够有效提高第一层学习器的性能.。

在实际使用集成学习时,bagging方法最终得到的模型几乎总是比单个模型效果要好。一般来讲,boosting比bagging对噪声更敏感,更易于过拟合。因此,在实际使用boosting时,要仔细调节参数以避免过拟合。通过选择合适的参数,boosting的效果可以明显好于bagging和单个模型。另外,bagging和boosting主要是聚合弱学习器,而stacking主要是聚合一些不那么“弱”的学习器。一般来讲,stacking适合在建模的最后阶段聚合已有的多个模型。

在集成学习中,一个很重要的概念是随机性。在随机森林中,我们通过bootstrap取样和在特征空间随机选择特征子集以引入随机性。在梯度上升算法中,我们也通过子取样来引入随机性。事实上,在stacking中,也可以采用类似的方法来引入随机性。为什么随机性重要呢?原因是在很多机器学习算法中,我们都是通过贪心学习(greedy learning)的方法从数据中学习模型的参数。贪心学习的好处是可以很快地收敛到局部最优点,坏处就是容易导致过拟合。因此,在集成学习中,通过引入随机性,我们希望能够消灭或者降低贪心学习带来的过拟合风险。

https://studio.azureml.net/

该模块是Permutation Feature Importance模块。

https://cran.r-project.org/web/packages/randomForest/index.html

https://cran.r-project.org/web/packages/adabag/index.html

本图来源于:Greg Ridgeway, Generalized Boosted Models: A guide to the gbm package。

https://cran.r-project.org/web/packages/gbm/index.html


相关图书

ChatGPT原理与应用开发
ChatGPT原理与应用开发
动手学机器学习
动手学机器学习
机器学习与数据挖掘
机器学习与数据挖掘
机器学习公式详解 第2版
机器学习公式详解 第2版
自然语言处理迁移学习实战
自然语言处理迁移学习实战
AI医学图像处理(基于Python语言的Dragonfly)
AI医学图像处理(基于Python语言的Dragonfly)

相关文章

相关课程