-
Notifications
You must be signed in to change notification settings - Fork 360
(prototype) Add lightly.optim with LARS, the schedulers and param_groups #2036
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| """Optimisers, learning-rate schedules and parameter groups.""" | ||
|
|
||
| from lightly.optim.lars import LARS | ||
| from lightly.optim.param_groups import param_groups | ||
| from lightly.optim.schedulers import ( | ||
| CosineWarmupScheduler, | ||
| cosine_schedule, | ||
| cosine_warmup_schedule, | ||
| linear_warmup_schedule, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "CosineWarmupScheduler", | ||
| "LARS", | ||
| "cosine_schedule", | ||
| "cosine_warmup_schedule", | ||
| "linear_warmup_schedule", | ||
| "param_groups", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| from typing import Any, Callable, Dict, Optional, overload | ||
|
|
||
| import torch | ||
| from torch.optim.optimizer import Optimizer | ||
|
|
||
|
|
||
| class LARS(Optimizer): | ||
| """Extends SGD in PyTorch with LARS scaling from the paper "Large batch training of | ||
| Convolutional Networks" [0]. | ||
|
|
||
| Implementation from PyTorch Lightning Bolts [1]. | ||
|
|
||
| - [0]: https://arxiv.org/pdf/1708.03888.pdf | ||
| - [1]: https://github.com/Lightning-Universe/lightning-bolts/blob/2dfe45a4cf050f120d10981c45cfa2c785a1d5e6/pl_bolts/optimizers/lars.py#L1 | ||
|
|
||
| Args: | ||
| params: | ||
| Iterable of parameters to optimize or dicts defining parameter groups. | ||
| lr: | ||
| Learning rate | ||
| momentum: | ||
| Momentum factor. | ||
| weight_decay: | ||
| Weight decay (L2 penalty). | ||
| dampening: | ||
| Dampening for momentum. | ||
| nesterov: | ||
| Enables Nesterov momentum. | ||
| trust_coefficient: | ||
| Trust coefficient for computing learning rate. | ||
| eps: | ||
| Eps for division denominator. | ||
|
|
||
| Example: | ||
| >>> model = torch.nn.Linear(10, 1) | ||
| >>> input = torch.Tensor(10) | ||
| >>> target = torch.Tensor([1.]) | ||
| >>> loss_fn = lambda input, target: (input - target) ** 2 | ||
| >>> optimizer = LARS(model.parameters(), lr=0.1, momentum=0.9) | ||
| >>> optimizer.zero_grad() | ||
| >>> loss_fn(model(input), target).backward() | ||
| >>> optimizer.step() | ||
|
|
||
| .. note:: | ||
| The application of momentum in the SGD part is modified according to | ||
| the PyTorch standards. LARS scaling fits into the equation in the | ||
| following fashion. | ||
|
|
||
| .. math:: | ||
| \begin{aligned} | ||
| g_{t+1} & = \text{lars_lr} * (\beta * p_{t} + g_{t+1}), \\ | ||
| v_{t+1} & = \\mu * v_{t} + g_{t+1}, \\ | ||
| p_{t+1} & = p_{t} - \text{lr} * v_{t+1}, | ||
| \\end{aligned} | ||
|
|
||
| where :math:`p`, :math:`g`, :math:`v`, :math:`\\mu` and :math:`\beta` denote the | ||
| parameters, gradient, velocity, momentum, and weight decay respectively. | ||
| The :math:`lars_lr` is defined by Eq. 6 in the paper. | ||
| The Nesterov version is analogously modified. | ||
|
|
||
| .. warning:: | ||
| Parameters with weight decay set to 0 will automatically be excluded from | ||
| layer-wise LR scaling. This is to ensure consistency with papers like SimCLR | ||
| and BYOL. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| params: Any, | ||
| lr: float, | ||
| momentum: float = 0, | ||
| dampening: float = 0, | ||
| weight_decay: float = 0, | ||
| nesterov: bool = False, | ||
| trust_coefficient: float = 0.001, | ||
| eps: float = 1e-8, | ||
| ): | ||
| if lr < 0.0: | ||
| raise ValueError(f"Invalid learning rate: {lr}") | ||
| if momentum < 0.0: | ||
| raise ValueError(f"Invalid momentum value: {momentum}") | ||
| if weight_decay < 0.0: | ||
| raise ValueError(f"Invalid weight_decay value: {weight_decay}") | ||
|
|
||
| defaults = dict( | ||
| lr=lr, | ||
| momentum=momentum, | ||
| dampening=dampening, | ||
| weight_decay=weight_decay, | ||
| nesterov=nesterov, | ||
| trust_coefficient=trust_coefficient, | ||
| eps=eps, | ||
| ) | ||
| if nesterov and (momentum <= 0 or dampening != 0): | ||
| raise ValueError("Nesterov momentum requires a momentum and zero dampening") | ||
|
|
||
| super().__init__(params, defaults) | ||
|
|
||
| def __setstate__(self, state: Dict[str, Any]) -> None: | ||
| super().__setstate__(state) | ||
| for group in self.param_groups: | ||
| group.setdefault("nesterov", False) | ||
|
|
||
| # Type ignore for overloads is required for Python 3.7. | ||
| @overload # type: ignore[override] | ||
| def step(self, closure: None = None) -> None: ... | ||
|
|
||
| @overload # type: ignore[override] | ||
| def step(self, closure: Callable[[], float]) -> float: ... | ||
|
|
||
| @torch.no_grad() | ||
| def step(self, closure: Optional[Callable[[], float]] = None) -> Optional[float]: | ||
| """Performs a single optimization step. | ||
|
|
||
| Args: | ||
| closure (callable, optional): A closure that reevaluates the model | ||
| and returns the loss. | ||
| """ | ||
| loss = None | ||
| if closure is not None: | ||
| with torch.enable_grad(): | ||
| loss = closure() | ||
|
|
||
| # Exclude scaling for params with 0 weight decay. | ||
| for group in self.param_groups: | ||
| weight_decay = group["weight_decay"] | ||
| momentum = group["momentum"] | ||
| dampening = group["dampening"] | ||
| nesterov = group["nesterov"] | ||
|
|
||
| for p in group["params"]: | ||
| if p.grad is None: | ||
| continue | ||
|
|
||
| d_p = p.grad | ||
| p_norm = torch.norm(p.data) | ||
| g_norm = torch.norm(p.grad.data) | ||
|
|
||
| # Apply Lars scaling and weight decay. | ||
| if weight_decay != 0: | ||
| if p_norm != 0 and g_norm != 0: | ||
| lars_lr = p_norm / ( | ||
| g_norm + p_norm * weight_decay + group["eps"] | ||
| ) | ||
| lars_lr *= group["trust_coefficient"] | ||
|
|
||
| d_p = d_p.add(p, alpha=weight_decay) | ||
| d_p *= lars_lr | ||
|
|
||
| # Apply momentum. | ||
| if momentum != 0: | ||
| param_state = self.state[p] | ||
| if "momentum_buffer" not in param_state: | ||
| buf = param_state["momentum_buffer"] = torch.clone(d_p).detach() | ||
| else: | ||
| buf = param_state["momentum_buffer"] | ||
| buf.mul_(momentum).add_(d_p, alpha=1 - dampening) | ||
|
|
||
| if nesterov: | ||
| d_p = d_p.add(buf, alpha=momentum) | ||
| else: | ||
| d_p = buf | ||
|
|
||
| p.add_(d_p, alpha=-group["lr"]) | ||
|
|
||
| return loss |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| """Optimiser parameter groups.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Any, Dict, List | ||
|
|
||
| from torch.nn import Module, Parameter | ||
|
|
||
| from lightly.models.utils import get_weight_decay_parameters | ||
|
|
||
| __all__ = ["param_groups"] | ||
|
|
||
|
|
||
| def param_groups(*modules: Module, weight_decay: float) -> List[Dict[str, Any]]: | ||
| """Splits parameters into a decayed group and a group that is not decayed. | ||
|
|
||
| Normalization parameters and biases are the ones left out, which is what | ||
| every SSL reference implementation does and what the published numbers were | ||
| produced with. | ||
|
|
||
| Example: | ||
| >>> optimizer = LARS(param_groups(backbone, head, weight_decay=1e-6), lr=4.8) | ||
|
|
||
| Args: | ||
| *modules: The modules whose parameters to group. | ||
| weight_decay: The decay applied to everything except norms and biases. | ||
|
|
||
| Returns: | ||
| Two groups, ready to hand to an optimiser. Both carry an explicit | ||
| ``weight_decay``, so the optimiser's own default never applies. | ||
| """ | ||
| decayed: List[Parameter] | ||
| not_decayed: List[Parameter] | ||
| decayed, not_decayed = get_weight_decay_parameters(modules) | ||
| return [ | ||
| {"name": "decay", "params": decayed, "weight_decay": weight_decay}, | ||
| {"name": "no_weight_decay", "params": not_decayed, "weight_decay": 0.0}, | ||
| ] | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a model ties a
Parameterbetween submodules, or when overlapping modules are passed (for example,param_groups(model, model.head, ...)),get_weight_decay_parametersreturns the same object once per traversal. The resulting optimizer groups therefore contain duplicates: the bundledLARSloops over every entry and updates that parameter multiple times per step, while a parameter classified into different groups can make PyTorch reject the optimizer entirely. Deduplicate parameters by identity before returning the groups.Useful? React with 👍 / 👎.