0


深入浅出之CSPNet网络

一、提出背景

CSPNet(Cross Stage Partial Network)的提出背景主要源于对现有计算机视觉模型的分析和挑战。在计算资源受限的情况下,轻量级神经网络模型越来越受到关注,但这类模型在轻量化的同时往往会牺牲模型的准确性。此外,现有的模型在推断过程中存在计算瓶颈和内存开销较大的问题,这限制了模型在嵌入式设备和边缘计算平台上的应用。CSPNet旨在解决这些问题,通过优化网络结构和计算流程,提高轻量级模型的学习能力,以在保持较高准确性的同时实现轻量化。

在本文中,作者提出了跨阶段局部网络(CSPNet),用来缓解以往工作需要从网络架构角度进行大量推理计算的问题,作者把这个问题归结为网络优化中的「重复梯度信息」

CSPNet通过整合网络阶段开始和结束的特征图来尊重梯度的可变性,在我们的实验中,该方法减少了20%的计算量,在ImageNet数据集上具有相当、甚至更高的精度,并且在MS COCO目标检测数据集上的AP50方面显著优于最先进的方法。

除此之外,CSPNet易于实现,并且足够通用,可以处理基于「ResNet」「ResNeXt」「DenseNet」的体系结构。

二、时间与作者

CSPNet由Chien-Yao Wang等人在2019年提出,并发表在论文《CSPNET: A NEW BACKBONE THAT CAN ENHANCE LEARNING CAPABILITY OF CNN》中。

三、主要贡献

设计CSPNet的主要目的是使该体系结构能够实现「更丰富的梯度组合,同时减少计算量」。通过将基础层的特征图划分为两个部分,然后通过提出的跨阶段层次结构将它们合并,可以实现此目标。

「作者的主要想法」是通过分割梯度流,使梯度流通过不同的网络路径传播。通过切换串联和过渡步骤,传播的梯度信息可以具有较大的相关性差异。此外,CSPNet可以大大减少计算量,并提高推理速度和准确性,如图1所示。

CSPNet的主要贡献包括:

  1. 「增强了CNN的学习能力」。现有的CNN经过轻量化后,其准确性大大降低,所以作者希望加强CNN的学习能力,使其在轻量化的同时保持足够的准确性。所提出的CSPNet可以很容易地应用于ResNet、ResNeXt和DenseNet。将CSPNet应用于上述网络后,计算量从10%减少到20%,但在精度方面,在ImageNet上进行图像分类任务时,其表现优于ResNet、ResNeXt、DenseNet、HarDNet等一流的算法。
  2. 「消除计算瓶颈」。过高的计算瓶颈将导致更多的周期来完成推理过程,否则一些算术单元将经常空闲。因此,作者希望在CNN中能够将每一层的计算量平均分配,从而有效的提高每一个计算单元的利用率,减少不必要的能源消耗。注意到所提出的CSPNet使PeleeNet的计算瓶颈减半。此外,在基于MS COCO数据集的目标检测实验中,该模型在基于YOLOv3的模型上测试时,可以有效地减少80%的计算瓶颈。
  3. 「减少内存成本」。动态随机存取存储器(DRAM)的晶圆制造成本非常高,同时也占用大量的空间。如果能有效降低存储成本,将大大降低ASIC的成本。此外,小面积晶圆可以用于各种边缘计算设备。为了减少内存使用,在特征金字塔生成过程中,作者采用了跨通道的[6]pooling 来压缩特征映射。这样,在生成特征金字塔时,使用目标检测器的CSPNet可以减少PeleeNet 75%的内存使用。

四、网络结构

CSPNet的网络结构主要包括跨阶段部分连接(CSPBlock)模块。CSPBlock将基础层的特征图划分成两部分,一部分经过密集连接(Dense Connection)处理,另一部分则直接进行卷积操作。然后将这两部分的输出进行合并,形成最终的特征图。这种设计既保留了DenseNet重用特征特性的优点,又通过截断梯度流防止了过多的重复梯度信息。

4.1 CSP结构

论文提出Cross Stage Partial(CSP)结构,其初衷是减少计算量并且增强梯度的表现。主要思想是:在输入block之前,将输入分为两个部分,其中一部分通过block进行计算,另一部分直接通过一个shortbut进行concatenate。

作者基于Dense block(对DenseNet结构不了解的同学可以查看我以前的文章)讲述了CSP的结构,如下图:

图片来自于论文

上图中,上者为原始的Dense Block,下者为Partial Dense Block。假设将输入按照part_ratio=0.5的比例分成两部分,并且假设一个Dense Block的输入为w h c,growth rate为 d,Dense block的layer数量为m,则:对于原始的DenseBlock,其CIO为(Convolutional input/Output)为cm + [(mm+m)d]/2而对于使用了CSP结构的结构来说,其CIO下降为[cm + (mm+m)d]/2

作者在论文阐述了CSP结构的优点:

(1)加强CNN的学习能力;

(2)减少计算瓶颈,现在的网络大多计算代价昂贵,不利于工业的落地;

(3)减少内存消耗。

4.2 关于Partial Transition Layer

关于Patial Transition Layer,论文提出了三种不同的结构,如下图:

图片来自论文

Transition layer的含义同DenseNet,是一组1x1的卷积层。上图中,不同类型的transition layer决定了梯度的结构方式,并且各有其优势:

(c)图Fusion First的先将两个部分进行concatenate,然后再进行输入到Transion layer中,采用这种做法会是的大量特梯度信息被重用,有利于网络学习;

(d)图中Fusion Last先将部分特征输入Transition layer,然后再进行concatenate,这样做损失了部分的梯度重用,但是由于Transition的输入维度比(c)图少,大大减少计算复杂度。论文中CSPNet采用的是图(b)中的结构,其结合了(c)(d)的结合,进一步提升了学习能力,但是也进一步提高了一些计算复杂度。 作者在论文中给出其使用不同Partial Transition Layer的实验结果,如下图。具体使用哪种结构可以根据条件和使用场景进行调整。

图片来自论文

五、特点

CSPNet的特点可以归纳为以下几点:

  1. 跨阶段部分连接:通过CSPBlock模块实现跨阶段的特征融合,提高了模型的学习能力。
  2. 轻量化与高效性:在保证模型准确性的同时,实现了模型的轻量化,并提高了推断速度。
  3. 灵活性:CSPNet可以轻松地整合到现有的深度学习架构中,适应不同的任务需求。
  4. 减少梯度重用:通过截断梯度流,减少了不同层之间梯度信息的重复利用,提高了模型的训练效率。

六、优缺点

优点

  • 提高了轻量级模型的准确性。
  • 降低了计算复杂度和内存占用。
  • 通用性强,适用于多种计算机视觉任务。

缺点

  • 相对于一些更复杂的模型,CSPNet在极端复杂场景下的性能可能略有不足。
  • 在某些特定任务上,可能需要结合其他技术或方法进行优化。

七、 pytorch实现CSP-DenseNet,CSP-ResNeXt

7.1 CSP-DenseNet

CSPNet沿用网络的Architecture,只是对于基础block进行改造,根据之前博客介绍的DenseNet结构修改相应代码即可实现。

7.1.1 Partial Dense block

沿用之前介绍DenseNet写的Dense Block代码:

  1. class DenseBlock(nn.Module):
  2. def __init__(self, input_channels, num_layers, growth_rate):
  3. super(DenseBlock, self).__init__()
  4. self.num_layers = num_layers
  5. self.k0 = input_channels
  6. self.k = growth_rate
  7. self.layers = self.__make_layers()
  8. def __make_layers(self):
  9. layer_list = []
  10. for i in range(self.num_layers):
  11. layer_list.append(nn.Sequential(
  12. BN_Conv2d(self.k0 + i * self.k, 4 * self.k, 1, 1, 0),
  13. BN_Conv2d(4 * self.k, self.k, 3, 1, 1)
  14. ))
  15. return layer_list
  16. def forward(self, x):
  17. feature = self.layers[0](x)
  18. out = torch.cat((x, feature), 1)
  19. for i in range(1, len(self.layers)):
  20. feature = self.layers[i](out)
  21. out = torch.cat((feature, out), 1)
  22. return out

Partial Dense Block的实现:

  1. class CSP_DenseBlock(nn.Module):
  2. def __init__(self, in_channels, num_layers, k, part_ratio=0.5):
  3. super(CSP_DenseBlock, self).__init__()
  4. self.part1_chnls = int(in_channels * part_ratio)
  5. self.part2_chnls = in_channels - self.part1_chnls
  6. self.dense = DenseBlock(self.part2_chnls, num_layers, k)
  7. # trans_chnls = self.part2_chnls + k * num_layers
  8. # self.transtion = BN_Conv2d(trans_chnls, trans_chnls, 1, 1, 0)
  9. def forward(self, x):
  10. part1 = x[:, :self.part1_chnls, :, :]
  11. part2 = x[:, self.part1_chnls:, :, :]
  12. part2 = self.dense(part2)
  13. # part2 = self.transtion(part2)
  14. out = torch.cat((part1, part2), 1)
  15. return out

上面的代码采用的是Fusion Last的Partial transition layer,取消其中的三行注释就是论文中采用的Partial transition layer。由于,我之前的Transition layer是写在整体网络结构中,没有放在block中,所以就不实现Fusion first的形式了。

7.1.2 网络整体结构

在之前原始的DenseNet结构的基础上进行修改,增加一个partion-ratio参数来控制使用的block结构和通道划分的比例就可以了,代码:

  1. class DenseNet(nn.Module):
  2. def __init__(self, layers: object, k, theta, num_classes, part_ratio=0) -> object:
  3. super(DenseNet, self).__init__()
  4. # params
  5. self.layers = layers
  6. self.k = k
  7. self.theta = theta
  8. self.Block = DenseBlock if part_ratio == 0 else CSP_DenseBlock # 通过part_tatio参数控制block type
  9. # layers
  10. self.conv = BN_Conv2d(3, 2 * k, 7, 2, 3)
  11. self.blocks, patches = self.__make_blocks(2 * k)
  12. self.fc = nn.Linear(patches, num_classes)
  13. def __make_transition(self, in_chls):
  14. out_chls = int(self.theta * in_chls)
  15. return nn.Sequential(
  16. BN_Conv2d(in_chls, out_chls, 1, 1, 0),
  17. nn.AvgPool2d(2)
  18. ), out_chls
  19. def __make_blocks(self, k0):
  20. """
  21. make block-transition structures
  22. :param k0:
  23. :return:
  24. """
  25. layers_list = []
  26. patches = 0
  27. for i in range(len(self.layers)):
  28. layers_list.append(self.Block(k0, self.layers[i], self.k))
  29. patches = k0 + self.layers[i] * self.k # output feature patches from Dense Block
  30. if i != len(self.layers) - 1:
  31. transition, k0 = self.__make_transition(patches)
  32. layers_list.append(transition)
  33. return nn.Sequential(*layers_list), patches
  34. def forward(self, x):
  35. out = self.conv(x)
  36. out = F.max_pool2d(out, 3, 2, 1)
  37. # print(out.shape)
  38. out = self.blocks(out)
  39. # print(out.shape)
  40. out = F.avg_pool2d(out, 7)
  41. # print(out.shape)
  42. out = out.view(out.size(0), -1)
  43. out = F.softmax(self.fc(out))
  44. return out
  45. def csp_densenet_121(num_classes=1000):
  46. return DenseNet([6, 12, 24, 16], k=32, theta=0.5, num_classes=num_classes, part_ratio=0.5)
  47. def csp_densenet_169(num_classes=1000):
  48. return DenseNet([6, 12, 32, 32], k=32, theta=0.5, num_classes=num_classes, part_ratio=0.5)
  49. def csp_densenet_201(num_classes=1000):
  50. return DenseNet([6, 12, 48, 32], k=32, theta=0.5, num_classes=num_classes, part_ratio=0.5)
  51. def csp_densenet_264(num_classes=1000):
  52. return DenseNet([6, 12, 64, 48], k=32, theta=0.5, num_classes=num_classes, part_ratio=0.5)

7.2 CSP-ResNeXt

论文也给出了CSP-ResNet结构。我们知道在ResNeXt的实现中,若groups设置为1,就和ResNet结构相似,这里仅搭建CSP-ResNeXt。

图片来自论文

讨论:论文中提到由于输入的通道减半了,CSPNet不需要使用bottleneck结构了。但是我查看DarkNet框架下的网络参数,发现还是1x1,3x3,1x1的组合结构。有点纳闷,不知道我是不是对bottleneck结构有误解。可能作者指的是bottleneck指的是输出通道比输入通道数量要少的卷积层。

作者给的code链接是darknet框架下的cfg参数,由于我不熟悉这个框架,有点看不清楚。
这里,我是按照论文的思想和自己理解搭建CSP-ResNeXt,网络输入尺寸和整体结构仍沿用ResNeXt的,不是论文实验确实采用的确切网络结构,谨供参考和了解思路。

我们再回顾一下ResNeXt的整体结构,之前博客的传送门:

图片来自ResNeXt论文

ResNeXt的主题是一些不同尺寸的block组成的,共4组,以ResNeXt-53为例,每组blocks的数量分别为[3, 4, 6, 3]。根据CSPNet的思想去除bottleneck结构,采用Fusion First和Fusion Last的模式。针对每一组,可以设计如下的结构:

图片来源:自己画的~

由上图,之前ResNeXt中Residual block中的最后一层1x1滤波器数量进行了2倍的expansion,这里改成三层均是一样的数量,那么我们根据这个结构进行编码。

再次提醒,这个复现不是SoAT算法所用的确切CSPResNet。但是其block结构符合上图中CSPNet的基本思想,整体架构沿用ResNet架构——虽然目前不准备实验验证,但我想它应该具有一定精度。

7.2.0 BN_CONV_LeakyReLU

参考作者,激活函数采用了LeakyReLU,但在一些目标检测框架中会用Mish作为激活函数,以更高的计算代价获取网络更好的收敛性。

  1. class BN_Conv2d_Leaky(nn.Module):
  2. """
  3. BN_CONV_LeakyRELU
  4. """
  5. def __init__(self, in_channels: object, out_channels: object, kernel_size: object, stride: object, padding: object,
  6. dilation=1, groups=1, bias=False) -> object:
  7. super(BN_Conv2d_Leaky, self).__init__()
  8. self.seq = nn.Sequential(
  9. nn.Conv2d(in_channels, out_channels, kernel_size=kernel_size, stride=stride,
  10. padding=padding, dilation=dilation, groups=groups, bias=bias),
  11. nn.BatchNorm2d(out_channels)
  12. )
  13. def forward(self, x):
  14. return F.leaky_relu(self.seq(x))

2.2.1 Residual block

在这里,将最后一层1x1的数量改为和之前两层一致。在shortcut的实现上,我用均值池化来应对stride不为1的情况,因为感觉我之前在ResNeXt写的在1x1卷积的时候设置stride处理方式会损失一定的输入信息。 代码如下:

  1. class ResidualBlock(nn.Module):
  2. """
  3. Residual block for CSP-ResNeXt
  4. """
  5. def __init__(self, in_channels, cardinality, group_width, stride=1):
  6. super(ResidualBlock, self).__init__()
  7. self.out_channels = cardinality * group_width
  8. self.conv1 = BN_Conv2d_Leaky(in_channels, self.out_channels, 1, 1, 0)
  9. self.conv2 = BN_Conv2d_Leaky(self.out_channels, self.out_channels, 3, stride, 1, groups=cardinality)
  10. self.conv3 = nn.Conv2d(self.out_channels, self.out_channels, 1, 1, 0)
  11. self.bn = nn.BatchNorm2d(self.out_channels)
  12. # make shortcut
  13. layers = []
  14. if in_channels != self.out_channels:
  15. layers.append(nn.Conv2d(in_channels, self.out_channels, 1, 1, 0))
  16. layers.append(nn.BatchNorm2d(self.out_channels))
  17. if stride != 1:
  18. layers.append(nn.AvgPool2d(stride))
  19. self.shortcut = nn.Sequential(*layers)
  20. def forward(self, x):
  21. out = self.conv3(self.conv2(self.conv1(x)))
  22. out = self.bn(out)
  23. out += self.shortcut(x)
  24. return F.leaky_relu(out)

7.2.3 网络Architecture

1、把上图中设计的结构作为一个整体来实现,称之为Stem,代码如下:

  1. class Stem(nn.Module):
  2. def __init__(self, in_channels, num_blocks, cardinality, group_with, stride=2):
  3. super(Stem, self).__init__()
  4. self.c0 = in_channels // 2
  5. self.c1 = in_channels - in_channels // 2
  6. self.hidden_channels = cardinality * group_with
  7. self.out_channels = self.hidden_channels * 2
  8. self.transition = BN_Conv2d_Leaky(self.hidden_channels, in_channels, 1, 1, 0) # to match channel size
  9. self.trans_part0 = nn.Sequential(BN_Conv2d_Leaky(self.c0, self.hidden_channels, 1, 1, 0), nn.AvgPool2d(stride))
  10. self.block = self.__make_block(num_blocks, self.c1, cardinality, group_with, stride)
  11. self.trans_part1 = BN_Conv2d_Leaky(self.hidden_channels, self.hidden_channels, 1, 1, 0)
  12. self.trans = BN_Conv2d_Leaky(self.out_channels, self.out_channels, 1, 1, 0)
  13. def __make_block(self, num_blocks, in_channels, cardinality, group_with, stride):
  14. strides = [stride] + [1] * (num_blocks-1)
  15. channels = [in_channels] + [self.hidden_channels] * (num_blocks-1)
  16. return nn.Sequential(*[ResidualBlock(c, cardinality, group_with, s)
  17. for c, s in zip(channels, strides)])
  18. def forward(self, x):
  19. x = self.transition(x) # to match channels
  20. x0 = x[:, :self.c0, :, :]
  21. x1 = x[:, self.c0:, :, :]
  22. out0 = self.trans_part0(x0)
  23. out1 = self.trans_part1(self.block(x1))
  24. out = torch.cat((out0, out1), 1)
  25. return self.trans(out)

2、以下代码提供了CSP-ResNeXt的架构以及CSP-ResNeXt-53(32x4d)的接口:

  1. class CSP_ResNeXt(nn.Module):
  2. def __init__(self, num_blocks, cadinality, group_width, num_classes):
  3. super(CSP_ResNeXt, self).__init__()
  4. self.conv0 = BN_Conv2d_Leaky(3, 64, 7, 2, 3)
  5. self.pool1 = nn.MaxPool2d(3, 2, 1)
  6. self.conv1 = BN_Conv2d_Leaky(64, 128, 1, 1, 0)
  7. self.stem0 = Stem(cadinality*group_width*2, num_blocks[0], cadinality, group_width, stride=1)
  8. self.stem1 = Stem(cadinality*group_width*4, num_blocks[1], cadinality, group_width*2)
  9. self.stem2 = Stem(cadinality*group_width*8, num_blocks[2], cadinality, group_width*4)
  10. self.stem3 = Stem(cadinality*group_width*16, num_blocks[3], cadinality, group_width*8)
  11. self.global_pool = nn.AdaptiveAvgPool2d((1, 1))
  12. self.fc = nn.Linear(cadinality*group_width*16, num_classes)
  13. def forward(self, x):
  14. out = self.conv0(x)
  15. out = self.pool1(out)
  16. out = self.conv1(out)
  17. out = self.stem0(out)
  18. out = self.stem1(out)
  19. out = self.stem2(out)
  20. out = self.stem3(out)
  21. out = self.global_pool(out)
  22. out = out.view(out.size(0), -1)
  23. out = self.fc(out)
  24. return F.softmax(out)
  25. def csp_resnext_50_32x4d(num_classes=1000):
  26. return CSP_ResNeXt([3, 4, 6, 3], 32, 4, num_classes)

3、搭建完毕,测试网络:

  1. net = csp_resnext_50_32x4d()
  2. summary(net, (3, 256, 256))

测试结果如下:

八、应用场景

CSPNet的设计使其适用于各种计算机视觉任务,包括但不限于:

  • 图像分类:提升常规图像分类任务的准确性。
  • 目标检测:在对象检测框架如YOLO或SSD中,CSPNet可作为基础网络以提高检测性能。
  • 语义分割:对于需要理解像素级别的图像任务,CSPNet也能提供强大的支持。
  • 视频分析:由于其高效特性,CSPNet也可以应用于实时视频理解等任务。

总的来说,CSPNet作为一种新型的深度学习架构,在保持精度的同时降低了计算复杂度和内存占用,为计算机视觉领域的研究和应用提供了新的思路和方法。

参考:

  • CSPNet——PyTorch实现CSPDenseNet和CSPResNeXt
  • 【CSPNet】一种增强学习能力的跨阶段局部网络

本文转载自: https://blog.csdn.net/a8039974/article/details/142242325
版权归原作者 浩瀚之水_csdn 所有, 如有侵权,请联系我们删除。

“深入浅出之CSPNet网络”的评论:

还没有评论