处理不平衡数据的五种方法

108 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

大家好,我是小寒。

原文链接

你是否曾经遇到过这样的问题:你的数据集中的正类样本非常小,以至于模型无法学习?

在这种情况下,仅通过预测多数类就可以获得相当高的准确度,但无法捕获少数类。

这样的数据集很常见,被称为不平衡数据集。

不平衡数据集是分类问题的一种特殊情况,其中类之间分布不均匀。通常,它们由两个类组成:多数(负)类和少数(正)类。

在不同的领域都会有这样的数据集,例如:

  • 金融:欺诈检测数据集的欺诈率通常约为 1%-2%
  • 广告服务:点击预测数据集的点击率也不高。
  • 运输/航空公司:飞机发生故障的概率也非常低。
  • 医疗:患者是否患有癌症的概率很低。

那么我们如何解决这些问题呢?

1、随机欠采样和过采样

处理高度不平衡的数据集的一种被广泛采用的方法是重采样。它包括从多数类中删除样本(欠采样)和 从少数类中添加更多样本(过采样)。

让我们首先创建一个不平衡的数据集。

from sklearn.datasets import make_classification
X, y = make_classification(
    n_classes=2, class_sep=1.5, weights=[0.9, 0.1],
    n_informative=3, n_redundant=1, flip_y=0,
    n_features=20, n_clusters_per_class=1,
    n_samples=100, random_state=10
)
X = pd.DataFrame(X)
X['target'] = y

我们现在可以使用以下方法进行随机过采样和欠采样:

num_0 = len(X[X['target']==0])
num_1 = len(X[X['target']==1])
print(num_0,num_1)

#random undersample
undersampled_data = pd.concat([X[X['target']==0].sample(num_1),X[X['target']==1]])
print(len(undersampled_data))

# random oversample
oversampled_data = pd.concat([X[X['target']==0],X[X['target']==1].sample(num_0,replace=True)])
print(len(oversampled_data))

2、使用 imblearn 进行欠采样和过采样

imblearn 是 python 的一个包,用于解决不平衡的数据集。它提供了多种欠采样和过采样的方法。

a、使用 Tomek Links 进行欠采样

它提供的其中一种方法称为 Tomek Links,Tomek Links 是邻近的两个相反类的例子。

在这个算法中,我们最终从 Tomek Links 中删除了多数类别的元素,这为分类器提供了一个更好的决策边界。

原理:如果有两个不同类别的样本,它们的最近邻都是对方,也就是A的最近邻是B,B的最近邻是A,那么A,B就是Tomek link。我们要做的就是将所有 Tomek link 都删除掉。那么一个删除 Tomek link 的方法就是,将组成 Tomek link 的两个样本,如果有一个属于多数类样本,就将该多数类样本删除掉。

from imblearn.under_sampling import TomekLinks
tl = TomekLinks(return_indices=True, ratio='majority')
X_tl, y_tl, id_tl = tl.fit_sample(X, y)
b. 使用 SMOTE 进行过采样。

在 SMOTE(Synthetic Minority Oversampling Technique)中,我们在现有元素附近为少数类合成元素。

from imblearn.under_sampling import TomekLinks
tl = TomekLinks(return_indices=True, ratio='majority')
X_tl, y_tl, id_tl = tl.fit_sample(X, y)

imblearn 包中还有多种其他方法可用于欠采样(Cluster Centroids、NearMiss 等)和过采样(ADASYN 和 bSMOTE),你可以查看它们。

3、模型中的 class_weights

大多数机器学习模型都提供了一个名为 class_weights 参数。例如,在随机森林分类器中, 我们可以使用字典为少数类指定更高的权重。

from sklearn.linear_model import LogisticRegression
clf = LogisticRegression(class_weight={0:1,1:10})

我们来看一下它背后的细节。

在逻辑回归中,我们使用交叉熵计算每个样本的损失:

Loss = −ylog(p) − (1−y)log(1−p)

在这种特殊形式中,我们对正类和负类赋予相同的权重。

当我们将 class_weight 设置为 class_weight = {0:1,1:20}时,后台的分类器会尝试最小化:

NewLoss = −20*ylog(p) − 1*(1−y)log(1−p)

那么这里到底发生了什么?

  • 如果我们的模型给出的概率为 0.3,并且我们对正样本进行了错误分类,则 NewLoss 的值为 -20 log(0.3) = 10.45

  • 如果我们的模型给出的概率为 0.7,并且我们错误分类了一个负例,则 NewLoss 的值是 -log(0.3) = 0.52

这意味着,在这种情况下,当模型错误地分类了一个少数正样本时,我们对模型的惩罚要高出大约 20 倍。

我们如何计算 class_weights?

没有一种方法可以做到这一点,这应该构建为针对你的特定问题的超参数搜索问题。

但是,如果你想使用 y 变量的分布来获取 class_weights,你可以使用 sklearn 来实现。

from sklearn.utils.class_weight import compute_class_weight
class_weights = compute_class_weight('balanced', np.unique(y), y)

4、改变你的评估指标

每当我们处理不平衡的数据集时,选择正确的评估指标非常重要。通常,在这种情况下,F1-Score 是我想要的评估指标

那么它有什么帮助呢?

让我们从一个二元预测问题开始。 我们正在预测小行星是否会撞击地球。

因此,我们创建了一个模型,将整个数据集都预测为 “否”。

准确度是多少(通常是最常用的评估指标)?

超过 99%,所以从准确率上看,这个模型还不错,但一文不值。

现在,F1 score 是多少?

正类的召回率是多少?它为零。因此 F1 分数也是 0。

简单来说, F1 score 在分类器的精度和召回率之间保持平衡

你可以使用以下方法计算二元预测问题的 F1 score:

from sklearn.metrics import f1_score
y_true = [0, 1, 1, 0, 1, 1]
y_pred = [0, 0, 1, 0, 0, 1]
f1_score(y_true, y_pred)

5、其它

根据你的用例和你要解决的问题,其他各种方法可能会起作用:

a、收集更多数据

如果可以的话,这是你应该尝试的明确的事情。使用更多正面示例,获取更多数据将有助于你的模型从多数和少数类别的角度获得更多样化的视角。

b、将问题视为异常检测

你可能希望将分类问题视为异常检测问题。

异常检测 是通过与大多数数据显著不同而引起怀疑的稀有项目、事件或观察结果的识别。

c、基于模型

有些模型特别适合不平衡的数据集。

例如,在提升模型中,我们给在每次树迭代中被错误分类的案例赋予更多的权重。

本文由mdnice多平台发布