| |
| |
| |
| |
|
|
| from inspect import signature |
| import math |
| from typing import Any, Literal |
|
|
| import torch |
| from packaging.version import Version |
| from torch.optim import lr_scheduler as torch_schedulers |
| from torch.optim.optimizer import Optimizer |
|
|
| TORCH_VERSION = Version(torch.__version__) |
|
|
|
|
| def annealing_cos(start, end, pct): |
| "Cosine anneal from `start` to `end` as pct goes from 0.0 to 1.0." |
| cos_out = math.cos(math.pi * pct) + 1 |
| return end + (start - end) / 2.0 * cos_out |
|
|
|
|
| def annealing_linear(start, end, pct): |
| "Linearly anneal from `start` to `end` as pct goes from 0.0 to 1.0." |
| return (end - start) * pct + start |
|
|
|
|
| class WarmupOneCycleLR(torch_schedulers.LRScheduler): |
| def __init__( |
| self, |
| optimizer: Optimizer, |
| max_lr: float | None = None, |
| total_steps: int = 0, |
| warmup_iters: int = 0, |
| warmup_ratio: float = 0.0, |
| pct_start: float = 0.295, |
| anneal_strategy: Literal["cos", "linear"] = "cos", |
| base_momentum: float = 0.85, |
| max_momentum: float = 0.95, |
| div_factor: float = 25.0, |
| final_div_factor: float = 1000.0, |
| use_beta1: bool = True, |
| update_momentum: bool = True, |
| last_epoch: int = -1, |
| ): |
| """ |
| A variant of OneCycleLR with a warmup on top which potentially |
| replaces the first phase of the original OneCycleLR. |
| """ |
| self.warmup_iters = warmup_iters |
| self.warmup_ratio = warmup_ratio |
| self.max_lr = max_lr |
| self.min_point = float(pct_start * total_steps) |
| self.base_momentum = base_momentum |
| self.max_momentum = max_momentum |
| self.total_steps = total_steps |
| self.use_beta1 = use_beta1 |
| self.anneal_strategy = anneal_strategy |
| self.final_div_factor = final_div_factor |
| self.update_momentum = update_momentum |
| assert self.anneal_strategy in [ |
| "cos", |
| "linear", |
| ], f"Only cosine and linear-annealing strategy supported, got {self.anneal_strategy}" |
| assert total_steps > 0 |
|
|
| |
| for group in optimizer.param_groups: |
| if "initial_lr" not in group: |
| assert last_epoch == -1 |
| ml = group["lr"] |
| assert isinstance(ml, float) |
| group["initial_lr"] = ml / div_factor |
| group["max_lr"] = ml |
| group["min_lr"] = group["initial_lr"] / final_div_factor |
| |
| group["lr"] = ml / final_div_factor if self.warmup_iters > 0 else group["initial_lr"] |
| if self.use_beta1: |
| group["betas"] = (self.max_momentum, *group["betas"][1:]) |
| elif self.update_momentum: |
| group["momentum"] = self.max_momentum |
|
|
| super().__init__(optimizer, last_epoch) |
|
|
| def _anneal_func(self, *args, **kwargs): |
| if self.anneal_strategy == "cos": |
| return annealing_cos(*args, **kwargs) |
| elif self.anneal_strategy == "linear": |
| return annealing_linear(*args, **kwargs) |
|
|
| def _compute_lr_momentum(self, optimizer_param_group): |
| |
| |
| step_num = (self._step_count - 1) if self.last_epoch != -1 else 0 |
| momentum = 0 |
| if step_num < self.warmup_iters: |
| if self.warmup_ratio: |
| k = (1 - step_num / self.warmup_iters) * (1 - self.warmup_ratio) |
| warmup_lr = optimizer_param_group["max_lr"] * (1 - k) |
| thelr = warmup_lr * (1 - step_num / self.total_steps) |
| else: |
| gmax = ( |
| optimizer_param_group["max_lr"] * (1 + math.cos(math.pi * step_num / float(self.total_steps))) / 2 |
| ) |
| thelr = optimizer_param_group["max_lr"] / self.final_div_factor + gmax * step_num / float( |
| self.warmup_iters |
| ) |
| else: |
| pct = (step_num - self.warmup_iters) / float(self.total_steps - self.warmup_iters) |
| step_num_to_use = step_num |
| momentum = self._anneal_func( |
| self.base_momentum, |
| self.max_momentum, |
| pct, |
| ) |
| if self.anneal_strategy == "cos": |
| step_num_to_use += 1 |
| thelr = self._anneal_func( |
| optimizer_param_group["max_lr"], |
| optimizer_param_group["min_lr"], |
| step_num_to_use / float(self.total_steps), |
| ) |
| return thelr, momentum |
|
|
| def get_lr(self): |
| """Compute the learning rate of each parameter group.""" |
| if TORCH_VERSION >= Version("2.4.0"): |
| torch_schedulers._warn_get_lr_called_within_step(self) |
|
|
| lrs = [] |
| step_num = self.last_epoch |
|
|
| if step_num > self.total_steps: |
| raise ValueError( |
| f"Tried to step {step_num} times. The specified number of total steps is {self.total_steps}" |
| ) |
|
|
| for group in self.optimizer.param_groups: |
| computed_lr, computed_momentum = self._compute_lr_momentum(group) |
| lrs.append(computed_lr) |
| if self.use_beta1: |
| group["betas"] = (computed_momentum, *group["betas"][1:]) |
| elif self.update_momentum: |
| group["momentum"] = computed_momentum |
|
|
| return lrs |
|
|
|
|
| def build_scheduler( |
| scheduler_type: str, |
| optimizer: Optimizer, |
| lr: float, |
| total_iter: int, |
| constructor_kwargs: dict[str, Any], |
| ): |
| _kwargs = {} |
| _kwargs.update(**constructor_kwargs) |
| constructor_fn = SCHEDULERS_DICT[scheduler_type] |
| accepted_kwargs = signature(constructor_fn).parameters.keys() |
| keywords = list(constructor_kwargs.keys()) |
| for key in keywords: |
| if key not in accepted_kwargs: |
| |
| _kwargs.pop(key) |
| if scheduler_type in ["OneCycleLR", "WarmupOneCycleLR", "WarmupMultiStepLR"]: |
| _kwargs.update( |
| dict( |
| max_lr=lr, |
| total_steps=total_iter, |
| ) |
| ) |
| elif scheduler_type in [ |
| "ConstantLR", |
| "LinearLR", |
| "PolynomialLR", |
| ]: |
| constructor_kwargs.update(dict(total_iters=total_iter)) |
|
|
| return constructor_fn(optimizer, **_kwargs) |
|
|
|
|
| SCHEDULERS_DICT = { |
| "ConstantLR": torch_schedulers.ConstantLR, |
| "LinearLR": torch_schedulers.LinearLR, |
| "MultiStepLR": torch_schedulers.MultiStepLR, |
| "PolynomialLR": torch_schedulers.PolynomialLR, |
| "StepLR": torch_schedulers.StepLR, |
| "OneCycleLR": torch_schedulers.OneCycleLR, |
| "WarmupOneCycleLR": WarmupOneCycleLR, |
| } |
|
|