optimized-svm 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,258 @@
1
+ """Two-step training: SMO warm start (``smo_solver``) and Newton refinement (Strasdat, Ch. 8.4)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+ import numpy as np
10
+ import smo_solver
11
+ from numpy.typing import NDArray
12
+
13
+ from .linalg import solve_bordered
14
+ from .problem import TrainingProblem
15
+
16
+ Array = NDArray[np.float64]
17
+
18
+
19
+ class ConvergenceError(RuntimeError):
20
+ """The Newton method could not find a descent step."""
21
+
22
+
23
+ @dataclass
24
+ class Solution:
25
+ """Solution ``(a, b, c)`` of a training problem with ``ka = K a / lam``."""
26
+
27
+ a: Array
28
+ b: float
29
+ c: float
30
+ ka: Array
31
+ residual: float = np.nan
32
+ newton_steps: int = 0
33
+ smo_steps: int = 0
34
+ time_smo: float = 0.0
35
+ time_newton: float = 0.0
36
+ info: dict[str, Any] = field(default_factory=dict)
37
+
38
+
39
+ def residual(prob: TrainingProblem, a: Array, b: float, c: float, ka: Array, use_c: bool) -> float:
40
+ """Mean squared violation of the optimality conditions ``a + phi'(dec) = 0``, ``sum(a) = 0``
41
+ and (if the 1-norm constraint is active) ``s^T phi'(dec) + A = 0``."""
42
+ g = prob.loss.d1(prob.decision(b, c, ka), prob.terms)
43
+ res = float(np.mean((a + g) ** 2) + g.sum() ** 2)
44
+ if use_c:
45
+ res += float(g @ prob.sign + prob.max_asum) ** 2
46
+ return res
47
+
48
+
49
+ def _newton_direction(
50
+ prob: TrainingProblem, a: Array, g: Array, h: Array, use_c: bool
51
+ ) -> tuple[Array, float, float]:
52
+ """Semismooth Newton direction for ``a + phi'(K a / lam + b + s c) = 0`` and constraints."""
53
+ lam = prob.lam
54
+ p = h > 0.0
55
+ z = ~p
56
+ da = np.zeros_like(a)
57
+ da[z] = a[z] + g[z] # terms with vanishing curvature: a_Z = -phi'_Z after the step
58
+ k_pz_dz = prob.kernel(p, z) @ da[z] / lam if z.any() and p.any() else np.zeros(p.sum())
59
+ q = prob.kernel(p, p) / lam + np.diag(1.0 / h[p])
60
+ rhs = (a[p] + g[p]) / h[p] - k_pz_dz
61
+ cols = [np.ones(p.sum())]
62
+ rhs0 = [a[p].sum() - g[z].sum()]
63
+ if use_c:
64
+ s = prob.sign
65
+ cols.append(s[p])
66
+ rhs0.append(a @ s - prob.max_asum - s[z] @ da[z])
67
+ d_p, v = solve_bordered(q, np.stack(cols, axis=1), rhs, np.array(rhs0))
68
+ da[p] = d_p
69
+ return da, float(v[0]), float(v[1]) if use_c else 0.0
70
+
71
+
72
+ def newton(
73
+ prob: TrainingProblem,
74
+ a: Array,
75
+ b: float,
76
+ c: float,
77
+ *,
78
+ tol: float = 1e-12,
79
+ max_steps: int = 100,
80
+ max_backtracks: int = 20,
81
+ sigma: float = 1e-4,
82
+ beta: float = 0.5,
83
+ verbose: int = 0,
84
+ ) -> Solution:
85
+ """Damped semismooth Newton method for the (smoothed) primal training problem.
86
+
87
+ Requires a loss with (piecewise) second derivative. The 1-norm constraint is treated as
88
+ active iff ``c > 0`` at the starting point.
89
+ """
90
+ use_c = prob.max_asum is not None and c > 0.0
91
+ if not use_c:
92
+ c = 0.0
93
+ a = np.array(a, dtype=np.float64)
94
+ ka = prob.ka(a)
95
+ obj = prob.objective(a, b, c, ka)
96
+ for step in range(max_steps + 1):
97
+ dec = prob.decision(b, c, ka)
98
+ g = prob.loss.d1(dec, prob.terms)
99
+ r = a + g
100
+ res = float(np.mean(r**2) + g.sum() ** 2)
101
+ grad_c = float(g @ prob.sign + prob.max_asum) if use_c else 0.0
102
+ res += grad_c**2
103
+ if verbose:
104
+ print(f"[newton {step:3d}] f = {obj:.10g}, residual = {res:.3e}")
105
+ if res < tol or step == max_steps:
106
+ break
107
+ # gradient of the objective w.r.t. (a, b, c): (K r / lam, sum g, s^T g + A)
108
+ grad_b = float(g.sum())
109
+ h = prob.loss.d2(dec, prob.terms)
110
+ directions = []
111
+ try:
112
+ directions.append(_newton_direction(prob, a, g, h, use_c))
113
+ except np.linalg.LinAlgError:
114
+ pass
115
+ # preconditioned gradient as fallback
116
+ directions.append((r, prob.lam * grad_b, prob.lam * grad_c))
117
+ for da, db, dc in directions:
118
+ kda = prob.ka(da)
119
+ slope = float(r @ kda) + db * grad_b + dc * grad_c
120
+ if slope <= 0.0:
121
+ continue
122
+ t = 1.0
123
+ for _ in range(max_backtracks):
124
+ a_new, b_new, c_new = a - t * da, b - t * db, c - t * dc
125
+ ka_new = ka - t * kda
126
+ obj_new = prob.objective(a_new, b_new, c_new, ka_new)
127
+ if obj_new <= obj - sigma * t * slope + 1e-15 * abs(obj):
128
+ break
129
+ t *= beta
130
+ else:
131
+ continue
132
+ a, b, c, ka, obj = a_new, b_new, c_new, ka_new, obj_new
133
+ break
134
+ else:
135
+ if res < 1e3 * tol:
136
+ break
137
+ raise ConvergenceError(f"no descent step found (residual {res:.3e})")
138
+ # recompute ka from scratch to remove accumulated rounding errors
139
+ ka = prob.ka(a)
140
+ return Solution(
141
+ a, float(b), float(c), ka, residual(prob, a, b, c, ka, use_c), newton_steps=step
142
+ )
143
+
144
+
145
+ #: SMO tolerance used before a Newton refinement
146
+ ROUGH_SMO_TOL = 0.1
147
+
148
+ _SMO_OPTIONS = {
149
+ "tol",
150
+ "max_steps",
151
+ "time_limit",
152
+ "working_set",
153
+ "two_sided",
154
+ "shrinking",
155
+ "shrinking_period",
156
+ "cache_size_mb",
157
+ }
158
+
159
+
160
+ def smo_warm_start(
161
+ prob: TrainingProblem, alpha0: Array | None = None, **options: Any
162
+ ) -> tuple[Array, float, float, int]:
163
+ """Solves the dual with quadratic smoothing by SMO (``smo_solver``); zeros if not applicable.
164
+
165
+ ``alpha0`` (e.g. the solution for neighbouring hyperparameters) is used as starting point
166
+ if it is feasible.
167
+ """
168
+ unknown = set(options) - _SMO_OPTIONS
169
+ if unknown:
170
+ raise ValueError(f"unknown SMO options: {sorted(unknown)}")
171
+ loss = prob.loss
172
+ if loss.smo_kind is None:
173
+ return np.zeros(prob.size), 0.0, 0.0, 0
174
+ common = dict(gamma=1.0, weights=prob.w, smoothing=loss.smoothing, max_asum=prob.max_asum)
175
+
176
+ def run(start: Array | None) -> Any:
177
+ if loss.smo_kind == "classification":
178
+ return smo_solver.solve_classification(
179
+ prob.x, prob.y, prob.lam, shift=loss.shift, alpha0=start, **common, **options
180
+ )
181
+ return smo_solver.solve_regression(
182
+ prob.x, prob.y, prob.lam, epsilon=loss.epsilon, alpha0=start, **common, **options
183
+ )
184
+
185
+ res = None
186
+ if alpha0 is not None:
187
+ start = _feasible_start(prob, alpha0)
188
+ if start is not None:
189
+ try:
190
+ res = run(start)
191
+ except ValueError:
192
+ res = None
193
+ if res is None:
194
+ res = run(None)
195
+ return np.asarray(res.alpha), res.b, res.c, res.steps
196
+
197
+
198
+ def _feasible_start(prob: TrainingProblem, alpha0: Array) -> Array | None:
199
+ """Projects a previous solution onto the box and repairs ``sum(a) = 0``; ``None`` if this
200
+ fails (e.g. different problem size)."""
201
+ if alpha0.shape != (prob.size,):
202
+ return None
203
+ t = prob.terms
204
+ lo = np.where(t.sign > 0, 0.0, -t.w) if prob.loss.signed else np.full(prob.size, -np.inf)
205
+ hi = np.where(t.sign > 0, t.w, 0.0) if prob.loss.signed else np.full(prob.size, np.inf)
206
+ a = np.clip(alpha0, lo, hi)
207
+ if prob.max_asum is not None and (t.sign * a).sum() > prob.max_asum:
208
+ a = a * (prob.max_asum / (t.sign * a).sum()) * (1 - 1e-12)
209
+ # distribute the violation of sum(a) = 0 over variables with room in that direction
210
+ excess = a.sum()
211
+ room = (a - lo) if excess > 0 else (hi - a)
212
+ total = room.sum()
213
+ if not total > abs(excess):
214
+ return None
215
+ a = a - excess * room / total
216
+ if prob.max_asum is not None and (t.sign * a).sum() > prob.max_asum:
217
+ return None
218
+ return np.ascontiguousarray(a)
219
+
220
+
221
+ def train(
222
+ prob: TrainingProblem,
223
+ smo_options: dict[str, Any] | None = None,
224
+ newton_options: dict[str, Any] | None = None,
225
+ alpha0: Array | None = None,
226
+ ) -> Solution:
227
+ """Two-step training: SMO warm start followed by Newton refinement.
228
+
229
+ For non-smooth losses (``smoothing = 0``) the SMO solution is already exact; for losses
230
+ without SMO formulation (squared, Poisson) Newton starts at zero.
231
+ """
232
+ smo_options = dict(smo_options or {})
233
+ smooth = getattr(prob.loss, "smoothing", 1.0) > 0.0
234
+ if smooth:
235
+ # Newton refines the solution, so a rough SMO solution suffices as starting point
236
+ smo_options.setdefault("tol", ROUGH_SMO_TOL)
237
+ t0 = time.perf_counter()
238
+ a, b, c, smo_steps = smo_warm_start(prob, alpha0, **smo_options)
239
+ t_smo = time.perf_counter() - t0
240
+ t0 = time.perf_counter()
241
+ if not smooth:
242
+ sol = Solution(a, b, c, prob.ka(a))
243
+ else:
244
+ try:
245
+ sol = newton(prob, a, b, c, **(newton_options or {}))
246
+ if prob.max_asum is not None and sol.c < 0.0:
247
+ # the constraint is inactive at the solution: solve without it
248
+ sol = newton(prob, sol.a, sol.b, 0.0, **(newton_options or {}))
249
+ except ConvergenceError:
250
+ # fall back to an accurate SMO solution (exact for quadratic smoothing)
251
+ smo_options["tol"] = 1e-6
252
+ a, b, c, more = smo_warm_start(prob, a, **smo_options)
253
+ smo_steps += more
254
+ sol = Solution(a, b, c, prob.ka(a), info={"newton": "failed"})
255
+ sol.smo_steps = smo_steps
256
+ sol.time_smo = t_smo
257
+ sol.time_newton = time.perf_counter() - t0
258
+ return sol
@@ -0,0 +1,76 @@
1
+ """Validation error measures ``E(f) = sum_v L_v(f_v)`` for the upper level problem (Sec. 9.1.4).
2
+
3
+ All measures are differentiable so that the hyperparameter objective has a gradient.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+
10
+ import numpy as np
11
+ from numpy.typing import NDArray
12
+ from scipy.special import gammaln
13
+
14
+ from .smooth_max import Degree, smooth_max
15
+
16
+ Array = NDArray[np.float64]
17
+
18
+
19
+ class ValidationLoss:
20
+ """Returns the summed loss and its gradient w.r.t. the predictions."""
21
+
22
+ def __call__(self, f: Array, y: Array) -> tuple[float, Array]:
23
+ raise NotImplementedError
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class SmoothHingeValidation(ValidationLoss):
28
+ """``sum_v w_v m_delta(shift - y_v f_v)``; ``balanced`` weights both classes equally."""
29
+
30
+ shift: float = 1.0
31
+ smoothing: float = 0.5
32
+ degree: Degree = "6"
33
+ balanced: bool = False
34
+
35
+ def __call__(self, f: Array, y: Array) -> tuple[float, Array]:
36
+ m, dm, _ = smooth_max(self.smoothing, self.degree)
37
+ w = np.ones_like(y)
38
+ if self.balanced:
39
+ pos = y > 0
40
+ frac = pos.mean()
41
+ if 0 < frac < 1:
42
+ w = 0.5 / np.where(pos, frac, 1.0 - frac)
43
+ arg = self.shift - y * f
44
+ return float(w @ m(arg)), -w * y * dm(arg)
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class SmoothAbsoluteValidation(ValidationLoss):
49
+ """Smoothed absolute error ``sum_v m_delta(f_v - y_v) + m_delta(y_v - f_v)`` (Huber-like)."""
50
+
51
+ smoothing: float = 0.1
52
+ degree: Degree = "2"
53
+
54
+ def __call__(self, f: Array, y: Array) -> tuple[float, Array]:
55
+ m, dm, _ = smooth_max(self.smoothing, self.degree)
56
+ e = f - y
57
+ return float(m(e).sum() + m(-e).sum()), dm(e) - dm(-e)
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class SquaredValidation(ValidationLoss):
62
+ """``1/2 sum_v (f_v - y_v)^2``."""
63
+
64
+ def __call__(self, f: Array, y: Array) -> tuple[float, Array]:
65
+ e = f - y
66
+ return 0.5 * float(e @ e), e
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class PoissonValidation(ValidationLoss):
71
+ """Poisson deviance-type loss ``sum_v exp(f_v) - y_v f_v + log(y_v!)``."""
72
+
73
+ def __call__(self, f: Array, y: Array) -> tuple[float, Array]:
74
+ with np.errstate(over="ignore"):
75
+ ef = np.exp(f)
76
+ return float(ef.sum() - f @ y + gammaln(y + 1).sum()), ef - y
@@ -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,14 @@
1
+ optimized_svm/__init__.py,sha256=P7uqJ31jT2DxWYp1vjG7A3ikMIq7Gx5VeYpwi3I-mr4,1052
2
+ optimized_svm/estimators.py,sha256=RrwaFgIV29pcYzDaMLeY3_q3WiIejj7bX619w0N_1V4,27355
3
+ optimized_svm/hypergradient.py,sha256=Le2MNLtJz-q70SHW-KuAHuuKjArCYOkmIP9nQCGNq4U,5187
4
+ optimized_svm/kernels.py,sha256=DmaZUZTwhrTew-OPQVBAPb-FDw6zgsqFOmZSUz4dpE8,2439
5
+ optimized_svm/linalg.py,sha256=F324x9fuJic2EAEWgU3OXreeqMsdUbfX99zzSjN8AlA,734
6
+ optimized_svm/losses.py,sha256=5MzjNkyq4X2zeI-iE1UFFd3tuKwiyPlgNiC1wi8XEfQ,5651
7
+ optimized_svm/problem.py,sha256=plrjiZ4n4UfWUOGZ_7IPhIfJ8po8nVVXBhrShAeTbFM,3699
8
+ optimized_svm/smooth_max.py,sha256=l3Kwt9Gj68l-WAYVECW7yFX4PE_ODyPuOZUR79e5hDg,2966
9
+ optimized_svm/training.py,sha256=UJDhaldrKmVe8QjBsFs5wbV48YNBGgHermKfvoFqkoc,9141
10
+ optimized_svm/validation.py,sha256=SILMorbg1s3cDEMm8Jkuw8bGXJeahs2w2BMm2ryTkD0,2306
11
+ optimized_svm-0.1.0.dist-info/METADATA,sha256=wvknVL2hnhYEjPQXE_lz00nlhhYwu5Ybmwos7YM7YYw,5280
12
+ optimized_svm-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
13
+ optimized_svm-0.1.0.dist-info/licenses/LICENSE,sha256=xg1gX4NDaA0vqOtx1QTqjFg-r-edA4M823f1r876NK0,1505
14
+ optimized_svm-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.