简介:基于卷积神经网络(CNN)的人脸在线识别系统毕业设计资源包,面向计算机、人工智能等专业需要完成毕业设计、期末大作业或课程设计的学生,尤其适合需要完整可运行项目参考的初学者。代码已获导师指导并通过,属于高分项目,可直接用于项目展示与二次开发。压缩包共10个文件,大小约195.48MB,核心是4个Python脚本(模型训练、人脸识别、辅助功能等),并配套TensorFlow模型文件(.meta、.index、.data-00000-of-00001、checkpoint)、lfw.tgz人脸数据集以及训练日志,结构清晰,便于对照学习。目前已有421人学习。下载后可使用lfw标准数据集,通过faces_train.py、face_recognition.py等脚本完成从训练到在线识别的完整流程;模型检查点支持断点续训或直接加载,配合文件说明可快速复现实验,作为毕业设计演示或人工智能课程项目都很有价值。
1. 在本地用摄像头实时识别人脸,难点从来不在算法理论,而在训练和推理能不能连贯
这套基于 TensorFlow 的卷积神经网络人脸识别系统,把 LFW 数据集、CNN 模型训练、checkpoint 保存和人脸在线识别串在了一个压缩包里。它不是一个只有前端演示的半成品,而是包含数据加载、模型构建、训练保存、推理比对完整链路的代码。对于课程设计、期末大作业以及想拿真实数据集跑通人脸识别流程的同学,这份项目能直接作为骨架,省去从零搭环境的痛苦。读完你会理解为什么 LFW 数据集适合做人脸识别实验,训练代码里每一步在做什么,以及在线识别和离线分类到底差在哪。
2. 先从数据集和文件结构说起:LFW 与这套代码的关系
2.1 解压 LFW 数据集,搞清目录结构
LFW,全称 Labeled Faces in the Wild,是真实场景下的人脸数据集,包含超过 1.3 万张网络图片,覆盖 1680 个人。每张图片大小固定为 250x250 像素,目录结构是“每个人名一个子文件夹”,子文件夹里放该人的多张照片。LFW 的典型用途是人脸验证与人脸识别算法评估,因为它的图片来自真实网络,背景杂乱、光照不均,比实验室自拍数据集更有说服力。
项目用lfw.tgz作为训练输入,解压后的目录结构需要保持原样:
tar xzf lfw.tgz mv lfw data/ # 查看前20个人名目录,确认解压正确 find data/lfw -maxdepth 1 -type d | head -20find命令列出前 20 个子目录,能确认数据是否解压完整。如果看到的是FOLDER_0这类编号目录而不是人名,说明压缩包被二次打包过,需要先修正结构。接下来生成训练标签时,脚本会基于这些目录名自动映射 label,所以目录名不能改动,否则标签会整体错乱。
注意:LFW 原始图片含较多背景噪声,训练前通常需要裁剪或归一化。这个项目一般直接用 160x160 居中裁剪,后续识别阶段也需要使用同样尺寸。
2.2 数据预处理:为什么要统一像素与人名标签
CNN 输入要求固定尺寸张量,而 LFW 图片是 250x250 且人脸不总在中心。常见做法是先用 OpenCV 或 MTCNN 检测人脸,再对齐到 160x160。对于初版毕业设计,中心裁剪加缩放是最省事的方案,虽然会损失部分头部区域,但能让训练先跑通,后续再升级检测逻辑。
下面这段是典型的数据读取脚本,思路是把文件夹名映射成整型标签,再统一缩放:
import os import cv2 import numpy as np def load_data(root, img_size=160): X, y = [], [] for label, name in enumerate(sorted(os.listdir(root))): person_dir = os.path.join(root, name) if not os.path.isdir(person_dir): continue for fname in os.listdir(person_dir): if not fname.endswith('.jpg'): continue img = cv2.imread(os.path.join(person_dir, fname)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (img_size, img_size)) X.append(img) y.append(label) return np.array(X), np.array(y)这段代码按目录名排序生成 label,保证同一个人的所有照片对应同一个整型标签。cv2.resize把图片统一为 160x160。注意这里没有做人脸对齐,如果数据集里存在多人合影或误检图片,准确率会受影响。后面的img_size参数必须和faces_train.py里的输入维度一致,否则模型训练时张量维度对不上。
还有一个容易忽略的点:训练前要按人的标签做分层划分,避免某些人只出现在训练集或只出现在验证集。常见做法是使用train_test_split的stratify参数:
from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42)stratify=y确保每个类别在训练集和验证集中的比例相近。random_state固定随机种子,保证实验可复现。很多同学忽略这一步,同一个项目跑两次结果差异很大,问题往往就出在这里。
2.3 文件依赖关系与参数配置
解压后的项目里,几个文件的分工很明确:
| 文件 | 作用 |
|---|---|
faces_train.py | 训练主脚本,负责加载数据、构建 CNN、保存 checkpoint |
faces_my.py | 自定义模型或数据调试脚本,通常是训练脚本的可修改副本 |
face_recognition.py | 在线识别脚本,加载模型并对单张图或摄像头帧进行识别 |
faces_other.py | 辅助脚本,用于测试不同预处理或对比不同模型 |
train_faces.model-900.meta等 | TensorFlow 训练到第 900 步保存的模型文件 |
events.out.tfevents.* | TensorBoard 日志文件 |
checkpoint | 记录最新模型路径 |
这套结构的核心是训练与识别分离。faces_train.py只负责输出模型,face_recognition.py只负责读取模型,中间通过train_faces.model-900三个文件衔接。如果解压后有一个master文件夹,通常模型文件会被输出到里面,识别脚本里的路径也需要同步改成master/train_faces.model-900。
很多人会在这一步踩坑:把.meta、.data-00000-of-00001和.index三个文件分开存放,导致 saver 找不到模型。正确做法是保持同一目录,且使用同一命名前缀,缺一个都无法还原计算图。
3. 卷积神经网络模型搭建与训练
3.1 网络结构选型:层数不是越深越好
从文件命名和 checkpoint 格式来看,这个项目运行在 TensorFlow 1.x 环境下,模型采用卷积神经网络提取人脸特征。在毕设场景下,常见选择是 LeNet-5 的改进版或像 VGG 一样堆叠卷积层。LeNet-5 只有两层卷积,用于灰度手写数字可以,但处理 RGB 人脸时特征表达能力不足。VGG 结构规整、容易理解,但参数太多,CPU 上训不动。更合理的是三层卷积加两层全连接的轻量结构,配合 BatchNorm 和 Dropout。
import tensorflow as tf def inference(images, num_classes=1680): # images: [batch, 160, 160, 3] with tf.variable_scope('conv1'): net = tf.layers.conv2d(images, 32, 3, activation=tf.nn.relu, padding='same') net = tf.layers.batch_normalization(net, training=True) net = tf.layers.max_pooling2d(net, 2, 2) with tf.variable_scope('conv2'): net = tf.layers.conv2d(net, 64, 3, activation=tf.nn.relu, padding='same') net = tf.layers.batch_normalization(net, training=True) net = tf.layers.max_pooling2d(net, 2, 2) with tf.variable_scope('conv3'): net = tf.layers.conv2d(net, 128, 3, activation=tf.nn.relu, padding='same') net = tf.layers.batch_normalization(net, training=True) net = tf.layers.max_pooling2d(net, 2, 2) with tf.variable_scope('fc'): net = tf.layers.flatten(net) embedding = tf.layers.dense(net, 128, activation=tf.nn.relu, name='embedding') dropout = tf.layers.dropout(embedding, rate=0.5, training=True) logits = tf.layers.dense(dropout, num_classes, name='logits') return logits, embedding这里的embedding层是 128 维特征向量,训练时走最后的 softmax 分类,测试时直接取embedding输出做比对。batch_normalization能让训练更稳定,max_pooling降低特征图尺寸同时保留主要位置信息。注意num_classes要等于数据集里面的人数,而不是图片总数。如果填错,损失计算和准确率统计都会报错。
3.2 训练脚本的核心逻辑
faces_train.py通常按四步走:加载数据、构建模型、定义优化器、启动 Session 训练。TensorFlow 1.x 里训练主循环是显式session.run,模型保存使用saver.save。参考代码如下:
x = tf.placeholder(tf.float32, [None, 160, 160, 3], name='input') y = tf.placeholder(tf.int64, [None], name='label') logits, _ = inference(x, num_classes=1680) loss = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits( logits=logits, labels=y)) train_op = tf.train.AdamOptimizer(0.001).minimize(loss) saver = tf.train.Saver(max_to_keep=5) with tf.Session() as sess: sess.run(tf.global_variables_initializer()) for step in range(900): batch_x, batch_y = get_batch(batch_size=128) _, loss_value = sess.run([train_op, loss], feed_dict={x: batch_x, y: batch_y}) if step % 100 == 0: print("step %d, loss %.4f" % (step, loss_value)) saver.save(sess, "train_faces.model", global_step=step)tf.sparse_softmax_cross_entropy_with_logits需要label是整型序列,对应我们前面生成的y。AdamOptimizer(0.001)在多数小数据集上都能直接收敛,不需要手动调学习率衰减。每 100 步保存一次模型,所以最终文件名会有-900。如果训练中断,checkpoint文件会记录最后一次保存位置,tf.train.latest_checkpoint能自动找到它。
3.3 训练参数:默认值能跑通,但需要理解
下面这组参数是这类项目常见的初始值,也适用于本项目的faces_train.py:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 图像尺寸 | 160x160 | 太小丢失五官细节,太大显存占用高 |
| batch_size | 64~128 | CPU 环境建议降到 32,否则一次迭代很慢 |
| 学习率 | 0.001 | Adam 默认值,一般不手动衰减 |
| 训练步数 | 900 | 小数据集上能收敛,但不一定达到最好精度 |
| dropout | 0.5 | 放在全连接层后,防止过拟合 |
| optimizer | Adam | 对学习率不敏感,适合初学者 |
训练步数 900 在 LFW 子集上可能只覆盖几个 epoch。如果损失曲线明显锯齿状抖动,优先把学习率降到 0.0001,再考虑调 batch。反过来,如果验证 loss 一直不降,通常不是参数问题,而是数据预处理没对齐或标签顺序乱了。
3.4 用 TensorBoard 观察训练过程
项目解压后的events.out.tfevents.*就是 TensorBoard 日志。训练脚本会在每个 step 写入 loss 和 accuracy。启动命令:
tensorboard --logdir=./ --port=6006然后浏览器打开http://localhost:6006。观察 loss 曲线是否平滑下降,也可以看直方图了解权重分布。如果损失在几百步后变成 NaN,第一检查点就是输入图像是否归一化到 0~1。很多人直接传 0~255 的像素值,梯度瞬间爆炸,这是最高频的坑。
4. 在线识别:从模型推理到摄像头实时框人
4.1 加载 checkpoint 并提取特征向量
在线识别的核心是加载训练好的train_faces.model-900三个文件,提取输入图片的 embedding。TensorFlow 1.x 里可以使用import_meta_graph直接恢复计算图:
import tensorflow as tf import numpy as np import cv2 tf.reset_default_graph() saver = tf.train.import_meta_graph('train_faces.model-900.meta') sess = tf.Session() saver.restore(sess, tf.train.latest_checkpoint('./')) graph = tf.get_default_graph() x = graph.get_tensor_by_name('input:0') embedding = graph.get_tensor_by_name('fc/embedding/Relu:0') def get_embedding(img): img = cv2.resize(img, (160, 160)) img = img.astype(np.float32) / 255.0 vec = sess.run(embedding, feed_dict={x: [img]}) return vec[0]latest_checkpoint读取根目录下的checkpoint文件,自动找到最后一次保存的模型。get_tensor_by_name需要知道计算图里的 tensor 名称,这里的fc/embedding/Relu:0对应我们前面模型定义里的embedding层输出。如果实际项目里层名不同,会报 KeyError,此时用graph.get_operations()打印所有节点名来确认。拿到 128 维向量后,后续所有识别都只在这组浮点数上运算,不再依赖原始图片。
4.2 人脸检测与对齐:在线识别里最容易被忽略
训练时如果直接用整张图,识别阶段也必须用相同方式取出人脸区域。最稳妥的方案是 MTCNN,它本身也是一个小型 CNN,能同时输出人脸框和五个关键点。参考代码:
from mtcnn import MTCNN detector = MTCNN() img = cv2.cvtColor(cv2.imread('test.jpg'), cv2.COLOR_BGR2RGB) face = detector.detect_faces(img) if face: x, y, w, h = face[0]['box'] crop = img[y:y+h, x:x+w] vec = get_embedding(crop) else: print("no face detected")face[0]取置信度最高的人脸框。直接裁剪会把背景也带进来,框太紧会切掉额头,框太松又引入无关区域。更精细的做法是使用face[0]['keypoints']做仿射变换,把双眼对准固定坐标。没有这一步,输入图片角度稍微变化,提取出的向量就会明显漂移,相似度阈值再准也认不出。
4.3 相似度计算与阈值判定
人脸识别最后一步是比较两个向量的相似度。余弦相似度比欧氏距离更常用,因为它对向量长度不敏感,受光照和对比度影响小。
def cosine_similarity(vec1, vec2): return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2)) threshold = 0.65 if cosine_similarity(vec_a, vec_b) > threshold: print("same person") else: print("different person")阈值 0.65 是经验值,不同数据集上分布差异很大。更可靠的方法是在验证集上计算所有正负样本对的相似度,画 ROC 曲线取等错误率对应的阈值。项目初始代码里写死阈值是为了方便演示,但答辩或实际应用前一定要重新标定。
4.4 在线识别的三种运行模式对比
这套系统的在线识别可以落在三种形式上,对应face_recognition.py的几种调用路径:
| 模式 | 输入源 | 延迟 | 适用场景 |
|---|---|---|---|
| 单张图片 | 文件路径 | 瞬时 | 验证一张图的身份 |
| 摄像头逐帧 | USB / 笔记本摄像头 | 100~300ms | 门禁、签到演示 |
| 视频文件 | mp4 / avi | 取决于帧率 | 离线回放、调试 |
摄像头模式需要每帧都跑一次人脸检测和特征提取,检测耗时超过帧间隔就会卡顿。常见做法是每隔 3 帧检测一次,中间帧用上一次的人脸框跟踪。也可以把送入网络的图片从 160x160 压缩到 112x112,识别速度提升近一倍,精度损失可控。
5. 排错与改进:这套系统还能怎么压榨
5.1 训练 loss 不降或震荡
三个高频原因:学习率过大、标签错乱、输入未归一化。先把 Adam 学习率从 0.001 降到 0.0001,看前 200 步 loss 是否平稳下降。标签错乱大多是os.listdir的排序在训练和验证脚本中不一致导致的,建议统一用sorted并且固定 random seed。输入图像必须除以 255,使像素范围落在 0~1,否则梯度数值容易溢出。
5.2 checkpoint 加载报错
如果报 “Cannot find checkpoint” 或 “No variables to save”,先确认三个.model-*文件是否在同一目录,加载时是否使用了绝对路径。再看tf.reset_default_graph()是否在import_meta_graph之前调用,否则之前残留的计算图节点会造成 tensor 名冲突。如果项目代码里的模型输出到了master/子目录,路径要对应改成master/train_faces.model-900。
5.3 提高识别准确度的具体技巧
不要一开始就上 triplet loss,小数据集上收敛极慢,毕设阶段容易失控。更实际的做法是:对同一人的多张照片分别提取 embedding,求平均后作为该人的注册向量。修改face_recognition.py就能做到。注册照片至少要覆盖正面、左右偏转和不同光照,只拍一张正面照会导致光线变化时相似度暴跌。
还可以检查faces_my.py里是否开启了数据增强。随机水平翻转、亮度抖动能明显提升鲁棒性,但注意验证集不能做增强。如果是纯 CPU 环境,把 batch_size 降到 16,图像尺寸从 160 降到 128,训练速度会快一倍,精度损失不大。调参前后各记录一组验证集准确率,答辩时直接拿出对比数据,比口头解释更有说服力。
本文还有配套的精品资源,点击获取