简介:京东JDD大赛猪脸识别项目以商品猪个体身份识别为赛题,涵盖数据预处理、模型训练、测试与可视化全流程,适合计算机、数学、电子信息等专业学生用于课程设计、期末大作业或毕业设计参考。压缩包共61个文件,其中24个Python脚本构成核心代码,覆盖模型定义、数据加载、训练与测试流程;22个Markdown文档提供多份项目说明、代码注解与实验记录;14张图片为网络结构示意或训练过程可视化结果;整体仅5.62MB,轻量却包含完整工程结构。项目代码基于PyTorch等深度学习框架,包含VGG16、VGG19、ResNet50、FCN等经典网络实现,同时提供视频抽帧、图片增强、测试集CSV生成等辅助脚本,并划分data、classify、fcn、model、summary等清晰目录,从数据准备到模型评估均有对应脚本,便于按模块阅读。当前已有233人学习下载,既有赛题方案又有工程细节,适合希望掌握图像分类/分割实战项目或快速搭建完整训练流程的开发者参考。
1. 猪脸识别不是换个人脸模型就能跑通
拿到一份 JDD 猪脸识别项目源码,最先要认清:猪脸识别和人脸识别的技术栈高度重合,但数据分布完全不同。猪的个体差异集中在鼻子纹路、眼周斑块和耳廓轮廓上,这些区域在整幅监控画面里占比很小,姿态、光照和泥污随时会盖掉特征。直接套用给人脸设计的 ArcFace 流程,往往验证集 90% 以上,一上测试集就掉到七八成,问题大多出在数据管线和损失函数。下面按这类赛题的标准路线——数据管线、骨干网络与损失函数、训练调参、推理优化——逐层给出可复现方案,适合做动物个体识别、细粒度检索或正在备赛的工程师参考。
2. 猪脸识别数据管线:抽帧策略、检测裁剪与关键点对齐
2.1 视频抽帧:先过滤模糊帧再做感知哈希去重
赛题给的原始数据一般是监控视频,直接用 OpenCV 按固定间隔抽帧,会带回来大量模糊帧和静止重复帧。常见做法是每 30 帧取一帧,然后用拉普拉斯方差过滤低清晰度帧。拉普拉斯算子对图像求二阶导,方差越小说明边缘越少、图像越模糊。阈值取值要看实际分辨率,720p 视频我一般先用 80 做初筛,再根据分布回调。
import cv2 def extract_sharp_frames(video_path, interval=30, blur_thresh=80): cap = cv2.VideoCapture(video_path) frames = [] idx = 0 while True: ret, frame = cap.read() if not ret: break if idx % interval != 0: # 每隔 interval 帧取一帧 idx += 1 continue gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) var = cv2.Laplacian(gray, cv2.CV_64F).var() # 拉普拉斯方差,越小越模糊 if var > blur_thresh: frames.append((idx, frame, var)) idx += 1 cap.release() return framesinterval=30对应 30fps 视频每秒取一个候选帧,blur_thresh=80是拉普拉斯方差门槛。这个参数不建议直接抄,先跑一遍统计分布,取方差中位数的 1.2 到 1.5 倍作为阈值更稳妥,否则猪快速走动时会把相对清晰的运动帧也滤掉。抽帧之后还要做感知哈希去重,猪在食槽前长时间不动会产生大量近似帧,这些重复样本会让训练集的有效多样性大幅缩水,去重时把哈希距离小于 5 的相邻帧只保留方差最大的一张。
2.2 检测与裁剪:微调 YOLO 比从零训练更稳妥
抽帧得到的画面里有栏杆、地面、食槽等大量背景,直接整图送入分类网络会引入强烈的背景干扰,模型很可能学到"站在食槽左边的是猪 A"这种伪规律。自己从零标几万张检测框不现实,常见做法是拿 COCO 预训练的 YOLOv8n,用二三十张图人工标注猪脸框,冻结 backbone 微调 50 到 60 个 epoch,就能得到一个可用的检测器。
from ultralytics import YOLO model = YOLO("yolov8n.pt") model.train( data="pig_face.yaml", epochs=60, imgsz=640, lr0=0.005, freeze=10, batch=16, )freeze=10表示冻结前 10 层 backbone,只更新后半部分和检测头,这样小样本标注也能收敛。lr0=0.005比默认的 0.01 低一半,迁移学习场景下学习率过大容易把预训练权重冲坏。pig_face.yaml里只保留一个类别 pig_face,标注用 YOLO 的 txt 格式,每行是class x_center y_center width height,坐标归一化到 0 到 1。微调完成后用model.predict(source=..., conf=0.5)批量推理,把检测框裁出来并向外扩展 20 像素左右,避免猪鼻子或耳朵被切掉。
2.3 关键点对齐:仿射变换统一猪脸朝向
检测裁剪之后,猪脸的朝向仍然混乱,有的正对镜头,有的侧转 30 度。直接把图送进特征提取网络,模型要额外学习姿态不变性,训练成本翻倍。常见做法是标注双眼和鼻尖三个关键点,用仿射变换把这三个点映射到固定模板位置。
import cv2 import numpy as np def align_face(image, eye_left, eye_right, nose, size=(112, 112)): src = np.array([eye_left, eye_right, nose], dtype=np.float32) dst = np.array([[30, 38], [82, 38], [56, 72]], dtype=np.float32) # 标准模板坐标 M = cv2.getAffineTransform(src, dst) return cv2.warpAffine(image, M, size)dst中的三组坐标对应标准模板里左眼、右眼、鼻尖的位置,(112, 112)是输出尺寸。cv2.getAffineTransform用三对点求解仿射矩阵,warpAffine完成重采样。对齐之后所有样本的鼻子都落在同一区域,网络可以把更多容量用在学习个体纹理差异上。需要注意类别里若有少量照片无法标注关键点,直接丢弃比硬对齐好,错位样本对训练的伤害远大于少一个样本。检测与对齐的质量决定了整个识别系统的天花板,这部分做得粗糙,后面换再大的模型也补不回来。
3. 骨架网络与损失函数:猪脸识别为什么用 ArcFace
3.1 个体识别本质是度量学习问题
赛题通常要求判断"这张猪脸属于哪头猪",看起来是分类任务,但实际部署时会遇到训练集之外的新个体,因此要学的不是记住类别 ID,而是一个特征空间:同一头猪不同照片的特征距离近,不同猪的照片特征距离远,这就是度量学习。纯 Softmax 分类头也能得到可用的特征向量,但 Softmax 只追求类别可分,不显式约束类内紧凑。在类别少、每类样本多的数据集上,它和 ArcFace 的差距不明显,但猪脸数据通常每个个体只有十几张图,Softmax 学出来的特征在空间中散布较大,检索时容易误判。所以赛题源码里几乎无一例外使用带 margin 的度量学习损失,核心思路是在分类目标之外再压紧类内距离。
3.2 ArcFace 损失的设计逻辑与 PyTorch 实现
ArcFace 在 Softmax 的基础上,给特征向量和权重向量之间的夹角加一个角度裕度 m,把目标类别的 logit 从 cos(θ) 变成 cos(θ + m),迫使网络把每个类别的特征压缩到更小的角度范围。m 越大类内约束越强,训练也越难收敛。人脸任务里 m 通常取 0.5,猪脸赛题我建议从 0.3 起调,因为猪脸类间差异更小,margin 太大容易训不上去。
import torch import torch.nn as nn import torch.nn.functional as F class ArcFaceLoss(nn.Module): def __init__(self, in_features, out_features, s=32.0, m=0.3): super().__init__() self.weight = nn.Parameter(torch.FloatTensor(out_features, in_features)) nn.init.xavier_normal_(self.weight) self.s = s self.m = m def forward(self, inputs, labels): cosine = F.linear(F.normalize(inputs), F.normalize(self.weight)) theta = torch.acos(torch.clamp(cosine, -1.0, 1.0)) # 反余弦求角度 target_logits = torch.cos(theta + self.m) # 只对目标类加角度裕度 one_hot = F.one_hot(labels, num_classes=cosine.size(1)) logits = torch.where(one_hot.bool(), target_logits, cosine) return F.cross_entropy(logits * self.s, labels)F.normalize分别对特征和权重做 L2 归一化,让内积等价于余弦相似度。s=32.0是缩放因子,把余弦值从 [-1, 1] 放大到 [-32, 32] 再算 softmax,避免数值区间过小导致梯度消失。m=0.3加在目标类别的夹角上,torch.where保证只有目标类别的 logit 被修改。需要特别注意torch.acos的输入先 clamp 到 [-1, 1],否则特征归一化后的浮点误差会直接产出 NaN。
提示:
m从 0.3 开始,若训练 loss 下降正常但验证集分数徘徊,先调m而不是调学习率。每降 0.05 看一次验证集 Recall@1,找到不震荡的最大值。
3.3 骨干网络选型与输出层改造
赛题项目里最常见的骨干是 ResNet50,输入 112x112 对齐图,输出 512 维特征,最后接一个 BN 层和 Dropout 再进 ArcFace。ResNet34 参数量小一半,在样本量只有几千张时泛化性往往更好,两个都跑一遍用验证集决定。如果训练资源有限,MobileNetV3 也能用,但特征维度建议降到 256,轻量网络在小数据集上的过拟合风险更低,上限也低一截。
import torchvision.models as models backbone = models.resnet50(pretrained=True) backbone.fc = nn.Sequential( nn.BatchNorm1d(2048), nn.Dropout(0.4), nn.Linear(2048, 512), )pretrained=True的 ImageNet 权重对猪脸这类自然图像有明确的迁移收益,即便数据集中没有猪脸类别,底层边缘和纹理特征仍然通用。把最后全连接层替换为 BN-Dropout-Linear,是为了让特征输出经过归一化,减少各维度尺度不一致对余弦距离的影响。Dropout 设 0.4 在小数据集上是保守值,训练损失下降缓慢时降到 0.2,验证集波动大时优先加大 Dropout 而不是减学习率。
一个常见误区是训练时冻结整个 backbone,只训练新加的层。人脸识别任务里底层特征确实通用,但猪脸的纹理模式与 ImageNet 类别差异较大,冻结 backbone 会导致特征不足。我一般只冻结前两三个 stage,后面的 stage 与全连接层一起更新,这样既保留通用边缘特征,又让高层语义适应猪脸分布。
4. 训练策略与推理优化:让模型在猪脸数据上稳定收敛
4.1 数据增强:针对猪脸场景的组合
猪脸数据的干扰主要来自光照变化、泥污遮挡和姿态偏转,增强策略对准这三类即可。随机亮度对比度扰动模拟不同时段的监控光照,RandomErasing 模拟泥污或栏杆遮挡,随机旋转 15 度以内模拟姿态偏移。不要用随机裁剪当主要增强手段,裁剪会破坏对齐后的关键点位置语义,让模型学到错误的对应关系。推荐的增强组合如下。
| 增强操作 | 参数范围 | 作用 | 注意事项 |
|---|---|---|---|
| RandomBrightnessContrast | brightness=0.3 contrast=0.3 | 模拟光照变化 | 与 HSV 扰动二选一,不要叠加 |
| RandomErasing | p=0.3, scale=(0.02, 0.1) | 模拟泥污遮挡 | 遮挡面积超过 10% 等于制造噪声 |
| Rotate | limit=15 | 模拟姿态偏转 | 超过 20 度会让对齐失效 |
| HorizontalFlip | p=0.5 | 增加样本多样性 | 猪脸左右不对称时先验证再开 |
RandomErasing的 scale 控制在图片面积的 2% 到 10%,覆盖泥污和栏杆遮挡的常见比例,再大就会把鼻子这块关键判别区域整片抹掉。HorizontalFlip 是否安全取决于数据分布,如果训练集里猪总是从固定方向进食,翻转后会大量出现与真实姿态冲突的样本,先用小模型对比翻转前后的验证集分数再决定开不开启。
注意:验证集必须按个体划分,不能按图片划分,否则同一头猪的图片同时出现在训练集和验证集,分数虚高到完全失去参考价值。
4.2 关键超参与训练循环
小样本个体识别任务里,优化器、学习率和 batch size 三者互相牵制。我常用的配置是 AdamW + 初始学习率 1e-4 + batch size 64。batch size 小于 32 时 ArcFace 的角度裕度在梯度上体现不充分,类别间区分力下降;显存不够时优先降低输入分辨率,而不是调小 batch size。
from torch.utils.data import DataLoader train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=8, drop_last=True) # 丢弃不完整 batch,稳定 BN 统计量 optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=5e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=60)drop_last=True丢弃最后一个不完整 batch,避免 BN 在 batch 过小时统计量抖动。weight_decay=5e-4走 AdamW 的解耦权重衰减路径,对 BN 层参数没有影响,这是期望行为。T_max=60与总 epoch 数一致,余弦退火让学习率从 1e-4 平滑降到接近 0,最后几个 epoch 的特征空间会被精细打磨,对检索任务尤其重要。
训练结束后不要只保存最后一个 checkpoint。ArcFace 训练中验证集最佳出现在最后 10 个 epoch 内的概率很高,每个 epoch 结束都算验证集 Recall@1,保存分数最高的权重。我在实际项目里见过最后 epoch 比最佳 epoch 低 2 到 3 个点的情况,直接拿最后一个 checkpoint 上线会白白损失精度。
4.3 推理:特征 L2 归一化与余弦相似度检索
推理阶段把对齐图送入 backbone 得到 512 维特征,L2 归一化后与库里所有个体的特征做余弦相似度,排名第一且相似度超过阈值才判定为对应个体,否则判为未知。候选库的特征在训练结束后离线提取一次,不需要每次查询都重新过网络。
import numpy as np def build_gallery(model, gallery_loader, device): model.eval() gallery, ids = [], [] with torch.no_grad(): for images, labels in gallery_loader: emb = model(images.to(device)) emb = F.normalize(emb, dim=1) # 查询与库特征统一做 L2 归一化 gallery.append(emb.cpu().numpy()) ids.extend(labels.numpy()) return np.concatenate(gallery), np.array(ids) def query(model, image, gallery, ids, device, threshold=0.4): model.eval() with torch.no_grad(): emb = model(image.unsqueeze(0).to(device)) emb = F.normalize(emb, dim=1).cpu().numpy() sims = gallery @ emb.T # 矩阵乘计算全库余弦相似度 best_idx = sims.argmax() if sims[best_idx] >= threshold: return int(ids[best_idx]), sims[best_idx] return -1, sims[best_idx]gallery @ emb.T是矩阵乘实现的全库相似度计算,因为两侧特征都做过 L2 归一化,内积等价于余弦相似度。threshold=0.4是初始值,最终取值要在验证集上画相似度分布确定。model.eval()必须调用,否则 BN 层会继续更新统计量,推理特征会漂移,而且这个漂移在类别多的时候会被放大。
5. 猪脸识别提分的三个细节:阈值、多帧投票与类别加权
5.1 阈值取相似度分布曲线的交点
模型精度不是全部,实际系统里"误判"和"拒绝"是方向不同的两类错误。在按个体划分的验证集上,把同类相似度和异类相似度各画一条分布曲线,取两条曲线的交点作为阈值,同类误拒和异类误识的概率大致相等。业务更看重少误识时,把阈值向异类分布方向移 0.05 到 0.1。若两条曲线大量重叠,调阈值只是在转移错误,要回头检查增强策略和训练 epoch。
5.2 多帧投票比单帧 TTA 更划算
TTA 用水平翻转做特征平均,在猪脸赛题里一般只有 1 到 2 个点收益。监控视频给查询端带来了连续多帧,把同一头猪连续几帧的余弦相似度取平均再判阈值,能显著抑制单帧模糊或遮挡造成的偶发误判。多帧平均和 TTA 可以叠加,但推理耗时翻倍,时限紧时优先保多帧投票。
5.3 类别加权采样缓解样本不均衡
猪脸数据里每头猪的照片数差异很大,样本多的个体在 ArcFace 特征空间里会撑大角度区域,挤压少数类别。用 WeightedRandomSampler 按类别样本数倒数做加权采样,让不同个体在每个 epoch 中出现的次数接近相等,通常稳定提升 1 到 3 个点且不增加训练时间。
import collections from torch.utils.data import WeightedRandomSampler counts = collections.Counter(dataset.labels) weights = [1.0 / counts[label] for label in dataset.labels] # 样本越少权重越高 sampler = WeightedRandomSampler(weights, len(weights), replacement=True) loader = DataLoader(dataset, batch_size=64, sampler=sampler)replacement=True表示有放回采样,num_samples与原始样本数保持一致,迭代步数不变。权重取倒数后少数类别会被重复抽到,配合数据增强能缓解过拟合;若少数类别本身图像质量差,权重别设过高,否则会把噪声样本反复喂给模型,我一般把权重上限截断在 5 倍。
本文还有配套的精品资源,点击获取