YOLO v2/v3:多尺度检测的进化之路

系列文章 目标检测第 3 篇 / 共 8 篇

从 R-CNN 到 YOLO 全系列,深入目标检测的技术迭代与工业部署实践。

  1. R-CNN到Faster R-CNN:两阶段检测的演进之路
  2. YOLO v1:实时目标检测的革命
  3. YOLO v2/v3:多尺度检测的进化之路
  4. YOLO v4:CSPNet与数据增强的艺术
  5. YOLO v5:工业化的成功
  6. YOLO v8:Ultralytics的现代架构
  7. YOLO变种:RT-DETR、YOLO-NAS等
  8. YOLO实战:从训练到部署

引言

YOLO v1虽然实现了实时检测,但精度相对较低。YOLO v2和YOLO v3的发布标志着YOLO系列的第一次重大进化,通过引入锚框机制、多尺度检测和更好的网络架构,在保持实时性的同时大幅提升了检测精度[1][2]

YOLO v2/v3的核心改进

  • 🎯 锚框机制:引入锚框,提升检测精度
  • 📏 多尺度检测:不同尺度特征图检测不同大小目标
  • 🏗️ 更好网络:Darknet-19/53,更强的特征提取能力
  • 🚀 实时性能:保持高速度的同时提升精度

本系列学习路径

R-CNN系列 → YOLO v1 → YOLO v2/v3(本文) → YOLO v4 → YOLO v5 → YOLO v8

YOLO v2:YOLO9000的突破

📄 论文信息
作者 Joseph Redmon, Ali Farhadi (University of Washington)
发表 CVPR (2017)
arXiv 1612.08242

核心改进

锚框机制(Anchor Boxes)

YOLO v2引入锚框机制[1]

def generate_anchors(base_size=32, ratios=[1, 2, 0.5], scales=[1, 2, 4]):
    """
    生成YOLO v2锚框
    
    Args:
        base_size: 基础尺寸
        ratios: 宽高比列表
        scales: 尺度列表
    
    Returns:
        anchors: 锚框列表 (num_anchors, 4)
    """
    anchors = []
    
    for scale in scales:
        for ratio in ratios:
            # 计算锚框尺寸
            w = base_size * scale * np.sqrt(ratio)
            h = base_size * scale / np.sqrt(ratio)
            
            # 锚框坐标(以(0,0)为中心)
            x1 = -w / 2
            y1 = -h / 2
            x2 = w / 2
            y2 = h / 2
            
            anchors.append([x1, y1, x2, y2])
    
    return np.array(anchors)

# 使用示例
anchors = generate_anchors()
print(f"生成了 {len(anchors)} 个锚框")
print(f"锚框形状: {anchors.shape}")  # (9, 4)

边界框预测改进

YOLO v2边界框预测

class YOLOv2BBox:
    def __init__(self, tx, ty, tw, th, confidence):
        """
        YOLO v2边界框表示
        
        Args:
            tx, ty: 边界框中心相对于网格单元的偏移
            tw, th: 边界框宽高相对于锚框的缩放
            confidence: 置信度分数
        """
        self.tx = tx  # 中心x偏移
        self.ty = ty  # 中心y偏移
        self.tw = tw  # 宽度缩放
        self.th = th  # 高度缩放
        self.confidence = confidence
    
    def decode(self, grid_cell, anchor, img_w, img_h):
        """
        解码边界框预测
        
        Args:
            grid_cell: 网格单元信息
            anchor: 锚框信息
            img_w, img_h: 图像宽高
        
        Returns:
            bbox: 解码后的边界框 (x1, y1, x2, y2)
        """
        # 计算网格单元中心
        grid_x = grid_cell['cell_id'][1]
        grid_y = grid_cell['cell_id'][0]
        
        # 计算边界框中心
        bx = self.tx + grid_x
        by = self.ty + grid_y
        
        # 计算边界框尺寸
        bw = anchor['w'] * np.exp(self.tw)
        bh = anchor['h'] * np.exp(self.th)
        
        # 转换为绝对坐标
        abs_x = bx * (img_w / 13)  # 13×13网格
        abs_y = by * (img_h / 13)
        abs_w = bw
        abs_h = bh
        
        # 转换为左上角和右下角坐标
        x1 = abs_x - abs_w / 2
        y1 = abs_y - abs_h / 2
        x2 = abs_x + abs_w / 2
        y2 = abs_y + abs_h / 2
        
        return (x1, y1, x2, y2)

Darknet-19网络架构

YOLO v2使用Darknet-19作为特征提取网络[1]

class Darknet19(nn.Module):
    def __init__(self, num_classes=1000):
        super(Darknet19, self).__init__()
        
        # 特征提取网络
        self.features = nn.Sequential(
            # 第一个卷积块
            nn.Conv2d(3, 32, 3, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2, stride=2),
            
            # 第二个卷积块
            nn.Conv2d(32, 64, 3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2, stride=2),
            
            # 第三个卷积块
            nn.Conv2d(64, 128, 3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 64, 1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 128, 3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2, stride=2),
            
            # 第四个卷积块
            nn.Conv2d(128, 256, 3, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 128, 1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 256, 3, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2, stride=2),
            
            # 第五个卷积块
            nn.Conv2d(256, 512, 3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 256, 1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 512, 3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 256, 1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 512, 3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2, stride=2),
            
            # 第六个卷积块
            nn.Conv2d(512, 1024, 3, padding=1),
            nn.BatchNorm2d(1024),
            nn.ReLU(inplace=True),
            nn.Conv2d(1024, 512, 1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 1024, 3, padding=1),
            nn.BatchNorm2d(1024),
            nn.ReLU(inplace=True),
            nn.Conv2d(1024, 512, 1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 1024, 3, padding=1),
            nn.BatchNorm2d(1024),
            nn.ReLU(inplace=True),
        )
        
        # 分类器
        self.classifier = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Flatten(),
            nn.Linear(1024, num_classes)
        )
    
    def forward(self, x):
        x = self.features(x)
        x = self.classifier(x)
        return x

多尺度训练

YOLO v2多尺度训练策略[1]

def multi_scale_training(model, dataloader, num_epochs=100):
    """
    YOLO v2多尺度训练
    
    训练策略:
    - 每10个batch随机选择新的输入尺寸
    - 尺寸范围:320×320到608×608
    - 步长:32像素
    """
    
    scales = [320, 352, 384, 416, 448, 480, 512, 544, 576, 608]
    
    for epoch in range(num_epochs):
        for batch_idx, (images, targets) in enumerate(dataloader):
            # 每10个batch改变输入尺寸
            if batch_idx % 10 == 0:
                scale = random.choice(scales)
                # 调整图像尺寸
                images = resize_images(images, scale)
                targets = adjust_targets(targets, scale)
            
            # 前向传播
            outputs = model(images)
            
            # 计算损失
            loss = compute_yolo_loss(outputs, targets)
            
            # 反向传播
            loss.backward()
            optimizer.step()

def resize_images(images, target_size):
    """调整图像尺寸"""
    resized_images = []
    for image in images:
        resized = F.interpolate(
            image.unsqueeze(0), 
            size=(target_size, target_size),
            mode='bilinear',
            align_corners=False
        )
        resized_images.append(resized.squeeze(0))
    
    return torch.stack(resized_images)

YOLO v3:多尺度检测的巅峰

📄 论文信息
作者 Joseph Redmon, Ali Farhadi (University of Washington)
发表 arXiv (2018)
arXiv 1804.02767

核心改进

多尺度检测

YOLO v3使用三个不同尺度的特征图[2]

class YOLOv3(nn.Module):
    def __init__(self, num_classes=80, num_anchors=3):
        super(YOLOv3, self).__init__()
        
        self.num_classes = num_classes
        self.num_anchors = num_anchors
        
        # 特征提取网络(Darknet-53)
        self.backbone = Darknet53()
        
        # 多尺度检测头
        self.detection_head_1 = DetectionHead(1024, num_classes, num_anchors)  # 13×13
        self.detection_head_2 = DetectionHead(512, num_classes, num_anchors)   # 26×26
        self.detection_head_3 = DetectionHead(256, num_classes, num_anchors)   # 52×52
        
        # 特征融合网络
        self.fpn = FeaturePyramidNetwork()
    
    def forward(self, x):
        # 特征提取
        features = self.backbone(x)
        
        # 特征融合
        fpn_features = self.fpn(features)
        
        # 多尺度检测
        detections_1 = self.detection_head_1(fpn_features[0])  # 13×13
        detections_2 = self.detection_head_2(fpn_features[1])  # 26×26
        detections_3 = self.detection_head_3(fpn_features[2])  # 52×52
        
        return [detections_1, detections_2, detections_3]

class DetectionHead(nn.Module):
    def __init__(self, in_channels, num_classes, num_anchors):
        super(DetectionHead, self).__init__()
        
        self.num_classes = num_classes
        self.num_anchors = num_anchors
        
        # 检测头网络
        self.conv1 = nn.Conv2d(in_channels, in_channels*2, 3, padding=1)
        self.conv2 = nn.Conv2d(in_channels*2, (num_classes + 5) * num_anchors, 1)
        
    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = self.conv2(x)
        
        # 重塑输出
        batch_size = x.size(0)
        x = x.view(batch_size, self.num_anchors, self.num_classes + 5, 
                  x.size(2), x.size(3))
        x = x.permute(0, 1, 3, 4, 2).contiguous()
        
        return x

特征金字塔网络(FPN)

YOLO v3使用FPN进行特征融合[2]

class FeaturePyramidNetwork(nn.Module):
    def __init__(self):
        super(FeaturePyramidNetwork, self).__init__()
        
        # 特征融合网络
        self.lateral_conv1 = nn.Conv2d(1024, 512, 1)
        self.lateral_conv2 = nn.Conv2d(512, 256, 1)
        
        self.fpn_conv1 = nn.Conv2d(512, 512, 3, padding=1)
        self.fpn_conv2 = nn.Conv2d(256, 256, 3, padding=1)
        self.fpn_conv3 = nn.Conv2d(256, 256, 3, padding=1)
    
    def forward(self, features):
        """
        特征金字塔网络前向传播
        
        Args:
            features: 特征图列表 [C3, C4, C5]
        
        Returns:
            fpn_features: 融合后的特征图列表
        """
        C3, C4, C5 = features
        
        # 顶层特征
        P5 = self.lateral_conv1(C5)
        
        # 中层特征
        P4 = self.lateral_conv2(C4)
        P4 = P4 + F.interpolate(P5, size=P4.shape[2:], mode='nearest')
        P4 = self.fpn_conv2(P4)
        
        # 底层特征
        P3 = C3
        P3 = P3 + F.interpolate(P4, size=P3.shape[2:], mode='nearest')
        P3 = self.fpn_conv3(P3)
        
        return [P5, P4, P3]

Darknet-53网络架构

YOLO v3使用Darknet-53作为特征提取网络[2]

class Darknet53(nn.Module):
    def __init__(self):
        super(Darknet53, self).__init__()
        
        # 特征提取网络
        self.conv1 = self._make_conv_block(3, 32, 3, 1, 1)
        self.conv2 = self._make_conv_block(32, 64, 3, 2, 1)
        self.conv3 = self._make_conv_block(64, 128, 3, 2, 1)
        self.conv4 = self._make_conv_block(128, 256, 3, 2, 1)
        self.conv5 = self._make_conv_block(256, 512, 3, 2, 1)
        self.conv6 = self._make_conv_block(512, 1024, 3, 2, 1)
        
        # 残差块
        self.residual_blocks = nn.ModuleList([
            self._make_residual_block(256, 2),
            self._make_residual_block(512, 8),
            self._make_residual_block(1024, 8),
        ])
    
    def _make_conv_block(self, in_channels, out_channels, kernel_size, stride, padding):
        """创建卷积块"""
        return nn.Sequential(
            nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        )
    
    def _make_residual_block(self, channels, num_blocks):
        """创建残差块"""
        blocks = []
        for _ in range(num_blocks):
            blocks.append(ResidualBlock(channels))
        return nn.Sequential(*blocks)
    
    def forward(self, x):
        # 特征提取
        x = self.conv1(x)
        x = self.conv2(x)
        x = self.conv3(x)
        
        # 残差块
        x = self.residual_blocks[0](x)
        x = self.conv4(x)
        x = self.residual_blocks[1](x)
        x = self.conv5(x)
        x = self.residual_blocks[2](x)
        x = self.conv6(x)
        
        return x

class ResidualBlock(nn.Module):
    def __init__(self, channels):
        super(ResidualBlock, self).__init__()
        
        self.conv1 = nn.Conv2d(channels, channels//2, 1)
        self.conv2 = nn.Conv2d(channels//2, channels, 3, padding=1)
        self.bn1 = nn.BatchNorm2d(channels//2)
        self.bn2 = nn.BatchNorm2d(channels)
        self.relu = nn.ReLU(inplace=True)
    
    def forward(self, x):
        residual = x
        
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        
        x = self.conv2(x)
        x = self.bn2(x)
        
        x = x + residual
        x = self.relu(x)
        
        return x

多尺度锚框

YOLO v3使用不同尺度的锚框

def generate_multiscale_anchors():
    """
    生成YOLO v3多尺度锚框
    
    Returns:
        anchors: 多尺度锚框字典
    """
    anchors = {
        # 13×13特征图(大目标)
        'large': [
            (116, 90), (156, 198), (373, 326)
        ],
        
        # 26×26特征图(中目标)
        'medium': [
            (30, 61), (62, 45), (59, 119)
        ],
        
        # 52×52特征图(小目标)
        'small': [
            (10, 13), (16, 30), (33, 23)
        ]
    }
    
    return anchors

# 使用示例
anchors = generate_multiscale_anchors()
print("多尺度锚框:")
for scale, anchor_list in anchors.items():
    print(f"{scale}: {anchor_list}")

YOLOv3 FPN多尺度架构

graph LR
    I[Input] --> DK[Darknet-53<br/>Backbone]
    DK --> C3["Feature C3<br/>52×52 · 小目标"]
    DK --> C4["Feature C4<br/>26×26"]
    DK --> C5["Feature C5<br/>13×13 · 大目标"]
    C5 --> P5["Head 13×13<br/>大 Anchor"]
    C5 --> UP4[Upsample]
    C4 --> CAT4[Concat]
    UP4 --> CAT4
    CAT4 --> P4["Head 26×26<br/>中 Anchor"]
    CAT4 --> UP3[Upsample]
    C3 --> CAT3[Concat]
    UP3 --> CAT3
    CAT3 --> P3["Head 52×52<br/>小 Anchor"]

YOLO v2/v3性能对比 [1][2]

速度对比

方法 推理时间 FPS 加速比
YOLO v1 0.022秒 45
YOLO v2 0.020秒 50 1.1×
YOLO v3 0.025秒 40 0.9×

精度对比

方法 VOC 2007 mAP VOC 2012 mAP COCO mAP 说明
YOLO v1 63.4% 57.9% - 基准
YOLO v2 76.8% 73.4% 21.6% +13.4%
YOLO v3 78.6% 75.2% 33.0% +15.2%

小目标检测对比

方法 小目标mAP 中目标mAP 大目标mAP 说明
YOLO v1 45.2% 67.3% 78.1% 基准
YOLO v2 52.1% 71.8% 82.3% 多尺度训练
YOLO v3 58.7% 76.4% 85.2% 多尺度检测

YOLO v2/v3的优势与局限

✅ 主要优势

精度大幅提升 [1][2]

精度提升:
- YOLO v2: +13.4% mAP
- YOLO v3: +15.2% mAP
- 小目标检测: +13.5% mAP

多尺度检测 [2]

多尺度检测优势:
- 不同尺度特征图检测不同大小目标
- 小目标检测能力大幅提升
- 密集目标检测能力增强

锚框机制 [1]

锚框机制优势:
- 更好的边界框回归
- 提高检测精度
- 减少训练难度

❌ 主要局限

速度略有下降

速度问题:
- YOLO v3比YOLO v1慢10%
- 多尺度检测增加计算量
- 网络复杂度增加

小目标检测仍有局限

小目标检测问题:
- 52×52特征图分辨率仍有限
- 小目标检测精度相对较低
- 密集小目标检测困难

训练复杂度增加

训练复杂度:
- 多尺度训练策略
- 锚框匹配策略
- 损失函数设计复杂

YOLO v2/v3的历史意义

技术贡献

YOLO v2/v3的技术贡献[1][2]

  1. 锚框机制:引入锚框,提升检测精度
  2. 多尺度检测:不同尺度特征图检测不同大小目标
  3. 特征融合:FPN特征融合,提升小目标检测
  4. 网络架构:Darknet-19/53,更强的特征提取能力

技术影响

YOLO v2/v3的技术影响

后续发展:
YOLO v2/v3 → YOLO v4 → YOLO v5 → YOLO v8

技术演进:
- 锚框机制 → 更复杂的锚框策略
- 多尺度检测 → 更精细的多尺度设计
- 特征融合 → 更高级的特征融合方法
- 网络架构 → 更高效的网络设计

应用价值

YOLO v2/v3的应用价值

应用领域:
- 自动驾驶:多尺度目标检测
- 视频分析:实时多目标检测
- 工业检测:小目标检测
- 移动应用:平衡精度和速度

总结

YOLO v2/v3的核心贡献 [1][2]

  1. 锚框机制:引入锚框,提升检测精度
  2. 多尺度检测:不同尺度特征图检测不同大小目标
  3. 特征融合:FPN特征融合,提升小目标检测
  4. 网络架构:Darknet-19/53,更强的特征提取能力

技术特点总结

YOLO v2特点:
- 锚框机制:9个锚框
- 多尺度训练:320×320到608×608
- 网络架构:Darknet-19
- 精度提升:+13.4% mAP

YOLO v3特点:
- 多尺度检测:3个尺度特征图
- 特征融合:FPN特征融合
- 网络架构:Darknet-53
- 精度提升:+15.2% mAP

为后续发展奠定基础

YOLO v2/v3通过锚框机制和多尺度检测,在保持实时性的同时大幅提升了检测精度[1][2],为后续YOLO系列的发展奠定了重要基础。


参考资料

  1. Redmon, J. & Farhadi, A. (2017). YOLO9000: Better, Faster, Stronger. In CVPR 2017. arXiv:1612.08242.
  2. Redmon, J. & Farhadi, A. (2018). YOLOv3: An Incremental Improvement. arXiv:1804.02767.

代码实现

数据集

  • PASCAL VOC - 目标检测基准数据集
  • COCO - 大规模目标检测数据集

文章目录