哪些特征导致过拟合?使用ParShap 方法精准定位导致模型泛化能力下降的关键特征

本文涉及的产品
智能开放搜索 OpenSearch行业算法版,1GB 20LCU 1个月
实时数仓Hologres,5000CU*H 100GB 3个月
实时计算 Flink 版,5000CU*H 3个月
简介: 本文探讨了如何识别导致模型过拟合的特征,提出了一种基于SHAP值和偏相关性的新方法——ParShap。通过分析德国健康登记数据集,作者展示了传统特征重要性无法准确反映特征在新数据上的表现,而ParShap能有效识别出过拟合特征。实验表明,移除这些特征可以显著减少过拟合现象,验证了该方法的有效性。

机器学习的核心目标是在未见过的新数据上实现准确预测。

当模型在训练数据上表现良好,但在测试数据上表现不佳时,即出现“过拟合”。这意味着模型从训练数据中学习了过多的噪声模式,从而丧失了在新数据上的泛化能力。

那么,过拟合的根本原因是什么?具体来说,

哪些特征(数据集的列)阻碍了模型在新数据上的有效泛化

本文将基于实际数据集,探讨一种先进的方法来解答这一问题。

特征重要性在此场景下不再适用

如果你的第一反应是“我会查看特征重要性”,那么请重新考虑。

特征重要性无法直接反映特征在新数据上的表现。

实际上,特征重要性仅是模型在训练阶段所学内容的表现。如果模型在训练过程中学习到关于“年龄”特征的复杂模式,那么该特征的特征重要性将会很高。但这并不意味着这些模式是准确的(“准确”指的是一种具备泛化能力的模式,即在新的数据上依然成立)。

因此,我们需要采用不同的方法来解决这个问题。

案例

为了阐述该方法,我将使用一个包含1984年至1988年德国健康登记数据的数据集(该数据集可通过 Pydataset 库获得,并遵循 MIT 许可证)。

下载数据的方式非常简单:

 import pydataset
 X = pydataset.data('rwm5yr')  
 y = (X['hospvis'] > 1).astype(int)  
 X = X.drop('hospvis', axis = 1)
AI 代码解读

该数据集包含19,609行,每行记录了一名患者在特定年份的一些信息。需要注意的是,患者在不同年份都可能被观测到,因此同一患者可能出现在数据框的多行中。

目标变量是:

  • **hospvis**:患者在相应年份住院天数是否超过1天。

我们拥有16个特征列:

  1. **id**:患者ID(1-7028);
  2. **docvis**:年内就诊次数(0-121);
  3. **year**:年份(1984-1988);
  4. **edlevel**:教育水平(1-4);
  5. **age**:年龄(25-64);
  6. **outwork**:是否失业,1表示失业,0表示在职;
  7. **female**:性别,1表示女性,0表示男性;
  8. **married**:婚姻状况,1表示已婚,0表示未婚;
  9. **kids**:是否有子女,1表示有,0表示无;
  10. **hhninc**:家庭年收入(马克);
  11. **educ**:受教育年限(7-18);
  12. **self**:是否自雇,1表示自雇,0表示非自雇;
  13. **edlevel1**:是否为高中以下学历,1表示是,0表示否;
  14. **edlevel2**:是否为高中学历,1表示是,0表示否;
  15. **edlevel3**:是否为大学/学院学历,1表示是,0表示否;
  16. **edlevel4**:是否为研究生学历,1表示是,0表示否。

现在,将数据集划分为训练集和测试集。虽然存在更严谨的方法,如交叉验证,但为了保持简洁,我们采用简单的划分方法。但在本文中我们(简单地)将所有列视为数值特征。

 from sklearn.model_selection import train_test_split  
 from catboost import CatBoostClassifier

 X_train, X_test, y_train, y_test = train_test_split(
   X, y, test_size = .2, stratify = y)

 cat = CatBoostClassifier(silent = True).fit(X_train, y_train)
AI 代码解读

模型训练完成后,我们来分析特征重要性:

 import pandas as pd

 fimpo = pd.Series(cat.feature_importances_, index = X_train.columns)
AI 代码解读

已训练模型的特征重要性。

不出所料,

docvis
AI 代码解读

(就诊次数)在预测患者是否住院超过1天方面至关重要。

age
AI 代码解读

(年龄)和

hhninc
AI 代码解读

(收入)这两个特征的重要性也符合预期。但是,患者ID(

id
AI 代码解读

)的重要性排名第二则值得警惕,尤其是在我们将其视为数值特征的情况下。

接下来,计算模型在训练集和测试集上的性能指标(ROC曲线下面积,AUC):

 from sklearn.metrics import roc_auc_score

 roc_train = roc_auc_score(y_train, cat.predict_proba(X_train)[:, 1])  
 roc_test = roc_auc_score(y_test, cat.predict_proba(X_test)[:, 1])
AI 代码解读

模型在训练集和测试集上的性能。

结果显示,训练集和测试集之间的性能差距显著。这表明存在明显的过拟合现象。那么,究竟是哪些特征导致了过拟合?

什么是SHAP值?

我们有多种指标可以衡量模型在特定数据上的表现。但如何衡量特征在特定数据上的表现?

“SHAP值”是解决此问题的有力工具。

通常,你可以使用专门的Python库来高效计算任何预测模型的SHAP值。但这里为了简单,我们将利用Catboost的原生方法:

 from catboost import Pool

 shap_train = pd.DataFrame(  
   data = cat.get_feature_importance(  
     data = Pool(X_train),   
     type = 'ShapValues')[:, :-1],   
   index = X_train.index,   
   columns = X_train.columns  
 )

 shap_test = pd.DataFrame(  
   data = cat.get_feature_importance(  
     data = Pool(X_test),   
     type = 'ShapValues')[:, :-1],   
   index = X_test.index,   
   columns = X_test.columns  
 )
AI 代码解读

观察

shap_train
AI 代码解读

shap_test
AI 代码解读

,你会发现它们的形状与各自的数据集相同。

SHAP值可以量化每个特征对模型在单个或多个观测值上的最终预测的影响

看几个例子:

原始数据及其对应的SHAP值。

第12071行的患者就诊次数为0,相应的SHAP值为-0.753,这意味着该信息将患者住院超过1天的概率(实际上是对数几率)降低了0.753。相反,第18650行的患者就诊4次,这使得她住院超过1天的对数几率提高了0.918。

认识ParShap

一个特征在数据集上的性能可以通过该特征的SHAP值与目标变量之间的相关性来近似表示。如果模型在某个特征上学习到有效的模式,那么该特征的SHAP值应与目标变量高度正相关。

例如,如果我们想计算

docvis
AI 代码解读

特征与测试集中观测数据的目标变量之间的相关性:

 import numpy as np

 np.corrcoef(shap_test['docvis'], y_test)
AI 代码解读

然而,SHAP值具有可加性,即最终预测是所有特征SHAP值的总和。因此在计算相关性之前,先消除其他特征的影响会更有意义。这正是“偏相关”的定义。偏相关的便捷实现方式可以在 Python 库 Pingouin 中找到:

 import pingouin

 pingouin.partial_corr(  
   data = pd.concat([shap_test, y_test], axis = 1).astype(float),   
   x = 'docvis',   
   y = y_test.name,  
   x_covar = [feature for feature in shap_test.columns if feature != 'docvis']   
 )
AI 代码解读

这段代码的含义是:“计算

docvis
AI 代码解读

特征的SHAP值与测试集观测数据的目标变量之间的相关性,同时消除所有其他特征的影响。”

为了方便将此公式称为 “ParShap” (Partial correlation of Shap values,SHAP值的偏相关)。

我们可以对训练集和测试集中的每个特征重复此过程:

 parshap_train = partial_correlation(shap_train, y_train)  
 parshap_test = partial_correlation(shap_test, y_test)
AI 代码解读

注意:你可以在本文末尾找到

partial_correlation
AI 代码解读

函数的定义。

现在在 x 轴上绘制

parshap_train
AI 代码解读

,在 y 轴上绘制

parshap_test
AI 代码解读

 import matplotlib.pyplot as plt

 plt.scatter(parshap_train, parshap_test)
AI 代码解读

SHAP值与目标变量的偏相关性,分别在训练集和测试集上。注意:颜色条表示特征重要性。[作者提供的图片]

如果一个特征位于对角线上,则表示它在训练集和测试集上的表现完全一致。这是理想情况,既没有过拟合也没有欠拟合。反之如果一个特征位于对角线下方,则表示它在测试集上的表现不如训练集。这属于过拟合区域。

通过视觉观察,我们可以立即发现哪些特征表现不佳:我已用蓝色圆圈标记出它们。

蓝色圆圈标记的特征是当前模型中最容易出现过拟合的特征。[作者提供的图片]

因此,

parshap_test
AI 代码解读

parshap_train
AI 代码解读

之间的算术差(等于每个特征与对角线之间的垂直距离)可以量化该特征对模型的过拟合程度。

 parshap_diff = parshap_test - parshap_train
AI 代码解读

parshap_test
AI 代码解读

parshap_train
AI 代码解读

之间的算术差。[作者提供的图片]

应该如何解读这个结果?基于以上分析,该值越负,则该特征导致的过拟合程度越高

验证

我们是否能找到一种方法来验证本文提出的观点的正确性?

从逻辑上讲,如果从数据集中移除“过拟合特征”,应该能够减少过拟合现象(即,缩小

roc_train
AI 代码解读

roc_test
AI 代码解读

之间的差距)。

因此尝试每次删除一个特征,并观察ROC曲线下面积的变化。

根据特征重要性(左)或ParShap(右)排序,依次删除一个特征时,模型在训练集和测试集上的性能。

左侧的图中,每次移除一个特征,并按照特征重要性进行排序。首先移除最不重要的特征(

edlevel4
AI 代码解读

),然后移除两个最不重要的特征(

edlevel4
AI 代码解读

edlevel1
AI 代码解读

),以此类推。

右侧的图中,执行相同的操作,但是移除的顺序由ParShap决定。首先移除ParShap值最小(最负)的特征(

id
AI 代码解读

),然后移除两个ParShap值最小的特征(

id
AI 代码解读

year
AI 代码解读

),以此类推。

正如预期的那样,移除ParShap值最负的特征显著减少了过拟合。实际上,

roc_train
AI 代码解读

的值逐渐接近

roc_test
AI 代码解读

的值。

需要注意的是,这仅仅是用于验证我们推理过程的测试。通常来说,ParShap 不应被用作特征选择的方法。因为,某些特征容易出现过拟合并不意味着这些特征完全没有用处!(例如,本例中的收入和年龄)。

但是ParShap在为我们提供模型调试的线索方面非常有用。它可以帮助我们将注意力集中在那些需要更多特征工程或正则化的特征上。

最后以下是本文的完整,有兴趣的可以进行复现测试

本文使用的完整代码(由于随机种子,结果可能略有不同):

 # Import libraries  
 import pandas as pd  
 import pydataset  
 from sklearn.model_selection import train_test_split  
 from catboost import CatBoostClassifier, Pool  
 from sklearn.metrics import roc_auc_score  
 from pingouin import partial_corr  
 import matplotlib.pyplot as plt

 # Print documentation and read data
 print('################# Print docs') # 打印文档
 pydataset.data('rwm5yr', show_doc = True)
 X = pydataset.data('rwm5yr')  
 y = (X['hospvis'] > 1).astype(int)  
 X = X.drop('hospvis', axis = 1)

 # Split data in train + test
 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = .2, stratify = y)

 # Fit model
 cat = CatBoostClassifier(silent = True).fit(X_train, y_train)

 # Show feature importance
 fimpo = pd.Series(cat.feature_importances_, index = X_train.columns)  
 fig, ax = plt.subplots()  
 fimpo.sort_values().plot.barh(ax = ax)  
 fig.savefig('fimpo.png', dpi = 200, bbox_inches="tight")  
 fig.show()

 # Compute metrics
 roc_train = roc_auc_score(y_train, cat.predict_proba(X_train)[:, 1])  
 roc_test = roc_auc_score(y_test, cat.predict_proba(X_test)[:, 1])
 print('\n################# Print roc') # 打印roc
 print('roc_auc train: {:.2f}'.format(roc_train))  
 print('roc_auc  test: {:.2f}'.format(roc_test))

 # Compute SHAP values
 shap_train = pd.DataFrame(  
   data = cat.get_feature_importance(data = Pool(X_train), type = 'ShapValues')[:, :-1],  
   index = X_train.index,   
   columns = X_train.columns  
 )

 shap_test = pd.DataFrame(  
   data = cat.get_feature_importance(data = Pool(X_test), type = 'ShapValues')[:, :-1],  
   index = X_test.index,   
   columns = X_test.columns  
 )
 print('\n################# Print df shapes') # 打印数据框的形状
 print(f'X_train.shape:    {X_train.shape}')  
 print(f'X_test.shape:     {X_test.shape}\n')  
 print(f'shap_train.shape: {shap_train.shape}')  
 print(f'shap_test.shape:  {shap_test.shape}')

 print('\n################# Print data and SHAP') # 打印数据和SHAP值
 print('Original data:')  
 display(X_test.head(3))  
 print('\nCorresponding SHAP values:')  
 display(shap_test.head(3).round(3))

 # Define function for partial correlation
 def partial_correlation(X, y):  
   out = pd.Series(index = X.columns, dtype = float)  
   for feature_name in X.columns:  
     out[feature_name] = partial_corr(  
       data = pd.concat([X, y], axis = 1).astype(float),   
       x = feature_name,   
       y = y.name,  
       x_covar = [f for f in X.columns if f != feature_name]   
     ).loc['pearson', 'r']  
   return out

 # Compute ParShap
 parshap_train = partial_correlation(shap_train, y_train)  
 parshap_test = partial_correlation(shap_test, y_test)  
 parshap_diff = pd.Series(parshap_test - parshap_train, name = 'parshap_diff')

 print('\n################# Print parshap_diff') # 打印parshap差异
 print(parshap_diff.sort_values())

 # Plot parshap
 plotmin, plotmax = min(parshap_train.min(), parshap_test.min()), max(parshap_train.max(), parshap_test.max())  
 plotbuffer = .05 * (plotmax - plotmin)  
 fig, ax = plt.subplots()  
 if plotmin < 0:  
     ax.vlines(0, plotmin - plotbuffer, plotmax + plotbuffer, color = 'darkgrey', zorder = 0)  
     ax.hlines(0, plotmin - plotbuffer, plotmax + plotbuffer, color = 'darkgrey', zorder = 0)  
 ax.plot(  
     [plotmin - plotbuffer, plotmax + plotbuffer], [plotmin - plotbuffer, plotmax + plotbuffer],   
     color = 'darkgrey', zorder = 0  
 )  
 sc = ax.scatter(  
     parshap_train, parshap_test,   
     edgecolor = 'grey', c = fimpo, s = 50, cmap = plt.cm.get_cmap('Reds'), vmin = 0, vmax = fimpo.max())  
 ax.set(title = 'Partial correlation bw SHAP and target...', xlabel = '... on Train data', ylabel = '... on Test data') # SHAP值和目标之间偏相关,在训练数据上,在测试数据上
 cbar = fig.colorbar(sc)  
 cbar.set_ticks([])  
 for txt in parshap_train.index:  
     ax.annotate(txt, (parshap_train[txt], parshap_test[txt] + plotbuffer / 2), ha = 'center', va = 'bottom')  
 fig.savefig('parshap.png', dpi = 300, bbox_inches="tight")  
 fig.show()

 # Feature selection
 n_drop_max = 5  
 iterations = 4
 features = {'parshap': parshap_diff, 'fimpo': fimpo}  
 features_dropped = {}  
 roc_auc_scores = {  
   'fimpo': {'train': pd.DataFrame(), 'test': pd.DataFrame()},  
   'parshap': {'train': pd.DataFrame(), 'test': pd.DataFrame()}  
 }

 for type_ in ['parshap', 'fimpo']:  
   for n_drop in range(n_drop_max + 1):  
     features_drop = features[type_].sort_values().head(n_drop).index.to_list()  
     features_dropped[type_] = features_drop  
     X_drop = X.drop(features_drop, axis = 1)  
     for i in range(iterations):  
       X_train, X_test, y_train, y_test = train_test_split(X_drop, y, test_size = .2, stratify = y)  
       cat = CatBoostClassifier(silent = True).fit(X_train, y_train)  
       roc_auc_scores[type_]['train'].loc[n_drop, i] = roc_auc_score(y_train, cat.predict_proba(X_train)[:, 1])  
       roc_auc_scores[type_]['test'].loc[n_drop, i] = roc_auc_score(y_test, cat.predict_proba(X_test)[:, 1])

 # Plot feature selection
 fig, axs = plt.subplots(1, 2, sharey = True, figsize = (8, 3))  
 plt.subplots_adjust(wspace = .1)  
 axs[0].plot(roc_auc_scores['fimpo']['train'].index, roc_auc_scores['fimpo']['train'].mean(axis = 1), lw = 3, label = 'Train')  
 axs[0].plot(roc_auc_scores['fimpo']['test'].index, roc_auc_scores['fimpo']['test'].mean(axis = 1), lw = 3, label = 'Test')  
 axs[0].set_xticks(roc_auc_scores['fimpo']['train'].index)  
 axs[0].set_xticklabels([''] + features_dropped['fimpo'], rotation = 90)  
 axs[0].set_title('Feature Importance') # 特征重要性
 axs[0].set_xlabel('Feature dropped') # 删除的特征
 axs[0].grid()  
 axs[0].legend(loc = 'center left')  
 axs[0].set(ylabel = 'ROC-AUC score') # ROC-AUC分数
 axs[1].plot(roc_auc_scores['parshap']['train'].index, roc_auc_scores['parshap']['train'].mean(axis = 1), lw = 3, label = 'Train')  
 axs[1].plot(roc_auc_scores['parshap']['test'].index, roc_auc_scores['parshap']['test'].mean(axis = 1), lw = 3, label = 'Test')  
 axs[1].set_xticks(roc_auc_scores['parshap']['train'].index)  
 axs[1].set_xticklabels([''] + features_dropped['parshap'], rotation = 90)  
 axs[1].set_title('ParShap')  
 axs[1].set_xlabel('Feature dropped') # 删除的特征
 axs[1].grid()  
 axs[1].legend(loc = 'center left')  
 fig.savefig('feature_selection.png', dpi = 300, bbox_inches="tight")  
 fig.show()
AI 代码解读

https://avoid.overfit.cn/post/47520a73a5c6469cab1116b2f036accd

作者:Samuele Mazzanti

目录
打赏
0
76
79
6
530
分享
相关文章
多模型DCA曲线:如何展现和解读乳腺癌风险评估模型的多样性和鲁棒性?
多模型DCA曲线:如何展现和解读乳腺癌风险评估模型的多样性和鲁棒性?
207 1
LLM-Mixer: 融合多尺度时间序列分解与预训练模型,可以精准捕捉短期波动与长期趋势
近年来,大型语言模型(LLMs)在自然语言处理领域取得显著进展,研究人员开始探索将其应用于时间序列预测。Jin等人提出了LLM-Mixer框架,通过多尺度时间序列分解和预训练的LLMs,有效捕捉时间序列数据中的短期波动和长期趋势,提高了预测精度。实验结果显示,LLM-Mixer在多个基准数据集上优于现有方法,展示了其在时间序列预测任务中的巨大潜力。
135 3
LLM-Mixer: 融合多尺度时间序列分解与预训练模型,可以精准捕捉短期波动与长期趋势
|
2月前
分布匹配蒸馏:扩散模型的单步生成优化方法研究
扩散模型在生成高质量图像方面表现出色,但其迭代去噪过程计算开销大。分布匹配蒸馏(DMD)通过将多步扩散简化为单步生成器,结合分布匹配损失和对抗生成网络损失,实现高效映射噪声图像到真实图像,显著提升生成速度。DMD利用预训练模型作为教师网络,提供高精度中间表征,通过蒸馏机制优化单步生成器的输出,从而实现快速、高质量的图像生成。该方法为图像生成应用提供了新的技术路径。
114 2
TimeMOE: 使用稀疏模型实现更大更好的时间序列预测
TimeMOE是一种新型的时间序列预测基础模型,通过稀疏混合专家(MOE)设计,在提高模型能力的同时降低了计算成本。它可以在多种时间尺度上进行预测,并且经过大规模预训练,具备出色的泛化能力。TimeMOE不仅在准确性上超越了现有模型,还在计算效率和灵活性方面表现出色,适用于各种预测任务。该模型已扩展至数十亿参数,展现了时间序列领域的缩放定律。研究结果显示,TimeMOE在多个基准测试中显著优于其他模型,特别是在零样本学习场景下。
746 64
特征工程在营销组合建模中的应用:基于因果推断的机器学习方法优化渠道效应估计
因果推断方法为特征工程提供了一个更深层次的框架,使我们能够区分真正的因果关系和简单的统计相关性。这种方法在需要理解干预效果的领域尤为重要,如经济学、医学和市场营销。
166 1
特征工程在营销组合建模中的应用:基于因果推断的机器学习方法优化渠道效应估计
使用ClassificationThresholdTuner进行二元和多类分类问题阈值调整,提高模型性能增强结果可解释性
在分类问题中,调整决策的概率阈值虽常被忽视,却是提升模型质量的有效步骤。本文深入探讨了阈值调整机制,尤其关注多类分类问题,并介绍了一个名为 ClassificationThresholdTuner 的开源工具,该工具自动化阈值调整和解释过程。通过可视化功能,数据科学家可以更好地理解最优阈值及其影响,尤其是在平衡假阳性和假阴性时。此外,工具支持多类分类,解决了传统方法中的不足。
82 2
使用ClassificationThresholdTuner进行二元和多类分类问题阈值调整,提高模型性能增强结果可解释性
在模型训练中,如何衡量和平衡通用性和特定任务需求的重要性?
在模型训练中,如何衡量和平衡通用性和特定任务需求的重要性?
109 2
|
9月前
偏微分方程有了基础模型:样本需求数量级减少,14项任务表现最佳
【6月更文挑战第16天】研究人员提出Poseidon模型,减少求解偏微分方程(PDEs)的样本需求,提升效率。在15个挑战任务中,该模型在14项表现最优。基于scOT的多尺度架构, Poseidon降低了计算成本,但仍有泛化和资源限制。[论文链接](https://arxiv.org/pdf/2405.19101)**
116 4
。这不仅可以减少过拟合的风险,还可以提高模型的准确性、降低计算成本,并帮助理解数据背后的真正含义。`sklearn.feature_selection`模块提供了多种特征选择方法,其中`SelectKBest`是一个元变换器,可以与任何评分函数一起使用来选择数据集中K个最好的特征。
。这不仅可以减少过拟合的风险,还可以提高模型的准确性、降低计算成本,并帮助理解数据背后的真正含义。`sklearn.feature_selection`模块提供了多种特征选择方法,其中`SelectKBest`是一个元变换器,可以与任何评分函数一起使用来选择数据集中K个最好的特征。
【机器学习】自然语言引导下的单目深度估计:泛化能力与鲁棒性的新挑战
【机器学习】自然语言引导下的单目深度估计:泛化能力与鲁棒性的新挑战
102 0
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等