☰
人脸活体检测比赛Top方案复现:从ACER指标到时序建模全解析
2026/10/1 14:46:55 网站建设 项目流程

CVPR2019那届ChaLearn人脸防伪挑战赛,应该是我入行以来复现过最“拧巴”的一场比赛模型。表面上比的是“能不能分清真人脸和假人脸”,实际上拼的是对成像物理过程的理解、对数据不均匀性的处理,以及对评估指标的理解深度。我当时花了两周时间,把榜单Top3公开的那份代码从头到尾拆了一遍,又花了一个多月复现、调参、迁移到自己的项目里。这篇文章就把我当时拆代码、跑模型、踩坑的全过程整理出来,重点放在“这套Top3方案到底怎么设计”“开源代码里哪些部分是真正决定成绩的细节”和“复现过程中哪些环节最容易翻车”。

这篇文章适合谁看呢?一类是准备参加人脸活体检测/防伪方向比赛的选手,可以拿它当“比赛解题套路”参考;另一类是想把学术模型落地到门禁、支付、隐私设备等真实场景的工程同学,因为Top方案里很多数据增强、评估策略、序列建模思路,在真实业务里同样管用。我会尽量少贴一堆没用的网络结构图,多讲每步为什么这么设计、代码里对应的位置在哪、跑起来之后实际是什么效果。

1. 这场挑战赛到底在比什么:任务边界与评估口径

先说清楚比赛任务本身。人脸防伪检测(Face Anti-Spoofing)在学术界的标准定义是:给定一段视频或者一张图片,判断里面的人脸是真实活体,还是用打印照片、手机屏幕重放、平板翻拍、3D面具等方式伪造的。注意,这里的“伪造”不一定是高成本的硅胶面具,更多是攻击者拿一张照片对着摄像头晃动、拿另一台手机播放目标人物视频这种低成本手段。

普遍的新手误解是把它当成一个二分类任务:输入RGB图,输出真/假。但Top3方案基本都不会这么做。原因很直接:真脸和假脸在单帧RGB上的差异太小了。一张打印照片在光线合适的时候,纹理细节和真脸几乎无法区分;手机屏幕翻拍还会引入摩尔纹、反光,但这本身就不稳定。单帧二分类模型很容易过拟合到训练集里的某种特定攻击设备,换一个屏幕、换一个打印机,立刻失效。

所以比赛排名靠前的方案,几乎都做成了“时序任务”或者“多任务任务”。Top3代码里一个很明显的特征是:输入不是单张图,而是一段连续帧序列;模型输出也不仅仅是“真/假”的置信度,还会包含深度图、rPPG信号或者纹理一致性之类的辅助预测。这个设计逻辑我后面详细拆。

再强调一下评估指标,这是理解整套代码的钥匙。ChaLearn Challenge当时主用的指标是ACER(Average Classification Error Rate),由两个指标取平均:

指标含义直观理解
APCER假脸被当成真脸的比例攻击成功率,越低越安全
BPCER真脸被当成假脸的比例误杀率,越低越好用
ACER(APCER + BPCER) / 2综合评分,排名看它

很多复现者犯的错误是只盯着分类准确率调模型,结果ACER怎么都降不下去。原因就是,在活体检测里,把假脸放进来和把真脸杀掉,是两种不同代价的错误,必须同时看。Top3方案里的阈值选择、损失函数设计,都是围绕ACER来做的,而不是“谁的准确率高谁赢”。

我当时复现完第一版,单帧模型准确率差不多能到96%,但ACER只有0.20左右,排名连前50都进不了。后来把序列建模和深度图监督加上去,准确率没涨多少(大概97%),ACER却降到了0.06上下。这个反差值得所有做比赛的人记住:指标定义什么,模型就优化什么。

2. Top3方案的一个典型骨架:帧级特征 + 时序建模 + 深度图监督

2.1 为什么双流结构是主流

如果把当时Top3的代码框架平均一下,会发现大家最终都长得很像:一个CNN主干提取帧级特征,再接一个时序建模模块,同时头上挂一个深度图回归分支。这不完全是“卷”出来的,而是三个方向的干脆妥协。

  • 帧级特征负责内容:ResNet18/34这类结构用来捕捉人脸的纹理、局部区域反光、屏幕摩尔纹等空间线索。
  • 时序模块负责连续性:真实人脸有微表情、眨眼、头部自然晃动带来的视角变化,这些在不同帧之间形成某种规律;而打印照片无论怎么动,画面深度和纹理都“冻结”着。
  • 深度图分支负责几何线索:真脸在不同角度下有合理的3D结构,平面照片/屏幕是平的,这个线索和纹理互补。

我在复现时试过去掉某个模块看效果。去掉深度图分支后,在训练集分布内的攻击类型上成绩几乎没掉,但在跨设备测试(换一个没见过的屏幕录制)时ACER直接翻倍。这说明深度图监督起的作用不是“提高当前测试集的分数”,而是让模型不要只记设备指纹,而是去学“人脸应该有立体的深度变化”这个更本质的规律。

2.2 深度图监督如何提升泛化

深度图监督最麻烦的地方不是网络结构,而是标签从哪儿来。比赛数据集中有一部分提供了真脸对应的深度图,但通常不会全部提供,更不会提供假脸的深度图(假脸应该是平面,但标注不一致)。

当时Top3开源代码里常见做法:用PRNet或者类似3D人脸重建方法,从真人脸视频自动生成伪深度标签。这个伪标签质量其实粗糙,但足够用来做辅助监督。原理上是这样:对真脸帧做3D关键点拟合,得到一张和人脸轮廓对齐的深度图;对假脸帧呢?有两种处理策略:

  • 策略A:直接把假脸的深度图标签设成全零平面,强制模型学到“真脸有起伏、假脸是平的”这个区别。
  • 策略B:用同一套PRNet对假脸也生成深度图,但损失权重降低,避免把错误的几何信息强行塞给模型。

实测下来策略A在比赛榜上更常见,也非常有效。但有个坑:深度图监督和主干特征会互相纠缠。如果深度图分支的loss权重太大,主干特征会偏向几何信息,导致在模糊、暗光环境下特征退化;如果权重太小,分支形同虚设。Top3代码里通常会控制一个0.1到0.5之间的比例,并且只在训练中后期把权重拉高,让分类分支先进化完再“教”几何纪律。

2.3 序列级融合的两种做法

时序建模部分,Top3代码里基本分成两派:3D卷积派和LSTM/GRU派。

3D卷积的优点是能通过3D卷积核直接学到时空联合特征,缺点是两个:显存开销大、数据量需求高。人脸视频帧之间变化其实很小,3D卷积动不动就上亿参数,在小数据集上非常容易过拟合。

LSTM/GRU这类循环网络的优点是参数少、对序列长度灵活,缺点是长序列依赖有衰减,而且训练时梯度在时间维度上容易爆炸或消失。

我当时复现的Top3代码用的是“折中方案”:CNN主干提取每一帧的特征,得到一组帧级特征向量,然后接一层轻量级的双向GRU,最后取最后一个时间步(或者所有时间步平均池化)接分类头。这样既保留了时序信息,又不会像3D卷积那样显存爆炸。在8GB显存的卡上,序列长度设为5,batch size开到32,完全跑得动。

3. 开源代码逐段拆解:从数据加载到损失函数

说句实话,比赛Top方案的代码质量未必都很好,但可读性通常比论文原文高——因为大家要拿它的结果去水下一篇文章,代码里往往藏着论文里不写的细节。我拆的这份代码,整体是PyTorch实现的,组织结构大致是这样:

config/ default.yaml data/ dataset.py augment.py models/ backbone.py temporal_head.py depth_head.py loss/ loss.py train.py test.py

这一节我不贴整段代码,只挑最容易被忽略却又决定成败的三个地方讲。

3.1 数据组织与样本均衡:负样本不够,怎么造

人脸防伪数据集有个天然问题:真脸视频多,假脸攻击视频少,而且攻击类型极度不平衡。打印照片可能有上千条,视频重放可能只有几百条,3D面具干脆就几十条。

Top3代码处理方式不是简单过采样,而是用了“攻击类型感知采样器”。思路是:在构造每个batch的时候,先保证每个batch里真脸样本占一半,剩下的一半从不同攻击类型里各取一部分。这样不管某类攻击样本总量多少,模型每步都能看到一个均衡的“攻击分布”。

另外,我还注意到代码里对视频做了帧偏移采样。同一段视频,训练时不是总从第0帧开始取,而是随机偏移起始位置,再连续取N帧。这相当于做了一次“时间方向的数据增强”,也让模型没见过一个固定的起始帧模式。

3.2 预处理细节与数据增强策略:关键帧选择与颜色扰动

很多复现者以为数据增强无非是随机裁剪、翻转、颜色抖动,但Top3代码里的增强明显更“算计”:

  • 人脸对齐:用MTCNN或者RetinaFace检测人脸关键点,然后按两眼位置做相似变换。这一步不做,后面所有时序稳定性都是空谈——因为人脸的晃动幅度会盖过真假脸本身的差异。
  • 随机遮挡:在帧上随机抹掉一块矩形区域。这个增强在普通分类里是用来提高鲁棒性的,在防伪检测里还有一个额外作用:逼模型不要依赖某一小块区域的纹理(比如屏幕反光点),而是综合全局信息判断。
  • 色彩通道扰动:对RGB三个通道施加不同强度的亮度偏移。真实场景里不同摄像头的白平衡差异非常大,这个增强能显著提升跨设备泛化。

我复现时加过一个“运动模糊模拟”,效果反而不理想。因为Top3代码用的是短序列(5帧),运动模糊会把时序差异抹掉,模型反而丢失了微表情这种活体线索。

3.3 核心网络与损失:一个多任务平衡游戏

这份代码的损失函数写得很精炼,核心是三部分之和:

total_loss = ce_loss + alpha * depth_loss + beta * temporal_consistency_loss

其中:

  • ce_loss:主分类头的交叉熵,区分真/假。
  • depth_loss:深度图回归的MSE损失,让深度分支输出逼近伪深度标签。
  • temporal_consistency_loss:相邻帧特征之间的L2距离约束,惩罚时序特征突变。

alpha和beta在训练过程中不是固定的。代码里用一个简单的scheduler:前10个epoch只让分类loss起作用,之后alpha从0.1线性增到0.3,beta固定0.01。这个设计很聪明,前期让主干先收敛到合理的特征空间,后期深度监督才开始纠偏,避免一开始就用粗糙的伪深度标签干扰训练。

我当时手贱,把alpha一开始就设成0.3,结果模型很快陷入“看着深度图学,把纹理忘了”的状态:训练loss很好看,验证集ACER却不降。后来按代码里的scheduler重跑,才恢复到正常水平。

4. 训练过程中的调参记录与效率优化

4.1 优化器、学习率与batch size的三角关系

这份Top3代码用的优化器是SGD(momentum=0.9, weight_decay=5e-4),初始学习率0.01,配合CosineAnnealing。好看在地方在于,它没有用花哨的AdamW或者LAMB,因为比赛模型的收敛稳定性比峰值精度更重要——ACER这种指标要求模型在两个错误率之间取得平衡,学习率太激进很容易在后期震荡,导致验证集上BPCER反复跳动。

batch size的影响比想象中大。我把batch size从32减到16后,ACER从0.06涨到0.12。原因是活体检测里每个batch必须包含足够多样的攻击样本,batch小了,攻击类型覆盖不够,梯度方向偏来偏去。这算是我复现过程中最朴素也最疼的发现。

4.2 短序列采样如何逼近长视频效果

很多论文里用16帧甚至32帧做序列建模,但算力有限。Top3代码里给出了一种“短序列长步长”的采样策略:序列长度固定在5,但相邻帧之间的间隔从1逐轮增大到4。训练前期用小间隔让模型先看到“平滑连续”的运动,后期用大间隔强制模型关注更长尺度的变化,比如眼睛从睁到闭、头部从左侧转到右侧。

这个策略直接解决了我显卡不够的问题——单卡8G显存,5帧序列老老实实跑,最终效果比一开始贪心用16帧还稳定。原理也好理解:长序列模型吃的是信息密度,不是帧数。只要帧间跨度足够,5帧里已经覆盖了“微表情-整体姿态”两个尺度的线索。

4.3 显存受限时的梯度累积与混合精度

复现过程中最烦的就是OOM。Top3这份代码结构里有三个头(分类、深度、时序),中间特征图全保存在显存里,batch size稍微调大就爆显存。

我当时的做法是梯度累积,模拟更大batch:

accumulation_steps = 4 scaler = torch.cuda.amp.GradScaler() for step, (frames, label, depth) in enumerate(dataloader): with torch.cuda.amp.autocast(): logit, depth_pred = model(frames) loss = compute_loss(logit, depth_pred, label, depth) / accumulation_steps scaler.scale(loss).backward() if (step + 1) % accumulation_steps == 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad()

混合精度在这里风险不大,因为深度图回归的MSE损失数值范围稳定,FP16精度足够用。但要注意:如果数据里存在极端亮度的帧,FP16容易溢出,表现为loss突然变成NaN。解决方法是给输入做一次标准化,把RGB像素映射到约[-1, 1]区间,而不是直接用0-255的整数除法结果。

5. 复现结果验证与落地迁移思考

5.1 复现出来的成绩与预期差异

完整跑完这套流程后,我的复现成绩是:在官方验证集上ACER约0.065,BPCER约0.05,APCER约0.08。和Top3原报告的数字(ACER 0.03-0.04区间)有一定差距。差距来源我分析有三个:

  • 我没有完全复现原代码的数据清洗环节,尤其是那些“标注错误”的样本处理,原方案里对某些明显标反的帧有手工清洗。
  • 深度图伪标签的生成参数不一样。PRNet有个关键参数是人脸框的松紧度,松紧不同,生成的伪深度图差异很大。
  • 原方案大概率做了模型融合或者多折交叉,而我只有一个单模型。

这些差异反过来给我一个提醒:比赛数字不等于方案上限,更不等于方案在真实场景的效果。复现的目标从来不是一比一还原榜单分数,而是把里面的设计思路拿出来用。

5.2 从比赛模型到真实业务系统的改动点

比赛模型挪到真实业务,最直接的落差是“相机条件变了”。比赛数据基本是网络摄像头正脸采集,真实场景可能是门禁机的俯视角度、手机的前置广角、或者监控摄像头的远距离画面。我迁移时的实际改动有三处:

第一,主干换成了MobileNetV3,因为端侧设备跑不起ResNet34。好在深度图监督和时序建模是独立于主干的结构,换主干几乎不影响其他部分。

第二,序列长度从5帧降到3帧,减少交互延迟。真实场景不可能等用户录够5帧再出结果,3帧配合帧间隔1,大概0.3秒内能出活体分数。

第三,阈值重新标定。比赛里ACER是平均两个错误率,业务上往往更重视BPCER不能太高(不能老把真用户挡在门外)。我会先固定BPCER的上限,再反推阈值,然后把剩余的压力放在攻击路径的安防策略上。

最后分享一个我个人的体会:很多人把活体检测模型当成一个“分类模型”来调,一上来就刷准确率刷F1,方向从一开始就偏了。CVPR2019这次挑战赛的Top3方案,厉害的地方不在于网络有多深多复杂,而在于它同时建模了空间纹理、时序动态和几何深度三个维度的“活体证据”,并且用一套多任务损失把它们拧成一股绳。复现这套代码之后,我自己最大的收获不是排行榜上的名次,而是搞清楚了“假脸骗过模型,到底骗的是哪条特征通道”。搞懂这一件事,比刷高零点几个点的ACER值值钱得多。

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

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

立即咨询