【深度学习】【目标检测】【Ultralytics-YOLO系列】YOLOV3核心文件common.py解读

【深度学习】【目标检测】【Ultralytics-YOLO系列】YOLOV3核心文件common.py解读


前言

在详细解析YOLOV3网络之前,首要任务是搭建Ultralytics–YOLOV3【Windows11下YOLOV3人脸检测】所需的运行环境,并完成模型的训练和测试,展开后续工作才有意义。
本博文对models/common.py代码进行解析,common.py文件存放着YOLOV3网络搭建常见的通用模块。其他代码后续的博文将会陆续讲解。这里只做YOLOV3相关模块的代码解析,其他的通用模块是YOLO后续系列中创新和提出的。


autopad函数

用于自动计算卷积层的填充值(padding),以确保当步幅为1时卷积操作后的输出特征图尺寸保持不变。

def autopad(k, p=None):  # kernel, padding
    """
    用于自动计算卷积层的填充值
    :param k:卷积核大小
    :param p:填充值
    :return:计算得到的填充值
    """
    if p is None:   # 检查是否需要自动计算填充值,否则用户指定填充值
        # 正方形卷积核:isinstance(k, int)表示k是一个整数,卷积核是正方形,填充值p被为k//2,即卷积核大小的一半向下取整
        # 非正方形卷积核: 是一个列表或元组,则对每个维度分别计算填充值,同理计算填充值p
        p = k // 2 if isinstance(k, int) else [x // 2 for x in k]  # auto-pad
    return p

理论基础: 在二维卷积中,输出特征图的尺寸可以通过以下公式计算:
O u t p u t S i z e = I n p u t S i z e + 2 p − k s + 1 OutputSize = \frac{{InputSize + 2p - k}}{{\rm{s}}} + 1 OutputSize=sInputSize+2pk+1
其中: I n p u t S i z e InputSize InputSize是输入特征图的尺寸; p p p是填充大小; k k k是卷积核大小; s s s是步长。
根据上述方程,当 s = 1 s=1 s=1且输出尺寸等于输入尺寸时,解出 p p p的值:
p = ⌈ k − 1 2 ⌉ = ⌊ k 2 ⌋ p = \left\lceil {\frac{{k - 1}}{2}} \right\rceil = \left\lfloor {\frac{k}{2}} \right\rfloor p=2k1=2k
由于填充大小 p p p必须是整数,因此使用向下取整除法(//),即 p = k / / 2 p=k//2 p=k//2 来自动计算填充值,确保了无论卷积核大小 k k k是奇数还是偶数,都可以正确计算出合适的填充大小,使得在步长为1的情况下,输出尺寸尽可能接近输入尺寸。对于非对称的卷积核,分别对每个维度应用此规则以保证各自的输出尺寸匹配相应的输入尺寸。


Conv类

__init__成员函数

构造函数,初始化自定义的卷积模块,通常包含卷积层、批量归一化层和激活函数。调用了【models/common.py】的autopad函数。

def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True):  # ch_in, ch_out, kernel, stride, padding, groups
    """
    构造函数,初始化自定义的卷积模块
    :param c1:输入通道数
    :param c2:输出通道数
    :param k:卷积核大小,默认为 1
    :param s:步幅,默认为 1
    :param p:填充,默认为None(不指定为None会自动计算填充值)
    :param g:分组卷积的分组数,默认为 1(标准卷积)
    :param act:是否使用激活函数,默认为True(使用nn.SiLU()),False则使用nn.Identity()(相当于没使用任何函数),提供了指定激活函数对象则使用指定的激活函数
    """
    super().__init__()
    # 创建一个二维卷积层
    self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False)
    # 创建一个批量归一化层
    self.bn = nn.BatchNorm2d(c2)
    # 设置激活函数
    self.act = nn.SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())

本博文讲解的代码原论文的源码,是YOLOV5团队复现改进后的YOLOV3代码,因此自定义的卷积模块的结构与原论文的结构在激活函数的选择上有所不同。

在YOLOV5中,SiLU替换了早期版本中的Leaky ReLU,显著提升了模型的性能。在EfficientNet系列中,SiLU成为了默认激活函数。

forward成员函数

前向传播,执行自定义的卷积模块。

def forward(self, x):
    """
    前向传播,执行模块
    :param x:输入数据张量
    :return:卷积块的输出
    """
    return self.act(self.bn(self.conv(x)))

forward_fuse成员函数

融合前向传播,跳过了批量归一化层,推理阶段使用可以提高计算效率。

def forward_fuse(self, x):
    """
    融合前向传播,跳过了批量归一化层,推理阶段使用可以提高计算效率
    :param x:输入数据张量
    :return:卷积块的输出
    """
    return self.act(self.conv(x))

Bottleneck类

__init__成员函数

自定义的瓶颈模块,通常用于构建类似残差网络的残差块。

def __init__(self, c1, c2, shortcut=True, g=1, e=0.5):  # ch_in, ch_out, shortcut, groups, expansion
    """
    构造函数,构建类似残差网络的残差块
    :param c1:输入通道数
    :param c2:输出通道数
    :param shortcut:是否使用残差连接,默认为 True
    :param g: 分组卷积的分组数,默认为1(标准卷积)
    :param e:扩展比例,用于计算中间隐藏层的通道数默认为 0.5
    """
    super().__init__()
    c_ = int(c2 * e)  # 计算中间隐藏层的通道数
    self.cv1 = Conv(c1, c_, 1, 1)       # 第一层1×1卷积层
    self.cv2 = Conv(c_, c2, 3, 1, g=g)  # 第二层3×3卷积层
    self.add = shortcut and c1 == c2    # 判断是否添加残差连接:使用残差连接且输入通道数等于输出通道数

原论文中YOLOV3的瓶颈模块都是带有残差连接的,同时YOLOV3的结构中有很多1×1卷积+3×3卷积这种顺次排列的情况,与残差连接的瓶颈模块相似(但不是瓶颈模块)。因此,Ultralytics-YOLOV3代码为了yaml配置文件的简洁明了,将二则结合起来成了新的瓶颈模块,即可以选择是否激活残差连接。

如下图标记的配置文件部分,假设不结合在一起,每个Bottleneck就要单独写俩个Conv,分别是1×1 Conv和3×3 Conv。

forward成员函数

前向传播,执行自定义的瓶颈模块。

def forward(self, x):
    """
    前向传播,执行模块
    :param x:输入数据张量
    :return:瓶颈模块的输出
    """
    # 如果进行残差连接,则将输入x和瓶颈模块的输出结果相加
    return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))

Concat类

__init__成员函数

用于在深度学习模型中实现张量的拼接操作。

def __init__(self, dimension=1):
    """
    构造函数,用于实现张量在指定维度上的拼接操作
    :param dimension: 指定拼接的维度,默认为 1
    """
    super().__init__()
    self.d = dimension

forward成员函数

前向传播,执行拼接。

def forward(self, x):
    """
    前向传播,执行拼接
    :param x:输入数据张量列表
    :return:拼接后的张量
    """
    # 将输入张量列表沿着指定维度拼接成新的单一张量
    return torch.cat(x, self.d)

DetectMultiBackend类

__init__成员函数

构造函数,通用的模型加载器支持多种深度学习框架。调用了【models/experimental.py】的attempt_load函数。

def __init__(self, weights='yolov3.pt', device=None, dnn=True):
     """
     构造函数,通用的模型加载器支持多种深度学习框架
     :param weights:模型权重文件路径,默认为'yolov3.pt'
     :param device:指定设备(GPU或CPU),默认为 None
     :param dnn:是否使用OpenCV DNN模块加载ONNX模型,默认为 True
     """
     super().__init__()
     w = str(weights[0] if isinstance(weights, list) else weights)   # 要是权重路径列表则选择第一个权重路径,否则直接使用给的权重路径

     # 提取权重文件的后缀名,并检查其是否属于支持的后缀列表
     suffix, suffixes = Path(w).suffix.lower(), ['.pt', '.onnx', '.tflite', '.pb', '', '.mlmodel']
     #   weights不同深度学习框架的用法:
     #   PyTorch:                *.pt
     #   TorchScript:            *.torchscript.pt
     #   CoreML:                 *.mlmodel
     #   TensorFlow:             *_saved_model
     #   TensorFlow:             *.pb
     #   TensorFlow Lite:        *.tflite
     #   ONNX Runtime:           *.onnx
     #   OpenCV DNN:             *.onnx with dnn=True
     check_suffix(w, suffixes)

     pt, onnx, tflite, pb, saved_model, coreml = (suffix == x for x in suffixes)     # 根据后缀名判断模型的后端类型
     jit = pt and 'torchscript' in w.lower()     # 为PyTorch模型且包含torchscript关键字则标记为TorchScript模型
     stride, names = 64, [f'class{i}' for i in range(1000)]  # 生成一个包含1000个类名的列表

     # 根据不同的后端类型加载相应的模型:
     if jit:  # TorchScript模型
         # 打印加载 TorchScript 模型的日志信息
         LOGGER.info(f'Loading {w} for TorchScript inference...')
         extra_files = {'config.txt': ''}  # 参数用于提取模型的额外元数据
         # 使用torch.jit.load加载模型,并提取元数据
         model = torch.jit.load(w, _extra_files=extra_files)
         if extra_files['config.txt']:   # 存在config.txt文件,解析元数据
             d = json.loads(extra_files['config.txt'])       # 使用json.loads解析内容
             stride, names = int(d['stride']), d['names']    # 提取步长stride和类别名称names

     elif pt:  # PyTorch模型
         from models.experimental import attempt_load  # 从models.experimental模块中导入attempt_load函数,避免循环依赖问题
         # 如果文件名包含torchscript使用torch.jit.load加载模型,否则使用attempt_load函数加载权重文件
         model = torch.jit.load(w) if 'torchscript' in w else attempt_load(weights, map_location=device)
         stride = int(model.stride.max())    # 计算模型的最大步长
         names = model.module.names if hasattr(model, 'module') else model.names  # 获取类别名称

     elif coreml:  # CoreML模型(苹果设备上的优化推理框架,适合部署到iOS和macOS平台)
         import coremltools as ct
         model = ct.models.MLModel(w)    # 加载 CoreML 格式的模型

     elif dnn:  # ONNX模型(OpenCV DNN)
         # 打印加载ONNX模型的日志信息
         LOGGER.info(f'Loading {w} for ONNX OpenCV DNN inference...')
         # 确保安装了OpenCV的版本不低于4.5.4
         check_requirements(('opencv-python>=4.5.4',))
         # 使用OpenCV的DNN模块加载ONNX模型
         net = cv2.dnn.readNetFromONNX(w)

     elif onnx:  #ONNX模型(ONNX Runtime)
         # 打印加载ONNX模型的日志信息
         LOGGER.info(f'Loading {w} for ONNX Runtime inference...')
         # 确保安装了onnx和onnxruntime库,如果设备支持 CUDA则安装onnxruntime-gpu版本
         check_requirements(('onnx', 'onnxruntime-gpu' if torch.has_cuda else 'onnxruntime'))
         import onnxruntime
         # 使用onnxruntime.InferenceSession创建推理会话
         session = onnxruntime.InferenceSession(w, None)

     else:   # TensorFlow模型
         import tensorflow as tf
         if pb:  # TensorFlow冻结图模型
             # 定义wrap_frozen_graph函数
             def wrap_frozen_graph(gd, inputs, outputs):
                 #  # 使用tf.compat.v1.wrap_function包装冻结图
                 x = tf.compat.v1.wrap_function(lambda: tf.compat.v1.import_graph_def(gd, name=""), [])
                 # prune方法用于修剪图,提取指定的输入和输出节点
                 return x.prune(tf.nest.map_structure(x.graph.as_graph_element, inputs),
                                tf.nest.map_structure(x.graph.as_graph_element, outputs))
             # 打印加载.pb文件的日志信息
             LOGGER.info(f'Loading {w} for TensorFlow *.pb inference...')
             # 创建一个空的GraphDef对象
             graph_def = tf.Graph().as_graph_def()
             # 使用ParseFromString方法从文件中读取冻结图的定义
             graph_def.ParseFromString(open(w, 'rb').read())
             # 调用wrap_frozen_graph函数:传入冻结图定义,输入节点x:0和输出节点Identity:0,返回修剪后的对象用于后续推理
             frozen_func = wrap_frozen_graph(gd=graph_def, inputs="x:0", outputs="Identity:0")

         elif saved_model:   # TensorFlow SavedModel格式
             # 打印加载 SavedModel 的日志信息
             LOGGER.info(f'Loading {w} for TensorFlow saved_model inference...')
             # 返回一个Keras模型对象直接用于推理
             model = tf.keras.models.load_model(w)

         elif tflite:  # TensorFlow Lite模型
             if 'edgetpu' in w.lower():  #  Edge TPU模型
                 # 打印加载Edge TPU模型的日志信息
                 LOGGER.info(f'Loading {w} for TensorFlow Edge TPU inference...')
                 # 使用 tflite_runtime.interpreter库加载TFLite模型
                 import tflite_runtime.interpreter as tfli
                 # 根据操作系统类型选择对应 Edge TPU运行库路径
                 delegate = {'Linux': 'libedgetpu.so.1',  # install https://coral.ai/software/#edgetpu-runtime
                             'Darwin': 'libedgetpu.1.dylib',
                             'Windows': 'edgetpu.dll'}[platform.system()]
                 # tfli.load_delegate加载代理,tfli.Interpreter加载模型并传入代理
                 interpreter = tfli.Interpreter(model_path=w, experimental_delegates=[tfli.load_delegate(delegate)])
             else:   # 普通TensorFlow Lite模型
                 # 打印加载 TFLite 模型的日志信息
                 LOGGER.info(f'Loading {w} for TensorFlow Lite inference...')
                 # 使用tf.lite.Interpreter加载模型
                 interpreter = tf.lite.Interpreter(model_path=w)
             # 调用allocate_tensors方法分配张量内存
             interpreter.allocate_tensors()
             # get_input_details和get_output_details获取输入和输出的详细信息(形状、数据类型等)
             input_details = interpreter.get_input_details()
             output_details = interpreter.get_output_details()
     self.__dict__.update(locals())  # 更新类属性,将所有局部变量赋值给类实例的属性

forward成员函数

前向传播,实现了对不同后端模型的推理逻辑。调用了【utils/general.py】的xywh2xyxy函数。

def forward(self, im, augment=False, visualize=False, val=False):
    """
    实现了对不同后端模型的推理逻辑
    :param im:输入图像张量
    :param augment:是否启用数据增强(仅适用于部分模型)
    :param visualize:是否可视化中间特征图(仅适用于部分模型)
    :param val:是否处于验证模式(影响返回值的格式)
    :return: 推理结果(通常为检测框,置信度和类别信息)
    """
    # 输入数据的基本信息
    b, ch, h, w = im.shape  # 批次大小,通道数,图像高度,图像宽度
    if self.pt:  # PyTorch模型推理
        # 使用self.model 进行推理
        # 如果是TorchScript模型直接调用模型,否则传递augment 和 visualize参数
        y = self.model(im) if self.jit else self.model(im, augment=augment, visualize=visualize)
        # 如果处于验证模式返回完整的输出;否则仅返回第一个输出,通常是检测结果
        return y if val else y[0]

    elif self.coreml:  # CoreML模型推理
        im = im.permute(0, 2, 3, 1).cpu().numpy()  # 输入张量从PyTorch的BCHW格式转换为NumPy的BHWC格式
        im = Image.fromarray((im[0] * 255).astype('uint8'))     # NumPy数组转换为PIL图像并缩放到[0, 255]范围
        # im = im.resize((192, 320), Image.ANTIALIAS)
        y = self.model.predict({'image': im})  # 使用self.model.predict进行推理获取检测框坐标和置信度
        # 检测框坐标由(xywh)转换为(x1y1x2y2)并反归一化
        box = xywh2xyxy(y['coordinates'] * [[w, h, w, h]])
        # 提取置信度最大值及其对应的类别索引
        conf, cls = y['confidence'].max(1), y['confidence'].argmax(1).astype(np.float)
        # 将检测框,置信度和类别信息拼接成一个NumPy数组
        y = np.concatenate((box, conf.reshape(-1, 1), cls.reshape(-1, 1)), 1)

    elif self.onnx:  # ONNX模型推理
        im = im.cpu().numpy()  # 将输入张量从PyTorch转换为NumPy数组
        if self.dnn:            # OpenCV DNN加载ONNX模型
            self.net.setInput(im)   # 使用self.net.setInput设置输入数据
            y = self.net.forward()  # 调用 self.net.forward 进行推理
        else:  # ONNX Runtime加载ONNX模型
            # 调用self.session.run进行推理,指定输入和输出名称
            y = self.session.run([self.session.get_outputs()[0].name], {self.session.get_inputs()[0].name: im})[0]

    else:  # TensorFlow模型推理
        im = im.permute(0, 2, 3, 1).cpu().numpy()   # 输入张量从PyTorch的BCHW格式转换为NumPy的BHWC格式
        if self.pb:     # TensorFlow冻结图模型
            # 将输入张量转换为TensorFlow常量,调用self.frozen_func进行推理
            y = self.frozen_func(x=self.tf.constant(im)).numpy()
        elif self.saved_model:      # SavedModel 格式
            # 调用self.model进行推理,指定 training=False
            y = self.model(im, training=False).numpy()
        elif self.tflite:   # TensorFlow Lite模型
            # 获取输入和输出的详细信息
            input, output = self.input_details[0], self.output_details[0]
            int8 = input['dtype'] == np.uint8  # 如果是量化模型int8
            if int8:
                # # 对输入数据进行反量化
                scale, zero_point = input['quantization']
                im = (im / scale + zero_point).astype(np.uint8)
            # 使用self.interpreter.set_tensor设置输入数据
            self.interpreter.set_tensor(input['index'], im)
            # 调用 self.interpreter.invoke 进行推理
            self.interpreter.invoke()
            # 获取输出张量
            y = self.interpreter.get_tensor(output['index'])
            # 如果是量化模型,对输出数据进行重新量化
            if int8:
                scale, zero_point = output['quantization']
                y = (y.astype(np.float32) - zero_point) * scale
        # 反归一化检测框
        y[..., 0] *= w  # x
        y[..., 1] *= h  # y
        y[..., 2] *= w  # w
        y[..., 3] *= h  # h
    # 将推理结果转换为 PyTorch张量,统一返回格式
    y = torch.tensor(y)
    # 如验证模式返回(y, []),否则直接返回y
    return (y, []) if val else y

总结

尽可能简单、详细的介绍了核心文件common.py文件的作用:存放着YOLOV3网络搭建常见的通用模块。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值