Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions lightly/optim/__init__.py
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",
]
166 changes: 166 additions & 0 deletions lightly/optim/lars.py
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
38 changes: 38 additions & 0 deletions lightly/optim/param_groups.py
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Deduplicate shared parameters before returning groups

When a model ties a Parameter between submodules, or when overlapping modules are passed (for example, param_groups(model, model.head, ...)), get_weight_decay_parameters returns the same object once per traversal. The resulting optimizer groups therefore contain duplicates: the bundled LARS loops 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 👍 / 👎.

return [
{"name": "decay", "params": decayed, "weight_decay": weight_decay},
{"name": "no_weight_decay", "params": not_decayed, "weight_decay": 0.0},
]
Loading
Loading