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
+29
-4
@@ -287,6 +287,21 @@ class SimCLRTrainer:
|
||||
# 否则从 0 出发只会一路留在 0。
|
||||
for group in self.optimizer.param_groups:
|
||||
group["lr"] = group.get("initial_lr", group["lr"])
|
||||
|
||||
if self.cfg.get("restart_schedule"):
|
||||
# 续训到调度末段时,cosine 会把新设的 lr 乘上一个接近 0 的系数
|
||||
# (2229/2500 处只剩 2.9%),"调大学习率接着练"就落空了。
|
||||
# 打开这个开关:按剩余轮数重开一条曲线,新 lr 从满值开始。
|
||||
remaining = max(1, self.epochs - self.start_epoch)
|
||||
self.scheduler = build_scheduler(self.optimizer, self.cfg.get("scheduler"), remaining)
|
||||
logger.warning(
|
||||
"restart_schedule=true:学习率调度从 epoch %d 重新开始,覆盖剩余 %d 轮,"
|
||||
"起始 lr=%.2e",
|
||||
self.start_epoch,
|
||||
remaining,
|
||||
self.optimizer.param_groups[0]["lr"],
|
||||
)
|
||||
return
|
||||
self.scheduler = build_scheduler(self.optimizer, self.cfg.get("scheduler"), self.epochs)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore") # 快进阶段的 "step before optimizer.step()" 属预期
|
||||
@@ -311,13 +326,23 @@ class SimCLRTrainer:
|
||||
|
||||
if changes:
|
||||
logger.warning("配置与存档不一致,按**当前配置**执行:%s", ";".join(changes))
|
||||
group = self.optimizer.param_groups[0]
|
||||
initial = group.get("initial_lr", float("nan"))
|
||||
logger.info(
|
||||
"续训超参:lr=%.2e(initial_lr=%.2e)weight_decay=%s,共 %d 轮",
|
||||
self.optimizer.param_groups[0]["lr"],
|
||||
self.optimizer.param_groups[0].get("initial_lr", float("nan")),
|
||||
[g["weight_decay"] for g in self.optimizer.param_groups],
|
||||
"续训超参:lr=%.2e(initial_lr=%.2e,调度进度 %d/%d)weight_decay=%s",
|
||||
group["lr"],
|
||||
initial,
|
||||
self.start_epoch,
|
||||
self.epochs,
|
||||
[g["weight_decay"] for g in self.optimizer.param_groups],
|
||||
)
|
||||
if initial and group["lr"] < initial * 0.1 and not self.cfg.get("restart_schedule"):
|
||||
logger.warning(
|
||||
"当前 lr 只有 initial_lr 的 %.1f%%——调度已接近末尾,此时调大 lr 收效有限。"
|
||||
"想让新 lr 真正生效可加 --set simclr.restart_schedule=true(按剩余轮数重开曲线),"
|
||||
"或调大 simclr.epochs 拉长整条曲线。",
|
||||
group["lr"] / initial * 100,
|
||||
)
|
||||
|
||||
def _checkpoint(self, epoch: int) -> None:
|
||||
"""同时落两份:给下游用的骨架,和给续训用的完整状态。"""
|
||||
|
||||
@@ -200,6 +200,19 @@ class Trainer:
|
||||
# 否则从检查点带来的 lr 出发会算出一条错的曲线。
|
||||
for group in self.optimizer.param_groups:
|
||||
group["lr"] = group.get("initial_lr", group["lr"])
|
||||
|
||||
if self.cfg.get("restart_schedule"):
|
||||
# 续训到调度末段时 cosine 会把新设的 lr 乘上一个接近 0 的系数,
|
||||
# "调大学习率接着练"就落空了。打开这个开关按剩余轮数重开一条曲线。
|
||||
remaining = max(1, self.epochs - self.start_epoch)
|
||||
self.scheduler = build_scheduler(self.optimizer, self.cfg.get("scheduler"), remaining)
|
||||
logger.warning(
|
||||
"restart_schedule=true:学习率调度从 epoch %d 重新开始,覆盖剩余 %d 轮,起始 lr=%.2e",
|
||||
self.start_epoch,
|
||||
remaining,
|
||||
self.optimizer.param_groups[0]["lr"],
|
||||
)
|
||||
return
|
||||
self.scheduler = build_scheduler(self.optimizer, self.cfg.get("scheduler"), self.epochs)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore") # 快进阶段的 "step before optimizer.step()" 属预期
|
||||
|
||||
Reference in New Issue
Block a user