yolo目标检测笔记(1)
最近在看杨建华的《yolo 目标检测》,打算按章节做一点笔记。封面是这样的:

一编:读了这本书,感觉这本书讲得很不清楚、表述也不够明白(很乱),自己学习过程中搜了很多东西,不是特别推荐,整理的笔记已经尽力了)
文中的源代码可以到该项目的github中找。
二编:有的代码结构和书中表示出来略有差别。对于初学者来说,这本书还是非常不推荐。打算放弃这本书,去看别的资料了。关于这本书的内容就更新这么多了。
01 第一章:目标检测架构浅析
1.1 目标检测发展史
2014年,R-CNN问世。
利用R-CNN,基本思路是先使用一个搜索算法从图像中提取出若干感兴趣区域(region of interest,RoI),然后使用一个卷积神经网络(convolutional neural network,CNN)分别处理每一个感兴趣区域,提取特征,最后用一个支持向量机来完成最终的分类:

搜索算法会先给出约2000个感兴趣区域,然后交给后续的CNN去分别提取每一个感兴趣区域的特征,十分耗时,后来又诞生了Fast R-CNN和Faster R-CNN。
像R-CNN “这样,先提取,后识别”的检测范式,被称为“两阶段”(two-stage)检测。
即,先提取出可能包含目标的区域,再依次对每个区域进行识别,最后经过处理得到最终的检测结果。
2015年,YOLO(You Only Look Once)问世。
YOLO的作者团队认为,提取候选区域(定位)和逐一识别(分类)完全可由一个单独的网络来同时完成,无须分成两个阶段,不需要对每一个特征区域进行依次分类,从而能够减少处理过程中的大量冗余操。

因而YOLO在检测速度上具有天然的优势。
YOLO所采用的这种端到端的检测方式,将定位和分类耦合在一起,同步完成,被称为“单阶段”(one-stage)检测。
在YOLO框架大火之后,目标检测领域正式诞生了以下两大流派:
- 以R-CNN为代表的two-stage流派;
- 以YOLO为首的one-stage流派。
one-stage框架相关工作占据了很大的比重,如SSD、RetinaNet和FCOS等。
近来,这一套框架又由基于Transformer的DETR系列做了一次大幅度的革新。
因此,在入门目标检测领域时,学习one-stage框架相关工作是比较契合主流的选择。
1.2 目标检测网络框架
一个常见的目标检测网络往往可以分为三大部分:主干网络(backbone network)、颈部网络(neck network)和检测头(detection head)。

- 主干网络。主干网络是目标检测网络中最核心的部分,其关键作用就是提取输入图像中的高级特征,减少图像中的冗余信息,以便于后续的网络去做深入的处理。
- 颈部网络。颈部网络的主要作用是将由主干网络输出的特征进行二次处理。其整合方式有很多,最为常见的就是特征金字塔网络(feature pyramid network,FPN),其核心是将不同尺度的特征进行充分的融合,以提升检测器的多尺度检测的性能。(还有RFB,ASPP,YOLOV4所使用的SPP模块等,添加在主干网络之后,可以进一步地处理和丰富特征信息,扩大模型的感受野)
一点对FPN的解释:
(FPN认为网络中的不同大小的特征图所包含的信息是不一样的,即浅层特征图包含更多的位置信息,且分辨率较高,感受野较小,便于检测小物体;而深层特征图包含更多的语义信息,且分辨率较低,感受野较大,便于检测大物体。)

- 检测头。其主要作用就是提取类别信息和位置信息,输出最终的预测结果。(在某些工作里,检测头也被称为解码器(decoder))
1.2.1 主干网络
为了检测出图像中目标的类别和位置,我们会先从输入的图像中提取出必要的特征信息,比如HOG特征。
HOG的一点基本介绍:


不论是基于传统方法还是深度学习方法,提取特征这一步都是至关重要的,区别只在于提取的方式。然后利用这些特征去完成后续的定位和分类。
在目标检测框架中,提取特征通常被称为主干网络。
1.2.2 颈部网络
由于主干网络往往是从图像分类任务中迁移过来的,这些网络的设计很少会考虑到包括目标检测、语义分割等下游任务,它们提取的特征也就有可能不太适合目标检测任务。
因此,在主干网络处理完毕之后,需要设计一些额外的模块来对其特征做进一步的处理,以便适应目标检测任务。
因为这一部分是在主干网络之后、检测头之前,因此被形象地称为颈部网络。
常用的颈部网络,除了前面提到的FPN,还有常用到的SPP结构:
SPP结构包含4条并行的分支,且每条分支用了不同大小的池化核。
SPP结构可以有效地聚合不同尺度的显著特征,同时扩大网络的感受野,进而提升模型的性能。

1.2.3 检测头
当一张输入图像经过主干网络和颈部网络两部分的处理后,得到的特征就可以用于后续的检测了。
大多数的检测框架所采用的技术路线,都是在处理好的特征图上,通过部署数层卷积来完成定位和分类。
如下图,是当前常用的一种检测头结构RetinaNet,采用了“解耦”结构的检测头。
它由两条并行的分支组成,每一条分支都包含若干层普通卷积、非线性激活函数以及用于最终预测的线性卷积层。

(这里没太明白也没事,后面在代码中,再详细去理解)
02 第二章:数据集
略
03 第三章:YOLO v1
3.1 YOLOv1的网络结构
基本思想:使用一个卷积神经网络来端到端地检测目标。
核心思想是:把目标检测看作一次回归问题,用一个神经网络直接从整张图片预测边界框(Bounding Box)和类别概率,而不是像 R-CNN 那样先生成候选框再分类。
3.1.1 GoogLeNet的结构
先来了解一下GoogLeNet的结构:
主要由:普通卷积层 + Inception 模块 + 辅助分类器组成;
结构大概流程:

Inception 模块核心思想:
传统 CNN 每一层卷积核的大小是固定的,比如 ResNet 第一层就是 7×7 卷积。
GoogLeNet 的创新是:
- 同一层并行使用不同卷积核(1×1、3×3、5×5)+ 最大池化;
- 然后把它们的输出通道在通道维度上拼接(concat);
- 1×1 卷积在这里不仅用来提取特征,还起到了降维作用(减少计算量)
Inception的示意图为:

3.1.2 YOLOv1的结构
YOLOv1仿照GoogLeNet网络来设计主干网络,但没有采用GoogLeNet的Inception模块,而是使用串联的11卷积(用来降维)和33卷积(用来提取特征)所组成的模块。
YOLO v1 把整张图像划分为 S×S 网格(论文中 S=7),结构如下:

在YOLO所处的年代,图像分类网络都会将特征图展平(flatten),得到一个一维特征向量,然后连接全连接层去做预测。
YOLOv1在VOC数据集上与其他模型的性能对比的两个指标:
一个是衡量模型检测性能的平均精度(mean average precision,mAP),另一个是衡量模型检测速度的每秒帧数(frames per second,FPS)。
mAP是目标检测领域常用的性能评价指标,其数值越高,意味着模型的检测性能越好。
FPS是指网络每秒可以处理多少张图像(从输入一张图像到输出图像中的目标的检测结果)。FPS的数值越大,意味着模型的检测速度越快。
3.2 YOLOv1的检测原理
根据前面3-1的图,原文给出如下理解:

可能有一点费解,大概降采样的特征图是图3-1全连接层处理前的结构(771024的结构)。
经过全连接层后,得到7730的结构。
7730这个结构,可理解为:每一个网格(1*1的维度)
都是一个特征维度为30的向量。
对于30这个数字,是通过这样的式子计算的:

为什么上面式子中B前面的系数是5呢?
是因为每个边界框包含的信息有5个:置信度C(表征的是每一类的概率),边界框位置参数(x,y,w,h)(x,y是边界框的中心点相较于网格最上角的偏移量)(w,h是边界框的宽和高)
YOLOv1 中 B=2,用两个边界框是为了解决单个边界框不够灵活的问题。
(有点好笑,当时第一反应,没有反应过来边界框是干什么的)

如下图,可以看出每一个网格的基本结构:

那么提问,为什么边界框是两组,类别概率只是一组呢?
是一个取舍问题,请看gpt老师的解答:

所以,整体维度变化,可以大概用下图理解:

综上,YOLOv1的检测理念就是将输入图像划分成网格,然后在网格上做预测。
3.3 YOLOv1的制作训练正样本的方法
本节主要介绍YOLOv1的这30个参数的定义和学习策略,以及如何为每一个参数制作用于训练的正样本。
3.3.1 边界框的位置参数 x,y,w,h
YOLOv1是通过检测图像中的目标中心点来达到检测目标的目的。
只有包含目标中心点的网格才会被认为是有物体的。
为此,YOLOv1定义了一个“objectness”概念,表示此处是否有物体,即
表示此网格处有物体,反之,
表示此网格处没有物体。
有物体的网格会被标记为正样本候选区域,其他区域的预测都是该目标的负样本。
关于中心点偏移量的计算式,这里还是借gpt老师的回答:

图3-7直观地展示了YOLOv1中的中心点偏移量的概念。
在推理阶段,YOLOv1先用预测的边界框置信度来找出包含目标中心点的网格,再通过这一网格所预测出的中心点偏移量得到最终的中心点坐标(上面公式的逆计算就可以得到x和y)

YOLOv1还要输出边界框的宽和高,以确定边界框大小。如何做呢?

注意上面提到,预测较大数据的时候,容易导致梯度震荡,从而不容易收敛。
使用如下公式,进行归一化:

其中,上式中的分子是目标的边界框的宽和高,分母是输入图像的宽和高。
则,归一化后的尺寸就在0~1之间。
这样,既避免了损失过大所导致的训练发散问题,又和其他部分的损失取得了较好的平衡。
3.3.2 置信度
有一个很重要的问题:如何让网络确定中心点的位置,如何确定置信度?
引入一个概念,交并比(intersection of union,IoU):
计算出两个矩形框的交集(intersection)和并集(union),它们的比值即为IoU。

如果预测框接近真实框,IoU则接近1。
基于上面的概念,进行边界框置信度和类别置信度的理解:


3.4 损失函数
损失函数的公式如下:

- 第一行和第二行表示的是边界框的位置参数的损失;
- 第三行和第四行表示的是边界框的置信度的损失
- 最后一行就是正样本处的类别的损失。
提问,为什么宽高的损失函数和别的项处理略有不同?
解答如下:

3.5 前向推理
YOLOv1的前向推理,即是算出某个边界框的得分。
为得到最终检测结果,一般的步骤有:
- 计算所有预测的边界框的得分;
- 得分阈值筛选;
- 计算边界框的中心点坐标以及宽和高;
- 使用费极大值抑制第二次筛选;
(由于YOLOv1可能对同一个目标给出多个得分较高的边界框,需要对这种冗余检测进行抑制,以剔除那些不必要的重复检测。
引入非极大值抑制(non-maximal suppression,NMS)这一概念:
非极大值抑制的思想很简单,对于某一类别目标的所有边界框,先挑选出得分最高的边界框,再依次计算其他边界框与这个得分最高的边界框的IoU,超过设定的IoU阈值的边界框则被认为是重复检测,将其剔除。对所有类别的边界框都进行上述操作,直到无边界框可剔除为止)
04 第四章:搭建YOLOv1网络
本章搭建的YOLOv1是在原网络进行优化后的。
4.1 改进YOLOv1
我们要构建的YOLOv1网络是一个全卷积结构,其中不包含任何全连接层,这一点可以避免YOLOv1中存在的因全连接层而导致的参数过多的问题。
(但实际上,YOLOv2工作才开始转变为全卷积结构)
需要搭建的网络结构如下:

4.1.1 改进主干网络
用ResNet网络代替YOLOv1的GoogLeNet风格的主干网络。
相较于原本的主干网络,ResNet使用了诸如批归一化(batch normalization,BN)、残差连接(residual connection)等操作,有助于稳定训练更大更深的网络。(目前,这两个操作几乎成了绝大多数卷积神经网络的标准结构之一)
ResNet-18网络的结构如下:

提问,将图像分类网络用作目标检测网络的主干网络时,通常是不需要最后的平均池化层和分类层的。如何理解这句话呢?

因此,这里我们去除ResNet-18网络中的最后的平均池化层和分类层。
不同于YOLOv1原本的GoogLeNet网络的64倍降采样,ResNet-18网络的最大降采样倍数为32,故而一张448448的图像输入后,主干网络会输出一个空间大小为1414的特征图。
若输入图像的尺寸为416416,则该尺寸的图像输入后,输出张量为1313*512。
下面是基于pytorch框架resnet-18的部分代码:
import torch
import torch.nn as nn
import torch.utils.model_zoo as model_zoo
# --------------------- ResNet modules ---------------------
def conv3x3(in_planes, out_planes, stride=1): # 定义3×3 卷积的便捷函数:自动带 padding=1 保持尺寸(在 stride=1 时)
"""3x3 convolution with padding"""
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
padding=1, bias=False)
def conv1x1(in_planes, out_planes, stride=1): # 定义1×1 卷积:常用于通道变换/降维或做下采样时调通道数
"""1x1 convolution"""
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(BasicBlock, self).__init__()
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = nn.BatchNorm2d(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes)
self.bn2 = nn.BatchNorm2d(planes)
self.downsample = downsample # downsample:当尺寸或通道不匹配时,用它把“捷径分支(identity)”对齐(通常是 1×1 卷积)
self.stride = stride
def forward(self, x):
identity = x # 保存捷径分支
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
identity = self.downsample(x) # 对齐尺寸/通道
out += identity # 残差相加
out = self.relu(out) # 再激活
return out
class Bottleneck(nn.Module):
expansion = 4 # 输出通道扩张倍数为 4(1×1 降维 → 3×3 特征 → 1×1 扩维)
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(Bottleneck, self).__init__()
self.conv1 = conv1x1(inplanes, planes)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = conv3x3(planes, planes, stride)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = conv1x1(planes, planes * self.expansion)
self.bn3 = nn.BatchNorm2d(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
# --------------------- ResNet -----------------------
class ResNet(nn.Module):
def __init__(self, block, layers, zero_init_residual=False):
super(ResNet, self).__init__()
self.inplanes = 64
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3,
bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 7×7/2 卷积把步长减半,BN+ReLU;再 3×3/2 最大池化再减半。到此步下采样 4 倍(/2 再 /2)
self.layer1 = self._make_layer(block, 64, layers[0]) # stride=1, 不降采样
self.layer2 = self._make_layer(block, 128, layers[1], stride=2) # /2
self.layer3 = self._make_layer(block, 256, layers[2], stride=2) # /2
self.layer4 = self._make_layer(block, 512, layers[3], stride=2) # /2
for m in self.modules():
if isinstance(m, nn.Conv2d): # 判断当前子模块m是不是二维卷积层(Conv2d),如果是,就执行下面的卷积层权重初始化
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, nn.BatchNorm2d): # 如果当前模块是批归一化层(BatchNorm2d),执行下面的初始化
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
# Zero-initialize the last BN in each residual branch,
# so that the residual branch starts with zeros, and each residual block behaves like an identity.
# This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
if zero_init_residual: # 把每个残差分支最后一层 BN 的缩放系数 γ 初始化为 0,使初始时残差分支输出≈0,整个块近似恒等映射。这个小技巧能让训练更稳定、精度小涨(论文小结论)
for m in self.modules():
if isinstance(m, Bottleneck):
nn.init.constant_(m.bn3.weight, 0)
elif isinstance(m, BasicBlock):
nn.init.constant_(m.bn2.weight, 0)
def _make_layer(self, block, planes, blocks, stride=1): # 用来创建resnet中的一整层;block:残差块的类,比如BasicBlock或Bottleneck;planes:这一层内部卷积的输出通道数(瓶颈层的通道数);blocks:该层中包含多少个残差块
downsample = None # 初始化变量 downsample 为 None,后续用来存放可能的下采样操作(调整残差连接输入维度)
if stride != 1 or self.inplanes != planes * block.expansion: # 判断是否需要做下采样或改变通道数(如果步长 stride 不等于1,说明要缩小特征图尺寸(下采样);或者输入通道数 self.inplanes 不等于输出通道数(planes * block.expansion),残差连接的维度不匹配)
downsample = nn.Sequential( # 如果满足上面条件,就定义 downsample 为一个由两层组成的序列
conv1x1(self.inplanes, planes * block.expansion, stride), # conv1x1:1×1卷积,改变通道数和(或)步长,实现尺寸和通道匹配
nn.BatchNorm2d(planes * block.expansion), # BatchNorm2d:批归一化,保持训练稳定性
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample)) # 添加第一个残差块:
self.inplanes = planes * block.expansion # 更新 self.inplanes 为该层残差块输出的通道数(planes 乘以扩张倍数 block.expansion)
for _ in range(1, blocks): # 通过循环,添加剩余的残差块(从第2个到第blocks个):
layers.append(block(self.inplanes, planes))
return nn.Sequential(*layers)
def forward(self, x):
"""
Input:
x: (Tensor) -> [B, C, H, W]
Output:
c5: (Tensor) -> [B, C, H/32, W/32]
"""
c1 = self.conv1(x) # [B, C, H/2, W/2]
c1 = self.bn1(c1) # [B, C, H/2, W/2]
c1 = self.relu(c1) # [B, C, H/2, W/2]
c2 = self.maxpool(c1) # [B, C, H/4, W/4]
c2 = self.layer1(c2) # [B, C, H/4, W/4]
c3 = self.layer2(c2) # [B, C, H/8, W/8]
c4 = self.layer3(c3) # [B, C, H/16, W/16]
c5 = self.layer4(c4) # [B, C, H/32, W/32]
return c5
4.1.2 添加一个颈部网络
选用改进的SPP模块作为颈部网络:

5×5的最大池化层等效于5×5、9×9和13×13这三条并行的最大池化层分支,从而降低计算开销。
这部分代码为:
# --------------------- Basic modules ---------------------
def get_conv2d(c1, c2, k, p, s, d, g, bias=False): # 返回一个2D卷积层,c1是输入通道数,c2是输出通道数,k是卷积核大小,p(padding),s(stride),d(dilation膨胀卷积系数),g:groups(分组卷积,1=普通卷积,c1=c2=g=depthwise卷积)
conv = nn.Conv2d(c1, c2, k, stride=s, padding=p, dilation=d, groups=g, bias=bias)
return conv
def get_activation(act_type=None): # 功能:根据字符串返回对应激活函数
if act_type == 'relu':
return nn.ReLU(inplace=True) # inplace=True:表示在原地操作,节省内存
elif act_type == 'lrelu':
return nn.LeakyReLU(0.1, inplace=True)
elif act_type == 'mish':
return nn.Mish(inplace=True)
elif act_type == 'silu':
return nn.SiLU(inplace=True)
elif act_type is None:
return nn.Identity() # nn.Identity():什么也不做,直接返回输入
else:
raise NotImplementedError
def get_norm(norm_type, dim): # 功能:根据字符串返回对应归一化层
if norm_type == 'BN': # BN → BatchNorm2d;GN → GroupNorm;None → 不做归一化
return nn.BatchNorm2d(dim)
elif norm_type == 'GN':
return nn.GroupNorm(num_groups=32, num_channels=dim)
elif norm_type is None:
return nn.Identity()
else:
raise NotImplementedError
class BasicConv(nn.Module): # 功能:封装卷积 + BN + 激活函数模块,可选择depthwise卷积
def __init__(self,
in_dim, # in channels
out_dim, # out channels
kernel_size=1, # kernel size
padding=0, # padding
stride=1, # padding
dilation=1, # dilation
act_type :str = 'lrelu', # activation
norm_type :str = 'BN', # normalization
depthwise :bool = False # depthwise:是否使用深度可分离卷积。
):
super(BasicConv, self).__init__()
self.depthwise = depthwise
use_bias = False if norm_type is not None else True # 如果有归一化层,则卷积不使用偏置,因为BN中会有偏置项
if not depthwise: # 普通卷积
self.conv = get_conv2d(in_dim, out_dim, k=kernel_size, p=padding, s=stride, d=dilation, g=1, bias=use_bias)
self.norm = get_norm(norm_type, out_dim)
else: # 深度可分离卷积
self.conv1 = get_conv2d(in_dim, in_dim, k=kernel_size, p=padding, s=stride, d=dilation, g=in_dim, bias=use_bias)
self.norm1 = get_norm(norm_type, in_dim)
self.conv2 = get_conv2d(in_dim, out_dim, k=1, p=0, s=1, d=1, g=1)
self.norm2 = get_norm(norm_type, out_dim)
self.act = get_activation(act_type)
def forward(self, x):
if not self.depthwise: # 普通卷积:Conv → BN → 激活
return self.act(self.norm(self.conv(x)))
else: # Depthwise:Depthwise Conv → BN → Pointwise Conv → BN → 激活
# Depthwise conv
x = self.norm1(self.conv1(x))
# Pointwise conv
x = self.act(self.norm2(self.conv2(x)))
return x
# Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher
class SPPF(nn.Module):
"""
This code referenced to https://github.com/ultralytics/yolov5
"""
def __init__(self, cfg, in_dim, out_dim):
super().__init__()
## ----------- Basic Parameters -----------
inter_dim = round(in_dim * cfg.neck_expand_ratio)
self.out_dim = out_dim
## ----------- Network Parameters -----------
self.cv1 = BasicConv(in_dim, inter_dim,
kernel_size=1, padding=0, stride=1,
act_type=cfg.neck_act, norm_type=cfg.neck_norm) # 1×1卷积降低通道数到中间维度
self.cv2 = BasicConv(inter_dim * 4, out_dim,
kernel_size=1, padding=0, stride=1,
act_type=cfg.neck_act, norm_type=cfg.neck_norm) # 1×1卷积融合SPPF的池化结果
self.m = nn.MaxPool2d(kernel_size=cfg.spp_pooling_size,
stride=1,
padding=cfg.spp_pooling_size // 2)
# Initialize all layers
self.init_weights()
def init_weights(self): # 初始化卷积层参数
"""Initialize the parameters."""
for m in self.modules():
if isinstance(m, torch.nn.Conv2d):
# In order to be consistent with the source code,
# reset the Conv2d initialization parameters
m.reset_parameters()
def forward(self, x):
x = self.cv1(x)
y1 = self.m(x)
y2 = self.m(y1)
return self.cv2(torch.cat((x, y1, y2, self.m(y2)), 1))
4.1.3 修改检测头
采用解耦检测头作为YOLOv1的检测头,由类别分支和回归分支组成,分别提取类别特征和位置特征。

实现代码:
class Yolov1DetHead(nn.Module):
def __init__(self, cfg, in_dim: int = 256): # cfg 是一个配置对象,存储网络超参数
super().__init__()
# --------- Basic Parameters ----------
self.in_dim = in_dim
self.cls_head_dim = cfg.head_dim
self.reg_head_dim = cfg.head_dim
self.num_cls_head = cfg.num_cls_head
self.num_reg_head = cfg.num_reg_head
self.act_type = cfg.head_act
self.norm_type = cfg.head_norm
self.depthwise = cfg.head_depthwise
# --------- Network Parameters ----------
## cls head
cls_feats = [] # cls_feats:存放分类头卷积层
for i in range(self.num_cls_head):
if i == 0:
cls_feats.append(
BasicConv(in_dim, self.cls_head_dim,
kernel_size=3, padding=1, stride=1,
act_type=self.act_type,
norm_type=self.norm_type,
depthwise=self.depthwise)
)
else:
cls_feats.append(
BasicConv(self.cls_head_dim, self.cls_head_dim,
kernel_size=3, padding=1, stride=1,
act_type=self.act_type,
norm_type=self.norm_type,
depthwise=self.depthwise)
)
## reg head
reg_feats = []
for i in range(self.num_reg_head):
if i == 0:
reg_feats.append(
BasicConv(in_dim, self.reg_head_dim,
kernel_size=3, padding=1, stride=1,
act_type=self.act_type,
norm_type=self.norm_type,
depthwise=self.depthwise)
)
else:
reg_feats.append(
BasicConv(self.reg_head_dim, self.reg_head_dim,
kernel_size=3, padding=1, stride=1,
act_type=self.act_type,
norm_type=self.norm_type,
depthwise=self.depthwise)
)
# 将卷积层组合成顺序模块
self.cls_feats = nn.Sequential(*cls_feats) # 用 nn.Sequential 将列表里的卷积层串联成一个模块
self.reg_feats = nn.Sequential(*reg_feats)
self.init_weights()
def init_weights(self):
"""Initialize the parameters."""
for m in self.modules():
if isinstance(m, torch.nn.Conv2d):
# In order to be consistent with the source code,
# reset the Conv2d initialization parameters
m.reset_parameters()
def forward(self, x):
"""
in_feats: (Tensor) [B, C, H, W]
"""
cls_feats = self.cls_feats(x)
reg_feats = self.reg_feats(x)
return cls_feats, reg_feats
4.1.4 修改预测层
在最后的预测层,使用1*1的卷积层在特征图上做预测。如下图:

其中通道维度上的1表示边界框的置信度,c表示类别的总数,4表示边界框的4个位置参数。这里不再有表示每个网格的边界框数量的B。
结构分析:

4.1.5 修改损失函数
置信度损失:

类别损失:

边界框位置参数的损失:

总的损失:

4.2 搭建YOLOv1网络
实现代码为:
# YOLOv1
class Yolov1(nn.Module):
def __init__(self,
cfg,
is_val=False,
) -> None:
super(Yolov1, self).__init__()
# ---------------------- Basic setting ----------------------
self.cfg = cfg
self.num_classes = cfg.num_classes
## Post-process parameters
self.topk_candidates = cfg.val_topk if is_val else cfg.test_topk
self.conf_thresh = cfg.val_conf_thresh if is_val else cfg.test_conf_thresh
self.nms_thresh = cfg.val_nms_thresh if is_val else cfg.test_nms_thresh
self.no_multi_labels = False if is_val else True
# ---------------------- Network Parameters ----------------------
self.backbone = Yolov1Backbone(cfg)
self.neck = SPPF(cfg, self.backbone.feat_dim, cfg.head_dim)
self.head = Yolov1DetHead(cfg, self.neck.out_dim)
self.pred = Yolov1DetPredLayer(cfg)
def post_process(self, obj_preds, cls_preds, box_preds):
"""
We process predictions at each scale hierarchically
Input:
obj_preds: torch.Tensor -> [B, M, 1], B=1
cls_preds: torch.Tensor -> [B, M, C], B=1
box_preds: torch.Tensor -> [B, M, 4], B=1
Output:
bboxes: np.array -> [N, 4]
scores: np.array -> [N,]
labels: np.array -> [N,]
"""
obj_preds = obj_preds[0]
cls_preds = cls_preds[0]
box_preds = box_preds[0]
if self.no_multi_labels:
# [M,]
scores, labels = torch.max(
torch.sqrt(obj_preds.sigmoid() * cls_preds.sigmoid()), dim=1)
# Keep top k top scoring indices only.
num_topk = min(self.topk_candidates, box_preds.size(0))
# topk candidates
predicted_prob, topk_idxs = scores.sort(descending=True)
topk_scores = predicted_prob[:num_topk]
topk_idxs = topk_idxs[:num_topk]
# filter out the proposals with low confidence score
keep_idxs = topk_scores > self.conf_thresh
scores = topk_scores[keep_idxs]
topk_idxs = topk_idxs[keep_idxs]
labels = labels[topk_idxs]
bboxes = box_preds[topk_idxs]
else:
# [M, C] -> [MC,]
scores = torch.sqrt(obj_preds.sigmoid() * cls_preds.sigmoid()).flatten()
# Keep top k top scoring indices only.
num_topk = min(self.topk_candidates, box_preds.size(0))
# torch.sort is actually faster than .topk (at least on GPUs)
predicted_prob, topk_idxs = scores.sort(descending=True)
topk_scores = predicted_prob[:num_topk]
topk_idxs = topk_idxs[:num_topk]
# filter out the proposals with low confidence score
keep_idxs = topk_scores > self.conf_thresh
scores = topk_scores[keep_idxs]
topk_idxs = topk_idxs[keep_idxs]
anchor_idxs = torch.div(topk_idxs, self.num_classes, rounding_mode='floor')
labels = topk_idxs % self.num_classes
bboxes = box_preds[anchor_idxs]
# to cpu & numpy
scores = scores.cpu().numpy()
labels = labels.cpu().numpy()
bboxes = bboxes.cpu().numpy()
# nms
scores, labels, bboxes = multiclass_nms(
scores, labels, bboxes, self.nms_thresh, self.num_classes)
return bboxes, scores, labels
def forward(self, x):
# ---------------- Backbone ----------------
x = self.backbone(x)
# ---------------- Neck ----------------
x = self.neck(x)
# ---------------- Heads ----------------
cls_feats, reg_feats = self.head(x)
# ---------------- Preds ----------------
outputs = self.pred(cls_feats, reg_feats)
outputs['image_size'] = [x.shape[2], x.shape[3]]
if not self.training:
obj_preds = outputs['pred_obj']
cls_preds = outputs['pred_cls']
box_preds = outputs['pred_box']
# post process
bboxes, scores, labels = self.post_process(
obj_preds, cls_preds, box_preds)
outputs = {
"scores": scores,
"labels": labels,
"bboxes": bboxes
}
return outputs
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)