☰
Python期末大作业:CNN人脸表情识别系统从源码到跑通全指南
2026/9/28 5:05:54 网站建设 项目流程

简介:这份资源是面向计算机相关专业学生与项目实战学习者的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(), dtype='float32')) # 归一化到 0-1,CNN 对输入尺度敏感 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_range=10, # 旋转范围,超过 15 度会引入不真实样本 width_shift_range=0.1, # 水平平移比例 height_shift_range=0.1, # 垂直平移比例 horizontal_flip=True, # 水平翻转,表情识别必开 fill_mode='nearest' # 填充方式,边缘用最近像素补 )

参数说明: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_classes=7): model = models.Sequential([ # 第一组:32 个 3x3 卷积核,提取边缘和纹理 layers.Conv2D(32, (3, 3), activation='relu', padding='same', input_shape=input_shape), layers.BatchNormalization(), # 加速收敛,防止梯度消失 layers.Conv2D(32, (3, 3), activation='relu', padding='same'), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), # 丢弃 25% 神经元,抑制过拟合 # 第二组:64 个卷积核,提取更复杂的局部特征 layers.Conv2D(64, (3, 3), activation='relu', padding='same'), layers.BatchNormalization(), layers.Conv2D(64, (3, 3), activation='relu', padding='same'), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), # 第三组:128 个卷积核,捕捉表情相关的全局模式 layers.Conv2D(128, (3, 3), activation='relu', padding='same'), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), layers.Flatten(), layers.Dense(256, activation='relu'), layers.BatchNormalization(), layers.Dropout(0.5), # 全连接层丢弃比例更高 layers.Dense(num_classes, activation='softmax') # 7 类概率输出 ]) return model

逐层解释:padding='same'保证卷积后尺寸不变,方便堆叠;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( optimizer=Adam(learning_rate=1e-3), loss='categorical_crossentropy', metrics=['accuracy'] ) callbacks = [ ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5, min_lr=1e-6), EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True) ]

patience=5表示验证 loss 连续 5 轮不降就减学习率,EarlyStopping的patience=10表示连续 10 轮不降就停,restore_best_weights=True保证拿回验证集上最好的那组权重,相当于一颗后悔药。

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_size=64), steps_per_epoch=len(X_train) // 64, epochs=80, validation_data=(X_val, y_val), callbacks=callbacks, verbose=1 )

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, scaleFactor=1.1, minNeighbors=5, minSize=(48, 48)) for (x, y, w, h) in faces: roi = gray[y:y+h, x:x+w] roi = cv2.resize(roi, (48, 48)) / 255.0 roi = roi.reshape(1, 48, 48, 1) pred = model.predict(roi, verbose=0)[0] label = emotions[np.argmax(pred)] conf = np.max(pred) cv2.rectangle(frame, (x, y), (x+w, y+h), (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()

scaleFactor=1.1控制每次图像缩小的比例,越小检测越慢但越全;minNeighbors=5是检测框保留阈值,调高会减少误检但可能漏检;minSize=(48, 48)避免对太小的区域做无意义预测。推理时verbose=0关掉每帧的进度条输出,否则控制台会刷屏。

3. 从零跑通:环境配置、训练命令与结果验证的完整步骤

上一章拆了模块,这一章把零件装成一台能跑的车。很多人卡在环境上,不是缺包就是版本冲突,所以先把环境说清楚,再给训练和验证的完整命令。

3.1 环境配置:Python 版本、TensorFlow 和 OpenCV 的版本匹配

TensorFlow 对 Python 版本很挑。截至我写这篇笔记时的稳定组合是 Python 3.8 到 3.10,TensorFlow 2.10 到 2.13,OpenCV 4.5 以上。Python 3.11 以上装 TensorFlow 2.10 会报找不到对应 wheel,这是血泪经验。建议用 conda 建独立环境,不要往系统 Python 里装。

conda create -n emotion python=3.9 -y conda activate emotion pip install tensorflow==2.12.0 opencv-python==4.8.0.74 numpy==1.23.5 pandas==1.5.3 scikit-learn==1.2.2 matplotlib==3.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.txt

train.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, axis=1) y_true = np.argmax(y_test, axis=1) print(classification_report(y_true, y_pred_classes, target_names=emotions)) 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:y+h, x:x+w], (48, 48)) / 255.0 roi = roi.reshape(1, 48, 48, 1) pred = model.predict(roi, verbose=0)[0] print(emotions[np.argmax(pred)], np.max(pred))

如果检测不到人脸,先把minNeighbors降到 3,或者换用 OpenCV 的 DNN 人脸检测器,后者对侧脸和遮挡更鲁棒,但需要额外下载模型文件。

4. 避坑与排查:CNN 表情识别最常翻车的五个地方

这一章是我自己踩过和帮别人排查过的问题合集,每条按「现象 → 原因 → 解决」写,遇到对应症状直接对号入座。

4.1 训练 loss 不降反升,最后变成 nan

现象:训练开始几轮 loss 从 1.9 涨到 3.0 以上,然后变成 nan,accuracy 停在 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, training=False)直接调用,省掉 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_smoothing=0.1)用上,验证准确率通常能涨 1 到 2 个百分点,而且训练更稳。

from tensorflow.keras.losses import CategoricalCrossentropy model.compile( optimizer=Adam(1e-3), loss=CategoricalCrossentropy(label_smoothing=0.1), metrics=['accuracy'] )

label_smoothing=0.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_rate=1e-3, decay_steps=1000, alpha=1e-6) model.compile(optimizer=Adam(learning_rate=lr_schedule), loss='categorical_crossentropy', metrics=['accuracy'])

decay_steps设成总步数,alpha是最终学习率下限。这个改动对最终准确率提升不明显,但能让训练曲线更平滑,减少后期波动。

第三个技巧是测试时增强(TTA)。推理时对同一张图做水平翻转,两次预测取平均,能小幅提升鲁棒性,尤其对侧脸和光照不均的图有效。

def predict_with_tta(model, face_img): pred1 = model.predict(face_img, verbose=0) pred2 = model.predict(face_img[:, ::-1, :, :], verbose=0) # 水平翻转 return (pred1 + pred2) / 2

face_img[:, ::-1, :, :]沿宽度轴翻转,注意通道轴不要动。TTA 的代价是推理时间翻倍,实时场景要权衡。

最后一个习惯:每次改完参数,固定随机种子跑三次取平均,不要只看一次结果。深度学习训练本身有随机性,单次涨跌 1 个百分点说明不了问题。我一般会在train.py开头加tf.random.set_seed(42)和np.random.seed(42),保证可复现。这套流程走下来,一份期末大作业级别的 CNN 人脸表情识别系统,从源码到能演示、能讲清楚每个模块为什么这么设计,基本就稳了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询