├── detach_rocket
├── __init__.py
├── __pycache__
│ ├── utils.cpython-38.pyc
│ ├── __init__.cpython-38.pyc
│ ├── detach_classes.cpython-38.pyc
│ └── utils_datasets.cpython-38.pyc
├── utils.py
├── utils_datasets.py
└── detach_classes.py
├── logo
├── logo.png
└── detach_logo.png
├── setup.py
├── README.md
└── examples
├── Detach_Ensemble_example_UEA.ipynb
├── SFD_example_tsfresh.ipynb
└── Detach_ROCKET_example_UCR.ipynb
/detach_rocket/__init__.py:
--------------------------------------------------------------------------------
1 | """detach_rocket."""
2 |
3 | __version__ = "0.0.1"
4 |
--------------------------------------------------------------------------------
/logo/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gon-uri/detach_rocket/HEAD/logo/logo.png
--------------------------------------------------------------------------------
/logo/detach_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gon-uri/detach_rocket/HEAD/logo/detach_logo.png
--------------------------------------------------------------------------------
/detach_rocket/__pycache__/utils.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gon-uri/detach_rocket/HEAD/detach_rocket/__pycache__/utils.cpython-38.pyc
--------------------------------------------------------------------------------
/detach_rocket/__pycache__/__init__.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gon-uri/detach_rocket/HEAD/detach_rocket/__pycache__/__init__.cpython-38.pyc
--------------------------------------------------------------------------------
/detach_rocket/__pycache__/detach_classes.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gon-uri/detach_rocket/HEAD/detach_rocket/__pycache__/detach_classes.cpython-38.pyc
--------------------------------------------------------------------------------
/detach_rocket/__pycache__/utils_datasets.cpython-38.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gon-uri/detach_rocket/HEAD/detach_rocket/__pycache__/utils_datasets.cpython-38.pyc
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="detach_rocket",
8 | version="0.0.1",
9 | author="Gonzalo Uribarri & Federico Barone",
10 | description="Sequential Feature Detachment for Random Convolutional Kernel models.",
11 | long_description_content_type="text/markdown",
12 | url="https://github.com/gon-uri/detach_rocket",
13 | packages=setuptools.find_packages(),
14 | classifiers=[
15 | "Development Status :: 3 - Alpha",
16 | "Intended Audience :: Science/Research",
17 | "License :: OSI Approved :: BSD 3-Clause License",
18 | "Operating System :: OS Independent",
19 | "Programming Language :: Python :: 3.7",
20 | "Programming Language :: Python :: 3.8",
21 | "Programming Language :: Python :: 3.9",
22 | ],
23 | python_requires='>=3.7',
24 | )
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Detach-ROCKET
6 |
7 |
8 |
9 |
10 | Official repository for [Detach-ROCKET: Sequential feature selection for time series classification with random convolutional kernels](https://link.springer.com/article/10.1007/s10618-024-01062-7) and [Classification of raw MEG/EEG data with detach-rocket ensemble: an improved rocket algorithm for multivariate time series analysis](https://www.arxiv.org/abs/2408.02760).
11 |
12 | ## Overview
13 |
14 | This repository contains Python implementations of Sequential Feature Detachment (SFD) for feature selection and Detach-ROCKET for time-series classification. Developed entirely in Python using primarly NumPy, PyTorch, Scikit-Learn and Sktime libraries, the core functionalities are encapsulated within the following classes:
15 |
16 | - `DetachRocket`: Detach-ROCKET model class. It is constructed by pruning an initial ROCKET, MiniRocket or MultiROCKET model using SFD and selecting the optimal size.
17 |
18 | - `DetachMatrix`: Class for applying Sequential Feature Detachment to any dataset matrix structured as (n_instances, n_features).
19 |
20 | - `DetachEnsemble`: Detach-ROCKET Ensemble model class. It creates an ensemble of Detach models. We recommend using this class for multivariate time series, especially if they are high-dimensional. After training, these models are also able to provide channel relevance estimation and label probability.
21 |
22 | For a detailed explanation of the models and methods please refer to the [Detach-ROCKET article](https://link.springer.com/article/10.1007/s10618-024-01062-7) and the [Detach-ROCKET Ensemble article](https://www.arxiv.org/abs/2408.02760).
23 |
24 | ## Installation
25 |
26 | To install the required dependencies, execute:
27 |
28 | ```bash
29 | pip install numpy scikit-learn pyts torch matplotlib sktime==0.30.0
30 | pip install git+https://github.com/gon-uri/detach_rocket --quiet
31 | ```
32 |
33 | ## Usage - DetachRocket
34 | The model usage is the same as in the scikit-learn library.
35 |
36 | ```python
37 | # Import Model
38 | from detach_rocket.detach_classes import DetachRocket
39 |
40 | # Instantiate Model
41 | DetachRocketModel = DetachRocket('rocket', num_kernels=10000)
42 |
43 | # Trian Model
44 | DetachRocketModel.fit(X_train,y_train)
45 |
46 | # Predict Test Set
47 | y_pred = DetachRocketModel.predict(X_test)
48 | ```
49 |
50 | For univariate time series, the shape of `X_train` should be (n_instances, n_timepoints).
51 |
52 | For multivariate time series, the shape of `X_train` should be (n_instances, n_variables, n_timepoints).
53 |
54 | ## Usage - DetachRocket Ensemble
55 | This model is more suitable for Multivariate Time Series with a large number of channels/dimensions.
56 |
57 | ```python
58 | # Import Model
59 | from detach_rocket.detach_classes import DetachEnsemble
60 |
61 | # Instantiate Model
62 | DetachRocketEnsemble = DetachEnsemble('pytorch_minirocket', num_kernels=10000)
63 |
64 | # Trian Model
65 | DetachRocketEnsemble.fit(X_train,y_train)
66 |
67 | # Predict Test Set
68 | y_pred = DetachRocketEnsemble.predict(X_test)
69 | ```
70 |
71 | ## Notebook Examples
72 |
73 | Detailed usage examples can be found in the included Jupyter notebooks in the [examples folder](/examples).
74 |
75 | ## Upcoming Features
76 |
77 | - [x] Built-in support for multilabel classification. (DONE!)
78 | - [x] Pytorch implementation of Detach-MiniRocket. (DONE!)
79 | - [x] Add channel releavance for Detach-MiniRocket. (DONE!)
80 | - [x] Implementation of Detach-ROCKET Ensemble. (DONE!)
81 | - [x] Add channel releavance and label probability for Detach-ROCKET Ensemble. (DONE!)
82 | - [ ] Pytorch implementations of Detach-MultiRocket. (Coming soon...)
83 | - [ ] Fully pytorch implementation of ROCKET with on-the-fly convolutions during training.
84 | - [ ] Pytorch implementation of SFD for Multilayer Perceptrons.
85 |
86 | ## License
87 |
88 | This project is licensed under the BSD-3-Clause License.
89 |
90 | ## Citation
91 |
92 | If you find these methods useful in your research, please cite the following articles:
93 |
94 | *APA*
95 | ```
96 | Uribarri, G., Barone, F., Ansuini, A., & Fransén, E. (2024). Detach-ROCKET: Sequential feature selection for time series classification with random convolutional kernels. Data Mining and Knowledge Discovery, 1-26.
97 |
98 | Solana, A., Fransén, E., & Uribarri, G. (2024). Classification of raw MEG/EEG data with detach-rocket ensemble: an improved rocket algorithm for multivariate time series analysis. arXiv preprint arXiv:2408.02760.
99 | ```
100 |
101 | *BIBTEX*
102 | ```
103 | @article{uribarri2024detach,
104 | title={Detach-ROCKET: Sequential feature selection for time series classification with random convolutional kernels},
105 | author={Uribarri, Gonzalo and Barone, Federico and Ansuini, Alessio and Frans{\'e}n, Erik},
106 | journal={Data Mining and Knowledge Discovery},
107 | pages={1--26},
108 | year={2024},
109 | publisher={Springer}
110 | }
111 |
112 | @article{solana2024classification,
113 | title={Classification of raw MEG/EEG data with detach-rocket ensemble: an improved rocket algorithm for multivariate time series analysis},
114 | author={Solana, Adri{\`a} and Frans{\'e}n, Erik and Uribarri, Gonzalo},
115 | journal={arXiv preprint arXiv:2408.02760},
116 | year={2024}
117 | }
118 | ```
119 |
120 |
122 |
--------------------------------------------------------------------------------
/detach_rocket/utils.py:
--------------------------------------------------------------------------------
1 | """
2 | Utility functions for SFD feature detachment and Detach-ROCKET model.
3 | """
4 |
5 | from sklearn.linear_model import (RidgeClassifierCV,RidgeClassifier)
6 | import numpy as np
7 | import matplotlib.pyplot as plt
8 |
9 |
10 | def feature_detachment(classifier,
11 | X_train: np.ndarray,
12 | X_test: np.ndarray,
13 | y_train: np.ndarray,
14 | y_test: np.ndarray,
15 | drop_percentage: float = 0.05,
16 | total_number_steps: int = 150,
17 | multilabel_type: str = "norm",
18 | verbose = True):
19 | """
20 | Applies Sequential Feature Detachment (SFD) to a feature matrix.
21 |
22 | Parameters
23 | ----------
24 | classifier: sklearn model
25 | Ridge linear classifier trained on the full training dataset.
26 | X_train: numpy array
27 | Training features matrix, a 2d array of shape (# instances, # features).
28 | X_test: numpy array
29 | Test features matrix, a 2d array of shape (# instances, # features).
30 | y_train: numpy array
31 | Training labels
32 | y_test: nparray
33 | Test labels
34 | drop_percentage: float
35 | Proportion of features drop at each step of the detachment process
36 | total_number_steps: int
37 | Total number of detachment steps performed during SFD
38 | verbose: bool
39 | If true, prints a message at the end of each step
40 |
41 | Returns
42 | -------
43 | percentage_vector: numpy array
44 | Array with the the model size at each detachment step (proportion of the original full model)
45 | score_list_train: numpy array
46 | Balanced accuraccy on the training set at each detachment step
47 | score_list_test: numpy array
48 | Balanced accuraccy on the test set at each detachment step
49 | feature_importance_matrix: numpy array
50 | A 2d array of shape (# steps, # features) with the importance of the features at each detachment step
51 | """
52 |
53 | # Check if training set is normalized
54 | mean_vector = X_train.mean(axis=0)
55 | zeros_vector = np.zeros_like(mean_vector)
56 | nomalized_condition = np.isclose(mean_vector, zeros_vector,atol=1e-02)
57 | # assert all(nomalized_condition), "The feature matrix should be normalized before training classifier."
58 | total_feats = X_train.shape[1]
59 |
60 | # Feature importance from full model
61 |
62 | # Check if problem is multilabel
63 | if len(np.shape(classifier.coef_))>1: # OLD WAY (sklearn 1.3.2): if np.shape(classifier.coef_)[0]>1:
64 | # The shape for univariate in 1.6.1 is (num_features,) and for multivariate (num_classes,num_features)
65 | # The shape for univariate in 1.3.2 is (1,num_features) and for multivariate (num_classes,num_features)
66 | if multilabel_type == "norm":
67 | feature_importance_full = np.linalg.norm(classifier.coef_[:,:],axis=0,ord=2)
68 | elif multilabel_type == "max":
69 | feature_importance_full = np.linalg.norm(classifier.coef_[:,:],axis=0,ord=np.inf)
70 | elif multilabel_type == "avg":
71 | feature_importance_full = np.linalg.norm(classifier.coef_[:,:],axis=0,ord=1)
72 | else:
73 | raise ValueError('Invalid multilabel_type argument. Choose from: "norm", "max", or "avg".')
74 | else:
75 | feature_importance_full = np.abs(classifier.coef_)[:] # OLD WAY (sklearn 1.3.2): np.abs(classifier.coef_)[0,:]
76 |
77 | # Define percentage vector
78 | keep_percentage = 1-drop_percentage
79 | powers_vector = np.arange(total_number_steps)
80 | percentage_vector_unif = np.power(keep_percentage, powers_vector)
81 |
82 | num_feat_per_step = np.unique((percentage_vector_unif*total_feats).astype(int))
83 | num_feat_per_step = num_feat_per_step[::-1]
84 | num_feat_per_step = num_feat_per_step[num_feat_per_step>0]
85 | percentage_vector = num_feat_per_step/total_feats
86 |
87 | # Define lists and matrices
88 | score_list_train = []
89 | score_list_test = []
90 | feature_importance = np.copy(feature_importance_full)
91 | feature_importance_matrix = np.zeros((len(percentage_vector),len(feature_importance_full)))
92 | # feature_selection_matrix = np.full((len(percentage_vector),len(feature_importance_full)), False)
93 |
94 | # Begin iterative feature selection
95 | for count, feat_num in enumerate(num_feat_per_step):
96 |
97 | per = percentage_vector[count]
98 |
99 | # Cumpute mask for selected features
100 | drop_features = total_feats - feat_num
101 |
102 | selected_idxs = np.argsort(feature_importance)[drop_features:]
103 | selection_mask = np.full(total_feats, False)
104 | selection_mask[selected_idxs] = True
105 |
106 | # Apply mask
107 | X_train_subsampled = X_train[:,selection_mask]
108 | X_test_subsampled = X_test[:,selection_mask]
109 |
110 | # Train model for selected features
111 | classifier.fit(X_train_subsampled, y_train)
112 |
113 | # Compute scores for train and test sets
114 | avg_score_train = classifier.score(X_train_subsampled, y_train)
115 | avg_score_test = classifier.score(X_test_subsampled, y_test)
116 | score_list_train.append(avg_score_train)
117 | score_list_test.append(avg_score_test)
118 |
119 | # Save current feature importance and selected features
120 | feature_importance_matrix[count,:] = feature_importance
121 | # feature_selection_matrix[count,:] = selection_mask
122 |
123 | # Kill masked features
124 | feature_importance[~selection_mask] = 0
125 |
126 | # Compute feature importance taking into account multilabel type
127 | if len(np.shape(classifier.coef_))>1: # OLD WAY (sklearn 1.3.2): if np.shape(classifier.coef_)[0]>1:
128 | # The shape for univariate in 1.6.1 is (num_features,) and for multivariate (num_classes,num_features)
129 | # The shape for univariate in 1.3.2 is (1,num_features) and for multivariate (num_classes,num_features)
130 | if multilabel_type == "norm":
131 | feature_importance[selection_mask] = np.linalg.norm(classifier.coef_[:,:],axis=0,ord=2)
132 | elif multilabel_type == "max":
133 | feature_importance[selection_mask] = np.linalg.norm(classifier.coef_[:,:],axis=0,ord=np.inf)
134 | elif multilabel_type == "avg":
135 | feature_importance[selection_mask] = np.linalg.norm(classifier.coef_[:,:],axis=0,ord=1)
136 | else:
137 | raise ValueError('Invalid multilabel_type argument. Choose from: "norm", "max", or "avg".')
138 | else:
139 | feature_importance[selection_mask] = np.abs(classifier.coef_)[:] # OLD WAY (sklearn 1.3.2): np.abs(classifier.coef_)[0,:]
140 |
141 | if verbose==True:
142 | print("Step {} out of {}".format(count+1, total_number_steps))
143 | print('{:.3f}% of features used'.format(100*per))
144 |
145 | return percentage_vector, np.asarray(score_list_train), np.asarray(score_list_test), feature_importance_matrix #,feature_selection_matrix
146 |
147 |
148 | def select_optimal_model(percentage_vector,
149 | acc_test,
150 | full_model_score_test,
151 | acc_size_tradeoff_coef: float=0.1, # 0 means only weighting accuracy, +inf only weighting model size
152 | smoothing_points: int = 3,
153 | graphics = True):
154 |
155 | """
156 | Function that selects the optimal model size after SFD procces.
157 |
158 | Parameters
159 | ----------
160 | percentage_vector: numpy array
161 | Array with the the model size at each detachment step (proportion of the initial full model)
162 | acc_test: numpy array
163 | Balanced accuracy on the test set at each detachment step
164 | full_model_score_test: float
165 | Balanced accuracy of the full initial full classifier on the test set
166 | acc_size_tradeoff_coef: float
167 | Parameter that governs the tradeoff between size and accuracy. 0 means only weighting accuracy, +inf only weighting model size
168 | smoothing_points: int
169 | Level of smoothing applied to the acc_test
170 | graphics: bool
171 | If true, prints a matplotlib figure with the desition criteria
172 |
173 | Returns
174 | -------
175 | max_index: int
176 | Index of the optimal model (optimal number of SFD steps)
177 | max_percentage: float
178 | Size of the selected optimal model (proportion of the initial full model)
179 | """
180 |
181 | # Create model percentage vector
182 | x_vec = (1-percentage_vector)
183 |
184 | # Create smoothed relative test acc vector
185 | y_vec = (acc_test/full_model_score_test)
186 | box = np.ones(smoothing_points)/smoothing_points
187 | y_vec_smooth = np.convolve(y_vec, box, mode='same')
188 |
189 | # Define the functio to optimize
190 | optimality_curve = acc_size_tradeoff_coef*x_vec+y_vec_smooth
191 |
192 | # Compute max of the function
193 | max_index = np.argmax(optimality_curve)
194 | max_x_vec = x_vec[max_index]
195 | max_percentage = percentage_vector[max_index]
196 |
197 | # Plot results
198 | if graphics == True:
199 | margin = int((smoothing_points)/2)
200 | plt.plot(x_vec,y_vec, label='Relative test accuracy')
201 | plt.plot(x_vec[margin:-margin],optimality_curve[margin:-margin], label='Function to optimize')
202 | plt.scatter(max_x_vec,optimality_curve[max_index],c='C2', label='Maximum')
203 | plt.scatter(max_x_vec,y_vec[max_index],c='C3', label='Selected value')
204 | plt.legend()
205 | plt.ylabel('Relative Classification Accuracy')
206 | plt.xlabel('% of features Dropped')
207 | plt.show()
208 |
209 | return max_index, max_percentage
210 |
211 |
212 | def retrain_optimal_model(feature_mask,
213 | X_train_scaled_transform,
214 | y_train,
215 | max_index,
216 | model_alpha = None,
217 | verbose = True):
218 |
219 | """
220 | Function that retrains a Ridge classifier with the optimal subset of selected features.
221 |
222 | Parameters
223 | ----------
224 | feature_importance_matrix: numpy array
225 | A 2d array of shape (# steps, # features) with the importance of the features at each detachment step
226 | X_train_scaled_transform: numpy array
227 | Training features matrix, a 2d array of shape (# instances, # dimensions)
228 | X_test_scaled_transform: numpy array
229 | Test features matrix, a 2d array of shape (# instances, # dimensions)
230 | max_index: int
231 | Index of the optimal model (optimal number of SFD steps)
232 | model_alpha: float
233 | Alpha regularization parameter to be used with the Ridge classifier.
234 | If None recompute alpha using CrossValidation on the optimal features.
235 | verbose: bool
236 | If true, prints the results
237 |
238 | Returns
239 | -------
240 | optimal_classifier: sklearn model
241 | Ridge classifier trained on selected features, alpha is recomputed from the training set
242 | optimal_acc_train: float
243 | Balanced accuracy on the training set with optimal_classifier
244 | optimal_acc_train: float
245 | Balanced accuracy on the test set with optimal_classifier
246 | """
247 |
248 | masked_X_train = X_train_scaled_transform[:,feature_mask]
249 |
250 | if model_alpha==None:
251 | # CV to find best alpha
252 | cv_classifier = RidgeClassifierCV(alphas=np.logspace(-10, 10, 20))
253 | cv_classifier.fit(masked_X_train, y_train)
254 | model_alpha = cv_classifier.alpha_
255 |
256 | # Refit with all training set
257 | optimal_classifier = RidgeClassifier(alpha=model_alpha)
258 | optimal_classifier.fit(masked_X_train, y_train)
259 | optimal_acc_train = optimal_classifier.score(masked_X_train, y_train)
260 |
261 | print('TRAINING RESULTS Detach Model:')
262 | print('Optimal Alpha Detach Model: {:.2f}'.format(model_alpha))
263 | print('Train Accuraccy Detach Model: {:.2f}%'.format(100*optimal_acc_train))
264 | print('-------------------------')
265 |
266 | return optimal_classifier, optimal_acc_train
267 |
--------------------------------------------------------------------------------
/detach_rocket/utils_datasets.py:
--------------------------------------------------------------------------------
1 | """
2 | Utility functions for the UCR time series classification archive.
3 | """
4 | # Code copied (and modified) from pyts library code:
5 | # Author: Johann Faouzi
6 | # License: BSD-3-Clause
7 |
8 | import numpy as np
9 | import os
10 | import pickle
11 | from scipy.io.arff import loadarff
12 | from sklearn.utils import Bunch
13 | from urllib.request import urlretrieve
14 | from pyts.datasets import ucr_dataset_list, ucr_dataset_info, uea_dataset_list
15 | import zipfile
16 |
17 |
18 | def _correct_ucr_name_download(dataset):
19 | if dataset == 'CinCECGtorso':
20 | return 'CinCECGTorso'
21 | elif dataset == 'MixedShapes':
22 | return 'MixedShapesRegularTrain'
23 | elif dataset == 'NonInvasiveFetalECGThorax1':
24 | return 'NonInvasiveFatalECGThorax1'
25 | elif dataset == 'NonInvasiveFetalECGThorax2':
26 | return 'NonInvasiveFatalECGThorax2'
27 | elif dataset == 'StarlightCurves':
28 | return 'StarLightCurves'
29 | else:
30 | return dataset
31 |
32 |
33 | def _correct_ucr_name_description(dataset):
34 | if dataset == 'CinCECGTorso':
35 | return 'CinCECGtorso'
36 | elif dataset == 'MixedShapesRegularTrain':
37 | return 'MixedShapes'
38 | elif dataset == 'NonInvasiveFatalECGThorax1':
39 | return 'NonInvasiveFetalECGThorax1'
40 | elif dataset == 'NonInvasiveFatalECGThorax2':
41 | return 'NonInvasiveFetalECGThorax2'
42 | elif dataset == 'StarLightCurves':
43 | return 'StarlightCurves'
44 | else:
45 | return dataset
46 |
47 |
48 | # def ucr_dataset_list():
49 | # """List of available UCR datasets.
50 |
51 | # Returns
52 | # -------
53 | # datasets : list
54 | # List of available datasets from the UCR Time Series
55 | # Classification Archive.
56 |
57 | # References
58 | # ----------
59 | # .. [1] `List of datasets on the UEA & UCR archive
60 | # `_
61 |
62 | # Examples
63 | # --------
64 | # >>> from pyts.datasets import ucr_dataset_list
65 | # >>> ucr_dataset_list()[:3]
66 | # ['ACSF1', 'Adiac', 'AllGestureWiimoteX']
67 |
68 | # """
69 | # module_path = os.path.dirname(__file__)
70 | # finfo = os.path.join(module_path, 'info', 'ucr.pickle')
71 | # dictionary = pickle.load(open(finfo, 'rb'))
72 | # datasets = sorted(dictionary.keys())
73 | # return datasets
74 |
75 |
76 | #def ucr_dataset_info(dataset=None):
77 | # """Information about the UCR datasets.
78 | #
79 | # Parameters
80 | # ----------
81 | # dataset : str, list of str or None (default = None)
82 | # The data sets for which the information will be returned.
83 | # If None, the information for all the datasets is returned.
84 |
85 | # Returns
86 | # -------
87 | # dictionary : dict
88 | # Dictionary with the information for each dataset.
89 |
90 | # References
91 | # ----------
92 | # .. [1] `List of datasets on the UEA & UCR archive
93 | # `_
94 |
95 | # Examples
96 | # --------
97 | # >>> from pyts.datasets import ucr_dataset_info
98 | # >>> ucr_dataset_info('Adiac')['n_classes']
99 | # 37
100 |
101 | # """
102 | # module_path = os.path.dirname(__file__)
103 | # finfo = os.path.join(module_path, 'info', 'ucr.pickle')
104 | # dictionary = pickle.load(open(finfo, 'rb'))
105 | # datasets = list(dictionary.keys())
106 |
107 | # if dataset is None:
108 | # return dictionary
109 | # elif isinstance(dataset, str):
110 | # if dataset not in datasets:
111 | # raise ValueError(
112 | # "{0} is not a valid name. The list of available names "
113 | # "can be obtained by calling the "
114 | # "'pyts.datasets.ucr_dataset_list' function."
115 | # .format(dataset)
116 | # )
117 | # else:
118 | # return dictionary[dataset]
119 | # elif isinstance(dataset, (list, tuple, np.ndarray)):
120 | # dataset = np.asarray(dataset)
121 | # invalid_datasets = np.setdiff1d(dataset, datasets)
122 | # if invalid_datasets.size > 0:
123 | # raise ValueError(
124 | # "The following names are not valid: {0}. The list of "
125 | # "available names can be obtained by calling the "
126 | # "'pyts.datasets.ucr_dataset_list' function."
127 | # .format(invalid_datasets)
128 | # )
129 | # else:
130 | # info = {}
131 | # for data in dataset:
132 | # info[data] = dictionary[data]
133 | # return info
134 |
135 |
136 | def fetch_ucr_dataset(dataset, use_cache=True, data_home=None,
137 | return_X_y=False):
138 | r"""Fetch dataset from UCR TSC Archive by name.
139 |
140 | Fetched data sets are automatically saved in the
141 | ``pyts/datasets/_cached_datasets`` folder. To avoid
142 | downloading the same data set several times, it is
143 | highly recommended not to change the default values
144 | of ``use_cache`` and ``path``.
145 |
146 | Parameters
147 | ----------
148 | dataset : str
149 | Name of the dataset.
150 |
151 | use_cache : bool (default = True)
152 | If True, look if the data set has already been fetched
153 | and load the fetched version if it is the case. If False,
154 | download the data set from the UCR Time Series Classification
155 | Archive.
156 |
157 | data_home : None or str (default = None)
158 | The path of the folder containing the cached data set.
159 | If None, the ``pyts.datasets.cached_datasets/UCR/`` folder is
160 | used. If the data set is not found, it is downloaded and cached
161 | in this path.
162 |
163 | return_X_y : bool (default = False)
164 | If True, returns ``(data_train, data_test, target_train, target_test)``
165 | instead of a Bunch object. See below for more information about the
166 | `data` and `target` object.
167 |
168 | Returns
169 | -------
170 | data : Bunch
171 | Dictionary-like object, with attributes:
172 |
173 | data_train : array of floats
174 | The time series in the training set.
175 | data_test : array of floats
176 | The time series in the test set.
177 | target_train : array of integers
178 | The classification labels in the training set.
179 | target_test : array of integers
180 | The classification labels in the test set.
181 | DESCR : str
182 | The full description of the dataset.
183 | url : str
184 | The url of the dataset.
185 |
186 | (data_train, data_test, target_train, target_test) : tuple if ``return_X_y`` is True
187 |
188 | Notes
189 | -----
190 | Missing values are represented as NaN's.
191 |
192 | References
193 | ----------
194 | .. [1] H. A. Dau et al, "The UCR Time Series Archive".
195 | arXiv:1810.07758 [cs, stat], 2018.
196 |
197 | .. [2] A. Bagnall et al, "The UEA & UCR Time Series Classification
198 | Repository", www.timeseriesclassification.com.
199 |
200 | """ # noqa: E501
201 | if dataset not in ucr_dataset_list():
202 | raise ValueError(
203 | "{0} is not a valid name. The list of available names "
204 | "can be obtained with ``pyts.datasets.ucr_dataset_list()``"
205 | .format(dataset)
206 | )
207 | if data_home is None:
208 | import pyts
209 | home = '/'.join(pyts.__file__.split('/')[:-2]) + '/'
210 | relative_path = 'pyts/datasets/cached_datasets/UCR/'
211 | path = home + relative_path
212 | else:
213 | path = data_home
214 | if not os.path.exists(path):
215 | os.makedirs(path)
216 |
217 | correct_dataset = _correct_ucr_name_download(dataset)
218 | if use_cache and os.path.exists(path + correct_dataset):
219 | bunch = _load_ucr_dataset(correct_dataset, path=path)
220 | else:
221 | # CHANGED LINE --------
222 | # url = ("http://www.timeseriesclassification.com/Downloads/{0}.zip"
223 | # url = ("http://www.timeseriesclassification.com/ClassificationDownloads/{0}.zip".format(correct_dataset))
224 | url = ("https://www.timeseriesclassification.com/aeon-toolkit/{0}.zip".format(correct_dataset))
225 | #print(url)
226 | # ---------------------
227 | filename = 'temp_{}'.format(correct_dataset)
228 | _ = urlretrieve(url, path + filename)
229 | zipfile.ZipFile(path + filename).extractall(path + correct_dataset)
230 | os.remove(path + filename)
231 | bunch = _load_ucr_dataset(correct_dataset, path)
232 |
233 | if return_X_y:
234 | return (bunch.data_train, bunch.data_test,
235 | bunch.target_train, bunch.target_test)
236 | return bunch
237 |
238 |
239 | def _load_ucr_dataset(dataset, path):
240 | """Load a UCR data set from a local folder.
241 |
242 | Parameters
243 | ----------
244 | dataset : str
245 | Name of the dataset.
246 |
247 | path : str
248 | The path of the folder containing the cached data set.
249 |
250 | Returns
251 | -------
252 | data : Bunch
253 | Dictionary-like object, with attributes:
254 |
255 | data_train : array of floats
256 | The time series in the training set.
257 | data_test : array of floats
258 | The time series in the test set.
259 | target_train : array
260 | The classification labels in the training set.
261 | target_test : array
262 | The classification labels in the test set.
263 | DESCR : str
264 | The full description of the dataset.
265 | url : str
266 | The url of the dataset.
267 |
268 | Notes
269 | -----
270 | Padded values are represented as NaN's.
271 |
272 | """
273 | new_path = path + dataset + '/'
274 | try:
275 | with(open(new_path + dataset + '.txt', encoding='utf-8')) as f:
276 | description = f.read()
277 | except UnicodeDecodeError:
278 | with(open(new_path + dataset + '.txt', encoding='ISO-8859-1')) as f:
279 | description = f.read()
280 | try:
281 | data_train = np.genfromtxt(new_path + dataset + '_TRAIN.txt')
282 | data_test = np.genfromtxt(new_path + dataset + '_TEST.txt')
283 |
284 | X_train, y_train = data_train[:, 1:], data_train[:, 0]
285 | X_test, y_test = data_test[:, 1:], data_test[:, 0]
286 |
287 | except IndexError:
288 | train = loadarff(new_path + dataset + '_TRAIN.arff')
289 | test = loadarff(new_path + dataset + '_TEST.arff')
290 |
291 | data_train = np.asarray([train[0][name] for name in train[1].names()])
292 | X_train = data_train[:-1].T.astype('float64')
293 | y_train = data_train[-1]
294 |
295 | data_test = np.asarray([test[0][name] for name in test[1].names()])
296 | X_test = data_test[:-1].T.astype('float64')
297 | y_test = data_test[-1]
298 |
299 | try:
300 | y_train = y_train.astype('float64').astype('int64')
301 | y_test = y_test.astype('float64').astype('int64')
302 | except ValueError:
303 | y_train = y_train.astype(str)
304 | y_test = y_test.astype(str)
305 |
306 | bunch = Bunch(
307 | data_train=X_train, target_train=y_train,
308 | data_test=X_test, target_test=y_test,
309 | DESCR=description,
310 | url=("http://www.timeseriesclassification.com/"
311 | "description.php?Dataset={}".format(dataset))
312 | )
313 |
314 | return bunch
315 |
316 |
317 |
318 |
319 |
320 | """
321 | Utility functions for the UEA multivariate time series classification
322 | archive.
323 | """
324 |
325 | # Author: Johann Faouzi
326 | # License: BSD-3-Clause
327 |
328 | def _correct_uea_name_download(dataset):
329 | if dataset == 'Ering':
330 | return 'ERing'
331 | else:
332 | return dataset
333 |
334 |
335 |
336 | def fetch_uea_dataset(dataset, use_cache=True, data_home=None,
337 | return_X_y=False): # noqa 207
338 | """Fetch dataset from UEA TSC Archive by name.
339 |
340 | Fetched data sets are saved by default in the
341 | ``pyts/datasets/cached_datasets/UEA/`` folder. To avoid
342 | downloading the same data set several times, it is
343 | highly recommended not to change the default values
344 | of ``use_cache`` and ``path``.
345 |
346 | Parameters
347 | ----------
348 | dataset : str
349 | Name of the dataset.
350 |
351 | use_cache : bool (default = True)
352 | If True, look if the data set has already been fetched
353 | and load the fetched version if it is the case. If False,
354 | download the data set from the UCR Time Series Classification
355 | Archive.
356 |
357 | data_home : None or str (default = None)
358 | The path of the folder containing the cached data set.
359 | If None, the ``pyts.datasets.cached_datasets/UEA/`` folder is
360 | used. If the data set is not found, it is downloaded and cached
361 | in this path.
362 |
363 | return_X_y : bool (default = False)
364 | If True, returns ``(data_train, data_test, target_train, target_test)``
365 | instead of a Bunch object. See below for more information about the
366 | `data` and `target` object.
367 |
368 | Returns
369 | -------
370 | data : Bunch
371 | Dictionary-like object, with attributes:
372 |
373 | data_train : array of floats
374 | The time series in the training set.
375 | data_test : array of floats
376 | The time series in the test set.
377 | target_train : array of integers
378 | The classification labels in the training set.
379 | target_test : array of integers
380 | The classification labels in the test set.
381 | DESCR : str
382 | The full description of the dataset.
383 | url : str
384 | The url of the dataset.
385 |
386 | (data_train, data_test, target_train, target_test) : tuple if \
387 | ``return_X_y`` is True
388 |
389 | Notes
390 | -----
391 | Missing values are represented as NaN's.
392 |
393 | References
394 | ----------
395 | .. [1] A. Bagnall et al, "The UEA multivariate time series
396 | classification archive, 2018". arXiv:1811.00075 [cs, stat],
397 | 2018.
398 |
399 | .. [2] A. Bagnall et al, "The UEA & UCR Time Series Classification
400 | Repository", www.timeseriesclassification.com.
401 |
402 | """
403 | if dataset not in uea_dataset_list():
404 | raise ValueError(
405 | "{0} is not a valid name. The list of available names "
406 | "can be obtained with ``pyts.datasets.uea_dataset_list()``"
407 | .format(dataset)
408 | )
409 | if data_home is None:
410 | import pyts
411 | home = os.sep.join(pyts.__file__.split(os.sep)[:-2])
412 | path = os.path.join(home, 'pyts', 'datasets', 'cached_datasets', 'UEA')
413 | else:
414 | path = data_home
415 | if not os.path.exists(path):
416 | os.makedirs(path)
417 |
418 | correct_dataset = _correct_uea_name_download(dataset)
419 | if use_cache and os.path.exists(os.path.join(path, correct_dataset)):
420 | bunch = _load_uea_dataset(correct_dataset, path)
421 | else:
422 | #url = ("http://www.timeseriesclassification.com/"
423 | # "ClassificationDownloads/{0}.zip"
424 | # .format(correct_dataset))
425 | url = ("https://www.timeseriesclassification.com/aeon-toolkit/{0}.zip".format(correct_dataset))
426 | filename = 'temp_{}'.format(correct_dataset)
427 | _ = urlretrieve(url, os.path.join(path, filename))
428 | zipfile.ZipFile(os.path.join(path, filename)).extractall(
429 | os.path.join(path, correct_dataset)
430 | )
431 | os.remove(os.path.join(path, filename))
432 | bunch = _load_uea_dataset(correct_dataset, path)
433 |
434 | if return_X_y:
435 | return (bunch.data_train, bunch.data_test,
436 | bunch.target_train, bunch.target_test)
437 | return bunch
438 |
439 |
440 | def _load_uea_dataset(dataset, path):
441 | """Load a UEA data set from a local folder.
442 |
443 | Parameters
444 | ----------
445 | dataset : str
446 | Name of the dataset.
447 |
448 | path : str
449 | The path of the folder containing the cached data set.
450 |
451 | Returns
452 | -------
453 | data : Bunch
454 | Dictionary-like object, with attributes:
455 |
456 | data_train : array of floats
457 | The time series in the training set.
458 | data_test : array of floats
459 | The time series in the test set.
460 | target_train : array
461 | The classification labels in the training set.
462 | target_test : array
463 | The classification labels in the test set.
464 | DESCR : str
465 | The full description of the dataset.
466 | url : str
467 | The url of the dataset.
468 |
469 | Notes
470 | -----
471 | Missing values are represented as NaN's.
472 |
473 | """
474 | new_path = os.path.join(path, dataset)
475 | try:
476 | description_file = [
477 | file for file in os.listdir(new_path)
478 | if ('Description.txt' in file
479 | or f'{dataset}.txt' in file)
480 | ][0]
481 | except IndexError:
482 | description_file = None
483 |
484 | if description_file is not None:
485 | try:
486 | with open(os.path.join(new_path, description_file),
487 | encoding='utf-8') as f:
488 | description = f.read()
489 | except UnicodeDecodeError:
490 | with open(os.path.join(new_path, description_file),
491 | encoding='ISO-8859-1') as f:
492 | description = f.read()
493 | else:
494 | description = None
495 |
496 | data_train = loadarff(os.path.join(new_path, f'{dataset}_TRAIN.arff'))
497 | X_train, y_train = _parse_relational_arff(data_train)
498 |
499 | data_test = loadarff(os.path.join(new_path, f'{dataset}_TEST.arff'))
500 | X_test, y_test = _parse_relational_arff(data_test)
501 |
502 | bunch = Bunch(
503 | data_train=X_train, target_train=y_train,
504 | data_test=X_test, target_test=y_test,
505 | DESCR=description,
506 | url=("http://www.timeseriesclassification.com/"
507 | "description.php?Dataset={}".format(dataset))
508 | )
509 |
510 | return bunch
511 |
512 |
513 | def _parse_relational_arff(data):
514 | X_data = np.asarray(data[0])
515 | n_samples = len(X_data)
516 | X, y = [], []
517 |
518 | if X_data[0][0].dtype.names is None:
519 | for i in range(n_samples):
520 | X_sample = np.asarray(
521 | [X_data[i][name] for name in X_data[i].dtype.names]
522 | )
523 | X.append(X_sample.T)
524 | y.append(X_data[i][1])
525 | else:
526 | for i in range(n_samples):
527 | X_sample = np.asarray(
528 | [X_data[i][0][name] for name in X_data[i][0].dtype.names]
529 | )
530 | X.append(X_sample.T)
531 | y.append(X_data[i][1])
532 |
533 | X = np.asarray(X).astype('float64')
534 | y = np.asarray(y)
535 |
536 | try:
537 | y = y.astype('float64').astype('int64')
538 | except ValueError:
539 | y = y.astype(str)
540 |
541 | return X, y
542 |
--------------------------------------------------------------------------------
/examples/Detach_Ensemble_example_UEA.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {
6 | "id": "EaTES2tMkBAR"
7 | },
8 | "source": [
9 | "## Install Required Libraries"
10 | ]
11 | },
12 | {
13 | "cell_type": "code",
14 | "execution_count": 1,
15 | "metadata": {
16 | "colab": {
17 | "base_uri": "https://localhost:8080/"
18 | },
19 | "id": "fMLwcJUJ5ynP",
20 | "outputId": "f44eda72-1cf6-4ac6-dfa2-59ab001370c6"
21 | },
22 | "outputs": [],
23 | "source": [
24 | "!pip install numpy scikit-learn pyts torch matplotlib sktime==0.30.0 --quiet\n",
25 | "!pip install git+https://github.com/gon-uri/detach_rocket --quiet"
26 | ]
27 | },
28 | {
29 | "cell_type": "markdown",
30 | "metadata": {
31 | "id": "v4WN7u9ql-0h"
32 | },
33 | "source": [
34 | "## Download Dataset from UEA"
35 | ]
36 | },
37 | {
38 | "cell_type": "code",
39 | "execution_count": 2,
40 | "metadata": {
41 | "id": "ruXkm-gumddl"
42 | },
43 | "outputs": [],
44 | "source": [
45 | "# Download Dataset\n",
46 | "from detach_rocket.utils_datasets import fetch_uea_dataset\n",
47 | "\n",
48 | "dataset_name_list = ['SelfRegulationSCP1'] \n",
49 | "current_dataset = fetch_uea_dataset(dataset_name_list[0])"
50 | ]
51 | },
52 | {
53 | "cell_type": "markdown",
54 | "metadata": {
55 | "id": "dgzA1-1tci-q"
56 | },
57 | "source": [
58 | "## Prepare Dataset Matrices"
59 | ]
60 | },
61 | {
62 | "cell_type": "code",
63 | "execution_count": 3,
64 | "metadata": {
65 | "colab": {
66 | "base_uri": "https://localhost:8080/"
67 | },
68 | "id": "NmzM3E_OxaU1",
69 | "outputId": "f1179dc7-5864-4db1-d9c1-0519f323dd7d"
70 | },
71 | "outputs": [
72 | {
73 | "name": "stdout",
74 | "output_type": "stream",
75 | "text": [
76 | "Dataset Matrix Shape: ( # of instances , # of channels , time series length )\n",
77 | " \n",
78 | "Train: (268, 6, 896)\n",
79 | " \n",
80 | "Test: (293, 6, 896)\n"
81 | ]
82 | }
83 | ],
84 | "source": [
85 | "print(f\"Dataset Matrix Shape: ( # of instances , # of channels , time series length )\")\n",
86 | "print(f\" \")\n",
87 | "\n",
88 | "# Train Matrix\n",
89 | "X_train = current_dataset['data_train']\n",
90 | "print(f\"Train: {X_train.shape}\")\n",
91 | "y_train = current_dataset['target_train']\n",
92 | "\n",
93 | "print(f\" \")\n",
94 | "\n",
95 | "# Test Matrix\n",
96 | "X_test = current_dataset['data_test']\n",
97 | "print(f\"Test: {X_test.shape}\")\n",
98 | "y_test = current_dataset['target_test']"
99 | ]
100 | },
101 | {
102 | "cell_type": "markdown",
103 | "metadata": {
104 | "id": "IkgnHeNtmFec"
105 | },
106 | "source": [
107 | "## Train and Evaluate the Model"
108 | ]
109 | },
110 | {
111 | "cell_type": "code",
112 | "execution_count": 4,
113 | "metadata": {
114 | "colab": {
115 | "background_save": true,
116 | "base_uri": "https://localhost:8080/"
117 | },
118 | "id": "o5uOBpflHKjJ",
119 | "outputId": "f28e5bb3-ed41-4fc8-e42f-4496de73274f"
120 | },
121 | "outputs": [
122 | {
123 | "name": "stdout",
124 | "output_type": "stream",
125 | "text": [
126 | "TRAINING RESULTS Full ROCKET:\n",
127 | "Optimal Alpha Full ROCKET: 428.13\n",
128 | "Train Accuraccy Full ROCKET: 98.51%\n",
129 | "-------------------------\n",
130 | "TRAINING RESULTS Detach Model:\n",
131 | "Optimal Alpha Detach Model: 37.93\n",
132 | "Train Accuraccy Detach Model: 95.15%\n",
133 | "-------------------------\n",
134 | "TRAINING RESULTS Full ROCKET:\n",
135 | "Optimal Alpha Full ROCKET: 428.13\n",
136 | "Train Accuraccy Full ROCKET: 98.88%\n",
137 | "-------------------------\n",
138 | "TRAINING RESULTS Detach Model:\n",
139 | "Optimal Alpha Detach Model: 37.93\n",
140 | "Train Accuraccy Detach Model: 98.88%\n",
141 | "-------------------------\n",
142 | "TRAINING RESULTS Full ROCKET:\n",
143 | "Optimal Alpha Full ROCKET: 428.13\n",
144 | "Train Accuraccy Full ROCKET: 98.13%\n",
145 | "-------------------------\n",
146 | "TRAINING RESULTS Detach Model:\n",
147 | "Optimal Alpha Detach Model: 37.93\n",
148 | "Train Accuraccy Detach Model: 94.78%\n",
149 | "-------------------------\n",
150 | "TRAINING RESULTS Full ROCKET:\n",
151 | "Optimal Alpha Full ROCKET: 428.13\n",
152 | "Train Accuraccy Full ROCKET: 98.51%\n",
153 | "-------------------------\n",
154 | "TRAINING RESULTS Detach Model:\n",
155 | "Optimal Alpha Detach Model: 37.93\n",
156 | "Train Accuraccy Detach Model: 94.03%\n",
157 | "-------------------------\n",
158 | "TRAINING RESULTS Full ROCKET:\n",
159 | "Optimal Alpha Full ROCKET: 428.13\n",
160 | "Train Accuraccy Full ROCKET: 98.13%\n",
161 | "-------------------------\n",
162 | "TRAINING RESULTS Detach Model:\n",
163 | "Optimal Alpha Detach Model: 37.93\n",
164 | "Train Accuraccy Detach Model: 92.54%\n",
165 | "-------------------------\n",
166 | "Train Accuracy: 97.01%\n",
167 | "Test Accuracy: 93.52%\n"
168 | ]
169 | }
170 | ],
171 | "source": [
172 | "from detach_rocket.detach_classes import DetachEnsemble\n",
173 | "from sklearn.metrics import accuracy_score\n",
174 | "\n",
175 | "# Select initial model characteristics\n",
176 | "num_models = 5\n",
177 | "num_kernels = 1000\n",
178 | "\n",
179 | "# Create model object\n",
180 | "DetachEnsembleModel = DetachEnsemble(num_models=num_models, num_kernels=num_kernels)\n",
181 | "\n",
182 | "# Train Model\n",
183 | "DetachEnsembleModel.fit(X_train, y_train)\n",
184 | "\n",
185 | "# Evaluate Performance on Train set\n",
186 | "y_train_pred = DetachEnsembleModel.predict(X_train)\n",
187 | "print('Train Accuracy: {:.2f}%'.format(100*accuracy_score(y_train, y_train_pred)))\n",
188 | "\n",
189 | "y_test_pred = DetachEnsembleModel.predict(X_test)\n",
190 | "print('Test Accuracy: {:.2f}%'.format(100*accuracy_score(y_test, y_test_pred)))"
191 | ]
192 | },
193 | {
194 | "cell_type": "markdown",
195 | "metadata": {
196 | "id": "lwFoPnnFmN5V"
197 | },
198 | "source": [
199 | "## Estimate and plot channel relevance"
200 | ]
201 | },
202 | {
203 | "cell_type": "code",
204 | "execution_count": 5,
205 | "metadata": {
206 | "colab": {
207 | "background_save": true
208 | },
209 | "id": "zmyz2GeojYwl",
210 | "outputId": "34ff3981-faa0-49fe-ec8b-58f996ac9c54"
211 | },
212 | "outputs": [
213 | {
214 | "data": {
215 | "image/png": "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",
216 | "text/plain": [
217 | ""
218 | ]
219 | },
220 | "metadata": {
221 | "needs_background": "light"
222 | },
223 | "output_type": "display_data"
224 | }
225 | ],
226 | "source": [
227 | "import matplotlib.pyplot as plt\n",
228 | "\n",
229 | "x = range(1, DetachEnsembleModel.num_channels + 1)\n",
230 | "channel_relevance = DetachEnsembleModel.estimate_channel_relevance()\n",
231 | "\n",
232 | "plt.figure(figsize=(8,3.5))\n",
233 | "plt.bar(x, channel_relevance, color='C7', zorder=2)\n",
234 | "\n",
235 | "plt.title('Channel relevance estimation')\n",
236 | "plt.grid(True, linestyle='-', alpha=0.5, zorder=1)\n",
237 | "plt.xlabel('Channels')\n",
238 | "plt.ylabel('Relevance Estimation (arb. unit)')\n",
239 | "plt.show()"
240 | ]
241 | }
242 | ],
243 | "metadata": {
244 | "colab": {
245 | "provenance": []
246 | },
247 | "kernelspec": {
248 | "display_name": "Python 3",
249 | "name": "python3"
250 | },
251 | "language_info": {
252 | "codemirror_mode": {
253 | "name": "ipython",
254 | "version": 3
255 | },
256 | "file_extension": ".py",
257 | "mimetype": "text/x-python",
258 | "name": "python",
259 | "nbconvert_exporter": "python",
260 | "pygments_lexer": "ipython3",
261 | "version": "3.8.10"
262 | }
263 | },
264 | "nbformat": 4,
265 | "nbformat_minor": 0
266 | }
267 |
--------------------------------------------------------------------------------
/examples/SFD_example_tsfresh.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {
6 | "id": "EaTES2tMkBAR"
7 | },
8 | "source": [
9 | "## Install Required Libraries"
10 | ]
11 | },
12 | {
13 | "cell_type": "code",
14 | "execution_count": 1,
15 | "metadata": {
16 | "colab": {
17 | "base_uri": "https://localhost:8080/"
18 | },
19 | "id": "fMLwcJUJ5ynP",
20 | "outputId": "90d2e7a0-ef9c-4e4d-e360-3f936410193f"
21 | },
22 | "outputs": [],
23 | "source": [
24 | "!pip install sktime --quiet\n",
25 | "!pip install pyts --quiet\n",
26 | "!pip install tsfresh --quiet\n",
27 | "#!pip install git+https://github.com/gon-uri/detach_rocket --quiet"
28 | ]
29 | },
30 | {
31 | "cell_type": "code",
32 | "execution_count": 2,
33 | "metadata": {},
34 | "outputs": [
35 | {
36 | "name": "stdout",
37 | "output_type": "stream",
38 | "text": [
39 | "/Users/uribarri/Desktop/DETACH/detach_rocket\n"
40 | ]
41 | }
42 | ],
43 | "source": [
44 | "%load_ext autoreload\n",
45 | "%autoreload 2\n",
46 | "\n",
47 | "import sys\n",
48 | "import os\n",
49 | "\n",
50 | "# Get the path to the current notebook\n",
51 | "current_dir = os.path.dirname(os.path.dirname(os.path.abspath('__file__')))\n",
52 | "print(current_dir)\n",
53 | "\n",
54 | "# Add the local subfolder 'library_name' to the beginning of sys.path\n",
55 | "#sys.path.insert(0, os.path.join(current_dir, 'detach_rocket'))\n",
56 | "sys.path.insert(0, current_dir)"
57 | ]
58 | },
59 | {
60 | "cell_type": "code",
61 | "execution_count": 6,
62 | "metadata": {},
63 | "outputs": [
64 | {
65 | "name": "stdout",
66 | "output_type": "stream",
67 | "text": [
68 | "The autoreload extension is already loaded. To reload it, use:\n",
69 | " %reload_ext autoreload\n"
70 | ]
71 | },
72 | {
73 | "data": {
74 | "text/plain": [
75 | "'max'"
76 | ]
77 | },
78 | "execution_count": 6,
79 | "metadata": {},
80 | "output_type": "execute_result"
81 | }
82 | ],
83 | "source": [
84 | "%load_ext autoreload\n",
85 | "\n",
86 | "from detach_rocket.detach_classes import DetachMatrix\n",
87 | "import numpy as np\n",
88 | "\n",
89 | "np.random.seed(4)\n",
90 | "\n",
91 | "# Create model object\n",
92 | "DetachMatrixModel = DetachMatrix()\n",
93 | "\n",
94 | "DetachMatrixModel.multilabel_type"
95 | ]
96 | },
97 | {
98 | "cell_type": "markdown",
99 | "metadata": {
100 | "id": "v4WN7u9ql-0h"
101 | },
102 | "source": [
103 | "## Download Dataset from UCR"
104 | ]
105 | },
106 | {
107 | "cell_type": "code",
108 | "execution_count": 7,
109 | "metadata": {
110 | "id": "ruXkm-gumddl"
111 | },
112 | "outputs": [],
113 | "source": [
114 | "from detach_rocket.utils_datasets import fetch_ucr_dataset\n",
115 | "\n",
116 | "# Download Dataset\n",
117 | "dataset_name_list = ['PhalangesOutlinesCorrect'] # PhalangesOutlinesCorrect ProximalPhalanxOutlineCorrect #Fordb\n",
118 | "current_dataset = fetch_ucr_dataset(dataset_name_list[0])"
119 | ]
120 | },
121 | {
122 | "cell_type": "markdown",
123 | "metadata": {
124 | "id": "dgzA1-1tci-q"
125 | },
126 | "source": [
127 | "## Prepare Dataset Matrices"
128 | ]
129 | },
130 | {
131 | "cell_type": "code",
132 | "execution_count": 8,
133 | "metadata": {
134 | "colab": {
135 | "base_uri": "https://localhost:8080/"
136 | },
137 | "id": "NmzM3E_OxaU1",
138 | "outputId": "e65485e2-bf7e-4339-e55f-dc19c8fa2b1d"
139 | },
140 | "outputs": [
141 | {
142 | "name": "stdout",
143 | "output_type": "stream",
144 | "text": [
145 | "Dataset Matrix Shape: ( # of instances , time series length )\n",
146 | " \n",
147 | "Train: (1800, 80)\n",
148 | " \n",
149 | "Test: (858, 80)\n"
150 | ]
151 | }
152 | ],
153 | "source": [
154 | "import numpy as np\n",
155 | "\n",
156 | "# Create data matrices and remove possible rows with nans\n",
157 | "\n",
158 | "print(f\"Dataset Matrix Shape: ( # of instances , time series length )\")\n",
159 | "print(f\" \")\n",
160 | "\n",
161 | "# Train Matrix\n",
162 | "X_train = current_dataset['data_train']\n",
163 | "print(f\"Train: {X_train.shape}\")\n",
164 | "non_nan_mask_train = ~np.isnan(X_train).any(axis=1)\n",
165 | "non_inf_mask_train = ~np.isinf(X_train).any(axis=1)\n",
166 | "mask_train = np.logical_and(non_nan_mask_train,non_inf_mask_train)\n",
167 | "X_train = X_train[mask_train]\n",
168 | "X_train = X_train.reshape(X_train.shape[0],1,X_train.shape[1])\n",
169 | "y_train = current_dataset['target_train']\n",
170 | "y_train = y_train[mask_train]\n",
171 | "\n",
172 | "print(f\" \")\n",
173 | "\n",
174 | "# Test Matrix\n",
175 | "X_test = current_dataset['data_test']\n",
176 | "#print(f\"Number of test instances: {len(X_test)}\")\n",
177 | "print(f\"Test: {X_test.shape}\")\n",
178 | "non_nan_mask_test = ~np.isnan(X_test).any(axis=1)\n",
179 | "non_inf_mask_test = ~np.isinf(X_test).any(axis=1)\n",
180 | "mask_test = np.logical_and(non_nan_mask_test,non_inf_mask_test)\n",
181 | "X_test = X_test[mask_test]\n",
182 | "X_test = X_test.reshape(X_test.shape[0],1,X_test.shape[1])\n",
183 | "y_test = current_dataset['target_test']\n",
184 | "y_test = y_test[mask_test]"
185 | ]
186 | },
187 | {
188 | "cell_type": "markdown",
189 | "metadata": {
190 | "id": "AXonEpdttc9D"
191 | },
192 | "source": [
193 | "## Apply TSFresh Transformation to Time Series"
194 | ]
195 | },
196 | {
197 | "cell_type": "code",
198 | "execution_count": 9,
199 | "metadata": {
200 | "colab": {
201 | "base_uri": "https://localhost:8080/"
202 | },
203 | "id": "evUwhQg6s6kU",
204 | "outputId": "73c1a7e0-6c65-43bd-bb59-45e6e9f995ec"
205 | },
206 | "outputs": [
207 | {
208 | "name": "stderr",
209 | "output_type": "stream",
210 | "text": [
211 | "Feature Extraction: 100%|██████████| 1800/1800 [01:11<00:00, 25.32it/s]\n",
212 | "Feature Extraction: 100%|██████████| 858/858 [00:34<00:00, 24.75it/s]\n"
213 | ]
214 | },
215 | {
216 | "name": "stdout",
217 | "output_type": "stream",
218 | "text": [
219 | " \n",
220 | "TSFresh Features Matrix Shape: ( # of instances , # of features )\n",
221 | " \n",
222 | "Train: (1800, 783)\n",
223 | " \n",
224 | "Test: (858, 783)\n"
225 | ]
226 | }
227 | ],
228 | "source": [
229 | "from sktime.transformations.panel.tsfresh import TSFreshFeatureExtractor\n",
230 | "\n",
231 | "# Create TSFresh trasformation\n",
232 | "ts_fresh_transform = TSFreshFeatureExtractor(default_fc_parameters=\"comprehensive\", show_warnings=False, disable_progressbar=False)\n",
233 | "\n",
234 | "# Fit and transform Time Series\n",
235 | "X_train_ts = ts_fresh_transform.fit_transform(X_train)\n",
236 | "X_test_ts = ts_fresh_transform.transform(X_test)\n",
237 | "\n",
238 | "print(f\" \")\n",
239 | "print(f\"TSFresh Features Matrix Shape: ( # of instances , # of features )\")\n",
240 | "print(f\" \")\n",
241 | "print(f\"Train: {X_train_ts.shape}\")\n",
242 | "print(f\" \")\n",
243 | "print(f\"Test: {X_test_ts.shape}\")"
244 | ]
245 | },
246 | {
247 | "cell_type": "markdown",
248 | "metadata": {
249 | "id": "IkgnHeNtmFec"
250 | },
251 | "source": [
252 | "## Train and Evaluate the Model"
253 | ]
254 | },
255 | {
256 | "cell_type": "code",
257 | "execution_count": 10,
258 | "metadata": {
259 | "colab": {
260 | "base_uri": "https://localhost:8080/"
261 | },
262 | "id": "o5uOBpflHKjJ",
263 | "outputId": "0809acca-be87-4792-9cdc-7b24ec8400cc"
264 | },
265 | "outputs": [
266 | {
267 | "name": "stdout",
268 | "output_type": "stream",
269 | "text": [
270 | "The autoreload extension is already loaded. To reload it, use:\n",
271 | " %reload_ext autoreload\n",
272 | "TRAINING RESULTS Full Features:\n",
273 | "Optimal Alpha Full Features: 428.13\n",
274 | "Train Accuraccy Full Features: 84.28%\n",
275 | "-------------------------\n",
276 | "TRAINING RESULTS Detach Model:\n",
277 | "Optimal Alpha Detach Model: 37.93\n",
278 | "Train Accuraccy Detach Model: 82.39%\n",
279 | "-------------------------\n",
280 | "Test Accuraccy Full Model: 75.52%\n",
281 | "Test Accuraccy Detach Model: 76.11%\n"
282 | ]
283 | }
284 | ],
285 | "source": [
286 | "%load_ext autoreload\n",
287 | "\n",
288 | "from detach_rocket.detach_classes import DetachMatrix\n",
289 | "\n",
290 | "np.random.seed(4)\n",
291 | "\n",
292 | "# Create model object\n",
293 | "DetachMatrixModel = DetachMatrix()\n",
294 | "\n",
295 | "# Trian Model\n",
296 | "DetachMatrixModel.fit(X_train_ts,y_train)\n",
297 | "\n",
298 | "# Evaluate Performance on Test Set\n",
299 | "detach_test_score, full_test_score= DetachMatrixModel.score(X_test_ts,y_test)\n",
300 | "print('Test Accuraccy Full Model: {:.2f}%'.format(100*full_test_score))\n",
301 | "print('Test Accuraccy Detach Model: {:.2f}%'.format(100*detach_test_score))"
302 | ]
303 | },
304 | {
305 | "cell_type": "markdown",
306 | "metadata": {
307 | "id": "lwFoPnnFmN5V"
308 | },
309 | "source": [
310 | "## Plot SFD Curve and Optimal Model Selection"
311 | ]
312 | },
313 | {
314 | "cell_type": "code",
315 | "execution_count": 12,
316 | "metadata": {
317 | "colab": {
318 | "base_uri": "https://localhost:8080/",
319 | "height": 366
320 | },
321 | "id": "zmyz2GeojYwl",
322 | "outputId": "16eab3f8-cbc1-4336-c0fb-e29a8310e582"
323 | },
324 | "outputs": [
325 | {
326 | "data": {
327 | "image/png": "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nj2+tD/+8Y+ICBs3bgz5/vPnz+f222+v9jlLlizhscceCzm/QE6cOMH48ePp2rUr48eP5+TJkwHPu/nmm2nVqhV9+vQplX7fffexcuXKGpXBGGOqozZXnnsL2IHTWv9fnOfa76h2jucJVeXKK69k+vTp7N69m127dnH69GkeeuihoNeVDext2rRh4cKFbheXvn378uabb/reL1y4kF69ermer7+nnnqK//qv/6rRPZ588knGjh3L7t27GTt2LE8++WTA82666SaWLl1aLv2OO+6o8BpjjHFTba8811lVf6yqK1W1+l8jziMrV64kLi6OH/3oR4CzuP8f//hHXnrpJXJycpg/fz7Tpk3jsssuo3v37jz66KMAPPDAA+zdu5cBAwZw//33k5aW5mtVzp8/n+nTpzNlyhQ6derE3LlzeeaZZxg4cCAXXXQRJ06cAOD5559nyJAh9O/fnxkzZpCTk1NpeadPn8677zrrBn333Xc0adKEli1b+o6/8cYb9O3blz59+vDzn//cl75gwQK6devGyJEjWbt2rS/92LFjzJgxgyFDhjBkyJBSxwLZtWsX9evXp0WLFgAcOXKEH/7wh/Tv35/+/fuzbt26Sj8DwLvvvsuNN94IwI033siSJUsCnnfppZfSrFmzcukdOnQgIyODw4cPh5SfMcaEi+uPu4nIGFVdCcQD08qOL6rqO9XOtbbdfTexX34JUWHczG7AAPjTnyo8vG3bNgYPHlwqLSEhgfbt2/u6vL/44gu2bt1KfHw8Q4YM4YorruDJJ59k69atbNmyBaDcykNbt25l8+bN5OXl0aVLF373u9+xefNm7rnnHv7+979z9913c+WVV3LrrbcC8PDDD/Piiy9yxx3BO1kSEhJo164dW7du5d1332XWrFm8/PLLgDMc8POf/5xNmzZxwQUXMGHCBJYsWcKwYcN4/PHH+fLLL2nSpAmjR49m4MCBANx1113cc889XHzxxezfv5+JEycGfSxu7dq1DBo0yPf+zjvvZOTIkSxevJji4mJOnz4NwCWXXFJqdSavp59+mnHjxnHkyBFat24NQOvWrTl69GjQzx3IoEGDWLt2LTNmzKjytcYYU10igoigqq5NnhsJrASmBDimwLkT2OuAqgacbOWfPn78eJo3bw7AlVdeyZo1a5g+fXrQ+44ePZrGjRvTuHFjmjRpwpQpzh9P3759+frrrwEn+D/88MNkZmZy+vRpJk4Mbffba665hjfffJNly5axYsUKX2DfsGEDo0aN8rXgZ8+ezaeffgo4LV9v+qxZs9i1axcAy5cvZ/v27b57Z2VlBQzIXunp6aV6CFauXMnf//53wOntaNKkCQD//ve/Q/osNdGqVSsOHQq44KExxrgqKiqK4uJid1rsqvr/PC8f8+y05uNZFe7c8ac/UVjLz1v37t2bRYsWlUrLysriwIEDdO7cmU2bNpUL/KHMuvb/DFFRUb73UVFRFBUVAc748ZIlS+jfvz/z589n1apVIZV5ypQp3H///aSkpJCQ8J+HHoJ9c6yozCUlJXz22Wc0aNAgpLwbNGjAqVOnKj2vshZ7YmIi6enptG7dmvT0dFq1ahVS/v7y8vJCLrcxxoSTN7C7Pca+KECa+7O5znFjx44lJyfH1+osLi7m3nvv5aabbiI+Ph6Ajz/+mBMnTpCbm8uSJUsYMWIEjRs3DtqyDUV2djatW7emsLCQ1157LeTrGjRowO9+97tyE/yGDRvG6tWrOX78OMXFxbzxxhuMHDmSYcOG8emnn5KRkUFhYSFvv/2275oJEyYwd+5c33vv0EJFevbsWWpW/tixY3n22WcBp+6ysrIAp8W+ZcuWcj/jxjlbE0ydOpUFC5yHNhYsWMC0adNC/vxeu3btKjdb3hhjaoN3nN2VWfEi0kNEZgBNRORKv5+bgLN+gZq6JiIsXryYt99+m65du9KtWzfi4uL4zW9+4zvn4osvZs6cOQwYMIAZM2aQkpJC8+bNGTFiBH369OH++++vVt6PP/44w4YNY/z48fTo0aNK115zzTWlxrrBGav+7W9/y+jRo+nfvz+DBg1i2rRptG7dmocffpjhw4czbty4Utf95S9/YePGjfTr149evXrxt7/9LWi+l156KZs3b/Z9S/3zn//MJ598Qt++fRk8eDDbtm0LqfwPPPAAH3/8MV27duXjjz/mgQceAJx5ApdffrnvvGuvvZbhw4ezc+dOkpOTefHFFwFnKeI9e/aQkpISUn7GGBNO3sBekxa7VHSxiEwDpgNTcTZn8coG3lTV0KYp14KUlBQt+7z1jh076Nmzp+/92bb06fz589m4cWOpVu25KJz1etdddzFlyhRf67suLF68mC+//JLHH3884PGyf6/ckpaWFjk7Zp0lrE7dEVH1OmqU8zvE4Us3/P73v+fMmTN069aN6667Lui5IrJJVcu1QoKNsb8LvCsiw1X1s5oX15jgfvGLX/D555/XaRmKioq4995767QMxpjzVzha7KHsx75ZRH4K9MavC15Vb652roabbrqJm266qa6LcVZJTExk6tSpdVqGq666qk7zN8ac38IR2EOZPPcKkARMBFYDyTjd8cYYY4wJI1cnz/npoqq/BM6o6gLgCqBvtXM0xhhjTEDeR4jdbrEXen5nikgfoAnQsdo5GmOMMSagcLTYQxljnyciFwC/xJkd3wj4VbVzNMYYY0xAtTLGrqovqOpJVV2tqheqaitVDf5QsgHgiSeeoHfv3vTr148BAwb4ZnyPGjWK7t27M2DAAAYMGODbvS06OpoBAwbQu3dv+vfvzzPPPFOjb23GGGPOLa7OiheRnwW7UFWfqW6mInIV8AjQExiqqhs96eOBJ4F6QAFwv2cjmnPOZ599xj//+U++/PJL6tevz/Hjx0vtxf7aa6+VWwSlQYMGvhXajh49ynXXXcepU6d8O7+FQ1FRETExoXTUGGOMqW1uT55rXMlPTWwFrgQ+LZN+HJiiqn2BG3Fm5J+T0tPTadGihW/xlhYtWtCmTZuQr2/VqhXz5s1j7ty5Ab+5PfXUU/Tt25f+/fv7VlcbNWoU3oV6jh8/7ls0Yv78+Vx11VVMmTKFCRMmMGvWLP71r3/57nXTTTexaNEiiouLuf/++xkyZAj9+vXjueeeq+7HN8YYUw2utthVNXzNxPL33gHlNxBR1c1+b7cBcSJSX1Xza5Lfhx9+SHp6ekibrFRFUlISkyZNCnhswoQJPPbYY3Tr1o1x48Yxa9YsRo4c6Ts+e/Zs30YjK1as8O3y5u/CCy+kpKSEo0ePkpiYWOrzLFmyhM8//5z4+HjfPuzBfPbZZ3z99dc0a9aMxYsX89Zbb3H55ZdTUFDAihUrePbZZ3nxxRdp0qQJGzZsID8/nxEjRjBhwgQ6dTq39vwxxphzVa1MnhORbsCzQKKq9hGRfsBUVf11tXMNzQxgc0VBXURuA24DaNu2bbl9y4uKisjPdy5NT09n//79YS+gqvryKCs2NpZ169axZs0aVq9ezaxZs3j88ce54YYbKCkp4eWXXy61X7v3PmXv583DP33ZsmVcf/31REdHk5+fT8OGDcnPz6ekpISCggLf+d5rCwsLGTNmjO+8MWPGcMcdd5CVlcVHH33EiBEjiIqKYunSpXzzzTe+zVxOnTrFtm3bgvY0eHeUO18UFRWV+7vmhoyMDNfzON9Ynbojkuo1KS8PgMO18G+8It4h24KCgmr/XxPKYOvzwP3AcwCq+rWIvA4EDewishxnYZuyHvIsVxvs2t7A74AJFZ2jqvOAeeCsFV92reIdO3b4usFbt27tvW+wbKssKSmp0nXSJ0yYwIQJExg4cCALFizg1ltvJSoqinr16gW81j/tu+++Izo6mnbt2pUqe0XX16tXj5iYGOrXr+/b971+/frExsaSkJDgO79+/fqMHj2aVatW8c477zB79mzq16+PiDB37tyQ928PVOZIFxMTU2vrYkfM+ttnEatTd0RMvcY5i6vW5efx7v5Zk/9rQgns8ar6RZmgWGkzTVWrtZOHiCQDi4EbVHVvde5R1qRJk2p9E5idO3cSFRVF165dAWfb0g4dOoR8/bFjx/jxj3/M7bffXu4Libeb/7rrrvN1xTdr1oyOHTuyadMmhg4d6ptpX5FrrrmGF154gY0bNzJ//nwAJk6cyLPPPsuYMWOIjY1l165dtG3bloYNG1btwxtjjKmW2nqO/biIdAYUQERmAunVzjEIEWkKfAA8qKpr3cijtpw+fZo77riDzMxMYmJi6NKlC/PmzQt6TW5uLgMGDKCwsJCYmBjmzJnDz35W/uGEyy67jC1btpCSkkK9evW4/PLL+c1vfsN9993H1VdfzSuvvMKYMWOC5jVhwgRuuOEGpk6dSr169QC45ZZbSEtLY9CgQagqLVu2ZMmSJdWuA2OMMVUTjpXnKty21S+TC3G6vH8AnARSgdmquq/amYr8EPgfoCWQCWxR1Yki8jDwILDb7/QJqno02P3OxW1bI8X5Vq+2beu5y+rUHRFVr2fBtq1vvPEGO3fu5IILLuCuu+4Kem6Vt231UtXvgHEi0hDn8bhcYBZQ7cCuqotxutvLpv+aSsbujTHGmEjl6spzIpIgIg+KyFzPwjE5OM+W7wGurnaOxhhjjAnI7TH2V3C63j8DbgX+G2dFuOmquqXaORpjjDEmoEGDBtGpUyeys6u/O3qwwH6hZwU4ROQFnFXh2qvqObMXu/eRL2PCoSZdY8YYE4rOnTsD1Gi9jGBLynq3a0VVi4HUcymox8XFkZGRYf8Zm7BQVTIyMojzPOdqjDFnq2At9v4ikuV5LUADz3sBVFUTXC9dDSQnJ3Pw4EGOHTsG2OYnbjmf6jUuLo7k5OS6LoYxxgQVbK346NosSLjFxsaWWuM8oh7JOItYvRpjzNml0v3YjTHGGHPusMBujDHGRBAL7MYYY0wEqXRJ2XOBiByj8pXwWuA8smfCy+rVHVav4Wd16g6rV3eEUq8dVLVl2cSICOyhEJGNgdbUNTVj9eoOq9fwszp1h9WrO2pSr9YVb4wxxkQQC+zGGGNMBDmfAnvwzdBNdVm9usPqNfysTt1h9eqOatfreTPGbowxxpwPzqcWuzHGGBPxIiKwi8hLInJURLb6pTUTkY9FZLfn9wV+xx4UkT0islNEJtZNqc89InKPiGwTka0i8oaIxAWrZxMaEWkqIgtF5FsR2SEiw61ew0NEokVks4j80/Pe6rUGRKSdiHzi+Xu6TUTu8qRbvYaRiFzmiU97ROSBql4fEYEdmA9cVibtAWCFqnYFVnjeIyK9gGuA3p5r/ioi5/S6+LVBRNoCdwIpqtoHiMapx4D1bKrkz8BSVe0B9Ad2YPUaLnfh1KeX1WvNFAH3qmpP4CLgp57/U61ew8QTj/4XmAT0Aq711HHIIiKwq+qnwIkyydOABZ7XC4Dpfulvqmq+qqYCe4ChtVHOCBCDs8tfDBAPHKLiejYhEJEE4FLgRQBVLVDVTKxea0xEkoErgBf8kq1ea0BV01X1S8/rbJwvTW2xeg2nocAeVf1OVQuAN3HqN2QREdgrkKiq6eD8ZQRaedLbAgf8zjvoSTNBqOr3wNPAfiAdOKWqH1FxPZvQXAgcA172dBm/ICINsXoNhz8B/w2U+KVZvYaJiHQEBgKfY/UaTjWOUZEc2CsiAdLs0YBKeMbMpgGdgDZAQxG5vm5LFRFigEHAs6o6EDiDdWPWmIhMBo6q6qa6LkskEpFGwCLgblXNquvyRJgax6hIDuxHRKQ1gOf3UU/6QaCd33nJOF3KJrhxQKqqHlPVQuAd4AdUXM8mNAeBg6r6uef9QpxAb/VaMyOAqSKShtOVOUZEXsXqtcZEJBYnqL+mqu94kq1ew6fGMSqSA/t7wI2e1zcC7/qlXyMi9UWkE9AV+KIOyneu2Q9cJCLxIiLAWJzxtYrq2YRAVQ8DB0SkuydpLLAdq9caUdUHVTVZVTviTPJcqarXY/VaI55/+y8CO1T1Gb9DVq/hswHoKiKdRKQezt/f96pyg4hYoEZE3gBG4eyGcwT4f8AS4B9Ae5ygdJWqnvCc/xBwM84Mz7tV9cPaL/W5R0QeBWbh1Ntm4BagERXUswmNiAzAmeBVD/gO+BHOl26r1zAQkVHAfao6WUSaY/VabSJyMfBv4Bv+M3fhFzjj7FavYSIil+PMEYkGXlLVJ6p0fSQEdmOMMcY4Irkr3hhjjDnvWGA3xhhjIogFdmOMMSaCWGA3xhhjIogFdmOMMSaCWGA3xiUi0lJE1nh2w5vul/6uiLSpxr0+9yw7e0mZY6s8O0F9JSIbPI/PBbvXAM/jNJXlmSIif6lKOYPc6yYRmVtB+jER2eL5+Xs17/+LmpfSmMhggd0Y91yLsyHGcOB+ABGZAnypqlVd7XAs8K2qDlTVfwc4PltV+wN/BX5fyb0GAJUGdlXdqKp3VrGc1fGWqg7w/NxQzXtUObB7NjMyJuJYYDfGPYVAA6A+UOIJJHcTJPCKSAcRWSEiX3t+t/e0wJ8CLve0ahsEyfMzPBtGiEhDEXnJ04rfLCLTPCtZPQbM8txrlogMFZF1nnPWeVfBE5FRfvuYP+K51yoR+U5EfAFfRK4XkS8893vOuw2yiPxIRHaJyGqcJV5DJiL3e8r9tWdhJG/6EhHZJM5e4Ld50p7E2XVwi4i8JiIdRWSr3zX3icgjnterROQ3njLdJSKDRWS1557L/JZFvVNEtnvyf7MqZTemrtk3VmPc87rn5wbg58B/AX9X1Zwg18z1nLNARG4G/qKq00XkV0CKqt5eSZ6X4ay6CPAQzlKqN4tIU5ylk5cDpe4lnq1jVbVIRMYBvwFmBLh3D2A00BjYKSLPAl1wViMcoaqFIvJXYLaIfAw8CgwGTgGf4KxWGMgsz4pm4OxN/z3OUs9DcTbEeE9ELvVsz3yzqp7wfLnZICKLVPUBEbldVQd4Pk/HSuqoqaqOFGfN89XANFU9JiKzgCdwVqV8AOikqvmeujPmnGGB3RiXqOopnP3Avbvj/Ry4UkSeBy4A/qCqn5W5bDhwpef1Kzgt9VC8Js52r9E4m8gATMDZCOU+z/s4nCU/y2oCLBCRrji7SMVWkMcHqpoP5IvIUSARZ4hgME6QBaeH4igwDFilqsc8n/8toFsF933L/wuLiDztKbv3i0AjnED/KXCniPzQk97Ok55RwX0r8pbnd3egD/Cxp+zROFsSA3yNU6dL+M8XJWPOCRbYjakdv8JpDV4LbMJpyb+L0wIOJtQ1n2cDXwFPAv+L8+VAgBmqutP/RBEZVubax4FPVPWHntbuqgryyPd7XYzz/4cAC1T1wTJ5TK9C2csS4Leq+lyZe47C2WVwuKrmiMgqnC8rZRVRepix7Dln/PLZpqrDA9zjCuBSYCrwSxHprapFVfwcxtQJG2M3xmWelnAbVV0NxONsnqEEDkrrcHZzAidYrwk1H892ug/j7MLXE1gG3CGe5qiIDPScmo3Tne7VBKf7G+CmUPPzWAHMFJFWnjyaiUgHnE1BRolIc0+X91VVuOcy4GZx9vxGRNp67t8EOOkJ6j2Ai/yuKfTkA85GUK08edcHJleQz06gpYgM9+QTKyK9RSQKaKeqnwD/DTTF6TUw5pxggd0Y9z2BE3AB3sAJnuuBpwOceyfwIxH5GpgD3FWVjFQ1F/gDcB9OSzwW+Nozmexxz2mfAL28k+dwuvt/KyJrcbqjq5LfdpzP9pGnzB8DrVU1HXgEZzLfcuDLKtzzI5wejc9E5BucPeobA0uBGE8+j+PUodc8z+d8zfMF5zGcLxf/BL6tIJ8CYCbwOxH5CtgC/ACnDl715L0Z+KOqZoZafmPqmu3uZowxxkQQa7EbY4wxEcQCuzHGGBNBLLAbY4wxEcQCuzHGGBNBLLAbY4wxEcQCuzHGGBNBLLAbY4wxEcQCuzHGGBNB/j/hk7BauHYF5wAAAABJRU5ErkJggg==",
328 | "text/plain": [
329 | ""
330 | ]
331 | },
332 | "metadata": {
333 | "needs_background": "light"
334 | },
335 | "output_type": "display_data"
336 | },
337 | {
338 | "name": "stdout",
339 | "output_type": "stream",
340 | "text": [
341 | "Optimal Model Size: 8.05% of full model\n"
342 | ]
343 | }
344 | ],
345 | "source": [
346 | "import matplotlib.pyplot as plt\n",
347 | "\n",
348 | "percentage_vector = DetachMatrixModel._percentage_vector\n",
349 | "acc_curve = DetachMatrixModel._sfd_curve\n",
350 | "\n",
351 | "c = DetachMatrixModel.trade_off\n",
352 | "\n",
353 | "x=(percentage_vector) * 100\n",
354 | "y=(acc_curve/acc_curve[0]-1) * 100\n",
355 | "\n",
356 | "point_x = x[DetachMatrixModel._max_index]\n",
357 | "#point_y = y[DetachMatrixModel._max_index]\n",
358 | "\n",
359 | "plt.figure(figsize=(8,3.5))\n",
360 | "plt.axvline(x = point_x, color = 'r',label=f'Optimal Model (c={c})')\n",
361 | "plt.plot(x, y, label='SFD curve', linewidth=2.5, color='C7', alpha=1)\n",
362 | "#plt.scatter(point_x, point_y, s=50, marker='o', label=f'Optimal point (c={c})')\n",
363 | "\n",
364 | "plt.grid(True, linestyle='-', alpha=0.5)\n",
365 | "plt.xlim(102,-2)\n",
366 | "plt.xlabel('% of Retained Features')\n",
367 | "plt.ylabel('Relative Validation Set Accuracy (%)')\n",
368 | "plt.legend()\n",
369 | "plt.show()\n",
370 | "\n",
371 | "print('Optimal Model Size: {:.2f}% of full model'.format(point_x))"
372 | ]
373 | },
374 | {
375 | "cell_type": "code",
376 | "execution_count": null,
377 | "metadata": {},
378 | "outputs": [],
379 | "source": []
380 | }
381 | ],
382 | "metadata": {
383 | "colab": {
384 | "provenance": []
385 | },
386 | "kernelspec": {
387 | "display_name": "Python 3",
388 | "name": "python3"
389 | },
390 | "language_info": {
391 | "codemirror_mode": {
392 | "name": "ipython",
393 | "version": 3
394 | },
395 | "file_extension": ".py",
396 | "mimetype": "text/x-python",
397 | "name": "python",
398 | "nbconvert_exporter": "python",
399 | "pygments_lexer": "ipython3",
400 | "version": "3.8.13"
401 | }
402 | },
403 | "nbformat": 4,
404 | "nbformat_minor": 0
405 | }
406 |
--------------------------------------------------------------------------------
/examples/Detach_ROCKET_example_UCR.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {
6 | "id": "EaTES2tMkBAR"
7 | },
8 | "source": [
9 | "## Install Required Libraries"
10 | ]
11 | },
12 | {
13 | "cell_type": "code",
14 | "execution_count": 2,
15 | "metadata": {
16 | "colab": {
17 | "base_uri": "https://localhost:8080/"
18 | },
19 | "id": "fMLwcJUJ5ynP",
20 | "outputId": "f44eda72-1cf6-4ac6-dfa2-59ab001370c6"
21 | },
22 | "outputs": [],
23 | "source": [
24 | "!pip install numpy scikit-learn pyts torch matplotlib sktime==0.30.0 --quiet\n",
25 | "!pip install git+https://github.com/gon-uri/detach_rocket --quiet"
26 | ]
27 | },
28 | {
29 | "cell_type": "markdown",
30 | "metadata": {
31 | "id": "v4WN7u9ql-0h"
32 | },
33 | "source": [
34 | "## Download Dataset from UCR"
35 | ]
36 | },
37 | {
38 | "cell_type": "code",
39 | "execution_count": 3,
40 | "metadata": {
41 | "id": "ruXkm-gumddl"
42 | },
43 | "outputs": [],
44 | "source": [
45 | "from detach_rocket.utils_datasets import fetch_ucr_dataset\n",
46 | "\n",
47 | "# Download Dataset\n",
48 | "dataset_name_list = ['FordB'] # PhalangesOutlinesCorrect ProximalPhalanxOutlineCorrect #Fordb\n",
49 | "current_dataset = fetch_ucr_dataset(dataset_name_list[0])"
50 | ]
51 | },
52 | {
53 | "cell_type": "markdown",
54 | "metadata": {
55 | "id": "dgzA1-1tci-q"
56 | },
57 | "source": [
58 | "## Prepare Dataset Matrices"
59 | ]
60 | },
61 | {
62 | "cell_type": "code",
63 | "execution_count": 5,
64 | "metadata": {
65 | "colab": {
66 | "base_uri": "https://localhost:8080/"
67 | },
68 | "id": "NmzM3E_OxaU1",
69 | "outputId": "f1179dc7-5864-4db1-d9c1-0519f323dd7d"
70 | },
71 | "outputs": [
72 | {
73 | "name": "stdout",
74 | "output_type": "stream",
75 | "text": [
76 | "Dataset Matrix Shape: ( # of instances , time series length )\n",
77 | " \n",
78 | "Train: (3636, 500)\n",
79 | " \n",
80 | "Test: (810, 500)\n"
81 | ]
82 | }
83 | ],
84 | "source": [
85 | "import numpy as np\n",
86 | "\n",
87 | "# Create data matrices and remove possible rows with nans\n",
88 | "\n",
89 | "print(f\"Dataset Matrix Shape: ( # of instances , time series length )\")\n",
90 | "print(f\" \")\n",
91 | "\n",
92 | "# Train Matrix\n",
93 | "X_train = current_dataset['data_train']\n",
94 | "print(f\"Train: {X_train.shape}\")\n",
95 | "non_nan_mask_train = ~np.isnan(X_train).any(axis=1)\n",
96 | "non_inf_mask_train = ~np.isinf(X_train).any(axis=1)\n",
97 | "mask_train = np.logical_and(non_nan_mask_train,non_inf_mask_train)\n",
98 | "X_train = X_train[mask_train]\n",
99 | "X_train = X_train.reshape(X_train.shape[0],1,X_train.shape[1])\n",
100 | "y_train = current_dataset['target_train']\n",
101 | "y_train = y_train[mask_train]\n",
102 | "\n",
103 | "print(f\" \")\n",
104 | "\n",
105 | "# Test Matrix\n",
106 | "X_test = current_dataset['data_test']\n",
107 | "#print(f\"Number of test instances: {len(X_test)}\")\n",
108 | "print(f\"Test: {X_test.shape}\")\n",
109 | "non_nan_mask_test = ~np.isnan(X_test).any(axis=1)\n",
110 | "non_inf_mask_test = ~np.isinf(X_test).any(axis=1)\n",
111 | "mask_test = np.logical_and(non_nan_mask_test,non_inf_mask_test)\n",
112 | "X_test = X_test[mask_test]\n",
113 | "X_test = X_test.reshape(X_test.shape[0],1,X_test.shape[1])\n",
114 | "y_test = current_dataset['target_test']\n",
115 | "y_test = y_test[mask_test]"
116 | ]
117 | },
118 | {
119 | "cell_type": "markdown",
120 | "metadata": {
121 | "id": "IkgnHeNtmFec"
122 | },
123 | "source": [
124 | "## Train and Evaluate the Model"
125 | ]
126 | },
127 | {
128 | "cell_type": "code",
129 | "execution_count": 6,
130 | "metadata": {
131 | "colab": {
132 | "background_save": true,
133 | "base_uri": "https://localhost:8080/"
134 | },
135 | "id": "o5uOBpflHKjJ",
136 | "outputId": "f28e5bb3-ed41-4fc8-e42f-4496de73274f"
137 | },
138 | "outputs": [
139 | {
140 | "name": "stderr",
141 | "output_type": "stream",
142 | "text": [
143 | "OMP: Info #271: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.\n"
144 | ]
145 | },
146 | {
147 | "name": "stdout",
148 | "output_type": "stream",
149 | "text": [
150 | "TRAINING RESULTS Full ROCKET:\n",
151 | "Optimal Alpha Full ROCKET: 4832.93\n",
152 | "Train Accuraccy Full ROCKET: 99.81%\n",
153 | "-------------------------\n",
154 | "TRAINING RESULTS Detach Model:\n",
155 | "Optimal Alpha Detach Model: 3.36\n",
156 | "Train Accuraccy Detach Model: 95.24%\n",
157 | "-------------------------\n",
158 | "Test Accuraccy Full Model: 81.11%\n",
159 | "Test Accuraccy Detach-ROCKET: 81.73%\n"
160 | ]
161 | }
162 | ],
163 | "source": [
164 | "from detach_rocket.detach_classes import DetachRocket\n",
165 | "\n",
166 | "np.random.seed(2)\n",
167 | "\n",
168 | "# Select initial model characteristics\n",
169 | "model_type = \"rocket\"\n",
170 | "num_kernels = 10000\n",
171 | "\n",
172 | "# Create model object\n",
173 | "DetachRocketModel = DetachRocket(model_type, num_kernels=num_kernels)\n",
174 | "\n",
175 | "# Trian Model\n",
176 | "DetachRocketModel.fit(X_train,y_train)\n",
177 | "\n",
178 | "# Evaluate Performance on Test Set\n",
179 | "detach_test_score, full_test_score= DetachRocketModel.score(X_test,y_test)\n",
180 | "print('Test Accuraccy Full Model: {:.2f}%'.format(100*full_test_score))\n",
181 | "print('Test Accuraccy Detach-ROCKET: {:.2f}%'.format(100*detach_test_score))"
182 | ]
183 | },
184 | {
185 | "cell_type": "markdown",
186 | "metadata": {
187 | "id": "lwFoPnnFmN5V"
188 | },
189 | "source": [
190 | "## Plot SFD Curve and Optimal Model Selection"
191 | ]
192 | },
193 | {
194 | "cell_type": "code",
195 | "execution_count": 8,
196 | "metadata": {
197 | "colab": {
198 | "background_save": true
199 | },
200 | "id": "zmyz2GeojYwl",
201 | "outputId": "34ff3981-faa0-49fe-ec8b-58f996ac9c54"
202 | },
203 | "outputs": [
204 | {
205 | "data": {
206 | "image/png": "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6ioyMzMjOnAtlPz8fBYtWuTO+b700ku56aabQqbHramioiKWLl3KqlWrajTNJy4ujnHjxpGRkVHpMqbR4E0TPGfOHEpKSliyZInfXPiGZO/evW5rSufOnenevXu1z5WcnExSUhLnzp0jJycnogHAmTNn3KfYPn36lHtq9aYn/utf/8r58+dZuXJllc6fmprqtyAU+LcAeGeX1NS6devcYCtwmmW/fv14//33KSoqIjExkZ49ewJOve7bt49Dhw5RVlYWdqA+b968ctMjL7/8cnbt2uW3PVItADExMcTFxfk13/smEIK67wII5zt9CJgnIt7cjJ2BaRG4drC/VIHffUX7hHOscwKR+3G6D0hJSXETPwSKZnpLUzWqytq1a9mwYQN9+vShX79+UWvOvnDhAmvXri237OnevXt59tln6d27N+np6RG//oEDB1i5cmWNU7e2b9+eESNG0Lp164hlR6uubt26sXv3brZs2cKWLVvK9XfWd4cPH+b99993/9j27du3xnWanJzMrl27yMnJYdeuXRFrVdq8ebN7s+jYsWOFf9eGDRvGsmXLqrSEs/fpP9g5mzRpQmFhIXv27KFr165VKnPgoLvS0lI3z0KrVq2Ii4srd8309HSys7MZMGAABw4cAL68QRcXF/P555/TqlWrSq9dVlbmDiT0atmyJUlJSeX+T06dOlVhfVZVYABw2WWXsXXrVk6dOgU4gZTvtSoamOj93itS3fJWGgCo6moR6QlciXPj3QZEon1vP3CJz/uuQGAC6Ir2SQjjWABUdSYwEyAjI0NTU1ODFubuu++mrKyMvXv3uqNoTWRUpU5PnDjB0qVL+eKLLygtLWXjxo3k5OSQmZnJoEGD/FZhq4mSkhI+++wzVqxY4TbDxcTEcPXVVwO4T+WbN29m7969jBs3jj59+tT4Cfvs2bMsXrzYL3lL165duemmm/yaWMOxd+/ecv20dSkmJsbtyigoKIhIzoHasn//fpYsWeL+sZ4yZUqV0yQHc+rUKXbt2kVxcTExMTFU9PenqrwZGNu0acPgwYMr/LlMTU1l6NChVXqCjIuLq/ApuH379hw4cIDi4uJqfS++x6xfv97t37/++uvLdTl4958wYUK58nlbNGJjY8Mqx9GjR92AacKECfTv35+EhARiYmI4d+6c250CTtAWqf+npk2busmNkpKS6N27Nz17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",
207 | "text/plain": [
208 | ""
209 | ]
210 | },
211 | "metadata": {
212 | "needs_background": "light"
213 | },
214 | "output_type": "display_data"
215 | },
216 | {
217 | "name": "stdout",
218 | "output_type": "stream",
219 | "text": [
220 | "Optimal Model Size: 0.20% of full model\n"
221 | ]
222 | }
223 | ],
224 | "source": [
225 | "import matplotlib.pyplot as plt\n",
226 | "\n",
227 | "percentage_vector = DetachRocketModel._percentage_vector\n",
228 | "acc_curve = DetachRocketModel._sfd_curve\n",
229 | "\n",
230 | "c = DetachRocketModel.trade_off\n",
231 | "\n",
232 | "x=(percentage_vector) * 100\n",
233 | "y=(acc_curve/acc_curve[0]-1) * 100\n",
234 | "\n",
235 | "point_x = x[DetachRocketModel._max_index]\n",
236 | "#point_y = y[DetachRocketModel._max_index]\n",
237 | "\n",
238 | "plt.figure(figsize=(8,3.5))\n",
239 | "plt.axvline(x = point_x, color = 'r',label=f'Optimal Model (c={c})')\n",
240 | "plt.plot(x, y, label='SFD curve', linewidth=2.5, color='C7', alpha=1)\n",
241 | "#plt.scatter(point_x, point_y, s=50, marker='o', label=f'Optimal point (c={c})')\n",
242 | "\n",
243 | "plt.grid(True, linestyle='-', alpha=0.5)\n",
244 | "plt.xlim(102,-2)\n",
245 | "plt.xlabel('% of Retained Features')\n",
246 | "plt.ylabel('Relative Validation Set Accuracy (%)')\n",
247 | "plt.legend()\n",
248 | "plt.show()\n",
249 | "\n",
250 | "print('Optimal Model Size: {:.2f}% of full model'.format(point_x))"
251 | ]
252 | },
253 | {
254 | "cell_type": "code",
255 | "execution_count": null,
256 | "metadata": {
257 | "id": "mfGs8ajkMyNl"
258 | },
259 | "outputs": [],
260 | "source": []
261 | }
262 | ],
263 | "metadata": {
264 | "colab": {
265 | "provenance": []
266 | },
267 | "kernelspec": {
268 | "display_name": "Python 3",
269 | "name": "python3"
270 | },
271 | "language_info": {
272 | "codemirror_mode": {
273 | "name": "ipython",
274 | "version": 3
275 | },
276 | "file_extension": ".py",
277 | "mimetype": "text/x-python",
278 | "name": "python",
279 | "nbconvert_exporter": "python",
280 | "pygments_lexer": "ipython3",
281 | "version": "3.8.13"
282 | }
283 | },
284 | "nbformat": 4,
285 | "nbformat_minor": 0
286 | }
287 |
--------------------------------------------------------------------------------
/detach_rocket/detach_classes.py:
--------------------------------------------------------------------------------
1 | """
2 | DetachRocket end-to-end model class, DetachMatrix class and DetachEnsemble class.
3 | """
4 |
5 | from detach_rocket.utils import (feature_detachment, select_optimal_model, retrain_optimal_model)
6 |
7 | from sklearn.linear_model import (RidgeClassifierCV ,RidgeClassifier)
8 | from sklearn.preprocessing import StandardScaler, LabelEncoder
9 | from sktime.transformations.panel.rocket import (
10 | Rocket,
11 | MiniRocketMultivariate,
12 | MultiRocketMultivariate
13 | )
14 | from sklearn.model_selection import train_test_split
15 | import numpy as np
16 | import torch
17 |
18 | class DetachRocket:
19 |
20 | """
21 | Rocket model with feature detachment.
22 | For univariate time series, the shape of `X_train` should be (n_instances, n_timepoints).
23 | For multivariate time series, the shape of `X_train` should be (n_instances, n_variables, n_timepoints).
24 |
25 | Parameters:
26 | - model_type: Type of the rocket model ("rocket", "minirocket", "multirocket", or "pytorch_minirocket").
27 | - num_kernels: Number of kernels for the rocket model.
28 | - trade_off: Trade-off parameter to set optimal pruning.
29 | - recompute_alpha: Whether to recompute alpha for optimal model training.
30 | - val_ratio: Validation set ratio.
31 | - verbose: Verbosity for logging.
32 | - multilabel_type: Type of feature ranking in case of multilabel classification ("max" by default).
33 | - fixed_percentage: If not None, the trade_off parameter is ignored and the model is fitted with a fixed percentage of selected features.
34 |
35 | Attributes:
36 | - _sfd_curve: Curve for Sequential Feature Detachment.
37 | - _full_transformer: Full transformer for rocket model.
38 | - _full_classifier: Full classifier for baseline.
39 | - _full_model_alpha: Alpha for full model.
40 | - _classifier: Classifier for optimal model.
41 | - _feature_matrix: Feature matrix.
42 | - _feature_importance_matrix: Matrix for feature importance. Zero values indicate pruned features. Dimension: [Number of steps, Number of features].
43 | - _percentage_vector: Vector of percentage values.
44 | - _scaler: Scaler for feature matrix.
45 | - _labels: Labels.
46 | - _acc_train: Training accuracy.
47 | - _max_index: Index for maximum percentage.
48 | - _max_percentage: Maximum percentage.
49 | - _is_fitted: Flag indicating if the model is fitted.
50 | - _optimal_computed: Flag indicating if optimal model is computed.
51 |
52 | Methods:
53 | - fit: Fit the DetachRocket model.
54 | - fit_trade_off: Fit the model with a given trade-off.
55 | - fit_fixed_percentage: Fit the model with a fixed percentage of features.
56 | - predict: Make predictions using the fitted model.
57 | - score: Get the accuracy score of the model.
58 |
59 | """
60 |
61 |
62 | def __init__(
63 | self,
64 | model_type='rocket',
65 | num_kernels=10000,
66 | trade_off=0.1,
67 | recompute_alpha = True,
68 | val_ratio=0.33,
69 | verbose = False,
70 | multilabel_type = 'max',
71 | fixed_percentage = None
72 | ):
73 |
74 | self._sfd_curve = None
75 | #self._transformer = None
76 | self._full_transformer = None
77 | self._full_classifier = None
78 | self._full_model_alpha = None
79 | self._classifier = None
80 | self._feature_matrix = None
81 | self._feature_matrix_val = None
82 | self._feature_importance_matrix = None
83 | self._percentage_vector = None
84 | self._scaler = None
85 | self._labels = None
86 | self._acc_train = None
87 | self._max_index = None
88 | self._max_percentage = None
89 | self._is_fitted = False
90 | self._optimal_computed = False
91 |
92 | self.num_kernels = num_kernels
93 | self.trade_off = trade_off
94 | self.val_ratio = val_ratio
95 | self.recompute_alpha = recompute_alpha
96 | self.verbose = verbose
97 | self.multilabel_type = multilabel_type
98 | self.fixed_percentage = fixed_percentage
99 |
100 | # Create rocket model
101 | if model_type == "rocket":
102 | self._full_transformer = Rocket(num_kernels=num_kernels)
103 | elif model_type == "minirocket":
104 | self._full_transformer = MiniRocketMultivariate(num_kernels=num_kernels)
105 | elif model_type == "multirocket":
106 | self._full_transformer = MultiRocketMultivariate(num_kernels=num_kernels)
107 | elif model_type == "pytorch_minirocket":
108 | self._full_transformer = PytorchMiniRocketMultivariate(num_features=num_kernels)
109 | else:
110 | raise ValueError('Invalid model_type argument. Choose from: "rocket", "minirocket", "multirocket" or "pytorch_minirocket".')
111 |
112 | self._full_classifier = RidgeClassifierCV(alphas=np.logspace(-10,10,20))
113 | self._scaler = StandardScaler(with_mean=True)
114 |
115 | return
116 |
117 | def fit(self, X, y=None, val_set=None, val_set_y=None, X_test=None, y_test=None):
118 |
119 | assert y is not None, "Labels are required to fit Detach Rocket"
120 |
121 | if self.fixed_percentage is not None:
122 | # If fixed percentage is provided, no validation set is required
123 | # Assert there is no validation set
124 | assert val_set is None, "Validation set is not allowed when using fixed percentage of features, since it is not required for training"
125 | # Assert that both X_test set and y_test labels are provided
126 | assert X_test is not None, "X_test is required to fit Detach Rocket with fixed percentage. It is not used for training, but for plotting the feature detachment curve."
127 | assert y_test is not None, "y_test is required to fit Detach Rocket with fixed percentage. . It is not used for training, but for plotting the feature detachment curve."
128 |
129 | if self.verbose == True:
130 | print('Applying Data Transformation')
131 |
132 | self._feature_matrix = self._full_transformer.fit_transform(X)
133 | self._labels = y
134 |
135 | if self.verbose == True:
136 | print('Fitting Full Model')
137 |
138 | # scale feature matrix
139 | self._feature_matrix = self._scaler.fit_transform(self._feature_matrix)
140 |
141 | if val_set is not None:
142 | self._feature_matrix_val = self._full_transformer.transform(val_set)
143 | self._feature_matrix_val = self._scaler.transform(self._feature_matrix_val)
144 |
145 | # Train full rocket as baseline
146 | self._full_classifier.fit(self._feature_matrix, y)
147 | self._full_model_alpha = self._full_classifier.alpha_
148 |
149 | print('TRAINING RESULTS Full ROCKET:')
150 | print('Optimal Alpha Full ROCKET: {:.2f}'.format(self._full_model_alpha))
151 | print('Train Accuraccy Full ROCKET: {:.2f}%'.format(100*self._full_classifier.score(self._feature_matrix, y)))
152 | print('-------------------------')
153 |
154 | # If fixed percentage is not provided, we set the number of features using the validation set
155 | if self.fixed_percentage is None:
156 |
157 | # Assert no test set is provided
158 | assert X_test is None, "X_test is not allowed when using trade-off, SFD curves are computed with a validation set."
159 |
160 | if val_set is not None:
161 | X_train = self._feature_matrix
162 | X_val = self._feature_matrix_val
163 | y_train = y
164 | y_val = val_set_y
165 | else:
166 | # Train-Validation split
167 | X_train, X_val, y_train, y_val = train_test_split(self._feature_matrix,
168 | y,
169 | test_size=self.val_ratio,
170 | random_state=42,
171 | stratify=y)
172 |
173 | # Train model for selected features
174 | sfd_classifier = RidgeClassifier(alpha=self._full_model_alpha)
175 | sfd_classifier.fit(X_train, y_train)
176 |
177 | # Feature Detachment
178 | if self.verbose == True:
179 | print('Applying Sequential Feature Detachment')
180 |
181 | self._percentage_vector, _, self._sfd_curve, self._feature_importance_matrix = feature_detachment(sfd_classifier, X_train, X_val, y_train, y_val, verbose=self.verbose, multilabel_type = self.multilabel_type)
182 |
183 | self._is_fitted = True
184 |
185 | # Training Optimal Model
186 | if self.verbose == True:
187 | print('Training Optimal Model')
188 |
189 | self.fit_trade_off(self.trade_off)
190 |
191 | else:
192 | # If fixed percentage is provided, no validation set is required
193 | # We don't need to split the data into train and validation
194 | # We are using a fixed percentage of features
195 | X_train = self._feature_matrix
196 | y_train = y
197 | X_test = self._scaler.transform(self._full_transformer.transform(X_test))
198 |
199 | # Train model for selected features
200 | sfd_classifier = RidgeClassifier(alpha=self._full_model_alpha)
201 | sfd_classifier.fit(X_train, y_train)
202 |
203 | # Feature Detachment
204 | if self.verbose == True:
205 | print('Applying Sequential Feature Detachment')
206 |
207 | self._percentage_vector, _, self._sfd_curve, self._feature_importance_matrix = feature_detachment(sfd_classifier, X_train, X_test, y_train, y_test, verbose=self.verbose, multilabel_type = self.multilabel_type)
208 |
209 | self._is_fitted = True
210 |
211 | if self.verbose == True:
212 | print('Using fixed percentage of features')
213 | self.fit_fixed_percentage(self.fixed_percentage)
214 |
215 | return
216 |
217 | def fit_trade_off(self,trade_off=None):
218 |
219 | assert trade_off is not None, "Missing argument"
220 | assert self._is_fitted == True, "Model not fitted. Call fit method first."
221 |
222 | # Select optimal
223 | max_index, max_percentage = select_optimal_model(self._percentage_vector, self._sfd_curve, self._sfd_curve[0], self.trade_off, graphics=False)
224 | self._max_index = max_index
225 | self._max_percentage = max_percentage
226 |
227 | # Check if alpha will be recomputed
228 | if self.recompute_alpha:
229 | alpha_optimal = None
230 | else:
231 | alpha_optimal = self._full_model_alpha
232 |
233 | # Create feature mask
234 | self._feature_mask = self._feature_importance_matrix[max_index]>0
235 |
236 | # Re-train optimal model
237 | self._classifier, self._acc_train = retrain_optimal_model(self._feature_mask,
238 | self._feature_matrix,
239 | self._labels,
240 | self._max_index,
241 | alpha_optimal,
242 | verbose = self.verbose)
243 |
244 | return
245 |
246 | def fit_fixed_percentage(self, fixed_percentage=None, graphics=True):
247 |
248 | assert fixed_percentage is not None, "Missing argument"
249 | assert self._is_fitted == True, "Model not fitted. Call fit method first."
250 |
251 | self._max_index = (np.abs(self._percentage_vector - self.fixed_percentage)).argmin()
252 | self._max_percentage = self._percentage_vector[self._max_index]
253 |
254 | # Check if alpha will be recomputed
255 | if self.recompute_alpha:
256 | alpha_optimal = None
257 | else:
258 | alpha_optimal = self._full_model_alpha
259 |
260 | # Create feature mask
261 | self._feature_mask = self._feature_importance_matrix[self._max_index]>0
262 |
263 | # Re-train optimal model
264 | self._classifier, self._acc_train = retrain_optimal_model(self._feature_mask,
265 | self._feature_matrix,
266 | self._labels,
267 | self._max_index,
268 | alpha_optimal,
269 | verbose = self.verbose)
270 |
271 | return
272 |
273 | def predict(self,X):
274 |
275 | assert self._is_fitted == True, "Model not fitted. Call fit method first."
276 |
277 | # Transform time series to feature matrix
278 | transformed_X = np.asarray(self._full_transformer.transform(X))
279 | transformed_X = self._scaler.transform(transformed_X)
280 | masked_transformed_X = transformed_X[:,self._feature_mask]
281 |
282 | y_pred = self._classifier.predict(masked_transformed_X)
283 |
284 | return y_pred
285 |
286 | def score(self, X, y):
287 |
288 | assert self._is_fitted == True, "Model not fitted. Call fit method first."
289 |
290 | # Transform time series to feature matrix
291 | transformed_X = np.asarray(self._full_transformer.transform(X))
292 | transformed_X = self._scaler.transform(transformed_X)
293 | masked_transformed_X = transformed_X[:,self._feature_mask]
294 |
295 | return self._classifier.score(masked_transformed_X, y), self._full_classifier.score(transformed_X, y)
296 |
297 |
298 | class DetachMatrix:
299 | """
300 | A class for pruning a feature matrix using feature detachment.
301 | The shape of the input matrix should be (n_instances, n_features).
302 |
303 | Parameters:
304 | - trade_off: Trade-off parameter.
305 | - recompute_alpha: Whether to recompute alpha for optimal model training.
306 | - val_ratio: Validation set ratio.
307 | - verbose: Verbosity for logging.
308 | - multilabel_type: Type of multilabel classification ("max" by default).
309 |
310 | Attributes:
311 | - _sfd_curve: Curve for Sequential Feature Detachment.
312 | - _scaler: Scaler for feature matrix.
313 | - _classifier: Classifier for optimal model.
314 | - _acc_train: Training accuracy.
315 | - _full_classifier: Full classifier for baseline.
316 | - _percentage_vector: Vector for percentage values.
317 | - _feature_matrix: Feature matrix.
318 | - _labels: Labels.
319 | - _feature_importance_matrix: Matrix for feature importance.
320 | - _full_model_alpha: Alpha for full model.
321 | - _max_index: Index for maximum percentage.
322 | - _max_percentage: Maximum percentage.
323 | - _is_fitted: Flag indicating if the model is fitted.
324 | - _optimal_computed: Flag indicating if optimal model is computed.
325 | - trade_off: Trade-off parameter.
326 | - val_ratio: Validation set ratio.
327 | - recompute_alpha: Whether to recompute alpha for optimal model training.
328 | - verbose: Verbosity for logging.
329 | - multilabel_type: Type of feature ranking in case of multilabel classification ("max" by default).
330 |
331 | Methods:
332 | - fit: Fit the DetachMatrix model.
333 | - fit_trade_off: Fit the model with a given trade-off.
334 | - predict: Make predictions using the fitted model.
335 | - score: Get the accuracy score of the model.
336 |
337 | """
338 |
339 | def __init__(
340 | self,
341 | trade_off=0.1,
342 | recompute_alpha = True,
343 | val_ratio=0.33,
344 | verbose = False,
345 | multilabel_type = 'max',
346 | fixed_percentage = None
347 | ):
348 |
349 | self._sfd_curve = None
350 | self._scaler = None
351 | self._classifier = None
352 | self._acc_train = None
353 | self._full_classifier = None
354 | self._percentage_vector = None
355 | self._feature_matrix = None
356 | self._labels = None
357 | self._feature_importance_matrix = None
358 | self._full_model_alpha = None
359 | self._max_index = None
360 | self._max_percentage = None
361 | self._is_fitted = False
362 | self._optimal_computed = False
363 |
364 |
365 | self.trade_off = trade_off
366 | self.val_ratio = val_ratio
367 | self.recompute_alpha = recompute_alpha
368 | self.verbose = verbose
369 | self.multilabel_type = multilabel_type
370 | self.fixed_percentage = fixed_percentage
371 |
372 | self._full_classifier = RidgeClassifierCV(alphas=np.logspace(-10,10,20))
373 | self._scaler = StandardScaler(with_mean=True)
374 |
375 | return
376 |
377 | def fit(self, X, y=None, val_set=None, val_set_y=None, X_test=None, y_test=None):
378 |
379 | assert y is not None, "Labels are required to fit Detach Matrix"
380 |
381 | self._feature_matrix = X
382 | self._labels = y
383 |
384 | if val_set is not None:
385 | self._feature_matrix_val = val_set
386 |
387 | if self.verbose == True:
388 | print('Fitting Full Model')
389 |
390 | # scale feature matrix
391 | self._feature_matrix = self._scaler.fit_transform(self._feature_matrix)
392 |
393 |
394 | # Train full rocket as baseline
395 | self._full_classifier.fit(self._feature_matrix, y)
396 | self._full_model_alpha = self._full_classifier.alpha_
397 |
398 | print('TRAINING RESULTS Full Features:')
399 | print('Optimal Alpha Full Features: {:.2f}'.format(self._full_model_alpha))
400 | print('Train Accuraccy Full Features: {:.2f}%'.format(100*self._full_classifier.score(self._feature_matrix, y)))
401 | print('-------------------------')
402 |
403 |
404 | # If fixed percentage is not provided, we set the number of features using the validation set
405 | if self.fixed_percentage is None:
406 |
407 | # Assert no test set is provided
408 | assert X_test is None, "X_test is not allowed when using trade-off, SFD curves are computed with a validation set."
409 |
410 | if val_set is not None:
411 | X_train = self._feature_matrix
412 | X_val = self._feature_matrix_val
413 | y_train = y
414 | y_val = val_set_y
415 | else:
416 | # Train-Validation split
417 | X_train, X_val, y_train, y_val = train_test_split(self._feature_matrix,
418 | y,
419 | test_size=self.val_ratio,
420 | random_state=42,
421 | stratify=y)
422 |
423 | # Train model for selected features
424 | sfd_classifier = RidgeClassifier(alpha=self._full_model_alpha)
425 | sfd_classifier.fit(X_train, y_train)
426 |
427 | # Feature Detachment
428 | if self.verbose == True:
429 | print('Applying Sequential Feature Detachment')
430 |
431 | self._percentage_vector, _, self._sfd_curve, self._feature_importance_matrix = feature_detachment(sfd_classifier, X_train, X_val, y_train, y_val, verbose=self.verbose, multilabel_type = self.multilabel_type)
432 |
433 | self._is_fitted = True
434 |
435 | # Training Optimal Model
436 | if self.verbose == True:
437 | print('Training Optimal Model')
438 |
439 | self.fit_trade_off(self.trade_off)
440 |
441 | # If fixed percentage is provided, no validation set is required
442 | else:
443 | # Assert there is no validation set
444 | assert val_set is None, "Validation set is not allowed when using fixed percentage of features, since it is not required for training"
445 | # Assert that both X_test set and y_test labels are provided
446 | assert X_test is not None, "X_test is required to fit Detach Matrix with fixed percentage. It is not used for training, but for plotting the feature detachment curve."
447 | assert y_test is not None, "y_test is required to fit Detach Matrix with fixed percentage. . It is not used for training, but for plotting the feature detachment curve."
448 |
449 | # We don't need to split the data into train and validation
450 | # We are using a fixed percentage of features
451 | X_train = self._feature_matrix
452 | y_train = y
453 | X_test = self._scaler.transform(X_test)
454 |
455 | # Train model for selected features
456 | sfd_classifier = RidgeClassifier(alpha=self._full_model_alpha)
457 | sfd_classifier.fit(X_train, y_train)
458 |
459 | # Feature Detachment
460 | if self.verbose == True:
461 | print('Applying Sequential Feature Detachment')
462 |
463 | self._percentage_vector, _, self._sfd_curve, self._feature_importance_matrix = feature_detachment(sfd_classifier, X_train, X_test, y_train, y_test, verbose=self.verbose, multilabel_type = self.multilabel_type)
464 |
465 | self._is_fitted = True
466 |
467 | if self.verbose == True:
468 | print('Using fixed percentage of features')
469 | self.fit_fixed_percentage(self.fixed_percentage)
470 |
471 | return
472 |
473 | def fit_trade_off(self,trade_off=None):
474 |
475 | assert trade_off is not None, "Missing argument"
476 | assert self._is_fitted == True, "Model not fitted. Call fit method first."
477 |
478 | # Select optimal
479 | max_index, max_percentage = select_optimal_model(self._percentage_vector, self._sfd_curve, self._sfd_curve[0], self.trade_off, graphics=False)
480 | self._max_index = max_index
481 | self._max_percentage = max_percentage
482 |
483 | # Check if alpha will be recomputed
484 | if self.recompute_alpha:
485 | alpha_optimal = None
486 | else:
487 | alpha_optimal = self._full_model_alpha
488 |
489 | # Create feature mask
490 | self._feature_mask = self._feature_importance_matrix[max_index]>0
491 |
492 | # Re-train optimal model
493 | self._classifier, self._acc_train = retrain_optimal_model(self._feature_mask,
494 | self._feature_matrix,
495 | self._labels,
496 | max_index,
497 | alpha_optimal,
498 | verbose = self.verbose)
499 |
500 | return
501 |
502 | def fit_fixed_percentage(self, fixed_percentage=None, graphics=True):
503 |
504 | assert fixed_percentage is not None, "Missing argument"
505 | assert self._is_fitted == True, "Model not fitted. Call fit method first."
506 |
507 | self._max_index = (np.abs(self._percentage_vector - self.fixed_percentage)).argmin()
508 | self._max_percentage = self._percentage_vector[self._max_index]
509 |
510 | # Check if alpha will be recomputed
511 | if self.recompute_alpha:
512 | alpha_optimal = None
513 | else:
514 | alpha_optimal = self._full_model_alpha
515 |
516 | # Create feature mask
517 | self._feature_mask = self._feature_importance_matrix[self._max_index]>0
518 |
519 | # Re-train optimal model
520 | self._classifier, self._acc_train = retrain_optimal_model(self._feature_mask,
521 | self._feature_matrix,
522 | self._labels,
523 | self._max_index,
524 | alpha_optimal,
525 | verbose = self.verbose)
526 |
527 | return
528 |
529 | def predict(self,X):
530 |
531 | assert self._is_fitted == True, "Model not fitted. Call fit method first."
532 |
533 | # Transform time series to feature matrix
534 | scaled_X = self._scaler.transform(X)
535 | masked_scaled_X = scaled_X[:,self._feature_mask]
536 |
537 | y_pred = self._classifier.predict(masked_scaled_X)
538 |
539 | return y_pred
540 |
541 | def score(self, X, y):
542 |
543 | assert self._is_fitted == True, "Model not fitted. Call fit method first."
544 |
545 | # Transform time series to feature matrix
546 | scaled_X = self._scaler.transform(X)
547 | masked_scaled_X = scaled_X[:,self._feature_mask]
548 |
549 |
550 | return self._classifier.score(masked_scaled_X, y), self._full_classifier.score(scaled_X, y)
551 |
552 |
553 |
554 | class PytorchMiniRocketMultivariate(torch.nn.Module):
555 | """This is a Pytorch implementation of MiniRocket developed by Malcolm McLean and Ignacio Oguiza
556 |
557 | MiniRocket paper citation:
558 | @article{dempster_etal_2020,
559 | author = {Dempster, Angus and Schmidt, Daniel F and Webb, Geoffrey I},
560 | title = {{MINIROCKET}: A Very Fast (Almost) Deterministic Transform for Time Series Classification},
561 | year = {2020},
562 | journal = {arXiv:2012.08791}
563 | }
564 | Original paper: https://arxiv.org/abs/2012.08791
565 | Original code: https://github.com/angus924/minirocket
566 |
567 | The class was edited by Adrià Solana for the Detach Rocket Ensemble study."""
568 |
569 | kernel_size, num_kernels, fitting = 9, 84, False
570 |
571 | def __init__(self, num_features=10_000, max_dilations_per_kernel=32, device=None):
572 | super().__init__()
573 | self.num_features = num_features
574 | self.max_dilations_per_kernel = max_dilations_per_kernel
575 | self.device = device if device else torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
576 |
577 | def fit(self, X, chunksize=128):
578 | self.c_in, self.seq_len = X.shape[1], X.shape[2]
579 | self.num_features = self.num_features // self.num_kernels * self.num_kernels
580 |
581 | # Define the convolutional kernels
582 | indices = torch.combinations(torch.arange(self.kernel_size), 3).unsqueeze(1)
583 | kernels = (-torch.ones(self.num_kernels, 1, self.kernel_size)).scatter_(2, indices, 2)
584 | self.kernels = torch.nn.Parameter(kernels.repeat(self.c_in, 1, 1), requires_grad=False)
585 |
586 | # Dilations & padding
587 | self._set_dilations(self.seq_len)
588 |
589 | # Channel combinations (multivariate)
590 | if self.c_in > 1:
591 | self._set_channel_combinations(self.c_in)
592 |
593 | # Define the biases for each dilation
594 | for i in range(self.num_dilations):
595 | self.register_buffer(f'biases_{i}', torch.empty((self.num_kernels, self.num_features_per_dilation[i])))
596 | self.register_buffer('prefit', torch.BoolTensor([False]))
597 |
598 | # Move the defined model to device before computation
599 | self.to(self.device)
600 |
601 | # Compute biases with the initial forward pass using a subset of samples
602 | num_samples = X.shape[0]
603 | if chunksize is None:
604 | chunksize = min(num_samples, self.num_dilations * self.num_kernels) # Deterministic
605 | else:
606 | chunksize = min(num_samples, chunksize) # Stochastic for chunksize < num_samples
607 | idxs = np.random.choice(num_samples, chunksize, False)
608 | self.fitting = True
609 | if isinstance(X, np.ndarray):
610 | self(torch.from_numpy(X[idxs]).float().to(self.device))
611 | else:
612 | self(X[idxs].to(self.device))
613 | self._set_parameter_indices()
614 | self.fitting = False
615 |
616 | return self
617 |
618 | def forward(self, x):
619 | _features = []
620 | for i, (dilation, padding) in enumerate(zip(self.dilations, self.padding)): # Max 32
621 | _padding1 = i%2
622 |
623 | # Convolution randomly combining channels for MTS
624 | C = torch.nn.functional.conv1d(x, self.kernels, padding=padding, dilation=dilation, groups=self.c_in)
625 | if self.c_in > 1:
626 | C = C.reshape(x.shape[0], self.c_in, self.num_kernels, -1)
627 | channel_combination = getattr(self, f'channel_combinations_{i}')
628 | C = torch.mul(C, channel_combination)
629 | C = C.sum(1)
630 |
631 | # Draw the biases (and compute them if the model is fitting)
632 | if not self.prefit or self.fitting:
633 | num_features_this_dilation = self.num_features_per_dilation[i]
634 | bias_this_dilation = self._get_bias(C, num_features_this_dilation)
635 | setattr(self, f'biases_{i}', bias_this_dilation)
636 | if self.fitting:
637 | if i < self.num_dilations - 1:
638 | continue
639 | else:
640 | self.prefit = torch.BoolTensor([True])
641 | return
642 | elif i == self.num_dilations - 1:
643 | self.prefit = torch.BoolTensor([True])
644 | else:
645 | bias_this_dilation = getattr(self, f'biases_{i}')
646 |
647 | # Pool into PPVs with alternating padding
648 | _features.append(self._get_PPVs(C[:, _padding1::2], bias_this_dilation[_padding1::2]))
649 | _features.append(self._get_PPVs(C[:, 1-_padding1::2, padding:-padding], bias_this_dilation[1-_padding1::2]))
650 | return torch.cat(_features, dim=1)
651 |
652 | def _set_parameter_indices(self):
653 | # Simulate a forward pass but keep the indices that match a kernel and bias with a feature
654 | for i, (dilation, padding) in enumerate(zip(self.dilations, self.padding)): # Max 32
655 | _padding1 = i%2
656 |
657 | # Indices for the kernels / channel combinations & biases
658 | bias_this_dilation = getattr(self, f'biases_{i}')
659 | num_kernels, num_quantiles = bias_this_dilation.shape
660 | bias_indices = torch.arange(num_kernels*num_quantiles, dtype=int).reshape(num_quantiles, num_kernels).transpose(1, 0)
661 |
662 | kernel_indices = torch.arange(num_kernels, dtype=int)
663 | kernel_indices = torch.stack([kernel_indices]*num_quantiles, dim=-1)
664 |
665 | # Simulated feature maps for the "even" kernels
666 | C_even = kernel_indices[_padding1::2] # (num kernels / 2, num quantiles)
667 | bias_this_dilation_even = bias_indices[_padding1::2] # (num kernels / 2, num quantiles)
668 |
669 | # Simulate the PPV reshape
670 | C_even = C_even.flatten() # replaces .mean(2).flatten(1) and removes placeholder dimensions
671 | bias_this_dilation_even = bias_this_dilation_even.flatten()
672 |
673 | # Do the same for "odd" kernels
674 | C_odd = kernel_indices[1-_padding1::2] # (num kernels / 2, num quantiles)
675 | bias_this_dilation_odd = bias_indices[1-_padding1::2] # (num kernels / 2, num quantiles)
676 |
677 | # Simulate the PPV reshape
678 | C_odd = C_odd.flatten() # replaces .mean(2).flatten(1) and removes placeholder dimensions
679 | bias_this_dilation_odd = bias_this_dilation_odd.flatten()
680 |
681 | # Stack into flat arrays
682 | C_full = torch.cat((C_even, C_odd))
683 | bias_this_dilation_full = torch.cat((bias_this_dilation_even, bias_this_dilation_odd))
684 |
685 | setattr(self, f'kernel_indices_{i}', C_full)
686 | setattr(self, f'bias_indices_{i}', bias_this_dilation_full)
687 |
688 | return
689 |
690 | def get_kernel_features(self, which, where):
691 | # Get the "which" kernel parameters at "where" certain indices
692 | full_features = np.empty(shape=(0,), dtype=float)
693 |
694 | if which == 'channels':
695 | full_features = np.empty(shape=(0, self.c_in), dtype=float)
696 | where = where[:, np.newaxis]
697 | where = np.repeat(where, self.c_in, axis=1)
698 | elif which == 'weights':
699 | full_features = np.empty(shape=(0, self.kernel_size), dtype=float)
700 | where = where[:, np.newaxis]
701 | where = np.repeat(where, self.kernel_size, axis=1)
702 |
703 | for i, (dilation, padding) in enumerate(zip(self.dilations, self.padding)):
704 |
705 | biases_this_dilation = getattr(self, f'biases_{i}')
706 | num_quantiles = biases_this_dilation.shape[1]
707 |
708 | kernel_indices = getattr(self, f'kernel_indices_{i}')
709 | bias_indices = getattr(self, f'bias_indices_{i}')
710 |
711 | # Biases (=features): as many as num dilations * num kernels * num quantiles
712 | if which == 'biases':
713 | sorted_biases = biases_this_dilation.flatten()[bias_indices]
714 | full_features = np.append(full_features, sorted_biases.cpu().numpy())
715 |
716 | # Channel combinations: num_dilations * num_kernel, where each combination has self.c_in indices
717 | elif which == 'channels':
718 | channel_combinations = getattr(self, f'channel_combinations_{i}')
719 |
720 | for q in range(0, num_quantiles):
721 | selected_kernels = kernel_indices[q * self.num_kernels : q * self.num_kernels + self.num_kernels].cpu().numpy()
722 | channel_combinations_q = channel_combinations[:, :, selected_kernels]
723 | channel_combinations_q = torch.transpose(channel_combinations_q.squeeze(), 0, 1).cpu().numpy()
724 |
725 | full_features = np.append(full_features, channel_combinations_q, axis=0)
726 |
727 | # Weights: num dilations * num kernels, where each kernel has 9 weights
728 | elif which == 'weights':
729 | weights = self.kernels.view(-1, self.num_kernels, self.kernel_size)[0].cpu().numpy() # Kernels are equal for all channels, pick the first one
730 |
731 | for q in range(0, num_quantiles):
732 | selected_kernels = kernel_indices[q * self.num_kernels : q * self.num_kernels + self.num_kernels].cpu().numpy()
733 | weights_q = weights[selected_kernels]
734 |
735 | full_features = np.append(full_features, weights_q, axis=0)
736 |
737 | elif which == 'dilations':
738 | expanded_dilations = np.repeat(dilation, self.num_kernels*num_quantiles, axis=0)
739 | full_features = np.append(full_features, expanded_dilations)
740 |
741 | elif which == 'paddings':
742 | expanded_dilations = np.repeat(padding, self.num_kernels*num_quantiles, axis=0)
743 | full_features = np.append(full_features, expanded_dilations)
744 |
745 | else: raise ValueError(f'"{which}" is not recognized as a feature. Possible feaures are "biases", "channels", "weights", "dilations" or "paddings"')
746 |
747 | return np.where(where, full_features, np.nan)
748 |
749 | def _get_PPVs(self, C, bias):
750 | C = C.unsqueeze(-1)
751 | bias = bias.view(1, bias.shape[0], 1, bias.shape[1])
752 | return (C > bias).float().mean(2).flatten(1)
753 |
754 | def _set_dilations(self, input_length):
755 | num_features_per_kernel = self.num_features // self.num_kernels
756 | true_max_dilations_per_kernel = min(num_features_per_kernel, self.max_dilations_per_kernel)
757 | multiplier = num_features_per_kernel / true_max_dilations_per_kernel
758 | max_exponent = np.log2((input_length - 1) / (9 - 1))
759 | dilations, num_features_per_dilation = \
760 | np.unique(np.logspace(0, max_exponent, true_max_dilations_per_kernel, base = 2).astype(np.int32), return_counts = True)
761 | num_features_per_dilation = (num_features_per_dilation * multiplier).astype(np.int32)
762 | remainder = num_features_per_kernel - num_features_per_dilation.sum()
763 | i = 0
764 | while remainder > 0:
765 | num_features_per_dilation[i] += 1
766 | remainder -= 1
767 | i = (i + 1) % len(num_features_per_dilation)
768 | self.num_features_per_dilation = num_features_per_dilation
769 | self.num_dilations = len(dilations)
770 | self.dilations = dilations
771 | self.padding = []
772 | for i, dilation in enumerate(dilations):
773 | self.padding.append((((self.kernel_size - 1) * dilation) // 2))
774 |
775 | def _set_channel_combinations(self, num_channels):
776 | num_combinations = self.num_kernels * self.num_dilations
777 | max_num_channels = min(num_channels, 9)
778 | max_exponent_channels = np.log2(max_num_channels + 1)
779 | num_channels_per_combination = (2 ** np.random.uniform(0, max_exponent_channels, num_combinations)).astype(np.int32)
780 | channel_combinations = torch.zeros((1, num_channels, num_combinations, 1))
781 | for i in range(num_combinations):
782 | channel_combinations[:, np.random.choice(num_channels, num_channels_per_combination[i], False), i] = 1 # From all the channels, set to 1 those that will be combined without repeating
783 | channel_combinations = torch.split(channel_combinations, self.num_kernels, 2) # split by dilation
784 | for i, channel_combination in enumerate(channel_combinations):
785 | self.register_buffer(f'channel_combinations_{i}', channel_combination) # per dilation
786 |
787 | def _get_quantiles(self, n):
788 | return torch.tensor([(_ * ((np.sqrt(5) + 1) / 2)) % 1 for _ in range(1, n + 1)]).float()
789 |
790 | def _get_bias(self, C, num_features_this_dilation):
791 | # Gets as many biases as features this dilation, from the quantiles of random samples
792 | idxs = np.random.choice(C.shape[0], self.num_kernels)
793 | samples = C[idxs].diagonal().T
794 | biases = torch.quantile(samples, self._get_quantiles(num_features_this_dilation).to(C.device), dim=1).T
795 | return biases
796 |
797 | def transform(self, o, chunksize=128):
798 | o = torch.tensor(o).float()
799 | if isinstance(o, np.ndarray): o = torch.from_numpy(o).to(self.device)
800 |
801 | _features = []
802 | for oi in torch.split(o, chunksize):
803 | _features.append(self(oi.to(self.device)))
804 |
805 | return torch.cat(_features).cpu()
806 |
807 | def fit_transform(self, o):
808 | return self.fit(o).transform(o)
809 |
810 |
811 | class DetachEnsemble():
812 | def __init__(self,
813 | num_models=25,
814 | num_kernels=10000,
815 | model_type='pytorch_minirocket',
816 | trade_off=0.1,
817 | recompute_alpha=True,
818 | val_ratio=0.33,
819 | verbose=False,
820 | multilabel_type='max',
821 | fixed_percentage=None,
822 | ):
823 |
824 | assert model_type == 'pytorch_minirocket', f"Incorrect model_type {model_type}: DetachEnsemble currently only supports 'pytorch_minirocket'"
825 | self.num_models = num_models
826 | self.num_kernels = num_kernels
827 | self.model_type = model_type
828 | self.derockets = []
829 | for _ in range(num_models):
830 | _DetachRocket = DetachRocket(
831 | model_type='pytorch_minirocket',
832 | num_kernels=num_kernels,
833 | trade_off=trade_off,
834 | recompute_alpha=recompute_alpha,
835 | val_ratio=val_ratio,
836 | verbose=verbose,
837 | multilabel_type=multilabel_type,
838 | fixed_percentage=fixed_percentage,
839 | )
840 |
841 | self.derockets.append(_DetachRocket)
842 |
843 | self.label_encoder = LabelEncoder()
844 | self._is_fitted = False
845 |
846 | # Transformer / Classifier methods
847 | def fit(self, X, y):
848 | [model.fit(X, y) for model in self.derockets]
849 | self.num_channels = X.shape[1]
850 | self.label_encoder.fit(y)
851 |
852 | self._is_fitted = True
853 | return self
854 |
855 | def predict_proba(self, X, proba='soft'):
856 | assert self._is_fitted == True, "Model not fitted. Call fit method first."
857 | weight_matrix = np.zeros((X.shape[0], len(self.label_encoder.classes_), self.num_models)) # (samples, classes, estimators)
858 |
859 | for m, model in enumerate(self.derockets):
860 | encoded_predictions = self.label_encoder.transform(model.predict(X))
861 |
862 | for p, pred in enumerate(encoded_predictions):
863 | weight_matrix[p, pred, m] = model._acc_train
864 |
865 | if proba == 'soft':
866 | votes = weight_matrix.sum(axis=2)
867 | elif proba == 'hard':
868 | votes = (weight_matrix != 0).astype(int).sum(axis=2)
869 | pass
870 | else:
871 | raise ValueError(f'proba={proba} is not valid. Use "soft" or "hard".')
872 |
873 | probas = votes / votes.sum(axis=(1), keepdims=True)
874 | return probas
875 |
876 | def predict(self, X):
877 | predictions = self.predict_proba(X).argmax(axis=1)
878 | return self.label_encoder.inverse_transform(predictions)
879 |
880 | def estimate_channel_relevance(self):
881 | channel_relevance_matrix = np.zeros((self.num_models, self.num_channels))
882 |
883 | for m, model in enumerate(self.derockets):
884 | # Get the weights and the channel combination matrix for the selected features
885 | feature_weights = model._feature_importance_matrix[model._max_index] # Sparse float array (num_features,)
886 | selection_mask = feature_weights > 0
887 |
888 | channel_combinations_derocket = model._full_transformer.get_kernel_features('channels', selection_mask) # Indicator matrix (num_features, num_channels)
889 | num_channels_in_kernel = np.nansum(channel_combinations_derocket, axis=1)
890 |
891 | # Divide weights by the number of channels (num_features,)
892 | full_weights = (feature_weights[num_channels_in_kernel != 0] / num_channels_in_kernel[num_channels_in_kernel != 0])
893 |
894 | # Get the weighted channel combination matrix (num_features, num_channels)
895 | weighted_channel_combinations = channel_combinations_derocket[num_channels_in_kernel != 0]*full_weights[:, np.newaxis]
896 |
897 | # Sum contributions and normalize (num_channels,)
898 | channel_relevance = np.sum(weighted_channel_combinations, axis=0) / np.sum(weighted_channel_combinations)
899 |
900 | # Add to the ensemble matrix
901 | channel_relevance_matrix[m] = channel_relevance
902 |
903 | return np.median(channel_relevance_matrix, axis=0)
904 |
--------------------------------------------------------------------------------