ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

开放集动物再识别如何落地?校准相似度与图聚类实战指南

开放集动物再识别如何落地?校准相似度与图聚类实战指南 在野生动物监测、动物园个体管理和智慧养殖项目中经常遇到这样的场景摄像头拍到一只动物需要快速判断它是不是之前已经记录过的个体或者判断它是否属于一个新出现的身份。很多人第一反应是“这不就是一个图像检索任务吗”但真正落地时会比想象中复杂得多不同机位光照差异大动物姿态多变同一个体在不同季节的外形可能完全不同而不同个体之间又可能长得非常接近。更麻烦的是现实场景里随时会出现“从未见过的个体”这就不能再用传统闭集分类的思路去处理了。本文围绕 Open-Set Animal Re-Identification开放集动物再识别展开重点拆解其中两类关键技术Calibrated Similarity校准相似度和 Graph Clustering图聚类。会先讲清楚它们各自解决什么问题再给出一个可运行的简化版代码示例最后整理落地时常见的坑和工程建议。适合正在做再识别、智慧养殖、野生动物监测相关项目的同学阅读。1. 背景与核心概念1.1 动物再识别要解决什么问题Animal Re-Identification动物再识别本质上是一个“跨摄像头、跨时间段匹配同一个体”的任务。给定一张查询图query需要从一批已经标注过的图库图gallery中找出所有属于同一只动物的图片。这个任务在行人再识别Person Re-ID中非常常见但迁移到动物上会更难主要原因有三个外观可变性动物没有固定着装皮毛花纹可能随季节、光照、姿态发生明显变化。类间相似性同一品种的个体之间区分度可能非常低尤其是斑马、奶牛、老虎这类条纹或斑点动物。标注成本高人工标注动物个体需要专业背景标注错误率也比行人更高。所以动物再识别通常需要更鲁棒的特征表达也更依赖后处理阶段的结构化约束。单纯“查一张图排序打分”的方式往往达不到实际需求。1.2 开放集设置与闭集设置的区别传统分类模型默认使用闭集closed-set假设训练集和测试集的身份类别完全一致模型只需要把输入归到已知类别之一。但真实场景并不是这样。野生动物观测站记录到的动物、养殖场新购入的牲畜、动物园新引入的个体都可能不属于历史数据集中的任何已知身份。这种设置称为开放集open-set。开放集再识别有两点关键要求能找出查询图与图库中哪一张图属于同一只动物。能判断查询图是否属于“图库中完全不存在的个体”。第二点在工业落地时尤其重要。如果没有开放集判断能力系统就会把陌生个体强行匹配给某只已知动物造成错误统计、重复计数等问题。可以这样简单理解设置查询身份是否属于图库核心输出典型场景闭集再识别一定属于相似度排序列表学术基准测试开放集再识别可能不属于身份归属 陌生判定实际监控系统1.3 为什么需要校准相似度与图聚类在开放集动物再识别中最简单的做法是提取特征计算余弦相似度设定一个阈值判断是否同一身份。但这个做法有两个隐患第一相似度分数并不可靠。不同类别之间的相似度分布往往重叠严重很难找到统一阈值同时保证召回率和精确率。有些身份之间的相似度天然偏低有些则偏高直接比较相似度大小容易误判。第二逐对匹配缺乏全局视角。单张图与单张图之间的相似度容易受到姿态、遮挡、光照等因素干扰。如果图库中有同一个体的多张图片它们彼此之间应该是互相支持的逐对匹配没有利用这种关系。因此出现了两条改进路线校准相似度让分数更有物理意义图聚类让匹配决策在全局结构上完成。两者结合就形成了一种“先聚类、后校准、再判定”的开放集再识别方法。2. 方法整体流程拆解2.1 整体流程概览Open-Set Animal Re-Identification 的方法流程可以拆成四个阶段图像输入 - 特征提取 - 图构建与聚类 - 相似度校准与开放集判定注意这里的顺序不是固定的。比较常见的做法是用深度网络提取所有图像的特征。在特征空间上根据相似度关系构建图结构。对图做聚类得到一组“候选身份簇”。计算查询图与每个候选簇之间的校准相似度。根据校准相似度阈值输出“属于某已知个体”或“未知个体”。2.2 三个核心阶段第一阶段是特征提取。通常使用 ResNet、ViT 等主干网络在行人再识别数据集上预训练再通过三连损失、交叉熵损失等做度量学习。第二阶段是图聚类。把图库中的所有图像作为节点节点之间的边权是特征相似度通过聚类算法把图划分成若干个簇每个簇代表一个动物个体。第三阶段是相似度校准。由于原始相似度存在尺度不一致的问题这里引入校准模块把相似度转换为更接近“预测置信度”或者“匹配概率”的分数。这三个阶段不是孤立的。聚类效果影响校准标签的可靠性校准后的相似度又可以反过来优化聚类结果。2.3 流程各模块的职责模块输入输出核心目标特征提取网络原始图像特征向量拉近同类特征推开异类特征图聚类模块图库特征 相似度矩阵簇划分结果把属于同一身份的图片聚合在一起校准相似度模块查询特征 簇中心特征匹配概率/校准分数让分数可比、阈值可用开放集判定模块校准分数身份标签/未知标签拒绝未知身份减少误检实际论文中这三个模块可以联合训练也可以分阶段训练。后面我会用简化代码说明各模块的协作方式。3. Calibrated Similarity 原理详解3.1 相似度直接使用的困境假设我们已经有一个训练好的特征提取网络查询特征与图库特征的余弦相似度分布在 0.6 到 0.95 之间。是否可以直接用 0.8 作为阈值问题在于不同动物身份的类内相似度差异很大。例如某只虎皮花纹非常独特那么它的不同照片特征高度一致相似度可能稳定在 0.9 以上但另一只肤色均匀的动物不同照片之间的相似度可能只有 0.75。这种情况下固定阈值对前者太松对后者又太紧。此外很多动物再识别数据集中样本数极度不均衡。常见个体可能有几百张图稀有个体只有几张图。稀有个体的簇中心估计方差大对应的相似度置信度也会不同。校准的目标是学习一个“相似度到匹配概率”的映射使相同置信度下正确率一致也就是让分数更可解释。3.2 一种可落地的校准思路很多研究都假设相似度可以通过一个单调映射校准为概率。这里介绍一种工程上常用的思路温度缩放Temperature Scaling。对查询特征q和簇中心特征c原始余弦相似度记为ss cos(q, c)校准后的分数可以写作p sigmoid((s - b) / T)其中T是温度参数控制分数分布的陡峭程度。b是偏移参数控制判定阈值的位置。p可以理解为“查询图是否属于该簇”的概率估计。在验证集上我们可以用网格搜索方式寻找最优的T和b目标是让校准后的概率与真实匹配标签尽量一致。常见评价指标是二分类交叉熵或期望校准误差ECE。当然完整方法中校准模块不一定这么简单它可以是一个小神经网络输入查询特征和图库特征输出匹配置信度。但其核心思路是一致的让相似度分数具备校准性。3.3 校准与聚类的协同校准相似度并不只用于最终判定。在聚类过程中聚类置信度也可以反过来影响校准如果聚类结果非常确定说明该簇内部一致性高对应的样本可以用来训练校准模块。如果聚类结果模糊例如一个节点与多个簇都有较高相似度说明该样本质量存疑在训练校准模块时可以降低权重。所以更完整的流程是初始聚类 - 高置信度样本训练校准模型 - 校准模型调整相似度 - 重新聚类。这个循环一般只需要迭代一到两次就能稳定。4. Graph Clustering 原理详解4.1 图库如何构建成图图聚类的前提是先把图库表示成图结构。假设图库中有 N 张图像提取特征后每个节点i对应一张图像的特征f_i。计算所有节点之间的相似度矩阵S。根据相似度矩阵构建邻接矩阵A。图构建方式直接影响聚类效果常见方式有kNN 图每个节点只连接最相似的 k 个节点。阈值图相似度高于设定阈值的节点之间连边。混合方式先做 kNN再剔除低置信度边。之所以使用图而不是直接用聚类算法是因为图结构可以保留局部邻域信息。在动物再识别场景中同一个体在不同摄像头下的图片可能分布在特征空间的不同区域通过图上的连通关系可以弥补局部相似度不足的问题。4.2 聚类模块如何工作图聚类方法有很多选择谱聚类Spectral Clustering对拉普拉斯矩阵做特征分解再把特征向量输入 K-Means。图神经网络聚类用 GNN 对节点特征做信息传递输出节点属于哪个簇的概率。连通分量与并查集在邻接矩阵上直接找连通子图。在工程上如果相似度阈值和 k 值选得好简单的并查集算法就能达到不错效果如果图结构比较复杂、存在大量弱连接谱聚类更稳健。需要特别注意的是聚类算法通常需要指定簇的数量。但在开放集再识别中图库中的真实身份数量往往是未知的因此很多方法会通过“簇置信度”来自动决定是否把一个簇拆分或合并。4.3 从簇到身份开放集判定聚类完成后每个簇对应一个候选身份。簇中心可以取簇内所有特征的平均c_j mean({ f_i | i in cluster j })对于查询图q先计算它与每个簇中心c_j的相似度再经过校准模块得到匹配概率。最终决策可以写成if max_j p(q, c_j) tau: 预测身份 argmax_j p(q, c_j) else: 预测为未知个体这里的tau是开放集判定阈值通常在验证集上通过调节误检率和召回率来确定。这个流程很简单但它把“开放集判定”变成了“阈值选择问题”所以p的可靠性非常重要。这也解释了为什么校准相似度是开放集再识别的关键一环。5. 简化版代码实现下面的示例用于说明核心思路不是对某一篇论文的完整复现。我会用随机模拟特征来演示完整流程代码基于 Python NumPy可以独立运行。5.1 项目结构animal_reid_demo/ ├── data.py # 模拟数据生成 ├── cluster.py # 图聚类 ├── calibrate.py # 相似度校准 └── main.py # 完整流程5.2 模拟数据生成文件路径animal_reid_demo/data.pyimport numpy as np def make_features(num_known10, imgs_per_known8, num_unknown5, feature_dim128, noise0.20, seed42): 生成模拟特征。 - num_known: 已知个体数量 - imgs_per_known: 每个已知个体在 gallery 中的图片数 - num_unknown: query 中未知个体数量 - noise: 类内噪声大小 rng np.random.default_rng(seed) # 每个个体的“原型”特征 known_centers rng.normal(0, 1, size(num_known, feature_dim)) unknown_centers rng.normal(0, 1, size(num_unknown, feature_dim)) # 归一化辅助函数 def normalize(x): return x / np.linalg.norm(x, axis1, keepdimsTrue) known_centers normalize(known_centers) unknown_centers normalize(unknown_centers) # 生成 gallery每个已知个体若干张图 gallery_features [] gallery_labels [] for person_id, center in enumerate(known_centers): for _ in range(imgs_per_known): feat center noise * rng.normal(0, 1, sizefeature_dim) gallery_features.append(feat) gallery_labels.append(person_id) # 生成 query一部分来自已知个体一部分来自未知个体 query_features [] query_labels [] for person_id, center in enumerate(known_centers): feat center noise * rng.normal(0, 1, sizefeature_dim) query_features.append(feat) query_labels.append(person_id) for person_id, center in enumerate(unknown_centers): feat center noise * rng.normal(0, 1, sizefeature_dim) query_features.append(feat) query_labels.append(100 person_id) # 用 100 开头区分未知身份 gallery_features normalize(np.array(gallery_features)) query_features normalize(np.array(query_features)) return { gallery_features: gallery_features, gallery_labels: np.array(gallery_labels), query_features: query_features, query_labels: np.array(query_labels), }这段代码生成了两类数据gallery 中只包含已知个体query 中既包含已知个体也包含未知个体。后面我们用 gallery 聚类建立“候选身份”再用 query 验证开放集判定能力。5.3 相似度计算与图聚类文件路径animal_reid_demo/cluster.pyimport numpy as np def cosine_similarity(features): 计算一个矩阵内部两两之间的余弦相似度。 norm features / np.linalg.norm(features, axis1, keepdimsTrue) sim norm norm.T return np.clip(sim, -1.0, 1.0) def build_knn_graph(sim_matrix, k5, eps1e-8): 构建 kNN 邻接矩阵。 只在每个节点相似度最高的 k 个邻居之间建立连接。 n sim_matrix.shape[0] adj np.zeros((n, n)) for i in range(n): # 排除自身 order np.argsort(sim_matrix[i])[::-1] order order[order ! i] for j in order[:k]: if sim_matrix[i, j] eps: adj[i, j] 1 adj[j, i] 1 return adj class UnionFind: def __init__(self, n): self.parent list(range(n)) self.rank [0] * n def find(self, x): while self.parent[x] ! x: self.parent[x] self.parent[self.parent[x]] x self.parent[x] return x def union(self, a, b): ra, rb self.find(a), self.find(b) if ra rb: return if self.rank[ra] self.rank[rb]: ra, rb rb, ra self.parent[rb] ra if self.rank[ra] self.rank[rb]: self.rank[ra] 1 def graph_clustering(adj_matrix, num_nodes): 在邻接矩阵上使用并查集找连通分量。 返回每个节点所属簇的 id。 uf UnionFind(num_nodes) for i in range(num_nodes): for j in range(i 1, num_nodes): if adj_matrix[i, j] 0: uf.union(i, j) root_to_new {} cluster_ids np.zeros(num_nodes, dtypeint) for i in range(num_nodes): root uf.find(i) if root not in root_to_new: root_to_new[root] len(root_to_new) cluster_ids[i] root_to_new[root] return cluster_ids这里用 kNN 图加并查集是为了避免在不知道真实身份数量时强行设置n_clusters。kNN 图的k值是一个超参数实际使用中需要根据验证集调整。5.4 校准相似度模块文件路径animal_reid_demo/calibrate.pyimport numpy as np def sigmoid(x): return 1.0 / (1.0 np.exp(-x)) def calibrated_score(sim, T, b): 温度缩放后的匹配概率。 return sigmoid((sim - b) / T) def search_best_calibration(sim_values, match_flags, T_list, b_list): 在验证集上网格搜索最优的 T 和 b。 sim_values: 相似度数组 match_flags: 1 表示同一身份0 表示不同身份 best_acc -1.0 best_params (1.0, 0.0) for T in T_list: for b in b_list: prob calibrated_score(sim_values, T, b) pred (prob 0.5).astype(int) acc (pred match_flags).mean() if acc best_acc: best_acc acc best_params (T, b) return best_params, best_acc这段代码的输入是两个数组sim_values是查询与簇中心的相似度match_flags表示哪些匹配是真正同一身份。通过验证集搜索可以让校准后的概率更接近“是否同一身份”的标签。5.5 主流程与评估文件路径animal_reid_demo/main.pyimport numpy as np from data import make_features from cluster import ( cosine_similarity, build_knn_graph, graph_clustering, ) from calibrate import calibrated_score, search_best_calibration def cluster_center(gallery_features, cluster_ids, num_clusters): centers [] for c in range(num_clusters): feat gallery_features[cluster_ids c].mean(axis0) norm np.linalg.norm(feat) centers.append(feat / (norm 1e-8)) return np.array(centers) def normalize_rows(x): return x / np.linalg.norm(x, axis1, keepdimsTrue) def main(): data make_features(seed42) gallery data[gallery_features] gallery_labels data[gallery_labels] query data[query_features] query_labels data[query_labels] # 1. 图库相似度与聚类 gallery_sim cosine_similarity(gallery) adj build_knn_graph(gallery_sim, k5) cluster_ids graph_clustering(adj, gallery.shape[0]) num_clusters int(cluster_ids.max()) 1 centers cluster_center(gallery, cluster_ids, num_clusters) print(gallery 聚类簇数量:, num_clusters) # 2. 构造验证数据用 gallery 与簇中心相似度 query_gallery_sim normalize_rows(query) centers.T # 构造匹配标签 # 把 query 的身份映射到簇 id cluster_of_person {} for pid in np.unique(gallery_labels): idx np.where(gallery_labels pid)[0][0] cluster_of_person[pid] cluster_ids[idx] match_flags [] sim_values [] for i in range(query.shape[0]): labels_here query_labels[i] for c in range(num_clusters): sim_values.append(query_gallery_sim[i, c]) if labels_here in cluster_of_person and cluster_of_person[labels_here] c: match_flags.append(1) else: match_flags.append(0) sim_values np.array(sim_values) match_flags np.array(match_flags) # 3. 搜索校准参数 T_list np.linspace(0.05, 1.0, 20) b_list np.linspace(0.0, 1.0, 21) (best_T, best_b), acc search_best_calibration( sim_values, match_flags, T_list, b_list ) print(最优 T:, round(best_T, 3), 最优 b:, round(best_b, 3)) print(校准分类准确率:, round(acc, 3)) # 4. 开放集判定 # 对每个 query取与所有簇中心的最大校准概率 prob_matrix calibrated_score(query_gallery_sim, best_T, best_b) max_prob prob_matrix.max(axis1) pred_cluster prob_matrix.argmax(axis1) tau 0.6 # 开放集阈值实际项目中应在验证集上调优 pred_unknown max_prob tau # 已知个体识别准确率 known_mask np.array([q in cluster_of_person for q in query_labels]) known_correct 0 known_total int(known_mask.sum()) # 未知个体拒绝率 unknown_mask ~known_mask unknown_reject 0 unknown_total int(unknown_mask.sum()) for i in range(query.shape[0]): if known_mask[i]: expected_cluster cluster_of_person[query_labels[i]] if (not pred_unknown[i]) and pred_cluster[i] expected_cluster: known_correct 1 else: if pred_unknown[i]: unknown_reject 1 print(已知个体识别率: {}/{} {:.3f}.format( known_correct, known_total, known_correct / max(known_total, 1) )) print(未知个体拒绝率: {}/{} {:.3f}.format( unknown_reject, unknown_total, unknown_reject / max(unknown_total, 1) )) if __name__ __main__: main()运行方式cd animal_reid_demo python main.py运行后你可以看到类似下面的输出gallery 聚类簇数量: 10 最优 T: 0.1 最优 b: 0.65 校准分类准确率: 0.981 已知个体识别率: 10/10 1.000 未知个体拒绝率: 5/5 1.000这个模拟结果比较理想是因为模拟数据本身可分性较好。真实项目中特征质量、噪声大小、k 值选择都会影响结果。这里需要注意的是真实论文中的图聚类模块通常比并查集复杂得多可能包含可学习的图神经网络、基于梯度传播的聚类损失等。但演示版保留了核心逻辑先聚类再校准最后开放集判定。6. 常见问题与排查思路在实际落地过程中最容易遇到下面这些问题问题现象常见原因解决思路聚类簇数量远大于真实个体数kNN 图 k 值过大弱连接过多降低 k或提高建图相似度阈值同一个体被分成多个簇类内差异大图被切断增大 k或先做特征增强/重排序未知个体被误判为已知个体开放集阈值 tau 过低在验证集上统计误检率适当提高阈值校准后概率整体偏高温度 T 太小时 sigmoid 过于尖锐增大 T使概率分布更平缓已知个体识别率低簇中心估计不准检查噪声水平是否需要更鲁棒的特征聚合方式图库太大并查集计算慢邻接矩阵 O(N^2) 内存爆炸分块处理、近似近邻检索替代全量相似度计算下面展开说明几个高频问题。6.1 聚类簇数量不稳定怎么办并查集方法对 kNN 图中的边非常敏感。k3 时可能得到 30 个簇k5 时变成 12 个簇k8 时变成 5 个簇。这时需要结合验证集做超参数选择。更稳健的做法是使用谱聚类并配合“特征间隙”eigengap自动确定簇数量。特征间隙是拉普拉斯矩阵特征值之间的差距通常表示特征空间中自然的簇边界。6.2 开放集阈值如何选择开放集判定本质上是在“误接受率”和“误拒绝率”之间做权衡阈值提高更容易拒绝样本未知个体误检降低但会把已知个体误判为未知。阈值降低已知个体更容易被接受但未知个体也容易被误匹配。工程上建议绘制 ROC 曲线根据业务需求选择工作点。例如在动物计数任务中误匹配可能导致个体重复计数这时应该更保守提高阈值。6.3 特征噪声太大怎么办模拟数据中噪声是可控的真实数据中噪声来自光照、遮挡、运动模糊等。除了换更强的骨干网络还可以在聚类之前使用重排序re-ranking方法例如基于 k-reciprocal 编码的排序优化让相似度分布更清晰。但重排序会显著增加计算量实时性要求高的场景需要谨慎使用。7. 最佳实践与工程建议如果你准备把这类方法落地到真实项目中有几点建议可以参考。7.1 数据划分必须保证身份隔离训练、验证、测试三部分数据的身份编码不能有重叠。如果同一只动物的图片同时出现在训练集和验证集评估结果会虚高开放集判定能力也会被高估。7.2 对类别不平衡做专门处理动物数据集中个体图像数量差异很大有的个体几十张有的只有两三张。簇中心估计时样本少的簇方差大校准模块容易对它们过度自信。可以考虑簇中心使用加权平均权重与样本数量相关。对样本数量过少的簇适当降低置信度。7.3 评估指标要分层看开放集再识别不能只看整体准确率。建议至少同时报告已知个体的 Rank-1 / mAP。未知个体的拒绝率。组合指标例如 open-set F-score 或 ACCE。这样可以防止“模型把所有样本都判为未知”这种看似正确但没有使用价值的情况被误判为有效。7.4 大图库场景要控制图规模图聚类需要计算 O(N^2) 相似度当图库达到百万级别时内存和算力压力非常大。建议方案先用近似最近邻检索例如 Faiss召回每个节点的 top-k 邻居。只对高置信度邻居建边把稠密图变成稀疏图。分块聚类后再合并簇。7.5 保留人工审核接口自动识别在开放集场景下不可能 100% 正确。建议对低置信度样本、边界样本设置人工复核队列。实际项目中把自动识别准头和人工复核接口分开设计比单纯追求模型精度更稳妥。7.6 校验模块要定期重训环境变化后摄像头角度、光照、季节都会影响特征分布。原来训练好的校准参数可能逐渐失效。建议定期在积累的新标注数据上重新搜索校准参数或者设计在线更新机制。8. 总结与下一步学习这篇文章围绕 Calibrated Similarity 和 Graph Clustering 两条主线梳理了开放集动物再识别的基本流程特征提取、图构建与聚类、相似度校准、开放集判定。还给出了一个完整的简化代码示例方便你理解各模块之间的协作关系。如果你准备深入研究下一步建议重点关注这三个方向端到端联合训练。很多方法把聚类损失、校准损失和特征提取放在同一个优化目标中效果往往比分阶段训练更好但工程实现复杂度也更高。更细粒度的图结构建模。除了相似度连接还可以引入时间、摄像头位置、身体部位等先验信息帮助聚类模块处理复杂场景。大规模增量更新。真实系统会不断出现新个体如何在不重新全量聚类的情况下高效更新图结构和校准模块是一个很有价值的研究方向。说到底校准相似度和图聚类都不是孤立的技巧它们解决的是“特征空间里的分数不可靠”和“个体匹配缺乏全局结构”这两个核心痛点。动手写一个 demo 只是第一步建议你在自己的数据上先跑通流程再逐步引入更复杂的模块。你对哪一块感兴趣可以在评论区留言后面可以继续展开。
返回列表