tensor-optix 2.0.0__tar.gz → 2.0.1__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.
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/PKG-INFO +815 -815
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/README.md +746 -746
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/pyproject.toml +127 -127
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/setup.cfg +4 -4
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/__init__.py +145 -145
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/jax/__init__.py +4 -4
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/jax/flax_agent.py +116 -116
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/jax/flax_evaluator.py +27 -27
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/pytorch/__init__.py +6 -6
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/pytorch/torch_agent.py +170 -154
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/pytorch/torch_evaluator.py +73 -73
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/tensorflow/__init__.py +6 -6
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/tensorflow/tf_agent.py +163 -163
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/tensorflow/tf_evaluator.py +70 -70
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/__init__.py +17 -17
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/flax_ppo.py +333 -333
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/tf_dqn.py +229 -229
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/tf_ppo.py +315 -315
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/tf_ppo_continuous.py +333 -333
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/tf_sac.py +369 -369
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/tf_td3.py +335 -335
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/torch_dqn.py +302 -302
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/torch_ppo.py +357 -338
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/torch_ppo_continuous.py +364 -364
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/torch_rainbow_dqn.py +505 -505
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/torch_recurrent_ppo.py +498 -498
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/torch_sac.py +435 -398
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/torch_td3.py +465 -465
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/callbacks/__init__.py +6 -6
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/callbacks/rich_dashboard.py +350 -350
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/callbacks/tensorboard_callback.py +176 -176
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/callbacks/wandb_callback.py +203 -203
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/cli.py +143 -143
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/config.py +360 -360
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/__init__.py +12 -12
- tensor_optix-2.0.1/tensor_optix/core/async_callback.py +131 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/backoff_scheduler.py +315 -315
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/base_agent.py +197 -146
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/base_evaluator.py +191 -191
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/base_optimizer.py +58 -58
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/base_pipeline.py +42 -42
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/checkpoint_confirmation.py +125 -125
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/checkpoint_registry.py +215 -205
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/device.py +51 -51
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/diagnostic_controller.py +207 -207
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/ensemble_agent.py +78 -78
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/her_buffer.py +238 -238
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/loop_controller.py +684 -684
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/meta_controller.py +170 -170
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/noisy_linear.py +138 -138
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/normalizers.py +123 -123
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/policy_manager.py +810 -810
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/regime_detector.py +102 -102
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/replay_buffer.py +253 -253
- tensor_optix-2.0.1/tensor_optix/core/resume_state.py +72 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/reward_shape.py +135 -135
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/trajectory_buffer.py +82 -82
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/types.py +136 -130
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/distributed/__init__.py +4 -4
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/distributed/async_learner.py +400 -400
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/distributed/vtrace.py +101 -101
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/exploration/__init__.py +3 -3
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/exploration/rnd.py +199 -199
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/factory.py +798 -798
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ml/__init__.py +11 -11
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ml/dataset_pipeline.py +185 -185
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ml/loss_registry.py +289 -289
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ml/ml_agent.py +229 -229
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/__init__.py +26 -26
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/agent/__init__.py +3 -3
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/agent/graph_agent.py +610 -610
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/brain_network.py +518 -518
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/controller/__init__.py +3 -3
- tensor_optix-2.0.1/tensor_optix/neuroevo/controller/topology_callback.py +66 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/controller/topology_controller.py +676 -647
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/graph/__init__.py +23 -23
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/graph/neuron.py +547 -547
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/graph/neuron_graph.py +850 -850
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/graph/topology_ops.py +328 -328
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/hebbian.py +192 -192
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/neuromodulator.py +275 -275
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/optimizer/__init__.py +3 -3
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/optimizer/topology_aware_adam.py +104 -104
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/optimizer.py +384 -384
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/optimizers/__init__.py +7 -7
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/optimizers/adaptive_optimizer.py +294 -294
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/optimizers/backoff_optimizer.py +167 -167
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/optimizers/momentum_optimizer.py +210 -210
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/optimizers/pbt_optimizer.py +163 -163
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/optimizers/spsa_optimizer.py +230 -230
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/orchestrator.py +469 -469
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/pipeline/__init__.py +4 -4
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/pipeline/batch_pipeline.py +138 -138
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/pipeline/live_pipeline.py +161 -161
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/pipeline/vector_pipeline.py +167 -167
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/simple.py +285 -285
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/strategy/promotion.py +14 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/utils/__init__.py +3 -3
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/utils/window_size.py +149 -149
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix.egg-info/PKG-INFO +815 -815
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix.egg-info/SOURCES.txt +3 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tests/test_ml.py +478 -478
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ledger/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ledger/callback.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ledger/provenance.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ledger/records.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ledger/schema.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ledger/store.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/nanonets/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/nanonets/abstention.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/nanonets/features.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/nanonets/pooling.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/nanonets/selector.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/nanonets/smalln.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/runtime/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/runtime/baseline.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/runtime/experiment_stage.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/runtime/live_runtime.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/runtime/observer.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/runtime/promote.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/scheduler/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/scheduler/_mp_common.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/scheduler/budget.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/scheduler/dedup.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/scheduler/isolation.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/scheduler/pool.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/scheduler/retry.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/scheduler/worker.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/selection/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/selection/backfill.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/selection/selector.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/selection/uncertainty.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/strategy/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/strategy/candidate.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/strategy/comparator.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/strategy/grammar.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/strategy/runner.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/strategy/worker.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix.egg-info/dependency_links.txt +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix.egg-info/entry_points.txt +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix.egg-info/requires.txt +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix.egg-info/top_level.txt +0 -0
|
@@ -1,815 +1,815 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: tensor-optix
|
|
3
|
-
Version: 2.0.
|
|
4
|
-
Summary: Autonomous training loop for any sequential learning model — PPO, DQN, SAC, TD3, Rainbow DQN, Recurrent PPO for TensorFlow, PyTorch, and JAX/Flax; distributed async actor-learner (IMPALA + V-trace)
|
|
5
|
-
Author: sup3rus3r
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Requires-Python: >=3.11
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
Requires-Dist: gymnasium[box2d]>=1.0.0
|
|
10
|
-
Requires-Dist: numpy>=1.24.0
|
|
11
|
-
Requires-Dist: matplotlib>=3.7.0
|
|
12
|
-
Requires-Dist: optuna>=3.0.0
|
|
13
|
-
Requires-Dist: swig>=4.4.1
|
|
14
|
-
Requires-Dist: pyyaml>=6.0
|
|
15
|
-
Requires-Dist: torch>=2.11.0
|
|
16
|
-
Provides-Extra: tensorflow
|
|
17
|
-
Requires-Dist: tensorflow>=2.18.0; extra == "tensorflow"
|
|
18
|
-
Provides-Extra: tensorflow-gpu
|
|
19
|
-
Requires-Dist: tensorflow[and-cuda]>=2.18.0; sys_platform == "linux" and extra == "tensorflow-gpu"
|
|
20
|
-
Provides-Extra: torch
|
|
21
|
-
Requires-Dist: torch>=2.11.0; extra == "torch"
|
|
22
|
-
Requires-Dist: torchvision; extra == "torch"
|
|
23
|
-
Requires-Dist: torchaudio; extra == "torch"
|
|
24
|
-
Provides-Extra: jax
|
|
25
|
-
Requires-Dist: flax>=0.12.6; extra == "jax"
|
|
26
|
-
Requires-Dist: jax>=0.10.0; extra == "jax"
|
|
27
|
-
Requires-Dist: optax>=0.2.8; extra == "jax"
|
|
28
|
-
Provides-Extra: cuda
|
|
29
|
-
Requires-Dist: nvidia-cuda-nvcc-cu12; sys_platform == "linux" and extra == "cuda"
|
|
30
|
-
Requires-Dist: tensorflow[and-cuda]>=2.18.0; sys_platform == "linux" and extra == "cuda"
|
|
31
|
-
Provides-Extra: box2d
|
|
32
|
-
Requires-Dist: gymnasium[box2d]>=1.0.0; extra == "box2d"
|
|
33
|
-
Provides-Extra: atari
|
|
34
|
-
Requires-Dist: gymnasium[atari]>=1.0.0; extra == "atari"
|
|
35
|
-
Requires-Dist: ale-py>=0.8; extra == "atari"
|
|
36
|
-
Provides-Extra: mujoco
|
|
37
|
-
Requires-Dist: gymnasium[mujoco]>=1.0.0; extra == "mujoco"
|
|
38
|
-
Provides-Extra: all
|
|
39
|
-
Requires-Dist: tensorflow>=2.18.0; extra == "all"
|
|
40
|
-
Requires-Dist: torch>=2.11.0; extra == "all"
|
|
41
|
-
Requires-Dist: torchvision; extra == "all"
|
|
42
|
-
Requires-Dist: torchaudio; extra == "all"
|
|
43
|
-
Requires-Dist: gymnasium[atari]>=1.0.0; extra == "all"
|
|
44
|
-
Requires-Dist: gymnasium[mujoco]>=1.0.0; extra == "all"
|
|
45
|
-
Requires-Dist: ale-py>=0.8; extra == "all"
|
|
46
|
-
Requires-Dist: jax>=0.10.0; extra == "all"
|
|
47
|
-
Requires-Dist: flax>=0.12.6; extra == "all"
|
|
48
|
-
Requires-Dist: optax>=0.2.8; extra == "all"
|
|
49
|
-
Requires-Dist: torch>=2.11.0; extra == "all"
|
|
50
|
-
Provides-Extra: neuroevo
|
|
51
|
-
Requires-Dist: torch>=2.11.0; extra == "neuroevo"
|
|
52
|
-
Provides-Extra: onnx
|
|
53
|
-
Requires-Dist: onnx>=1.14; extra == "onnx"
|
|
54
|
-
Requires-Dist: onnxruntime>=1.16; extra == "onnx"
|
|
55
|
-
Requires-Dist: onnxscript>=0.1; extra == "onnx"
|
|
56
|
-
Provides-Extra: wandb
|
|
57
|
-
Requires-Dist: wandb>=0.16; extra == "wandb"
|
|
58
|
-
Provides-Extra: tensorboard
|
|
59
|
-
Requires-Dist: torch>=2.11.0; extra == "tensorboard"
|
|
60
|
-
Requires-Dist: tensorboard>=2.14; extra == "tensorboard"
|
|
61
|
-
Provides-Extra: dev
|
|
62
|
-
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
63
|
-
Requires-Dist: pytest-cov; extra == "dev"
|
|
64
|
-
Requires-Dist: black; extra == "dev"
|
|
65
|
-
Requires-Dist: ruff; extra == "dev"
|
|
66
|
-
Requires-Dist: mypy; extra == "dev"
|
|
67
|
-
Requires-Dist: twine>=5.0; extra == "dev"
|
|
68
|
-
Requires-Dist: torch>=2.11.0; extra == "dev"
|
|
69
|
-
|
|
70
|
-
# tensor-optix
|
|
71
|
-
|
|
72
|
-
tensor-optix is a training loop framework with statistical convergence control, online hyperparameter optimisation, and an optional neuroevolution subsystem for dynamic topology.
|
|
73
|
-
|
|
74
|
-
The core loop runs your agent against a pipeline, maintains a separate validation signal, and manages four states: ACTIVE, COOLING, DORMANT, and watchdog shutdown. Convergence is detected using a corrected t-test on the smoothed score slope plus lag-1 autocorrelation, not a fixed patience counter. Hyperparameters are updated every episode via SPSA gradient estimates, with automatic routing to momentum-based or sign-only updates depending on the autocorrelation structure of the score landscape. Checkpointing and rollback are driven by the validation signal only, never training score. On DORMANT, a MetaController evaluates the generalization gap and its slope to decide between spawning a policy variant, pruning the ensemble, or stopping.
|
|
75
|
-
|
|
76
|
-
The neuroevo subsystem (`pip install tensor-optix[neuroevo]`) represents the policy as a `NeuronGraph`: a mutable directed graph of heterogeneous scalar neurons (point, GRU, LSTM, trainable-GRU, trainable-LSTM) with variable-delay edges. Weights for excitatory and inhibitory neurons follow softplus Dale's Law: raw parameter θ maps to `softplus(θ)` (excitatory) or `-softplus(θ)` (inhibitory), eliminating gradient dead zones at weight boundaries. `TopologyController` runs as a loop callback and evaluates three independent signals per episode: improvement slope significance, residual autocorrelation structure, and gradient utilization across hidden neurons. All three must cross their thresholds before a grow operation fires. Pruning is by importance score (incident edge weight magnitude times mean absolute activation). Merging is by Pearson correlation of per-episode activation histories. After every structural mutation the controller calls `graph.invalidate_compile()` to reset for the new topology. `TopologyAwareAdam` resets momentum state for parameters affected by any structural change. `BrainNetwork` composes multiple named `NeuronGraph` regions with sparse learnable inter-region edges — when a neuron is pruned from a region, all inter-region edges referencing it are automatically cleaned up. `HebbianHook` accumulates co-activation products across each episode and applies an Oja-style weight update after the PPO gradient step. `NeuromodulatorSignal` maps a `RegimeDetector` output (trending / ranging / volatile) to simultaneous changes in Hebbian learning rate, entropy coefficient, and topology grow/prune thresholds.
|
|
77
|
-
|
|
78
|
-
The entire system, including neuroevo, is accessed through a six-method `BaseAgent` interface:
|
|
79
|
-
|
|
80
|
-
```python
|
|
81
|
-
class BaseAgent(ABC):
|
|
82
|
-
def act(self, observation) -> any: ...
|
|
83
|
-
def learn(self, episode_data: EpisodeData) -> dict: ...
|
|
84
|
-
def get_hyperparams(self) -> HyperparamSet: ...
|
|
85
|
-
def set_hyperparams(self, hyperparams: HyperparamSet) -> None: ...
|
|
86
|
-
def save_weights(self, path: str) -> None: ...
|
|
87
|
-
def load_weights(self, path: str) -> None: ...
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Fifteen agents are included across PyTorch, TensorFlow, and JAX/Flax. Bring your own by implementing the interface above.
|
|
91
|
-
|
|
92
|
-
---
|
|
93
|
-
|
|
94
|
-
## Install
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
# Core loop only, no algorithm implementations
|
|
98
|
-
pip install tensor-optix
|
|
99
|
-
|
|
100
|
-
# PyTorch algorithms
|
|
101
|
-
pip install tensor-optix[torch]
|
|
102
|
-
|
|
103
|
-
# TensorFlow algorithms
|
|
104
|
-
pip install tensor-optix[tensorflow]
|
|
105
|
-
|
|
106
|
-
# JAX/Flax
|
|
107
|
-
pip install tensor-optix[jax]
|
|
108
|
-
|
|
109
|
-
# Neuroevo (requires torch)
|
|
110
|
-
pip install tensor-optix[neuroevo]
|
|
111
|
-
|
|
112
|
-
# GPU (Linux/WSL2, CUDA 12.8)
|
|
113
|
-
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128
|
|
114
|
-
pip install tensor-optix[torch]
|
|
115
|
-
|
|
116
|
-
# All frameworks and environment extras
|
|
117
|
-
pip install tensor-optix[all]
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
Environment extras: `[box2d]`, `[atari]`, `[mujoco]`
|
|
121
|
-
Logging extras: `[wandb]`, `[tensorboard]`
|
|
122
|
-
Export: `[onnx]`
|
|
123
|
-
|
|
124
|
-
---
|
|
125
|
-
|
|
126
|
-
## Algorithms
|
|
127
|
-
|
|
128
|
-
All agents implement `BaseAgent` and are interchangeable with `RLOptimizer`.
|
|
129
|
-
|
|
130
|
-
### PyTorch
|
|
131
|
-
|
|
132
|
-
| Agent | Algorithm | Action Space |
|
|
133
|
-
|---|---|---|
|
|
134
|
-
| `TorchPPOAgent` | PPO + GAE-λ | Discrete |
|
|
135
|
-
| `TorchGaussianPPOAgent` | PPO | Continuous |
|
|
136
|
-
| `TorchRecurrentPPOAgent` | PPO + GRU/LSTM hidden state | Discrete |
|
|
137
|
-
| `TorchDQNAgent` | DQN + PER + n-step returns | Discrete |
|
|
138
|
-
| `TorchRainbowDQNAgent` | Rainbow DQN (NoisyNet, distributional, PER, n-step, dueling, double) | Discrete |
|
|
139
|
-
| `TorchSACAgent` | SAC, twin Q-critics, automatic entropy tuning | Continuous |
|
|
140
|
-
| `TorchTD3Agent` | TD3 | Continuous |
|
|
141
|
-
|
|
142
|
-
### TensorFlow
|
|
143
|
-
|
|
144
|
-
`TFPPOAgent`, `TFGaussianPPOAgent`, `TFDQNAgent`, `TFSACAgent`, `TFTDDAgent`
|
|
145
|
-
|
|
146
|
-
### JAX/Flax
|
|
147
|
-
|
|
148
|
-
`FlaxPPOAgent`
|
|
149
|
-
|
|
150
|
-
### Auto-selection
|
|
151
|
-
|
|
152
|
-
`make_agent` inspects the environment action space and returns a fully constructed agent. Pass an algorithm name as the first argument, or let it be inferred.
|
|
153
|
-
|
|
154
|
-
```python
|
|
155
|
-
from tensor_optix import make_agent
|
|
156
|
-
import gymnasium as gym
|
|
157
|
-
|
|
158
|
-
env = gym.make("LunarLanderContinuous-v3")
|
|
159
|
-
agent = make_agent(env) # -> TorchSACAgent (continuous)
|
|
160
|
-
agent = make_agent("SAC", env) # same, explicit
|
|
161
|
-
agent = make_agent(env, framework="tf") # -> TFSACAgent
|
|
162
|
-
agent = make_agent(env, deterministic=True) # -> TorchTD3Agent
|
|
163
|
-
|
|
164
|
-
# Discrete action spaces
|
|
165
|
-
env = gym.make("CartPole-v1")
|
|
166
|
-
agent = make_agent(env) # -> TorchPPOAgent (default discrete)
|
|
167
|
-
agent = make_agent(env, algorithm="DQN") # -> TorchDQNAgent
|
|
168
|
-
agent = make_agent(env, algorithm="RAINBOW") # -> TorchRainbowDQNAgent
|
|
169
|
-
|
|
170
|
-
# Neuroevo path: NeuronGraph + GraphAgent with Hebbian + TopologyController
|
|
171
|
-
agent = make_agent(env, neuroevo=True)
|
|
172
|
-
agent = make_agent("PPO", env, neuroevo=True, graph_hidden=16, hebbian_lr=1e-3)
|
|
173
|
-
|
|
174
|
-
# Feature-extractor mode: NeuronGraph -> features concatenated to obs, fed into SAC
|
|
175
|
-
agent = make_agent("SAC", env, neuroevo=True, neuroevo_mode="feature_extractor")
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
Neuroevo options: `graph_in` (input neurons, default `min(obs_dim, 16)`), `graph_hidden` (GRU neurons, default 8), `graph_out` (output neurons, default `act_dim + 1`), `hebbian_lr`, `hebbian_decay`, `grow_cooldown`.
|
|
179
|
-
|
|
180
|
-
`neuroevo_mode` controls how the graph is used:
|
|
181
|
-
- `"policy"` (default) — graph IS the policy, wrapped as `GraphAgent` (PPO-based)
|
|
182
|
-
- `"feature_extractor"` — graph runs in parallel as a feature extractor; its `graph_out`-dim output is concatenated with the raw observation before SAC actor/critic networks. The base SAC layer handles exploration and replay; the graph adds adaptive temporal features via GRU + LSTM neurons with Hebbian learning.
|
|
183
|
-
|
|
184
|
-
---
|
|
185
|
-
|
|
186
|
-
## One-line training with Optimizer
|
|
187
|
-
|
|
188
|
-
`Optimizer` wraps `RLOptimizer` with sensible defaults. It auto-computes `window_size`, wires neuroevo callbacks, and activates SPSA when the agent has `default_param_bounds`.
|
|
189
|
-
|
|
190
|
-
```python
|
|
191
|
-
from tensor_optix import make_agent, Optimizer
|
|
192
|
-
import gymnasium as gym
|
|
193
|
-
|
|
194
|
-
env = gym.make("HumanoidStandup-v5")
|
|
195
|
-
agent = make_agent("SAC", env, neuroevo=True)
|
|
196
|
-
opt = Optimizer(agent, env)
|
|
197
|
-
opt.run()
|
|
198
|
-
|
|
199
|
-
# Vectorized: 8 parallel envs
|
|
200
|
-
opt = Optimizer(agent, lambda: gym.make("CartPole-v1"), n_envs=8)
|
|
201
|
-
opt.run()
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
`optimal_window_size(env, algorithm)` computes the window size formula used internally: `clip(k * mean_episode_steps, 512, 8192)` where k=4.0 for on-policy (PPO) and k=1.0 for off-policy (SAC/TD3).
|
|
205
|
-
|
|
206
|
-
```python
|
|
207
|
-
from tensor_optix import optimal_window_size
|
|
208
|
-
window = optimal_window_size(env, "PPO") # e.g. 2000 for CartPole
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
---
|
|
212
|
-
|
|
213
|
-
## Pipelines
|
|
214
|
-
|
|
215
|
-
A pipeline steps an environment (or data source), collects `EpisodeData`, and yields it to the agent. Three implementations are provided.
|
|
216
|
-
|
|
217
|
-
```python
|
|
218
|
-
from tensor_optix import BatchPipeline, LivePipeline, VectorBatchPipeline
|
|
219
|
-
|
|
220
|
-
# Gymnasium env: steps continuously, no reset between windows
|
|
221
|
-
pipeline = BatchPipeline(env=gym.make("CartPole-v1"), agent=agent, window_size=200)
|
|
222
|
-
|
|
223
|
-
# External data stream: background thread with bounded queue, configurable episode boundaries
|
|
224
|
-
pipeline = LivePipeline(
|
|
225
|
-
data_source=MyFeed(),
|
|
226
|
-
agent=agent,
|
|
227
|
-
episode_boundary_fn=LivePipeline.every_n_seconds(300),
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
# N parallel envs via gymnasium.vector, sync or async subprocess
|
|
231
|
-
pipeline = VectorBatchPipeline(
|
|
232
|
-
env_fns=[lambda: gym.make("CartPole-v1")] * 8,
|
|
233
|
-
agent=agent,
|
|
234
|
-
window_size=200,
|
|
235
|
-
)
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
---
|
|
239
|
-
|
|
240
|
-
## The loop
|
|
241
|
-
|
|
242
|
-
```python
|
|
243
|
-
from tensor_optix import RLOptimizer
|
|
244
|
-
|
|
245
|
-
opt = RLOptimizer(
|
|
246
|
-
agent=agent,
|
|
247
|
-
pipeline=pipeline,
|
|
248
|
-
|
|
249
|
-
# Separate validation pipeline. All checkpoint and rollback decisions use val score only.
|
|
250
|
-
val_pipeline=val_pipeline,
|
|
251
|
-
rollback_on_degradation=True,
|
|
252
|
-
|
|
253
|
-
# Optional external scorer run at checkpoint evaluation (e.g. held-out backtest)
|
|
254
|
-
checkpoint_score_fn=lambda a: evaluate(a, held_out_env),
|
|
255
|
-
|
|
256
|
-
# Convergence parameters
|
|
257
|
-
dormant_threshold=10, # consecutive episodes without improvement -> DORMANT
|
|
258
|
-
min_episodes_before_dormant=50, # statistical warmup before convergence detection activates
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
opt.run()
|
|
262
|
-
opt.best_snapshot # -> PolicySnapshot: best weights + EvalMetrics + HyperparamSet
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
Loop state transitions: `ACTIVE` -> `COOLING` -> `DORMANT` -> watchdog shutdown or policy spawn.
|
|
266
|
-
|
|
267
|
-
On shutdown the loop restores best-known weights, not the final checkpoint.
|
|
268
|
-
|
|
269
|
-
---
|
|
270
|
-
|
|
271
|
-
## Hyperparameter optimisation
|
|
272
|
-
|
|
273
|
-
All optimisers operate in normalised [0, 1] parameter space and update every episode. No restarts required.
|
|
274
|
-
|
|
275
|
-
```python
|
|
276
|
-
from tensor_optix.optimizers import SPSAOptimizer, AdaptiveOptimizer
|
|
277
|
-
|
|
278
|
-
# SPSA: Rademacher perturbation vector, two-episode gradient estimate
|
|
279
|
-
optimizer = SPSAOptimizer(
|
|
280
|
-
param_bounds={"learning_rate": (1e-4, 3e-3), "clip_ratio": (0.1, 0.3)},
|
|
281
|
-
log_params={"learning_rate"}, # log-space normalisation for params spanning orders of magnitude
|
|
282
|
-
)
|
|
283
|
-
|
|
284
|
-
# AdaptiveOptimizer: routes between SPSA, Momentum, Backoff, and PBT
|
|
285
|
-
# based on lag-1 autocorrelation of the score stream and relative performance gap
|
|
286
|
-
optimizer = AdaptiveOptimizer(param_bounds={...})
|
|
287
|
-
|
|
288
|
-
opt = RLOptimizer(agent=agent, pipeline=pipeline, optimizer=optimizer)
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
| Optimizer | Routing condition |
|
|
292
|
-
|---|---|
|
|
293
|
-
| `SPSAOptimizer` | i.i.d. score noise, no autocorrelation structure |
|
|
294
|
-
| `MomentumOptimizer` | Positive lag-1 autocorrelation (smooth landscape) |
|
|
295
|
-
| `BackoffOptimizer` | Negative lag-1 autocorrelation (oscillating landscape, sign-only updates) |
|
|
296
|
-
| `PBTOptimizer` | Score below 20th percentile of history (exploit checkpoint population) |
|
|
297
|
-
| `AdaptiveOptimizer` | Routes automatically based on the two signals above |
|
|
298
|
-
|
|
299
|
-
### Trial-level search
|
|
300
|
-
|
|
301
|
-
`TrialOrchestrator` runs N independent short trials via Optuna TPE before the main run, then warm-starts from the best trial's weights and config.
|
|
302
|
-
|
|
303
|
-
```python
|
|
304
|
-
from tensor_optix import TrialOrchestrator
|
|
305
|
-
|
|
306
|
-
orch = TrialOrchestrator(
|
|
307
|
-
agent_factory=make_agent,
|
|
308
|
-
pipeline_factory=make_pipeline,
|
|
309
|
-
param_space={
|
|
310
|
-
"learning_rate": ("log_float", 1e-4, 3e-3),
|
|
311
|
-
"clip_ratio": ("float", 0.1, 0.3),
|
|
312
|
-
"batch_size": ("int", 32, 512),
|
|
313
|
-
},
|
|
314
|
-
n_trials=20,
|
|
315
|
-
trial_episodes=50,
|
|
316
|
-
)
|
|
317
|
-
best_config = orch.run()
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
---
|
|
321
|
-
|
|
322
|
-
## Ensemble and policy evolution
|
|
323
|
-
|
|
324
|
-
`PolicyManager` runs as a loop callback. On each DORMANT event, `MetaController` evaluates the generalization gap (train minus val, normalised) and its slope, and the validation improvement rate, then issues one of: SPAWN, PRUNE, or STOP. Spawned variants are cloned from the best checkpoint with perturbed hyperparameters. `EnsembleAgent` wraps all active variants behind the `BaseAgent` interface, with weighted action averaging.
|
|
325
|
-
|
|
326
|
-
```python
|
|
327
|
-
from tensor_optix import PolicyManager
|
|
328
|
-
|
|
329
|
-
pm = PolicyManager(registry, max_spawns=4)
|
|
330
|
-
cb = pm.as_callback(agent, agent_factory=make_agent)
|
|
331
|
-
cb.set_stop_fn(opt.stop)
|
|
332
|
-
opt.add_callback(cb)
|
|
333
|
-
opt.run()
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
---
|
|
337
|
-
|
|
338
|
-
## Callbacks
|
|
339
|
-
|
|
340
|
-
```python
|
|
341
|
-
from tensor_optix.callbacks import RichDashboardCallback, WandbCallback, TensorBoardCallback
|
|
342
|
-
|
|
343
|
-
opt.add_callback(RichDashboardCallback()) # Rich live terminal panel
|
|
344
|
-
opt.add_callback(WandbCallback(project="run"))
|
|
345
|
-
opt.add_callback(TensorBoardCallback(log_dir="./tb"))
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
Custom callbacks subclass `LoopCallback` and override any of:
|
|
349
|
-
|
|
350
|
-
```python
|
|
351
|
-
class LoopCallback:
|
|
352
|
-
def on_loop_start(self) -> None: ...
|
|
353
|
-
def on_loop_stop(self) -> None: ...
|
|
354
|
-
def on_episode_end(self, episode_id: int, eval_metrics) -> None: ...
|
|
355
|
-
def on_improvement(self, snapshot) -> None: ...
|
|
356
|
-
def on_plateau(self, episode_id: int, state) -> None: ...
|
|
357
|
-
def on_dormant(self, episode_id: int) -> None: ...
|
|
358
|
-
def on_degradation(self, episode_id: int, eval_metrics) -> None: ...
|
|
359
|
-
def on_hyperparam_update(self, old: dict, new: dict) -> None: ...
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
`HebbianHook` and `NeuromodulatorSignal` are both `LoopCallback` subclasses and can be passed directly to `Optimizer` or `RLOptimizer` via `callbacks=`.
|
|
363
|
-
|
|
364
|
-
---
|
|
365
|
-
|
|
366
|
-
## Distributed training (IMPALA + V-trace)
|
|
367
|
-
|
|
368
|
-
`AsyncActorLearner` implements IMPALA-style async actor-learner. N actor subprocesses read weights from shared memory (lock-free), collect trajectories, and push them to a queue. The learner dequeues trajectories, applies V-trace importance-sampling correction, and writes updated weights back to shared memory.
|
|
369
|
-
|
|
370
|
-
```python
|
|
371
|
-
from tensor_optix.distributed import AsyncActorLearner
|
|
372
|
-
|
|
373
|
-
learner = AsyncActorLearner(
|
|
374
|
-
actor=actor,
|
|
375
|
-
critic=critic,
|
|
376
|
-
optimizer=optimizer,
|
|
377
|
-
env_factory=lambda: gym.make("ALE/Pong-v5"),
|
|
378
|
-
n_actors=8,
|
|
379
|
-
trajectory_len=64,
|
|
380
|
-
)
|
|
381
|
-
stats = learner.run(max_steps=10_000_000)
|
|
382
|
-
# stats["steps_per_second"] -> ~4x single-process throughput on CPU
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
---
|
|
386
|
-
|
|
387
|
-
## Neuroevo
|
|
388
|
-
|
|
389
|
-
`NeuronGraph` is a mutable directed graph of scalar neurons with variable-delay edges. `GraphAgent` wraps it as a `BaseAgent` with PPO-style weight learning. `TopologyController` mutates the graph live during the training loop.
|
|
390
|
-
|
|
391
|
-
```bash
|
|
392
|
-
pip install tensor-optix[neuroevo]
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
### Graph construction
|
|
396
|
-
|
|
397
|
-
```python
|
|
398
|
-
from tensor_optix.neuroevo import NeuronGraph, GraphAgent, GRUNeuron, LSTMNeuron
|
|
399
|
-
|
|
400
|
-
graph = NeuronGraph()
|
|
401
|
-
|
|
402
|
-
for _ in range(4):
|
|
403
|
-
graph.add_neuron(role="input", activation="linear")
|
|
404
|
-
for _ in range(8):
|
|
405
|
-
graph.add_neuron(role="hidden", activation="tanh")
|
|
406
|
-
# or: graph.add_neuron(role="hidden", neuron=GRUNeuron())
|
|
407
|
-
# or: graph.add_neuron(role="hidden", neuron=LSTMNeuron())
|
|
408
|
-
graph.add_neuron(role="output", activation="linear") # last output neuron is the value head
|
|
409
|
-
|
|
410
|
-
graph.add_edge(src_id, dst_id, weight=0.0, delay=0) # feedforward (d=0)
|
|
411
|
-
graph.add_edge(src_id, dst_id, weight=0.0, delay=1) # recurrent (d>=1, reads from history buffer)
|
|
412
|
-
|
|
413
|
-
agent = GraphAgent(graph, obs_dim=4, n_actions=2)
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
All edges initialise at weight=0.0, which is function-preserving at insertion time.
|
|
417
|
-
|
|
418
|
-
### Neuron types
|
|
419
|
-
|
|
420
|
-
| Type | Hidden state | Gradient through state |
|
|
421
|
-
|---|---|---|
|
|
422
|
-
| `Neuron` | None (point neuron) | N/A |
|
|
423
|
-
| `GRUNeuron` | Scalar h, detached | No |
|
|
424
|
-
| `LSTMNeuron` | Scalar h and c, detached | No |
|
|
425
|
-
| `TrainableGRUNeuron` | Scalar h, not detached | Yes, up to chunk_len steps |
|
|
426
|
-
| `TrainableLSTMNeuron` | Scalar h and c, not detached | Yes, up to chunk_len steps |
|
|
427
|
-
|
|
428
|
-
All types implement the same protocol: `step()`, `importance()`, `can_merge_with()`, `make_relay()`, `split_copy()`. `NeuronGraph` and `TopologyController` are type-blind.
|
|
429
|
-
|
|
430
|
-
### Trainable recurrent neurons
|
|
431
|
-
|
|
432
|
-
`TrainableGRUNeuron` and `TrainableLSTMNeuron` set `is_recurrent = True`. `RecurrentGraphAgent` detects this flag and switches from shuffled-minibatch PPO to sequential chunk training with truncated BPTT.
|
|
433
|
-
|
|
434
|
-
```python
|
|
435
|
-
from tensor_optix.neuroevo import TrainableGRUNeuron, TrainableLSTMNeuron, RecurrentGraphAgent
|
|
436
|
-
|
|
437
|
-
graph = NeuronGraph()
|
|
438
|
-
# ... input and output neurons ...
|
|
439
|
-
graph.add_neuron(role="hidden", neuron=TrainableGRUNeuron())
|
|
440
|
-
graph.add_neuron(role="hidden", neuron=TrainableLSTMNeuron())
|
|
441
|
-
|
|
442
|
-
agent = RecurrentGraphAgent(
|
|
443
|
-
graph, obs_dim=4, n_actions=2,
|
|
444
|
-
hyperparams=HyperparamSet(params={"chunk_len": 64}),
|
|
445
|
-
)
|
|
446
|
-
# Falls back to standard shuffled-minibatch PPO if no recurrent neurons are present
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
### Topology controller
|
|
450
|
-
|
|
451
|
-
```python
|
|
452
|
-
from tensor_optix.neuroevo import TopologyController
|
|
453
|
-
|
|
454
|
-
controller = TopologyController.for_graph(
|
|
455
|
-
graph=graph,
|
|
456
|
-
scheduler=opt._scheduler,
|
|
457
|
-
grow_grad_threshold=0.7, # fraction of hidden neurons with |grad| > eps required to grow
|
|
458
|
-
prune_neuron_threshold=1e-4, # importance score below this -> prune candidate
|
|
459
|
-
prune_edge_threshold=1e-3, # |weight| below this for prune_edge_patience episodes -> prune
|
|
460
|
-
merge_similarity_threshold=0.95, # Pearson correlation threshold for merge
|
|
461
|
-
)
|
|
462
|
-
opt.add_callback(controller)
|
|
463
|
-
opt.run()
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
Grow fires only when all three signals agree:
|
|
467
|
-
1. Improvement slope t-test is not significant (gradient updates are not making progress)
|
|
468
|
-
2. Score residuals have significant autocorrelation (capacity is underutilised)
|
|
469
|
-
3. Gradient utilization exceeds `grow_grad_threshold` (existing neurons are saturated)
|
|
470
|
-
|
|
471
|
-
For multi-region graphs, use `TopologyController.for_brain(brain, scheduler=...)`. Each region gets independent signal buffers and cooldown timers. When a neuron is pruned from a region, all inter-region `BrainNetwork` edges referencing it are automatically removed.
|
|
472
|
-
|
|
473
|
-
### Save and load
|
|
474
|
-
|
|
475
|
-
Topology-aware checkpointing works identically to non-evo agents. The topology (neuron types, roles, edges) is saved alongside weights so the graph is fully reconstructed on load — no manual topology reconstruction required.
|
|
476
|
-
|
|
477
|
-
```python
|
|
478
|
-
# Save
|
|
479
|
-
agent.save_weights("checkpoint.pt")
|
|
480
|
-
|
|
481
|
-
# Load into any agent instance — topology is reconstructed automatically
|
|
482
|
-
agent2 = make_agent(env, neuroevo=True)
|
|
483
|
-
agent2.load_weights("checkpoint.pt")
|
|
484
|
-
|
|
485
|
-
# Or skip constructing an agent entirely
|
|
486
|
-
agent3 = GraphAgent.from_checkpoint("checkpoint.pt")
|
|
487
|
-
agent3.act(obs)
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
Rollback on degradation uses the same path internally — when the loop restores a best checkpoint, the topology at the time of that checkpoint is fully restored, even if the graph has grown or been pruned since.
|
|
491
|
-
|
|
492
|
-
### BrainNetwork
|
|
493
|
-
|
|
494
|
-
```python
|
|
495
|
-
from tensor_optix.neuroevo import BrainNetwork, TopologyController
|
|
496
|
-
|
|
497
|
-
brain = BrainNetwork()
|
|
498
|
-
brain.add_region("sensory", sensory_graph)
|
|
499
|
-
brain.add_region("memory", memory_graph)
|
|
500
|
-
brain.add_region("executive", executive_graph)
|
|
501
|
-
|
|
502
|
-
brain.add_pathway("sensory", "memory", n_connections=8, delay=1)
|
|
503
|
-
brain.add_pathway("memory", "executive", n_connections=8, delay=0)
|
|
504
|
-
|
|
505
|
-
controller = TopologyController.for_brain(brain, scheduler=opt._scheduler)
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
Inter-region edges are learnable parameters. Regions are executed in topological order each forward pass.
|
|
509
|
-
|
|
510
|
-
### Hebbian learning
|
|
511
|
-
|
|
512
|
-
`HebbianHook` applies an Oja-style local weight update after each episode. The rule is: `dw = eta * mean_t(h_pre * h_post) - lambda * w`. Call `record()` after each `act()` to accumulate co-activation products, then `apply()` after the PPO gradient step.
|
|
513
|
-
|
|
514
|
-
```python
|
|
515
|
-
from tensor_optix.neuroevo import HebbianHook
|
|
516
|
-
|
|
517
|
-
hook = HebbianHook(graph, hebbian_lr=1e-3, weight_decay=1e-4)
|
|
518
|
-
|
|
519
|
-
for step in episode:
|
|
520
|
-
action = agent.act(obs)
|
|
521
|
-
hook.record()
|
|
522
|
-
obs, reward, done, _ = env.step(action)
|
|
523
|
-
|
|
524
|
-
agent.learn(episode_data)
|
|
525
|
-
hook.apply()
|
|
526
|
-
hook.reset()
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
`HebbianHook` is a `LoopCallback` — pass it directly to `Optimizer` or `RLOptimizer` via `callbacks=` and it wires itself automatically. Use `HebbianHook.from_brain(brain, ...)` for `BrainNetwork` graphs.
|
|
530
|
-
|
|
531
|
-
### Neuromodulation
|
|
532
|
-
|
|
533
|
-
`NeuromodulatorSignal` takes a `RegimeDetector` classification and applies coordinated parameter changes across `HebbianHook`, `GraphAgent`, and `TopologyController` simultaneously. It is a `LoopCallback` and can be passed directly to `Optimizer`.
|
|
534
|
-
|
|
535
|
-
```python
|
|
536
|
-
from tensor_optix.neuroevo import NeuromodulatorSignal
|
|
537
|
-
from tensor_optix.core import RegimeDetector
|
|
538
|
-
|
|
539
|
-
signal = NeuromodulatorSignal(
|
|
540
|
-
detector=RegimeDetector(),
|
|
541
|
-
hebbian_hook=hook, # optional
|
|
542
|
-
agent=agent, # optional — modulates entropy_coef
|
|
543
|
-
topology_controller=tc, # optional — modulates grow/prune thresholds
|
|
544
|
-
)
|
|
545
|
-
|
|
546
|
-
# In your training loop, after each episode:
|
|
547
|
-
signal.step(metrics_history)
|
|
548
|
-
# trending -> lower entropy_coef, lower hebbian_lr (consolidate)
|
|
549
|
-
# ranging -> raise hebbian_lr, lower grow threshold (explore structure)
|
|
550
|
-
# volatile -> lower hebbian_lr, raise entropy_coef (cautious plasticity)
|
|
551
|
-
|
|
552
|
-
# Or wire as a callback — called automatically each episode
|
|
553
|
-
opt = Optimizer(agent, env, callbacks=[hook, signal])
|
|
554
|
-
opt.run()
|
|
555
|
-
```
|
|
556
|
-
|
|
557
|
-
### Dale's Law
|
|
558
|
-
|
|
559
|
-
```python
|
|
560
|
-
# clamp mode (default): outgoing weights clamped post-step
|
|
561
|
-
graph = NeuronGraph(dale_mode="clamp")
|
|
562
|
-
graph.add_neuron(role="hidden", activation="relu", cell_type="excitatory") # weights >= 0
|
|
563
|
-
graph.add_neuron(role="hidden", activation="tanh", cell_type="inhibitory") # weights <= 0
|
|
564
|
-
|
|
565
|
-
# softplus mode: raw parameter theta, effective weight = softplus(theta) * sign
|
|
566
|
-
# gradient-safe, no dead zone at the clamp boundary
|
|
567
|
-
# enforce_dale() is a no-op in this mode
|
|
568
|
-
graph = NeuronGraph(dale_mode="softplus")
|
|
569
|
-
w = graph.effective_weight(edge_id) # reads post-softplus value
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
### TopologyAwareAdam
|
|
573
|
-
|
|
574
|
-
Drop-in Adam replacement that resets (m, v) momentum state for parameters touched by a grow, prune, or merge operation. Stale momentum estimates from before a structural change would otherwise corrupt the first update on modified parameters.
|
|
575
|
-
|
|
576
|
-
```python
|
|
577
|
-
from tensor_optix.neuroevo import TopologyAwareAdam
|
|
578
|
-
|
|
579
|
-
optimizer = TopologyAwareAdam(graph.parameters(), lr=3e-4)
|
|
580
|
-
optimizer.notify_topology_change(new_params) # call after any topology mutation
|
|
581
|
-
```
|
|
582
|
-
|
|
583
|
-
### Compiled forward
|
|
584
|
-
|
|
585
|
-
`NeuronGraph` runs in **eager mode by default**. Because `_raw_forward` mutates Python-side neuron state (`neuron._current`, `push_history`), `torch.compile` cannot safely trace it without replaying those side effects. The default eager path is safe for training, recurrent neurons, and dynamic topologies.
|
|
586
|
-
|
|
587
|
-
If the topology is static and you manage neuron state externally, you can opt in to a compiled forward:
|
|
588
|
-
|
|
589
|
-
```python
|
|
590
|
-
graph.compile_forward() # one-time call; re-call after any topology mutation
|
|
591
|
-
```
|
|
592
|
-
|
|
593
|
-
The backend is selected automatically: `inductor` on Linux/macOS, `aot_eager` on Windows. Has no effect on PyTorch < 2.0.
|
|
594
|
-
|
|
595
|
-
`TopologyController` calls `graph.invalidate_compile()` after every grow, prune, and merge. If you mutate the graph directly outside the controller, call it yourself:
|
|
596
|
-
|
|
597
|
-
```python
|
|
598
|
-
graph.add_edge(src_id, dst_id, weight=0.0, delay=1)
|
|
599
|
-
graph.invalidate_compile()
|
|
600
|
-
```
|
|
601
|
-
|
|
602
|
-
`invalidate_compile()` rebuilds the matrix cache and resets to eager mode. If `compile_forward()` was previously called, it also evicts stale Dynamo kernels (`torch._dynamo.reset()`) and recompiles — this reset is process-global, so all `NeuronGraph` instances in the process retrace on their next forward call.
|
|
603
|
-
|
|
604
|
-
---
|
|
605
|
-
|
|
606
|
-
## ML support
|
|
607
|
-
|
|
608
|
-
tensor-optix also supports general ML training via `Optimizer`. Pass any `nn.Module` and a PyTorch `Dataset` or `DataLoader` — the same SPSA hyperparameter tuning, rollback on degradation, convergence detection, and checkpointing apply automatically.
|
|
609
|
-
|
|
610
|
-
```python
|
|
611
|
-
import tensor_optix as optix
|
|
612
|
-
import torch.nn as nn
|
|
613
|
-
|
|
614
|
-
# Supervised — loss auto-detected from dataset
|
|
615
|
-
model = nn.Sequential(nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10))
|
|
616
|
-
opt = optix.Optimizer(model, train_dataset)
|
|
617
|
-
opt.run()
|
|
618
|
-
|
|
619
|
-
# Explicit loss
|
|
620
|
-
opt = optix.Optimizer(model, train_dataset, loss="cross_entropy")
|
|
621
|
-
opt = optix.Optimizer(model, train_dataset, loss="mse")
|
|
622
|
-
|
|
623
|
-
# Autoencoder
|
|
624
|
-
opt = optix.Optimizer(autoencoder, train_dataset, loss="reconstruction")
|
|
625
|
-
|
|
626
|
-
# VAE — model must return (reconstruction, mu, logvar)
|
|
627
|
-
opt = optix.Optimizer(vae, train_dataset, loss="vae")
|
|
628
|
-
|
|
629
|
-
# Contrastive (SimCLR) — dataset yields (view1, view2) pairs
|
|
630
|
-
opt = optix.Optimizer(encoder, pairs_dataset, loss="contrastive")
|
|
631
|
-
|
|
632
|
-
# DataLoader works too
|
|
633
|
-
opt = optix.Optimizer(model, DataLoader(dataset, batch_size=64))
|
|
634
|
-
|
|
635
|
-
# Custom loss
|
|
636
|
-
opt = optix.Optimizer(model, dataset, loss=nn.HuberLoss())
|
|
637
|
-
opt.run()
|
|
638
|
-
```
|
|
639
|
-
|
|
640
|
-
Available `loss=` strings:
|
|
641
|
-
|
|
642
|
-
| String | Criterion | Use case |
|
|
643
|
-
|---|---|---|
|
|
644
|
-
| `"auto"` | detected from data | default |
|
|
645
|
-
| `"cross_entropy"` | `CrossEntropyLoss` | multi-class classification |
|
|
646
|
-
| `"bce"` | `BCEWithLogitsLoss` | binary classification |
|
|
647
|
-
| `"mse"` | `MSELoss` | regression |
|
|
648
|
-
| `"mae"` | `L1Loss` | regression, outlier-robust |
|
|
649
|
-
| `"huber"` | `HuberLoss` | regression, very outlier-robust |
|
|
650
|
-
| `"reconstruction"` | MSE(output, input) | autoencoders |
|
|
651
|
-
| `"vae"` | ELBO: recon + KL | variational autoencoders |
|
|
652
|
-
| `"contrastive"` | NT-Xent | SimCLR-style contrastive learning |
|
|
653
|
-
|
|
654
|
-
Any `nn.Module` or callable can be passed directly as `loss=`.
|
|
655
|
-
|
|
656
|
-
Save and load work identically to RL agents:
|
|
657
|
-
|
|
658
|
-
```python
|
|
659
|
-
agent = optix.Optimizer(model, dataset, loss="cross_entropy")
|
|
660
|
-
# Checkpoints written automatically to checkpoint_dir
|
|
661
|
-
# Load back:
|
|
662
|
-
from tensor_optix.ml import MLAgent
|
|
663
|
-
ml_agent.load_weights("checkpoint.pt")
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
---
|
|
667
|
-
|
|
668
|
-
## Core utilities
|
|
669
|
-
|
|
670
|
-
### Normalizers
|
|
671
|
-
|
|
672
|
-
Online Welford mean/variance. `ObsNormalizer` normalises observations. `RewardNormalizer` divides rewards by return standard deviation (not reward std), preserving sign.
|
|
673
|
-
|
|
674
|
-
```python
|
|
675
|
-
from tensor_optix.core.normalizers import ObsNormalizer, RewardNormalizer
|
|
676
|
-
|
|
677
|
-
obs_norm = ObsNormalizer(shape=(obs_dim,))
|
|
678
|
-
obs_norm.update(obs_batch)
|
|
679
|
-
normalized = obs_norm.normalize(obs)
|
|
680
|
-
|
|
681
|
-
rew_norm = RewardNormalizer()
|
|
682
|
-
```
|
|
683
|
-
|
|
684
|
-
### Hindsight Experience Replay
|
|
685
|
-
|
|
686
|
-
Wraps `PrioritizedReplayBuffer` with episode-level goal relabeling. Supports `future` (default), `final`, and `episode` relabeling strategies.
|
|
687
|
-
|
|
688
|
-
```python
|
|
689
|
-
from tensor_optix.core.her_buffer import HERBuffer
|
|
690
|
-
|
|
691
|
-
her = HERBuffer(obs_dim=obs_dim, act_dim=act_dim, goal_dim=goal_dim, strategy="future", k=4)
|
|
692
|
-
her.store_episode(obs_list, act_list, rew_list, next_obs_list, done_list,
|
|
693
|
-
achieved_goals, compute_reward_fn)
|
|
694
|
-
obs_b, act_b, rew_b, next_b, done_b, weights, idx, n = her.sample(batch_size)
|
|
695
|
-
```
|
|
696
|
-
|
|
697
|
-
### Checkpoint registry
|
|
698
|
-
|
|
699
|
-
```python
|
|
700
|
-
from tensor_optix.core.checkpoint_registry import CheckpointRegistry
|
|
701
|
-
|
|
702
|
-
registry = CheckpointRegistry(checkpoint_dir="./checkpoints", max_snapshots=10)
|
|
703
|
-
registry.save(agent, eval_metrics, hyperparams)
|
|
704
|
-
registry.load_best(agent)
|
|
705
|
-
registry.load_ensemble(agent, top_k=3) # stochastic weight averaging over top-k snapshots
|
|
706
|
-
```
|
|
707
|
-
|
|
708
|
-
### Regime detection
|
|
709
|
-
|
|
710
|
-
Classifies score history into one of three regimes using detrended coefficient of variation. Detrended CV measures noise around the trend, not raw score variance.
|
|
711
|
-
|
|
712
|
-
```python
|
|
713
|
-
from tensor_optix.core import RegimeDetector
|
|
714
|
-
|
|
715
|
-
detector = RegimeDetector()
|
|
716
|
-
regime = detector.detect(metrics_history) # "trending" | "ranging" | "volatile"
|
|
717
|
-
```
|
|
718
|
-
|
|
719
|
-
---
|
|
720
|
-
|
|
721
|
-
## Learning-to-Learn Engine
|
|
722
|
-
|
|
723
|
-
tensor-optix also includes an optional Learning-to-Learn Engine: a layer that makes experiment history actionable — recording every trial with enough structure to compare reliably, running experiments concurrently under explicit budgets, learning which training strategies help on which tasks, and (recommend-only, never automatic) proposing interventions for a human to review before anything is promoted to a deployed configuration.
|
|
724
|
-
|
|
725
|
-
### Experiment ledger
|
|
726
|
-
|
|
727
|
-
Every experiment — a trial, a strategy candidate, a re-evaluation of a deployed agent — is recorded to a SQLite-backed ledger with full provenance (code revision, library versions, hostname) and a typed lifecycle.
|
|
728
|
-
|
|
729
|
-
```python
|
|
730
|
-
from tensor_optix.ledger import ExperimentStore, ExperimentState
|
|
731
|
-
|
|
732
|
-
store = ExperimentStore("./experiments.sqlite3")
|
|
733
|
-
record = store.create_experiment(
|
|
734
|
-
task_env="CartPole-v1", algorithm="PPO", framework="torch",
|
|
735
|
-
seed=0, checkpoint_dir="./ckpt", solve_threshold=475.0,
|
|
736
|
-
)
|
|
737
|
-
store.start_experiment(record.experiment_id)
|
|
738
|
-
store.record_progress(record.experiment_id, best_primary_score=480.0)
|
|
739
|
-
store.finalize_experiment(record.experiment_id, state=ExperimentState.COMPLETED)
|
|
740
|
-
```
|
|
741
|
-
|
|
742
|
-
Writes are idempotent on `experiment_id`, and every method opens a short, single-statement transaction — nothing here ever holds a lock for the duration of training.
|
|
743
|
-
|
|
744
|
-
### Concurrent trial scheduler
|
|
745
|
-
|
|
746
|
-
`ConcurrentTrialScheduler` runs multiple Optuna trials as real OS processes, with the parent process as sole owner of the Optuna study and the ledger.
|
|
747
|
-
|
|
748
|
-
```python
|
|
749
|
-
from tensor_optix.scheduler import ConcurrentTrialScheduler
|
|
750
|
-
|
|
751
|
-
scheduler = ConcurrentTrialScheduler(
|
|
752
|
-
agent_factory=make_agent, pipeline_factory=make_pipeline,
|
|
753
|
-
param_space={"learning_rate": ("log_float", 1e-4, 3e-3)},
|
|
754
|
-
n_trials=20, n_workers=4, ledger_store=store,
|
|
755
|
-
task_env="CartPole-v1", algorithm="PPO", solve_threshold=475.0,
|
|
756
|
-
)
|
|
757
|
-
best_params, best_score = scheduler.run()
|
|
758
|
-
```
|
|
759
|
-
|
|
760
|
-
### Cross-task transfer (Nanonets)
|
|
761
|
-
|
|
762
|
-
`NanonetsSelector` pools evidence from other tasks' ledger history when deciding how to allocate a new task's trial budget — but only when a conservative abstention gate confirms a source task's data is actually comparable (median performance at or above the target's own 95th percentile, both normalized against each task's own solve threshold so differently-scaled/signed scores are never compared on raw terms). When nothing passes the gate, it falls back to the non-learned baseline rather than guess.
|
|
763
|
-
|
|
764
|
-
```python
|
|
765
|
-
from tensor_optix.nanonets import NanonetsSelector
|
|
766
|
-
|
|
767
|
-
selector = NanonetsSelector(store, source_task_envs=["Acrobot-v1"])
|
|
768
|
-
decision = selector.propose(task_env="CartPole-v1", methods=["TPESampler"], budget=10)
|
|
769
|
-
print(decision.rationale) # states explicitly whether it pooled or fell back
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
### Strategy discovery
|
|
773
|
-
|
|
774
|
-
A small, closed set of typed training-procedure operations (learning-rate trend adjustment, checkpoint rollback, exploration bounds, optimizer-method choice, budget allocation, schedule changes, policy-variant selection) — never arbitrary generated code. Each candidate is run paired against a no-strategy baseline, compared via a conservative confidence-interval gate, and checked against a 6-criterion promotion policy.
|
|
775
|
-
|
|
776
|
-
```python
|
|
777
|
-
from tensor_optix.strategy import LearningRateTrendOp, StrategyCandidate, StrategyTrialRunner, PromotionPolicy
|
|
778
|
-
|
|
779
|
-
candidate = StrategyCandidate.from_op(LearningRateTrendOp(window=5, increase_factor=1.2))
|
|
780
|
-
runner = StrategyTrialRunner(
|
|
781
|
-
agent_factory=make_agent, pipeline_factory=make_pipeline,
|
|
782
|
-
trial_steps=50_000, ledger_store=store, task_env="CartPole-v1",
|
|
783
|
-
algorithm="PPO", solve_threshold=475.0,
|
|
784
|
-
)
|
|
785
|
-
results = runner.run_candidate(candidate, seeds=[0, 1, 2])
|
|
786
|
-
|
|
787
|
-
policy = PromotionPolicy(store, margin=5.0, min_seeds=3, approval_mode="human")
|
|
788
|
-
verdict = policy.evaluate(candidate_id=candidate.candidate_id, train_task_envs=["CartPole-v1"], held_out_task_envs=[])
|
|
789
|
-
```
|
|
790
|
-
|
|
791
|
-
`approval_mode="human"` (the default) means `verdict.criteria["human_reviewed"]` can only ever become `True` via a separate, explicit `store.approve_strategy(...)` call — `PromotionPolicy.evaluate()` never sets it itself.
|
|
792
|
-
|
|
793
|
-
### Live adaptive runtime
|
|
794
|
-
|
|
795
|
-
`LiveAdaptiveRuntime` runs the full observe → detect → propose → experiment → evaluate → recommend loop against a long-running deployed agent, standing in for live inference feedback. It is **recommend-only**: it issues durable, auditable recommendation records to the ledger, but the loop itself never calls `approve_strategy` and never changes which agent is deployed. Promoting a candidate requires two separate, explicit human actions outside the loop (`store.approve_strategy(...)`, then `promote_candidate_to_baseline(...)`).
|
|
796
|
-
|
|
797
|
-
```python
|
|
798
|
-
from tensor_optix.runtime import LiveAdaptiveRuntime, RuntimeConfig
|
|
799
|
-
|
|
800
|
-
config = RuntimeConfig(task_env="CartPole-v1", algorithm="PPO", max_cycles=20)
|
|
801
|
-
runtime = LiveAdaptiveRuntime(store, config, agent_factory=make_agent, pipeline_factory=make_pipeline)
|
|
802
|
-
observations, recommendations = runtime.run()
|
|
803
|
-
```
|
|
804
|
-
|
|
805
|
-
Full guide: [Learning-to-Learn Engine](https://sup3rus3r.github.io/tensor-optix/guides/learning-to-learn/).
|
|
806
|
-
|
|
807
|
-
---
|
|
808
|
-
|
|
809
|
-
## Requirements
|
|
810
|
-
|
|
811
|
-
- Python >= 3.11
|
|
812
|
-
- gymnasium >= 1.0
|
|
813
|
-
- numpy >= 1.24
|
|
814
|
-
|
|
815
|
-
The core loop, `PolicyManager`, and all ensemble and evolution logic have no framework dependency. Framework installs are opt-in via extras.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tensor-optix
|
|
3
|
+
Version: 2.0.1
|
|
4
|
+
Summary: Autonomous training loop for any sequential learning model — PPO, DQN, SAC, TD3, Rainbow DQN, Recurrent PPO for TensorFlow, PyTorch, and JAX/Flax; distributed async actor-learner (IMPALA + V-trace)
|
|
5
|
+
Author: sup3rus3r
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: gymnasium[box2d]>=1.0.0
|
|
10
|
+
Requires-Dist: numpy>=1.24.0
|
|
11
|
+
Requires-Dist: matplotlib>=3.7.0
|
|
12
|
+
Requires-Dist: optuna>=3.0.0
|
|
13
|
+
Requires-Dist: swig>=4.4.1
|
|
14
|
+
Requires-Dist: pyyaml>=6.0
|
|
15
|
+
Requires-Dist: torch>=2.11.0
|
|
16
|
+
Provides-Extra: tensorflow
|
|
17
|
+
Requires-Dist: tensorflow>=2.18.0; extra == "tensorflow"
|
|
18
|
+
Provides-Extra: tensorflow-gpu
|
|
19
|
+
Requires-Dist: tensorflow[and-cuda]>=2.18.0; sys_platform == "linux" and extra == "tensorflow-gpu"
|
|
20
|
+
Provides-Extra: torch
|
|
21
|
+
Requires-Dist: torch>=2.11.0; extra == "torch"
|
|
22
|
+
Requires-Dist: torchvision; extra == "torch"
|
|
23
|
+
Requires-Dist: torchaudio; extra == "torch"
|
|
24
|
+
Provides-Extra: jax
|
|
25
|
+
Requires-Dist: flax>=0.12.6; extra == "jax"
|
|
26
|
+
Requires-Dist: jax>=0.10.0; extra == "jax"
|
|
27
|
+
Requires-Dist: optax>=0.2.8; extra == "jax"
|
|
28
|
+
Provides-Extra: cuda
|
|
29
|
+
Requires-Dist: nvidia-cuda-nvcc-cu12; sys_platform == "linux" and extra == "cuda"
|
|
30
|
+
Requires-Dist: tensorflow[and-cuda]>=2.18.0; sys_platform == "linux" and extra == "cuda"
|
|
31
|
+
Provides-Extra: box2d
|
|
32
|
+
Requires-Dist: gymnasium[box2d]>=1.0.0; extra == "box2d"
|
|
33
|
+
Provides-Extra: atari
|
|
34
|
+
Requires-Dist: gymnasium[atari]>=1.0.0; extra == "atari"
|
|
35
|
+
Requires-Dist: ale-py>=0.8; extra == "atari"
|
|
36
|
+
Provides-Extra: mujoco
|
|
37
|
+
Requires-Dist: gymnasium[mujoco]>=1.0.0; extra == "mujoco"
|
|
38
|
+
Provides-Extra: all
|
|
39
|
+
Requires-Dist: tensorflow>=2.18.0; extra == "all"
|
|
40
|
+
Requires-Dist: torch>=2.11.0; extra == "all"
|
|
41
|
+
Requires-Dist: torchvision; extra == "all"
|
|
42
|
+
Requires-Dist: torchaudio; extra == "all"
|
|
43
|
+
Requires-Dist: gymnasium[atari]>=1.0.0; extra == "all"
|
|
44
|
+
Requires-Dist: gymnasium[mujoco]>=1.0.0; extra == "all"
|
|
45
|
+
Requires-Dist: ale-py>=0.8; extra == "all"
|
|
46
|
+
Requires-Dist: jax>=0.10.0; extra == "all"
|
|
47
|
+
Requires-Dist: flax>=0.12.6; extra == "all"
|
|
48
|
+
Requires-Dist: optax>=0.2.8; extra == "all"
|
|
49
|
+
Requires-Dist: torch>=2.11.0; extra == "all"
|
|
50
|
+
Provides-Extra: neuroevo
|
|
51
|
+
Requires-Dist: torch>=2.11.0; extra == "neuroevo"
|
|
52
|
+
Provides-Extra: onnx
|
|
53
|
+
Requires-Dist: onnx>=1.14; extra == "onnx"
|
|
54
|
+
Requires-Dist: onnxruntime>=1.16; extra == "onnx"
|
|
55
|
+
Requires-Dist: onnxscript>=0.1; extra == "onnx"
|
|
56
|
+
Provides-Extra: wandb
|
|
57
|
+
Requires-Dist: wandb>=0.16; extra == "wandb"
|
|
58
|
+
Provides-Extra: tensorboard
|
|
59
|
+
Requires-Dist: torch>=2.11.0; extra == "tensorboard"
|
|
60
|
+
Requires-Dist: tensorboard>=2.14; extra == "tensorboard"
|
|
61
|
+
Provides-Extra: dev
|
|
62
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
63
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
64
|
+
Requires-Dist: black; extra == "dev"
|
|
65
|
+
Requires-Dist: ruff; extra == "dev"
|
|
66
|
+
Requires-Dist: mypy; extra == "dev"
|
|
67
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
68
|
+
Requires-Dist: torch>=2.11.0; extra == "dev"
|
|
69
|
+
|
|
70
|
+
# tensor-optix
|
|
71
|
+
|
|
72
|
+
tensor-optix is a training loop framework with statistical convergence control, online hyperparameter optimisation, and an optional neuroevolution subsystem for dynamic topology.
|
|
73
|
+
|
|
74
|
+
The core loop runs your agent against a pipeline, maintains a separate validation signal, and manages four states: ACTIVE, COOLING, DORMANT, and watchdog shutdown. Convergence is detected using a corrected t-test on the smoothed score slope plus lag-1 autocorrelation, not a fixed patience counter. Hyperparameters are updated every episode via SPSA gradient estimates, with automatic routing to momentum-based or sign-only updates depending on the autocorrelation structure of the score landscape. Checkpointing and rollback are driven by the validation signal only, never training score. On DORMANT, a MetaController evaluates the generalization gap and its slope to decide between spawning a policy variant, pruning the ensemble, or stopping.
|
|
75
|
+
|
|
76
|
+
The neuroevo subsystem (`pip install tensor-optix[neuroevo]`) represents the policy as a `NeuronGraph`: a mutable directed graph of heterogeneous scalar neurons (point, GRU, LSTM, trainable-GRU, trainable-LSTM) with variable-delay edges. Weights for excitatory and inhibitory neurons follow softplus Dale's Law: raw parameter θ maps to `softplus(θ)` (excitatory) or `-softplus(θ)` (inhibitory), eliminating gradient dead zones at weight boundaries. `TopologyController` runs as a loop callback and evaluates three independent signals per episode: improvement slope significance, residual autocorrelation structure, and gradient utilization across hidden neurons. All three must cross their thresholds before a grow operation fires. Pruning is by importance score (incident edge weight magnitude times mean absolute activation). Merging is by Pearson correlation of per-episode activation histories. After every structural mutation the controller calls `graph.invalidate_compile()` to reset for the new topology. `TopologyAwareAdam` resets momentum state for parameters affected by any structural change. `BrainNetwork` composes multiple named `NeuronGraph` regions with sparse learnable inter-region edges — when a neuron is pruned from a region, all inter-region edges referencing it are automatically cleaned up. `HebbianHook` accumulates co-activation products across each episode and applies an Oja-style weight update after the PPO gradient step. `NeuromodulatorSignal` maps a `RegimeDetector` output (trending / ranging / volatile) to simultaneous changes in Hebbian learning rate, entropy coefficient, and topology grow/prune thresholds.
|
|
77
|
+
|
|
78
|
+
The entire system, including neuroevo, is accessed through a six-method `BaseAgent` interface:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
class BaseAgent(ABC):
|
|
82
|
+
def act(self, observation) -> any: ...
|
|
83
|
+
def learn(self, episode_data: EpisodeData) -> dict: ...
|
|
84
|
+
def get_hyperparams(self) -> HyperparamSet: ...
|
|
85
|
+
def set_hyperparams(self, hyperparams: HyperparamSet) -> None: ...
|
|
86
|
+
def save_weights(self, path: str) -> None: ...
|
|
87
|
+
def load_weights(self, path: str) -> None: ...
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Fifteen agents are included across PyTorch, TensorFlow, and JAX/Flax. Bring your own by implementing the interface above.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Install
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Core loop only, no algorithm implementations
|
|
98
|
+
pip install tensor-optix
|
|
99
|
+
|
|
100
|
+
# PyTorch algorithms
|
|
101
|
+
pip install tensor-optix[torch]
|
|
102
|
+
|
|
103
|
+
# TensorFlow algorithms
|
|
104
|
+
pip install tensor-optix[tensorflow]
|
|
105
|
+
|
|
106
|
+
# JAX/Flax
|
|
107
|
+
pip install tensor-optix[jax]
|
|
108
|
+
|
|
109
|
+
# Neuroevo (requires torch)
|
|
110
|
+
pip install tensor-optix[neuroevo]
|
|
111
|
+
|
|
112
|
+
# GPU (Linux/WSL2, CUDA 12.8)
|
|
113
|
+
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128
|
|
114
|
+
pip install tensor-optix[torch]
|
|
115
|
+
|
|
116
|
+
# All frameworks and environment extras
|
|
117
|
+
pip install tensor-optix[all]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Environment extras: `[box2d]`, `[atari]`, `[mujoco]`
|
|
121
|
+
Logging extras: `[wandb]`, `[tensorboard]`
|
|
122
|
+
Export: `[onnx]`
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Algorithms
|
|
127
|
+
|
|
128
|
+
All agents implement `BaseAgent` and are interchangeable with `RLOptimizer`.
|
|
129
|
+
|
|
130
|
+
### PyTorch
|
|
131
|
+
|
|
132
|
+
| Agent | Algorithm | Action Space |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| `TorchPPOAgent` | PPO + GAE-λ | Discrete |
|
|
135
|
+
| `TorchGaussianPPOAgent` | PPO | Continuous |
|
|
136
|
+
| `TorchRecurrentPPOAgent` | PPO + GRU/LSTM hidden state | Discrete |
|
|
137
|
+
| `TorchDQNAgent` | DQN + PER + n-step returns | Discrete |
|
|
138
|
+
| `TorchRainbowDQNAgent` | Rainbow DQN (NoisyNet, distributional, PER, n-step, dueling, double) | Discrete |
|
|
139
|
+
| `TorchSACAgent` | SAC, twin Q-critics, automatic entropy tuning | Continuous |
|
|
140
|
+
| `TorchTD3Agent` | TD3 | Continuous |
|
|
141
|
+
|
|
142
|
+
### TensorFlow
|
|
143
|
+
|
|
144
|
+
`TFPPOAgent`, `TFGaussianPPOAgent`, `TFDQNAgent`, `TFSACAgent`, `TFTDDAgent`
|
|
145
|
+
|
|
146
|
+
### JAX/Flax
|
|
147
|
+
|
|
148
|
+
`FlaxPPOAgent`
|
|
149
|
+
|
|
150
|
+
### Auto-selection
|
|
151
|
+
|
|
152
|
+
`make_agent` inspects the environment action space and returns a fully constructed agent. Pass an algorithm name as the first argument, or let it be inferred.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from tensor_optix import make_agent
|
|
156
|
+
import gymnasium as gym
|
|
157
|
+
|
|
158
|
+
env = gym.make("LunarLanderContinuous-v3")
|
|
159
|
+
agent = make_agent(env) # -> TorchSACAgent (continuous)
|
|
160
|
+
agent = make_agent("SAC", env) # same, explicit
|
|
161
|
+
agent = make_agent(env, framework="tf") # -> TFSACAgent
|
|
162
|
+
agent = make_agent(env, deterministic=True) # -> TorchTD3Agent
|
|
163
|
+
|
|
164
|
+
# Discrete action spaces
|
|
165
|
+
env = gym.make("CartPole-v1")
|
|
166
|
+
agent = make_agent(env) # -> TorchPPOAgent (default discrete)
|
|
167
|
+
agent = make_agent(env, algorithm="DQN") # -> TorchDQNAgent
|
|
168
|
+
agent = make_agent(env, algorithm="RAINBOW") # -> TorchRainbowDQNAgent
|
|
169
|
+
|
|
170
|
+
# Neuroevo path: NeuronGraph + GraphAgent with Hebbian + TopologyController
|
|
171
|
+
agent = make_agent(env, neuroevo=True)
|
|
172
|
+
agent = make_agent("PPO", env, neuroevo=True, graph_hidden=16, hebbian_lr=1e-3)
|
|
173
|
+
|
|
174
|
+
# Feature-extractor mode: NeuronGraph -> features concatenated to obs, fed into SAC
|
|
175
|
+
agent = make_agent("SAC", env, neuroevo=True, neuroevo_mode="feature_extractor")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Neuroevo options: `graph_in` (input neurons, default `min(obs_dim, 16)`), `graph_hidden` (GRU neurons, default 8), `graph_out` (output neurons, default `act_dim + 1`), `hebbian_lr`, `hebbian_decay`, `grow_cooldown`.
|
|
179
|
+
|
|
180
|
+
`neuroevo_mode` controls how the graph is used:
|
|
181
|
+
- `"policy"` (default) — graph IS the policy, wrapped as `GraphAgent` (PPO-based)
|
|
182
|
+
- `"feature_extractor"` — graph runs in parallel as a feature extractor; its `graph_out`-dim output is concatenated with the raw observation before SAC actor/critic networks. The base SAC layer handles exploration and replay; the graph adds adaptive temporal features via GRU + LSTM neurons with Hebbian learning.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## One-line training with Optimizer
|
|
187
|
+
|
|
188
|
+
`Optimizer` wraps `RLOptimizer` with sensible defaults. It auto-computes `window_size`, wires neuroevo callbacks, and activates SPSA when the agent has `default_param_bounds`.
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from tensor_optix import make_agent, Optimizer
|
|
192
|
+
import gymnasium as gym
|
|
193
|
+
|
|
194
|
+
env = gym.make("HumanoidStandup-v5")
|
|
195
|
+
agent = make_agent("SAC", env, neuroevo=True)
|
|
196
|
+
opt = Optimizer(agent, env)
|
|
197
|
+
opt.run()
|
|
198
|
+
|
|
199
|
+
# Vectorized: 8 parallel envs
|
|
200
|
+
opt = Optimizer(agent, lambda: gym.make("CartPole-v1"), n_envs=8)
|
|
201
|
+
opt.run()
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`optimal_window_size(env, algorithm)` computes the window size formula used internally: `clip(k * mean_episode_steps, 512, 8192)` where k=4.0 for on-policy (PPO) and k=1.0 for off-policy (SAC/TD3).
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from tensor_optix import optimal_window_size
|
|
208
|
+
window = optimal_window_size(env, "PPO") # e.g. 2000 for CartPole
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Pipelines
|
|
214
|
+
|
|
215
|
+
A pipeline steps an environment (or data source), collects `EpisodeData`, and yields it to the agent. Three implementations are provided.
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from tensor_optix import BatchPipeline, LivePipeline, VectorBatchPipeline
|
|
219
|
+
|
|
220
|
+
# Gymnasium env: steps continuously, no reset between windows
|
|
221
|
+
pipeline = BatchPipeline(env=gym.make("CartPole-v1"), agent=agent, window_size=200)
|
|
222
|
+
|
|
223
|
+
# External data stream: background thread with bounded queue, configurable episode boundaries
|
|
224
|
+
pipeline = LivePipeline(
|
|
225
|
+
data_source=MyFeed(),
|
|
226
|
+
agent=agent,
|
|
227
|
+
episode_boundary_fn=LivePipeline.every_n_seconds(300),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# N parallel envs via gymnasium.vector, sync or async subprocess
|
|
231
|
+
pipeline = VectorBatchPipeline(
|
|
232
|
+
env_fns=[lambda: gym.make("CartPole-v1")] * 8,
|
|
233
|
+
agent=agent,
|
|
234
|
+
window_size=200,
|
|
235
|
+
)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## The loop
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
from tensor_optix import RLOptimizer
|
|
244
|
+
|
|
245
|
+
opt = RLOptimizer(
|
|
246
|
+
agent=agent,
|
|
247
|
+
pipeline=pipeline,
|
|
248
|
+
|
|
249
|
+
# Separate validation pipeline. All checkpoint and rollback decisions use val score only.
|
|
250
|
+
val_pipeline=val_pipeline,
|
|
251
|
+
rollback_on_degradation=True,
|
|
252
|
+
|
|
253
|
+
# Optional external scorer run at checkpoint evaluation (e.g. held-out backtest)
|
|
254
|
+
checkpoint_score_fn=lambda a: evaluate(a, held_out_env),
|
|
255
|
+
|
|
256
|
+
# Convergence parameters
|
|
257
|
+
dormant_threshold=10, # consecutive episodes without improvement -> DORMANT
|
|
258
|
+
min_episodes_before_dormant=50, # statistical warmup before convergence detection activates
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
opt.run()
|
|
262
|
+
opt.best_snapshot # -> PolicySnapshot: best weights + EvalMetrics + HyperparamSet
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Loop state transitions: `ACTIVE` -> `COOLING` -> `DORMANT` -> watchdog shutdown or policy spawn.
|
|
266
|
+
|
|
267
|
+
On shutdown the loop restores best-known weights, not the final checkpoint.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Hyperparameter optimisation
|
|
272
|
+
|
|
273
|
+
All optimisers operate in normalised [0, 1] parameter space and update every episode. No restarts required.
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from tensor_optix.optimizers import SPSAOptimizer, AdaptiveOptimizer
|
|
277
|
+
|
|
278
|
+
# SPSA: Rademacher perturbation vector, two-episode gradient estimate
|
|
279
|
+
optimizer = SPSAOptimizer(
|
|
280
|
+
param_bounds={"learning_rate": (1e-4, 3e-3), "clip_ratio": (0.1, 0.3)},
|
|
281
|
+
log_params={"learning_rate"}, # log-space normalisation for params spanning orders of magnitude
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# AdaptiveOptimizer: routes between SPSA, Momentum, Backoff, and PBT
|
|
285
|
+
# based on lag-1 autocorrelation of the score stream and relative performance gap
|
|
286
|
+
optimizer = AdaptiveOptimizer(param_bounds={...})
|
|
287
|
+
|
|
288
|
+
opt = RLOptimizer(agent=agent, pipeline=pipeline, optimizer=optimizer)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
| Optimizer | Routing condition |
|
|
292
|
+
|---|---|
|
|
293
|
+
| `SPSAOptimizer` | i.i.d. score noise, no autocorrelation structure |
|
|
294
|
+
| `MomentumOptimizer` | Positive lag-1 autocorrelation (smooth landscape) |
|
|
295
|
+
| `BackoffOptimizer` | Negative lag-1 autocorrelation (oscillating landscape, sign-only updates) |
|
|
296
|
+
| `PBTOptimizer` | Score below 20th percentile of history (exploit checkpoint population) |
|
|
297
|
+
| `AdaptiveOptimizer` | Routes automatically based on the two signals above |
|
|
298
|
+
|
|
299
|
+
### Trial-level search
|
|
300
|
+
|
|
301
|
+
`TrialOrchestrator` runs N independent short trials via Optuna TPE before the main run, then warm-starts from the best trial's weights and config.
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
from tensor_optix import TrialOrchestrator
|
|
305
|
+
|
|
306
|
+
orch = TrialOrchestrator(
|
|
307
|
+
agent_factory=make_agent,
|
|
308
|
+
pipeline_factory=make_pipeline,
|
|
309
|
+
param_space={
|
|
310
|
+
"learning_rate": ("log_float", 1e-4, 3e-3),
|
|
311
|
+
"clip_ratio": ("float", 0.1, 0.3),
|
|
312
|
+
"batch_size": ("int", 32, 512),
|
|
313
|
+
},
|
|
314
|
+
n_trials=20,
|
|
315
|
+
trial_episodes=50,
|
|
316
|
+
)
|
|
317
|
+
best_config = orch.run()
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Ensemble and policy evolution
|
|
323
|
+
|
|
324
|
+
`PolicyManager` runs as a loop callback. On each DORMANT event, `MetaController` evaluates the generalization gap (train minus val, normalised) and its slope, and the validation improvement rate, then issues one of: SPAWN, PRUNE, or STOP. Spawned variants are cloned from the best checkpoint with perturbed hyperparameters. `EnsembleAgent` wraps all active variants behind the `BaseAgent` interface, with weighted action averaging.
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
from tensor_optix import PolicyManager
|
|
328
|
+
|
|
329
|
+
pm = PolicyManager(registry, max_spawns=4)
|
|
330
|
+
cb = pm.as_callback(agent, agent_factory=make_agent)
|
|
331
|
+
cb.set_stop_fn(opt.stop)
|
|
332
|
+
opt.add_callback(cb)
|
|
333
|
+
opt.run()
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Callbacks
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
from tensor_optix.callbacks import RichDashboardCallback, WandbCallback, TensorBoardCallback
|
|
342
|
+
|
|
343
|
+
opt.add_callback(RichDashboardCallback()) # Rich live terminal panel
|
|
344
|
+
opt.add_callback(WandbCallback(project="run"))
|
|
345
|
+
opt.add_callback(TensorBoardCallback(log_dir="./tb"))
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Custom callbacks subclass `LoopCallback` and override any of:
|
|
349
|
+
|
|
350
|
+
```python
|
|
351
|
+
class LoopCallback:
|
|
352
|
+
def on_loop_start(self) -> None: ...
|
|
353
|
+
def on_loop_stop(self) -> None: ...
|
|
354
|
+
def on_episode_end(self, episode_id: int, eval_metrics) -> None: ...
|
|
355
|
+
def on_improvement(self, snapshot) -> None: ...
|
|
356
|
+
def on_plateau(self, episode_id: int, state) -> None: ...
|
|
357
|
+
def on_dormant(self, episode_id: int) -> None: ...
|
|
358
|
+
def on_degradation(self, episode_id: int, eval_metrics) -> None: ...
|
|
359
|
+
def on_hyperparam_update(self, old: dict, new: dict) -> None: ...
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
`HebbianHook` and `NeuromodulatorSignal` are both `LoopCallback` subclasses and can be passed directly to `Optimizer` or `RLOptimizer` via `callbacks=`.
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Distributed training (IMPALA + V-trace)
|
|
367
|
+
|
|
368
|
+
`AsyncActorLearner` implements IMPALA-style async actor-learner. N actor subprocesses read weights from shared memory (lock-free), collect trajectories, and push them to a queue. The learner dequeues trajectories, applies V-trace importance-sampling correction, and writes updated weights back to shared memory.
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
from tensor_optix.distributed import AsyncActorLearner
|
|
372
|
+
|
|
373
|
+
learner = AsyncActorLearner(
|
|
374
|
+
actor=actor,
|
|
375
|
+
critic=critic,
|
|
376
|
+
optimizer=optimizer,
|
|
377
|
+
env_factory=lambda: gym.make("ALE/Pong-v5"),
|
|
378
|
+
n_actors=8,
|
|
379
|
+
trajectory_len=64,
|
|
380
|
+
)
|
|
381
|
+
stats = learner.run(max_steps=10_000_000)
|
|
382
|
+
# stats["steps_per_second"] -> ~4x single-process throughput on CPU
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Neuroevo
|
|
388
|
+
|
|
389
|
+
`NeuronGraph` is a mutable directed graph of scalar neurons with variable-delay edges. `GraphAgent` wraps it as a `BaseAgent` with PPO-style weight learning. `TopologyController` mutates the graph live during the training loop.
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
pip install tensor-optix[neuroevo]
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Graph construction
|
|
396
|
+
|
|
397
|
+
```python
|
|
398
|
+
from tensor_optix.neuroevo import NeuronGraph, GraphAgent, GRUNeuron, LSTMNeuron
|
|
399
|
+
|
|
400
|
+
graph = NeuronGraph()
|
|
401
|
+
|
|
402
|
+
for _ in range(4):
|
|
403
|
+
graph.add_neuron(role="input", activation="linear")
|
|
404
|
+
for _ in range(8):
|
|
405
|
+
graph.add_neuron(role="hidden", activation="tanh")
|
|
406
|
+
# or: graph.add_neuron(role="hidden", neuron=GRUNeuron())
|
|
407
|
+
# or: graph.add_neuron(role="hidden", neuron=LSTMNeuron())
|
|
408
|
+
graph.add_neuron(role="output", activation="linear") # last output neuron is the value head
|
|
409
|
+
|
|
410
|
+
graph.add_edge(src_id, dst_id, weight=0.0, delay=0) # feedforward (d=0)
|
|
411
|
+
graph.add_edge(src_id, dst_id, weight=0.0, delay=1) # recurrent (d>=1, reads from history buffer)
|
|
412
|
+
|
|
413
|
+
agent = GraphAgent(graph, obs_dim=4, n_actions=2)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
All edges initialise at weight=0.0, which is function-preserving at insertion time.
|
|
417
|
+
|
|
418
|
+
### Neuron types
|
|
419
|
+
|
|
420
|
+
| Type | Hidden state | Gradient through state |
|
|
421
|
+
|---|---|---|
|
|
422
|
+
| `Neuron` | None (point neuron) | N/A |
|
|
423
|
+
| `GRUNeuron` | Scalar h, detached | No |
|
|
424
|
+
| `LSTMNeuron` | Scalar h and c, detached | No |
|
|
425
|
+
| `TrainableGRUNeuron` | Scalar h, not detached | Yes, up to chunk_len steps |
|
|
426
|
+
| `TrainableLSTMNeuron` | Scalar h and c, not detached | Yes, up to chunk_len steps |
|
|
427
|
+
|
|
428
|
+
All types implement the same protocol: `step()`, `importance()`, `can_merge_with()`, `make_relay()`, `split_copy()`. `NeuronGraph` and `TopologyController` are type-blind.
|
|
429
|
+
|
|
430
|
+
### Trainable recurrent neurons
|
|
431
|
+
|
|
432
|
+
`TrainableGRUNeuron` and `TrainableLSTMNeuron` set `is_recurrent = True`. `RecurrentGraphAgent` detects this flag and switches from shuffled-minibatch PPO to sequential chunk training with truncated BPTT.
|
|
433
|
+
|
|
434
|
+
```python
|
|
435
|
+
from tensor_optix.neuroevo import TrainableGRUNeuron, TrainableLSTMNeuron, RecurrentGraphAgent
|
|
436
|
+
|
|
437
|
+
graph = NeuronGraph()
|
|
438
|
+
# ... input and output neurons ...
|
|
439
|
+
graph.add_neuron(role="hidden", neuron=TrainableGRUNeuron())
|
|
440
|
+
graph.add_neuron(role="hidden", neuron=TrainableLSTMNeuron())
|
|
441
|
+
|
|
442
|
+
agent = RecurrentGraphAgent(
|
|
443
|
+
graph, obs_dim=4, n_actions=2,
|
|
444
|
+
hyperparams=HyperparamSet(params={"chunk_len": 64}),
|
|
445
|
+
)
|
|
446
|
+
# Falls back to standard shuffled-minibatch PPO if no recurrent neurons are present
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Topology controller
|
|
450
|
+
|
|
451
|
+
```python
|
|
452
|
+
from tensor_optix.neuroevo import TopologyController
|
|
453
|
+
|
|
454
|
+
controller = TopologyController.for_graph(
|
|
455
|
+
graph=graph,
|
|
456
|
+
scheduler=opt._scheduler,
|
|
457
|
+
grow_grad_threshold=0.7, # fraction of hidden neurons with |grad| > eps required to grow
|
|
458
|
+
prune_neuron_threshold=1e-4, # importance score below this -> prune candidate
|
|
459
|
+
prune_edge_threshold=1e-3, # |weight| below this for prune_edge_patience episodes -> prune
|
|
460
|
+
merge_similarity_threshold=0.95, # Pearson correlation threshold for merge
|
|
461
|
+
)
|
|
462
|
+
opt.add_callback(controller)
|
|
463
|
+
opt.run()
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Grow fires only when all three signals agree:
|
|
467
|
+
1. Improvement slope t-test is not significant (gradient updates are not making progress)
|
|
468
|
+
2. Score residuals have significant autocorrelation (capacity is underutilised)
|
|
469
|
+
3. Gradient utilization exceeds `grow_grad_threshold` (existing neurons are saturated)
|
|
470
|
+
|
|
471
|
+
For multi-region graphs, use `TopologyController.for_brain(brain, scheduler=...)`. Each region gets independent signal buffers and cooldown timers. When a neuron is pruned from a region, all inter-region `BrainNetwork` edges referencing it are automatically removed.
|
|
472
|
+
|
|
473
|
+
### Save and load
|
|
474
|
+
|
|
475
|
+
Topology-aware checkpointing works identically to non-evo agents. The topology (neuron types, roles, edges) is saved alongside weights so the graph is fully reconstructed on load — no manual topology reconstruction required.
|
|
476
|
+
|
|
477
|
+
```python
|
|
478
|
+
# Save
|
|
479
|
+
agent.save_weights("checkpoint.pt")
|
|
480
|
+
|
|
481
|
+
# Load into any agent instance — topology is reconstructed automatically
|
|
482
|
+
agent2 = make_agent(env, neuroevo=True)
|
|
483
|
+
agent2.load_weights("checkpoint.pt")
|
|
484
|
+
|
|
485
|
+
# Or skip constructing an agent entirely
|
|
486
|
+
agent3 = GraphAgent.from_checkpoint("checkpoint.pt")
|
|
487
|
+
agent3.act(obs)
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Rollback on degradation uses the same path internally — when the loop restores a best checkpoint, the topology at the time of that checkpoint is fully restored, even if the graph has grown or been pruned since.
|
|
491
|
+
|
|
492
|
+
### BrainNetwork
|
|
493
|
+
|
|
494
|
+
```python
|
|
495
|
+
from tensor_optix.neuroevo import BrainNetwork, TopologyController
|
|
496
|
+
|
|
497
|
+
brain = BrainNetwork()
|
|
498
|
+
brain.add_region("sensory", sensory_graph)
|
|
499
|
+
brain.add_region("memory", memory_graph)
|
|
500
|
+
brain.add_region("executive", executive_graph)
|
|
501
|
+
|
|
502
|
+
brain.add_pathway("sensory", "memory", n_connections=8, delay=1)
|
|
503
|
+
brain.add_pathway("memory", "executive", n_connections=8, delay=0)
|
|
504
|
+
|
|
505
|
+
controller = TopologyController.for_brain(brain, scheduler=opt._scheduler)
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Inter-region edges are learnable parameters. Regions are executed in topological order each forward pass.
|
|
509
|
+
|
|
510
|
+
### Hebbian learning
|
|
511
|
+
|
|
512
|
+
`HebbianHook` applies an Oja-style local weight update after each episode. The rule is: `dw = eta * mean_t(h_pre * h_post) - lambda * w`. Call `record()` after each `act()` to accumulate co-activation products, then `apply()` after the PPO gradient step.
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
from tensor_optix.neuroevo import HebbianHook
|
|
516
|
+
|
|
517
|
+
hook = HebbianHook(graph, hebbian_lr=1e-3, weight_decay=1e-4)
|
|
518
|
+
|
|
519
|
+
for step in episode:
|
|
520
|
+
action = agent.act(obs)
|
|
521
|
+
hook.record()
|
|
522
|
+
obs, reward, done, _ = env.step(action)
|
|
523
|
+
|
|
524
|
+
agent.learn(episode_data)
|
|
525
|
+
hook.apply()
|
|
526
|
+
hook.reset()
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
`HebbianHook` is a `LoopCallback` — pass it directly to `Optimizer` or `RLOptimizer` via `callbacks=` and it wires itself automatically. Use `HebbianHook.from_brain(brain, ...)` for `BrainNetwork` graphs.
|
|
530
|
+
|
|
531
|
+
### Neuromodulation
|
|
532
|
+
|
|
533
|
+
`NeuromodulatorSignal` takes a `RegimeDetector` classification and applies coordinated parameter changes across `HebbianHook`, `GraphAgent`, and `TopologyController` simultaneously. It is a `LoopCallback` and can be passed directly to `Optimizer`.
|
|
534
|
+
|
|
535
|
+
```python
|
|
536
|
+
from tensor_optix.neuroevo import NeuromodulatorSignal
|
|
537
|
+
from tensor_optix.core import RegimeDetector
|
|
538
|
+
|
|
539
|
+
signal = NeuromodulatorSignal(
|
|
540
|
+
detector=RegimeDetector(),
|
|
541
|
+
hebbian_hook=hook, # optional
|
|
542
|
+
agent=agent, # optional — modulates entropy_coef
|
|
543
|
+
topology_controller=tc, # optional — modulates grow/prune thresholds
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
# In your training loop, after each episode:
|
|
547
|
+
signal.step(metrics_history)
|
|
548
|
+
# trending -> lower entropy_coef, lower hebbian_lr (consolidate)
|
|
549
|
+
# ranging -> raise hebbian_lr, lower grow threshold (explore structure)
|
|
550
|
+
# volatile -> lower hebbian_lr, raise entropy_coef (cautious plasticity)
|
|
551
|
+
|
|
552
|
+
# Or wire as a callback — called automatically each episode
|
|
553
|
+
opt = Optimizer(agent, env, callbacks=[hook, signal])
|
|
554
|
+
opt.run()
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### Dale's Law
|
|
558
|
+
|
|
559
|
+
```python
|
|
560
|
+
# clamp mode (default): outgoing weights clamped post-step
|
|
561
|
+
graph = NeuronGraph(dale_mode="clamp")
|
|
562
|
+
graph.add_neuron(role="hidden", activation="relu", cell_type="excitatory") # weights >= 0
|
|
563
|
+
graph.add_neuron(role="hidden", activation="tanh", cell_type="inhibitory") # weights <= 0
|
|
564
|
+
|
|
565
|
+
# softplus mode: raw parameter theta, effective weight = softplus(theta) * sign
|
|
566
|
+
# gradient-safe, no dead zone at the clamp boundary
|
|
567
|
+
# enforce_dale() is a no-op in this mode
|
|
568
|
+
graph = NeuronGraph(dale_mode="softplus")
|
|
569
|
+
w = graph.effective_weight(edge_id) # reads post-softplus value
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### TopologyAwareAdam
|
|
573
|
+
|
|
574
|
+
Drop-in Adam replacement that resets (m, v) momentum state for parameters touched by a grow, prune, or merge operation. Stale momentum estimates from before a structural change would otherwise corrupt the first update on modified parameters.
|
|
575
|
+
|
|
576
|
+
```python
|
|
577
|
+
from tensor_optix.neuroevo import TopologyAwareAdam
|
|
578
|
+
|
|
579
|
+
optimizer = TopologyAwareAdam(graph.parameters(), lr=3e-4)
|
|
580
|
+
optimizer.notify_topology_change(new_params) # call after any topology mutation
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Compiled forward
|
|
584
|
+
|
|
585
|
+
`NeuronGraph` runs in **eager mode by default**. Because `_raw_forward` mutates Python-side neuron state (`neuron._current`, `push_history`), `torch.compile` cannot safely trace it without replaying those side effects. The default eager path is safe for training, recurrent neurons, and dynamic topologies.
|
|
586
|
+
|
|
587
|
+
If the topology is static and you manage neuron state externally, you can opt in to a compiled forward:
|
|
588
|
+
|
|
589
|
+
```python
|
|
590
|
+
graph.compile_forward() # one-time call; re-call after any topology mutation
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
The backend is selected automatically: `inductor` on Linux/macOS, `aot_eager` on Windows. Has no effect on PyTorch < 2.0.
|
|
594
|
+
|
|
595
|
+
`TopologyController` calls `graph.invalidate_compile()` after every grow, prune, and merge. If you mutate the graph directly outside the controller, call it yourself:
|
|
596
|
+
|
|
597
|
+
```python
|
|
598
|
+
graph.add_edge(src_id, dst_id, weight=0.0, delay=1)
|
|
599
|
+
graph.invalidate_compile()
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
`invalidate_compile()` rebuilds the matrix cache and resets to eager mode. If `compile_forward()` was previously called, it also evicts stale Dynamo kernels (`torch._dynamo.reset()`) and recompiles — this reset is process-global, so all `NeuronGraph` instances in the process retrace on their next forward call.
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
## ML support
|
|
607
|
+
|
|
608
|
+
tensor-optix also supports general ML training via `Optimizer`. Pass any `nn.Module` and a PyTorch `Dataset` or `DataLoader` — the same SPSA hyperparameter tuning, rollback on degradation, convergence detection, and checkpointing apply automatically.
|
|
609
|
+
|
|
610
|
+
```python
|
|
611
|
+
import tensor_optix as optix
|
|
612
|
+
import torch.nn as nn
|
|
613
|
+
|
|
614
|
+
# Supervised — loss auto-detected from dataset
|
|
615
|
+
model = nn.Sequential(nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10))
|
|
616
|
+
opt = optix.Optimizer(model, train_dataset)
|
|
617
|
+
opt.run()
|
|
618
|
+
|
|
619
|
+
# Explicit loss
|
|
620
|
+
opt = optix.Optimizer(model, train_dataset, loss="cross_entropy")
|
|
621
|
+
opt = optix.Optimizer(model, train_dataset, loss="mse")
|
|
622
|
+
|
|
623
|
+
# Autoencoder
|
|
624
|
+
opt = optix.Optimizer(autoencoder, train_dataset, loss="reconstruction")
|
|
625
|
+
|
|
626
|
+
# VAE — model must return (reconstruction, mu, logvar)
|
|
627
|
+
opt = optix.Optimizer(vae, train_dataset, loss="vae")
|
|
628
|
+
|
|
629
|
+
# Contrastive (SimCLR) — dataset yields (view1, view2) pairs
|
|
630
|
+
opt = optix.Optimizer(encoder, pairs_dataset, loss="contrastive")
|
|
631
|
+
|
|
632
|
+
# DataLoader works too
|
|
633
|
+
opt = optix.Optimizer(model, DataLoader(dataset, batch_size=64))
|
|
634
|
+
|
|
635
|
+
# Custom loss
|
|
636
|
+
opt = optix.Optimizer(model, dataset, loss=nn.HuberLoss())
|
|
637
|
+
opt.run()
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
Available `loss=` strings:
|
|
641
|
+
|
|
642
|
+
| String | Criterion | Use case |
|
|
643
|
+
|---|---|---|
|
|
644
|
+
| `"auto"` | detected from data | default |
|
|
645
|
+
| `"cross_entropy"` | `CrossEntropyLoss` | multi-class classification |
|
|
646
|
+
| `"bce"` | `BCEWithLogitsLoss` | binary classification |
|
|
647
|
+
| `"mse"` | `MSELoss` | regression |
|
|
648
|
+
| `"mae"` | `L1Loss` | regression, outlier-robust |
|
|
649
|
+
| `"huber"` | `HuberLoss` | regression, very outlier-robust |
|
|
650
|
+
| `"reconstruction"` | MSE(output, input) | autoencoders |
|
|
651
|
+
| `"vae"` | ELBO: recon + KL | variational autoencoders |
|
|
652
|
+
| `"contrastive"` | NT-Xent | SimCLR-style contrastive learning |
|
|
653
|
+
|
|
654
|
+
Any `nn.Module` or callable can be passed directly as `loss=`.
|
|
655
|
+
|
|
656
|
+
Save and load work identically to RL agents:
|
|
657
|
+
|
|
658
|
+
```python
|
|
659
|
+
agent = optix.Optimizer(model, dataset, loss="cross_entropy")
|
|
660
|
+
# Checkpoints written automatically to checkpoint_dir
|
|
661
|
+
# Load back:
|
|
662
|
+
from tensor_optix.ml import MLAgent
|
|
663
|
+
ml_agent.load_weights("checkpoint.pt")
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
## Core utilities
|
|
669
|
+
|
|
670
|
+
### Normalizers
|
|
671
|
+
|
|
672
|
+
Online Welford mean/variance. `ObsNormalizer` normalises observations. `RewardNormalizer` divides rewards by return standard deviation (not reward std), preserving sign.
|
|
673
|
+
|
|
674
|
+
```python
|
|
675
|
+
from tensor_optix.core.normalizers import ObsNormalizer, RewardNormalizer
|
|
676
|
+
|
|
677
|
+
obs_norm = ObsNormalizer(shape=(obs_dim,))
|
|
678
|
+
obs_norm.update(obs_batch)
|
|
679
|
+
normalized = obs_norm.normalize(obs)
|
|
680
|
+
|
|
681
|
+
rew_norm = RewardNormalizer()
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### Hindsight Experience Replay
|
|
685
|
+
|
|
686
|
+
Wraps `PrioritizedReplayBuffer` with episode-level goal relabeling. Supports `future` (default), `final`, and `episode` relabeling strategies.
|
|
687
|
+
|
|
688
|
+
```python
|
|
689
|
+
from tensor_optix.core.her_buffer import HERBuffer
|
|
690
|
+
|
|
691
|
+
her = HERBuffer(obs_dim=obs_dim, act_dim=act_dim, goal_dim=goal_dim, strategy="future", k=4)
|
|
692
|
+
her.store_episode(obs_list, act_list, rew_list, next_obs_list, done_list,
|
|
693
|
+
achieved_goals, compute_reward_fn)
|
|
694
|
+
obs_b, act_b, rew_b, next_b, done_b, weights, idx, n = her.sample(batch_size)
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
### Checkpoint registry
|
|
698
|
+
|
|
699
|
+
```python
|
|
700
|
+
from tensor_optix.core.checkpoint_registry import CheckpointRegistry
|
|
701
|
+
|
|
702
|
+
registry = CheckpointRegistry(checkpoint_dir="./checkpoints", max_snapshots=10)
|
|
703
|
+
registry.save(agent, eval_metrics, hyperparams)
|
|
704
|
+
registry.load_best(agent)
|
|
705
|
+
registry.load_ensemble(agent, top_k=3) # stochastic weight averaging over top-k snapshots
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
### Regime detection
|
|
709
|
+
|
|
710
|
+
Classifies score history into one of three regimes using detrended coefficient of variation. Detrended CV measures noise around the trend, not raw score variance.
|
|
711
|
+
|
|
712
|
+
```python
|
|
713
|
+
from tensor_optix.core import RegimeDetector
|
|
714
|
+
|
|
715
|
+
detector = RegimeDetector()
|
|
716
|
+
regime = detector.detect(metrics_history) # "trending" | "ranging" | "volatile"
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
## Learning-to-Learn Engine
|
|
722
|
+
|
|
723
|
+
tensor-optix also includes an optional Learning-to-Learn Engine: a layer that makes experiment history actionable — recording every trial with enough structure to compare reliably, running experiments concurrently under explicit budgets, learning which training strategies help on which tasks, and (recommend-only, never automatic) proposing interventions for a human to review before anything is promoted to a deployed configuration.
|
|
724
|
+
|
|
725
|
+
### Experiment ledger
|
|
726
|
+
|
|
727
|
+
Every experiment — a trial, a strategy candidate, a re-evaluation of a deployed agent — is recorded to a SQLite-backed ledger with full provenance (code revision, library versions, hostname) and a typed lifecycle.
|
|
728
|
+
|
|
729
|
+
```python
|
|
730
|
+
from tensor_optix.ledger import ExperimentStore, ExperimentState
|
|
731
|
+
|
|
732
|
+
store = ExperimentStore("./experiments.sqlite3")
|
|
733
|
+
record = store.create_experiment(
|
|
734
|
+
task_env="CartPole-v1", algorithm="PPO", framework="torch",
|
|
735
|
+
seed=0, checkpoint_dir="./ckpt", solve_threshold=475.0,
|
|
736
|
+
)
|
|
737
|
+
store.start_experiment(record.experiment_id)
|
|
738
|
+
store.record_progress(record.experiment_id, best_primary_score=480.0)
|
|
739
|
+
store.finalize_experiment(record.experiment_id, state=ExperimentState.COMPLETED)
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
Writes are idempotent on `experiment_id`, and every method opens a short, single-statement transaction — nothing here ever holds a lock for the duration of training.
|
|
743
|
+
|
|
744
|
+
### Concurrent trial scheduler
|
|
745
|
+
|
|
746
|
+
`ConcurrentTrialScheduler` runs multiple Optuna trials as real OS processes, with the parent process as sole owner of the Optuna study and the ledger.
|
|
747
|
+
|
|
748
|
+
```python
|
|
749
|
+
from tensor_optix.scheduler import ConcurrentTrialScheduler
|
|
750
|
+
|
|
751
|
+
scheduler = ConcurrentTrialScheduler(
|
|
752
|
+
agent_factory=make_agent, pipeline_factory=make_pipeline,
|
|
753
|
+
param_space={"learning_rate": ("log_float", 1e-4, 3e-3)},
|
|
754
|
+
n_trials=20, n_workers=4, ledger_store=store,
|
|
755
|
+
task_env="CartPole-v1", algorithm="PPO", solve_threshold=475.0,
|
|
756
|
+
)
|
|
757
|
+
best_params, best_score = scheduler.run()
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
### Cross-task transfer (Nanonets)
|
|
761
|
+
|
|
762
|
+
`NanonetsSelector` pools evidence from other tasks' ledger history when deciding how to allocate a new task's trial budget — but only when a conservative abstention gate confirms a source task's data is actually comparable (median performance at or above the target's own 95th percentile, both normalized against each task's own solve threshold so differently-scaled/signed scores are never compared on raw terms). When nothing passes the gate, it falls back to the non-learned baseline rather than guess.
|
|
763
|
+
|
|
764
|
+
```python
|
|
765
|
+
from tensor_optix.nanonets import NanonetsSelector
|
|
766
|
+
|
|
767
|
+
selector = NanonetsSelector(store, source_task_envs=["Acrobot-v1"])
|
|
768
|
+
decision = selector.propose(task_env="CartPole-v1", methods=["TPESampler"], budget=10)
|
|
769
|
+
print(decision.rationale) # states explicitly whether it pooled or fell back
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
### Strategy discovery
|
|
773
|
+
|
|
774
|
+
A small, closed set of typed training-procedure operations (learning-rate trend adjustment, checkpoint rollback, exploration bounds, optimizer-method choice, budget allocation, schedule changes, policy-variant selection) — never arbitrary generated code. Each candidate is run paired against a no-strategy baseline, compared via a conservative confidence-interval gate, and checked against a 6-criterion promotion policy.
|
|
775
|
+
|
|
776
|
+
```python
|
|
777
|
+
from tensor_optix.strategy import LearningRateTrendOp, StrategyCandidate, StrategyTrialRunner, PromotionPolicy
|
|
778
|
+
|
|
779
|
+
candidate = StrategyCandidate.from_op(LearningRateTrendOp(window=5, increase_factor=1.2))
|
|
780
|
+
runner = StrategyTrialRunner(
|
|
781
|
+
agent_factory=make_agent, pipeline_factory=make_pipeline,
|
|
782
|
+
trial_steps=50_000, ledger_store=store, task_env="CartPole-v1",
|
|
783
|
+
algorithm="PPO", solve_threshold=475.0,
|
|
784
|
+
)
|
|
785
|
+
results = runner.run_candidate(candidate, seeds=[0, 1, 2])
|
|
786
|
+
|
|
787
|
+
policy = PromotionPolicy(store, margin=5.0, min_seeds=3, approval_mode="human")
|
|
788
|
+
verdict = policy.evaluate(candidate_id=candidate.candidate_id, train_task_envs=["CartPole-v1"], held_out_task_envs=[])
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
`approval_mode="human"` (the default) means `verdict.criteria["human_reviewed"]` can only ever become `True` via a separate, explicit `store.approve_strategy(...)` call — `PromotionPolicy.evaluate()` never sets it itself.
|
|
792
|
+
|
|
793
|
+
### Live adaptive runtime
|
|
794
|
+
|
|
795
|
+
`LiveAdaptiveRuntime` runs the full observe → detect → propose → experiment → evaluate → recommend loop against a long-running deployed agent, standing in for live inference feedback. It is **recommend-only**: it issues durable, auditable recommendation records to the ledger, but the loop itself never calls `approve_strategy` and never changes which agent is deployed. Promoting a candidate requires two separate, explicit human actions outside the loop (`store.approve_strategy(...)`, then `promote_candidate_to_baseline(...)`).
|
|
796
|
+
|
|
797
|
+
```python
|
|
798
|
+
from tensor_optix.runtime import LiveAdaptiveRuntime, RuntimeConfig
|
|
799
|
+
|
|
800
|
+
config = RuntimeConfig(task_env="CartPole-v1", algorithm="PPO", max_cycles=20)
|
|
801
|
+
runtime = LiveAdaptiveRuntime(store, config, agent_factory=make_agent, pipeline_factory=make_pipeline)
|
|
802
|
+
observations, recommendations = runtime.run()
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
Full guide: [Learning-to-Learn Engine](https://sup3rus3r.github.io/tensor-optix/guides/learning-to-learn/).
|
|
806
|
+
|
|
807
|
+
---
|
|
808
|
+
|
|
809
|
+
## Requirements
|
|
810
|
+
|
|
811
|
+
- Python >= 3.11
|
|
812
|
+
- gymnasium >= 1.0
|
|
813
|
+
- numpy >= 1.24
|
|
814
|
+
|
|
815
|
+
The core loop, `PolicyManager`, and all ensemble and evolution logic have no framework dependency. Framework installs are opt-in via extras.
|