简介:基于Python机器学习的人脸识别期末大作业项目,适合高校学生作为课程设计或期末大作业参考。项目已获导师指导并被评为97分高分,包含完整可运行的源码、课程报告与项目说明,覆盖人脸识别、性别检测等典型应用场景,代码注释清晰,目录结构明朗,下载后无需修改即可运行。压缩包共10个文件,其中6个Python脚本负责训练、测试与实时识别,3个PDF为课堂汇报、中期报告与最终报告,另有1个Markdown说明文件,整体大小约3.93MB。目前已有428人学习下载,可作为机器学习综合实践的良好范例。资源提供了从数据预处理、模型构建到可视化展示的完整流程,并附带BP神经网络实现性别检测的扩展实验,能够帮助学习者快速复现人脸识别系统,理解特征提取与分类器设计的关键方法,是兼顾课程汇报与代码实践的优质资料。
1. 为什么把课程设计选成人脸识别
人脸识别是机器学习课程作业里性价比最高的题。它不需要在本科阶段硬磕大规模卷积网络,而是用 OpenCV 的 Haar Cascade 先把人脸框出来,再交给 BP 神经网络做性别或身份分类,检测、特征、训练、评估四个环节全部覆盖,完整跑通只需要一个摄像头和一台普通笔记本。这份拿到 97 分的大作业源码恰好是按这个套路组织的,里面既有 photo/video 两套输入链路,也有 BP 网络做性别检测的完整预测代码,还有可直接提交的课程报告和课堂汇报材料,适合正在做课程设计、期末大作业,或者想找一个可复现人脸识别 baseline 的读者。下面按文件结构、BP 原理、代码走读、调参与验收的顺序展开,每段代码都能抄下来直接改。
2. 项目文件结构与 BP 神经网络的输入输出链路
2.1 解压后每个文件是干什么的
先把 zip 解压开,逐个对文件用途做标注。
| 文件名 | 类型 | 用途 |
|---|---|---|
| photo_sex_rec.py | 图片 + 性别 | 读入单张图片,检测人脸并预测性别 |
| photo_lib_sex_rec.py | 图片 + 建库 | 对一批图片建立人脸特征库并执行识别 |
| video_sex_rec.py | 视频/摄像头 + 性别 | 逐帧检测,实时性别分类 |
| video_lib_sex_rec.py | 视频 + 建库 | 视频流中与预建库比对身份 |
| photo_test.py | 测试脚本 | 单张图片快速验证模型文件和权重文件是否可用 |
| video_face_rec.py | 视频 + 人脸检测 | 只画人脸框,不做分类 |
| README.md | 说明文档 | 运行顺序、环境依赖与目录说明 |
| 课堂汇报.pdf | 答辩材料 | 答辩用演示稿 |
| 最终报告.pdf / 工作报告.pdf | 课程报告 | 算法描述、实验数据与结果分析 |
脚本命名本身就在拆解工作流:photo 是静态图片检测,video 是摄像头或视频流检测,lib 后缀代表需要先构建人脸库,sex 是性别分类,face_rec 是纯检测。命名越规范,报告里画系统架构图越省事,老师一眼能看出模块划分。photo_test.py 是调试入口,先跑它确认权重没加载错,再去看视频链路,能省掉大部分环境问题排查时间。
2.2 BP 神经网络的输入特征与网络设计
2.2.1 人脸区域归一化与特征向量拼接
BP 网络的输入层节点数等于特征维度。如果把整帧视频画面直接送进去,几十万维的输入会让权重矩阵膨胀,训练极慢且极易过拟合。所以标准做法是先靠 Haar 级联把背景信息剔除,只保留人脸框,再压缩成固定尺寸的小图。常见做法是 resize 到 32x32 或 64x64 的灰度图,输入维度是 1024 或 4096。
我一般把像素值直接除以 255 缩放到 [0,1],而不是做标准化。sigmoid 的输出域也在 (0,1) 区间,两边尺度对齐之后,网络初始权重在 ±0.5 附近时,前向输出不会一开始就顶到饱和区,梯度消失的概率低很多。如果想把准确率再往上拉一点,可以把特征扩展为像素向量加灰度均值加灰度方差,形成一个 1026 维的输入,这个拼接在一行 concatenate 里就能完成。
import cv2 import numpy as np def extract_feature(roi_gray, target_size=(32, 32)): # 统一尺寸,避免不同分辨率下人脸区域维度不一致 roi = cv2.resize(roi_gray, target_size) # 像素归一化到 0~1,匹配 sigmoid 输出域 pixels = roi.reshape(-1) / 255.0 # 附加全局光照信息,辅助 BP 区分明暗差异 extra = np.array([roi.mean(), roi.std()]) return np.hstack([pixels, extra])逻辑说明:第一行 resize 把所有检测框强制压成相同尺寸,这是批量训练的前提;reshape(-1) 把二维灰度图拉平成一行,得到 1024 维向量;mean 和 std 分别描述人脸区域整体亮度和对比度,属于最简单的全局特征。
参数说明:target_size 越小训练越快,但 16x16 会丢掉眉毛、嘴唇等性别相关细节,一般不要低于 24;均值特征对人脸偏暗或过曝的场景有一定补偿作用,实测在混合光照数据上能把性别准确率提升 2~3 个百分点。
2.2.2 三层 BP 的前向传播、损失与参数选择
性别检测本质是二分类。典型的网络结构是输入层 1024 个节点、隐藏层 64 个节点、输出层 1 个节点,隐藏层激活函数用 sigmoid,输出层也走 sigmoid,损失函数用交叉熵而不是均方误差。交叉熵在预测完全错误时梯度更大,对课程设计这种几百条样本的小数据集收敛更干脆。
import numpy as np def sigmoid(x): return 1.0 / (1.0 + np.exp(-x)) def sigmoid_deriv(x): return x * (1.0 - x) class BPNet: def __init__(self, n_in=1026, n_hidden=64, n_out=1, lr=0.01): self.lr = lr # 小随机初始化避免权重对称,0.5 是经验缩放值 self.w1 = np.random.randn(n_in, n_hidden) * 0.5 self.b1 = np.zeros(n_hidden) self.w2 = np.random.randn(n_hidden, n_out) * 0.5 self.b2 = np.zeros(n_out) def forward(self, X): # 输入经过隐藏层,再压缩到单输出节点 self.z1 = X.dot(self.w1) + self.b1 self.a1 = sigmoid(self.z1) self.z2 = self.a1.dot(self.w2) + self.b2 self.a2 = sigmoid(self.z2) return self.a2 def backward(self, X, y, pred): # 批量梯度下降,m 是当前批次样本数 m = X.shape[0] d2 = (pred - y) * sigmoid_deriv(self.a2) d1 = d2.dot(self.w2.T) * sigmoid_deriv(self.a1) self.w2 -= self.lr * self.a1.T.dot(d2) / m self.b2 -= self.lr * d2.mean(axis=0) self.w1 -= self.lr * X.T.dot(d1) / m self.b1 -= self.lr * d1.mean(axis=0)逻辑说明:forward 把 1026 维输入压缩到 64 维隐藏层,再映射到 1 个输出节点,输出值越接近 1 判定为某一性别,越接近 0 判定为另一性别。backward 里 d2 和 d1 是交叉熵误差对输出层、隐藏层的梯度,除以 m 是为了让更新步长与 batch 大小解耦。
参数说明:n_hidden 太小网络学不到有效特征,太大在几百条样本上必然过拟合,32 到 128 都可接受;lr 从 0.01 起步,训练时 loss 震荡就降到 0.001;w1 的初始化缩放系数 0.5 是适配 sigmoid 的常见选择,换成 1.0 容易让隐藏层输出一开始就饱和。
| 参数 | 推荐值 | 取值依据 |
|---|---|---|
| n_hidden | 32 ~ 128 | 节点过少欠拟合,过多过拟合 |
| lr | 0.01 起调 | loss 震荡则降为 0.001 |
| epochs | 100 ~ 300 | 小数据集再增加无实际收益 |
| batch | 全部样本或 64 | numpy 内存足够,不需刻意设小 |
2.3 训练数据组织、权重保存与恢复
课程设计的隐藏加分项是数据来源交代清楚。报告里必须写明训练集、验证集、测试集的拆分比例,并贴几张预处理后的样本图。常见做法是从 UTKFace 或 IMDB-WIKI 这类公开人脸数据集里按性别归档,也可以收集同学正面照手动标注,但报告数据页要打码或说明已征得同意。本项目没有把原始训练集放进 zip,README 给出的是推荐目录组织方式:train/male、train/female、test/male、test/female。按这个目录存放图片,训练脚本读取时最省事。
训练完的权重用 numpy 直接落盘,预测脚本不依赖训练数据也能独立运行。
# 训练结束后保存全部权重到单个 npz 文件 np.savez('bp_sex.npz', w1=w1, b1=b1, w2=w2, b2=b2) def load_net(path='bp_sex.npz'): data = np.load(path) net = BPNet() # 把保存的参数逐项恢复到网络实例 net.w1, net.b1 = data['w1'], data['b1'] net.w2, net.b2 = data['w2'], data['b2'] return net逻辑说明:savez 把四个权重矩阵打包成一个 npz 文件,磁盘占用通常不到 200KB;load_net 返回的是一个已经能直接 forward 的 BPNet 实例,photo 和 video 脚本共用这个入口。
参数说明:npz 是压缩格式,加载前不要手动解压成单个文件;如果报告里对比了 BP 与 CNN 的性能差异,建议在同一份测试集上记录准确率和单帧耗时,即使只是文字说明,也能明显提升答辩时的方法对比深度。
3. 图片与视频双链路走读:photo_sex_rec.py 与 video_sex_rec.py
3.1 图片静态检测的完整调用链
图片链路的大致执行流程是:解析命令行参数、读图、转灰度、Haar 检测人脸、对每个检测框裁剪缩放、送入 BP 预测、画框打标签、写回结果。这类工程的检测函数通常封装成一个统一的 detect_and_predict,视频链路只是把这个函数套进帧循环里。
import cv2 import numpy as np face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') def detect_and_predict(img, net, target_size=(32, 32), threshold=0.5): gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # scaleFactor 控制图像缩放步长,minNeighbors 控制候选框确认数 faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(24, 24)) for (x, y, w, h) in faces: roi = gray[y:y+h, x:x+w] feature = extract_feature(roi, target_size) feature = feature.reshape(1, -1) pred = net.forward(feature)[0][0] label = 'male' if pred > threshold else 'female' # 在原始彩色图上绘制矩形与标签 cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.putText(img, label, (x, y - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) return img逻辑说明:detectMultiScale 返回若干个人脸矩形坐标,代码对每个矩形做一次特征提取与预测,所以一张合影里出现多张脸时,循环会分别给每个脸打上性别标签。reshape(1, -1) 是把一维特征包装成一行样本,因为 BPNet.forward 期望输入是二维矩阵。
参数说明:scaleFactor=1.1 是精度和速度的平衡点,改大检测变快但容易漏检;minNeighbors=5 表示候选框至少要得到 5 个相邻框支持才确认为人脸,误检多时调到 8,漏检多时调到 3;minSize=(24, 24) 过滤掉小噪点框,做合影识别时如果人脸较小,要调小到 (20, 20),否则输出结果会漏掉后排人脸。
photo_sex_rec.py 的 main 入口一般就是拼装上面的函数。比较稳妥的命令行写法是支持图片路径和权重路径两个参数,例如 python photo_sex_rec.py --image test.jpg --model bp_sex.npz。threshold 最理想的做法是从训练集的正负样本比例推算,样本均衡时直接用 0.5;现场演示时为了减少性别标签来回跳,我通常固定为 0.6。
3.2 视频逐帧检测与库模式比对的差异
3.2.1 lib 模式:先建特征库再算距离
photo_lib_sex_rec.py 和 video_lib_sex_rec.py 的核心思路是:先对一批已知人物的图片提取人脸区域特征,保存为特征库;检测时把当前人脸的特征与库中逐一计算欧氏距离,取距离最小且低于阈值的那条作为识别结果。这样识别的是“这个人是谁”,而不是只分男女。
def build_library(image_paths, target_size=(64, 64)): library = [] for p in image_paths: img = cv2.imread(p) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5) if len(faces) == 0: continue x, y, w, h = faces[0] roi = cv2.resize(gray[y:y+h, x:x+w], target_size) # 入库存特征也做归一化,保证与查询特征同一量纲 library.append(roi.reshape(-1) / 255.0) return np.array(library) def match_face(lib, query, dist_thresh=0.6): # 逐行计算欧氏距离,取最近且低于阈值的编号 dists = np.linalg.norm(lib - query, axis=1) idx = np.argmin(dists) return idx, dists[idx] if dists[idx] < dist_thresh else -1逻辑说明:build_library 对每一张入库图片取第一个检测到的人脸作为目标,统一缩放到 64x64;match_face 用 numpy 的广播机制一次性算出查询特征与库中所有人脸的距离,argmin 拿到最近邻索引。
参数说明:建库尺寸用 64x64 而没有沿用性别分类的 32x32,是因为身份比对需要更细的纹理信息,模糊到 32 会明显拉高不同人之间的相似度;dist_thresh=0.6 是拒绝阈值,用于把陌生人挡在识别结果之外,具体数值和库的大小强相关,库里有 10 个人和 50 个人时同样的距离含义不同,提交前要用带干扰背景的样本重新标定。
3.2.2 实时模式:摄像头采集与逐帧预测
video_sex_rec.py 做的事情就是循环读摄像头帧。一个常见问题是默认分辨率过高导致 Haar 检测耗时过长,笔记本自带摄像头 640x480 是比较划算的档位。
cap = cv2.VideoCapture(0) # 外接摄像头索引可能是 1,找不到画面时从 1 开始试 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while True: ret, frame = cap.read() if not ret: break frame = detect_and_predict(frame, net, threshold=0.6) cv2.imshow('face sex rec', frame) # waitKey(1) 等待 1ms,返回键盘输入的 ASCII 码 if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()逻辑说明:cap.read 每次取一帧 BGR 图像,ret 为 False 表示摄像头被占用或索引错误,此时需要切换 VideoCapture 的索引参数。detect_and_predict 在函数内部完成灰度转换和 Haar 检测,所以摄像头取到的原始彩色帧直接传入即可。
参数说明:waitKey(1) 的 1 表示每帧最多等待 1 毫秒,数字越小循环越快;& 0xFF 是取低 8 位,避免不同平台上特殊按键返回多字节码导致判断失效。如果帧率明显偏低,优先把缩放帧宽降到 480,而不是一味调小 minSize,因为 minSize 过小会引入大量误检框,耗时反而更高。
3.3 三种典型失败场景的排查顺序
代码能跑通但结果不对时,按三个方向排查。第一种是检测框为空,先确认 cascade 文件加载路径是否正确,再用 photo_test.py 跑一张近距离正脸图,如果仍然无框,把 minNeighbors 调到 3 试一次。第二种是性别预测结果恒为 0.5 附近,几乎可以肯定是网络权重没加载成功,此时 forward 使用的还是随机初始化参数,检查 load_net 调用后 w1 是否被替换。第三种是视频画面卡顿,先去掉 cv2.imshow 单跑耗时测试,定位瓶颈是在 Haar 检测还是 BP 前向传播,常见瓶颈是前者。
4. 环境搭建、训练收敛与跨平台踩坑
4.1 依赖安装与摄像头索引确认
项目基于 Python 3.7 以上版本和 OpenCV,运行前安装这几个包就够。sklearn 用不上时可以不装,但训练脚本通常会用 train_test_split 划分数据集,装齐更稳妥。
pip install opencv-python numpy matplotlib scikit-learnopencv-python 自带 Haar 级联模型文件,numpy 提供矩阵运算,sklearn 用于划分数据集和生成分类报告。如果本身在 conda 环境里运行,用 conda install -c conda-forge opencv 也是常见方式,两者不会冲突,但不要同时混装多个渠道的 opencv,避免出现 libopencv 版本覆盖问题。
摄像头索引的判断建议写个小循环,依次尝试 0、1、2,打印每个索引是否能拿到帧。
for idx in range(3): cap = cv2.VideoCapture(idx) ret, frame = cap.read() print(idx, ret, frame.shape if ret else 'no frame') cap.release()逻辑说明:笔记本内置摄像头一般是 0,外接 USB 摄像头经常变成 1 或 2,这个循环能把可用索引一次性试出来,比反复改参数再运行整个视频脚本高效。
4.2 训练收敛的判断与早停策略
训练时只看最终准确率是不够的,要同时盯 loss 曲线和验证集准确率。正常情况是 loss 单调下降后趋于平稳,验证集准确率小幅波动;如果 loss 震荡,先把 lr 从 0.01 降到 0.001;如果 accuracy 停在 50% 附近,先检查标签是否打反,再看归一化是否遗漏。性别数据一张张标注很容易出现目录名和标签不一致的情况。
早停是课程设计里容易被忽略但很加分的小策略:
best_acc = 0.0 patience = 30 wait = 0 for epoch in range(300): train_one_epoch() # 实际代码里是分批训练循环 acc = evaluate_val() if acc > best_acc: best_acc = acc wait = 0 save_weights() else: wait += 1 if wait >= patience: print(f'early stop at epoch {epoch}, best_acc={best_acc:.4f}') break逻辑说明:连续 30 个 epoch 验证集准确率没有刷新,就停止训练并保留历史最优权重。这样既防止过拟合,也节省调参时的重复等待时间。
参数说明:patience 视数据集规模调整,几百张样本时 20~30 足够,如果数据量到了几千张可以放宽到 50;save_weights 只保存 best_acc 对应的那版权重,不要在所有 epoch 结束后再覆盖。
4.3 跨平台运行时的常见差异
这份项目在 Windows、macOS、Linux 上表现不同,主要有三个注意点。第一,cv2.imshow 在纯 SSH 服务器上无法弹出窗口,无桌面系统下要把 imshow 换成 imwrite,把检测结果写回文件再查看。第二,macOS 上摄像头权限需要在系统设置里单独授权终端应用,否则 VideoCapture 返回的 ret 一直是 False。第三,OpenCV 4.x 里 cv2.data.haarcascades 路径是固定的,但如果读者换用了 OpenCV 3.x,这个常量不存在,要手动指定 xml 文件的绝对路径。
Linux 服务器上最常见的错误是缺少 GUI 依赖库,运行 demo 时报 cv2.error 且提示 libGL.so.1 找不到。解决办法是安装 libgl1-mesa-glx,比如 Debian 系发行版执行 apt-get install -y libgl1-mesa-glx,不需要重新编译 OpenCV。
5. 答辩验收的三个加分细节:混淆矩阵、演示阈值与预录视频
5.1 用混淆矩阵替代单一准确率
答辩时只报一个“准确率 91%”很难展开提问。用 sklearn 生成混淆矩阵,把错判集中在哪里直接可视化,老师能顺着具体样例继续追问,而你有充分准备。生成方式很简单:在测试集预测结束后调用 metrics.confusion_matrix,然后用 matplotlib 画成热力图,横纵轴分别标注 male/female,对角线上数字越大代表分类越可靠。
报告里配合矩阵写一句“女性被误判为男性的比例明显高于反向误判”,紧接着解释原因:训练集中女性样本偏少,或者女性测试样本里存在刘海遮挡和侧脸。这种分析比单纯贴一个 accuracy 数值有信息量得多,也容易撑起一个答辩小节。
5.2 现场演示前固定阈值与曝光
现场答辩的灯光往往不是你能控制的,摄像头白平衡会在不同角度自动漂移,性别预测结果可能在同一张脸上跳来跳去。演示前把 detect_and_predict 的 threshold 参数固定为 0.6,让预测只在置信度足够时切换标签,能有效减少抖动。另外在现场演示脚本里关闭自动曝光,固定为手动曝光和固定白平衡,OpenCV 里通过 cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) 可以调整,但不同摄像头的支持程度不同,最好在答辩前一晚实拍三段不同方向的光线做验证。
5.3 30 秒预录 demo 的录制参数
无论现场演示多顺利,都建议预录一段 30 秒的视频作为兜底。录制时覆盖三种场景:单人正脸、多人同框、戴眼镜暗光,每段 10 秒,并在画面角落叠加当前 FPS。帧率是老师最常追问的指标,叠加后无需口头解释。
| 场景 | 分辨率 | 目标 FPS | 关注点 |
|---|---|---|---|
| 单人正脸 | 960x720 | 25 | 性别标签稳定 |
| 多人同框 | 960x720 | 20 | 多人框同时出现 |
| 戴眼镜暗光 | 640x480 | 15 | Haar 是否漏检 |
录屏时用 OBS 或系统自带录屏都行,关键是把演示窗口和 FPS 计数器一起录进去。视频文件答辩前放在与源码同目录的 demo 文件夹里,万一现场摄像头驱动冲突,直接播放这段素材也能完整展示整个检测链路的工作状态,这是整个项目档次的直接证明。
本文还有配套的精品资源,点击获取