yolov3 原理代码复现1

yolov3 整个网络代码复现1

yolov3 代码复现2之loss函数的实现
yolov3代码复现3之图像预测处理
2020 4.13
最近学了一下神经网络,将基本的各种网络结构敲出了之后,打算研究一下yolov3,说实话,我本以为网络搭建是最复杂的部分,但是这一周下来,网络搭建部分恰恰是最简单的,因为这部分论文里面有,而且大部分博文都介绍的很详细。其余部分,特别是对loss的处理,花费的时间更多。中间走了很多弯路,有时候看其他人的解析,看他人代码的时候过于大意,导致对有些关键的地方理解不到位甚至理解错误,中间有很多次,我都劝自己不要弄了,只要远离大概弄清楚就行,没必要代码复现出来,可能是由于强迫症,不甘心走到快结束了选择放弃,所以在烦闷的心情之下不停的看别人源代码,看不下去了然后玩会手机,最终总算将百分之八十的部分敲出来了,但是由于疫情影响,还在家里,家里只有一台六年前的笔记本,跑神经网络的代码肯定跑步起来,所以最后的加载数据,训练数据这些代码就没写了,可能以后回学校了会写。
其实,这篇文章,我是基于研究了快一周的yolov3,不写出来,总感觉对不起自己,既是对整个过程的总结,也算是给后面对yolov3感兴趣的同学踩坑吧。第一次写博客,很可能会出现逻辑混乱的情况,希望大家谅解一下。而且我的博文的代码,由于时间有限,加上电脑跑步起来,所以是没有做实验的,而且,不像darknet这些已经搭建好的框架可以随意的更改网络,可以随意的选择数据集进行训练,我强调一下,我的这篇博文只是针对yolov3的原理的复现,如何将这些代码广泛应用是我没考虑的,也没必要考虑,因为你要是要利用yolov3训练模型,现成的darknet这个框架就可以直接用,所以如果读者用我的代码去实现自己的想法,肯定很多地方需要更改的,当然,如果是原理的理解不到位导致代码的原理就出现了问题,欢迎大家在留言指正。在这里得感谢小小小绿叶这位博主,他对yolov3的讲解为我梳理整个流程帮助很大。下面是正文。

环境:python + pytorch
提前预备知识:神经网络的搭建,cnn,resnet这些知识,我就不再累述,我相信会看这篇文章的人,这一部分的知识都是具备了的。
yolov3原理这篇文章对整个yolov3的原理介绍的很清楚,我就不再介绍了,介绍原理,这位博主才是专业的。
理解了基本原理,我们首先要搭建的就是网络结构了,我是完全按照论文里面的说明,用resnet搭建的。基本的网络结构如下。yolov3结构图
对于这张图的理解,我相信,看了我上面推荐的博主的文章,对这图片的基本含义还是可以做到大概知道个意思了。好,接下来我们看看这张照片的左下角,在这个左下角,我们可以看见DBL,这是一个基本神经元,这个基本神经元是根据conv-batchnormalnize-relu组成的,对于这个基本单元,我们代码实现如下。

'''this code is designed by nike hu'''
#最基本的单元,conv-batch-leakyRelu
class DBL(nn.Module):
    def __init__(self, chin, chout, kernel=1, stride = 1, padding=1):
        super(DBL, self).__init__()
        self.net = nn.Sequential(
            nn.Conv2d(chin, chout, kernel_size=kernel, stride=stride, padding=padding),
            nn.BatchNorm2d(chout),
            nn.LeakyReLU(leakReluRate)
        )

    def forward(self, x):
        x = self.net(x)
        # print(x.shape)
        return x

在基本的单元实现之后,我们接下来看看DBL旁边的res_unit,这是一个基本的resnet结构,对于这部分,我们代码实现如下:

'''this code is designed by nike hu'''
#整体单元,两个DBL加上一个短接
class Res_Unit(nn.Module):
    def __init__(self, chin, chout):
        super(Res_Unit, self).__init__()
        self.block = nn.Sequential(
            DBL(chout, chin, kernel=1, stride=1, padding=0).net,
            DBL(chin, chout, kernel=3, stride=1, padding=1).net
        )
        self.extra = nn.Sequential()
        if chout != chout:
            self.extra = nn.Sequential(
                DBL(chin, chout, kernel=1, stride=1, padding=0).net
            )

    def forward(self, x):
        x1 = self.block(x)
        x2 = self.extra(x)
        x = x1 + x2
        # print(x1.shape, x2.shape, x.shape)
        return x

在我们将基本的resnet单元搭建出来之后,我们接下来就可以按照论文来搭建网络了,这个网络,如下图所示:
整个网络搭建流程
整个网络的搭建流程,我们可以很清晰的根据这张图来了解,在这张图里面,1x,2x, 8x,代表的是有1个2个或者8个resnet基本单元,也就是上面的Res_uint,那么问题就来了,我们上面是定义的一个Res_unit,对于多个res_unit,我们要怎么构建呢?别慌,接下来就来了。如下:

'''this code is designed by nike hu'''
#接下来定义带有个数的基本单元
class Num_unite(nn.Module):
    def __init__(self, chin, chout, number):
        super(Num_unite, self).__init__()
        blocks = []
        for i in range(number):
            blocks.append(Res_Unit(chin, chout))
        self.net = nn.Sequential(*blocks)

    def forward(self, x):
        x = self.net(x)
        # print(x.shape)
        return x

通过上面的代码,我们就可以达到随意的根据自己的需求来搭建多个Res_unit,是不是很神奇啊,哈哈,开个玩笑,对各位大佬,这些都是小儿科。
好了,接下来我们就要开始搭建整个网络,我们现在搭建整个yolov3的网络了,而上面的那张图只能实现yolov3的主体部分,也就是第一张图红色虚线的那一部分。对于红色边框后面的这部分,我们需要根据对第一张图的理解去构建,还好,有些博主很厉害,将yolov3整个流程,包括主体和后面三个分支的过程都弄得很详细,我这就直接拿来用了,因为我也不知道这我这篇文章的所有图片究竟最初的地方在哪,所以就不写这些图片的出处了,各位见谅。具体的请看下面图片:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
仔细看着三张图片,图片中的第一次第二次下采样这些,就是每次res_unit集合体向前传导的过程。每次的detection就是对每个分支的处理。对于yolov3整个网络的代码,如下:

'''this code is designed by nike hu'''
# 正式构建darknet-53
class Darknet_53(nn.Module):
    def __init__(self):
        super(Darknet_53, self).__init__()
        self.layer1 = DBL(3, 32, kernel=3, stride=1, padding=1)
        self.layer2 = DBL(32, 64, kernel=3, stride=2, padding=1)
        self.layer3 = Num_unite(32, 64, 1)
        self.layer4 = DBL(64, 128, kernel=3, stride=2, padding=1)
        self.layer5 = Num_unite(64, 128, 2)
        self.layer6 = DBL(128, 256, kernel=3, stride=2, padding=1)
        self.layer7 = Num_unite(128, 256, 8)
        self.layer8 = DBL(256, 512, kernel=3, stride=2, padding=1)
        self.layer9 = Num_unite(256, 512, 8)
        self.layer10 = DBL(512, 1024, kernel=3, stride=2, padding=1)
        self.layer11 = Num_unite(512, 1024, 4)
        # 下面这个是卷积层之后最上面那一层的处理
        self.bk1 = nn.Sequential(
            DBL(1024, 512, kernel=1, stride=1, padding=0),
            DBL(512, 1024, kernel=3, stride=1, padding=1),
            DBL(1024, 512, kernel=1, stride=1, padding=0),
            DBL(512, 1024, kernel=3, stride=1, padding=1),
            DBL(1024, 512, kernel=1, stride=1, padding=0)
        )
        self.bk2 = nn.Sequential(
            DBL(512, 1024, kernel=3, stride=1, padding=1),
            nn.Conv2d(1024, 255, kernel_size=1, stride=1, padding=0)
        )
        # 第二条通道和第一条某个值维度加起来,反卷积和上采样不一样
        self.route1 = nn.Sequential(
            DBL(1024, 256, kernel=1, stride=1, padding=0),
            nn.Upsample(scale_factor=2, mode='nearest')
            # nn.ConvTranspose2d(1024, 256, kernel_size=1, stride=2, padding=0)
        )
        # 第二条通道的处理
        self.route1_conv = nn.Sequential(
            DBL(768, 256, kernel=1, stride=1, padding=0),
            DBL(256, 512, kernel=3, stride=1, padding=1),
            DBL(512, 256, kernel=1, stride=1, padding=0),
            DBL(256, 512, kernel=3, stride=1, padding=1),
            DBL(512, 256, kernel=1, stride=1, padding=0)
        )
        # 第二条通道第二次处理
        self.route1_conv_last = nn.Sequential(
            DBL(256, 512, kernel=3, stride=1, padding=1),
            nn.Conv2d(512, 255, kernel_size=1, stride=1, padding=0)
        )
        # 第三条通道的处理
        self.route2 = nn.Sequential(
            DBL(256, 128, kernel=1, stride=1, padding=0),
            nn.Upsample(scale_factor=2, mode='nearest')
        )
        self.route2_conv = nn.Sequential(
            DBL(384, 128, kernel=1, stride=1, padding=0),
            DBL(128, 256, kernel=3, stride=1, padding=1),
            DBL(256, 128, kernel=1, stride=1, padding=0),
            DBL(128, 256, kernel=3, stride=1, padding=1),
            DBL(256, 128, kernel=1, stride=1, padding=0),
            DBL(128, 256, kernel=3, stride=1, padding=1),
            nn.Conv2d(256, 255, kernel_size=1, stride=1, padding=0)
        )

    def forward(self, x):
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        x = self.layer5(x)
        x = self.layer6(x)
        x = self.layer7(x)
        x3 = x # 这是倒数第三个卷积层的输出
        # print('x3->', x3.shape)
        x = self.layer8(x)
        x = self.layer9(x)
        x2 = x # 这是倒数第二个卷积层的输出
        # print('x2->', x2.shape)
        x = self.layer10(x)
        x = self.layer11(x)
        x1 = x # 这个是卷积层最后的输出
        # print('x1->', x1.shape)
        x1 = self.bk1(x1)
        # print(x.shape)
        x1 = self.bk2(x1)
        # print('第一个通道的最后输出维度->', x1.shape)
        x1_1 = self.route1(x) # x1上采样完成
        x2 = torch.cat([x1_1, x2], dim=1)
        # print(x1.shape)
        x2 = self.route1_conv(x2)
        # print(x1.shape)
        x2_1 = x2
        x2 = self.route1_conv_last(x2)
        # print('这是第二个通道的输出维度', x2.shape)
        x3_1 = self.route2(x2_1)
        x3 = torch.cat([x3, x3_1], dim=1)#通道合并
        # print('第二条通道上和第三个合并后->', x3.shape)
        x3 = self.route2_conv(x3)
        # print('第三个通道输出维度_>', x3.shape)
        return x1, x2, x3

整个网络的搭建就是这样了,有兴趣的同学可以去试一下,输入的维度是[batchsize, 3, 416, 416],我们最后的输出有三个尺度,13x13, 26x26,52x52,用维度表示,就是[batchsize, 255, 13, 13],[batchsize, 255, 26, 26],[batchsize, 255, 52, 52]。注意了,为什么第二个维度是255,这个不是随意来的,255=3*(5+80),5指的是x,y,w,h,置信度,80指的是数据集的类别,我们使用的是coco数据集,而coco数据集的类别是80,所以对于不同的数据集,类别总数不一样,导致第二个维度的值也不一样,这也是我为什么说我的代码可扩展性不行,要训练自己的数据集,要更改的地方太多,真要训练数据集,去学学darknet框架就可以了,或者github上找对应的源代码。至于上面的3,各种博文里面指的是3个anchor,我的理解是,如果把最后输出的13x13,26x26这些当成一张照片(实际上不是照片,是feature map),那么就是对于这张照片的每个像素,有三个不同尺度的边框去对这个像素进行预测。下面是我对这个网络的实验结果:

if __name__ == '__main__':
    x = torch.randn(1, 3, 416, 416)
    net = Darknet_53()
    x1, x2, x3 = net(x)
    print(x1.shape, x2.shape, x3.shape)
# 输出结果:
torch.Size([1, 255, 13, 13]) torch.Size([1, 255, 26, 26]) torch.Size([1, 255, 52, 52])

好了,这篇文章咱就将整个网络的基本情况以及代码的实现介绍了,第二篇我们将介绍整个网络向前走的过程中的loss的计算,这一部分,让我纠结了很久,希望能让大家少走一些弯路。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值