简介面向全国大学生电子设计竞赛备赛者与算法竞赛入门学生这份压缩包取自京东JDD猪脸识别比赛以真实赛题为主线提供完整可运行的实战源码与说明文档。包内共5个文件包括4个Python脚本和1个Markdown说明分别对应模型微调、训练、测试、生成提交结果等环节并配有README快速说明代码结构与调用关系整体仅6KB轻量而紧凑适合在电赛或同类视觉任务中直接借鉴工程组织方式。代码按功能拆分注释清晰便于二次修改与扩展。目前已有251人学习下载作者zy_dreamer将竞赛经验沉淀为可复现的示例既能帮助理解图像识别项目从数据准备到结果提交的完整流程也可作为课程设计、毕业设计或项目起步时的参考模板。1. JDD猪脸识别比赛先想清楚这是分类还是度量学习打开一个名为「京东JDD猪脸识别比赛.zip」的压缩包绝大多数人的第一反应是解压、找 train.py、把学习率调低然后硬训。但真正决定你在排行榜上位置的往往不是模型结构选得多新而是你如何定义这个问题。猪脸识别和普通的人脸识别一样本质上是一个细粒度个体识别问题数据里可能只有几百头猪每头猪有若干张不同角度、不同光照、甚至不同脏污程度的照片你要做的是判断“这一张脸属于哪一头猪”。这和 ImageNet 式的分类有本质区别。分类任务里每个类别有大量样本、类间差异大而猪脸识别类内差异大同一头猪在不同生长阶段脸型会变、类间差异小纯黑猪和纯黑猪之间五官差异极其细微。比赛给的 zip 里通常包含训练集图片、标注 JSON 和评估脚本但你真正需要关心的第一件事不是模型而是评估协议它用 Top-1 Acc 还是 RecallK决定了你的特征输出要不要做归一化决定了损失函数选 Softmax 还是 Triplet。这篇文章我会从一个参赛工程师的视角把数据处理、检测、识别、评估和部署这条链路完整拆开每个环节都给出可直接抄走的代码和调参经验。2. 打开JDD比赛包猪脸检测与数据清洗2.1 先读数据协议标签、划分与评估口径JDD 猪脸识别比赛的数据组织方式通常是一个 train 目录加上若干个 JSON 标注文件。标签文件里的核心字段一般是image_id、pig_id和标注框坐标部分版本还会附带猪舍编号、拍摄时间等元信息。第一步永远不是写模型而是写一个数据检查脚本把标注分布打印出来确认三个事情类别总数、每类样本数的最小值和最大值、以及图片是否存在损坏或标注越界。这类比赛的数据集典型特点是长尾分布可能一半的猪有上百张图但尾部有几头猪只有三五张。直接用原始分布训练模型会对高频个体过拟合低频个体几乎学不到判别特征。常见的做法是先统计频次设定一个阈值比如少于 10 张的个体决定是丢弃、扩充还是做类别加权。另一个高频坑是标注框边界越界PIL 读取后坐标可能是负数或超出图像宽高这类噪声如果不处理数据增强时会直接报错而且特别难排查。import json from PIL import Image from collections import Counter with open(annotations/train.json) as f: anns json.load(f) pig_counter Counter() bad_images [] for item in anns: img_path ftrain/{item[image_id]}.jpg try: img Image.open(img_path) w, h img.size x, y, bw, bh item[bbox] # 坐标可能为负、可能超界统一做 clip if x 0 or y 0 or x bw w or y bh h: item[bbox] [max(0, x), max(0, y), min(bw, w - x), min(bh, h - y)] pig_counter[item[pig_id]] 1 except Exception: bad_images.append(img_path) print(个体总数:, len(pig_counter)) print(最少样本数:, min(pig_counter.values())) print(坏图:, bad_images)这段脚本的核心价值在于把「标注质量」变成可量化的指标。min(pig_counter.values())如果小于 10你的数据增强策略就要偏向裁剪、旋转这类保持身份信息的变换而不能用随机擦除这类可能把判别区域抹掉的强增强。bbox的 clip 操作一定要在数据加载器外部完成不要在__getitem__里做否则每次迭代重复计算会拖慢训练速度。2.2 猪脸检测检测框质量直接决定识别上限猪脸识别的流程通常是两段式先用目标检测模型从整图中裁出脸部区域再把裁好的脸图送入识别模型。检测的质量决定了识别模型看到的是什么——框大了会把脖子和身体背景带进来框小了会切掉耳朵和眼睛这两种情况都会让识别模型的输入分布漂移。比赛环境下我一般不会去用 Cascade R-CNN 这类重模型主要原因是训练时间和显存开销大而猪脸检测本身不是难点难度在于尺度。猪脸在画面中的占比可能从 5% 到 40% 不等这要求检测器对小目标有足够的召回。YOLOv8 的s或m级别即可输入分辨率设到 960 以上mAP 通常能到 95 以上足够用了。from ultralytics import YOLO model YOLO(yolov8m.yaml).load(yolov8m.pt) model.train( datapig_face.yaml, epochs60, imgsz960, batch16, lr00.005, augmentTrue, patience15, save_period10, )pig_face.yaml里的数据格式是 YOLO 标准格式标注框需要从 JSON 转换成归一化的中心点坐标。imgsz960是这类比赛的甜点参数再往上显存消耗倍增但 mAP 提升有限。augmentTrue启用 Ultralytics 内置的马赛克和随机仿射变换能有效应对猪只在栏舍内的姿态变化但要注意马赛克增强会改变目标尺度分布训练后期可以关掉。检测模型训完后要做一次全量推理把裁出来的猪脸图保存成独立文件。这里有个容易忽略的点推理时的置信度阈值不要用默认的 0.25比赛场景下误检的代价远低于漏检建议降到 0.1把边界样本都保留下来让识别模型去处理难例。2.3 数据清洗用特征聚类找出标注错误标注错误是这类比赛里最隐蔽的杀手。猪脸的标注如果标错了个体模型会学到错误映射而且这种错误在验证集上几乎发现不了因为验证集的标注同样可能是错的。清洗标注最有效的手段不是人眼看图而是用一个预训练好的通用视觉模型提取特征然后对同一头猪的所有图片做聚类找出离群样本。from sklearn.cluster import DBSCAN import numpy as np features np.load(features/pig_007.npy) # 形状: [N, 512] feat_norm features / np.linalg.norm(features, axis1, keepdimsTrue) clustering DBSCAN(eps0.35, min_samples3, metriccosine) labels clustering.fit_predict(feat_norm) outliers np.where(labels -1)[0] print(疑似标注错误样本索引:, outliers)DBSCAN 的好处是不需要预先指定类别数eps0.35这个阈值是从经验出发的起点实际使用时要根据特征分布的相似度矩阵调整。min_samples3意味着小于 3 张图的聚类会被视为噪声。标注错误通常表现为同一头猪的图片特征在空间上形成多个分散的小簇或者个别特征向量明显游离在主簇之外。3. 猪脸识别核心从Softmax到度量学习3.1 为什么通用分类损失在猪脸识别上不够用直接用 ResNet 加 Softmax 训练猪脸识别验证集准确率可以做到 95% 以上但线上成绩往往不理想原因在于 Softmax 训练出的特征只是「类别可分」而不是「度量可比较」。Softmax 的决策边界会把特征空间划分成扇形区域但同一头猪的不同角度照片可能落在扇形边缘而不同猪的正面照反而可能距离更近。这个问题在测试阶段会彻底暴露比赛通常会用测试集里的图片构建检索任务计算 query 与图库之间的距离这时候特征向量之间的度量方式直接决定结果。如果训练时没有显式地优化特征距离余弦相似度和欧氏距离的结果会有很大差异。所以真正可靠的做法是切换成度量学习损失让同类样本的特征在超球面上聚集、异类样本相互排斥这样推理阶段无论用哪种距离度量都能得到稳定结果。3.2 用ArcFace把特征拉进超球面ArcFace 是当前人脸识别领域的事实标准用在猪脸识别上同样成立。它的核心是给 Softmax 加上角度间隔让模型在训练时就学会压缩类内角度距离、扩大类间角度差异。相比 Triplet LossArcFace 不需要难例挖掘的工程复杂度训练稳定性也好很多。import torch import torch.nn as nn import torch.nn.functional as F class ArcFaceLoss(nn.Module): def __init__(self, in_features, out_features, s32.0, m0.50): super().__init__() self.weight nn.Parameter(torch.FloatTensor(out_features, in_features)) nn.init.xavier_normal_(self.weight) self.s s self.m m self.cos_m torch.cos(torch.tensor(m)) self.sin_m torch.sin(torch.tensor(m)) self.th torch.tensor(torch.cos(torch.pi - m)) self.mm torch.tensor(torch.sin(torch.pi - m) * m) def forward(self, features, labels): # 特征归一化消除样本数量带来的尺度差异 features F.normalize(features, p2, dim1) weight_norm F.normalize(self.weight, p2, dim1) cos_theta torch.matmul(features, weight_norm.T) cos_theta torch.clamp(cos_theta, -1.0 1e-7, 1.0 - 1e-7) sin_theta torch.sqrt(1.0 - torch.square(cos_theta)) cos_theta_m cos_theta * self.cos_m - sin_theta * self.sin_m # 处理角度超出范围的情况防止梯度异常 cond cos_theta self.th cos_theta_m torch.where(cond, cos_theta_m, cos_theta - self.mm) one_hot F.one_hot(labels, num_classesself.weight.shape[0]) output torch.where(one_hot.bool(), cos_theta_m, cos_theta) return F.cross_entropy(output * self.s, labels)代码里两个关键参数是s32.0和m0.50。s是缩放系数控制特征向量的模长值越大训练初期收敛越快但太大容易梯度爆炸32 是人脸识别领域经过大量实验验证的安全值。m是角度间隔间隔越大类内约束越强但如果训练样本过少过大的间隔会导致模型欠拟合尾部类别样本数小于 10 时建议把m降到 0.3。这里的th和mm用于处理角度接近 π 的边界情况不加这两个约束训练会偶发 NaN。骨架网络建议用 ResNet50 或更轻量的 RepVGG输出特征维度设在 512。训练时输入分辨率统一到 224x224增强策略只保留随机水平翻转、小角度旋转和轻度颜色抖动不要用 RandomErasing猪脸的被遮挡区域往往是身份信息的一部分。3.3 推理提取特征做向量检索训练完成后识别阶段不再需要分类头。要做的是冻结骨干网络去掉最后一层全连接让每张猪脸图输出一个 512 维的归一化特征然后把整个图库的特征存成矩阵查询时用矩阵乘法一次性计算 query 和所有图库样本的余弦相似度。import torch import torch.nn.functional as F import numpy as np def extract_features(model, dataloader, device): model.eval() features, ids [], [] with torch.no_grad(): for images, labels in dataloader: images images.to(device) feat model(images) # 输出 [B, 512] feat F.normalize(feat, p2, dim1) features.append(feat.cpu().numpy()) ids.extend(labels.numpy()) return np.vstack(features), np.array(ids) def search(feat_query, gallery_feats, gallery_ids, top_k5): sim feat_query gallery_feats.T # 余弦相似度矩阵 idx np.argsort(sim)[::-1][:top_k] return gallery_ids[idx], sim[idx]这里的extract_features中的归一化必须保留因为训练时 ArcFace 也做了同样的操作推理和训练的一致性直接决定最终效果。search函数里用矩阵乘法替代循环计算余弦相似度图库量级在几万张以下时耗时在毫秒级完全不需要引入 FAISS 一类的工具。如果图库规模超过十万张才考虑用 FAISS 的 IVF 索引做近似检索。4. 评估、调参与模型部署4.1 用RecallK评估个体识别效果比赛的评估指标决定了你的调参方向。如果官方用 Top-1 Acc那么模型输出就是每张 query 匹配到的最近邻类别如果用的是 RecallK那么要求真实类别出现在前 K 个结果里就算命中。这两种指标对应不同的训练策略前者更看重特征判别力后者更看重特征的鲁棒性和覆盖度。def recall_at_k(sim_matrix, labels, query_ids, k5): sim_matrix: [Q, N], labels: 图库标签, query_ids: query标签 topk_idx np.argsort(-sim_matrix, axis1)[:, :k] correct 0 for i, qid in enumerate(query_ids): hit np.any(labels[topk_idx[i]] qid) correct int(hit) return correct / len(query_ids)实际评估时要特别关注尾部类别的 RecallK普通个体准确率 99% 时尾部可能只有 60%。一个实用的做法是把图库按样本数量分桶统计样本数小于 10 的个体单独报告这些数字才是你判断模型是否过拟合到高频个体的依据。4.2 推理优化ONNX导出与批处理比赛演示和实际部署的场景不同但优化路径一致。PyTorch 模型在 CPU 上跑单张推理耗时可能 200 毫秒导出成 ONNX 后用 TensorRT 或 OpenVINO 加速可以降到 20 毫秒以内。导出前要把模型切换成 eval 模式并把动态维度固定下来。import torch.onnx model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, pig_face.onnx, input_names[input], output_names[embedding], dynamic_axes{input: {0: batch}, embedding: {0: batch}}, opset_version17, )dynamic_axes里把 batch 维度设成动态这样同一个模型可以同时接受单张查询和批量图库提取的任务避免分别导出两个模型。ONNX 导出后建议用onnxruntime自带的模型优化器做一次图优化再把固定 batch 的版本交给 TensorRT 做 FP16 量化。如果目标环境是 CPU优先选 OpenVINO 而不是 TensorRT。4.3 比赛zip里的工程化陋习与对策这类比赛项目 zip 里最常见的工程化问题有三个一是路径写死训练代码里硬编码了绝对路径换机器就崩二是随机种子未固定导致实验结果不可复现三是没有 checkpoint 恢复机制训练中断必须从头来。import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark FalsedeterministicTrue会让卷积选择确定性的算法训练速度稍微下降但结果可复现。benchmarkFalse关闭了 cuDNN 的自动调优避免同一个模型在不同机器上产生不同行为。这两个设置加在训练脚本第一行是比赛复现的基本功也是工程协作的底线。5. 三个提分技巧硬样本挖掘、测试时增强与特征后处理第一个技巧是困难样本挖掘。用当前最好的模型对训练集做一次前向推理找出每个 query 的错误匹配和低置信度匹配把这些样本的权重放大或加入一个额外的难例数据集参与训练。具体做法是维护一个「错例池」每次 epoch 开始时从池里采样一部分样本混入训练集比例控制在 20% 左右。这个操作在 Pig 数据集上通常能带来 1 到 3 个百分点的提升性价比远高于换更大的骨干网络。第二个技巧是测试时增强。对每张 query 图片做水平翻转把原始特征和翻转特征相加后归一化再拿去检索。实现很简单代码只需要几行from torchvision import transforms flip_transform transforms.Compose([ transforms.RandomHorizontalFlip(p1.0), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) feat_orig model(orig_tensor.unsqueeze(0)) feat_flip model(flip_transform(orig_img).unsqueeze(0)) feat_fused F.normalize(feat_orig feat_flip, p2, dim1)这里的相加不是平均而是先加再归一化保留了方向信息又消除了模长差异。TTA 会把推理时间加倍但比赛环境下的特征提取通常是离线完成线上只有查询阶段的单次前向所以图库特征用 TTA 提取、query 用 TTA 提取完全不影响实时性。第三个技巧是特征后处理中的 PCA 白化。在图库上统计特征的协方差矩阵做 PCA 降维到 256 维后再白化能够有效消除不同拍摄设备、不同光照引入的偏移分量。这一步对单个模型的提升不如前两个技巧明显但如果你做了多模型集成白化能显著提高特征融合的稳定性。最后提醒一个容易被忽略的点所有技巧加完之后回到测试集重新计算一次完整 pipeline 的 RecallK。比赛包的评估脚本如果只提供了 Top-1 Acc你自己要补上 Recall5 和 Recall10 的统计这两个数字比单个准确率更能反映特征质量。用同一套评估逻辑对比加 TTA 前后的差异再去决定是否保留这个耗时操作。本文还有配套的精品资源点击获取