4. 图像模态基础
图像模态,说白了就是让算法学会“看”。在多模态感知里,图像是最核心的信息源之一。而卷积神经网络(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上训练好的)。原因很简单:你手头的数据集通常不够大,从头训练容易过拟合。预训练模型已经学会了通用的视觉特征,你只需要微调或者直接提取。
提取特征的步骤:
- 加载预训练模型(去掉最后的全连接层)
- 输入图像,前向传播
- 取某一层的输出作为特征向量
# 使用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通过卷积、池化、激活的堆叠,从图像中提取层次化特征
- VGG用3×3卷积堆叠,结构简单但参数量大
- ResNet引入残差连接,解决了深层网络的退化问题
- 实际项目中,用预训练模型提取特征是最快的方式