手撕yolo3系列——详解主干网络darknet53代码(详细注释)

完整代码百度云直达链接(包含预训练权重)(小白注释)
https://pan.baidu.com/s/1US6e93OaCYOghmF21v0UIA
提取码:z8at

参考链接
【注】代码是大神的代码,在此基础上添加了详细的小白注释,方便我以后阅读。

本系列代码基于yolov3的pytorch版本。
本节代码所在文件pytorch_yolo3/nets/darknet.py

darknet53网络结构图

文字版:卷积+(下采样卷积+1残差块)+(下采样卷积+2残差块)+(下采样卷积+8残差块)+(下采样卷积+8残差块)+(下采样卷积+4*残差块)

鸣谢:图片来源

在这里插入图片描述
是不是很有规律?不难看出,darknet53就是重复堆叠下采样卷积+n*残差块(n为残差块的个数)这个结构而组成的。而更基本的结构就是残差块了,因此我们先构建出残差块,然后重复堆叠上述结构darknet53就完成了。

残差块结构

残差块结构不止下图这一种,但我们只讨论darknet53中用到的这种。如下图所示,残差块结构由两条支路组成,一条支路将上一层输出的feature map进行卷积等操作,另一条支路将上一层输出的feature map恒等映射,并与刚才卷积操作完的feature map进行逐元素相加,因此两条支路的通道数必须相等。也就是说发生卷积等操作的那条支路(残差路)的输出不能改变feature map的通道数。
在这里插入图片描述
另外,darknet53在所有的卷积之后和激活(leakyrelu)之前会插入bn层(batch normalization)。

import torch
import torch.nn as nn
# 残差模块
class ResidualBlock(nn.Module):
    def __init__(self,inplanes,planes):
        # inplanes是下采样卷积完输入到残差支路的通道数,planes是一个列表,planes[0]是残差第一个卷积操作输出通道数,也是第二个卷积操作输入通道数
        # planes[1]是残差第二个卷积操作输出通道数
        super(ResidualBlock,self).__init__()
        # 残差支路的第一个卷积操作:卷积核1*1,步长1,不填充,不加偏置
        self.conv1 = nn.Conv2d(inplanes,planes[0],kernel_size=1,stride=1,padding=0,bias=False)
        # bn层的输入参数是上一层输出的通道数
        self.bn1 = nn.BatchNorm2d(planes[0])
        # LeakyReLU的参数是负半轴的斜率,正半轴是1
        self.relu1 = nn.LeakyReLU(0.1)
        # 残差支路的第二个卷积操作:卷积核3*3,步长1,填充1(p=1),不加偏置
        # 卷积输出尺寸计算公式 (n-f+2p)/s+1 此处f=3,p=1,s=1
        self.conv2 = nn.Conv2d(planes[0],planes[1],kernel_size=3,stride=1,padding=1,bias=False)
        self.bn2 = nn.BatchNorm2d(planes[1])
        self.relu2 = nn.LeakyReLU(0.1)

        # 【注】darknet的基本操作:卷积+bn+relu

    def forward(self,x):
        # 恒等映射支路
        residual = x 
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu1(out)
        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu2(out)
		# 将恒等映射与残差路逐元素相加	
        out = residual + out

基本结构:下采样卷积+残差块

图示下采样卷积+n*残差块结构:
在这里插入图片描述
在这里插入图片描述
除了第一个单独的卷积+bn+激活操作,一共有5组下采样卷积+n*残差块,按顺序是:(下采+1残差)(下采+2残差)(下采+8残差)(下采+8残差)(下采+4*残差),所以待会儿会出现一个储存残差块个数的列表blocks_num=[1,2,8,8,4]

darknet53

接下来就是构建图示下采样卷积+n*残差块这个结构了,可以把它作为类的方法放到DarkNet这个类里,在类里构建整个darknet53网络。

class DarkNet(nn.Module):
    # blocks_num是一个列表,存放着每一个(下采样卷积+n*残差块)结构中的n的值,即每个结构中残差块的个数
    def __init__(self,blocks_num):
        super(DarkNet,self).__init__()
        # darknet主干网络第一个卷积层输出通道数为32,输入通道就是下面Conv2d里面的3(输入RGB图片)
        self.inplanes = 32
        # 全网第一个也是单独的卷积+bn+激活组合
        self.conv1 = nn.Conv2d(3,self.inplanes,kernel_size=3,stride=1,padding=1,bias=False)
        self.bn1 = nn.BatchNorm2d(self.inplanes)
        self.relu1 = nn.LeakyReLU(0.1)

        # 一个layer就是一个(下采样卷积 + n * 残差块)结构,_make_layer函数就在构建该建构
        # _make_layer第一个参数可以简单视为每个残差块的输入、输出通道数,第二个参数则为残差块的数量
        self.layer1 = self._make_layer([32,64],blocks_num[0])
        self.layer2 = self._make_layer([64,128],blocks_num[1])
        self.layer3 = self._make_layer([128,256],blocks_num[2])
        self.layer4 = self._make_layer([256,512],blocks_num[3])
        self.layer5 = self._make_layer([512,1024],blocks_num[4])
		# 这三个数是darknet53三条支路(yolo_head)的输入通道数(不清楚的可以对照网络图),这一节不会用到这三个数,在yolo3里面构建yolo_head支路时用到
        self.layers_out_filters = [256, 512, 1024]

        # 初始化卷积、bn层的权重
        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 _make_layer(self,planes,blocks):
        layers = []
        # ds:down sample,下采样的卷积操作
        # 把层的名字及具体对象加入layers列表中,方便后面转换成字典
        layers.append(("ds_conv",nn.Conv2d(self.inplanes,planes[1],kernel_size=3,stride=2,padding=1)))
        layers.append(("ds_bn",nn.BatchNorm2d(planes[1])))
        layers.append(("ds_relu",nn.LeakyReLU(0.1)))
        # 进行完下采样conv后更新一下inplanes,作为下一个layer的输入通道,同时也是残差模块的输入通道数
        self.inplanes = planes[1]

        for i in range(0,blocks):
            # 构建若干个残差块,blocks为残差块的数量
            # 每个layers列表里包含[("ds_conv",Conv2d),("ds_bn",BatchNorm2d),("ds_relu",LeakyReLU),("residual_0",BasicBlock),("residual_1",BasicBlock)...]
            layers.append(("residual_{}".format(i),ResidualBlock(self.inplanes,planes)))

        # 将每个layers列表转化成有序字典(可以用层名字来索引该层具体对象)
        # nn.Sequential可以简单理解成把层都打包起来组成(下采样卷积 + n * 残差块)结构
        # 等待传入一个输入,就可以通过forward按顺序调用各个层并得到一个输出了
        return nn.Sequential(OrderedDict(layers))

    def forward(self,x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu1(x)
        x = self.layer1(x)
        x = self.layer2(x)
        # 输出三路分支
        out3 = self.layer3(x)
        out4 = self.layer4(out3)
        out5 = self.layer5(out4)

        return out3, out4, out5

至此,darkne53网络已经构建完成,可以定义一个函数创建生成该模型。

def darknet53(pretrained, **kwargs):
    model = DarkNet([1, 2, 8, 8, 4])
    # 如果有导入预训练权重的路径,切路径正确,则导入预训练权重
    # 注意这里的预训练权重只是darknet53的,并不是整个yolo3的权重,yolo_head还没有构建
    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

同时也可以把模型打印出来看看结构

model = DarkNet([1, 2, 8, 8, 4])
print(model)

我只截取网络开头的一部分
在这里插入图片描述
如上图所示:网络开始单独的卷积+bn+激活操作,接着就是layer1,它是_make_layer函数的返回值,这个函数返回的是nn.Sequential,从输出的信息也证实了这点。nn.Sequential打包的就是下采样卷积+n*残差块这个结构了,因为layer1只有一个残差块(忘了为何只有一个的话回去看看带绿框的那个图),所以只有residual_0这么一个模块。如果有多个残差块的话,后面会追加residual_1、residual_2…

等等,刚才DarkNet返回的out3, out4, out5好像还没有用上,既然返回了就肯定有用,这就是下一节要讲的内容了。

下一节:详解yolo3整体网络代码

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值