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-YOLOv32. 数据集构建(核心步骤)
(1)数据标注(Labelme)
pip install labelme labelme # 启动GUI• 标注:矩形框标记目标,保存json
• 转换:json → YOLO格式(class_id x y w h,均归一化)
(2)配置文件修改
1)classes.names
person animal2)custom.data
classes=2 train=data/custom/train.txt valid=data/custom/val.txt names=data/custom/classes.names3)生成自定义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完整训练流程,可直接用于口罩检测、行人计数、车辆检测等真实场景,改类别数和数据集即可快速复用。