原文地址:Complete Guide to Parameter Tuning in Gradient Boosting (GBM) in Python by Aarshay Jain
原文翻译与校对:@酒酒Angie([email protected]) && 寒小阳([email protected])
时间:2016年9月。
出处:http://blog.csdn.net/han_xiaoyang/article/details/52663170
声明:版权所有,转载请联系作者并注明出
如果一直以来你只把GBM当作黑匣子,只知调用却不明就里,是时候来打开这个黑匣子一探究竟了!
这篇文章是受Owen Zhang (DataRobot的首席产品官,在Kaggle比赛中位列第三)在NYC Data Science Academy里提到的方法启发而成。他当时的演讲大约有2小时,我在这里取其精华,总结一下主要内容。
不像bagging算法只能改善模型高方差(high variance)情况,Boosting算法对同时控制偏差(bias)和方差都有非常好的效果,而且更加高效。如果你需要同时处理模型中的方差和偏差,认真理解这篇文章一定会对你大有帮助,因为我不仅会用Python阐明GBM算法,更重要的是会介绍如何对GBM调参,而恰当的参数往往能令结果大不相同。
特别鸣谢: 非常感谢Sudalai Rajkumar对我的大力帮助,他在AV Rank中位列第二。如果没有他的指导就不会有这篇文章了。
Boosting可以将一系列弱学习因子(weak learners)相结合来提升总体模型的预测准确度。在任意时间t,根据t-1时刻得到的结果我们给当前结果赋予一个权重。之前正确预测的结果获得较小权重,错误分类的结果得到较大权重。回归问题的处理方法也是相似的。
让我们用图像帮助理解:
对图3的输出结果的理解也是类似的。这个算法一直如此持续进行直到所有的学习模型根据它们的预测结果都被赋予了一个权重,这样我们就得到了一个总体上更为准确的预测模型。
现在你是否对Boosting更感兴趣了?不妨看看下面这些文章(主要讨论GBM):
总的来说GBM的参数可以被归为三类:
从树参数开始,首先一个决定树的大致结构是这样的:
现在我们看一看定义一个决定树所需要的参数。注意我在这里用的都是python里scikit-learn里面的术语,和其他软件比如R里用到的可能不同,但原理都是相同的。
在继续介绍其他参数前,我们先看一个简单的GBM二分类伪代码:
1. 初始分类目标的参数值
2. 对所有的分类树进行迭代:
2.1 根据前一轮分类树的结果更新分类目标的权重值(被错误分类的有更高的权重)
2.2 用训练的子样本建模
2.3 用所得模型对所有的样本进行预测
2.4 再次根据分类结果更新权重值
3. 返回最终结果
以上步骤是一个极度简化的BGM模型,而目前我们所提到的参数会影响2.2这一步,即建模的过程。现在我们来看看影响boosting过程的参数:
subsample
好了,现在我们已经介绍了树参数和boosting参数,此外还有第三类参数,它们能影响到模型的总体功能:
loss
我知道我列了太多的参数,所以我在我的GitHub里整理出了一张表,可以直接下载:GitHub地址
接下来要用的数据集来自Data Hackathon 3.x AV hackathon。比赛的细节可以在比赛网站上找到(http://datahack.analyticsvidhya.com/contest/data-hackathon-3x),数据可以从这里下载:http://www.analyticsvidhya.com/wp-content/uploads/2016/02/Dataset.rar。我对数据做了一些清洗:
EMI_Loan_Submitted_Missing
这个变量,当EMI_Loan_Submitted
变量值缺失时它的值为1,否则为0。然后舍弃了EMI_Loan_Submitted
。Interest_Rate_Missing
变量,类似于#3,当Interest_Rate
有值时它的值为0,反之为1,原来的Interest_Rate变量被舍弃了Loan_Amount_Applied
和 Loan_Tenure_Applied
的中位数填补了缺失值Loan_Amount_Submitted_Missing
变量,当Loan_Amount_Submitted
有缺失值时为1,反之为0,原本的Loan_Amount_Submitted
变量被舍弃Loan_Tenure_Submitted_Missing
变量,当Loan_Tenure_Submitted
有缺失值时为1,反之为0,原本的Loan_Tenure_Submitted
变量被舍弃Processing_Fee_Missing
变量,当Processing_Fee
有缺失值时为1,反之为0,原本的Processing_Fee
变量被舍弃你们可以从GitHub里data_preparation iPython notebook中看到这些改变。
首先,我们加载需要的library和数据:
#Import libraries:
import pandas as pd
import numpy as np
from sklearn.ensemble import GradientBoostingClassifier #GBM algorithm
from sklearn import cross_validation, metrics #Additional scklearn functions
from sklearn.grid_search import GridSearchCV #Perforing grid search
import matplotlib.pylab as plt
%matplotlib inline
from matplotlib.pylab import rcParams
rcParams['figure.figsize'] = 12, 4
train = pd.read_csv('train_modified.csv')
target = 'Disbursed'
IDcol = 'ID'
然后我们来写一个创建GBM模型和CV值的函数。
def modelfit(alg, dtrain, predictors, performCV=True, printFeatureImportance=True, cv_folds=5):
#Fit the algorithm on the data
alg.fit(dtrain[predictors], dtrain['Disbursed'])
#Predict training set:
dtrain_predictions = alg.predict(dtrain[predictors])
dtrain_predprob = alg.predict_proba(dtrain[predictors])[:,1]
#Perform cross-validation:
if performCV:
cv_score = cross_validation.cross_val_score(alg, dtrain[predictors], dtrain['Disbursed'], cv=cv_folds, scoring='roc_auc')
#Print model report:
print "\nModel Report"
print "Accuracy : %.4g" % metrics.accuracy_score(dtrain['Disbursed'].values, dtrain_predictions)
print "AUC Score (Train): %f" % metrics.roc_auc_score(dtrain['Disbursed'], dtrain_predprob)
if performCV:
print "CV Score : Mean - %.7g | Std - %.7g | Min - %.7g | Max - %.7g" % (np.mean(cv_score),np.std(cv_score),np.min(cv_score),np.max(cv_score))
#Print Feature Importance:
if printFeatureImportance:
feat_imp = pd.Series(alg.feature_importances_, predictors).sort_values(ascending=False)
feat_imp.plot(kind='bar', title='Feature Importances')
plt.ylabel('Feature Importance Score')
接着就要创建一个基线模型(baseline model)。这里我们用AUC来作为衡量标准,所以用常数的话AUC就是0.5。一般来说用默认参数设置的GBM模型就是一个很好的基线模型,我们来看看这个模型的输出和特征重要性:
#Choose all predictors except target & IDcols
predictors = [x for x in train.columns if x not in [target, IDcol]]
gbm0 = GradientBoostingClassifier(random_state=10)
modelfit(gbm0, train, predictors)
从图上看出,CV的平均值是0.8319,后面调整的模型会做得比这个更好。
之前说过,我们要调节的参数有两种:树参数和boosting参数。learning rate没有什么特别的调节方法,因为只要我们训练的树足够多learning rate总是小值来得好。
虽然随着决定树的增多GBM并不会明显得过度拟合,高learing rate还是会导致这个问题,但如果我们一味地减小learning rate、增多树,计算就会非常昂贵而且需要运行很长时间。了解了这些问题,我们决定采取以下方法调参:
为了决定boosting参数,我们得先设定一些参数的初始值,可以像下面这样:
注意我们目前定的都是初始值,最终这些参数的值应该是多少还要靠调参决定。现在我们可以根据learning rate的默认值0.1来找到所需要的最佳的决定树数量,可以利用网格搜索(grid search)实现,以10个数递增,从20测到80。
#Choose all predictors except target & IDcols
predictors = [x for x in train.columns if x not in [target, IDcol]]
param_test1 = {'n_estimators':range(20,81,10)}
gsearch1 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, min_samples_split=500,min_samples_leaf=50,max_depth=8,max_features='sqrt',subsample=0.8,random_state=10),
param_grid = param_test1, scoring='roc_auc',n_jobs=4,iid=False, cv=5)
gsearch1.fit(train[predictors],train[target])
来看一下输出结果:
gsearch1.grid_scores_, gsearch1.best_params_, gsearch1.best_score_
可以看出对于0.1的learning rate, 60个树是最佳的,而且60也是一个合理的决定树数量,所以我们就直接用60。但在一些情况下上面这段代码给出的结果可能不是我们想要的,比如:
树参数可以按照这些步骤调节:
num_samples_split
min_samples_leaf
需要注意一下调参顺序,对结果影响最大的参数应该优先调节,就像max_depth
和num_samples_split
。
重要提示:接着我会做比较久的网格搜索(grid search),可能会花上15-30分钟。你在自己尝试的时候应该根据电脑情况适当调整需要测试的值。
max_depth可以相隔两个数从5测到15,而min_samples_split
可以按相隔200从200测到1000。这些完全凭经验和直觉,如果先测更大的范围再用迭代去缩小范围也是可行的。
param_test2 = {'max_depth':range(5,16,2), 'min_samples_split':range(200,1001,200)}
gsearch2 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, n_estimators=60, max_features='sqrt', subsample=0.8, random_state=10),
param_grid = param_test2, scoring='roc_auc',n_jobs=4,iid=False, cv=5)
gsearch2.fit(train[predictors],train[target])
gsearch2.grid_scores_, gsearch2.best_params_, gsearch2.best_score_
从结果可以看出,我们从30种组合中找出最佳的max_depth是9,而最佳的min_smaples_split
是1000。1000是我们设定的范围里的最大值,有可能真正的最佳值比1000还要大,所以我们还要继续增加min_smaples_split
。树深就用9。接着就来调节min_samples_leaf
,可以测30,40,50,60,70这五个值,同时我们也试着调大min_samples_leaf
的值。
param_test3 = {'min_samples_split':range(1000,2100,200), 'min_samples_leaf':range(30,71,10)}
gsearch3 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, n_estimators=60,max_depth=9,max_features='sqrt', subsample=0.8, random_state=10),
param_grid = param_test3, scoring='roc_auc',n_jobs=4,iid=False, cv=5)
gsearch3.fit(train[predictors],train[target])
gsearch3.grid_scores_, gsearch3.best_params_, gsearch3.best_score_
这样min_samples_split
的最佳值是1200,而min_samples_leaf
的最佳值是60。注意现在CV值增加到了0.8396。现在我们就根据这个结果来重新建模,并再次评估特征的重要性。
modelfit(gsearch3.best_estimator_, train, predictors)
比较之前的基线模型结果可以看出,现在我们的模型用了更多的特征,并且基线模型里少数特征的重要性评估值过高,分布偏斜明显,现在分布得更加均匀了。
接下来就剩下最后的树参数max_features
了,可以每隔两个数从7测到19。
param_test4 = {'max_features':range(7,20,2)}
gsearch4 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, n_estimators=60,max_depth=9, min_samples_split=1200, min_samples_leaf=60, subsample=0.8, random_state=10),
param_grid = param_test4, scoring='roc_auc',n_jobs=4,iid=False, cv=5)
gsearch4.fit(train[predictors],train[target])
gsearch4.grid_scores_, gsearch4.best_params_, gsearch4.best_score_
最佳的结果是7,正好就是我们设定的初始值(平方根)。当然你可能还想测测小于7的值,我也鼓励你这么做。而按照我们的设定,现在的树参数是这样的:
min_samples_split
: 1200min_samples_leaf
: 60max_depth
: 9max_features
: 7接下来就可以调节子样本占总样本的比例,我准备尝试这些值:0.6,0.7,0.75,0.8,0.85,0.9。
param_test5 = {'subsample':[0.6,0.7,0.75,0.8,0.85,0.9]}
gsearch5 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, n_estimators=60,max_depth=9,min_samples_split=1200, min_samples_leaf=60, subsample=0.8, random_state=10,max_features=7),
param_grid = param_test5, scoring='roc_auc',n_jobs=4,iid=False, cv=5)
gsearch5.fit(train[predictors],train[target])
gsearch5.grid_scores_, gsearch5.best_params_, gsearch5.best_score_
给出的结果是0.85。这样所有的参数都设定好了,现在我们要做的就是进一步减少learning rate,就相应地增加了树的数量。需要注意的是树的个数是被动改变的,可能不是最佳的,但也很合适。随着树个数的增加,找到最佳值和CV的计算量也会加大,为了看出模型执行效率,我还提供了我每个模型在比赛的排行分数(leaderboard score),怎么得到这个数据不是公开的,你很难重现这个数字,它只是为了更好地帮助我们理解模型表现。
现在我们先把learning rate降一半,至0.05,这样树的个数就相应地加倍到120。
predictors = [x for x in train.columns if x not in [target, IDcol]]
gbm_tuned_1 = GradientBoostingClassifier(learning_rate=0.05, n_estimators=120,max_depth=9, min_samples_split=1200,min_samples_leaf=60, subsample=0.85, random_state=10, max_features=7)
modelfit(gbm_tuned_1, train, predictors)
排行得分:0.844139
接下来我们把learning rate进一步减小到原值的十分之一,即0.01,相应地,树的个数变为600。
predictors = [x for x in train.columns if x not in [target, IDcol]]
gbm_tuned_2 = GradientBoostingClassifier(learning_rate=0.01, n_estimators=600,max_depth=9, min_samples_split=1200,min_samples_leaf=60, subsample=0.85, random_state=10, max_features=7)
modelfit(gbm_tuned_2, train, predictors)
排行得分:0.848145
继续把learning rate缩小至二十分之一,即0.005,这时候我们有1200个树。
predictors = [x for x in train.columns if x not in [target, IDcol]]
gbm_tuned_3 = GradientBoostingClassifier(learning_rate=0.005, n_estimators=1200,max_depth=9, min_samples_split=1200, min_samples_leaf=60, subsample=0.85, random_state=10, max_features=7,
warm_start=True)
modelfit(gbm_tuned_3, train, predictors, performCV=False)
排行得分:0.848112
排行得分稍微降低了,我们停止减少learning rate,只单方面增加树的个数,试试1500个树。
predictors = [x for x in train.columns if x not in [target, IDcol]]
gbm_tuned_4 = GradientBoostingClassifier(learning_rate=0.005, n_estimators=1500,max_depth=9, min_samples_split=1200, min_samples_leaf=60, subsample=0.85, random_state=10, max_features=7,
warm_start=True)
modelfit(gbm_tuned_4, train, predictors, performCV=False)
排行得分:0.848747
看,就这么简单,排行得分已经从0.844升高到0.849了,这可是一个很大的提升。
还有一个技巧就是用“warm_start”选项。这样每次用不同个数的树都不用重新开始。所有的代码都可以从我的Github里下载到。
这篇文章详细地介绍了GBM模型。我们首先了解了何为boosting,然后详细介绍了各种参数。 这些参数可以被分为3类:树参数,boosting参数,和其他影响模型的参数。最后我们提到了用GBM解决问题的 一般方法,并且用AV Data Hackathon 3.x problem数据运用了这些方法。最后,希望这篇文章确实帮助你更好地理解了GBM,在下次运用GBM解决问题的时候也更有信心。