- Published on
CNN 架构完全指南:从 LeNet 到 EfficientNet、Vision Transformer
- Authors

- Name
- Youngju Kim
- @fjvbn20031
CNN 架构完全指南
卷积神经网络(CNN,Convolutional Neural Network)是计算机视觉革命的核心。从 1998 年 LeNet 的登场,到 2022 年的 ConvNeXt 和 Vision Transformer,CNN 架构以惊人的速度不断演进。本指南将带你理解各主要 CNN 架构的结构性创新,并通过 PyTorch 完整掌握其实现方法。
1. CNN 基础
直观理解卷积运算
卷积(Convolution)是从图像中提取局部模式的运算。较小的滤波器(卷积核)在图像上滑动,生成特征图(Feature Map)。
输入图像 (5x5) 卷积核 (3x3) 输出特征图 (3x3)
1 1 1 0 0 1 0 1 4 3 4
0 1 1 1 0 * 0 1 0 = 2 4 3
0 0 1 1 1 1 0 1 2 3 4
0 0 1 1 0
0 1 1 0 0
在每个位置上,卷积核与图像块对应元素乘积之和,就是输出特征图的值。
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
# 卷积运算可视化
def visualize_convolution():
# 示例图像
image = torch.tensor([[
[1., 1., 1., 0., 0.],
[0., 1., 1., 1., 0.],
[0., 0., 1., 1., 1.],
[0., 0., 1., 1., 0.],
[0., 1., 1., 0., 0.]
]]).unsqueeze(0) # (1, 1, 5, 5)
# 边缘检测卷积核
edge_kernel = torch.tensor([[
[[-1., -1., -1.],
[-1., 8., -1.],
[-1., -1., -1.]]
]]) # (1, 1, 3, 3)
output = F.conv2d(image, edge_kernel, padding=1)
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
axes[0].imshow(image[0, 0].numpy(), cmap='gray')
axes[0].set_title('输入图像')
axes[1].imshow(edge_kernel[0, 0].numpy(), cmap='RdYlBu')
axes[1].set_title('边缘检测卷积核')
axes[2].imshow(output[0, 0].detach().numpy(), cmap='gray')
axes[2].set_title('输出特征图')
plt.tight_layout()
plt.show()
visualize_convolution()
卷积核/滤波器、步幅、填充
import torch
import torch.nn as nn
# 基本 Conv2d 参数
conv = nn.Conv2d(
in_channels=3, # 输入通道数(RGB=3)
out_channels=64, # 输出通道数(滤波器数量)
kernel_size=3, # 卷积核大小(3x3)
stride=1, # 步幅
padding=1, # 填充(same padding)
bias=True
)
# 输出尺寸计算公式
# H_out = floor((H_in + 2*padding - kernel_size) / stride + 1)
# W_out = floor((W_in + 2*padding - kernel_size) / stride + 1)
def calc_output_size(input_size, kernel_size, stride, padding):
return (input_size + 2 * padding - kernel_size) // stride + 1
# 示例
print(calc_output_size(224, 3, 1, 1)) # 224(same padding)
print(calc_output_size(224, 3, 2, 1)) # 112(stride 2 时减半)
print(calc_output_size(224, 7, 2, 3)) # 112(AlexNet 第一层)
# 参数量计算
# Conv2d: (kernel_h * kernel_w * in_channels + 1) * out_channels
params = (3 * 3 * 3 + 1) * 64
print(f"Conv(3->64, 3x3) 参数量: {params:,}") # 1,792
池化(Max、Average、Global)
import torch
import torch.nn as nn
import torch.nn.functional as F
x = torch.randn(1, 64, 28, 28)
# Max Pooling
max_pool = nn.MaxPool2d(kernel_size=2, stride=2)
out_max = max_pool(x) # (1, 64, 14, 14)
# Average Pooling
avg_pool = nn.AvgPool2d(kernel_size=2, stride=2)
out_avg = avg_pool(x) # (1, 64, 14, 14)
# Global Average Pooling (GAP) - 将空间维度压缩为 1x1
gap = nn.AdaptiveAvgPool2d(1)
out_gap = gap(x) # (1, 64, 1, 1)
out_gap_flat = out_gap.flatten(1) # (1, 64)
# Adaptive Pooling - 指定输出尺寸
adaptive = nn.AdaptiveAvgPool2d((7, 7))
out_adaptive = adaptive(x) # (1, 64, 7, 7) - 任意输入尺寸都可以
print(f"输入: {x.shape}")
print(f"MaxPool: {out_max.shape}")
print(f"GAP: {out_gap_flat.shape}")
感受野(Receptive Field)计算
def calculate_receptive_field(layers):
"""
计算每一层的感受野
layers: [(kernel_size, stride, dilation), ...]
"""
rf = 1
jump = 1
for k, s, d in layers:
effective_k = d * (k - 1) + 1
rf = rf + (effective_k - 1) * jump
jump = jump * s
return rf
# VGG 风格(只使用 3x3 卷积)
vgg_layers = [
(3, 1, 1), # conv1
(3, 1, 1), # conv2
(2, 2, 1), # pool
(3, 1, 1), # conv3
(3, 1, 1), # conv4
(2, 2, 1), # pool
]
rf = calculate_receptive_field(vgg_layers)
print(f"VGG 6 层后的感受野: {rf}x{rf} 像素")
# 参考:两个 3x3 = 一个 5x5 的感受野
# 但参数量为 2*(9*C^2) vs 25*C^2 → 两个 3x3 更高效
2. CNN 发展史
LeNet-5(1998,LeCun)— 最初的实用 CNN
LeNet-5 是 Yann LeCun 于 1998 年开发的最初的实用 CNN,被用于手写数字识别(MNIST)。
结构:Input(32x32) → C1(conv, 6@28x28) → S2(pool, 6@14x14) → C3(conv, 16@10x10) → S4(pool, 16@5x5) → C5(conv, 120@1x1) → F6(fc, 84) → Output(10)
import torch
import torch.nn as nn
class LeNet5(nn.Module):
"""LeNet-5 实现(在原版基础上加入 ReLU)"""
def __init__(self, num_classes=10):
super(LeNet5, self).__init__()
self.features = nn.Sequential(
# C1: 1@32x32 -> 6@28x28
nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=0),
nn.Tanh(),
# S2: 6@28x28 -> 6@14x14
nn.AvgPool2d(kernel_size=2, stride=2),
# C3: 6@14x14 -> 16@10x10
nn.Conv2d(6, 16, kernel_size=5, stride=1, padding=0),
nn.Tanh(),
# S4: 16@10x10 -> 16@5x5
nn.AvgPool2d(kernel_size=2, stride=2),
# C5: 16@5x5 -> 120@1x1
nn.Conv2d(16, 120, kernel_size=5, stride=1, padding=0),
nn.Tanh(),
)
self.classifier = nn.Sequential(
# F6: 120 -> 84
nn.Linear(120, 84),
nn.Tanh(),
# Output: 84 -> num_classes
nn.Linear(84, num_classes)
)
def forward(self, x):
x = self.features(x)
x = x.flatten(1)
x = self.classifier(x)
return x
# 测试
model = LeNet5(num_classes=10)
x = torch.randn(4, 1, 32, 32)
out = model(x)
print(f"LeNet-5 输出: {out.shape}") # (4, 10)
# 参数量
total_params = sum(p.numel() for p in model.parameters())
print(f"LeNet-5 总参数量: {total_params:,}") # ~60,000
AlexNet(2012,Krizhevsky)— 深度学习复兴的起点
AlexNet 在 2012 年 ImageNet 竞赛中取得 top-5 错误率 15.3% 的成绩,大幅超越此前最佳成绩(26.2%),开启了深度学习时代。
核心创新:
- 引入 ReLU 激活函数(训练速度比 Tanh 快 6 倍)
- Dropout(0.5)防止过拟合
- 数据增强(裁剪、翻转)
- 局部响应归一化(LRN)
- 双 GPU 并行训练
import torch
import torch.nn as nn
class AlexNet(nn.Module):
"""AlexNet 实现"""
def __init__(self, num_classes=1000):
super(AlexNet, self).__init__()
self.features = nn.Sequential(
# Layer 1: 3@224x224 -> 96@55x55
nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=2),
nn.ReLU(inplace=True),
nn.LocalResponseNorm(size=5, alpha=1e-4, beta=0.75, k=2),
nn.MaxPool2d(kernel_size=3, stride=2), # 96@27x27
# Layer 2: 96@27x27 -> 256@27x27
nn.Conv2d(96, 256, kernel_size=5, stride=1, padding=2),
nn.ReLU(inplace=True),
nn.LocalResponseNorm(size=5, alpha=1e-4, beta=0.75, k=2),
nn.MaxPool2d(kernel_size=3, stride=2), # 256@13x13
# Layer 3: 256@13x13 -> 384@13x13
nn.Conv2d(256, 384, kernel_size=3, stride=1, padding=1),
nn.ReLU(inplace=True),
# Layer 4: 384@13x13 -> 384@13x13
nn.Conv2d(384, 384, kernel_size=3, stride=1, padding=1),
nn.ReLU(inplace=True),
# Layer 5: 384@13x13 -> 256@13x13
nn.Conv2d(384, 256, kernel_size=3, stride=1, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2), # 256@6x6
)
self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
self.classifier = nn.Sequential(
nn.Dropout(p=0.5),
nn.Linear(256 * 6 * 6, 4096),
nn.ReLU(inplace=True),
nn.Dropout(p=0.5),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Linear(4096, num_classes)
)
def forward(self, x):
x = self.features(x)
x = self.avgpool(x)
x = x.flatten(1)
x = self.classifier(x)
return x
model = AlexNet(num_classes=1000)
x = torch.randn(4, 3, 224, 224)
out = model(x)
print(f"AlexNet 输出: {out.shape}") # (4, 1000)
total_params = sum(p.numel() for p in model.parameters())
print(f"AlexNet 总参数量: {total_params:,}") # ~61M
VGGNet(2014,Simonyan)— 深度的力量
VGGNet 由牛津大学 Visual Geometry Group 开发。其核心洞见是所有卷积层只使用 3x3 卷积核,从而将深度大幅增加。
为什么是 3x3?
- 两个 3x3 = 一个 5x5 的感受野(参数量为 2×9C² vs 25C²,节省 28%)
- 三个 3x3 = 一个 7x7 的感受野(参数量为 3×9C² vs 49C²,节省 45%)
- 更多非线性变换,提升表达能力
import torch
import torch.nn as nn
from typing import List, Union
class VGG(nn.Module):
"""VGG 通用实现"""
def __init__(self, features: nn.Module, num_classes: int = 1000, dropout: float = 0.5):
super(VGG, self).__init__()
self.features = features
self.avgpool = nn.AdaptiveAvgPool2d((7, 7))
self.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(inplace=True),
nn.Dropout(p=dropout),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Dropout(p=dropout),
nn.Linear(4096, num_classes)
)
self._initialize_weights()
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.features(x)
x = self.avgpool(x)
x = x.flatten(1)
x = self.classifier(x)
return x
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0)
def make_layers(cfg: List[Union[str, int]], batch_norm: bool = False) -> nn.Sequential:
"""根据配置列表生成 VGG 层"""
layers: List[nn.Module] = []
in_channels = 3
for v in cfg:
if v == 'M':
layers.append(nn.MaxPool2d(kernel_size=2, stride=2))
else:
v = int(v)
conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
if batch_norm:
layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
else:
layers += [conv2d, nn.ReLU(inplace=True)]
in_channels = v
return nn.Sequential(*layers)
# VGG 配置
cfgs = {
'vgg11': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'vgg13': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'vgg16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
'vgg19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}
def vgg16(num_classes=1000):
return VGG(make_layers(cfgs['vgg16'], batch_norm=True), num_classes=num_classes)
def vgg19(num_classes=1000):
return VGG(make_layers(cfgs['vgg19'], batch_norm=True), num_classes=num_classes)
# 测试
model_vgg16 = vgg16()
x = torch.randn(2, 3, 224, 224)
out = model_vgg16(x)
print(f"VGG-16 输出: {out.shape}")
params_vgg16 = sum(p.numel() for p in model_vgg16.parameters())
print(f"VGG-16 参数量: {params_vgg16:,}") # ~138M
GoogLeNet/Inception(2014,Szegedy)— 并行多尺度处理
Inception 模块的核心在于,在同一层中并行处理不同大小的卷积核(1x1、3x3、5x5),从而同时提取多种尺度的特征。
import torch
import torch.nn as nn
import torch.nn.functional as F
class InceptionModule(nn.Module):
"""基本 Inception 模块"""
def __init__(self, in_channels, n1x1, n3x3_reduce, n3x3,
n5x5_reduce, n5x5, pool_proj):
super(InceptionModule, self).__init__()
# 1x1 分支
self.branch1 = nn.Sequential(
nn.Conv2d(in_channels, n1x1, kernel_size=1),
nn.BatchNorm2d(n1x1),
nn.ReLU(inplace=True)
)
# 3x3 分支(1x1 瓶颈 + 3x3)
self.branch2 = nn.Sequential(
nn.Conv2d(in_channels, n3x3_reduce, kernel_size=1),
nn.BatchNorm2d(n3x3_reduce),
nn.ReLU(inplace=True),
nn.Conv2d(n3x3_reduce, n3x3, kernel_size=3, padding=1),
nn.BatchNorm2d(n3x3),
nn.ReLU(inplace=True)
)
# 5x5 分支(1x1 瓶颈 + 5x5)
self.branch3 = nn.Sequential(
nn.Conv2d(in_channels, n5x5_reduce, kernel_size=1),
nn.BatchNorm2d(n5x5_reduce),
nn.ReLU(inplace=True),
nn.Conv2d(n5x5_reduce, n5x5, kernel_size=5, padding=2),
nn.BatchNorm2d(n5x5),
nn.ReLU(inplace=True)
)
# Pool 分支(MaxPool + 1x1)
self.branch4 = nn.Sequential(
nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
nn.Conv2d(in_channels, pool_proj, kernel_size=1),
nn.BatchNorm2d(pool_proj),
nn.ReLU(inplace=True)
)
def forward(self, x):
b1 = self.branch1(x)
b2 = self.branch2(x)
b3 = self.branch3(x)
b4 = self.branch4(x)
# 按通道维度拼接
return torch.cat([b1, b2, b3, b4], dim=1)
# 测试
module = InceptionModule(192, 64, 96, 128, 16, 32, 32)
x = torch.randn(2, 192, 28, 28)
out = module(x)
print(f"Inception 输出: {out.shape}") # (2, 256, 28, 28) = 64+128+32+32
ResNet(2015,He)— 用残差连接解决梯度消失
ResNet 是 He Kaiming 在 2015 年发表的创新架构。通过残差连接(Skip Connection),梯度得以传递到更深的层,使得训练 152 层这样极深的网络成为可能。
核心思想:H(x) = F(x) + x
层学习的不是直接的 H(x),而是残差 F(x) = H(x) - x。当最优函数接近恒等函数时,把残差逼近为 0 会更容易。
import torch
import torch.nn as nn
from typing import Optional, Type, List
class BasicBlock(nn.Module):
"""用于 ResNet-18/34 的基本块"""
expansion = 1
def __init__(self, in_channels: int, out_channels: int,
stride: int = 1, downsample: Optional[nn.Module] = None):
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3,
stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3,
stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
self.downsample = downsample # 用于匹配维度
def forward(self, x: torch.Tensor) -> torch.Tensor:
identity = x # 为残差连接保存输入
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
# 维度不一致时做投影
if self.downsample is not None:
identity = self.downsample(x)
out += identity # 核心:残差连接
out = self.relu(out)
return out
class Bottleneck(nn.Module):
"""用于 ResNet-50/101/152 的瓶颈块"""
expansion = 4
def __init__(self, in_channels: int, out_channels: int,
stride: int = 1, downsample: Optional[nn.Module] = None):
super(Bottleneck, self).__init__()
# 1x1 conv(通道压缩)
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
# 3x3 conv(空间处理)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3,
stride=stride, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
# 1x1 conv(通道扩展:out_channels * 4)
self.conv3 = nn.Conv2d(out_channels, out_channels * self.expansion,
kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(out_channels * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
def forward(self, x: torch.Tensor) -> torch.Tensor:
identity = x
out = self.relu(self.bn1(self.conv1(x)))
out = self.relu(self.bn2(self.conv2(out)))
out = self.bn3(self.conv3(out))
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
class ResNet(nn.Module):
"""完整的 ResNet 实现"""
def __init__(self, block: Type[nn.Module], layers: List[int],
num_classes: int = 1000):
super(ResNet, self).__init__()
self.in_channels = 64
# 主干(Stem)
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 4 个 Stage
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
# 分类器
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)
self._initialize_weights()
def _make_layer(self, block, out_channels, blocks, stride=1):
downsample = None
if stride != 1 or self.in_channels != out_channels * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.in_channels, out_channels * block.expansion,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels * block.expansion)
)
layers = [block(self.in_channels, out_channels, stride, downsample)]
self.in_channels = out_channels * block.expansion
for _ in range(1, blocks):
layers.append(block(self.in_channels, out_channels))
return nn.Sequential(*layers)
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# 主干
x = self.maxpool(self.relu(self.bn1(self.conv1(x))))
# 4 个 Stage
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
# 分类
x = self.avgpool(x)
x = x.flatten(1)
x = self.fc(x)
return x
def resnet18(num_classes=1000):
return ResNet(BasicBlock, [2, 2, 2, 2], num_classes)
def resnet34(num_classes=1000):
return ResNet(BasicBlock, [3, 4, 6, 3], num_classes)
def resnet50(num_classes=1000):
return ResNet(Bottleneck, [3, 4, 6, 3], num_classes)
def resnet101(num_classes=1000):
return ResNet(Bottleneck, [3, 4, 23, 3], num_classes)
def resnet152(num_classes=1000):
return ResNet(Bottleneck, [3, 8, 36, 3], num_classes)
# 测试
for name, model_fn in [('ResNet-18', resnet18), ('ResNet-50', resnet50)]:
model = model_fn()
x = torch.randn(2, 3, 224, 224)
out = model(x)
params = sum(p.numel() for p in model.parameters())
print(f"{name}: 输出={out.shape}, 参数量={params:,}")
DenseNet(2017,Huang)— 密集连接
DenseNet 将每一层都与前面所有层连接起来。当有 L 个层时,ResNet 是 L 个连接,而 DenseNet 会产生 L(L+1)/2 个连接。
import torch
import torch.nn as nn
import torch.nn.functional as F
class DenseLayer(nn.Module):
"""DenseNet 的单个层"""
def __init__(self, in_channels, growth_rate, bn_size=4, drop_rate=0.0):
super(DenseLayer, self).__init__()
# Bottleneck:用 1x1 conv 限制通道数
self.norm1 = nn.BatchNorm2d(in_channels)
self.relu1 = nn.ReLU(inplace=True)
self.conv1 = nn.Conv2d(in_channels, bn_size * growth_rate,
kernel_size=1, bias=False)
# 3x3 conv
self.norm2 = nn.BatchNorm2d(bn_size * growth_rate)
self.relu2 = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(bn_size * growth_rate, growth_rate,
kernel_size=3, padding=1, bias=False)
self.drop_rate = drop_rate
def forward(self, x):
if isinstance(x, torch.Tensor):
prev_features = [x]
else:
prev_features = x
# 拼接所有先前的特征图
concat_input = torch.cat(prev_features, dim=1)
out = self.conv1(self.relu1(self.norm1(concat_input)))
out = self.conv2(self.relu2(self.norm2(out)))
if self.drop_rate > 0:
out = F.dropout(out, p=self.drop_rate, training=self.training)
return out
class DenseBlock(nn.Module):
"""由多个 DenseLayer 组成的 Dense Block"""
def __init__(self, num_layers, in_channels, growth_rate, bn_size=4, drop_rate=0.0):
super(DenseBlock, self).__init__()
self.layers = nn.ModuleList()
for i in range(num_layers):
layer = DenseLayer(
in_channels + i * growth_rate,
growth_rate, bn_size, drop_rate
)
self.layers.append(layer)
def forward(self, x):
features = [x]
for layer in self.layers:
new_feat = layer(features)
features.append(new_feat)
return torch.cat(features, dim=1)
class TransitionLayer(nn.Module):
"""Dense Block 之间的过渡层(调整通道数 + 下采样)"""
def __init__(self, in_channels, out_channels):
super(TransitionLayer, self).__init__()
self.norm = nn.BatchNorm2d(in_channels)
self.relu = nn.ReLU(inplace=True)
self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)
self.pool = nn.AvgPool2d(kernel_size=2, stride=2)
def forward(self, x):
return self.pool(self.conv(self.relu(self.norm(x))))
class DenseNet121(nn.Module):
"""DenseNet-121 实现"""
def __init__(self, num_classes=1000, growth_rate=32, num_init_features=64):
super(DenseNet121, self).__init__()
# DenseNet-121 配置:6, 12, 24, 16 层
block_config = [6, 12, 24, 16]
compression = 0.5
# 主干
self.features = nn.Sequential(
nn.Conv2d(3, num_init_features, kernel_size=7, stride=2, padding=3, bias=False),
nn.BatchNorm2d(num_init_features),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
# Dense Block + Transition Layer
num_features = num_init_features
for i, num_layers in enumerate(block_config):
block = DenseBlock(num_layers, num_features, growth_rate)
self.features.add_module(f'denseblock{i+1}', block)
num_features = num_features + num_layers * growth_rate
if i != len(block_config) - 1: # 排除最后一个 block
out_features = int(num_features * compression)
transition = TransitionLayer(num_features, out_features)
self.features.add_module(f'transition{i+1}', transition)
num_features = out_features
# 最终 BN
self.features.add_module('norm_final', nn.BatchNorm2d(num_features))
self.features.add_module('relu_final', nn.ReLU(inplace=True))
# 分类器
self.classifier = nn.Linear(num_features, num_classes)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
def forward(self, x):
features = self.features(x)
out = self.avgpool(features)
out = out.flatten(1)
out = self.classifier(out)
return out
# 测试
model = DenseNet121(num_classes=1000)
x = torch.randn(2, 3, 224, 224)
out = model(x)
params = sum(p.numel() for p in model.parameters())
print(f"DenseNet-121 输出: {out.shape}, 参数量: {params:,}")
MobileNet(2017)— 轻量化革命
MobileNet 引入了 Depthwise Separable Convolution,实现了可在移动/边缘设备上运行的轻量级 CNN。
import torch
import torch.nn as nn
class DepthwiseSeparableConv(nn.Module):
"""Depthwise Separable Convolution"""
def __init__(self, in_channels, out_channels, stride=1):
super(DepthwiseSeparableConv, self).__init__()
# Depthwise:每个输入通道独立处理
self.depthwise = nn.Sequential(
nn.Conv2d(in_channels, in_channels, kernel_size=3,
stride=stride, padding=1, groups=in_channels, bias=False),
nn.BatchNorm2d(in_channels),
nn.ReLU6(inplace=True)
)
# Pointwise:用 1x1 conv 组合通道
self.pointwise = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False),
nn.BatchNorm2d(out_channels),
nn.ReLU6(inplace=True)
)
def forward(self, x):
x = self.depthwise(x)
x = self.pointwise(x)
return x
class InvertedResidual(nn.Module):
"""MobileNetV2 的反向残差块"""
def __init__(self, in_channels, out_channels, stride, expand_ratio):
super(InvertedResidual, self).__init__()
self.stride = stride
hidden_dim = int(in_channels * expand_ratio)
self.use_res_connect = (stride == 1 and in_channels == out_channels)
layers = []
if expand_ratio != 1:
# Expand
layers += [
nn.Conv2d(in_channels, hidden_dim, 1, bias=False),
nn.BatchNorm2d(hidden_dim),
nn.ReLU6(inplace=True)
]
layers += [
# Depthwise
nn.Conv2d(hidden_dim, hidden_dim, 3, stride=stride,
padding=1, groups=hidden_dim, bias=False),
nn.BatchNorm2d(hidden_dim),
nn.ReLU6(inplace=True),
# Project
nn.Conv2d(hidden_dim, out_channels, 1, bias=False),
nn.BatchNorm2d(out_channels)
]
self.conv = nn.Sequential(*layers)
def forward(self, x):
if self.use_res_connect:
return x + self.conv(x)
else:
return self.conv(x)
class MobileNetV2(nn.Module):
"""MobileNetV2 实现"""
def __init__(self, num_classes=1000, width_mult=1.0):
super(MobileNetV2, self).__init__()
# t=expand_ratio, c=out_channels, n=num_layers, s=stride
inverted_residual_settings = [
[1, 16, 1, 1],
[6, 24, 2, 2],
[6, 32, 3, 2],
[6, 64, 4, 2],
[6, 96, 3, 1],
[6, 160, 3, 2],
[6, 320, 1, 1],
]
input_channel = int(32 * width_mult)
last_channel = int(1280 * max(1.0, width_mult))
features = [
nn.Sequential(
nn.Conv2d(3, input_channel, 3, stride=2, padding=1, bias=False),
nn.BatchNorm2d(input_channel),
nn.ReLU6(inplace=True)
)
]
for t, c, n, s in inverted_residual_settings:
output_channel = int(c * width_mult)
for i in range(n):
stride = s if i == 0 else 1
features.append(
InvertedResidual(input_channel, output_channel, stride, expand_ratio=t)
)
input_channel = output_channel
features.append(nn.Sequential(
nn.Conv2d(input_channel, last_channel, 1, bias=False),
nn.BatchNorm2d(last_channel),
nn.ReLU6(inplace=True)
))
self.features = nn.Sequential(*features)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.classifier = nn.Sequential(
nn.Dropout(0.2),
nn.Linear(last_channel, num_classes)
)
def forward(self, x):
x = self.features(x)
x = self.avgpool(x)
x = x.flatten(1)
x = self.classifier(x)
return x
# 参数量对比
standard_conv_params = 3 * 3 * 512 * 512 # 标准卷积
dw_sep_params = (3 * 3 * 512) + (512 * 512) # Depthwise Separable
print(f"标准卷积: {standard_conv_params:,}")
print(f"Depthwise Separable: {dw_sep_params:,}")
print(f"节省比例: {(1 - dw_sep_params/standard_conv_params):.1%}")
EfficientNet(2019,Tan)— 复合缩放
EfficientNet 提出了同时对宽度(Width)、深度(Depth)、分辨率(Resolution)进行复合缩放的方法。
import torch
import torch.nn as nn
import math
class MBConvBlock(nn.Module):
"""EfficientNet 的 MBConv 块(基于 MobileNetV2)"""
def __init__(self, in_channels, out_channels, kernel_size,
stride, expand_ratio, se_ratio=0.25):
super(MBConvBlock, self).__init__()
self.stride = stride
self.use_res = (stride == 1 and in_channels == out_channels)
hidden_dim = in_channels * expand_ratio
layers = []
if expand_ratio != 1:
layers += [
nn.Conv2d(in_channels, hidden_dim, 1, bias=False),
nn.BatchNorm2d(hidden_dim, momentum=0.01, eps=1e-3),
nn.SiLU()
]
# Depthwise
layers += [
nn.Conv2d(hidden_dim, hidden_dim, kernel_size, stride=stride,
padding=kernel_size//2, groups=hidden_dim, bias=False),
nn.BatchNorm2d(hidden_dim, momentum=0.01, eps=1e-3),
nn.SiLU()
]
# Squeeze and Excitation
se_channels = max(1, int(in_channels * se_ratio))
layers += [
# Squeeze
nn.AdaptiveAvgPool2d(1),
]
self.se_reduce = nn.Conv2d(hidden_dim, se_channels, 1)
self.se_expand = nn.Conv2d(se_channels, hidden_dim, 1)
self.se_act = nn.SiLU()
# Projection
self.project = nn.Sequential(
nn.Conv2d(hidden_dim, out_channels, 1, bias=False),
nn.BatchNorm2d(out_channels, momentum=0.01, eps=1e-3)
)
self.conv = nn.Sequential(*layers[:len(layers)]) # 排除 SE
self._hidden_dim = hidden_dim
def forward(self, x):
identity = x
out = x
# Expand + Depthwise
for layer in self.conv:
if isinstance(layer, nn.AdaptiveAvgPool2d):
break
out = layer(out)
# SE(Squeeze-and-Excitation)
se = out.mean([2, 3], keepdim=True)
se = self.se_act(self.se_reduce(se))
se = torch.sigmoid(self.se_expand(se))
out = out * se
# Project
out = self.project(out)
if self.use_res:
out = out + identity
return out
# EfficientNet 缩放系数
efficientnet_params = {
'b0': (1.0, 1.0, 224, 0.2),
'b1': (1.0, 1.1, 240, 0.2),
'b2': (1.1, 1.2, 260, 0.3),
'b3': (1.2, 1.4, 300, 0.3),
'b4': (1.4, 1.8, 380, 0.4),
'b5': (1.6, 2.2, 456, 0.4),
'b6': (1.8, 2.6, 528, 0.5),
'b7': (2.0, 3.1, 600, 0.5),
}
# (width_coeff, depth_coeff, resolution, dropout_rate)
print("EfficientNet 缩放参数:")
for version, (w, d, r, drop) in efficientnet_params.items():
print(f" B{version[1]}: 宽度={w:.1f}, 深度={d:.1f}, 分辨率={r}, Dropout={drop}")
ConvNeXt(2022,Liu)— 现代化 ConvNet
ConvNeXt 将 ViT 的设计原则应用于 CNN,是一个达到了与 Transformer 相当性能的"modernized" ConvNet。
import torch
import torch.nn as nn
class ConvNeXtBlock(nn.Module):
"""ConvNeXt 块"""
def __init__(self, dim, layer_scale_init_value=1e-6):
super(ConvNeXtBlock, self).__init__()
# Depthwise Conv(较大的 7x7 卷积核)
self.dwconv = nn.Conv2d(dim, dim, kernel_size=7, padding=3, groups=dim)
# LayerNorm
self.norm = nn.LayerNorm(dim, eps=1e-6)
# Inverted Bottleneck(通道扩展 4 倍)
self.pwconv1 = nn.Linear(dim, 4 * dim)
self.act = nn.GELU()
self.pwconv2 = nn.Linear(4 * dim, dim)
# Layer Scale
self.gamma = nn.Parameter(
layer_scale_init_value * torch.ones(dim),
requires_grad=True
) if layer_scale_init_value > 0 else None
def forward(self, x):
identity = x
x = self.dwconv(x)
# (N, C, H, W) -> (N, H, W, C),用于 LayerNorm
x = x.permute(0, 2, 3, 1)
x = self.norm(x)
x = self.pwconv1(x)
x = self.act(x)
x = self.pwconv2(x)
if self.gamma is not None:
x = self.gamma * x
# (N, H, W, C) -> (N, C, H, W)
x = x.permute(0, 3, 1, 2)
return identity + x
class ConvNeXt(nn.Module):
"""ConvNeXt 实现"""
def __init__(self, in_channels=3, num_classes=1000,
depths=[3, 3, 9, 3], dims=[96, 192, 384, 768]):
super(ConvNeXt, self).__init__()
# 主干(Patchify)
self.downsample_layers = nn.ModuleList()
stem = nn.Sequential(
nn.Conv2d(in_channels, dims[0], kernel_size=4, stride=4),
nn.LayerNorm(dims[0], eps=1e-6) if False else # 未使用
nn.GroupNorm(1, dims[0]) # 按通道归一化
)
# 实际上更简单地实现
self.stem = nn.Sequential(
nn.Conv2d(in_channels, dims[0], kernel_size=4, stride=4),
)
# 4 个 Stage
self.stages = nn.ModuleList()
self.downsamples = nn.ModuleList()
for i in range(4):
if i > 0:
self.downsamples.append(nn.Sequential(
nn.GroupNorm(1, dims[i-1]),
nn.Conv2d(dims[i-1], dims[i], kernel_size=2, stride=2)
))
else:
self.downsamples.append(nn.Identity())
stage = nn.Sequential(
*[ConvNeXtBlock(dims[i]) for _ in range(depths[i])]
)
self.stages.append(stage)
self.norm = nn.LayerNorm(dims[-1], eps=1e-6)
self.head = nn.Linear(dims[-1], num_classes)
def forward(self, x):
x = self.stem(x)
for i, (ds, stage) in enumerate(zip(self.downsamples, self.stages)):
if i > 0:
x = ds(x)
x = stage(x)
x = x.mean([-2, -1]) # Global Average Pooling
x = self.norm(x)
x = self.head(x)
return x
# ConvNeXt-T: depths=[3,3,9,3], dims=[96,192,384,768]
model = ConvNeXt(num_classes=1000, depths=[3, 3, 9, 3], dims=[96, 192, 384, 768])
x = torch.randn(2, 3, 224, 224)
out = model(x)
params = sum(p.numel() for p in model.parameters())
print(f"ConvNeXt-T 输出: {out.shape}, 参数量: {params:,}")
3. Vision Transformer(ViT)
ViT 是将图像切分为多个 patch,再应用 Transformer 的开创性方法。
import torch
import torch.nn as nn
import math
class PatchEmbedding(nn.Module):
"""将图像转换为 patch 嵌入"""
def __init__(self, image_size=224, patch_size=16, in_channels=3, embed_dim=768):
super(PatchEmbedding, self).__init__()
self.image_size = image_size
self.patch_size = patch_size
self.num_patches = (image_size // patch_size) ** 2
# 通过单个卷积将 patch 转换为嵌入
self.projection = nn.Conv2d(
in_channels, embed_dim,
kernel_size=patch_size, stride=patch_size
)
def forward(self, x):
# x: (B, C, H, W)
x = self.projection(x) # (B, embed_dim, H/patch, W/patch)
x = x.flatten(2) # (B, embed_dim, num_patches)
x = x.transpose(1, 2) # (B, num_patches, embed_dim)
return x
class MultiHeadSelfAttention(nn.Module):
"""多头自注意力"""
def __init__(self, embed_dim, num_heads, dropout=0.0):
super(MultiHeadSelfAttention, self).__init__()
self.num_heads = num_heads
self.head_dim = embed_dim // num_heads
self.scale = self.head_dim ** -0.5
self.qkv = nn.Linear(embed_dim, embed_dim * 3)
self.proj = nn.Linear(embed_dim, embed_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
B, N, C = x.shape
# 生成 Q、K、V
qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim)
qkv = qkv.permute(2, 0, 3, 1, 4)
q, k, v = qkv.unbind(0) # 各自形状为 (B, heads, N, head_dim)
# 注意力权重
attn = (q @ k.transpose(-2, -1)) * self.scale
attn = attn.softmax(dim=-1)
attn = self.dropout(attn)
# 加权求和
x = (attn @ v).transpose(1, 2).reshape(B, N, C)
x = self.proj(x)
return x
class TransformerBlock(nn.Module):
"""Transformer 块"""
def __init__(self, embed_dim, num_heads, mlp_ratio=4.0, dropout=0.0):
super(TransformerBlock, self).__init__()
self.norm1 = nn.LayerNorm(embed_dim)
self.attn = MultiHeadSelfAttention(embed_dim, num_heads, dropout)
self.norm2 = nn.LayerNorm(embed_dim)
mlp_hidden = int(embed_dim * mlp_ratio)
self.mlp = nn.Sequential(
nn.Linear(embed_dim, mlp_hidden),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(mlp_hidden, embed_dim),
nn.Dropout(dropout)
)
def forward(self, x):
x = x + self.attn(self.norm1(x)) # 残差连接
x = x + self.mlp(self.norm2(x)) # 残差连接
return x
class VisionTransformer(nn.Module):
"""Vision Transformer(ViT)实现"""
def __init__(self, image_size=224, patch_size=16, in_channels=3,
num_classes=1000, embed_dim=768, depth=12, num_heads=12,
mlp_ratio=4.0, dropout=0.0):
super(VisionTransformer, self).__init__()
# Patch 嵌入
self.patch_embed = PatchEmbedding(image_size, patch_size, in_channels, embed_dim)
num_patches = self.patch_embed.num_patches
# CLS 令牌 + 位置嵌入
self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
self.pos_embedding = nn.Parameter(
torch.zeros(1, num_patches + 1, embed_dim)
)
self.pos_dropout = nn.Dropout(dropout)
# Transformer 块
self.blocks = nn.Sequential(*[
TransformerBlock(embed_dim, num_heads, mlp_ratio, dropout)
for _ in range(depth)
])
self.norm = nn.LayerNorm(embed_dim)
self.head = nn.Linear(embed_dim, num_classes)
self._init_weights()
def _init_weights(self):
nn.init.trunc_normal_(self.pos_embedding, std=0.02)
nn.init.trunc_normal_(self.cls_token, std=0.02)
for m in self.modules():
if isinstance(m, nn.Linear):
nn.init.trunc_normal_(m.weight, std=0.02)
if m.bias is not None:
nn.init.zeros_(m.bias)
def forward(self, x):
B = x.shape[0]
# Patch 嵌入
x = self.patch_embed(x) # (B, num_patches, embed_dim)
# 添加 CLS 令牌
cls_tokens = self.cls_token.expand(B, -1, -1)
x = torch.cat([cls_tokens, x], dim=1) # (B, num_patches+1, embed_dim)
# 添加位置嵌入
x = x + self.pos_embedding
x = self.pos_dropout(x)
# Transformer 处理
x = self.blocks(x)
x = self.norm(x)
# 用 CLS 令牌进行分类
cls_output = x[:, 0]
logits = self.head(cls_output)
return logits
# ViT 的几种变体
def vit_small(num_classes=1000):
return VisionTransformer(
image_size=224, patch_size=16, embed_dim=384, depth=12,
num_heads=6, num_classes=num_classes
)
def vit_base(num_classes=1000):
return VisionTransformer(
image_size=224, patch_size=16, embed_dim=768, depth=12,
num_heads=12, num_classes=num_classes
)
def vit_large(num_classes=1000):
return VisionTransformer(
image_size=224, patch_size=16, embed_dim=1024, depth=24,
num_heads=16, num_classes=num_classes
)
# 测试
model = vit_base()
x = torch.randn(2, 3, 224, 224)
out = model(x)
params = sum(p.numel() for p in model.parameters())
print(f"ViT-Base 输出: {out.shape}, 参数量: {params:,}")
4. 目标检测:YOLO
import torch
import torch.nn as nn
class YOLOHead(nn.Module):
"""YOLO 检测头(简化版)"""
def __init__(self, in_channels, num_anchors, num_classes):
super(YOLOHead, self).__init__()
self.num_anchors = num_anchors
self.num_classes = num_classes
# 预测: (x, y, w, h, objectness, num_classes) * num_anchors
out_channels = num_anchors * (5 + num_classes)
self.head = nn.Sequential(
nn.Conv2d(in_channels, in_channels * 2, kernel_size=3, padding=1),
nn.BatchNorm2d(in_channels * 2),
nn.LeakyReLU(0.1),
nn.Conv2d(in_channels * 2, out_channels, kernel_size=1)
)
def forward(self, x):
out = self.head(x)
B, C, H, W = out.shape
# (B, num_anchors, H, W, 5+classes)
out = out.reshape(B, self.num_anchors, 5 + self.num_classes, H, W)
out = out.permute(0, 1, 3, 4, 2).contiguous()
return out
# 简单的 YOLOv1 风格模型
class SimpleYOLO(nn.Module):
def __init__(self, backbone, num_classes=80, num_boxes=2):
super(SimpleYOLO, self).__init__()
self.backbone = backbone
self.num_classes = num_classes
self.num_boxes = num_boxes
# 预测头: grid_size x grid_size x (B*5 + C)
self.head = nn.Sequential(
nn.AdaptiveAvgPool2d((7, 7)),
nn.Flatten(),
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(4096, 7 * 7 * (num_boxes * 5 + num_classes))
)
def forward(self, x):
features = self.backbone(x)
out = self.head(features)
out = out.reshape(-1, 7, 7, self.num_boxes * 5 + self.num_classes)
return out
5. 图像分割:U-Net
import torch
import torch.nn as nn
import torch.nn.functional as F
class DoubleConv(nn.Module):
"""U-Net 的双重卷积块"""
def __init__(self, in_channels, out_channels):
super(DoubleConv, self).__init__()
self.double_conv = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True),
nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)
def forward(self, x):
return self.double_conv(x)
class UNet(nn.Module):
"""U-Net 实现(医学图像分割)"""
def __init__(self, in_channels=1, num_classes=2, features=[64, 128, 256, 512]):
super(UNet, self).__init__()
self.encoders = nn.ModuleList()
self.decoders = nn.ModuleList()
self.pool = nn.MaxPool2d(2, 2)
# Encoder
for feature in features:
self.encoders.append(DoubleConv(in_channels, feature))
in_channels = feature
# Bottleneck
self.bottleneck = DoubleConv(features[-1], features[-1] * 2)
# Decoder
for feature in reversed(features):
self.decoders.append(
nn.ConvTranspose2d(feature * 2, feature, kernel_size=2, stride=2)
)
self.decoders.append(DoubleConv(feature * 2, feature))
# 最终分类层
self.final_conv = nn.Conv2d(features[0], num_classes, kernel_size=1)
def forward(self, x):
skip_connections = []
# Encoder
for encoder in self.encoders:
x = encoder(x)
skip_connections.append(x)
x = self.pool(x)
# Bottleneck
x = self.bottleneck(x)
skip_connections = skip_connections[::-1] # 反转顺序
# Decoder
for i in range(0, len(self.decoders), 2):
x = self.decoders[i](x) # Upsample
skip = skip_connections[i // 2]
# 尺寸不一致时调整
if x.shape != skip.shape:
x = F.interpolate(x, size=skip.shape[2:])
x = torch.cat([skip, x], dim=1) # Skip Connection
x = self.decoders[i + 1](x) # DoubleConv
return self.final_conv(x)
# 测试
model = UNet(in_channels=1, num_classes=2)
x = torch.randn(4, 1, 572, 572)
out = model(x)
print(f"U-Net 输出: {out.shape}") # (4, 2, 572, 572)
params = sum(p.numel() for p in model.parameters())
print(f"U-Net 参数量: {params:,}")
6. 迁移学习实战指南
使用 torchvision.models
import torch
import torch.nn as nn
import torchvision.models as models
from torch.utils.data import DataLoader
from torchvision import transforms, datasets
import torch.optim as optim
from tqdm import tqdm
# 加载预训练模型
model_resnet = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)
model_efficientnet = models.efficientnet_b4(weights=models.EfficientNet_B4_Weights.DEFAULT)
model_vit = models.vit_b_16(weights=models.ViT_B_16_Weights.IMAGENET1K_V1)
print(f"ResNet-50 参数量: {sum(p.numel() for p in model_resnet.parameters()):,}")
def feature_extraction(num_classes, freeze=True):
"""Feature Extraction:冻结骨干网络,只训练分类器"""
model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)
if freeze:
for param in model.parameters():
param.requires_grad = False
# 替换分类器
in_features = model.fc.in_features
model.fc = nn.Sequential(
nn.Dropout(0.5),
nn.Linear(in_features, 256),
nn.ReLU(),
nn.Linear(256, num_classes)
)
# 只有最后一层可训练
for param in model.fc.parameters():
param.requires_grad = True
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(f"可训练参数: {trainable:,} / {total:,} ({trainable/total:.1%})")
return model
def fine_tuning(num_classes, unfreeze_layers=2):
"""Fine-tuning:训练最后几层"""
model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)
# 全部冻结
for param in model.parameters():
param.requires_grad = False
# 解冻最后 N 层
layers = [model.layer4, model.avgpool, model.fc]
for layer in layers[-unfreeze_layers:]:
for param in layer.parameters():
param.requires_grad = True
# 替换分类器
model.fc = nn.Linear(model.fc.in_features, num_classes)
return model
# 训练循环
def train_model(model, train_loader, val_loader, epochs=10,
learning_rate=1e-3, device='cuda'):
model = model.to(device)
criterion = nn.CrossEntropyLoss()
# 按参数组设置不同学习率
backbone_params = [p for n, p in model.named_parameters()
if 'fc' not in n and p.requires_grad]
head_params = [p for n, p in model.named_parameters()
if 'fc' in n and p.requires_grad]
optimizer = optim.AdamW([
{'params': backbone_params, 'lr': learning_rate * 0.1},
{'params': head_params, 'lr': learning_rate}
], weight_decay=1e-4)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
best_val_acc = 0.0
history = {'train_loss': [], 'val_loss': [], 'train_acc': [], 'val_acc': []}
for epoch in range(epochs):
# 训练
model.train()
train_loss, train_correct, train_total = 0.0, 0, 0
for images, labels in tqdm(train_loader, desc=f'Epoch {epoch+1}/{epochs}'):
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
# Gradient Clipping
nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
train_loss += loss.item() * images.size(0)
train_correct += (outputs.argmax(1) == labels).sum().item()
train_total += images.size(0)
# 验证
model.eval()
val_loss, val_correct, val_total = 0.0, 0, 0
with torch.no_grad():
for images, labels in val_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
val_loss += loss.item() * images.size(0)
val_correct += (outputs.argmax(1) == labels).sum().item()
val_total += images.size(0)
scheduler.step()
train_acc = train_correct / train_total
val_acc = val_correct / val_total
epoch_train_loss = train_loss / train_total
epoch_val_loss = val_loss / val_total
history['train_loss'].append(epoch_train_loss)
history['val_loss'].append(epoch_val_loss)
history['train_acc'].append(train_acc)
history['val_acc'].append(val_acc)
print(f"Epoch {epoch+1}: Train={train_acc:.4f}, Val={val_acc:.4f}")
# 保存最优模型
if val_acc > best_val_acc:
best_val_acc = val_acc
torch.save(model.state_dict(), 'best_model.pt')
print(f"最佳验证准确率: {best_val_acc:.4f}")
return model, history
# 数据增强
def get_transforms(image_size=224):
train_transforms = transforms.Compose([
transforms.RandomResizedCrop(image_size),
transforms.RandomHorizontalFlip(),
transforms.RandomRotation(15),
transforms.ColorJitter(brightness=0.2, contrast=0.2,
saturation=0.2, hue=0.1),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
val_transforms = transforms.Compose([
transforms.Resize(int(image_size * 1.14)),
transforms.CenterCrop(image_size),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
return train_transforms, val_transforms
架构性能对比
| 模型 | 年份 | Top-1 准确率 | 参数量 | FLOPs |
|---|---|---|---|---|
| LeNet-5 | 1998 | ~99%(MNIST) | 60K | - |
| AlexNet | 2012 | 56.5% | 61M | 724M |
| VGG-16 | 2014 | 71.6% | 138M | 15.5G |
| GoogLeNet | 2014 | 68.7% | 6.8M | 1.5G |
| ResNet-50 | 2015 | 75.3% | 25M | 4.1G |
| DenseNet-121 | 2017 | 74.4% | 8M | 2.9G |
| MobileNetV2 | 2018 | 71.8% | 3.4M | 300M |
| EfficientNet-B0 | 2019 | 77.1% | 5.3M | 390M |
| ConvNeXt-T | 2022 | 82.1% | 28M | 4.5G |
| ViT-B/16 | 2020 | 81.8% | 86M | 17.6G |
结语
CNN 架构在持续演进:
- LeNet(1998):最初的实用 CNN,确立了基本结构
- AlexNet(2012):深度学习复兴,引入 ReLU 和 Dropout
- VGGNet(2014):3x3 卷积的力量,证明了深度的重要性
- ResNet(2015):用残差连接解决梯度消失,实现数百层的训练
- DenseNet(2017):密集连接最大化特征复用
- MobileNet(2017):轻量化实现移动端部署
- EfficientNet(2019):复合缩放达成最高效率
- ConvNeXt(2022):将 Transformer 的洞见应用于 CNN
- ViT(2020):将图像也当作序列处理的新范式
在实战中,建议从 torchvision 的预训练模型出发,通过迁移学习快速适配到目标任务。
参考资料
- PyTorch Vision Models
- ResNet 论文:He et al., "Deep Residual Learning for Image Recognition"(arXiv:1512.03385)
- EfficientNet 论文:Tan & Le, "EfficientNet: Rethinking Model Scaling"(arXiv:1905.11946)
- ViT 论文:Dosovitskiy et al., "An Image is Worth 16x16 Words"(arXiv:2010.11929)
- ConvNeXt 论文:Liu et al., "A ConvNet for the 2020s"(arXiv:2201.03545)