Stochastic Morphisms¶
The FinStoch Category¶
FinStoch is the category of finite sets and stochastic maps (Markov kernels):
- Objects: finite sets (as in the base category)
- Morphisms \(A \to B\): stochastic matrices of shape \((|A|, |B|)\) with rows summing to 1
- Composition: standard matrix multiplication
- Identity: Kronecker delta
FinStoch is the Kleisli category of the Giry monad, restricted to finite sets.
from quivers.stochastic import StochasticMorphism, FinStoch
from quivers.core.objects import FinSet
import torch
X = FinSet(name="X", cardinality=3)
Y = FinSet(name="Y", cardinality=4)
# Create a stochastic morphism (Markov kernel)
f = StochasticMorphism(X, Y)
# Check that rows sum to 1
assert (f.tensor.sum(dim=-1) - 1.0).abs().max() < 1e-5
# Composition preserves row-stochasticity
g = StochasticMorphism(Y, FinSet(name="Z", cardinality=5))
h = f >> g
assert (h.tensor.sum(dim=-1) - 1.0).abs().max() < 1e-5
MarkovAlgebra¶
The enrichment for FinStoch uses the Markov algebra:
Composition of stochastic matrices uses this structure:
which is standard matrix multiplication.
import torch
from quivers.stochastic import MARKOV
assert MARKOV.name == "Markov"
# Composition uses sum-product (matrix mult)
f_tensor = torch.tensor([[0.7, 0.3], [0.4, 0.6]])
g_tensor = torch.tensor([[0.5, 0.5], [0.8, 0.2]])
composed = MARKOV.compose(f_tensor, g_tensor, n_contract=1)
expected = f_tensor @ g_tensor
assert torch.allclose(composed, expected)
Stochastic Morphisms¶
A stochastic morphism is a learnable Markov kernel parameterized by an unconstrained weight matrix and a softmax normalization:
from quivers.stochastic import StochasticMorphism
from quivers.core.objects import FinSet
X = FinSet(name="X", cardinality=4)
Y = FinSet(name="Y", cardinality=6)
f = StochasticMorphism(X, Y)
# Tensor is softmax applied to raw weights
# f.tensor has shape (4, 6) with rows summing to 1
assert f.tensor.shape == (4, 6)
assert (f.tensor.sum(dim=-1) - 1.0).abs().max() < 1e-5
# Gradient descent updates via raw weights
module = f.module()
params = list(module.parameters())
assert len(params) == 1 # one weight matrix
An alias CategoricalMorphism is provided with explicit categorical semantics.
Discretized Distribution Families¶
When the codomain is continuous (not a finite set), quivers provides discretized versions of standard distributions. These map a finite input set to a bucketed approximation of the distribution.
from quivers.stochastic.families import (
DiscretizedNormal,
DiscretizedLogitNormal,
DiscretizedBeta,
DiscretizedTruncatedNormal,
)
from quivers.core.objects import FinSet
X = FinSet(name="X", cardinality=10)
# Discretized normal: the codomain `FinSet` cardinality is the bin count.
Bins20 = FinSet(name="Bins20", cardinality=20)
normal = DiscretizedNormal(X, Bins20, low=0.0, high=1.0)
# Input-conditioned parameters via learned linear maps
# μ, σ computed from input
tensor = normal.tensor # (10, 20) stochastic matrix
# Discretized beta on (0, 1)
Bins15 = FinSet(name="Bins15", cardinality=15)
beta = DiscretizedBeta(X, Bins15)
Conditioning and Bayesian Updates¶
Bayesian conditioning via Bayes' rule. Given a joint distribution \(p(x, y)\) and observation \(\text{obs}(y)\), compute \(p(x | \text{obs}(y))\):
import torch
from quivers.core.objects import FinSet
from quivers.stochastic import StochasticMorphism, condition, ConditionedMorphism
X = FinSet(name="X", cardinality=3)
Y = FinSet(name="Y", cardinality=4)
# Prior: X → Y
prior = StochasticMorphism(X, Y)
# Evidence: a likelihood vector over Y (e.g., a soft observation "Y is unlikely to be 1")
evidence = torch.tensor([1.0, 0.0, 1.0, 1.0])
# Posterior: f|e(a, b) ∝ f(a, b) · e(b), row-renormalized
posterior = condition(prior, evidence)
assert isinstance(posterior, ConditionedMorphism)
assert (posterior.tensor.sum(dim=-1) - 1.0).abs().max() < 1e-5
Mixture Morphisms¶
Convex combination of stochastic morphisms:
import torch
from quivers.core.objects import FinSet
from quivers.stochastic import StochasticMorphism, mix, MixtureMorphism
X = FinSet(name="X", cardinality=4)
Y = FinSet(name="Y", cardinality=6)
f = StochasticMorphism(X, Y)
g = StochasticMorphism(X, Y)
# Mixture with a learnable weight (sigmoid of init_logit=0.0, so initial weight 0.5)
mixture = mix(f, g, learnable=True, init_logit=0.0)
assert isinstance(mixture, MixtureMorphism)
# Initial tensor is 0.5 * f + 0.5 * g
expected = 0.5 * f.tensor + 0.5 * g.tensor
assert torch.allclose(mixture.tensor, expected)
Factored Morphisms¶
Pointwise reweighting by a non-negative weight vector over the codomain:
import torch
from quivers.core.objects import FinSet
from quivers.stochastic import StochasticMorphism, factor, FactoredMorphism
X = FinSet(name="X", cardinality=4)
Y = FinSet(name="Y", cardinality=6)
f = StochasticMorphism(X, Y)
weights = torch.tensor([1.0, 0.5, 2.0, 1.0, 0.0, 1.0]) # over Y (|Y|=6)
# factor(f, w)(a, b) = f(a, b) · w(b)
factored = factor(f, weights)
assert isinstance(factored, FactoredMorphism)
# Note: result is unnormalized; use normalize() after
Normalization¶
Renormalize rows to sum to 1:
import torch
from quivers.core.objects import FinSet
from quivers.stochastic import StochasticMorphism, factor, normalize, NormalizedMorphism
X = FinSet(name="X", cardinality=4)
Y = FinSet(name="Y", cardinality=6)
weights = torch.tensor([1.0, 0.5, 2.0, 1.0, 0.0, 1.0])
factored = factor(StochasticMorphism(X, Y), weights)
# `factored` is a morphism whose tensor need not be row-stochastic
normalized = normalize(factored)
assert isinstance(normalized, NormalizedMorphism)
assert (normalized.tensor.sum(dim=-1) - 1.0).abs().max() < 1e-5
Queries: Probability and Expectation¶
Extract probabilities from stochastic morphisms:
import torch
from quivers.core.objects import FinSet
from quivers.stochastic import StochasticMorphism, prob, marginal_prob, expectation
X = FinSet(name="X", cardinality=4)
Y = FinSet(name="Y", cardinality=6)
f = StochasticMorphism(X, Y)
# Query: P(y=2 | x=0)
p = prob(f, torch.tensor([0]), torch.tensor([2])) # shape (1,)
assert (0 <= p).all() and (p <= 1).all()
# Marginal P(y) under a uniform prior over X
m = marginal_prob(f, torch.tensor([0, 1, 2, 3, 4, 5])) # |Y|=6
# Expectation of a real-valued function over the codomain, per domain element
values = torch.arange(6, dtype=torch.float)
expect = expectation(f, values) # shape (|X|,)
The Giry Monad and FinStoch¶
The Giry monad \(\mathcal{G}\) sends a space \(X\) to its space of probability measures. On finite sets, this becomes:
The Kleisli category of \(\mathcal{G}\) restricted to finite sets is FinStoch.
from quivers.core.objects import FinSet
from quivers.stochastic import StochasticMorphism
from quivers.stochastic.giry import GiryMonad, FinStoch
giry = GiryMonad()
# Unit η_X: X → X is the Kronecker delta (each element to its point mass)
X = FinSet(name="X", cardinality=3)
Y = FinSet(name="Y", cardinality=4)
unit_X = giry.unit(X)
# Kleisli composition of two stochastic morphisms via the monad
f = StochasticMorphism(X, Y)
g = StochasticMorphism(Y, FinSet(name="Z", cardinality=2))
h = giry.kleisli_compose(f, g)
# FinStoch is the Kleisli category of the Giry monad
fs = FinStoch()
h2 = fs.compose(f, g)
Stochastic morphisms are composed exactly as in FinStoch (matrix multiplication), not via Kleisli composition. They form a category in their own right.