
简介这份资源面向希望入门深度学习与小程序开发的初学者及在校学生提供一套完整的猫狗表情识别实战方案帮助读者打通从数据预处理、模型训练到服务端部署的全流程。压缩包共492个文件约30.88MB其中468张jpg图片构成分类数据集另有py训练脚本、json配置、js与wxml/wxss小程序页面文件及txt说明文档覆盖数据、模型与前端展示各环节。代码基于Python与PyTorch环境依次运行数据集文本生成、深度学习模型训练和Flask服务端脚本即可完成训练与推理训练过程会保存模型与记录每个epoch验证集损失和准确率的日志。数据集预处理采用短边补灰边转正方形与旋转角度等方式扩增样本提升模型泛化能力。目前已有156人学习适合作为课程设计、毕业项目或深度学习练手参考。1. 小程序版猫狗表情识别从一张图片到微信端推理的完整链路微信小程序里做图像识别很多人第一反应是调云端 API但一旦涉及猫狗表情这种细分场景通用接口的准确率往往撑不住。这个标题指向的方案是用 Python 深度学习训练一个猫狗表情分类模型再把模型部署到小程序端做本地推理配套一份图片数据集。它解决的核心问题是——不依赖网络请求、不上传用户图片、在小程序里直接完成识别。适合谁有 Python 基础、想入门深度学习落地、又希望产物能直接在微信里跑起来的开发者。数据集是起点模型是中间产物小程序是最终载体三者缺一不可。下面按「数据怎么整 → 模型怎么训 → 小程序怎么接 → 坑在哪」的顺序拆开讲。2. 数据集处理与猫狗表情分类的标签体系设计2.1 猫狗表情类别怎么定先看数据再定标签拿到一份猫狗图片数据集第一步不是急着写 DataLoader而是先搞清楚里面有什么。常见做法是猫和狗各分若干表情类别比如猫的「正常、炸毛、眯眼、张嘴」狗的「正常、龇牙、歪头、吐舌」。但实际数据集往往没这么规整可能只有「猫」「狗」两个大类表情标签需要你自己标注或通过规则生成。我一般会先跑一段统计脚本看看每个类别的样本数量和分辨率分布import os from collections import Counter from PIL import Image data_root dataset/train counter Counter() size_list [] for cls_name in os.listdir(data_root): cls_dir os.path.join(data_root, cls_name) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): fpath os.path.join(cls_dir, fname) try: img Image.open(fpath) size_list.append(img.size) counter[cls_name] 1 except Exception as e: print(f坏图: {fpath}, 原因: {e}) print(类别分布:, counter) print(分辨率样本数:, len(size_list)) print(最小尺寸:, min(size_list), 最大尺寸:, max(size_list))这段脚本做三件事统计每个类别的图片数量、检测无法打开的坏图、记录分辨率范围。参数上data_root指向你的训练集根目录目录结构要求是train/类别名/图片文件。如果类别分布严重不均比如某个表情只有十几张后续训练必须做重采样或数据增强否则模型会偏向多数类。2.2 统一尺寸与增强策略别让分辨率成为隐形杀手猫狗图片的原始分辨率可能从 100×100 到 2000×2000 不等。直接 resize 到 224×224 是最省事的做法但如果原图长宽比差异大强行拉伸会让猫脸变扁、狗嘴变宽表情特征直接失真。更稳的做法是「短边缩放 中心裁剪」from torchvision import transforms train_transform transforms.Compose([ transforms.Resize(256), # 短边缩到256保持长宽比 transforms.CenterCrop(224), # 中心裁剪到224×224 transforms.RandomHorizontalFlip(), # 随机水平翻转增加多样性 transforms.ColorJitter(brightness0.2, contrast0.2), # 轻度颜色扰动 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) # ImageNet统计值 ]) val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])训练集用随机翻转和颜色扰动验证集只做确定性变换。Normalize的均值和标准差用的是 ImageNet 的统计值因为后面要加载预训练权重输入分布必须对齐。如果你的数据集整体偏暗或偏亮可以自己算一遍均值和标准差替换掉但差异不大时没必要折腾。注意数据增强只在训练集做验证集和测试集绝对不能加随机变换否则评估结果会飘。2.3 训练集/验证集/测试集的划分比例与目录结构常见做法是 7:2:1 或 8:1:1。如果某个表情类别样本少于 100 张建议用 8:1:1把更多数据留给训练。目录结构推荐dataset/ ├── train/ │ ├── cat_normal/ │ ├── cat_angry/ │ ├── dog_normal/ │ └── dog_angry/ ├── val/ │ ├── cat_normal/ │ └── ... └── test/ ├── cat_normal/ └── ...每个类别文件夹下放对应图片。这种结构可以直接用torchvision.datasets.ImageFolder加载不需要自己写 Dataset 类。划分时要注意同一只猫或狗的不同照片不能同时出现在训练集和验证集里否则验证准确率会虚高。如果数据集里没有个体 ID 信息至少保证同一来源的图片分到同一侧。3. 用迁移学习训练猫狗表情分类模型3.1 为什么选 ResNet18 而不是从零搭 CNN猫狗表情识别的数据量通常不大几千到几万张。从零训练一个 CNN卷积核参数随机初始化需要大量数据才能收敛到合理精度。迁移学习用 ImageNet 预训练的 ResNet18底层卷积已经学会了边缘、纹理、形状等通用特征只需要微调高层就能适配表情分类。ResNet18 参数量约 1100 万模型文件约 45MB转成 ONNX 后还能压到 20MB 以内适合放进小程序包。选型对比模型参数量模型大小适合场景ResNet1811M~45MB数据量中等精度与体积平衡MobileNetV35.4M~22MB追求极致轻量精度略低EfficientNet-B05.3M~21MB精度较高但推理框架兼容性需验证自建CNN可调可调数据量10万张时考虑我一般先用 ResNet18 跑基线如果小程序包体积超限再换 MobileNetV3。3.2 训练脚本冻结策略、学习率与早停import torch import torch.nn as nn from torchvision import models, datasets, transforms from torch.utils.data import DataLoader device torch.device(cuda if torch.cuda.is_available() else cpu) # 数据加载 train_ds datasets.ImageFolder(dataset/train, transformtrain_transform) val_ds datasets.ImageFolder(dataset/val, transformval_transform) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) # 模型加载预训练权重替换最后一层 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_classes len(train_ds.classes) model.fc nn.Linear(model.fc.in_features, num_classes) model model.to(device) # 冻结底层只训练fc层和前两个stage for name, param in model.named_parameters(): if layer3 not in name and layer4 not in name and fc not in name: param.requires_grad False # 损失函数与优化器 criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, patience3) best_acc 0.0 patience_counter 0 for epoch in range(30): model.train() running_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() # 验证 model.eval() correct, total 0, 0 val_loss 0.0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) val_loss criterion(outputs, labels).item() _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) acc correct / total scheduler.step(val_loss) print(fEpoch {epoch1}, Loss: {running_loss/len(train_loader):.4f}, Val Acc: {acc:.4f}) if acc best_acc: best_acc acc torch.save(model.state_dict(), best_model.pth) patience_counter 0 else: patience_counter 1 if patience_counter 7: print(早停触发) break关键参数说明lr1e-3是 Adam 的初始学习率冻结底层后这个值比较稳patience3是学习率调度器的耐心值验证损失 3 个 epoch 不降就降学习率patience_counter 7是早停阈值防止过拟合。冻结策略上只训练layer3、layer4和fc底层conv1到layer2保持 ImageNet 权重不动。如果数据量超过 5 万张可以解冻全部层做微调学习率降到 1e-4。3.3 导出 ONNX让模型能进小程序PyTorch 模型不能直接在小程序里跑需要转成 ONNX 格式再用微信小程序的推理框架加载。import torch from torchvision import models import torch.nn as nn # 重建模型结构 model models.resnet18(weightsNone) model.fc nn.Linear(model.fc.in_features, num_classes) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() # 构造示例输入 dummy_input torch.randn(1, 3, 224, 224) # 导出ONNX torch.onnx.export( model, dummy_input, cat_dog_expression.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, opset_version11 ) print(ONNX导出完成)opset_version11是兼容性较好的版本微信小程序推理框架对 11 和 12 支持较稳。dynamic_axes允许变长 batch实际部署时 batch 固定为 1 也行。导出后建议用onnxruntime跑一遍验证输出是否一致import onnxruntime as ort import numpy as np sess ort.InferenceSession(cat_dog_expression.onnx) test_input np.random.randn(1, 3, 224, 224).astype(np.float32) onnx_out sess.run(None, {input: test_input})[0] print(ONNX输出形状:, onnx_out.shape)如果 ONNX 输出和 PyTorch 输出差异超过 1e-4检查是否有不支持的操作被静默替换。4. 小程序端推理从 ONNX 到微信 Canvas 的完整对接4.1 小程序推理框架选型与模型加载微信小程序本身不直接支持 ONNX需要用微信官方的推理能力或第三方 WASM 方案。常见做法有两种一是用微信小程序的wx.createInferenceSession部分基础库版本支持二是用 ONNX Runtime Web 编译成 WASM 在小程序里跑。前者更轻量但兼容性受基础库版本限制后者更通用但包体积会增加 2-3MB。我一般先用官方推理接口试如果基础库版本不够再降级到 WASM。模型文件放在static/models/目录下通过wx.getFileSystemManager().readFile读取为 ArrayBuffer。// pages/recognize/recognize.js Page({ data: { result: , loading: false }, async onLoad() { this.session null; try { // 创建推理会话 this.session await wx.createInferenceSession({ model: static/models/cat_dog_expression.onnx, precisionLevel: 0, // 0FP32, 1FP16 typicalShape: [1, 3, 224, 224] }); console.log(推理会话创建成功); } catch (e) { console.error(推理会话创建失败, e); } }, async recognizeImage(imgPath) { if (!this.session) return; this.setData({ loading: true }); // 读取图片并预处理 const imgData await this.preprocess(imgPath); // 执行推理 const outputs await this.session.run({ input: { data: imgData, shape: [1, 3, 224, 224] } }); // 解析输出 const scores outputs.output.data; const maxIdx scores.indexOf(Math.max(...scores)); const labels [猫-正常, 猫-生气, 狗-正常, 狗-生气]; this.setData({ result: labels[maxIdx], loading: false }); } });precisionLevel: 0表示 FP32 精度如果模型体积太大可以改 1 用 FP16精度损失通常在 1% 以内。typicalShape必须和导出 ONNX 时的输入形状一致。4.2 图片预处理小程序端如何对齐训练时的变换训练时用了 Resize(256) CenterCrop(224) Normalize小程序端必须做同样的处理否则输入分布偏移会导致预测全错。小程序里可以用 Canvas 2D 做缩放和裁剪async preprocess(imgPath) { const canvas wx.createOffscreenCanvas({ type: 2d, width: 224, height: 224 }); const ctx canvas.getContext(2d); const img canvas.createImage(); await new Promise((resolve, reject) { img.onload resolve; img.onerror reject; img.src imgPath; }); // 短边缩放到256再中心裁剪224 const scale 256 / Math.min(img.width, img.height); const newW img.width * scale; const newH img.height * scale; const offsetX (newW - 224) / 2; const offsetY (newH - 224) / 2; ctx.drawImage(img, -offsetX, -offsetY, newW, newH); const imageData ctx.getImageData(0, 0, 224, 224); const { data } imageData; // 归一化HWC - CHW并应用mean/std const mean [0.485, 0.456, 0.406]; const std [0.229, 0.224, 0.225]; const float32 new Float32Array(3 * 224 * 224); for (let i 0; i 224 * 224; i) { for (let c 0; c 3; c) { const val data[i * 4 c] / 255.0; float32[c * 224 * 224 i] (val - mean[c]) / std[c]; } } return float32.buffer; }这段代码做了三件事短边缩放到 256、中心裁剪 224×224、像素值归一化并转成 CHW 排列的 Float32Array。mean和std必须和训练时完全一致差一点都会影响精度。getImageData返回的是 RGBA 四通道只取前三个通道。4.3 推理结果后处理与置信度展示模型输出是每个类别的 logits需要做 softmax 转成概率function softmax(logits) { const maxLogit Math.max(...logits); const exps logits.map(v Math.exp(v - maxLogit)); const sumExps exps.reduce((a, b) a b, 0); return exps.map(v v / sumExps); } // 在recognizeImage中调用 const probs softmax(Array.from(scores)); const maxIdx probs.indexOf(Math.max(...probs)); const confidence probs[maxIdx]; this.setData({ result: labels[maxIdx], confidence: (confidence * 100).toFixed(1) %, loading: false });置信度低于 60% 时建议提示用户「图片不够清晰或角度偏差较大建议重新拍摄」。这个阈值可以根据验证集上的表现调整如果验证集整体置信度偏低说明模型欠拟合或数据分布有差异。5. 避坑与排查猫狗表情识别落地时最容易翻车的五个点5.1 现象小程序里预测结果全是同一个类别原因预处理没对齐。训练时用了 Normalize小程序端如果忘了减均值除标准差或者通道顺序从 RGB 变成了 BGR模型输入分布完全变了输出会退化成常数。解决在小程序端打印预处理后的前 10 个像素值和 Python 端对同一张图的预处理结果逐位对比。差异超过 0.01 就说明变换没对齐。重点检查getImageData返回的是 RGBA 还是 RGB以及 Canvas 绘制时有没有做 Y 轴翻转。5.2 现象ONNX 模型在小程序里加载失败报「不支持的操作」原因PyTorch 导出 ONNX 时某些算子如自适应池化、自定义激活函数在微信推理框架里没有实现。解决导出后用onnxruntime先跑一遍再用onnx-simplifier做图优化。如果仍有不支持的操作把模型换成 MobileNetV3 或手动替换掉问题算子。常见做法是避免使用AdaptiveAvgPool2d以外的池化激活函数统一用 ReLU。5.3 现象验证集准确率 95%小程序实测只有 60%原因数据泄露或分布偏移。验证集和训练集可能来自同一批图片的相邻帧导致验证准确率虚高。另外小程序端用户拍摄的图片和数据集里的图片在光照、角度、背景上差异很大。解决重新划分数据集确保同一只猫或狗的照片只出现在一个集合里。在小程序端收集真实场景图片做一轮微调或者至少做一次测试集评估测试集必须完全独立。5.4 现象模型文件太大小程序包体积超限原因ResNet18 的 ONNX 文件约 45MB微信小程序主包限制 2MB总包限制 20MB。解决换 MobileNetV3 或对 ResNet18 做量化。ONNX 支持 INT8 量化可以把模型压到 10MB 以内。量化脚本from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( cat_dog_expression.onnx, cat_dog_expression_int8.onnx, weight_typeQuantType.QUInt8 )量化后精度通常掉 1-3 个百分点如果掉太多就改用 FP16。另外模型文件可以放在分包里通过wx.loadSubpackage按需加载。5.5 现象推理速度慢一张图要等 2-3 秒原因小程序端 CPU 推理本身较慢加上图片预处理用了同步 Canvas 操作阻塞了主线程。解决把预处理放到 Worker 里做推理也放到 Worker 里主线程只负责 UI 更新。另外把输入分辨率从 224 降到 192 或 160速度能提升 30%-50%精度损失通常在 2% 以内。如果还是慢考虑用微信的 GPU 推理接口部分机型支持。6. 进阶技巧用混淆矩阵定位猫狗表情识别的最弱类别模型训完之后光看总体准确率不够得知道哪些类别容易混。混淆矩阵是最直接的工具import numpy as np from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs imgs.to(device) outputs model(imgs) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) print(classification_report(all_labels, all_preds, target_namestrain_ds.classes)) sns.heatmap(cm, annotTrue, fmtd, xticklabelstrain_ds.classes, yticklabelstrain_ds.classes) plt.xlabel(预测) plt.ylabel(真实) plt.show()跑完看两件事一是哪个类别的召回率最低二是哪两个类别互相混淆最多。比如「猫-生气」和「猫-正常」经常混说明这两个表情的视觉差异不够明显需要补充更多区分度高的样本或者调整数据增强策略对这两个类别做针对性增强。另一个技巧是测试时增强TTA对同一张图做水平翻转和轻微缩放分别推理后取平均概率。TTA 通常能提升 1-2 个百分点代价是推理时间翻倍。如果小程序端性能允许可以只在置信度低于 70% 时触发 TTA兼顾速度和精度。我自己的习惯是每次训完模型先跑混淆矩阵把最弱的两个类别找出来然后针对性地补数据或调增强参数通常两三轮就能把最弱类别的召回率从 70% 拉到 85% 以上。模型部署到小程序后定期收集用户实际拍摄的图片做一轮增量训练比一次性追求高准确率更实际。希望帮到你。本文还有配套的精品资源点击获取