文章目录
摘要
随着神经网络的快速发展,神经网络现已经能够在目标检测等计算机视觉任务上取得令人难以置信的结果。然而,这种成功很大程度上依赖于昂贵的计算资源,这阻碍这种先进技术的普及。在本文中,我们提出了跨阶段局部网络(CSPNet)来缓解之前的神经网络需要从网络架构角度进行大量推理计算的问题。我们将着重解决网络优化中的重复梯度信息。对所提出的网络通过整合网络阶段开始和结束的特征图来应对梯度的可变性,在我们的实验中,这在 ImageNet 数据集上以同等甚至更高的精度减少了 20% 的计算量, 是MS COCO数据集中$AP_{50}$最优的方法。 CSPNet 易于实现且通用性,可以轻松应对基于 ResNet、ResNeXt 和 DenseNet 的架构。
简介
当神经网络变得更深和更宽时,神经网络已被证明特别强大。 然而,扩展神经网络的架构通常会带来更多的计算量,这使得大多数人无法负担诸如目标检测之类的计算量大的任务。 轻量级计算逐渐受到越来越多的关注,因为现实世界的应用程序通常需要在小型设备上进行较短的时间推理,这对计算机视觉算法提出了严峻的挑战。 尽管一些方法是专门为移动 CPU设计的,但它们采用的深度可分离卷积技术与工业 IC 设计不兼容,例如专用集成电路 (ASIC) 边缘计算系统。 在这项工作中,我们研究了 ResNet、ResNeXt和 DenseNet等最先进方法的计算负担。我们进一步开发计算效率高的组件,使上述网络能够部署在 CPU 和移动 GPU 上时不会牺牲性能。
![提出的 CSPNet 可以应用于 ResNet [7]、ResNeXt [39]、DenseNet [11] 等。它不仅降低了这些网络的计算成本和内存使用量,而且有利于推理速度和准确性。](http://blog.halashuo.cn/wp-content/uploads/2022/04/CSPNet与其他网络架构FPS、准确率对比图-1-e1651228437281.png)
我们提出的 CSPNet 可以应用于 ResNet 、ResNeXt、DenseNet等。它不仅降低了这些网络的计算成本和内存使用量,而且有利于推理速度和准确性。
1) 增强一个CNN的学习能力:现有的CNN在轻量化后精度大大降低,所以我们希望加强CNN的学习能力,使其在轻量化的同时保持足够的精度。提出的CSPNet可以很容易地应用于ResNet、ResNeXt 和DenseNet。 在上述网络上应用 CSPNet 后,计算量可以从 10% 减少到 20%,但它在 ImageNet上进行图像分类任务时准确度方面仍优于 ResNet,ResNeXt、DenseNet、HarDNet、Elastic和 Res2Net。
2)消除计算瓶颈:计算瓶颈太高会导致需要更多的周期来完成训练过程,或者一些计算单元经常空闲。 因此,我们希望能够将 CNN 中每一层的计算量平均分配,从而有效提升各计算单元的利用率,从而减少不必要的能耗。 值得注意的是,所提出的 CSPNet 使 PeleeNet的计算瓶颈减少了一半。 此外,在基于MS COCO数据集的目标检测实验中,我们提出的模型在基于 YOLOv3 的模型上进行测试时可以有效减少 80% 的计算瓶颈。
3)降低内存成本:动态随机存取存储器(DRAM)的晶圆制造成本非常昂贵,而且占用大量空间。 如果能有效地降低内存成本,TA将大大降低ASIC的成本。 此外,小面积晶圆可用于各种边缘计算设备。 在减少内存使用方面,我们在特征金字塔生成过程中采用跨通道池化来压缩特征图。 通过这种方式,带有目标检测器的 CSPNet 在生成特征金字塔时可以减少 PeleeNet 上 75% 的内存使用。
由于 CSPNet 能够提升CNN的学习能力,因此我们使用更小的模型来获得更好的准确性。我们提出的模型可以在GTX 1080ti 上以 109 fps 的速度实现 50% COCO $AP_{50}$。 由于 CSPNet 可以有效减少大量内存流量,我们提出的方法可以在 Intel 酷睿 i9-9900K 上以 52 fps 的速度实现 40% COCO AP 50。 此外,由于 CSPNet 可以显着降低计算瓶颈,而精确融合模型 (EFM) 可以有效减少所需的内存带宽,我们提出的方法可以在 Nvidia Jetson TX2 上以 49 fps 的速度实现 42% COCO AP 50。
相关工作
CNN 架构设计。在 ResNeXt中首先证明基数比宽度和深度的维度更有效。由于采用大量重用特征的策略,DenseNet可以显着减少参数和计算的数量。并且它将前面所有层的输出特征连接在一起作为下一个输入,这可以被认为是最大化基数的方式。 SparseNet将密集连接调整为指数间隔连接可以有效提高参数利用率,从而产生更好的结果。ResNet(PRN)进一步解释了为什么高基数和稀疏连接可以通过梯度组合的概念提高网络的学习能力。为了提高 CNN 的推理速度,介绍了设计 ShuffleNet-v2 时要遵循的四个准则。Chao则提出了一种称为 Harmonic DenseNet (HarDNet) 的低内存流量 CNN 和一个度量卷积输入/输出 (CIO),它是与实际 DRAM 流量测量成比例的 DRAM 流量的近似值。
实时物体检测器。最著名的两个实时目标检测器是 YOLOv3和 SSD。基于 SSD,LRF和 RFBNet可以在 GPU 上实现最先进的实时对象检测性能。最近,基于无锚定的对象检测器已成为主流的目标检测系统。两种此类目标检测器是 CenterNet和 CornerNet-Lite,它们在效率和功效方面都表现得非常好。对于 CPU 或移动 GPU 上的实时对象检测,基于 SSD 的 Pelee、基于 YOLOv3 的PRN和基于 Light-Head RCNN的 ThunderNet在对象检测方面都获得了出色的性能。
方法
跨阶段局部网络(Cross Stage Partial Network)
DenseNet。图 2 (a) 显示了 Huang 等人提出的 DenseNet 的一个阶段的详细结构。DenseNet的每个阶段都包含一个密集块和一个过渡层,每个密集块由k个密集层(dense layers)组成。$i^{th}$ 密集层的输出将与$i^{th}$ 密集层的输入连接,连接后的结果将成为$(i+1)^{th}$ 密集层的输入。显示上述机制的方程可以表示为:
$$
\begin{aligned}
x_1=&w_1*x_0\\
x_2=&w_2*[x_0,x_1]\\
&\vdots\\
x_k=&w_k*[x_0,x_1,\cdots,x_{k-1}]\\
\end{aligned}
$$
这里$*$表示卷积算子,$[x_0,x_1,\cdots]$表示连接$x_0,x_1,\cdots$,$w_i$和$x_i$分别是$i^{th}$密集层的权重和输出。
如果使用反向传播算法更新权重,则权重更新方程可以写为:
$$
\begin{aligned}
w’_1=&f(w_1,g_0)\\
w’_2=&f(w_2,g_0,g_1)\\
w’_3=&f(w_2,g_0,g_1,g_2)\\
&\vdots\\
w’_k=&f(w_2,g_0,g_1,g_2,\cdots,g_{k-1})\\
\end{aligned}
$$
其中$f$是权重更新的函数,$g_i$表示传播到$i^{th}$密集层的梯度。 我们可以发现,大量的梯度信息被重用于更新不同密集层的权重。 这将导致不同的密集层重复学习复制的梯度信息。
Cross Stage Partial DenseNet。所提出的CSPDenseNet的一个阶段的架构如图2(b)所示。CSPDenseNet的一个阶段由部分密集块(partialdense block)和部分过渡层组成。在一个部分密集块中,一个stage中基础层的特征图通过通道$x_0=[x_o’,x”_o]$分成两部分。在$x_o’$和$x”_o$中,后者直接与stage结尾相连,前者则要经过一个密集块。部分过渡层涉及的所有步骤如下:
首先,密集层的输出$[x”_0,x_1,\cdots,x_k]$将经历一个过渡层。其次,该过渡层的输出$x_T$将与$x”_0$连接并经过另一个过渡层,然后生成输出$x_U$。CSPDenseNet 的前馈传递和权重更新方程分别如下面两个公式所示。
$$
\begin{aligned}
x_k=&w_k*[x_0,x_1,\cdots,x_{k-1}]\\
x_T=&w_T*[x_0,x_1,\cdots,x_{k}]\\
x_U=&w_U*[x_0,x_T]\\
\end{aligned}
$$
$$
\begin{aligned}
w’_k=&f(w_k,g_0,g_1,g_2,\cdots,g_{k-1})\\
w’_T=&f(w_T,g_0,g_1,g_2,\cdots,g_{k})\\
w’_U=&f(w_U,g_0,g_T)\\
\end{aligned}
$$
我们可以看到来自密集层的梯度是单独集成的。 另一方面,没有经过密集层的特征图$x_0’$也被单独整合。 对于更新权重的梯度信息,双方不包含属于对方的重复梯度信息。
总体而言,所提出的 CSPDenseNet 保留了 DenseNet 特征重用特性的优势,但同时通过截断梯度流来防止过多的重复梯度信息。 这个想法是通过设计分层特征融合策略实现的,并用于部分过渡层。
Partial Dense Block。设计部分密集块的目的是 1.) 增加梯度路径:通过拆分和合并策略,可以使梯度路径的数量增加一倍。由于跨阶段策略,可以缓解使用显式特征图副本进行连接所带来的缺点; 2.) 每一层的平衡计算:通常,DenseNet 的基础层中的通道数远大于增长率。由于部分稠密块中稠密层操作所涉及的基层通道数仅占原始数量的一半,因此可以有效解决近一半的计算瓶颈; 3.) 减少内存流量:假设 DenseNet 中密集块的基本特征图大小为 w × h × c,增长率为 d,共有 m 个密集层。那么,该密集块的 CIO 为 (c × m) + ((m 2 + m) × d)/2 ,部分密集块的 CIO 为 ((c × m) + (m 2 + m) × d)/2。虽然 m 和 d 通常远小于 c ,但部分密集块能够节省网络的大部分内存流量。
Partial Transition Layer。设计部分过渡层的目的是为了最大化梯度组合的差异。部分过渡层是一种分层特征融合机制,它使用截断梯度流的策略来防止不同层学习重复的梯度信息。在这里,我们设计了 CSPDenseNet 的两种变体,以展示这种梯度流截断如何影响网络的学习能力。图 3 (上图)(c) 和 3 (d) 显示了两种不同的融合策略。 CSP(fusion first)就是把两部分生成的特征图拼接起来,然后做transition操作。如果采用这种策略,将会重复使用大量的梯度信息。对于 CSP(fusion last)策略,密集块的输出将通过过渡层,然后与来自第 1 部分的特征图进行连接。如果采用 CSP(fusion last)策略,则梯度信息由于梯度流被截断,因此不会被重用。如果我们使用图3所示的四种架构进行图像分类,对应的结果如图4(下图)所示。可以看出,如果采用CSP(fusion last)策略进行图像分类,计算成本显着下降,但 top-1 准确率仅下降0.1%。另一方面,CSP(fusion first)策略确实有助于显着降低计算成本,但 top-1 准确率显着下降了 1.5%。通过使用跨阶段的拆分和合并策略,我们能够有效地减少信息集成过程中重复的可能性。从图 4 所示的结果可以看出,如果能够有效地减少重复梯度信息,网络的学习能力将大大提高。
将 CSPNet 应用于其他架构。CSPNet 也可以很容易地应用于 ResNet 和 ResNeXt,架构如图 5(下图) 所示。由于只有一半的特征通道通过 Res(X)Blocks,因此不再需要引入瓶颈层。 这使得当浮点运算 (FLOP) 固定时内存访问成本 (MAC) 的理论下限。
YOLOv4中的CSPDarkNet53
YOLOV3的Darknet53的结构是由一系列残差网络结构构成。在Darknet53中,其存在resblock_body模块,其由一次下采样和多次残差结构的堆叠构成,Darknet53便是由resblock_body模块组合而成。在YOLOV4中,其对该部分进行了一定的修改。
其一是将DarknetConv2D的激活函数由LeakyReLU修改成了Mish,因此卷积块DarknetConv2D_BN_Leaky变成了DarknetConv2D_BN_Mish。
其二是将resblock_body的堆叠进行了一个拆分,拆成左右两部分,主干部分继续进行原来的残差块的堆叠;另一部分则像一个残差边一样,经过少量处理直接连接到最后。此时YOLOV4当中的Darknet53被修改成了CSPDarknet53。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
#---------------------------------------------------# # CSPdarknet的结构块 # 存在一个大残差边 # 这个大残差边绕过了很多的残差结构 #---------------------------------------------------# class Resblock_body(nn.Module): def __init__(self, in_channels, out_channels, num_blocks, first): super(Resblock_body, self).__init__() self.downsample_conv = BasicConv(in_channels, out_channels, 3, stride=2) if first: self.split_conv0 = BasicConv(out_channels, out_channels, 1) self.split_conv1 = BasicConv(out_channels, out_channels, 1) self.blocks_conv = nn.Sequential( Resblock(channels=out_channels, hidden_channels=out_channels//2), BasicConv(out_channels, out_channels, 1) ) self.concat_conv = BasicConv(out_channels*2, out_channels, 1) else: self.split_conv0 = BasicConv(out_channels, out_channels//2, 1) self.split_conv1 = BasicConv(out_channels, out_channels//2, 1) self.blocks_conv = nn.Sequential( *[Resblock(out_channels//2) for _ in range(num_blocks)], BasicConv(out_channels//2, out_channels//2, 1) ) self.concat_conv = BasicConv(out_channels, out_channels, 1) def forward(self, x): x = self.downsample_conv(x) x0 = self.split_conv0(x) x1 = self.split_conv1(x) x1 = self.blocks_conv(x1) x = torch.cat([x1, x0], dim=1) x = self.concat_conv(x) return x #-------------------------------------------------# # MISH激活函数 #-------------------------------------------------# class Mish(nn.Module): def __init__(self): super(Mish, self).__init__() def forward(self, x): return x * torch.tanh(F.softplus(x)) #-------------------------------------------------# # 卷积块 # CONV+BATCHNORM+MISH #-------------------------------------------------# class BasicConv(nn.Module): def __init__(self, in_channels, out_channels, kernel_size, stride=1): super(BasicConv, self).__init__() self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, kernel_size//2, bias=False) self.bn = nn.BatchNorm2d(out_channels) self.activation = Mish() def forward(self, x): x = self.conv(x) x = self.bn(x) x = self.activation(x) return x #---------------------------------------------------# # CSPdarknet的结构块的组成部分 # 内部堆叠的残差块 #---------------------------------------------------# class Resblock(nn.Module): def __init__(self, channels, hidden_channels=None, residual_activation=nn.Identity()): super(Resblock, self).__init__() if hidden_channels is None: hidden_channels = channels self.block = nn.Sequential( BasicConv(channels, hidden_channels, 1), BasicConv(hidden_channels, channels, 3) ) def forward(self, x): return x+self.block(x) #---------------------------------------------------# # CSPdarknet的结构块 # 存在一个大残差边 # 这个大残差边绕过了很多的残差结构 #---------------------------------------------------# class Resblock_body(nn.Module): def __init__(self, in_channels, out_channels, num_blocks, first): super(Resblock_body, self).__init__() self.downsample_conv = BasicConv(in_channels, out_channels, 3, stride=2) if first: self.split_conv0 = BasicConv(out_channels, out_channels, 1) self.split_conv1 = BasicConv(out_channels, out_channels, 1) self.blocks_conv = nn.Sequential( Resblock(channels=out_channels, hidden_channels=out_channels//2), BasicConv(out_channels, out_channels, 1) ) self.concat_conv = BasicConv(out_channels*2, out_channels, 1) else: self.split_conv0 = BasicConv(out_channels, out_channels//2, 1) self.split_conv1 = BasicConv(out_channels, out_channels//2, 1) self.blocks_conv = nn.Sequential( *[Resblock(out_channels//2) for _ in range(num_blocks)], BasicConv(out_channels//2, out_channels//2, 1) ) self.concat_conv = BasicConv(out_channels, out_channels, 1) def forward(self, x): x = self.downsample_conv(x) x0 = self.split_conv0(x) x1 = self.split_conv1(x) x1 = self.blocks_conv(x1) x = torch.cat([x1, x0], dim=1) x = self.concat_conv(x) return x class CSPDarkNet(nn.Module): def __init__(self, layers): super(CSPDarkNet, self).__init__() self.inplanes = 32 self.conv1 = BasicConv(3, self.inplanes, kernel_size=3, stride=1) self.feature_channels = [64, 128, 256, 512, 1024] self.stages = nn.ModuleList([ Resblock_body(self.inplanes, self.feature_channels[0], layers[0], first=True), Resblock_body(self.feature_channels[0], self.feature_channels[1], layers[1], first=False), Resblock_body(self.feature_channels[1], self.feature_channels[2], layers[2], first=False), Resblock_body(self.feature_channels[2], self.feature_channels[3], layers[3], first=False), Resblock_body(self.feature_channels[3], self.feature_channels[4], layers[4], first=False) ]) self.num_features = 1 # 进行权值初始化 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, nn.BatchNorm2d): m.weight.data.fill_(1) m.bias.data.zero_() def forward(self, x): x = self.conv1(x) x = self.stages[0](x) x = self.stages[1](x) out3 = self.stages[2](x) out4 = self.stages[3](out3) out5 = self.stages[4](out4) return out3, out4, out5 def darknet53(pretrained, **kwargs): model = CSPDarkNet([1, 2, 8, 8, 4]) if pretrained: if isinstance(pretrained, str): model.load_state_dict(torch.load(pretrained)) else: raise Exception("darknet request a pretrained path. got [{}]".format(pretrained)) return model |
暂无评论