Repository: aeon-toolkit/aeon
Pull request #545 (2023-07-06 20:37:15)

[ENH] Implement configurable interval base class with classifier and regressor
#### Reference Issues/PRs

Relies on #511

Part of #499.

#### What does this implement/fix? Explain your changes.

Implements a configurable interval forest base class, with a classifier and regressor which use it. This is set up in a way that you could configure any of our current interval based approaches using the base class.

There is a lot going on here, if any additional explanation is required I can provide.

MatthewMiddlehurst: In the classifier/regressor the base class is the leftmost when inheriting, so it should have priority when calling methods. https://www.programiz.com/python-programming/multiple-inheritance

Many of the interval classifiers follow the approach `transform series -> select intervals -> extract features -> build tree` with various forest-like qualities. It made sense just to have a single class that follows that formula, IMO. 
TonyBagnall: > In the classifier/regressor the base class is the leftmost when inheriting, so it should have priority when calling methods. https://www.programiz.com/python-programming/multiple-inheritance
> 
> Many of the interval classifiers follow the approach `transform series -> select intervals -> extract features -> build tree` with various forest-like qualities. It made sense just to have a single class that follows that formula, IMO.

TonyBagnall: sorry, accidentally closed it!

Patch:
diff --git a/aeon/base/estimator/__init__.py b/aeon/base/estimator/__init__.py
new file mode 100644
index 00000000..0c896e49
--- /dev/null
+++ b/aeon/base/estimator/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+"""Base classes for estimators which are part of multiple aeon modules."""
diff --git a/aeon/base/estimator/interval_based/__init__.py b/aeon/base/estimator/interval_based/__init__.py
new file mode 100644
index 00000000..fe14232f
--- /dev/null
+++ b/aeon/base/estimator/interval_based/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# copyright: aeon developers, BSD-3-Clause License (see LICENSE file)
+"""Base classes for interval-based time series estimators."""
+
+__all__ = ["BaseIntervalForest"]
diff --git a/aeon/base/estimator/interval_based/base_interval_forest.py b/aeon/base/estimator/interval_based/base_interval_forest.py
new file mode 100644
index 00000000..91e43c12
--- /dev/null
+++ b/aeon/base/estimator/interval_based/base_interval_forest.py
@@ -0,0 +1,1030 @@
+# -*- coding: utf-8 -*-
+# copyright: aeon developers, BSD-3-Clause License (see LICENSE file)
+"""A base class for interval extracting forest estimators."""
+
+__author__ = ["MatthewMiddlehurst"]
+__all__ = ["BaseIntervalForest"]
+
+import inspect
+import time
+import warnings
+from abc import ABCMeta, abstractmethod
+
+import numpy as np
+from joblib import Parallel, delayed
+from sklearn.base import BaseEstimator, is_classifier, is_regressor
+from sklearn.preprocessing import FunctionTransformer
+from sklearn.tree import BaseDecisionTree, DecisionTreeClassifier, DecisionTreeRegressor
+from sklearn.utils import check_random_state
+
+from aeon.base._base import _clone_estimator
+from aeon.classification.sklearn import ContinuousIntervalTree
+from aeon.transformations.base import BaseTransformer
+from aeon.transformations.collection import RandomIntervals, SupervisedIntervals
+from aeon.utils.numba.stats import row_mean, row_slope, row_std
+from aeon.utils.validation import check_n_jobs
+
+
+class BaseIntervalForest(metaclass=ABCMeta):
+    """A base class for interval extracting forest estimators.
+
+    Allows the implementation of classifiers and regressors along the lines of [1][2][3]
+    which extract intervals and create an ensemble from the subsequent features.
+
+    Parameters
+    ----------
+    base_estimator : BaseEstimator or None, default=None
+        scikit-learn BaseEstimator used to build the interval ensemble. If None, use a
+        simple decision tree.
+    n_estimators : int, default=200
+        Number of estimators to build for the ensemble.
+    interval_selection_method : "random", "supervised" or "random-supervised",
+            default="random"
+        The interval selection transformer to use.
+            - "random" uses a RandomIntervalTransformer.
+            - "supervised" uses a SupervisedIntervalTransformer.
+            - "random-supervised" uses a SupervisedIntervalTransformer with
+                randomised elements.
+
+        Supervised methods can only be used for classification tasks, and require
+        function inputs for interval_features rather than transformers.
+    n_intervals : int, str, list or tuple, default="sqrt"
+        Number of intervals to extract per tree for each series_transformers series.
+
+        An int input will extract that number of intervals from the series, while a str
+        input will return a function of the series length (may differ per
+        series_transformers output) to extract that number of intervals.
+        Valid str inputs are:
+            - "sqrt": square root of the series length.
+            - "sqrt-div": sqrt of series length divided by the number
+                of series_transformers.
+
+        A list or tuple of ints and/or strs will extract the number of intervals using
+        the above rules and sum the results for the final n_intervals. i.e. [4, "sqrt"]
+        will extract sqrt(n_timepoints) + 4 intervals.
+
+        Different number of intervals for each series_transformers series can be
+        specified using a nested list or tuple. Any list or tuple input containing
+        another list or tuple must be the same length as the number of
+        series_transformers.
+
+        While random interval extraction will extract the n_intervals intervals total
+        (removing duplicates), supervised intervals will run the supervised extraction
+        process n_intervals times, returning more intervals than specified.
+    min_interval_length : int, float, list, or tuple, default=3
+        Minimum length of intervals to extract from series. float inputs take a
+        proportion of the series length to use as the minimum interval length.
+
+        Different minimum interval lengths for each series_transformers series can be
+        specified using a list or tuple. Any list or tuple input must be the same length
+        as the number of series_transformers.
+    max_interval_length : int, float, list, or tuple, default=np.inf
+        Maximum length of intervals to extract from series. float inputs take a
+        proportion of the series length to use as the maximum interval length.
+
+        Different maximum interval lengths for each series_transformers series can be
+        specified using a list or tuple. Any list or tuple input must be the same length
+        as the number of series_transformers.
+
+        Ignored for supervised interval_selection_method inputs.
+    interval_features : TransformerMixin, callable, list, tuple, or None, default=None
+        The features to extract from the intervals using transformers or callable
+        functions. If None, use the mean, standard deviation, and slope of the series.
+
+        Both transformers and functions should be able to take a 2D np.ndarray input.
+        Functions should output a 1d array (the feature for each series), and
+        transformers should output a 2d array where rows are the features for each
+        series. A list or tuple of transformers and/or functions will extract all
+        features and concatenate the output.
+
+        Different features for each series_transformers series can be specified using a
+        nested list or tuple. Any list or tuple input containing another list or tuple
+        must be the same length as the number of series_transformers.
+    series_transformers : TransformerMixin, list, tuple, or None, default=None
+        The transformers to apply to the series before extracting intervals. If None,
+        use the series as is.
+
+        Both transformers and functions should be able to take a 3D np.ndarray input.
+        A list or tuple of transformers and/or functions will extract intervals from
+        all transformations concatenate the output. Including None in the list or tuple
+        will use the series as is for interval extraction.
+    att_subsample_size : int, float, list, tuple or None, default=None
+        The number of attributes to subsample for each estimator. If None, use all
+
+        If int, use that number of attributes for all estimators. If float, use that
+        proportion of attributes for all estimators.
+
+        Different subsample sizes for each series_transformers series can be specified
+        using a list or tuple. Any list or tuple input must be the same length as the
+        number of series_transformers.
+    replace_nan : "nan", int, float or None, default=None
+        The value to replace NaNs and infinite values with before fitting the base
+        estimator. int or float input will replace with the specified value, while
+        "nan" will replace infinite values with NaNs. If None, do not replace NaNs.
+    time_limit_in_minutes : int, default=0
+        Time contract to limit build time in minutes, overriding n_estimators.
+        Default of 0 means n_estimators are used.
+    contract_max_n_estimators : int, default=500
+        Max number of estimators when time_limit_in_minutes is set.
+    save_transformed_data : bool, default=False
+        Save the data transformed in fit for use in _get_train_probs.
+    random_state : int, RandomState instance or None, default=None
+        If `int`, random_state is the seed used by the random number generator;
+        If `RandomState` instance, random_state is the random number generator;
+        If `None`, the random number generator is the `RandomState` instance used
+        by `np.random`.
+    n_jobs : int, default=1
+        The number of jobs to run in parallel for both `fit` and `predict`.
+        ``-1`` means using all processors.
+    parallel_backend : str, ParallelBackendBase instance or None, default=None
+        Specify the parallelisation backend implementation in joblib, if None a 'prefer'
+        value of "threads" is used by default.
+        Valid options are "loky", "multiprocessing", "threading" or a custom backend.
+        See the joblib Parallel documentation for more details.
+
+    Attributes
+    ----------
+    n_instances_ : int
+        The number of train cases.
+    n_channels_ : int
+        The number of channels per case.
+    n_timepoints_ : int
+        The length of each series.
+    total_intervals_ : int
+        Total number of intervals per tree from all representations.
+    estimators_ : list of shape (n_estimators) of BaseEstimator
+        The collections of estimators trained in fit.
+    intervals_ : list of shape (n_estimators) of BaseTransformer
+        Stores the interval extraction transformer for all estimators.
+    transformed_data_ : list of shape (n_estimators) of ndarray with shape
+    (n_instances,total_intervals * att_subsample_size)
+        The transformed dataset for all estimators. Only saved when
+        save_transformed_data is true.
+
+    References
+    ----------
+    .. [1] H.Deng, G.Runger, E.Tuv and M.Vladimir, "A time series forest for
+       classification and feature extraction", Information Sciences, 239, 2013
+    .. [2] Matthew Middlehurst and James Large and Anthony Bagnall. "The Canonical
+       Interval Forest (CIF) Classifier for Time Series Classification."
+       IEEE International Conference on Big Data 2020
+    .. [3] Cabello, Nestor, et al. "Fast and Accurate Time Series Classification
+       Through Supervised Interval Search." IEEE ICDM 2020
+    """
+
+    @abstractmethod
+    def __init__(
+        self,
+        base_estimator=None,
+        n_estimators=200,
+        interval_selection_method="random",
+        n_intervals="sqrt",
+        min_interval_length=3,
+        max_interval_length=np.inf,
+        interval_features=None,
+        series_transformers=None,
+        att_subsample_size=None,
+        replace_nan=None,
+        time_limit_in_minutes=None,
+        contract_max_n_estimators=500,
+        save_transformed_data=False,
+        random_state=None,
+        n_jobs=1,
+        parallel_backend=None,
+    ):
+        self.base_estimator = base_estimator
+        self.n_estimators = n_estimators
+        self.interval_selection_method = interval_selection_method
+        self.n_intervals = n_intervals
+        self.min_interval_length = min_interval_length
+        self.max_interval_length = max_interval_length
+        self.interval_features = interval_features
+        self.series_transformers = series_transformers
+        self.att_subsample_size = att_subsample_size
+        self.replace_nan = replace_nan
+        self.time_limit_in_minutes = time_limit_in_minutes
+        self.contract_max_n_estimators = contract_max_n_estimators
+        self.save_transformed_data = save_transformed_data
+        self.random_state = random_state
+        self.n_jobs = n_jobs
+        self.parallel_backend = parallel_backend
+
+        super(BaseIntervalForest, self).__init__()
+
+    # if subsampling attributes, an interval_features transformer must contain a
+    # parameter name from transformer_feature_selection and an attribute name
+    # (or property) from transformer_feature_names to allow features to be subsampled
+    transformer_feature_selection = ["features"]
+    transformer_feature_names = [
+        "features_arguments_",
+        "_features_arguments",
+        "get_features_arguments",
+        "_get_features_arguments",
+    ]
+    # an interval_features transformer must contain one of these attribute names to
+    # be able to skip transforming features in predict
+    transformer_feature_skip = ["transform_features_", "_transform_features"]
+
+    def _fit(self, X, y):
+        rng = check_random_state(self.random_state)
+
+        self.n_instances_, self.n_channels_, self.n_timepoints_ = X.shape
+
+        self._base_estimator = self.base_estimator
+        if self.base_estimator is None:
+            if is_classifier(self):
+                self._base_estimator = DecisionTreeClassifier(criterion="entropy")
+            elif is_regressor(self):
+                self._base_estimator = DecisionTreeRegressor(criterion="absolute_error")
+            else:
+                raise ValueError(
+                    f"{self} must be a scikit-learn compatible classifier or "
+                    "regressor."
+                )
+        # base_estimator must be an sklearn estimator
+        elif not isinstance(self.base_estimator, BaseEstimator):
+            raise ValueError(
+                "base_estimator must be a scikit-learn BaseEstimator or None. "
+                f"Found: {self.base_estimator}"
+            )
+
+        # use the base series if series_transformers is None
+        if self.series_transformers is None or self.series_transformers == []:
+            Xt = [X]
+            self._series_transformers = [None]
+        # clone series_transformers if it is a transformer and transform the input data
+        elif _is_transformer(self.series_transformers):
+            t = _clone_estimator(self.series_transformers, random_state=rng)
+            Xt = [t.fit_transform(X, y)]
+            self._series_transformers = [t]
+        # clone each series_transformers transformer and include the base series if None
+        # is in the list
+        elif isinstance(self.series_transformers, (list, tuple)):
+            Xt = []
+            self._series_transformers = []
+
+            for transformer in self.series_transformers:
+                if transformer is None:
+                    Xt.append(X)
+                    self._series_transformers.append(None)
+                elif _is_transformer(transformer):
+                    t = _clone_estimator(transformer, random_state=rng)
+                    Xt.append(t.fit_transform(X, y))
+                    self._series_transformers.append(t)
+                else:
+                    raise ValueError(
+                        f"Invalid series_transformers list input. Found {transformer}"
+                    )
+        # other inputs are invalid
+        else:
+            raise ValueError(
+                f"Invalid series_transformers input. Found {self.series_transformers}"
+            )
+
+        # if only a single n_intervals value is passed it must be an int or str
+        if isinstance(self.n_intervals, (int, str)):
+            n_intervals = [[self.n_intervals]] * len(Xt)
+        elif isinstance(self.n_intervals, (list, tuple)):
+            # if input is a list and only contains ints or strs, use the list for all
+            # series in Xt
+            if all(isinstance(item, (int, str)) for item in self.n_intervals):
+                n_intervals = [self.n_intervals] * len(Xt)
+            # other lists must be the same length as Xt
+            elif len(self.n_intervals) != len(Xt):
+                raise ValueError(
+                    "n_intervals as a list or tuple containing other lists or tuples "
+                    "must be the same length as series_transformers."
+                )
+            # list items can be a list of items or a single item for each
+            # series_transformer, but each individual item must be an int or str
+            else:
+                n_intervals = []
+                for items in self.n_intervals:
+                    if isinstance(items, (list, tuple)):
+                        if not all(isinstance(item, (int, str)) for item in items):
+                            raise ValueError(
+                                "Individual items in a n_intervals list or tuple must "
+                                f"be an int or str. Input {items} does not contain "
+                                "only ints or strs"
+                            )
+                        n_intervals.append(items)
+                    elif isinstance(items, (int, str)):
+                        n_intervals.append([items])
+                    else:
+                        raise ValueError(
+                            "Individual items in a n_intervals list or tuple must be "
+                            f"an int or str. Found: {items}"
+                        )
+        # other inputs are invalid
+        else:
+            raise ValueError(f"Invalid n_intervals input. Found {self.n_intervals}")
+
+        # add together the number of intervals for each series_transformer
+        # str input must be one of a set valid options
+        self._n_intervals = [0] * len(Xt)
+        for i, series in enumerate(Xt):
+            for method in n_intervals[i]:
+                if isinstance(method, int):
+                    self._n_intervals[i] += method
+                elif isinstance(method, str):
+                    # sqrt of series length
+                    if method.lower() == "sqrt":
+                        self._n_intervals[i] += int(
+                            np.sqrt(series.shape[2]) * np.sqrt(series.shape[1])
+                        )
+                    # sqrt of series length divided by the number of series_transformers
+                    elif method.lower() == "sqrt-div":
+                        self._n_intervals[i] += int(
+                            (np.sqrt(series.shape[2]) * np.sqrt(series.shape[1]))
+                            / len(Xt)
+                        )
+                    else:
+                        raise ValueError(
+                            "Invalid str input for n_intervals. Must be "
+                            f'("sqrt","sqrt-div"). Found {method}'
+                        )
+
+        # each series_transformer must have at least 1 interval extracted
+        for i, n in enumerate(self._n_intervals):
+            if n <= 0:
+                self._n_intervals[i] = 1
+
+        self.total_intervals_ = sum(self._n_intervals)
+
+        # minimum interval length
+        if isinstance(self.min_interval_length, int):
+            self._min_interval_length = [self.min_interval_length] * len(Xt)
+        # min_interval_length must be at less than one if it is a float (proportion of
+        # of the series length)
+        elif (
+            isinstance(self.min_interval_length, float)
+            and self.min_interval_length <= 1
+        ):
+            self._min_interval_length = [
+                int(self.min_interval_length * t.shape[2]) for t in Xt
+            ]
+        # if the input is a list, it must be the same length as the number of
+        # series_transformers
+        # list values must be ints or floats. The same checks as above are performed
+        elif isinstance(self.min_interval_length, (list, tuple)):
+            if len(self.min_interval_length) != len(Xt):
+                raise ValueError(
+                    "min_interval_length as a list or tuple must be the same length "
+                    "as series_transformers."
+                )
+
+            self._min_interval_length = []
+            for i, length in enumerate(self.min_interval_length):
+                if isinstance(length, float) and length <= 1:
+                    self._min_interval_length.append(int(length * Xt[i].shape[2]))
+                elif isinstance(length, int):
+                    self._min_interval_length.append(length)
+                else:
+                    raise ValueError(
+                        "min_interval_length list items must be int or floats. "
+                        f"Found {length}"
+                    )
+        # other inputs are invalid
+        else:
+            raise ValueError(
+                f"Invalid min_interval_length input. Found {self.min_interval_length}"
+            )
+
+        # min_interval_length cannot be less than 3 or greater than the series length
+        for i, n in enumerate(self._min_interval_length):
+            if n > Xt[i].shape[2]:
+                self._min_interval_length[i] = Xt[i].shape[2]
+            elif n < 3:
+                self._min_interval_length[i] = 3
+
+        # maximum interval length
+        if (
+            isinstance(self.max_interval_length, int)
+            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
+        # of the series length)
+        elif (
+            isinstance(self.max_interval_length, float)
+            and self.max_interval_length <= 1
+        ):
+            self._max_interval_length = [
+                int(self.max_interval_length * t.shape[2]) for t in Xt
+            ]
+        # if the input is a list, it must be the same length as the number of
+        # series_transformers
+        # list values must be ints or floats. The same checks as above are performed
+        elif isinstance(self.max_interval_length, (list, tuple)):
+            if len(self.max_interval_length) != len(Xt):
+                raise ValueError(
+                    "max_interval_length as a list or tuple must be the same length "
+                    "as series_transformers."
+                )
+
+            self._max_interval_length = []
+            for i, length in enumerate(self.max_interval_length):
+                if isinstance(length, float) and length <= 1:
+                    self._max_interval_length.append(int(length * Xt[i].shape[2]))
+                elif isinstance(length, int):
+                    self._max_interval_length.append(length)
+                else:
+                    raise ValueError(
+                        "max_interval_length list items must be int or floats. "
+                        f"Found {length}"
+                    )
+        # other inputs are invalid
+        else:
+            raise ValueError(
+                f"Invalid max_interval_length input. Found {self.max_interval_length}"
+            )
+
+        # max_interval_length cannot be less than min_interval_length or greater than
+        # the series length
+        for i, n in enumerate(self._max_interval_length):
+            if n < self._min_interval_length[i]:
+                self._max_interval_length[i] = self._min_interval_length[i]
+            elif n > Xt[i].shape[2]:
+                self._max_interval_length[i] = Xt[i].shape[2]
+
+        # we store whether each series_transformer contains a transformer and/or
+        # function in its interval_features
+        self._interval_transformer = [False] * len(Xt)
+        self._interval_function = [False] * len(Xt)
+        # single transformer or function for all series_transformers
+        if isinstance(self.interval_features, BaseTransformer):
+            self._interval_transformer = [True] * len(Xt)
+            transformer = _clone_estimator(self.interval_features, random_state=rng)
+            self._interval_features = [[transformer]] * len(Xt)
+        elif callable(self.interval_features):
+            self._interval_function = [True] * len(Xt)
+            self._interval_features = [[self.interval_features]] * len(Xt)
+        elif isinstance(self.interval_features, (list, tuple)):
+            # if input is a list and only contains transformers or functions, use the
+            # list for all series in Xt
+            if all(
+                isinstance(item, BaseTransformer) or callable(item)
+                for item in self.interval_features
+            ):
+                for feature in self.interval_features:
+                    if isinstance(feature, BaseTransformer):
+                        self._interval_transformer[0] = True
+                    elif callable(feature):
+                        self._interval_function[0] = True
+                self._interval_features = [self.interval_features] * len(Xt)
+            # other lists must be the same length as Xt
+            elif len(self.interval_features) != len(Xt):
+                raise ValueError(
+                    "interval_features as a list or tuple containing other lists or "
+                    "tuples must be the same length as series_transformers."
+                )
+            # list items can be a list of items or a single item for each
+            # series_transformer, but each individual item must be a transformer
+            # or function
+            else:
+                self._interval_features = []
+                for i, feature in enumerate(self.interval_features):
+                    if isinstance(feature, (list, tuple)):
+                        for method in feature:
+                            if isinstance(method, BaseTransformer):
+                                self._interval_transformer[i] = True
+                                feature = _clone_estimator(feature, random_state=rng)
+                            elif callable(method):
+                                self._interval_function[i] = True
+                            else:
+                                raise ValueError(
+                                    "Individual items in a interval_features list or "
+                                    "tuple must be a transformer or function. Input "
+                                    f"{feature} does not contain only transformers and "
+                                    f"functions."
+                                )
+                        self._interval_features.append(feature)
+                    elif isinstance(feature, BaseTransformer):
+                        self._interval_transformer[i] = True
+                        feature = _clone_estimator(feature, random_state=rng)
+                        self._interval_features.append([feature])
+                    elif callable(feature):
+                        self._interval_function[i] = True
+                        self._interval_features.append([feature])
+                    else:
+                        raise ValueError(
+                            "Individual items in a interval_features list or tuple "
+                            f"must be a transformer or function. Found {feature}"
+                        )
+        # use basic summary stats by default if None
+        elif self.interval_features is None:
+            self._interval_function = [True] * len(Xt)
+            self._interval_features = [[row_mean, row_std, row_slope]] * len(Xt)
+        # other inputs are invalid
+        else:
+            raise ValueError(
+                f"Invalid interval_features input. Found {self.interval_features}"
+            )
+
+        # att_subsample_size must be at least one if it is an int
+        if isinstance(self.att_subsample_size, int):
+            if self.att_subsample_size < 1:
+                raise ValueError(
+                    "att_subsample_size must be at least one if it is an int."
+                )
+
+            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)
+        elif isinstance(self.att_subsample_size, float):
+            if self.att_subsample_size > 1 or self.att_subsample_size <= 0:
+                raise ValueError(
+                    "att_subsample_size must be between 0 and 1 if it is a float."
+                )
+
+            self._att_subsample_size = [self.att_subsample_size] * len(Xt)
+        # default is no attribute subsampling with None
+        elif self.att_subsample_size is None:
+            self._att_subsample_size = [self.att_subsample_size] * len(Xt)
+        # if the input is a list, it must be the same length as the number of
+        # series_transformers
+        # list values must be ints, floats or None. The same checks as above are
+        # performed
+        elif isinstance(self.att_subsample_size, (list, tuple)):
+            if len(self.att_subsample_size) != len(Xt):
+                raise ValueError(
+                    "att_subsample_size as a list or tuple must be the same length as "
+                    "series_transformers."
+                )
+
+            self._att_subsample_size = []
+            for ssize in self.att_subsample_size:
+                if isinstance(ssize, int):
+                    if ssize < 1:
+                        raise ValueError(
+                            "att_subsample_size in list must be at least one if it is "
+                            "an int."
+                        )
+
+                    self._att_subsample_size.append(ssize)
+                elif isinstance(ssize, float):
+                    if ssize > 1:
+                        raise ValueError(
+                            "att_subsample_size in list must be between 0 and 1 if it "
+                            "is a "
+                            "float."
+                        )
+
+                    self._att_subsample_size.append(ssize)
+                elif ssize is None:
+                    self._att_subsample_size.append(ssize)
+                else:
+                    raise ValueError(
+                        "Invalid interval_features input in list. Found "
+                        f"{self.att_subsample_size}"
+                    )
+        # other inputs are invalid
+        else:
+            raise ValueError(
+                f"Invalid interval_features input. Found {self.att_subsample_size}"
+            )
+
+        # if we are subsampling attributes for a series_transformer and it uses a
+        # BaseTransformer, we must ensure it has the required parameters and
+        # attributes to do so
+        self._transformer_feature_selection = [[]] * len(Xt)
+        self._transformer_feature_names = [[]] * len(Xt)
+        for r, att_subsample in enumerate(self._att_subsample_size):
+            if att_subsample is not None:
+                for transformer in self._interval_features[r]:
+                    if isinstance(transformer, BaseTransformer):
+                        params = inspect.signature(transformer.__init__).parameters
+
+                        # the transformer must have a parameter with one of the
+                        # names listed in transformer_feature_selection as a way to
+                        # select which features the transformer should transform
+                        has_params = False
+                        for n in self.transformer_feature_selection:
+                            if params.get(n, None) is not None:
+                                has_params = True
+                                self._transformer_feature_selection[r].append(n)
+                                break
+
+                        if not has_params:
+                            raise ValueError(
+                                "All transformers in interval_features must have a "
+                                "parameter named in transformer_feature_selection to "
+                                "be used in attribute subsampling."
+                            )
+
+                        # the transformer must have an attribute with one of the
+                        # names listed in transformer_feature_names as a list or tuple
+                        # of valid options for the previous parameter
+                        has_feature_names = False
+                        for n in self.transformer_feature_names:
+                            if hasattr(transformer, n) and isinstance(
+                                getattr(transformer, n), (list, tuple)
+                            ):
+                                has_feature_names = True
+                                self._transformer_feature_names[r].append(n)
+                                break
+
+                        if not has_feature_names:
+                            raise ValueError(
+                                "All transformers in interval_features must have an "
+                                "attribute or propertynamed in "
+                                "transformer_feature_names to be used in attribute "
+                                "subsampling."
+                            )
+
+        # verify the interval_selection_method is a valid string
+        if isinstance(self.interval_selection_method, str):
+            # SupervisedIntervals cannot currently handle transformers or regression
+            if (
+                self.interval_selection_method.lower() == "supervised"
+                or self.interval_selection_method.lower() == "random-supervised"
+            ):
+                if any(self._interval_transformer):
+                    raise ValueError(
+                        "Supervised interval_selection_method must only have function "
+                        "inputs for interval_features."
+                    )
+
+                if is_regressor(self):
+                    raise ValueError(
+                        "Supervised interval_selection_method cannot be used for "
+                        "regression."
+                    )
+            # RandomIntervals
+            elif not self.interval_selection_method.lower() == "random":
+                raise ValueError(
+                    'Unknown interval_selection_method, must be one of ("random",'
+                    '"supervised","random-supervised"). '
+                    f"Found: {self.interval_selection_method}"
+                )
+        # other inputs are invalid
+        else:
+            raise ValueError(
+                'Unknown interval_selection_method, must be one of ("random",'
+                '"supervised","random-supervised"). '
+                f"Found: {self.interval_selection_method}"
+            )
+
+        # verify replace_nan is a valid string, number or None
+        if (
+            (not isinstance(self.replace_nan, str) or self.replace_nan.lower() != "nan")
+            and not isinstance(self.replace_nan, (int, float))
+            and self.replace_nan is not None
+        ):
+            raise ValueError(f"Invalid replace_nan input. Found {self.replace_nan}")
+
+        self._n_jobs = check_n_jobs(self.n_jobs)
+
+        if self.time_limit_in_minutes is not None and self.time_limit_in_minutes > 0:
+            time_limit = self.time_limit_in_minutes * 60
+            start_time = time.time()
+            train_time = 0
+
+            self._n_estimators = 0
+            self.estimators_ = []
+            self.intervals_ = []
+            self.transformed_data_ = []
+
+            while (
+                train_time < time_limit
+                and self._n_estimators < self.contract_max_n_estimators
+            ):
+                fit = Parallel(
+                    n_jobs=self._n_jobs,
+                    backend=self.parallel_backend,
+                    prefer="threads",
+                )(
+                    delayed(self._fit_estimator)(
+                        Xt,
+                        y,
+                        rng.randint(np.iinfo(np.int32).max),
+                    )
+                    for _ in range(self._n_jobs)
+                )
+
+                (
+                    estimators,
+                    intervals,
+                    transformed_data,
+                ) = zip(*fit)
+
+                self.estimators_ += estimators
+                self.intervals_ += intervals
+                self.transformed_data_ += transformed_data
+
+                self._n_estimators += self._n_jobs
+                train_time = time.time() - start_time
+        else:
+            self._n_estimators = self.n_estimators
+
+            fit = Parallel(
+                n_jobs=self._n_jobs,
+                backend=self.parallel_backend,
+                prefer="threads",
+            )(
+                delayed(self._fit_estimator)(
+                    Xt,
+                    y,
+                    rng.randint(np.iinfo(np.int32).max),
+                )
+                for _ in range(self._n_estimators)
+            )
+
+            (
+                self.estimators_,
+                self.intervals_,
+                self.transformed_data_,
+            ) = zip(*fit)
+
+        return self
+
+    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,
+                prefer="threads",
+            )(
+                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)
+        else:
+            return np.array(
+                [self.classes_[int(np.argmax(prob))] for prob in self._predict_proba(X)]
+            )
+
+    def _predict_proba(self, X):
+        Xt = self._predict_setup(X)
+
+        y_probas = Parallel(
+            n_jobs=self._n_jobs, backend=self.parallel_backend, prefer="threads"
+        )(
+            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) / (
+            np.ones(self.n_classes_) * self._n_estimators
+        )
+        return output
+
+    def _fit_estimator(self, Xt, y, seed):
+        # random state for this estimator
+        rng = check_random_state(seed)
+
+        intervals = []
+        transform_data_lengths = []
+        interval_features = np.empty((self.n_instances_, 0))
+
+        # for each transformed series
+        for r in range(len(Xt)):
+            # subsample attributes if enabled
+            if self._att_subsample_size[r] is not None:
+                # separate transformers and functions in separate lists
+                # add the feature names of transformers to a list to subsample from
+                # and calculate the total number of features
+                all_transformers = []
+                all_transformer_features = []
+                all_function_features = []
+                for feature in self._interval_features[r]:
+                    if isinstance(feature, BaseTransformer):
+                        all_transformer_features += getattr(
+                            feature,
+                            self._transformer_feature_names[r][len(all_transformers)],
+                        )
+                        all_transformers.append(feature)
+                    else:
+                        all_function_features.append(feature)
+
+                # handle float subsample size
+                num_features = len(all_transformer_features) + len(
+                    all_function_features
+                )
+                att_subsample_size = self._att_subsample_size[r]
+                if isinstance(self._att_subsample_size[r], float):
+                    att_subsample_size = int(att_subsample_size * num_features)
+
+                # if the att_subsample_size is greater than the number of features
+                # give a warning and add all features
+                features = []
+                if att_subsample_size < num_features:
+                    # subsample the transformer and function features by index
+                    atts = rng.choice(
+                        num_features,
+                        att_subsample_size,
+                        replace=False,
+                    )
+                    atts.sort()
+
+                    # subsample the feature transformers using the
+                    # transformer_feature_names and transformer_feature_selection
+                    # attributes.
+                    # the presence of valid attributes is verified in fit.
+                    count = 0
+                    length = 0
+                    for n, transformer in enumerate(all_transformers):
+                        this_len = len(
+                            getattr(transformer, self._transformer_feature_names[r][n])
+                        )
+                        length += this_len
+
+                        # subsample feature names from this transformer
+                        t_features = []
+                        while count < len(atts) and atts[count] < length:
+                            t_features.append(
+                                getattr(
+                                    transformer,
+                                    self._transformer_feature_names[r][n],
+                                )[atts[count] + this_len - length]
+                            )
+                            count += 1
+
+                        # tell this transformer to only transform the selected features
+                        if len(t_features) > 0:
+                            new_transformer = _clone_estimator(transformer, seed)
+                            setattr(
+                                new_transformer,
+                                self._transformer_feature_selection[r][n],
+                                t_features,
+                            )
+                            features.append(new_transformer)
+
+                    # subsample the remaining function features
+                    for i in range(att_subsample_size - count):
+                        features.append(all_function_features[atts[count + i] - length])
+                else:
+                    warnings.warn(
+                        f"Attribute subsample size {att_subsample_size} is larger than "
+                        f"or equal to the number of attributes {num_features} for "
+                        f"series {self._series_transformers[r]}"
+                    )
+                    for feature in self._interval_features[r]:
+                        if isinstance(feature, BaseTransformer):
+                            features.append(_clone_estimator(feature, seed))
+                        else:
+                            features.append(feature)
+            # add all features while cloning estimators if not subsampling
+            else:
+                features = []
+                for feature in self._interval_features[r]:
+                    if isinstance(feature, BaseTransformer):
+                        features.append(_clone_estimator(feature, seed))
+                    else:
+                        features.append(feature)
+
+            # create the selected interval selector and set its parameters
+            if self.interval_selection_method == "random":
+                selector = RandomIntervals(
+                    n_intervals=self._n_intervals[r],
+                    min_interval_length=self._min_interval_length[r],
+                    max_interval_length=self._max_interval_length[r],
+                    features=features,
+                    random_state=seed,
+                )
+            elif self.interval_selection_method == "supervised":
+                selector = SupervisedIntervals(
+                    n_intervals=self._n_intervals[r],
+                    min_interval_length=self._min_interval_length[r],
+                    features=features,
+                    randomised_split_point=False,
+                    random_state=seed,
+                )
+            elif self.interval_selection_method == "random-supervised":
+                selector = SupervisedIntervals(
+                    n_intervals=self._n_intervals[r],
+                    min_interval_length=self._min_interval_length[r],
+                    features=features,
+                    randomised_split_point=True,
+                    random_state=seed,
+                )
+
+            # fit the interval selector, transform the current series using it and save
+            # the transformer
+            intervals.append(selector)
+            f = intervals[r].fit_transform(Xt[r], y)
+
+            # concatenate the data and save this transforms number of attributes
+            transform_data_lengths.append(f.shape[1])
+            interval_features = np.hstack((interval_features, f))
+
+        if isinstance(self.replace_nan, str) and self.replace_nan.lower() == "nan":
+            interval_features = np.nan_to_num(
+                interval_features, False, np.nan, np.nan, np.nan
+            )
+        elif isinstance(self.replace_nan, (int, float)):
+            interval_features = np.nan_to_num(
+                interval_features,
+                False,
+                self.replace_nan,
+                self.replace_nan,
+                self.replace_nan,
+            )
+
+        # clone and fit the base estimator using the transformed data
+        tree = _clone_estimator(self._base_estimator, random_state=seed)
+        tree.fit(interval_features, y)
+
+        # find the features used in the tree and inform the interval selectors to not
+        # transform these features if possible
+        self._efficient_predictions = True
+        relevant_features = None
+        if isinstance(tree, BaseDecisionTree):
+            relevant_features = np.unique(tree.tree_.feature[tree.tree_.feature >= 0])
+        elif isinstance(tree, ContinuousIntervalTree):
+            relevant_features, _ = tree.tree_node_splits_and_gain()
+
+        if relevant_features is not None:
+            features_to_transform = [False] * interval_features.shape[1]
+            for i in relevant_features:
+                features_to_transform[i] = True
+
+            count = 0
+            for r in range(len(Xt)):
+                intervals[r].transformer_feature_skip = self.transformer_feature_skip
+
+                # if the transformers don't have valid attributes to skip False is
+                # returned
+                completed = intervals[r].set_features_to_transform(
+                    features_to_transform[count : count + transform_data_lengths[r]],
+                    raise_error=False,
+                )
+                count += transform_data_lengths[r]
+
+                if not completed:
+                    self._efficient_predictions = False
+        else:
+            self._efficient_predictions = False
+
+        return [
+            tree,
+            intervals,
+            interval_features if self.save_transformed_data else None,
+        ]
+
+    def _predict_setup(self, X):
+        n_instances, n_channels, n_timepoints = X.shape
+
+        if n_channels != self.n_channels_:
+            raise ValueError(
+                "The number of channels in the train data does not match the number "
+                "of channels in the test data"
+            )
+        if n_timepoints != self.n_timepoints_:
+            raise ValueError(
+                "The series length of the train data does not match the series length "
+                "of the test data"
+            )
+
+        Xt = []
+        for transformer in self._series_transformers:
+            if transformer is None:
+                Xt.append(X)
+            elif _is_transformer(transformer):
+                Xt.append(transformer.transform(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))
+
+        if isinstance(self.replace_nan, str) and self.replace_nan.lower() == "nan":
+            interval_features = np.nan_to_num(
+                interval_features, False, np.nan, np.nan, np.nan
+            )
+        elif isinstance(self.replace_nan, (int, float)):
+            interval_features = np.nan_to_num(
+                interval_features,
+                False,
+                self.replace_nan,
+                self.replace_nan,
+                self.replace_nan,
+            )
+
+        if predict_proba:
+            return estimator.predict_proba(interval_features)
+        else:
+            return estimator.predict(interval_features)
+
+
+def _is_transformer(obj):
+    if isinstance(obj, BaseTransformer) or isinstance(obj, FunctionTransformer):
+        return True
+    return False
diff --git a/aeon/classification/interval_based/__init__.py b/aeon/classification/interval_based/__init__.py
index cf01e6f5..f0d0c93f 100644
--- a/aeon/classification/interval_based/__init__.py
+++ b/aeon/classification/interval_based/__init__.py
@@ -5,6 +5,7 @@
 __all__ = [
     "CanonicalIntervalForest",
     "DrCIF",
+    "IntervalForestClassifier",
     "RandomIntervalClassifier",
     "SupervisedIntervalClassifier",
     "RandomIntervalSpectralEnsemble",
@@ -15,6 +16,7 @@ __all__ = [
 
 from aeon.classification.interval_based._cif import CanonicalIntervalForest
 from aeon.classification.interval_based._drcif import DrCIF
+from aeon.classification.interval_based._interval_forest import IntervalForestClassifier
 from aeon.classification.interval_based._interval_pipelines import (
     RandomIntervalClassifier,
     SupervisedIntervalClassifier,
diff --git a/aeon/classification/interval_based/_interval_forest.py b/aeon/classification/interval_based/_interval_forest.py
new file mode 100644
index 00000000..d2b4bf03
--- /dev/null
+++ b/aeon/classification/interval_based/_interval_forest.py
@@ -0,0 +1,265 @@
+# -*- coding: utf-8 -*-
+# copyright: aeon developers, BSD-3-Clause License (see LICENSE file)
+"""Interval forest classifier."""
+
+__author__ = ["MatthewMiddlehurst"]
+__all__ = ["IntervalForestClassifier"]
+
+import numpy as np
+
+from aeon.base.estimator.interval_based.base_interval_forest import BaseIntervalForest
+from aeon.classification.base import BaseClassifier
+
+
+class IntervalForestClassifier(BaseIntervalForest, BaseClassifier):
+    """Configurable interval extracting forest classifier.
+
+    Extracts multiple phase-dependent intervals from time series data and builds a
+    base classifier on summary statistic extracted from each interval. Forms and
+    ensemble of these classifiers.
+
+    Allows the implementation of classifiers along the lines of [1][2][3]
+    which extract intervals and create an ensemble from the subsequent features.
+
+    By default, uses a configuration similar to TimeSeriesFroest [1].
+
+    Parameters
+    ----------
+    base_estimator : BaseEstimator or None, default=None
+        scikit-learn BaseEstimator used to build the interval ensemble. If None, use a
+        simple decision tree.
+    n_estimators : int, default=200
+        Number of estimators to build for the ensemble.
+    interval_selection_method : "random", "supervised" or "random-supervised",
+            default="random"
+        The interval selection transformer to use.
+            - "random" uses a RandomIntervalTransformer.
+            - "supervised" uses a SupervisedIntervalTransformer.
+            - "random-supervised" uses a SupervisedIntervalTransformer with
+                randomised elements.
+    n_intervals : int, str, list or tuple, default="sqrt"
+        Number of intervals to extract per tree for each series_transformers series.
+
+        An int input will extract that number of intervals from the series, while a str
+        input will return a function of the series length (may differ per
+        series_transformers output) to extract that number of intervals.
+        Valid str inputs are:
+            - "sqrt": square root of the series length.
+            - "sqrt-div": sqrt of series length divided by the number
+                of series_transformers.
+
+        A list or tuple of ints and/or strs will extract the number of intervals using
+        the above rules and sum the results for the final n_intervals. i.e. [4, "sqrt"]
+        will extract sqrt(series_length) + 4 intervals.
+
+        Different number of intervals for each series_transformers series can be
+        specified using a nested list or tuple. Any list or tuple input containing
+        another list or tuple must be the same length as the number of
+        series_transformers.
+
+        While random interval extraction will extract the n_intervals intervals total
+        (removing duplicates), supervised intervals will run the supervised extraction
+        process n_intervals times, returning more intervals than specified.
+    min_interval_length : int, float, list, or tuple, default=3
+        Minimum length of intervals to extract from series. float inputs take a
+        proportion of the series length to use as the minimum interval length.
+
+        Different minimum interval lengths for each series_transformers series can be
+        specified using a list or tuple. Any list or tuple input must be the same length
+        as the number of series_transformers.
+    max_interval_length : int, float, list, or tuple, default=np.inf
+        Maximum length of intervals to extract from series. float inputs take a
+        proportion of the series length to use as the maximum interval length.
+
+        Different maximum interval lengths for each series_transformers series can be
+        specified using a list or tuple. Any list or tuple input must be the same length
+        as the number of series_transformers.
+
+        Ignored for supervised interval_selection_method inputs.
+    interval_features : TransformerMixin, callable, list, tuple, or None, default=None
+        The features to extract from the intervals using transformers or callable
+        functions. If None, use the mean, standard deviation, and slope of the series.
+
+        Both transformers and functions should be able to take a 2D np.ndarray input.
+        Functions should output a 1d array (the feature for each series), and
+        transformers should output a 2d array where rows are the features for each
+        series. A list or tuple of transformers and/or functions will extract all
+        features and concatenate the output.
+
+        Different features for each series_transformers series can be specified using a
+        nested list or tuple. Any list or tuple input containing another list or tuple
+        must be the same length as the number of series_transformers.
+    series_transformers : TransformerMixin, list, tuple, or None, default=None
+        The transformers to apply to the series before extracting intervals. If None,
+        use the series as is.
+
+        Both transformers and functions should be able to take a 3D np.ndarray input.
+        A list or tuple of transformers and/or functions will extract intervals from
+        all transformations concatenate the output. Including None in the list or tuple
+        will use the series as is for interval extraction.
+    att_subsample_size : int, float, list, tuple or None, default=None
+        The number of attributes to subsample for each estimator. If None, use all
+
+        If int, use that number of attributes for all estimators. If float, use that
+        proportion of attributes for all estimators.
+
+        Different subsample sizes for each series_transformers series can be specified
+        using a list or tuple. Any list or tuple input must be the same length as the
+        number of series_transformers.
+    replace_nan : "nan", int, float or None, default=None
+        The value to replace NaNs and infinite values with before fitting the base
+        estimator. int or float input will replace with the specified value, while
+        "nan" will replace infinite values with NaNs. If None, do not replace NaNs.
+    time_limit_in_minutes : int, default=0
+        Time contract to limit build time in minutes, overriding n_estimators.
+        Default of 0 means n_estimators are used.
+    contract_max_n_estimators : int, default=500
+        Max number of estimators when time_limit_in_minutes is set.
+    save_transformed_data : bool, default=False
+        Save the data transformed in fit for use in _get_train_probs.
+    random_state : int, RandomState instance or None, default=None
+        If `int`, random_state is the seed used by the random number generator;
+        If `RandomState` instance, random_state is the random number generator;
+        If `None`, the random number generator is the `RandomState` instance used
+        by `np.random`.
+    n_jobs : int, default=1
+        The number of jobs to run in parallel for both `fit` and `predict`.
+        ``-1`` means using all processors.
+    parallel_backend : str, ParallelBackendBase instance or None, default=None
+        Specify the parallelisation backend implementation in joblib, if None a 'prefer'
+        value of "threads" is used by default.
+        Valid options are "loky", "multiprocessing", "threading" or a custom backend.
+        See the joblib Parallel documentation for more details.
+
+    Attributes
+    ----------
+    n_instances_ : int
+        The number of train cases.
+    n_channels_ : int
+        The number of channels per case.
+    n_timepoints_ : int
+        The length of each series.
+    total_intervals_ : int
+        Total number of intervals per tree from all representations.
+    estimators_ : list of shape (n_estimators) of BaseEstimator
+        The collections of estimators trained in fit.
+    intervals_ : list of shape (n_estimators) of BaseTransformer
+        Stores the interval extraction transformer for all estimators.
+    transformed_data_ : list of shape (n_estimators) of ndarray with shape
+    (n_instances,total_intervals * att_subsample_size)
+        The transformed dataset for all estimators. Only saved when
+        save_transformed_data is true.
+
+    References
+    ----------
+    .. [1] H.Deng, G.Runger, E.Tuv and M.Vladimir, "A time series forest for
+       classification and feature extraction", Information Sciences, 239, 2013
+    .. [2] Matthew Middlehurst and James Large and Anthony Bagnall. "The Canonical
+       Interval Forest (CIF) Classifier for Time Series Classification."
+       IEEE International Conference on Big Data 2020
+    .. [3] Cabello, Nestor, et al. "Fast and Accurate Time Series Classification
+       Through Supervised Interval Search." IEEE ICDM 2020
+
+    Examples
+    --------
+    >>> from aeon.classification.interval_based import IntervalForestClassifier
+    >>> from aeon.datasets import make_example_3d_numpy
+    >>> X, y = make_example_3d_numpy(n_cases=10, n_channels=1, n_timepoints=12,
+    ...                              return_y=True, random_state=0)
+    >>> clf = IntervalForestClassifier(n_estimators=10, random_state=0)
+    >>> clf.fit(X, y)
+    IntervalForestClassifier(n_estimators=10, random_state=0)
+    >>> clf.predict(X)
+    array([0, 1, 0, 1, 0, 0, 1, 1, 1, 0])
+    """
+
+    _tags = {
+        "capability:multivariate": True,
+        "capability:train_estimate": True,
+        "capability:contractable": True,
+        "capability:multithreading": True,
+        "algorithm_type": "interval",
+    }
+
+    def __init__(
+        self,
+        base_estimator=None,
+        n_estimators=200,
+        interval_selection_method="random",
+        n_intervals="sqrt",
+        min_interval_length=3,
+        max_interval_length=np.inf,
+        interval_features=None,
+        series_transformers=None,
+        att_subsample_size=None,
+        replace_nan=None,
+        time_limit_in_minutes=None,
+        contract_max_n_estimators=500,
+        save_transformed_data=False,
+        random_state=None,
+        n_jobs=1,
+        parallel_backend=None,
+    ):
+        super(IntervalForestClassifier, self).__init__(
+            base_estimator=base_estimator,
+            n_estimators=n_estimators,
+            interval_selection_method=interval_selection_method,
+            n_intervals=n_intervals,
+            min_interval_length=min_interval_length,
+            max_interval_length=max_interval_length,
+            interval_features=interval_features,
+            series_transformers=series_transformers,
+            att_subsample_size=att_subsample_size,
+            replace_nan=replace_nan,
+            time_limit_in_minutes=time_limit_in_minutes,
+            contract_max_n_estimators=contract_max_n_estimators,
+            save_transformed_data=save_transformed_data,
+            random_state=random_state,
+            n_jobs=n_jobs,
+            parallel_backend=parallel_backend,
+        )
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Return testing parameter settings for the estimator.
+
+        Parameters
+        ----------
+        parameter_set : str, default="default"
+            Name of the set of test parameters to return, for use in tests. If no
+            special parameters are defined for a value, will return `"default"` set.
+            IntervalForestClassifier provides the following special sets:
+                 "results_comparison" - used in some classifiers to compare against
+                    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
+                    functionality
+                "train_estimate" - used in some classifiers that set the
+                    "capability:train_estimate" tag to True to allow for more efficient
+                    testing when relevant parameters are available
+
+        Returns
+        -------
+        params : dict or list of dict, default={}
+            Parameters to create testing instances of the class.
+            Each dict are parameters to construct an "interesting" test instance, i.e.,
+            `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
+            `create_test_instance` uses the first (or only) dictionary in `params`.
+        """
+        if parameter_set == "results_comparison":
+            return {"n_estimators": 10, "n_intervals": 2}
+        elif parameter_set == "contracting":
+            return {
+                "time_limit_in_minutes": 5,
+                "contract_max_n_estimators": 2,
+                "n_intervals": 2,
+            }
+        elif parameter_set == "train_estimate":
+            return {
+                "n_estimators": 2,
+                "n_intervals": 2,
+                "save_transformed_data": True,
+            }
+        else:
+            return {"n_estimators": 2, "n_intervals": 2}
diff --git a/aeon/regression/deep_learning/__init__.py b/aeon/regression/deep_learning/__init__.py
index dbba0616..7f89bae1 100644
--- a/aeon/regression/deep_learning/__init__.py
+++ b/aeon/regression/deep_learning/__init__.py
@@ -2,14 +2,18 @@
 """Deep learning based regressors."""
 __all__ = [
     "CNNRegressor",
+    "FCNRegressor",
     "InceptionTimeRegressor",
     "IndividualInceptionRegressor",
+    "ResNetRegressor",
     "TapNetRegressor",
 ]
 
 from aeon.regression.deep_learning.cnn import CNNRegressor
+from aeon.regression.deep_learning.fcn import FCNRegressor
 from aeon.regression.deep_learning.inception_time import (
     InceptionTimeRegressor,
     IndividualInceptionRegressor,
 )
+from aeon.regression.deep_learning.resnet import ResNetRegressor
 from aeon.regression.deep_learning.tapnet import TapNetRegressor
diff --git a/aeon/regression/deep_learning/fcn.py b/aeon/regression/deep_learning/fcn.py
new file mode 100644
index 00000000..68f9ffc4
--- /dev/null
+++ b/aeon/regression/deep_learning/fcn.py
@@ -0,0 +1,319 @@
+# -*- coding: utf-8 -*-
+"""Fully Connected Neural Network (CNN) for regression."""
+
+__author__ = ["James-Large", "AurumnPegasus", "hadifawaz1999"]
+__all__ = ["FCNRegressor"]
+
+import os
+import time
+from copy import deepcopy
+
+from sklearn.utils import check_random_state
+
+from aeon.networks.fcn import FCNNetwork
+from aeon.regression.deep_learning.base import BaseDeepRegressor
+from aeon.utils.validation._dependencies import _check_dl_dependencies
+
+_check_dl_dependencies(severity="warning")
+
+
+class FCNRegressor(BaseDeepRegressor):
+    """Fully Connected Neural Network (FCN), as described in [1]_.
+
+    Parameters
+    ----------
+    n_layers        : int, default = 3
+        number of convolution layers
+    n_filters       : int or list of int, default = [128,256,128]
+        number of filters used in convolution layers
+    kernel_size     : int or list of int, default = [8,5,3]
+        size of convolution kernel
+    dilation_rate   : int or list of int, default = 1
+        the dilation rate for convolution
+    strides         : int or list of int, default = 1
+        the strides of the convolution filter
+    padding         : str or list of str, default = "same"
+        the type of padding used for convolution
+    activation      : str or list of str, default = "relu"
+        activation used after the convolution
+    use_bias        : bool or list of bool, default = True
+        whether or not ot use bias in convolution
+    n_epochs        : int, default = 2000
+        the number of epochs to train the model
+    batch_size      : int, default = 16
+        the number of samples per gradient update.
+    use_mini_batch_size : bool, default = True,
+        whether or not to use the mini batch size formula
+    random_state    : int or None, default=None
+        Seed for random number generation.
+    verbose         : boolean, default = False
+        whether to output extra information
+    output_activation   : str, default = "linear",
+        the output activation of the regressor
+    loss            : string, default="mean_squared_error"
+        fit parameter for the keras model
+    metrics         : list of strings, default=["accuracy"],
+    optimizer       : keras.optimizers object, default = Adam(lr=0.01)
+        specify the optimizer and the learning rate to be used.
+    file_path       : str, default = "./"
+        file path to save best model
+    save_best_model     : bool, default = False
+        Whether or not to save the best model, if the
+        modelcheckpoint callback is used by default,
+        this condition, if True, will prevent the
+        automatic deletion of the best saved model from
+        file and the user can choose the file name
+    save_last_model     : bool, default = False
+        Whether or not to save the last model, last
+        epoch trained, using the base class method
+        save_last_model_to_file
+    best_file_name      : str, default = "best_model"
+        The name of the file of the best model, if
+        save_best_model is set to False, this parameter
+        is discarded
+    last_file_name      : str, default = "last_model"
+        The name of the file of the last model, if
+        save_last_model is set to False, this parameter
+        is discarded
+    callbacks       : keras.callbacks, default = None
+    Notes
+    -----
+    Adapted from the implementation from Fawaz et. al
+    https://github.com/hfawaz/dl-4-tsc/blob/master/classifiers/fcn.py
+
+    References
+    ----------
+    .. [1] Zhao et. al, Convolutional neural networks for time series classification,
+    Journal of Systems Engineering and Electronics, 28(1):2017.
+
+    Examples
+    --------
+    >>> from aeon.regression.deep_learning import FCNRegressor
+    >>> from aeon.datasets import make_example_3d_numpy
+    >>> X, y = make_example_3d_numpy(n_cases=10, n_channels=1, n_timepoints=12,
+    ...                              return_y=True, regression_target=True,
+    ...                              random_state=0)
+    >>> rgs = FCNRegressor(n_epochs=20,batch_size=4)  # doctest: +SKIP
+    >>> rgs.fit(X, y)  # doctest: +SKIP
+    FCNRegressor(...)
+    """
+
+    _tags = {
+        "python_dependencies": "tensorflow",
+        "capability:multivariate": True,
+        "algorithm_type": "deeplearning",
+    }
+
+    def __init__(
+        self,
+        n_layers=3,
+        n_filters=None,
+        kernel_size=None,
+        dilation_rate=1,
+        strides=1,
+        padding="same",
+        activation="relu",
+        file_path="./",
+        save_best_model=False,
+        save_last_model=False,
+        best_file_name="best_model",
+        last_file_name="last_model",
+        n_epochs=2000,
+        batch_size=16,
+        use_mini_batch_size=True,
+        callbacks=None,
+        verbose=False,
+        output_activation="linear",
+        loss="mse",
+        metrics=None,
+        random_state=None,
+        use_bias=True,
+        optimizer=None,
+    ):
+        _check_dl_dependencies(severity="error")
+        super(FCNRegressor, self).__init__(last_file_name=last_file_name)
+
+        self.n_layers = n_layers
+        self.kernel_size = kernel_size
+        self.n_filters = n_filters
+        self.strides = strides
+        self.activation = activation
+        self.dilation_rate = dilation_rate
+        self.padding = padding
+        self.use_bias = use_bias
+        self.output_activation = output_activation
+        self.callbacks = callbacks
+        self.n_epochs = n_epochs
+        self.batch_size = batch_size
+        self.use_mini_batch_size = use_mini_batch_size
+        self.verbose = verbose
+        self.loss = loss
+        self.metrics = metrics
+        self.random_state = random_state
+        self.optimizer = optimizer
+        self.history = None
+        self.file_path = file_path
+        self.save_best_model = save_best_model
+        self.save_last_model = save_last_model
+        self.best_file_name = best_file_name
+        self.last_file_name = last_file_name
+        self._network = FCNNetwork(
+            random_state=self.random_state,
+            n_layers=self.n_layers,
+            kernel_size=self.kernel_size,
+            n_filters=self.n_filters,
+            strides=self.strides,
+            padding=self.padding,
+            dilation_rate=self.dilation_rate,
+            activation=self.activation,
+            use_bias=self.use_bias,
+        )
+
+    def build_model(self, input_shape, **kwargs):
+        """Construct a compiled, un-trained, keras model that is ready for training.
+
+        In aeon, time series are stored in numpy arrays of shape (d,m), where d
+        is the number of dimensions, m is the series length. Keras/tensorflow assume
+        data is in shape (m,d). This method also assumes (m,d). Transpose should
+        happen in fit.
+
+        Parameters
+        ----------
+        input_shape : tuple
+            The shape of the data fed into the input layer, should be (m,d)
+
+        Returns
+        -------
+        output : a compiled Keras Model
+        """
+        import tensorflow as tf
+
+        tf.random.set_seed(self.random_state)
+
+        if self.metrics is None:
+            metrics = ["accuracy"]
+        else:
+            metrics = self.metrics
+        input_layer, output_layer = self._network.build_network(input_shape, **kwargs)
+
+        output_layer = tf.keras.layers.Dense(
+            units=1,
+            activation=self.output_activation,
+        )(output_layer)
+
+        self.optimizer_ = (
+            tf.keras.optimizers.Adam() if self.optimizer is None else self.optimizer
+        )
+
+        model = tf.keras.models.Model(inputs=input_layer, outputs=output_layer)
+        model.compile(
+            loss=self.loss,
+            optimizer=self.optimizer_,
+            metrics=metrics,
+        )
+
+        return model
+
+    def _fit(self, X, y):
+        """Fit the regressor on the training set (X, y).
+
+        Parameters
+        ----------
+        X : np.ndarray of shape = (n_instances (n), n_channels (d), series_length (m))
+            The training input samples.
+        y : np.ndarray of shape n
+            The training data target values.
+
+        Returns
+        -------
+        self : object
+        """
+        import tensorflow as tf
+
+        # Transpose to conform to Keras input style.
+        X = X.transpose(0, 2, 1)
+
+        check_random_state(self.random_state)
+
+        self.input_shape = X.shape[1:]
+        self.training_model_ = self.build_model(self.input_shape)
+
+        if self.verbose:
+            self.training_model_.summary()
+
+        if self.use_mini_batch_size:
+            mini_batch_size = min(self.batch_size, X.shape[0] // 10)
+        else:
+            mini_batch_size = self.batch_size
+
+        self.file_name_ = (
+            self.best_file_name if self.save_best_model else str(time.time_ns())
+        )
+
+        self.callbacks_ = (
+            [
+                tf.keras.callbacks.ReduceLROnPlateau(
+                    monitor="loss", factor=0.5, patience=50, min_lr=0.0001
+                ),
+                tf.keras.callbacks.ModelCheckpoint(
+                    filepath=self.file_path + self.file_name_ + ".hdf5",
+                    monitor="loss",
+                    save_best_only=True,
+                ),
+            ]
+            if self.callbacks is None
+            else self.callbacks
+        )
+
+        self.history = self.training_model_.fit(
+            X,
+            y,
+            batch_size=mini_batch_size,
+            epochs=self.n_epochs,
+            verbose=self.verbose,
+            callbacks=self.callbacks_,
+        )
+
+        try:
+            self.model_ = tf.keras.models.load_model(
+                self.file_path + self.file_name_ + ".hdf5", compile=False
+            )
+            if not self.save_best_model:
+                os.remove(self.file_path + self.file_name_ + ".hdf5")
+        except FileNotFoundError:
+            self.model_ = deepcopy(self.training_model_)
+
+        if self.save_last_model:
+            self.save_last_model_to_file(file_path=self.file_path)
+
+        return self
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Return testing parameter settings for the estimator.
+
+        Parameters
+        ----------
+        parameter_set : str, default="default"
+            Name of the set of test parameters to return, for use in tests. If no
+            special parameters are defined for a value, will return `"default"` set.
+            For Regressors, a "default" set of parameters should be provided for
+            general testing, and a "results_comparison" set for comparing against
+            previously recorded results if the general set does not produce suitable
+            probabilities to compare against.
+
+        Returns
+        -------
+        params : dict or list of dict, default={}
+            Parameters to create testing instances of the class.
+            Each dict are parameters to construct an "interesting" test instance, i.e.,
+            `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
+            `create_test_instance` uses the first (or only) dictionary in `params`.
+        """
+        param = {
+            "n_epochs": 10,
+            "batch_size": 4,
+            "use_bias": False,
+        }
+
+        return [param]
diff --git a/aeon/regression/deep_learning/resnet.py b/aeon/regression/deep_learning/resnet.py
new file mode 100644
index 00000000..72a80f2d
--- /dev/null
+++ b/aeon/regression/deep_learning/resnet.py
@@ -0,0 +1,339 @@
+# -*- coding: utf-8 -*-
+"""Residual Network (ResNet) for regression."""
+
+__author__ = ["James-Large", "AurumnPegasus", "nilesh05apr", "hadifawaz1999"]
+__all__ = ["ResNetRegressor"]
+
+import os
+import time
+from copy import deepcopy
+
+from sklearn.utils import check_random_state
+
+from aeon.networks.resnet import ResNetNetwork
+from aeon.regression.deep_learning.base import BaseDeepRegressor
+from aeon.utils.validation._dependencies import _check_dl_dependencies
+
+_check_dl_dependencies(severity="warning")
+
+
+class ResNetRegressor(BaseDeepRegressor):
+    """
+    Residual Neural Network as described in [1].
+
+    Parameters
+    ----------
+        n_residual_blocks           : int, default = 3,
+            the number of residual blocks of ResNet's model
+        n_conv_per_residual_block   : int, default = 3,
+            the number of convolution blocks in each residual block
+        n_filters                   : int or list of int, default = [128, 64, 64],
+            the number of convolution filters for all the convolution layers in the same
+            residual block, if not a list, the same number of filters is used in all
+            convolutions of all residual blocks.
+        kernel_sizes                : int or list of int, default = [8, 5, 3],
+            the kernel size of all the convolution layers in one residual block, if not
+            a list, the same kernel size is used in all convolution layers
+        strides                     : int or list of int, default = 1,
+            the strides of convolution kernels in each of the
+            convolution layers in one residual block, if not
+            a list, the same kernel size is used in all convolution layers
+        dilation_rate               : int or list of int, default = 1,
+            the dilation rate of the convolution layers in one residual block, if not
+            a list, the same kernel size is used in all convolution layers
+        padding                     : str or list of str, default = 'padding',
+            the type of padding used in the convolution layers
+            in one residual block, if not
+            a list, the same kernel size is used in all convolution layers
+        activation                  : str or list of str, default = 'relu',
+            keras activation used in the convolution layers
+            in one residual block, if not
+            a list, the same kernel size is used in all convolution layers
+        output_activation   : str, default = "linear",
+            the output activation for the regressor
+        use_bias                    : bool or list of bool, default = True,
+            condition on wether or not to use bias values in
+            the convolution layers in one residual block, if not
+            a list, the same kernel size is used in all convolution layers
+        n_epochs                   : int, default = 1500
+            the number of epochs to train the model
+        batch_size                  : int, default = 16
+            the number of samples per gradient update.
+        use_mini_batch_size         : bool, default = False
+            condition on using the mini batch size formula Wang et al.
+        callbacks                   : callable or None, default
+        ReduceOnPlateau and ModelCheckpoint
+            list of tf.keras.callbacks.Callback objects.
+        file_path                   : str, default = './'
+            file_path when saving model_Checkpoint callback
+        save_best_model     : bool, default = False
+            Whether or not to save the best model, if the
+            modelcheckpoint callback is used by default,
+            this condition, if True, will prevent the
+            automatic deletion of the best saved model from
+            file and the user can choose the file name
+        save_last_model     : bool, default = False
+            Whether or not to save the last model, last
+            epoch trained, using the base class method
+            save_last_model_to_file
+        best_file_name      : str, default = "best_model"
+            The name of the file of the best model, if
+            save_best_model is set to False, this parameter
+            is discarded
+        last_file_name      : str, default = "last_model"
+            The name of the file of the last model, if
+            save_last_model is set to False, this parameter
+            is discarded
+        verbose                     : boolean, default = False
+            whether to output extra information
+        loss                        : string, default="mean_squared_error"
+            fit parameter for the keras model
+        optimizer                   : keras.optimizer, default=keras.optimizers.Adam(),
+        metrics                     : list of strings, default=["accuracy"],
+
+    Notes
+    -----
+    Adapted from the implementation from source code
+    https://github.com/hfawaz/dl-4-tsc/blob/master/classifiers/resnet.py
+
+    References
+    ----------
+        .. [1] Wang et. al, Time series classification from
+    scratch with deep neural networks: A strong baseline,
+    International joint conference on neural networks (IJCNN), 2017.
+
+    Examples
+    --------
+    >>> from aeon.regression.deep_learning import ResNetRegressor
+    >>> from aeon.datasets import make_example_3d_numpy
+    >>> X, y = make_example_3d_numpy(n_cases=10, n_channels=1, n_timepoints=12,
+    ...                              return_y=True, regression_target=True,
+    ...                              random_state=0)
+    >>> rgs = ResNetRegressor(n_epochs=20, bacth_size=4) # doctest: +SKIP
+    >>> rgs.fit(X, y) # doctest: +SKIP
+    ResNetRegressor(...)
+    """
+
+    _tags = {
+        "python_dependencies": "tensorflow",
+        "capability:multivariate": True,
+        "algorithm_type": "deeplearning",
+    }
+
+    def __init__(
+        self,
+        n_residual_blocks=3,
+        n_conv_per_residual_block=3,
+        n_filters=None,
+        kernel_size=None,
+        strides=1,
+        dilation_rate=1,
+        padding="same",
+        activation="relu",
+        use_bias=True,
+        n_epochs=1500,
+        callbacks=None,
+        verbose=False,
+        loss="mse",
+        output_activation="linear",
+        metrics=None,
+        batch_size=64,
+        use_mini_batch_size=True,
+        random_state=None,
+        file_path="./",
+        save_best_model=False,
+        save_last_model=False,
+        best_file_name="best_model",
+        last_file_name="last_model",
+        optimizer=None,
+    ):
+        _check_dl_dependencies(severity="error")
+        super(ResNetRegressor, self).__init__(last_file_name=last_file_name)
+        self.n_residual_blocks = n_residual_blocks
+        self.n_conv_per_residual_block = n_conv_per_residual_block
+        self.n_filters = n_filters
+        self.kernel_size = kernel_size
+        self.padding = padding
+        self.strides = strides
+        self.dilation_rate = dilation_rate
+        self.n_epochs = n_epochs
+        self.callbacks = callbacks
+        self.verbose = verbose
+        self.loss = loss
+        self.metrics = metrics
+        self.batch_size = batch_size
+        self.use_mini_batch_size = use_mini_batch_size
+        self.random_state = random_state
+        self.activation = activation
+        self.output_activation = output_activation
+        self.use_bias = use_bias
+        self.file_path = file_path
+        self.save_best_model = save_best_model
+        self.save_last_model = save_last_model
+        self.best_file_name = best_file_name
+        self.last_file_name = last_file_name
+        self.optimizer = optimizer
+        self.history = None
+        self._network = ResNetNetwork(
+            n_residual_blocks=self.n_residual_blocks,
+            n_conv_per_residual_block=self.n_conv_per_residual_block,
+            n_filters=self.n_filters,
+            kernel_size=self.kernel_size,
+            strides=self.strides,
+            use_bias=self.use_bias,
+            activation=self.activation,
+            dilation_rate=self.dilation_rate,
+            padding=self.padding,
+            random_state=random_state,
+        )
+
+    def build_model(self, input_shape, **kwargs):
+        """Construct a compiled, un-trained, keras model that is ready for training.
+
+        In aeon, time series are stored in numpy arrays of shape (d,m), where d
+        is the number of dimensions, m is the series length. Keras/tensorflow assume
+        data is in shape (m,d). This method also assumes (m,d). Transpose should
+        happen in fit.
+
+        Parameters
+        ----------
+        input_shape : tuple
+            The shape of the data fed into the input layer, should be (m,d)
+
+        Returns
+        -------
+        output : a compiled Keras Model
+        """
+        import tensorflow as tf
+
+        tf.random.set_seed(self.random_state)
+
+        self.optimizer_ = (
+            tf.keras.optimizers.Adam(learning_rate=0.01)
+            if self.optimizer is None
+            else self.optimizer
+        )
+
+        if self.metrics is None:
+            metrics = ["accuracy"]
+        else:
+            metrics = self.metrics
+
+        input_layer, output_layer = self._network.build_network(input_shape, **kwargs)
+
+        output_layer = tf.keras.layers.Dense(
+            units=1,
+            activation=self.output_activation,
+        )(output_layer)
+
+        model = tf.keras.models.Model(inputs=input_layer, outputs=output_layer)
+        model.compile(
+            loss=self.loss,
+            optimizer=self.optimizer_,
+            metrics=metrics,
+        )
+
+        return model
+
+    def _fit(self, X, y):
+        """Fit the regressor on the training set (X, y).
+
+        Parameters
+        ----------
+        X : np.ndarray of shape = (n_instances (n), n_channels (d), series_length (m))
+            The training input samples.
+        y : np.ndarray of shape n
+            The training data target values.
+
+        Returns
+        -------
+        self : object
+        """
+        import tensorflow as tf
+
+        # Transpose to conform to Keras input style.
+        X = X.transpose(0, 2, 1)
+
+        check_random_state(self.random_state)
+
+        self.input_shape = X.shape[1:]
+        self.training_model_ = self.build_model(self.input_shape)
+
+        if self.verbose:
+            self.training_model_.summary()
+
+        self.file_name_ = (
+            self.best_file_name if self.save_best_model else str(time.time_ns())
+        )
+
+        self.callbacks_ = (
+            [
+                tf.keras.callbacks.ReduceLROnPlateau(
+                    monitor="loss", factor=0.5, patience=50, min_lr=0.0001
+                ),
+                tf.keras.callbacks.ModelCheckpoint(
+                    filepath=self.file_path + self.file_name_ + ".hdf5",
+                    monitor="loss",
+                    save_best_only=True,
+                ),
+            ]
+            if self.callbacks is None
+            else self.callbacks
+        )
+
+        if self.use_mini_batch_size:
+            mini_batch_size = min(self.batch_size, X.shape[0] // 10)
+        else:
+            mini_batch_size = self.batch_size
+
+        self.history = self.training_model_.fit(
+            X,
+            y,
+            batch_size=mini_batch_size,
+            epochs=self.n_epochs,
+            verbose=self.verbose,
+            callbacks=self.callbacks_,
+        )
+
+        try:
+            self.model_ = tf.keras.models.load_model(
+                self.file_path + self.file_name_ + ".hdf5", compile=False
+            )
+            if not self.save_best_model:
+                os.remove(self.file_path + self.file_name_ + ".hdf5")
+        except FileNotFoundError:
+            self.model_ = deepcopy(self.training_model_)
+
+        if self.save_last_model:
+            self.save_last_model_to_file(file_path=self.file_path)
+
+        return self
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Return testing parameter settings for the estimator.
+
+        Parameters
+        ----------
+        parameter_set : str, default="default"
+            Name of the set of test parameters to return, for use in tests. If no
+            special parameters are defined for a value, will return `"default"` set.
+            For regressors, a "default" set of parameters should be provided for
+            general testing, and a "results_comparison" set for comparing against
+            previously recorded results if the general set does not produce suitable
+            probabilities to compare against.
+
+        Returns
+        -------
+        params : dict or list of dict, default={}
+            Parameters to create testing instances of the class.
+            Each dict are parameters to construct an "interesting" test instance, i.e.,
+            `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
+            `create_test_instance` uses the first (or only) dictionary in `params`.
+        """
+        param = {
+            "n_epochs": 10,
+            "batch_size": 4,
+        }
+
+        return [param]
diff --git a/aeon/regression/interval_based/__init__.py b/aeon/regression/interval_based/__init__.py
index 81c11dce..b5f0c799 100644
--- a/aeon/regression/interval_based/__init__.py
+++ b/aeon/regression/interval_based/__init__.py
@@ -3,9 +3,11 @@
 """Implement interval-based time series regression estimators."""
 
 __all__ = [
+    "IntervalForestRegressor",
     "RandomIntervalRegressor",
     "TimeSeriesForestRegressor",
 ]
 
+from aeon.regression.interval_based._interval_forest import IntervalForestRegressor
 from aeon.regression.interval_based._interval_pipelines import RandomIntervalRegressor
 from aeon.regression.interval_based._tsf import TimeSeriesForestRegressor
diff --git a/aeon/regression/interval_based/_interval_forest.py b/aeon/regression/interval_based/_interval_forest.py
new file mode 100644
index 00000000..1774d792
--- /dev/null
+++ b/aeon/regression/interval_based/_interval_forest.py
@@ -0,0 +1,263 @@
+# -*- coding: utf-8 -*-
+# copyright: aeon developers, BSD-3-Clause License (see LICENSE file)
+"""Interval forest regressor."""
+
+__author__ = ["MatthewMiddlehurst"]
+__all__ = ["IntervalForestRegressor"]
+
+import numpy as np
+
+from aeon.base.estimator.interval_based.base_interval_forest import BaseIntervalForest
+from aeon.regression.base import BaseRegressor
+
+
+class IntervalForestRegressor(BaseIntervalForest, BaseRegressor):
+    """Configurable interval extracting forest regressor.
+
+    Extracts multiple phase-dependent intervals from time series data and builds a
+    base regressor on summary statistic extracted from each interval. Forms and
+    ensemble of these regressors.
+
+    Allows the implementation of regressors along the lines of [1][2][3]
+    which extract intervals and create an ensemble from the subsequent features.
+
+    By default, uses a configuration similar to TimeSeriesFroest [1].
+
+    Parameters
+    ----------
+    base_estimator : BaseEstimator or None, default=None
+        scikit-learn BaseEstimator used to build the interval ensemble. If None, use a
+        simple decision tree.
+    n_estimators : int, default=200
+        Number of estimators to build for the ensemble.
+    interval_selection_method : "random", default="random"
+        The interval selection transformer to use.
+            - "random" uses a RandomIntervalTransformer.
+    n_intervals : int, str, list or tuple, default="sqrt"
+        Number of intervals to extract per tree for each series_transformers series.
+
+        An int input will extract that number of intervals from the series, while a str
+        input will return a function of the series length (may differ per
+        series_transformers output) to extract that number of intervals.
+        Valid str inputs are:
+            - "sqrt": square root of the series length.
+            - "sqrt-div": sqrt of series length divided by the number
+                of series_transformers.
+
+        A list or tuple of ints and/or strs will extract the number of intervals using
+        the above rules and sum the results for the final n_intervals. i.e. [4, "sqrt"]
+        will extract sqrt(series_length) + 4 intervals.
+
+        Different number of intervals for each series_transformers series can be
+        specified using a nested list or tuple. Any list or tuple input containing
+        another list or tuple must be the same length as the number of
+        series_transformers.
+
+        While random interval extraction will extract the n_intervals intervals total
+        (removing duplicates), supervised intervals will run the supervised extraction
+        process n_intervals times, returning more intervals than specified.
+    min_interval_length : int, float, list, or tuple, default=3
+        Minimum length of intervals to extract from series. float inputs take a
+        proportion of the series length to use as the minimum interval length.
+
+        Different minimum interval lengths for each series_transformers series can be
+        specified using a list or tuple. Any list or tuple input must be the same length
+        as the number of series_transformers.
+    max_interval_length : int, float, list, or tuple, default=np.inf
+        Maximum length of intervals to extract from series. float inputs take a
+        proportion of the series length to use as the maximum interval length.
+
+        Different maximum interval lengths for each series_transformers series can be
+        specified using a list or tuple. Any list or tuple input must be the same length
+        as the number of series_transformers.
+
+        Ignored for supervised interval_selection_method inputs.
+    interval_features : TransformerMixin, callable, list, tuple, or None, default=None
+        The features to extract from the intervals using transformers or callable
+        functions. If None, use the mean, standard deviation, and slope of the series.
+
+        Both transformers and functions should be able to take a 2D np.ndarray input.
+        Functions should output a 1d array (the feature for each series), and
+        transformers should output a 2d array where rows are the features for each
+        series. A list or tuple of transformers and/or functions will extract all
+        features and concatenate the output.
+
+        Different features for each series_transformers series can be specified using a
+        nested list or tuple. Any list or tuple input containing another list or tuple
+        must be the same length as the number of series_transformers.
+    series_transformers : TransformerMixin, list, tuple, or None, default=None
+        The transformers to apply to the series before extracting intervals. If None,
+        use the series as is.
+
+        Both transformers and functions should be able to take a 3D np.ndarray input.
+        A list or tuple of transformers and/or functions will extract intervals from
+        all transformations concatenate the output. Including None in the list or tuple
+        will use the series as is for interval extraction.
+    att_subsample_size : int, float, list, tuple or None, default=None
+        The number of attributes to subsample for each estimator. If None, use all
+
+        If int, use that number of attributes for all estimators. If float, use that
+        proportion of attributes for all estimators.
+
+        Different subsample sizes for each series_transformers series can be specified
+        using a list or tuple. Any list or tuple input must be the same length as the
+        number of series_transformers.
+    replace_nan : "nan", int, float or None, default=None
+        The value to replace NaNs and infinite values with before fitting the base
+        estimator. int or float input will replace with the specified value, while
+        "nan" will replace infinite values with NaNs. If None, do not replace NaNs.
+    time_limit_in_minutes : int, default=0
+        Time contract to limit build time in minutes, overriding n_estimators.
+        Default of 0 means n_estimators are used.
+    contract_max_n_estimators : int, default=500
+        Max number of estimators when time_limit_in_minutes is set.
+    save_transformed_data : bool, default=False
+        Save the data transformed in fit for use in _get_train_probs.
+    random_state : int, RandomState instance or None, default=None
+        If `int`, random_state is the seed used by the random number generator;
+        If `RandomState` instance, random_state is the random number generator;
+        If `None`, the random number generator is the `RandomState` instance used
+        by `np.random`.
+    n_jobs : int, default=1
+        The number of jobs to run in parallel for both `fit` and `predict`.
+        ``-1`` means using all processors.
+    parallel_backend : str, ParallelBackendBase instance or None, default=None
+        Specify the parallelisation backend implementation in joblib, if None a 'prefer'
+        value of "threads" is used by default.
+        Valid options are "loky", "multiprocessing", "threading" or a custom backend.
+        See the joblib Parallel documentation for more details.
+
+    Attributes
+    ----------
+    n_instances_ : int
+        The number of train cases.
+    n_channels_ : int
+        The number of channels per case.
+    n_timepoints_ : int
+        The length of each series.
+    total_intervals_ : int
+        Total number of intervals per tree from all representations.
+    estimators_ : list of shape (n_estimators) of BaseEstimator
+        The collections of estimators trained in fit.
+    intervals_ : list of shape (n_estimators) of BaseTransformer
+        Stores the interval extraction transformer for all estimators.
+    transformed_data_ : list of shape (n_estimators) of ndarray with shape
+    (n_instances,total_intervals * att_subsample_size)
+        The transformed dataset for all estimators. Only saved when
+        save_transformed_data is true.
+
+    References
+    ----------
+    .. [1] H.Deng, G.Runger, E.Tuv and M.Vladimir, "A time series forest for
+       classification and feature extraction", Information Sciences, 239, 2013
+    .. [2] Matthew Middlehurst and James Large and Anthony Bagnall. "The Canonical
+       Interval Forest (CIF) Classifier for Time Series Classification."
+       IEEE International Conference on Big Data 2020
+    .. [3] Cabello, Nestor, et al. "Fast and Accurate Time Series Classification
+       Through Supervised Interval Search." IEEE ICDM 2020
+
+    Examples
+    --------
+    >>> from aeon.regression.interval_based import IntervalForestRegressor
+    >>> from aeon.datasets import make_example_3d_numpy
+    >>> X, y = make_example_3d_numpy(n_cases=10, n_channels=1, n_timepoints=12,
+    ...                              return_y=True, regression_target=True,
+    ...                              random_state=0)
+    >>> reg = IntervalForestRegressor(n_estimators=10, random_state=0)
+    >>> reg.fit(X, y)
+    IntervalForestRegressor(n_estimators=10, random_state=0)
+    >>> reg.predict(X)
+    array([0.7252543 , 1.50132442, 0.95608366, 1.64399016, 0.42385504,
+           0.60639322, 1.01919317, 1.30157483, 1.66017354, 0.2900776 ])
+    """
+
+    _tags = {
+        "capability:multivariate": True,
+        "capability:train_estimate": True,
+        "capability:contractable": True,
+        "capability:multithreading": True,
+        "algorithm_type": "interval",
+    }
+
+    def __init__(
+        self,
+        base_estimator=None,
+        n_estimators=200,
+        interval_selection_method="random",
+        n_intervals="sqrt",
+        min_interval_length=3,
+        max_interval_length=np.inf,
+        interval_features=None,
+        series_transformers=None,
+        att_subsample_size=None,
+        replace_nan=None,
+        time_limit_in_minutes=None,
+        contract_max_n_estimators=500,
+        save_transformed_data=False,
+        random_state=None,
+        n_jobs=1,
+        parallel_backend=None,
+    ):
+        super(IntervalForestRegressor, self).__init__(
+            base_estimator=base_estimator,
+            n_estimators=n_estimators,
+            interval_selection_method=interval_selection_method,
+            n_intervals=n_intervals,
+            min_interval_length=min_interval_length,
+            max_interval_length=max_interval_length,
+            interval_features=interval_features,
+            series_transformers=series_transformers,
+            att_subsample_size=att_subsample_size,
+            replace_nan=replace_nan,
+            time_limit_in_minutes=time_limit_in_minutes,
+            contract_max_n_estimators=contract_max_n_estimators,
+            save_transformed_data=save_transformed_data,
+            random_state=random_state,
+            n_jobs=n_jobs,
+            parallel_backend=parallel_backend,
+        )
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Return testing parameter settings for the estimator.
+
+        Parameters
+        ----------
+        parameter_set : str, default="default"
+            Name of the set of test parameters to return, for use in tests. If no
+            special parameters are defined for a value, will return `"default"` set.
+            IntervalForestRegressor provides the following special sets:
+                 "results_comparison" - used in some regressors to compare against
+                    previously generated results where the default set of parameters
+                    cannot produce suitable probability estimates
+                "contracting" - used in regressors that set the
+                    "capability:contractable" tag to True to test contacting
+                    functionality
+                "train_estimate" - used in some regressors that set the
+                    "capability:train_estimate" tag to True to allow for more efficient
+                    testing when relevant parameters are available
+
+        Returns
+        -------
+        params : dict or list of dict, default={}
+            Parameters to create testing instances of the class.
+            Each dict are parameters to construct an "interesting" test instance, i.e.,
+            `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
+            `create_test_instance` uses the first (or only) dictionary in `params`.
+        """
+        if parameter_set == "results_comparison":
+            return {"n_estimators": 10, "n_intervals": 2}
+        elif parameter_set == "contracting":
+            return {
+                "time_limit_in_minutes": 5,
+                "contract_max_n_estimators": 2,
+                "n_intervals": 2,
+            }
+        elif parameter_set == "train_estimate":
+            return {
+                "n_estimators": 2,
+                "n_intervals": 2,
+                "save_transformed_data": True,
+            }
+        else:
+            return {"n_estimators": 2, "n_intervals": 2}
diff --git a/aeon/utils/estimator_checks.py b/aeon/utils/estimator_checks.py
index 6fa33b54..d0735bf2 100644
--- a/aeon/utils/estimator_checks.py
+++ b/aeon/utils/estimator_checks.py
@@ -5,29 +5,16 @@ __author__ = ["fkiraly"]
 __all__ = ["check_estimator"]
 
 from inspect import isclass
-from warnings import warn
-
-
-# todo 0.17.0:
-# * remove the return_exceptions arg
-# * move the raise_exceptions arg to 2nd place
-# * change its default to False, from None
-# * update the docstring - remove return_exceptions
-# * update the docstring - move raise_exceptions block to 2nd place
-# * update the docstring - remove deprecation references
-# * update the docstring - condition in return block, refer only to raise_exceptions
-# * update the docstring - condition in raises block to refer only to raise_exceptions
-# * remove the code block for input handling
-# * remove import of warn
+
+
 def check_estimator(
     estimator,
-    return_exceptions=None,
+    raise_exceptions=False,
     tests_to_run=None,
     fixtures_to_run=None,
     verbose=True,
     tests_to_exclude=None,
     fixtures_to_exclude=None,
-    raise_exceptions=None,
 ):
     """Run all tests on one single estimator.
 
@@ -39,13 +26,10 @@ def check_estimator(
     Parameters
     ----------
     estimator : estimator class or estimator instance
-    return_exceptions : bool, optional, default=True
-        whether to return exceptions/failures, or raise them
-            if True: returns exceptions in returned `results` dict
-            if False: raises exceptions as they occur
-        deprecated since 0.15.1, and will be replaced by `raise_exceptions` in 0.17.0.
-        Overridden to `False` if `raise_exceptions=True`.
-        For safe deprecation, use `raise_exceptions` instead of `return_exceptions`.
+    raise_exceptions : bool, optional, default=False
+        whether to return exceptions/failures in the results dict, or raise them
+            if False: returns exceptions in returned `results` dict
+            if True: raises exceptions as they occur
     tests_to_run : str or list of str, optional. Default = run all tests.
         Names (test/function name string) of tests to run.
         sub-sets tests that are run to the tests given here.
@@ -62,13 +46,6 @@ def check_estimator(
     fixtures_to_exclude : str or list of str, fixtures to exclude. default = None
         removes test-fixture combinations that should not be run.
         This is done after subsetting via fixtures_to_run.
-    raise_exceptions : bool, optional, default=False
-        whether to return exceptions/failures in the results dict, or raise them
-            if False: returns exceptions in returned `results` dict
-            if True: raises exceptions as they occur
-        Overrides `return_exceptions` if used as a keyword argument.
-        both `raise_exceptions=True` and `return_exceptions=True`.
-        Will move to replace `return_exceptions` as 2nd arg in 0.17.0.
 
     Returns
     -------
@@ -77,11 +54,9 @@ def check_estimator(
         entries are the string "PASSED" if the test passed,
             or the exception raised if the test did not pass
         returned only if all tests pass,
-        or both return_exceptions=True and raise_exceptions=False
 
     Raises
     ------
-    if return_exceptions=False, or raise_exceptions=True,
     raises any exception produced by the tests directly
 
     Examples
@@ -124,22 +99,6 @@ def check_estimator(
     from aeon.tests.test_all_estimators import TestAllEstimators, TestAllObjects
     from aeon.transformations.tests.test_all_transformers import TestAllTransformers
 
-    # todo 0.17.0: remove this code block
-    if return_exceptions is None and raise_exceptions is None:
-        raise_exceptions = False
-
-    if return_exceptions is not None and raise_exceptions is None:
-        warn(
-            "The return_exceptions argument of check_estimator has been deprecated "
-            "since 0.15.1, and will be replaced by raise_exceptions in 0.17.0. "
-            "For safe deprecation: use raise_exceptions argument instead of "
-            "return_exceptions when using keywords. Avoid positional use, instead "
-            "ensure to use keywords. When not using keywords, the "
-            "default behaviour will not change."
-        )
-        raise_exceptions = not return_exceptions
-    # end block to remove
-
     testclass_dict = dict()
     testclass_dict["classifier"] = TestAllClassifiers
     testclass_dict["early_classifier"] = TestAllEarlyClassifiers


Test patch:
diff --git a/aeon/base/estimator/interval_based/tests/__init__.py b/aeon/base/estimator/interval_based/tests/__init__.py
new file mode 100644
index 00000000..e5e5fd74
--- /dev/null
+++ b/aeon/base/estimator/interval_based/tests/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# copyright: aeon developers, BSD-3-Clause License (see LICENSE file)
+"""Testing for interval-based base classes."""
diff --git a/aeon/base/estimator/interval_based/tests/test_base_interval_forest.py b/aeon/base/estimator/interval_based/tests/test_base_interval_forest.py
new file mode 100644
index 00000000..aa8eb3e1
--- /dev/null
+++ b/aeon/base/estimator/interval_based/tests/test_base_interval_forest.py
@@ -0,0 +1,199 @@
+# -*- coding: utf-8 -*-
+# copyright: aeon developers, BSD-3-Clause License (see LICENSE file)
+"""Tests for the BaseIntervalForest class."""
+
+import numpy as np
+import pytest
+from sklearn.pipeline import make_pipeline
+from sklearn.tree import DecisionTreeClassifier
+
+from aeon.base._base import _clone_estimator
+from aeon.classification.interval_based._interval_forest import IntervalForestClassifier
+from aeon.classification.sklearn import ContinuousIntervalTree
+from aeon.transformations.collection import (
+    AutocorrelationFunctionTransformer,
+    Catch22,
+    SevenNumberSummaryTransformer,
+)
+from aeon.transformations.series.func_transform import FunctionTransformer
+from aeon.utils._testing.collection import make_3d_test_data
+from aeon.utils.numba.stats import row_mean, row_numba_min
+
+
+@pytest.mark.parametrize(
+    "base_estimator",
+    [DecisionTreeClassifier(), ContinuousIntervalTree()],
+)
+def test_interval_forest_feature_skipping(base_estimator):
+    """Test BaseIntervalForest feature skipping with different base estimators."""
+    X, y = make_3d_test_data(random_state=0)
+
+    est = IntervalForestClassifier(
+        base_estimator=base_estimator,
+        n_estimators=2,
+        n_intervals=2,
+        random_state=0,
+    )
+    est.fit(X, y)
+    preds = est.predict(X)
+
+    assert est._efficient_predictions is True
+
+    est = IntervalForestClassifier(
+        base_estimator=make_pipeline(base_estimator),
+        n_estimators=2,
+        n_intervals=2,
+        random_state=0,
+    )
+    est.fit(X, y)
+
+    assert est._efficient_predictions is False
+    assert (preds == est.predict(X)).all()
+
+
+def test_interval_forest_invalid_feature_skipping():
+    """Test BaseIntervalForest with an invalid transformer for feature skipping."""
+    X, y = make_3d_test_data()
+
+    est = IntervalForestClassifier(
+        n_estimators=2,
+        n_intervals=2,
+        interval_features=SevenNumberSummaryTransformer(),
+    )
+    est.fit(X, y)
+
+    assert est._efficient_predictions is False
+
+
+@pytest.mark.parametrize(
+    "interval_selection_method",
+    ["random", "supervised", "random-supervised"],
+)
+def test_interval_forest_selection_methods(interval_selection_method):
+    """Test BaseIntervalForest with different interval selection methods."""
+    X, y = make_3d_test_data()
+
+    est = IntervalForestClassifier(
+        n_estimators=2,
+        n_intervals=2,
+        interval_selection_method=interval_selection_method,
+    )
+    est.fit(X, y)
+
+    assert est.predict_proba(X).shape == (10, 2)
+
+
+@pytest.mark.parametrize(
+    "n_intervals,n_intervals_len",
+    [
+        ("sqrt", 24),
+        ("sqrt-div", 12),
+        (["sqrt-div", 2], 24),
+        ([[1, 2], "sqrt-div"], 15),
+    ],
+)
+def test_interval_forest_n_intervals(n_intervals, n_intervals_len):
+    """Test BaseIntervalForest n_interval options."""
+    X, y = make_3d_test_data(n_timepoints=20)
+
+    est = IntervalForestClassifier(
+        n_estimators=2,
+        n_intervals=n_intervals,
+        series_transformers=[None, FunctionTransformer(np.log1p)],
+        save_transformed_data=True,
+        random_state=0,
+    )
+    est.fit(X, y)
+    est.predict_proba(X)
+
+    data = est.transformed_data_
+    assert data[0].shape[1] == n_intervals_len
+
+
+att_subsample_c22 = Catch22(
+    features=[
+        "DN_HistogramMode_5",
+        "DN_HistogramMode_10",
+        "SB_BinaryStats_diff_longstretch0",
+    ]
+)
+
+
+@pytest.mark.parametrize(
+    "features,output_len",
+    [
+        (None, 3),
+        (_clone_estimator(att_subsample_c22), 3),
+        ([_clone_estimator(att_subsample_c22), _clone_estimator(att_subsample_c22)], 6),
+        (
+            [
+                row_mean,
+                Catch22(features=["DN_HistogramMode_5", "DN_HistogramMode_10"]),
+                row_numba_min,
+            ],
+            4,
+        ),
+    ],
+)
+def test_interval_forest_attribute_subsample(features, output_len):
+    """Test BaseIntervalForest subsampling with different interval features."""
+    X, y = make_3d_test_data()
+
+    est = IntervalForestClassifier(
+        n_estimators=2,
+        n_intervals=2,
+        att_subsample_size=0.5,
+        interval_features=features,
+        replace_nan=0,
+        save_transformed_data=True,
+        random_state=0,
+    )
+    est.fit(X, y)
+    est.predict_proba(X)
+
+    data = est.transformed_data_
+    assert data[0].shape[1] == int(output_len * 0.5) * 2
+
+
+def test_interval_forest_invalid_attribute_subsample():
+    """Test BaseIntervalForest with an invalid transformer for subsampling."""
+    X, y = make_3d_test_data()
+
+    est = IntervalForestClassifier(
+        n_estimators=2,
+        n_intervals=2,
+        att_subsample_size=2,
+        interval_features=SevenNumberSummaryTransformer(),
+    )
+
+    with pytest.raises(ValueError):
+        est.fit(X, y)
+
+
+@pytest.mark.parametrize(
+    "series_transformer",
+    [
+        FunctionTransformer(np.log1p),
+        [None, FunctionTransformer(np.log1p)],
+        [FunctionTransformer(np.log1p), AutocorrelationFunctionTransformer(n_lags=6)],
+    ],
+)
+def test_interval_forest_series_transformer(series_transformer):
+    """Test BaseIntervalForest with different series transformers."""
+    X, y = make_3d_test_data()
+
+    est = IntervalForestClassifier(
+        n_estimators=2,
+        n_intervals=2,
+        series_transformers=series_transformer,
+        save_transformed_data=True,
+        random_state=0,
+    )
+    est.fit(X, y)
+    est.predict_proba(X)
+
+    data = est.transformed_data_
+    expected = (
+        len(series_transformer) * 6 if isinstance(series_transformer, list) else 6
+    )
+    assert data[0].shape[1] == expected