19 KiB
从全连接层到卷积:深度学习中的空间智慧
引言
想象一下,当你在看一张照片时,你的眼睛会自动聚焦在局部区域——边缘、纹理、形状——然后将这些局部信息整合起来识别整体内容。卷积神经网络(CNN)正是模仿了这种视觉系统的运作方式。让我带你从数学原理出发,理解为什么卷积层比全连接层更适合处理图像数据。
一、全连接层的困境
1.1 表格数据的成功
在之前的章节中,我们已经看到多层感知机(MLP)在处理表格数据时表现出色:
import torch
from torch import nn
# 典型的MLP处理表格数据
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
# 隐藏层:学习特征交互
self.hidden = nn.Linear(784, 256)
self.relu = nn.ReLU()
self.output = nn.Linear(256, 10)
def forward(self, x):
x = self.flatten(x)
x = self.relu(self.hidden(x))
return self.output(x)
对于表格数据,每个样本是一行特征,我们需要学习特征之间的交互关系,MLP 正是为此而生。
1.2 高维图像的灾难
然而,当面对高维图像数据时,全连接层的参数数量会爆炸式增长:
# 假设:200×200 像素的灰度图像
input_size = 200 * 200 # = 40,000
hidden_size = 1000
# 全连接层参数数量
params_fc = input_size * hidden_size # 40,000,000 参数!
print(f"全连接层参数: {params_fc:,}")
# 对于彩色图像(RGB三通道)
input_rgb = 200 * 200 * 3 # = 120,000
params_rgb = input_rgb * hidden_size # 120,000,000 参数!
print(f"RGB图像全连接层参数: {params_rgb:,}")
问题本质:对于 200×200 的灰度图像,仅一个全连接层就需要 4000 万个参数!这对于训练和存储都是噩梦。
1.3 缺乏结构先验
全连接层假设所有输入之间都是平等交互的:
全连接层视角:
输入层: [x₁] ──┬── [h₁]
│
[x₂] ──┬── [h₂]
│ │ ... 每个输入都连接到每个输出
[x₃] ──┴── [h₃]
但图像有天然的空间结构:
- 相邻像素通常高度相关
- 图像特征可以出现在任何位置
- 局部区域比全局组合更有意义
二、空间先验:视觉智能的核心
2.1 沃尔玛在哪里?
让我们玩一个思想实验。想象在沃尔玛游戏中:
游戏包含混乱的场景,沃尔玛藏在各种位置,我们需要找出他。
关键洞察:沃尔玛的样子不取决于他藏在哪里。无论他出现在左上角还是右下角,我们都能认出他。
这启发了两个核心原则:
| 原则 | 含义 | 数学表达 |
|---|---|---|
| 平移不变性 | 无论物体在图像何处,检测器应该以相同方式工作 | V(i,j,a,b) = V(a,b)(不依赖位置) |
| 局部性 | 只看局部区域,不需要关注远处像素 | V(a,b) = 0 当 |a| > Δ 或 |b| > Δ |
2.2 数学形式化
让我们从全连接层的数学表示开始,逐步推导出卷积层。
原始全连接层表示
对于输入图像 X ∈ R^(h×w),隐藏表示 H ∈ R^(h×w):
H(i,j) = U(i,j) + Σₖ Σₗ W(i,j,k,l) · X(k,l)
其中 W 是四阶权重张量,参数数量为 h × w × h × w。
应用平移不变性
根据平移不变性原则,权重不应依赖位置 (i,j):
╔════════════════════════════════════════════════════════════╗
║ H(i,j) = u + Σₐ Σᵦ V(a,b) · X(i+a,j+b) ║
╚════════════════════════════════════════════════════════════╝
这已经是我们熟悉的卷积操作了!
应用局部性约束
进一步限制只访问局部区域(窗口大小为 2Δ+1):
╔═══════════════════════════════════════════════════════════════════╗
║ H(i,j) = u + Σₐ₌₋Δ^Δ Σᵦ₌₋Δ^Δ V(a,b) · X(i+a,j+b) ║
╚═══════════════════════════════════════════════════════════════════╝
这就是卷积层! 从数十亿参数减少到几百个参数。
# 直观理解:卷积操作就是"滑动窗口加权求和"
def conv2d_manual(image, kernel, stride=1, padding=0):
"""
手动实现2D卷积
image: 输入图像 (H, W)
kernel: 卷积核 (K, K),学习参数
"""
# 添加padding
if padding > 0:
image = np.pad(image, padding, mode='constant')
h, w = image.shape
k = kernel.shape[0]
out_h = (h - k) // stride + 1
out_w = (w - k) // stride + 1
output = np.zeros((out_h, out_w))
for i in range(0, out_h * stride, stride):
for j in range(0, out_w * stride, stride):
# 取局部区域并加权求和
region = image[i:i+k, j:j+k]
output[i//stride, j//stride] = np.sum(region * kernel)
return output
三、卷积的数学本质
3.1 一维卷积
在数学中,两个函数 f, g: R → R 的卷积定义为:
(f * g)(x) = ∫ f(z) · g(x-z) dz
直观理解:将函数 g "翻转"后,在位置 x 处与 f 的重叠程度。
import numpy as np
import matplotlib.pyplot as plt
def convolution_1d(f, g):
"""
计算一维卷积 (f * g)
积分 -> 求和,翻转操作通过索引实现
"""
n = len(f)
result = np.zeros(n)
for i in range(n):
for a in range(n):
if 0 <= i - a < n:
result[i] += f[a] * g[i - a] # g(index - a) 实现翻转
return result
# 示例信号
t = np.linspace(0, 1, 100)
f = np.sin(2 * np.pi * 5 * t) # 5Hz正弦波
g = np.exp(-10 * t) # 指数衰减
# 计算卷积
conv_result = convolution_1d(f, g)
3.2 二维卷积
对于图像(2D 信号),卷积推广为:
(f * g)(i, j) = Σₐ Σᵦ f(a,b) · g(i-a, j-b)
在深度学习中,我们通常使用互相关(cross-correlation):
(f ★ g)(i, j) = Σₐ Σᵦ f(i+a, j+b) · g(a,b)
PyTorch 的 nn.Conv2d 实际上计算的是互相关,但为了简洁,通常直接称为卷积。
import torch
import torch.nn as nn
# PyTorch 的卷积层
conv = nn.Conv2d(
in_channels=3, # 输入通道数(RGB图像=3)
out_channels=16, # 输出通道数(特征图数量)
kernel_size=3, # 卷积核大小 3×3
stride=1, # 步长
padding=1 # 填充,保持尺寸
)
# 输入: (batch_size, channels, height, width)
x = torch.randn(1, 3, 224, 224)
y = conv(x)
print(f"输入尺寸: {x.shape}")
print(f"输出尺寸: {y.shape}") # [1, 16, 224, 224]
四、沃尔玛检测器:卷积的直观理解
让我们通过一个具体例子理解卷积如何工作:
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt
# 创建一个小图像
image = torch.zeros(7, 7)
# 模拟一个简单的形状(十字)
image[3, 2:5] = 1
image[2:5, 3] = 1
# 定义卷积核(检测垂直边缘)
vertical_edge = torch.tensor([
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]
], dtype=torch.float32).unsqueeze(0)
# 应用卷积
output = F.conv2d(
image.unsqueeze(0).unsqueeze(0), # 添加batch和channel维度
vertical_edge.unsqueeze(0),
stride=1,
padding=1
)
print("卷积核(检测垂直边缘):")
print(vertical_edge[0])
print("\n卷积输出(边缘响应):")
print(output[0, 0])
4.1 卷积核的直观解释
| 卷积核类型 | 核矩阵 | 作用 |
|---|---|---|
| 锐化 | [[0,-1,0], [-1,5,-1], [0,-1,0]] |
增强细节 |
| 模糊 | 1/9 * [[1,1,1], [1,1,1], [1,1,1]] |
平滑噪声 |
| 垂直边缘 | [[-1,0,1], [-2,0,2], [-1,0,1]] |
检测垂直边缘 |
| 水平边缘 | [[-1,-2,-1], [0,0,0], [1,2,1]] |
检测水平边缘 |
五、通道:多维特征的表示
5.1 为什么需要通道?
现实世界的图像是三维的(高度 × 宽度 × 通道):
# RGB图像:张量形状 (3, H, W)
rgb_image = torch.randn(3, 224, 224)
# 多通道卷积层
conv_multi_channel = nn.Conv2d(
in_channels=3, # 输入3通道(RGB)
out_channels=16, # 输出16通道(特征图)
kernel_size=3
)
# 卷积核形状: (16, 3, 3, 3)
# 16个输出通道,每个通道对应3个输入通道的3×3卷积核
print(f"卷积核权重形状: {conv_multi_channel.weight.shape}")
5.2 多通道卷积公式
对于多通道输入 X ∈ R^(h×w×c_in) 和多通道输出:
H(i,j,d) = u(d) + Σₐ₌₋Δ^Δ Σᵦ₌₋Δ^Δ Σ꜀ V(a,b,c,d) · X(i+a,j+b,c)
其中:
- c:输入通道索引
- d:输出通道索引
- V ∈ R^((2Δ+1)×(2Δ+1)×c_in×c_out)
5.3 通道的语义意义
每一层的通道可以看作是对图像不同特征的响应:
# 可视化不同通道学到的特征
def visualize_channels(conv_layer, input_image):
"""
假设输入是一张包含多种元素的图像
不同通道可能专门响应不同特征:
- 通道0: 检测边缘
- 通道1: 检测纹理
- 通道2: 检测颜色
- ...
"""
with torch.no_grad():
activations = conv_layer(input_image)
# activations: (batch, channels, H, W)
# 每个通道都是对输入的空间响应图
return activations
六、从数学到实现
6.1 PyTorch 卷积层详解
import torch
import torch.nn as nn
class ConvLayerDemo(nn.Module):
"""
演示卷积层的各个参数
"""
def __init__(self):
super().__init__()
# 基础卷积
self.conv1 = nn.Conv2d(
in_channels=3, # RGB图像
out_channels=64, # 输出64个特征图
kernel_size=3, # 3×3卷积核
stride=1, # 步长1
padding=1, # 周围填充1像素
padding_mode='zeros' # 填充方式
)
# 大卷积核(感受野更大)
self.conv_large = nn.Conv2d(64, 64, kernel_size=7, padding=3)
# 空洞卷积(扩大感受野)
self.dilated_conv = nn.Conv2d(
64, 64, kernel_size=3,
padding=2, dilation=2 # 空洞率2
)
def forward(self, x):
x = self.conv1(x)
x = torch.relu(x)
return x
# 测试不同配置
model = ConvLayerDemo()
x = torch.randn(1, 3, 224, 224)
print(f"输入: {x.shape}")
# 查看卷积层参数数量
total_params = sum(p.numel() for p in model.parameters())
print(f"总参数量: {total_params:,}")
6.2 感受野的概念
感受野(Receptive Field):输出特征图上的一个像素能看到多大的输入区域。
def compute_receptive_field(layer_config):
"""
计算感受野
公式: RF = RF_prev + (kernel_size - 1) * product_of_strides
"""
rf = 1
stride_product = 1
for kernel_size, stride in layer_config:
rf = rf + (kernel_size - 1) * stride_product
stride_product *= stride
return rf
# 示例:三个3×3卷积层的感受野
# 第一层: RF = 1 + (3-1) * 1 = 3
# 第二层: RF = 3 + (3-1) * 1 = 5
# 第三层: RF = 5 + (3-1) * 1 = 7
print(f"三层3×3卷积的感受野: {compute_receptive_field([(3,1), (3,1), (3,1)])}")
print(f"一层7×7卷积的感受野: {compute_receptive_field([(7,1)])}")
七、全连接层 vs 卷积层
7.1 参数对比
| 方面 | 全连接层 | 卷积层 |
|---|---|---|
| 参数数量 | H_in × H_out | C_out × C_in × K × K |
| 权重共享 | 无 | 有(整个图像共享同一卷积核) |
| 空间结构 | 忽略 | 保留 |
| 平移不变性 | 无 | 有 |
| 适用数据 | 表格数据 | 图像、音频、序列 |
# 参数数量对比
def compare_parameters():
# 假设输入: 224×224×3
h_in, w_in, c_in = 224, 224, 3
# 全连接层(flatten后)
fc_params = h_in * w_in * c_in * 512
print(f"全连接层参数: {fc_params:,}")
# 卷积层
conv_params = 64 * c_in * 3 * 3
print(f"卷积层参数: {conv_params:,}")
print(f"\n参数减少比例: {fc_params / conv_params:.1f}x")
return fc_params, conv_params
compare_parameters()
7.2 何时使用哪种层?
class HybridNet(nn.Module):
"""
现代网络通常混合使用卷积层和全连接层
"""
def __init__(self, num_classes=1000):
super().__init__()
# 卷积层:处理图像,提取特征
self.features = nn.Sequential(
nn.Conv2d(3, 64, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2), # 空间尺寸减半
nn.Conv2d(64, 128, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(128, 256, 3, padding=1),
nn.ReLU(),
nn.AdaptiveAvgPool2d(1) # 全局平均池化
)
# 全连接层:分类(只在最后使用)
self.classifier = nn.Sequential(
nn.Dropout(0.5),
nn.Linear(256, num_classes)
)
def forward(self, x):
x = self.features(x)
x = torch.flatten(x, 1) # 展平
x = self.classifier(x)
return x
八、归纳偏置:CNN 的先验知识
8.1 什么是归纳偏置?
归纳偏置:学习算法对数据分布的先验假设。
卷积神经网络包含以下归纳偏置:
- 平移不变性:特征检测器对位置不敏感
- 局部性:只关注局部区域
- 层次化表示:从边缘到纹理到物体部件到完整物体
8.2 偏置的双刃剑
# 偏置的好处:样本效率高
# 因为有先验知识,网络不需要从头学习所有东西
# 偏置的代价:灵活性降低
# 如果数据不满足假设,性能可能下降
# 例子:ImageNet上的ResNet vs Transformer
# CNN在自然图像上表现好
# Transformer在小样本、分布外数据上可能更鲁棒
8.3 何时 CNN 可能不够好?
| 场景 | 问题 | 解决方案 |
|---|---|---|
| 旋转/缩放变换 | 平移不变但非旋转不变 | 数据增强、STN |
| 长距离依赖 | 局部性限制 | attention mechanism |
| 非结构化输入 | 无空间结构 | MLP-Mixer、Transformer |
九、代码实践:亲手实现卷积层
9.1 从零理解卷积
import torch
import torch.nn as nn
import numpy as np
def my_conv2d(image, kernel, stride=1, padding=0):
"""
纯Python实现2D卷积
image: (H, W) numpy数组
kernel: (K, K) numpy数组
"""
if padding > 0:
image = np.pad(image, padding, mode='constant')
h, w = image.shape
k = kernel.shape[0]
out_h = (h - k) // stride + 1
out_w = (w - k) // stride + 1
output = np.zeros((out_h, out_w))
for i in range(0, out_h * stride, stride):
for j in range(0, out_w * stride, stride):
# 取局部区域,加权求和
output[i//stride, j//stride] = np.sum(
image[i:i+k, j:j+k] * kernel
)
return output
# 测试
image = np.random.randn(5, 5)
kernel = np.array([[1, 0, -1], [2, 0, -2], [1, 0, -1]]) # 边缘检测
result = my_conv2d(image, kernel, padding=1)
print("自定义卷积结果:\n", result)
# 与PyTorch对比
t_image = torch.tensor(image, dtype=torch.float32).unsqueeze(0).unsqueeze(0)
t_kernel = torch.tensor(kernel, dtype=torch.float32).unsqueeze(0).unsqueeze(0)
t_result = torch.nn.functional.conv2d(t_image, t_kernel, padding=1)
print("\nPyTorch卷积结果:\n", t_result[0, 0].numpy())
9.2 可视化卷积操作
import matplotlib.pyplot as plt
def visualize_convolution():
"""可视化卷积操作"""
# 创建测试图像(棋盘格)
image = np.zeros((8, 8))
image[::2, ::2] = 1
image[1::2, 1::2] = 1
# 定义不同的卷积核
kernels = {
'原图': np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]]),
'水平边缘': 0.25 * np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]]),
'垂直边缘': 0.25 * np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]),
'锐化': np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]])
}
fig, axes = plt.subplots(1, len(kernels), figsize=(15, 4))
for ax, (name, kernel) in zip(axes, kernels.items()):
result = my_conv2d(image, kernel, padding=1)
ax.imshow(result, cmap='gray')
ax.set_title(name)
ax.axis('off')
plt.tight_layout()
plt.savefig('convolution_demo.png', dpi=150)
print("已保存卷积演示图")
visualize_convolution()
十、总结与展望
10.1 核心要点
┌─────────────────────────────────────────────────────────────┐
│ 从全连接层到卷积 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 问题:全连接层处理图像 → 参数爆炸,忽略空间结构 │
│ ↓ │
│ 解决方案:引入空间先验 │
│ ↓ │
│ 平移不变性 + 局部性 → 卷积层 │
│ ↓ │
│ 结果:参数减少 × 空间感知 ✓ │
│ │
└─────────────────────────────────────────────────────────────┘
10.2 CNN 的能力与局限
CNN 擅长的任务:
- 图像分类(ResNet、EfficientNet)
- 目标检测(YOLO、Faster R-CNN)
- 语义分割(U-Net、DeepLab)
- 图像生成(DCGAN、Pix2Pix)
需要额外技术的情况:
- 旋转/尺度变化 → 数据增强、STN
- 长距离依赖 → attention、non-local
- 3D 数据 → 3D CNN、PointNet
10.3 继续学习
卷积层只是开始,后续你将学习:
- 卷积层变体:空洞卷积、转置卷积、分组卷积
- 经典架构:LeNet、AlexNet、VGG、ResNet
- 现代模块:残差连接、注意力机制、特征金字塔
- 应用领域:目标检测、语义分割、实例分割
参考资源
- D2L 中文版:卷积神经网络 https://zh-v2.d2l.ai/chapter_convolutional-neural-networks/index.html
- PyTorch Conv2d 文档 https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html