文章目录
本研究是有由UC Berkeley的Trevor Darrell组发表于2018年CVPR。因为,工作中应用到CenterNet,文章中使用了DLA作为backbone,能够以较高的速度完成推理并维持较高的AP。因此再回顾一下DLA这篇文章。
DLA文章:cvf open access
DLA代码:官方代码
CenterNet代码
论文学习
网络结构如,LeNet,AlexNet和ResNet都是层层堆叠的网络结构,更注重网络的深度,但依然无法确保多深的网络是足以在一个任务上提取到最具代表性的特征表示。
在Deep Layer Aggregation这篇文章中,探讨的是如何aggregate整合不同层级以实现语义和空间信息的聚合。
文章中提出了两种Deep Layer Aggregation(DLA)结构:
- iterative deep aggregation (IDA),用于聚合不同分辨率和尺度
- hierarchical deep aggregation (HDA),用于聚合各个模块和通道的特征。
Iterative Deep Aggregation
现有的聚合方式(skip connection)是通过将较浅和较深层达到不同尺度和分辨率的聚合。这种方式通常是线性且最浅层的层级的聚合程度是最少的。
因此,文中提出了IDA方式实现逐步聚合且不断深化特征表示。
上图为IDA示意图。可见,聚合过程是从最浅层开始,逐步与跟深层合并,浅层级的特征能在不同stage的聚合中进行传播。
Hierarchical Deep Aggregation
基础的HDA是一个树状结构,通过合并blocks和stages的特征达到对特征的保留和结合,如(d)所示。
进一步,作者又改进了这个结构,形成如(e)的结构。可以看到,下一个子树的输入由上一个block的输出变为了上一个子树聚合节点的输出。这样,之前所有blocks的信息就都能够被传播和保留。(f)是在(e)的基础上,进一步将同一深度的节点合并以提升效率。
网络结构
IDA和HDA的结构中都包含了聚合节点(Aggregation Node),即结构图中的绿色方块。聚合节点的结构为conv+BN+非线性激活函数。在分类网络中,所有聚合节点使用的是1x1conv;语义分割的上采样蹭,会额外加入一个包含3x3 conv的IDA。
应用DLA
在面向不同visual recognition的网络中应用DLA。
分类网络
分类网络如ResNet和ResNeXT一般以分辨率下降一半为一个stage结束标志,总共分为6个stages。stages之间通过IDA和HDA进行连接,stage内同时通过HDA进行连接。
分割网络
通过DLA对分割网络的增强与分类网络的增强主要却别在上采样部分,如上图所示。
结果对比
仅展示ImageNet的结果,详细的结果对比请参照原文。从结果对比,作者想要说明的是,DLA能够以更少的参数、更少的memory消耗达到与SOTA一致甚至更佳的精度。
在ImageNet 分类结果上的对比
与ResNet、ResNeXT比较
与SqueezeNet的对比
代码学习
此处援引的是作者提供的官方代码。
网络组成如上图所示,我们以DLA-34为例进行学习。
DLA的block是简单的Basic Residual Block,每个stage的通道数都列在表格中了,及16,32,64,128,256,512分别代表stage1-6的通道数,而-前的数字代表的是聚合深度 。
定义如下图所示:
class BasicBlock(nn.Module):
def __init__(self, inplanes, planes, stride=1, dilation=1):
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=3,
stride=stride, padding=dilation,
bias=False, dilation=dilation)
self.bn1 = BatchNorm(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3,
stride=1, padding=dilation,
bias=False, dilation=dilation)
self.bn2 = BatchNorm(planes)
self.stride = stride
def forward(self, x, residual=None):
if residual is None:
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out += residual
out = self.relu(out)
return out
我们先看一下DLA-34的基本定义:
def dla34(pretrained=None, **kwargs): # DLA-34
model = DLA([1, 1, 1, 2, 2, 1],
[16, 32, 64, 128, 256, 512],
block=BasicBlock, **kwargs)
if pretrained is not None:
model.load_pretrained_model(pretrained, 'dla34')
return model
DLA的第一个参数代表每一个Stage的聚合深度,第二个参数是每一个Stage的通道数,第三个参数表示block采用的是Basic Residual Block。
那我们下面看一下DLA是如何定义的
class DLA(nn.Module):
def __init__(self, levels, channels, num_classes=1000,
block=BasicBlock, residual_root=False, return_levels=False,
pool_size=7, linear_root=False):
super(DLA, self).__init__()
self.channels = channels
self.return_levels = return_levels
self.num_classes = num_classes
self.base_layer = nn.Sequential(
nn.Conv2d(3, channels[0], kernel_size=7, stride=1,
padding=3, bias=False),
BatchNorm(channels[0]),
nn.ReLU(inplace=True))
self.level0 = self._make_conv_level(
channels[0], channels[0], levels[0])
self.level1 = self._make_conv_level(
channels[0], channels[1], levels[1], stride=2)
self.level2 = Tree(levels[2], block, channels[1], channels[2], 2,
level_root=False,
root_residual=residual_root)
self.level3 = Tree(levels[3], block, channels[2], channels[3], 2,
level_root=True, root_residual=residual_root)
self.level4 = Tree(levels[4], block, channels[3], channels[4], 2,
level_root=True, root_residual=residual_root)
self.level5 = Tree(levels[5], block, channels[4], channels[5], 2,
level_root=True, root_residual=residual_root)
self.avgpool = nn.AvgPool2d(pool_size)
self.fc = nn.Conv2d(channels[-1], num_classes, kernel_size=1,
stride=1, padding=0, bias=True)
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
elif isinstance(m, BatchNorm):
m.weight.data.fill_(1)
m.bias.data.zero_()
def _make_level(self, block, inplanes, planes, blocks, stride=1):
downsample = None
if stride != 1 or inplanes != planes:
downsample = nn.Sequential(
nn.MaxPool2d(stride, stride=stride),
nn.Conv2d(inplanes, planes,
kernel_size=1, stride=1, bias=False),
BatchNorm(planes),
)
layers = []
layers.append(block(inplanes, planes, stride, downsample=downsample))
for i in range(1, blocks):
layers.append(block(inplanes, planes))
return nn.Sequential(*layers)
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 = planes
return nn.Sequential(*modules)
def forward(self, x):
y = []
x = self.base_layer(x)
for i in range(6):
x = getattr(self, 'level{}'.format(i))(x)
y.append(x)
if self.return_levels:
return y
else:
x = self.avgpool(x)
x = self.fc(x)
x = x.view(x.size(0), -1)
return x
def load_pretrained_model(self, data_name, name):
assert data_name in dataset.__dict__, \
'No pretrained model for {}'.format(data_name)
data = dataset.__dict__[data_name]
fc = self.fc
if self.num_classes != data.classes:
self.fc = nn.Conv2d(
self.channels[-1], data.classes,
kernel_size=1, stride=1, padding=0, bias=True)
try:
model_url = get_model_url(data, name)
except KeyError:
raise ValueError(
'{} trained on {} does not exist.'.format(data.name, name))
self.load_state_dict(model_zoo.load_url(model_url))
self.fc = fc
解读一下:
- _make_level函数是一个带MaxPool下采样的Stage层,代码中未搜索到调用,此处略过
- _make_conv_level从代码中可以看出是Stage1和Stage2的组成部分,Conv+BN+ReLU,较基础,也略过不说
- 重点看一下Stage3-6调用的函数Tree
class Tree(nn.Module):
def __init__(self, levels, block, in_channels, out_channels, stride=1,
level_root=False, root_dim=0, root_kernel_size=1,
dilation=1, root_residual=False):
super(Tree, self).__init__()
if root_dim == 0:
root_dim = 2 * out_channels
if level_root:
root_dim += in_channels
if levels == 1:
self.tree1 = block(in_channels, out_channels, stride,
dilation=dilation)
self.tree2 = block(out_channels, out_channels, 1,
dilation=dilation)
else:
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)
if levels == 1:
self.root = Root(root_dim, out_channels, root_kernel_size,
root_residual)
self.level_root = level_root
self.root_dim = root_dim
self.downsample = None
self.project = None
self.levels = levels
if stride > 1:
self.downsample = nn.MaxPool2d(stride, stride=stride)
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)
)
def forward(self, x, residual=None, children=None):
children = [] if children is None else children
bottom = self.downsample(x) if self.downsample else x
residual = self.project(bottom) if self.project else bottom
if self.level_root:
children.append(bottom)
x1 = self.tree1(x, residual)
if self.levels == 1:
x2 = self.tree2(x1)
x = self.root(x2, x1, *children)
else:
children.append(x1)
x = self.tree2(x1, children=children)
return x
可以看到,当levels大于1时,是通过递归对该模块进行定义。看完代码下来,我理解IDA和HDA虽然具有不同的含义,但从实际从代码实现上是二者实际上都是通过Tree这个模块实现的。
我们就拿level2,也就是Stage3来详细分析:
self.level2 = Tree(levels[2], block, channels[1], channels[2], 2,
level_root=False,
root_residual=residual_root)
也就是:
- levels为2
- block为Basic Block
- in_channels为32
- out_channels为64
- stride为2
- level_root为False
- root_dim为0
- root_kernel_size为1
- dilation为1
- root_residual为False
基本实现的就是下图这样的结构:
x会先过Tree(1,BasicBlock,32,64,stride=2,root_dim=0)和Tree(1,BasicBlock,64,64,stride=1,root_dim=192); 每一个Tree会根据输入参数经过前一个Tree为block(32,64,stride=2)和block(64,64,stride=1)经过Root(128,64),后一个Tree为block(64,64,stride=2)和block(64,64,stride=1)经过Root(192,64)。链接顺序还是要回归到代码中,此处仅整理一下大致思路。
对照这张图看整个结构还是比较清晰的。
Tree里面还调用了Root函数,定义也贴一下:
class Root(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, residual):
super(Root, self).__init__()
self.conv = nn.Conv2d(
in_channels, out_channels, kernel_size,
stride=1, bias=False, padding=(kernel_size - 1) // 2)
self.bn = BatchNorm(out_channels)
self.relu = nn.ReLU(inplace=True)
self.residual = residual
def forward(self, *x):
children = x
x = self.conv(torch.cat(x, 1))
x = self.bn(x)
if self.residual:
x += children[0]
x = self.relu(x)
return x