tensor-optix 2.0.2__tar.gz → 2.0.3__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.2 → tensor_optix-2.0.3}/PKG-INFO +52 -5
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/README.md +49 -4
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/pyproject.toml +5 -1
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/tf_dqn.py +10 -6
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/tf_ppo.py +9 -6
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/tf_ppo_continuous.py +6 -5
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/tf_sac.py +43 -12
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/tf_td3.py +40 -9
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/torch_dqn.py +14 -6
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/torch_ppo.py +12 -5
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/torch_ppo_continuous.py +8 -5
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/torch_rainbow_dqn.py +11 -7
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/torch_sac.py +41 -8
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/torch_td3.py +29 -8
- tensor_optix-2.0.3/tensor_optix/core/obs_utils.py +120 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/replay_buffer.py +31 -6
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/trajectory_buffer.py +31 -5
- tensor_optix-2.0.3/tensor_optix/encoders/__init__.py +15 -0
- tensor_optix-2.0.3/tensor_optix/encoders/obs_encoder.py +229 -0
- tensor_optix-2.0.3/tensor_optix/encoders/obs_encoder_tf.py +80 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/factory.py +263 -75
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ml/ml_agent.py +12 -0
- tensor_optix-2.0.3/tensor_optix/ml_core/__init__.py +53 -0
- tensor_optix-2.0.3/tensor_optix/ml_core/adapters/__init__.py +0 -0
- tensor_optix-2.0.3/tensor_optix/ml_core/adapters/flax_model.py +230 -0
- tensor_optix-2.0.3/tensor_optix/ml_core/adapters/tf_model.py +208 -0
- tensor_optix-2.0.3/tensor_optix/ml_core/adapters/torch_model.py +241 -0
- tensor_optix-2.0.3/tensor_optix/ml_core/base_model.py +150 -0
- tensor_optix-2.0.3/tensor_optix/ml_core/lr_schedules.py +67 -0
- tensor_optix-2.0.3/tensor_optix/ml_core/metrics.py +188 -0
- tensor_optix-2.0.3/tensor_optix/ml_core/metrics_impl/__init__.py +0 -0
- tensor_optix-2.0.3/tensor_optix/ml_core/metrics_impl/classification.py +101 -0
- tensor_optix-2.0.3/tensor_optix/ml_core/metrics_impl/regression.py +46 -0
- tensor_optix-2.0.3/tensor_optix/ml_core/trainer.py +207 -0
- tensor_optix-2.0.3/tensor_optix/ml_core/transfer.py +152 -0
- tensor_optix-2.0.3/tensor_optix/multi_agent/__init__.py +34 -0
- tensor_optix-2.0.3/tensor_optix/multi_agent/aec_pipeline.py +173 -0
- tensor_optix-2.0.3/tensor_optix/multi_agent/multi_agent_episode.py +18 -0
- tensor_optix-2.0.3/tensor_optix/multi_agent/multi_agent_runtime.py +53 -0
- tensor_optix-2.0.3/tensor_optix/multi_agent/parallel_pipeline.py +145 -0
- tensor_optix-2.0.3/tensor_optix/multi_agent/seat_env.py +81 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/simple.py +6 -2
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix.egg-info/PKG-INFO +52 -5
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix.egg-info/SOURCES.txt +23 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix.egg-info/requires.txt +3 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/setup.cfg +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/jax/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/jax/flax_agent.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/jax/flax_evaluator.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/pytorch/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/pytorch/torch_agent.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/pytorch/torch_evaluator.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/tensorflow/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/tensorflow/tf_agent.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/tensorflow/tf_evaluator.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/flax_ppo.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/torch_recurrent_ppo.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/callbacks/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/callbacks/rich_dashboard.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/callbacks/tensorboard_callback.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/callbacks/wandb_callback.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/cli.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/config.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/async_callback.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/backoff_scheduler.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/base_agent.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/base_evaluator.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/base_optimizer.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/base_pipeline.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/checkpoint_confirmation.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/checkpoint_registry.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/device.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/diagnostic_controller.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/ensemble_agent.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/her_buffer.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/loop_controller.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/meta_controller.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/noisy_linear.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/normalizers.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/policy_manager.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/regime_detector.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/resume_state.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/reward_shape.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/types.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/distributed/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/distributed/async_learner.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/distributed/vtrace.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/exploration/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/exploration/rnd.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ledger/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ledger/callback.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ledger/provenance.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ledger/records.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ledger/schema.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ledger/store.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ml/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ml/dataset_pipeline.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ml/loss_registry.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/nanonets/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/nanonets/abstention.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/nanonets/features.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/nanonets/pooling.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/nanonets/selector.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/nanonets/smalln.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/agent/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/agent/graph_agent.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/brain_network.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/controller/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/controller/topology_callback.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/controller/topology_controller.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/graph/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/graph/neuron.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/graph/neuron_graph.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/graph/topology_ops.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/hebbian.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/neuromodulator.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/optimizer/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/optimizer/topology_aware_adam.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/optimizer.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/optimizers/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/optimizers/adaptive_optimizer.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/optimizers/backoff_optimizer.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/optimizers/momentum_optimizer.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/optimizers/pbt_optimizer.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/optimizers/spsa_optimizer.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/orchestrator.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/pipeline/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/pipeline/batch_pipeline.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/pipeline/live_pipeline.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/pipeline/vector_pipeline.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/runtime/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/runtime/baseline.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/runtime/experiment_stage.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/runtime/live_runtime.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/runtime/observer.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/runtime/promote.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/scheduler/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/scheduler/_mp_common.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/scheduler/budget.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/scheduler/dedup.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/scheduler/isolation.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/scheduler/pool.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/scheduler/retry.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/scheduler/worker.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/selection/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/selection/backfill.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/selection/selector.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/selection/uncertainty.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/strategy/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/strategy/candidate.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/strategy/comparator.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/strategy/grammar.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/strategy/promotion.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/strategy/runner.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/strategy/worker.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/utils/__init__.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/utils/window_size.py +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix.egg-info/dependency_links.txt +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix.egg-info/entry_points.txt +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix.egg-info/top_level.txt +0 -0
- {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tests/test_ml.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tensor-optix
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.3
|
|
4
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
5
|
Author: sup3rus3r
|
|
6
6
|
License-Expression: MIT
|
|
@@ -35,6 +35,8 @@ Requires-Dist: gymnasium[atari]>=1.0.0; extra == "atari"
|
|
|
35
35
|
Requires-Dist: ale-py>=0.8; extra == "atari"
|
|
36
36
|
Provides-Extra: mujoco
|
|
37
37
|
Requires-Dist: gymnasium[mujoco]>=1.0.0; extra == "mujoco"
|
|
38
|
+
Provides-Extra: pettingzoo
|
|
39
|
+
Requires-Dist: pettingzoo>=1.24.0; extra == "pettingzoo"
|
|
38
40
|
Provides-Extra: all
|
|
39
41
|
Requires-Dist: tensorflow>=2.18.0; extra == "all"
|
|
40
42
|
Requires-Dist: torch>=2.11.0; extra == "all"
|
|
@@ -69,13 +71,22 @@ Requires-Dist: torch>=2.11.0; extra == "dev"
|
|
|
69
71
|
|
|
70
72
|
# tensor-optix
|
|
71
73
|
|
|
72
|
-
tensor-optix is a training
|
|
74
|
+
tensor-optix is a training framework for reinforcement learning and for supervised/unsupervised machine learning. Both have statistical convergence control, online hyperparameter optimisation, and real checkpointing.
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
It has two parallel cores:
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
- **RL core**. An episode-driven loop (`LoopController`/`RLOptimizer`) built around `BaseAgent`'s `act()`/`learn()` contract. Statistical convergence control, SPSA-based online hyperparameter tuning, and an optional neuroevolution subsystem for dynamic topology.
|
|
79
|
+
- **ML core** (`tensor_optix.ml_core`). An epoch/batch-driven loop (`Trainer`, wrapped by `BaseMLModel.fit()`) built around a sklearn-style `fit()`/`predict()`/`evaluate()` contract. Real classification metrics (accuracy, precision/recall/F1, confusion matrix) and regression metrics (MAE, RMSE, R²), validation-based early stopping, and checkpointing on the best metric.
|
|
77
80
|
|
|
78
|
-
|
|
81
|
+
Both cores reuse the same lower-level, algorithm-agnostic machinery: `CheckpointRegistry`, `BackoffScheduler`'s convergence/plateau detection, `SPSAOptimizer`. Each core has its own contract on top because an RL episode and a training epoch aren't the same shape. See [Concepts](https://sup3rus3r.github.io/tensor-optix/getting-started/concepts/).
|
|
82
|
+
|
|
83
|
+
## RL core
|
|
84
|
+
|
|
85
|
+
The RL 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 via a linear trend fit over the recent score window compared against an adaptive, noise-scaled floor, not a fixed patience counter (see [Concepts](https://sup3rus3r.github.io/tensor-optix/getting-started/concepts/) for the exact math; `BackoffScheduler` is the implementation). 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 (`AdaptiveOptimizer`). 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.
|
|
86
|
+
|
|
87
|
+
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.
|
|
88
|
+
|
|
89
|
+
The RL core (including neuroevo) is accessed through a six-method `BaseAgent` interface:
|
|
79
90
|
|
|
80
91
|
```python
|
|
81
92
|
class BaseAgent(ABC):
|
|
@@ -89,6 +100,40 @@ class BaseAgent(ABC):
|
|
|
89
100
|
|
|
90
101
|
Fifteen agents are included across PyTorch, TensorFlow, and JAX/Flax. Bring your own by implementing the interface above.
|
|
91
102
|
|
|
103
|
+
## ML core
|
|
104
|
+
|
|
105
|
+
The ML loop trains your model for a number of epochs, evaluates on a held-out validation set each epoch, and stops early once the validation metric plateaus. It restores the best checkpoint automatically. It is accessed through a `BaseMLModel` interface, with `fit()` as the one-call entry point built on top:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
class BaseMLModel(ABC):
|
|
109
|
+
def predict(self, X) -> Any: ...
|
|
110
|
+
def train_step(self, batch) -> float: ...
|
|
111
|
+
def evaluate(self, data) -> MLMetrics: ...
|
|
112
|
+
def iter_batches(self, data, batch_size: int): ...
|
|
113
|
+
def save_weights(self, path: str) -> None: ...
|
|
114
|
+
def load_weights(self, path: str) -> None: ...
|
|
115
|
+
# fit(train_data, val_data, epochs, early_stopping_patience, ...) is provided
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import torch.nn as nn
|
|
120
|
+
from tensor_optix.ml_core import TorchMLModel
|
|
121
|
+
|
|
122
|
+
model = TorchMLModel(
|
|
123
|
+
nn.Sequential(nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10)),
|
|
124
|
+
loss_fn=nn.CrossEntropyLoss(),
|
|
125
|
+
)
|
|
126
|
+
history = model.fit(
|
|
127
|
+
(X_train, y_train), (X_val, y_val),
|
|
128
|
+
epochs=50, early_stopping_patience=5, checkpoint_dir="./checkpoints",
|
|
129
|
+
)
|
|
130
|
+
print(history.val_metrics[-1].metrics) # {'accuracy': 0.94, 'f1_macro': 0.93, ...}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Metrics are auto-detected from the target dtype. Integer/class-index targets get classification metrics (accuracy, per-class and macro precision/recall/F1, confusion matrix). Float targets get regression metrics (MAE, RMSE, R²). Pass `metrics="classification"`, `metrics="regression"`, or a list of custom `(y_true, y_pred) -> float` callables to override. `TorchMLModel`, `TFMLModel`, and `FlaxMLModel` are provided. The Torch and TF adapters accept a raw `(X, y)` array pair or a native `Dataset`/`DataLoader`/`tf.data.Dataset`; the Flax adapter accepts a raw `(X, y)` array pair only. `fit()` also takes `lr_schedule=` (see `tensor_optix.ml_core.lr_schedules`), `augment=` for per-batch data augmentation, and there are `freeze_layers()`/`unfreeze_layers()`/`load_pretrained_backbone()` helpers for transfer learning. See [Train an ML model](https://sup3rus3r.github.io/tensor-optix/guides/ml-core/) for the full guide.
|
|
134
|
+
|
|
135
|
+
There is also an older, RL-loop-based way to train an arbitrary `nn.Module`: `Optimizer(model, dataset)`, which runs the model through the RL loop as a fake episode. See [ML support](#ml-support) below. It predates `ml_core`, is kept for backward compatibility, and has no real classification/regression metrics and no validation-based early stopping. Use `ml_core` for new code.
|
|
136
|
+
|
|
92
137
|
---
|
|
93
138
|
|
|
94
139
|
## Install
|
|
@@ -605,6 +650,8 @@ graph.invalidate_compile()
|
|
|
605
650
|
|
|
606
651
|
## ML support
|
|
607
652
|
|
|
653
|
+
> **Use [`tensor_optix.ml_core`](#ml-core) for new code.** The `Optimizer(model, dataset)` path below is the original way to train an arbitrary `nn.Module`. It runs the model through the RL loop's `BaseAgent`/`RLOptimizer` machinery by treating each mini-batch as a fake RL episode (`reward = -loss`). It is deprecated but kept working for backward compatibility — `MLAgent` emits a `DeprecationWarning` pointing to `ml_core` on construction, and there is no removal planned. It has no real classification/regression metrics (checkpointing and rollback compare mean "episode return," which is just negative loss) and no validation-split-based early stopping. `ml_core`'s `BaseMLModel`/`Trainer` reuse the same convergence/checkpointing machinery through a real `fit()`/`evaluate()` contract with real metrics.
|
|
654
|
+
|
|
608
655
|
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
656
|
|
|
610
657
|
```python
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
# tensor-optix
|
|
2
2
|
|
|
3
|
-
tensor-optix is a training
|
|
3
|
+
tensor-optix is a training framework for reinforcement learning and for supervised/unsupervised machine learning. Both have statistical convergence control, online hyperparameter optimisation, and real checkpointing.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It has two parallel cores:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **RL core**. An episode-driven loop (`LoopController`/`RLOptimizer`) built around `BaseAgent`'s `act()`/`learn()` contract. Statistical convergence control, SPSA-based online hyperparameter tuning, and an optional neuroevolution subsystem for dynamic topology.
|
|
8
|
+
- **ML core** (`tensor_optix.ml_core`). An epoch/batch-driven loop (`Trainer`, wrapped by `BaseMLModel.fit()`) built around a sklearn-style `fit()`/`predict()`/`evaluate()` contract. Real classification metrics (accuracy, precision/recall/F1, confusion matrix) and regression metrics (MAE, RMSE, R²), validation-based early stopping, and checkpointing on the best metric.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
Both cores reuse the same lower-level, algorithm-agnostic machinery: `CheckpointRegistry`, `BackoffScheduler`'s convergence/plateau detection, `SPSAOptimizer`. Each core has its own contract on top because an RL episode and a training epoch aren't the same shape. See [Concepts](https://sup3rus3r.github.io/tensor-optix/getting-started/concepts/).
|
|
11
|
+
|
|
12
|
+
## RL core
|
|
13
|
+
|
|
14
|
+
The RL 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 via a linear trend fit over the recent score window compared against an adaptive, noise-scaled floor, not a fixed patience counter (see [Concepts](https://sup3rus3r.github.io/tensor-optix/getting-started/concepts/) for the exact math; `BackoffScheduler` is the implementation). 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 (`AdaptiveOptimizer`). 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.
|
|
15
|
+
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
The RL core (including neuroevo) is accessed through a six-method `BaseAgent` interface:
|
|
10
19
|
|
|
11
20
|
```python
|
|
12
21
|
class BaseAgent(ABC):
|
|
@@ -20,6 +29,40 @@ class BaseAgent(ABC):
|
|
|
20
29
|
|
|
21
30
|
Fifteen agents are included across PyTorch, TensorFlow, and JAX/Flax. Bring your own by implementing the interface above.
|
|
22
31
|
|
|
32
|
+
## ML core
|
|
33
|
+
|
|
34
|
+
The ML loop trains your model for a number of epochs, evaluates on a held-out validation set each epoch, and stops early once the validation metric plateaus. It restores the best checkpoint automatically. It is accessed through a `BaseMLModel` interface, with `fit()` as the one-call entry point built on top:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
class BaseMLModel(ABC):
|
|
38
|
+
def predict(self, X) -> Any: ...
|
|
39
|
+
def train_step(self, batch) -> float: ...
|
|
40
|
+
def evaluate(self, data) -> MLMetrics: ...
|
|
41
|
+
def iter_batches(self, data, batch_size: int): ...
|
|
42
|
+
def save_weights(self, path: str) -> None: ...
|
|
43
|
+
def load_weights(self, path: str) -> None: ...
|
|
44
|
+
# fit(train_data, val_data, epochs, early_stopping_patience, ...) is provided
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import torch.nn as nn
|
|
49
|
+
from tensor_optix.ml_core import TorchMLModel
|
|
50
|
+
|
|
51
|
+
model = TorchMLModel(
|
|
52
|
+
nn.Sequential(nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10)),
|
|
53
|
+
loss_fn=nn.CrossEntropyLoss(),
|
|
54
|
+
)
|
|
55
|
+
history = model.fit(
|
|
56
|
+
(X_train, y_train), (X_val, y_val),
|
|
57
|
+
epochs=50, early_stopping_patience=5, checkpoint_dir="./checkpoints",
|
|
58
|
+
)
|
|
59
|
+
print(history.val_metrics[-1].metrics) # {'accuracy': 0.94, 'f1_macro': 0.93, ...}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Metrics are auto-detected from the target dtype. Integer/class-index targets get classification metrics (accuracy, per-class and macro precision/recall/F1, confusion matrix). Float targets get regression metrics (MAE, RMSE, R²). Pass `metrics="classification"`, `metrics="regression"`, or a list of custom `(y_true, y_pred) -> float` callables to override. `TorchMLModel`, `TFMLModel`, and `FlaxMLModel` are provided. The Torch and TF adapters accept a raw `(X, y)` array pair or a native `Dataset`/`DataLoader`/`tf.data.Dataset`; the Flax adapter accepts a raw `(X, y)` array pair only. `fit()` also takes `lr_schedule=` (see `tensor_optix.ml_core.lr_schedules`), `augment=` for per-batch data augmentation, and there are `freeze_layers()`/`unfreeze_layers()`/`load_pretrained_backbone()` helpers for transfer learning. See [Train an ML model](https://sup3rus3r.github.io/tensor-optix/guides/ml-core/) for the full guide.
|
|
63
|
+
|
|
64
|
+
There is also an older, RL-loop-based way to train an arbitrary `nn.Module`: `Optimizer(model, dataset)`, which runs the model through the RL loop as a fake episode. See [ML support](#ml-support) below. It predates `ml_core`, is kept for backward compatibility, and has no real classification/regression metrics and no validation-based early stopping. Use `ml_core` for new code.
|
|
65
|
+
|
|
23
66
|
---
|
|
24
67
|
|
|
25
68
|
## Install
|
|
@@ -536,6 +579,8 @@ graph.invalidate_compile()
|
|
|
536
579
|
|
|
537
580
|
## ML support
|
|
538
581
|
|
|
582
|
+
> **Use [`tensor_optix.ml_core`](#ml-core) for new code.** The `Optimizer(model, dataset)` path below is the original way to train an arbitrary `nn.Module`. It runs the model through the RL loop's `BaseAgent`/`RLOptimizer` machinery by treating each mini-batch as a fake RL episode (`reward = -loss`). It is deprecated but kept working for backward compatibility — `MLAgent` emits a `DeprecationWarning` pointing to `ml_core` on construction, and there is no removal planned. It has no real classification/regression metrics (checkpointing and rollback compare mean "episode return," which is just negative loss) and no validation-split-based early stopping. `ml_core`'s `BaseMLModel`/`Trainer` reuse the same convergence/checkpointing machinery through a real `fit()`/`evaluate()` contract with real metrics.
|
|
583
|
+
|
|
539
584
|
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.
|
|
540
585
|
|
|
541
586
|
```python
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tensor-optix"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.3"
|
|
8
8
|
description = "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)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -60,6 +60,10 @@ atari = [
|
|
|
60
60
|
mujoco = [
|
|
61
61
|
"gymnasium[mujoco]>=1.0.0",
|
|
62
62
|
]
|
|
63
|
+
# Multi-agent environments (tensor_optix.multi_agent)
|
|
64
|
+
pettingzoo = [
|
|
65
|
+
"pettingzoo>=1.24.0",
|
|
66
|
+
]
|
|
63
67
|
# Convenience bundles
|
|
64
68
|
all = [
|
|
65
69
|
"tensorflow>=2.18.0",
|
|
@@ -7,6 +7,7 @@ import tensorflow as tf
|
|
|
7
7
|
from tensor_optix.core.base_agent import BaseAgent
|
|
8
8
|
from tensor_optix.core.types import EpisodeData, HyperparamSet
|
|
9
9
|
from tensor_optix.core.replay_buffer import PrioritizedReplayBuffer
|
|
10
|
+
from tensor_optix.core.obs_utils import to_batch_numpy, to_tf_batch, already_batched_to_tf, batch_index
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class TFDQNAgent(BaseAgent):
|
|
@@ -97,7 +98,7 @@ class TFDQNAgent(BaseAgent):
|
|
|
97
98
|
epsilon = float(self._hyperparams.params.get("epsilon", 0.1))
|
|
98
99
|
if np.random.random() < epsilon:
|
|
99
100
|
return np.random.randint(self._n_actions)
|
|
100
|
-
obs =
|
|
101
|
+
obs = to_tf_batch(observation)
|
|
101
102
|
q_values = self._q(obs, training=False)
|
|
102
103
|
return int(tf.argmax(q_values, axis=-1).numpy()[0])
|
|
103
104
|
|
|
@@ -109,7 +110,10 @@ class TFDQNAgent(BaseAgent):
|
|
|
109
110
|
epsilon_decay = float(hp.get("epsilon_decay", 0.995))
|
|
110
111
|
epsilon_min = float(hp.get("epsilon_min", 0.05))
|
|
111
112
|
|
|
112
|
-
|
|
113
|
+
# DQN pushes the RAW observation into the buffer (not pre-encoded)
|
|
114
|
+
# -- same single-network constraint as TorchDQNAgent, see
|
|
115
|
+
# torch_dqn.py's learn() comment for the full rationale.
|
|
116
|
+
obs_arr = to_batch_numpy(list(episode_data.observations))
|
|
113
117
|
act_arr = episode_data.actions
|
|
114
118
|
rew_arr = episode_data.rewards
|
|
115
119
|
done_arr = episode_data.dones
|
|
@@ -117,8 +121,8 @@ class TFDQNAgent(BaseAgent):
|
|
|
117
121
|
T = len(rew_arr)
|
|
118
122
|
for t in range(T - 1):
|
|
119
123
|
self._buffer.push(
|
|
120
|
-
obs_arr
|
|
121
|
-
obs_arr
|
|
124
|
+
batch_index(obs_arr, t), int(act_arr[t]), float(rew_arr[t]),
|
|
125
|
+
batch_index(obs_arr, t + 1), bool(done_arr[t]),
|
|
122
126
|
)
|
|
123
127
|
self._buffer.flush_episode()
|
|
124
128
|
|
|
@@ -138,10 +142,10 @@ class TFDQNAgent(BaseAgent):
|
|
|
138
142
|
|
|
139
143
|
for _ in range(n_updates):
|
|
140
144
|
obs_b, act_b, rew_b, next_obs_b, done_b, weights, indices, n_steps = self._buffer.sample(batch_size)
|
|
141
|
-
obs_b =
|
|
145
|
+
obs_b = already_batched_to_tf(obs_b)
|
|
142
146
|
act_b = tf.cast(act_b, tf.int32)
|
|
143
147
|
rew_b = tf.cast(rew_b, tf.float32)
|
|
144
|
-
next_obs_b =
|
|
148
|
+
next_obs_b = already_batched_to_tf(next_obs_b)
|
|
145
149
|
done_b = tf.cast(done_b, tf.float32)
|
|
146
150
|
weights = tf.cast(weights, tf.float32)
|
|
147
151
|
gammas_n = tf.cast(gamma ** n_steps.astype(np.float32), tf.float32)
|
|
@@ -4,6 +4,7 @@ import tensorflow as tf
|
|
|
4
4
|
from tensor_optix.core.base_agent import BaseAgent
|
|
5
5
|
from tensor_optix.core.types import EpisodeData, HyperparamSet
|
|
6
6
|
from tensor_optix.core.trajectory_buffer import compute_gae, make_minibatches, resolve_gae_bootstrap
|
|
7
|
+
from tensor_optix.core.obs_utils import to_batch_numpy, to_tf_batch, already_batched_to_tf
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class TFPPOAgent(BaseAgent):
|
|
@@ -89,7 +90,7 @@ class TFPPOAgent(BaseAgent):
|
|
|
89
90
|
Sample action from π(·|obs), cache log_prob and V(obs) for learn().
|
|
90
91
|
Returns a Python int (action index).
|
|
91
92
|
"""
|
|
92
|
-
obs =
|
|
93
|
+
obs = to_tf_batch(observation)
|
|
93
94
|
|
|
94
95
|
logits = self._actor(obs, training=False) # [1, n_actions]
|
|
95
96
|
log_probs_all = tf.nn.log_softmax(logits) # [1, n_actions]
|
|
@@ -98,7 +99,9 @@ class TFPPOAgent(BaseAgent):
|
|
|
98
99
|
|
|
99
100
|
value = float(tf.squeeze(self._critic(obs, training=False)).numpy())
|
|
100
101
|
|
|
101
|
-
|
|
102
|
+
# Cache the raw observation -- learn() re-batches via
|
|
103
|
+
# to_batch_numpy(), which handles both array and dict observations.
|
|
104
|
+
self._cache_obs.append(np.squeeze(observation) if not isinstance(observation, dict) else observation)
|
|
102
105
|
self._cache_log_probs.append(log_prob)
|
|
103
106
|
self._cache_values.append(value)
|
|
104
107
|
|
|
@@ -134,7 +137,7 @@ class TFPPOAgent(BaseAgent):
|
|
|
134
137
|
f"Cache underflow: expected >= {T} entries but got {len(self._cache_obs)}. "
|
|
135
138
|
"Ensure act() is called exactly once per environment step."
|
|
136
139
|
)
|
|
137
|
-
obs_arr =
|
|
140
|
+
obs_arr = to_batch_numpy(self._cache_obs[:T])
|
|
138
141
|
old_lp_arr = np.array(self._cache_log_probs[:T], dtype=np.float32)
|
|
139
142
|
values_arr = np.array(self._cache_values[:T], dtype=np.float32)
|
|
140
143
|
rewards = list(episode_data.rewards)
|
|
@@ -155,7 +158,7 @@ class TFPPOAgent(BaseAgent):
|
|
|
155
158
|
# bootstrap value per env, and n_envs tells compute_gae how the
|
|
156
159
|
# rewards/values array is interleaved across envs.
|
|
157
160
|
def _critic_value(obs):
|
|
158
|
-
obs_t =
|
|
161
|
+
obs_t = to_tf_batch(obs)
|
|
159
162
|
return float(tf.squeeze(self._critic(obs_t, training=False)).numpy())
|
|
160
163
|
|
|
161
164
|
last_value, n_envs = resolve_gae_bootstrap(episode_data, _critic_value)
|
|
@@ -183,7 +186,7 @@ class TFPPOAgent(BaseAgent):
|
|
|
183
186
|
|
|
184
187
|
for _ in range(n_epochs):
|
|
185
188
|
for batch in make_minibatches(rollout, mb_size):
|
|
186
|
-
obs_b =
|
|
189
|
+
obs_b = already_batched_to_tf(batch["obs"])
|
|
187
190
|
act_b = tf.cast(batch["actions"], tf.int32)
|
|
188
191
|
old_b = tf.cast(batch["old_lp"], tf.float32)
|
|
189
192
|
adv_b = tf.cast(batch["advantages"], tf.float32)
|
|
@@ -248,7 +251,7 @@ class TFPPOAgent(BaseAgent):
|
|
|
248
251
|
Used by PolicyManager.ensemble_action() to average distributions
|
|
249
252
|
across ensemble members rather than averaging sampled actions.
|
|
250
253
|
"""
|
|
251
|
-
obs =
|
|
254
|
+
obs = to_tf_batch(observation)
|
|
252
255
|
logits = self._actor(obs, training=False)
|
|
253
256
|
return tf.nn.softmax(logits)[0].numpy()
|
|
254
257
|
|
|
@@ -5,6 +5,7 @@ import tensorflow as tf
|
|
|
5
5
|
from tensor_optix.core.base_agent import BaseAgent
|
|
6
6
|
from tensor_optix.core.types import EpisodeData, HyperparamSet
|
|
7
7
|
from tensor_optix.core.trajectory_buffer import compute_gae, make_minibatches, resolve_gae_bootstrap
|
|
8
|
+
from tensor_optix.core.obs_utils import to_batch_numpy, to_tf_batch, already_batched_to_tf
|
|
8
9
|
|
|
9
10
|
LOG_STD_MIN = -5
|
|
10
11
|
LOG_STD_MAX = 2
|
|
@@ -133,7 +134,7 @@ class TFGaussianPPOAgent(BaseAgent):
|
|
|
133
134
|
Sample action ∈ (-1, 1)^action_dim; cache log_prob and V(obs) for learn().
|
|
134
135
|
Returns a float numpy array of shape [action_dim].
|
|
135
136
|
"""
|
|
136
|
-
obs =
|
|
137
|
+
obs = to_tf_batch(observation)
|
|
137
138
|
|
|
138
139
|
out = self._actor(obs, training=False) # [1, 2*action_dim]
|
|
139
140
|
mean = out[:, :self._action_dim]
|
|
@@ -147,7 +148,7 @@ class TFGaussianPPOAgent(BaseAgent):
|
|
|
147
148
|
log_prob = self._gaussian_log_prob(pre_tanh, mean, log_std) # [1]
|
|
148
149
|
value = float(tf.squeeze(self._critic(obs, training=False)).numpy())
|
|
149
150
|
|
|
150
|
-
self._cache_obs.append(np.squeeze(observation))
|
|
151
|
+
self._cache_obs.append(np.squeeze(observation) if not isinstance(observation, dict) else observation)
|
|
151
152
|
self._cache_log_probs.append(float(tf.squeeze(log_prob).numpy()))
|
|
152
153
|
self._cache_values.append(value)
|
|
153
154
|
|
|
@@ -179,7 +180,7 @@ class TFGaussianPPOAgent(BaseAgent):
|
|
|
179
180
|
"Ensure act() is called exactly once per environment step."
|
|
180
181
|
)
|
|
181
182
|
|
|
182
|
-
obs_arr =
|
|
183
|
+
obs_arr = to_batch_numpy(self._cache_obs[:T])
|
|
183
184
|
old_lp_arr = np.array(self._cache_log_probs[:T], dtype=np.float32)
|
|
184
185
|
val_arr = np.array(self._cache_values[:T], dtype=np.float32)
|
|
185
186
|
act_arr = np.array(episode_data.actions, dtype=np.float32)
|
|
@@ -194,7 +195,7 @@ class TFGaussianPPOAgent(BaseAgent):
|
|
|
194
195
|
rewards = list(self._reward_normalizer.normalize(np.array(rewards, dtype=np.float32)))
|
|
195
196
|
|
|
196
197
|
def _critic_value(obs):
|
|
197
|
-
obs_t =
|
|
198
|
+
obs_t = to_tf_batch(obs)
|
|
198
199
|
return float(tf.squeeze(self._critic(obs_t, training=False)).numpy())
|
|
199
200
|
|
|
200
201
|
last_value, n_envs = resolve_gae_bootstrap(episode_data, _critic_value)
|
|
@@ -220,7 +221,7 @@ class TFGaussianPPOAgent(BaseAgent):
|
|
|
220
221
|
|
|
221
222
|
for _ in range(n_epochs):
|
|
222
223
|
for batch in make_minibatches(rollout, mb_size):
|
|
223
|
-
obs_b =
|
|
224
|
+
obs_b = already_batched_to_tf(batch["obs"])
|
|
224
225
|
act_b = tf.cast(batch["actions"], tf.float32)
|
|
225
226
|
old_b = tf.cast(batch["old_lp"], tf.float32)
|
|
226
227
|
adv_b = tf.cast(batch["advantages"], tf.float32)
|
|
@@ -7,6 +7,7 @@ import tensorflow as tf
|
|
|
7
7
|
from tensor_optix.core.base_agent import BaseAgent
|
|
8
8
|
from tensor_optix.core.types import EpisodeData, HyperparamSet
|
|
9
9
|
from tensor_optix.core.replay_buffer import PrioritizedReplayBuffer
|
|
10
|
+
from tensor_optix.core.obs_utils import to_batch_numpy, to_tf_batch, already_batched_to_tf
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class TFSACAgent(BaseAgent):
|
|
@@ -132,14 +133,28 @@ class TFSACAgent(BaseAgent):
|
|
|
132
133
|
def is_on_policy(self) -> bool:
|
|
133
134
|
return False # replay buffer — rollback without buffer clear is harmful
|
|
134
135
|
|
|
136
|
+
def _encode(self, observation_batched):
|
|
137
|
+
"""Run the actor's encoder alone, if it has one (for non-flat
|
|
138
|
+
observations); identity pass-through for the flat-Box case,
|
|
139
|
+
exactly matching today's behavior. Used to pre-encode observations
|
|
140
|
+
before they enter the replay buffer -- see tensor_optix.encoders'
|
|
141
|
+
module docstring for why SAC's critic only ever sees already-
|
|
142
|
+
encoded flat vectors, never raw observations. self._actor.encoder
|
|
143
|
+
is the standalone encoder Model, attached by the factory alongside
|
|
144
|
+
head_model (see _make_sac_tf), so this never has to split the
|
|
145
|
+
composed actor Model's graph apart at call time."""
|
|
146
|
+
encoder = getattr(self._actor, "encoder", None)
|
|
147
|
+
if encoder is None:
|
|
148
|
+
return observation_batched
|
|
149
|
+
return encoder(observation_batched, training=False)
|
|
150
|
+
|
|
135
151
|
def act(self, observation) -> np.ndarray:
|
|
136
152
|
"""
|
|
137
153
|
Sample action from the squashed Gaussian policy.
|
|
138
154
|
Returns np.ndarray of shape [action_dim], values in (-1, 1).
|
|
139
155
|
"""
|
|
140
|
-
obs =
|
|
156
|
+
obs = to_tf_batch(observation)
|
|
141
157
|
action, _ = self._sample_action(obs, training=False)
|
|
142
|
-
self._last_obs = np.array(observation, dtype=np.float32)
|
|
143
158
|
return action.numpy()[0]
|
|
144
159
|
|
|
145
160
|
def learn(self, episode_data: EpisodeData) -> dict:
|
|
@@ -153,7 +168,8 @@ class TFSACAgent(BaseAgent):
|
|
|
153
168
|
batch_size = int(hp.get("batch_size", 256))
|
|
154
169
|
updates_per_step = float(hp.get("updates_per_step", 1))
|
|
155
170
|
|
|
156
|
-
|
|
171
|
+
obs_batched = to_tf_batch(list(episode_data.observations))
|
|
172
|
+
obs_arr = self._encode(obs_batched).numpy()
|
|
157
173
|
act_arr = np.array(episode_data.actions, dtype=np.float32)
|
|
158
174
|
rew_arr = episode_data.rewards
|
|
159
175
|
done_arr = episode_data.dones
|
|
@@ -267,15 +283,25 @@ class TFSACAgent(BaseAgent):
|
|
|
267
283
|
# Internal helpers
|
|
268
284
|
# ------------------------------------------------------------------
|
|
269
285
|
|
|
270
|
-
def _sample_action(self, obs_tf, training: bool):
|
|
286
|
+
def _sample_action(self, obs_tf, training: bool, pre_encoded: bool = False):
|
|
271
287
|
"""
|
|
272
288
|
Reparameterized squashed Gaussian sample.
|
|
273
289
|
|
|
290
|
+
pre_encoded=True: obs_tf is already a flat, encoder-output vector
|
|
291
|
+
(buffer-sampled data) -- route through the actor's head_model only,
|
|
292
|
+
skipping its encoder a second time (see tensor_optix.encoders'
|
|
293
|
+
module docstring). pre_encoded=False (default): obs_tf is a raw
|
|
294
|
+
observation (from act()) -- route through the full composed actor
|
|
295
|
+
(encoder + head). For the flat-Box case (actor has no head_model
|
|
296
|
+
attribute, it IS the head) this falls back to calling it directly,
|
|
297
|
+
identical to today's behavior.
|
|
298
|
+
|
|
274
299
|
Returns:
|
|
275
300
|
action: tanh(mean + std * ε), shape [batch, action_dim]
|
|
276
301
|
log_prob: log π(action|obs) with tanh correction, shape [batch]
|
|
277
302
|
"""
|
|
278
|
-
|
|
303
|
+
net = getattr(self._actor, "head_model", self._actor) if pre_encoded else self._actor
|
|
304
|
+
out = net(obs_tf, training=training) # [batch, 2*action_dim]
|
|
279
305
|
mean, log_std = tf.split(out, 2, axis=-1)
|
|
280
306
|
log_std = tf.clip_by_value(log_std, self.LOG_STD_MIN, self.LOG_STD_MAX)
|
|
281
307
|
std = tf.exp(log_std)
|
|
@@ -306,18 +332,23 @@ class TFSACAgent(BaseAgent):
|
|
|
306
332
|
def _update_step(self, batch_size: int, gamma: float, tau: float):
|
|
307
333
|
"""One full SAC gradient step (critic + actor + alpha + soft update)."""
|
|
308
334
|
obs_b, act_b, rew_b, next_obs_b, done_b, weights, indices, n_steps = self._buffer.sample(batch_size)
|
|
309
|
-
obs_b =
|
|
335
|
+
obs_b = already_batched_to_tf(obs_b)
|
|
310
336
|
act_b = tf.cast(act_b, tf.float32)
|
|
311
337
|
rew_b = tf.cast(rew_b, tf.float32)
|
|
312
|
-
next_obs_b =
|
|
338
|
+
next_obs_b = already_batched_to_tf(next_obs_b)
|
|
313
339
|
done_b = tf.cast(done_b, tf.float32)
|
|
314
340
|
weights = tf.cast(weights, tf.float32)
|
|
315
341
|
gammas_n = tf.cast(gamma ** n_steps.astype(np.float32), tf.float32)
|
|
316
342
|
alpha = tf.exp(self._log_alpha)
|
|
317
343
|
|
|
344
|
+
# Buffer-sampled obs_b/next_obs_b are already flat, pre-encoded
|
|
345
|
+
# vectors -- actor updates below train only the head's variables,
|
|
346
|
+
# never the (frozen, collection-time-only) encoder's.
|
|
347
|
+
actor_trainable_vars = getattr(self._actor, "head_model", self._actor).trainable_variables
|
|
348
|
+
|
|
318
349
|
# ---- Critic update ----
|
|
319
350
|
with tf.GradientTape() as tape:
|
|
320
|
-
next_a, next_lp = self._sample_action(next_obs_b, training=False)
|
|
351
|
+
next_a, next_lp = self._sample_action(next_obs_b, training=False, pre_encoded=True)
|
|
321
352
|
ci_next = self._critic_input(next_obs_b, next_a)
|
|
322
353
|
q1_tgt = tf.squeeze(self._c1_tgt(ci_next, training=False), -1)
|
|
323
354
|
q2_tgt = tf.squeeze(self._c2_tgt(ci_next, training=False), -1)
|
|
@@ -339,19 +370,19 @@ class TFSACAgent(BaseAgent):
|
|
|
339
370
|
|
|
340
371
|
# ---- Actor update ----
|
|
341
372
|
with tf.GradientTape() as tape:
|
|
342
|
-
new_a, new_lp = self._sample_action(obs_b, training=True)
|
|
373
|
+
new_a, new_lp = self._sample_action(obs_b, training=True, pre_encoded=True)
|
|
343
374
|
ci_new = self._critic_input(obs_b, new_a)
|
|
344
375
|
q1_new = tf.squeeze(self._c1(ci_new, training=False), -1)
|
|
345
376
|
q2_new = tf.squeeze(self._c2(ci_new, training=False), -1)
|
|
346
377
|
a_loss = tf.reduce_mean(alpha * new_lp - tf.minimum(q1_new, q2_new))
|
|
347
378
|
|
|
348
|
-
a_grads = tape.gradient(a_loss,
|
|
379
|
+
a_grads = tape.gradient(a_loss, actor_trainable_vars)
|
|
349
380
|
a_grads, _ = tf.clip_by_global_norm(a_grads, 10.0)
|
|
350
|
-
self._actor_opt.apply_gradients(zip(a_grads,
|
|
381
|
+
self._actor_opt.apply_gradients(zip(a_grads, actor_trainable_vars))
|
|
351
382
|
|
|
352
383
|
# ---- Alpha (entropy temperature) update ----
|
|
353
384
|
with tf.GradientTape() as tape:
|
|
354
|
-
_, new_lp2 = self._sample_action(obs_b, training=False)
|
|
385
|
+
_, new_lp2 = self._sample_action(obs_b, training=False, pre_encoded=True)
|
|
355
386
|
alpha_loss = -tf.reduce_mean(
|
|
356
387
|
self._log_alpha * tf.stop_gradient(new_lp2 + self._target_entropy)
|
|
357
388
|
)
|
|
@@ -62,6 +62,7 @@ import tensorflow as tf
|
|
|
62
62
|
from tensor_optix.core.base_agent import BaseAgent
|
|
63
63
|
from tensor_optix.core.types import EpisodeData, HyperparamSet
|
|
64
64
|
from tensor_optix.core.replay_buffer import PrioritizedReplayBuffer
|
|
65
|
+
from tensor_optix.core.obs_utils import to_tf_batch, already_batched_to_tf
|
|
65
66
|
|
|
66
67
|
|
|
67
68
|
class TFTDDAgent(BaseAgent):
|
|
@@ -97,6 +98,20 @@ class TFTDDAgent(BaseAgent):
|
|
|
97
98
|
self._c1_tgt.set_weights(critic1.get_weights())
|
|
98
99
|
self._c2_tgt.set_weights(critic2.get_weights())
|
|
99
100
|
|
|
101
|
+
# clone_model() reconstructs the Model graph/config but does not
|
|
102
|
+
# carry over head_model/encoder (plain Python attributes the
|
|
103
|
+
# factory attaches post-construction, not part of the Keras
|
|
104
|
+
# graph/config clone_model reads from) -- re-derive them the same
|
|
105
|
+
# way so _update_step()'s getattr(self._actor_tgt, "head_model",
|
|
106
|
+
# ...) lookups actually find the real head-only submodel instead
|
|
107
|
+
# of silently falling back to the full (encoder+head) actor_tgt.
|
|
108
|
+
if hasattr(actor, "head_model"):
|
|
109
|
+
self._actor_tgt.head_model = tf.keras.models.clone_model(actor.head_model)
|
|
110
|
+
self._actor_tgt.head_model.set_weights(actor.head_model.get_weights())
|
|
111
|
+
if hasattr(actor, "encoder"):
|
|
112
|
+
self._actor_tgt.encoder = tf.keras.models.clone_model(actor.encoder)
|
|
113
|
+
self._actor_tgt.encoder.set_weights(actor.encoder.get_weights())
|
|
114
|
+
|
|
100
115
|
self._action_dim = action_dim
|
|
101
116
|
self._actor_opt = actor_optimizer
|
|
102
117
|
self._critic_opt = critic_optimizer
|
|
@@ -120,9 +135,17 @@ class TFTDDAgent(BaseAgent):
|
|
|
120
135
|
def is_on_policy(self) -> bool:
|
|
121
136
|
return False
|
|
122
137
|
|
|
138
|
+
def _encode(self, observation_batched):
|
|
139
|
+
"""Run the actor's encoder alone, if it has one -- see
|
|
140
|
+
tf_sac.py's _encode() docstring for the full rationale."""
|
|
141
|
+
encoder = getattr(self._actor, "encoder", None)
|
|
142
|
+
if encoder is None:
|
|
143
|
+
return observation_batched
|
|
144
|
+
return encoder(observation_batched, training=False)
|
|
145
|
+
|
|
123
146
|
def act(self, observation) -> np.ndarray:
|
|
124
147
|
"""Deterministic action: a = π_θ(s) = tanh(actor(s))."""
|
|
125
|
-
obs =
|
|
148
|
+
obs = to_tf_batch(observation)
|
|
126
149
|
action = self._actor(obs, training=False)
|
|
127
150
|
return action.numpy()[0]
|
|
128
151
|
|
|
@@ -134,7 +157,8 @@ class TFTDDAgent(BaseAgent):
|
|
|
134
157
|
updates_per_step = float(hp.get("updates_per_step", 1))
|
|
135
158
|
policy_delay = int(hp.get("policy_delay", 2))
|
|
136
159
|
|
|
137
|
-
|
|
160
|
+
obs_batched = to_tf_batch(list(episode_data.observations))
|
|
161
|
+
obs_arr = self._encode(obs_batched).numpy()
|
|
138
162
|
act_arr = np.array(episode_data.actions, dtype=np.float32)
|
|
139
163
|
rew_arr = episode_data.rewards
|
|
140
164
|
done_arr = episode_data.dones
|
|
@@ -265,14 +289,21 @@ class TFTDDAgent(BaseAgent):
|
|
|
265
289
|
obs_b, act_b, rew_b, next_b, done_b, weights, indices, n_steps = \
|
|
266
290
|
self._buffer.sample(batch_size)
|
|
267
291
|
|
|
268
|
-
obs_b =
|
|
292
|
+
obs_b = already_batched_to_tf(obs_b)
|
|
269
293
|
act_b = tf.cast(act_b, tf.float32)
|
|
270
294
|
rew_b = tf.cast(rew_b, tf.float32)
|
|
271
|
-
next_b =
|
|
295
|
+
next_b = already_batched_to_tf(next_b)
|
|
272
296
|
done_b = tf.cast(done_b, tf.float32)
|
|
273
297
|
weights = tf.cast(weights, tf.float32)
|
|
274
298
|
gammas_n = tf.cast(gamma ** n_steps.astype(np.float32), tf.float32)
|
|
275
299
|
|
|
300
|
+
# Buffer-sampled obs_b/next_b are already flat, pre-encoded vectors
|
|
301
|
+
# -- route through head_model only, skipping the encoder a second
|
|
302
|
+
# time (see tf_sac.py's _update_step comment for the full
|
|
303
|
+
# rationale). Falls back to the full actor/actor_tgt for flat-Box.
|
|
304
|
+
actor_head = getattr(self._actor, "head_model", self._actor)
|
|
305
|
+
actor_tgt_head = getattr(self._actor_tgt, "head_model", self._actor_tgt)
|
|
306
|
+
|
|
276
307
|
# ---- Critic update ----
|
|
277
308
|
with tf.GradientTape() as tape:
|
|
278
309
|
# Fix 3: target policy smoothing
|
|
@@ -282,7 +313,7 @@ class TFTDDAgent(BaseAgent):
|
|
|
282
313
|
target_noise_clip,
|
|
283
314
|
)
|
|
284
315
|
next_action = tf.clip_by_value(
|
|
285
|
-
|
|
316
|
+
actor_tgt_head(next_b, training=False) + noise, -1.0, 1.0
|
|
286
317
|
)
|
|
287
318
|
|
|
288
319
|
# Fix 1: twin critics
|
|
@@ -311,20 +342,20 @@ class TFTDDAgent(BaseAgent):
|
|
|
311
342
|
a_loss = 0.0
|
|
312
343
|
if update_actor:
|
|
313
344
|
with tf.GradientTape() as tape:
|
|
314
|
-
new_a =
|
|
345
|
+
new_a = actor_head(obs_b, training=True)
|
|
315
346
|
ci_new = tf.concat([obs_b, new_a], axis=-1)
|
|
316
347
|
# Deterministic policy gradient: maximise Q1(s, π(s))
|
|
317
348
|
a_loss_tf = -tf.reduce_mean(
|
|
318
349
|
tf.squeeze(self._c1(ci_new, training=False), -1)
|
|
319
350
|
)
|
|
320
351
|
|
|
321
|
-
a_grads = tape.gradient(a_loss_tf,
|
|
352
|
+
a_grads = tape.gradient(a_loss_tf, actor_head.trainable_variables)
|
|
322
353
|
a_grads, _ = tf.clip_by_global_norm(a_grads, 10.0)
|
|
323
354
|
self._actor_opt.apply_gradients(
|
|
324
|
-
zip(a_grads,
|
|
355
|
+
zip(a_grads, actor_head.trainable_variables)
|
|
325
356
|
)
|
|
326
357
|
a_loss = float(a_loss_tf.numpy())
|
|
327
|
-
self._soft_update(
|
|
358
|
+
self._soft_update(actor_head, actor_tgt_head, tau)
|
|
328
359
|
|
|
329
360
|
self._soft_update(self._c1, self._c1_tgt, tau)
|
|
330
361
|
self._soft_update(self._c2, self._c2_tgt, tau)
|