yolov5修改骨干网络–原网络说明
yolov5修改骨干网络-使用pytorch自带的网络-以Mobilenet和efficientnet为例
yolov5修改骨干网络-使用自己搭建的网络-以efficientnetv2为例
- 增加网络的深度depth能够得到更加丰富、复杂的特征并且能够很好的应用到其它任务中。但网络的深度过深会面临梯度消失,训练困难的问题。
- 增加网络的width能够获得更高细粒度的特征并且也更容易训练,但对于width很大而深度较浅的网络往往很难学习到更深层次的特征。
- 增加输入网络的图像分辨率能够潜在得获得更高细粒度的特征模板,但对于非常高的输入分辨率,准确率的增益也会减小。但大分辨率图像会增加计算量。
efficientnet则是通过NAS搜索,同时增加width、depth以及resolution,使网络结构达到最优。
下表为EfficientNet-B0的网络框架(B1-B7就是在B0的基础上修改Resolution,Channels以及Layers),可以看出网络总共分成了9个Stage。
第一个Stage是一个卷积核大小为3x3,stride为2的普通卷积层(包含BN和Swish激活函数);
Stage2~Stage8都是在重复堆叠MBConv结构(Layers表示该Stage重复MBConv结构多少次),Stage9由一个普通的1x1的卷积层 + 平均池化层 + 全连接层组成。
MBConv后的1或6就是倍率因子n,即MBConv中第一个1x1的卷积层会将输入特征矩阵的channels扩充为n倍,其中k3x3或k5x5表示MBConv中Depthwise Conv所采用的卷积核大小。Channels表示通过该Stage后输出特征矩阵的Channels。
MBConv结构如下:
MBConv主要由一个 1x1 的卷积进行升维 (它的卷积核个数是输入特征矩阵channel的n倍,
n
∈
{
1
,
6
}
n \in \left\{1, 6\right\}
n∈{1,6},当n=1时,不升维),一个kxk的Depthwise Conv卷积,k主要有3x3和5x5两种情况,一个SE模块,然后接一个1x1的普通卷积进行降维作用,再加一个Droupout,最后再进行特征图融合。
仅当输入MBConv结构的特征矩阵与输出的特征矩阵shape相同时shortcut连接才存在(代码中可通过stride== 1 and inputc_channels==output_channels条件来判断)
SE模块,由一个全局平均池化,两个全连接层组成。
假设输入图像H×W×C,第一个全连接层的节点个数是输入该MBConv特征矩阵 channels 乘SERadio,一般SERadio为 0.25,所以channe为
C
4
\frac{C}{4}
4C ,然后是Swish激活函数。
第二个全连接层的节点个数等于Depthwise Conv层输出的特征矩阵 channels,即
C
C
C,且使用Sigmoid激活函数,这样就拉伸成了1×1×C,然后再与原图像相乘,将每个通道赋予权重。这样就实现了注意力。
classSqueezeExcite_efficientv2(nn.Module):def__init__(self, c1, c2, se_ratio=0.25, act_layer=nn.ReLU):super().__init__()
self.gate_fn = nn.Sigmoid()
reduced_chs =int(c1 * se_ratio)
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.conv_reduce = nn.Conv2d(c1, reduced_chs,1, bias=True)
self.act1 = act_layer(inplace=True)
self.conv_expand = nn.Conv2d(reduced_chs, c2,1, bias=True)defforward(self, x):# 先全局平均池化
x_se = self.avg_pool(x)# 再全连接(这里是用的1x1卷积,效果与全连接一样,但速度快)
x_se = self.conv_reduce(x_se)# ReLU激活
x_se = self.act1(x_se)# 再全连接
x_se = self.conv_expand(x_se)# sigmoid激活
x_se = self.gate_fn(x_se)# 将x_se 维度扩展为和x一样的维度
x = x *(x_se.expand_as(x))return x
Dropout层在源码实现中只有使用shortcut的时候才有Dropout层。
EfficientNetV1在训练图像的尺寸很大时,训练速度非常慢,而且非常吃显存。
在网络浅层中使用Depthwise convolutions速度会很慢。虽然Depthwise convolutions结构相比普通卷积拥有更少的参数以及更小的FLOPs,但通常无法充分利用现有的一些加速器,于是有人提出了Fused-MBConv结构去更好的利用移动端或服务端的加速器。
Fused-MBConv结构也非常简单,即将原来的MBConv结构主分支中的 conv1x1和depthwise conv3x3替换成一个普通的conv3x3,如图所示。
EfficientNetV2网络框架相比与EfficientNetV1,主要有以下不同:
- EfficientNetV2中除了使用到MBConv模块外,还使用了Fused-MBConv模块(主要是在网络浅层中使用)。
- EfficientNetV2使用较小的expansion ratio(MBConv中第一个expand conv1x1或者Fused-MBConv中第一个expand conv3x3)比如4,在EfficientNetV1中基本都是6. 这样的好处是能够减少内存访问开销。
- EfficientNetV2中更偏向使用更小(3x3)的kernel_size,在EfficientNetV1中使用了很多5x5的kernel_size。通过下表可以看到使用的kernel_size全是3x3的,由于3x3的感受野是要比5x5小的,所以需要堆叠更多的层结构以增加感受野。
- 移除了EfficientNetV1中最后一个步距为1的stage,就是EfficientNetV1中的stage8,可能是因为它的参数数量过多并且内存访问开销过大。
Conv3x3就是普通的3x3卷积 + 激活函数(SiLU)+ BN
Fused-MBConv 模块模块名称后跟的1,4表示expansion ratio,k3x3表示kenel_size为3x3,注意当expansion ratio等于1时是没有expand conv的,还有这里是没有使用到SE结构的(原论文图中有SE)。
当stride=1且输入输出channel相等时才有shortcut连接。
当有shortcut连接时才有Dropout层,而且这里的Dropout层是Stochastic Depth,即会随机丢掉整个block的主分支(只剩捷径分支,相当于直接跳过了这个block)也可以理解为减少了网络的深度。
MBConv模块和EfficientNetV1中是一样的,其中模块名称后跟的4,6表示expansion ratio,SE0.25表示使用了SE模块,0.25表示SE模块中第一个全连接层的节点个数是输入该MBConv模块特征矩阵channels的
1
4
\frac{1}{4}
41
注意当stride=1且输入输出Channels相等时才有shortcut连接。同样这里的Dropout层是Stochastic Depth。
Stride就是步距,注意每个Stage中会重复堆叠Operator模块多次,只有第一个Opertator模块的步距是按照表格中Stride来设置的,其他的默认都是1。 #Channels表示该Stage输出的特征矩阵的Channels,Layers表示该Stage重复堆叠Operator的次数。
网络代码
根据这个结构图进行代码编写,首先是一个步长为2的3x3矩阵,输出channel为24,后面当然也是有bn+激活的。这里先写一个base,通过修改yaml文件对其操作。
这一行的yaml参数应该如下:
[-1, 1, stem, [24, 3, 2]],
class stem(nn.Module):
def __init__(self, c1, c2, kernel_size=3, stride=1, groups=1):
super().__init__()# kernel_size为3时,padding 为1,kernel为1时,padding为0
padding =(kernel_size - 1) // 2# 由于要加bn层,所以不加偏置
self.conv = nn.Conv2d(c1, c2, kernel_size, stride, padding=padding, groups=groups, bias=False)
self.bn = nn.BatchNorm2d(c2, eps=1e-3, momentum=0.1)
self.act = nn.SiLU(inplace=True)
def forward(self, x):
# print(x.shape)
x = self.conv(x)
x = self.bn(x)
x = self.act(x)return x
然后是FusedMBConv,根据这个流程图编写:
注意,FusedMBConv是没有SE模块的,虽然上面画了SE。
Fused-MBConv1 后面这个1表示expansion=1,不升维;若不等于1,则升维到原维度的n倍;
后面layers=2表示使用两次这个bolck,所以第一个Fused-MBConv1, k3x3的yaml参数应为
[-1, 2, FusedMBConv, [24, 3, 1, 1, 0]]
[24:out_channer, 3:kernel_size, 1:stride,1:expansion, 0:se_ration]
# Fused-MBConv 将 MBConv 中的 depthwise conv3×3 和扩展 conv1×1 替换为单个常规 conv3×3。classFusedMBConv(nn.Module):def__init__(self, c1, c2, k=3, s=1, expansion=1, se_ration=0, dropout_rate=0.2, drop_connect_rate=0.2):super().__init__()# 当stride=1且输入输出Channels相等时才有shortcut连接,只有使用shortcut时,才用dropout
self.has_shortcut =(s ==1and c1 == c2)# 只要是步长为1并且输入输出特征图大小相等,就是True 就可以使用到残差结构连接# expansion是为了先升维,再卷积,再降维,再残差
self.has_expansion = expansion !=1# expansion==1 为false expansion不为1时,输出特征图维度就为expansion*c1,k倍的c1,扩展维度
expanded_c = c1 * expansion
if self.has_expansion:
self.expansion_conv = stem(c1, expanded_c, kernel_size=k, stride=s)
self.project_conv = stem(expanded_c, c2, kernel_size=1, stride=1)else:
self.project_conv = stem(c1, c2, kernel_size=k, stride=s)
self.drop_connect_rate = drop_connect_rate
if self.has_shortcut and drop_connect_rate >0:
self.dropout = DropPath(drop_connect_rate)defforward(self, x):if self.has_expansion:
result = self.expansion_conv(x)
result = self.project_conv(result)else:
result = self.project_conv(x)if self.has_shortcut:if self.drop_connect_rate >0:
result = self.dropout(result)
result += x
return result
stage2: Fused-MBConv4, k3x3 2 48 4
表示用kernelsize=3的卷积核,先升维4倍,outchannel=48,重复四次,注意stride=2只有在第一次重复时才有,后面三次的stride都是1,所以yaml应该写为:
第一个的stride为2
[-1, 1, FusedMBConv, [48, 3, 2, 4, 0]]
后面三个的stride为1
[-1, 3, FusedMBConv, [48, 3, 1, 4, 0]]
同理
stage 3 Fused-MBConv4, k3x3 2 64 4
[-1, 1, FusedMBConv, [64, 3, 2, 4, 0]],
[-1, 3, FusedMBConv, [64, 3, 1, 4, 0]],
然后是
stage 4 MBConv4, k3x3, SE0.25 2 128 6
表示6个MBConv模块,第一次用kernel size=3的卷积核升维四倍,SERadio为0.25,第一次的stride为2,后三次为1,输出channel为128。
yaml参数就应该为:
[-1,1, MBConv,[128,3,2,4,0.25]],# 先用步长为2的卷积[-1,5, MBConv,[128,3,1,4,0.25]],# 后面5个block用步长为1的卷积
classMBConv(nn.Module):def__init__(self, c1, c2, k=3, s=1, expansion=1, se_ration=0, dropout_rate=0.2, drop_connect_rate=0.2):super().__init__()
self.has_shortcut =(s ==1and c1 == c2)
expanded_c = c1 * expansion
self.expansion_conv = stem(c1, expanded_c, kernel_size=1, stride=1)
self.dw_conv = stem(expanded_c, expanded_c, kernel_size=k, stride=s, groups=expanded_c)
self.se = SqueezeExcite_efficientv2(expanded_c, expanded_c, se_ration)if se_ration >0else nn.Identity()
self.project_conv = stem(expanded_c, c2, kernel_size=1, stride=1)
self.drop_connect_rate = drop_connect_rate
if self.has_shortcut and drop_connect_rate >0:
self.dropout = DropPath(drop_connect_rate)defforward(self, x):# 先用1x1的卷积增加升维
result = self.expansion_conv(x)# 再用一般的卷积特征提取
result = self.dw_conv(result)# 添加se模块
result = self.se(result)# 再用1x1的卷积降维
result = self.project_conv(result)# 如果使用shortcut连接,则加入dropout操作if self.has_shortcut:if self.drop_connect_rate >0:
result = self.dropout(result)# shortcut就是到残差结构,输入输入的channel大小相等,这样就能相加了
result += x
return result
同理stage5和stage6的参数分别为:
[-1,1, MBConv,[160,3,2,6,0.25]],[-1,8, MBConv,[160,3,1,6,0.25]],[-1,1, MBConv,[256,3,2,4,0.25]],[-1,14, MBConv,[256,3,1,4,0.25]],
注意,我们不需要stage7,因为我们只需要进行特征提取,不需要进行分类
然后是修改concat连接的位置:
下面注释中很清晰的写了特征图大小变化,以及为什么要和那一层连接。默认输入图片尺寸为640*640
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license# Parameters
nc:80# number of classes
depth_multiple:1.0# model depth multiple
width_multiple:1.0# layer channel multiple
anchors:-[10,13,16,30,33,23]# P3/8-[30,61,62,45,59,119]# P4/16-[116,90,156,198,373,326]# P5/32# YOLOv5 v6.0 backbone
backbone:[[-1,1, stem,[24,3,2]],# 0-P1/2 efficientnetv2 一开始是Stem = 普通的卷积+bn+激活 640*640*3 --> 320*320*24# # [out_channel,kernel_size,stride,expansion,se_ration][-1,2, FusedMBConv,[24,3,1,1,0]],# 1 2个FusedMBConv=3*3conv+se+1*1conv 320*320*24-->320*320*24[-1,1, FusedMBConv,[48,3,2,4,0]],# 2 这里strid2=2,特征图尺寸缩小一半,expansion=4输出特征图的深度变为原来的4倍 320*320*24-->160*160*48[-1,3, FusedMBConv,[48,3,1,4,0]],# 3 三个FusedMBConv[-1,1, FusedMBConv,[64,3,2,4,0]],# 4 160*160*48-->80*80*64[-1,3, FusedMBConv,[64,3,1,4,0]],# 5[-1,1, MBConv,[128,3,2,4,0.25]],# 6 这里strid2=2,特征图尺寸缩小一半, 40*40*128[-1,5, MBConv,[128,3,1,4,0.25]],# 7[-1,1, MBConv,[160,3,2,6,0.25]],# 8 这里 strid2=2,特征图尺寸缩小一半,20*20*160[-1,8, MBConv,[160,3,1,6,0.25]],# 9[-1,1, MBConv,[256,3,2,4,0.25]],# 10 这里strid2=2,特征图尺寸缩小一半,10*10*160[-1,14, MBConv,[256,3,1,4,0.25]],# 11[-1,1, SPPF,[1024,5]],#12]# YOLOv5 v6.0 head
head:[[-1,1, Conv,[512,1,1]],# 13 10*10[-1,1, nn.Upsample,[None,2,'nearest']],# 14 20*20[[-1,9],1, Concat,[1]],# 15 cat backbone P4 15 这里特征图大小为20*20,所以应该和9号连接[-1,3, C3,[512,False]],# 16 20*20[-1,1, Conv,[256,1,1]],#17 20*20[-1,1, nn.Upsample,[None,2,'nearest']],#18 40*40[[-1,7],1, Concat,[1]],# cat backbone P3 19 7号特征图大小也是40*40[-1,3, C3,[256,False]],# 20 (P3/8-small)[-1,1, Conv,[256,3,2]],#21 卷积步长为2,所以特征图尺寸缩小,为 20*20[[-1,17],1, Concat,[1]],# cat head P4 17层的特征图也是20*20[-1,3, C3,[512,False]],# 23 (P4/16-medium) [-1,1, Conv,[512,3,2]],# 24 10*10 [[-1,13],1, Concat,[1]],# cat head P5 13层的特征图大小就是10*10[-1,3, C3,[1024,False]],# 26 (P5/32-large)[[20,23,26],1, Detect,[nc, anchors]],# Detect(P3, P4, P5)]
然后是修改yolo.py,这边很好改,后面加上
stem, FusedMBConv, MBConv
if m in[Conv, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF....
stem, FusedMBConv, MBConv]
common.py中应该添加的efficientnet代码如下:
# ------------------------------efficientnetv2--------------------------------------classstem(nn.Module):def__init__(self, c1, c2, kernel_size=3, stride=1, groups=1):super().__init__()# kernel_size为3时,padding 为1,kernel为1时,padding为0
padding =(kernel_size -1)//2# 由于要加bn层,所以不加偏置
self.conv = nn.Conv2d(c1, c2, kernel_size, stride, padding=padding, groups=groups, bias=False)
self.bn = nn.BatchNorm2d(c2, eps=1e-3, momentum=0.1)
self.act = nn.SiLU(inplace=True)defforward(self, x):# print(x.shape)
x = self.conv(x)
x = self.bn(x)
x = self.act(x)return x
defdrop_path(x, drop_prob:float=0., training:bool=False):if drop_prob ==0.ornot training:return x
keep_prob =1- drop_prob
shape =(x.shape[0],)+(1,)*(x.ndim -1)
random_tensor = keep_prob + torch.rand(shape, dtype=x.dtype, device=x.device)
random_tensor.floor_()# binarize
output = x.div(keep_prob)* random_tensor
return output
classDropPath(nn.Module):def__init__(self, drop_prob=None):super(DropPath, self).__init__()
self.drop_prob = drop_prob
defforward(self, x):return drop_path(x, self.drop_prob, self.training)classSqueezeExcite_efficientv2(nn.Module):def__init__(self, c1, c2, se_ratio=0.25, act_layer=nn.ReLU):super().__init__()
self.gate_fn = nn.Sigmoid()
reduced_chs =int(c1 * se_ratio)
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.conv_reduce = nn.Conv2d(c1, reduced_chs,1, bias=True)
self.act1 = act_layer(inplace=True)
self.conv_expand = nn.Conv2d(reduced_chs, c2,1, bias=True)defforward(self, x):# 先全局平均池化
x_se = self.avg_pool(x)# 再全连接(这里是用的1x1卷积,效果与全连接一样,但速度快)
x_se = self.conv_reduce(x_se)# ReLU激活
x_se = self.act1(x_se)# 再全连接
x_se = self.conv_expand(x_se)# sigmoid激活
x_se = self.gate_fn(x_se)# 将x_se 维度扩展为和x一样的维度
x = x *(x_se.expand_as(x))return x
# Fused-MBConv 将 MBConv 中的 depthwise conv3×3 和扩展 conv1×1 替换为单个常规 conv3×3。classFusedMBConv(nn.Module):def__init__(self, c1, c2, k=3, s=1, expansion=1, se_ration=0, dropout_rate=0.2, drop_connect_rate=0.2):super().__init__()# shorcut 是指到残差结构 expansion是为了先升维,再卷积,再降维,再残差
self.has_shortcut =(s ==1and c1 == c2)# 只要是步长为1并且输入输出特征图大小相等,就是True 就可以使用到残差结构连接
self.has_expansion = expansion !=1# expansion==1 为false expansion不为1时,输出特征图维度就为expansion*c1,k倍的c1,扩展维度
expanded_c = c1 * expansion
if self.has_expansion:
self.expansion_conv = stem(c1, expanded_c, kernel_size=k, stride=s)
self.project_conv = stem(expanded_c, c2, kernel_size=1, stride=1)else:
self.project_conv = stem(c1, c2, kernel_size=k, stride=s)
self.drop_connect_rate = drop_connect_rate
if self.has_shortcut and drop_connect_rate >0:
self.dropout = DropPath(drop_connect_rate)defforward(self, x):if self.has_expansion:
result = self.expansion_conv(x)
result = self.project_conv(result)else:
result = self.project_conv(x)if self.has_shortcut:if self.drop_connect_rate >0:
result = self.dropout(result)
result += x
return result
classMBConv(nn.Module):def__init__(self, c1, c2, k=3, s=1, expansion=1, se_ration=0, dropout_rate=0.2, drop_connect_rate=0.2):super().__init__()
self.has_shortcut =(s ==1and c1 == c2)
expanded_c = c1 * expansion
self.expansion_conv = stem(c1, expanded_c, kernel_size=1, stride=1)
self.dw_conv = stem(expanded_c, expanded_c, kernel_size=k, stride=s, groups=expanded_c)
self.se = SqueezeExcite_efficientv2(expanded_c, expanded_c, se_ration)if se_ration >0else nn.Identity()
self.project_conv = stem(expanded_c, c2, kernel_size=1, stride=1)
self.drop_connect_rate = drop_connect_rate
if self.has_shortcut and drop_connect_rate >0:
self.dropout = DropPath(drop_connect_rate)defforward(self, x):# 先用1x1的卷积增加升维
result = self.expansion_conv(x)# 再用一般的卷积特征提取
result = self.dw_conv(result)# 添加se模块
result = self.se(result)# 再用1x1的卷积降维
result = self.project_conv(result)# 如果使用shortcut连接,则加入dropout操作if self.has_shortcut:if self.drop_connect_rate >0:
result = self.dropout(result)# shortcut就是到残差结构,输入输入的channel大小相等,这样就能相加了
result += x
return result
# ------------------------------efficientnetv2--------------------------------------
版权归原作者 猛男技术控 所有, 如有侵权,请联系我们删除。