阅读量:0
yolo模型
以下代码位于yolov5/models/yolo.py
一、导包模块
# Ultralytics YOLOv5 🚀, AGPL-3.0 license """ # 这是YOLOv5项目的一部分,遵循AGPL-3.0开源许可证。 # 该文件包含YOLOv5特有的模块定义和一些实用工具函数。 # 使用说明: # $ python models/yolo.py --cfg yolov5s.yaml # 上述命令行用于演示如何使用本文件中的模块来加载并运行YOLOv5模型, # 其中'yolov5s.yaml'是模型的配置文件。 # 导入必要的Python库和模块 import argparse # 用于解析命令行参数 import contextlib # 提供上下文管理器 import math # 提供数学函数 import os # 操作系统接口 import platform # 获取平台信息 import sys # 访问或修改解释器变量 from copy import deepcopy # 复制模块,用于深复制对象 from pathlib import Path # 文件系统路径操作 # 获取当前文件的绝对路径 FILE = Path(__file__).resolve() # 定义YOLOv5的根目录 ROOT = FILE.parents[1] # YOLOv5的根目录是当前文件的上两级目录 # 将YOLOv5的根目录添加到系统路径中,以便可以从中导入其他模块 if str(ROOT) not in sys.path: sys.path.append(str(ROOT)) # 如果当前系统不是Windows,则将根目录设置为相对路径 if platform.system() != "Windows": ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # 相对于当前工作目录的相对路径 # 从YOLOv5的其他文件中导入各种模块和类 # 这些模块和类是YOLOv5架构中不同组件的实现 from models.common import ( C3, C3SPP, C3TR, SPP, SPPF, Bottleneck, BottleneckCSP, C3Ghost, C3x, Classify, Concat, Contract, Conv, CrossConv, DetectMultiBackend, DWConv, DWConvTranspose2d, Expand, Focus, GhostBottleneck, GhostConv, Proto ) # 导入YOLOv5的一些实用工具函数 from utils.autoanchor import check_anchor_order # 检查锚点顺序的正确性 from utils.general import ( # 各种通用的辅助函数 LOGGER, check_version, check_yaml, colorstr, make_divisible, print_args ) from utils.plots import feature_visualization # 特征可视化工具 from utils.torch_utils import ( # PyTorch相关的辅助函数 fuse_conv_and_bn, initialize_weights, model_info, profile, scale_img, select_device, time_sync ) # 尝试导入thop模块,用于计算网络的FLOPs # 如果模块不存在,thop将被设为None try: import thop except ImportError: thop = None
二、检测头
class Detect(nn.Module): """ YOLOv5的检测头,用于检测模型。 """ stride = None # 在构建时计算的步长 dynamic = False # 强制重新构造网格 export = False # 导出模式 def __init__(self, nc=80, anchors=(), ch=(), inplace=True): """ 初始化YOLOv5检测层。 参数: nc (int): 类别数量,默认为80。 anchors (tuple): 锚框列表。 ch (tuple): 输入通道数列表。 inplace (bool): 是否使用原地操作。 """ super().__init__() # 调用父类nn.Module的初始化方法 self.nc = nc # 类别数量 self.no = nc + 5 # 每个锚框的输出数量 self.nl = len(anchors) # 检测层数量 self.na = len(anchors[0]) // 2 # 每个层级的锚框数量 self.grid = [torch.empty(0) for _ in range(self.nl)] # 初始化网格列表 self.anchor_grid = [torch.empty(0) for _ in range(self.nl)] # 初始化锚框网格列表 self.register_buffer("anchors", torch.tensor(anchors).float().view(self.nl, -1, 2)) # 注册锚框张量 self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch) # 输出卷积层列表 self.inplace = inplace # 是否使用原地操作标志 def forward(self, x): """ 前向传播函数,处理输入数据并生成检测结果。 参数: x (list[Tensor]): 模型的特征图列表。 返回: list[Tensor]: 检测结果列表,每个元素对应一个层级的输出。 """ z = [] # 初始化用于存储检测输出的列表 for i in range(self.nl): # 遍历每个检测层级 x[i] = self.m[i](x[i]) # 卷积操作 bs, _, ny, nx = x[i].shape # 获取批次大小、通道数、高度和宽度 # 调整张量形状为(batch, anchors, grid_height, grid_width, outputs) x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous() if not self.training: # 如果在推理阶段 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) # 根据Segment类或Detect类的不同,分别处理输出 if isinstance(self, Segment): 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: 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) # 合并输出 # 将输出张量reshape为(batch, num_anchors * grid_h * grid_w, num_outputs) 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")): """ 生成网格和锚框网格,兼容不同版本的PyTorch。 参数: nx (int): 网格宽度。 ny (int): 网格高度。 i (int): 当前检测层级索引。 torch_1_10 (bool): PyTorch版本是否大于等于1.10。 返回: tuple[Tensor, Tensor]: 网格张量和锚框网格张量。 """ d = self.anchors[i].device # 设备类型 t = self.anchors[i].dtype # 数据类型 shape = 1, self.na, ny, nx, 2 # 目标网格形状 # 创建网格张量 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 # 创建网格 # 创建锚框网格张量 anchor_grid = (self.anchors[i] * self.stride[i]).view((1, self.na, 1, 1, 2)).expand(shape) return grid, anchor_grid
三、分割头
class Segment(Detect): """ YOLOv5的分割头,用于分割模型。 """ def __init__(self, nc=80, anchors=(), nm=32, npr=256, ch=(), inplace=True): """ 初始化YOLOv5分割头。 参数: nc (int): 类别数量,默认为80。 anchors (tuple): 锚框列表。 nm (int): 掩模数量,默认为32。 npr (int): 原型数量,默认为256。 ch (tuple): 输入通道数列表。 inplace (bool): 是否使用原地操作。 """ # 调用父类Detect的初始化方法,继承其属性和功能 super().__init__(nc, anchors, ch, inplace) self.nm = nm # 掩模数量 self.npr = npr # 原型数量 self.no = 5 + nc + self.nm # 每个锚框的输出数量(包含掩模) # 更新输出卷积层以适应新的输出数量 self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch) # 添加原型模块,用于生成原型掩模 self.proto = Proto(ch[0], self.npr, self.nm) # 重定义detect属性,直接指向父类的forward方法,以便在forward中使用 self.detect = Detect.forward def forward(self, x): """ 前向传播函数,处理输入数据并生成检测结果和原型掩模。 参数: x (list[Tensor]): 模型的特征图列表。 返回: tuple: 包含检测结果和原型掩模的元组,根据训练/导出模式调整输出。 """ # 通过原型模块生成原型掩模 p = self.proto(x[0]) # 调用父类的前向传播方法来获取检测结果 x = self.detect(self, x) # 根据训练/导出模式调整输出 if self.training: # 训练模式下返回检测结果和原型掩模 return x, p elif self.export: # 导出模式下仅返回检测结果和原型掩模 return x[0], p else: # 其他模式下返回检测结果、原型掩模以及额外的输出(如果有的话) return x[0], p, x[1]
四、基础类模型
class BaseModel(nn.Module): """YOLOv5的基础模型类,继承自PyTorch的nn.Module.""" def forward(self, x, profile=False, visualize=False): """执行YOLOv5基础模型的单尺度推理或训练过程,可选择开启性能分析和特征可视化. 参数: x (Tensor): 输入张量. profile (bool): 是否进行性能分析. visualize (bool): 是否启用特征可视化. 返回: Tensor: 模型的输出. """ return self._forward_once(x, profile, visualize) # 单尺度推理或训练. def _forward_once(self, x, profile=False, visualize=False): """执行YOLOv5模型的一次前向传播,允许性能分析和特征可视化选项. 参数: x (Tensor): 输入张量. profile (bool): 是否进行性能分析. visualize (bool): 是否启用特征可视化. 返回: Tensor: 最终输出张量. """ y, dt = [], [] # 保存各层输出和时间差 for m in self.model: # 遍历模型中的每一层 if m.f != -1: # 如果层不是从上一层接收输入 x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f] if profile: # 如果需要性能分析 self._profile_one_layer(m, x, dt) x = m(x) # 执行层的操作 y.append(x if m.i in self.save else None) # 保存输出,如果需要 if visualize: # 如果需要特征可视化 feature_visualization(x, m.type, m.i, save_dir=visualize) return x # 返回最终输出 def _profile_one_layer(self, m, x, dt): """对单层进行性能分析,计算GFLOPs、执行时间和参数数量. 参数: m (Module): 当前层. x (Tensor): 输入张量. dt (list): 存储每层的时间差. """ c = m == self.model[-1] # 是否是最后一层,用于防止inplace操作 o = thop.profile(m, inputs=(x.copy() if c else x), verbose=False)[0] / 1e9 * 2 if thop else 0 t = time_sync() # 同步时间 for _ in range(10): # 运行10次以获得平均时间 m(x.copy() if c else x) dt.append((time_sync() - t) * 100) # 计算时间差 if m == self.model[0]: # 如果是第一层,打印标题 LOGGER.info("time (ms) GFLOPs params 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") def fuse(self): """融合Conv2d和BatchNorm2d层以提高推理速度. 返回: self: 修改后的模型. """ LOGGER.info("Fusing layers... ") for m in self.model.modules(): # 遍历所有模块 if isinstance(m, (Conv, DWConv)) and hasattr(m, "bn"): # 如果是Conv或DWConv且有BN层 m.conv = fuse_conv_and_bn(m.conv, m.bn) # 融合卷积和BN delattr(m, "bn") # 删除BN属性 m.forward = m.forward_fuse # 使用融合后的前向传播 self.info() # 打印模型信息 return self # 返回自身 def info(self, verbose=False, img_size=640): """打印模型信息,包括详细程度和输入图像大小. 参数: verbose (bool): 是否详细打印. img_size (int): 图像大小. """ model_info(self, verbose, img_size) # 调用model_info函数 def _apply(self, fn): """应用变换如to(), cpu(), cuda(), half()到模型张量,但不包括参数或注册缓冲区. 参数: fn (function): 要应用的函数. 返回: self: 修改后的模型. """ self = super()._apply(fn) # 应用变换到模型 m = self.model[-1] # 获取最后一个模块,通常是检测器 if isinstance(m, (Detect, Segment)): # 如果是检测或分割模块 m.stride = fn(m.stride) # 应用变换到stride m.grid = list(map(fn, m.grid)) # 应用变换到grid if isinstance(m.anchor_grid, list): m.anchor_grid = list(map(fn, m.anchor_grid)) # 应用变换到anchor_grid return self # 返回自身
五、检测模型类
class DetectionModel(BaseModel): # YOLOv5 detection model def __init__(self, cfg="yolov5s.yaml", ch=3, nc=None, anchors=None): # 构造函数初始化YOLOv5模型,接受配置文件名、输入通道数、类别数量和自定义锚点。 super().__init__() # 调用基类构造函数 if isinstance(cfg, dict): # 如果cfg是一个字典,说明是已经解析过的模型配置 self.yaml = cfg # 将字典赋值给self.yaml else: # 否则,假设cfg是一个指向YAML配置文件的路径 import yaml # 导入YAML库用于读取配置文件 self.yaml_file = Path(cfg).name # 获取配置文件名 with open(cfg, encoding="ascii", errors="ignore") as f: # 打开并读取配置文件 self.yaml = yaml.safe_load(f) # 加载YAML配置文件到self.yaml # 定义模型 ch = self.yaml["ch"] = self.yaml.get("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 # 更新配置文件中的类别数量 if anchors: # 如果传递了自定义锚点 LOGGER.info(f"Overriding model.yaml anchors with anchors={anchors}") # 记录覆盖信息 self.yaml["anchors"] = round(anchors) # 更新配置文件中的锚点 self.model, self.save = parse_model(deepcopy(self.yaml), ch=[ch]) # 解析模型配置并构建模型 self.names = [str(i) for i in range(self.yaml["nc"])] # 默认类别名称列表 self.inplace = self.yaml.get("inplace", True) # 是否使用原位运算 # 构建步长和锚点 m = self.model[-1] # 获取模型的最后一个模块(通常是Detect或Segment) if isinstance(m, (Detect, Segment)): # 如果最后一个模块是Detect或Segment def _forward(x): # 定义一个内部函数来前向传播 return self.forward(x)[0] if isinstance(m, Segment) else self.forward(x) s = 256 # 最小步长的两倍 m.inplace = self.inplace # 设置模块的原位运算属性 m.stride = torch.tensor([s / x.shape[-2] for x in _forward(torch.zeros(1, ch, s, s))]) # 计算步长 check_anchor_order(m) # 检查锚点顺序 m.anchors /= m.stride.view(-1, 1, 1) # 调整锚点大小 self.stride = m.stride # 设置模型的步长属性 self._initialize_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) # 增强推断 return self._forward_once(x, profile, visualize) # 单尺度推断 def _forward_augment(self, x): # 在不同的尺度和翻转下执行增强推断,返回组合后的检测结果。 img_size = x.shape[-2:] # 图像的高度和宽度 s = [1, 0.83, 0.67] # 不同的缩放比例 f = [None, 3, None] # 翻转类型(无翻转,水平翻转,垂直翻转) y = [] # 存储输出 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] # 前向传播 yi = self._descale_pred(yi, fi, si, img_size) # 反缩放预测结果 y.append(yi) # 添加到输出列表 y = self._clip_augmented(y) # 裁剪增强推断的尾巴 return torch.cat(y, 1), None # 返回拼接后的输出 def _descale_pred(self, p, flips, scale, img_size): # 反缩放增强推断的预测结果,调整翻转和图像尺寸。 if self.inplace: # 如果使用原位运算 p[..., :4] /= scale # 反缩放边界框坐标 if flips == 2: # 如果进行了垂直翻转 p[..., 1] = img_size[0] - p[..., 1] # 反翻转y坐标 elif flips == 3: # 如果进行了水平翻转 p[..., 0] = img_size[1] - p[..., 0] # 反翻转x坐标 else: # 如果不使用原位运算 x, y, wh = p[..., 0:1] / scale, p[..., 1:2] / scale, p[..., 2:4] / scale # 分离坐标和宽高 if flips == 2: # 如果进行了垂直翻转 y = img_size[0] - y # 反翻转y坐标 elif flips == 3: # 如果进行了水平翻转 x = img_size[1] - x # 反翻转x坐标 p = torch.cat((x, y, wh, p[..., 4:]), -1) # 重新组合坐标和宽高 return p # 返回反缩放后的预测结果 def _clip_augmented(self, y): # 裁剪增强推断的尾巴,影响第一个和最后一个张量基于网格点和层数。 nl = self.model[-1].nl # 检测层数 g = sum(4**x for x in range(nl)) # 总网格点数 e = 1 # 排除层数计数 i = (y[0].shape[1] // g) * sum(4**x for x in range(e)) # 大尺度裁剪索引 y[0] = y[0][:, :-i] # 大尺度裁剪 i = (y[-1].shape[1] // g) * sum(4 ** (nl - 1 - x) for x in range(e)) # 小尺度裁剪索引 y[-1] = y[-1][:, i:] # 小尺度裁剪 return y # 返回裁剪后的结果 def _initialize_biases(self, cf=None): # 初始化YOLOv5的Detect()模块的偏置,可选地使用类别频率。 m = self.model[-1] # 获取Detect模块 for mi, s in zip(m.m, m.stride): # 遍历模块和步长 b = mi.bias.view(m.na, -1) # 查看偏置为(锚点数, 类别数+5) b.data[:, 4] += math.log(8 / (640 / s) ** 2) # 对象偏置初始化 b.data[:, 5 : 5 + m.nc] += ( math.log(0.6 / (m.nc - 0.99999)) if cf is None else torch.log(cf / cf.sum()) ) # 类别偏置初始化 mi.bias = torch.nn.Parameter(b.view(-1), requires_grad=True) # 更新偏置参数 Model = DetectionModel # 保留YOLOv5 'Model'类以便向后兼容
六、分割模型类
class SegmentationModel(DetectionModel): # YOLOv5 segmentation model def __init__(self, cfg="yolov5s-seg.yaml", ch=3, nc=None, anchors=None): """Initializes a YOLOv5 segmentation model with configurable params: cfg (str) for configuration, ch (int) for channels, nc (int) for num classes, anchors (list).""" super().__init__(cfg, ch, nc, anchors)
七、分类模型类
# 定义ClassificationModel类,用于YOLOv5分类任务,继承自BaseModel。 # class ClassificationModel(BaseModel): # YOLOv5 classification model def __init__(self, cfg=None, model=None, nc=1000, cutoff=10): """Initializes YOLOv5 model with config file `cfg`, input channels `ch`, number of classes `nc`, and `cuttoff` index. """ super().__init__() # 调用基类构造器 self._from_detection_model(model, nc, cutoff) if model is not None else self._from_yaml(cfg) # 根据model参数决定从检测模型转换或从配置文件创建 def _from_detection_model(self, model, nc=1000, cutoff=10): """Creates a classification model from a YOLOv5 detection model, slicing at `cutoff` and adding a classification layer. """ if isinstance(model, DetectMultiBackend): # 如果model是DetectMultiBackend实例,获取其内部模型 model = model.model # unwrap DetectMultiBackend model.model = model.model[:cutoff] # 截断模型,保留至cutoff层作为主干网络 m = model.model[-1] # 获取截断后模型的最后一层 ch = m.conv.in_channels if hasattr(m, "conv") else m.cv1.conv.in_channels # 获取最后一层的输入通道数 c = Classify(ch, nc) # 创建分类层,传入输入通道数和类别数 c.i, c.f, c.type = m.i, m.f, "models.common.Classify" # 设置分类层的索引、来源和类型 model.model[-1] = c # 替换模型的最后一层为分类层 self.model = model.model # 将处理后的模型赋值给self.model self.stride = model.stride # 设置步长属性 self.save = [] # 初始化保存列表 self.nc = nc # 设置类别数量属性 def _from_yaml(self, cfg): """Creates a YOLOv5 classification model from a specified *.yaml configuration file.""" self.model = None # 当前实现仅设置了self.model为None,实际模型构建逻辑应在后续代码中
八、定义模型结构
def parse_model(d, ch): # 定义解析YOLOv5模型结构的函数 """从字典`d`中解析YOLOv5模型,根据输入通道数`ch`和模型架构配置各层。""" LOGGER.info(f"\n{'':>3}{'from':>18}{'n':>3}{'params':>10} {'module':<40}{'arguments':<30}") # 打印模型概览的表头 anchors, nc, gd, gw, act, ch_mul = ( # 从配置字典中提取模型参数 d["anchors"], d["nc"], d["depth_multiple"], d["width_multiple"], d.get("activation"), d.get("channel_multiple"), ) if act: # 如果配置中有激活函数设置 Conv.default_act = eval(act) # 重新定义卷积层的默认激活函数 LOGGER.info(f"{colorstr('activation:')} {act}") # 打印所用的激活函数 if not ch_mul: # 如果通道乘数未设置 ch_mul = 8 # 设定默认的通道乘数值 na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors # 计算锚点的数量 no = na * (nc + 5) # 计算每个锚点的输出数量 layers, save, c2 = [], [], ch[-1] # 初始化模型层列表,保存列表,和最后一个通道数变量 for i, (f, n, m, args) in enumerate(d["backbone"] + d["head"]): # 遍历模型的主干和头部配置 m = eval(m) if isinstance(m, str) else m # 如果模块名是字符串,转换为对应的类对象 for j, a in enumerate(args): # 遍历模块参数 with contextlib.suppress(NameError): # 忽略NameError异常 args[j] = eval(a) if isinstance(a, str) else a # 如果参数是字符串,转换为对应的对象 n = n_ = max(round(n * gd), 1) if n > 1 else n # 计算模块的重复次数 if m in { # 判断模块类型,调整输入输出通道数和参数 Conv, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, MixConv2d, Focus, CrossConv, BottleneckCSP, C3, C3TR, C3SPP, C3Ghost, nn.ConvTranspose2d, DWConvTranspose2d, C3x, }: c1, c2 = ch[f], args[0] # 获取输入和输出通道数 if c2 != no: # 如果不是输出层 c2 = make_divisible(c2 * gw, ch_mul) # 调整输出通道数 args = [c1, c2, *args[1:]] # 更新参数列表 if m in {BottleneckCSP, C3, C3TR, C3Ghost, C3x}: # 对于特定模块,插入重复次数 args.insert(2, n) n = 1 elif m is nn.BatchNorm2d: # 如果是BatchNorm层 args = [ch[f]] # 输入通道数作为参数 elif m is Concat: # 如果是Concat层 c2 = sum(ch[x] for x in f) # 计算拼接后的通道数 elif m in {Detect, Segment}: # 如果是检测或分割层 args.append([ch[x] for x in f]) # 添加输入通道数列表 if isinstance(args[1], int): # 如果锚点数量是整数 args[1] = [list(range(args[1] * 2))] * len(f) # 转换为锚点列表 if m is Segment: # 如果是分割层 args[3] = make_divisible(args[3] * gw, ch_mul) # 调整参数 elif m is Contract: # 如果是收缩层 c2 = ch[f] * args[0] ** 2 # 计算收缩后的通道数 elif m is Expand: # 如果是扩张层 c2 = ch[f] // args[0] ** 2 # 计算扩张后的通道数 else: # 其他类型的模块 c2 = ch[f] # 输出通道数等于输入通道数 m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args) # 创建模块实例 t = str(m)[8:-2].replace("__main__.", "") # 获取模块类型名称 np = sum(x.numel() for x in m_.parameters()) # 计算参数数量 m_.i, m_.f, m_.type, m_.np = i, f, t, np # 附加模块的元数据 LOGGER.info(f"{i:>3}{str(f):>18}{n_:>3}{np:10.0f} {t:<40}{str(args):<30}") # 打印模块信息 save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1) # 更新保存列表 layers.append(m_) # 添加模块到模型层列表 if i == 0: # 如果是第一个模块 ch = [] # 清空通道数列表 ch.append(c2) # 更新通道数列表 return nn.Sequential(*layers), sorted(save) # 返回模型和排序后的保存列表
九、主函数
if __name__ == "__main__": # 当脚本直接运行时执行以下代码 parser = argparse.ArgumentParser() # 创建命令行参数解析器 parser.add_argument("--cfg", type=str, default="yolov5s.yaml", help="model.yaml") # 添加模型配置文件参数 parser.add_argument("--batch-size", type=int, default=1, help="total batch size for all GPUs") # 添加批量大小参数 parser.add_argument("--device", default="", help="cuda device, i.e. 0 or 0,1,2,3 or cpu") # 添加设备参数 parser.add_argument("--profile", action="store_true", help="profile model speed") # 添加模型速度剖析选项 parser.add_argument("--line-profile", action="store_true", help="profile model speed layer by layer") # 添加分层速度剖析选项 parser.add_argument("--test", action="store_true", help="test all yolo*.yaml") # 添加测试所有Yolo配置文件的选项 opt = parser.parse_args() # 解析命令行参数 opt.cfg = check_yaml(opt.cfg) # 检查并标准化配置文件路径 print_args(vars(opt)) # 打印解析后的参数 device = select_device(opt.device) # 选择合适的设备进行计算 # 创建模型 im = torch.rand(opt.batch_size, 3, 640, 640).to(device) # 创建随机输入张量 model = Model(opt.cfg).to(device) # 根据配置文件创建模型并移至指定设备 # 选项处理 if opt.line_profile: # 如果选择了分层剖析 model(im, profile=True) # 剖析模型的每一层 elif opt.profile: # 如果选择了整体模型剖析 results = profile(input=im, ops=[model], n=3) # 多次运行模型并收集性能数据 elif opt.test: # 如果选择了测试所有模型 for cfg in Path(ROOT / "models").rglob("yolo*.yaml"): # 遍历所有符合条件的Yolo配置文件 try: _ = Model(cfg) # 尝试创建模型 except Exception as e: # 捕获任何异常 print(f"Error in {cfg}: {e}") # 打印错误信息 else: # 如果没有特殊选项,报告融合后的模型摘要 model.fuse() # 融合模型中的重复操作
以下代码位于yolov5/models/yolov5n.yaml
十、YOLO配置文件
# Ultralytics YOLOv5 🚀, AGPL-3.0 license # 参数定义 nc: 80 # 类别数,即模型将识别80个不同的目标类别 depth_multiple: 0.33 # 模型深度的倍数,用于控制模型复杂度 width_multiple: 0.25 # 层通道数的倍数,用于控制模型宽度 anchors: # 锚框尺寸,用于不同尺度的特征图 - [10, 13, 16, 30, 33, 23] # 对应于P3/8特征图的锚框尺寸 - [30, 61, 62, 45, 59, 119] # 对应于P4/16特征图的锚框尺寸 - [116, 90, 156, 198, 373, 326] # 对应于P5/32特征图的锚框尺寸 # YOLOv5 v6.0 主干网络 backbone: # 主干网络定义,包含一系列的层及其参数 # [from, number, module, args] 表示从哪个层开始,重复次数,模块类型,以及参数 [ [-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2 卷积层,输入通道数为64,核大小为6x6,步长为2,填充为2 [-1, 1, Conv, [128, 3, 2]], # 1-P2/4 卷积层,输入通道数为128,核大小为3x3,步长为2 [-1, 3, C3, [128]], # 2-C3模块,输入通道数为128,重复3次 [-1, 1, Conv, [256, 3, 2]], # 3-P3/8 卷积层,输入通道数为256,核大小为3x3,步长为2 [-1, 6, C3, [256]], # 4-C3模块,输入通道数为256,重复6次 [-1, 1, Conv, [512, 3, 2]], # 5-P4/16 卷积层,输入通道数为512,核大小为3x3,步长为2 [-1, 9, C3, [512]], # 6-C3模块,输入通道数为512,重复9次 [-1, 1, Conv, [1024, 3, 2]], # 7-P5/32 卷积层,输入通道数为1024,核大小为3x3,步长为2 [-1, 3, C3, [1024]], # 8-C3模块,输入通道数为1024,重复3次 [-1, 1, SPPF, [1024, 5]], # 9-SPPF模块,输入通道数为1024,核大小为5x5 ] # YOLOv5 v6.0 头部网络 head: # 头部网络定义,用于特征融合和最终预测 [ [-1, 1, Conv, [512, 1, 1]], # 卷积层,输入通道数为512,核大小为1x1 [-1, 1, nn.Upsample, [None, 2, "nearest"]], # 上采样层,放大2倍,采用最近邻插值 [[-1, 6], 1, Concat, [1]], # 拼接操作,将上采样后的特征与P4特征图拼接 [-1, 3, C3, [512, False]], # C3模块,输入通道数为512,不使用shortcut连接 [-1, 1, Conv, [256, 1, 1]], # 卷积层,输入通道数为256,核大小为1x1 [-1, 1, nn.Upsample, [None, 2, "nearest"]], # 上采样层,放大2倍,采用最近邻插值 [[-1, 4], 1, Concat, [1]], # 拼接操作,将上采样后的特征与P3特征图拼接 [-1, 3, C3, [256, False]], # C3模块,输入通道数为256,不使用shortcut连接 [-1, 1, Conv, [256, 3, 2]], # 卷积层,输入通道数为256,核大小为3x3,步长为2 [[-1, 14], 1, Concat, [1]], # 拼接操作,将特征与之前P4特征图拼接 [-1, 3, C3, [512, False]], # C3模块,输入通道数为512,不使用shortcut连接 [-1, 1, Conv, [512, 3, 2]], # 卷积层,输入通道数为512,核大小为3x3,步长为2 [[-1, 10], 1, Concat, [1]], # 拼接操作,将特征与之前P5特征图拼接 [-1, 3, C3, [1024, False]], # C3模块,输入通道数为1024,不使用shortcut连接 [[17, 20, 23], 1, Detect, [nc, anchors]], # Detect层,输入为三个不同尺度的特征图,进行目标检测 ]
例:Conv
卷积层位于yolov5/models/common.py
class Conv(nn.Module): # Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation) # 这是一个标准的卷积层类,接收参数包括输入通道数(ch_in),输出通道数(ch_out),卷积核大小(kernel),步长(stride),填充(padding),组数(groups),膨胀率(dilation),和激活函数(activation)。 default_act = nn.SiLU() # default activation # 设置默认的激活函数为SiLU(Swish的简化版本),这是一个自定义的类属性,可以在实例化时不改变的情况下被所有Conv类实例共享。 def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True): """Initializes a standard convolution layer with optional batch normalization and activation.""" # 构造函数初始化一个标准的卷积层,带有可选的批量归一化和激活函数。 super().__init__() # 调用父类nn.Module的构造函数初始化模块。 self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False) # 创建一个2D卷积层,参数为: # c1: 输入通道数 # c2: 输出通道数 # k: 卷积核大小 # s: 步长 # autopad(k, p, d): 自动计算的padding值,如果p为None,则自动计算以保持输入输出的尺寸相同,考虑dilation的影响。 # groups: 分组卷积的组数,默认为1,表示标准卷积。 # dilation: 膨胀率,控制卷积核元素之间的间距。 # bias: 是否使用偏置项,这里设为False。 self.bn = nn.BatchNorm2d(c2) # 创建一个2D批量归一化层,参数为c2,即卷积层的输出通道数。 self.act = self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity() # 设置激活函数,如果act为True,则使用默认激活函数(default_act); # 如果act是nn.Module的实例,则直接使用act; # 否则,使用恒等函数Identity()。 def forward(self, x): """Applies a convolution followed by batch normalization and an activation function to the input tensor `x`.""" # 定义前向传播方法,对输入张量x执行卷积、批量归一化和激活函数。 return self.act(self.bn(self.conv(x))) # 应用顺序为:卷积 -> 批量归一化 -> 激活函数。 def forward_fuse(self, x): """Applies a fused convolution and activation function to the input tensor `x`.""" # 定义融合的前向传播方法,只适用于不使用批量归一化的情况,直接将卷积和激活函数融合在一起。 return self.act(self.conv(x)) # 应用顺序为:卷积 -> 激活函数,省略了批量归一化步骤。