尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ResNet50猪脸识别APP落地:数据、压缩与部署全解析

ResNet50猪脸识别APP落地:数据、压缩与部署全解析 简介一份基于ResNet50模型的猪脸识别APP设计研究PDF论文面向计算机视觉、深度学习及农业信息化方向的开发者、研究者和相关专业学生兼具参考文献与专业指导价值。资源包内为1个PDF文件体积2.03MB包含论文正文、图表与实验数据方便直接阅读与引用。目前已有308人学习浏览。论文针对传统猪只识别中RFID标签方案耗时长、成本高、耳标易丢失等痛点系统阐述了完整技术路径使用OpenCV对视频流分帧借助labelImg完成猪脸标注并将数据集按3:1:1划分为训练集、验证集、测试集用Keras数据生成器做旋转扩充在ResNet50网络末尾增加最大池化、Dropout与全连接层通过对比学习率发现lr1e-4时损失函数收敛最快验证集准确率达到92%左右APP以MUI与Django为框架实现登录注册、天气气温、猪只信息修改、照片识别等模块。读者可从中获得数据采集与预处理、深度学习模型优化、移动端与服务端联调的一体化设计思路对毕业设计、论文写作及相关项目研发均有较高参考价值。1. ResNet50 不是拿来即用的猪脸识别 APP 要过数据、部署、个体检索三道关养殖场想按个体管理猪耳标会掉、会磨损靠人眼认猪又不可靠。用手机对着猪脸拍一张照片几秒内返回个体 ID、日龄和免疫记录这就是猪脸识别 APP 的典型诉求。ResNet50 作为骨干网络搭配移动端推理引擎是当前做畜禽个体视觉识别的常见起点也是很多算法工程师复现论文时的第一选择。这篇笔记按我的实践路径来写从数据集、训练参数、模型压缩、手机端部署到排错把这套方案从“能跑通”讲到“能上线”。2. 猪脸数据采集与 ResNet50 训练样本怎么来模型怎么改先立住原理ResNet50 在 ImageNet 上预训练可以用在猪脸个体识别上但前提是把它从 1000 类改造成个体分类器。猪作为个体识别对象数据分布和常规目标识别很不一样——同一头猪在不同时段、不同光照、不同角度下的外观差异可能比不同猪之间的差异还大。这也是为什么不能下载一个 ResNet50 跑通一通就交付。很多人会去搜 ResNet50 网络结构示意图实际落地时结构改动不大重点反而在数据怎么组织。2.1 采集端固定机位和移动端是两套数据逻辑数据来源一般分两类。一是养殖场通道或称重站安装固定摄像头猪经过时触发拍摄画面背景相对干净角度基本固定。二是 APP 端用户手持手机拍摄视角随意背景复杂经常有栏杆、饲料槽、其他猪只入画。如果只针对第一种来源训练模型很可能在手机实拍时翻车。采集时要覆盖几个变化维度不同时段的光照、不同季节的皮毛状态、不同拍摄距离和俯仰角度以及猪脸沾泥、带耳标、闭眼张嘴这类真实状态。每个个体建议收集 30 到 50 张有效人脸图。以 50 头猪起步算数据量在 1500 到 3000 张左右。如果只有 10 头猪、每头 10 张图模型能跑起来但泛化能力非常差换一头没见过的猪基本就废了。数据划分有个容易犯的错按图片随机分 train 和 val。同一头猪的连续帧照片高度相关随机划分会让验证集虚高线上表现远达不到测试水平。正确做法是按个体划分同一头猪的照片不能既出现在训练集又出现在验证集更严格一点还要按时间段切前两周的照片训练后两周的照片验证。2.2 标注粒度个体分类和检测框怎么衔接猪脸识别在实际工程里分两步第一步用目标检测算法把猪脸从画面中框出来第二步用分类或识别模型确认个体。ResNet50 负责第二步。第一步常见做法是用 YOLOv5s 或更轻量的检测模型输出人脸框按框裁剪再缩放到 224×224 送给识别模型。标注建议用 CVAT 或 LabelImg先给猪脸打框导出 COCO 或 VOC 格式然后写脚本把标注框裁剪、清洗、按 ID 分目录。关键点是标签不能只写“猪”要写个体编号比如 P001、P002。分类用的目录结构大致如下dataset/pig_faces/train/P001/xxx.jpg dataset/pig_faces/train/P002/xxx.jpg dataset/pig_faces/val/P001/xxx.jpg dataset/pig_faces/val/P002/xxx.jpg这种结构 PyTorch 的ImageFolder可以直接读取。如果前期已经做了检测模型也可以用检测结果把猪脸区域裁出来再按个体归档省去重复标注的人力。注意清洗环节要剔除模糊、严重遮挡、只有半张脸的图片这类样本对训练的干扰远大于帮助。2.3 训练脚本用 PyTorch 把 ResNet50 改成猪脸个体分类器这里给出我常用的训练脚本核心代码import torch import torch.nn as nn import torchvision.models as models from torchvision import transforms, datasets from torch.utils.data import DataLoader num_classes 50 # 换成你的猪个体总数 device torch.device(cuda if torch.cuda.is_available() else cpu) # 加载预训练 ResNet50替换最后一层全连接 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, num_classes) model.to(device) # 微调策略前四层冻结只训练 layer4 和 fc for name, param in model.named_parameters(): if name.startswith(layer4) or name.startswith(fc): param.requires_grad True else: param.requires_grad False # 数据增强猪脸图不要硬套 ImageNet 的 mean/std train_tf transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2), transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) ])逻辑说明这里冻结 backbone 前几层。如果每头猪样本量只有几十张全量微调容易过拟合只训练 layer4 和 fc能保留 ImageNet 学到的通用纹理特征收敛也更快。如果数据量达到每头猪 100 张以上可以放开 layer3。数据增强里的 RandomResizedCrop 模拟不同拍摄距离ColorJitter 模拟猪场光照变化这两项对猪脸识别的泛化帮助最大。参数说明学习率初始 1e-4优化器用 Adam 或 SGD with momentummomentum 设 0.9weight_decay 5e-4batch_size 取 32 或 64训练 60 到 80 轮。学习率调度用 CosineAnnealingLR降到 1e-6 为止。Normalize 我习惯用 0.5/0.5/0.5 而不是 ImageNet 的 mean/std因为猪脸图像整体灰度分布差异较大用 0.5 归一化在部署阶段不容易出错。训练后的评估不要只盯准确率要打印混淆矩阵重点看哪些个体经常互相认错。再给一段验证代码# 用验证集统计 top-1 / top-5 准确率 model.eval() all_top5, all_labels [], [] with torch.no_grad(): for images, labels in val_loader: images images.to(device) outputs model(images) _, preds torch.topk(outputs, k5, dim1) all_top5.append(preds.cpu().numpy()) all_labels.append(labels.cpu().numpy()) top1 sum([1 for pl, lbl in zip(all_top5, all_labels) if pl[0] lbl]) / len(all_labels) top5 sum([1 for pl, lbl in zip(all_top5, all_labels) if lbl in pl]) / len(all_labels)说明top-5 也要看因为猪脸识别在遮挡严重时 top-1 不稳定。APP 端交互可以设计成“返回 top-3 候选 人工确认”比只给一个硬结果更实用这和刷榜要的 top-1 是不同的工程指标。保存模型时保留最后一轮和最佳验证集两个 checkpoint后面做 ONNX 转换时用最佳那个。3. 从 PyTorch 到手机端推理ONNX 导出、NCNN 量化与 APP 集成训练好的 ResNet50 是 fp32 权重直接搬到手机上体积太大。常规落地路径是把 PyTorch 权重转成 ONNX再用 NCNN 框架转成手机端可加载的格式配合量化减小体积。3.1 ONNX 导出与验证固定输入尺寸是第一个坑先导出 ONNXimport torch device torch.device(cpu) model.load_state_dict(torch.load(resnet50_pigface.pth, map_locationcpu)) model.eval().to(device) dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, pigface_resnet50.onnx, input_names[input], output_names[output], opset_version11, dynamic_axesNone, do_constant_foldingTrue, )逻辑说明dynamic_axesNone的意思是输入尺寸固定为 224×224这是刻意为之。如果设置成动态尺寸NCNN 转换时容易报错手机端推理时还要动态分配内存。固定输入尺寸后模型在手机端的耗时和峰值内存都可预估也方便后续量化。这里有个容易翻车的点导出前必须把模型切到eval()模式包含 BatchNorm 层时尤其重要。否则导出的 ONNX 会把训练时的批量统计量写进图里推理结果直接乱掉。导出后必须做一致性验证import onnxruntime as ort import numpy as np sess ort.InferenceSession(pigface_resnet50.onnx, providers[CPUExecutionProvider]) onnx_output sess.run(None, {input: dummy_input.numpy()})[0] torch_output model(dummy_input).detach().numpy() print(np.max(np.abs(torch_output - onnx_output)))说明差值应该在 1e-4 量级。如果发现差值在 0.1 以上基本是预处理不一致或者 BatchNorm 统计量没被冻结。建议先查model.eval()是否执行再查图片加载时的通道顺序和归一化参数。这个验证步骤不要跳很多后续 NCNN 转换的玄学问题都能在这一步拦下来。注意ONNX 的输入输出名要和后续代码里的 input / output 完全对应否则加载时会找不到张量。3.2 NCNN 转换与量化体积变小精度损失要盯紧转 NCNN常见做法是先简化 ONNX再转# 用 onnxsim 消除多余节点 python -m onnxsim pigface_resnet50.onnx pigface_resnet50_sim.onnx # 转 ncnn 格式得到 .param 和 .bin onnx2ncnn pigface_resnet50_sim.onnx pigface_resnet50.param pigface_resnet50.bin转换完成后得到 param 和 bin 两个文件param 描述网络结构bin 存权重。模型能加载但不做量化的话体积还是偏大需要在手机端流畅运行还得再压。NCNN 支持 fp16 和 uint8 量化命令如下# fp16精度损失小推荐优先尝试 ncnnoptimize pigface_resnet50.param pigface_resnet50.bin pigface_resnet50_fp16.param pigface_resnet50_fp16.bin 1 # uint8 量化体积最小但猪脸识别慎用 ncnn2table pigface_resnet50.param pigface_resnet50.bin calibration_images.txt mean 0.5 0.5 0.5 norm 0.5 0.5 0.5 shape 224 224 pixel BGR thread 4 ncnn2int8 pigface_resnet50.param pigface_resnet50.bin pigface_resnet50_int8.param pigface_resnet50_int8.bin table逻辑说明ncnnoptimize最后一个参数 1 表示 fp16 模式。ncnn2table需要一张校准图列表从验证集抽 200 张左右有代表性的图片每行写一个路径。uint8 量化会把权重和激活都压到 8 bit模型体积从 33 MB 压到大约 8 MB但猪脸是细粒度识别特征差异本来就细微我实测 top-1 会掉 3 到 5 个百分点还出现过个别个体彻底无法区分的情况。所以我的建议是优先 fp16只有在存储或内存紧张时才考虑 uint8。参数说明mean和norm必须和训练时的预处理完全一致。训练用 0.5/0.5/0.5这里就写 0.5 0.5 0.5像素通道顺序也要确认Android 端读到的 Bitmap 通常是 RGB我统一在代码层转成 BGR 再喂给模型避免转换链路上出现通道错位。3.3 Android 端集成用 NCNN Java API 封装猪脸识别Android Studio 里常见做法是引入 NCNN 的预编译库param 和 bin 文件放 assets 目录Kotlin 侧调用示例class PigFaceRecognizer { private val net ncnn.Net() init { net.loadParam(pigface_resnet50_fp16.param) net.loadModel(pigface_resnet50_fp16.bin) net.opt.useGpu false net.opt.numThreads 4 } fun recognize(bitmap: Bitmap): IntArray { val scaled Bitmap.createScaledBitmap(bitmap, 224, 224, true) val pixels IntArray(224 * 224) scaled.getPixels(pixels, 0, 224, 0, 0, 224, 224) // 转浮点数组注意 NCNN 需要 CHW 顺序 BGR val input FloatArray(3 * 224 * 224) for (i in pixels.indices) { val r (pixels[i] shr 16 and 0xFF) / 255.0f val g (pixels[i] shr 8 and 0xFF) / 255.0f val b (pixels[i] and 0xFF) / 255.0f input[0 * 224 * 224 i] (b - 0.5f) / 0.5f input[1 * 224 * 224 i] (g - 0.5f) / 0.5f input[2 * 224 * 224 i] (r - 0.5f) / 0.5f } val extractor net.createExtractor() extractor.input(input, ncnn.Mat(input)) val out ncnn.Mat() extractor.extract(output, out) return out.topk(5) // 返回 top-5 个体 ID } }逻辑说明这段代码把 Bitmap 拆成 RGB从 0-255 转到 0-1减均值除方差后放进 NCNN 的 Mat。注意通道排列是 CHW第一块存 B、第二块存 G、第三块存 R和训练时的数据预处理顺序保持一致。很多人在这一步翻车就是把 RGB 当 BGR 喂进去输出结果牛头不对马嘴。参数说明useGpu false是我踩过坑后的选择部分国产手机的 GPU 后端兼容性一般CPU 推理在 ResNet50 224 输入下单帧大约 80 到 180 毫秒满足拍照识别的使用节奏。如果要做视频流实时识别建议把输入裁到 160 或换轻量网络那已经是另一个方向了。4. 猪脸识别排错实战四类高频坑与排查路径这套流程跑通不难难的是在真实猪场里识别率不达标。我把排查路径整理成四条按现象、原因、处理来写。4.1 过拟合在猪脸数据上特别容易发生val 准确率虚高现象训练集准确率接近 100%验证集准确率也不低线上实拍却频繁翻车。原因把同一头猪的连续帧图片按张随机分配进了 train 和 val导致验证集里大多是和训练集高度重复的同场景图片模型其实没学会泛化只是记住了场景。解决数据划分严格按个体分同一头猪的照片不能同时出现在 train 和 val。更严格的做法是按时段划分前两周的照片训练后两周的照片验证能直接反映模型对时间变化的鲁棒性。补数据时也要按这个逻辑归档不要图省事随手拖几百张图进去。4.2 换一台手机拍摄识别率崩了现象在开发机上识别正常同事用低端 Android 机对着同一头猪拍识别率立刻下降。原因开发机的摄像头位置、白平衡和训练数据里的固定机位拍摄差异大低端机的成像噪声和色偏会把细节破坏。说白了就是训练分布和线上分布不一致。解决采集阶段就加入手持手机拍摄的样本数据增强里把 ColorJitter 的亮度抖动调大RandomResizedCrop 的范围调宽。APP 端加一个拍摄辅助框强制用户把猪脸对准框中央再拍照减少无效输入。还有一个做法是把 APP 内确认过的照片回流到训练集形成数据闭环这是最有效但最容易被忽略的一步。4.3 ONNX 转 NCNN 之后输出完全不对现象PyTorch 识别正确转换后的 NCNN 模型输出概率接近均匀分布或者 top-1 随机跳。原因模型导出时没切 eval 模式BatchNorm 统计值被导出成训练状态或者转换命令里的 mean/norm 参数和代码里的预处理不一致也可能是 Android 代码里通道顺序错了。解决先重跑一遍 ONNX 导出确认model.eval()已调用再到 NCNN 转换命令里核对 mean、norm、shape 和 pixel 类型为 BGR最后检查 Java/Kotlin 代码里的预处理是否一致。如果还不对把 NCNN 输出和 ONNX Runtime 输出逐类对比定位是从哪一层开始发散。4.4 同一头猪洗澡前后被识别成不同的个体现象用户反馈同一头猪今天认出来了明天又变成另一个 ID。原因猪脸识别本质是表观识别洗澡后毛色、泥污区域完全变化模型提取的特征和底库里的参考特征差距太大这是视觉方法的边界不是改几个参数能解决的。解决APP 端不要只返回硬 top-1要返回 top-3 候选让饲养员在界面上确认。同时提供“更新底库照片”入口把确认后的照片重新提取特征替换掉旧特征。这个产品兜底比单纯调模型更管用也是实际项目中猪场能接受的工作方式。5. 从“能识别”到“认得准”加一个 ArcFace 度量学习分支前面用 ResNet50 做个体分类softmax 交叉熵能解决固定个体集合的问题但真实猪场的猪会出栏、增群个体集合一直在变。每次加一头猪都要改分类器重训整个模型非常痛苦。度量学习可以解决这个问题把 ResNet50 改成特征嵌入网络每头猪对应一个特征向量识别时算余弦相似度找最近邻。ArcFace 是我常用的方案。网络结构上把最后一层 fc 去掉换成一个 Embedding 层输出 128 或 256 维特征训练时用一个带 angle margin 的分类损失来约束特征分布class ArcFaceHead(nn.Module): def __init__(self, feat_dim, num_classes, s64.0, m0.5): super().__init__() self.s s self.m m self.weight nn.Parameter(torch.Tensor(feat_dim, num_classes)) nn.init.kaiming_normal_(self.weight) def forward(self, emb, labels): emb_norm nn.functional.normalize(emb, p2, dim1) w_norm nn.functional.normalize(self.weight, p2, dim0) cos_theta torch.mm(emb_norm, w_norm) one_hot torch.zeros_like(cos_theta).scatter_(1, labels.view(-1, 1), 1.0) cos_theta_m cos_theta - self.m * one_hot return self.s * cos_theta_m # 训练时backbone 输出128维特征再传给 ArcFaceHead feat model(image) # shape: (N, 128) logits arcface_head(feat, labels) loss nn.CrossEntropyLoss()(logits, labels)说明s64是特征尺度m0.5是角度间隔这两个参数是常用组合一般不需要大幅调整。模型收敛后每头猪在底库里有几张参考特征APP 推理时对查询特征做归一化返回余弦相似度最高的个体和分数。验证方法把数据集拆成 gallery底库和 query查询两部分统计 top-1 召回率。我习惯底库里每头猪只放 3 张照片其余全部进 query这样更接近实际场景。做过一次对比同样数据量下ArcFace 的 top-1 通常比纯 softmax 高 2 到 4 个点更重要的是新个体入库不需要重训模型只是往底库表里加几条特征向量这个优势在长期运营中很值钱。这套流程做完你已经有一条从数据采集、模型训练、模型压缩到 APP 部署、线上排错、增量入库的完整链路。猪脸识别这个方向技术栈相对固定难得不是模型结构而是数据组织和工程细节。希望这篇笔记能帮你在做同一方向时少走几步弯路。本文还有配套的精品资源点击获取
返回列表