CNN-卷积神经网络
CNN-卷积神经网络










1. 卷积神经网络发展史
1.1 什么是 CNN
卷积神经网络是一类专门处理网格状结构数据的神经网络,最典型的是图像(2D 像素网格)。其核心思想是:局部连接 + 参数共享 + 层级抽象。
2012 年 AlexNet 在 ImageNet 上取得突破性成绩后,CNN 成为计算机视觉领域的主导模型。
1.2 生物启发:视觉系统的层级结构
1962 年,神经科学家 Hubel & Wiesel 通过猫的视觉皮层实验发现:
| 细胞类型 | 功能 | CNN 对应 |
|---|---|---|
| 简单细胞 | 对特定方向边缘响应 | 卷积层(特征提取) |
| 复杂细胞 | 对位置偏移不敏感 | 池化层(平移不变性) |
核心发现:视觉处理是层级化的——从边缘到纹理再到物体部件。
这正是深度学习优于传统特征工程的根本原因:CNN 的每一层自动学习不同抽象层次的特征,无需手工设计 SIFT、HOG 等特征提取器。
浅层 ──→ 边缘、颜色斑点
中层 ──→ 纹理、简单图案
深层 ──→ 眼睛、轮子、人脸
1962年,生物学家Torsten Wisesl 和 David H.Hunel (1981的诺贝尔医学奖)对猫的视觉结构进行研究,首次发现猫的视觉系统中存在层级结构, 并且发现了两种重要的细胞simple cells和complex cells,不同类型细胞承受不同抽象层次的视觉感知功能。
注意:层级结构的发现是十分十分重要的,正是因为有了层级结构,才能够在不同层抽取不同程度的图像特征,才让深度卷积神经网络大行其道,击败了传统的图像特征工程,就是因为深度卷积网络能够在不同层能提取出不同程度的特征,替代了传统的特征工程,就是因为深度卷积网络,不同层就是对应了不同的图像特征工程,如初始层是边缘特征提取器,中间层是纹理特征,后面的卷积层则是高级语义特征。
simple cells和complex cells:则对应了卷积和池化操作;
可以说1962年的生物实验是卷积神经网络诞生的理论基石。
1.3 发展里程碑
| 年份 | 模型 | 贡献 |
|---|---|---|
| 1980 | Neocognitron | CNN 雏形 |
| 1998 | LeNet-5 | 首个完整 CNN,MNIST 手写识别 |
| 2012 | AlexNet | ImageNet 夺冠,开启深度学习时代 |
| 2014 | VGG | 小卷积核(3×3),更深 |
| 2014 | GoogLeNet | Inception 模块,多尺度 |
| 2015 | ResNet | 残差连接,可训练超深层(152 层) |
| 2017 | DenseNet | 密集连接,极致特征复用 |
2. 卷积操作
2.1 数学定义
对于输入图像 X∈RH×W×CinX \in \mathbb{R}^{H \times W \times C_{in}}X∈RH×W×Cin 和卷积核 K∈Rkh×kw×Cin×CoutK \in \mathbb{R}^{k_h \times k_w \times C_{in} \times C_{out}}K∈Rkh×kw×Cin×Cout,卷积输出 Y∈RH′×W′×CoutY \in \mathbb{R}^{H' \times W' \times C_{out}}Y∈RH′×W′×Cout 为:
Y[i,j,cout]=∑u=0kh−1∑v=0kw−1∑cin=0Cin−1X[i+u,j+v,cin]⋅K[u,v,cin,cout]+b[cout]Y[i,j,c_{out}] = \sum_{u=0}^{k_h-1} \sum_{v=0}^{k_w-1} \sum_{c_{in}=0}^{C_{in}-1} X[i+u, j+v, c_{in}] \cdot K[u, v, c_{in}, c_{out}] + b[c_{out}]Y[i,j,cout]=u=0∑kh−1v=0∑kw−1cin=0∑Cin−1X[i+u,j+v,cin]⋅K[u,v,cin,cout]+b[cout]
2.2 输出尺寸公式
输出维度由以下参数决定:
- 输入尺寸:H,WH, WH,W
- 卷积核尺寸:kkk
- 填充(padding):ppp
- 步长(stride):sss
H′=⌊H+2p−ks⌋+1H' = \left\lfloor \frac{H + 2p - k}{s} \right\rfloor + 1H′=⌊sH+2p−k⌋+1
W′=⌊W+2p−ks⌋+1W' = \left\lfloor \frac{W + 2p - k}{s} \right\rfloor + 1W′=⌊sW+2p−k⌋+1
2.3 PyTorch 代码示例
import torch
import torch.nn as nn
import torch.nn.functional as F
# 输入特征图: [batch, channels, height, width]
x = torch.randn(1, 3, 32, 32) # 单张 32x32 RGB 图像
# 标准卷积层: 3通道输入 → 16通道输出
conv = nn.Conv2d(
in_channels=3,
out_channels=16,
kernel_size=3,
stride=1,
padding=1 # padding=1 保证尺寸不变 (32+2-3)/1+1=32
)
y = conv(x)
print(f"输入尺寸: {x.shape}") # torch.Size([1, 3, 32, 32])
print(f"输出尺寸: {y.shape}") # torch.Size([1, 16, 32, 32])
# ============================================
# 手动实现二维卷积(教育目的)
def conv2d_manual(x, kernel, stride=1, padding=0):
"""x: (C_in, H, W), kernel: (C_out, C_in, kH, kW)"""
c_in, h, w = x.shape
c_out, _, k_h, k_w = kernel.shape
# 填充
x_pad = F.pad(x, (padding, padding, padding, padding))
h_out = (h + 2*padding - k_h) // stride + 1
w_out = (w + 2*padding - k_w) // stride + 1
out = torch.zeros(c_out, h_out, w_out)
for i in range(c_out):
for j in range(h_out):
for k in range(w_out):
# 滑动窗口
h_start, w_start = j * stride, k * stride
region = x_pad[:, h_start:h_start+k_h, w_start:w_start+k_w]
out[i, j, k] = torch.sum(region * kernel[i])
return out
# 验证
x_simple = torch.randn(1, 5, 5)
kernel = torch.randn(2, 1, 3, 3)
manual_out = conv2d_manual(x_simple, kernel, stride=1, padding=0)
pytorch_out = F.conv2d(x_simple.unsqueeze(0), kernel)
print(f"手动实现与 PyTorch 一致: {torch.allclose(manual_out, pytorch_out.squeeze(0), atol=1e-6)}")
2.4 工程要点
| 参数 | 典型值 | 影响 |
|---|---|---|
| kernel_size | 3×3 | 更大感受野但参数多(现多用堆叠小核) |
| stride | 1(特征提取)/ 2(下采样) | 步长 > 1 可替代池化 |
| padding | ‘same’ 保持尺寸 | 保留边缘信息 |
| dilation | 1(标准) | 空洞卷积,增大感受野不增参数 |
3. 池化操作
3.1 数学定义
最大池化(Max Pooling)在 k×kk \times kk×k 窗口内取最大值:
Y[i,j]=max0≤u<k,0≤v<kX[i⋅s+u,j⋅s+v]Y[i,j] = \max_{0 \le u < k, 0 \le v < k} X[i \cdot s + u, j \cdot s + v]Y[i,j]=0≤u<k,0≤v<kmaxX[i⋅s+u,j⋅s+v]
平均池化(Average Pooling):
Y[i,j]=1k2∑u=0k−1∑v=0k−1X[i⋅s+u,j⋅s+v]Y[i,j] = \frac{1}{k^2} \sum_{u=0}^{k-1} \sum_{v=0}^{k-1} X[i \cdot s + u, j \cdot s + v]Y[i,j]=k21u=0∑k−1v=0∑k−1X[i⋅s+u,j⋅s+v]
全局平均池化(Global Average Pooling, GAP):
- 将整个特征图 H×WH \times WH×W 池化为 1×11 \times 11×1
- 常用于分类层前,替代全连接层,大幅减少参数
3.2 PyTorch 代码示例
import torch
import torch.nn as nn
x = torch.randn(1, 64, 28, 28)
# 最大池化: 2x2 窗口, stride=2 → 尺寸减半
maxpool = nn.MaxPool2d(kernel_size=2, stride=2)
y_max = maxpool(x)
print(f"最大池化: {x.shape} → {y_max.shape}") # [1,64,28,28] → [1,64,14,14]
# 平均池化
avgpool = nn.AvgPool2d(kernel_size=2, stride=2)
y_avg = avgpool(x)
# 全局平均池化 (常用于分类层前)
gap = nn.AdaptiveAvgPool2d(1)
y_gap = gap(x) # [1,64,28,28] → [1,64,1,1]
y_gap = y_gap.view(1, -1) # → [1,64]
print(f"全局平均池化: {y_gap.shape}")
# 可以接分类头
classifier = nn.Linear(64, 10)
logits = classifier(y_gap)
3.3 池化的替代方案
现代 CNN 中,池化逐渐被 stride=2 卷积 或 空洞卷积 替代:
# 用卷积做下采样(更可学习,效果通常更好)
downsample_conv = nn.Conv2d(64, 64, kernel_size=3, stride=2, padding=1)
y_conv_down = downsample_conv(x) # [1,64,28,28] → [1,64,14,14]
4. LeNet-5 及 CNN 结构进化史
4.1 LeNet-5 完整实现
import torch.nn as nn
class LeNet5(nn.Module):
"""LeNet-5 for MNIST (32x32 input)"""
def __init__(self, num_classes=10):
super().__init__()
# C1: 卷积 + 池化
self.conv1 = nn.Conv2d(1, 6, kernel_size=5, padding=2) # 32→32 (保持)
self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2) # 32→16
# C3: 卷积 + 池化
self.conv2 = nn.Conv2d(6, 16, kernel_size=5) # 16→12 (无padding)
self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2) # 12→6
# 全连接层
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, num_classes)
def forward(self, x):
x = torch.tanh(self.conv1(x))
x = self.pool1(x)
x = torch.tanh(self.conv2(x))
x = self.pool2(x)
x = x.view(x.size(0), -1) # 展平
x = torch.tanh(self.fc1(x))
x = torch.tanh(self.fc2(x))
x = self.fc3(x)
return x
# 测试
model = LeNet5()
dummy = torch.randn(1, 1, 32, 32)
print(f"LeNet-5 输出: {model(dummy).shape}") # [1, 10]
4.2 关键架构创新(带代码片段)
ResNet:残差连接
y=F(x,{Wi})+xy = F(x, \{W_i\}) + xy=F(x,{Wi})+x
class ResidualBlock(nn.Module):
def __init__(self, channels):
super().__init__()
self.conv1 = nn.Conv2d(channels, channels, 3, padding=1)
self.bn1 = nn.BatchNorm2d(channels)
self.conv2 = nn.Conv2d(channels, channels, 3, padding=1)
self.bn2 = nn.BatchNorm2d(channels)
def forward(self, x):
residual = x
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += residual # 残差连接
return F.relu(out)
Inception:多尺度并行
class InceptionBlock(nn.Module):
def __init__(self, in_ch, out1, out3_1, out3, out5_1, out5, pool_proj):
super().__init__()
# 1x1 分支
self.branch1 = nn.Conv2d(in_ch, out1, 1)
# 3x3 分支
self.branch2 = nn.Sequential(
nn.Conv2d(in_ch, out3_1, 1),
nn.Conv2d(out3_1, out3, 3, padding=1)
)
# 5x5 分支
self.branch3 = nn.Sequential(
nn.Conv2d(in_ch, out5_1, 1),
nn.Conv2d(out5_1, out5, 5, padding=2)
)
# 池化分支
self.branch4 = nn.Sequential(
nn.MaxPool2d(3, stride=1, padding=1),
nn.Conv2d(in_ch, pool_proj, 1)
)
def forward(self, x):
return torch.cat([self.branch1(x), self.branch2(x),
self.branch3(x), self.branch4(x)], dim=1)
4.3 进化趋势总结
| 特性 | 早期 (LeNet) | 现代 (ResNet/EfficientNet) |
|---|---|---|
| 深度 | 5-7 层 | 50-100+ 层 |
| 卷积核 | 5×5, 7×7 | 3×3 为主 |
| 归一化 | 无 | BatchNorm + LayerNorm |
| 激活函数 | tanh, sigmoid | ReLU, GELU, Swish |
| 池化 | 平均池化 | 较少用,stride卷积替代 |
| 正则化 | 无 | Dropout, DropPath, 数据增强 |
| 连接方式 | 直连 | 残差、密集连接 |
5. 总结与工程讨论
5.1 核心公式速查
| 操作 | 公式 |
|---|---|
| 卷积参数数 | params=kh×kw×Cin×Cout\text{params} = k_h \times k_w \times C_{in} \times C_{out}params=kh×kw×Cin×Cout |
| 感受野(叠 k×k) | RF=1+(k−1)×LRF = 1 + (k-1) \times LRF=1+(k−1)×L(L 为经过层数) |
| BatchNorm | x^=x−μσ2+ϵ,y=γx^+β\hat{x} = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}}, \quad y = \gamma \hat{x} + \betax^=σ2+ϵx−μ,y=γx^+β |
| Xavier 初始化 | W∼U[−6nin+nout,6nin+nout]W \sim \mathcal{U}\left[-\frac{\sqrt{6}}{\sqrt{n_{in}+n_{out}}}, \frac{\sqrt{6}}{\sqrt{n_{in}+n_{out}}}\right]W∼U[−nin+nout6,nin+nout6] |
5.2 工程实战建议
1. 选择默认配置
# 标准卷积块(工程最佳实践)
class ConvBlock(nn.Module):
def __init__(self, in_ch, out_ch, kernel=3, stride=1):
super().__init__()
self.conv = nn.Conv2d(in_ch, out_ch, kernel, stride, padding=kernel//2)
self.bn = nn.BatchNorm2d(out_ch)
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
return self.relu(self.bn(self.conv(x)))
2. 记忆/速度平衡
| 需求 | 推荐方案 |
|---|---|
| 最快推理 | MobileNetV3, EfficientNet-Lite |
| 最高精度 | ConvNeXt, ResNet-RS |
| 显存受限 | Group Convolution, Depthwise Conv |
| 小数据集 | 使用预训练模型 fine-tune |
3. 常见调试问题
# 检查梯度消失
for name, param in model.named_parameters():
if param.grad is not None:
print(f"{name}: mean_grad={param.grad.abs().mean():.2e}")
# 检查特征图尺寸变化
def print_shape_hook(module, input, output):
print(f"{module.__class__.__name__}: {input[0].shape} → {output.shape}")
5.3 局限性与讨论
- 归纳偏置:CNN 假设局部性和平移不变性,适用于图像,但不适合非网格数据
- 旋转/尺度敏感:需要数据增强或引入 SE、CoordConv 等模块
- 长距离依赖不足:Attention(如 ViT)在某些任务上超越 CNN
- 可解释性:特征图可视化、Grad-CAM 可部分解释,但仍未完全解决
5.4 未来趋势
- CNN + Transformer 混合架构(ConvNeXt, CoAtNet)
- 神经架构搜索(NAS):自动化设计高效 CNN
- 边缘部署:量化、剪枝、知识蒸馏
- 自监督 CNN:SimCLR, BYOL,减少标注依赖
附录:完整训练示例(PyTorch)
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
# 1. 数据加载
transform = transforms.Compose([
transforms.Resize(32),
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
train_data = datasets.MNIST('.', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_data, batch_size=64, shuffle=True)
# 2. 模型
model = LeNet5()
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)
# 3. 损失与优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 4. 训练循环
model.train()
for epoch in range(5):
total_loss = 0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f"Epoch {epoch+1}, Loss: {total_loss/len(train_loader):.4f}")
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)