Python下Gradient Boosting Machine(GBM)调参完整指导
简介:如果你现在仍然将GBM作为一个黑盒使用,或许你应该点开这篇文章,看看他是如何工作的。
Boosting 算法在平衡偏差和方差方面扮演了重要角色。和bagging算法仅仅只能处理模型高方差不同,boosting在处理这两个方面都十分有效。真正地理解GBM能够给你处理这些关键问题的信心。
在这篇文章中,我将使用python去揭示GBM背后的科学知识,最重要的是你将学会如何调节这些参数并获得难以置信的结果。
内容目录:
- Boosting是如何工作的?
- 理解boosting参数
- 结合实例调参
一、Boosting是如何工作的?
Boosting是一个序列的集成算法,他整合了一系列弱分类器给出一个提升的预测精度。任何t时刻的输出结果是对之前t-1时刻输出结果的加权。正确分类的样本被赋予一个更低的权重,错误分类的样本被赋予一个更大的权重。回归问题采用相似的策略。
我们来进行一个更直观的理解:
注解:
- Box 1:第一个弱分类器(左起)
初始化所有点,具有相同的权值(通过大小来表示)
决策边界正确预测了2个正类和5个负类
- Box 2:第二个弱分类器
在Box 1中正确分类的样本被赋予更小的权值,反之亦然。
现在模型更关注高权重的点并将他们正确分类,但是现在其他点又被错误分类
相同的趋势可以在Box 3中观察到。进行多次迭代后,模型根据每个弱分类器的正确率对其预测结果进行加权整合输出。
- GBM参数
所有参数可以被分为三类:
- 树特定参数:这些参数影响模型中每棵独立的树。
- Boosting参数:这些参数影响模型的Boosting操作。
- 其他参数:其他的参数影响模型整体性能。
我将从树特定参数开始。首先,我们来看一棵决策树的一般结构:
定义一棵树的参数在后面会进一步解释。现在我将使用scikit-learn来进行说明。
- min_samples_split
定义在分裂时考虑的一个节点中的最小样本数量
用于控制过拟合。一个高的值将阻止模型学习一棵树中选择的特定样本所具有的高特异性关系。
太高的值可能会导致欠拟合,因此这个参数应该使用交叉验证来调节。
- min_samples_leaf
定义终端节点或叶子节点的最小样本数
和min_samples_split一样用于控制过拟合
一般会选择一个较低的值以处理样本类别不均衡问题,因为在多数类别中少数类别数量非常少。
- min_weight_fraction_leaf
和min_samples_leaf相似,只是min_samples_leaf为int值,而该参数为百分数。
- max_depth
定义一棵树的最大深度
用于控制过拟合,由于一个高的深度允许模型去学习特定样本具有的特异关系。
应该使用交叉验证调节
5、max_leaf_nodes
定义一棵树中终端节点或叶子节点的最大数量
可以代替max_depth参数定义,因为对于一个二叉树,一颗深度为n的树,将产生最多2^n个叶子。
如果这个参数被定义,GBM将忽略max_depth参数。
6、max_features
定义最优分裂时搜索的特征数目。
根据经验,一般设置为特征总数目的平方根,但我们应该最少对总特征数目的30%~40%进行搜索。
更高的值可能导致过拟合,视具体情况而定。
在讲解其他参数之前,我们先看看用于2分类的GBM算法的整体伪码:
1、初始化输出
2、从1迭代到模型中树的数目
2.1基于前面的预测结果更新样本权值
2.2在选择的子采样数据上拟合模型
2.3对所有测试集进行预测
2.4调整学习率,根据当前结果来更新输出结果
3、返回最终输出
这是对GBM工作原理的一个及其简单的阐述。上面我们已经介绍的参数将影响2.2步骤,即模型构建。下面我们将介绍管理Boosting的参数:
- learning_rate
该参数决定了对于每棵树的最终输出的影响(步骤2.4)。GBM以一个初始化的预测开始工作,然后根据每棵树的输出更进行新。学习率控制预测中这种变化的幅度。
较低的值通常是优选的,因为它们使模型对树的特定特性具有鲁棒性,从而使其具有更好的泛化性。
较低的值将会使模型需要更多的树来拟合所有的关系,并且将花费更高的计算代价。
- n_estimators
模型中序列化树的数目(步骤2)
虽然GBM在大量树上足够鲁棒,但是任然可能在一点上过拟合。因此,这个参数应该在一个特定学习率下使用交叉验证来调节。
3、Subsample
每一颗树选择的样本数(百分比),选择通常是随机采样
当值接近1时,通过减少方差来使模型更健壮
典型的值0.8可以工作的很好,但也可以进一步调节。
剩下的还有一些其他参数影响整体性能:
- Loss
定义每次分裂的最小损失函数
对于分类和回归情况,该参数有很多值。一般默认值就可以工作的很好。其他的值,如果你理解他对模型整体的影响,你也可以选择。
- Init
定义输出的初始化
如果我们使用另一个模型的输出作为GBM的初始化预测,则要使用这个参数
- random_state
定义随机数种子以保证每次产生相同的种子数
这对于参数调节非常重要。如果我们不固定种子数,对于相同的参数,我们的运行会得到不同的结果
- verbose
当模型拟合的时候,打印输出的类型,不同的值代表:
0:没有产生输出(默认)
1:在一定间隔时间树的产生输出
>1:所有树的产生输出
- warm_start
如何使用恰当,这个参数将有一个有趣的应用,并可以产生很大的帮助
使用这个参数,我们可以在之前拟合的模型上去拟合额外的树。这将节约很多时间,
- Presort
选择是否为更快的分割预置数据
该参数通过默认值进行自动选择,也可以根据需要来改变
- 结合实例的参数调节
我将采用来自Data Hackathon 3.x AV hackathon的数据集。详情请点击https://datahack.analyticsvidhya.com/contest/data-hackathon-3x/。
你可以在这里下载数据集https://www.analyticsvidhya.com/wp-content/uploads/2016/02/Dataset.rar
我进行了以下步骤的处理:
1、因为取值太多而去掉了City变量
2、DOB被转换成了Age,去掉DOB
3、EMI_Loan_Submitted_Missing:如果EMI_Loan_Submitted的值缺失,取值为1,否则取0。去掉原始的EMI_Loan_Submitted变量
4、因为太多取值,去掉EmployerName
5、因为仅有111个值缺失,对Existing_EMI采用0(中值)填充
6、Interest_Rate_Missing:如果Interest_Rate的值缺失,取值为1,否则取0,去掉原始Interest_Rate变量
7、因为对结果没有直观的影响,去掉Lead_Creation_Date
8、对Loan_Amount_Applied、Loan_Tenure_Applied进行中值填充
9、Loan_Amount_Submitted_Missing:如果Loan_Amount_Submitted的值缺失,取值为1,否则取0,去掉原始的Loan_Amount_Submitted变量
10、Loan_Tenure_Submitted_Missing:如果Loan_Tenure_Submitted的值缺失,取值为1,否则取0,去掉原始的Loan_Tenure_Submitted变量
11、去掉LoggedIn、Salary_Account变量
12、Processing_Fee_Missing:如果Processing_Fee的值缺失,取值为1,否则取0,去掉原始的Processing_Fee变量
13、Source:排名前2位的保持不变,其余的组合成不同类别
14、采用数值和独热编码
下面上代码:
#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模型和进行交叉验证。
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')
首先创建一个基准模型,在这种情况下,评价度量是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)
平均交叉验证分数是0.8319,我们可以对参数进行调整,使得模型变得更好。
一般调参方法:
正如我们之前讨论的,有两类的参数-基于树的和Boosting参数需要调整。由于一个较低的值总是能工作的很好,所以对learning rate没有最优值,对于给定的值,我们将在充足数量的树上进行训练。
虽然,随着树数量的增加,GBM足够健壮可以避免过拟合,但是在特定的learning rate下,太高数目的树还是可能导致过拟合。而且,随着我们降低learning rate增加树的数量,需要花费更昂贵的计算代价,需要运行更长的时间。
记住下面的我们采用的方法:
- 选择一个相对高的learning rate,一般可以是默认的0.1。但是在0.05-0.2之间应该可以解决不同的问题。
- 在这个learning rate下,确定树的最优数目。一般应该在40-70之间。记住,选择一个可以使你的系统运行比较快的值。这是因为它将被用于测试各种场景和确定树参数。
- 在确定的learning rate和树的数目下,调整树的特定参数。我们可以选择不同的参数取定义一棵树,我将在这里列举一个实例。
- 更低的learning rate,适当的增加树的数目以获得更加健壮的模型。
固定learning rate和树的数目以调节树的特定参数:
为了确定Boosting参数,我们需要初始化一些其他参数,我们进行如下设置:
1、min_samples_split = 500:应该设置为总样本的0.5%-1%,因为这是一个样本类别不平衡的问题,我们将在这个范围里采用一个小的值。
2、min_samples_leaf = 50:基于直觉设置,仅仅被用于避免过拟合,选择一个小的值同样是因为样本类别不平衡的问题。
3、max_depth = 8:基于训练集和测试集的数量,应该选择5-8,训练集有87K行,49列,我们设置为8.
4、max_features = ‘sqrt’:这个参数一般设置为特征数目的平方根。
5、subsample = 0.8:这是一个普遍采用的初始值。
请注意以上只是初始化的评估值,稍后我们将进行调整。我们将learning rate
设置为默认值0.1,来确定该learning rate下最优的树的数目。为了达到这个目的,我们采用网格搜索,测试值范围20-80,步长为10。
#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_
可以看到,在learning rate=0.1下,得到了最优树的数目为60。注意:60在这里是一个可以使用的合理的值,但并不是在所有情况下都一样。其他情况:
- 如果值在20左右,你或许应该尝试降低learning rate=0.05,然后再次运行网格搜索。
- 如果值太高,达到了100,你或许应该尝试一个更高的learning rate,调整其他参数可能会花费更多的时间。
调整树的特定参数:
现在我们来调整树的特定参数。我将按照一下步骤进行:
- 调整max_depth和num_samples_split
- 调整min_samples_leaf
- 调整max_features
参数调整的顺序应该仔细考虑。首先应该选择那些对输出结果影响最大的变量。例如,max_depth和num_samples_split对于结果有重要影响,应该首先调整。
重要提示:在这一部分我们将做一些繁重的网格搜索,可能会花费15-20分钟,甚至更长的时间,这取决于你的系统。你可以根据你的系统的处理能力来调整值的数目进行测试。
我将max_depth值的范围设为5-15,步长为2,num_samples_split值的范围设为200-1000,步长为200。这仅仅是根据我的经验。你也可以设置一个更宽的范围,然后通过多次迭代得到一个更小的范围。
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,num_samples_split=1000。注意,1000是我们测试的边界值,所以有可能最优值要大于1000,我们应该再测试一些更大的值。
这里,我将设置max_depth=9作为最优值,不尝试更高的num_samples_split下的不同值,这或许不是最优的方案,但进一步观察结果你会发现,在大多数的组合中,max_depth=9的结果要更好。然后我们将min_samples_leaf范围设置为30-70,步长为10,以及更高的num_samples_split值。
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_
这里,我们得到了最优的num_samples_split=1200,min_samples_leaf=60。我们可以看到,交叉验证分数增长到0.8396。我们再次拟合模型,来看看特征重要性。
modelfit(gsearch3.best_estimator_, train, predictors)
如果对比基准模型的特征重要性,你会发现现在我们能够从更多的变量中得到价值。而在之前,模型对于一些变量过于重视,但现在它已经被公平的分配了。
现在,我们来调整最后的树特定参数,即max_features,设置范围为7-19,步长为2。
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_
这里,得到最优值max_features=7,一般都是平方根。所以我们的初始值是最优的。你或许想要测试一个更低的值,如果你喜欢,当然可以。我将设置max_features=7。
因此,我最终的树特异参数为:
min_samples_split: 1200
min_samples_leaf: 60
max_depth: 9
max_features: 7
调整subsample参数以及使模型有一个更低的learning rate
下面我将采用不同的采样值:分别为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_
这里,我们得到最优值subsample=0.85。最终我们得到了所有参数,现在我们需要一个更低的学习率以及适当增加树的数目。注意:这些树或许不是最优值,但这是一个很好的基准。
随着树数量的增加采用交叉验证得到最优值将花费更大的计算代价。为了让你更好的理解模型性能,我提供了private leaderboard scores,由于数据未公开,你将无法复制该数据,但这有助于理解。
我们将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)
Private LB Score: 0.844139
我们将初始值降低至十分之一(即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)
Private LB Score: 0.848145
我们再将初始值降至二十分之一(即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)
Private LB Score: 0.848112
这里我们可以看到分数略有降低。所以我们将树的数目增加至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)
Private LB Score: 0.848747
因此,你可以清晰的看到这是非常重要的步骤,Private LB Score从0.844增加到0.849。
另一个可以使用的黑科技是GBM的‘warm_start’参数。你使用它取增加树的数目,不用每次都从头开始,,只需简单的几步以及测试不同的值。
最后总结:
这篇文章基于端到端的GBM模型。我们首先介绍了Boosting原理,然后详细讨论了所涉及的各种参数。根据它们对模型的影响,将参数分为3类,即树特定参数,Boosting参数和其他参数。
最后,我们讨论了解决GBM问题的一般方法,并通过该方法解决了AV Data Hackathon 3.x问题。
如果你觉得这篇文章对你有帮助,转载请注明出处!