【Neck】DLA

编程入门 行业动态 更新时间:2024-10-07 14:25:22

【Neck】<a href=https://www.elefans.com/category/jswz/34/1765255.html style=DLA"/>

【Neck】DLA

DLA

DLA 全称 Deep Layer Aggregation, 发表在2018年CVPR上,属于neck的一种特征融合方法,准确度和模型复杂度平衡的很好

论文:Deep_Layer_Aggregation
github:官方实现,CenterNet实现, FairMOT

一、 背景

Aggretation 聚合是目前设计网络结构常用的一种结构。

如何将不同深度,不同stage,block之间的信息进行融合是一个探索的目标。

目前常见的聚合方式有

1、块内聚合skip connection和dense connection, 如ResNet,DenseNet。
这种连接的优点是可以聚合语义信息
但这种融合方式仅限于块内部,并且融合方式仅限于简单叠加。
2、块外聚合, 如Feature Pyramids
这种连接的有点是可以聚合空间信息。(从不同深度的特征图提取的信息也不一样)

如下图所示,DLA结合了这两者的优点,从而可以更好的获取what和where的信息

二、 不同的聚合方式

如下图所示,作者实验了几种不同的聚合方式,结果表明设计的DLA有效。

a 无聚合

  • 没有聚合块,这是分类和回归网络的默认配置

b stage浅聚合

  • 具有简单的聚合功能,只将前一个步骤的特征进行聚合,通常用于检测和分类网络

c stage浅深度聚合(Iterative Deep Aggregation, IDA)
具有简单的深度聚合功能,将最浅层特征聚合最多以提取出最多信息

d 树结构block深度聚合
通过块构成树形结构分层聚合以进行更多特征聚合处理。这里分为是否断开底层块连接。d为保存底层块连接,

e 和f断开了底层块连接,只在次底层块之间进行连接,这会传播所有先前聚合的块,而不是单独传播前一个块,以更好的提取保留特征。

f 的原型感觉还是skipnet,这个合并了相同深度的节点(垂直线上节点)

作者为DLA引入迭代深度聚合(Iterative Deep Aggregation, IDA)和分层深度聚合(Hierarchical Deep Aggregation, HDA).
IDA-stage层次融合,专注于融合分辨率和比例
HDA-block层次融合,专注于合并来自所有模块和通道的功能。

三、DLA模块

IDA

IDA聚合,如图2-c所示,从最浅最小的尺度开始,然后迭代地合并更深、更大的尺度。通过这种方式,浅层特征在它们通过不同的聚合阶段传播时被细化。

这里I表示IDA函数, x n x_n xn​表示第n层网络,N是聚合节点。
虽然IDA有效的组合了Stage,但它不足以融合网络的许多块,因为它仍然只是顺序的.
IDA聚合进行插值,阶段从浅到深融合在一起,形成一个越来越深和更高分辨率的解码器。示意图如下

用于插值的IDA通过投影和上采样来增加深度和分辨率。
IDA在框架中会用到两次-第一次连接骨干网络中的各个阶段,另一次用于恢复分辨率

HDA

HDA合并树中的块和阶段来保留和组合特征通道,他将更浅和更深的层结合起来,来学习更丰富的组合,跨越更多的特征层次结构。
使用HDA,从任何块到根的最短路径至多是层次结构的深度,因此延聚合路径可能不会出现梯度递减或爆炸。

总框架示意图如下:

不要被这里方块和颜色迷惑, 强烈推荐去看代码,DLA34推荐看(e)理解。

一个树类定义了左右子树和聚合节点(ROOT)、预处理和后处理,因此下采样是在树内进行的,在HDA块之间是没有进行下采样的。

根树没有root节点

这里HDA2即level4,根树没有root节点,图上维度较高的只是右子树的根节点,有四个输入进右子树root进行聚合,其中黄色为上一层聚合节点输入(同样也是下采样的64),剩下三个均为本层子树输入,左边输入是左子树的聚合输入,中间输入是右子树的左叶子节点输出,右边输入是右子树的右叶子节点输出。

四、 代码实现

DLA模块包含:

  • 基础块:由BasicBlock、Bottleneck及BottleneckX组成
  • 聚合块:由基础块组成的HDA树型结构
  • stage块:由HDA结构的各个stage组合而成

基础模块

这三个模块都是ResNet里的基础块

DLA-34调用的是BasicBlock,其余均调用Bottleneck及BottleneckX
是树型结构中的block

聚合节点ROOT

ROOT是连接两个树的根节点,如下图绿色方块
DLA-Aggretation_Node:

在树中被调用,输入是多个层的输出,用来聚合多个层的信息

HDA树型结构

树型结构Tree-HDA聚合方式组成

Tree类对应图中的HDA模块,是最核心复杂的地方。其核心就是递归调用Tree类的构建。

树型结构分为左右子节点和根节点。

没个树类都会记录当前level、根节点的维度root_dim,是否是根节点层level_root

其左右子节点根据level的数量来划分为是基础块还是子树。

当level=1时,为叶子节点即基础块。因此此时只有两层结构,底层为叶子节点。顶层为根节点

if levels == 1:  # 当levels=1时只有block 基础网络self.tree1 = block(in_channels, out_channels, stride, dilation=dilation)self.tree2 = block(out_channels, out_channels, 1, dilation=dilation)if levels == 1:self.root = Root(root_dim, out_channels, root_kernel_size, root_residual)

当level>1时,为左右子树。所以这里是有一个递归结构,将level-1作为左右子树的level直至level=1递归到叶子节点。

if level > 1:self.tree1 = Tree(levels - 1, block, in_channels, out_channels,stride, root_dim=0,root_kernel_size=root_kernel_size,dilation=dilation, root_residual=root_residual)self.tree2 = Tree(levels - 1, block, out_channels, out_channels,root_dim=root_dim + out_channels,root_kernel_size=root_kernel_size,dilation=dilation, root_residual=root_residual)

self.project是起到转变维度的作用,当输入输出通道不相同的时候来变换。

if in_channels != out_channels:self.project = nn.Sequential(nn.Conv2d(in_channels, out_channels,kernel_size=1, stride=1, bias=False),BatchNorm(out_channels))

在forward里,还有一个参数children,这里children是一个list来保存所有传给root的成员即聚合节点,即根节点的下一层,由底层向上聚合。

这里举两个例子:

layers=[1,1,1,2,2,1], channels = [16,32,64,128,256,512]

从第三层开始为树型结构
第三层代码构建如下:

self.level2 = Tree(levels[2], block, channels[1], channels[2], 2,
level_root=False, root_residual=residual_root)

其中,layers[2]=1, level_root=False,root_residual=False,stride=2
因为第三层只有叶节点作为输入,所以此时level_root设置为False, 当设置为True时会接受其他HDA的聚合输入,root_residual 为设置根节点是否为残差连接。
对应框架图中下图的情况:

DLA-Aggretation_Node-level3:

这里在Tree类的forward推理情况就变成了如下:

stride=2 -> self.downsample = nn.MaxPool2d(stride, stride=stride)
inchannels=32, out_channels=64 ->self.project = nn.Sequential(
nn.Conv2d(in_channels, out_channels,
kernel_size=1, stride=1, bias=False),
BatchNorm(out_channels))
level_root = False
levels = 1 - > x = self.root(x2, x1, *children)## 没有子树聚合输入,使用最大池化层进行2倍下采样,使用卷积层进行2倍上采样
def forward(self, x, residual=None, children=None):
children = []
bottom = self.downsample(x)
residual = self.project(bottom)
x1 = self.tree1(x, residual)
x2 = self.tree2(x1)
x = self.root(x2, x1, *children)return x

第四层代码构建如下:

    self.level3 = Tree(levels[3], block, channels[2], channels[3], 2,level_root=True, root_residual=residual_root)

其中,layer[3] = 2, level_root=True,root_residual=False,
此时,第四层有两层,并且有了第三层的聚合输入(输入维度为64),所以level_root设置为True
有level1的聚合输入,和本身HDA结构的1个聚合输入和2个叶节点的输入。
对应框架图下图情况:

即,当前有左右两颗子树
这里在Tree类的forward推理情况就变成了如下:
根树属性:

self.tree1 = Tree(levels=1, in_channels=64, out_channels=128, stride=2, root_dim=0)
self.tree2 = Tree(levels=1, in_channels=out_channels, out_channels=128, stride=1, root_dim=448)
# 注意在代码中区分,因为tree2是要给root聚合节点输出的。这里root_dim = 2*oc+ic+oc = 2*128+64+128
self.root_dim = 320
self.downsample = maxpool
self.project = None # 因为此时是第三层直接输入所以in_channels=out_channels
(当时因为代码和附图不匹配看了好久。。。)

左子树属性:

self.tree1 = block(ic=64, oc=128, stride=2)
self.tree2 = block(ic=128, oc=128, stride=1)
self.root = Root(root_dim=256, oc=128)
self.downsample = maxpool(2, stride=2) # 2倍下采样
self.project = (conv2d+BN) # 2倍上采样forward:输入x,children
bottom = self.downsample(x) # 2倍下采样 128->64
residual = self.project(bottom) # 上采样恢复 64->128x1 = self.tree1 # 64->128
x2 = self.tree2 # 128->128
x = self.root(x2, x1) # 维度1上拼接 128+128 -> 128

右子树属性:

self.tree1 = block(ic=128, oc=128, stride=1)
self.tree2 = block(ic=128, oc=128, stride=1)
self.root = Root(root_dim=448, oc=128)
self.downsample = None
self.project = Noneforward: 输入x1, children # 由根数输入children聚合节点包含: 左子树输出x1, 根树输入的下采样bottom
x1 = self.tree1(x1, x1) # 残差为其本身 128->128
x2 = self.tree2(x1) # 128->128
x = self.root(x2, x1, children) # 128+128+128+64 ->128

总的推理变为

def forward(self, x, residual=None, children=None): # HDA过程初始children为0
children = []
bottom = self.downsample(x) # 进行2倍下采样 128->64
residual = bottom # 输入输出通道此时在根树中是相同的,所以先不用做下采样!!!children.append(bottom) # 保存聚合节点x1 = self.tree1(x, residual) # 进行左子树推理,个人觉得,这里residual传给子树residual每起作用。
children.append(x1)
x = self.tree2(x1, children=children) # 进行右子树推理 x1,children=[x1, bottom]
return x

这样第四层就构建完成。这里还是推荐手画一下。

整体解释

DLA一共有6层结构,其中前2层为基础块结构,后4层为树型结构

这六层结构根据预设的每层的层数和通道,结构都有所不同。

以下以DLA34为例解释。

DLA34:

Layer = [1,1,1,2,1]Channels = [16,32,64,128,256,512]Block = BasicBlock

第一层layer=1,channel=16,由16x16,k=3的二维卷积组成后续接BatchNorm和ReLu组成

代码构建如下:

self.level0 = self._make_conv_level(channels[0], channels[0], levels[0])def _make_conv_level(self, inplanes, planes, convs, stride=1, dilation=1):modules = []for i in range(convs):modules.extend([nn.Conv2d(inplanes, planes, kernel_size=3,stride=stride if i == 0 else 1,padding=dilation, bias=False, dilation=dilation),BatchNorm(planes),nn.ReLU(inplace=True)])inplanes = planesreturn nn.Sequential(*modules)

第二层layer=1,inplanes=16,planes=32,由16x32,k=3的二维卷积组成后续接BatchNorm和Relu组成

self.level1 = self._make_conv_level(channels[0], channels[1], levels[1], stride=2)

第三层layer=2,是树型结构的第一层

self.level2 = Tree(levels[2], block, channels[1], channels[2], 2,level_root=False, root_residual=residual_root)

第四层layer=3,是树型结构的第二层

self.level2 = Tree(levels[2], block, channels[1], channels[2], 2,level_root=False, root_residual=residual_root)

这两层搞清楚了,剩下的就是手动推理一下就可以清楚了。

cites

更多推荐

【Neck】DLA

本文发布于:2024-02-19 16:49:19,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1765006.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:Neck   DLA

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!