【白话机器学习】算法理论+实战之Xgboost算法
1. 写在前面
如果想从事数据挖掘或者机器学习的工作,掌握常用的机器学习算法是非常有必要的,在这简单的先捋一捋, 常见的机器学习算法:
这个系列已经基本包含了上面这些算法的原理和基本使用。但是,如果仅仅是会用这些算法可是不够的, 我们也得跟着时代的步伐前进,近几年,有很多大佬又在上面的某些算法上加以改进,发明了更加厉害的算法,而这些算法才是当今时代解决问题的主流,所以我们学习的一个方式就是掌握传统,而又得紧跟时代。
所以,后面考虑加上当前流行的一些主流机器学习算法,既当复习,又当提升。由于不想和传统的机器学习算法混合起来,故称之为番外,也是传统机器学习算法的延伸, 同样是尽量白话,同样是丰富实战,但会夹杂数学的身影,毕竟后面的很多算法如果没有了数学就仿佛失去了灵魂,无法活灵活现。所以机器学习算法的故事还没有完,我们还得继续走着。
学习算法的过程,获得的不应该只有算法理论,还应该有乐趣和解决实际问题的能力!
今天分享的这个算法堪称数据科学竞赛界的神器, 它似乎是用于赢得数据科学竞赛的分类器/预测器必不可少的算法, 那就是。听这个名字,你可能一下就想到了传统机器学习算法里面的,哈哈,联想和对比才能更加理解算法的精华。你还别说,这个算法和那个来自于同一个家族,都是集成学习算法,都属于流派,但是两者的采用了不同的策略,而就是这策略的不同,导致成了目前竞赛者眼中的红人,它是目前最快最好的开源 tree 工具包,比常见的工具包快 10 倍以上, 那么到底采用了什么策略呢?它又是如何做到高准确率和高速度的呢?和到底有什么不同呢?又如何来解决实际问题呢? 这些问题,在这篇文章中都会一一来解剖。
大纲如下:
Ok, let's go!
2. ? 这个故事还得先从和GBDT说起
我觉得,学习一个算法的时候,有时候不能直接单拿出一个算法来说,这样感觉显得突兀了些,不知道突然从哪冒出来一样。所以,讲之前,我想先带你回顾一下我们之前的集成学习。
所谓集成学习,就是指构建多个弱分类器对数据集进行预测,然后用某种策略将多个分类器预测的结果集成起来,作为最终预测结果。在这里就不再讲了(可以理解成集成学习是一种把大家伙叫到一块,集思广益想办法解决问题的方式吧),在这里想说的是集成学习的那两大流派:和。
怎么还有两个流派呢?集思广益不就完事?哈哈, 集思广益也有不同的方式吗?比如针对同一个问题,把问题划分成不相干的子问题,然后分派给不同的人各干各的是一种, 或者同一个问题,划分成串行的子问题, 先由一个人解决一部分,解决不了的,后面的人再来这又是一种。把上面这两种方式用官方的语言描述就是:根据各个弱分类器之间有无依赖关系,分为和。
关于流派的 (随机森林)算法,也是比较常用的,简单的说就是各个弱分类器是独立的、每个分类器在样本堆里随机选一批样本,随机选一批特征进行独立训练,各个分类器之间没有啥关系, 最后投票表决, 这个在这里不做详述,后面遇到的时候再统一总结,今天的主角是,所以我们主要是了解一下流派, 这这里面的最具代表性的算法之一就是, 这个我这里不做过多的表述,详细的可以看一下专栏之前的文章, 这里只回顾一下它的算法原理,这样好引出后面的GBDT和,并且可以进行策略上的对比。
,是英文" "(自适应增强),它的自适应在于:前一个基本分类器分错的样本会得到加强,加权后的全体样本再次被用来训练下一个基本分类器。同时,在每一轮中加入一个新的弱分类器,直到达到某个预定的足够小的错误率或达到预先指定的最大迭代次数。白话的讲,就是它在训练弱分类器之前,会给每个样本一个权重,训练完了一个分类器,就会调整样本的权重,前一个分类器分错的样本权重会加大,这样后面再训练分类器的时候,就会更加注重前面分错的样本, 然后一步一步的训练出很多个弱分类器,最后,根据弱分类器的表现给它们加上权重,组合成一个强大的分类器,就足可以应付整个数据集了。 这就是, 它强调自适应,不断修改样本权重, 不断加入弱分类器进行。
那么,还有没有别的方式呢?GBDT( Boost Tree)就是另一种的方式, 上面说到训练弱分类器关注的是那些被分错的样本,每一次训练都是为了减少错误分类的样本。而GBDT训练弱分类器关注的是残差,也就是上一个弱分类器的表现与完美答案之间的差距,GBDT每一次训练分类器,都是为了减少这个差距,GBDT每一次的计算是都为了减少上一次的残差,进而在残差减少(负梯度)的方向上建立一个新的模型。这是什么意思呢? 我可以举个例子,假设我们去银行借钱,我们想让一个决策树系统来预测可以借给我们多少钱, 如果标准答案是1000的话,假设第一棵决策树预测,可以借给我们950块钱, 那么离标准答案的1000还差50, 效果不算好,能不能提高一些呢?我们就再加一棵决策树,这课决策树过来之后,看到前面的那个已经预测到950了,只是差50, 那么我可以聚焦在这个50上,把这个残差变得再小一些,所以第二个决策树预测结果是30, 那么前两棵决策树预测结果结合起来是980, 离标准答案差20, 所以加了一棵树之后,效果好了。那么还能不能提升呢?我再来一棵树,发现残差只有20了,那我把残差变得再小, 结果第三个决策树预测20, 那么这三棵树就可以正确的预测最终的1000了。
所以GBDT就是这样的一个学习方式了,GBDT是集成学习,集成学习由多个相关联的决策树联合决策, 什么是相关联?就是我上面的例子:
有一个样本[数据->标签]是:[(,,)-> 1000块]第一棵决策树用这个样本训练的预测为950那么第二棵决策树训练时的输入,这个样本就变成了:[(,,)->50]第二棵决策树用这个样本训练的预测为30那么第三棵决策树训练时的输入,这个样本就变成了:[(,,)->20]第三棵决策树用这个样本训练的预测为20
搞定,也就是说,下一棵决策树输入样本会与前面决策树的训练和预测相关。用个图来表示类似这样:
这就是GBDT的工作原理了, GBDT是旨在不断减少残差(回归),通过不断加入新的树旨在在残差减少(负梯度)的方向上建立一个新的模型。——即损失函数是旨在最快速度降低残差。
那么为啥要讲GBDT呢? 我先卖个关子,不妨先看一下是怎么解决问题的。这里用原作者陈天奇的讲座PPT中的那个图来看
假设我想预测,这一家子人中每个人想玩游戏的意愿值。我们用解决这个问题,就是我先训练出来第一棵决策树, 预测了一下小男孩想玩游戏的意愿是2, 然后发现离标准答案差一些,又训练出来了第二棵决策树, 预测了一下小男孩想玩游戏的意愿是0.9, 那么两个相加就是最终的答案2.9。这个其实就接近了标准答案。所以是训练出来的弱分类结果进行累加就是最终的结论。
恩,你可能要拍案而起了,惊呼,这不是跟上面介绍的GBDT乃异曲同工么?事实上,如果不考虑工程实现、解决问题上的一些差异,与gbdt比较大的不同就是目标函数的定义,但这俩在策略上是类似的,都是聚焦残差(更准确的说, 其实是gbdt算法在工程上的一种实现方式),GBDT旨在通过不断加入新的树最快速度降低残差,而则可以人为定义损失函数(可以是最小平方差、 loss 、hinge loss 或者人为定义的loss ),只需要知道该loss 对参数的一阶、二阶导数便可以进行,其进一步增大了模型的泛化能力,其贪婪法寻找添加树的结构以及loss 中的损失函数与正则项等一系列策略也使得预测更准确。
所以,这就是我讲的故事之前,要简单说一下和GBDT的原因了,这样脑海里面是不是对不那么陌生了啊,你要知道,这三个同是属于集成学习的流派,叫做自适应提升,和GBDT,提升时采用的策略不同,前者聚焦错误样本,后者聚焦与标准答案的残差。而GBDT和叫做集成学习,提升时策略类似,都是聚焦残差,但是降低残差的方式又各有不同。
好了,铺垫到此为止,下面真正进入主角部分 -- 的基本原理。
3. 的基本原理
的全称是 ,由华盛顿大学的陈天奇博士提出,在的希格斯子信号识别竞赛中使用,因其出众的效率与较高的预测准确度而引起了广泛的关注。
如果算法每一步的弱分类器生成都是依据损失函数的梯度方向,则称之为梯度提升( ),算法是采用分步前向加性模型,只不过在每次迭代中生成弱学习器后不再需要计算一个系数, 是由 k 个基模型组成的一个加法运算式:
\hat{y}_{i}=\sum_{t=1}^{k} f_{t}\left(x_{i}\right)
算法通过优化结构化损失函数(加入了正则项的损失函数,可以起到降低过拟合的风险)来实现弱学习器的生成,并且算法没有采用搜索方法,而是直接利用了损失函数的一阶导数和二阶导数值,并通过预排序、加权分位数等技术来大大提高了算法的性能。”
如此白话,应该能听懂了吧, 但还没真正讲的数学原理呢, 所以后面的数学原理我打算换一种方式,从一个例子展开,剖析数学公式,这里面就全是数学的原理了,如果你感觉直接上数学压力有点大,那么可以先跟着我继续往下,从一个例子中看看树到底是如何生成的,然后再回头看数学原理也不迟 ;)
下面就通过算法流程图举一个例子来详解树的生成。
先给出一个流程图, 不懂不要紧,可以看一遍后面的步骤,然后再回来:
为了让数学原理的部分不那么, 我们跟着一个例子走吧:
假设我想预测学生考试分数, 给定若干个学生属性(比如天赋,每天学习时间,是否谈恋爱等),
后面依次类推,还可能有更多的决策树通过学生的某些属性来推断分数。就是这样一个不断生成新的决策树A,B,C,D…的算法,最终生成的决策树算法就是树A+B+C+D+…的和的决策树。”
我们针对这个问题看看详细的建树过程吧:
首先,我们有三个学生, 属性和标签如下:
我们初始化三个样本的考试成绩预测值为0。
我们有了第一棵树, 通过这个树的预测结果:
那么我们建立第二棵树的时候,我们是考虑的残差,也就是样本其实变成了下面这样:
通过最小化残差学习到一个通过学习时间属性构建的决策树得到了90+5,60+5,90-5的预测值,再继续通过(100-95=5)(70-65)(86-85)的残差构建下一个决策树,以此类推,当迭代次数达到上限或是残差不再减小是停止,就得到一个拥有多个(迭代次数)决策树的强分类器。这个就是工作的宏观过程了。光宏观部分确实挺好理解,但具体细节呢?比如我每一次建树是怎么建的呢?既然说计算收益的方式不同, 那么我考虑分裂的时候是怎么计算收益的呢?目前你心中肯定会有这些疑问, 莫慌莫慌, 下面我把建树的细节给你娓娓道来,不过道来的过程中得崎岖一点,需要用到数学的语言。那么究竟是如何得到一棵新的树的呢?下面就是的精髓了。前方高能,一大波数学公式来袭, 请戴好安全帽!!!
目标函数的化简:这是的精髓了。我们首先看一个关于目标函数的简单等式变换, 把上面的目标函数拿过来:\begin{}O b j^{(t)} &=\sum_{i=1}^{n} l\left(y_{i}, \hat{y}_{i}^{t}\right)+\sum_{i=1}^{t} \Omega\left(f_{i}\right) \\&=\sum_{i=1}^{n} l\left(y_{i}, \hat{y}_{i}^{t-1}+f_{t}\left(x_{i}\right)\right)+\sum_{i=1}^{t} \Omega\left(f_{i}\right)\end{}
我们看看这里的
l\left(y_{i}, \hat{y}_{i}^{t-1}+f_{t}\left(x_{i}\right)\right)
,这个部分,如果结合伟大的的话,会发生什么情况,你还记得泰勒吗?
在这里插入图片描述”
这里我们以平方损失函数为例:”
\sum_{i=1}^{n}\left(y_{i}-\left(\hat{y}_{i}^{t-1}+f_{t}\left(x_{i}\right)\right)\right)^{2}
则对于每一个样本:”
\begin{}&g_{i}=\frac{\\left(\hat{y}^{t-1}-y_{i}\right)^{2}}{\ \hat{y}^{t-1}}=2\left(\hat{y}^{t-1}-y_{i}\right)\\&h_{i}=\frac{\^{2}\left(\hat{y}^{t-1}-y_{i}\right)^{2}}{\hat{y}^{t-1}}=2\end{}
基于决策树的目标函数的终极化简上面的目标函数先放下来:\{Obj}^{(t)} \ \sum_{i=1}^{n}\left[g_{i} f_{t}\left(x_{i}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(x_{i}\right)\right]+\sum_{i=1}^{t} \Omega\left(f_{i}\right)
\sum_{i=1}^{n}\left[g_{i} f_{t}\left(x_{i}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(x_{i}\right)\right] = \sum_{i=1}^{n}\left[g_{i} w_{q\left(x_{i}\right)}+\frac{1}{2} h_{i} w_{q\left(x_{i}\right)}^{2}\right] = \sum_{j=1}^{T}\left[\left(\sum_{i \in I_{j}} g_{i}\right) w_{j}+\frac{1}{2}\left(\sum_{i \in I_{j}} h_{i}\right) w_{j}^{2}\right]
\Omega\left(f_{t}\right)=\gamma T+\frac{1}{2} \ \sum_{j=1}^{T} w_{j}^{2}
这张图给出了基于决策树的 的正则项的求解方式。
w_{j}^{*}=-\frac{G_{j}}{H_{j}+\}
那么这个目标函数又可以进行化简:
ob j=-\frac{1}{2} \sum_{j=1}^{T} \frac{G_{j}^{2}}{H_{j}+\}+\gamma T
这个就是基于决策树的模型的目标函数最终版本了,这里的G和H的求法,就需要明确的给出损失函数来, 然后求一阶导和二阶导,然后代入样本值即得出。
上面是可以判断出来一棵树究竟好不好,那么建立树的时候应该怎么建立呢?一棵树的结构近乎无限多,总不能一个一个去测算它们的好坏程度,然后再取最好的吧(这是个NP问题)。所以,我们仍然需要采取一点策略,这就是逐步学习出最佳的树结构。这与我们将K棵树的模型分解成一棵一棵树来学习是一个道理,只不过从一棵一棵树变成了一层一层节点而已。这叫什么?emmm, 贪心(找到每一步最优的分裂结果)!采用二叉树, 开始的时候, 全部样本在一个叶子节点上, 然后叶子节点不断通过二分裂,逐渐生成一棵树。那么在叶子节点分裂成树的过程中最关键的一个问题就是应该在哪个特征的哪个点上进行分裂,也就是寻找最优切分点的过程。
最优切分点划分算法及优化策略在决策树的生长过程中,一个非常关键的问题是如何找到节点的最优切分点, 我们学过了决策树的建树过程,那么我们知道ID3也好,C4.5或者是CART,它们寻找最优切分点的时候都有一个计算收益的东西,分别是信息增益,信息增益比和基尼系数。而这里的切分, 其实也有一个类似于这三个的东西来计算每个特征点上分裂之后的收益。假设我们在某一节点完成特征分裂,则分列前的目标函数可以写为:O b j_{1}=-\frac{1}{2}\left[\frac{\left(G_{L}+G_{R}\right)^{2}}{H_{L}+H_{R}+\}\right]+\gamma
分裂后的目标函数:
O b j_{2}=-\frac{1}{2}\left[\frac{G_{L}^{2}}{H_{L}+\}+\frac{G_{R}^{2}}{H_{R}+\}\right]+2 \gamma
则对于目标函数来说,分裂后的收益为(Obj1-Obj2):”
\text {Gain}=\frac{1}{2}\left[\frac{G_{L}^{2}}{H_{L}+\}+\frac{G_{R}^{2}}{H_{R}+\}-\frac{\left(G_{L}+G_{R}\right)^{2}}{H_{L}+H_{R}+\}\right]-\gamma
注意该特征收益也可作为特征重要性输出的重要依据。那么我们就可以来梳理一下最优切分点的划分算法了:
上面就是最优切分点划分算法的过程, 看完之后,是不是依然懵逼, 这到底是怎么做的啊, 下面就看一个寻找最优切分点的栗子吧:还是上面玩游戏的那个例子,假设我有这一家子人样本,每个人有性别,年龄,兴趣等几个特征,我想用建立一棵树预测玩游戏的意愿值。首先,五个人都聚集在根节点上,现在就考虑根节点分叉,我们就遍历每个特征,对于当前的特征,我们要去寻找最优切分点以及带来的最大收益,比如当前特征是年龄,我们需要知道两点:* 按照年龄分是否有效,也就是是否减少了obj的值 * 如果真的可以分,特征收益比较大, 那么我们从哪个年龄点分开呢?
对于这两个问题,我们可以这样做,首先我们先把年龄进行一个排序, 如下图:
这就是贪心建树的一个思路了,即遍历所有特征以及所有分割点,每次选最好的那个。GBDT也是采用的这种方式, 这算法的确不错,但是有个问题你发现了没?就是计算代价太大了,尤其是数据量很大,分割点很多的时候,计算起来非常复杂并且也无法读入内存进行计算。所以作者想到了一种近似分割的方式(可以理解为分割点分桶的思路),选出一些候选的分裂点,然后再遍历这些较少的分裂点来找到最佳分裂点。那么怎么进行分桶选候选分裂点才比较合理呢?我们一般的思路可能是根据特征值的大小直接进行等宽或者等频分桶, 像下面这样(这个地方理解起来有点难,得画画了,图可能不太好看,能说明问题就行,哈哈):
在这里插入图片描述
上面就是等频和等宽分桶的思路了(这个不用较真,我这里只是为了和作者的想法产生更清晰的对比才这样举得例子),这样选择出的候选点是不是比就少了好多了?但是这样划分其实是有问题的,因为这样划分没有啥依据啊, 比如我上面画的等频分桶,我是5个训练样本放一个桶,但是你说你还想10个一组来,没有个标准啥的啊。即上面那两种常规的划分方式缺乏可解释性,所以重点来了, 作者这里采用了一种对loss的影响权重的等值(百分比分位数)划分算法( ), 我上面的这些铺垫也正是为了引出这个方式,下面就来看看作者是怎么做的,这个地方其实不太好理解,所以慢一些作者进行候选点选取的时候,考虑的是想让loss在左右子树上分布的均匀一些,而不是样本数量的均匀,因为每个样本对降低loss的贡献可能不一样,按样本均分会导致分开之后左子树和右子树loss分布不均匀,取到的分位点会有偏差。这是啥意思呢?再来一个图(这个图得看明白了):
\begin{}\{L}^{(t)} & \simeq \sum_{i=1}^{n}\left[l\left(y_{i}, \hat{y}_{i}^{(t-1)}\right)+g_{i} f_{t}\left(\{x}_{i}\right)+\frac{1}{2} h_{i} f_{t}^{2}\left(\{x}_{i}\right)\right]+\Omega\left(f_{t}\right) \\&=\sum_{i=1}^{n}\left[\frac{1}{2} h_{i} \cdot \frac{2 \cdot g_{i} f_{t}\left(\{x}_{i}\right)}{h_{i}}+\frac{1}{2} h_{i} \cdot f_{t}^{2}\left(\{x}_{i}\right)\right]+\Omega\left(f_{t}\right) \\&=\sum_{i=1}^{n} \frac{1}{2} h_{i}\left[2 \cdot \frac{g_{i}}{h_{i}} \cdot f_{t}\left(\{x}_{i}\right)+f_{t}^{2}\left(\{x}_{i}\right)\right]+\Omega\left(f_{t}\right) \\&=\sum_{i=1}^{n} \frac{1}{2} h_{i}\left[\left(2 \cdot \frac{g_{i}}{h_{i}} \cdot f_{t}\left(\{x}_{i}\right)+f_{t}^{2}\left(\{x}_{i}\right)+\left(\frac{g_{i}}{h_{i}}\right)^{2}\right)-\left(\frac{g_{i}}{h_{i}}\right)^{2}\right]+\Omega\left(f_{t}\right) \\&=\sum_{i=1}^{n} \frac{1}{2} h_{i}\left[\left(f_{t}\left(\{x}_{i}\right)+\frac{g_{i}}{h_{i}}\right)^{2}\right]+\Omega\left(f_{t}\right)+ \\&=\sum_{i=1}^{n} \frac{1}{2} h_{i}\left(f_{t}\left(\{x}_{i}\right)-\left(-\frac{g_{i} }{ h_{i}}\right)\right)^{2}+\Omega\left(f_{t}\right)+\end{}
到这终于把这一块描述完了, 有点多,稍微理一理逻辑,前面那一部分是围绕着如何建立一棵树进行的,即采用贪心的方式从根节点开始一层层的建立树结构(每一层争取最优),然后就是建树过程中一个关键的问题:如何寻找最优切分点,给出了最优切分点算法,基于这个算法就可以建立树了。后面这一部分是一个优化的过程,提出了一种 的算法,这个算法可以将原来的分割点进行分桶,然后找到合适的候选分裂点,这样可以减少遍历时尝试的分裂点的数量,是相比于GBDT做出的切分点优化策略,现在知道为啥要快了吧,因为寻找切分点的时候不用遍历所有的,而是只看候选点就可以了。而且在特征上,是可以并行处理的。这样的建树过程及优化策略基本上就是这些了,当然这里面还有很多的细节,由于篇幅的原因就不写在这里了。
利用新的决策树预测样本值,并累加到原来的值上若干个决策树是通过加法训练的, 所谓加法训练,本质上是一个元算法,适用于所有的加法模型,它是一种启发式算法。运用加法训练,我们的目标不再是直接优化整个目标函数,而是分步骤优化目标函数,首先优化第一棵树,完了之后再优化第二棵树,直至优化完K棵树。整个过程如下图所示:
在这里插入图片描述
好了, 到这里为止,的数学原理部分就描述完了,希望我描述清楚了吧。简单的回顾一下上面的过程吧: 是好多弱分类器的集成,训练弱分类器的策略就是尽量的减小残差,使得答案越来越接近正确答案。的精髓部分是目标函数的化简,这样就引入了损失函数的一阶和二阶导数。然后又把样本的遍历转成了对叶子节点的遍历,得到了最终的目标函数。这个函数就是衡量一棵树好坏的标准。在建树过程中,采用了贪心策略,并且对寻找分割点也进行了优化。基于这个,才有了后面的最优点切分建立一棵树的过程。训练的时候,是通过加法进行训练,也就是每一次只训练一棵树出来, 最后的预测结果是所有树的加和表示。 关于,依然还有很多的细节没有说到,具体的去看论文吧。下面,我们就进行的实战部分, 这里我们简单的做一个分类任务, 主要是看看主要怎么用, 尤其是在一个数据竞赛中(这次重点总结了一些用法)。
3. 实战二分类
安装:默认可以通过pip安装,若是安装不上可以通过:
~//
网站下载相关安装包,将安装包拷贝到的安装目录的目录下, 然后pip 安装包安装
3.1 的基本使用
我们使用做一个分类任务,可以直接使用。
代码语言:
代码运行次数:0
复制
Cloud 代码运行
# 0 1:1 9:1 19:1 21:1 24:1 34:1 36:1 39:1 42:1 53:1 56:1 65:1 69:1 77:1 86:1 88:1 92:1 95:1 102:1 106:1 117:1 122:1
# 1 3:1 9:1 19:1 21:1 30:1 34:1 36:1 40:1 41:1 53:1 58:1 65:1 69:1 77:1 86:1 88:1 92:1 95:1 102:1 106:1 118:1 124:1
# 0 1:1 9:1 20:1 21:1 24:1 34:1 36:1 39:1 41:1 53:1 56:1 65:1 69:1 77:1 86:1 88:1 92:1 95:1 102:1 106:1 117:1 122:1
# 0 3:1 9:1 19:1 21:1 24:1 34:1 36:1 39:1 51:1 53:1 56:1 65:1 69:1 77:1 86:1 88:1 92:1 95:1 102:1 106:1 116:1 122:1
# 0 4:1 7:1 11:1 22:1 29:1 34:1 36:1 40:1 41:1 53:1 58:1 65:1 69:1 77:1 86:1 88:1 92:1 95:1 102:1 105:1 119:1 124:1
# 0 3:1 10:1 20:1 21:1 23:1 34:1 37:1 40:1 42:1 54:1 55:1 65:1 69:1 77:1 86:1 88:1 92:1 95:1 102:1 106:1 118:1 126:1
# 1 3:1 9:1 11:1 21:1 30:1 34:1 36:1 40:1 51:1 53:1 58:1 65:1 69:1 77:1 86:1 88:1 92:1 95:1 102:1 106:1 117:1 124:1
"""上面是libsvm的数据存储格式, 也是一种常用的格式,存储的稀疏数据。
第一列是label. a:b a表示index, b表示在该index下的数值, 这就类似于one-hot"""
import numpy as np
import scipy.sparse # 稀疏矩阵的处理
import pickle
import xgboost as xgb
# libsvm format data 的读入方式, 直接用xgb的DMatrix
dtrain = xgb.DMatrix('./xgbdata/agaricus.txt.train')
dtest = xgb.DMatrix('./xgbdata/agaricus.txt.test')
下面我们进行参数设置:关于的参数,详细的可以看上面的参数说明,这里拿分类器来说,解释一些参数:
xgb1 = ( =0.1, =1000, =5, =1, gamma=0, =0.8, =0.8, = ':', =4, =1, seed=27)
”
当然不需要全记住,常用的几个记住即可。可以结合着上面的数学原理,看看哪个参数到底对于有什么作用,这样利于调参。设置好参数,训练测试就行了,使用起来和的模型非常像
代码语言:
代码运行次数:0
复制
Cloud 代码运行
"""paramet setting"""
param = {
'max_depth': 2,
'eta': 1,
'silent': 1,
'objective': 'binary:logistic'
}
watch_list = [(dtest, 'eval'), (dtrain, 'train')] # 这个是观测的时候在什么上面的结果 观测集
num_round = 5
model = xgb.train(params=param, dtrain=dtrain, num_boost_round=num_round, evals=watch_list)
然后就是预测:
代码语言:
代码运行次数:0
复制
Cloud 代码运行
"""预测"""
pred = model.predict(dtest) # 这里面表示的是正样本的概率是多少
from sklearn.metrics import accuracy_score
predict_label = [round(values) for values in pred]
accuracy_score(labels, predict_label) # 0.993
模型的保存了解一下:
代码语言:
代码运行次数:0
复制
Cloud 代码运行
"""两种方式: 第一种, pickle的序列化和反序列化"""
pickle.dump(model, open('./model/xgb1.pkl', 'wb'))
model1 = pickle.load(open('./model/xgb1.pkl', 'rb'))
model1.predict(dtest)
"""第二种模型的存储与导入方式 - sklearn的joblib"""
from sklearn.externals import joblib
joblib.dump(model, './model/xgb.pkl')
model2 = joblib.load('./model/xgb.pkl')
model2.predict(dtest)
3.2 交叉验证 xgb.cv
代码语言:
代码运行次数:0
复制
Cloud 代码运行
# 这是模型本身的参数
param = {'max_depth':2, 'eta':1, 'silent':1, 'objective':'binary:logistic'}
num_round = 5 # 这个是和训练相关的参数
xgb.cv(param, dtrain, num_round, nfold=5, metrics={'error'}, seed=3)
3.3 调整样本权重
这个是针对样本不平衡的情况,可以在训练时设置样本的权重, 训练的时候设置这个参数, 相当于在训练之前先对样本预处理。
代码语言:
代码运行次数:0
复制
Cloud 代码运行
# 这个函数是说在训练之前,先做一个预处理,计算一下正负样本的个数,然后加一个权重,解决样本不平衡的问题
def preproc(dtrain, dtest, param):
labels = dtrain.get_label()
ratio = float(np.sum(labels==0)) / np.sum(labels==1)
param['scale_pos_ratio'] = ratio
return (dtrain, dtest, param)
# 下面我们在做交叉验证, 指明fpreproc这个参数就可以调整样本权重
xgb.cv(param, dtrain, num_round, nfold=5, metrics={'auc'}, seed=3, fpreproc=preproc)
3.4 自定义目标函数(损失函数)
如果在一个比赛中,人家给了自己的评判标准,那么这时候就需要用人家的这个评判标准,这时候需要修改的损失函数, 但是这时候请注意一定要提供一阶和二阶导数
代码语言:
代码运行次数:0
复制
Cloud 代码运行
# 自定义目标函数(log似然损失),这个是逻辑回归的似然损失。 交叉验证
# 注意: 需要提供一阶和二阶导数
def logregobj(pred, dtrain):
labels = dtrain.get_label()
pred = 1.0 / (1+np.exp(-pred)) # sigmoid函数
grad = pred - labels
hess = pred * (1-pred)
return grad, hess # 返回一阶导数和二阶导数
def evalerror(pred, dtrain):
labels = dtrain.get_label()
return 'error', float(sum(labels!=(pred>0.0)))/len(labels)
训练的时候,把损失函数指定就可以了:
代码语言:
代码运行次数:0
复制
Cloud 代码运行
param = {'max_depth':2, 'eta':1, 'silent':1}
# 自定义目标函数训练
model = xgb.train(param, dtrain, num_round, watch_list, logregobj, evalerror)
# 交叉验证
xgb.cv(param, dtrain, num_round, nfold=5, seed=3, obj=logregobj, feval=evalerror)
3.5 用前n棵树做预测
太多的树可能发生过拟合,这时候我们可以指定前n棵树做预测, 预测的时候设置这个参数
代码语言:
代码运行次数:0
复制
Cloud 代码运行
# 前1棵
pred1 = model.predict(dtest, ntree_limit=1)
evalerror(pred2, dtest)
3.6 画出特征重要度
代码语言:
代码运行次数:0
复制
Cloud 代码运行
from xgboost import plot_importance
plot_importance(model, max_num_features=10)
3.7 同样,也可以用的调参
代码语言:
代码运行次数:0
复制
Cloud 代码运行
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import StratifiedKFold
model = XGBClassifier()
learning_rate = [0.0001, 0.001, 0.1, 0.2, 0.3]
param_grid = dict(learning_rate=learning_rate)
kfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=7)
grid_search = GridSearchCV(model, param_grid, scoring="neg_log_loss", n_jobs=-1, cv=kfold)
grid_result = grid_search.fit(x_train, y_train)
print("best: %f using %s" %(grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
params = grid_result.cv_results_['params']
for mean, param in zip(means, params):
print("%f with: %r" % (mean, param))
好了,实战部分就整理这么多吧, 重点在于怎么使用,使用起来和的模型也是非常像, 也是.fit(), .()方法,只不过的参数很多,这个调起来会比较复杂, 但是懂了原理之后,至少每个参数是干啥的就了解了,关于调参的技术, 得从经验中多学习,多尝试,多总结才能慢慢修炼出来。
4. 总结
哇,终于完了,到这里,终于把的一些知识说清楚了, 每一次不知不觉就会这么多字, 可能是因为这个算法太重要了吧,所以多写了点, 赶紧回顾一下:首先, 我们从集成算法开始讲起,回顾了一下,GBDT, 然后引出了, 我们知道同属流派,但集成策略又有不同, 即使集成策略类似,那么得到最后结果的方式又不同。但对比之中,我们能更加体会它们的原理。其次,我们从数学原理的角度剖析了一下, 看到了它的目标函数,看到了如何生成一棵树,看到了如何化简,知道了为什么需要损失函数的一二阶导数,也明白了为啥这个算法这么快。最后,我们通过实战一个二分类问题,见识到了的代码实现,基本使用和一些高级策略。下面看看相比于GBDT有哪些优点(面试的时候可能会涉及):
上面的这些优点,我在描述的时候基本上都涉及到了,正是因为有了这些优点,才让它变得非常火,堪称神器了现在,但是真的了吗?正所谓金无足赤,人无完人, 也同样如此,比如虽然利用了预排序和近似算法可以降低寻找最优分裂点的计算量,但在节点分裂过程中仍需要遍历整个数据集。预排序过程的空间复杂度过高,不仅需要存储特征值,还需要存储特征对应样本梯度统计值的索引,相当于消耗了两倍的内存。所以在内存和计算方面还是有很大的优化空间的。那么还可以在哪些角度进行优化呢?后面通过的故事再说给你听 ;)
的故事就先讲到这里了,希望对你有所帮助,当然还有很多的细节没有提到,本文只是抛砖引玉,具体的建议去看看原文,毕竟这个算法还是超级重要的,面试的时候也会抠得很细, 不看原文的话有些精华get不到。
参考: