optimized-svm 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.
Files changed (37) hide show
  1. optimized_svm-0.1.0/.github/workflows/ci.yml +44 -0
  2. optimized_svm-0.1.0/.github/workflows/release.yml +32 -0
  3. optimized_svm-0.1.0/.gitignore +11 -0
  4. optimized_svm-0.1.0/CHANGELOG.md +23 -0
  5. optimized_svm-0.1.0/LICENSE +28 -0
  6. optimized_svm-0.1.0/PKG-INFO +131 -0
  7. optimized_svm-0.1.0/README.md +108 -0
  8. optimized_svm-0.1.0/benchmarks/thesis_regression.py +121 -0
  9. optimized_svm-0.1.0/docs/api.md +41 -0
  10. optimized_svm-0.1.0/docs/changelog.md +23 -0
  11. optimized_svm-0.1.0/docs/getting-started.md +71 -0
  12. optimized_svm-0.1.0/docs/index.md +108 -0
  13. optimized_svm-0.1.0/docs/javascripts/katex.js +10 -0
  14. optimized_svm-0.1.0/docs/reproduction.md +42 -0
  15. optimized_svm-0.1.0/docs/theory/hypergradient.md +57 -0
  16. optimized_svm-0.1.0/docs/theory/training.md +56 -0
  17. optimized_svm-0.1.0/examples/ard_feature_relevance.py +28 -0
  18. optimized_svm-0.1.0/examples/low_level_api.py +25 -0
  19. optimized_svm-0.1.0/examples/quickstart.py +35 -0
  20. optimized_svm-0.1.0/mkdocs.yml +59 -0
  21. optimized_svm-0.1.0/pyproject.toml +52 -0
  22. optimized_svm-0.1.0/src/optimized_svm/__init__.py +38 -0
  23. optimized_svm-0.1.0/src/optimized_svm/estimators.py +735 -0
  24. optimized_svm-0.1.0/src/optimized_svm/hypergradient.py +153 -0
  25. optimized_svm-0.1.0/src/optimized_svm/kernels.py +63 -0
  26. optimized_svm-0.1.0/src/optimized_svm/linalg.py +24 -0
  27. optimized_svm-0.1.0/src/optimized_svm/losses.py +176 -0
  28. optimized_svm-0.1.0/src/optimized_svm/problem.py +99 -0
  29. optimized_svm-0.1.0/src/optimized_svm/smooth_max.py +90 -0
  30. optimized_svm-0.1.0/src/optimized_svm/training.py +258 -0
  31. optimized_svm-0.1.0/src/optimized_svm/validation.py +76 -0
  32. optimized_svm-0.1.0/tests/conftest.py +13 -0
  33. optimized_svm-0.1.0/tests/test_estimators.py +132 -0
  34. optimized_svm-0.1.0/tests/test_hypergradient.py +93 -0
  35. optimized_svm-0.1.0/tests/test_smooth_max_and_losses.py +86 -0
  36. optimized_svm-0.1.0/tests/test_training.py +78 -0
  37. optimized_svm-0.1.0/uv.lock +3698 -0
@@ -0,0 +1,44 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python: ["3.10", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v6
18
+ with:
19
+ python-version: ${{ matrix.python }}
20
+ - name: Install
21
+ run: |
22
+ uv venv
23
+ uv pip install -e . pytest cvxpy ruff
24
+ - run: uv run --no-sync ruff check src tests examples benchmarks
25
+ - run: uv run --no-sync pytest -q
26
+
27
+ docs:
28
+ runs-on: ubuntu-latest
29
+ permissions:
30
+ contents: write
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ - uses: astral-sh/setup-uv@v6
34
+ - name: Build documentation
35
+ run: |
36
+ uv venv
37
+ uv pip install -e . mkdocs-material "mkdocstrings[python]"
38
+ uv run --no-sync mkdocs build --strict
39
+ - name: Deploy to GitHub Pages
40
+ if: github.ref == 'refs/heads/main'
41
+ uses: peaceiris/actions-gh-pages@v4
42
+ with:
43
+ github_token: ${{ secrets.GITHUB_TOKEN }}
44
+ publish_dir: ./site
@@ -0,0 +1,32 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v6
14
+ - run: uv build
15
+ - uses: actions/upload-artifact@v4
16
+ with:
17
+ name: dist
18
+ path: dist
19
+
20
+ publish:
21
+ # requires a PyPI "trusted publisher" configured for this repository
22
+ needs: build
23
+ runs-on: ubuntu-latest
24
+ environment: pypi
25
+ permissions:
26
+ id-token: write
27
+ steps:
28
+ - uses: actions/download-artifact@v4
29
+ with:
30
+ name: dist
31
+ path: dist
32
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .coverage
8
+ htmlcov/
9
+ site/
10
+ .ruff_cache/
11
+ .mypy_cache/
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 – 2026-10-28
4
+
5
+ Complete overhaul of the 2022 code (tag `legacy-2022`).
6
+
7
+ ### Changed
8
+ - training backend: Rust solver `smo-solver` instead of the Nim package `nimsmo`
9
+ - new package layout (`src/`), `pyproject.toml`, snake_case API, scikit-learn compatible
10
+ estimators `SmoothSVC`, `SmoothSVR`, `OptimizedSVC`, `OptimizedSVR`
11
+ - adjoint formulation of the hypergradient (one linear system per fold)
12
+
13
+ ### Added
14
+ - one-vs-rest multi-class classification, class and sample weights
15
+ - warm starts across hyperparameter evaluations, rough SMO tolerance before Newton
16
+ - `decision_gradient()` (replaces the unfinished `estimate_sensitivity`)
17
+ - test suite (finite-difference checks of all gradients, scikit-learn estimator checks),
18
+ documentation, examples and a reproduction script for thesis Table 10.4.1
19
+
20
+ ### Fixed
21
+ - see `LEGACY_BUGS.md` in the project workspace (P1–P13), notably: regression HPO with
22
+ `maxAsum` did not terminate, SMO and Newton used different kernel parameters, the 1-norm
23
+ constraint was always treated as active
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2022-2026, Nico Strasdat
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.5
2
+ Name: optimized-svm
3
+ Version: 0.1.0
4
+ Summary: Support vector machines with hyperparameters optimized by exact gradients (implicit function theorem)
5
+ Project-URL: Homepage, https://wotzlaff.github.io/svm-site/
6
+ Project-URL: Documentation, https://wotzlaff.github.io/optimized-svm/
7
+ Project-URL: Repository, https://github.com/wotzlaff/optimized-svm
8
+ Author-email: Nico Strasdat <nstrasdat@gmail.com>
9
+ License-Expression: BSD-3-Clause
10
+ License-File: LICENSE
11
+ Keywords: bilevel optimization,hyperparameter optimization,kernel methods,svm
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: numpy>=1.23
19
+ Requires-Dist: scikit-learn>=1.6
20
+ Requires-Dist: scipy>=1.10
21
+ Requires-Dist: smo-solver>=0.1
22
+ Description-Content-Type: text/markdown
23
+
24
+ # optimized-svm
25
+
26
+ **Support vector machines whose hyperparameters are tuned by exact gradients instead of grid search.**
27
+
28
+ `optimized-svm` treats hyperparameter selection as a bilevel optimization problem: the upper level
29
+ minimizes a (smoothed) cross-validation error, the lower level is the SVM training problem. Smoothing
30
+ the loss makes the training solution differentiable in the hyperparameters, so the gradient of the
31
+ CV error follows from the implicit function theorem and a quasi-Newton method (L-BFGS-B) can be used.
32
+ This works for the regularization parameter **and** for per-feature kernel scalings (automatic
33
+ relevance determination), where grid search is hopeless.
34
+
35
+ The methods are developed in
36
+
37
+ > N. Strasdat, *Duality, Derivative-Based Training Methods and Hyperparameter Optimization for
38
+ > Support Vector Machines*, Dissertation, Technische Universität Dresden, 2023.
39
+ > [urn:nbn:de:bsz:14-qucosa2-874731](https://nbn-resolving.org/urn:nbn:de:bsz:14-qucosa2-874731)
40
+
41
+ Training uses the Rust SMO solver [`smo-solver`](https://github.com/wotzlaff/smo-solver) as a warm start
42
+ followed by a semismooth Newton method on the smoothed primal problem.
43
+
44
+ ## Features
45
+
46
+ - scikit-learn estimators: `OptimizedSVC`, `OptimizedSVR` (tuned) and `SmoothSVC`, `SmoothSVR`
47
+ (fixed hyperparameters); binary and one-vs-rest classification
48
+ - losses: smoothed hinge, smoothed ε-insensitive, squared (LS-SVM), Poisson; smoothing of degree
49
+ 2, 3 or 6; sample/class weights; optional bound on `Σ|α|` (generalized ν-SVM)
50
+ - exact hypergradients via one adjoint linear system per fold (verified against finite differences)
51
+ - ARD kernels (`blocks="features"`) with two-stage optimization, warm starts, time limits,
52
+ full optimization history
53
+ - `decision_gradient()` for local feature sensitivities
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ pip install optimized-svm
59
+ ```
60
+
61
+ During development (sibling checkout of `smo-solver`, Rust toolchain required):
62
+
63
+ ```bash
64
+ git clone https://github.com/wotzlaff/smo-solver
65
+ git clone https://github.com/wotzlaff/optimized-svm && cd optimized-svm
66
+ uv sync --group dev
67
+ uv run pytest
68
+ ```
69
+
70
+ ## Quick start
71
+
72
+ ```python
73
+ from sklearn.datasets import make_friedman1
74
+ from optimized_svm import OptimizedSVR
75
+
76
+ x, y = make_friedman1(600, noise=0.5, random_state=0)
77
+ model = OptimizedSVR(blocks="features").fit(x, y) # one kernel scaling per feature
78
+
79
+ model.lam_ # optimized regularization parameter
80
+ model.scaling_opt_ # optimized scalings: features 5..9 (pure noise) are switched off
81
+ model.history_ # every evaluation of the CV error
82
+ model.predict(x[:5])
83
+ ```
84
+
85
+ On a 1000-sample classification task (`examples/quickstart.py`) the optimizer needs 16 CV
86
+ evaluations (≈10 s) versus 143 grid points (≈20 s) for `GridSearchCV` with `sklearn.svm.SVC`,
87
+ with comparable test accuracy (0.844 vs. 0.856).
88
+
89
+ ## How it works
90
+
91
+ | step | method | thesis |
92
+ |---|---|---|
93
+ | training, warm start | SMO on the dual with quadratic smoothing (`smo-solver`) | Ch. 7 |
94
+ | training, refinement | semismooth Newton on `a + φ'(Ka/λ + b + sc) = 0` | Ch. 8.4 |
95
+ | CV error gradient | implicit function theorem, adjoint system per fold | Ch. 9.2 |
96
+ | hyperparameter search | L-BFGS-B over `(log λ, log s₁, …, log s_K)` | Ch. 9.1, 10.4 |
97
+
98
+ ## Reproducing the thesis
99
+
100
+ `benchmarks/thesis_regression.py` repeats the protocol of Table 10.4.1 (LIBSVM regression data,
101
+ 30 random 80/20 splits, ε-SVR with cubic smoothing and LS-SVM):
102
+
103
+ ```bash
104
+ python benchmarks/thesis_regression.py --datasets pyrim triazines mpg housing --splits 30
105
+ ```
106
+
107
+ ## Documentation
108
+
109
+ Full documentation including the mathematical background: <https://wotzlaff.github.io/optimized-svm/>.
110
+ Build locally with `uv run --group docs mkdocs serve`.
111
+
112
+ ## Citation
113
+
114
+ If you use this software, please cite the dissertation:
115
+
116
+ ```bibtex
117
+ @phdthesis{strasdat2023,
118
+ author = {Nico Strasdat},
119
+ title = {Duality, Derivative-Based Training Methods and
120
+ Hyperparameter Optimization for Support Vector Machines},
121
+ school = {Technische Universit{\"a}t Dresden},
122
+ type = {Dissertation},
123
+ year = {2023},
124
+ url = {https://nbn-resolving.org/urn:nbn:de:bsz:14-qucosa2-874731},
125
+ note = {urn:nbn:de:bsz:14-qucosa2-874731}
126
+ }
127
+ ```
128
+
129
+ ## License
130
+
131
+ BSD-3-Clause. The 2022 version (Nim backend) is preserved in the git tag `legacy-2022`.
@@ -0,0 +1,108 @@
1
+ # optimized-svm
2
+
3
+ **Support vector machines whose hyperparameters are tuned by exact gradients instead of grid search.**
4
+
5
+ `optimized-svm` treats hyperparameter selection as a bilevel optimization problem: the upper level
6
+ minimizes a (smoothed) cross-validation error, the lower level is the SVM training problem. Smoothing
7
+ the loss makes the training solution differentiable in the hyperparameters, so the gradient of the
8
+ CV error follows from the implicit function theorem and a quasi-Newton method (L-BFGS-B) can be used.
9
+ This works for the regularization parameter **and** for per-feature kernel scalings (automatic
10
+ relevance determination), where grid search is hopeless.
11
+
12
+ The methods are developed in
13
+
14
+ > N. Strasdat, *Duality, Derivative-Based Training Methods and Hyperparameter Optimization for
15
+ > Support Vector Machines*, Dissertation, Technische Universität Dresden, 2023.
16
+ > [urn:nbn:de:bsz:14-qucosa2-874731](https://nbn-resolving.org/urn:nbn:de:bsz:14-qucosa2-874731)
17
+
18
+ Training uses the Rust SMO solver [`smo-solver`](https://github.com/wotzlaff/smo-solver) as a warm start
19
+ followed by a semismooth Newton method on the smoothed primal problem.
20
+
21
+ ## Features
22
+
23
+ - scikit-learn estimators: `OptimizedSVC`, `OptimizedSVR` (tuned) and `SmoothSVC`, `SmoothSVR`
24
+ (fixed hyperparameters); binary and one-vs-rest classification
25
+ - losses: smoothed hinge, smoothed ε-insensitive, squared (LS-SVM), Poisson; smoothing of degree
26
+ 2, 3 or 6; sample/class weights; optional bound on `Σ|α|` (generalized ν-SVM)
27
+ - exact hypergradients via one adjoint linear system per fold (verified against finite differences)
28
+ - ARD kernels (`blocks="features"`) with two-stage optimization, warm starts, time limits,
29
+ full optimization history
30
+ - `decision_gradient()` for local feature sensitivities
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install optimized-svm
36
+ ```
37
+
38
+ During development (sibling checkout of `smo-solver`, Rust toolchain required):
39
+
40
+ ```bash
41
+ git clone https://github.com/wotzlaff/smo-solver
42
+ git clone https://github.com/wotzlaff/optimized-svm && cd optimized-svm
43
+ uv sync --group dev
44
+ uv run pytest
45
+ ```
46
+
47
+ ## Quick start
48
+
49
+ ```python
50
+ from sklearn.datasets import make_friedman1
51
+ from optimized_svm import OptimizedSVR
52
+
53
+ x, y = make_friedman1(600, noise=0.5, random_state=0)
54
+ model = OptimizedSVR(blocks="features").fit(x, y) # one kernel scaling per feature
55
+
56
+ model.lam_ # optimized regularization parameter
57
+ model.scaling_opt_ # optimized scalings: features 5..9 (pure noise) are switched off
58
+ model.history_ # every evaluation of the CV error
59
+ model.predict(x[:5])
60
+ ```
61
+
62
+ On a 1000-sample classification task (`examples/quickstart.py`) the optimizer needs 16 CV
63
+ evaluations (≈10 s) versus 143 grid points (≈20 s) for `GridSearchCV` with `sklearn.svm.SVC`,
64
+ with comparable test accuracy (0.844 vs. 0.856).
65
+
66
+ ## How it works
67
+
68
+ | step | method | thesis |
69
+ |---|---|---|
70
+ | training, warm start | SMO on the dual with quadratic smoothing (`smo-solver`) | Ch. 7 |
71
+ | training, refinement | semismooth Newton on `a + φ'(Ka/λ + b + sc) = 0` | Ch. 8.4 |
72
+ | CV error gradient | implicit function theorem, adjoint system per fold | Ch. 9.2 |
73
+ | hyperparameter search | L-BFGS-B over `(log λ, log s₁, …, log s_K)` | Ch. 9.1, 10.4 |
74
+
75
+ ## Reproducing the thesis
76
+
77
+ `benchmarks/thesis_regression.py` repeats the protocol of Table 10.4.1 (LIBSVM regression data,
78
+ 30 random 80/20 splits, ε-SVR with cubic smoothing and LS-SVM):
79
+
80
+ ```bash
81
+ python benchmarks/thesis_regression.py --datasets pyrim triazines mpg housing --splits 30
82
+ ```
83
+
84
+ ## Documentation
85
+
86
+ Full documentation including the mathematical background: <https://wotzlaff.github.io/optimized-svm/>.
87
+ Build locally with `uv run --group docs mkdocs serve`.
88
+
89
+ ## Citation
90
+
91
+ If you use this software, please cite the dissertation:
92
+
93
+ ```bibtex
94
+ @phdthesis{strasdat2023,
95
+ author = {Nico Strasdat},
96
+ title = {Duality, Derivative-Based Training Methods and
97
+ Hyperparameter Optimization for Support Vector Machines},
98
+ school = {Technische Universit{\"a}t Dresden},
99
+ type = {Dissertation},
100
+ year = {2023},
101
+ url = {https://nbn-resolving.org/urn:nbn:de:bsz:14-qucosa2-874731},
102
+ note = {urn:nbn:de:bsz:14-qucosa2-874731}
103
+ }
104
+ ```
105
+
106
+ ## License
107
+
108
+ BSD-3-Clause. The 2022 version (Nim backend) is preserved in the git tag `legacy-2022`.
@@ -0,0 +1,121 @@
1
+ """Reproduces the protocol of Table 10.4.1 (Strasdat, Sec. 10.4.2) on LIBSVM regression data.
2
+
3
+ For every dataset and split (80 % for hyperparameter optimisation + training, 20 % for testing):
4
+ features and targets are standardised on the training part, lambda and the RBF scaling are
5
+ optimised by minimising the 5-fold CV error (smoothed absolute error, delta = 0.1) and the
6
+ test MSE/MAE are reported in original units. Models: smoothed epsilon-SVR (cubic smoothing,
7
+ delta = epsilon / 4) for epsilon in {0.1, 0.25, 0.5} and the LS-SVM.
8
+
9
+ Usage::
10
+
11
+ python benchmarks/thesis_regression.py --datasets pyrim triazines --splits 30
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import time
18
+ import urllib.request
19
+ from pathlib import Path
20
+
21
+ import numpy as np
22
+ from sklearn.datasets import load_svmlight_file
23
+ from sklearn.model_selection import train_test_split
24
+ from sklearn.preprocessing import StandardScaler
25
+
26
+ from optimized_svm import OptimizedSVR
27
+
28
+ URL = "https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/regression/{}"
29
+ FILES = {
30
+ "pyrim": "pyrim",
31
+ "triazines": "triazines",
32
+ "mpg": "mpg",
33
+ "housing": "housing",
34
+ "spacega": "space_ga",
35
+ "abalone": "abalone",
36
+ "cpusmall": "cpusmall",
37
+ }
38
+ #: values of Table 10.4.1 (validation loss 10.4.1): MSE / MAE for eps 0.1, 0.25, 0.5, LS-SVM
39
+ THESIS = {
40
+ "pyrim": ([0.00656, 0.00625, 0.00645, 0.00597], [0.0532, 0.0512, 0.0526, 0.0491]),
41
+ "triazines": ([0.0244, 0.0239, 0.0237, 0.0232], [0.107, 0.109, 0.111, 0.110]),
42
+ "mpg": ([6.50, 6.58, 7.37, 6.63], [1.81, 1.84, 1.98, 1.82]),
43
+ "housing": ([10.8, 10.9, 11.9, 9.54], [2.10, 2.17, 2.43, 2.07]),
44
+ "spacega": ([0.0133, 0.0127, 0.0128, 0.0127], [0.0849, 0.0845, 0.0854, 0.0845]),
45
+ "abalone": ([4.72, 4.61, 4.56, 4.53], [1.50, 1.50, 1.52, 1.53]),
46
+ "cpusmall": ([19.1, 20.0, 32.6, 18.7], [2.70, 2.96, 3.66, 2.70]),
47
+ }
48
+ MODELS = [("eps=0.1", 0.1), ("eps=0.25", 0.25), ("eps=0.5", 0.5), ("LS-SVM", None)]
49
+
50
+
51
+ def load(name: str, cache: Path) -> tuple[np.ndarray, np.ndarray]:
52
+ cache.mkdir(parents=True, exist_ok=True)
53
+ path = cache / FILES[name]
54
+ if not path.exists():
55
+ urllib.request.urlretrieve(URL.format(FILES[name]), path)
56
+ x, y = load_svmlight_file(str(path))
57
+ return np.asarray(x.todense()), np.asarray(y)
58
+
59
+
60
+ def model(eps: float | None, n_features: int, time_limit: float) -> OptimizedSVR:
61
+ common = dict(
62
+ lam_init=1e-2,
63
+ scaling_init=float(np.sqrt(np.log(2.0) / n_features)),
64
+ lam_bounds=(1e-6, 1.0),
65
+ scaling_bounds=(1e-6, 1e2),
66
+ validation="absolute",
67
+ validation_smoothing=0.1,
68
+ normalize_y=False, # targets are standardised by the protocol
69
+ time_limit=time_limit,
70
+ )
71
+ if eps is None:
72
+ return OptimizedSVR(loss="squared", **common)
73
+ return OptimizedSVR(epsilon=eps, smoothing=eps / 4, degree="3", **common)
74
+
75
+
76
+ def run(name: str, splits: int, time_limit: float, cache: Path, max_size: int) -> None:
77
+ x, y = load(name, cache)
78
+ rng = np.random.default_rng(0)
79
+ mse = np.zeros((splits, len(MODELS)))
80
+ mae = np.zeros_like(mse)
81
+ t0 = time.perf_counter()
82
+ for s in range(splits):
83
+ if len(y) > max_size:
84
+ idx = rng.choice(len(y), max_size, replace=False)
85
+ xs, ys = x[idx], y[idx]
86
+ else:
87
+ xs, ys = x, y
88
+ xa, xb, ya, yb = train_test_split(xs, ys, test_size=0.2, random_state=s)
89
+ sx, sy = StandardScaler().fit(xa), StandardScaler().fit(ya[:, None])
90
+ xa_n, xb_n = sx.transform(xa), sx.transform(xb)
91
+ ya_n = sy.transform(ya[:, None]).ravel()
92
+ for m, (_, eps) in enumerate(MODELS):
93
+ est = model(eps, x.shape[1], time_limit).fit(xa_n, ya_n)
94
+ pred = sy.inverse_transform(est.predict(xb_n)[:, None]).ravel()
95
+ mse[s, m] = np.mean((pred - yb) ** 2)
96
+ mae[s, m] = np.mean(np.abs(pred - yb))
97
+ elapsed = time.perf_counter() - t0
98
+ ref_mse, ref_mae = THESIS[name]
99
+ print(f"\n{name} (n = {min(len(y), max_size)}, {splits} splits, {elapsed:.0f} s)")
100
+ print(f"{'model':<10}{'MSE':>11}{'thesis':>11}{'MAE':>11}{'thesis':>11}")
101
+ for m, (label, _) in enumerate(MODELS):
102
+ print(
103
+ f"{label:<10}{mse[:, m].mean():>11.4g}{ref_mse[m]:>11.4g}"
104
+ f"{mae[:, m].mean():>11.4g}{ref_mae[m]:>11.4g}"
105
+ )
106
+
107
+
108
+ def main() -> None:
109
+ parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
110
+ parser.add_argument("--datasets", nargs="+", default=list(FILES), choices=list(FILES))
111
+ parser.add_argument("--splits", type=int, default=30)
112
+ parser.add_argument("--time-limit", type=float, default=600.0)
113
+ parser.add_argument("--max-size", type=int, default=1000)
114
+ parser.add_argument("--cache", type=Path, default=Path.home() / ".cache" / "optimized-svm")
115
+ args = parser.parse_args()
116
+ for name in args.datasets:
117
+ run(name, args.splits, args.time_limit, args.cache, args.max_size)
118
+
119
+
120
+ if __name__ == "__main__":
121
+ main()
@@ -0,0 +1,41 @@
1
+ # API reference
2
+
3
+ ## Estimators
4
+
5
+ ::: optimized_svm.OptimizedSVC
6
+
7
+ ::: optimized_svm.OptimizedSVR
8
+
9
+ ::: optimized_svm.SmoothSVC
10
+
11
+ ::: optimized_svm.SmoothSVR
12
+
13
+ ## Training
14
+
15
+ ::: optimized_svm.TrainingProblem
16
+
17
+ ::: optimized_svm.train
18
+
19
+ ::: optimized_svm.newton
20
+
21
+ ::: optimized_svm.Solution
22
+
23
+ ## Losses
24
+
25
+ ::: optimized_svm.losses
26
+
27
+ ## Hypergradient
28
+
29
+ ::: optimized_svm.cv_error_and_gradient
30
+
31
+ ::: optimized_svm.fold_error_and_gradient
32
+
33
+ ## Validation losses
34
+
35
+ ::: optimized_svm.validation
36
+
37
+ ## Kernels and smoothing
38
+
39
+ ::: optimized_svm.kernels
40
+
41
+ ::: optimized_svm.smooth_max
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 – 2026-10-28
4
+
5
+ Complete overhaul of the 2022 code (tag `legacy-2022`).
6
+
7
+ ### Changed
8
+ - training backend: Rust solver `smo-solver` instead of the Nim package `nimsmo`
9
+ - new package layout (`src/`), `pyproject.toml`, snake_case API, scikit-learn compatible
10
+ estimators `SmoothSVC`, `SmoothSVR`, `OptimizedSVC`, `OptimizedSVR`
11
+ - adjoint formulation of the hypergradient (one linear system per fold)
12
+
13
+ ### Added
14
+ - one-vs-rest multi-class classification, class and sample weights
15
+ - warm starts across hyperparameter evaluations, rough SMO tolerance before Newton
16
+ - `decision_gradient()` (replaces the unfinished `estimate_sensitivity`)
17
+ - test suite (finite-difference checks of all gradients, scikit-learn estimator checks),
18
+ documentation, examples and a reproduction script for thesis Table 10.4.1
19
+
20
+ ### Fixed
21
+ - see `LEGACY_BUGS.md` in the project workspace (P1–P13), notably: regression HPO with
22
+ `maxAsum` did not terminate, SMO and Newton used different kernel parameters, the 1-norm
23
+ constraint was always treated as active
@@ -0,0 +1,71 @@
1
+ # Getting started
2
+
3
+ ## Classification
4
+
5
+ ```python
6
+ from sklearn.datasets import load_breast_cancer
7
+ from sklearn.model_selection import train_test_split
8
+ from sklearn.preprocessing import StandardScaler
9
+ from optimized_svm import OptimizedSVC
10
+
11
+ x, y = load_breast_cancer(return_X_y=True)
12
+ x = StandardScaler().fit_transform(x)
13
+ x_train, x_test, y_train, y_test = train_test_split(x, y, random_state=0)
14
+
15
+ clf = OptimizedSVC(cv=5, verbose=1).fit(x_train, y_train)
16
+ print(clf.lam_, clf.scaling_opt_, clf.score(x_test, y_test))
17
+ ```
18
+
19
+ `OptimizedSVC` minimizes the 5-fold cross-validation error of a smoothed hinge loss over
20
+ $\log\lambda$ and the log kernel scaling $\log s$ (the RBF parameter is $\gamma = s^2$). After the
21
+ search the model is refitted on all data with the best parameters.
22
+
23
+ !!! tip "Feature scaling"
24
+ The kernel acts on the raw features. Standardize them (e.g. with `StandardScaler`) or use
25
+ `blocks="features"` so that every feature gets its own scaling.
26
+
27
+ ## Regression and feature relevance
28
+
29
+ ```python
30
+ from sklearn.datasets import make_friedman1
31
+ from optimized_svm import OptimizedSVR
32
+
33
+ x, y = make_friedman1(600, noise=0.5, random_state=0)
34
+ reg = OptimizedSVR(epsilon=0.1, blocks="features").fit(x, y)
35
+ print(reg.scaling_opt_.round(3)) # small values = irrelevant features
36
+ ```
37
+
38
+ With `blocks="features"` the kernel is $k(x, z) = \exp(-\sum_j s_j^2 (x_j - z_j)^2)$. With
39
+ `two_stage=True` (default) a single common scaling is optimized first and used as starting point.
40
+
41
+ ## Choosing the model
42
+
43
+ | estimator | loss | parameters |
44
+ |---|---|---|
45
+ | `SmoothSVC` / `OptimizedSVC` | smoothed hinge $w_i\, m_\delta(\text{shift} - y_i f(x_i))$ | `smoothing`, `degree`, `shift`, `class_weight`, `max_asum` |
46
+ | `SmoothSVR` / `OptimizedSVR` (`loss="epsilon_insensitive"`) | $w_i (m_\delta(f_i - y_i - \varepsilon) + m_\delta(y_i - f_i - \varepsilon))$ | `epsilon`, `smoothing`, `degree` |
47
+ | `loss="squared"` | $\tfrac{w_i}{2} (f_i - y_i)^2$ (LS-SVM) | – |
48
+ | `loss="poisson"` | $w_i (e^{f_i} - y_i f_i)$, prediction $e^{f}$ | – |
49
+
50
+ `smoothing` ($\delta$) controls how strongly the kinks of the loss are rounded; `degree` selects
51
+ the smoothing polynomial ("2": $C^1$, exactly solvable by SMO; "3" and "6": $C^2$).
52
+
53
+ ## Useful attributes
54
+
55
+ - `lam_`, `scaling_opt_`: optimized hyperparameters
56
+ - `history_`: list of all CV evaluations (`lam`, `scaling`, `error`, `gradient_norm`, `time`, …)
57
+ - `cv_error_`, `time_`, `n_iter_`
58
+ - `support_`, `dual_coef_`, `intercept_`, `training_info_` of the final model
59
+ - `decision_gradient(X)`: derivative of the decision function w.r.t. the inputs
60
+
61
+ ## Lower-level building blocks
62
+
63
+ ```python
64
+ from optimized_svm import EpsilonInsensitiveLoss, TrainingProblem, train
65
+
66
+ prob = TrainingProblem(x * 0.5, y, EpsilonInsensitiveLoss(0.1, 0.05, "3"), lam=1e-3)
67
+ sol = train(prob) # SMO warm start + Newton refinement
68
+ sol.a, sol.b, sol.residual
69
+ ```
70
+
71
+ See `examples/` in the repository for complete scripts.