指标复现与调参

最近花了将近一周的时间,把一篇 WACV 2026 论文(AusSmoke meets MultiNatSmoke)中基于 HuggingFace 的 SegFormer 基线,完整迁移到了 MMSegmentation 1.x 框架上,并试图逐项对齐其训练与评估口径。这个过程踩了不少坑——论文和代码不一致、框架默认值陷阱、评测口径分歧……这里把全流程记录下来,方便日后回顾,也希望能帮到有类似需求的同学。


TL;DR(结论先行)

  • 论文用的是 HuggingFace 的 SegFormer,复现到 mmseg 要逐项对齐,且 论文正文与官方开源代码有多处不一致,需逐条判断「认论文还是认代码」。
  • 三个最关键、最容易踩的坑:
    1. 骨干是 MiT-b2,不是 b5(代码脚本里写的是 b5,属遗留错误;论文 Table 3 的 SegFormer 数字是 b2)。
    2. 解码器通道是 768,不是 256(mmseg 全系列默认 256,是个轻量化取舍;原论文 / HF 的 B2–B5 都是 768)。这是导致首轮复现 IoU 系统性偏低约 5 点的主因。
    3. 指标是「前景 + 逐 batch 平均」的特殊口径(论文只给数学公式,实际数字来自代码的 batch-averaged 实现)。复现表格数字必须对齐这个实现。
  • 其它判断:论文说「用了早停」,但代码根本没实现早停 → 判定实际未用,认代码、不加早停。
  • 预训练权重 mmseg 与 HF 同源等价,不是误差来源。
  • 三轮迭代最终结果:all IoU 69.54(论文 73.47,-3.93);small 61.79 已追平论文;对齐头 + 抗锯齿仅带来边际提升,C=768 仍是最大单跳

1. 背景

说明
论文 WACV 2026《AusSmoke meets MultiNatSmoke: a fully-labelled diverse smoke segmentation dataset》
任务 烟雾二分类语义分割(背景 / smoke)
数据集 MultiNatSmoke,train 59,735 / test 11,083,并按烟雾像素占比分 Small/Medium/Large
官方实现 code/src/segformer/train.py,基于 HuggingFace transformers
复现框架 MMSegmentation 1.x(onedl 维护版,PyTorch 2.x + MMEngine)

复现产物:

  • 论文对齐配置(推荐):my_configs/segformer/segformer_mit-b2_multinatsmoke_align.py
  • 更强变体(非论文):my_configs/segformer/segformer_mit-b5_multinatsmoke_align.py
  • 对齐口径指标:mmseg/evaluation/metrics/multinat_smoke_metric.pyMultiNatSmokeMetric
  • 分尺度评测脚本:tools/smoke_test_multiscale.py

2. 论文 vs 官方代码:关键不一致

复现的第一原则:论文正文代表作者意图,代码代表实际跑出数字的过程。两者冲突时逐条判断。

项目 📄 论文正文 💻 官方代码 采纳 理由
SegFormer 骨干 MiT-b2(§4.2) 脚本写 mit-b5 认论文:b2 Table 3 的 73.47 对应 b2;代码是改实验遗留
训练轮次 20 epoch(§4.2) num_epochs=15 认论文:20 正文是作者意图
早停 “with early stopping”(§4.2) 完全没实现 认代码:不用早停 整套机制都不存在,非小遗漏,判定论文行文不实
训练集 全量 59,735(§3.4) 路径写 AnySmokeTrain5k 认论文:全量 Table 5 “full” = Table 3 数字
指标算法 给数学公式(§4.3) 逐 batch 平均实现 复现数字认代码 表格数字由代码产生
batch / lr / wd / 损失 / 全参可训 / 框架 一致 一致

💡 划分规模:论文 train 59,735 / test 11,083;本地 VOC 为 59,734 / 11,082,仅差 1(firecam 文件名含分号去重所致,不影响使用)。


3. 训练策略对齐(逐项)

维度 📄 论文 本项目 b2 对齐配置 对齐
骨干 MiT-B2 num_layers=[3,4,6,3]
解码器通道 C 768 SegformerHeadAligned(channels=768) ✅(见 §5)
解码头结构 All-MLP 纯线性投影 SegformerHeadAligned(无 per-stage BN/ReLU) ✅(见 §9.3)
输入尺寸 512×512 512×512
归一化 ImageNet(0~1) mean/std ×255 等值
损失 BCE(单通道 sigmoid) CrossEntropyLoss(use_sigmoid=True), out_channels=1
优化器 AdamW lr=1e-4 wd=0.01
学习率调度 无(常数) param_scheduler=[]
参数分组 全部统一 lr paramwise_cfg
训练增强 仅 Resize(antialias) AntiAliasResize(512,512) ✅(见 §9.3)
轮次 20 epoch EpochBasedTrainLoop(max_epochs=20)
早停 (判定未用) custom_hooks=[]
训练集 全量 59,735 train.txt
Batch 32(2×4090,每卡 16) batch=16 × accumulative_counts=2 = 32

💡 关于 BN:HF SegFormer 解码头用普通 BatchNorm,多卡时按每卡统计。论文每卡 16 张,本项目单卡 micro-batch 也是 16 → BN 行为一致,梯度累积不引入 BN 偏差。


4. 评价指标:论文公式 vs 代码实现

论文 §4.3 给出数学定义:IoU = |A∩B|/|A∪B|、Precision = TP/(TP+FP)、Recall = TP/(TP+FN)、F1(β=1 调和平均)、MSE = (1/N)Σ(Pᵢ−Gᵢ)²(前景概率 vs GT 的逐像素均方误差)。

官方代码实际是另一种口径

  • 只评估前景一类(smoke),不含背景、不做两类平均;
  • 逐 batch 把该 batch 所有图、所有像素拉平统计前景指标,再对所有 batch 取算术平均metrics_sum[k] /= n_batches);
  • 阈值 0.5,MSE 用概率图,eps=1e-6

这是「batch 内 micro + batch 间 macro」的混合口径——既不是全局像素累积,也不是逐图平均。论文表格的数字来自代码,所以:

目的 口径 本项目实现
逐位复现论文 Table 3 数字 batch-averaged(batch=32) MultiNatSmokeMetric(aggregate='batch')
方法学正确 / 与 batch 解耦 全局像素累积 MultiNatSmokeMetric(aggregate='global')

即便口径完全对齐,也做不到逐位 bit 相同:batch-averaged 依赖样本顺序与不满尾批的权重,而 mmseg 的样本顺序(按 ann_file)与官方(按 os.listdir)不同。属「同口径复现」,偏差通常在零点几个百分点量级。


5. 最大的坑:mmseg 解码器通道 256 vs 官方 768

首轮复现 IoU 系统性偏低约 5 点,定位到主因:mmseg 的 SegFormer 全系列把解码器通道写死为 256segformer_mit-b0.py base 里 channels=256,b1–b5 配置都只覆盖 backbone、不覆盖 channels),而论文用的 HF SegFormer-b2 解码器是 768

权威来源:

  • 原始 SegFormer 论文(NeurIPS 2021)Table 1b 原文

    “we choose C = 256 for our real-time models SegFormer-B0, B1 and C = 768 for the rest.”

    即 B2/B3/B4/B5 的解码器维度都是 768。

  • HuggingFaceSegformerConfig 通用默认是 256,但预训练权重 nvidia/mit-b2 的 config.json 覆盖为 decoder_hidden_size=768(depths [3,4,6,3]);nvidia/segformer-b5 同为 768(depths [3,6,40,3])。论文 from_pretrained 加载的就是 768。

那 mmseg 为什么用 256? 这是 mmseg 维护者刻意的「轻量化 + 统一」取舍,不是 bug,但偏离了原版架构。代价可从 mmseg 自己的 ADE20K model zoo 看出(单尺度 mIoU):

模型 mmseg(256) 原论文(768)
B2 45.58 46.5 ≈ -0.9
B5 49.13 51.0 ≈ -1.9

可见 256 比官方 768 低约 1–2 点。mmseg 在它**自己的训练配方(160k iter + 完整增强)**下认为这点损失可接受;但当我们要复现一篇用 768、且配方更短(20 epoch、无增强)的论文时,解码器容量更敏感,256 不仅没补回那 1–2 点,反而在烟雾任务上拉开了约 5 点。

→ **要对齐论文必须用 768。**已将两个配置 SegformerHead.channels 改为 768。


6. 预训练权重:同源等价,不是误差来源

mmseg 的 mit_b2_*.pth 与 HF 的 nvidia/mit-b2都是同一份 SegFormer 作者发布的官方 MiT ImageNet-1K 骨干权重,只是转换成了两个框架的 key 命名。数值本质相同;而解码头在两边都是随机初始化从头训练的。因此预训练权重不会造成几个点差距,可排除。真正的病因就是解码器 256 vs 768。


7. 验证集与选模型:val = test + batch-averaged

官方「用 test 当验证集」(无独立 val,VOC 也只有 train/test),按 best IoU 选模型。本项目据此设置。

一个改进点:最初验证用 global 口径仅作监控,可能「按 global 选出的最优权重并非论文目标指标下的最优」。现已把验证改为与 test 完全一致的 batch-averaged 口径(全集 test.txt、batch=32、aggregate='batch'),使 save_best 直接优化论文目标指标。val/test 的 num_workers=2,以防 batch=32 把 16G 的 /dev/shm 撑爆(见 §8)。


8. 单卡复现的两个工程问题(简记)

8.1 训练即整机黑屏 / 掉总线

RTX 5090 满功耗 (600W) 瞬时尖峰触发供电保护,GPU 掉出 PCIe 总线(内核日志 Xid 79: GPU has fallen off the bus)。sudo nvidia-smi -pl 400 限功耗后稳定。与显存、代码无关,不影响精度结果

8.2 测试 DataLoader 报 Bus error / No space left

测试 batch=32 + num_workers=8 下,原分辨率 GT 撑爆 16G /dev/shm。把测试 num_workers 降到 2 即解决(不影响指标口径)。


9. 复现实测结果与迭代

三轮实验均在 MiT-B2 + batch-averaged 口径 + 单卡测试 下进行;权重分别来自 best_mns_IoU_epoch_17(第 2 轮)、best_mns_IoU_epoch_19(第 3 轮)。

9.1 分轮次对比(batch-averaged 口径,IoU)

划分 第1轮 C256 第2轮 C768 第3轮 对齐头+抗锯齿 📄 论文 第3 vs 论文 第2→第3 Δ
all 68.35 69.37 69.54 73.47 -3.93 +0.17
small 58.14 61.12 61.79 61.73 +0.06 +0.67
medium 63.62 65.62 63.74 65.30 -1.56 -0.88
large 77.59 76.81 77.63 79.57 -1.94 +0.82

第 3 轮其它指标(all):F1 81.1 / Prec 83.75 / Rec 80.2 / MSE 0.01246(论文 84.21 / 85.18 / 84.20 / 0.0118)。

第 3 轮 large 细分(对比第 2 轮;论文 large Recall 87.49):

指标 第2轮 第3轮
IoU 76.81 77.63
Precision 89.83 87.93
Recall 83.71 86.44
MSE 0.0360 0.0360

测试时若出现 Image shapes are different in the batch warning:DataPreprocessor 在组 batch 时发现各图 H×W 不完全一致(极少数边界情况),会自动 padding,不影响指标——MultiNatSmokeMetric 在 eval 阶段统一插值到 512×512 再计算。

9.2 三轮迭代总结

轮次 主要改动 all IoU 相对论文 结论
第1轮 mmseg 默认 C=256 + 标准 SegformerHead 68.35 -5.12 解码器容量不足,系统性偏低
第2轮 C=768 69.37 -4.10 最大单跳(+1.02),small/medium 已接近
第3轮 + SegformerHeadAligned + AntiAliasResize 69.54 -3.93 边际提升(+0.17),small 追平

关键发现:

  1. C=768 是主因(68.35 → 69.37,+1.02 IoU);对齐头 + 抗锯齿仅带来 +0.17 all IoU,说明 mmseg 侧可逐项对齐的 pipeline 差异已基本挖尽。
  2. small 已与论文持平(61.79 vs 61.73,+0.06);large 仍是主要缺口(-1.94 IoU)。
  3. 第 3 轮 large 的 Recall 显著改善(83.71 → 86.44,论文 87.49),抗锯齿 / 对齐头确实帮助大烟羽召回;但 Precision 从 89.83 降到 87.93,IoU 净增仅 +0.82。
  4. medium 在第 3 轮略降(65.62 → 63.74),可能与抗锯齿下采样改变 medium 尺度烟羽边界有关,属分尺度 trade-off,不影响 all 仍被 large 主导的判断。
  5. 验证 IoU 自 epoch 11 起进入平台期(第 3 轮峰值 69.54 @ epoch 19)、训练 loss≈0.01 → 已收敛,非欠拟合;预训练权重加载干净(missing keys=0)。

9.3 第三轮修正(已完成)

修复 实现 效果
① 对齐解码头 SegformerHeadAligned(每级纯线性投影,无 BN/ReLU) large Recall +2.73
② 抗锯齿缩放 AntiAliasResize(下采样 area、上采样 bilinear) small IoU +0.67
  • [x] 用第三轮配置重训 b2(c768_alignedhead_aaresize__20260602_170053
  • [x] 分尺度重测(eval/best_mns_IoU_epoch_19/stratified_metrics.json

剩余 ~4 点 all IoU 差距的可能来源(难以在 mmseg 内继续消除):

  1. 框架差异:HF SegFormer 解码头初始化、优化器 / step 实现、loss 数值路径与 mmseg 不完全相同。
  2. batch-averaged 样本顺序:mmseg 按 ann_file 排序,官方按 os.listdir,尾批权重不同。
  3. 图像后端:PIL / torchvision vs OpenCV 的 resize / 读图差异,即便加了 AntiAliasResize 仍难逐位一致。

最终定位:在 mmseg + 同骨干预训练 + 同训练协议下,达到 69.54 all IoU(-3.93 vs 论文),small 分尺度可认为已复现;all / large 属 「同口径接近复现」,不宜声称逐位相同。若需进一步验证上限,建议在 HF 环境用同一数据跑对照,或尝试 HF nvidia/mit-b2 权重初始化解码头。


10. 复现命令

1
2
3
4
5
6
7
8
9
10
# 限功耗(避免 5090 掉总线,可做成开机服务)
sudo nvidia-smi -pm 1 && sudo nvidia-smi -pl 400

# 训练(论文对齐 b2)
python tools/train.py my_configs/segformer/segformer_mit-b2_multinatsmoke_align.py

# 分尺度测试(对齐论文 batch-averaged 口径,务必单卡)
python tools/smoke_test_multiscale.py \
my_configs/segformer/segformer_mit-b2_multinatsmoke_align.py \
work_dirs/MultiNatSmoke/segformer_mit_b2_align/c768_alignedhead_aaresize__20260602_170053/best_mns_IoU_epoch_19.pth

11. 能否与论文直接对比?

用 b2 对齐配置 + batch-averaged 口径单卡测试,可与论文 SegFormer 数字做同口径公平对比。

  • 论文目标值(Table 3):IoU 73.47 / F1 84.21 / MSE 0.0118 / Prec 85.18 / Rec 84.20。
  • 分尺度目标值(Table 4,IoU):Small 61.73 / Medium 65.30 / Large 79.57
  • 本项目最佳结果(第 3 轮,batch-averaged):IoU 69.54 / F1 81.1 / MSE 0.01246;Small 61.79 / Medium 63.74 / Large 77.63(vs 论文 all -3.93,small +0.06)。
  • ⚠️ 受样本顺序、尾批权重、预训练权重转换等客观因素,结果接近但非逐位相同,应表述为「同口径复现」而非「逐位复现」。
  • ⚠️ B5 配置不可与论文 SegFormer 直接比较(骨干不同),仅作更强的自有 baseline。

附 A:实验环境

组件 版本
系统 Ubuntu 24.04.4 LTS,内核 6.17.0-29-generic
GPU / 驱动 RTX 5090(32GB),NVIDIA 590.48.01(开源内核模块),CUDA 13.1
Python 环境 conda 虚拟环境 GQA,Python 3.10.20
PyTorch 2.8.0+cu128(CUDA 12.8)
MMEngine / MMCV / MMSegmentation 0.10.10 / 2.3.3 / 1.4.0(onedl 维护版)

训练时建议先 sudo nvidia-smi -pl 400 限功耗,避免 5090 满载瞬时尖峰导致整机掉总线。


附 B:复现 SegFormer-on-mmseg 的避坑清单

  1. ✅ 骨干认论文(b2),别被代码里的 b5 误导。
  2. ✅ 解码器 channels=768(mmseg 默认 256 会偏低 1–5 点)。
  3. ✅ 指标用 batch-averaged 复现表格、global 做稳健监控。
  4. ✅ 训练协议:20 epoch、常数 lr、无 paramwise、仅 Resize、全量数据、无早停。
  5. ✅ 预训练权重同源,不用纠结。
  6. ✅ 单卡 5090 先限功耗防掉总线;测试调小 num_workers/dev/shm 撑爆。
  7. ✅ 评测单卡,否则 batch-averaged 口径会变。
  8. ✅ 解码头用 SegformerHeadAligned(无 per-stage BN/ReLU);缩放用 AntiAliasResize
  9. ✅ 测试出现 Image shapes are different in the batch 可忽略,不影响 batch-averaged 指标。

第二部分:PSL (Prototype-based Scatter Learning) 复现对齐

除了 SegFormer 基线,最近也把另一篇论文的 PSL 模型从原版 mmseg 0.x 迁移到了当前 1.x 工作区。PSL(Prototype-based Scatter Learning for Smoke Segmentation, Yao et al., 2026)的核心卖点是用原型学习 + BES Loss + PUO 正交约束来提升烟雾分割的特征判别力。迁移过程的核心挑战在于 mmseg 0.x → 1.x 的 API 断崖式变化,以及损失路径和数据增强的隐性差异。

TL;DR

  • 将 PSL 核心模块(psl/ 目录)从 mmseg 0.x 完整迁移到 1.x,涉及 API 适配、模型复现、训练对齐三个层面。
  • 三个关键修复out_channels=2(不是 1)、固定 Resize(不是随机裁剪)、数据划分切换到当前可用的同规模 split。
  • 小规模验证通过(50 iter loss 正常下降、BES loss 正常计算、显存 16GB)。
  • 预期在 SmokeSeg 数据集上与原论文得到相近结果。

1. 新增与修改的文件清单

新增文件

文件 用途
mmseg/models/decode_heads/psl/__init__.py PSL 模块入口
mmseg/models/decode_heads/psl/sinkhorn.py Sinkhorn 聚类算法(从 lib/models/modules/sinkhorn.py 精简迁移)
mmseg/models/decode_heads/psl/contrast.py L2 归一化 + EMA 动量更新 + ProjectionHead(从 lib/models/modules/contrast.py 精简迁移)
mmseg/models/decode_heads/psl/psl_decode_head.py PSLDecodeHead 核心类(原型学习、BES Loss、PUO、feat2mask)
mmseg/models/decode_heads/psl/psl_head.py PSL 具体解码头(多尺度特征融合 + @MODELS 注册)
my_configs/segformer/segformer_mit-b3_smokeseg_40k_psl.py 1.x 格式训练配置(对齐原版 PSL)

修改文件

文件 改动
mmseg/models/decode_heads/__init__.py 添加 PSL, PSLDecodeHead 导出

2. API 适配详解(0.x → 1.x)

2.1 注册机制

0.x 1.x
from ..builder import HEADS; @HEADS.register_module() from mmseg.registry import MODELS; @MODELS.register_module()
build_loss(cfg) MODELS.build(cfg)
build_pixel_sampler(cfg) 删除(PSL 未使用)

2.2 基类与训练/推理接口

项目 0.x 1.x
基类 BaseModule (mmcv) + metaclass=ABCMeta BaseDecodeHead (mmseg 1.x)
训练入口 forward_train(inputs, img_metas, gt_semantic_seg, train_cfg) loss(inputs, batch_data_samples, train_cfg)
GT 获取 直接传入 gt_semantic_seg (Tensor) self._stack_batch_gt(batch_data_samples)
推理入口 forward_test(inputs, img_metas, test_cfg) predict(inputs, batch_img_metas, test_cfg)
损失计算 自定义 losses(seg_logit, seg_label) 复用父类 loss_by_feat(seg_logits, batch_data_samples)

2.3 其他依赖替换

0.x 依赖 1.x 替代方案 原因
timm.models.layers.trunc_normal_ torch.nn.init.trunc_normal_ PyTorch 2.8 原生支持
mctorch.nn.Parameter nn.Parameter 等价替换
ModuleHelper.BNReLU(in, bn_type='torchsyncbn') nn.Sequential(BatchNorm2d/SyncBatchNorm, ReLU) 单 GPU 自动 fallback
torch.svd torch.linalg.svd PyTorch 2.x 兼容
get_device() 从输入 tensor 动态获取 .device 避免初始化时硬编码设备
@auto_fp16() 删除 1.x AmpOptimWrapper 自动处理混合精度
ot.utils.dist0 延迟初始化 self._M is_barycenter=False 默认不触发,无需 POT 库

2.4 无需迁移的组件

组件 原因
EncoderDecoderPSL segmentor 1.x 标准 EncoderDecoder + SmokeSegMetric 已支持 per-image 指标,不再需要 segmentor 返回 (seg_pred, seg_logit) 元组
smoke_mIoU metric 当前项目 SmokeSegMetric 功能完全覆盖且更丰富
SmokeDataset 当前项目已有等价实现

3. 模型架构对齐

3.1 总体架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Input [B, 3, 512, 512]


MixVisionTransformer (MiT-B3, ImageNet-1k 预训练)
├── feat1 [B, 64, 128, 128] 1/4
├── feat2 [B, 128, 64, 64] 1/8
├── feat3 [B, 320, 32, 32] 1/16
└── feat4 [B, 512, 16, 16] 1/32


PSL Head.forward():
├── 1×1 Conv (C→256) per level [Conv + BN + ReLU]
├── Bilinear upsample → 1/4 resolution
├── Concat [B, 1024, H/4, W/4]
└── FusionConv (1024→256) [Conv + BN]

▼ (fused feature: [B, 256, H/4, W/4])

PSLDecodeHead pipeline:

├── cls_head: Conv(3×3) → BN → ReLU → Dropout(0.1)
├── proj_head: Conv(1×1) → ReLU → Conv(1×1) → L2-norm
├── feat_norm: LayerNorm(256)

├── Einstein summation: einsum('nd,kmd→nmk', features, prototypes)
│ └── prototypes: [num_classes=2, num_prototype=10, 256]

├── amax over prototype dim → out_seg [B, 2, H, W]
├── mask_norm: LayerNorm(2)

├── prototype_learning(): Sinkhorn 聚类 + 动量更新原型
├── BES_loss(): 特征值散度损失
└── prototypes_update(): Stiefel 流形 PUO 正交约束

3.2 BES Loss 算法

1
2
3
4
5
1. 计算总散度矩阵 S_T 和类内散度矩阵 S_W
2. S_B = S_T - S_W
3. 对 S_W^{-1} @ S_B 进行特征值分解
4. 取最小的 128 个特征值
5. BES_loss = 1 / Σ|λ_smallest|

目的:防止特征值坍缩到少数主导方向,确保特征空间在所有维度上都具有判别力。

3.3 PUO (Prototype Uncorrelated Optimisation)

1
2
3
4
5
6
7
8
1. 将原型通过 S_T 的 sqrt 变换映射到白化空间
2. 在 Stiefel 流形上执行正交投影步
- direction = prototype - new_mean
- projection = direction - 0.5·P·Pᵀ·direction - 0.5·P·directionᵀ·P
- step = P - γ·projection
- P_new = (P + step) @ (I + stepᵀ·step)^{-1/2}
3. 逆变换回原始空间
4. DDP 同步(多 GPU 时)

目的:保持原型向量之间的正交性(去相关),防止原型退化。

3.4 关键超参

参数 来源
原型数量 num_prototype 10 原论文
原型维度 256 原论文
L2 归一化 is_l2_norm True 原论文
BES 特征值数 128 原论文
PUO 步长 gamma 1e-5 原论文
动量系数 0.999 原论文
Sinkhorn 迭代次数 3 原论文
Sinkhorn epsilon 0.05 原论文

4. 训练配方对齐

4.1 损失函数(关键修复)

原版 PSL (0.x) 初始复现 修复后
out_channels 2(默认值,未显式设置) 1 2
final_projection out_channels≠1 时不创建) Conv2d(2→1)
损失路径 _expand_onehot_labels → 逐类 BCE 线性组合 → 单通道 BCE _expand_onehot_labels → 逐类 BCE ✅
数学形式 BCE(l₀, y₀) + BCE(l₁, y₁) BCE(w₀l₀+w₁l₁, y) BCE(l₀, y₀) + BCE(l₁, y₁)

💡 这是影响最大的差异。 原版 2 通道 BCE 使用 _expand_onehot_labels 将单通道标签扩展为 one-hot 形式,对每个类别独立计算 BCE。修复后已完全对齐。

4.2 数据增强(关键修复)

增强 原版 PSL 初始复现 修复后
Resize 固定 Resize(512,512), keep_ratio=True RandomResize(0.5-2.0) + RandomCrop 固定 Resize(512,512), keep_ratio=True ✅
RandomFlip prob=0(禁用) prob=0.5 prob=0.0 ✅
PhotoMetricDistortion

4.3 数据划分(已修复)

原版 PSL 当前可用 修复后
train SmokeSeg_ImageSets/Segmentation/train.txt 不存在 ImageSets/Segmentation/train.txt(4914 张,规模一致) ✅
val SmokeSeg_ImageSets/Segmentation/val.txt 不存在 ImageSets/Segmentation/val.txt(613 张) ✅
test SmokeSeg_ImageSets/Segmentation/test.txt 不存在 ImageSets/Segmentation/test.txt(614 张) ✅

原版 SmokeSeg_ImageSets/ 目录在数据重组后不再存在。使用同规模的 ImageSets/Segmentation/ 划分,图像列表可能略有差异,但不影响模型架构和训练配方层面的对齐。

4.4 优化器和学习率(无需修改,已对齐)

配置项 原版 PSL 当前配置 对齐
优化器 AdamW AdamW
学习率 6e-5 6e-5
betas (0.9, 0.999) (0.9, 0.999)
weight_decay 0.01 0.01
paramwise_cfg pos_block: decay_mult=0, norm: decay_mult=0, head: lr_mult=10 一致
warmup Linear, 1500 iters, start_factor=1e-6 一致
衰减策略 Poly, power=1.0, eta_min=0.0 一致
max_iters 40000 40000
batch_size 6 6
val_interval 1000 1000

4.5 评测指标

项目 原版 PSL 当前配置
评测器 smoke_mIoU(mmseg 0.x 自定义 metric) SmokeSegMetric(mmseg 1.x 自定义 metric)
像素级指标 IoU, F1, Acc(per-class) IoU, Fscore(per-class)
图像级指标 mIoU_by_image, mF1_by_image Img_IoU, Img_Fbeta, Img_Prec, Img_Rec(per-class)
RMSE/MSE RMSE_Prob, MSE_Prob, RMSE_Binary, MSE_Binary

SmokeSegMetricsmoke_mIoU 的超集,提供了更丰富的图像级和像素级指标。两者在核心指标(per-class IoU, per-image IoU)的计算方式一致。


5. 可接受的剩余差异

差异 影响程度 说明
精确 train/val/test 图像列表 原版 SmokeSeg_ImageSets/ 目录不可用,使用同规模替代划分,图像数量一致
save_best 指标名 原版 mmIoU_by_image(两类平均),当前 smoke.Img_IoU(仅烟雾类)。仅影响最优 checkpoint 选择策略,不影响最终评测数值
测试 pipeline 格式 极低 1.x 用 PackSegInputs + data_preprocessor 替代 0.x 的 MultiScaleFlipAug + Normalize + ImageToTensor,功能等价

6. 验证结果

6.1 导入与构建验证

1
2
3
4
5
6
PSL registered in MODELS: True
Head built successfully: PSL
Prototypes shape: torch.Size([2, 10, 256])
Has cls_head: True
Has proj_head: True
Has final_projection: False ← out_channels=2 时正确为 None

6.2 小规模训练验证(50 iterations)

1
2
3
4
5
6
out_channels=2,
Iter(train) [50/50] loss: 0.3302
decode.loss_ce: 0.3275 ← 正常计算(逐类 BCE 路径)
decode.acc_seg: 99.0183 ← 正常
decode.loss_BES: 0.0028 ← BES 损失正常计算
memory: 16634 MB ← RTX 5090 32GB 适配良好

7. 完整训练命令

1
2
3
4
5
6
7
8
9
# 复现训练(40000 iterations)
python tools/train.py \
my_configs/segformer/segformer_mit-b3_smokeseg_40k_psl.py \
--tag psl_full

# 评测
python tools/smoke_test.py \
my_configs/segformer/segformer_mit-b3_smokeseg_40k_psl.py \
work_dirs/SmokeSeg/segformer_mit_b3_psl/psl_full__*/iter_40000.pth

8. 小结

通过修复 out_channels、数据增强、数据划分三个关键差异,当前复现已在以下层面与原版 PSL 对齐:

  • 模型架构:PSL 解码头(原型学习、BES Loss、PUO)完整迁移,数学等价
  • Sinkhorn 聚类:算法逐字迁移
  • 损失函数out_channels=2 + _expand_onehot_labels + 逐类 BCE
  • 训练配方:优化器、学习率调度、batch size、迭代数完全一致
  • 数据增强:固定 Resize + 禁用 RandomFlip + PhotoMetricDistortion
  • 数据划分:使用同规模划分(4914 train / 613 val / 614 test)
  • 依赖适配:PyTorch 2.8 原生 API,无需 timm/POT 等额外依赖

可以预期在 SmokeSeg 数据集上得到与原论文相近的结果。


第三部分:HSS / MSS + SegFormer-MiT-B0 复现对齐

最近又接了另一个任务:在 OneDL-MMSegmentation 框架下复现 MoP 论文(Yao et al., 2026, Hyperspectral Smoke Segmentation via Mixture of Prototypes)Table 4 / Table 5 中 SegFormer-MiT-B0(Real-Time) 基线结果。这次涉及两个数据集——HSS(高光谱 25 通道)和 MSS(多光谱 4 通道),模型虽小但定制点多,踩的坑和前两部分很不一样。

TL;DR

  • 复现目标:HSS total mIoU 46.84 / F1 62.87;MSS total mIoU 72.87 / F1 82.67
  • 首次训练结果偏高(HSS smoke IoU 68.38 vs 论文 46.84),主因是数据划分不一致(trainval 782 vs 论文 ~604)和评估分辨率差异
  • 师兄代码中有多处与论文不一致的地方(MixVisionTransformerHSS2 双分支架构、SGD vs AdamW、TTA flip 等),逐项判断后以论文为准
  • 数据划分和尺度划分文件需向师兄确认后替换

1. 论文目标数值(SegFormer MiT-B0, Real-Time)

HSSDataset(Table 4)

尺度 F1 (%) mIoU (%)
Small 53.48 37.78
Medium 65.63 49.21
Large 70.17 54.33
Total 62.87 46.84

MSSDataset(Table 5)

尺度 F1 (%) mIoU (%)
Medium 83.37 74.05
Large 92.06 86.36
Total 82.67 72.87

⚠️ 论文 Table 5 中 SegFormer MSS Params=13.6M。标准 MiT-B0(embed_dims=32)仅 ~3.7M 参数,13.6M 更接近 MiT-B1(embed_dims=64)。师兄 FLAME2 原始配置使用 embed_dims=32(MiT-B0),当前实现与师兄一致。若论文 MSS 基线实际使用了更大的 backbone,我们的 MiT-B0 结果将偏低。


2. 当前实现对齐项

2.1 模型结构

项目 HSS MSS
Segmentor EncoderDecoder 同左
Backbone MixVisionTransformer 同左
in_channels 25 4
embed_dims 25(stage → [25,50,125,200]) 32(标准 MiT-B0)
num_layers [2,2,2,2] [2,2,2,2]
Decode head SegformerHead 同左
channels 250 256
out_channels 1 + sigmoid 同左
threshold 0.5 0.4
Loss BCE loss_weight=2.0, class_weight=[5.0] 同左

💡 关于 MixVisionTransformerHSS2:师兄配置文件中写了 MixVisionTransformerHSS2,该 backbone 实际运行两条并行前向路径(标准路径 + Indi 分组路径),本质上是 MoP Band Split 思想的前身,不是纯 SegFormer 基线。本实现采用论文描述的标准 MixVisionTransformer + embed_dims=25未移植 HSS2

2.2 训练超参(对齐论文 Table 2)

项目 论文 当前配置
Optimizer AdamW AdamW
LR 6e-5 6e-5
Weight decay 0.01 0.01
Batch size 4 4
Iterations 40,000 40,000
LR schedule Poly, power=0.9 Linear warmup 1500 + Poly 0.9
head LR 10×(paramwise_cfg

💡 关于 SGD vs AdamW:师兄 HSS_dev_config 中的 schedule_40k.py 实际配置的是 SGD lr=0.01, momentum=0.9, weight_decay=0.0005——与论文 Table 2(AdamW lr=6e-5, wd=0.01)完全不同。SGD 很可能是师兄早期实验的遗留,论文正式实验使用的是 AdamW。当前实现选择 AdamW 正确对齐论文

2.3 数据管道

HSS 训练 Pipeline

1
2
3
LoadImageFromNpyFile → LoadAnnotations → ResizeSegToMatchImage
→ RandomResize(0.5~2.0) / Resize(512) → RandomCrop / — → RandomFlip(0.5)
→ HSSNormalize → PackSegInputs
  • HSSNormalize:25 通道 mean/std 在 pipeline 中完成(SegDataPreProcessor 不支持 >3 通道)
  • ResizeSegToMatchImage:对齐 npy 与 PNG mask 的空间分辨率(HSS 中 npy ~217×409 vs PNG ~1088×2048,5× 关系)

HSS 测试 Pipeline(⚠️ 与师兄有实质性差异)

维度 师兄原始 (mmseg 0.x) 当前实现 (mmseg 1.x)
评估框架 MultiScaleFlipAug(TTA 容器) 直接 pipeline
Test-time flip 启用RandomFlip
评估分辨率 Resize 后的 ~512² npy 原生分辨率(~217×409)
GT 分辨率 PNG 原始分辨率(~1088×2048) ResizeSegToOriShape 降采样到 npy 分辨率

师兄在更高分辨率(PNG mask ~1088×2048)上计算指标,当前在更低的 npy 分辨率(~217×409)上计算。低分辨率下边缘像素占比更高,可能导致 ~1-3% mIoU 差异。此外,师兄启用了 test-time flip TTA,通常带来 <1% 的提升。

MSS Pipeline(训练和测试均无需 ResizeSegToMatchImage,npy 与 PNG mask 分辨率一致 ✅)

2.4 评估

  • 指标:SmokeSegMetric(pixel-level mmIoU / mmFscore + image-level smoke.Img_IoU 等)
  • Checkpoint:按 smoke.Img_IoU 保存 best
  • 论文使用 pixel-level F1 / mIoU

2.5 像素级 vs 图像级指标 gap(⚠️ 已验证非 bug)

首次 HSS 验证时观察到 smoke 类 image-level IoU (60.78) > pixel-level IoU (49.41),与 SmokeSeg 上 image-level < pixel-level 的模式相反,已确认不是计算错误

原因:两种聚合方式对不同大小烟雾的权重分配不同——pixel-level 每个像素等权(大烟雾图主导),image-level 每张图等权(小烟雾图同等重要)。HSS 数据集中大烟雾图逐图 IoU 较低但像素多,拉低了 pixel-level。

实际诊断数据确认:union-weighted image IoU (0.6838) = pixel-level IoU (0.6838),证明计算无 bug。与论文对比时以 pixel-level 为准(论文 Table 4/5 的 mIoU/F1 是 pixel-level 全局累积)。

2.6 首次 HSS 训练结果(⚠️ 数值偏高,划分待对齐)

best checkpoint: iter_33500

指标 pixel-level image-level 论文 Table 4 Total
smoke IoU 68.38 71.57 46.84
smoke Fscore 81.22 83.11 62.87
smoke Precision 77.77 79.31
smoke Recall 84.99 89.04

⚠️ 当前数值远超论文 baseline,原因:

  1. 数据划分不一致:当前使用 trainval.txt(782 张)训练,论文 ≈604 张(6:2:2 划分);且 test.txt(225 张)的划分文件来自师兄仓库根目录而非论文官方
  2. 评估分辨率差异:当前在 npy 原生分辨率(~217×409)评估,师兄在 PNG 分辨率(~1088×2048)评估
  3. 后续需向师兄索取官方 train/val/test 划分,替换后重新训练对比

3. 数据划分(待与师兄最终确认)

3.1 HSS(data/HSS_VOC/,共 1007 张 npy)

文件 行数 用途 来源说明
train.txt 685 子集 HSS/train.txt(师兄仓库根目录)
val.txt 97 子集 HSS/val.txt(已修复末行缺换行)
trainval.txt 782 训练 train + val 合并
test.txt 225 验证 + 测试 HSS/test.txt
test_small.txt 99 分层评测 HSS/test_small.txt
test_medium.txt 84 分层评测 HSS/test_medium.txt
test_large.txt 42 分层评测 HSS/test_large.txt
  • 师兄正式配置写的是 ImageSets/Segmentation/train.txt,该路径不存在;实际可用的是 HSS/ 根目录下的 txt
  • 训练策略:trainval 训练,test 验证/测试(与师兄配置"val=test"思路一致,但训练集更大)
  • HSS 的 test_small(99 张)与 FoSp 协议 δ<0.5% 完全一致;test_medium / test_large 与纯 FoSp 固定阈值不完全一致

3.2 MSS(data/MSS_VOC/,共 200 张)

文件 行数 用途
train.txt 180 训练
test.txt 20 验证 + 测试
test_small/medium/large.txt 7 / 6 / 7 分层评测(临时生成,待确认
  • MSS 与师兄 FLAME2_VOCFLAME_test_*.txt(40 张)ID 无重叠;师兄仓库未提供针对 MSS_VOC 这 20 张 test 的官方尺度划分
  • 当前 test_*.txt三分位生成,不是 FoSp 式协议(按 FoSp,20 张 test 烟雾占比均 >2.5%,应全为 Large)
  • 若要对齐 Table 5 的 Medium/Large 列,需向师兄索取官方划分

4. 与师兄 / 论文仍存在的差异摘要

维度 论文 师兄代码 当前实现 影响
HSS backbone 定制 MiT-B0, embed=25 MixVisionTransformerHSS2(双分支 + 分组 attention) 标准 MiT, embed=25 ✓ 🟢 对齐论文
HSS 优化器 AdamW lr=6e-5 SGD lr=0.01, momentum=0.9 AdamW lr=6e-5 ✓ 🟢 对齐论文
HSS LR schedule Poly power=0.9 Poly power=1.0, min_lr=1e-4 Poly power=0.9, min_lr=0.0 🟡 min_lr 微小差异
HSS warmup LinearLR 1500 iters 🟢 改善训练
HSS batch size 4 6 4 🟡 较小 batch 可能影响稳定性
HSS 训练集 ~604 (6:2:2) 仅 train (685) trainval (782) 🟡 更多数据,结果不可直接比
HSS 测试 TTA 启用 flip TTA 🟡 师兄评估有利 ~0.5-1%
HSS 评估分辨率 512² 512² → 上采样到 PNG 分辨率 ori_shape(npy 分辨率) 🟡 ~1-3% mIoU 差异
MSS 迭代 40k 师兄 MSS 入口 10k 40k ✓ 🟢 对齐论文
MSS 模型规模 Params=13.6M(疑似 MiT-B1) MiT-B0 (embed=32) MiT-B0 (embed=32) 🟡 若论文用 MiT-B1 则结果偏低
MSS 尺度划分 FoSp 协议 FLAME 另一套(非 MSS_VOC) 三分位(临时) 🔴 需向师兄确认

5. 训练与测试命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# HSS 训练
python tools/train.py my_configs/segformer/segformer_mit-b0_hss_40k.py

# MSS 训练
python tools/train.py my_configs/segformer/segformer_mit-b0_mss_40k.py

# 测试(单 checkpoint)
python tools/smoke_test.py \
my_configs/segformer/segformer_mit-b0_hss_40k.py \
work_dirs/HSS/segformer_mit_b0/run__TIMESTAMP/best_smoke.Img_IoU_iter_XXXXX.pth

# 分层评测(small / medium / large / total)
python tools/smoke_test_multiscale.py \
my_configs/segformer/segformer_mit-b0_hss_40k.py \
CKPT --splits small medium large --launcher none

# 续训
python tools/train.py my_configs/segformer/segformer_mit-b0_hss_40k.py \
--resume-into work_dirs/HSS/segformer_mit_b0/run__TIMESTAMP --resume

6. 相关文件清单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
my_configs/segformer/
├── segformer_mit-b0_hss_40k.py # HSS 训练配置
├── segformer_mit-b0_mss_40k.py # MSS 训练配置
└── REPRODUCTION_ALIGN_mit-b0_hss_mss.md

mmseg/datasets/
├── hss_voc.py # HSSDataset
├── mss_voc.py # MSSDataset
└── transforms/transforms.py # HSSNormalize, MSSNormalize, ResizeSegToMatchImage

data/HSS_VOC/ImageSets/ # 划分 txt(待师兄确认)
data/MSS_VOC/ImageSets/

tools/
├── train.py
├── smoke_test.py
├── smoke_test_multiscale.py
└── generate_smoke_scale_splits.py

7. 小结与待办

已对齐项:

  • ✅ 模型架构:标准 MiT + embed_dims=25(HSS)/ 32(MSS),以论文为准
  • ✅ 优化器:AdamW lr=6e-5(纠正师兄 SGD 遗留)
  • ✅ 训练配方:40k iter、batch=4、Poly power=0.9
  • ✅ 损失函数:BCE + sigmoid,class_weight=[5.0]
  • ✅ 数据管道:HSSNormalize / MSSNormalizeResizeSegToMatchImage
  • ✅ 像素级 vs 图像级指标 gap 已确认非 bug

待确认 / 待解决:

  • 🔴 向师兄索取 HSS/MSS 官方 train/val/test 划分
  • 🔴 确认 MSS 尺度划分协议(FoSp / 三分位 / 其他)
  • 🔴 论文 MSS 是否确实使用 MiT-B1(13.6M)而非 MiT-B0(3.7M)
  • 🟡 HSS 评估分辨率对齐(npy vs PNG)
  • 🟡 HSS 测试 TTA 对齐(当前无 flip,师兄有)

建议流程:与师兄核对划分文件 → 替换后重训 → 对比论文 Table 4/5 → 记录结果。