为什么我无法匹配LGBM的交叉验证分数?

3

我无法手动匹配LGBM的交叉验证分数。

以下是一个MCVE示例:

from sklearn.datasets import load_breast_cancer
import pandas as pd
from sklearn.model_selection import train_test_split, KFold
from sklearn.metrics import roc_auc_score
import lightgbm as lgb
import numpy as np

data = load_breast_cancer()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = pd.Series(data.target)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

folds = KFold(5, random_state=42)

params = {'random_state': 42}

results = lgb.cv(params, lgb.Dataset(X_train, y_train), folds=folds, num_boost_round=1000, early_stopping_rounds=100, metrics=['auc'])
print('LGBM\'s cv score: ', results['auc-mean'][-1])

clf = lgb.LGBMClassifier(**params, n_estimators=len(results['auc-mean']))

val_scores = []
for train_idx, val_idx in folds.split(X_train):
    clf.fit(X_train.iloc[train_idx], y_train.iloc[train_idx])
    val_scores.append(roc_auc_score(y_train.iloc[val_idx], clf.predict_proba(X_train.iloc[val_idx])[:,1]))
print('Manual score: ', np.mean(np.array(val_scores)))

我原本希望两个CV分数是相同的——我设置了随机种子,并且做了完全相同的事情。但它们却不同。
以下是我得到的输出:
"最初的回答"
LGBM's cv score:  0.9851513530737058
Manual score:  0.9903622177441328

为什么我使用LGMB的cv模块时有问题?最初的回答。

1
你的手动分类器是否没有使用相同的 num_boost_roundearly_stopping_rounds 参数?我看到你在 __init__ 方法或调用 fit 时都没有显式传递它们。 - devforfu
num_boost_roundn_estimators是一样的。如果我明确地设置了估计器数量,就不需要提前停止。 - EuRBamarth
啊,好的,但如果我们在一个情况下启用了早停止,而在另一个情况下没有启用,这可能是差异的原因吗? - devforfu
在“cv”情况下,您设置了最大迭代次数,并在您的交叉验证分数未改善超过“early_stopping_rounds”次迭代时停止。在另一种情况下,我直接将该数字设置为要处理的迭代次数。 - EuRBamarth
1个回答

8
你正在将 X 分成 X_train 和 X_test 两部分。对于交叉验证,你将 X_train 再次分成了 5 个部分,而手动分割 X 则是将其分为 5 个部分。也就是说,手动分割比交叉验证使用了更多的数据点。
results = lgb.cv(params, lgb.Dataset(X_train, y_train) 改为 results = lgb.cv(params, lgb.Dataset(X, y)
此外,还可能会有不同的参数。例如,lightgbm 使用的线程数量会改变结果。在交叉验证期间,模型是并行拟合的。因此,所使用的线程数可能与手动顺序训练时不同。
在进行第一次更正后,你可以使用以下代码来实现手动分割 / 交叉验证获得相同的结果:
from sklearn.datasets import load_breast_cancer
import pandas as pd
from sklearn.model_selection import train_test_split, KFold
from sklearn.metrics import roc_auc_score
import lightgbm as lgb
import numpy as np

data = load_breast_cancer()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = pd.Series(data.target)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

folds = KFold(5, random_state=42)


params = {
        'task': 'train',
        'boosting_type': 'gbdt',
        'objective':'binary',
        'metric':'auc',
        }

data_all = lgb.Dataset(X_train, y_train)

results = lgb.cv(params, data_all, 
                 folds=folds.split(X_train), 
                 num_boost_round=1000, 
                 early_stopping_rounds=100)

print('LGBM\'s cv score: ', results['auc-mean'][-1])

val_scores = []
for train_idx, val_idx in folds.split(X_train):

    data_trd = lgb.Dataset(X_train.iloc[train_idx], 
                           y_train.iloc[train_idx], 
                           reference=data_all)

    gbm = lgb.train(params,
                    data_trd,
                    num_boost_round=len(results['auc-mean']),
                    verbose_eval=100)

    val_scores.append(roc_auc_score(y_train.iloc[val_idx], gbm.predict(X_train.iloc[val_idx])))
print('Manual score: ', np.mean(np.array(val_scores)))

产出
LGBM's cv score:  0.9914524426410262
Manual score:  0.9914524426410262

区别在于这一行reference=data_all。在交叉验证期间,变量的划分(参见lightgbm文档)是使用整个数据集(X_train)构建的,而您手动循环中的划分是在训练子集上构建的(X_train.iloc[train_idx])。通过传递指向包含所有数据的数据集的引用,lightGBM将重用相同的划分,从而产生相同的结果。


那是个好地方——我已经修复了那个错误。然而,结果仍然不匹配。所以一旦我从 lightgbm.cv 得到一个结果,如果 lightgbm.LGBMClassifier 的行为并不完全相同,我该怎么使用它呢?我只需要接受它们的差异吗? - EuRBamarth
1
问题出在分箱过程中。我正在编辑我的帖子,为您提供一个可再现的例子。 - Florian Mutel
哇,这太棒了!谢谢Florian,我非常感激。 - EuRBamarth
1
你问到如何使用它,一个好的方法是在整个数据集(data_all)上重新训练模型,使用更多的树(例如len(results['auc-mean']) *1.1),而不进行验证。通过这样做,您应该期望在X_test/y_test拆分上获得性能提升。(因为您使用了更多的数据,所以要添加更多的树)。 - Florian Mutel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接