YOLOv5系列(二十四) 解析C3,Focus及其替换为6*6卷积(详尽)

118 篇文章 2 订阅
52 篇文章 14 订阅

C3

一、定义自动padding

  • 为了保持图像大小卷积前后一致,就需要用到自动padding
def autopad(k, p=None):                        # kernel  padding 根据卷积核大小k自动计算卷积核padding数(0填充)
    """
    :param k: 卷积核的 kernel_size
    :param p: 卷积的padding  一般是None
    :return:  自动计算的需要pad值(0填充)
    """
    if p is None:
        # k 是 int 整数则除以2, 若干的整数值则循环整除
        p = k // 2 if isinstance(k, int) else [x // 2 for x in k]
    return p

二、基本Conv模块

  • C3, SPP, SPPF模块中都有基本的Conv模块,需要编写基本的Conv模块,方面复用,提高代码的复用性
    在这里插入图片描述
class Conv(nn.Module):
    def __init__(self, c1, c2, k=1, s=1, p=None, act=True, g=1):
        """
        :param c1: 输入的channel值
        :param c2: 输出的channel值
        :param k: 卷积的kernel_size
        :param s: 卷积的stride
        :param p: 卷积的padding  一般是None
        :param act: 激活函数类型   True就是SiLU(), False就是不使用激活函数
        :param g: 卷积的groups数  =1就是普通的卷积  >1就是深度可分离卷积
        """
        super(Conv, self).__init__()

        self.conv_1 = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=True)
        self.bn = nn.BatchNorm2d(c2)

        self.act = nn.SiLU() if act else nn.Identity()     # 若act=True, 则激活,  act=False, 不激活

    def forward(self, x):

        return self.act(self.bn(self.conv_1(x)))

三、Bottleneck模块

  • Bottleneck模块中包含一个残差连接结构(左),和不包含的残差结构(右),就需要传入参数,来判断是否需要使用残差结构
    在这里插入图片描述
class Bottleneck(nn.Module):
    def __init__(self, c1, c2, e=0.5, shortcut=True, g=1):
        """
        :param c1: 整个Bottleneck的输入channel
        :param c2: 整个Bottleneck的输出channel
        :param e: expansion ratio  c2*e 就是第一个卷积的输出channel=第二个卷积的输入channel
        :param shortcut: bool Bottleneck中是否有shortcut,默认True
        :param g: Bottleneck中的3x3卷积类型  =1普通卷积  >1深度可分离卷积
        """
        super(Bottleneck, self).__init__()

        c_ = int(c2*e)                            # 使通道减半, c_具体多少取决于e
        self.conv_1 = Conv(c1, c_, 1, 1)
        self.conv_2 = Conv(c_, c2, 3, 1, g=g)
        self.add = shortcut and c1 == c2

    def forward(self, x):
        return x + self.conv_2(self.conv_1(x)) if self.add else self.conv_2(self.conv_1(x))

四、C3模块

  • 在这里插入图片描述
class C3(nn.Module):
    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
        """
        :param c1: 整个 C3 的输入channel
        :param c2: 整个 C3 的输出channel
        :param n: 有n个Bottleneck
        :param shortcut: bool Bottleneck中是否有shortcut,默认True
        :param g: C3中的3x3卷积类型  =1普通卷积  >1深度可分离卷积
        :param e: expansion ratio
        """
        super(C3, self).__init__()
        c_ = int(c2 * e)
        self.cv_1 = Conv(c1, c_, 1, 1)
        self.cv_2 = Conv(c1, c_, 1, 1)
        # *操作符可以把一个list拆开成一个个独立的元素,然后再送入Sequential来构造m,相当于m用了n次Bottleneck的操作
        self.m = nn.Sequential(*[Bottleneck(c_, c_, e=1, shortcut=True, g=1) for _ in range(n)])
        self.cv_3 = Conv(2*c_, c2, 1, 1)

    def forward(self, x):
        return self.cv_3(torch.cat((self.m(self.cv_1(x)), self.cv_2(x)), dim=1))

五、其他模块

class BottleneckCSP(nn.Module):
    def __init__(self, c1, c2, e=0.5, n=1):
        """
        :param c1: 整个BottleneckCSP的输入channel
        :param c2: 整个BottleneckCSP的输出channel
        :param e: expansion ratio c2*e=中间其他所有层的卷积核个数/中间所有层的输入输出channel数
        :param n: 有 n 个Bottleneck
        """
        super(BottleneckCSP, self).__init__()
        c_ = int(c2*e)
        self.conv_1 = Conv(c1, c_, 1, 1)
        self.m = nn.Sequential(*[Bottleneck(c_, c_, e=1, shortcut=True, g=1) for _ in range(n)])
        self.conv_3 = Conv(c_, c_, 1, 1)
        self.conv_2 = Conv(c1, c_, 1, 1)
        self.bn = nn.BatchNorm2d(2*c_)
        self.LeakyRelu = nn.LeakyReLU()
        self.conv_4 = Conv(2*c_, c2, 1, 1)

    def forward(self, x):
        x_1 = self.conv_3(self.m(self.conv_1(x)))
        x_2 = self.conv_2(x)
        x_3 = torch.cat([x_1, x_2], dim=1)
        x_4 = self.LeakyRelu(self.bn(x_3))
        x = self.conv_4(x_4)
        return x
class SPP(nn.Module):
    def __init__(self, c1, c2, e=0.5, k1=5, k2=9, k3=13):
        """
        :param c1: SPP模块的输入channel
        :param c2: SPP模块的输出channel
        :param e: expansion ratio
        :param k1: Maxpool 的卷积核大小
        :param k2: Maxpool 的卷积核大小
        :param k3: Maxpool 的卷积核大小
        """
        super(SPP, self).__init__()
        c_ = int(c2*e)
        self.cv_1 = Conv(c1, c_, 1, 1)
        self.pool_1 = nn.MaxPool2d(kernel_size=k1, stride=1, padding=k1 // 2)
        self.pool_2 = nn.MaxPool2d(kernel_size=k2, stride=1, padding=k2 // 2)
        self.pool_3 = nn.MaxPool2d(kernel_size=k3, stride=1, padding=k3 // 2)
        self.cv_2 = Conv(4*c_, c2, 1, 1)

    def forward(self, x):
        return self.cv_2(torch.cat((self.pool_1(self.cv_1(x)), self.pool_2(self.cv_1(x)), self.pool_3(self.cv_1(x)), self.cv_1(x)), dim=1))

六、编写模块构建网络实现分类

6.1 数据集操作

total_dir = './weather_photos/'

transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize(std=[0.5, 0.5, 0.5], mean=[0.5, 0.5, 0.5])
])

total_data = torchvision.datasets.ImageFolder(total_dir, transform)
print(total_data)
print(total_data.class_to_idx)

idx_to_class = dict((v, k) for k,v in total_data.class_to_idx.items())
print(idx_to_class)

train_size = int(len(total_data) * 0.8)
test_size = int(len(total_data)) - train_size

train_dataset, test_dataset = torch.utils.data.random_split(total_data, [train_size, test_size])

train_dataloader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_dataloader = DataLoader(test_dataset, batch_size=32, shuffle=True)

6.1 构建网络

  • 在编写上述模块时,发现最主要的参数就是输入通道,和输入通道,只需要保证调用模块时,上次一层的输入通道与下一层输出通道保持一致就行。

6.2 linear神经元个数判定

  • 在构建网络模块时不知道第一个全连接神经元个数,可用如下方法进行判断,构建好初全连接以外的层,在forward进行传播,并在最后一个层前向传播完进行打印size。
class model(nn.Module):
	def __init__(self):
        super(model, self).__init__()

        self.conv = Conv(3, 32, 3, 2)                   # 3:输入通道  32:输出通道  3: kernel  2:stride
        self.spp = SPP(32, 64)
        self.c3 = C3(64, 128, n=1, shortcut=True, g=1, e=0.5)
        # 接下来就是 linear 层

    def forward(self, x):
        x = self.conv(x)
        x = self.spp(x)
        x = self.c3(x)
        print(x.size())
        return x


device = 'cuda' if torch.cuda.is_available() else 'cpu'
model = model().to(device)

for x, y in train_dataloader:
    x, y = x.to(device), y.to(device)
    y_pre = model(x)
    break
  • 打印输出 torch.Size([32, 128, 112, 112]), 32为batch-size,全连接神经元个数就是128112112,
  • 重新编写网络结构函数
class model(nn.Module):
    def __init__(self):
        super(model, self).__init__()

        self.conv = Conv(3, 32, 3, 2)                   # 3:输入通道  32:输出通道  3: kernel  2:stride
        self.spp = SPP(32, 64)
        self.c3 = C3(64, 128, n=1, shortcut=True, g=1, e=0.5)
        self.linear = nn.Sequential(
            nn.Linear(128*112*112, 1000),
            nn.ReLU(),

            nn.Linear(1000, 4)
        )

    def forward(self, x):
        x = self.conv(x)
        x = self.spp(x)
        x = self.c3(x) 
        x = x.view(-1, 128*112*112)
        x = self.linear(x)
        return x
  • 并运行代码: Process finished with exit code 0
  • 按照之前的代码进行训练评估即可

Foucs

1.Focus简介

Foucs模块使将原本的feature map按下图方式分成四份,再concat到一起进行一次卷积.
Foucs
这里是Focus模块的源码:

class Focus(nn.Module):
    # Focus wh information into c-space
    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True):  
    # ch_in, ch_out, kernel, stride, padding, groups
        super().__init__()
        self.conv = Conv(c1 * 4, c2, k, s, p, g, act)
def forward(self, x):  # x(b,c,w,h) -> y(b,4c,h/2,w/2)
        return self.conv(torch.cat([x[..., ::2, ::2], x[..., 1::2, ::2], 
        					x[..., ::2, 1::2], x[..., 1::2, 1::2]], 1))

为了理解torch.cat()中拼接部分具体操作,我们需要自己做个小实验验证一下:

import torch
import numpy as np
import torch.nn as nn

# foucs 结构
a = torch.tensor([[[[0,1],[2,3]],
                [[0,1],[2,3]],
                [[0,1],[2,3]],
                ]])

print(a.size())  # torch.Size([1, 3, 2, 2]) (N,C,H,W)
print(a[...].size()) # ...第0维上所有都取 torch.Size([1, 3, 2, 2])
print(a[...,:2,:2].size()) #  :2 取该维度上前两位  torch.Size([1, 3, 2, 2])
print(a[...,::2,::2].size()) # ::2 取该维度上总数/2 torch.Size([1, 3, 1, 1])
print(a[...,::2,::2].size(), '\n', a[...,::2,::2]) # torch.Size([1, 3, 1, 1])  取0
print(a[...,1::2,::2].size(), '\n', a[...,1::2,::2]) # torch.Size([1, 3, 1, 1]) 取2
print(a[...,::2,1::2].size(), '\n', a[...,::2,1::2]) # torch.Size([1, 3, 1, 1]) 取1
print(a[...,1::2,1::2].size(), '\n', a[...,1::2,1::2]) # torch.Size([1, 3, 1, 1]) 取3
b = torch.cat([a[...,::2,::2], a[...,1::2,::2]
                ,a[...,::2,1::2], a[...,1::2,1::2]],1) # 最后的1代表在通道数上拼接
print(b.size())# torch.Size([1, 12, 1, 1])
print(b)
# tensor([[[[0]], [[0]],[[0]],[[2]],[[2]], [[2]],[[1]],[[1]],[[1]],[[3]],[[3]],[[3]]]])

a.shape[1, 3, 2, 2]变至[1, 12, 1, 1],通道数增大四倍,w与h缩小两倍.
a[...,::2,::2] 取左上角
a[...,1::2,::2]取左下角
a[...,::2,1::2]取右上角
a[...,1::2,1::2]取右下角
再通过torch.cat()其在第二个维度上堆叠起来,在对其进行kernel_size=3,stride=1卷积特征提取.

2.对Focus的疑问

github上有人问过focus对mAP的影响,原作者是这么回答的
在这里插入图片描述
大意是Foucs()模块是为了减少了FLOPs并增加计算速度设计的,并不会增加mAP;另一方面1个foucs模块代替了3个yolov3/4层.
我们来计算一下focus的参数量与计算量(忽略bias)
F L O P s = ( 3 ∗ 4 ) ∗ 32 ∗ 3 ∗ 3 ∗ 320 ∗ 320 = 353894400 FLOPs = (34)3233320320=353894400 FLOPs=(3∗4)∗32∗3∗3∗320∗320=353894400
P a r a m = 3 ∗ 4 ∗ 32 ∗ 3 ∗ 3 = 3456 Param =343233=3456 Param=3∗4∗32∗3∗3=3456
再来计算一下卷积层的参数量与计算量(忽略bias)
F L O P s = 3 ∗ 32 ∗ 3 ∗ 3 ∗ 320 ∗ 320 = 88473600 FLOPs = 33233320320=88473600 FLOPs=3∗32∗3∗3∗320∗320=88473600
P a r a m = 3 ∗ 32 ∗ 3 ∗ 3 = 864 Param =3
3233=864 Param=3∗32∗3∗3=864
一个Focus的参数量与计算量大约是conv的4倍,但一个focus结构可以替换3个conv层,所以参数量与计算量都是减少的.
再来计算一下k=6卷积层的参数量与计算量(忽略bias)
F L O P s = 3 ∗ 32 ∗ 6 ∗ 6 ∗ 320 ∗ 320 = 35389440 FLOPs = 33266320320=35389440 FLOPs=3∗32∗6∗6∗320∗320=35389440
P a r a m = 3 ∗ 32 ∗ 6 ∗ 6 = 3456 Param =3
3266=3456 Param=3∗32∗6∗6=3456
所以k=6卷积层在理论上等价于focus结构
那么focus虽然降低了计算成本,但是其采样方式会不会破坏图片的空间信息或者混淆GT框的边界坐标呢?
在这里插入图片描述
将空间信息堆叠到通道空间中,可能会减少一个像素的回归信息价值,但对于大多数实例而言回归精度都不会接近一个像素,因此focus的采样方式并对回归精度产生影响极小.另外focus结构的位置问题,在分类网络中放在后面可能会更有用,但是在目标检测模型中对mAP@0.5:0.95的影响更大,mAP@0.5影响较小.
对于focus的采样方式是否会破坏原有图像的空间信息,我们只要将focus的采样的4个模块输出来看一看即可.

import cv2
import numpy as np
img = cv2.imread(r'C:/Users/HP/Desktop/p.jpg')
# print(img.shape,"\n",img)
# img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
new_img = cv2.resize(img, (320, 320))
cv2.imwrite(r'./0.jpg',new_img)
img = img.transpose(2,1,0)
img = np.expand_dims(img, 0)
print(img.shape) # (1, 3, 640, 640)
# cv2.imshow('img',img)
# cv2.wimgitKey(0)
# print(img[...,::2,::2].size(), '\n', img[...,::2,::2])
# print(img[...,1::2,::2].size(), '\n', img[...,1::2,::2])
# print(img[...,::2,1::2].size(), '\n', img[...,::2,1::2])
# print(img[...,1::2,1::2].size(), '\n', img[...,1::2,1::2])
new_img = img[...,::2,::2]
print(new_img.shape) # (1, 3, 160, 160)
new_img = new_img[0]  
print('shape',new_img.shape)  # shape (3, 160, 160)
new_img = new_img.transpose(2,1,0) # 维度转换
cv2.imwrite(r'./1.jpg',new_img)
new_img = img[...,1::2,::2]
print(new_img.shape) # (1, 3, 320, 320)
new_img = new_img[0]
new_img = new_img.transpose(2,1,0)
cv2.imwrite(r'./2.jpg',new_img)
new_img = img[...,::2,::2]
print(new_img.shape)  # (1, 3, 320, 320)
new_img = new_img[0]
new_img = new_img.transpose(2,1,0)
cv2.imwrite(r'./3.jpg',new_img)
new_img = img[...,1::2,1::2]
print(new_img.shape) # (1, 3, 320, 320)
new_img = new_img[0]
new_img = new_img.transpose(2,1,0)
cv2.imwrite(r'./4.jpg',new_img)
img1 = cv2.imread(r'./1.jpg')
img2 = cv2.imread(r'./2.jpg')
img3 = cv2.imread(r'./3.jpg')
img4 = cv2.imread(r'./4.jpg')
h = np.hstack((img1, img2, img3, img4))
cv2.imwrite(r'./5.jpg',h)

原图3x640x640:
在这里插入图片描述
focus模块下采样后四张图片3x320x320:
在这里插入图片描述
通过两幅图片对比得知,经过focus下采样得到的特征图并不会丢失特征信息,对语义的影响并不大,且下采样后的每一个像素点的感受野都增大了,与卷积层的下采样方式一样,没有丢失原图的信息.

3.Focus为何又被替换成卷积层

yolov5后期将focus结构又替换成了k=6的卷积层,我猜想可能是因为这样的方式来代替3个conv层的原因是在提高了运算速度前提下,运用较大的卷积核增大了感受野,在性能方面可能会获得了一定的提升.
另一方面,考虑到focus的切片处理,与卷积相比,多出了几次运算concat操作,可能会增加一定的内存开销.

4.Focus是否可以作为一种数据增强的处理方式

Focus可以被看作一种下采样方式,与transforms.Resize相比哪种方式的效果更好,或是可以在调整图片大小时将两种方式混用,在以后的实验中可以一试.

#------------------------------------------------------------------------------------------------------------------------------------------------------------------------
2022.6.26
更正一下focus的感受野,与6x6conv感受野大小相同,并不是kener_size设为6会增大感受野
在这里插入图片描述
4个3x3感受野相加相当于原图上6x6,所以focus与6x6的conv在计算量,参数量和感受野应该是相同的,因此理论上相同。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小酒馆燃着灯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值