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:
co-authored by
Claude Opus 5
parent
506fde0290
commit
92c88b785c
+36
-1
@@ -37,9 +37,13 @@ def test_parse_override_accepts_yaml_style_scalars():
|
||||
|
||||
|
||||
def test_apply_overrides_creates_missing_nodes():
|
||||
cfg = apply_overrides({}, ["a.b.c=1"])
|
||||
"""非严格模式下仍可新建嵌套节点;严格模式(load_config 默认)会拦下未知键。"""
|
||||
cfg = apply_overrides({}, ["a.b.c=1"], strict=False)
|
||||
assert cfg == {"a": {"b": {"c": 1}}}
|
||||
|
||||
with pytest.raises(KeyError, match="不存在"):
|
||||
apply_overrides({}, ["a.b.c=1"])
|
||||
|
||||
|
||||
def test_load_config_with_base(tmp_path):
|
||||
(tmp_path / "base.yaml").write_text("train:\n lr: 0.1\n epochs: 10\n", encoding="utf-8")
|
||||
@@ -152,3 +156,34 @@ def test_eval_config_matches_its_training_backbone():
|
||||
("configs/train/resnet18.yaml", "configs/eval/resnet18.yaml"),
|
||||
]:
|
||||
assert load_config(train_cfg)["model"] == load_config(eval_cfg)["model"], eval_cfg
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"override,expected_hint",
|
||||
[
|
||||
("simclr.optmizer.weight_decay=0.0", "optimizer"), # 少了个 i
|
||||
("simclr.optimizer.lrr=1e-3", "lr"), # 多了个 r
|
||||
("num_clusers=4", "num_clusters"), # 少了个 t
|
||||
],
|
||||
)
|
||||
def test_set_rejects_typos_with_suggestion(override, expected_hint):
|
||||
"""--set 打错字母会凭空造出没人读的键,训练照跑但改动无效——必须拦下并给出建议。"""
|
||||
with pytest.raises(KeyError) as excinfo:
|
||||
load_config("configs/cluster/film_simclr_resnet50.yaml", [override])
|
||||
assert expected_hint in str(excinfo.value)
|
||||
|
||||
|
||||
def test_set_accepts_existing_keys():
|
||||
cfg = load_config(
|
||||
"configs/cluster/film_simclr_resnet50.yaml",
|
||||
["simclr.optimizer.lr=0.001", "simclr.optimizer.weight_decay=0.0", "num_clusters=7"],
|
||||
)
|
||||
assert cfg["simclr"]["optimizer"]["lr"] == 0.001
|
||||
assert cfg["simclr"]["optimizer"]["weight_decay"] == 0.0
|
||||
assert cfg["num_clusters"] == 7
|
||||
|
||||
|
||||
def test_apply_overrides_can_add_keys_when_not_strict():
|
||||
from utils.config import apply_overrides
|
||||
|
||||
assert apply_overrides({"a": {"b": 1}}, ["a.c=2"], strict=False) == {"a": {"b": 1, "c": 2}}
|
||||
|
||||
@@ -292,6 +292,39 @@ def test_resume_with_a_longer_schedule_rebuilds_the_lr_curve(tiny_image_dir, tmp
|
||||
assert lr == pytest.approx(1e-3 * (1 + math.cos(math.pi * 10 / 30)) / 2, rel=1e-3)
|
||||
|
||||
|
||||
def test_restart_schedule_gives_the_new_lr_full_value(tiny_image_dir, tmp_path):
|
||||
"""续训到调度末段时,cosine 会把新 lr 乘上接近 0 的系数;
|
||||
restart_schedule 按剩余轮数重开曲线,让新 lr 从满值开始。"""
|
||||
import math
|
||||
|
||||
work_dir = tmp_path / "run"
|
||||
_make_trainer(
|
||||
tiny_image_dir, work_dir, epochs=9,
|
||||
optimizer={"name": "adamw", "lr": 1e-3},
|
||||
).fit()
|
||||
|
||||
def resumed(restart):
|
||||
return SimCLRTrainer(
|
||||
BACKBONES.build({"name": "tiny_conv", "widths": [8, 16], "depths": [1, 1]}),
|
||||
_make_trainer(tiny_image_dir, work_dir, epochs=1).dataloader,
|
||||
{"epochs": 10, "amp": False, "log_interval": 999, "save_interval": 1,
|
||||
"scheduler": {"name": "cosine"}, "optimizer": {"name": "adamw", "lr": 1e-3},
|
||||
"projection": {"hidden_channels": 16, "out_channels": 8},
|
||||
"swanlab": {"enabled": False}, "restart_schedule": restart},
|
||||
torch.device("cpu"),
|
||||
work_dir,
|
||||
resume=work_dir / "simclr_last.pth",
|
||||
)
|
||||
|
||||
# 默认:沿用整条曲线,9/10 处只剩很小的系数
|
||||
default_lr = resumed(False).optimizer.param_groups[0]["lr"]
|
||||
assert default_lr == pytest.approx(1e-3 * (1 + math.cos(math.pi * 9 / 10)) / 2)
|
||||
assert default_lr < 1e-3 * 0.1
|
||||
|
||||
# 打开开关:新 lr 从满值开始
|
||||
assert resumed(True).optimizer.param_groups[0]["lr"] == pytest.approx(1e-3)
|
||||
|
||||
|
||||
def test_resume_takes_hyperparams_from_config_not_checkpoint(tiny_image_dir, tmp_path):
|
||||
"""续训时 lr / weight_decay / 总轮数一律以 yaml 为准,存档只提供进度。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user