Python+CNN深度学习实现狗狗表情识别:从数据集到模型部署

发布时间:2026/9/23 1:30:20
Python+CNN深度学习实现狗狗表情识别:从数据集到模型部署 简介一套基于Python与PyTorch的CNN狗狗表情识别项目适合深度学习入门及图像分类实践者使用。资源将数据预处理、模型训练与PyQt界面展示整合在同一流程中能帮助读者从零跑通一个完整的识别Demo。压缩包内共906个文件其中896张jpg与4张jpeg构成狗狗表情图片数据集3个txt文件记录了训练集与验证集的图片路径及标签3个py脚本分别对应数据集文本制作、模型训练和可视化界面运行整体包体约80.35MB结构清晰。目前已有114人学习下载。代码对数据集做了灰边补正方形和随机旋转增强可提升模型泛化能力训练完成后会自动保存本地模型便于后续调用。配套的requirement.txt和环境安装指引降低了搭建门槛适合想快速体验卷积神经网络训练流程、或需要参考图像分类项目工程化设计的读者。1. 狗狗表情识别为什么值得用 CNN 跑一遍如果你做过宠物摄像头或者智能喂食器这类小项目大概率会碰到同一个需求把狗狗的“开心”“警戒”“困倦”“难过”从画面里自动分出来。这个标题“基于python-CNN深度学习的狗狗表情识别-含图片数据集.zip”之所以被搜索得很多是因为它正好把深度学习入门的四件事打包在一起Python 工程入口、CNN 模型、可训练图片数据集、一个能跑通的结果。实际做的时候你会发现识别不是瓶颈瓶颈是数据怎么组织、模型怎么调、以及“表情”这类主观标签怎么验证。这篇就按从零搭系统的顺序来讲新手能照着命令跑通熟手可以重点看第 4 章之后的迁移学习和置信度阈值处理。2. CNN 图像分类的最小基线先用 3 层卷积跑通2.1 全连接网络分类图片为什么不行传统前馈神经网络处理图片时第一步会把图片拉平成一维向量。一张 128×128 的彩色图就有 128×128×3 49152 个像素值如果第一层全连接用 512 个神经元这一层的参数量就是 49152×512 ≈ 2500 万。这还只是第一层训练时不仅显存吃紧还非常容易过拟合。CNN 解决这个问题的思路是局部连接和权值共享卷积核只在某个小窗口上做乘加运算同一个核扫描整张图参数数量只和核大小与通道数有关和图片尺寸没有直接关系。这也是图像处理选 CNN 而不是前馈神经网络最核心的理由简单说就是“用更少的参数把二维空间结构的信息抓住”。至于热词里常对比的 CNN 和 RNN区别也很清晰RNN 处理序列依赖而表情识别是静态图分类特征在空间上分布天然适合二维卷积。2.2 一个只有 3 层卷积的基线模型先别一上来就上 ResNet、Vision Transformer 这类大网络。第一次跑通用一个结构简单的 CNN 足够3 个卷积块每个块包含卷积、池化和 ReLU最后接全连接分类头。用 TensorFlow/Keras 实现如下。import tensorflow as tf from tensorflow.keras import layers IMG_SIZE (128, 128) NUM_CLASSES 4 # happy / angry / sleepy / neutral model tf.keras.Sequential([ tf.keras.Input(shape(IMG_SIZE[0], IMG_SIZE[1], 3)), layers.Rescaling(1.0 / 255.0, namerescaling), layers.Conv2D(32, 3, paddingsame, activationrelu), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, 3, paddingsame, activationrelu), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, 3, paddingsame, activationrelu), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dense(128, activationrelu), layers.Dropout(0.5), layers.Dense(NUM_CLASSES, activationsoftmax), ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losssparse_categorical_crossentropy, metrics[accuracy], ) model.summary()代码里几个关键点说清楚。Rescaling 层把 0~255 的像素整数缩放到 0~1比手动在数据预处理里做归一化更省事而且放在模型内部导出后推理时不容易漏掉预处理。卷积核数量从 32 到 64 再到 128逐层增加是因为浅层提取边缘、纹理这些低级特征深层需要更多通道组合出“舌头伸出来”“耳朵垂下去”这类高级语义。Dropout 放在全连接之前随机丢弃一半神经元是当前数据量下最廉价的抗过拟合手段。损失函数用 sparse_categorical_crossentropy对应标签是整数编码不需要做 one-hot省一层转换。下面这张表是这个基线的固定参数后面调优都从这组值出发。参数基线值说明IMG_SIZE128×128速度和精度的中间值先跑通再考虑 224卷积核32/64/128按 2 倍递增参数量可控Dropout0.5数据少时先设 0.5过拟合严重再加大learning_rate1e-3Adam 默认学习率换 SGD 时降到 1e-2 以下batch_size32显存不够就降到 16一般保持 2 的幂2.3 训练时只盯 accuracy 会漏掉什么训练代码不复杂但监控指标要选对。model.fit( train_dataset, epochs30, validation_dataval_dataset, callbacks[ tf.keras.callbacks.EarlyStopping( monitorval_loss, patience5, restore_best_weightsTrue ), tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience2, min_lr1e-6 ), ], )EarlyStopping 的 monitor 我建议用 val_loss 而不是 val_accuracy。原因是准确率在类别不平衡时会“假高”如果数据里 happy 占 70%模型全部预测 happy 也有 70% 准确率但 val_loss 依然很高。ReduceLROnPlateau 在 val_loss 连续 2 个 epoch 不下降时把学习率减半这是最朴素也最有效的收敛技巧。很多入门教程不会写这一行但实际训练里它的作用比调网络结构还明显。3. 图片数据集的组织与增强喂给 CNN 之前要做的事3.1 数据集目录结构宁可多建文件夹不要一张大表拿到一个含图片数据集的压缩包第一步不是解压后直接训练而是先把它整理成 Keras 的 image_dataset_from_directory 能直接读取的目录结构。常见做法是每个类别一个文件夹文件夹名就是标签。dogs_dataset/ train/ happy/ happy_001.jpg happy_002.jpg angry/ sleepy/ neutral/ val/ happy/ angry/ sleepy/ neutral/这种结构的好处是目录名自动成为标签不会因为 Excel 表排序错位导致标签和图片对不上后续做数据增强、按类别统计数量也方便。一个常见坑是压缩包里的文件名可能是中文或者带空格比如“金毛 开心.jpg”读取时容易出现编码问题最好在解压后统一重命名为英文加编号这一步用一段简单的 Python 脚本做掉不要手工改。3.2 用 image_dataset_from_directory 读取并划分数据集train_ds tf.keras.utils.image_dataset_from_directory( dogs_dataset/train, validation_split0.2, subsettraining, seed42, image_sizeIMG_SIZE, batch_size32, ) val_ds tf.keras.utils.image_dataset_from_directory( dogs_dataset/train, validation_split0.2, subsetvalidation, seed42, image_sizeIMG_SIZE, batch_size32, )这段代码里 validation_split0.2 表示从训练目录里分出 20% 作为验证集注意两个读取必须用同一个 seed否则训练集和验证集会重叠。image_size 会直接把图片 resize 到 128×128不需要手动预处理。还有一个坑class_names 的顺序是按目录名的字母表排序的不是按你创建文件夹的顺序。如果目录是 angry、happy、neutral、sleepy那么类别索引 0 是 angry索引 1 是 happy后面打印结果或者画混淆矩阵时一定要按这个顺序对齐否则会看到“预测错了一半”实际只是标签名字贴错位。3.3 数据增强的两个方向几何变换和颜色扰动狗狗表情识别的难点在于同一只狗在不同角度、不同光线下表情差异可能比不同狗之间的差异还大。数据增强是缓解这个问题最直接的手段推荐直接使用 Keras 的预处理层放在模型里而不是在数据管道里做训练时有效、预测时自动关闭。data_augmentation tf.keras.Sequential([ layers.RandomFlip(horizontal), layers.RandomRotation(0.1), layers.RandomZoom(0.1), ])增强层加到 Rescaling 之后、第一个卷积之前即可。水平翻转对狗脸是安全的因为狗脸左右对称表情不会因为翻转而变含义。RandomRotation 和 RandomZoom 控制在 0.1 左右超过 0.2 会让耳朵、嘴巴等关键部位裁出画面反而引入噪声。颜色类增强比如亮度和对比度扰动用在表情识别上有风险同一个“开心”的表情在背光环境下可能因为亮度被拉高而变成“警戒”。所以这里我一般只用几何增强颜色扰动等数据量明显不够时再少量加入。3.4 Python 环境配置的常见坑如果你是先搜了“python安装教程”再装的 Python很可能机器里有多个 Python 版本。VSCode 里能 import tensorflow但终端运行 python train.py 却报 ModuleNotFoundError这就是解释器选错了。建议新建一个虚拟环境不要在系统 Python 上直接装深度学习依赖。conda create -n dog_cnn python3.10 conda activate dog_cnn pip install tensorflow matplotlib numpy用 conda 而不是直接 pip 装全局好处是后续跑不同项目时互不干扰。深度学习环境配置最容易出问题的是 TensorFlow 版本和 Python 版本不匹配所以先固定 Python 3.10再装 tensorflow正常会带一个匹配的稳定版。PyCharm 用户记得在项目解释器里选到 conda 环境的 python.exe不要依赖默认的 Project Interpreter。4. 从基线到可交付迁移学习与 3 个必调超参数4.1 用 ResNet50V2 做特征提取器自定义分类头3 层 CNN 在几百张狗狗表情图上通常只能跑出 60% 到 70% 的准确率再往上很吃力。这时换迁移学习是常规动作把在 ImageNet 上预训练好的 ResNet50V2 拿来去掉它原来的全连接分类头保留卷积部分作为特征提取器再接一个小的自定义分类头。from tensorflow.keras.applications import ResNet50V2 from tensorflow.keras.applications.resnet_v2 import preprocess_input base_model ResNet50V2( include_topFalse, weightsimagenet, input_shape(160, 160, 3) ) base_model.trainable False # 先冻结主干 inputs tf.keras.Input(shape(160, 160, 3)) x preprocess_input(inputs) x base_model(x, trainingFalse) x layers.GlobalAveragePooling2D()(x) x layers.Dropout(0.3)(x) x layers.Dense(128, activationrelu)(x) x layers.Dropout(0.2)(x) outputs layers.Dense(NUM_CLASSES, activationsoftmax)(x) model tf.keras.Model(inputs, outputs) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-4), losssparse_categorical_crossentropy, metrics[accuracy], )这里有两个细节值得展开。preprocess_input 是 ResNet 系列专属的预处理函数它和之前基线里的 Rescaling 不一样用的是 ImageNet 的均值和方差做标准化不能混用。base_model 设置为 trainableFalse意思是预训练权重全部冻结只训练后面的分类头。原因是狗狗表情数据和 ImageNet 相比数据量太小如果一开始就解冻全部主干几十步迭代就会把学好的通用特征破坏掉这个现象在迁移学习里叫灾难性遗忘损失反而比冻结时更高。ImageNet 里没有“狗的表情”这个标注但它有大量狗的品种分类数据所以网络底部已经学会了狗脸、毛发纹理、耳朵形状这些通用特征。我们训练时只需要让最后的分类头学会“把这些特征组合成情绪判断”这就是迁移学习在这个任务上比从头训练 3 层 CNN 更稳的原因。4.2 三个必调超参数学习率、Dropout、输入尺寸迁移学习调参比调结构更有效重点说三个参数。参数范围我的经验learning_rate1e-4 ~ 1e-3冻结主干用 1e-3 也能稳定收敛解冻后必须降到 1e-5Dropout0.2 ~ 0.5训练集不足 500 张时用 0.4图片量大到 5000 张可降到 0.2输入尺寸160 ~ 224迁移网络优先 224显存不够就用 160精度差距在 1% 以内学习率是这个任务里影响最大的参数。基线 CNN 用 1e-3 没问题但迁移学习的分类头是随机初始化的主干是预训练好的两部分对学习率的敏感度完全不同。1e-3 会让分类头快速收敛但同样幅度的更新应用在主干上就是剧烈扰动。所以冻结阶段我用 1e-4解冻阶段降到 1e-5。Dropout 的两个位置作用不同GlobalAveragePooling2D 之后的 Dropout 抑制分类头的过拟合Dense(128) 之后的 Dropout 相当于对特征组合做二次随机采样数据量越小这个值越该调大。输入尺寸从 128 提升到 224 并不是必须的。ResNet50V2 的池化层输出会和输入尺寸成正比224 比 160 的参数多约一倍但狗狗表情这种任务关键信息集中在嘴巴和眼睛区域160 已经足够。我的做法是先用 160 跑通全部流程最后如果时间允许再跑 224 对比一次。4.3 解冻主干第二段训练的启动方式冻结主干训练 10 到 15 个 epoch 后val_loss 会进入平台期。这时可以解冻主干的后半部分做微调。base_model.trainable True for layer in base_model.layers[:-30]: layer.trainable False model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-5), losssparse_categorical_crossentropy, metrics[accuracy], )解冻一半而不是全部是考虑到狗狗表情数据量远小于 ImageNet靠后的层已经编码了非常具体的高级语义改了容易过拟合。只解冻最后 30 层相当于只对接近分类头的部分做小幅调整。微调阶段必须重新 compile因为 trainable 的修改不会在已有 optimizer 里生效。这段训练通常只需要 5 到 8 个 epoch早停设置 patience3 就够。4.4 常见失败的排错方向如果迁移学习效果反而不如基线按顺序检查三个方向。第一类别是否严重不平衡例如 angry 只有 30 张而 happy 有 300 张需要在数据层面做复制少量类别的操作或者给损失函数加 class_weight直接训练加权交叉熵。第二图片内容与标签是否一致这是表情识别特有的坑人类对狗的表情判断本身就有主观性如果标注者把“张嘴喘气”标成“开心”而狗其实只是在散热模型会一直被这个噪声带偏表现为主的类别准确率低。第三输入图片如果没有做脸部裁剪模型很可能学到的是背景和场景信息例如室外草地对应开心、笼子里对应难过换个环境就全部失效。所以拿到一个号称含图片数据集的压缩包先抽样看 20 张图确认标签分布和图像内容比直接训练更重要。5. 用手机拍一张照片做推理置信度阈值与误判诊断5.1 单张图像推理脚本训练完成后把模型在真实场景里验证一遍这一步必须用自己拍的、不在数据集里的照片而不是从网上再找一张和训练集同源的图片。import numpy as np from tensorflow.keras.preprocessing import image CLASS_NAMES [angry, happy, neutral, sleepy] TARGET_SIZE (160, 160) img image.load_img(live_dog.jpg, target_sizeTARGET_SIZE) arr image.img_to_array(img) arr np.expand_dims(arr, axis0) pred model.predict(arr, verbose0)[0] idx int(np.argmax(pred)) print(f{CLASS_NAMES[idx]} : {pred[idx]:.3f}) print(pred)这个脚本里最容易忽略的是 target_size 要与训练时一致。如果训练用 160推理时传 128输入尺寸变化会直接导致模型报错或者输出异常概率。我一般会在脚本顶部把 TARGET_SIZE 单独提出来和训练脚本共用同一个配置常量而不是在推理脚本里再写一遍数字。pred 数组里是所有类别的概率分布不只是最大值的那个打印出来能看到模型在最有把握的两个类别之间犹豫的程度。5.2 置信度阈值宁可拒识也不硬猜动物表情识别和人类表情识别一样都存在大量“不确定”的情况狗半眯着眼可能是困了也可能只是阳光刺眼。模型 softmax 输出 0.9 和 0.45 都指向同一个类别但可靠程度完全不同。一个直接的做法是加置信度阈值低于阈值的样本不输出类别。THRESHOLD 0.6 if pred[idx] THRESHOLD: print(low confidence, skip) else: print(f{CLASS_NAMES[idx]} : {pred[idx]:.3f})阈值设置在 0.5 到 0.7 之间具体取决于应用场景。如果是宠物摄像头推送通知0.6 是合适的宁可漏报也不要频繁误报如果只是离线分析录像可以降到 0.4把更多低置信度样本留给人工复核。这个阈值不是拍脑袋定的应该从验证集上统计所有错误预测对应的概率分布把阈值定在“错误样本概率普遍低于正确样本”的分界处。5.3 用混淆矩阵定位高频误判只报告单张结果无法指导模型改进。把验证集全部预测一遍看哪些类别之间互相混淆这是最后一层调试。如果结果集中在 happy 被误判为 neutral多半是训练集里这两类图片的光线分布不一致例如 happy 全是户外亮图neutral 全是室内暗图模型学会了亮度而不是表情。如果误判成对出现且概率几乎相等说明这两个类别的图像内容在数据集中本身就高度重合例如“警戒”和“难过”都可能表现为耳朵后贴这时应该考虑合并类别而不是硬调模型。每次调整数据或模型后都重新跑一遍推理脚本观察的是相对变化而不是单次准确率这一类数据集项目的边界和手感就是这么磨出来的。本文还有配套的精品资源点击获取