如何处理机器学习中类的不平衡问题?

2017年07月25日 由 yining 发表 350100 0
不平衡类使机器学习的“准确性”受到破坏。这在机器学习(特别是分类)中是一个非常普遍的问题,在每个类中都有一个不成比例的数据集。标准的准确性不再可靠地度量性能,这使得模型培训更加棘手。

在本教程中,我们将探讨5种处理不平衡类的有效方法。如何处理机器学习中类的不平衡问题?

在我们开始之前的重要说明:


首先,请注意,我们不会分离出一个单独的测试集,调优超参数,或者实现交叉验证。

换句话说,我们不打算遵循最佳实践。相反,本教程的重点是解决不平衡的类。

此外,并不是所有的技术都适用于每个问题。然而,10次中有9次,其中至少有一种技术是可以获得成功的。

Balance Scale数据集


此教程中,我们将使用一个名为Balance Scale Data的合成数据集,你可以从UCI机器学习存储库中下载。下载地址:   http://archive.ics.uci.edu/ml/datasets/balance+scale

这个数据集最初是用来模拟心理实验结果的,但是它对我们很有用,因为它是一个可管理的规模并且有着不平衡的类。
import pandas as pd
import numpy as np

# Read dataset
df = pd.read_csv('balance-scale.data',
names=['balance', 'var1', 'var2', 'var3', 'var4'])

# Display example observations
df.head()

如何处理机器学习中类的不平衡问题?

这个数据集包含关于一个天平是否平衡的信息,基于两个天平臂的重量和距离。

  • 它有一个目标变量,我们把它标记为balance。

  • 它有四个输入特性,我们通过var4把它标记为var1。


目标变量有三个类

  • 右重的R,即当var3 * var4 > var1 * var2时

  • 左重的L,即当var3 * var4 < var1 * var2时

  • 平衡的B,即当var3 * var4 = var1 * var2时


df['balance'].value_counts()
# R 288
# L 288
# B 49
# Name: balance, dtype: int64

但是在本教程,我们将把它转换成一个二进制的分类问题。如果天平平衡,我们把每个观察标记为1(正类) 或者如果比例不平衡的话,每个观察标记为0(负类):
# Transform into binary classification
df['balance'] = [1 if b=='B' else 0 for b in df.balance]

df['balance'].value_counts()
# 0 576
# 1 49
# Name: balance, dtype: int64
# About 8% were balanced

正如你所看到的,只有8%的观测结果是平衡的。

因此,如果我们总是预测0,我们就能达到92%的准确率。

不平衡类的危害


现在我们有了一个数据集,我们可以真正地展示不平衡类的危害。首先,让我们导入逻辑回归算法和 Scikit-Learn的精确性度量。
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score

接下来,我们将使用一个非常简单的模型来使用所有的默认设置。
# Separate input features (X) and target variable (y)
y = df.balance
X = df.drop('balance', axis=1)

# Train model
clf_0 = LogisticRegression().fit(X, y)

# Predict on training set
pred_y_0 = clf_0.predict(X)

正如上面所提到的,许多机器学习算法的设计是为了在默认情况下最大化总体的精确性。

我们可以证实这一点:
# How's the accuracy?
print( accuracy_score(pred_y_0, y) )
# 0.9216

所以我们的模型有92%的准确率,但这是因为它只预测了一个类吗?
# Should we be excited?
print( np.unique( pred_y_0 ) )
# [0]

正如你所看到的,这个模型只预测0,这意味着它完全忽略了少数类,而偏向于多数类。

接下来,我们将研究处理不平衡类的第一个技巧:对少数类进行采样。

1.上采样少数类


上采样是随机复制少数类的观察结果,以强化其信号。这样做有几个启发,但最常用的方法是简单地用替换来重新采样。

首先,我们将从scikit-learn导入重采样模块:
from sklearn.utils import resample

接下来,我们将创建一个带有上采样的少数类的新DataFrame。

下面是步骤:

  1. 首先,我们将把每个类的观察分离到不同的DataFrames。

  2. 接下来,我们将用替换来对少数类进行重新取样,并设置与多数类相匹配的样本数量。

  3. 最后,我们将把上采样的少数类DataFrame与原始的多数类DataFrame合并在一起。


代码:
# Separate majority and minority classes
df_majority = df[df.balance==0]
df_minority = df[df.balance==1]

# Upsample minority class
df_minority_upsampled = resample(df_minority,
replace=True, # sample with replacement
n_samples=576, # to match majority class
random_state=123) # reproducible results

# Combine majority class with upsampled minority class
df_upsampled = pd.concat([df_majority, df_minority_upsampled])

# Display new class counts
df_upsampled.balance.value_counts()
# 1 576
# 0 576
# Name: balance, dtype: int64

正如你所看到的,新的DataFrame比原始数据有更多的观察值,而这两个类的比率现在是1:1。

让我们用逻辑回归来训练另一个模型,这次是在平衡数据集上:
# Separate input features (X) and target variable (y)
y = df_upsampled.balance
X = df_upsampled.drop('balance', axis=1)

# Train model
clf_1 = LogisticRegression().fit(X, y)

# Predict on training set
pred_y_1 = clf_1.predict(X)

# Is our model still predicting just one class?
print( np.unique( pred_y_1 ) )
# [0 1]

# How's our accuracy?
print( accuracy_score(y, pred_y_1) )
# 0.513888888889

很好,现在模型不再只预测一个类了。虽然准确性也在急剧下降,但作为一个性能指标,它现在更有意义了。

2.下采样多数类


为了防止它的信号在学习算法中占主导地位,下采样会随机地从多数类中去除观察结果。最常见的做法是重新抽样,而且不需要替换。这个过程类似于上采样的过程。

下面是步骤:

  1. 首先,我们将把每个类的观察分离到不同的DataFrames。

  2. 接下来,我们将在没有替换的情况下对多数类进行重新取样,并设置与少数类相匹配的样本数量。

  3. 最后,我们将把下采样的多数类DataFrame与原始的少数类DataFrame合并在一起。


代码:
# Separate majority and minority classes
df_majority = df[df.balance==0]
df_minority = df[df.balance==1]

# Downsample majority class
df_majority_downsampled = resample(df_majority,
replace=False, # sample without replacement
n_samples=49, # to match minority class
random_state=123) # reproducible results

# Combine minority class with downsampled majority class
df_downsampled = pd.concat([df_majority_downsampled, df_minority])

# Display new class counts
df_downsampled.balance.value_counts()
# 1 49
# 0 49
# Name: balance, dtype: int64

这一次,新的DataFrame的观测数据比原来的少,而这两个类的比率现在是1:1。

让我们再一次用逻辑回归来训练一个模型:
# Separate input features (X) and target variable (y)
y = df_downsampled.balance
X = df_downsampled.drop('balance', axis=1)

# Train model
clf_2 = LogisticRegression().fit(X, y)

# Predict on training set
pred_y_2 = clf_2.predict(X)

# Is our model still predicting just one class?
print( np.unique( pred_y_2 ) )
# [0 1]

# How's our accuracy?
print( accuracy_score(y, pred_y_2) )
# 0.581632653061

这个模型并不是只预测一个类,而且它的准确性更高。我们仍然希望在一个不可见的测试数据集上验证模型。

3.改变你的性能指标


到目前为止,我们已经研究了通过重新采样数据集来解决不平衡类的两种方法。接下来,我们将考虑使用其他性能指标来评估模型。

爱因斯坦曾经说过:“如果你根据能不能爬树来判断一条鱼的能力,那你一生都会认为它是愚蠢的。”这句话确实强调了选择正确的评价指标的重要性。对于一个用于分类的通用度量标准,我们推荐在ROC曲线下的区域(AUROC)。

  • 我们不会在本教程中详细介绍它的细节,但你可以在这里阅读更多相关内容。

  • 直观地说,AUROC代表了你的模型将观察与两个类区分开来的可能性。

  • 换句话说,如果你随机地从每个类中选择一个观察,你的模型能够正确地排列它们的概率是多少?


我们可以从Scikit-Learn中导入这个指标:
from sklearn.metrics import roc_auc_score

要计算AUROC,你需要预测类的概率,而不是预测类。你可以使用.predictproba()函数来获得它们:
# Predict class probabilities
prob_y_2 = clf_2.predict_proba(X)

# Keep only the positive class
prob_y_2 = [p[1] for p in prob_y_2]

prob_y_2[:5] # Example
# [0.45419197226479618,
# 0.48205962213283882,
# 0.46862327066392456,
# 0.47868378832689096,
# 0.58143856820159667]

那么,这个模型(在下采样的数据集上的训练)是如何在AUROC上做的呢?
print( roc_auc_score(y, prob_y_2) )
# 0.568096626406

好吧……这与在不平衡的数据集上训练的原始模型相比如何?
prob_y_0 = clf_0.predict_proba(X)
prob_y_0 = [p[1] for p in prob_y_0]

print( roc_auc_score(y, prob_y_0) )
# 0.530718537415

请记住,我们在不平衡的数据集上训练的原始模型的精确度是92%,这远远高于在采样数据集中训练的模型的58%的准确率。然而,后面的模型有57%的AUROC,高于原始模型53%的比例(但不是很多)。

注意:如果你有一个0.47的AUROC,它只是意味着你需要对预测进行反转,因为Scikit-Learn正在曲解正类。AUROC应该>= 0.5。

4.惩罚算法(代价敏感训练)


下一个策略是使用惩罚的学习算法来增加少数群体的分类错误的成本。这种技术的一个流行算法是Penalized-SVM:
from sklearn.svm import SVC

在训练过程中,我们可以用“class_weight='balanced'”来惩罚少数群体的错误,这与他们所代表的不足比例成正比。如果我们想要支持SVM算法的概率估计,我们还需要包含probability=True。

让我们在原始不平衡的数据集上使用Penalized-SVM来训练一个模型:
# Separate input features (X) and target variable (y)
y = df.balance
X = df.drop('balance', axis=1)

# Train model
clf_3 = SVC(kernel='linear',
class_weight='balanced', # penalize
probability=True)

clf_3.fit(X, y)

# Predict on training set
pred_y_3 = clf_3.predict(X)

# Is our model still predicting just one class?
print( np.unique( pred_y_3 ) )
# [0 1]

# How's our accuracy?
print( accuracy_score(y, pred_y_3) )
# 0.688

# What about AUROC?
prob_y_3 = clf_3.predict_proba(X)
prob_y_3 = [p[1] for p in prob_y_3]
print( roc_auc_score(y, prob_y_3) )
# 0.5305236678

同样,我们的目的只是为了说明这个技巧。要真正确定这些策略中哪一种最适合这个问题,你需要在一个测试集上对模型进行评估。

5.使用树型结构算法


我们将考虑的最后一种策略是使用树型结构算法。

决策树通常在不平衡的数据集上表现良好,因为它们的层次结构允许它们从两个类中学习信号。

在现代的应用机器学习中,树群(随机的森林,梯度增长的树木等)几乎总是比奇异的决策树表现得更好,所以我们直接跳到那里:
from sklearn.ensemble import RandomForestClassifier

现在,让我们在原始不平衡的数据集上使用一个随机的森林来训练一个模型。
# Separate input features (X) and target variable (y)
y = df.balance
X = df.drop('balance', axis=1)

# Train model
clf_4 = RandomForestClassifier()
clf_4.fit(X, y)

# Predict on training set
pred_y_4 = clf_4.predict(X)

# Is our model still predicting just one class?
print( np.unique( pred_y_4 ) )
# [0 1]

# How's our accuracy?
print( accuracy_score(y, pred_y_4) )
# 0.9744

# What about AUROC?
prob_y_4 = clf_4.predict_proba(X)
prob_y_4 = [p[1] for p in prob_y_4]
print( roc_auc_score(y, prob_y_4) )
# 0.999078798186

哇! 97%的准确率和100%的AUROC?

然而,虽然这些结果是令人鼓舞的,但是模型可能会过于合适,所以在做出最终决定之前,你仍然应该在一个看不见的测试集中评估你的模型。

注意:由于算法的随机性,你的数字可能略有不同。你可以为可再生的结果设置一个随机的“种子”。

此外,有一些策略并没有进入到本教程中:

创建合成样本(数据增加)


创建合成样本是近距离抽样的“近亲”,有些人可能把它们归为一种。例如, SMOTE算法是一种从少数类中重新采样的方法,同时略微扰动特征值,从而创建“新的”样本。你可以在 imblearn库中找到一个SMOTE的实现。

结合少数类


将目标变量的少数类组合在一起可能适合于一些多类问题。

例如,假设你希望预测信用卡欺诈。在你的数据集中,每一种欺骗方法都可能被单独标记,但是你可能不关心如何去区分它们。你可以将它们组合成一个单一的“欺诈”类,并将此问题作为二进制分类。

结论与展望


在本教程中,我们讨论了5个处理机器学习不平衡类的方法。这些策略受制于没有No Free Lunch theorem这个定理,你应该尝试它们其中的几个,并使用来自测试集的结果来决定你的问题的最佳解决方案。

 

 

此文为编译作品,原网站:https://elitedatascience.com/imbalanced-classes
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
写评论取消
回复取消