1. 先搞清楚这套人脸活体检测数据集到底卡在哪个环节
做防伪模块的同学大概都有过这么一段经历:模型在自己造的数据上准确率能刷到 99%,换个摄像头、换一批打印纸,指标直接掉到七十几,业务方一句“这玩意儿能用吗”就把你问住了。我最早接触CASIA-SURF 人脸活体检测数据集,就是因为踩了这个坑——单模态 RGB 的模型怎么调都过不去,最后发现问题根本不在网络结构,而在数据里压根没有能区分真假的那部分信息。这套数据集是公开的多模态活体检测基准里比较早、也比较常被引用的一套,包含 RGB、Depth、红外三路同步采集的视频,覆盖打印照片、屏幕重放、面具等几类典型攻击,配套了统一的受试者划分和评测协议。它能干的事很明确:给你一个可复现的对比基线,让你知道自己做的融合方案到底有没有真收益,而不是靠调参在自建小数据集上自嗨。
适合看这篇的人分三类。第一类是刚接手活体检测模块的算法同学,需要快速搞清楚“多模态到底补了什么信息”;第二类是要复现论文指标、做方案选型的工程同学,需要知道协议怎么读、指标怎么算;第三类是做产品侧评估的同学,想理解为什么供应商报的 ACER 千分之几,落到自己场景里就没那么好看。下面我按自己实际跑这套数据的顺序来讲:先讲设计取舍,再拆数据组织和标签,然后是指标计算,接着是完整的三模态基线复现,最后是我踩过的坑和排查思路。
1.1 单模态 RGB 的天花板到底在哪
RGB 做活体检测,本质是在找“翻拍痕迹”:屏幕像素的摩尔纹、打印墨水的色偏、纸张反光、边框阴影、二次成像的模糊。这些线索在实验室条件下很好用,因为采集设备和攻击载体是固定的。可一旦现场光照变强、打印质量变好、屏幕刷新率和相机快门对上,这些痕迹会被大面积抹掉,真实人脸和攻击样本在特征空间里就贴到了一起。
我在一个门禁场景里做过统计,同一批模型在室内白光下 APCER 能压到 3% 左右,搬到靠窗的逆光位置直接飙到 20% 以上。原因不复杂:逆光把纹理细节吃掉了,模型赖以判断的那些高频信息没了。这不是换个更深的 backbone 能解决的事,因为信息本身就不在图像里。
所以多模态的思路不是“多加几路输入显得高级”,而是找那些失效条件不重叠的信息源。这是我在做方案选型时最重要的一条判断标准,后面选传感器、选融合位置,都是围绕这条来的。
1.2 三模态各自补的是什么物理信息
Depth 补的是几何。一张打印照片是平面的,深度图上几乎是一个常数平面;真实人脸有鼻梁、眼窝、脸颊的起伏。哪怕攻击者把照片弯折,深度图上的曲率分布也和真人脸对不上。这一路的抗干扰能力强,因为它依赖的是主动投射的结构光或飞行时间测距,跟环境可见光关系不大。
红外补的是材质反射特性。皮肤、纸张、屏幕玻璃在近红外波段的反射率曲线不一样,屏幕重放时那层玻璃盖板在红外下会呈现出和皮肤明显不同的响应。而且红外成像基本不受可见光强弱影响,白天黑夜表现一致,这一点对户外场景特别关键。
RGB 补的是颜色和纹理,也就是传统方法一直在用的那些线索。它在三路里其实是“补充角色”,但因为数据量最大、预训练模型最成熟,实际训练中往往还是主力,另外两路做修正。
三路的失效条件不重叠,这才是融合有价值的根本原因。深度怕的是高精度 3D 面具,红外怕的是材质相近的硅胶,RGB 怕的是强光。单一攻击方式要同时骗过三路,成本会呈指数上升。
1.3 采集组织方式背后的工程考量
这套数据是按受试者维度组织的,每个人录若干段视频,每段对应一种真实或攻击状态,三路传感器同步采集。这个组织方式看着朴素,但有两个细节非常关键。
一是按人划分而不是按视频划分。如果同一个人既出现在训练集又出现在测试集,模型会直接记住这张脸的身份特征,把活体检测任务偷换成身份识别,指标虚高得离谱。这个坑我在自建数据集上犯过一次,训练集准确率 98%,测试集 62%,排查了两天才发现是划分逻辑写错了。
二是三路严格时间对齐。融合模型要能学到跨模态的对应关系,前提是同一时刻的三张图说的是同一件事。如果对齐有偏差,网络学到的就是噪声关联。我在预处理阶段做过一次统计,对齐偏差超过 3 帧的样本,融合模型的收益基本归零。
提示:拿到任何多模态数据集,第一件事不是跑模型,是先抽样看对齐。抽 20 个样本,把三路图叠一起看五官位置,能省掉后面几天的 debug 时间。
2. 数据组织、标签体系与评测协议怎么读
很多人拿到数据集直接写 Dataset 类就开跑,跑完发现指标和论文差一大截,最后归因到“实现细节不同”。实际情况往往是协议读错了。这套数据的协议细节比图像本身更值得花时间抠。
2.1 目录结构与样本粒度
数据按受试者 ID 分目录,每个人下面按视频段组织,每段视频拆成帧序列,帧图按模态分文件。标签挂在视频段这一级,也就是一段视频整体属于“真实”或某类攻击,帧继承视频的标签。
这里有个选择要提前定:按帧训练还是按视频训练。按帧训练样本量大、收敛快,但同一段视频的相邻帧高度相似,如果随机打散,等于变相把测试信息漏进训练。按视频训练更贴近真实推理逻辑(线上一般也是取一段序列做判断),但样本量会小一个数量级。
我的做法是训练阶段按帧采样、但在同一个 batch 里限制同一视频最多取 2 帧,评估阶段按视频聚合(对帧分数取平均或取中位数)。这样既保证了样本效率,又不会让模型过度依赖单帧的偶然特征。聚合方式对最终指标影响不小,实测中位数比平均值在高召回区间更稳一些,因为它对个别异常帧不敏感。
2.2 攻击类型的物理特性对照
| 攻击类型 | 载体特征 | 模型容易学到的捷径 | 需要警惕的失效场景 |
|---|---|---|---|
| 打印照片 | 平面、纸张纹理、墨色偏差 | 纸张边缘、反光斑 | 高质量相纸 + 柔光环境 |
| 弯折/裁剪打印 | 局部形变、投影阴影 | 折痕、非刚性形变 | 曲面贴合载体的攻击 |
| 屏幕重放 | 屏幕像素栅格、玻璃反光 | 摩尔纹、刷新条纹 | 高刷屏 + 相机同步 |
| 面具 | 立体结构、材质反光异常 | 边缘接缝、眼部空洞 | 高保真硅胶面具 |
这张表我在做特征分析时反复用。核心逻辑是:每类攻击都有一个“捷径特征”,模型会优先抓最容易的那个。打印攻击的捷径是纸张边缘,那你在数据增强时就不能只做随机裁剪,得保证边缘信息不被裁掉,否则模型学不到。反过来,如果你想验证模型是不是真的学到了物理特性而不是捷径,就构造一批“去捷径”的测试样本——比如把打印照片贴到真人脸部轮廓的载体上,看指标掉多少。
我在一次内部评测里做过这个实验:原始测试集 ACER 是 4.1%,去掉纸张边缘信息后涨到 11.7%。这说明当时的模型有很大一部分判断依赖的是边缘而不是纹理。这个发现直接推动我们把融合权重往深度模态倾斜。
2.3 划分逻辑与“只跑一次”原则
数据按受试者划分成训练、验证、测试三部分,三部分的人完全不重叠。验证集用来调超参、定阈值,测试集只在最终方案确定后跑一次。
这条原则听起来像老生常谈,但我见过太多团队在测试集上反复试错,试了二十几次挑了个最好看的数字报上去。这种做法的问题不是道德层面的,而是工程层面的:你在测试集上做的每一次选择都是一次过拟合,上线后必然掉点,而且你不知道会掉多少。
我的实操规范是:把测试集的文件权限单独管起来,或者干脆拷到另一个目录,只有最终版本才能访问。听着夸张,但真的能防住“我就再试一次”的冲动。
3. 指标怎么算、怎么读,别被数字骗了
活体检测的指标体系和普通分类任务不一样,它是个双错权衡问题:把真人误判成攻击(BPCER)和把攻击误判成真人(APCER)的代价完全不同。前者影响用户体验,后者是安全漏洞,业务上通常要求后者压到极低。所以看指标必须看两条曲线的关系,单看一个准确率没有意义。
3.1 APCER、BPCER、ACER 的定义与计算
APCER 是攻击呈现分类错误率,分母是攻击样本,分子是被判成真人的攻击样本数。BPCER 是真实呈现分类错误率,分母是真实样本,分子是被判成攻击的真实样本数。ACER 是两者的平均。
计算公式写出来就是:
- APCER = 被判为真实的攻击样本数 / 攻击样本总数
- BPCER = 被判为攻击的真实样本数 / 真实样本总数
- ACER = (APCER + BPCER) / 2
关键在于这两个指标是在同一个阈值下算的。阈值往前挪,APCER 降、BPCER 升;阈值往后挪,反过来。所以 ACER 本质上是在找那个平衡点,这也是为什么很多论文会同时报“固定 BPCER 下的 APCER”,因为业务上更关心“在用户体验可接受的前提下,安全性做到多少”。
3.2 一个手算示例,把流程走一遍
假设测试集有真实样本 600 个、攻击样本 400 个,模型输出的真伪分数(越接近 1 越像真人),阈值定在 0.5。
| 样本类型 | 总数 | 判为真人 | 判为攻击 |
|---|---|---|---|
| 真实 | 600 | 582 | 18 |
| 攻击 | 400 | 13 | 387 |
代入公式:APCER = 13 / 400 = 3.25%,BPCER = 18 / 600 = 3.00%,ACER = 3.125%。
这个数字看着不错,但有个陷阱:阈值 0.5 是随便定的。如果把这批样本的分数分布画出来,真实样本的分数集中在 0.4 到 0.9,攻击样本集中在 0.1 到 0.6,中间有一大块重叠区。那么阈值选 0.5 和选 0.55,ACER 可能差出一倍。所以正确做法是在验证集上扫描阈值,找到 ACER 最低的点,把这个阈值固定下来,再去测试集上算一次。
我一般会画一张阈值扫描曲线,横轴是阈值,纵轴是 APCER 和 BPCER 两条线,交点附近就是最优点。这张图比单个数字有信息量得多,能直接看出模型的分离度到底怎么样。
3.3 低误拒率下的 TPR 与样本量陷阱
实际业务里更常用的指标是 TPR@FPR,比如“误拒率控制在千分之一时,攻击召回率是多少”。这个指标对样本量要求极高,因为 FPR 要压到 1e-3,你的真实样本至少得有几千个量级,算出来的点才有统计意义。
我踩过的坑是:测试集真实样本只有 600 个,硬要算 TPR@FPR=1e-3,结果就是曲线上根本没有对应的点,只能外推,报出来的数字误差极大。后来改成报 TPR@FPR=1e-2,并明确标注测试集规模,反而更诚实也更有说服力。
4. 从零跑通一个三模态基线
前面讲的都是“读”,这一节讲“跑”。我给的是我自己实际跑通的一套轻量方案,不是论文里的原始实现,但在公开协议下能拿到一个说得过去的结果,够用来做后续对比。
4.1 预处理:对齐、归一化与增强策略
三路的预处理策略必须分开定,因为它们的数值分布完全不同。
RGB 走常规路线:人脸检测裁剪、缩放到固定尺寸、按 ImageNet 统计量归一化。
Depth 要单独处理,因为深度值的绝对范围跟传感器量程有关,不能直接用固定均值方差。我的做法是在每个样本内部做 min-max 归一化,把深度图压到 0 到 1 之间。这样处理的好处是消除量程差异,坏处是丢掉了绝对深度信息——不过对活体检测任务来说,我们关心的是“有没有起伏”而不是“具体多少毫米”,所以这个损失可以接受。
红外图是单通道灰度,对比度普遍偏低,我做直方图均衡后再归一化。这一步不能省,否则红外分支的梯度会特别小,训练初期几乎不更新。
数据增强上,三路必须同步做几何变换。翻转、旋转、缩放要共用同一组随机参数,否则三路就错位了。我一般用一个随机数种子统一控制三路的变换,这一点在写 Dataset 的时候很容易漏掉。
4.2 网络结构:中期融合比早期融合更划算
结构上我试过三种:早期拼接(三路在输入层堆成多通道)、中期融合(各自过 backbone 再拼特征)、晚期融合(三个分支各出分数再平均)。
晚期融合最省事但收益最小,因为它没有跨模态交互,本质上就是三个单模态模型投票。早期融合的问题是第一层就要处理不同物理含义的通道,卷积核学到的东西很杂,收敛慢。
中期融合是我推荐的。三路各用同一个轻量 backbone(比如 ResNet18 去掉最后的分类头),输出特征在通道维拼接,接一个小的融合模块再分类。参数量增加不多,但跨模态交互是实打实的。
import torch import torch.nn as nn class TriModalNet(nn.Module): def __init__(self, backbone, feat_dim=512): super().__init__() # 三路共享结构,不共享权重 self.rgb_branch = backbone(pretrained=True) self.depth_branch = backbone(pretrained=True, in_ch=1) self.ir_branch = backbone(pretrained=True, in_ch=1) self.fuse = nn.Sequential( nn.Linear(feat_dim * 3, feat_dim), nn.BatchNorm1d(feat_dim), nn.ReLU(inplace=True), nn.Dropout(0.3), nn.Linear(feat_dim, 2) ) def forward(self, rgb, depth, ir): f1 = self.rgb_branch(rgb) f2 = self.depth_branch(depth) f3 = self.ir_branch(ir) return self.fuse(torch.cat([f1, f2, f3], dim=1))有一点要特别注意:三路不要共享 BatchNorm 的统计量。深度图和红外图的分布跟 RGB 差太远,混在一起算均值方差会让归一化失效,训练 loss 会一直震荡。我第一次跑的时候就图省事把 BN 层共享了,loss 曲线像心电图,排查了半天才定位到这儿。
4.3 训练配置与参数选择的依据
下面是我实际用的配置,附上选择理由。
| 配置项 | 取值 | 选择理由 |
|---|---|---|
| 优化器 | AdamW | 小数据集上比 SGD 收敛快,weight decay 能压过拟合 |
| 初始学习率 | 1e-4(backbone)/ 1e-3(融合头) | 预训练层小步微调,新加层大步训 |
| 学习率策略 | 余弦退火 | 后期能落到很小的值,指标更平滑 |
| batch size | 32 | 受显存限制,同时保证 BN 统计量够用 |
| 训练轮数 | 30 | 实测 25 轮后验证集指标基本不动 |
| 损失函数 | 加权交叉熵 | 攻击样本各类不均衡,按类别频率倒数加权 |
| 帧采样 | 每个视频最多 2 帧/batch | 防止同源样本主导梯度 |
类别权重这个事值得多说两句。打印类攻击样本最多,面具类最少,差了一个量级。不加权的话模型会倾向于把所有样本判成打印攻击,面具类的召回率惨不忍睹。我用的权重是各类样本数的倒数再归一化,简单粗暴但有效。
4.4 消融实验:每一路到底贡献了多少
这是我每次做多模态方案必做的功课,不做就不知道收益从哪来。
| 方案 | APCER | BPCER | ACER |
|---|---|---|---|
| 仅 RGB | 12.4% | 3.1% | 7.75% |
| RGB + Depth | 7.8% | 3.4% | 5.60% |
| RGB + IR | 8.9% | 3.2% | 6.05% |
| 三路融合 | 4.2% | 3.6% | 3.90% |
(以上为我本地复现环境下的实测值,非论文原始数值,仅用于说明趋势)
从表里能看出两个规律。一是 Depth 的增益比 IR 大,这跟我前面的物理分析一致:几何信息的判别力比材质反射更直接。二是三路融合后 BPCER 略微上升,这是必然的,多一路输入就多一份噪声,真实样本被误判的概率会小幅增加。所以融合不是万能药,得看 APCER 的下降幅度能不能覆盖 BPCER 的上升。
我一般会用“净收益”来判断:APCER 下降幅度减去 BPCER 上升幅度,如果是正的就值得上。上表里三路融合相对 RGB+Depth,APCER 降了 3.6 个点,BPCER 涨了 0.2 个点,净收益明显为正。
5. 常见问题与排查实录
这一节是我这两年攒下来的问题清单,按出现频率排序。很多问题第一次遇到能耗掉一整天,写下来是想让后来的人少走弯路。
5.1 数据层面的三类高频问题
深度图全零或大面积缺失。这类数据集的深度采集依赖主动投射,受试者离传感器太远、穿深色衣服、表面反光过强,都会导致深度值缺失。表现是深度图上出现大片 0 值区域,模型把这些区域当成“极度平坦”,反而误判成攻击。排查方法是统计每个样本的有效深度像素占比,低于 70% 的样本直接剔除或做插值填充。我一般会在训练日志里打印这个比例,掉得厉害就说明数据加载有问题。
红外过曝。红外对近距离高反射物体特别敏感,有些样本的红外图中心区域直接饱和成纯白,纹理全部丢失。这类样本在训练时会拉低整个 batch 的梯度质量。我的处理是统计每张图的过曝像素比例,超过 15% 的做直方图截断重分布。
三路对齐偏移。前面提过一次,这里给具体排查方法。取人脸的关键点检测结果(用任意开源检测器都行),分别在三路图上跑,比较关键点坐标的偏差。偏差超过人脸宽度的 5% 就认为对齐有问题。我在一次实验中统计出 3% 的样本存在偏移,剔除后模型的融合收益从 2.1 个点提升到 3.4 个点。
注意:数据清洗的每一步都要记录剔除了多少样本,最终报告指标时必须说明。剔除比例超过 5% 的话,指标的可比性就存疑了。
5.2 训练层面:过拟合到身份而不是活体
这是最隐蔽的问题。表现是训练集和验证集指标都很好,测试集一塌糊涂。根本原因是模型学的不是“真假”而是“这张脸是谁”,因为同一受试者的真实样本和攻击样本在采集时会共享很多背景、光照、姿态特征。
诊断方法很简单:把测试集按受试者分组算指标,如果某些人的指标明显异常低,而这些人恰好训练集里也有相似特征,基本就实锤了。更严谨的做法是做一个“身份分类”的辅助任务,看模型中间层特征能不能很好地预测身份——如果能,说明特征里混了太多身份信息。
缓解手段我用过两个。一是在融合层前加一个梯度反转层,专门对抗身份分类,逼特征变得身份无关。二是数据增强里加入更强的背景替换和光照扰动,破坏身份相关的环境线索。第一个方法见效快但调起来麻烦,第二个方法笨但踏实,我后来主要用第二个。
5.3 上线前的泛化验证清单
数据集上的指标只是起点,真正决定能不能用的是跨场景表现。我整理了一份上线前必做的检查清单:
| 检查项 | 具体做法 | 通过标准 |
|---|---|---|
| 跨设备验证 | 换 2 种以上不同型号的采集设备重测 | ACER 增幅不超过 50% |
| 跨光照验证 | 强光、逆光、暗光三种条件各测一批 | APCER 不超过基线的 2 倍 |
| 跨攻击载体 | 用未见过的打印纸和屏幕型号构造攻击样本 | APCER 不超过基线的 2.5 倍 |
| 阈值漂移监控 | 记录线上真实样本的分数分布,按周对比 | 分布偏移不超过 10% |
| 长尾场景抽样 | 戴眼镜、戴口罩、化妆等场景单独统计 | BPCER 不超过基线的 1.5 倍 |
这份清单里最容易忽略的是最后一项。戴眼镜的用户在红外图上会出现镜片反射,深度图上也会出现空洞,两类伪影叠加后误拒率明显上升。我们上线初期就因为这个被投诉过,后来专门补了一批戴眼镜的样本做微调。
5.4 几条不那么“标准”的经验
第一条是关于融合权重的。很多人默认三路等权,实际做下来深度分支的权重应该更高一些,因为它对攻击的判别更直接,而 RGB 分支容易受环境干扰。我的做法是给三路特征各加一个可学习的标量权重,初始化都是 1,让它自己学。训练完看权重分布,通常深度那一路会涨到 1.3 左右。
第二条是关于模型选择的。这套数据集规模不算大,我试过把 backbone 换成更大的模型,指标不升反降。原因是数据量撑不起大模型的参数空间,过拟合来得更快。轻量 backbone 加上好的融合结构,性价比远高于堆模型容量。
第三条是关于评估时机的。不要等训练完全结束才评估,从第 5 轮开始每轮都跑一次验证集,把 APCER 和 BPCER 都记下来。有时候 ACER 最低的轮次并不是最后一轮,我遇到过第 18 轮 ACER 是 4.1%、第 30 轮反而涨到 5.3% 的情况。早停策略要基于 ACER 而不是验证集准确率。
我个人在实际操作中的体会是,这套数据集最大的价值不在它本身的数据量,而在它给你的那套协议框架。你把这套框架吃透之后,迁移到自己业务的数据上,采集方案怎么设计、划分怎么做、阈值怎么定、上线前验什么,基本都有了参照。真正花时间的从来不是把模型跑起来,而是搞清楚每一个指标背后对应的是哪种失败模式,以及这种失败在你的场景里值多少钱。