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

[MNT, ENH, DOC] Rework similarity search #2473

Open
wants to merge 36 commits into
base: main
Choose a base branch
from

Conversation

baraline
Copy link
Member

@baraline baraline commented Dec 26, 2024

Reference Issues/PRs

Fixes #2341, #2236, #2028, #2020, #1806, #2475, #2538

What does this implement/fix? Explain your changes.

The previous structure for similarity search was not in line with the structure we would expect considering other aeon modules, the lack of distinct base classes for some tasks, as well as the initial design choice (due to the lack of practical experience with using and expanding the module) lead to some really complex code when working on #2341 to make everything work together. Further expanding the module would have made thing worse.

To make the module more flexible and comprehensible, the following rework is proposed in this PR (AEP to be updated acordingly):

The module structure is now :

|-similarity_search
|------series
|------------neighbors #NN on subsequences 
|------------motifs #Motif extraction on subsequences methods
|------collection
|------------neighbors # series methods for subsequence NN adapted to collection case or Approximate NN on whole series
|------------motifs #Series methods adapted to collection case

Base classes are BaseSimilaritySearch, BaseSeriesSimilaritySearch, BaseCollectionSimilaritySearch
Implemented estimators are :

  • (series/neighbors) MassSNN : subsequence nearest neighbors and distance profile computation
  • (series/neighbors) DummySNN : brute force subsequence nearest neighbors
  • (series/motifs) StompMotifs : top k motifs extraction (supports motifs pairs, k-motif or r-motifs)
  • (collection/neighbors) RandomProjectionIndexANN: Approximate nearest neighbors on whole series using a random projection LSH method.

The sufix of the estimators (SNN/ANN/Motifs) remains an open discussion, not sure it's the right way to go.

I removed the support for collections for Stomp and Mass for now to focus on the "expected and well known" use cases, I'll make them in another PR.

All similarity search estimators now use fit/predict interface, with predict returning two arrays (NN/Motifs indexes, and NN/Motifs distances).

Does your contribution introduce a new dependency? If yes, which one?

No.

Any other comments?

As this is still a WIP, I would love some inputs on the structure (notably from @patrickzib !) to make the module more future-proof to future additions and easier to use.

TODO list :

  • Finish to include testing suite for base estimators in the testing module for the SubsequenceSearch part and fix them
  • Implement LSH index as a simple first case for BaseCollectionSimilaritySearch
  • Implement tests for base classes and estimators
  • Update API docs / doc pages
  • Update notebooks
  • Check docstrings
  • Cleanup TODOs in the code
  • updated aeon's CODEOWNERS to receive notifications about future changes to these files.

@baraline baraline linked an issue Dec 26, 2024 that may be closed by this pull request
@aeon-actions-bot aeon-actions-bot bot added documentation Improvements or additions to documentation enhancement New feature, improvement request or other non-bug code enhancement maintenance Continuous integration, unit testing & package distribution similarity search Similarity search package labels Dec 26, 2024
@aeon-actions-bot
Copy link
Contributor

aeon-actions-bot bot commented Dec 26, 2024

Thank you for contributing to aeon

I have added the following labels to this PR based on the title: [ $\color{#F3B9F8}{\textsf{documentation}}$, $\color{#FEF1BE}{\textsf{enhancement}}$, $\color{#EC843A}{\textsf{maintenance}}$ ].
I have added the following labels to this PR based on the changes made: [ $\color{#006b75}{\textsf{similarity search}}$ ]. Feel free to change these if they do not properly represent the PR.

The Checks tab will show the status of our automated tests. You can click on individual test runs in the tab or "Details" in the panel below to see more information if there is a failure.

If our pre-commit code quality check fails, any trivial fixes will automatically be pushed to your PR unless it is a draft.

Don't hesitate to ask questions on the aeon Slack channel if you have any.

PR CI actions

These checkboxes will add labels to enable/disable CI functionality for this PR. This may not take effect immediately, and a new commit may be required to run the new configuration.

  • Run pre-commit checks for all files
  • Run mypy typecheck tests
  • Run all pytest tests and configurations
  • Run all notebook example tests
  • Run numba-disabled codecov tests
  • Stop automatic pre-commit fixes (always disabled for drafts)
  • Disable numba cache loading
  • Push an empty commit to re-run CI checks

@patrickzib
Copy link
Contributor

patrickzib commented Jan 2, 2025

Thank you very much for working on this.

Some thoughts:

  • Focus the module on two distinct tasks : find_neighbors and find_motifs for all type for similarity search estimators. Similarly to the fit/ predict interface we already know well, here, we first fit and then either find_motifs or find_neighbors ("predict" keyword don't make much sense here). We give a collection to use as database in fit, and a single series in find_neighbors or find_motifs to use as query for the search.

That is an interesting problem. Here is my view:

For whole series similarity search fit requires a dataset of time series of equal length, and find_neighbors would get one or many query series of this length.

For subsequence similarity search fit requires a single time series, and find_neighbors commonly gets a single query sequence which length is shorter than the single series length. It would be fine however, to extend it to multiple short sequences.

There is only one whole series consensus motif search paper, which would be the use case of whole matching and motif discovery. The input to fit would be the whole dataset, and find_motif has no input series X. not sure, what an input series X should trigger.

Most papers solve the problem of motif discovery in a single long time series, defined as subsequences of the time series. Here, fit gets a single series, and find_motif has no input series X.

  • Distinguish between two kind of similarity search tasks with the two submodules, SeriesSearch and SubsequencesSearch. The SubsequencesSearch focuses on tasks for which the goal is to find motifs or neighbors in subsequences of time series (e.g. Matrix Profiles, Motiflets, etc.). the SeriesSearch focuses on task using whole series (e.g. Indexes such as LSH, iSAX, etc.)
  • Have base classes for families of method to limit code duplication (e.g. BaseMatrixProfile, and STOMP, where most existing code was ported), so the we can focus on implementing the computational logic when adding new estimators.

What is the difference between BaseMatrixProfile and STOMP?

At least for Motiflets, we cannot use STOMP/MP, as it only gives a 1-NN profile, but we need k-NN profiles. Same problem would be the case, if you want to solve k-nearest neighbors similarity search.

Questions to answers for motif search :

  • Do we want to make providing X optional in find_motifs ? Providing X means that we search for subsequences in X that are motifs in the collection given in fit. Not providing X would mean that we search for motifs in the collection given in fit only. I think it would make sense to make it optional, but would love some comment from people actually doing motif discovery.

I think that X is not meaningful for motif discovery.

@baraline
Copy link
Member Author

baraline commented Jan 2, 2025

Thanks for the inputs @patrickzib😄

For whole series similarity search fit requires a dataset of time series of equal length, and find_neighbors would get one or many query series of this length.

Completely in line with this, but what about the case of unequal length series, with, for example, elastic distance measures? Wouldn't that be a plausible use case? (all whole series estimators don't have to support it)

For subsequence similarity search fit requires a single time series, and find_neighbors commonly gets a single query sequence which length is shorter than the single series length. It would be fine however, to extend it to multiple short sequences.

For this case, I'm defining a length parameter during __init__, and accept a 2D subsequence of shape (n_channels, length) for find_neighbors, ensuring that length is not bigger than the length of any series given in fit. However, any reason why you think we should restrain ourselves to a single series for fit ?

There is only one whole series consensus motif search paper, which would be the use case of whole matching and motif discovery. The input to fit would be the whole dataset, and find_motif has no input series X. not sure, what an input series X should trigger.

This is the tricky one for me too. I'm not sure how giving X during find_motifs on whole series would fit any use case.

Most papers solve the problem of motif discovery in a single long time series, defined as subsequences of the time series. Here, fit gets a single series, and find_motif has no input series X.

I've been kinda frustrated by this limitation for practical use cases, wouldn't it be fine to loop on series of a collection with the motif discovery methods and then merge the results ? That's how I implemented STOMP for now for example. For each subsequence in X, it computes the distance profile to all series in a collection, and keep the top k among all of them (also storing the sample ID and timepoint ID).

What is the difference between BaseMatrixProfile and STOMP?
At least for Motiflets, we cannot use STOMP/MP, as it only gives a 1-NN profile, but we need k-NN profiles. Same problem would be the case, if you want to solve k-nearest neighbors similarity search.

BaseMatrixProfile (which inherit BaseSubsequenceSearch) is simply a base class that defines abstract compute_matrix_profile and compute_distance_profile methods to be implemented by child classes such as STOMP and the likes (STUMP, etc...). The logic for finding neighbors / motifs is then handled in the BaseMatrixProfile. I wanted to leave the door open to alternative methods and not just focus on matrix profiles, hence the split.

As stated above, I already extended STOMP to support k-NN profiles for collections (multivariate and unequal length compatible).

I suppose that in this context, motiflets would either inherit from BaseMatrixProfile if you need to implement methods like compute_matrix_profile and compute_distance_profile. Otherwise, It would inherit from BaseSubsequenceSearch and make its own methods to answer the find_neighbors/find_motifs tasks. (I would need to read the paper again!)

Note that it's possible to simply raise a "NotImplementedError" or something similar if an estimator would only support neighbors or motifs search.

My goal here is to find a base class structure that enables us to move most common code to there and focus on the computational optimisations of each method in the child classes.

I think that X is not meaningful for motif discovery.

In the context of motif search in a single series I agree, but wouldn't there be some interest when dealing with a collection ? For example find motifs in the collection at the condition that they are similar to a subsequence in X ? (This is pure speculation)

@patrickzib
Copy link
Contributor

Completely in line with this, but what about the case of unequal length series, with, for example, elastic distance measures? Wouldn't that be a plausible use case? (all whole series estimators don't have to support it)

Sure. I did not think of this.

For subsequence similarity search fit requires a single time series, and find_neighbors commonly gets a single query sequence which length is shorter than the single series length. It would be fine however, to extend it to multiple short sequences.

For this case, I'm defining a length parameter during __init__, and accept a 2D subsequence of shape (n_channels, length) for find_neighbors, ensuring that length is not bigger than the length of any series given in fit. However, any reason why you think we should restrain ourselves to a single series for fit ?

Simplicity :) But I agree that you could have multiple series in fit, too - this would mimic the Shapelet use case, I suppose?

Most papers solve the problem of motif discovery in a single long time series, defined as subsequences of the time series. Here, fit gets a single series, and find_motif has no input series X.

I've been kinda frustrated by this limitation for practical use cases, wouldn't it be fine to loop on series of a collection with the motif discovery methods and then merge the results ? That's how I implemented STOMP for now for example. For each subsequence in X, it computes the distance profile to all series in a collection, and keep the top k among all of them (also storing the sample ID and timepoint ID).

Sorry, yes, that is what the authors refer to as consensus motif:
https://www.cs.ucr.edu/~eamonn/consensus_Motif_ICDM_Long_version.pdf

BaseMatrixProfile (which inherit BaseSubsequenceSearch) is simply a base class that defines abstract compute_matrix_profile and compute_distance_profile methods to be implemented by child classes such as STOMP and the likes (STUMP, etc...).

I see. I personally do not like to use the terms matrix-profile for simple k-NN distances or k-NN indices though. It was a brilliant re-framing of EK, such that all 1-NN algorithms are now suddenly an instance of matrix profile. Yet, the concept is much older.

As stated above, I already extended STOMP to support k-NN profiles for collections (multivariate and unequal length compatible).

Great.

I think that X is not meaningful for motif discovery.

In the context of motif search in a single series I agree, but wouldn't there be some interest when dealing with a collection ? For example find motifs in the collection at the condition that they are similar to a subsequence in X ? (This is pure speculation)

I would not say that this is impossible, but I have not seen it. :)

@baraline
Copy link
Member Author

baraline commented Jan 2, 2025

Simplicity :) But I agree that you could have multiple series in fit, too - this would mimic the Shapelet use case, I suppose?

I'm not 100% sure what you mean, but in a sense yes ? For example with a brute force neighbour search, just compute the distance of the subsequence given in find_neighbors to all candidates subsequences in all series of the collection given in fit, and take the k best overall, (considering neighbouring matches/self matches if specified by parameters).

I see. I personally do not like to use the terms matrix-profile for simple k-NN distances or k-NN indices though. It was a brilliant re-framing of EK, such that all 1-NN algorithms are now suddenly an instance of matrix profile. Yet, the concept is much older.

I'm not against the idea of a different naming, especially if methods labelled differently from MPs would fit in the base class without much change of parameter/interface. Would you have any proposal? Something like BaseNeighborhoodSearch ?

@patrickzib
Copy link
Contributor

I'm not against the idea of a different naming, especially if methods labelled differently from MPs would fit in the base class without much change of parameter/interface. Would you have any proposal? Something like BaseNeighborhoodSearch ?

In sklearn it is simply NearestNeighbors ? :) And it returns indices and distances.

https://scikit-learn.org/1.5/modules/neighbors.html

@baraline
Copy link
Member Author

baraline commented Feb 1, 2025

I'll leave the implementation of k-motiflets for later as a separate estimator so I can focus on getting the testing, tags and docs right for this PR to have the structure for the module set.

@baraline baraline marked this pull request as ready for review February 2, 2025 15:08
Copy link
Contributor

@patrickzib patrickzib left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, only a very brief review. I would have to test the code to give a better review, but I will be leaving for vacation...

from aeon.similarity_search._base import BaseSimilaritySearch


class BaseCollectionSimilaritySearch(BaseCollectionEstimator, BaseSimilaritySearch):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if I get the difference between this base class and the other base class. What is the purpose of it?

Copy link
Member Author

@baraline baraline Mar 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The BaseCollectionSimilaritySearch is used for estimators taking collections of time series during fit/predict, while the BaseSeriesSimilaritySearch is for single series estimators (similar to the transformer module)

Parameters
----------
X : np.ndarray, shape = (n_cases, n_channels, n_tiempoints)
Collections of series to predict on.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling error: n_tiempoints

And why is the input different from fit?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean different from BaseSimilaritySearch fit ?

I convert single series to 3D (1, n_channels, n_timepoints), but I didn't yet add support for unequal length as there was no estimator using it.

@@ -0,0 +1 @@
"""Motif search for time series collection."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Motif discovery not search :)

What do you mean by "for time series collection"? Is this the same as a Consensus Motifs? Then better call it that way?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was aiming for something more generic than consensus motifs, simply motif discovery on a collection of time series instead on a single series. I didn't want to bind the module to one terminology. But consensus motif would be the first thing to implement in there yes !

@MatthewMiddlehurst MatthewMiddlehurst added the codecov actions Run the codecov action on a PR label Mar 17, 2025
@aeon-actions-bot aeon-actions-bot bot added the full pytest actions Run the full pytest suite on a PR label Mar 17, 2025
Copy link
Member

@MatthewMiddlehurst MatthewMiddlehurst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments for the bits outside the actual module itself. I will have a look at the rest as well, but generally trust you and Patrick agree on something sensible together. Let me know if any of this contradicts a previous review.

Comment on lines +24 to +26
- [**Similarity search**](api_reference/similarity_search), where the goal is to find
time series motifs or nearest neighbors in an efficient way for either single series
or collections.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an example on this page which is now outdated.

@@ -155,7 +155,7 @@ class : identifier for the base class of objects this tag applies to
"values?",
},
"input_data_type": {
"class": "transformer",
"class": ["transformer", "similarity-search"],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would update the description this is needed for more than transformers now

@@ -451,15 +454,56 @@ def get_subsequence_with_mean_std(
return values, means, stds


@njit(cache=True, fastmath=True, parallel=True)
def compute_mean_stds_collection_parallel(X):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for updating the list at the top, I think this one is still missing though? Alternatively should be protected

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not required here but helpful: updating the utils API page where relevant

@@ -92,6 +92,7 @@ def all_estimators(
# ignore test modules and base classes
"base",
"tests",
"similarity_search"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't think we want to ignore these. All of these are essentially empty of estimators or for testing only.

@@ -87,7 +87,6 @@ Mock Estimators
MockUnivariateSeriesTransformer
MockMultivariateSeriesTransformer
MockSeriesTransformerNoFit
MockSimilaritySearch
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing the two new ones

Comment on lines -32 to -33
# similarity search
"MockSimilaritySearch",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing the two new ones

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
codecov actions Run the codecov action on a PR documentation Improvements or additions to documentation enhancement New feature, improvement request or other non-bug code enhancement full pytest actions Run the full pytest suite on a PR maintenance Continuous integration, unit testing & package distribution similarity search Similarity search package
Projects
None yet
3 participants