ChatGPT解决这个技术问题 Extra ChatGPT

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

我在理解 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 比例的负:正训练样本翻译成 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

这一次,结果完全出乎意料。对于除 auto 之外的每个 class_weight 值,我所有的回忆都很少(< 0.05)。所以我只能假设我对如何设置 class_weight 字典的理解是错误的。有趣的是,对于 C 的所有值,网格搜索中“auto”的 class_weight 值约为 59%,我猜它平衡到 1:1?

我的问题

您如何正确使用 class_weight 来实现与实际提供的训练数据不同的平衡?具体来说,我应该将什么字典传递给 class_weight 以使用 n:m 比例的负:正训练样本?如果您将各种 class_weight 字典传递给 GridSearchCV,在交叉验证期间,它是否会根据字典重新平衡训练折叠数据,但使用给定的真实样本比例来计算我在测试折叠上的评分函数?这一点很关键,因为任何指标只有在来自观察到的比例数据时才对我有用。就比例而言,class_weight 的 auto 值有什么作用?我阅读了文档,我假设“平衡数据与它们的频率成反比”只是意味着它是 1:1。这个对吗?如果没有,有人可以澄清吗?

当使用 class_weight 时,损失函数会被修改。例如,它不是交叉熵,而是加权交叉熵。 towardsdatascience.com/…

A
Andreas Mueller

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

关于 class_weight 的工作原理:它用 class_weight[i] 而不是 1 来惩罚 class[i] 样本中的错误。因此,较高的类权重意味着您希望更加强调一个类。从您所说的看来,0 类的频率似乎是 1 类的 19 倍。因此,您应该相对于 0 类增加 1 类的 class_weight,例如 {0:.1, 1:.9}。如果 class_weight 不和为 1,它基本上会改变正则化参数。

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


谢谢!快速提问:为了清楚起见,我提到了召回,实际上我正在尝试决定使用哪个 AUC 作为我的衡量标准。我的理解是,我应该最大化 ROC 曲线下的面积或召回率与精度曲线下的面积来查找参数。以这种方式选择参数后,我相信我是通过沿曲线滑动来选择分类的阈值。这是你的意思吗?如果是这样,如果我的目标是捕获尽可能多的 TP,那么两条曲线中哪一条最有意义?另外,感谢您对 scikit-learn 所做的工作和贡献!!!
我认为使用 ROC 将是更标准的方式,但我认为不会有很大的不同。不过,您确实需要一些标准来选择曲线上的点。
@MiNdFrEaK我认为安德鲁的意思是估计器在少数类中复制样本,以便不同类的样本是平衡的。它只是以一种隐式的方式过采样。
@MiNdFrEaK 和 Shawn Tian:当您使用“平衡”时,基于 SV 的分类器不会产生更多较小类的样本。它实际上惩罚了在小班上犯的错误。否则是错误的并且具有误导性,尤其是在大型数据集中,当您负担不起创建更多样本时。必须编辑此答案。
scikit-learn.org/dev/glossary.html#term-class-weight 类权重的使用会因算法而异:对于线性模型(例如线性 SVM 或逻辑回归),类权重将通过按其类权重对每个样本的损失进行加权来改变损失函数。对于基于树的算法,类权重将用于重新加权分裂标准。但是请注意,这种重新平衡并未考虑每个类别中样本的权重。
c
citynorman

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

使用不平衡学习

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

概括

对于没有噪声的中度不平衡数据,应用类权重没有太大区别

对于有噪声和严重不平衡的中度不平衡数据,最好应用类权重

param class_weight="balanced" 在您不想手动优化的情况下工作得很好

使用 class_weight="balanced" 您可以捕获更多真实事件(更高的 TRUE 召回率),但您也更有可能收到错误警报(较低的 TRUE 精度),因此,由于所有误报,总 TRUE 百分比可能高于实际值如果误报是一个问题,AUC 可能会在此处误导您

因此,由于所有误报,总 % TRUE 可能高于实际值

如果误报是一个问题,AUC 可能会在此处误导您

无需将决策阈值更改为不平衡百分比,即使对于严重的不平衡,也可以保持 0.5(或取决于您的需要)

注意

使用 RF 或 GBM 时,结果可能会有所不同。 sklearn does not have class_weight="balanced" 用于 GBM,但 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