Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 91 additions & 70 deletions aeon/base/_estimators/interval_based/base_interval_forest.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ class BaseIntervalForest(ABC):
----------
base_estimator : BaseEstimator or None, default=None
scikit-learn BaseEstimator used to build the interval ensemble. If None, use a
simple decision tree.
simple decision tree, i.e. ``DecisionTreeClassifier(criterion="entropy")`` for
classification and ``DecisionTreeRegressor(criterion="squared_error")`` for
regression. Versions prior to and including 1.5.0 used
``DecisionTreeRegressor(criterion="absolute_error")`` for regression by
default, which scales poorly with the number of cases and can be orders of
magnitude slower for larger datasets. Pass such an estimator explicitly to
restore the old behaviour.
n_estimators : int, default=200
Number of estimators to build for the ensemble.
interval_selection_method : "random", "supervised" or "random-supervised",
Expand Down Expand Up @@ -225,21 +231,34 @@ def _fit(self, X, y):

return self

def _eval_estimators(self, tasks):
"""Run a list of joblib ``delayed`` tasks over the estimators.

Runs sequentially when ``n_jobs == 1`` to avoid the joblib dispatch
overhead (a ``Parallel`` object plus per-task wrapping) for what is the
default and dominant case, otherwise runs them in parallel. The tasks
must be built as a list so that any random draws in their arguments
happen in the same order joblib would consume a generator, keeping the
result identical to the parallel path.
"""
if self._n_jobs == 1:
return [func(*args, **kwargs) for func, args, kwargs in tasks]
return Parallel(n_jobs=self._n_jobs, backend=self.parallel_backend)(tasks)

def _predict(self, X):
if is_regressor(self):
Xt = self._predict_setup(X)

y_preds = Parallel(
n_jobs=self._n_jobs,
backend=self.parallel_backend,
)(
delayed(self._predict_for_estimator)(
Xt,
self.estimators_[i],
self.intervals_[i],
predict_proba=False,
)
for i in range(self._n_estimators)
y_preds = self._eval_estimators(
[
delayed(self._predict_for_estimator)(
Xt,
self.estimators_[i],
self.intervals_[i],
predict_proba=False,
)
for i in range(self._n_estimators)
]
)

return np.mean(y_preds, axis=0)
Expand All @@ -251,14 +270,16 @@ def _predict(self, X):
def _predict_proba(self, X):
Xt = self._predict_setup(X)

y_probas = Parallel(n_jobs=self._n_jobs, backend=self.parallel_backend)(
delayed(self._predict_for_estimator)(
Xt,
self.estimators_[i],
self.intervals_[i],
predict_proba=True,
)
for i in range(self._n_estimators)
y_probas = self._eval_estimators(
[
delayed(self._predict_for_estimator)(
Xt,
self.estimators_[i],
self.intervals_[i],
predict_proba=True,
)
for i in range(self._n_estimators)
]
)

output = np.sum(y_probas, axis=0) / (
Expand All @@ -272,14 +293,16 @@ def _fit_predict(self, X, y) -> np.ndarray:
if is_regressor(self):
Xt = self._fit_forest(X, y, save_transformed_data=True)

p = Parallel(n_jobs=self._n_jobs, backend=self.parallel_backend)(
delayed(self._train_estimate_for_estimator)(
Xt,
y,
i,
check_random_state(rng.randint(np.iinfo(np.int32).max)),
)
for i in range(self._n_estimators)
p = self._eval_estimators(
[
delayed(self._train_estimate_for_estimator)(
Xt,
y,
i,
check_random_state(rng.randint(np.iinfo(np.int32).max)),
)
for i in range(self._n_estimators)
]
)
y_preds, oobs = zip(*p)

Expand Down Expand Up @@ -314,15 +337,17 @@ def _fit_predict_proba(self, X, y) -> np.ndarray:

rng = check_random_state(self.random_state)

p = Parallel(n_jobs=self._n_jobs, backend=self.parallel_backend)(
delayed(self._train_estimate_for_estimator)(
Xt,
y,
i,
check_random_state(rng.randint(np.iinfo(np.int32).max)),
probas=True,
)
for i in range(self._n_estimators)
p = self._eval_estimators(
[
delayed(self._train_estimate_for_estimator)(
Xt,
y,
i,
check_random_state(rng.randint(np.iinfo(np.int32).max)),
probas=True,
)
for i in range(self._n_estimators)
]
)
y_probas, oobs = zip(*p)

Expand Down Expand Up @@ -352,7 +377,7 @@ def _fit_forest(self, X, y, save_transformed_data=False):
if is_classifier(self):
self._base_estimator = DecisionTreeClassifier(criterion="entropy")
elif is_regressor(self):
self._base_estimator = DecisionTreeRegressor(criterion="absolute_error")
self._base_estimator = DecisionTreeRegressor(criterion="squared_error")
else:
raise ValueError(
f"{self} must be a scikit-learn compatible classifier or "
Expand Down Expand Up @@ -520,7 +545,7 @@ def _fit_forest(self, X, y, save_transformed_data=False):
or self.max_interval_length == np.inf
):
self._max_interval_length = [self.max_interval_length] * len(Xt)
# max_interval_length must be at less than one if it is a float (proportion of
# max_interval_length must be less than one if it is a float (proportion
# of the series length)
elif (
isinstance(self.max_interval_length, float)
Expand Down Expand Up @@ -610,7 +635,7 @@ def _fit_forest(self, X, y, save_transformed_data=False):
self._interval_function[i] = True
else:
raise ValueError(
"Individual items in a interval_features list or "
"Individual items in an interval_features list or "
"tuple must be a transformer or function. Input "
f"{feature} does not contain only transformers and "
f"functions."
Expand All @@ -625,7 +650,7 @@ def _fit_forest(self, X, y, save_transformed_data=False):
self._interval_features.append([feature])
else:
raise ValueError(
"Individual items in a interval_features list or tuple "
"Individual items in an interval_features list or tuple "
f"must be a transformer or function. Found {feature}"
)
# use basic summary stats by default if None
Expand All @@ -646,8 +671,8 @@ def _fit_forest(self, X, y, save_transformed_data=False):
)

self._att_subsample_size = [self.att_subsample_size] * len(Xt)
# att_subsample_size must be at less than one if it is a float (proportion of
# total attributed to subsample)
# att_subsample_size must be less than one if it is a float (proportion of
# total attributes to subsample)
elif isinstance(self.att_subsample_size, float):
if self.att_subsample_size > 1 or self.att_subsample_size <= 0:
raise ValueError(
Expand Down Expand Up @@ -804,17 +829,16 @@ def _fit_forest(self, X, y, save_transformed_data=False):
train_time < time_limit
and self._n_estimators < self.contract_max_n_estimators
):
fit = Parallel(
n_jobs=self._n_jobs,
backend=self.parallel_backend,
)(
delayed(self._fit_estimator)(
Xt,
y,
rng.randint(np.iinfo(np.int32).max),
save_transformed_data=save_transformed_data,
)
for _ in range(self._n_jobs)
fit = self._eval_estimators(
[
delayed(self._fit_estimator)(
Xt,
y,
rng.randint(np.iinfo(np.int32).max),
save_transformed_data=save_transformed_data,
)
for _ in range(self._n_jobs)
]
)

(
Expand All @@ -832,17 +856,16 @@ def _fit_forest(self, X, y, save_transformed_data=False):
else:
self._n_estimators = self.n_estimators

fit = Parallel(
n_jobs=self._n_jobs,
backend=self.parallel_backend,
)(
delayed(self._fit_estimator)(
Xt,
y,
rng.randint(np.iinfo(np.int32).max),
save_transformed_data=save_transformed_data,
)
for _ in range(self._n_estimators)
fit = self._eval_estimators(
[
delayed(self._fit_estimator)(
Xt,
y,
rng.randint(np.iinfo(np.int32).max),
save_transformed_data=save_transformed_data,
)
for _ in range(self._n_estimators)
]
)

(
Expand Down Expand Up @@ -1063,11 +1086,9 @@ def _predict_setup(self, X):
return Xt

def _predict_for_estimator(self, Xt, estimator, intervals, predict_proba=False):
interval_features = np.empty((Xt[0].shape[0], 0))

for r in range(len(Xt)):
f = intervals[r].transform(Xt[r])
interval_features = np.hstack((interval_features, f))
interval_features = np.hstack(
[intervals[r].transform(Xt[r]) for r in range(len(Xt))]
)

if isinstance(self.replace_nan, str) and self.replace_nan.lower() == "nan":
interval_features = np.nan_to_num(
Expand Down
2 changes: 1 addition & 1 deletion aeon/classification/interval_based/_drcif.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ def _get_test_params(cls, parameter_set="default"):
previously generated results where the default set of parameters
cannot produce suitable probability estimates
"contracting" - used in classifiers that set the
"capability:contractable" tag to True to test contacting
"capability:contractable" tag to True to test contracting
functionality
"train_estimate" - used in some classifiers that set the
"capability:train_estimate" tag to True to allow for more efficient
Expand Down
6 changes: 5 additions & 1 deletion aeon/regression/interval_based/_cif.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ class CanonicalIntervalForestRegressor(BaseIntervalForest, BaseRegressor):
----------
base_estimator : BaseEstimator or None, default=None
scikit-learn BaseEstimator used to build the interval ensemble. If None, use a
simple decision tree.
simple decision tree, i.e.
``DecisionTreeRegressor(criterion="squared_error")``. Versions prior to and
including 1.5.0 used ``DecisionTreeRegressor(criterion="absolute_error")``
by default, which scales poorly with the number of cases. Pass such an
estimator explicitly to restore the old behaviour.
n_estimators : int, default=200
Number of estimators to build for the ensemble.
n_intervals : int, str, list or tuple, default="sqrt"
Expand Down
8 changes: 6 additions & 2 deletions aeon/regression/interval_based/_drcif.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ class DrCIFRegressor(BaseIntervalForest, BaseRegressor):
----------
base_estimator : BaseEstimator or None, default=None
scikit-learn BaseEstimator used to build the interval ensemble. If None, use a
simple decision tree.
simple decision tree, i.e.
``DecisionTreeRegressor(criterion="squared_error")``. Versions prior to and
including 1.5.0 used ``DecisionTreeRegressor(criterion="absolute_error")``
by default, which scales poorly with the number of cases. Pass such an
estimator explicitly to restore the old behaviour.
n_estimators : int, default=200
Number of estimators to build for the ensemble.
n_intervals : int, str, list or tuple, default="sqrt"
Expand Down Expand Up @@ -240,7 +244,7 @@ def _get_test_params(cls, parameter_set="default"):
previously generated results where the default set of parameters
cannot produce suitable probability estimates
"contracting" - used in classifiers that set the
"capability:contractable" tag to True to test contacting
"capability:contractable" tag to True to test contracting
functionality

Returns
Expand Down
6 changes: 5 additions & 1 deletion aeon/regression/interval_based/_interval_forest.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ class IntervalForestRegressor(BaseIntervalForest, BaseRegressor):
----------
base_estimator : BaseEstimator or None, default=None
scikit-learn BaseEstimator used to build the interval ensemble. If None, use a
simple decision tree.
simple decision tree, i.e.
``DecisionTreeRegressor(criterion="squared_error")``. Versions prior to and
including 1.5.0 used ``DecisionTreeRegressor(criterion="absolute_error")``
by default, which scales poorly with the number of cases. Pass such an
estimator explicitly to restore the old behaviour.
n_estimators : int, default=200
Number of estimators to build for the ensemble.
interval_selection_method : "random", default="random"
Expand Down
6 changes: 5 additions & 1 deletion aeon/regression/interval_based/_rise.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ class RandomIntervalSpectralEnsembleRegressor(BaseIntervalForest, BaseRegressor)
----------
base_estimator : BaseEstimator or None, default=None
scikit-learn BaseEstimator used to build the interval ensemble. If None, use a
simple decision tree.
simple decision tree, i.e.
``DecisionTreeRegressor(criterion="squared_error")``. Versions prior to and
including 1.5.0 used ``DecisionTreeRegressor(criterion="absolute_error")``
by default, which scales poorly with the number of cases. Pass such an
estimator explicitly to restore the old behaviour.
n_estimators : int, default=200
Number of estimators to build for the ensemble.
min_interval_length : int, float, list, or tuple, default=3
Expand Down
6 changes: 5 additions & 1 deletion aeon/regression/interval_based/_tsf.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ class TimeSeriesForestRegressor(BaseIntervalForest, BaseRegressor):
----------
base_estimator : BaseEstimator or None, default=None
scikit-learn BaseEstimator used to build the interval ensemble. If None, use a
simple decision tree.
simple decision tree, i.e.
``DecisionTreeRegressor(criterion="squared_error")``. Versions prior to and
including 1.5.0 used ``DecisionTreeRegressor(criterion="absolute_error")``
by default, which scales poorly with the number of cases. Pass such an
estimator explicitly to restore the old behaviour.
n_estimators : int, default=200
Number of estimators to build for the ensemble.
n_intervals : int, str, list or tuple, default="sqrt"
Expand Down
Loading
Loading