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

Skip to content

Commit 0206d3e

Browse files
Jitensidjeremiedbb
andauthored
MAINT validate parameters for MLPRregressor and MLPClassifier (#23789)
Co-authored-by: jeremie du boisberranger <[email protected]>
1 parent 551417a commit 0206d3e

File tree

3 files changed

+67
-125
lines changed

3 files changed

+67
-125
lines changed

sklearn/neural_network/_multilayer_perceptron.py

Lines changed: 67 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# Jiyuan Qian
77
# License: BSD 3 clause
88

9+
from numbers import Integral, Real
910
import numpy as np
1011

1112
from abc import ABCMeta, abstractmethod
@@ -35,6 +36,7 @@
3536
from ..utils.multiclass import type_of_target
3637
from ..utils.optimize import _check_optimize_result
3738
from ..utils.metaestimators import available_if
39+
from ..utils._param_validation import StrOptions, Interval
3840

3941

4042
_STOCHASTIC_SOLVERS = ["sgd", "adam"]
@@ -54,6 +56,40 @@ class BaseMultilayerPerceptron(BaseEstimator, metaclass=ABCMeta):
5456
.. versionadded:: 0.18
5557
"""
5658

59+
_parameter_constraints = {
60+
"hidden_layer_sizes": [
61+
"array-like",
62+
Interval(Integral, 1, None, closed="left"),
63+
],
64+
"activation": [StrOptions({"identity", "logistic", "tanh", "relu"})],
65+
"solver": [StrOptions({"lbfgs", "sgd", "adam"})],
66+
"alpha": [Interval(Real, 0, None, closed="left")],
67+
"batch_size": [
68+
StrOptions({"auto"}),
69+
Interval(Integral, 1, None, closed="left"),
70+
],
71+
"learning_rate": [StrOptions({"constant", "invscaling", "adaptive"})],
72+
"learning_rate_init": [Interval(Real, 0, None, closed="neither")],
73+
"power_t": [Interval(Real, 0, None, closed="left")],
74+
"max_iter": [Interval(Integral, 1, None, closed="left")],
75+
"shuffle": ["boolean"],
76+
"random_state": ["random_state"],
77+
"tol": [Interval(Real, 0, None, closed="left")],
78+
"verbose": ["verbose"],
79+
"warm_start": ["boolean"],
80+
"momentum": [Interval(Real, 0, 1, closed="both")],
81+
"nesterovs_momentum": ["boolean"],
82+
"early_stopping": ["boolean"],
83+
"validation_fraction": [Interval(Real, 0, 1, closed="left")],
84+
"beta_1": [Interval(Real, 0, 1, closed="left")],
85+
"beta_2": [Interval(Real, 0, 1, closed="left")],
86+
"epsilon": [Interval(Real, 0, None, closed="neither")],
87+
# TODO update when ScalarOptions available
88+
# [Interval(Integral, 1, None, closed="left"), ScalarOptions({np.inf})]
89+
"n_iter_no_change": [Interval(Real, 0, None, closed="right")],
90+
"max_fun": [Interval(Integral, 1, None, closed="left")],
91+
}
92+
5793
@abstractmethod
5894
def __init__(
5995
self,
@@ -381,8 +417,6 @@ def _fit(self, X, y, incremental=False):
381417
hidden_layer_sizes = [hidden_layer_sizes]
382418
hidden_layer_sizes = list(hidden_layer_sizes)
383419

384-
# Validate input parameters.
385-
self._validate_hyperparameters()
386420
if np.any(np.array(hidden_layer_sizes) <= 0):
387421
raise ValueError(
388422
"hidden_layer_sizes must be > 0, got %s." % hidden_layer_sizes
@@ -452,67 +486,6 @@ def _fit(self, X, y, incremental=False):
452486

453487
return self
454488

455-
def _validate_hyperparameters(self):
456-
if not isinstance(self.shuffle, bool):
457-
raise ValueError(
458-
"shuffle must be either True or False, got %s." % self.shuffle
459-
)
460-
if self.max_iter <= 0:
461-
raise ValueError("max_iter must be > 0, got %s." % self.max_iter)
462-
if self.max_fun <= 0:
463-
raise ValueError("max_fun must be > 0, got %s." % self.max_fun)
464-
if self.alpha < 0.0:
465-
raise ValueError("alpha must be >= 0, got %s." % self.alpha)
466-
if (
467-
self.learning_rate in ["constant", "invscaling", "adaptive"]
468-
and self.learning_rate_init <= 0.0
469-
):
470-
raise ValueError(
471-
"learning_rate_init must be > 0, got %s." % self.learning_rate
472-
)
473-
if self.momentum > 1 or self.momentum < 0:
474-
raise ValueError("momentum must be >= 0 and <= 1, got %s" % self.momentum)
475-
if not isinstance(self.nesterovs_momentum, bool):
476-
raise ValueError(
477-
"nesterovs_momentum must be either True or False, got %s."
478-
% self.nesterovs_momentum
479-
)
480-
if not isinstance(self.early_stopping, bool):
481-
raise ValueError(
482-
"early_stopping must be either True or False, got %s."
483-
% self.early_stopping
484-
)
485-
if self.validation_fraction < 0 or self.validation_fraction >= 1:
486-
raise ValueError(
487-
"validation_fraction must be >= 0 and < 1, got %s"
488-
% self.validation_fraction
489-
)
490-
if self.beta_1 < 0 or self.beta_1 >= 1:
491-
raise ValueError("beta_1 must be >= 0 and < 1, got %s" % self.beta_1)
492-
if self.beta_2 < 0 or self.beta_2 >= 1:
493-
raise ValueError("beta_2 must be >= 0 and < 1, got %s" % self.beta_2)
494-
if self.epsilon <= 0.0:
495-
raise ValueError("epsilon must be > 0, got %s." % self.epsilon)
496-
if self.n_iter_no_change <= 0:
497-
raise ValueError(
498-
"n_iter_no_change must be > 0, got %s." % self.n_iter_no_change
499-
)
500-
501-
# raise ValueError if not registered
502-
if self.activation not in ACTIVATIONS:
503-
raise ValueError(
504-
"The activation '%s' is not supported. Supported activations are %s."
505-
% (self.activation, list(sorted(ACTIVATIONS)))
506-
)
507-
if self.learning_rate not in ["constant", "invscaling", "adaptive"]:
508-
raise ValueError("learning rate %s is not supported. " % self.learning_rate)
509-
supported_solvers = _STOCHASTIC_SOLVERS + ["lbfgs"]
510-
if self.solver not in supported_solvers:
511-
raise ValueError(
512-
"The solver %s is not supported. Expected one of: %s"
513-
% (self.solver, ", ".join(supported_solvers))
514-
)
515-
516489
def _fit_lbfgs(
517490
self, X, y, activations, deltas, coef_grads, intercept_grads, layer_units
518491
):
@@ -617,7 +590,7 @@ def _fit_stochastic(
617590
if self.batch_size == "auto":
618591
batch_size = min(200, n_samples)
619592
else:
620-
if self.batch_size < 1 or self.batch_size > n_samples:
593+
if self.batch_size > n_samples:
621594
warnings.warn(
622595
"Got `batch_size` less than 1 or larger than "
623596
"sample size. It is going to be clipped"
@@ -759,6 +732,8 @@ def fit(self, X, y):
759732
self : object
760733
Returns a trained MLP model.
761734
"""
735+
self._validate_params()
736+
762737
return self._fit(X, y, incremental=False)
763738

764739
def _check_solver(self):
@@ -770,25 +745,6 @@ def _check_solver(self):
770745
)
771746
return True
772747

773-
@available_if(_check_solver)
774-
def partial_fit(self, X, y):
775-
"""Update the model with a single iteration over the given data.
776-
777-
Parameters
778-
----------
779-
X : {array-like, sparse matrix} of shape (n_samples, n_features)
780-
The input data.
781-
782-
y : ndarray of shape (n_samples,)
783-
The target values.
784-
785-
Returns
786-
-------
787-
self : object
788-
Trained MLP model.
789-
"""
790-
return self._fit(X, y, incremental=True)
791-
792748

793749
class MLPClassifier(ClassifierMixin, BaseMultilayerPerceptron):
794750
"""Multi-layer Perceptron classifier.
@@ -800,7 +756,7 @@ class MLPClassifier(ClassifierMixin, BaseMultilayerPerceptron):
800756
801757
Parameters
802758
----------
803-
hidden_layer_sizes : tuple, length = n_layers - 2, default=(100,)
759+
hidden_layer_sizes : array-like of shape(n_layers - 2,), default=(100,)
804760
The ith element represents the number of neurons in the ith
805761
hidden layer.
806762
@@ -1205,16 +1161,17 @@ def partial_fit(self, X, y, classes=None):
12051161
self : object
12061162
Trained MLP model.
12071163
"""
1164+
if not hasattr(self, "coefs_"):
1165+
self._validate_params()
1166+
12081167
if _check_partial_fit_first_call(self, classes):
12091168
self._label_binarizer = LabelBinarizer()
12101169
if type_of_target(y).startswith("multilabel"):
12111170
self._label_binarizer.fit(y)
12121171
else:
12131172
self._label_binarizer.fit(classes)
12141173

1215-
super().partial_fit(X, y)
1216-
1217-
return self
1174+
return self._fit(X, y, incremental=True)
12181175

12191176
def predict_log_proba(self, X):
12201177
"""Return the log of probability estimates.
@@ -1273,7 +1230,7 @@ class MLPRegressor(RegressorMixin, BaseMultilayerPerceptron):
12731230
12741231
Parameters
12751232
----------
1276-
hidden_layer_sizes : tuple, length = n_layers - 2, default=(100,)
1233+
hidden_layer_sizes : array-like of shape(n_layers - 2,), default=(100,)
12771234
The ith element represents the number of neurons in the ith
12781235
hidden layer.
12791236
@@ -1606,3 +1563,25 @@ def _validate_input(self, X, y, incremental, reset):
16061563
if y.ndim == 2 and y.shape[1] == 1:
16071564
y = column_or_1d(y, warn=True)
16081565
return X, y
1566+
1567+
@available_if(lambda est: est._check_solver)
1568+
def partial_fit(self, X, y):
1569+
"""Update the model with a single iteration over the given data.
1570+
1571+
Parameters
1572+
----------
1573+
X : {array-like, sparse matrix} of shape (n_samples, n_features)
1574+
The input data.
1575+
1576+
y : ndarray of shape (n_samples,)
1577+
The target values.
1578+
1579+
Returns
1580+
-------
1581+
self : object
1582+
Trained MLP model.
1583+
"""
1584+
if not hasattr(self, "coefs_"):
1585+
self._validate_params()
1586+
1587+
return self._fit(X, y, incremental=True)

sklearn/neural_network/tests/test_mlp.py

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -524,41 +524,6 @@ def test_nonfinite_params():
524524
clf.fit(X, y)
525525

526526

527-
@pytest.mark.parametrize(
528-
"args",
529-
[
530-
{"hidden_layer_sizes": -1},
531-
{"max_iter": -1},
532-
{"shuffle": "true"},
533-
{"alpha": -1},
534-
{"learning_rate_init": -1},
535-
{"momentum": 2},
536-
{"momentum": -0.5},
537-
{"nesterovs_momentum": "invalid"},
538-
{"early_stopping": "invalid"},
539-
{"validation_fraction": 1},
540-
{"validation_fraction": -0.5},
541-
{"beta_1": 1},
542-
{"beta_1": -0.5},
543-
{"beta_2": 1},
544-
{"beta_2": -0.5},
545-
{"epsilon": -0.5},
546-
{"n_iter_no_change": -1},
547-
{"solver": "hadoken"},
548-
{"learning_rate": "converge"},
549-
{"activation": "cloak"},
550-
],
551-
)
552-
def test_params_errors(args):
553-
# Test that invalid parameters raise value error
554-
X = [[3, 2], [1, 6]]
555-
y = [1, 0]
556-
clf = MLPClassifier
557-
558-
with pytest.raises(ValueError):
559-
clf(**args).fit(X, y)
560-
561-
562527
def test_predict_proba_binary():
563528
# Test that predict_proba works as expected for binary class.
564529
X = X_digits_binary[:50]

sklearn/tests/test_common.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,8 +493,6 @@ def test_estimators_do_not_raise_errors_in_init_or_set_params(Estimator):
493493
"LatentDirichletAllocation",
494494
"LedoitWolf",
495495
"LocallyLinearEmbedding",
496-
"MLPClassifier",
497-
"MLPRegressor",
498496
"MinCovDet",
499497
"MiniBatchDictionaryLearning",
500498
"MultiOutputClassifier",

0 commit comments

Comments
 (0)