-
Notifications
You must be signed in to change notification settings - Fork 917
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/probabilistic ensemble #1692
Changes from 28 commits
406b4d0
b3a4d52
a840eb5
e3dbed5
de5c096
b8ac4ae
0cc9b3b
b0d46b2
e61c89f
0116f83
36fd3a9
f085dbd
396d852
6a16737
2ed3928
d9b0121
d5f0a39
c4a27ae
cb43cf8
53ed564
5238632
5baa581
09e0087
5bceba8
69a7455
f565778
3beee65
f07f416
02dab52
23e399a
cc71a6d
d0dd636
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,9 @@ | |
|
||
logger = get_logger(__name__) | ||
|
||
# arbitrary threshold to raise warning for probabilistic forecasting models | ||
madtoinou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
SAMPLES_WARNING_THRESHOLD = 1e6 | ||
|
||
|
||
class RegressionEnsembleModel(EnsembleModel): | ||
def __init__( | ||
|
@@ -28,9 +31,11 @@ def __init__( | |
], | ||
regression_train_n_points: int, | ||
regression_model=None, | ||
regression_train_num_samples: Optional[int] = 1, | ||
regression_train_samples_reduction: Optional[Union[str, float]] = "median", | ||
): | ||
""" | ||
Use a regression model for ensembling individual models' predictions. | ||
Use a regression model for ensembling individual models' predictions using the stacking technique [1]_. | ||
madtoinou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
The provided regression model must implement ``fit()`` and ``predict()`` methods | ||
(e.g. scikit-learn regression models). Note that here the regression model is used to learn how to | ||
|
@@ -47,8 +52,26 @@ def __init__( | |
regression_model | ||
Any regression model with ``predict()`` and ``fit()`` methods (e.g. from scikit-learn) | ||
Default: ``darts.model.LinearRegressionModel(fit_intercept=False)`` | ||
|
||
.. note:: | ||
if `regression_model` is probabilistic, the `RegressionEnsembleModel` will also be probabilistic. | ||
.. | ||
regression_train_num_samples | ||
Number of prediction samples from each forecasting model to train the regression model (samples are | ||
averaged). Should be set to 1 for deterministic models. Default: 1. | ||
regression_train_samples_reduction | ||
madtoinou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
If `forecasting models` are probabilistic and `regression_train_num_samples` > 1, method used to | ||
reduce the samples before passing them to the regression model. Possible values: "mean", "median" | ||
or float value corresponding to the desired quantile. Default: "median" | ||
References | ||
---------- | ||
.. [1] D. H. Wolpert, “Stacked generalization”, Neural Networks, vol. 5, no. 2, pp. 241–259, Jan. 1992 | ||
""" | ||
super().__init__(forecasting_models) | ||
super().__init__( | ||
forecasting_models, | ||
train_num_samples=regression_train_num_samples, | ||
train_samples_reduction=regression_train_samples_reduction, | ||
) | ||
if regression_model is None: | ||
regression_model = LinearRegressionModel( | ||
lags=None, lags_future_covariates=[0], fit_intercept=False | ||
|
@@ -69,6 +92,52 @@ def __init__( | |
f"{regression_model.lags}", | ||
) | ||
|
||
raise_if( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we move all those tests to the base class? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, there will be a small discrepancies since the name of the argument/attributes are slightly different between the two classes ( |
||
regression_train_num_samples > 1 and not self._models_are_probabilistic(), | ||
madtoinou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"`regression_train_num_samples` is greater than 1 but the `RegressionEnsembleModel` " | ||
"contains at least one non-probabilistic forecasting model.", | ||
logger, | ||
) | ||
|
||
# check the reduction method | ||
supported_reduction = ["mean", "median"] | ||
if isinstance(regression_train_samples_reduction, float): | ||
# this is already checked by `ts.quantile()`, maybe too redundant | ||
raise_if( | ||
regression_train_samples_reduction > 1.0 | ||
or regression_train_samples_reduction < 0, | ||
madtoinou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
f"`regression_train_samples_reduction` should be comprised between " | ||
f"0 and 1 ({regression_train_samples_reduction}).", | ||
madtoinou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
logger, | ||
) | ||
elif isinstance(regression_train_samples_reduction, str): | ||
raise_if( | ||
regression_train_samples_reduction not in supported_reduction, | ||
f"`regression_train_samples_reduction` should be one of {supported_reduction}, " | ||
f"received ({regression_train_samples_reduction})", | ||
madtoinou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
logger, | ||
) | ||
else: | ||
logger.exception( | ||
madtoinou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
f"`regression_train_samples_reduction` type not supported " | ||
f"({regression_train_samples_reduction}). Must be `float` " | ||
f" or one of {supported_reduction}." | ||
) | ||
|
||
if ( | ||
regression_train_num_samples | ||
* regression_train_n_points | ||
* len(forecasting_models) | ||
> SAMPLES_WARNING_THRESHOLD | ||
madtoinou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
): | ||
logger.warning( | ||
madtoinou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
f"Considering the number of models present in this ensemble ({len(forecasting_models)}), " | ||
f"`regression_train_n_points` ({regression_train_n_points}) and `regression_train_num_samples` " | ||
f"({regression_train_num_samples}) the number of sampled values to train the regression model " | ||
f"will be very large ({regression_train_num_samples*regression_train_n_points*len(forecasting_models)}" | ||
f">{SAMPLES_WARNING_THRESHOLD})." | ||
) | ||
|
||
self.regression_model = regression_model | ||
self.train_n_points = regression_train_n_points | ||
|
||
|
@@ -101,7 +170,7 @@ def fit( | |
|
||
raise_if( | ||
train_n_points_too_big, | ||
"regression_train_n_points parameter too big (must be smaller or " | ||
"`regression_train_n_points` parameter too big (must be smaller or " | ||
"equal to the number of points in training_series)", | ||
logger, | ||
) | ||
|
@@ -126,9 +195,13 @@ def fit( | |
series=forecast_training, | ||
past_covariates=past_covariates, | ||
future_covariates=future_covariates, | ||
num_samples=1, | ||
num_samples=self.train_num_samples, | ||
) | ||
|
||
# component-wise reduction of the probabilistic forecasting models predictions | ||
if predictions[0].n_samples > 1: | ||
predictions = self._predictions_reduction(predictions) | ||
|
||
# train the regression model on the individual models' predictions | ||
self.regression_model.fit( | ||
series=regression_target, future_covariates=predictions | ||
|
@@ -152,6 +225,7 @@ def ensemble( | |
self, | ||
predictions: Union[TimeSeries, Sequence[TimeSeries]], | ||
series: Optional[Sequence[TimeSeries]] = None, | ||
num_samples: int = 1, | ||
) -> Union[TimeSeries, Sequence[TimeSeries]]: | ||
|
||
is_single_series = isinstance(series, TimeSeries) or series is None | ||
|
@@ -160,7 +234,10 @@ def ensemble( | |
|
||
ensembled = [ | ||
self.regression_model.predict( | ||
n=len(prediction), series=serie, future_covariates=prediction | ||
n=len(prediction), | ||
series=serie, | ||
future_covariates=prediction, | ||
num_samples=num_samples, | ||
) | ||
for serie, prediction in zip(series, predictions) | ||
] | ||
|
@@ -179,3 +256,10 @@ def extreme_lags( | |
]: | ||
extreme_lags_ = super().extreme_lags | ||
return (extreme_lags_[0] - self.train_n_points,) + extreme_lags_[1:] | ||
|
||
def _is_probabilistic(self) -> bool: | ||
""" | ||
A RegressionEnsembleModel is probabilistic if its regression | ||
model is probabilistic (ensembling layer) | ||
""" | ||
return self.regression_model._is_probabilistic() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is true for naive ensemble but not for RegressionEnsembleModel, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, the docstring is different in
RegressionEnsembleModel
. This note could probably be removed sinceEnsembleModel
cannot be instantiated anyway.