YOLO(You Only Look Once)作为单阶段目标检测的开山之作,凭借速度快、端到端、工程友好的优势,成为实时检测领域的标配算法。本文从v1→v2→v3梳理核心演进逻辑,并手把手带你用YOLOv3完成自定义数据集训练,理论+实战一步到位。

一、YOLO核心定位:单阶段检测的王者

1.目标检测分为两大流派:

• Two-stage:Faster R-CNN/Mask R-CNN → 精度高、速度慢(≈5FPS)

• One-stage:YOLO/SSD → 速度极快、适合实时检测,精度持续追赶

YOLO的本质:把检测变成回归问题,一次前向推理直接输出目标位置+类别+置信度,无需候选框生成,这是它快的根本原因。

2. IOU(交并比)

IOU(Intersection over Union)用来衡量预测框与真实框的重叠程度,是目标检测最基础的评价标准。

公式:IOU = 交集面积 / 并集面积

  • 交集:两个框重叠区域
  • 并集:两个框覆盖的所有区域
  • 取值范围:0 ~ 1
  • 常用阈值:0.5(mAP50)、0.5~0.95(mAP50-95)
  • IOU = 1:完全重合
  • IOU ≥ 0.5:通常认为检测正确
  • IOU = 0:无重叠,完全错误

3. mAP(平均精度均值)

mAP(Mean Average Precision)是综合衡量检测模型好坏的核心指标,比单纯看精确率 / 召回率更全面。

先看懂 4 个基础值(混淆矩阵)
  • TP(真正例):预测正确,框住目标
  • FP(假正例):预测错误,把背景当目标
  • FN(假反例):漏检,有目标没框出来
两个基础率
  • 精确率 Precision = TP / (TP+FP)预测结果里,有多少是对的
  • 召回率 Recall = TP / (TP+FN)真实目标里,有多少被找出来
常用 mAP 定义
  • mAP50:IoU=0.5 时的 mAP(宽松、常用)
  • mAP50-95:IoU 从 0.5 到 0.95、步长 0.05 的平均 mAP(更严格、更全面)

二、YOLOv1:开创单阶段检测时代

1. 核心思想

• 图像划分为 S×S网格(默认7×7),目标中心点落在哪个网格,该网格就负责预测此目标。

• 每个网格预测 2个边界框 + 置信度 + 20类概率,最终输出 7×7×30 张量。

• 置信度 = 有无目标 × 预测框与真实框IoU。

2. 网络结构

• 基于GoogLeNet改进:24个卷积层 + 2个全连接层

• 输入尺寸:448×448

• 无先验框,纯回归定位。

3. 损失函数(三大组成)

总损失 = 坐标损失(x,y,w,h) + 置信度损失(有无目标) + 分类损失(类别概率)

• 对有目标框的坐标损失加大权重

• 对无目标框的置信度损失降低权重,缓解样本不均衡。

4. YOLOv1优缺点

优点:速度快(45FPS)、端到端、简单易用
缺点:小目标差、重叠目标漏检、每个网格只能预测1类、框精度一般

 

三、YOLOv2(YOLO9000):精度与速度双升级

v2针对性解决v1缺陷,是工程化里程碑版本。

1. 关键改进

1. Batch Norm:卷积层后全加BN,放弃Dropout,mAP↑2%。

2. 高分辨率分类器:训练用448×448,适配检测分辨率,mAP↑4%。

3. Anchor先验框:用K-Means聚类训练集框得到5种先验框,回归偏移量,稳定收敛。

4. Darknet-19:轻量主干,1×1降维+3×3提取特征,参数更少、速度更快。

5. Passthrough细粒度特征:融合26×26浅层特征,改善小目标检测。

6. 多尺度训练:输入320~608(32倍数),提升鲁棒性。

2. 核心效果

• VOC mAP:63.4(v1) → 78.6(v2)

• 速度保持实时,精度逼近Two-stage算法。

四、YOLOv3:工业界最实用版本(本文实战主角)

v3是平衡速度/精度/工程化的巅峰,至今仍广泛落地。

1. 革命性改进

1. Darknet-53主干:全卷积+残差连接,更深但更快,特征更强。

2. FPN多尺度预测:3个输出层,分别检测大/中/小目标

◦ 13×13:大目标(感受野最大)

◦ 26×26:中目标

◦ 52×52:小目标(感受野最小)

3. 9种先验框:3尺度×3种,聚类自COCO数据集,适配更多形状目标。

4. Logistic替代Softmax:支持多标签分类(如“人+行人”),每个类别独立概率。

5. 多标签损失:用BCEWithLogitsLoss,适配多类别共存场景。

2. 为什么选YOLOv3做实战?

• 速度:416尺寸≈50FPS,满足实时

• 精度:COCO mAP50≈55.3%,足够工业落地

• 代码成熟、部署简单、适配自定义数据集零门槛
 

五、YOLOv3自定义数据集训练实战(PyTorch版)

本文基于PyTorch-YOLOv3复现,从环境→标注→配置→训练→测试全流程。

1. 环境准备
# 基础依赖
pip install torch torchvision pillow pyqt5 labelme
# 下载项目
git clone https://github.com/eriklindernoren/PyTorch-YOLOv3.git
cd PyTorch-YOLOv3
2. 数据集构建(核心步骤)

(1)数据标注(Labelme)

pip install labelme
labelme  # 启动GUI

• 标注:矩形框标记目标,保存json

• 转换:json → YOLO格式(class_id x y w h,均归一化)

(2)配置文件修改

1)classes.names

person
animal

2)custom.data

classes=2
train=data/custom/train.txt
valid=data/custom/val.txt
names=data/custom/classes.names

3)生成自定义cfg

bash create_custom_model.sh 2  # 2为类别数
3. 启动训练
from models import *
# from utils.logger import *
from utils.utils import *
from utils.datasets import *
from utils.parse_config import *
from test import evaluate

import warnings
warnings.filterwarnings("ignore")

from terminaltables import AsciiTable

import os
import time
import datetime
import argparse

import torch
from torch.utils.data import DataLoader
from torch.autograd import Variable

"""配置参数:
--model_def
config/yolov3-custom.cfg
--data_config
config/custom1.data
--pretrained_weights
weights/darknet53.conv.74
"""
if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--epochs", type=int, default=10, help="number of epochs") #训练次数
    parser.add_argument("--batch_size", type=int, default=1, help="size of each image batch")   #batch的大小
    parser.add_argument("--gradient_accumulations", type=int, default=1, help="number of gradient accums before step")#在每一步(更新模型参数)之前累积梯度的次数”
    parser.add_argument("--model_def", type=str, default="config/yolov3.cfg", help="path to model definition file") #模型的配置文件
    parser.add_argument("--data_config", type=str, default="config/coco.data", help="path to data config file") #数据的配置文件
    parser.add_argument("--pretrained_weights", type=str, help="if specified starts from checkpoint model") #预训练文件
    parser.add_argument("--n_cpu", type=int, default=0, help="number of cpu threads to use during batch generation")#数据加载过程中应使用的CPU线程数。
    parser.add_argument("--img_size", type=int, default=416, help="size of each image dimension")
    parser.add_argument("--checkpoint_interval", type=int, default=1, help="interval between saving model weights")#隔多少个epoch保存一次模型权重
    parser.add_argument("--evaluation_interval", type=int, default=1, help="interval evaluations on validation set")#多少个epoch进行一次验证集的验证
    parser.add_argument("--compute_map", default=False, help="if True computes mAP every tenth batch")
    parser.add_argument("--multiscale_training", default=True, help="allow for multi-scale training")#允许多尺寸特征图融合的训练
    opt = parser.parse_args()
    print(opt)

    # logger = Logger("logs")#日志文件

    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    os.makedirs("output", exist_ok=True)
    os.makedirs("checkpoints", exist_ok=True)

    # Get data configuration
    data_config = parse_data_config(opt.data_config)
    train_path = data_config["train"]
    valid_path = data_config["valid"]
    class_names = load_classes(data_config["names"])

    # Initiate model
    model = Darknet(opt.model_def).to(device)
    model.apply(weights_init_normal)#model.apply(fn)表示将fn函数应用到神经网络的各个模块上,包括该神经网络本身。这通常在初始化神经网络的参数时使用,本处用于初始化神经网络的权值

    # If specified we start from checkpoint
    if opt.pretrained_weights:
        if opt.pretrained_weights.endswith(".pth"): #用于检查字符串是否以指定的后缀结束。如果字符串以指定的后缀结束,则返回True,否则返回False。
            model.load_state_dict(torch.load(opt.pretrained_weights))
        else:
            model.load_darknet_weights(opt.pretrained_weights)

    # Get dataloader
    dataset = ListDataset(train_path, augment=True, multiscale=opt.multiscale_training)
    dataloader = torch.utils.data.DataLoader(
        dataset,
        batch_size=opt.batch_size,  #1个样本打包成一个batch进行加载
        shuffle=True,               #对数据进行随机打乱,
        num_workers=opt.n_cpu,      #用于指定子进程的数量,用于并行地加载数据。默认情况下,num_workers的值为0,表示没有使用子进程,所有数据都会在主进程中加载。当设置num_workers大于0时,DataLoader会创建指定数量的子进程,每个子进程都会负责加载一部分数据,然后主进程负责从这些子进程中获取数据。
                                    # 使用子进程可以加快数据的加载速度,因为每个子进程可以并行地加载一部分数据,从而充分利用多核CPU的计算能力。但是需要注意的是,使用子进程可能会导致数据的顺序被打乱,因此如果需要保持数据的原始顺序,应该将shuffle参数设置为False。
                                    # num_workers的值应该根据具体情况进行调整。如果数据集较大,可以考虑增加num_workers的值以充分利用计算机的资源。但是需要注意的是,如果num_workers的值过大,可能会导致内存消耗过大或者CPU负载过重,从而影响程序的性能。因此,需要根据实际情况进行调整。
        pin_memory=True,            #指定是否将加载进内存的数据的指针固定(pin),这个参数在某些情况下可以提高数据加载的速度。
                                    # 当设置pin_memory=True时,DataLoader会将加载进内存的数据的指针固定,即不进行移动操作。这样做的目的是为了提高数据传输的效率。因为当数据从磁盘或者网络等地方传输到内存中时,如果指针不固定,可能会导致数据在传输过程中被移动,从而需要重新读取,浪费了时间。而固定指针可以避免这种情况的发生,从而提高了数据传输的效率。
                                    # 需要注意的是,pin_memory参数的效果与操作系统和硬件的性能有关。在一些高性能的计算机上,固定指针可能并不会带来太大的性能提升。但是在一些内存带宽较小的计算机上,固定指针可能会显著提高数据加载的效率。因此,需要根据实际情况进行调整。
        collate_fn=dataset.collate_fn,
                                    # collate_fn是一个函数,用于对每个batch的数据进行合并。这个函数的输入是一个batch的数据,输出是一个合并后的数据。
                                    # collate_fn函数的主要作用是对每个batch的数据进行预处理,例如将不同数据类型的张量合并成一个张量,或者对序列数据进行padding操作等。这样可以使得每个batch的数据格式一致,便于模型进行训练。
                                    # 在默认情况下,collate_fn函数会将每个batch的数据按照第一个元素的张量形状进行合并。例如,如果一个batch的数据中第一个元素的张量形状是[
                                    # 3, 224, 224],那么collate_fn函数会将该batch的所有数据都调整为这个形状。
    )

    optimizer = torch.optim.Adam(model.parameters())

    metrics = [
        "grid_size",
        "loss",
        "x",
        "y",
        "w",
        "h",
        "conf",
        "cls",
        "cls_acc",
        "recall50",
        "recall75",
        "precision",
        "conf_obj",
        "conf_noobj",
    ]

    for epoch in range(opt.epochs):
        model.train()
        start_time = time.time()
        for batch_i, (_, imgs, targets) in enumerate(dataloader):
            batches_done = len(dataloader) * epoch + batch_i

            imgs = Variable(imgs.to(device))    #Variable类是PyTorch中的一个包装器,它将张量和它们的梯度信息封装在一起。当我们对一个张量进行操作时,PyTorch会自动地创建一个对应的Variable对象,其中包含了原始张量、梯度等信息。通过使用Variable,我们可以方便地进行自动微分和优化。
            targets = Variable(targets.to(device), requires_grad=False)
            print ('imgs',imgs.shape)
            print ('targets',targets.shape)
            loss, outputs = model(imgs, targets)
            loss.backward()

            if batches_done % opt.gradient_accumulations:
                # Accumulates gradient before each step
                optimizer.step()
                optimizer.zero_grad()

            # ----------------
            #   Log progress
            # ----------------

            log_str = "\n---- [Epoch %d/%d, Batch %d/%d] ----\n" % (epoch, opt.epochs, batch_i, len(dataloader))

            metric_table = [["Metrics", *[f"YOLO Layer {i}" for i in range(len(model.yolo_layers))]]]

            # Log metrics at each YOLO layer
            for i, metric in enumerate(metrics):
                formats = {m: "%.6f" for m in metrics}
                formats["grid_size"] = "%2d"
                formats["cls_acc"] = "%.2f%%"
                row_metrics = [formats[metric] % yolo.metrics.get(metric, 0) for yolo in model.yolo_layers]
                metric_table += [[metric, *row_metrics]]

                # Tensorboard logging
                tensorboard_log = []
                for j, yolo in enumerate(model.yolo_layers):
                    for name, metric in yolo.metrics.items():
                        if name != "grid_size":
                            tensorboard_log += [(f"{name}_{j+1}", metric)]
                tensorboard_log += [("loss", loss.item())]
                # logger.list_of_scalars_summary(tensorboard_log, batches_done)

            log_str += AsciiTable(metric_table).table
            log_str += f"\nTotal loss {loss.item()}"

            # Determine approximate time left for epoch
            epoch_batches_left = len(dataloader) - (batch_i + 1)
            time_left = datetime.timedelta(seconds=epoch_batches_left * (time.time() - start_time) / (batch_i + 1))
            log_str += f"\n---- ETA {time_left}"

            print(log_str)

            model.seen += imgs.size(0)

        if epoch % opt.evaluation_interval == 0:
            print("\n---- Evaluating Model ----")
            # Evaluate the model on the validation set
            precision, recall, AP, f1, ap_class = evaluate(
                model,
                path=valid_path,
                iou_thres=0.5,
                conf_thres=0.5,
                nms_thres=0.5,
                img_size=opt.img_size,
                batch_size=8,
            )
            evaluation_metrics = [
                ("val_precision", precision.mean()),
                ("val_recall", recall.mean()),
                ("val_mAP", AP.mean()),
                ("val_f1", f1.mean()),
            ]
            # logger.list_of_scalars_summary(evaluation_metrics, epoch)

            # Print class APs and mAP
            ap_table = [["Index", "Class name", "AP"]]
            for i, c in enumerate(ap_class):
                ap_table += [[c, class_names[c], "%.5f" % AP[i]]]
            print(AsciiTable(ap_table).table)
            print(f"---- mAP {AP.mean()}")

        if epoch % opt.checkpoint_interval == 0:
            torch.save(model.state_dict(), f"checkpoints/yolov3_ckpt_%d.pth" % epoch)
4. 训练参数说明(train.py核心)

• epochs:训练轮数,建议100+

• batch_size:按GPU显存调整(16G→8/16)

• img_size:建议416(32倍数,多尺度友好)

• pretrained_weights:用Darknet53预训练权重,加速收敛

六、YOLOv1→v3演进总结表

版本 主干网络 先验框 输出尺度 核心亮点 mAP
YOLOv1 GoogLeNet 7×7 单阶段开创
YOLOv2 Darknet-19 5 种 13×13 BN、Anchor、多尺度
YOLOv3 Darknet-53 9 种 13/26/52 残差、FPN、多标签

七、总结

YOLO从v1到v3,完成了从概念验证→工程实用的蜕变:

• v1:定义单阶段检测范式

• v2:补齐精度短板,落地可用

• v3:多尺度+残差+多标签,成为工业标配

本文配套YOLOv3完整训练流程,可直接用于口罩检测、行人计数、车辆检测等真实场景,改类别数和数据集即可快速复用。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐