使用不平衡数据构建 ML 分类器

Posted

技术标签:

【中文标题】使用不平衡数据构建 ML 分类器【英文标题】:Building ML classifier with imbalanced data 【发布时间】:2021-09-18 08:29:11 【问题描述】:

我有一个包含 1400 个 obs 和 19 列的数据集。 Target 变量具有值 1(我最感兴趣的值)和 0。类的分布显示不平衡 (70:30)。

使用下面的代码,我得到了奇怪的值(全为 1)。我不知道这是由于数据过度拟合/不平衡问题还是由于特征选择问题(我使用了 Pearson 相关性,因为所有值都是数字/布尔值)。 我认为遵循的步骤是错误的。

import numpy as np
import math
import sklearn.metrics as metrics
from sklearn.metrics import f1_score

y = df['Label']
X = df.drop('Label',axis=1)

def create_cv(X,y):
    if type(X)!=np.ndarray:
        X=X.values
        y=y.values
 
    test_size=1/5
    proportion_of_true=y[y==1].shape[0]/y.shape[0]
    num_test_samples=math.ceil(y.shape[0]*test_size)
    num_test_true_labels=math.floor(num_test_samples*proportion_of_true)
    num_test_false_labels=math.floor(num_test_samples-num_test_true_labels)
    
    y_test=np.concatenate([y[y==0][:num_test_false_labels],y[y==1][:num_test_true_labels]])
    y_train=np.concatenate([y[y==0][num_test_false_labels:],y[y==1][num_test_true_labels:]])

    X_test=np.concatenate([X[y==0][:num_test_false_labels] ,X[y==1][:num_test_true_labels]],axis=0)
    X_train=np.concatenate([X[y==0][num_test_false_labels:],X[y==1][num_test_true_labels:]],axis=0)
    return X_train,X_test,y_train,y_test

X_train,X_test,y_train,y_test=create_cv(X,y)
X_train,X_crossv,y_train,y_crossv=create_cv(X_train,y_train)
    
tree = DecisionTreeClassifier(max_depth = 5)
tree.fit(X_train, y_train)       

y_predict_test = tree.predict(X_test)

print(classification_report(y_test, y_predict_test))
f1_score(y_test, y_predict_test)

输出:

     precision    recall  f1-score   support

           0       1.00      1.00      1.00        24
           1       1.00      1.00      1.00        70

    accuracy                           1.00        94
   macro avg       1.00      1.00      1.00        94
weighted avg       1.00      1.00      1.00        94

在数据不平衡、使用 CV 和/或抽样不足时,是否有人在构建分类器时遇到过类似问题?很高兴分享整个数据集,以防您可能想要复制输出。 我想问你一些明确的答案,可以告诉我步骤和我做错了什么。

我知道,为了减少过度拟合和处理平衡数据,有一些方法,例如随机抽样(过度/不足)、SMOTE、CV。我的想法是

在考虑不平衡的情况下拆分训练/测试数据 在火车组上执行 CV 仅在测试折叠上应用欠采样 借助 CV 选择模型后,对训练集进行欠采样并训练分类器 估计未触及测试集的性能 (f1-score)

正如这个问题中所概述的那样:CV and under sampling on a test fold。

我认为上述步骤应该是有意义的,但很高兴收到您对此的任何反馈。

【问题讨论】:

只是一个指针。我使用 SMOTE+ENN 作为过采样和欠采样的组合。这对我的数据产生了很好的效果。 非常感谢 Kabilan Mohanraj。我也会看看这种方法。我认为比较不同的方法会很好:) 我不会担心 70:30 比例的决策树不平衡,我会完全消除它。只需进行适当的交叉验证。您的报告说树对测试集进行了完美分类,这很奇怪,我会检查您那里所有 X_/y_ 变量的形状,以确保您得到预期的拆分。如果一切看起来都不错,您的观察中是否有可能有重复的数据?或者也许标签确实可以从观察中完全预测。 谢谢你,sturgemeister,你的建议。我将使用几个分类器,包括上面示例中的决策树,进行比较。我担心的是交叉验证。我认为我的预测出了点问题。我会排除重复数据(如果上述步骤 - 包括我拥有的所有内容 - 不要创建重复数据),但我会说预测采用了错误的字段 看看imbalanced-learn.org/stable 【参考方案1】:

在模型级别还有另一种解决方案 - 使用支持样本权重的模型,例如 Gradient Boosted Trees。其中,CatBoost 通常是最好的,因为它的训练方法可以减少泄漏(如他们的article 中所述)。

示例代码:

从 catboost 导入 CatBoostClassifier

y = df['Label']
X = df.drop('Label',axis=1)
label_ratio = (y==1).sum() / (y==0).sum()
model = CatBoostClassifier(scale_pos_weight = label_ratio)
model.fit(X, y)

等等。 这是因为 Catboost 对每个样本都赋予了权重,因此您可以提前确定类权重 (scale_pos_weight)。 这比下采样要好,在技术上等同于过采样(但需要更少的内存)。

此外,处理不平衡数据的一个主要部分是确保您的指标也得到加权,或者至少是明确定义的,因为您可能希望这些指标具有相同的性能(或倾斜的性能)。

如果您想要比 sklearn 的分类报告更直观的输出,您可以使用 Deepchecks 内置检查之一(披露 - 我是维护者之一):

from deepchecks.checks import PerformanceReport
from deepchecks import Dataset
PerformanceReport().run(Dataset(train_df, label='Label'), Dataset(test_df, label='Label'), model)

【讨论】:

【参考方案2】:

只是想将阈值和成本敏感学习添加到其他人提到的可能方法列表中。前者在here 中得到了很好的描述,包括找到一个新的阈值来对正类和负类进行分类(通常为 0.5,但它可以被视为超参数)。后者包括对类进行加权以应对它们的不平衡性。 This article 对我了解如何处理不平衡的数据集非常有用。在其中,您还可以找到使用决策树作为模型的具有特定解释​​的成本敏感学习。此外,所有其他方法都得到了很好的评价,包括:自适应合成采样、知情欠采样等。

【讨论】:

【参考方案3】:

您对分层训练/测试创建的实施不是最优的,因为它缺乏随机性。数据通常是成批出现的,因此按原样获取数据序列而不进行混洗不是一个好习惯。

正如@sturgemeister 所提到的,班级比例 3:7 并不重要,因此您不必太担心班级不平衡。当您在训练中人为地改变数据平衡时,您需要通过乘以某些算法的先验来补偿它。

至于您的“完美”结果,要么您的模型过度训练,要么模型确实完美地分类了数据。使用不同的训练/测试拆分来检查这一点。

另一点:您的测试集只有 94 个数据点。这绝对不是 1400 的 1/5。检查你的数字。

要获得切合实际的估计,您需要大量的测试数据。这就是您需要应用交叉验证策略的原因。

至于 5 倍简历的一般策略,我建议如下:

    根据标签将数据拆分为 5 折(这称为分层拆分,您可以使用 StratifiedShuffleSplit 函数) 进行 4 次拆分并训练您的模型。如果您想使用欠采样/过采样,请修改这 4 个训练拆分中的数据。 将模型应用于其余部分。不要低于/超过测试部分的样本数据。通过这种方式,您可以获得实际的性能估计。保存结果。 对所有测试拆分重复 2. 和 3.(显然总共 5 次)。重要提示:在训练时不要更改模型的参数(例如树深度) - 它们对于所有拆分都应该相同。 现在您无需接受培训即可测试所有数据点。这是交叉验证的核心思想。连接所有保存的结果,并估计性能。

【讨论】:

【参考方案4】:

当您的数据不平衡时,您必须执行分层。通常的方法是对具有较少值的类进行过采样。

另一个选择是用更少的数据训练你的算法。如果你有一个很好的数据集,那应该不是问题。在这种情况下,您首先从代表较少的类中获取样本,使用集合的大小来计算从另一个类中获取多少样本:

此代码可以帮助您以这种方式拆分数据集:

def split_dataset(dataset: pd.DataFrame, train_share=0.8):
    """Splits the dataset into training and test sets"""
    all_idx = range(len(dataset))
    train_count = int(len(all_idx) * train_share)

    train_idx = random.sample(all_idx, train_count)
    test_idx = list(set(all_idx).difference(set(train_idx)))

    train = dataset.iloc[train_idx]
    test = dataset.iloc[test_idx]

    return train, test

def split_dataset_stratified(dataset, target_attr, positive_class, train_share=0.8):
    """Splits the dataset as in `split_dataset` but with stratification"""

    data_pos = dataset[dataset[target_attr] == positive_class]
    data_neg = dataset[dataset[target_attr] != positive_class]

    if len(data_pos) < len(data_neg):
        train_pos, test_pos = split_dataset(data_pos, train_share)
        train_neg, test_neg = split_dataset(data_neg, len(train_pos)/len(data_neg))
        # set.difference makes the test set larger
        test_neg = test_neg.iloc[0:len(test_pos)]
    else:
        train_neg, test_neg = split_dataset(data_neg, train_share)
        train_pos, test_pos = split_dataset(data_pos, len(train_neg)/len(data_pos))
        # set.difference makes the test set larger
        test_pos = test_pos.iloc[0:len(test_neg)]

    return train_pos.append(train_neg).sample(frac = 1).reset_index(drop = True), \
           test_pos.append(test_neg).sample(frac = 1).reset_index(drop = True)

用法:

train_ds, test_ds = split_dataset_stratified(data, target_attr, positive_class)

您现在可以在 train_ds 上执行交叉验证并在 test_ds 中评估您的模型。

【讨论】:

【参考方案5】:

交叉验证或保留集

首先,您没有进行交叉验证。您正在将数据拆分到训练/验证/测试集中,这很好,并且通常在训练样本数量很大时就足够了(例如,&gt;2e4)。但是,当样本数量很少时(即您的情况),交叉验证就变得很有用了。

在scikit-learn's documentation中有详细解释。您将首先从数据中取出一个测试集,就像您的 create_cv 函数所做的那样。然后,您将其余的训练数据拆分为例如3个分裂。然后,对于1, 2, 3 中的i,您可以这样做:对数据j != i 进行训练,对数据i 进行评估。文档用更漂亮的彩色图形解释它,你应该看看!实现起来可能会很麻烦,但希望 scikit 开箱即用。

对于不平衡的数据集,在每组中保持相同的标签比例是一个非常好的主意。但同样,您可以让 scikit 为您处理!

目的

此外,交叉验证的目的是为超参数选择正确的值。您需要适量的正则化,不要太大(欠拟合)也不要太小(过拟合)。如果您使用的是决策树,则最大深度(或每片叶子的最小样本数)是估计regularization of your method 的正确指标。

结论

只需使用GridSearchCV。您将为您完成交叉验证和标签平衡。

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=1/5, stratified=True)
tree = DecisionTreeClassifier()
parameters = 'min_samples_leaf': [1, 5, 10]
clf = GridSearchCV(svc, parameters, cv=5)  # Specifying cv does StratifiedShuffleSplit, see documentation
clf.fit(iris.data, iris.target)
sorted(clf.cv_results_.keys())

您还可以将cv 变量替换为更高级的洗牌器,例如StratifiedGroupKFold(组之间没有交集)。

我还建议研究随机树,它们的解释性较差,但据说在实践中具有更好的性能。

【讨论】:

以上是关于使用不平衡数据构建 ML 分类器的主要内容,如果未能解决你的问题,请参考以下文章

如何为包含大量异常值的 ML 分类任务准备 IOT 数据?

数据不平衡不平衡采样调整分类阈值过采样欠采样SMOTEEasyEnsemble加入数据平衡的流程代价敏感学习BalanceCascade

不平衡数据集上的一类文本分类

使用多层感知器对不平衡数据集进行分类

Xgboost 处理不平衡的分类数据

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