ARTICLE DETAIL

资讯详情

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

深度学习表情识别实战:CNN模型、源码、数据集与训练全解析

深度学习表情识别实战:CNN模型、源码、数据集与训练全解析 简介这份资源面向深度学习入门者、计算机视觉方向学生及需要完成课程大作业的开发者提供一套基于卷积神经网络的人脸面部表情识别完整项目。内容涵盖Fer2013与Emoji表情集两类面部表情数据采用Keras与TensorFlow-gpu搭建CNN、VGG、ResNet等多种网络模型并配有基于OpenCV正态贝叶斯分类器训练的对比方案可帮助读者理解从数据预处理、模型训练到表情分类的完整流程。压缩包共37个文件约446.18MB包含14个py脚本、5个ipynb交互笔记、5个docx与1个pdf论文文档、1个pptx答辩演示、1个mp4示例视频以及训练好的模型文件和Haar级联人脸检测xml覆盖代码、论文、数据与模型全链路。目前已有285人学习适合作为人工智能大作业或表情识别入门实践参考。1. 从一张 48×48 灰度图说起表情识别项目到底在做什么你手里有一张 48×48 的灰度人脸图像素值 0 到 255模型要在 7 个类别里选一个生气、厌恶、恐惧、开心、悲伤、惊讶、中性。这就是人脸面部表情识别最经典的入门任务形态。标题里说的「深度学习 卷积神经网络 源码 论文 数据集 训练好的模型」拆开看其实是一条完整链路数据集负责喂数据CNN 负责提特征训练脚本负责把权重跑出来推理脚本负责把权重用起来论文负责把「为什么这么设计」讲清楚。很多人卡住的地方不是不会搭网络而是数据集格式对不上、训练完不知道模型到底行不行、换一张自己的图就翻车。这篇笔记按「数据怎么进 → 网络怎么搭 → 训练怎么调 → 坑怎么避 → 模型怎么验」的顺序走一遍目标是你照着能跑通一个可复现的版本而不是只收藏一份源码。2. 数据集与预处理FER2013 这类表情数据怎么读、怎么切、怎么增强2.1 先搞清楚你拿到的是哪种格式人脸表情识别最常见的数据集是 FER2013它的原始形态是一份 CSV每行三列emotion 标签0 到 6、pixels 字符串2304 个空格分隔的灰度值、UsageTraining / PublicTest / PrivateTest。很多人第一次打开就懵因为 pixels 不是图片文件是一长串数字。你得先把它还原成 48×48 的数组再决定是存成 npy 还是直接喂给 Dataset。常见做法是先把 CSV 转成内存里的 numpy 数组训练时再按需 reshape。这样做的好处是读取快坏处是占内存——FER2013 训练集约 28709 张48×48 的 uint8 大概 66MB完全放得下。如果你用的是自己采集的数据格式可能是文件夹分目录每个表情一个文件夹那就用ImageFolder那套逻辑但要注意类别顺序必须固定否则标签和索引会对不上。import pandas as pd import numpy as np def load_fer2013(csv_path): df pd.read_csv(csv_path) # pixels 是空格分隔的字符串拆成 2304 维再 reshape 成 48x48 pixels df[pixels].apply(lambda x: np.array(x.split(), dtypefloat32)) images np.stack(pixels.values).reshape(-1, 48, 48, 1) labels df[emotion].values.astype(int64) usage df[Usage].values return images, labels, usage images, labels, usage load_fer2013(fer2013.csv) train_mask usage Training val_mask usage PublicTest # 注意PrivateTest 留到最后再动训练中途不要碰这段代码的关键点是np.stack之前每个元素长度必须一致如果 CSV 里有空行或异常行np.stack会直接报错。参数上dtypefloat32是为了后面归一化方便如果你打算做直方图均衡化可以在 reshape 之后再做。usage这一列千万别丢它是官方划好的训练/验证/测试边界自己乱切会导致结果和论文不可比。2.2 归一化和增强别小看这两步像素值 0 到 255 直接进网络收敛会慢常见做法是除以 255 归一到 [0,1]或者减 127.5 再除 127.5 归一到 [-1,1]。后者对 ReLU 系列更友好我一般用后者。增强方面表情识别不能像通用分类那样随便翻转因为左右翻转会改变「厌恶」和「惊讶」的细微方向性但水平翻转在 FER2013 上通常还是能涨点垂直翻转绝对不要用。随机裁剪要小心48×48 本来就小裁太狠会把嘴巴或眼睛裁掉建议裁剪幅度控制在 4 像素以内。from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen ImageDataGenerator( rescale1./127.5, preprocessing_functionlambda x: x - 1.0, # 配合 rescale 实现 [-1,1] rotation_range10, width_shift_range0.1, height_shift_range0.1, horizontal_flipTrue, zoom_range0.1 ) # 验证集只做归一化不做任何随机增强 val_datagen ImageDataGenerator(rescale1./127.5, preprocessing_functionlambda x: x - 1.0)这里rotation_range10是经验值再大人脸就歪得不像表情了。zoom_range0.1对应 10% 的缩放超过 0.15 容易把边缘像素补成黑边反而引入噪声。注意验证集的生成器必须和训练集用同样的归一化参数否则验证准确率会莫名其妙偏低这是血泪经验。2.3 类别不平衡中性脸太多怎么办FER2013 里「开心」和「中性」样本偏多「厌恶」和「恐惧」偏少直接训练会让模型偏向多数类。常见做法有两种一是用class_weight给少数类加权二是用重采样。我一般先用 class_weight因为它不改数据分布只是改损失权重。from sklearn.utils.class_weight import compute_class_weight class_weights compute_class_weight( balanced, classesnp.unique(labels[train_mask]), ylabels[train_mask] ) class_weight_dict dict(enumerate(class_weights)) # 训练时 model.fit(..., class_weightclass_weight_dict)compute_class_weight的balanced模式会按n_samples / (n_classes * np.bincount(y))算权重少数类权重自然变大。注意这个权重只对训练集算验证集和测试集不要用否则评估结果会失真。3. 卷积神经网络怎么搭从 4 层基线到可用的表情分类器3.1 先跑一个能出数的基线别一上来就堆 ResNet很多人一上来就想用 ResNet50 或 EfficientNet结果发现 48×48 的输入根本喂不进去强行 resize 到 224 又慢又容易过拟合。表情识别的经典做法是用 4 到 6 层卷积每层后面接 BatchNorm 和 MaxPooling最后全局平均池化接全连接。这个结构参数量小训练快在 FER2013 上做到 65% 到 68% 的验证准确率是合理的。import tensorflow as tf from tensorflow.keras import layers, models def build_baseline_cnn(input_shape(48, 48, 1), num_classes7): model models.Sequential([ layers.Conv2D(32, (3, 3), paddingsame, input_shapeinput_shape), layers.BatchNormalization(), layers.Activation(relu), layers.Conv2D(32, (3, 3), paddingsame), layers.BatchNormalization(), layers.Activation(relu), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), layers.Conv2D(64, (3, 3), paddingsame), layers.BatchNormalization(), layers.Activation(relu), layers.Conv2D(64, (3, 3), paddingsame), layers.BatchNormalization(), layers.Activation(relu), layers.MaxPooling2D((2, 2)), layers.Dropout(0.25), layers.Conv2D(128, (3, 3), paddingsame), layers.BatchNormalization(), layers.Activation(relu), layers.GlobalAveragePooling2D(), layers.Dropout(0.5), layers.Dense(num_classes, activationsoftmax) ]) return model model build_baseline_cnn() model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losssparse_categorical_crossentropy, metrics[accuracy] ) model.summary()这里用GlobalAveragePooling2D替代Flatten是为了减少参数量48×48 经过两次池化变成 12×12再经过一次卷积后全局平均直接得到 128 维向量。Dropout放在池化后和全连接前比例 0.25 和 0.5 是常用值。paddingsame保证卷积后尺寸不变只有池化在降维。损失函数用sparse_categorical_crossentropy是因为标签是整数不是 one-hot省一步转换。3.2 训练参数怎么设学习率、batch size、早停学习率从 1e-3 开始如果 loss 震荡就降到 1e-4如果 loss 下降太慢就升到 3e-3。batch size 在 64 到 128 之间选48×48 的图不大128 一般显存够。早停用EarlyStopping监控val_losspatience 设 8 到 10再配一个ReduceLROnPlateau在验证 loss 不降时把学习率砍半。callbacks [ tf.keras.callbacks.EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ), tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience4, min_lr1e-6 ) ] history model.fit( train_datagen.flow(images[train_mask], labels[train_mask], batch_size128), validation_dataval_datagen.flow(images[val_mask], labels[val_mask], batch_size128), epochs100, callbackscallbacks, class_weightclass_weight_dict )restore_best_weightsTrue很重要它保证训练结束后模型回到验证 loss 最低的那个 epoch而不是最后一个 epoch。ReduceLROnPlateau的patience4比早停的 patience 小这样学习率先降给模型更多机会实在不行再早停。class_weight直接传字典Keras 会自动按类别加权。3.3 换更深的网络时注意什么如果你想上 VGG 或 ResNet 的小型变体输入尺寸至少要到 64×64否则下采样几次就没了。常见做法是把 48×48 先 resize 到 64×64 或 96×96但 resize 会引入插值模糊对表情这种细节敏感的任务不一定划算。我一般先在 48×48 上把基线调到最好再考虑换网络。如果非要换优先选参数量在 1M 到 5M 之间的轻量结构别直接上 ResNet50。4. 训练完模型怎么验混淆矩阵、单图推理和常见翻车现场4.1 别只看准确率先看混淆矩阵准确率 65% 听起来还行但如果模型把所有「厌恶」都预测成「生气」那在实际使用里就是废的。混淆矩阵能告诉你哪两个类在互相混。from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt test_mask usage PrivateTest preds model.predict(images[test_mask]) pred_labels np.argmax(preds, axis1) true_labels labels[test_mask] cm confusion_matrix(true_labels, pred_labels) sns.heatmap(cm, annotTrue, fmtd, cmapBlues) plt.xlabel(Predicted) plt.ylabel(True) plt.show() print(classification_report(true_labels, pred_labels, digits4))classification_report会给出每个类的 precision、recall、f1-score。如果「厌恶」的 recall 特别低说明模型几乎认不出这个类要么加权重要么加样本。PrivateTest是官方留出的测试集训练过程中绝对不能碰否则你看到的准确率是假的。4.2 单张图推理从文件到预测结果训练完保存模型推理时加载权重把图片转成 48×48 灰度归一化后送进去。from tensorflow.keras.models import load_model from PIL import Image import numpy as np model load_model(best_emotion_model.h5) emotion_labels [Angry, Disgust, Fear, Happy, Sad, Surprise, Neutral] def predict_emotion(image_path): img Image.open(image_path).convert(L).resize((48, 48)) arr np.array(img, dtypefloat32) / 127.5 - 1.0 arr arr.reshape(1, 48, 48, 1) pred model.predict(arr, verbose0) idx np.argmax(pred) return emotion_labels[idx], float(pred[0][idx]) label, score predict_emotion(test_face.jpg) print(f{label}: {score:.4f})convert(L)强制转灰度resize((48,48))用默认的双线性插值。verbose0关掉进度条批量推理时更干净。注意如果你的训练用了 [-1,1] 归一化推理也必须用同样的公式否则输入分布对不上预测会乱跳。4.3 三个最常见的翻车点第一训练时用了rescale1./255推理时忘了除结果所有预测都偏向同一个类。第二用 OpenCV 读图默认是 BGR转灰度后和 PIL 的灰度不完全一样最好统一用同一种读图方式。第三模型在 FER2013 上表现不错但换到手机自拍或监控截图就崩因为域差异太大这时候要么做微调要么做人脸对齐。5. 避坑与排查表情识别项目里最容易踩的 5 个坑5.1 现象训练 loss 一直不降准确率卡在 14% 左右原因通常是标签没对齐或者输入归一化把数据压成了全零。14% 接近 1/7说明模型在瞎猜。解决方法是先打印一个 batch 的像素均值和方差确认不是全零或全 255再检查class_weight的 key 是不是从 0 开始如果标签是 1 到 7 而权重字典是 0 到 6就会错位。5.2 现象验证准确率比训练准确率高很多这通常不是好事而是验证集太小或者验证集和训练集分布不一致。FER2013 的 PublicTest 只有 3589 张波动大。解决方法是把训练集再切一小部分出来做内部验证或者用 k 折交叉验证看稳定性。如果验证准确率比训练高 10 个点以上先怀疑数据泄漏。5.3 现象模型对某些表情完全认不出比如「恐惧」和「惊讶」混淆严重。原因是这两个类在 48×48 灰度图里差异本来就小加上样本少。解决方法是先看混淆矩阵确认是哪两类混然后针对性做增强比如对少数类做额外的旋转和缩放或者用 focal loss 替代交叉熵。5.4 现象保存的模型加载后预测结果和训练时不一致常见原因是保存时用了model.save()但加载时自定义了层或损失函数导致权重没完全恢复。解决方法是统一用load_model并确保自定义对象都注册了或者只保存权重model.save_weights()加载时先建同结构模型再load_weights。5.5 现象推理速度慢单张图要几百毫秒如果是在 CPU 上跑48×48 的基线模型不该这么慢。检查是不是每次推理都重新加载了模型或者输入没有做 batch 导致反复建图。解决方法是把模型加载提到循环外推理时用model.predict而不是model.__call__并开tf.function做图优化。6. 把模型用起来从单图预测到批量评估的一个实用技巧训练完模型只是第一步真正要用起来你得有一套可重复的评估流程。我一般会写一个脚本把测试集按类别分组分别算准确率再画一张每类召回率的柱状图。这样一眼就能看出模型在哪个表情上最弱。具体做法是先用classification_report拿到每类 recall再用 matplotlib 画出来横轴是表情名纵轴是 recall低于 0.5 的类标红。另一个实用技巧是温度缩放temperature scaling用来校准模型的置信度。表情识别模型经常出现「预测对了但置信度只有 0.4」或者「预测错了但置信度 0.9」的情况温度缩放可以在不改变预测类别的前提下让置信度更可靠。做法是在验证集上拟合一个温度参数 T推理时把 logits 除以 T 再 softmax。import tensorflow as tf def calibrate_temperature(model, val_images, val_labels): logits model.predict(val_images) # 简单网格搜索找最优 T best_t, best_loss 1.0, float(inf) for t in np.arange(0.5, 3.0, 0.1): scaled logits / t loss tf.reduce_mean( tf.keras.losses.sparse_categorical_crossentropy(val_labels, scaled) ).numpy() if loss best_loss: best_loss, best_t loss, t return best_t # 推理时probs tf.nn.softmax(logits / best_t)这个技巧在需要输出置信度的场景里很有用比如你后面要接一个「置信度低于阈值就转人工」的逻辑。温度 T 一般落在 1.0 到 2.5 之间太大反而会让所有预测都变得很平。我自己做这类项目最大的习惯是每换一次数据增强策略或网络结构一定重新跑一遍完整测试集不看训练曲线就下结论。因为训练集上的准确率是可以「背」出来的只有测试集上的混淆矩阵不会骗人。希望帮到你。本文还有配套的精品资源点击获取
返回列表