ARTICLE DETAIL

资讯详情

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

Python+CNN人脸识别:从原理到阈值校准实战解析

Python+CNN人脸识别:从原理到阈值校准实战解析 简介面向希望掌握深度学习人脸识别完整流程的 Python 初学者与计算机视觉开发者这份资料以 CNN 为主线覆盖数据预处理、模型架构设计、训练优化、评估与部署等关键环节对应安全监控、门禁系统、移动设备身份验证等常见应用场景。压缩包共 2000 个文件约 363.34MB1160 张 jpg 与 833 张 png 构成人脸样本数据5 个 py 文件负责数据读取、模型训练与测试1 个 pt 文件提供预训练权重txt 文件可用于核对实验说明。通过学习代码与权重可复现基于卷积神经网络的人脸识别实验并参考其中的图像归一化、尺寸调整、数据增强与超参数调优思路替换数据集或调整网络结构进一步理解 VGGFace、FaceNet 等模型的工程落地。已有 179 人学习下载适合作为课程设计、毕业设计或入门深度学习项目的参考资料。1. 基于Python-CNN的人脸识别为什么值得自己复现一遍如果你是从毕设、课设或者想做个门禁demo的角度下载这个zip大概率会经历同一个心理过程解压后看到一堆.py文件和缺失的datasets/目录第一反应是这不是一个能双击运行的东西。但换个角度看这份代码恰恰是理解深度学习最完整的闭环——它把图片输入、卷积神经网络、特征提取、分类比对这四件事全部串在了一条本地可复现的流水线里。基于Python-CNN的人脸识别核心并不深奥用CNN替代早期的人脸描述子比如LBP、HOG从人脸图像里学出一个固定长度的特征向量再用向量距离判断两个人是不是同一个人。它解决的是离线、低成本、可自行改造的人脸识别问题不需要云服务也不需要厂商SDK。适合三类人准备做毕业设计的学生、刚把Python安装和环境配置跑通、想从分类任务迈向真正应用的入门者以及需要给小型场景做原型验证的工程师。把这份代码吃透一遍比刷十篇CNN卷积神经网络的科普文章都有用。2. 人脸识别不是一张CNN就完事先分清检测、特征、比对这四步2.1 检测层为什么通常选轻量方案而不是一上来就上大CNN很多人拿到这类代码会习惯性搜人脸识别CNN原理然后误以为整个过程就是把整张图片丢进一个神经网络、输出这人是谁。真实落地流程要长得多第一步是检测第二步是对齐第三步才是特征提取第四步是比对判断。四个步骤在代码里往往对应不同的模型和函数混在一起看很容易卡住。先看检测。检测层的任务是回答图里有没有人脸人脸在哪典型输出是一个矩形框x, y, w, h。业界常见做法是OpenCV的Haar级联、OpenCV DNN模块里的轻量人脸检测模型或者MTCNN这种小型CNN检测器。这套基于Python-CNN的方案里如果作者选的是MTCNN检测部分本身就是一个小CNN如果选的是OpenCV的Haar特征那检测和识别用的是完全两套技术千万别以为CNN指的是检测阶段。我一般会建议在本地CPU环境优先用OpenCV DNN的res10_300x300_ssd_iter_140000.caffemodel或者MTCNN的Pytorch实现。原因很实在这些模型在单张图上检测耗时在几十毫秒到一两百毫秒之间而一个大号的检测网络比如YOLO系列在CPU上跑视频流会卡到没法用。人脸识别场景里检测对象尺度相对固定背景相对干净不需要太重的检测器。真正决定识别性能的是下一步的特征提取网络。2.2 CNN的最后一层不是拿来比相似度的取出embedding才是关键这是整套代码里最容易被误解、也最值得亲手验证的地方。分类网络最后一层通常是Linear(num_classes)加Softmax输出长度等于训练时的人脸类别数。比如训练数据有50个人最后输出就是50维。很多新手会直接把这一层输出的50维向量当特征去算相似度结果发现效果很差——因为Softmax概率是为分类服务的它对类别间可分做了优化对同一人不同照片应靠近并没有直接约束。这就要说到embedding的概念。embedding是网络中倒数第二层或者专门设计的特征层输出的向量常见维度是128、256、512。训练时网络通过分类损失学会把这个向量空间映射成同一人聚成一团、不同人尽量分开。识别时用的是这个向量而不是分类层输出。业界熟悉的人脸识别方案里FaceNet用的是Triplet Loss学embeddingArcFace在Softmax上加了角度边界让同类更聚拢。但这套毕设级Python-CNN项目通常不会那么复杂最朴素的改法是训练时用Softmax分类推理时把模型最后一层Linear截掉拿前面一层的输出当特征向量再做归一化最后用余弦距离或欧氏距离比对。提取embedding的代码长这样import torch import torch.nn.functional as F # model 是训练好的分类模型embedding_dim 是最后一层 Linear 的输入维度 def extract_embedding(model, face_tensor): 输入经过预处理的人脸图输出归一化后的特征向量 model.eval() with torch.no_grad(): # 把模型最后一个全连接层之前的部分视为特征提取器 # 常见写法是注册一个 forward 钩子或直接调用 model.features(x) features model.features(face_tensor) # shape: [1, embedding_dim] embedding F.normalize(features, p2, dim1) # 归一化到单位长度 return embedding.cpu().numpy().flatten()逻辑说明先切换到eval()模式——这行很容易漏训练模式下BatchNorm和Dropout行为不同会导致同一张照片两次提取的向量不一样。F.normalize把向量长度归一化为1这样两个embedding的余弦相似度就等于它们的点积计算成本低且数值稳定。特征比对时余弦相似度接近1代表同一个人接近0代表完全不同实际阈值通常在0.4到0.7之间具体要看训练集与网络能力。参数说明embedding_dim由模型结构决定常见取128或256。128维在几十个人的小数据集上完全够用内存占用小后续存人像库时压力也小256维容错性稍好但训练和推理耗时略有增加。不建议一上来就搜一个512维的预训练模型数据量跟不上时高维特征更容易过拟合。2.3 识别任务的评估指标别只看准确率看阈值下的精确率与召回率分类任务可以轻松说准确率95%因为每张图都有一个人名标签预测错了就是错了。人脸识别不是这样判断这张脸是不是张三取决于你设的相似度阈值。阈值严陌生人会被拒之门外但张三换个光线自己人也进不来阈值松谁都能过闸机但误认率上升。因此正规做法是把数据分成训练集和验证集验证集里既包含已知人员的照片也包含陌生人的照片然后在验证集上扫描阈值画出误识率随阈值变化的曲线。这节先记住一个原则以后看到任何声称识别率高的项目先问一句阈值设在多少再问陌生人测试集有吗。没有这两个信息准确率这个数字只是安慰自己用的。在代码层面验证阶段做的事是对每个测试样本提取embedding计算它与库中所有已注册样本的余弦相似度取最高值若最高值超过阈值则判为该身份否则判为未知。def evaluate_threshold(feature_bank, labels, query_features, query_labels, threshold): 用阈值扫描验证集返回精确率、召回率、误识率 correct_known 0 total_known 0 false_accept 0 total_unknown 0 for feat, true_id in zip(query_features, query_labels): # feature_bank 是字典: {person_id: [list of embeddings]} best_score -1.0 best_id None for pid, feats in feature_bank.items(): for ref_feat in feats: score float(feat ref_feat.T) # 归一化后点积即余弦相似度 if score best_score: best_score score best_id pid if best_score threshold: pred_id best_id else: pred_id unknown if true_id unknown: if pred_id ! unknown: false_accept 1 total_unknown 1 else: total_known 1 if pred_id true_id: correct_known 1 precision correct_known / (correct_known false_accept 1e-6) recall correct_known / (total_known 1e-6) return precision, recall逻辑说明这段代码把所有已知人员的参考图预先存在feature_bank里查询时对每张验证图算与参考向量的最大余弦相似度。若超过阈值就接受这个身份否则判成未知。unknown样本在这个逻辑里扮演闯入者角色能让阈值选择有依据。参数说明threshold是全局标量可在0.3到0.8之间以0.02步进扫描。实际项目里门禁场景会选误识率尽可能低的点比如0.02以下哪怕召回率只有0.85也能接受考勤场景会选召回率和精确率的平衡点因为员工不会愿意反复刷脸失败。3. 收到zip后怎么最快跑通目录对照、环境准备与第一次运行3.1 目录结构对照多数项目包里一定有这几个文件不同来源的zip包目录名差异很大但骨架高度相似。我拿到一个Python-CNN人脸识别zip第一步不是装环境而是打开压缩包看目录结构确认它是PyTorch版本还是TensorFlow版本。判断方法很简单看代码里是import torch还是import tensorflow或者看requirements.txt里写了什么。PyTorch版本近几年在学术和毕设里占多数下面以它为例讲解。常见目录结构如下. ├── datasets/ # 数据目录按人名/ID分子文件夹 │ ├── person_01/ │ │ ├── 001.jpg │ │ ├── 002.jpg │ │ └── ... │ ├── person_02/ │ └── ... ├── models/ │ └── best_model.pth # 训练好的权重 ├── train.py # 训练脚本 ├── recognize.py # 单张图片识别/摄像头识别 ├── utils.py # 预处理、数据加载等公共函数 ├── requirements.txt └── README.md逻辑说明datasets是训练数据models存放权重文件。recognize.py是判断这个包质量的关键文件——很多毕设包只写了训练识别部分要自己补如果只有 train.py 没有识别脚本后面3.3的小节会给出一个最小可用的推理脚本。建议拿到包后先看一眼 README了解它是训练识别完整还是只有训练这决定你要不要自己动手补代码。参数说明best_model.pth这种文件名很常见说明代码里在训练过程中按验证集指标保存了最优模型。如果你解压后 models 目录为空那就必须自己跑一遍训练如果 weights 文件存在可以直接跳到3.3节先验证推理通不通。3.2 环境准备Python版本与依赖CPU机器也跑得动环境配置是人脸识别项目翻车的重灾区。多数情况下这类项目依赖的库并不刁钻常见的坑集中在Python版本不对、OpenCV安装失败、PyTorch装成了CPU版本但代码里硬要cuda()。我给一个保守且稳定的环境组合conda create -n face_cnn python3.9 -y conda activate face_cnn # 先装 PyTorch CPU 版保证一定能跑通 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 再装图像与计算依赖 pip install opencv-python numpy matplotlib scikit-learn # 如果包里有 requirements.txt只装缺口部分不要整体覆盖 pip install -r requirements.txt逻辑说明先装torch再装其他库是为了避免requirements.txt里写死的torch版本把环境搞乱。CPU版torch对入门项目完全够用——几十个人的小数据集在CPU上练一个浅层CNN每轮也就几分钟不会慢到不能接受。OpenCV主要负责cv2.imread、cv2.rectangle这些操作图片读入和画框都靠它。参数说明Python选3.9是因为这版本兼容性最稳3.10、3.11也能用但如果你要跑一些老的MTCNN库比如带编译过的Cython代码3.9的坑最少。--index-url指定了CPU源装出来是cpu后缀的torch版本。安装完成后用python -c import torch; print(torch.__version__)验证能打印版本号就说明环境没坏。3.3 用CPU跑一次训练再跑一次识别脚本环境就绪后先按包里 README 的说明跑训练。绝大多数这类项目训练入口长这样python train.py --data ./datasets --epochs 30 --batch-size 32 --lr 0.001跑之前要确认数据目录里每个子文件夹都有足够照片。如果包自带的示例数据只有每个人三四张图训练出来的模型基本不能用于实际识别——后面第4章会讲数据要求。训练结束后识别脚本的调用通常有两种风格。一种是直接单图识别python recognize.py --model ./models/best_model.pth --image ./test_me.jpg另一种是摄像头实时识别python recognize.py --model ./models/best_model.pth --camera 0如果包里只有训练代码没有推理代码那就需要自己写一个最小的识别脚本。这个脚本会直接体现你对这套方案的理解程度我给一个能直接改着用的版本import cv2 import torch import numpy as np import torch.nn.functional as F def load_model(model_path, embedding_dim128, num_classes10): 按训练时的结构重建模型再载入权重 # 这里需要根据包里 train.py 的模型定义替换成实际结构 # 常见命名model SimpleFaceCNN(embedding_dim128, num_classes50) model SimpleFaceCNN(embedding_dimembedding_dim, num_classesnum_classes) model.load_state_dict(torch.load(model_path, map_locationcpu)) model.eval() return model def preprocess(face_img, size(112, 112)): 把检测到的人脸块做缩放和归一化必须与训练预处理一致 face cv2.resize(face_img, size) face face.astype(np.float32) / 255.0 face (face - np.array([0.5, 0.5, 0.5])) / np.array([0.5, 0.5, 0.5]) face torch.from_numpy(face).permute(2, 0, 1).unsqueeze(0) return face def recognize_from_image(model, image_path, registered_bank, threshold0.55): img cv2.imread(image_path) # 检测部分用 OpenCV DNN 或 MTCNN 拿到人脸框这里省略 # 假设已经得到 face_crop face_tensor preprocess(face_crop) with torch.no_grad(): embedding model(face_tensor) embedding F.normalize(embedding, p2, dim1).numpy().flatten() best_score -1.0 best_name unknown for name, ref_embeddings in registered_bank.items(): for ref_emb in ref_embeddings: score np.dot(embedding, ref_emb) if score best_score: best_score score best_name name if best_score threshold: best_name unknown return best_name, best_score逻辑说明load_model里的模型结构定义必须和训练时完全一致否则load_state_dict会报 key 不匹配。preprocess里的归一化参数均值0.5、标准差0.5是我常用的简化方案如果训练代码里用的是ImageNet的均值和标准差这里就必须同步替换预处理不一致是推理效果差的头号原因。识别逻辑就是遍历注册库里所有参考向量取最大余弦相似度再与阈值比较。参数说明threshold0.55是个起点不是最优值。不同网络、不同数据集训练出的embedding分布差异很大后面第5章会讲如何用验证集把阈值定准。size(112,112)是轻量人脸网络的常见输入尺寸如果训练代码里是resize((160,160))这里必须改成160否则网络会按错误尺寸的输入跑。4. 自己完整训练一遍数据集组织、模型结构与必调超参4.1 数据怎么组织一个人一个文件夹每个ID至少20到30张人脸识别训练数据的组织方式直接影响代码运行和最终效果。常见做法是让数据加载器扫描datasets下的子文件夹每个子文件夹名就是一个人ID文件夹里放着这个人的多张人脸照片。OSROpen Set Recognition场景下测试阶段必须引入训练集中没见过的人但训练集本身要保证每个ID有足够的样本。样本数量上我的经验线是每个ID少于10张模型基本只能记住特定光照下的样子换角度直接翻车大于等于20到30张配合数据增强才能学到这个人长什么样而不仅仅是训练集里这个人长什么样。数据来源通常是摄像头定时抓拍每0.5秒存一帧或从一段视频里抽帧然后做检测裁剪。两种方式都行关键是清洗。datasets/ ├── zhangsan/ │ ├── 001.jpg │ ├── 002.jpg │ ├── ... │ └── 030.jpg ├── lisi/ └── wangwu/逻辑说明文件夹名即身份标签建议用拼音或工号不要用中文——某些Windows环境中文路径在OpenCV读取时会出奇怪问题。放进去之前先人工过一遍删除模糊的、裁剪错的、把多个人脸框在一块的图。模糊图对CNN训练是玄学级别的伤害它会把网络往学纹理噪声的方向带偏。参数说明类的数量就是文件夹数量。如果只有5个人网络结构里num_classes就应该是5而embedding_dim与类别数无关可以保持128。训练时batch_size建议小于等于ID数量乘以2——因为每个batch希望尽量采到不同人的样本这样batch内类别均衡模型收敛更稳。4.2 模型结构用小型残差CNN而不是动辄几十层的ResNet很多毕设项目给的默认模型是ResNet50或ResNet18。ResNet50在一两百类的小数据集上属于典型的容量过剩训练慢、容易过拟合、权重文件上百MB。我一般会建议换成一个自定义的小型残差网络层数浅但保留残差连接和BatchNorm足够应付注册人数在几十到一两百的场景。这里给一个可以直接换成训练代码里的模型结构import torch.nn as nn class ResidualBlock(nn.Module): def __init__(self, in_ch, out_ch, stride1): super().__init__() self.conv1 nn.Conv2d(in_ch, out_ch, 3, stride, 1, biasFalse) self.bn1 nn.BatchNorm2d(out_ch) self.conv2 nn.Conv2d(out_ch, out_ch, 3, 1, 1, biasFalse) self.bn2 nn.BatchNorm2d(out_ch) self.shortcut nn.Sequential() if stride ! 1 or in_ch ! out_ch: self.shortcut nn.Sequential( nn.Conv2d(in_ch, out_ch, 1, stride, biasFalse), nn.BatchNorm2d(out_ch) ) self.relu nn.ReLU(inplaceTrue) def forward(self, x): identity self.shortcut(x) out self.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) out identity return self.relu(out) class SimpleFaceCNN(nn.Module): 输入 112x112 彩色人脸图输出 embedding 和分类 logits def __init__(self, embedding_dim128, num_classes50): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, 3, 1, 1, biasFalse), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), ResidualBlock(32, 32), ResidualBlock(32, 64, stride2), ResidualBlock(64, 64), ResidualBlock(64, 128, stride2), ResidualBlock(128, 128), nn.AdaptiveAvgPool2d(1) # 全局平均池化把特征图压成 128 维 ) self.embedding nn.Linear(128, embedding_dim) self.classifier nn.Linear(embedding_dim, num_classes) def forward(self, x): x self.features(x) # [B, 128, 1, 1] x x.view(x.size(0), -1) # [B, 128] emb self.embedding(x) # [B, embedding_dim] logits self.classifier(emb) return logits逻辑说明这是一个缩小版的类ResNet结构。ResidualBlock的残差连接让梯度在浅层也能流畅回传BatchNorm2d对少量数据特别友好能稳住分布加速收敛。AdaptiveAvgPool2d(1)把每个通道的特征图压成一个值参数少、不易过拟合。features部分的输出在推理时可以被单独调用正好对应第2章的embedding提取逻辑。参数说明embedding_dim128是特征向量长度别设太小低于64会损失区分度也别被越高越好误导——128在人脸识别小项目里是足够好且开销适中的选择。num_classes在训练时传真实类别数推理时这个分支可以完全丢掉。通道数从32逐级增加到128在CPU上跑一轮几十人的训练也就几分钟比ResNet50快好几倍。4.3 训练主循环用Softmax交叉熵数据量不够时别轻易上Triplet Loss常见做法里小数据集上最稳的是普通多分类训练。不少人看到FaceNet的Triplet Loss很兴奋但在二三十个人的小数据集上Triplet采样困难一旦batch内没有合适的正负样本loss会震荡得让人想弃坑。我自己的经验是先用Softmax交叉熵把模型训到一个能用的基线如果确实需要进一步提升同类聚类效果再引入ArcFace的margin而不是一上来就搞metric learning。给一个标准的PyTorch训练循环核心import torch import torch.nn as nn from torch.utils.data import DataLoader def train_one_epoch(model, dataloader, optimizer, criterion, device): model.train() total_loss 0.0 correct 0 total 0 for imgs, labels in dataloader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() logits model(imgs) loss criterion(logits, labels) loss.backward() optimizer.step() total_loss loss.item() preds logits.argmax(dim1) correct (preds labels).sum().item() total labels.size(0) return total_loss / len(dataloader), correct / total逻辑说明每个batch三件事清空梯度、前向算出logits与loss、反向传播并更新参数。argmax(dim1)取概率最大的类别作为训练阶段的预测这里的准确率只能反映分类任务的表现与最终识别效果的对应关系是间接的。device建议在训练脚本开头统一设置CPU就写torch.device(cpu)有GPU再切cuda这能避免模型在CPU上建了一半、数据在GPU上另一半的尴尬报错。参数说明criterion用nn.CrossEntropyLoss()不需要额外改权重因为数据组织时已尽量保证每类数量接近。优化器用Adam初始学习率1e-3每10个轮次乘0.1降一次。batch_size在CPU上取16到32比较舒服取8容易让梯度噪声过大训练不稳。4.4 超参数表第一条保存路径与调参基线我在训练这类项目时有一套默认参数基线先按这个跑再根据loss曲线微调超参数推荐值说明输入图像尺寸112 × 112太小丢细节太大训练变慢112是轻量方案的常见折中batch_size32CPU内存够就32不够降到16embedding_dim128特征向量长度不随类别数变化初始学习率0.001Adam默认建议数据很少时降到0.0005学习率调度每15个epoch衰减0.1用ReduceLROnPlateau看验证loss也可以训练轮数3050小数据集30轮左右就能收敛再多会过拟合数据增强水平翻转、随机亮度、HSV扰动每次做翻转和颜色抖动能显著提升跨场景鲁棒性检查点保存方面我习惯每个epoch看一次验证集准确率只保存验证loss最低的那个权重文件名里带上epoch和loss值比如best_model_epoch_20_loss_0.23.pth。这样后续复盘训练过程时一眼能看出哪一步开始过拟合。模型文件保存时用model.state_dict()而不是整个model对象——前者只保存参数体积小加载时结构还得按代码重建后者把网络结构也序列化进去换环境加载容易出兼容性问题。5. 要识别得稳先把这5个坑排掉5.1 摄像头里谁都像目标阈值设太低现象系统对张三的识别结果出现在李四脸上甚至对一张桌子上的海报都给出相似度0.6导致大量误识。原因祝贺你你遇到了人脸识别的经典翻车——阈值拍脑袋定了0.5或更低。不同模型训练出的embedding分布差异极大同一对本人照片的相似度在不同模型下可能从0.4到0.9之间波动。0.5这个数字对某些模型意味着张三和李四也很像。解决用第2章写过的evaluate_threshold那段代码在验证集上以0.02步进扫描阈值画出误识率对陌生人样本的接受率和漏识率对本人样本的拒绝率两条线取两条曲线的交叉点或者按场景需求取误识率小于0.02的最高阈值。我实践中发现轻量CNN模型训练得一般时有效阈值往往在0.6以上设置过低带来的灾难远大于阈值偏高带来的不便。5.2 自己拍的照片总识别失败预处理不一致现象训练时跑得很好验证集准确率95%但拿手机随便拍一张自己照片丢进去结果永远是unknown。原因九成情况是预处理不一致——训练代码把人脸图resize到112×112并做了归一化而推理代码要么没resize要么用了不同的缩放方式或者是RGB和BGR通道序搞混了——OpenCV读图默认BGR而训练时若用PIL读图是RGB模型学到的颜色分布就错位了。解决把预处理封装成独立函数get_preprocess_pipeline()训练和推理两个脚本都调用它。检查三件事resize尺寸是否一致、归一化均值标准差是否一致、通道顺序是否一致。调试方法很简单用同一张训练图片分别走训练代码和推理代码比对提取出的embedding是否相同——如果不同说明管线有差异这个问题在模型识别烂之前必须先修好。5.3 模型文件动辄几百MB网络容量对一两百类人严重过剩现象训练完生成的.pth文件有300MB甚至更大拷贝到别的机器费半天加载也要好几秒。原因常见做法是用了ImageNet预训练的ResNet50当backbone并且保存了整个model对象包括优化器状态和完整图结构。一个人脸识别小项目数据里可能就几十个人ResNet50最后一层全连接有2000多万参数绝大部分权重和识别这几十个人毫无关系。解决给embedding部分单独开一层1024或128维的线性层模型结构换成第4章的小型残差网络。保存时用torch.save(model.state_dict(), path)。这样模型文件通常能压到20到60MB之间加载速度快一个数量级。如果还嫌大可以用半精度保存state_dict体积再减一半CPU推理时再转回float32——精度损失在可接受范围内。5.4 训练时loss不降甚至发散先查学习率再看数据顺序现象训练日志里loss在前面几个epoch下降随后开始剧烈震荡甚至跳到初始值的几倍GPU如果用了占用正常但模型越训越烂。原因最常见的是学习率太大导致loss发散。另一类隐蔽原因是DataLoader没做shuffle每个batch里全是同一个人梯度朝一个方向猛冲再被下一个人拽回来训练过程像拉锯战。还有一类是标签错位——文件夹名或label排序在跨平台时不一致比如Windows下文件读取顺序和Linux不一样造成模型学到的标签与实际身份错位。解决先把初始学习率降到0.0003到0.0005重新跑确认DataLoader(..., shuffleTrue)对标签排序做显式处理比如在读取数据集时对文件夹名先sorted()再编号。同时打印batch内标签分布连续几个batch如果出现同一ID占大半就要怀疑采样逻辑而不是网络结构。5.5 验证集准确率很高、换场景就翻车数据太干净了现象在实验室环境拍的验证集上精度接近满分拿到走廊、户外或另一台摄像头前立刻下降到不可用。原因训练数据是在同一个摄像头、同一时间段、差不多的角度拍的模型学到的特征大量混入了背景、光照、摄像头色彩倾向。验证集又来自同一场景指标自然虚高。这种场景偏置是人脸识别小项目最常见的自我感动。解决在训练数据准备阶段就有意识地做增强——随机亮度、对比度、色温抖动、轻微模糊水平翻转必开。采集数据时尽量让每个人在不同时段、不同角度、不同背景下各拍几张。更重要的验证手段是单独准备一个跨场景测试集用另一个摄像头或另一个房间里拍的照片做验证这比任何loss调整都有说服力。如果跨场景测试不过关优先增加数据多样性而不是加宽网络。6. 最后要做的事用特征库替代重训练以及阈值定标方法6.1 不用重训练加新人特征库模式模型训好之后最常见的一个问题来了个新员工怎么把他加进系统很多新手第一反应是重训模型实际上完全没必要。常见做法是把CNN当作特征提取器新员工只需要提供少量照片提取出embedding存进注册库里即可。下一次识别时模型先提取待识别人脸的embedding再与注册库里的所有向量比对取最相似的那个决定身份。这样做的好处是注册成本极低——新增一个人不需要重新训练模型存储的只是一个128维浮点向量约0.5KB。我一般会把人脸特征按person_id分组存成两个文件feature_bank.npy存所有向量feature_labels.json存对应的ID映射。比对方块实时扫描注册库几十个人的库全量比对也就几毫秒完全没必要上faiss这类索引工具。真正的瓶颈永远在检测和预处理不在特征比对。6.2 阈值定标在验证集上扫描别猜0.5阈值是最后一道玄学关卡。所谓合适的阈值取决于你的场景门禁场景宁可把熟人拒之门外也不能放陌生人进来阈值要往高选考勤场景则希望员工刷卡刷脸体验顺畅可以容忍少量误识。我习惯的训练收尾流程是训练结束后单独写一个calibrate_threshold.py在验证集上扫描0.30到0.80的阈值每0.02步进输出误识率、漏识率、精确率、召回率四列。然后在场景规则下选阈值把这个值写进config.json。注意验证集的人必须包含训练集以外的ID否则未知人这一项无法验证选出的阈值等于没校准。import json best_f1 0.0 best_threshold 0.5 for t in [round(x * 0.02 0.30, 2) for x in range(26)]: precision, recall evaluate_threshold(feature_bank, labels, query_features, query_labels, t) f1 2 * precision * recall / (precision recall 1e-6) print(fthreshold{t:.2f} precision{precision:.4f} recall{recall:.4f} f1{f1:.4f}) if f1 best_f1: best_f1 f1 best_threshold t print(fbest_threshold{best_threshold:.2f} f1{best_f1:.4f}) with open(config.json, w) as f: json.dump({threshold: best_threshold}, f)逻辑说明这段代码本质是穷举法把阈值扫一遍用F1分数选出平衡点作为初始值。实际部署时可以再按场景偏好把阈值往上或往下微调。evaluate_threshold函数就是第2章里那段验证代码这里复用即可。在门禁场景我会把选出的最优阈值再加0.05到0.1宁严勿松。6.3 我现在的收尾习惯每次做完这类Python-CNN人脸识别项目我会固定做三件事第一用另一个摄像头哪怕是手机录5个已知ID各10张照片作为跨场景验证集跑一遍识别率低于80%就回头补数据或调阈值不急着交付第二录20张陌生人的照片验证未知人是否会被正确拒识——这个指标比已知人识别率更能反映系统是否可用第三把config.json、feature_bank.npy、模型权重这三样产物固定下来连同calibrate_threshold.py一起放进项目目录方便后续新场景下重新校准。人脸识别这类项目问题往往不在CNN本身而在数据质量、预处理一致性和阈值选择这些看不见的地方。把这三件事做扎实比换更深的网络带来的提升明显得多。这份经验是我在好几个类似项目里踩坑换来的希望帮到你。本文还有配套的精品资源点击获取
返回列表