尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

DiffYOLO:融合扩散模型与YOLO的目标检测去噪方案解析

DiffYOLO:融合扩散模型与YOLO的目标检测去噪方案解析 简介DiffYOLO提出了一种将去噪扩散概率模型DDPM与YOLO目标检测相结合的创新框架旨在解决低质量、含噪声图像中目标检测性能退化的问题。面向目标检测、深度学习与人工智能方向的研究者尤其适合关注模型鲁棒性、迁移学习及扩散模型应用的读者。这份PDF文档共1个文件压缩包大小约829KB为英文原文全文涵盖DiffYOLO的完整技术路线从DDPM的U-Net中提取特征图并融合至YOLO颈部模块在高质量数据集上微调然后在低质量数据集上测试包括实验设置与初步结果分析。通过阅读该论文读者可以理解如何在不重新训练整个网络的前提下利用扩散模型增强特征提升模型的抗噪能力并了解作者后续计划扩展更多数据集与网络架构的方向。已有970人学习/下载适合作为论文精读、课题参考或技术调研的入门资料。1. 为什么 DiffYOLO 要把 YOLO 和扩散模型绑在一起YOLO 在干净数据上的表现已经相当好可一旦图片里出现传感器底噪、夜间增益噪声、JPEG 压缩伪影甚至细粒度对抗扰动mAP 就会明显往下掉。问题往往不在检测头而在卷积特征高频噪声沿着骨干网络逐层放大等到输出层时已经和真实纹理混在一起。DiffYOLO 不是某一家独有结构而是一类方案的统称在 YOLO 的骨干或特征金字塔旁边接一个用扩散模型思路训练的去噪模块先把被噪声打偏的特征拉回正常分布再送入检测头。扩散模型天然适合干这件事因为它的训练过程就是不断加噪再学去掉噪。下面按一个可落地的 DiffYOLO 方案顺序把原理、接法、训练和验证一次讲清楚。2. 扩散模型的去噪能力从哪来加噪、采样和流形2.1 前向过程给特征安排一个可控的信噪比扩散模型的前向过程就是不断往干净数据上叠加高斯噪声直到信噪比接近零。第 t 步的破坏可以用一个封闭形式表示x_t sqrt(alpha_bar_t) * x0 sqrt(1 - alpha_bar_t) * epsilon这个形式意味着不需要逐步迭代只要给出alphas_cumprod就能从干净特征一步采到任意噪声强度的带噪特征。DiffYOLO 里我们把 x0 理解成某层特征图而不是原始图像。import torch def q_sample_feature(x0, t, alphas_cumprod, noiseNone): # x0: 干净特征 [B, C, H, W] # t: 噪声等级索引取值范围 [0, T-1] noise torch.randn_like(x0) if noise is None else noise sqrt_a torch.sqrt(alphas_cumprod[t]).view(-1, 1, 1, 1) sqrt_1ma torch.sqrt(1 - alphas_cumprod[t]).view(-1, 1, 1, 1) x_t sqrt_a * x0 sqrt_1ma * noise return x_t, noisealphas_cumprod由前向调度的 beta 序列累乘得到通常用线性调度或余弦调度。对目标检测来说我不建议把 T 设成 1000 后让噪声任务一路跑到 tT因为检测不关心生成整张图只关心把现场噪声压低。更常见的做法是把 t 归一化到 0~1 的小范围例如 0.1~0.6让去噪分支重视真实系统里会遇到的低中强度噪声。2.2 反向过程去噪网络到底在学习什么DDPM 的训练目标一般写成最小化噪声预测误差L E[ || epsilon_theta(x_t, t) - epsilon ||^2 ]这个目标等价于 denoising score matching也就是让网络学会当前数据分布的分数函数。训练完成后把预测出的噪声从带噪特征里剥掉就得到干净特征的估计。落到目标检测时我一般会把“预测噪声”换成“直接回归干净特征”因为特征图不像自然图像那样高频细节丰富直接回归干净特征在小数据集上更容易收敛。两种参数化的差别如下参数化网络输出适合场景注意点预测噪声 epsilon噪声图图像生成、完整反向采样需要多步采样预测干净特征 x0去噪后特征DiffYOLO 特征净化单步即可部署预测 v 参数速度场扩散蒸馏需要额外处理这轮选择直接决定后面接检测头时要多费多少推理时间。DiffYOLO 不是把 YOLO 换成扩散采样器而是只借用扩散模型的训练方式所以“单步去噪”是性价比最高的落法。如果设备上需要把扩散分支折叠成普通卷积单步回归干净特征也最容易做结构重参数化。2.3 流形视角为什么在特征层做而不是像素层做扩散模型之所以能去噪不是单纯做了低通滤波而是把噪声点拉回数据流形。干净目标特征基本分布在低维流形附近加噪之后相当于把点推到流形外面训练好的去噪分支学会从这种偏移里做投影。这个视角能解释为什么应该在特征层做抗噪声处理直接在像素上扩散去噪容易把树影、雨滴、小目标的边缘一起磨平导致小目标检测反而变差。特征层去噪面对的是语义和纹理的混合噪声位置更集中流形假设也更贴近真实情况。所以在 DiffYOLO 里输入图可以不做任何预处理去噪分支放在 YOLO 骨干网络之后。这样既保住了像素级细节的筛选空间又让去噪过程直接受检测损失监督模型不会只顾着把画面磨干净还会把类别和位置信息一起保留下来。3. DiffYOLO 去噪分支的接法放在 YOLO 的哪一层、怎么放3.1 从 YOLO 的特征金字塔看噪声的传播路径YOLO 通常使用 P3、P4、P5 三层特征分别对应 stride 8、16、32。P3 分辨率最高负责小目标也最容易把传感器噪声当纹理学进去P5 语义强噪声影响相对小但空间细节已经损失。PAN-FPN 在 P3 和 P5 之间来回融合会把 P3 的高频噪声带到深层。因此插一个去噪模块的位置直接决定抗噪声效果接入位置噪声暴露程度优点代价P3 之后最明显能保护小目标纹理计算量最大P4 之后中等平衡细节与语义小目标收益有限P5 之后较低语义更稳定对高频噪声响应慢检测头之前混合统一修正三层特征需要额外内存我的默认做法是在 P3、P4、P5 之后各挂一个轻量去噪分支三个分支不共享权重。原因很简单三个层的通道数、分辨率和噪声敏感度都不一样共享权重会让浅层分支的更新被深层梯度干扰。如果设备实在紧张优先只在 P3 后挂因为 P3 是最先被噪声打穿的层。3.2 一个可迁移的 FeatureDenoiser 模块这个模块不需要做完整 U-Net保留一个残差卷积块加时间嵌入就够了。时间嵌入的作用是告诉模块当前噪声强度使同一个权重能处理 0.1 和 0.6 两种不同噪声等级。import math import torch import torch.nn as nn class TimestepEmbedding(nn.Module): def __init__(self, dim128): super().__init__() self.dim dim self.fc nn.Sequential( nn.Linear(dim, dim * 2), nn.SiLU(), nn.Linear(dim * 2, dim), ) def forward(self, t): half self.dim // 2 freqs torch.exp(-math.log(10000) * torch.arange(half, devicet.device) / half) args t.unsqueeze(-1) * freqs emb torch.cat([torch.cos(args), torch.sin(args)], dim-1) return self.fc(emb) class FeatureDenoiser(nn.Module): def __init__(self, in_ch, embed_dim128): super().__init__() self.time_embed TimestepEmbedding(embed_dim) self.gn1 nn.GroupNorm(min(8, in_ch), in_ch) self.conv1 nn.Conv2d(in_ch, in_ch, 3, padding1, biasFalse) self.act nn.SiLU() self.gn2 nn.GroupNorm(min(8, in_ch), in_ch) self.conv2 nn.Conv2d(in_ch, in_ch, 3, padding1, biasFalse) self.time_proj nn.Linear(embed_dim, in_ch) nn.init.zeros_(self.conv2.weight) nn.init.zeros_(self.conv2.bias) nn.init.zeros_(self.time_proj.weight) nn.init.zeros_(self.time_proj.bias) def forward(self, x, t): te self.time_proj(self.time_embed(t))[:, :, None, None] h self.act(self.gn1(self.conv1(x))) h self.conv2(self.gn2(h)) return x h te这份代码有几个关键点要在初始化时留意。conv2 和时间投影都做了零初始化意味着模块刚挂到预训练 YOLO 上时输出等于输入不会一上来就把原有的特征打乱。训练中期模块只需要学习一个残差修正量检测头的预训练分布不会被结构改变破坏。t 建议在送入模块前归一化到 [-1,1]比如t t * 2 - 1这样正弦时间嵌入的低频和高频段都能被用到。3.3 与 YOLO 头的接口和推理时的噪声等级接入过程很短紧跟在骨干输出后面feats self.backbone(img) # 假设返回 [P3, P4, P5] refined [ denoiser(f, t_expand) for denoiser, f in zip(self.denoisers, feats) ] preds self.head(refined)这里 t_expand 的取值在训练和推理时不一样。训练时按噪声强度随机采样推理时没有标准答案通常按部署场景设一个固定小值例如 t0.2。更稳一点的做法是从特征图的高频残差里估计噪声等级把特征图减掉它的 3×3 平均池化结果再取标准差作为噪声强度。这个值越大说明当前帧受噪声影响越大DiffYOLO 就自动把去噪力度调大。这个分支对图像亮度突变、雨天和镜头上水汽的场景特别有用也不会增加多少延迟因为只需要在每层特征上算一次池化和减法。4. 训练 DiffYOLO 的损失函数与时间步设置让检测损失先稳住4.1 总损失公式与梯度流向DiffYOLO 的总损失由目标检测损失和扩散损失两部分组成可以写成L L_det lambda_diff * L_diffL_det 就是 YOLO 自己的损失函数通常包含 bbox 回归、类别分类和 DFL 分布损失L_diff 是去噪分支和干净特征之间的均方误差。关键是梯度怎么流理想情况下扩散损失应该回传到骨干网络让骨干学习输出更容易被去噪处理的中间特征。但直接放进一个从零开始的分支里很容易在头几个 epoch 把预训练骨干冲乱。def training_step(self, batch): img, labels batch clean_feats self.encoder(img) # 干净图特征 t sample_t(img.shape[0]) # 0.1~0.6 noisy_feats, noise q_sample_feature(clean_feats, t, self.alphas_cumprod) pred_clean self.denoiser(noisy_feats, t) # 直接回归干净特征 diff_loss F.mse_loss(pred_clean, clean_feats.detach()) refined self.denoiser(noisy_feats, t.detach()) det_loss self.det_criterion(self.head(refined), labels) loss det_loss self.lambda_diff * diff_loss return loss这里 clean_feats.detach() 是否保留取决于你愿不愿意让扩散损失的梯度进入骨干。如果骨干是预训练的且数据集不够大我建议前 10 个 epoch 把它 detach 掉等检测损失稳定后再放开 diffusion 分支自己的参数。noise 在方案里其实没有直接参与 loss因为选择了回归干净特征这也是和原版 DDPM 最大的不同。4.2 时间采样范围和分布t 的范围是 DiffYOLO 最值得调的超参数。如果 t 拉到接近 1网络会把特征几乎完全抹掉检测头看到的全是光滑但无结构的语义块小目标率先消失如果 t 只在小范围内现场噪声大了就压不住。常用配置是让 t 服从偏向低噪声的分布def sample_t(batch_size, min_t0.1, max_t0.6, devicecuda): # 均匀采样一个低中噪声范围 return torch.rand(batch_size, devicedevice) * (max_t - min_t) min_t更精细的做法是用 Beta 分布让大部分样本落在 0.2 附近少量样本负责 0.6 的高强度噪声。这样 DiffYOLO 在常见弱噪声环境下收益明显又不会在面对严重噪声时直接失效。4.3 三个容易让训练崩掉的配置配置症状处理方式lambda_diff 太大干净图的 mAP 掉 2 个点以上降到 0.1~0.3先保检测t 范围太宽检测头收到过度平滑特征限制 t 上限在 0.6一上来就全参数训练骨干震荡、损失不降前 5 个 epoch 只训检测头我一般把 lambda_diff 初始化为 0.3然后看干净验证集 mAP 是否下降。若干净 mAP 掉了但噪声 mAP 涨了说明分支在牺牲正常场景换噪声场景这个权衡要用自己数据里的噪声比例去定不能拍脑袋。等上面三张表都稳定后再去动去噪模块里的 embed_dim 和卷积通道数。4.4 数据集里的“干净特征”哪里来训练时需要干净的 x0 作为回归目标。最简单的是用同一张干净图过一遍骨干把输出特征当作目标。但每张图都跑两遍骨干会让训练成本翻倍。我一般用两种近似一是从训练 batch 里随机抽一部分干净图计算目标特征其余图只承担检测损失二是用 EMA 教师网络维护一套移动平均特征让目标更稳定。要注意的是目标的特征如果是实时更新的扩散损失和检测损失之间会出现“追尾巴”的问题所以目标特征建议用更强正则的模型或直接冻结前几个 stage。5. 部署前压测 DiffYOLO用三类噪声和特征偏差检查效果5.1 用 10 行脚本合成三类现场噪声不是所有噪声都是高斯噪声。低光照相机主要出现泊松入射噪声压缩管线会带来块状伪影镜头脏污接近乘性斑点噪声。压测脚本至少覆盖高斯、泊松、斑点三类def add_noise(img, kindgauss, sigma15): if kind gauss: return img torch.randn_like(img) * sigma if kind speckle: return img * (1 torch.randn_like(img) * (sigma / 255) * 2) if kind poisson: level sigma / 255 return torch.poisson(img.clamp(min0) / level) * level验证时不要只报一个总 mAP要分别记录mAP_clean和mAP_gauss15、mAP_speckle25。DiffYOLO 的目标是噪声图涨点同时干净图不掉点。5.2 用固定噪声等级和特征偏差定位问题验证集上把 t 固定成 0.2然后对比干净特征和去噪后特征的偏差比。这个比率比 mAP 更早暴露分支是否在“真的去噪”with torch.no_grad(): n encoder(noisy_img) c encoder(clean_img) r denoiser(n, t_const) ratio (r - c).abs().mean() / (n - c).abs().mean()ratio 越小说明分支把噪声造成的特征偏移压得越狠。如果 ratio 没有降到 0.3 以下优先检查时间嵌入是否用了 [-1,1] 归一化再检查 lambda_diff 是否被检测损失压住。5.3 部署前的三个阈值指标参考阈值未达标时的动作clean mAP 下降 1.5 个点降低 lambda_diffnoisy mAP 提升 3 个点扩大 t 范围或加噪声数据增强特征偏差比 0.3调大分支容量或检查 t 采样分布把 ratio 压到 0.3 以下之后再回头验证集上做一次完整评测此时 DiffYOLO 的噪声收益通常就很稳了。本文还有配套的精品资源点击获取
返回列表