简介:本资源是一个基于YOLOv5与ArcFace双模型协同的人脸识别完整项目实现,面向深度学习初学者与计算机视觉实践者,解决人脸检测精度低、识别鲁棒性差等实际问题,适用于门禁系统、安防监控、身份核验等AI落地场景。压缩包共51个文件,含26个核心Python脚本(如main.py、train.py、predict_net.py)、7个配置类YAML/Cfg文件(定义网络结构与训练参数)、4张示例人脸图像(chenlinong.jpg等)、2个预训练权重PTH模型(含YOLOv5-face与ArcFace主干),以及README.md项目说明文档,整体仅3.41MB,轻量易部署。已有1512人学习下载,资源结构清晰分层:yoloV5_face模块负责高精度人脸定位,arc_face模块实现特征提取与余弦相似度比对,silentFace_model支持活体检测扩展,配套gen_data.py与recognition.py提供数据生成与识别推理全流程脚本,开箱即用,便于复现、调试与二次开发。
1. 人脸识别不是“检测+分类”二连击:YOLOv5 + ArcFace 组合为什么比单模型更稳、更准、更适合真实场景?
很多人一上来就用 YOLOv5 检出人脸,再拿 ResNet50 提特征、Softmax 分类——结果在实验室准确率 98%,部署到某高校门禁系统里,连续三天把戴口罩的 A 同学识别成隔壁班的 B 同学。问题不在模型差,而在任务错配:人脸识别本质是度量学习(Metric Learning),不是封闭集多分类。YOLOv5 负责把“一张图里哪块是人脸”这件事干得又快又鲁棒(尤其小脸、侧脸、遮挡),ArcFace 则专注把“这张脸到底像谁”这件事建模成高维空间里的角度距离。二者组合不是简单拼接,而是分工明确的流水线:YOLOv5 是“找人的眼睛”,ArcFace 是“认人的脑子”。这个项目完整实现了从图像输入 → 人脸检测 → 关键点对齐 → 特征提取 → 余弦相似度比对 → ID 匹配的全链路,所有代码可离线运行,不依赖任何在线服务或云 API。适合正在做门禁考勤、会议签到、实验室准入等中小型落地项目的开发者,也适合想吃透人脸识别工业级 pipeline 的在校学生——它不讲玄学,只告诉你每个.pt文件怎么来、每个--conf参数为什么设 0.45、每张人脸对齐后为何必须 resize 到 112×112。
2. YOLOv5 检测模块:为什么不用 MTCNN 或 RetinaFace?轻量、泛化、可训是硬指标
YOLOv5 在本项目中承担唯一且不可替代的角色:在复杂光照、低分辨率、部分遮挡条件下稳定框出所有人脸区域。我们没选 MTCNN(速度慢、对模糊人脸漏检率高)、也没用 RetinaFace(参数量大、嵌入边缘设备吃力),核心依据是三组实测数据:在自建的 327 张含口罩/眼镜/侧脸的测试图上,YOLOv5s 的 mAP@0.5 达 0.862,推理耗时均值 12.3ms(RTX 3060),而 MTCNN 均值 41.7ms 且漏检 9 张;在部署到 Jetson Nano 时,YOLOv5n 模型(2.1MB)可维持 8.2 FPS,RetinaFace-R50 的 ONNX 模型(47MB)直接 OOM。更重要的是,YOLOv5 支持端到端微调——当你的场景出现大量工装帽、安全面罩等新遮挡物时,只需补充 200 张标注图,3 小时就能 finetune 出专属检测头。
2.1 检测模型选型与权重加载逻辑
项目默认使用yolov5s-face.pt(非官方原版,是社区针对人脸优化的变体,已集成关键点回归分支)。该权重在 WIDER FACE 验证集上 val-mAP@0.5=0.891,且输出层额外提供 5 个关键点坐标(左眼、右眼、鼻尖、左嘴角、右嘴角),为后续对齐提供强先验。
# detect_face.py 核心加载逻辑 import torch from models.experimental import attempt_load # 加载模型(自动适配 CPU/GPU) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = attempt_load('weights/yolov5s-face.pt', map_location=device) model.eval() # 设置推理参数:置信度过滤 + NMS IOU 阈值 conf_thres = 0.45 # 低于此值的检测框直接丢弃 iou_thres = 0.5 # NMS 中框重叠超此值则抑制提示:
conf_thres=0.45是血泪经验。设 0.5 以上会导致戴口罩人脸大量漏检;设 0.3 以下则背景纹理(如格子衬衫、窗帘)易被误判为人脸。这个值必须结合你的真实场景图像分布校准——建议用val_images/下 50 张典型图跑一遍test.py,观察 precision-recall 曲线拐点。
2.2 关键点驱动的人脸对齐:为什么不用 bbox 中心裁剪?
仅靠检测框(x1,y1,x2,y2)粗暴裁剪会导致特征提取严重失真:同一人左右转头 30°,bbox 中心裁出的图可能完全不对齐眼睛位置,ArcFace 特征向量余弦相似度直接从 0.92 掉到 0.31。本项目强制走关键点仿射变换对齐流程:
- 从 YOLOv5 输出中解析
landmarks(5×2 数组,顺序固定); - 定义标准参考点(
std_landmarks = np.array([[30.2946, 51.6963], [65.5318, 51.5014], [48.0252, 71.7366], [33.5493, 92.3655], [62.7299, 92.2041]]),单位为像素,对应 112×112 标准脸; - 用
cv2.estimateAffinePartial2D计算从实际关键点到标准关键点的仿射矩阵; cv2.warpAffine一次性完成旋转+缩放+平移。
import cv2 import numpy as np def align_face(img, landmarks, std_size=(112, 112)): std_landmarks = np.array([ [30.2946, 51.6963], # left eye [65.5318, 51.5014], # right eye [48.0252, 71.7366], # nose [33.5493, 92.3655], # left mouth [62.7299, 92.2041] # right mouth ]) # 确保 landmarks 是 float32 类型 landmarks = landmarks.astype(np.float32) std_landmarks = std_landmarks.astype(np.float32) # 计算仿射变换矩阵(仅需前两行,cv2.warpAffine 只要 2x3) M, _ = cv2.estimateAffinePartial2D(landmarks, std_landmarks) if M is None: return None # 对齐失败,返回空 aligned = cv2.warpAffine(img, M, std_size, flags=cv2.INTER_CUBIC) return aligned # 使用示例 # 假设 det_output 是 [x1,y1,x2,y2,conf,landmarks] 形状的 numpy array for *xyxy, conf, landmarks in det_output: face_img = img[int(xyxy[1]):int(xyxy[3]), int(xyxy[0]):int(xyxy[2])] aligned_face = align_face(face_img, landmarks.reshape(5,2)) if aligned_face is not None: # 后续送入 ArcFace 提取特征 pass参数说明:
std_landmarks坐标来自 CASIA-WebFace 数据集统计均值,已适配 ArcFace 训练时的预处理尺度;cv2.INTER_CUBIC插值保证对齐后图像锐度,比INTER_LINEAR更抗模糊;- 若
M为None(常见于关键点共线或严重畸变),函数主动返回None,避免后续崩溃——这是工业级健壮性的第一道防线。
2.3 检测后处理:NMS 之外必须加的三道过滤
原始 YOLOv5 输出含大量重叠框和低质量框,仅靠torchvision.ops.nms不够。本项目在utils/general.py中封装了增强后处理:
- 尺寸过滤:丢弃短边 < 40 像素的框(太小的人脸 ArcFace 特征不可靠);
- 长宽比过滤:
abs(w/h - 1) > 0.4的框剔除(明显非人脸矩形,如门牌、手机屏幕); - 关键点置信度过滤:若 5 个关键点中任意一个置信度 < 0.3(YOLOv5-face 输出含 keypoint conf),整框丢弃。
def non_max_suppression_kpts(prediction, conf_thres=0.25, iou_thres=0.45): """ 扩展版 NMS,支持关键点置信度过滤 + 尺寸/长宽比硬约束 prediction shape: (batch, num_anchors, 5+num_classes+10) 最后 10 维:5 个点 x,y + 5 个点 conf """ output = [] for i, pred in enumerate(prediction): # Step 1: 置信度过滤 conf_mask = pred[:, 4] > conf_thres pred = pred[conf_mask] # Step 2: 尺寸 & 长宽比过滤 boxes = pred[:, :4] w = boxes[:, 2] - boxes[:, 0] h = boxes[:, 3] - boxes[:, 1] valid_size = (w > 40) & (h > 40) valid_ratio = abs(w/h - 1) < 0.4 valid_mask = valid_size & valid_ratio # Step 3: 关键点置信度过滤(取 5 个点 conf 的最小值) kpt_confs = pred[:, 6::2] # x 坐标后的 conf(索引 6,8,10,12,14) min_kpt_conf = kpt_confs.min(dim=1)[0] if kpt_confs.numel() else torch.ones(len(pred)) kpt_valid = min_kpt_conf > 0.3 final_mask = valid_mask & kpt_valid pred = pred[final_mask] # Step 4: 标准 NMS if len(pred) > 0: scores = pred[:, 4] * pred[:, 5:].max(1)[0] # class-agnostic score keep = torchvision.ops.nms(boxes[final_mask], scores, iou_thres) output.append(pred[keep]) else: output.append(torch.empty((0, pred.shape[1]), device=pred.device)) return output逻辑说明:
scores = pred[:, 4] * pred[:, 5:].max(1)[0]是关键——将检测置信度与最高类别分(此处为背景/人脸二分类)相乘,比单纯用pred[:,4]更鲁棒;kpt_confs = pred[:, 6::2]的切片逻辑源于 YOLOv5-face 的输出格式:[x1,y1,conf1, x2,y2,conf2, ...],所以 conf 位于偶数索引位(6,8,10,12,14);- 所有过滤必须在 NMS之前执行,否则 NMS 可能保留一个“尺寸合格但关键点崩坏”的框,导致后续对齐彻底失败。
3. ArcFace 特征提取:为什么不用 FaceNet 或 OpenFace?角度间隔损失才是正解
ArcFace 的核心创新在于损失函数:cos(θ + m),其中θ是特征向量与对应类别中心的夹角,m是附加角度间隔。相比 FaceNet 的 triplet loss(难样本挖掘不稳定)或 OpenFace 的 softmax+center loss(类别中心易漂移),ArcFace 在训练后期能强制同类样本在超球面上聚成更紧致的簇,类间边界更清晰。我们在 LFW、CFP-FP、AgeDB-30 三个公开 benchmark 上实测:同一 backbone(IR-SE50),ArcFace 微调后平均准确率比 FaceNet 高 2.3%,在跨年龄识别(AgeDB)上优势达 4.1%。更重要的是,ArcFace 的特征向量天然具备余弦相似度可解释性:0.85 以上基本可判定为同一人,0.3 以下基本排除,中间区间(0.5~0.7)才需加活体或二次验证——这为业务系统设计阈值策略提供了确定性依据。
3.1 模型结构与权重加载:IR-SE50 vs MobileFaceNet 的取舍
项目提供两个预训练权重:
arcface_irse50.pth:基于 IR-SE50 backbone,在 MS1M-ArcFace 数据集上训练,特征维度 512,精度高、体积大(98MB);arcface_mobilefacenet.pth:MobileFaceNet 轻量版,特征维度 128,体积仅 4.2MB,精度略降(LFW 99.42% vs 99.83%),但可在树莓派 4B 上达 15 FPS。
选择依据不是“越小越好”,而是吞吐量与精度的帕累托前沿。实测表明:当单日识别请求 < 5000 次(如小型实验室门禁),MobileFaceNet 完全够用;当需支撑 200 人同时刷脸的会议签到系统,IR-SE50 的 0.4% 精度提升能显著降低人工复核率。
import torch import torch.nn as nn from backbones.ir_se import IR_SE_50 from backbones.mobilefacenet import MobileFaceNet def load_arcface_model(model_name='ir_se50', device='cuda'): if model_name == 'ir_se50': model = IR_SE_50(input_size=(112, 112)) weight = torch.load('weights/arcface_irse50.pth', map_location=device) elif model_name == 'mobilefacenet': model = MobileFaceNet(embedding_size=128) weight = torch.load('weights/arcface_mobilefacenet.pth', map_location=device) else: raise ValueError("model_name must be 'ir_se50' or 'mobilefacenet'") model.load_state_dict(weight) model.to(device) model.eval() return model # 使用示例 arcface_model = load_arcface_model('mobilefacenet', device)注意:IR-SE50 的
input_size=(112,112)是硬约束,必须与对齐步骤输出尺寸一致;MobileFaceNet 同样要求 112×112,但对输入归一化方式略有不同——IR-SE50 用img = (img - 127.5) / 128.0,MobileFaceNet 用img = img / 255.0。项目中preprocess.py已封装自动判断逻辑。
3.2 特征提取全流程:从对齐图到 512 维向量
ArcFace 输入必须是严格归一化的 112×112 RGB 图像。本项目preprocess.py实现了无损预处理链:
- BGR → RGB(OpenCV 默认 BGR,PyTorch 模型训练用 RGB);
cv2.resize到 112×112(INTER_AREA用于缩小,INTER_CUBIC用于放大);- 数据类型转
float32; - 归一化:IR-SE50 用
img = (img - 127.5) / 128.0,MobileFaceNet 用img = img / 255.0; np.transpose(2,0,1)调整通道顺序(HWC → CHW);torch.from_numpy().unsqueeze(0)增加 batch 维度。
import cv2 import numpy as np import torch def preprocess_face(img, model_name='ir_se50'): """ img: aligned face, numpy array (112,112,3), dtype=uint8 """ # Step 1: BGR to RGB img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # Step 2: Resize (though already 112x112, ensure no float error) img = cv2.resize(img, (112, 112), interpolation=cv2.INTER_AREA) # Step 3: To float32 img = img.astype(np.float32) # Step 4: Normalize if model_name == 'ir_se50': img = (img - 127.5) / 128.0 else: # mobilefacenet img = img / 255.0 # Step 5: HWC -> CHW img = np.transpose(img, (2, 0, 1)) # Step 6: To tensor, add batch dim img_tensor = torch.from_numpy(img).unsqueeze(0) return img_tensor # 提取特征 aligned_face = align_face(...) # 来自 2.2 节 input_tensor = preprocess_face(aligned_face, 'ir_se50').to(device) with torch.no_grad(): embedding = arcface_model(input_tensor) # shape: (1, 512) embedding = torch.nn.functional.normalize(embedding, p=2, dim=1) # L2 归一化关键说明:
torch.nn.functional.normalize(..., p=2, dim=1)是必须步骤!ArcFace 的余弦相似度计算依赖单位向量,未归一化的 embedding 直接点积会因模长差异导致错误排序;with torch.no_grad()省去梯度计算,提速 30% 且显存减半;embedding输出即为最终可用于比对的特征向量,后续所有操作(入库、检索、阈值判断)均基于此。
3.3 特征库构建与快速检索:FAISS 为什么比暴力搜索快 200 倍?
当注册人脸超 1000 张,暴力计算 query embedding 与全部 gallery embedding 的余弦相似度(O(N))会成为瓶颈。本项目默认集成 FAISS(Facebook AI Similarity Search),其 IVF(Inverted File)索引将查询复杂度降至 O(log N),实测 10,000 人脸库下平均查询耗时 1.2ms(RTX 3060),而暴力搜索需 230ms。
import faiss import numpy as np class FaceFeatureDB: def __init__(self, feature_dim=512, nlist=100): self.feature_dim = feature_dim self.nlist = nlist # 创建 IVF 索引:先聚类,再查最近聚类中心的倒排列表 quantizer = faiss.IndexFlatIP(feature_dim) # 内积索引(等价于余弦,因已 L2 归一化) self.index = faiss.IndexIVFFlat(quantizer, feature_dim, nlist) self.index.train(np.random.random((10000, feature_dim)).astype('float32')) self.index.nprobe = 10 # 查找 10 个最近聚类中心 def add(self, embeddings, ids): """embeddings: (N, 512) numpy array, ids: list of str""" self.index.add(embeddings.astype('float32')) self.ids = ids # 外部维护 ID 映射 def search(self, query_embedding, k=5): """query_embedding: (1, 512) numpy array""" D, I = self.index.search(query_embedding.astype('float32'), k) # D 是内积值(即余弦相似度),I 是索引数组 return D[0], [self.ids[i] for i in I[0]] # 初始化并添加特征 db = FaceFeatureDB(feature_dim=512) gallery_embeddings = np.stack([emb.detach().cpu().numpy() for emb in gallery_embs]) # (N,512) db.add(gallery_embeddings, gallery_ids) # 查询 sim_scores, top_ids = db.search(query_embedding.numpy(), k=3) print(f"Top match: {top_ids[0]} (score: {sim_scores[0]:.3f})")参数说明:
nlist=100:聚类中心数,经验值。nlist ≈ sqrt(N)时平衡精度与速度,10,000 人脸库用 100 合理;nprobe=10:每次查询扫描的聚类中心数,越大越准越慢,默认 1,这里设 10 是精度/速度折中;IndexFlatIP用内积而非余弦,因输入 embedding 已 L2 归一化,dot(a,b) == cos(θ),FAISS 内积索引比余弦索引快 2 倍。
4. 避坑:YOLOv5 + ArcFace 流水线中五个必踩的“静默陷阱”
这些坑不会报错,但会让你调试三天找不到原因,最后发现是某个参数设错了。全是某高校门禁项目上线前翻车实录。
4.1 现象:YOLOv5 检测框坐标全为负数或极大值
原因:detect_face.py中未设置device,模型在 CPU 上加载,但推理时img在 GPU 上(或反之),导致张量设备不匹配,.cpu().numpy()返回乱码内存地址。
解决:严格统一设备——model.to(device)后,所有输入img_tensor必须img_tensor = img_tensor.to(device);检查print(img_tensor.device, model.device)是否一致。
4.2 现象:ArcFace 提取的特征向量全为 nan
原因:对齐后图像aligned_face为None(关键点对齐失败),但代码未判空直接传入preprocess_face(),cv2.cvtColor(None, ...)返回None,后续astype(np.float32)报TypeError被静默捕获,最终embedding为 nan。
解决:在align_face()后强制判空,并记录日志:
aligned_face = align_face(face_img, landmarks.reshape(5,2)) if aligned_face is None: logger.warning(f"Face alignment failed for bbox {xyxy}, skip") continue4.3 现象:同一个人不同照片的余弦相似度忽高忽低(0.4~0.9 波动)
原因:ArcFace 输入未做 L2 归一化。虽然模型输出理论上已归一化,但实际部署中因精度损失(FP16 推理、ONNX 转换)可能导致模长偏离 1.0,直接点积结果失真。
解决:无论模型是否声称“输出已归一化”,在embedding = arcface_model(input_tensor)后必须加:
embedding = torch.nn.functional.normalize(embedding, p=2, dim=1)4.4 现象:FAISS 搜索返回的相似度 > 1.0 或 < -1.0
原因:FAISSIndexFlatIP要求输入向量严格单位化,但gallery_embeddings在入库前未做normalize,导致内积超出 [-1,1] 范围。
解决:入库前统一归一化:
gallery_embeddings = np.stack([...]) gallery_embeddings = gallery_embeddings / np.linalg.norm(gallery_embeddings, axis=1, keepdims=True) db.add(gallery_embeddings.astype('float32'), gallery_ids)4.5 现象:戴口罩人脸被识别为他人,且相似度高达 0.82
原因:YOLOv5 检测框包含大量口罩区域,对齐后图像中嘴部区域占据主导,ArcFace 特征过度关注口罩纹理而非五官结构。
解决:在align_face()后加口罩区域掩码(mask):
def mask_mask_region(aligned_face): # 在 112x112 图上,口罩区域大致为 y>60 的下半部分 mask = np.ones_like(aligned_face) mask[60:, :, :] = 0.5 # 半透明覆盖,减弱口罩区域权重 return aligned_face * mask # 使用 aligned_face = align_face(...) masked_face = mask_mask_region(aligned_face) # 传入 preprocess_face血泪经验:这个掩码不是为了“去掉口罩”,而是告诉 ArcFace “别太信下半脸”,实测使戴口罩识别准确率提升 11.2%。
5. 阈值工程:如何用三组测试图动态确定你的业务阈值?
人脸识别落地最头疼的不是模型不准,而是“设多少分算识别成功”——设高了拒真率高(员工进不了门),设低了认假率高(陌生人混入)。本项目不给你一个拍脑袋的 0.7,而是教你用业务真实数据驱动阈值决策。
5.1 构建三类黄金测试集
必须准备三组独立图像(不能和训练/验证集重叠):
| 类别 | 数量 | 要求 | 用途 |
|---|---|---|---|
| 正样本(Same-Person) | ≥100 对 | 同一人不同时间、光照、姿态的照片,至少 30% 含口罩/眼镜 | 测量最大可接受拒真率(FRR) |
| 负样本(Diff-Person) | ≥100 对 | 外貌相似者(同性别、同年龄段、同发型),如双胞胎、同事 | 测量最大可接受认假率(FAR) |
| 挑战样本(Challenge) | ≥50 对 | 侧脸、低头、强逆光、运动模糊、低分辨率(<200p) | 测量系统鲁棒性下限 |
提示:不要用网络下载图!必须是你部署环境的真实抓拍。某公司曾用百度图片设阈值 0.75,上线后发现监控摄像头夜间红外模式下图像偏绿,同一个人相似度全掉到 0.62,FRR 飙升至 40%。
5.2 绘制 DET 曲线:找到你的帕累托最优阈值
对三组测试集,遍历阈值t从 0.1 到 0.95(步长 0.01),计算:
- FRR(t)= 正样本中相似度 < t 的比例;
- FAR(t)= 负样本中相似度 ≥ t 的比例;
- Challenge-Acc(t)= 挑战样本中相似度 ≥ t 的比例。
用matplotlib绘制曲线,交点即为平衡点:
import matplotlib.pyplot as plt import numpy as np # 假设 same_scores, diff_scores, challenge_scores 是三组相似度列表 t_range = np.arange(0.1, 0.96, 0.01) frr_list = [] far_list = [] acc_list = [] for t in t_range: frr = np.mean(np.array(same_scores) < t) far = np.mean(np.array(diff_scores) >= t) acc = np.mean(np.array(challenge_scores) >= t) frr_list.append(frr) far_list.append(far) acc_list.append(acc) # 绘图 plt.figure(figsize=(10,6)) plt.plot(t_range, frr_list, label='FRR (False Rejection Rate)') plt.plot(t_range, far_list, label='FAR (False Acceptance Rate)') plt.plot(t_range, acc_list, label='Challenge Accuracy') plt.xlabel('Threshold') plt.ylabel('Rate') plt.title('DET Curve for Threshold Selection') plt.legend() plt.grid(True) plt.show() # 找 FRR=1% 和 FAR=0.1% 的交点(行业常用安全基线) target_frr = 0.01 target_far = 0.001 optimal_t = t_range[np.argmin(np.abs(np.array(frr_list) - target_frr) + np.abs(np.array(far_list) - target_far))] print(f"Optimal threshold: {optimal_t:.3f} (FRR={frr_list[np.argmin(np.abs(t_range-optimal_t))]:.3f}, FAR={far_list[np.argmin(np.abs(t_range-optimal_t))]:.3f})")解读:
- 若业务允许 1% 员工被拒(如门禁可刷工卡),则看 FRR=0.01 对应的
t; - 若安全要求极高(如财务室),则看 FAR=0.001 对应的
t; - 最优实践:取
FRR=0.01与FAR=0.001的加权平衡点,本项目默认推荐t=0.72(经 5 个真实场景验证,FRR=0.8%, FAR=0.09%)。
5.3 动态阈值补偿:光照/模糊自适应调整
固定阈值在复杂环境仍会失效。我们在inference.py中加入实时补偿模块:
- 计算当前人脸图像的清晰度得分(Laplacian 方差);
- 计算光照均匀度(直方图标准差);
- 若清晰度 < 100 或光照标准差 > 50,则阈值
t自动下调 0.05(放宽识别); - 若两者均优,则
t上调 0.03(收紧防伪)。
def calc_sharpness(img): """img: uint8 (112,112,3)""" gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) return cv2.Laplacian(gray, cv2.CV_64F).var() def calc_light_uniformity(img): """计算 RGB 三通道直方图标准差均值""" hists = [cv2.calcHist([img], [i], None, [256], [0,256]) for i in range(3)] stds = [np.std(hist) for hist in hists] return np.mean(stds) # 在推理循环中 sharpness = calc_sharpness(aligned_face) light_std = calc_light_uniformity(aligned_face) base_threshold = 0.72 if sharpness < 100 or light_std > 50: threshold = base_threshold - 0.05 else: threshold = base_threshold + 0.03 if sim_score >= threshold: print(f"Accepted: {top_id} (score={sim_score:.3f}, threshold={threshold:.3f})")从那以后我每次部署新场景,都强制走一遍这三组测试图 + DET 曲线流程,哪怕客户说“就用你们默认的 0.72”。因为阈值不是数学题,是业务风险题——0.72 在实验室是 99.2%,在强光玻璃门后可能是 83.7%。希望帮到你。
本文还有配套的精品资源,点击获取