卷积神经网络的引入4 —— 局部扰动与空间结构破坏下的鲁棒性验证

在前三篇文章中,我们依次验证了:

  1. CNN 对平移等空间扰动具有天然优势
  2. 在低维灰度图(Fashion-MNIST)上,CNN 与 MLP 差距有限
  3. 在中等复杂度数据集(CIFAR-10)上,差距迅速拉大

到这里,一个重要问题浮现:

CNN 的优势到底来自“更大的数据集”还是来自“图像的空间结构”?
换句话说:是否即便不换更大的数据集,只要破坏空间结构,MLP 就会更吃亏?

本篇将从这一关键视角展开实验。


一、实验目标

本篇希望进一步回答:

🧪 1. 如果我们“破坏图像的局部结构”,MLP 与 CNN 谁更稳健?

🧪 2. 当图像遭遇“局部遮挡”“随机噪声”“随机擦除”等扰动,CNN 是否仍能保持较强泛化能力?

🧪 3. 既然 MLP 完全依赖全局平铺输入,空间破坏是否会对 MLP 造成毁灭性影响?


二、实验策略(不再更换数据集,而是更换扰动方式)

我们继续使用 CIFAR-10 —— 数据集本身不变。

但重点修改“输入图像”,通过注入不同类别的空间扰动,让模型面临真正的对抗性挑战。

本文采用一类典型扰动:

🔶 1. Random Erasing(随机擦除局部区域)

模拟遮挡:

  • 随机挖掉图片中的一小块(如 16×16)
  • 广泛用于训练 CNN 的增强策略

CNN 可通过周边区域补偿
MLP 因完全 flatten,会失去空间结构 → 特征被破坏

三、实验步骤

  1. 使用 CIFAR-10
  2. 训练 MLP 与 CNN(结构与上一章一致)
  3. 每种扰动分别训练 10 epoch,记录:
    • 训练集精度
    • 验证集精度
    • 收敛速度
  4. 使用折线图展示 MLP vs CNN 的精度差异

四、核心实验代码(加入图像扰动增强)

以下为结构化 Demo,方便直接复现实验。

# -*- coding: utf-8 -*-
# 卷积神经网络的引入4 —— 空间扰动下的 MLP 与 CNN 鲁棒性对比实验
# Author: zhchoice
# Date: 2025-11-XX

import torch
import torchvision
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import random

device = 'mps' if torch.backends.mps.is_available() else 'cpu'

# =============================
# 1️⃣ 扰动类型选择(重点)
# =============================
# 可选: none / erasing / gaussian / cutout
AUG_TYPE = 'erasing'

print(f"===> 使用扰动方式:{AUG_TYPE}")

# =============================
# 2️⃣ 定义扰动函数(核心部分)
# =============================

def add_gaussian_noise(img, mean=0.0, std=0.1):
    noise = torch.randn_like(img) * std + mean
    return torch.clamp(img + noise, -1, 1)

def cutout(img, size=16):
    _, h, w = img.size()
    y = random.randint(size, h - size)
    x = random.randint(size, w - size)
    img[:, y-size:y+size, x-size:x+size] = 0
    return img


class CutoutTransform:
    """可在 transforms 中使用的 cutout 封装"""
    def __call__(self, img):
        return cutout(img)


class GaussianNoiseTransform:
    """高斯噪声封装"""
    def __call__(self, img):
        return add_gaussian_noise(img)


# =============================
# 3️⃣ 图像增强 pipeline 设置
# =============================
train_transforms = [
    transforms.ToTensor(),
    transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5)),
]

if AUG_TYPE == 'erasing':
    train_transforms.append(transforms.RandomErasing(p=1.0, scale=(0.1,0.2)))
elif AUG_TYPE == 'gaussian':
    train_transforms.append(GaussianNoiseTransform())
elif AUG_TYPE == 'cutout':
    train_transforms.append(CutoutTransform())

train_transform = transforms.Compose(train_transforms)

test_transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))
])

# =============================
# 4️⃣ CIFAR10 数据加载
# =============================
trainset = datasets.CIFAR10('./data', train=True, download=True, transform=train_transform)
testset = datasets.CIFAR10('./data', train=False, download=True, transform=test_transform)

train_loader = DataLoader(trainset, batch_size=64, shuffle=True)
test_loader = DataLoader(testset, batch_size=256)

# =============================
# 5️⃣ 定义模型(沿用上一章)
# =============================
class MLP(nn.Module):
    def __init__(self, input_dim=32*32*3, hidden=1024):
        super().__init__()
        self.net = nn.Sequential(
            nn.Flatten(),
            nn.Linear(input_dim, hidden),
            nn.ReLU(),
            nn.Linear(hidden, 10)
        )
    def forward(self, x):
        return self.net(x)

class CNN(nn.Module):
    def __init__(self, in_ch=3):
        super().__init__()
        self.net = nn.Sequential(
            nn.Conv2d(in_ch, 32, 3, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(),
            nn.MaxPool2d(2),

            nn.Conv2d(32, 64, 3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.MaxPool2d(2),

            nn.Conv2d(64, 128, 3, padding=1),
            nn.ReLU(),
            nn.AdaptiveAvgPool2d(1),

            nn.Flatten(),
            nn.Linear(128, 10)
        )
    def forward(self, x):
        return self.net(x)

# =============================
# 6️⃣ 训练 & 验证
# =============================
loss_fn = nn.CrossEntropyLoss()

def train_one_epoch(model, loader, opt):
    model.train()
    tot_loss, tot_correct = 0, 0
    for x, y in loader:
        x, y = x.to(device), y.to(device)
        out = model(x)
        loss = loss_fn(out, y)

        opt.zero_grad()
        loss.backward()
        opt.step()

        tot_loss += loss.item()
        tot_correct += (out.argmax(1) == y).sum().item()

    return tot_loss / len(loader), tot_correct / len(loader.dataset)


def evaluate(model, loader):
    model.eval()
    tot_correct = 0
    with torch.no_grad():
        for x, y in loader:
            x, y = x.to(device), y.to(device)
            tot_correct += (model(x).argmax(1) == y).sum().item()
    return tot_correct / len(loader.dataset)

# =============================
# 7️⃣ 实验执行
# =============================
mlp = MLP().to(device)
cnn = CNN().to(device)

opt_mlp = torch.optim.Adam(mlp.parameters(), lr=1e-3)
opt_cnn = torch.optim.Adam(cnn.parameters(), lr=1e-3)

epochs = 10

mlp_train, mlp_test = [], []
cnn_train, cnn_test = [], []

for ep in range(epochs):
    _, acc_m = train_one_epoch(mlp, train_loader, opt_mlp)
    _, acc_c = train_one_epoch(cnn, train_loader, opt_cnn)

    val_m = evaluate(mlp, test_loader)
    val_c = evaluate(cnn, test_loader)

    mlp_train.append(acc_m)
    cnn_train.append(acc_c)
    mlp_test.append(val_m)
    cnn_test.append(val_c)

    print(f"[{ep+1}/{epochs}] MLP val={val_m:.3f} | CNN val={val_c:.3f}")

# =============================
# 8️⃣ 画精度曲线
# =============================
plt.figure(figsize=(10,6))
plt.plot(range(1,epochs+1), mlp_train, 'r--o', label='MLP Train')
plt.plot(range(1,epochs+1), mlp_test, 'r-', label='MLP Val')
plt.plot(range(1,epochs+1), cnn_train, 'b--o', label='CNN Train')
plt.plot(range(1,epochs+1), cnn_test, 'b-', label='CNN Val')

plt.title(f"Accuracy Curve under Perturbation: {AUG_TYPE}")
plt.xlabel("Epoch")
plt.ylabel("Accuracy")
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()

五、训练结果表现

image

从本次 “Random Erasing(随机擦除)” 的实验结果中,我们能够观察到以下几个清晰结论:

  1. CNN 在局部遮挡下的鲁棒性显著优于 MLP

从第 1 个 epoch 起:

CNN 验证集精度:0.426

MLP 验证集精度:0.437

两者相差不大。但随着训练继续进行,CNN 的表现开始快速提升:

最终 CNN val ≈ 0.660

而 MLP val ≈ 0.505

CNN 的验证精度始终保持 10%~15% 的稳定优势,并且在整个训练过程中曲线平滑、稳步上升。

结论:CNN 对于随机擦除造成的局部结构破坏具有天然抗性,而 MLP 曲线提升缓慢且上限更低。

  1. 随机擦除对 MLP 的影响远大于对 CNN 的影响

从曲线可以看到:

MLP 的训练曲线与验证曲线始终存在明显 gap

且验证集精度增长缓慢,后期几乎进入停滞

随机擦除破坏了 MLP flatten 后的全局向量,使模型难以恢复有效特征

而 CNN:

即便被遮挡一部分区域

仍能通过局部卷积核从未被遮挡的区域提取足够信息

验证曲线平滑且稳定上升

这表明:MLP 缺乏空间补偿机制,一旦局部像素被破坏,整体特征都会被扰乱;而 CNN 则具备“局部容错”能力。

  1. CNN 的收敛速度明显快于 MLP

观察前 3 个 epoch:

CNN val:从 0.426 → 0.562 → 0.590

MLP val:从 0.437 → 0.465 → 0.475

CNN 的增长速度更快,曲线也更陡峭。

CNN 在早期就掌握了稳健的局部纹理特征,而 MLP 则需要更长时间才开始学习到有效结构。

  1. 随机擦除反而进一步凸显了 CNN 的优势

与上一章(无扰动)相比:

CNN 在 erasing 下精度略降,但仍维持高稳定性与高上限

MLP 在 erasing 下损失明显加剧,最终验证精度也更低

这说明:

扰动越强,MLP 越脆弱;
扰动越强,CNN 越体现其辨识局部结构的能力。

🎯 最终总结(卷4核心结论)

基于本次实验数据可以明确得出:

CNN 的优势不依赖于更大的数据集,而是来源于“空间结构敏感性 + 局部特征补偿能力”。

随机擦除本质上破坏了:

局部纹理

部分语义区域

MLP 由于 flatten 特性:

完全没有空间意识

局部损坏会扰乱整个输入向量

因此验证精度严重受损

CNN 则可以:

靠周边特征补偿缺失的信息

保持稳定学习

在遮挡环境下仍能正确分类

因此:

在任何涉及图像“遮挡、噪声、局部损坏”的任务中,CNN 都显著优于 MLP。

posted @ 2025-11-26 21:20  方子敬  阅读(2)  评论(0)    收藏  举报