1. 从零训练的目标检测:DSOD到底想解决什么问题
目标检测这个领域,过去这些年基本被一条铁律统治着:你得先有个强力的预训练骨干网络,最好是在大规模分类数据集上跑过的,然后在此基础上做微调。不管是Faster R-CNN系列、SSD还是YOLO的早期版本,背后都站着一个在ImageNet上刷过榜的骨干。这套范式几乎成了默认操作,没人觉得有什么问题——直到有人问了一句:如果我就是没有预训练权重呢?
DSOD(Deeply Supervised Object Detector)就是冲着这个问题去的。它的核心主张非常直接:从零开始训练一个目标检测器,不依赖任何预训练模型,性能还能打。这件事在当年听起来有点反直觉,因为大家普遍认为检测任务的数据量远小于分类任务,没有预训练就等于让一个婴儿直接学跑步。但DSOD用一套精心设计的深层监督机制和密集连接结构,把这个假设给推翻了。
我第一次接触这个工作的时候,正在做一个边缘设备上的检测项目。场景很尴尬:模型要部署在算力有限的嵌入式板子上,而可用的训练数据只有几千张标注图,类别也就那么几类。按常规做法,我得先找一个在公开分类数据集上预训练过的轻量骨干,再迁移过来微调。但问题在于,预训练权重的输入分辨率、通道配置往往和我的实际需求对不上,迁移过来还得改结构,改完又得重新调。更麻烦的是,有些预训练权重本身带着一堆我根本用不上的类别信息,反而在微调时引入了噪声。
DSOD的思路让我眼前一亮:既然预训练和微调之间存在这么多结构上的妥协,那为什么不干脆设计一个天生就适合从零训练的检测架构?它不需要你去迁就任何现成的骨干,所有参数都从随机初始化开始,通过深层监督和密集连接让梯度流动得更顺畅,从而在数据量有限的情况下也能收敛到一个可用的检测器。
这篇文章我会从设计思路、核心结构、实操细节、常见坑几个角度,把DSOD这套东西拆开讲清楚。适合谁看?如果你正在做小样本检测、边缘端部署,或者单纯对“不依赖预训练”这个方向感兴趣,那这篇内容应该能给你一些可以直接抄作业的东西。我会尽量把每个设计选择背后的“为什么”讲透,而不是只丢一堆结构图让你自己悟。
2. 核心设计思路拆解:为什么敢不用预训练
2.1 预训练到底帮了什么忙
要理解DSOD为什么敢去掉预训练,得先搞清楚预训练在检测任务里到底起了什么作用。很多人以为预训练主要是提供“特征提取能力”,这个说法对,但不完整。预训练真正帮的忙,其实有三层。
第一层是初始化质量。随机初始化的卷积核在训练初期输出的特征图基本是噪声,梯度方向混乱,收敛慢且容易卡在糟糕的局部最优。预训练权重相当于给网络一个“合理的起点”,让它在训练一开始就能提取到边缘、纹理这类低级特征。
第二层是正则化效应。在大规模分类数据集上训练过的权重,本身已经编码了大量通用视觉模式。当你用少量检测数据微调时,这些权重相当于一个强先验,抑制了模型在小数据上的过拟合倾向。
第三层是梯度流动的稳定性。深层网络从零训练时,梯度在反向传播过程中容易消失或爆炸,尤其是检测网络这种多任务、多尺度的结构。预训练权重让浅层网络已经处于一个“工作状态”,反向传播时梯度不会太离谱。
DSOD要做的,就是在不依赖预训练的前提下,把这三层作用用其他机制补回来。它的答案分别是:用密集连接改善梯度流动和特征复用,用深层监督给浅层提供直接的梯度信号,用精心设计的结构让从零训练也能稳定收敛。
2.2 密集连接为什么适合从零训练
DSOD的骨干部分借鉴了DenseNet的思想,但做了针对检测任务的改造。DenseNet的核心是密集块:每一层的输入都来自前面所有层的输出拼接。这种结构有两个直接好处。
第一个好处是梯度高速公路。在传统链式结构中,梯度从损失函数反向传播到浅层要经过几十甚至上百层,每经过一层都可能衰减。密集连接相当于在任意两层之间都修了直达通道,浅层参数能直接收到来自检测损失的梯度信号。这对于从零训练至关重要,因为浅层卷积核在随机初始化状态下,如果没有强梯度信号,很难学到有意义的东西。
第二个好处是特征复用。检测任务需要同时处理大目标和小目标,浅层的高分辨率特征对定位小目标很关键,深层的语义特征对分类很关键。密集连接让每一层都能访问到前面所有层的特征,相当于把不同抽象层次的信息都保留下来,而不是像传统网络那样越深越抽象、浅层信息被逐渐丢弃。
但DSOD并不是照搬DenseNet。它做了一个关键改动:在密集块之间插入过渡层时,压缩率调得更保守。DenseNet为了控制参数量,过渡层会把通道数压缩得很厉害,但检测任务对特征丰富度要求更高,压缩太狠会丢失小目标信息。DSOD在这一点上做了平衡,具体参数我在后面实操部分会展开。
2.3 深层监督是怎么补回预训练效果的
深层监督是DSOD另一个核心机制。传统检测网络只在最后几个检测头输出损失,浅层网络只能通过反向传播间接感受到检测目标。DSOD的做法是在网络的多个中间层直接接出辅助检测分支,让这些中间层也直接参与损失计算。
这个设计背后的逻辑很直白:浅层网络如果只靠反向传播的间接信号,在从零训练时很难知道自己该学什么。但如果我在中间层就接一个检测头,告诉它“你这一层的特征也要能预测出目标框和类别”,那这一层的参数就会收到直接的、强烈的梯度信号,学习效率大幅提升。
实际操作中,辅助分支的损失权重需要仔细调。权重太高,浅层会过度关注检测任务而忽略基础特征提取;权重太低,又起不到监督作用。DSOD论文里给出的经验值是辅助损失权重在0.1到0.3之间,具体取决于你的数据集规模和类别数。我在自己的项目里试过0.2这个值,在几千张图的规模下表现比较稳。
还有一个细节:辅助分支不是随便接的。DSOD选择在每个密集块的输出后接辅助检测头,因为这些位置的特征已经经过了一定程度的抽象,既有语义信息又有空间分辨率,适合做检测。如果你接得太浅,特征太低级,检测头学不到东西;接得太深,又和主检测头重复了。
2.4 和SSD的继承关系与关键差异
DSOD的整体框架是基于SSD的,这一点从它的多尺度检测头设计就能看出来。SSD在多个不同分辨率的特征图上分别预测目标框,大特征图负责小目标,小特征图负责大目标。DSOD继承了这个多尺度思路,但在骨干网络和训练策略上做了根本性改动。
关键差异有三个。第一,SSD用VGG或ResNet做骨干,这些骨干通常是在分类数据集上预训练过的;DSOD用密集连接结构从头搭骨干,不依赖任何预训练。第二,SSD只在最后几个特征层接检测头;DSOD在中间层也接辅助检测头,形成深层监督。第三,SSD的训练策略假设你有预训练权重,学习率、权重衰减等超参都是围绕微调场景调的;DSOD的超参配置是针对从零训练优化的,比如初始学习率通常设得更大,warmup阶段更长。
这些差异导致两者的使用场景完全不同。如果你手头有充足的标注数据和预训练权重,SSD系列可能更省事;但如果你数据有限、部署环境对模型结构有特殊要求,或者你就是不想被预训练权重的许可证和结构限制绑住,DSOD这套思路就值得认真考虑。
3. 核心结构细节与实操要点
3.1 密集块的具体配置与参数选择
DSOD的骨干由多个密集块串联而成,每个密集块内部有若干层,每层的输出通道数固定(这个值叫增长率)。假设增长率设为k,那么第l层的输入通道数就是k0 + (l-1)×k,其中k0是初始通道数。这个线性增长的通道数意味着越深的层输入越宽,计算量也越大。
在实际配置中,增长率k的选择很关键。k太小,特征表达能力不足,检测精度上不去;k太大,参数量和计算量爆炸,边缘设备根本跑不动。DSOD论文里用的k值在32到48之间,具体取决于你的算力预算。我在一个嵌入式项目里把k压到了24,配合输入分辨率降到320×320,在目标类别不多的情况下精度损失可以接受,但推理速度提升明显。
每个密集块之后有一个过渡层,负责降采样和通道压缩。过渡层通常是一个1×1卷积加一个2×2平均池化。1×1卷积的压缩率(输出通道数除以输入通道数)在DSOD里一般设0.5到0.75。这里有个坑:如果你压缩率设得太低(比如0.3),小目标的特征会被压没,召回率明显下降。我试过0.4的压缩率,在小目标占比高的数据集上,mAP掉了将近5个点。
注意:密集块的增长率k和过渡层压缩率是一对需要联合调参的量。我的经验是先把k定在32,压缩率定在0.6,跑一轮看小目标召回率,如果召回率低于预期就提高压缩率或增大k,如果速度不达标就反向操作。
3.2 辅助检测头的接入位置与损失权重
辅助检测头的接入位置直接决定了深层监督的效果。DSOD的做法是在每个密集块的输出特征图上接一个辅助检测分支,这个分支的结构和主检测头类似,但通常更轻量——比如少几个卷积层,或者通道数减半。
为什么不在每个过渡层之后都接?因为过渡层做了降采样,特征图分辨率已经变了,接检测头需要额外的上采样操作,增加复杂度。而在密集块输出处接,特征图分辨率还保持在该块对应的尺度上,直接复用主检测头的锚框配置就行。
损失权重的调整我踩过几次坑。一开始我设了0.5,结果浅层网络过度关注检测损失,基础特征提取能力没学好,最终mAP反而比不加辅助分支还低。后来降到0.2,效果明显改善。再后来我试了动态权重:训练初期辅助损失权重大一些(0.3),让浅层快速学到东西;训练后期降到0.1,让主检测头主导优化。这个策略在小数据集上特别有效,收敛速度比固定权重快了不少。
还有一个细节:辅助检测头的损失函数和主检测头保持一致,都是分类损失加定位损失的加权和。但辅助分支的定位损失权重可以适当降低,因为浅层特征的空间精度本身就不如深层,强行要求它精确定位反而会引入噪声。
3.3 从零训练的超参配置策略
从零训练和微调的超参配置完全是两码事。微调时学习率通常设得很小(1e-4到1e-5),因为预训练权重已经很好,只需要微调。但从零训练时,学习率设太小会导致收敛极慢甚至卡住,设太大又会震荡发散。
DSOD的推荐配置是:初始学习率0.1(批量大小32时),用余弦退火或步进衰减,warmup阶段设为500到1000次迭代。warmup的作用是在训练初期用很小的学习率让网络先“热起来”,避免随机初始化权重被大学习率直接打飞。我实测下来,warmup少于500次迭代时,训练损失在前几百步经常出现尖峰,甚至偶尔NaN。
权重衰减方面,从零训练通常需要比微调更强的正则化。DSOD用的权重衰减系数在5e-4左右,比SSD微调时的1e-4要大。批量归一化的动量参数也建议调低一些(0.9而不是0.99),因为从零训练时统计量的估计还不稳定,动量太大会让统计量更新滞后。
还有一个容易被忽略的点:数据增强的强度。从零训练时,模型对数据分布的依赖更强,数据增强相当于在扩充有效样本量。但增强太猛会让模型难以收敛,尤其是颜色抖动和随机裁剪同时上时。我的做法是训练前中期用较强的增强(随机裁剪、颜色抖动、水平翻转),后期最后几十个epoch关掉颜色抖动,只保留翻转和轻微缩放,让模型在接近真实分布的数据上收尾。
3.4 输入分辨率与锚框设置的配合
DSOD继承SSD的多尺度检测框架,输入分辨率直接决定了各层特征图的尺寸,进而影响锚框的尺度和数量。常见的输入分辨率有300×300、320×320、512×512几档。分辨率越高,小目标检测能力越强,但计算量也越大。
锚框的设置需要和你的数据集目标尺度分布匹配。SSD默认的锚框尺度是从0.2到0.9线性分布,宽高比有1、2、3、1/2、1/3几种。但如果你的数据集里目标普遍偏小,默认配置下小锚框数量不够,召回率会很低。我的做法是先统计训练集里所有目标框的相对面积,画出分布直方图,然后根据分布调整锚框的最小尺度和各尺度的锚框数量。
具体操作上,如果你用现成的检测框架,锚框配置通常在配置文件里以列表形式给出。比如最小尺度设0.1而不是0.2,最大尺度设0.8而不是0.9,中间多插一档。宽高比也可以根据实际目标形状调整,比如你的目标大多是细长条,那就多加几个极端宽高比。
提示:调整锚框后一定要重新计算正负样本匹配阈值。SSD默认IoU大于0.5为正样本,小于0.3为负样本。锚框尺度变了之后,这个阈值可能需要微调,否则正样本数量会剧烈变化,影响训练稳定性。
4. 完整实操流程:从数据准备到模型导出
4.1 数据准备与标注格式转换
假设你手头有一批标注数据,格式可能是VOC XML、COCO JSON或者YOLO txt。DSOD的实现通常基于某个检测框架,你需要先把数据转成框架要求的格式。以常见的COCO格式为例,每张图的标注包括图像文件名、宽高、以及若干个目标框的类别和坐标。
数据准备阶段有几个容易出问题的地方。第一是类别不平衡。如果你的数据集里某些类别样本极少,从零训练时这些类别几乎学不到。我的做法是对稀有类别做过采样,或者在损失函数里给稀有类别更高的分类权重。第二是标注质量。从零训练对标注噪声更敏感,因为模型没有预训练先验来“纠正”错误标注。如果标注框普遍偏大或偏小,模型会学到这个偏差。建议在训练前随机抽几十张图可视化检查一遍。
第三是训练集和验证集的划分。从零训练时,验证集的作用比微调时更重要,因为你需要靠它来判断模型是否过拟合或欠拟合。划分时要注意类别分布均衡,不能出现某个类别只在训练集有、验证集没有的情况。我通常按8:2划分,如果数据量少于5000张,会用交叉验证来更充分地利用数据。
4.2 网络搭建与配置文件修改
如果你用现成的DSOD实现,网络结构通常已经定义好了,你只需要改配置文件。配置文件里需要关注几个关键参数:增长率k、过渡层压缩率、辅助检测头的接入位置和损失权重、输入分辨率、锚框配置。
如果你要自己搭网络,核心是三个模块:密集块、过渡层、检测头。密集块里每一层的实现要注意通道拼接的顺序,通常是按通道维度拼接前面所有层的输出。过渡层里的1×1卷积用来压缩通道,2×2平均池化用来降采样。检测头部分和SSD一致,每个特征图位置预测若干个锚框的偏移量和类别概率。
代码层面,密集块的前向传播可以这样写(以PyTorch风格为例):
class DenseBlock(nn.Module): def __init__(self, in_channels, growth_rate, num_layers): super().__init__() self.layers = nn.ModuleList() for i in range(num_layers): self.layers.append(self._make_layer(in_channels + i * growth_rate, growth_rate)) def _make_layer(self, in_ch, growth_rate): return nn.Sequential( nn.BatchNorm2d(in_ch), nn.ReLU(inplace=True), nn.Conv2d(in_ch, growth_rate, kernel_size=3, padding=1, bias=False) ) def forward(self, x): features = [x] for layer in self.layers: out = layer(torch.cat(features, dim=1)) features.append(out) return torch.cat(features, dim=1)这段代码的关键点是每次拼接后通道数增加growth_rate,下一层的输入通道数相应变大。实际使用时要注意显存占用,因为拼接后的特征图通道数增长很快,如果k设得大、层数又多,显存很容易爆。
4.3 训练过程监控与调参节奏
从零训练DSOD时,训练过程的监控比微调更重要。我通常关注三个指标:总损失、分类损失、定位损失。总损失应该是稳定下降的,如果出现剧烈震荡或上升,大概率是学习率太大或数据有问题。
分类损失和定位损失的比例也值得关注。如果分类损失远大于定位损失,说明模型在类别区分上遇到困难,可能需要增加分类头的容量或调整分类损失的权重。反之如果定位损失主导,可能是锚框配置不合理,正样本匹配质量差。
训练节奏上,我一般分三个阶段。第一阶段是warmup,学习率从0线性升到初始值,持续500到1000次迭代。第二阶段是主训练,学习率按余弦或步进衰减,持续到总迭代数的80%左右。第三阶段是收尾,学习率降到初始值的1%以下,关掉强数据增强,让模型在接近真实分布的数据上微调几十个epoch。
每个阶段结束后我都会跑一次验证集,记录mAP和小目标召回率。如果第二阶段结束时mAP还很低(比如低于0.2),说明网络结构或超参有根本问题,继续训下去也是浪费时间,不如停下来检查。
4.4 模型导出与推理部署
训练完成后,模型导出成推理格式是最后一步。如果你用PyTorch训练,可以导出ONNX格式,再用推理引擎加载。导出时要注意几个点:输入尺寸要固定,动态尺寸虽然支持但某些推理引擎对动态shape的优化不好;后处理(非极大值抑制)通常不包含在ONNX图里,需要在推理代码里单独实现。
部署到边缘设备时,量化是常用的加速手段。从零训练的DSOD模型对量化的敏感度比预训练模型高,因为权重分布可能更分散。建议用量化感知训练而不是训练后量化,在训练最后阶段插入伪量化节点,让模型适应量化误差。我实测下来,量化感知训练能把int8量化的精度损失控制在1个mAP点以内,而直接训练后量化可能掉3到5个点。
推理速度方面,DSOD的密集连接结构在GPU上并行度不错,但在某些边缘芯片上,密集连接带来的通道拼接操作可能成为瓶颈。如果遇到这种情况,可以考虑在导出前把密集块里的拼接操作融合成普通卷积,牺牲一点精度换速度。
5. 常见问题与排查技巧实录
5.1 训练不收敛或损失震荡
这是从零训练最常见的问题。表现是训练损失在前几百步剧烈震荡,或者干脆不下降。原因通常有三个:学习率太大、warmup不够、数据归一化有问题。
排查顺序:先把学习率降到原来的十分之一跑几百步,如果损失开始稳定下降,说明学习率是主因。如果降了学习率还是震荡,检查warmup是否足够,把warmup迭代数加倍试试。如果还不行,检查输入数据的归一化,均值方差是否计算正确,有没有异常值。
我遇到过一次损失震荡,排查了半天发现是数据加载时某些图像的像素值范围不对(有的在0-255,有的在0-1),导致输入分布混乱。统一归一化后问题消失。这个坑很隐蔽,因为数据加载代码看起来没问题,但实际数据里混了不同来源的图。
5.2 小目标检测效果差
DSOD在小目标上的表现取决于浅层特征的质量。如果小目标召回率低,先检查辅助检测头是否正常工作。辅助分支的损失如果一直很高,说明浅层特征没学好,可能需要增大辅助损失权重或增加辅助分支的容量。
另一个常见原因是锚框配置不匹配。统计一下你的数据集里小目标的相对面积,如果大部分小目标面积小于0.05,而你的最小锚框尺度是0.2,那这些小目标根本匹配不到正样本。把最小锚框尺度降到0.05到0.1之间,同时增加小尺度锚框的数量。
还有一个容易忽略的点是输入分辨率。如果输入分辨率太低(比如224×224),小目标在特征图上只有几个像素,再强的网络也无力回天。适当提高输入分辨率是最直接的改善手段,但要注意计算量会平方级增长。
5.3 过拟合与欠拟合的判断与处理
从零训练时,过拟合和欠拟合的界限比微调时更模糊。判断标准是看训练损失和验证损失的差距。如果训练损失持续下降但验证损失在某个点后开始上升,就是过拟合。如果两者都居高不下,就是欠拟合。
过拟合的处理手段:增强数据增强、增大权重衰减、减少网络容量(降低增长率k或减少密集块层数)、增加dropout。欠拟合的处理手段:增大网络容量、降低权重衰减、延长训练时间、检查数据标注质量。
我个人的经验是,从零训练DSOD时,欠拟合比过拟合更常见,尤其是在数据量不大的情况下。很多人一看到验证损失高就以为是过拟合,急着加正则化,结果越加越差。实际上可能是模型容量不够,或者训练时间不足。先确认训练损失是否已经降到足够低,如果训练损失本身就不低,那问题在欠拟合这边。
5.4 推理速度不达标的优化方向
推理速度不达标时,优化方向按性价比排序:降低输入分辨率、减少增长率k、减少密集块层数、剪枝、量化。降低输入分辨率最直接,但精度损失也最明显。减少k和层数需要重新训练,但精度和速度的平衡可以调得更精细。
剪枝方面,密集连接结构对剪枝的容忍度比普通卷积网络高,因为特征复用让某些通道即使被剪掉,信息也能从其他通道传过来。我试过对DSOD做通道剪枝,剪掉30%的通道后精度只掉了1个多点,速度提升约20%。
量化前面提过了,量化感知训练是首选。如果推理引擎支持混合精度,也可以试试FP16推理,速度提升明显且精度损失很小。
| 问题现象 | 可能原因 | 排查手段 | 解决方案 |
|---|---|---|---|
| 损失震荡不收敛 | 学习率过大 | 降学习率跑几百步 | 调小初始学习率,延长warmup |
| 小目标召回低 | 锚框不匹配 | 统计目标面积分布 | 调整锚框尺度和数量 |
| 验证损失上升 | 过拟合 | 对比训练/验证损失 | 增强正则化,减少容量 |
| 训练损失居高不下 | 欠拟合 | 检查训练损失曲线 | 增大容量,延长训练 |
| 推理速度慢 | 分辨率过高 | 测各阶段耗时 | 降分辨率,剪枝,量化 |
6. 一些实操后的个人体会
DSOD这套东西我前后在三个项目里用过,场景分别是小样本工业质检、嵌入式安防和移动端商品识别。最大的感受是:从零训练不是万能药,但在特定约束下确实能打开一些之前被堵死的路。
工业质检那个项目,数据只有两千多张,类别就三类,但目标尺度差异极大,最小的缺陷只有十几个像素。用预训练模型微调时,预训练权重的输入分辨率是224,迁移到高分辨率检测时浅层特征完全对不上,微调效果很差。换成DSOD从零训练,配合320×320输入和调整后的锚框,小目标召回率从0.4出头提到了0.7以上。代价是训练时间长了三倍,但最终模型更贴合实际数据分布。
嵌入式安防那个项目,算力限制很死,模型参数量必须控制在2M以内。预训练骨干动辄十几M参数,剪枝量化后还是超标。DSOD用k=24、四个密集块、输入256×256,参数量压到了1.8M,精度在可接受范围内。这个项目让我意识到,DSOD的密集连接结构在参数量效率上确实有优势,因为特征复用减少了冗余参数。
移动端商品识别那个项目最后没用DSOD,因为数据量够大(十万张以上),预训练模型微调的效果更好且训练更快。这也说明DSOD的适用边界:数据量小、结构约束强、预训练权重不匹配时,它优势明显;数据充足、算力充裕时,预训练微调仍然是更省事的选择。
最后分享一个小技巧:如果你打算尝试DSOD但从零搭网络太麻烦,可以找一个SSD的实现,把骨干部分替换成密集连接结构,然后在中间层接辅助检测头。这样改动量最小,也最容易对比效果。辅助损失权重从0.2开始调,warmup至少1000步,学习率0.1起步,这三个参数定好了,剩下的就是耐心等它收敛。