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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions aeon/transformations/collection/interval_based/_interval_features.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""Shared helpers for extracting transformer features from interval slices.

``RandomIntervals`` and ``SupervisedIntervals`` both extract features from single
channel slices of a collection that the top-level ``fit`` has already validated.
Calling a transformer feature's public ``fit``/``fit_transform``/``transform`` on
every slice repeats that validation thousands of times per forest fit, so for aeon
collection transformers these helpers call the private methods instead, which
assume input already in the ``numpy3D`` inner type.
"""

__maintainer__ = []
__all__ = ["_fit_feature", "_fit_transform_feature", "_transform_feature"]

import numpy as np

from aeon.transformations.collection.base import BaseCollectionTransformer


def _fit_feature(feature, X, y=None, expand_fallback=True):
"""Fit a transformer feature on a single channel interval slice.

Parameters
----------
feature : BaseTransformer
The transformer feature to fit.
X : 2D np.ndarray of shape (n_cases, interval_length)
A single channel interval slice, taken from input the top-level ``fit``
has already validated.
y : 1D np.ndarray or None, default=None
Class labels, passed on to the feature.
expand_fallback : bool, default=True
Whether a feature that is not a collection transformer receives the
slice expanded to ``numpy3D`` along with ``y``, or the 2D slice on its
own. See the note below.

Returns
-------
feature : BaseTransformer
The fitted feature.

Notes
-----
``expand_fallback`` exists only to preserve the two callers' pre-existing
behaviour for features that are not aeon collection transformers:
``RandomIntervals`` passes such a feature the expanded slice and ``y``,
``SupervisedIntervals`` passes the 2D slice and no ``y``. The collection
transformer path, which is the one this module exists to speed up, is the
same for both.
"""
if isinstance(feature, BaseCollectionTransformer):
return feature._fit(np.expand_dims(X, axis=1), y)
if expand_fallback:
return feature.fit(np.expand_dims(X, axis=1), y)
return feature.fit(X)


def _fit_transform_feature(feature, X, y=None, expand_fallback=True):
"""Fit and transform a transformer feature on a single channel interval slice.

Parameters
----------
feature : BaseTransformer
The transformer feature to fit and apply.
X : 2D np.ndarray of shape (n_cases, interval_length)
A single channel interval slice, taken from input the top-level ``fit``
has already validated.
y : 1D np.ndarray or None, default=None
Class labels, passed on to the feature.
expand_fallback : bool, default=True
Whether a feature that is not a collection transformer receives the
slice expanded to ``numpy3D`` along with ``y``, or the 2D slice on its
own. See the note in ``_fit_feature``.

Returns
-------
Xt : np.ndarray
The extracted features, in whatever shape the feature returns.
"""
if isinstance(feature, BaseCollectionTransformer):
return feature._fit_transform(np.expand_dims(X, axis=1), y)
if expand_fallback:
return feature.fit_transform(np.expand_dims(X, axis=1), y)
return feature.fit_transform(X)


def _transform_feature(feature, X, expand_fallback=True):
"""Transform a single channel interval slice with a fitted transformer feature.

Parameters
----------
feature : BaseTransformer
The fitted transformer feature to apply.
X : 2D np.ndarray of shape (n_cases, interval_length)
A single channel interval slice, taken from input the top-level
``transform`` has already validated.
expand_fallback : bool, default=True
Whether a feature that is not a collection transformer receives the
slice expanded to ``numpy3D``, or the 2D slice. See the note in
``_fit_feature``.

Returns
-------
Xt : np.ndarray
The extracted features, in whatever shape the feature returns.
"""
if isinstance(feature, BaseCollectionTransformer):
return feature._transform(np.expand_dims(X, axis=1))
if expand_fallback:
return feature.transform(np.expand_dims(X, axis=1))
return feature.transform(X)
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
from aeon.base._base import _clone_estimator
from aeon.transformations.base import BaseTransformer
from aeon.transformations.collection.base import BaseCollectionTransformer
from aeon.transformations.collection.interval_based._interval_features import (
_fit_feature,
_fit_transform_feature,
_transform_feature,
)
from aeon.utils._parallel import _run_jobs
from aeon.utils.numba.stats import (
row_mean,
Expand Down Expand Up @@ -361,30 +366,25 @@ def _generate_interval(self, X, y, seed, transform):

for feature in self._features:
if isinstance(feature, BaseTransformer):
# X was validated by the top-level fit, so a slice of it is
# already in the numpy3D inner type once expanded; the helpers
# skip the redundant per-slice checks for collection
# transformers.
interval = X[:, dim, interval_start:interval_end:dilation]
if transform:
feature = _clone_estimator(
feature,
seed,
)

t = feature.fit_transform(
np.expand_dims(
X[:, dim, interval_start:interval_end:dilation], axis=1
),
y,
)
t = _fit_transform_feature(feature, interval, y)

if t.ndim == 3 and t.shape[1] == 1:
t = t.reshape((t.shape[0], t.shape[2]))

Xt_parts.append(t)
else:
feature.fit(
np.expand_dims(
X[:, dim, interval_start:interval_end:dilation], axis=1
),
y,
)
_fit_feature(feature, interval, y)
elif transform:
t = np.asarray(
feature(X[:, dim, interval_start:interval_end:dilation])
Expand Down Expand Up @@ -413,8 +413,9 @@ def _transform_interval(self, X, idx, keep_transform):
return np.zeros((X.shape[0], 1))

if isinstance(feature, BaseTransformer):
Xt = feature.transform(
np.expand_dims(X[:, dim, interval_start:interval_end:dilation], axis=1)
# See _generate_interval: private path skips redundant per-slice checks.
Xt = _transform_feature(
feature, X[:, dim, interval_start:interval_end:dilation]
)

if Xt.ndim == 3:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
from aeon.base._base import _clone_estimator
from aeon.transformations.base import BaseTransformer
from aeon.transformations.collection.base import BaseCollectionTransformer
from aeon.transformations.collection.interval_based._interval_features import (
_fit_transform_feature,
_transform_feature,
)
from aeon.utils.numba.general import z_normalise_series_3d
from aeon.utils.numba.stats import (
fisher_score,
Expand Down Expand Up @@ -411,7 +415,9 @@ def _transform_intervals(self, X, idx):
start, end, dim, feature = self.intervals_[idx]

if isinstance(feature, BaseTransformer):
return feature.transform(X[:, dim, start:end]).flatten()
return _transform_feature(
feature, X[:, dim, start:end], expand_fallback=False
).flatten()
else:
return feature(X[:, dim, start:end])

Expand Down Expand Up @@ -445,8 +451,12 @@ def _supervised_search(
sub_interval_1 = X[:, div_point:]

if feature_is_transformer:
interval_feature_0 = feature.fit_transform(sub_interval_0).flatten()
interval_feature_1 = feature.fit_transform(sub_interval_1).flatten()
interval_feature_0 = _fit_transform_feature(
feature, sub_interval_0, expand_fallback=False
).flatten()
interval_feature_1 = _fit_transform_feature(
feature, sub_interval_1, expand_fallback=False
).flatten()
else:
interval_feature_0 = feature(sub_interval_0)
interval_feature_1 = feature(sub_interval_1)
Expand All @@ -463,8 +473,8 @@ def _supervised_search(
if keep_transform:
if self.normalise_for_search:
if feature_is_transformer:
interval_feature_to_use = feature.transform(
X_ori[:, ini_idx:end]
interval_feature_to_use = _transform_feature(
feature, X_ori[:, ini_idx:end], expand_fallback=False
).flatten()
else:
interval_feature_to_use = feature(X_ori[:, ini_idx:end])
Expand All @@ -490,8 +500,8 @@ def _supervised_search(
if keep_transform:
if self.normalise_for_search:
if feature_is_transformer:
interval_feature_to_use = feature.transform(
X_ori[:, ini_idx:end]
interval_feature_to_use = _transform_feature(
feature, X_ori[:, ini_idx:end], expand_fallback=False
).flatten()
else:
interval_feature_to_use = feature(X_ori[:, ini_idx:end])
Expand Down
Loading