基于CNN的人脸表情识别系统:从FER2013到实时识别完整实战
2026/9/24 23:47:10 网站建设 项目流程

简介:一套基于卷积神经网络(CNN)的人脸表情识别Python期末大作业源码,面向计算机专业学生、课程设计者及深度学习入门者,解决人脸表情识别从数据处理、模型训练到实际识别的完整工程参考问题。这套源码曾获导师认可,评审得分98分,并在本地编译调试通过,具备可运行性。压缩包共23个文件,以10个Python脚本为核心,覆盖数据读取、模型构建、训练、图片识别和摄像头实时识别等模块,另含TensorFlow模型与数据文件、示例图片、可视化网页展示和说明文档,整体大小约19兆字节。已有51人学习下载。通过该项目,学习者可掌握卷积神经网络原理、人脸特征提取与表情分类的代码实现,同时了解数据预处理、模型调参与结果展示的完整流程;利用示例图片可快速验证效果,结合摄像头模块还能体验实时识别,适合将其改造为课程设计或期末项目。

1. 基于 CNN 的人脸表情识别是什么作业:先认清它的四个组件

一份“基于 CNN 的人脸表情识别系统的期末大作业源码”,说白了就是用 Python 把深度学习里最经典的一条图像分类链路完整跑通:从摄像头或图片里框出人脸,把这块人脸区域交给卷积神经网络,让它输出“生气、厌恶、恐惧、开心、中性、悲伤、惊讶”这七类表情中的一种。难的不是模型本身,而是四个组件缺一不可:人脸检测、人脸预处理、CNN 分类、结果可视化,任何一个环节掉链子,现场演示都会翻车。这套作业适合两类人:正在做课程设计或毕业设计、需要一份能跑能讲能写进报告的项目的人;想快速验证 CNN 完整图像分类流程的开发者——它比猫狗分类多了一道“先找脸再分类”的步骤,练完你对怎么组织图像项目会有非常具体的体感。先把一句话放在这:模型不是越复杂越好。

2. 方案选型:为什么期末大作业的稳妥组合是 Haar 检测 + 自搭 CNN

2.1 系统的完整链路:从摄像头画面到七种表情标签

答辩时老师问得最多的一句话是“你的系统入口在哪”。你要能脱口而出完整链路:用 cv2 读取摄像头或图片 → 转灰度 → 用 OpenCV 自带的 Haar 级联检测器框出人脸 → 把人脸区域裁剪、缩放到 48×48 → 像素归一化到 0 到 1 → 送入 CNN → softmax 得到七个类别的概率 → 取最大值的下标对应到表情名称,最后把检测框、标签和置信度画在画面上。

很多人第一版代码直接跳过人脸检测,对整帧画面做分类,结果验证准确率惨不忍睹。原因很直观:CNN 在 48×48 的输入上学到的是人脸局部纹理,你把背景、头发、衣领全塞进去,等于给分类器加噪声。所以检测这一步不是可选项,它决定了后面 CNN 的输入分布稳不稳定。为什么先转灰度?Haar 检测器本身工作在灰度图上,FER2013 数据集也全是灰度图,整条链路里彩色信息是多余的。转灰度不是损失,而是让预处理与数据集保持一致,减少训练和推理时的分布漂移。

2.2 人脸检测器选型:Haar、Dlib、MTCNN 怎么选

常见的候选有三个:OpenCV 自带的 Haar Cascade、Dlib 的 HOG + 68 点、MTCNN。对期末作业这个场景,我直接给一张对比表:

方案依赖与准备成本检测质量运行速度适合场景
Haar Cascadeopencv-python 自带,无需额外文件框会轻微抖动,但够用CPU 极快作业打底方案,首选
Dlib HOG + 68 点需要 dlib 包和权重文件框更稳,可做人脸对齐较慢报告里的“改进方向”,加分项
MTCNN需要 mtcnn、tensorflow 等一堆依赖准,带关键点追求效果但不考虑答辩复杂度

大部分期末作业选 Haar 打底就够了。它不需要在没网的环境里临时下载额外权重,opencv-python 安装包里自带 haarcascade_frontalface_default.xml,这对演示现场是巨大优势。环境准备也简单:先在 VSCode 里把 Python 解释器选到项目虚拟环境,然后 pip install opencv-python numpy pandas matplotlib tensorflow,如果只缺 cv2,单独敲一句 pip install opencv-python 很快就能装好。这里想提醒一句:环境配置时最好在项目目录下建虚拟环境,别把包装进系统全局,否则换到教室电脑上演示,一运行就报缺包,场面非常尴尬。

Dlib 作为加分项,价值在于“人脸对齐”:用 68 个关键点把眼睛拉平、人脸摆正再做裁剪。对齐后的输入能让 CNN 在侧脸角度下也稳定,但代价是项目多出十几行坐标计算代码和一份权重文件。如果报告篇幅够,我一般建议只写在“不足与改进”里,写一句“引入关键点对齐可缓解侧脸误判”,不用真做。

2.3 分类网络选型:自搭 CNN 还是迁移学习

分类器这条路上也有分歧:自搭小型 CNN、微调 VGG16/ResNet50、用 MobileNet 轻量网络。我的建议很明确:自搭一个几十万参数的小 CNN 作为主方案,迁移学习可以作为对比实验写进报告,但别拿来当主模型。原因有三:第一,作业标题写的是基于 CNN,你搭的每一层都要能讲出为什么这么设;第二,预训练权重动辄上百兆,微调几轮也要等很久,在普通笔记本上体验很差;第三,答辩时老师更可能问“你这网络和 VGG 比差在哪”,而不是“你用 VGG 达到多少准确率”。

自搭网络也不代表随便堆卷积。48×48 输入本身分辨率低,三层卷积堆完后特征图已经非常小,没必要再加深。参数量控制在百万以内、单 epoch 训练在十秒级别,才是期末作业该有的手感。另外不用纠结为什么不用 RNN:表情识别处理的是单张静态图,像素之间有局部空间关系,卷积天然匹配这种结构;RNN 擅长时序依赖,把单帧图像硬塞进去反而把简单问题复杂化了。

2.4 对“期末大作业源码”的态度:整包搬运的风险

最后泼一盆冷水。期末大作业里“源码”两个字最诱人,也最容易翻车。直接拿一份整包源码改个文件名就交,风险全在答辩环节:老师随口一句“你的 Conv2D 参数怎么定的”“为什么最后是 7 个神经元”,背不下来就尬在台上。更实际的问题是,整包源码往往带着原作者的目录结构、训练日志和绝对路径,你改不动,想加一张测试图都要小心翼翼。

我一般建议把别人的源码当“参考答案”,自己动手把三个模块写出来:数据读取、模型搭建、实时推理。代码量加起来不到两百行,却是你答辩时敢跟老师对视的底气。很多同学问源码里为什么有那么多多余文件,其实那是作者训练时留下的缓存和日志;自己重建项目时,只要维护好数据、模型、主脚本三个模块就够了。

3. FER2013 数据集准备:CSV 解析、目录落盘与数据增强

3.1 为什么选 FER2013 而不是自己爬图

做表情识别绕不开数据集。常见做法是直接用 FER2013——一个被大量论文引用的标准人脸表情数据集:48×48 灰度图,七类标签,带官方划分好的训练、验证、测试集。选它的理由很实际:样本量对课程作业刚合适,约三万五千张,整个文件就是一个 CSV,不用多人协作下载;标签已经分好,不用写爬虫和清洗脚本;网上可参考的结果多,你做到什么水平心里有数。

自己爬图听上去更“原创”,但坑全在数据侧:抓下来的图有漫画、有侧脸、有人脸占比过小的远景,标注质量没法保证。期末作业时间有限,把精力花在清洗图片上,不如站在一个标准数据集上把完整流程吃透。报告里一句“选用学术界广泛使用的 FER2013,便于与已有工作对比”就交代得很体面。

拿到 CSV 后第一件事是确认列结构。标准 FER2013 有三个字段:emotion,0 到 6 的整数标签;pixels,一串用空格隔开的 0~255 灰度值;Usage,标记每行数据属于 Training、PublicTest 还是 PrivateTest。Usage 是官方划分:Training 当训练集,PublicTest 当验证集,PrivateTest 当测试集。

3.2 解析 CSV 并画出第一批样本

代码实现的第一步,是把 CSV 转成能看的图片。

import pandas as pd import numpy as np import matplotlib.pyplot as plt df = pd.read_csv("fer2013.csv") # 标签顺序全局固定,后面所有地方都用这一份 emotion_names = ["anger", "disgust", "fear", "happiness", "neutral", "sadness", "surprise"] def pixels_to_image(row: pd.Series) -> np.ndarray: pixels = np.array(row["pixels"].split(), dtype=np.float32) return pixels.reshape(48, 48) # 每个类别挑一张图拼成画布,先人工确认数据没坏 fig, axes = plt.subplots(2, 4, figsize=(10, 5)) for i, ax in enumerate(axes.flat): sample = df[df["emotion"] == i % 7].iloc[i // 7] ax.imshow(pixels_to_image(sample), cmap="gray") ax.set_title(emotion_names[sample["emotion"]]) ax.axis("off") plt.show() print(df["Usage"].value_counts())

这段代码里有两个细节值得说。pixels 字段是字符串,必须先按空格 split 再转 float32;转成 float32 而不是 int,是因为后面归一化要除以 255,如果用整型相除,结果会全部变成 0,整个数据集等于废了。reshape 的顺序是从左到右、从上到下,顺序反了画面会变成斜条纹,这也是最容易识别的数据损坏特征。

画图前先用 value_counts 看一眼三个子集的样本分布。如果 Training、PublicTest、PrivateTest 的比例大约在 8:1:1,说明文件结构正常。如果你拿到的版本只有两列、没有 Usage,就要自己按比例切分,这时务必先用固定随机种子,保证每次运行切分结果一致,否则模型结果复现不了,报告里写的数字就对不上。

3.3 按目录结构落盘:让 Keras 的 ImageDataGenerator 直接读取

图像分类项目最常见的组织方式,是按类别文件夹分层存放:train 目录下建 0 到 6 七个文件夹,每个文件夹放对应类别的 jpg。Keras 的 ImageDataGenerator.flow_from_directory 天然认识这种结构,训练代码可以省掉一大堆自己写数据集的样板代码。

import os import cv2 SPLITS = { "train": "Training", "val": "PublicTest", "test": "PrivateTest", } BASE = "fer2013_images" for split_name, usage in SPLITS.items(): sub = df[df["Usage"] == usage] for emotion_id in range(7): out_dir = os.path.join(BASE, split_name, str(emotion_id)) os.makedirs(out_dir, exist_ok=True) for idx, row in sub.iterrows(): img = pixels_to_image(row) # FER2013 是单通道灰度图,统一转成三通道 jpg 再存 img_rgb = cv2.cvtColor(img.astype(np.uint8), cv2.COLOR_GRAY2BGR) out_path = os.path.join( BASE, split_name, str(row["emotion"]), f"{row.name:05d}.jpg" ) ok = cv2.imwrite(out_path, img_rgb) if not ok: print("写入失败:", out_path) break

这里“统一存成三通道”是故意的。FER2013 原始像素是单通道,但很多现成模型代码默认输入是三通道 BGR。与其在训练代码里写“单通道就重复三次”这种分支,不如在数据准备阶段一次性统一成三通道,训练和推理代码都少一个判断。代价是磁盘上多两倍冗余,对一两万张小图无关痛痒。

写入失败的那行检查不是摆设。cv2.imwrite 在遇到中文路径或目标目录不存在时会静默返回 False,而不是抛异常;等训练时报找不到文件再排查,就多绕了一大圈。在这里打印出来,是成本最低的及时反馈。

3.4 数据增强:让期末小模型多学几个变化

FER2013 的训练集不到三万张,对一个百万参数级别的 CNN 来说属于“偏少且容易过拟合”的范畴。数据增强是解决这个问题最直接的手段,Keras 里几行配置就能搞定:

from tensorflow.keras.preprocessing.image import ImageDataGenerator train_gen = ImageDataGenerator( rescale=1.0 / 255.0, rotation_range=10, width_shift_range=0.1, height_shift_range=0.1, zoom_range=0.1, horizontal_flip=True, ) valid_gen = ImageDataGenerator(rescale=1.0 / 255.0)

参数为什么这么取,要能说出理由。rotation_range=10 表示随机旋转正负 10 度,作业里的合理范围是 5 到 15 度——转多了人脸姿态就失真,CNN 学到的会变成“旋转不变性”而不是“表情不变性”。width_shift_range 和 height_shift_range 控制平移比例,0.1 表示平移幅度不超过原图宽高的 10%,这恰好模拟了 Haar 检测框轻微抖动的效果。zoom_range=0.1 模拟远近变化。horizontal_flip=True 对人脸是安全的——表情不因左右翻转改变语义,这是最划算的一项增强。

我建议不要把 brightness_range 也打开。FER2013 本身是实验室条件下的人脸灰度图,亮度分布相对统一;在灰度图上强行叠加亮度扰动,等于给网络引入跟任务无关的噪声,训练损失可能降得更慢,而且答辩时这个选择不好圆。增强参数多不等于效果好,每加一项要能说清“它模拟了真实场景里的什么变化”。

3.5 验证集和测试集要不要做增强

这个问题答辩时几乎必问。答案是不做,只做 rescale。验证集的作用是估计模型在没见过的数据上的表现,如果对验证集也做平移翻转,验证损失反映的就不是真实泛化能力,而是模型对增强数据的适应能力。测试集更严格,它模拟最终部署环境,任何统计意义上的扩充都会让测试分数虚高。所以 valid_gen 只有 rescale,flow_from_directory 的 shuffle 在验证集上也要设成 False,保证每次评估的都是同一批图片,结果才有可比性。

4. 搭 CNN 与训练调参:模型结构、关键超参与评估指标

4.1 一个参数少、能跑进 65% 以上的卷积神经网络结构

很多人第一次搭卷积神经网络,喜欢照搬 VGG16 的思路:连续四五个卷积、每层 512 个滤波器。在 48×48 的输入上这么干,参数量直接上千万,训练一个 epoch 要等好几分钟,最终效果还不一定好——因为数据量撑不起这么大的模型容量。我给的参考结构是三层卷积的轻量网络:

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import ( Conv2D, BatchNormalization, MaxPooling2D, Dropout, GlobalAveragePooling2D, Dense ) def build_model(input_shape=(48, 48, 3), num_classes=7): model = Sequential([ Conv2D(32, (3, 3), padding="same", activation="relu", input_shape=input_shape), BatchNormalization(), Conv2D(64, (3, 3), padding="same", activation="relu"), BatchNormalization(), MaxPooling2D((2, 2)), Dropout(0.25), Conv2D(128, (3, 3), padding="same", activation="relu"), BatchNormalization(), MaxPooling2D((2, 2)), Dropout(0.25), GlobalAveragePooling2D(), Dense(128, activation="relu"), Dropout(0.5), Dense(num_classes, activation="softmax"), ]) return model model = build_model() model.summary()

结构不新奇,但每一层都能讲出理由。两个卷积再接池化,是为了先扩大感受野再压缩尺寸:第一个卷积提取边缘、纹理这类低级特征,第二个卷积在低级特征上组合出眼睛、嘴巴这类局部模式,池化把 48×48 变成 24×24,既降低计算量又提供平移鲁棒性。每个卷积后接 BatchNormalization,能让中间特征分布更稳定、收敛更快,这在数据量偏小的作业里能明显减少调学习率的痛苦。

输出层之前用 GlobalAveragePooling2D 而不是 Flatten,这是一个答辩加分点:Flatten 会把最后一层特征图的每个位置都展开成全连接输入,参数多、容易过拟合;全局平均池化是对每张特征图求均值,直接把三维特征图压成一维,参数骤减,而且强迫网络在整张特征图上做判断,对小目标的位移不那么敏感。老师问“为什么不加全连接层”,答案就是:用全局平均池化替代第一层全连接,是保留全局信息同时控制参数量的常用做法。

4.2 训练参数:optimizer、batch size、epoch 与回调函数

模型搭好后,训练配置决定你能否在午睡时间内把结果调出来。

from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import ( ModelCheckpoint, ReduceLROnPlateau, EarlyStopping ) model.compile( optimizer=Adam(learning_rate=1e-3), loss="categorical_crossentropy", metrics=["accuracy"], ) callbacks = [ ModelCheckpoint("best_model.keras", monitor="val_accuracy", save_best_only=True), ReduceLROnPlateau(monitor="val_loss", factor=0.5, patience=3, min_lr=1e-5), EarlyStopping(monitor="val_loss", patience=5, restore_best_weights=True), ] history = model.fit( train_gen, validation_data=valid_gen, epochs=30, callbacks=callbacks, )

optimizer 选 Adam 而不是 SGD 的理由是鲁棒:学习率 1e-3 是 Adam 在多数图像小任务上的甜点,它带自适应学习率,几乎不需要手动安排学习率下降节点。loss 用 categorical_crossentropy,要求标签是 one-hot 编码,flow_from_directory 的 class_mode="categorical" 会处理好这一步,不用手写循环。如果你的 Keras 版本较新,模型保存格式写成 best_model.keras,老版本改成 best_model.h5 即可,逻辑不变。

回调函数做了三件重要的事。ModelCheckpoint 只在验证准确率变好时覆盖保存,保证最后拿到的模型是历史最优而不是最后一轮——训练后期验证指标经常回摆,没有这个回调,你保存的可能是一个退步的权重。ReduceLROnPlateau 在验证损失连续 3 个 epoch 不降时把学习率砍半,这是最省心的学习率策略。EarlyStopping 连续 5 轮没改进就停,避免晚上睡觉忘记盯训练、第二天早上发现已经过拟合到没法看。batch size 64 在 48×48 小图上是稳妥值,显存占用小,收敛也稳。epoch 设 30 只是初始值,实际以 EarlyStopping 触发为准。

4.3 训练曲线与预期:FER2013 上什么成绩算正常

训练完第一件事是画曲线,这也是报告里必须出现的一张图。

import matplotlib.pyplot as plt plt.figure(figsize=(12, 4)) plt.subplot(1, 2, 1) plt.plot(history.history["accuracy"], label="train_acc") plt.plot(history.history["val_accuracy"], label="val_acc") plt.title("Accuracy") plt.legend() plt.subplot(1, 2, 2) plt.plot(history.history["loss"], label="train_loss") plt.plot(history.history["val_loss"], label="val_loss") plt.title("Loss") plt.legend() plt.show()

看到曲线后要有一个合理预期。FER2013 这个数据集标注噪声不小——众包标注时很多图是侧脸、遮挡甚至标签本身存疑,人肉区分也只有 65% 左右的水平。在这种背景下,验证准确率 60% 出头算合格,65% 以上是能写进报告的较好结果,想再往 70% 以上推,需要更复杂的模型和更长的训练时间,投入产出比会骤降。期末大作业真正该参考的标尺不是论文,而是“比随机分类的 14% 高出一大截、训练过程稳定可复现”。

注意:报告里写准确率时,务必注明是训练集、验证集还是测试集上的值。三者之间正常的落点是训练集比验证集高几个百分点,验证集和测试集接近。如果训练集 95%、验证集 60%,那是典型过拟合,优先检查 Dropout 是否生效、增强是否太弱。

4.4 评估不只看准确率:混淆矩阵与逐类召回率

准确率会把很多问题盖住。期末验收时老师常问“哪类表情最容易错”,准确率答不了这个问题。用 sklearn 跑一份逐类报告和混淆矩阵:

from sklearn.metrics import classification_report, confusion_matrix import numpy as np # test_gen 对应 3.3 写好的 PrivateTest 目录 y_true = test_gen.classes y_pred = model.predict(test_gen).argmax(axis=1) print(classification_report(y_true, y_pred, target_names=emotion_names)) cm = confusion_matrix(y_true, y_pred) print(cm)

对照真实标签和预测结果,你会发现厌恶(disgust)的召回率常年垫底,恐惧和惊讶互相混——这不是模型写得不对,而是数据本身就这么分布的:disgust 训练样本只有几百张,fear 与 surprise 在灰度小图上视觉区分度非常低。把这张混淆矩阵放进报告,比单凭一个 65% 准确率有说服力得多。如果想让各类更均衡,可以在 fit 里加 class_weight,按样本数的倒数给少样本类加权,但要有心理准备:整体准确率可能不升反降,因为它牺牲了多数类的把握来换少数类的召回,报告里写清楚取舍就行。

4.5 对比实验:让报告从“能跑”变成“有结论”

学有余力时,给项目加一组微型对比实验,报告会好看一个档次。常见做法是固定模型结构,只改一个变量:一组用完整增强,另一组关掉增强;或者一组用 ReLU,另一组换 LeakyReLU。每组训到早停,记录验证准确率和模型大小,做成一个三行五列的表格放进报告。

比如列可以是“配置 / 验证准确率 / 测试准确率 / 主要结论”,行放“基准 + 增强”“基准 − 增强”“基准 + LeakyReLU”。注意每组实验都要真实跑出来再填数,不要编造。你会发现一个现象:去掉增强后训练集准确率反而更高,但验证集落下来,这个结论恰好支撑报告里写“数据增强是本任务中抑制过拟合的关键手段”。一份源码加一个能讲出因果的实验,比多堆十层卷积更让老师信服。

5. 期末作业常见排查:从训练翻车到答辩演示的五个教训

这一章把我在帮人调这类作业时见过最多的问题集中列出来,按现象、原因、解决三步走,每一条都是一段实际调试经验。

5.1 损失一直在降,验证准确率却卡在 50% 附近

现象:训练集 loss 一路下滑到 0.2 以下,训练准确率逼近 95%,验证准确率始终在 50% 上下波动,偶尔还往下掉。原因:模型容量对 FER2013 来说偏大,加上训练轮次过多,网络开始背题,把训练集里的噪声细节一起记住了,没学到泛化的表情模式。解决:回模型结构上做三件事——确认卷积输出接的是 GlobalAveragePooling2D 而不是 Flatten;把最后那个 Dense(128) 暂时去掉,看验证集是否反弹;把 Dropout 比例从 0.25 提到 0.5。增强参数也可以适当加大 rotation_range 到 12、width_shift_range 到 0.15,但每次只改一个变量。如果验证集在 50% 上下发生在第 3 轮以内,先不用管,可能是没收敛;真正要警惕的是“训练集一路涨、验证集十轮不涨”的剪刀差。把训练过程当成黑匣子看没有意义,曲线才是第一现场。

5.2 cv2.imread 读不出来,返回 None,图片明明存在

现象:cv2.imread 某张 jpg 返回 None,用 os.path.exists 检查文件又确实存在,Windows 上最常见,换到 Linux 上同样的代码又正常。原因:OpenCV 的 imread 底层走的是 C++ 文件接口,对中文路径和中文文件名支持很差;很多同学的项目放在“桌面\期末大作业”这种目录下,路径里带中文,cv2 直接罢工。解决:不要用 cv2.imread,改用 imdecode 从字节流解码,写一个通用函数:

import numpy as np import cv2 def imread_unicode(path: str): data = np.fromfile(path, dtype=np.uint8) if data.size == 0: return None return cv2.imdecode(data, cv2.IMREAD_COLOR)

这个函数替代 cv2.imread 后,中文路径问题基本绝迹。更省心的办法是项目一开始就约定所有目录和文件用英文命名,但交作业的电脑上总有人用“新建文件夹 (2)”这类目录,所以这份兼容函数建议直接放进工具模块里,训练和推理共用。

5.3 摄像头实时识别卡到只有几帧每秒

现象:本地测试训练和单张图识别都正常,一打开摄像头就卡成 PPT,画面延迟明显。原因:Haar 检测不慢,慢的是“每一帧都送进 CNN 推理”。如果不信,可以在画面角落画一个实时 FPS 数值,低于 8 基本就是推理频率过高。解决:跳帧策略,不需要每帧都跑模型。

frame_skip = 3 frame_count = 0 while True: ok, frame = cap.read() if not ok: break frame_count += 1 if frame_count % frame_skip != 0: continue # 只有这里才做检测和分类 boxes = detector.detectMultiScale(gray, ...)

把 frame_skip 设成 3 或 5,换来每秒两到三次的识别更新,演示时画面依然是流畅的——表情本来就不是高频变化。另一个常用手段是先把摄像头分辨率降到 640×480,Haar 在小图上检测也快很多。加一个跳帧注释,答辩时还能顺势讲出“我考虑了实时性约束”,这是加分点。

5.4 恐惧、厌恶几乎永远预测不对

现象:混淆矩阵里恐惧和惊讶互相混、厌恶的召回率只有个位数,其余五类还算正常。原因:FER2013 里 disgust 样本只有几百张,占总量的百分之二三,CNN 在这种严重不均衡的分布下,会把不确定的样本全部押到多数类;恐惧和惊讶在 48×48 灰度图上边缘纹理接近,人也不太好分。解决:先别急着改模型。把混淆矩阵放进报告,说明这是数据集本身的类别分布问题,比硬调参更有说服力。如果真想动手,给 fit 加 class_weight 是最均衡的方案:

from sklearn.utils.class_weight import compute_class_weight class_weight = compute_class_weight( class_weight="balanced", classes=np.array([0, 1, 2, 3, 4, 5, 6]), y=train_gen.classes, ) class_weight = dict(enumerate(class_weight))

加了之后观察 disgust 的召回率是否上升、整体准确率是否下滑,把这个反向趋势写进报告作为实验分析,老师不会认为这是失败,反而觉得你做了真实的权衡。

5.5 临时换一台电脑演示,模型跑不起来

现象:答辩前把项目拷到教室电脑上,双击运行直接报错,或者模型加载失败。原因:最常见两类——依赖没装齐,模型文件路径写死在你的绝对路径里。换一台机器,pip list 里没有你的 tensorflow 和 opencv;你写的是 /Users/xxx/Desktop/project/best_model.keras,换电脑必崩。解决:项目根目录放一个 requirements.txt,把关键依赖写全;代码里所有权重路径改成相对路径,并加一段启动检查:

import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent MODEL_PATH = BASE_DIR / "models" / "best_model.keras" if not MODEL_PATH.exists(): raise FileNotFoundError( "模型文件不存在,请先训练或从项目根目录运行" )

演示前留出十分钟做完整冷启动:删掉pycache,在项目根目录用 python main.py 从零跑一遍。能在干净环境下跑通,才算真正交付。

6. 把模型变成能演示的作业:实时推理、GUI 壳与报告组织

6.1 摄像头实时识别:用跳帧和置信度条撑起演示

训练出的模型最终要落实成“系统”,实时推理代码是演示主角。至少要有三样东西同时显示在画面上:检测框、表情标签、置信度条。置信度条默认用小矩形条,根椐概率值变长,比纯文字直观得多。代码结构和 5.3 的跳帧逻辑一致,加载模型后循环读帧,检测到人脸就送入 CNN,把结果画回去。参数上把脸的最小检测尺寸设到 64×64,避免远处噪声被误检成人脸;scaleFactor 保持 1.1,这是速度与召回率的常用平衡点。演示时把摄像头往后拉一点、脸约占画面四分之一再启动,Haar 在这个尺度下最稳定;教室光线暗就先开一盏正面灯,避免半张脸在阴影里。

6.2 一个 60 行不到的 Tkinter 壳子:让演示有“系统”感

期末大作业重要的是“系统”两个字。用 Tkinter 包一个最薄的壳,一个“打开摄像头”按钮、一个“退出”按钮就够。摄像头循环要放进子线程,否则会卡死界面,退出时用 root.quit 收尾。这个壳子不解决技术难点,解决的是演示观感:双击一个 py 文件弹出有标题、有按钮的窗口,比直接弹黑底控制台窗口,给老师的“这是一个系统”的印象完全不同。

6.3 实验报告与答辩材料怎么收尾

源码之外,报告决定这门作业的上限。结构按七节走就够:需求分析、方案设计、数据集介绍、模型结构、训练过程、结果分析、不足与改进。需要配的图就是前面生成的四张:数据样本、模型 summary、训练曲线、混淆矩阵。报告里的每个数字都要能对应到代码或日志,随手写“准确率 93%”但拿不出训练日志,是答辩现场最危险的破绽。被问频率最高的三处是:为什么用 Haar、为什么只有三层卷积、验证集和测试集什么区别——这三处在前文都有答案,提前用大白话练两遍。回答时优先讲对比了什么、为什么这么选,哪怕说“我试过更深的结构,笔记本显存不够”也比空谈理论可信。

最后说一个我自己的教训。以前带过一个同学,拿了整包源码,连模型层名都没改就交上去,答辩老师问“你的输入为什么是 48×48”,他愣了半天答不出。后来他把代码推倒重来两天,自己把模型写了,反而磕磕绊绊答得老师连连点头。期末大作业源码真正值钱的,不是那几 KB 代码本身,而是你跑通之后能讲出的完整链路:数据怎么读、特征怎么提、参数为什么这样调。把话说到这份上,这门课基本就稳了。希望帮到你。

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

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

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

立即咨询