YOLO v2/v3:多尺度检测的进化之路
引言
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的突破
核心改进
锚框机制(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:多尺度检测的巅峰
核心改进
多尺度检测
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 | 1× |
| 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]:
- 锚框机制:引入锚框,提升检测精度
- 多尺度检测:不同尺度特征图检测不同大小目标
- 特征融合:FPN特征融合,提升小目标检测
- 网络架构:Darknet-19/53,更强的特征提取能力
技术影响
YOLO v2/v3的技术影响:
后续发展:
YOLO v2/v3 → YOLO v4 → YOLO v5 → YOLO v8
技术演进:
- 锚框机制 → 更复杂的锚框策略
- 多尺度检测 → 更精细的多尺度设计
- 特征融合 → 更高级的特征融合方法
- 网络架构 → 更高效的网络设计
应用价值
YOLO v2/v3的应用价值:
应用领域:
- 自动驾驶:多尺度目标检测
- 视频分析:实时多目标检测
- 工业检测:小目标检测
- 移动应用:平衡精度和速度
总结
YOLO v2/v3的核心贡献 [1][2]
- 锚框机制:引入锚框,提升检测精度
- 多尺度检测:不同尺度特征图检测不同大小目标
- 特征融合:FPN特征融合,提升小目标检测
- 网络架构: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系列的发展奠定了重要基础。
参考资料
- Redmon, J. & Farhadi, A. (2017). YOLO9000: Better, Faster, Stronger. In CVPR 2017. arXiv:1612.08242.
- Redmon, J. & Farhadi, A. (2018). YOLOv3: An Incremental Improvement. arXiv:1804.02767.
代码实现
- YOLO v2/v3官方 - 原始C实现
- PyTorch实现 - 现代PyTorch实现
- TensorFlow实现 - TensorFlow实现
数据集
- PASCAL VOC - 目标检测基准数据集
- COCO - 大规模目标检测数据集