Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] InceptionTime does not support validation data with fewer classes than in the training data #5864

Open
cedricdonie opened this issue Jun 17, 2022 · 8 comments · May be fixed by sktime/sktime-dl#132 or #6373
Labels
bug Something isn't working module:classification classification module: time series classification
Projects

Comments

@cedricdonie
Copy link

Describe the bug
When I input a validation dataset that contains fewer categories than the training dataset, I get the following error.

  File "src/models/train_model.py", line 162, in train
    classifier.fit(X, y, validation_X=X_val, validation_y=y_val)
  File "/usr/local/lib/python3.6/dist-packages/sktime_dl/classification/_inceptiontime.py", line 189, in fit
    validation_data=validation_data,
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/keras/engine/training.py", line 1225, in fit
    _use_cached_eval_dataset=True)
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/keras/engine/training.py", line 1489, in evaluate
    tmp_logs = self.test_function(iterator)
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/eager/def_function.py", line 889, in __call__
    result = self._call(*args, **kwds)
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/eager/def_function.py", line 933, in _call
    self._initialize(args, kwds, add_initializers_to=initializers)
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/eager/def_function.py", line 764, in _initialize
    *args, **kwds))
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/eager/function.py", line 3050, in _get_concrete_function_internal_garbage_collected
    graph_function, _ = self._maybe_define_function(args, kwargs)
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/eager/function.py", line 3444, in _maybe_define_function
    graph_function = self._create_graph_function(args, kwargs)
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/eager/function.py", line 3289, in _create_graph_function
    capture_by_value=self._capture_by_value),
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/framework/func_graph.py", line 999, in func_graph_from_py_func
    func_outputs = python_func(*func_args, **func_kwargs)
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/eager/def_function.py", line 672, in wrapped_fn
    out = weak_wrapped_fn().__wrapped__(*args, **kwds)
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/framework/func_graph.py", line 986, in wrapper
    raise e.ag_error_metadata.to_exception(e)
ValueError: in user code:

    /usr/local/lib/python3.6/dist-packages/tensorflow/python/keras/engine/training.py:1323 test_function  *
        return step_function(self, iterator)
    /usr/local/lib/python3.6/dist-packages/tensorflow/python/keras/engine/training.py:1314 step_function  **
        outputs = model.distribute_strategy.run(run_step, args=(data,))
    /usr/local/lib/python3.6/dist-packages/tensorflow/python/distribute/distribute_lib.py:1285 run
        return self._extended.call_for_each_replica(fn, args=args, kwargs=kwargs)
    /usr/local/lib/python3.6/dist-packages/tensorflow/python/distribute/distribute_lib.py:2833 call_for_each_replica
        return self._call_for_each_replica(fn, args, kwargs)
    /usr/local/lib/python3.6/dist-packages/tensorflow/python/distribute/distribute_lib.py:3608 _call_for_each_replica
        return fn(*args, **kwargs)
    /usr/local/lib/python3.6/dist-packages/tensorflow/python/keras/engine/training.py:1307 run_step  **
        outputs = model.test_step(data)
    /usr/local/lib/python3.6/dist-packages/tensorflow/python/keras/engine/training.py:1269 test_step
        y, y_pred, sample_weight, regularization_losses=self.losses)
    /usr/local/lib/python3.6/dist-packages/tensorflow/python/keras/engine/compile_utils.py:204 __call__
        loss_value = loss_obj(y_t, y_p, sample_weight=sw)
    /usr/local/lib/python3.6/dist-packages/tensorflow/python/keras/losses.py:155 __call__
        losses = call_fn(y_true, y_pred)
    /usr/local/lib/python3.6/dist-packages/tensorflow/python/keras/losses.py:259 call  **
        return ag_fn(y_true, y_pred, **self._fn_kwargs)
    /usr/local/lib/python3.6/dist-packages/tensorflow/python/util/dispatch.py:206 wrapper
        return target(*args, **kwargs)
    /usr/local/lib/python3.6/dist-packages/tensorflow/python/keras/losses.py:1644 categorical_crossentropy
        y_true, y_pred, from_logits=from_logits)
    /usr/local/lib/python3.6/dist-packages/tensorflow/python/util/dispatch.py:206 wrapper
        return target(*args, **kwargs)
    /usr/local/lib/python3.6/dist-packages/tensorflow/python/keras/backend.py:4862 categorical_crossentropy
        target.shape.assert_is_compatible_with(output.shape)
    /usr/local/lib/python3.6/dist-packages/tensorflow/python/framework/tensor_shape.py:1161 assert_is_compatible_with
        raise ValueError("Shapes %s and %s are incompatible" % (self, other))

    ValueError: Shapes (None, 3) and (None, 4) are incompatible

To Reproduce
I'm having trouble producing a MWE. The following code works unexpectedly.

from sktime.datasets import load_basic_motions
from sktime_dl.classification import InceptionTimeClassifier
import numpy as np


def test_validation_has_fewer_classes_than_test():
    X, _ = load_basic_motions(return_X_y=True, split="train")
    X_val, _ = load_basic_motions(return_X_y=True, split="test")

    y = np.ones(len(X))
    y[0] = 0
    y[1] = 2

    y_val = np.ones_like(y)
    y_val[0] = 0

    clf = InceptionTimeClassifier(nb_epochs=2)

    clf.fit(X, y, validation_data=(X_val, y_val))

Expected behavior
The label encoder will pad with zeros, allowing training with validation data with fewer classes.

Additional context
The onehot encoder is refitted to validation data. Maybe that is the issue?

Versions

Linux-5.13.0-51-generic-x86_64-with-Ubuntu-20.04-focal Python 3.6.15 (default, Apr 25 2022, 01:55:53) [GCC 9.4.0] NumPy 1.19.5 SciPy 1.5.4 sktime 0.7.0 sktime_dl 0.2.0
@cedricdonie cedricdonie added the bug Something isn't working label Jun 17, 2022
@fkiraly fkiraly transferred this issue from sktime/sktime-dl Jan 30, 2024
@fkiraly fkiraly added the module:classification classification module: time series classification label Jan 30, 2024
@fkiraly fkiraly added this to Needs triage & validation in Bugfixing via automation Jan 30, 2024
@cedricdonie
Copy link
Author

I just had a brief glance at the former sktime-dl classifiers migrated to the sktime repository. It seems like the issue is fixed, but there is a potential footgun. I would be interested in a maintainers opinion on how to proceed. Or is my interpretation incorrect?

Potential problem

One one hand, it seems like OneHotEncoder is only used in convert_y_to_keras.

def convert_y_to_keras(self, y):
"""Convert y to required Keras format."""
self.label_encoder = LabelEncoder()
y = self.label_encoder.fit_transform(y)
self.classes_ = self.label_encoder.classes_
self.n_classes_ = len(self.classes_)
y = y.reshape(len(y), 1)
# in sklearn 1.2, sparse was renamed to sparse_output
if _check_soft_dependencies("sklearn>=1.2", severity="none"):
sparse_kw = {"sparse_output": False}
else:
sparse_kw = {"sparse": False}
self.onehot_encoder = OneHotEncoder(categories="auto", **sparse_kw)
# categories='auto' to get rid of FutureWarning
y = self.onehot_encoder.fit_transform(y)
return y

In turn, convert_y_to_keras is referenced by all classifiers in the _fit but not in the _predict functions.
def _fit(self, X, y):
"""Fit the classifier on the training set (X, y).
Parameters
----------
X : np.ndarray of shape = (n_instances (n), n_dimensions (d), series_length (m))
The training input samples.
y : np.ndarray of shape n
The training data class labels.
Returns
-------
self : object
"""
y_onehot = self.convert_y_to_keras(y)
.
Thus, predicting will not implicitly (and wrongly) re-fit the label-encoder.

On the other hand, _predict_proba methods will presumably output a OneHot-encoded result too [1]. A naive user might be tempted to do something like this:

X_train, y_train = load_training_data(...)  # y is encoded as categorical class labels.
X_test, y_test = load_training_data(...)  # y is encoded as categorical class labels.

clf = InceptionTimeClassifier()  # Or many other deep classifiers
clf.fit(X_train, y_train)
score = clf.score(X_test, clf.convert_y_to_keras(y_test))  # Because clf.score(X_test, y_test) will fail.

Possible solutions

  • Add a fit=False keyword argument to convert_y_to_keras and only set it to true in the _fit method of the subclasses. When fit=False, the transform methods OneHotEncoder and LabelEncoder are called instead of fit_transform.
  • Make convert_y_to_keras private and let the user decide on a suitable solution.
  • Offer a public and private version of convert_y_to_keras, perhaps with a more user-friendly name and only use encoder.transform in the public version.
  • Live with the risk of users refitting the label/one-hot encoder inadvertently.

[1] https://keras.io/api/layers/core_layers/dense/, see "Output shape" section:

N-D tensor with shape: (batch_size, ..., units). For instance, for a 2D input with shape (batch_size, input_dim), the output would have shape (batch_size, units).

@cedricdonie
Copy link
Author

PS: I initially observed this problem when training InceptionTime https://github.com/cedricdonie/tsc-for-wrist-motion-pd-detection/blob/main/src/models/train_model.py and fixed it for myself via sktime/sktime-dl#132. The dataset is quite large however, so it won't be trivial to reproduce.

@fkiraly
Copy link
Collaborator

fkiraly commented Mar 31, 2024

Welcome back, @cedricdonie!

Sorry for the long silence, we migrated sktime-dl into sktime, after having built a framework which allows to manage dependencies on estimator level.

We've pinged you here
#3351 (comment)
about the fix you made to sktime-dl.

If it is still relevant, it would be greta if you could move it to sktime proper!

InceptionTimeClassifier is now in classification.deep_learning.inceptiontime.

The tests are currently skipped in tests._config, this skip should be removed once the fix is applied.

@cedricdonie
Copy link
Author

Hi @fkiraly, after migration to sktime proper, sktime/sktime-dl#132 is definitely not a valid fix anymore. Thus, I tried to understand whether this issue still remains in sktime, but would need some help by someone who knows the code and API design better (see comment above).

@fkiraly
Copy link
Collaborator

fkiraly commented Mar 31, 2024

Hm, interesting. I think your observation - that the output of predict_proba is not compliant with user expectations, is valid, and in fact a bug, due to interface non-conformance.

I would prefer convert_y_to_keras to be entirely private, that looks like the only way the estimator will be compliant with the general interface, among the four options you listed - is that correct? Because it is the only one which does away with the expectation towards the user to call it.

Besides that, I would combine with your suggestion to not refit the encoder in predict, that avoids issues with unequal sets of classes, and ensure that _predict_proba inverts the transformation correctly.

FYI,

@cedricdonie
Copy link
Author

cedricdonie commented Apr 28, 2024

I would prefer convert_y_to_keras to be entirely private, that looks like the only way the estimator will be compliant with the general interface, among the four options you listed - is that correct? Because it is the only one which does away with the expectation towards the user to call it.

@fkiraly I agree with your proposal. Let's simply make it private to prevent potentially causing bugs for users.

Besides that, I would combine with your suggestion to not refit the encoder in predict, that avoids issues with unequal sets of classes, and ensure that _predict_proba inverts the transformation correctly.

I think that this is already the case, as per my above comment:
"[...] it seems like OneHotEncoder is only used in convert_y_to_keras. In turn, convert_y_to_keras is referenced by all classifiers in the _fit but not in the _predict functions."
But please check again that this is not the case.

In fact, it could be beneficial to implement a test case that ensures that predict (or perhaps _predict) is free of side-effects by predicting random data, predicting other data, and then asserting that predicting the same random data again yield the same results. Might even by suited to something along the lines of hypothesis testing.

@fkiraly
Copy link
Collaborator

fkiraly commented Apr 28, 2024

Excellent! Would you then like to make a PR to sktime?

Recarding your comments:

  • yes, _predict should be fine, as you say
  • there already is a general test case which loops over all estimators in sktime, in TestAllEstimators, that checks that predict calls are side-effect free, this is test_methods_have_no_side_effects
    • if you have reason to believe this is not working, we need to investigate further. Do you have indications that there are side effects, and the test did not catch this? You can run it via check_estimator from sktime.utils.

@cedricdonie cedricdonie linked a pull request May 1, 2024 that will close this issue
6 tasks
@cedricdonie
Copy link
Author

cedricdonie commented May 1, 2024

Excellent! Would you then like to make a PR to sktime?

Yes, I created PR #6373.

Recarding your comments:

  • yes, _predict should be fine, as you say

  • there already is a general test case which loops over all estimators in sktime, in TestAllEstimators, that checks that predict calls are side-effect free, this is test_methods_have_no_side_effects

    • if you have reason to believe this is not working, we need to investigate further. Do you have indications that there are side effects, and the test did not catch this? You can run it via check_estimator from sktime.utils.

Great! I have no reason to believe that this is not working. And the present issue is a bug only if the user calls convert_y_to_keras himself with a y of different classes than the y used for fit.

However, we might have an issue with classifier.fit(X_train, y_train_multilabel); classifier.score(X_test, y_test_multilabel) because I am not sure what classifier.score expects from its call to predict (or _predict). I've put my thoughts about that in the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working module:classification classification module: time series classification
Projects
Bugfixing
Needs triage & validation
2 participants