humancompatible-train 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {humancompatible_train-0.3.0/src/humancompatible_train.egg-info → humancompatible_train-0.3.2}/PKG-INFO +2 -4
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/README.md +0 -2
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/pyproject.toml +2 -2
- humancompatible_train-0.3.2/src/humancompatible/train/dual_optim/__init__.py +4 -0
- humancompatible_train-0.3.2/src/humancompatible/train/dual_optim/alm.py +369 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/dual_optim/barrier.py +19 -20
- humancompatible_train-0.3.2/src/humancompatible/train/dual_optim/ialm.py +330 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/dual_optim/moreau.py +24 -14
- humancompatible_train-0.3.2/src/humancompatible/train/dual_optim/nupi.py +357 -0
- humancompatible_train-0.3.2/src/humancompatible/train/dual_optim/pbm.py +503 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/fairness/utils/balanced_batch_sampler.py +7 -5
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/optim/PBM.py +1 -1
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2/src/humancompatible_train.egg-info}/PKG-INFO +2 -4
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible_train.egg-info/SOURCES.txt +2 -1
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/tests/test_alm.py +3 -3
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/tests/test_balanced_batch_sampler.py +30 -26
- humancompatible_train-0.3.2/tests/test_pbm.py +532 -0
- humancompatible_train-0.3.0/src/humancompatible/train/dual_optim/__init__.py +0 -3
- humancompatible_train-0.3.0/src/humancompatible/train/dual_optim/alm.py +0 -204
- humancompatible_train-0.3.0/src/humancompatible/train/dual_optim/pbm.py +0 -307
- humancompatible_train-0.3.0/src/humancompatible/train/fairness/__init__.py +0 -0
- humancompatible_train-0.3.0/tests/test_pbm.py +0 -94
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/LICENCE.txt +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/setup.cfg +0 -0
- {humancompatible_train-0.3.0/src/humancompatible → humancompatible_train-0.3.2/src/humancompatible/train}/__init__.py +0 -0
- {humancompatible_train-0.3.0/src/humancompatible/train → humancompatible_train-0.3.2/src/humancompatible/train/benchmark}/__init__.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/benchmark/algorithms/Algorithm.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/benchmark/algorithms/__init__.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/benchmark/algorithms/ghost.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/benchmark/algorithms/optim_wrapper.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/benchmark/algorithms/sgd.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/benchmark/algorithms/ssl_alm.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/benchmark/algorithms/switching_subgradient.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/benchmark/algorithms/utils.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/benchmark/constraints/__init__.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/benchmark/constraints/constraint.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/benchmark/constraints/constraint_fns.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/benchmark/constraints/torch/__init__.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/benchmark/constraints/torch/constraints.py +0 -0
- {humancompatible_train-0.3.0/src/humancompatible/train/benchmark → humancompatible_train-0.3.2/src/humancompatible/train/fairness}/__init__.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/fairness/utils/__init__.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/optim/__init__.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/optim/barrier.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/optim/ssl_alm.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/optim/ssl_alm_adam.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/optim/ssl_alm_adam_moment.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/optim/ssw.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible/train/optim/ssw_barrier.py +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible_train.egg-info/dependency_links.txt +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible_train.egg-info/requires.txt +0 -0
- {humancompatible_train-0.3.0 → humancompatible_train-0.3.2}/src/humancompatible_train.egg-info/top_level.txt +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: humancompatible-train
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: PyTorch-based package for constrained training of neural networks
|
|
5
5
|
Author: Gilles Bareilles, Adam Bosak, Jana Lepsova, Jakub Marecek
|
|
6
6
|
Author-email: Andrii Kliachkin <kliacand@fel.cvut.cz>
|
|
7
7
|
Maintainer-email: Andrii Kliachkin <kliacand@fel.cvut.cz>
|
|
8
|
-
Requires-Python:
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENCE.txt
|
|
11
11
|
Requires-Dist: torch
|
|
@@ -156,8 +156,6 @@ The constraint options are:
|
|
|
156
156
|
- `equalized_odds_vec`: constraint on the Positive Rate of each group as defined by `fairret.NormLoss`;
|
|
157
157
|
- `weight_norm`: constraint on the Frobenius norm of the weights and biases of each layer of the neural network.
|
|
158
158
|
|
|
159
|
-
The above command will run the benchmark and save the logs and models in the `benchmark/results/[dataset]_[task]` directory; it will also plot the trajectories into a .png file saved in the same location.
|
|
160
|
-
|
|
161
159
|
The benchmarking code (all of which is contained in the `benchmark` directory) is easy to parse and extend with other datasets and constraints.
|
|
162
160
|
|
|
163
161
|
|
|
@@ -124,8 +124,6 @@ The constraint options are:
|
|
|
124
124
|
- `equalized_odds_vec`: constraint on the Positive Rate of each group as defined by `fairret.NormLoss`;
|
|
125
125
|
- `weight_norm`: constraint on the Frobenius norm of the weights and biases of each layer of the neural network.
|
|
126
126
|
|
|
127
|
-
The above command will run the benchmark and save the logs and models in the `benchmark/results/[dataset]_[task]` directory; it will also plot the trajectories into a .png file saved in the same location.
|
|
128
|
-
|
|
129
127
|
The benchmarking code (all of which is contained in the `benchmark` directory) is easy to parse and extend with other datasets and constraints.
|
|
130
128
|
|
|
131
129
|
|
|
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "humancompatible-train"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
dependencies = [
|
|
9
9
|
"torch",
|
|
10
10
|
"numpy",
|
|
11
11
|
]
|
|
12
|
-
requires-python = ">= 3.11
|
|
12
|
+
requires-python = ">= 3.11"
|
|
13
13
|
authors = [
|
|
14
14
|
{name = "Andrii Kliachkin", email = "kliacand@fel.cvut.cz"},
|
|
15
15
|
{name = "Gilles Bareilles"},
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from torch.nn import Parameter
|
|
3
|
+
from torch.optim import Optimizer
|
|
4
|
+
from typing import Any, Tuple
|
|
5
|
+
from torch import clamp_, Tensor
|
|
6
|
+
|
|
7
|
+
# cite: Stochastic Smoothed Primal-Dual Algorithms for Nonconvex Optimization with Linear Inequality Constraints
|
|
8
|
+
# https://arxiv.org/pdf/2504.07607
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ALM(Optimizer):
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
m: int = None,
|
|
15
|
+
lr: float = 0.01,
|
|
16
|
+
init_duals: float | Tensor = None,
|
|
17
|
+
penalty: float = 1.0,
|
|
18
|
+
*,
|
|
19
|
+
dual_range: Tuple[float, float] = (-100.0, 100.0),
|
|
20
|
+
momentum: float = 0.0,
|
|
21
|
+
dampening: float = 0.0,
|
|
22
|
+
is_ineq: bool = False,
|
|
23
|
+
ctol: float = 0.,
|
|
24
|
+
device=None,
|
|
25
|
+
) -> None:
|
|
26
|
+
|
|
27
|
+
if momentum > 0 and dampening == 0:
|
|
28
|
+
dampening = momentum
|
|
29
|
+
|
|
30
|
+
# self.dual_range = dual_range
|
|
31
|
+
# self.ctol = ctol
|
|
32
|
+
|
|
33
|
+
self.penalty = penalty
|
|
34
|
+
duals, defaults = _init_constraint_group(
|
|
35
|
+
m, lr, momentum, dampening, init_duals, dual_range, is_ineq, device
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
super().__init__(duals, defaults)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def duals(self) -> Tensor:
|
|
42
|
+
"""
|
|
43
|
+
:return: Dual variables, concatenated into a single tensor.
|
|
44
|
+
:rtype: Tensor
|
|
45
|
+
"""
|
|
46
|
+
return torch.cat([group["params"][0] for group in self.param_groups])
|
|
47
|
+
|
|
48
|
+
def add_constraint_group(
|
|
49
|
+
self,
|
|
50
|
+
m: int,
|
|
51
|
+
lr: float = None,
|
|
52
|
+
momentum: float = None,
|
|
53
|
+
dampening: float = None,
|
|
54
|
+
init_duals: Tensor = None,
|
|
55
|
+
dual_range: tuple[float, float] = None,
|
|
56
|
+
is_ineq: bool = False,
|
|
57
|
+
device = None
|
|
58
|
+
) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Allows to add a group of dual variables with separate initial values and learning rates.
|
|
61
|
+
|
|
62
|
+
:param m: Size of group (number of dual variables to add)
|
|
63
|
+
:type m: int
|
|
64
|
+
:param lr: Dual variable update rate.
|
|
65
|
+
:type lr: float
|
|
66
|
+
:param momentum: Momentum/Smoothing factor for dual variables. Equivalent to SGD momentum. Set to `0` to disable.
|
|
67
|
+
:type momentum: float
|
|
68
|
+
:param dampening: Dampening for momentum. Equivalent to SGD dampening. Set to `0` to disable.
|
|
69
|
+
:type dampening: float
|
|
70
|
+
:param init_duals: Initial values for the new dual variables. Defaults to the value set when creating the optimizer.
|
|
71
|
+
:type init_duals: Tensor
|
|
72
|
+
:param dual_range: After each dual update, the dual variables will be clamped to this range.
|
|
73
|
+
:type dual_range: Tuple[float, float]
|
|
74
|
+
:param is_ineq: Whether to treat the constraints as equality or inequality. If`True`, dual variables will be relaxed on strict satisfaction and lower-bounded by `max(dual_range[0], 0)`.
|
|
75
|
+
:type is_ineq: bool
|
|
76
|
+
|
|
77
|
+
.. note::
|
|
78
|
+
Parameters here will default to values set when initializing the dual optimizer.
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
duals, settings_dict = _init_constraint_group(
|
|
82
|
+
m, lr, momentum, dampening, init_duals, dual_range, is_ineq, device
|
|
83
|
+
)
|
|
84
|
+
param_group_dict = {"params": duals, **settings_dict}
|
|
85
|
+
self.add_param_group(param_group_dict)
|
|
86
|
+
|
|
87
|
+
def _add_penalty_term(self, lagrangian: Tensor, constraints: Tensor) -> None:
|
|
88
|
+
"""Add penalty term to lagrangian in-place."""
|
|
89
|
+
if self.penalty == 0:
|
|
90
|
+
return
|
|
91
|
+
elif constraints.ndim > 0:
|
|
92
|
+
lagrangian.add_(
|
|
93
|
+
0.5
|
|
94
|
+
* self.penalty
|
|
95
|
+
* torch.dot(constraints, constraints)
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
lagrangian.add_(
|
|
99
|
+
0.5
|
|
100
|
+
* self.penalty
|
|
101
|
+
* torch.square(constraints)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def forward(self, loss: Tensor, constraints: Tensor) -> Tensor:
|
|
106
|
+
"""
|
|
107
|
+
Calculates and returns the Augmented Lagrangian.
|
|
108
|
+
|
|
109
|
+
Computes the augmented Lagrangian::
|
|
110
|
+
|
|
111
|
+
L = loss + sum(duals_i @ constraints_i for all groups) + 0.5 * penalty * ||constraints||^2
|
|
112
|
+
|
|
113
|
+
where `loss` is the objective value, `duals_i` are the dual variables, `constraints_i` are constraint values,
|
|
114
|
+
`penalty` is the penalty parameter, and the sum is over all constraint groups.
|
|
115
|
+
|
|
116
|
+
:param loss: Loss (objective function) value
|
|
117
|
+
:type loss: Tensor
|
|
118
|
+
:param constraints: Tensor of constraint values
|
|
119
|
+
:type constraints: Tensor
|
|
120
|
+
:return: Lagrangian
|
|
121
|
+
:rtype: Tensor
|
|
122
|
+
"""
|
|
123
|
+
lagrangian = torch.zeros_like(loss)
|
|
124
|
+
lagrangian.add_(loss)
|
|
125
|
+
|
|
126
|
+
for i in range(len(self.param_groups)):
|
|
127
|
+
duals, group_constraints = _process_constraint_group(
|
|
128
|
+
self.param_groups[i], i, constraints, update_duals=False
|
|
129
|
+
)
|
|
130
|
+
lagrangian.add_(duals @ group_constraints)
|
|
131
|
+
|
|
132
|
+
self._add_penalty_term(lagrangian, constraints)
|
|
133
|
+
return lagrangian
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def update(self, constraints: Tensor) -> None:
|
|
137
|
+
"""
|
|
138
|
+
Updates the dual variables using constrained gradient ascent with optional momentum.
|
|
139
|
+
|
|
140
|
+
For each constraint group, performs the dual variable update.
|
|
141
|
+
|
|
142
|
+
First, update the momentum buffer (if momentum > 0)::
|
|
143
|
+
|
|
144
|
+
if momentum > 0:
|
|
145
|
+
buffer_i = momentum * buffer_i + (1 - dampening) * constraints_i
|
|
146
|
+
else:
|
|
147
|
+
buffer_i = constraints_i
|
|
148
|
+
|
|
149
|
+
Then, update the dual variables with clamping::
|
|
150
|
+
|
|
151
|
+
duals_i = clamp(duals_i + lr * buffer_i, lower_bound, upper_bound)
|
|
152
|
+
|
|
153
|
+
where `buffer_i` is the momentum buffer, `constraints_i` are constraint values, `duals_i` are dual variables,
|
|
154
|
+
and `clamp(x, lb, ub)` projects to the dual range.
|
|
155
|
+
|
|
156
|
+
:param constraints: Tensor of constraint values
|
|
157
|
+
:type constraints: Tensor
|
|
158
|
+
"""
|
|
159
|
+
for i in range(len(self.param_groups)):
|
|
160
|
+
_process_constraint_group(
|
|
161
|
+
self.param_groups[i], i, constraints, update_duals=True
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# evaluate the Lagrangian and update the dual variables
|
|
165
|
+
def forward_update(self, loss: Tensor, constraints: Tensor) -> Tensor:
|
|
166
|
+
"""
|
|
167
|
+
Combines `forward` and `update`; slightly faster than calling both separately.
|
|
168
|
+
|
|
169
|
+
Updates dual variables::
|
|
170
|
+
|
|
171
|
+
duals_i = clamp(duals_i + lr * buffer_i, lower_bound, upper_bound)
|
|
172
|
+
|
|
173
|
+
Then computes the augmented Lagrangian::
|
|
174
|
+
|
|
175
|
+
L = loss + sum(duals_i @ constraints_i for all groups) + 0.5 * penalty * ||constraints||^2
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
where the momentum buffer is updated as in :meth:`update`.
|
|
179
|
+
|
|
180
|
+
:param loss: Loss (objective function) value
|
|
181
|
+
:type loss: Tensor
|
|
182
|
+
:param constraints: Tensor of constraint values
|
|
183
|
+
:type constraints: Tensor
|
|
184
|
+
:return: Lagrangian
|
|
185
|
+
:rtype: Tensor
|
|
186
|
+
"""
|
|
187
|
+
lagrangian = torch.zeros_like(loss)
|
|
188
|
+
lagrangian.add_(loss)
|
|
189
|
+
|
|
190
|
+
for i in range(len(self.param_groups)):
|
|
191
|
+
duals, group_constraints = _process_constraint_group(
|
|
192
|
+
self.param_groups[i], i, constraints, update_duals=True
|
|
193
|
+
)
|
|
194
|
+
lagrangian.add_(duals @ group_constraints)
|
|
195
|
+
|
|
196
|
+
self._add_penalty_term(lagrangian, constraints)
|
|
197
|
+
return lagrangian
|
|
198
|
+
|
|
199
|
+
def state_dict(self) -> dict[str, Any]:
|
|
200
|
+
""""""
|
|
201
|
+
state_dict = super().state_dict()
|
|
202
|
+
state_dict["state"]["penalty"] = self.penalty
|
|
203
|
+
# save params themselves in state_dict instead of param ID in default PyTorch
|
|
204
|
+
for id_pg, pg in enumerate(state_dict["param_groups"]):
|
|
205
|
+
pg["params"] = [
|
|
206
|
+
self.param_groups[id_pg]["params"][param_id]
|
|
207
|
+
for param_id in pg["params"]
|
|
208
|
+
]
|
|
209
|
+
return state_dict
|
|
210
|
+
|
|
211
|
+
def load_state_dict(self, state_dict: dict[str, Any]) -> None:
|
|
212
|
+
""""""
|
|
213
|
+
self.penalty = state_dict["state"]["penalty"]
|
|
214
|
+
# self.dual_range = state_dict["state"]["dual_range"]
|
|
215
|
+
params = state_dict["param_groups"]
|
|
216
|
+
self.param_groups = []
|
|
217
|
+
for param in params:
|
|
218
|
+
self.param_groups.append(param)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _process_constraint_group(
|
|
222
|
+
group: dict[str, Any],
|
|
223
|
+
group_idx: int,
|
|
224
|
+
constraints: Tensor,
|
|
225
|
+
update_duals: bool = False,
|
|
226
|
+
) -> Tuple[Tensor, Tensor]:
|
|
227
|
+
"""
|
|
228
|
+
Process a single constraint group: extract duals/constraints and optionally update duals.
|
|
229
|
+
|
|
230
|
+
:param group: The constraint group dictionary
|
|
231
|
+
:param group_idx: Index of the constraint group
|
|
232
|
+
:param constraints: Full constraints tensor
|
|
233
|
+
:param update_duals: Whether to update dual variables
|
|
234
|
+
:return: Tuple of (duals, group_constraints)
|
|
235
|
+
"""
|
|
236
|
+
duals = group["params"][0]
|
|
237
|
+
if constraints.ndim > 0:
|
|
238
|
+
group_constraints = (
|
|
239
|
+
constraints[group_idx * len(duals) : (group_idx + 1) * len(duals)]
|
|
240
|
+
)
|
|
241
|
+
else:
|
|
242
|
+
group_constraints = constraints.unsqueeze(0)
|
|
243
|
+
|
|
244
|
+
lr = group.get("lr")
|
|
245
|
+
momentum = group.get("momentum", 0.0)
|
|
246
|
+
dampening = group.get("dampening", 0.0)
|
|
247
|
+
momentum_buffer = group["momentum_buffer"]
|
|
248
|
+
dual_lb = group.get("lower_bound")
|
|
249
|
+
dual_ub = group.get("upper_bound")
|
|
250
|
+
is_ineq = group.get("is_ineq")
|
|
251
|
+
|
|
252
|
+
with torch.no_grad():
|
|
253
|
+
if momentum > 0:
|
|
254
|
+
_update_c_buffers(group_constraints, momentum, dampening, momentum_buffer)
|
|
255
|
+
if update_duals:
|
|
256
|
+
_update_duals(duals, momentum_buffer if momentum > 0 else group_constraints, lr)
|
|
257
|
+
clamp_(duals, min=dual_lb, max=dual_ub)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
return duals, group_constraints
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _init_constraint_group(
|
|
264
|
+
m: int = None,
|
|
265
|
+
lr: float = None,
|
|
266
|
+
momentum: float = None,
|
|
267
|
+
dampening: float = None,
|
|
268
|
+
init_duals: float | Tensor = None,
|
|
269
|
+
dual_range: Tuple[float, float] = None,
|
|
270
|
+
is_ineq: bool = None,
|
|
271
|
+
device = None,
|
|
272
|
+
):
|
|
273
|
+
## checks ##
|
|
274
|
+
if init_duals is None and m is None:
|
|
275
|
+
raise ValueError("At least one of m, init_duals must be set")
|
|
276
|
+
|
|
277
|
+
if momentum is not None and (momentum < 0 or momentum > 1):
|
|
278
|
+
raise ValueError(f"momentum must be within [0,1]; got {momentum}")
|
|
279
|
+
|
|
280
|
+
if not isinstance(is_ineq, bool):
|
|
281
|
+
raise ValueError(f"Expected a Boolean value for is_ineq, got {is_ineq}")
|
|
282
|
+
|
|
283
|
+
m = m if m is not None else len(init_duals)
|
|
284
|
+
|
|
285
|
+
if init_duals is None: # initialize duals if not set or set to scalar
|
|
286
|
+
init_duals = torch.zeros(m, requires_grad=False, device=device)
|
|
287
|
+
elif isinstance(init_duals, float):
|
|
288
|
+
init_duals = torch.zeros(m, requires_grad=False, device=device) + init_duals
|
|
289
|
+
|
|
290
|
+
duals = Parameter(init_duals, requires_grad=False)
|
|
291
|
+
|
|
292
|
+
if dual_range is None and not is_ineq:
|
|
293
|
+
dual_range = (None, None)
|
|
294
|
+
elif dual_range is None and is_ineq:
|
|
295
|
+
dual_range = (0, None)
|
|
296
|
+
|
|
297
|
+
settings_dict = {
|
|
298
|
+
"lr": lr,
|
|
299
|
+
"momentum": momentum,
|
|
300
|
+
"dampening": dampening,
|
|
301
|
+
"momentum_buffer": torch.zeros_like(
|
|
302
|
+
init_duals, requires_grad=False, device=device
|
|
303
|
+
),
|
|
304
|
+
"lower_bound": max(dual_range[0], 0) if is_ineq else dual_range[0],
|
|
305
|
+
"upper_bound": dual_range[1],
|
|
306
|
+
"is_ineq": is_ineq
|
|
307
|
+
}
|
|
308
|
+
settings_dict = {k: v for k, v in settings_dict.items() if v is not None}
|
|
309
|
+
|
|
310
|
+
param_group = ([duals], settings_dict)
|
|
311
|
+
return param_group
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _update_c_buffers(
|
|
315
|
+
constraints: Tensor,
|
|
316
|
+
momentum: float,
|
|
317
|
+
dampening: float,
|
|
318
|
+
buffer: Tensor,
|
|
319
|
+
) -> None:
|
|
320
|
+
"""Update the constraint buffer with momentum."""
|
|
321
|
+
if momentum == 0:
|
|
322
|
+
buffer = constraints
|
|
323
|
+
else:
|
|
324
|
+
buffer.mul_(momentum).add_(constraints, alpha=1 - dampening)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _update_duals(
|
|
328
|
+
duals: Tensor,
|
|
329
|
+
buffer: Tensor,
|
|
330
|
+
lr: float,
|
|
331
|
+
) -> None:
|
|
332
|
+
"""Update duals using the buffered constraint gradients."""
|
|
333
|
+
duals.add_(buffer, alpha=lr)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
ALM.__doc__ = (
|
|
338
|
+
|
|
339
|
+
# \textbf{input}: \gamma \text{ (lr) }, \pmb{\lambda}_t \text{ (dual variables, created by method) }, \\
|
|
340
|
+
# \mathbf{c}(\theta) \text{ (constraints) }, f(\theta) \text{ (objective) }, \rho \text{ (penalty coefficient) } \\
|
|
341
|
+
r"""
|
|
342
|
+
A Dual Optimizer that works on the dual maximization tasks according to the Augmented Lagrangian rule. Creates and updates dual variables. Reference: https://doi.org/10.48550/arXiv.2504.07607
|
|
343
|
+
|
|
344
|
+
.. math::
|
|
345
|
+
|
|
346
|
+
\pmb{\lambda}_{t+1} & \leftarrow \pmb{\lambda}_t + \gamma \mathbf{c}_t(\theta_{t})
|
|
347
|
+
|
|
348
|
+
\mathcal{L}_{t+1} & \leftarrow f_t(\theta_{t}) + \pmb{\lambda}_{t+1}^T \mathbf{c}_t(\theta_{t}) + \frac{\rho}{2} \| \mathbf{c}_t(\theta_{t}) \|^2_2
|
|
349
|
+
|
|
350
|
+
:param m: Number of constraints (determines the number of dual variables to create)
|
|
351
|
+
:type m: int
|
|
352
|
+
:param lr: Dual variable update rate.
|
|
353
|
+
:type lr: float
|
|
354
|
+
:param init_duals: Initial values for the new dual variables. Defaults to 0 for all.
|
|
355
|
+
:type init_duals: float | Tensor
|
|
356
|
+
:param penalty: Augmented Lagrangian penalty parameter. Defaults to`1.`
|
|
357
|
+
:type penalty: float
|
|
358
|
+
:param dual_range: Safeguarding range for dual variables; they will be`clamp`-ed to this range.
|
|
359
|
+
:type dual_range: Tuple[float, float]
|
|
360
|
+
:param momentum: Momentum/Smoothing factor for dual variables. Equivalent to SGD momentum. Set to `0` to disable.
|
|
361
|
+
:type momentum: float
|
|
362
|
+
:param dampening: Dampening for momentum. Equivalent to SGD dampening. Set to `0` to disable.
|
|
363
|
+
:type dampening: float
|
|
364
|
+
:param is_ineq: Whether to treat the constraints as equality or inequality. If`True`, dual variables will be decreased on strict satisfaction and lower-bounded by `max(dual_range[0], 0)`.
|
|
365
|
+
:type is_ineq: bool
|
|
366
|
+
:param ctol: Constraint tolerance; allows tiny violations of constraints to account for noise.
|
|
367
|
+
:type ctol: float
|
|
368
|
+
"""
|
|
369
|
+
)
|
|
@@ -8,23 +8,22 @@ https://www.researchgate.net/publication/2775406_PenaltyBarrier_Multiplier_Metho
|
|
|
8
8
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
|
|
11
12
|
def exponential_penalty(t):
|
|
12
13
|
|
|
13
14
|
return torch.exp(t) - 1.0
|
|
14
15
|
|
|
16
|
+
|
|
15
17
|
def modified_log_barrier(t):
|
|
16
18
|
|
|
17
|
-
return -torch.log(1-t)
|
|
19
|
+
return -torch.log(1 - t)
|
|
20
|
+
|
|
18
21
|
|
|
19
22
|
def augmented_lagrangian(t):
|
|
20
23
|
"""
|
|
21
24
|
Vectorized version of augmented_lagrangian
|
|
22
25
|
"""
|
|
23
|
-
return torch.where(
|
|
24
|
-
t >= -1,
|
|
25
|
-
t + 0.5 * torch.square(t),
|
|
26
|
-
-0.5 * torch.ones_like(t)
|
|
27
|
-
)
|
|
26
|
+
return torch.where(t >= -1, t + 0.5 * torch.square(t), -0.5 * torch.ones_like(t))
|
|
28
27
|
|
|
29
28
|
|
|
30
29
|
def quad_log(t):
|
|
@@ -36,7 +35,7 @@ def quad_log(t):
|
|
|
36
35
|
|
|
37
36
|
mask = t >= -0.5
|
|
38
37
|
out[mask] = t[mask] + 0.5 * torch.pow(t[mask], 2)
|
|
39
|
-
out[~mask] = -0.25 * torch.log(-2 * t[~mask]) - 3/8
|
|
38
|
+
out[~mask] = -0.25 * torch.log(-2 * t[~mask]) - 3 / 8
|
|
40
39
|
|
|
41
40
|
return out
|
|
42
41
|
|
|
@@ -48,27 +47,26 @@ def quad_recipr(t):
|
|
|
48
47
|
|
|
49
48
|
out = torch.empty_like(t)
|
|
50
49
|
|
|
51
|
-
mask = t >= -1/3
|
|
50
|
+
mask = t >= -1 / 3
|
|
52
51
|
out[mask] = t[mask] + 0.5 * torch.pow(t[mask], 2)
|
|
53
|
-
out[~mask] = (32/27) * (1 / (1 - t[~mask])) - 7/6
|
|
52
|
+
out[~mask] = (32 / 27) * (1 / (1 - t[~mask])) - 7 / 6
|
|
53
|
+
|
|
54
|
+
return out
|
|
54
55
|
|
|
55
|
-
return out
|
|
56
56
|
|
|
57
57
|
def exponential_penalty_derivative(t):
|
|
58
58
|
|
|
59
|
-
return torch.exp(t)
|
|
59
|
+
return torch.exp(t)
|
|
60
|
+
|
|
60
61
|
|
|
61
62
|
def modified_log_barrier_derivative(t):
|
|
62
63
|
|
|
63
|
-
return 1 / (1-t)
|
|
64
|
+
return 1 / (1 - t)
|
|
65
|
+
|
|
64
66
|
|
|
65
67
|
def aug_lagr_der(t):
|
|
66
68
|
|
|
67
|
-
return torch.where(
|
|
68
|
-
t >= -1,
|
|
69
|
-
1 + t,
|
|
70
|
-
torch.zeros_like(t)
|
|
71
|
-
)
|
|
69
|
+
return torch.where(t >= -1, 1 + t, torch.zeros_like(t))
|
|
72
70
|
|
|
73
71
|
|
|
74
72
|
def quad_log_der(t):
|
|
@@ -81,12 +79,13 @@ def quad_log_der(t):
|
|
|
81
79
|
|
|
82
80
|
return out
|
|
83
81
|
|
|
82
|
+
|
|
84
83
|
def quad_recipr_der(t):
|
|
85
84
|
|
|
86
85
|
out = torch.empty_like(t)
|
|
87
86
|
|
|
88
|
-
mask = t >= -1/3
|
|
87
|
+
mask = t >= -1 / 3
|
|
89
88
|
out[mask] = 1 + t[mask]
|
|
90
|
-
out[~mask] = (32/27) * (1 / torch.square(1 - t[~mask]))
|
|
89
|
+
out[~mask] = (32 / 27) * (1 / torch.square(1 - t[~mask]))
|
|
91
90
|
|
|
92
|
-
return out
|
|
91
|
+
return out
|