diff --git a/.readthedocs.yml b/.readthedocs.yml index 44d7f569e..7a479688a 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -9,7 +9,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.8" + python: "3.10" # Build documentation in the docs/ directory with Sphinx sphinx: diff --git a/CHANGELOG.md b/CHANGELOG.md index 542df512c..b5d2d37a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ Changelogs for this project are recorded in this file since v0.2.0. ## [Towards v0.7] +### Changed + +* The `shapelets` module now depends on Keras3+ (should be keras-backend-blind) and not anymore on TF + ## [v0.6.3] ### Changed diff --git a/azure-pipelines.yml b/azure-pipelines.yml index cc04f70c0..347525f62 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,8 +10,8 @@ jobs: vmImage: 'ubuntu-latest' strategy: matrix: - Python38: - python.version: '3.8' + # Python38: + # python.version: '3.8' Python39: python.version: '3.9' variables: @@ -42,8 +42,7 @@ jobs: set -xe python -m pip install pytest pytest-azurepipelines python -m pip install scikit-learn==1.2 - python -m pip install tensorflow==2.9.0 - python -m pytest -v tslearn/ --doctest-modules + export KERAS_BACKEND="torch" && python -m pytest -v tslearn/ --doctest-modules displayName: 'Test' - job: 'linux_without_torch' @@ -80,9 +79,7 @@ jobs: - script: | set -xe python -m pip install pytest pytest-azurepipelines - python -m pip install scikit-learn==1.2 - python -m pip install tensorflow==2.9.0 - python -m pytest -v tslearn/ --doctest-modules -k 'not tslearn.metrics.softdtw_variants.soft_dtw and not tslearn.metrics.softdtw_variants.cdist_soft_dtw and not tslearn.metrics.dtw_variants.dtw or tslearn.metrics.dtw_variants.dtw_' + python -m pytest -v tslearn/ --doctest-modules --ignore tslearn/shapelets/ -k 'not tslearn.metrics.softdtw_variants.soft_dtw and not tslearn.metrics.softdtw_variants.cdist_soft_dtw and not tslearn.metrics.dtw_variants.dtw or tslearn.metrics.dtw_variants.dtw_' displayName: 'Test' @@ -124,8 +121,7 @@ jobs: python -m pip install coverage pytest-cov python -m pip install cesium pandas stumpy python -m pip install scikit-learn==1.2 - python -m pip install tensorflow==2.9.0 - python -m pytest -v tslearn/ --doctest-modules --cov=tslearn + export KERAS_BACKEND="torch" && python -m pytest -v tslearn/ --doctest-modules --cov=tslearn displayName: 'Test' # Upload coverage to codecov.io @@ -139,8 +135,8 @@ jobs: vmImage: 'macOS-12' strategy: matrix: - Python38: - python.version: '3.8' + # Python38: + # python.version: '3.8' Python39: python.version: '3.9' Python310: @@ -176,8 +172,7 @@ jobs: set -xe python -m pip install pytest pytest-azurepipelines python -m pip install scikit-learn==1.2 - python -m pip install tensorflow==2.9.0 - python -m pytest -v tslearn/ --doctest-modules -k 'not test_all_estimators' + export KERAS_BACKEND="torch" && python -m pytest -v tslearn/ --doctest-modules -k 'not test_all_estimators' displayName: 'Test' @@ -186,9 +181,9 @@ jobs: vmImage: 'windows-latest' strategy: matrix: - Python38: - python_ver: '38' - python.version: '3.8' + # Python38: + # python_ver: '38' + # python.version: '3.8' Python39: python_ver: '39' python.version: '3.9' @@ -219,6 +214,5 @@ jobs: - script: | python -m pip install pytest pytest-azurepipelines python -m pip install scikit-learn==1.2 - python -m pip install tensorflow==2.9.0 - python -m pytest -v tslearn/ --doctest-modules --ignore tslearn/tests/test_estimators.py --ignore tslearn/utils/cast.py + set KERAS_BACKEND=torch && python -m pytest -v tslearn/ --doctest-modules --ignore tslearn/tests/test_estimators.py --ignore tslearn/utils/cast.py displayName: 'Test' diff --git a/docs/examples/classification/plot_shapelet_distances.py b/docs/examples/classification/plot_shapelet_distances.py index fdd9a3073..c11995564 100644 --- a/docs/examples/classification/plot_shapelet_distances.py +++ b/docs/examples/classification/plot_shapelet_distances.py @@ -24,7 +24,7 @@ from tslearn.datasets import CachedDatasets from tslearn.preprocessing import TimeSeriesScalerMinMax from tslearn.shapelets import LearningShapelets -from tensorflow.keras.optimizers import Adam +from keras.optimizers import Adam # Set a seed to ensure determinism numpy.random.seed(42) @@ -45,7 +45,7 @@ # Define the model and fit it using the training data shp_clf = LearningShapelets(n_shapelets_per_size=shapelet_sizes, weight_regularizer=0.0001, - optimizer=Adam(lr=0.01), + optimizer=Adam(learning_rate=0.01), max_iter=300, verbose=0, scale=False, diff --git a/docs/examples/classification/plot_shapelet_locations.py b/docs/examples/classification/plot_shapelet_locations.py index c55987d27..556af2251 100644 --- a/docs/examples/classification/plot_shapelet_locations.py +++ b/docs/examples/classification/plot_shapelet_locations.py @@ -25,7 +25,7 @@ from tslearn.preprocessing import TimeSeriesScalerMinMax from tslearn.shapelets import LearningShapelets, \ grabocka_params_to_shapelet_size_dict -from tensorflow.keras.optimizers import Adam +from keras.optimizers import Adam # Set a seed to ensure determinism numpy.random.seed(42) @@ -51,7 +51,7 @@ # Define the model and fit it using the training data shp_clf = LearningShapelets(n_shapelets_per_size=shapelet_sizes, weight_regularizer=0.001, - optimizer=Adam(lr=0.01), + optimizer=Adam(learning_rate=0.01), max_iter=250, verbose=0, scale=False, diff --git a/docs/examples/classification/plot_shapelets.py b/docs/examples/classification/plot_shapelets.py index 150b70a19..202b5ebad 100644 --- a/docs/examples/classification/plot_shapelets.py +++ b/docs/examples/classification/plot_shapelets.py @@ -16,7 +16,7 @@ import numpy from sklearn.metrics import accuracy_score -import tensorflow as tf +import keras import matplotlib.pyplot as plt from tslearn.datasets import CachedDatasets @@ -49,7 +49,7 @@ # Define the model using parameters provided by the authors (except that we # use fewer iterations here) shp_clf = LearningShapelets(n_shapelets_per_size=shapelet_sizes, - optimizer=tf.optimizers.Adam(.01), + optimizer=keras.optimizers.Adam(learning_rate=.01), batch_size=16, weight_regularizer=.01, max_iter=200, diff --git a/docs/requirements_rtd.txt b/docs/requirements_rtd.txt index 6859395e1..b1ab951cb 100644 --- a/docs/requirements_rtd.txt +++ b/docs/requirements_rtd.txt @@ -8,7 +8,7 @@ ipykernel nbsphinx sphinx-gallery pillow -tensorflow>=2 +keras>=3 Pygments numba sphinx_bootstrap_theme diff --git a/requirements.txt b/requirements.txt index b23ded6e5..79ce5ae66 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ numba scipy scikit-learn joblib>=0.12 -tensorflow>=2 +keras>=3 pandas cesium h5py diff --git a/requirements_nocast.txt b/requirements_nocast.txt index 270f87f5d..2d71623df 100644 --- a/requirements_nocast.txt +++ b/requirements_nocast.txt @@ -3,5 +3,5 @@ numba scipy scikit-learn joblib>=0.12 -tensorflow>=2 +keras>=3 h5py diff --git a/tslearn/shapelets/__init__.py b/tslearn/shapelets/__init__.py index cb5bc5707..b84c11e17 100644 --- a/tslearn/shapelets/__init__.py +++ b/tslearn/shapelets/__init__.py @@ -1,7 +1,7 @@ """ The :mod:`tslearn.shapelets` module gathers Shapelet-based algorithms. -It depends on the `tensorflow` library for optimization (TF2 is required). +It depends on the `keras` library (Keras3+ is required). **User guide:** See the :ref:`Shapelets ` section for further details. diff --git a/tslearn/shapelets/shapelets.py b/tslearn/shapelets/shapelets.py index 50476a2ba..6b000ffac 100644 --- a/tslearn/shapelets/shapelets.py +++ b/tslearn/shapelets/shapelets.py @@ -1,19 +1,20 @@ -from tensorflow.keras.models import Model, model_from_json -from tensorflow.keras.layers import (InputSpec, Dense, Conv1D, Layer, Input, - concatenate, add) -from tensorflow.keras.metrics import (categorical_accuracy, - categorical_crossentropy, - binary_accuracy, binary_crossentropy) -from tensorflow.keras.utils import to_categorical +import keras +from keras.models import Model, model_from_json +from keras.layers import (InputSpec, Dense, Conv1D, Layer, Input, + concatenate, add) +from keras.metrics import (categorical_accuracy, + categorical_crossentropy, + binary_accuracy, binary_crossentropy) +from keras.utils import to_categorical +from keras.regularizers import l2 +from keras.initializers import Initializer +import keras.ops as ops + from sklearn.base import ClassifierMixin, TransformerMixin from sklearn.utils import check_array, check_X_y from sklearn.utils.validation import check_is_fitted from sklearn.utils.multiclass import unique_labels -from tensorflow.keras.regularizers import l2 -from tensorflow.keras.initializers import Initializer -import tensorflow.keras.backend as K import numpy -import tensorflow as tf import warnings @@ -35,33 +36,46 @@ class GlobalMinPooling1D(Layer): Examples -------- - >>> x = tf.constant([5.0, numpy.nan, 6.8, numpy.nan, numpy.inf]) - >>> x = tf.reshape(x, [1, 5, 1]) + >>> x = numpy.array([5.0, numpy.nan, 6.8, numpy.nan, numpy.inf]) + >>> x = x.reshape([1, 5, 1]) >>> GlobalMinPooling1D()(x).numpy() array([[5.]], dtype=float32) """ - def __init__(self, **kwargs): + def __init__(self, data_format=None, keepdims=False, **kwargs): super().__init__(**kwargs) + + self.data_format = ( + "channels_last" if data_format is None else data_format + ) + self.keepdims = keepdims self.input_spec = InputSpec(ndim=3) def compute_output_shape(self, input_shape): return input_shape[0], input_shape[2] - def call(self, inputs, **kwargs): - inputs_without_nans = tf.where(tf.math.is_finite(inputs), - inputs, - tf.zeros_like(inputs) + numpy.inf) - return tf.reduce_min(inputs_without_nans, axis=1) + def call(self, inputs): + steps_axis = 1 if self.data_format == "channels_last" else 2 + inputs_without_nans = ops.where(ops.isfinite(inputs), + inputs, + ops.zeros_like(inputs) + ops.max(inputs[ops.isfinite(inputs)])) + return ops.min(inputs_without_nans, axis=steps_axis, keepdims=self.keepdims) class GlobalArgminPooling1D(Layer): - """Global min pooling operation for temporal data. + """Global argmin pooling operation for temporal data. # Input shape 3D tensor with shape: `(batch_size, steps, features)`. # Output shape 2D tensor with shape: `(batch_size, features)` + + Examples + -------- + >>> x = numpy.array([5.0, numpy.nan, 6.8, numpy.nan, numpy.inf]) + >>> x = x.reshape([1, 5, 1]) + >>> GlobalArgminPooling1D()(x).numpy() + array([[0.]], dtype=float32) """ def __init__(self, **kwargs): @@ -72,7 +86,10 @@ def compute_output_shape(self, input_shape): return input_shape[0], input_shape[2] def call(self, inputs, **kwargs): - return K.cast(K.argmin(inputs, axis=1), dtype=K.floatx()) + inputs_without_nans = ops.where(ops.isfinite(inputs), + inputs, + ops.zeros_like(inputs) + ops.max(inputs[ops.isfinite(inputs)])) + return ops.cast(ops.argmin(inputs_without_nans, axis=1), dtype=float) def _kmeans_init_shapelets(X, n_shapelets, shp_len, n_draw=10000): @@ -106,7 +123,7 @@ def __call__(self, shape, dtype=None): shapelets = _kmeans_init_shapelets(self.X_, n_shapelets, shp_len)[:, :, 0] - return tf.convert_to_tensor(shapelets, dtype=K.floatx()) + return ops.convert_to_tensor(shapelets, dtype=float) def get_config(self): return {'data': self.X_} @@ -140,11 +157,11 @@ def build(self, input_shape): def call(self, x, **kwargs): # (x - y)^2 = x^2 + y^2 - 2 * x * y - x_sq = K.expand_dims(K.sum(x ** 2, axis=2), axis=-1) - y_sq = K.reshape(K.sum(self.kernel ** 2, axis=1), + x_sq = ops.expand_dims(ops.sum(x ** 2, axis=2), axis=-1) + y_sq = ops.reshape(ops.sum(self.kernel ** 2, axis=1), (1, 1, self.n_shapelets)) - xy = K.dot(x, K.transpose(self.kernel)) - return (x_sq + y_sq - 2 * xy) / K.int_shape(self.kernel)[1] + xy = ops.dot(x, ops.transpose(self.kernel)) + return (x_sq + y_sq - 2 * xy) / ops.shape(self.kernel)[1] def compute_output_shape(self, input_shape): return input_shape[0], input_shape[1], self.n_shapelets @@ -426,7 +443,7 @@ def fit(self, X, y): self._check_series_length(X) if self.random_state is not None: - tf.keras.utils.set_random_seed(seed=self.random_state) + keras.utils.set_random_seed(seed=self.random_state) n_ts, sz, d = X.shape self._X_fit_dims = X.shape @@ -567,7 +584,7 @@ def locate(self, X): >>> X[0, 4:7, 0] = numpy.array([1, 2, 3]) >>> y = [1, 0, 0] >>> # Data is all zeros except a motif 1-2-3 in the first time series - >>> clf = LearningShapelets(n_shapelets_per_size={3: 1}, max_iter=0, + >>> clf = LearningShapelets(n_shapelets_per_size={3: 1}, max_iter=1, ... verbose=0) >>> _ = clf.fit(X, y) >>> weights_shapelet = [ @@ -780,7 +797,7 @@ def get_weights(self, layer_name=None): -------- >>> from tslearn.generators import random_walk_blobs >>> X, y = random_walk_blobs(n_ts_per_blob=100, sz=256, d=1, n_blobs=3) - >>> clf = LearningShapelets(n_shapelets_per_size={10: 5}, max_iter=0, + >>> clf = LearningShapelets(n_shapelets_per_size={10: 5}, max_iter=1, ... verbose=0) >>> clf.fit(X, y).get_weights("classification")[0].shape (5, 3) @@ -816,7 +833,7 @@ def set_weights(self, weights, layer_name=None): -------- >>> from tslearn.generators import random_walk_blobs >>> X, y = random_walk_blobs(n_ts_per_blob=10, sz=16, d=1, n_blobs=3) - >>> clf = LearningShapelets(n_shapelets_per_size={3: 1}, max_iter=0, + >>> clf = LearningShapelets(n_shapelets_per_size={3: 1}, max_iter=1, ... verbose=0) >>> _ = clf.fit(X, y) >>> weights_shapelet = [ diff --git a/tslearn/tests/test_estimators.py b/tslearn/tests/test_estimators.py index 08108e38d..0cb62edd1 100644 --- a/tslearn/tests/test_estimators.py +++ b/tslearn/tests/test_estimators.py @@ -77,7 +77,7 @@ def _get_all_classes(): # keras is likely not installed warnings.warn('Skipped common tests for shapelets ' 'as it could not be imported. keras ' - '(and tensorflow) are probably not ' + 'is probably not ' 'installed!') continue elif name.endswith('pytorch_backend'): diff --git a/tslearn/tests/test_shapelets.py b/tslearn/tests/test_shapelets.py index f58a519b9..466f10b5c 100644 --- a/tslearn/tests/test_shapelets.py +++ b/tslearn/tests/test_shapelets.py @@ -8,9 +8,9 @@ def test_shapelets(): - pytest.importorskip('tensorflow') + pytest.importorskip('keras') from tslearn.shapelets import LearningShapelets - import tensorflow as tf + import keras n, sz, d = 15, 10, 2 rng = np.random.RandomState(0) @@ -27,7 +27,7 @@ def test_shapelets(): clf = LearningShapelets(n_shapelets_per_size={2: 5}, max_iter=1, verbose=0, - optimizer=tf.optimizers.Adam(.1), + optimizer=keras.optimizers.Adam(learning_rate=.1), random_state=0) cross_validate(clf, time_series, y, cv=2) @@ -62,7 +62,7 @@ def test_shapelets(): def test_shapelet_lengths(): - pytest.importorskip('tensorflow') + pytest.importorskip('keras') from tslearn.shapelets import LearningShapelets # Test variable-length @@ -97,7 +97,7 @@ def test_shapelet_lengths(): def test_series_lengths(): - pytest.importorskip('tensorflow') + pytest.importorskip('keras') from tslearn.shapelets import LearningShapelets # Test long shapelets