一、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 是一个方法,它接受两个参数 arg1 和 arg2。在类中定义方法时,第一个参数通常是 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'变量:
- '
out = self.conv1(x)':将输入x输入到第一个卷积层conv1中进行卷积操作,得到输出out; - '
out = self.bn1(out)':将卷积后的输出out输入到批归一化层bn1进行批归一化操作; - '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.Sequential将layers列表中的所有子层组合成一个序列,并返回这个序列。
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 模型,用于进行后续的训练或推断任务。
3207

被折叠的 条评论
为什么被折叠?



