ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

多列卷积实现密集人群计数:从原理到PyTorch实战

多列卷积实现密集人群计数:从原理到PyTorch实战 简介项目基于多列卷积神经网络实现密集人群计数是机器学习课程高分大作业适合人工智能、计算机、通信工程、自动化等专业学生用于课程设计或毕业设计参考。资源包共两千个文件其中一千九百八十五个为npy格式的数据文件九个为Python源码脚本五个为xml配置文件一个为md说明文档整体大小约为一百二十三兆字节。数据目录提供上海科技大学和加州大学圣地亚哥分校两个数据集模型目录包含两个结构相近的计数网络权重目录保存训练好的模型数据预处理脚本与训练测试脚本职责清晰方便直接复现。代码经测试运行成功项目评审平均分达九十六分作者可远程答疑适合初学者进阶或项目初期演示。目录结构完整另有附带说明文档辅助快速上手目前已有九十六人浏览学习可作为完整的人群计数课程设计方案。1. 密集人群计数与多列卷积这门大作业在解决什么现实问题高铁站监控屏上黑压压一片领导让你数人数演唱会现场照片发到工作群你第一反应是“这得雇多少学生来数”——密集人群计数就是这类任务。基于多列卷积的密集人群计数不再走“逐人检测再数框”的老路而是用机器学习里的多列卷积神经网络MCNN把一张RGB图直接回归成密度图再把密度图逐像素求和得到总人数。拆开这个标题就是三件事搞懂多列卷积为什么能扛住人挤人的场景把python源码、文档说明、数据和模型组成的项目跑通最后用训练好的模型做验证并写进高分课程设计。这个方向同时覆盖算法理解、图像处理和工程落地适合正在纠结机器学习大作业选题或者期末冲刺课程设计拿分的同学。2. 多列卷积的结构逻辑人群计数为什么不适合直接跑目标检测2.1 从检测框到密度图把“数人头”改写成像素回归面对人头这种目标第一反应通常是目标检测画框、分类、去重最后数框的数量。但在密集场景里这条路会很快翻车人贴人时严重遮挡不同视角造成形变检测框很容易把几个人框进同一个区域或者只数出一半。本质原因在于我们最终想要的不是每个人精确的边界框而是“一共有多少个头部”。把计数任务强行拆成先检测再统计是在给网络增加无谓负担。MCNN换了个监督目标密度图。构造方式很直观把每个人头标注点展开成一个二维高斯峰把所有高斯峰叠加得到一张和输入图像等分辨率的密度图每个像素的读数可以理解为“单位面积里有几个人头”。整张密度图求和就是总人数。这样背景区域朝零收敛头部区域有高有低模型学习的是“图像上每个像素属于头部中心的可能性”而不是去区分每张脸是谁。密度图里有一个关键参数 sigma决定单个高斯核扩散多少。sigma 固定会两头吃亏人群密集区互相压成一个高亮块而近处的大目标峰又太窄。经典做法是几何自适应核对每个人头取它最近的 k 个相邻人头距离的平均值再乘以缩放系数作为该点 sigma。实现时 k 一般取 3 或 4缩放系数在 0.10.2 附近。这段逻辑写进课程设计报告很出彩因为它直接回答了离散标注点如何变成连续训练目标。把计数当成回归来做还能绕开大量检测相关的工程细节不需要 anchor、不需要 NMS、不需要 IOU 匹配。对一份课时有限的机器学习大作业来说这些前置成本被大幅压低。你真正要做的是把标注变成密度图、把网络输出变成密度图然后用 L2 约束两者靠近。这也是这个课题能在半个学期内跑出量化指标的根本原因。2.2 5×5 / 7×7 / 9×9 三路分支不同感受野各管一个尺段多列卷积不是花架子它把人群计数里绕不开的尺度问题直接体现在结构上。假设一张街头照片近处几个人头占了很大的像素面积远处几十个人头只有十几个像素如果全网络只有一种感受野要么照顾大目标要么照顾小目标很难同时兼顾。MCNN 在同一张输入上并行伸出三列卷积分支就是让每一列用不同覆盖范围去吸收一个尺度段。常见实现里三列分别使用 5×5、7×7、9×9 的卷积核每列内部做几次卷积后接一个最大池化。9×9 的单层覆盖范围最大适合捕捉人群和背景的整体布局5×5 的视野更聚焦能保留头部边缘的细节响应7×7 介于两者之间辅助刻画相邻人头的相对关系。三条路径输出的特征在 channel 维度拼接后再交给两个 1×1 卷积把多列特征融合成单通道密度图。1×1 卷积本质是通道间的线性融合不改变空间尺寸所以输出密度图可以同标注分辨率对齐。这个设计带来的额外收益是训练更稳。多路并行相当于给网络提供了多组候选视野它可以让不同分支分别激活在不同尺度的头区域而不是靠加深单条网络来压缩分辨率。很多课程作业丢分正是因为报告里只画结构图没把感受野分工讲清楚。能把“哪一路管什么”讲明白评审就知道不是照抄网络结构。如果要在源码里验证这个分工最直接的做法是分别冻结另外两列只放某一列训练 50 轮比较单列测试 MAE再开放三列一起训练。这种消融实验只改几行代码却是文档说明里很加分的部分实操成本比想象中低很多。2.3 用L2当损失、MAE/MSE当验收训练目标为什么这么定MCNN 的训练在 PyTorch 里就是一个典型的最小二乘回归。损失函数最常见的是nn.MSELoss()做法是让预测密度图与标注密度图每个像素尽量一致。使用 L1 也可以但 L1 对密度图中大量接近零的背景区域相对钝感收敛后容易留下系统性偏差。课程设计缺省我建议用 L2优化器选 Adam初始学习率 1e-4 到 1e-5 比较稳。如果换 SGD必须同时调好 momentum 和学习率衰减否则 loss 曲线颠簸得很厉害。评价指标必报两个数MAE 和 MSE。MAE 是所有测试图abs(预测总人数 - 真实总人数)的平均值反映平均偏多少人MSE 在平方后平均再开方会放大少数几张图预测得特别差的代价。课程设计答疑时老师大概率先看 MAE接着看真实人数和预测人数的散点图最后才看 loss 曲线。建议把验证集每张图的真实人数和预测人数做成散点能直接看出模型是否在人数越多的图上越容易低估。开始训练前先把计数口径定死密度图求和时要不要先恢复到原图分辨率。不少实现因为网络里放了 MaxPool输出空间尺寸只有输入一半直接对输出求和会引入比例偏差。如果训练时标注密度图也跟着做了同样倍数的下采样那训练和验证口径一致问题不大如果要求测试集最终人数对应原图预测图先 resize 回原图再求和更稳妥。这一条看着是小细节实际占了人群计数翻车案例里的三成。3. 把这套python源码跑通环境确认、密度图生成与训练主流程3.1 项目包目录核对先确认标注格式再谈环境拿到项目包先解压再打开目录看结构不要急着跑 train.py。常见布局是data目录放图像和标注models目录放网络定义utils目录放 IO 与密度图生成根目录下有train.py、evaluate.py和文档说明。你需要先确认一件事标注格式和训练脚本的读法是否一致。很多课程设计包里的标注是.mat文件脚本用scipy.io.loadmat读取另一些可能是 json 或 txt 坐标列表。如果加载就报 IndexError多半是标注坐标是列向量还是行向量没对齐或者坐标轴顺序反了。依赖环境可以直接用 conda 隔离避免把系统 Python 搞乱conda create -n mcnn python3.8 -y conda activate mcnn pip install torch1.13.1 torchvision0.14.1 --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python numpy scipy matplotlib tqdm pandas这里把 torch 固定到 1.13.x是为了兼容大多数课程设计源码里常用的接口写法。没有 NVIDIA GPU 就把--index-url换成 CPU 版本代码不需要改只是训练时间会拉长。OpenCV 负责图片读写scipy 负责.mat解析和 KDTree 搜索后面生成自适应高斯核要直接用到。3.2 把标注点转成密度图带自适应高斯核的完整脚本标注转密度图是整个项目最容易埋坑的环节也是数据预处理里最核心的函数。下面这段是我在类似课程设计中会默认采用的一版import numpy as np import scipy.spatial as spatial def gaussian_peak(xx, yy, cx, cy, sigma): return np.exp(-((xx - cx) ** 2 (yy - cy) ** 2) / (2 * sigma * sigma)) def points_to_density(height, width, points, adaptiveTrue, k3, sigma_fixed15.0, k_sigma0.15): density np.zeros((height, width), dtypenp.float32) pts np.asarray(points, dtypenp.float32) if len(pts) 0: return density if adaptive: tree spatial.KDTree(pts) dists, _ tree.query(pts, kk 1) # 第一个邻居是自身 sigma np.mean(dists[:, 1:], axis1) * k_sigma else: sigma np.full(len(pts), sigma_fixed) for i, (x, y) in enumerate(pts): s max(sigma[i], 1.0) # 防止 sigma 过小退化成尖峰 x0, x1 int(max(0, x - 3 * s)), int(min(width, x 3 * s 1)) y0, y1 int(max(0, y - 3 * s)), int(min(height, y 3 * s 1)) xx, yy np.meshgrid(np.arange(x0, x1), np.arange(y0, y1)) density[y0:y1, x0:x1] gaussian_peak(xx, yy, x, y, s) return density这个函数的逻辑分三步先检查标注点是否为空空图直接返回全零密度图再用 KDTree 求每个点的 k 近邻平均距离距离越大说明当前人头周围越稀疏sigma 就越大最后按每个点的 sigma 裁出局部窗口叠加高斯峰。k_sigma0.15是扩散系数太大会让密集区域的峰互相覆盖太小会让监督图接近脉冲信号导致 L2 损失很难优化。代码里强制sigma最小为 1.0是为了避免两个坐标重叠时核函数退化成单像素亮点让模型几乎拿不到该位置的梯度。训练前还有个检查习惯随机抽几张图把生成的密度图求和看看是否等于标注点数。如果对不上问题基本出在坐标读取或坐标系换算上此时再多调网络结构也没有意义。3.3 MCNN三列模型与训练循环最小可跑pytorch实现模型定义按标题里的多列卷积来写最直接的就是三分支结构import torch import torch.nn as nn class MCNN(nn.Module): def __init__(self): super().__init__() # 5x5 分支倾向局部细节 self.branch1 nn.Sequential( nn.Conv2d(3, 20, kernel_size5, padding2), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(20, 24, kernel_size5, padding2), nn.ReLU(), nn.Conv2d(24, 32, kernel_size5, padding2), nn.ReLU(), ) # 7x7 分支中等感受野 self.branch2 nn.Sequential( nn.Conv2d(3, 18, kernel_size7, padding3), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(18, 22, kernel_size7, padding3), nn.ReLU(), nn.Conv2d(22, 32, kernel_size7, padding3), nn.ReLU(), ) # 9x9 分支覆盖范围更大 self.branch3 nn.Sequential( nn.Conv2d(3, 16, kernel_size9, padding4), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(16, 20, kernel_size9, padding4), nn.ReLU(), nn.Conv2d(20, 32, kernel_size9, padding4), nn.ReLU(), ) # 三路输出拼接后融合成单通道密度图 self.fuse nn.Sequential( nn.Conv2d(96, 64, kernel_size1), nn.ReLU(), nn.Conv2d(64, 1, kernel_size1), ) def forward(self, x): f1 self.branch1(x) f2 self.branch2(x) f3 self.branch3(x) out torch.cat([f1, f2, f3], dim1) return self.fuse(out)这里每列通道数刻意压低是一种轻量版 MCNN。三个分支都经历两次卷积和一次 MaxPool空间尺寸减半最终融合层的输入通道数是 96因为 3 个分支输出都是 32 通道。padding保证了卷积后尺寸不缩和论文里常见的“same”写法等价。融合层全部用 1×1 卷积只做跨通道融合不引入额外空间下采样这样密度图分辨率不会进一步丢失。训练循环可以压得很短核心逻辑如下from torch.utils.data import DataLoader import torch.optim as optim EPOCHS 200 BATCH_SIZE 16 LR 1e-4 model MCNN().cuda() criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lrLR) train_loader DataLoader(train_set, batch_sizeBATCH_SIZE, shuffleTrue, num_workers4, pin_memoryTrue) for epoch in range(EPOCHS): model.train() total_loss 0.0 for imgs, density_maps in train_loader: imgs imgs.cuda() density_maps density_maps.cuda() pred model(imgs) loss criterion(pred, density_maps) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() if (epoch 1) % 10 0: torch.save(model.state_dict(), fcheckpoints/mcnn_{epoch 1}.pth) print(fepoch {epoch 1:3d} loss{total_loss / len(train_loader):.4f})这里train_set需要自己实现一个 Dataset返回值是(图像张量, 密度图张量)。batch size 16 是常规设置训练图如果使用 256×256 到 384×384 的随机裁剪单卡 8G 显存一般能顶住显存吃紧就降到 4 或 8。学习率 1e-4 配 Adam 是最省心的起点。每 10 个 epoch 存一次检查点宁可多几个.pth文件也不要训练了一半断点后只能从头再来。3.4 训练中必须盯住的三个数值第一个是训练 loss 的下降节奏。MCNN 通常在前 20 到 50 个 epoch 有段明显下降之后变成缓慢收敛。如果 loss 卡在某个值纹丝不动先怀疑学习率和梯度问题再看数据增强是否把密度图尺度搞乱了。第二个是验证集 MAE。建议每 10 个 epoch 跑一次evaluate.py。MAE 和训练 loss 一起下降说明模型在正常学习如果训练 loss 还在降但 MAE 不再动基本就是过拟合需要加 weight decay、做更多随机裁剪增强或者提前停。第三个是密度图求和值。这个值应当和标注人数基本一致。定期挑几张图把预测密度图和标注密度图叠在原图上肉眼对比不要等到最后再验证。损失曲线只能告诉你模型在数值上拟合了不能告诉你位置有没有偏、峰有没有塌。4. 密集人群计数的五个高频坑现象、定位、处理4.1 预测总数只有真实数的一半尺度不对齐是头号嫌疑现象训练 loss 降到很低但测试集上把预测密度图求和总数明显偏小而且人数越多偏得越厉害。原因大概率在监督尺度训练用的密度图把原图缩了一半模型学到的输出尺寸和测试口径不一致另一种常见情况是自适应核里k_sigma设得太小密度图整体偏尖L2 对低值背景区域过度宽容预测值被压扁。解决先用脚本逐张检查训练密度图的 sum 是否等于标注人数再确认模型输出尺寸与训练监督尺寸一致。两个都不对时优先修数据链路不要急着改网络结构。4.2 密度图中心点漂移都是数据增强不同步惹的祸现象预测热力图看得出人头位置但每个峰值中心比真实标注偏了几个像素密集区域尤其乱。原因随机裁剪、缩放、翻转的时候只改了图像没同步改标注坐标或者用 DataLoader 对图像做标准化固定坐标却仍按原图尺寸生成密度图两者对不上。解决把随机裁剪、缩放、翻转统一写成一个函数先算好图像变换的映射关系再同时作用于原图和坐标优先保证“坐标相对新图像的位置”正确再重新生成密度图。不要先按原图生成密度图再 resize那样峰值位置和图像内容会脱节。4.3 密集区域糊成一团先查自适应核参数再查下采样现象预测密度图把人头密集区糊成一大块连续高亮看不到多个峰。原因k_sigma偏大相邻几个高斯峰叠加成一个或者网络输出分辨率太低MaxPool 把局部细节抹平了。解决把扩散系数从 0.15 往 0.080.1 调重新生成密度图后可视化对比如果网络在第二次池化之后就已经丢失太多空间信息把后段卷积换成空洞卷积或者直接移除第二个 MaxPool让输出分辨率保持原图四分之一以上。人群计数本质上依赖空间精度不是一个只看总 loss 的任务。4.4 一加大batch就显存不足训练尺度与推理尺度的取舍现象batch size 提到 16 跑不了多久就 OOM甚至直接中断。原因人群数据集里常有 1000×2000 的大图全图直接进 GPU 等于把显存当饭吃。解决训练时随机裁剪成固定尺寸比如 256×256 或 384×384按裁剪区域内的标注点重新生成密度图推理时再对原图做滑窗预测或分块预测最后拼接回完整密度图。显存还不够就把 batch 降到 1用梯度累积模拟更大 batch。模型其实不挑输入尺寸但 GPU 显存挑这个取舍越早做越好。4.5 输出全图接近零值最后一层激活函数把回归任务带偏现象预测密度图几乎全黑只有零星像素有响应。原因最后的输出层加了 sigmoid 或 softmax密度被当成概率分布压到零附近或者最后一层卷积没有 bias又叠加了过强的标准化网络直接躺平输出常数。解决确认融合层的最后一个 1×1 卷积后面不带激活函数更不要 sigmoid。调试时可以打印torch.max(model(img))如果最大值小于 0.01基本就是输出层卡壳。密度图回归不需要把输出约束在 01网络完全有能力自己学会合适幅度。5. 从跑通到拿高分验证曲线、可视化对比与最后一公里调优课程设计的评分通常不只盯着最终 MAE更看重报告里展示的验证过程。建议在项目里固定一套输出训练 loss 曲线图、测试集上真实人数与预测人数的散点图、以及抽样可视化页。每 5 张测试图挑一张把预测密度图叠在原图上生成热力图能直接看出模型是在人群密集区翻车、还是小目标区域漏检。验证脚本值得单独留一个固定计数口径import torch import torch.nn.functional as F import numpy as np def evaluate(model, test_loader): model.eval() mae_list, mse_list [], [] with torch.no_grad(): for img, gt_density in test_loader: pred model(img.cuda()).cpu() # 若输出尺寸与监督不一致先插值回同一尺寸再求和 pred F.interpolate(pred, sizegt_density.shape[2:], modebilinear, align_cornersFalse) p_count pred.sum().item() g_count gt_density.sum().item() mae_list.append(abs(p_count - g_count)) mse_list.append((p_count - g_count) ** 2) return np.mean(mae_list), np.sqrt(np.mean(mse_list))这里的interpolate是为了保证预测和标注在同一个空间分辨率下比否则求和数字没有可比性。如果训练阶段已经统一了分辨率这句不执行也可以但留着更安全。想再压一点 MAE低成本手段是测试时增强。把测试图水平翻转后再预测一次两张密度图取均值能抵消一部分左右不对称的漏检。再把输入缩放到 0.8 和 1.2 两个倍数分别预测取平均通常在公开数据集上能换来 2%5% 的误差下降。训练侧配合随机缩放增强模型对尺度的鲁棒性会明显比固定尺度训练好。另一个值得做的是消融对比单列、双列、三列各跑一套验证把结果做成表格写进文档说明。多列卷积拿高分的说服力不在于网络多复杂而在于你能证明多列确实比单列好。这个结论用验证集 MAE 摆出来比任何文字描述都有力。我给自己定的习惯是每改一次损失函数或数据增强先输出一张预测热力图看三秒再拿指标说话。光看终端 loss 数字很多时候只是黑匣子把可视化和分析绑定在一起才能定位是数据问题、结构问题还是训练策略问题。希望这份路径能帮你在机器学习大作业里少绕路真正把多列卷积的密集人群计数做成一个能讲清楚原理、能复现结果的高分课程设计。本文还有配套的精品资源点击获取
返回列表