From 814e21ad236c6947156f615d596addfeb3ef7a08 Mon Sep 17 00:00:00 2001 From: stijn Date: Mon, 24 Mar 2025 10:43:40 +0100 Subject: [PATCH 1/6] Use n_jobs parameter in KNeighborsTimeSeriesClassifier. --- .../distance_based/_time_series_neighbors.py | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/aeon/classification/distance_based/_time_series_neighbors.py b/aeon/classification/distance_based/_time_series_neighbors.py index f89b1be636..7caaaca530 100644 --- a/aeon/classification/distance_based/_time_series_neighbors.py +++ b/aeon/classification/distance_based/_time_series_neighbors.py @@ -13,6 +13,7 @@ from typing import Callable, Union import numpy as np +from joblib import Parallel, delayed from aeon.classification.base import BaseClassifier from aeon.distances import get_distance_function @@ -134,16 +135,9 @@ def _predict_proba(self, X): The class probabilities of the input samples. Classes are ordered by lexicographic order. """ - preds = np.zeros((len(X), len(self.classes_))) - for i in range(len(X)): - idx, weights = self._kneighbors(X[i]) - for id, w in zip(idx, weights): - predicted_class = self.y_[id] - preds[i, predicted_class] += w - - preds[i] = preds[i] / np.sum(preds[i]) - - return preds + self._check_is_fitted() + preds = Parallel(n_jobs=self.n_jobs)(delayed(self._proba_row)(x) for x in X) + return np.array(preds, dtype=self.classes_.dtype) def _predict(self, X): """ @@ -162,18 +156,24 @@ def _predict(self, X): Class labels for each data sample. """ self._check_is_fitted() - - preds = np.empty(len(X), dtype=self.classes_.dtype) - for i in range(len(X)): - scores = np.zeros(len(self.classes_)) - idx, weights = self._kneighbors(X[i]) - for id, w in zip(idx, weights): - predicted_class = self.y_[id] - scores[predicted_class] += w - - preds[i] = self.classes_[np.argmax(scores)] - - return preds + preds = Parallel(n_jobs=self.n_jobs)(delayed(self._predict_row)(x) for x in X) + return np.array(preds, dtype=self.classes_.dtype) + + def _proba_row(self, x): + scores = self._predict_scores(x) + return scores / np.sum(scores) + + def _predict_row(self, x): + scores = self._predict_scores(x) + return self.classes_[np.argmax(scores)] + + def _predict_scores(self, x): + scores = np.zeros(len(self.classes_)) + idx, weights = self._kneighbors(x) + for id, weight in zip(idx, weights): + predicted_class = self.y_[id] + scores[predicted_class] += weight + return scores def _kneighbors(self, X): """ From c6541eb0edacd8826ce19895d4ade9f346714457 Mon Sep 17 00:00:00 2001 From: stijn Date: Mon, 24 Mar 2025 12:02:49 +0100 Subject: [PATCH 2/6] Don't convert proba dtype. --- aeon/classification/distance_based/_time_series_neighbors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aeon/classification/distance_based/_time_series_neighbors.py b/aeon/classification/distance_based/_time_series_neighbors.py index 7caaaca530..faf9e9085e 100644 --- a/aeon/classification/distance_based/_time_series_neighbors.py +++ b/aeon/classification/distance_based/_time_series_neighbors.py @@ -137,7 +137,7 @@ def _predict_proba(self, X): """ self._check_is_fitted() preds = Parallel(n_jobs=self.n_jobs)(delayed(self._proba_row)(x) for x in X) - return np.array(preds, dtype=self.classes_.dtype) + return np.array(preds) def _predict(self, X): """ From 38485c14ea5e06805e0b75da96eaad1c7cdd0143 Mon Sep 17 00:00:00 2001 From: Stijn Date: Sat, 29 Mar 2025 09:43:15 +0100 Subject: [PATCH 3/6] Remove is_fitted checks, update docstring. --- aeon/classification/distance_based/_time_series_neighbors.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/aeon/classification/distance_based/_time_series_neighbors.py b/aeon/classification/distance_based/_time_series_neighbors.py index faf9e9085e..94e789b3e0 100644 --- a/aeon/classification/distance_based/_time_series_neighbors.py +++ b/aeon/classification/distance_based/_time_series_neighbors.py @@ -47,11 +47,10 @@ class KNeighborsTimeSeriesClassifier(BaseClassifier): n_timepoints)`` as input and returns a float. distance_params : dict, default = None Dictionary for metric parameters for the case that distance is a str. - n_jobs : int, default = None + n_jobs : int, default = 1 The number of parallel jobs to run for neighbors search. ``None`` means 1 unless in a :obj:`joblib.parallel_backend` context. ``-1`` means using all processors. - for more details. Parameter for compatibility purposes, still unimplemented. Examples -------- @@ -135,7 +134,6 @@ def _predict_proba(self, X): The class probabilities of the input samples. Classes are ordered by lexicographic order. """ - self._check_is_fitted() preds = Parallel(n_jobs=self.n_jobs)(delayed(self._proba_row)(x) for x in X) return np.array(preds) @@ -155,7 +153,6 @@ def _predict(self, X): y : array of shape (n_cases) Class labels for each data sample. """ - self._check_is_fitted() preds = Parallel(n_jobs=self.n_jobs)(delayed(self._predict_row)(x) for x in X) return np.array(preds, dtype=self.classes_.dtype) From 4c7f2039f982e5324b152f7184aa52e292d3b218 Mon Sep 17 00:00:00 2001 From: Stijn Date: Sat, 29 Mar 2025 09:52:27 +0100 Subject: [PATCH 4/6] Implement parallel backend param. --- .../distance_based/_time_series_neighbors.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/aeon/classification/distance_based/_time_series_neighbors.py b/aeon/classification/distance_based/_time_series_neighbors.py index 94e789b3e0..54cabca55d 100644 --- a/aeon/classification/distance_based/_time_series_neighbors.py +++ b/aeon/classification/distance_based/_time_series_neighbors.py @@ -51,6 +51,11 @@ class KNeighborsTimeSeriesClassifier(BaseClassifier): The number of parallel jobs to run for neighbors search. ``None`` means 1 unless in a :obj:`joblib.parallel_backend` context. ``-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. Examples -------- @@ -79,11 +84,13 @@ def __init__( n_neighbors: int = 1, weights: Union[str, Callable] = "uniform", n_jobs: int = 1, + parallel_backend: str = None, ) -> None: self.distance = distance self.distance_params = distance_params self.n_neighbors = n_neighbors self.n_jobs = n_jobs + self.parallel_backend = parallel_backend self._distance_params = distance_params if self._distance_params is None: @@ -134,7 +141,9 @@ def _predict_proba(self, X): The class probabilities of the input samples. Classes are ordered by lexicographic order. """ - preds = Parallel(n_jobs=self.n_jobs)(delayed(self._proba_row)(x) for x in X) + preds = Parallel(n_jobs=self.n_jobs, parallel_backend=self.parallel_backend)( + delayed(self._proba_row)(x) for x in X + ) return np.array(preds) def _predict(self, X): @@ -153,7 +162,9 @@ def _predict(self, X): y : array of shape (n_cases) Class labels for each data sample. """ - preds = Parallel(n_jobs=self.n_jobs)(delayed(self._predict_row)(x) for x in X) + preds = Parallel(n_jobs=self.n_jobs, parallel_backend=self.parallel_backend)( + delayed(self._predict_row)(x) for x in X + ) return np.array(preds, dtype=self.classes_.dtype) def _proba_row(self, x): From 7dc8d2376f01e6400e917c6f128c62d87da22388 Mon Sep 17 00:00:00 2001 From: Stijn Date: Sat, 29 Mar 2025 12:24:56 +0100 Subject: [PATCH 5/6] Correct keyword argument for Parallel. --- aeon/classification/distance_based/_time_series_neighbors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aeon/classification/distance_based/_time_series_neighbors.py b/aeon/classification/distance_based/_time_series_neighbors.py index 54cabca55d..9d58dcb8cc 100644 --- a/aeon/classification/distance_based/_time_series_neighbors.py +++ b/aeon/classification/distance_based/_time_series_neighbors.py @@ -141,7 +141,7 @@ def _predict_proba(self, X): The class probabilities of the input samples. Classes are ordered by lexicographic order. """ - preds = Parallel(n_jobs=self.n_jobs, parallel_backend=self.parallel_backend)( + preds = Parallel(n_jobs=self.n_jobs, backend=self.parallel_backend)( delayed(self._proba_row)(x) for x in X ) return np.array(preds) @@ -162,7 +162,7 @@ def _predict(self, X): y : array of shape (n_cases) Class labels for each data sample. """ - preds = Parallel(n_jobs=self.n_jobs, parallel_backend=self.parallel_backend)( + preds = Parallel(n_jobs=self.n_jobs, backend=self.parallel_backend)( delayed(self._predict_row)(x) for x in X ) return np.array(preds, dtype=self.classes_.dtype) From 10444a6df7793efb5795be78c7264517ec3bfa1a Mon Sep 17 00:00:00 2001 From: Stijn Date: Wed, 9 Apr 2025 11:26:26 +0200 Subject: [PATCH 6/6] Add check_n_jobs. --- .../distance_based/_time_series_neighbors.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/aeon/classification/distance_based/_time_series_neighbors.py b/aeon/classification/distance_based/_time_series_neighbors.py index 9d58dcb8cc..b706d7392e 100644 --- a/aeon/classification/distance_based/_time_series_neighbors.py +++ b/aeon/classification/distance_based/_time_series_neighbors.py @@ -17,6 +17,7 @@ from aeon.classification.base import BaseClassifier from aeon.distances import get_distance_function +from aeon.utils.validation import check_n_jobs WEIGHTS_SUPPORTED = ["uniform", "distance"] @@ -141,7 +142,8 @@ def _predict_proba(self, X): The class probabilities of the input samples. Classes are ordered by lexicographic order. """ - preds = Parallel(n_jobs=self.n_jobs, backend=self.parallel_backend)( + n_jobs = check_n_jobs(self.n_jobs) + preds = Parallel(n_jobs=n_jobs, backend=self.parallel_backend)( delayed(self._proba_row)(x) for x in X ) return np.array(preds) @@ -162,7 +164,8 @@ def _predict(self, X): y : array of shape (n_cases) Class labels for each data sample. """ - preds = Parallel(n_jobs=self.n_jobs, backend=self.parallel_backend)( + n_jobs = check_n_jobs(self.n_jobs) + preds = Parallel(n_jobs=n_jobs, backend=self.parallel_backend)( delayed(self._predict_row)(x) for x in X ) return np.array(preds, dtype=self.classes_.dtype)