猪脸识别实战:从度量学习到向量检索的完整技术路线
发布时间:2026/9/14 23:55:39 锦皓数字建站

简介京东IDL猪脸识别参赛作品是一套面向高校人工智能、计算机、电子信息、物联网等相关专业师生的完整实战项目包Python源码、项目说明与设计报告PPT齐备可以直接用于毕业设计、课程设计、竞赛或项目立项演示。压缩包共185个文件、约364MB核心包括38个py源码、5个ipynb分析与训练笔记覆盖数据增强、CNN基础、Inception-v3训练与预测等环节、22个md说明、11个pdf报告、37个png和58个jpg数据集/结果图以及pptx设计报告PPT和xml/iml工程配置。项目各步骤均有可运行的代码和图文记录便于对照复现、学习进阶和二次开发也可作为课程项目演示遇到配置或运行问题还可借助说明文档快速排查。目前已有44人浏览学习非常适合想快速上手深度学习图像识别项目的小白也适合需要高效完成课设与毕设的开发者。1. 猪脸识别不是「人脸识别换张脸」先看懂 IDL 类项目在解什么题猪脸识别在 CV 里是个典型的「看起来容易、落地磨人」的方向。标题里的 IDL 可以指京东深度学习研究院这类机构缩写也可以理解成 instance discrimination实例判别这条技术路线对参赛作品来说后者才是真正要解的技术题不是把猪脸分类成「猪」而是要从一堆监控截图里认出「这头是 0327 号不是 0328 号」。它比人脸识别更难的地方在于姿态完全不可控、栏舍光线差、猪还会在脸上蹭泥、耳朵打标后伤口结痂改变纹理。这篇文章按「特征边界 → 数据准备 → 训练调参 → 部署检索」的顺序把细粒度识别与度量学习这套方案讲透适合 CV 工程师、农业科技团队和准备算法岗面试的人源码包里的东西到最后你也能自己搭出来。2. 先立住理论细粒度识别的特征边界与损失函数2.1 为什么这题不能直接套 softmax 分类很多参赛作品的第一版代码是把预训练 ResNet 的最后一层全连接改成 NN 等于猪的个体数然后直接 softmax 训练。这样做好看但有个致命前提类别是封闭的。养猪场的猪会出栏、会死亡、会新购入第二天类别就变了模型要跟着重训更麻烦的是算法上线后遇到的最大概率场景是「这头猪从来没见过」softmax 分类器在这种情况下只会硬给一个最高分 ID没有任何拒绝识别的机制。正确做法是让模型去学一个 embedding把一张猪脸图像压成一个低维向量训练目标是同一头猪的任意两张照片向量距离足够近不同猪之间足够远。推理时不再做「分类」而是拿新图特征到全场猪只的特征库里做最近邻检索没有候选命中就报「未知个体」。这是 IDL 类项目报告里常见的最终流程图检测 → 编码器 → 度量学习 → 向量检索模型真正要训练好的只有中间两步损失函数决定了 embedding 空间的长相。2.2 度量学习三件套triplet loss、center loss 与 ArcFace 怎么选损失函数核心思想适合场景主要坑Triplet Loss构造 anchor/positive/negative 三元组拉近正例、推远负例小数据、类别数多、需要拒识随机采样收敛极慢必须配难样本挖掘Center Loss为每类维护一个中心向量叠加分类损失一起训类别数稳定、想快速看效果中心若重叠类间判别力弱类别新增要重训ArcFace在角度域加间隔 m强制类间拉开数据量大、追求最高精度m 和特征缩放系数 s 调不好会不收敛人脸识别领域沉淀下来的经验在猪脸识别上基本可以直接搬。ArcFace 是这里最稳的默认选择它不依赖难样本挖掘也能训出一个边界清晰的 embedding适合做 baselinetriplet loss 负责在 ArcFace 基础上继续压紧同类距离但必须配合 batch 内难样本挖掘否则每步的梯度都在浪费。center loss 我一般只在类别数很少、先验证数据质量的时候用一下不放进最终方案。2.3 用 PyTorch 写一个 ArcFace 特征头见源码时能快速对上参赛源码包里那个看起来最长的 model.py核心通常就是下面这段逻辑。先看一个可以直接跑的最小实现import torch import torch.nn as nn import torch.nn.functional as F class ArcFaceHead(nn.Module): def __init__(self, in_features, num_classes, s32.0, m0.5): super().__init__() self.s s # 特征缩放系数控制 logits 尺度 self.m m # 角度间隔单位是弧度 self.cos_m torch.cos(m) self.sin_m torch.sin(m) self.th torch.cos(torch.pi - m) self.mm torch.sin(torch.pi - m) * m self.weight nn.Parameter( torch.FloatTensor(num_classes, in_features) ) nn.init.xavier_uniform_(self.weight) def forward(self, x, labels): # x: [N, D]通常是全连接输出的特征先做 L2 归一化 cosine F.linear(F.normalize(x), F.normalize(self.weight)) # 把 cos 转成 sin计算加了角度间隔后的余弦值 phi sine torch.sqrt(1.0 - cosine.clamp(-1, 1) ** 2) phi cosine * self.cos_m - sine * self.sin_m # 超出角度边界时退化为普通余弦防止训练震荡 phi torch.where(cosine self.th, phi, cosine - self.mm) one_hot torch.zeros_like(cosine).scatter_( 1, labels.view(-1, 1), 1 ) logits (one_hot * phi (1.0 - one_hot) * cosine) * self.s return logits这段代码的输入 x 是主干网络输出的特征向量label 是猪个体 ID。forward 里做了三件关键的事特征和类中心向量都做 L2 归一化让内积等价于余弦相似度模型只关注方向不关注模长在真实类别对应的 logit 上减去一个角度间隔 m强迫模型把同类向量拉得更近最后统一乘特征缩放系数 s控制 logits 的数值范围。损失就是常规交叉熵logits arcface_head(features, labels) loss F.cross_entropy(logits, labels)这里的 s 取 32 或 64 是常见起点s 太小分类边界太软前期 loss 降不下去m 从 0.3 开始调每档加 0.1超过 0.6 后同类角度间隔会跨越 π 边界训练容易炸。特征维度 D 取 128 比较稳妥类别数几千到一万都够用。3. 动手准备数据抽帧、检测、裁切与标注组织3.1 从现场视频到干净图片用 ffmpeg 抽帧与感知哈希去重猪脸识别的原始数据几乎都是监控录像或手机拍摄的视频一段 1 小时的录像按 2fps 抽帧会得到 7200 张图其中一半以上是相邻重复帧直接拿去标注既浪费人力也污染训练集。常见的抽帧命令是这样ffmpeg -i herd_cam01.mp4 -vf fps2,scale960:-1 -q:v 3 frames/%05d.jpg这条命令把视频按每秒 2 帧抽出来宽度缩到 960 像素JPEG 质量设为 3。参数含义分别是fps2 控制抽帧率1fps 会漏掉猪低头抬头这种快速姿态变化5fps 以上会产生大量相邻重复帧scale960 保证检测阶段的输入尺寸适中太大会拖慢后续处理太小会丢失耳廓和眼部的细节-q:v 3 是接近无损的 JPEG 质量太低在后续裁切放大时边缘会出现明显马赛克。抽帧完成后要去重常见做法是感知哈希把每张图缩成 8×8 灰度计算 64 位 hash两张图汉明距离小于 6 判定为近似重复只保留其中一张。这步用 Python 加 Pillow 几十行就能写完运行一次能把数据集体积砍掉 60% 到 70%。要注意抽帧时间窗尽量选白天光线均匀的时段晚上红外补光拍出来的图像色温异常混进训练集后会让裁切模型在白天场景下误检漏检。3.2 检测裁切为什么放在训练之前而不是端到端一起学有人会问既然有 YOLO 这类检测器为什么不把检测和识别串成一个端到端模型直接学答案在生产场景里很现实栏舍里猪脸占比很小背景里有围栏、食槽、积水、杂物把整张图直接送进识别网络特征会被背景大面积污染训练很难收敛。参赛作品里最稳的架构是两阶段先用目标检测模型裁出猪脸框再统一缩放到 224×224 或 256×256 送进识别网络。两阶段分开调的好处是识别效果差时可以明确判断是「框没框准」还是「特征没学对」不用一次调两个耦合模块。检测模型的数据成本很低一张图里标一个猪脸框2000 张左右就能训到 mAP0.5 超过 0.9。裁切时有一个容易被忽略的细节猪脸长宽比接近 1:1检测框输出后不要直接拉伸按正方形向外扩边扩边余量留 10% 到 20%避免把耳朵边缘切掉。猪低头时检测框经常会只框到耳朵和头顶这类裁切图要单独归到 hard 集合里后面难样本挖掘要用。3.3 标注格式与数据质量体检Laplacian 模糊检测 样本数统计源码包里真正干活的往往不是模型文件而是 data 目录的组织方式。比较标准的布局是这样dataset/ ├── JPEGImages/ # 原始抽帧图 ├── Annotations/ # VOC 格式 XML 标注 ├── ImageSets/Main/ # train.txt / val.txt 划分 └── crops/ # 按猪ID存放的裁切图 ├── 0321/ ├── 0327/ └── ...train.py 读的是 crops 目录而不是 JPEGImages识别模型只在裁切图上训练。拿到一批裁切图后别急着训练先跑一遍数据质量体检脚本很简单import cv2, os, numpy as np for pid in sorted(os.listdir(crops)): path os.path.join(crops, pid) if not os.path.isdir(path): continue imgs [os.path.join(path, f) for f in os.listdir(path) if f.endswith(.jpg)] stats {count: len(imgs)} if imgs: gray [cv2.cvtColor(cv2.imread(p), cv2.COLOR_BGR2GRAY) for p in imgs[:20]] lap [cv2.Laplacian(g, cv2.CV_64F).var() for g in gray] stats[mean_sharpness] float(np.mean(lap)) print(pid, stats)这段脚本对每个猪 ID 统计两件事样本总数以及前 20 张图的 Laplacian 方差均值。Laplacian 方差是经典的模糊度指标数值越大表示边缘越锐利判读标准可以按下面这个表来mean_sharpness判定处理建议 300清晰主力正常参与训练100 ~ 300可用但偏软保留不额外加权 100模糊帧删除或归入 hard 集count 30样本不足回补数据或过采样注意 Laplacian 对「猪脸被泥糊住」这类低对比度模糊不敏感泥巴遮盖的画面边缘依然锐利但跟正常猪脸差异巨大。我一般会额外做一次颜色直方图统计把灰度分布异常的样本单独拉出来归到「易混集」里而不是直接删除后面难样本挖掘时它们价值很高。4. 训练与调参把损失函数和难样本挖出来4.1 模型选型表ResNet50、MobileNetV3 与 Swin-T 怎么选模型参数量单张 CPU 推理相对精度适用场景ResNet5025.6M约 40ms高实验室 baselineGPU 服务器MobileNetV3-Large5.4M约 15ms中高Jetson、边缘盒子Swin-T28M约 60ms最高数据量大、离线训练EfficientNet-B05.3M约 18ms中高精度和速度折中猪场个体识别数量通常在千级MobileNetV3-Large 足够撑住只有单场生猪头数过万才需要上 Swin-T 这类带注意力机制的模型。参赛 PPT 里 Top1 数字好不好看主要不取决于选哪个骨架而取决于数据清洗狠不狠、难样本挖得够不够模型选型只要不在 CPU 上跑 ResNet50 这种明显不匹配的组合就行。Embedding 维度方面千级 ID 用 64 维通常就够万级以上再提到 128。4.2 三个必调训练策略冻结 BN、warmup、EMA换上新骨架后第一个必须处理的是 BatchNorm 的 running mean 和 running var。它们来自 ImageNet 预训练统计量跟猪脸图像的分布差得很远小 batch 训练时 BN 统计量会剧烈抖动前 10 个 epoch 会把底层特征冲坏。常见做法是冻结骨干网络前几个 stage 的 BN只让 head 部分更新10 个 epoch 之后解冻再整体微调。第二个策略是 warmup。预训练模型直接上大学习率等于把已经学好的底层边缘和纹理特征一把推翻线性 warmup 5 个 epoch 从 1e-5 升到 3e-4再走 cosine 退火到 1e-6收敛速度和最终精度都会明显变好。第三个是 EMA每一步用 0.999 的衰减系数维护一份参数滑动平均推理和验证都用 EMA 版本的权重而不是原始权重。这行代码不加白不加很多榜单上的小数精度就是这么抠出来的。配合这三个策略训练脚本通常长这样optimizer torch.optim.AdamW(model.parameters(), lr3e-4) scheduler CosineAnnealingLR(optimizer, T_maxepochs) for epoch in range(epochs): model.train() for batch in loader: if epoch 10: freeze_bn(model, freezeTrue) # 前10轮冻结BN loss criterion(model, batch) loss.backward() optimizer.step() ema_update(model, ema_model, decay0.999)warmup 的步数不是拍脑袋定的要看 batch size64 的 batch 下 5 个 epoch 大约是 400 步batch 减半则 warmup 步数翻倍保证模型见过的数据量相近。4.3 手工实现 Batch Hard Triplet 挖掘不加难样本挖掘的 triplet loss 是出了名的低效随机采样的三元组里绝大多数已经是「正例足够近、负例足够远」梯度几乎为零。正常的做法是 batch 内挖掘每个 batch 采 8 个以上的猪 ID每个 ID 4 到 8 张图然后在这个小矩阵里找出每张图最难的正例和最难负例def hard_triplet_loss(feats, labels, margin0.3): # feats: [N, D] 已做 L2 归一化labels: [N] sim feats feats.T # 余弦相似度矩阵 n feats.size(0) same labels[:, None] labels[None, :] eye torch.eye(n, dtypetorch.bool, devicefeats.device) # 跟自身不算正例去掉对角线 pos sim[same ~eye].view(n, -1) # 每个样本的正例相似度 neg sim[~same].view(n, -1) # 每个样本的负例相似度 hardest_neg neg.max(dim1).values # 最像的负例 hardest_pos pos.min(dim1).values # 最不像的正例 loss F.relu(hardest_pos - hardest_neg margin).mean() return loss这里的核心是相似度矩阵一次算完整整个 batch 的全部两两关系向量化之后速度很快。mask 构造时一定要抠掉对角线否则每张图的 self-similarity 等于 1直接变成正例最大值loss 会瞬间拉爆。hardest_neg 取每行最大相似度对应的负例即「模型最容易认错的那头猪」hardest_pos 取最不像的正例即「这头猪自己变化最大的那张脸」两者之间的间隔小于 margin 就产生惩罚。这个函数可以直接叠加到第 2 章的 ArcFace loss 后面total_loss ce_loss 0.1 * triplet_loss注意 batch 里每个 ID 样本数小于 4 时hard mining 形同虚设因为正例对太少挖掘不出有意义的梯度。4.4 核心参数表与四个典型翻车现场参数起手值调整方向batch_size64显存不足降到 32配合梯度累积embedding 维度128千级 ID 可降到 64margin0.3类间距仍粘连时升到 0.5特征缩放 s32训练不稳时降到 16每个 ID 最小样本数30少于 30 做回补或过采样每 batch 覆盖 ID 数≥ 8太少则 hard mining 失效训练中见过最多的四个问题。第一loss 掉到 0.3 左右就不降了但验证集 Top1 还在涨这不是 bug检索任务本来就该看最近邻分布而不是分类 loss。第二训练集准确率 99%验证 Top1 只有 78%优先怀疑裁切偏移检测框在训练集里偏松、验证时框偏紧两边特征对不上把随机裁切增强打开再试。第三同类距离开得很散先加 margin 而不是加模型容量0.3 升到 0.5 通常立竿见影。第四某个 ID 的样本全部学成「挡脸」猪低头时裁切框里只有耳朵去查这个 ID 的 sharpness 分布把耳朵图单独挖出来当难样本喂回来。5. 部署前的小改造向量检索、模型裁剪与失败自检5.1 从「最后一层判断」到 Faiss 全群检索模型训好后推理不能再用分类头直接输出 ID正确的生产做法是拿全部已知猪只的特征建一个向量索引。这一步用 Faiss两行代码的事import faiss import numpy as np # X: [N, D] 训练集全部猪脸特征已做 L2 归一化 X np.ascontiguousarray(X.astype(float32)) index faiss.IndexFlatIP(D) # 内积等价于余弦相似度 index.add(X) scores, preds index.search(query_feat, k5) # 新图特征查 top5IndexFlatIP 是暴力精确检索结果最准但数据量大时延迟高。换 IndexIVFFlat 先把特征空间聚类nlist 取候选集数量的平方根左右检索时只用 nprobe 个最近的聚类簇nprobe 从 10 试到 30单次查询能压到毫秒级quantizer faiss.IndexFlatIP(D) index faiss.IndexIVFFlat(quantizer, D, nlist, faiss.METRIC_INNER_PRODUCT) index.train(X) index.nprobe 20 index.add(X)线上流程是摄像头抽帧 → 检测器裁出猪脸 → 编码器提 128 维特征 → Faiss 返回 Top5 → 超过距离阈值直接报「未知个体」。这一步是很多参赛作品源码包里「上线」和「演示」的分水岭报告 PPT 里那张检索流程图落地后就是这个服务。5.2 用 ONNX 导出和 INT8 量化压缩模型边缘盒子算力有限需要把 PyTorch 模型转成 ONNX 再走 TensorRT 或 OpenVINO。导出命令通常是python export.py --weights best.pth --include onnx --opset 12 trtexec --onnxmodel.onnx --int8 --calibcalib.txt --saveEnginemodel.engineINT8 量化需要准备校准集从训练集里随机抽 512 张覆盖不同光照条件的猪脸图而不是全部灌进去。量化后 Top1 掉 1 到 2 个点属于正常范围掉超过 5 个点说明校准集没有覆盖低照度或运动模糊场景回去补样本重新生成校准文件。导出时注意把 batch 固定成 1ONNX 里动态 batch 在 TensorRT 下经常炸这个坑排查起来很费时间。5.3 失败自检距离分布告诉你要补什么数据每次训练完跑一轮验证把每个 ID 的「同类最近邻距离中位数」拉出来画分布超过全局 P95 的 ID 单独记进一个脏名单。脏名单的去向有三种全删、降采样、单独喂给难样本挖掘。回补数据的优先级我一般定为遮挡 ≥ 模糊 姿态 光照。对这些脏名单样本回到原始录像按时间轴前后各取 3 秒重新抽帧能拿到该 ID 更完整的姿态变化直接补进训练集这往往是比调 loss 更快的提点手段。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。