4、数据层面方法(下):欠采样(Undersampling)与数据增强(Data Augmentation)策略
上一节我们聊了过采样,说白了就是给少数类「加人」。那这一节反过来,讲讲怎么给多数类「裁员」——也就是欠采样。再加上数据增强这个万能工具箱,咱们把数据层面的招数凑齐。
我个人习惯把这两招放在一起讲,因为它们在实战中经常搭配使用。你想想看,一个负责砍掉冗余样本,一个负责创造新样本,配合好了效果翻倍。
4.1 欠采样:给多数类「瘦身」
欠采样的思路很直接:从多数类里挑一部分样本出来,让正负样本数量接近。听起来简单,但怎么挑是个技术活。
4.1.1 随机欠采样
最朴素的做法,就是从多数类里随机抽一批样本,扔掉剩下的。我在项目中遇到过一个小问题:随机欠采样虽然快,但容易丢掉重要信息。比如检测行人时,多数类里可能包含各种姿态、光照下的样本,随机一扔,关键特征就没了。
# 随机欠采样示例
import random
import numpy as np
def random_undersample(X_major, y_major, target_count):
"""
X_major: 多数类特征
y_major: 多数类标签
target_count: 目标样本数
"""
indices = random.sample(range(len(X_major)), target_count)
return X_major[indices], y_major[indices]
# 假设多数类有10000张图,目标保留2000张
X_major_under, y_major_under = random_undersample(X_major, y_major, 2000)
print(f"欠采样后多数类样本数: {len(X_major_under)}")
4.1.2 基于聚类的欠采样
那有没有更聪明的办法?有。基于聚类的欠采样就是先把多数类聚成K个簇,然后从每个簇里均匀采样。这样能保证采样后的样本分布和原始分布一致。
说白了,就是保留多数类的「多样性」。我习惯用K-Means做聚类,简单高效。
from sklearn.cluster import KMeans
import numpy as np
def cluster_undersample(X_major, y_major, n_clusters=10, samples_per_cluster=200):
"""
基于聚类的欠采样
"""
kmeans = KMeans(n_clusters=n_clusters, random_state=42)
cluster_labels = kmeans.fit_predict(X_major)
sampled_indices = []
for cluster_id in range(n_clusters):
cluster_indices = np.where(cluster_labels == cluster_id)[0]
if len(cluster_indices) > samples_per_cluster:
selected = np.random.choice(cluster_indices, samples_per_cluster, replace=False)
else:
selected = cluster_indices
sampled_indices.extend(selected)
return X_major[sampled_indices], y_major[sampled_indices]
4.1.3 Tomek Links 与 NearMiss
还有两个经典方法值得一提。Tomek Links会找到那些「互为最近邻但类别不同」的样本对,然后删除多数类的那一个。这能有效清理边界上的噪声。
NearMiss则更激进,它会选择那些离少数类最近的多数类样本。嗯,这里要注意:NearMiss有3种变体,我常用的是NearMiss-2,它选的是离少数类最近的K个多数类样本的平均距离最小的那些。
| 方法 | 核心思想 | 适用场景 | 缺点 |
|---|---|---|---|
| 随机欠采样 | 随机丢弃多数类样本 | 数据量极大,快速实验 | 信息丢失严重 |
| 聚类欠采样 | 保留分布多样性 | 多数类分布不均匀 | 计算量较大 |
| Tomek Links | 清理边界噪声 | 类别边界模糊 | 对噪声敏感 |
| NearMiss-2 | 保留难分样本 | 少数类样本少且分散 | 可能过拟合 |
4.2 数据增强:从有限数据中「变」出无限可能
数据增强,说白了就是给数据「化妆」。一张图翻转一下、调个亮度、加个噪声,就变成了一张新图。这在目标检测里尤其重要,因为模型需要学会在各种变化下认出目标。
我刚开始做检测时,总觉得数据增强是锦上添花。直到有一次,一个项目只有500张标注图,靠增强硬是撑到了5000张的效果。从那以后,我再也不敢小看这招了。
4.2.1 几何变换
几何变换是最基础也最常用的增强手段。包括水平翻转、旋转、缩放、裁剪等。注意,目标检测里做几何变换时,标注框也要跟着变。
import cv2
import numpy as np
def horizontal_flip(image, boxes):
"""
水平翻转图像和标注框
image: 图像数组
boxes: [[x1, y1, x2, y2], ...]
"""
h, w = image.shape[:2]
flipped_image = cv2.flip(image, 1)
flipped_boxes = boxes.copy()
flipped_boxes[:, 0] = w - boxes[:, 2] # x1_new = w - x2_old
flipped_boxes[:, 2] = w - boxes[:, 0] # x2_new = w - x1_old
return flipped_image, flipped_boxes
4.2.2 颜色变换
颜色变换包括亮度、对比度、饱和度、色相的调整。这些变换模拟了不同光照条件下的场景。我个人习惯用HSV空间做颜色抖动,效果比RGB空间更自然。
def color_jitter(image, brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1):
"""
颜色抖动增强
"""
import torchvision.transforms as T
transform = T.ColorJitter(
brightness=brightness,
contrast=contrast,
saturation=saturation,
hue=hue
)
return transform(image)
4.2.3 混合增强:MixUp 与 CutMix
这是近几年比较火的增强方法。MixUp是把两张图按比例混合,标签也按同样比例混合。CutMix则是从一张图上切一块贴到另一张图上。
我在一个多类别检测项目里试过CutMix,效果出奇的好。特别是对于小目标,CutMix能强制模型关注局部特征。
def cutmix(image1, boxes1, labels1, image2, boxes2, labels2, alpha=1.0):
"""
CutMix增强
"""
lam = np.random.beta(alpha, alpha)
h, w = image1.shape[:2]
# 随机生成裁剪区域
cut_ratio = np.sqrt(1. - lam)
cut_w = int(w * cut_ratio)
cut_h = int(h * cut_ratio)
cx = np.random.randint(w)
cy = np.random.randint(h)
x1 = np.clip(cx - cut_w // 2, 0, w)
y1 = np.clip(cy - cut_h // 2, 0, h)
x2 = np.clip(cx + cut_w // 2, 0, w)
y2 = np.clip(cy + cut_h // 2, 0, h)
# 混合图像
mixed_image = image1.copy()
mixed_image[y1:y2, x1:x2] = image2[y1:y2, x1:x2]
# 混合标签(需要处理框的归属)
# 这里简化处理,实际需要判断框是否在裁剪区域内
mixed_boxes = np.vstack([boxes1, boxes2])
mixed_labels = np.hstack([labels1, labels2])
return mixed_image, mixed_boxes, mixed_labels
4.2.4 Mosaic 增强
Mosaic是YOLOv4里提出的方法,把4张图拼成一张大图。这样做的好处是:
- 一张图里能看到多个场景,相当于变相增大了batch size
- 小目标被放大了(因为原图被缩小了)
- 背景多样性增加
我曾经在一个工业检测项目里,用了Mosaic后mAP直接涨了3个点。说白了,就是让模型一次性看到更多样化的数据。
4.3 欠采样 + 数据增强的组合策略
实战中,我很少只用一种方法。通常的做法是:
- 先用欠采样把多数类压缩到合理比例(比如3:1到5:1)
- 再用数据增强对少数类做扩充
- 最后整体做一些通用增强(翻转、颜色抖动等)
举个例子,一个行人检测项目,背景图(负样本)有10万张,行人图(正样本)只有2000张。我的做法是:
- 对背景图做聚类欠采样,保留1万张
- 对行人图做Mosaic+CutMix,扩充到8000张
- 整体做随机翻转和颜色抖动
最终正负样本比从1:50变成了1:1.25,模型效果提升明显。
4.4 小结
欠采样和数据增强,一个做减法,一个做加法。两者结合,基本能解决大部分类别不平衡问题。但要注意,数据增强不是万能的,如果原始数据质量太差,再怎么增强也是白搭。
下一节我们会聊聊损失函数层面的方法,看看怎么从优化角度解决不平衡问题。嗯,那又是另一个故事了。