数据采样:解决数据不平衡问题的有效策略
1. 数据插补与采样概述
通过插补,我们可以“填充”所有明确缺失的值,这使得许多统计测试和机器学习算法成为可能。接下来,我们将探讨更广泛的采样问题。
1.1 采样概念
- 分类变量和离散化连续变量 :分类变量具有明确的类别,而连续变量可通过离散化转化为类似分类变量的形式。
- 平衡目标类值 :确保不同类别的样本数量相对均衡,以避免模型偏向多数类。
- 无放回采样 :每次采样后,样本不会再次被选中。
- 有放回采样 :每次采样后,样本会被放回,可能再次被选中。
- 重复过采样 :通过复制少数类样本增加其数量。
- 模糊统计过采样 :利用特定算法生成新的少数类样本。
采样是对数据集进行修改,以某种方式重新平衡数据。数据不平衡可能反映了数据收集技术的问题,也可能是所测量现象的潜在模式导致的。当变量为分类变量且类别分布有明显计数时,这种不平衡尤为明显。
1.2 连续变量的不平衡
连续变量的分布不均匀也会导致不平衡问题。许多可测量的量,如正态分布或β分布,通常呈现不均匀分布。但我们会排除一些“长尾”分布,如幂律分布或指数分布。对于长尾分布,可通过取对数或离散化为分位数等方法将其转化为更线性的分布。
以人类身高为例,它大致呈正态分布,但实际上可能基于性别呈现双峰分布,还可能受国籍、年龄等因素影响。尽管人类身高存在差异,但在最短新生儿和最高成年人之间,差异不到5倍;仅考虑成年人时,差异几乎总是在1.5倍以内。这表明身高本质上是线性量,但并非均匀分布。
1.3 示例数据读取与分析
为了更直观地观察身高分布,我们将使用R Tidyverse读取一个包含25,000名(模拟)人类身体测量数据的数据集。
%load_ext rpy2.ipython
%%R -o humans
library('tidyverse')
humans <- read_csv('data/height-weight.csv')
humans
输出结果如下:
── Column specification ───
cols(
Height = col_double(),
Weight = col_double()
)
# A tibble: 25,000 x 2
Height Weight
<dbl> <dbl>
1 167. 51.3
2 182. 61.9
3 176. 69.4
4 173. 64.6
5 172. 65.5
6 174. 55.9
7 177. 64.2
8 178. 61.9
9 172. 51.0
10 170. 54.7
# ... with 24,990 more rows
将身高划分为规则的数值区间,我们可以看到大致的高斯分布,中等身高的出现频率远高于较矮或较高的范围。而且,样本中的所有人(几乎所有成年人)身高都在153 cm至191 cm的狭窄范围内。
humans.hist(figsize=(10,3), bins=12);
通过以下代码查看身高区间的分布:
%%R
table(cut(humans$Height, breaks = 5))
输出结果为:
(153,161] (161,168] (168,176] (176,183] (183,191]
145 4251 14050 6229 325
如果我们试图根据其他特征(如营养、国籍、性别、年龄、收入等)预测身高,对于许多机器学习模型来说,“非常矮”和“非常高”等稀有类别的预测几乎不可能。因为与少数非常矮的人在其他特征上相似的人太多,默认预测可能只是“有点矮”甚至“平均身高”。
此外,如果自变量的参数空间区域不平衡,也会出现类似问题。例如,在假设的训练集中,如果印度尼西亚或荷兰的样本很少,而其他国家的样本很多,我们就很难利用这两个国家居民分别具有最短和最高平均身高这一事实。而且,如果少数样本中包含特别矮的荷兰人或特别高的印度尼西亚人,类别值的存在可能会使预测朝着与我们期望相反的方向偏差。
2. 欠采样
2.1 示例数据集
我们以UCI机器学习1997年的汽车评估数据集为例。原始数据集使用各种分类词表示顺序值,如后备箱大小为“小”“中”“大”,维护价格为“低”“中”“高”“非常高”。在处理中,这些被转换为顺序整数,但我们关注的整体评级仍保留描述性词语。
%%R
cars <- read_csv('data/cars.csv', col_types = cols("i", "i", "i", "i", "i", "i"))
cars
输出结果如下:
# A tibble: 1,728 x 7
price_buy price_maintain doors passengers trunk safety
<int> <int> <int> <int> <int> <int>
1 1 0 3 6 0 0
2 2 2 3 6 2 1
3 2 2 5 2 1 1
4 0 1 3 2 2 1
5 2 1 5 2 0 1
6 3 1 2 6 2 1
7 0 2 4 4 0 0
8 1 2 2 4 2 0
9 1 0 4 4 0 1
10 1 3 3 2 0 0
# ... with 1,718 more rows
假设我们要根据汽车的其他特征预测其“可接受性”。从数据的前几行可以看出,有大量汽车是不可接受的。我们来查看整体评级的类别分布:
%%R
table(cars$rating)
输出结果为:
Unacceptable Acceptable Very Good Good
1210 384 65 69
这些汽车的评估者可能比较挑剔,认为只有极少数汽车是好的或非常好的。这表明评级特征存在严重的不平衡,我们可能会将其作为分类模型的目标。
2.2 欠采样的条件
不同的建模技术受采样技术的影响程度不同。例如,线性模型对类别不平衡基本不敏感,而K近邻模型则对这些问题高度敏感。欠采样在以下三个条件满足时是可行的:
- 数据集中有大量的行。
- 即使是不常见的类别也有合理数量的样本。
- 样本能够很好地覆盖参数空间。
如果幸运地满足所有这些条件,选择最小类别的样本大小就足够了。但如果无法满足这些条件,特别是最小类别的样本太少,允许一定程度的不平衡通常不是很严重的问题。例如,50:1的不平衡可能会有问题,而2:1的不平衡可能影响不大。
2.3 欠采样操作
对于汽车评估数据集,我们尝试从每个类别中选取100个样本,如果不足则选取尽可能多的样本。
%%R
unacc <- sample(which(cars$rating == "Unacceptable"), 100)
acc <- sample(which(cars$rating == "Acceptable"), 100)
good <- sample(which(cars$rating == "Good"), 69)
vgood <- sample(which(cars$rating == "Very Good"), 65)
samples <- slice(cars, c(vgood, good, acc, unacc))
samples
输出结果如下:
# A tibble: 334 x 7
price_buy price_maintain doors passengers trunk safety
<int> <int> <int> <int> <int> <int>
1 0 1 2 6 2 2
2 0 0 4 4 2 2
3 1 0 3 6 1 2
4 0 0 5 6 1 2
5 1 0 3 4 2 2
6 1 1 3 6 1 2
7 1 0 5 4 1 2
8 1 0 4 4 1 2
9 0 0 3 6 2 2
10 1 1 4 6 2 2
# ... with 324 more rows
这里我们手动选择了每个类别的可用行数,而没有使用像DMwR、caret或ROSE等高级库,这些库可以使采样更加简洁。在Python中,imbalanced-learn是常用的选择,它包含了上述R包中的大多数技术。
R和Python的工具虽然有很多重叠,但在文化和重点上存在一些差异。R更专注于统计,统计领域的库更丰富;而Python倾向于围绕常见的库和API发展。例如,NumPy、Pandas、scikit-learn和imbalanced-learn是Python的“标准”API,而在R中,data.table、data.frame和tibble的API和优势各不相同,DMwR、caret和ROSE也是如此。
2.4 欠采样结果验证
我们查看采样后的数据分布,以确保操作符合预期。
%%R
samples %>%
group_by(rating) %>%
count()
输出结果为:
# A tibble: 4 x 2
# Groups: rating [4]
rating n
<fct> <int>
1 Unacceptable 100
2 Acceptable 100
3 Very Good 65
4 Good 69
不常见类别的样本只有60多个,可能过于稀疏。在很大程度上,样本少的类别无法很好地覆盖特征的参数空间。虽然从较大类别中选取的100个样本增加的数量不多,但由于基础总体较大且采样无偏,这些样本不太可能完全错过参数区域。
2.5 结合欠采样和过采样
虽然采样并不完美,但我们可以通过结合欠采样和过采样来避免可能导致模型偏差的目标不平衡问题。我们通过有放回采样从每个类别中选取150个样本。
%%R
# Find indices for each class (dups OK)
indices <- unlist(
lapply(
# For each level of the rating factor,
levels(cars$rating),
# sample with replacement 150 indices
function(rating) {
pred <- which(cars$rating == rating)
sample(pred, 150, replace = TRUE) }))
# Check that we have drawn evenly
slice(cars, indices) %>%
group_by(rating) %>%
count()
输出结果为:
# A tibble: 4 x 2
# Groups: rating [4]
rating n
<fct> <int>
1 Unacceptable 150
2 Acceptable 150
3 Very Good 150
4 Good 150
以下是欠采样和过采样的流程图:
graph LR
A[原始数据集] --> B{是否满足欠采样条件}
B -- 是 --> C[进行欠采样]
B -- 否 --> D{是否需要平衡数据}
D -- 是 --> E[结合欠采样和过采样]
D -- 否 --> F[使用原始数据集]
C --> G[验证采样结果]
E --> G
G --> H[使用处理后数据集训练模型]
综上所述,欠采样和过采样是解决数据不平衡问题的有效方法,但需要根据具体情况选择合适的策略。在实际应用中,我们应充分考虑数据集的特点和模型的需求,以获得更好的建模效果。
3. 过采样
3.1 过采样的必要性
当数据丰富时,欠采样是为机器学习模型生成更平衡训练数据的快速方法。但大多数情况下,数据集对参数空间的覆盖并不理想,直接进行欠采样丢弃训练数据存在风险。即使有大量观测值,常见类别也会在高维空间的原型区域聚集。如果需要尽可能敏感地评估参数空间,丢弃数据是不明智的。当然,也可能由于模型类型和计算资源的限制,无法在完整数据集上训练模型,此时欠采样有其独立的优势,并且考虑类别敏感性进行欠采样是有益的。
3.2 简单过采样方法
3.2.1 有放回采样至最大类别数量
我们已经了解了最简单的过采样方法,例如在汽车评估数据集中,可以通过有放回采样使每个类别达到最常见类别的数量。但这种方法会在最常见类别中引入一些噪声,因为一些样本会被重复,而另一些会被遗漏。
如果最常见类别有100个样本,有放回采样后大约会遗漏36个样本,同时重复其他样本。而对于初始只有10个样本的类别,有放回采样到100个样本时,几乎可以确定每个样本至少会出现一次。
3.2.2 重复少数类样本
另一种方法是简单地重复不常见类别,使其数量与更常见类别接近。以下是Python代码示例:
import pandas as pd
# 读取原始数据并统计最常见的评级
cars = pd.read_csv('data/cars.csv')
cars2 = cars.copy() # 修改数据框的副本
most_common = max(cars2.rating.value_counts())
for rating in cars2.rating.unique():
# 仅包含一个评级类别的数据框
rating_class = cars2[cars2.rating == rating]
# 重复次数,直到接近最常见类别数量
num_dups = (most_common // len(rating_class)) - 1
for _ in range(num_dups):
cars2 = pd.concat([cars2, rating_class])
print(cars2.rating.value_counts())
输出结果如下:
Unacceptable 1210
Good 1173
Very Good 1170
Acceptable 1152
Name: rating, dtype: int64
这种方法在不使重复不均匀的情况下,使每个不常见类别尽可能接近多数类别的频率。允许轻微的不平衡是更好的方法。
3.3 高级过采样技术:SMOTE和ADASYN
比简单过采样更有趣的是合成少数类过采样技术(Synthetic Minority Over - sampling TEchnique,SMOTE)和用于不平衡数据的自适应合成采样方法(Adaptive Synthetic Sampling Method for Imbalanced Data,ADASYN)。
在R中有多种实现SMOTE和类似技术的库,如smotefamily、DMwR和ROSE等。但接下来的代码示例我们将使用Python的imbalanced - learn库,因为所需的库选择较少。
SMOTE家族的几种技术虽然有一些技术差异,但总体相似。它们使用K近邻技术生成新的数据点。在少数类样本中,它们查看特征参数空间中的几个最近邻,然后在该参数空间区域内创建一个新的合成样本,该样本与任何现有观测值都不相同。我们可以非正式地将其称为“模糊”过采样。当然,分配给这个合成点的类或目标与现有的少数类观测值簇相同。这种具有特征值模糊性的过采样通常比简单过采样创建更有用的合成样本。
3.4 使用SMOTE进行过采样的Python代码示例
汽车评级类别的不平衡情况如下:
print(cars.rating.value_counts())
输出结果为:
Unacceptable 1210
Acceptable 384
Good 69
Very Good 65
Name: rating, dtype: int64
imbalanced - learn库提供了几种类似的过采样技术,这些技术都基于scikit - learn的API构建,可以包含在scikit - learn管道中,并与该库进行交互。以下是使用SMOTE进行过采样的代码:
from imblearn.over_sampling import SMOTE
# 将数据框分为特征X和目标y
X = cars.drop('rating', axis = 1)
y = cars['rating']
# 创建重采样后的特征和目标
X_res, y_res = SMOTE(k_neighbors = 4).fit_resample(X, y)
# 合并特征和目标回到类似原始的数据框
synth_cars = X_res.copy()
synth_cars['rating'] = y_res
print(synth_cars.sample(8, random_state = 2))
输出结果如下:
price_buy price_maintain doors passengers trunk safety rating
748 2 2 5 6 0 0 Unacceptable
72 0 3 2 6 0 0 Unacceptable
2213 3 0 2 4 0 0 Acceptable
1686 2 3 5 2 0 0 Unacceptable
3578 0 0 4 6 1 0 Good
3097 0 0 2 4 0 0 Good
4818 0 1 4 4 1 0 Very Good
434 2 3 5 6 2 0 Unacceptable
我们可以看到目标类别已经完全平衡:
print(synth_cars.rating.value_counts())
输出结果为:
Good 1210
Very Good 1210
Unacceptable 1210
Acceptable 1210
Name: rating, dtype: int64
与执行SMOTE的几个R库不同,imbalanced - learn保留了特征的数据类型。特别是,特征的顺序整数保持为整数。对于某些特征,如价格从“低”到“非常高”编码为0 - 3的连续值可能是合理的,但门的数量从语义上讲是整数。不过,对于许多模型来说,连续变量提供了更有用的聚类,因此可能更倾向于使用浮点输入进行训练。
以下是将数据类型转换为浮点型并再次进行重采样的代码:
cars.iloc[:, :6] = cars.iloc[:, :6].astype(float)
print(cars.head())
# 将数据框分为特征X和目标y
X = cars.drop('rating', axis = 1)
y = cars['rating']
# 创建重采样后的特征和目标
X_, y_ = SMOTE().fit_resample(X, y)
print(pd.concat([X_, y_], axis = 1).sample(6, random_state = 4))
输出结果如下:
price_buy price_maintain doors passengers trunk safety rating
0 1.0 0.0 3.0 6.0 0.0 0.0 Unacceptable
1 2.0 2.0 3.0 6.0 2.0 1.0 Acceptable
2 2.0 2.0 5.0 2.0 1.0 1.0 Unacceptable
3 0.0 1.0 3.0 2.0 2.0 1.0 Unacceptable
4 2.0 1.0 5.0 2.0 0.0 1.0 Unacceptable
3.5 过采样方法对比
| 过采样方法 | 优点 | 缺点 |
|---|---|---|
| 有放回采样至最大类别数量 | 实现简单 | 会在多数类引入噪声,可能遗漏部分样本 |
| 重复少数类样本 | 操作直接,可使少数类接近多数类数量 | 可能导致数据分布过于集中,缺乏多样性 |
| SMOTE和ADASYN | 生成新的合成样本,增加数据多样性 | 计算复杂度相对较高,可能生成不合理的样本 |
3.6 过采样流程图
graph LR
A[原始数据集] --> B{是否需要过采样}
B -- 是 --> C{选择过采样方法}
C -- 有放回采样至最大类别数量 --> D[执行有放回采样]
C -- 重复少数类样本 --> E[重复少数类样本]
C -- SMOTE或ADASYN --> F[使用SMOTE或ADASYN生成合成样本]
D --> G[验证过采样结果]
E --> G
F --> G
G --> H[使用过采样后数据集训练模型]
B -- 否 --> I[使用原始数据集训练模型]
4. 总结
数据不平衡是机器学习中常见的问题,欠采样和过采样是解决这一问题的有效策略。欠采样适用于数据丰富且满足一定条件的情况,而过采样则在数据对参数空间覆盖不佳或需要保留更多数据信息时更为有用。
简单的过采样方法如简单重复和有放回采样易于实现,但可能存在一些局限性。而SMOTE和ADASYN等高级过采样技术通过生成合成样本增加了数据的多样性,但计算复杂度相对较高。
在实际应用中,我们需要根据数据集的特点、模型的需求以及计算资源等因素,综合考虑选择合适的采样方法。同时,还可以结合欠采样和过采样,以达到更好的平衡效果,提高模型的性能和泛化能力。
通过本文介绍的方法和代码示例,希望读者能够更好地理解和应用数据采样技术,解决实际中的数据不平衡问题。
超级会员免费看
3万+

被折叠的 条评论
为什么被折叠?



