本文旨在总结一下常见树模型的行、列抽样特点以及特征重要性的计算方式,也会带着过一遍算法基本原理,一些细节很容易忘记啊。
主要是分类和回归两类任务,相信能搜索这篇文章的你,应该对树模型有一定的了解。
可以搜索 总结 ,直接定位到要讲的重点,跳过算法基础讲解的部分。
一、早期历史
1.1 决策树核心算法
ID3算法(1986年):只能用来分类,使用的分裂准则是信息增益,用分裂前的entropy - child leaf的entropy。
假设样本总共有m个类别,每个类别在当前结点的概率p_k,则信息熵计算如下。
遍历特征,特征必须是离散值,不能处理连续值,本质上是个大的if then else,容易过拟合,比较老了都不用这个了,但这个是入门基础。
C4.5:是对ID3的改进,①可以处理连续值,就是将某个特征排序,取两个数的中间值来划分;②将信息增益改为信息增益比,避免有些特征取值很多,分裂时容易有倾向性;
【机器学习(三)】机器学习中:信息熵,信息增益,信息增益比,原理,案例,代码实现。-CSDN博客
这篇文章讲的挺清楚的;
这两个算法是老黄历了,只能分类,并非可以是多叉树,是基础,但实际上我们根本不会用这些。
Cart树(classification and regression tree):是二叉树,看名字就知道,既可以分类又可以回归,属于重大革新了。
分类问题中,采用gini系数作为不纯度来计算分裂收益,找到两个叶子结点gini系数之和最小的划分方式,等同于父节点gini - 两个子节点gini 之差最大,这样鸡贼地避免了对数运算,加快了速度;
在回归中,就是MSE均方差作为分裂计算标准,准确点说,是squared error之差,看取哪个特征哪一个点,能让划分的两个子类,误差较之前减少最多。
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0.00001,0.999999 , 10000)
entropy = -x*np.log2(x) - (1-x)*np.log2(1-x)
gini = 2*x*(1-x)plt.plot(x,entropy, label='entropy',color='purple',alpha=0.5)
plt.plot(x,gini, label='gini',color='blue',alpha=0.5)plt.title('two class entropy & gini sum plt')
plt.xticks(np.arange(0,1.1,0.1))
plt.axvline(0.5,color='black',linestyle='--',alpha=0.2)
plt.legend()
plt.show()
可以看到,这两个计算方式的图像的性质基本相同,如果一个叶子里含有两类,那就是没完全区分,一个的比例是p另一个就是1-p,如果只剩一类就是0;
虽然基尼系数简单好用,但相对于信息熵,还是准确率方面是稍微差那么一点的,譬如二分类,对比两个划分点位,带来的信息增益VS基尼不纯度减少,采用两种计算方式,在两个点位,用同一种计算方式差别很小的情况下,可能不同的计算方式会选择不同的划分点位,退一万步来讲,又计算简单又准,咋不上天呢?不然xgboost和之后树模型为何还是使用了logloss,有兴趣的可以深入研究下两者区别。
1.2 发展路线
bagging和boosting和stacking
决策树本身只是一棵树,在后续的发展中,基本是往bagging和boosting两条路发展,stacking这种混合模式就不在本文讨论范围中;
本文准备了两组数据,一个是自带的乳腺癌,一个是波士顿零元购房价数据,相信大家都很熟悉;
2.bagging-随机森林
bagging算法主要是随机森林和极限森林两种:
2.1随机森林分类(RandomForestClassifier)
在sklearn.ensemble里有分类、回归和RandomTreesEmbedding,其中Embedding基本用来将数据变成高维的One-Hot矩阵,再喂给其他模型,本文不讨论这么广;
所有参数如下:
ccp_alpha Classifier(n_estimators=100, *,criterion='gini', # {"gini", "entropy", "log_loss"}, default="gini"# entropy应该是可多分类也可二分类# log_loss应该是只能二分类max_depth=None, #int, default=None,最大深度,默认直到世界的尽头min_samples_split=2, # 父节点可分裂,要求的最少样本数# int为个数,float为占总数的比例min_samples_leaf=1, # 孩子节点最少样本数,同父亲一样int\floatmin_weight_fraction_leaf=0.0, # float,默认0即不设置,可忽视# 叶子结点最少的权重数合计比例# 不设sample_weight等同结点样本占比# 如设,比如二分类不平衡,将少数样本权重比例设为10# 则可控制要往下分裂所需的最小权重之和# 用得少,不如用其他参数,不平衡数据实在难崩再考虑max_features='sqrt', # {"sqrt", "log2", None}, int or float, default="sqrt"max_leaf_nodes=None, # int,最大叶子节点数,默认None不限制min_impurity_decrease=0.0, # 往下分裂所需不纯度减少要求,注意看说明文档公式# 还有个当前结点占总体数量的比例# 说实话这参数,不太好控制# 除非效果太差或追求极致,一般不用这个控过拟合bootstrap=True, # bool, default=True,默认对样本放回抽样# 基本绝对会抽到重复的,这是随机森林行抽样的特色# False则每棵树用全部样本# 所以随机森林要么不放回会有重复,要么全部样本oob_score=False, # 见下文n_jobs=None, # 默认None只开一核,无脑-1全核即可random_state=None, # 固定种子,则多次运行行列抽样等可固定对比verbose=0, # 0->不输出日志,n->迭代n次输一次warm_start=False, # 默认重新训练模型,为真则当传入新数据fit# 可以继续新增树,之前的树可保留class_weight=None, # 一般样本不平衡时考虑用,注意多分类传入方式# 解释文档说的很清楚{"balanced", "balanced_subsample"}# balanced则将少数样本按占比的反比提升# balanced_subsample是按抽的样本中的比例而不是总体比例调整ccp_alpha=0.0, # 类似L2或者xgboost的gamma Tree 参数,详见下max_samples=None, # 必须要bootstrap为True,因为False则用全样本# None则从所有m个样本中放回抽m个# int,float则为最大个数、比例# 均为不放回抽样,都有重复的# 因为不放回抽样本就有很多没抽到,一般不用这个monotonic_cst=None, # 单调性约束,跟特征数相同的列表# 比如有个特征越高,分类越趋向于1,则设为1# 0则无约束,-1则表示负相关# 在一起情况下,应该能提升准确率# 感觉没啥用,直接换算法来得快一些
)
单独说明:
bootstrap:如果默认放回抽样,假设有m个样本,则每个样本被抽到的概率为1/m,要抽m次(等于样本总数),则有的样本会重复,有的样本会没有被抽到,这是绝对会发生的,极限情况下,可证明约有36.8%的样本没被抽到,即为Out Of Bag袋外数据。
oob_score:是否用袋外数据对模型进行评分,前提是开了bootstrap,分类默认是accuracy,也可以传个函数进去。
有些参数,应该是后期更新加进去的,因为我的版本应该比较新。
ccp_alpha:在复杂度(单颗树的叶子节点数)和不纯度减少\MSE减少中,取最小值,附带链接!
决策树后剪枝算法(一)代价复杂度剪枝CPP-CSDN博客
2.2随机森林回归(RandomForestRegressor)
只看跟分类的区别,因为分类问题往往难一些;
RandomForestRegressor(criterion='squared_error', # default="squared_error"# {"squared_error", "absolute_error", "friedman_mse", "poisson"}oob_score=False, # 默认用R方,即sklearn.metrics.r2_score)
仔细猛地一看,与分类只有两个区别:
①criterion:分裂标准默认是SSE(sum of squaerd error),跟MSE一个道理,乘了样本数,假如左、右叶子结点的SSE比父结点少,即可分裂,当然是找分裂增益最大的那个啦,说MSE是不太准确的,难道写代码的时候,先求个平均再乘个总数,这不是有病么,所以肯定是直接求平方和;一般不会用绝对值的L1_loss,因为求导分两种情况并且导数为正负1,难顶。
friedman_mse,相当于调整了一下,有点F1-score的感觉。
# friedman_mse
diff = mean_left - mean_rightimprovement = (n_left * n_right) * diff**2 / (n_left + n_right)
poisson ,应该是,泊松偏差则是适用于一个特殊场景的(泊松分布是正整数的分布),当需要预测的标签全部为正整数时,标签的分布可以被认为是类似于泊松分布的。 正整数预测在实际应用中非常常见,比如预测点击量、预测客户/离职人数、预测销售量等。
②oob_score:用的R方
R方一般会在0到1之间,但假如万一出其不意一不留神模型极其之差,可为负数,R方越接近1越好,基本跟MSE是一个道理;
重要属性:
挑几个重要点的
①oob_score_, 返回accuracy或者R方,必须在参数里先把oob_score设为真;
②oob_decision_function_,返回袋外数据的预测结果,实测过就是oob_score_的源数据;
比如二分类问题,样本m个,每次不放回抽样,基本有三四成样本没被抽到,此时第一棵树训练出来了,可以得到这三四成的预测结果,综合n颗树,可以投票得到一个结果。
model.oob_decision_function_,跟model.predict(所有训练data_x)的结果肯定不同,理解了随机森林抽样和袋外验证的原理,就很容易知其原因。
③feature_importances_,返回特征重要性,按不纯度减少(看你选的哪个criterion)或者MSE(SSE--sum squared error)减少,所有树的所有分裂结点先累加,再归一化,可以看到所有的feature_importances_加和为1;
亦有一种说法是,特征重要性还有第二种计算方式,用袋外数据,随机打乱某特征列,看对结果造成的误差大小,判断重要性,也有很多文章阐述了这个事实,但我在sklearn里好像没有什么参数能指定用哪种方式计算。
不过也不太需要深究,
what the fu**,I didn`t plan to write things about the parameters.but As the writing progresses,I got fu**ing stuck in thest sh*t until all done.还写得很全...
总结:
随机森林,行抽样,默认bootstrap=True是放回的,大概会有36.8%的数据不会被抽到,同时你还可以搞事情,用max_samples限制抽样的总次数,这样会有更多数据没被抽到,也可强行指定用全部数据,bootstrap = False。
列抽样,取开根号、log2或者指定数字、比例或全部,分类中默认用sqrt开根号,回归默认用全部特征;
但随机森林列抽样跟xgboost之类的不同,是在每次分裂的时候,才开始选指定数量的列,而xgboost等是在建树的时候就已经先选定了那些列,当前树的分裂过程中,不会再抽,即每棵树一开始就定死了那些列,随机森林每次分裂都抽(翻出去可查证)
特征重要性,不纯度减少、MSE减少,结果是归一化的,关于打乱特征计算重要性的方法,sklearn中未见参数可指定用哪种方式计算,个人认为就是按criterion的减少计算的,但这也不重要啦。
这个obb_score是个创意,直接不用分train和test了,本身抽样就留出了快四成。
一般碰到一个数据集,不知道哪个特征重要,丢给随机森林跑一遍总没错,一来快,二来模型方差不高,堪称居家旅行、数据狗必会。
2.3决策树--漏了补上
其实是应该先说决策树,但懒得改编号了,随机森林说白了就是多个决策树;
from sklearn.tree import DecisionTreeClassifier,DecisionTreeRegressor
# 只说跟随机森林不同的,但是逻辑顺序反了,懒得改了
DecisionTreeClassifier(splitter='best',criterion='gini'
)# {"best", "random"}, default="best"
# {"gini", "entropy", "log_loss"}, default="gini"# 回归问题
DecisionTreeRegressor(*,criterion='squared_error',# {"squared_error", "friedman_mse", "absolute_error", "poisson"}, # default="squared_error"max_features=None, # 默认是全部特征,分类基本都是开根号
)
splitter:
best不难理解,正常是这样,遍历所有特征所有阈值,查找最佳分裂点;
random:中文网上难找到具体答案,出去查阅了下:
一种说法是,会先找到分裂增益最大的点,选定这个特征,在这个特征中不选最佳的阈值,选个其他的,控制过拟合;
第二种是单纯随便挑一个特征的一个阈值,直接进行分裂;
第三种是极限森林的玩法,每个特征都先随便选一个阈值,再对比哪个最好,选定这个相对最好的;关于这个随便,亦有人试验循环很多次,阈值的挑选会选择最佳阈值的附近值,大部分会选择最佳特征的最佳阈值附近,有点均值上下一两个标准差的感觉,也会选择非最佳特征的非最佳值,但总体占比少一些,很少会选非常边缘的值,有点正态分布靠中间位置的意思。
众说纷纭,但目前第三种说法,比较占优,个人也认同,如果有大佬精通,请批评指正。
总结:
没有行抽样,因为就一棵树,毕竟行抽样是随机森林是在决策树上改进添加的;
列抽样:可选取sqrt、log2或者指定数字、比例或全部,分类回归默认用全部特征;
同时warm_start,bootstrap,oob_score,n_jobs这些需要bagging多棵树的东西都没有,因为懒得改顺序,其实应该先说决策树参数,再讲随机森林增加了哪些内容。
特征重要性:criterion减少值并归一化
2.4极限树--漏了补上
查看参数,同决策树一模一样,将splitter设为best,即启用决策树,用random即极限树,我猜测之前可能是有区别的,决策树就是best,极限树就是random,后期有改动,将两个功能合并了。
总结:
单颗极限树和单颗决策树,现在区别仅在一个splitter设置上,没有行抽样,默认使用了全部行,列抽样和随机森林一样,特征重要性也是根据criterion计算;
2.5 极限森林
经查,同随机森林参数完全一样,不过内部的特征分裂点选择,不能像单颗树一样随意切换,单颗树导包导错了,换个splitter一样用,森林则不行。
极限森林这种random选择阈值的方式,可以减小模型总体的方差,代价是偏差会略微高一点,可能在交叉验证中,极限森林会取得更好的平均成绩。
如何理解不选最佳分裂阈值,模型方差还会小呢:比如判定一个人是否有钱,选定了一个特征,每月银行卡入账的金额,随机森林可能选了每月>10万,极限森林可能选了>50万,有的人虽然入账多,他可能工作性质就这样,搞销售、跑业务请吃请和,经常进货倒货,这样的人并不一定非常富有,而月入账50万的,是假富人的概率更低。虽然在训练数据集上,每个都分得不够仔细,但树多了,对新加入的陌生数据集,预测的总体误差很可能减少,毕竟开宝马7系不一定真有钱,但开帕加尼风之子很难说他没钱。
总结:
极限森林行采样:稍微不同,默认bootstrap=False,毕竟分裂点不是最佳,故用全部行,当然可以设置bootstrap=True再叠加一个max_samples,就变成了有放回的行抽样;
列抽样:同随机森林,分类默认sqrt,回归默认全部特征;
特征重要性:同随机森林,criterion减少值并归一化
因为默认bootstrap=False,则袋外数据结果obb_score默认也是False
3.boosting
基本是adboost、GBDT、xgboost、lightgbm、catboost之类的
3.1AdaBoost算法
是boosting,基本是用来进行分类的,使用指数损失函数,单纯地分类来说,准确率是挺高的,回归使用的是中位数,一般不会有人用来回归,缺点是计算比较慢,是真的慢龟速,毕竟涉及到太多对数和指数运算。
损失函数:比较特殊,是集成学习里较少使用的指数损失函数。
二分类大概原理:
①先搞一棵树,计算分类错误率:错误率必须小于0.5,如果大于则反转预测结果即可小于0.5
②计算这棵树的权重: 可知错误率越低,权重越高,反之错误越多,权重就低;
③更新每个样本的权重:第一棵树分类错的,就增加权重,分对的就降低权重;
如果预测对:则y_i * f(x_i) = 1 ,指数上面是负数,会变小;
如果预测错误:则y_i * f(x_i) = -1 ,指数上面是正数,会变大;
由于每个样本权重都变化了,新的权重要归一化。
④循环往复,直到达到设定的树的棵树,或者最后一棵树能全部预测正确;
⑤最终结果:
很多公式都忘记打上学习率了,虽然默认学习率是1。
一般要自己先搞一个基学习器,不然默认DecisionTreeClassifier(max_depth=1)有点低,大部分情况下base_estimator都是决策树。
可以证明训练误差是以指数的速度下降的:https://www.cnblogs.com/liuwu265/p/4692347.html
算法内部最后的预测概率代码实现跟原理公式写的有点区别:
每棵树预测概率*树权重加和-->除以权重之和归一化-->一般将第一个标签变成-1-->再对-1和1的概率预测相加(记为p)-->-1和1的概率为[-p,p],最终进行sigmoid。
多分类时:每棵树的权重计算公式有点变化;不会降低预测正确样本的权重,仅增加预测错误样本权重,再归一化,也相当于变相一增一减,有兴趣可以研究下两种算法SAMME和SAMME.R的区别;
其实没必要搞这么清楚,最终也是殊途同归。
回归:略
总结:
由于要使用基学习器,而基学习器基本都是决策树,行列的抽样,又回到了决策树那一套模式上了;
特征重要性,也是基于每棵树给的值计算,adaboost对每个学习器,根据错误率计算每棵树的权重,那么计算特征重要性的时候,有加权吗?经过查找很多资料发现,并没有任何文章提及加权的事,个人认为就是单纯的每棵树相加,再归一化。
3.2 GBDT
梯度提升树,CART回归树,分类也是用的回归的均方差,不停地拟合残差,最终预测结果为F(x)=f(0)+learning_rate*f(x),准确率基本是优于随机森林的,GBDT主要是降低模型的偏差,而随机森林降低模型的方差,说人话就是GBDT过拟合重一些但更准确,随机森林没那么准但预测没见过的数据集(特征不能很多超过训练数据集范围)可能总体误差更小。
二分类中:可选损失函数为log_loss(默认值,一般基本都用这个),exponential指数损失,选指数就变成了adaboost;
难点可能是分类问题中,如何计算叶子结点的权重weight(叶子结点算出的值),因为很难算,用的是泰勒公式一阶、二阶导求极值代替。
详见:https://zhuanlan.zhihu.com/p/89549390
# 只考虑和随机森林不一样的参数
GradientBoostingClassifier(*,loss='log_loss', # {'log_loss', 'exponential'}, default='log_loss'subsample=1.0, # 不放回抽样,跟随机森林不同criterion='friedman_mse', # {'friedman_mse', 'squared_error'}, default='friedman_mse'# 分裂增益的计算公式 validation_fraction=0.1,# 留出用来验证的数据集比例,要先设early_stop才可用# 虽然可不用分训练、验证数据集,自带一个不错的功能# 但一般我们习惯手动CVn_iter_no_change=None, # early_stop的迭代次数tol=0.0001, # 如果总体损失降低没有tol下限高,则不迭代了ccp_alpha=0.0, # 同决策树、随机森林的正则参数
)
多分类:构造n_class个二分类器,用one_vs_rest,最终将K个类预测的结果,用softmax进行归一化。
详细请看大神文章:深入理解GBDT多分类算法 - 知乎 (zhihu.com)
回归:
GradientBoostingRegressor(loss='squared_error',# 可选{'squared_error', 'absolute_error', 'huber', 'quantile'}# 见下文criterion='friedman_mse',# 可选{'friedman_mse', 'squared_error'}# 分裂增益计算,同分类alpha=0.9,# 如损失函数选了huber\quantile则可使用,见下文
)
比较值得注意的是损失函数,squared_error默认方差,比较容易理解;absolute_error绝对值差,由于导数为+1或者-1,一般较少用;剩下两个huber和quantile见下图
回归总体简单很多,就是下一棵树,拟合上一棵树的残差,叶子结点预测值是样本均值,最终乘个学习率即可。
总结:
GBDT行抽样,分类回归默认都是全部样本,如果指定了比例,则不放回抽样,变成了一种随机梯度提升树 (Stochastic Gradient Boosting Tree, SGBT),一般用GBDT,先留出验证集,训练集就用全部行,或者交叉验证。
列抽样:不同的是分类回归默认都是用所有特征,可选的同随机森林,GBDT就是要算到极致,行变少了、列不选全,怎么算到底,当然你也可以选择只用一部分。
特征重要性:按照每个特征带来的criterion减少值计算的,也是归一化的结果,criterion有friedman_mse和squared_error两种,故重要性的计算方式也有两种。
GBDT单纯按算法原理,会过拟合,但默认值,在学习率、最大深度、等方面设得比较稳健,再加上人为设置行、列抽样,故没有那么严重的过拟合,一般要交叉验证,要调参,设置各种剪枝参数。
GBDT 性能强于随机森林,这句话基本没错,不然为什么后面的都用GBDT作为基础。
3.3 XGBOOST-eXtreme Gradient Boosting tree
xgboost是2014年提出的,极致梯度提升树,分类回归都是cart回归树,现在的xgboost经过偷偷地不断更新,已经涵盖了很多功能,比如可以使用直方图计算,可以进行梯度采样,可以特征互相绑定,可以直接支持类别特征(需要手动指定),这些我记得很多都是lightgbm、catboost的东西。
这些新增功能,在很多原理文章中,可能都找不到描述。
xgboost参数太多,而且新增很多,我曾写过一篇,但也不全,原理参数方面略。
总结:
行采样:subsample,每棵树不放回抽样,一般是个0.5~0.9之类的浮点;在用gpu_hist的时候,还可以像lightgbm一样梯度采样。
列采样:colsample_bytree,每棵树生成之时就抽相应的比例的特征,跟随机森林不同,随机森林是每次分裂的时候抽,而xgboost这棵树在生成的时候,抽了哪些特征就定下来。同时还在colsample_bylevel,colsample_bynode这些二级、三级列抽样参数。
特征重要性:
‘weight’: the number of times a feature is used to split the data across all trees.
简单来说,就是在子树模型分裂时,用到的特征次数。这里计算的是所有的树。
‘gain’: the average gain across all splits the feature is used in.
特征在作为划分属性时gain的平均值
‘cover’: the average coverage across all splits the feature is used in.
这个是cover是略有不明白的,按照解释,是使用某个特征进行分裂所影响到的样本数量,可以计算父节点或者左右两个子节点,毕竟结果是一样的。亦有一种说法,是两个特征分裂产生的子节点二阶导之和,回归问题中,二阶导为1没毛病,但分类问题中,二阶导是sigmoid(1-sigmoid),这一样就与回归计算结果肯定不同;个人比较支持第一种说法。
‘total_gain’: the total gain across all splits the feature is used in.
不算平均算总数,由于一般都会进行列抽样,在计算平均时,用到的列次数很可能不同,所以结果会与'gain'有区别,这个计算方式同随机森林、GBDT比较像,因为最终也会进行归一化。
‘total_cover’: the total coverage across all splits the feature is used in.
同上。
xgboost一般都习惯用原生train方法,快,但缺点①是参数字典要自己写,基本得翻一下,不然谁记得那么多名字。②无法用gridsearch之类的调参,自带的CV只能寻找既定参数下的最佳迭代次数,但往往我们都需要批量或者分小批调参。
不过sklearn版的,好像进行了更新,速度也不错,用sklearn的也行。
3.4 lightgbm
参数方面可参考一个帅逼写的,自认为写的还是比较全的,花了不少时间看原文。LightGbm参数案例详解、参数讲解全又多-CSDN博客
总结:
行采样:subsample控制比例,subsample_freq 设置隔多少次迭代再重新采样行。
如果data_sample_strategy设为goss,则会用单边梯度采样,不可用行抽样和行抽样间隔,但是可以用列抽样。
还可以进行正负样本采样比例设置。
列抽样:colsample_bytree(原生方法里叫feature_fraction),feature_fraction_bynode子节点列采样比例,有两个,xgboost里是三个;
lightgbm绝对会用直方图,所以force_row_wise和force_col_wise绝对会用一个,如果自己不设,模型会都试一下看哪个快帮你定,建议参考使用场景说明。
特征重要性:有两个,大厂搞的东西,往往会削减那些花里胡哨的东西,我觉得两个差不多就够了。
The type of feature importance to be filled into ``feature_importances_``.
If 'split', result contains numbers of times the feature is used in a model.
If 'gain', result contains total gains of splits which use the feature.
3.5 catboost
略
参考:
决策树三种算法比较(ID3、C4.5、CART)_三种决策树算法的区别-CSDN博客