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 ADDED
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ .pytest_cache/
4
+ dist/
5
+ build/
6
+ *.Rcheck/
7
+ *.tar.gz
8
+ .Rproj.user/
9
+ .Rhistory
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