├── 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 | Logo 2 |
3 | 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 | repo logo 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+44ePZrGjRvTuHFjmjRpwpQpzh9P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", 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 | --------------------------------------------------------------------------------