fix(cluster): 配置层级写错不再静默忽略,并始终打印生效的超参

聚类配置里 optimizer/epochs 等键都在 `simclr:` 段下。写到顶层会被完全忽略、
不报任何错——用户改了 lr 却毫无反应,极难排查。同样的坑还有:改了基类
configs/cluster/film.yaml 的 simclr.optimizer,但运行的是子配置
film_simclr_resnet18.yaml,子配置里的同名键会盖掉基类的修改。

- 顶层出现 optimizer / scheduler / epochs / temperature / projection /
  save_interval / resume / checkpoint / log_interval 时警告并给出正确写法
- method != simclr 却写了 simclr: 段时提示该段不会生效
- SimCLRTrainer 初始化时无条件打印实际生效的 epochs / temperature /
  optimizer / scheduler,不再只在续训时才打印
- 修掉一处假差异:旧存档没有 scheduler_cfg 字段时会误报 "scheduler None -> cosine",
  那是"不知道"而非"当时没有调度器"

已验证 --set 覆盖、直接改 yaml、旧格式存档三条路径下 lr/weight_decay 均正常生效。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Shaun Zhou
2026-09-07 19:11:31 +08:00
co-authored by Claude Opus 5
parent 105cb38831
commit 506fde0290
3 changed files with 90 additions and 1 deletions
+10 -1
View File
@@ -92,6 +92,14 @@ class SimCLRTrainer:
self.global_step = 0
self.start_epoch = 0
# 无论是否续训都打印一次实际生效的超参:配置写错层级时最难发现的就是"改了没反应"
logger.info(
"SimCLR 超参:epochs=%d temperature=%.3g optimizer=%s scheduler=%s",
self.epochs,
self.temperature,
dict(self.cfg.get("optimizer", {})) or "(默认 adamw lr=1e-3)",
(dict(self.cfg.get("scheduler") or {})).get("name") or "(无)",
)
if resume:
self.load_state(resume)
@@ -297,7 +305,8 @@ class SimCLRTrainer:
changes.append(f"optimizer.{key} {archived} -> {current}")
archived_scheduler = (state.get("scheduler_cfg") or {}).get("name")
current_scheduler = (dict(self.cfg.get("scheduler") or {})).get("name")
if archived_scheduler != current_scheduler:
# 旧存档没有 scheduler_cfg 字段,那是"不知道"而不是"当时没有调度器",不能报成差异
if "scheduler_cfg" in state and archived_scheduler != current_scheduler:
changes.append(f"scheduler {archived_scheduler} -> {current_scheduler}")
if changes:
+29
View File
@@ -353,6 +353,33 @@ def save_preview(path: Path, metas: list[dict], indices: list[int], cell: int =
canvas.save(path)
#: 这些键只在 `simclr:` 段下有意义,写到顶层会被静默忽略
SIMCLR_ONLY_KEYS = (
"optimizer",
"scheduler",
"epochs",
"temperature",
"projection",
"save_interval",
"resume",
"checkpoint",
"log_interval",
)
def warn_misplaced_keys(cfg, method: str) -> None:
"""顶层出现本该放在 simclr: 下的键时警告——否则改了配置却毫无反应,极难排查。"""
misplaced = [key for key in SIMCLR_ONLY_KEYS if key in cfg]
if misplaced:
logger.warning(
"配置顶层出现了只在 `simclr:` 段下生效的键 %s,它们会被忽略。"
"把它们缩进到 simclr: 下面,例如 `simclr:\n optimizer: {lr: 0.0005}`",
misplaced,
)
if method != "simclr" and cfg.get("simclr"):
logger.info("当前 method=%s,`simclr:` 段下的配置不会生效", method)
def sweep_clusters(embeddings, spec: str, cluster_kwargs: dict) -> None:
"""扫一遍簇数,同时看轮廓系数和换种子的稳定性。
@@ -431,6 +458,8 @@ def main() -> int:
logger.error("--train-only 与 --sweep-k 不能同时用:前者不聚类,后者要靠聚类才能扫")
return 1
warn_misplaced_keys(cfg, method)
dataset = build_dataset(dict(cfg["dataset"]))
logger.info(
"扫描到 %d 张图,来自 %d 个目录;簇数 %d,method=%s,设备 %s",
+51
View File
@@ -489,6 +489,57 @@ def _cluster_argv(work_dir, image_dir, *extra):
]
def test_warns_when_simclr_keys_are_at_top_level(tiny_image_dir, caplog):
"""顶层写 optimizer 会被静默忽略,必须警告——否则"改了配置没反应"极难排查。"""
import logging
sys.path.insert(0, "scripts")
import cluster_unlabeled as cu
cfg = {
"dataset": {"name": "unlabeled", "root": str(tiny_image_dir)},
"optimizer": {"name": "adamw", "lr": 0.9}, # ← 应该放在 simclr: 下
"epochs": 100, # ← 同上
"simclr": {"epochs": 2},
}
with caplog.at_level(logging.WARNING, logger="mte"):
cu.warn_misplaced_keys(cfg, "simclr")
assert "optimizer" in caplog.text and "epochs" in caplog.text
assert "simclr" in caplog.text
def test_no_warning_when_keys_are_correctly_nested(tiny_image_dir, caplog):
import logging
sys.path.insert(0, "scripts")
import cluster_unlabeled as cu
cfg = {"dataset": {"name": "unlabeled", "root": str(tiny_image_dir)},
"simclr": {"optimizer": {"lr": 0.9}, "epochs": 2}}
with caplog.at_level(logging.WARNING, logger="mte"):
cu.warn_misplaced_keys(cfg, "simclr")
assert "会被忽略" not in caplog.text
def test_effective_hyperparams_are_logged_on_every_run(tiny_image_dir, tmp_path, caplog):
"""不只是续训时——每次训练开始都要打印实际生效的超参。"""
import logging
trainer = _make_trainer(
tiny_image_dir, tmp_path / "run", epochs=1,
optimizer={"name": "adamw", "lr": 0.00042, "weight_decay": 0.007},
)
del trainer
with caplog.at_level(logging.INFO, logger="mte"):
_make_trainer(
tiny_image_dir, tmp_path / "run2", epochs=1,
optimizer={"name": "adamw", "lr": 0.00042, "weight_decay": 0.007},
)
assert "SimCLR 超参" in caplog.text
assert "0.00042" in caplog.text
def test_train_only_stops_before_clustering(tiny_image_dir, tmp_path, monkeypatch):
"""--train-only 只训练存权重,不抽特征也不聚类。"""
sys.path.insert(0, "scripts")