CNN概述#

为什么需要CNN?#

全连接网络的局限性

  • 参数过多:对于 $28 \times 28$ 图像,需要 $784 \times 128 = 100,352$ 个参数
  • 忽略空间结构:将图像展平丢失空间信息
  • 计算量大:难以处理大图像

CNN的优势#

  1. 参数共享:同一卷积核在整个图像上共享
  2. 局部连接:每个神经元只连接局部区域
  3. 平移不变性:能够识别不同位置的相同模式
  4. 层次化特征:自动学习从低级到高级的特征

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$

池化层#

作用#

  1. 降维:减少特征图尺寸
  2. 平移不变性:对小的平移不敏感
  3. 减少参数:降低计算量

最大池化(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 x

ResNet残差块#

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()

总结#

  1. CNN:专门处理图像数据的神经网络
  2. 卷积层:提取局部特征,参数共享
  3. 池化层:降维,增加平移不变性
  4. 全连接层:将特征映射到输出
  5. 经典架构:LeNet、AlexNet、VGG、ResNet等

关键要点

  • CNN通过卷积操作自动学习特征
  • 参数共享和局部连接大大减少参数数量
  • 层次化特征:从边缘到对象
  • 残差连接使训练更深网络成为可能