Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 329adf1

Browse files
stefmolinStefanie Molinjeremiedbb
authored
MAINT Parameter validation for descendants of BaseLibSVM (#24001)
Co-authored-by: Stefanie Molin <[email protected]> Co-authored-by: jeremiedbb <[email protected]>
1 parent 4f315db commit 329adf1

File tree

5 files changed

+80
-113
lines changed

5 files changed

+80
-113
lines changed

sklearn/ensemble/tests/test_gradient_boosting.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Testing for the gradient boosting module (sklearn.ensemble.gradient_boosting).
33
"""
4+
import re
45
import warnings
56
import numpy as np
67
from numpy.testing import assert_allclose
@@ -1233,9 +1234,14 @@ def test_gradient_boosting_with_init_pipeline():
12331234
# sure we make the distinction between ValueError raised by a pipeline that
12341235
# was passed sample_weight, and a ValueError raised by a regular estimator
12351236
# whose input checking failed.
1236-
with pytest.raises(ValueError, match="nu <= 0 or nu > 1"):
1237+
invalid_nu = 1.5
1238+
err_msg = (
1239+
"The 'nu' parameter of NuSVR must be a float in the"
1240+
f" range (0.0, 1.0]. Got {invalid_nu} instead."
1241+
)
1242+
with pytest.raises(ValueError, match=re.escape(err_msg)):
12371243
# Note that NuSVR properly supports sample_weight
1238-
init = NuSVR(gamma="auto", nu=1.5)
1244+
init = NuSVR(gamma="auto", nu=invalid_nu)
12391245
gb = GradientBoostingRegressor(init=init)
12401246
gb.fit(X, y, sample_weight=np.ones(X.shape[0]))
12411247

sklearn/svm/_base.py

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import warnings
2-
import numbers
32
from abc import ABCMeta, abstractmethod
3+
from numbers import Integral, Real
44

55
import numpy as np
66
import scipy.sparse as sp
@@ -22,6 +22,7 @@
2222
from ..utils.validation import _num_samples
2323
from ..utils.validation import _check_sample_weight, check_consistent_length
2424
from ..utils.multiclass import check_classification_targets
25+
from ..utils._param_validation import Interval, StrOptions
2526
from ..exceptions import ConvergenceWarning
2627
from ..exceptions import NotFittedError
2728

@@ -69,6 +70,30 @@ class BaseLibSVM(BaseEstimator, metaclass=ABCMeta):
6970
Parameter documentation is in the derived `SVC` class.
7071
"""
7172

73+
_parameter_constraints = {
74+
"kernel": [
75+
StrOptions({"linear", "poly", "rbf", "sigmoid", "precomputed"}),
76+
callable,
77+
],
78+
"degree": [Interval(Integral, 0, None, closed="left")],
79+
"gamma": [
80+
StrOptions({"scale", "auto"}),
81+
Interval(Real, 0.0, None, closed="neither"),
82+
],
83+
"coef0": [Interval(Real, None, None, closed="neither")],
84+
"tol": [Interval(Real, 0.0, None, closed="neither")],
85+
"C": [Interval(Real, 0.0, None, closed="neither")],
86+
"nu": [Interval(Real, 0.0, 1.0, closed="right")],
87+
"epsilon": [Interval(Real, 0.0, None, closed="left")],
88+
"shrinking": ["boolean"],
89+
"probability": ["boolean"],
90+
"cache_size": [Interval(Real, 0, None, closed="neither")],
91+
"class_weight": [StrOptions({"balanced"}), dict, None],
92+
"verbose": ["verbose"],
93+
"max_iter": [Interval(Integral, -1, None, closed="left")],
94+
"random_state": ["random_state"],
95+
}
96+
7297
# The order of these must match the integer values in LibSVM.
7398
# XXX These are actually the same in the dense case. Need to factor
7499
# this out.
@@ -152,6 +177,7 @@ def fit(self, X, y, sample_weight=None):
152177
If X is a dense array, then the other methods will not support sparse
153178
matrices as input.
154179
"""
180+
self._validate_params()
155181

156182
rnd = check_random_state(self.random_state)
157183

@@ -160,13 +186,6 @@ def fit(self, X, y, sample_weight=None):
160186
raise TypeError("Sparse precomputed kernels are not supported.")
161187
self._sparse = sparse and not callable(self.kernel)
162188

163-
if hasattr(self, "decision_function_shape"):
164-
if self.decision_function_shape not in ("ovr", "ovo"):
165-
raise ValueError(
166-
"decision_function_shape must be either 'ovr' or 'ovo', "
167-
f"got {self.decision_function_shape}."
168-
)
169-
170189
if callable(self.kernel):
171190
check_consistent_length(X, y)
172191
else:
@@ -222,26 +241,8 @@ def fit(self, X, y, sample_weight=None):
222241
self._gamma = 1.0 / (X.shape[1] * X_var) if X_var != 0 else 1.0
223242
elif self.gamma == "auto":
224243
self._gamma = 1.0 / X.shape[1]
225-
else:
226-
raise ValueError(
227-
"When 'gamma' is a string, it should be either 'scale' or "
228-
f"'auto'. Got '{self.gamma!r}' instead."
229-
)
230-
elif isinstance(self.gamma, numbers.Real):
231-
if self.gamma <= 0:
232-
msg = (
233-
f"gamma value must be > 0; {self.gamma!r} is invalid. Use"
234-
" a positive number or use 'auto' to set gamma to a"
235-
" value of 1 / n_features."
236-
)
237-
raise ValueError(msg)
244+
elif isinstance(self.gamma, Real):
238245
self._gamma = self.gamma
239-
else:
240-
msg = (
241-
"The gamma value should be set to 'scale', 'auto' or a"
242-
f" positive float value. {self.gamma!r} is not a valid option"
243-
)
244-
raise ValueError(msg)
245246

246247
fit = self._sparse_fit if self._sparse else self._dense_fit
247248
if self.verbose:
@@ -691,6 +692,14 @@ def n_support_(self):
691692
class BaseSVC(ClassifierMixin, BaseLibSVM, metaclass=ABCMeta):
692693
"""ABC for LibSVM-based classifiers."""
693694

695+
_parameter_constraints = {
696+
**BaseLibSVM._parameter_constraints, # type: ignore
697+
"decision_function_shape": [StrOptions({"ovr", "ovo"})],
698+
"break_ties": ["boolean"],
699+
}
700+
for unused_param in ["epsilon", "nu"]:
701+
_parameter_constraints.pop(unused_param)
702+
694703
@abstractmethod
695704
def __init__(
696705
self,

sklearn/svm/_classes.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -587,14 +587,15 @@ class SVC(BaseSVC):
587587
588588
degree : int, default=3
589589
Degree of the polynomial kernel function ('poly').
590-
Ignored by all other kernels.
590+
Must be non-negative. Ignored by all other kernels.
591591
592592
gamma : {'scale', 'auto'} or float, default='scale'
593593
Kernel coefficient for 'rbf', 'poly' and 'sigmoid'.
594594
595595
- if ``gamma='scale'`` (default) is passed then it uses
596596
1 / (n_features * X.var()) as value of gamma,
597-
- if 'auto', uses 1 / n_features.
597+
- if 'auto', uses 1 / n_features
598+
- if float, must be non-negative.
598599
599600
.. versionchanged:: 0.22
600601
The default value of ``gamma`` changed from 'auto' to 'scale'.
@@ -850,14 +851,15 @@ class NuSVC(BaseSVC):
850851
851852
degree : int, default=3
852853
Degree of the polynomial kernel function ('poly').
853-
Ignored by all other kernels.
854+
Must be non-negative. Ignored by all other kernels.
854855
855856
gamma : {'scale', 'auto'} or float, default='scale'
856857
Kernel coefficient for 'rbf', 'poly' and 'sigmoid'.
857858
858859
- if ``gamma='scale'`` (default) is passed then it uses
859860
1 / (n_features * X.var()) as value of gamma,
860-
- if 'auto', uses 1 / n_features.
861+
- if 'auto', uses 1 / n_features
862+
- if float, must be non-negative.
861863
862864
.. versionchanged:: 0.22
863865
The default value of ``gamma`` changed from 'auto' to 'scale'.
@@ -1037,6 +1039,12 @@ class NuSVC(BaseSVC):
10371039

10381040
_impl = "nu_svc"
10391041

1042+
_parameter_constraints = {
1043+
**BaseSVC._parameter_constraints, # type: ignore
1044+
"nu": [Interval(Real, 0.0, 1.0, closed="right")],
1045+
}
1046+
_parameter_constraints.pop("C")
1047+
10401048
def __init__(
10411049
self,
10421050
*,
@@ -1114,14 +1122,15 @@ class SVR(RegressorMixin, BaseLibSVM):
11141122
11151123
degree : int, default=3
11161124
Degree of the polynomial kernel function ('poly').
1117-
Ignored by all other kernels.
1125+
Must be non-negative. Ignored by all other kernels.
11181126
11191127
gamma : {'scale', 'auto'} or float, default='scale'
11201128
Kernel coefficient for 'rbf', 'poly' and 'sigmoid'.
11211129
11221130
- if ``gamma='scale'`` (default) is passed then it uses
11231131
1 / (n_features * X.var()) as value of gamma,
1124-
- if 'auto', uses 1 / n_features.
1132+
- if 'auto', uses 1 / n_features
1133+
- if float, must be non-negative.
11251134
11261135
.. versionchanged:: 0.22
11271136
The default value of ``gamma`` changed from 'auto' to 'scale'.
@@ -1142,7 +1151,7 @@ class SVR(RegressorMixin, BaseLibSVM):
11421151
Epsilon in the epsilon-SVR model. It specifies the epsilon-tube
11431152
within which no penalty is associated in the training loss function
11441153
with points predicted within a distance epsilon from the actual
1145-
value.
1154+
value. Must be non-negative.
11461155
11471156
shrinking : bool, default=True
11481157
Whether to use the shrinking heuristic.
@@ -1247,6 +1256,10 @@ class SVR(RegressorMixin, BaseLibSVM):
12471256

12481257
_impl = "epsilon_svr"
12491258

1259+
_parameter_constraints = {**BaseLibSVM._parameter_constraints} # type: ignore
1260+
for unused_param in ["class_weight", "nu", "probability", "random_state"]:
1261+
_parameter_constraints.pop(unused_param)
1262+
12501263
def __init__(
12511264
self,
12521265
*,
@@ -1329,14 +1342,15 @@ class NuSVR(RegressorMixin, BaseLibSVM):
13291342
13301343
degree : int, default=3
13311344
Degree of the polynomial kernel function ('poly').
1332-
Ignored by all other kernels.
1345+
Must be non-negative. Ignored by all other kernels.
13331346
13341347
gamma : {'scale', 'auto'} or float, default='scale'
13351348
Kernel coefficient for 'rbf', 'poly' and 'sigmoid'.
13361349
13371350
- if ``gamma='scale'`` (default) is passed then it uses
13381351
1 / (n_features * X.var()) as value of gamma,
1339-
- if 'auto', uses 1 / n_features.
1352+
- if 'auto', uses 1 / n_features
1353+
- if float, must be non-negative.
13401354
13411355
.. versionchanged:: 0.22
13421356
The default value of ``gamma`` changed from 'auto' to 'scale'.
@@ -1451,6 +1465,10 @@ class NuSVR(RegressorMixin, BaseLibSVM):
14511465

14521466
_impl = "nu_svr"
14531467

1468+
_parameter_constraints = {**BaseLibSVM._parameter_constraints} # type: ignore
1469+
for unused_param in ["class_weight", "epsilon", "probability", "random_state"]:
1470+
_parameter_constraints.pop(unused_param)
1471+
14541472
def __init__(
14551473
self,
14561474
*,
@@ -1523,14 +1541,15 @@ class OneClassSVM(OutlierMixin, BaseLibSVM):
15231541
15241542
degree : int, default=3
15251543
Degree of the polynomial kernel function ('poly').
1526-
Ignored by all other kernels.
1544+
Must be non-negative. Ignored by all other kernels.
15271545
15281546
gamma : {'scale', 'auto'} or float, default='scale'
15291547
Kernel coefficient for 'rbf', 'poly' and 'sigmoid'.
15301548
15311549
- if ``gamma='scale'`` (default) is passed then it uses
15321550
1 / (n_features * X.var()) as value of gamma,
1533-
- if 'auto', uses 1 / n_features.
1551+
- if 'auto', uses 1 / n_features
1552+
- if float, must be non-negative.
15341553
15351554
.. versionchanged:: 0.22
15361555
The default value of ``gamma`` changed from 'auto' to 'scale'.
@@ -1645,6 +1664,10 @@ class OneClassSVM(OutlierMixin, BaseLibSVM):
16451664

16461665
_impl = "one_class"
16471666

1667+
_parameter_constraints = {**BaseLibSVM._parameter_constraints} # type: ignore
1668+
for unused_param in ["C", "class_weight", "epsilon", "probability", "random_state"]:
1669+
_parameter_constraints.pop(unused_param)
1670+
16481671
def __init__(
16491672
self,
16501673
*,

sklearn/svm/tests/test_svm.py

Lines changed: 2 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -455,9 +455,6 @@ def test_decision_function_shape(SVM):
455455
dec = clf.decision_function(X_train)
456456
assert dec.shape == (len(X_train), 10)
457457

458-
with pytest.raises(ValueError, match="must be either 'ovr' or 'ovo'"):
459-
SVM(decision_function_shape="bad").fit(X_train, y_train)
460-
461458

462459
def test_svr_predict():
463460
# Test SVR's decision_function
@@ -685,19 +682,10 @@ def test_auto_weight():
685682

686683

687684
def test_bad_input():
688-
# Test that it gives proper exception on deficient input
689-
# impossible value of C
690-
with pytest.raises(ValueError):
691-
svm.SVC(C=-1).fit(X, Y)
692-
693-
# impossible value of nu
694-
clf = svm.NuSVC(nu=0.0)
695-
with pytest.raises(ValueError):
696-
clf.fit(X, Y)
697-
685+
# Test dimensions for labels
698686
Y2 = Y[:-1] # wrong dimensions for labels
699687
with pytest.raises(ValueError):
700-
clf.fit(X, Y2)
688+
svm.SVC().fit(X, Y2)
701689

702690
# Test with arrays that are non-contiguous.
703691
for clf in (svm.SVC(), svm.LinearSVC(random_state=0)):
@@ -745,60 +733,6 @@ def test_svc_nonfinite_params():
745733
clf.fit(X, y)
746734

747735

748-
@pytest.mark.parametrize(
749-
"Estimator, data",
750-
[
751-
(svm.SVC, datasets.load_iris(return_X_y=True)),
752-
(svm.NuSVC, datasets.load_iris(return_X_y=True)),
753-
(svm.SVR, datasets.load_diabetes(return_X_y=True)),
754-
(svm.NuSVR, datasets.load_diabetes(return_X_y=True)),
755-
(svm.OneClassSVM, datasets.load_iris(return_X_y=True)),
756-
],
757-
)
758-
@pytest.mark.parametrize(
759-
"gamma, err_msg",
760-
[
761-
(
762-
"auto_deprecated",
763-
"When 'gamma' is a string, it should be either 'scale' or 'auto'",
764-
),
765-
(
766-
-1,
767-
"gamma value must be > 0; -1 is invalid. Use"
768-
" a positive number or use 'auto' to set gamma to a"
769-
" value of 1 / n_features.",
770-
),
771-
(
772-
0.0,
773-
"gamma value must be > 0; 0.0 is invalid. Use"
774-
" a positive number or use 'auto' to set gamma to a"
775-
" value of 1 / n_features.",
776-
),
777-
(
778-
np.array([1.0, 4.0]),
779-
"The gamma value should be set to 'scale',"
780-
f" 'auto' or a positive float value. {np.array([1.0, 4.0])!r}"
781-
" is not a valid option",
782-
),
783-
(
784-
[],
785-
"The gamma value should be set to 'scale', 'auto' or a positive"
786-
f" float value. {[]} is not a valid option",
787-
),
788-
(
789-
{},
790-
"The gamma value should be set to 'scale', 'auto' or a positive"
791-
" float value. {} is not a valid option",
792-
),
793-
],
794-
)
795-
def test_svm_gamma_error(Estimator, data, gamma, err_msg):
796-
X, y = data
797-
est = Estimator(gamma=gamma)
798-
with pytest.raises(ValueError, match=(re.escape(err_msg))):
799-
est.fit(X, y)
800-
801-
802736
def test_unicode_kernel():
803737
# Test that a unicode kernel name does not cause a TypeError
804738
clf = svm.SVC(kernel="linear", probability=True)

sklearn/tests/test_common.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -473,12 +473,9 @@ def test_estimators_do_not_raise_errors_in_init_or_set_params(Estimator):
473473
"MultiTaskElasticNet",
474474
"MultiTaskLasso",
475475
"NeighborhoodComponentsAnalysis",
476-
"NuSVC",
477-
"NuSVR",
478476
"Nystroem",
479477
"OAS",
480478
"OPTICS",
481-
"OneClassSVM",
482479
"OneVsOneClassifier",
483480
"OneVsRestClassifier",
484481
"PatchExtractor",
@@ -492,8 +489,6 @@ def test_estimators_do_not_raise_errors_in_init_or_set_params(Estimator):
492489
"RegressorChain",
493490
"RidgeCV",
494491
"RidgeClassifierCV",
495-
"SVC",
496-
"SVR",
497492
"SelectFdr",
498493
"SelectFpr",
499494
"SelectFromModel",

0 commit comments

Comments
 (0)