ARTICLE DETAIL

资讯详情

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

MaskCLIP无监督分割实战

MaskCLIP无监督分割实战

MaskCLIP 复现完整记录

第一阶段:尝试复现与基础问题修复

1. 安装与环境配置

在 GitHub 上克隆源文件:chongzhou96/MaskCLIP: Official PyTorch implementation of "Extract Free Dense Labels from CLIP" (ECCV 22 Oral)

安装必要的依赖。

环境适配说明:本人配置为 RTX 5060,必须搭配CUDA 12.x 及 PyTorch 2.x;而MaskCLIP官方代码库基于早期 OpenMMLab 体系(mmsegmentationv0.x /mmcv1.x),默认要求的 Python 3.7/3.8 和 PyTorch 1.x 无法在新显卡上调用 CUDA,于是禁用所有 CUDA,改用 CPU 计算。

2. 下载并转换 CLIP 模型

运行convert_clip_weights.py:生成一个ViT16_clip_backbone.pth权重文件。

  • --backbone参数时:提取出ViT16_clip_backbone.pth(主干网络权重)。
  • 不带--backbone参数时:提取出ViT16_clip_weights.pth(解码头需要的投影层权重)
3. 准备感兴趣对象的文本嵌入

运行prompt_engineering.py,提取 PASCAL VOC 数据集的类别文本特征,用来做无监督分割:voc_ViT16_clip_text.pth文本特征文件生成并保存到pretrain/目录下。

运行python tools/test.py configs/maskclip/maskclip_vit16_520x520_pascal_context_59.py pretrain/ViT16_clip_backbone.pth --show-dir output/,生成了context_ViT16_clip_text.pth文件。

3.1 解决 mmcv._ext 模块缺失问题

mmseg在启动时会自动加载所有的网络组件和损失函数,其中包含了一些用 C++ 写的算子(如focal_loss)。它调用的mmcv.ops试图加载编译好的 C++ 动态链接库mmcv._ext。因为在 Windows + 较新版本 PyTorch 下,通过标准pip安装的mmcv通常只包含了纯 Python 代码,缺少编译好的_ext.pyd文件,所以抛出了ModuleNotFoundError: No module named 'mmcv._ext'

4. 数据集准备

去 PASCAL Context 官网下载数据集。

E:\MaskCLIP\data\ └── VOCdevkit/ └── VOC2010/ ├── JPEGImages/ <-- 从 VOCtrainval_03-May-2010 得到的所有 .jpg 图片 │ ├── 2008_000002.jpg │ └── ... ├── ImageSets/ │ └── SegmentationContext/ │ └── val.txt └── SegmentationClassContext/ <-- 从 Stanford 的 59_context_labels.tar.gz 中解压 ├── 2008_000002.png └── ...
5. 获取定量结果(mIoU)—— 首次尝试失败
5.1 为什么 mIoU 只有 1.42 且一直报 Missing Keys
5.1.1 权重文件被拆分

权重文件被拆分了tools/test.py加载的pretrain/ViT16_clip_backbone.pth文件里只有主干网络 (Backbone) 的权重,完全不包含decode_head的文本向量和投影矩阵。

tools/test.py里重写了校验逻辑,确保在模型推理前,强制把文本向量和 1x1 卷积权重补充挂载进去

5.1.2 源码拼写错误

源码自带致命拼写 Bug:在mmseg/models/decode_heads/maskclip_head.pyinit_weights函数中,原作者把类名拼错了:写成了super(MaskCLIPHead, self)(多了个大写的L),导致 Python 报NameError并跳过了权重的初始化。

5.1.3 DataContainer 格式解包失败

DataContainer格式解包失败:在mmsegmentation/mmcv中:

  1. 单卡/CPU 测试(single_gpu_test)期望从 DataLoader 传进来的img_metas是一个标准的list(比如[{'ori_shape': ...}])。
  2. 但启动了强制 CPU 模式,没有用标准的MMDataParallel包装,DataLoader 吐出来的img_metas被封装在了一个DataContainer对象里(没有自动被.data[0]解包出来)。
  3. 导致模型在读取图像尺寸ori_shape时,拿着DataContainerlist迭代,直接抛出了TypeError
5.1.4 CPU 单卡运行问题

CPU 单卡运行时,没有 MMCV 的MMDataParallel自动拆包,数据流里的img_meta在每个流程节点都需要手动把.data提出来。

5.1.5 img_meta 数据结构问题

问题原因:测试时img_meta经过forward_test整理后是List[List[dict]]结构(外层=测试增强,内层=batch 图片),inferenceimg_meta[0]拿到的是内层 list 而非 dict,导致img_meta[0]['ori_shape']抛出TypeError

修复内容:inference方法中增加一层扁平化处理,将[[{...}]]规范化为[{...}]

5.2 修复后 mIoU 仍然很低

mIoU仍然很低~1.4%:所有类别的余弦相似度得分都在0.25 ~ 0.32之间,非常接近。

???

尝试改在wsl里面运行


第二阶段:深入诊断与根因分析

MaskCLIP Zero-Shot 评估全流程诊断报告
一、问题现象

在 PASCAL Context 59 类上运行 MaskCLIP (ViT-B/16) 零样本评估,命令:

python tools/test.py configs/maskclip/maskclip_vit16_520x520_pascal_context_59.py pretrain/ViT16_clip_backbone.pth --eval mIoU

结果:mIoU = 1.63%(随机基线 1/59 ≈ 1.69%),几乎等于瞎猜。

二、排查过程
阶段 1:标签映射 (已解决,非根因)

问题:最初怀疑_LABEL_MAP映射错误。

关键发现:PASCAL Context 官方 Stanford 59_context_labels 是调色板/索引 PNG (mode='P'),像素值直接为 1~59。

重要教训cv2.imread(GRAYSCALE)在读取调色板 PNG 时会读取 RGB 调色板色值而非索引值,导致错误分析。必须用PIL/Pillow读取:

# 错误方式 cv2.imread(file, cv2.IMREAD_GRAYSCALE) # 读取调色板颜色值,非索引 # 正确方式 np.array(Image.open(file)) # 正确返回 1~59 的调色板索引

最终状态mmseg/datasets/pascal_context.pyself.label_map = Nonereduce_zero_label=True独自处理 1→0, 2→1, ..., 59→58 的映射。所有 5,105 张验证图片的 59 个类别标注均完整。

阶段 2:后处理阈值 (不相关)

Config 中ks_thresh=0.pd_thresh=0.已为 0,refine_output()方法实际上跳过所有后处理,直接返回原始 logits。

阶段 3:权重加载验证 (全部正确)
Backbone (151/151 keys 全部匹配)
checkpoint = load_checkpoint(model, 'pretrain/ViT16_clip_backbone.pth', ...)

验证了pos_embedcls_tokenpatch_embed.projection.weight、所有 12 层的attn/ln/ffn权重 ——全部与 checkpoint 完全一致

Decode Head

MaskClipHead.__init__()分别从两个文件加载:

文件内容状态
pretrain/context_ViT16_clip_text.pthtext_embeddings [59, 512], L2 norm ≈ 1.0正确加载
pretrain/ViT16_clip_weights.pthproj.weight [512, 768] + CLIP 原始权重备份正确加载
阶段 4:前向传播逐阶段追踪 (核心发现)

对单张图片2008_000002.jpg(390×520)进行逐阶段追踪:

  • Step 1: Backbone → v (value-path features)
    • shape: [1, 768, 25, 33]
    • per-pixel norm (channel L2): mean = 27.71 ≈ sqrt(768)
  • Step 2: proj(v) [Conv2d 768→512]
    • shape: [1, 512, 25, 33]
    • per-pixel norm: mean = 7.87
  • Step 3: L2 normalize per pixel
    • 每个 512-dim 向量 → 单位向量
  • Step 4: Cosine similarity with text_embeddings [59, 512]
    • mean = 0.000685 ←正交!
    • std = 0.031694
  • Step 5: ×100 (logit_scale)
    • range: [-8.52, 9.52]
  • Step 6: Softmax
    • 最大置信度 per-pixel: mean = 0.33, median = 0.36
    • 仅 19/59 类别被预测到
阶段 5:GitHub Issue 线索

找到了一个相同的 GitHub issue (Xuefei98, 2023年7月26日),同样是在 PASCAL Context 59 上测试结果不对。评论者 WJsebastian 建议 "adding the pre-trained weights' path could be of help"。

阶段 6:convert_clip_weights.py 源码分析
# convert_clip_weights.py, line 28-33 if 'ViT' in args.model and prefix in key: new_key = key[len(f'{prefix}.'):] if new_key == 'proj': all_model['proj'] = {} all_model['proj']['weight'] = state_dict[key].float().t() # ← 转置! continue

CLIP ViT 的visual.proj形状为[768, 512](width=768, output_dim=512)。脚本将其转置[512, 768]保存,匹配 MaskCLIP head 中Conv2d(768, 512, 1)的权重格式。这个转置是正确的。

阶段 7:prompt_engineering.py 分析

使用标准 CLIP 提示工程(85 个模板),对每个类别进行:

  1. 对每个模板填充类名 → tokenize
  2. 用 CLIP text_encoder 编码 → 得到 embedding
  3. L2 normalize
  4. 对同一类别的 85 个模板 embedding 取平均 → 最终类 embedding
  5. L2 normalize 最终结果
三、根因判断(完整版)
3.1 排除法:四个怀疑方向全部验证通过

我们对四个最可能出问题的地方做了逐项验证,全部排除

测试项验证方法结论
Root Cause Ainit_weights()覆盖已加载权重对比load_visual_projs()前后 decode_head 参数✅ 通过 —load_visual_projs()init_weights()之后调用,CLIP 权重不会被覆盖
Root Cause B:文本嵌入 L2 归一化维度错误检查所有 59 个类别的 L2 norm✅ 通过 — 全部 ≈ 1.0,沿dim=-1归一化正确
Root Cause Cproj权重转换错误对比 CLIPvisual.proj和转换后proj.weight✅ 通过 — 转置.t()后形状正确,数值完全一致
Root Cause D:MaskCLIP ViT 前向传播与原始 CLIP ViT 存在差异逐层对比 mmcv ViT 与 PyTorch 原生 MultiheadAttention✅ 通过 — 实现完全等价
3.2 Test D 详细验证:mmcv ViT == PyTorch ViT

这是最关键的一次验证。我们写了一个修正了残差连接处理的诊断脚本,将 mmcv 的 MultiheadAttention 和 PyTorch 原生的 nn.MultiheadAttention 在完全相同的输入和权重下做对比:

Attention output comparison (residual removed): max_diff = 2.542511e-07 cos = 1.00000131 >>> IDENTICAL: mmcv MultiheadAttention == torch.nn.MultiheadAttention Full pre-norm block comparison: max_diff = 4.768372e-07 cos = 1.00000048 >>> IDENTICAL: mmcv block == naive torch block

结论:mmcv 的 ViT 实现和 PyTorch 原生实现在数值上完全一致,不存在任何计算 Bug。

唯一的微小差异来自 LayerNorm 的 eps 参数(MaskCLIP 配置使用 eps=1e-6,CLIP 原版使用 eps=1e-5),这个差异在 12 层 Transformer 中累积后会导致 CLS token 余弦相似度约 0.94(~6% 的方向偏移),但这属于正常的数值行为,不是 Bug,也不足以解释 mIoU 1.63% 的惨烈结果。

3.3 真正的根因

所有代码层面的可能性都被排除了。真正的问题在于:

PASCAL Context 59 类的 CLIP 文本嵌入高度相关。

数据分析显示:

  • 59 个文本嵌入的平均向量范数:0.9003
  • 类间余弦相似度:最小 +0.159,均值 +0.322(全部为正!)

什么意思呢?在 CLIP 的 512 维特征空间里,这 59 个类别的文本描述全部挤在同一个狭窄的锥面内,彼此之间没有负相关(即不存在"对立"的语义方向)。所以无论你输入什么图片,模型在每个像素上都只能看到一堆相似度差不多的分数,softmax 后接近于均匀分布。

换数据集无法解决这个问题。我们用随机噪声torch.randn(1, 3, 520, 520)测试时,raw cosine mean 已经是 -0.002——即在完全没有语义信息的随机输入上,视觉特征就已经和文本嵌入正交了。这说明问题出在 proj 投影层的翻译能力,而非数据集本身。

CLIP 的visual.proj权重是为全局 [CLS] token 训练的,不是为 patch-level value features 训练的。把它直接用在每个像素的 v 特征上,投影后的方向就不对——这就是 "投影层翻译坏了" 的本质。

四、其他已修复的次要问题
问题修复位置说明
persistent_workers 错误mmseg/datasets/builder.py:145-147num_workers=0 时自动禁用 persistent_workers
_LABEL_MAP 误用mmseg/datasets/pascal_context.py已移除,依赖 reduce_zero_label=True
single_gpu_test 兼容性tools/test.py:239使用 scatter_kwargs 代替 MMDataParallel
cv2 读取调色板 PNG无代码改动教训:必须用 PIL Image.open() 读取 indexed/palette PNG
convert_clip_weights.py LN 命名脚本中 norm 应改为 lnmmcv build_norm_layer 将 LN 命名为 ln0/ln1,非 norm0/norm1

第三阶段:PASCAL VOC 2012 完整评估结果

maskclip_a 在 Pascal VOC 2012 上的完整评估结果

1449张验证集图片,单尺度推理,mIoU = 24.62%

每类 IoU(按降序排列):

类别IoU类别IoU
dog (狗)53.80%sheep (羊)18.12%
bus (公交车)53.32%train (火车)16.36%
car (汽车)44.01%boat (船)14.72%
bird (鸟)41.43%bicycle (自行车)11.77%
horse (马)40.12%sofa (沙发)9.20%
dining table (餐桌)38.09%motorbike (摩托车)8.31%
cow (牛)38.07%potted plant (盆栽)7.18%
cat (猫)37.19%airplane (飞机)5.89%
bottle (瓶子)31.05%tv monitor (电视)3.76%
chair (椅子)18.67%person (人)1.26%
切换到官方 mmseg 实现后的 VOC 20 类结果

后来,我用官方 mmseg 版本的 MaskCLIP(而非简化版)重新在 VOC 2012 上跑了一遍。这次的结果是:

官方 mmseg 版 VOC 2012 每类 IoU:
类别IoU类别IoU
cat69.2%cow54.6%
airplane64.9%car53.2%
train60.1%motorbike49.9%
dog59.6%person49.7%
bus58.2%bottle42.1%
horse57.8%sofa40.5%
bird57.5%tv monitor39.2%
potted plant57.0%bicycle38.4%
sheep56.2%boat32.1%
dining table28.8%chair20.6%

mIoU: 49.49%!

这个数字远远高于之前非官方版本的 24.62%。

对比总结
对比项e:/MaskCLIP (mmseg)maskclippytorch
VOC 2012 mIoU49.49%24.62%
Pascal Context 59 mIoU0.62%?
架构风格mmseg 0.x, MaskClipHead独立封装, MaskCLIP.forward
输入分辨率512×512 (bicubic pos embed)512×512
归一化ImageNet (mean/std RGB)CLIP (mean/std)
v features 处理out_proj + v+=x residual直接取 transformer layer v

原论文里两个数据集的零样本结果是:

  • PASCAL Context 59 类:21.7-22.9%
  • PASCAL VOC 20 类:没有直接报告原始 MaskCLIP 的零样本数字,但从 MaskCLIP+ 的提升幅度(从 ~35% 到 86%)和社区复现的 58.5% 来看,VOC 20 类的合理零样本基线应该在 50%-60% 区间。

所以这次跑出的 49.49% 在 VOC 20 类上完全是合理的

真正的问题是 PASCAL Context 59 类只有 1.6%。那为什么 PASCAL Context 59 类只有 1.6%,而 VOC 20 类有 49.49%?关键差异已经在之前的诊断中确定了——59 个类别的文本嵌入在 CLIP 空间中高度相关,类间余弦相似度全部为正值,模型无法有效区分。简单说就是:VOC 20 类的语义空间区分度足够,而 PASCAL Context 59 类的语义空间太挤。

49.49% 对零样本 MaskCLIP 在 VOC 2012 上是合理的。它是以下因素的组合:

  • 正确的骨干架构(ViT + CLIP 权重)
  • 较高的输入分辨率(512 vs 224)
  • 正确的 v 特征提取(out_proj+ 残差连接)
最终结论

不是数据集的问题。

官方的 mmseg 版 MaskCLIP 在 VOC 2012(20类)上跑得相当好,mIoU = 49.49%,没有任何一个类别零分,所有20类都有显著的 IoU。这说明 mmseg 实现本身是正常工作的。

返回列表