当前位置: 首页 > news >正文

【YOLOv3】 源码总体结构分析

概述

框架与代码结构分析

  • train.py文件调用Backbone层以及Neck层中的模块
  • Backbone层对应的是model.py和yolo.py文件
  • Neck中的具实现存在于模型定义文件中
  • Head部分的实现细节同样在模型定义文件中,但是train.py文件中负责训练的关键部分,也就是损失计算梯度更新参数优化
  • 后处理阶段:detect.py

模块对应关系

Backbone

该模块的主要功能就是特则提取网络,主要负责提取多层次特征

解析backbone部分网络结构

  •  对应Backbone模块
  • 负责将输入图像(416×416×3)逐步下采样,生成多个特征层(52×52、26×26、13×13)
def parse_model(d, ch): # 将解析的网络模型结构作为输入,是字典形式,输入通道数(通常为3)
    """
        主要功能:parse_model模块用来解析模型文件(从Model中传来的字典形式),并搭建网络结构。
        在上面Model模块的__init__函数中调用

        这个函数其实主要做的就是: 更新当前层的args(参数),计算c2(当前层的输出channel) =>
                              使用当前层的参数搭建当前层 =>
                              生成 layers + save

        :params d: model_dict 模型文件 字典形式 yolov3.yaml中的网络结构元素 + 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}")
    print(f"\n{'':>3}{'from':>18}{'n':>3}{'params':>10}  {'module':<40}{'arguments':<30}")

    # 读取d字典中的anchors和parameters(nc、depth_multiple、width_multiple)
    #  nc(number of classes)数据集类别个数;
    # depth_multiple,通过深度参数depth gain在搭建每一层的时候,实际深度 = 理论深度(每一层的参数n) * depth_multiple,起到一个动态调整模型深度的作用。
    # width_multiple,在模型中间层的每一层的实际输出channel = 理论channel(每一层的参数c2) * width_multiple,起到一个动态调整模型宽度的作用。
    anchors, nc, gd, gw = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple']

    """
    如果anchors是一个列表,则计算锚点的数量na。
    具体来说,取anchors列表的第一个元素的长度除以2,
    因为每个锚点由两个值(宽度和高度)表示。如果anchors不是列表,则直接使用 anchors 的值。
    """
    na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors  # na为每个检测头的anchor数量
    no = na * (nc + 5)  # 计算输出数量 no。输出数量等于锚点数量乘以(类别数nc加上5)。这里的5包括4个边界框坐标(x, y, w, h)和1个置信度分数。

    # 开始搭建网络
    # layers: 保存每一层的层结构
    # save: 记录下所有层结构中from中不是-1的层结构序号
    # c2: 保存当前层的输出channel
    layers, save, c2 = [], [], ch[-1]  # layers: 保存每一层的层结构,save: 记录下所有层结构中from中不是-1的层结构序号,c2: 保存当前层的输出channel
    for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']):  # 遍历模型的backbone和head部分,获取from, number, module, args
        m = eval(m) if isinstance(m, str) else m  # 如果m是字符串,则使用eval函数将其转换为实际的模块类或函数,计算该模块的值。
        for j, a in enumerate(args):
            try:
                # 如果 a 是一个字符串,则使用 eval(a) 计算其值,并将结果赋给 args[j]
                # 如果 a 不是字符串,则直接将 a 赋给 args[j]。
                args[j] = eval(a) if isinstance(a, str) else a
            except NameError:
                pass
        # print("argshaha", args)

        # 该部分借用yolov5算法的中方法,利用调整系数gd来改变对应模块的重复次数,以达到增大模型大小的目标
        # 原本的yolov3是没有这个功能的,该版本的代码传承了Ultralytics公司的,yolov5就是该公司出品的
        n = n_ = max(round(n * gd), 1) if n > 1 else n  # depth gain
        # if m in [Conv, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, MixConv2d, Focus, CrossConv,
        #          BottleneckCSP, C3, C3TR, C3SPP, C3Ghost]:

        if m in [Conv,  Bottleneck,  SPP,  MixConv2d, Focus, CrossConv]:
            c1, c2 = ch[f], args[0]  # 获取当前层输入通道数 c1 和输出通道数 c2。
            if c2 != no:  # if not output # 判断是否等于输出通道大小。
                # make_divisible 函数的作用是将输入x调整为大于或等于x且能被divisor整除的最小整数。
                # 它使用 math.ceil 函数来实现这一目的。
                # 其中调整系数gw来改变对应模块的通道大小
                # 原本的yolov3是没有这个功能的,该版本的代码传承了Ultralytics公司的,yolov5就是该公司出品的
                c2 = make_divisible(c2 * gw, 8)
            args = [c1, c2, *args[1:]]  # 更新 args,将输入通道数 c1 和调整后的输出通道数 c2 作为新的参数列表的前两个元素。
        elif m is nn.BatchNorm2d:
            args = [ch[f]]  # 仅将输入通道数 ch[f] 作为参数 args。
        elif m is Concat:
            c2 = sum(ch[x] for x in f)  # 计算多个输入通道数 ch[x] 的总和,得到新的输出通道数 c2。
        elif m is Detect:
            args.append([ch[x] for x in f]) # 在参数 args 中附加包含输入通道数的列表 ch[x]。
            if isinstance(args[1], int):  # number of anchors
                args[1] = [list(range(args[1] * 2))] * len(f)  #如果 args[1] 是整数,则将其转换为包含适当数量的锚框数的列表。
        elif m is Contract:
            c2 = ch[f] * args[0] ** 2  # 根据输入通道数 ch[f] 和参数 args[0] 的平方,计算新的输出通道数 c2。
        elif m is Expand:
            c2 = ch[f] // args[0] ** 2  # 根据输入通道数 ch[f] 和参数 args[0] 的平方,计算新的输出通道数 c2
        else:
            c2 = ch[f]  # 其他的情况,默认将当前输入通道数 ch[f] 作为输出通道数 c2

        # 在Python中,前面的*符号用于解包参数列表。*args 允许你将一个参数列表传递给函数
        # 而在函数内部可以将这个参数列表解包成单独的参数。
        # 义了一个变量m_,其值取决于变量n的大小。如果n大于1,则创建一个包含n个m(*args)实例的nn.Sequential模块;
        # 否则,直接创建一个 m(*args) 实例。具体来说,这段代码是在处理神经网络模块的堆叠和实例化。
        m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args)  # module
        # 这行代码将模块m转换为字符串,并截取其类型字符串的中间部分(去掉前8个字符和最后2个字符),然后去掉__main__.前缀。
        t = str(m)[8:-2].replace('__main__.', '')  # module type

        # 这行代码计算模块m_ 中所有参数的总数量。m_.parameters()
        # 返回模块的参数迭代器,x.numel() 返回每个参数的元素数量,sum 计算所有参数的总数量。
        np = sum(x.numel() for x in m_.parameters())  # number params
        # 这行代码将索引i、'from'索引f、模块类型字符串t、参数数量np附加到模块m_上,方便后续使用。
        m_.i, m_.f, m_.type, m_.np = i, f, t, np  # attach index, 'from' index, type, number params
        print(f'{i:>3}{str(f):>18}{n_:>3}{np:10.0f}  {t:<40}{str(args):<30}')  # print
        # 将满足条件的元素添加到 save 列表中
        # 将模块 m_ 添加到 layers 列表中。
        save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1)  # append to savelist
        layers.append(m_)

        # 初始化列表ch,并不断保存输出通道数到该列表中。
        if i == 0:
            ch = []
        ch.append(c2)

    return nn.Sequential(*layers), sorted(save)

Neck

前向推理脚本(detect.py)

总体逻辑

主要流程

  • 初始化配置:解析命令行参数,设置设备、模型路径等
  • 加载模型和参数:选择设备,加载模型权重,设置模型参数(如半精度)
  • 加载数据:根据输入源(图片、视频、流等)加载数据
  • 推理过程:对每一帧或图片进行前向传播,得到预测结果
  • 后处理:应用非极大值抑制(NMS),绘制边界框,保存或展示结果
  • 结果输出:保存检测结果,打印推理速度等信息

模块分析

源码中将整个推理过程都统一放在了一个run函数中,仅挑选重要的组成学习

初始化,参数分析

def run(weights=ROOT / 'yolov3.pt',  # 模型文件路径,默认加载 YOLOv3 模型('yolov3.pt')
        source=ROOT / 'data/images',  # 数据源,可以是文件路径、目录路径、URL、通配符(如 '*.jpg'),或者 '0' 表示使用摄像头
        imgsz=640,  # 推理时输入图像的大小,单位为像素,默认为 640(图像会被缩放到此尺寸进行处理)
        conf_thres=0.25,  # 置信度阈值,只有置信度大于该值的检测结果才会被保留
        iou_thres=0.45,  # 非极大值抑制(NMS)中的 IOU 阈值,IOU 超过此值的重叠框会被抑制
        max_det=1000,  # 每张图像的最大检测框数,默认最大为 1000 个
        device='',  # 设备设置,可以是 '0'、'0,1' 等 GPU 设备索引,或 'cpu' 表示使用 CPU 进行推理
        view_img=False,  # 是否显示检测结果图像,默认为 False,即不显示
        save_txt=False,  # 是否将检测结果保存为文本文件 (*.txt),默认为 False
        save_conf=False,  # 是否在保存的文本文件中包含每个检测框的置信度,默认为 False
        save_crop=False,  # 是否保存裁剪后的预测框图像,默认为 False
        nosave=False,  # 是否不保存图像/视频,默认为 False,保存处理后的图像
        classes=None,  # 按类别过滤,指定要检测的类别,默认情况下检测所有类别。例如,传入 [0] 只检测类别 0 的物体
        agnostic_nms=False,  # 是否启用类别无关的非极大值抑制(NMS),默认 False
        augment=False,  # 是否进行增强推理,增强推理使用多种图像变换来提高模型鲁棒性,默认为 False
        visualize=False,  # 是否可视化特征图,默认为 False
        update=False,  # 是否自动更新模型权重,默认为 False
        project=ROOT / 'runs/detect',  # 结果保存的根目录路径,默认保存在 'runs/detect' 文件夹中
        name='exp',  # 结果保存的项目名称,默认是 'exp',可以根据需要修改
        exist_ok=False,  # 是否允许已存在的文件夹名称,如果为 False,则会递增文件夹名(例如 'exp1', 'exp2' 等)
        line_thickness=3,  # 边界框的线条厚度(像素),默认为 3
        hide_labels=False,  # 是否隐藏标签文字,默认为 False,即显示标签
        hide_conf=False,  # 是否隐藏置信度值,默认为 False,即显示置信度
        half=False,  # 是否启用 FP16 半精度推理,以加速计算和节省内存,默认为 False
        dnn=False,  # 是否使用 OpenCV DNN 模块进行 ONNX 推理,默认为 False,使用 PyTorch 推理
        ):

初始化配置

# ===================================== 1、初始化一些配置 =====================================

# 将 source 转换为字符串类型,确保后续操作中 source 是一个路径字符串
source = str(source)

# 根据 'nosave' 和 'source' 是否是文本文件来决定是否保存推理图像
# 'save_img' 表示是否保存推理后的图像,只有在不为 'nosave' 且 source 不是文本文件时才保存
save_img = not nosave and not source.endswith('.txt')  # 是否保存推理图像

# 检查 source 是否为文件格式(图像或视频),通过文件扩展名判断
# 'is_file' 为 True 表示 source 是有效的图像或视频文件
is_file = Path(source).suffix[1:] in (IMG_FORMATS + VID_FORMATS)  # 检查是否为文件

# 检查 source 是否为 URL,如果 URL 以 'rtsp://'、'rtmp://'、'http://' 或 'https://' 开头,则为 URL
# 'is_url' 为 True 表示 source 是一个 URL 地址
is_url = source.lower().startswith(('rtsp://', 'rtmp://', 'http://', 'https://'))  # 检查是否为 URL

# 检查 source 是否为摄像头输入
# 如果 source 是数字(如 '0' 或 '1',表示摄像头索引)或以 '.txt' 结尾(表示文件列表),
# 或者是 URL 并且不是一个文件,则认为是摄像头输入
webcam = source.isnumeric() or source.endswith('.txt') or (is_url and not is_file)  # 检查是否为摄像头输入

# 如果 source 是 URL 且同时是一个文件,执行文件下载操作
# 使用 check_file 函数来检查并下载文件
if is_url and is_file:
    source = check_file(source)  # 下载文件

# 创建保存结果的目录路径
# 'save_dir' 表示存储推理结果的目录,使用 'increment_path' 函数来确保目录名称的唯一性
# 如果已存在相同目录,则会递增名称(例如:'exp', 'exp1', 'exp2' 等)
save_dir = increment_path(Path(project) / name, exist_ok=exist_ok)  # 递增运行目录

# 创建存储文件的目录
# 根据是否保存文本文件('save_txt')来选择不同的子目录:
# - 如果保存文本文件,创建 'labels' 子目录
# - 如果不保存文本文件,直接在 'save_dir' 目录下创建保存文件夹
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True)  # 创建目录

加载模块调整参数(训练模型准备阶段)

# ===================================== 2、载入模型和模型参数并调整模型 =====================================

# 选择推理设备,通过 'select_device' 函数选择使用的设备(如 CPU 或 GPU)
device = select_device(device)  # 选择设备

# 加载 YOLOv3 模型,'DetectMultiBackend' 用于支持不同后端(PyTorch、ONNX、TensorRT 等)
# 'weights' 是模型的路径,'device' 是设备(如 CPU 或 GPU),'dnn' 表示是否使用 OpenCV DNN 后端
model = DetectMultiBackend(weights, device=device, dnn=dnn)  # 加载模型

# 获取模型的一些重要属性:
# - stride: 模型的步幅(影响推理时的图像尺寸选择)
# - names: 模型支持的类别名称(物体检测的类别)
# - pt: 是否是 PyTorch 模型
# - jit: 是否是 TorchScript 模型
# - onnx: 是否是 ONNX 模型
stride, names, pt, jit, onnx = model.stride, model.names, model.pt, model.jit, model.onnx  # 获取模型属性

# 检查输入图像的大小,确保它与模型的步幅(stride)匹配
# 'check_img_size' 会根据模型的步幅调整输入图像大小,确保图像尺寸是步幅的整数倍
imgsz = check_img_size(imgsz, s=stride)  # 检查图像大小

# 设置半精度推理,'half' 表示是否启用 FP16(半精度浮点数)推理
# 如果设备是 GPU 且模型是 PyTorch 模型,则启用半精度推理
half &= pt and device.type != 'cpu'  # 半精度仅支持在 CUDA 上的 PyTorch

# 如果是 PyTorch 模型,根据 'half' 参数设置模型的精度:
# 如果 'half' 为 True,则将模型转换为 FP16 精度;否则,使用 FP32(默认精度)
if pt:
    model.model.half() if half else model.model.float()  # 使用半精度或单精度浮点数

加载推理数据

  • 根据输入源(摄像头或文件夹路径)选择不同的数据加载器
  • 如果使用 GPU 且为 PyTorch 模型,执行模型预热,以便在第一次推理时减少延迟
  • 初始化用于视频流输出的路径和写入器
  • 准备数据和性能监控变量,开始推理过程
# ===================================== 3、加载推理数据 =====================================

# 设置数据加载器
if webcam:
    # 如果输入是摄像头视频流:
    
    # 使用 check_imshow() 检查是否支持显示图像
    # 该函数通常会检查显示窗口是否可以成功打开,适用于显示推理结果
    view_img = check_imshow()  # 检查是否可以显示图像

    # 设置 cudnn.benchmark 为 True 可以加速恒定图像大小的推理
    # cudnn.benchmark 通常用于 GPU 上的卷积操作,如果图像尺寸恒定,可以加速计算
    cudnn.benchmark = True  # 设置为 True 可以加速恒定图像大小的推理

    # 使用 LoadStreams 加载视频流数据(如摄像头流、RTSP、RTMP 等)
    # 'source' 是输入的视频流源,'img_size' 为输入图像的大小,'stride' 为模型的步幅,'auto' 取决于是否为 PyTorch 模型
    dataset = LoadStreams(source, img_size=imgsz, stride=stride, auto=pt and not jit)  # 加载流数据

    # 批量大小为数据流的长度,即摄像头输入的流数
    bs = len(dataset)  # 批量大小

else:
    # 如果不是摄像头输入,加载本地文件目录中的图像或视频文件
    
    # 使用 LoadImages 加载文件夹中的图像或视频文件
    # 'source' 为文件夹路径,'img_size' 为输入图像的大小,'stride' 为模型的步幅,'auto' 取决于是否为 PyTorch 模型
    dataset = LoadImages(source, img_size=imgsz, stride=stride, auto=pt and not jit)  # 加载图像数据

    # 批量大小设为 1,因为一般是逐张加载图像
    bs = 1  # batch_size

# 初始化视频路径和写入器
# 如果输入是视频流或摄像头输入,我们需要为每个流初始化一个视频路径和视频写入器
vid_path, vid_writer = [None] * bs, [None] * bs  # 视频路径和写入器初始化

# 如果使用的是 PyTorch 模型且设备类型不是 'cpu',执行预热步骤:
if pt and device.type != 'cpu':
    # 使用全零张量作为输入进行预热
    # 这里通过创建一个形状为 (1, 3, *imgsz) 的全零张量,并将其移动到指定的设备上('device')进行模型的预热
    # 这样做是为了在推理前提前加载模型并优化性能
    # 'type_as' 确保输入张量的数据类型与模型参数的类型匹配
    model(torch.zeros(1, 3, *imgsz).to(device).type_as(next(model.model.parameters())))  # warmup

# 初始化时间和数据计数变量
# dt 存储不同阶段的时间(推理时间等),seen 用于记录处理过的图像数量
dt, seen = [0.0, 0.0, 0.0], 0  # 初始化变量 dt 和 seen

推理过程(重点)

  • 遍历数据集中的每张图像或视频流
  • 对每张图像进行处理(格式转换、归一化、维度扩展等)
  • 使用模型进行推理,并记录每个阶段的时间(数据处理、推理、NMS)
  • 对推理结果进行非极大值抑制,去除冗余的检测框
  • (可选)如果启用可视化,保存图像处理结果
# ===================================== 5、正式推理 =====================================

# 遍历数据集中的每张图像或每个视频流
for path, im, im0s, vid_cap, s in dataset:
    # path: 当前图像或视频文件的路径
    # im: 经 resize 和 padding 后的图像(用于推理的标准尺寸图像)
    # im0s: 原始尺寸的图像(用于保存或展示原图)
    # vid_cap: 如果是读取图片,则为 None;如果是读取视频,则为视频源对象

    # 5.1、对每张图片或每个视频进行前向推理
    t1 = time_sync()  # 记录推理开始时间

    # 将图像数据转为 PyTorch tensor,并移动到指定的设备(CPU 或 GPU)
    im = torch.from_numpy(im).to(device)   # 5.2、处理每一张图片/视频的格式
    
    # 如果需要半精度推理(FP16),将图像数据转换为半精度;否则使用默认的 FP32 精度
    im = im.half() if half else im.float()  # 半精度训练 uint8 转换为 fp16 或 fp32

    # 归一化处理,将图像像素值从 0-255 转换为 0.0 - 1.0
    im /= 255  # 归一化 0 - 255 到 0.0 - 1.0

    # 如果输入图像是三维的(表示 RGB 图像),则在前面添加一个维度,使其变成四维,代表一个 batch
    # 这里需要确保输入图像是 [batch_size, channel, width, height] 这样的四维数据格式
    if len(im.shape) == 3:
        im = im[None]  # expand for batch dim,添加 batch 维度,表示 batch_size=1

    t2 = time_sync()  # 获取当前时间,并进行时间同步
    dt[0] += t2 - t1  # 累加时间差到 dt[0] 中,用于记录数据预处理的时间

    # 如果需要可视化推理结果,则创建一个保存路径,并为其创建目录
    visualize = increment_path(save_dir / Path(path).stem, mkdir=True) if visualize else False

    # 使用模型进行推理预测,'augment' 参数表示是否使用数据增强
    # 'visualize' 参数表示是否进行可视化(保存特征图等)
    pred = model(im, augment=augment, visualize=visualize)

    # 获取当前时间并记录推理过程时间
    t3 = time_sync()
    dt[1] += t3 - t2  # 累加时间差到 dt[1] 中,用于记录推理的时间

    # 使用非极大值抑制(NMS)来处理预测结果,减少冗余的框
    # 'conf_thres' 为置信度阈值,'iou_thres' 为 IOU 阈值,'classes' 为要检测的类别,'agnostic_nms' 表示是否类别无关
    # 'max_det' 控制每张图像检测的最大框数
    pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)

    # 累加 NMS 阶段的时间差到 dt[2] 中
    dt[2] += time_sync() - t3

后处理与数据保存

  • 检测框从标准尺寸映射回原图
  • 可选的保存预测框到文件
  • 将预测结果绘制到图像上并保存
  • 对视频流的处理,确保检测后的视频能够保存下来
# ===================================== 6、保存或打印预测信息 =====================================

# 遍历每张图像的预测结果
for i, det in enumerate(pred):  # per image
    seen += 1  # 统计处理过的图像数

    # 处理 webcam 输入时的批量(batch_size >= 1)
    if webcam:
        p, im0, frame = path[i], im0s[i].copy(), dataset.count
        s += f'{i}: '  # 记录图像索引
    else:
        # 处理非摄像头输入(如文件夹中的单张图像或视频)
        p, im0, frame = path, im0s.copy(), getattr(dataset, 'frame', 0)

    # 将路径转为 Path 类型,方便操作
    p = Path(p)  # to Path
    
    # 保存图片的路径(生成新的文件名)
    save_path = str(save_dir / p.name)  # im.jpg
    
    # 保存预测框坐标的 txt 文件路径
    txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}')  # im.txt
    
    # 打印图片的尺寸信息(宽度和高度)
    s += '%gx%g ' % im.shape[2:]  # 输出图像形状 (w, h)

    # 归一化增益,用于将检测框从标准化尺寸(640x640)映射回原图尺寸
    gn = torch.tensor(im0.shape)[[1, 0, 1, 0]]  # normalization gain whwh gain gn = [w, h, w, h]
    
    # 如果需要保存裁剪图像,将裁剪后的图像赋值给 imc(否则仍使用原图)
    imc = im0.copy() if save_crop else im0  # imc: for save_crop
    
    # 初始化 Annotator 类用于在图像上绘制边框
    annotator = Annotator(im0, line_width=line_thickness, example=str(names))

    # 如果检测到目标物体
    if len(det):
        # 将检测框从标准化大小(640x640)还原回原图大小
        det[:, :4] = scale_coords(im.shape[2:], det[:, :4], im0.shape).round()

        # 打印检测到的物体类别及数量
        for c in det[:, -1].unique():  # 遍历所有检测到的类别
            n = (det[:, -1] == c).sum()  # 每个类别的检测数量
            s += f"{n} {names[int(c)]}{'s' * (n > 1)}, "  # 输出类别名称和数量

        # 保存预测结果
        for *xyxy, conf, cls in reversed(det):
            # 如果需要保存预测框坐标到文件(txt 文件)
            if save_txt:
                # 将预测框坐标从 [xyxy] 转换为 [xywh],并归一化
                xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist()  # normalized xywh
                # 生成保存文件的内容
                line = (cls, *xywh, conf) if save_conf else (cls, *xywh)  # 包含或不包含置信度
                # 将检测框写入到 .txt 文件
                with open(txt_path + '.txt', 'a') as f:
                    f.write(('%g ' * len(line)).rstrip() % line + '\n')

            # 如果需要保存图像、裁剪图像或查看图像(view_img),则在图像上绘制边框
            if save_img or save_crop or view_img:
                c = int(cls)  # 获取类别的整数表示
                # 根据是否隐藏标签/置信度来决定绘制标签的内容
                label = None if hide_labels else (names[c] if hide_conf else f'{names[c]} {conf:.2f}')
                # 在图像上绘制预测框和标签
                annotator.box_label(xyxy, label, color=colors(c, True))
                
                # 如果需要保存裁剪后的图像,则裁剪并保存
                if save_crop:
                    save_one_box(xyxy, imc, file=save_dir / 'crops' / names[c] / f'{p.stem}.jpg', BGR=True)

    # 打印推理时间
    print(f'{s}Done. ({t3 - t2:.3f}s)')

    # 如果需要显示推理结果(显示包含检测框的图像)
    im0 = annotator.result()  # 获取绘制框后的图像
    if view_img:
        # 使用 OpenCV 显示图像
        cv2.imshow(str(p), im0)
        cv2.waitKey(1)  # 等待 1 毫秒刷新图像

    # 如果需要保存检测后的图像或视频
    if save_img:
        if dataset.mode == 'image':  # 如果是图像模式
            # 保存图像文件
            cv2.imwrite(save_path, im0)  # 保存单张图像
        else:  # 如果是视频或实时流模式
            if vid_path[i] != save_path:  # 如果保存路径与当前视频路径不同(表示新的视频)
                vid_path[i] = save_path
                # 释放之前的视频写入器(如果存在)
                if isinstance(vid_writer[i], cv2.VideoWriter):
                    vid_writer[i].release()
                if vid_cap:  # 如果是视频文件
                    fps = vid_cap.get(cv2.CAP_PROP_FPS)  # 获取视频的帧率
                    w = int(vid_cap.get(cv2.CAP_PROP_FRAME_WIDTH))  # 获取视频帧宽度
                    h = int(vid_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))  # 获取视频帧高度
                else:  # 如果是实时流
                    fps, w, h = 30, im0.shape[1], im0.shape[0]  # 设置默认的帧率和图像尺寸
                    save_path += '.mp4'  # 添加文件扩展名
                
                # 创建新的视频写入器
                vid_writer[i] = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h))

            # 将当前帧写入视频
            vid_writer[i].write(im0)

解析命令行参数(训练中参数可以在此处指定)

def parse_opt():
    parser = argparse.ArgumentParser()
    parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'weight/yolov3.pt', help='model path(s)')  # weights: 模型的权重地址 默认 weights/best.pt
    parser.add_argument('--source', type=str, default=ROOT / 'data/images', help='file/dir/URL/glob, 0 for webcam')  # source: 测试数据文件(图片或视频)的保存路径 默认data/images
    parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[416], help='inference size h,w')  # imgsz: 网络输入图片的大小 默认640
    parser.add_argument('--conf-thres', type=float, default=0.6, help='confidence threshold') # conf-thres: object置信度阈值 默认0.25
    parser.add_argument('--iou-thres', type=float, default=0.5, help='NMS IoU threshold')  # iou-thres: 做nms的iou阈值 默认0.45
    parser.add_argument('--max-det', type=int, default=1000, help='maximum detections per image')   # max-det: 每张图片最大的目标个数 默认1000
    parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')  # device: 设置代码执行的设备 cuda device, i.e. 0 or 0,1,2,3 or cpu
    parser.add_argument('--view-img', action='store_true', default=True, help='show results')  # view-img: 是否展示预测之后的图片或视频 默认False
    parser.add_argument('--save-txt', action='store_true', default=True, help='save results to *.txt')  # save-txt: 是否将预测的框坐标以txt文件格式保存 默认False 会在runs/detect/expn/labels下生成每张图片预测的txt文件
    parser.add_argument('--save-conf', action='store_true', default=True, help='save confidences in --save-txt labels')  # save-conf: 是否保存预测每个目标的置信度到预测tx文件中 默认False
    parser.add_argument('--save-crop', action='store_true', default=True, help='save cropped prediction boxes')  # save-crop: 是否需要将预测到的目标从原图中扣出来 剪切好 并保存 会在runs/detect/expn下生成crops文件,将剪切的图片保存在里面  默认False
    parser.add_argument('--nosave', action='store_true', help='do not save images/vidruns/train/exp/weights/best.pteos')  # nosave: 是否不要保存预测后的图片  默认False 就是默认要保存预测后的图片
    parser.add_argument('--classes', nargs='+', type=int, help='filter by class: --classes 0, or --classes 0 2 3')  # classes: 在nms中是否是只保留某些特定的类 默认是None 就是所有类只要满足条件都可以保留, default=[0,6,1,8,9, 7]
    parser.add_argument('--agnostic-nms', action='store_true', help='class-agnostic NMS')  # agnostic-nms: 进行nms是否也除去不同类别之间的框 默认False
    parser.add_argument('--augment', action='store_true', help='augmented inference')  # 是否使用数据增强进行推理,默认为False
    parser.add_argument('--visualize', action='store_true', help='visualize features')  #  -visualize:是否可视化特征图,默认为 False
    parser.add_argument('--update', action='store_true', help='update all models')  # -update: 如果为True,则对所有模型进行strip_optimizer操作,去除pt文件中的优化器等信息,默认为False
    parser.add_argument('--project', default=ROOT / 'runs/detect', help='save results to project/name')  # project: 当前测试结果放在哪个主文件夹下 默认runs/detect
    parser.add_argument('--name', default='exp', help='save results to project/name')  # name: 当前测试结果放在run/detect下的文件名  默认是exp
    parser.add_argument('--exist-ok', action='store_true', default=False, help='existing project/name ok, do not increment')  # -exist-ok: 是否覆盖已有结果,默认为 False
    parser.add_argument('--line-thickness', default=3, type=int, help='bounding box thickness (pixels)')  # -line-thickness:画 bounding box 时的线条宽度,默认为 3
    parser.add_argument('--hide-labels', default=False, action='store_true', help='hide labels')  # -hide-labels:是否隐藏标签信息,默认为 False
    parser.add_argument('--hide-conf', default=False, action='store_true', help='hide confidences')  # -hide-conf:是否隐藏置信度信息,默认为 False
    parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')  # half: 是否使用半精度 Float16 推理 可以缩短推理时间 但是默认是False
    parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference')  # -dnn:是否使用 OpenCV DNN 进行 ONNX 推理,默认为 False
    opt = parser.parse_args()  # 解析命令行参数,并将结果存储在opt对象中
    opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1  # 如果imgsz参数的长度为1,则将其值乘以2;否则保持不变
    print_args(FILE.stem, opt)  # 打印解析后的参数,FILE.stem是文件的名称(不含扩展名)
    return opt

训练脚本(train.py)

总体逻辑

  • 初始化
    • 解析命令行参数(如模型配置、数据集路径、超参数等)
    • 设置训练设备(CPU 或 GPU)
    • 初始化日志记录器和可视化工具(如 WandB)
    • 加载和验证数据集配置
    • 创建训练和验证数据加载器
  • 模型加载与配置
    • 加载预训练权重(如果提供)
    • 根据配置文件初始化模型架构
    • 冻结指定层(如果需要)
  • 优化器与学习率调度器设置
    • 配置优化器(如 SGD 或 Adam)
    • 设置学习率调度策略(如 One Cycle 或线性调度)
  • 训练循环(逐步遍历每个epoch)
    • 训练模式下进行前向传播、损失计算、反向传播和参数更新
    • 可选的多尺度训练和图像权重调整
    • 在特定条件下进行验证
    • 记录和保存训练状态(如最佳模型、最新模型)
  • 保存最终结果

模块分析

参数初始化

# 将各种超参数从opt(一个包含各种配置参数的对象)中提取出来
save_dir, epochs, batch_size, weights, single_cls, evolve, data, cfg, resume, noval, nosave, workers, freeze = \
    Path(opt.save_dir), opt.epochs, opt.batch_size, opt.weights, opt.single_cls, opt.evolve, opt.data, opt.cfg, \
    opt.resume, opt.noval, opt.nosave, opt.workers, opt.freeze

# 创建保存路径相关的目录
w = save_dir / 'weights'  # 设置保存模型权重的路径,例如:runs/train/exp18/weights
(w.parent if evolve else w).mkdir(parents=True, exist_ok=True)  # 如果启用evolve(超参进化),则保存路径为父目录,否则直接保存到w路径
# 如果目录不存在,则创建目录;parents=True表示会创建多级目录,exist_ok=True表示如果目录已存在则不报错

# 设置模型权重文件的路径
last, best = w / 'last.pt', w / 'best.pt'  # 最后训练的权重和最佳权重保存文件的路径

# 加载并输出超参数信息
# 判断hyp(超参数配置)是否为字符串类型(通常是一个文件路径)
if isinstance(hyp, str):
    with open(hyp, errors='ignore') as f:  # 打开文件并忽略可能的错误
        hyp = yaml.safe_load(f)  # 解析YAML格式的超参数配置文件

# 输出日志,记录超参数信息,LOGGER是日志对象
LOGGER.info(colorstr('hyperparameters: ') + ', '.join(f'{k}={v}' for k, v in hyp.items()))
# 这里将超参数信息格式化为 "key=value" 的形式并输出日志

# 将超参数hyp保存为YAML文件到指定目录
with open(save_dir / 'hyp.yaml', 'w') as f:  # 保存超参数到hyp.yaml文件
    yaml.safe_dump(hyp, f, sort_keys=False)  # 将超参数写入文件,并保持原有顺序

# 将opt配置(包含训练配置、路径等)保存为YAML文件
with open(save_dir / 'opt.yaml', 'w') as f:  # 保存训练配置到opt.yaml文件
    yaml.safe_dump(vars(opt), f, sort_keys=False)  # 将opt对象中的所有属性保存为YAML格式

# 变量data_dict初始化为空(可能会在后续的代码中进行填充)
data_dict = None

日志器初始化

if RANK in [-1, 0]:  # 仅在主要进程中初始化日志记录器
    loggers = Loggers(save_dir, weights, opt, hyp, LOGGER)  # 创建日志记录器实例
    if loggers.wandb:  # 如果使用 wandb 进行日志记录
        data_dict = loggers.wandb.data_dict
        if resume:  # 如果是恢复训练
            weights, epochs, hyp = opt.weights, opt.epochs, opt.hyp  # 从 opt 中获取权重、epochs 和超参数

    # 注册回调函数
    for k in methods(loggers):  # 获取 loggers 的方法
        callbacks.register_action(k, callback=getattr(loggers, k))  # 注册每个方法为回调函数

数据集检查与加载

  • 检查和加载数据集配置
  • 确定训练和验证数据的路径
  • 设置类别数量和类别名称
  • 验证配置的一致性
# 配置
plots = not evolve  # 创建绘图
cuda = device.type != 'cpu'  # 检查是否使用 GPU
init_seeds(1 + RANK)  # 初始化随机种子
with torch_distributed_zero_first(LOCAL_RANK):  # 在分布式训练中确保只有一个进程执行
    data_dict = data_dict or check_dataset(data)  # 检查数据集,如果为 None 则验证数据集
train_path, val_path = data_dict['train'], data_dict['val']  # 获取训练和验证路径
nc = 1 if single_cls else int(data_dict['nc'])  # 类别数量
names = ['item'] if single_cls and len(data_dict['names']) != 1 else data_dict['names']  # 类别名称
# 检查类别名称与类别数量是否匹配
assert len(names) == nc, f'{len(names)} names found for nc={nc} dataset in {data}'
is_coco = isinstance(val_path, str) and val_path.endswith('coco/val2017.txt')  # 检查是否为 COCO 数据集

加载与配置模型

  • 检查并加载预训练权重(如果提供)
  • 根据配置文件初始化模型架构
  • 处理模型状态字典的加载,包括排除某些键(如锚框)
# 检查权重文件的后缀名是否为.pt
check_suffix(weights, '.pt')  # 如果权重文件的后缀不是.pt,抛出错误

# 判断权重文件是否为预训练模型文件(即后缀名为.pt)
pretrained = weights.endswith('.pt')  # 如果权重文件以'.pt'结尾,则认为是预训练模型

# 如果是预训练模型(pretrained为True),执行以下操作
if pretrained:
    # 在分布式训练中,确保只有一个进程执行文件下载操作
    with torch_distributed_zero_first(LOCAL_RANK):  # LOCAL_RANK是当前进程的分布式rank(编号)
        weights = attempt_download(weights)  # 如果本地不存在指定的权重文件,则下载

    # 加载预训练模型的检查点(checkpoint)
    ckpt = torch.load(weights, map_location=device)  # 使用指定的设备(device)加载权重文件

    # 创建模型,配置模型的结构。可以通过配置文件(cfg)或检查点文件中的模型配置来创建
    # 如果cfg存在,则使用cfg,否则使用检查点文件ckpt中的模型配置(ckpt['model'].yaml)
    model = Model(cfg or ckpt['model'].yaml, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device)

    # 定义需要排除的键,通常是“anchor”键,防止因模型配置不匹配而导致加载错误
    exclude = ['anchor'] if (cfg or hyp.get('anchors')) and not resume else []

    # 获取检查点的模型状态字典,并将其转换为FP32(浮点32位)格式
    csd = ckpt['model'].float().state_dict()  # 将模型参数的类型转换为浮点数32位
    # 通过intersect_dicts函数取模型状态字典(ckpt)与当前模型状态字典(model)之间的交集
    csd = intersect_dicts(csd, model.state_dict(), exclude=exclude)  # 只加载匹配的部分
    # 加载模型状态字典,strict=False表示允许部分参数不匹配
    model.load_state_dict(csd, strict=False)

    # 记录加载了多少项模型参数
    LOGGER.info(f'从 {weights} 转移了 {len(csd)}/{len(model.state_dict())} 项')  # 输出日志,显示从权重文件加载的参数数量和总参数数量
else:
    # 如果不是预训练模型(即weights不是.pt文件),直接创建一个新的模型
    model = Model(cfg, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device)  # 创建一个新的模型,并将其送到指定设备上(GPU/CPU)

冻结指定层

冻结模型中指定的层,使其在训练过程中不被更新(一般适用于微调预训练模型)

# 冻结层
freeze = [f'model.{x}.' for x in range(freeze)]  # 要冻结的层
for k, v in model.named_parameters():
    v.requires_grad = True  # 允许所有层进行训练
    if any(x in k for x in freeze):  # 如果当前参数名在冻结列表中
        LOGGER.info(f'冻结 {k}')  # 记录冻结的层
        v.requires_grad = False  # 取消该层的梯度计算

图像大小与批量大小设置

  • 确保输入图像大小与模型步幅(stride)兼容
  • 根据模型和图像大小自动调整批量大小(如果未指定)
# Image size
gs = max(int(model.stride.max()), 32)  # 网格大小(最大步幅)
imgsz = check_img_size(opt.imgsz, gs, floor=gs * 2)  # 验证图像大小是 gs 的倍数

# Batch size
if RANK == -1 and batch_size == -1:  # 仅在单 GPU 下,估计最佳批量大小
    batch_size = check_train_batch_size(model, imgsz)  # 检查并确定最佳批量大小

优化器与学习率调度器设置

  • 配置优化器参数组:
    • g0:BatchNorm 层的权重,不进行权重衰减
    • g1:其他权重,进行权重衰减
    • g2:偏置参数,不进行权重衰减
  • 选择优化器类型(SGD 或 Adam)
  • 根据批量大小调整权重衰减
# Optimizer部分的代码开始

nbs = 64  # 名义批量大小(即理论上的批量大小),用于计算和调整权重衰减
# 计算累积梯度的步数,通常在小批量训练时使用。累积梯度的目的是在多次小批量训练后再进行一次优化更新。
accumulate = max(round(nbs / batch_size), 1)  # 计算累积次数,即每次优化前累积的损失步数
# 根据批量大小和累积的步数调整权重衰减(L2正则化)。大批量时,权重衰减应该适当增大
hyp['weight_decay'] *= batch_size * accumulate / nbs  # 按照批量大小的变化调整权重衰减
LOGGER.info(f"Scaled weight_decay = {hyp['weight_decay']}")  # 输出调整后的权重衰减值

# 初始化不同的优化器参数组
g0, g1, g2 = [], [], []  # g0用于存储不带衰减的权重,g1用于存储带衰减的权重,g2用于存储偏置

# 遍历模型中的每一个模块,并根据模块的属性将其加入相应的参数组
for v in model.modules():  # 遍历模型的每一个子模块
    if hasattr(v, 'bias') and isinstance(v.bias, nn.Parameter):  # 检查模块是否有偏置项(bias)
        g2.append(v.bias)  # 将偏置项添加到g2组中,偏置一般不进行L2正则化
    if isinstance(v, nn.BatchNorm2d):  # 检查模块是否是BatchNorm层
        g0.append(v.weight)  # BatchNorm层的权重(不进行权重衰减),添加到g0组
    elif hasattr(v, 'weight') and isinstance(v.weight, nn.Parameter):  # 检查模块是否有权重项
        g1.append(v.weight)  # 普通权重(会进行权重衰减),添加到g1组

# 根据选择的优化器类型初始化优化器
if opt.adam:  # 如果选用了Adam优化器
    optimizer = Adam(g0, lr=hyp['lr0'], betas=(hyp['momentum'], 0.999))  # 使用Adam优化器,调整beta1为动量
else:  # 否则使用SGD优化器
    optimizer = SGD(g0, lr=hyp['lr0'], momentum=hyp['momentum'], nesterov=True)  # 使用SGD优化器,设置nesterov动量

# 为优化器添加新的参数组
# 为g1(带衰减的权重)添加参数组,并设置权重衰减
optimizer.add_param_group({'params': g1, 'weight_decay': hyp['weight_decay']})
# 为g2(偏置)添加参数组,偏置通常不使用权重衰减
optimizer.add_param_group({'params': g2})

# 输出优化器的类型及其参数组的数量和组成信息
LOGGER.info(f"{colorstr('optimizer:')} {type(optimizer).__name__} with parameter groups "
            f"{len(g0)} weight, {len(g1)} weight (no decay), {len(g2)} bias")

# 删除中间变量g0, g1, g2,以节省内存
del g0, g1, g2

学习率调度器

# 学习率调度器
if opt.linear_lr:
    lf = lambda x: (1 - x / (epochs - 1)) * (1.0 - hyp['lrf']) + hyp['lrf']  # 线性学习率调整
else:
    lf = one_cycle(1, hyp['lrf'], epochs)  # 余弦学习率调整,从 1 到 hyp['lrf']
scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf)  # 创建学习率调度器
# plot_lr_scheduler(optimizer, scheduler, epochs)  # 可选:绘制学习率调度曲线

模型指数移动平均

使用 EMA 技术对模型参数进行平滑,以提高模型的泛化能力和稳定性

# 单卡训练: 使用EMA(指数移动平均)对模型的参数做平均
ema = ModelEMA(model) if RANK in [-1, 0] else None

加载预训练检查点

  • 如果加载了预训练检查点,恢复优化器状态、EMA 状态和训练轮数
  • 处理继续训练(resume)的逻辑,确保训练从正确的轮数开始
# 使用预训练模型开始训练
start_epoch, best_fitness = 0, 0.0  # 初始化起始轮数和最佳性能

# 如果是预训练模型(pretrained为True),执行以下步骤
if pretrained:
    # 如果检查点(ckpt)包含优化器的状态,则加载优化器的状态
    if ckpt['optimizer'] is not None:  # 检查优化器状态是否存在
        optimizer.load_state_dict(ckpt['optimizer'])  # 加载优化器的状态字典
        best_fitness = ckpt['best_fitness']  # 更新当前最佳的模型性能(fitness)

    # 如果启用了EMA(指数移动平均),并且检查点中包含EMA的状态,加载EMA状态
    if ema and ckpt.get('ema'):  # 检查是否启用了EMA,并且ckpt中包含ema
        ema.ema.load_state_dict(ckpt['ema'].float().state_dict())  # 加载EMA的状态字典
        ema.updates = ckpt['updates']  # 更新EMA的更新次数

    # 获取训练的起始轮数
    start_epoch = ckpt['epoch'] + 1  # 从检查点中获取已训练的轮数并加1,作为新训练的起始轮数
    if resume:  # 如果是恢复训练模式
        assert start_epoch > 0, f'{weights} training to {epochs} epochs is finished, nothing to resume.'
        # 如果起始轮数小于等于0,说明训练已经完成,不需要继续恢复训练
    
    # 如果计划训练的总轮数(epochs)小于起始轮数,说明训练已经结束,因此跳过剩余轮次
    if epochs < start_epoch:
        LOGGER.info(f"{weights} has been trained for {ckpt['epoch']} epochs. Fine-tuning for {epochs} more epochs.")
        # 输出日志信息,说明检查点的训练已经完成,接下来将进行进一步的微调
        epochs += ckpt['epoch']  # 将总训练轮数加上已完成的轮数,以便进行微调

    # 删除加载的检查点和状态字典,释放内存
    del ckpt, csd  # 删除检查点数据和模型状态字典,避免内存泄漏

多GPU训练设置

  • 如果使用多 GPU,建议使用分布式数据并行(DDP)而非 DataParallel,因为 DDP 性能更好
  • 如果启用了同步 BatchNorm(--sync-bn),转换模型的 BatchNorm 层为同步模式,适用于 DDP 训练
# 是否使用DP mode
if cuda and RANK == -1 and torch.cuda.device_count() > 1:
    LOGGER.warning('WARNING: DP not recommended, use torch.distributed.run for best DDP Multi-GPU results.\n'
                   'See Multi-GPU Tutorial at https://github.com/ultralytics/yolov5/issues/475 to get started.')
    model = torch.nn.DataParallel(model)

# SyncBatchNorm  是否使用跨卡BN
if opt.sync_bn and cuda and RANK != -1:
    model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device)
    LOGGER.info('Using SyncBatchNorm()')

创建数据加载器

  • 创建训练数据加载器,包括数据增强、缓存、图像权重调整等
  • 验证数据集中标签的类别是否超出定义的类别数量
# Trainloader
train_loader, dataset = create_dataloader(
    train_path,
    imgsz,
    batch_size // WORLD_SIZE,
    gs,
    single_cls,
    hyp=hyp,
    augment=True,
    cache=opt.cache,
    rect=opt.rect,
    rank=LOCAL_RANK,
    workers=workers,
    image_weights=opt.image_weights,
    quad=opt.quad,
    prefix=colorstr('train: '),
    shuffle=True
)
# 获取最大标签类
mlc = int(np.concatenate(dataset.labels, 0)[:, 0].max())  # max label class
nb = len(train_loader)  # 批次数量
# 断言标签类不超过类别数
assert mlc < nc, f'Label class {mlc} exceeds nc={nc} in {data}. Possible class labels are 0-{nc - 1}'

验证数据加载器(只存在于主线程)

  • 仅在主进程(或单 GPU)中创建验证数据加载器
  • 绘制标签分布图(如果启用)
  • 检查和调整锚框(如果未禁用自动锚框调整)
  • 运行预训练结束时的回调函数
# Process 0
if RANK in [-1, 0]:
    # 创建验证数据加载器
    val_loader = create_dataloader(val_path, imgsz, batch_size // WORLD_SIZE * 2, gs, single_cls,
                                   hyp=hyp, cache=None if noval else opt.cache, rect=True, rank=-1,
                                   workers=workers, pad=0.5,
                                   prefix=colorstr('val: '))[0]

    if not resume:
        # 合并标签
        labels = np.concatenate(dataset.labels, 0)
        if plots:
            # 绘制标签分布图
            plot_labels(labels, names, save_dir)

        # 检查锚框
        if not opt.noautoanchor:
            check_anchors(dataset, model=model, thr=hyp['anchor_t'], imgsz=imgsz)
        model.half().float()  # 预减锚框精度

    # 运行回调函数
    callbacks.run('on_pretrain_routine_end')

分布式数据并行模式

如果使用 DDP 进行多 GPU 训练,将模型包装为 DDP 模式,以便在多个进程间同步梯度

# DDP模式
if cuda and RANK != -1:
    # 将模型包装为分布式数据并行模式
    model = DDP(model, device_ids=[LOCAL_RANK], output_device=LOCAL_RANK)

相关文章:

  • Linux部署dnsmasq软件
  • 【前端】【面试】【功能函数】写一个JavaScript树形结构操作函数:`filter` 与 `forEach`
  • C++ QT 6.6.1 QCustomPlot的导入及使用注意事项和示例 | 关于高版本QT使用QCustomPlot报错问题解决的办法
  • vue+element ui 实现选择季度组件
  • Linux(CentOS)安装 Nginx
  • java23种设计模式-命令模式
  • 安全性质量属性场景
  • 策略模式结合SpringBoot
  • 银行信贷业务解析:运营与数据双视角下的业务本质与技术支撑
  • C#连接sql server
  • 什么是SEO通俗准确的解释
  • angular轮播图
  • 交换机与路由器连接方式
  • 2.25力扣每日一题--设计内存分配器
  • 排序算法适合的场景
  • TCP,http,WebSocket
  • android aosp系统定制如何监控系统性能
  • 改进的Siddon算法与原算法的区别及具体改进
  • SSL/TLS 协议、SSL证书 和 SSH协议 的区别和联系
  • 中级软考笔记-基础知识-8-网络与信息安全
  • 电商网站设计公司立找亿企邦/2345浏览器网址导航
  • seo自己做网站吗/电子商务培训
  • 广东省深圳市公司/网站优化方案范文
  • 1688做网站多少钱/学编程的正规学校
  • 网站开发公司可行报告/旅游网站网页设计
  • 简单房地产网站/优化大师官方免费下载