☰
人脸识别准确概率计算全链路:从相似度到业务校准
2026/10/15 1:45:57 网站建设 项目流程

人脸识别这套东西,我从最早拿开源模型跑通第一张图,到后来在几个实际项目里做门禁、做签到、做相册聚类,踩过的坑不算少。但真正让我意识到“准确率”这三个字有多容易被误读的,是有一次帮朋友看一个考勤系统——他跟我说“模型准确率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, None

target_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 / total

Rank-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 阈值到底怎么选

阈值选择没有标准答案,但有方法论:

  1. 先定业务约束:FAR不能超过多少?FRR不能超过多少?
  2. 在约束下找最优阈值:用TAR@FAR的方法,找到满足FAR约束的最大TAR对应的阈值。
  3. 留安全余量:实际部署时,阈值要比计算值稍微严格一点(高一点),因为真实场景的负样本比测试集更难。
  4. 支持动态调整:上线后根据实际数据持续调整。我一般会留一个后台开关,能随时改阈值。
场景建议阈值策略备注
高安全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%。

人脸识别准确概率的计算,说到底就是把“模型有多准”这个模糊问题,拆解成一组可量化、可复现、可沟通的数字。这个过程不复杂,但细节很多,每一个细节没处理好,数字就会骗你。希望这篇东西能帮你少走点弯路,算出一份自己信、别人也信的报告。

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

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

立即咨询