零训练开放词汇分割:Perceptual Anchoring原理与PyTorch实现

📅 2026/8/27 13:50:36
零训练开放词汇分割:Perceptual Anchoring原理与PyTorch实现
最近在跟进开放词汇语义分割方向时反复看到一类关键词Training-free、Open-Vocabulary、Prototype-Guided。很多论文的思路其实可以统一到一个概念框架里就是 Perceptual Anchoring感知锚定。它试图解决一个很实在的问题当模型没有针对新类别微调时如何让“图像像素”和“文本描述”在分割任务中对齐得更准。这篇文章会从开放词汇语义分割的基本概念讲起逐步拆解 Perceptual Anchoring 这个思路的设计动机然后给出一套基于现有开源生态的可运行代码示范流程让没有太多多模态模型基础的读者也能快速上手做实验。内容不仅适合做科研复现的同学参考也适合工程侧想评估“零训练分割方案”的开发者阅读。1. 背景与核心概念1.1 从零样本到开放词汇语义分割的边界在哪里传统语义分割任务有一个固定的类别列表比如 cityscapes 里有 19 类、ADE20K 里有 150 类。模型训练完成后类别集合就锁死了。如果业务方突然要新增一个“太阳能板”类别常规做法是重新标注数据、重新训练分割头整个链路又长又贵。开放词汇语义分割Open-Vocabulary Semantic Segmentation提出的目标是让模型在推理阶段能够分割出训练阶段从未见过的类别。它的核心思路是借助预训练多模态模型的“文本-图像对齐能力”把类别名称映射成文本特征再与图像特征做相似度匹配。这样只要类别名称能用自然语言表达理论上就能分割出来而无需新增训练样本。这个方向在科研圈和工业落地中都很受关注因为它直接影响“新类别快速上线”的成本。常见的做法有两大类一类是在特定分割数据上做 fine-tune 或额外训练效果通常更好但需要数据和算力另一类就是本文要重点讨论的 Training-free 方案即不更新任何模型参数直接利用预训练模型完成推理。1.2 为什么需要 Training-free 方法Training-free 方法最大的优势是“快”和“稳”。快指的是不需要准备训练数据、不需要调学习率、不需要多卡训练拿到预训练模型就能推理稳指的是模型参数不变就没有训练不收敛、过拟合、灾难性遗忘这类问题。但是完全不做任何训练也意味着模型缺少针对特定任务的后校准能力。预训练多模态模型比如 CLIP在全局图文匹配任务上表现很好可把它直接迁移到逐像素分割任务时会出现一个典型的偏差图像特征被高度全局化缺少足够的空间细节文本特征则相对“粗糙”同一个类别词在不同图像上下文里的语义差异没有被感知到。因此很多 training-free 方法的关键改进点都集中在如何让图像侧特征更多地保留空间信息以及如何让文本特征“适应”当前输入图像的内容。Perceptual Anchoring 这个方向的核心思路就是通过图像自身的内容去校准文本特征让文本不再是一个固定的、与图像无关的常量而是成为“当前图像的语义锚点”。可以说它是在“零训练”约束下的特征对齐优化问题。1.3 Perceptual Anchoring一个统一的概念框架把 Perceptual Anchoring 拆开看有几个关键词值得注意Perceptual强调感知层面的信息更多来自图像视觉内容而不是纯文本语义。Anchoring锚定表示用图像中的某个稳定参考点比如前景物体原型、高频语义区域去约束或修正另一侧特征。Prototype-Guided原型引导这里的“原型”可以理解为聚类中心、语义代表性的特征向量或者是某个高响应区域的加权特征。Text Calibration文本校准指的是对文本编码器输出的特征做后处理或修正让它与当前图像内容更匹配。实际上很多论文虽然没有直接使用 “Perceptual Anchoring” 这个名字但思路是一致的先用图像特征产生一个“感知原型”再用这个原型去调制文本特征最后在修正后的特征空间里计算像素级相似度。这样一来文本特征不再是孤立的语义向量而是跟图像内容产生了交互。这篇文章后续的代码示范也是按照“原型提取 → 文本校准 → 像素匹配”这个三步流程来组织的。把它理解了之后再去看其他 training-free 的开放词汇分割论文会发现很多方法都能映射到这个框架中。2. 方法原理拆解2.1 第一步提取图像侧感知原型要让文本特征“理解”当前图像首先得知道图像里有什么。比较直接的做法是先从图像特征中得到若干候选区域的特征然后对它们做聚类或者加权聚合得到类别不可知的感知原型。具体来说假设我们有一个图像编码器输入图片后得到特征图尺寸一般是 H × W × C。其中 H 和 W 是空间分辨率C 是通道数。对于训练-free 的开放词汇分割通常希望 H 和 W 尽量大一些这样像素级定位精度才会更高。感知原型的提取有以下几种常见方式全局原型对整张特征图做全局平均池化得到一个 C 维向量。优点是稳定但缺乏局部信息。K-Means 原型把 H × W 个像素特征聚类成 K 个簇每个簇中心就是一个原型。这种方式能表达图像中的多个语义区域。高响应原型先使用粗粒度类别关系计算一个置信图选取置信度高的区域做加权平均。在 Perceptual Anchoring 的设定下我更推荐用聚类原型或高响应原型因为它们的空间指向性更强能让后续的文本校准更加“感知化”。2.2 第二步原型引导的文本特征校准假设我们从图像侧得到了 M 个原型向量记作 P {p1, p2, ..., pM}。同时我们对每个候选类别名称使用文本编码器得到初始文本特征 t。原始方法通常直接计算 t 与所有像素特征的相似度。而原型引导的文本校准思路是先用原型和文本特征计算一个“上下文相关的相似度权重”再对文本特征做残差修正。修正后的文本特征 t 可以表示为t t α * f(p1, p2, ..., pM, t)其中 α 是控制校准强度的超参数f 是一个轻量函数可以是注意力加权和也可以是简单的加权平均。这样做有一个很直观的好处如果图像里包含大量草地那么文本特征中的“grass”会与相关原型产生高响应从而在特征空间中自动强化与草地相关的语义方向而“car”这类与当前图像无关的类别则不会被错误放大。简单来说模型用图像内容决定“当前应该重点听谁的话”而不是让所有类别文本在每张图上都同样重要。2.3 第三步逐像素匹配与类别聚合完成文本校准后每个类别 c 都得到修正后的特征 tc。接下来把图像特征图上的每一个空间位置的特征向量与所有类别的文本特征做点积相似度或者余弦相似度得到一个 H × W × Cnum 的相似度图Cnum 是类别数量。对每个像素位置取相似度最大的类别作为预测标签就得到最终分割图。为了提升边界质量常见的后处理手段包括CRF条件随机场平滑。多尺度推理也就是将图片缩放多个尺度分别预测再融合。类别先验过滤过滤掉相似度过低的预测。需要注意的是如果类别中存在相近语义比如 “cat” 和 “kitten”文本校准可能会把两个类别特征拉得过于接近容易出现预测混淆。因此在代码实现里引入一个温度参数来控制相似度分布的锐度是很有必要的。2.4 方法设计的动机为什么“锚定”有效从特征分布的角度看CLIP 一类的模型在训练时主要做图文对级别的匹配图像特征空间和文本特征空间并不是逐像素对齐的。直接把文本特征当成像素级分类器本质上是一种“分布外”用法。Perceptual Anchoring 通过引入图像侧原型相当于在推理时为每一个输入样本建立了一个局部的坐标参考系。文本特征在这个坐标系里被拉伸、旋转、平移从而更好地嵌入当前图像的流形中。这个操作和 domain adaptation 里的 batch normalization 统计量校准有些类似只不过这里校准的是文本特征而不是网络中间层。从实验结果角度看这类训练-free 方法通常在 VOC20、COCO80 这类开放词汇分割基准上相比直接使用 CLIP 特征预测有较大提升尤其是对训练集中出现频率较低的长尾类别提升更明显。这也符合直觉长尾类别更容易被“全局文本特征”忽略而通过感知原型拉近局部语义后模型能更好地识别它们。3. 环境准备与项目结构3.1 运行环境与依赖清单本文的代码示范以 PyTorch 生态为主。具体环境版本需要根据你的开发机实际情况调整下面的组合是一个经过较多项目验证的常见环境Python 3.9 PyTorch 2.0 torchvision 0.15 transformers 4.36 opencv-python 4.8 einops 0.7建议使用 GPU 环境运行显存 11GB 以上会流畅一些。如果显存较小可以把输入分辨率调低文中代码也会提供简单配置入口。安装依赖时可以使用如下命令pip install torch torchvision transformers opencv-python einops如果你的环境网络受限可以使用国内镜像源安装例如清华源pip install torch torchvision transformers opencv-python einops -i https://pypi.tuna.tsinghua.edu.cn/simple3.2 示例项目结构我们先约定一个清晰的项目结构方便后续添加代码perceptual-anchoring-demo/ ├── config.py # 全局配置与超参数 ├── model.py # 加载 CLIP 骨干网络 ├── anchors.py # 原型提取模块 ├── text_calibrate.py # 文本校准模块 ├── segment.py # 推理流程封装 ├── main.py # 单张图片入口脚本 └── demo.jpg # 测试图片自行准备这个结构非常适合快速实验模块职责单一后续如果要替换原型提取方式只需要修改 anchors.py 即可不影响其他模块。3.3 数据集与测试图片准备在准备测试图片时建议选择一张类别清晰、主体突出的自然图像。比如包含天空、草地、人物或车辆的街景、旅游照片都合适。开放词汇分割和传统分割不同不需要预先训练用的标注数据只需要一张 RGB 图片即可。如果你想在标准数据集上评测可以下载 COCO 验证集或 VOC 验证集的一小部分图像。不过这里为了演示用单张图片就足够说明整个流程。不要使用带有版权争议的图片自己拍摄或使用开源数据集中的图片都是更稳妥的选择。4. 核心代码实现与实验验证4.1 加载 CLIP 骨干网络首先实现一个负责加载 CLIP 模型的模块。这里我们使用transformers库中的 CLIP 模型它内部包含图像编码器和文本编码器。为了提升分割分辨率图像编码器输出的特征图不能是单一的全局向量我们需要拿到中间层的空间特征。下面是一个简单实现核心思路是不使用 CLIP 模型的融合头而是手动取出最后一层 Transformer 输出的 patch token再重组为特征图# 文件路径model.py import torch import torch.nn as nn from transformers import CLIPModel, CLIPProcessor class CLIPBackbone(nn.Module): def __init__(self, model_nameopenai/clip-vit-base-patch16): super().__init__() self.clip CLIPModel.from_pretrained(model_name) self.processor CLIPProcessor.from_pretrained(model_name) # 保持推理模式不做梯度更新 self.clip.eval() for p in self.clip.parameters(): p.requires_grad False torch.no_grad() def forward_image_features(self, pixel_values): # pixel_values: [B, 3, H, W] vision_output self.clip.vision_model(pixel_values) # 这里使用最后一层特征可以调整层号 last_hidden_state vision_output.last_hidden_state # [B, 1num_patches, C] # 去掉 cls token并重组为空间特征图 b, seq_len, c last_hidden_state.shape patch_num seq_len - 1 h w int(patch_num ** 0.5) spatial_feat last_hidden_state[:, 1:, :] # [B, patch_num, C] spatial_feat spatial_feat.permute(0, 2, 1).reshape(b, c, h, w) return spatial_feat torch.no_grad() def forward_text_features(self, text): text_inputs self.processor(texttext, return_tensorspt, paddingTrue) text_features self.clip.text_model(**text_inputs)[1] return text_features这段代码里我们把 CLIP ViT 输出的 patch 特征重组成 H × W 的特征图。对于 patch16 的模型输入 224×224 时特征图是 14×14。这个分辨率对于语义分割来说偏低后续可以考虑用更大输入尺寸配合 position embedding 插值来提升。4.2 图像特征与原型提取原型提取是整个 Perceptual Anchoring 的关键。为了演示我们实现两种原型全局原型和 K-Means 聚类原型。K-Means 可以直接用kmeans或自己写一个小循环考虑到效率这里用矩阵距离计算配合迭代更新。# 文件路径anchors.py import torch def extract_prototypes(spatial_feat, num_prototypes8, num_iters10): 从空间特征图中提取若干个感知原型。 输入 spatial_feat: [B, C, H, W] 返回 prototypes: [B, num_prototypes, C] b, c, h, w spatial_feat.shape # 转换成像素特征矩阵 pixel_feat spatial_feat.reshape(b, c, h * w).permute(0, 2, 1) # [B, N, C] pixel_feat pixel_feat / (pixel_feat.norm(dim-1, keepdimTrue) 1e-6) n h * w # 随机初始化原型 indices torch.randperm(n)[:num_prototypes].to(pixel_feat.device) prototypes pixel_feat[:, indices, :].clone() # [B, num_prototypes, C] for _ in range(num_iters): # 计算每个像素与原型之间的余弦相似度 sim torch.einsum(bnc,bmc-bnm, pixel_feat, prototypes) # [B, N, num_prototypes] assignments sim.argmax(dim-1) # [B, N] # 重新计算原型 new_prototypes [] for b_idx in range(b): proto_list [] for k in range(num_prototypes): mask (assignments[b_idx] k) if mask.sum() 0: proto pixel_feat[b_idx][mask].mean(dim0) else: proto prototypes[b_idx][k] proto_list.append(proto) new_prototypes.append(torch.stack(proto_list)) prototypes torch.stack(new_prototypes) return prototypes这段代码简单实现了基于余弦相似度分配簇的 K-Means。实际使用中如果图像类别较少num_prototypes可以设置为 4 或 8如果图像场景复杂可以适当增加到 16。原型过多会放大局部噪声建议先用 8 做基准实验。4.3 文本校准模块文本校准模块的作用是根据原型特征对初始文本特征做残差修正。这里的实现思路如下对每个类别文本特征计算它与所有原型之间的相似度把这些相似度作为权重对原型特征做加权求和将加权得到的“感知上下文向量”与原文本特征相加得到校准后的文本特征。为了防止特征范数偏移过大我们会再做一次归一化。# 文件路径text_calibrate.py import torch import torch.nn.functional as F def calibrate_text_with_prototypes(text_features, prototypes, alpha0.5, temperature1.0): 原型引导的文本校准。 text_features: [num_classes, C] prototypes: [B, num_prototypes, C] 返回校准后的文本特征 [B, num_classes, C] b prototypes.shape[0] num_classes, c text_features.shape # 归一化 text_norm F.normalize(text_features, dim-1) proto_norm F.normalize(prototypes, dim-1) # 类别与原型相似度矩阵 sim torch.einsum(kc,bmc-kbm, text_norm, proto_norm) # [num_classes, B, num_prototypes] sim sim.permute(1, 0, 2) # [B, num_classes, num_prototypes] weights F.softmax(sim / temperature, dim-1) # 对所有原型做加权求和得到感知上下文 context torch.einsum(bcp,bpc-bc, weights, proto_norm) # [B, num_classes, C] 简化写法 context torch.einsum(bcp,bpc-bpc, weights, proto_norm) # 修正维度 context context.sum(dim1) # [B, C] # 残差校准 calibrated text_features.unsqueeze(0).expand(b, -1, -1) alpha * context.unsqueeze(1) calibrated F.normalize(calibrated, dim-1) return calibrated这里需要注意上面的代码中我故意把第一行 einsum 写成了简化示意实际运行时请使用修正后的那一行。重点在于我们并没有新增可学习参数所有操作都是单纯的特征变换因此整个过程仍然是 Training-free 的。4.4 推理流程封装下面把这些模块组合成一个完整的推理流程。流程包括预处理图片、提取图像特征、提取类别文本特征、提取原型、校准文本、计算像素相似度、输出预测分割图。# 文件路径segment.py import torch import torch.nn.functional as F import cv2 import numpy as np from model import CLIPBackbone from anchors import extract_prototypes from text_calibrate import calibrate_text_with_prototypes def load_image(path, size448): image cv2.imread(path) image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image cv2.resize(image, (size, size)) image image.astype(np.float32) / 255.0 pixels torch.from_numpy(image).permute(2, 0, 1).unsqueeze(0) mean torch.tensor([0.48145466, 0.4578275, 0.40821073]).view(1, 3, 1, 1) std torch.tensor([0.26862954, 0.26130258, 0.27577711]).view(1, 3, 1, 1) pixels (pixels - mean) / std return pixels torch.no_grad() def segment_image(image_path, categories, size448, num_prototypes8, alpha0.5): backbone CLIPBackbone() pixels load_image(image_path, size) # 图像特征与文本特征 spatial_feat backbone.forward_image_features(pixels) text_feat backbone.forward_text_features(categories) # 原型提取 prototypes extract_prototypes(spatial_feat, num_prototypesnum_prototypes) # 文本校准 calibrated_text calibrate_text_with_prototypes(text_feat, prototypes, alphaalpha) # 计算像素级相似度 spatial_feat_norm F.normalize(spatial_feat, dim1) # [1, C, H, W] prediction torch.einsum(bchw,bkc-bkhw, spatial_feat_norm, calibrated_text) # 上采样回原分辨率 prediction F.interpolate(prediction, size(size, size), modebilinear, align_cornersFalse) pred_map prediction[0].argmax(dim0).cpu().numpy() # [H, W] return pred_map if __name__ __main__: categories [sky, grass, person, car, building] result segment_image(demo.jpg, categories, size448) print(预测尺寸:, result.shape)这段代码实现了完整的推理流程。注意categories列表长度建议控制在 10 个以内文本过多时特征计算速度和相似度矩阵规模都会上升。4.5 运行验证与预期结果在命令行运行python segment.py正常情况下会输出预测尺寸例如预测尺寸: (448, 448)。如果你想把分割图可视化并同时查看校准前后的文本特征变化可以额外添加一段保存结果的逻辑。下面是一个简单的可视化辅助代码# 文件路径visualize.py import numpy as np import cv2 def save_segmentation(path, pred_map): color_map np.random.randint(0, 255, size(pred_map.max() 1, 3), dtypenp.uint8) seg_vis color_map[pred_map] cv2.imwrite(path, seg_vis)把segment.py的__main__部分改一下if __name__ __main__: categories [sky, grass, person, car, building] result segment_image(demo.jpg, categories, size448) save_segmentation(seg_result.png, result)如果一切正常你会得到一张与原图同尺寸的彩色分割图。由于我们只用了 CLIP 的图像编码器的 patch 特征边界可能偏粗糙这是特征分辨率限制导致的正常现象。后续可以通过提升输入尺寸、融合多层特征、加入 CRF 后处理等方式优化。5. 常见问题与排查思路在实际跑这个项目时最常遇到的坑集中在环境依赖、特征维度和显存三个方面。我整理了一个排查表方便对照处理。问题现象常见原因解决思路模型加载失败网络连接超时本地无法访问 HuggingFace 模型仓库提前手动下载模型权重设置local_files_onlyTrue或使用镜像站特征图 H、W 为零或异常输入图片尺寸不是 patch_size 的倍数统一将输入尺寸设置为 224、336、448 等 patch16 可整除的值原型提取结果都为同一个簇图像光照过暗或内容单一调整图像归一化参数或减少 num_prototypes校准后所有类别相似度接近alpha 设置过大文本特征被过度拉向图像侧将 alpha 调整为 0.1~0.5 区间配合温度参数一起调显存不足输入尺寸过大原型数量过多降低 size将 num_prototypes 从 16 改为 8预测图有很多噪点特征空间分辨率低缺少空间约束增加 CRF 后处理或融合浅层特征图5.1 模型加载失败transformers默认会去 HuggingFace 下载模型。如果你的开发环境无法访问外网就会卡在这一步。解决方案是先在网络正常的机器上执行CLIPModel.from_pretrained()然后把缓存目录整体拷贝到目标机器并修改代码为self.clip CLIPModel.from_pretrained(/your/local/path, local_files_onlyTrue)5.2 相似度分数不具区分度当你发现pred_map中几乎所有像素都指向同一类别大概率是文本校准过强导致的。建议先把alpha调成 0确认基线预测是否正常基线正常后再逐步增大alpha观察哪一类像素被改善。这样可以快速定位是特征问题还是校准策略问题。5.3 分割边界粗糙patch16 模型在 224×224 输入下只有 14×14 特征图直接上采样到原图后边界一定粗糙。可行的缓解手段有两个一是把输入分辨率提高到 448 甚至 672同时插值位置编码二是结合浅层特征或使用 SAM 等模型的边缘信息做辅助。由于本文重点在文本校准边界优化属于后处理范畴建议在核心流程稳定后再加强。6. 最佳实践与工程建议6.1 类别文本的命名策略在开放词汇分割中类别名称怎么写对结果影响很大。建议优先使用稳定的英文单数名词并在必要是加上场景限定词。例如好的写法a person,a car,a grass area较差的写法people,cars,grassland这是因为 CLIP 的文本编码器在预训练时见过大量 “a photo of a ...” 风格的自然语言表达带冠词的短语往往比裸名词更稳定。需要注意的是类别名称变化直接影响文本特征分布因此做实验对比时文本模板最好固定不变只改变类别列表。6.2 原型数量与特征分辨率的选择原型数量不是越大越好。原型太少无法表达图像内的多语义区域原型太多则会把噪声和背景碎片也当成锚点反而干扰文本校准。以一张 448×448 的输入图片为例我建议在 4 到 16 之间做网格搜索先看中间结果的可视化效果再调最终分割指标。特征分辨率方面如果显存允许优先使用 ViT-B/16 在 448 分辨率下的输出特征。如果显存紧张可以退回到 336 或 224但后续的文本校准收益会受限因为像素级匹配本身精度就低。6.3 显存占用与速度优化Training-free 方法虽然没有训练开销但推理时如果批量处理多张图片显存占用依然明显。可以从三个方向优化关闭梯度计算这个代码里已经做了但要确认推理代码始终处于torch.no_grad()上下文内。类别文本特征只在类别列表变化时计算一次不要在每张图片上重复计算文本编码器。原型提取和文本校准可以放到半精度下运行将模型参数和输入转为float16在较新 GPU 上能显著提速且精度损失很小。半精度推理示例backbone.half() pixels pixels.half()6.4 评测指标与可控变量在评测开放词汇分割效果时建议同时记录 mIoU、类别平均准确率以及每类的 IoU。由于不同类别难易差异很大只看整体 mIoU 容易被头部类别主导。对长尾类别要单独观察校准前后的变化。每次实验尽量只改一个变量。比如先固定类别名称和原型数量只调 alpha确定 alpha 后再调整原型数量。不要同时修改多个超参数否则很难归因。7. 总结与下一步学习路线这篇文章从开放词汇语义分割的背景出发梳理了 Training-free 方法的痛点并围绕 Perceptual Anchoring 这个思路拆解成原型提取、文本校准、像素匹配三个模块。核心思想可以概括为一句话用图像内容生成感知原型再用原型去校准文本特征使预训练多模态模型在像素级任务上更“接地气”。代码部分给出了一个完整的最小实现你可以直接拿一张图片跑起来。跑通之后建议按以下顺序做深入探索把 K-Means 原型换成高响应区域原型对比不同原型提取方式的差异。尝试融合 CLIP 的浅层特征观察对边界质量的影响。引入 CRF 后处理或者在相似度图上加高斯先验。在 VOC20 或 COCO80 等标准基准上做离线评测记录校准前后的指标变化。如果在实操中遇到特征维度不匹配、模型加载缓慢、分割图出现大面积错分等问题可以回到第 5 节的排查表逐项检查。训练-free 的方法虽然不用训练但它对特征分布、文本模板和超参数的敏感度并不低多跑几组对照实验是理解这个方向最好的方式。