holonomy-lib 0.8.0__py3-none-any.whl
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.
- holonomy_explorer/__init__.py +1 -0
- holonomy_explorer/__main__.py +79 -0
- holonomy_explorer/batching.py +182 -0
- holonomy_explorer/common.py +187 -0
- holonomy_explorer/curvature.py +154 -0
- holonomy_explorer/datasets.py +346 -0
- holonomy_explorer/graphs.py +286 -0
- holonomy_explorer/server.py +172 -0
- holonomy_explorer/static/app.js +435 -0
- holonomy_explorer/static/charts.js +114 -0
- holonomy_explorer/static/index.html +45 -0
- holonomy_explorer/static/layout.js +85 -0
- holonomy_explorer/static/style.css +121 -0
- holonomy_explorer/static/view.js +195 -0
- holonomy_explorer/topology.py +118 -0
- holonomy_lib/__init__.py +9 -0
- holonomy_lib/_graph_utils.py +52 -0
- holonomy_lib/_quadrature.py +74 -0
- holonomy_lib/algebra/__init__.py +32 -0
- holonomy_lib/algebra/eigsh_sparse.py +251 -0
- holonomy_lib/algebra/lanczos.py +361 -0
- holonomy_lib/algebra/linear.py +180 -0
- holonomy_lib/algebra/solvers.py +547 -0
- holonomy_lib/convex/__init__.py +27 -0
- holonomy_lib/convex/least_squares.py +184 -0
- holonomy_lib/discrete_geometry/__init__.py +38 -0
- holonomy_lib/discrete_geometry/forman.py +242 -0
- holonomy_lib/discrete_geometry/ricci.py +855 -0
- holonomy_lib/experimental/__init__.py +13 -0
- holonomy_lib/experimental/heterogeneous_kappa.py +361 -0
- holonomy_lib/experimental/laplacian_eigenmaps.py +200 -0
- holonomy_lib/hyperbolic/__init__.py +27 -0
- holonomy_lib/hyperbolic/frechet_mean.py +381 -0
- holonomy_lib/hyperbolic/heat_kernel.py +608 -0
- holonomy_lib/hyperbolic/manifold_inner.py +81 -0
- holonomy_lib/info_geometry/__init__.py +64 -0
- holonomy_lib/info_geometry/divergences.py +224 -0
- holonomy_lib/info_geometry/fisher.py +179 -0
- holonomy_lib/lie/__init__.py +54 -0
- holonomy_lib/lie/so3.py +385 -0
- holonomy_lib/lie/spherical_harmonics.py +189 -0
- holonomy_lib/manifolds/__init__.py +53 -0
- holonomy_lib/manifolds/comparison.py +343 -0
- holonomy_lib/manifolds/fixed_rank.py +453 -0
- holonomy_lib/manifolds/lorentz.py +699 -0
- holonomy_lib/manifolds/lorentzian.py +567 -0
- holonomy_lib/manifolds/product.py +425 -0
- holonomy_lib/manifolds/spd.py +504 -0
- holonomy_lib/manifolds/stereographic.py +680 -0
- holonomy_lib/optimization/__init__.py +42 -0
- holonomy_lib/optimization/base.py +65 -0
- holonomy_lib/optimization/sgd.py +112 -0
- holonomy_lib/py.typed +0 -0
- holonomy_lib/sheaf/__init__.py +74 -0
- holonomy_lib/sheaf/graph_sheaf.py +352 -0
- holonomy_lib/sheaf/laplacian.py +529 -0
- holonomy_lib/simplicial/__init__.py +55 -0
- holonomy_lib/simplicial/_face_lookup.py +99 -0
- holonomy_lib/simplicial/complex.py +168 -0
- holonomy_lib/simplicial/sparse_complex.py +145 -0
- holonomy_lib/simplicial/vietoris_rips.py +277 -0
- holonomy_lib/spectral/__init__.py +55 -0
- holonomy_lib/spectral/diffusion_map.py +169 -0
- holonomy_lib/spectral/effective_resistance.py +199 -0
- holonomy_lib/spectral/embedding.py +131 -0
- holonomy_lib/spectral/heat_kernel.py +236 -0
- holonomy_lib/spectral/laplacian.py +441 -0
- holonomy_lib/spectral/magnetic.py +351 -0
- holonomy_lib/spectral/spectral_dimension.py +104 -0
- holonomy_lib/tensor_calculus/__init__.py +23 -0
- holonomy_lib/tensor_calculus/decomposition.py +208 -0
- holonomy_lib/topology/__init__.py +28 -0
- holonomy_lib/topology/_filtration.py +147 -0
- holonomy_lib/topology/_reduction.py +227 -0
- holonomy_lib/topology/hodge.py +250 -0
- holonomy_lib/topology/persistent_homology.py +301 -0
- holonomy_lib-0.8.0.dist-info/METADATA +161 -0
- holonomy_lib-0.8.0.dist-info/RECORD +82 -0
- holonomy_lib-0.8.0.dist-info/WHEEL +5 -0
- holonomy_lib-0.8.0.dist-info/entry_points.txt +2 -0
- holonomy_lib-0.8.0.dist-info/licenses/LICENSE.md +105 -0
- holonomy_lib-0.8.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# SPDX-License-Identifier: FSL-1.1-ALv2
|
|
2
|
+
# Copyright 2026 Synoros
|
|
3
|
+
|
|
4
|
+
"""holonomy_lib.info_geometry: information-geometric primitives.
|
|
5
|
+
|
|
6
|
+
Information geometry studies probability distributions as points on a
|
|
7
|
+
Riemannian manifold whose metric is the Fisher information. Three
|
|
8
|
+
families of primitives appear here:
|
|
9
|
+
|
|
10
|
+
Bregman divergences: `bregman_divergence(p, q, potential)`.
|
|
11
|
+
The asymmetric "distance" induced by a convex potential function.
|
|
12
|
+
Generalizes squared-Euclidean (potential = ½‖·‖²) and Itakura-
|
|
13
|
+
Saito (potential = −log(·)) and Kullback-Leibler (potential =
|
|
14
|
+
Σ p log p, the negative entropy). Banerjee et al. (2005) is the
|
|
15
|
+
canonical reference.
|
|
16
|
+
|
|
17
|
+
KL divergences: closed forms for the common exponential
|
|
18
|
+
families:
|
|
19
|
+
`kl_divergence_categorical(p, q)`: discrete distributions.
|
|
20
|
+
`kl_divergence_gaussian(mu_p, Sigma_p, mu_q, Sigma_q)`: multi-
|
|
21
|
+
variate Gaussians.
|
|
22
|
+
|
|
23
|
+
Fisher metric: the Riemannian inner product on the statistical
|
|
24
|
+
manifold. For q near p, KL(p || q) is half the squared length of
|
|
25
|
+
q - p in this metric. Plus `natural_gradient`, the steepest-descent
|
|
26
|
+
direction under this metric. See Amari (1998).
|
|
27
|
+
`fisher_information_categorical(p)`: diagonal Fisher on the simplex.
|
|
28
|
+
`fisher_information_gaussian_mean(Sigma)`: Fisher for the mean parameter.
|
|
29
|
+
`natural_gradient(grad, fisher_matrix)`: F^{-1} · grad.
|
|
30
|
+
|
|
31
|
+
Each primitive takes a leading batch dimension and cites its source.
|
|
32
|
+
|
|
33
|
+
References:
|
|
34
|
+
Amari, S.-I. (1998). Natural gradient works efficiently in learning.
|
|
35
|
+
Neural Computation 10(2):251–276.
|
|
36
|
+
Amari, S.-I. (2016). Information Geometry and Its Applications.
|
|
37
|
+
Applied Mathematical Sciences 194, Springer. The textbook.
|
|
38
|
+
Banerjee, A., Merugu, S., Dhillon, I. S., Ghosh, J. (2005).
|
|
39
|
+
Clustering with Bregman divergences. JMLR 6:1705–1749.
|
|
40
|
+
Martens, J. (2020). New insights and perspectives on the natural
|
|
41
|
+
gradient method. JMLR 21(146):1–76.
|
|
42
|
+
Nielsen, F. (2020). An elementary introduction to information
|
|
43
|
+
geometry. Entropy 22(10):1100.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from holonomy_lib.info_geometry.divergences import (
|
|
47
|
+
bregman_divergence,
|
|
48
|
+
kl_divergence_categorical,
|
|
49
|
+
kl_divergence_gaussian,
|
|
50
|
+
)
|
|
51
|
+
from holonomy_lib.info_geometry.fisher import (
|
|
52
|
+
fisher_information_categorical,
|
|
53
|
+
fisher_information_gaussian_mean,
|
|
54
|
+
natural_gradient,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"bregman_divergence",
|
|
59
|
+
"fisher_information_categorical",
|
|
60
|
+
"fisher_information_gaussian_mean",
|
|
61
|
+
"kl_divergence_categorical",
|
|
62
|
+
"kl_divergence_gaussian",
|
|
63
|
+
"natural_gradient",
|
|
64
|
+
]
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# SPDX-License-Identifier: FSL-1.1-ALv2
|
|
2
|
+
# Copyright 2026 Synoros
|
|
3
|
+
|
|
4
|
+
"""Bregman and KL divergences, batched over leading dimensions on any device.
|
|
5
|
+
|
|
6
|
+
A Bregman divergence is the asymmetric "distance" induced by a strictly
|
|
7
|
+
convex generator F : Ω ⊂ ℝᵈ → ℝ:
|
|
8
|
+
|
|
9
|
+
D_F(p ‖ q) = F(p) − F(q) − ⟨∇F(q), p − q⟩.
|
|
10
|
+
|
|
11
|
+
It is the gap between the function value at `p` and its first-order
|
|
12
|
+
Taylor approximation at `q`. Choices of `F` recover familiar
|
|
13
|
+
divergences:
|
|
14
|
+
|
|
15
|
+
F(x) = ½ ⟨x, x⟩ → squared Euclidean ½‖p−q‖²
|
|
16
|
+
F(x) = Σ x_i log x_i − x_i → generalized KL (Csiszár 1991)
|
|
17
|
+
(= KL on the simplex)
|
|
18
|
+
F(x) = − Σ log x_i → Itakura-Saito
|
|
19
|
+
F(X) = −log det X (for SPD matrices) → LogDet / Burg matrix divergence
|
|
20
|
+
|
|
21
|
+
This module provides:
|
|
22
|
+
|
|
23
|
+
bregman_divergence(p, q, potential)
|
|
24
|
+
The fully general form, parameterized by a callable `potential`
|
|
25
|
+
that returns (F(x), ∇F(x)) given x. Caller supplies the convex
|
|
26
|
+
function; we evaluate the Bregman formula and respect the leading
|
|
27
|
+
batch dimensions of `p`/`q`.
|
|
28
|
+
|
|
29
|
+
kl_divergence_categorical(p, q)
|
|
30
|
+
KL(p ‖ q) for discrete distributions on the simplex, with the
|
|
31
|
+
0·log 0 = 0 convention. On the simplex this equals `bregman_divergence`
|
|
32
|
+
with `potential = (x · log x − x).sum()`, and it is cheaper. The
|
|
33
|
+
Bregman form also adds Σq − Σp, which vanishes only when both sum to one.
|
|
34
|
+
|
|
35
|
+
Autograd note: `xlogy` preserves zero-mass terms and returns
|
|
36
|
+
`+inf` when `p_i > 0` and `q_i = 0` (support mismatch).
|
|
37
|
+
Gradients through near-zero bins are not numerically reliable;
|
|
38
|
+
use only for inference unless you're sure about the support.
|
|
39
|
+
|
|
40
|
+
kl_divergence_gaussian(mu_p, Sigma_p, mu_q, Sigma_q)
|
|
41
|
+
KL between two multivariate Gaussians:
|
|
42
|
+
|
|
43
|
+
KL = ½ ( tr(Σ_q⁻¹ Σ_p) + (μ_q − μ_p)ᵀ Σ_q⁻¹ (μ_q − μ_p)
|
|
44
|
+
− d + log det Σ_q − log det Σ_p ).
|
|
45
|
+
|
|
46
|
+
Uses Cholesky-based solves for stability.
|
|
47
|
+
|
|
48
|
+
References:
|
|
49
|
+
Bregman, L. M. (1967). The relaxation method of finding the common
|
|
50
|
+
point of convex sets ... USSR Comp. Math. Math. Phys. 7(3):200-217.
|
|
51
|
+
The original definition.
|
|
52
|
+
Banerjee, A., Merugu, S., Dhillon, I. S., Ghosh, J. (2005).
|
|
53
|
+
Clustering with Bregman divergences. JMLR 6:1705-1749, §2.
|
|
54
|
+
Csiszár, I. (1991). Why least squares and maximum entropy?
|
|
55
|
+
An axiomatic approach to inference for linear inverse problems.
|
|
56
|
+
Ann. Stat. 19(4):2032-2066. Bregman ↔ generalized KL connection.
|
|
57
|
+
Amari, S.-I. (2016). Information Geometry and Its Applications, §1.5.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
from __future__ import annotations
|
|
61
|
+
|
|
62
|
+
from collections.abc import Callable
|
|
63
|
+
|
|
64
|
+
import torch
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def bregman_divergence(
|
|
68
|
+
p: torch.Tensor,
|
|
69
|
+
q: torch.Tensor,
|
|
70
|
+
potential: Callable[[torch.Tensor], tuple[torch.Tensor, torch.Tensor]],
|
|
71
|
+
) -> torch.Tensor:
|
|
72
|
+
"""Bregman divergence `D_F(p ‖ q) = F(p) − F(q) − ⟨∇F(q), p − q⟩`.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
p, q: (B, d) tensors. Must broadcast against each other; output
|
|
76
|
+
has the broadcast batch shape.
|
|
77
|
+
potential: callable taking `x: (..., d)` and returning
|
|
78
|
+
`(F(x): (...,), grad_F(x): (..., d))`. Both outputs must
|
|
79
|
+
preserve the leading dims of the input. The caller is
|
|
80
|
+
responsible for choosing a strictly convex `F` and confirming
|
|
81
|
+
`p`, `q` live in its domain.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
D_F(p ‖ q): (B,) divergence values. Non-negative when F is
|
|
85
|
+
strictly convex (Bregman 1967); equals zero iff p = q.
|
|
86
|
+
|
|
87
|
+
Example (squared Euclidean):
|
|
88
|
+
>>> def half_norm_sq(x):
|
|
89
|
+
... return 0.5 * (x * x).sum(dim=-1), x
|
|
90
|
+
>>> bregman_divergence(p, q, half_norm_sq)
|
|
91
|
+
# = ½ ‖p − q‖²
|
|
92
|
+
|
|
93
|
+
References:
|
|
94
|
+
Bregman (1967).
|
|
95
|
+
Banerjee et al. (2005), §2.
|
|
96
|
+
"""
|
|
97
|
+
if p.shape[-1] != q.shape[-1]:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"p and q must have matching last dim; got p.shape[-1]="
|
|
100
|
+
f"{p.shape[-1]} vs q.shape[-1]={q.shape[-1]}"
|
|
101
|
+
)
|
|
102
|
+
# Only the gradient at q enters the formula, so the potential's gradient
|
|
103
|
+
# at p is discarded.
|
|
104
|
+
F_p, _ = potential(p)
|
|
105
|
+
F_q, grad_F_q = potential(q)
|
|
106
|
+
# ⟨∇F(q), p − q⟩, summed over the feature dim so it has the batch shape.
|
|
107
|
+
inner = (grad_F_q * (p - q)).sum(dim=-1)
|
|
108
|
+
return F_p - F_q - inner
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def kl_divergence_categorical(
|
|
112
|
+
p: torch.Tensor,
|
|
113
|
+
q: torch.Tensor,
|
|
114
|
+
) -> torch.Tensor:
|
|
115
|
+
"""KL divergence between two discrete distributions on the simplex.
|
|
116
|
+
|
|
117
|
+
KL(p ‖ q) = Σ_i p_i · (log p_i − log q_i).
|
|
118
|
+
|
|
119
|
+
Uses the convention 0 · log(0/x) = 0 (continuous limit). Both `p`
|
|
120
|
+
and `q` must be on the simplex (non-negative, sum to 1 along the
|
|
121
|
+
last dim); we do not enforce this, so it is a precondition.
|
|
122
|
+
Gradients at zero probabilities need not be finite; differentiate
|
|
123
|
+
only where both distributions have strictly positive entries.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
p, q: (B, k) probability tensors. Last dim is the support size.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
KL: (B,) divergence values, non-negative (Gibbs inequality).
|
|
130
|
+
|
|
131
|
+
References:
|
|
132
|
+
Cover, T. M., Thomas, J. A. (2006). Elements of Information
|
|
133
|
+
Theory, 2nd ed., §2.3.
|
|
134
|
+
Amari (2016), §2.4.
|
|
135
|
+
"""
|
|
136
|
+
if p.shape[-1] != q.shape[-1]:
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"p and q must have matching last dim; got {p.shape[-1]} vs "
|
|
139
|
+
f"{q.shape[-1]}"
|
|
140
|
+
)
|
|
141
|
+
# p log(p / q) is split into two xlogy terms. xlogy(p, x) is 0 wherever
|
|
142
|
+
# p is 0, so zero-mass bins drop out without a 0 * log 0 nan. No clamp
|
|
143
|
+
# is applied, so tiny positive p keep their exact p log p. A bin with
|
|
144
|
+
# p_i > 0 and q_i = 0 gives +inf, as the module note says.
|
|
145
|
+
return (torch.special.xlogy(p, p) - torch.special.xlogy(p, q)).sum(dim=-1)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def kl_divergence_gaussian(
|
|
149
|
+
mu_p: torch.Tensor,
|
|
150
|
+
Sigma_p: torch.Tensor,
|
|
151
|
+
mu_q: torch.Tensor,
|
|
152
|
+
Sigma_q: torch.Tensor,
|
|
153
|
+
) -> torch.Tensor:
|
|
154
|
+
"""KL divergence between two multivariate Gaussians.
|
|
155
|
+
|
|
156
|
+
KL(N(μ_p, Σ_p) ‖ N(μ_q, Σ_q))
|
|
157
|
+
= ½ ( tr(Σ_q⁻¹ Σ_p)
|
|
158
|
+
+ (μ_q − μ_p)ᵀ Σ_q⁻¹ (μ_q − μ_p)
|
|
159
|
+
− d
|
|
160
|
+
+ log det Σ_q − log det Σ_p ).
|
|
161
|
+
|
|
162
|
+
Stability: we factorize Σ_q via Cholesky once and reuse the
|
|
163
|
+
factorization for both the trace term `tr(Σ_q⁻¹ Σ_p)` and the
|
|
164
|
+
Mahalanobis term `(μ_q − μ_p)ᵀ Σ_q⁻¹ (μ_q − μ_p)`. `log det Σ_q`
|
|
165
|
+
comes for free from the Cholesky diagonal. For `Σ_p` we just need
|
|
166
|
+
`log det Σ_p`, also via Cholesky. A covariance that is not SPD makes
|
|
167
|
+
`torch.linalg.cholesky` raise, so bad input fails instead of returning
|
|
168
|
+
a number.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
mu_p, mu_q: (..., d) means.
|
|
172
|
+
Sigma_p, Sigma_q: (..., d, d) covariances. Must be SPD.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
KL: (...,) divergence values, non-negative.
|
|
176
|
+
|
|
177
|
+
References:
|
|
178
|
+
Amari (2016), §2.4; exponential families.
|
|
179
|
+
Petersen, K. B., Pedersen, M. S. (2012). The Matrix Cookbook,
|
|
180
|
+
eq. 380; Gaussian-Gaussian KL.
|
|
181
|
+
"""
|
|
182
|
+
d = mu_p.shape[-1]
|
|
183
|
+
if mu_q.shape[-1] != d:
|
|
184
|
+
raise ValueError(
|
|
185
|
+
f"mu_q's last dim must match mu_p's d={d}; "
|
|
186
|
+
f"got mu_q.shape={tuple(mu_q.shape)}"
|
|
187
|
+
)
|
|
188
|
+
if Sigma_p.shape[-1] != d or Sigma_p.shape[-2] != d:
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"Sigma_p must be (..., d, d) with d={d}; "
|
|
191
|
+
f"got {tuple(Sigma_p.shape)}"
|
|
192
|
+
)
|
|
193
|
+
if Sigma_q.shape[-1] != d or Sigma_q.shape[-2] != d:
|
|
194
|
+
raise ValueError(
|
|
195
|
+
f"Sigma_q must be (..., d, d) with d={d}; "
|
|
196
|
+
f"got {tuple(Sigma_q.shape)}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
L_q = torch.linalg.cholesky(Sigma_q)
|
|
200
|
+
# Σ_q⁻¹ Σ_p through the Cholesky factor of Σ_q: two triangular solves,
|
|
201
|
+
# so no explicit inverse of Σ_q is formed.
|
|
202
|
+
Sigma_q_inv_Sigma_p = torch.cholesky_solve(Sigma_p, L_q)
|
|
203
|
+
trace_term = torch.diagonal(
|
|
204
|
+
Sigma_q_inv_Sigma_p, dim1=-2, dim2=-1,
|
|
205
|
+
).sum(dim=-1)
|
|
206
|
+
|
|
207
|
+
diff = (mu_q - mu_p).unsqueeze(dim=-1) # (..., d, 1)
|
|
208
|
+
# Σ_q⁻¹ Δμ via cholesky_solve; then ⟨Δμ, ·⟩ is a dot product.
|
|
209
|
+
Sigma_q_inv_diff = torch.cholesky_solve(diff, L_q).squeeze(dim=-1)
|
|
210
|
+
mahalanobis = ((mu_q - mu_p) * Sigma_q_inv_diff).sum(dim=-1)
|
|
211
|
+
|
|
212
|
+
# log det Σ = 2 Σ log diag(L) for the Cholesky factor.
|
|
213
|
+
log_det_Sigma_q = 2.0 * torch.log(
|
|
214
|
+
torch.diagonal(L_q, dim1=-2, dim2=-1)
|
|
215
|
+
).sum(dim=-1)
|
|
216
|
+
L_p = torch.linalg.cholesky(Sigma_p)
|
|
217
|
+
log_det_Sigma_p = 2.0 * torch.log(
|
|
218
|
+
torch.diagonal(L_p, dim1=-2, dim2=-1)
|
|
219
|
+
).sum(dim=-1)
|
|
220
|
+
|
|
221
|
+
return 0.5 * (
|
|
222
|
+
trace_term + mahalanobis - float(d)
|
|
223
|
+
+ log_det_Sigma_q - log_det_Sigma_p
|
|
224
|
+
)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# SPDX-License-Identifier: FSL-1.1-ALv2
|
|
2
|
+
# Copyright 2026 Synoros
|
|
3
|
+
|
|
4
|
+
"""Fisher information metric + natural-gradient primitives.
|
|
5
|
+
|
|
6
|
+
The Fisher information matrix is the canonical Riemannian metric on a
|
|
7
|
+
parametric statistical manifold:
|
|
8
|
+
|
|
9
|
+
g_{ij}(θ) = E_{p_θ}[ (∂_i log p_θ)(∂_j log p_θ) ]
|
|
10
|
+
= − E_{p_θ}[ ∂_i ∂_j log p_θ ].
|
|
11
|
+
|
|
12
|
+
It is the second-order Taylor coefficient of KL near the diagonal:
|
|
13
|
+
|
|
14
|
+
KL(p_θ ‖ p_{θ + dθ}) = ½ dθᵀ F(θ) dθ + O(‖dθ‖³).
|
|
15
|
+
|
|
16
|
+
Information geometry studies the geodesics of this metric. In
|
|
17
|
+
optimization, the natural gradient `F(θ)^{−1} ∇L` is the steepest-descent
|
|
18
|
+
direction under this metric. The metric does not depend on how θ is
|
|
19
|
+
parameterized, so the direction does not either (Amari 1998).
|
|
20
|
+
|
|
21
|
+
Two closed-form Fisher matrices are exposed for the most common
|
|
22
|
+
families; users with a custom likelihood can compute their own Fisher
|
|
23
|
+
either analytically or via `torch.autograd` over `log p`.
|
|
24
|
+
|
|
25
|
+
fisher_information_categorical(p)
|
|
26
|
+
Diagonal Fisher on the simplex, g_ii(p) = 1/p_i. Recovers the
|
|
27
|
+
Hellinger-Bhattacharyya geometry whose geodesic distance is twice
|
|
28
|
+
the Hellinger angle.
|
|
29
|
+
|
|
30
|
+
fisher_information_gaussian_mean(Sigma)
|
|
31
|
+
The Fisher information for the mean of N(μ, Σ) at fixed Σ. The
|
|
32
|
+
closed form is g(μ) = Σ^{−1}, which makes the Fisher-metric
|
|
33
|
+
geodesics between two means coincide with the Mahalanobis-distance
|
|
34
|
+
straight lines for that fixed Σ.
|
|
35
|
+
|
|
36
|
+
natural_gradient(grad, fisher_matrix)
|
|
37
|
+
Apply the Fisher inverse to a Euclidean gradient via a Cholesky
|
|
38
|
+
solve (no explicit inverse). The output is the steepest-descent
|
|
39
|
+
direction under the Fisher metric.
|
|
40
|
+
|
|
41
|
+
References:
|
|
42
|
+
Amari, S.-I. (1998). Natural gradient works efficiently in learning.
|
|
43
|
+
Neural Computation 10(2):251–276. The natural-gradient paper.
|
|
44
|
+
Amari, S.-I. (2016). Information Geometry and Its Applications.
|
|
45
|
+
Applied Mathematical Sciences 194, Springer. §2.5, §4.
|
|
46
|
+
Martens, J. (2020). New insights and perspectives on the natural
|
|
47
|
+
gradient method. JMLR 21(146):1–76. Modern revisit.
|
|
48
|
+
Petersen, K. B., Pedersen, M. S. (2012). The Matrix Cookbook,
|
|
49
|
+
§6 (Statistics); Fisher information for the Gaussian family.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import torch
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def fisher_information_categorical(p: torch.Tensor) -> torch.Tensor:
|
|
58
|
+
"""Fisher information metric on the simplex Δ^{k−1}.
|
|
59
|
+
|
|
60
|
+
For p in the open simplex, the Fisher information metric is the
|
|
61
|
+
diagonal matrix
|
|
62
|
+
|
|
63
|
+
g_ij(p) = δ_ij / p_i.
|
|
64
|
+
|
|
65
|
+
This is the metric induced by the KL divergence's second-order
|
|
66
|
+
Taylor expansion: KL(p ‖ p + δ) ≈ ½ Σ_i δ_i² / p_i.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
p: (B, k) probability vectors on the open simplex. Entries should
|
|
70
|
+
be positive. Metric coefficients diverge as p_i approaches zero,
|
|
71
|
+
but the boundary is at finite Fisher distance. For example, the
|
|
72
|
+
Bernoulli distance from p=1/2 to p=0 is pi/2.
|
|
73
|
+
Entries below `1e-9` are clamped up to `1e-9` in the input dtype
|
|
74
|
+
(float16 is a special case, see the body).
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Fisher matrix: (B, k, k) diagonal SPD.
|
|
78
|
+
|
|
79
|
+
References:
|
|
80
|
+
Amari (2016), §2.4 example 4.
|
|
81
|
+
Nielsen, F. (2020). An elementary introduction to information
|
|
82
|
+
geometry. Entropy 22(10):1100.
|
|
83
|
+
"""
|
|
84
|
+
if p.ndim < 1:
|
|
85
|
+
raise ValueError(f"p must have at least 1 dim; got p.shape={tuple(p.shape)}")
|
|
86
|
+
# The clamp is what keeps 1/p finite at the boundary. The floor is the
|
|
87
|
+
# library-wide convention `numerical_floor_convention` in
|
|
88
|
+
# notes/magic_numbers.md, not a per-call knob. Keeping p in the interior
|
|
89
|
+
# is the caller's job; a clamped entry gives a large but finite value.
|
|
90
|
+
# float16 cannot represent 1e-9 (it rounds to 0), so the floor is raised
|
|
91
|
+
# to the dtype's smallest normal number when that is larger. Every other
|
|
92
|
+
# float dtype keeps the 1e-9 convention.
|
|
93
|
+
p_safe = p.clamp(min=max(1e-9, torch.finfo(p.dtype).tiny))
|
|
94
|
+
inv_p = torch.reciprocal(p_safe)
|
|
95
|
+
return torch.diag_embed(inv_p)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def fisher_information_gaussian_mean(Sigma: torch.Tensor) -> torch.Tensor:
|
|
99
|
+
"""Fisher information for the mean μ of N(μ, Σ), at fixed Σ.
|
|
100
|
+
|
|
101
|
+
Standard result (Amari 2016, eq. 2.43): for the Gaussian family
|
|
102
|
+
parameterized by the mean with covariance held fixed,
|
|
103
|
+
|
|
104
|
+
g(μ) = Σ^{−1}.
|
|
105
|
+
|
|
106
|
+
The Fisher-geodesic distance between two means μ_1, μ_2 is then
|
|
107
|
+
`sqrt((μ_1 − μ_2)ᵀ Σ^{−1} (μ_1 − μ_2))`, i.e. the Mahalanobis
|
|
108
|
+
distance for the given Σ.
|
|
109
|
+
|
|
110
|
+
Computed from a Cholesky factor with `cholesky_inverse`, not
|
|
111
|
+
`linalg.inv`. A matrix that is not SPD makes `cholesky` raise.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
Sigma: (B, d, d) SPD covariance matrices.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Fisher matrix: (B, d, d) SPD.
|
|
118
|
+
|
|
119
|
+
References:
|
|
120
|
+
Amari (2016), §2.4 example 5.
|
|
121
|
+
Petersen-Pedersen (2012), §6.
|
|
122
|
+
"""
|
|
123
|
+
if Sigma.ndim < 2 or Sigma.shape[-1] != Sigma.shape[-2]:
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"Sigma must be (..., d, d); got Sigma.shape={tuple(Sigma.shape)}"
|
|
126
|
+
)
|
|
127
|
+
# Σ is SPD, so factor it once and invert from the factor. This skips
|
|
128
|
+
# the pivoting an LU inverse does, and kl_divergence_gaussian uses the
|
|
129
|
+
# same factorization.
|
|
130
|
+
L = torch.linalg.cholesky(Sigma)
|
|
131
|
+
return torch.cholesky_inverse(L)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def natural_gradient(
|
|
135
|
+
grad: torch.Tensor,
|
|
136
|
+
fisher_matrix: torch.Tensor,
|
|
137
|
+
) -> torch.Tensor:
|
|
138
|
+
"""Natural gradient: F(θ)^{−1} · ∇L.
|
|
139
|
+
|
|
140
|
+
The natural gradient (Amari 1998) is the steepest-descent direction
|
|
141
|
+
on a statistical manifold under the Fisher metric. Unlike the
|
|
142
|
+
Euclidean gradient, the direction does not change under a smooth
|
|
143
|
+
reparameterization of θ, since the metric is the same geometric
|
|
144
|
+
object in every coordinate system. That removes one source of
|
|
145
|
+
parameterization-dependent behavior. It does not make an
|
|
146
|
+
ill-conditioned F any better conditioned.
|
|
147
|
+
|
|
148
|
+
Implementation: solve `F · d = grad` through a Cholesky factor of F,
|
|
149
|
+
rather than forming `inv(F) @ grad`. The solve inherits whatever
|
|
150
|
+
conditioning F has, without the extra rounding from an explicit inverse.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
grad: (B, d) Euclidean gradient at θ.
|
|
154
|
+
fisher_matrix: (B, d, d) Fisher information matrix at θ, SPD.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
(B, d) natural gradient.
|
|
158
|
+
|
|
159
|
+
References:
|
|
160
|
+
Amari (1998).
|
|
161
|
+
Martens (2020), §2; modern derivation.
|
|
162
|
+
"""
|
|
163
|
+
if fisher_matrix.ndim < 2:
|
|
164
|
+
raise ValueError(
|
|
165
|
+
f"fisher_matrix must be (..., d, d) with at least 2 dims; "
|
|
166
|
+
f"got fisher_matrix.shape={tuple(fisher_matrix.shape)}"
|
|
167
|
+
)
|
|
168
|
+
if grad.shape[-1] != fisher_matrix.shape[-1]:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
f"grad last dim {grad.shape[-1]} must match fisher_matrix "
|
|
171
|
+
f"last dim {fisher_matrix.shape[-1]}"
|
|
172
|
+
)
|
|
173
|
+
if fisher_matrix.shape[-1] != fisher_matrix.shape[-2]:
|
|
174
|
+
raise ValueError(
|
|
175
|
+
f"fisher_matrix must be (..., d, d); got "
|
|
176
|
+
f"fisher_matrix.shape={tuple(fisher_matrix.shape)}"
|
|
177
|
+
)
|
|
178
|
+
L = torch.linalg.cholesky(fisher_matrix)
|
|
179
|
+
return torch.cholesky_solve(grad.unsqueeze(dim=-1), L).squeeze(dim=-1)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# SPDX-License-Identifier: FSL-1.1-ALv2
|
|
2
|
+
# Copyright 2026 Synoros
|
|
3
|
+
|
|
4
|
+
"""holonomy_lib.lie : Lie group primitives for SO(3) and the 2-sphere.
|
|
5
|
+
|
|
6
|
+
The Lie group `SO(3)` is the group of orientation-preserving
|
|
7
|
+
rotations of R³; its Lie algebra `so(3)` is the 3-dim space of
|
|
8
|
+
skew-symmetric matrices (equivalently, angular-velocity vectors).
|
|
9
|
+
This module ships the standard exp/log/random/compose primitives
|
|
10
|
+
plus real spherical harmonics, the standard basis for functions on the
|
|
11
|
+
unit sphere that rotate in a known way under SO(3).
|
|
12
|
+
|
|
13
|
+
Currently exposed:
|
|
14
|
+
so3.axis_angle_to_matrix(axis, angle) : Rodrigues formula.
|
|
15
|
+
so3.matrix_to_axis_angle(R) : inverse log map.
|
|
16
|
+
so3.so3_exp(omega) : matrix exp on so(3).
|
|
17
|
+
so3.so3_log(R) : matrix log on SO(3).
|
|
18
|
+
so3.random_so3(batch_size, generator) : Haar-uniform via
|
|
19
|
+
quaternion sampling.
|
|
20
|
+
so3.compose(R1, R2) : group product (matmul).
|
|
21
|
+
real_spherical_harmonics(directions, l_max)
|
|
22
|
+
: closed-form Y_lm for
|
|
23
|
+
l ≤ 4 evaluated at
|
|
24
|
+
(B, 3) direction vectors
|
|
25
|
+
(normalized internally).
|
|
26
|
+
|
|
27
|
+
Planned, not implemented yet:
|
|
28
|
+
Wigner-D matrices (real basis) for higher-l rotation actions on
|
|
29
|
+
spherical-harmonic features.
|
|
30
|
+
Clebsch-Gordan coefficients for SO(3) tensor products (l₁ ⊗ l₂ → L).
|
|
31
|
+
SE(3), SU(2), SL(n) primitives.
|
|
32
|
+
|
|
33
|
+
References:
|
|
34
|
+
Hall, B. C. (2015). Lie Groups, Lie Algebras, and Representations,
|
|
35
|
+
2nd ed. Springer GTM 222. The canonical reference for matrix Lie
|
|
36
|
+
groups; §3.1 covers SO(3) and its Lie algebra.
|
|
37
|
+
Edmonds, A. R. (1957). Angular Momentum in Quantum Mechanics.
|
|
38
|
+
Princeton University Press. Real and complex spherical harmonics
|
|
39
|
+
+ Wigner D-matrices.
|
|
40
|
+
Shoemake, K. (1992). Uniform random rotations. In Graphics Gems
|
|
41
|
+
III, pp. 124–132. Academic Press. The quaternion-from-3-uniforms
|
|
42
|
+
construction used by `random_so3`.
|
|
43
|
+
Cohen, T. S., Geiger, M., Köhler, J., Welling, M. (2018).
|
|
44
|
+
Spherical CNNs. ICLR. Modern application of these primitives in
|
|
45
|
+
SO(3)-equivariant deep learning.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from holonomy_lib.lie import so3
|
|
49
|
+
from holonomy_lib.lie.spherical_harmonics import real_spherical_harmonics
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"real_spherical_harmonics",
|
|
53
|
+
"so3",
|
|
54
|
+
]
|