萌小帅 一路向前

要有朴素的生活和遥远的梦想,不管明天天寒地冻,路遥马亡...^-^...

【源码解读】YOLO v3 训练 - 02 网络结构

  首先,看一下YOLO v3 中的网络结构。

YOLO v3 的整体流程

  番外步骤: 对训练集图片标记后产生的数据进行K-Means处理,筛选9个anchor-box

  详见:https://www.cnblogs.com/monologuesmw/p/12761653.html

  进入YOLO v3的结构中:

  1. 图像缩放。将训练集的图片缩放至416*416中,包括两种缩放的方式:等比缩放和非等比缩放(非等比缩放一般只在训练中)图像缩放至416*416以后,便可以作为YOLO v3 结构的输入。

  2. Darknet-53下采样过程,其内部的结构可以看成一个3*3,步长(1*1)(fitters:32)的same卷积 和 若干个(一个3*3,步长(2*2),fitters总是比前面的fitters多一倍的窄卷积 和 fitters减半的1*1卷积、fitters恢复的3*3同卷积 )组成的残差组合块(组合的意思是,一个3*3步长(2*2)其实不是卷积块的内容,但二者有依存关系)。 这样就会将416*416*3 的图像转换为13*13*1024的feature_map。过程中会包含26*26*512的feature_map 和 52*52*256 的feature_map。其内部是这样定义的,步长(1,1)的为同卷积,步长为(2*2)的为窄卷积(使用步长为2的卷积代替pool操作)。

   3. 多尺度预测与特征融合上采样,YOLO v3与v2结构上的不同点就在于v3增添了多尺度的预测特征融合的上采样(不同大小的特征适应于不同大小物体的检测)。并且卷积的结构没有池化层,在卷积层后都会搭载一个Leaky-ReLU的激活函数,并且在激活函数的输入中不使用偏执bias

  在生成13*13*1024后,会再经历两对1*1和3*3卷积,使得三种尺度的feature_map变为13*13*512, 26*26*256。(因为最大尺度的是52*52*256),然后通过上采样的方式,13*13*512变为26*26*512 ,26*26*256变为52*52*256, 这样便可以与高尺度的拼接融合。生成最终的13*13*255、26*26*255 、52*52*255的feature_map输出。

  P.S. 上采样的内部机制实际上是最近邻域插值的方式使维度翻倍。

  先一睹YOLOv3具体的网络结构:

代码解析

1. Darknet53部分

1 darknet = Model(inputs, darknet_body(inputs)) # 此处返回了第一个下采样13*13 (?,416*416*32)->(?,13*13*1024)

  darknet_body(x)内部

 1 def darknet_body(x):
 2     '''Darknent body having 52 Convolution2D layers
 3     有52个卷积层(2D)
 4     '''
 5     x = DarknetConv2D_BN_Leaky(32, (3, 3))(x)      # (内置same卷积)输出的x-》416*416*32
 6     x = resblock_body(x, 64, 1)   # num_filters = 64 , num_blocks = 1(重复次数)  返回结果208*208*64
 7     x = resblock_body(x, 128, 2)    # 返回结果 104*104*128
 8     x = resblock_body(x, 256, 8)    # 返回结果 52*52*256
 9     x = resblock_body(x, 512, 8)    # 返回结果 26*26*512
10     x = resblock_body(x, 1024, 4)    # 返回结果 13*13*1024     5组重复的resblock_body()单元
11     return x         # 第一个下采样特征生成

  上述代码是Darknet53的主体部分。

 a. 其中,第5行是 一个3*3,步长(1*1)(fitters:32)的same卷积。 其中包含:

  •  1个Darknet的2维卷积Conv2D层,即DarknetConv2D();
  •  1个批归一化(BN)层,即BatchNormalization();
  •  1个LeakyReLU层,斜率是0.1,LeakyReLU是ReLU的变换;
1 def DarknetConv2D_BN_Leaky(*args, **kwargs):
2     """Darknet Convolution2D followed by BatchNormalization and LeakyReLU."""
3     no_bias_kwargs = {'use_bias': False}
4     no_bias_kwargs.update(kwargs)
5     return compose(
6         DarknetConv2D(*args, **no_bias_kwargs),
7         BatchNormalization(),
8         LeakyReLU(alpha=0.1))  

 b. 第6-9行,为若干个残差块组。(1+2+8+8=19个重复次数)fitters的通道数会逐渐倍增(深度逐渐加深)。最终返回的x为13*13*1024

  • ZeroPadding2D():填充x的边界为0,由(?, 416, 416, 32)转换为(?, 417, 417, 32)。

  因为下一步卷积操作的步长为2,所以图的边长需要是奇数;--- 需要padding

  • DarknetConv2D_BN_Leaky()是DarkNet的2维卷积操作,核是(3,3),步长是(2,2),

  注意,这会导致特征尺寸变小,由(?, 417, 417, 32)转换为(?, 208, 208, 64)。由于num_filters是64,所以产生64个通道。

  • compose():输出预测图y,功能是组合函数,先执行1x1的卷积操作,再执行3x3的卷积操作,filter先降低2倍后恢复,最后与输入相同,都是64; --- 残差块的另一分支
  • x = Add()([x, y])是残差(Residual)操作,将x的值与y的值相加。残差操作可以避免,在网络较深时所产生的梯度弥散问题(Vanishing Gradient Problem)。
 1 def resblock_body(x, num_filters, num_blocks):
 2     '''A series of resblocks starting with a downsampling Convolution2D
 3     '''
 4     # Darknet uses left and top padding instead of 'same' mode
 5     x = ZeroPadding2D(((1, 0), (1, 0)))(x)   # 增加一圈0 x有(?,416,416,32)-> (?,417,417,32)  Keras中的方法
 6     x = DarknetConv2D_BN_Leaky(num_filters, (3, 3), strides=(2, 2))(x)   # 步长上下2,则是窄卷积, 因此上述需padding(才会没有边缘遗失)
 7     for i in range(num_blocks):               # 此时,x的输出为208*208*64   (重复次数的循环)
 8         y = compose(
 9                 DarknetConv2D_BN_Leaky(num_filters//2, (1, 1)),   # 此时的输出为208*208*32  降一半
10                 DarknetConv2D_BN_Leaky(num_filters, (3, 3)))(x)      # 此时的输出为208 * 208 *64  恢复
11         x = Add()([x, y])   # 将残差块的结果加到源网络的结构上  (不影响运算) 208*208*64
12     return x

2. Darknet输出至y1,y2,y3部分(即13*13*1024至13*13*255. etc)

  A. 生成y1 ----> 13*13*255

1 x, y1 = make_last_layers(darknet.output, 512, num_anchors*(num_classes+5))

  make_last_layers()的内部

  该函数已经是 特征图输出的计算部分,结合结构图来观察,在13*13*1024的地方至13*13*255的y1处的运算:

  • 第1步,x执行多组1x1的卷积操作和3x3的卷积操作,filter先缩小再恢复,最后与输入的filter保持不变,仍为512(num_filters),则x由(?, 13, 13, 1024)转变为(?, 13, 13, 512);
  • 第2步,x先执行3x3的卷积操作,再执行不含BN和Leaky的1x1的卷积操作,作用类似于全连接操作,生成预测矩阵y;
  •  从结构上可以看出,x此时有两个任务,一个是继续y的运算,作为y1特征的输出;另一个是需要上采样到26*26的y2中。,所以分为了x和y两个部分,同时也会返回这两个部分。
 1 def make_last_layers(x, num_filters, out_filters):
 2     '''6 Conv2D_BN_Leaky layers followed by a Conv2D_linear layer
 3     '''
 4     x = compose(
 5             DarknetConv2D_BN_Leaky(num_filters, (1, 1)),
 6             DarknetConv2D_BN_Leaky(num_filters*2, (3, 3)),
 7             DarknetConv2D_BN_Leaky(num_filters, (1, 1)),
 8             DarknetConv2D_BN_Leaky(num_filters*2, (3, 3)),
 9             DarknetConv2D_BN_Leaky(num_filters, (1, 1)))(x)   # 此时13*13*512
10     y = compose(
11             DarknetConv2D_BN_Leaky(num_filters*2, (3, 3)),
12             DarknetConv2D(out_filters, (1,1)))(x)
13     return x, y  

  B. 生成y2 ---》26*26*255 此部分会涉及到尺度特征融合的部分,即13*13*512的上采样过程。

  • 13*13*512--》13*13*256
  • 13*13*256 上采样 26*26*256
  • 融合后的结果 26*26*768 (原来的是26*26*512,融合后即512+256 = 768)
1 # 第二部分 26*26*256 的y2
2 x = compose(
3         DarknetConv2D_BN_Leaky(256, (1,1)),          # 此时x的为13*13*256
4         UpSampling2D(2))(x)   # x需要参加的上采样   此时的x为26*26*256
5 x = Concatenate()([x, darknet.layers[152].output])  # 把之前结果的第152层的输出提取,即26*26*512的输出 即13*13*1024的前一层输出
6 x, y2 = make_last_layers(x, 256, num_anchors*(num_classes+5))    

  C. 生成y3 ----》52*52*255 此部分包含26*26*256的上采样过程

  • 26*26*256 --》 26*26*128
  • 26*26*128上采样到52*52*128
  • 融合后的结果52*52*384 (128+256=384)
1 # 第三部分 52*52*128*18(18=3*(1+5)1个类别,3个锚框) 的y3
2 x = compose(
3         DarknetConv2D_BN_Leaky(128, (1,1)),
4         UpSampling2D(2))(x)      # 同样需要26的x 的上采样
5 x = Concatenate()([x,darknet.layers[92].output])
6 x, y3 = make_last_layers(x, 128, num_anchors*(num_classes+5))

  当然,此时返回的这个x就没有什么任务了。

  模型结构创建完毕。

  可以看出,Darknet的框架是由多组Conv+Conv+Residual的结构叠加而成的。

  即1+1+1*2+1+2*2+1+8*2+1+8*2+1+4*2 = 52

  配置文件中 ############# 的分割 就是结构中darknet的结束。

  

  

  

  

posted on 2020-04-28 14:11  墨殇浅尘  阅读(2595)  评论(0编辑  收藏  举报

导航