首页 / 资讯中心 / 文章详情

华为DIGIX CV赛道第三名方案:目标检测实战与避坑指南

华为DIGIX CV赛道第三名方案:目标检测实战与避坑指南 ★ FEATURED ARTICLE
简介这份资源是2020华为DIGIX全球校园AI算法精英大赛计算机视觉赛道的第三名解决方案源码包面向计算机、数学、电子信息等专业的学生与算法竞赛爱好者适合作为竞赛项目学习资料帮助理解高分方案的模型设计、训练流程与工程组织方式。压缩包共508个文件约20.91MB以245个Python源码为核心辅以179个编译缓存文件、23个yml与9个yaml配置、20个xml、10个sh脚本及若干md、txt说明文档覆盖数据读取、模型定义、训练配置与运行脚本等环节目录结构清晰便于按模块检索与复现。目前已有269人学习下载。读者可从中获取完整赛题方案、可运行的代码框架、配置与脚本组织思路以及调试排错参考适合在理解代码的基础上自行调试与二次开发。1. 从华为 DIGIX 校园赛 CV 赛道说起一份第三名源码能教会你什么2020 年华为 DIGIX 全球校园 AI 算法精英大赛的计算机视觉赛道赛题方向是「基于手机拍摄图像的目标检测与分类」。当年这个比赛在高校圈子里热度很高不是因为奖金而是因为它的数据来自真实手机拍摄场景——光照不均、目标尺度差异大、背景杂乱跟学术数据集里那种干干净净的图片完全两回事。很多队伍在本地验证集上跑出很高的 mAP一提交就翻车排名直接掉到几百名开外。这份第三名解决方案的源码加学习说明核心价值不在于「第三名」这个名次而在于它完整记录了一套从数据清洗、增强策略、模型选型到推理后处理的工程链路。你如果正在做计算机视觉项目或者准备打类似的 AI 算法竞赛这份材料能让你少走很多弯路。它适合已经掌握 Python 和 PyTorch 基础、想看看工业级方案怎么落地的人不适合零基础入门。2. 赛题拆解与方案选型为什么是检测而不是分类2.1 赛题的真实难点在哪里先搞清楚这个赛道到底在考什么。手机拍摄图像的目标检测难点集中在三个地方第一目标尺度跨度大近处物体可能占画面三分之一远处目标只有几十个像素第二光照条件不可控逆光、暗光、过曝都会让颜色特征失效第三类别不均衡某些类别的样本量可能是其他类别的十几倍。很多参赛队伍一开始把它当成标准分类任务来做用 ResNet 加全连接层直接输出类别结果发现定位精度完全不够。赛题要求的是同时输出边界框和类别本质上是目标检测任务。选型上当年主流方案分两派两阶段检测器如 Faster R-CNN 精度高但推理慢单阶段检测器如 YOLO 和 RetinaNet 速度快但小目标容易漏。第三名方案最终选了 RetinaNet 作为基线原因是它的 Focal Loss 能有效缓解正负样本失衡问题而且特征金字塔结构对小目标更友好。2.2 数据预处理与增强策略数据增强这块方案里做了几件事。基础增强包括随机翻转、随机裁剪、颜色抖动这些是标配。真正拉开差距的是两处细节一是 Mosaic 增强把四张图拼成一张让模型见到更多尺度的组合二是针对手机图像的特定增强比如模拟不同色温的白平衡偏移和镜头畸变。import albumentations as A from albumentations.pytorch import ToTensorV2 # 训练集增强管线 train_transform A.Compose([ A.RandomResizedCrop(height512, width512, scale(0.5, 1.0)), A.HorizontalFlip(p0.5), A.ColorJitter(brightness0.3, contrast0.3, saturation0.3, hue0.1, p0.7), # 模拟手机白平衡偏移 A.RGBShift(r_shift_limit15, g_shift_limit15, b_shift_limit15, p0.3), A.GaussNoise(var_limit(10.0, 50.0), p0.2), A.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ToTensorV2(), ], bbox_paramsA.BboxParams(formatpascal_voc, label_fields[labels]))这段代码的关键参数说明RandomResizedCrop的scale设为 0.5 到 1.0意味着每张图至少保留一半面积避免裁掉太多目标ColorJitter的hue只给 0.1因为色相偏移太大会让某些类别的颜色特征完全失真RGBShift模拟的是手机自动白平衡在不同光源下的偏差这是手机图像特有的问题。bbox_params必须指定format和label_fields否则边界框不会跟着图像一起变换这是新手最容易踩的坑。2.3 模型结构与训练配置RetinaNet 的骨干网络选了 ResNet-50 加 FPN输入分辨率 512×512。训练用了 4 张 V100batch size 设为 16初始学习率 0.01余弦退火调度总共训练 50 个 epoch。损失函数就是标准的 Focal Loss 加 Smooth L1Focal Loss 的 alpha 设 0.25gamma 设 2.0。import torch import torch.nn as nn class FocalLoss(nn.Module): def __init__(self, alpha0.25, gamma2.0): super().__init__() self.alpha alpha self.gamma gamma def forward(self, preds, targets): # preds: [N, num_classes], targets: [N] ce_loss nn.functional.cross_entropy(preds, targets, reductionnone) pt torch.exp(-ce_loss) focal_loss self.alpha * (1 - pt) ** self.gamma * ce_loss return focal_loss.mean()Focal Loss 的核心逻辑是对容易分类的样本降权让模型聚焦在难样本上。alpha控制正负样本的权重gamma控制难易样本的权重。gamma 越大模型越关注特别难分的样本但太大也会导致训练不稳定。实践中 gamma 取 1.5 到 2.5 之间比较稳妥。3. 从源码到可复现环境搭建与训练全流程3.1 环境依赖与目录结构拿到源码后第一件事是配环境。这份方案基于 PyTorch 1.6 和 torchvision 0.7CUDA 版本 10.1。如果你现在用更新的版本大部分代码能跑但要注意torchvision.ops里的一些 API 可能有变化。# 创建虚拟环境 conda create -n digix_cv python3.7 conda activate digix_cv # 安装核心依赖 pip install torch1.6.0 torchvision0.7.0 pip install albumentations0.4.6 pip install opencv-python4.4.0.44 pip install pycocotools2.0.1 pip install tqdm tensorboard # 验证 GPU 可用 python -c import torch; print(torch.cuda.is_available())目录结构一般是data/放数据集models/放网络定义utils/放工具函数train.py和inference.py是入口脚本。先跑通train.py --help看看参数列表再决定怎么改配置。3.2 数据加载与标注格式转换赛题数据给的标注格式可能是 COCO 或 VOC需要统一转成训练脚本能读的格式。常见做法是转成 COCO 格式的 JSON因为 pycocotools 的评估接口最完善。import json import xml.etree.ElementTree as ET from pathlib import Path def voc_to_coco(voc_dir, output_json): coco {images: [], annotations: [], categories: []} categories {} # 类别名到 id 的映射 ann_id 1 for xml_file in Path(voc_dir).glob(*.xml): tree ET.parse(xml_file) root tree.getroot() img_name root.find(filename).text size root.find(size) w, h int(size.find(width).text), int(size.find(height).text) coco[images].append({id: len(coco[images]) 1, file_name: img_name, width: w, height: h}) img_id len(coco[images]) for obj in root.findall(object): cls_name obj.find(name).text if cls_name not in categories: categories[cls_name] len(categories) 1 coco[categories].append({id: categories[cls_name], name: cls_name}) bbox obj.find(bndbox) x1 float(bbox.find(xmin).text) y1 float(bbox.find(ymin).text) x2 float(bbox.find(xmax).text) y2 float(bbox.find(ymax).text) coco[annotations].append({ id: ann_id, image_id: img_id, category_id: categories[cls_name], bbox: [x1, y1, x2 - x1, y2 - y1], area: (x2 - x1) * (y2 - y1), iscrowd: 0 }) ann_id 1 with open(output_json, w) as f: json.dump(coco, f)这段转换脚本有几个边界情况要注意VOC 的 bbox 是xmin, ymin, xmax, ymaxCOCO 要的是x, y, width, height转换时别忘了减area字段在评估时会用到不能省iscrowd一般设 0但如果数据集里有密集遮挡区域需要单独处理。3.3 训练脚本的关键参数与启动命令训练入口一般长这样python train.py \ --data_dir ./data/coco_format \ --backbone resnet50 \ --epochs 50 \ --batch_size 16 \ --lr 0.01 \ --img_size 512 \ --num_classes 20 \ --pretrained ./weights/resnet50.pth \ --output_dir ./runs/exp1 \ --gpu_ids 0,1,2,3参数说明--lr是初始学习率配合余弦退火最低降到 1e-5--img_size如果显存不够可以降到 384但小目标召回会掉--pretrained加载 ImageNet 预训练权重能加速收敛--gpu_ids多卡训练时用nn.DataParallel或DistributedDataParallel后者更快但配置麻烦。训练过程中重点看两个指标训练 loss 是否平稳下降验证集 mAP 是否在涨。如果 loss 震荡厉害先把学习率降一半如果 mAP 早早饱和检查数据增强是不是太弱。4. 推理与后处理让模型在测试集上真正能打4.1 推理流程与置信度阈值训练完模型推理阶段的核心是控制好置信度阈值和 NMS 的 IoU 阈值。阈值设太高漏检多设太低误检多。第三名方案在验证集上做了网格搜索最终置信度阈值取 0.4NMS IoU 取 0.5。import torch from torchvision.ops import nms def postprocess(preds, conf_thresh0.4, iou_thresh0.5): # preds: [N, 41num_classes] 格式为 xyxy, obj_conf, cls_conf boxes preds[:, :4] scores preds[:, 4] * preds[:, 5:].max(dim1)[0] labels preds[:, 5:].argmax(dim1) keep scores conf_thresh boxes, scores, labels boxes[keep], scores[keep], labels[keep] # 按类别做 NMS final_boxes, final_scores, final_labels [], [], [] for cls in labels.unique(): mask labels cls keep_idx nms(boxes[mask], scores[mask], iou_thresh) final_boxes.append(boxes[mask][keep_idx]) final_scores.append(scores[mask][keep_idx]) final_labels.append(labels[mask][keep_idx]) return torch.cat(final_boxes), torch.cat(final_scores), torch.cat(final_labels)这里有个细节NMS 要按类别分别做不能所有类别混在一起做。混在一起做的话两个不同类别的框如果重叠度高会互相抑制导致漏检。这是很多新手推理时翻车的地方。4.2 多尺度测试与 TTA为了进一步提升精度方案里用了多尺度测试和水平翻转 TTA。具体做法是把测试图缩放到不同尺寸分别推理再把结果融合。def multi_scale_inference(model, image, scales[0.8, 1.0, 1.2]): all_preds [] for scale in scales: h, w image.shape[:2] new_h, new_w int(h * scale), int(w * scale) resized cv2.resize(image, (new_w, new_h)) tensor torch.from_numpy(resized).permute(2, 0, 1).float().unsqueeze(0).cuda() with torch.no_grad(): preds model(tensor) # 把框映射回原图尺寸 preds[:, [0, 2]] / scale preds[:, [1, 3]] / scale all_preds.append(preds) # 融合多尺度结果再做一次 NMS merged torch.cat(all_preds, dim0) return postprocess(merged, conf_thresh0.4, iou_thresh0.5)多尺度测试的收益在小目标上最明显因为放大尺度能让小目标占更多像素。但尺度也不是越多越好3 个尺度一般够用再多推理时间线性增长收益递减。4.3 结果提交格式与常见错误提交格式每个比赛不一样DIGIX 当年要求的是 JSON 文件每张图对应一个检测列表。常见错误包括坐标没转回原图尺寸、类别 id 从 0 开始还是从 1 开始搞混、JSON 里用了 numpy 的 float 类型导致序列化失败。import json import numpy as np def save_submission(results, output_path): submission [] for img_id, (boxes, scores, labels) in results.items(): for box, score, label in zip(boxes, scores, labels): submission.append({ image_id: int(img_id), category_id: int(label), bbox: [float(x) for x in box], # 必须转成 Python float score: float(score) }) with open(output_path, w) as f: json.dump(submission, f)float(x)这个转换看着多余但 numpy 的 float32 直接 json.dump 会报错这是血泪教训。5. 避坑与排查那些让排名掉几百名的细节5.1 验证集 mAP 很高但提交分数很低现象本地验证集 mAP 0.65提交上去只有 0.3。原因通常是验证集和测试集分布不一致或者验证集划分时把同一场景的图片分到了训练和验证两边导致验证集虚高。解决办法是按场景或按拍摄设备划分验证集确保验证集和测试集同分布。5.2 训练 loss 正常但 mAP 不涨现象loss 从 2.0 降到 0.3但验证集 mAP 一直在 0.2 左右徘徊。原因可能是学习率太大导致模型在局部最优附近震荡或者数据增强太强让模型学不到有效特征。先把学习率降一个数量级试试再把颜色抖动和随机裁剪的强度调低。5.3 推理速度太慢导致超时现象单张图推理要 2 秒测试集一万张图根本跑不完。原因可能是输入分辨率设太高或者没开半精度推理。把img_size从 512 降到 384开启torch.cuda.amp半精度速度能快一倍以上。5.4 小目标漏检严重现象大目标检测没问题小目标几乎全漏。原因可能是 FPN 层数不够或者锚框尺寸跟小目标不匹配。增加 P2 层特征图把锚框最小尺寸从 32 降到 16再配合多尺度测试小目标召回能明显改善。5.5 多卡训练时 batch size 设置错误现象4 张卡训练每张卡 batch size 设 16以为总 batch 是 16实际是 64。学习率没跟着调导致训练发散。多卡训练时总 batch size 等于单卡 batch 乘以卡数学习率要按线性缩放规则调整或者用 warmup 慢慢升上去。6. 进阶技巧从第三名方案里能提炼出的通用方法论这份方案里有一个容易被忽略但很实用的技巧在训练后期对难样本做重采样。具体做法是每个 epoch 结束后统计每个样本的 loss把 loss 最高的那 10% 样本在下个 epoch 里重复采样一次。这个操作能让模型在训练后期继续聚焦难样本mAP 通常能再涨 1 到 2 个点。class HardExampleSampler(torch.utils.data.Sampler): def __init__(self, dataset, sample_losses, top_ratio0.1): self.dataset dataset self.top_ratio top_ratio n len(sample_losses) top_k int(n * top_ratio) # 按 loss 从高到低排序取前 top_k 个索引 sorted_idx np.argsort(sample_losses)[::-1] self.hard_indices sorted_idx[:top_k] self.all_indices list(range(n)) def __iter__(self): # 难样本重复一次其余正常采样 indices self.all_indices list(self.hard_indices) np.random.shuffle(indices) return iter(indices) def __len__(self): return len(self.all_indices) len(self.hard_indices)这个采样器的逻辑是每个 epoch 结束后用当前模型跑一遍训练集记录每个样本的 loss然后构造下个 epoch 的采样索引。难样本被采两次相当于变相提高了难样本的权重。注意sample_losses要在每个 epoch 后更新不能一直用初始值。另一个值得借鉴的是模型集成。第三名方案最后用了三个不同随机种子的模型做加权融合权重按验证集 mAP 分配。融合时先各自做 NMS再把所有框合在一起做一次全局 NMS。这个操作在比赛后期是提分利器但推理成本翻三倍线上服务要权衡。我自己做计算机视觉项目这些年最大的习惯是每改一个参数就记一笔包括改之前和改之后的指标。很多时候你觉得某个改动没用过两周回头看日志才发现它其实在特定场景下有效。这份 DIGIX 方案的学习说明里也有类似的记录建议你拿到源码后先别急着跑花半小时把说明里的实验记录看一遍能省下大量试错时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站