使用XGB模型进行嵌套交叉验证的一种方法是:
from sklearn.model_selection import GridSearchCV, cross_val_scorefrom xgboost import XGBClassifier# 假设我们有用于二分类问题的某些数据:X (n_samples, n_features) 和 y (n_samples,)...gs = GridSearchCV(estimator=XGBClassifier(), param_grid={'max_depth': [3, 6, 9], 'learning_rate': [0.001, 0.01, 0.05]}, cv=2)scores = cross_val_score(gs, X, y, cv=2)
然而,关于XGB参数的调优,有几个教程(例如这个)利用了Python的hyperopt库。我希望能够使用hyperopt来调优XGB参数(如上所述)进行嵌套交叉验证。
为此,我编写了自己的Scikit-Learn估计器:
from hyperopt import fmin, tpe, hp, Trials, STATUS_OKfrom sklearn.base import BaseEstimator, ClassifierMixinfrom sklearn.model_selection import train_test_splitfrom sklearn.exceptions import NotFittedErrorfrom sklearn.metrics import roc_auc_scorefrom xgboost import XGBClassifierdef optimize_params(X, y, params_space, validation_split=0.2): """估计一组'最佳'模型参数。""" # 将X, y拆分为训练/验证集 X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=validation_split, stratify=y) # 估计XGB参数 def objective(_params): _clf = XGBClassifier(n_estimators=10000, max_depth=int(_params['max_depth']), learning_rate=_params['learning_rate'], min_child_weight=_params['min_child_weight'], subsample=_params['subsample'], colsample_bytree=_params['colsample_bytree'], gamma=_params['gamma']) _clf.fit(X_train, y_train, eval_set=[(X_train, y_train), (X_val, y_val)], eval_metric='auc', early_stopping_rounds=30) y_pred_proba = _clf.predict_proba(X_val)[:, 1] roc_auc = roc_auc_score(y_true=y_val, y_score=y_pred_proba) return {'loss': 1. - roc_auc, 'status': STATUS_OK} trials = Trials() return fmin(fn=objective, space=params_space, algo=tpe.suggest, max_evals=100, trials=trials, verbose=0)class OptimizedXGB(BaseEstimator, ClassifierMixin): """具有优化参数的XGB。 参数 ---------- custom_params_space : dict or None 如果不为None,则为字典,其键为要优化的XGB参数,相应的值为给定参数值的'先验'概率分布。如果为None,则使用默认参数空间。 """ def __init__(self, custom_params_space=None): self.custom_params_space = custom_params_space def fit(self, X, y, validation_split=0.3): """训练一个XGB模型。 参数 ---------- X : ndarray, shape (n_samples, n_features) 数据。 y : ndarray, shape (n_samples,) or (n_samples, n_labels) 标签。 validation_split : float (default: 0.3) 0到1之间的浮点数。对应于X中将用于估计'最佳'模型参数的验证数据的样本百分比。 """ # 如果没有给出自定义参数空间,则使用默认的。 if self.custom_params_space is None: _space = { 'learning_rate': hp.uniform('learning_rate', 0.0001, 0.05), 'max_depth': hp.quniform('max_depth', 8, 15, 1), 'min_child_weight': hp.quniform('min_child_weight', 1, 5, 1), 'subsample': hp.quniform('subsample', 0.7, 1, 0.05), 'gamma': hp.quniform('gamma', 0.9, 1, 0.05), 'colsample_bytree': hp.quniform('colsample_bytree', 0.5, 0.7, 0.05) } else: _space = self.custom_params_space # 使用X, y估计最佳参数 opt = optimize_params(X, y, _space, validation_split) # 使用优化后的参数实例化`xgboost.XGBClassifier` best = XGBClassifier(n_estimators=10000, max_depth=int(opt['max_depth']), learning_rate=opt['learning_rate'], min_child_weight=opt['min_child_weight'], subsample=opt['subsample'], gamma=opt['gamma'], colsample_bytree=opt['colsample_bytree']) best.fit(X, y) self.best_estimator_ = best return self def predict(self, X): """使用训练的XGB模型预测标签。 参数 ---------- X : ndarray, shape (n_samples, n_features) 返回 ------- output : ndarray, shape (n_samples,) or (n_samples, n_labels) """ if not hasattr(self, 'best_estimator_'): raise NotFittedError('在`predict`之前调用`fit`。') else: return self.best_estimator_.predict(X) def predict_proba(self, X): """使用训练的XGB模型预测标签概率。 参数 ---------- X : ndarray, shape (n_samples, n_features) 返回 ------- output : ndarray, shape (n_samples,) or (n_samples, n_labels) """ if not hasattr(self, 'best_estimator_'): raise NotFittedError('在`predict_proba`之前调用`fit`。') else: return self.best_estimator_.predict_proba(X)
我的问题是:
- 这是一种有效的方法吗?例如,在我的
OptimizedXGB
的fit
方法中,best.fit(X, y)
将会在X, y上训练一个XGB模型。然而,由于没有指定eval_set
来确保提前停止,这可能会导致过拟合。 - 在一个玩具示例中(著名的iris数据集),这个
OptimizedXGB
的表现比一个基本的LogisticRegression分类器要差。这是为什么呢?是因为示例过于简单吗?请看下面的示例代码。
示例:
import numpy as npfrom sklearn.datasets import load_irisfrom sklearn.linear_model import LogisticRegressionfrom sklearn.model_selection import GridSearchCV, cross_val_score, StratifiedKFoldfrom sklearn.pipeline import Pipelinefrom sklearn.preprocessing import StandardScalerX, y = load_iris(return_X_y=True)X = X[:, :2]X = X[y < 2]y = y[y < 2]skf = StratifiedKFold(n_splits=2, random_state=42)# 使用LogisticRegression分类器pipe = Pipeline([('scaler', StandardScaler()), ('lr', LogisticRegression())])gs = GridSearchCV(estimator=pipe, param_grid={'lr__C': [1., 10.]})lr_scores = cross_val_score(gs, X, y, cv=skf)# 使用OptimizedXGBxgb_scores = cross_val_score(OptimizedXGB(), X, y, cv=skf)# 打印结果print('使用LogisticRegression的准确率 = %.4f (+/- %.4f)' % (np.mean(lr_scores), np.std(lr_scores)))print('使用OptimizedXGB的准确率 = %.4f (+/- %.4f)' % (np.mean(xgb_scores), np.std(xgb_scores)))
输出:
使用LogisticRegression的准确率 = 0.9900 (+/- 0.0100)使用OptimizedXGB的准确率 = 0.9100 (+/- 0.0300)
尽管得分接近,但我本以为XGB模型至少应该和LogisticRegression分类器表现一样好。
编辑:
回答:
首先,查看这个帖子 – 可能会有所帮助 – 嵌套CV。
关于你的问题:
- 是的,这是正确的方法。一旦你选择了超参数,你应该在整个训练数据上拟合你的模型(选定的模型)。然而,由于这个模型内部包含了一个模型选择过程,你只能使用外部CV来“评分”它的泛化能力,就像你所做的那样。
- 由于你也在评分选择过程(而不仅仅是模型,比如XGB与线性回归),选择过程可能存在一些问题。也许你的超参数空间没有正确定义,你选择了不佳的参数?