invexapi 0.1.0__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.
- invexapi-0.1.0/.gitignore +20 -0
- invexapi-0.1.0/LICENSE +28 -0
- invexapi-0.1.0/PKG-INFO +178 -0
- invexapi-0.1.0/README.md +152 -0
- invexapi-0.1.0/environment.yml +15 -0
- invexapi-0.1.0/examples/_data.py +32 -0
- invexapi-0.1.0/examples/deblurring_log_fista.py +90 -0
- invexapi-0.1.0/examples/denoising_admm_tv.py +87 -0
- invexapi-0.1.0/examples/denoising_admm_tv_log.py +82 -0
- invexapi-0.1.0/examples/denoising_quasinorm.py +67 -0
- invexapi-0.1.0/imgs/logo.png +0 -0
- invexapi-0.1.0/invexapi/__init__.py +45 -0
- invexapi-0.1.0/invexapi/metadata.py +139 -0
- invexapi-0.1.0/invexapi/optim/__init__.py +7 -0
- invexapi-0.1.0/invexapi/optim/_certification.py +28 -0
- invexapi-0.1.0/invexapi/optim/_linesearch.py +28 -0
- invexapi-0.1.0/invexapi/optim/admm.py +137 -0
- invexapi-0.1.0/invexapi/optim/base.py +44 -0
- invexapi-0.1.0/invexapi/optim/conjugate_gradient.py +79 -0
- invexapi-0.1.0/invexapi/optim/fista.py +67 -0
- invexapi-0.1.0/invexapi/optim/gradient_descent.py +48 -0
- invexapi-0.1.0/invexapi/penalties/__init__.py +23 -0
- invexapi-0.1.0/invexapi/penalties/base.py +177 -0
- invexapi-0.1.0/invexapi/penalties/convex.py +62 -0
- invexapi-0.1.0/invexapi/penalties/l1.py +45 -0
- invexapi-0.1.0/invexapi/penalties/log.py +86 -0
- invexapi-0.1.0/invexapi/penalties/operators.py +100 -0
- invexapi-0.1.0/invexapi/penalties/quasinorm.py +134 -0
- invexapi-0.1.0/pyproject.toml +33 -0
- invexapi-0.1.0/tests/__init__.py +0 -0
- invexapi-0.1.0/tests/reference_numpy.py +73 -0
- invexapi-0.1.0/tests/test_admm.py +69 -0
- invexapi-0.1.0/tests/test_certificates.py +88 -0
- invexapi-0.1.0/tests/test_l1_penalty.py +30 -0
- invexapi-0.1.0/tests/test_log_penalty.py +57 -0
- invexapi-0.1.0/tests/test_metadata.py +48 -0
- invexapi-0.1.0/tests/test_operators.py +28 -0
- invexapi-0.1.0/tests/test_optimizers.py +92 -0
- invexapi-0.1.0/tests/test_quasinorm_penalty.py +67 -0
- invexapi-0.1.0/tests/test_solver_base.py +17 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
# ---- Python bytecode ---------------------------------------------------------
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
*.pyo
|
|
6
|
+
|
|
7
|
+
# ---- IDE / editor artefacts --------------------------------------------------
|
|
8
|
+
.cache/
|
|
9
|
+
.vscode/
|
|
10
|
+
.claude/
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
probes/
|
|
13
|
+
graphify-out/
|
|
14
|
+
|
|
15
|
+
# ---- Example data (fetched separately, see examples/data/download.py) --------
|
|
16
|
+
examples/data/
|
|
17
|
+
|
|
18
|
+
# ---- Miscellaneous -----------------------------------------------------------
|
|
19
|
+
CLAUDE.md
|
|
20
|
+
.gitignore
|
invexapi-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Samuel Pinilla
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
invexapi-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: invexapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: PyTorch Optimizers and Neural Network building blocks for Invex Results.
|
|
5
|
+
Project-URL: Homepage, https://github.com/samuelpinilla/invexapi
|
|
6
|
+
Author-email: Samuel Pinilla <spinilla@ieee.org>
|
|
7
|
+
License-Expression: BSD-3-Clause
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: torch>=2.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: numpy; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
18
|
+
Provides-Extra: examples
|
|
19
|
+
Requires-Dist: imageio; extra == 'examples'
|
|
20
|
+
Requires-Dist: matplotlib; extra == 'examples'
|
|
21
|
+
Requires-Dist: numpy; extra == 'examples'
|
|
22
|
+
Requires-Dist: pillow; extra == 'examples'
|
|
23
|
+
Requires-Dist: pywavelets; extra == 'examples'
|
|
24
|
+
Requires-Dist: scipy; extra == 'examples'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
<p align="center">
|
|
28
|
+
<img src="imgs/logo.png" alt="InvexAPI" width="480">
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
<p align="center">
|
|
32
|
+
<a href="LICENSE"><img alt="License: BSD-3-Clause" src="https://img.shields.io/badge/license-BSD--3--Clause-blue.svg"></a>
|
|
33
|
+
<img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10%2B-blue.svg">
|
|
34
|
+
<img alt="PyTorch only" src="https://img.shields.io/badge/backend-PyTorch-ee4c2c.svg">
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<h1 align="center">invexapi</h1>
|
|
38
|
+
|
|
39
|
+
<p align="center">
|
|
40
|
+
A minimal, PyTorch-only toolkit for <b>invex</b> optimization: penalties with
|
|
41
|
+
proven optimality certificates, and the solvers that use them.
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Why invexity?
|
|
47
|
+
|
|
48
|
+
Invex functions are a strict generalization of convexity: every stationary point
|
|
49
|
+
is still a global minimum, but the function itself need not be convex. That gives
|
|
50
|
+
non-convex penalties (sparsity-promoting, non-smooth, highly structured) the same
|
|
51
|
+
global-optimality guarantee convex optimization enjoys — without paying for it
|
|
52
|
+
with local minima.
|
|
53
|
+
|
|
54
|
+
`invexapi` packages this idea as code: penalties that carry a machine-checkable
|
|
55
|
+
**certificate** of which mathematical class they belong to (convex, invex,
|
|
56
|
+
quasi-convex, quasi-invex), and generic solvers that read those certificates to
|
|
57
|
+
warn you when no optimality guarantee applies.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install -e . # editable install
|
|
63
|
+
pip install -e ".[dev]" # + pytest, numpy for the test suite
|
|
64
|
+
pip install -e ".[examples]" # + deps used only by examples/
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import torch
|
|
71
|
+
from invexapi import QuasinormInvexPenalty
|
|
72
|
+
from invexapi.optim import FISTA
|
|
73
|
+
|
|
74
|
+
class DataFidelity:
|
|
75
|
+
def __init__(self, y): self.y = y
|
|
76
|
+
def value(self, x): return 0.5 * torch.sum((x - self.y) ** 2)
|
|
77
|
+
def grad(self, x): return x - self.y
|
|
78
|
+
|
|
79
|
+
y = torch.randn(100)
|
|
80
|
+
smooth = DataFidelity(y)
|
|
81
|
+
penalty = QuasinormInvexPenalty(lamb=0.1, q=0.5)
|
|
82
|
+
solver = FISTA(smooth, penalty, step=1.0)
|
|
83
|
+
|
|
84
|
+
x_hat, history = solver.run(y.clone())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Any object exposing the right methods (`value`, `grad`, `prox`) works as a
|
|
88
|
+
`smooth`/`penalty`/`objective` — the built-in penalties are just one plug-in
|
|
89
|
+
choice.
|
|
90
|
+
|
|
91
|
+
## What's inside
|
|
92
|
+
|
|
93
|
+
**Penalties** (`invexapi.penalties`) — loss/penalty terms plus a certificate of
|
|
94
|
+
what's provably known about each one:
|
|
95
|
+
|
|
96
|
+
| Penalty | Form | Certified as |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `QuasinormInvexPenalty(lamb, q)` | `λ·\|x\|^q` | invex |
|
|
99
|
+
| `LogInvexPenalty(lamb)` | `log(1+\|x\|) − \|x\|/(2+2\|x\|)` | invex |
|
|
100
|
+
| `TikhonovPenalty(lamb)` | `λ/2·‖x‖²` | convex, invex, quasi-convex |
|
|
101
|
+
| `L1Penalty(lamb)` | `λ·‖x‖₁` | convex, invex, quasi-convex |
|
|
102
|
+
|
|
103
|
+
Certificates are attached explicitly and never inferred — convexity composes
|
|
104
|
+
additively, but invexity does not, so a combined objective only carries a
|
|
105
|
+
certificate someone has actually proven for it.
|
|
106
|
+
|
|
107
|
+
**Solvers** (`invexapi.optim`) — generic, decoupled from the penalties above:
|
|
108
|
+
|
|
109
|
+
- `GradientDescent` — with optional Armijo backtracking line search
|
|
110
|
+
- `FISTA` — accelerated proximal gradient for `smooth(x) + penalty(x)`
|
|
111
|
+
- `NonlinearCG` — Polak-Ribière+ with automatic restart
|
|
112
|
+
- `LinearizedADMM` — for `smooth(x) + penalty(D@x)` (e.g. total variation)
|
|
113
|
+
|
|
114
|
+
All four warn (never error) when run on an objective without a convex/invex
|
|
115
|
+
certificate, since no global-optimum guarantee applies in that case.
|
|
116
|
+
|
|
117
|
+
**Linear operators** (`invexapi.penalties.operators`) — `Identity` and
|
|
118
|
+
`FiniteDifference2D` (2D total variation), each with a verified adjoint.
|
|
119
|
+
|
|
120
|
+
**Structured documentation** (`invexapi.metadata`) — design provenance,
|
|
121
|
+
rejected alternatives, and invariants as introspectable dataclasses rather than
|
|
122
|
+
prose, exportable as JSON for downstream tooling:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
import invexapi
|
|
126
|
+
print(invexapi.metadata.dump_all_json(indent=2))
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## GPU support
|
|
130
|
+
|
|
131
|
+
Nothing in `invexapi` is device-specific — every operation derives its device
|
|
132
|
+
from its input tensors. Move your data to CUDA before calling a solver and
|
|
133
|
+
everything downstream follows:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
137
|
+
x_hat, history = solver.run(y.to(device))
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Testing
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
pytest
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Penalty math is validated against independent NumPy reference implementations on
|
|
147
|
+
fixed-seed random inputs, with no CUDA dependency anywhere in this repo.
|
|
148
|
+
|
|
149
|
+
## Examples
|
|
150
|
+
|
|
151
|
+
See `examples/` for denoising and deblurring scripts covering FISTA and
|
|
152
|
+
Linearized ADMM with total variation, using both the quasinorm and log invex
|
|
153
|
+
penalties. The ADMM/TV examples need a test image, fetched separately:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
python examples/data/download.py
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
BSD 3-Clause, see [LICENSE](LICENSE).
|
|
162
|
+
|
|
163
|
+
## References
|
|
164
|
+
|
|
165
|
+
This library reproduces and generalizes results from the following papers:
|
|
166
|
+
|
|
167
|
+
1. Pinilla, S., Sanabria, A., Bi, J., & Egiazarian, K. (2025). *What makes neural
|
|
168
|
+
networks trainable? Invexity as a structural design principle in AI*.
|
|
169
|
+
2. Pinilla, S., & Thiyagalingam, J. (2024). *Global optimality for non-linear
|
|
170
|
+
constrained restoration problems via invexity*. International Conference on
|
|
171
|
+
Learning Representations (ICLR), 2024, pp. 11990–12027.
|
|
172
|
+
3. Pinilla, S., Mu, T., Bourne, N., & Thiyagalingam, J. (2022). *Improved imaging
|
|
173
|
+
by invex regularizers with global optima guarantees*. Advances in Neural
|
|
174
|
+
Information Processing Systems (NeurIPS), 35, pp. 10780–10794.
|
|
175
|
+
4. Pinilla, S., Yeung, S.-L., & Thiyagalingam, J. (2024). *Global convergence of
|
|
176
|
+
alternating direction method of multipliers for invex objective losses*. IEEE
|
|
177
|
+
International Conference on Acoustics, Speech and Signal Processing (ICASSP)
|
|
178
|
+
2024, pp. 9361–9365.
|
invexapi-0.1.0/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="imgs/logo.png" alt="InvexAPI" width="480">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="LICENSE"><img alt="License: BSD-3-Clause" src="https://img.shields.io/badge/license-BSD--3--Clause-blue.svg"></a>
|
|
7
|
+
<img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10%2B-blue.svg">
|
|
8
|
+
<img alt="PyTorch only" src="https://img.shields.io/badge/backend-PyTorch-ee4c2c.svg">
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<h1 align="center">invexapi</h1>
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
A minimal, PyTorch-only toolkit for <b>invex</b> optimization: penalties with
|
|
15
|
+
proven optimality certificates, and the solvers that use them.
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Why invexity?
|
|
21
|
+
|
|
22
|
+
Invex functions are a strict generalization of convexity: every stationary point
|
|
23
|
+
is still a global minimum, but the function itself need not be convex. That gives
|
|
24
|
+
non-convex penalties (sparsity-promoting, non-smooth, highly structured) the same
|
|
25
|
+
global-optimality guarantee convex optimization enjoys — without paying for it
|
|
26
|
+
with local minima.
|
|
27
|
+
|
|
28
|
+
`invexapi` packages this idea as code: penalties that carry a machine-checkable
|
|
29
|
+
**certificate** of which mathematical class they belong to (convex, invex,
|
|
30
|
+
quasi-convex, quasi-invex), and generic solvers that read those certificates to
|
|
31
|
+
warn you when no optimality guarantee applies.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install -e . # editable install
|
|
37
|
+
pip install -e ".[dev]" # + pytest, numpy for the test suite
|
|
38
|
+
pip install -e ".[examples]" # + deps used only by examples/
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import torch
|
|
45
|
+
from invexapi import QuasinormInvexPenalty
|
|
46
|
+
from invexapi.optim import FISTA
|
|
47
|
+
|
|
48
|
+
class DataFidelity:
|
|
49
|
+
def __init__(self, y): self.y = y
|
|
50
|
+
def value(self, x): return 0.5 * torch.sum((x - self.y) ** 2)
|
|
51
|
+
def grad(self, x): return x - self.y
|
|
52
|
+
|
|
53
|
+
y = torch.randn(100)
|
|
54
|
+
smooth = DataFidelity(y)
|
|
55
|
+
penalty = QuasinormInvexPenalty(lamb=0.1, q=0.5)
|
|
56
|
+
solver = FISTA(smooth, penalty, step=1.0)
|
|
57
|
+
|
|
58
|
+
x_hat, history = solver.run(y.clone())
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Any object exposing the right methods (`value`, `grad`, `prox`) works as a
|
|
62
|
+
`smooth`/`penalty`/`objective` — the built-in penalties are just one plug-in
|
|
63
|
+
choice.
|
|
64
|
+
|
|
65
|
+
## What's inside
|
|
66
|
+
|
|
67
|
+
**Penalties** (`invexapi.penalties`) — loss/penalty terms plus a certificate of
|
|
68
|
+
what's provably known about each one:
|
|
69
|
+
|
|
70
|
+
| Penalty | Form | Certified as |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `QuasinormInvexPenalty(lamb, q)` | `λ·\|x\|^q` | invex |
|
|
73
|
+
| `LogInvexPenalty(lamb)` | `log(1+\|x\|) − \|x\|/(2+2\|x\|)` | invex |
|
|
74
|
+
| `TikhonovPenalty(lamb)` | `λ/2·‖x‖²` | convex, invex, quasi-convex |
|
|
75
|
+
| `L1Penalty(lamb)` | `λ·‖x‖₁` | convex, invex, quasi-convex |
|
|
76
|
+
|
|
77
|
+
Certificates are attached explicitly and never inferred — convexity composes
|
|
78
|
+
additively, but invexity does not, so a combined objective only carries a
|
|
79
|
+
certificate someone has actually proven for it.
|
|
80
|
+
|
|
81
|
+
**Solvers** (`invexapi.optim`) — generic, decoupled from the penalties above:
|
|
82
|
+
|
|
83
|
+
- `GradientDescent` — with optional Armijo backtracking line search
|
|
84
|
+
- `FISTA` — accelerated proximal gradient for `smooth(x) + penalty(x)`
|
|
85
|
+
- `NonlinearCG` — Polak-Ribière+ with automatic restart
|
|
86
|
+
- `LinearizedADMM` — for `smooth(x) + penalty(D@x)` (e.g. total variation)
|
|
87
|
+
|
|
88
|
+
All four warn (never error) when run on an objective without a convex/invex
|
|
89
|
+
certificate, since no global-optimum guarantee applies in that case.
|
|
90
|
+
|
|
91
|
+
**Linear operators** (`invexapi.penalties.operators`) — `Identity` and
|
|
92
|
+
`FiniteDifference2D` (2D total variation), each with a verified adjoint.
|
|
93
|
+
|
|
94
|
+
**Structured documentation** (`invexapi.metadata`) — design provenance,
|
|
95
|
+
rejected alternatives, and invariants as introspectable dataclasses rather than
|
|
96
|
+
prose, exportable as JSON for downstream tooling:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import invexapi
|
|
100
|
+
print(invexapi.metadata.dump_all_json(indent=2))
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## GPU support
|
|
104
|
+
|
|
105
|
+
Nothing in `invexapi` is device-specific — every operation derives its device
|
|
106
|
+
from its input tensors. Move your data to CUDA before calling a solver and
|
|
107
|
+
everything downstream follows:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
111
|
+
x_hat, history = solver.run(y.to(device))
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Testing
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pytest
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Penalty math is validated against independent NumPy reference implementations on
|
|
121
|
+
fixed-seed random inputs, with no CUDA dependency anywhere in this repo.
|
|
122
|
+
|
|
123
|
+
## Examples
|
|
124
|
+
|
|
125
|
+
See `examples/` for denoising and deblurring scripts covering FISTA and
|
|
126
|
+
Linearized ADMM with total variation, using both the quasinorm and log invex
|
|
127
|
+
penalties. The ADMM/TV examples need a test image, fetched separately:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
python examples/data/download.py
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
BSD 3-Clause, see [LICENSE](LICENSE).
|
|
136
|
+
|
|
137
|
+
## References
|
|
138
|
+
|
|
139
|
+
This library reproduces and generalizes results from the following papers:
|
|
140
|
+
|
|
141
|
+
1. Pinilla, S., Sanabria, A., Bi, J., & Egiazarian, K. (2025). *What makes neural
|
|
142
|
+
networks trainable? Invexity as a structural design principle in AI*.
|
|
143
|
+
2. Pinilla, S., & Thiyagalingam, J. (2024). *Global optimality for non-linear
|
|
144
|
+
constrained restoration problems via invexity*. International Conference on
|
|
145
|
+
Learning Representations (ICLR), 2024, pp. 11990–12027.
|
|
146
|
+
3. Pinilla, S., Mu, T., Bourne, N., & Thiyagalingam, J. (2022). *Improved imaging
|
|
147
|
+
by invex regularizers with global optima guarantees*. Advances in Neural
|
|
148
|
+
Information Processing Systems (NeurIPS), 35, pp. 10780–10794.
|
|
149
|
+
4. Pinilla, S., Yeung, S.-L., & Thiyagalingam, J. (2024). *Global convergence of
|
|
150
|
+
alternating direction method of multipliers for invex objective losses*. IEEE
|
|
151
|
+
International Conference on Acoustics, Speech and Signal Processing (ICASSP)
|
|
152
|
+
2024, pp. 9361–9365.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Shared image-loading helper for the examples/ scripts that use real data.
|
|
2
|
+
|
|
3
|
+
Not part of the invexapi package — a plain sibling module the example scripts
|
|
4
|
+
import from each other, kept out of invexapi/ since loading/normalizing a test
|
|
5
|
+
image is an examples-only concern, not a library concern.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import torch
|
|
12
|
+
from PIL import Image
|
|
13
|
+
|
|
14
|
+
DATA_DIR = Path(__file__).resolve().parent / "data"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_image(relative_path: str) -> torch.Tensor:
|
|
18
|
+
"""Load an image under examples/data/ as a float tensor normalized to [0, 1].
|
|
19
|
+
|
|
20
|
+
``relative_path`` is relative to examples/data/, e.g. ``"images/noisy_image.tif"``.
|
|
21
|
+
Normalization divides by the dtype's max value (e.g. 65535 for uint16) so the
|
|
22
|
+
result is comparable to the [0, 1]-scaled synthetic signals the other examples
|
|
23
|
+
use; if the file is already floating point, it's returned as-is.
|
|
24
|
+
"""
|
|
25
|
+
path = DATA_DIR / relative_path
|
|
26
|
+
raw = np.array(Image.open(path))
|
|
27
|
+
|
|
28
|
+
if np.issubdtype(raw.dtype, np.integer):
|
|
29
|
+
factor = float(np.iinfo(raw.dtype).max)
|
|
30
|
+
raw = raw.astype(np.float32) / factor
|
|
31
|
+
|
|
32
|
+
return torch.from_numpy(raw.astype(np.float32))
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""1D deblurring with the log invex penalty, via FISTA.
|
|
2
|
+
|
|
3
|
+
Adapted from the FISTA/momentum deblurring loop in
|
|
4
|
+
``codes NEURIPS/model-based/modelEq10.py`` (DCT-domain deconvolution + invex prox on
|
|
5
|
+
wavelet coefficients), simplified to a synthetic 1D blur/sparse signal so the example
|
|
6
|
+
needs no external image files, ``pywt``, or ``scipy`` — just ``invexapi`` and
|
|
7
|
+
``torch``. The structure (smooth data-fidelity gradient step + invex prox step, run
|
|
8
|
+
through FISTA) mirrors the paper's pipeline exactly; only the transform (identity here
|
|
9
|
+
vs. DCT+wavelet there) is simplified, so the log penalty's prox stays exact rather
|
|
10
|
+
than approximated.
|
|
11
|
+
|
|
12
|
+
Problem: recover a sparse signal ``x_true`` from a blurred, noisy observation
|
|
13
|
+
``y = A x_true + noise`` (A = box blur), minimizing ``0.5*||Ax-y||^2 + lamb*g(x)``
|
|
14
|
+
where ``g`` is the log invex penalty from ``codes NEURIPS/model-based/modelEq10.py``.
|
|
15
|
+
|
|
16
|
+
Running this script still prints FISTA's UserWarning: the data-fidelity term alone
|
|
17
|
+
is certified convex, but the *combined* objective isn't (see
|
|
18
|
+
``invexapi.penalties.Sum`` — certificates never compose automatically), so the
|
|
19
|
+
global-optimum guarantee is still withheld here even though half the objective is
|
|
20
|
+
provably well-behaved.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import torch
|
|
24
|
+
|
|
25
|
+
from invexapi import Certificate, Loss, LogInvexPenalty
|
|
26
|
+
from invexapi.optim import FISTA
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _blur_matrix(n: int, width: int = 5) -> torch.Tensor:
|
|
30
|
+
A = torch.zeros(n, n)
|
|
31
|
+
half = width // 2
|
|
32
|
+
for i in range(n):
|
|
33
|
+
lo, hi = max(0, i - half), min(n, i + half + 1)
|
|
34
|
+
A[i, lo:hi] = 1.0 / (hi - lo)
|
|
35
|
+
return A
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _DataFidelity(Loss):
|
|
39
|
+
"""0.5*||Ax-y||^2. A@x is linear, so this quadratic is convex by inspection —
|
|
40
|
+
certified here (with no paper reference, since it's not from the source
|
|
41
|
+
papers) to show a Loss outside invexapi.penalties declaring its own certificate.
|
|
42
|
+
This does NOT silence FISTA's warning below: the *combined* smooth+penalty
|
|
43
|
+
objective (invexapi.penalties.Sum) still has no certificate of its own, by
|
|
44
|
+
design (see Sum's docstring) — only smooth's half is certified here."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, A: torch.Tensor, y: torch.Tensor):
|
|
47
|
+
super().__init__()
|
|
48
|
+
self.A = A
|
|
49
|
+
self.y = y
|
|
50
|
+
self._certify("convex", Certificate(status="assumed", source="manual"))
|
|
51
|
+
|
|
52
|
+
def value(self, x: torch.Tensor) -> torch.Tensor:
|
|
53
|
+
r = self.A @ x - self.y
|
|
54
|
+
return 0.5 * torch.sum(r * r)
|
|
55
|
+
|
|
56
|
+
def grad(self, x: torch.Tensor) -> torch.Tensor:
|
|
57
|
+
return self.A.T @ (self.A @ x - self.y)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main():
|
|
61
|
+
torch.manual_seed(0)
|
|
62
|
+
n = 200
|
|
63
|
+
sparsity = 20
|
|
64
|
+
|
|
65
|
+
x_true = torch.zeros(n)
|
|
66
|
+
idx = torch.randperm(n)[:sparsity]
|
|
67
|
+
x_true[idx] = torch.randn(sparsity) * 3.0
|
|
68
|
+
|
|
69
|
+
A = _blur_matrix(n)
|
|
70
|
+
noise = torch.randn(n) * 0.01
|
|
71
|
+
y = A @ x_true + noise
|
|
72
|
+
|
|
73
|
+
smooth = _DataFidelity(A, y)
|
|
74
|
+
penalty = LogInvexPenalty(lamb=5e-3)
|
|
75
|
+
|
|
76
|
+
L = torch.linalg.eigvalsh(A.T @ A).max().item()
|
|
77
|
+
solver = FISTA(smooth, penalty, step=1.0 / L, max_iter=300, tol=1e-10)
|
|
78
|
+
|
|
79
|
+
x_hat, history = solver.run(y.clone())
|
|
80
|
+
|
|
81
|
+
blurred_mse = torch.mean((y - x_true) ** 2).item()
|
|
82
|
+
deblurred_mse = torch.mean((x_hat - x_true) ** 2).item()
|
|
83
|
+
|
|
84
|
+
print(f"objective history: {history[0]:.4f} -> {history[-1]:.4f}")
|
|
85
|
+
print(f"MSE blurred: {blurred_mse:.4f}")
|
|
86
|
+
print(f"MSE deblurred: {deblurred_mse:.4f}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
main()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Total-variation image denoising via LinearizedADMM, on a real test image.
|
|
2
|
+
|
|
3
|
+
Adapted from ``code_ICLR/ADMM/ADMM_Lq.py``: TV-regularized denoising
|
|
4
|
+
(``min_x 0.5*||x-y||^2 + lamb*sum(|Dx|^q)``) where ``D`` is the 2D finite-difference
|
|
5
|
+
(TV) operator and the penalty is the quasinorm invex penalty already implemented in
|
|
6
|
+
this library — same configuration as the reference script, run here with
|
|
7
|
+
``LinearizedADMM`` + ``FiniteDifference2D`` + ``QuasinormInvexPenalty`` instead of
|
|
8
|
+
the reference's cupy/CUDA kernel, on ``examples/data/images/noisy_image.tif``
|
|
9
|
+
instead of the reference's own test image.
|
|
10
|
+
|
|
11
|
+
Running this script prints FISTA/GD/CG's usual UserWarning-style caveat: `smooth`
|
|
12
|
+
here (this example's own `_DataFidelity`) is uncertified, so LinearizedADMM warns
|
|
13
|
+
that no global-optimum guarantee applies — expected, same as the other examples.
|
|
14
|
+
|
|
15
|
+
Runs on CUDA automatically if available (``python examples/denoising_admm_tv.py``),
|
|
16
|
+
or force CPU with ``--device cpu``. Nothing in invexapi is device-specific — every
|
|
17
|
+
op inherits its device from its input tensors — so the only thing this script does
|
|
18
|
+
for GPU support is move the image tensor to the target device before construction;
|
|
19
|
+
everything downstream (the penalty's prox, the TV operator, the solver) follows.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import time
|
|
24
|
+
|
|
25
|
+
import matplotlib.pyplot as plt
|
|
26
|
+
import torch
|
|
27
|
+
|
|
28
|
+
from invexapi import Loss, QuasinormInvexPenalty
|
|
29
|
+
from invexapi.optim import LinearizedADMM
|
|
30
|
+
from invexapi.penalties.operators import FiniteDifference2D
|
|
31
|
+
|
|
32
|
+
from _data import load_image
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _DataFidelity(Loss):
|
|
36
|
+
def __init__(self, y: torch.Tensor):
|
|
37
|
+
super().__init__()
|
|
38
|
+
self.y = y
|
|
39
|
+
|
|
40
|
+
def value(self, x: torch.Tensor) -> torch.Tensor:
|
|
41
|
+
return 0.5 * torch.sum((x - self.y) ** 2)
|
|
42
|
+
|
|
43
|
+
def grad(self, x: torch.Tensor) -> torch.Tensor:
|
|
44
|
+
return x - self.y
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main():
|
|
48
|
+
parser = argparse.ArgumentParser()
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--device",
|
|
51
|
+
default="cuda" if torch.cuda.is_available() else "cpu",
|
|
52
|
+
help="torch device to run on, e.g. 'cuda', 'cuda:1', 'cpu' (default: cuda if available)",
|
|
53
|
+
)
|
|
54
|
+
args = parser.parse_args()
|
|
55
|
+
device = torch.device(args.device)
|
|
56
|
+
|
|
57
|
+
y = load_image("images/noisy_image.tif").to(device)
|
|
58
|
+
|
|
59
|
+
print(f"running on: {device}")
|
|
60
|
+
|
|
61
|
+
smooth = _DataFidelity(y)
|
|
62
|
+
penalty = QuasinormInvexPenalty(lamb=1.1e-1, q=0.85)
|
|
63
|
+
solver = LinearizedADMM(
|
|
64
|
+
smooth,
|
|
65
|
+
penalty,
|
|
66
|
+
D=FiniteDifference2D(),
|
|
67
|
+
rho=1.6,
|
|
68
|
+
project=lambda x: x.clamp(min=0.0),
|
|
69
|
+
max_iter=200,
|
|
70
|
+
tol=1e-8,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
start_time = time.time()
|
|
74
|
+
x_hat, history = solver.run(y.clone())
|
|
75
|
+
end_time = time.time()
|
|
76
|
+
|
|
77
|
+
print(f"image shape: {tuple(y.shape)}")
|
|
78
|
+
print(f"objective history: {history[0]:.4f} -> {history[-1]:.4f}")
|
|
79
|
+
print(f"denoised range: [{x_hat.min().item():.4f}, {x_hat.max().item():.4f}]")
|
|
80
|
+
print(f"mean |x_hat - y|: {torch.mean((x_hat - y).abs()).item():.6f}")
|
|
81
|
+
print(f"elapsed time: {end_time - start_time:.4f} seconds")
|
|
82
|
+
|
|
83
|
+
plt.imshow(x_hat.cpu().numpy(), cmap="gray")
|
|
84
|
+
plt.show()
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
main()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Total-variation image denoising via LinearizedADMM, using the log penalty.
|
|
2
|
+
|
|
3
|
+
Same setup as ``denoising_admm_tv.py`` (``min_x 0.5*||x-y||^2 + lamb*g(Dx)`` via
|
|
4
|
+
``LinearizedADMM`` + ``FiniteDifference2D`` on
|
|
5
|
+
``examples/data/images/noisy_image.tif``), but with ``g`` = ``LogInvexPenalty``
|
|
6
|
+
instead of ``QuasinormInvexPenalty`` — the reference ADMM scripts only pair TV with
|
|
7
|
+
the quasinorm/L1/Llq penalties, so there is no original CUDA kernel to validate
|
|
8
|
+
this specific combination against; it demonstrates that any `Penalty` (paper-sourced
|
|
9
|
+
or not) works as `LinearizedADMM`'s second argument, per its generic `penalty.prox`
|
|
10
|
+
contract.
|
|
11
|
+
|
|
12
|
+
Runs on CUDA automatically if available, or force CPU with ``--device cpu`` (see
|
|
13
|
+
``denoising_admm_tv.py``'s docstring for why no other device-handling is needed).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
import matplotlib.pyplot as plt
|
|
20
|
+
import torch
|
|
21
|
+
|
|
22
|
+
from invexapi import Loss, LogInvexPenalty
|
|
23
|
+
from invexapi.optim import LinearizedADMM
|
|
24
|
+
from invexapi.penalties.operators import FiniteDifference2D
|
|
25
|
+
|
|
26
|
+
from _data import load_image
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _DataFidelity(Loss):
|
|
30
|
+
def __init__(self, y: torch.Tensor):
|
|
31
|
+
super().__init__()
|
|
32
|
+
self.y = y
|
|
33
|
+
|
|
34
|
+
def value(self, x: torch.Tensor) -> torch.Tensor:
|
|
35
|
+
return 0.5 * torch.sum((x - self.y) ** 2)
|
|
36
|
+
|
|
37
|
+
def grad(self, x: torch.Tensor) -> torch.Tensor:
|
|
38
|
+
return x - self.y
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main():
|
|
42
|
+
parser = argparse.ArgumentParser()
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--device",
|
|
45
|
+
default="cuda" if torch.cuda.is_available() else "cpu",
|
|
46
|
+
help="torch device to run on, e.g. 'cuda', 'cuda:1', 'cpu' (default: cuda if available)",
|
|
47
|
+
)
|
|
48
|
+
args = parser.parse_args()
|
|
49
|
+
device = torch.device(args.device)
|
|
50
|
+
|
|
51
|
+
y = load_image("images/noisy_image.tif").to(device)
|
|
52
|
+
|
|
53
|
+
print(f"running on: {device}")
|
|
54
|
+
|
|
55
|
+
smooth = _DataFidelity(y)
|
|
56
|
+
penalty = LogInvexPenalty(lamb=3e-1)
|
|
57
|
+
solver = LinearizedADMM(
|
|
58
|
+
smooth,
|
|
59
|
+
penalty,
|
|
60
|
+
D=FiniteDifference2D(),
|
|
61
|
+
rho=1.6,
|
|
62
|
+
project=lambda x: x.clamp(min=0.0),
|
|
63
|
+
max_iter=200,
|
|
64
|
+
tol=1e-8,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
start_time = time.time()
|
|
68
|
+
x_hat, history = solver.run(y.clone())
|
|
69
|
+
end_time = time.time()
|
|
70
|
+
|
|
71
|
+
print(f"image shape: {tuple(y.shape)}")
|
|
72
|
+
print(f"objective history: {history[0]:.4f} -> {history[-1]:.4f}")
|
|
73
|
+
print(f"denoised range: [{x_hat.min().item():.4f}, {x_hat.max().item():.4f}]")
|
|
74
|
+
print(f"mean |x_hat - y|: {torch.mean((x_hat - y).abs()).item():.6f}")
|
|
75
|
+
print(f"elapsed time: {end_time - start_time:.4f} seconds")
|
|
76
|
+
|
|
77
|
+
plt.imshow(x_hat.cpu().numpy(), cmap="gray")
|
|
78
|
+
plt.show()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
main()
|