exponet 0.1.0__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.
- exponet-0.1.0/LICENSE +21 -0
- exponet-0.1.0/PKG-INFO +146 -0
- exponet-0.1.0/README.md +129 -0
- exponet-0.1.0/pyproject.toml +33 -0
- exponet-0.1.0/setup.cfg +4 -0
- exponet-0.1.0/src/exponet/__init__.py +8 -0
- exponet-0.1.0/src/exponet/_persistence.py +172 -0
- exponet-0.1.0/src/exponet/_training.py +259 -0
- exponet-0.1.0/src/exponet/_validation.py +163 -0
- exponet-0.1.0/src/exponet/activations.py +167 -0
- exponet-0.1.0/src/exponet/estimators.py +747 -0
- exponet-0.1.0/src/exponet/nn.py +147 -0
- exponet-0.1.0/src/exponet.egg-info/PKG-INFO +146 -0
- exponet-0.1.0/src/exponet.egg-info/SOURCES.txt +20 -0
- exponet-0.1.0/src/exponet.egg-info/dependency_links.txt +1 -0
- exponet-0.1.0/src/exponet.egg-info/requires.txt +7 -0
- exponet-0.1.0/src/exponet.egg-info/top_level.txt +1 -0
- exponet-0.1.0/tests/test_activations.py +635 -0
- exponet-0.1.0/tests/test_classifier.py +149 -0
- exponet-0.1.0/tests/test_estimators.py +212 -0
- exponet-0.1.0/tests/test_nn.py +294 -0
- exponet-0.1.0/tests/test_persistence.py +202 -0
exponet-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ExpoNet Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
exponet-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: exponet
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Trainable blend of ReLU and squared ReLU activations for PyTorch
|
|
5
|
+
Author: ExpoNet Contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: torch>=2.0.0
|
|
11
|
+
Requires-Dist: numpy>=1.24.0
|
|
12
|
+
Requires-Dist: scikit-learn>=1.3.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
15
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# ExpoNet
|
|
19
|
+
|
|
20
|
+
ExpoNet is a compact PyTorch library for dense numeric models with a trainable
|
|
21
|
+
blend of ReLU and squared ReLU:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
u = max(x, 0)
|
|
25
|
+
f(x; a) = (1 - a) * u + a * u * u
|
|
26
|
+
0 <= a <= 1
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The primary activation uses one learned coefficient per hidden neuron. It uses
|
|
30
|
+
multiplication and addition on activation values, not a general power. ExpoNet
|
|
31
|
+
also provides scikit-learn-compatible dense regression and classification
|
|
32
|
+
estimators with CPU or a single CUDA device.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
ExpoNet requires Python 3.11 or later, PyTorch, NumPy, and scikit-learn.
|
|
37
|
+
Install the PyTorch build appropriate for your platform and GPU first, using
|
|
38
|
+
the [official PyTorch selector](https://pytorch.org/get-started/locally/), then
|
|
39
|
+
install this checkout:
|
|
40
|
+
|
|
41
|
+
```powershell
|
|
42
|
+
python -m pip install .
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For a checkout, install the project and its development tools with:
|
|
46
|
+
|
|
47
|
+
```powershell
|
|
48
|
+
python -m pip install -e ".[dev]"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Package publication is not part of this release-readiness milestone; install a
|
|
52
|
+
built wheel locally with `python -m pip install dist/exponet-0.1.0-py3-none-any.whl`.
|
|
53
|
+
|
|
54
|
+
`device="auto"` uses CUDA when `torch.cuda.is_available()` is true, otherwise
|
|
55
|
+
CPU. `device="cuda"` or `device="cuda:N"` requires that device and raises if
|
|
56
|
+
it is unavailable; ExpoNet never silently falls back to CPU. A CUDA toolkit
|
|
57
|
+
compiler (`nvcc`) is not required for the prebuilt-PyTorch path.
|
|
58
|
+
|
|
59
|
+
Verified release-candidate coverage is Windows 11 CPU and CUDA on an NVIDIA
|
|
60
|
+
GeForce RTX 5060 (PyTorch 2.11.0+cu128, CUDA runtime 12.8, Python 3.12.10,
|
|
61
|
+
NumPy 2.5.2, scikit-learn 1.9.0). Linux has not yet been validated, so it is
|
|
62
|
+
not claimed as supported coverage.
|
|
63
|
+
|
|
64
|
+
## Quick start
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
import numpy as np
|
|
68
|
+
from exponet import ExpoRegressor
|
|
69
|
+
|
|
70
|
+
rng = np.random.default_rng(0)
|
|
71
|
+
X = rng.normal(size=(80, 2)).astype(np.float32)
|
|
72
|
+
y = (1.5 * X[:, 0] - 0.75 * X[:, 1] + 0.2).astype(np.float32)
|
|
73
|
+
|
|
74
|
+
model = ExpoRegressor(
|
|
75
|
+
hidden_dims=(8,),
|
|
76
|
+
normalization="none",
|
|
77
|
+
trainable_blend=False,
|
|
78
|
+
blend_init=0.0, # ReLU control
|
|
79
|
+
epochs=80,
|
|
80
|
+
lr=0.03,
|
|
81
|
+
device="auto",
|
|
82
|
+
random_state=0,
|
|
83
|
+
).fit(X, y)
|
|
84
|
+
|
|
85
|
+
predictions = model.predict(X[:3]) # shape: (3,)
|
|
86
|
+
print(model.device_, predictions)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The same verified examples are available for [direct PyTorch use](examples/direct_torch.py),
|
|
90
|
+
[regression](examples/regression.py), and [multiclass classification](examples/classification.py).
|
|
91
|
+
|
|
92
|
+
## Public API
|
|
93
|
+
|
|
94
|
+
| Export | Purpose | Output shape |
|
|
95
|
+
| --- | --- | --- |
|
|
96
|
+
| `ExpoActivation` | Reusable PyTorch activation. | Same as input tensor. |
|
|
97
|
+
| `ExpoMLP` | Dense `Linear -> optional LayerNorm -> ExpoActivation` blocks and a linear readout. | `(batch, out_features)` |
|
|
98
|
+
| `ExpoRegressor` | Dense numeric regression estimator. | `predict`: `(N,)` for 1-D targets, otherwise `(N, K)` |
|
|
99
|
+
| `ExpoClassifier` | Binary/multiclass integer or string-label estimator. | `predict`: `(N,)`; `predict_proba`: `(N, K)` in `classes_` order |
|
|
100
|
+
|
|
101
|
+
Both estimators accept dense finite numeric features shaped `(N, F)`, train in
|
|
102
|
+
float32, provide `fit`, `predict`, `score`, `get_blend_weights`, and restricted
|
|
103
|
+
inference `save`/`load` methods. See the [API contract](docs/API_CONTRACT.md)
|
|
104
|
+
for constructor parameters, validation, scaling, early stopping, persistence,
|
|
105
|
+
and reproducibility semantics.
|
|
106
|
+
|
|
107
|
+
## Scope and limitations
|
|
108
|
+
|
|
109
|
+
- This is a dense numeric-data library. Sparse inputs, missing values,
|
|
110
|
+
categorical encoding, image/sequence architectures, sample/class weights,
|
|
111
|
+
streaming, warm starts, AMP, compilation, distributed execution, and custom
|
|
112
|
+
training hooks are outside this release.
|
|
113
|
+
- Classification accepts homogeneous integer or string labels only; continuous,
|
|
114
|
+
mixed, multilabel, and multioutput labels are rejected.
|
|
115
|
+
- Float32 is the primary runtime dtype. CPU float64 is supported for low-level
|
|
116
|
+
activation mathematics tests. Very large positive activations can overflow,
|
|
117
|
+
particularly toward the squared-ReLU endpoint.
|
|
118
|
+
- Saved snapshots restore inference state only, not optimizer or RNG state; do
|
|
119
|
+
not treat a loaded estimator as an exact training-resume checkpoint.
|
|
120
|
+
- The initial controlled evaluation did not show a consistent predictive
|
|
121
|
+
advantage over native ReLU, so no superiority or universal-normalization
|
|
122
|
+
claim is made. See the [initial evaluation](docs/INITIAL_EVALUATION.md).
|
|
123
|
+
|
|
124
|
+
## Development validation
|
|
125
|
+
|
|
126
|
+
```powershell
|
|
127
|
+
python -B -m ruff format --check --no-cache --no-respect-gitignore src tests benchmarks examples
|
|
128
|
+
python -B -m ruff check --no-cache --no-respect-gitignore src tests benchmarks examples
|
|
129
|
+
python -B -m pytest -q -p no:cacheprovider
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The [roadmap](docs/ROADMAP.md) records exact validation evidence and release
|
|
133
|
+
scope. ExpoNet is available under the [MIT License](LICENSE).
|
|
134
|
+
|
|
135
|
+
## Design and evidence
|
|
136
|
+
|
|
137
|
+
| Document | Purpose |
|
|
138
|
+
| --- | --- |
|
|
139
|
+
| [API contract](docs/API_CONTRACT.md) | Implemented interfaces and exact behavior. |
|
|
140
|
+
| [Design](docs/DESIGN.md) | Mathematics, numerical behavior, normalization, and architecture. |
|
|
141
|
+
| [Decisions](docs/DECISIONS.md) | Accepted scope and defaults. |
|
|
142
|
+
| [Validation](docs/VALIDATION.md) | Correctness checks and experimental criteria. |
|
|
143
|
+
| [Initial evaluation](docs/INITIAL_EVALUATION.md) | P6 five-seed activation comparison. |
|
|
144
|
+
| [Activation timing](docs/ACTIVATION_BENCHMARK.md) | P1.04 CPU timing evidence and limitations. |
|
|
145
|
+
| [PSANN reuse assessment](docs/PSANN_REUSE.md) | Selective adaptation provenance; no PSANN runtime dependency. |
|
|
146
|
+
| [Roadmap](docs/ROADMAP.md) | Completed work, release evidence, and deferred scope. |
|
exponet-0.1.0/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# ExpoNet
|
|
2
|
+
|
|
3
|
+
ExpoNet is a compact PyTorch library for dense numeric models with a trainable
|
|
4
|
+
blend of ReLU and squared ReLU:
|
|
5
|
+
|
|
6
|
+
```text
|
|
7
|
+
u = max(x, 0)
|
|
8
|
+
f(x; a) = (1 - a) * u + a * u * u
|
|
9
|
+
0 <= a <= 1
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The primary activation uses one learned coefficient per hidden neuron. It uses
|
|
13
|
+
multiplication and addition on activation values, not a general power. ExpoNet
|
|
14
|
+
also provides scikit-learn-compatible dense regression and classification
|
|
15
|
+
estimators with CPU or a single CUDA device.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
ExpoNet requires Python 3.11 or later, PyTorch, NumPy, and scikit-learn.
|
|
20
|
+
Install the PyTorch build appropriate for your platform and GPU first, using
|
|
21
|
+
the [official PyTorch selector](https://pytorch.org/get-started/locally/), then
|
|
22
|
+
install this checkout:
|
|
23
|
+
|
|
24
|
+
```powershell
|
|
25
|
+
python -m pip install .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For a checkout, install the project and its development tools with:
|
|
29
|
+
|
|
30
|
+
```powershell
|
|
31
|
+
python -m pip install -e ".[dev]"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Package publication is not part of this release-readiness milestone; install a
|
|
35
|
+
built wheel locally with `python -m pip install dist/exponet-0.1.0-py3-none-any.whl`.
|
|
36
|
+
|
|
37
|
+
`device="auto"` uses CUDA when `torch.cuda.is_available()` is true, otherwise
|
|
38
|
+
CPU. `device="cuda"` or `device="cuda:N"` requires that device and raises if
|
|
39
|
+
it is unavailable; ExpoNet never silently falls back to CPU. A CUDA toolkit
|
|
40
|
+
compiler (`nvcc`) is not required for the prebuilt-PyTorch path.
|
|
41
|
+
|
|
42
|
+
Verified release-candidate coverage is Windows 11 CPU and CUDA on an NVIDIA
|
|
43
|
+
GeForce RTX 5060 (PyTorch 2.11.0+cu128, CUDA runtime 12.8, Python 3.12.10,
|
|
44
|
+
NumPy 2.5.2, scikit-learn 1.9.0). Linux has not yet been validated, so it is
|
|
45
|
+
not claimed as supported coverage.
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import numpy as np
|
|
51
|
+
from exponet import ExpoRegressor
|
|
52
|
+
|
|
53
|
+
rng = np.random.default_rng(0)
|
|
54
|
+
X = rng.normal(size=(80, 2)).astype(np.float32)
|
|
55
|
+
y = (1.5 * X[:, 0] - 0.75 * X[:, 1] + 0.2).astype(np.float32)
|
|
56
|
+
|
|
57
|
+
model = ExpoRegressor(
|
|
58
|
+
hidden_dims=(8,),
|
|
59
|
+
normalization="none",
|
|
60
|
+
trainable_blend=False,
|
|
61
|
+
blend_init=0.0, # ReLU control
|
|
62
|
+
epochs=80,
|
|
63
|
+
lr=0.03,
|
|
64
|
+
device="auto",
|
|
65
|
+
random_state=0,
|
|
66
|
+
).fit(X, y)
|
|
67
|
+
|
|
68
|
+
predictions = model.predict(X[:3]) # shape: (3,)
|
|
69
|
+
print(model.device_, predictions)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The same verified examples are available for [direct PyTorch use](examples/direct_torch.py),
|
|
73
|
+
[regression](examples/regression.py), and [multiclass classification](examples/classification.py).
|
|
74
|
+
|
|
75
|
+
## Public API
|
|
76
|
+
|
|
77
|
+
| Export | Purpose | Output shape |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| `ExpoActivation` | Reusable PyTorch activation. | Same as input tensor. |
|
|
80
|
+
| `ExpoMLP` | Dense `Linear -> optional LayerNorm -> ExpoActivation` blocks and a linear readout. | `(batch, out_features)` |
|
|
81
|
+
| `ExpoRegressor` | Dense numeric regression estimator. | `predict`: `(N,)` for 1-D targets, otherwise `(N, K)` |
|
|
82
|
+
| `ExpoClassifier` | Binary/multiclass integer or string-label estimator. | `predict`: `(N,)`; `predict_proba`: `(N, K)` in `classes_` order |
|
|
83
|
+
|
|
84
|
+
Both estimators accept dense finite numeric features shaped `(N, F)`, train in
|
|
85
|
+
float32, provide `fit`, `predict`, `score`, `get_blend_weights`, and restricted
|
|
86
|
+
inference `save`/`load` methods. See the [API contract](docs/API_CONTRACT.md)
|
|
87
|
+
for constructor parameters, validation, scaling, early stopping, persistence,
|
|
88
|
+
and reproducibility semantics.
|
|
89
|
+
|
|
90
|
+
## Scope and limitations
|
|
91
|
+
|
|
92
|
+
- This is a dense numeric-data library. Sparse inputs, missing values,
|
|
93
|
+
categorical encoding, image/sequence architectures, sample/class weights,
|
|
94
|
+
streaming, warm starts, AMP, compilation, distributed execution, and custom
|
|
95
|
+
training hooks are outside this release.
|
|
96
|
+
- Classification accepts homogeneous integer or string labels only; continuous,
|
|
97
|
+
mixed, multilabel, and multioutput labels are rejected.
|
|
98
|
+
- Float32 is the primary runtime dtype. CPU float64 is supported for low-level
|
|
99
|
+
activation mathematics tests. Very large positive activations can overflow,
|
|
100
|
+
particularly toward the squared-ReLU endpoint.
|
|
101
|
+
- Saved snapshots restore inference state only, not optimizer or RNG state; do
|
|
102
|
+
not treat a loaded estimator as an exact training-resume checkpoint.
|
|
103
|
+
- The initial controlled evaluation did not show a consistent predictive
|
|
104
|
+
advantage over native ReLU, so no superiority or universal-normalization
|
|
105
|
+
claim is made. See the [initial evaluation](docs/INITIAL_EVALUATION.md).
|
|
106
|
+
|
|
107
|
+
## Development validation
|
|
108
|
+
|
|
109
|
+
```powershell
|
|
110
|
+
python -B -m ruff format --check --no-cache --no-respect-gitignore src tests benchmarks examples
|
|
111
|
+
python -B -m ruff check --no-cache --no-respect-gitignore src tests benchmarks examples
|
|
112
|
+
python -B -m pytest -q -p no:cacheprovider
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The [roadmap](docs/ROADMAP.md) records exact validation evidence and release
|
|
116
|
+
scope. ExpoNet is available under the [MIT License](LICENSE).
|
|
117
|
+
|
|
118
|
+
## Design and evidence
|
|
119
|
+
|
|
120
|
+
| Document | Purpose |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| [API contract](docs/API_CONTRACT.md) | Implemented interfaces and exact behavior. |
|
|
123
|
+
| [Design](docs/DESIGN.md) | Mathematics, numerical behavior, normalization, and architecture. |
|
|
124
|
+
| [Decisions](docs/DECISIONS.md) | Accepted scope and defaults. |
|
|
125
|
+
| [Validation](docs/VALIDATION.md) | Correctness checks and experimental criteria. |
|
|
126
|
+
| [Initial evaluation](docs/INITIAL_EVALUATION.md) | P6 five-seed activation comparison. |
|
|
127
|
+
| [Activation timing](docs/ACTIVATION_BENCHMARK.md) | P1.04 CPU timing evidence and limitations. |
|
|
128
|
+
| [PSANN reuse assessment](docs/PSANN_REUSE.md) | Selective adaptation provenance; no PSANN runtime dependency. |
|
|
129
|
+
| [Roadmap](docs/ROADMAP.md) | Completed work, release evidence, and deferred scope. |
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "exponet"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Trainable blend of ReLU and squared ReLU activations for PyTorch"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{name = "ExpoNet Contributors"}]
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
|
+
dependencies = ["torch>=2.0.0", "numpy>=1.24.0", "scikit-learn>=1.3.0"]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = ["pytest>=7.0.0", "ruff>=0.1.0"]
|
|
18
|
+
|
|
19
|
+
[tool.setuptools]
|
|
20
|
+
packages = ["exponet"]
|
|
21
|
+
package-dir = {"" = "src"}
|
|
22
|
+
|
|
23
|
+
[tool.pytest.ini_options]
|
|
24
|
+
testpaths = ["tests"]
|
|
25
|
+
python_files = ["test_*.py"]
|
|
26
|
+
python_functions = ["test_*"]
|
|
27
|
+
|
|
28
|
+
[tool.ruff]
|
|
29
|
+
line-length = 88
|
|
30
|
+
target-version = "py311"
|
|
31
|
+
|
|
32
|
+
[tool.ruff.lint]
|
|
33
|
+
select = ["E", "F", "W", "I"]
|
exponet-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""ExpoNet: Trainable blend of ReLU and squared ReLU activations."""
|
|
2
|
+
|
|
3
|
+
from exponet.activations import ExpoActivation as ExpoActivation
|
|
4
|
+
from exponet.estimators import ExpoClassifier as ExpoClassifier
|
|
5
|
+
from exponet.estimators import ExpoRegressor as ExpoRegressor
|
|
6
|
+
from exponet.nn import ExpoMLP as ExpoMLP
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Restricted, versioned inference snapshots for ExpoNet estimators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import sklearn
|
|
12
|
+
import torch
|
|
13
|
+
from sklearn.preprocessing import StandardScaler
|
|
14
|
+
|
|
15
|
+
FORMAT_VERSION = 1
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _cpu_state_dict(model: torch.nn.Module) -> dict[str, torch.Tensor]:
|
|
19
|
+
"""Copy state tensors to CPU without changing the live model's device."""
|
|
20
|
+
return {
|
|
21
|
+
name: value.detach().cpu().clone() for name, value in model.state_dict().items()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _scaler_payload(scaler: StandardScaler | None) -> dict[str, object] | None:
|
|
26
|
+
if scaler is None:
|
|
27
|
+
return None
|
|
28
|
+
return {
|
|
29
|
+
"mean": torch.as_tensor(scaler.mean_, dtype=torch.float64).clone(),
|
|
30
|
+
"scale": torch.as_tensor(scaler.scale_, dtype=torch.float64).clone(),
|
|
31
|
+
"var": torch.as_tensor(scaler.var_, dtype=torch.float64).clone(),
|
|
32
|
+
"n_features_in": int(scaler.n_features_in_),
|
|
33
|
+
"n_samples_seen": int(np.asarray(scaler.n_samples_seen_).item()),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _primitive_constructor(params: dict[str, object]) -> dict[str, object]:
|
|
38
|
+
result = dict(params)
|
|
39
|
+
result["hidden_dims"] = list(result["hidden_dims"])
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def save_snapshot(estimator: Any, *, kind: str, path: str | Path) -> None:
|
|
44
|
+
"""Atomically write a restricted inference snapshot for a fitted estimator."""
|
|
45
|
+
destination = Path(path)
|
|
46
|
+
if not destination.parent.exists():
|
|
47
|
+
raise ValueError(f"snapshot directory does not exist: {destination.parent}")
|
|
48
|
+
if not destination.parent.is_dir():
|
|
49
|
+
raise ValueError(f"snapshot parent is not a directory: {destination.parent}")
|
|
50
|
+
payload: dict[str, object] = {
|
|
51
|
+
"format_version": FORMAT_VERSION,
|
|
52
|
+
"estimator_kind": kind,
|
|
53
|
+
"constructor": _primitive_constructor(estimator.get_params(deep=False)),
|
|
54
|
+
"model_state": _cpu_state_dict(estimator.model_),
|
|
55
|
+
"fitted": {
|
|
56
|
+
"n_features_in": int(estimator.n_features_in_),
|
|
57
|
+
"n_iter": int(estimator.n_iter_),
|
|
58
|
+
"best_epoch": estimator.best_epoch_,
|
|
59
|
+
"history": estimator.history_,
|
|
60
|
+
"feature_scaler": _scaler_payload(estimator.feature_scaler_),
|
|
61
|
+
},
|
|
62
|
+
"producer": {
|
|
63
|
+
"exponet": "0.1.0",
|
|
64
|
+
"torch": str(torch.__version__),
|
|
65
|
+
"numpy": np.__version__,
|
|
66
|
+
"scikit_learn": sklearn.__version__,
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
fitted = payload["fitted"]
|
|
70
|
+
assert isinstance(fitted, dict)
|
|
71
|
+
if kind == "regressor":
|
|
72
|
+
fitted.update(
|
|
73
|
+
{
|
|
74
|
+
"n_outputs": int(estimator.n_outputs_),
|
|
75
|
+
"target_was_1d": bool(estimator._target_was_1d),
|
|
76
|
+
"target_scaler": _scaler_payload(estimator.target_scaler_),
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
elif kind == "classifier":
|
|
80
|
+
fitted.update(
|
|
81
|
+
{
|
|
82
|
+
"n_classes": int(estimator.n_classes_),
|
|
83
|
+
"classes": estimator.classes_.tolist(),
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
raise ValueError(f"unsupported estimator kind: {kind!r}")
|
|
88
|
+
|
|
89
|
+
temporary_name: str | None = None
|
|
90
|
+
try:
|
|
91
|
+
with tempfile.NamedTemporaryFile(
|
|
92
|
+
mode="wb",
|
|
93
|
+
dir=destination.parent,
|
|
94
|
+
prefix=f".{destination.name}.",
|
|
95
|
+
delete=False,
|
|
96
|
+
) as temporary:
|
|
97
|
+
temporary_name = temporary.name
|
|
98
|
+
torch.save(payload, temporary_name)
|
|
99
|
+
os.replace(temporary_name, destination)
|
|
100
|
+
temporary_name = None
|
|
101
|
+
finally:
|
|
102
|
+
if temporary_name is not None:
|
|
103
|
+
Path(temporary_name).unlink(missing_ok=True)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def load_snapshot(path: str | Path, *, expected_kind: str) -> dict[str, object]:
|
|
107
|
+
"""Read and validate only the outer restricted snapshot envelope."""
|
|
108
|
+
try:
|
|
109
|
+
payload = torch.load(path, map_location="cpu", weights_only=True)
|
|
110
|
+
except Exception as error:
|
|
111
|
+
raise ValueError("invalid or unreadable ExpoNet snapshot") from error
|
|
112
|
+
if not isinstance(payload, dict):
|
|
113
|
+
raise ValueError("snapshot must contain a dictionary payload")
|
|
114
|
+
if payload.get("format_version") != FORMAT_VERSION:
|
|
115
|
+
raise ValueError("unsupported ExpoNet snapshot format version")
|
|
116
|
+
if payload.get("estimator_kind") != expected_kind:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"snapshot is for {payload.get('estimator_kind')!r}, not {expected_kind!r}"
|
|
119
|
+
)
|
|
120
|
+
if not isinstance(payload.get("constructor"), dict):
|
|
121
|
+
raise ValueError("snapshot constructor metadata is invalid")
|
|
122
|
+
if not isinstance(payload.get("fitted"), dict):
|
|
123
|
+
raise ValueError("snapshot fitted metadata is invalid")
|
|
124
|
+
if not isinstance(payload.get("model_state"), dict) or not all(
|
|
125
|
+
isinstance(name, str) and isinstance(value, torch.Tensor)
|
|
126
|
+
for name, value in payload["model_state"].items()
|
|
127
|
+
):
|
|
128
|
+
raise ValueError("snapshot model state is invalid")
|
|
129
|
+
return payload
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def restore_scaler(
|
|
133
|
+
payload: object, *, expected_features: int, name: str
|
|
134
|
+
) -> StandardScaler | None:
|
|
135
|
+
"""Rebuild a fitted StandardScaler from finite tensor-only metadata."""
|
|
136
|
+
if payload is None:
|
|
137
|
+
return None
|
|
138
|
+
if not isinstance(payload, dict):
|
|
139
|
+
raise ValueError(f"snapshot {name} metadata is invalid")
|
|
140
|
+
try:
|
|
141
|
+
count = payload["n_features_in"]
|
|
142
|
+
seen = payload["n_samples_seen"]
|
|
143
|
+
mean = payload["mean"]
|
|
144
|
+
scale = payload["scale"]
|
|
145
|
+
var = payload["var"]
|
|
146
|
+
except KeyError as error:
|
|
147
|
+
raise ValueError(f"snapshot {name} metadata is incomplete") from error
|
|
148
|
+
if (
|
|
149
|
+
not isinstance(count, int)
|
|
150
|
+
or isinstance(count, bool)
|
|
151
|
+
or count != expected_features
|
|
152
|
+
or not isinstance(seen, int)
|
|
153
|
+
or isinstance(seen, bool)
|
|
154
|
+
or seen <= 0
|
|
155
|
+
or not all(isinstance(value, torch.Tensor) for value in (mean, scale, var))
|
|
156
|
+
):
|
|
157
|
+
raise ValueError(f"snapshot {name} metadata is invalid")
|
|
158
|
+
arrays = [
|
|
159
|
+
value.detach().cpu().numpy().astype(np.float64, copy=True)
|
|
160
|
+
for value in (mean, scale, var)
|
|
161
|
+
]
|
|
162
|
+
if any(array.shape != (expected_features,) for array in arrays) or not all(
|
|
163
|
+
np.isfinite(array).all() for array in arrays
|
|
164
|
+
):
|
|
165
|
+
raise ValueError(f"snapshot {name} arrays are invalid")
|
|
166
|
+
if np.any(arrays[1] <= 0) or np.any(arrays[2] < 0):
|
|
167
|
+
raise ValueError(f"snapshot {name} arrays are invalid")
|
|
168
|
+
scaler = StandardScaler()
|
|
169
|
+
scaler.mean_, scaler.scale_, scaler.var_ = arrays
|
|
170
|
+
scaler.n_features_in_ = expected_features
|
|
171
|
+
scaler.n_samples_seen_ = seen
|
|
172
|
+
return scaler
|