目录
参考链接:
一、ResNet
前言
1. 残差网络待解决的问题
2. ResNet模型亮点
ResNet模型结构
1. 残差学习
2. Residual模块
3. ResNet模型
二、FPN
图像算法的几种方法
FPN的目的
FPN
为什么FPN能够很好的处理小目标?
三、ResNet+FPN
四、ResNet+FPN实现代码
参考链接:
【DL系列】ResNet网络结构详解、完整代码实现
ResNet+FPN实现+白嫖代码
详解FPN网络
什么是FPN(Feature Pyramid Networks--特征金字塔)?
一、ResNet
前言
1. 残差网络待解决的问题
网络退化
随着网络深度的增加,网络能获取的信息量随之增加,而且提取的特征更加丰富。但是在残差结构提出之前,根据实验表明,随着网络层不断的加深,模型的准确率起初会不断的提高,达到最大饱和值,然后随着网络深度的继续增加,模型准确率不但不会继续增加,反而会出现大幅度降低现象,即模型训练过程和测试过程的error比浅层模型更高。这是由于之前的网络模型随着网络层不断加深会造成梯度爆炸和梯度消失的问题。
2. ResNet模型亮点
提出Residual模块使用Batch Normalization加速训练(丢弃dropout)残差网络:易于收敛、很好的解决了退化问题、模型可以很深,准确率大大提高
ResNet模型结构
1. 残差学习
残差结构1
残差结构2
残差网络是一种非常有效的缓解梯度消失问题网络,极大的提高了可以有效训练的网络的深度。
残差结构1: 原论文将输入X(即input)经过一系列处理之后得到残差F(X),若是在shortcut分支上不经过downsample处理(即不经过conv1x1卷积+BN处理),则在最终得到的残差映射函数为F(X) + X,此种结构一般用在conv_x组块中的非第一层之后的层,即用在不需要改变前后输入输出维度的层。
残差结构2: 残差结构2提到的类似这种需要在shortcut分支上进行downsample处理的结构,一般用在每个conv_x组块的第一层中,即上一层的输出out_channel不符合此层所需要的in_channel,此时需要用conv1x1卷积进行升维操作,此时得到的残差映射函数为F(X) + G(X),G(X)为shortcut分支上对输入X进行处理后得到的恒等映射。
2. Residual模块
原论文Residual结构
Residual详细结构
Residual结构分为两种,BasicBlock和BottleNeck。 BasicBlock应用与ResNet-18、ResNet-34模型,BottleNeck应用与ResNet-50、ResNet-101、ResNet-152模型。
两种结构中,应用downsample的shortcut分支,即在上文提到的在每个conv_x组块中的第一层,此时上一层的输入out_channel 不等于此层所需要的in_channel,因此通过downsample进行维度调整。虚线shortcut分支,即在conv_x组块中经过第一层的downsample调整后in_channel = out_channel,此组块conv_x中的后续层不再需要调整channel_size,因此可以直接恒等映射X(即input)。
3. ResNet模型
ResNet-18、Res-50模型结构
原论文ResNet-layer模型
ResNet-layer中所有模型的大致结构都基本相似,不同layer的模型区别在于选择使用BasicBlock还是BottleNeck,其次就是在每个Conv_x组块中使用Residual的个数。
上图中ResNet-18和ResNet-50的模型中通道数的变化、图像尺寸的变化、是否使用downsample均已标出,可与原论文中给出的具体ResNet_layer模型进行相互对照学习。其它layer模型与这两个模型基本一致。
二、FPN
图像算法的几种方法
为了能最多的检测出图像上的大小目标,图像算法有一下几种方法:
(a)特征图像金字塔:生成不同尺寸的图片,构成图像金字塔,对图像金字塔的每一层提取特征,每张图片生成不同的特征,分别进行预测,最后统计所有尺寸的预测结果。只要在任意层检测到了目标,都算检测成功。但是,在实际应用中,基于神经网络的方法本身就非常耗时,如果再使用多个尺度的图像特征进行训练和测试,时间和内存的开销就更大了,因此这种方法很少被真正使用。
(b)feature map:卷积神经网络本身也具有金字塔结构,在特征图的不同层上,同样大小的检测窗口在原图的感受野的尺度是不同的。使用神经网络某一层输出的feature map进行预测,一般是网络最后一层feature map(例如Fast R-CNN、Faster R-CNN等);然而靠近网络输入层的feature map提取低级特征,如边缘、纹理和简单的形状,包含粗略的位置信息,更关注于细节和位置信息,可以进行位置细化;靠近最后网络最后一层的feature map更关注于语义信息,更有能力提取高级特征,如抽象的形状、模式和语义信息,而高层的语义信息能够帮助我们准确的检测出目标,因此我们可以利用最后一个卷积层上的feature map来进行预测。
(c)特征金字塔:使用不同层次的金字塔层feature map进行预测。可以同时使用不同层特征图检测不同尺度的目标,这是因为我们的一幅图像中可能具有多个不同大小的目标,区分不同的目标可能需要不同的特征,对于简单的目标我们仅仅需要浅层的特征就可以检测到它,对于复杂的目标我们就需要利用复杂的特征来检测它。SSD方法采用的就是这个思路,从网络不同层抽取不同尺寸的特征做预测,没有增加额外的计算量。但这也存在一个问题,特征图不同层次特征的表达能力不同,浅层特征主要反映明暗、边缘等细节,深层特征则反映更丰富的整体结构。单独使用浅层特征是无法包含整体结构信息的,会减弱特征的表达能力。
因为深层特征本身就是由浅层特征构建的,所以天然包含了浅层特征的信息,一个很自然的想法是,如果再把深层特征融合到浅层特征中,就兼顾了细节和整体,融合后的特征会具有更为丰富的表达能力。基于这种思想,特征金字塔网络就应运而生了。
(d)特征金字塔网络:在特征金字塔上选取若干层,这些层本身构成了一个由浅到深的层次关系,再把深层特征逐级向浅层合并,就构成了一个新的特征金字塔,这个新金字塔的每一层都融合了浅层和深层的信息,分别应用每一层的特征进行检测,就达到了检测不同尺度目标的目的。
FPN的目的
熟悉faster rcnn的人知道,faster rcnn利用的是vgg的最后的卷积特征,大小是7x7x512。而这造成了一个问题,经过多次卷积之后的特征通常拥有很大的感受野,它们比较适合用来检测大物体,或者说,它们在检测小物体任务上效果很差,所以像ssd和fpn这样的网络思想就是将前面和后面的的卷积层都拿出来,组成一个multisacle结果,既能检测大物体,又能检测小物体。
基于这个思想,fpn从ResNet 34层模型构造了一组新的特征,p2,p3,p4,p5,每一个pi都是ResNet中不同卷积层融合的结果,这保证了他们拥有多尺度信息。他们拥有相同的维度,都是256。
FPN
如下图,上半部分只取特征金字塔的最底层进行预测,下半部分对特征金字塔的所有层进行单独预测,最后再整合所有预测结果,FPN主要使用后者,使用多特征层进行预测。
FPN官方的backbone是ResNet。CNN的前馈计算就是自下而上的路径,特征图经过卷积核计算,通常是越变越小的,也有一些特征层的输出大小和输入大小一样。
思想:把高层的特征传下来,补充低层的语义,这样就可以获得高分辨率、强语义的特征,有利于小目标的检测。
特征金字塔网络包括自底向上、自顶向下和横向连接。
自底向上——resnet特征提取(feature map) 这是神经网络的前向计算,就是由卷积和池化层组成的特征提取网络。在这个自底向上的结构中,一个stage对应特征金字塔的一个level。对于以ResNet为backbone的主干网络,选取conv2、conv3、conv4、conv5层的最后一个残差block层特征作为FPN的特征,记为{C2、C3、C4、C5},也即是FPN网络的4个级别。这几个特征层相对于原图的步长分别为4、8、16、32。
自上向下 自上向下是前向计算后将输出的特征图放大的过程,我们一般采用upsample(上采样)来实现。
侧向连接——特征融合
FPN的巧妙之处就在于从高层特征上采样不仅可以利用顶层的高语义、低分辨率信息(有助于分类),而且利用浅层的、低语义、高分辨率信息(有助于定位)。为了将上述两者相结合,论文提出了类似于残差结构的侧向连接。向连接将上一层经过上采样后和当前层分辨率一致的特征,通过相加的方法(如:pytorch的torch.cat或torch.add)进行融合。同时为了保持所有级别的特征层通道数都保持一致,这里使用1*1卷积来实现。
注:横向连接的两层特征在空间尺寸上要相同,主要是为了利用底层的定位细节信息(由于底部的feature map包含更多的定位细节,而顶部的feature map包含更多的目标特征信息)。
特征金字塔的横向连接和自顶向下的连接示意图如下,其中 1×1 conv用来改变feature map的通道数:
具体的,C5层先经过1x1卷积,得到M5特征。M5通过上采样,再加上C4经过1x1卷积后的特征,得到M4。这个过程再做两次,分别得到M3和M2。M层特征再经过3x3卷积,得到最终的P2、P3、P4、P5层特征。另外,和传统的图像金字塔方式一样,所有M层的通道数都设计成一样的,本文都用d=256。细节图如下所示(以ResNet为例):
FPN本身不是检测算法,只是一个特征提取器。它需要和其他检测算法结合才能使用。
为什么FPN能够很好的处理小目标?
如上图所示,FPN能够很好地处理小目标的主要原因是:
FPN可以利用经过top-down模型后的那些上下文信息(高层语义信息);对于小目标而言,FPN增加了特征映射的分辨率(即在更大的feature map上面进行操作,这样可以获得更多关于小目标的有用信息),如图中所示;
三、ResNet+FPN
Resnet+FPN的简单结构如下图所示:
bottom-up就是简单的使用了ResNet34,主要是top-down中的思想。 在上文中我们提到c2-c5的大小和维度分别是56x56x64,28x28x128,14x14x256,7x7x512,所以在top-down中,先用了一个1x1x256的卷积将c5:7x7x512 变成了m5:7x7x256, 每一个m之后都接了一个3x3x256卷积用来消除不同层之间的混叠效果,其实也就是缓冲作用。 关于p4的构造,我们先将m5的feature map加倍,用简单的nearest neighbour upsamping方法就行,这样m5就变成了m5’:14x14x256,同时c4:14x14x256经过1x1x256得到c4’:14x14x256, 将m5’+c4’, element-wisely,就可以得到m4:14x14x256。 … 所以最后的p2-p5大小分别是 56x56x256。,28x28x256,14x14x256,7x7x256。
下图是具体的FPN网络,使用了Resnet-50的Backbone:
四、ResNet+FPN实现代码
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
class Bottleneck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, stride=stride, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = nn.Conv2d(planes, planes * 4, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(planes * 4)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
class FPN(nn.Module):
def __init__(self, block, layers):
super(FPN, self).__init__()
self.in_planes = 64
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
# Bottom-up layers
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
# Top layer
self.toplayer = nn.Conv2d(2048, 256, kernel_size=1, stride=1, padding=0) # Reduce channels
# Smooth layers
self.smooth1 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
self.smooth2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
self.smooth3 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
# Lateral layers
self.latlayer1 = nn.Conv2d(1024, 256, kernel_size=1, stride=1, padding=0)
self.latlayer2 = nn.Conv2d( 512, 256, kernel_size=1, stride=1, padding=0)
self.latlayer3 = nn.Conv2d( 256, 256, kernel_size=1, stride=1, padding=0)
def _make_layer(self, block, planes, blocks, stride=1):
downsample = None
if stride != 1 or self.in_planes != planes * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.in_planes, planes * block.expansion,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(planes * block.expansion),
)
layers = []
layers.append(block(self.in_planes, planes, stride, downsample))
self.in_planes = planes * block.expansion
for i in range(1, blocks):
layers.append(block(self.in_planes, planes))
return nn.Sequential(*layers)
def _upsample_add(self, x, y):
_,_,H,W = y.size()
return F.interpolate(x, size=(H,W), mode='bilinear',align_corners=True) + y
def forward(self, x):
# Bottom-up
c1 = F.relu(self.bn1(self.conv1(x)))
c1 = F.max_pool2d(c1, kernel_size=3, stride=2, padding=1)
#print(f'c1:{c1.shape}')
c2 = self.layer1(c1)
#print(f'c2:{c2.shape}')
c3 = self.layer2(c2)
#print(f'c3:{c3.shape}')
c4 = self.layer3(c3)
#print(f'c4:{c4.shape}')
c5 = self.layer4(c4)
#print(f'c5:{c5.shape}')
# Top-down
p5 = self.toplayer(c5)
#print(f'p5:{p5.shape}')
p4 = self._upsample_add(p5, self.latlayer1(c4))
#print(f'latlayer1(c4):{self.latlayer1(c4).shape}, p4:{p4.shape}')
p3 = self._upsample_add(p4, self.latlayer2(c3))
#print(f'latlayer1(c3):{self.latlayer2(c3).shape}, p3:{p3.shape}')
p2 = self._upsample_add(p3, self.latlayer3(c2))
#print(f'latlayer1(c2):{self.latlayer3(c2).shape}, p2:{p2.shape}')
# Smooth
p4 = self.smooth1(p4)
p3 = self.smooth2(p3)
p2 = self.smooth3(p2)
return p2, p3, p4, p5
def FPN101():
# return FPN(Bottleneck, [2,4,23,3])
return FPN(Bottleneck, [3,4,6,3])
def test():
net = FPN101()
fms = net(Variable(torch.randn(1,3,224,224)))
for fm in fms:
print(fm.size())
相关阅读
发表评论