yolov8源码学习
yolov8源码学习
一 Backbone:主体是CSPDarkNet结构
网络模型文件保存在models->v8->yolov8.yaml
# YOLOv8.0n backbone
backbone:
# [from, repeats, module, args]
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
- [-1, 3, C2f, [128, True]]
- [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- [-1, 6, C2f, [256, True]]
- [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- [-1, 6, C2f, [512, True]]
- [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- [-1, 3, C2f, [1024, True]]
- [-1, 1, SPPF, [1024, 5]] # 9
**from:**输入。-1表示上层的输出作为本层的输入。
**repeats:**模块的重复次数。
**module:**使用的模块。
**args:**模块里面的参数
计算size的公式:out_size =( in_size - k +2 * p)/ s + 1
1.1 卷积Conv
def autopad(k, p=None, d=1): # kernel, padding, dilation
"""Pad to 'same' shape outputs."""
# 当膨胀系数d>1时,计算实际卷积核尺寸(膨胀后的等效核尺寸)
if d > 1:
# 处理整数核尺寸或列表核尺寸的情况(例如3或[3,5])
k = d * (k - 1) + 1 if isinstance(k, int) else [d * (x - 1) + 1 for x in k] # 实际kernel尺寸
# 自动计算padding值(当未指定p时)
if p is None:
# 对整数核或列表核分别处理,取核尺寸整除2的结果(保持输入输出尺寸一致)
p = k // 2 if isinstance(k, int) else [x // 2 for x in k] # 自动填充
return p # 返回计算得到的padding值
class Conv(nn. Module):
"""Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation)."""
default_act = nn. SiLU() # 默认使用SiLU激活函数(可能存在拼写错误,应为nn.SiLU)
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
"""初始化卷积层,参数包含激活函数配置"""
super().__init__() # 调用父类nn.Module的初始化
# 创建卷积层:输入通道c1,输出通道c2,核尺寸k,步长s,自动计算padding
# 分组数g,膨胀系数d,禁用偏置(与BatchNorm配合使用)
self.conv = nn. Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False)
# 添加批量归一化层
self.bn = nn. BatchNorm2d(c2)
# 配置激活函数:
# act为True时使用默认激活函数
# act为nn.Module实例时直接使用
# 其他情况使用恒等映射(无激活)
self.act = self.default_act if act is True else act if isinstance(act, nn. Module) else nn. Identity()
def forward(self, x):
"""标准前向流程:卷积 -> 批归一化 -> 激活"""
return self.act(self.bn(self.conv(x))) # 注意执行顺序:conv -> bn -> act
def forward_fuse(self, x):
"""融合前向流程(通常用于模型优化):卷积 -> 激活"""
return self.act(self.conv(x)) # 跳过批归一化,直接执行conv -> act
conv(x)-> bn() -> SiLU()
1.1.1 Conv2d
class Conv2d(_ConvNd): # 继承自PyTorch卷积基类_ConvNd,提供通用卷积操作实现
def __init__(
self,
in_channels: int, # 输入特征图的通道数(如RGB图像的3通道)
out_channels: int, # 输出特征图的通道数(卷积核的数量)
kernel_size: _size_2_t, # 卷积核尺寸(接受int或tuple,如3或(3,5))
stride: _size_2_t = 1, # 卷积步长,控制特征图下采样率
padding: Union[str, _size_2_t] = 0, # 填充方式(int/tuple或"same"/"valid"等字符串)
dilation: _size_2_t = 1, # 膨胀系数,扩大卷积核感受野(默认为1不膨胀)
groups: int = 1, # 分组卷积的组数(默认为1普通卷积,groups=in_channels时为深度可分离卷积)
bias: bool = True, # 是否添加可训练偏置参数
padding_mode: str = "zeros", # 填充模式("zeros"/"reflect"/"replicate"等)
device=None, # 指定计算设备(如"cuda:0")
dtype=None, # 指定权重数据类型(如torch.float16)
) -> None:
# 创建包含设备和数据类型信息的字典,用于后续张量初始化
factory_kwargs = {"device": device, "dtype": dtype}
# 将卷积核尺寸转换为标准的二元组格式(例如3→(3,3))
kernel_size_ = _pair(kernel_size)
# 将步长转换为二元组格式(例如2→(2,2))
stride_ = _pair(stride)
# 处理padding参数:如果是字符串(如"same")直接使用,否则转为二元组
padding_ = padding if isinstance(padding, str) else _pair(padding)
# 将膨胀系数转换为二元组格式
dilation_ = _pair(dilation)
# 调用父类_ConvNd的构造函数,初始化卷积层核心参数
super().__init__(
in_channels, # 输入通道数
out_channels, # 输出通道数
kernel_size_, # 处理后的卷积核尺寸
stride_, # 处理后的步长
padding_, # 处理后的填充参数
dilation_, # 处理后的膨胀系数
False, # 是否为转置卷积(此处为普通卷积,故为False)
_pair(0), # 输出填充(output_padding,转置卷积时使用,普通卷积设为0)
groups, # 分组数
bias, # 是否使用偏置
padding_mode, # 填充模式
**factory_kwargs, # 传递设备和数据类型参数
)
1.1.2 BatchNorm2d(c2)
def _check_input_dim(self, input):
if input.dim() != 4:
raise ValueError(f"expected 4D input (got {input.dim()}D input)")
代码功能
这是某个归一化层(如 BatchNorm2d)的内部方法,用于检查输入张量的维度是否符合要求:
- 预期输入:4D 张量(形状为
[batch_size, channels, height, width])。 - 检查逻辑:如果输入维度不是4维(如3D或5D),抛出错误。
1.1.3 SiLU(Sigmoid Linear Unit)
激活函数的作用是对输入张量逐元素应用Sigmoid加权线性单元,结合了线性乘积和非线性Sigmoid门控机制,旨在增强神经网络的表达能力。以下是详细说明:
核心功能:
-
数学表达式:
SiLU(x)=x⋅σ(x)
其中,σ(x) 是Sigmoid函数
σ(x)=11+e−x σ(x)=\frac{1}{1+e^{-x} } σ(x)=1+e−x1
。该函数将输入 x 与其Sigmoid值相乘,形成平滑的非线性激活。 -
特性:
- 门控机制:Sigmoid部分充当“软开关”,根据输入值动态调整输出权重。正数区域的输出接近 x(Sigmoid趋近于1),负数区域的输出趋近于0(Sigmoid趋近于0)。
- 平滑梯度:相比ReLU,SiLU在负值区域具有非零梯度,可能缓解梯度消失问题。
核心代码:
class SiLU(Module): # 继承自PyTorch的Module基类,定义自定义神经网络模块
class SiLU(Module): # 继承自PyTorch的`Module`基类,定义自定义神经网络模块
__constants__ = ["inplace"] # 将"inplace"标记为TorchScript常量
inplace: bool # 类型注解:声明inplace属性为布尔值
def __init__(self, inplace: bool = False): # 构造函数
super().__init__() # 调用父类Module的初始化方法
self.inplace = inplace # 将参数值赋给实例变量
def forward(self, input: Tensor) -> Tensor: # 定义前向传播
return F.silu(input, inplace=self.inplace) # 调用PyTorch函数式API实现
def extra_repr(self) -> str: # 用于模块的额外信息表示
inplace_str = "inplace=True" if self.inplace else "" # 生成状态描述字符串
return inplace_str # 返回可读性描述(影响print输出)
1.2 c2f
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):
"""
Initialize a CSP bottleneck with 2 convolutions.
Args:
c1 (int): Input channels. # 输入特征图的通道数
c2 (int): Output channels. # 输出特征图的通道数
n (int): Number of Bottleneck blocks. # Bottleneck模块的重复次数
shortcut (bool): Whether to use shortcut connections. # 是否在Bottleneck中使用残差连接
g (int): Groups for convolutions. # 分组卷积的分组数
e (float): Expansion ratio. # 通道扩展系数,控制隐藏层通道数
"""
super().__init__()
self.c = int(c2 * e) # 计算隐藏层通道数(实际处理通道数 = 输出通道数 * 扩展系数)
self.cv1 = Conv(c1, 2 * self.c, 1, 1) # 输入投影层:1x1卷积将c1通道扩展为2倍隐藏通道
self.cv2 = Conv((2 + n) * self.c, c2, 1) # 输出投影层:1x1卷积将融合特征压缩回c2通道
# 创建n个Bottleneck模块组成的序列,每个模块处理self.c通道
self.m = nn.ModuleList(
Bottleneck(
self.c, # 输入通道
self.c, # 输出通道
shortcut, # 是否使用残差连接
g, # 分组数
k=((3, 3), (3, 3)), # 每个Bottleneck包含两个3x3卷积
e=1.0 # Bottleneck内部通道扩展比例(保持输入输出通道一致)
) for _ in range(n)
)
def forward(self, x):
"""Forward pass through C2f layer."""
# 将cv1的输出沿通道维度切分为两部分(各self.c通道)
y = list(self.cv1(x).chunk(2, 1)) # chunk返回张量视图列表
# 逐级处理:每个Bottleneck处理最后一个特征图,并将结果追加到列表
y.extend(m(y[-1]) for m in self.m) # y从2个元素扩展到2+n个元素
# 沿通道维度拼接所有特征图,并通过cv2输出
return self.cv2(torch.cat(y, 1)) # 最终通道数 = (2+n)*self.c
def forward_split(self, x):
"""Forward pass using split() instead of chunk()."""
# 替代实现:使用split进行张量分割(功能与chunk相同,底层实现不同)
y = self.cv1(x).split((self.c, self.c), 1) # 分割为两个self.c通道的张量
y = [y[0], y[1]] # 转换为列表形式
# 后续处理与forward方法一致
y.extend(m(y[-1]) for m in self.m)
return self.cv2(torch.cat(y, 1))
🎯 核心设计解析
- CSP结构优化
- 通道分割:将输入特征图分割为两部分,仅对其中一部分进行Bottleneck处理,减少计算量
- 特征融合:通过
torch.cat合并初始特征与处理后的多级特征,保留不同层次的特征信息
- Bottleneck堆叠
每个Bottleneck包含两个3x3卷积(通过k=((3,3),(3,3))配置),形成更深的子网络结构,增强非线性表达能力 - 两种前向方法
forward():使用chunk进行快速分割(返回视图,无内存复制)forward_split():使用split实现相同功能,适用于需要显式内存分配的场景
🌰 计算流程示例(设n=2)
- 输入x经过cv1后通道数变为
2*self.c - 分割为两个
self.c通道的特征图y1, y2 - y2通过第一个Bottleneck生成新特征y3(
self.c通道) - y3通过第二个Bottleneck生成新特征y4(
self.c通道) - 最终拼接[y1, y2, y3, y4]得到
(2+2)*self.c通道 - cv2将总通道数压缩回c2输出
对应功能代码:
1. 输入x经过cv1卷积
y = list(self.cv1(x).chunk(2, 1)) # 对应步骤1
- 代码操作:
self.cv1是第一个 1x1 卷积层,将输入x的通道数从c1扩展为2*self.c - 结果形状:假设输入
x为(B, c1, H, W)→ 输出变为(B, 2*self.c, H, W)
2. 分割为两个部分 y1, y2
y = list(...) # 对应步骤2
- 代码操作:通过
chunk(2, 1)将cv1的输出沿通道维度(dim=1)切分为两个张量 - 结果内容:
y = [y1, y2],每个张量形状为(B, self.c, H, W)
3. y2 通过第一个 Bottleneck 生成 y3
y.extend(m(y[-1]) for m in self.m) # 对应步骤3(第一次循环)
- 代码细节:
self.m是包含 2 个Bottleneck的模块列表(n=2)- 第一次循环:
m = self.m[0],处理y[-1](即y2) - 结果形状:
y3 = m(y2)→ 输出形状仍为(B, self.c, H, W)
- 此时
y的值:y = [y1, y2, y3]
4. y3 通过第二个 Bottleneck 生成 y4
y.extend(...) # 对应步骤4(第二次循环)
- 第二次循环:
m = self.m[1],处理y[-1](即y3) - 结果形状:
y4 = m(y3)→ 输出形状仍为(B, self.c, H, W) - 此时
y的值:y = [y1, y2, y3, y4]
5. 拼接所有特征图
return self.cv2(torch.cat(y, 1)) # 对应步骤5
- 代码操作:
torch.cat(y, 1)将列表y中的 4 个张量沿通道维度拼接 - 拼接后形状:
(B, (2+2)*self.c, H, W)→ 通道数为4*self.c
6.通过 cv2 压缩通道
return self.cv2(...) # 对应步骤6
- 代码操作:
self.cv2是第二个 1x1 卷积层,将通道数从4*self.c压缩回c2 - 最终输出形状:
(B, c2, H, W)
关键代码段解析
# 代码段:y.extend(m(y[-1]) for m in self.m)
for m in self.m: # 遍历每个Bottleneck模块(n=2时循环两次)
new_feature = m(y[-1]) # 总是处理当前最后一个特征图
y.append(new_feature) # 将结果追加到列表
动态更新逻辑
:
- 初始
y = [y1, y2] - 第一次循环后
y = [y1, y2, y3] - 第二次循环后
y = [y1, y2, y3, y4]
可视化流程
输入 x
│
↓
cv1(x) → [y1, y2] → y2 → Bottleneck1 → y3 → Bottleneck2 → y4
│ │ │
└──────────────────────┴──────────────────────────┘
最终拼接:y1 + y2 + y3 + y4 → cv2 → 输出
这种设计通过 逐步处理特征图并保留中间结果,实现了多级特征融合,同时保持了计算效率。
1.3 SPPF
class SPPF(nn.Module):
"""Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher."""
def __init__(self, c1, c2, k=5):
"""
Initialize the SPPF layer with given input/output channels and kernel size.
Args:
c1 (int): Input channels. # 输入特征图的通道数
c2 (int): Output channels. # 输出特征图的通道数
k (int): Kernel size. # 池化核尺寸(默认5x5)
Notes:
This module is equivalent to SPP(k=(5, 9, 13)). # 等效于使用5,9,13三种核尺寸的SPP
"""
super().__init__()
c_ = c1 // 2 # 计算隐藏层通道数(输入通道数减半)
# 第一个1x1卷积压缩通道数
self.cv1 = Conv(c1, c_, 1, 1) # 1x1卷积将c1通道压缩到c_(c1//2)通道
# 最后一个1x1卷积恢复通道数
self.cv2 = Conv(c_ * 4, c2, 1, 1) # 1x1卷积将拼接后的4*c_通道扩展到c2通道
# 定义最大池化层(核心实现)
self.m = nn.MaxPool2d(kernel_size=k, stride=1, padding=k // 2) # 固定步长1,padding=k//2保持特征图尺寸
def forward(self, x):
"""Apply sequential pooling operations to input and return concatenated feature maps."""
# 初始处理:通过第一个卷积层
y = [self.cv1(x)] # 对输入执行第一次卷积,结果作为第一个特征图
# 连续三次最大池化操作(等效不同尺寸感受野)
y.extend(self.m(y[-1]) for _ in range(3)) # 对前次结果重复池化3次,生成多尺度特征图
"""
执行过程示例:
y[0] = cv1(x) → 原始特征
y[1] = m(y[0]) → 5x5感受野
y[2] = m(y[1]) = m(m(y[0])) → 等效9x9感受野
y[3] = m(y[2]) = m(m(m(y[0]))) → 等效13x13感受野
"""
# 拼接并输出最终结果
return self.cv2(torch.cat(y, 1)) # 沿通道维度拼接4个特征图,最后通过卷积调整通道数
🎯 关键设计解析
- 等效多尺度感受野
通过单一大尺寸池化核(5x5)的连续应用,实现与标准SPP(使用5x5,9x9,13x13池化核)相同的多尺度特征提取效果:- 第1次池化:5x5感受野
- 第2次池化:5 + (5-1) = 9x9感受野
- 第3次池化:9 + (5-1) = 13x13感受野
- 计算效率优化
相比标准SPP需要三个独立池化层:- ✅ 参数共享:重复使用同一个池化层
- ✅ 内存优化:只需存储一个池化核参数
- ✅ 减少33%的池化计算量(实验证明精度无损)
- 通道压缩策略
cv1先将通道数减半(c1//2),降低后续池化计算量- 最终拼接后通过
cv2恢复目标通道数(c2)
Lout=⌊Lin+2×padding−dilation×(kernel_size−1)−1stride+1⌋ L_{\text{out}} = \left\lfloor \frac{L_{\text{in}} + 2 \times \text{padding} - \text{dilation} \times (\text{kernel\_size} - 1) - 1}{\text{stride}} + 1 \right\rfloor Lout=⌊strideLin+2×padding−dilation×(kernel_size−1)−1+1⌋
🌰 计算示例(假设输入形状为[1,64,640,640])
-
cv1处理
[1,64,640,640]→[1,32,640,640](通道减半) -
三次池化结果:
步骤 操作 输出形状 等效感受野 y[0] cv1(x) [1,32,640,640] - y[1] m(y[0]) [1,32,640,640] 5x5 y[2] m(y[1]) [1,32,640,640] 9x9 y[3] m(y[2]) [1,32,640,640] 13x13 -
最终拼接:
torch.cat(y,1)→[1,128,640,640](4 * 32通道)cv2处理→[1,c2,640,640](恢复目标通道)
1.4 总结
imput map channel:3 size:640*640
第0层(Conv[3,2,1]) channel:64 size:320*320
第1层(Conv[3,2,1]) channel:128 size:160*160
第2层(C2f)* 3 channel:128 size:160*160
第3层(Conv[3,2,1]) channel:256 size: 80*80
第4层(C2f)* 6 channel:256 size: 80*80
第5层(Conv[3,2,1]) channel:512 size: 40*40
第6层(C2f)* 6 channel:512 size: 40*40
第7层(Conv[3,2,1]) channel:1024 size: 20*20
第8层(C2f)* 3 channel:1024 size: 20*20
第9层(SPPF[k=5]) channel:1024 size: 20*20
二、Head
# YOLOv8.0n head
head:
- [-1, 1, nn.Upsample, [None, 2, 'nearest']]
- [[-1, 6], 1, Concat, [1]] # cat backbone P4
- [-1, 3, C2f, [512]] # 12
- [-1, 1, nn.Upsample, [None, 2, 'nearest']]
- [[-1, 4], 1, Concat, [1]] # cat backbone P3
- [-1, 3, C2f, [256]] # 15 (P3/8-small)
- [-1, 1, Conv, [256, 3, 2]]
- [[-1, 12], 1, Concat, [1]] # cat head P4
- [-1, 3, C2f, [512]] # 18 (P4/16-medium)
- [-1, 1, Conv, [512, 3, 2]]
- [[-1, 9], 1, Concat, [1]] # cat head P5
- [-1, 3, C2f, [1024]] # 21 (P5/32-large)
- [[15, 18, 21], 1, Detect, [nc]] # Detect(P3, P4, P5)
(1)第10层:经过SSPF的模块,接着进入上采样upsample。
None:不指定输出尺寸。
2:输出的尺寸为输入尺寸的2倍。
nearest:使用邻近插值算法。经过上采样后尺寸从10242020 -> 10244040。
经过路线如图所示:

(2)第11层:Concat模块,与经过上采样的上一层和第六层(P4)concat。(代码在:ultralytics -> nn -> modules-> init.py)
[1]:在维度为1上拼接;
此时经过上采样的尺寸为10244040 + 第6层输出尺寸为5124040 = 15364040。
class Concat(nn.Module):
"""Concatenate a list of tensors along dimension."""
def __init__(self, dimension=1):
"""Concatenates a list of tensors along a specified dimension."""
super().__init__()
self.d = dimension
def forward(self, x):
"""Forward pass for the YOLOv8 mask Proto module."""
return torch.cat(x, self.d)
(3)**第12层:**3C2f;通道数为512,不进行shortcut。此时尺寸从1536*40*40 -> 512*40*40
(4)**第13层:**upsample,第12层作为输入。与第10层原理一样。尺寸从512*40*40 -> 512*80*80
(5)**第14层:*Concat模块,与经过上采样的上一层(13层)和第四层(P3)连接。
此时经过上采样的尺寸为512*80*80 + 第4层输出尺寸为256*80*80 = 768*80*80。
(6)第15层:* 3*C2f;通道数为256,不进行shortcut。此时尺寸从768*80*80 -> 256*80*80。
(7)**第16层:**经过卷积Conv,通道256,k=3,s=2,计算公式上面介绍过。256*80*80 -> 256*40*40.
(8)**第17层:**Concat模块,与经过卷积的16和第12层连接。16层:256*40*40 + 第12层:512*40*40 = 768*40*40。
(9)**第18层:**3*C2f;通道数为512,不进行shortcut。此时尺寸从7684040 -> 51240*40。
(10) **第19层:**经过卷积Conv,通道512,k=3,s=2,计算公式上面介绍过。512*40*40 -> 512*20*20.
(11)**第20层:**Concat模块,与经过卷积的19和第9层连接。19层:512*20*20 + 第9层:1024*20*20 = 1536*20*20。
(12)第21层:3*C2f;通道数为1024,不进行shortcut。尺寸变化:1536*20*20 -> 1024*20*20
三、Detect
def __init__(self, nc=80, ch=()): # 初始化检测层
super().__init__()
self.nc = nc # 类别数量(COCO默认80)
self.nl = len(ch) # 检测层数量(对应P3/P4/P5)
self.reg_max = 16 # DFL回归参数最大值(用于边界框分布预测)
self.no = nc + self.reg_max * 4 # 每个anchor的输出维度(4个坐标参数*reg_max + 类别数)
self.stride = torch.zeros(self.nl) # 各检测层的步幅
# 通道数计算:c2用于回归分支,c3用于分类分支
c2 = max((16, ch[0] // 4, self.reg_max * 4)) # 回归分支通道至少16/reg_max*4
c3 = max(ch[0], self.nc) # 分类分支通道至少等于类别数
# 回归分支网络:3层卷积 -> 输出4*reg_max(坐标分布参数)
self.cv2 = nn.ModuleList([
nn.Sequential(
Conv(x, c2, 3), # 3x3卷积
Conv(c2, c2, 3), # 二次3x3卷积
nn.Conv2d(c2, 4 * self.reg_max, 1) # 1x1卷积输出分布参数
) for x in ch
])
# 分类分支网络:3层卷积 -> 输出nc(类别概率)
self.cv3 = nn.ModuleList([
nn.Sequential(
Conv(x, c3, 3), # 3x3卷积
Conv(c3, c3, 3), # 二次3x3卷积
nn.Conv2d(c3, self.nc, 1) # 1x1卷积输出类别分数
) for x in ch
])
# DFL模块:将分布参数转换为实际坐标(Distribution Focal Loss)
self.dfl = DFL(self.reg_max) if self.reg_max > 1 else nn.Identity()
def forward(self, x):
"""前向传播过程:生成预测框和类别概率"""
shape = x[0].shape # 获取输入特征图形状[B,C,H,W]
# 处理每个检测层的输出
for i in range(self.nl):
# 拼接回归分支和分类分支的输出
x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1)
# 训练模式直接返回原始输出
if self.training:
return x
# 推理模式下的动态调整(首次推理或特征图形状变化时)
elif self.dynamic or self.shape != shape:
# 生成锚点框和步幅(基于特征图尺寸)
self.anchors, self.strides = (x.transpose(0, 1) for x in make_anchors(x, self.stride, 0.5))
self.shape = shape # 缓存当前形状
# 拼接所有检测层的输出 [B,no,anchors_num]
x_cat = torch.cat([xi.view(shape[0], self.no, -1) for xi in x], 2)
# 处理导出模式(避免某些框架不支持split操作)
if self.export and self.format in ('saved_model', 'pb', 'tflite', 'edgetpu', 'tfjs'):
box = x_cat[:, :self.reg_max * 4] # 提取回归参数部分
cls = x_cat[:, self.reg_max * 4:] # 提取分类分数部分
else:
box, cls = x_cat.split((self.reg_max * 4, self.nc), 1) # 分割回归和分类结果
# 将分布参数转换为边界框坐标(使用DFL解码)
dbox = dist2bbox(self.dfl(box), self.anchors.unsqueeze(0), xywh=True, dim=1) * self.strides
# 拼接坐标和类别概率(sigmoid激活分类分数)
y = torch.cat((dbox, cls.sigmoid()), 1)
# 导出模式返回单张量,否则返回元组(包含中间结果)
return y if self.export else (y, x)
def bias_init(self):
"""偏置初始化:需要stride参数可用时执行"""
m = self # 获取当前Detect模块
# 遍历每个检测层的回归和分类分支
for a, b, s in zip(m.cv2, m.cv3, m.stride):
# 回归分支最后一层卷积偏置初始化为1.0(稳定初始预测)
a[-1].bias.data[:] = 1.0 # box
# 分类分支偏置初始化(基于类别频率的启发式初始化)
# 公式解释:log(5/(nc*0.01*(640/s)^2)) -> 平衡不同尺度的类别预测
b[-1].bias.data[:m.nc] = math.log(5 / m.nc / (640 / s) ** 2)

首先来看一下初始化的一些属性,
nc: 整数,表示图像分类问题中的类别数;
nl: 整数,表示检测模型中使用的检测层数;
reg_max: 整数,表示每个锚点输出的通道数;
no: 整数,表示每个锚点的输出数量,其中包括类别数和位置信息;
stride: 一个形状为(nl,)的张量,表示每个检测层的步长(stride);
cv2: 一个 nn.ModuleList 对象,包含多个卷积层,用于预测每个锚点的位置信息;
cv3: 一个 nn.ModuleList 对象,包含多个卷积层,用于预测每个锚点的类别信息;
dfl: DFL(Distribution Focal Loss)
shape属性表示模型期望的输入形状,如果模型只接受固定形状的输入,则 self.shape 存储该形状
在前向传播中,shape获取了输入张量x的形状,并保存在shape中。
for i in range(self.nl):
x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1)
这里我们可以print(x[i].size())看一下,发现是:1664040,1662020,1661010,因为我是两个类,所以66 = (2+416)这个416也就是self.no = nc + self.reg_max * 4。因为我输入尺寸是320320的,所以三个特征图是40,20,10,如果大家是640*640的,特征图应该是80,40,20。
这里同时可以打开onnx模型看一下,这一步是将cv2和cv3的输入进行concat,那么形状应该是这样:
接着如果是训练过程的话,这里的x就输出了。否则的话继续。
在这个代码片段中,self.dynamic和self.shape是两个属性,它们与输入张量的形状有关。如果 self.dynamic为真或者self.shape 与当前输入张量的形状不同,那么就会执行相应的操作。
self.dynamic属性通常用于指示模型是否支持动态形状输入。在 PyTorch 中,动态形状表示对形状进行推理,而不依赖于固定的形状尺寸。当使用动态形状时,模型可以处理任意形状的输入,并且可以通过在运行时计算形状信息来确定每个层的形状。如果模型支持动态形状输入,则 self.dynamic 属性通常设置为 True。
self.shape 属性通常用于存储模型所期望的输入形状。如果模型只接受固定形状的输入,则 self.shape 属性将存储该形状。在这种情况下,如果输入张量的形状与self.shape不匹配,则可能需要对输入进行重新调整,以适应模型的期望输入形状。
那么这个
self.anchors, self.strides = (x.transpose(0, 1) for x in make_anchors(x, self.stride, 0.5))
就需要看make_anchors了,这个方法在utils/tal.py中实现:
def make_anchors(feats, strides, grid_cell_offset=0.5):
"""Generate anchors from features."""
anchor_points, stride_tensor = [], []
assert feats is not None
dtype, device = feats[0].dtype, feats[0].device
for i, stride in enumerate(strides):
_, _, h, w = feats[i].shape
sx = torch.arange(end=w, device=device, dtype=dtype) + grid_cell_offset # shift x
sy = torch.arange(end=h, device=device, dtype=dtype) + grid_cell_offset # shift y
sy, sx = torch.meshgrid(sy, sx, indexing='ij') if TORCH_1_10 else torch.meshgrid(sy, sx)
anchor_points.append(torch.stack((sx, sy), -1).view(-1, 2))
stride_tensor.append(torch.full((h * w, 1), stride, dtype=dtype, device=device))
return torch.cat(anchor_points), torch.cat(stride_tensor)
feats: 一个列表,包含多个特征图;
strides: 一个列表,包含多个步长;
grid_cell_offset: 一个浮点数,表示每个网格单元的偏移量,默认为 0.5。
在实现中,首先遍历输入的特征图和步长,并分别获取它们的高度、宽度和步长值。然后,使用 PyTorch 的 arange() 函数生成一组横向和纵向的位移值,并添加一个偏移量(即 grid_cell_offset)以将锚点的中心对准每个网格单元的中心。
接下来,使用 PyTorch 的 meshgrid() 函数生成所有可能的锚点位置,并将其保存在 anchor_points 列表中。其中,每个锚点的位置由两个坐标值表示,即 (x, y),并被转换为形状为 (n, 2) 的张量,其中 n 表示特征图上的像素点数量。
同时,在每个特征图上都需要保存相应的步长信息,以便后续计算。因此,使用 PyTorch 的 full() 函数创建一个形状为 (h*w, 1) 的张量,其中 h 和 w 分别表示特征图的高度和宽度,每个元素都被初始化为当前特征图的步长值。
最终,通过将所有锚点位置和步长信息连接起来,可以得到形状为 (nnl, 2) 和 (nnl, 1) 的张量,其中 nl 表示特征图的数量,n 表示每个特征图上的像素点数量。这些张量将被用于计算每个锚点的位置和预测信息,并生成最终的预测结果。
所以经过transpose后,得到的anchor应该是(2,2100),stride是(1,2100)
三个x[i]进行concat:
x_cat = torch.cat([xi.view(shape[0], self.no, -1) for xi in x], 2)
得到的x_cat的size()应该是1662100,因为xi.view(shape[0],self.no,-1)中的-1表示根据其他维度的值组合成一维,即4040=1600,2020=400,10*10=100。
下一步的if else其实输出是一样的(我是这么认为的,若有错误请指点)
都是将x_cat的第二个维度66分成box的64和cls的2,这里的box的第二个维度经过dfl的操作变成4维的142100,与升维后的anchors.unsqueeze(0)送入dist2bbox进行计算,得到xywh值。
这里贴一下dist2bbox的实现:
def dist2bbox(distance, anchor_points, xywh=True, dim=-1):
"""Transform distance(ltrb) to box(xywh or xyxy)."""
lt, rb = distance.chunk(2, dim)
#print('lt:',lt.size())
#print('rb:',rb.size())
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
这里lt和rb分别代表x y w h的偏移量。
紧接着y = torch.cat((dbox, cls.sigmoid()), 1),将xywh和经过sigmoid归一化后的2个cls在第二维度上进行组合,形成了最终的162100,也就是最终的output。

更多推荐
所有评论(0)