
简介面向人工智能与计算机视觉方向毕业设计的学生和TensorFlow初学者这份基于TensorFlow的人脸识别神经网络完整毕业设计教程提供了一套从数据构建、模型训练到识别判断的可运行工程方案。包内共六个文件包含四个Python脚本分别用于采集自己的面部样本、获取他人参照照片、训练分类神经网络以及校验输入图像是否为目标人脸另有README说明文档和MIT开源许可证整个压缩包仅14KB下载和调试非常轻便。目前已有六百二十五人浏览学习。借助该资源读者能完整走通基于卷积神经网络的“数据准备—特征学习—身份判别”全流程掌握TensorFlow核心API的用法并能在现有代码基础上扩展多人识别功能为毕业设计或课程项目提供可直接演示的实用基础。1. 人脸识别毕业设计最常见的三个翻车点很多同学拿到“基于TensorFlow训练的人脸识别神经网络”这个题目后第一反应是去 GitHub 上找现成的 FaceNet 权重然后跑一次model.predict()把输出截图贴进论文最后在答辩现场被老师问一句“这个模型的损失函数是你自己实现的吗”就卡住了。另一种常见情况是直接用ResNet softmax去分 100 个班级的人脸身份训练时 Accuracy 确实能到 97%但换一个没参与训练的人就会认错。这两种做法本质上都绕开了人脸识别的核心问题它不是一个固定类别分类任务而是一个度量学习任务。这篇教程会从为什么算距离、选什么损失函数、怎么搭数据管道、怎么调优化器、怎么把模型变成一个能用的门禁演示程序讲起全程基于 TensorFlow 2 的 Keras API代码可以直接抄到自己的毕业设计里。如果你只是想把论文凑完这个路线也能帮你避开那些“看着会了、一跑就崩”的坑。2. 人脸识别神经网络必须掌握的度量学习原理与数据集构建2.1 为什么人脸识别不能当成普通分类任务常规的图片分类网络最后一层是Dense(num_classes, activationsoftmax)输出一个概率分布训练时用categorical_crossentropy去拟合标签。这种结构在类别数量固定且每个类别样本充足时表现不错但人脸识别系统的真实使用场景是“注册一张照片以后见到这个人就能认出来”。这意味着模型见过的人不能永远存在于输出层里新的人在推理时刻才会出现。所以我一般会把人脸识别拆成两部分一个卷积神经网络负责把一张人脸图片映射成一个固定长度的向量例如 128 维或 256 维的 embedding另一个比价模块负责计算两张人脸 embedding 之间的距离。距离小的就是同一个人距离大的就是不同人。这个思路就是度量学习而损失函数的作用就是让同类样本在向量空间里互相靠近异类样本被推开。在这个框架下训练集的类别数多少反而没那么重要重要的是网络如何学会对“身份差异”而不是“表情、光照、姿态差异”产生响应。数据集的正确组织方式比模型结构对最终效果的影响更大这条经验几乎适用于所有毕业设计级别的人脸识别项目。2.2 主流损失函数与网络结构选择Triplet Loss 到 ArcFace常见的人脸识别损失函数可以分成两类。第一类是直接对角度边界做约束的比如ArcFace它先把特征向量归一化再在角度空间里增加类间 margin然后在训练阶段等价于分类任务因此需要保留一个可增长的类别数第二类是样本对之间的度量约束典型的就是Triplet Loss它不需要输出层跟着类别数量变化适合数据集不大、身份增长频繁的毕业设计项目。选择上我给一个务实的建议如果教师要求你从零训练一个神经网络不要一开始就上ArcFace它对本机 TensorFlow 版本、数据质量、训练时长要求都比较高。先用Triplet Loss在小型数据集上把训练流程跑通等模型能正确区分几个陌生人之后再在论文中补充说明ArcFace的改进思路和数学推导。网络结构方面没必要自己写一个 50 层的 ResNet。TensorFlow 内置的MobileNetV2、EfficientNetB0在预训练权重上很容易迁移训练速度快单张 1080Ti 就能跑起来。我会把预训练模型去掉顶层输出一个 128 维的 embedding再用L2 normalization把它归一化到单位球面上最后 cosine distance 和欧式距离在这种空间中表现等价。2.3 数据集预处理与三元组生成的最小实现先要整理一个自己的数据集。推荐去下载公开的人脸数据集比如 LFW、CelebFaces AttributesCelebA或者 VGGFace2 的子集。下载后注意看图片有没有经过人脸对齐。很多人图省事直接用原图训练结果同一个人不同角度看起来距离比不同人还近。常见做法是用 MTCNN 检测人脸关键点然后根据眼睛、鼻子、嘴巴坐标做相似变换把每张脸裁剪到 160×160 或 224×224。TensorFlow 生态里有现成的mediapipe工具可以做这一步但介意在线调用的话也可以用face_recognition库自带的检测器。三元组格式是(anchor, positive, negative)其中 anchor 和 positive 属于同一个人negative 属于另一个人。最笨的生成方式是遍历所有合法组合但样本量一大组合数量就爆炸。我一般会给每个 identity 维护一个图片路径列表然后在每个 epoch 开始前在线生成新的三元组。下面这段代码能让你在内存里维护一个足够丰富的索引结构import random from collections import defaultdict class TripletIndex: def __init__(self, image_paths, labels): self.person_imgs defaultdict(list) for p, lbl in zip(image_paths, labels): self.person_imgs[lbl].append(p) self.person_ids list(self.person_imgs.keys()) def sample_triplet(self): anchor_label random.choice(self.person_ids) positive_list self.person_imgs[anchor_label] while len(positive_list) 2: anchor_label random.choice(self.person_ids) positive_list self.person_imgs[anchor_label] anchor, positive random.sample(positive_list, 2) negative_label random.choice(self.person_ids) while negative_label anchor_label: negative_label random.choice(self.person_ids) negative random.choice(self.person_imgs[negative_label]) return anchor, positive, negative这段代码的逻辑是每个身份至少要有 2 张图才能同时抽出 anchor 和 positivenegative 只需要 1 张。通过random.sample避免 anchor 和 positive 是同一张图这个细节影响到 Triplet Loss 什么有效信息都学不到。实际训练时这个TripletIndex会被放进tf.data.Dataset.from_generator让整个数据管道按需生成而不是一次性把几十万张图片都载入内存。梯度更新的稳定性需要靠困难样本挖掘来保证。最简单的做法是保持hard mining ratio在 0.4 左右也就是每个 batch 中有 40% 的 triplet 是刻意挑选出来的较难负样本其余随机生成。这样可以避免模型被大量“简单三元组”淹没陷入难样本不稳定的状况。3. 用 TensorFlow 搭建人脸特征提取模型与 Triplet Loss3.1 一个可直接运行的模型定义MNIST 人脸版现在开始写模型。核心是一个 base encoder输入(160, 160, 3)的图像输出 128 维向量。我不用冻结的预训练骨干因为毕业设计线上跑的场景更看重是否真是自己参与训练的效果所以这里直接让完整模型参与训练import tensorflow as tf from tensorflow.keras import layers def build_embedding_network(input_shape(160, 160, 3), embedding_dim128): base_model tf.keras.applications.MobileNetV2( input_shapeinput_shape, include_topFalse, weightsimagenet, poolingavg ) inputs tf.keras.Input(shapeinput_shape) x base_model(inputs, trainingFalse) x layers.Dropout(0.3)(x) x layers.Dense(embedding_dim, nameembedding)(x) x layers.Lambda(lambda t: tf.math.l2_normalize(t, axis1))(x) model tf.keras.Model(inputs, x) return model这里trainingFalse是预训练骨干在初期被冻结但为了避免只在最后加 Dense 层导致特征不更新我实际上会在自定义训练循环里对base_model的部分层做微调。Keras 默认模型构造方式对外层训练和内层冻结的管理不太透明所以更推荐用自定义train_step来控制每一层。l2_normalize是必须的一步。Triplet Loss 的双距离度量在归一化之后可以避免模型“靠拉长向量长度”作弊同时让损失值有稳定的上限。论文中一般称这一步为 embedding 归一化。3.2 Triplet Loss 与batch_hard挖掘的代码讲解模型建好后损失函数是重头戏。最基础的 Triplet Loss 公式是max(d(a,p) - d(a,n) margin, 0)如果anchor和positive距离加上 margin 已经小于hard样本距离loss 就是 0模型不再更新。对于毕业设计“Margin 参数设多少”是第一问。一般推荐 margin 在 0.2 到 0.5 之间。你可以把 margin 理解为“推开的力度”太大可能让模型训练不稳太小则正负样本区分得不够开。下面是不做在线挖掘的原始版方便阅读与配合model.fit使用class TripletLoss(tf.keras.losses.Loss): def __init__(self, margin0.3, **kwargs): super().__init__(**kwargs) self.margin margin def call(self, y_true, y_pred): anchor, positive, negative tf.split(y_pred, 3, axis0) pos_dist tf.reduce_sum(tf.square(anchor - positive), axis1) neg_dist tf.reduce_sum(tf.square(anchor - negative), axis1) loss tf.maximum(pos_dist - neg_dist self.margin, 0.0) return tf.reduce_mean(loss)但上面的方法很天真同一个tf.data.Dataset的 batch 顺序问题很容易导致y_pred的前后代表对象不一致。实战中我更建议在batch内部直接做 online hard mining。做法是假设一个 batch 由B张图组成其中每张图都有类别标签在计算 loss 前先动态找出每个 anchor 的最远正样本和最近负样本。def batch_hard_triplet_loss(labels, embeddings, margin0.3): emb_size tf.shape(embeddings)[1] dist_matrix tf.linalg.matmul( embeddings, embeddings, transpose_bTrue ) # 转换为距离矩阵 d^2 ||a||^2 ||b||^2 - 2ab但由于已经归一化2-2ab 可替代 dist_matrix 2.0 * (1.0 - dist_matrix) mask_positive tf.equal(labels[:, None], labels[None, :]) mask_negative tf.logical_not(mask_positive) # 自己和自己的距离清零 idx tf.range(tf.shape(labels)[0]) mask_same tf.equal(idx[:, None], idx[None, :]) mask_same tf.cast(mask_same, tf.float32) dist_matrix dist_matrix 1e6 * mask_same pos_dist tf.reduce_max( tf.where(mask_positive, dist_matrix, tf.zeros_like(dist_matrix)), axis1) neg_dist tf.reduce_min( tf.where(mask_negative, dist_matrix, tf.ones_like(dist_matrix) * 1e6), axis1) loss tf.maximum(pos_dist - neg_dist margin, 0.0) return tf.reduce_mean(loss)这里的核心逻辑是先使用归一化 embedding 的余弦相似度换算成距离矩阵然后对每个样本选取同身份中“最不像自己”的一张作 positive选不同身份中“最像自己”的一张作 negative。最终计算max-margin hinge loss。这样做的好处是不用预先构造三元组只要一个 batch 里各身份数量分布合理就自动完成了困难样本挖掘。配合前面二章的三元组生成二者可以互为备选答辩区分度高。3.3tf.data数据管道与图像增强配置数据输入方面建议完成归一化到[-1, 1]、随机裁剪、水平翻转与亮度调整并保证每个 batch 中的身份数量足够扩散。下面的代码把图片路径变成tf.data.Dataset并让每个 batch 含有n_classes个身份每个身份固定n_imgs_per_class张图。def load_data_from_paths(paths, labels, img_size(160, 160), batch_size64, n_classes8, imgs_per_class8): class_to_imgs {} for p, lbl in zip(paths, labels): class_to_imgs.setdefault(lbl, []).append(p) dataset_paths [] for lbl, imgs in class_to_imgs.items(): for path in imgs: dataset_paths.append((path, lbl)) # 通过 tf.data.interleave 保持类别顺序 ds tf.data.Dataset.from_tensor_slices(dataset_paths) def preprocess(path, lbl): img tf.io.read_file(path) img tf.image.decode_jpeg(img, channels3) img tf.image.resize(img, img_size) img tf.image.random_flip_left_right(img) img tf.image.random_brightness(img, max_delta0.2) img (tf.cast(img, tf.float32) / 127.5) - 1.0 return img, lbl ds ds.map(preprocess, num_parallel_callstf.data.AUTOTUNE) ds ds.batch(batch_size, drop_remainderTrue) ds ds.shuffle(buffer_size1000, reshuffle_each_iterationTrue) return ds这里有一个关键参数说明drop_remainderTrue会在 Epoch 期间把最后的不足一个批次的数据整段丢弃避免模型卡在深度学习中比较烦人的Incompatible shapes错误。如果你的样本数量很少建议把drop_remainder设为False但要在batch_hard_triplet_loss中考虑标签矩阵形状不一致的问题。数据增强集中在random_flip_left_right和random_brightness两个函数上对人脸任务来说这是最稳妥的两种增强不要增加太多裁剪否则会破坏人脸对齐结果。提示训练环境里 TensorFlow 2 的tf.data.AUTOTUNE能并行处理预处理不要把.map(preprocess)放到.batch之后否则每张图片都会在 GPU 侧被二次处理拖慢训练。4. 训练人脸识别模型时的优化器、学习率与正则化参数调优4.1 优化器选择SGD with Momentum 比 Adam 更像毕业设计在毕业设计论文明文里最安全的优化器是 SGD with Momentum而不是 Adam。Adam 收敛速度快但最后阶段的泛化边界通常没有 SGD 好而且一个杂乱的训练日志容易被认为是实验不够严谨。但 Adam 的好处是几乎不需要调学习率在时间紧张的毕设季我更愿意用它跑通全流程。所以常见做法是初版使用 Adam等论文要写最优化分析时再切换到 SGD with Momentum并说明它经过学习率扫描后的表现。例如可以用tf.keras.optimizers.SGD(learning_rate0.01, momentum0.9)配合一个CosineDecay学习率调度器initial_lr 0.01 lr_schedule tf.keras.optimizers.schedules.CosineDecay( initial_learning_rateinitial_lr, decay_stepstotal_steps, alpha0.0001 ) optimizer tf.keras.optimizers.SGD(learning_ratelr_schedule, momentum0.9)CosineDecay会让学习率从 0.01 平滑下降到 0.0001而不是阶梯下降。对人脸识别这种精细嵌入任务余弦衰减的优点是让模型在后期以较小幅度修正 embedding 方向不至于因为固定学习率过大而跳过最优区域。alpha0.0001表示最低学习率是初始学习率的万分之一这个参数不要忽略。下面是一份参数速查表。表格内数值对应公开小型数据集例如 LFW 裁剪集或自建 2000 类人脸数据。如果数据尺寸更大学习率建议按 batch size 的倍数做线性缩放batch size 从 64 变成 128学习率就乘 2。参数建议范围针对毕设的实际选择调整指标margin0.2 ~ 0.50.3正负样本距离的 overlapembedding_dim64 ~ 256128比对的准确率与显存需求batch size32 ~ 12864GPU 显存使用率learning rate0.0001 ~ 0.010.003Adam/ 0.01SGDloss 的下降斜率dense dropout0.2 ~ 0.50.3验证集准确率weight decay1e-5 ~ 1e-45e-5训练 loss 与验证 loss 的 gap4.2 正则化与过拟合抑制组合使用预训练 MobileNetV2 时默认已经继承了 BatchNorm 层批归一化本身就是一种正则化。但要注意预训练模型如果有 BN 层在微调时不能把trainingFalse一直设成 True否则会导致均值和方差在 BatchNorm 层不断更新产生很怪异的训练崩溃。稳妥做法是骨干前半段冻结、后半段参与训练并且自定义train_step里只对可训练变量使用GradientTape。optimizer tf.keras.optimizers.Adam(learning_rate1e-4) def train_step(images, labels): with tf.GradientTape() as tape: embeddings model(images, trainingTrue) loss batch_hard_triplet_loss(labels, embeddings) grads tape.gradient(loss, model.trainable_variables) optimizer.apply_gradients(zip(grads, model.trainable_variables)) return loss此代码块的逻辑是先预处理图像与标签通过网络输出 embedding计算 batch hard triplet loss接一个GradientTape利用自动微分得到梯度随后apply_gradients更新权值。我没有在这个循环里手动加 weight decay如果强调轻量化可以在优化器对象上加tf.keras.optimizers.experimental.SGDW或者给kernel_regularizer设置一个很小的 L2。但要注意Keras 的kernel_regularizer加的惩罚默认会被计入模型的losses而不是grads所以如果使用自定义train_step必须额外读取loss sum(model.losses)。另外一个很重要但常被忽略的点不要用model.fit配合验证集区分“人”而是要单独维护验证集用来检查模型能不能从没参与训练的人脸中判断同一个人。否则你正则化调参做的再好也只是对训练集过拟合。4.3 模型检查点与 TensorBoard 监控训练要能随时停掉才是好训练。我应该建议你在每个 epoch 结束后保存全部权重与优化器状态这样如果任务训练到一半机器断电也能够从中途快照恢复。tf.train.CheckpointManager是首选ckpt tf.train.Checkpoint(steptf.Variable(0), modelmodel, optimizeroptimizer) manager tf.train.CheckpointManager(ckpt, ./checkpoints, max_to_keep3) for epoch in range(epochs): for images, labels in dataset: loss train_step(images, labels) if epoch % 2 0: manager.save()manager.save()创建的是带优化器状态的完整快照文件大小一般在几十 MB 到一两百 MB毕业设计提交时可以把最终一份提取出来放论文附录。这里max_to_keep3指磁盘上只保留最近 3 份快照避免反复训练把笔记本硬盘塞满。TensorBoard 方面使用tf.summary.create_file_writer记录标量就可以看到 loss 曲线和每个 batch 的净资产平均距离summary_writer tf.summary.create_file_writer(./logs) with summary_writer.as_default(): tf.summary.scalar(triplet_loss, float(loss), stepepoch)在日志目录执行tensorboard --logdir ./logs就能在浏览器中观察训练曲线。如果损失曲线是个锯齿状且平均值大幅上升说明学习率太大或者 margin 太大。5. 人脸Embedding相似度计算、评估阈值与门禁演示程序5.1 计算相似度与识别流程训练完成后把所有注册照片送入模型得到一组基准 embedding 矩阵。待识别的人脸也经过同样的preprocess步骤送入模型得到一个 query embedding。剩下的事情就是计算 query 与所有基准 embedding 之间的距离并找出最小值。我通常使用余弦相似度而非欧式距离因为 embedding 已经被l2_normalize过两者数学上等价但余弦相似度在展示时更直观并且能在不改变阈值取值习惯的范围内给出 0.5 这样的参考值。下面的代码实现了纯 NumPy 的定义便于脱机运行import numpy as np def cosine_similarity(query_emb, enrolled_embs): query_emb query_emb / np.linalg.norm(query_emb, axis-1, keepdimsTrue) enrolled_embs enrolled_embs / np.linalg.norm(enrolled_embs, axis-1, keepdimsTrue) return np.dot(enrolled_embs, query_emb.T) # 使用示例输入 shape 为 [128]返回每个人对应的分数 scores cosine_similarity(query_embedding, enrolled_embeddings) best_idx int(np.argmax(scores))逻辑与参数说明先把每个向量归一化然后做内积得到余弦值数值越大越相似所以判断条件应为if scores[best_idx] threshold。代码里best_idx对应最初注册时的倒序编号你可以通过一个name_lookup[best_idx]拿回姓名。如果你更在意误差分布建议画一个阈值与假接受率、拒识率的关系曲线。最简单的方式是从验证集中选 1000 个正样本对和 1000 个负样本对计算各自相似度并记录分布再用sklearn.metrics.roc_curve得到 EER。这样答辩时你可以清楚说出“取某个阈值时 FAR 是 1%”而不是含糊地说“能识别出来”。5.2 本地摄像头门禁演示程序模型终究要落到应用场景。TensorFlow 搭配 OpenCV 可以做一个最简单的门禁机每次从摄像头捕捉一帧先用 MTCNN 或 OpenCV 自带的 Haar 特征检测人脸区域再裁剪放大后送入模型。为了简化依赖下面用 OpenCV 的 DNN 人脸检测器当示例import cv2 import numpy as np cap cv2.VideoCapture(0) face_net cv2.dnn.readNetFromTensorFlow(opencv_face_detector_uint8.pb, opencv_face_detector.pbtxt) while True: ret, frame cap.read() h, w frame.shape[:2] blob cv2.dnn.blobFromImage(frame, 1.0, (300, 300), (104.0, 177.0, 123.0)) face_net.setInput(blob) detections face_net.forward() # 只处理置信度最高的检测框 best_confidence 0 best_box None for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence best_confidence: best_confidence confidence x1, y1, x2, y2 (detections[0, 0, i, 3:7] * [w, h, w, h]).astype(int) best_box (x1, y1, x2, y2) if best_box: x1, y1, x2, y2 best_box face_img frame[y1:y2, x1:x2] face_img cv2.resize(face_img, (160, 160)) face_img (face_img.astype(np.float32) / 127.5) - 1.0 emb model.predict(face_img[None, ...])[0] scores cosine_similarity(emb, enrolled_embeddings) if np.max(scores) 0.7: cv2.putText(frame, fPass: {best_idx}, (40, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) else: cv2.putText(frame, No Match, (40, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) cv2.imshow(FaceGate, frame) if cv2.waitKey(1) 0xFF ord(q): break这段代码里face_net.forward()的输出是一个 1×1×N×7 的数组其中每行内容依次是 batch id、class id、confidence、4 个坐标必须通过 detections 的第三个维度去遍历。把阈值0.7放进去之后门禁实验才算真正闭环。参数说明blobFromImage的缩放因子和均值是根据原模型训练时定义的替换成自训练模型时不需要改但最好还是用你实际模型预处理时一致的 mean/std。如果在演示现场摄像头中没有检测到人脸程序只会持续等待不会崩溃。5.3 评估脚本的独立性为保证毕业设计项目可复现我会把评估单独写成eval_threshold.py它跟训练代码解耦。这份脚本从checkpoints加载模型然后读取一个val_pairs.txt文件每行是两个图片路径和一个1/0标签输出最优阈值和 ROC 曲线。答辩前把数据和脚本一起放进压缩包提交后老师就能看到完整的评测链路。6. 毕业设计排错与进阶技巧Loss 不降、显存不足与模型瘦身6.1 Loss 一直是 0 或直接变成 NaN 的排查步骤毕设训练中第一个常见毛病是 loss 一直是 0。原因通常是 margin 值过大或者所有的负样本都离 anchor 很远使得pos_dist - neg_dist margin恒小于 0。解决方案是把 margin 从 0.3 降到 0.1同时打印每一批的平均pos_dist和neg_distpos_mean tf.reduce_mean(pos_dist) neg_mean tf.reduce_mean(neg_dist) tf.print(pos/neg distance:, pos_mean, neg_mean)如果pos_mean本身已经大于 0.5说明图片区分性不好特征还没学起来应该先检查数据预处理是否符合模型输入要求尤其是是否做了归一化。另一个经常出现 loss 为 NaN 的原因是图像里存在 0 像素的纯黑图导致训练时标准差除数为 0。排查方法是统计图片保存时的通道均值和方差剔除异常图片。不要简单地把异常图片替换成一张灰图因为这会改变分布的语义。6.2 显存不足时优先使用梯度累积而不是缩小 Batch Size减少 Batch Size 能解决显存但会直接降低batch_hard_triplet_loss的挖掘质量。原因很简单batch 越小同类样本可做 hard pair 的机率越小负样本选择范围也越小。如果显卡只有 6GB我建议将 Batch Size 维持在 64用梯度累积模拟更大的 Batch Size。梯度累积的本质是先计算多次反向传播的梯度再一次性更新参数accum_steps 4 accumulation [tf.zeros_like(v) for v in model.trainable_variables] for step, (images, labels) in enumerate(dataset): with tf.GradientTape() as tape: emb model(images, trainingTrue) loss batch_hard_triplet_loss(labels, emb) grads tape.gradient(loss, model.trainable_variables) for i in range(len(grads)): accumulation[i] grads[i] if (step 1) % accum_steps 0: optimizer.apply_gradients(zip(accumulation, model.trainable_variables)) accumulation [tf.zeros_like(v) for v in model.trainable_variables]这段代码中的accum_steps4表示每 4 个小批量才做一次更新等效 batch size 放大为 64×4256。需要注意损失中的 BN 层仍然按单次前向的 batch 统计更新因此梯度累积不会完全等于大 batch但对 Triplet Loss 的挖掘阶段来说已经足够有效。6.3 模型瘦身与答辩演示时的验证技巧训练完成后保存的模型往往包含骨干网络和全连接层部署到 CPU 笔记本上会有几百毫秒延迟。一种比较有效的瘦身方法是把 MobileNetV2 的深度乘数调小到 0.35。MobileNetV2(alpha0.35)可以显著减少参数量但精度下降约 5%。如果毕设题目只要求演示识别效果我建议直接保留 alpha1.0在答辩时关闭笔记本的摄像头自动亮度调整保证输入图片的亮度和训练集接近。在答辩之前最好准备一个不依赖摄像头的离线比对 demo先把注册图片放在register/目录把待验证图片放在test/目录通过 TensorFlow Serving 或直接调用model.predict输出相似度。要特别留意 TensorFlow 在 GPU 和 CPU 上运行的结果一致性通常 GPU 模式下浮点数累积顺序不同会造成 ±0.001 级别的差异这不会影响门禁阈值结果但 PPT 上不要打出过于精确的小数位。最后在论文里记得附上训练曲线、模型结构和一版requirements.txt其中明确指出tensorflow2.10.0、opencv-python、numpy的安装版本。答辩现场提前五分钟跑一次摄像头程序数据集里的图片不需要加载很多每类准备三张人脸就够了。用静态图片比对做兜底再结合一段简短视频演示比在现场反复调摄像头更稳妥。本文还有配套的精品资源点击获取