2026/9/3 4:39:34

基于TensorFlow 2.x的FER2013表情识别:从数据预处理到CNN模型实战

基于TensorFlow 2.x的FER2013表情识别:从数据预处理到CNN模型实战 简介本资源是一份面向深度学习初学者与计算机视觉实践者的完整人脸表情识别项目基于TensorFlow框架与FER2013公开数据集利用轻量级卷积神经网络实现七类基础表情愤怒、厌恶、恐惧、高兴、悲伤、惊讶、中性的端到端识别。压缩包共9个文件含7个Python模块涵盖数据生成、模型构建、前向/后向传播、训练/测试及应用部署、1份README.md说明文档和1个.gitignore配置文件结构清晰、模块解耦便于理解CNN在图像分类任务中的全流程实现。资源仅14KB精简高效无冗余依赖适合作为课程设计、Kaggle入门或AI实验课参考。已有316人学习下载提供从数据加载、标准化、模型训练到实时预测的完整代码链附带可直接运行的CLI交互脚本fer_app.py与关键超参配置fer_config.py显著降低复现门槛助力快速掌握表情识别核心实现逻辑与工程规范。1. 项目缘起从零开始构建一个表情识别模型最近在整理一些旧项目翻到了一个几年前用TensorFlow 1.x写的表情识别demo代码结构混乱依赖也过时了。正好看到网上关于TensorFlow 2.x和PyTorch的讨论又热了起来特别是TensorFlow 2.18发布后安装和生态又有了一些新变化。我就在想不如用最新的稳定环境把这个人脸表情识别的经典项目Fer2013重新跑一遍梳理一个清晰、可复现的流程。这不仅是温故知新也能给刚入门计算机视觉的朋友一个完整的、踩过坑的实践参考。Fer2013数据集虽然有些年头了图像尺寸小、质量也一般但它作为表情识别领域的“MNIST”结构清晰、任务典型非常适合用来理解卷积神经网络CNN处理图像分类任务的全链路从数据加载、预处理、模型构建、训练到评估。整个过程就像搭积木每一步的选择都直接影响最终的“建筑”是否稳固。这次我们就用TensorFlow 2.x一步步把这个“积木”搭起来并重点聊聊那些官方教程里不会写的、容易翻车的细节。2. 环境搭建与数据准备万事开头难动手之前一个干净、可控的环境是避免后续各种“玄学”错误的基础。很多人喜欢直接往系统Python里装包一旦项目多了或者版本冲突排查起来极其痛苦。我的建议是为每个项目创建独立的虚拟环境。2.1 创建并激活虚拟环境这里以conda为例如果你用venv逻辑类似# 创建一个名为fer2013的Python 3.9环境3.8-3.10都是TensorFlow 2.x的稳定支持版本 conda create -n fer2013 python3.9 -y # 激活环境 conda activate fer2013激活后你的命令行提示符前应该会出现(fer2013)这表示后续的所有操作都隔离在这个环境里。2.2 安装核心依赖接下来安装TensorFlow。截至2024年TensorFlow 2.18是稳定版本。对于大多数不需要用到最新实验性功能的项目安装tensorflow这个元包即可它会自动拉取兼容的CPU和基础GPU版本。pip install tensorflow2.18.0 -i https://pypi.tuna.tsinghua.edu.cn/simple这里使用了清华镜像源加速下载。安装完成后强烈建议在Python交互环境里快速验证一下import tensorflow as tf print(tf.__version__) # 应该输出 2.18.0 print(GPU可用:, tf.config.list_physical_devices(GPU)) # 检查GPU是否被识别如果输出GPU可用恭喜你训练速度将大大提升。如果不可用可能是CUDA和cuDNN版本不匹配这又是一个大坑我们后面会简单提一下。除了TensorFlow我们还需要一些辅助工具pip install numpy pandas matplotlib opencv-python scikit-learn -i https://pypi.tuna.tsinghua.edu.cn/simplenumpy,pandas: 数据处理必备。matplotlib: 用于可视化图像、绘制训练曲线。opencv-python(cv2): 虽然不是必须但在图像预处理、可视化时非常方便。scikit-learn: 用于数据划分、生成分类报告等。2.3 解压与理解Fer2013数据集从网上下载的fer2013.zip解压后通常会得到一个fer2013.csv文件。这个文件就是整个数据集的核心它没有提供原始的图像文件而是将所有图像以像素字符串的形式存储在了表格里。用pandas加载看看import pandas as pd df pd.read_csv(fer2013.csv) print(df.head()) print(df.shape)你会看到类似这样的结构emotionpixelsUsage070 80 82 72 58 58 60 63 54 58 ...Training2151 150 147 155 148 133 111 140 170 ...PublicTest.........emotion: 表情标签共7类。对应关系通常是0Angry生气 1Disgust厌恶 2Fear恐惧 3Happy高兴 4Sad悲伤 5Surprise惊讶 6Neutral中性。pixels: 图像像素值。每张图是48x48的灰度图所以这个字符串包含48*482304个数字用空格分隔。Usage: 数据用途。分为Training训练集、PublicTest验证集/公共测试集和PrivateTest私有测试集。我们通常用Training训练用PublicTest做验证和早停用PrivateTest做最终测试。这里第一个坑就来了数据不平衡。你可以统计一下各类别的数量print(df[emotion].value_counts().sort_index())你会发现Happy (3)和Neutral (6)的样本数远多于Disgust (1)。直接训练模型会倾向于预测多数类。我们可以在损失函数上做文章如class_weight也可以在数据加载时进行过采样/欠采样这个我们放到数据生成器部分细说。3. 数据预处理与增强给模型喂“好消化”的粮食原始像素字符串和标签需要被转换成模型能直接处理的格式即(样本数, 高度, 宽度, 通道数)的四维张量Tensor和对应的标签张量。3.1 像素字符串到图像张量的转换我们需要写一个函数将pixels列中的字符串转换为(48, 48, 1)的numpy数组。import numpy as np def pixels_to_array(pixel_str): 将像素字符串转换为numpy数组 Args: pixel_str: 空格分隔的像素值字符串 Returns: numpy array with shape (48, 48, 1) # 将字符串按空格分割并转换为整数列表 pixels list(map(int, pixel_str.split())) # 转换为48x48的数组 image np.array(pixels, dtypenp.uint8).reshape(48, 48) # 增加通道维度灰度图为1 image np.expand_dims(image, axis-1) # 形状变为 (48, 48, 1) return image这里dtypenp.uint8是因为像素值范围是0-255。增加通道维度是为了符合TensorFlow卷积层期望的输入格式(batch, height, width, channels)。3.2 数据划分与标准化首先根据Usage列划分数据集# 划分数据 train_df df[df[Usage] Training] val_df df[df[Usage] PublicTest] test_df df[df[Usage] PrivateTest] print(f训练集: {len(train_df)} 验证集: {len(val_df)} 测试集: {len(test_df)})然后将每个子集的数据转换为数组。这里务必注意内存管理。Fer2013总共约3.5万张图全加载进内存的数组大小约为35887 * 48 * 48 * 1 * 1 byte ≈ 82 MB对于现代计算机内存可以接受。但如果数据集更大就需要使用生成器tf.data.Dataset或ImageDataGenerator.flow_from_dataframe来动态加载。def df_to_data_and_labels(dataframe): 将DataFrame转换为图像数据和标签数组 images [] labels [] for idx, row in dataframe.iterrows(): img_array pixels_to_array(row[pixels]) images.append(img_array) labels.append(row[emotion]) # 转换为numpy数组 images np.array(images, dtypenp.float32) # 注意转为float32以便后续计算 labels np.array(labels, dtypenp.int32) return images, labels X_train, y_train df_to_data_and_labels(train_df) X_val, y_val df_to_data_and_labels(val_df) X_test, y_test df_to_data_and_labels(test_df)接下来是标准化。这是一个关键步骤能帮助模型更快、更稳定地收敛。对于图像数据常见做法是将像素值从[0, 255]缩放到[0, 1]或进行均值方差归一化。这里我们采用简单的缩放X_train X_train / 255.0 X_val X_val / 255.0 X_test X_test / 255.0同时将标签进行独热编码以适应多分类任务的损失函数如categorical_crossentropy。from tensorflow.keras.utils import to_categorical num_classes 7 y_train_onehot to_categorical(y_train, num_classes) y_val_onehot to_categorical(y_val, num_classes) y_test_onehot to_categorical(y_test, num_classes)3.3 使用ImageDataGenerator进行数据增强Fer2013数据集样本不多尤其是某些类别。为了防止过拟合并提升模型泛化能力数据增强是必不可少的。TensorFlow Keras提供了强大的ImageDataGenerator。我们需要为训练集创建增强生成器而为验证集和测试集创建仅做标准化的生成器不增强。from tensorflow.keras.preprocessing.image import ImageDataGenerator # 训练数据生成器带增强 train_datagen ImageDataGenerator( rotation_range15, # 随机旋转角度范围 width_shift_range0.1, # 水平随机平移范围比例 height_shift_range0.1, # 垂直随机平移范围 shear_range0.1, # 随机错切变换范围 zoom_range0.1, # 随机缩放范围 horizontal_flipTrue, # 随机水平翻转对于人脸水平翻转通常是合理的 fill_modenearest # 填充新像素的策略 # 注意我们没有设置 rescale1./255因为之前已经做过了 ) # 验证和测试数据生成器仅标准化不增强 val_test_datagen ImageDataGenerator() # 创建生成器 batch_size 64 train_generator train_datagen.flow(X_train, y_train_onehot, batch_sizebatch_size, shuffleTrue) val_generator val_test_datagen.flow(X_val, y_val_onehot, batch_sizebatch_size, shuffleFalse) # 验证集无需打乱这里有个重要细节flow方法要求输入数据是四维的(n_samples, height, width, channels)并且标签是独热编码格式。它会在每个epoch中实时地对这批数据进行增强变换生成新的批次从而无限地提供“新”数据。4. 构建卷积神经网络模型设计你的特征提取器模型结构没有绝对的最优解我们需要在复杂度防止过拟合和表达能力防止欠拟合之间取得平衡。对于48x48的小图一个4-5层的卷积网络通常就够了。4.1 模型结构设计下面是一个参考结构使用了经典的“卷积-批归一化-激活-池化”堆叠模式最后接全连接层进行分类。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, BatchNormalization, Dropout, Flatten, Dense def create_cnn_model(input_shape(48, 48, 1), num_classes7): model Sequential(nameFER_CNN) # 第一卷积块提取低级特征如边缘、纹理 model.add(Conv2D(64, (3, 3), paddingsame, input_shapeinput_shape)) model.add(BatchNormalization()) # 加速训练稳定梯度 model.add(tf.keras.layers.Activation(relu)) model.add(MaxPooling2D(pool_size(2, 2))) model.add(Dropout(0.25)) # 随机丢弃25%的神经元防止过拟合 # 第二卷积块提取更复杂的特征 model.add(Conv2D(128, (3, 3), paddingsame)) model.add(BatchNormalization()) model.add(tf.keras.layers.Activation(relu)) model.add(MaxPooling2D(pool_size(2, 2))) model.add(Dropout(0.25)) # 第三卷积块 model.add(Conv2D(256, (3, 3), paddingsame)) model.add(BatchNormalization()) model.add(tf.keras.layers.Activation(relu)) model.add(MaxPooling2D(pool_size(2, 2))) model.add(Dropout(0.25)) # 将三维特征图展平为一维向量 model.add(Flatten()) # 全连接层 model.add(Dense(512)) model.add(BatchNormalization()) model.add(tf.keras.layers.Activation(relu)) model.add(Dropout(0.5)) # 全连接层更容易过拟合丢弃率设高一些 # 输出层7个神经元对应7类使用softmax激活输出概率分布 model.add(Dense(num_classes, activationsoftmax)) return model model create_cnn_model() model.summary() # 打印模型结构查看参数数量为什么这么设计卷积核大小(3,3)小卷积核能减少参数增加网络深度和非线性是VGG网络推广的经典选择。Paddingsame在卷积前对图像边缘进行填充使得输出特征图的空间尺寸高和宽与输入相同避免信息过快缩小。批归一化(BatchNormalization)这是现代深度网络训练的关键技巧。它对每一批数据进行归一化减均值除方差使得中间层的输出分布更稳定允许使用更大的学习率加速收敛并有一定正则化效果。注意BN层通常在卷积层之后、激活函数之前加入。Dropout随机让一部分神经元失活强迫网络学习更鲁棒的特征是防止过拟合的利器。卷积层后Dropout率一般较低0.25全连接层后较高0.5。池化层逐步降低特征图的空间尺寸减少计算量同时扩大后续卷积层的感受野使特征更加抽象和全局化。4.2 编译模型选择优化器和损失函数模型结构定义好后需要指定如何训练它即编译。from tensorflow.keras.optimizers import Adam from tensorflow.keras.losses import CategoricalCrossentropy from tensorflow.keras.metrics import CategoricalAccuracy # 使用Adam优化器学习率是核心超参数可以从1e-3开始尝试 initial_learning_rate 0.001 optimizer Adam(learning_rateinitial_learning_rate) # 多分类任务使用分类交叉熵损失标签必须是独热编码格式 loss CategoricalCrossentropy() # 评估指标用分类准确率 model.compile(optimizeroptimizer, lossloss, metrics[CategoricalAccuracy()])关于优化器Adam是自适应学习率优化器结合了Momentum和RMSProp的优点在大多数情况下表现良好是默认选择。如果训练后期发现验证集准确率波动可以尝试加入学习率衰减策略。关于类别不平衡之前提到数据不平衡我们可以在编译时通过class_weight参数给少数类更高的损失权重。首先计算每个类别的权重from sklearn.utils.class_weight import compute_class_weight import numpy as np # 计算类别权重 class_weights compute_class_weight(balanced, classesnp.unique(y_train), yy_train) # 转换为字典格式供model.fit使用 class_weight_dict dict(enumerate(class_weights)) print(类别权重:, class_weight_dict)然后在训练时传入class_weightclass_weight_dict。这会让模型在计算损失时更加“关注”样本数少的类别如Disgust。5. 模型训练与监控在迭代中寻找最优解训练不是简单地跑完epoch我们需要监控关键指标及时调整策略。5.1 设置回调函数回调函数是训练过程中的“监控员”和“控制员”能在特定时机执行操作。from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau, TensorBoard import os # 确保保存模型的目录存在 checkpoint_dir ./model_checkpoints if not os.path.exists(checkpoint_dir): os.makedirs(checkpoint_dir) # 1. 模型检查点保存验证集上性能最好的模型 checkpoint_callback ModelCheckpoint( filepathos.path.join(checkpoint_dir, best_model.weights.h5), # 保存权重 monitorval_categorical_accuracy, # 监控验证集准确率 modemax, # 希望准确率最大 save_best_onlyTrue, # 只保存最好的 save_weights_onlyTrue, # 只保存权重文件更小 verbose1 # 打印保存信息 ) # 2. 早停当验证集指标不再提升时提前停止训练防止过拟合 early_stopping_callback EarlyStopping( monitorval_categorical_accuracy, patience15, # 容忍连续15个epoch指标没有提升 modemax, restore_best_weightsTrue, # 停止后恢复最佳权重 verbose1 ) # 3. 动态降低学习率当指标停滞时降低学习率有助于微调模型 reduce_lr_callback ReduceLROnPlateau( monitorval_loss, # 监控验证集损失 factor0.5, # 学习率乘以0.5 patience7, # 容忍7个epoch min_lr1e-6, # 学习率下限 verbose1 ) # 4. TensorBoard回调可选但强烈推荐可视化训练过程 # tensorboard_callback TensorBoard(log_dir./logs, histogram_freq1) # 使用前需先启动TensorBoard服务: tensorboard --logdir./logs callbacks_list [checkpoint_callback, early_stopping_callback, reduce_lr_callback] # 可加入tensorboard_callback5.2 开始训练使用生成器进行训练。注意由于我们使用了数据增强每个epoch看到的“数据”都是不同的。epochs 100 # 设置一个较大的值靠早停回调来实际控制停止时机 history model.fit( train_generator, # 训练数据生成器 steps_per_epochlen(X_train) // batch_size, # 每个epoch迭代多少步 epochsepochs, validation_dataval_generator, # 验证数据生成器 validation_stepslen(X_val) // batch_size, callbackscallbacks_list, class_weightclass_weight_dict # 传入类别权重字典 )参数解释steps_per_epoch: 一个epoch需要从生成器中抽取多少个批次。通常设置为训练样本数 // batch_size这样每个epoch大致能遍历一次所有训练数据。validation_steps: 类似用于验证集。class_weight: 传入之前计算的类别权重字典用于平衡损失。训练开始后观察控制台输出。你会看到每个epoch的训练损失、准确率以及验证损失、准确率。理想情况下训练损失和验证损失都应稳步下降训练准确率和验证准确率稳步上升且两者差距不大。如果训练准确率远高于验证准确率说明过拟合了。5.3 可视化训练过程训练结束后history对象保存了所有历史指标。我们可以绘制学习曲线这是诊断模型问题的关键。import matplotlib.pyplot as plt def plot_training_history(history): fig, axes plt.subplots(1, 2, figsize(12, 4)) # 绘制损失曲线 axes[0].plot(history.history[loss], labelTrain Loss) axes[0].plot(history.history[val_loss], labelVal Loss) axes[0].set_title(Model Loss) axes[0].set_xlabel(Epoch) axes[0].set_ylabel(Loss) axes[0].legend() axes[0].grid(True) # 绘制准确率曲线 axes[1].plot(history.history[categorical_accuracy], labelTrain Accuracy) axes[1].plot(history.history[val_categorical_accuracy], labelVal Accuracy) axes[1].set_title(Model Accuracy) axes[1].set_xlabel(Epoch) axes[1].set_ylabel(Accuracy) axes[1].legend() axes[1].grid(True) plt.tight_layout() plt.show() plot_training_history(history)通过曲线你可以判断欠拟合训练和验证准确率都很低且不再上升。可能需要更复杂的模型、更长时间的训练或更强的特征工程。过拟合训练准确率很高但验证准确率在某个点后开始下降或停滞。需要加强正则化更多Dropout、数据增强、简化模型或收集更多数据。训练稳定两条曲线收敛且接近验证准确率达到可接受水平。6. 模型评估与测试是骡子是马拉出来遛遛训练完成后我们用保存的最佳模型权重ModelCheckpoint保存的加载模型并在从未参与训练和验证的测试集PrivateTest上进行最终评估。6.1 加载最佳模型并评估# 重新构建模型结构或直接使用训练好的model但加载权重更干净 model_for_test create_cnn_model() model_for_test.compile(optimizerAdam(learning_rate0.001), lossCategoricalCrossentropy(), metrics[CategoricalAccuracy()]) # 加载保存的最佳权重 best_weights_path ./model_checkpoints/best_model.weights.h5 model_for_test.load_weights(best_weights_path) # 在测试集上评估 test_loss, test_accuracy model_for_test.evaluate(X_test, y_test_onehot, verbose0) print(f测试集损失: {test_loss:.4f}) print(f测试集准确率: {test_accuracy:.4f})Fer2013上一个中等复杂度的CNN模型测试集准确率通常在60%-65%左右。这个数字看起来不高但考虑到数据集本身噪声大低分辨率、标注主观、类别不平衡、某些表情如恐惧和惊讶本身就相似这个结果是合理的。State-of-the-art的研究通过更复杂的模型如ResNet、注意力机制、更精细的数据预处理和集成学习可以将准确率提升到70%以上。6.2 深入分析混淆矩阵与分类报告准确率只是一个总体指标我们需要知道模型在哪些类别上表现好哪些类别上容易混淆。这时就需要混淆矩阵和分类报告。from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns # 获取模型在测试集上的预测结果概率 y_pred_probs model_for_test.predict(X_test, verbose0) # 将概率转换为类别标签 y_pred_classes np.argmax(y_pred_probs, axis1) y_true_classes np.argmax(y_test_onehot, axis1) # 将独热编码转回类别标签 # 计算混淆矩阵 cm confusion_matrix(y_true_classes, y_pred_classes) # 可视化混淆矩阵 plt.figure(figsize(10, 8)) emotion_labels [Angry, Disgust, Fear, Happy, Sad, Surprise, Neutral] sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsemotion_labels, yticklabelsemotion_labels) plt.title(Confusion Matrix on Test Set) plt.ylabel(True Label) plt.xlabel(Predicted Label) plt.show() # 打印详细的分类报告 print(classification_report(y_true_classes, y_pred_classes, target_namesemotion_labels, digits4))通过混淆矩阵你可能会发现Happy和Neutral的识别率通常最高样本多特征相对明显。Disgust样本极少可能被错误地预测为Angry或Fear。Fear和Surprise容易相互混淆都涉及眼睛和嘴巴的张大。Sad和Neutral也可能有混淆。分类报告会给出每个类别的精确率Precision、召回率Recall和F1-score帮你精准定位模型的薄弱环节。6.3 可视化预测结果最后我们可以随机抽取一些测试集图片直观地看下模型的预测结果。import random def visualize_predictions(images, true_labels, pred_labels, emotion_labels, num_samples10): indices random.sample(range(len(images)), num_samples) plt.figure(figsize(15, int(num_samples/2)*3)) for i, idx in enumerate(indices): plt.subplot(int(num_samples/5)1, 5, i1) # 图像是归一化后的显示时需要还原到[0,1]范围或直接显示 plt.imshow(images[idx].squeeze(), cmapgray) # squeeze去掉通道维度 true_emo emotion_labels[true_labels[idx]] pred_emo emotion_labels[pred_labels[idx]] color green if true_emo pred_emo else red plt.title(fTrue: {true_emo}\nPred: {pred_emo}, colorcolor) plt.axis(off) plt.tight_layout() plt.show() visualize_predictions(X_test, y_true_classes, y_pred_classes, emotion_labels, num_samples10)绿色标题表示预测正确红色表示错误。通过观察错误案例你能直观感受到任务的挑战性有些图像模糊不清人类都难以判断有些表情本身界限模糊。这也能为后续改进提供方向比如是否需要更清晰的数据集或者模型是否需要关注特定的面部区域引入注意力机制。7. 踩坑实录与进阶思考走完整个流程你可能已经成功了但也可能遇到了各种问题。下面分享几个我实践中遇到的典型问题和解决思路。7.1 内存溢出与数据加载优化如果你在加载数据到X_train, y_train时就遇到内存错误或者想处理更大的数据集必须使用生成器模式。我们可以利用tf.data.DatasetAPI它更高效、更灵活。import tensorflow as tf def parse_function(pixels_str, label): 将像素字符串和标签转换为Tensor # 解码像素字符串 pixels tf.strings.split(pixels_str, sep ) pixels tf.strings.to_number(pixels, out_typetf.float32) # 重塑为48x48x1 image tf.reshape(pixels, (48, 48, 1)) # 归一化 image image / 255.0 # 独热编码标签需提前将标签转为整数 label tf.one_hot(label, depthnum_classes) return image, label # 假设我们有一个包含像素字符串和整数标签的Dataset # 这里演示思路具体创建需要先将DataFrame转换为TensorSlice # train_ds tf.data.Dataset.from_tensor_slices((train_df[pixels].values, train_df[emotion].values)) # train_ds train_ds.map(parse_function, num_parallel_callstf.data.AUTOTUNE) # train_ds train_ds.shuffle(buffer_size10000).batch(batch_size).prefetch(tf.data.AUTOTUNE)tf.data管道可以并行化数据预处理并实现预取极大提升GPU利用率尤其适合大规模数据集。7.2 训练过程震荡或不收敛学习率太大这是最常见原因。尝试将Adam的初始学习率从1e-3降低到5e-4或1e-4。没有进行数据标准化确保输入数据被缩放到[0,1]或使用均值方差归一化。模型结构问题网络太深或太浅。对于小数据集过深的网络极易过拟合。可以尝试减少卷积层过滤器数量或层数。梯度爆炸/消失加入BatchNormalization层和合理的权重初始化如he_normal通常能缓解。在Conv2D中可以通过kernel_initializerhe_normal指定。损失函数选择确保标签格式与损失函数匹配。CategoricalCrossentropy对应独热编码标签SparseCategoricalCrossentropy对应整数标签。7.3 过拟合的应对策略除了之前提到的数据增强和Dropout还有L1/L2正则化在Conv2D或Dense层中添加kernel_regularizertf.keras.regularizers.l2(0.001)。更早的早停减小EarlyStopping的patience参数。简化模型减少神经元数量或网络层数。获取更多数据这是最根本但往往最难的方法。可以考虑使用其他表情数据集进行预训练或迁移学习。7.4 尝试迁移学习对于小数据集从在大规模数据集如ImageNet上预训练好的模型开始是提升性能的强有力手段。虽然ImageNet是彩色物体分类但其底层特征提取能力是通用的。from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras.layers import GlobalAveragePooling2D # 加载预训练模型不包括顶部分类层输入尺寸调整为48x48x3需要将灰度图复制到3个通道 base_model MobileNetV2(weightsimagenet, include_topFalse, input_shape(48, 48, 3)) # 冻结预训练模型的所有层先只训练我们新添加的层 base_model.trainable False # 构建新模型 model_transfer Sequential([ # 将单通道灰度图复制为三通道模拟RGB tf.keras.layers.Lambda(lambda x: tf.repeat(x, repeats3, axis-1), input_shape(48, 48, 1)), base_model, GlobalAveragePooling2D(), # 替代Flatten对空间维度进行平均输出维度更低 Dropout(0.5), Dense(128, activationrelu), Dropout(0.3), Dense(num_classes, activationsoftmax) ])先冻结预训练层训练几轮然后可以解冻后面几层进行微调。这种方法通常能获得比从头训练更好的起点和最终性能。整个项目从环境搭建到模型评估涉及了深度学习项目的大部分核心环节。Fer2013作为一个入门级但又不失挑战性的数据集非常适合用来练手和深入理解CNN的运作。希望这个详细的流程和其中穿插的经验之谈能帮你少走弯路更顺畅地搭建起自己的第一个表情识别模型。记住调参和迭代的过程本身就是学习的一部分多实验、多观察、多思考你会对模型有更深刻的直觉。本文还有配套的精品资源点击获取