基础介绍
在计算机视觉神经网络中,输入一张图像根据功能的不同得到不同的输出,有几个术语相信大家都陌生:空间信息、通道数、尺度以及语义理解。那它们的含义是什么呢?它们的作用是什么呢?它们之间的关系是什么呢?本文重点讲述这三方面的内容。首先从概念入手。
- 尺度:这个很容易理解,比如一张图像的分辨率为224*224,这就是它的尺度,当这个分辨率经过卷积操作变为112*112或者其它数值时,就表示尺度发生了变化,所以尺度通俗意义就是图像的宽度和高度。当通过卷积操作提取特征时,得到的特征图的尺度一般都是越来越小。直白的说就是特征图的分辨率大小(高度和宽度)。
- 通道数:通道结合图像的通道去理解,一般的rgb图像的通道数为3,经过卷积操作后,通道数一般越来越多,例如卷积后通道变为64、128、256、512等。
- 空间信息:一般表示图像中的物体的位置信息,比如在一张图像中有教室、黑板、桌椅,讲台和窗户,图像中黑板位于教室的最前方,其次是桌椅,桌椅在教室整整齐齐的排列,窗户位于教室的两侧,讲台相对于桌椅所在地面高一些。空间信息的含义就是描述图像中物体的相对位置。官方说法是空间信息指特征图中像素的位置关系和局部结构信息,包括物体的形状、纹理、边缘等空间特征。
- 语义理解:这个是经过高度抽象化得到的,它的含义是图像中表达的是什么含义,图像中物体之间关系等,举个例子:图像中有几段话,语义理解可以认为是描述这些段落之间的关系。比如通过语义理解我们可以知道这个图像中物体是什么类别。
不同尺度的特点
大尺度特征图
分辨率:接近输入图像
特点:
- 保留细节信息
- 位置信息准确
- 计算量大
用途:目标检测、边缘检测等
小尺度特征图
分辨率:经过多次下采样
特点:
- 包含高级语义信息
- 感受野大
- 计算量小
用途:目标分类、场景理解
尺度变换
关于图像的尺度如何变换有多中方式:
- 尺度变小:卷积操作、池化
- 尺度变大:转置卷积
- 尺度不变,通道数变化:逐点卷积
卷积通过控制一些参数,诸如空洞卷积、步长、卷积核、填充来来实现对图像的各种变化,卷积可以做到下采样。
多尺度融合
多尺度融合的含义是将多个多个特征图融合为一个特征度,请看下面的例子:
class MultiScaleFusion(nn.Module):
def __init__(self):
super(MultiScaleFusion, self).__init__()
# 不同尺度的特征提取
self.scale1 = nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1)
self.scale2 = nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1)
self.scale3 = nn.Conv2d(64, 128, kernel_size=3, stride=4, padding=1)
# 特征融合
self.fusion = nn.Conv2d(384, 128, kernel_size=1)
def forward(self, x):
# 提取不同尺度特征
f1 = self.scale1(x)
f2 = self.scale2(x)
f3 = self.scale3(x)
# 统一尺度
f2 = F.interpolate(f2, size=f1.shape[2:])
f3 = F.interpolate(f3, size=f1.shape[2:])
# 融合
return self.fusion(torch.cat([f1, f2, f3], dim=1))
上面呢的代码在构造函数中定义了不同尺度的卷积层以及尺度融合层,他们都是通过卷积操作来实现的。在前向传播时首先调用不同的卷积层得到不同尺度的特征图,如上面的例子得到的都是128通道的不同尺度的特征图,要融合尺度,关键是统一尺度,那么统一尺度的思路是什么呢?特征的形状shape是[batch_size, channel, height, width],关键操作:
f2 = F.interpolate(f2, size=f1.shape[2:])
f3 = F.interpolate(f3, size=f1.shape[2:])
上面的语句使用 F.interpolate
函数对张量 f2
和f3进行插值操作,将其大小调整为与张量 f1
的空间维度(即高度和宽度)相同。 通过这个操作使f2和f3达到了和f1的shape相同的维度,最后通过torch.cat将f1 f2 f3在维度1上进行拼接,拼接后的形状变成了[batch_size, 384, height,width],最后调用卷积操作,生成一个特征图,特征图的形状为[batch_size, 128, height, width]。
多尺度融合的作用
通过上面的例子我个人觉的,多尺度融合后会将低尺度特征图的特征融合到大尺度的特征图中,从而在融合后的特征图中既有大尺度特征图的特征,也有低尺度特征图的特征。结合前面所讲的大尺度特征图和小尺度特征图的特点和用途,可能能够实现减少计算量、能够实现多种用途。
空间信息
空间信息的定义
空间信息指特征图中像素的位置关系和局部结构信息,包括物体的形状、纹理、边缘等空间特征。空间信息可以理解图像中物体的形状、纹理、边缘、相对位置等空间特征。
空间信息的简单理解
想象你在看一张图片:
空间信息就是物体在图像中的“位置”、“排列方式”和“相互关系”。比如猫在沙发上,沙发在房间左边,这些都是空间信息。
举个生活的例子:
看一张教室的照片:
- 黑板在前面(位置信息)
- 课桌整齐排列(排列关系)
- 讲台比课桌高(相对关系)
- 窗户在墙上(位置关系)
尺度、通道数、空间信息、语义信息的关系
先举一个例子,说明卷积操作后各个参数的变化:
输入图像: 224×224×3
↓ 第一次卷积
特征图1: 112×112×64
↓ 第二次卷积
特征图2: 56×56×128
↓ 第三次卷积
特征图3: 28×28×256
对上面的例子进行总结,得到如下规律:
- 尺度:逐渐减少,由224x224->112x112->56x56->28x28
- 通道数:逐层增加,3->64->128->256
- 空间信息:逐渐抽象,早期层保留细节,如(边缘、纹理);中期层,部分特征,如形状和部件;深层,抽象特征,如目标类别等。
通过上面的空间信息的变化可以看到,边缘纹理属于一些细节,中期层通过边缘和纹理特征得到物体的形状信息,但是丢失了边缘和纹理特征,从这里可以看出进行了一次升华。在深层,又再一次进行了升华,通过形状信息得到了更抽象的特征,即物体是什么类别。
举一个生活中看人的例子:
1.第一层(分辨率达,通道少)
- 尺寸大,看的清细节
- 通道少,只能识别基本特征(边缘、颜色)
- 空间信息,非常具体(眼睛在哪儿,鼻子嘴巴在哪儿)
2.中间层
- 尺寸变小,开始丢失一些细节
- 通道增多,能识别一些复杂的特征
- 空间信息,直到五官的相对位置
3.最后层
- 尺寸小,细节已经不能看清楚
- 通道多,可以识别一些高级特征
- 空间信息,知道这是一个人脸
上面的例子有些可能不太恰当,因为随着网络的加深,空间信息是不断被丢失的,取而代之的语义信息的增加。也可以认为是空间信息/语义信息是一回事,只是在不同的阶段表达的含义不同,如果这样理解的话也没有问题。
关系总结
- 尺寸↓ → 通道数↑
- 空间精确度↓ → 语义信息↑
总结可以得到:
- 尺度越小,一般情况下通道数会越多,尤其是在计算机视觉处理中
- 空间信息细节越少,则会换来语义信息的不断增加
理解这三者的关系非常重要,我们在不同的任务中要进行平衡取舍:
- 如果需要对物体进行精确的定位?保留更多的空间信息
- 如果需要语义理解?增加通道数和深度
- 如果需要多尺度检测?使用特征金字塔
很多情况下任务不仅仅是单一的,比如目标检测,不仅仅需要知道这是什么,同样也需要知道目标的位置;图像分割同样也是知道物体的类别和位置,这种情况下就需要在网络深度、通道数之间做一个平衡,过度追求深度会丢失空间信息,过度追求空间信息,则无法有效判断物体的类别。
上面的通道数、尺度大小、网络深度只是一些基础项,有时候为了完成任务往往还需要配合一些算法或者手段来实现这些基础项的平衡,如特征金字塔(FPN),跳跃连接、注意力机制、空洞卷积、多尺度融合等。
应用分析
目标识别
目标识别主要关注“这是什么”的问题,需要强大的语义理解能力,对位置信息要要求相对较低。结合上一章的总结内容可以的得出目标识别任务需要更深的网络深度和更多的通道数(增强特征提取能力)以获得更多的复杂特征。请看下面的例子:
class ClassificationNet(nn.Module):
def __init__(self, num_classes):
super(ClassificationNet, self).__init__()
# 特点:
# 1. 通道数逐渐增大:增强语义理解
# 2. 空间尺寸可以较小:位置信息不是重点
# 3. 网络较深:增强特征提取能力
self.features = nn.Sequential(
# 第一层保持较大空间尺寸
nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(3, stride=2),
# 快速增加通道数,减小特征图尺寸
self._make_layer(64, 128, stride=2),
self._make_layer(128, 256, stride=2),
self._make_layer(256, 512, stride=2),
# 全局池化,丢弃位置信息
nn.AdaptiveAvgPool2d((1, 1))
)
self.classifier = nn.Linear(512, num_classes)
def forward(self, x):
x = self.features(x)
x = torch.flatten(x, 1)
return self.classifier(x)
设计原则
设计重点:
- 通道数:快速增加(64→128→256→512)
- 空间信息:可以逐渐丢失
- 网络深度:较深(提取抽象特征)
- 特殊设计:全局池化层
目标检测
目标检测需要同时知道“是什么”和“在哪里”,需要平衡语义信息和空间信息,即在网络深度、通道数,尺度之间做一个平衡,尺度不能太小,通道数不能太多,也不能太少,网络深度也不宜太深。但是有时候我们单纯靠调整这些基础项可能业务获得理想的结果,这时候可能就需要依赖一些算法,比如跳跃连接、多尺度融合等方法实现融合大尺度特征信息和小尺度复杂特征信息来完成任务。请看下面的例子:
class DetectionNet(nn.Module):
def __init__(self, num_classes):
super(DetectionNet, self).__init__()
# 特点:
# 1. 使用FPN保持多尺度特征
# 2. 平衡通道数和空间信息
# 3. 使用空间注意力机制
# 主干网络
self.backbone = nn.Sequential(
self._make_layer(3, 64, stride=1), # 保持高分辨率
self._make_layer(64, 128, stride=2),
self._make_layer(128, 256, stride=2),
self._make_layer(256, 512, stride=2)
)
# 特征金字塔网络
self.fpn = FeaturePyramidNetwork([64, 128, 256, 512])
# 空间注意力
self.spatial_attention = SpatialAttention()
# 检测头
self.detection_head = DetectionHead(num_classes)
def forward(self, x):
# 提取多层特征
features = []
for layer in self.backbone:
x = layer(x)
features.append(x)
# FPN处理
fpn_features = self.fpn(features)
# 增强空间信息
enhanced_features = [
self.spatial_attention(f) for f in fpn_features
]
# 检测预测
return self.detection_head(enhanced_features)
class FeaturePyramidNetwork(nn.Module):
def __init__(self, in_channels):
super(FeaturePyramidNetwork, self).__init__()
self.lateral_convs = nn.ModuleList([
nn.Conv2d(in_ch, 256, 1)
for in_ch in in_channels
])
self.fpn_convs = nn.ModuleList([
nn.Conv2d(256, 256, 3, padding=1)
for _ in range(len(in_channels))
])
设计原则
设计重点:
- 通道数:平缓增加
- 空间信息:多尺度保留
- 网络深度:中等
- 特殊设计:FPN + 空间注意力
目标分割
目标分割需要最精确的位置信息,同时也需要良好的语义理解,从神经网络来上来说既需要大尺度的特征图,也需要复杂的特征,而复杂的特征往往需要更深的网络,更多的通道数来获取。这两个看上是矛盾的,但是任务就是这样的,这时候就需要用到一些算法来将他们融合起来,如跳跃连接。若UNET网络就是采用了跳跃连接的方式来实现的图像分割,但是unet网络虽然可以像素级别的分割,但是不能再同一个类别中进行分割。那需要更加复杂的网络。
class SegmentationNet(nn.Module):
def __init__(self, num_classes):
super(SegmentationNet, self).__init__()
# 特点:
# 1. 保持高分辨率特征图
# 2. 使用空洞卷积扩大感受野
# 3. 使用跳跃连接保留空间细节
# 4. 多尺度特征融合
# 编码器
self.encoder = nn.ModuleList([
# 保持较高分辨率
nn.Sequential(
nn.Conv2d(3, 64, 3, stride=1, padding=1),
nn.BatchNorm2d(64),
nn.ReLU()
),
# 使用空洞卷积而不是stride
nn.Sequential(
nn.Conv2d(64, 128, 3, dilation=2, padding=2),
nn.BatchNorm2d(128),
nn.ReLU()
),
nn.Sequential(
nn.Conv2d(128, 256, 3, dilation=4, padding=4),
nn.BatchNorm2d(256),
nn.ReLU()
)
])
# ASPP模块用于多尺度特征提取
self.aspp = ASPP(256)
# 解码器(带跳跃连接)
self.decoder = DecoderWithSkipConnections(
in_channels=[256, 128, 64],
out_channels=num_classes
)
def forward(self, x):
# 保存中间特征用于跳跃连接
features = []
# 编码过程
for enc_layer in self.encoder:
x = enc_layer(x)
features.append(x)
# ASPP处理
x = self.aspp(x)
# 解码过程(使用跳跃连接)
x = self.decoder(x, features[::-1])
return x
class ASPP(nn.Module):
def __init__(self, in_channels):
super(ASPP, self).__init__()
# 不同膨胀率的空洞卷积
self.aspp_blocks = nn.ModuleList([
nn.Conv2d(in_channels, 256, 1),
nn.Conv2d(in_channels, 256, 3, dilation=6, padding=6),
nn.Conv2d(in_channels, 256, 3, dilation=12, padding=12),
nn.Conv2d(in_channels, 256, 3, dilation=18, padding=18)
])
def forward(self, x):
res = []
for block in self.aspp_blocks:
res.append(block(x))
return torch.cat(res, dim=1)
设计原则
设计重点:
- 通道数:缓慢增加
- 空间信息:最大程度保留
- 网络深度:编码器-解码器结构
- 特殊设计:空洞卷积 + 跳跃连接
三种应用的分析对比
特征图变化对比:
1. 目标识别
Input (3, 224, 224)
→ (64, 112, 112) # 快速降低分辨率
→ (128, 56, 56)
→ (256, 28, 28)
→ (512, 14, 14)
→ (512, 1, 1) # 全局池化2. 目标检测
Input (3, 224, 224)
→ (64, 224, 224) # 保持原始分辨率
→ (128, 112, 112)
→ (256, 56, 56)
→ (512, 28, 28)
+ FPN特征金字塔 # 多尺度特征3. 目标分割
Input (3, 224, 224)
→ (64, 224, 224) # 保持高分辨率
→ (128, 224, 224) # 使用空洞卷积
→ (256, 224, 224)
+ 跳跃连接 # 保留空间细节
注意事项
上面提到的设计原则不是绝对的,可以根据具体的任务需求进行调整,现代网络设计往往会综合使用多种技术来获取最佳效果。