大数跨境

一个强大数据预处理方法,数据增强 !!

一个强大数据预处理方法,数据增强 !! 机器学习和人工智能AI
2026-08-08
1

哈喽,大家好~

我们在实验中,数据集常常有限、偏倚或噪声较多,模型容易过拟合训练集而在测试时表现下滑。

数据增强通过对训练样本进行一系列标注保持(或可控的标签混合)的变换,模拟同分布内的多样性与合理的扰动,使得模型学习到更稳定的特征表示。

它有四个核心动因:

  • 增加有效样本数量:在不增加采集成本的情况下扩大训练覆盖。
  • 引入结构性先验:编码平移、旋转、色彩变换等不变性先验,减少模型对无关因素的敏感性。
  • 改善优化:通过引入噪声与扰动,使损失地形更平滑,促进更稳健的梯度下降。
  • 强化鲁棒性:提高对分布漂移、轻微扰动、遮挡与背景变化的适应能力。

就是说,数据增强既是工程上的技巧,也是统计学习观点下的一种近似风险估计与正则化方法~

01数据增强

我们设输入空间为 ,标签空间为 (例如分类任务 )。

数据由未知分布 生成。

模型 (对分类任务给出概率向量),损失函数为 (例如交叉熵)。

真实风险定义为:

由于我们只能获得有限样本 ,故采用经验风险最小化(ERM):

ERM在样本有限时可能导致过拟合,尤其当模型容量大时。

数据增强试图构造一种“局部”或“邻域”的分布来逼近真实风险的期望。

Vicinal Risk Minimization(邻域风险最小化)与数据增强

Vicinal Risk Minimization(VRM)当年由Chapelle等人提出,其思路是在样本点附近构造邻域分布 ,并对该邻域进行期望,从而近似真实风险:

是围绕样本点的“合适”局部扰动分布时,它能用更丰富的数据近似 在样本附近的质量。数据增强正是构造 的一种工程实现:

  • 选择若干标注保持的变换 ,例如平移、旋转、缩放、颜色抖动、透视变换等;
  • 在训练期间对每个样本产生 ,令 为变换的随机分布,那么 可近似为:

在实际训练中,采用蒙特卡洛估计该期望,即多次随机采样不同 ,从而实现VRM的思想。

不变性、等变性与群作用视角

很多任务具有对某些变换的不变性。例如在手写数字识别中,轻微旋转和平移不应改变类别。

我们可以从群(Group)作用的角度刻画这种结构。设 为变换群(如平移群、旋转群), 作用在输入 上记为

理想的分类器应满足对某些子群 的不变性:

数据增强通过在训练时随机采样 上的分布)并施加在样本上,迫使模型在这种非辨识的自由度上产生不变性。对于检测或分割任务,可能需要等变性(Equivariance):

其中 为与任务相关的输出结构(例如语义分割的像素坐标对齐)。数据增强在一定程度上能强化这种结构性关系。

一致性正则化与平滑化

数据增强可视作一致性正则化,要求模型对增强前后具有一致的预测。

典型形式:

其中 为相似性度量(如 距离或KL散度)。

将其加权融入总损失:

促使模型在增强扰动方向上平滑,减少“局部锯齿状”预测,提升泛化与鲁棒性。

Mixup/AugMix等著名方法的理论表述

Mixup(Zhang et al.)通过线性插值构造样本邻域,属于连续化的VRM实现。给定两样本 ,采样 ,构造:

其中 采用one-hot向量表示。训练最小化:

Mixup对决策边界进行“填充”,缓解过拟合与改善校准。

AugMix(Hendrycks et al.)将若干增强变换随机串联并混合,同时引入一致性损失,兼顾多样性与稳定性,在分布漂移下表现优异。

其思想可表述为:对多个随机增强流水线 产生增强样本 ,混合得到:

并引入一致性项 以稳定预测。

02泛化与复杂度分析

Rademacher复杂度与数据增强

Rademacher复杂度度量假设空间在样本上的拟合能力。

对假设类 ,样本 上的经验Rademacher复杂度定义为:

其中 为Rademacher变量。若我们采用数据增强,那么训练目标由 变为 ,等价地我们在更丰富、局部平滑的样本集上评估模型。

“填充”样本空间可减少模型对偶然噪声的过拟合倾向,从而在某些设置下降低有效复杂度。直观地说,增强将模型预测强制在某些变换下保持稳定,等价于限制了模型在这些方向上的自由度,从而减小 的“可振荡性”。

一个粗略的论证思路:如果增强变换 来自某群 ,且理想模型满足 ,那么允许的函数族变成“ -不变”的子空间 ,其Rademacher复杂度有望低于原空间。形式上可期望:

进而在同样样本规模下给出更紧的泛化误差上界。

PAC-Bayes与分布平滑

PAC-Bayes框架给出随机化分类器的泛化误差上界。数据增强可视作对输入分布的“平滑”,让学习者在某邻域的随机变换上保持一致,即对“后验分布”进行约束。

非形式化地,增强相当于在输入空间拉宽密度,使分类器在更广泛的区域表现一致,从而减少“坏事件”的概率质量。

相应的PAC-Bayes界受后验分布的KL项与经验风险影响,增强可以同时改善经验项(更鲁棒的拟合)与缩减有效假设类(后验更集中)。

Margin与局部 Lipschitz 平滑

对分类器 ,定义样本 处的判别边缘

增强在局部引入扰动,使得优化倾向于增大邻域内的最小边缘:

也被拉高。

等价地,增强推动模型具有较小的局部Lipschitz常数(对增强方向),即:

其中 尽量小。该平滑有利于稳定的决策边界与更好的泛化。

03鲁棒性

真实任务常面对:

  • 传感器噪声、光照变化、镜头畸变;
  • 对象遮挡、姿态变化、背景差异;
  • 训练-测试分布漂移(covariate shift);
  • 坏数据与异常值。

数据增强通过训练时注入这些“合理扰动”,让模型在测试中对相似变化具有不敏感性。理论上,我们可定义增强分布

训练目标近似为在 上的风险。若测试分布 更接近,则模型泛化更佳。

需要注意增强必须尽量“标签保持”,否则会引入系统偏差。

04完整案例

我们构建一个自定义数据集,包含三类几何形状:

  • 类0:圆形(Circle)
  • 类1:正方形(Square)
  • 类2:三角形(Triangle)

每张图像为 像素,背景为黑色或深色,形状为明亮颜色。训练集包含中等数量样本;验证与测试集适当规模。为了凸显增强效果,我们人为引入测试域的分布漂移:

  • 训练集颜色分布偏向暖色;
  • 测试集颜色分布偏向冷色,且对比度与亮度扰动更大。

我们训练两个模型:

  • Baseline:仅轻度预处理;
  • Augmented:强数据增强+ Mixup。

目标:比较两个模型在各种分析指标上的差异与提升。

import os
import random
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image, ImageDraw
from sklearn.manifold import TSNE
from sklearn.metrics import confusion_matrix, roc_curve, auc
from sklearn.preprocessing import label_binarize

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
import torchvision.transforms as T

def set_seed(seed=42):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)

set_seed(1234)

# 1. 图像数据集
class SyntheticShapesDataset(Dataset):
    """
    生成三类几何图形:圆形、正方形、三角形
    - image_size: 64x64
    - domain_shift: 若为True,会采用冷色系并加剧亮度/对比度漂移,模拟测试分布漂移
    每个样本:PIL Image, label(int 0/1/2)
    """

    def __init__(self, n_samples=1000, image_size=64, domain_shift=False):
        super().__init__()
        self.n_samples = n_samples
        self.image_size = image_size
        self.domain_shift = domain_shift
        self.data = []
        self.labels = []
        self._generate()

    def _bright_color(self, cls):
        ifnot self.domain_shift:
            # 训练/验证:偏暖色与多样颜色
            palette = [
                (2558080),
                (2551600),
                (2552200),
                (0200120),
                (80160255),
                (22080255)
            ]
        else:
            # 测试:偏冷色与对比度更高
            palette = [
                (80200255), 
                (0160255), 
                (0220160), 
                (40240200),
                (16080255),
                (0255255)  
            ]
        # 根据类别稍作偏好(非严格)
        idx = (cls + np.random.randint(0, len(palette))) % len(palette)
        return palette[idx]

    def _draw_circle(self, img, draw):
        s = self.image_size
        r = np.random.randint(s//10, s//4)
        cx = np.random.randint(r+4, s-r-4)
        cy = np.random.randint(r+4, s-r-4)
        color = self._bright_color(0)
        draw.ellipse((cx-r, cy-r, cx+r, cy+r), fill=color)
        return img

    def _draw_square(self, img, draw):
        s = self.image_size
        side = np.random.randint(s//6, s//3)
        x0 = np.random.randint(4, s-side-4)
        y0 = np.random.randint(4, s-side-4)
        color = self._bright_color(1)
        draw.rectangle((x0, y0, x0+side, y0+side), fill=color)
        # 随机旋转
        angle = np.random.uniform(-3030)
        img = img.rotate(angle)
        return img

    def _draw_triangle(self, img, draw):
        s = self.image_size
        # 随机三角形顶点
        x1, y1 = np.random.randint(4, s-4), np.random.randint(4, s-4)
        x2, y2 = np.random.randint(4, s-4), np.random.randint(4, s-4)
        x3, y3 = np.random.randint(4, s-4), np.random.randint(4, s-4)
        color = self._bright_color(2)
        draw.polygon([(x1,y1),(x2,y2),(x3,y3)], fill=color)
        # 略微透视仿射
        angle = np.random.uniform(-1515)
        img = img.rotate(angle)
        return img

    def _generate(self):
        s = self.image_size
        for i in range(self.n_samples):
            # 背景
            bg = np.random.randint(040)
            img = Image.new("RGB", (s, s), (bg, bg, bg))
            draw = ImageDraw.Draw(img)
            cls = np.random.randint(03)

            # 形状
            if cls == 0:
                img = self._draw_circle(img, draw)
            elif cls == 1:
                img = self._draw_square(img, draw)
            else:
                img = self._draw_triangle(img, draw)

            # Domain shift: 强化亮度/对比度变化
            if self.domain_shift:
                # 简单的伽马变换
                arr = np.array(img).astype(np.float32)/255.0
                gamma = np.random.uniform(0.61.5)
                arr = np.power(arr, gamma)
                arr = np.clip(arr*1.101)
                img = Image.fromarray((arr*255).astype(np.uint8))

            self.data.append(img)
            self.labels.append(cls)

    def __len__(self):
        return self.n_samples

    def __getitem__(self, idx):
        return self.data[idx], self.labels[idx]


# 2. 定义数据增强与预处理
def get_transforms(mode="baseline", image_size=64):
    if mode == "baseline":
        return T.Compose([
            T.ToTensor(),
            T.Normalize(mean=[0.40.40.4], std=[0.250.250.25])
        ])
    else:
        # 强数据增强
        return T.Compose([
            T.RandomResizedCrop(image_size, scale=(0.71.0), ratio=(0.91.1)),
            T.ColorJitter(brightness=0.4, contrast=0.5, saturation=0.5, hue=0.1),
            T.RandomAffine(degrees=30, translate=(0.10.1), scale=(0.91.1), shear=10),
            T.RandomPerspective(distortion_scale=0.3, p=0.7),
            T.RandomHorizontalFlip(p=0.5),
            T.RandomVerticalFlip(p=0.2),
            T.GaussianBlur(kernel_size=3, sigma=(0.11.5)),
            T.ToTensor(),
            T.RandomErasing(p=0.3, scale=(0.020.12), ratio=(0.32.5), value='random'),
            T.Normalize(mean=[0.40.40.4], std=[0.250.250.25])
        ])


# 3. 定义模型(CNN)
class SimpleCNN(nn.Module):
    def __init__(self, num_classes=3):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3323, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),

            nn.Conv2d(32643, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),

            nn.MaxPool2d(2),  # 32x32
            nn.Conv2d(641283, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),

            nn.MaxPool2d(2),  # 16x16
            nn.Conv2d(1281283, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),

            nn.MaxPool2d(2)  # 8x8
        )
        self.head = nn.Sequential(
            nn.Flatten(),
            nn.Linear(128*8*8256),
            nn.ReLU(inplace=True)
        )
        self.classifier = nn.Linear(256, num_classes)

    def forward(self, x, return_features=False):
        z = self.features(x)
        h = self.head(z)
        logits = self.classifier(h)
        if return_features:
            return h, logits
        return logits


# 4. Mixup实现
def mixup_data(x, y, alpha=0.4):
    # y为整数标签,需转one-hot
    if alpha > 0:
        lam = np.random.beta(alpha, alpha)
    else:
        lam = 1.0
    batch_size = x.size(0)
    index = torch.randperm(batch_size)
    mixed_x = lam * x + (1 - lam) * x[index, :]
    y_onehot = F.one_hot(y, num_classes=3).float()
    mixed_y = lam * y_onehot + (1 - lam) * y_onehot[index, :]
    return mixed_x, mixed_y

def soft_ce_loss(logits, soft_targets):
    # soft_targets: one-hot的soft标签
    log_probs = F.log_softmax(logits, dim=1)
    loss = -(soft_targets * log_probs).sum(dim=1).mean()
    return loss


# 5. 训练与评估
def train_model(model, train_loader, val_loader, optimizer, epochs=20, use_mixup=False, device='cpu'):
    history = {'train_loss': [], 'val_loss': [], 'train_acc': [], 'val_acc': []}
    model.to(device)

    for ep in range(epochs):
        model.train()
        train_loss = 0.0
        correct = 0
        total = 0

        for imgs, labels in train_loader:
            imgs = imgs.to(device)
            labels = torch.tensor(labels).to(device)

            optimizer.zero_grad()
            if use_mixup:
                mix_imgs, mix_labels = mixup_data(imgs, labels, alpha=0.4)
                logits = model(mix_imgs)
                loss = soft_ce_loss(logits, mix_labels)
            else:
                logits = model(imgs)
                loss = F.cross_entropy(logits, labels)

            loss.backward()
            optimizer.step()

            train_loss += loss.item() * imgs.size(0)
            # 对训练准确率的计算(mixup下仅作参考,使用硬标签)
            pred = logits.argmax(dim=1)
            correct += (pred == labels).sum().item()
            total += imgs.size(0)

        avg_train_loss = train_loss / total
        train_acc = correct / total

        # 验证
        model.eval()
        val_loss = 0.0
        v_correct = 0
        v_total = 0
        with torch.no_grad():
            for imgs, labels in val_loader:
                imgs = imgs.to(device)
                labels = torch.tensor(labels).to(device)
                logits = model(imgs)
                loss = F.cross_entropy(logits, labels)
                val_loss += loss.item() * imgs.size(0)
                pred = logits.argmax(dim=1)
                v_correct += (pred == labels).sum().item()
                v_total += imgs.size(0)

        avg_val_loss = val_loss / v_total
        val_acc = v_correct / v_total

        history['train_loss'].append(avg_train_loss)
        history['val_loss'].append(avg_val_loss)
        history['train_acc'].append(train_acc)
        history['val_acc'].append(val_acc)

        print(f"Epoch {ep+1:02d}: train_loss={avg_train_loss:.4f} val_loss={avg_val_loss:.4f} train_acc={train_acc:.4f} val_acc={val_acc:.4f}")

    return history

def get_features_and_preds(model, data_loader, device='cpu'):
    model.to(device)
    model.eval()
    all_features = []
    all_logits = []
    all_labels = []
    with torch.no_grad():
        for imgs, labels in data_loader:
            imgs = imgs.to(device)
            h, logits = model(imgs, return_features=True)
            all_features.append(h.cpu().numpy())
            all_logits.append(logits.cpu().numpy())
            all_labels.extend(labels)
    features = np.concatenate(all_features, axis=0)
    logits = np.concatenate(all_logits, axis=0)
    probs = softmax_np(logits)
    labels = np.array(all_labels)
    return features, probs, labels

def softmax_np(x):
    x = x - x.max(axis=1, keepdims=True)
    e = np.exp(x)
    return e / e.sum(axis=1, keepdims=True)

# 6. 数据加载与训练流程
def main():
    device = 'cpu'
    image_size = 64

    # 构造数据集(训练、验证、测试)
    train_ds = SyntheticShapesDataset(n_samples=1500, image_size=image_size, domain_shift=False)
    val_ds   = SyntheticShapesDataset(n_samples=300, image_size=image_size, domain_shift=False)
    test_ds  = SyntheticShapesDataset(n_samples=500, image_size=image_size, domain_shift=True)  # 模拟测试分布漂移

    baseline_tf = get_transforms("baseline", image_size=image_size)
    aug_tf      = get_transforms("aug", image_size=image_size)

    class WrapDataset(Dataset):
        def __init__(self, base_ds, tf):
            self.base_ds = base_ds
            self.tf = tf
        def __len__(self):
            return len(self.base_ds)
        def __getitem__(self, idx):
            img, label = self.base_ds[idx]
            img = self.tf(img)
            return img, label

    train_baseline = WrapDataset(train_ds, baseline_tf)
    val_baseline   = WrapDataset(val_ds, baseline_tf)
    test_baseline  = WrapDataset(test_ds, baseline_tf)

    train_aug = WrapDataset(train_ds, aug_tf)
    val_aug   = WrapDataset(val_ds, baseline_tf)  # 验证不增强
    test_aug  = WrapDataset(test_ds, baseline_tf) # 测试不增强

    # DataLoaders
    train_loader_baseline = DataLoader(train_baseline, batch_size=64, shuffle=True, num_workers=0)
    val_loader_baseline   = DataLoader(val_baseline, batch_size=64, shuffle=False, num_workers=0)
    test_loader_baseline  = DataLoader(test_baseline, batch_size=64, shuffle=False, num_workers=0)

    train_loader_aug = DataLoader(train_aug, batch_size=64, shuffle=True, num_workers=0)
    val_loader_aug   = DataLoader(val_aug, batch_size=64, shuffle=False, num_workers=0)
    test_loader_aug  = DataLoader(test_aug, batch_size=64, shuffle=False, num_workers=0)

    # 模型与优化器
    model_baseline = SimpleCNN(num_classes=3)
    model_aug      = SimpleCNN(num_classes=3)

    opt_b = torch.optim.Adam(model_baseline.parameters(), lr=1e-3)
    opt_a = torch.optim.Adam(model_aug.parameters(), lr=1e-3)

    # 训练
    print("Training Baseline ...")
    hist_b = train_model(model_baseline, train_loader_baseline, val_loader_baseline, opt_b, epochs=20, use_mixup=False, device=device)

    print("\nTraining Augmented (strong aug + Mixup) ...")
    hist_a = train_model(model_aug, train_loader_aug, val_loader_aug, opt_a, epochs=20, use_mixup=True, device=device)

    # 评估(在测试集)
    feats_b, probs_b, labels_b = get_features_and_preds(model_baseline, test_loader_baseline, device=device)
    feats_a, probs_a, labels_a = get_features_and_preds(model_aug, test_loader_aug, device=device)

    # 混淆矩阵
    preds_b = probs_b.argmax(axis=1)
    preds_a = probs_a.argmax(axis=1)
    cm_b = confusion_matrix(labels_b, preds_b, labels=[0,1,2])
    cm_a = confusion_matrix(labels_a, preds_a, labels=[0,1,2])

    # ROC曲线(一对多)
    y_test_bin = label_binarize(labels_b, classes=[0,1,2])
    fpr_b, tpr_b, auc_b = compute_roc(y_test_bin, probs_b, n_classes=3)
    y_test_bin_a = label_binarize(labels_a, classes=[0,1,2])
    fpr_a, tpr_a, auc_a = compute_roc(y_test_bin_a, probs_a, n_classes=3)

    # t-SNE可视化(对测试特征)
    tsne = TSNE(n_components=2, perplexity=30, learning_rate='auto', init='random', random_state=1234)
    # 为可比,合并再分割
    feats_concat = np.concatenate([feats_b, feats_a], axis=0)
    tsne_2d = tsne.fit_transform(feats_concat)
    tsne_b = tsne_2d[:len(feats_b)]
    tsne_a = tsne_2d[len(feats_b):]

    # 数据分析图
    fig = plot_four_panel(tsne_b, labels_b, tsne_a, labels_a, hist_b, hist_a, cm_b, cm_a, fpr_b, tpr_b, auc_b, fpr_a, tpr_a, auc_a)
    fig.show()

def compute_roc(y_true_bin, y_score, n_classes=3):
    fpr = dict()
    tpr = dict()
    roc_auc = dict()
    for i in range(n_classes):
        fpr[i], tpr[i], _ = roc_curve(y_true_bin[:, i], y_score[:, i])
        roc_auc[i] = auc(fpr[i], tpr[i])
    # micro/macro
    fpr["micro"], tpr["micro"], _ = roc_curve(y_true_bin.ravel(), y_score.ravel())
    roc_auc["micro"] = auc(fpr["micro"], tpr["micro"])
    # macro平均
    all_auc = [roc_auc[i] for i in range(n_classes)]
    roc_auc["macro"] = np.mean(all_auc)
    # 合并fpr/tpr用于绘图(不做严格平均)
    return fpr, tpr, roc_auc

def plot_four_panel(tsne_b, labels_b, tsne_a, labels_a, hist_b, hist_a, cm_b, cm_a, fpr_b, tpr_b, auc_b, fpr_a, tpr_a, auc_a):
    plt.figure(figsize=(1410))
    ax1 = plt.subplot(221)
    ax2 = plt.subplot(222)
    ax3 = plt.subplot(223)
    ax4 = plt.subplot(224)

    # 子图1:t-SNE投影(Baseline vs Augmented)
    colors = np.array([
        [255/25580/25580/255],
        [80/255160/255255/255],
        [0/255200/255120/255]
    ])
    ax1.set_title("t-SNE: Baseline(△) vs Augmented(○)", fontsize=12, fontweight='bold')
    for k in [0,1,2]:
        idx_b = labels_b == k
        idx_a = labels_a == k
        ax1.scatter(tsne_b[idx_b, 0], tsne_b[idx_b, 1], s=12, marker='^', c=[colors[k]], alpha=0.75, label=f"Base C{k}")
        ax1.scatter(tsne_a[idx_a, 0], tsne_a[idx_a, 1], s=12, marker='o', facecolors=colors[k], edgecolors='k', alpha=0.75, label=f"Aug C{k}")
    ax1.legend(loc='best', fontsize=9)
    ax1.grid(True, alpha=0.3)

    # 子图2:训练/验证曲线(loss与acc)
    epochs = np.arange(1, len(hist_b['train_loss'])+1)
    ax2.set_title("Learning Curves: Loss & Accuracy", fontsize=12, fontweight='bold')
    # 左y轴:损失
    ax2.plot(epochs, hist_b['train_loss'], color='#ff5544', lw=2, label='Base Train Loss')
    ax2.plot(epochs, hist_b['val_loss'], color='#ffbb33', lw=2, label='Base Val Loss')
    ax2.plot(epochs, hist_a['train_loss'], color='#33cc99', lw=2, label='Aug Train Loss')
    ax2.plot(epochs, hist_a['val_loss'], color='#3399ff', lw=2, label='Aug Val Loss')
    ax2.set_xlabel("Epoch")
    ax2.set_ylabel("Loss")
    ax2.grid(True, alpha=0.3)
    ax2.legend(loc='upper right', fontsize=9)

    # 次轴:准确率(Twin Axis)
    ax2b = ax2.twinx()
    ax2b.plot(epochs, hist_b['train_acc'], color='#aa22ff', lw=1.8, ls='--', label='Base Train Acc')
    ax2b.plot(epochs, hist_b['val_acc'],  color='#dd22aa', lw=1.8, ls='--', label='Base Val Acc')
    ax2b.plot(epochs, hist_a['train_acc'], color='#00ddaa', lw=1.8, ls='--', label='Aug Train Acc')
    ax2b.plot(epochs, hist_a['val_acc'],  color='#0066ff', lw=1.8, ls='--', label='Aug Val Acc')
    ax2b.set_ylabel("Accuracy")
    # 合并图例
    lines1, labels1 = ax2.get_legend_handles_labels()
    lines2, labels2 = ax2b.get_legend_handles_labels()
    ax2b.legend(lines1+lines2, labels1+labels2, loc='lower right', fontsize=8)

    # 子图3:混淆矩阵差异热图(Aug - Base)
    ax3.set_title("Confusion Matrix Difference (Aug - Base)", fontsize=12, fontweight='bold')
    diff = cm_a - cm_b
    im = ax3.imshow(diff, cmap='plasma')
    plt.colorbar(im, ax=ax3, fraction=0.046, pad=0.04)
    ax3.set_xticks([0,1,2]); ax3.set_yticks([0,1,2])
    ax3.set_xticklabels(['C0','C1','C2']); ax3.set_yticklabels(['C0','C1','C2'])
    # 标注数值
    for i in range(3):
        for j in range(3):
            ax3.text(j, i, f"{diff[i,j]}", ha='center', va='center', color='white', fontsize=11, fontweight='bold')
    ax3.set_xlabel("Predicted"); ax3.set_ylabel("True")
    ax3.grid(False)

    # 子图4:ROC曲线(每类+micro/macro),Baseline vs Augmented
    ax4.set_title("ROC Curves: Baseline vs Augmented", fontsize=12, fontweight='bold')
    cls_colors = ['#ff4444''#33b5e5''#00c851']
    for i in range(3):
        ax4.plot(fpr_b[i], tpr_b[i], color=cls_colors[i], lw=1.6, ls='--', label=f'Base C{i} AUC={auc_b[i]:.3f}')
        ax4.plot(fpr_a[i], tpr_a[i], color=cls_colors[i], lw=2.2, label=f'Aug C{i} AUC={auc_a[i]:.3f}')
    # micro & macro
    ax4.plot(fpr_b["micro"], tpr_b["micro"], color='#aa00ff', lw=1.6, ls='--', label=f'Base micro AUC={auc_b["micro"]:.3f}')
    ax4.plot(fpr_a["micro"], tpr_a["micro"], color='#aa00ff', lw=2.2, label=f'Aug micro AUC={auc_a["micro"]:.3f}')
    ax4.plot([0,1],[0,1], color='gray', lw=1.0, ls=':')
    ax4.set_xlabel("False Positive Rate"); ax4.set_ylabel("True Positive Rate")
    ax4.legend(loc='lower right', fontsize=8)
    ax4.grid(True, alpha=0.3)

    plt.tight_layout()
    return plt.gcf()

if __name__ == "__main__":
    main()

t-SNE特征空间可视化:

在测试集上,分别取基线模型与增强模型的倒数第二层特征,用t-SNE降到二维,并分别用三种鲜艳颜色表示三类。三角形点表示基线,圆形点表示增强模型。

  • 增强模型的同类聚类更紧凑、不同类之间分离更明显,说明数据增强帮助模型学到更稳定的特征表示;
  • 某些区域可能出现基线模型与增强模型特征的混杂,表明增强改善有限,或某类在测试域的分布漂移较大。

训练与验证曲线:

在同一子图中使用双轴展示损失与准确率。包含基线与增强两条曲线组:训练损失/验证损失、训练准确率/验证准确率。

  • 增强模型通常训练损失下降更慢(因为扰动更大),但验证准确率更高或者更稳定;
  • 基线模型可能训练准确率较快上升但验证准确率波动更大,提示过拟合。

混淆矩阵差异热图:

显示增强模型与基线模型在测试集混淆矩阵的差值(增强减基线),采用“plasma”鲜艳色系。数值为计数差异。

  • 对角线元素若为正值,说明增强模型在该类上的正确预测更高;
  • 非对角元素若为负值,说明增强在某些错误类型上减少了误判。

ROC曲线:

显示每类(One-vs-Rest)的ROC曲线,分别对基线与增强进行对比;同时展示micro平均曲线。颜色鲜艳,虚线代表基线,实线代表增强。

  • 增强的AUC应高于基线,特别是在分布漂移较强的测试域中;
  • micro曲线提供整体判别能力的度量,增强若提升明显说明对所有类别都有所改善。

整体而言,在该数据集与测试域设定下,强数据增强(几何+颜色)配合Mixup通常会带来更好的泛化与鲁棒性;

特征空间更分离、混淆矩阵对角线增强、ROC曲线AUC提升,是增强有效性的直观证据;

同时需警惕增强过强对标签保持性的破坏,保持增强强度、概率与任务结构的匹配。

总结

总的来说,数据增强是机器学习预处理中的关键技术,不仅是工程“技巧”,更可被严谨地理解为Vicinal Risk Minimization、群不变性约束与一致性正则化的组合。

从理论角度,它通过邻域分布近似真实风险,从优化角度,它使损失地形更平滑,减少不必要的振荡与过拟合。

从鲁棒性角度,它提高了模型对合理扰动与分布漂移的适应力。

最后

最近准备了16大块的内容,124个算法问题的总结,完整的机器学习小册,免费领取~
领取:备注「算法小册」即可~
扫码如有问题,记得添加微信号:xiaobai_ml012

【声明】内容源于网络
0
0
机器学习和人工智能AI
让我们一起期待 AI 带给我们的每一场变革!推送最新行业内最新最前沿人工智能技术!
内容 381
粉丝 0
机器学习和人工智能AI 让我们一起期待 AI 带给我们的每一场变革!推送最新行业内最新最前沿人工智能技术!
总阅读5.0k
粉丝0
内容381