ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python期末大作业:CNN人脸表情识别系统从源码到跑通全指南

Python期末大作业:CNN人脸表情识别系统从源码到跑通全指南 简介这份资源是面向计算机相关专业学生与项目实战学习者的Python期末大作业完整源码主题为基于CNN的人脸表情识别系统适合正在准备课程设计、需要中等难度实战案例的人群参考与二次开发。压缩包共23个文件约19.17MB以10个py脚本为核心涵盖数据读取与写入、模型定义、训练与识别等模块另含png、jpeg图像素材、tfrecord数据文件、Keras训练脚本、xml与html页面、md说明文档等结构清晰便于按模块理解项目全貌。目前已有144人学习下载说明该案例在同类作业中具有一定参考价值。项目经导师指导并获评审98分源码均经本地编译与严格调试可正常运行读者可据此掌握CNN表情识别的数据处理、模型搭建、训练调参及摄像头与图片识别等关键环节也能借鉴其目录组织与排错思路降低大作业落地难度。1. 从一份 Python 期末大作业说起CNN 人脸表情识别到底能跑出什么效果如果你正在搜「Python期末大作业基于CNN的人脸表情识别系统源码.zip」大概率是三种人之一赶期末作业的学生、想拿一个能跑通的深度学习项目练手的入门者、或者需要给课程设计找一个有演示效果的选题。这个标题背后其实是一套完整的工程链路用 Python 做开发语言用 CNN卷积神经网络做核心模型输入是人脸图像输出是高兴、悲伤、愤怒、惊讶、厌恶、恐惧、中性这七类表情之一。它不是一个玩具 demo而是一个能覆盖数据预处理、模型搭建、训练调参、推理部署的完整闭环。我见过太多人拿到这类源码后卡在第一步环境装不上、数据集路径对不上、训练几轮 loss 不降。所以这篇笔记不打算复述教科书里的卷积原理而是按「这个系统由什么组成 → 怎么在本地跑通 → 参数怎么调 → 哪里最容易翻车」的顺序把一份 CNN 人脸表情识别系统从源码到可演示结果的全过程拆开讲。适合有 Python 基础、学过一点深度学习概念、但还没独立跑通过一个完整视觉项目的人。读完你应该能自己复现训练、看懂每个模块在干什么、并且知道改哪里能让准确率往上走一截。2. 拆开这份源码CNN 人脸表情识别系统的四个核心模块一份能交作业、能演示的人脸表情识别系统不管源码怎么组织底层都逃不出四块数据层、模型层、训练层、推理层。很多人一上来就盯着模型结构看结果数据没处理好再深的网络也白搭。这一章先把每块讲清楚再给出可执行的代码骨架。2.1 数据层FER2013 这类数据集怎么读、怎么分、怎么增强人脸表情识别最常用的公开数据集是 FER2013它包含约 3.5 万张 48×48 的灰度人脸图标注为 7 类表情。原始文件通常是一个 CSV每行三个字段emotion 标签、pixels 像素串、usage 用途Training/PublicTest/PrivateTest。很多人拿到 CSV 直接pd.read_csv然后手动 reshape这里第一个坑就是像素串是空格分隔的字符串不是数组。import numpy as np import pandas as pd def load_fer2013(csv_path): df pd.read_csv(csv_path) # pixels 列是 12 34 56 ... 这样的字符串需要拆成 2304 个像素 pixels df[pixels].apply(lambda x: np.array(x.split(), dtypefloat32)) # 归一化到 0-1CNN 对输入尺度敏感 X np.stack(pixels.values) / 255.0 X X.reshape(-1, 48, 48, 1) # 灰度图只有 1 个通道 y pd.get_dummies(df[emotion]).values # one-hot 编码 return X, y这段代码的关键点有三个。第一x.split()默认按空白字符切分能同时处理空格和制表符比split( )稳。第二除以 255 是必须的不归一化的话梯度会炸loss 直接变 nan。第三reshape 成(-1, 48, 48, 1)而不是(48, 48)因为 Keras/TensorFlow 的卷积层要求输入带通道维度灰度图通道数是 1彩色图才是 3。数据划分上FER2013 自带的 usage 字段可以直接用但更稳妥的做法是自己用train_test_split按 8:1:1 重新分避免某些类别在测试集里分布不均。数据增强是提分的关键手段水平翻转、±10 度旋转、±10% 平移这三样对表情识别最有效因为人脸左右对称、轻微角度变化不应该改变表情类别。from tensorflow.keras.preprocessing.image import ImageDataGenerator datagen ImageDataGenerator( rotation_range10, # 旋转范围超过 15 度会引入不真实样本 width_shift_range0.1, # 水平平移比例 height_shift_range0.1, # 垂直平移比例 horizontal_flipTrue, # 水平翻转表情识别必开 fill_modenearest # 填充方式边缘用最近像素补 )参数说明rotation_range不要超过 15否则眉毛和嘴角的相对位置会失真horizontal_flip对「厌恶」和「恐惧」这两类要谨慎因为这两类表情有时依赖左右不对称特征但整体上开比不开好fill_mode选nearest比constant更自然不会在边缘引入黑边。2.2 模型层一个能打 baseline 的 CNN 该长什么样网上很多源码一上来就堆 VGG16 或 ResNet对期末作业来说没必要参数量大、训练慢、还容易过拟合。一个 4 层卷积 2 层全连接的轻量 CNN在 FER2013 上就能做到 60% 左右的验证准确率足够演示。结构设计上遵循「卷积 → 池化 → 卷积 → 池化 → 展平 → 全连接 → Dropout → 输出」的经典范式。from tensorflow.keras import layers, models def build_cnn(input_shape(48, 48, 1), num_classes7): model models.Sequential([ # 第一组32 个 3x3 卷积核提取边缘和纹理 layers.Conv2D(32, (3, 3), activationrelu, paddingsame, input_shapeinput_shape), layers.BatchNormalization(), # 加速收敛防止梯度消失 layers.Conv2D(32, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), # 丢弃 25% 神经元抑制过拟合 # 第二组64 个卷积核提取更复杂的局部特征 layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.BatchNormalization(), layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), # 第三组128 个卷积核捕捉表情相关的全局模式 layers.Conv2D(128, (3, 3), activationrelu, paddingsame), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), layers.Flatten(), layers.Dense(256, activationrelu), layers.BatchNormalization(), layers.Dropout(0.5), # 全连接层丢弃比例更高 layers.Dense(num_classes, activationsoftmax) # 7 类概率输出 ]) return model逐层解释paddingsame保证卷积后尺寸不变方便堆叠BatchNormalization放在卷积和激活之间还是之后有争议我一般放在激活之后、池化之前实测收敛更稳Dropout在卷积部分用 0.25全连接部分用 0.5这是经验值卷积层特征图本身有冗余丢太多会欠拟合。输出层用softmax是因为七类互斥如果改成多标签才用sigmoid。编译时的损失函数和优化器选择也有讲究。多分类用categorical_crossentropy如果标签是整数而非 one-hot 就用sparse_categorical_crossentropy。优化器首选Adam学习率从 1e-3 开始配合ReduceLROnPlateau在验证 loss 不降时自动减半。from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping model build_cnn() model.compile( optimizerAdam(learning_rate1e-3), losscategorical_crossentropy, metrics[accuracy] ) callbacks [ ReduceLROnPlateau(monitorval_loss, factor0.5, patience5, min_lr1e-6), EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) ]patience5表示验证 loss 连续 5 轮不降就减学习率EarlyStopping的patience10表示连续 10 轮不降就停restore_best_weightsTrue保证拿回验证集上最好的那组权重相当于一颗后悔药。2.3 训练层batch size、epoch 和验证集怎么配训练脚本的核心就一行model.fit但参数配错会让整个训练白跑。batch size 在 64 到 128 之间比较合适太小梯度震荡大太大显存吃紧且泛化变差。epoch 设 50 到 80配合 EarlyStopping 实际可能 30 轮左右就停。验证集比例 10% 到 15%不要低于 10%否则验证指标波动大看不出真实趋势。history model.fit( datagen.flow(X_train, y_train, batch_size64), steps_per_epochlen(X_train) // 64, epochs80, validation_data(X_val, y_val), callbackscallbacks, verbose1 )steps_per_epoch必须显式指定否则生成器会无限循环。validation_data用原始未增强的验证集这样评估的是模型真实泛化能力而不是增强后的分布。训练过程中重点看val_loss和val_accuracy两条曲线如果训练准确率一直涨但验证准确率停滞甚至下降说明过拟合该加 Dropout 或减模型容量。2.4 推理层用 OpenCV 做实时人脸检测 表情分类训练完保存模型只是半成品能演示的系统必须能对摄像头或图片实时输出表情。流程是OpenCV 读帧 → Haar 级联或 DNN 人脸检测器定位人脸 → 裁剪缩放成 48×48 灰度图 → 送入 CNN 预测 → 在画面上标注类别和置信度。import cv2 import numpy as np from tensorflow.keras.models import load_model model load_model(emotion_cnn.h5) face_cascade cv2.CascadeClassifier(cv2.data.haarcascades haarcascade_frontalface_default.xml) emotions [Angry, Disgust, Fear, Happy, Sad, Surprise, Neutral] cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, scaleFactor1.1, minNeighbors5, minSize(48, 48)) for (x, y, w, h) in faces: roi gray[y:yh, x:xw] roi cv2.resize(roi, (48, 48)) / 255.0 roi roi.reshape(1, 48, 48, 1) pred model.predict(roi, verbose0)[0] label emotions[np.argmax(pred)] conf np.max(pred) cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.putText(frame, f{label} {conf:.2f}, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(Emotion Recognition, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()scaleFactor1.1控制每次图像缩小的比例越小检测越慢但越全minNeighbors5是检测框保留阈值调高会减少误检但可能漏检minSize(48, 48)避免对太小的区域做无意义预测。推理时verbose0关掉每帧的进度条输出否则控制台会刷屏。3. 从零跑通环境配置、训练命令与结果验证的完整步骤上一章拆了模块这一章把零件装成一台能跑的车。很多人卡在环境上不是缺包就是版本冲突所以先把环境说清楚再给训练和验证的完整命令。3.1 环境配置Python 版本、TensorFlow 和 OpenCV 的版本匹配TensorFlow 对 Python 版本很挑。截至我写这篇笔记时的稳定组合是 Python 3.8 到 3.10TensorFlow 2.10 到 2.13OpenCV 4.5 以上。Python 3.11 以上装 TensorFlow 2.10 会报找不到对应 wheel这是血泪经验。建议用 conda 建独立环境不要往系统 Python 里装。conda create -n emotion python3.9 -y conda activate emotion pip install tensorflow2.12.0 opencv-python4.8.0.74 numpy1.23.5 pandas1.5.3 scikit-learn1.2.2 matplotlib3.7.1版本锁定不是迷信是避免numpy2.x 和 TensorFlow 2.12 不兼容这类玄学问题。opencv-python装 headless 版本也行但要做实时显示就必须用完整版。装完用下面三行验证python -c import tensorflow as tf; print(tf.__version__) python -c import cv2; print(cv2.__version__) python -c import numpy as np; print(np.__version__)三条都打印出版本号且不报错环境就算通了。如果import tensorflow报DLL load failed多半是 Visual C Redistributable 没装去微软官网下最新的 x64 版本装上即可。3.2 数据准备FER2013 的下载、解压与目录组织FER2013 的原始 CSV 可以从 Kaggle 上搜到下载后是一个约 60MB 的fer2013.csv。不要手动去分训练测试文件夹代码里用 pandas 读进来按 usage 字段切分最省事。目录结构建议这样组织project/ ├── data/ │ └── fer2013.csv ├── models/ │ └── emotion_cnn.h5 # 训练后保存 ├── train.py ├── predict.py └── requirements.txttrain.py里把上一章的加载、建模、训练串起来保存模型用model.save(models/emotion_cnn.h5)。注意.h5格式在 TensorFlow 2.12 里仍然支持但官方推荐.keras格式两者都能用.h5兼容性更好很多老源码都读这个格式。3.3 训练与验证一条命令跑完看哪几个指标训练命令就是python train.py但输出里要盯住四个数loss、accuracy、val_loss、val_accuracy。正常情况下前 5 轮 loss 从 1.9 左右快速降到 1.2 以下accuracy 从 20% 涨到 50% 以上。如果 5 轮后 loss 还在 1.8 附近晃检查三件事数据有没有归一化、标签有没有 one-hot、学习率是不是太大。训练结束后用混淆矩阵看每一类的表现这比只看总准确率有用得多。FER2013 上「高兴」和「惊讶」通常识别最好「恐惧」和「厌恶」最差因为这两类样本少且特征模糊。from sklearn.metrics import classification_report, confusion_matrix y_pred model.predict(X_test) y_pred_classes np.argmax(y_pred, axis1) y_true np.argmax(y_test, axis1) print(classification_report(y_true, y_pred_classes, target_namesemotions)) print(confusion_matrix(y_true, y_pred_classes))classification_report会给出每一类的 precision、recall、f1-score。如果「厌恶」的 recall 低于 0.3说明这一类基本没学到可以考虑在数据增强里对少数类做过采样或者用class_weight给少数类更高权重。3.4 推理验证拿一张自己的照片测比测试集更有说服力测试集准确率再高拿自己的照片一测就露馅这是常见现象。因为 FER2013 的图是 48×48 灰度、人脸居中、光照均匀而手机自拍往往角度偏、光照杂。验证时先用一张正脸、光照均匀的照片确认能出合理结果再逐步加难度。如果自己的照片总是预测成同一类多半是人脸检测框没对准或者输入没做灰度转换。img cv2.imread(test.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5, minSize(48, 48)) for (x, y, w, h) in faces: roi cv2.resize(gray[y:yh, x:xw], (48, 48)) / 255.0 roi roi.reshape(1, 48, 48, 1) pred model.predict(roi, verbose0)[0] print(emotions[np.argmax(pred)], np.max(pred))如果检测不到人脸先把minNeighbors降到 3或者换用 OpenCV 的 DNN 人脸检测器后者对侧脸和遮挡更鲁棒但需要额外下载模型文件。4. 避坑与排查CNN 表情识别最常翻车的五个地方这一章是我自己踩过和帮别人排查过的问题合集每条按「现象 → 原因 → 解决」写遇到对应症状直接对号入座。4.1 训练 loss 不降反升最后变成 nan现象训练开始几轮 loss 从 1.9 涨到 3.0 以上然后变成 nanaccuracy 停在 14% 左右七类随机猜的水平。原因九成是输入没归一化像素值在 0 到 255 之间卷积后激活值爆炸梯度溢出。少数情况是学习率设成了 0.1 这种大值。解决确认X X / 255.0这行执行了并且是在 reshape 之前还是之后不影响结果但必须在送入模型前。学习率从 1e-3 开始不要超过 1e-2。如果已经归一化还 nan检查标签是不是 one-hot用sparse_categorical_crossentropy时标签必须是整数。4.2 验证准确率卡在 25% 上不去现象训练准确率能到 50% 以上但验证准确率一直在 25% 左右两者差距越来越大。原因典型过拟合模型记住了训练集但没学到泛化特征。也可能是数据划分有问题训练集和验证集分布不一致。解决先加 Dropout卷积层 0.25、全连接层 0.5。再加数据增强水平翻转和轻微旋转。如果还不行减模型容量把第三组卷积去掉或者把 Dense(256) 降到 128。最后检查验证集是不是从训练集里随机分的不要用 FER2013 自带的 PublicTest那个分布和 Training 有差异。4.3 摄像头推理时画面卡顿帧率只有个位数现象predict.py跑起来后画面一顿一顿每帧要等一两秒才出结果。原因每帧都调用model.predict而 Keras 的 predict 有固定开销即使输入只有一张 48×48 的图。另外 Haar 级联检测本身也吃 CPU。解决把model.predict换成model(roi, trainingFalse)直接调用省掉 predict 的封装开销。或者每 3 帧检测一次人脸中间帧沿用上一次的检测框。如果还慢把 Haar 换成更轻的 LBP 级联或者缩小检测输入尺寸。4.4 保存的模型加载时报「Unknown layer」或「No model found」现象训练完model.save(emotion_cnn.h5)成功但load_model时报错说某个自定义层不认识。原因如果模型里用了自定义层或自定义损失函数加载时必须用custom_objects传进去。另外.h5和.keras格式混用也可能出问题。解决尽量用 Keras 内置层不要自己写 Lambda 层。如果非用不可保存时用model.save(model.keras)加载时load_model(model.keras)。路径问题也要注意load_model的相对路径是相对于运行脚本的目录不是脚本所在目录用绝对路径最稳。4.5 换自己的数据集后准确率暴跌到 20%现象用 FER2013 训练到 60%换成自己收集的人脸图后准确率只有 20% 左右。原因域偏移。自己的图可能分辨率不同、光照不同、人脸占比不同而模型只见过 48×48 居中灰度脸。解决推理前把人脸区域裁剪、缩放、灰度化尽量对齐 FER2013 的预处理流程。如果数据量够拿自己的数据微调几轮学习率设 1e-4只训练最后几层。数据量少的话至少做直方图均衡化让光照分布接近训练集。5. 让准确率再上一个台阶三个我实际用过的调优技巧跑通之后大部分人想让准确率从 60% 往 65% 甚至 70% 走。这一章给三个我实际验证过有效的技巧不堆理论只说怎么改、改完看什么。第一个技巧是标签平滑label smoothing。FER2013 里有些图标注本身就有歧义比如一张脸介于「中性」和「悲伤」之间硬标签会让模型过度自信。把CategoricalCrossentropy(label_smoothing0.1)用上验证准确率通常能涨 1 到 2 个百分点而且训练更稳。from tensorflow.keras.losses import CategoricalCrossentropy model.compile( optimizerAdam(1e-3), lossCategoricalCrossentropy(label_smoothing0.1), metrics[accuracy] )label_smoothing0.1表示把真实类别的概率从 1.0 降到 0.9剩下 0.1 均分给其他类。这个值不要超过 0.2否则模型学不到明确边界。第二个技巧是余弦退火学习率。固定学习率容易在后期震荡余弦退火让学习率按余弦曲线从 1e-3 平滑降到 1e-6配合 warmup 效果更好。Keras 里用CosineDecay回调实现。from tensorflow.keras.optimizers.schedules import CosineDecay lr_schedule CosineDecay(initial_learning_rate1e-3, decay_steps1000, alpha1e-6) model.compile(optimizerAdam(learning_ratelr_schedule), losscategorical_crossentropy, metrics[accuracy])decay_steps设成总步数alpha是最终学习率下限。这个改动对最终准确率提升不明显但能让训练曲线更平滑减少后期波动。第三个技巧是测试时增强TTA。推理时对同一张图做水平翻转两次预测取平均能小幅提升鲁棒性尤其对侧脸和光照不均的图有效。def predict_with_tta(model, face_img): pred1 model.predict(face_img, verbose0) pred2 model.predict(face_img[:, ::-1, :, :], verbose0) # 水平翻转 return (pred1 pred2) / 2face_img[:, ::-1, :, :]沿宽度轴翻转注意通道轴不要动。TTA 的代价是推理时间翻倍实时场景要权衡。最后一个习惯每次改完参数固定随机种子跑三次取平均不要只看一次结果。深度学习训练本身有随机性单次涨跌 1 个百分点说明不了问题。我一般会在train.py开头加tf.random.set_seed(42)和np.random.seed(42)保证可复现。这套流程走下来一份期末大作业级别的 CNN 人脸表情识别系统从源码到能演示、能讲清楚每个模块为什么这么设计基本就稳了。希望帮到你。本文还有配套的精品资源点击获取
返回列表