
简介面向计算机视觉与深度学习研究者的扩散模型应用资料聚焦目标计数问题中的数据增强挑战。资源复现《Diffusion-based Data Augmentation for Object Counting Problems》核心流程涵盖环境配置、预训练Stable Diffusion与ControlNet加载、高斯密度图生成、条件损失与计数损失设计、训练优化及合成人群图像生成并给出可运行的Python/PyTorch代码与逐段解释适合希望掌握扩散模型落地细节、改进小样本计数模型的研究人员和开发者。压缩包仅1个docx文档大小约24KB以代码示例和原理说明为主便于快速查阅关键实现。已有63人学习。通过这份资料读者可理解如何用ControlNet条件生成高逼真图像扩充训练集学会密度图与文本条件编码、损失函数构建及模型微调等关键技术为同类图像生成或目标检测项目提供可直接借鉴的解决思路。1. 扩散模型做数据增强为什么目标计数先吃到红利目标计数的标注成本在所有视觉任务里几乎是最高的一档一张包含 200 个行人的街景图标点加质检要花十分钟而分类任务标注一张图只需要两秒。标注贵直接限制了跨场景泛化——A 场景标了 5000 张图换到 B 场景性能立刻下滑。常规数据增强翻转、裁剪、色彩抖动只改变外观不改变目标空间排列和遮挡关系而计数模型最敏感的恰恰是“谁挡着谁、密度梯度长什么样”。扩散模型提供了不一样的数据增强路径给定一张密度图用条件扩散模型生成一张和它严格对应的场景图像目标位置、重叠程度、局部密度都随条件走生成数据和监督标签天然对齐不需要像 GAN 那样担心生成之后标注失效。这篇内容从选型原理讲到可运行代码和混合训练管线最后给出生成样本的质检方法适合正在做计数类项目落地的工程师也适合想评估“扩散模型增强到底值不值得上”的团队。2. 数据增强方法演进的取舍从 GAN 到扩散模型流形视角怎么说2.1 为什么传统数据增强解决不了目标计数的分布偏移传统增强方法谱系分两类第一类是几何与光度变换包括翻转、缩放、旋转、高斯噪声、颜色抖动第二类是样本混合包括 Mixup、CutMix、CutOut。几何变换改变目标的位置和尺度但目标排列的内在结构仍是原图的派生副本混合类增强制造了新的局部上下文却不产生具有语义合理性的遮挡或新增目标。对于目标计数模型这两类操作都只是对原分布的插值没有把分布推向未被标注但真实存在的高密度、强遮挡区域。从分布角度看传统增强的提升范数很小。计数模型在测试集遇到 3 人/m² 的极端密集区域时如果训练阶段没见过由 5 人/m² 合成的图像预测密度图就会整体偏低。数据增强的真正目标是扩展训练分布的支撑集而不是在同一条流形路径上反复摇摆。2.2 扩散模型原理前向加噪、反向去噪与条件控制扩散模型DDPM的出发点很简单前向过程把真实图像 x0 逐步加噪成纯高斯噪声时间步 t 越大噪声占比越高反向过程学习一个神经网络根据当前带噪图像 xt 和条件 c 预测 t 时刻加入的噪声然后沿时间轴逐级去噪。DDPM 的简化训练损失可以写成L E_{t,x0,ε} || ε - ε_θ(xt, t, c) ||²训练时对神经网络唯一的要求是“能预测噪声”。当采样结束纯噪声就被变换成了符合条件 c 的图像。这个框架的好处在于训练目标单一、稳定不涉及 GAN 判别器和生成器的对抗平衡条件 c 只需要在训练时与被噪声损坏的图像一起输入网络推理时不加噪声就能控制生成方向。噪声调度的定义是扩散模型原理里的第一个参数后续训练循环会直接用到betas torch.linspace(1e-4, 0.02, 1000) alphas 1.0 - betas alpha_bar torch.cumprod(alphas, dim0)betas控制加噪速度线性地从 1e-4 增加到 0.02 是稳定首选alpha_bar是每个时间步的累计保留比例用它可以在任意 t 时刻一步算出加噪后的图像不用逐帧推演。2.3 扩散模型和流形假设为什么从 GAN 换成扩散模型扩散模型的生成过程在高维图像空间里可以理解为“先远离真实数据流形再把样本投影回流形”。前向加噪让数据点逐步扩散到流形周围的各向同性高斯区域反向去噪的每一步学习的是分数函数本质上是流形附近对数密度的梯度。这样一来采样结果通常停留在数据流形邻近区域内不太会出现 GAN 常见的模式坍塌或生成纹理不连贯问题。增强方法是否改变目标语义标注能否自动获得训练稳定性可控性翻转/裁剪/颜色抖动否是高中Mixup/CutOut弱部分高低GAN是否低中扩散模型是是高高在目标计数场景中密度图条件把采样空间进一步压缩到一个子流形所有可能的图像都必须与给定的人群或车辆空间分布兼容。这也解决了数据增强的核心问题——生成样本的标注真实性。用密度图 c 生成图像 x训练计数模型的监督信号从 c 直接转换得到不需要人工二次标注。GAN 做不到这一点因为条件注入对抗过程后生成分布未必严格尊重细粒度空间条件。3. 条件扩散模型的目标计数数据增强实现可跑代码与关键参数3.1 整体生成管线从随机目标位置到“图像密度图”样本对常见做法是把合成数据构建分成三步。第一步在任一背景图上随机放置目标位置人群用行人贴图车辆用车辆贴图或者直接用 3D 引擎渲染场景第二步把目标位置转换成密度图通常以每个目标点为中心的二维高斯核累加第三步用密度图作为条件训练条件扩散模型生成更真实的图像。为什么不直接使用贴图后的合成图像因为贴图的拼接痕迹和光照不一致会让计数模型学习到背景伪影而不是目标语义。条件扩散模型的作用是“把密度图变成一张视觉上可信的图片”让生成样本的纹理分布更接近真实数据域同时保留密度图确定的语义结构。3.2 时间步嵌入与条件注入编写一个可运行的 CondUNet这里给出一个可在单卡上运行的最小实现。它不是一个生产级大模型但结构完整读者可以直接替换骨干网络。import torch import torch.nn as nn import math class TimeEmbedding(nn.Module): 将归一化时间步 t_norm0~1编码为高维向量并映射到 FiLM 参数。 def __init__(self, dim): super().__init__() self.half_dim dim // 2 self.fc nn.Sequential( nn.Linear(self.half_dim, dim), nn.SiLU(), nn.Linear(dim, dim) ) def forward(self, t_norm): freqs torch.exp(-math.log(10000) * torch.arange(self.half_dim, dtypetorch.float32) / self.half_dim).to(t_norm.device) args t_norm.unsqueeze(-1) * freqs.unsqueeze(0) # (B, half_dim) emb torch.cat([torch.sin(args), torch.cos(args)], dim-1) return self.fc(emb) class CondUNet(nn.Module): 小型 U-Net输入带噪图像 x_t、密度图条件 cond、时间步 t_norm。 def __init__(self, in_ch3, cond_ch1, base64): super().__init__() self.t_emb TimeEmbedding(dimbase * 4) self.cond_proj nn.Sequential( nn.Conv2d(cond_ch, base // 2, 3, padding1), nn.SiLU() ) self.enc1 nn.Sequential( nn.Conv2d(in_ch base // 2, base, 3, padding1), nn.SiLU(), nn.Conv2d(base, base, 3, padding1), nn.SiLU() ) self.down nn.Conv2d(base, base * 2, 4, stride2, padding1) self.enc2 nn.Sequential( nn.Conv2d(base * 2, base * 2, 3, padding1), nn.SiLU(), nn.Conv2d(base * 2, base * 2, 3, padding1), nn.SiLU() ) self.up nn.ConvTranspose2d(base * 2, base, 4, stride2, padding1) self.dec nn.Sequential( nn.Conv2d(base * 2, base, 3, padding1), nn.SiLU(), nn.Conv2d(base, in_ch, 3, padding1) ) def forward(self, x, t_norm, cond): t self.t_emb(t_norm) # (B, base*4) c self.cond_proj(cond) # (B, base/2, H, W) h1 self.enc1(torch.cat([x, c], dim1)) # (B, base, H, W) h2 self.enc2(self.down(h1)) # (B, base*2, H/2, W/2) scale, shift t.chunk(2, dim1) # 各 (B, base*2)FiLM 注入 h2 h2 * scale.unsqueeze(-1).unsqueeze(-1) shift.unsqueeze(-1).unsqueeze(-1) hup self.up(h2) return self.dec(torch.cat([hup, h1], dim1))代码说明TimeEmbedding把时间步做正弦位置编码再经过两层 MLP 得到 scale/shift 向量。扩散模型要求网络精确感知“当前噪声有多强”时间步编码不够细采样时不同去噪阶段会互相混淆。密度图条件通过cond_proj变成和输入图像同分辨率的特征图直接在编码器入口与图像通道拼接。目标计数对空间位置极度敏感这种通道拼接比后置交叉注意力更容易训练。enc1 / down / enc2 / up / dec是两层 U-Net 的最简骨架。base64 时参数量约千万级可以在 8GB 显存上训练 256×256 输入。需要更高分辨率时把down和up之间再堆 12 层即可。时间步注入采用 FiLM对 h2 每个通道做逐元素缩放和偏移。注意t_emb输出维度是 base4chunk成两部分后每部分 base2恰好匹配 h2 的通道数。3.3 训练循环与 DDIM 采样噪声调度和 CFG 参数扩散模型做数据增强的训练代码核心并不长关键是几个参数的配合。import random import torch import torch.nn.functional as F T 1000 betas torch.linspace(1e-4, 0.02, T) alphas 1.0 - betas alpha_bar torch.cumprod(alphas, dim0) sqrt_alpha_bar torch.sqrt(alpha_bar) sqrt_one_minus_alpha_bar torch.sqrt(1 - alpha_bar) drop_prob 0.1 # 条件丢弃概率用于 classifier-free guidance def train_step(model, optimizer, x, cond): x: (B,3,H,W) 归一化到 [-1,1]cond: (B,1,H,W) 密度图。 B x.size(0) t torch.randint(0, T, (B,), devicex.device) noise torch.randn_like(x) x_noisy (sqrt_alpha_bar[t].view(-1,1,1,1) * x sqrt_one_minus_alpha_bar[t].view(-1,1,1,1) * noise) if random.random() drop_prob: cond torch.zeros_like(cond) pred_noise model(x_noisy, t.float() / T, cond) loss F.mse_loss(pred_noise, noise) optimizer.zero_grad() loss.backward() optimizer.step() return loss.item() torch.no_grad() def sample_ddim(model, cond, n_steps50, cfg_scale3.0, img_size256): 用 DDIM 从条件密度图生成图像eta0 时采样是确定性的。 model.eval() cond cond.to(next(model.parameters()).device) x torch.randn(1, 3, img_size, img_size, devicecond.device) timesteps torch.linspace(T - 1, 0, n_steps).long() for i in range(n_steps): t timesteps[i] t_norm (t.float() / T).to(cond.device) eps_uncond model(x, t_norm, torch.zeros_like(cond)) eps_cond model(x, t_norm, cond) eps eps_uncond cfg_scale * (eps_cond - eps_uncond) a_bar_t alpha_bar[t].to(cond.device) x0 (x - torch.sqrt(1 - a_bar_t) * eps) / torch.sqrt(a_bar_t) if i n_steps - 1: a_bar_next alpha_bar[timesteps[i 1]].to(cond.device) else: a_bar_next torch.ones_like(a_bar_t) x torch.sqrt(a_bar_next) * x0 torch.sqrt(1 - a_bar_next) * eps return x训练循环说明t在 0 到 T-1 之间随机抽样模型每个 batch 都会见到不同噪声强度。batch 内图像越多噪声强度覆盖越全收敛越快所以 batch size 通常开到显存允许的上限。drop_prob0.1是 classifier-free guidance 的标准做法训练时以 10% 概率把条件置零采样时分别算有条件和无条件预测再外推。目标计数数据增强里CFG 系数建议从 1.5 起步太高会让生成图像只保留密度结构、丢掉背景多样性。DDIM 采样 50 步即可比 DDPM 需要的 1000 步少很多。x0是网络预测的干净图像下一时间步由x0和噪声方向共同插值得到eta0 时确定性采样同一张密度图总是生成同一张图做可控性测试很方便。参数建议值说明训练分辨率256×2568GB 显存可训练先跑通再放大噪声调度linearβ∈[1e-4, 0.02]稳定首选不需要一开始就上 cosineCFG 系数1.5~3.0计数场景偏保守防止过度拟合条件采样步数DDIM 50质量不够再升到 100条件丢弃率0.1CFG 必需太小则无条件预测不稳学习率1e-4 到 5e-5扩散模型对学习率敏感大 batch 配合降3.4 潜在扩散模型还是直接扩散模型工程选型上述代码跑在像素空间。256×256 输入在单卡训练没有问题但真实计数场景经常用 512×512 甚至更高分辨率像素空间扩散模型的计算量会指数上升。工程上更常见的选择是潜在扩散模型LDM先用 VAE 把图像压缩到 32×32 的潜变量再在潜变量空间运行同样的去噪过程显存和训练时间都降到可接受范围。潜在扩散模型结构里密度图条件通常先与 VAE 编码后的潜变量对齐再拼接到潜变量通道上。如果团队没有预训练底模建议先按 3.2 的代码在 256×256 像素空间跑通验证再迁移到 LDM 架构不要一上来就训 512×512 的潜在扩散模型迭代速度会拖累调参效率。4. 目标计数模型适配密度图回归、混合数据与评估清单4.1 从点标注到密度图监督信号的定义方法扩散模型生成样本后第一件事是把目标位置转成训练计数模型用的监督标签。目标计数领域的主流做法不是直接回归目标数量而是回归密度图模型输出的每个像素值代表该位置的局部目标密度对整幅密度图求和就得到目标总数。密度图生成一般使用固定 σ 的高斯核对每个目标点在其坐标处放置一个二维高斯核累加后归一化。自适应 σ 的常见做法是取第 k 近邻距离的 0.3 倍作为核宽对头部密集区域更友好。import numpy as np def points_to_density(points, h, w, sigma4.0): 把目标点列表转换成密度图。sigma 控制每个目标的影响范围。 xx, yy np.meshgrid(np.arange(w), np.arange(h)) density np.zeros((h, w), dtypenp.float32) for px, py in points: density np.exp(-((xx - px) ** 2 (yy - py) ** 2) / (2 * sigma ** 2)) return density参数说明sigma 太小会让每个目标只影响单个像素计数网络难以学习空间相关性sigma 太大则密度图过于平滑邻近目标合并为一块。人头计数任务 4.0 是不错起点车辆计数一般取 8.0 左右具体数值可以用验证集 MAE 微调。4.2 生成样本与真实样本混合的数据增强代码生成样本不建议一次性写入训练集并参与全部 epoch。常见做法是维护一个生成缓存每训练若干 epoch 重新采样一批新图避免计数模型把某次生成的纹理背下来。混合比例上真实样本与生成样本按 1:1 起步真实样本太少时最多不超过 1:3生成样本占比过高会拉低真实场景下的纹理适应能力。import cv2 import numpy as np import torch from torch.utils.data import Dataset class CountingDataset(Dataset): def __init__(self, image_paths, density_paths, size256, trainTrue): self.image_paths image_paths self.density_paths density_paths self.size size self.train train def __getitem__(self, idx): img cv2.imread(self.image_paths[idx]) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) den np.load(self.density_paths[idx]).astype(np.float32) if len(den.shape) 3: den den[:, :, 0] if self.train: top np.random.randint(0, img.shape[0] - self.size) left np.random.randint(0, img.shape[1] - self.size) img img[top:top self.size, left:left self.size] den den[top:top self.size, left:left self.size] if np.random.random() 0.5: img img[:, ::-1] den den[:, ::-1] img torch.from_numpy(img).permute(2, 0, 1).float() / 127.5 - 1.0 den torch.from_numpy(den).unsqueeze(0).float() return img, den这里只做随机裁剪和水平翻转避免对密度图做插值类增强造成计数误差。生成图片用 npy 保存密度图比存成 PNG 再换算更准确省去旋转、缩放时重新估计目标点的麻烦。提示密度图建议保存为 npy 文件而不是 PNGPNG 有损压缩会改变像素值导致计数网络学到错误的归一化关系。4.3 目标计数的评估指标MAE、MSE 和可视化核查指标公式适用场景MAE1/N Σ |pred_count - gt_count|衡量平均误差对离群点不敏感MSE1/N Σ (pred_count - gt_count)²对误差大的样本更敏感适合监控灾难性误判密度图像素 MSE1/(N·H·W) Σ (pred_den - gt_den)²反映空间位置是否正确不仅是总数正确计数任务有一个经典陷阱pred_count 和 gt_count 相等但预测密度图的位置完全错位。所以评估时至少要挑出每张测试图中预测误差最大的区域可视化观察网络是否把一群人预测到了错误位置。最终对外报告 MAE 和 MSE 即可密度图像素 MSE 放在内部监控。5. 生成样本质量检查与调参CFG 步数和流形偏移排查5.1 用预训练计数网络做生成样本质检FID 可以衡量生成图像和真实图像在特征分布上的靠近程度但它无法回答“密度图条件是否被正确表达”。更直接的验证手段是拿已经训练好的计数模型对生成图像预测密度图再与条件密度图对比。若 MAE 偏高说明扩散模型生成图像和条件不对齐不是“画得像不像”的问题而是分布偏移到了任务流形之外。torch.no_grad() def check_generated_pair(count_model, gen_image, gen_density, scale100.0): 用计数模型验证生成样本返回单图 MAE。 count_model.eval() pred count_model(gen_image.unsqueeze(0)) # 假设训练时密度图除以过 scale这里乘回来得到真实目标数 pred_count pred.view(pred.size(0), -1).sum(dim1) * scale gt_count gen_density.view(-1).sum() * scale return (pred_count - gt_count).abs().item()一个可用的技巧每轮评估抽 100 张生成图统计预测数量和条件数量的差值均值。这个值应该与真实测试集上的 MAE 在同一量级如果差很多先检查密度图有没有归一化到合适范围再检查 CFG 是否过大导致生成图像偏离条件。5.2 采样参数对增强质量的影响与常见故障现象可能原因建议调整生成图像语义混乱CFG 过小条件被忽略增大 CFG 到 3.05.0图像结构正常但目标数量不符密度图噪声大或采样步数不足采样步数升到 100或调低 CFG 到 1.5生成样本纹理单一CFG 过大抑制多样性调低 CFG增加随机噪声目标计数在真实数据上不涨生成域与真实域差异大提高真实样本比例减少极端密度样本潜在扩散模型容易被误用成大样本生成器一口气生成上万张图再训练。比较推荐的是让生成样本只覆盖真实数据里“比较差的尾部”比如高密度区域。设定密度图条件时从真实训练集统计密度分位数只对 P80 以上的高密度条件做生成补充增强增益最明显。地震数据、遥感计数等场景的常见做法也是同一套逻辑密度图条件越贴近实际分布合成样本价值越高。最后一个小建议把 100 张固定条件密度图保留下来每天用同一批条件重新生成一次对比计数模型的 MAE这比看 FID 能更早发现增强管线退化。本文还有配套的精品资源点击获取