验证GridSearchCV中自定义AUPRC评分器的实现正确性
问题分析与修正方案
一、自定义AUPRC评分器的核心错误
你的评分器代码存在关键问题:average_precision_score需要模型输出的正类概率(或置信度),而非硬分类的0/1标签。原代码中将y_pred转换为0/1的操作,完全丢失了模型的置信度信息,导致计算出的AUPRC毫无意义——这相当于只计算了单一阈值下的精确率,而非整个PR曲线的面积。
同时,原代码中函数名重复覆盖(custom_auprc_scorer既作为函数名,又作为make_scorer的结果变量),虽然不报错,但会降低代码可读性。
二、修正后的AUPRC评分器代码
from sklearn.metrics import average_precision_score, make_scorer import numpy as np def custom_auprc_score_func(y_true, y_pred_proba): # 将标签映射为0/1 pos_label = 'positive' y_true_mapped = np.where(y_true == pos_label, 1, 0) # y_pred_proba是模型输出的正类概率,直接传入即可 return average_precision_score(y_true_mapped, y_pred_proba) # 创建评分器,指定需要模型输出概率 custom_auprc_scorer = make_scorer( custom_auprc_score_func, greater_is_better=True, needs_proba=True # 关键:告诉GridSearchCV传入概率而非分类标签 ) scorer = {'AUPRC': custom_auprc_scorer, 'ROC_AUC': 'roc_auc'}
关键修正点:
- 添加
needs_proba=True参数:让GridSearchCV在调用评分器时,传入模型的predict_proba()输出(正类概率),而非predict()的分类结果。 - 移除对
y_pred的0/1转换:直接使用模型输出的概率值计算AUPRC。 - 重命名函数避免变量覆盖:将原函数名改为
custom_auprc_score_func,避免与最终的评分器变量冲突。
三、AUPRC作为超参数选择指标的有效性
对于你使用SMOTE处理的场景(说明数据集存在类别不平衡),AUPRC是比ROC AUC更合适的超参数优化指标:
- ROC AUC对不平衡数据不敏感,即使模型对少数类预测很差,也可能得到较高的ROC AUC分数。
- AUPRC聚焦于正类(少数类)的精确率与召回率 trade-off,能更准确反映模型在目标类别上的性能,非常适合不平衡分类任务的超参数选择。因此你选择
refit='AUPRC'是完全合理的。
四、evaluatealgorithm函数的补充优化
原函数整体逻辑没问题,但可以增加两点鲁棒性:
- 确保
y_train中仅包含pos_label和另一类标签,避免映射错误。 - 必须使用
imblearn的Pipeline,避免交叉验证中的数据泄露。
修正后的完整evaluatealgorithm函数:
from sklearn.ensemble import GradientBoostingClassifier from imblearn.pipeline import Pipeline # 注意:使用imblearn的Pipeline而非sklearn的 from imblearn.over_sampling import SMOTE from sklearn.model_selection import GridSearchCV def evaluatealgorithm(x_train, y_train, kfold, scorer): # 验证标签是否符合预期 unique_labels = np.unique(y_train) assert len(unique_labels) == 2, "数据集必须是二分类任务" assert 'positive' in unique_labels, "标签中必须包含'positive'类别" gbm = GradientBoostingClassifier(loss='log_loss') smote = SMOTE(sampling_strategy='minority') # 使用imblearn的Pipeline,确保SMOTE在每个交叉验证 fold 中仅拟合训练数据 pipeline = Pipeline(steps=[['smote', smote], ['gbm', gbm]]) parameters = { 'smote__k_neighbors': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'gbm__learning_rate': [0.001, 0.005, 0.01, 0.05, 0.1, 0.2, 0.5, 1], 'gbm__subsample': [0.4, 0.5, 0.6, 0.8, 1], 'gbm__n_estimators': [50, 100, 200, 300, 400, 600], 'gbm__max_depth': [3, 4, 5, 6, 7, 8, 9, 10], 'gbm__min_samples_split': [2, 3, 4, 5], 'gbm__min_samples_leaf': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] } grid_gbm = GridSearchCV(estimator=pipeline, param_grid=parameters, cv=kfold, verbose=1, n_jobs=-1, refit='AUPRC', scoring=scorer) grid_gbm.fit(x_train, y_train) auprc_score = grid_gbm.cv_results_['mean_test_AUPRC'][grid_gbm.best_index_] roc_score = grid_gbm.cv_results_['mean_test_ROC_AUC'][grid_gbm.best_index_] model_gbm = grid_gbm.best_estimator_ smote_params = model_gbm.named_steps['smote'].get_params() gbm_params = model_gbm.named_steps['gbm'].get_params() return smote_params, gbm_params, model_gbm, roc_score, auprc_score
额外注意:
- 必须使用
imblearn.pipeline.Pipeline:sklearn原生Pipeline会在整个数据集上拟合SMOTE后再做交叉验证,导致数据泄露;而imblearn的Pipeline会在每个交叉验证的训练fold中单独拟合SMOTE,符合交叉验证的严谨性要求。
内容的提问来源于stack exchange,提问作者Lucas F. T. Leonardo
相关产品推荐
相关产品推荐

