scikit-learn 中的 class_weight 参数是如何工作的?

How does the class_weight parameter in scikit-learn work?

我很难理解 scikit-learn 的逻辑回归中的 class_weight 参数是如何运作的。

情况

我想使用逻辑回归对非常不平衡的数据集进行二元分类。 类 被标记为 0(阴性)和 1(阳性),观察到的数据的比率约为 19:1,大多数样本的结果为阴性。

第一次尝试:手动准备训练数据

我将我拥有的数据分成不相交的数据集用于训练和测试(大约 80/20)。然后我对训练数据进行了随机抽样,得到了比19:1不同比例的训练数据;从 2:1 -> 16:1.

然后我在这些不同的训练数据子集上训练逻辑回归,并绘制召回率 (= TP/(TP+FN)) 作为不同训练比例的函数。当然,召回率是在不相交的 TEST 样本上计算的,这些样本的观察比例为 19:1。请注意,虽然我在不同的训练数据上训练了不同的模型,但我在相同的(不相交的)测试数据上计算了所有模型的召回率。

结果如预期:召回率在 2:1 训练比例下约为 60%,并且在达到 16:1 时下降得相当快。有几个比例 2:1 -> 6:1 召回率超过 5%。

第二次尝试:网格搜索

接下来,我想测试不同的正则化参数,因此我使用了 GridSearchCV 并制作了一个包含 C 参数和 class_weight 参数的多个值的网格。要将我的 n:m 比例的 negative:positive 训练样本翻译成 class_weight 的词典语言,我认为我只需指定几个词典如下:

{ 0:0.67, 1:0.33 } #expected 2:1
{ 0:0.75, 1:0.25 } #expected 3:1
{ 0:0.8, 1:0.2 }   #expected 4:1

我还包括 Noneauto

这次的结果完全是一团糟。对于 class_weight 除了 auto 之外的每个值,我所有的召回结果都很小(< 0.05)。所以我只能假设我对如何设置 class_weight 字典的理解是错误的。有趣的是,网格搜索中 'auto' 的 class_weight 值对于 C 的所有值约为 59%,我猜它与 1:1?

持平

我的问题

  1. 您如何正确使用 class_weight 来实现训练数据与您实际提供的数据的不同平衡?具体来说,我将什么字典传递给 class_weight 以使用 n:m 比例的 negative:positive 训练样本?

  2. 如果您将各种 class_weight 词典传递给 GridSearchCV,在交叉验证期间,它会根据词典重新平衡训练折叠数据,但使用真实的给定样本比例来计算我的评分函数在测试折叠?这很关键,因为任何指标只有来自观察比例的数据时才对我有用。

  3. class_weightauto值在比例上有什么作用?我阅读了文档,我假设 "balances the data inversely proportional to their frequency" 只是意味着它使它成为 1:1。这个对吗?如果不是,有人可以澄清一下吗?

首先,仅靠召回可能不太好。您可以通过 class 将所有内容都确定为正面 class 来简单地实现 100% 的召回率。 我通常建议使用 AUC 来选择参数,然后为您感兴趣的操作点(比如给定的精度水平)找到一个阈值。

关于 class_weight 的工作原理:它用 class_weight[i] 而不是 1 来惩罚 class[i] 样本中的错误。所以更高的 class-weight 意味着你想放更多强调一个class。从你所说的看来 class 0 比 class 1 的频率高 19 倍。所以你应该增加 class 1 相对于 class 0 的 class_weight ,说 {0:.1, 1:.9}。 如果 class_weight 不等于 1,它基本上会改变正则化参数。

关于class_weight="auto"的工作原理,你可以看看this discussion。 在开发版本中,您可以使用 class_weight="balanced",这更容易理解:它基本上意味着复制较小的 class,直到您拥有与较大的样本一样多的样本,但以隐含的方式。

第一个答案有助于理解它是如何工作的。但我想了解我应该如何在实践中使用它。

使用不平衡学习

对于不平衡数据,imbalanced-learn 中的方法比使用 class 权重参数产生更好的结果,尤其是在样本外。

摘要

  • 对于没有噪声的中度不平衡数据,应用 class 权重没有太大区别
  • 对于带有噪声的中度不平衡数据和严重不平衡的数据,最好应用 class 权重
  • param class_weight="balanced" 在您不想要手动优化的情况下效果不错
  • class_weight="balanced" 相比,您捕获了更多真实事件(更高的 TRUE 召回率),但您也更有可能获得错误警报(较低的 TRUE 精度)
    • 因此,由于所有误报,总的 % TRUE 可能高于实际值
    • 如果误报是一个问题,AUC 可能会在这里误导您
  • 无需将决策阈值更改为不平衡百分比,即使对于严重的不平衡,也可以保持 0.5(或根据您的需要在该值附近)

NB

使用 RF 或 GBM 时结果可能不同。 sklearn does not have class_weight="balanced" for GBM but lightgbmLGBMClassifier(is_unbalance=False)

代码

# scikit-learn==0.21.3
from sklearn import datasets
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score, classification_report
import numpy as np
import pandas as pd

# case: moderate imbalance
X, y = datasets.make_classification(n_samples=50*15, n_features=5, n_informative=2, n_redundant=0, random_state=1, weights=[0.8]) #,flip_y=0.1,class_sep=0.5)
np.mean(y) # 0.2

LogisticRegression(C=1e9).fit(X,y).predict(X).mean() # 0.184
(LogisticRegression(C=1e9).fit(X,y).predict_proba(X)[:,1]>0.5).mean() # 0.184 => same as first
LogisticRegression(C=1e9,class_weight={0:0.5,1:0.5}).fit(X,y).predict(X).mean() # 0.184 => same as first
LogisticRegression(C=1e9,class_weight={0:2,1:8}).fit(X,y).predict(X).mean() # 0.296 => seems to make things worse?
LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X).mean() # 0.292 => seems to make things worse?

roc_auc_score(y,LogisticRegression(C=1e9).fit(X,y).predict(X)) # 0.83
roc_auc_score(y,LogisticRegression(C=1e9,class_weight={0:2,1:8}).fit(X,y).predict(X)) # 0.86 => about the same
roc_auc_score(y,LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X)) # 0.86 => about the same

# case: strong imbalance
X, y = datasets.make_classification(n_samples=50*15, n_features=5, n_informative=2, n_redundant=0, random_state=1, weights=[0.95])
np.mean(y) # 0.06

LogisticRegression(C=1e9).fit(X,y).predict(X).mean() # 0.02
(LogisticRegression(C=1e9).fit(X,y).predict_proba(X)[:,1]>0.5).mean() # 0.02 => same as first
LogisticRegression(C=1e9,class_weight={0:0.5,1:0.5}).fit(X,y).predict(X).mean() # 0.02 => same as first
LogisticRegression(C=1e9,class_weight={0:1,1:20}).fit(X,y).predict(X).mean() # 0.25 => huh??
LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X).mean() # 0.22 => huh??
(LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict_proba(X)[:,1]>0.5).mean() # same as last

roc_auc_score(y,LogisticRegression(C=1e9).fit(X,y).predict(X)) # 0.64
roc_auc_score(y,LogisticRegression(C=1e9,class_weight={0:1,1:20}).fit(X,y).predict(X)) # 0.84 => much better
roc_auc_score(y,LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X)) # 0.85 => similar to manual
roc_auc_score(y,(LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict_proba(X)[:,1]>0.5).astype(int)) # same as last

print(classification_report(y,LogisticRegression(C=1e9).fit(X,y).predict(X)))
pd.crosstab(y,LogisticRegression(C=1e9).fit(X,y).predict(X),margins=True)
pd.crosstab(y,LogisticRegression(C=1e9).fit(X,y).predict(X),margins=True,normalize='index') # few prediced TRUE with only 28% TRUE recall and 86% TRUE precision so 6%*28%~=2%

print(classification_report(y,LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X)))
pd.crosstab(y,LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X),margins=True)
pd.crosstab(y,LogisticRegression(C=1e9,class_weight="balanced").fit(X,y).predict(X),margins=True,normalize='index') # 88% TRUE recall but also lot of false positives with only 23% TRUE precision, making total predicted % TRUE > actual % TRUE