目标检测算法之YOLO(YOLOv10)

Background

yolov10是清华大学发布的一个新yolo算法,作者认为NMS是影响yolo实时推理的一个主要问题,针对这个问题提出了dual assignments的方法来训练NMS-free的yolo模型。同时作者从速度和精度入手,提出了几个改进模型的方法。yolov10在推理速度以及参数量上都优于现有的模型,且仍具有不弱于其他模型的精度。接下来主要分析yolov10中的dual asignments和 Efficiency-Accuracy优化策略。

yolov10的论文地址:https://arxiv.org/abs/2405.14458
yolov10的代码地址:https://github.com/THU-MIG/yolov10

Consistent Dual Assignments for NMS-free Training

作者认为one-to-many label assign需要NMS进行后处理,从而影响模型部署后的推理速度,然而one-to-one label assign会带来额外的推理开销并且没有太好的表现。
基于这俩个原因,提出了dual label assignmets的策略,即在yolo中同时采用one-one和one-to-many。模型的结构见下图
在这里插入图片描述
从上图可以看出,相较于普通的yolo头,这里的yolo头有俩个,分别用于one-to-one和one-to-many。同时每个头都采用了解耦头的方式,即回归和分类分开。这里的思想跟v7中的辅助头是挺像的,通过一个aux head实现one-to-many,lead head实现one-to-one。最后在推理阶段丢弃aux head。

引入了俩个头之后,最大的问题就是如何去分配标签。这里作者对俩个头都用了一样的匹配度量。这里匹配度量的仍然是考虑了分类头和回归头,这里匹配损失记为 m m m,具体计算如下 m ( α , β ) = s ⋅ p α ⋅ I O U ( b ^ , b ) β m(\alpha,\beta) = s\cdot p^{\alpha}\cdot IOU(\hat{b},b)^{\beta} m(α,β)=spαIOU(b^,b)β其中 p p p是指分类分数, b ^ \hat{b} b^为预测框, b b b为真实框, α \alpha α β \beta β是超参数。
这样度量就同时考虑了分类任务和回归任务。对于one-to-one的任务记为 m o 2 o = m o 2 o ( α o 2 o , β o 2 o ) m_{o2o} = m_{o2o}(\alpha_{o2o},\beta_{o2o}) mo2o=mo2o(αo2o,βo2o),对于one-to-many的任务记为 m o 2 m = m o 2 m ( α o 2 m , β o 2 m ) m_{o2m}=m_{o2m}(\alpha_{o2m},\beta_{o2m}) mo2m=mo2m(αo2m,βo2m)

作者认为o2m能够提供更丰富的监督信息,同时希望o2m能够引导o2o。所以作者分析了o2o和o2m的差距,发现它们在回归任务上的表现没什么大的差异,所以认为它们的差距主要体现在分类任务上。然后就是针对分类任务进行改进。

先给定一个目标,然后记俩个头预测的最大IOU为 u ∗ u^* u(由于回归任务差不多,所任可以认为俩个头预测的最大IOU相等),然后记俩个头的最大匹配分数为 m o 2 o ∗ m^{*}_{o2o} mo2o m o 2 m ∗ m^{*}_{o2m} mo2m

同时假设 o 2 m o2m o2m的最佳匹配为 Ω \Omega Ω,而 o 2 o o2o o2o的最佳匹配为第 i i i个,并记为 m o 2 o , i = m o 2 o ∗ m_{o2o,i}=m_{o2o}^* mo2o,i=mo2o。然后有以下的等式关系 t o 2 m , j = u ∗ m o 2 m , j m o 2 m ∗ ≤ u ∗ t o 2 o , i = u ∗ m o 2 o , i m o 2 o ∗ = u ∗ \begin{align*}t_{o2m,j} =& u^*\frac{m_{o2m,j}}{m^{*}_{o2m}}\leq u^*\\ t_{o2o,i}=&u^*\frac{m_{o2o,i}}{m^*_{o2o}} = u^*\end{align*} to2m,j=to2o,i=umo2mmo2m,juumo2omo2o,i=u这里的等式是采取了跟TOOD中类似的归一化度量,首先这里认为分类损失匹配 t o 2 o ∣ o 2 m t_{o2o|o2m} to2oo2m m o 2 o ∣ o 2 m m_{o2o|o2m} mo2oo2m是等价的(这里不能用公式中的 p p p来看做 t t t)。然后借助TOOD中的归一化方法处理 m m m,即 n o r m ( u ) = max ⁡ ( i o u ) ∗ u max ⁡ ( u ) norm(u) = \max(iou)*\frac{u}{\max(u)} norm(u)=max(iou)max(u)u

然后定义了俩个头的差距用 1-Wasserstein distance表示,具体为 A = t o 2 o , i − 1 i ∈ Ω t o 2 m , i + ∑ k ∈ Ω ∖ { i } t o 2 m , k A =t_{o2o,i}-1_{i\in\Omega}t_{o2m,i}+\sum_{k\in{\Omega}\setminus \{i\}}t_{o2m,k} A=to2o,i1iΩto2m,i+kΩ{i}to2m,k直观地说,这个公式是使 t o 2 o t_{o2o} to2o越来越接近 t o 2 m t_{o2m} to2m。同使 t o 2 m , i t_{o2m,i} to2m,i尽可能地大(这里有点奇怪,不过合理的解释是当A的权重不大时,这里可以使 t o 2 o , i t_{o2o,i} to2o,i接近 t o 2 m , i t_{o2m,i} to2m,i,使 t o 2 o , k t_{o2o,k} to2o,k稍微抑制使其变小)

具体上,作者是对one2one和one2many都采用了TAL作为分配策略,但是对one2one采用top1,对one2many采用top10。这里v10是延续了v8的分配方式。

class v10DetectLoss:
    def __init__(self, model):
        self.one2many = v8DetectionLoss(model, tal_topk=10)
        self.one2one = v8DetectionLoss(model, tal_topk=1)
    
    def __call__(self, preds, batch):
        one2many = preds["one2many"]
        loss_one2many = self.one2many(one2many, batch)
        one2one = preds["one2one"]
        loss_one2one = self.one2one(one2one, batch)
        return loss_one2many[0] + loss_one2one[0], torch.cat((loss_one2many[1], loss_one2one[1]))

在源码中没有找到对齐损失1-Wasserstein distance的实现(有读者发现的话可以在评论说一下)。其余的loss function跟v8类似,就是引入了一个one2many。

Holistic Efficiency-Accuracy Driven Model Design

作者认为yolo架构存在较多的计算冗余,这样会阻碍它的能力。所以作者希望在速度和精度俩个方面对模型进行改进。

Efficiency driven model design

作者认为在yolo中下采样、basic building blocks和头部三个部分对速率影响较大。所以分别对这三部分进行改进。

Lightweight classification head

第一个是头部进行改进,作者通过实验验证了改进回归头对模型的表现影响较大,然后分类头的参数和计算量是回归头的俩倍多,所以作者使用了俩个深度可分离卷积( depthwise separable convolutions)替代了原来的分类头。具体代码如下

self.cv3 = nn.ModuleList(nn.Sequential(nn.Sequential(Conv(x, x, 3, g=x), Conv(x, c3, 1)), \
                                       nn.Sequential(Conv(c3, c3, 3, g=c3), Conv(c3, c3, 1)), \
                                        nn.Conv2d(c3, self.nc, 1)) for i, x in enumerate(ch))

Spatial-channel decoupled downsampling

第二个是对下采样部分进行改进,简单而言就是用了pointwise convolution和depthwise convolution进行下采样,替代了kernel为3x3且stride为2的卷积。假设对图像大小为 ( H × W × C ) (H\times W\times C) (H×W×C)的图像进行下采样得到 ( H 2 × W 2 × C 2 ) (\frac{H}{2}\times \frac{W}{2}\times C^2) (2H×2W×C2),在不替换前需要的计算量和参数量分别为 O ( 18 H W C 2 4 ) = O ( 9 H W C 2 2 ) O(\frac{18HWC^2}{4})=O(\frac{9HWC^2}{2}) O(418HWC2)=O(29HWC2) O ( 9 ∗ 2 C 2 ) = O ( 18 C 2 ) O(9*2C^2)=O(18C^2) O(92C2)=O(18C2)

使用pointwise convolution的将其 ( H × W × C ) → ( H × W × 2 C ) (H\times W\times C)\rightarrow(H\times W\times 2C) (H×W×C)(H×W×2C),计算量和参数分别为 O ( 2 H W C 2 ) O(2HWC^2) O(2HWC2) O ( 2 C 2 ) O(2C^2) O(2C2),然后使用3x3的depthwise convolution将其 ( H × W × 2 C ) → ( H 2 × W 2 × 2 C ) (H\times W\times 2C)\rightarrow(\frac{H}{2}\times \frac{W}{2}\times 2C) (H×W×2C)(2H×2W×2C),计算量和参数量分别为 O ( 9 W H ∗ 2 C 4 ) = O ( 9 W H C 2 ) O(\frac{9WH*2C}{4}) =O(\frac{9WHC}{2}) O49WH2C)=O(29WHC) O ( 18 C ) O(18C) O(18C)。所以总的计算量和参数量为 O ( 2 H W C 2 + 9 W H C 2 ) O(2HWC^2+\frac{9WHC}{2}) O(2HWC2+29WHC) O ( 2 C 2 + 18 C ) O(2C^2+18C) O(2C2+18C)

总的而言这个方式可以使计算量从 O ( 9 H W C 2 2 ) O(\frac{9HWC^2}{2}) O(29HWC2)降至 O ( 2 H W C 2 + 9 W H C 2 ) O(2HWC^2+\frac{9WHC}{2}) O(2HWC2+29WHC),参数量从 O ( 18 C 2 ) O(18C^2) O(18C2)降至 O ( 2 C 2 + 18 C ) O(2C^2+18C) O(2C2+18C)

Rank-guided block design

第三部分是basic building block。作者认为模型在重复使用同个模块拼接后,会带来较大的信息冗余。所以作者对yolov8各个大小的模型的每个阶段的最后一个卷积进行秩的分析,通过下图可以发现模型较大且较深时,秩相对较小。说明了大模型存在较多的信息冗余,也就是说有用的信息不多。所以作者提出了CIB模块,CIB模块由3个3x3的DW卷积和2个1x1的卷积组成,下图展示了将CIB模块嵌入ELAN的方法。作者希望通过CIB模块降低模型的复杂度,同时保证模型的ap值。
在这里插入图片描述
具体的操作方式,就是先训练一个模型,然后对模型的每个阶段进行分析,选取秩最小的那个阶段,插入CIB模块,判断是否能带来AP增益,是的话则继续替换(替换过的不在参加),不然就直接输出。具体的流程如下
在这里插入图片描述
这个方法可以帮助模型降低参数量,同时保持模型的精度。这个思想是值得借鉴的,在许多大参数量的模型中,借助这种方法可以实现参数量的下降。

Accuracy driven model design

作者从大卷积核和self-attention俩个部分提高模型的准确率。

第一部分,作者将CIB模块中第二个 3 × 3 3\times3 3×3的卷积核换成了7x7,并采用了重参数化的方式,代码部分如下

class RepVGGDW(torch.nn.Module):
    def __init__(self, ed) -> None:
        super().__init__()
        self.conv = Conv(ed, ed, 7, 1, 3, g=ed, act=False)
        self.conv1 = Conv(ed, ed, 3, 1, 1, g=ed, act=False)
        self.dim = ed
        self.act = nn.SiLU()
    
    def forward(self, x):
        return self.act(self.conv(x) + self.conv1(x))
    
    def forward_fuse(self, x):
        return self.act(self.conv(x))

    @torch.no_grad()
    def fuse(self):
        conv = fuse_conv_and_bn(self.conv.conv, self.conv.bn)
        conv1 = fuse_conv_and_bn(self.conv1.conv, self.conv1.bn)
        
        conv_w = conv.weight
        conv_b = conv.bias
        conv1_w = conv1.weight
        conv1_b = conv1.bias
        
        conv1_w = torch.nn.functional.pad(conv1_w, [2,2,2,2])

        final_conv_w = conv_w + conv1_w
        final_conv_b = conv_b + conv1_b

        conv.weight.data.copy_(final_conv_w)
        conv.bias.data.copy_(final_conv_b)

        self.conv = conv
        del self.conv1

class CIB(nn.Module):
    """Standard bottleneck."""

    def __init__(self, c1, c2, shortcut=True, e=0.5, lk=False):
        """Initializes a bottleneck module with given input/output channels, shortcut option, group, kernels, and
        expansion.
        """
        super().__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = nn.Sequential(
            Conv(c1, c1, 3, g=c1),
            Conv(c1, 2 * c_, 1),
            #lk为true时使用大卷积核
            Conv(2 * c_, 2 * c_, 3, g=2 * c_) if not lk else RepVGGDW(2 * c_),
            Conv(2 * c_, c2, 1),
            Conv(c2, c2, 3, g=c2),
        )

        self.add = shortcut and c1 == c2

    def forward(self, x):
        """'forward()' applies the YOLO FPN to input data."""
        return x + self.cv1(x) if self.add else self.cv1(x)

同时作者主要将其应用小模型上,因为大模型的感受野比较大,卷积核的作用就会比较小。

第二部分是Partial self-attention,其模型结构如下
在这里插入图片描述
从上述结构图可以看出,其采用了于CSP类似的方法,将输入的 x x x 1 × 1 1\times 1 1×1的卷积后分成俩个部分(有的是直接用1x1的卷积实现)。然后将其中的一部分直接输出至最后,另一部分输入 N p s a N_{psa} Npsa模块,并将query和key的channel砍半,显而易见这种方式是能够降低参数量。其他就是正常的self-attention和csp部分了。这部分的代码如下

class Attention(nn.Module):
    def __init__(self, dim, num_heads=8,
                 attn_ratio=0.5):
        super().__init__()
        self.num_heads = num_heads
        self.head_dim = dim // num_heads
        self.key_dim = int(self.head_dim * attn_ratio)
        self.scale = self.key_dim ** -0.5
        nh_kd = nh_kd = self.key_dim * num_heads
        #原本是3*dim,现在变成了2*dim
        h = dim + nh_kd * 2
        self.qkv = Conv(dim, h, 1, act=False)
        self.proj = Conv(dim, dim, 1, act=False)
        self.pe = Conv(dim, dim, 3, 1, g=dim, act=False)

    def forward(self, x):
        B, C, H, W = x.shape
        N = H * W
        qkv = self.qkv(x)
        q, k, v = qkv.view(B, self.num_heads, self.key_dim*2 + self.head_dim, N).split([self.key_dim, self.key_dim, self.head_dim], dim=2)

        attn = (
            (q.transpose(-2, -1) @ k) * self.scale
        )
        attn = attn.softmax(dim=-1)
        x = (v @ attn.transpose(-2, -1)).view(B, C, H, W) + self.pe(v.reshape(B, C, H, W))
        x = self.proj(x)
        return x

class PSA(nn.Module):

    def __init__(self, c1, c2, e=0.5):
        super().__init__()
        assert(c1 == c2)
        self.c = int(c1 * e)
        self.cv1 = Conv(c1, 2 * self.c, 1, 1)
        self.cv2 = Conv(2 * self.c, c1, 1)
        
        self.attn = Attention(self.c, attn_ratio=0.5, num_heads=self.c // 64)
        self.ffn = nn.Sequential(
            Conv(self.c, self.c*2, 1),
            Conv(self.c*2, self.c, 1, act=False)
        )
        
    def forward(self, x):
        a, b = self.cv1(x).split((self.c, self.c), dim=1)
        b = b + self.attn(b)
        b = b + self.ffn(b)
        return self.cv2(torch.cat((a, b), 1))

作者只将这部分用于低分辨率的部分,由于低分辨率的长度 ( H × W ) (H\times W) (H×W)会比较短,所以这样能够使这块的复杂度不会太高。

Conclusion

yolov10借助了许多轻量化卷积的方法嵌入到模型中,降低模型的参数量和计算量。同时提出了dual assigment的方法用于模型训练,从而实现了NMS-free。个人认为yolov10的意义主要在于其在NMS上的改进。NMS是目标检测模型很难避开的一个算法,当然以前也有许多NMS-free的模型,但是都会面对巨大的计算量或者训练不收敛等问题,

  • 35
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值