Imbalanced-learn 3. Under-sampling

3. 欠采样(Under-sampling)

处理类别不平衡数据集的一种方法是从除少数类之外的所有类别中减少样本数量。少数类指的是观测数量最少的类别。这一类别中最著名的算法是随机欠采样,其做法是以随机方式从目标类别中移除部分样本。

但除此之外还有许多其他算法可以帮助我们减少数据集中样本的数量。根据它们的欠采样策略,这些算法可以分为以下两类:

  • 原型生成方法(Prototype generation methods)
  • 原型选择方法(Prototype selection methods)

在后者中又可进一步细分为:

  • 受控欠采样(Controlled undersampling)
  • 清洗方法(Cleaning methods)

我们将在本文档中讨论这些不同的算法。

另请参阅:比较欠采样方法

3.1 原型生成(Prototype generation)

对于一个原始数据集 S,原型生成算法会生成一个新的集合 S′,满足 |S′| < |S| 且 S′ 不是 S 的子集(S′⊄S)。换句话说,原型生成技术会减少目标类别中的样本数量,而保留下来的样本并非直接从原始数据集中选取,而是通过一定方法生成的。

ClusterCentroids 使用 K-means 聚类方法来减少样本数量。因此,每个类别的样本将被 K-means 方法得到的聚类中心所替代,而不是使用原始样本:

>>> from collections import Counter
>>> from sklearn.datasets import make_classification
>>> X, y = make_classification(n_samples=5000, n_features=2, n_informative=2,
...                            n_redundant=0, n_repeated=0, n_classes=3,
...                            n_clusters_per_class=1,
...                            weights=[0.01, 0.05, 0.94],
...                            class_sep=0.8, random_state=0)
>>> print(sorted(Counter(y).items()))
[(0, 64), (1, 262), (2, 4674)]
>>> from imblearn.under_sampling import ClusterCentroids
>>> cc = ClusterCentroids(random_state=0)
>>> X_resampled, y_resampled = cc.fit_resample(X, y)
>>> print(sorted(Counter(y_resampled).items()))
[(0, 64), (1, 64), (2, 64)]

下图展示了这种欠采样的效果。

_images/sphx_glr_plot_comparison_under_sampling_001.png

ClusterCentroids 提供了一种使用较少样本有效表示数据簇的方式。请注意,此方法要求你的数据能够聚类成簇。此外,设置的质心数量应使得欠采样后的簇能够代表原始数据的分布情况。

警告

ClusterCentroids 支持稀疏矩阵。然而,生成的新样本并不特别稀疏。因此,即使最终的矩阵是稀疏的,在这种情况下该算法的效率也不高。

3.2. 原型选择

原型选择算法会从原始集合 S 中选取样本,生成一个新的数据集 S′,其中 |S′| < |S| 且 S′ ⊂ S。换句话说,S′ 是 S 的一个子集。

原型选择算法可分为两大类:(i) 受控欠采样技术 和 (ii) 清理欠采样技术。

受控欠采样方法将多数类(或多个多数类)中的观测数量减少到用户任意指定的样本数量。通常,这些方法会将观测数量减少至与少数类中观测数量相同的水平。

相比之下,清理欠采样技术则通过移除“噪声”或“过于容易分类”的观测(具体取决于所使用的方法),以对特征空间进行“清洗”。每一类最终保留的观测数量取决于具体的清理方法,而无法由用户指定。

3.2.1. 受控欠采样技术

受控欠采样技术会将目标类别中的观测数量减少到用户指定的数量。

3.2.1.1. 随机欠采样

RandomUnderSampler 是一种快速且简便的平衡数据方法,它通过对目标类随机选择一部分样本来实现数据均衡:

>>> from imblearn.under_sampling import RandomUnderSampler
>>> rus = RandomUnderSampler(random_state=0)
>>> X_resampled, y_resampled = rus.fit_resample(X, y)
>>> print(sorted(Counter(y_resampled).items()))
[(0, 64), (1, 64), (2, 64)]

_images/sphx_glr_plot_comparison_under_sampling_002.png

通过将 replacement 设置为 TrueRandomUnderSampler 可以对数据进行有放回采样(Bootstrap)。当存在多个类别时,每个目标类别将被独立地进行欠采样:

>>> import numpy as np
>>> print(np.vstack([tuple(row) for row in X_resampled]).shape)
(192, 2)
>>> rus = RandomUnderSampler(random_state=0, replacement=True)
>>> X_resampled, y_resampled = rus.fit_resample(X, y)
>>> print(np.vstack(np.unique([tuple(row) for row in X_resampled], axis=0)).shape)
(181, 2)

RandomUnderSampler 支持处理异构数据类型,例如数值型、类别型、日期型等:

>>> X_hetero = np.array([['xxx', 1, 1.0], ['yyy', 2, 2.0], ['zzz', 3, 3.0]],
...                     dtype=object)
>>> y_hetero = np.array([0, 0, 1])
>>> X_resampled, y_resampled = rus.fit_resample(X_hetero, y_hetero)
>>> print(X_resampled)
[['xxx' 1 1.0]
 ['zzz' 3 3.0]]
>>> print(y_resampled)
[0 1]

RandomUnderSampler 同样支持将 pandas 数据框作为输入进行欠采样:

>>> from sklearn.datasets import fetch_openml
>>> df_adult, y_adult = fetch_openml(
...     'adult', version=2, as_frame=True, return_X_y=True)
>>> df_adult.head()  
>>> df_resampled, y_resampled = rus.fit_resample(df_adult, y_adult)
>>> df_resampled.head()  

NearMiss 则加入了一些启发式规则来选择样本[MZ03]。 NearMiss 实现了三种不同类型的启发式方法,可通过参数 version 进行选择:

>>> from imblearn.under_sampling import NearMiss
>>> nm1 = NearMiss(version=1)
>>> X_resampled_nm1, y_resampled = nm1.fit_resample(X, y)
>>> print(sorted(Counter(y_resampled).items()))
[(0, 64), (1, 64), (2, 64)]

正如下一节中所述,NearMiss 的启发式规则基于最近邻算法。因此,参数 n_neighborsn_neighbors_ver3 可接受 scikit-learn 中继承自 KNeighborsMixin 的分类器。前一个参数用于计算到邻居的平均距离,后一个参数则用于对感兴趣的样本进行预选。

3.2.1.2 数学公式

正样本定义为属于需要进行欠采样的目标类别的样本。而负样本则指代来自少数类(即代表性不足最严重的类别)的样本。

NearMiss-1 方法会选择那些到负类样本中最近的 N 个样本平均距离最小的正类样本。

_images/sphx_glr_plot_illustration_nearmiss_001.png

NearMiss-2 方法会选择那些到负类样本中最远的 N 个样本平均距离最小的正类样本。

_images/sphx_glr_plot_illustration_nearmiss_002.png

NearMiss-3 是一个两步算法。首先,对于每一个负类样本,保留其 M 个最近邻样本;然后选择那些到其 N 个最近邻样本的平均距离最大的正类样本。

_images/sphx_glr_plot_illustration_nearmiss_003.png

在接下来的例子中,我们将在前面提到的简单示例上应用不同的 NearMiss 方法。可以看到,每种方法得到的决策边界是有所区别的。

在对特定类别进行欠采样时,NearMiss-1 方法可能会受到噪声的影响。事实上,这意味着目标类别的样本将会围绕这些噪声样本被选择,如下图中黄色类别所示的情况。然而,在正常情况下,通常会选择靠近类别边界附近的样本。而 NearMiss-2 则不会产生这种效果,因为该方法关注的是距离最远的样本,而非最近的样本。我们可以设想,在存在边缘异常值的情况下,噪声同样可能影响 NearMiss-2 的采样结果。相比之下,由于第一步就进行了样本筛选,NearMiss-3 版本受噪声影响的可能性可能是最小的。

_images/sphx_glr_plot_comparison_under_sampling_003.png

3.2.2. 清洗型欠采样技术

清洗型欠采样方法通过对特征空间进行“清洗”,移除那些被认为是“噪声”的观测点或某些“过于容易分类”的观测点(具体取决于所采用的方法)。最终每个目标类别中的样本数量会因不同的清洗方法而异,并不能由用户自行指定。

3.2.2.1. Tomek’s links(Tomek 链接)

当两个来自不同类别的样本互为彼此最近邻时,就构成了一个 Tomek’s link。

从数学上讲,对于两个来自不同类别的样本 x 和 y,如果对于任意样本 z 均满足:

d(x,y) < d(x,z) 且 d(x,y) < d(y,z)

则称这两个样本之间存在一个 Tomek’s link,其中 d(.) 表示两个样本之间的距离。

TomekLinks 用于检测并移除 Tomek 关联 [Tom76b]。其背后的思想是,Tomek 关联中的样本通常是噪声或难以分类的观测值,对算法寻找合适的判别边界没有帮助。

在下图中,一个连接类别 + 和类别 − 的 Tomek 关联以绿色高亮显示:

_images/sphx_glr_plot_illustration_tomek_links_001.png

TomekLinks 找到一个 Tomek 关联时,可以选择仅移除多数类样本,或者同时移除两个样本。参数 sampling_strategy 控制将从关联中移除哪些样本。默认情况下(即 sampling_strategy='auto'),它会移除来自多数类的样本。若希望同时移除多数类和少数类的样本,则可将 sampling_strategy 设置为 'all'

以下图形展示了这种行为:左侧仅移除了多数类的样本,而右侧则移除了整个 Tomek 关联中的两个样本:

_images/sphx_glr_plot_illustration_tomek_links_002.png

3.2.2.2. 使用最近邻编辑数据
3.2.2.2.1. 编辑最近邻

编辑最近邻(Edited Nearest Neighbours)方法通过 K-最近邻算法来识别目标类别样本的邻居,并在某个样本的任意一个或大多数邻居属于不同类别时将其删除 [Wil72]。

EditedNearestNeighbours 执行以下步骤:

  1. 使用完整的数据集训练一个 K-最近邻模型。
  2. 查找每个样本的 K 个最近邻(仅针对目标类别)。
  3. 如果某个样本的任意一个或大多数邻居属于不同类别,则将该样本删除。

以下是代码示例:

>>> sorted(Counter(y).items())
[(0, 64), (1, 262), (2, 4674)]
>>> from imblearn.under_sampling import EditedNearestNeighbours
>>> enn = EditedNearestNeighbours()
>>> X_resampled, y_resampled = enn.fit_resample(X, y)
>>> print(sorted(Counter(y_resampled).items()))
[(0, 64), (1, 213), (2, 4568)]

换一种方式解释步骤 3,EditedNearestNeighbours 在其大多数全部邻居都属于同一类别时,会保留多数类中的样本。为了控制这种行为,可以分别设置 kind_sel='mode'kind_sel='all'。因此,kind_sel='all'kind_sel='mode' 更不保守,会导致更多的样本被删除:

>>> enn = EditedNearestNeighbours(kind_sel="all")
>>> X_resampled, y_resampled = enn.fit_resample(X, y)
>>> print(sorted(Counter(y_resampled).items()))
[(0, 64), (1, 213), (2, 4568)]
>>> enn = EditedNearestNeighbours(kind_sel="mode")
>>> X_resampled, y_resampled = enn.fit_resample(X, y)
>>> print(sorted(Counter(y_resampled).items()))
[(0, 64), (1, 234), (2, 4666)]

参数 n_neighbors 接收整数值。该整数表示针对每个样本需要检查的邻居数量。此外,也可以传入一个从 scikit-learn 的 KNeighborsMixin 派生的分类器子类。当传递一个分类器时请注意,如果你传入的是一个 3-近邻分类器,则在清理过程中只会检查其中的两个邻居,因为第三个样本正是当前用于欠采样的样本本身,它已经包含在 fit 方法所提供的数据中。

3.2.2.2.2. 重复编辑近邻方法(Repeated Edited Nearest Neighbours)

RepeatedEditedNearestNeighbours 通过多次重复算法扩展了 EditedNearestNeighbours [Tom76a]。通常来说,重复使用该算法会删除更多的数据:

>>> from imblearn.under_sampling import RepeatedEditedNearestNeighbours
>>> renn = RepeatedEditedNearestNeighbours()
>>> X_resampled, y_resampled = renn.fit_resample(X, y)
>>> print(sorted(Counter(y_resampled).items()))
[(0, 64), (1, 208), (2, 4551)]

用户可以通过参数 max_iter 设置编辑最近邻方法应重复的次数。

当满足以下任一条件时,重复过程将停止:

  1. 达到最大迭代次数,或
  2. 没有更多的观测值被移除,或
  3. 某个多数类变为少数类,或
  4. 在欠采样过程中某个多数类消失。
3.2.2.2.3. AllKNN

AllKNNRepeatedEditedNearestNeighbours 的一种变体,其区别在于在每次 EditedNearestNeighbours 过程中使用的邻居数会逐步增加。它首先基于 1-最近邻进行过滤,并在每次迭代中将邻居数量增加 1 [Tom76a]:

>>> from imblearn.under_sampling import AllKNN
>>> allknn = AllKNN()
>>> X_resampled, y_resampled = allknn.fit_resample(X, y)
>>> print(sorted(Counter(y_resampled).items()))
[(0, 64), (1, 220), (2, 4601)]

当检查邻居的最大数量(由用户通过参数 n_neighbors 设置)达到上限,或者当多数类变成少数类时,AllKNN 将停止清理过程。

在下面的例子中,我们看到 EditedNearestNeighboursRepeatedEditedNearestNeighbours 以及 AllKNN 在清理类别边界处的“噪声”样本时具有类似的效果。

_images/sphx_glr_plot_comparison_under_sampling_004.png

3.2.2.3. Condensed 最近邻方法

CondensedNearestNeighbour 使用一个 1 最近邻规则来迭代地决定是否应移除某个样本 [Har68]。该算法的执行步骤如下:

  1. 将所有少数类样本放入集合 C 中。
  2. 从目标类别(需要欠采样的类别)中选取一个样本加入集合 C,并将该类别的其他所有样本放入集合 S 中。
  3. 基于集合 C 训练一个 1-最近邻分类器。
  4. 按照顺序逐个遍历集合 S 中的样本,并使用步骤 3 中训练好的 1-最近邻规则对其进行分类。
  5. 如果某个样本被错误分类,则将其添加到集合 C 中,并进入第 6 步。
  6. 重复步骤 3 到步骤 5,直到集合 S 中的所有样本都被检查完毕。

最终的数据集为 S,其中包含少数类的所有观测样本以及被连续的 1-近邻算法错误分类的多数类样本。

可以按照以下方式使用 CondensedNearestNeighbour

>>> from imblearn.under_sampling import CondensedNearestNeighbour
>>> cnn = CondensedNearestNeighbour(random_state=0)
>>> X_resampled, y_resampled = cnn.fit_resample(X, y)
>>> print(sorted(Counter(y_resampled).items()))
[(0, 64), (1, 24), (2, 115)]

CondensedNearestNeighbour 对噪声敏感,并可能会引入一些噪声样本(详见后面的图示)。

3.2.2.3.1. 单边选择(One Sided Selection)

为了消除由 CondensedNearestNeighbour 引入的噪声观测值,OneSidedSelection 首先会找出那些难以分类的观测样本,然后使用 TomekLinks 来移除这些噪声样本[Har68]。其执行流程如下:

  1. 将所有少数类样本放入集合 C 中。
  2. 将目标类别(即需要欠采样的类别)中的一个样本加入集合 C,并将该类别的其他所有样本放入集合 S 中。
  3. 在集合 C 上训练一个 1-近邻分类器(1-Nearest Neighbors)。
  4. 使用步骤 3 中训练好的 1 近邻分类规则,对集合 S 中的所有样本进行分类。
  5. 将所有被错误分类的样本加入集合 C。
  6. 从集合 C 中移除 Tomek Links。

最终的数据集为 S,它包含来自少数类的所有观测样本、随机添加的部分多数类样本,以及被 1-近邻算法错误分类的多数类样本。

请注意,与 CondensedNearestNeighbour 不同, OneSidedSelection 并不会在每次样本被错误分类后重新训练一个 K 近邻模型。它在步骤 3 中使用 1 近邻方法对所有多数类样本进行一次分类。该类可以如下使用:

>>> from imblearn.under_sampling import OneSidedSelection
>>> oss = OneSidedSelection(random_state=0)
>>> X_resampled, y_resampled = oss.fit_resample(X, y)
>>> print(sorted(Counter(y_resampled).items()))
[(0, 64), (1, 174), (2, 4404)]

我们的实现提供了一个选项,可通过参数 n_seeds_S 来设定随机放入集合 C 中的观测数量。

NeighbourhoodCleaningRule 更侧重于清洗数据而非压缩数据 [Lau01]。因此,它将结合 EditedNearestNeighbours 所拒绝的样本,并通过一个 3 近邻分类器的输出结果来进行数据清理。该类可以如下使用:

>>> from imblearn.under_sampling import NeighbourhoodCleaningRule
>>> ncr = NeighbourhoodCleaningRule(n_neighbors=11)
>>> X_resampled, y_resampled = ncr.fit_resample(X, y)
>>> print(sorted(Counter(y_resampled).items()))
[(0, 64), (1, 193), (2, 4535)]

_images/sphx_glr_plot_comparison_under_sampling_005.png

3.2.3. 其他欠采样技术

3.2.3.1. 实例难度阈值(Instance Hardness Threshold)

实例难度(Instance Hardness)用于衡量正确分类某个样本或观测值的难易程度。换句话说,较难的实例是指那些难以被正确分类的观测。

本质上,难以正确分类的实例是指学习算法或分类器对其预测为正确类别标签的概率较低的那些样本。

我们的思路是,如果从数据集中移除这些难以分类的实例,将有助于分类器更有效地识别不同类别 [SMGC14]。

InstanceHardnessThreshold 会在数据上训练一个分类器,然后移除预测概率较低的样本 [SMGC14]。换句话说,它会保留那些具有较高类别预测概率的观测样本。

在我们的实现中,InstanceHardnessThreshold 几乎是一种受控欠采样方法:它会保留目标类别中的特定数量的观测样本,该数量由用户指定(见下文注意事项)。

此类的使用方式如下:

>>> from sklearn.linear_model import LogisticRegression
>>> from imblearn.under_sampling import InstanceHardnessThreshold
>>> iht = InstanceHardnessThreshold(random_state=0,
...                                 estimator=LogisticRegression())
>>> X_resampled, y_resampled = iht.fit_resample(X, y)
>>> print(sorted(Counter(y_resampled).items()))
[(0, 64), (1, 64), (2, 64)]

InstanceHardnessThreshold 包含两个重要的参数。参数 estimator 可接受任何具有 predict_proba 方法的 scikit-learn 分类器。该分类器将用于识别难以分类的样本。训练过程通过交叉验证完成,交叉验证的方式可通过参数 ``cv` 指定。

注意

InstanceHardnessThreshold 几乎可以被看作一种受控欠采样方法。然而,由于概率输出的原因,有时可能无法精确获得用户指定的样本数量。

下图展示了在一个示例数据集上进行实例硬度欠采样的效果。

_images/sphx_glr_plot_comparison_under_sampling_006.png

le/references/generated/imblearn.under_sampling.InstanceHardnessThreshold.html#imblearn.under_sampling.InstanceHardnessThreshold) 几乎可以被看作一种受控欠采样方法。然而,由于概率输出的原因,有时可能无法精确获得用户指定的样本数量。

下图展示了在一个示例数据集上进行实例硬度欠采样的效果。

[外链图片转存中…(img-gJyBTSER-1751008927739)]

该篇文章由ChatGPT翻译,如有疑问,欢迎提交Issues

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值