在野生动物监测、保护区物种调查和动物行为研究中,个体再识别(Animal Re-Identification)承担的任务比普通图像分类复杂得多:摄像头拍到一只动物后,系统要判断“这只个体之前是否出现过”,如果出现过,还要回答“它是谁”。难点在于同一个体的外观会随着季节、姿态、拍摄距离和光照条件剧烈变化,而不同个体之间又可能有非常相似的花纹、体型和体表特征。更关键的是,野外抓拍数据中始终存在大量没有记录的新个体,算法不能只做“从候选集中找相似样本”的封闭式识别,还必须能在开放集(Open-Set)条件下把不认识的个体挑出来。解决这条任务链的常见技术路线是:先从图像中学到可度量的特征表示,再计算样本间的校准相似度(Calibrated Similarity),最后通过图聚类(Graph Clustering)把同一个体的图像聚合到一起,同时保留对未知身份的处理空间。
这篇文章只围绕一个主题展开:如何把“特征提取 -> 相似度校准 -> 图聚类 -> 结果验证”这条链路在一个真实可运行的代码结构里落地。内容会覆盖开放集动物再识别的任务定义、特征与相似度的关系、校准相似度的最小实现、图聚类建边与聚类策略、评估方法和常见问题排查。无论你是准备复现一篇论文,还是打算用现成模型搭建一个个体识别原型,都可以按这里的方法逐步推进。
1. 先理解开放集动物再识别的任务与评估方式
1.1 为什么动物再识别是开放集问题
如果把动物再识别当成普通的“图像检索”,可以这样理解:给定一张查询图像,算法从图库中返回相似度最高的若干图像,然后计算 Rank-1、Rank-5、mAP 这类指标。这个流程在封闭集假设下是成立的,因为候选集中只有已知个体,查询图像也一定属于其中某个身份。但在真实野外场景,这个假设几乎不成立。
野外摄像头、红外相机和群体拍摄数据中,每个拍摄时段都可能出现新个体。算法面对的输入除了“已知个体在不同角度、不同光照下的图像”,还有大量从未入库的新个体。这时系统要做的并不是简单排序,而是回答一个更严谨的问题:这条查询图像与已知个体中的哪一个属于同一身份,如果与所有已知个体的相似度都低于某个边界,又应该把它判定为未知身份。这个“未知身份”分支就是开放集识别与封闭集检索之间最重要的区别。
在技术实现上,这种区别会直接影响模型设计。封闭集识别可以训练一个分类器,最后一层输出的类别数固定为已知个体数量;开放集识别则要求模型输出一个低维 embedding,让同类样本在特征空间中靠近、异类样本远离,然后依靠相似度阈值或聚类算法完成身份划分。如果只把分类结果拿出来做后处理,几乎没有处理未知身份的能力。
1.2 相似度、校准和聚类的分工
开放集动物再识别通常被拆成三个环节,每个环节解决的问题不同。
第一个环节是特征提取。模型的输入是一张裁剪后的动物个体图像,输出是一个向量,比如 128 维或 512 维的 embedding。特征提取模型的质量决定了相似度计算的上限。如果模型把同一只动物的不同姿态映射到特征空间的不同区域,后续任何聚类算法都很难补救。
第二个环节是相似度计算与校准。得到 embedding 后,常见做法是用余弦相似度或者欧氏距离的负数来衡量两张图之间的接近程度。原始相似度的问题在于,不同图像对之间得到的分数值域不统一,直接拿一个固定阈值做判断会非常脆弱。校准的目的就是让相似度分数更接近“可解释的概率”或者“可比较的决策边界”,从而让后续聚类更稳定。
第三个环节是图聚类。把所有图像看作节点,把校准后的相似度看作节点之间的边,然后用图聚类算法把强连接的节点划分到同一个簇。聚类结果既可以是最终的身份分组,也可以作为人工复核的候选集合。相比单独用阈值判定每个图像对,图聚类能利用全局结构信息,减少孤立的误判。
1.3 评估指标先说清楚,否则调参没有方向
在进入代码之前,先确定评估方式。否则后面调温度、调 topk、调聚类数量时,很难判断效果是变好还是变差。
封闭集检索阶段常用指标是 Rank-1 和 mAP。Rank-1 表示查询图像在候选集中排第一位的图像是否属于同一身份,mAP 则衡量所有正确结果在排序中的整体位置。
开放集阶段需要额外关注两个问题:已知个体是否被正确分组,未知个体是否会被强行塞进某个已知簇。常用的评估指标包括:
- B-cubed Precision / Recall:把每个图像与同簇内其他图像的配对关系作为评价对象,能同时反映簇内纯度和完整性。
- Adjusted Rand Index(ARI):衡量聚类结果与真实身份标签的一致性,值越接近 1 越好。
- Normalized Mutual Information(NMI):衡量聚类簇和真实身份的互信息。
- 未知身份漏检率 / 误检率:有多少未知个体被错误分到了已知簇,又有多少已知个体被误标成未知。
这些指标不需要一开始全部计算。最小闭环阶段可以先看 Rank-1、B-cubed F1 和 ARI,这三个指标基本能判断链路是否跑通。
2. 环境准备与数据组织
2.1 依赖清单:至少需要哪些包
下面示例基于 PyTorch 实现,但在正式安装前要确认自己电脑上的 CUDA、Python 和 PyTorch 版本是否匹配。如果原始项目没有锁定版本,推荐先建立独立虚拟环境,再安装依赖。一般需要以下包:
| 包名 | 用途 | 说明 |
|---|---|---|
| torch | 特征提取模型与自动求导 | 1.10 以上或 2.x 均可,按显卡驱动选版本 |
| torchvision | 预训练模型与图像变换 | 与 torch 版本配套 |
| numpy | 数组与矩阵运算 | 计算相似度矩阵、构建邻接矩阵 |
| scikit-learn | 聚类算法与评估指标 | 提供谱聚类、NMI、ARI |
| networkx | 图结构与连通分量分析 | 建图、查看簇的分裂与合并 |
| matplotlib / seaborn | 可视化相似度矩阵和聚类结果 | 排查用 |
| opencv-python | 图像读取与预处理 | 有些数据集需要处理 EXIF 方向 |
| tqdm | 批量特征提取时的进度条 | 便于观察运行耗时 |
安装命令可以按环境选择:
pip install torch torchvision numpy scikit-learn networkx matplotlib seaborn opencv-python tqdm pandas实际项目中,如果用的是公司内部镜像源或者离线安装,需要提前把 wheel 包准备好。不要直接用最新版本号代替所有约束,torch 与 torchvision 的版本匹配关系应先确认。
2.2 数据目录与标签格式
动物再识别数据集通常以个体 ID 作为类别标签,但不同数据集的组织方式差异很大。这里用最通用的目录结构做说明:
dataset/ images/ known/ id_001/ 1.jpg 2.jpg id_002/ 1.jpg 2.jpg query/ q_001.jpg q_002.jpg meta/ metadata.csvknown目录下每个子目录对应一个已知个体,query目录存放待查询图像。如果已经做过多目标检测和裁剪,只需要把裁剪后的个体图像放到对应目录;如果还没有裁剪,需要先跑检测模型。
metadata.csv至少要包含文件路径和个体标签两列,便于后续构建训练集和验证集:
image_path,identity,source images/known/id_001/1.jpg,id_001,train images/known/id_001/2.jpg,id_001,train images/query/q_001.jpg,q_001,query这里有一个容易忽略的问题:训练集、验证集和测试集中的个体身份不能交叉。实际做法是先按个体 ID 划分文件,再在每个个体内部划分图像。如果直接在图像层面随机划分,同一只动物的不同图像可能同时出现在训练集和验证集中,评估结果会虚高。
2.3 一个可直接检查的环境清单
在学习环境中,建议按以下清单逐步确认环境可用:
- 确认 Python 版本是否为 3.8 到 3.11 范围内,避免某些包没编译对应版本。
- 确认 PyTorch 可以调用 CUDA:运行
python -c "import torch; print(torch.cuda.is_available())"。 - 确认
torchvision.models.ResNet18_Weights.DEFAULT能正常下载权重,如果网络受限,要提前配置权重文件路径。 - 确认
sklearn.cluster.SpectralClustering可以使用precomputed亲和矩阵。 - 准备一个 10 到 20 张图的测试目录,先把代码流程跑通,再上完整数据集。
这个检查清单不是形式化步骤。特征提取、相似度计算、聚类、可视化每一步都可能因为环境问题中断,提前把最基础的依赖和环境变量检查完,能省下大量排查时间。
3. 实现校准相似度:从特征到相似度矩阵
3.1 特征提取:让模型输出 embedding 而不是分类 logits
在动物再识别任务中,最常用的做法是使用一个在通用图像分类任务上预训练的模型,去掉最后的全连接分类层,把倒数第二层或任意指定层的输出作为 embedding。下面以 ResNet18 为例,说明最小实现方式。
import torch import torch.nn.functional as F from torchvision import models, transforms def build_backbone(): model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT) model.fc = torch.nn.Identity() model.eval() return model def preprocess_image(image_path): 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]) ]) from PIL import Image img = Image.open(image_path).convert("RGB") return transform(img) def extract_embedding(model, img_tensor): with torch.no_grad(): emb = model(img_tensor.unsqueeze(0)).squeeze(0) emb = F.normalize(emb, p=2, dim=0) return emb.detach().cpu().numpy()这段代码解决了三个问题。第一,model.fc = torch.nn.Identity()移除了分类层,让输出变成特征向量,而不是某个类别的高维得分。第二,F.normalize将 embedding 归一化到单位长度,这样后续用余弦相似度计算时,数值范围可以控制在 -1 到 1 之间。第三,torch.no_grad避免梯度计算,特征提取阶段只需要前向传播。
预训练 ResNet18 只是一个快速原型。真正用于动物个体识别时,模型性能往往不够,需要通过度量学习训练才可能达到可用水平。常见的训练方式是三元组损失或分类代理损失,让同一身份在 embedding 空间中的距离更近。这里不展开训练细节,只强调一种常见误区:不要直接用分类模型的 logits 做相似度比较,因为 logits 的分布受训练类别数量和先验分布影响,并不能稳定表达样本之间的相近程度。
3.2 为什么原始余弦相似度需要校准
得到 embedding 后,最简单的相似度计算方式是余弦相似度。公式如下:
sim(i, j) = embedding_i · embedding_j由于 embedding 已经归一化,这个点积就是两个向量之间的夹角余弦。余弦相似度的优点是计算简单、鲁棒性好,但它并不天然具备“概率”语义。同样是 0.6 的相似度,在某个物种个体图像上可能已经说明是同一只动物,在另一个物种上却可能是完全不同的个体。
这是开放集任务中很关键的问题:相似度分布往往受到数据难度、摄像头老化程度、拍摄距离等因素影响。如果不做任何校准,直接设一个全局阈值,比如“相似度大于 0.7 就是同一个体”,那么在不同子数据集上很容易出现一高一低两个极端。校准相似度的目标,就是让相似度分数在不同图像对之间具有可比性,同时找到更合理的决策边界。
一种常用思路是在验证集上寻找最优温度参数。温度缩放最早用于分类任务的概率校准,但同样可以应用到相似度矩阵上。它的作用是控制相似度矩阵的“锐度”:温度越低,相似度矩阵中的相对差异越大;温度越高,分数越平滑。这里的实现非常简单:
def compute_calibrated_similarity(embedding_matrix, temperature=0.07): # embedding_matrix: shape (N, D),已归一化 sim = embedding_matrix @ embedding_matrix.T calibrated_sim = sim / temperature return calibrated_sim除以温度后,相似度没有改变对比关系,但改变了分数范围。比如原始相似度 0.7,除以 0.07 后变成 10.0,这样多个分数之间的梯度会被放大,聚类算法更容易区分强关系和弱关系。
这里要注意,温度参数在训练阶段和推理阶段可以扮演不同角色。度量学习训练时,温度用于缩放特征相似度并计算 softmax 交叉熵损失;推理阶段,温度可以当作相似度的后处理校准参数。两者目的不同,不要简单混用。
3.3 阈值选择:在验证集上找最优决策边界
温度只是缩放相似度范围,最终的开放集判断还需要一个决策阈值。这个阈值不应该靠感觉定,而应该在验证集上搜索。
首先构造验证图像对。把验证集中所有图像两两配对,如果两张图属于同一个个体,标签记为 1;否则记为 0。然后计算每个配对的相似度,得到一张包含sim和label两列的表,再搜索使 F1 分数最高的阈值。
import numpy as np from sklearn.metrics import f1_score def choose_best_threshold(pair_sims, pair_labels, candidates): best_thr = 0.5 best_score = -1 for thr in candidates: pred = (pair_sims >= thr).astype(int) score = f1_score(pair_labels, pred) if score > best_score: best_score = score best_thr = thr return best_thr, best_score调用时,可以在一段等距候选值上搜索,比如 0.05 到 0.95,步长 0.05:
best_thr, best_f1 = choose_best_threshold( pair_sims, pair_labels, candidates=np.arange(0.05, 0.95, 0.05) ) print(f"best threshold: {best_thr:.3f}, best F1: {best_f1:.3f}")这个搜索过程的意义在于,它把所有“是否判断为同一身份”的决策都统一到同一个可复现的规则上,而不是每个场景各定一个经验值。
3.4 参数说明:温度、阈值、topk 和最小簇大小
在相似度校准和图聚类这条链路中,有四个参数最常需要调整,建议用表格记录:
| 参数 | 含义 | 常见初始值 | 调小的影响 | 调大的影响 |
|---|---|---|---|---|
| temperature | 相似度缩放温度 | 0.07 | 相似度范围变大,正负样本差异更尖锐 | 相似度范围变小,边界更容易模糊 |
| threshold | 判定同一身份的相似度下限 | 验证集搜索 | 更多不相似样本被判为同类 | 更多同身份样本被判为未知 |
| topk | 构建图时每个节点连接的邻居数 | 5 到 10 | 图更稀疏,簇更容易分裂 | 图更稠密,不同身份更容易被连成一个大簇 |
| min_cluster_size | 聚类后被丢弃的最小簇大小 | 2 到 5 | 保留更多噪声小簇 | 可能把真实个体的小样本簇误删 |
这些参数并非独立。threshold 和 topk 共同影响图的连通性,min_cluster_size 则影响未知个体的处理方式。调参时不要只调一个指标,而是先固定聚类参数,搜索阈值;再固定阈值,搜索 topk 和最小簇大小。
4. 用图聚类完成开放集个体确认
4.1 为什么选择图聚类而不是简单阈值
最简单的身份判定方式是两两比较:查询图像与图库中每张图像计算相似度,高于阈值就归为同一身份。这个方案在数据量小、图像质量稳定时可能够用,但真实场景中会出现两个明显问题。
第一,阈值判定的结果不稳定。如果同一个个体的正面照和侧面照相似度为 0.62,而另一只相似个体的正面照相似度为 0.65,那么基于固定阈值的判定必然会在两个方向都出错。
第二,两两判定忽略了全局一致性。正确的结果应该满足“传递性”:A 和 B 是同一个体,B 和 C 是同一个体,那么 A 和 C 也应该被归为同一个体。简单阈值无法保证这一点,A 和 B 判为同类、B 和 C 判为同类、但 A 和 C 却低于阈值,就会产生矛盾。
图聚类把“两两关系”升级成“全局结构”。图像是节点,强连接的边构成簇,同一个簇内的图像共享同一个身份标签。这种方案更适合开放集任务,因为它天然允许某些节点无法被归入任何簇。
4.2 构建相似度图:邻接矩阵和 topk 建边
构建图时,核心问题是决定哪些节点之间应该有边。直接让所有相似度大于阈值的节点对都相连,会让图非常稠密,尤其是相似个体较多时会产生一个巨大簇。更稳妥的方式是采用 topk 限制,每个节点只与相似度最高的 k 个邻居相连,同时要求连接必须高于阈值。
import numpy as np import networkx as nx def build_graph(calibrated_sim, topk=8, threshold=0.3): n = calibrated_sim.shape[0] graph = nx.Graph() graph.add_nodes_from(range(n)) for i in range(n): order = np.argsort(-calibrated_sim[i])[:topk] for j in order: if j == i: continue if calibrated_sim[i, j] >= threshold: graph.add_edge(i, j, weight=float(calibrated_sim[i, j])) return graph这里传入的calibrated_sim已经除以温度,因此 threshold 也需要在同样的温度体系下重新搜索。networkx.Graph是无向图,因为个体身份关系是对称的,A 和 B 同身份,B 和 A 也一定同身份。topk的作用是降低噪声边的干扰,让每个节点只与自己最相似的少数节点建立联系。
建边后,可以先看图的统计信息:
print("nodes:", graph.number_of_nodes()) print("edges:", graph.number_of_edges())如果边数异常多,通常是 topk 过大或 threshold 过低;如果边数几乎为零,则说明特征区分度不够或者阈值过高。
4.3 谱聚类与标签传播:两种可落地方案
建图之后,需要选择聚类算法。两个常用选项是谱聚类和标签传播。
谱聚类适合中等规模的图。它通过图的拉普拉斯矩阵做特征分解,再在低维空间聚类,能够揭示图的非线性结构。在 scikit-learn 中可以直接使用:
from sklearn.cluster import SpectralClustering def cluster_with_spectral(calibrated_sim, n_clusters, threshold=0.3): adjacency = (calibrated_sim >= threshold).astype(float) np.fill_diagonal(adjacency, 0) model = SpectralClustering( n_clusters=n_clusters, affinity="precomputed", random_state=42 ) return model.fit_predict(adjacency)这里的calibrated_sim已经经过温度缩放,但阈值却要与温度匹配。affinity="precomputed"表示传入的是相似度矩阵,算法会把它当成图连接强度来使用。需要注意,这种做法只把超过阈值的元素作为边,低于阈值的元素视为不连接,聚类结果对阈值非常敏感。
标签传播则是一种针对图结构的快速方法,适合图很大但边稀疏的场景。scikit-learn 也提供了LabelPropagation,但使用方式和谱聚类不同。标签传播的好处是无需提前指定簇数量,但它需要一个可靠的种子标签集合,对于完全无人监督的动物个体聚类,初始化质量会直接影响结果。
在最小闭环中,谱聚类更容易理解和调试,因为它以相似度矩阵作为输入,参数逻辑与学生掌握的“降维 + 聚类”思路一致。
4.4 处理未知身份:小簇如何被丢弃
图聚类算法会尝试把所有节点都分到某个簇,但这与开放集需求冲突。开放集场景中,必然有部分图像属于从未见过的个体,它们的数量可能不足以形成稳定簇。这些图像往往分布在稀疏的边连接里,最终会被分到某些小簇或者噪声簇。
处理方式是在聚类后增加一个后处理步骤:统计每个簇的节点数量,如果小于min_cluster_size,就把整个簇标记为未知。
def prune_small_clusters(labels, min_cluster_size=3): labels = np.array(labels).copy() unique, counts = np.unique(labels, return_counts=True) for cls, cnt in zip(unique, counts): if cnt < min_cluster_size: labels[labels == cls] = -1 return labels-1表示未知身份。这里的设定逻辑是:一个真实个体在一定规模的采集中通常会被拍到多张图,如果只有一个节点或两个节点形成独立簇,很可能是误检图、错误裁剪或者极端姿态导致的异常样本。min_cluster_size并不是越大越好。如果设置成 5,但部分野外个体只被抓拍到 2 到 3 张图,就会把真实身份误删成未知。需要根据数据采集密度决定。
5. 运行验证与结果解读
5.1 学习环境小样例:20 个个体、200 张图
为了快速验证链路,不需要一开始上完整数据集。可以构造一个 20 个个体、每个个体 10 张图的小样例,跑通后再扩展。
完整流程如下:
- 遍历图片目录,用
extract_embedding提取所有图像的 embedding,存成(N, D)矩阵。 - 对 embedding 矩阵计算校准相似度。
- 在验证集上搜索阈值。
- 用相似度矩阵构建图。
- 执行谱聚类或连通分量聚类。
- 剪除小簇,得到最终标签。
- 与真实身份标签计算 ARI、NMI、B-cubed F1。
这个流程在 200 张图的规模下,运行时间通常在几分钟以内。目的是验证每个环节的逻辑是否正确,而不是追求精度。
5.2 聚类质量怎么看
聚类结果不能只凭肉眼看,需要用指标量化。
from sklearn.metrics import adjusted_rand_score, normalized_mutual_info_score true_labels = load_true_labels() # 读取真实身份 ID 对应的整数标签 cluster_labels = prune_small_clusters(spectral_labels, min_cluster_size=3) print("ARI:", adjusted_rand_score(true_labels, cluster_labels)) print("NMI:", normalized_mutual_info_score(true_labels, cluster_labels))ARI 会惩罚把不同身份放在同一个簇,也会惩罚把同一身份拆成多个簇。NMI 更关注簇与身份之间的信息重叠,但受簇数量影响较大。两个指标一起看,能更全面描述聚类效果。
B-cubed F1 是更贴近动物个体识别的指标,它把每个节点的配对关系作为最小单元。这个概念比较抽象,最小闭环阶段可以先不实现,等确定模型和聚类参数后再补。
5.3 可视化检查:相似度热力图与簇分布
指标只是数字,排查问题时需要可视化。
import matplotlib.pyplot as plt plt.figure(figsize=(8, 6)) plt.imshow(sim_matrix, cmap="viridis") plt.colorbar() plt.title("Similarity Matrix") plt.tight_layout() plt.savefig("similarity_matrix.png")按真实个体 ID 排序后查看热力图,如果同一个体出现了明显的高亮方块,说明特征与相似度计算是有效的。如果热力图没有明显对角块,或者所有样本都非常亮,则说明特征没有区分度,或者温度参数导致分数整体偏高。
聚类结果还可以按照簇重新排序相似度矩阵,观察每个簇内部是否形成连续块。如果某个簇在热力图中被夹在另一个簇之间,说明两个簇在特征空间中比较接近,需要考虑合并还是拆分。
5.4 学习环境与生产环境的差异
学习环境下只要能把小数据集跑通,就算成功。生产环境还差很多事情:
| 维度 | 学习环境 | 生产环境 |
|---|---|---|
| 数据规模 | 百张到千张 | 十万张到百万张 |
| 计算资源 | 单张 GPU | 多卡分布式或离线批量处理 |
| 特征提取 | 一次性全量计算 | 流式新增,增量更新特征库 |
| 相似度矩阵 | 全量 N x N 可放内存 | 需要分块计算或向量检索 |
| 阈值和参数 | 在验证集上固定 | 按机位、物种、时间段分别校准 |
| 聚类策略 | 全量重聚类 | 增量聚类、周期性全量复核 |
| 日志与监控 | 打印 | 结构化日志、指标曲线、告警 |
| 数据安全 | 本地文件 | 权限控制、加密存储、审计日志 |
这里最关键的变化是“数据规模”。当图像数量到达百万级时,全量相似度矩阵会占满显存,图聚类也会变得很慢。生产环境通常需要先通过向量检索召回 topk 候选,再在小图上做聚类,而不是对全量矩阵做谱分解。
6. 常见问题与排查链路
6.1 特征没有区分度:模型本身的问题
现象:相似度热力图整体都很亮,聚类结果把大量不同个体合并到一个大簇。可能原因包括:使用了未训练的随机初始化模型、预训练模型与目标图像分布差异过大、没有做 L2 归一化、图像裁剪不准确导致背景占比过高。
检查方式:随机抽取同一个体的 5 对图像和不同个体的 5 对图像,比较它们的相似度分布。如果两类分布几乎重叠,说明特征提取模型的区分能力不足。
处理建议:先换成更强的预训练模型;其次在目标物种数据上做度量学习微调;最后检查图像预处理流程,确认输入图像是否包含过多背景。
6.2 聚类数量严重偏差
现象:聚类算法返回的簇数量远大于真实个体数,或者明显小于真实个体数。
可能原因:谱聚类需要指定n_clusters,但这个值在开放集中很难预先知道。如果直接设为已知个体数,会忽略新个体,导致不同身份被强行合并。如果设得过大,又会让同一个体被拆到多个簇。
检查方式:先查看构建出的图有多少个连通分量。连通分量数量可以作为簇数量的初始估计。如果图过于稠密,连通分量数会偏少;如果图过于稀疏,连通分量数会偏多。
处理建议:先用nx.number_connected_components(graph)看看自然分组数量,再结合 B-cubed F1 调整建边参数。不要一开始就试图让聚类算法自动估计簇数量,先检查图结构是否合理。
6.3 同个身份被拆簇,不同身份被合并
现象:聚类结果中存在明显的“同一身份被拆成两个簇”或“两个身份混在同一个簇”。
常见原因有三类。第一,topk 太小,导致同一个个体的图像没有形成足够多的边,被切分成多个子图。第二,threshold 太低,导致不同个体之间形成跨身份的长边,最后聚类算法把多个身份连成一个簇。第三,相似度分布本身是非均匀的,某只个体的图像质量好,特征集中;另一只个体的图像角度差异大,特征分散。
处理建议:
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 同一身份拆成多个簇 | topk 太小或特征区分度过低 | 计算该身份内部相似度的均值 | 增大 topk;提高特征训练质量 |
| 不同身份合为一个簇 | threshold 太低或 topk 太大 | 看簇内两两相似度分布 | 调高阈值;降低 topk |
| 小样本个体全部变成未知 | min_cluster_size 太大 | 统计真实个体图像数分布 | 按图像数分布调整最小簇大小 |
| 聚类结果每次不同 | 谱聚类随机初始化 | 固定 random_state | 设置随机种子并记录版本 |
6.4 排查清单:按顺序检查
如果整条链路没有达到预期,按以下顺序排查,不要跳过输入检查直接改参数。
- 检查输入图片:文件是否损坏、是否包含重复图像、是否有多人入镜。
- 检查标签:train、val、query 之间是否存在身份交叉。
- 检查 embedding:是否有大量 NaN 或零向量。
- 检查相似度矩阵:对角线是否为 1,范围是否在预期区间。
- 检查热力图可视化:同类是否成块,异类是否分离。
- 检查建边参数:连通分量数量是否合理。
- 检查聚类参数:簇数量、随机种子、最小簇大小是否合理。
- 检查评估指标:是在哪些数据上算出来的,是否与调参数据重叠。
这条清单的价值在于固定顺序。很多问题并不是聚类算法的问题,而是前面某一步留下了“脏数据”,后面对聚类参数怎么调都没有意义。
7. 生产落地的最佳实践与扩展方向
7.1 数据与标注层面的建议
开放集动物再识别落地时,数据质量比模型结构更重要。建议在项目启动阶段就建立以下规范。
按个体 ID 预先划分数据,避免同一个个体的图像同时进入训练集和验证集。对每个拍摄机位记录尽可能完整的时间、地点和视角信息,方便后期分析相似度漂移。对不确定身份的图像单独存放,不要强行打上伪标签。如果数据来源包含多个物种,建议为每个物种单独训练或校准,因为不同物种的个体外观差异度不同,统一阈值很难工作。
7.2 模型训练与相似度校准的建议
不要停留在 ResNet18 预处理阶段。真实场景中,建议在目标物种数据上继续训练 ReID 模型,常用损失是三元组损失和带有温度参数的 softmax 代理损失。训练完成后,用验证集重新选择温度和阈值,而不是沿用训练阶段的默认值。
生产环境还要关注模型更新后的兼容问题。模型版本升级后,旧特征与新特征不在同一个 embedding 空间,聚类时混用会导致严重偏差。比较好的做法是为特征增加版本号,并在向量检索系统中只查询同一版本的特征。
7.3 聚类工程化与运维保障
当数据量变大时,图聚类不能一直全量重跑。可以按时间段、摄像机或区域做分片聚类,再用周期性全量聚类校准。即使采用分片方案,也要保留人工复核通道。算法输出的簇要被标记为“高置信”和“待确认”,由研究人员或保护区工作人员对低置信簇做二次确认。
日志和监控需要覆盖到每个环节:特征提取耗时、相似度分布、簇数量、小簇比例、未知身份比例。当这些指标出现明显波动时,应该触发告警。例如某天新增图像的未知身份比例从 5% 突然升高到 30%,很可能是摄像头位置变动或新个体出现,也可能是模型特征漂移。
7.4 可以继续扩展的方向
单纯依靠相似度和聚类只能解决静态场景。后续如果想进一步提升系统能力,可以沿着以下几个方向扩展。
增量聚类方向:新图像持续加入时,如何在不全量重聚类的前提下判断“它属于已有簇”还是“应该新建一个簇”。跨摄像机方向:不同摄像机之间的光照、视角和距离差异很大,需要设计域适应或相机无关的特征表示。主动学习方向:让算法主动挑选最难判断的图像对,提高人工标注的效率。多物种联合建模方向:训练一个能够同时处理多种物种的通用模型,减少在每一个新物种上都重新训练的成本。
在你已经跑通校准相似度与图聚类链路之后,最值得做的一件事是回到数据上,把相似度分布和聚类错误样本可视化出来,逐一分析错误来源。这个分析过程通常比继续堆叠模型更能暴露问题:到底是拍摄角度不够、图像模糊、检测框不准,还是聚类参数不合理。把这些问题记录下来,再决定下一步是调参、换模型,还是优化数据管线。