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.
@@ -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/
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ *.egg
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
+ [![PyPI version](https://img.shields.io/pypi/v/otloss.svg)](https://pypi.org/project/otloss/)
38
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
39
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
40
+ [![Tests](https://github.com/Maqbool61/otloss/actions/workflows/tests.yml/badge.svg)](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
+ [![PyPI version](https://img.shields.io/pypi/v/otloss.svg)](https://pypi.org/project/otloss/)
6
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
+ [![Tests](https://github.com/Maqbool61/otloss/actions/workflows/tests.yml/badge.svg)](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