哈喽,大家好~
我们在实验中,数据集常常有限、偏倚或噪声较多,模型容易过拟合训练集而在测试时表现下滑。
数据增强通过对训练样本进行一系列标注保持(或可控的标签混合)的变换,模拟同分布内的多样性与合理的扰动,使得模型学习到更稳定的特征表示。
它有四个核心动因:
-
增加有效样本数量:在不增加采集成本的情况下扩大训练覆盖。 -
引入结构性先验:编码平移、旋转、色彩变换等不变性先验,减少模型对无关因素的敏感性。 -
改善优化:通过引入噪声与扰动,使损失地形更平滑,促进更稳健的梯度下降。 -
强化鲁棒性:提高对分布漂移、轻微扰动、遮挡与背景变化的适应能力。
就是说,数据增强既是工程上的技巧,也是统计学习观点下的一种近似风险估计与正则化方法~
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 = [
(255, 80, 80),
(255, 160, 0),
(255, 220, 0),
(0, 200, 120),
(80, 160, 255),
(220, 80, 255)
]
else:
# 测试:偏冷色与对比度更高
palette = [
(80, 200, 255),
(0, 160, 255),
(0, 220, 160),
(40, 240, 200),
(160, 80, 255),
(0, 255, 255)
]
# 根据类别稍作偏好(非严格)
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(-30, 30)
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(-15, 15)
img = img.rotate(angle)
return img
def _generate(self):
s = self.image_size
for i in range(self.n_samples):
# 背景
bg = np.random.randint(0, 40)
img = Image.new("RGB", (s, s), (bg, bg, bg))
draw = ImageDraw.Draw(img)
cls = np.random.randint(0, 3)
# 形状
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.6, 1.5)
arr = np.power(arr, gamma)
arr = np.clip(arr*1.1, 0, 1)
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.4, 0.4, 0.4], std=[0.25, 0.25, 0.25])
])
else:
# 强数据增强
return T.Compose([
T.RandomResizedCrop(image_size, scale=(0.7, 1.0), ratio=(0.9, 1.1)),
T.ColorJitter(brightness=0.4, contrast=0.5, saturation=0.5, hue=0.1),
T.RandomAffine(degrees=30, translate=(0.1, 0.1), scale=(0.9, 1.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.1, 1.5)),
T.ToTensor(),
T.RandomErasing(p=0.3, scale=(0.02, 0.12), ratio=(0.3, 2.5), value='random'),
T.Normalize(mean=[0.4, 0.4, 0.4], std=[0.25, 0.25, 0.25])
])
# 3. 定义模型(CNN)
class SimpleCNN(nn.Module):
def __init__(self, num_classes=3):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 32, 3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(inplace=True),
nn.Conv2d(32, 64, 3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(2), # 32x32
nn.Conv2d(64, 128, 3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.MaxPool2d(2), # 16x16
nn.Conv2d(128, 128, 3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.MaxPool2d(2) # 8x8
)
self.head = nn.Sequential(
nn.Flatten(),
nn.Linear(128*8*8, 256),
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=(14, 10))
ax1 = plt.subplot(2, 2, 1)
ax2 = plt.subplot(2, 2, 2)
ax3 = plt.subplot(2, 2, 3)
ax4 = plt.subplot(2, 2, 4)
# 子图1:t-SNE投影(Baseline vs Augmented)
colors = np.array([
[255/255, 80/255, 80/255],
[80/255, 160/255, 255/255],
[0/255, 200/255, 120/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、群不变性约束与一致性正则化的组合。
从理论角度,它通过邻域分布近似真实风险,从优化角度,它使损失地形更平滑,减少不必要的振荡与过拟合。
从鲁棒性角度,它提高了模型对合理扰动与分布漂移的适应力。
最后

