yolov1原理学习与代码复现
发表于 2026-02-01 共 5955 字
分类于 探索分类于 计算机视觉

yolov1原理学习与代码复现

背景介绍

YOLO (You Only Look Once) 已经用了很久了,但是一直没有去深入挖掘其背后的原理。于是,就有了这次探索,从yolov1版本学起。

YOLO之所以叫这个名字,大概率是因为,人类看一眼图像就能立即知道图像中的物体是什么。

在当年,YOLO在进行预测时能够对图像进行全局推理,且速度极快。

论文解读

论文arxiv链接 You Only Look Once: Unified, Real-Time Object Detection

推理过程

先解读一下推理的过程和约定的参数含义。

在论文中提到,首先将输入的图像划分为 $S \times S$ 个小网格 (如 $S=7$,$7 \times 7$),模型推理会得到若干个检测框,如果检测框的中心点落入一个网格中,我们就认为这个网格负责这个框的检测。

每个网格都负责 $B$ 个检测框 (中心都落入这个网格内),因此最多能检测出 $S \times S \times B$ 个检测框,在yolov1中 $B=2$。

每个检测框可以用 $(x,y,w,h,c)$ 这五个量来描述,代表矩形框的中心点为 $(x,y)$,宽为 $w$,高为 $h$,用 $c$ 代表"框内确实有物体"的置信度 (confidence)。

对于每个网格,会用一个大小为30的张量 (Tensor)来描述:$(x_{1},y_{1},w_{1},h_{1},c_{1},x_{2},y_{2},w_{2},h_{2},c_{2},p_{1},p_{2},...,p_{20})$。

前面10个量很好理解,就是 $B=2$ 个检测框的 $5 \times 2=10$个属性,而后面的20个 $p_{n}$ 则代表了20种类别的条件概率 $P(Class_{i} | Object)$,也就是"在框内有物体的条件下,该物体是第 $i$ 个类别的概率"。从这里可以看出来,默认了一个网格内 $B$ 个检测框的类别是一致的,只是它们框选的置信度 $c_{i}$ 不同,那么对于一个网格内,两个检测框内"有第 $i$ 个类别物体"的概率分别为 $ c_{1} \times p_{i} $ 和 $ c_{2} \times p_{i} $。

至于为什么是20个类别,因为论文中使用的PASCAL VOC数据集就包含20种类别,比如说Car, Bus, Cat, Person之类的。

检测过程

如图所示,"Class probability map"就是将每个格子最有可能的物体类别 (也就是 $p_{i}$ 最大的类别) 用对应颜色可视化出来,"Bounding boxes + confidence"则是把 $7 \times 7 \times 2=98$ 个检测框都标出来了,边线粗的代表框选置信度 $c_{i}$ 高。

那么为了得到最终的"Final detections",可以用NMS非极大值抑制的方法,本质上就是如果有两个框重叠度非常高,那就可以认为它们是同一个对象,去掉置信度更低的那一个。

具体来说,将98个检测框按置信度从高到低排序后,两两之间计算交并比$IoU=\frac{交集面积}{并集面积}$ (这样完全重叠时IoU就是1),如果大于一个阈值就可以把置信度低的删去,时间复杂度 $O(n^2)$。

卷积神经网络

网络结构

受到GoogLeNet启发,论文配图描述了网络的结构,输入448x448 RGB三通道的图像。使用了多种卷积核尺寸(7x7, 3x3, 1x1),总共24个卷积层,还使用了 2x2 池化层进行下采样 (降维压缩)。最后两个全连接层,得到推理的结果 $7 \times 7 \times 30$ 张量。

模型网络结构

卷积的理解

概率论中计算连续型随机变量之和的概率密度函数,需要用到卷积公式 $\int_{-\infty}^{\infty} f(\tau) g(t - \tau) \, d\tau$,本质上是一种加权平均。

而图像处理用到的二维卷积是 $f * g(x, y) = \int_{-\infty}^{\infty} \int_{-\infty}^{\infty} f(u, v) g(x - u, y - v) \, du \, dv$,其实说白了就是两个函数在每个位置相乘累加,如果把 $g(x,y)$ "旋转180度"($x-u$ 和 $u$ 互换,$y-v$ 和 $v$ 互换),离散地来看,那其实就是$f * g[m, n] = \sum_{i=0}^{M-1} \sum_{j=0}^{N-1} f[i, j] \cdot g[i, j]$。

我们把 $g$ 用一个矩阵表示,叫做卷积核,比如说是 $$ \begin{bmatrix} 1 &0 &1 \\ 0 &1 &0 \\ 1 &0 &1 \end{bmatrix} $$

然后我们让这个卷积核在 $f$ 矩阵上"滑动",这样可以算出9个值,如下面动画所示 (假设偏置为0):

卷积动画

那这有什么用呢?卷积又被称为滤波器 (Filter)。对一张图像数据进行不同的卷积操作,可以得到很多种神奇的效果:

卷积图像效果

比如说边缘检测的卷积核:

$$ \begin{bmatrix} -1 &-1 &-1 \\ -1 &8 &-1 \\ -1 &-1 &-1 \end{bmatrix} $$

之所以能达到这种效果,是因为如果一个像素周围像素值和它几乎相等,那么它应该不是边缘点,这一块卷积之后结果就是0。

人们可以设计出不同的卷积核,对图片进行不同的处理。从锐化边缘提取我们可以看出来,卷积核有利于提取图像的一些"特征"。

深度学习的出现使得不再需要人工设计卷积核,卷积核本质是一些参数,可以通过机器学习训练得到。把卷积层引入神经网络就变成了卷积神经网络。

我的理解为,容易发现卷积是线性运算,理论上全连接层也可以达到这种效果,但是用卷积操作极大简化了网络参数量 (相当于连接更稀疏了),尤其是我们认为它能反映图像的特征,在计算机视觉中很有效。

以上说的是单通道单卷积核的操作,实际上用的是多通道多卷积核。

比如说用3x3卷积核把一个3通道RGB图像卷积得到16通道的输出,是如何进行的?

就是用了16个3x3卷积核,各分别得到一个通道输出,注意每个卷积核有3层 (和输入通道数一致,是3,所以共48个3x3矩阵),

考虑其中一个卷积核的三层分别叫$W_{Ri}, W_{Gi}, W_{Bi}$,那么用 $W_{Ri}$ 对R通道进行卷积,用 $W_{Gi}$ 对G通道进行卷积,用 $W_{Bi}$ 对B通道进行卷积,得到三个矩阵,把它们相加,再加上一个偏置值,就得到一个通道的卷积结果,16个通道都是这么来的。

下图展示了从 $C$ 个通道的输入卷积得到 $FN$ 个通道输出的过程,使用 $FN$ 个 $C$ 层卷积核进行操作。

多通道卷积图示

卷积核滑动的时候还有步长 $S$ (Stride) 和填充 $P$ (Padding) 参数,不过多解释。

如果一个卷积核高 $k_h$,宽 $k_w$,那么输出的尺寸为:

$$ \begin{cases} H_{out} = \frac{H_{in} + 2P - k_h}{S} + 1 \\ W_{out} = \frac{W_{in} + 2P - k_w}{S} + 1 \end{cases} $$

总之,不同的卷积核可以理解为从不同视角去"提取特征"。

Loss函数设计

如论文所述,loss function是五部分相加。

下述 $\mathbf{1}_{i}^{\text{obj}}$ 表示物体出现在 (应该意思是只要网格内有一个检测框负责就行) 网格 $i$ 中时为1,否则为0,$\mathbf{1}_{ij}^{\text{obj}}$ 表示网格 $i$ 中的第 $j$ 个检测框负责该预测时为1,否则为0。同理"noobj"上标和"obj"含义是反的。

注意,一个网格中有 $B$ 个检测框,和标注数据 IoU 最大的那个检测框才是对其负责的。

检测框中心点定位误差

$\lambda_{\text{coord}} \sum_{i=0}^{S^2} \sum_{j=0}^{B} \mathbf{1}_{ij}^{\text{obj}} \left[ (x_i - \hat{x}_i)^2 + (y_i - \hat{y}_i)^2 \right]$

很好理解,就是遍历所有的检测框,如果这个框内有负责的物体,就加上与标定值的平方和误差。

检测框尺寸误差

$\lambda_{\text{coord}} \sum_{i=0}^{S^2} \sum_{j=0}^{B} \mathbf{1}_{ij}^{\text{obj}} \left[ \left( \sqrt{w_i} - \sqrt{\hat{w}_i} \right)^2 + \left( \sqrt{h_i} - \sqrt{\hat{h}_i} \right)^2 \right]$

宽和高的误差,之所以这里要开根号,是为了突显小框的误差,想象一下,一个边长30的正方形预测成29,和一个边长3的正方形预测成2,哪个更严重?显然是小尺寸的检测框偏差1像素更致命,而开根就让这种误差更敏感。

有物体时的置信度误差

$\sum_{i=0}^{S^2} \sum_{j=0}^{B} \mathbf{1}_{ij}^{\text{obj}} \left( C_i - \hat{C}_i \right)^2$

如果标定的数据集中这个框内有负责的物体,那么 $P(object)=1$,这里的 $\hat{C}_i=P(object) \times IoU_{truth}^{pred}=IoU_{truth}^{pred}$,也就是矩形框和数据集中标定框的交并面积之比。

无物体时的置信度误差

$\lambda_{\text{noobj}} \sum_{i=0}^{S^2} \sum_{j=0}^{B} \mathbf{1}_{ij}^{\text{noobj}} \left( C_i - \hat{C}_i \right)^2$

如果标定的数据集中这个框没有负责的物体,那么 $\hat{C}_i=0$。

有物体时的类别概率误差

$\sum_{i=0}^{S^2} \mathbf{1}_{i}^{\text{obj}} \sum_{c \in \text{classes}} \left( p_i(c) - \hat{p}_i(c) \right)^2$

如果数据集对这个网格标注的类别为 $c$,那么应该是只有 $\hat{p}_i(c)$ 为1,其他 $\hat{p}_i$ 都为0。

也有变种实现使用的是交叉熵损失。

权重说明

注意到存在 $\lambda_{\text{coord}}$ 和 $\lambda_{\text{noobj}}$,论文中的解释为,在图像中许多网格单元不包含任何物体,这会将这些单元格的置信度推向零,通常会压倒包含物体的单元格的梯度,可能会导致模型训练不稳定。所以就设置$\lambda_{\text{coord}}=5, \lambda_{\text{noobj}}=0.5$,以降低无物体误差的权重。

代码复现

模型结构实现

再放一遍图,使用Pytorch编写,复刻图示结构:

模型网络结构

用表格梳理:

模块 操作说明 输出尺寸 (H×W×C)
输入 RGB 图像 448×448×3
conv1 7×7 大卷积核,stride=2,输出 64 通道,提取粗特征 224×224×64
pool1 MaxPool(2×2, stride=2),缩小一半 112×112×64
conv2 3×3 卷积,stride=1,输出 192 通道 112×112×192
pool2 MaxPool(2×2, stride=2) 56×56×192
conv3 一组 [1×1, 3×3, 1×1, 3×3],通道扩展到 512 56×56×512
pool3 MaxPool(2×2, stride=2) 28×28×512
conv4 多组 [1×1, 3×3] 堆叠,最后扩展到 1024 通道 28×28×1024
pool4 MaxPool(2×2, stride=2) 14×14×1024
conv5 [1×1, 3×3] 堆叠,空间缩小到 7×7,保持在 1024 通道,最后一个 3×3 stride=2 下采样 7×7×1024
conv6 两个 3×3 卷积,输出仍是 1024 通道 7×7×1024

最后的全连接层使用线性激活函数,其他部分全部使用参数为0.1的LeakyReLU激活函数:

$$ \phi(x) = \begin{cases} x, & \text{if } x > 0, \\ 0.1x, & \text{otherwise} \end{cases} $$

import torch
import torch.nn as nn

class Block(nn.Module):
    def __init__(self, layers_param, use_pool=True):
        super().__init__()
        layers = []
        for in_ch, out_ch, k_size, stride, padding in layers_param:
            layers.append(nn.Conv2d(in_ch, out_ch, k_size, stride, padding, bias=False))
            layers.append(nn.LeakyReLU(0.1))
        self.conv = nn.Sequential(*layers)
        self.pool = nn.MaxPool2d(2) if use_pool else None

    def forward(self, x):
        x = self.conv(x)
        if self.pool is not None:
            x = self.pool(x)
        return x

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

        self.conv1 = Block([[3, 64, 7, 2, 3]])
        self.conv2 = Block([[64, 192, 3, 1, 1]])
        self.conv3 = Block([
            [192, 128, 1, 1, 0],
            [128, 256, 3, 1, 1],
            [256, 256, 1, 1, 0],
            [256, 512, 3, 1, 1]
        ])
        self.conv4 = Block([
            [512, 256, 1, 1, 0],
            [256, 512, 3, 1, 1],
            [512, 256, 1, 1, 0],
            [256, 512, 3, 1, 1],
            [512, 256, 1, 1, 0],
            [256, 512, 3, 1, 1],
            [512, 256, 1, 1, 0],
            [256, 512, 3, 1, 1],
            [512, 512, 1, 1, 0],
            [512, 1024, 3, 1, 1]
        ])
        self.conv5 = Block([
            [1024, 512, 1, 1, 0],
            [512, 1024, 3, 1, 1],
            [1024, 512, 1, 1, 0],
            [512, 1024, 3, 1, 1],
            [1024, 1024, 3, 1, 1],
            [1024, 1024, 3, 2, 1]
        ], use_pool=False)
        self.conv6 = Block([
            [1024, 1024, 3, 1, 1],
            [1024, 1024, 3, 1, 1]
        ], use_pool=False)

        # 初始化
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='leaky_relu')

    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = self.conv3(x)
        x = self.conv4(x)
        x = self.conv5(x)
        x = self.conv6(x)
        return x


if __name__ == "__main__":
    x = torch.randn(1, 3, 448, 448)
    net = DarkNet()
    print(net)
    out = net(x)
    print(out.shape)

直接运行,会输出网络的结构,同时测试得到输出shape为torch.Size([1, 1024, 7, 7]),张量大小是对的,说明网络没问题。

预训练

按论文所述,模型首先在ImageNet数据集上进行预训练,参与预训练的模型并不是前面图示完整的模型结构,而是前20个卷积层+1个平均池化层+1个全连接层,且参与输入图像尺寸是244x244。预训练完成后,再添加4个卷积层和2个全连接层,将输入增加到448x448,用完整的结构在PASCAL VOC数据集上进行微调训练,以实现目标检测。

通俗来说就是,我们认为图像分类和目标检测两种任务具有相似性,让模型先在大型数据集ImageNet (1000种类别) 上"学习图像的特征",用学到的权重参数来初始化卷积层,这样能加速后面的训练,同时能增强模型的泛化能力。

论文中提到他们用了大约一周的时间来完成预训练,由于数据集太大且耗时长,我们可以利用ResNet50预训练权重的一部分,并对其进行修改以模拟替代这个结构 (详细参见GitHub上有人写的这个pytorch实现 https://github.com/EclipseR33/yolo_v1_pytorch,其部分实现与原论文存在差异):

import torch
from torchvision.models.resnet import ResNet, Bottleneck

class ResNet_(ResNet):
    def __init__(self, block, layers):
        super(ResNet_, self).__init__(block=block, layers=layers)

    def _forward_impl(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.maxpool(x)
        x = self.layer3(x)
        x = self.maxpool(x)
        return x

    def forward(self, x):
        return self._forward_impl(x)

def _resnet(block, layers, pretrained):
    model = ResNet_(block, layers)
    if pretrained is not None:
        state_dict = torch.load(pretrained)
        model.load_state_dict(state_dict)
    return model

def resnet_1024ch(pretrained=None) -> ResNet:
    resnet = _resnet(Bottleneck, [3, 4, 6, 3], pretrained)
    return resnet

if __name__ == '__main__':
    x = torch.randn([1, 3, 448, 448])
    net = resnet_1024ch('resnet50-19c8e357.pth')
    print(net)
    y = net(x)
    print(y.size())

ResNet50预训练权重从pytorch官网下载resnet50-19c8e357.pth

再用表格看看每层的效果:

模块 操作说明 输出尺寸 (H×W×C)
输入 RGB 图像 448×448×3
conv1 7×7 卷积,stride=2,输出 64 通道 224×224×64
maxpool 3×3 池化,stride=2 112×112×64
layer1 3 个 Bottleneck,输出 256 通道 112×112×256
layer2 4 个 Bottleneck,第一个 stride=2,下采样 56×56×512
额外 maxpool 3×3 池化,stride=2 28×28×512
layer3 6 个 Bottleneck,第一个 stride=2,下采样 14×14×1024
额外 maxpool 3×3 池化,stride=2 7×7×1024

原有的layer4和后面的部分删去,相当于利用了ResNet的部分结构,继承重写了原有的类,额外添加两个池化层,使得最终输出尺寸和yolov1结构的一致。

有可能是拼接了ResNet的原因,这样训练出来权重会比较大 (900多MB)。

数据预处理

首先获取数据集,现在可以从这里下载,Kaggle PASCAL VOC 2007 and 2012

在Annotations目录里,有每张图片的标注数据,类似这样:

<annotation>
    <folder>VOC2007</folder>
    <filename>000001.jpg</filename>
    <source>
        <database>The VOC2007 Database</database>
        <annotation>PASCAL VOC2007</annotation>
        <image>flickr</image>
        <flickrid>341012865</flickrid>
    </source>
    <owner>
        <flickrid>Fried Camels</flickrid>
        <name>Jinky the Fruit Bat</name>
    </owner>
    <size>
        <width>353</width>
        <height>500</height>
        <depth>3</depth>
    </size>
    <segmented>0</segmented>
    <object>
        <name>dog</name>
        <pose>Left</pose>
        <truncated>1</truncated>
        <difficult>0</difficult>
        <bndbox>
            <xmin>48</xmin>
            <ymin>240</ymin>
            <xmax>195</xmax>
            <ymax>371</ymax>
        </bndbox>
    </object>
    <object>
        <name>person</name>
        <pose>Left</pose>
        <truncated>1</truncated>
        <difficult>0</difficult>
        <bndbox>
            <xmin>8</xmin>
            <ymin>12</ymin>
            <xmax>352</xmax>
            <ymax>498</ymax>
        </bndbox>
    </object>
</annotation>

可以看到包含了每个object的类别名称,还有框选的xmin, ymin, xmax, ymax坐标。

这种XML格式可以用python的xml模块进行解析。然后要实现一个yolo类,首先需要对数据集进行解析,详见注释:

def label2grid(self, label):
    """
    将原始标注(xmin, ymin, xmax, ymax, class)转换为所需网格格式。
    YOLOv1 假设每个网格最多一个目标,所以这里只保留第一个落入该网格的目标。
    :param label: Tensor,形状 [N, 5],每行为 [xmin, ymin, xmax, ymax, class_id]
    :return: List,长度为 S*S。每个元素为:
        - None:该网格无目标
        - Tensor [1, 5]:该网格有目标,格式 [cx, cy, w, h, class_id]
    """
    # 初始化 S*S 个网格都为 None
    output = [None for _ in range(self.s ** 2)]
    # 计算每个网格的尺寸
    size = self.image_size // self.s
    # 获取标注框数量
    n_bbox = label.size(0)
    # 如果没有标注框,直接返回全 None
    if n_bbox == 0:
        return output

    # 创建新张量 label_c
    label_c = torch.zeros_like(label)
    # 将 (xmin, ymin, xmax, ymax) 转换为中心点 + 宽高
    # 中心点 x 坐标 = (xmin + xmax) / 2
    label_c[:, 0] = (label[:, 0] + label[:, 2]) / 2
    # 中心点 y 坐标 = (ymin + ymax) / 2
    label_c[:, 1] = (label[:, 1] + label[:, 3]) / 2
    # 宽度 = xmax - xmin(假设 xmax > xmin)
    label_c[:, 2] = label[:, 2] - label[:, 0]
    # 高度 = ymax - ymin(假设 ymax > ymin)
    label_c[:, 3] = label[:, 3] - label[:, 1]
    # 保留类别标签
    label_c[:, 4] = label[:, 4]

    # 确定每个目标中心点所属的网格索引
    # idx_x[i] = 第 i 个目标中心点所在的 x 方向网格索引
    idx_x = [int(label_c[i][0]) // size for i in range(n_bbox)]
    # idx_y[i] = 第 i 个目标中心点所在的 y 方向网格索引
    idx_y = [int(label_c[i][1]) // size for i in range(n_bbox)]

    # 中心点坐标归一化,先对 size 取模,再除以 size,归一化到 [0, 1]
    label_c[:, 0] = torch.div(torch.fmod(label_c[:, 0], size), size)
    label_c[:, 1] = torch.div(torch.fmod(label_c[:, 1], size), size)
    # 将宽高归一化到整张图像
    label_c[:, 2] = torch.div(label_c[:, 2], self.image_size)
    label_c[:, 3] = torch.div(label_c[:, 3], self.image_size)

    # 将每个目标分配到对应的网格中
    for i in range(n_bbox):
        # 计算展开的一维网格索引
        idx = idx_y[i] * self.s + idx_x[i]
        # 如果该网格还没有目标,就放入这个目标,这样只留第一个,官方 Darknet 是覆盖为最后一个
        if output[idx] is None:
            output[idx] = torch.unsqueeze(label_c[i], dim=0)

    return output

损失函数实现

def compute_loss(self, input, target):
    """
    计算单张图像的损失
    :param input: 单图模型输出,[S*S, B*5 + num_classes]
    :param target: 经过 label_direct2grid_v1 处理后的标签,List[None 或 Tensor[1,5]]
    :return: (总损失, xy_loss, wh_loss, conf_loss, class_loss),都是标量 tensor
    """
    # 原文中的参数
    lambda_coord = 5.0
    lambda_noobj = 0.5
    # 提取预测框部分 [cx, cy, w, h, conf]
    input_bbox = input[:, :self.b * 5].reshape(-1, self.b, 5)
    # 提取分类 [S*S, num_classes]
    input_class = input[:, self.b * 5:]
    # 对预测框坐标和置信度应用 sigmoid,约束输出归一化
    input_bbox = torch.sigmoid(input_bbox)

    # 初始化各网格的损失
    loss = torch.zeros(1, device=self.device)
    xy_loss = torch.zeros(1, device=self.device)
    wh_loss = torch.zeros(1, device=self.device)
    conf_loss = torch.zeros(1, device=self.device)
    class_loss = torch.zeros(1, device=self.device)

    for i in range(self.s ** 2):
        # 当前网格无物体
        if target[i] is None:
            # 所有 B 个预测框的置信度都应该趋近于 0
            obj_conf_target = torch.zeros(self.b, device=self.device)
            conf_loss += torch.sum(lambda_noobj * (input_bbox[i, :, 4] - obj_conf_target) ** 2)

        # 当前网格有物体
        else:
            # 计算每个预测框与标注数据的 IoU
            true_box = target[i][:, :4]
            pred_boxes = input_bbox[i, :, :4]
            ious = self.get_iou(pred_boxes, true_box, i)
            # 找到 IoU 最大的预测框,也就是"负责人"
            best_box_idx = torch.argmax(ious).item()
            best_iou = ious[best_box_idx]

            # 中心点坐标损失
            xy_loss += lambda_coord * (
                (input_bbox[i, best_box_idx, 0] - target[i][0, 0]) ** 2 +
                (input_bbox[i, best_box_idx, 1] - target[i][0, 1]) ** 2
            )

            # 尺寸损失,对 w, h 取平方根,添加 1e-8 防止 sqrt(0) 的梯度问题
            wh_loss += lambda_coord * (
                (torch.sqrt(input_bbox[i, best_box_idx, 2] + 1e-8) - torch.sqrt(target[i][0, 2])) ** 2 +
                (torch.sqrt(input_bbox[i, best_box_idx, 3] + 1e-8) - torch.sqrt(target[i][0, 3])) ** 2
            )

            # 置信度损失,置信度目标 = IoU
            conf_loss += (input_bbox[i, best_box_idx, 4] - best_iou) ** 2

            # 注意,虽然网格内有物体,但只有一个预测框对其负责,这里还有对剩下框noobj的置信度损失
            for b in range(self.b):
                if b != best_box_idx: # 非负责框
                    conf_loss += lambda_noobj * (input_bbox[i, b, 4] - 0) ** 2

            # 类别概率损失
            # 创建 one-hot 目标向量
            true_class = int(target[i][0, 4])
            class_target = torch.zeros(self.num_classes, device=self.device)
            # 只有标注类别概率为 1,其他全 0
            class_target[true_class] = 1.0
            class_loss += torch.sum((input_class[i] - class_target) ** 2)

    loss = xy_loss + wh_loss + conf_loss + class_loss # 总损失
    return loss, xy_loss, wh_loss, conf_loss, class_loss

训练过程

论文中提到:

Our learning rate schedule is as follows: For the first epochs we slowly raise the learning rate from $10^{−3}$ to $10^{−2}$. If we start at a high learning rate our model often diverges due to unstable gradients. We continue training with $10^{−2}$ for 75 epochs, then $10^{−3}$ for 30 epochs, and finally $10^{−4}$ for 30 epochs.

总共135遍训练,学习率是先逐渐增大,再逐渐降低的。初始使用一个很大的学习率训练会导致数值不稳定,所以会在开始使用一个小的学习率,使参数逐渐接近解空间的大致位置,后续为了更加接近目标解,学习率会逐渐降低。

除此之外,还提到对原始数据使用了随机缩放和随机亮度色彩调整,以防止过拟合。

一个稍微简化的实现见Github仓库,实测在5060ti显卡上训练一个epoch要10分钟左右,还是有点慢的......

局限性和思考

预测结果

yolov1每个网格只能预测一种目标,且最多检测98个目标,针对成群的小型物体时效果就比较差。但是在当年这种快速且准确率还不错的方法开创了新道路,YOLO的后续版本也基于此进行了若干改进。

解读完yolov1,可能会有一些问题和思考。首先为什么要得到98个检测框再进行抑制?因为CNN是有固定维度的输出张量,所以表示固定数量的框。而不同对象的位置又不一样,所以很自然地就想到划分网格单元,负责不同位置问题的检测。

那么一个网格内为什么又要多个框呢?感觉就是提供了"候选项",进行冗余预测,这样可能定位更精准。

卷积层又为什么要这么设计呢?除了让shape与预期输出一致,其实感觉还是基于经验的,能work就行(

筛选文章
类别选择 (分类/标签)
全屏 关闭