GBDT(sklearn/lightgbm)调参小结

GBDT(sklearn/lightgbm)调参小结

一、原理

我们知道GBDT是由许多回归树组成的森林,后一棵树采用前一颗树的预测结果与真实结果的残差来作为拟合目标,每棵树的生成过程都是一颗标准的回归树的生成过程。

GBDT的详细原理在这里

二、调参方法

对于基于决策树的模型,调参的方法都是大同小异。一般都需要如下步骤:

1、首先选择较高的学习率,大概0.1附近,这样是为了加快收敛的速度。这对于调参是很有必要的;
2、对决策树基本参数调参(估计器的数目、树的高度、叶节点的个数等);
3、正则化参数调参(限制叶节点样本数目、限制分裂、采样率、正则化参数);
4、最后降低学习率,这里是为了最后提高准确率。

三、sklearn调参过程

在sklearn中,GradientBoostingClassifier为GBDT的分类树, 而GradientBoostingRegressor为GBDT的回归类。两者的参数类型完全相同,当然有些参数比如损失函数loss的可选择项并不相同。

这些参数中,可以把重要参数分为两类,第一类是Boosting框架的重要参数,第二类是基学习器即CART回归树的重要参数。

1、boosting框架参数

首先,我们来看boosting框架相关的重要参数。由于GradientBoostingClassifier和GradientBoostingRegressor的参数绝大部分相同,我们下面会一起来讲,不同点会单独指出。

  • n_estimators: 也就是弱学习器的最大迭代次数,或者说最大的弱学习器的个数。一般来说n_estimators太小,容易欠拟合,n_estimators太大,又容易过拟合,一般选择一个适中的数值。默认是100。在实际调参的过程中,我们常常将n_estimators和下面介绍的参数learning_rate一起考虑。

  • learning_rate: 即每个弱学习器的权重缩减系数ν,也称作步长。强学习器的迭代公式为 f k ( x ) = f k − 1 ( x ) + ν h k ( x ) f_{k}(x) = f_{k-1}(x) + \nu h_k(x) fk(x)=fk1(x)+νhk(x)。ν的取值范围为 0 < ν ≤ 1 0<ν≤1 0<ν1。对于同样的训练集拟合效果,较小的ν意味着我们需要更多的弱学习器的迭代次数。通常我们用步长和迭代最大次数一起来决定算法的拟合效果。所以这两个参数n_estimators和learning_rate要一起调参。一般来说,可以从一个小一点的ν开始调参,默认是1。

  • subsample: 即子采样,取值为(0,1]。注意这里的子采样和随机森林不一样,随机森林使用的是放回抽样,而这里是不放回抽样。如果取值为1,则全部样本都使用,等于没有使用子采样。如果取值小于1,则只有一部分样本会去做GBDT的决策树拟合。选择小于1的比例可以减少方差,即防止过拟合,但是会增加样本拟合的偏差,因此取值不能太低。推荐在[0.5, 0.8]之间,默认是1.0,即不使用子采样。

  • init: 即初始化时候的弱学习器,对应GBDT原理里面的 f 0 ( x ) f_{0}(x) f0(x),如果不输入,则用训练集样本来做样本集的初始化分类回归预测。否则用init参数提供的学习器做初始化分类回归预测。一般用在我们对数据有先验知识,或者之前做过一些拟合的时候,如果没有的话就不用管这个参数了。

  • loss: 即我们GBDT算法中的损失函数。分类模型和回归模型的损失函数是不一样的。

    • 对于分类模型,有对数似然损失函数"deviance"和指数损失函数"exponential"两者输入选择。默认是对数似然损失函数"deviance"。一般来说,推荐使用默认的"deviance"。它对二元分离和多元分类各自都有比较好的优化。而指数损失函数等于把我们带到了Adaboost算法。
    • 对于回归模型,有均方差"ls", 绝对损失"lad", Huber损失"huber"和分位数损失“quantile”。默认是均方差"ls"。一般来说,如果数据的噪音点不多,用默认的均方差"ls"比较好。如果是噪音点较多,则推荐用抗噪音的损失函数"huber"。而如果我们需要对训练集进行分段预测的时候,则采用“quantile”。
  • alpha:这个参数只有GradientBoostingRegressor有,当我们使用Huber损失"huber"和分位数损失“quantile”时,需要指定分位数的值。默认是0.9,如果噪音点较多,可以适当降低这个分位数的值。

2、基学习器参数

由于GBDT使用了CART回归决策树,因此它的参数基本来源于决策树类,也就是说,和DecisionTreeClassifier和DecisionTreeRegressor的参数基本类似。

  • 划分时考虑的最大特征数max_features: 可以使用很多种类型的值,默认是"None",意味着划分时考虑所有的特征数;如果是"log2"意味着划分时最多考虑log2N个特征;如果是"sqrt"或者"auto"意味着划分时最多考虑N−−√个特征。如果是整数,代表考虑的特征绝对数。如果是浮点数,代表考虑特征百分比,即考虑(百分比xN)取整后的特征数。其中N为样本总特征数。一般来说,如果样本特征数不多,比如小于50,我们用默认的"None"就可以了,如果特征数非常多,我们可以灵活使用刚才描述的其他取值来控制划分时考虑的最大特征数,以控制决策树的生成时间。

  • 决策树最大深度max_depth: 默认可以不输入,如果不输入的话,默认值是3。一般来说,数据少或者特征少的时候可以不管这个值。如果模型样本量多,特征也多的情况下,推荐限制这个最大深度,具体的取值取决于数据的分布。常用的可以取值10-100之间。

  • 内部节点再划分所需最小样本数min_samples_split: 这个值限制了子树继续划分的条件,如果某节点的样本数少于min_samples_split,则不会继续再尝试选择最优特征来进行划分。 默认是2.如果样本量不大,不需要管这个值。如果样本量数量级非常大,则推荐增大这个值。

  • 叶子节点最少样本数min_samples_leaf: 这个值限制了叶子节点最少的样本数,如果某叶子节点数目小于样本数,则会和兄弟节点一起被剪枝。 默认是1,可以输入最少的样本数的整数,或者最少样本数占样本总数的百分比。如果样本量不大,不需要管这个值。如果样本量数量级非常大,则推荐增大这个值。

  • 叶子节点最小的样本权重和min_weight_fraction_leaf:这个值限制了叶子节点所有样本权重和的最小值,如果小于这个值,则会和兄弟节点一起被剪枝。 默认是0,就是不考虑权重问题。一般来说,如果我们有较多样本有缺失值,或者分类树样本的分布类别偏差很大,就会引入样本权重,这时我们就要注意这个值了。

  • 最大叶子节点数max_leaf_nodes: 通过限制最大叶子节点数,可以防止过拟合,默认是"None”,即不限制最大的叶子节点数。如果加了限制,算法会建立在最大叶子节点数内最优的决策树。如果特征不多,可以不考虑这个值,但是如果特征分成多的话,可以加以限制,具体的值可以通过交叉验证得到。

  • 节点划分最小不纯度min_impurity_split: 这个值限制了决策树的增长,如果某节点的不纯度(基于基尼系数,均方差)小于这个阈值,则该节点不再生成子节点。即为叶子节点 。一般不推荐改动默认值1e-7。

3、GBDT调参

首先引入类库:

import pandas as pd
import numpy as np
from sklearn.ensemble import GradientBoostingClassifier
from sklearn import cross_validation, metrics
from sklearn.grid_search import GridSearchCV

import matplotlib.pylab as plt

载入数据,看一眼数据类型分布:

# 准备数据
path = '/mnt/e/data/'
train = pd.read_csv(path + 'train_modified.csv')
target='Disbursed'    # Disbursed的值就是二元分类的输出
IDcol = 'ID'
print(train['Disbursed'].value_counts())

输出:

0    19680
1      320
Name: Disbursed, dtype: int64

划分数据:

x_columns = [x for x in train.columns if x not in [target, IDcol]]
X = train[x_columns]      # 样本
y = train['Disbursed']    # 标签

x_train, x_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=2019)

不管任何参数,都用默认的,我们拟合下数据看看:

gbm0 = GradientBoostingClassifier(random_state=2019)
gbm0.fit(x_train, y_train)

print("tr-accuracy: %.4g" % accuracy_score(y_train.values, gbm0.predict(x_train)))      # Accuracy : 0.9855
print("tr-AUC: %f" % roc_auc_score(y_train, gbm0.predict_proba(x_train)[:, 1]))      # AUC Score (Train): 0.910597

print("val-accuracy: %.4g" % accuracy_score(y_val, gbm0.predict(x_val)))      # Accuracy : 0.9825
print("val-AUC: %f" % roc_auc_score(y_val, gbm0.predict_proba(x_val)[:, 1]))      # AUC Score (Train): 0.827217

输出结果拟合的还可以,接下来调节参数,提高模型的泛化性能:

1.1 选择基学习器数目

首先我们从步长(learning rate)和迭代次数(n_estimators)入手。一般来说,开始选择一个较小的步长来网格搜索最好的迭代次数。

这里,我们将步长初始值设置为0.1。对于迭代次数进行网格搜索如下:

param_test1 = {'n_estimators':range(20,81,10)}

gsearch1 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, min_samples_split=300,
                        min_samples_leaf=20,max_depth=8,max_features='sqrt', subsample=0.8,random_state=2019),
                        param_grid = param_test1, scoring='roc_auc',iid=False,cv=5)
gsearch1.fit(X, y)

print(gsearch1.cv_results_['mean_test_score'], gsearch1.best_params_, gsearch1.best_score_)

输出:

[0.81264331 0.8131308  0.81313477 0.81276756 0.81276597 0.81117966 0.81096291] 
{'n_estimators': 40} 0.813134765625

找到了一个合适的迭代次数60,现在我们开始对决策树进行调参。

1.2 选择max_depth和min_samples_split

首先我们对决策树最大深度max_depth和内部节点再划分所需最小样本数min_samples_split进行网格搜索:

param_test2 = {'max_depth':range(3,14,2),
               'min_samples_split':range(100,801,200)}

gsearch2 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, n_estimators=60, min_samples_leaf=20,
                        max_features='sqrt', subsample=0.8, random_state=2019),
                        param_grid = param_test2, scoring='roc_auc',iid=False, cv=5)

gsearch2.fit(X, y)

print(gsearch2.cv_results_['mean_test_score'], gsearch2.best_params_, gsearch2.best_score_)

输出:

{'max_depth': 7, 'min_samples_split': 300}, 0.8213724275914632

可见最好的最大树深度是7,内部节点再划分所需最小样本数是300。

由于决策树深度7是一个比较合理的值,我们把它定下来,对于内部节点再划分所需最小样本数min_samples_split,我们暂时不能一起定下来,因为这个还和决策树其他的参数存在关联。下面我们再对内部节点再划分所需最小样本数min_samples_split和叶子节点最少样本数min_samples_leaf一起调参。

1.3 选择min_samples_split和min_samples_leaf
param_test3 = {'min_samples_split':range(800,1900,200),
               'min_samples_leaf':range(60,101,10)}

gsearch3 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, n_estimators=60,max_depth=7,
                        max_features='sqrt', subsample=0.8, random_state=2019),
                        param_grid = param_test3, scoring='roc_auc',iid=False, cv=5, verbose=3)

gsearch3.fit(X, y)

print(gsearch3.cv_results_['mean_test_score'], gsearch3.best_params_, gsearch3.best_score_)

输出:

{'min_samples_leaf': 60, 'min_samples_split': 1200}, 0.8222032996697154

可见这个min_samples_leaf在边界值,还有进一步调试小于边界60的必要。由于这里只是例子,所以大家可以自己下来用包含小于60的网格搜索来寻找合适的值。

调了这么多参数了,终于可以都放到GBDT类里面去看看效果了。现在我们用新参数拟合数据:

gbm1 = GradientBoostingClassifier(learning_rate=0.1, n_estimators=60,max_depth=7, min_samples_leaf =60,
                                min_samples_split =1200, max_features='sqrt', subsample=0.8, random_state=2019)
gbm1.fit(x_train, y_train)

print("tr-accuracy: %.4g" % accuracy_score(y_train.values, gbm1.predict(x_train)))      # Accuracy : 0.9841
print("tr-AUC: %f" % roc_auc_score(y_train, gbm1.predict_proba(x_train)[:, 1]))      # AUC Score (Train): 0.907378

print("val-accuracy: %.4g" % accuracy_score(y_val, gbm1.predict(x_val)))      # Accuracy : 0.9835
print("val-AUC: %f" % roc_auc_score(y_val, gbm1.predict_proba(x_val)[:, 1]))      # AUC Score (Train): 0.845787

对比最开始完全不调参的拟合效果,可见精确度稍有下降,主要原理是我们使用了0.8的子采样,20%的数据没有参与拟合。

但是验证集上的预测结果相对提高一些,说明泛化性能变好了。

1.4 选择max_features:特征采样

现在我们再对最大特征数max_features进行网格搜索。

param_test4 = {'max_features':range(7,20,2)}

gsearch4 = GridSearchCV(estimator = GradientBoostingClassifier(learning_rate=0.1, n_estimators=60,max_depth=7, min_samples_leaf =60,
                        min_samples_split =1200, subsample=0.8, random_state=2019),
                        param_grid = param_test4, scoring='roc_auc',iid=False, cv=5)
gsearch4.fit(X, y)

print(gsearch4.cv_results_['mean_test_score'], gsearch4.best_params_, gsearch4.best_score_)

输出:

{'max_features': 9}, 0.822412506351626
1.5 选择subsample:样本采样

现在我们再对子采样的比例进行网格搜索:

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=7, min_samples_leaf =60,
                        min_samples_split =1200, max_features=9, random_state=2019),
                        param_grid = param_test5, scoring='roc_auc',iid=False, cv=5)
gsearch5.fit(X, y)

print(gsearch5.cv_results_['mean_test_score'], gsearch5.best_params_, gsearch5.best_score_)

输出:

{'subsample': 0.7}, 0.8234378969766262

现在我们基本已经得到所有调优的参数结果。放到GBDT里面去看看效果:

gbm2 = GradientBoostingClassifier(learning_rate=0.1, n_estimators=60,max_depth=7, min_samples_leaf =60,
               min_samples_split =1200, max_features=9, subsample=0.7, random_state=2019)
gbm2.fit(x_train, y_train)

print("tr-accuracy: %.4g" % accuracy_score(y_train.values, gbm2.predict(x_train)))      # Accuracy : 0.9841
print("tr-AUC: %f" % roc_auc_score(y_train, gbm2.predict_proba(x_train)[:, 1]))      # AUC Score (Train): 0.898388

print("val-accuracy: %.4g" % accuracy_score(y_val, gbm2.predict(x_val)))      # Accuracy : 0.9835
print("val-AUC: %f" % roc_auc_score(y_val, gbm2.predict_proba(x_val)[:, 1]))      # AUC Score (Train): 0.827899

相对于gbm1,又进一步降低了特征采样和样本采样的比例,故测试数据上的精确度和AUC稍有下降;

但是验证集上的预测结果AUC又提高一些,说明泛化性能更好。

1.6 提高拟合能力和泛化能力

这时我们可以减半步长,最大迭代次数加倍来增加我们模型的拟合能力和泛化能力。再次拟合我们的模型:

gbm3 = GradientBoostingClassifier(learning_rate=0.05, n_estimators=120,max_depth=7, min_samples_leaf =60,
               min_samples_split =1200, max_features=9, subsample=0.7, random_state=2019)
gbm3.fit(x_train, y_train)

print("tr-accuracy: %.4g" % accuracy_score(y_train.values, gbm3.predict(x_train)))      # Accuracy : 0.9841
print("tr-AUC: %f" % roc_auc_score(y_train, gbm3.predict_proba(x_train)[:, 1]))      # AUC Score (Train): 0.904004

print("val-accuracy: %.4g" % accuracy_score(y_val, gbm3.predict(x_val)))      # Accuracy : 0.9835
print("val-AUC: %f" % roc_auc_score(y_val, gbm3.predict_proba(x_val)[:, 1]))      # AUC Score (Train): 0.841206

可以看到训练集和验证集的AUC都有一定提高,说明减半步长,最大迭代次数加倍可以提高模型的拟合能力和泛化能力。

下面我们继续将步长缩小5倍,最大迭代次数增加5倍,继续拟合我们的模型:

gbm4 = GradientBoostingClassifier(learning_rate=0.01, n_estimators=600,max_depth=7, min_samples_leaf =60,
               min_samples_split =1200, max_features=9, subsample=0.7, random_state=2019)
gbm4.fit(x_train, y_train)

print("tr-accuracy: %.4g" % accuracy_score(y_train.values, gbm4.predict(x_train)))      # Accuracy : 0.9841
print("tr-AUC: %f" % roc_auc_score(y_train, gbm4.predict_proba(x_train)[:, 1]))      # AUC Score (Train): 0.907238

print("val-accuracy: %.4g" % accuracy_score(y_val, gbm4.predict(x_val)))      # Accuracy : 0.9835
print("val-AUC: %f" % roc_auc_score(y_val, gbm4.predict_proba(x_val)[:, 1]))      # AUC Score (Train): 0.839831

最后我们继续步长缩小一半,最大迭代次数增加2倍,拟合我们的模型:

gbm5 = GradientBoostingClassifier(learning_rate=0.005, n_estimators=1200,max_depth=7, min_samples_leaf =60,
               min_samples_split =1200, max_features=9, subsample=0.7, random_state=2019)
gbm5.fit(x_train, y_train)

print("tr-accuracy: %.4g" % accuracy_score(y_train.values, gbm5.predict(x_train)))      # Accuracy : 0.9841
print("tr-AUC: %f" % roc_auc_score(y_train, gbm5.predict_proba(x_train)[:, 1]))      # AUC Score (Train): 0.908706

print("val-accuracy: %.4g" % accuracy_score(y_val, gbm5.predict(x_val)))      # Accuracy : 0.9835
print("val-AUC: %f" % roc_auc_score(y_val, gbm5.predict_proba(x_val)[:, 1]))      # AUC Score (Train): 0.839446

我们发现训练集上的AUC一直在提高,但是验证集上的AUC开始下降,这说明减半步长,最大迭代次数加倍一定可以提高模型的拟合能力,但随着模型的复杂度提高,拟合能力上升,其泛化能力开始下降,故模型的迭代次数不能持续增加。

根据迭代次数,绘制模型在验证集上AUC的变化:

estimators = range(20, 400, 20)
val_AUC = []

for estimator in estimators:
    gbm5 = GradientBoostingClassifier(learning_rate=0.05, n_estimators=estimator,max_depth=7, min_samples_leaf =60,
                   min_samples_split =1200, max_features=9, subsample=0.7, random_state=2019)
    gbm5.fit(x_train, y_train)

    print("estimators: %d, val-accuracy: %.4g" % (estimator, accuracy_score(y_val, gbm5.predict(x_val))))
    print("estimators: %d, val-AUC: %f" % (estimator, roc_auc_score(y_val, gbm5.predict_proba(x_val)[:, 1])))

    val_AUC.append(roc_auc_score(y_val, gbm5.predict_proba(x_val)[:, 1]))

plt.plot(estimators, val_AUC)
plt.xlabel(r"Estimators number")
plt.xlabel(r"AUC on validation set")
plt.title(r"The AUC change of the validation set")

plt.show()

GBDT(sklearn/lightgbm)调参小结_第1张图片
由上图可知,当基学习器的数量达到150左右时,验证集的AUC达到最高,继续增加基学习器数目,泛化性能下降。

四、lightgbm调参过程

首先引入类库:

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split, GridSearchCV
import lightgbm as lgb
from sklearn.preprocessing import MinMaxScaler, OneHotEncoder, LabelEncoder
from sklearn.metrics import log_loss

对输入数据作如下处理:

print('开始one-hot...')
for col in category_feature:
    onehot_feats = pd.get_dummies(data[col], prefix = col)
    data.drop([col], axis = 1, inplace = True)
    data = pd.concat([data, onehot_feats], axis = 1)
print('one-hot结束')

train = data[data['Label'] != -1]
target = train.pop('Label')
test = data[data['Label'] == -1]
test.drop(['Label'], axis = 1, inplace = True)

# 划分数据集
print('划分数据集...')
x_train, x_val, y_train, y_val = train_test_split(train, target, test_size = 0.2, random_state = 2018)
1、选择估计器数目

先把学习率设定为一个较高的值,比如:取 learning_rate = 0.1,为了确定估计器的数目,也就是boosting迭代的次数,或者说是残差树的数目,参数名n_estimators / num_iterations / num_round / num_boost_round

我们可以先将该参数设成一个较大的数,然后在cv结果中查看最优的迭代次数;

在这之前,我们必须给其他重要参数一个初始值。初始值的意义不大,只是为了方便确定其他参数。下面先给定一下初始值:

params = {
        'boosting_type': 'gbdt',
        'objective': 'binary',    # 用于分类

        'learning_rate': 0.1,    # 选定一较高的值,通常是0.1
        'num_leaves': 50,    # 由于lightGBM是leaves_wise生长,官方说法是要小于2^max_depth
        'max_depth': 6,    # 由于数据集不大,所以选择一个适中的值,4-10都可以

        'subsample': 0.8,    # 数据采样
        'colsample_bytree': 0.8,    # 特征采样
    }

下面采用LightGBM的cv函数得到合适的估计器数目:

data_train = lgb.Dataset(x_train, y_train, silent=True)
cv_results = lgb.cv(params, data_train, num_boost_round=1000, nfold=5, stratified=False, shuffle=True, 
                    metrics='binary_logloss', early_stopping_rounds=50, verbose_eval=50, show_stdv=True, seed=2019)
print('best n_estimators:', len(cv_results['binary_logloss-mean']))
print('best cv score:', cv_results['binary_logloss-mean'][-1])

输出结果:

[50]	cv_agg's binary_logloss: 0.479664 + 0.0450307
[100]	cv_agg's binary_logloss: 0.515577 + 0.0542185
best n_estimators: 14
best cv score: 0.4673083963694773

由于数据集不大,所以在学习率为0.1时,最优的迭代次数只有14。接下来,我们就可以代入(0.1, 43)进入其他参数的整定。当然在硬件条件允许的条件下,学习率还是越小越好。

2、max_depth 和 num_leaves

这两个参数是提高精确度的最重要的参数。

  • max_depth :设置树深度,深度太大可能过拟合;
  • num_leaves:因为 LightGBM 使用的是 leaf-wise 的算法,因此在调节树的复杂程度时,使用的是 num_leaves 而不是 max_depth。大致换算关系: n u m _ l e a v e s = 2 m a x _ d e p t h num\_leaves = 2^{max\_depth} num_leaves=2max_depth,但是它的值的设置应该小于 2 m a x _ d e p t h 2^{max\_depth} 2max_depth,否则可能会导致过拟合。

我们可以同时调节这两个参数,对这两个参数调优,先粗调,再细调:

调参一般是采用网格搜索的方法,首先引入sklearn里的GridSearchCV()函数进行搜索。

print('网格搜索,参数优化')
param_grid = {'max_depth': range(3, 8, 2),
              'num_leaves': range(20, 60, 20)
             }

estimator = lgb.LGBMClassifier(objective='binary',
                        learning_rate=0.1,
                        n_estimators=14,
                        subsample=0.8,
                        colsample_bytree=0.8,
                        num_leaves=50,
                        )

gbm = GridSearchCV(estimator, param_grid, scoring='neg_log_loss', cv=5, verbose=5)

gbm.fit(x_train, y_train,
        eval_set=[(x_val, y_val)],
        eval_names=['val'],
        )

print('Best parameters found by grid search are:', gbm.best_params_, gbm.best_score_)

输出结果:

Best parameters found by grid search are: {'max_depth': 3, 'num_leaves': 20} -0.4685151604326247

由于这里采用的测试数据较少,所以网格搜索得到的树深度较小,也是比较合理的,这里取树的深度为3,同时叶节点的个数也要做出调整,应该小于8。

这里必须说一下,sklearn模型评估里的scoring参数都是采用的higher return values are better than lower return values(较高的返回值优于较低的返回值)。

但是,lightgbm的metric策略采用的是log_loss,越低越好,所以sklearn就提供了neg_log_loss参数,也就是返回metric的负数,所以metric变成负数也就是越大越好了。

至此,我们将这步得到的最优解代入第三步。其实,这里只进行了粗调,如果要得到更好的效果,可以将max_depth在3附近多取几个值,num_leaves在8附近多取几个值:

param_grid = {'max_depth': [2, 3, 4],
              'num_leaves': range(2, 8, 2)
             }

最大深度为3是没问题的,但是看细节的话,发现在最大深度为7的情况下,叶结点的数量对分数并没有影响。

3、min_data_in_leaf 和 min_sum_hessian_in_leaf

下面要开始降低过拟合了,调节这两个参数:

  • min_data_in_leaf:是一个很重要的参数, 也叫min_child_samples,它的值取决于训练数据的样本个数和num_leaves。将其设置的较大可以避免生成一个过深的树, 但有可能导致欠拟合。
  • min_sum_hessian_in_leaf:也叫min_child_weight,使一个结点分裂的最小海森值之和(Minimum sum of hessians in one leaf to allow a split. Higher values potentially decrease overfitting)。

采用跟上面相同的方法进行网格搜索:

param_grid = {'min_child_samples': [16, 18, 20],
              'min_child_weight':[0.0005, 0.001, 0.01]
                 }

输出结果:

Best parameters found by grid search are: {'min_child_weight': 0.001, 'min_child_samples': 18} -0.46680694245942184

我经过粗调后细调的结果,可以看到,min_data_in_leaf的最优值为18,仔细查看输出数据,发现min_sum_hessian_in_leaf对最后的值影响较小,最终选择18和0.001。这里的neg_log_loss相对之前的参数有所提升。

4、feature_fraction 和 bagging_fraction

这两个参数都是为了降低过拟合。

  • feature_fraction参数来进行特征的子抽样。这个参数可以用来防止过拟合及提高训练速度。
  • bagging_fraction+bagging_freq参数必须同时设置,bagging_fraction相当于subsample样本采样,可以使bagging更快的运行,同时也可以降拟合。bagging_freq默认0,表示bagging的频率,0意味着没有使用bagging,k意味着每k轮迭代进行一次bagging。

采用跟上面相同的方法进行网格搜索:

param_grid={
    'feature_fraction': [0.5, 0.6, 0.7, 0.8, 0.9],
    'bagging_fraction': [0.6, 0.7, 0.8, 0.9, 1.0]
}

estimator = lgb.LGBMClassifier(objective='binary',
                            learning_rate=0.1,
                            n_estimators=14,
                            max_depth=3,
                            num_leaves=4,
                            min_child_samples=18,
                            min_child_weight=0.001,
                            bagging_freq=5,
                            )

gbm = GridSearchCV(estimator, param_grid, scoring='neg_log_loss', cv=5, verbose=5)

gbm.fit(x_train, y_train,
        eval_set=[(x_train, y_train), (x_val, y_val)],
        eval_names=['train', 'val'],
        )

print('Best parameters found by grid search are:', gbm.best_params_, gbm.best_score_)

输出结果:

Best parameters found by grid search are: {'feature_fraction': 0.8, 'bagging_fraction': 1.0} -0.46680694245942184

从这里可以看出来,bagging_feaction和feature_fraction的理想值分别是1.0和0.8,一个很重要原因就是,样本数量比较小,但是特征数量较多。

5、正则化参数

正则化参数lambda_l1(reg_alpha), lambda_l2(reg_lambda),用于降低过拟合,两者分别对应l1正则化和l2正则化。我们也来尝试一下使用这两个参数。

param_grid = {'reg_alpha': [0, 0.01, 0.5],
                  'reg_lambda': [0, 0.01, 0.5]
                 }

    estimator = lgb.LGBMClassifier(objective='binary',
                            learning_rate=0.1,
                            n_estimators=14,
                            max_depth=3,
                            num_leaves=4,
                            min_child_samples=18,
                            min_child_weight=0.001,
                            feature_fraction=0.8,
                            )

    gbm = GridSearchCV(estimator, param_grid, scoring='neg_log_loss', cv=5, verbose=5)

    gbm.fit(x_train, y_train,
            eval_set=[(x_train, y_train), (x_val, y_val)],
            eval_names=['train', 'val'],
            )

    print('Best parameters found by grid search are:', gbm.best_params_, gbm.best_score_)

输出结果:

Best parameters found by grid search are: {'reg_alpha': 0, 'reg_lambda': 0} -0.46680694245942184

看来不需要加入正则化。

6、降低learning_rate

前面使用较高的学习速率是因为可以让收敛更快,但是准确度肯定没有细水长流来的好。最后,我们使用较低的学习速率,以及使用更多的决策树n_estimators来训练数据,看能不能可以进一步的优化分数。

我们将前面优化好的参数带入lightGBM的cv函数:

params = {
        'boosting_type': 'gbdt',
        'objective': 'binary',

        'learning_rate': 0.005,
        'max_depth': 3,
        'num_leaves': 4,
        'min_child_samples': 18,
        'min_child_weight': 0.001,
        'feature_fraction': 0.8,
    }

data_train = lgb.Dataset(x_train, y_train, silent=True)
cv_results = lgb.cv(params, data_train, num_boost_round=10000, nfold=5, stratified=False, shuffle=True,
                    metrics='binary_logloss', early_stopping_rounds=50, verbose_eval=50, show_stdv=True, seed=2019)

print('best n_estimators:', len(cv_results['binary_logloss-mean']))
print('best cv score:', cv_results['binary_logloss-mean'][-1])

输出结果:

开始one-hot...
one-hot结束
划分数据集...
[50]	cv_agg's binary_logloss: 0.489786 + 0.0384423
[100]	cv_agg's binary_logloss: 0.483116 + 0.0397041
[150]	cv_agg's binary_logloss: 0.478844 + 0.0407797
[200]	cv_agg's binary_logloss: 0.475892 + 0.0405198
[250]	cv_agg's binary_logloss: 0.47361 + 0.0399196
[300]	cv_agg's binary_logloss: 0.471568 + 0.0397002
[350]	cv_agg's binary_logloss: 0.469862 + 0.0397894
[400]	cv_agg's binary_logloss: 0.468318 + 0.0398841
[450]	cv_agg's binary_logloss: 0.467538 + 0.039813
[500]	cv_agg's binary_logloss: 0.466849 + 0.0397821
[550]	cv_agg's binary_logloss: 0.466432 + 0.039553
[600]	cv_agg's binary_logloss: 0.46607 + 0.0395426
[650]	cv_agg's binary_logloss: 0.465791 + 0.0393371
[700]	cv_agg's binary_logloss: 0.465417 + 0.039221
[750]	cv_agg's binary_logloss: 0.465257 + 0.0390924
[800]	cv_agg's binary_logloss: 0.465179 + 0.0390695
best n_estimators: 786
best cv score: 0.46507833000170373
结束

可见,最终的指数损失有所降低。

7、总结

这里只是采用较少的数据,目的在于总结调参的一般过程;这个过程可以对以后的工作有所指导,使模型的调参过程尽量流程化,规范化,不会太过主观。

五、特征重要度

基于决策树的算法都可以输出特征的重要度,其中sklearn中GBDT的特征重要度函数为feature_importances_,lightgbm中的特征重要度函数为feature_importance,这里以sklearn中的GBDT为例,输出特征重要度:

def plot_feature_importance(dataset, model_bst):
    list_feature_name = list(dataset.columns[:])
    # list_feature_importance = list(model_bst.feature_importance(importance_type='split', iteration=-1))
    list_feature_importance = list(model_bst.feature_importances_)
    dataframe_feature_importance = pd.DataFrame(
        {'feature_name': list_feature_name, 'importance': list_feature_importance})
    dataframe_feature_importance20 = dataframe_feature_importance.sort_values(by='importance', ascending=False)[:20]
    print(dataframe_feature_importance20)
    x = range(len(dataframe_feature_importance20['feature_name']))
    plt.xticks(x, dataframe_feature_importance20['feature_name'], rotation=90, fontsize=8)
    plt.plot(x, dataframe_feature_importance20['importance'])
    plt.xlabel("Feature name")
    plt.ylabel("Importance")
    plt.title("The importance of features")
    plt.show()


gbm6 = GradientBoostingClassifier(learning_rate=0.05, n_estimators=160,max_depth=7, min_samples_leaf =60,
                   min_samples_split =1200, max_features=9, subsample=0.7, random_state=2019)

gbm6.fit(x_train, y_train)

plot_feature_importance(x_train, gbm6)

输出:
GBDT(sklearn/lightgbm)调参小结_第2张图片
上图显示模型中最重要的20个特征。

特征重要性的计算方法:

(1)Decision Tree:
GBDT(sklearn/lightgbm)调参小结_第3张图片

该特征带来的信息增益的总量(需要经过标准化). 也被称为基尼重要性。

(2)XGBoost:

importance_type (string, default “gain”) – The feature importance type for the feature_importances_ property: either “gain”, “weight”, “cover”, “total_gain” or “total_cover”.

默认特征重要度度量方式为:得分增益gain,也可以修改为其他方式:weight、cover、total_gain、total_cover。

(3)lightgbm:
GBDT(sklearn/lightgbm)调参小结_第4张图片

split: 使用该特征的次数。
gain: 该特征的总增益。

本文参考和引用自以下文章:
1、scikit-learn 梯度提升树(GBDT)调参小结
2、LightGBM 调参方法(具体操作)

3、使用LightGBM完成训练及预测任务
4、树模型中, 特征重要性的计算方法

你可能感兴趣的:(机器学习,GBDT,sklearn,lightgbm,调参)