欢迎来到《计算机视觉实战》系列教程的第十章。在第九章我们学习了Faster R-CNN等两阶段检测器,它们精度高但速度慢。本章我们将学习单阶段检测器(One-stage Detector),特别是YOLO和SSD,它们在保持可观精度的同时实现了实时检测。


1. 环境声明

  • Python版本Python 3.12+
  • PyTorch版本PyTorch 2.2+
  • torchvision版本0.17+
  • NumPy版本1.26+

2. 单阶段 vs 两阶段检测器

2.1 核心思想对比

两阶段检测器(Faster R-CNN)

  1. 第一阶段:RPN生成候选区域提议
  2. 第二阶段:对每个提议进行分类和边界框精调

单阶段检测器(YOLO/SSD)

  • 在单次前向传播中直接预测类别和边界框
  • 无需区域提议网络和两步处理
def compare_detectors():
    """单阶段 vs 两阶段检测器对比"""

    print("性能对比 (COCO mAP @ 0.5:0.95):")
    print("=" * 60)
    print(f"{'模型':<20} {'mAP':<10} {'FPS':<10} {'特点':<30}")
    print("-" * 60)
    print(f"{'Faster R-CNN':<20} {'42.0':<10} {'5':<10} {'最高精度':<30}")
    print(f"{'RetinaNet':<20} {'40.8':<10} {'12':<10} {'Focal Loss':<30}")
    print(f"{'SSD512':<20} {'38.5':<10} {'22':<10} {'多尺度':<30}")
    print(f"{'YOLOv5m':<20} {'45.0':<10} {'120':<10} {'平衡':<30}")
    print(f"{'YOLOv8n':<20} {'37.4':<10} {'300':<10} {'超实时':<30}")
    print("=" * 60)

    print("\n关键洞察:")
    print("1. 两阶段检测器精度高,但速度慢")
    print("2. 单阶段检测器速度快,适合实时应用")
    print("3. YOLOv5/v8在速度和精度上取得了很好的平衡")
    print("4. 现代YOLO版本精度已接近Faster R-CNN")

compare_detectors()

3. YOLO系列详解

3.1 YOLOv1:开创性的单阶段检测

import torch
import torch.nn as nn

class YOLOv1(nn.Module):
    """YOLOv1简化版实现"""

    def __init__(self, num_classes=20, num_grids=7, num_bboxes=2):
        super().__init__()
        self.num_classes = num_classes
        self.num_grids = num_grids
        self.num_bboxes = num_bboxes

        # 主干网络 (类似GoogLeNet)
        self.backbone = nn.Sequential(
            nn.Conv2d(3, 64, 7, stride=2, padding=3), nn.LeakyReLU(0.1),
            nn.MaxPool2d(2, 2),  # 1/2

            nn.Conv2d(64, 192, 3, padding=1), nn.LeakyReLU(0.1),
            nn.MaxPool2d(2, 2),  # 1/4

            nn.Conv2d(192, 128, 1), nn.LeakyReLU(0.1),
            nn.Conv2d(128, 256, 3, padding=1), nn.LeakyReLU(0.1),
            nn.MaxPool2d(2, 2),  # 1/8

            nn.Conv2d(256, 256, 1), nn.LeakyReLU(0.1),
            nn.Conv2d(256, 512, 3, padding=1), nn.LeakyReLU(0.1),
            nn.MaxPool2d(2, 2),  # 1/16

            nn.Conv2d(512, 256, 1), nn.LeakyReLU(0.1),
            nn.Conv2d(256, 512, 3, padding=1), nn.LeakyReLU(0.1),
            nn.Conv2d(512, 512, 1), nn.LeakyReLU(0.1),
            nn.Conv2d(512, 1024, 3, padding=1), nn.LeakyReLU(0.1),
            nn.MaxPool2d(2, 2),  # 1/32

            nn.Conv2d(1024, 512, 1), nn.LeakyReLU(0.1),
            nn.Conv2d(512, 1024, 3, padding=1), nn.LeakyReLU(0.1),
            nn.Conv2d(1024, 1024, 3, stride=2, padding=1), nn.LeakyReLU(0.1),  # 1/64

            nn.Conv2d(1024, 1024, 3, padding=1), nn.LeakyReLU(0.1),
            nn.Conv2d(1024, 1024, 3, padding=1), nn.LeakyReLU(0.1),
        )

        # 检测头
        # 输出: S x S x (B*5 + C)
        # 每个格子预测B个边界框,每个框有5个值(cx, cy, w, h, conf)
        # 加上C个类别概率
        self.detector = nn.Sequential(
            nn.Flatten(),
            nn.Linear(1024 * 7 * 7, 4096),
            nn.LeakyReLU(0.1),
            nn.Linear(4096, num_grids * num_grids * (num_bboxes * 5 + num_classes)),
        )

    def forward(self, x):
        features = self.backbone(x)
        output = self.detector(features)
        # reshape为 (batch, S, S, B*5+C)
        return output.view(-1, self.num_grids, self.num_grids,
                          self.num_bboxes * 5 + self.num_classes)

print("YOLOv1的核心创新:")
print("=" * 50)
print("1. 将图像划分为S×S网格")
print("2. 每个格子预测B个边界框和置信度")
print("3. 每个框预测4个坐标值(cx,cy,w,h)和置信度")
print("4. 加上C个类别概率")
print("5. 单次前向传播完成检测,速度极快")
print("\n但YOLOv1存在:")
print("- 对小物体检测效果差")
print("- 召回率较低")
print("- 定位精度不如两阶段方法")

3.2 YOLOv3:多尺度预测

import torch
import torch.nn as nn

class YOLOv3(nn.Module):
    """YOLOv3简化版 - 使用多尺度预测"""

    def __init__(self, num_classes=80):
        super().__init__()
        self.num_classes = num_classes

        # 主干网络 (Darknet-53风格)
        self.backbone = self._build_backbone()

        # 多尺度检测头
        # 大特征图 -> 小物体
        # 中特征图 -> 中等物体
        # 小特征图 -> 大物体

        self.detect_large = nn.Conv2d(256, 3 * (5 + num_classes), 1)
        self.detect_medium = nn.Conv2d(512, 3 * (5 + num_classes), 1)
        self.detect_small = nn.Conv2d(1024, 3 * (5 + num_classes), 1)

    def forward(self, x):
        """
        返回三个尺度的检测结果
        - 大: (batch, 3*(5+C), 13, 13)
        - 中: (batch, 3*(5+C), 26, 26)
        - 小: (batch, 3*(5+C), 52, 52)
        """
        # 简化的前向传播
        return None, None, None

print("YOLOv3的改进:")
print("=" * 50)
print("1. Darknet-53主干网络,更强的特征提取")
print("2. FPN多尺度特征金字塔")
print("3. 3个不同尺度的检测头")
print("4. 使用sigmoid代替softmax支持多标签分类")
print("5. 13x13, 26x26, 52x52三种特征图")
print("\n改进效果:")
print("- 小物体检测大幅提升")
print("- 中等和大物体保持高性能")
print("- 速度仍然很快 (YOLOv3-608: 20 FPS)")

3.3 YOLOv5/v8:现代YOLO

import torch

def yolov8_architecture():
    """YOLOv8架构特点"""

    print("YOLOv8核心设计:")
    print("=" * 50)
    print("1. CSPDarknet主干 + PANet颈部")
    print("2. Anchor-free检测头 (YOLOX开创)")
    print("3. Decoupled检测头 (分类和回归分开)")
    print("4. 新的损失函数: Bbox Loss + Distribution Focal Loss")
    print("5. 数据增强: Mosaic, MixUp, CopyPaste")

    print("\nYOLOv8 YAML配置 (COCO mAP):")
    configs = {
        'n': {'mAP50': 37.4, 'mAP50-95': 28.0, 'params': 3.2, 'FPS': 300},
        's': {'mAP50': 44.9, 'mAP50-95': 33.7, 'params': 11.2, 'FPS': 200},
        'm': {'mAP50': 51.2, 'mAP50-95': 39.2, 'params': 25.9, 'FPS': 120},
        'l': {'mAP50': 53.1, 'mAP50-95': 41.8, 'params': 43.7, 'FPS': 90},
        'x': {'mAP50': 54.8, 'mAP50-95': 43.4, 'params': 68.2, 'FPS': 60},
    }

    print(f"{'模型':<8} {'mAP@50':<12} {'mAP@50-95':<15} {'参数量(M)':<15} {'FPS':<10}")
    print("-" * 60)
    for name, cfg in configs.items():
        print(f"YOLOv8-{name:<4} {cfg['mAP50']:<12} {cfg['mAP50-95']:<15} {cfg['params']:<15} {cfg['FPS']:<10}")

yolov8_architecture()

4. SSD:多尺度单阶段检测

class SSD(nn.Module):
    """SSD (Single Shot MultiBox Detector)"""

    def __init__(self, num_classes=21):
        super().__init__()

        # VGG16主干网络
        self.vgg = nn.Sequential(
            nn.Conv2d(3, 64, 3, padding=1), nn.ReLU(),
            nn.Conv2d(64, 64, 3, padding=1), nn.ReLU(),
            nn.MaxPool2d(2, 2),

            nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(),
            nn.Conv2d(128, 128, 3, padding=1), nn.ReLU(),
            nn.MaxPool2d(2, 2),

            nn.Conv2d(128, 256, 3, padding=1), nn.ReLU(),
            nn.Conv2d(256, 256, 3, padding=1), nn.ReLU(),
            nn.Conv2d(256, 256, 3, padding=1), nn.ReLU(),
            nn.MaxPool2d(2, 2),  # 38x38

            nn.Conv2d(256, 512, 3, padding=1), nn.ReLU(),
            nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(),
            nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(),
            nn.MaxPool2d(2, 2),  # 19x19

            nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(),
            nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(),
            nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(),
            nn.MaxPool2d(2, 2),  # 10x10

            nn.Conv2d(512, 1024, 3, padding=1), nn.ReLU(),
            nn.Conv2d(1024, 1024, 1), nn.ReLU(),
            # 额外卷积层
            nn.Conv2d(1024, 256, 1), nn.ReLU(),
            nn.Conv2d(256, 512, 3, stride=2, padding=1), nn.ReLU(),  # 5x5
        )

        # 多尺度特征图检测
        # 38x38, 19x19, 10x10, 5x5, 3x3, 1x1
        self.detection_heads = nn.ModuleList([
            nn.Conv2d(512, 4 * (num_classes + 5), 3, padding=1),  # 38x38
            nn.Conv2d(1024, 6 * (num_classes + 5), 3, padding=1),  # 19x19
            nn.Conv2d(512, 6 * (num_classes + 5), 3, padding=1),  # 10x10
            nn.Conv2d(256, 6 * (num_classes + 5), 3, padding=1),  # 5x5
            nn.Conv2d(256, 4 * (num_classes + 5), 3, padding=1),  # 3x3
            nn.Conv2d(256, 4 * (num_classes + 5), 1),  # 1x1
        ])

print("SSD的设计特点:")
print("=" * 50)
print("1. VGG16主干网络,保留额外卷积层")
print("2. 6个不同尺度的特征图")
print("3. 每个位置使用多个默认框 (default boxes)")
print("4. 多尺度特征适合检测不同大小的物体")
print("5. 比YOLO稍慢但精度更好")

5. RetinaNet与Focal Loss

def focal_loss():
    """Focal Loss解决单阶段检测器正负样本不均衡问题"""

    print("样本不均衡问题:")
    print("=" * 50)
    print("- 单阶段检测器需要在所有位置预测")
    print("- 背景区域远多于物体区域 (如1000:1)")
    print("- 大量易分类的背景样本主导梯度")
    print("- 难分类的物体样本被忽视")

    print("\nFocal Loss公式:")
    print("FL(pt) = -αt(1-pt)^γ * log(pt)")
    print("- γ (gamma): 聚焦参数,越大越关注难样本")
    print("- α (alpha): 平衡正负样本的权重")
    print("- pt: 分类概率")

    print("\n实验结果:")
    print("- γ=0: 标准交叉熵")
    print("- γ=2, α=0.25: Focal Loss")
    print("- 相比标准交叉熵,AP提升约3个点")

focal_loss()

6. 实战:使用YOLOv8

import torch

def use_yolov8():
    """使用YOLOv8进行目标检测"""

    # 实际使用ultralytics库
    # from ultralytics import YOLO

    print("YOLOv8使用示例:")
    print("=" * 50)
    print("""
    from ultralytics import YOLO

    # 加载预训练模型
    model = YOLO('yolov8n.pt')  # nano版本
    model = YOLO('yolov8s.pt')  # small版本
    model = YOLO('yolov8m.pt')  # medium版本

    # 推理
    results = model('image.jpg')

    # 绘制结果
    results[0].show()

    # 获取检测框
    boxes = results[0].boxes
    for box in boxes:
        x1, y1, x2, y2 = box.xyxy[0]
        conf = box.conf[0]
        cls = box.cls[0]
        print(f"类别: {int(cls)}, 置信度: {conf:.2f}, 位置: ({x1:.0f}, {y1:.0f}, {x2:.0f}, {y2:.0f})")
    """)

use_yolov8()

7. 本章小结

通过本章学习,我们掌握了:

  1. 单阶段 vs 两阶段:理解了两类检测器的核心区别
  2. YOLO演进:从YOLOv1到YOLOv8的发展历程
  3. 多尺度预测:FPN在单阶段检测中的应用
  4. Focal Loss:解决样本不均衡的创新方法
  5. SSD设计:多尺度特征图检测策略

一句话总结:YOLO系列通过不断的工程优化和算法改进,在实时检测领域建立了标杆地位,YOLOv8更是将精度和速度做到了极致平衡。


下一章预告:第11章《图像分割:语义分割与实例分割》将带你学习U-Net、Mask R-CNN等分割网络。


如果本章内容对你有帮助,欢迎点赞、收藏和关注。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐