Skip to main content

Optimal Transport training objectives for PyTorch — drop-in replacement for cross-entropy and MSE

Project description

otloss

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.

PyPI version Python 3.9+ License: MIT Tests


Why WassersteinLoss?

Cross-entropy and KL divergence have a fundamental flaw: their gradients vanish when the model and target distributions don't overlap. This causes:

  • Mode collapse in GANs and generative models
  • Overconfident, poorly calibrated probability outputs
  • Brittle behaviour under distribution shift

The Wasserstein-2 distance solves all three. It defines a geometric distance between distributions using the ground metric of the feature space:

W₂(μ, ν) = inf_{γ ∈ Π(μ,ν)} ∫ ‖x - y‖² dγ(x, y)

It always has meaningful gradients, naturally respects the geometry of your output space, and produces smooth, interference-free learning signals.


Installation

pip install otloss

Or from source:

git clone https://github.com/Maqbool61/otloss.git
cd otloss
pip install -e ".[dev]"

Requirements: Python ≥ 3.9, PyTorch ≥ 2.0


Quick start

from otloss import WassersteinLoss

# Drop-in replacement for nn.MSELoss
criterion = WassersteinLoss(p=2, blur=0.05)

pred   = torch.randn(32, 100, 2, requires_grad=True)  # (batch, particles, dim)
target = torch.randn(32, 100, 2)

loss = criterion(pred, target)
loss.backward()   # gradients flow through pred

Layered API

High-level (nn.Module)

from otloss import WassersteinLoss, SlicedWassersteinLoss
from otloss.losses import WassersteinGANLoss

# Exact Wasserstein via Sinkhorn (best accuracy)
criterion = WassersteinLoss(
    p=2,           # Wasserstein order (1 or 2)
    blur=0.05,     # entropic regularisation ε = blur²
    max_iter=100,  # Sinkhorn iterations
    debias=True,   # Sinkhorn divergence debiasing
    reduction="mean",
)

# Fast O(n log n) approximation via random projections
criterion = SlicedWassersteinLoss(
    n_projections=200,
    p=2,
)

# WGAN-GP critic/generator losses
criterion = WassersteinGANLoss(gp_weight=10.0)
d_loss = criterion.critic_loss(real_scores, fake_scores)
gp     = criterion.gradient_penalty(critic, real, fake)
g_loss = criterion.generator_loss(fake_scores)

Functional API (low-level, full control)

from otloss import (
    otloss,      # full Wasserstein via Sinkhorn
    sliced_otloss,
    sinkhorn,              # raw Sinkhorn solver
    cost_matrix,           # ground cost C_{ij} = ‖xᵢ - yⱼ‖ᵖ
    dual_variables,        # Kantorovich dual potentials (f, g)
)

# Compute cost matrix
C = cost_matrix(x, y, p=2)          # (N, M)

# Run Sinkhorn and get dual potentials + transport cost
f, g, cost = sinkhorn(a, b, C, blur=0.05, debias=True)

# Recover soft transport plan P_{ij}
from otloss.utils import transport_plan
P = transport_plan(f, g, C, blur=0.05)  # (N, M)

# Wasserstein barycenter
from otloss.utils import wasserstein_barycenter_weights
barycenter = wasserstein_barycenter_weights(measures, weights=[0.3, 0.7], support=X)

Real-world use cases

1. GAN training without mode collapse

from otloss.losses import WassersteinGANLoss

criterion = WassersteinGANLoss(gp_weight=10.0)

# Critic update
c_loss = criterion.critic_loss(D(real), D(fake.detach()))
gp     = criterion.gradient_penalty(D, real, fake)
(c_loss + gp).backward()

# Generator update
g_loss = criterion.generator_loss(D(fake))
g_loss.backward()

Or even simpler — no critic needed:

criterion = WassersteinLoss(blur=0.05)

# Directly minimise W₂ between fake and real sample clouds
fake = G(noise)   # (B, N, D)
real = real_data  # (B, N, D)
loss = criterion(fake, real)
loss.backward()

2. LLM calibration (confidence matches accuracy)

from otloss import WassersteinLoss
from otloss.distributions import label_smoothed_weights

criterion = WassersteinLoss(p=2, blur=0.05)

# Class positions as 1-D support
support = torch.linspace(0, 1, n_classes).unsqueeze(-1)   # (K, 1)
support = support.unsqueeze(0).expand(B, -1, -1)           # (B, K, 1)

pred_weights = torch.softmax(logits, dim=-1)               # (B, K)
target_weights = label_smoothed_weights(y, n_classes)      # (B, K)

loss = criterion(support, support,
                 pred_weights=pred_weights,
                 target_weights=target_weights)

3. Drug molecule generation (diverse scaffolds)

from otloss import SlicedWassersteinLoss

# Measures structural diversity in 8-D property space
criterion = SlicedWassersteinLoss(n_projections=200)

generated = model.decode(z)          # (B, N, 8)  property vectors
reference  = real_molecules          # (B, N, 8)
loss = criterion(generated, reference)
loss.backward()
# → model covers the full pharmacological distribution

4. Financial time-series generation (fat tails)

criterion = WassersteinLoss(p=2, blur=0.01)   # small blur → sharp tails

generated_returns = model(noise)   # (B, T, 1)
real_returns      = historical     # (B, T, 1)
loss = criterion(generated_returns, real_returns)
# → VaR/CVaR of generated paths matches real market data

5. RLHF reward model training

criterion = WassersteinLoss(p=2, blur=0.05, debias=True)

# Reward model outputs as distributions over preference scores
pred_rewards = reward_model(responses)    # (B, K, 1)
human_prefs  = preference_labels         # (B, K, 1)
loss = criterion(pred_rewards, human_prefs)
# → smoother reward landscape → better RLHF alignment

Mathematical background

Entropic regularisation (Sinkhorn)

Direct computation of W₂ is O(n³). We solve the entropy-regularised problem:

W_ε(a, b) = min_{P ≥ 0} ⟨C, P⟩ − ε · H(P)
             s.t.  P·1 = a,  Pᵀ·1 = b

Via Sinkhorn-Knopp iterations in log-domain (numerically stable):

fᵢ ← ε · log(aᵢ) − ε · LSE_j[(gⱼ − Cᵢⱼ) / ε]
gⱼ ← ε · log(bⱼ) − ε · LSE_i[(fᵢ − Cᵢⱼ) / ε]

Sinkhorn divergence (debiasing)

Raw Sinkhorn overestimates W due to entropic bias. We correct with:

S_ε(a, b) = W_ε(a, b) − ½W_ε(a, a) − ½W_ε(b, b)

This ensures S_ε(a, a) = 0 (positive definite) and S_ε → W as ε → 0.

Sliced Wasserstein Distance

Projects to 1-D random lines and uses the closed-form 1-D solution:

SW_p(μ, ν) = ( ∫_{S^{D-1}} W_p(θ#μ, θ#ν)^p dσ(θ) )^{1/p}

Exact W in 1-D reduces to: W_p = ‖sort(x) − sort(y)‖_p / N^{1/p}. Complexity: O(n log n) vs O(n³) for exact OT.


Choosing blur

Scenario Recommended blur
Tight distributions (calibration) 0.01 – 0.03
Moderate spread (generation) 0.05 – 0.1
Very spread / high-dimensional 0.1 – 0.5
Rule of thumb blur ≈ std(data) × 0.05

Smaller blur = more accurate but more Sinkhorn iterations. Blur annealing (enabled by default via scaling=0.5) starts coarse and refines automatically.


Running tests

pytest tests/ -v

Citation

If you use WassersteinLoss in your research:

@software{otloss_2026,
  author  = {Maqbool61},
  title   = {otloss: Optimal Transport objectives for PyTorch},
  year    = {2026},
  url     = {https://github.com/Maqbool61/otloss},
}

Key papers:

  • Villani (2008) — Optimal Transport: Old and New
  • Cuturi (2013) — Sinkhorn Distances: Lightspeed Computation of Optimal Transport
  • Arjovsky et al. (2017) — Wasserstein GAN
  • Gulrajani et al. (2017) — Improved Training of Wasserstein GANs
  • Feydy et al. (2019) — Interpolating between Optimal Transport and MMD using Sinkhorn Divergences

License

MIT © Maqbool61

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

otloss-0.1.0.tar.gz (27.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

otloss-0.1.0-py3-none-any.whl (16.8 kB view details)

Uploaded Python 3

File details

Details for the file otloss-0.1.0.tar.gz.

File metadata

  • Download URL: otloss-0.1.0.tar.gz
  • Upload date:
  • Size: 27.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for otloss-0.1.0.tar.gz
Algorithm Hash digest
SHA256 7453129d93b52dbd7370a4b010592501340fb8684d829a079e1e78e5458dc4bd
MD5 d91705e7f898aa7956fa48b1e9810fee
BLAKE2b-256 652de93c7e06aec04599611888f510752297abcaa7705f794e30df53c535515e

See more details on using hashes here.

File details

Details for the file otloss-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: otloss-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 16.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for otloss-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 eddd99f865fd53f2bcb032f74c81851c21e6223375a5b2ccef5482cc102e4412
MD5 6601cf3ea2d030e33b75d739eb58d1e5
BLAKE2b-256 af9413a3564f431e3030f0f258c45b64ce7b0cc565a899e7216b2af95525d501

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page