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]
实战技巧:聚类数n_clusters一般设为多数类与少数类比例的1/10左右。比如多数类10000,少数类1000,比例10:1,那n_clusters可以设10-20。

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个点。说白了,就是让模型一次性看到更多样化的数据。

我的建议:Mosaic和CutMix可以搭配使用。比如前10个epoch用Mosaic,后面切回普通增强。这样模型先学全局特征,再学细节特征。

4.3 欠采样 + 数据增强的组合策略

实战中,我很少只用一种方法。通常的做法是:

  1. 先用欠采样把多数类压缩到合理比例(比如3:1到5:1)
  2. 再用数据增强对少数类做扩充
  3. 最后整体做一些通用增强(翻转、颜色抖动等)

举个例子,一个行人检测项目,背景图(负样本)有10万张,行人图(正样本)只有2000张。我的做法是:

  • 对背景图做聚类欠采样,保留1万张
  • 对行人图做Mosaic+CutMix,扩充到8000张
  • 整体做随机翻转和颜色抖动

最终正负样本比从1:50变成了1:1.25,模型效果提升明显。

避坑指南:我曾经在一个项目里过度使用数据增强,结果模型在真实场景下反而变差了。原因是增强后的图像太「假」,比如颜色抖动过大导致图像失真。记住,增强的目的是模拟真实变化,不是创造不存在的数据。

4.4 小结

欠采样和数据增强,一个做减法,一个做加法。两者结合,基本能解决大部分类别不平衡问题。但要注意,数据增强不是万能的,如果原始数据质量太差,再怎么增强也是白搭。

下一节我们会聊聊损失函数层面的方法,看看怎么从优化角度解决不平衡问题。嗯,那又是另一个故事了。