LightGBM参数调优

LightGBM模型在各领域运用广泛,但想获得更好的模型表现,调参这一过程必不可少,下面我们就来聊聊LightGBM在sklearn接口下调参数的方法,也会在文末给出调参的代码模板。

概述

按经验预先固定的参数

  • learning_rate
  • n_estimators
  • min_split_gain
  • min_child_sample
  • min_child_weight

需要算法调节的参数

  • max_depth
  • num_leaves
  • subsample
  • colsample_bytree
  • reg_alpha
  • reg_lambda

LightGBM参数详解

LightGBM有众多参数,建议大家在使用LightGBM前,先仔细阅读参数介绍。

参数介绍传送门:

英文版:https://lightgbm.readthedocs.io/en/latest/Parameters.html

中文版:https://lightgbm.apachecn.org/#/docs/6

其他注解:https://medium.com/@gabrieltseng/gradient-boosting-and-xgboost-c306c1bcfaf5

我们提取对模型性能比较重要的参数来介绍下。

\[ w_j=\text{learning rate}\times\frac{\sum_{i\in I_j}\frac{\partial loss}{\partial(\hat{y}=0)}}{\sum_{i\in I_j}(\frac{\partial^2loss}{\partial(\hat{y}=0)^2})+\lambda} \]

  1. learning_rate: 学习率。默认设置为0.1,一般设置在0.05-0.1之间。选择比较小的学习率能获得稳定较好的模型性能。
  2. n_estimators: boosting的迭代次数。默认设置为100。一般根据数据集和特征数据选择100~1000之间。更保守的做法是设置一个较大的值配合early_stopping_round来让模型根据性能自动选择最好的迭代次数。选择比较大的迭代次数会在训练集获得比较好的性能但容易过拟合造成测试集的性能下降。
  3. min_split_gain: 执行节点分裂的最小增益。默认设置为0。不建议去调整。增大这个数值会得到相对浅的树深。可调整其他参数得到类似效果。
  4. min_child_sample: 一个叶子上的最小数据量。默认设置为20。根据数据量来确定,当数据量比较大时,应该提升这个数值,让叶子节点的数据分布相对稳定,提高模型的泛华能力。
  5. min_child_weight: 一个叶子上的最小hessian和。默认设置为0.001,一般设置为1。不建议调整,增大数值会得到较浅的树深。
  6. max_depth: 树模型的最大深度。防止过拟合的最重要的参数,一般限制为3~5之间。是需要调整的核心参数,对模型性能和泛化能力有决定性作用。
  7. num_leaves: 一棵树上的叶子节点个数。默认设置为31,和max_depth配合来空值树的形状,一般设置为(0, 2^max_depth - 1]的一个数值。是一个需要重点调节的参数,对模型性能影响很大。
  8. subsample: 若此参数小于1.0,LightGBM将会在每次迭代中在不进行重采样的情况下随机选择部分数据(row),可以用来加速训练及处理过拟合。默认设置为1,一般设置为0。8~1.0之间,防止过拟合。
  9. colsample_bytree: 若此参数小于1.0,LightGBM将会在每次迭代中随机选择部分特征(col),可以用来加速训练及处理过拟合。默认设置为1,一般设置为0.8~1.0之间,防止过拟合。

\[ L=\sum_{i=0}^nloss(y_{res},h(x))+\frac12\lambda\sum_{j=1}^Tw_j^2+\alpha\sum_{j=1}^T|w_j| \]

  1. reg_alpha: L1正则化参数,别名:lambda_l1。默认设置为0。一般经过特征选择后这个参数不会有特别大的差异,如果发现这个参数数值大,则说明有一些没有太大作用的特征在模型内。需要调节来控制过拟合
  2. reg_lambda: L2正则化参数,别名:lambda_l2。默认设置为0。较大的数值会让各个特征对模型的影响力趋于均匀,不会有单个特征把持整个模型的表现。需要调节来控制过拟合

调参建议

接下来介绍一下根据我们自己的经验和网上相关帖子总结出来的一点小经验供参考,不同参数在不同数据集上表现会有一定的差异性。

建议根据经验确定的参数:

1.learning_rate:

通常来说,学习率越小模型表现的最终表现容易获得比较好的结果,但是过小的学习率往往会导致模型的过拟合以及影响模型训练的时间。一般来说,在调参的过程中会预设一个固定的值如0.1或者0.05,再其他参数确定后再在0.05-0.2之间搜索一个不错的值作为最终模型的参数。通常在学习率较小的时候,n_estimators的数值会大,而学习率大的时候, n_estimators会比较小,他们是一对此消彼长的参数对。

2.n_estimators:

  1. 通常来说迭代次数越多模型表现越好,但是过大的迭代次往往会导致模型的过拟合以及影响模型训练的时间。一般我们选择的值在100~1000之间,训练时需要时刻关注过拟合的情况以便及时调整迭代次数。通常通过lgb.plot_metrics(model, metrics='auc)来观察学习曲线的变化,如果在测试集表现趋于下降的时候模型还没有停止训练就说明出现过拟合了。
  2. 通常为了防止过拟合,都会选一个比较大的n_estimators,然后设置early_stop_round为20, 50, 100来让模型停止在测试集效果还不错的地方,但如果模型过早的停止训练,比如只迭代了20次,那可能这样的结果是有问题的,需要再仔细研究下原因。
  3. 还有个通过交叉检验确定n_estimators的办法,但我们实验的结果表明没有加early-stop_round来的稳定,但也分享给大家,说不定在你的项目里有奇效。具体做法:跑3-5折的交叉检验,训练时加上early_stop_round,记录下每折模型停止时的n_estimators的数值,然后n_estimators取交叉检验模型停止的迭代次数的平均值的1.1倍。然后确定这个数值来调整其他参数,最终模型再通过early_stop_round得到最终的n_estimators的数值。

3.min_split_gain:

不建议去调整。增大这个数值会得到相对浅的树深。可调整其他参数得到类似效果。如果实在要调整,可以画出第一颗树和最后一颗树,把每次决策分叉的gain的数值画出来看一看大致范围,然后确定一个下限。但往往设置后模型性能会下降不少,所以如果不是过拟合很严重且没有其他办法缓解才建议调整这个参数。

4.min_child_sample:

这个参数需要根据数据集来确定,一般小数据集用默认的20就够了,但大数据集还用这个20的话会使得生成的叶子节点上数据量过小,这会出现数据集没有代表性的问题,所以建议按树深为4共16个叶子时平均的训练数据个数的25%的数值来确定这个参数或者在这个范围稍微搜索下。这样模型的稳定性会有所保障。

5.min_child_weight:

和min_child_sample的作用类似,但这个参数本身对模型的性能影响并不大,而且影响的方式不容易被人脑所理解,不建议过多的进行调整。

需要通过算法来搜索的参数:

  1. max_depth: 一般在3,4,5这三个数里挑一个就好了,设置过大的数值过拟合会比较严重。
  2. num_leaves: 在LightGBM里,叶子节点数设置要和max_depth来配合,要小于2max_depth-1。一般max_depth取3时,叶子数要<=23-1=7。如果比这个数值大的话,LightGBM可能会有奇怪的结果。在参数搜索时,需要用max_depth去限制num_leaves的取值范围。
  3. subsample: 不建议过度的精细的调节,比如用搜索算法搜一个0.814325这样一个数值就不是很好。一般给出大致的搜索范围如[0.8, 0.9, 1.0]这样几个比较整的数值就足够了。
  4. colsample_bytree: 和subsample同理,在[0.8, 0.9, 1.0]这样几个比较整的数值搜索就足够了。不建议过度调节。
  5. reg_alpha: 此参数服务于L1正则化,一般我们取0-1000的范围去进行调参。如果优化出来这个参数数值过大,则说明有一些不必要的特征可以剔除,可以先做特征筛选后再进行调参,然后调节出来模型效果好的时候reg_alpha是个相对小的数值,那我们对这个模型的信心会大很多。
  6. reg_lambda: 此参数服务于L2正则化,一般也是在0-1000的范围去进行调参。如果有非常强势的特征,可以人为加大一些reg_lambda使得整体特征效果平均一些,一般会比reg_alpha的数值略大一些,但如果这个参数大的夸张也需要再查看一遍特征是否合理。

总的来说,再开始时调参前应该做特征筛选,在确定特征后,根据数据规模和几个模型尝试的结果来初步敲定learning_rate, n_estimators, min_split_gain, min_child_sample, min_child_weight这几个参数,然后使用grid_search, Bayesian optimization或random search来调整 max_depth, num_leaves, subsample, colsample_bytree, reg_alpha, reg_lambda。其中重点要调节max_depth, num_leaves,并且注意他们的约束关系 num_leaves<=2^max_depth-1,其次subsample, colsample_bytree在[0.8, 0.9, 1.0]几个粗略的离散值上调整下即可,reg_alpha, reg_lambda在[0, 1000]的范围调整,最后比较好的模型上这俩参数值不应该过大,尤其是reg_alpha,过大的话需要查看特征。


贝叶斯优化调参实战

在参数调节中,常用的调参算法有

1 grid search

2 Bayesian optimization

3 random search

其中Bayesian optimization是个性价比比较高的方法,可以在比较短的时间内找出还不错的参数组合。但实际操作中,如果时间等得起,我们会同时使用这三个方法去搜参数然后对比下结果,找出三个算法都认为好的参数组合作为最终模型的结果。接下来给出详细的代码实操。

参数空间示例:

使用到hyperopt包定义参数空间:

http://hyperopt.github.io/hyperopt/getting-started/search_spaces/

1
2
3
4
5
6
7
8
space = {
'max_depth': hp.choice('max_depth', [3, 4, 5]),
'num_leaves': hp.choice('num_leaves', [5, 6, 7, 12, 13, 14, 15, 28, 29, 30, 31]),
'subsample': hp.choice('subsample', [0.8, 0.9, 1.0]),
'colsample_bytree': hp.choice('colsample_bytree', [0.8, 0.9, 1.0]),
'reg_alpha': hp.loguniform('reg_alpha', np.log(0.01), np.log(1000)),
'reg_lambda': hp.loguniform('reg_lambda', np.log(0.01), np.log(1000))
}

具体调参的类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
class LGBBO:
def __init__(self, fp_path, **kwargs):
self.fp_path = fp_path
self.iter = 0
self.train_set = None

self.kfold = kwargs.get('kfold', 3)
self.n_estimators = kwargs.get('n_estimators', 800)

csv_conn = open(self.fp_path, 'w')
writer = csv.writer(csv_conn)
writer.writerow(['loss', 'train_auc', 'valid_auc', 'train_ks', 'valid_ks',
'lst_train_auc', 'lst_valid_auc', 'lst_train_ks', 'lst_valid_ks',
'params', 'iteration', 'train_time'])
csv_conn.close()

def load_data(self, df_data, feature_list, label):
self.df_data = df_data.reset_index(drop=True)
self.feature_list = feature_list
self.label = label

def objective(self, params):
def eval_ks(ytrue, yprob):
fpr, tpr, thr = roc_curve(ytrue, yprob)
ks = max(tpr - fpr)
return "ks", ks, True

def eval_auc(ytrue, yprob):
auc = roc_auc_score(ytrue, yprob)
return "auc", auc, True

self.iter += 1
start = timer()
model = lgb.LGBMClassifier(**params,
learning_rate=0.1,
min_child_samples=20000,
objective='cross_entropy',
importance_type='gain',
class_weight='balanced',
boosting_type='gbdt', n_estimators=self.n_estimators,
silent=True, n_jobs=1, random_state=0
)

lst_train_auc, lst_valid_auc = list(), list()
lst_train_ks, lst_valid_ks = list(), list()

for k in range(self.kfold):
df_cv_train = self.df_data[self.df_data[f"cv{k}"] == 'train']
df_cv_valid = self.df_data[self.df_data[f"cv{k}"] == 'valid']
print(k, df_cv_train.shape, df_cv_valid.shape)
print(params)

eval_set = [(df_cv_train[self.feature_list], df_cv_train[self.label]),
(df_cv_valid[self.feature_list], df_cv_valid[self.label])
]

model.fit(df_cv_train[self.feature_list], df_cv_train[self.label],
eval_set=eval_set,
eval_metric=lambda ytrue, yprob: [eval_auc(ytrue, yprob), eval_ks(ytrue, yprob)],
early_stopping_rounds=50,
verbose=20)

yprob = model.predict_proba(df_cv_train[self.feature_list])[:, 1]
_, train_auc, _ = eval_auc(df_cv_train[self.label], yprob)
_, train_ks, _ = eval_ks(df_cv_train[self.label], yprob)
yprob = model.predict_proba(df_cv_valid[self.feature_list])[:, 1]
_, valid_auc, _ = eval_auc(df_cv_valid[self.label], yprob)
_, valid_ks, _ = eval_ks(df_cv_valid[self.label], yprob)

lst_train_auc.append(train_auc)
lst_valid_auc.append(valid_auc)
lst_train_ks.append(train_ks)
lst_valid_ks.append(valid_ks)

print(train_auc, valid_auc, train_ks, valid_ks)

run_time = timer() - start

train_auc_avg = np.mean(lst_train_auc)
valid_auc_avg = np.mean(lst_valid_auc)
train_ks_avg = np.mean(lst_train_ks)
valid_ks_avg = np.mean(lst_valid_ks)

loss = -valid_ks_avg

csv_conn = open(self.fp_path, 'a')
writer = csv.writer(csv_conn)

writer.writerow([loss,
train_auc_avg, valid_auc_avg,
train_ks_avg, valid_ks_avg,
lst_train_auc, lst_valid_auc,
lst_train_ks, lst_valid_ks,
params, self.iter, run_time])

res = {'loss': loss,
'train_auc': train_auc_avg, 'valid_auc': valid_auc_avg,
'train_ks': train_ks_avg, 'valid_ks': valid_ks_avg,
'lst_train_auc': lst_train_auc, 'lst_valid_auc': lst_valid_auc,
'lst_train_ks': lst_train_ks, 'lst_valid_ks': lst_valid_ks,
'params': params, 'iteration': self.iter, 'train_time': run_time,
'status': STATUS_OK}

print(self.iter)
print(res)

return res

def optimize(self, max_evals):
self.iter = 0

space = {
'max_depth': hp.choice('max_depth', [3, 4, 5]),
'num_leaves': hp.choice('num_leaves', [5, 6, 7, 12, 13, 14, 15, 28, 29, 30, 31]),
'subsample': hp.choice('subsample', [0.8, 0.9, 1.0]),
'colsample_bytree': hp.choice('colsample_bytree', [0.8, 0.9, 1.0]),
'reg_alpha': hp.loguniform('reg_alpha', np.log(0.01), np.log(1000)),
'reg_lambda': hp.loguniform('reg_lambda', np.log(0.01), np.log(1000))
}

best = fmin(fn=self.objective, space=space, algo=tpe.suggest, max_evals=max_evals,
trials=Trials(), rstate=np.random.RandomState(0))

print(best)
return best

调用样例代码:

1
2
3
4
5
6
7
8
9
10
11
kf = StratifiedKFold(n_splits=3, random_state=0, shuffle=True)
pdf_train = pdf_train.reset_index(drop=True)
for k, (itrain, ivalid) in enumerate(kf.split(pdf_train[selected_features], pdf_train[label])):
pdf_train[f"cv{k}"] = None
pdf_train.loc[itrain, f'cv{k}'] = 'train'
pdf_train.loc[ivalid, f'cv{k}'] = 'valid'

print('start tuning...')
bo = LGBBO(f'param.csv')
bo.load_data(pdf_train, selected_features, label)
bo.optimize(10000)

最后我们想说下,一般在样本不均衡时会额外调节scale_pos_weight这个参数,但在我们实际项目中,如果样本不是特别的偏,class_weight='balanced'就足够能产生不错的效果了,所以在参数调节中没有强调。一般情况下,这一整套调参流程跑下来是足够得到一个还不错的模型效果的参数的。


LightGBM参数调优
https://linxkon.github.io/LightGBM参数调优.html
作者
linxkon
发布于
2022年11月9日
许可协议