TensorFlow人脸识别卷积神经网络实战教程:从数据采集到模型训练
发布时间:2026/10/1 3:03:59来源:尧图网络
简介这是一份基于TensorFlow实现人脸识别神经网络的完整项目代码适合正在做毕业设计或初学卷积神经网络CNN的开发者参考。项目围绕“让网络认识指定人脸”的目标清晰划分了数据准备与模型训练流程get_my_faces.py 负责采集并预处理本人面部图像作为正样本set_other_faces.py 收集他人照片作为负样本train_faces.py 构建并训练卷积神经网络is_my_face.py 加载模型判断画面中是否为目标人脸四步形成可运行的闭环。资源包共6个文件除4个Python源码外另含 README 说明文件与 LICENSE 授权文件整体仅14KB结构紧凑、注释清晰方便阅读和二次修改README 中对目录结构和运行步骤也有简要说明便于快速上手。运行环境支持 Windows 或 Linux搭配 Python 3.x 与 TensorFlow 即可使用。目前已有625人学习下载对希望快速上手 TensorFlow 人脸识别项目、完成课程设计或毕业设计的学生具有不错的借鉴价值。1. 把 TensorFlow 人脸识别跑起来这份教程能解决什么人脸识别是 TensorFlow 卷积神经网络里最适合做入门实战的方向因为它数据好采集、效果肉眼可见、训练周期也不长。这份《基于 TensorFlow 训练的人脸识别神经网络 毕业设计完整教程》包含四个 Python 脚本从采集自己的人脸、扒取其他人照片做参照集到训练 CNN 模型最后用摄像头判断画面里是不是“我”整个链路是闭环的。不像网上那些只给训练代码、不给数据集的半吊子资源这套脚本把数据生产、训练、推理的每一步都拆开了而且是 TensorFlow 1.x 的原生写法逻辑直白适合大学生做毕设也适合想搞清楚 TensorFlow CNN 到底怎么运作的入门开发者。我用它跑通了一次完整流程用自己的照片训练出一个能认出我的模型然后接上摄像头实时判断。整个过程里最大的感受是——这套东西的难点不在训练而在数据采集和图片预处理很多人卡在 loss 不降、识别不准这些问题上九成都是数据没整理好。下文我会把环境搭建、数据采集、训练参数、避坑经验按实际操作顺序讲清楚。2. 为什么人脸识别要用卷积神经网络选型理由与网络结构拆解2.1 传统人脸识别方法的瓶颈老牌的人脸识别方案走的是“特征工程 分类器”路线用 Haar-like 特征、LBP 特征或 HOG 特征把脸部纹理描述出来再丢给 SVM 或 Adaboost 分类。这种方案在小规模、受控场景下比如门禁机正对摄像头、光线均匀效果尚可但一遇到侧脸、遮挡、光照突变就明显下降。因为手工设计的特征本质上是在用人的经验去猜“什么特征最能代表人脸”而这种猜测的氛围是有限的。卷积神经网络CNN的思路完全不同。它让卷积核自己去学习该提取什么特征。浅层卷积核学到的是边缘、颜色块深层卷积核学到的是眼睛、鼻子这种语义部件最终的全连接层再把高维特征映射到类别得分。整个过程把“特征提取 分类”揉进了一个可端到端训练的网络里直接对原始像素输入做优化。这也是为什么近年人脸识别门禁机、刷脸支付全部转向了 CNN 方案——不是因为它听起来高级而是它确实在复杂场景下鲁棒性更好。2.2 这套教程的网络结构三层卷积 全连接 Softmax打开 train_faces.py 可以看到这个例程走的是标准的 CNN 分类路线不是 Siamese Network也不是 FaceNet 那套度量学习方案。它做的事很简单把脸图分为“我”和“不是我”两类。这意味着网络输出层只有两个神经元经过 softmax 后得到属于“我”的概率。网络主体由卷积层、池化层、全连接层堆叠而来。输入的图片统一被缩放到特定尺寸经过卷积核提取特征图再用最大池化做降维把平移和局部变化带来的影响压下去最后展平后接入全连接层用 ReLU 做激活输出层接 softmax。这一套在 TensorFlow 1.x 里通常用tf.nn.conv2d、tf.nn.max_pool、tf.nn.relu组合实现。整个设计遵循了 2012 年 AlexNet 之后的主流范式卷积层的深度不用太深因为数据集规模很小太深的网络容易过拟合。人脸识别这个任务本身有一个好处——所有样本的结构高度对齐眼睛在上、嘴巴在下因此 CNN 的局部连接特性天然适合捕捉这类空间结构。2.3 二分类与多分类的权衡为什么先做“我 vs 不是我也”有人会问既然要做人脸识别为什么不直接训练一个能识别 ABCD 四个人的四分类模型原因是多分类需要为每个人都准备足够多且均衡的照片数据采集和维护成本都会上来。而这套教程的目标是“这台摄像头认不认识我”本质上是一个验证问题二分类就够用了。二分类还有一个隐性好处负样本的获取比正样本容易。正样本只要拍自己几百张就行负样本可以抓取任意其他人的照片数量管够。这正好契合 CNN 训练对数据量的需求。如果你后续想扩展成多分类其实不需要重写网络结构只需要把最后的[2, n_classes]改成[1, n_classes]对应的权重维度再换数据加载逻辑就行下面我会讲到具体改法。3. 数据采集与预处理get_my_faces.py 和 set_other_faces.py 的核心逻辑3.1 用开拍自己的脸get_my_faces.py 详解get_my_faces.py 的作用是调用 OpenCV 的人脸检测器从摄像头画面中框出人脸并保存为图片。脚本里关键的三行逻辑如下import cv2 # 使用 OpenCV 自带的 Haar 级联分类器检测人脸 face_detector cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) cap cv2.VideoCapture(0) count 0 while count 500: ret, frame cap.read() # 读一帧摄像头画面 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 转为灰度检测更快更稳定 faces face_detector.detectMultiScale( gray, scaleFactor1.2, minNeighbors5, minSize(64, 64) ) for (x, y, w, h) in faces: face frame[y:yh, x:xw] # 裁剪出人脸区域 face cv2.resize(face, (64, 64)) # 统一缩放到 64x64 cv2.imwrite(f./my_faces/{count}.jpg, face) count 1 print(f已保存第 {count} 张人脸)这段代码的运行逻辑是每读取一帧画面就做一次人脸检测检测到人脸后直接裁剪并缩放保存。scaleFactor1.2表示每次搜索窗口缩放 1.2 倍这个值越小检测越慢但召回率越高minNeighbors5表示最少有 5 个邻近窗口都认为这是人脸才确认值越大误检越少但可能漏检小尺寸人脸。注意这里有一个关键细节裁剪是从frameBGR 彩色图里切的不是从gray里切的。因为后续训练需要 RGB 颜色信息如果存成灰度图模型可能只学到了亮度特征真实场景下的泛化能力会打折扣。我个人会建议在保存时做一次cv2.cvtColor(face, cv2.COLOR_BGR2RGB)再存保持数据格式统一避免训练和推理时颜色通道不一致的问题。3.2 收集参照人脸set_other_faces.py 的两种实现思路set_other_faces.py 负责准备“不是我”的数据集。脚本的常见做法有两种一种是从网上下载其他人的公开人脸图片另一种是用摄像头自己在不同背景下采集非本人的脸。网络下载方式比较省事但要注意图片质量和版权自己拍虽然慢一点但样本的光照分布和你实际使用场景更接近模型在真实场景下的误判率会更低。无论哪种方式脚本里一定会有和 get_my_faces.py 相同的后处理逻辑检测人脸区域、裁剪、缩放、保存。这里提醒一句负样本的多样性比数量更重要。同样是一千张照片如果全部是同一个人在同一个背景下拍的模型只会学到“这个人不是目标”而不是“非目标这类人不是目标”。负样本应该覆盖不同年龄、不同肤色、不同拍摄角度、不同光照条件的人脸。3.3 数据目录结构与训练脚本的读取约定train_faces.py 读取数据时约定按目录区分正负样本典型结构如下data/ ├── my_faces/ # 自己拍的人脸照片作为正样本 └── other_faces/ # 他人的照片作为负样本训练脚本读取数据时通常会遍历这两个目录把文件夹名字当作 label 编号来用。这里最容易犯错的是一个文件夹里的图片尺寸大小不一。训练脚本内部虽然会对图片做统一缩放但不同脚本的缩放尺寸可能不一致——get_my_faces.py 存的是 64×64而 train_faces.py 读入后可能还要再 resize 一次。我的建议是让采集和训练统一到同一个尺寸省掉一次重采样减少信息损失。4. train_faces.py 训练全过程参数设置与 loss 崩坏排查4.1 读取数据集并构造占位符train_faces.py 的数据加载部分核心逻辑如下import tensorflow as tf import os import cv2 import numpy as np def load_data(data_dir): images [] labels [] # 遍历 my_faces 和 other_faces 两个子目录 for label_name, label_id in [(my_faces, 1), (other_faces, 0)]: dir_path os.path.join(data_dir, label_name) for img_name in os.listdir(dir_path): img_path os.path.join(dir_path, img_name) img cv2.imread(img_path) img cv2.resize(img, (64, 64)) # 统一尺寸 img img.astype(np.float32) / 255.0 # 归一化到 0~1 images.append(img) labels.append(label_id) # 打乱顺序避免同类别样本连续出现影响梯度更新 idx np.random.permutation(len(images)) return np.array(images)[idx], np.array(labels)[idx] x tf.placeholder(tf.float32, [None, 64, 64, 3]) y tf.placeholder(tf.float32, [None, 2])注意这里归一化除以 255 是必须的步骤。如果不做归一化像素值在 0~255 之间卷积层输出的特征图数值范围很大梯度下降会非常不稳定典型表现是 loss 在前几十步就变成 NaN。tf.placeholder的第一个维度设为None表示在训练时可以每次喂一个 batch不必一次性把全量数据塞进显存。4.2 构建卷积神经网络主体网络结构用 TensorFlow 原生 API 搭建典型写法如下def conv_net(x): # 第一层卷积5x5 卷积核输入通道 3输出 32 个特征图 conv1 tf.nn.conv2d(x, tf.Variable(tf.truncated_normal([5, 5, 3, 32], stddev0.1)), strides[1, 1, 1, 1], paddingSAME) pool1 tf.nn.max_pool(tf.nn.relu(conv1), ksize[1, 2, 2, 1], strides[1, 2, 2, 1], paddingSAME) # 第二层卷积5x5 卷积核输入 32 通道输出 64 个特征图 conv2 tf.nn.conv2d(pool1, tf.Variable(tf.truncated_normal([5, 5, 32, 64], stddev0.1)), strides[1, 1, 1, 1], paddingSAME) pool2 tf.nn.max_pool(tf.nn.relu(conv2), ksize[1, 2, 2, 1], strides[1, 2, 2, 1], paddingSAME) # 池化后特征图尺寸64x64 输入经过两次池化变成 16x16 flatten tf.reshape(pool2, [-1, 16 * 16 * 64]) fc1 tf.nn.relu(tf.layers.dense(flatten, 256)) out tf.layers.dense(fc1, 2) return outtf.truncated_normal初始化卷积核权重标准差设为 0.1是为了让初始权重落在较小范围内避免激活值饱和。paddingSAME保证了卷积操作不改变特征图的空间尺寸这样池化后的尺寸可以根据输入尺寸直接推算64×64 经过两次 2×2 最大池化后是 16×16。如果你把输入尺寸换成 128×128这里的全连接层第一个维度的数字要同步改成32 * 32 * 64这是新手最容易忽略的坑。tf.layers.dense是 TensorFlow 的高层 API内部自动创建权重和偏置省掉了手动定义tf.Variable的样板代码。输出层设成 2 个神经元正好对应“是我”和“不是我”两个类别。4.3 损失函数与优化器选择训练部分的核心代码逻辑如下logits conv_net(x) # softmax 交叉熵损失内部会做 softmax 计算 loss tf.reduce_mean( tf.nn.softmax_cross_entropy_with_logits_v2(logitslogits, labelsy) ) # Adam 优化器学习率 0.001 train_op tf.train.AdamOptimizer(learning_rate0.001).minimize(loss) # 计算准确率用于监控训练效果 correct_pred tf.equal(tf.argmax(logits, 1), tf.argmax(y, 1)) accuracy tf.reduce_mean(tf.cast(correct_pred, tf.float32)) with tf.Session() as sess: sess.run(tf.global_variables_initializer()) # 训练 200 个 epoch每个 batch 取 64 张图 for epoch in range(200): for start in range(0, len(train_x), batch_size): batch_x train_x[start:start 64] batch_y train_y[start:start 64] sess.run(train_op, feed_dict{x: batch_x, y: batch_y})这里用softmax_cross_entropy_with_logits_v2而不是先对 logits 做 softmax 再算交叉熵是因为这个函数在内部同时做了 softmax 和交叉熵计算数值上更稳定不会因为概率值太小出现 log(0) 导致的 NaN。学习率 0.001 是 Adam 优化器最常用的默认值在这类小数据集上表现得相当稳妥不建议轻易加大到 0.01——我跑过一次loss 直接震荡到飞起。如果你的 TensorFlow 版本较老函数名末尾不带_v2可以直接用tf.nn.softmax_cross_entropy_with_logits两者的接口完全一致。4.4 epoch、batch_size 与数据量怎么配这套训练脚本里200 个 epoch 配合 batch_size 64 属于一个比较稳的配置。你的总数据量如果在 1000 张左右那么每个 epoch 大约有 15 个 batch 更新200 个 epoch 就是 3000 次梯度更新这个量级对三层卷积网络刚好够收敛。训练过程里最值得关注的是 loss 曲线的形态。正常的收敛过程应该是前几十个 epoch loss 快速下降中段缓慢下降后期基本平缓。如果 loss 在某个值附近震荡但不下降不是网络有问题而是学习率偏大如果 loss 直接变成 NaN基本可以断定是输入数据里有异常值或者梯度爆炸。下面避坑章节我会专门展开讲。5. 人脸识别常见问题与排查我从跑通到翻车的全过程记录5.1 现象loss 为 NaN训练无法继续原因输入图片没有归一化到 0~1像素值 0~255 直接喂进网络激活值和梯度的数值范围太大导致梯度爆炸。另一个常见原因是某一类样本图片全部是纯黑或纯白图卷积核学习到的特征方差为 0。解决在数据加载阶段加入img.astype(np.float32) / 255.0并且检查数据集中是否有损坏图片。可以用下面这段代码快速筛查import cv2 import os for root, dirs, files in os.walk(./data): for f in files: path os.path.join(root, f) img cv2.imread(path) if img is None: print(f损坏图片: {path}, f) elif img.shape ! (64, 64, 3): print(f尺寸不符: {path}, img.shape)5.2 现象训练准确率很高但摄像头实测一直认不出我原因过拟合。模型只是背下了训练集里我那些照片的角度、光线和背景一旦我换了个角度或者换了环境光线特征就对不上了。这是人脸识别里最常见的翻车现场——训练集和测试集分布不一致。解决采集照片时人为增加多样性。拍的时候要转动头部、改变距离、换不同的环境光正样本至少覆盖五个角度。另外可以在训练时加入数据增强最朴素的做法是水平翻转import cv2 import numpy as np img cv2.imread(./my_faces/1.jpg) flip_img cv2.flip(img, 1) # 水平翻转 cv2.imwrite(./my_faces/1_flip.jpg, flip_img)5.3 现象识别结果里“其他人也被认成我”的误判率高原因负样本多样性不足。如果 set_other_faces.py 下载的照片全是同一个人模型只会学到“这个人不是我”而不是泛化出“这一类都不是我”的边界。解决扩充负样本确保负样本里面包含不同性别、年龄、肤色、有无戴眼镜的人脸照片。负样本数量最好是正样本的 2 倍以上。二分类问题中负样本充裕能让决策边界更紧地贴合正样本的真实分布。5.4 现象TensorFlow 报错 ValueError: Cannot feed value of shape (64, 64, 3)原因placeholder 定义的是四维张量[None, 64, 64, 3]但读取图片时丢了 batch 维度直接喂进去了一张三维图。解决在喂数据前手动加一个维度batch_x batch_x.reshape(-1, 64, 64, 3)或者加载图片时就用np.expand_dims(img, axis0)包一层。这个问题在训练时不容易暴露因为 train_faces.py 里加载数据后已经是四维数组了但在你自己写推理脚本或测试脚本时极容易踩中。5.5 现象tf.layers.dense 报 AttributeError原因TensorFlow 1.0 早期版本的tf.layers.dense接口与后续版本有差异或者你用的是 TensorFlow 2.xtrain_op tf.train.AdamOptimizer(...).minimize(loss)这种 1.x API 已经被移除了。解决如果用的是 TensorFlow 2.x需要开启兼容模式import tensorflow.compat.v1 as tf tf.disable_v2_behavior()如果不想费劲处理兼容问题就直接用 1.14 或 1.15 版本跑这套代码一次成功。6. is_my_face.py 验证与进阶从单张图片到实时摄像头6.1 加载模型并做推理判断训练完成后模型已经保存在 checkpoint 文件里is_my_face.py 会把模型重新加载回来对摄像头画面里的每一张人脸做判断。核心推理逻辑如下import tensorflow as tf import cv2 # 复用训练时的网络结构必须和 train_faces.py 保持完全一致 def conv_net(x): # 这里复制训练脚本里的完整网络结构 pass x tf.placeholder(tf.float32, [None, 64, 64, 3]) logits conv_net(x) prob tf.nn.softmax(logits) # 得到每个类别的概率 saver tf.train.Saver() with tf.Session() as sess: saver.restore(sess, ./model/face_model.ckpt) # 读入一张待测试的图片 img cv2.imread(./test_me.jpg) img cv2.resize(img, (64, 64)) img img.astype(np.float32) / 255.0 input_img img.reshape(-1, 64, 64, 3) result sess.run(prob, feed_dict{x: input_img}) my_face_prob result[0][1] # 类别 1 对应的概率 print(f属于我的概率: {my_face_prob:.2f})如果拿到了my_face_prob接下来要做的就是设定一个阈值。阈值定多少是个值得说的问题——默认情况下取 0.5 只是数学上最优实际场景里如果你更在意不能误认别人阈值可以调到 0.8如果你更在意不能漏掉自己阈值调到 0.3 也行。这个参数没有人能替你定因为每个人的照片质量、采集环境都不一样得自己拿十几张没参加过训练的照片实测统计一下正样本和负样本的概率分布再选定一个分界点。6.2 接上摄像头做实时判断实时识别比单张图片多了一步在循环里不断读取画面检测人脸缩放到 64×64喂入模型把结果显示在画面里。流程可以这样组织cap cv2.VideoCapture(0) face_detector cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) while True: ret, frame cap.read() gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_detector.detectMultiScale(gray, 1.2, 5, minSize(64, 64)) for (x, y, w, h) in faces: face frame[y:yh, x:xw] face cv2.resize(face, (64, 64)) input_img face.astype(np.float32) / 255.0 input_img input_img.reshape(-1, 64, 64, 3) prob sess.run(my_face_prob, feed_dict{x: input_img}) label Me if prob 0.7 else Other cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.putText(frame, f{label} {prob:.2f}, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(Face Recognition, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码里有一个值得注意的细节每帧做一次人脸检测其实是比较耗 CPU 的在性能较弱的机器上会出现画面卡顿。常见做法是跳帧检测比如每 3 帧检测一次中间两帧直接沿用上一帧的检测框位置这样画面流畅度会好很多。另外cv2.waitKey(1)后面的0xFF不能省在 Windows 和 Linux 下返回值的高 8 位有不同的填充行为少了这个掩码按键退出经常失灵。6.3 从二分类扩展到多分类的改法如果你想让这套系统认识多个不同的人把二分类改成多分类不算大改。核心改动只有三处label 映射从[{my_faces: 1, other_faces: 0}]扩展成多个人各自的目录和编号网络输出层从2改成人数ntf.argmax(logits, 1)的结果就对应不同的身份。但这里有一个实际约束——每个人要有足够数量的样本才能训练出有效边界我的经验是每人至少 300 张低于这个数模型很容易把不同的人混在一起。还有一个方向是只保留正样本拿本教程的模型做预训练的特征提取器把全连接层之前那层向量当做人脸特征再用度量学习的方法去比较相似度。不过这套改法比直接多分类复杂需要改 loss 函数和推理逻辑适合学有余力的人去折腾。我自己跑这个项目掉进过最大的坑是训练前期偷懒对着摄像头不动拍了 500 张几乎一模一样的照片结果模型在训练集上准确率到了 98%面对真实场景时只有 40% 出头的识别率。后来我把采集脚本改成每 5 帧存一张并且拍摄时刻意转动头部、改变与摄像头之间的距离数据集质量上来之后实测准确率直接回到了 90% 以上。从那以后我每次做图像相关的训练都强制先把数据集的多样性检查做一遍再谈调参。这套教程教会我的不只是 TensorFlow 神经网络怎么写更重要的是它让我理解了数据质量才是影响识别效果的绝对主导因素。希望这篇拆解能让你的毕设少走一点弯路把踩坑的时间花在网络结构和参数优化上。本文还有配套的精品资源点击获取
网站建设高端定制企业官网