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.
- optimized_svm-0.1.0/.github/workflows/ci.yml +44 -0
- optimized_svm-0.1.0/.github/workflows/release.yml +32 -0
- optimized_svm-0.1.0/.gitignore +11 -0
- optimized_svm-0.1.0/CHANGELOG.md +23 -0
- optimized_svm-0.1.0/LICENSE +28 -0
- optimized_svm-0.1.0/PKG-INFO +131 -0
- optimized_svm-0.1.0/README.md +108 -0
- optimized_svm-0.1.0/benchmarks/thesis_regression.py +121 -0
- optimized_svm-0.1.0/docs/api.md +41 -0
- optimized_svm-0.1.0/docs/changelog.md +23 -0
- optimized_svm-0.1.0/docs/getting-started.md +71 -0
- optimized_svm-0.1.0/docs/index.md +108 -0
- optimized_svm-0.1.0/docs/javascripts/katex.js +10 -0
- optimized_svm-0.1.0/docs/reproduction.md +42 -0
- optimized_svm-0.1.0/docs/theory/hypergradient.md +57 -0
- optimized_svm-0.1.0/docs/theory/training.md +56 -0
- optimized_svm-0.1.0/examples/ard_feature_relevance.py +28 -0
- optimized_svm-0.1.0/examples/low_level_api.py +25 -0
- optimized_svm-0.1.0/examples/quickstart.py +35 -0
- optimized_svm-0.1.0/mkdocs.yml +59 -0
- optimized_svm-0.1.0/pyproject.toml +52 -0
- optimized_svm-0.1.0/src/optimized_svm/__init__.py +38 -0
- optimized_svm-0.1.0/src/optimized_svm/estimators.py +735 -0
- optimized_svm-0.1.0/src/optimized_svm/hypergradient.py +153 -0
- optimized_svm-0.1.0/src/optimized_svm/kernels.py +63 -0
- optimized_svm-0.1.0/src/optimized_svm/linalg.py +24 -0
- optimized_svm-0.1.0/src/optimized_svm/losses.py +176 -0
- optimized_svm-0.1.0/src/optimized_svm/problem.py +99 -0
- optimized_svm-0.1.0/src/optimized_svm/smooth_max.py +90 -0
- optimized_svm-0.1.0/src/optimized_svm/training.py +258 -0
- optimized_svm-0.1.0/src/optimized_svm/validation.py +76 -0
- optimized_svm-0.1.0/tests/conftest.py +13 -0
- optimized_svm-0.1.0/tests/test_estimators.py +132 -0
- optimized_svm-0.1.0/tests/test_hypergradient.py +93 -0
- optimized_svm-0.1.0/tests/test_smooth_max_and_losses.py +86 -0
- optimized_svm-0.1.0/tests/test_training.py +78 -0
- 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,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.
|