使用分类权重解决数据不平衡的问题

2022-11-11 17:49:23 浏览数 (3)

点击上方“Deephub Imba”,关注公众号,好文章不错过 !

在分类任务中,不平衡数据集是指数据集中的分类不平均的情况,会有一个或多个类比其他类多的多或者少的多。

在我们的日常生活中,不平衡的数据是非常常见的比如本篇文章将使用一个最常见的例子,信用卡欺诈检测来介绍,在我们的日常使用中欺诈的数量要远比正常使用的数量少很多,对于我们来说这就是数据不平衡的问题。

我们使用kaggle上的信用卡交易数据集作为本文的数据集。数据的细节不是特别重要。因为为了进行脱敏,这个数据集的特征是经过PCA降维后输出的,所以讨论这些特征代表什么没有任何意义。除了PCA输出的特征以外,这个数据集还包括与每笔交易相关的美元金额、以秒为单位的连续时间索引,以及一个表示存在或不存在欺诈的二进制目标。对于时间索引,我们考虑到某些特征工程,它可能会很有用,但这不是本文的重点。对于我们真实可见的数据只有金额一项,这个很重要!

我们再看看目标,在284,807行数据中只有0.173%的行是欺诈案例,这绝对是不平衡数据的样例,这种数据的分布会使建模和预测欺诈行为变得有非常的棘手。

性能指标

在不平衡数据时,可以使用几个有价值的性能指标来了解模型的性能。通常情况下,指标的选择很大程度上取决于应用以及与正负相关的结果。单独的一种方法不能适用于所有人。

在信用卡欺诈的背景下,我们不会对产生高准确度分数的模型感兴趣。因为数据集非常不平衡欺诈的数据很少,如果我们将所有样本分类为不存在欺诈,那么准确率还是很高。但是我们对准确预测信用卡交易何时不存在欺诈不感兴趣,我们关心的是信用卡是否存在欺诈,也就是样本量少的分类是否能够被判断出来。

最简单的办法就是召回分数作为模型性能的主要指标。召回是衡量有多少正面案例被模型准确预测的指标。在我们的特定用例中,更高的召回分数意味着我们检测到更多的欺诈案例。

在本文中,我们除了使用召回以外还将分类与最后的财务指标相结合,还记得我们前面提到的数据集的包含交易的美元金额吗?我们也将把它纳入绩效评估,称之为“财务召回”。我们将在下面详细介绍。

数据准备

首先,让我们读入数据,并将其分成训练集和测试集:

代码语言:javascript复制
import pandas as pd
from sklearn.model_selection import train_test_split

df = pd.read_csv('creditcard.csv')

# enumerate the feature columns
feats = [col for col in df.columns if 'V' in col]

# Split data
x = df[feats   ['Amount']]
y = df['Class']

X_train, X_test, y_train, y_test = train_test_split(
    x, y,
    test_size=.2,
    stratify=y,
    random_state=41)

如果以前没有在train_test_split中使用过stratify参数,那么在处理不平衡数据时应该使用该参数,train_test_split分割后欺诈案例的比例会根据传递列的比例进行分配(具体使用方法可以查看sklearn的文档),我们的目标是为了确保我们在训练集和测试集中保持相同比例类别分布。

基类模型

我们将创建并训练一个基本的逻辑回归模型作为基线。但在此之前我们先创建一个小函数,将每笔交易的金额纳入性能评估。

因为关心的是能够正确分类的欺诈案件的数量,并且我们也关心(可能更关心)将欺诈损失的美元保持在最低限度。这就是我们之前讨论的“财务召回”。让我们创建一个函数来计算正确识别的欺诈金额:

代码语言:javascript复制
from sklearn.metrics import recall_score

def assess_test_set_performance(model):
    # create predictions
    yhat = model.predict(X_test[feats])
    # evaluate performance using counts
    model_recall = recall_score(y_test, yhat)*100
    print("Classification Test Performance:")
    print(f"Recall: {model_recall:.2f}%")
    
    # calculate financial performance using the dollar amount associated with each transaction
    performance_df = pd.DataFrame({'Amount':X_test['Amount'], 'Actual':y_test, 'Pred':yhat})
    performance_df['fraud_amount'] = performance_df['Amount']*performance_df['Actual']
    performance_df['fraud_prevented'] = performance_df['fraud_amount']*performance_df['Pred']
    performance_df['fraud_realized'] = performance_df['fraud_amount'] - performance_df['fraud_prevented']
    
    financial_recall = (performance_df['fraud_prevented'].sum() / (performance_df['fraud_prevented'].sum()   performance_df['fraud_realized'].sum()))*100

    print()
    print("Financial Test Performance:")
    print(f"Financial Recall: {financial_recall:.2f}%")

现在我们已经确定了评估函数,让我们训练基线模型并评估它的性能:

代码语言:javascript复制
from sklearn.linear_model import LogisticRegression

baseline_model = LogisticRegression()
baseline_model.fit(X_train[feats], y_train)

assess_test_set_performance(baseline_model)

我们的基线模型实现了59%的召回和61%的财务召回,这实际上比预期的要好。但是这在实际使用时肯定不好,所以其实我们可以做的更好。

改进模型加入类权重

基线模型将两个类设置成同等重要,因为模型不知道我们更关心欺诈的情况,所以我们需要重新定义我们的损失函数。sklearn API提供了让模型知道对正确识别欺诈的偏好:class_weight参数。

当使用class_weight时,模型接收一个字典,每个类都有一个键,其中的值是该类的权重。我们有两类,为了说明这个例子,我们假设欺诈案的重要性是前者的10倍。在这种情况下,我们可以像这样向class_weight传递一个字典:

代码语言:javascript复制
fraud_class_weights = {0:1, 1:10}

但是sklearn API实际上使这个过程更容易。class_weight参数也可以取'balanced'的值。我们需要做的是使用下面的公式建立一个字典,其中权重与数据中的类分布成比例:

代码语言:javascript复制
len(X_train) / (2 * numpy.bincount(y_train))

将上面的公式应用到我们的数据中,我们估计正情况实际上比负情况重要575倍。

当我们把这个新的代码放到逻辑回归模型中时,它将更专注于正确地对我们的欺诈交易进行分类。这正是我们想要的结果!

让我们设置它,然后研究性能:

代码语言:javascript复制
lr_class_weights = LogisticRegression(class_weight='balanced')
lr_class_weights.fit(X_train[feats], y_train)

assess_test_set_performance(lr_class_weights)

这个简单的改变将使我们的召回率提高到90%和93% !因为现在对欺诈更加敏感,比使用基线模型准确地识别更多的欺诈案例。

总结

在模型中使用class_weight肯定会帮助从模型中获得更多相关的性能。并且它很容易设置,至少值得试一试。本文中介绍的方法是解决分类不平衡问题的一种过简单的方法,在这个领域中还有许多其他的方法可以讨论,但是为分类设置权重是一个非常好的开始。

作者;Greg J


MORE

kaggle比赛交流和组队

加我的微信,邀你进群

喜欢就关注一下吧:

点个 在看 你最好看!

1 人点赞