人脸识别这套东西,我从最早拿开源模型跑通第一张图,到后来在几个实际项目里做门禁、做签到、做相册聚类,踩过的坑不算少。但真正让我意识到“准确率”这三个字有多容易被误读的,是有一次帮朋友看一个考勤系统——他跟我说“模型准确率99%”,结果上线第一天就有人刷不开门,也有人拿照片就混进去了。问题出在哪?出在他把“模型在测试集上的分类准确率”直接当成了“业务场景下的识别准确概率”。这两者之间差着十万八千里。
所以这篇东西,我想把“人脸识别准确概率计算”这件事从头到尾掰开讲清楚。它不是一个公式能搞定的事,而是从相似度度量、阈值选择、数据集划分、指标计算到业务校准的一整条链路。适合谁看?如果你正在做人脸相关的项目,不管是门禁、考勤、相册聚类还是活体检测,只要你需要向别人解释“你这套系统到底有多准”,或者你需要自己算出一个能写进报告、能说服自己的数字,那这篇就是给你准备的。我会尽量用大白话把每个环节讲透,该给的公式、代码、参数计算过程一个不落,让你看完能直接上手复现。
1. 先搞清楚“准确概率”到底在算什么
很多人一上来就问“准确率多少”,但这个问题本身是模糊的。人脸识别里至少有三种完全不同的“准确”含义,混在一起谈就会鸡同鸭讲。我先把这三个层次拆开,后面所有的计算都建立在这个分类之上。
1.1 三个层次:验证、识别、检测
人脸验证(Verification)回答的是“这两张脸是不是同一个人”,是一个一对一的二分类问题。典型场景是刷脸支付、手机解锁。它的核心指标是误拒率(FRR)和误受率(FAR),以及由这两个衍生出来的等错误率(EER)。
人脸识别(Identification)回答的是“这张脸是谁”,是一个一对多的检索问题。典型场景是门禁里“刷脸开门并记录是谁”、相册里“把同一个人的照片聚到一起”。它的核心指标是首位命中率(Rank-1 Accuracy)、Top-K命中率。
人脸检测(Detection)回答的是“画面里有没有人脸、在哪”,是识别的前置步骤。它的指标是召回率、精确率、IoU那一套,跟识别准确率是两码事,但检测漏了人,后面识别再准也没用。
我见过太多项目把这三个混着说。比如一个门禁系统,检测阶段漏检了,用户刷三次才开一次门,但后台报告写的是“识别准确率99.2%”——这个99.2%其实只是识别阶段的Rank-1,跟用户体验到的“能不能顺利开门”根本不是一回事。所以第一步,你必须明确你要算的是哪一层的概率。
1.2 为什么“准确率”这个词本身就有坑
假设你有1000对人脸,其中990对是同一个人的不同照片(正样本对),10对是不同人(负样本对)。如果模型无脑判断“所有对都是同一个人”,那准确率就是990/1000 = 99%。这个数字好看吧?但它的FAR是100%——任何两个不同的人都会被判成同一个。这就是类别不平衡带来的准确率陷阱。
人脸识别场景里,正负样本的比例往往极度不均衡。真实门禁场景中,同一个人的比对请求远多于不同人的比对请求,但安全上真正要防的是“不同人被判成同一人”(误受)。所以单纯看准确率毫无意义,必须把FAR和FRR分开看,并且根据业务场景决定你更在意哪一个。
提示:任何只给你一个“准确率”数字而不说明FAR/FRR、不说数据集构成的人脸识别方案,都要打一个大大的问号。
1.3 相似度分数:一切计算的起点
现代人脸识别模型(不管是基于ArcFace、CosFace还是其他度量学习方案)输出的不是“是/否”,而是一个相似度分数。最常见的是两张人脸特征向量的余弦相似度,取值范围在-1到1之间(实际人脸场景中通常在0到1之间)。
计算过程是这样的:模型把人脸图片映射成一个512维(或128维、256维)的浮点向量,然后对两个向量做归一化,再算点积:
import numpy as np def cosine_similarity(feat1, feat2): # 先做L2归一化 f1 = feat1 / np.linalg.norm(feat1) f2 = feat2 / np.linalg.norm(feat2) # 点积即为余弦相似度 return np.dot(f1, f2)这个分数越高,说明两张脸越像。但“多高才算同一个人”?这就需要一个阈值(threshold)。阈值定得高,误受少但误拒多(严格);阈值定得低,误拒少但误受多(宽松)。准确概率的计算,本质上就是在不同阈值下,统计FAR和FRR的变化,然后找到符合业务要求的那个工作点。
2. 数据集准备:算得准的前提是分得对
我见过最离谱的一个案例,有人把训练集和测试集混在一起算准确率,结果报出99.9%,实际上模型早就见过这些脸了。数据集划分这件事,看起来基础,但它是整个准确概率计算的地基,地基歪了后面全白算。
2.1 训练集、验证集、测试集的正确切法
标准做法是三份:
- 训练集(Train):用来训练模型参数。
- 验证集(Val):用来调超参数、选阈值。
- 测试集(Test):只在最后评估时用一次,绝对不能参与任何调参。
关键点在于:划分必须按“人”来分,而不是按“图片”来分。也就是说,如果张三有20张照片,这20张必须全部落在同一个集合里。如果张三的10张在训练集、10张在测试集,那测试集对模型来说就是“见过的人”,准确率会虚高。这个错误叫数据泄漏(Data Leakage),是人脸识别评估里最常见的坑。
import random from collections import defaultdict def split_by_identity(image_list, train_ratio=0.7, val_ratio=0.15): # image_list: [(identity_id, image_path), ...] id_to_images = defaultdict(list) for identity, path in image_list: id_to_images[identity].append(path) identities = list(id_to_images.keys()) random.seed(42) random.shuffle(identities) n = len(identities) n_train = int(n * train_ratio) n_val = int(n * val_ratio) train_ids = identities[:n_train] val_ids = identities[n_train:n_train + n_val] test_ids = identities[n_train + n_val:] def collect(ids): result = [] for i in ids: result.extend(id_to_images[i]) return result return collect(train_ids), collect(val_ids), collect(test_ids)2.2 验证对(Verification Pair)的构造方法
要算FAR和FRR,你需要构造正负样本对。正样本对是同一个人的两张不同照片,负样本对是不同人的两张照片。构造时有几个讲究:
正样本对的数量:如果某个人只有1张照片,那他就无法参与正样本对构造。所以数据集里每个人至少要有2张照片。实际项目中,我一般要求每个人至少5张,这样正样本对才有统计意义。
负样本对的采样策略:负样本对不能随便采。如果随机采,大部分负样本对都是“一看就不像”的(比如不同性别、不同年龄段),模型轻松就能分开,算出来的FAR会偏低,给你虚假的安全感。正确做法是难负样本挖掘(Hard Negative Mining)——优先选那些相似度分数较高的负样本对,这些才是真正考验模型的部分。
正负样本比例:理论上应该按照真实场景的比例来。但真实场景中负样本对远多于正样本对(因为不同人的组合数远大于同一个人的组合数),如果完全按真实比例,正样本对会少到统计不稳定。常见做法是构造1:1到1:10之间的比例,然后在报告里明确说明。
| 样本对类型 | 构造方式 | 数量建议 | 注意事项 |
|---|---|---|---|
| 正样本对 | 同一人不同照片两两组合 | 至少5000对 | 避免用连续帧(太像) |
| 负样本对(随机) | 不同人随机组合 | 与正样本对1:1 | 容易,FAR偏低 |
| 负样本对(难例) | 选相似度高的不同人对 | 单独统计 | 更接近真实风险 |
2.3 跨数据集评估的必要性
在自己划分的测试集上算出来的数字,只能说明模型在这个数据分布下的表现。真实场景中,光照、角度、遮挡、摄像头型号都跟训练数据不一样。所以我强烈建议至少在一个外部数据集上再算一遍。如果两个数据集上的EER差距超过3个百分点,说明模型的泛化能力有问题,上线要谨慎。
注意:外部数据集的选择要跟你的业务场景接近。做门禁的,就找类似门禁摄像头拍的数据;做手机解锁的,就找类似前置摄像头的数据。用一个人脸证件照数据集去评估门禁场景,参考价值有限。
3. 核心指标计算:从相似度到准确概率
这一节是重头戏。我会把FAR、FRR、EER、TAR@FAR这几个核心指标的计算过程一步步写出来,包括阈值扫描的完整代码。你照着跑一遍,就能得到自己模型的准确概率曲线。
3.1 FAR与FRR的定义与计算
FAR(False Acceptance Rate,误受率):不同人被判定为同一个人的比例。公式是:
FAR = 被错误接受的负样本对数 / 负样本对总数
FRR(False Rejection Rate,误拒率):同一个人被判定为不同人的比例。公式是:
FRR = 被错误拒绝的正样本对数 / 正样本对总数
给定一个阈值t,相似度分数大于等于t判为“同一人”,小于t判为“不同人”。遍历所有样本对,就能算出这个阈值下的FAR和FRR。
def compute_far_frr(genuine_scores, impostor_scores, threshold): """ genuine_scores: 正样本对的相似度分数列表 impostor_scores: 负样本对的相似度分数列表 threshold: 当前阈值 """ # 正样本对:分数 < 阈值 视为误拒 false_rejects = sum(1 for s in genuine_scores if s < threshold) frr = false_rejects / len(genuine_scores) # 负样本对:分数 >= 阈值 视为误受 false_accepts = sum(1 for s in impostor_scores if s >= threshold) far = false_accepts / len(impostor_scores) return far, frr这里有个细节:边界值怎么算。分数正好等于阈值时,算接受还是拒绝?这个要在代码里统一,通常用>=算接受。不同实现可能不一样,但同一份报告里必须一致,否则数字对不上。
3.2 阈值扫描与EER的求解
EER(Equal Error Rate)是FAR等于FRR时的那个值。它不是一个可以直接算出来的数,而是通过扫描一系列阈值,找到FAR和FRR最接近相等的那个点,然后取两者的平均值(或插值)。
import numpy as np def compute_eer(genuine_scores, impostor_scores, n_thresholds=10000): genuine_scores = np.array(genuine_scores) impostor_scores = np.array(impostor_scores) # 阈值范围:从所有分数的最小值到最大值 all_scores = np.concatenate([genuine_scores, impostor_scores]) thresholds = np.linspace(all_scores.min(), all_scores.max(), n_thresholds) far_list = [] frr_list = [] for t in thresholds: frr = np.mean(genuine_scores < t) far = np.mean(impostor_scores >= t) far_list.append(far) frr_list.append(frr) far_list = np.array(far_list) frr_list = np.array(frr_list) # 找FAR和FRR差值最小的点 diff = np.abs(far_list - frr_list) idx = np.argmin(diff) eer = (far_list[idx] + frr_list[idx]) / 2 eer_threshold = thresholds[idx] return eer, eer_threshold, thresholds, far_list, frr_list阈值扫描的粒度:上面代码用了10000个点。粒度太粗,EER会不准;粒度太细,计算量大但提升有限。我的经验是10000个点足够,再细就是浪费。如果样本对数量很少(比如几百对),可以适当减少到1000个点。
EER的解读:EER越低越好。业界一般认为,EER在1%以下算优秀,1%-3%算良好,3%-5%算可用,超过5%就要慎重了。但这不是绝对的,还要看具体场景。比如一个内部相册聚类功能,EER 5%也能接受;但一个金融级的人脸验证,EER必须压到0.1%以下。
3.3 TAR@FAR:业务场景下更有用的指标
EER有个问题:它假设FAR和FRR同等重要。但真实业务里,两者往往不对等。比如门禁场景,误拒(自己人刷不开)只是体验问题,误受(陌生人混进去)是安全问题。这时候我们更关心的是:在FAR固定为某个可接受的值时,TAR(True Acceptance Rate,即1-FRR)能到多少。
这就是TAR@FAR指标。比如TAR@FAR=1e-4,意思是“当误受率控制在万分之一时,通过率是多少”。这个指标比EER更贴近实际。
def compute_tar_at_far(genuine_scores, impostor_scores, target_far): genuine_scores = np.array(genuine_scores) impostor_scores = np.array(impostor_scores) # 找到使FAR <= target_far的最大阈值 # 因为FAR随阈值增大而减小,所以从高往低找 thresholds = np.linspace(impostor_scores.max(), impostor_scores.min(), 100000) for t in thresholds: far = np.mean(impostor_scores >= t) if far <= target_far: tar = np.mean(genuine_scores >= t) return tar, t, far return 0.0, None, Nonetarget_far怎么选:这个要根据业务风险来定。我一般这样建议:
| 业务场景 | 建议target_far | 理由 |
|---|---|---|
| 手机解锁 | 1e-3 ~ 1e-4 | 安全与体验平衡 |
| 门禁通行 | 1e-4 ~ 1e-5 | 安全优先 |
| 金融验证 | 1e-5 ~ 1e-6 | 极高安全要求 |
| 相册聚类 | 1e-2 | 体验优先,误聚可手动改 |
3.4 Rank-1与Top-K:一对多识别的指标
如果你做的是“这张脸是谁”的识别任务,那指标就不是FAR/FRR了,而是Rank-K命中率。做法是:拿一张查询图片,跟底库里所有注册人脸算相似度,按分数从高到低排序。如果正确身份排在第一位,就是Rank-1命中;如果排在前K位,就是Rank-K命中。
def compute_rank_k(query_feats, query_labels, gallery_feats, gallery_labels, k=1): correct = 0 total = len(query_feats) for i in range(total): q_feat = query_feats[i] q_label = query_labels[i] # 算跟底库所有人的相似度 sims = [] for j in range(len(gallery_feats)): sim = cosine_similarity(q_feat, gallery_feats[j]) sims.append((sim, gallery_labels[j])) # 按相似度降序排序 sims.sort(key=lambda x: x[0], reverse=True) # 看前K个里有没有正确身份 top_k_labels = [label for _, label in sims[:k]] if q_label in top_k_labels: correct += 1 return correct / totalRank-1和FAR/FRR的关系:Rank-1命中率其实隐含了一个“阈值”概念——它假设底库里一定存在正确的人。如果底库里根本没有这个人(开放集识别),那Rank-1就不适用了,得用开集识别指标(比如DIR@FAR)。这个后面在常见问题里会展开。
4. 实操全流程:从零算出一份可信的准确概率报告
前面讲了原理和代码片段,这一节我把它们串起来,给你一个完整的、可以直接跑的流程。我会用一个模拟项目来演示,假设我们有一个自己的人脸数据集,要算出一份包含EER、TAR@FAR、Rank-1的报告。
4.1 环境准备与依赖安装
我用的环境是Python 3.9,主要依赖numpy、scikit-learn、matplotlib。人脸特征提取部分,我用一个通用的预训练模型来演示(实际项目中你用自己的模型即可)。
pip install numpy scikit-learn matplotlib如果你要自己提取特征,还需要深度学习框架。但特征提取不是这篇的重点,我假设你已经有了特征向量。没有的话,可以用任意开源人脸识别模型跑一遍,把特征存成npy文件。
4.2 特征提取与相似度矩阵构建
假设我们已经有了一个特征文件,格式是(N, 512)的numpy数组,以及对应的身份标签(N,)。
import numpy as np # 加载特征和标签 features = np.load('face_features.npy') # shape: (N, 512) labels = np.load('face_labels.npy') # shape: (N,) # L2归一化 features = features / np.linalg.norm(features, axis=1, keepdims=True) # 构建相似度矩阵(N x N) sim_matrix = np.dot(features, features.T) # 构建标签匹配矩阵:同一个人为True label_matrix = labels[:, None] == labels[None, :] # 去掉对角线(自己跟自己比没意义) np.fill_diagonal(sim_matrix, -np.inf) np.fill_diagonal(label_matrix, False)为什么要去掉对角线:自己跟自己比相似度必然是1,如果不去掉,正样本对的分数会被拉高,EER虚低。这个坑我踩过,当时算出来EER只有0.3%,高兴了半天,后来发现是对角线没去。
4.3 正负样本对的提取与分数统计
# 提取正样本对分数(上三角,避免重复) triu_indices = np.triu_indices_from(sim_matrix, k=1) all_sims = sim_matrix[triu_indices] all_matches = label_matrix[triu_indices] genuine_scores = all_sims[all_matches] impostor_scores = all_sims[~all_matches] print(f"正样本对数量: {len(genuine_scores)}") print(f"负样本对数量: {len(impostor_scores)}") print(f"正样本对分数均值: {genuine_scores.mean():.4f}") print(f"负样本对分数均值: {impostor_scores.mean():.4f}")样本对数量的问题:如果N=10000,那负样本对数量大约是5000万,计算量很大。实际中我会对负样本对做下采样,比如随机抽100万对。但正样本对不能下采样,因为本来就少。下采样后要在报告里注明。
4.4 完整评估脚本与结果解读
把前面的函数串起来:
# 计算EER eer, eer_threshold, thresholds, far_list, frr_list = compute_eer( genuine_scores, impostor_scores ) print(f"EER: {eer*100:.4f}%") print(f"EER对应阈值: {eer_threshold:.4f}") # 计算TAR@FAR for target_far in [1e-2, 1e-3, 1e-4, 1e-5]: tar, t, actual_far = compute_tar_at_far( genuine_scores, impostor_scores, target_far ) print(f"TAR@FAR={target_far:.0e}: TAR={tar*100:.4f}%, " f"阈值={t:.4f}, 实际FAR={actual_far:.2e}")跑出来的结果大概长这样(我用模拟数据演示):
EER: 1.2345% EER对应阈值: 0.3821 TAR@FAR=1e-02: TAR=99.1234%, 阈值=0.4512, 实际FAR=9.87e-03 TAR@FAR=1e-03: TAR=97.5678%, 阈值=0.5234, 实际FAR=9.12e-04 TAR@FAR=1e-04: TAR=94.3210%, 阈值=0.5891, 实际FAR=8.76e-05 TAR@FAR=1e-05: TAR=89.6543%, 阈值=0.6423, 实际FAR=7.23e-06怎么解读这份报告:EER 1.23%说明模型整体不错。但真正决定上线的是TAR@FAR。如果这是个门禁项目,我要求FAR不超过1e-4,那TAR是94.32%——意味着100个自己人刷脸,大约有5.7个需要刷第二次。这个体验能不能接受,要跟产品经理确认。如果接受不了,要么换模型,要么加活体检测+多模态(比如人脸+刷卡)。
提示:报告里一定要写清楚样本对数量、数据集来源、阈值。只给一个EER数字的报告是不合格的。
5. 常见问题与排查技巧实录
这一节是我这些年踩坑的总结。很多问题在标准教程里不会讲,但实际做项目一定会遇到。
5.1 为什么我的EER低得离谱
如果你算出来EER低于0.1%,先别高兴,检查这几件事:
数据泄漏:训练集和测试集有没有按人划分?如果同一个人同时出现在两边,EER会虚低。检查方法很简单:看测试集的身份ID有没有在训练集里出现过。
对角线没去:前面提过,自己跟自己比相似度是1,不去对角线正样本分数会虚高。
正负样本对比例失衡:如果负样本对里全是“一看就不像”的简单样本,FAR会偏低。检查方法:看负样本对分数的分布,如果大部分集中在0附近,说明太简单了。
图片质量过高:如果测试集全是高清正脸证件照,而实际场景是监控摄像头,那EER再低也没用。检查方法:看测试集图片的分辨率、光照、角度分布。
5.2 阈值到底怎么选
阈值选择没有标准答案,但有方法论:
- 先定业务约束:FAR不能超过多少?FRR不能超过多少?
- 在约束下找最优阈值:用TAR@FAR的方法,找到满足FAR约束的最大TAR对应的阈值。
- 留安全余量:实际部署时,阈值要比计算值稍微严格一点(高一点),因为真实场景的负样本比测试集更难。
- 支持动态调整:上线后根据实际数据持续调整。我一般会留一个后台开关,能随时改阈值。
| 场景 | 建议阈值策略 | 备注 |
|---|---|---|
| 高安全 | FAR优先,阈值偏高 | 宁可误拒,不可误受 |
| 高体验 | FRR优先,阈值偏低 | 宁可误受,不可误拒 |
| 平衡 | EER对应阈值 | 作为基准线 |
| 动态 | 根据时段/风险调整 | 如夜间更严格 |
5.3 跨场景准确率下降怎么办
在自己数据集上EER 1%,换到实际场景EER 5%,这是常态。原因通常是域偏移(Domain Shift)——训练数据分布和实际数据分布不一致。解决办法:
数据层面:收集实际场景的数据,哪怕少量,加入训练或做微调。这是最有效的办法。
算法层面:用域适应(Domain Adaptation)技术,或者用更强的数据增强(模拟实际场景的光照、模糊、角度)。
工程层面:在实际场景中加质量过滤——如果检测到人脸质量太差(太模糊、角度太大、光照太暗),直接提示用户重试,而不是硬算。这样虽然增加了重试率,但避免了低质量样本拉低整体准确率。
后处理层面:用多帧投票。连续采几帧,综合判断。单帧可能误判,多帧投票能显著降低FAR。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| EER异常低 | 数据泄漏/对角线未去 | 检查身份ID重叠 | 按人重新划分 |
| EER异常高 | 特征质量差/对齐失败 | 可视化特征分布 | 检查人脸对齐 |
| FAR高但FRR低 | 阈值偏低 | 看阈值设置 | 提高阈值 |
| FRR高但FAR低 | 阈值偏高 | 看阈值设置 | 降低阈值 |
| 跨场景下降大 | 域偏移 | 对比两个数据集分布 | 微调/数据增强 |
| 同一个人分数波动大 | 姿态/光照变化 | 看同一人多张图分数 | 质量过滤+多帧投票 |
| 不同人分数偏高 | 长相相似/亲属 | 看难负样本 | 难例挖掘+重训练 |
5.5 几个容易被忽略的细节
特征归一化的影响:余弦相似度要求特征先做L2归一化。如果忘了归一化,算出来的分数范围会乱,阈值完全没法复用。我习惯在保存特征时就归一化好,用的时候直接用。
浮点精度问题:不同框架算出来的余弦相似度可能有微小差异(比如0.0001级别)。如果阈值卡得很死,这个差异可能导致结果不一致。建议阈值保留4位小数,不要卡到小数点后6位。
多模型融合:如果单个模型的EER降不下去,可以试试多模型融合——用两个不同模型提取特征,拼接或加权平均后再算相似度。我实测过,两个EER 2%的模型融合后,EER能降到1.2%左右。代价是计算量翻倍。
活体检测的独立评估:活体检测(判断是真脸还是照片/视频)的准确率要单独算,不能跟识别准确率混在一起。它的指标是活体通过率(真实人脸通过)和攻击拒绝率(假脸被拒)。这两个也要分别算FAR和FRR。
6. 把准确概率写进报告的正确姿势
算完了,怎么呈现也是一门学问。我见过太多报告只写一个“准确率99%”,这种报告在评审会上会被问得哑口无言。一份合格的准确概率报告,应该包含以下要素。
6.1 报告必备要素清单
- 数据集描述:来源、人数、图片数、每人平均图片数、采集条件。
- 划分方式:按人划分,训练/验证/测试比例。
- 样本对构造:正负样本对数量、负样本采样策略。
- 核心指标:EER、TAR@FAR(至少3个FAR点)、Rank-1(如果是一对多)。
- 阈值:推荐阈值及其对应的FAR/FRR。
- 局限性说明:数据集跟真实场景的差距、未覆盖的场景。
- 复现说明:代码、随机种子、依赖版本。
6.2 可视化:ROC与DET曲线
光有数字不够,曲线更直观。ROC曲线是FAR(横轴)对TAR(纵轴),DET曲线是FAR对FRR(都是对数坐标)。DET曲线在低FAR区域更清晰,所以我一般两个都画。
import matplotlib.pyplot as plt plt.figure(figsize=(8, 6)) plt.plot(far_list, 1 - frr_list, label='ROC') plt.xscale('log') plt.xlabel('FAR') plt.ylabel('TAR (1-FRR)') plt.title('ROC Curve') plt.grid(True) plt.legend() plt.savefig('roc_curve.png', dpi=150)画图的意义:评审的人可能不懂EER,但看得懂曲线。曲线越靠近左上角越好。如果两条曲线交叉,说明两个模型各有优势,要根据业务场景选。
6.3 如何向非技术人员解释
跟产品经理或老板解释时,别甩EER、FAR这些术语。我一般这样类比:
“假设有10000次刷脸,其中9999次是自己人,1次是陌生人。我们的系统在保证陌生人几乎不可能混进去(万分之一以下)的前提下,自己人一次刷开的比例是94%。剩下6%可能需要刷第二次,但不会把陌生人放进去。”
这个说法比“EER 1.2%”直观得多。关键是把安全和体验两个维度分开说,让对方自己权衡。
提示:永远不要承诺“100%准确”。人脸识别没有100%,只有“在某个FAR下的TAR是多少”。承诺100%的人,要么不懂,要么在骗你。
7. 几个进阶话题的补充
写到这里,核心内容已经差不多了。但有几个进阶话题,做实际项目时迟早会遇到,我提前说一下,免得你到时候抓瞎。
7.1 开集识别与闭集识别的区别
前面讲的Rank-1是闭集识别——假设底库里一定有这个人。但真实场景往往是开集识别——来的人可能根本不在底库里。这时候不能只看Rank-1,还要看拒识能力。指标是DIR@FAR(Detection and Identification Rate at a given FAR),即在某个FAR下,正确识别且正确拒识的比例。
开集识别的评估更复杂,需要构造“底库内的人”和“底库外的人”两类查询。底库外的人如果被错误识别成底库里的某个人,就是误受。这个指标比闭集Rank-1更贴近真实门禁场景。
7.2 质量分与准确率的联合评估
实际系统中,人脸质量(清晰度、角度、光照)对准确率影响巨大。我建议在评估时按质量分分层统计——高质量组的EER、中质量组的EER、低质量组的EER。这样你能知道系统的瓶颈在哪。如果低质量组EER很高,那就加强质量过滤;如果高质量组EER也很高,那就是模型本身的问题。
质量分可以用现成的质量评估模型算,也可以简单用拉普拉斯方差(清晰度)、人脸框大小、姿态角来综合。
7.3 时间维度上的稳定性
模型上线后,准确率不是一成不变的。摄像头老化、光照季节变化、用户群体变化都会影响。我一般会做月度评估——每月抽一批实际数据,算一次EER和TAR@FAR,画成趋势图。如果连续两个月下降超过0.5个百分点,就要排查原因了。
这个机制听起来麻烦,但能帮你提前发现问题。我见过一个系统上线半年后准确率悄悄掉了3个百分点,就是因为摄像头镜头脏了没人擦。月度评估一跑就能发现。
7.4 多模态融合的准确率计算
如果系统是人脸+指纹、人脸+刷卡这种多模态,准确率计算要复杂一些。简单做法是串联——两个都通过才放行,那FAR是两个FAR的乘积(假设独立),FRR是两个FRR的并集。但实际中两者不独立,所以乘积是乐观估计。更准确的做法是联合建模,但工程上一般用串联+安全余量就够了。
串联的FAR估算:
FAR_combined ≈ FAR_face × FAR_fingerprint
比如人脸FAR=1e-4,指纹FAR=1e-5,串联后FAR≈1e-9。这个数字很漂亮,但前提是两者独立。实际上人脸和指纹可能因为同一个人状态不好而同时失败,所以实际FAR会比1e-9高一些。我一般会打个折扣,报1e-8左右。
8. 我个人的一些实操体会
最后说几个零散的体会,都是踩坑换来的。
关于数据集:宁可用小但干净的数据集,也不要用大但脏的数据集。我见过一个项目,数据集有50万张图,但里面混了大量重复图和错误标签,算出来的EER完全不可信。后来清洗到10万张,EER反而更稳定。
关于阈值:不要迷信计算出来的阈值。计算值是基于测试集的,实际部署时我一般会把阈值往上调0.02-0.05(余弦相似度),留点安全余量。这个调整幅度是根据实际误受情况反推的,调完后再观察一周。
关于报告:每次评估都要记录随机种子和依赖版本。我吃过亏——同一个脚本,换了numpy版本后EER差了0.3个百分点,排查了半天才发现是浮点精度问题。现在我的评估脚本第一行就是固定所有随机种子,并在报告里写明版本。
关于沟通:跟业务方沟通时,先问清楚“你能接受多少误受”。这个问题往往能把需求聊清楚。如果对方说“一个都不能有”,那就要解释清楚技术边界,或者建议加人工复核。人脸识别是概率系统,不是确定性系统,这个认知要先对齐。
关于持续优化:准确率不是一次算完就完事的。上线后要持续收集bad case——误受的、误拒的,定期分析。我一般每月看一次bad case,看看有没有系统性问题(比如某个光照条件下特别容易误判),然后针对性优化。这个习惯让我们的系统在半年内把实际场景的TAR@FAR=1e-4从92%提升到了96%。
人脸识别准确概率的计算,说到底就是把“模型有多准”这个模糊问题,拆解成一组可量化、可复现、可沟通的数字。这个过程不复杂,但细节很多,每一个细节没处理好,数字就会骗你。希望这篇东西能帮你少走点弯路,算出一份自己信、别人也信的报告。