人脸识别准确率上不去、陌生人被误认成注册用户,这类问题在工程落地时非常常见。不少团队用ArcFace训练出的模型在测试集上指标很好看,一部署到真实环境就出现大量误识(FAR偏高),根本原因往往不在模型本身,而在阈值设置不合理以及特征向量没有做归一化处理。本文从原理到代码,完整拆解这两个关键环节。

一、先弄清楚误识率与阈值的关系
误识率(FAR,False Accept Rate)指的是把不同的人认成同一个人的比例,与之对应的是拒识率(FRR,False Reject Rate),即把同一个人认成不同人。这两个指标天然是此消彼长的关系:阈值调高,误识率下降但拒识率上升;阈值调低,用户体验好但安全性变差。所谓调阈值,本质是在这条ROC曲线上找一个符合业务要求的平衡点。
用ArcFace训练的模型,特征向量经过归一化后,两个特征的相似度就是余弦相似度,取值范围是-1到1。实际中同一个人的相似度大多落在0.5以上,不同人则集中在0.2以下,两者的分布交界处就是阈值敏感区。很多开发者随手写死一个0.5或者0.7的阈值就上线,这在训练数据与业务场景人脸分布差异较大时极易出错。
正确做法是基于自己的业务数据绘制相似度分布直方图,统计正样本对与负样本对的重叠区域,再结合目标FAR(比如金融场景要求FAR低于十万分之一)反推阈值。经验上,在LFW上测试时0.35左右的阈值能取得较好平衡,但在门禁、支付等高安全场景,往往需要提高到0.6甚至0.7以上,并辅以活体检测兜底。
二、特征归一化为什么是必做步骤
ArcFace的损失函数在训练时会对特征和权重都做L2归一化,因此模型学到的判别逻辑完全建立在单位球面上的余弦距离之上。如果推理阶段直接拿原始特征算欧氏距离或者点积,不同图片提取出的特征模长波动会严重干扰结果,模长大的样本相似度天然偏高,导致阈值形同虚设。
具体来看,归一化就是将向量除以其L2范数,让它变成模长为1的单位向量。此时两个特征的点积在数学上严格等于余弦相似度,阈值才有稳定的物理意义。下面这段代码直观展示了归一化前后的差异:
import numpy as np
# 模拟两张不同人脸图片提取出的特征
feat1 = np.array([2.1, 3.4, 1.2, 0.8])
feat2 = np.array([4.2, 6.8, 2.4, 1.6]) # 实际是feat1方向接近、模长翻倍
# 未归一化:直接算点积,数值受模长影响很大
raw_score = np.dot(feat1, feat2)
print("未归一化点积:", raw_score) # 输出一个很大的数,阈值不稳定
# L2归一化后再计算
feat1_n = feat1 / np.linalg.norm(feat1)
feat2_n = feat2 / np.linalg.norm(feat2)
cos_score = np.dot(feat1_n, feat2_n)
print("归一化后余弦相似度:", cos_score) # 输出接近1,含义明确从输出可以看到,未归一化时点积可能达到几十甚至上百,完全无法用一个固定阈值衡量;归一化后数值被压缩到-1到1之间,且与ArcFace训练时的度量空间一致。此外还有一个细节:如果使用faiss等向量检索库建索引,入库特征和查询特征必须采用同一套归一化流程,否则检索结果的top-k分数会整体漂移。
三、工程上的完整校准流程与降误识技巧
在真实项目中,建议按以下流程做阈值校准:第一步,收集与业务场景一致的测试集,包含同人不同照和不同人对照各若干对;第二步,全部经过归一化后计算相似度,绘制两类分布曲线;第三步,根据业务能接受的FAR找到交点右侧的阈值,并用FRR验证用户体验是否达标;第四步,上线后持续收集误识样本回流分析,定期微调阈值。
除了调阈值,还可以从多个维度压低误识率。质量检测是第一道关,把模糊、遮挡、大角度侧脸的图片在入库和比对前过滤掉,能显著减少特征不可靠带来的误判。多帧融合也有效,对视频流取多帧特征求平均再归一化,相当于对噪声做了平滑。安全等级高的场景可以叠加注意力活体或红外活体,把攻击面和人脸比对问题分开处理。
def compare_faces(feat_a, feat_b, threshold=0.6):
"""标准的ArcFace特征比对流程"""
# 统一做L2归一化,保证度量空间与训练一致
a = feat_a / np.linalg.norm(feat_a)
b = feat_b / np.linalg.norm(feat_b)
sim = float(np.dot(a, b))
return sim, sim >= threshold
# 多帧融合:先平均再归一化
def fusion_feature(feats):
avg = np.mean(np.stack(feats), axis=0)
return avg / np.linalg.norm(avg)最后提醒一点,阈值不是一次定终身的常量。随着模型迭代、摄像头更换、光照环境变化,相似度分布都会缓慢漂移,把阈值做成可配置项并建立线上监控告警,才是长期稳定控制误识率的正确姿势。归一化保证分数有意义,校准保证阈值合身,两者配合好,误识问题基本可以得到根治。