|
1 | 1 | # Scalarization |
2 | 2 |
|
3 | | -A `Scalarizer` reduces a tensor of values (typically a vector of per-task or per-instance losses) |
4 | | -into a single scalar that can be optimized with a standard `loss.backward()`. Scalarizers are the |
5 | | -simple baseline against which the Jacobian-descent [aggregators](../aggregation) are compared: |
6 | | -instead of combining the per-loss gradients, a scalarizer combines the losses directly. |
| 3 | +This package implements the `Scalarizer`s: objects that reduce a tensor of values (typically a |
| 4 | +vector of losses) into a single scalar optimizable with a standard `loss.backward()`. |
7 | 5 |
|
8 | | -Full documentation for every scalarizer is at |
9 | | -[torchjd.org](https://torchjd.org/latest/docs/scalarization/). |
| 6 | +This file is for contributors working on scalarizers. For the list of available scalarizers and their |
| 7 | +full API, see [torchjd.org](https://torchjd.org/latest/docs/scalarization/). |
10 | 8 |
|
11 | | -## Usage |
| 9 | +## The contract |
| 10 | + |
| 11 | +A scalarizer subclasses `Scalarizer` (in [`_scalarizer_base.py`](_scalarizer_base.py)) and implements |
| 12 | +one method: |
12 | 13 |
|
13 | 14 | ```python |
14 | | -import torch |
15 | | -from torch.nn import Linear |
16 | | -from torchjd.scalarization import Mean |
| 15 | +def forward(self, values: Tensor, /) -> Tensor: |
| 16 | + ... |
| 17 | +``` |
17 | 18 |
|
18 | | -model = Linear(3, 2) |
19 | | -scalarizer = Mean() |
| 19 | +- **Any shape in, scalar out:** it reduces over *all* elements of `values` (0-dim, vector, matrix, |
| 20 | + higher-dim) into a 0-dim scalar. |
| 21 | +- **`values`, not `losses`:** a scalarizer is generic and not tied to losses. |
| 22 | +- **Pure and differentiable:** the output depends only on `values` and the configured parameters, so |
| 23 | + that `scalarizer(values).backward()` produces the gradient. |
20 | 24 |
|
21 | | -features = torch.randn(8, 3) |
22 | | -losses = model(features).pow(2).mean(dim=0) # one loss per output dimension |
23 | | -loss = scalarizer(losses) |
24 | | -loss.backward() # gradients flow to the model parameters |
25 | | -``` |
| 25 | +## Adding one |
| 26 | + |
| 27 | +A new scalarizer is a class plus the files that register it. Mirror an existing scalarizer of the |
| 28 | +same kind: |
| 29 | + |
| 30 | +- `_<name>.py` — the class. |
| 31 | +- `__init__.py` — the import and an `__all__` entry. |
| 32 | +- `docs/source/docs/scalarization/<name>.rst` — the docs page, added to the `index.rst` toctree. |
| 33 | +- `tests/unit/scalarization/test_<name>.py` — the tests. |
| 34 | +- `CHANGELOG.md` — an entry under `[Unreleased]`. |
| 35 | + |
| 36 | +## State |
| 37 | + |
| 38 | +Most scalarizers are stateless. Keep yours stateless unless the method genuinely needs state (learned |
| 39 | +weights, a loss history). When it does: |
| 40 | + |
| 41 | +- **Subclass `Stateful`** (`from torchjd._mixins import Stateful`) and implement `reset()` to restore |
| 42 | + the initial state. |
| 43 | +- **Keep `forward` self-contained.** Do not hide cross-call state or side effects inside it. When the |
| 44 | + method must carry information between calls, expose it through an explicit, named method and |
| 45 | + document the protocol (e.g. a per-epoch `step()`, or an `update()` after the optimizer step). |
| 46 | +- **`nn.Parameter` vs buffer:** trainable state is an `nn.Parameter`; non-trained tensors that must |
| 47 | + move with `.to()` are registered with `register_buffer`. |
| 48 | + |
| 49 | +## What is not a scalarizer |
| 50 | + |
| 51 | +A scalarizer only ever sees the loss values. |
| 52 | + |
| 53 | +Anything that needs the model, its parameters, or the per-task gradients belongs in the |
| 54 | +[aggregation](../aggregation) package as a `Weighting` / `Aggregator`, which operates on the Jacobian |
| 55 | +or its Gramian. If you reach for gradient norms or the network inside `forward`, you are writing an |
| 56 | +aggregator, not a scalarizer. |
| 57 | + |
| 58 | +## Things to be careful about |
26 | 59 |
|
27 | | -## Available scalarizers |
28 | | - |
29 | | -- **Constant**: combines the values with constant, pre-determined weights. |
30 | | -- **COSMOS**: linear scalarization minus a cosine-similarity penalty toward a preference direction. |
31 | | -- **DWA**: weights each value by the relative rate at which its loss decreased over the two previous |
32 | | - epochs. |
33 | | -- **FAMO**: decreases all task losses at an approximately equal rate, learning the task weights |
34 | | - internally. |
35 | | -- **GeometricMean**: geometric mean of the values (also known as GLS). |
36 | | -- **IMTLL**: learns a per-task scale and combines the values as the sum of `exp(s_i) * L_i - s_i`. |
37 | | -- **Mean**: mean of the values. |
38 | | -- **PBI**: decomposes the values along a preference direction and penalizes the perpendicular |
39 | | - distance. |
40 | | -- **Random**: combines the values with positive random weights summing to one. |
41 | | -- **STCH**: smooth approximation of the weighted, shifted maximum of the values. |
42 | | -- **Sum**: sum of the values. |
43 | | -- **UW**: weights the values using learned per-task uncertainties. |
44 | | - |
45 | | -`UW`, `IMTLL`, and `FAMO` are trainable, and `DWA` and `FAMO` carry state between calls, so they |
46 | | -need a little more than a single call (an optimizer, a per-epoch `step()`, or an `update()`). See |
47 | | -the documentation for the exact usage. |
| 60 | +- **Determinism and side effects:** the output should depend only on `values` and the configured |
| 61 | + parameters. Any state change must be deliberate, explicit, and undone by `reset()`. |
| 62 | +- **Numerical stability:** keep the reduction finite on the edges of its domain (log-sum-exp |
| 63 | + centering, an eps under a norm or in a denominator, etc.), and explain any value shift in a comment |
| 64 | + and a `.. note::`. |
| 65 | +- **Hyperparameters:** when a coefficient has no single good value across problems, make it required |
| 66 | + rather than guessing a default, and validate it in `__init__`. |
| 67 | +- **Shape validation:** check parameter shapes against `values` at call time and raise `ValueError`. |
| 68 | +- **Preconditions:** if the method is undefined on some inputs, document it in a `.. note::` and lock |
| 69 | + it with a test (e.g. assert `nan` propagates rather than being silently clamped). |
0 commit comments