ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CNN人脸识别考勤系统实战:预训练模型选型与阈值调优

CNN人脸识别考勤系统实战:预训练模型选型与阈值调优 简介一套基于CNN的人脸识别考勤系统完整源码包面向计算机视觉初学者、毕业设计开发者及需要快速落地考勤场景的工程师。资源包含可直接运行的预训练模型h5文件与配套源码省去从零训练的时间与算力成本8个Python脚本覆盖数据集加载、人脸特征提取、匹配判定、考勤记录等核心环节另有UI界面文件与XML配置文件便于二次开发与界面调整。压缩包共4848个文件其中以4835张JPG人脸样本图片为主包含不同角度、光照与表情的样本可支撑模型微调与效果验证整体108.06MB目录结构清晰上手门槛较低。已有306人学习下载。借助该资源可系统掌握CNN人脸识别在考勤场景中的完整落地流程理解预训练模型迁移、特征比对与实时识别逻辑适合课程设计、毕业设计或企业原型系统参考。1. 能直接运行的 CNN 人脸识别考勤系统先验三件事拿到一套带预训练模型、号称能直接运行的 CNN 人脸识别考勤系统最常见的情况不是打不开而是摄像头亮了、画面里也框出了人脸但刷谁都不认识。反直觉的点在于预训练模型能跑通和考勤能落地是两回事。前者只代表 ONNX 权重被正确加载后者还需要确认检测模型和特征提取模型的输入输出是否匹配、比对阈值是否被默认值带偏、底库里的特征向量是什么时候用哪一版权重生成的。本文按选型、首跑、业务表设计、参数校正的顺序把这套系统从能出框调成能打卡。2. 拆解 CNN 人脸识别考勤系统的识别链路与预训练模型选型2.1 一个 CNN 不够检测、对齐、特征提取是三个模型接力人脸识别考勤系统里的CNN不是一个大网络而是一条流水线。第一步是人脸检测常见的是 RetinaFace、SCRFD、MTCNN 这类轻量检测网络输出人脸框和五个关键点双眼、鼻尖、左右嘴角第二步是对齐根据五个关键点做相似变换把图片里歪着、侧着的脸归一化成标准正脸图第三步才是真正意义上的人脸识别也就是特征提取网络常见的是 ResNet50、ResNet100、MobileFaceNet 这一类 backbone 加上 ArcFace 或 CosFace 的损失函数训练出来的特征模型。这个区分很重要因为预训练模型在这条流水线里通常指的是第三步的特征模型而检测和对齐可能来自另一个预训练权重。判断一套系统能不能直接跑第一件事就是看它有没有把所有阶段的权重文件都带齐而不是只给一个*.onnx就当交付。特征提取模型的工作方式是把对齐后的 112×112 或 160×160 的彩色人脸图映射到一个高维向量。以 ArcFace 系列为例输出一般是 512 维的浮点特征并且已经做了 L2 归一化。后面的比对不再依赖 CNN而是对特征向量做余弦相似度计算相似度超过阈值就算同一个人。2.2 为什么直接用预训练模型比重训更划算很多人拿到考勤系统的第一步是想着再训练一轮想把员工照片 finetune 进去。实际上对于人脸识别这种任务直接使用开源免费商用的预训练模型效果通常远好于基于几百张员工照片做微调。原因在于预训练模型是在百万级甚至千万级人脸库上训练出来的backbone 已经学到了光照变化、姿态变化、年龄变化下的鲁棒表示。考勤场景里摄像头位置固定、光线相对可控属于预训练模型覆盖范围内的常见分布。真正需要做的不是重新训练 CNN而是选择一个合适的预训练模型并保证底库里的特征和线上识别用的是同一套权重。如果底库是旧权重生成的 512 维向量识别端换成了新权重那么两个人的相似度会普遍偏低表现为谁都不认识。这种情况在工程里非常常见却不是模型的问题而是特征版本不一致。另外要注意免费商用的边界。开源的人脸识别模型大多只开放了推理权重和推理代码训练数据和使用限制要看具体许可证。落地到企业内部考勤问题不大但如果要做成对外销售的人脸识别门禁机或 SaaS 产品就得核对授权条款。选型时我一般会把可商用性和输入输出格式放在同等位置。2.3 预训练模型选型参数输入尺寸、特征维度和损失函数下表列出做考勤系统时常见的预训练模型选型参考维度不涉及具体下载地址只说明业界常见的几种配置倾向模型系列输入尺寸特征维度适用场景说明ArcFaceResNet100 backbone112×112512精度优先适合闸机、门禁一体机CPU 推理偏慢ArcFaceMobileFaceNet112×112128轻量设备或边缘盒子识别速度更快精度略低FaceNetInception ResNet v1160×160128经典方案部署资料多适合快速验证原型SCRFD 检测 ArcFace 识别组合检测 640×640识别 112×112512考勤系统最常见的组合兼顾检测召回和识别精度选型时除了看准确率还要看模型文件大小和推理耗时。考勤场景是多人排队刷脸单帧识别时间最好控制在 100ms 以内所以 MobileFaceNet 这类轻量模型在普通 USB 摄像头方案里更常见如果现场配的是专用人脸识别门禁机设备内部往往已经跑了一个嵌入式优化过的模型这时候考勤系统要做的只是接收设备传来的特征值或者人脸 ID。2.4 比对逻辑的最小代码相似度怎么算、阈值怎么给预训练模型输出的是归一化特征向量代码里比对两人的相似度时不需要重算余弦公式里的除法直接点乘即可。下面这段是识别逻辑的最小代码也是后续考勤系统判定的基础import numpy as np def cosine_similarity(feat_a: np.ndarray, feat_b: np.ndarray) - float: # 预训练模型输出通常已经做 L2 归一化点乘即余弦相似度 if feat_a.shape ! feat_b.shape: raise ValueError(f特征维度不一致: {feat_a.shape} vs {feat_b.shape}) score float(np.dot(feat_a, feat_b)) # 防御性检查超过 1.0 的浮点误差裁回 1.0避免影响后续阈值判断 return min(max(score, -1.0), 1.0) def is_same_person(score: float, threshold: float 0.5) - bool: # 阈值只影响误识率FAR和拒识率FRR的取舍不改变特征本身 return score threshold逻辑说明np.dot在这里等同于余弦相似度因为两个向量都已经做过 L2 归一化模长为 1。阈值 0.5 是经验起点不是通用答案。调高到 0.55 以上误识率下降但容易拒识调到 0.45 以下员工刷脸通过率提高但可能把长得像的两个人判成同一人。后面第 5 章会说怎么用真实数据把阈值标定出来。3. 预训练模型本地跑通注册、比对、摄像头识别的第一版代码3.1 拿到预训练模型先核对四处配置可以直接运行的模型包通常只包含权重文件而权重对输入数据有严格约定。我在接入一个新模型包时最先核对的是四件事输入图像的通道顺序是 RGB 还是 BGR输入尺寸是 112×112、160×160 还是其他像素归一化是除以 255 还是减均值除以标准差输出特征是否需要再做一次 L2 归一化。这四项里任何一项不一致识别准确率都会大幅下降而且表现很像系统坏了——因为不是逻辑错误是数据分布整体错位。最常见的错误发生在通道顺序上。OpenCV 读进来的是 BGR而大部分预训练模型是用 RGB 图像训练的。如果加载模型后没有cv2.cvtColor(img, cv2.COLOR_BGR2RGB)识别率会掉到接近随机猜。另一个隐蔽问题是关键点坐标的尺度检测模型输出的五个关键点坐标是在原图分辨率下的像素坐标对齐时如果换算倍数错了切出来的人脸就是歪的特征质量自然差。3.2 用 ONNX Runtime 写一个特征提取引擎下面是基于 ONNX Runtime 和 OpenCV 的通用特征提取实现兼容以 ONNX 格式分发的预训练检测模型和识别模型。检测模型输出人脸框和五个关键点识别模型接收对齐后的标准人脸图并输出特征向量。import cv2 import numpy as np import onnxruntime as ort class FaceEngine: def __init__(self, det_model: str, rec_model: str, input_size: int 112): # 优先用 CUDA不存在则回退 CPU避免在无 GPU 的考勤机上直接报错 providers [CUDAExecutionProvider, CPUExecutionProvider] self.det_session ort.InferenceSession(det_model, providersproviders) self.rec_session ort.InferenceSession(rec_model, providersproviders) self.input_size input_size # 记录模型要求的输入名不同预训练模型的输入节点名并不统一 self.det_input self.det_session.get_inputs()[0].name self.rec_input self.rec_session.get_inputs()[0].name print(f检测模型输入: {self.det_input}, 识别模型输入: {self.rec_input}) def detect_faces(self, bgr_img: np.ndarray): 返回人脸框和五点的粗略实现省略 NMS 细节以保证可读性。 # 检测模型输入通常要求固定尺寸这里按 640x640 缩放并保持原图比例不变 h, w bgr_img.shape[:2] scale 640 / max(h, w) resized cv2.resize(bgr_img, (int(w * scale), int(h * scale))) # 构造 NCHW 输入归一化到 [0,1] blob cv2.dnn.blobFromImage(resized, 1.0 / 255, (640, 640), (0, 0, 0), swapRBTrue) outputs self.det_session.run(None, {self.det_input: blob}) # outputs 的具体格式取决于模型需要按实际权重解析 bbox 和 landmark return outputs def get_embedding(self, bgr_img: np.ndarray) - np.ndarray: 检测并裁剪出最大人脸对齐后提取 512 维特征。 dets self.detect_faces(bgr_img) # 实际项目中这里要解析出 bbox 与五个关键点并按相似变换把脸对齐到 input_size face_align cv2.resize(bgr_img, (self.input_size, self.input_size)) # 识别模型同样要求 RGB 顺序与 [0,1] 归一化 blob cv2.dnn.blobFromImage(face_align, 1.0 / 255, (self.input_size, self.input_size), (0, 0, 0), swapRBTrue) feat self.rec_session.run(None, {self.rec_input: blob})[0].flatten() # L2 归一化后再入库保证后面点乘即余弦相似度 feat feat / np.linalg.norm(feat) return feat逻辑说明blobFromImage里的swapRBTrue是因为 ONNX 模型大多数按 RGB 训练而 OpenCV 默认 BGR这一步负责把通道顺序转换掉。检测结果解析我没有展开因为不同预训练模型的输出设计差异很大有的直接输出 bbox 和关键点坐标有的输出的是 stride 网格需要按模型文档解码。实际落地时我会先用一张单人照片打印outputs的 shape 和数值范围确认输出结构后再写解析逻辑。3.3 注册第一位员工完成认人闭环from pathlib import Path import numpy as np import cv2 engine FaceEngine(det.onnx, rec.onnx) def register_employee(emp_id: str, photo_path: str, db_dir: str face_db): 把一张干净的正面照变成特征向量以 npy 文件落库。 img cv2.imread(photo_path) feat engine.get_embedding(img) db_dir Path(db_dir) db_dir.mkdir(exist_okTrue) np.save(db_dir / f{emp_id}.npy, feat) print(f员工 {emp_id} 注册完成特征维度: {feat.shape[0]}) def recognize(photo_path: str, db_dir: str face_db, threshold: float 0.5): 识别一张照片属于哪个员工返回员工ID与相似度。 img cv2.imread(photo_path) feat engine.get_embedding(img) best_id, best_score None, -1.0 for npy_file in Path(db_dir).glob(*.npy): db_feat np.load(npy_file) score float(np.dot(feat, db_feat)) if score best_score: best_id, best_score npy_file.stem, score if best_score threshold: return None, best_score return best_id, best_score # 注册一个测试员工然后用同一张照片验证识别 register_employee(E1001, photos/zhangsan.jpg) emp_id, score recognize(photos/zhangsan_live.jpg, threshold0.5) print(f识别结果: {emp_id}, 相似度: {score:.3f})参数说明threshold0.5是初始值实际部署时应该用第 5 章的方法重新标定。db_dir用 npy 文件存特征适合几十人到几百人的规模超过 1000 人时逐个加载 npy 的耗时不可忽略这一段的瓶颈就从 CNN 推理转移到了 IO 和向量比对需要一个更快的索引结构。3.4 首次运行最容易卡住的三个位置现象原因处理方式模型加载报错 No such file or directory路径含中文或模型文件缺失把模型放到纯英文路径确认det.onnx、rec.onnx都存在识别率极低所有人的分数都在 0.2 以下通道顺序反了或输入尺寸与模型要求不一致检查swapRB参数和 resize 尺寸是否匹配模型输入同一个人的两次抓拍相似度只有 0.3对齐环节没生效直接resize整张图当人脸把对齐后的人脸图保存出来看一眼确认五官位置居中这三类问题占了模型接入阶段八成以上的故障时间。第 2 类是权重配置问题第 3 类是对齐逻辑问题这也是为什么我坚持把保存中间结果写进工程代码——没有中间人脸图调试时就只能靠猜。4. 考勤数据落地人脸识别的结果怎么变成迟到早退记录4.1 考勤表设计人员、特征、打卡记录三张表识别链路跑通后下一个核心问题是数据模型。很多自研考勤系统只建了一张打卡表存员工 ID、时间和照片路径等要出月度报表时发现迟到没法算因为没有基准排班表。常见的做法是拆三张表employees存人员基础信息face_features存特征向量和版本号attendance存每次识别事件。特征单独成表而不是并进人员表是为了将来更换识别模型时能通过版本字段批量重算。CREATE TABLE employees ( emp_id TEXT PRIMARY KEY, name TEXT NOT NULL, department TEXT, hire_date TEXT, status INTEGER DEFAULT 1 ); CREATE TABLE face_features ( emp_id TEXT PRIMARY KEY, feature BLOB NOT NULL, model_version TEXT NOT NULL, updated_at TEXT DEFAULT (datetime(now, localtime)), FOREIGN KEY (emp_id) REFERENCES employees(emp_id) ); CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, emp_id TEXT NOT NULL, clock_time TEXT NOT NULL, device_id TEXT, similarity REAL, status TEXT, FOREIGN KEY (emp_id) REFERENCES employees(emp_id) ); CREATE INDEX idx_attendance_emp_time ON attendance(emp_id, clock_time);建表逻辑说明feature字段存的是特征向量的二进制序列化结果而不是 Base64 字符串因为 BLOB 检索和存储开销更小。model_version字段记录这次特征是用哪套权重生成的识别端加载模型后会把模型文件名或版本号带进来只有同版本的特征才能直接比对。attendance表只存原始事件迟到、早退的判定不写在这里而是靠视图或统计 SQL 计算这样排班规则改动时不需要重写历史记录。4.2 打卡事件判定迟到、早退、缺卡的去重规则考勤业务里容易出问题的不是识别而是同一个员工在闸机前反复刷脸导致产生多条记录。判定逻辑通常先做去重再打状态标签。常见做法是同一个员工 5 分钟内的连续识别只保留第一次这样进公司时刷两次不会重复但下班时同一个人可能只刷一次就走因此去重窗口不能设太长。下面的 SQL 把当天打卡记录按员工分组取出每个人当天最早的打卡和最后的打卡再根据班次信息标记状态WITH daily_records AS ( SELECT emp_id, date(clock_time) AS work_day, MIN(clock_time) AS first_in, MAX(clock_time) AS last_out, COUNT(*) AS punch_count FROM attendance WHERE clock_time date(now, localtime, -1 day) GROUP BY emp_id, date(clock_time) ) SELECT emp_id, work_day, CASE WHEN time(first_in) 09:15:00 THEN 迟到 ELSE 正常 END AS morning_status, CASE WHEN punch_count 1 THEN 缺少下班记录 ELSE 已打卡 END AS evening_status FROM daily_records;这段 SQL 将一个人当天上午第一次有效识别作为上班时间晚上最后一次识别作为下班时间。MIN(clock_time)在去重后的表上做才能保证准确如果直接在原始表上取最小值员工在闸机前多刷几次就会把上班时间提前。实际系统里我会把去重放在写入阶段完成而不是查询阶段这样报表查询时不需要重复做窗口去重。4.3 识别通过后如何写入考勤记录import sqlite3 from datetime import datetime def punch(emp_id: str, similarity: float, device_id: str, conn: sqlite3.Connection): 写入一次打卡事件5 分钟内同一个人重复识别直接丢弃。 now datetime.now().strftime(%Y-%m-%d %H:%M:%S) dup conn.execute( SELECT COUNT(*) FROM attendance WHERE emp_id ? AND clock_time datetime(?, -5 minutes) , (emp_id, now), ).fetchone()[0] if dup: return False conn.execute( INSERT INTO attendance (emp_id, clock_time, device_id, similarity) VALUES (?, ?, ?, ?), (emp_id, now, device_id, similarity), ) conn.commit() return True写入逻辑说明去重查询里用datetime(?, -5 minutes)生成窗口起点避免在代码里做时间字符串拼接。similarity字段保留识别时的相似度方便后续排查阈值过低导致的误打卡。写入失败时整个事务回滚避免摄像头端显示成功但库里没记录的幽灵打卡。4.4 上班高峰的并发压力先别怪模型用 JMeter 压一下接口人脸识别考勤系统在早上 8:50 到 9:10 之间会迎来瞬时并发几十个人同时站在摄像头前刷脸。这时候设备端的推理耗时是一方面更大的瓶颈往往是识别服务接收图片、写库、返回结果的 HTTP 链路。拿 JMeter 测试人脸识别接口时我会把注册照片变成 Base64 字符串作为 POST 参数设置线程组模拟 20 个并发用户循环打卡观察接口的响应时间分布。如果发现 P95 响应时间超过 1 秒先看是不是 SQLite 写锁冲突再看识别服务里有没有把图片解码放到请求线程里。这两个问题通常比 CNN 推理更先暴露。SQLite 在高并发写入场景下表现一般如果打卡设备数量超过 10 台我会把 attendance 表迁到 MySQL 或 PostgreSQLface_features 表因为写入频率低留在 SQLite 反而更简单。5. 越用越准线上考勤系统最常调的 4 个识别参数5.1 识别阈值0.4 到 0.55 之间怎么量着定默认阈值 0.5 只适合 Demo。考勤系统上线前我会收集三类样本同一员工不同时段的抓拍、不同员工的抓拍、员工拿工牌照片刷脸的抓拍分别算相似度画出分布。正常情况是同一个人相似度集中在 0.55~0.8不同人集中在 0.1~0.35中间的空档就是阈值可调区间。如果两类分布重叠严重说明特征质量差调阈值没用要先查对齐或图像质量。import numpy as np def pick_threshold(pos_scores: list[float], neg_scores: list[float], max_far: float 0.001) - float: 在满足误识率上限的前提下选使通过率最高的阈值。 best_t, best_tar 0.5, 0.0 for t in np.arange(0.3, 0.7, 0.005): far sum(1 for s in neg_scores if s t) / len(neg_scores) tar sum(1 for s in pos_scores if s t) / len(pos_scores) if far max_far and tar best_tar: best_t, best_tar round(t, 3), tar return best_t, best_tar参数说明max_far表示允许的最大误识率考勤场景我建议定在 0.1% 以下也就是一万次刷脸最多误通过一次pos_scores是同一人两张照片的相似度集合neg_scores是不同人照片的相似度集合。这套方法把阈值从拍脑袋变成了数据决策而且每次重录底库后都能重新跑一遍。5.2 摄像头安装距离决定检测参数人脸识别考勤系统最常见的部署问题是摄像头装得太高或者太远导致人脸在画面里只有几十个像素宽。检测模型看的是整张图det_size决定内部缩放后的分辨率det_thresh决定最低置信度。摄像头装在 1.2 米高度、人脸宽度占画面 1/5 时det_thresh用 0.5 没问题如果是吸顶安装仰拍det_thresh要降到 0.3 左右同时把det_size从 640×640 提到 960×960否则漏检率会很高。这里有个常见误区det_thresh调低会增加误检框表现为画面里出现乱七八糟的框这时候不是继续调低阈值而是检查是不是把墙上的海报、显示器里的照片当成了人脸。考勤机和人脸识别门禁机的差异就在这体现——门禁机通常用结构光或双目摄像头限制只能识别真人而普通 USB 摄像头方案只能靠算法过滤。5.3 底库超过 1000 人后特征存储要换索引用 npy 文件存特征加载 100 人时每张 npy 只有 4KB全部读一遍只要十几毫秒到了 1000 人每次识别都要遍历 1000 个文件磁盘 IO 时间可能超过 CNN 推理时间。这时候常见做法是把特征全部加载到内存用 NumPy 矩阵一次算完所有点乘或者引入 FAISS 这类向量索引。在写考勤系统的批量导入功能时也要注意不要逐个人调用识别接口提取特征而是一次性把照片路径列表喂给批量任务先把特征算完再统一写入数据库。def batch_extract(photo_dir: str, conn: sqlite3.Connection, engine): 批量提取特征并写入 face_features 表适合初始化底库或换模型后重算。 rows [] for photo_path in Path(photo_dir).glob(*.jpg): emp_id photo_path.stem img cv2.imread(str(photo_path)) feat engine.get_embedding(img) rows.append((emp_id, feat.tobytes(), v2)) conn.executemany( INSERT OR REPLACE INTO face_features (emp_id, feature, model_version) VALUES (?, ?, ?), rows, ) conn.commit()逻辑说明feat.tobytes()把 float32 数组序列化成 BLOB读取时用np.frombuffer(blob, dtypenp.float32)还原。批量提取的价值不只是快更重要的是保证底库里所有特征出自同一个model_version——否则第 2 章说的新旧权重混用导致全都不认识就会出现。换模型后只需要把v2改成新模型的版本号全表重跑一遍即可。5.4 高频误报的三类样本镜子、工牌照片、远处的侧脸考勤系统上线后收到的打卡异常投诉大部分不是误识而是漏识。漏识最多的三种情况一是员工戴着帽子或口罩五官被遮挡严重二是画面里有镜子检测模型在镜子里也检测到了人脸三是员工从摄像头侧面快步走过侧脸角度太大。针对第 2 种我不会去优化检测模型而是在业务层加限制——考勤机通常只取画面里宽度最大的那张脸作为打卡对象针对第 1 种和第 3 种靠的是连续多帧确认策略同一人在 1 秒内的 5 帧里至少 3 帧识别为同一员工才写入打卡记录而不是单帧通过就打卡。6. 收尾动作写一个自检脚本把阈值和误识率都变成看得见的报告系统上线后最容易被质问的一个问题就是你凭什么把阈值设在 0.5。与其解释不如把自检脚本写进工程里定期生成一份识别质量报告。做法是维护一个测试集每个员工注册照之外再采集两张现场照作为正样本对再随机组合不同员工的照片作为负样本对然后批量计算相似度输出报告。import csv import random from pathlib import Path import numpy as np from face_engine import FaceEngine engine FaceEngine(det.onnx, rec.onnx) def build_pairs(db_dir: str, live_dir: str, n_neg: int 500): 构造正负样本对正样本同一人注册照vs现场照负样本随机双人组合。 emps list(Path(db_dir).glob(*.npy)) pairs [] for npy in emps: # 现场照命名约定: {emp_id}_live.jpg 放在 live_dir live_path Path(live_dir) / f{npy.stem}_live.jpg if live_path.exists(): pairs.append((pos, npy.stem, str(live_path))) all_ids [p.stem for p in emps] for _ in range(n_neg): a, b random.sample(all_ids, 2) pairs.append((neg, a, b)) return pairs def generate_report(pairs, out_csv: str similarity_report.csv): 跑完全部样本对输出相似度分布和推荐阈值。 rows, pos_scores, neg_scores [], [], [] for label, id_a, id_b in pairs: if label pos: # 现场照实时提特征与注册底库特征比较 feat_live engine.get_embedding(cv2.imread(id_b)) feat_db np.load(Path(face_db) / f{id_a}.npy) score float(np.dot(feat_live, feat_db)) pos_scores.append(score) else: feat_a np.load(Path(face_db) / f{id_a}.npy) feat_b np.load(Path(face_db) / f{id_b}.npy) score float(np.dot(feat_a, feat_b)) neg_scores.append(score) rows.append((label, id_a, id_b, round(score, 4))) with open(out_csv, w, newline) as f: writer csv.writer(f) writer.writerow([label, id_a, id_b, similarity]) writer.writerows(rows) # 推荐阈值的判定标准负样本误识率不高于 0.1% best_t, best_tar pick_threshold(pos_scores, neg_scores, max_far0.001) print(f正样本对数量: {len(pos_scores)}, 负样本对数量: {len(neg_scores)}) print(f推荐阈值: {best_t}, 对应通过率: {best_tar:.2%}) return best_t这个脚本解决的是更新底库后要不要调阈值的维护问题。每次新增员工、更换摄像头或换预训练模型就重新生成一次similarity_report.csv并归档如果推荐阈值和线上阈值偏差超过 0.03说明上线后的环境发生了变化需要把新阈值同步到考勤识别服务里。报告文件本身也应该按日期命名留存比如similarity_report_20250115.csv这样事后追溯某天开始误识率变高时可以直接对照报告数据定位是阈值漂移还是底库特征问题。本文还有配套的精品资源点击获取
返回列表