当前位置: 首页 > article >正文

YOLO目标检测3

 一. 参考资料

《YOLO目标检测》 by 杨建华博士

本篇文章的主要内容来自于这本书,只是作为学习记录进行分享。

二. 搭建YOLOv1的网络

2.1 YOLOv1的网络结构

        作者带我们构建的YOLOv1网络是一个全卷积结构,其中不包含任何全连接层,这一点可以避免YOLOv1中存在的因全连接层而导致的参数过多的问题。尽管YOLO网络是在YOLOv2工作才开始转变为全卷积结构,但我们已经了解了全连接层的弊端,因此没有必要再循规蹈矩地照搬YOLOv1的原始网络结构,这也符合我们设计YOLOv1的初衷。

2.1.1 主干网络

        使用当下流行的ResNet网络代替YOLOv1的GoogLeNet风格的主干网络。相较于原本的主干网络,ResNet使用了诸如批归一化(batch normalization,BN)、残差连接(residual connection)等操作,有助于稳定训练更大更深的网络。

        前面已经讲过,将图像分类网络用作目标检测网络的主干网络时,通常是不需要最后的平均池化层和分类层的,因此,这里去除ResNet-18网络中的最后的平均池化层和全连接层,

        这里使用的ResNet-18网络的最大降采样倍数为32,在这个网络中,默认输入图像尺寸为416 \times 416,最后的输出图像为14 \times 14,要比传统的YOLOv1更精细些。

        根据书中提供的代码,实现ResNet主干网络的关键部分的代码为:

# YOLO_Tutorial/models/yolov1/yolov1_backbone.py
# --------------------------------------------------------
...
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)
        self.layer1=self._make_layer(block, 64, layers[0])
        self.layer2=self._make_layer(block, 128, layers[1], stride=2)
        self.layer3=self._make_layer(block, 256, layers[2], stride=2)
        self.layer4=self._make_layer(block, 512, layers[3], stride=2)
 
    def forward(self, x):
        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
2.1.2 颈部网络

        出于参数和性能的综合考虑,作者使用性价比较高的空间金字塔池化(SPP)模块,遵循主流的YOLO框架的做法,对SPP模块进行适当的改进。

改进的SPP模块的网络结构设计参考了YOLOv5开源项目中的实现方法,让一层5×5的最大池化层等效于先前讲过的5×5、9×9和13×13这三条并行的最大池化层分支,从而降低计算开销,这也和之前所讲的空间金字塔的特性相同,通过逐层卷积能够从小到大找到不同尺寸的目标,再将不同的卷积结果叠起来进行最终的输出。

# YOLO_Tutorial/models/yolov1/yolov1_neck.py
# --------------------------------------------------------
...
class SPPF(nn.Module):
    def __init__(self, in_dim, out_dim, expand_ratio=0.5, pooling_size=5,
                 act_type='lrelu', norm_type='BN'):
        super().__init__()
        inter_dim=int(in_dim * expand_ratio)
        self.out_dim=out_dim
        self.cv1=Conv(in_dim, inter_dim, k=1, act_type=act_type, norm_type=
          norm_type)
        self.cv2=Conv(inter_dim * 4, out_dim, k=1, act_type=act_type, norm_type=
          norm_type)
        self.m=nn.MaxPool2d(kernel_size=pooling_size, stride=1, padding=pooling_
          size // 2)
 
    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-2中,输入的特征图会先被一层1 \times 1卷积处理,其通道数会被压缩一半,随后再由一层5 \times 5最大池化层连续处理三次,依据感受野的原理,该处理方式等价于分别使用5 \times 59 \times 913 \times 13最大池化层并行地处理特征图。最后,将所有处理后的特征图沿通道拼接,再由另一层1 \times 1卷积做一次输出的映射,将其通道映射至指定数目的输出通道。

2.1.3 检测头

在YOLOv1中,检测头部分用的是全连接层,全连接层具有参数过多,过于占用内存空间的缺点,这里,我们抛弃全连接层,改用卷积网络。由于当前主流的检测头是解耦检测头,因此,我们也采用解耦检测头作为YOLOv1的检测头,由类别分支和回归分支组成,类别分支进行类别和置信度预测,回归分支进行位置参数预测,如图4-4所示。

检测头的结构十分简单,共输出两种不同的特征:类别特征\mathbf{F}_{cls} \in \mathbb{R}^{13 \times 13 \times 512}和位置特征\mathbf{F}_{reg} \in \mathbb{R}^{13 \times 13 \times 512},没有复杂结构,代码编写简单,作者实现了相关代码,如以下代码所示:

# YOLO_Tutorial/models/yolov1/yolov1_head.py
# --------------------------------------------------------
...
class DecoupledHead(nn.Module):
    def __init__(self, cfg, in_dim, out_dim, num_classes=80):
        super().__init__()
        print('==============================')
        print('Head: Decoupled Head')
        self.in_dim=in_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']
 
        # cls head
        cls_feats=[]
        self.cls_out_dim=max(out_dim, num_classes)
        for i in range(cfg['num_cls_head']):
            if i==0:
                cls_feats.append(
                    Conv(in_dim, self.cls_out_dim, k=3, p=1, s=1,
                        act_type=self.act_type,
                        norm_type=self.norm_type,
                        depthwise=cfg['head_depthwise'])
                        )
            else:
                cls_feats.append(
                    Conv(self.cls_out_dim, self.cls_out_dim, k=3, p=1, s=1,
                        act_type=self.act_type,
                        norm_type=self.norm_type,
                        depthwise=cfg['head_depthwise'])
                        )
        # reg head
        reg_feats=[]
        self.reg_out_dim=max(out_dim, 64)
        for i in range(cfg['num_reg_head']):
            if i==0:
                reg_feats.append(
                    Conv(in_dim, self.reg_out_dim, k=3, p=1, s=1,
                        act_type=self.act_type,
                        norm_type=self.norm_type,
                        depthwise=cfg['head_depthwise'])
                        )
            else:
                reg_feats.append(
                    Conv(self.reg_out_dim, self.reg_out_dim, k=3, p=1, s=1,
                        act_type=self.act_type,
                        norm_type=self.norm_type,
                        depthwise=cfg['head_depthwise'])
                        )
 
        self.cls_feats=nn.Sequential(*cls_feats)
        self.reg_feats=nn.Sequential(*reg_feats)
 
    def forward(self, x):
        cls_feats=self.cls_feats(x)
        reg_feats=self.reg_feats(x)
 
        return cls_feats, reg_feats
2.1.4 预测层

        在官方的YOLOv1中,每个网格预测两个边界框,而这两个边界框的学习完全依赖自身预测的边界框位置的准确性,YOLOv1本身并没有对这两个边界框做任何约束。可以认为,这两个边界框是“平权”的,谁学得好谁学得差完全是随机的,二者之间没有显式的互斥关系,且每个网格处最终只会输出置信度最大的边界框,那么可以将这两个“平权”的边界框修改为一个边界框,即每个网格处只需要输出一个边界框。于是,我们的YOLOv1网络最终输出的张量为\mathbf{Y} \in \mathbb{R}^{13 \times 13 \times (1+N_c+4)},其中通道维度上的1表示边界框的置信度,N_c表示类别的总数,4表示边界框的4个位置参数。这里不再有表示每个网格的边界框数量的B

        预测层的结构如下图所示:

        预测层中的几个部分:

        (1) 边界框置信度:使用类别特征\mathbf{F}_{cls} \in \mathbb{R}^{13 \times 13 \times 512}来完成边界框置信度的预测。另外,不同于YOLOv1中使用预测框与目标框的IoU作为优化目标,我们暂时采用简单的二分类标签0/1作为置信度的学习标签。这样改进并不表示二分类标签比将IoU作为学习标签的方法更好,而仅仅是图方便,省去了在训练过程中计算IoU的麻烦,且便于读者上手。由于边界框的置信度的值域在0~1范围内,为了确保这一点,避免网络输出超出这一值域的“不合理”数值,作者使用Sigmoid函数将网络的置信度输出映射到0~1范围内。

        (2) 类别置信度:作者使用类别特征\mathbf{F}_{cls} \in \mathbb{R}^{13 \times 13 \times 512}来完成类别置信度的预测。因此,类别特征将分别被用于有无目标的检测和类别分类两个子任务中。类别置信度显然也在0~1范围内,因此我们使用Sigmoid函数来输出对每个类别置信度的预测。

        (3) 边界框位置参数:作者使用位置特征\mathbf{F}_{reg} \in \mathbb{R}^{13 \times 13 \times 512}来完成边界框位置参数的预测。我们已经知道,边界框的中心点偏差(t_x,t_y)的值域是0~1,因此,我们也对网络输出的中心点偏差t_xt_y使用Sigmoid函数。另外两个参数Wh是非负数,这也就意味着,我们必须保证网络输出的这两个量是非负数,否则没有意义。一种办法是用ReLU函数来保证这一点,然而ReLU的负半轴梯度为0,无法回传梯度,有“死元”的潜在风险。另一种办法则是仍使用线性输出,但添加一个不小于0的不等式约束。但不论是哪一种方法,都存在约束问题,这一点往往是不利于训练优化的。为了解决这一问题,我们采用指数函数来处理,该方法既能保证输出范围是实数域,又是全局可微的,不需要额外的不等式约束。两个参数Wh的计算如以下公式(4-1)所示,其中,指数函数外部乘了网络的输出步长S,这就意味着预测的t_wt_h都是相对于网格尺度来表示的。

W=s \times e^{t_w} \\ h =s \times e^{t_h} \\

2.1.5 修改损失函数

        在改变了检测头和预测层之后,同样需要修改YOLO的损失函数,包括置信度损失,类别损失和边界框的位置参数损失,以便后续能够正常的训练模型。

        (1) 置信度损失:由于置信度的输出经过Sigmoid函数的处理,因此我们采用二元交叉熵(binary cross entropy,BCE)函数来计算置信度损失,如公式(4-2)所示,其中,N_{pos}是正样本的数量。

\mathbf{L}_{conf}=- \frac{1}{N_{pos}} \sum_{i=1}^{S^2} \left[ (1 - \hat{C}_i) \log (1 - C_i) + \hat{C}_i \log (C_i) \right]

        (2) 类别损失:接着是修改类别置信度的损失函数。由于类别预测中的每个类别置信度都经过Sigmoid函数的处理,因此,我们同样采用BCE函数来计算类别损失,如下式所示。

\mathbf{L}_{cls}=- \frac{1}{N_{pos}} \sum_{i=1}^{N_c} \sum_{i=1}^{S^2} \mathbb{I}_i^{obj} \left[ (1 - \hat{C}_i) \log (1 - C_i) + \hat{C}_i \log (C_i) \right]

        (3) 边界框位置参数的损失:对于位置损失,我们采用更主流的办法。具体来说,我们首先根据预测的中心点偏差以及宽和高来得到预测框B_{pred},然后计算预测框B_{pred}与目标框B_{gt}的GIoU(generalized IoU),最后,使用线性GIoU损失函数去计算位置参数损失,如下式所示:

\mathbf{L}_{reg}= \frac{1}{N_{pos}} \sum_{i=0}^{S^2} \mathbb{I}_i^{obj}[1-GIOU(B_{pred},B_{gt})]

        最后,将以上三项相加就可以得到最终的损失函数,最终的损失函数的表示形式如下式所示:

L_{loss}=L_{conf}+L_{cls}+\lambda_{reg}L_{reg}        


http://www.kler.cn/a/519747.html

相关文章:

  • 【25美赛A题-F题全题目解析】2025年美国大学生数学建模竞赛(MCM/ICM)解题思路|完整代码论文集合
  • unity学习20:time相关基础 Time.time 和 Time.deltaTime
  • 与机器学习相关的概率论重要概念的介绍和说明
  • MYSQL数据库 - 启动与连接
  • React第二十五章(受控组件/非受控组件)
  • 路径总和(力扣112)
  • ShardingJDBC私人学习笔记
  • 【fly-iot飞凡物联】(20):2025年总体规划,把物联网整套技术方案和实现并落地,完成项目开发和课程录制。
  • 2025数学建模美赛|C题成品论文|第一问
  • WPS数据分析000006
  • [C++技能提升]插件模式
  • 低代码系统-氚云、简道云表单控件对比
  • Typesrcipt泛型约束详细解读
  • 实现宿主机(Windows 10 Docker Desktop)和Linux容器之间的数据挂载的三种方法
  • 【力扣每日一题】LeetCode 2412: 完成所有交易的初始最少钱数
  • Oracle之开窗函数使用
  • Vue编程式路由跳转多次执行报错
  • go入门Windows环境搭建
  • doris: CSV导入数据
  • (一)HTTP协议 :请求与响应
  • Android Toast在指定的Display里面显示
  • TLF35584 基本介绍
  • JAVASE入门十脚-红黑树,比较器,泛型
  • 校园商铺管理系统设计与实现(代码+数据库+LW)
  • 计算机网络 (62)移动通信的展望
  • ChatGPT的本质是什么?