机器视觉笔记6——Darknet53代码解析

一、Darknet53源代码

        Darknet53模型原理参考之前博文机器视觉笔记4——DarkNet53模型详细解析-CSDN博客 ,参考Bubbliiiing-CSDN博客Darknet53源代码如下:

import math
from collections import OrderedDict

import torch.nn as nn


#---------------------------------------------------------------------#
#   残差结构
#   利用一个1x1卷积下降通道数,然后利用一个3x3卷积提取特征并且上升通道数
#   最后接上一个残差边
#---------------------------------------------------------------------#
class BasicBlock(nn.Module):
    def __init__(self, inplanes, planes):
        super(BasicBlock, self).__init__()
        self.conv1  = nn.Conv2d(inplanes, planes[0], kernel_size=1, stride=1, padding=0, bias=False)
        self.bn1    = nn.BatchNorm2d(planes[0])
        self.relu1  = nn.LeakyReLU(0.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)

    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
        return out

class DarkNet(nn.Module):
    def __init__(self, layers):
        super(DarkNet, self).__init__()
        self.inplanes = 32
        # 416,416,3 -> 416,416,32
        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)

        # 416,416,32 -> 208,208,64
        self.layer1 = self._make_layer([32, 64], layers[0])
        # 208,208,64 -> 104,104,128
        self.layer2 = self._make_layer([64, 128], layers[1])
        # 104,104,128 -> 52,52,256
        self.layer3 = self._make_layer([128, 256], layers[2])
        # 52,52,256 -> 26,26,512
        self.layer4 = self._make_layer([256, 512], layers[3])
        # 26,26,512 -> 13,13,1024
        self.layer5 = self._make_layer([512, 1024], layers[4])

        self.layers_out_filters = [64, 128, 256, 512, 1024]

        # 进行权值初始化
        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_()

    #---------------------------------------------------------------------#
    #   在每一个layer里面,首先利用一个步长为2的3x3卷积进行下采样
    #   然后进行残差结构的堆叠
    #---------------------------------------------------------------------#
    def _make_layer(self, planes, blocks):
        layers = []
        # 下采样,步长为2,卷积核大小为3
        layers.append(("ds_conv", nn.Conv2d(self.inplanes, planes[1], kernel_size=3, stride=2, padding=1, bias=False)))
        layers.append(("ds_bn", nn.BatchNorm2d(planes[1])))
        layers.append(("ds_relu", nn.LeakyReLU(0.1)))
        # 加入残差结构
        self.inplanes = planes[1]
        for i in range(0, blocks):
            layers.append(("residual_{}".format(i), BasicBlock(self.inplanes, planes)))
        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

def darknet53():
    model = DarkNet([1, 2, 8, 8, 4])
    return model

二、class BasicBlock()

        代码中引用了类class BasicBlock定义了神经网络基本构建块,该块由两个卷积层组成,后跟批归一化和激活函数。

        其中,类作为对象的抽象,定义了对象的属性(变量)和方法(函数);'def'关键字用于定义函数,在类class定义的函数称为方法,这些方法可以用于操作该类的实例对象。

class MyClass:
    def my_method(self, arg1, arg2):
        # 方法体

        这个例子中,my_method 是一个方法,它接受两个参数 arg1arg2。在类中定义方法时,第一个参数通常是 self,它表示类的实例对象本身,用于访问该实例的属性和其他方法。‘self’是方法(函数)中必须的,是一种约定成俗的命名规范,用于表示对象本身,也就是调用该方法的实例对象。

        下面是Darknet53中关于  'class BasicBlock(nn.Module)  '的源代码:

class BasicBlock(nn.Module):
    def __init__(self, inplanes, planes):
        super(BasicBlock, self).__init__()
        self.conv1  = nn.Conv2d(inplanes, planes[0], kernel_size=1, stride=1, padding=0, bias=False)
        self.bn1    = nn.BatchNorm2d(planes[0])
        self.relu1  = nn.LeakyReLU(0.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)

    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
        return out

        于是,定义'class BasicBlock(nn.Module)'的代码中:'BasicBlock'类通过继承'nn.Module'类,可以利用PyTorch中提供的模型组件和功能,函数'def __init__(self, inplanes, planes)'和'def forward(self, x)'中的self则表示'BasicBlock'的类。也就是,self表示basicblock的类,该basicblock类继承nn.module属性。

        '__init__(self, inplanes, planes)'为'class BasicBlock(nn.Module)'类的初始化函数,通常也称为构造函数,在Python中用于在创建类的实例时进行初始化操作。初始化函数的名称通常采用'__init__',也是一种约定成俗的命名方法,便于代码理解。在类实例化时,Python解释器会自动调用该类的 __init__ 方法来初始化对象,这个函数会被自动调用,用于初始化对象的属性。着这种自动调用的机制也是Python类的一部分,它是不需要显性地调用 __init__ 方法,而是由Python解释器自动处理。这样做可以减少冗余代码,并提高代码的可读性和可维护性。

        'super(BasicBlock, self).__init__()'被用来调用'BasicBlock'类的父类(在该代码类指'nn.Module'类)的构造函数,确保了在 BasicBlock 类的实例化过程中,父类 nn.Module 的初始化操作也会被执行,以保证类的完整性和正确性。即当一个类继承自父类时,通常会调用父类的构造函数(该处指'nn.Module'类),以确保子类(该处指'BasicBlock '类)在实例化时能够正确地初始化这样做地好处是,通过调用父类的构造函数,确保了 BasicBlock 类在实例化时会继承 nn.Module 类的一些基本功能和属性,比如模型参数的管理、模型的状态跟踪等。这样,BasicBlock 类就可以利用 PyTorch 提供的模型组件和功能,比如模型参数的自动管理、模型状态的保存等。总结来说就是:’'__init__'初始化的是继承了'nn.Module'类的'BasicBlock '类。

        其中,'super()'函数是Python中内置函数,用于调用父类的方法。在类的方法中,super() 函数通常与 __init__() 方法一起使用,__init__() 方法是作为 super() 函数的一个参数。super() 函数的一般语法是:

super(子类名, self/cls).父类方法名()

        'BasicBlock'的类'self'在初始化__init__()中进行了两次CBRL(卷积、批归一化、激活函数),分别是利用一个1x1卷积下降通道数,然后利用一个3x3卷积提取特征并且上升通道数。第一个卷积层(1x1卷积)中,将输出通道数设置为较小的值,以实现通道数的降低;第二个卷积层用于提取特征并增加通道数。

        代码中前向传播方法'forward()'函数,描述了数据在该实例中如何进行前向传播。首先,将输入'x'赋值给'residual'变量:

  1. 'out = self.conv1(x)':将输入 x 输入到第一个卷积层 conv1 中进行卷积操作,得到输出 out
  2. 'out = self.bn1(out)':将卷积后的输出 out 输入到批归一化层 bn1 进行批归一化操作;
  3. 'out = self.relu1(out)':将批归一化后的输出 out 输入到激活函数 relu1 中进行激活操作。

        第二段CBRL操作与上述类似,其实forward(self, x)函数与__init__(self, inplanes, planes)函数对应卷积、批次化、激活函数对应一致,保持了模型在前向传播过程中的一致性。其中,'residual=x'创建了一个变量 residual 并将输入初始变量 x 的值赋给它,与上述两次CBRL相对应,将得到的结果加上初始变量即为'out +=  residual'。这样通过残差连接,网络可以有效地学习到残差信息,缓解梯度消失问题,从而提高了模型地性能和训练效率。

残差函数简单解析

        残差是指网络学习到的差异或变化。这个差异可以来自于恒等映射,也可以来自于其他学习函数。

        残差值是指在残差块中,最终的输出与输入之间的差异或变化。在恒等映射的情况下,残差值为零,因为最终的输出等于输入。而在其他情况下,残差值不为零,表示了残差块学习到的差异或变化。

        假设我们有一个恒等映射的残差块F(x) = x,其中 F(x) 表示残差块的学习函数。在这种情况下,如果残差块成功学习到了 F(x) = x,那么当输入信号经过残差块后,最终的输出就会等于输入信号加上残差,即 out = x + residual。恒等映射输出与输入相等,网络没有对输入进行任何修改或转换,而是将输入直接传递到输出。

        假设我们的残差块的学习函数是 F(x) = x + W(x),其中 W(x) 表示对输入信号 x 的一些非线性变换。在这种情况下,如果残差块成功学习到了 F(x),那么最终的输出将不再等于输入信号。假设学习函数是'F(x) = x + 2 ',当输入信号经过残差块后,最终的输出将等于输入信号加上2,而不是与输入相等。

        代码假设如下:

import torch
import torch.nn as nn

class ResidualBlock(nn.Module):
    def __init__(self):
        super(ResidualBlock, self).__init__()

    def forward(self, x):
        residual = x
        out = x + 2  # F(x) = x + 2
        return out

# 测试
res_block = ResidualBlock()
x = torch.tensor([1, 2, 3])
out = res_block(x)
print(out)

        在这个例子中,ResidualBlock 的前向传播函数中,out = x + 2 表示残差块的学习函数。当输入信号 x 经过残差块后,最终的输出将等于输入信号加上2,而不是与输入相等。这个例子中的残差值为2。

        当我们训练深度神经网络时,我们通常使用梯度下降或其变种来最小化损失函数。在反向传播算法中,我们计算损失函数关于网络参数的梯度,然后使用这些梯度来更新参数。然而,在深度网络中,梯度可能会随着反向传播的深入而逐渐减小,导致梯度消失的问题。这意味着网络的较早层可能收到的梯度非常小,几乎无法更新参数,从而导致网络难以训练。残差连接的核心思想是引入跨层的直接连接,允许梯度直接通过这些连接传播到较早的层。通过添加残差连接,即将较早层的输出(也称为“残差”)与后续层的输出相加。

        我们首先将输入信号 x 保存到变量 residual 中以备后用。然后,我们通过一系列卷积层和批量归一化层对输入信号进行处理,并将结果保存到 out 中。接着,我们将 residual 添加到 out 上,即执行残差连接操作。最后,我们将 out 通过激活函数(这里是ReLU)输出。

三、class DarkNet(nn.Module)

        在class DarkNet(nn.Module)中定义了名为'DarkNet'的类,用于实现基于PyTorch模型的DarkNet-53网络结构。其中包括'__init__(self,layers)'类的初始化定义、'_make_layer(self,planes,blocks)'残差块序列定义、'forward(self,x)'前向传播方法的定义以及'darknet53()'创建DarkNet-53网络模型。

        同样定义'class DarkNet(nn.Module)'的代码中:'BasicBlock'类通过继承'nn.Module'类,可以利用PyTorch中提供的模型组件和功能,函数'__init__(self,layers)'、'_make_layer(self,planes,blocks)'和''forward(self,x)''中的self则表示'DarkNet'的类。也就是,self表示DarkNet的类,该DarkNet类继承nn.module属性。

__init__(self,layers)

        该类中初始化函数代码如下:

    def __init__(self,layers):
        super(DarkNet, self).__init__()
        self.inplanes  = 32
        # 416,416,3 -> 416,416,32输入图片416*416
        self.conv1  =   nn.Conv2d(3,self.inplates,kernel_size=3,stride=1,padding=1,bias=False)
        self.bn1    =   nn.BatchNorm2d(self.inplanes)
        self.relu1  =   nn.LeakyReLU(0.1)
        
        ## 416,416,32 -> 208,208,64
        self.layer1 =   self._make_layer([32,64],layer[0])
        # 208,208,64 -> 104,104,128
        self.layer2 = self._make_layer([64, 128], layers[1])
        # 104,104,128 -> 52,52,256
        self.layer3 = self._make_layer([128, 256], layers[2])
        # 52,52,256 -> 26,26,512
        self.layer4 = self._make_layer([256, 512], layers[3])
        # 26,26,512 -> 13,13,1024
        self.layer5 = self._make_layer([512, 1024], layers[4])
    
        self.layers_out_filters = [64, 128, 256, 512, 1024]
        
        #进行权值初始化
        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_()

        其中,'super(DarkNet, self).__init__()'中同样使用'super()'函数来调用父类'nn.Module'的初始化方法。Yolo输入图片的尺寸'416,416,3'表示输入图片的尺寸为 416x416,通道数为 3,网络中的第一层卷积层 self.conv1 会将输入的 3 个通道转换为 32 个通道,这样输出的特征图尺寸将仍然是 416x416,但通道数变为了 32,即每个像素点会有 32 个特征值,用于表征图像的不同特征。同样,来一套CBRL(卷积、批归一化、激活函数)。

   (416,416,32)尺寸的特征图通过416,416,32 -> 208,208,64 -> 104,104,128 -> 52,52,256 -> 26,26,512 -> 13,13,1024一系列残差块'_make_layer()'处理,主要过程是逐渐减少特征图的尺寸,并逐渐增加通道数,以便提取更加抽象的特征。这样做的理由如下:

1.特征图尺寸减小

        通过减小特征图的尺寸,网络逐渐聚焦于图像的局部信息,而不是全局信息。这使得网络能够更好地捕捉对象的局部细节和结构。例如,较小的特征图有助于检测图像中的边缘、纹理等细微特征,这些特征对于识别和分类任务至关重要。

2.通道数增加

        通过逐渐增加通道数,网络可以从输入图像中提取更多的特征信息,并且将这些特征信息组合成更加复杂和抽象的表示。每个通道可以被看作是一个特征探测器,而每个层次中增加的通道数则允许网络学习更多种类的特征。这有助于网络学习到更加丰富和复杂的特征表示,从而提高网络对输入数据的理解能力。

        然后,采用'for m in self.modules()'循环遍历模型中所有的模块,'if isinstance(m, nn.Conv2d)'表示如果当前的模块是卷积层('nn.Conv2d')。

        'm.kernel_size[0] * m.kernel_size[1] * m.out_channels'用来计算卷积层的参数数量;在卷积层中,每个卷积核都有一个权重矩阵,该矩阵的大小由卷积核的高度、宽度和通道数决定:

        m.kernel_size[0]m.kernel_size[1] 分别是卷积核的高度和宽度;

        m.out_channels 是卷积层的输出通道数,也就是卷积核的数量;

        因此,m.kernel_size[0] * m.kernel_size[1] * m.out_channels 就是卷积核中所有权重的总数量。

        m.weight.data.normal_(0, math.sqrt(2. / n)):对当前卷积层的权重参数进行初始化。使用正态分布来初始化权重,均值为 0,标准差为 math.sqrt(2. / n)。这里 math.sqrt(2. / n) 是 Xavier 初始化方法中的一个常用选择,可以使得网络的初始化更加稳定,有助于训练时的收敛。

  elif isinstance(m, nn.BatchNorm2d):如果当前模块是批归一化层 (nn.BatchNorm2d),则执行以下操作:

  m.weight.data.fill_(1):将批归一化层的权重参数初始化为全 1;

  m.bias.data.zero_():将批归一化层的偏置参数初始化为全 0。

_make_layer(self,planes,blocks)

    #在每个layer里面,首先利用一个步长为2的3x3卷积进行下采样,然后进行残差结构的堆叠
    def _make_layer(self,planes,blocks):
        layers   =   []
        #下采样,步长为2,卷积核大小为3,append()只能包含一个参数,CBRL一套组合拳
        layers.append(("ds_conv",nn.Conv2d(self.inplanes,planes[1],kernel_size=3,stride=2,padding=1,bias=False)))
        layers.append(("ds_bn", nn.BatchNorm2d(planes[1])))
        layers.append(("ds_relu", nn.LeakyReLU(0.1)))
        #加入残差结构
        self.inplanes   =   planes[1]
        for i in range(0,blocks):
            layers.append(("residual_{}".format(i),BasicBlock(self.inplanes,planes)))
        return  nn.Sequential(OrderedDict(layers))

       _make_layer 方法用于创建模型中的一个层,在每个layer里面,首先利用一个步长为2的3x3卷积进行下采样,然后进行残差结构的堆叠。

        它按照给定的参数创建了一个由卷积层、批归一化层和激活函数组成的子层序列,并加入了残差结构。让我们逐步解释这个方法的作用:

  • 首先,它创建了一个空列表 layers 用于存储该层中的各个子层。

  • 接着,它向 layers 中添加了下采样的步骤。这里使用了一个卷积层 (nn.Conv2d) 用于下采样,设置了步长为2,这样能够将特征图的尺寸减半。紧接着是批归一化层 (nn.BatchNorm2d) 和 LeakyReLU 激活函数 (nn.LeakyReLU)。这三个操作一起形成了一个下采样的子层序列。

  • 接下来,该方法根据传入的参数 blocks,循环创建了 blocks 个残差结构。在每个残差结构中,会包含一个由 BasicBlock 组成的子层序列。残差结构的目的是在特征图的尺寸不变的情况下,逐渐增加通道数,并提取更加抽象的特征。

  • 最后,使用 nn.Sequentiallayers 列表中的所有子层组合成一个序列,并返回这个序列。

forward(self,x)

    #在每个layer里面,首先利用一个步长为2的3x3卷积进行下采样,然后进行残差结构的堆叠
    def _make_layer(self,planes,blocks):
        layers   =   []
        #下采样,步长为2,卷积核大小为3,append()只能包含一个参数,CBRL一套组合拳
        layers.append(("ds_conv",nn.Conv2d(self.inplanes,planes[1],kernel_size=3,stride=2,padding=1,bias=False)))
        layers.append(("ds_bn", nn.BatchNorm2d(planes[1])))
        layers.append(("ds_relu", nn.LeakyReLU(0.1)))
        #加入残差结构
        self.inplanes   =   planes[1]
        for i in range(0,blocks):
            layers.append(("residual_{}".format(i),BasicBlock(self.inplanes,planes)))
        return  nn.Sequential(OrderedDict(layers))

forward 方法定义了模型的前向传播过程:

  • 首先,输入 x 通过模型的第一个卷积层 (self.conv1),然后经过批归一化层 (self.bn1) 和 LeakyReLU 激活函数 (self.relu1)。

  • 然后,经过模型的每个层 (self.layer1, self.layer2, self.layer3, self.layer4, self.layer5),依次提取更高级别的特征。这些层都是通过之前定义的 _make_layer 方法创建的,其中包含了下采样操作和残差结构,能够逐步提取更加抽象的特征。

  • 最后,将最后几个层的输出(即特征图)返回,其中 out3, out4, out5 分别表示模型的第三、第四、第五层的输出。

      这个前向传播过程,模型能够接受输入并逐步提取并输出各个层的特征,以便后续用于分类、目标检测或其他任务。

darknet53()

def darknet53():
    model = DarkNet([1, 2, 8, 8, 4])
    return model   

darknet53() 用于创建一个 DarkNet 模型的实例。具体来说:

  • 首先,它使用 DarkNet 类创建了一个模型实例 model。在创建模型实例时,传入了一个列表 [1, 2, 8, 8, 4] 作为参数,这个列表定义了模型的层数和每个层中残差结构的数量。这个列表表示 DarkNet 模型的结构为:1 个下采样层(第一层),2 个残差结构的层(第二层),8 个残差结构的层(第三层),8 个残差结构的层(第四层),4 个残差结构的层(第五层)。

  • 然后,它返回了创建的模型实例 model

通过调用这个函数,可以方便地创建一个预先定义好的 DarkNet 模型,用于进行后续的训练或推断任务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值