4、图像模态基础:卷积神经网络(CNN)原理、经典架构(ResNet、VGG)、图像特征提取实践

图像模态,说白了就是让算法学会“看”。在多模态感知里,图像是最核心的信息源之一。而卷积神经网络(CNN),就是目前处理图像最成熟的一套工具。我刚开始接触这个领域时,总觉得CNN是个黑盒子,后来亲手调过几个模型,才慢慢摸清它的脾气。

4.1 卷积神经网络的核心原理

CNN为什么适合图像?因为它抓住了图像的局部性和平移不变性。一张猫的图片,不管猫在左边还是右边,CNN都能认出它是猫。这就是平移不变性——靠卷积核在整张图上滑动来实现。

4.1.1 卷积层:特征提取的“扫描仪”

卷积层做的事情很简单:用一个小的窗口(卷积核)在图像上滑动,每次计算窗口内像素的加权和。这个加权和就是特征图上的一个点。

关键参数:

  • 卷积核大小(Kernel Size):常用3×3或5×5。3×3是业界标配,计算量小,堆叠多层后感受野足够大。
  • 步长(Stride):每次滑动的像素数。步长=2时,特征图尺寸减半。
  • 填充(Padding):在图像边缘补0,保持输出尺寸不变。我习惯用“same”填充,省去算尺寸的麻烦。

举个例子,输入一张224×224的RGB图像,用64个3×3卷积核,步长1,same填充,输出就是224×224×64的特征图。每个卷积核学习一种模式——有的学边缘,有的学纹理,有的学颜色。

4.1.2 池化层:降维与抗干扰

池化层的作用是下采样。最大池化(Max Pooling)取窗口内的最大值,平均池化(Average Pooling)取平均值。我个人更常用最大池化,因为它能保留最显著的特征,对噪声不敏感。

为什么需要池化?

  • 减少参数量,防止过拟合
  • 扩大感受野,让高层看到更大的区域
  • 提供一定的平移不变性

我的经验:在早期网络(如VGG)中,池化层很关键。但在ResNet等现代架构中,我经常用步长为2的卷积代替池化,效果更稳定。

4.1.3 全连接层与激活函数

卷积和池化提取的是空间特征,全连接层负责把这些特征映射到最终的分类结果。激活函数引入非线性——ReLU是最常用的,计算快,能缓解梯度消失。

ReLU的公式很简单:f(x) = max(0, x)。负值直接变0,正值保留。我在项目中遇到过一个问题:ReLU会让部分神经元“死亡”(永远输出0)。后来改用Leaky ReLU,情况好了一些。

4.2 经典架构:VGG与ResNet

讲完原理,咱们看看两个里程碑式的架构。VGG教会我们“深”的重要性,ResNet解决了“太深”的问题。

4.2.1 VGG:堆叠的力量

VGG的核心思想很简单:全部用3×3卷积,堆叠更多层。VGG-16有16个权重层,VGG-19有19个。别看它结构简单,效果在当时是顶尖的。

层名 VGG-16配置 输出尺寸
Conv1 2×[3×3, 64] 224×224×64
Pool1 2×2最大池化 112×112×64
Conv2 2×[3×3, 128] 112×112×128
Pool2 2×2最大池化 56×56×128
Conv3 3×[3×3, 256] 56×56×256
Pool3 2×2最大池化 28×28×256
Conv4 3×[3×3, 512] 28×28×512
Pool4 2×2最大池化 14×14×512
Conv5 3×[3×3, 512] 14×14×512
Pool5 2×2最大池化 7×7×512
FC 4096→4096→1000 1×1×1000

避坑指南:VGG的参数量很大(约1.38亿),训练时显存消耗惊人。我曾经在GTX 1080上跑VGG-16,batch size只能设到32。如果你资源有限,建议先用轻量级网络做实验。

4.2.2 ResNet:残差学习解决退化

按理说,网络越深效果越好。但实际发现,层数超过一定深度后,准确率反而下降——这不是过拟合,是退化问题。ResNet用残差学习解决了这个问题。

残差块的核心思想:让网络学习F(x) = H(x) - x,而不是直接学习H(x)。这样即使梯度回传时变弱,也能通过恒等映射x直接传递。

# 一个简单的残差块实现(PyTorch风格)
class BasicBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        
        # 如果输入输出通道数不同,用1×1卷积调整
        self.shortcut = nn.Sequential()
        if stride != 1 or in_channels != out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, 1, stride, bias=False),
                nn.BatchNorm2d(out_channels)
            )
    
    def forward(self, x):
        out = self.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out += self.shortcut(x)  # 残差连接
        out = self.relu(out)
        return out

ResNet的常见变体有ResNet-18、ResNet-34、ResNet-50、ResNet-101。数字越大,层数越深。我在实际项目中,ResNet-50用得最多——效果和速度的平衡点。

4.3 图像特征提取实践

理论讲完了,咱们动手。特征提取是CNN最直接的应用——把图像变成向量,供下游任务使用。

4.3.1 使用预训练模型提取特征

我建议直接用预训练模型(在ImageNet上训练好的)。原因很简单:你手头的数据集通常不够大,从头训练容易过拟合。预训练模型已经学会了通用的视觉特征,你只需要微调或者直接提取。

提取特征的步骤:

  1. 加载预训练模型(去掉最后的全连接层)
  2. 输入图像,前向传播
  3. 取某一层的输出作为特征向量
# 使用ResNet-50提取图像特征
import torch
import torchvision.models as models
from torchvision import transforms
from PIL import Image

# 加载预训练模型,去掉最后的全连接层
model = models.resnet50(pretrained=True)
model = torch.nn.Sequential(*list(model.children())[:-1])  # 去掉fc层
model.eval()

# 图像预处理
transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# 加载图像并提取特征
img = Image.open('cat.jpg')
img_tensor = transform(img).unsqueeze(0)  # 增加batch维度

with torch.no_grad():
    features = model(img_tensor)  # 输出形状: [1, 2048, 1, 1]
    feature_vector = features.squeeze()  # 变成2048维向量

print(f"特征向量维度: {feature_vector.shape}")

我的习惯:提取特征时,我通常取倒数第二层(全局平均池化后的输出)。这一层既保留了高层语义,又去掉了空间位置信息,适合做图像检索或分类。

4.3.2 特征可视化:看看CNN学到了什么

光提取特征还不够,你得知道模型关注图像的哪些区域。Grad-CAM是一种常用的可视化方法——通过梯度计算,生成热力图,显示模型决策时关注的区域。

举个例子,输入一张猫的图片,Grad-CAM会高亮猫的脸部、耳朵等关键部位。如果高亮区域在背景上,说明模型学歪了——我在项目中遇到过这种情况,后来通过数据增强解决了。

4.3.3 实践中的常见问题

  • 输入尺寸不一致:不同模型要求的输入尺寸不同(VGG要224×224,Inception要299×299)。统一用Resize+CenterCrop处理。
  • 归一化参数:ImageNet的均值和标准差是固定的(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225])。用预训练模型时,必须用这套参数,否则特征会偏移。
  • Batch Normalization:提取特征时,记得把模型设为eval模式,否则BN层的行为会变。

4.4 本章知识结构图

下面这张图总结了CNN的核心流程和经典架构的关系。你看一眼就能明白:输入图像经过卷积、池化、激活的反复堆叠,最终通过全连接层输出结果。VGG和ResNet的区别在于残差连接——ResNet在卷积块之间加了跳跃连接。

CNN知识结构图 CNN核心流程与经典架构 输入图像 卷积层 + ReLU + 池化层 (重复多次,提取层次化特征) 高层特征图 全连接层 → 分类/回归 VGG:堆叠3×3卷积 ResNet:残差连接 经典架构 关键区别 VGG:简单堆叠,参数量大 ResNet:残差块,可训练更深 特征提取:取倒数第二层输出 可视化:Grad-CAM热力图

本章要点:

  • CNN通过卷积、池化、激活的堆叠,从图像中提取层次化特征
  • VGG用3×3卷积堆叠,结构简单但参数量大
  • ResNet引入残差连接,解决了深层网络的退化问题
  • 实际项目中,用预训练模型提取特征是最快的方式