共计 3259 个字符,预计需要花费 9 分钟才能阅读完成。
背景痛点
CIFAR100 作为经典的图像分类数据集,包含 32×32 像素的小尺寸图像,这给模型设计带来了独特挑战:

- 空间信息有限 :32×32 分辨率下,传统 CNN 的局部感受野难以捕获长距离依赖关系
- 类别间相似度高 :100 个细粒度类别(如 20 个超类下的子类)导致类间差异细微
- 数据量瓶颈 :50,000 张训练图像对 Transformer 这类数据饥渴型架构构成挑战
传统 CNN(如 ResNet)在 CIFAR100 上常出现:
1. 高层特征丢失细粒度信息
2. 跨区域特征关联建模能力不足
3. 对小物体旋转 / 尺度变化敏感
技术选型
在 Transformer 变体中,我们选择标准 ViT 而非 Swin Transformer,主要考虑:
- 计算效率 :Swin 的窗口注意力在 32×32 分辨率下优势不明显
- 实现复杂度 :ViT 的全局注意力更适合学术验证
- Patch 尺寸 :实验表明 4 ×4 patches(共 64 个 token)在精度与速度间最佳平衡
关键计算量对比(FLOPs):
| 模型 | Patch 大小 | FLOPs |
|---|---|---|
| ViT-Tiny | 4×4 | 0.5G |
| Swin-Tiny | 4×4 | 1.1G |
| ViT-Small | 8×8 | 1.8G |
实现细节
核心模块代码
import torch
import torch.nn as nn
from einops import rearrange
class PatchEmbedding(nn.Module):
"""将图像分割为 patch 并线性投影"""
def __init__(self, img_size=32, patch_size=4, in_chans=3, embed_dim=192):
super().__init__()
self.proj = nn.Conv2d(in_chans, embed_dim,
kernel_size=patch_size,
stride=patch_size)
self.num_patches = (img_size // patch_size) ** 2
def forward(self, x):
x = self.proj(x) # [B, C, H, W] -> [B, E, H/P, W/P]
x = rearrange(x, 'b e h w -> b (h w) e')
return x
class TransformerBlock(nn.Module):
"""标准 Transformer 编码器块"""
def __init__(self, dim, num_heads, mlp_ratio=4., drop=0.):
super().__init__()
self.norm1 = nn.LayerNorm(dim)
self.attn = nn.MultiheadAttention(dim, num_heads, dropout=drop)
self.norm2 = nn.LayerNorm(dim)
self.mlp = nn.Sequential(nn.Linear(dim, int(dim * mlp_ratio)),
nn.GELU(),
nn.Dropout(drop),
nn.Linear(int(dim * mlp_ratio), dim)
)
def forward(self, x):
# 残差连接 + 层归一化
x = x + self.attn(self.norm1(x), self.norm1(x), self.norm1(x))[0]
x = x + self.mlp(self.norm2(x))
return x
数据增强策略
针对小尺寸图像优化 RandAugment 参数:
from torchvision.transforms import autoaugment, transforms
train_transform = transforms.Compose([transforms.RandomCrop(32, padding=4, padding_mode='reflect'),
transforms.RandomHorizontalFlip(),
autoaugment.RandAugment(
num_ops=2, # 操作次数减少
magnitude=9, # 强度降低
num_magnitude_bins=31,
interpolation=transforms.InterpolationMode.BILINEAR
),
transforms.ToTensor(),
transforms.Normalize((0.5071, 0.4867, 0.4408), (0.2675, 0.2565, 0.2761))
])
标签平滑实现
class LabelSmoothingCrossEntropy(nn.Module):
def __init__(self, epsilon=0.1):
super().__init__()
self.epsilon = epsilon
def forward(self, logits, targets):
log_probs = -torch.log_softmax(logits, dim=-1)
nll_loss = log_probs.gather(dim=-1, index=targets.unsqueeze(1))
smooth_loss = log_probs.mean(dim=-1)
loss = (1 - self.epsilon) * nll_loss + self.epsilon * smooth_loss
return loss.mean()
性能优化
混合精度训练配置
scaler = torch.cuda.amp.GradScaler()
for inputs, targets in train_loader:
optimizer.zero_grad()
with torch.cuda.amp.autocast():
outputs = model(inputs)
loss = criterion(outputs, targets)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
梯度累积技巧
effective_batch_size = 512
accum_steps = effective_batch_size // batch_size
for i, (inputs, targets) in enumerate(train_loader):
with torch.cuda.amp.autocast():
outputs = model(inputs)
loss = criterion(outputs, targets) / accum_steps
scaler.scale(loss).backward()
if (i + 1) % accum_steps == 0:
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()
避坑指南
- 学习率 warmup:
- 错误做法:直接从峰值学习率开始
-
正确实现:线性 warmup 5-10 个 epoch
scheduler = torch.optim.lr_scheduler.SequentialLR( optimizer, schedulers=[torch.optim.lr_scheduler.LinearLR(optimizer, 0.001, 1, total_iters=5), torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs-5) ], milestones=[5] ) -
位置编码陷阱 :
- 问题:直接使用 ImageNet 预训练的位置编码会导致过拟合
- 解决:重新初始化或采用可学习的位置编码
测试结果
| 模型 | Top-1 Acc | Top-5 Acc | 参数量 |
|---|---|---|---|
| ResNet-50 | 68.2% | 89.5% | 25.5M |
| ViT-Tiny | 72.1% | 91.3% | 5.7M |
| ViT-Small | 74.8% | 92.6% | 22.1M |
开放问题
在小样本场景下,如何改进 ViT 的样本效率?可能的思路包括:
– 知识蒸馏(从 CNN 教师模型迁移)
– 自监督预训练(如 MAE、DINO)
– 引入卷积先验(Hybrid 架构)
欢迎在评论区分享你的实践经验!
正文完
发表至: 未分类
近两天内
