ARTICLE DETAIL

资讯详情

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

基于TensorFlow的人脸识别实战:从数据对齐到ArcFace训练与部署

基于TensorFlow的人脸识别实战:从数据对齐到ArcFace训练与部署 简介面向TensorFlow初学者与毕业设计开发者的完整人脸识别神经网络实战教程以卷积神经网络为核心讲解人脸特征提取与身份识别原理帮助读者从零搭建可运行的识别模型。压缩包共含6个文件以4个Python源码脚本为主体依次实现采集自己的人脸样本、构建他人参照数据集、训练人脸分类模型与实时判别是否为目标身份另附README说明文档与LICENSE开源许可包体仅14KB结构清晰便于逐模块学习。资源从运行环境搭建讲起覆盖Window/Linux双系统下的Python 3.x与TensorFlow配置并完整呈现两组数据集的准备思路直观展示如何让模型记住目标人脸并区分他人。目前已有626人浏览学习适合动手实践CNN图像识别、开展人脸识别课程设计或毕业设计的开发者直接套用与二次开发。1. 基于TensorFlow的人脸识别神经网络毕设最容易翻车的环节不在模型结构做这个方向的同学十个里有九个一开始就在纠结网络结构结果真正卡住他们的往往是三件事TensorFlow环境装不上、数据集没对齐、训练loss一直不降。这个标题要解决的问题很直接用TensorFlow 2把一条人脸识别链路完整走通从数据准备、网络搭建、损失函数到训练和评估。适合正在开题、或者已经训练到一半发现效果不对的同学。先说结论毕设级的人脸识别骨干网络直接用预训练模型不要从零训练CNN你真正要动手改的是数据管线、ArcFace损失和训练节奏这三件事做对了识别率不会差。2. 把训练环境坐实TensorFlow安装、数据集选型与对齐脚本2.1 TensorFlow安装GPU版本比你想的更挑版本人脸识别训练对算力有要求但毕设的数据量通常不大CPU也能跑只是慢。先说我一般怎么装用 conda 建独立环境不要往系统Python里塞东西。conda create -n face python3.10 -y conda activate face pip install tensorflow2.10 tensorflow-addons0.20 pip install mtcnn opencv-python scikit-learn matplotlib装完先验证GPU能不能用这一步没确认后面训练到一半才发现一直在用CPU心态会崩。import tensorflow as tf print(TF version:, tf.__version__) print(GPU list:, tf.config.list_physical_devices(GPU))输出里GPU列表为空说明TensorFlow没有拿到显卡。常见原因是TF版本和CUDA/cuDNN版本不匹配。这里有个坑TensorFlow从2.11开始不再提供Windows原生GPU支持所以Windows用户装2.10是最省事的方案。CUDA装完还要装配套的cuDNN手动下载容易版本对不上更省心的做法是直接用conda装conda install -c conda-forge cudnn8.6.0如果你只有CPU也能把毕设跑完只是训练时间会拉长。策略是把输入图缩小到112×112、embedding降到128维、batch size设16左右。数据量几千张的情况下MobileNetV2微调几个epochCPU上几小时也能出结果。2.2 数据集怎么选LFW、CASIA-WebFace还是自己拍人脸识别公开数据集不少但用途完全不同选错会影响整个毕设节奏。数据集规模适合做什么注意事项LFW13233张、5749人标准评估基准每人1~多张评测协议固定答辩必用CASIA-WebFace约50万张、1万人小规模训练年代较早有噪声需要清洗MS1M-ArcFace清洗后约500万张、8.5万人中等规模训练从MS-Celeb-1M清洗而来质量高但下载慢自制数据集数百张到数千张小场景识别、门禁模拟注意人物肖像授权自采需本人知情同意毕设的实用组合是用CASIA-WebFace或MS1M的子集训练用LFW做评估再用自制的几个人脸做演示。不要一上来就下几百万张图训练时间、硬盘空间都不划算。数据集的组织方式统一成一个目录每个子文件夹是一个身份命名随便但要一致。import os def collect_samples(data_dir): image_paths, labels [], [] for label, person in enumerate(sorted(os.listdir(data_dir))): person_dir os.path.join(data_dir, person) if not os.path.isdir(person_dir): continue for f in os.listdir(person_dir): if f.lower().endswith((.jpg, .jpeg, .png)): image_paths.append(os.path.join(person_dir, f)) labels.append(label) return image_paths, labels这段代码把磁盘上的目录结构映射成训练所需的路径列表和标签列表。目录里每个子文件夹对应一个身份标签类别数就是子文件夹个数。要注意如果某个身份只有一张图后面训练和评估都会出问题后面避坑章节会细说。如果要做实时采集可以用OpenCV调摄像头每20帧抽一帧存盘人的脸在画面里转动一下角度数据多样性会好很多。采集时每个身份至少收集20~30张否则训练出的embedding缺乏泛化能力。2.3 对齐是数据准备里最容易被忽略的一步很多人把图片resize到固定尺寸就直接丢进网络这是人脸识别效果差的头号原因。同样的脸歪5度和正脸提取出的特征差距很大网络被迫去学“姿态不变性”数据量又不够于是泛化失败。业界通行做法是先检测人脸再用关键点做仿射变换把眼睛鼻子对齐到固定坐标。from mtcnn import MTCNN import cv2 import numpy as np detector MTCNN() def align_face(img_path, output_size112): img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) result detector.detect_faces(img) if not result: return None keypoints result[0][keypoints] left_eye np.array(keypoints[left_eye], dtypenp.float32) right_eye np.array(keypoints[right_eye], dtypenp.float32) nose np.array(keypoints[nose], dtypenp.float32) dst_left np.array([0.32, 0.32], dtypenp.float32) * output_size dst_right np.array([0.68, 0.32], dtypenp.float32) * output_size dst_nose np.array([0.50, 0.50], dtypenp.float32) * output_size src np.array([left_eye, right_eye, nose], dtypenp.float32) dst np.array([dst_left, dst_right, dst_nose], dtypenp.float32) M, _ cv2.estimateAffinePartial2D(src, dst) aligned cv2.warpAffine(img, M, (output_size, output_size)) return aligned三点仿射变换把两只眼睛和鼻尖映射到固定位置顺便缩放旋转人脸。这里的关键参数是眼睛目标位置水平方向放在0.32和0.68也就是脸稍微居中靠上垂直方向放在0.32左右。这套坐标参考了常见人脸识别模型的预处理方式训练和推理必须用同一套否则模型看到的数据分布不一致识别率会明显下降。MTCNN检测不到人脸时代码返回None调用方要跳过这张图不要硬喂给网络。对齐这一步相当于给模型一个“标准化角度”后面训练省力很多。3. 从CNN到ArcFace人脸识别神经网络结构这条线怎么选3.1 人脸识别结构演进前馈计算和反向传播在解决什么问题人脸识别的核心任务可以概括成一句话把一张人脸图像映射成一个向量让同一个人的向量距离近不同人的向量距离远。早期做法是手工特征加分类器后来CNN出现特征提取变成了端到端学习。这里需要理解的关键点在于前馈神经网络计算的是“图像到特征向量”的映射反向传播调整的是卷积核权重让映射结果满足上述距离关系。结构演进上有几个标志性节点DeepFace用深度CNN配合Siamese网络FaceNet提出三元组损失直接优化特征向量的相对距离到了SphereFace、CosFace和ArcFace这一代大家发现与其在损失函数里做文章不如把“类间间隔”直接加到角度上让模型在训练时强制不同人的特征在超球面上分开。毕设答辩时能把“前馈算相似度、反向传播调margin”这条线讲清楚比背十个网络结构都有说服力。FaceNet的triplet loss有个众所周知的问题需要大量采样三元组选择“难样本”很费劲训练不稳定。这也是现在人脸识别主流转向softmax加margin损失的原因——不需要精心构造样本对训练流程和普通分类一样简单。3.2 一版可跑的MiniNet结构MobileNetV2骨干加128维Embedding网络结构选型上毕设不建议自己设计复杂CNN。常见做法是MobileNetV2或ResNet50做骨干去掉分类头接一个全局池化和一个全连接层输出128维的embedding向量。import tensorflow as tf def build_face_net(input_shape(160, 160, 3), embedding_dim128): base tf.keras.applications.MobileNetV2( input_shapeinput_shape, include_topFalse, weightsimagenet, poolingNone, ) base.trainable False # 先冻结骨干后面微调阶段再解冻 inputs tf.keras.Input(shapeinput_shape) x base(inputs, trainingFalse) x tf.keras.layers.GlobalAveragePooling2D()(x) x tf.keras.layers.Dropout(0.4)(x) embed tf.keras.layers.Dense(embedding_dim, use_biasFalse)(x) embed tf.keras.layers.Lambda(lambda t: tf.nn.l2_normalize(t, axis1))(embed) model tf.keras.Model(inputs, embed) return model model build_face_net() model.summary()这段代码里有几个参数值得解释。输入尺寸160×160这是FaceNet惯用配置比112×112多保留了细节又不至于让训练变慢embedding维度选128足够区分大规模身份且存储和比对开销小最后的L2归一化很关键它把所有特征向量压到单位球面上后续计算余弦相似度才有意义否则向量模长会干扰距离判断。MobileNetV2做骨干的理由是参数少、推理快、预训练权重容易拿。ResNet50精度上限更高但训练和推理都更慢。毕设场景下MobileNetV2足够。global average pooling把空间特征压缩成向量Dropout防止过拟合Dense层不带bias是为了让特征向量过原点配合L2归一化更干净。3.3 为什么毕设不建议从零训练骨干网络人脸识别的骨干网络动辄千万参数从随机初始化训练到收敛需要百万级人脸数据加几十个GPU小时这对毕设不现实。预训练模型已经在ImageNet或人脸大数据上学到了通用视觉特征我们要做的是“迁移”而不是“重学”。常见策略是两阶段第一阶段冻结骨干只训练后面的分类头让模型先适配当前数据集的类别分布第二阶段解冻骨干用很小的学习率微调使底层特征更贴合人脸任务。直接解冻骨干加高学习率几乎必然造成灾难性遗忘前期学到的分类头被破坏loss直接飞掉。一个额外的收益是预训练MobileNetV2本身很小用CPU推理单张图片只要几十毫秒后面部署到边缘设备也留得住。4. 训练一张能认人的脸ArcFace损失函数、完整训练脚本与三个必调参数4.1 ArcFace到底在做什么margin的几何含义ArcFace是当前人脸识别最主流的一类损失函数。它的想法很直接普通softmax分类只能让不同类之间“线性可分”但人脸识别要求同类特征在超球面上高度聚拢、异类特征明显分开于是直接在角度上增加一个惩罚项margin。class ArcFaceLayer(tf.keras.layers.Layer): def __init__(self, num_classes, margin0.5, scale64, **kwargs): super().__init__(**kwargs) self.num_classes num_classes self.margin margin self.scale scale def build(self, input_shape): self.w self.add_weight( shape(input_shape[-1], self.num_classes), initializertf.keras.initializers.HeNormal(), trainableTrue, namearcface_w, ) def call(self, inputs, labelsNone): x tf.nn.l2_normalize(inputs, axis1) w tf.nn.l2_normalize(self.w, axis0) cos_theta tf.matmul(x, w, transpose_bTrue) if labels is None: return cos_theta theta tf.acos(tf.clip_by_value(cos_theta, -1.0 1e-7, 1.0 - 1e-7)) one_hot tf.one_hot(labels, self.num_classes) cos_theta_m tf.cos(theta self.margin) output tf.where(one_hot 0, cos_theta_m, cos_theta) return self.scale * outputArcFace的实现细节都在这段代码里。输入特征和权重矩阵都做L2归一化两个归一化向量的内积就是余弦相似度。对正确类别对应的那个余弦值先反解出角度加上margin再重新取余弦。这个操作让正确类别的logits变小迫使模型把该类别的特征往权重方向推得更近。scale64把余弦值放大到合理范围不然几十上百类的logits太小softmax梯度会消失。这里的clip_by_value不是可有可无的保险是必写的。浮点计算里余弦值偶尔会越过[-1, 1]边界acos遇到越界输入返回NaN一旦出现一个NaN整个loss都会变成NaN训练直接报废。4.2 一份可复制的训练脚本从tf.data到训练主循环数据读取用tf.data构建pipeline增强操作直接在图上做省去大量IO。def load_and_augment(path, label): img tf.io.read_file(path) img tf.image.decode_jpeg(img, channels3) img tf.image.resize(img, [160, 160]) img tf.image.random_flip_left_right(img) img tf.image.random_brightness(img, max_delta0.2) img tf.image.random_contrast(img, lower0.8, upper1.2) img tf.cast(img, tf.float32) / 127.5 - 1.0 return img, label def build_pipeline(data_dir, batch_size64): image_paths, labels collect_samples(data_dir) ds tf.data.Dataset.from_tensor_slices((image_paths, labels)) ds ds.shuffle(10000).map(load_and_augment, num_parallel_callstf.data.AUTOTUNE) ds ds.batch(batch_size).prefetch(tf.data.AUTOTUNE) return ds这个pipeline用了随机水平翻转、随机亮度和对比度扰动。对于人脸识别水平翻转和亮度扰动通常是性价比最高的增强手段几何形变和旋转增强容易破坏人脸结构效果反而不好。归一化采用/127.5 - 1.0把像素范围映射到[-1, 1]这要和骨干网络的预训练输入范围一致如果MobileNetV2的预处理是[-1, 1]就用这个不要混用。训练主体用自定义Trainer类把ArcFace层和骨干封装到一起。class Trainer(tf.keras.Model): def __init__(self, backbone, num_classes, margin0.5, scale64): super().__init__() self.backbone backbone self.arcface ArcFaceLayer(num_classes, margin, scale) def call(self, x, labelsNone): emb self.backbone(x, trainingTrue) if labels is None: return emb return self.arcface(emb, labels) def train_step(self, data): x, y data with tf.GradientTape() as tape: logits self(x, labelsy) loss tf.reduce_mean( tf.nn.sparse_softmax_cross_entropy_with_logits(labelsy, logitslogits) ) grads tape.gradient(loss, self.trainable_variables) self.optimizer.apply_gradients(zip(grads, self.trainable_variables)) return {loss: loss}训练时先冻结骨干跑十几个epoch再解冻微调。train_ds build_pipeline(data/train, batch_size64) num_classes len(os.listdir(data/train)) backbone build_face_net() model Trainer(backbone, num_classes) model.compile(optimizertf.keras.optimizers.AdamW(learning_rate1e-3, weight_decay5e-4)) model.fit(train_ds, epochs15) backbone.trainable True model.compile(optimizertf.keras.optimizers.AdamW(learning_rate1e-4, weight_decay5e-4)) model.fit(train_ds, epochs20)这里有两个细节。第一第一阶段冻结骨干时trainable_variables只包含分类层的参数梯度更新范围小学习率用1e-3没有问题第二阶段解冻后trainable_variables变成全部参数学习率必须降到1e-4否则骨干预训练权重会被破坏。第二AdamW的weight_decay对正则化有效果比L2惩罚更干净人脸识别这种高维特征任务建议加上weight_decay。4.3 batch size、学习率、embedding维度这三个参数怎么定这三个参数是训练阶段最值得调的核心旋钮它们的推荐值如下。参数推荐值说明batch size64CPU用16~32太小loss波动大太大单步更新慢embedding维度128够用且检索开销小256不会显著提升效果ArcFace margin0.5标准值margin太大会拖慢收敛甚至不收敛scale64固定值一般不调学习率1e-3/1e-4两阶段冻结骨干和全量微调分开设置输入尺寸160×160更大的图只增加开销不明显的精度增益关于margin和batch size的联动有一个经验值得说数据量小的数据集margin可以适当调小到0.3~0.4因为margin强制拉开特征角度数据不够时模型学不到这么强约束。数据量大再调回0.5。batch size太小会让margin的梯度估计噪声大如果batch size只能设16建议把margin降一档学习率也降到5e-4左右换稳定换精度。训练过程中用TensorBoard观察loss和梯度范数比肉眼看终端输出可靠得多。loss下降变缓时不要急着加epoch先确认是学习率太低还是数据问题。5. 人脸识别训练中5个高发坑现象、原因与解决方案5.1 loss卡在某个值不动或者直接变NaN现象训练刚开始loss在一个数值附近震荡就是不下降或者跑了几个batch突然变成NaN之后一直是NaN。原因loss高概率是学习率过大优化器一步跨过最优区域梯度爆炸。loss变NaN的最大嫌疑则是ArcFace里的acos函数收到了越界输入cos_theta略大于1或略小于-1时acos返回NaN。样本特征极端相似时这种情况很容易发生。解决先检查ArcFaceLayer里有没有tf.clip_by_value(cos_theta, -1.0 1e-7, 1.0 - 1e-7)这行保护。然后学习率按数量级下调从1e-3降到1e-4。如果已经变NaN直接重新开始训练不要指望能恢复。5.2 TensorFlow装好了但GPU列表是空的现象tf.config.list_physical_devices(GPU)返回空列表训练时终端没有任何GPU日志跑一个epoch要以前几倍时间。原因Windows用户最常见的原因是装了TF 2.11以上的版本原生GPU支持被移除了也可能是CUDA和cuDNN版本和TF不配套。TF 2.10对应CUDA 11.2和cuDNN 8.6版本对不上就白装。解决先卸载重装TF 2.10再用conda装cuDNN 8.6.0重开conda环境确认加载成功。如果还不行检查NVIDIA驱动版本驱动太老也会失败。这个坑极其耗耐心建议从一开始就按版本号锁定环境不要装最新的TF。5.3 每个类别图片数量差很多模型只认得图片多的那几个人现象loss看起来正常但查看分类结果发现图片多的身份准确率高只有一两张图的身份几乎全错。原因人脸数据天然长尾不做处理的话模型会把重心放在高频类别上。ArcFace对所有类平等加margin低频类样本太少学不出稳定特征。解决最简单的方案是限制训练类别把少于5张图的身份剔除或合并为“陌生人”类。更稳妥的常见做法是按类别采样每个batch里每个身份出现的次数尽量均衡而不是按原始分布随机抽样。毕设规模下剔除低频类别是性价比最高的方案。5.4 训练集准确率很高一到LFW测试准确率只有90%左右现象训练loss降到很低训练集识别几乎100%但LFW评估只有90%上下离95%还有差距。原因数据增强不够导致过拟合骨干网络没解冻只在分类头层面适配底层特征没有针对人脸做微调或者训练集身份数太少模型没见过足够多的人脸变化。解决先检查训练状态是不是只冻了骨干——第一阶段结束一定要解冻微调这一步通常能带来2~3个百分点的提升。然后把增强策略补上至少保持随机翻转和亮度扰动。最后确认训练集大小少于500人时别指望LFW能出太高的分数数据规模决定效果上限。5.5 评测效果不错打开摄像头实时识别就翻车现象LFW上测到96%以上但用摄像头对着人测频繁误识别或识别不出。原因LFW里所有图都是对齐好的推理链路没有对齐直接把检测框resize后丢给模型数据分布完全乱了。另外光照、模糊、侧脸角度这些干扰在真实场景里很常见评测集里遇不到。解决推理链路必须和训练链路一致MTCNN检测人脸、五点对齐、仿射变换到160×160再做同样的归一化。如果还不行把推理阈值调高比如余弦相似度从0.5调到0.6减少误识别。最终方案是收集一批摄像头拍摄的样本加入训练集这是毕设演示阶段最有效的补丁。6. 拿去答辩与部署LFW评估和门禁机边缘设备优化6.1 LFW评估用6000对pair算准确率LFW有一套标准评测协议构造3000对同人图像和3000对异人图像让模型判断每对图像是否属于同一人算总体准确率。严格协议要做十折交叉验证毕设中可以简化成整体阈值扫描但要说明评估方式。def compute_similarity(model, img1_path, img2_path): emb1 model.predict(align_face(img1_path)[None], verbose0)[0] emb2 model.predict(align_face(img2_path)[None], verbose0)[0] sim float(np.dot(emb1, emb2) / (np.linalg.norm(emb1) * np.linalg.norm(emb2))) return sim逐个对比pair的相似度后遍历阈值0.3到0.7找一个最优切分点记录准确率。评估时人脸必须走训练时的对齐流程否则分数偏低而且在论文里写明“未使用十折交叉验证简化版协议”即可不要含糊成“按标准协议评估”。6.2 部署思路把模型压小再放到门禁机这类设备上门禁机这类边缘设备算力有限完整训练模型不能直接跑。常见做法是把embedding网络导出为TensorFlow Lite做动态范围量化或INT8量化。converter tf.lite.TFLiteConverter.from_keras_model(backbone) converter.optimizations [tf.lite.Optimize.DEFAULT] tflite_model converter.convert() open(face_embedding.tflite, wb).write(tflite_model)INT8量化需要代表性数据集校准否则精度损失明显。在K210这类MCU级平台上部署时一条完整链路是摄像头采集人脸图检测加对齐送入量化后的embedding模型输出一个128维向量和本地人脸库比对最大相似度超过阈值则放行。阈值可以在测试集上扫出来部署后留一个配置入口方便现场调整。我自己做这类项目有个习惯每跑通一个阶段就把训练命令、环境版本号、当前效果记录在一个文本文件里答辩前一周集中调参是最容易出乱子的时候有记录不慌。希望帮到你。本文还有配套的精品资源点击获取
返回列表