ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

开集动物个体重识别:校准相似度与图聚类原理与工程实践

开集动物个体重识别:校准相似度与图聚类原理与工程实践 做动物个体识别项目时最大的难点往往不是“这两张图是不是同一只动物”而是“测试阶段突然冒出来一只训练集里从来没见过的新个体”。传统 Closed-Set 分类器会把所有图片强行归入已知类别真实野生动物监测场景里相机部署一段时间后总会拍到新个体模型就会在这种时刻失去可靠性。本文围绕 Calibrated Similarity 和 Graph Clustering 这套组合思路完整拆解 Open-Set Animal Re-Identification 的方法原理、模块实现、代码示例和工程落地建议。适合正在做动物个体识别、行人重识别、开集识别方向的学生或工程师参考。1. 开集动物个体重识别是什么1.1 从重识别说起重识别Re-IdentificationRe-ID要解决的问题是给定一张查询图判断它是否和数据库里某一张历史图像属于同一个目标个体。对动物而言这个“个体”就是某一只大象、某一只斑马、某一只鲸鲨而不是“大象”这个物种。传统方法依赖人工设计的斑点、条纹、鳍部特征近几年则主要用深度学习提取全局或局部特征再用度量学习拉近同一个个体的特征距离。如果把它形式化可以理解为训练阶段模型见过一部分个体每个个体有多张图像。测试阶段给定候选图像集合模型需要回答这些图像分别属于哪些个体。问题在于大多数 Re-ID 方法默认“测试阶段出现的个体都在训练集里出现过”这就是 Closed-Set 假设。但真实场景几乎不满足这个假设。1.2 为什么 Open-Set 更贴近真实场景野生动物监测通常在一个固定区域内部署多个相机。第一次部署时我们可能只拍到了区域内一部分个体。三个月后一只从未出现过的雄狮进入领地相机拍到了它。这时候系统面对的就是一个“新个体”。如果用封闭集模型这只雄狮的图像会被强制分配到某个已知个体 ID 上结果是新个体没有被识别出来反而污染了原有 ID 的图库。后续聚类和统计出现偏差种群数量统计会偏少或偏多。对动物保护决策产生误导。Open-Set 的核心目标就是让系统既能认出已知个体又能识别出“这是一个数据库里没有的新个体”。这比 Closed-Set 要难得多因为模型既要保持已知类别的判别力又要对未知类别有拒识能力。1.3 两个核心技术名词本文标题中的两个关键词是解决 Open-Set 问题的两个关键环节Calibrated Similarity校准相似度对原始特征相似度进行修正让高不确定性的匹配分数降下来减少“看似相似但实际不是”的误匹配。Graph Clustering图聚类把测试图像看成一个图上的节点节点之间用校准后的相似度连接边然后通过社区发现算法把属于同一个体的节点聚成一簇。这套思路最直观的价值是不再直接给每张图分配一个固定 ID而是把整批测试图放到一起做聚类分析个体数量也是聚类结果自然给出的天然适合 Open-Set。2. 整体方法框架2.1 四个核心模块一个完整的 Open-Set Animal Re-ID 流程通常由四个模块组成模块作用关键输出特征提取将图像编码为向量表示特征向量 Embedding相似度校准修正原始特征相似度校准后的相似度矩阵图构建与聚类将相似度转化为图结构并分组聚类标签开集后处理识别并分离新个体最终个体 ID特征提取模块在训练阶段通过度量学习得到相似度校准模块关注如何抑制不可靠匹配图聚类模块关注如何把“成对相似度”提升为“全局分组结果”开集后处理则决定哪些簇足够可信、哪些簇需要被标记为新个体。2.2 处理流程推理阶段的处理流程如下对测试集所有图像提取特征和不确定性值。对特征做 L2 归一化计算两两相似度。使用不确定性对相似度矩阵进行校准。基于校准相似度构建 kNN 图。在图结构上运行社区发现算法得到初步簇。对每个簇计算簇内一致性指标判断簇是已知个体还是新个体。这套流程不依赖预设的个体数量因此比“先训练分类器再预测”的方法更适合动态变化的野外数据。2.3 与通用 Re-ID 的区别通用 Re-ID 在推理时通常只做“查询图 vs 数据库图”的两两比对然后按分数排序。Open-Set 方法更重视全局一致性两两相似度只代表局部关系容易受光照、遮挡、姿态影响。图聚类把某张图与周围一大批图的连接关系都纳入判断能平滑掉局部噪声。Open-Set 还需要回答“这个簇是不是新个体”所以后处理比封闭集多一步。3. 环境准备与数据准备3.1 实验环境本文示例代码以 Python 为基础版本需要根据你的项目实际情况调整重点演示实现思路。常见环境如下python3.8 torch2.0 torchvision0.15 numpy1.24 faiss-cpu networkx3.0 scikit-learn1.2 opencv-python安装命令可参考pip install torch torchvision pip install faiss-cpu pip install networkx scikit-learn opencv-python如果 GPU 环境可用faiss 可以安装 GPU 版处理万级规模图像时速度更快。3.2 数据集目录结构动物 Re-ID 数据集的常见组织方式是按个体 ID 分目录data/ ├── train/ │ ├── id_0001/ │ │ ├── 0001.jpg │ │ └── 0002.jpg │ ├── id_0002/ │ └── ... ├── gallery/ │ ├── id_0001/ │ └── ... └── query/ ├── 0001.jpg └── ...训练时使用 train 目录每个子目录代表一个已知个体。测试时把待识别图像放到一个目录里不要求知道它属于哪个个体由模型聚类得出结果。3.3 图像预处理动物图像通常来自野外相机尺寸和清晰度差异较大。预处理阶段建议做from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomAffine(degrees10, translate(0.1, 0.1)), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) test_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])需要说明的是Resize 会丢失部分细节实际项目中可以按数据集分辨率选择 Resize 或 CenterCrop保留身体关键区域。4. Calibrated Similarity 相似度校准4.1 为什么需要校准深度模型提取的特征不是“同等可靠”的。一张清晰正脸照的特征置信度和一张严重遮挡、运动模糊的照片的特征置信度理应不同。但普通余弦相似度没有考虑这一点它只计算两个向量之间的夹角不管这两个向量本身是否可靠。在动物 Re-ID 场景中这种问题尤其明显同一只动物的两张模糊照片由于背景噪声产生较高相似度属于“可靠但碰巧相似”。不同个体的特征非常接近在度量空间中被挤压在一起容易误判。剪影、局部遮挡会让特征偏向背景信息带来系统偏差。因此我们需要一种手段让不可靠特征参与匹配时自动降低其影响力。4.2 校准的常见方式校准方式思路适用场景温度缩放对相似度除以温度参数提升置信度差异特征空间较规范时不确定性加权模型额外预测不确定性按不确定性降低匹配分数野外遮挡、模糊样本多时邻域一致性校准根据周边样本的相似度分布调整当前匹配分数数据集规模较大时属性辅助校准利用物种、性别、年龄段等属性修正相似度有额外标注信息时本文采用“不确定性加权”的思路因为它在实现上比较直接也符合 Calibrated Similarity 的核心语义先估计每张图像的不可靠程度再用它修正匹配矩阵。4.3 代码实现首先定义带不确定性分支的特征提取网络# models/feature_extractor.py import torch import torch.nn as nn import torchvision.models as models class FeatureExtractor(nn.Module): def __init__(self, embed_dim512, pretrainedTrue): super().__init__() backbone models.resnet50(pretrainedpretrained) # 去掉 ResNet 最后的全局池化层和分类层 self.backbone nn.Sequential(*list(backbone.children())[:-2]) self.embed_head nn.Sequential( nn.Conv2d(2048, 512, kernel_size1), nn.BatchNorm2d(512), nn.ReLU(inplaceTrue), nn.AdaptiveAvgPool2d(1) ) self.feature_fc nn.Linear(512, embed_dim) self.uncertainty_fc nn.Linear(512, 1) def forward(self, x): feat self.backbone(x) feat self.embed_head(feat).flatten(1) embedding self.feature_fc(feat) embedding nn.functional.normalize(embedding, p2, dim1) uncertainty torch.sigmoid(self.uncertainty_fc(feat)) return embedding, uncertainty网络输出两个东西embeddingL2 归一化后的特征向量用于相似度度量。uncertainty经过 sigmoid 的值范围在 0 到 1 之间越大表示该样本越不可靠。接着实现校准相似度计算# metrics/calibrated_similarity.py import torch def calibrated_cosine_similarity(features, uncertainties, alpha1.0): features: (N, D) 已归一化的特征矩阵 uncertainties: (N, 1) 不确定性值 alpha: 校准强度超参数 # 余弦相似度矩阵 sim torch.mm(features, features.t()) confidence 1.0 - uncertainties.squeeze(1) # (N,) confidence_matrix confidence.view(-1, 1) * confidence.view(1, -1) calibrated_sim sim * torch.pow(confidence_matrix, alpha) return calibrated_sim这段代码的原理是当某个样本不确定性较高时confidence较小那么该样本对应行和列的相似度都会被压低。alpha控制压低强度alpha0时退化为普通余弦相似度alpha越大低置信度样本的匹配分数降得越明显。4.4 参数调整建议alpha的选择没有固定值需要结合验证集表现来调alpha过小校准效果不明显模糊样本仍然容易误匹配。alpha过大部分低置信度但真实匹配的正样本对也会被压低导致召回率下降。实际项目中建议先在不做校准的条件下跑出一批匹配结果观察哪些错误匹配来自模糊样本再逐步增大alpha观察指标变化。5. Graph Clustering 图聚类5.1 从相似度到图有了校准后的相似度矩阵我们可以把所有测试图像看作带权全连接图上的节点。理论上任意两张图之间都有一条边但全连接图计算量和噪声都太大所以一般保留每个节点的 Top-k 最近邻居作为边得到 kNN 图。这一步的工程意义有两个减少后续聚类算法的计算量。剪掉大量低质量相似度边减少噪声连接。5.2 kNN 图构建代码使用 faiss 的 Flat Index 做内积搜索因为特征已经 L2 归一化内积等价于余弦相似度# clustering/knn_graph.py import faiss import numpy as np def build_knn_graph(features, k10): features: np.ndarray, shape (N, D), 必须是 float32 k: 每个节点保留的邻居数量 features np.ascontiguousarray(features, dtypenp.float32) n, d features.shape index faiss.IndexFlatIP(d) index.add(features) sims, idxs index.search(features, k 1) # 多查一个因为会包含自身 sims sims[:, 1:] idxs idxs[:, 1:] edges [] for i in range(n): for j, s in zip(idxs[i], sims[i]): if s 0: continue edges.append((int(i), int(j), float(s))) edges.append((int(j), int(i), float(s))) # 构造无向图 return edges注释里有个容易踩的坑k 1是因为 IndexFlatIP 会把自己的相似度也查出来实际使用时要去掉第一列。5.3 Louvain 社区发现networkx 提供了 Louvain 社区发现算法它可以自动发现社区数量不需要事先指定个体数因此很适合 Open-Set 场景# clustering/graph_clustering.py import networkx as nx from networkx.algorithms import community def graph_clustering(edges, seed42, resolution1.0): G nx.Graph() G.add_weighted_edges_from(edges) clusters community.louvain_communities( G, weightweight, seedseed, resolutionresolution ) labels [0] * G.number_of_nodes() for cluster_id, nodes in enumerate(clusters): for node in nodes: labels[node] cluster_id return labels, clustersresolution参数可以理解为“社区分裂倾向”数值较大时倾向于产生更多小社区。数值较小时倾向于合并成大社区。在动物 Re-ID 中一个个体通常有多个图像但如果图像数量很少聚类时很容易被拆散可以适当调低resolution。5.4 Open-Set 后处理聚类完成后还需要识别哪些簇对应“新个体”。这里给出一种简单的工程判断方法# clustering/open_set_postprocess.py import numpy as np def assign_open_set_ids(labels, features, sim_matrix, min_cluster_size3, min_confidence0.3): 根据簇大小和簇内平均相似度判断是否属于已知个体。 返回: final_ids: list, 每个元素为最终分配的个体 ID is_unknown: list, 每个元素是否为未知个体 labels np.asarray(labels) n len(labels) final_ids [-1] * n is_unknown [False] * n unique_clusters list(set(labels)) next_id 0 for c in unique_clusters: idx np.where(labels c)[0] if len(idx) min_cluster_size: for i in idx: is_unknown[i] True final_ids[i] -1 continue # 计算簇内平均相似度 cluster_sim [] for i in range(len(idx)): for j in range(i 1, len(idx)): cluster_sim.append(sim_matrix[idx[i], idx[j]]) avg_sim float(np.mean(cluster_sim)) if cluster_sim else 0.0 if avg_sim min_confidence: for i in idx: is_unknown[i] True final_ids[i] -1 else: for i in idx: final_ids[i] next_id next_id 1 return final_ids, is_unknown这段代码包含两个判断条件簇内图像数量太少无法形成可靠个体特征视为孤立点或新个体。簇内平均相似度过低说明簇内图像可能来自多个不同个体直接标记为未知更安全。实际项目中min_cluster_size和min_confidence都需要根据验证集统计得到而不是拍脑袋定。比如统计所有已知个体在测试集上的平均簇内相似度然后取略低于该值的数作为阈值。6. 训练流程与损失函数6.1 网络结构前面已经给出了特征提取网络的代码。实际训练时通常还需要一个分类头用于辅助学习或者直接使用度量学习损失。推荐的结构是主干网络ResNet50。嵌入头输出 L2 归一化的特征向量。不确定性头输出每个样本的不确定性。分类头将特征映射到训练集中的个体 ID。# models/full_model.py import torch.nn as nn from models.feature_extractor import FeatureExtractor class AnimalReIDModel(nn.Module): def __init__(self, num_classes, embed_dim512, pretrainedTrue): super().__init__() self.extractor FeatureExtractor(embed_dimembed_dim, pretrainedpretrained) self.classifier nn.Linear(embed_dim, num_classes) def forward(self, x): embedding, uncertainty self.extractor(x) logits self.classifier(embedding) return embedding, uncertainty, logits6.2 损失函数组合训练阶段建议同时使用三元组损失和交叉熵损失三元组损失负责调整特征空间结构让同类距离近、异类距离远。交叉熵损失负责提供类别级别的监督信号稳定训练过程。一个简洁的 batch-hard 三元组损失实现如下# losses/triplet_loss.py import torch import torch.nn as nn def batch_hard_triplet_loss(embeddings, labels, margin0.3): embeddings: (N, D) labels: (N,) device embeddings.device labels labels.view(-1) dist torch.cdist(embeddings, embeddings, p2) pos_mask labels.unsqueeze(0) labels.unsqueeze(1) eye torch.eye(embeddings.size(0), devicedevice, dtypetorch.bool) pos_mask pos_mask ~eye neg_mask ~pos_mask neg_mask neg_mask ~eye # 距离矩阵中正样本位置保留原值其余位置为 0 hardest_positive torch.max( torch.where(pos_mask, dist, torch.zeros_like(dist)), dim1 ).values # 负样本位置保留原值其余位置设为极大值 neg_dist dist (~neg_mask).float() * 1e6 hardest_negative torch.min(neg_dist, dim1).values loss torch.clamp(hardest_positive - hardest_negative margin, min0.0) return loss.mean()训练时需要注意采样策略。三元组损失对 batch 内 ID 分布敏感建议每个 batch 包含若干个个体每个个体至少 4 张图否则很难找到有效的 positive 对。6.3 训练循环骨架下面是一个简化的训练循环示例只演示核心思路# train.py 核心片段 import torch import torch.nn as nn from torch.utils.data import DataLoader from models.full_model import AnimalReIDModel from losses.triplet_loss import batch_hard_triplet_loss device torch.device(cuda if torch.cuda.is_available() else cpu) model AnimalReIDModel(num_classesnum_train_ids).to(device) optimizer torch.optim.Adam(model.parameters(), lr1e-4) ce_loss_fn nn.CrossEntropyLoss() for epoch in range(num_epochs): model.train() for imgs, ids in train_loader: imgs imgs.to(device) ids ids.to(device) embedding, uncertainty, logits model(imgs) triplet_loss batch_hard_triplet_loss(embedding, ids, margin0.3) ce_loss ce_loss_fn(logits, ids) loss triplet_loss 0.5 * ce_loss optimizer.zero_grad() loss.backward() optimizer.step() # 每个 epoch 结束后在验证集上评估 print(fepoch {epoch}, loss: {loss.item():.4f})不确定性分支在训练时不强制增加额外损失网络会通过“低质量样本特征不可分”间接学到较高不确定性。更复杂的论文可以给不确定性加上正则约束让它与分类置信度对齐但这里给出的版本已经可以用于工程实验。7. 推理与评估7.1 完整推理流程把前面的模块串起来推理流程可以封装成一个函数# inference.py import torch import numpy as np from metrics.calibrated_similarity import calibrated_cosine_similarity from clustering.knn_graph import build_knn_graph from clustering.graph_clustering import graph_clustering from clustering.open_set_postprocess import assign_open_set_ids torch.no_grad() def inference(model, test_loader, alpha1.0, k10, min_cluster_size3, min_confidence0.3): model.eval() all_features [] all_uncertainties [] for imgs, _ in test_loader: imgs imgs.to(device) embedding, uncertainty model.extractor(imgs) all_features.append(embedding.cpu().numpy()) all_uncertainties.append(uncertainty.cpu().numpy()) features np.concatenate(all_features, axis0) uncertainties np.concatenate(all_uncertainties, axis0) # 校准相似度 sim_matrix calibrated_cosine_similarity( torch.from_numpy(features), torch.from_numpy(uncertainties), alphaalpha ).numpy() # kNN 图 聚类 edges build_knn_graph(features, kk) labels, clusters graph_clustering(edges) # 开集后处理 final_ids, is_unknown assign_open_set_ids( labels, features, sim_matrix, min_cluster_sizemin_cluster_size, min_confidencemin_confidence ) return final_ids, is_unknown, labels, sim_matrix推理流程的关键点在于整批图像是同时参与的而不是逐张查询。这样图聚类才能用到全局信息。7.2 评估指标Open-Set Animal Re-ID 的评估通常分成两个维度已知个体部分mAPmean Average Precision排序结果中正样本排得越靠前mAP 越高。CMC Top-1 / Top-5查询图在候选集中命中的概率。未知个体部分未知样本召回率真正的新个体有多少被标记为 unknown。已知个体误报率已知个体被错误标记为 unknown 的比例。聚类纯度每个聚类簇是否只包含同一个个体。实际项目中建议以“已知个体 mAP 未知个体召回率”为主要指标因为这两个指标更能反映 Open-Set 模型的真实能力。8. 常见问题与排查思路问题现象常见原因解决思路faiss 查询结果全是自身查询时包含了自身节点搜索k 1个邻居后去掉第一列聚类结果非常碎kNN 图 k 值太小适当增大 k或降低 Louvain 的 resolution所有图被聚成一个大簇k 值太大噪声边过多减小 k提高相似度边阈值不确定性与特征无关训练时缺少不确定性监督加入不确定性伪标签或分类置信度约束新个体被并进已知簇簇内相似度阈值过低统计已知个体簇内相似度分布后抬高阈值同一只动物被拆成多个 IDmin_cluster_size 太大缩小 min_cluster_size或检查聚类参数相似度校准后性能反而下降alpha 调得过大在验证集上做网格搜索排查这类问题有一条通用路径先检查特征分布再检查相似度矩阵最后检查聚类结果。这三步可以串成一条数据管线分别输出中间结果快速定位是哪一环出了问题。9. 最佳实践与工程建议9.1 数据层面动物 Re-ID 的数据质量往往比算法更关键。建议拍摄或收集数据时尽量覆盖多角度、多光照、多姿态。如果数据量有限可以先用物种分类模型做预筛选把明显不是目标物种的图像去除。此外训练集和测试集的采集时间最好错开。如果训练集全部来自夏季测试集全部来自冬季模型会学到季节背景偏差Open-Set 表现会很差。9.2 模型层面特征维度不需要一味加大512 维在大多数动物 Re-ID 任务上已经足够。保留图像局部信息可以采用 Part-based 结构把图像切成若干水平条分别提取特征再融合。不确定性分支建议放在最后一个卷积层之后而不是直接放在分类特征上。9.3 聚类与后处理层面聚类结束后不要直接相信绝对标签。建议对每个簇额外统计簇内最大相似度与最小相似度。簇内图像的时间跨度。簇内图像来自多少个不同的相机机位。时间跨度和相机机位这两个信息在野外场景中非常有用。同一个体如果在一个小时内出现在两个相距很远的相机中就需要人工复核因为可能是误聚类。9.4 部署与日志推理服务建议保存每个样本的特征、不确定性、聚类标签以及被哪个阈值决策为 unknown 的完整日志。这样后续调阈值、复现线上问题时才有据可查。线上模型更新时不要直接替换整个特征库。建议保留旧特征库通过简单对比新旧特征在新样本上的聚类一致性防止特征空间漂移导致历史识别结果混乱。10. 总结与延伸围绕 Calibrated Similarity 和 Graph Clustering 的组合这套 Open-Set Animal Re-ID 方案的核心思路可以概括为四步用特征网络提取可靠表示用不确定性校准相似度用图聚类完成全局分组用一致性阈值识别新个体。相比传统封闭集方法它的优势在于不依赖预先固定的个体数量能够在数据动态增长的真实场景中持续工作。训练阶段三元组损失加上分类辅助损失基本能满足大多数动物数据集的需求推理阶段faiss 构建 kNN 图再配合 Louvain 聚类是工程上比较成熟且代码量可控的路线。真正决定 Open-Set 效果上限的往往不是某个网络结构而是数据采集的覆盖度、相似度校准的强度以及簇级别的置信度阈值设置。后续可以从这几个方向继续深入在不确定性分支中引入显式的伪标签监督把聚类结果反向用于难样本挖掘或者在图聚类之前先做一轮基于属性的粗筛选。如果本文对你有帮助可以收藏备用也欢迎在评论区交流你项目中遇到的 Open-Set 识别问题。
返回列表