Files
Obsidian/博客/机器学习/从全连接层到卷积.md

650 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 从全连接层到卷积:深度学习中的空间智慧
## 引言
想象一下,当你在看一张照片时,你的眼睛会自动聚焦在局部区域——边缘、纹理、形状——然后将这些局部信息整合起来识别整体内容。卷积神经网络(CNN)正是模仿了这种视觉系统的运作方式。让我带你从数学原理出发,理解为什么卷积层比全连接层更适合处理图像数据。
## 一、全连接层的困境
### 1.1 表格数据的成功
在之前的章节中,我们已经看到多层感知机(MLP)在处理表格数据时表现出色:
```python
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 高维图像的灾难
然而,当面对高维图像数据时,全连接层的参数数量会爆炸式增长:
```python
# 假设: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) ║
╚═══════════════════════════════════════════════════════════════════╝
```
**这就是卷积层!** 从数十亿参数减少到几百个参数。
```python
# 直观理解:卷积操作就是"滑动窗口加权求和"
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 的重叠程度。
```python
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` 实际上计算的是互相关,但为了简洁,通常直接称为卷积。
```python
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]
```
## 四、沃尔玛检测器:卷积的直观理解
让我们通过一个具体例子理解卷积如何工作:
```python
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 为什么需要通道?
现实世界的图像是三维的(高度 × 宽度 × 通道):
```python
# 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 通道的语义意义
每一层的通道可以看作是对图像不同特征的响应:
```python
# 可视化不同通道学到的特征
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 卷积层详解
```python
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):输出特征图上的一个像素能看到多大的输入区域。
```python
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 |
| 权重共享 | 无 | 有(整个图像共享同一卷积核) |
| 空间结构 | 忽略 | 保留 |
| 平移不变性 | 无 | 有 |
| 适用数据 | 表格数据 | 图像、音频、序列 |
```python
# 参数数量对比
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 何时使用哪种层?
```python
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 什么是归纳偏置?
**归纳偏置**:学习算法对数据分布的先验假设。
卷积神经网络包含以下归纳偏置:
1. **平移不变性**:特征检测器对位置不敏感
2. **局部性**:只关注局部区域
3. **层次化表示**:从边缘到纹理到物体部件到完整物体
### 8.2 偏置的双刃剑
```python
# 偏置的好处:样本效率高
# 因为有先验知识,网络不需要从头学习所有东西
# 偏置的代价:灵活性降低
# 如果数据不满足假设,性能可能下降
# 例子:ImageNet上的ResNet vs Transformer
# CNN在自然图像上表现好
# Transformer在小样本、分布外数据上可能更鲁棒
```
### 8.3 何时 CNN 可能不够好?
| 场景 | 问题 | 解决方案 |
|------|------|----------|
| 旋转/缩放变换 | 平移不变但非旋转不变 | 数据增强、STN |
| 长距离依赖 | 局部性限制 | attention mechanism |
| 非结构化输入 | 无空间结构 | MLP-Mixer、Transformer |
## 九、代码实践:亲手实现卷积层
### 9.1 从零理解卷积
```python
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 可视化卷积操作
```python
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 继续学习
卷积层只是开始,后续你将学习:
1. **卷积层变体**:空洞卷积、转置卷积、分组卷积
2. **经典架构**LeNet、AlexNet、VGG、ResNet
3. **现代模块**:残差连接、注意力机制、特征金字塔
4. **应用领域**:目标检测、语义分割、实例分割
## 参考资源
- D2L 中文版:卷积神经网络
https://zh-v2.d2l.ai/chapter_convolutional-neural-networks/index.html
- PyTorch Conv2d 文档
https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html