otloss 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.
- otloss-0.1.0/.github/workflows/tests.yml +34 -0
- otloss-0.1.0/.gitignore +7 -0
- otloss-0.1.0/LICENSE +21 -0
- otloss-0.1.0/PKG-INFO +332 -0
- otloss-0.1.0/README.md +300 -0
- otloss-0.1.0/comparison/01_gan_comparison.py +128 -0
- otloss-0.1.0/comparison/02_calibration_comparison.py +143 -0
- otloss-0.1.0/comparison/03_convergence_comparison.py +167 -0
- otloss-0.1.0/comparison/04_rlhf_reward_comparison.py +170 -0
- otloss-0.1.0/comparison/05_molecule_comparison.py +210 -0
- otloss-0.1.0/comparison/README.md +46 -0
- otloss-0.1.0/comparison/run_all.py +35 -0
- otloss-0.1.0/examples/01_gan_image_generation.py +145 -0
- otloss-0.1.0/examples/02_llm_calibration.py +150 -0
- otloss-0.1.0/examples/03_molecule_generation.py +158 -0
- otloss-0.1.0/otloss/__init__.py +53 -0
- otloss-0.1.0/otloss/distributions.py +126 -0
- otloss-0.1.0/otloss/functional.py +384 -0
- otloss-0.1.0/otloss/losses.py +308 -0
- otloss-0.1.0/otloss/utils.py +178 -0
- otloss-0.1.0/pyproject.toml +68 -0
- otloss-0.1.0/tests/test_wasserstein.py +328 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.9", "3.10", "3.11"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: |
|
|
26
|
+
python -m pip install --upgrade pip
|
|
27
|
+
pip install torch --index-url https://download.pytorch.org/whl/cpu
|
|
28
|
+
pip install -e ".[dev]"
|
|
29
|
+
|
|
30
|
+
- name: Run tests
|
|
31
|
+
run: pytest tests/ -v --tb=short
|
|
32
|
+
|
|
33
|
+
- name: Check formatting
|
|
34
|
+
run: black --check otloss/ tests/
|
otloss-0.1.0/.gitignore
ADDED
otloss-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maqbool61
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
otloss-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: otloss
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Optimal Transport training objectives for PyTorch — drop-in replacement for cross-entropy and MSE
|
|
5
|
+
Project-URL: Homepage, https://github.com/Maqbool61/otloss
|
|
6
|
+
Project-URL: Repository, https://github.com/Maqbool61/otloss
|
|
7
|
+
Project-URL: Issues, https://github.com/Maqbool61/otloss/issues
|
|
8
|
+
Author: Maqbool61
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: calibration,deep-learning,generative-models,loss-function,machine-learning,optimal-transport,pytorch,wasserstein
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: torch>=2.0.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: black; extra == 'dev'
|
|
24
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
28
|
+
Provides-Extra: examples
|
|
29
|
+
Requires-Dist: matplotlib>=3.5; extra == 'examples'
|
|
30
|
+
Requires-Dist: numpy>=1.22; extra == 'examples'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# otloss
|
|
34
|
+
|
|
35
|
+
**Optimal Transport training objectives for PyTorch** — a drop-in replacement for cross-entropy and MSE that eliminates mode collapse, improves calibration, and produces robust distributional representations.
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/otloss/)
|
|
38
|
+
[](https://www.python.org/downloads/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
[](https://github.com/Maqbool61/otloss/actions)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Why WassersteinLoss?
|
|
45
|
+
|
|
46
|
+
Cross-entropy and KL divergence have a fundamental flaw: **their gradients vanish when the model and target distributions don't overlap**. This causes:
|
|
47
|
+
|
|
48
|
+
- Mode collapse in GANs and generative models
|
|
49
|
+
- Overconfident, poorly calibrated probability outputs
|
|
50
|
+
- Brittle behaviour under distribution shift
|
|
51
|
+
|
|
52
|
+
The Wasserstein-2 distance solves all three. It defines a *geometric* distance between distributions using the ground metric of the feature space:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
W₂(μ, ν) = inf_{γ ∈ Π(μ,ν)} ∫ ‖x - y‖² dγ(x, y)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
It always has **meaningful gradients**, naturally respects the geometry of your output space, and produces smooth, interference-free learning signals.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install otloss
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Or from source:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
git clone https://github.com/Maqbool61/otloss.git
|
|
72
|
+
cd otloss
|
|
73
|
+
pip install -e ".[dev]"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Requirements:** Python ≥ 3.9, PyTorch ≥ 2.0
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Quick start
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from otloss import WassersteinLoss
|
|
84
|
+
|
|
85
|
+
# Drop-in replacement for nn.MSELoss
|
|
86
|
+
criterion = WassersteinLoss(p=2, blur=0.05)
|
|
87
|
+
|
|
88
|
+
pred = torch.randn(32, 100, 2, requires_grad=True) # (batch, particles, dim)
|
|
89
|
+
target = torch.randn(32, 100, 2)
|
|
90
|
+
|
|
91
|
+
loss = criterion(pred, target)
|
|
92
|
+
loss.backward() # gradients flow through pred
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Layered API
|
|
98
|
+
|
|
99
|
+
### High-level (nn.Module)
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from otloss import WassersteinLoss, SlicedWassersteinLoss
|
|
103
|
+
from otloss.losses import WassersteinGANLoss
|
|
104
|
+
|
|
105
|
+
# Exact Wasserstein via Sinkhorn (best accuracy)
|
|
106
|
+
criterion = WassersteinLoss(
|
|
107
|
+
p=2, # Wasserstein order (1 or 2)
|
|
108
|
+
blur=0.05, # entropic regularisation ε = blur²
|
|
109
|
+
max_iter=100, # Sinkhorn iterations
|
|
110
|
+
debias=True, # Sinkhorn divergence debiasing
|
|
111
|
+
reduction="mean",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Fast O(n log n) approximation via random projections
|
|
115
|
+
criterion = SlicedWassersteinLoss(
|
|
116
|
+
n_projections=200,
|
|
117
|
+
p=2,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# WGAN-GP critic/generator losses
|
|
121
|
+
criterion = WassersteinGANLoss(gp_weight=10.0)
|
|
122
|
+
d_loss = criterion.critic_loss(real_scores, fake_scores)
|
|
123
|
+
gp = criterion.gradient_penalty(critic, real, fake)
|
|
124
|
+
g_loss = criterion.generator_loss(fake_scores)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Functional API (low-level, full control)
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from otloss import (
|
|
131
|
+
otloss, # full Wasserstein via Sinkhorn
|
|
132
|
+
sliced_otloss,
|
|
133
|
+
sinkhorn, # raw Sinkhorn solver
|
|
134
|
+
cost_matrix, # ground cost C_{ij} = ‖xᵢ - yⱼ‖ᵖ
|
|
135
|
+
dual_variables, # Kantorovich dual potentials (f, g)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Compute cost matrix
|
|
139
|
+
C = cost_matrix(x, y, p=2) # (N, M)
|
|
140
|
+
|
|
141
|
+
# Run Sinkhorn and get dual potentials + transport cost
|
|
142
|
+
f, g, cost = sinkhorn(a, b, C, blur=0.05, debias=True)
|
|
143
|
+
|
|
144
|
+
# Recover soft transport plan P_{ij}
|
|
145
|
+
from otloss.utils import transport_plan
|
|
146
|
+
P = transport_plan(f, g, C, blur=0.05) # (N, M)
|
|
147
|
+
|
|
148
|
+
# Wasserstein barycenter
|
|
149
|
+
from otloss.utils import wasserstein_barycenter_weights
|
|
150
|
+
barycenter = wasserstein_barycenter_weights(measures, weights=[0.3, 0.7], support=X)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Real-world use cases
|
|
156
|
+
|
|
157
|
+
### 1. GAN training without mode collapse
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from otloss.losses import WassersteinGANLoss
|
|
161
|
+
|
|
162
|
+
criterion = WassersteinGANLoss(gp_weight=10.0)
|
|
163
|
+
|
|
164
|
+
# Critic update
|
|
165
|
+
c_loss = criterion.critic_loss(D(real), D(fake.detach()))
|
|
166
|
+
gp = criterion.gradient_penalty(D, real, fake)
|
|
167
|
+
(c_loss + gp).backward()
|
|
168
|
+
|
|
169
|
+
# Generator update
|
|
170
|
+
g_loss = criterion.generator_loss(D(fake))
|
|
171
|
+
g_loss.backward()
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Or even simpler — no critic needed:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
criterion = WassersteinLoss(blur=0.05)
|
|
178
|
+
|
|
179
|
+
# Directly minimise W₂ between fake and real sample clouds
|
|
180
|
+
fake = G(noise) # (B, N, D)
|
|
181
|
+
real = real_data # (B, N, D)
|
|
182
|
+
loss = criterion(fake, real)
|
|
183
|
+
loss.backward()
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 2. LLM calibration (confidence matches accuracy)
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from otloss import WassersteinLoss
|
|
190
|
+
from otloss.distributions import label_smoothed_weights
|
|
191
|
+
|
|
192
|
+
criterion = WassersteinLoss(p=2, blur=0.05)
|
|
193
|
+
|
|
194
|
+
# Class positions as 1-D support
|
|
195
|
+
support = torch.linspace(0, 1, n_classes).unsqueeze(-1) # (K, 1)
|
|
196
|
+
support = support.unsqueeze(0).expand(B, -1, -1) # (B, K, 1)
|
|
197
|
+
|
|
198
|
+
pred_weights = torch.softmax(logits, dim=-1) # (B, K)
|
|
199
|
+
target_weights = label_smoothed_weights(y, n_classes) # (B, K)
|
|
200
|
+
|
|
201
|
+
loss = criterion(support, support,
|
|
202
|
+
pred_weights=pred_weights,
|
|
203
|
+
target_weights=target_weights)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 3. Drug molecule generation (diverse scaffolds)
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from otloss import SlicedWassersteinLoss
|
|
210
|
+
|
|
211
|
+
# Measures structural diversity in 8-D property space
|
|
212
|
+
criterion = SlicedWassersteinLoss(n_projections=200)
|
|
213
|
+
|
|
214
|
+
generated = model.decode(z) # (B, N, 8) property vectors
|
|
215
|
+
reference = real_molecules # (B, N, 8)
|
|
216
|
+
loss = criterion(generated, reference)
|
|
217
|
+
loss.backward()
|
|
218
|
+
# → model covers the full pharmacological distribution
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 4. Financial time-series generation (fat tails)
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
criterion = WassersteinLoss(p=2, blur=0.01) # small blur → sharp tails
|
|
225
|
+
|
|
226
|
+
generated_returns = model(noise) # (B, T, 1)
|
|
227
|
+
real_returns = historical # (B, T, 1)
|
|
228
|
+
loss = criterion(generated_returns, real_returns)
|
|
229
|
+
# → VaR/CVaR of generated paths matches real market data
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### 5. RLHF reward model training
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
criterion = WassersteinLoss(p=2, blur=0.05, debias=True)
|
|
236
|
+
|
|
237
|
+
# Reward model outputs as distributions over preference scores
|
|
238
|
+
pred_rewards = reward_model(responses) # (B, K, 1)
|
|
239
|
+
human_prefs = preference_labels # (B, K, 1)
|
|
240
|
+
loss = criterion(pred_rewards, human_prefs)
|
|
241
|
+
# → smoother reward landscape → better RLHF alignment
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Mathematical background
|
|
247
|
+
|
|
248
|
+
### Entropic regularisation (Sinkhorn)
|
|
249
|
+
|
|
250
|
+
Direct computation of W₂ is O(n³). We solve the entropy-regularised problem:
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
W_ε(a, b) = min_{P ≥ 0} ⟨C, P⟩ − ε · H(P)
|
|
254
|
+
s.t. P·1 = a, Pᵀ·1 = b
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Via Sinkhorn-Knopp iterations in log-domain (numerically stable):
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
fᵢ ← ε · log(aᵢ) − ε · LSE_j[(gⱼ − Cᵢⱼ) / ε]
|
|
261
|
+
gⱼ ← ε · log(bⱼ) − ε · LSE_i[(fᵢ − Cᵢⱼ) / ε]
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Sinkhorn divergence (debiasing)
|
|
265
|
+
|
|
266
|
+
Raw Sinkhorn overestimates W due to entropic bias. We correct with:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
S_ε(a, b) = W_ε(a, b) − ½W_ε(a, a) − ½W_ε(b, b)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
This ensures `S_ε(a, a) = 0` (positive definite) and `S_ε → W` as `ε → 0`.
|
|
273
|
+
|
|
274
|
+
### Sliced Wasserstein Distance
|
|
275
|
+
|
|
276
|
+
Projects to 1-D random lines and uses the closed-form 1-D solution:
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
SW_p(μ, ν) = ( ∫_{S^{D-1}} W_p(θ#μ, θ#ν)^p dσ(θ) )^{1/p}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Exact W in 1-D reduces to: `W_p = ‖sort(x) − sort(y)‖_p / N^{1/p}`.
|
|
283
|
+
Complexity: **O(n log n)** vs O(n³) for exact OT.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Choosing `blur`
|
|
288
|
+
|
|
289
|
+
| Scenario | Recommended blur |
|
|
290
|
+
|---|---|
|
|
291
|
+
| Tight distributions (calibration) | 0.01 – 0.03 |
|
|
292
|
+
| Moderate spread (generation) | 0.05 – 0.1 |
|
|
293
|
+
| Very spread / high-dimensional | 0.1 – 0.5 |
|
|
294
|
+
| Rule of thumb | `blur ≈ std(data) × 0.05` |
|
|
295
|
+
|
|
296
|
+
Smaller blur = more accurate but more Sinkhorn iterations. Blur annealing (enabled by default via `scaling=0.5`) starts coarse and refines automatically.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Running tests
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
pytest tests/ -v
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Citation
|
|
309
|
+
|
|
310
|
+
If you use WassersteinLoss in your research:
|
|
311
|
+
|
|
312
|
+
```bibtex
|
|
313
|
+
@software{otloss_2026,
|
|
314
|
+
author = {Maqbool61},
|
|
315
|
+
title = {otloss: Optimal Transport objectives for PyTorch},
|
|
316
|
+
year = {2026},
|
|
317
|
+
url = {https://github.com/Maqbool61/otloss},
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Key papers:**
|
|
322
|
+
- Villani (2008) — *Optimal Transport: Old and New*
|
|
323
|
+
- Cuturi (2013) — *Sinkhorn Distances: Lightspeed Computation of Optimal Transport*
|
|
324
|
+
- Arjovsky et al. (2017) — *Wasserstein GAN*
|
|
325
|
+
- Gulrajani et al. (2017) — *Improved Training of Wasserstein GANs*
|
|
326
|
+
- Feydy et al. (2019) — *Interpolating between Optimal Transport and MMD using Sinkhorn Divergences*
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## License
|
|
331
|
+
|
|
332
|
+
MIT © Maqbool61
|
otloss-0.1.0/README.md
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# otloss
|
|
2
|
+
|
|
3
|
+
**Optimal Transport training objectives for PyTorch** — a drop-in replacement for cross-entropy and MSE that eliminates mode collapse, improves calibration, and produces robust distributional representations.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/otloss/)
|
|
6
|
+
[](https://www.python.org/downloads/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://github.com/Maqbool61/otloss/actions)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why WassersteinLoss?
|
|
13
|
+
|
|
14
|
+
Cross-entropy and KL divergence have a fundamental flaw: **their gradients vanish when the model and target distributions don't overlap**. This causes:
|
|
15
|
+
|
|
16
|
+
- Mode collapse in GANs and generative models
|
|
17
|
+
- Overconfident, poorly calibrated probability outputs
|
|
18
|
+
- Brittle behaviour under distribution shift
|
|
19
|
+
|
|
20
|
+
The Wasserstein-2 distance solves all three. It defines a *geometric* distance between distributions using the ground metric of the feature space:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
W₂(μ, ν) = inf_{γ ∈ Π(μ,ν)} ∫ ‖x - y‖² dγ(x, y)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
It always has **meaningful gradients**, naturally respects the geometry of your output space, and produces smooth, interference-free learning signals.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install otloss
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or from source:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/Maqbool61/otloss.git
|
|
40
|
+
cd otloss
|
|
41
|
+
pip install -e ".[dev]"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Requirements:** Python ≥ 3.9, PyTorch ≥ 2.0
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from otloss import WassersteinLoss
|
|
52
|
+
|
|
53
|
+
# Drop-in replacement for nn.MSELoss
|
|
54
|
+
criterion = WassersteinLoss(p=2, blur=0.05)
|
|
55
|
+
|
|
56
|
+
pred = torch.randn(32, 100, 2, requires_grad=True) # (batch, particles, dim)
|
|
57
|
+
target = torch.randn(32, 100, 2)
|
|
58
|
+
|
|
59
|
+
loss = criterion(pred, target)
|
|
60
|
+
loss.backward() # gradients flow through pred
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Layered API
|
|
66
|
+
|
|
67
|
+
### High-level (nn.Module)
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from otloss import WassersteinLoss, SlicedWassersteinLoss
|
|
71
|
+
from otloss.losses import WassersteinGANLoss
|
|
72
|
+
|
|
73
|
+
# Exact Wasserstein via Sinkhorn (best accuracy)
|
|
74
|
+
criterion = WassersteinLoss(
|
|
75
|
+
p=2, # Wasserstein order (1 or 2)
|
|
76
|
+
blur=0.05, # entropic regularisation ε = blur²
|
|
77
|
+
max_iter=100, # Sinkhorn iterations
|
|
78
|
+
debias=True, # Sinkhorn divergence debiasing
|
|
79
|
+
reduction="mean",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Fast O(n log n) approximation via random projections
|
|
83
|
+
criterion = SlicedWassersteinLoss(
|
|
84
|
+
n_projections=200,
|
|
85
|
+
p=2,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# WGAN-GP critic/generator losses
|
|
89
|
+
criterion = WassersteinGANLoss(gp_weight=10.0)
|
|
90
|
+
d_loss = criterion.critic_loss(real_scores, fake_scores)
|
|
91
|
+
gp = criterion.gradient_penalty(critic, real, fake)
|
|
92
|
+
g_loss = criterion.generator_loss(fake_scores)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Functional API (low-level, full control)
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from otloss import (
|
|
99
|
+
otloss, # full Wasserstein via Sinkhorn
|
|
100
|
+
sliced_otloss,
|
|
101
|
+
sinkhorn, # raw Sinkhorn solver
|
|
102
|
+
cost_matrix, # ground cost C_{ij} = ‖xᵢ - yⱼ‖ᵖ
|
|
103
|
+
dual_variables, # Kantorovich dual potentials (f, g)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Compute cost matrix
|
|
107
|
+
C = cost_matrix(x, y, p=2) # (N, M)
|
|
108
|
+
|
|
109
|
+
# Run Sinkhorn and get dual potentials + transport cost
|
|
110
|
+
f, g, cost = sinkhorn(a, b, C, blur=0.05, debias=True)
|
|
111
|
+
|
|
112
|
+
# Recover soft transport plan P_{ij}
|
|
113
|
+
from otloss.utils import transport_plan
|
|
114
|
+
P = transport_plan(f, g, C, blur=0.05) # (N, M)
|
|
115
|
+
|
|
116
|
+
# Wasserstein barycenter
|
|
117
|
+
from otloss.utils import wasserstein_barycenter_weights
|
|
118
|
+
barycenter = wasserstein_barycenter_weights(measures, weights=[0.3, 0.7], support=X)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Real-world use cases
|
|
124
|
+
|
|
125
|
+
### 1. GAN training without mode collapse
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from otloss.losses import WassersteinGANLoss
|
|
129
|
+
|
|
130
|
+
criterion = WassersteinGANLoss(gp_weight=10.0)
|
|
131
|
+
|
|
132
|
+
# Critic update
|
|
133
|
+
c_loss = criterion.critic_loss(D(real), D(fake.detach()))
|
|
134
|
+
gp = criterion.gradient_penalty(D, real, fake)
|
|
135
|
+
(c_loss + gp).backward()
|
|
136
|
+
|
|
137
|
+
# Generator update
|
|
138
|
+
g_loss = criterion.generator_loss(D(fake))
|
|
139
|
+
g_loss.backward()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Or even simpler — no critic needed:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
criterion = WassersteinLoss(blur=0.05)
|
|
146
|
+
|
|
147
|
+
# Directly minimise W₂ between fake and real sample clouds
|
|
148
|
+
fake = G(noise) # (B, N, D)
|
|
149
|
+
real = real_data # (B, N, D)
|
|
150
|
+
loss = criterion(fake, real)
|
|
151
|
+
loss.backward()
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 2. LLM calibration (confidence matches accuracy)
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from otloss import WassersteinLoss
|
|
158
|
+
from otloss.distributions import label_smoothed_weights
|
|
159
|
+
|
|
160
|
+
criterion = WassersteinLoss(p=2, blur=0.05)
|
|
161
|
+
|
|
162
|
+
# Class positions as 1-D support
|
|
163
|
+
support = torch.linspace(0, 1, n_classes).unsqueeze(-1) # (K, 1)
|
|
164
|
+
support = support.unsqueeze(0).expand(B, -1, -1) # (B, K, 1)
|
|
165
|
+
|
|
166
|
+
pred_weights = torch.softmax(logits, dim=-1) # (B, K)
|
|
167
|
+
target_weights = label_smoothed_weights(y, n_classes) # (B, K)
|
|
168
|
+
|
|
169
|
+
loss = criterion(support, support,
|
|
170
|
+
pred_weights=pred_weights,
|
|
171
|
+
target_weights=target_weights)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 3. Drug molecule generation (diverse scaffolds)
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from otloss import SlicedWassersteinLoss
|
|
178
|
+
|
|
179
|
+
# Measures structural diversity in 8-D property space
|
|
180
|
+
criterion = SlicedWassersteinLoss(n_projections=200)
|
|
181
|
+
|
|
182
|
+
generated = model.decode(z) # (B, N, 8) property vectors
|
|
183
|
+
reference = real_molecules # (B, N, 8)
|
|
184
|
+
loss = criterion(generated, reference)
|
|
185
|
+
loss.backward()
|
|
186
|
+
# → model covers the full pharmacological distribution
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 4. Financial time-series generation (fat tails)
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
criterion = WassersteinLoss(p=2, blur=0.01) # small blur → sharp tails
|
|
193
|
+
|
|
194
|
+
generated_returns = model(noise) # (B, T, 1)
|
|
195
|
+
real_returns = historical # (B, T, 1)
|
|
196
|
+
loss = criterion(generated_returns, real_returns)
|
|
197
|
+
# → VaR/CVaR of generated paths matches real market data
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### 5. RLHF reward model training
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
criterion = WassersteinLoss(p=2, blur=0.05, debias=True)
|
|
204
|
+
|
|
205
|
+
# Reward model outputs as distributions over preference scores
|
|
206
|
+
pred_rewards = reward_model(responses) # (B, K, 1)
|
|
207
|
+
human_prefs = preference_labels # (B, K, 1)
|
|
208
|
+
loss = criterion(pred_rewards, human_prefs)
|
|
209
|
+
# → smoother reward landscape → better RLHF alignment
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Mathematical background
|
|
215
|
+
|
|
216
|
+
### Entropic regularisation (Sinkhorn)
|
|
217
|
+
|
|
218
|
+
Direct computation of W₂ is O(n³). We solve the entropy-regularised problem:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
W_ε(a, b) = min_{P ≥ 0} ⟨C, P⟩ − ε · H(P)
|
|
222
|
+
s.t. P·1 = a, Pᵀ·1 = b
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Via Sinkhorn-Knopp iterations in log-domain (numerically stable):
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
fᵢ ← ε · log(aᵢ) − ε · LSE_j[(gⱼ − Cᵢⱼ) / ε]
|
|
229
|
+
gⱼ ← ε · log(bⱼ) − ε · LSE_i[(fᵢ − Cᵢⱼ) / ε]
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Sinkhorn divergence (debiasing)
|
|
233
|
+
|
|
234
|
+
Raw Sinkhorn overestimates W due to entropic bias. We correct with:
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
S_ε(a, b) = W_ε(a, b) − ½W_ε(a, a) − ½W_ε(b, b)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
This ensures `S_ε(a, a) = 0` (positive definite) and `S_ε → W` as `ε → 0`.
|
|
241
|
+
|
|
242
|
+
### Sliced Wasserstein Distance
|
|
243
|
+
|
|
244
|
+
Projects to 1-D random lines and uses the closed-form 1-D solution:
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
SW_p(μ, ν) = ( ∫_{S^{D-1}} W_p(θ#μ, θ#ν)^p dσ(θ) )^{1/p}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Exact W in 1-D reduces to: `W_p = ‖sort(x) − sort(y)‖_p / N^{1/p}`.
|
|
251
|
+
Complexity: **O(n log n)** vs O(n³) for exact OT.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Choosing `blur`
|
|
256
|
+
|
|
257
|
+
| Scenario | Recommended blur |
|
|
258
|
+
|---|---|
|
|
259
|
+
| Tight distributions (calibration) | 0.01 – 0.03 |
|
|
260
|
+
| Moderate spread (generation) | 0.05 – 0.1 |
|
|
261
|
+
| Very spread / high-dimensional | 0.1 – 0.5 |
|
|
262
|
+
| Rule of thumb | `blur ≈ std(data) × 0.05` |
|
|
263
|
+
|
|
264
|
+
Smaller blur = more accurate but more Sinkhorn iterations. Blur annealing (enabled by default via `scaling=0.5`) starts coarse and refines automatically.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Running tests
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
pytest tests/ -v
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Citation
|
|
277
|
+
|
|
278
|
+
If you use WassersteinLoss in your research:
|
|
279
|
+
|
|
280
|
+
```bibtex
|
|
281
|
+
@software{otloss_2026,
|
|
282
|
+
author = {Maqbool61},
|
|
283
|
+
title = {otloss: Optimal Transport objectives for PyTorch},
|
|
284
|
+
year = {2026},
|
|
285
|
+
url = {https://github.com/Maqbool61/otloss},
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Key papers:**
|
|
290
|
+
- Villani (2008) — *Optimal Transport: Old and New*
|
|
291
|
+
- Cuturi (2013) — *Sinkhorn Distances: Lightspeed Computation of Optimal Transport*
|
|
292
|
+
- Arjovsky et al. (2017) — *Wasserstein GAN*
|
|
293
|
+
- Gulrajani et al. (2017) — *Improved Training of Wasserstein GANs*
|
|
294
|
+
- Feydy et al. (2019) — *Interpolating between Optimal Transport and MMD using Sinkhorn Divergences*
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## License
|
|
299
|
+
|
|
300
|
+
MIT © Maqbool61
|