目录
引言
Joseph Redmon在2018年YOLOv3论文《YOLOv3: An Incremental Improvement》提出了DarkNet53框架,作为YOLOv3目标检测算法的主干网络(backbone),顾名思义有53层网络层数,深化、丰富了神经网络模型,具有较强特征提取能力。
可参见机器视觉笔记3——卷积残差及代码实现文章中相关论述。
一、DarkNet53概述
DarkNet53深度卷积神经网络由卷积层、池化层和残差块组成,设计灵感来源于ResNet和初代DarkNet网络,包括53个卷积层和5个池化层组成。
DarkNet53网络结构图如下。
二、模型框架详细解析
从DarkNet53模型网络框架图中可以看出,对于输入图像(1, 3, 416, 416)(批次1张图像,每张图像3个通道,像素大小416x416 ),先进行卷积conv1(3,32,3,1,1)(注:输入为RGB三通道图片,输出32通道,卷积核为3x3,步幅为1,填充为1),得到具有32个通道的特征图。再进行卷积conv2 = Conv(32, 64, 3, 2, 1),输出64通道特征图。
再进行conv3_4 = ConvResidual(64)残差块计算,在残差块中先采用“c = c_in // 2
”对输入特征图通道数减半,可以在降低模型计算的复杂度的同时尽可能地保留输入的主要特征;接着残差块采用1×1 卷积降低输入通道的维度,然后1×1 卷积对降维度后的特征图进行特征提取;最终,通过残差连接将提取到的特征与原始输入特征相加,从而保持特征图的维度不变。
代码实现类的定义如下:
class Conv(nn.Module):
def __init__(self, c_in, c_out, k, s, p, bias=True):
"""
自定义一个卷积块,一次性完成卷积+归一化+激活,这在类似于像DarkNet53这样的深层网络编码上可以节省很多代码
:param c_in: in_channels
:param c_out: out_channels
:param k: kernel_size
:param s: stride
:param p: padding
:param bias: …
"""
super(Conv, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(c_in, c_out, k, s, p, bias=bias),
nn.BatchNorm2d(c_out),
nn.LeakyReLU(0.1),
)
def forward(self, entry):
return self.conv(entry)
class ConvResidual(nn.Module):
def __init__(self, c_in): # converlution * 2 + residual
"""
自定义残差单元,只需给出通道数,该单元完成两次卷积,并进行加残差后返回相同维度的特征图
:param c_in: 通道数
"""
c = c_in // 2
super(ConvResidual, self).__init__()
# 采用 1*1 + 3*3 的形式加深网络深度,加强特征抽象
self.conv = nn.Sequential(
Conv(c_in, c, 1, 1, 0), # kernel_size = 1进行降通道
Conv(c, c_in, 3, 1, 1), # 再用kernel_size = 3把通道升回去
)
def forward(self, entry):
return entry + self.conv(entry) # 加残差,既保留原始信息,又融入了提取到的特征
上述代码中先定义了class Conv(nn.Module),在残差块class ConvResidual(nn.Module)类中调用卷积函数。由于Conv(c_in, c, 1, 1, 0)和Conv(c, c_in, 3, 1, 1)完成了先减少通道数再恢复原来的通道数,所以残差块处理保证输出特征图与输入特征图的尺寸保持不变。
接着进行第5次卷积conv5 = Conv(64, 128, 3, 2, 1),输入为上述残差块输出的64通道特征图,conv5卷积处理后输出128通道的特征图。
然后又是残差块的计算,“2×ConvResidual()”表明在该过程中进行了两次的残差块运算,并且第一次残差块输出的特征图作为第二次残差块运算的输入。conv6_9 = nn.Sequential( ConvResidual(128),ConvResidual(128)),采用nn.Sequential函数顺序包括系列神经网络顺序运行ConvResidual(128),输入为conv5输出的128通道特征图,输出128通道特征图。可知,残差块运算对特征图像的通道数没有影响,旨在对特征图进行特征提取和特征融合。
如图所示,后续操作类似,依次进行:
conv10 = Conv(128, 256, 3, 2, 1)、conv11_26=8×ConvResidual(256)、conv27 = Conv(256, 512, 3, 2, 1)、conv28_43=8×ConvResidual(512)、conv44 = Conv(512, 1024, 3, 2, 1)、conv45_52=4×ConvResidual(1024)。
上述便是6个单独的卷积层和23个Residual(),共计52层卷积,还有最后一层是在YOLOv3中
在前52层特征提取基础上进行预测值,在这个模型为最后一个Residual()模块输出,即残差块中forward(self, entry)函数return。共计53层,这便是DarkNet53由来。
这里DarkNet53模型框架输出conv45_52、conv28_43和conv11_26三个特征图用于检测不同尺度地目标,通过后续的处理解码来预测目标位置和类别信息。在解码的过程,主要是将特征图与预定义的锚框(anchor boxes)进行匹配,并使用目标检测算法(如非极大值抑制)来筛选和优化检测结果。最终的输出结果通常是检测到的目标的位置坐标、类别预测以及置信度得分。
匹配过程分两步:
-
锚框生成(Anchor Box Generation):在特征图的每个位置生成一组锚框。这些锚框通常由多个尺寸和比例的框组成,以覆盖不同尺度和宽高比的目标。对于每个特征图上的位置,生成的锚框通常覆盖了整个图像。
-
锚框匹配(Anchor Box Matching):将生成的锚框与真实目标框进行匹配,以确定每个锚框应该负责检测图像中的哪些目标。匹配通常基于锚框与真实目标框之间的重叠程度(如IoU),如果一个锚框与某个目标框的重叠超过了一定阈值,则将该锚框标记为正样本;如果没有与任何目标框重叠或者与目标框重叠不足,则将其标记为负样本。
在YOLOv3中,conv45_52、conv28_43和conv11_26分别代表了不同分辨率级别下的特征图。例如,conv45_52是一个尺度较大的特征图,适合检测较大尺寸的目标;而conv11_26是一个尺度较小的特征图,适合检测较小尺寸的目标。因此,通过在不同分辨率级别下使用不同尺寸的锚框,并将它们与真实目标框进行匹配,模型可以更好地学习和检测不同尺寸的目标。
三、YOLOv3论文初窥
首先看一下Abstract,
摘要是论文的核心,这里主要意思就是Joseph Redmon对YOLO算法进行了一些更新,版本稍微大一些、但更加准确。表现在在320×320的分辨率下,YOLOv3的运行时间为22毫秒,mAP为28.2,与SSD一样准确,但速度快三倍。当我们查看旧的0.5 IOU mAP检测度量时,YOLOv3表现得相当不错。在Titan X上,它在51毫秒内达到了57.9的AP50,而RetinaNet在198毫秒内达到了57.5的AP50,性能相似但快3.8倍。并且所有的代码都是开源的。
相比较YOLOv2的DarkNet19升级到了DarkNet53,加深了网络的层数,并且创新性的引入了ResNet中残差块方法,给出了各网络精度性能对比图。
从准确度、十亿次操作、每秒十亿次浮点运算和各种网络的FPS, 列举出了backbone的对比。得出“This new network is much more powerful than Darknet-19 but still more efficient than ResNet-101 or ResNet-152.”这样一个结论。
其中,Darknet-53处理速度每秒78张图,比Darknet-19慢不少,但是比同精度的ResNet快很多,Yolov3依然保持了高性能。也就是能够达到速度保持的前提下,又快又准。
并且饶有兴趣地给出个执行时间、FPS对比图夸夸YOLOv3。具体详细解释等笔者详细研究再拓展。
四、代码实现
这里采用输入随机数据(1,3,416,416),经过Darknet53模型处理后,反馈conv45_52、conv28_43和conv11_26三个特征图形的shapes。
import torch
from torch import nn
class Conv(nn.Module):
def __init__(self, c_in, c_out, k, s, p, bias=True):
"""
自定义一个卷积块,一次性完成卷积+归一化+激活,这在类似于像DarkNet53这样的深层网络编码上可以节省很多代码
:param c_in: in_channels
:param c_out: out_channels
:param k: kernel_size
:param s: stride
:param p: padding
:param bias: …
"""
super(Conv, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(c_in, c_out, k, s, p, bias=bias),
nn.BatchNorm2d(c_out),
nn.LeakyReLU(0.1),
)
def forward(self, entry):
return self.conv(entry)
class ConvResidual(nn.Module):
def __init__(self, c_in): # converlution * 2 + residual
"""
自定义残差单元,只需给出通道数,该单元完成两次卷积,并进行加残差后返回相同维度的特征图
:param c_in: 通道数
"""
c = c_in // 2
super(ConvResidual, self).__init__()
# 采用 1*1 + 3*3 的形式加深网络深度,加强特征抽象
self.conv = nn.Sequential(
Conv(c_in, c, 1, 1, 0), # kernel_size = 1进行降通道
Conv(c, c_in, 3, 1, 1), # 再用kernel_size = 3把通道升回去
)
def forward(self, entry):
return entry + self.conv(entry) # 加残差,既保留原始信息,又融入了提取到的特征
class Darknet53(nn.Module):
def __init__(self):
super(Darknet53, self).__init__()
self.conv1 = Conv(3, 32, 3, 1, 1) # 一个卷积块 = 1层卷积
self.conv2 = Conv(32, 64, 3, 2, 1)
self.conv3_4 = ConvResidual(64) # 一个残差块 = 2层卷积
self.conv5 = Conv(64, 128, 3, 2, 1)
self.conv6_9 = nn.Sequential( # = 4层卷积
ConvResidual(128),
ConvResidual(128),
)
self.conv10 = Conv(128, 256, 3, 2, 1)
self.conv11_26 = nn.Sequential( # = 16层卷积
ConvResidual(256),
ConvResidual(256),
ConvResidual(256),
ConvResidual(256),
ConvResidual(256),
ConvResidual(256),
ConvResidual(256),
ConvResidual(256),
)
self.conv27 = Conv(256, 512, 3, 2, 1)
self.conv28_43 = nn.Sequential( # = 16层卷积
ConvResidual(512),
ConvResidual(512),
ConvResidual(512),
ConvResidual(512),
ConvResidual(512),
ConvResidual(512),
ConvResidual(512),
ConvResidual(512),
)
self.conv44 = Conv(512, 1024, 3, 2, 1)
self.conv45_52 = nn.Sequential( # = 8层卷积
ConvResidual(1024),
ConvResidual(1024),
ConvResidual(1024),
ConvResidual(1024),
)
def forward(self, entry):
conv1 = self.conv1(entry)
conv2 = self.conv2(conv1)
conv3_4 = self.conv3_4(conv2)
conv5 = self.conv5(conv3_4)
conv6_9 = self.conv6_9(conv5)
conv10 = self.conv10(conv6_9)
conv11_26 = self.conv11_26(conv10)
conv27 = self.conv27(conv11_26)
conv28_43 = self.conv28_43(conv27)
conv44 = self.conv44(conv28_43)
conv45_52 = self.conv45_52(conv44)
return conv45_52, conv28_43, conv11_26 # YOLOv3用,所以输出了3次特征
class Darknet53(nn.Module):
def __init__(self):
super(Darknet53, self).__init__()
self.conv1 = Conv(3, 32, 3, 1, 1) # 一个卷积块 = 1层卷积
self.conv2 = Conv(32, 64, 3, 2, 1)
self.conv3_4 = ConvResidual(64) # 一个残差块 = 2层卷积
self.conv5 = Conv(64, 128, 3, 2, 1)
self.conv6_9 = nn.Sequential( # = 4层卷积
ConvResidual(128),
ConvResidual(128),
)
self.conv10 = Conv(128, 256, 3, 2, 1)
self.conv11_26_2 = nn.Sequential(*[ConvResidual(256) for i in range(8)]) # = 16层卷积
self.conv27 = Conv(256, 512, 3, 2, 1)
self.conv28_43_2 = nn.Sequential(*[ConvResidual(512) for i in range(8)]) # = 16层卷积
self.conv44 = Conv(512, 1024, 3, 2, 1)
self.conv45_52_2 = nn.Sequential(*[ConvResidual(1024) for i in range(4)]) # = 8层卷积
def forward(self, entry):
conv1 = self.conv1(entry)
conv2 = self.conv2(conv1)
conv3_4 = self.conv3_4(conv2)
conv5 = self.conv5(conv3_4)
conv6_9 = self.conv6_9(conv5)
conv10 = self.conv10(conv6_9)
conv11_26 = self.conv11_26_2(conv10)
conv27 = self.conv27(conv11_26)
conv28_43 = self.conv28_43_2(conv27)
conv44 = self.conv44(conv28_43)
conv45_52 = self.conv45_52_2(conv44)
return conv45_52, conv28_43, conv11_26 # YOLOv3用,所以输出了3次特征
# 示例输入
input_tensor = torch.randn(1, 3, 416, 416)
# 实例化模型
model = Darknet53()
# 模型推断
out3, out4, out5 = model(input_tensor)
print("Output shapes:\n{}\n{}\n{}".format(out3.shape, out4.shape, out5.shape))
运行代码后结果如下: