CNN概述#
为什么需要CNN?#
全连接网络的局限性:
- 参数过多:对于 $28 \times 28$ 图像,需要 $784 \times 128 = 100,352$ 个参数
- 忽略空间结构:将图像展平丢失空间信息
- 计算量大:难以处理大图像
CNN的优势#
- 参数共享:同一卷积核在整个图像上共享
- 局部连接:每个神经元只连接局部区域
- 平移不变性:能够识别不同位置的相同模式
- 层次化特征:自动学习从低级到高级的特征
CNN的基本结构#
输入图像 → 卷积层 → 激活函数 → 池化层 → ... → 全连接层 → 输出卷积层#
卷积操作#
基本概念#
卷积(Convolution) 是在图像上滑动一个小的滤波器(卷积核),计算局部区域的加权和。
数学表示#
对于输入图像 $I$ 和卷积核 $K$ :
$$ (I * K)[i, j] = \sum_m \sum_n I[i+m, j+n] \cdot K[m, n] $$示例#
输入图像($5 \times 5$):
[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]
卷积核($3 \times 3$):
[-1 -1 -1]
[-1 8 -1]
[-1 -1 -1]
输出特征图($3 \times 3$):
通过滑动窗口计算卷积参数#
1. 卷积核大小(Kernel Size)#
常见大小:$3 \times 3$ 、$5 \times 5$ 、$7 \times 7$
- 小卷积核 ($3 \times 3$ ):参数少,感受野小
- 大卷积核 ($5 \times 5$ 、$7 \times 7$ ):参数多,感受野大
- 趋势:使用多个 $3 \times 3$ 卷积代替大卷积核
2. 步长(Stride)#
卷积核移动的步数。
- Stride=1:输出尺寸 ≈ 输入尺寸
- Stride=2:输出尺寸 ≈ 输入尺寸/2
输出尺寸计算:
$$ \text{output\_size} = \frac{\text{input\_size} - \text{kernel\_size}}{\text{stride}} + 1 $$3. 填充(Padding)#
在输入周围添加零值。
- Valid Padding:不填充,输出尺寸 < 输入尺寸
- Same Padding:填充使输出尺寸 = 输入尺寸
填充大小:
$$ \text{padding} = \frac{\text{kernel\_size} - 1}{2} \quad \text{(对于stride=1)} $$4. 输入/输出通道#
- 输入通道:输入图像的通道数(RGB=3,灰度=1)
- 输出通道:卷积核的数量,每个卷积核产生一个特征图
多通道卷积#
对于RGB图像(3通道):
每个卷积核有3个通道(对应RGB)
$$ \text{输出} = \sum_i (\text{输入通道}_i \times \text{卷积核}_i) + \text{偏置} $$参数数量:
$$ \text{参数} = (\text{kernel\_h} \times \text{kernel\_w} \times \text{input\_channels} + 1) \times \text{output\_channels} $$$1 \times 1$ 卷积#
作用:
- 降维/升维:改变通道数
- 非线性:增加非线性变换
- 参数效率:参数少
示例:
输入:$32 \times 32 \times 256$
$1 \times 1$ 卷积:$32 \times 32 \times 64$
参数:$(1 \times 1 \times 256 + 1) \times 64 = 16,448$池化层#
作用#
- 降维:减少特征图尺寸
- 平移不变性:对小的平移不敏感
- 减少参数:降低计算量
最大池化(Max Pooling)#
取局部区域的最大值。
输入($4 \times 4$):
[1 3 2 4]
[5 2 1 3]
[2 6 4 1]
[1 2 3 4]
Max Pooling($2 \times 2$,stride=2):
[5 4]
[6 4]平均池化(Average Pooling)#
取局部区域的平均值。
全局池化(Global Pooling)#
对整个特征图进行池化:
- Global Max Pooling:取全局最大值
- Global Average Pooling:取全局平均值
作用:替代全连接层,减少参数。
全连接层#
作用#
将卷积层提取的特征映射到最终输出。
展平操作#
将多维特征图展平为一维向量:
输入:$7 \times 7 \times 64$
展平:$3136$
全连接:$3136 \to 128 \to 10$经典CNN架构#
1. LeNet-5(1998)#
特点:
- 第一个成功的CNN
- 用于手写数字识别
结构:
输入($32 \times 32$) → Conv($6@28 \times 28$) → Pool → Conv($16@10 \times 10$) → Pool → FC($120$) → FC($84$) → FC($10$)2. AlexNet(2012)#
特点:
- 深度学习复兴的标志
- 使用ReLU、Dropout
- 数据增强
结构:
输入($224 \times 224 \times 3$) → Conv → Pool → Conv → Pool → Conv → Conv → Conv → Pool → FC($4096$) → Dropout → FC($4096$) → Dropout → FC($1000$)3. VGG(2014)#
特点:
- 使用小的 $3 \times 3$ 卷积核
- 深度网络($11$ -$19$ 层)
VGG16结构:
Conv(64) → Conv(64) → Pool
Conv(128) → Conv(128) → Pool
Conv(256) → Conv(256) → Conv(256) → Pool
Conv(512) → Conv(512) → Conv(512) → Pool
Conv(512) → Conv(512) → Conv(512) → Pool
FC(4096) → FC(4096) → FC(1000)4. ResNet(2015)#
特点:
- 残差连接:解决梯度消失问题
- 可以训练非常深的网络(50-152层)
残差块:
输入 x
↓
Conv → BN → ReLU → Conv → BN
↓ ↓
└──────── 相加 ──────────────┘
↓
ReLU
↓
输出5. Inception(2014)#
特点:
- 多尺度特征:并行使用不同大小的卷积核
- $1 \times 1$ 卷积降维
6. MobileNet(2017)#
特点:
- 深度可分离卷积:减少参数和计算量
- 适合移动设备
代码实现#
基础CNN#
import torch
import torch.nn as nn
import torch.nn.functional as F
class SimpleCNN(nn.Module):
def __init__(self, num_classes=10):
super(SimpleCNN, self).__init__()
# 卷积层
self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
# 池化层
self.pool = nn.MaxPool2d(2, 2)
# 全连接层
self.fc1 = nn.Linear(128 * 4 * 4, 512)
self.fc2 = nn.Linear(512, num_classes)
# Dropout
self.dropout = nn.Dropout(0.5)
def forward(self, x):
# 输入: [batch, 3, 32, 32]
x = self.pool(F.relu(self.conv1(x))) # [batch, 32, 16, 16]
x = self.pool(F.relu(self.conv2(x))) # [batch, 64, 8, 8]
x = self.pool(F.relu(self.conv3(x))) # [batch, 128, 4, 4]
# 展平
x = x.view(-1, 128 * 4 * 4)
# 全连接
x = F.relu(self.fc1(x))
x = self.dropout(x)
x = self.fc2(x)
return x带BatchNorm的CNN#
class CNNWithBN(nn.Module):
def __init__(self, num_classes=10):
super(CNNWithBN, self).__init__()
self.conv1 = nn.Conv2d(3, 32, 3, padding=1)
self.bn1 = nn.BatchNorm2d(32)
self.conv2 = nn.Conv2d(32, 64, 3, padding=1)
self.bn2 = nn.BatchNorm2d(64)
self.conv3 = nn.Conv2d(64, 128, 3, padding=1)
self.bn3 = nn.BatchNorm2d(128)
self.pool = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(128 * 4 * 4, 512)
self.fc2 = nn.Linear(512, num_classes)
def forward(self, x):
x = self.pool(F.relu(self.bn1(self.conv1(x))))
x = self.pool(F.relu(self.bn2(self.conv2(x))))
x = self.pool(F.relu(self.bn3(self.conv3(x))))
x = x.view(-1, 128 * 4 * 4)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return xResNet残差块#
class ResidualBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super(ResidualBlock, self).__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride=stride, padding=1)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1)
self.bn2 = nn.BatchNorm2d(out_channels)
# 如果输入输出尺寸不同,需要下采样
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, 1, stride=stride),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
residual = x
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += self.shortcut(residual) # 残差连接
out = F.relu(out)
return out使用预训练模型#
import torchvision.models as models
# 加载预训练模型
resnet18 = models.resnet18(pretrained=True)
# 修改最后一层
num_classes = 10
resnet18.fc = nn.Linear(resnet18.fc.in_features, num_classes)
# 冻结部分层(可选)
for param in resnet18.parameters():
param.requires_grad = False
resnet18.fc.requires_grad = True # 只训练最后一层可视化特征图#
import matplotlib.pyplot as plt
def visualize_features(model, x, layer_name='conv1'):
"""可视化卷积层的特征图"""
model.eval()
# 注册hook
activations = {}
def hook(name):
def hook_fn(module, input, output):
activations[name] = output.detach()
return hook_fn
hooks = []
for name, module in model.named_modules():
if name == layer_name:
hooks.append(module.register_forward_hook(hook(name)))
# 前向传播
_ = model(x)
# 可视化
feature_maps = activations[layer_name][0] # 取第一个样本
num_features = feature_maps.size(0)
fig, axes = plt.subplots(4, 8, figsize=(16, 8))
for idx in range(min(32, num_features)):
row, col = idx // 8, idx % 8
axes[row, col].imshow(feature_maps[idx].cpu(), cmap='gray')
axes[row, col].axis('off')
plt.tight_layout()
plt.show()
# 移除hooks
for hook in hooks:
hook.remove()总结#
- CNN:专门处理图像数据的神经网络
- 卷积层:提取局部特征,参数共享
- 池化层:降维,增加平移不变性
- 全连接层:将特征映射到输出
- 经典架构:LeNet、AlexNet、VGG、ResNet等
关键要点:
- CNN通过卷积操作自动学习特征
- 参数共享和局部连接大大减少参数数量
- 层次化特征:从边缘到对象
- 残差连接使训练更深网络成为可能