fix(config): --set 拦下不存在的键,并支持续训时重开学习率曲线

用户反馈"聚类配置里改不了 lr/weight_decay",实测是两件事:

1. --set 的键打错字母会凭空造出一个谁也不读的键,训练照跑但改动无效。
   实例:simclr.optmizer.weight_decay=0.0(optimizer 少个 i)被静默接受。
   现在 apply_overrides 默认只允许覆盖已存在的键,报错时给出同级可用键
   与最接近的拼写("是不是想写 'optimizer'?")。需要新增键时可 strict=False。

2. lr 其实生效了,但从 2229/2500 续训时 cosine 只剩满值的 3%,
   新设的 1e-3 落到 2.87e-5。新增 restart_schedule 开关:按剩余轮数重开曲线,
   新 lr 从满值开始;SimCLR 与检测训练都支持。续训日志补上调度进度,
   并在 lr 不足 initial_lr 的 10% 时警告并给出两种解法。

顺带:严格校验立刻暴露出 configs/cluster/film.yaml 里根本没有 scatter 段
(代码 cfg.get("scatter", {}) 兜底走默认值,所以功能正常但配置项形同虚设),已补上。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Shaun Zhou
2026-09-07 19:30:17 +08:00
co-authored by Claude Opus 5
parent 506fde0290
commit 92c88b785c
8 changed files with 165 additions and 7 deletions
+26 -2
View File
@@ -134,21 +134,45 @@ def parse_override(item: str) -> tuple[str, Any]:
return key.strip(), parsed if isinstance(parsed, (bool, int, float, list, type(None))) else raw
def apply_overrides(cfg: dict, overrides: Iterable[str]) -> dict:
def apply_overrides(cfg: dict, overrides: Iterable[str], strict: bool = True) -> dict:
"""把 `a.b.c=value` 写进配置。
`strict` 时只允许覆盖**已存在**的键:`--set` 打错一个字母(optimizer -> optmizer)
会凭空造出一个谁也不读的键,训练照跑但改动毫无效果,事后极难发现。
"""
for item in overrides:
key, value = parse_override(item)
parts = key.split(".")
node = cfg
for part in parts[:-1]:
for depth, part in enumerate(parts[:-1]):
nxt = node.get(part)
if not isinstance(nxt, dict):
if strict and part not in node:
raise KeyError(_unknown_key_message(key, ".".join(parts[:depth]), node, part))
nxt = {}
node[part] = nxt
node = nxt
if strict and parts[-1] not in node:
raise KeyError(_unknown_key_message(key, ".".join(parts[:-1]), node, parts[-1]))
node[parts[-1]] = value
return cfg
def _unknown_key_message(full_key: str, prefix: str, node: Mapping, missing: str) -> str:
"""报错时给出同级可用键与最接近的拼写,方便一眼看出打错在哪。"""
import difflib
available = sorted(k for k in node if not k.startswith("_"))
close = difflib.get_close_matches(missing, available, n=3, cutoff=0.6)
where = f"`{prefix}` 下" if prefix else "配置顶层"
hint = f",是不是想写 {close[0]!r}?" if close else ""
return (
f"--set 里的 {full_key!r} 在配置中不存在:{where}没有 {missing!r}{hint} "
f"同级可用的键有 {available}。"
"(只允许覆盖已存在的键;确实要新增请先写进 yaml)"
)
def load_config(path: str | Path, overrides: Iterable[str] = ()) -> Config:
cfg = _load_raw(Path(path), set())
cfg = apply_overrides(cfg, overrides)