资讯动态

CenterNet目标检测实战:从Anchor-Free原理到边缘端部署优化

发布时间:2026/10/15 9:48:32 来源:尧图企业网站定制
1. 为什么在众多检测器里我最终选了CenterNet目标检测这个方向做了几年下来从最早的滑动窗口加手工特征到后来的两阶段、单阶段再到各种Anchor-Free方案我基本都踩过一遍。2023年那会儿接手一个工业质检的小项目需要在产线边缘设备上跑实时检测算力卡得很死一张入门级推理卡要同时处理四路视频流帧率还不能低于25。当时试过几个主流方案要么精度够但速度上不去要么速度快但小目标漏检严重。折腾了两周之后我把目光锁定在了CenterNet上原因很简单它把检测问题转化成了关键点估计问题结构干净后处理极简没有Anchor那一堆超参要调部署起来心里有底。CenterNet的核心思想一句话就能说清楚把每个目标看作一个点检测就是找点、回归尺寸。听起来简单但它背后解决的是传统Anchor-Based方法里最让人头疼的几个问题。你想想Faster R-CNN那一套先要生成几千个候选框再逐个分类回归NMS还要再筛一遍整个流程又长又重。YOLO系列虽然快但Anchor的尺寸、比例、数量都是超参换个数据集就得重新聚类一遍调起来很烦。CenterNet直接绕开了这些它只预测三类东西热力图上的峰值点中心点、中心点的偏移量、以及目标的宽高。推理的时候取热力图上响应最高的几个点加上偏移配上宽高框就出来了连NMS都可以用简单的max pooling替代。这个方案特别适合什么场景我总结下来是三类第一边缘端部署因为结构简单量化友好第二小目标密集场景因为热力图是逐像素预测的不像Anchor那样受限于预设尺度第三需要快速迭代的项目因为超参少换数据集时调参成本低。当然它也不是万能的后面我会详细说它在哪些情况下会翻车。这篇文章我打算把自己从零搭建、训练、调优到部署CenterNet的完整过程拆开来讲包括模型结构的设计逻辑、损失函数的细节、数据增强的坑、训练技巧、以及实际部署时的性能优化。不管你是刚接触目标检测的新手还是想找个轻量方案落地的老手应该都能从里面找到能直接抄作业的东西。2. CenterNet的整体架构与设计思路拆解2.1 从Anchor-Based到Anchor-Free的思维转变要理解CenterNet得先明白它跟传统方法在思路上有什么本质区别。Anchor-Based的方法比如RetinaNet、YOLOv3是在特征图的每个位置上预设一堆不同尺寸和比例的框然后让网络去判断每个框里有没有目标、是什么类别、需要怎么调整。这种做法的问题在于Anchor的设计本身就是一个强先验你得根据数据集的分布去聚类出合适的尺度和比例。如果数据集里目标尺寸跨度很大比如既有几十像素的小零件又有上千像素的大设备那Anchor就得设很多组导致正负样本极度不平衡训练起来很费劲。CenterNet的做法完全不同。它不预设任何框而是把目标压缩成一个中心点网络只需要学会两件事第一在热力图上把中心点位置点亮第二回归出这个点对应的目标宽高。这样一来正样本的数量就从“与Anchor匹配的框”变成了“目标中心点附近的几个像素”数量大大减少而且定义非常明确不需要计算IoU来分配正负样本。我第一次看这个思路的时候感觉像是从“穷举法”换成了“定位法”效率提升是必然的。但这里有个关键细节中心点怎么定义最直接的做法是取标注框的几何中心但实际中目标不一定对称几何中心可能落在背景上。CenterNet的处理方式是先把标注框的中心点算出来然后除以下采样倍数通常是4得到热力图上的坐标。如果这个坐标不是整数就取整作为正样本位置同时记录小数部分作为偏移量让网络去回归。这个设计很巧妙既保证了正样本的确定性又通过偏移量回归弥补了量化误差。2.2 骨干网络的选择与特征图分辨率权衡CenterNet本身不绑定特定的骨干网络论文里用了ResNet-18、ResNet-101、DLA-34、Hourglass-104等好几种。选哪个取决于你的算力预算和精度要求。我那个工业质检项目一开始用的是ResNet-18因为参数量小推理快。但实测下来发现对于产线上那些几十像素的缺陷ResNet-18的感受野和特征表达能力都不太够漏检率偏高。后来换成了DLA-34精度上去了但速度掉了差不多30%。最后折中了一下用ResNet-50加了几层可变形卷积算是找到了一个平衡点。这里有个容易被忽略的点输出特征图的分辨率。CenterNet通常把输入图像下采样4倍也就是说一张512x512的图输出热力图是128x128。这个分辨率直接决定了小目标的检测能力。下采样倍数越大特征图越小计算量越低但小目标在热力图上可能就剩一个像素甚至消失了。我试过下采样8倍速度确实快了不少但那些小于16像素的目标基本就检不出来了。所以如果你的场景里小目标多老老实实用下采样4倍别为了速度牺牲精度。另外为了弥补下采样带来的信息损失CenterNet在骨干网络后面通常会加一个上采样模块把低层特征和高层特征融合起来。具体做法是取骨干网络最后三个阶段输出的特征图分别经过可变形卷积调整通道数然后上采样到同一分辨率再逐元素相加。这个结构在DLA-34里叫IDAIterative Deep Aggregation在ResNet里一般用反卷积加跳跃连接来实现。我实测下来加了上采样融合之后小目标的召回率能提升5到8个百分点代价是推理时间增加15%左右这个 trade-off 是值得的。2.3 三个输出头的分工与协同CenterNet的输出分三个头每个头负责不同的任务但共享同一个骨干特征。这三个头分别是热力图头Heatmap Head输出通道数等于类别数每个通道是一张热力图表示该类目标中心点出现的概率。用的是高斯核在中心点位置生成软标签越靠近中心响应越高越远越低。这个设计是为了让网络学习到一个平滑的分布而不是硬性的0/1标签训练更稳定。偏移头Offset Head输出两个通道分别预测中心点在x和y方向上的偏移量。因为热力图是下采样后的中心点坐标会有量化误差偏移头就是用来补偿这个误差的。偏移量的范围通常在-0.5到0.5之间用Sigmoid激活或者直接回归都行。尺寸头Size Head输出两个通道预测目标的宽和高。注意这里预测的是相对于下采样后特征图的尺寸还是原始图像的尺寸不同实现有差异。我一般直接回归原始尺寸的对数因为对数空间更稳定梯度不会因为目标大小差异过大而爆炸。这三个头是联合训练的损失函数是加权和。热力图用Focal Loss的变体偏移和尺寸用L1 Loss。权重方面论文里给的是热力图1.0、偏移1.0、尺寸0.1。我实际调的时候发现尺寸损失的权重可以适当调高到0.2到0.3尤其是当数据集中目标尺寸变化大的时候这样网络会更关注尺寸回归的准确性。注意三个头的输出通道数一定要跟类别数对齐热力图头是类别数偏移和尺寸头固定是2。如果类别数变了热力图头的通道数要跟着改但预训练权重里这一层通常要重新初始化。3. 核心细节解析与实操要点3.1 高斯核生成与热力图标签制作热力图的标签制作是CenterNet训练里最核心的一步也是最容易出错的地方。具体做法是对于每个标注框先算出它的中心点在下采样后特征图上的坐标然后以这个坐标为中心用一个二维高斯核生成一个软标签。高斯核的半径不是固定的而是根据目标的宽高自适应计算的。论文里给了一个公式大意是如果目标宽高分别是w和h那么高斯核的半径r跟w和h的几何平均成正比同时还要考虑一个最小半径的约束防止小目标的高斯核太小导致正样本太少。我一开始没太在意这个半径计算直接用了固定半径结果训练出来的模型对大目标还行小目标基本检不出来。后来仔细看了论文的附录才发现半径是自适应的。具体实现的时候可以用一个查表法预先计算好不同尺寸对应的半径存成一个数组训练时直接查表比每次实时计算快很多。这个技巧在数据加载阶段能省不少时间尤其是数据集大的时候。还有一个坑是高斯核的截断。理论上高斯分布是无限延伸的但实际实现时不可能用无限大的核一般会截断到3倍标准差。如果截断得太小中心点周围的响应会突然降到0网络学到的分布不连续如果截断得太大计算量又会增加。我试过2倍和4倍最后发现3倍是最稳的既保证了平滑性计算量也可接受。另外如果两个目标的中心点靠得很近它们的高斯核会重叠。这时候不能简单覆盖而要取逐元素的最大值。这个细节在密集场景下特别重要比如人群检测或者零件堆叠的场景。我见过有人直接用后一个覆盖前一个结果训练出来的模型在密集区域漏检严重。3.2 损失函数的设计与权重调优CenterNet的损失函数是三个部分的加权和我逐个拆开说。热力图损失用的是Focal Loss的变体公式大致是对于正样本位置损失是 -(1-p)^2 * log(p)对于负样本位置损失是 -(1-y)^4 * p^2 * log(1-p)其中y是高斯核生成的软标签值p是网络预测值。这个设计的好处是对于中心点附近的负样本y不为0但很小损失会被抑制不会像硬标签那样产生过大的梯度。我实测下来这个损失函数对训练稳定性帮助很大尤其是早期epochloss曲线很平滑不会出现剧烈震荡。偏移损失和尺寸损失都用L1 Loss但只对正样本位置计算。也就是说只有中心点那个像素的偏移和尺寸参与损失计算其他位置不管。这个设计很合理因为只有中心点才有对应的目标尺寸其他位置没有意义。但这里有个细节正样本位置的定义。通常是把高斯核响应最高的那个点作为正样本但如果两个目标的高斯核重叠可能会出现多个峰值。我的做法是在生成标签的时候就记录下每个目标的精确中心点坐标训练时直接取这些坐标对应的位置作为正样本避免歧义。权重方面论文里给的是1.0、1.0、0.1。我实际调的时候发现尺寸损失的权重可以适当提高。原因在于热力图损失和偏移损失的量级通常在0.1到1之间而尺寸损失因为直接回归像素值量级可能到几十甚至上百。如果不加权尺寸损失会主导梯度导致热力图学不好。但权重也不能太高否则网络会过度关注尺寸回归忽略中心点定位。我一般从0.1开始试如果发现框的尺寸偏差大就逐步加到0.2或0.3同时观察热力图的峰值是否清晰。提示训练初期可以先冻结尺寸头只训练热力图和偏移头等热力图收敛了再解冻尺寸头联合训练。这个策略在我几个项目里都管用能加快收敛速度减少早期震荡。3.3 数据增强的策略与禁忌数据增强这块CenterNet跟其他检测器大同小异但有几个地方需要特别注意。随机翻转是最常用的水平翻转基本没问题但垂直翻转要看场景。如果是自然图像垂直翻转通常不合理因为天空不会跑到下面去。但如果是工业质检或者遥感图像垂直翻转往往是可以的甚至必要的。我那个产线项目图像是从上往下拍的垂直翻转完全合理加了之后召回率提升了3个点。随机缩放是必须的因为CenterNet对尺度比较敏感。但缩放的范围要控制好一般0.6到1.4之间比较稳妥。缩得太小小目标会消失放得太大大目标会超出图像边界。我试过0.5到1.5发现边缘目标被截断的情况明显增多后来收窄到0.7到1.3效果更稳。颜色抖动对热力图的影响不大但能提升模型的泛化能力。亮度、对比度、饱和度都可以调幅度控制在0.2以内就行。注意不要用太强的颜色变换否则可能把某些目标的特征颜色改得面目全非反而有害。Cutout和Mixup这类增强在CenterNet上要慎用。Cutout会遮挡部分区域如果正好遮住中心点那这个目标就变成负样本了但网络可能还能从周围特征推断出这里有目标导致标签不一致。Mixup把两张图叠加中心点会混在一起热力图标签很难处理。我试过Mixup训练loss一直下不去后来放弃了。还有一个容易被忽略的点图像归一化。CenterNet的输入通常归一化到0到1之间或者用ImageNet的均值和标准差。我建议用后者因为预训练权重是在ImageNet上训的归一化方式一致能更好地利用预训练特征。具体就是每个通道减去均值再除以标准差均值一般是[0.485, 0.456, 0.406]标准差是[0.229, 0.224, 0.225]。4. 完整训练流程与关键环节实现4.1 环境搭建与依赖配置我平时用PyTorch比较多所以这里以PyTorch为例。环境配置这块说简单也简单说坑也多。首先是CUDA和cuDNN的版本匹配这个不用多说装错了连import都过不了。我一般用conda建一个独立环境Python版本选3.8或3.9太新的版本有些库还不支持。核心依赖就几个torch、torchvision、numpy、opencv-python、pycocotools如果做COCO格式评估、tqdm进度条。如果要可视化热力图可以加个matplotlib。版本方面torch 1.10以上都行我实测1.12和1.13都比较稳。注意torchvision的版本要跟torch匹配不然会有各种奇怪的报错。conda create -n centernet python3.9 conda activate centernet pip install torch1.13.0 torchvision0.14.0 pip install numpy opencv-python pycocotools tqdm matplotlib如果你要用DLA-34作为骨干还需要额外装一个DLA的库或者直接把DLA的网络定义代码拷到项目里。我一般选择后者因为第三方库的版本兼容性不好保证自己维护一份更放心。注意如果是在边缘设备上部署训练环境跟推理环境可以分开。训练用服务器推理用TensorRT或者ONNX Runtime。但要注意算子兼容性有些训练时用的算子推理引擎不支持最好在训练阶段就用ONNX导出测试一下。4.2 数据集准备与标注格式转换CenterNet支持多种标注格式但最通用的是COCO格式。如果你的数据是VOC格式需要先转成COCO。转换脚本网上很多但要注意几个细节VOC的坐标是左上角和右下角COCO是左上角坐标加宽高转换时别忘了减一或者加一不同实现有差异最好统一。我一般会把数据集组织成这样的结构dataset/ annotations/ train.json val.json images/ train/ xxx.jpg val/ xxx.jpgJSON文件的格式跟COCO一致包含images、annotations、categories三个字段。images里记录图片id、文件名、宽高annotations里记录标注id、图片id、类别id、bboxx,y,w,h、area、iscrowdcategories里记录类别id和名称。这里有个坑iscrowd字段。COCO里iscrowd1表示这个标注是密集区域评估时会特殊处理。CenterNet训练时一般忽略iscrowd1的标注因为密集区域没有明确的中心点。如果你的数据里有很多密集标注建议把它们拆成单个目标或者直接跳过。另一个坑是图片尺寸。CenterNet训练时通常会把图片resize到固定尺寸比如512x512。如果原始图片长宽比差异很大直接resize会导致目标变形。我的做法是保持长宽比短边resize到512长边按比例缩放然后padding到512x512。这样目标不会变形但会有黑边。黑边区域在生成热力图标签时要忽略不然网络会学到错误的背景。4.3 模型定义与初始化技巧模型定义这块我建议直接参考官方实现但有几个地方可以优化。首先是骨干网络的选择前面说了DLA-34精度好但慢ResNet-50折中ResNet-18快但精度一般。如果你有预训练权重一定要加载能省很多训练时间。注意加载的时候要跳过最后一层全连接因为分类任务的类别数跟检测任务不一样。import torch import torch.nn as nn from torchvision.models import resnet50 class CenterNet(nn.Module): def __init__(self, num_classes80, backboneresnet50): super().__init__() self.backbone resnet50(pretrainedTrue) # 去掉最后的全连接和平均池化 self.backbone nn.Sequential(*list(self.backbone.children())[:-2]) # 上采样融合模块 self.upsample nn.Sequential( nn.ConvTranspose2d(2048, 256, 4, 2, 1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.ConvTranspose2d(256, 256, 4, 2, 1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.ConvTranspose2d(256, 64, 4, 2, 1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue) ) # 三个输出头 self.hm_head nn.Sequential( nn.Conv2d(64, 64, 3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(64, num_classes, 1) ) self.wh_head nn.Sequential( nn.Conv2d(64, 64, 3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(64, 2, 1) ) self.off_head nn.Sequential( nn.Conv2d(64, 64, 3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(64, 2, 1) ) def forward(self, x): feat self.backbone(x) feat self.upsample(feat) hm torch.sigmoid(self.hm_head(feat)) wh self.wh_head(feat) off self.off_head(feat) return hm, wh, off初始化方面热力图头的最后一层卷积偏置要设成负值比如-2.19这是Focal Loss论文里推荐的能防止训练初期正样本被大量负样本淹没。其他层用默认的Kaiming初始化就行。4.4 训练循环与学习率调度训练循环这块我习惯用PyTorch原生的写法不依赖高级框架这样出问题好排查。核心步骤就是前向传播、计算损失、反向传播、更新参数。但有几个细节要注意。学习率方面我一般用Adam优化器初始学习率1e-4配合余弦退火或者StepLR。Adam对学习率不那么敏感1e-4到5e-4之间都能跑。如果loss震荡厉害就降到1e-4如果收敛太慢就加到5e-4。我实测下来1.25e-4是个比较稳的值。Batch Size方面CenterNet对batch size比较敏感因为热力图损失是逐像素计算的batch size太小会导致梯度噪声大。我建议至少8能上16更好。如果显存不够可以用梯度累积比如batch size4累积4次等效于16。训练轮数方面COCO数据集一般训140个epoch小数据集可以少一些50到100个epoch通常够了。我那个产线项目数据量不大大概5000张图训了80个epoch就收敛了。判断收敛的标准是验证集上的mAP不再提升同时训练loss趋于平稳。optimizer torch.optim.Adam(model.parameters(), lr1.25e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size30, gamma0.5) for epoch in range(num_epochs): model.train() for images, targets in train_loader: images images.cuda() hm, wh, off model(images) loss, loss_stats compute_loss(hm, wh, off, targets) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() # 验证 model.eval() with torch.no_grad(): evaluate(model, val_loader)提示训练过程中一定要定期保存检查点尤其是验证集指标最好的那个模型。我吃过亏训练到一半机器断电几天的成果全没了。现在都是每个epoch存一次保留最近5个和最好的1个。4.5 推理与后处理实现推理阶段CenterNet的后处理非常简洁。输入一张图前向传播得到热力图、宽高和偏移然后在热力图上做3x3的max pooling保留局部峰值再取top-k个峰值点根据阈值筛选最后把偏移和宽高加上去就得到检测框了。def decode(hm, wh, off, K100, thresh0.3): # hm: [1, C, H, W] # wh: [1, 2, H, W] # off: [1, 2, H, W] hm hm.sigmoid() # 如果训练时没用sigmoid # 3x3 max pooling hm_pool F.max_pool2d(hm, 3, stride1, padding1) hm hm * (hm_pool hm).float() # 取top-k scores, inds hm.view(-1).topk(K) scores scores / K # 归一化 # 解码坐标 ys inds // hm.size(3) xs inds % hm.size(3) # 加上偏移 off off.view(2, -1)[:, inds] xs xs.float() off[0] ys ys.float() off[1] # 加上宽高 wh wh.view(2, -1)[:, inds] x1 xs - wh[0] / 2 y1 ys - wh[1] / 2 x2 xs wh[0] / 2 y2 ys wh[1] / 2 # 筛选 keep scores thresh return torch.stack([x1, y1, x2, y2], dim1)[keep], scores[keep]这里有个细节top-k的K值怎么选。K太小密集场景会漏检K太大会增加后处理时间而且可能引入误检。我一般设K100对于大多数场景够用了。如果类别特别多或者目标特别密集可以加到200。另外热力图的阈值也很关键。阈值太低误检多阈值太高漏检多。我一般从0.3开始试根据验证集上的precision-recall曲线调整。如果场景对误检容忍度低就调到0.4或0.5如果对漏检容忍度低就降到0.2。5. 常见问题与排查技巧实录5.1 训练不收敛或Loss震荡这是最常见的问题原因可能有很多。我按排查顺序列一下第一检查学习率。学习率太大是最常见的原因。如果loss在前几个epoch就飙到nan那基本就是学习率太大了。降到1e-4甚至5e-5试试。如果loss下降很慢那可能是学习率太小适当调大。第二检查数据标签。热力图标签生成有没有问题中心点坐标算对了吗高斯核半径合理吗我遇到过有人把中心点坐标算错了导致热力图峰值位置偏移网络怎么学都学不对。建议可视化几张热力图标签跟原图对照一下看看峰值是不是在目标中心。第三检查损失权重。尺寸损失的权重如果太大会主导梯度导致热力图学不好。可以先把尺寸损失权重设成0只训热力图和偏移看看loss能不能降下去。如果能再逐步加尺寸损失。第四检查BatchNorm。如果batch size太小BatchNorm的统计量不准会导致训练不稳定。可以试试用GroupNorm替代或者增大batch size。第五检查预训练权重。如果骨干网络没有加载预训练权重训练会慢很多而且容易过拟合。确保加载了ImageNet预训练权重并且冻结了前几层。5.2 小目标漏检严重小目标漏检是CenterNet的常见问题原因通常是下采样倍数太大小目标在热力图上只剩一个像素甚至消失了。解决办法有几个第一减小下采样倍数。从4倍改成2倍特征图分辨率翻倍小目标能保留更多信息。但计算量会增加4倍左右需要权衡。第二增加上采样融合。把骨干网络浅层的特征也融合进来浅层特征分辨率高包含更多小目标信息。具体做法是在上采样模块里加一个从浅层到深层的跳跃连接。第三调整高斯核半径。小目标的高斯核半径如果太小正样本数量不足网络学不到。可以适当增大最小半径的约束保证每个目标至少有3x3的正样本区域。第四数据增强时注意小目标。随机缩放的时候小目标容易被缩得更小导致消失。可以在缩放后检查一下如果目标小于某个阈值就跳过这次增强。第五推理时降低阈值。小目标的响应通常比较弱阈值太高会被过滤掉。可以适当降低阈值但要注意误检会增加。5.3 密集场景中心点重叠密集场景下多个目标的中心点可能靠得很近高斯核重叠严重导致热力图上峰值不清晰网络难以区分。这个问题在人群检测、零件堆叠等场景很常见。我的解决办法是在生成热力图标签时对重叠区域取逐元素最大值而不是覆盖。这样每个目标的中心点都能保留一定的响应不会因为被覆盖而消失。另外可以适当减小高斯核半径让峰值更尖锐便于区分。推理时3x3的max pooling可能不够可以改成5x5但要注意这会增加后处理时间。另一个技巧是在top-k选择时如果两个峰值距离太近可以只保留响应更高的那个避免重复检测。5.4 部署时速度不达标训练时精度够了但部署到边缘设备上速度不达标这是很常见的问题。优化方向有几个第一模型剪枝。把不重要的通道剪掉参数量能减少30%到50%速度提升明显。但剪枝后需要微调不然精度会掉。第二量化。把FP32量化成INT8速度能提升2到3倍精度损失通常在1到2个点。注意量化时要校准用一批代表性数据跑一遍确定激活值的范围。第三算子融合。把ConvBNReLU融合成一个算子减少内存访问和计算量。TensorRT和ONNX Runtime都支持自动融合。第四输入分辨率。如果场景允许可以把输入从512x512降到384x384速度能提升近一倍精度损失大概2到3个点。第五骨干网络替换。把ResNet-50换成MobileNetV3或者ShuffleNetV2参数量大幅减少速度提升明显。但精度会掉一些需要重新训练。下面是我在实际项目中总结的问题速查表问题现象可能原因排查方法解决方案Loss为nan学习率太大检查前几个epoch的loss降低学习率到1e-4或5e-5Loss不下降标签错误可视化热力图标签修正中心点坐标和半径小目标漏检下采样倍数大检查热力图分辨率改用下采样2倍或加上采样融合密集场景漏检高斯核重叠可视化热力图峰值取逐元素最大值减小半径推理速度慢模型太大profile各层耗时剪枝、量化、换骨干误检多阈值太低看PR曲线提高热力图阈值框尺寸不准尺寸损失权重低看尺寸回归误差提高尺寸损失权重到0.2-0.35.5 独家避坑技巧说几个我在实际项目中踩过的坑文档里不会写的。第一个数据加载的num_workers。PyTorch的DataLoader如果num_workers设得太大会导致内存爆炸尤其是图片分辨率高的时候。我一般设成4到8根据CPU核心数调整。如果发现训练过程中内存持续增长大概率是num_workers的问题。第二个混合精度训练。用AMP自动混合精度能省显存、加速训练但CenterNet的热力图损失在FP16下容易溢出。我的做法是前向传播用FP16损失计算用FP32这样既省显存又稳定。第三个验证集的batch size。验证时如果batch size跟训练时不一样BatchNorm的统计量会不一致导致验证指标偏低。我一般验证时也用跟训练一样的batch size或者直接用训练模式的统计量。第四个模型保存的坑。保存模型时如果用了DataParallel或者DistributedDataParallelstate_dict的key会多一个module.前缀。加载的时候要么去掉前缀要么用同样的并行方式加载。我一般保存的时候就把前缀去掉加载时直接用单卡加载。第五个热力图的可视化。训练过程中定期可视化热力图能帮你快速发现问题。如果热力图上一片模糊没有清晰的峰值说明网络没学好如果峰值位置偏移说明标签有问题如果某些类别完全没有响应说明该类别的样本太少或者损失权重有问题。6. 性能优化与部署实战6.1 模型导出与推理引擎选择训练完了要部署第一步是导出模型。PyTorch原生格式.pth只能在PyTorch环境里跑部署到边缘设备上通常要转成ONNX或者TensorRT。ONNX通用性好跨平台支持广TensorRT在NVIDIA设备上性能最好但只支持NVIDIA的卡。导出ONNX的时候要注意几个点第一输入输出的名字要固定方便后续推理代码对接第二动态轴要设置好如果输入分辨率不固定要把batch和宽高设成动态第三算子版本要选对有些算子在不同ONNX版本里行为不一样建议用opset 11或13。dummy_input torch.randn(1, 3, 512, 512).cuda() torch.onnx.export( model, dummy_input, centernet.onnx, input_names[input], output_names[hm, wh, off], dynamic_axes{input: {0: batch, 2: height, 3: width}}, opset_version11 )导出后一定要用ONNX Runtime跑一遍跟PyTorch的输出对比确保数值一致。我遇到过导出后精度掉了一大截的情况排查发现是某个算子在ONNX里的实现跟PyTorch不一样后来换了opset版本就好了。6.2 量化与剪枝的实操细节量化方面我一般用PyTorch的静态量化。步骤是先准备一批校准数据跑一遍模型收集各层的激活值范围然后插入量化节点把FP32转成INT8最后微调几个epoch恢复精度。model.eval() model.qconfig torch.quantization.get_default_qconfig(fbgemm) model_fused torch.quantization.fuse_modules(model, [[conv, bn, relu]]) model_prepared torch.quantization.prepare(model_fused) # 校准 for images, _ in calib_loader: model_prepared(images) model_quantized torch.quantization.convert(model_prepared)剪枝方面我一般用结构化剪枝按通道的重要性排序剪掉最不重要的30%到50%。剪枝后要微调学习率设小一点1e-5左右训10到20个epoch。注意量化后的模型在CPU上跑得快但在GPU上不一定。如果你的部署环境是GPU建议用TensorRT的INT8量化性能更好。如果是CPUPyTorch的量化就够用了。6.3 实际部署中的性能调优部署到边缘设备上除了模型本身的优化还有几个工程上的点要注意。第一预处理和后处理的时间。很多人只关注模型推理时间忽略了预处理resize、归一化和后处理解码、NMS的时间。实际上后处理在CPU上可能比模型推理还慢。我的做法是把后处理也放到GPU上用CUDA核函数实现能快好几倍。第二内存拷贝。数据在CPU和GPU之间来回拷贝很耗时尽量让整个流程都在GPU上完成。如果必须拷贝用pinned memory能快一些。第三多流并行。如果设备支持可以用CUDA流把预处理、推理、后处理重叠起来提升吞吐量。我那个四路视频流的项目就是用多流并行把帧率从18提到了26。第四批处理。如果延迟允许可以把多帧拼成一个batch一起推理能充分利用GPU的并行能力。但batch太大会增加延迟需要根据场景权衡。6.4 精度与速度的平衡策略最后说一下精度和速度的平衡。在实际项目中没有绝对的最优解只有最适合当前场景的折中。我的经验是先确定速度的硬性要求比如必须达到25帧然后在这个约束下尽量提升精度。具体的调优顺序是先选骨干网络根据算力预算选ResNet-18、ResNet-50还是DLA-34再定输入分辨率512x512是精度和速度的平衡点如果速度不够就降到384x384然后调后处理参数阈值和top-k对速度影响不大但对精度影响明显要仔细调最后考虑量化和剪枝这些是锦上添花前提是模型本身已经调好了。我那个产线项目最终的配置是ResNet-50骨干、输入512x512、下采样4倍、INT8量化、TensorRT推理在入门级推理卡上四路视频流能跑到28帧mAP比FP32只掉了1.2个点完全满足产线要求。这个方案后续还可以往几个方向扩展一是加入关键点检测或者实例分割的头做成多任务模型二是用知识蒸馏用大模型教小模型提升小模型的精度三是尝试更高效的骨干网络比如RepVGG或者EfficientNet看能不能在精度和速度上找到更好的平衡点。这些我都还在实验中有结果了再分享。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑