hyperbolix 0.4.0__tar.gz → 0.4.2__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.
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/PKG-INFO +2 -2
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/README.md +1 -1
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/nn_layers/__init__.py +1 -2
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/nn_layers/hyperboloid_conv.py +70 -232
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/nn_layers/hyperboloid_linear.py +128 -70
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/nn_layers/hyperboloid_regression.py +3 -3
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/nn_layers/poincare_conv.py +26 -19
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/nn_layers/poincare_linear.py +37 -18
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/pyproject.toml +1 -1
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/__init__.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/distributions/__init__.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/distributions/_common.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/distributions/_wrapped_normal_base.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/distributions/uniform_poincare.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/distributions/wrapped_normal_hyperboloid.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/distributions/wrapped_normal_poincare.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/manifolds/__init__.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/manifolds/_base.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/manifolds/euclidean.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/manifolds/hyperboloid.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/manifolds/isometry_mappings.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/manifolds/poincare.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/manifolds/protocol.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/nn_layers/_helpers.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/nn_layers/hybrid_regularization.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/nn_layers/hyperboloid_activations.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/nn_layers/hyperboloid_attention.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/nn_layers/hyperboloid_core.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/nn_layers/hyperboloid_positional.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/nn_layers/hyperboloid_regularization.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/nn_layers/poincare_activations.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/nn_layers/poincare_regression.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/nn_layers/poincare_rl.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/optim/__init__.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/optim/_riemannian_base.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/optim/manifold_metadata.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/optim/riemannian_adam.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/optim/riemannian_sgd.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/utils/__init__.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/utils/helpers.py +0 -0
- {hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/utils/math_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: hyperbolix
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Hyperbolic Deep Learning in JAX
|
|
5
5
|
Keywords: hyperbolic,deep-learning,jax,flax,machine-learning,riemannian,poincare,hyperboloid,neural-networks,geometric-deep-learning
|
|
6
6
|
Author: Timo Klein, Thomas Lang
|
|
@@ -42,7 +42,7 @@ Pure JAX implementation of hyperbolic deep learning with manifold operations, ne
|
|
|
42
42
|
## Features
|
|
43
43
|
|
|
44
44
|
- 🌐 **3 Manifolds**: Euclidean, Poincaré Ball, Hyperboloid
|
|
45
|
-
- 🧠 **13+ Neural Network Layers**: Linear, convolutional
|
|
45
|
+
- 🧠 **13+ Neural Network Layers**: Linear, convolutional, regression
|
|
46
46
|
- ⚡ **4 Hyperbolic Activations**: ReLU, Leaky ReLU, Tanh, Swish
|
|
47
47
|
- 📈 **Riemannian Optimizers**: RAdam and RSGD with automatic manifold detection
|
|
48
48
|
- 🚀 **Pure JAX/Flax NNX**: vmap-native API, JIT-compatible (10-100x speedup)
|
|
@@ -12,7 +12,7 @@ Pure JAX implementation of hyperbolic deep learning with manifold operations, ne
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
14
|
- 🌐 **3 Manifolds**: Euclidean, Poincaré Ball, Hyperboloid
|
|
15
|
-
- 🧠 **13+ Neural Network Layers**: Linear, convolutional
|
|
15
|
+
- 🧠 **13+ Neural Network Layers**: Linear, convolutional, regression
|
|
16
16
|
- ⚡ **4 Hyperbolic Activations**: ReLU, Leaky ReLU, Tanh, Swish
|
|
17
17
|
- 📈 **Riemannian Optimizers**: RAdam and RSGD with automatic manifold detection
|
|
18
18
|
- 🚀 **Pure JAX/Flax NNX**: vmap-native API, JIT-compatible (10-100x speedup)
|
|
@@ -19,7 +19,7 @@ from .hyperboloid_attention import (
|
|
|
19
19
|
HyperbolicSoftmaxAttention,
|
|
20
20
|
focus_transform,
|
|
21
21
|
)
|
|
22
|
-
from .hyperboloid_conv import FGGConv2D, HypConv2DHyperboloid,
|
|
22
|
+
from .hyperboloid_conv import FGGConv2D, HypConv2DHyperboloid, LorentzConv2D
|
|
23
23
|
from .hyperboloid_core import build_spacelike_V, hrc, htc, lorentz_midpoint, lorentz_residual, spatial_to_hyperboloid
|
|
24
24
|
from .hyperboloid_linear import FGGLinear, HTCLinear, HypLinearHyperboloidFHCNN
|
|
25
25
|
from .hyperboloid_positional import HyperbolicRoPE, HypformerPositionalEncoding, hope
|
|
@@ -43,7 +43,6 @@ __all__ = [
|
|
|
43
43
|
"HTCLinear",
|
|
44
44
|
"HypConv2DHyperboloid",
|
|
45
45
|
"HypConv2DPoincare",
|
|
46
|
-
"HypConv3DHyperboloid",
|
|
47
46
|
"HypLinearHyperboloidFHCNN",
|
|
48
47
|
"HypLinearPoincare",
|
|
49
48
|
"HypLinearPoincarePP",
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Dimension key:
|
|
4
4
|
B: batch size
|
|
5
|
-
H: output height W: output width
|
|
6
|
-
C: channels (in/out) K: kernel elements (kh*kw
|
|
5
|
+
H: output height W: output width
|
|
6
|
+
C: channels (in/out) K: kernel elements (kh*kw)
|
|
7
7
|
A: ambient dimension (in_channels or hcat output dim)
|
|
8
8
|
"""
|
|
9
9
|
|
|
@@ -18,7 +18,7 @@ from hyperbolix.manifolds.hyperboloid import Hyperboloid
|
|
|
18
18
|
|
|
19
19
|
from ._helpers import validate_hyperboloid_manifold
|
|
20
20
|
from .hyperboloid_core import hrc
|
|
21
|
-
from .hyperboloid_linear import
|
|
21
|
+
from .hyperboloid_linear import _fgg_linear_forward, _fhcnn_forward, _get_effective_kernel
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class LorentzConv2D(nnx.Module):
|
|
@@ -230,17 +230,13 @@ class HypConv2DHyperboloid(nnx.Module):
|
|
|
230
230
|
d = in_channels - 1 # Input manifold dimension
|
|
231
231
|
hcat_out_ambient_dim = d * N + 1 # HCat output ambient dimension
|
|
232
232
|
|
|
233
|
-
#
|
|
234
|
-
|
|
235
|
-
self.
|
|
236
|
-
|
|
237
|
-
in_dim=hcat_out_ambient_dim,
|
|
238
|
-
out_dim=out_channels,
|
|
239
|
-
rngs=rngs,
|
|
240
|
-
input_space="manifold", # HCat output is always on manifold
|
|
241
|
-
learnable_scale=False,
|
|
242
|
-
normalize=False,
|
|
233
|
+
# Trainable parameters — owned directly for flat parameter paths
|
|
234
|
+
bound = 0.02
|
|
235
|
+
self.kernel = nnx.Param(
|
|
236
|
+
jax.random.uniform(rngs.params(), (out_channels, hcat_out_ambient_dim), minval=-bound, maxval=bound)
|
|
243
237
|
)
|
|
238
|
+
self.bias = nnx.Param(jnp.zeros((1, out_channels)))
|
|
239
|
+
self.scale = 2.3 # not learnable (matches FHCNN default)
|
|
244
240
|
|
|
245
241
|
def _extract_patches(
|
|
246
242
|
self,
|
|
@@ -322,7 +318,18 @@ class HypConv2DHyperboloid(nnx.Module):
|
|
|
322
318
|
hcat_out_NA = jax.vmap(self.manifold.hcat, in_axes=(0, None))(patches_flat_NKC, c) # (B*H*W, hcat_dim)
|
|
323
319
|
|
|
324
320
|
# Linear: (hcat_dim,) -> (out_channels,)
|
|
325
|
-
linear_out_NC =
|
|
321
|
+
linear_out_NC = _fhcnn_forward(
|
|
322
|
+
hcat_out_NA,
|
|
323
|
+
self.kernel[...],
|
|
324
|
+
self.bias[...],
|
|
325
|
+
self.manifold,
|
|
326
|
+
c,
|
|
327
|
+
"manifold",
|
|
328
|
+
None,
|
|
329
|
+
False,
|
|
330
|
+
self.scale,
|
|
331
|
+
1e-5,
|
|
332
|
+
) # (B*H*W, out_channels)
|
|
326
333
|
|
|
327
334
|
# Reshape back to spatial
|
|
328
335
|
output_BHWC = linear_out_NC.reshape(batch, out_h, out_w, self.out_channels)
|
|
@@ -330,210 +337,6 @@ class HypConv2DHyperboloid(nnx.Module):
|
|
|
330
337
|
return output_BHWC
|
|
331
338
|
|
|
332
339
|
|
|
333
|
-
class HypConv3DHyperboloid(nnx.Module):
|
|
334
|
-
"""
|
|
335
|
-
Hyperbolic 3D Convolutional Layer for Hyperboloid model.
|
|
336
|
-
|
|
337
|
-
This layer implements fully hyperbolic 3D convolution, extending the 2D approach
|
|
338
|
-
from "Fully Hyperbolic Convolutional Neural Networks for Computer Vision" to
|
|
339
|
-
volumetric data.
|
|
340
|
-
|
|
341
|
-
Computation steps:
|
|
342
|
-
1) Extract receptive field (kernel_size x kernel_size x kernel_size) of hyperbolic points
|
|
343
|
-
2) Apply HCat (Lorentz direct concatenation) to combine receptive field points
|
|
344
|
-
3) Pass through hyperbolic linear layer (LFC)
|
|
345
|
-
|
|
346
|
-
Parameters
|
|
347
|
-
----------
|
|
348
|
-
manifold_module : object
|
|
349
|
-
Class-based Hyperboloid manifold instance
|
|
350
|
-
in_channels : int
|
|
351
|
-
Number of input channels
|
|
352
|
-
out_channels : int
|
|
353
|
-
Number of output channels
|
|
354
|
-
kernel_size : int or tuple[int, int, int]
|
|
355
|
-
Size of the convolutional kernel (depth, height, width)
|
|
356
|
-
rngs : nnx.Rngs
|
|
357
|
-
Random number generators for parameter initialization
|
|
358
|
-
stride : int or tuple[int, int, int]
|
|
359
|
-
Stride of the convolution (default: 1)
|
|
360
|
-
padding : str
|
|
361
|
-
Padding mode, either 'SAME' or 'VALID' (default: 'SAME')
|
|
362
|
-
input_space : str
|
|
363
|
-
Type of the input tensor, either 'tangent' or 'manifold' (default: 'manifold').
|
|
364
|
-
Note: This is a static configuration - changing it after initialization requires recompilation.
|
|
365
|
-
|
|
366
|
-
Notes
|
|
367
|
-
-----
|
|
368
|
-
JIT Compatibility:
|
|
369
|
-
This layer is designed to work with nnx.jit. Configuration parameters (padding, input_space)
|
|
370
|
-
are treated as static and will be baked into the compiled function.
|
|
371
|
-
|
|
372
|
-
References
|
|
373
|
-
----------
|
|
374
|
-
Ahmad Bdeir, Kristian Schwethelm, and Niels Landwehr. "Fully hyperbolic convolutional neural networks for computer vision."
|
|
375
|
-
arXiv preprint arXiv:2303.15919 (2023).
|
|
376
|
-
"""
|
|
377
|
-
|
|
378
|
-
def __init__(
|
|
379
|
-
self,
|
|
380
|
-
manifold_module: Hyperboloid,
|
|
381
|
-
in_channels: int,
|
|
382
|
-
out_channels: int,
|
|
383
|
-
kernel_size: int | tuple[int, int, int],
|
|
384
|
-
*,
|
|
385
|
-
rngs: nnx.Rngs,
|
|
386
|
-
stride: int | tuple[int, int, int] = 1,
|
|
387
|
-
padding: str = "SAME",
|
|
388
|
-
input_space: str = "manifold",
|
|
389
|
-
):
|
|
390
|
-
if padding not in ["SAME", "VALID"]:
|
|
391
|
-
raise ValueError(f"padding must be either 'SAME' or 'VALID', got '{padding}'")
|
|
392
|
-
if input_space not in ["tangent", "manifold"]:
|
|
393
|
-
raise ValueError(f"input_space must be either 'tangent' or 'manifold', got '{input_space}'")
|
|
394
|
-
|
|
395
|
-
# Static configuration
|
|
396
|
-
validate_hyperboloid_manifold(manifold_module, required_methods=("expmap_0", "hcat"))
|
|
397
|
-
self.manifold = manifold_module
|
|
398
|
-
self.in_channels = in_channels
|
|
399
|
-
self.out_channels = out_channels
|
|
400
|
-
self.input_space = input_space
|
|
401
|
-
self.padding = padding
|
|
402
|
-
|
|
403
|
-
# Handle kernel_size as int or tuple
|
|
404
|
-
if isinstance(kernel_size, int):
|
|
405
|
-
self.kernel_size = (kernel_size, kernel_size, kernel_size)
|
|
406
|
-
else:
|
|
407
|
-
self.kernel_size = kernel_size
|
|
408
|
-
|
|
409
|
-
# Handle stride as int or tuple
|
|
410
|
-
if isinstance(stride, int):
|
|
411
|
-
self.stride = (stride, stride, stride)
|
|
412
|
-
else:
|
|
413
|
-
self.stride = stride
|
|
414
|
-
|
|
415
|
-
# Compute dimensions for the linear layer
|
|
416
|
-
# Receptive field: kernel_d x kernel_h x kernel_w voxels, each is a point in in_channels-dim ambient space
|
|
417
|
-
# HCat input: N = kernel_d x kernel_h x kernel_w points, each in in_channels ambient dimensions
|
|
418
|
-
# HCat output: ambient dim = (in_channels - 1) x N + 1
|
|
419
|
-
# = (in_channels - 1) x (kernel_d x kernel_h x kernel_w) + 1
|
|
420
|
-
kernel_d, kernel_h, kernel_w = self.kernel_size
|
|
421
|
-
N = kernel_d * kernel_h * kernel_w # Number of points in receptive field
|
|
422
|
-
d = in_channels - 1 # Input manifold dimension
|
|
423
|
-
hcat_out_ambient_dim = d * N + 1 # HCat output ambient dimension
|
|
424
|
-
|
|
425
|
-
# Create the linear transformation layer
|
|
426
|
-
# Input: hcat_out_ambient_dim, Output: out_channels
|
|
427
|
-
self.linear = HypLinearHyperboloidFHCNN(
|
|
428
|
-
manifold_module=self.manifold,
|
|
429
|
-
in_dim=hcat_out_ambient_dim,
|
|
430
|
-
out_dim=out_channels,
|
|
431
|
-
rngs=rngs,
|
|
432
|
-
input_space="manifold", # HCat output is always on manifold
|
|
433
|
-
learnable_scale=False,
|
|
434
|
-
normalize=False,
|
|
435
|
-
)
|
|
436
|
-
|
|
437
|
-
def _extract_patches(
|
|
438
|
-
self,
|
|
439
|
-
x: Float[Array, "batch depth height width in_channels"],
|
|
440
|
-
) -> Float[Array, "batch out_depth out_height out_width kernel_d kernel_h kernel_w in_channels"]:
|
|
441
|
-
"""Extract patches (receptive fields) from the 3D input using optimized JAX primitives."""
|
|
442
|
-
batch, depth, height, width, in_channels = x.shape
|
|
443
|
-
kernel_d, kernel_h, kernel_w = self.kernel_size
|
|
444
|
-
stride_d, stride_h, stride_w = self.stride
|
|
445
|
-
|
|
446
|
-
# 1. Handle Padding (Manually, to ensure manifold validity)
|
|
447
|
-
if self.padding == "SAME":
|
|
448
|
-
out_depth = (depth + stride_d - 1) // stride_d
|
|
449
|
-
out_height = (height + stride_h - 1) // stride_h
|
|
450
|
-
out_width = (width + stride_w - 1) // stride_w
|
|
451
|
-
|
|
452
|
-
pad_d = max((out_depth - 1) * stride_d + kernel_d - depth, 0)
|
|
453
|
-
pad_h = max((out_height - 1) * stride_h + kernel_h - height, 0)
|
|
454
|
-
pad_w = max((out_width - 1) * stride_w + kernel_w - width, 0)
|
|
455
|
-
|
|
456
|
-
pad_front = pad_d // 2
|
|
457
|
-
pad_back = pad_d - pad_front
|
|
458
|
-
pad_top = pad_h // 2
|
|
459
|
-
pad_bottom = pad_h - pad_top
|
|
460
|
-
pad_left = pad_w // 2
|
|
461
|
-
pad_right = pad_w - pad_left
|
|
462
|
-
|
|
463
|
-
x = jnp.pad(
|
|
464
|
-
x,
|
|
465
|
-
((0, 0), (pad_front, pad_back), (pad_top, pad_bottom), (pad_left, pad_right), (0, 0)),
|
|
466
|
-
mode="edge",
|
|
467
|
-
)
|
|
468
|
-
|
|
469
|
-
# 2. Extract Patches — output: (B, Z, H, W, C*kd*kh*kw)
|
|
470
|
-
patches_flat_BZHW_CKdKhKw = jax.lax.conv_general_dilated_patches(
|
|
471
|
-
lhs=x,
|
|
472
|
-
filter_shape=(kernel_d, kernel_h, kernel_w),
|
|
473
|
-
window_strides=(stride_d, stride_h, stride_w),
|
|
474
|
-
padding="VALID",
|
|
475
|
-
dimension_numbers=("NDHWC", "OIDHW", "NDHWC"),
|
|
476
|
-
)
|
|
477
|
-
|
|
478
|
-
# 3. Reshape to separate kernel dims and channels, then transpose
|
|
479
|
-
out_d, out_h, out_w = (
|
|
480
|
-
patches_flat_BZHW_CKdKhKw.shape[1],
|
|
481
|
-
patches_flat_BZHW_CKdKhKw.shape[2],
|
|
482
|
-
patches_flat_BZHW_CKdKhKw.shape[3],
|
|
483
|
-
)
|
|
484
|
-
# Flattened dim order from rhs_spec="OIDHW": (C, kd, kh, kw)
|
|
485
|
-
patches_BZHWCkdkhkw = patches_flat_BZHW_CKdKhKw.reshape(
|
|
486
|
-
batch, out_d, out_h, out_w, in_channels, kernel_d, kernel_h, kernel_w
|
|
487
|
-
)
|
|
488
|
-
patches_BZHWkdkhkwC = patches_BZHWCkdkhkw.transpose(0, 1, 2, 3, 5, 6, 7, 4) # move C last
|
|
489
|
-
|
|
490
|
-
return patches_BZHWkdkhkwC
|
|
491
|
-
|
|
492
|
-
def __call__(
|
|
493
|
-
self,
|
|
494
|
-
x: Float[Array, "batch depth height width in_channels"],
|
|
495
|
-
c: float = 1.0,
|
|
496
|
-
) -> Float[Array, "batch out_depth out_height out_width out_channels"]:
|
|
497
|
-
"""
|
|
498
|
-
Forward pass through the hyperbolic 3D convolutional layer.
|
|
499
|
-
|
|
500
|
-
Parameters
|
|
501
|
-
----------
|
|
502
|
-
x : Array of shape (batch, depth, height, width, in_channels)
|
|
503
|
-
Input feature map where each voxel is a point on the Hyperboloid manifold
|
|
504
|
-
c : float
|
|
505
|
-
Manifold curvature (default: 1.0)
|
|
506
|
-
|
|
507
|
-
Returns
|
|
508
|
-
-------
|
|
509
|
-
out : Array of shape (batch, out_depth, out_height, out_width, out_channels)
|
|
510
|
-
Output feature map on the Hyperboloid manifold
|
|
511
|
-
"""
|
|
512
|
-
# Map to manifold if needed (static branch - JIT friendly)
|
|
513
|
-
if self.input_space == "tangent":
|
|
514
|
-
x_flat_NC = x.reshape(-1, x.shape[-1]) # (B*Z*H*W, C)
|
|
515
|
-
x_mapped_NC = jax.vmap(self.manifold.expmap_0, in_axes=(0, None))(x_flat_NC, c)
|
|
516
|
-
x = x_mapped_NC.reshape(x.shape) # (B, Z, H, W, C)
|
|
517
|
-
|
|
518
|
-
# Extract patches: (B, Z, H, W, kd, kh, kw, C)
|
|
519
|
-
patches_BZHWkdkhkwC = self._extract_patches(x)
|
|
520
|
-
batch, out_d, out_h, out_w, kd, kh, kw, in_c = patches_BZHWkdkhkwC.shape
|
|
521
|
-
|
|
522
|
-
# Flatten batch+spatial for parallel processing: (B*Z*H*W, K, C)
|
|
523
|
-
patches_flat_NKC = patches_BZHWkdkhkwC.reshape(-1, kd * kh * kw, in_c)
|
|
524
|
-
|
|
525
|
-
# HCat: (K, C) -> (hcat_dim,) per patch
|
|
526
|
-
hcat_out_NA = jax.vmap(self.manifold.hcat, in_axes=(0, None))(patches_flat_NKC, c) # (B*Z*H*W, hcat_dim)
|
|
527
|
-
|
|
528
|
-
# Linear: (hcat_dim,) -> (out_channels,)
|
|
529
|
-
linear_out_NC = self.linear(hcat_out_NA, c) # (B*Z*H*W, out_channels)
|
|
530
|
-
|
|
531
|
-
# Reshape back to spatial
|
|
532
|
-
output_BZHWC = linear_out_NC.reshape(batch, out_d, out_h, out_w, self.out_channels)
|
|
533
|
-
|
|
534
|
-
return output_BZHWC
|
|
535
|
-
|
|
536
|
-
|
|
537
340
|
class FGGConv2D(nnx.Module):
|
|
538
341
|
"""Fast and Geometrically Grounded Lorentz 2D convolutional layer.
|
|
539
342
|
|
|
@@ -595,7 +398,7 @@ class FGGConv2D(nnx.Module):
|
|
|
595
398
|
padding: str = "SAME",
|
|
596
399
|
pad_mode: str = "origin",
|
|
597
400
|
activation: Callable | None = None,
|
|
598
|
-
reset_params: str = "
|
|
401
|
+
reset_params: str = "lorentz_kaiming",
|
|
599
402
|
use_weight_norm: bool = False,
|
|
600
403
|
init_bias: float = 0.5,
|
|
601
404
|
eps: float = 1e-7,
|
|
@@ -627,17 +430,45 @@ class FGGConv2D(nnx.Module):
|
|
|
627
430
|
kh, kw = self.kernel_size
|
|
628
431
|
hcat_out_ambient = (in_channels - 1) * kh * kw + 1
|
|
629
432
|
|
|
630
|
-
#
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
433
|
+
# Trainable parameters — owned directly for flat parameter paths
|
|
434
|
+
if reset_params not in ("eye", "xavier", "kaiming", "lorentz_kaiming", "mlr"):
|
|
435
|
+
raise ValueError(
|
|
436
|
+
f"reset_params must be 'eye', 'xavier', 'kaiming', 'lorentz_kaiming', or 'mlr', got '{reset_params}'"
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
in_spatial = hcat_out_ambient - 1 # I
|
|
440
|
+
out_spatial = out_channels - 1 # O
|
|
441
|
+
|
|
442
|
+
self.activation = activation
|
|
443
|
+
self.use_weight_norm = use_weight_norm
|
|
444
|
+
|
|
445
|
+
# Initialize Euclidean weight U: (I, O)
|
|
446
|
+
# Reference computes std from ambient dimensions (hcat_out_ambient, out_channels)
|
|
447
|
+
key = rngs.params()
|
|
448
|
+
if reset_params == "eye":
|
|
449
|
+
U_init = 0.5 * jnp.eye(in_spatial, out_spatial)
|
|
450
|
+
elif reset_params == "xavier":
|
|
451
|
+
std = jnp.sqrt(1.0 / (hcat_out_ambient + out_channels))
|
|
452
|
+
U_init = jax.random.normal(key, (in_spatial, out_spatial)) * std
|
|
453
|
+
elif reset_params == "kaiming":
|
|
454
|
+
std = jnp.sqrt(2.0 / hcat_out_ambient)
|
|
455
|
+
U_init = jax.random.normal(key, (in_spatial, out_spatial)) * std
|
|
456
|
+
elif reset_params == "lorentz_kaiming":
|
|
457
|
+
std = jnp.sqrt(1.0 / hcat_out_ambient)
|
|
458
|
+
U_init = jax.random.normal(key, (in_spatial, out_spatial)) * std
|
|
459
|
+
else: # mlr
|
|
460
|
+
std = jnp.sqrt(5.0 / hcat_out_ambient)
|
|
461
|
+
U_init = jax.random.normal(key, (in_spatial, out_spatial)) * std
|
|
462
|
+
|
|
463
|
+
# Weight normalization: decompose kernel = softplus(kernel_scale) * kernel_dir / ||kernel_dir||
|
|
464
|
+
if use_weight_norm:
|
|
465
|
+
self.kernel_dir = nnx.Param(U_init) # (I, O) direction
|
|
466
|
+
g_init_val = jnp.sqrt(1.0 / (hcat_out_ambient + out_channels))
|
|
467
|
+
self.kernel_scale = nnx.Param(jnp.full((out_spatial,), g_init_val)) # (O,)
|
|
468
|
+
else:
|
|
469
|
+
self.kernel = nnx.Param(U_init) # (I, O)
|
|
470
|
+
|
|
471
|
+
self.bias = nnx.Param(jnp.full((out_spatial,), init_bias)) # (O,)
|
|
641
472
|
|
|
642
473
|
def _extract_patches(
|
|
643
474
|
self,
|
|
@@ -717,8 +548,15 @@ class FGGConv2D(nnx.Module):
|
|
|
717
548
|
# HCat: (K, C) -> (hcat_dim,) per patch
|
|
718
549
|
hcat_out_NA = jax.vmap(self.manifold.hcat, in_axes=(0, None))(patches_flat_NKC, c)
|
|
719
550
|
|
|
720
|
-
#
|
|
721
|
-
|
|
551
|
+
# FGG forward: (hcat_dim,) -> (out_channels,)
|
|
552
|
+
U_IO = _get_effective_kernel(
|
|
553
|
+
getattr(self, "kernel", None),
|
|
554
|
+
getattr(self, "kernel_dir", None),
|
|
555
|
+
getattr(self, "kernel_scale", None),
|
|
556
|
+
self.use_weight_norm,
|
|
557
|
+
self.eps,
|
|
558
|
+
)
|
|
559
|
+
linear_out_NC = _fgg_linear_forward(hcat_out_NA, U_IO, self.bias[...], c, self.activation, self.eps)
|
|
722
560
|
|
|
723
561
|
# Reshape back to spatial
|
|
724
562
|
return linear_out_NC.reshape(batch, out_h, out_w, self.out_channels)
|
|
@@ -25,6 +25,107 @@ from ._helpers import validate_hyperboloid_manifold
|
|
|
25
25
|
from .hyperboloid_core import build_spacelike_V, htc
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def _fhcnn_forward(
|
|
29
|
+
x_BI: Float[Array, "batch in_dim"],
|
|
30
|
+
kernel_OI: Array,
|
|
31
|
+
bias_1O: Array,
|
|
32
|
+
manifold: Manifold,
|
|
33
|
+
c: float,
|
|
34
|
+
input_space: str,
|
|
35
|
+
activation: Callable[[Array], Array] | None,
|
|
36
|
+
normalize: bool,
|
|
37
|
+
scale_val: float | Array,
|
|
38
|
+
eps: float,
|
|
39
|
+
) -> Float[Array, "batch out_dim"]:
|
|
40
|
+
"""Pure-function FHCNN forward pass.
|
|
41
|
+
|
|
42
|
+
Used by both HypLinearHyperboloidFHCNN and HypConv2DHyperboloid.
|
|
43
|
+
"""
|
|
44
|
+
# Map to manifold if needed (static branch - JIT friendly)
|
|
45
|
+
if input_space == "tangent":
|
|
46
|
+
x_BI = jax.vmap(manifold.expmap_0, in_axes=(0, None), out_axes=0)(x_BI, c)
|
|
47
|
+
|
|
48
|
+
# Apply activation if provided (static branch - JIT friendly)
|
|
49
|
+
if activation is not None:
|
|
50
|
+
x_BI = activation(x_BI)
|
|
51
|
+
|
|
52
|
+
# Linear transformation: (B, in_dim) -> (B, out_dim)
|
|
53
|
+
x_BO = jnp.einsum("bi,oi->bo", x_BI, kernel_OI) + bias_1O
|
|
54
|
+
|
|
55
|
+
# Split into time and space: x0 is first coord, x_rem is spatial
|
|
56
|
+
x0_B1 = x_BO[:, 0:1] # (B, 1) -- time coordinate
|
|
57
|
+
x_rem_BD = x_BO[:, 1:] # (B, D) where D = out_dim - 1
|
|
58
|
+
|
|
59
|
+
# Static branch - JIT friendly
|
|
60
|
+
if normalize:
|
|
61
|
+
x_rem_norm_B1 = jnp.linalg.norm(x_rem_BD, ord=2, axis=-1, keepdims=True) # (B, 1)
|
|
62
|
+
|
|
63
|
+
# Learnable sigmoid scaling
|
|
64
|
+
scale_B1 = jnp.exp(scale_val) * jax.nn.sigmoid(x0_B1) # (B, 1)
|
|
65
|
+
|
|
66
|
+
res0_B1 = jnp.sqrt(scale_B1**2 + 1 / c + eps) # (B, 1)
|
|
67
|
+
res_rem_BD = scale_B1 * x_rem_BD / x_rem_norm_B1 # (B, D)
|
|
68
|
+
|
|
69
|
+
res_BA = jnp.concatenate([res0_B1, res_rem_BD], axis=-1) # (B, A)
|
|
70
|
+
|
|
71
|
+
# Cast near-zero-norm vectors to origin
|
|
72
|
+
origin_time_B1 = jnp.sqrt(1 / c) * jnp.ones_like(res0_B1)
|
|
73
|
+
origin_BA = jnp.concatenate([origin_time_B1, jnp.zeros_like(res_rem_BD)], axis=-1)
|
|
74
|
+
|
|
75
|
+
mask_B1 = x_rem_norm_B1 <= 1e-5
|
|
76
|
+
res_BA = jnp.where(mask_B1, origin_BA, res_BA)
|
|
77
|
+
else:
|
|
78
|
+
# Reconstruct time from space: x0 = sqrt(||x_rem||^2 + 1/c)
|
|
79
|
+
res0_B1 = jnp.sqrt(jnp.sum(x_rem_BD**2, axis=-1, keepdims=True) + 1 / c) # (B, 1)
|
|
80
|
+
res_BA = jnp.concatenate([res0_B1, x_rem_BD], axis=-1) # (B, A)
|
|
81
|
+
|
|
82
|
+
return res_BA
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _get_effective_kernel(
|
|
86
|
+
kernel: Array | None,
|
|
87
|
+
kernel_dir: Array | None,
|
|
88
|
+
kernel_scale: Array | None,
|
|
89
|
+
use_weight_norm: bool,
|
|
90
|
+
eps: float,
|
|
91
|
+
) -> Array:
|
|
92
|
+
"""Compute effective weight matrix, handling weight normalization."""
|
|
93
|
+
if use_weight_norm:
|
|
94
|
+
assert kernel_scale is not None and kernel_dir is not None
|
|
95
|
+
g_pos_O = jax.nn.softplus(kernel_scale) # (O,) force positive magnitudes
|
|
96
|
+
v_norm_O = jnp.sqrt(jnp.sum(kernel_dir**2, axis=0) + eps) # (O,)
|
|
97
|
+
return g_pos_O[None, :] * kernel_dir / v_norm_O[None, :] # (I, O)
|
|
98
|
+
assert kernel is not None
|
|
99
|
+
return kernel
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _fgg_linear_forward(
|
|
103
|
+
x_BAi: Float[Array, "batch in_features"],
|
|
104
|
+
U_IO: Array,
|
|
105
|
+
bias_O: Array,
|
|
106
|
+
c: float,
|
|
107
|
+
activation: Callable[[jax.Array], jax.Array] | None,
|
|
108
|
+
eps: float,
|
|
109
|
+
) -> Float[Array, "batch out_features"]:
|
|
110
|
+
"""Pure-function FGG forward: build_spacelike_V -> matmul -> activation -> time reconstruct."""
|
|
111
|
+
# Build V_mink from (kernel, bias) -- Minkowski metric absorbed
|
|
112
|
+
V_AiO = build_spacelike_V(U_IO, bias_O, c, eps) # (Ai, O)
|
|
113
|
+
# Cast V to match input dtype (avoids float32/float64 scatter warnings)
|
|
114
|
+
V_AiO = V_AiO.astype(x_BAi.dtype)
|
|
115
|
+
|
|
116
|
+
# Minkowski inner products via matmul (metric in V)
|
|
117
|
+
z_BO = x_BAi @ V_AiO # (B, O)
|
|
118
|
+
|
|
119
|
+
# Apply Euclidean activation (Lorentzian wrapping implicit via cancellation)
|
|
120
|
+
if activation is not None:
|
|
121
|
+
z_BO = activation(z_BO)
|
|
122
|
+
|
|
123
|
+
# Reconstruct hyperboloid point: spatial = z, time from constraint
|
|
124
|
+
y_0_B1 = jnp.sqrt(jnp.sum(z_BO**2, axis=-1, keepdims=True) + 1.0 / c) # (B, 1)
|
|
125
|
+
|
|
126
|
+
return jnp.concatenate([y_0_B1, z_BO], axis=-1) # (B, Ao)
|
|
127
|
+
|
|
128
|
+
|
|
28
129
|
class HypLinearHyperboloidFHCNN(nnx.Module):
|
|
29
130
|
"""
|
|
30
131
|
Fully Hyperbolic Convolutional Neural Networks fully connected layer (Hyperboloid model).
|
|
@@ -147,46 +248,19 @@ class HypLinearHyperboloidFHCNN(nnx.Module):
|
|
|
147
248
|
res : Array of shape (batch, out_dim)
|
|
148
249
|
Output on the Hyperboloid manifold
|
|
149
250
|
"""
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
x
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
x_rem_BD = x_BO[:, 1:] # (B, D) where D = out_dim - 1
|
|
164
|
-
|
|
165
|
-
# Static branch - JIT friendly
|
|
166
|
-
if self.normalize:
|
|
167
|
-
x_rem_norm_B1 = jnp.linalg.norm(x_rem_BD, ord=2, axis=-1, keepdims=True) # (B, 1)
|
|
168
|
-
|
|
169
|
-
# Learnable sigmoid scaling
|
|
170
|
-
scale_val = self.scale[...] if isinstance(self.scale, nnx.Param) else self.scale
|
|
171
|
-
scale_B1 = jnp.exp(scale_val) * jax.nn.sigmoid(x0_B1) # (B, 1)
|
|
172
|
-
|
|
173
|
-
res0_B1 = jnp.sqrt(scale_B1**2 + 1 / c + self.eps) # (B, 1)
|
|
174
|
-
res_rem_BD = scale_B1 * x_rem_BD / x_rem_norm_B1 # (B, D)
|
|
175
|
-
|
|
176
|
-
res_BA = jnp.concatenate([res0_B1, res_rem_BD], axis=-1) # (B, A)
|
|
177
|
-
|
|
178
|
-
# Cast near-zero-norm vectors to origin
|
|
179
|
-
origin_time_B1 = jnp.sqrt(1 / c) * jnp.ones_like(res0_B1)
|
|
180
|
-
origin_BA = jnp.concatenate([origin_time_B1, jnp.zeros_like(res_rem_BD)], axis=-1)
|
|
181
|
-
|
|
182
|
-
mask_B1 = x_rem_norm_B1 <= 1e-5
|
|
183
|
-
res_BA = jnp.where(mask_B1, origin_BA, res_BA)
|
|
184
|
-
else:
|
|
185
|
-
# Reconstruct time from space: x₀ = sqrt(||x_rem||² + 1/c)
|
|
186
|
-
res0_B1 = jnp.sqrt(jnp.sum(x_rem_BD**2, axis=-1, keepdims=True) + 1 / c) # (B, 1)
|
|
187
|
-
res_BA = jnp.concatenate([res0_B1, x_rem_BD], axis=-1) # (B, A)
|
|
188
|
-
|
|
189
|
-
return res_BA
|
|
251
|
+
scale_val = self.scale[...] if isinstance(self.scale, nnx.Param) else self.scale
|
|
252
|
+
return _fhcnn_forward(
|
|
253
|
+
x,
|
|
254
|
+
self.kernel[...],
|
|
255
|
+
self.bias[...],
|
|
256
|
+
self.manifold,
|
|
257
|
+
c,
|
|
258
|
+
self.input_space,
|
|
259
|
+
self.activation,
|
|
260
|
+
self.normalize,
|
|
261
|
+
scale_val,
|
|
262
|
+
self.eps,
|
|
263
|
+
)
|
|
190
264
|
|
|
191
265
|
|
|
192
266
|
class HTCLinear(nnx.Module):
|
|
@@ -392,27 +466,28 @@ class FGGLinear(nnx.Module):
|
|
|
392
466
|
self.eps = eps
|
|
393
467
|
|
|
394
468
|
# Initialize Euclidean weight U: (I, O)
|
|
469
|
+
# Reference computes std from ambient dimensions (in_features, out_features)
|
|
395
470
|
key = rngs.params()
|
|
396
471
|
if reset_params == "eye":
|
|
397
472
|
U_init = 0.5 * jnp.eye(in_spatial, out_spatial)
|
|
398
473
|
elif reset_params == "xavier":
|
|
399
|
-
std = jnp.sqrt(1.0 / (
|
|
474
|
+
std = jnp.sqrt(1.0 / (in_features + out_features))
|
|
400
475
|
U_init = jax.random.normal(key, (in_spatial, out_spatial)) * std
|
|
401
476
|
elif reset_params == "kaiming":
|
|
402
|
-
std = jnp.sqrt(2.0 /
|
|
477
|
+
std = jnp.sqrt(2.0 / in_features)
|
|
403
478
|
U_init = jax.random.normal(key, (in_spatial, out_spatial)) * std
|
|
404
479
|
elif reset_params == "lorentz_kaiming":
|
|
405
|
-
std = jnp.sqrt(1.0 /
|
|
480
|
+
std = jnp.sqrt(1.0 / in_features)
|
|
406
481
|
U_init = jax.random.normal(key, (in_spatial, out_spatial)) * std
|
|
407
482
|
else: # mlr
|
|
408
|
-
std = jnp.sqrt(5.0 /
|
|
483
|
+
std = jnp.sqrt(5.0 / in_features)
|
|
409
484
|
U_init = jax.random.normal(key, (in_spatial, out_spatial)) * std
|
|
410
485
|
|
|
411
486
|
# Weight normalization: decompose kernel = softplus(kernel_scale) * kernel_dir / ||kernel_dir||
|
|
412
487
|
if use_weight_norm:
|
|
413
488
|
# Reference: kernel_dir from reset_params (normalized in forward), kernel_scale fixed magnitude
|
|
414
489
|
self.kernel_dir = nnx.Param(U_init) # (I, O) direction
|
|
415
|
-
g_init_val = jnp.sqrt(1.0 / (
|
|
490
|
+
g_init_val = jnp.sqrt(1.0 / (in_features + out_features))
|
|
416
491
|
self.kernel_scale = nnx.Param(jnp.full((out_spatial,), g_init_val)) # (O,)
|
|
417
492
|
else:
|
|
418
493
|
self.kernel = nnx.Param(U_init) # (I, O)
|
|
@@ -422,13 +497,13 @@ class FGGLinear(nnx.Module):
|
|
|
422
497
|
|
|
423
498
|
def _get_kernel(self) -> jax.Array:
|
|
424
499
|
"""Return the effective weight matrix, handling weight normalization."""
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
500
|
+
return _get_effective_kernel(
|
|
501
|
+
self.kernel[...] if not self.use_weight_norm else None,
|
|
502
|
+
self.kernel_dir[...] if self.use_weight_norm else None,
|
|
503
|
+
self.kernel_scale[...] if self.use_weight_norm else None,
|
|
504
|
+
self.use_weight_norm,
|
|
505
|
+
self.eps,
|
|
506
|
+
)
|
|
432
507
|
|
|
433
508
|
def __call__(
|
|
434
509
|
self,
|
|
@@ -451,22 +526,5 @@ class FGGLinear(nnx.Module):
|
|
|
451
526
|
Output points on the hyperboloid with curvature ``c``.
|
|
452
527
|
Ao = out_features (ambient dimension).
|
|
453
528
|
"""
|
|
454
|
-
# 1. Get effective kernel (handle weight norm)
|
|
455
529
|
U_IO = self._get_kernel() # (I, O)
|
|
456
|
-
|
|
457
|
-
# 2. Build V_mink from (kernel, bias) — Minkowski metric absorbed
|
|
458
|
-
V_AiO = build_spacelike_V(U_IO, self.bias[...], c, self.eps) # (Ai, O)
|
|
459
|
-
# Cast V to match input dtype (avoids float32/float64 scatter warnings)
|
|
460
|
-
V_AiO = V_AiO.astype(x_BAi.dtype)
|
|
461
|
-
|
|
462
|
-
# 3. Minkowski inner products via matmul (metric in V)
|
|
463
|
-
z_BO = x_BAi @ V_AiO # (B, O)
|
|
464
|
-
|
|
465
|
-
# 4. Apply Euclidean activation (Lorentzian wrapping implicit via cancellation)
|
|
466
|
-
if self.activation is not None:
|
|
467
|
-
z_BO = self.activation(z_BO)
|
|
468
|
-
|
|
469
|
-
# 5. Reconstruct hyperboloid point: spatial = z, time from constraint
|
|
470
|
-
y_0_B1 = jnp.sqrt(jnp.sum(z_BO**2, axis=-1, keepdims=True) + 1.0 / c) # (B, 1)
|
|
471
|
-
|
|
472
|
-
return jnp.concatenate([y_0_B1, z_BO], axis=-1) # (B, Ao)
|
|
530
|
+
return _fgg_linear_forward(x_BAi, U_IO, self.bias[...], c, self.activation, self.eps)
|
|
@@ -174,13 +174,13 @@ class FGGLorentzMLR(nnx.Module):
|
|
|
174
174
|
self.eps = eps
|
|
175
175
|
|
|
176
176
|
# Hyperplane normals (spatial) and bias offsets
|
|
177
|
+
# Reference computes std from ambient dimension (in_features)
|
|
177
178
|
key = rngs.params()
|
|
178
179
|
if reset_params == "mlr":
|
|
179
|
-
|
|
180
|
-
std = jnp.sqrt(5.0 / in_spatial)
|
|
180
|
+
std = jnp.sqrt(5.0 / in_features)
|
|
181
181
|
self.kernel = nnx.Param(jax.random.normal(key, (in_spatial, num_classes)) * std)
|
|
182
182
|
else: # default
|
|
183
|
-
stdv = 1.0 / jnp.sqrt(jnp.array(
|
|
183
|
+
stdv = 1.0 / jnp.sqrt(jnp.array(in_features, dtype=jnp.float32))
|
|
184
184
|
self.kernel = nnx.Param(jax.random.uniform(key, (in_spatial, num_classes), minval=-stdv, maxval=stdv))
|
|
185
185
|
self.bias = nnx.Param(jnp.full((num_classes,), init_bias))
|
|
186
186
|
|
|
@@ -25,7 +25,7 @@ from jaxtyping import Array, Float
|
|
|
25
25
|
from hyperbolix.manifolds.poincare import Poincare
|
|
26
26
|
|
|
27
27
|
from ._helpers import validate_poincare_manifold
|
|
28
|
-
from .poincare_linear import
|
|
28
|
+
from .poincare_linear import _poincare_pp_forward
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class HypConv2DPoincare(nnx.Module):
|
|
@@ -150,25 +150,23 @@ class HypConv2DPoincare(nnx.Module):
|
|
|
150
150
|
concat_dim = K2 * in_channels
|
|
151
151
|
beta_n = jax.scipy.special.beta(concat_dim / 2.0, 0.5)
|
|
152
152
|
beta_ni = jax.scipy.special.beta(in_channels / 2.0, 0.5)
|
|
153
|
-
self.beta_scale = beta_n / beta_ni
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
self.
|
|
157
|
-
manifold_module=self.manifold,
|
|
158
|
-
in_dim=concat_dim,
|
|
159
|
-
out_dim=out_channels,
|
|
160
|
-
rngs=rngs,
|
|
161
|
-
input_space="manifold", # input is on manifold after expmap_0
|
|
162
|
-
clamping_factor=clamping_factor,
|
|
163
|
-
smoothing_factor=smoothing_factor,
|
|
164
|
-
)
|
|
153
|
+
self.beta_scale = float(beta_n / beta_ni)
|
|
154
|
+
|
|
155
|
+
self.clamping_factor = clamping_factor
|
|
156
|
+
self.smoothing_factor = smoothing_factor
|
|
165
157
|
|
|
166
|
-
#
|
|
167
|
-
# (van Spengler et al. 2023)
|
|
168
|
-
#
|
|
158
|
+
# Trainable parameters — owned directly for flat parameter paths
|
|
159
|
+
# Weight initialization: identity from reference (van Spengler et al. 2023)
|
|
160
|
+
# or scaled normal std = 1/sqrt(fan_in)
|
|
169
161
|
if id_init:
|
|
170
|
-
|
|
171
|
-
|
|
162
|
+
# W = 1/2 * I(C_out, K^2*C_in)
|
|
163
|
+
# The 1/2 factor compensates for the factor of 2 in the HNN++ distance formula.
|
|
164
|
+
kernel_init = 0.5 * jnp.eye(out_channels, concat_dim)
|
|
165
|
+
else:
|
|
166
|
+
std = 1.0 / jnp.sqrt(concat_dim)
|
|
167
|
+
kernel_init = jax.random.normal(rngs.params(), (out_channels, concat_dim)) * std
|
|
168
|
+
self.kernel = nnx.Param(kernel_init)
|
|
169
|
+
self.bias = nnx.Param(jnp.zeros((out_channels, 1)))
|
|
172
170
|
|
|
173
171
|
def __call__(
|
|
174
172
|
self,
|
|
@@ -228,7 +226,16 @@ class HypConv2DPoincare(nnx.Module):
|
|
|
228
226
|
) # (N, K²·C_in) on Poincaré ball
|
|
229
227
|
|
|
230
228
|
# Step 5: HNN++ FC — (N, K²·C_in) on manifold → (N, C_out) on manifold
|
|
231
|
-
fc_out_NC =
|
|
229
|
+
fc_out_NC = _poincare_pp_forward(
|
|
230
|
+
manifold_pts_NKC,
|
|
231
|
+
self.kernel[...],
|
|
232
|
+
self.bias[...],
|
|
233
|
+
self.manifold,
|
|
234
|
+
c,
|
|
235
|
+
"manifold",
|
|
236
|
+
self.clamping_factor,
|
|
237
|
+
self.smoothing_factor,
|
|
238
|
+
)
|
|
232
239
|
|
|
233
240
|
# Step 6: Map back to tangent space
|
|
234
241
|
tangent_out_NC = jax.vmap(self.manifold.logmap_0, in_axes=(0, None))(fc_out_NC, c) # (N, C_out)
|
|
@@ -128,6 +128,40 @@ class HypLinearPoincare(nnx.Module):
|
|
|
128
128
|
return res_BO
|
|
129
129
|
|
|
130
130
|
|
|
131
|
+
def _poincare_pp_forward(
|
|
132
|
+
x_BI: Float[Array, "batch in_dim"],
|
|
133
|
+
kernel_OI: Array,
|
|
134
|
+
bias_O1: Array,
|
|
135
|
+
manifold: Poincare,
|
|
136
|
+
c: float,
|
|
137
|
+
input_space: str,
|
|
138
|
+
clamping_factor: float,
|
|
139
|
+
smoothing_factor: float,
|
|
140
|
+
) -> Float[Array, "batch out_dim"]:
|
|
141
|
+
"""Pure-function HNN++ forward pass.
|
|
142
|
+
|
|
143
|
+
Used by both HypLinearPoincarePP and HypConv2DPoincare.
|
|
144
|
+
"""
|
|
145
|
+
# Map to manifold if needed (static branch - JIT friendly)
|
|
146
|
+
if input_space == "tangent":
|
|
147
|
+
x_BI = jax.vmap(manifold.expmap_0, in_axes=(0, None), out_axes=0)(x_BI, c)
|
|
148
|
+
|
|
149
|
+
# Compute multinomial linear regression
|
|
150
|
+
v = manifold.compute_mlr_pp(x_BI, kernel_OI, bias_O1, c, clamping_factor, smoothing_factor)
|
|
151
|
+
|
|
152
|
+
# Generalized linear transformation
|
|
153
|
+
sqrt_c = jnp.sqrt(c)
|
|
154
|
+
w_BO = sinh(sqrt_c * v) / sqrt_c
|
|
155
|
+
w2_B1 = jnp.sum(w_BO**2, axis=-1, keepdims=True)
|
|
156
|
+
denom_B1 = 1 + jnp.sqrt(1 + c * w2_B1)
|
|
157
|
+
res_BO = w_BO / denom_B1 # (B, 1) broadcasts over (B, O)
|
|
158
|
+
|
|
159
|
+
# Project results to the manifold
|
|
160
|
+
res_BO = jax.vmap(manifold.proj, in_axes=(0, None), out_axes=0)(res_BO, c)
|
|
161
|
+
|
|
162
|
+
return res_BO
|
|
163
|
+
|
|
164
|
+
|
|
131
165
|
class HypLinearPoincarePP(nnx.Module):
|
|
132
166
|
"""
|
|
133
167
|
Hyperbolic Neural Networks ++ fully connected layer (Poincaré ball model).
|
|
@@ -220,28 +254,13 @@ class HypLinearPoincarePP(nnx.Module):
|
|
|
220
254
|
res : Array of shape (batch, out_dim)
|
|
221
255
|
Output on the Poincaré ball manifold
|
|
222
256
|
"""
|
|
223
|
-
|
|
224
|
-
if self.input_space == "tangent":
|
|
225
|
-
x = jax.vmap(self.manifold.expmap_0, in_axes=(0, None), out_axes=0)(x, c)
|
|
226
|
-
|
|
227
|
-
# Compute multinomial linear regression
|
|
228
|
-
v = self.manifold.compute_mlr_pp(
|
|
257
|
+
return _poincare_pp_forward(
|
|
229
258
|
x,
|
|
230
259
|
self.kernel[...],
|
|
231
260
|
self.bias[...],
|
|
261
|
+
self.manifold,
|
|
232
262
|
c,
|
|
263
|
+
self.input_space,
|
|
233
264
|
self.clamping_factor,
|
|
234
265
|
self.smoothing_factor,
|
|
235
266
|
)
|
|
236
|
-
|
|
237
|
-
# Generalized linear transformation
|
|
238
|
-
sqrt_c = jnp.sqrt(c)
|
|
239
|
-
w_BO = sinh(sqrt_c * v) / sqrt_c
|
|
240
|
-
w2_B1 = jnp.sum(w_BO**2, axis=-1, keepdims=True)
|
|
241
|
-
denom_B1 = 1 + jnp.sqrt(1 + c * w2_B1)
|
|
242
|
-
res_BO = w_BO / denom_B1 # (B, 1) broadcasts over (B, O)
|
|
243
|
-
|
|
244
|
-
# Project results to the manifold
|
|
245
|
-
res_BO = jax.vmap(self.manifold.proj, in_axes=(0, None), out_axes=0)(res_BO, c)
|
|
246
|
-
|
|
247
|
-
return res_BO
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyperbolix-0.4.0 → hyperbolix-0.4.2}/hyperbolix/distributions/wrapped_normal_hyperboloid.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|