fabricpc 0.4.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.
- fabricpc/__init__.py +84 -0
- fabricpc/core/__init__.py +111 -0
- fabricpc/core/_frozen.py +59 -0
- fabricpc/core/activations.py +390 -0
- fabricpc/core/energy.py +520 -0
- fabricpc/core/inference.py +389 -0
- fabricpc/core/initializers.py +482 -0
- fabricpc/core/learning.py +60 -0
- fabricpc/core/mupc.py +421 -0
- fabricpc/core/positional.py +49 -0
- fabricpc/core/scaling.py +111 -0
- fabricpc/core/state_ops.py +54 -0
- fabricpc/core/topology.py +84 -0
- fabricpc/core/types.py +217 -0
- fabricpc/experiments/__init__.py +33 -0
- fabricpc/experiments/ab_experiment.py +648 -0
- fabricpc/experiments/statistics.py +135 -0
- fabricpc/graph_assembly/__init__.py +5 -0
- fabricpc/graph_assembly/graph_construction.py +260 -0
- fabricpc/graph_initialization/__init__.py +24 -0
- fabricpc/graph_initialization/params_initializer.py +63 -0
- fabricpc/graph_initialization/state_initializer.py +386 -0
- fabricpc/jax_config.py +122 -0
- fabricpc/models/__init__.py +5 -0
- fabricpc/models/transformer.py +160 -0
- fabricpc/nodes/__init__.py +58 -0
- fabricpc/nodes/base.py +746 -0
- fabricpc/nodes/convolutional.py +241 -0
- fabricpc/nodes/identity.py +163 -0
- fabricpc/nodes/linear.py +228 -0
- fabricpc/nodes/linear_explicit_grad.py +174 -0
- fabricpc/nodes/linear_residual.py +202 -0
- fabricpc/nodes/pooling.py +428 -0
- fabricpc/nodes/skip_connection.py +144 -0
- fabricpc/nodes/storkey_hopfield.py +421 -0
- fabricpc/nodes/transformer.py +512 -0
- fabricpc/nodes/transformer_v2.py +422 -0
- fabricpc/training/__init__.py +66 -0
- fabricpc/training/multi_gpu.py +43 -0
- fabricpc/training/natural_gradients.py +117 -0
- fabricpc/training/optimizers.py +13 -0
- fabricpc/training/train.py +845 -0
- fabricpc/training/train_autoregressive.py +758 -0
- fabricpc/training/train_backprop.py +714 -0
- fabricpc/tuning/__init__.py +3 -0
- fabricpc/tuning/bayesian_tuner.py +418 -0
- fabricpc/utils/__init__.py +24 -0
- fabricpc/utils/dashboarding/__init__.py +80 -0
- fabricpc/utils/dashboarding/_aim_available.py +68 -0
- fabricpc/utils/dashboarding/callbacks.py +171 -0
- fabricpc/utils/dashboarding/extractors.py +287 -0
- fabricpc/utils/dashboarding/inference_tracking.py +312 -0
- fabricpc/utils/dashboarding/trackers.py +544 -0
- fabricpc/utils/data/__init__.py +25 -0
- fabricpc/utils/data/data_utils.py +45 -0
- fabricpc/utils/data/dataloader.py +657 -0
- fabricpc/utils/helpers.py +19 -0
- fabricpc-0.4.0.dist-info/METADATA +184 -0
- fabricpc-0.4.0.dist-info/RECORD +62 -0
- fabricpc-0.4.0.dist-info/WHEEL +5 -0
- fabricpc-0.4.0.dist-info/licenses/LICENSE +21 -0
- fabricpc-0.4.0.dist-info/top_level.txt +1 -0
fabricpc/__init__.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FabricPC-JAX: Predictive Coding Networks in JAX
|
|
3
|
+
================================================
|
|
4
|
+
|
|
5
|
+
A functional, high-performance implementation of predictive coding networks
|
|
6
|
+
using JAX for automatic differentiation, JIT compilation, and multi-device parallelism.
|
|
7
|
+
|
|
8
|
+
Key Features:
|
|
9
|
+
- Functional programming paradigm (immutable data structures)
|
|
10
|
+
- JIT-compiled inference and training loops
|
|
11
|
+
- Multi-GPU/TPU support with pmap
|
|
12
|
+
- XLA optimization for maximum performance
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> from fabricpc.nodes import Linear
|
|
16
|
+
>>> from fabricpc.core.topology import Edge
|
|
17
|
+
>>> from fabricpc.graph_assembly import TaskMap, graph
|
|
18
|
+
>>> from fabricpc.graph_initialization import initialize_params
|
|
19
|
+
>>> from fabricpc.training import train_pcn, evaluate_pcn
|
|
20
|
+
>>>
|
|
21
|
+
>>> # Define nodes
|
|
22
|
+
>>> input_node = Linear(shape=(784,), name="input")
|
|
23
|
+
>>> hidden = Linear(shape=(128,), name="hidden")
|
|
24
|
+
>>> output = Linear(shape=(10,), name="output")
|
|
25
|
+
>>>
|
|
26
|
+
>>> # Build graph
|
|
27
|
+
>>> structure = graph(
|
|
28
|
+
... nodes=[input_node, hidden, output],
|
|
29
|
+
... edges=[
|
|
30
|
+
... Edge(source=input_node, target=hidden.slot("in")),
|
|
31
|
+
... Edge(source=hidden, target=output.slot("in")),
|
|
32
|
+
... ],
|
|
33
|
+
... task_map=TaskMap(x=input_node, y=output),
|
|
34
|
+
... inference=InferenceSGD(eta_infer=0.05, infer_steps=10),
|
|
35
|
+
... )
|
|
36
|
+
>>> params = initialize_params(structure, rng_key)
|
|
37
|
+
>>> trained_params, history, _ = train_pcn(params, structure, train_loader, config)
|
|
38
|
+
>>> metrics = evaluate_pcn(trained_params, structure, test_loader, config)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from importlib.metadata import version
|
|
42
|
+
|
|
43
|
+
__version__ = version("fabricpc")
|
|
44
|
+
|
|
45
|
+
# Submodules (for advanced use)
|
|
46
|
+
from fabricpc import (
|
|
47
|
+
core,
|
|
48
|
+
graph_initialization,
|
|
49
|
+
nodes,
|
|
50
|
+
training,
|
|
51
|
+
utils,
|
|
52
|
+
graph_assembly,
|
|
53
|
+
models,
|
|
54
|
+
experiments,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Core API - what most users need
|
|
58
|
+
from fabricpc.graph_initialization import initialize_params
|
|
59
|
+
from fabricpc.training import train_pcn, evaluate_pcn
|
|
60
|
+
from fabricpc.jax_config import setup_jax
|
|
61
|
+
|
|
62
|
+
# Types - for type hints
|
|
63
|
+
from fabricpc.core.types import GraphParams, GraphState, GraphStructure
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
# Core API (common use)
|
|
67
|
+
"initialize_params",
|
|
68
|
+
"train_pcn",
|
|
69
|
+
"evaluate_pcn",
|
|
70
|
+
"setup_jax",
|
|
71
|
+
# Types (for type hints)
|
|
72
|
+
"GraphParams",
|
|
73
|
+
"GraphState",
|
|
74
|
+
"GraphStructure",
|
|
75
|
+
# Submodules (advanced use)
|
|
76
|
+
"core",
|
|
77
|
+
"graph_assembly",
|
|
78
|
+
"graph_initialization",
|
|
79
|
+
"models",
|
|
80
|
+
"nodes",
|
|
81
|
+
"training",
|
|
82
|
+
"utils",
|
|
83
|
+
"experiments",
|
|
84
|
+
]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Core JAX predictive coding components."""
|
|
2
|
+
|
|
3
|
+
# Type definitions
|
|
4
|
+
from fabricpc.core.types import (
|
|
5
|
+
GraphParams,
|
|
6
|
+
GraphState,
|
|
7
|
+
GraphStructure,
|
|
8
|
+
NodeInfo,
|
|
9
|
+
EdgeInfo,
|
|
10
|
+
SlotInfo,
|
|
11
|
+
NodeParams,
|
|
12
|
+
NodeState,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Activation functions
|
|
16
|
+
from fabricpc.core.activations import (
|
|
17
|
+
ActivationBase,
|
|
18
|
+
IdentityActivation,
|
|
19
|
+
SigmoidActivation,
|
|
20
|
+
TanhActivation,
|
|
21
|
+
ReLUActivation,
|
|
22
|
+
LeakyReLUActivation,
|
|
23
|
+
GeluActivation,
|
|
24
|
+
SoftmaxActivation,
|
|
25
|
+
HardTanhActivation,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Energy functions
|
|
29
|
+
from fabricpc.core.energy import (
|
|
30
|
+
EnergyFunctional,
|
|
31
|
+
GaussianEnergy,
|
|
32
|
+
BernoulliEnergy,
|
|
33
|
+
CrossEntropyEnergy,
|
|
34
|
+
LaplacianEnergy,
|
|
35
|
+
HuberEnergy,
|
|
36
|
+
KLDivergenceEnergy,
|
|
37
|
+
compute_energy,
|
|
38
|
+
compute_energy_gradient,
|
|
39
|
+
get_energy_and_gradient,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Inference functions and classes
|
|
43
|
+
from fabricpc.core.inference import (
|
|
44
|
+
InferenceBase,
|
|
45
|
+
InferenceSGD,
|
|
46
|
+
InferenceSGDNormClip,
|
|
47
|
+
gather_inputs,
|
|
48
|
+
run_inference,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Initializers
|
|
52
|
+
from fabricpc.core.initializers import (
|
|
53
|
+
InitializerBase,
|
|
54
|
+
ZerosInitializer,
|
|
55
|
+
OnesInitializer,
|
|
56
|
+
NormalInitializer,
|
|
57
|
+
UniformInitializer,
|
|
58
|
+
XavierInitializer,
|
|
59
|
+
KaimingInitializer,
|
|
60
|
+
MuPCInitializer,
|
|
61
|
+
initialize,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
# Types
|
|
66
|
+
"GraphParams",
|
|
67
|
+
"GraphState",
|
|
68
|
+
"GraphStructure",
|
|
69
|
+
"NodeInfo",
|
|
70
|
+
"EdgeInfo",
|
|
71
|
+
"SlotInfo",
|
|
72
|
+
"NodeParams",
|
|
73
|
+
"NodeState",
|
|
74
|
+
# Activation functions
|
|
75
|
+
"ActivationBase",
|
|
76
|
+
"IdentityActivation",
|
|
77
|
+
"SigmoidActivation",
|
|
78
|
+
"TanhActivation",
|
|
79
|
+
"ReLUActivation",
|
|
80
|
+
"LeakyReLUActivation",
|
|
81
|
+
"GeluActivation",
|
|
82
|
+
"SoftmaxActivation",
|
|
83
|
+
"HardTanhActivation",
|
|
84
|
+
# Energy functions
|
|
85
|
+
"EnergyFunctional",
|
|
86
|
+
"GaussianEnergy",
|
|
87
|
+
"BernoulliEnergy",
|
|
88
|
+
"CrossEntropyEnergy",
|
|
89
|
+
"LaplacianEnergy",
|
|
90
|
+
"HuberEnergy",
|
|
91
|
+
"KLDivergenceEnergy",
|
|
92
|
+
"compute_energy",
|
|
93
|
+
"compute_energy_gradient",
|
|
94
|
+
"get_energy_and_gradient",
|
|
95
|
+
# Inference
|
|
96
|
+
"InferenceBase",
|
|
97
|
+
"InferenceSGD",
|
|
98
|
+
"InferenceSGDNormClip",
|
|
99
|
+
"gather_inputs",
|
|
100
|
+
"run_inference",
|
|
101
|
+
# Initializers
|
|
102
|
+
"InitializerBase",
|
|
103
|
+
"ZerosInitializer",
|
|
104
|
+
"OnesInitializer",
|
|
105
|
+
"NormalInitializer",
|
|
106
|
+
"UniformInitializer",
|
|
107
|
+
"XavierInitializer",
|
|
108
|
+
"KaimingInitializer",
|
|
109
|
+
"MuPCInitializer",
|
|
110
|
+
"initialize",
|
|
111
|
+
]
|
fabricpc/core/_frozen.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Shared immutability mixin for stateless config value objects.
|
|
2
|
+
|
|
3
|
+
``ActivationBase``, ``EnergyFunctional``, and ``InitializerBase`` place a single
|
|
4
|
+
default instance directly in node ``__init__`` signatures. A signature default
|
|
5
|
+
is evaluated once at import and shared by every defaulted call, so that instance
|
|
6
|
+
is only safe if it cannot be mutated. ``FrozenConfig`` is the single source of
|
|
7
|
+
that freeze for all three families.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import types
|
|
11
|
+
|
|
12
|
+
# Immutable scalar types accepted as config values. Tuples are accepted too,
|
|
13
|
+
# recursively. A future structured value (e.g. a per-channel alpha) is a tuple,
|
|
14
|
+
# not a list or an array.
|
|
15
|
+
_IMMUTABLE_SCALARS = (bool, int, float, str, bytes, type(None))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _validate_immutable(value, key, owner):
|
|
19
|
+
"""Reject any config value that is not an immutable scalar or a tuple of them."""
|
|
20
|
+
if isinstance(value, _IMMUTABLE_SCALARS):
|
|
21
|
+
return
|
|
22
|
+
if isinstance(value, tuple):
|
|
23
|
+
for item in value:
|
|
24
|
+
_validate_immutable(item, key, owner)
|
|
25
|
+
return
|
|
26
|
+
raise TypeError(
|
|
27
|
+
f"{owner} config value {key!r} must be an immutable scalar "
|
|
28
|
+
f"(int, float, str, bool, bytes, None) or a tuple of those; "
|
|
29
|
+
f"got {type(value).__name__}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FrozenConfig:
|
|
34
|
+
"""Freezes an instance after construction.
|
|
35
|
+
|
|
36
|
+
Once ``__init__`` has run, attributes cannot be set or deleted, and
|
|
37
|
+
``config`` is a read-only mapping whose keys cannot be added, removed, or
|
|
38
|
+
reassigned. Config values are validated at construction: only immutable
|
|
39
|
+
scalars (int, float, str, bool, bytes, None) and tuples of those are
|
|
40
|
+
accepted, so the whole object is immutable, not just its top level.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, **config):
|
|
44
|
+
for key, value in config.items():
|
|
45
|
+
_validate_immutable(value, key, type(self).__name__)
|
|
46
|
+
# object.__setattr__ bypasses the freeze below to set these two fields
|
|
47
|
+
# once; every later assignment goes through __setattr__ and is rejected.
|
|
48
|
+
object.__setattr__(self, "config", types.MappingProxyType(config))
|
|
49
|
+
object.__setattr__(self, "_frozen", True)
|
|
50
|
+
|
|
51
|
+
def __setattr__(self, name, value):
|
|
52
|
+
if getattr(self, "_frozen", False):
|
|
53
|
+
raise AttributeError(
|
|
54
|
+
f"{type(self).__name__} is immutable; cannot set {name!r}"
|
|
55
|
+
)
|
|
56
|
+
object.__setattr__(self, name, value)
|
|
57
|
+
|
|
58
|
+
def __delattr__(self, name):
|
|
59
|
+
raise AttributeError(f"{type(self).__name__} is immutable")
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Activation functions for predictive coding networks in JAX.
|
|
3
|
+
|
|
4
|
+
This module provides:
|
|
5
|
+
- ActivationBase abstract class with constructor-based configuration
|
|
6
|
+
- Built-in activations (Identity, Sigmoid, Tanh, ReLU, LeakyReLU, GELU, Softmax, HardTanh)
|
|
7
|
+
|
|
8
|
+
All functions are pure and compatible with JAX transformations (jit, vmap, grad).
|
|
9
|
+
|
|
10
|
+
User Extensibility
|
|
11
|
+
------------------
|
|
12
|
+
Users can create custom activations by extending ActivationBase:
|
|
13
|
+
|
|
14
|
+
class MyActivation(ActivationBase):
|
|
15
|
+
def __init__(self, temperature=1.0):
|
|
16
|
+
super().__init__(temperature=temperature)
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def forward(x, config=None):
|
|
20
|
+
temp = config.get("temperature", 1.0) if config else 1.0
|
|
21
|
+
return jnp.tanh(x / temp)
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def derivative(x, config=None):
|
|
25
|
+
temp = config.get("temperature", 1.0) if config else 1.0
|
|
26
|
+
t = jnp.tanh(x / temp)
|
|
27
|
+
return (1 - t**2) / temp
|
|
28
|
+
|
|
29
|
+
Usage
|
|
30
|
+
-----
|
|
31
|
+
Activations are instantiated with their parameters:
|
|
32
|
+
|
|
33
|
+
act = SigmoidActivation()
|
|
34
|
+
act = LeakyReLUActivation(alpha=0.02)
|
|
35
|
+
z_mu = type(act).forward(x, act.config)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import math
|
|
39
|
+
from abc import ABC, abstractmethod
|
|
40
|
+
from typing import Dict, Any
|
|
41
|
+
import jax.numpy as jnp
|
|
42
|
+
from jax import nn
|
|
43
|
+
|
|
44
|
+
from fabricpc.core._frozen import FrozenConfig
|
|
45
|
+
|
|
46
|
+
# =============================================================================
|
|
47
|
+
# Activation Base Class
|
|
48
|
+
# =============================================================================
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ActivationBase(FrozenConfig, ABC):
|
|
52
|
+
"""
|
|
53
|
+
Abstract base class for activation functions.
|
|
54
|
+
|
|
55
|
+
Activation functions define how pre-activation values are transformed.
|
|
56
|
+
Each activation provides both the forward function and its derivative.
|
|
57
|
+
|
|
58
|
+
All methods are static for JAX compatibility (pure functions, no state).
|
|
59
|
+
|
|
60
|
+
Instances are frozen after construction (see ``FrozenConfig``): attributes
|
|
61
|
+
cannot be set or deleted and ``config`` keys cannot be added, removed, or
|
|
62
|
+
reassigned, so one default instance is safe to share as a signature default.
|
|
63
|
+
The freeze is shallow: construct only with immutable scalar config values.
|
|
64
|
+
|
|
65
|
+
Required methods:
|
|
66
|
+
- forward(): Apply activation function
|
|
67
|
+
- derivative(): Compute derivative w.r.t. pre-activation
|
|
68
|
+
|
|
69
|
+
Example implementation:
|
|
70
|
+
class MyActivation(ActivationBase):
|
|
71
|
+
def __init__(self, temperature=1.0):
|
|
72
|
+
super().__init__(temperature=temperature)
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def forward(x, config=None):
|
|
76
|
+
temp = config.get("temperature", 1.0) if config else 1.0
|
|
77
|
+
return jnp.tanh(x / temp)
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def derivative(x, config=None):
|
|
81
|
+
temp = config.get("temperature", 1.0) if config else 1.0
|
|
82
|
+
t = jnp.tanh(x / temp)
|
|
83
|
+
return (1 - t**2) / temp
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def forward(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
89
|
+
"""
|
|
90
|
+
Apply activation function.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
x: Pre-activation values, any shape
|
|
94
|
+
config: Optional configuration dict for activation parameters
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Activated values, same shape as x
|
|
98
|
+
"""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
@abstractmethod
|
|
103
|
+
def derivative(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
104
|
+
"""
|
|
105
|
+
Compute derivative w.r.t. pre-activation.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
x: Pre-activation values, any shape
|
|
109
|
+
config: Optional configuration dict for activation parameters
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Derivative values, same shape as x
|
|
113
|
+
|
|
114
|
+
Note:
|
|
115
|
+
This method is NOT called by the default framework path — autodiff
|
|
116
|
+
computes gradient automatically via
|
|
117
|
+
``jax.value_and_grad`` in ``forward_and_latent_grads()``.
|
|
118
|
+
|
|
119
|
+
It is provided as a convenience for node subclasses that override
|
|
120
|
+
``forward_and_latent_grads()`` with explicit (non-autodiff) gradient
|
|
121
|
+
computation (see ``LinearExplicitGrad`` for the pattern).
|
|
122
|
+
"""
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def variance_gain(config: Dict[str, Any] = None) -> float:
|
|
127
|
+
"""
|
|
128
|
+
Return the Kaiming-style gain for variance preservation.
|
|
129
|
+
|
|
130
|
+
This is the factor g such that when pre-activations have
|
|
131
|
+
Var(z) = g^2, the post-activation output has Var(f(z)) ≈ 1.
|
|
132
|
+
Used by muPC to compensate for activation-induced variance
|
|
133
|
+
contraction in the forward scaling formula:
|
|
134
|
+
|
|
135
|
+
a = gain / sqrt(fan_in * K)
|
|
136
|
+
|
|
137
|
+
Subclasses should override with activation-specific values.
|
|
138
|
+
Default returns 1.0 (no correction, appropriate for identity).
|
|
139
|
+
"""
|
|
140
|
+
return 1.0
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def jacobian(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
144
|
+
"""
|
|
145
|
+
Full Jacobian matrix J_{ij} = df_i/dx_j, shape (..., D, D).
|
|
146
|
+
|
|
147
|
+
For element-wise activations, the Jacobian is diag(derivative(x)) —
|
|
148
|
+
use derivative() directly as the diagonal. Override this method for
|
|
149
|
+
non-element-wise activations (e.g., softmax) where off-diagonal
|
|
150
|
+
terms are needed.
|
|
151
|
+
|
|
152
|
+
This is a convenience for explicit (non-autodiff) gradient
|
|
153
|
+
implementations that need the full Jacobian.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
x: Pre-activation values, shape (..., D)
|
|
157
|
+
config: Optional configuration dict
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Jacobian matrix, shape (..., D, D)
|
|
161
|
+
"""
|
|
162
|
+
raise NotImplementedError(
|
|
163
|
+
"jacobian() not implemented for this activation. "
|
|
164
|
+
"For element-wise activations, use derivative() as the diagonal."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def jacobian_gain(config: Dict[str, Any] = None) -> float:
|
|
169
|
+
"""
|
|
170
|
+
Return the Jacobian compensation factor for muPC topdown gradient scaling.
|
|
171
|
+
|
|
172
|
+
In local PC inference, topdown gradients propagate one hop per step
|
|
173
|
+
through the Jacobian diag(act'(z)) @ (a*W). The per-hop gradient
|
|
174
|
+
attenuation is approximately variance_gain * rms(act'(z)), where
|
|
175
|
+
z ~ N(0, variance_gain^2). This factor compensates:
|
|
176
|
+
|
|
177
|
+
jacobian_gain = 1 / (variance_gain * rms(act'(z)))
|
|
178
|
+
|
|
179
|
+
so that topdown_grad_scale = a * jacobian_gain yields ~1.0 per-hop
|
|
180
|
+
gradient propagation factor.
|
|
181
|
+
|
|
182
|
+
Default returns 1.0 (exact for identity, ReLU, LeakyReLU where
|
|
183
|
+
variance_gain * rms(act') = 1.0 by construction).
|
|
184
|
+
"""
|
|
185
|
+
return 1.0
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# =============================================================================
|
|
189
|
+
# Built-in Activations
|
|
190
|
+
# =============================================================================
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class IdentityActivation(ActivationBase):
|
|
194
|
+
"""Identity activation: f(x) = x"""
|
|
195
|
+
|
|
196
|
+
def __init__(self):
|
|
197
|
+
super().__init__()
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def forward(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
201
|
+
return x
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def derivative(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
205
|
+
return jnp.ones_like(x)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class SigmoidActivation(ActivationBase):
|
|
209
|
+
"""Sigmoid activation: σ(x) = 1 / (1 + exp(-x))"""
|
|
210
|
+
|
|
211
|
+
def __init__(self):
|
|
212
|
+
super().__init__()
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def forward(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
216
|
+
return nn.sigmoid(x)
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def derivative(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
220
|
+
s = nn.sigmoid(x)
|
|
221
|
+
return s * (1 - s)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class TanhActivation(ActivationBase):
|
|
225
|
+
"""Tanh activation: (exp(x) - exp(-x)) / (exp(x) + exp(-x))"""
|
|
226
|
+
|
|
227
|
+
def __init__(self):
|
|
228
|
+
super().__init__()
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def forward(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
232
|
+
return jnp.tanh(x)
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
def derivative(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
236
|
+
t = jnp.tanh(x)
|
|
237
|
+
return 1 - t**2
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def variance_gain(config: Dict[str, Any] = None) -> float:
|
|
241
|
+
return math.sqrt(5.0 / 3.0)
|
|
242
|
+
|
|
243
|
+
@staticmethod
|
|
244
|
+
def jacobian_gain(config: Dict[str, Any] = None) -> float:
|
|
245
|
+
# rms(tanh'(z)) ≈ 0.6144 for z ~ N(0, 5/3)
|
|
246
|
+
# jacobian_gain = 1 / (sqrt(5/3) * 0.6144) ≈ 1.261
|
|
247
|
+
return 1.261
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class ReLUActivation(ActivationBase):
|
|
251
|
+
"""ReLU activation: max(0, x)"""
|
|
252
|
+
|
|
253
|
+
def __init__(self):
|
|
254
|
+
super().__init__()
|
|
255
|
+
|
|
256
|
+
@staticmethod
|
|
257
|
+
def forward(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
258
|
+
return nn.relu(x)
|
|
259
|
+
|
|
260
|
+
@staticmethod
|
|
261
|
+
def derivative(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
262
|
+
return (x > 0).astype(jnp.float32)
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def variance_gain(config: Dict[str, Any] = None) -> float:
|
|
266
|
+
return math.sqrt(2.0)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class LeakyReLUActivation(ActivationBase):
|
|
270
|
+
"""
|
|
271
|
+
Leaky ReLU activation: max(alpha * x, x)
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
alpha: Negative slope (default: 0.01)
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
def __init__(self, alpha=0.01):
|
|
278
|
+
super().__init__(alpha=alpha)
|
|
279
|
+
|
|
280
|
+
@staticmethod
|
|
281
|
+
def forward(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
282
|
+
alpha = config.get("alpha", 0.01) if config else 0.01
|
|
283
|
+
return jnp.where(x > 0, x, alpha * x)
|
|
284
|
+
|
|
285
|
+
@staticmethod
|
|
286
|
+
def derivative(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
287
|
+
alpha = config.get("alpha", 0.01) if config else 0.01
|
|
288
|
+
return jnp.where(x > 0, 1.0, alpha)
|
|
289
|
+
|
|
290
|
+
@staticmethod
|
|
291
|
+
def variance_gain(config: Dict[str, Any] = None) -> float:
|
|
292
|
+
alpha = config.get("alpha", 0.01) if config else 0.01
|
|
293
|
+
return math.sqrt(2.0 / (1.0 + alpha**2))
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class GeluActivation(ActivationBase):
|
|
297
|
+
"""GELU activation: x * 0.5 * (1 + erf(x / sqrt(2)))"""
|
|
298
|
+
|
|
299
|
+
def __init__(self):
|
|
300
|
+
super().__init__()
|
|
301
|
+
|
|
302
|
+
@staticmethod
|
|
303
|
+
def forward(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
304
|
+
return nn.gelu(x)
|
|
305
|
+
|
|
306
|
+
@staticmethod
|
|
307
|
+
def derivative(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
308
|
+
# Gelu(x): = x * normal_CDF
|
|
309
|
+
sqrt_2_over_pi = jnp.sqrt(2 / jnp.pi)
|
|
310
|
+
x_cubed = x**3
|
|
311
|
+
apx_norm_cdf = 0.5 * (
|
|
312
|
+
1 + jnp.tanh(sqrt_2_over_pi * (x + 0.044715 * x_cubed))
|
|
313
|
+
) # Approximation of normal CDF via tanh
|
|
314
|
+
norm_cdf_prime = (0.5 * sqrt_2_over_pi * (1 + 3 * 0.044715 * x**2)) * (
|
|
315
|
+
1 - jnp.tanh(sqrt_2_over_pi * (x + 0.044715 * x_cubed)) ** 2
|
|
316
|
+
)
|
|
317
|
+
return apx_norm_cdf + x * norm_cdf_prime
|
|
318
|
+
|
|
319
|
+
@staticmethod
|
|
320
|
+
def variance_gain(config: Dict[str, Any] = None) -> float:
|
|
321
|
+
return math.sqrt(2.0)
|
|
322
|
+
|
|
323
|
+
@staticmethod
|
|
324
|
+
def jacobian_gain(config: Dict[str, Any] = None) -> float:
|
|
325
|
+
# rms(gelu'(z)) ≈ 0.605 for z ~ N(0, 2)
|
|
326
|
+
# jacobian_gain = 1 / (sqrt(2) * 0.605) ≈ 1.168
|
|
327
|
+
return 1.168
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class SoftmaxActivation(ActivationBase):
|
|
331
|
+
"""Softmax activation: exp(x) / sum(exp(x)) along the last axis"""
|
|
332
|
+
|
|
333
|
+
def __init__(self):
|
|
334
|
+
super().__init__()
|
|
335
|
+
|
|
336
|
+
@staticmethod
|
|
337
|
+
def forward(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
338
|
+
exp_x = jnp.exp(
|
|
339
|
+
x - jnp.max(x, axis=-1, keepdims=True)
|
|
340
|
+
) # relative to max value for numerical stability
|
|
341
|
+
return exp_x / jnp.sum(exp_x, axis=-1, keepdims=True)
|
|
342
|
+
|
|
343
|
+
@staticmethod
|
|
344
|
+
def derivative(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
345
|
+
s = SoftmaxActivation.forward(x)
|
|
346
|
+
# Diagonal of the Jacobian diag(s) - s @ s.T.
|
|
347
|
+
# The off-diagonal terms are omitted; valid for element-wise PC gradients.
|
|
348
|
+
return s * (1 - s)
|
|
349
|
+
|
|
350
|
+
@staticmethod
|
|
351
|
+
def jacobian(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
352
|
+
"""Full softmax Jacobian: J_{ij} = s_i * (delta_{ij} - s_j)."""
|
|
353
|
+
s = SoftmaxActivation.forward(x, config)
|
|
354
|
+
eye = jnp.eye(s.shape[-1])
|
|
355
|
+
return jnp.expand_dims(s, -1) * (eye - jnp.expand_dims(s, -2))
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class HardTanhActivation(ActivationBase):
|
|
359
|
+
"""
|
|
360
|
+
Hard tanh activation: clip(x, min_val, max_val)
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
min_val: Minimum output value (default: -1.0)
|
|
364
|
+
max_val: Maximum output value (default: 1.0)
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
def __init__(self, min_val=-1.0, max_val=1.0):
|
|
368
|
+
super().__init__(min_val=min_val, max_val=max_val)
|
|
369
|
+
|
|
370
|
+
@staticmethod
|
|
371
|
+
def forward(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
372
|
+
min_val = config.get("min_val", -1.0) if config else -1.0
|
|
373
|
+
max_val = config.get("max_val", 1.0) if config else 1.0
|
|
374
|
+
return jnp.clip(x, min_val, max_val)
|
|
375
|
+
|
|
376
|
+
@staticmethod
|
|
377
|
+
def derivative(x: jnp.ndarray, config: Dict[str, Any] = None) -> jnp.ndarray:
|
|
378
|
+
min_val = config.get("min_val", -1.0) if config else -1.0
|
|
379
|
+
max_val = config.get("max_val", 1.0) if config else 1.0
|
|
380
|
+
return ((x > min_val) & (x < max_val)).astype(jnp.float32)
|
|
381
|
+
|
|
382
|
+
@staticmethod
|
|
383
|
+
def variance_gain(config: Dict[str, Any] = None) -> float:
|
|
384
|
+
return math.sqrt(5.0 / 3.0)
|
|
385
|
+
|
|
386
|
+
@staticmethod
|
|
387
|
+
def jacobian_gain(config: Dict[str, Any] = None) -> float:
|
|
388
|
+
# rms(hardtanh'(z)) ≈ 0.749 for z ~ N(0, 5/3)
|
|
389
|
+
# jacobian_gain = 1 / (sqrt(5/3) * 0.749) ≈ 1.035
|
|
390
|
+
return 1.035
|