之前肝了三篇XGBoost
涉及到的起源挂枝儿:从Boosting到BDT再到GBDTzhuanlan.zhihu.com
到XGBoost的总体推到过程挂枝儿:再从GBDT到XGBoost!zhuanlan.zhihu.com
再到代码层面的参数初步理解挂枝儿:XGBoost从原理到调参zhuanlan.zhihu.com
现在基本能够大概讲清楚XGBoost到底是怎么一回事了,但是遇到具体的问题的时候对于XGBoost 到底怎么去兼顾分类和回归问题,其中有哪些区别感觉还是有些疑惑,我总结了一下,就是目前为止的对于算法的理解其实还缺了一个很大的部分,那就是:
形象的理解树 到底是怎么长的?
所以今天的这篇(应该是最后一篇了)就是想结合具体的案例,分别用XGBoost来进行回归和分类来彻底理解每棵树是怎么长的,个人过了一遍之后对于XGBoost正则化参数的理解感觉又比之前上了一个层次。
下面开始:
之前的原理篇有提到,XGBoost最厉害的地方是他是一个抽象的算法框架,无论我们是拿他来进行回归,我们都可以根据泰勒展开后的目标函数得到下式,下面简单再进行一下重新推导:
根据上式,我们需要最小化Ovalue,求导=0可得(<===重点1!!!)! !
再将求得的Ovalue最小值带回我们的目标函数,得到我们的叶子节点分数(<===重点2!!!)
注意到上式最后把1/2约去了,那是因为求最小值常量不碍事可直接约去
开始做分类与回归:根据上面的式子我们可以发现,无论是做分类还是回归,为了分裂节点建树,我们都需要:
1. 把最小化叶子节点分数得到,为了得到最小化的叶子节点分数,我们需要计算损失函数对于叶子节点的一阶到二阶导
2. 计算Similarity Score
3. 计算子节点的Similarity Score,与父节点相减得到gain(需要最大化gain)
4. 重复以上...
5. 确定树结构后通过lambda,gamma进行剪枝(对应下图计算梯度和分裂节点的部分)
XGBoost的回归树
我们先从回归树说起(更简单一些)
对于回归问题,一般来说我们最常用的损失函数就是
一阶导gi (用到链式法则),可以理解为残差
二阶导hi (求没了已经) 全部为1
写到这里,我们可以惊喜的发现,对于回归问题,XGBoost的最优化叶子节点式子就可以写成:
分子就是残差,分母就是叶子节点的样本数量+lambda
相应的目标函数就可以写为:
分子就是残差和的平方,分母就是叶子节点的样本数量+lambda
接下来我们通过boston放假数据集来验证我们的想法,作为样例:
from sklearn.datasets import load_iris, load_boston,load_iris
from xgboost import XGBClassifier,XGBRegressor
boston = load_boston()
train = pd.DataFrame(boston['data'])
label = pd.Series(boston['target'],name='label')
full = pd.concat((train,label),axis=1)
model = XGBRegressor(n_estimators=3,max_depth=1,reg_lambda=0,reg_alpha=0)
model.fit(train,label)
model.get_booster().trees_to_dataframe()
我们可以发现根节点的gain = 19339.546900
我们再通过手动计算一遍,看看手动的梯度计算,叶子节点的计算分数是否能和算法输出出来的数字一致:
full['g'] = full['label'] - 0.5
full['h'] = 1
root_score = full['g'].sum() ** 2 / full.shape[0]
left_df = full[full.iloc[:,5] < 6.9410]
right_df = full[full.iloc[:,5] >= 6.9410]
left_score = left_df['g'].sum() ** 2 / 430
right_score = right_df['g'].sum() ** 2 / 76
print('The Gain for Root is left node score{} + right node score{} - root score{} = {}' \
.format(left_score,right_score,root_score,right_score+left_score - root_score))
>>> The Gain for Root is left node score162397.88895348838 + right node score102576.61065789477 - root score245634.94458498035 =
>>> 19339.555026402784
可以发现我们的gain分数是完全对的上的!
刚刚为了方便计算,我们故意把lambda设成了0,这次我们设成1,再看看结果
train = pd.DataFrame(boston['data'])
label = pd.Series(boston['target'],name='label')
full = pd.concat((train,label),axis=1)
full['g'] = full['label'] - 0.5
full['h'] = 1
model = XGBRegressor(n_estimators=3,max_depth=1,reg_lambda=1,reg_alpha=0)
model.fit(train,label)
model.get_booster().trees_to_dataframe()
full['g'] = full['label'] - 0.5
full['h'] = 1
root_score = full['g'].sum() ** 2 / (506 + 1)
left_df = full[full.iloc[:,12] < 9.7250]
right_df = full[full.iloc[:,12] >= 9.7250]
left_score = left_df['g'].sum() ** 2 / (212 + 1)
right_score = right_df['g'].sum() ** 2 / (294 + 1)
print('The Gain for Root is left node score{} + right node score{} - root score{} = {}' \
.format(left_score,right_score,root_score,right_score+left_score - root_score))
>>> The Gain for Root is left node score180271.60356807514 + right node score83126.45423728814 - root score245150.457514793 =
18247.600290570263
可以发现第一棵树的数字被压缩了,这也正是正则化参数lambda存在的目的,其实从他在狮子中的位置也可以看得出来:lambda越大,节点少的叶子的权重会被稀释的越厉害,而叶子节点多的权重收到的影响会稍小一些。用高深点的话说:lambda>0时,会shrink我们的分数值,让输出的结果分更小。(我看到这里感觉有种他和学习率eta双重绑定,环环相扣的感觉)
在后剪枝的过程中,gamma会参与进来,所有分裂得分
分类树:
接下来再来啃一下有点劝退的分类树:
分类函数的损失一般用logloss
我们把logloss转换一下(先放一个p,log_odds,odds解释图,看不懂的话可以先理解一图里的式子)
所以最终我们的损失函数的等于:
一阶导gi (依旧是残差,只不过是概率
二阶导hi (一个很奇妙的相互乘的式子)
得到了这两个式子后,我们就可以得到分类树的叶子节点最优分:
分子依旧是残差,分母是前一项预测值交叉乘 + lambda
以及对应的结构分
同样的,我们来用一个例子验证一下我们的式子
iris = load_iris()
train = pd.DataFrame(iris['data'])
label = pd.Series(iris['target'],name='label')
full = pd.concat((train,label),axis=1)
full = full[full.label != 2]
train = full[[x for x in full.columns if x != 'label']]
label = full['label']
model = XGBClassifier(n_estimators=3,max_depth=1,reg_lambda=0,reg_alpha=0)
model.fit(train,label)
model.get_booster().trees_to_dataframe()
full['g'] = full['label'] - 0.5
full['h'] = 0.5 * (1-0.5)
root_score = full['g'].sum() ** 2 / full.shape[0]
left_df = full[full.iloc[:,2] < 2.45]
right_df = full[full.iloc[:,2] >= 2.45]
left_score = left_df['g'].sum() ** 2 / 12.5
right_score = right_df['g'].sum() ** 2 / 12.5
print('The Gain for Root is left node score{} + right node score{} - root score{} = {}' \
.format(left_score,right_score,root_score,right_score+left_score - root_score))
>>> The Gain for Root is left node score50.0 + right node score50.0 - root score0.0 =
100.0
可以发现也是对的上的。同时我们可以发现,xgboost中的一个minchildweight参数其实就是回归树中的分母的叶子数量,分类树中的概率交互乘项,可以发现在定义这个参数时分类与回归树的区别还是要注意,2个参数在不同的分类任务下的值域显然是不同的。
最后放一个分类回归分数计算和结果分数的式子对比,方便加深印象总结:分类树 回归树所用的算法思想是一致的,都是在XGBOOST的框架下,不停的去拟合上一轮的残差(都是0.5分开始)
两者的损失函数不同造成两者生长的策略有所不同,导致XGBOOST 的参数像是minchildweight设置的值域也不同.https://www.youtube.com/watch?v=ZVFeW798-2I