2026/10/11 11:34:35

猪脸识别工程实战:从数据集到推理脚本的完整落地路径

猪脸识别工程实战:从数据集到推理脚本的完整落地路径 简介这份资源是面向深度学习与计算机视觉学习者的猪脸识别工程文件及代码包基于目标检测思路实现猪只面部特征的检测与识别适合具备Python基础、希望了解深度学习在农业场景落地实践的中高级开发者参考。压缩包共34个文件约178.95MB以zip模块包为主辅以py脚本、分卷压缩文件及说明文档涵盖数据准备、模型定义、训练、评估与推理等环节并附有依赖与配置说明。资源围绕Objectdetectionapi-master展开包含数据集、预处理、模型结构、训练与测试脚本、推理接口及配置文件等模块可帮助读者理解YOLO、SSD、Faster R-CNN等检测算法在猪脸任务中的组织方式并思考品种差异、光照变化、遮挡等问题的处理思路。目前已有1573人学习下载适合作为深度学习目标检测项目实战与农业智能化应用的参考案例。1. 猪脸识别工程文件拆包从数据集到推理脚本的完整落地路径猪脸识别这个方向第一次听到的人多半会愣一下——人脸、车牌、条码都见过猪脸是个什么场景其实在规模化养殖里个体识别是刚需采食量、发情周期、体重曲线、用药记录全都得绑定到具体某一头猪身上。耳标会掉、会磨损、会被咬烂而猪脸不会。这套工程文件及代码解决的就是「用深度学习做猪脸个体识别」从数据到推理的全链路问题技术栈是 Python 深度学习框架覆盖数据标注、模型训练、评估、导出和单张图片推理。适合两类人一是想找一个非人脸生物特征识别项目练手的算法工程师二是养殖行业里想做智能化改造、需要一份可跑通基线代码的技术负责人。下面我按自己拆包复现的顺序把这份资源讲透。2. 工程目录与数据管线先搞清楚文件往哪放拿到一个深度学习项目包最怕的就是上来就python train.py然后满屏FileNotFoundError。猪脸识别这类项目目录结构决定了你后面改配置、换数据集顺不顺手。我一般会先把整个包解压用tree或者文件管理器把两级目录看一遍再决定从哪个脚本切入。2.1 典型目录结构与各文件职责这类工程常见的组织方式是按「数据—代码—配置—输出」四块切分下面是我拆过的包里边比较合理的一种布局pigface/ ├── configs/ │ └── default.yaml # 训练超参、路径、模型名集中在这里 ├── data/ │ ├── raw/ # 原始图片按个体 ID 分子文件夹 │ │ ├── pig_001/ │ │ ├── pig_002/ │ │ └── ... │ ├── train/ # 划分后的训练集 │ ├── val/ # 验证集 │ └── labels.csv # 图片路径与个体 ID 的映射表 ├── src/ │ ├── dataset.py # Dataset 类负责读图与增强 │ ├── model.py # 网络定义 │ ├── train.py # 训练入口 │ ├── evaluate.py # 验证集评估 │ └── infer.py # 单张/批量推理 ├── weights/ │ └── best.pth # 训练产出的权重 └── requirements.txt这个结构里configs/default.yaml是全局开关data/raw下按个体 ID 建子文件夹是最省事的标注方式——文件夹名就是类别名dataset.py直接遍历子目录生成标签不用额外写标注文件。如果你的数据是散在一堆图里、靠 CSV 记录标签那就得改dataset.py里的读取逻辑让它走labels.csv而不是ImageFolder。2.2 数据划分与增强策略猪脸数据和常见图像分类数据有个明显区别同一头猪在不同光照、不同姿态下的照片差异可能很大而不同猪在某些角度下又长得很像。所以划分数据集时不能纯随机我一般会保证每头猪在训练集和验证集里都有足够样本避免某头猪只在验证集出现导致指标虚低。# src/dataset.py 关键片段 import os import random from PIL import Image from torch.utils.data import Dataset, DataLoader from torchvision import transforms class PigFaceDataset(Dataset): def __init__(self, root_dir, transformNone): self.samples [] self.class_to_idx {} self.transform transform # 遍历每个个体文件夹文件夹名即类别 for idx, cls_name in enumerate(sorted(os.listdir(root_dir))): cls_dir os.path.join(root_dir, cls_name) if not os.path.isdir(cls_dir): continue self.class_to_idx[cls_name] idx for fname in os.listdir(cls_dir): if fname.lower().endswith((.jpg, .png, .jpeg)): self.samples.append((os.path.join(cls_dir, fname), idx)) def __len__(self): return len(self.samples) def __getitem__(self, index): path, label self.samples[index] img Image.open(path).convert(RGB) if self.transform: img self.transform(img) return img, label # 训练集增强随机裁剪、翻转、颜色抖动模拟猪只走动和光照变化 train_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.3, contrast0.3), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这段代码里class_to_idx把文件夹名映射成 0 到 N-1 的整数标签samples列表存的是「图片路径 标签」二元组。增强部分用了水平翻转、±15 度旋转和颜色抖动这几个是针对猪脸场景比较稳的组合——猪在栏里左右走动、低头抬头翻转和旋转能覆盖大部分姿态变化颜色抖动则对抗猪舍里忽明忽暗的灯光。注意Normalize用的还是 ImageNet 的均值和方差如果你从零训练而不是用预训练权重可以改成自己数据集的统计值但用预训练权重时保持默认就行。提示划分训练集和验证集时建议按个体分层抽样而不是把所有图片混在一起随机切。否则可能出现某头猪的图片全在训练集、验证集里一头都没有的情况指标会好看但没意义。3. 模型选型与训练为什么用迁移学习而不是从零训猪脸识别本质上是一个细粒度分类问题——类别之间差异小类别内部差异大。从零训练一个卷积网络在几千张图的规模下基本训不动准确率会卡在很低的水平。所以这份工程里用的是迁移学习路线拿在 ImageNet 上预训练好的骨干网络换掉最后的全连接层用猪脸数据微调。3.1 骨干网络选择与修改常见做法是选 ResNet18 或 ResNet50 作为骨干。ResNet18 参数少、推理快适合边缘设备部署ResNet50 精度更高但显存占用和推理延迟都上去了。如果猪只数量在几十到几百头之间ResNet18 微调通常够用超过五百头、且个体之间花纹差异很小再考虑换 ResNet50 或 EfficientNet。# src/model.py import torch.nn as nn from torchvision import models def build_model(num_classes, backboneresnet18, pretrainedTrue): if backbone resnet18: model models.resnet18(pretrainedpretrained) in_features model.fc.in_features elif backbone resnet50: model models.resnet50(pretrainedpretrained) in_features model.fc.in_features else: raise ValueError(f不支持的骨干网络: {backbone}) # 替换最后的全连接层输出维度改为猪只个体数 model.fc nn.Linear(in_features, num_classes) return modelnum_classes就是你的猪只个体总数等于data/raw下的文件夹数量。pretrainedTrue会加载预训练权重第一次运行需要联网下载如果环境不能联网得提前把权重文件放到缓存目录。替换model.fc之后新加的全连接层是随机初始化的训练时这一层的梯度会比较大所以常见做法是给全连接层设一个更大的学习率或者先冻结骨干只训分类头几轮再解冻整体微调。3.2 训练循环与关键超参训练脚本的核心就是数据加载、前向传播、算损失、反向传播、更新参数这几步。猪脸识别用交叉熵损失就够了优化器选 Adam 或 SGD 都行我一般先用 Adam 跑通再换 SGD 调精度。# src/train.py 核心训练循环 import torch import torch.nn as nn from torch.optim import Adam from torch.utils.data import DataLoader from dataset import PigFaceDataset, train_tf from model import build_model device torch.device(cuda if torch.cuda.is_available() else cpu) train_ds PigFaceDataset(data/train, transformtrain_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) num_classes len(train_ds.class_to_idx) model build_model(num_classes, backboneresnet18).to(device) criterion nn.CrossEntropyLoss() # 分类头学习率设大一点骨干设小一点 optimizer Adam([ {params: model.fc.parameters(), lr: 1e-3}, {params: model.layer4.parameters(), lr: 1e-4}, ], lr1e-4) for epoch in range(30): model.train() total_loss, correct, total 0, 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() total_loss loss.item() * imgs.size(0) correct (outputs.argmax(1) labels).sum().item() total imgs.size(0) print(fEpoch {epoch1}, Loss: {total_loss/total:.4f}, Acc: {correct/total:.4f}) torch.save(model.state_dict(), weights/best.pth)这里有几个参数值得说。batch_size32是显存够的情况下的常用值显存不够就降到 16 或 8但太小会让 BatchNorm 统计不稳。num_workers4是数据加载的并行进程数设成 CPU 核心数的一半左右比较合适设太大反而会因为进程切换拖慢。优化器用了参数组分类头lr1e-3、layer4用lr1e-4、其余骨干用lr1e-4这是微调的常见做法——新层学得快一点预训练层动得慢一点避免把学到的通用特征冲掉。训练轮数 30 是个起点实际要看验证集准确率什么时候不再涨涨不动了就停别硬跑。注意每个 epoch 都覆盖保存best.pth其实不是最优做法应该只在验证集指标提升时才保存。上面为了代码简洁直接存了实际用的时候加一个best_acc变量做判断。4. 评估与推理指标怎么看、单张图怎么跑训练完不是看训练准确率就完事了训练集上的准确率没有任何参考价值——模型可能只是记住了训练样本。真正要看的是验证集上的表现以及推理脚本能不能对一张新图给出合理结果。4.1 验证集评估与混淆矩阵评估脚本要做的事加载权重、在验证集上跑一遍、算准确率和混淆矩阵。猪脸识别里混淆矩阵比单一准确率有用得多因为你能看出哪两头猪容易被搞混。# src/evaluate.py import torch from torch.utils.data import DataLoader from sklearn.metrics import confusion_matrix, classification_report from dataset import PigFaceDataset, train_tf from model import build_model device torch.device(cuda if torch.cuda.is_available() else cpu) val_ds PigFaceDataset(data/val, transformtrain_tf) val_loader DataLoader(val_ds, batch_size32, shuffleFalse) model build_model(len(val_ds.class_to_idx)).to(device) model.load_state_dict(torch.load(weights/best.pth, map_locationdevice)) model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs imgs.to(device) outputs model(imgs) preds outputs.argmax(1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, target_nameslist(val_ds.class_to_idx.keys()))) print(confusion_matrix(all_labels, all_preds))classification_report会给出每个类别的精确率、召回率和 F1confusion_matrix则是一个 N×N 的矩阵对角线是分对的非对角线是分错的。如果发现某两头猪之间互相错分特别多说明它们的脸部特征在模型看来太接近可以考虑给这两类多补一些不同角度的样本或者换更强的骨干网络。4.2 单张图片推理与结果解读推理脚本是最终交付给业务方用的东西输入一张猪脸图输出个体 ID 和置信度。# src/infer.py import torch from PIL import Image from torchvision import transforms from model import build_model CLASS_NAMES [pig_001, pig_002, pig_003] # 按训练时的顺序填 device torch.device(cuda if torch.cuda.is_available() else cpu) model build_model(len(CLASS_NAMES)).to(device) model.load_state_dict(torch.load(weights/best.pth, map_locationdevice)) model.eval() tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) img Image.open(test.jpg).convert(RGB) tensor tf(img).unsqueeze(0).to(device) with torch.no_grad(): logits model(tensor) probs torch.softmax(logits, dim1) conf, pred probs.max(1) print(f预测个体: {CLASS_NAMES[pred.item()]}, 置信度: {conf.item():.4f})CLASS_NAMES的顺序必须和训练时class_to_idx的映射一致否则预测出来的 ID 会张冠李戴。unsqueeze(0)是给单张图加一个 batch 维度因为模型期望输入是[N, C, H, W]。置信度低于某个阈值时业务上应该输出「未知个体」而不是硬给一个 ID这个阈值一般设在 0.6 到 0.8 之间看你对误识别的容忍度。5. 避坑与排查那些让我重跑训练的血泪经验这一章是我拆包复现过程中真正卡过的地方每条都按「现象 → 原因 → 解决」写你照着排查能省不少时间。现象一训练 loss 一直不降准确率停在随机水平。原因通常是标签映射错了。ImageFolder或自定义 Dataset 按文件夹名排序生成标签但推理时CLASS_NAMES手写的顺序和训练时不一致导致模型学的是 A 标签、你读出来是 B。另一种可能是数据增强里的Normalize用了错误的均值方差把输入分布搞乱了。解决训练前打印一次class_to_idx把它存成 JSON 文件推理时直接读这个文件不要手写类别名。现象二验证集准确率很高但实际拍的新图全错。这是典型的域偏移。训练图可能都是在固定光照、固定角度下拍的而实际场景里猪在动、光线在变。解决训练时增强力度加大尤其是亮度和对比度抖动如果条件允许在真实场景里补拍一批图加入训练集。另外检查一下验证集是不是和训练集来自同一批拍摄如果是那验证集指标参考价值有限。现象三显存溢出报 CUDA out of memory。原因可能是batch_size太大或者num_workers设太高导致每个 worker 都缓存了一份数据。解决先把batch_size降到 8 试再逐步往上加num_workers设成 2 或 4 就够别设成 CPU 核心数。如果还不行把图片输入尺寸从 224 降到 160 或 128显存占用会明显下降。现象四训练到一半 loss 突然变成 NaN。学习率太大是首要嫌疑。Adam 的默认学习率 1e-3 在某些数据上会炸尤其是分类头刚初始化的时候。解决把分类头学习率降到 1e-4或者加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0)。另外检查数据里有没有损坏的图片读进来是全黑或全白的也会导致 loss 异常。现象五推理速度慢单张图要几百毫秒。如果跑在 CPU 上这个速度正常。如果跑在 GPU 上还这么慢检查是不是每次推理都重新加载了模型权重。解决模型加载一次常驻内存推理时只做前向传播。另外可以开torch.no_grad()关掉梯度计算能省不少时间。6. 进阶技巧用特征向量做个体检索而不是死分类分类模型有个天然限制类别数在训练时就固定了。养殖场里猪只进进出出今天 50 头明天可能 55 头每加一头就重训一次模型不现实。更实用的做法是把模型当特征提取器用——去掉最后的分类层输出一个 512 维ResNet18或 2048 维ResNet50的特征向量然后用向量相似度做检索。具体操作是把model.fc换成一个恒等映射前向传播拿到池化后的特征对每头猪的注册图算一个平均特征向量存起来。新图来了算特征向量和库里所有向量算余弦相似度取最高的那个作为预测结果。这样加新个体只需要加一条特征记录不用重训。# 特征提取模式 import torch.nn as nn def build_embedder(backboneresnet18): model models.resnet18(pretrainedTrue) # 去掉最后的全连接层保留池化输出 model.fc nn.Identity() return model # 提取特征 embedder build_embedder().to(device).eval() with torch.no_grad(): feat embedder(tensor) # shape: [1, 512] feat feat / feat.norm(dim1, keepdimTrue) # L2 归一化L2 归一化之后余弦相似度就等价于点积算起来更快。检索时设一个相似度阈值比如 0.75低于这个值就判为「未知个体」避免把新来的猪硬塞给某个已有 ID。这个阈值需要根据你的数据调注册图越多、质量越好阈值可以设得越高。验证这套检索方案是否靠谱我一般会做一件事从每头猪的图里留出几张不参与注册专门用来测试。如果这些留出的图能被正确检索到对应个体且相似度明显高于其他个体说明特征区分度够用。如果发现某两头猪的相似度一直在 0.9 以上那要么是数据里这两头本来就难分要么是特征维度不够得换更强的骨干。从那以后我每次拿到一个新的识别类工程包都会先跑一遍特征提取、看一眼类间相似度分布再决定是走分类还是走检索。这个习惯帮我省掉了好几次「训完才发现类别不可分」的后悔药。希望帮到你。本文还有配套的精品资源点击获取