tsCaptum 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tscaptum-0.0.1/LICENSE +21 -0
- tscaptum-0.0.1/PKG-INFO +54 -0
- tscaptum-0.0.1/README.md +38 -0
- tscaptum-0.0.1/pyproject.toml +22 -0
- tscaptum-0.0.1/setup.cfg +4 -0
- tscaptum-0.0.1/src/tsCaptum/_forwarder.py +47 -0
- tscaptum-0.0.1/src/tsCaptum/_tsCaptum_loader.py +15 -0
- tscaptum-0.0.1/src/tsCaptum/_utils.py +118 -0
- tscaptum-0.0.1/src/tsCaptum/explainers.py +231 -0
- tscaptum-0.0.1/src/tsCaptum/visualization.py +27 -0
- tscaptum-0.0.1/src/tsCaptum.egg-info/PKG-INFO +54 -0
- tscaptum-0.0.1/src/tsCaptum.egg-info/SOURCES.txt +13 -0
- tscaptum-0.0.1/src/tsCaptum.egg-info/dependency_links.txt +1 -0
- tscaptum-0.0.1/src/tsCaptum.egg-info/requires.txt +6 -0
- tscaptum-0.0.1/src/tsCaptum.egg-info/top_level.txt +1 -0
tscaptum-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 MLGIG: Machine Learning Georgiana Ifrim Group
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tscaptum-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: tsCaptum
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A Captum wrapper for Time Series XAI
|
|
5
|
+
Author-email: Davide Serramazza <davide.serramazza@ucdconnect.ie>
|
|
6
|
+
Project-URL: Homepage, https://github.com/mlgig/tscaptum/
|
|
7
|
+
Requires-Python: <3.12,>=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: scikit-learn
|
|
11
|
+
Requires-Dist: aeon
|
|
12
|
+
Requires-Dist: openpyxl
|
|
13
|
+
Requires-Dist: tqdm
|
|
14
|
+
Requires-Dist: torch>=2.0.0
|
|
15
|
+
Requires-Dist: captum==0.7.0
|
|
16
|
+
|
|
17
|
+
# tsCaptum - A library for univariate and multivariate time series explanation
|
|
18
|
+
|
|
19
|
+
tsCaptum is a library that adapts the popular post-hoc attribution methods implemented in the Captum
|
|
20
|
+
framework to work with time series. Unlike previous libraries tsCaptum is :
|
|
21
|
+
1) Fully compatible with scikit-learn and popular time-series frameworks such as aeon and sktime (e.g., so it is easy to combine ROCKET with SHAP).
|
|
22
|
+
2) Takes advantage of TS locality by using time series segmentation (so SHAP runs fast with long time series and suffers less from vanishing attribution weights)
|
|
23
|
+
3) It's extremely easy to use requiring almost no effort by the users
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
from aeon.classification.convolution_based import RocketClassifier
|
|
27
|
+
clf = RocketClassifier(n_jobs=-1)
|
|
28
|
+
clf.fit(MP_X_train,MP_y_train)
|
|
29
|
+
|
|
30
|
+
from tsCaptum.explainers import Feature_Ablation
|
|
31
|
+
myFA = Feature_Ablation(clf)
|
|
32
|
+
exp = myFA.explain(samples=CMJ_X_test_samples, labels=CMJ_y_test_samples, n_segments=10, normalise=False, baseline=0)
|
|
33
|
+
|
|
34
|
+
from tsCaptum.explainers import Shapley_Value_Sampling as SHAP
|
|
35
|
+
mySHAP = SHAP(clf)
|
|
36
|
+
exp = mySHAP.explain(CMJ_X_test_samples, labels=CMJ_y_test_samples, n_segments=10, normalise=False, baseline=0)
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
It can be installed by typing the command "pip install tsCaptum" or in case you have problems
|
|
41
|
+
installing torch (e.g. you use Linux as OS and you want a lighter installation) use
|
|
42
|
+
"pip3 install torch --index-url https://download.pytorch.org/whl/cpu && pip install tsCaptum"
|
|
43
|
+
|
|
44
|
+
In case you use this library please cite:
|
|
45
|
+
```
|
|
46
|
+
@misc{tsCaptum,
|
|
47
|
+
author = {Davide Italo Serramazza, Thach Le Nguyen, Georgiana Ifrim},
|
|
48
|
+
title = {tsCaptum: adapting Captum explainers for time series and scikit-learn-like predictors},
|
|
49
|
+
howpublished = {GitHub},
|
|
50
|
+
year = {2024},
|
|
51
|
+
note = {Temporary bibitex entry},
|
|
52
|
+
url = { https://github.com/mlgig/tscaptum },
|
|
53
|
+
}
|
|
54
|
+
```
|
tscaptum-0.0.1/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# tsCaptum - A library for univariate and multivariate time series explanation
|
|
2
|
+
|
|
3
|
+
tsCaptum is a library that adapts the popular post-hoc attribution methods implemented in the Captum
|
|
4
|
+
framework to work with time series. Unlike previous libraries tsCaptum is :
|
|
5
|
+
1) Fully compatible with scikit-learn and popular time-series frameworks such as aeon and sktime (e.g., so it is easy to combine ROCKET with SHAP).
|
|
6
|
+
2) Takes advantage of TS locality by using time series segmentation (so SHAP runs fast with long time series and suffers less from vanishing attribution weights)
|
|
7
|
+
3) It's extremely easy to use requiring almost no effort by the users
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
from aeon.classification.convolution_based import RocketClassifier
|
|
11
|
+
clf = RocketClassifier(n_jobs=-1)
|
|
12
|
+
clf.fit(MP_X_train,MP_y_train)
|
|
13
|
+
|
|
14
|
+
from tsCaptum.explainers import Feature_Ablation
|
|
15
|
+
myFA = Feature_Ablation(clf)
|
|
16
|
+
exp = myFA.explain(samples=CMJ_X_test_samples, labels=CMJ_y_test_samples, n_segments=10, normalise=False, baseline=0)
|
|
17
|
+
|
|
18
|
+
from tsCaptum.explainers import Shapley_Value_Sampling as SHAP
|
|
19
|
+
mySHAP = SHAP(clf)
|
|
20
|
+
exp = mySHAP.explain(CMJ_X_test_samples, labels=CMJ_y_test_samples, n_segments=10, normalise=False, baseline=0)
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
It can be installed by typing the command "pip install tsCaptum" or in case you have problems
|
|
25
|
+
installing torch (e.g. you use Linux as OS and you want a lighter installation) use
|
|
26
|
+
"pip3 install torch --index-url https://download.pytorch.org/whl/cpu && pip install tsCaptum"
|
|
27
|
+
|
|
28
|
+
In case you use this library please cite:
|
|
29
|
+
```
|
|
30
|
+
@misc{tsCaptum,
|
|
31
|
+
author = {Davide Italo Serramazza, Thach Le Nguyen, Georgiana Ifrim},
|
|
32
|
+
title = {tsCaptum: adapting Captum explainers for time series and scikit-learn-like predictors},
|
|
33
|
+
howpublished = {GitHub},
|
|
34
|
+
year = {2024},
|
|
35
|
+
note = {Temporary bibitex entry},
|
|
36
|
+
url = { https://github.com/mlgig/tscaptum },
|
|
37
|
+
}
|
|
38
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69.5.1"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tsCaptum"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
authors = [ { name="Davide Serramazza", email="davide.serramazza@ucdconnect.ie" },]
|
|
9
|
+
description = "A Captum wrapper for Time Series XAI"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.8,<3.12"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"scikit-learn",
|
|
14
|
+
"aeon",
|
|
15
|
+
"openpyxl",
|
|
16
|
+
"tqdm",
|
|
17
|
+
"torch>=2.0.0",
|
|
18
|
+
"captum==0.7.0"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
"Homepage" = "https://github.com/mlgig/tscaptum/"
|
tscaptum-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import torch
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# documentation for each method!
|
|
6
|
+
|
|
7
|
+
class _Forwarder:
|
|
8
|
+
"""
|
|
9
|
+
class automatically detecting the right forward function to use with the captum explainers
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# TODO specify possible clf type as aeonClf | SktimeClf | ....
|
|
13
|
+
|
|
14
|
+
def __init__(self, predictor, predictor_type):
|
|
15
|
+
"""
|
|
16
|
+
init function for this class
|
|
17
|
+
|
|
18
|
+
:param predictor: the predictor to be explained
|
|
19
|
+
:
|
|
20
|
+
param predictor_type: (optional) the predictor variable's type i.e. regressor or classifier
|
|
21
|
+
"""
|
|
22
|
+
# TODO should I make the following two variables private?
|
|
23
|
+
self.predictor = predictor
|
|
24
|
+
if predictor_type == "regressor":
|
|
25
|
+
self.raw_result_func = self.predictor.predict
|
|
26
|
+
elif predictor_type == "classifier":
|
|
27
|
+
self.raw_result_func = self.predictor.predict_proba
|
|
28
|
+
else:
|
|
29
|
+
raise " provided model not recognized. Please specify whether is a classifier or regressor "
|
|
30
|
+
|
|
31
|
+
def forward(self, X):
|
|
32
|
+
"""
|
|
33
|
+
function adapting scikit-learn-like predictor to captum explainers
|
|
34
|
+
|
|
35
|
+
:param X: instances to be explained
|
|
36
|
+
|
|
37
|
+
:return: predictor's output as a torch tensor
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# convert X to pytorch tensor
|
|
41
|
+
X_numpy: np.array = X.detach().numpy()
|
|
42
|
+
# use the model forward function
|
|
43
|
+
preds: np.array = self.raw_result_func(X_numpy)
|
|
44
|
+
# return result as torch tensor as expected by captum attribution method
|
|
45
|
+
preds_torch: torch.tensor = torch.tensor(preds)
|
|
46
|
+
|
|
47
|
+
return preds_torch
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from torch.utils.data import Dataset
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class _tsCaptum_loader(Dataset):
|
|
5
|
+
|
|
6
|
+
def __init__(self, X, labels):
|
|
7
|
+
super().__init__()
|
|
8
|
+
self.X = X
|
|
9
|
+
self.labels = labels
|
|
10
|
+
|
|
11
|
+
def __len__(self):
|
|
12
|
+
return self.X.shape[0]
|
|
13
|
+
|
|
14
|
+
def __getitem__(self, idx):
|
|
15
|
+
return self.X[idx], self.labels[idx]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import torch
|
|
3
|
+
from torch.utils.data import DataLoader
|
|
4
|
+
from sklearn.preprocessing import LabelEncoder
|
|
5
|
+
|
|
6
|
+
from ._tsCaptum_loader import _tsCaptum_loader
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# TODO check to aeon, sktime, torch and captum how function in utils are called (with or without leading _) ?
|
|
10
|
+
|
|
11
|
+
def _equal_length_segmentation(n_chunks: int, n_channels: int, series_length: int):
|
|
12
|
+
r"""
|
|
13
|
+
function returning how to group time points into time Series accordingly to the given arguments
|
|
14
|
+
To be noted that it operates channel-wise i.e. each channel is divided into 'n_chunks' chunks
|
|
15
|
+
|
|
16
|
+
:param n_chunks: number of chunks to be used
|
|
17
|
+
|
|
18
|
+
:param n_channels: number of channel of each instance in the dataset
|
|
19
|
+
|
|
20
|
+
:param series_length: length of each channel of each instance in the dataset
|
|
21
|
+
|
|
22
|
+
:return: a torch tensor representing how to group time points
|
|
23
|
+
"""
|
|
24
|
+
quotient, reminder = np.floor(series_length / n_chunks).astype(int), series_length % n_chunks
|
|
25
|
+
|
|
26
|
+
first_group = np.array([[i + j * n_chunks for i in range(reminder)] for j in range(n_channels)])
|
|
27
|
+
first_group = np.expand_dims(np.repeat(first_group, (quotient + 1), axis=1), 0)
|
|
28
|
+
|
|
29
|
+
second_group = np.array([[i + j * n_chunks for i in range(reminder, n_chunks)] for j in range(n_channels)])
|
|
30
|
+
second_group = np.expand_dims(np.repeat(second_group, quotient, axis=1), 0)
|
|
31
|
+
|
|
32
|
+
final_group = np.concatenate((first_group, second_group), axis=-1)
|
|
33
|
+
return torch.tensor(final_group).to(torch.int64)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _normalise_result(X):
|
|
37
|
+
"""
|
|
38
|
+
function to normalize obtained saliency map
|
|
39
|
+
|
|
40
|
+
:param X: the saliency map to be normalized
|
|
41
|
+
|
|
42
|
+
:return: normalized version of X
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
assert len(X.shape) == 3
|
|
46
|
+
results = []
|
|
47
|
+
for x in X:
|
|
48
|
+
scaling_factor = 1 / max(np.abs(x.max()), np.abs(x.min()))
|
|
49
|
+
results.append(scaling_factor * x)
|
|
50
|
+
return results
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _check_labels(labels, predictor_type):
|
|
54
|
+
r"""
|
|
55
|
+
function checking the label argument provided to explain method and converting them into integer representation as
|
|
56
|
+
required by captum
|
|
57
|
+
|
|
58
|
+
:param labels: provided labels
|
|
59
|
+
|
|
60
|
+
:param predictor_type: predictor's type i.e. classifier or regressor
|
|
61
|
+
|
|
62
|
+
:return: label encoder and relative integer indices
|
|
63
|
+
"""
|
|
64
|
+
if predictor_type == "classifier":
|
|
65
|
+
# transform to numeric labels
|
|
66
|
+
le = LabelEncoder()
|
|
67
|
+
labels_idx = torch.tensor(le.fit_transform(labels)).type(torch.int64)
|
|
68
|
+
|
|
69
|
+
elif predictor_type == "regressor":
|
|
70
|
+
if labels is not None:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
"specified labels when predictor type is regressor"
|
|
73
|
+
)
|
|
74
|
+
le = None
|
|
75
|
+
labels_idx = None
|
|
76
|
+
|
|
77
|
+
else:
|
|
78
|
+
raise (
|
|
79
|
+
" provided predictor type not recognized. Please specify whether is a classifier or regressor "
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return le, labels_idx
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _check_convert_data_format(X, labels, batch_size):
|
|
86
|
+
r"""
|
|
87
|
+
function checking and converting provided samples and labels to explain method
|
|
88
|
+
|
|
89
|
+
:param X: sample to explain. Can be provided as numpy array or as torch tensor
|
|
90
|
+
|
|
91
|
+
:param labels: labels provided to explain method
|
|
92
|
+
|
|
93
|
+
:param batch_size: batch size provided to explain method
|
|
94
|
+
|
|
95
|
+
:return: data loader to be used in the explain method
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
if X is None and labels is not None:
|
|
99
|
+
if X.shape[0] != labels.shape[0]:
|
|
100
|
+
# if both X and labels are provided having no matching dimensions
|
|
101
|
+
raise ValueError(
|
|
102
|
+
"provided samples and labels have different dimensions"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if isinstance(X, np.ndarray):
|
|
106
|
+
X = torch.tensor(X).type(torch.float)
|
|
107
|
+
elif isinstance(X, torch.Tensor):
|
|
108
|
+
X = X.type(torch.float)
|
|
109
|
+
else:
|
|
110
|
+
raise TypeError(
|
|
111
|
+
" Data format has to be either numpy array or torch tensor "
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if labels is None:
|
|
115
|
+
labels = torch.ones(X.shape[0]) * -1
|
|
116
|
+
loader = DataLoader(_tsCaptum_loader(X, labels), shuffle=False, batch_size=batch_size)
|
|
117
|
+
|
|
118
|
+
return loader
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from tqdm import tqdm
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
4
|
+
from captum.attr._utils.attribution import PerturbationAttribution
|
|
5
|
+
from captum.attr import (FeatureAblation as _FeatureAblationCaptum,
|
|
6
|
+
FeaturePermutation as _FeaturePermutationCaptum,
|
|
7
|
+
KernelShap as _KernelShapCaptum,
|
|
8
|
+
Lime as _LimeCaptum,
|
|
9
|
+
ShapleyValueSampling as _ShapleyValueSamplingCaptum)
|
|
10
|
+
|
|
11
|
+
from ._forwarder import _Forwarder
|
|
12
|
+
from ._utils import _check_convert_data_format, _check_labels, _normalise_result, _equal_length_segmentation
|
|
13
|
+
from ._utils import *
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _tsCaptum_Method:
|
|
17
|
+
"""
|
|
18
|
+
super class for all attribution methods
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, explainer, predictor, predictor_type: str = None):
|
|
22
|
+
r"""
|
|
23
|
+
init method for the superclass
|
|
24
|
+
|
|
25
|
+
:param explainer: the actual explainer that will be used for computing the saliency maps.
|
|
26
|
+
Each subclass fix this argument as the corresponding Captum explainer
|
|
27
|
+
|
|
28
|
+
:param predictor: the predictor that will be explained
|
|
29
|
+
:param predictor_type: which type the predictor is i.e. classifier or regressor
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
# check argument values
|
|
33
|
+
if not issubclass(explainer, PerturbationAttribution):
|
|
34
|
+
raise (
|
|
35
|
+
" provided explainer has to be an instance of 'captum.attr._utils.attribution.PerturbationAttribution' ")
|
|
36
|
+
|
|
37
|
+
if predictor_type not in ["classifier", "regressor", None]:
|
|
38
|
+
raise (
|
|
39
|
+
" clf_type argument has to be either 'classifier' or 'regressor' ")
|
|
40
|
+
|
|
41
|
+
# in case predictor_type argument isn't provided tell it calling the predict_proba method:
|
|
42
|
+
# if it's present the predictor is a classifier otherwise is a regressor
|
|
43
|
+
self.predictor_type = predictor_type
|
|
44
|
+
if self.predictor_type is None:
|
|
45
|
+
self.predictor_type = "classifier"
|
|
46
|
+
try:
|
|
47
|
+
predictor.predict_proba
|
|
48
|
+
except AttributeError:
|
|
49
|
+
self.predictor_type = "regressor"
|
|
50
|
+
|
|
51
|
+
# set also forward function and explainer to be used
|
|
52
|
+
self._Forwarder = _Forwarder(predictor, self.predictor_type)
|
|
53
|
+
self._explainer = explainer(self._Forwarder.forward)
|
|
54
|
+
|
|
55
|
+
def explain(self, samples, labels=None, batch_size=8, n_segments=10, normalise=False, baseline=0):
|
|
56
|
+
r"""
|
|
57
|
+
main method to get a saliency map by the selected explainer
|
|
58
|
+
|
|
59
|
+
:param samples: samples to be explained
|
|
60
|
+
|
|
61
|
+
:param labels: labels associated to samples in case of classification
|
|
62
|
+
|
|
63
|
+
:param batch_size: the batch_size to be used i.e. number of samples to be explained at the same time
|
|
64
|
+
|
|
65
|
+
:param n_segments: number of segments the timeseries is dived to. If you want to explain point-wise provide -1 as
|
|
66
|
+
value #TODO should we put more information about how it works?
|
|
67
|
+
|
|
68
|
+
:param normalise: whether or not to normalise the result
|
|
69
|
+
|
|
70
|
+
:param baseline: the baseline which will substitute time series's values when ablated. It can be either a scalar
|
|
71
|
+
(each time series's value is substituted by this scalar) or a single time series
|
|
72
|
+
(both as numpy array or torch.tensor)
|
|
73
|
+
|
|
74
|
+
:return: the saliency maps as a 3D tensor ( samples, channels, time points)
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
# check arguments and get a DataLoader
|
|
78
|
+
n_2explain, n_channels, series_length = samples.shape
|
|
79
|
+
le, labels_idx = _check_labels(labels, self.predictor_type)
|
|
80
|
+
loader = _check_convert_data_format(samples, labels_idx, batch_size)
|
|
81
|
+
|
|
82
|
+
explanations = []
|
|
83
|
+
with tqdm(total=n_2explain) as pbar:
|
|
84
|
+
with torch.no_grad():
|
|
85
|
+
for n, (X, y) in enumerate(loader):
|
|
86
|
+
# fix kwargs for the relative captum method
|
|
87
|
+
kwargs = self._define_kwargs(baseline, n_channels, n_segments, series_length, y)
|
|
88
|
+
# get the current saliency maps, convert it to numpy array and store it to a temp list
|
|
89
|
+
current_exps = self._explainer.attribute(X, **kwargs)
|
|
90
|
+
explanations.append(
|
|
91
|
+
_normalise_result(current_exps.detach().numpy())) if normalise \
|
|
92
|
+
else explanations.append(current_exps.detach().numpy())
|
|
93
|
+
pbar.update(batch_size)
|
|
94
|
+
pbar.close()
|
|
95
|
+
|
|
96
|
+
# convert the list to numpy array and return it as result
|
|
97
|
+
explanations = np.concatenate(explanations)
|
|
98
|
+
return explanations
|
|
99
|
+
|
|
100
|
+
def _define_kwargs(self, baseline, n_channels, n_segments, series_length, y):
|
|
101
|
+
r"""
|
|
102
|
+
inner function that checking provided argument to explain, define the correct kwarg dictionary
|
|
103
|
+
|
|
104
|
+
:return: kwarg dictionary for the relative captum method
|
|
105
|
+
"""
|
|
106
|
+
kwargs = {}
|
|
107
|
+
|
|
108
|
+
# checking baseline
|
|
109
|
+
if not isinstance(self, Feature_Permutation):
|
|
110
|
+
if np.isscalar(baseline):
|
|
111
|
+
kwargs['baselines'] = baseline
|
|
112
|
+
elif type(baseline) is np.ndarray:
|
|
113
|
+
kwargs['baselines'] = torch.tensor(baseline)
|
|
114
|
+
elif type(baseline) is torch.Tensor:
|
|
115
|
+
kwargs['baselines'] = baseline
|
|
116
|
+
|
|
117
|
+
# labels
|
|
118
|
+
if self.predictor_type == "classifier":
|
|
119
|
+
kwargs['target'] = y
|
|
120
|
+
|
|
121
|
+
# define feature mask looking at the desired number of segment
|
|
122
|
+
if n_segments != -1:
|
|
123
|
+
groups = _equal_length_segmentation(n_segments, n_channels, series_length)
|
|
124
|
+
kwargs['feature_mask'] = groups
|
|
125
|
+
|
|
126
|
+
return kwargs
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class Feature_Ablation(_tsCaptum_Method):
|
|
130
|
+
r"""
|
|
131
|
+
Wrapper for Feature Ablation method
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def __init__(self, clf, clf_type=None):
|
|
135
|
+
super().__init__(_FeatureAblationCaptum, clf, clf_type)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Feature_Permutation(_tsCaptum_Method):
|
|
139
|
+
r"""
|
|
140
|
+
Wrapper for feature permutation method
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def __init__(self, clf, clf_type=None):
|
|
144
|
+
super().__init__(_FeaturePermutationCaptum, clf, clf_type)
|
|
145
|
+
|
|
146
|
+
def explain(self, samples, **kwargs):
|
|
147
|
+
r"""
|
|
148
|
+
extending _tsCaptum_Method's explain by fixing some arguments as defined by Captum implementation.
|
|
149
|
+
For Feature Permutation takes care of the batch_size > 2 and baseline which can't be provided
|
|
150
|
+
|
|
151
|
+
:param samples: samples to be explained
|
|
152
|
+
|
|
153
|
+
:param kwargs: additional arguments
|
|
154
|
+
|
|
155
|
+
:calling: _tsCaptum_Method'e explain
|
|
156
|
+
"""
|
|
157
|
+
if 'batch_size' in kwargs and kwargs['batch_size'] != 1:
|
|
158
|
+
warnings.warn(
|
|
159
|
+
"batch_size set to 2 as Feature Permutation require more than 1 sample to work"
|
|
160
|
+
)
|
|
161
|
+
kwargs['batch_size'] = 2
|
|
162
|
+
|
|
163
|
+
if 'baseline' in kwargs:
|
|
164
|
+
warnings.warn(
|
|
165
|
+
"specified baseline will be ignored as Feature Permutation algorithm has its own baseline"
|
|
166
|
+
)
|
|
167
|
+
return super().explain(samples, **kwargs)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class Kernel_Shap(_tsCaptum_Method):
|
|
171
|
+
r"""
|
|
172
|
+
Wrapper for KernelSHAP method
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
def __init__(self, clf, clf_type=None):
|
|
176
|
+
super().__init__(_KernelShapCaptum, clf, clf_type)
|
|
177
|
+
|
|
178
|
+
def explain(self, samples, **kwargs):
|
|
179
|
+
r"""
|
|
180
|
+
extending _tsCaptum_Method'e explain by fixing some arguments as defined by Captum implementation.
|
|
181
|
+
For KernelSHAP takes care of the batch_size suggested to be equal to 1
|
|
182
|
+
|
|
183
|
+
:param samples: samples to be explained
|
|
184
|
+
|
|
185
|
+
:param kwargs: additional arguments
|
|
186
|
+
|
|
187
|
+
:calling: _tsCaptum_Method's explain
|
|
188
|
+
"""
|
|
189
|
+
if 'batch_size' in kwargs and kwargs['batch_size'] != 1:
|
|
190
|
+
warnings.warn(
|
|
191
|
+
"batch_size set to 1 as suggested by Captum for Lime and KernelSHAP"
|
|
192
|
+
)
|
|
193
|
+
kwargs['batch_size'] = 1
|
|
194
|
+
return super().explain(samples, **kwargs)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class LIME(_tsCaptum_Method):
|
|
198
|
+
r"""
|
|
199
|
+
Wrapper for LIME method
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def __init__(self, clf, clf_type=None):
|
|
203
|
+
super().__init__(_LimeCaptum, clf, clf_type)
|
|
204
|
+
|
|
205
|
+
def explain(self, samples, **kwargs):
|
|
206
|
+
r"""
|
|
207
|
+
extending _tsCaptum_Method'e explain by fixing some arguments as defined by Captum implementation.
|
|
208
|
+
For LIME takes care of the batch_size suggested to be equal to 1
|
|
209
|
+
|
|
210
|
+
:param samples: samples to be explained
|
|
211
|
+
|
|
212
|
+
:param kwargs: additional arguments
|
|
213
|
+
|
|
214
|
+
:calling: _tsCaptum_Method's explain
|
|
215
|
+
"""
|
|
216
|
+
if 'batch_size' in kwargs and kwargs['batch_size'] != 1:
|
|
217
|
+
warnings.warn(
|
|
218
|
+
"batch_size set to 1 as suggested by Captum for Lime and KernelSHAP"
|
|
219
|
+
)
|
|
220
|
+
kwargs['batch_size'] = 1
|
|
221
|
+
return super().explain(samples, **kwargs)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class Shapley_Value_Sampling(_tsCaptum_Method):
|
|
225
|
+
r"""
|
|
226
|
+
Wrapper for Shapley Value Sampling method. Most of the time this is the best approximation of the intractable
|
|
227
|
+
Shapley values
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
def __init__(self, clf, clf_type=None):
|
|
231
|
+
super().__init__(_ShapleyValueSamplingCaptum, clf, clf_type)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
from scipy.interpolate import interp1d
|
|
4
|
+
|
|
5
|
+
def plot_saliency_map_uni(sample, attribution, title = 'Saliency map'):
|
|
6
|
+
|
|
7
|
+
def transform(X):
|
|
8
|
+
ma,mi = np.max(X), np.min(X)
|
|
9
|
+
X = (X - mi)/(ma-mi)
|
|
10
|
+
return X*100
|
|
11
|
+
|
|
12
|
+
weight = transform(abs(attribution))
|
|
13
|
+
ts = np.squeeze(sample)
|
|
14
|
+
|
|
15
|
+
max_length1, max_length2 = ts.shape[0],10000 #
|
|
16
|
+
x1 = np.linspace(0,max_length1,num = max_length1)
|
|
17
|
+
x2 = np.linspace(0,max_length1,num = max_length2)
|
|
18
|
+
y1 = ts
|
|
19
|
+
|
|
20
|
+
f = interp1d(x1, y1) # interpolate time series
|
|
21
|
+
fcas = interp1d(x1, weight) # interpolate weight color
|
|
22
|
+
weight = fcas(x2) # convert vector of original weight vector to new weight vector
|
|
23
|
+
|
|
24
|
+
plt.figure(figsize=(6, 2))
|
|
25
|
+
plt.scatter(x2,f(x2), c = weight, cmap = 'jet', marker='.', s= 1,vmin=0,vmax = 100)
|
|
26
|
+
plt.title(title)
|
|
27
|
+
plt.show()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: tsCaptum
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A Captum wrapper for Time Series XAI
|
|
5
|
+
Author-email: Davide Serramazza <davide.serramazza@ucdconnect.ie>
|
|
6
|
+
Project-URL: Homepage, https://github.com/mlgig/tscaptum/
|
|
7
|
+
Requires-Python: <3.12,>=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: scikit-learn
|
|
11
|
+
Requires-Dist: aeon
|
|
12
|
+
Requires-Dist: openpyxl
|
|
13
|
+
Requires-Dist: tqdm
|
|
14
|
+
Requires-Dist: torch>=2.0.0
|
|
15
|
+
Requires-Dist: captum==0.7.0
|
|
16
|
+
|
|
17
|
+
# tsCaptum - A library for univariate and multivariate time series explanation
|
|
18
|
+
|
|
19
|
+
tsCaptum is a library that adapts the popular post-hoc attribution methods implemented in the Captum
|
|
20
|
+
framework to work with time series. Unlike previous libraries tsCaptum is :
|
|
21
|
+
1) Fully compatible with scikit-learn and popular time-series frameworks such as aeon and sktime (e.g., so it is easy to combine ROCKET with SHAP).
|
|
22
|
+
2) Takes advantage of TS locality by using time series segmentation (so SHAP runs fast with long time series and suffers less from vanishing attribution weights)
|
|
23
|
+
3) It's extremely easy to use requiring almost no effort by the users
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
from aeon.classification.convolution_based import RocketClassifier
|
|
27
|
+
clf = RocketClassifier(n_jobs=-1)
|
|
28
|
+
clf.fit(MP_X_train,MP_y_train)
|
|
29
|
+
|
|
30
|
+
from tsCaptum.explainers import Feature_Ablation
|
|
31
|
+
myFA = Feature_Ablation(clf)
|
|
32
|
+
exp = myFA.explain(samples=CMJ_X_test_samples, labels=CMJ_y_test_samples, n_segments=10, normalise=False, baseline=0)
|
|
33
|
+
|
|
34
|
+
from tsCaptum.explainers import Shapley_Value_Sampling as SHAP
|
|
35
|
+
mySHAP = SHAP(clf)
|
|
36
|
+
exp = mySHAP.explain(CMJ_X_test_samples, labels=CMJ_y_test_samples, n_segments=10, normalise=False, baseline=0)
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
It can be installed by typing the command "pip install tsCaptum" or in case you have problems
|
|
41
|
+
installing torch (e.g. you use Linux as OS and you want a lighter installation) use
|
|
42
|
+
"pip3 install torch --index-url https://download.pytorch.org/whl/cpu && pip install tsCaptum"
|
|
43
|
+
|
|
44
|
+
In case you use this library please cite:
|
|
45
|
+
```
|
|
46
|
+
@misc{tsCaptum,
|
|
47
|
+
author = {Davide Italo Serramazza, Thach Le Nguyen, Georgiana Ifrim},
|
|
48
|
+
title = {tsCaptum: adapting Captum explainers for time series and scikit-learn-like predictors},
|
|
49
|
+
howpublished = {GitHub},
|
|
50
|
+
year = {2024},
|
|
51
|
+
note = {Temporary bibitex entry},
|
|
52
|
+
url = { https://github.com/mlgig/tscaptum },
|
|
53
|
+
}
|
|
54
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/tsCaptum/_forwarder.py
|
|
5
|
+
src/tsCaptum/_tsCaptum_loader.py
|
|
6
|
+
src/tsCaptum/_utils.py
|
|
7
|
+
src/tsCaptum/explainers.py
|
|
8
|
+
src/tsCaptum/visualization.py
|
|
9
|
+
src/tsCaptum.egg-info/PKG-INFO
|
|
10
|
+
src/tsCaptum.egg-info/SOURCES.txt
|
|
11
|
+
src/tsCaptum.egg-info/dependency_links.txt
|
|
12
|
+
src/tsCaptum.egg-info/requires.txt
|
|
13
|
+
src/tsCaptum.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tsCaptum
|