☰
PaddlePaddle人流密度检测实战:从密度图回归到模型调优
2026/10/14 8:46:59 网站建设 项目流程

PaddlePaddle人流密度检测比赛已经结束有一阵子了,结果也早就出来了。我们队伍最后拿了复赛第15名,不算特别亮眼的成绩,但整个过程踩过的坑、总结出来的经验,我觉得比名次本身值钱得多。下面这些内容,是我从初赛到复赛完整走下来的实战笔记,里面有方案选型的纠结过程、数据处理的细节、模型训练的参数设置,还有十几个真实踩过的坑,希望能给后面参加类似比赛或者准备做人群计数方向项目的同学一点参考。

先说清楚:这虽然是一个密度检测比赛,但本质上解决的是**人群计数(Crowd Counting)**问题。比赛提供了多个监控场景的视频和图像数据,要求对每一帧画面预测出人流密度分布,并给出区域内的人头数量。评估指标用的是准确率和召回率综合计算,这一点和学术界的MAE/MSE不太一样,后面会细说。

1. 任务拆解与基线方案选择

1.1 如何理解“人流密度检测”

比赛的原始数据是监控摄像头拍下的俯视或斜俯视画面,场景包含商场出入口、站台、景区步道等。目标是输出两个东西:一是每个像素点附近的人头密集程度(也就是所谓的密度图),二是整张图片上的总人数。

如果只看总数,其实就是一个回归任务;但如果还要结合视频空间位置,密度图就变成了一张和原图尺寸对应的二维热度图。模型要做的事,就是把输入图片映射成这张热度图,热度越高表示这个位置越可能有人头。

为什么比赛不直接让目标检测模型来数人?因为监控画面里人挤人的情况太常见了,行人框互相遮挡严重,检测框覆盖率极低。用目标检测的思路做,就面临从NMS(非极大值抑制)的框筛选到小目标漏检的一串麻烦事。密度回归方案的天然优势是把“数人”转成“积分问题”:对密度图求和,就得到人数估计,天然避开了框重叠的困扰,也更贴近实际业务中只关心人流量级别、不关心具体人框的需求。

1.2 技术演进路线与选型背后的取舍

人群计数方向近十年的主流方案大致经历了一个清晰的演变过程:

第一阶段是“检测为主”的时期。早期百度的相关项目里,也尝试过用YOLO或者Faster R-CNN一类检测器去做人头识别,但真实监控场景里人头饱和度很高,重叠区域太多,检测性能很容易触顶。

第二阶段是“密度图回归”时期。典型代表是MCNN(多列卷积网络)和CSRNet。这类模型把输入图像映射成密度图,再对密度图积分得到人数,核心思想是让网络学习位置信息和人群分布的全局规律。MCNN用不同感受野的列并联来捕捉尺度变化,CSRNet更大胆地用空洞卷积扩大感受野,在保持分辨率的同时提高了密度图质量。

第三阶段是“Transformer与点监督”时期。近两年出现了一些基于Transformer的密度估计模型,以及更极端的单点标注监督方案。但这类方案普遍需要更庞大的数据量和更复杂的训练策略,比赛周期短,团队人力有限,性价比不高。

我们最终选择了CSRNet结构 + HRNet主干特征提取的组合,理由有三个方面:

第一,CSRNet结构简单,训练稳定,比赛这种短平快的场景里不需要冒险尝试新奇模型;第二,HRNet能同时保留高分辨率和多尺度语义信息,对监控视角下人头大小差异很大的场景效果更好;第三,PaddlePaddle框架里这两个组件都有现成实现,不需要自己从头搭。

1.3 方案设计的总览图

整个系统的数据流向是这样的:

原始图片先做归一化和尺寸调整,送入主干网络(HRNet)提取多尺度特征,然后通过空洞卷积堆叠的头部网络生成密度图,最后对密度图按区域求和得到人数预测。训练阶段用真实标注生成的高斯核密度图作为监督信号,用欧氏距离损失加上SSIM结构一致性损失来做优化。

音频特征这里设计了一个关键点:验证阶段不直接用人数误差来选模型,而是用比赛官方给出的“准确率和召回率”指标来切模型,效果会好很多。这也是我们后期分数提升的一个隐含原因。

2. 数据预处理与增强的实操细节

2.1 标注数据的清洗与密度图生成

初赛给的数据大概有两千多张图,官方提供了图像中每个人头中心点的坐标标注。训练的第一步是把点坐标转成密度图。

这一步有个容易犯的错误:直接用固定高斯核在原分辨率的黑色背景上画点,然后缩放到模型输入尺寸。如果原图是1080p,模型输入是512x512,直接缩放会导致密度图里的人数不等于原始人数。正确做法是先缩放到目标分辨率,再在目标分辨率上以每个点为中心做高斯分布,或者用尺度归一化的方式处理。

点标注转密度图的核心代码不复杂:

import numpy as np import cv2 def points_to_densitymap(size, points, sigma=4): """ size: (h, w) 目标密度图的尺寸 points: [[x1,y1],[x2,y2],...] 标注点坐标列表 sigma: 高斯核标准差 """ h, w = size density = np.zeros((h, w), dtype=np.float32) for x, y in points: if x < 0 or y < 0 or x >= w or y >= h: continue # 生成以(x,y)为中心的高斯核 gauss_x = np.arange(0, w, 1, dtype=np.float32) - x gauss_y = np.arange(0, h, 1, dtype=np.float32) - y gauss_2d = np.exp(-(gauss_x[None, :] ** 2 + gauss_y[:, None] ** 2) / (2 * sigma * sigma)) density += gauss_2d return density

sigma的选择直接影响训练质量。过小会变成稀疏的点阵,网络学到的梯度不稳定;过大会让人头之间的密度重叠得面目全非,计数偏差大。实际操作中,我根据场景做了缩放:人头小的远场景用sigma=3~4,近场景人头大,用sigma=5~8。比赛数据里摄像头离人群远近不一,最稳妥的是按人头距离的平均间距动态设置sigma,而不是全局写死。

2.2 训练集与验证集的划分策略

数据划分上,不能随机打乱。原因很简单:很多连续帧来自同一个监控视频,直接随机切分会导致同一场景的内容同时出现在训练和验证里,模型没有真正见过的新场景,但验证指标会虚高,给后面复赛带来风险。

我们的划分方式是基于场景ID来分:把每个视频或场景的所有帧归为一组,以场景为单位按8:2切分。保证训练集和验证集没有共同的场景,这样验证分数才更能反映真实的新场景泛化能力。

划分完之后,我又手动审计了验证集合每一张图片的人数分布,确保验证数据里既有人流稀疏的时段(小于10人),也有密集时段(超过100人),避免验证集本身偏向一个极端。这个步骤虽然耗时一两个小时,但能避免后续模型调参时被片面指标误导。

2.3 数据增强策略与PaddlePaddle实现

监控视频截图的特点是场景相对固定,但光照变化大,人群密度分布差异大。训练时我用了三组增强手段:

第一组是几何增强,包括随机水平翻转、随机裁剪、多尺度缩放。特别说明一下,随机裁剪非常关键,因为交通枢纽这种场景里,不同区域的人群密度差异很大,裁剪能让模型看到更多局部拥挤的样例。通常先resize到原图的1.0~1.5倍,再随机裁剪到目标尺寸,相当于做了随机尺度增强。

第二组是光照与噪声增强。监控画面有夜间红外、强背光、窗户反光等复杂光照,使用随机亮度扰动、gamma变换、高斯噪声,让模型不要依赖绝对亮度来判断是否有人的特征。

第三组是场景拼贴增强。从训练集里抽取其他场景的小图块,随机粘贴到当前图片的空白区域,这样等于变相增加了高密度区域的样本量。比较有意思的是,粘贴的图块要带有来自不同场景的成像噪声和色调,否则模型会学习到“拼接边界”这种伪特征。

在PaddlePaddle里面,这些增强都可以通过paddle.vision.transforms配合自定义函数实现:

import random import numpy as np import paddle from paddle.vision.transforms import Compose, RandomHorizontalFlip class RandomCropResize: def __init__(self, target_size=512, scale_range=(0.8, 1.5)): self.target_size = target_size self.scale_range = scale_range def __call__(self, img, density): h, w = img.shape[:2] scale = random.uniform(*self.scale_range) nh, nw = int(h * scale), int(w * scale) img = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_LINEAR) density = cv2.resize(density, (nw, nh), interpolation=cv2.INTER_LINEAR) # 注意:密度图resize后必须按缩放比例反向补偿,保证积分值不变 density *= (scale * scale) # 随机裁剪到目标尺寸 if nh > self.target_size and nw > self.target_size: y = random.randint(0, nh - self.target_size) x = random.randint(0, nw - self.target_size) img = img[y:y + self.target_size, x:x + self.target_size, :] density = density[y:y + self.target_size, x:x + self.target_size] return img, density

上面代码里“密度图resize后必须按缩放比例补偿”这一句是重点。因为密度图的积分代表人数,直接resize会把人数也缩放掉,不乘回比例系数的话,模型的学习目标就出错了。我当时第一次没加这个补偿,训练loss迟迟降不下去,排查了半天才发现是这个问题。

2.4 数据集加载器性能优化

两千多张图逐张读入并做增强,如果不做优化,一个epoch就要跑很久。我们用内存缓存加异步数据加载的方式来解决。

具体做法是把所有训练图片read进内存放在RAM里,训练每个epoch时直接从内存读取而不是反复IO。这一批数据加起来大概十几个G,比赛要求使用带GPU的服务器,内存通常比较充足,实际可用。换成SSD随机读取后,数据加载瓶颈反而从IO变成了图像处理和增强计算,此时再配合Dataloader的num_workers多进程预取,速度就能提上去。

PaddlePaddle的DataLoader里面设置num_workers参数就可以了:

train_loader = paddle.io.DataLoader( train_dataset, batch_size=16, shuffle=True, num_workers=8, drop_last=True, use_shared_memory=True )

2.5 数据增强的“度”如何把握

增强不是越多越好。增强过强会引入失真噪声,模型学不到真实场景的结构特征;增强过弱又容易过拟合。经过十几轮实验,我总结了几条判断标准:

密度图的增强必须跟随图像增强做同样的变换,人和密度图之间是像素级对齐关系,剪裁区域、翻转方向、缩放比例都必须一致,这一点容易忽略。

高频增强要慎重。高斯噪声、色彩抖动这类强度不宜过大,否则会让模型在低纹理区域产生虚假密度响应,把墙面错判成人头。

每个epoch里的增强强度可以动态调整。前50个epoch用较弱增强,让模型学基础特征;后面逐步增强,提高鲁棒性。这样训练比全程用强增强更平滑,也更容易收敛。

3. 模型选型与训练调优全过程

3.1 主干网络与head结构的选型分析

我们对比过三个方案:VGG16作为backbone的CSRNet、ResNet50加Dilated Backbone、以及HRNet-W32加CSRNet头。训练速度和验证分数的数据表明HRNet-W32的方案前期略占优,尤其在密集场景下,它保留的高分辨率细节能明显增加密度图的峰值清晰度。

CSRNet的head部分,用的是连续的dilated卷积层组合。dilated rate从2、4、8、4、2这样一组配置,最终输出的密度图和输入分辨率相同(或1/2尺寸)。用空洞卷积而不是普通下采样卷积,是因为下采样会把密度图的空间分辨率降低,返回积分的时候精度损失较大。

head部分代码:

import paddle import paddle.nn as nn import paddle.nn.functional as F class DilatedHead(nn.Layer): def __init__(self, in_channels=720, mid_channels=256): super(DilatedHead, self).__init__() self.conv1 = nn.Conv2D(in_channels, mid_channels, 3, padding=2, dilation=2) self.conv2 = nn.Conv2D(mid_channels, mid_channels, 3, padding=4, dilation=4) self.conv3 = nn.Conv2D(mid_channels, mid_channels, 3, padding=8, dilation=8) self.conv4 = nn.Conv2D(mid_channels, mid_channels, 3, padding=4, dilation=4) self.conv5 = nn.Conv2D(mid_channels, mid_channels, 3, padding=2, dilation=2) self.conv_out = nn.Conv2D(mid_channels, 1, 1) self._init_weights() def forward(self, x): x = F.relu(self.conv1(x)) x = F.relu(self.conv2(x)) x = F.relu(self.conv3(x)) x = F.relu(self.conv4(x)) x = F.relu(self.conv5(x)) out = F.relu(self.conv_out(x)) return out def _init_weights(self): for m in self.sublayers(): if isinstance(m, nn.Conv2D): k = m.weight.shape[2] * m.weight.shape[3] m.weight.set_value( paddle.normal(mean=0, std=1.0 / (k ** 0.5), shape=m.weight.shape) )

3.2 损失函数的设计与调参实验

密度估计的常用损失函数是欧氏距离损失(L2 Loss)。L2损失单独使用的问题在于,它假设各像素独立,但实际密度图是强相关的空间结构,人头的峰值和周围的高斯分布有内在关联。模型完全靠L2损失训练出来的密度图峰值往往偏低,背景区域却又会出现雾状伪密度。

我们最终使用了三个损失的加权组合:

第一项L2损失监督全局密度值回归量; 第二项SSIM损失(结构化相似损失)感知密度图的空间结构相似性,SSIM能迫使模型输出的密度图在局部区域与人头位置的空间关系保持一致性; 第三项局部一致性损失,将密度图划分成小patch,约束每个patch内的密度积分与真实值的误差。

权重比例经过几轮回调,最终确定为L2权重为10,SSIM权重为0.1,局部一致性权重为0.05。SSIM权重过大时,模型会把整张图的纹理趋势抓得很准,但人数总和反而不太准;L2权重过大时,SSIM起不到作用,密度图容易出现高频噪声。

损失函数实现:

class DensityLoss(nn.Layer): def __init__(self, l2_weight=10.0, ssim_weight=0.1, patch_weight=0.05): super(DensityLoss, self).__init__() self.l2_weight = l2_weight self.ssim_weight = ssim_weight self.patch_weight = patch_weight def forward(self, pred, target): # 欧氏距离损失 l2_loss = F.mse_loss(pred, target) # SSIM损失(用paddle的ssim接口) ssim_value = paddle.nn.functional.ssim( pred, target, data_range=1.0 ) ssim_loss = 1.0 - ssim_value # 局部patch一致性损失 pred_patches = pred.reshape([pred.shape[0], 1, -1, 16, 16]).sum(axis=[2, 3, 4]) target_patches = target.reshape([target.shape[0], 1, -1, 16, 16]).sum(axis=[2, 3, 4]) patch_loss = F.l1_loss(pred_patches, target_patches) return self.l2_weight * l2_loss + self.ssim_weight * ssim_loss + self.patch_weight * patch_loss

3.3 优化器、学习率与训练时长配置

优化器选用Adam,初始学习率定为1e-4,权重衰减1e-5。为什么不用SGD?密度回归对初始学习率很敏感,SGD配大学习率容易飞掉,配小学习率收敛太慢。Adam的适应性学习率机制在这个场景下更省心。

学习率策略上,用的是PaddlePaddle的CosineAnnealingDecay,最小学习率设为1e-6,预热了5个epoch。预热阶段让统计信息先稳定,避免开局就冲向loss的震荡区。

配置文件:

scheduler = paddle.optimizer.lr.CosineAnnealingDecay( learning_rate=1e-4, T_max=120, eta_min=1e-6, last_epoch=-1, verbose=False ) optimizer = paddle.optimizer.Adam( parameters=model.parameters(), learning_rate=scheduler, weight_decay=1e-5 )

训练轮次上,总共训练了120个epoch。实际观察曲线,前40个epochloss下降非常快,60~100个epoch在验证集上逐步稳定,100~120个epoch验证指标略微震荡,但此时保存的是历史最优模型,不用太担心过拟合的问题。

3.4 训练过程中的显存优化技巧

HRNet-W32加上膨胀卷积head,单卡batch_size=16确实可以跑,但在512x512输入分辨率下显存占用接近12G。如果显存只有11G,那就要做一些调整,有几个技巧:

第一是梯度累积。用batch_size=4的小批次,梯度累积4步再更新一次参数,等效于batch_size=16的效果。内存占用从12G降到6G以下。

accum_steps = 4 optimizer.clear_grad() for step, (images, targets) in enumerate(train_loader): outputs = model(images) loss = criterion(outputs, targets) loss = loss / accum_steps loss.backward() if (step + 1) % accum_steps == 0: optimizer.step() optimizer.clear_grad() scheduler.step()

第二是用混合精度训练。在PaddlePaddle里开启AMP(自动混合精度)非常简单,只需要配置amp_init,大部分计算用float16执行,显存直接减半,而且在这类任务上精度损失几乎可以忽略。

model = paddle.amp.decorate( models=model, level='O2', save_dtype='float32' ) scaler = paddle.amp.GradScaler(init_loss_scaling=1024.0)

第三是把输入尺寸先减到384x384来跑通基线,再提升到512x512对比精度。很多人一上来就追求高分辨率,但384和512的精度差距在0.5%以内的时候,优先选择384,训练速度快接近一倍,能多跑很多实验版本。

3.5 不同场景下的模型表现差异

比赛中我发现模型在不同类型场景的表现差异很大。平均准确率有91%左右,但把结果按场景分类统计之后,商场景点的准确率明显高于交通枢纽场景。分析原因有二:商场画面的视角更平直,人头大小相对一致,密度分布均匀;交通枢纽的俯视视角下人头小而密集,交错遮挡严重。

这个差异提醒我一件事:要针对性地做场景自适应。我们后面在粗剪数据阶段,把交通枢纽的视频片段单独取出,做了更多尺度的增强、更大范围的随机裁剪,才把这个场景的分数追上来。

这类“按场景拆分组”的评测方式在比赛里很常见,早一点做分组统计,能提前发现模型的偏科问题,比最后看一个平均分数有用得多。

4. 推理优化与后处理逻辑

4.1 多尺度推理的实际效果

推理阶段,我们使用了三尺度测试(0.8x、1.0x、1.2x)。多尺度推理的原理是:不同尺度下模型对人群密集程度的响应不同,把多个尺度的密度图对齐回原分辨率并求平均,能减少单一尺度下的预测偏差。

对齐时要注意尺寸取整问题。每个尺度的密度图resize回原图尺寸,人头的积分值会被缩放,需要除以缩放系数的平方来补偿人数。比如0.8x推理的密度图resize回原图时,要除以0.64。

多尺度推理的收益在我们实测中约提升0.5~1%的准确率。代价是推理时间乘了三倍。如果是线上实时场景,可以用flip测试(水平翻转推理再加权平均),效果接近但代价只有两倍。

4.2 从密度图到最终人数的转译策略

去掉多尺度推理之后,整合密度图有两种常见方式:

直接对整张密度图求和,得到总人数。这是最直觉的做法,适用于评估阶段只要求总人数的任务。

按区域求和再相加。如果监控场景有固定的物理分区(闸机口、扶梯口等),可以对每个区域独立求积分。

这里有个容易踩的坑:对于人群极度密集、密度图峰值互相重叠的区域,直接整图求和会略微少估人数。因为多个相邻人头的密度峰值叠加后会被模型压到一个范围里,积分就不等于实际总数。

我们的解决方案是引入一个后处理调制系数:用验证集统计真实人数和预测人数的比值,把这个比值作为一个全局校正因子。虽然看起来很朴素,但确实能提升最终赛果0.5%左右。逻辑上是因为模型对密集区域的系统性低估,乘以一个大于1的系数能部分补偿这个偏差。

4.3 推理时的预处理细节

推理阶段的预处理比训练阶段简单很多,但有几个细节值得注意:

色域归一化要和训练一致。如果训练时用的是PaddleX或PaddleDetection自带的Normalize,推理阶段也必须走完全相同的均值和标准差。很多人在这一步用错了顺序,导致输入分布和训练分布不一致。

推理图片长宽比不同时,可以等比缩放后做padding,而不是直接拉伸。直接拉伸会扭曲人头形状,密度图的峰值位置会受到形变的干扰。

预测密度图低于某个阈值的峰值要直接置零。密度图在背景区域会有轻微响应,如果不对峰值做筛选,那些低幅度的噪声也会被加进总数里。阈值选择通过验证集搜索确定,我们最终用的是密度图上所有像素最大值的10%作为过滤阈值。

推理代码示例:

def inference_one_image(model, img, scales=[0.8, 1.0, 1.2]): model.eval() h, w = img.shape[:2] density_sum = np.zeros((h, w), dtype=np.float32) for scale in scales: nh, nw = int(h * scale), int(w * scale) resized = cv2.resize(img, (nw, nh)) inp = transform(resized).unsqueeze(0) # shape: 1,3,N,H,W inp = paddle.to_tensor(inp) with paddle.no_grad(): pred = model(inp)[0, 0].numpy() # resize回原分辨率时需要补偿缩放 pred = cv2.resize(pred, (w, h)) if scale != 1.0: pred = pred / (scale * scale) density_sum += pred density_avg = density_sum / len(scales) total_count = density_avg.sum() return density_avg, total_count

4.4 输出格式与官方评估口径的适配

比赛要求提交的结果是每张图片对应的总人数,但官方排行榜的计分方式并不是对人数误差直接排序,而是综合了准确率和召回率。具体来说,预测人数和真实人数分别作为正样本和检出的置信度来算指标,也就是说,要让最终评分的准确率尽量高,模型的预测就应该在某些情况下有偏置。

这里要特别警惕:如果只追求准确率,那预测人数应当尽量接近真实人数,但官方指标里还有召回率,说明他们更关心“检出的目标占真实目标的比例”,一味压低预测值反而可能伤到召回率。

我们最后做的调优策略是:训练时以损失函数优化为主,推理时用验证集上综合F1最大化的阈值来确定全局人数校正系数,而不是简单地让总人数无偏估计。这一步调整在排行榜上提升了0.3个百分点左右。

5. 比赛中的问题排查经验实录

5.1 训练loss正常但验证指标极差

这是比赛中段最让人头疼的问题。训练集上的loss一直在降,但验证集上准确率死活上不去,有时还往下掉。

排查思路:先检查是否过拟合。如果验证loss和训练loss差距过大,说明模型记住了训练集特殊场景。检查数据划分是否按场景隔离。按场景隔离后如果验证指标仍然差,那就是增强强度不够,模型学到了场景的色调分布等背景特征。

最终在我们的项目里找到的原因不是过拟合,而是训练集图片的裁剪范围太小。很多训练图裁出来都是单调的地板纹理,模型学到的是“地板上没有人”的特征。换成全局场景+局部抽查的组合增强后,验证分数提升非常明显。

5.2 人群高度密集区域预测偏少

预测偏少几乎是所有人流密度模型都会遇到的问题,原因在于密度图在极度拥挤区域的峰值重叠严重,模型建模能力饱和。

处理方法有三个:

  1. 对输入图片的高密度区域进行切块推理。把原图切成2x2的小块,分别预测再合并。注意切块边缘会有“边缘效应”,即靠近切块边界的人群被低估。解决方式是切块时保留10~15%的重叠区域,合并时取重叠区域的平均值。

  2. 在训练时增加局部区域的高斯增强,让模型对密集小目标更敏感。

  3. 对极端密集样本做密度图峰值裁剪,设定密度值上限,避免网络为了优化几个极端像素而牺牲整体分布。

5.3 GPU显存不足导致训练中断

比赛过程中遇到过约三次OOM(显存溢出),都在将batch_size调到16以上时。解决方式:

  • 将输入尺寸调整为384x384,而不是512x512;
  • 开启AMP混合精度;
  • 使用梯度累积。

如果你在比赛环境里遇到OOM,建议先查这四个地方:输入图片尺寸是否过大、batch_size是否过高、是否启用了混合精度、backbone通道数是否太大。通常调整前两个就能解决问题。

5.4 模型在不同摄像头视角下的泛化问题

初赛和复赛的数据分布差距很大,初赛多为俯视视角,复赛出现了大量的平视和低角度监控画面。很多模型的初赛成绩很好,复赛却翻车了。

实际上,人群计数模型对视角高度极其敏感。训练数据里俯视视角样本多,导致模型的密度分布学习偏向于小尺度人头特征。平视视角下人头的表观特征完全不同,需要模型去适应。

一个有效的对策是在训练数据里做视角增强变换:对图像做随机透视变换(perspective transform),模拟不同摄像头高度和角度下的视角变化。这个增强方式强度不宜过大,否则会产生畸变的假样本。

5.5 比赛提交后评分与本地验证不一致

这个问题几乎是比赛必踩的坑。我们在本地验证集上有93%的准确率,但提交给官方初赛榜单只有88%。分析出来有两个原因:

第一,本地验证集的场景和初赛测试集场景重叠过少,评分波动大。解决办法是切出一部分训练数据作为本地评测,确保场景多样性。

第二,官方评测用的可能不是同一套阈值或者后处理逻辑。后来发现我们本地做过调制的全局人数校正系数,在官方口径下并不起反作用,但sigmoid输出密度图的阈值设置对官方计分有较大影响。

6. 比赛成绩提升的经验总结与碎碎念

6.1 提分最容易的五个方向

按投入产出比排序,我个人认为提分最快的方向依次是:

  1. 保证数据划分和增强策略的正确性,这个投入小收益巨大;
  2. 把损失函数从纯L2升级为L2+SSIM+局部一致性组合;
  3. 多尺度推理;
  4. 根据视角做定向增强;
  5. 后处理阈值搜索与全局人数校正。

这里面第一条看起来最简单,但很多人一开始就犯了划分错误,导致后面所有调参都建立在一个扭曲的评价指标上。如果一开始就把这些打牢,后面会省下大量时间。

6.2 关于PaddlePaddle框架的几个使用看法

比赛中实际使用的PaddlePaddle版本,动态图带来不少便利,在数据集迭代和模型调试阶段尤其好用,打印中间层输出、动态修改网络结构都很顺手。

PaddleDetection和PaddleSeg里预训练模型机制很完善,直接加载ImageNet预训练权重很简单。但框架适配比赛时有个实际问题:不少第三方修改过的模型实现采用的是TensorFlow或PyTorch代码的习惯,迁移到PaddlePaddle时要改动的地方不少,这可能才是大家用起来最耗时的地方。

建议比赛周期内尽量少魔改模型结构,不要试图在框架差异上浪费精力。除非有明确必须改的原因,否则沿用官方实现最稳。

6.3 如果再来一次,我会怎么做

如果现在再参加一次同等任务,我会把更多的精力提前投入到场景视角的分析上。很多队伍(包括我们自己)一开始都在模型结构上花了太多时间,不断换backbone、调head结构,但真正拉开差距的其实是数据理解和后处理的细致程度。

对密集人群场景,backbone的差异远不如训练数据的场景丰富度重要。后处理的细节,如区域自适应阈值、高密度区域切块、多尺度对齐策略,提升效果比换一个模型明显得多。

人群密度检测这个方向,比赛的终点往往不是模型的准确率比拼,而是工程细节和数据集理解的竞争。希望这份记录能帮后续参赛的同学少走点弯路。

最后分享一个小操作习惯,建议每次实验的配置、代码、得分都记录在表格里。后期对比模型的时候,这能帮你少跑很多无效实验。赛后回看的时候,也会更有迹可循。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询