一、parse_model函数解读
def parse_model(d, ch): # model_dict, input_channels(3)
"""用在下面Model模块中
解析模型文件(字典形式),并搭建网络结构
这个函数其实主要做的就是: 更新当前层的args(参数),计算c2(当前层的输出channel) =>
使用当前层的参数搭建当前层 =>
生成 layers + save
@Params d: model_dict 模型文件 字典形式 {dict:7} [yolov5s.yaml](https://github.com/Oneflow-Inc/one-yolov5/blob/main/models/yolov5s.yaml)中的6个元素 + ch
#Params ch: 记录模型每一层的输出channel 初始ch=[3] 后面会删除
@return nn.Sequential(*layers): 网络的每一层的层结构
@return sorted(save): 把所有层结构中from不是-1的值记下 并排序 [4, 6, 10, 14, 17, 20, 23]
"""
LOGGER.info(f"\n{'':>3}{'from':>18}{'n':>3}{'params':>10} {'module':<40}{'arguments':<30}")
# 读取d字典中的anchors和parameters(nc、depth_multiple、width_multiple)
anchors, nc, gd, gw = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple']
# na: number of anchors 每一个predict head上的anchor数 = 3
na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors # number of anchors
no = na * (nc + 5) # number of outputs = anchors * (classes + 5) 每一个predict head层的输出channel
# 开始搭建网络
# layers: 保存每一层的层结构
# save: 记录下所有层结构中from中不是-1的层结构序号
# c2: 保存当前层的输出channel
layers, save, c2 = [], [], ch[-1] # layers, savelist, ch out
# enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在 for 循环当中。
for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']): # from, number, module, args
m = eval(m) if isinstance(m, str) else m # eval strings
for j, a in enumerate(args):
# args是一个列表,这一步把列表中的内容取出来
with contextlib.suppress(NameError):
args[j] = eval(a) if isinstance(a, str) else a # eval strings
# 将深度与深度因子相乘,计算层深度。深度最小为1.
n = n_ = max(round(n * gd), 1) if n > 1 else n # depth gain
# 如果当前的模块m在本项目定义的模块类型中,就可以处理这个模块
if m in (Conv, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, MixConv2d, Focus, CrossConv,
BottleneckCSP, C3, C3TR, C3SPP, C3Ghost, nn.ConvTranspose2d, DWConvTranspose2d, C3x):
# c1: 输入通道数 c2:输出通道数
c1, c2 = ch[f], args[0]
# 该层不是最后一层,则将通道数乘以宽度因子 也就是说,宽度因子作用于除了最后一层之外的所有层
if c2 != no: # if not output
# make_divisible的作用,使得原始的通道数乘以宽度因子之后取整到8的倍数,这样处理一般是让模型的并行性和推理性能更好。
c2 = make_divisible(c2 * gw, 8)
# 将前面的运算结果保存在args中,它也就是这个模块最终的输入参数。
args = [c1, c2, *args[1:]]
# 根据每层网络参数的不同,分别处理参数 具体各个类的参数是什么请参考它们的__init__方法这里不再详细解释了
if m in [BottleneckCSP, C3, C3TR, C3Ghost, C3x]:
# 这里的意思就是重复n次,比如conv这个模块重复n次,这个n 是上面算出来的 depth
args.insert(2, n) # number of repeats
n = 1
elif m is nn.BatchNorm2d:
args = [ch[f]]
elif m is Concat:
c2 = sum(ch[x] for x in f)
elif m is Detect:
args.append([ch[x] for x in f])
if isinstance(args[1], int): # number of anchors
args[1] = [list(range(args[1] * 2))] * len(f)
elif m is Contract:
c2 = ch[f] * args[0] ** 2
elif m is Expand:
c2 = ch[f] // args[0] ** 2
else:
c2 = ch[f]
# 构建整个网络模块 这里就是根据模块的重复次数n以及模块本身和它的参数来构建这个模块和参数对应的Module
m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args) # module
# 获取模块(module type)具体名例如 models.common.Conv , models.common.C3 , models.common.SPPF 等。
t = str(m)[8:-2].replace('__main__.', '') # replace函数作用是字符串"__main__"替换为'',在当前项目没有用到这个替换。
np = sum(x.numel() for x in m_.parameters()) # number params
m_.i, m_.f, m_.type, m_.np = i, f, t, np # attach index, 'from' index, type, number params
LOGGER.info(f'{i:>3}{str(f):>18}{n_:>3}{np:10.0f} {t:<40}{str(args):<30}') # print
"""
如果x不是-1,则将其保存在save列表中,表示该层需要保存特征图。
这里 x % i 与 x 等价例如在最后一层 :
f = [17,20,23] , i = 24
y = [ x % i for x in ([f] if isinstance(f, int) else f) if x != -1 ]
print(y) # [17, 20, 23]
# 写成x % i 可能因为:i - 1 = -1 % i (比如 f = [-1],则 [x % i for x in f] 代表 [11] )
"""
save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1) # append to savelist
layers.append(m_)
if i == 0: # 如果是初次迭代,则新创建一个ch(因为形参ch在创建第一个网络模块时需要用到,所以创建网络模块之后再初始化ch)
ch = []
ch.append(c2)
# 将所有的层封装为nn.Sequential , 对保存的特征图排序
return nn.Sequential(*layers), sorted(save)
这段代码的主要功能是解析一个YOLOv5模型的配置字典(通常来源于一个 .yaml
文件),并根据该配置构建相应的网络层结构。以下是代码逐步分解和详细解释:
逐步分解
-
函数定义:
def parse_model(d, ch):
该函数接受两个参数:
d
代表模型的配置字典,ch
代表输入通道数。 -
日志记录和参数提取:
LOGGER.info(f"\n{'':>3}{'from':>18}{'n':>3}{'params':>10} {'module':<40}{'arguments':<30}") anchors, nc, gd, gw, act = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple'], d.get('activation')
记录参数表头信息。
-
从字典中提取以下参数:
-
anchors
: 锚框设置。 -
nc
: 类别数。 -
gd
: 深度乘数。 -
gw
: 宽度乘数。 -
act
: 激活函数。
-
-
-
打印模型关键信息:
print(f"{colorstr('anchors:')} {anchors}") print(f"{colorstr('classes:')} {nc}") print(f"{colorstr('depth multiple:')} {gd}") print(f"{colorstr('width multiple:')} {gw}") print(f"{colorstr('activation:')} {act}")
打印出提取的参数信息,以便于调试和验证。
-
激活函数的设置:
if act: Conv.default_act = eval(act)
如果存在自定义的激活函数,将其设置为默认激活函数。
-
计算锚框数量和输出数量:
na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors no = na * (nc + 5)
na
: 根据锚框的数量计算锚框数。-
no
: 计算输出的总数,包括每个锚框预测的类别信息和边界框信息。
-
-
层的构建:
layers, save, c2 = [], [], ch[-1]
初始化用于存储网络层的列表
layers
和sava
,以及当前输出通道数c2
。 -
循环解析模型结构:
for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']):
遍历模型的主干(backbone)和头(head)部分。
-
f
: 来源索引,n
: 重复次数,m
: 模块名称,args
: 参数。
-
-
模块的创建和参数处理:
-
根据模块类型(例如卷积层、批归一化层等)构建相应的网络层。
-
包括对深度增益的应用和按需调整输出通道数。
-
-
记录模块信息和保存输出:
m_.i, m_.f, m_.type, m_.np = i, f, t, np LOGGER.info(...) save.extend(...) layers.append(m_)
将构建的信息附加到所创建的模块上,并记录到日志中。
-
更新保存的输出列表。
-
-
返回构建的模型:
return nn.Sequential(*layers), sorted(save)
将所有网络层包装成一个顺序模型并返回。
总结
该代码的主要功能是解析YOLOv5模型的配置字典,并根据该配置构建相应的网络结构。关键在于通过提取模型参数、计算所需的通道数以及相应的模块,从而创建一个可用于目标检测的深层神经网络。它对于构建和定制YOLOv5模型的灵活性和扩展性具有重要意义。
二、detect函数
Detect 模块是 YOLO 网络模型的最后一层 (对应 yaml 文件最后一行),通过 yaml 文件进行声明,格式为:
[*from], 1, Detect, [nc, anchors]
其中 nc 为分类数,anchors 为先验框,修改 yaml 文件的前几行即可。
在 parse_model 函数中,会根据 from 参数,找到对应网络层的输出通道数。传参给 Detect 对象后,生成对应的 Conv2d,为后面的计算损失或者NMS后处理作准备。
class Detect(nn.Module):
# YOLOv5 Detect head for detection models
stride = None # strides computed during build
dynamic = False # force grid reconstruction
export = False # export mode
def __init__(self, nc=80, anchors=(), ch=(), inplace=True): # detection layer
super().__init__()
self.nc = nc # number of classes
self.no = nc + 5 # number of outputs per anchor
self.nl = len(anchors) # number of detection layers
self.na = len(anchors[0]) // 2 # number of anchors
self.grid = [torch.empty(0) for _ in range(self.nl)] # init grid
self.anchor_grid = [torch.empty(0) for _ in range(self.nl)] # init anchor grid
self.register_buffer('anchors', torch.tensor(anchors).float().view(self.nl, -1, 2)) # shape(nl,na,2)
self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch) # output conv
self.inplace = inplace # use inplace ops (e.g. slice assignment)
def forward(self, x):
z = [] # inference output
for i in range(self.nl):
x[i] = self.m[i](x[i]) # conv
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
if not self.training: # inference
if self.dynamic or self.grid[i].shape[2:4] != x[i].shape[2:4]:
self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i)
if isinstance(self, Segment): # (boxes + masks)
xy, wh, conf, mask = x[i].split((2, 2, self.nc + 1, self.no - self.nc - 5), 4)
xy = (xy.sigmoid() * 2 + self.grid[i]) * self.stride[i] # xy
wh = (wh.sigmoid() * 2) ** 2 * self.anchor_grid[i] # wh
y = torch.cat((xy, wh, conf.sigmoid(), mask), 4)
else: # Detect (boxes only)
xy, wh, conf = x[i].sigmoid().split((2, 2, self.nc + 1), 4)
xy = (xy * 2 + self.grid[i]) * self.stride[i] # xy
wh = (wh * 2) ** 2 * self.anchor_grid[i] # wh
y = torch.cat((xy, wh, conf), 4)
z.append(y.view(bs, self.na * nx * ny, self.no))
return x if self.training else (torch.cat(z, 1),) if self.export else (torch.cat(z, 1), x)
def _make_grid(self, nx=20, ny=20, i=0, torch_1_10=check_version(torch.__version__, '1.10.0')):
d = self.anchors[i].device
t = self.anchors[i].dtype
shape = 1, self.na, ny, nx, 2 # grid shape
y, x = torch.arange(ny, device=d, dtype=t), torch.arange(nx, device=d, dtype=t)
yv, xv = torch.meshgrid(y, x, indexing='ij') if torch_1_10 else torch.meshgrid(y, x) # torch>=0.7 compatibility
grid = torch.stack((xv, yv), 2).expand(shape) - 0.5 # add grid offset, i.e. y = 2.0 * x - 0.5
anchor_grid = (self.anchors[i] * self.stride[i]).view((1, self.na, 1, 1, 2)).expand(shape)
return grid, anchor_grid
这个代码定义了一个名为 Detect
的类,属于 PyTorch 的 nn.Module
类,主要用于实现 YOLOv5 的检测头部(Detection Head)。下面将逐步分解并详细解释代码的各个部分。
2.1 类的定义及属性:
class Detect(nn.Module):
stride = None # strides computed during build
dynamic = False # force grid reconstruction
export = False # export mode
- 这是一个继承自
nn.Module
的类Detect
,它包含三个类属性:stride
,dynamic
,export
。这些属性的初始值为None
或False
,在构建过程中会被初始化。
-
stride = None
:- 这行代码定义了一个类变量
stride
,初始值为None
。该变量代表网络的步幅(stride),步幅是卷积操作中滑动窗口的步长。在模型构建过程中,这个变量将被计算并赋值。
- 这行代码定义了一个类变量
-
dynamic = False
:- 这行代码定义了一个类变量
dynamic
,初始值为False
。这个变量用于控制模型的动态网格重建模式。如果设置为True
,模型将强制重新计算网格(grid),通常是在推理过程中需要改变输入图像的大小时使用。
- 这行代码定义了一个类变量
-
export = False
:- 这行代码定义了一个类变量
export
,初始值为False
。此变量指示是否处于导出模式,通常在模型需要导出到另一个格式(例如 ONNX)时会改变此变量的值。
- 这行代码定义了一个类变量
以上代码段是类定义中的一些属性初始化。主要功能是:
- 定义关键的模型参数,特别是与推理和导出相关的设置;
stride
会在模型构建时决定卷积操作的步幅大小;dynamic
用于控制是否在推理期间重新计算网格,以适应不同的输入尺寸;export
则用来监控模型是否处于准备导出的状态。
这些参数在模型的构建和运行阶段对性能和功能具有重要影响。
2.2 构造方法 __init__
:
def __init__(self, nc=80, anchors=(), ch=(), inplace=True): # detection layer
super().__init__()
# nc: 数据集类别数量
self.nc = nc
# no: 表示每个anchor的输出数,前nc个01字符对应类别,后5个对应:是否有目标,目标框的中心,目标框的宽高
self.no = nc + 5 # nc+5=nc+(x,y,w,h,conf)
# nl: 表示预测层数,yolov5是3层预测
self.nl = len(anchors)
# na: 表示anchors的数量,除以2是因为[10,13, 16,30, 33,23]这个长度是6,对应3个anchor
self.na = len(anchors[0]) // 2
# grid: 表示初始化grid列表大小,下面会计算grid,grid就是每个格子的x,y坐标(整数,比如0-19),左上角为(1,1),右下角为(input.w/stride,input.h/stride)
self.grid = [torch.zeros(1)] * self.nl
# anchor_grid: 表示初始化anchor_grid列表大小,空列表
self.anchor_grid = [torch.zeros(1)] * self.nl # init anchor grid
# 注册常量anchor,并将预选框(尺寸)以数对形式存入,并命名为anchors
self.register_buffer('anchors', torch.tensor(anchors).float().view(self.nl, -1, 2)) # shape(nl,na,2) 注意后面就可以通过self.anchors来访问它了
# 每一张进行三次预测,每一个预测结果包含nc+5个值
# (n, 255, 80, 80),(n, 255, 40, 40),(n, 255, 20, 20) --> ch=(255, 255, 255)
# 255 -> (nc+5)*3 ===> 为了提取出预测框的位置信息以及预测框尺寸信息
self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch) # output conv 3个输出层最后的1乘1卷积
# inplace: 一般都是True,默认不使用AWS,Inferentia加速
self.inplace = inplace # use in-place ops (e.g. slice assignment)
# 如果模型不训练那么将会对这些预测得到的参数进一步处理,然后输出,可以方便后期的直接调用
# 包含了三个信息pred_box [x,y,w,h] pred_conf[confidence] pre_cls[cls0,cls1,cls2,...clsn]
nc
: 类别数量(默认80,YOLOv5在COCO数据集上的类别数)。no
: 每个锚点的输出数(类别数 + 5,5是用于边界框的坐标、置信度等)。nl
: 检测层的数量(根据提供的锚点数量)。na
: 每个检测层的锚点数量(每个锚点有两个坐标)。grid
和anchor_grid
: 用于存放动态计算的网格和锚点网格。register_buffer
用于注册不需要梯度的缓冲区(即锚点)。m
是一个包含多个卷积层的模块列表,输出每个锚点的预测结果。inplace
: 控制是否使用原地操作,优化内存使用。
这段代码是YOLOv5模型中“Detect”类的初始化方法(__init__
),用于设置检测层的参数和结构。以下是对每一行的逐步分析:
-
方法定义:
def __init__(self, nc=80, anchors=(), ch=(), inplace=True): # detection layer
__init__
:这是类的构造方法,用于初始化类的实例。nc=80
:表示类别数量,默认为80,适用于常见的COCO数据集。anchors=()
:预定义的锚框,默认为空元组。ch=()
:用于构建网络层的输入通道数。inplace=True
:指示是否使用原生操作(如切片赋值)来节省内存。
-
调用父类构造方法:
super().__init__()
- 调用父类的构造方法,以确保基类的属性和方法可以在该类中使用。
-
设置类别数量:
self.nc = nc # number of classes
- 将传入的类别数赋值给实例变量
self.nc
。
- 将传入的类别数赋值给实例变量
-
计算每个锚框的输出数量:
self.no = nc + 5 # number of outputs per anchor
- 每个锚框的输出数量等于类别数加上5,其中5指的是边界框的坐标(x, y, 宽, 高)以及置信度。
-
计算检测层数量:
self.nl = len(anchors) # number of detection layers
- 检测层的数量等于锚框的数量。
-
计算锚框的数量:
self.na = len(anchors[0]) // 2 # number of anchors
- 每个检测层的锚框数量由每个锚框的维度决定,通常情况下,每个锚框是由宽和高组成,因此数量需要除以2。
-
初始化网格:
self.grid = [torch.empty(0) for _ in range(self.nl)] # init grid
- 创建一个空的网格列表,用于后续的特征图处理。格子坐标系,左上角为(1,1),右下角为(input.w/stride,input.h/stride)
-
初始化锚框网格:
self.anchor_grid = [torch.empty(0) for _ in range(self.nl)] # init anchor grid
- 创建一个空的锚框网格列表,结构与
self.grid
一致。
- 创建一个空的锚框网格列表,结构与
-
注册锚框缓冲区:
self.register_buffer('anchors', torch.tensor(anchors).float().view(self.nl, -1, 2)) # shape(nl,na,2)
- 将锚框转为张量并注册为模型的缓冲区,在网络中不需要梯度更新。其形状为
(nl, na, 2)
,即(检测层数量,锚框数量,坐标维度)。
- 将锚框转为张量并注册为模型的缓冲区,在网络中不需要梯度更新。其形状为
-
定义输出卷积层:
self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch) # output conv
- 创建一个
ModuleList
,包含多个卷积层。每个卷积层的输入通道数由ch
给出,输出通道数为self.no * self.na
,即每个锚框的输出。
- 创建一个
-
设置是否采用原地操作的标志:
self.inplace = inplace # use inplace ops (e.g. slice assignment)
- 将
inplace
参数的值赋给实例变量,控制是否使用原地操作。
- 将
这段代码的主要功能是初始化YOLOv5的检测层。该检测层负责处理输入特征图,生成检测框,预测框的类别和位置。它通过设置锚框、类别数、网络层数等参数,构建卷积层用于输出,并为后续的推理过程做好准备。整体上,此部分代码是YOLOv5模型中的关键组件,为目标检测任务提供基础功能。
2.3 前向传播方法 forward
:
def forward(self, x):
"""
:return train: 一个tensor list 存放三个元素 [bs, anchor_num, grid_w, grid_h, xywh+c+classes_num]
分别是 [1, 3, 80, 80, 25] [1, 3, 40, 40, 25] [1, 3, 20, 20, 25]
inference: 0 [1, 19200+4800+1200, 25] = [bs, anchor_num*grid_w*grid_h, xywh+c+classes_num]
1 一个tensor list 存放三个元素 [bs, anchor_num, grid_w, grid_h, xywh+c+classes_num]
[1, 3, 80, 80, 25] [1, 3, 40, 40, 25] [1, 3, 20, 20, 25]
"""
'''
向前传播时需要将相对坐标转换到grid绝对坐标系中
'''
z = [] # inference output
for i in range(self.nl):
x[i] = self.m[i](x[i]) # conv
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
# 维度重排列: bs, 先验框组数, 检测框行数, 检测框列数, 属性数 + 分类数
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous() # contiguous 将数据保证内存中位置连续
if not self.training: # inference
# 构造网格
# 因为推理返回的不是归一化后的网格偏移量 需要再加上网格的位置 得到最终的推理坐标 再送入nms
# 所以这里构建网格就是为了记录每个grid的网格坐标 方面后面使用
# 换输入后重新设定锚框
if self.dynamic or self.grid[i].shape[2:4] != x[i].shape[2:4]:
self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i)
if isinstance(self, Segment): # (boxes + masks)
xy, wh, conf, mask = x[i].split((2, 2, self.nc + 1, self.no - self.nc - 5), 4)
xy = (xy.sigmoid() * 2 + self.grid[i]) * self.stride[i] # xy
wh = (wh.sigmoid() * 2) ** 2 * self.anchor_grid[i] # wh
y = torch.cat((xy, wh, conf.sigmoid(), mask), 4)
else: # Detect (boxes only)
'''
按损失函数的回归方式来转换坐标
'''
xy, wh, conf = x[i].sigmoid().split((2, 2, self.nc + 1), 4)
# stride: 是一个grid cell的实际尺寸
# 经过sigmoid, 值范围变成了(0-1),下一行代码将值变成范围(-0.5,1.5)
xy = (xy * 2 + self.grid[i]) * self.stride[i] # xy
# 范围变成(0-4)倍,设置为4倍的原因是下层的感受野是上层的2倍
# 因下层注重检测大目标,相对比上层而言,计算量更小,4倍是一个折中的选择
wh = (wh * 2) ** 2 * self.anchor_grid[i] # wh
y = torch.cat((xy, wh, conf), 4)
# z是一个tensor list 三个元素 分别是[1, 19200, 85] [1, 4800, 85] [1, 1200, 85]
z.append(y.view(bs, self.na * nx * ny, self.no))
return x if self.training else (torch.cat(z, 1),) if self.export else (torch.cat(z, 1), x)
forward
方法负责将输入数据x
传递通过网络。- 对于每个检测层,执行卷积操作,计算输出。
view
和permute
调整输出的形状,以便于后续处理。- 最后返回适合推理或训练的数据格式。
这段代码是一个深度学习模型中的 forward
方法,主要用于处理输入数据并生成预测结果,具体是针对 YOLOv5 模型的检测头。
这段代码主要是对三个feature map分别进行处理:(n, 255, 80, 80),(n, 255, 40, 40),(n, 255, 20, 20)
首先进行for循环,每次i的循环,产生一个z。维度重排列:(n, 255, , ) -> (n, 3, nc+5, ny, nx) -> (n, 3, ny, nx, nc+5),三层分别预测了80*80、40*40、20*20次。
接着 构造网格,因为推理返回的不是归一化后的网格偏移量,需要再加上网格的位置,得到最终的推理坐标,再送入nms。所以这里构建网格就是为了纪律每个grid的网格坐标 方面后面使用
最后按损失函数的回归方式来转换坐标,利用sigmoid激活函数计算定位参数,cat(dim=-1)为直接拼接。注意: 训练阶段直接返回x ,而预测阶段返回3个特征图拼接的结果
-
初始化输出容器:
z = [] # inference output
这里初始化一个空列表
z
,用于存储每个检测层的推理输出。 -
循环遍历检测层:
for i in range(self.nl):
循环
self.nl
次,self.nl
是检测模型的层数。 -
卷积计算:
x[i] = self.m[i](x[i]) # conv
对每一层的输入
x[i]
进行卷积操作,更新x[i]
。 -
获取输入的形状:
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
获取当前输入的批次大小
bs
、通道数_
(在这个上下文中用于丢弃)、高度ny
和宽度nx
。 -
重塑和排列输出:
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
将
x[i]
重新变形为(bs, self.na, self.no, ny, nx)
的形状,然后排列维度以符合后续处理的要求。 -
推理阶段的处理:
if not self.training: # inference
判断当前模式是否为推理模式,如果是则执行相关处理。
-
动态网格创建:
if self.dynamic or self.grid[i].shape[2:4] != x[i].shape[2:4]: self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i)
如果动态模式为真,或者当前网格的形状与输入shape不一致,则生成新的网格和锚框。
-
分割输出(针对分割模型):
if isinstance(self, Segment): # (boxes + masks) xy, wh, conf, mask = x[i].split((2, 2, self.nc + 1, self.no - self.nc - 5), 4) xy = (xy.sigmoid() * 2 + self.grid[i]) * self.stride[i] # xy wh = (wh.sigmoid() * 2) ** 2 * self.anchor_grid[i] # wh y = torch.cat((xy, wh, conf.sigmoid(), mask), 4)
这一段代码主要用于在YOLOv5模型的检测过程中处理模型输出。下面对每一行代码进行逐步分解和详细解释:
xy, wh, conf, mask = x[i].split((2, 2, self.nc + 1, self.no - self.nc - 5), 4)
该行代码将模型的输出张量
x[i]
切分成四个部分:xy
,wh
,conf
和mask
。split
方法按指定的切分大小返回多个张量。这里的切分大小依次为(2, 2, self.nc + 1, self.no - self.nc - 5)
,代表:xy
:表示坐标(x, y)大小为 2。wh
:表示宽高(w, h)大小为 2。conf
:表示置信度,其大小为类别数self.nc + 1
(其中 +1 通常是目标的置信度)。mask
:表示分割掩码,其大小为输出总数self.no - self.nc - 5
。参数
4
表示沿着哪个维度进行切分(通常是最后一个维度)。xy = (xy.sigmoid() * 2 + self.grid[i]) * self.stride[i] # xy
对于
xy
坐标进行处理。首先通过sigmoid()
函数将值映射到(0, 1)的范围内。然后乘以 2,并加上
self.grid[i]
,这一步的目的是将预测值转换到输入图像的实际坐标系统中。最后乘以
self.stride[i]
,这个操作将坐标从特征图尺寸转换回原始图像尺寸,确保可以正确定位目标。wh = (wh.sigmoid() * 2) ** 2 * self.anchor_grid[i] # wh
处理
wh
,宽高。首先同样通过sigmoid()
将值映射到(0, 1)范围内。然后乘以 2,接着通过平方 (
** 2
) 进行缩放,这样可以保持比例关系。最后乘以
self.anchor_grid[i]
,使用默认的锚框大小,确保预测的宽高合适目标的实际尺寸。y = torch.cat((xy, wh, conf.sigmoid(), mask), 4)
该行代码将之前处理的
xy
,wh
,conf
和mask
重新组合成一个完整的输出张量y
。使用
torch.cat()
方法沿着指定维度(此处为 4)连接四个张量,以便后续处理。代码总结
此段代码的主要功能是从YOLOv5模型的输出中提取目标检测的关键信息。它对目标的坐标、宽高、置信度和分割掩码进行了处理,并最终将这些信息组合成一个完整的输出,准备进行后续的目标识别和定位。通过这一过程,模型可以输出每个检测到的物体在图像中的位置、尺寸及其类别的置信度,从而有效地进行目标检测任务。
-
处理检测模型的输出:
else: # Detect (boxes only) xy, wh, conf = x[i].sigmoid().split((2, 2, self.nc + 1), 4) xy = (xy * 2 + self.grid[i]) * self.stride[i] # xy wh = (wh * 2) ** 2 * self.anchor_grid[i] # wh y = torch.cat((xy, wh, conf), 4)
这段代码是YOLOv5模型中处理检测输出的关键部分,具体的解读分为以下几个步骤:
sigmoid激活及分割:
xy, wh, conf = x[i].sigmoid().split((2, 2, self.nc + 1), 4)
在这行代码中,
x[i]
代表着模型输出的一个特定层(第i层)的特征图。首先,应用sigmoid
激活函数以确保模型输出的值限制在0到1之间,特别是在YOLO模型中用于预测坐标和置信度。接着,使用split
方法将sigmoid的输出分割成三个部分:xy
:目标框的中心坐标(通常是相对于网格的坐标,包含x和y两个值)。wh
:目标框的宽高(包含宽和高两个值)。conf
:目标的置信度(以及类别概率,通常是类别数量加一(包含置信度))。计算目标框的坐标:
xy = (xy * 2 + self.grid[i]) * self.stride[i] # xy
这行代码首先将预测的xy坐标乘以2,然后加上当前网格的xy坐标(
self.grid[i]
)。这样做是因为YOLO在训练过程中使用了一个特定的坐标变换,确保中心坐标可以正确地表示在图像上的位置。最后乘以对应的stride[i]
,将坐标转换回原图像的尺度。计算目标框的宽高:
wh = (wh * 2) ** 2 * self.anchor_grid[i] # wh
类似地,预测的宽高(wh)首先乘以2并平方,最后乘以定义的锚框(
self.anchor_grid[i]
),用以计算最终的目标框尺寸。这样可以确保宽高的比例和锚框的比例一致,从而提高检测的准确性。组合所有信息:
y = torch.cat((xy, wh, conf), 4)
最后,通过
torch.cat
将xy
、wh
和conf
这三部分信息在最后一个维度上连接起来,形成一个完整的输出张量y
。这个张量将包含每个检测框的中心坐标、宽高和置信度信息。总结
这段代码主要负责将YOLOv5模型在某一特征层的输出转换为实际目标检测框的信息。通过将sigmoid激活结合网格坐标和锚框尺寸,模型能够有效地将相对坐标转换为绝对坐标,从而生成准确的检测结果。最终的输出包含了每个检测框的位置信息(中心坐标和尺寸)以及该框的置信度,供后续的处理步骤使用(例如进行非极大值抑制等)。
-
存储当前层的输出:
z.append(y.view(bs, self.na * nx * ny, self.no))
将当前层的输出
y
添加到z
列表中,改变输出形状以便后续处理。 -
返回结果:
return x if self.training else (torch.cat(z, 1),) if self.export else (torch.cat(z, 1), x)
根据当前模式返回不同的结果:
- 如果是训练模式,直接返回输入
x
。 - 如果是导出模式,将所有层的输出按列合并返回。
- 否则,返回所有层的合并输出和输入
x
。
- 如果是训练模式,直接返回输入
该 forward
方法是深度学习检测模型(YOLOv5)的核心部分,处理输入数据以生成目标检测的结果。它包括卷积计算、形状重塑、动态网格生成和输出的整理,并根据推理或训练模式返回不同的结果。该方法确保了在推理阶段输出对象检测所需的信息,如边界框坐标、宽高和置信度,适用于实时物体检测任务。
以上部分的原理部分,可以先翻看yolov3论文中对于anchor box回归的介绍:
这里的bx∈[Cx,Cx+1],by∈[Cy,Cy+1],bw∈(0,+∞),bh∈(0,+∞)
而yolov5里这段公式变成了:
使得bx∈[Cx-0.5,Cx+1.5],by∈[Cy-0.5,Cy+1.5],bw∈[0,4pw],bh∈[0,4ph]
这是因为在对anchor box回归时,用了三个grid的范围来预测,而并非1个,可以从loss.py的代码看出:
g = 0.5 # bias
off = torch.tensor([[0, 0],
[1, 0], [0, 1], [-1, 0], [0, -1], # j,k,l,m
# [1, 1], [1, -1], [-1, 1], [-1, -1], # jk,jm,lk,lm
], device=targets.device).float() * g # offsets
# Offsets
gxy = t[:, 2:4] # grid xy
gxi = gain[[2, 3]] - gxy # inverse
j, k = ((gxy % 1. < g) & (gxy > 1.)).T
l, m = ((gxi % 1. < g) & (gxi > 1.)).T
j = torch.stack((torch.ones_like(j), j, k, l, m))
t = t.repeat((5, 1, 1))[j]
offsets = (torch.zeros_like(gxy)[None] + off[:, None])[j]
这段代码的大致意思是,当标签在grid左侧半部分时,会将标签往左偏移0.5个grid,在上、下、右侧同理。具体如图所示:
grid B中的标签在右上半部分,所以标签偏移0.5个gird到E中,A,B,C,D同理,即每个网格除了回归中心点在该网格的目标,还会回归中心点在该网格附近周围网格的目标。以E左上角为坐标(Cx,Cy),所以bx∈[Cx-0.5,Cx+1.5],by∈[Cy-0.5,Cy+1.5],而bw∈[0,4pw],bh∈[0,4ph]应该是为了限制anchor的大小。
这一策略可以提高召回率(因为每个grid的预测范围变大了),但会略微降低精确度,总体提升mAP。
2.4 网格生成方法 _make_grid
:
def _make_grid(self, nx=20, ny=20, i=0, torch_1_10=check_version(torch.__version__, '1.10.0')):
d = self.anchors[i].device
t = self.anchors[i].dtype
shape = 1, self.na, ny, nx, 2 # grid shape
y, x = torch.arange(ny, device=d, dtype=t), torch.arange(nx, device=d, dtype=t)
yv, xv = torch.meshgrid(y, x, indexing='ij') if torch_1_10 else torch.meshgrid(y, x)
grid = torch.stack((xv, yv), 2).expand(shape) - 0.5 # add grid offset
anchor_grid = (self.anchors[i] * self.stride[i]).view((1, self.na, 1, 1, 2)).expand(shape)
return grid, anchor_grid
make_grid
生成检测的网格和相应的锚点网格。- 使用
meshgrid
来创建网格,然后计算出带有偏移量的坐标。 - 返回生成的网格和锚点网格。
这段代码主要是将相对坐标转换到grid绝对坐标系。
首先构造网格标尺坐标
- indexing='ij' : 表示的是i是同一行,j表示同一列
- indexing='xy' : 表示的是x是同一列,y表示同一行
grid复制成3倍,因为是3个框。anchor_grid是每个anchor宽高。anchor_grid = (self.anchors[i].clone() * self.stride[i])。注意这里为啥要乘呢?因为在外面已经把anchors给除了对应的下采样率,这里再乘回来。
这段代码定义了一个名为 _make_grid
的方法,用于生成 YOLOv5 中的网格和锚框网格。下面是对代码的逐步分解与详细解释:
-
方法参数:
nx=20
和ny=20
:分别表示网格在x轴和y轴的数量。i=0
:表示当前处理的锚框索引。torch_1_10=check_version(torch.__version__, '1.10.0')
:检查当前 PyTorch 版本是否为 1.10.0 或以上。
-
获取锚框的信息:
d = self.anchors[i].device t = self.anchors[i].dtype
d
:获取当前锚框的设备信息(如 CPU 或 GPU)。t
:获取当前锚框的数据类型(如浮点数)。
-
设置网格的形状:
shape = 1, self.na, ny, nx, 2 # grid shape
shape
:定义生成的网格的形状,包括批量大小(1),锚框数量 (self.na
),y轴和x轴的数量(ny
和nx
),以及2个坐标值(x 和 y)。
-
生成网格坐标:
y, x = torch.arange(ny, device=d, dtype=t), torch.arange(nx, device=d, dtype=t)
y
和x
:分别生成从0到ny-1
和0到nx-1
的序列,作为网格的纵轴和横轴坐标。
-
创建网格:
yv, xv = torch.meshgrid(y, x, indexing='ij') if torch_1_10 else torch.meshgrid(y, x)
meshgrid
:根据y
和x
的值生成网格坐标。如果 PyTorch 版本是 1.10.0 以上,使用ij
索引方式。
-
堆叠网格并调整形状:
grid = torch.stack((xv, yv), 2).expand(shape) - 0.5
grid
:将生成的xv
和yv
堆叠成一个三维张量,并扩展成预定义的shape
,之后减去0.5以调整网格的偏移。- grid --> (20, 20, 2), 复制成3倍,因为是三个框 -> (3, 20, 20, 2)
-
生成锚框网格:
anchor_grid = (self.anchors[i] * self.stride[i]).view((1, self.na, 1, 1, 2)).expand(shape)
anchor_grid
:对指定的锚框参数进行缩放(与当前网格的步幅相乘),并调整形状以符合网格的大小。- anchor_grid即每个格子对应的anchor宽高,stride是下采样率,三层分别是8,16,32
-
返回结果:
return grid, anchor_grid
- 返回生成的网格和锚框网格。
这段代码的主要功能是生成一个二维网格以及相应的锚框网格,这使得 YOLOv5 在目标检测时能够有效地对图像进行分层,并计算每个网格中的目标位置。它通过提供网格坐标和调整后的锚框,使模型能够在不同的尺度和位置上进行预测,从而提高检测精度。这是 YOLOv5 实现实时目标检测的关键部分。
2.5 代码总结
总体而言,这段代码实现了 YOLOv5 检测头部的主体功能。它定义了如何从输入的特征图中提取目标检测的输出,包括每个锚点的边界框位置、宽高和置信度。通过定义卷积层和计算相应的网格,该模块能够高效地处理目标检测任务,并为实际推理做好准备。
三、DetectionModel函数
class DetectionModel(BaseModel):
# YOLOv5 detection model
def __init__(self, cfg='yolov5s.yaml', ch=3, nc=None, anchors=None): # model, input channels, number of classes
super().__init__()
if isinstance(cfg, dict):
self.yaml = cfg # model dict
else: # is *.yaml
import yaml # for torch hub
self.yaml_file = Path(cfg).name
with open(cfg, encoding='ascii', errors='ignore') as f:
self.yaml = yaml.safe_load(f) # model dict
# Define model
ch = self.yaml['ch'] = self.yaml.get('ch', ch) # input channels
# print(ch)
# # print(nc)
# # print(anchors)
if nc and nc != self.yaml['nc']:
LOGGER.info(f"Overriding model.yaml nc={self.yaml['nc']} with nc={nc}")
self.yaml['nc'] = nc # override yaml value
if anchors:
LOGGER.info(f'Overriding model.yaml anchors with anchors={anchors}')
self.yaml['anchors'] = round(anchors) # override yaml value
self.model, self.save = parse_model(deepcopy(self.yaml), ch=[ch]) # model, savelist
self.names = [str(i) for i in range(self.yaml['nc'])] # default names
self.inplace = self.yaml.get('inplace', True)
# Build strides, anchors
m = self.model[-1] # Detect()
if isinstance(m, (Detect, Segment)):
s = 256 # 2x min stride
m.inplace = self.inplace
forward = lambda x: self.forward(x)[0] if isinstance(m, Segment) else self.forward(x)
m.stride = torch.tensor([s / x.shape[-2] for x in forward(torch.zeros(1, ch, s, s))]) # forward
check_anchor_order(m)
m.anchors /= m.stride.view(-1, 1, 1)
self.stride = m.stride
self._initialize_biases() # only run once
# Init weights, biases
initialize_weights(self)
self.info()
LOGGER.info('')
def forward(self, x, augment=False, profile=False, visualize=False):
if augment:
return self._forward_augment(x) # augmented inference, None
return self._forward_once(x, profile, visualize) # single-scale inference, train
def _forward_augment(self, x):
img_size = x.shape[-2:] # height, width
s = [1, 0.83, 0.67] # scales
f = [None, 3, None] # flips (2-ud, 3-lr)
y = [] # outputs
for si, fi in zip(s, f):
xi = scale_img(x.flip(fi) if fi else x, si, gs=int(self.stride.max()))
yi = self._forward_once(xi)[0] # forward
# cv2.imwrite(f'img_{si}.jpg', 255 * xi[0].cpu().numpy().transpose((1, 2, 0))[:, :, ::-1]) # save
yi = self._descale_pred(yi, fi, si, img_size)
y.append(yi)
y = self._clip_augmented(y) # clip augmented tails
return torch.cat(y, 1), None # augmented inference, train
def _descale_pred(self, p, flips, scale, img_size):
# de-scale predictions following augmented inference (inverse operation)
if self.inplace:
p[..., :4] /= scale # de-scale
if flips == 2:
p[..., 1] = img_size[0] - p[..., 1] # de-flip ud
elif flips == 3:
p[..., 0] = img_size[1] - p[..., 0] # de-flip lr
else:
x, y, wh = p[..., 0:1] / scale, p[..., 1:2] / scale, p[..., 2:4] / scale # de-scale
if flips == 2:
y = img_size[0] - y # de-flip ud
elif flips == 3:
x = img_size[1] - x # de-flip lr
p = torch.cat((x, y, wh, p[..., 4:]), -1)
return p
def _clip_augmented(self, y):
# Clip YOLOv5 augmented inference tails
nl = self.model[-1].nl # number of detection layers (P3-P5)
g = sum(4 ** x for x in range(nl)) # grid points
e = 1 # exclude layer count
i = (y[0].shape[1] // g) * sum(4 ** x for x in range(e)) # indices
y[0] = y[0][:, :-i] # large
i = (y[-1].shape[1] // g) * sum(4 ** (nl - 1 - x) for x in range(e)) # indices
y[-1] = y[-1][:, i:] # small
return y
def _initialize_biases(self, cf=None): # initialize biases into Detect(), cf is class frequency
# https://arxiv.org/abs/1708.02002 section 3.3
# cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1.
m = self.model[-1] # Detect() module
for mi, s in zip(m.m, m.stride): # from
b = mi.bias.view(m.na, -1) # conv.bias(255) to (3,85)
b.data[:, 4] += math.log(8 / (640 / s) ** 2) # obj (8 objects per 640 image)
b.data[:, 5:5 + m.nc] += math.log(0.6 / (m.nc - 0.99999)) if cf is None else torch.log(cf / cf.sum()) # cls
mi.bias = torch.nn.Parameter(b.view(-1), requires_grad=True)
这段代码定义了一个名为 DetectionModel
的类,这是YOLOv5(一个流行的物体检测模型)的实现之一。下面是对代码的逐步分解和详细解释,以及最终的总结。
3.1. 类的定义与初始化
class DetectionModel(BaseModel):
# YOLOv5 detection model
def __init__(self, cfg='yolov5s.yaml', ch=3, nc=None, anchors=None):
super().__init__()
DetectionModel
类继承自BaseModel
,表示这是一个检测模型。__init__
方法接收模型配置文件 (cfg
)、输入通道数 (ch
)、类别数 (nc
) 和锚框 (anchors
) 作为参数。
3.2. 处理配置文件
if isinstance(cfg, dict):
self.yaml = cfg # model dict
else: # is *.yaml
import yaml # for torch hub
self.yaml_file = Path(cfg).name
with open(cfg, encoding='ascii', errors='ignore') as f:
self.yaml = yaml.safe_load(f) # model dict
- 代码检查
cfg
是否为字典。如果是,则将其直接赋值给self.yaml
。 - 如果是文件名(字符串),则通过
yaml
库读取文件,将内容解析为字典。
这段代码的功能是处理输入的模型配置参数 cfg
,并将其转换为一个字典格式的模型配置(self.yaml
)。以下是对代码逐步分解的详细解释:
-
判断输入类型:
if isinstance(cfg, dict):
这行代码检查
cfg
是否为字典类型。如果是,它表示用户已经以字典格式提供了模型的配置。 -
处理字典输入:
self.yaml = cfg # model dict
如果
cfg
是字典,代码将直接将其赋值给self.yaml
,这意味着后续代码可以直接使用self.yaml
来访问模型的配置。 -
处理 YAML 文件输入:
else: # is *.yaml import yaml # for torch hub
如果
cfg
不是字典类型,意味着它是一个指向 YAML 文件的路径。这段代码执行了文件的导入,以便后面读取 YAML 文件内容。 -
获取文件名:
self.yaml_file = Path(cfg).name
这行代码使用
Path
对象从文件路径中提取文件名,将其存储为self.yaml_file
属性,提供对文件名的直接访问。 -
读取并解析 YAML 文件:
with open(cfg, encoding='ascii', errors='ignore') as f: self.yaml = yaml.safe_load(f) # model dict
这部分使用
with
语句打开指定的 YAML 文件。yaml.safe_load(f)
将读取的文件内容解析为 Python 字典,并将其赋值给self.yaml
。safe_load
方法用于确保安全地加载 YAML 数据。
这段代码的主要功能是根据输入的模型配置参数(可以是字典或 YAML 文件路径)来初始化模型配置。首先,它检查 cfg
是否为字典,若是,则直接使用该字典。如果是文件路径,则读取文件内容并解析为字典格式,便于后续使用。这样的设计使得用户可以灵活地通过两种方式指定模型配置,提高了代码的通用性和灵活性。
3.3. 定义模型参数
ch = self.yaml['ch'] = self.yaml.get('ch', ch) # input channels
if nc and nc != self.yaml['nc']:
LOGGER.info(f"Overriding model.yaml nc={self.yaml['nc']} with nc={nc}")
self.yaml['nc'] = nc # override yaml value
if anchors:
LOGGER.info(f'Overriding model.yaml anchors with anchors={anchors}')
self.yaml['anchors'] = round(anchors) # override yaml value
- 通过
self.yaml
获取输入通道数,如果未指定则使用默认值ch
。 - 如果指定了类别数并且与配置文件中的不匹配,则记录日志并覆盖配置文件中的值。
- 如果提供了锚框,也记录日志并覆盖配置文件中的锚框值。
这段代码的目的是定义一个YOLOv5模型,并进行一些模型参数的初始化。下面逐步分解并详细解释代码中的每一部分:
-
输入通道的定义:
ch = self.yaml['ch'] = self.yaml.get('ch', ch) # input channels
这里获取模型配置文件(
self.yaml
)中的输入通道数(ch
)。如果在配置文件中没有定义,则使用传入的ch
值。通过这种方式确保了模型的输入通道数量是确定的。 -
类别数的覆盖:
if nc and nc != self.yaml['nc']: LOGGER.info(f"Overriding model.yaml nc={self.yaml['nc']} with nc={nc}") self.yaml['nc'] = nc # override yaml value
这段代码检查是否传入了类别数
nc
,并且其值是否与模型配置文件中的类别数(self.yaml['nc']
)不同。如果两者不同,则用新的类别数(nc
)覆盖原有的值,并记录这个操作的信息。这确保了模型使用的是最新的类别数量,便于调整模型以处理不同的任务。 -
锚点的覆盖:
if anchors: LOGGER.info(f'Overriding model.yaml anchors with anchors={anchors}') self.yaml['anchors'] = round(anchors) # override yaml value
如果新提供了
anchors
(锚点),则通过日志记录这个操作,并将配置文件中的锚点值替换为传入的值。这一过程同样保证了锚点符合最新要求,适应不同形状的目标检测任务。 -
模型的解析与创建:
self.model, self.save = parse_model(deepcopy(self.yaml), ch=[ch]) # model, savelist
调用
parse_model
函数,并传入模型的YAML配置(深拷贝以防止对原配置的修改)和输入通道数。parse_model
函数将解析模型结构并构建网络层,返回构造的模型和需保存的层信息。 -
类别名称的初始化:
self.names = [str(i) for i in range(self.yaml['nc'])] # default names
这里创建一个默认的类别名称列表,名称为数字字符串形式,由类别的数量决定。这样可以为每个检测类别提供一个便于使用的名称。
-
执行位置的设置:
self.inplace = self.yaml.get('inplace', True)
最后,获取配置文件中的
inplace
参数,并默认设置为True
。这个参数通常用于决定模型是否使用就地操作,这对内存管理和计算效率有影响。
这段代码主要功能是在初始化YOLOv5的检测模型时,设置和检验网络结构的各种参数,包括输入通道数、类别数、锚点、模型结构以及类别名称等。通过这些步骤,模型确保能够适应不同的检测任务需求,从而达到可调性和灵活性的目的。
3.4. 解析模型结构
self.model, self.save = parse_model(deepcopy(self.yaml), ch=[ch]) # model, savelist
self.names = [str(i) for i in range(self.yaml['nc'])] # default names
self.inplace = self.yaml.get('inplace', True)
- 调用
parse_model
函数从 YAML 字典构建模型,并返回模型和保存的层列表。 - 创建类别名称的列表,默认以索引字符串命名。
这段代码主要功能是为YOLOv5模型的最后一层(通常是检测层)初始化步幅(stride)、锚框(anchors),并调整其参数以适应输入图像大小。
-
获取最后一层模型:
m = self.model[-1] # Detect()
这行代码获取YOLOv5模型中的最后一层,通常是检测层(
Detect
或Segment
)。 -
检查类型:
if isinstance(m, (Detect, Segment)):
这里检查最后一层是不是
Detect
或Segment
类型,以确定是否需要进行后续操作。 -
设置初始步幅:
s = 256 # 2x min stride
定义一个变量
s
,初值设置为256
,这个值表示最小步幅的两倍,步幅用于影响网络输出特征图的分辨率。 -
设置是否使用就地操作:
m.inplace = self.inplace
将实例中的
inplace
属性赋值给模型m
,如果设置为True
,可以提高模型的内存使用效率。 -
定义向前传播函数:
forward = lambda x: self.forward(x)[0] if isinstance(m, Segment) else self.forward(x)
定义一个匿名函数
forward
,根据最后一层的类型决定如何进行前向传播,从而获得输出。对于Segment
类型的模型,返回第一个输出,其它类型则返回全部输出。 -
计算步幅:
m.stride = torch.tensor([s / x.shape[-2] for x in forward(torch.zeros(1, ch, s, s))]) # forward
使用刚才定义的
forward
函数,以一个全零的张量作为输入进行前向传播,计算每个输出特征图的步幅。具体地,每个特征图的步幅是定义的s
除以其高度。 -
检查锚框顺序:
check_anchor_order(m)
调用
check_anchor_order
函数,确保模型中的锚框顺序正确,以便进行有效的物体检测。 -
调整锚框:
m.anchors /= m.stride.view(-1, 1, 1)
通过将锚框除以步幅来调整锚框的大小,以确保它们可以适应不同的特征图大小。
-
记录步幅:
self.stride = m.stride
将计算出的步幅记录到实例属性中,以便在后续的推理或训练中使用。
-
初始化偏置:
self._initialize_biases() # only run once
调用
_initialize_biases
方法来初始化模型的偏置参数,该方法只需运行一次。
这段代码的核心功能是初始化和调整YOLOv5模型最后一层的步幅和锚框。通过计算步幅并调整锚框的大小,以确保模型能够正确地处理输入图像,进行有效的物体检测。同时,它还保证了模型在推理时的内存效率,并对偏置进行初始化,以提升模型的学习能力。
3.5. 构建步幅与锚框
m = self.model[-1] # Detect()
if isinstance(m, (Detect, Segment)):
s = 256 # 2x min stride
m.inplace = self.inplace
forward = lambda x: self.forward(x)[0] if isinstance(m, Segment) else self.forward(x)
m.stride = torch.tensor([s / x.shape[-2] for x in forward(torch.zeros(1, ch, s, s))]) # forward
check_anchor_order(m)
m.anchors /= m.stride.view(-1, 1, 1)
self.stride = m.stride
self._initialize_biases() # only run once
- 取出模型的最后一层(通常是检测层)。
- 如果这一层是
Detect
或Segment
类的实例,初始化模型的步幅和锚框。 - 使用零张量进行前向传播计算步幅,并检查锚框的顺序。
- 对锚框进行调整,最后初始化 Bias。
以下是对所提供代码段的逐步分解和详细解释:
-
获取模型的最后一层:
m = self.model[-1] # Detect()
这行代码获取当前模型中的最后一层,通常是检测层(Detect或Segment)。
self.model
是一个包含所有层的序列,self.model[-1]
指向最后一层。 -
检查最后一层的类型:
if isinstance(m, (Detect, Segment)):
这行代码检查最后一层是否为Detect或Segment类型。如果是,这段代码将对这层进行进一步的初始化和处理。
-
设置最小步幅:
s = 256 # 2x min stride
变量
s
被设置为256,用于计算步幅。在目标检测中,步幅是指在输入图像中,特征图的尺寸与输入图像尺寸的比例。 -
设置就地操作选项:
m.inplace = self.inplace
这行代码将
self.inplace
的值赋给模型的就地操作选项。就地操作可以节省内存,减少模型的内存占用。 -
定义前向传播函数:
forward = lambda x: self.forward(x)[0] if isinstance(m, Segment) else self.forward(x)
这行代码定义了一个前向传播的lambda函数,它用于执行模型的前向传播。如果最后一层是Segment类型,则返回前向传播的第一个输出。否则,直接返回前向传播的结果。
-
计算步幅:
m.stride = torch.tensor([s / x.shape[-2] for x in forward(torch.zeros(1, ch, s, s))]) # forward
通过在输入为零图像的情况下调用前向传播,获得每个特征图的高度值(
x.shape[-2]
)。然后,步幅被计算为s
与每个特征图的高度之比,并将其作为张量保存在m.stride
中。 -
检查锚框顺序:
check_anchor_order(m)
这行代码调用一个函数来检查锚框的顺序,以确保它们的定义是正确的。在YOLO模型中,锚框是用于预测目标位置和大小的预定义框。
-
根据步幅调整锚框:
m.anchors /= m.stride.view(-1, 1, 1)
这行代码将锚框进行缩放,以适应特征图的步幅。将锚框的尺寸除以步幅,使得锚框在特征图上的位置和尺寸都是正确的。
-
保存步幅信息:
self.stride = m.stride
将计算得到的步幅保存到模型的属性中,以便后续使用。
-
初始化偏差:
self._initialize_biases() # only run once
这行代码初始化模型中检测层的偏差值。此操作只需执行一次,以确保模型的初始状态良好。
这段代码主要的功能是初始化YOLO模型的最后一层(检测层),计算和设置步幅以及锚框的尺寸,同时进行必要的属性设置和偏差初始化。这些步骤是目标检测模型推理过程中的关键要素,确保模型能够在输入图像上正确进行预测,并且能够根据特征图中的位置准确地调整锚框。
3.6. 初始化权重与偏差
initialize_weights(self)
self.info()
LOGGER.info('')
- 调用
initialize_weights
函数初始化模型权重。 - 调用
info
方法打印模型信息,记录日志。
这段代码是用来初始化模型的权重和偏置,并打印模型的信息。接下来我们逐步分解并详细解释这三行代码:
-
initialize_weights(self)
:- 这一行调用了
initialize_weights
函数来初始化模型的权重和偏置。假设这个函数是通过某种方法(比如 Xavier 初始化或 Kaiming 初始化)来设置网络各个层的初始权重。初始化权重对于训练深度学习模型至关重要,它可以帮助模型更快地收敛,并提高最终的性能。
- 这一行调用了
-
self.info()
:- 这一行调用了
info
方法。根据上下文,此方法的主要作用是打印当前模型的基本信息,比如模型的结构、参数数量、输入大小等。这样有助于用户了解模型的基本配置,从而可以判断是否符合他们的需求或进行相应的调整。
- 这一行调用了
-
LOGGER.info('')
:- 这一行使用了
LOGGER
来记录一个空字符串。通常,这行代码的目的是在日志中添加一个空行,使得日志的输出更加整洁和可读。这样可以分隔不同的日志信息,使得阅读日志变得更加方便。
- 这一行使用了
这段代码的主要功能是为一个深度学习模型(如 YOLOv5)初始化其权重和偏置,并打印出模型的基本信息以供参考。当新模型被创建时,权重的初始化是非常关键的一步,能够有效地影响模型的训练过程和最终性能,而模型信息的打印有助于开发者或研究者理解模型的结构和参数配置。
3.7. 前向传播
def forward(self, x, augment=False, profile=False, visualize=False):
if augment:
return self._forward_augment(x) # augmented inference, None
return self._forward_once(x, profile, visualize) # single-scale inference, train
- 定义
forward
方法,接收输入x
。 - 根据
augment
参数决定是否进行增强前向传播(如数据增强)或单尺度前向传播。
这段代码是一个条件语句,出现在某个类的方法中,具体的上下文是一个深度学习模型的前向传播过程。我们逐步分解并详细解释这段代码。
-
if augment:
- 这是一个条件判断,检查
augment
变量的值。如果augment
为真(True),则执行第一个返回语句;如果为假(False),则执行第二个返回语句。
- 这是一个条件判断,检查
-
return self._forward_augment(x)
- 如果
augment
为真,调用当前类的_forward_augment
方法,并将输入x
作为参数传入。这个方法通常用于实现数据增强(augmented inference),目的是在推理(inference)阶段对输入数据进行一些变换或处理,采用不同的视图和比例来提升模型的鲁棒性和准确性。通过增强训练可以帮助模型更好地泛化。
- 如果
-
return self._forward_once(x, profile, visualize)
- 如果
augment
为假,调用当前类的_forward_once
方法,将输入x
,以及可能的profile
和visualize
参数传入。这个方法用于进行单尺度(single-scale)推理,通常是在训练阶段使用的,执行一次前向传播(forward pass),并返回预测结果。
- 如果
这段代码主要实现了根据 augment
参数的值选择不同的前向传播方法。在数据增强情况下,调用 _forward_augment
方法,以提高模型在推理过程中的多样性和健壮性;而在常规推理情况下,则调用 _forward_once
方法来执行单尺度的前向传播。这种设计可以灵活地处理数据的不同输入形式,以期提高模型的性能和效果。
3.8. 数据增强的前向传播
def _forward_augment(self, x):
img_size = x.shape[-2:] # height, width
s = [1, 0.83, 0.67] # scales
f = [None, 3, None] # flips (2-ud, 3-lr)
y = [] # outputs
for si, fi in zip(s, f):
xi = scale_img(x.flip(fi) if fi else x, si, gs=int(self.stride.max()))
yi = self._forward_once(xi)[0] # forward
yi = self._descale_pred(yi, fi, si, img_size)
y.append(yi)
y = self._clip_augmented(y) # clip augmented tails
return torch.cat(y, 1), None # augmented inference, train
- 处理数据增强,通过不同的缩放和翻转方式生成增强样本,并使用
_forward_once
做前向推理。 - 将结果进行去缩放,并剪切增强后结果,最终返回拼接后的结果。
这段代码是 YOLOv5 模型中的一个方法,用于进行增强推理。下面将逐步分解并详细解释每一部分。
-
获取图像大小
img_size = x.shape[-2:] # height, width
x
是输入的图像张量,通常其形状为(batch_size, channels, height, width)
。此行代码提取出输入图像的高和宽。
-
定义缩放比例和翻转方式
s = [1, 0.83, 0.67] # scales f = [None, 3, None] # flips (2-ud, 3-lr)
s
是一个列表,定义了三种不同的缩放比例(1、0.83 和 0.67),用于多尺度推理。f
列表则指定了对应的翻转方式,None
表示不翻转,3
表示左右翻转。
-
初始化输出列表
y = [] # outputs
y
将用于存储经过推理处理后的结果。
-
循环处理每个缩放和翻转组合
for si, fi in zip(s, f):
zip(s, f)
创建一个迭代器,将缩放比例和翻转方式配对,接下来的代码将遍历这对值。
-
图像缩放和翻转
xi = scale_img(x.flip(fi) if fi else x, si, gs=int(self.stride.max()))
- 如果存在翻转(即
fi
不为None
),则先对输入图像x
进行翻转;否则,使用原始图像。 - 然后,
scale_img
函数将对图像xi
进行根据缩放比例si
的重缩放。
- 如果存在翻转(即
-
前向传播
yi = self._forward_once(xi)[0] # forward
self._forward_once(xi)
调用前向传播方法,传入缩放后的图像xi
,返回的结果yi
是模型的预测输出。
-
逆缩放预测结果
yi = self._descale_pred(yi, fi, si, img_size)
- 将经过模型处理的结果
yi
进行逆缩放,恢复到原始图像尺寸。这一步对于在使用增强方法(如缩放和翻转)后,确保输出结果与原始输入匹配至关重要。
- 将经过模型处理的结果
-
收集结果
y.append(yi)
- 将处理后的结果
yi
添加到输出列表y
中。
- 将处理后的结果
-
剪裁增强推理的尾部
y = self._clip_augmented(y) # clip augmented tails
- 在多尺度推理中,可能会出现一些多余的预测数据,通过
_clip_augmented
方法对y
进行修剪以去除这些数据。
- 在多尺度推理中,可能会出现一些多余的预测数据,通过
-
返回最终结果
return torch.cat(y, 1), None # augmented inference, train
torch.cat(y, 1)
将收集的所有结果在维度1
(即预测的类别维度)上进行拼接,返回构成一个大的推理结果。第二个返回值为None
。
这段代码的主要功能是进行增强推理(augmented inference),通过多种缩放比例和翻转操作,生成多个变换后的图像,最终利用这些变换后的图像进行模型的预测。它的流程包括获取输入图像的大小、定义不同的缩放和翻转方式、进行图像处理(缩放和翻转)、前向传播,并且将结果逆缩放以恢复到原始尺寸。通过这种增强方法,模型能够在不同的视角和尺度上进行学习,提高检测性能。最后,将所有处理过的输出结果进行拼接返回,用于后续的模型评估或训练。
3.9 常规前向传播
def _forward_once(self, x, profile=False, visualize=False):
y, dt = [], [] # outputs
for m in self.model:
if m.f != -1: # if not from previous layer
x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f] # from earlier layers
if profile:
self._profile_one_layer(m, x, dt)
x = m(x) # run
y.append(x if m.i in self.save else None) # save output
if visualize:
feature_visualization(x, m.type, m.i, save_dir=visualize)
return x
这段代码定义了一个名为 _forward_once
的方法,它是一个神经网络前向传播过程的一部分,用于处理输入数据并生成输出。以下是代码的逐步分解和详细解释:
-
方法参数:
self
: 表示类实例。x
: 输入数据,通常是一个张量,代表输入到网络的图像数据。profile
: 布尔值,用于指示是否进行性能分析(计算推理时间)。visualize
: 布尔值,用于指示是否进行特征可视化。
-
输出和计时列表的初始化:
y, dt = [], [] # outputs
y
用于存储每一层的输出。dt
用于记录每一层的推理时间。
-
遍历模型的每一层:
for m in self.model:
self.model
是一个包含多个层的模型结构,循环会逐层处理输入数据。
-
获取当前层的输入:
if m.f != -1: # if not from previous layer x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f] # from earlier layers
- 检查当前层的输入来源
m.f
:- 如果
m.f
是 -1,表示当前层直接使用输入x
。 - 如果
m.f
是一个整数,表示从之前的某一层提取输出作为当前层的输入。 - 如果
m.f
是一个列表,则通过列表推导式从不同的层获取输出。
- 如果
- 检查当前层的输入来源
-
性能分析:
if profile: self._profile_one_layer(m, x, dt)
- 如果
profile
为真,调用_profile_one_layer
方法来分析当前层的性能。
- 如果
-
运行当前层的前向传播:
x = m(x) # run
- 将输入
x
传递给当前层m
,并将其输出赋值给x
。
- 将输入
-
保存当前层的输出:
y.append(x if m.i in self.save else None) # save output
- 如果当前层的索引
m.i
在要保存的层列表self.save
中,则将当前层的输出x
添加到y
中;否则添加None
。
- 如果当前层的索引
-
特征可视化:
if visualize: feature_visualization(x, m.type, m.i, save_dir=visualize)
- 如果
visualize
为真,调用feature_visualization
函数可视化当前层输出的特征。
- 如果
-
返回最后的输出:
return x
- 返回最后一层的输出。
该 _forward_once
方法实现了神经网络的单次前向传播功能。在处理输入数据时:
- 它能够灵活处理来自不同层的输入,并根据需要进行性能分析和特征可视化。
- 方法的主要功能是通过模型的每一层逐步计算输出,同时记录输出和运行时间,最终返回神经网络的输出结果。这种结构使得模型的推理过程更加模块化和可调试。
def _profile_one_layer(self, m, x, dt):
c = m == self.model[-1] # is final layer, copy input as inplace fix
o = thop.profile(m, inputs=(x.copy() if c else x,), verbose=False)[0] / 1E9 * 2 if thop else 0 # FLOPs
t = time_sync()
for _ in range(10):
m(x.copy() if c else x)
dt.append((time_sync() - t) * 100)
if m == self.model[0]:
LOGGER.info(f"{'time (ms)':>10s} {'GFLOPs':>10s} {'params':>10s} module")
LOGGER.info(f'{dt[-1]:10.2f} {o:10.2f} {m.np:10.0f} {m.type}')
if c:
LOGGER.info(f"{sum(dt):10.2f} {'-':>10s} {'-':>10s} Total")
这段代码定义了一个名为 _profile_one_layer
的方法,主要用于对模型的每一层进行性能分析,评估其运行时间和计算量(FLOPs)。下面是对该代码逐行的详细解释:
-
判断是否为最后一层:
c = m == self.model[-1] # is final layer, copy input as inplace fix
这里,
c
是一个布尔值,表示当前层m
是否为模型的最后一层。self.model[-1]
指向模型的最后一层。如果是最后一层,输入的复制操作会使用x.copy()
,以避免内存中的原地修改。 -
计算FLOPs:
o = thop.profile(m, inputs=(x.copy() if c else x,), verbose=False)[0] / 1E9 * 2 if thop else 0 # FLOPs
通过
thop.profile
函数计算当前层m
的浮点运算次数(FLOPs)。如果thop
模块可以正常导入和使用,该计算的结果会进行单位转换(从10亿为单位)并乘以2(通常是因为YOLO模型中使用了两次运算)。如果不使用thop
,则o
取值为0。 -
记录时间:
t = time_sync()
调用
time_sync()
函数记录当前时间,以便后续测量层的运行时间。 -
运行和记录时间:
for _ in range(10): m(x.copy() if c else x) dt.append((time_sync() - t) * 100)
循环10次运行层
m
,以保证时间测量的准确性。每次循环都使用time_sync()
计算耗时,并将其乘以100(可能是将时间单位转换为毫秒)并追加到dt
列表中。 -
日志输出:
if m == self.model[0]: LOGGER.info(f"{'time (ms)':>10s} {'GFLOPs':>10s} {'params':>10s} module") LOGGER.info(f'{dt[-1]:10.2f} {o:10.2f} {m.np:10.0f} {m.type}')
首先,如果当前层是模型的第一层,则输出表头信息。然后输出当前层的耗时、计算量(GFLOPs)和参数数量。
-
输出总耗时:
if c: LOGGER.info(f"{sum(dt):10.2f} {'-':>10s} {'-':>10s} Total")
如果当前层是最后一层,则输出总耗时的日志信息。
该代码的主要功能是在YOLOv5模型的每一层上进行性能分析。通过记录每层的计算量(FLOPs)、执行时间和参数数量,旨在帮助开发者理解模型各层的性能瓶颈。这对于优化模型的运行速度和效率至关重要。
3.10. 去缩放预测结果
def _descale_pred(self, p, flips, scale, img_size):
# de-scale predictions following augmented inference (inverse operation)
if self.inplace:
p[..., :4] /= scale # de-scale
if flips == 2:
p[..., 1] = img_size[0] - p[..., 1] # de-flip ud
elif flips == 3:
p[..., 0] = img_size[1] - p[..., 0] # de-flip lr
else:
x, y, wh = p[..., 0:1] / scale, p[..., 1:2] / scale, p[..., 2:4] / scale # de-scale
if flips == 2:
y = img_size[0] - y # de-flip ud
elif flips == 3:
x = img_size[1] - x # de-flip lr
p = torch.cat((x, y, wh, p[..., 4:]), -1)
return p
- 去缩放预测结果,使其回到输入图片的规模。
这段代码是一个类方法,主要用于对模型在进行增强推理后得到的预测结果进行反缩放和反翻转处理。以下是对代码的逐步分解和详细解释:
-
方法定义:
def _descale_pred(self, p, flips, scale, img_size):
该方法的名称为
_descale_pred
,接收四个参数:self
:指向类实例的引用。p
:一个包含预测结果的张量,通常包含目标框的位置和大小信息。flips
:一个整数,用于指示图像是否进行了翻转,可能的值有:0
:没有翻转2
:上下翻转3
:左右翻转
scale
:缩放比例,用于反缩放预测框的坐标。img_size
:一个包含图像高度和宽度的元组或列表。
-
反缩放和翻转处理:
if self.inplace: p[..., :4] /= scale # de-scale
如果
self.inplace
为真,则直接对p
进行反缩放操作,将前四个维度(通常是目标框的坐标)除以scale
。 -
处理翻转:
if flips == 2: p[..., 1] = img_size[0] - p[..., 1] # de-flip ud elif flips == 3: p[..., 0] = img_size[1] - p[..., 0] # de-flip lr
根据
flips
的值,对y坐标或x坐标进行反翻转处理:- 如果
flips
为2
,执行上下翻转操作; - 如果
flips
为3
,执行左右翻转操作。
- 如果
-
非就地操作:
else: x, y, wh = p[..., 0:1] / scale, p[..., 1:2] / scale, p[..., 2:4] / scale # de-scale
如果
self.inplace
为假,则创建新的变量来存储反缩放后的坐标:x
:横坐标,反缩放处理;y
:纵坐标,反缩放处理;wh
:宽高信息,反缩放处理。
-
翻转处理:
if flips == 2: y = img_size[0] - y # de-flip ud elif flips == 3: x = img_size[1] - x # de-flip lr
同上,根据
flips
的值,进行上下或左右翻转处理。 -
合并新的张量:
p = torch.cat((x, y, wh, p[..., 4:]), -1)
最后,将处理后的
x
、y
、wh
和p
的剩余部分(通常是置信度和类别信息)沿最后一个维度拼接在一起,形成一个新的预测结果张量。 -
返回结果:
return p
返回处理后的张量
p
,该张量现在是经过反缩放和反翻转处理后的预测结果。
此方法 _descale_pred
的主要功能是将经过数据增强(例如缩放和翻转)处理后的模型预测结果转回原始图像空间,使得预测框的坐标和尺寸反映正确的实际位置。这对于后续的后处理步骤如非极大值抑制(NMS)等是非常重要的,以确保检测到的目标框在图像上的位置是准确的。
3.11. 剪切增强后的预测结果
def _clip_augmented(self, y):
# Clip YOLOv5 augmented inference tails
nl = self.model[-1].nl # number of detection layers (P3-P5)
g = sum(4 ** x for x in range(nl)) # grid points
e = 1 # exclude layer count
i = (y[0].shape[1] // g) * sum(4 ** x for x in range(e)) # indices
y[0] = y[0][:, :-i] # large
i = (y[-1].shape[1] // g) * sum(4 ** (nl - 1 - x) for x in range(e)) # indices
y[-1] = y[-1][:, i:] # small
return y
- 此方法处理经过数据增强后的结果,确保输出符合预期。
这段代码是YOLOv5模型的一部分,主要功能是对经过增强推理(augmented inference)处理的预测结果进行裁剪,以去除不必要的部分,确保输出结果的有效性。下面逐步分解并详细解释这段代码:
-
函数定义
_clip_augmented(self, y)
:- 这是类中的私有方法,接受一个参数
y
,它通常是模型推理的输出结果。
- 这是类中的私有方法,接受一个参数
-
获取检测层数量:
nl = self.model[-1].nl # number of detection layers (P3-P5)
self.model[-1]
表示模型的最后一个模块(通常是检测头),nl
是这个模块中检测层的数量,代表不同尺度(例如P3到P5)进行目标检测。
-
计算网格点数量:
g = sum(4 ** x for x in range(nl)) # grid points
- 通过计算
4 ** x
的总和来得到网格点的数量,其中x
为层的索引。这是因为YOLO每个检测层通常使用不同数量的预测框,通过幂次函数表示不同尺度下的框数量。
- 通过计算
-
设置排除层数:
e = 1 # exclude layer count
- 这里
e
设置为1,表示在计算时排除的层数。
- 这里
-
计算大尺度输出的索引:
i = (y[0].shape[1] // g) * sum(4 ** x for x in range(e)) # indices
- 计算
y[0]
(最大的尺度输出)的形状,然后通过整除g
来获取预测框的数量,再乘以排除层的网格数量。该操作的目的是确定应当去除多少个输出。
- 计算
-
裁剪大型输出:
y[0] = y[0][:, :-i] # large
- 通过切片操作,将
y[0]
中的最后i
个元素去掉,从而只保留有效的检测结果。
- 通过切片操作,将
-
计算小尺度输出的索引:
i = (y[-1].shape[1] // g) * sum(4 ** (nl - 1 - x) for x in range(e)) # indices
- 该行用于计算小尺度输出
y[-1]
的索引,同样通过整除计算预测框的数量,乘以对应的网格数量来得到应当裁剪的数量。
- 该行用于计算小尺度输出
-
裁剪小型输出:
y[-1] = y[-1][:, i:] # small
- 通过切片操作,将小尺度的输出裁剪到只保留从索引
i
开始到结束的结果,以保留有效的检测信息。
- 通过切片操作,将小尺度的输出裁剪到只保留从索引
-
返回处理后的输出:
return y
- 最后,返回裁剪后更新的
y
。
- 最后,返回裁剪后更新的
这段代码的主要功能是处理YOLOv5模型在增强推理阶段的输出结果,通过裁剪不必要的部分来优化预测结果。它确保每个尺度的输出都仅包含有效的检测信息,去掉了由于增强输入后可能生成的多余信息。通过这种方式,可以提高模型推理的有效性和精确性。
3.11. 初始化偏差
def _initialize_biases(self, cf=None): # initialize biases into Detect(), cf is class frequency
# https://arxiv.org/abs/1708.02002 section 3.3
# cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1.
m = self.model[-1] # Detect() module
for mi, s in zip(m.m, m.stride): # from
b = mi.bias.view(m.na, -1) # conv.bias(255) to (3,85)
b.data[:, 4] += math.log(8 / (640 / s) ** 2) # obj (8 objects per 640 image)
b.data[:, 5:5 + m.nc] += math.log(0.6 / (m.nc - 0.99999)) if cf is None else torch.log(cf / cf.sum()) # cls
mi.bias = torch.nn.Parameter(b.view(-1), requires_grad=True)
- 初始化检测模块中的偏差,以适应物体检测的要求。
这段代码是YOLOv5模型中的一个方法,用于初始化检测层(Detect)中的偏置(bias)。下面逐步分解并解释这段代码。
-
函数定义:
def _initialize_biases(self, cf=None):
- 这是一个名为
_initialize_biases
的方法,接受一个参数cf
,代表类的频率(class frequency),默认为None
。
- 这是一个名为
-
模型的最后一层:
m = self.model[-1] # Detect() module
m
被赋值为模型的最后一层,通常是检测层(Detect)模块。
-
遍历检测层的每个卷积层及其步幅:
for mi, s in zip(m.m, m.stride):
- 使用
zip
函数同时遍历检测模块中的卷积层m.m
和对应的步幅m.stride
。
- 使用
-
视图变换以便于处理偏置:
b = mi.bias.view(m.na, -1) # conv.bias(255) to (3,85)
- 将卷积层的偏置(通常为255维)变换为一个形状为
(3, 85)
的张量,其中3
为锚点的数量(na),85
为每个锚点的输出(包括框的位置信息和类别信息)。
- 将卷积层的偏置(通常为255维)变换为一个形状为
-
初始化目标(obj)偏置:
b.data[:, 4] += math.log(8 / (640 / s) ** 2)
- 这里是对目标(objects)的偏置进行初始化。
640
是输入图像的大小,s
是当前层的步幅。这一行通过引入一个常数使得模型在640x640输入图像的情况下适应目标的比例。
- 这里是对目标(objects)的偏置进行初始化。
-
初始化类别(cls)偏置:
b.data[:, 5:5 + m.nc] += math.log(0.6 / (m.nc - 0.99999)) if cf is None else torch.log(cf / cf.sum())
- 这里是对每个类别的偏置初始化。如果没有提供类频率
cf
,就用0.6/类数初始化。否则,使用类频率的比例来初始化。
- 这里是对每个类别的偏置初始化。如果没有提供类频率
-
更新偏置:
mi.bias = torch.nn.Parameter(b.view(-1), requires_grad=True)
- 将修改后的偏置重新赋值到卷积层中,并将其设置为可训练的参数(
requires_grad=True
)。
- 将修改后的偏置重新赋值到卷积层中,并将其设置为可训练的参数(
这个方法的主要功能是在YOLOv5的检测模型中初始化最后一层的偏置。通过对不同类别和目标数量的偏置进行合理的初始化,使模型的学习更为高效。偏置的初始化使用目标的数量和类别的分布,旨在帮助模型在推理阶段快速适应训练数据,从而提高检测精度和召回率。这一过程是机器学习模型训练中重要的一步,能够有效地影响模型的性能。
3.12 总结
DetectionModel
类实现了YOLOv5的核心检测功能,主要用于物体检测任务。它从配置文件中加载模型参数,定义网络结构,处理输入数据,并支持数据增强等特性。该模型通过重写 forward
方法实现推理过程,能够根据需要进行单尺度推理或数据增强推理。模型的设计旨在快速高效地处理图像,并输出检测结果,为物体检测任务提供了强大的基础。
参考: