import inspect
import warnings
from copy import deepcopy
from operator import attrgetter
import numpy as np
from scipy.stats import norm
from sklearn.base import MetaEstimatorMixin, get_tags, is_regressor
from sklearn.exceptions import NotFittedError
from sklearn.utils.validation import (
check_array,
check_is_fitted,
check_random_state,
)
from ..base import (
SkactivemlRegressor,
ProbabilisticRegressor,
)
from ..utils import (
is_labeled,
match_signature,
check_n_features,
check_scalar,
check_type,
MISSING_LABEL,
TargetSpec,
resolve_target_spec,
)
from ..utils._target import _check_target_spec_capability
from ..utils._wrapper_state import (
_resolve_own_fitted_attribute,
_restore_wrapper_attributes,
)
successful_skorch_torch_import = False
try:
from torch import nn
from skactiveml.base import SkorchMixin
from skactiveml.utils import make_criterion_tuple_aware
successful_skorch_torch_import = True
except ImportError: # pragma: no cover
pass
def _narrow_to_single_output_prediction(estimator, prediction):
"""Return `prediction` with its primary output as shape `(n_samples,)`.
A single-output regression target is described by one value per sample,
so the wrapped estimator's predictions are narrowed to that shape. The
target type is resolved from the prediction itself and checked against
the capabilities of `estimator`, so that a multi-output prediction is
rejected instead of being flattened into meaningless values.
Parameters
----------
estimator : SkactivemlRegressor
The regressor whose capabilities the prediction must satisfy.
prediction : array-like or tuple
The predictions of the wrapped estimator. A tuple carries the
predicted target values as its first element, e.g., together with
their standard deviations or further model outputs.
Returns
-------
prediction : numpy.ndarray of shape (n_samples,) or tuple
The predictions with their primary output narrowed. Further tuple
elements are returned unchanged, because their shapes describe
model outputs instead of target values.
Raises
------
ValueError
If the predicted target values do not describe a target type
supported by `estimator`.
"""
is_tuple = isinstance(prediction, tuple)
y_pred = prediction[0] if is_tuple else prediction
prediction_spec = resolve_target_spec(
y_pred,
task="regression",
target_type="auto",
annotation_type="single-annotator",
classes=None,
missing_label=estimator.missing_label,
)
_check_target_spec_capability(
type(estimator).__name__,
prediction_spec,
estimator._target_capabilities,
)
y_pred = np.asarray(y_pred).reshape(-1)
if is_tuple:
return (y_pred, *prediction[1:])
return y_pred
[docs]
class SklearnRegressor(SkactivemlRegressor, MetaEstimatorMixin):
"""Sklearn Regressor
Implementation of a wrapper class for scikit-learn regressors such that
missing labels can be handled. Therefore, samples with missing values are
filtered.
Parameters
----------
estimator : sklearn.base.RegressorMixin with predict method
scikit-learn regressor.
include_unlabeled_samples : bool, default=False
- If `False`, only labeled samples are passed to the `fit` method of
the `estimator`.
- If `True`, all samples including the unlabeled ones are passed to
the `fit` method of the `estimator`. Ensure that your `estimator`
is able to handle unlabeled samples marked by `missing_label`.
Otherwise, `missing_label` is interpreted as a regular target value.
missing_label : scalar or string or np.nan or None, default=np.nan
Value to represent a missing label.
random_state : int or RandomState instance or None, default=None
Determines random number for `predict` method. Pass an int for
reproducible results across multiple method calls.
target_type : "auto" or "single-output", default="auto"
Declared target type. This wrapper supports only single-output
regression.
Attributes
----------
target_spec_ : skactiveml.utils.TargetSpec
Immutable target specification established by a successful fit. Its
`target_type` is `"single-output"` and its `classes` field is `None`
for supported version 1.1 execution.
Notes
-----
Attributes this wrapper does not hold itself are read from the wrapped
`estimator`. The fitted attributes it resolves itself, e.g. `target_spec_`,
never are: around a pre-fitted `estimator` they resolve this wrapper's own
target semantics on first access, and they raise the usual not-fitted error
while no such semantics exist. A pre-fitted estimator's own target
specification stays readable as `estimator.target_spec_`.
"""
#: Fitted attributes this wrapper resolves itself, which `__getattr__`
#: therefore never forwards to the wrapped `estimator`. `n_features_in_`
#: is deliberately absent: it describes the input data rather than the
#: target, both objects agree on it, and `partial_fit` reads it through
#: `hasattr` to decide whether to reset the feature count.
_own_fitted_attributes = frozenset(
{
"_label_mean",
"_label_std",
"check_X_dict_",
"estimator_",
"is_fitted_",
"target_spec_",
}
)
def __init__(
self,
estimator,
include_unlabeled_samples=False,
missing_label=MISSING_LABEL,
random_state=None,
target_type="auto",
):
super().__init__(
random_state=random_state,
missing_label=missing_label,
target_type=target_type,
)
self.estimator = estimator
self.include_unlabeled_samples = include_unlabeled_samples
[docs]
@match_signature("estimator", "fit")
def fit(self, X, y, sample_weight=None, **fit_kwargs):
"""Fit the model using X as training data and y as labels.
Parameters
----------
X : matrix-like of shape (n_samples, n_features)
The sample matrix X is the feature matrix representing the samples.
y : array-like of shape (n_samples,)
It contains the numeric target values of the training samples.
Missing labels are represented as `self.missing_label`.
sample_weight : array-like of shape (n_samples,), default=None
It contains the weights of the training samples´ labels. It
must have the same shape as y.
fit_kwargs : dict-like
Further parameters are passed as input to the `fit` method of the
'estimator'.
Returns
-------
self: SklearnRegressor,
The SklearnRegressor is fitted on the training data.
"""
return self._fit(
fit_function="fit",
X=X,
y=y,
sample_weight=sample_weight,
**fit_kwargs,
)
[docs]
@match_signature("estimator", "partial_fit")
def partial_fit(self, X, y, sample_weight=None, **fit_kwargs):
"""Partially fitting the model using X as training data and y as
labels.
Parameters
----------
X : matrix-like of shape (n_samples, n_features)
The sample matrix X is the feature matrix representing the samples.
y : array-like of shape (n_samples,)
It contains the numeric labels of the training samples.
Missing labels are represented the attribute `self.missing_label`.
sample_weight : array-like of shape (n_samples,)
It contains the weights of the training samples' numeric labels. It
must have the same shape as y.
fit_kwargs : dict-like
Further parameters as input to the `fit` method of the `estimator`.
Returns
-------
self : SklearnRegressor,
The `SklearnRegressor` is fitted on the training data.
"""
return self._fit(
fit_function="partial_fit",
X=X,
y=y,
sample_weight=sample_weight,
**fit_kwargs,
)
def _fit(self, fit_function, X, y, sample_weight, **fit_kwargs):
"""Fit or partially fit this wrapper as a single transaction.
The snapshot taken here covers the entire fit, not only the estimator
call: a rejection raised while validating the estimator, the inputs,
or the target specification rolls the wrapper back just as a failing
estimator fit would. Without that, a failing re-fit would leave an
already fitted wrapper reporting metadata from the abandoned attempt,
e.g. an `n_features_in_` that contradicts its `estimator_`, so that it
could no longer predict on the data it was trained on.
The label-mean fallback is not a failure and keeps its state, because
`_validate_and_fit` returns `self` for it rather than raising. The
transaction also covers this wrapper only: as
`_restore_wrapper_attributes` documents, a `partial_fit` that already
mutated `estimator_` in place cannot be rolled back.
Parameters
----------
fit_function : "fit" or "partial_fit"
Name of the estimator method to call.
X : matrix-like of shape (n_samples, n_features)
The sample matrix X is the feature matrix representing the samples.
y : array-like of shape (n_samples,)
It contains the numeric target values of the training samples.
sample_weight : array-like of shape (n_samples,)
It contains the weights of the training samples' labels.
fit_kwargs : dict-like
Further parameters as input to `fit_function` of the `estimator`.
Returns
-------
self : SklearnRegressor
The wrapper fitted on the training data.
"""
attributes_before = dict(self.__dict__)
try:
return self._validate_and_fit(
fit_function=fit_function,
X=X,
y=y,
sample_weight=sample_weight,
**fit_kwargs,
)
except Exception:
_restore_wrapper_attributes(self, attributes_before)
raise
def _validate_and_fit(
self, fit_function, X, y, sample_weight, **fit_kwargs
):
"""Validate the inputs and fit the estimator, committing state freely.
This method may write fitted attributes before a later step rejects
the call, because its only caller `_fit` rolls them back. See `_fit`
for the parameters and the transactional guarantee.
"""
if not is_regressor(estimator=self.estimator):
raise TypeError(
"'{}' must be a scikit-learn "
"regressor.".format(self.estimator)
)
check_type(
self.include_unlabeled_samples, "include_unlabeled_samples", bool
)
self.check_X_dict_ = {
"ensure_min_samples": 0,
"ensure_min_features": 0,
"allow_nd": True,
"dtype": None,
}
established_spec = (
getattr(self, "target_spec_", None)
if fit_function == "partial_fit"
else None
)
target_spec = self._resolve_fitting_target_spec(
y, established_spec=established_spec
)
X, y, sample_weight = self._validate_data(
X,
y,
sample_weight,
check_X_dict=self.check_X_dict_,
reset=fit_function == "fit" or not hasattr(self, "n_features_in_"),
target_spec=target_spec,
)
is_lbld = is_labeled(y, missing_label=self.missing_label_)
if self.include_unlabeled_samples:
is_included = np.full_like(y, True, dtype=bool)
else:
is_included = is_lbld
X_train = X[is_included]
y_train = y[is_included]
estimator_params = dict(fit_kwargs) if fit_kwargs is not None else {}
if sample_weight is not None:
estimator_params["sample_weight"] = sample_weight[is_included]
self._label_mean = np.mean(y[is_lbld]) if np.sum(is_lbld) > 0 else 0
self._label_std = np.std(y[is_lbld]) if np.sum(is_lbld) > 1 else 1
if fit_function != "partial_fit" or not hasattr(self, "estimator_"):
self.estimator_ = deepcopy(self.estimator)
try:
attrgetter(fit_function)(self.estimator_)(
X_train, y_train, **estimator_params
)
self.is_fitted_ = True
except Exception as e:
warnings.warn(
f"The 'estimator' could not be fitted because of"
f" '{e}'. Therefore, the empirical label mean "
f"`_label_mean={self._label_mean}` and the "
f"empirical label standard deviation "
f"`_label_std={self._label_std}` will be used to make "
f"predictions."
)
self.is_fitted_ = False
return self
[docs]
@match_signature("estimator", "predict")
def predict(self, X, **predict_kwargs):
"""Return label predictions for the input data `X`.
Parameters
----------
X : array-like of shape (n_samples, n_features)
Input samples.
predict_kwargs : dict-like
Further parameters are passed as input to the `predict` method of
the `estimator`. If the estimator could not be fitted, only
`return_std` is supported as keyword argument.
Returns
-------
y : ndarray of shape (n_samples,)
Predicted labels of the input samples.
"""
check_is_fitted(self)
predict_dict = {"ensure_min_samples": 1, "ensure_min_features": 1}
X = check_array(X, **(self.check_X_dict_ | predict_dict))
check_n_features(self, X, reset=False)
if self.is_fitted_:
prediction = self.estimator_.predict(X, **predict_kwargs)
return _narrow_to_single_output_prediction(self, prediction)
warnings.warn(
f"Since the 'estimator' could not be fitted when"
f" calling the `fit` method, the label "
f"mean `_label_mean={self._label_mean}` and optionally the "
f"label standard deviation `_label_std={self._label_std}` is "
f"used to make the predictions."
)
has_std = predict_kwargs.pop("return_std", False)
if has_std:
return (
np.full(len(X), self._label_mean),
np.full(len(X), self._label_std),
)
else:
return np.full(len(X), self._label_mean)
[docs]
@match_signature("estimator", "sample_y")
def sample_y(self, X, n_samples=1, **sample_kwargs):
"""Assumes a probabilistic regressor. Samples are drawn from a
predicted target distribution.
Parameters
----------
X : array-like of shape (n_samples_X, n_features)
Input samples from which the target values are drawn.
n_samples : int, default=1
Number of random samples to be drawn.
**sample_kwargs : dict
Additional keyword arguments for sampling. For example:
random_state : int, RandomState instance or None, default=None
Determines the random number generation for drawing samples.
Pass an int for reproducible results across multiple method
calls.
Returns
-------
y_samples : ndarray of shape (n_samples_X, n_samples)
Drawn random target samples.
"""
return self._sample(
sample_function="sample_y",
X=X,
n_samples=n_samples,
**sample_kwargs,
)
[docs]
@match_signature("estimator", "sample")
def sample(self, X, n_samples=1, **sample_kwargs):
"""Assumes a probabilistic regressor. Samples are drawn from a
predicted target distribution.
Parameters
----------
X : array-like of shape (n_samples_X, n_features)
Input samples from which the target values are drawn.
n_samples : int, default=1
Number of random samples to be drawn.
**sample_kwargs : dict
Additional keyword arguments for sampling. For example:
random_state : int, RandomState instance or None, default=None
Determines the random number generation for drawing samples.
Pass an int for reproducible results across multiple method
calls.
Returns
-------
y_samples : ndarray of shape (n_samples_X, n_samples)
Drawn random target samples.
"""
return self._sample(
sample_function="sample", X=X, n_samples=n_samples, **sample_kwargs
)
def _sample(self, sample_function, X, n_samples=1, **sample_kwargs):
check_is_fitted(self)
predict_dict = {"ensure_min_samples": 1, "ensure_min_features": 1}
X = check_array(X, **(self.check_X_dict_ | predict_dict))
check_n_features(self, X, reset=False)
try:
return attrgetter(sample_function)(self.estimator_)(
X, n_samples, **sample_kwargs
)
except NotFittedError:
warnings.warn(
f"Since the 'estimator' could not be fitted when"
f" calling the `fit` method, the label "
f"mean `_label_mean={self._label_mean}` and optionally the "
f"label standard deviation `_label_std={self._label_std}` is "
f"used to make the predictions."
)
random_state = sample_kwargs.get("random_state", None)
random_state = check_random_state(random_state)
check_scalar(
n_samples,
"n_samples",
min_val=1,
min_inclusive=True,
target_type=int,
)
y_samples = random_state.randn(len(X), n_samples)
y_samples *= self._label_std
y_samples += self._label_mean
return y_samples
def __sklearn_is_fitted__(self):
if "is_fitted_" in self.__dict__:
return True
try:
check_is_fitted(self.estimator)
except NotFittedError:
return False
target_spec = self._resolve_prefitted_target_spec()
# set attributes that would be set by the fit function
self.is_fitted_ = True
self.target_spec_ = target_spec
self._label_mean = 0
self._label_std = 1
self.estimator_ = deepcopy(self.estimator)
self.check_X_dict_ = {
"ensure_min_samples": 0,
"ensure_min_features": 0,
"allow_nd": True,
"dtype": None,
}
return True
def _resolve_prefitted_target_spec(self):
target_type = (
"single-output" if self.target_type == "auto" else self.target_type
)
target_spec = TargetSpec(
task="regression",
target_type=target_type,
annotation_type="single-annotator",
classes=None,
)
_check_target_spec_capability(
type(self).__name__, target_spec, self._target_capabilities
)
estimator_target_spec = getattr(self.estimator, "target_spec_", None)
if estimator_target_spec is None:
estimator_target_type = self._prefitted_estimator_target_type(
self.estimator
)
if estimator_target_type is None:
raise ValueError(
"Cannot establish a single-output target specification "
"from the pre-fitted estimator. The estimator must expose "
"`target_spec_` or fitted output metadata."
)
estimator_target_spec = TargetSpec(
task="regression",
target_type=estimator_target_type,
annotation_type="single-annotator",
classes=None,
)
elif not isinstance(estimator_target_spec, TargetSpec):
raise ValueError(
"The pre-fitted estimator's `target_spec_` must be a "
"`TargetSpec`."
)
_check_target_spec_capability(
type(self).__name__,
estimator_target_spec,
self._target_capabilities,
)
return target_spec
@classmethod
def _prefitted_estimator_target_type(cls, estimator):
target_tags = get_tags(estimator).target_tags
if not target_tags.multi_output:
return "single-output"
if not target_tags.single_output:
return "multi-output"
n_outputs = getattr(estimator, "n_outputs_", None)
if n_outputs is not None:
return "single-output" if n_outputs == 1 else "multi-output"
# Dual coefficients have one column per prediction output, just like
# stored training targets (e.g., for kernel ridge regression).
for attr in ("y_train_", "_y", "dual_coef_"):
target_values = getattr(estimator, attr, None)
if target_values is not None:
target_values = np.asarray(target_values)
is_single_output = (
target_values.ndim == 1
or target_values.ndim == 2
and target_values.shape[1] == 1
)
return "single-output" if is_single_output else "multi-output"
intercept = getattr(estimator, "intercept_", None)
if intercept is not None:
intercept = np.asarray(intercept)
if intercept.ndim > 0:
return (
"single-output" if intercept.size == 1 else "multi-output"
)
coefficients = np.asarray(getattr(estimator, "coef_", []))
if coefficients.ndim == 2:
return (
"single-output"
if coefficients.shape[0] == 1
else "multi-output"
)
return "single-output"
steps = getattr(estimator, "steps", None)
if steps:
return cls._prefitted_estimator_target_type(steps[-1][1])
return None
def __getattr__(self, item):
if item in self._own_fitted_attributes:
return _resolve_own_fitted_attribute(self, item)
if "estimator_" in self.__dict__:
return getattr(self.estimator_, item)
else:
return getattr(self.estimator, item)
[docs]
class SklearnNormalRegressor(ProbabilisticRegressor, SklearnRegressor):
"""Sklearn Normal Regressor
Implementation of a wrapper class for scikit-learn probabilistic regressors
such that missing labels can be handled and the target distribution can be
estimated. Therefore, samples with missing values are filtered and a normal
distribution is fitted using the predicted means and standard deviations.
The wrapped regressor of sklearn needs `return_std` as a keyword argument
for `predict`.
Parameters
----------
estimator : sklearn.base.RegressorMixin with predict method
scikit-learn regressor.
missing_label : scalar or string or np.nan or None, default=np.nan
Value to represent a missing label.
random_state : int or RandomState instance or None, default=None
Determines random number for `predict` method. Pass an int for
reproducible results across multiple method calls.
target_type : "auto" or "single-output", default="auto"
Declared target type. This wrapper supports only single-output
regression.
"""
def __init__(
self,
estimator,
missing_label=MISSING_LABEL,
random_state=None,
target_type="auto",
):
super().__init__(
estimator,
missing_label=missing_label,
random_state=random_state,
target_type=target_type,
)
def _fit(self, fit_function, X, y, sample_weight, **fit_kwargs):
"""Reject an estimator without `return_std`, then fit as usual.
The rejection precedes `SklearnRegressor._fit` and therefore the
transaction it opens. It writes nothing itself, so the wrapper is
equally untouched by it, and every later failure is rolled back by the
inherited transaction. See `SklearnRegressor._fit` for the parameters.
"""
if (
hasattr(self.estimator, "predict")
and "return_std"
not in inspect.signature(self.estimator.predict).parameters.keys()
and inspect.getfullargspec(self.estimator.predict).varkw is None
):
raise ValueError(
f"`{self.estimator}` must have keyword argument"
f"`return_std` for predict."
)
return super()._fit(fit_function, X, y, sample_weight, **fit_kwargs)
[docs]
def predict_target_distribution(self, X):
"""Returns the estimated target normal distribution conditioned on the
test samples `X`.
Parameters
----------
X : array-like of shape (n_samples, n_features)
Input samples.
Returns
-------
dist : scipy.stats._distn_infrastructure.rv_frozen
The distribution of the targets at the test samples.
"""
check_is_fitted(self)
try:
loc, scale = SklearnRegressor.predict(self, X, return_std=True)
return norm(loc=loc, scale=scale)
except TypeError as e:
if (
"predict() got an unexpected keyword argument 'return_std'"
in str(e)
):
raise ValueError(
"SklearnNormalRegressors require the Regressor from"
"`sklearn` to accept `return_std`."
) from e
if successful_skorch_torch_import:
[docs]
class SkorchRegressor(SkactivemlRegressor, SkorchMixin):
"""SkorchRegressor
Implement a regression wrapper class, to make it possible to use
`torch` with `skactiveml`. This is achieved by providing a wrapper
around `torch` that has a `skactiveml` interface and can handle
missing labels. This wrapper is based on the open-source library
`skorch` [1]_.
Notes
-----
Adjust your `criterion` and `module.forward` outputs consistently.
See the documentation of the parameters `forward_outputs` and
`criterion_output_keys` for further details.
Parameters
----------
module : torch.nn.Module.__class__ or torch.nn.Module
A PyTorch `torch.nn.Module`. In general, the uninstantiated class
should be passed, although instantiated modules will also work.
criterion : torch.nn.Module or torch.nn.Module.__class__, \
default=torch.nn.MSELoss
The loss (criterion) used to optimize the module. A concrete
default is given because regressors support a single target
type, whereas `SkorchClassifier` defers its criterion until
the target type is known.
- If a class (subclass of `torch.nn.Module`) is passed
(e.g. `torch.nn.MSELoss`), it is instantiated
internally.
- If an instance is passed (e.g. `torch.nn.MSELoss()`),
that instance (or a wrapped copy of it) is used.
By default, `torch.nn.MSELoss` is used as criterion.
forward_outputs : dict[str, tuple[int, Callable | None]] or None,\
default=None
Dictionary that describes how to get and post-process the outputs
of `module.forward` for prediction. This parameter replaces the
functionality of `predict_nonlinearity` in a `skorch.net.NeuralNet`
(see documentation of `neural_net_param_dict`).
Given `raw_outputs = module.forward(x)`, each entry
`name -> (idx, transform)` in `forward_outputs` is interpreted as:
- `idx` : int
Index into `raw_outputs` (0-based).
- `transform` : callable or `None`
If not `None`, it is applied to the selected raw tensor
`raw_outputs[idx]`. Otherwise, the raw tensor is used.
This allows multiple named outputs to reference the same raw tensor
with different transforms, for example::
forward_outputs = {
"raw-pred": (0, None), # raw predicted targets
"log-pred": (0, torch.log), # log predicted targets
"emb": (1, None), # embeddings
}
The first entry in `forward_outputs` defines the primary
scores used for prediction:
- In `predict`, the transformed first output is interpreted as
predicted targets.
If ``forward_outputs`` is ``None``, a single-output module is
assumed to already produce values in the target space, and the
effective mapping is::
{"output": (0, None)}
The mapping does not have to describe the shape of the predicted
targets. `predict` narrows them to one value per sample itself,
so a module returning a column is handled like one returning a
flat array.
criterion_output_keys : str or sequence of str or None, default=None
Name or names of the forward outputs that are passed to the
loss / criterion during training. Use this when
`module.forward` returns multiple outputs
(e.g. `(logits, embeddings, ...)`), but the criterion expects
a single tensor input or a specific tuple of inputs.
The names must refer to keys of the effective `forward_outputs`
mapping. If `criterion_output_keys` is not `None` and
`forward_outputs` is `None`, a `ValueError` is raised
because the names cannot be resolved.
- If a `str`, the corresponding named output of
`module.forward` (i.e., the raw tensor selected via its
index in `forward_outputs` before applying the transform)
is passed to the criterion (e.g. `"raw-pred"` to use only the
raw predicted targets).
- If a sequence of `str`, the selected named outputs are passed to
the criterion in that order. Each raw forward output index may
appear at most once: using multiple names that resolve to the
same underlying index (e.g. `"raw-pred"` and `"log-pred"` both
pointing to index 0) is not allowed and results in a
`ValueError`.
- If `None`, the first output defined by the effective
`forward_outputs` mapping is used as criterion input.
To pass all distinct forward outputs to the criterion in the
same order as `forward_outputs`, choose one representative name
per raw output index and set, for example::
# assuming that each key refers to a different raw index
criterion_output_keys = tuple(forward_outputs.keys())
If `forward_outputs` contains multiple names that refer to the
same raw output index (aliases such as `"raw-pred"` and`"log-pred"`
both mapping to index 0), you must select at most one name per
raw index in `criterion_output_keys`.
neural_net_param_dict : dict, default=None
Additional arguments for `skorch.net.NeuralNet`. If
`neural_net_param_dict` is `None`, no additional arguments are
added.
sample_dtype : str or type, default=np.float32
Dtype to which input samples are cast inside the estimator. If set
to `None`, the input dtype is preserved. The label data type is
always cast to `np.float32`.
include_unlabeled_samples : bool, default=False
- If `False`, only labeled samples are passed to the `fit` method
of the `estimator`.
- If `True`, all samples including the unlabeled ones are passed to
the `fit` method of the `estimator`. Ensure that the `criterion`
is able to handle unlabeled samples marked by `missing_label`.
Otherwise, `missing_label` is interpreted as a regular target
value.
missing_label : scalar or string or np.nan or None, default=np.nan
Value to represent a missing label.
random_state : int or RandomState instance or None, default=None
Determines random number for 'predict' method. Pass an int for
reproducible results across multiple method calls.
target_type : "auto" or "single-output", default="auto"
Declared target type. This wrapper supports only single-output
regression.
References
----------
.. [1] Marian Tietz, Thomas J. Fan, Daniel Nouri, Benjamin Bossan, and
skorch Developers. skorch: A scikit-learn compatible neural network
library that wraps PyTorch, July 2017.
"""
def __init__(
self,
module,
criterion=nn.MSELoss,
forward_outputs=None,
criterion_output_keys=None,
neural_net_param_dict=None,
sample_dtype=np.float32,
include_unlabeled_samples=False,
missing_label=MISSING_LABEL,
random_state=None,
target_type="auto",
):
super(SkorchRegressor, self).__init__(
missing_label=missing_label,
random_state=random_state,
target_type=target_type,
)
self.module = module
self.criterion = criterion
self.forward_outputs = forward_outputs
self.criterion_output_keys = criterion_output_keys
self.neural_net_param_dict = neural_net_param_dict
self.include_unlabeled_samples = include_unlabeled_samples
self.sample_dtype = sample_dtype
[docs]
def fit(self, X, y, **fit_params):
"""Initialize and fit the module.
If the module was already initialized, by calling fit, the module
will be re-initialized (unless `warm_start` is True).
Parameters
----------
X : matrix-like, shape (n_samples, n_features)
Training data set, usually complete, i.e. including the labeled
and unlabeled samples
y : array-like of shape (n_samples,)
Labels of the training data set (possibly including unlabeled
ones indicated by self.missing_label)
fit_params : dict-like
Further parameters as input to the 'fit' method of the
`skorch.net.NeuralNet`.
Returns
-------
self: SkorchRegressor,
`SkorchRegressor` fitted on the training data.
"""
return self._fit("fit", X, y, **fit_params)
[docs]
def partial_fit(self, X, y, **fit_params):
"""Fit the module without re-initialization.
If the module was already initialized, by calling `partial_fit`,
the module will not be re-initialized again.
Parameters
----------
X : matrix-like, shape (n_samples, n_features)
Training data set, usually complete, i.e. including the labeled
and unlabeled samples
y : array-like of shape (n_samples, )
Labels of the training data set (possibly including unlabeled
ones indicated by `self.missing_label`)
fit_params : dict-like
Further parameters as input to the 'partial_fit' method of the
`skorch.net.NeuralNet`.
Returns
-------
self: SkorchRegressor,
`SkorchRegressor` object fitted on the training data.
"""
return self._fit("partial_fit", X, y, **fit_params)
[docs]
def predict(self, X, extra_outputs=None):
"""Return predicted targets for the test data `X`.
By default, this method returns only the predicted targets
`y_pred`. If `extra_outputs` is provided, a tuple is returned whose
first element is `y_pred` and whose remaining elements are the
requested additional forward outputs, in the order specified by
`extra_outputs`.
Parameters
----------
X : array-like of shape (n_samples, ...)
Test samples.
extra_outputs : None or str or sequence of str, default=None
Names of additional outputs to return next to `y_pred`. The
names must be a subset of the keys of the effective
`forward_outputs` mapping.
For example, if::
self.forward_outputs = {
"raw-pred": (0, None),
"log-pred": (0, None),
"emb": (1, None),
}
then valid values for `extra_outputs` include `"emb"` or
`["emb", "log-pred"]`.
- If `extra_outputs is None`, only `y_pred` is returned.
- If `extra_outputs` is a string, e.g. `"emb"`, the
return value is `(y_pred, emb)`.
- If `extra_outputs` is a sequence of strings, the return
value is `(y_pred, out_1, out_2, ...)`, where `out_i`
corresponds to the i-th name in `extra_outputs`.
Returns
-------
y_pred : numpy.ndarray of shape (n_samples,)
Predicted targets of the test samples.
*extras : numpy.ndarray, optional
Additional outputs. Only present if `extra_outputs` is not
`None`. In that case, the method returns a single tuple whose
first element is `y_pred` and whose remaining elements
(`extras`) correspond to the requested forward outputs in the
order given by `extra_outputs`.
"""
# Initialize module, if not done yet.
if not hasattr(self, "neural_net_"):
self.initialize()
# Check input parameters.
X = check_array(X, **self.check_X_dict_)
check_n_features(
self, X, reset=not hasattr(self, "n_features_in_")
)
# Resolve effective forward_outputs (either user-provided or
# defaulted based on the criterion).
forward_outputs = self._effective_forward_outputs()
# Forward propagation whose return values depends on the request
# ones.
prediction = self._forward_with_named_outputs(
X, forward_outputs=forward_outputs, extra_outputs=extra_outputs
)
# The module decides the shape of its outputs, so the predicted
# target values are narrowed to the declared target type.
return _narrow_to_single_output_prediction(self, prediction)
def _effective_forward_outputs(self):
"""Return the effective `forward_outputs` mapping.
If the user did not specify `forward_outputs`, choose a reasonable
default for common criteria (e.g., `nn.MSELoss`) and a
simple single-output module.
The returned mapping has the form::
{name: (idx, transform)}
where `idx` is the index into the tuple returned by
`module.forward` (0-based) and `transform` is a callable or
`None`. For the defaults below, a single-output module is assumed,
i.e., `idx == 0`.
"""
# User explicitly provided a mapping: trust it.
if self.forward_outputs is not None:
return self.forward_outputs
# The single forward output is treated as already being in the
# desired target space. Its shape is not the mapping's concern:
# `predict` narrows the predicted target values itself, and the
# transforms are ignored while training.
return {"output": (0, None)}
def _net_parts(self, X=None, y=None):
"""Assemble and validate network components.
Implementations should perform any optional checks or normalization
of constructor/init parameters (e.g., shape consistency, dtype
checks, wrapping criteria), then return the ready-to-use pieces for
`skorch.NeuralNet`.
Parameters
----------
X : array-like of shape (n_samples, ...), default=None
Input samples for optional validation.
y : array-like of shape (n_samples, ...), default=None
Target values for optional validation.
Returns
-------
module : torch.nn.Module.__class__ or torch.nn.Module
A PyTorch `torch.nn.Module`. In general, the uninstantiated
class should be passed, although instantiated modules will also
work.
criterion : torch.nn.Module.__class__
The uninitialized criterion (loss) used to optimize the module.
predict_nonlinearity : Callable
The nonlinearity to be applied to the prediction.
params : dict
Keyword arguments (excluding `predict_non_linearity`) for
`skorch.NeuralNet` construction. Must be a mapping and may be
empty.
"""
criterion = self.criterion
criterion = make_criterion_tuple_aware(
criterion=criterion,
criterion_output_keys=self.criterion_output_keys,
forward_outputs=self._effective_forward_outputs(),
)
return (
self.module,
criterion,
self.neural_net_param_dict or {},
)
def _validate_data_kwargs(self):
"""Return kwargs forwarded to `_validate_data`.
Returns
-------
kwargs : dict or None
Keyword arguments consumed by `_validate_data`.
"""
self.check_X_dict_ = {
"ensure_min_samples": 0,
"ensure_min_features": 0,
"allow_nd": True,
"dtype": self.sample_dtype,
}
check_type(
self.include_unlabeled_samples,
"include_unlabeled_samples",
bool,
)
return {"check_X_dict": self.check_X_dict_}
def _return_training_data(self, X, y):
"""
Return only samples and labels required for training.
Parameters
----------
X : array-like of shape (n_samples, ...)
Input samples.
y : array-like of shape (n_samples, ...)
Targets with unlabeled entries following the subclass'
convention.
Returns
-------
X_train : ndarray or None
Training samples or `None` if none exist.
y_train : ndarray or None
Training labels or `None` if none exist.
"""
X_train, y_train = None, None
if self.include_unlabeled_samples:
is_included = np.full_like(y, fill_value=True, dtype=bool)
else:
is_included = is_labeled(y, missing_label=self.missing_label_)
if np.sum(is_included) > 0:
X_train = X[is_included]
y_train = y[is_included]
if y_train is not None:
y_train = y_train.astype(np.float32, copy=True).reshape(-1, 1)
return X_train, y_train