目标检测算法——YOLOV11——算法详解

目标检测算法——YOLOV11——算法详解

关键词:YOLO V11、目标检测、算法、解读、详解、教程、结构图、分析

一、主要贡献

    其实到了YOLOV5 基本创新点就不太多了,主要就是大家互相排列组合复用不同的网络模块、损失函数和样本匹配策略,需要注意YOLO V5、V8 V11 都是1个公司的,其余的个人建议看看V6美团的,剩下的了解就好。

    V11支持多种视觉任务:物体检测、实例分割、图像分类、姿态估计和定向物体检测(OBB)。

    Yolo v11 基本和YOLOV8同源,甚至git目前都是1个,部分代码注释还是YOLOV8的,所以建议先看我写的YOLOV8相关博客,对比YOLOV8主要涉及到:

    *backbone 中的使用C2f模块 变为 c3k2 模块。

    *backbone 中的最后一层(sppf层)后增加了C2PSA模块。

    *head 解耦头中的分类检测头两个Conv 变为 DWConv。

    整体技术而言:

    *backbone 使用了C2K2模块+最后SPPF模块级联C2PSA模块;

    *neck 使用PAN结构,并且里面也使用C3K2模块;

    *head使用了anchor-free + Decoupled-head,其中回归头使用正常的卷积,分类头使用DWConv;

    *损失函数使用了分类BCE、回归CIOU + VFL的组合;

    *框匹配策略由静态匹配改为了Task-Aligned Assigner匹配方式;

    *训练策略没有提及,其中YOLOV8可以参考如下最后 10 个 epoch 关闭 Mosaic 的操作、训练总 epoch 数从 300 提升到了 500。

    PS

    这篇有部分内容来自我的YOLOV8部分,目的是避免读者来回翻浪费时间。也建议大家先学习一下YOLOV8,感受技术进步(狗头)。

    这篇主要讲检测部分,分类、分割部分后续再看需求编写。

    目前241009看网上的博客,网络结构还没有画对的,尤其一系列付费博客画得离谱啊(还收费一百多块教你改进,这就。。。)

二、主要思路

    整体的检测算法框架图如下(对应的是型号n,摘自cdsn-一勺汤,后面是自己画,细节大多数都画错了。。。。)。算法版本20241011 Tag 分支8.3.8同期的main分支(应该是8.3.0)。需要注意配合后面的配置文件来看,这里不同位置的C3K2参数是不一样的。

    PS:这样的整体图怎么来的,是根据模型配置文件(左边是V8的,右边是V11的)+解析函数(来自nn文件夹下的task.py 的 parse_model 函数)来构建模型。只需要这个配置文件,就能梳理画出上面的整体框图。

    配置文件:ultralytics/ultralytics/cfg/models/11/yolo11.yaml at main · ultralytics/ultralytics · GitHub

    解析函数:ultralytics/ultralytics/nn/tasks.py at main · ultralytics/ultralytics · GitHub

三、具体细节

1、input

    输入要求以及预处理,可选项比较多,可以参考这个配置文件:ultralytics/ultralytics/cfg/default.yaml at main · ultralytics/ultralytics · GitHub 的Hyperparameters 部分。

    基础输入仍然为640*640。预处理就是熟悉的letterbox(根据参数配置可以为不同的缩放填充模式,主要用于resize到640)+ 转换rgb、chw、int8(0-255)->float(0-1),注意没有归一化操作。需要注意的是作者实现的mosaic和网上看到的不同,对比如下图(左边网上版本,右边是YOLO的实现)。并且作者添加了在最后10轮关闭mosaic增强(YOLOV8开始支持,具体原因个人的经验如我的这篇文章:yolov5 mosaic相关,关闭参数在 Train settings 部分的close_mosaic 选项)

    

 

2、backbone

    主干网络以及改进

    这里不去特意强调对比YOLOv5、V8等等的改进,因为各个系列都在疯狂演进,个人认为没必要花费时间看差异,着重看看一些比较重要的模块即可。源代码:

    大多数模块:ultralytics/ultralytics/nn/modules/block.py at main · ultralytics/ultralytics · GitHub 

    head 部分:ultralytics/ultralytics/nn/modules/head.py at main · ultralytics/ultralytics · GitHub

    串联模块构造网络:ultralytics/ultralytics/nn/tasks.py at main · ultralytics/ultralytics · GitHub 

    1)CBS 模块(后面叫做Conv)

    就是pytorch 自带的conv + BN +SiLU,这里对应上面的配置文件的Conv 的 args 比如[64, 3, 2] 就是 conv2d 的c2=64、k=3、 s =2、c1 自动为上一层参数、p 为自动计算,真实需要计算scales 里面的with 和 max_channels 缩放系数。

    这里连续使用两个3*3卷积stride为2的CBS模块直接横竖各降低了4倍分辨率(整体变为原来1/16)。这个还是比较猛的,敢在如此小的感受野下连续两次仅仅用一层卷积就下采样,当然作为代价它的特征图还是比较厚的分别为16、32。    

 class Conv(nn.Module): """Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation).""" default_act = nn.SiLU() # default activation def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True): """Initialize Conv layer with given arguments including activation.""" super().__init__() self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False) self.bn = nn.BatchNorm2d(c2) self.act = self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity() def forward(self, x): """Apply convolution, batch normalization and activation to input tensor.""" return self.act(self.bn(self.conv(x))) def forward_fuse(self, x): """Perform transposed convolution of 2D data.""" return self.act(self.conv(x))

    2)c3k2 模块

    Bottleneck

    有两种结构,需要参数shortcut和两个conv的宽度是否相同来控制。

    C3 & C3K

    都是CSP bottleneck module with 3 convolutions, C3 代表3个卷积层, K代表其中bottleneck中的卷积核为支持自定义,其实这里c3k作者使用的默认的3*3卷积核也就等同于使用c3(c3是3*3卷积核)。

    c2f & c3k2 

    其实也就是仿照YOLOv7 的ELAN 结构,通过更多的分支夸层链接,丰富了模型的梯度流。C3K2模块其实就是C2F模块转变出来的,它代码中有一个设置,就是当c3k这个参数为FALSE的时候,C3K2模块就是C2F模块,也就是说它的Bottleneck是普通的Bottleneck;反之当它为true的时候,将Bottleneck模块替换成C3K模块。模块中存在 Split 等操作对特定硬件部署没有之前那么友好了。需要针对自己的硬件进行测试看对最终推理速度的影响。

    可视化关系如下,这里需要注意配置文件中的参数,比如21行[-1, 2, C3k2, [512, False, 0.25]] 512代表宽度、false代表是否使用shortcut、0.25代表c2f的宽度缩放。也就是第一个Conv的输出宽度。    

    源代码如下: 

class Bottleneck(nn.Module): """Standard bottleneck.""" def __init__(self, c1, c2, shortcut=True, g=1, k=(3, 3), e=0.5): """Initializes a standard bottleneck module with optional shortcut connection and configurable parameters.""" super().__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, k[0], 1) self.cv2 = Conv(c_, c2, k[1], 1, g=g) self.add = shortcut and c1 == c2 def forward(self, x): """Applies the YOLO FPN to input data.""" return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x)) class C3(nn.Module): """CSP Bottleneck with 3 convolutions.""" def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): """Initialize the CSP Bottleneck with given channels, number, shortcut, groups, and expansion values.""" super().__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c1, c_, 1, 1) self.cv3 = Conv(2 * c_, c2, 1) # optional act=FReLU(c2) self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, k=((1, 1), (3, 3)), e=1.0) for _ in range(n))) def forward(self, x): """Forward pass through the CSP bottleneck with 2 convolutions.""" return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), 1)) class C3k(C3): """C3k is a CSP bottleneck module with customizable kernel sizes for feature extraction in neural networks.""" def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5, k=3): """Initializes the C3k module with specified channels, number of layers, and configurations.""" super().__init__(c1, c2, n, shortcut, g, e) c_ = int(c2 * e) # hidden channels # self.m = nn.Sequential(*(RepBottleneck(c_, c_, shortcut, g, k=(k, k), e=1.0) for _ in range(n))) self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, k=(k, k), e=1.0) for _ in range(n))) class C2f(nn.Module): """Faster Implementation of CSP Bottleneck with 2 convolutions.""" def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5): """Initializes a CSP bottleneck with 2 convolutions and n Bottleneck blocks for faster processing.""" super().__init__() self.c = int(c2 * e) # hidden channels self.cv1 = Conv(c1, 2 * self.c, 1, 1) self.cv2 = Conv((2 + n) * self.c, c2, 1) # optional act=FReLU(c2) self.m = nn.ModuleList(Bottleneck(self.c, self.c, shortcut, g, k=((3, 3), (3, 3)), e=1.0) for _ in range(n)) def forward(self, x): """Forward pass through C2f layer.""" y = list(self.cv1(x).chunk(2, 1)) y.extend(m(y[-1]) for m in self.m) return self.cv2(torch.cat(y, 1)) def forward_split(self, x): """Forward pass using split() instead of chunk().""" y = list(self.cv1(x).split((self.c, self.c), 1)) y.extend(m(y[-1]) for m in self.m) return self.cv2(torch.cat(y, 1)) class C3k2(C2f): """Faster Implementation of CSP Bottleneck with 2 convolutions.""" def __init__(self, c1, c2, n=1, c3k=False, e=0.5, g=1, shortcut=True): """Initializes the C3k2 module, a faster CSP Bottleneck with 2 convolutions and optional C3k blocks.""" super().__init__(c1, c2, n, shortcut, g, e) self.m = nn.ModuleList( C3k(self.c, self.c, 2, shortcut, g) if c3k else Bottleneck(self.c, self.c, shortcut, g) for _ in range(n) )

    3)sppf 模块

    对比spp,将简单的并行max pooling 改为串行+并行的方式。对比如下(左边是SPP,右边是SPPF):    

 class SPPF(nn.Module): # Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher def __init__(self, c1, c2, k=5): # equivalent to SPP(k=(5, 9, 13)) super().__init__() c_ = c1 // 2 # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c_ * 4, c2, 1, 1) self.m = nn.MaxPool2d(kernel_size=k, stride=1, padding=k // 2) def forward(self, x): x = self.cv1(x) with warnings.catch_warnings(): warnings.simplefilter('ignore') # suppress torch 1.9.0 max_pool2d() warning y1 = self.m(x) y2 = self.m(y1) return self.cv2(torch.cat((x, y1, y2, self.m(y2)), 1))

    4)C2PSA 模块

    C2PSA它结合了PSA(Pointwise Spatial Attention)块,用于增强特征提取和注意力机制。下面的图建议从左到右看,这样才能更有条理的理解,其实PSA个人感觉就是仿着VIT 的Attention来做的,是把输入C2PSA的特征图的h*w 看做VIT 的path数(也可以理解为NLP中token 个数),特征图的channel 数看做VIT特征维度(CNN的宽度,或者理解为NLP中token 编码后的特征维度),然后计算出QKV(这里需要注意第四幅图的QKV是值,不是操作,所以标注成了圆角矩形,这里是为了大家好理解),这里的Attention其实是在h*w维度计算空间Attention,个人感觉是强制给了全局感受野,并且并联了一个3*3的深度可分离卷积的单空间部分,就是仅在每一个特征图上进行3*3卷积,具体实现是通过pytorch conv2d 的 group参数设置为特征图的通道数。特别的关于Conv的参数分别为:输入通道数、输出通道数、卷积核尺寸、pad尺寸、group数、是否有激活函数(默认silu)。图中的最后一幅省略了一些细节,可以参考源码。

    注意区别C2fPSA,C2fPSA才是对 C2f 模块的扩展,通过在标准 C2f 模块中引入 PSA 块,C2fPSA实现了更强大的注意力机制,从而提高了模型对重要特征的捕捉能力。作者实现了该模块但最终没有使用。

涉及的源码:

class Attention(nn.Module): """ Attention module that performs self-attention on the input tensor. Args: dim (int): The input tensor dimension. num_heads (int): The number of attention heads. attn_ratio (float): The ratio of the attention key dimension to the head dimension. Attributes: num_heads (int): The number of attention heads. head_dim (int): The dimension of each attention head. key_dim (int): The dimension of the attention key. scale (float): The scaling factor for the attention scores. qkv (Conv): Convolutional layer for computing the query, key, and value. proj (Conv): Convolutional layer for projecting the attended values. pe (Conv): Convolutional layer for positional encoding. """ def __init__(self, dim, num_heads=8, attn_ratio=0.5): """Initializes multi-head attention module with query, key, and value convolutions and positional encoding.""" 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 = self.key_dim * num_heads 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): """ Forward pass of the Attention module. Args: x (torch.Tensor): The input tensor. Returns: (torch.Tensor): The output tensor after self-attention. """ 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 PSABlock(nn.Module): """ PSABlock class implementing a Position-Sensitive Attention block for neural networks. This class encapsulates the functionality for applying multi-head attention and feed-forward neural network layers with optional shortcut connections. Attributes: attn (Attention): Multi-head attention module. ffn (nn.Sequential): Feed-forward neural network module. add (bool): Flag indicating whether to add shortcut connections. Methods: forward: Performs a forward pass through the PSABlock, applying attention and feed-forward layers. Examples: Create a PSABlock and perform a forward pass >>> psablock = PSABlock(c=128, attn_ratio=0.5, num_heads=4, shortcut=True) >>> input_tensor = torch.randn(1, 128, 32, 32) >>> output_tensor = psablock(input_tensor) """ def __init__(self, c, attn_ratio=0.5, num_heads=4, shortcut=True) -> None: """Initializes the PSABlock with attention and feed-forward layers for enhanced feature extraction.""" super().__init__() self.attn = Attention(c, attn_ratio=attn_ratio, num_heads=num_heads) self.ffn = nn.Sequential(Conv(c, c * 2, 1), Conv(c * 2, c, 1, act=False)) self.add = shortcut def forward(self, x): """Executes a forward pass through PSABlock, applying attention and feed-forward layers to the input tensor.""" x = x + self.attn(x) if self.add else self.attn(x) x = x + self.ffn(x) if self.add else self.ffn(x) return x class C2PSA(nn.Module): """ C2PSA module with attention mechanism for enhanced feature extraction and processing. This module implements a convolutional block with attention mechanisms to enhance feature extraction and processing capabilities. It includes a series of PSABlock modules for self-attention and feed-forward operations. Attributes: c (int): Number of hidden channels. cv1 (Conv): 1x1 convolution layer to reduce the number of input channels to 2*c. cv2 (Conv): 1x1 convolution layer to reduce the number of output channels to c. m (nn.Sequential): Sequential container of PSABlock modules for attention and feed-forward operations. Methods: forward: Performs a forward pass through the C2PSA module, applying attention and feed-forward operations. Notes: This module essentially is the same as PSA module, but refactored to allow stacking more PSABlock modules. Examples: >>> c2psa = C2PSA(c1=256, c2=256, n=3, e=0.5) >>> input_tensor = torch.randn(1, 256, 64, 64) >>> output_tensor = c2psa(input_tensor) """ def __init__(self, c1, c2, n=1, e=0.5): """Initializes the C2PSA module with specified input/output channels, number of layers, and expansion ratio.""" 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.m = nn.Sequential(*(PSABlock(self.c, attn_ratio=0.5, num_heads=self.c // 64) for _ in range(n))) def forward(self, x): """Processes the input tensor 'x' through a series of PSA blocks and returns the transformed tensor.""" a, b = self.cv1(x).split((self.c, self.c), dim=1) b = self.m(b) return self.cv2(torch.cat((a, b), 1))

3、neck & head

    1)检测头

    YOLOV11 Head 部分和YOLOV8是近似的,所以简单对比YOLOV5、V8、V11。

    如上面图,上边是YOLOV5 的结构,中是YOLOv8 的结构,下面是YOLOV11 结构

    Yolov5: 检测和分类共用一个卷积(coupled head)并且是anchor based ,其 卷积输出为(5+N class)*3,其中 5为bbox 四个值(具体代表什么不同版本略有不同,官方git有说明,历史版本见 目标检测算法——YOLOV5 )+ 一个obj 值 (是否有目标,这个是从YOLO V1 传承下来的,个人感觉有点绕和不合理,并且后面取消),N class 为类别数,3为anchor 的数量,默认是3个。

    YOLOv8:检测和分类的卷积是解耦的(decoupled),如中图,上面一条卷积支路是回归框,框的特征图channel为4*regmax,关于这个regmax 后面我们详细的解释,并不是anchor;分类的channel 为类别数。

    YOLOV11:检测和分类的卷积是解耦的(decoupled),如右图,上面一条卷积支路是回归框,框的特征图channel为4*regmax,关于这个regmax 后面我们详细的解释,并不是anchor;分类的channel 为类别数,分类使用深度可分离卷积替代常规卷积降低计算量。

    源码部分如下

class Detect(nn.Module): """YOLO Detect head for detection models.""" dynamic = False # force grid reconstruction export = False # export mode end2end = False # end2end max_det = 300 # max_det shape = None anchors = torch.empty(0) # init strides = torch.empty(0) # init def __init__(self, nc=80, ch=()): """Initializes the YOLO detection layer with specified number of classes and channels.""" super().__init__() self.nc = nc # number of classes self.nl = len(ch) # number of detection layers self.reg_max = 16 # DFL channels (ch[0] // 16 to scale 4/8/12/16/20 for n/s/m/l/x) self.no = nc + self.reg_max * 4 # number of outputs per anchor self.stride = torch.zeros(self.nl) # strides computed during build c2, c3 = max((16, ch[0] // 4, self.reg_max * 4)), max(ch[0], min(self.nc, 100)) # channels self.cv2 = nn.ModuleList( nn.Sequential(Conv(x, c2, 3), Conv(c2, c2, 3), nn.Conv2d(c2, 4 * self.reg_max, 1)) for x in ch ) self.cv3 = nn.ModuleList( nn.Sequential( nn.Sequential(DWConv(x, x, 3), Conv(x, c3, 1)), nn.Sequential(DWConv(c3, c3, 3), Conv(c3, c3, 1)), nn.Conv2d(c3, self.nc, 1), ) for x in ch ) self.dfl = DFL(self.reg_max) if self.reg_max > 1 else nn.Identity() if self.end2end: self.one2one_cv2 = copy.deepcopy(self.cv2) self.one2one_cv3 = copy.deepcopy(self.cv3) def forward(self, x): """Concatenates and returns predicted bounding boxes and class probabilities.""" if self.end2end: return self.forward_end2end(x) for i in range(self.nl): x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1) if self.training: # Training path return x y = self._inference(x) return y if self.export else (y, x)

    因此主要的变化可以认为有三个:(1)coupled head -> decoupled head ;(2)obj 分支消失;(3)anchor based——> anchor free ; 4) 深度可分离卷积。

    (1)coupled head -> decoupled head 

    这个解耦操作,看YOLO x 的论文,约有1% 的提升。逻辑和实现都比较直观易懂,不再赘述。

    (2)obj 分支消失;

        这个其实我自己再看YOLO V1 的时候就有疑问,它存在的意义。后来人们发现,其实obj 的在训练和推理过程中存在逻辑不一致性。具体而言(摘自“https://zhuanlan.zhihu.com/p/147691786”)

    A。用法不一致。训练的时候,分类和质量估计各自训练自个儿的,但测试的时候却又是乘在一起作为NMS score排序的依据,这个操作显然没有end-to-end,必然存在一定的gap。(个人认为还好,就是两个监督信号)

    B。对象不一致。借助Focal Loss的力量,分类分支能够使得少量的正样本和大量的负样本一起成功训练,但是质量估计通常就只针对正样本训练。那么,对于one-stage的检测器而言,在做NMS score排序的时候,所有的样本都会将分类score和质量预测score相乘用于排序,那么必然会存在一部分分数较低的“负样本”的质量预测是没有在训练过程中有监督信号的,对于大量可能的负样本,他们的质量预测是一个未定义行为。这就很有可能引发这么一个情况:一个分类score相对低的真正的负样本,由于预测了一个不可信的极高的质量score,而导致它可能排到一个真正的正样本(分类score不够高且质量score相对低)的前面。问题一如图所示:  

    (3)anchor based——> anchor free

    这里主要涉及怎么定义回归内容以及如何匹配GT框的问题。也就是如下:

 2)匹配策略

    A。回归的内容当前版本就是回归的lftp四个值(这四个值是距离匹配到的anchor 点的距离值!不是图片的绝对位置)。后面推理阶段通过 dist2bbox函数转换为需要的格式:

    https://github.com/ultralytics/ultralytics/blob/cc3c774bde86ffce694d202b7383da6cc1721c1b/ultralytics/nn/modules.py#L378

    https://github.com/ultralytics/ultralytics/blob/cc3c774bde86ffce694d202b7383da6cc1721c1b/ultralytics/yolo/utils/tal.py#L196。 

 def dist2bbox(distance, anchor_points, xywh=True, dim=-1): """Transform distance(ltrb) to box(xywh or xyxy).""" lt, rb = torch.split(distance, 2, dim) x1y1 = anchor_points - lt x2y2 = anchor_points + rb if xywh: c_xy = (x1y1 + x2y2) / 2 wh = x2y2 - x1y1 return torch.cat((c_xy, wh), dim) # xywh bbox return torch.cat((x1y1, x2y2), dim) # xyxy bbox

    B.匹配策略

    YOLOv5 采用静态的匹配策略,V8采用了动态的TaskAlignedAssigner,其余常见的动态匹配还有: YOLOX 的 simOTA、TOOD 的 TaskAlignedAssigner 和 RTMDet 的 DynamicSoftLabelAssigner。

    TaskAligned使用分类得分和IoU的高阶组合来衡量Task-Alignment的程度。使用上面公式来对每个实例计算Anchor-level 的对齐程度:s 和 u 分别为分类得分和 IoU 值,α 和 β 为权重超参。t 可以同时控制分类得分和IoU 的优化来实现 Task-Alignment,可以引导网络动态的关注于高质量的Anchor。采用一种简单的分配规则选择训练样本:对每个实例,选择m个具有最大t值的Anchor作为正样本,选择其余的Anchor作为负样本。然后,通过损失函数(针对分类与定位的对齐而设计的损失函数)进行训练。

   代码地址:ultralytics/ultralytics/yolo/utils/tal.py at c0c0c138c12699807ff9446f942cb3bd325d670b · ultralytics/ultralytics · GitHub

    默认参数如下(当前版本这些超参没有提供修改的接口,如需修改需要在源码上进行修改):    

4、loss function

    损失函数设计

    Loss 计算包括 2 个分支: 分类和回归分支,没有了之前的 objectness 分支。

    分类分支依然采用 BCE Loss。回归分支使用了 Distribution Focal Loss(DFL Reg_max默认为16)+ CIoU Loss。3 个 Loss 采用一定权重比例加权即可(默认如下:https://github.com/ultralytics/ultralytics/blob/main/ultralytics/yolo/configs/default.yaml#L83)。

    

    这里重点介绍一下DFL损失。目前被广泛使用的bbox表示可以看作是对bbox方框坐标建模了单一的狄拉克分布。但是在复杂场景中,一些检测对象的边界并非十分明确。如下图左面所示,对于滑板左侧被水花模糊,引起对左边界的预测分布是任意而扁平的,对右边界的预测分布是明确而尖锐的。对于这个问题,有学者提出直接回归一个任意分布来建模边界框,使用softmax实现离散的回归,将狄拉克分布的积分形式推导到一般形式的积分形式来表示边界框。

    狄拉克分布可以认为在一个点概率密度为无穷大,其他点概率密度为0,这是一种极端地认为离散的标签时绝对正确的。

    因为标签是一个离散的点,如果把标签认为是绝对正确的目标,那么学习出的就是狄拉克分布,概率密度是一条尖锐的竖线。然而真实场景,物体边界并非是十分明确的,因此学习一个宽范围的分布更为合理。我们需要获得的分布虽然不再像狄拉克分布那么极端(只存在标签值),但也应该在标签值附近。因此学者提出Distribution Focal Loss损失函数,目的让网络快速聚焦到标签附近的数值,是标签处的概率密度尽量大。思想是使用交叉熵函数,来优化标签y附近左右两个位置的概率,是网络分布聚焦到标签值附近。如下公式。Si 是网络的sigmod 输出(因为真是是多分类,所以是softmax),yi 和 yi+1 是上图的区间顺序,y是label 值。

    具体而言,针对我们将DFL的超参数Reg_max 设置为16的情况下:

    A。训练阶段:我们以回归left为例:目标的label 转换为ltrb后,y = ( left - 匹配到的anchor 中心点 x 坐标)/ 当前的下采样倍数,假设求得3.2。那么i 就应该为3,yi = 3 ,yi+1 = 4。

    B。推理阶段:因为没有label,直接将16个格子进行积分(离散变量为求和,也就是期望)结果就是最终的坐标偏移量(再乘以下采样倍数+ 匹配到的anchor的对应坐标)

    

    DFL的实现方式其实就是一个卷积:ultralytics/ultralytics/nn/modules.py at cc3c774bde86ffce694d202b7383da6cc1721c1b · ultralytics/ultralytics · GitHub

    

    NOTE:作者代码中的超参数Reg_max是写死的——16,并且代码内部做了强制截断到16,如果要修改需要修改源码,如果你的输入是640,最大下采样到20*20,那么16是够用的,如果输入没有resize或者超过了640一定要自己设置这个Reg_max参数,否则如果目标尺寸还大,将无法拟合到这个偏移量。 比如1280*1280的图片,目标1280*960,最大下采样32倍,1280/32/2=20 > 16(除以2是因为是一半的偏移量),超过了dfl 滑板右侧那个图的范围。至于为什么叫focal loss的变体,有兴趣看一下这个https://zhuanlan.zhihu.com/p/357415257https://zhuanlan.zhihu.com/p/147691786就可以,这里不再赘述是因为,如果先看这些,很容易犯晕,反而抓不住DFL 我认为的重点(离散的分布形式)

 class DFL(nn.Module): # Integral module of Distribution Focal Loss (DFL) proposed in Generalized Focal Loss https://ieeexplore.ieee.org/document/9792391 def __init__(self, c1=16): super().__init__() self.conv = nn.Conv2d(c1, 1, 1, bias=False).requires_grad_(False) x = torch.arange(c1, dtype=torch.float) self.conv.weight.data[:] = nn.Parameter(x.view(1, c1, 1, 1)) self.c1 = c1 def forward(self, x): b, c, a = x.shape # batch, channels, anchors return self.conv(x.view(b, 4, self.c1, a).transpose(2, 1).softmax(1)).view(b, 4, a) # return self.conv(x.view(b, self.c1, 4, a).softmax(1)).view(b, 4, a)

    PS

    这里说一下整体loss是怎么串联的,不然确实有点绕。这里说的是训练过程!!!!

    首先明确网络的输出是3个level分别预测大中小目标(可以粗暴这么理解),然后每个level的检测头有两个分支,分类的输出维度为类别数nc,检测的输出是4(ltrb)* 16 (dfl的reg max数),所以接下来的一切都是以这个为基础操作的。

    先通过DFL 推理过程(其实就是上面的卷积实现的求期望),获得ltrb的真实值,然后转化为bbox的预测值,然后结合分类输出使用上述TaskAlignedAssigner匹配标注框和检测框。

    然后计算对应的分类bce loss,DFL loss,此时注意IOU loss计算是通过DFL 的期望转化来的坐标计算的,并没有直接的网络输出值来回归!!!!

5、trics 

    单独训练trick或者重点重复概述上述所有trick

    C3k2模块、C2PSA模块、Decoupled-Head、Anchor-Free、BCE Loss作为分类损失 VFL Loss + CIOU Loss作为回归损失、Task-Aligned Assigner匹配方式、最后 10 个 epoch 关闭 Mosaic 的操作。

6、inference

测试阶段(非训练阶段)过程,这个过程只涉及分类、DFL(期望求和过程)外加NMS,没有iou loss、task align过程。

可以参考:YOLOv8 深度详解!一文看懂,快速上手本文详细分析和总结了最新的 YOLOv8 算法,从整体设计到模型结构、Loss - 掘金 主要就是多了DFL的积分/求和/解码(其实上面解释过就是一个卷积操作)过程。

四、结果

算法结果

打赏

你的打赏是我不断分享的动力,羞羞。点这里,嘿嘿。

参考链接

YOLOv11训练自己的数据集,YOLOv11网络解析_yolov11 github-ZEEKLOG博客

YOLO11 沉浸式讲解 YOLOV11网络结构以及代码剖析-ZEEKLOG博客

YOLOv11 | 一文带你深入理解ultralytics最新作品yolov11的创新 | 训练、推理、验证、导出 (附网络结构图)-ZEEKLOG博客

YOLOv11训练自己的数据集,YOLOv11网络解析_yolov11 github-ZEEKLOG博客

https://i-blog.ZEEKLOGimg.cn/direct/3d97b14552dd4bd8a5859ac46e80d372.png

Read more

Flutter 组件 flutterw_sidekick_plugin 适配鸿蒙 HarmonyOS 实战:侧翼脚手架扩展,构建工程自动化与环境一致性治理架构

Flutter 组件 flutterw_sidekick_plugin 适配鸿蒙 HarmonyOS 实战:侧翼脚手架扩展,构建工程自动化与环境一致性治理架构

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 flutterw_sidekick_plugin 适配鸿蒙 HarmonyOS 实战:侧翼脚手架扩展,构建工程自动化与环境一致性治理架构 前言 在鸿蒙(OpenHarmony)生态迈向大规模团队协作、涉及多分支并行开发及复杂的 SDK 版本管控的背景下,如何确保每一位开发者的本地构建环境(Flutter/Dart SDK)与生产基准完全对齐,已成为保障项目交付质量的“工程定海神针”。在鸿蒙设备这类强调定制化编译工具链与私有插件依赖的环境下,如果团队缺乏统一的脚手架工具,由于由于本地 SDK 版本的微小代差(如空安全检测差异),极易由于由于“环境不一致”导致代码在不同机器上产生不可预知的编译崩溃。 我们需要一种能够深度集成 Sidekick、支持自定义命令扩展且具备“强制版本锁死”能力的脚手架治理方案。 flutterw_sidekick_plugin 为 Flutter 开发者引入了基于 Sidekick

By Ne0inhk
Flutter 组件 freezed_collection 的鸿蒙化适配实战 - 驾驭极致集合不可变性大坝、构建 OpenHarmony 分布式端高性能、防篡改、类型安全的数据阵列方案

Flutter 组件 freezed_collection 的鸿蒙化适配实战 - 驾驭极致集合不可变性大坝、构建 OpenHarmony 分布式端高性能、防篡改、类型安全的数据阵列方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 freezed_collection 的鸿蒙化适配实战 - 驾驭极致集合不可变性大坝、构建 OpenHarmony 分布式端高性能、防篡改、类型安全的数据阵列方案 前言 在鸿蒙(OpenHarmony)生态的工业级交付、重型金融结算以及对业务逻辑零缺陷容忍的跨端政务系统中。“集合数据的不可变性与深层防篡改维度”是衡量整个系统架构鲁棒性的最终质量门禁。面对包含数万个 SKU 商品详情、海量设备状态快照、甚至是金融流水大波次的 0308 批次工程大盘。如果仅仅依靠 Dart 原生的 List.unmodifiable 或者是干瘪的运行时报错。不仅会导致在定位多线程并发竞态(Race Condition)时让架构师如同在逻辑废墟中盲人摸象。更会因为缺乏编译期强制约束。令整个系统的状态管理在跨设备同步时陷入严重的混乱盲区。 我们需要一种“逻辑严丝合缝、操作物理隔离”的集合资产保护艺术。 freezed_collection 是一套专注于无缝整

By Ne0inhk
【MySQL】win 10 / win11:mysql 5.7 下载、安装与配置

【MySQL】win 10 / win11:mysql 5.7 下载、安装与配置

目录 一、MySQL 下载 (1)MySQL 官网下载地址 (2)下载保存安装包 二、MySQL 安装 (1)运行安装包 (2)选择安装类型 (3)选择安装版本号 (4)配置服务端口 (5)配置 root 的密码 (6)配置服务名称 (7)安装完成 三、配置系统环境变量 (1)打开系统环境变量配置面板 (2)编辑系统变量 Path 四、验证安装完成 五、Navicat 测试连接 (1)连接数据库 (2)填写连接信息 (3)测试连接 (4)保存连接 (5)高级配置(

By Ne0inhk
【MYSQL】MYSQL学习的一大重点:MYSQL表的操作

【MYSQL】MYSQL学习的一大重点:MYSQL表的操作

🎬 个人主页:艾莉丝努力练剑 ❄专栏传送门:《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》 《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》 ⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平 🎬 艾莉丝的简介: 文章目录 * 0 ~> 概要 * 1 ~> 创建表 * 2 ~> 创建表的案例详解 * 3 ~> 查看表结构 * 4 ~> 修改表 * 4.1 什么时候需要修改表 * 4.2 修改方式 * 4.3 案例 * 4.3.1 在users表添加二条记录 * 4.

By Ne0inhk