使用不平衡数据构建机器学习分类器

5

我有一个数据集,包含1400个观测值和19列。目标变量的值为1(我最感兴趣的值)和0。类别分布不平衡(70:30)。

使用以下代码,我得到了奇怪的值(全部为1)。我无法确定这是由于过度拟合/不平衡数据还是特征选择问题(我使用了皮尔逊相关性,因为所有值都是数字/布尔类型)。我认为所采取的步骤可能有误。

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

在数据存在不平衡的情况下,使用交叉验证和/或欠采样构建分类器时,是否有人遇到类似的问题?如果您想要复制输出结果,我很乐意分享整个数据集。
我想要问您一些清晰的步骤,以便能够向我展示我正在做错了什么。
我知道为了减少过拟合并处理平衡数据,有一些方法,例如随机抽样(过度/欠采样),SMOTE,CV。我的想法是:
- 在考虑不平衡性的情况下将数据分成训练/测试集 - 对训练集进行交叉验证 - 仅对一个测试折进行欠采样 - 在通过CV选择模型后,对训练集进行欠采样并训练分类器 - 对未触及的测试集进行性能估计(f1分数)
就像在这个问题:CV and under sampling on a test fold中所概述的那样。
我认为上述步骤应该是有意义的,但我很乐意接受您可能对此的任何反馈。

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

3

还有另一种解决方案可以在模型层面上实现,即使用支持样本权重的模型,例如梯度提升树。其中,CatBoost通常是最好的选择,因为它的训练方法会导致更少的信息泄漏(正如他们的文章所描述的那样)。

示例代码:

from catboost import 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的classification_report更直观的输出,可以使用Deepchecks内置的检查之一(披露-我是维护者之一):

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

3

当您拥有不平衡的数据时,您需要执行分层抽样。通常的方法是对少量值的类别进行过采样。

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

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

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 中评估您的模型。

2

您的分层训练/测试创建实现不够优化,因为缺乏随机性。很多时候数据是批量输入的,因此不好的做法是将数据序列直接拿来使用,而不进行洗牌。
正如@sturgemeister所提到的,类别比率3:7并不严重,因此您不应过于担心类别不平衡问题。当您人为地改变训练数据的平衡时,您需要通过某些算法的先验概率进行补偿。
至于您的“完美”结果,要么是您的模型过度训练了,要么是模型确实完美地对数据进行分类。使用不同的训练/测试拆分来检查这一点。
另外一个问题是,您的测试集只有94个数据点。这明显不是1400的五分之一。请检查您的数字。
为了获得真实的估计值,您需要大量的测试数据。这就是为什么您需要应用交叉验证策略的原因。
至于5倍交叉验证的一般策略,我建议按照以下步骤进行:
1.根据标签将数据分成5个部分(这称为分层拆分,您可以使用StratifiedShuffleSplit函数)。 2.取4个部分并训练您的模型。如果您想使用欠采样/过采样,请修改这4个训练部分中的数据。 3.将模型应用于其余部分。不要在测试部分中欠采样/过采样数据。这样您就可以获得真实的性能估计结果。保存结果。 4.对所有测试拆分(共5次)重复步骤2和3。重要提示:在训练时不要更改模型的参数(例如树深度),它们应该对所有拆分都相同。 5.现在,您已经测试了所有数据点而没有对它们进行训练。这就是交叉验证的核心思想。连接所有保存的结果,并估算性能。

1

交叉验证或留出集

首先,您没有进行交叉验证。您正在将数据拆分为训练/验证/测试集,这很好,并且在训练样本数量较大(例如,>2e4)时通常足够。但是,当样本数量较少时,交叉验证变得有用。

scikit-learn的文档中详细解释了它。您将从数据中取出一个测试集,就像您的create_cv函数一样。然后,您将其余的训练数据拆分为例如3个部分。然后,对于i{1, 2, 3}中:在数据j!= i上进行训练,在数据i上进行评估。文档使用漂亮而丰富多彩的图表进行了解释,您应该查看一下!实现起来可能很麻烦,但希望scikit能够轻松完成。

关于数据集不平衡的问题,保持每个集合中标签的相同比例是一个非常好的主意。但是,你可以让scikit为你处理!
目的
此外,交叉验证的目的是选择超参数的正确值。你需要适当的正则化程度,不要太大(欠拟合)也不要太小(过拟合)。如果你正在使用决策树,则最大深度(或每个叶子节点的最小样本数)是要考虑的正确指标,以估计方法的正则化。
结论
只需使用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(组之间没有交集)。
我还建议看一下随机树,在实践中表现更好,尽管不太容易解释。

1

我想要补充一下其他人提到的可能的方法列表,包括阈值处理和成本敏感学习。前者在这里有很好的描述,它包括寻找新的分类正负类别的阈值(通常为0.5,但可以作为超参数处理)。后者则是通过加权来应对类别不平衡。这篇文章对我理解如何处理不平衡数据集非常有用。其中,您还可以找到使用决策树作为模型进行特定解释的成本敏感学习。此外,所有其他方法都得到了很好的审查,包括:自适应合成抽样、知情欠采样等。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接