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

19 KiB
Raw Permalink Blame History

从全连接层到卷积:深度学习中的空间智慧

引言

想象一下,当你在看一张照片时,你的眼睛会自动聚焦在局部区域——边缘、纹理、形状——然后将这些局部信息整合起来识别整体内容。卷积神经网络(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 什么是归纳偏置?

归纳偏置:学习算法对数据分布的先验假设。

卷积神经网络包含以下归纳偏置:

  1. 平移不变性:特征检测器对位置不敏感
  2. 局部性:只关注局部区域
  3. 层次化表示:从边缘到纹理到物体部件到完整物体

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 继续学习

卷积层只是开始,后续你将学习:

  1. 卷积层变体:空洞卷积、转置卷积、分组卷积
  2. 经典架构LeNet、AlexNet、VGG、ResNet
  3. 现代模块:残差连接、注意力机制、特征金字塔
  4. 应用领域:目标检测、语义分割、实例分割

参考资源