简介一套基于卷积神经网络CNN的人脸表情识别Python期末大作业源码面向计算机专业学生、课程设计者及深度学习入门者解决人脸表情识别从数据处理、模型训练到实际识别的完整工程参考问题。这套源码曾获导师认可评审得分98分并在本地编译调试通过具备可运行性。压缩包共23个文件以10个Python脚本为核心覆盖数据读取、模型构建、训练、图片识别和摄像头实时识别等模块另含TensorFlow模型与数据文件、示例图片、可视化网页展示和说明文档整体大小约19兆字节。已有51人学习下载。通过该项目学习者可掌握卷积神经网络原理、人脸特征提取与表情分类的代码实现同时了解数据预处理、模型调参与结果展示的完整流程利用示例图片可快速验证效果结合摄像头模块还能体验实时识别适合将其改造为课程设计或期末项目。1. 基于 CNN 的人脸表情识别是什么作业先认清它的四个组件一份“基于 CNN 的人脸表情识别系统的期末大作业源码”说白了就是用 Python 把深度学习里最经典的一条图像分类链路完整跑通从摄像头或图片里框出人脸把这块人脸区域交给卷积神经网络让它输出“生气、厌恶、恐惧、开心、中性、悲伤、惊讶”这七类表情中的一种。难的不是模型本身而是四个组件缺一不可人脸检测、人脸预处理、CNN 分类、结果可视化任何一个环节掉链子现场演示都会翻车。这套作业适合两类人正在做课程设计或毕业设计、需要一份能跑能讲能写进报告的项目的人想快速验证 CNN 完整图像分类流程的开发者——它比猫狗分类多了一道“先找脸再分类”的步骤练完你对怎么组织图像项目会有非常具体的体感。先把一句话放在这模型不是越复杂越好。2. 方案选型为什么期末大作业的稳妥组合是 Haar 检测 自搭 CNN2.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 有三个字段emotion0 到 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(), dtypenp.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), cmapgray) 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_okTrue) 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( rescale1.0 / 255.0, rotation_range10, width_shift_range0.1, height_shift_range0.1, zoom_range0.1, horizontal_flipTrue, ) valid_gen ImageDataGenerator(rescale1.0 / 255.0)参数为什么这么取要能说出理由。rotation_range10 表示随机旋转正负 10 度作业里的合理范围是 5 到 15 度——转多了人脸姿态就失真CNN 学到的会变成“旋转不变性”而不是“表情不变性”。width_shift_range 和 height_shift_range 控制平移比例0.1 表示平移幅度不超过原图宽高的 10%这恰好模拟了 Haar 检测框轻微抖动的效果。zoom_range0.1 模拟远近变化。horizontal_flipTrue 对人脸是安全的——表情不因左右翻转改变语义这是最划算的一项增强。我建议不要把 brightness_range 也打开。FER2013 本身是实验室条件下的人脸灰度图亮度分布相对统一在灰度图上强行叠加亮度扰动等于给网络引入跟任务无关的噪声训练损失可能降得更慢而且答辩时这个选择不好圆。增强参数多不等于效果好每加一项要能说清“它模拟了真实场景里的什么变化”。3.5 验证集和测试集要不要做增强这个问题答辩时几乎必问。答案是不做只做 rescale。验证集的作用是估计模型在没见过的数据上的表现如果对验证集也做平移翻转验证损失反映的就不是真实泛化能力而是模型对增强数据的适应能力。测试集更严格它模拟最终部署环境任何统计意义上的扩充都会让测试分数虚高。所以 valid_gen 只有 rescaleflow_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_classes7): model Sequential([ Conv2D(32, (3, 3), paddingsame, activationrelu, input_shapeinput_shape), BatchNormalization(), Conv2D(64, (3, 3), paddingsame, activationrelu), BatchNormalization(), MaxPooling2D((2, 2)), Dropout(0.25), Conv2D(128, (3, 3), paddingsame, activationrelu), BatchNormalization(), MaxPooling2D((2, 2)), Dropout(0.25), GlobalAveragePooling2D(), Dense(128, activationrelu), Dropout(0.5), Dense(num_classes, activationsoftmax), ]) 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( optimizerAdam(learning_rate1e-3), losscategorical_crossentropy, metrics[accuracy], ) callbacks [ ModelCheckpoint(best_model.keras, monitorval_accuracy, save_best_onlyTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience3, min_lr1e-5), EarlyStopping(monitorval_loss, patience5, restore_best_weightsTrue), ] history model.fit( train_gen, validation_datavalid_gen, epochs30, callbackscallbacks, )optimizer 选 Adam 而不是 SGD 的理由是鲁棒学习率 1e-3 是 Adam 在多数图像小任务上的甜点它带自适应学习率几乎不需要手动安排学习率下降节点。loss 用 categorical_crossentropy要求标签是 one-hot 编码flow_from_directory 的 class_modecategorical 会处理好这一步不用手写循环。如果你的 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], labeltrain_acc) plt.plot(history.history[val_accuracy], labelval_acc) plt.title(Accuracy) plt.legend() plt.subplot(1, 2, 2) plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_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(axis1) print(classification_report(y_true, y_pred, target_namesemotion_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, dtypenp.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×480Haar 在小图上检测也快很多。加一个跳帧注释答辩时还能顺势讲出“我考虑了实时性约束”这是加分点。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_weightbalanced, classesnp.array([0, 1, 2, 3, 4, 5, 6]), ytrain_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 代码本身而是你跑通之后能讲出的完整链路数据怎么读、特征怎么提、参数为什么这样调。把话说到这份上这门课基本就稳了。希望帮到你。本文还有配套的精品资源点击获取