kasn 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.
- kasn-0.1.0/.gitignore +9 -0
- kasn-0.1.0/LICENSE +21 -0
- kasn-0.1.0/PKG-INFO +77 -0
- kasn-0.1.0/README.md +48 -0
- kasn-0.1.0/examples/nonparametric_regression.py +128 -0
- kasn-0.1.0/pyproject.toml +51 -0
- kasn-0.1.0/src/kasn/__init__.py +49 -0
- kasn-0.1.0/src/kasn/_utils.py +55 -0
- kasn-0.1.0/src/kasn/base.py +595 -0
- kasn-0.1.0/src/kasn/classifier.py +130 -0
- kasn-0.1.0/src/kasn/datasets.py +141 -0
- kasn-0.1.0/src/kasn/losses.py +109 -0
- kasn-0.1.0/src/kasn/network.py +272 -0
- kasn-0.1.0/src/kasn/preprocessing.py +61 -0
- kasn-0.1.0/src/kasn/regressor.py +145 -0
- kasn-0.1.0/src/kasn/splines.py +99 -0
- kasn-0.1.0/src/kasn/theory.py +88 -0
- kasn-0.1.0/src/kasn/tuning.py +163 -0
- kasn-0.1.0/tests/test_kasn.py +269 -0
kasn-0.1.0/.gitignore
ADDED
kasn-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sami Abdurahman
|
|
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.
|
kasn-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: kasn
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Kolmogorov-Arnold Sieve Networks for nonparametric estimation and semiparametric inference
|
|
5
|
+
Project-URL: Repository, https://github.com/samiabd8/kasn
|
|
6
|
+
Project-URL: Issues, https://github.com/samiabd8/kasn/issues
|
|
7
|
+
Author: Sami Abdurahman
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: Kolmogorov-Arnold networks,econometrics,group lasso,nonparametric regression,quantile regression,sieve estimation,time series
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: numpy>=1.22
|
|
20
|
+
Requires-Dist: scikit-learn>=1.2
|
|
21
|
+
Requires-Dist: scipy>=1.8
|
|
22
|
+
Requires-Dist: torch>=2.3
|
|
23
|
+
Provides-Extra: examples
|
|
24
|
+
Requires-Dist: pandas>=1.5; extra == 'examples'
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pandas>=1.5; extra == 'test'
|
|
27
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# kasn
|
|
31
|
+
|
|
32
|
+
Kolmogorov-Arnold Sieve Networks. PyTorch backend, scikit-learn API.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install kasn
|
|
38
|
+
pip install -e "python[test]" && pytest python/tests # development
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from kasn import KASNRegressor, KASNClassifier, make_kasn_regression, load
|
|
45
|
+
|
|
46
|
+
sim = make_kasn_regression(n=2000, d=50, s=10, target="compositional", random_state=0)
|
|
47
|
+
X, y = sim.X, sim.y
|
|
48
|
+
m = KASNRegressor(depth=3, random_state=0).fit(X[:1000], y[:1000], X[1000:1500], y[1000:1500])
|
|
49
|
+
m.predict(X[1500:]); m.sparsity_report(); m.grid_coverage(X[1500:])
|
|
50
|
+
m.save("fit.kasn"); m = load("fit.kasn")
|
|
51
|
+
|
|
52
|
+
KASNRegressor(loss="quantile", quantile=0.9)
|
|
53
|
+
KASNRegressor(loss="huber", huber_delta=1.0)
|
|
54
|
+
KASNRegressor(loss="poisson")
|
|
55
|
+
KASNClassifier() # binary logistic
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`tune_kasn` runs the prune-aware greedy search. `examples/nonparametric_regression.py` reproduces the layout of Tables 1-4.
|
|
59
|
+
|
|
60
|
+
## Parameters
|
|
61
|
+
|
|
62
|
+
| Parameter | Definition | Default |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `gamma`, `grid_size` | `G_n = max(5, floor(n^gamma))` | `gamma = 0.4` |
|
|
65
|
+
| `depth` | `L_n` hidden layers, `L_n + 1` edge layers | `max(3, floor(log n))` |
|
|
66
|
+
| `width`, `width_scale` | `W_n = C_W floor(log n)` | `C_W = 5` |
|
|
67
|
+
| | `r_n = (d + 1) W_n + (L_n - 1) W_n^2` | |
|
|
68
|
+
| `spline_order` | de Boor order `k` | 4 |
|
|
69
|
+
| `grid_range` | `I_eps` | `(-0.05, 1.05)` |
|
|
70
|
+
| `penalty` | `lambda_n` | `C_lambda sqrt(log(L_n W_n^2) / n)` |
|
|
71
|
+
| `penalty_scale` | `C_lambda` | 0.05 |
|
|
72
|
+
| `delta_mode` | `'penalty'`, `'project'`, `'dual'` | `'penalty'` |
|
|
73
|
+
| `zeta_delta`, `delta_scale` | `Delta_n = max(5, delta_scale n^zeta_delta)` | 0.4, 1.0 |
|
|
74
|
+
| `max_active_edges` | `s_n` cap | none |
|
|
75
|
+
| `max_epochs`, `patience` | | 500, 50 |
|
|
76
|
+
|
|
77
|
+
`n` is the training fold size.
|
kasn-0.1.0/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# kasn
|
|
2
|
+
|
|
3
|
+
Kolmogorov-Arnold Sieve Networks. PyTorch backend, scikit-learn API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install kasn
|
|
9
|
+
pip install -e "python[test]" && pytest python/tests # development
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from kasn import KASNRegressor, KASNClassifier, make_kasn_regression, load
|
|
16
|
+
|
|
17
|
+
sim = make_kasn_regression(n=2000, d=50, s=10, target="compositional", random_state=0)
|
|
18
|
+
X, y = sim.X, sim.y
|
|
19
|
+
m = KASNRegressor(depth=3, random_state=0).fit(X[:1000], y[:1000], X[1000:1500], y[1000:1500])
|
|
20
|
+
m.predict(X[1500:]); m.sparsity_report(); m.grid_coverage(X[1500:])
|
|
21
|
+
m.save("fit.kasn"); m = load("fit.kasn")
|
|
22
|
+
|
|
23
|
+
KASNRegressor(loss="quantile", quantile=0.9)
|
|
24
|
+
KASNRegressor(loss="huber", huber_delta=1.0)
|
|
25
|
+
KASNRegressor(loss="poisson")
|
|
26
|
+
KASNClassifier() # binary logistic
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`tune_kasn` runs the prune-aware greedy search. `examples/nonparametric_regression.py` reproduces the layout of Tables 1-4.
|
|
30
|
+
|
|
31
|
+
## Parameters
|
|
32
|
+
|
|
33
|
+
| Parameter | Definition | Default |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `gamma`, `grid_size` | `G_n = max(5, floor(n^gamma))` | `gamma = 0.4` |
|
|
36
|
+
| `depth` | `L_n` hidden layers, `L_n + 1` edge layers | `max(3, floor(log n))` |
|
|
37
|
+
| `width`, `width_scale` | `W_n = C_W floor(log n)` | `C_W = 5` |
|
|
38
|
+
| | `r_n = (d + 1) W_n + (L_n - 1) W_n^2` | |
|
|
39
|
+
| `spline_order` | de Boor order `k` | 4 |
|
|
40
|
+
| `grid_range` | `I_eps` | `(-0.05, 1.05)` |
|
|
41
|
+
| `penalty` | `lambda_n` | `C_lambda sqrt(log(L_n W_n^2) / n)` |
|
|
42
|
+
| `penalty_scale` | `C_lambda` | 0.05 |
|
|
43
|
+
| `delta_mode` | `'penalty'`, `'project'`, `'dual'` | `'penalty'` |
|
|
44
|
+
| `zeta_delta`, `delta_scale` | `Delta_n = max(5, delta_scale n^zeta_delta)` | 0.4, 1.0 |
|
|
45
|
+
| `max_active_edges` | `s_n` cap | none |
|
|
46
|
+
| `max_epochs`, `patience` | | 500, 50 |
|
|
47
|
+
|
|
48
|
+
`n` is the training fold size.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Local nonparametric regression example (Section 4.1 of the KASN paper, scaled down).
|
|
2
|
+
|
|
3
|
+
Simulates the additive or compositional design, splits it chronologically
|
|
4
|
+
into train / validation / test (0.5 / 0.25 / 0.25), fits the KASN and a
|
|
5
|
+
validation-tuned Lasso, and reports the columns of Tables 1-4: RMSE and R^2
|
|
6
|
+
on each fold, the held-out L2 error against f_0, and the active edge count.
|
|
7
|
+
|
|
8
|
+
Runs on a laptop CPU in under a minute. ``--tune`` runs the prune-aware
|
|
9
|
+
hyperparameter search. Paper scale: ``--n 5000 --d 100 --s 30``.
|
|
10
|
+
|
|
11
|
+
python examples/nonparametric_regression.py --target compositional
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
from sklearn.linear_model import Lasso
|
|
21
|
+
from sklearn.metrics import mean_squared_error, r2_score
|
|
22
|
+
|
|
23
|
+
from kasn import (EmpiricalCDFTransformer, KASNRegressor, check_conditions,
|
|
24
|
+
make_kasn_regression, target_function, tune_kasn)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_args() -> argparse.Namespace:
|
|
28
|
+
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
29
|
+
p.add_argument("--target", default="compositional",
|
|
30
|
+
choices=["additive", "compositional", "deep"])
|
|
31
|
+
p.add_argument("--n", type=int, default=2000)
|
|
32
|
+
p.add_argument("--d", type=int, default=50)
|
|
33
|
+
p.add_argument("--s", type=int, default=10)
|
|
34
|
+
p.add_argument("--rho", type=float, default=0.7)
|
|
35
|
+
p.add_argument("--gamma", type=float, default=0.4)
|
|
36
|
+
p.add_argument("--depth", type=int, default=3)
|
|
37
|
+
p.add_argument("--epochs", type=int, default=500)
|
|
38
|
+
p.add_argument("--patience", type=int, default=50)
|
|
39
|
+
p.add_argument("--tune", action="store_true")
|
|
40
|
+
p.add_argument("--seed", type=int, default=0)
|
|
41
|
+
p.add_argument("--device", default="auto")
|
|
42
|
+
args, _ = p.parse_known_args() # Jupyter-safe
|
|
43
|
+
return args
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def metrics(name, pred, y, f0, idx, extra):
|
|
47
|
+
row = {"method": name}
|
|
48
|
+
for fold in ("train", "val", "test"):
|
|
49
|
+
i = idx[fold]
|
|
50
|
+
row[f"{fold}_rmse"] = float(np.sqrt(mean_squared_error(y[i], pred[i])))
|
|
51
|
+
row[f"{fold}_r2"] = float(r2_score(y[i], pred[i]))
|
|
52
|
+
i = idx["test"]
|
|
53
|
+
row["l2_error"] = float(np.sqrt(np.mean((pred[i] - f0[i]) ** 2)))
|
|
54
|
+
row.update(extra)
|
|
55
|
+
return row
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main() -> None:
|
|
59
|
+
a = parse_args()
|
|
60
|
+
check_conditions(a.gamma, m=2)
|
|
61
|
+
data = make_kasn_regression(n=a.n, d=a.d, s=a.s, target=a.target,
|
|
62
|
+
rho=a.rho, random_state=a.seed)
|
|
63
|
+
X, y, f0 = data.X, data.y, data.f0
|
|
64
|
+
n_tr, n_va = int(0.5 * a.n), int(0.25 * a.n)
|
|
65
|
+
idx = {"train": np.arange(n_tr), "val": np.arange(n_tr, n_tr + n_va),
|
|
66
|
+
"test": np.arange(n_tr + n_va, a.n)}
|
|
67
|
+
Xtr, ytr = X[idx["train"]], y[idx["train"]]
|
|
68
|
+
Xva, yva = X[idx["val"]], y[idx["val"]]
|
|
69
|
+
print(f"Design: target={a.target} n={a.n} d={a.d} S={a.s} rho_X={a.rho} "
|
|
70
|
+
f"(chronological split {n_tr}/{n_va}/{a.n - n_tr - n_va})")
|
|
71
|
+
|
|
72
|
+
params = dict(gamma=a.gamma, depth=a.depth, max_epochs=a.epochs,
|
|
73
|
+
patience=a.patience, device=a.device, random_state=a.seed)
|
|
74
|
+
if a.tune:
|
|
75
|
+
res = tune_kasn(Xtr, ytr, Xva, yva, base_params=params,
|
|
76
|
+
penalty_scale_grid=(0.05, 0.1, 1.0),
|
|
77
|
+
gamma_grid=(0.3, 0.4), width_scale_grid=(3, 5),
|
|
78
|
+
batch_size_grid=(32,), tuning_epochs=40,
|
|
79
|
+
random_state=a.seed)
|
|
80
|
+
params = res.best_params
|
|
81
|
+
print(f"Tuned in {res.selection_time:.0f}s: gamma={params['gamma']} "
|
|
82
|
+
f"penalty_scale={params['penalty_scale']} "
|
|
83
|
+
f"width_scale={params['width_scale']}")
|
|
84
|
+
|
|
85
|
+
rows = []
|
|
86
|
+
t0 = time.time()
|
|
87
|
+
kasn = KASNRegressor(**params, verbose=1).fit(Xtr, ytr, Xva, yva)
|
|
88
|
+
rep = kasn.sparsity_report()
|
|
89
|
+
rows.append(metrics("KASN", kasn.predict(X), y, f0, idx, {
|
|
90
|
+
"parameters": f"{rep['active_edges']:,}/{rep['r_n']:,}",
|
|
91
|
+
"fit_s": time.time() - t0}))
|
|
92
|
+
|
|
93
|
+
t0 = time.time()
|
|
94
|
+
cdf = EmpiricalCDFTransformer().fit(Xtr)
|
|
95
|
+
best = min((Lasso(alpha=al, max_iter=10_000).fit(cdf.transform(Xtr), ytr)
|
|
96
|
+
for al in (1e-5, 1e-3, 1e-2, 5e-2, 1e-1, 1.0)),
|
|
97
|
+
key=lambda m: mean_squared_error(yva, m.predict(cdf.transform(Xva))))
|
|
98
|
+
nz = int(np.sum(np.abs(best.coef_) > 1e-6))
|
|
99
|
+
rows.append(metrics("LASSO", best.predict(cdf.transform(X)), y, f0, idx,
|
|
100
|
+
{"parameters": f"{nz}/{a.d}", "fit_s": time.time() - t0}))
|
|
101
|
+
|
|
102
|
+
oracle = target_function(X, a.s, a.target)
|
|
103
|
+
rows.append(metrics("Oracle", oracle, y, f0, idx,
|
|
104
|
+
{"parameters": "-", "fit_s": 0.0}))
|
|
105
|
+
|
|
106
|
+
print(f"\n{'method':8s} {'trRMSE':>7s} {'trR2':>6s} {'vaRMSE':>7s} "
|
|
107
|
+
f"{'vaR2':>6s} {'teRMSE':>7s} {'teR2':>6s} {'||f-f0||':>8s} "
|
|
108
|
+
f"{'parameters':>14s} {'fit(s)':>7s}")
|
|
109
|
+
for r in rows:
|
|
110
|
+
print(f"{r['method']:8s} {r['train_rmse']:7.4f} {r['train_r2']:6.4f} "
|
|
111
|
+
f"{r['val_rmse']:7.4f} {r['val_r2']:6.4f} {r['test_rmse']:7.4f} "
|
|
112
|
+
f"{r['test_r2']:6.4f} {r['l2_error']:8.4f} {r['parameters']:>14s} "
|
|
113
|
+
f"{r['fit_s']:7.1f}")
|
|
114
|
+
|
|
115
|
+
print(f"\nKASN: sparsity ratio {rep['sparsity_ratio']:.3f}, "
|
|
116
|
+
f"output-connected edges {rep['output_connected_edges']:,}, "
|
|
117
|
+
f"Delta_n {rep['delta_n']:.2f} ({rep['delta_mode']}), "
|
|
118
|
+
f"lambda_n {rep['lambda_n']:.2e}, "
|
|
119
|
+
f"L_n={rep['L_n']} W_n={rep['W_n']} G_n={rep['G_n']}")
|
|
120
|
+
print("Grid coverage on the test fold (share of layer inputs outside I_eps):")
|
|
121
|
+
for c in kasn.grid_coverage(X[idx["test"]]):
|
|
122
|
+
print(f" layer {c['layer']}: range [{c['min']:+.3f}, {c['max']:+.3f}]"
|
|
123
|
+
f" outside grid {c['frac_outside_grid_range']:.4f}"
|
|
124
|
+
f" outside support {c['frac_outside_support']:.4f}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
main()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kasn"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Kolmogorov-Arnold Sieve Networks for nonparametric estimation and semiparametric inference"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
authors = [{ name = "Sami Abdurahman" }]
|
|
14
|
+
keywords = ["sieve estimation", "Kolmogorov-Arnold networks", "group lasso",
|
|
15
|
+
"nonparametric regression", "quantile regression", "econometrics",
|
|
16
|
+
"time series"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"numpy>=1.22",
|
|
28
|
+
"scipy>=1.8",
|
|
29
|
+
"scikit-learn>=1.2",
|
|
30
|
+
"torch>=2.3",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
examples = ["pandas>=1.5"]
|
|
35
|
+
test = ["pytest>=7", "pandas>=1.5"]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Repository = "https://github.com/samiabd8/kasn"
|
|
39
|
+
Issues = "https://github.com/samiabd8/kasn/issues"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.version]
|
|
42
|
+
path = "src/kasn/__init__.py"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/kasn"]
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.sdist]
|
|
48
|
+
include = ["src", "tests", "examples", "README.md", "LICENSE", "pyproject.toml"]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Kolmogorov-Arnold Sieve Networks (KASN).
|
|
2
|
+
|
|
3
|
+
Sieve extremum estimation with deep Kolmogorov-Arnold networks, learned
|
|
4
|
+
B-spline edges and Group Lasso regularisation.
|
|
5
|
+
|
|
6
|
+
KASNRegressor
|
|
7
|
+
Squared, Huber, quantile and Poisson criteria.
|
|
8
|
+
KASNClassifier
|
|
9
|
+
Binary logistic criterion.
|
|
10
|
+
tune_kasn
|
|
11
|
+
Prune-aware greedy hyperparameter search on a validation fold.
|
|
12
|
+
make_kasn_regression
|
|
13
|
+
Monte Carlo designs of Section 4.
|
|
14
|
+
load
|
|
15
|
+
Restore any estimator written by ``.save()``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from .base import _read_payload
|
|
21
|
+
from .classifier import KASNClassifier
|
|
22
|
+
from .datasets import make_kasn_regression, target_function
|
|
23
|
+
from .losses import make_loss
|
|
24
|
+
from .network import KASNLayer, KASNNetwork
|
|
25
|
+
from .preprocessing import EmpiricalCDFTransformer
|
|
26
|
+
from .regressor import KASNRegressor
|
|
27
|
+
from .splines import BSplineBasis
|
|
28
|
+
from .theory import architecture, check_conditions
|
|
29
|
+
from .tuning import TuningResult, tune_kasn
|
|
30
|
+
|
|
31
|
+
__version__ = "0.1.0"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load(path, device: Optional[str] = None):
|
|
35
|
+
"""Load a ``KASNRegressor`` or ``KASNClassifier`` written by ``.save()``."""
|
|
36
|
+
payload = _read_payload(path)
|
|
37
|
+
classes = {"KASNRegressor": KASNRegressor,
|
|
38
|
+
"KASNClassifier": KASNClassifier}
|
|
39
|
+
if payload["class"] not in classes:
|
|
40
|
+
raise ValueError(f"unknown estimator class {payload['class']!r}")
|
|
41
|
+
return classes[payload["class"]]._from_payload(payload, device)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"KASNRegressor", "KASNClassifier", "load", "tune_kasn", "TuningResult",
|
|
46
|
+
"make_kasn_regression", "target_function", "make_loss",
|
|
47
|
+
"EmpiricalCDFTransformer", "BSplineBasis", "KASNLayer", "KASNNetwork",
|
|
48
|
+
"architecture", "check_conditions", "__version__",
|
|
49
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Small internal helpers: devices, seeding and serialisation hygiene."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
from typing import Any, Iterator, Optional, Union
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import torch
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def resolve_device(device: Union[str, torch.device, None]) -> torch.device:
|
|
13
|
+
"""``'auto'`` (or ``None``) picks CUDA when available, otherwise CPU."""
|
|
14
|
+
if device is None or device == "auto":
|
|
15
|
+
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
16
|
+
return torch.device(device)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@contextlib.contextmanager
|
|
20
|
+
def seeded(random_state: Optional[int],
|
|
21
|
+
device: torch.device) -> Iterator[None]:
|
|
22
|
+
"""Seed torch inside a forked RNG so the caller's global state is untouched.
|
|
23
|
+
|
|
24
|
+
Every candidate in a hyperparameter search therefore starts from the same
|
|
25
|
+
initialisation, and repeated fits do not consume each other's draws.
|
|
26
|
+
"""
|
|
27
|
+
if random_state is None:
|
|
28
|
+
yield
|
|
29
|
+
return
|
|
30
|
+
devices = []
|
|
31
|
+
if device.type == "cuda":
|
|
32
|
+
devices = [device.index if device.index is not None
|
|
33
|
+
else torch.cuda.current_device()]
|
|
34
|
+
with torch.random.fork_rng(devices=devices):
|
|
35
|
+
torch.manual_seed(int(random_state))
|
|
36
|
+
yield
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def plain(obj: Any) -> Any:
|
|
40
|
+
"""Recursively convert numpy scalars and arrays to built-in Python types.
|
|
41
|
+
|
|
42
|
+
``torch.load(weights_only=True)`` accepts only built-in containers and
|
|
43
|
+
tensors, so everything saved alongside the state dict passes through here.
|
|
44
|
+
"""
|
|
45
|
+
if isinstance(obj, dict):
|
|
46
|
+
return {str(k): plain(v) for k, v in obj.items()}
|
|
47
|
+
if isinstance(obj, (list, tuple)):
|
|
48
|
+
return type(obj)(plain(v) for v in obj)
|
|
49
|
+
if isinstance(obj, np.generic):
|
|
50
|
+
return obj.item()
|
|
51
|
+
if isinstance(obj, np.ndarray):
|
|
52
|
+
return obj.tolist()
|
|
53
|
+
if isinstance(obj, torch.device):
|
|
54
|
+
return str(obj)
|
|
55
|
+
return obj
|