告别Anchor Box!用PyTorch从零复现FCOS目标检测模型(附完整代码)
在目标检测领域,Anchor Box曾长期占据主导地位,从Faster R-CNN到YOLO系列无不依赖这一设计。但2019年ICCV上提出的FCOS(Fully Convolutional One-Stage)模型彻底打破了这一范式,用更简洁的"逐像素预测"思路实现了SOTA性能。本文将带您从PyTorch实现角度,完整拆解这个革命性模型的核心设计。
1. 为什么需要Anchor-Free检测器?
传统Anchor-Based方法存在几个固有缺陷:
- 超参数敏感:Anchor的尺寸、宽高比需要针对不同数据集精心调整
- 计算冗余:典型设置下每张图像需处理超过10万个Anchor Box
- 样本失衡:正负样本比例常达到1:1000以上
FCOS的解决方案令人耳目一新:
- 完全移除Anchor Box设计
- 将特征图上的每个位置视为训练样本
- 通过FPN实现多尺度预测
- 引入Centerness解决低质量预测框问题
# 传统Anchor-Based vs FCOS预测方式对比 anchor_based = { 'prior_boxes': ['预设尺寸', '预设宽高比'], 'predictions': ['相对anchor的偏移量'] } fcos_style = { 'prior_boxes': ['特征图位置本身'], 'predictions': ['绝对坐标值'] }2. 模型架构深度解析
2.1 骨干网络与特征金字塔
FCOS采用标准的ResNet+FPN架构,但有几个关键细节:
class BackboneWithFPN(nn.Module): def __init__(self, backbone_name='resnet50'): super().__init__() # 骨干网络输出C3-C5特征 self.backbone = build_resnet(backbone_name) # FPN构建P3-P7金字塔 self.fpn = FPN( in_channels_list=[512, 1024, 2048], out_channels=256, extra_blocks=LastLevelMaxPool() )特征图映射关系:
| 特征层 | 步长 | 感受野 | 负责目标尺寸范围 |
|---|---|---|---|
| P3 | 8 | 56x56 | (0, 64] |
| P4 | 16 | 112x112 | (64, 128] |
| P5 | 32 | 224x224 | (128, 256] |
| P6 | 64 | 448x448 | (256, 512] |
| P7 | 128 | 896x896 | (512, ∞) |
注意:实际实现中会通过1x1卷积统一通道数,再通过3x3卷积消除上采样混叠效应
2.2 检测头设计奥秘
FCOS的检测头同时输出三类信息:
class FCOSHead(nn.Module): def __init__(self, num_classes=80): super().__init__() # 共享特征提取 self.shared_convs = nn.Sequential( nn.Conv2d(256, 256, 3, padding=1), nn.GroupNorm(32, 256), nn.ReLU() ) # 三个独立分支 self.cls_logits = nn.Conv2d(256, num_classes, 3, padding=1) self.bbox_pred = nn.Conv2d(256, 4, 3, padding=1) self.centerness = nn.Conv2d(256, 1, 3, padding=1)输出解析:
- 分类分支:C×H×W,C为类别数
- 回归分支:4×H×W,表示(l,t,r,b)四个边界距离
- 中心度分支:1×H×W,衡量预测点与目标中心的偏离程度
3. 正负样本分配策略
FCOS的样本分配是其最精妙的设计之一,分为三个关键步骤:
3.1 位置条件筛选
def get_positions(gt_boxes, feature_maps): # 计算每个特征点对应的原图坐标 grid_x = torch.arange(0, feature_map.size(3)) * stride grid_y = torch.arange(0, feature_map.size(2)) * stride # 检查是否落在GT框内 inside_gt = (grid_x > gt_left) & (grid_x < gt_right) & (grid_y > gt_top) & (grid_y < gt_bottom) # 进一步限制在中心区域 center_radius = 1.5 * stride in_center = (abs(grid_x - gt_cx) < center_radius) & (abs(grid_y - gt_cy) < center_radius) return inside_gt & in_center3.2 尺度分配规则
FCOS利用FPN实现"分而治之"策略:
| 特征层 | 分配规则 | 实际代码实现 |
|---|---|---|
| P3 | max(l,r,t,b) ∈ (0,64] | torch.where(max_reg < 64) |
| P4 | max(l,r,t,b) ∈ (64,128] | (max_reg >=64) & (max_reg<128) |
| ... | ... | ... |
3.3 Centerness计算
def compute_centerness(reg_targets): # reg_targets形状:[N, 4] (l,t,r,b) left_right = reg_targets[:, [0, 2]] top_bottom = reg_targets[:, [1, 3]] centerness = (left_right.min(dim=1)[0] / left_right.max(dim=1)[0]) * \ (top_bottom.min(dim=1)[0] / top_bottom.max(dim=1)[0]) return torch.sqrt(centerness)经验提示:Centerness的平方根变换能使训练更稳定
4. 损失函数与训练技巧
4.1 多任务损失组合
FCOS使用三种损失的加权和:
def forward(self, inputs, targets): # 计算各分支损失 cls_loss = self.focal_loss(cls_logits, gt_labels) reg_loss = self.giou_loss(bbox_pred, gt_boxes) cnt_loss = self.bce_loss(centerness, gt_centerness) # 加权求和 total_loss = cls_loss + reg_loss * 2.0 + cnt_loss * 0.25 return total_loss关键配置参数:
- 分类损失:Focal Loss (α=0.25, γ=2.0)
- 回归损失:GIoU Loss (权重2.0)
- 中心度损失:BCE Loss (权重0.25)
4.2 训练优化策略
学习率调度:
lr_scheduler = torch.optim.lr_scheduler.MultiStepLR( optimizer, milestones=[16, 22], gamma=0.1 )数据增强组合:
transform = A.Compose([ A.HorizontalFlip(p=0.5), A.RandomBrightnessContrast(p=0.2), A.ShiftScaleRotate( shift_limit=0.1, scale_limit=0.1, rotate_limit=5, p=0.5 ), A.Resize(800, 1333) # 保持长宽比 ], bbox_params=A.BboxParams(format='pascal_voc'))5. 推理部署实战
5.1 后处理流程
def postprocess(predictions, score_thresh=0.05): # 解码预测结果 boxes = decode_ltrb_to_xyxy(predictions['regression']) scores = predictions['classification'].sigmoid() centerness = predictions['centerness'].sigmoid() # 融合分类得分与中心度 final_scores = scores * centerness.unsqueeze(-1) # 过滤低分预测 keep = final_scores > score_thresh # NMS处理 return batched_nms(boxes[keep], final_scores[keep])5.2 性能优化技巧
模型量化:
quantized_model = torch.quantization.quantize_dynamic( model, {nn.Conv2d, nn.Linear}, dtype=torch.qint8 )ONNX导出:
torch.onnx.export( model, dummy_input, "fcos.onnx", opset_version=11, input_names=['images'], output_names=['boxes', 'scores', 'labels'] )6. 完整实现指南
以下是从零实现的关键步骤:
- 数据准备:
wget http://images.cocodataset.org/zips/train2017.zip unzip train2017.zip -d ./coco- 模型构建:
from torchvision.models.detection import fcos_resnet50_fpn model = fcos_resnet50_fpn( pretrained=False, num_classes=80, trainable_backbone_layers=3 )- 训练循环:
for epoch in range(24): for images, targets in train_loader: optimizer.zero_grad() loss_dict = model(images, targets) losses = sum(loss for loss in loss_dict.values()) losses.backward() optimizer.step() lr_scheduler.step()- 评估验证:
coco_evaluator = evaluate_on_coco( model, val_loader, device='cuda' ) print(coco_evaluator.get_results())在实现过程中,有几个常见陷阱需要注意:
- 正样本分配时忘记考虑FPN层级限制
- Centerness计算未进行梯度截断导致NaN
- 推理时忘记将分类得分与Centerness相乘
经过完整训练后,在COCO val2017上可以达到约37.2 AP的精度,与原始论文结果相当。这个实现相比官方版本更加简洁,适合作为理解Anchor-Free检测器的入门项目。