gpe-framework 1.0.0__py3-none-any.whl

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.
baselines/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """
2
+ Baseline Explainer Wrappers.
3
+
4
+ This module provides unified wrappers for comparing GPE with other
5
+ explanation methods:
6
+ - LIME (Local Interpretable Model-agnostic Explanations)
7
+ - SHAP (SHapley Additive exPlanations)
8
+ - Anchors
9
+
10
+ Author: Vladyslav Dehtiarov
11
+ """
12
+
13
+ from .lime_wrapper import LIMEWrapper
14
+ from .shap_wrapper import SHAPWrapper
15
+ from .anchors_wrapper import AnchorsWrapper
16
+
17
+ __all__ = ['LIMEWrapper', 'SHAPWrapper', 'AnchorsWrapper']
18
+
@@ -0,0 +1,257 @@
1
+ """
2
+ Anchors Wrapper for GPE Benchmark.
3
+
4
+ Provides a unified interface for Anchors explanations that matches
5
+ the GPE Explanation format.
6
+
7
+ Author: Vladyslav Dehtiarov
8
+ """
9
+
10
+ import numpy as np
11
+ import time
12
+ from typing import List, Optional, Dict, Any
13
+
14
+ import sys
15
+ sys.path.insert(0, '..')
16
+ from gpe.explanation import Condition, Rule, Explanation
17
+
18
+
19
+ class AnchorsWrapper:
20
+ """
21
+ Wrapper for Anchors explanations.
22
+
23
+ Anchors generate rule-based explanations that "anchor" the prediction,
24
+ providing sufficient conditions for the prediction to hold.
25
+
26
+ Parameters:
27
+ model: The model to explain
28
+ X_train: Training data
29
+ feature_names: Feature names
30
+ categorical_names: Dict mapping categorical feature indices to value names
31
+ threshold: Precision threshold for anchors (default: 0.95)
32
+
33
+ Example:
34
+ >>> wrapper = AnchorsWrapper(model, X_train, feature_names)
35
+ >>> explanation = wrapper.explain(x)
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ model,
41
+ X_train: np.ndarray,
42
+ feature_names: Optional[List[str]] = None,
43
+ categorical_names: Optional[Dict[int, List[str]]] = None,
44
+ threshold: float = 0.95
45
+ ):
46
+ self.model = model
47
+ self.X_train = X_train
48
+ self.threshold = threshold
49
+ self.categorical_names = categorical_names or {}
50
+
51
+ # Set feature names
52
+ if feature_names is None:
53
+ self.feature_names = [f"feature_{i}" for i in range(X_train.shape[1])]
54
+ else:
55
+ self.feature_names = feature_names
56
+
57
+ # Initialize Anchors
58
+ self._init_anchors()
59
+
60
+ def _init_anchors(self):
61
+ """Initialize the Anchors explainer."""
62
+ try:
63
+ from anchor import anchor_tabular
64
+ except ImportError:
65
+ raise ImportError(
66
+ "anchor-exp is not installed. Install with: pip install anchor-exp"
67
+ )
68
+
69
+ self.explainer = anchor_tabular.AnchorTabularExplainer(
70
+ class_names=['0', '1'], # Will be updated based on model
71
+ feature_names=self.feature_names,
72
+ train_data=self.X_train,
73
+ categorical_names=self.categorical_names
74
+ )
75
+
76
+ def explain(
77
+ self,
78
+ x: np.ndarray,
79
+ **kwargs
80
+ ) -> Explanation:
81
+ """
82
+ Generate an explanation using Anchors.
83
+
84
+ Args:
85
+ x: Instance to explain
86
+ **kwargs: Additional arguments for Anchors
87
+
88
+ Returns:
89
+ Explanation object in GPE format
90
+ """
91
+ start_time = time.time()
92
+
93
+ x = np.asarray(x).flatten()
94
+
95
+ # Get Anchors explanation
96
+ anchor_exp = self.explainer.explain_instance(
97
+ x,
98
+ self.model.predict,
99
+ threshold=self.threshold,
100
+ **kwargs
101
+ )
102
+
103
+ prediction = self.model.predict(x.reshape(1, -1))[0]
104
+
105
+ # Convert Anchors to GPE format
106
+ conditions = self._parse_anchor_rules(anchor_exp, x)
107
+
108
+ # Create rule
109
+ rule = Rule(
110
+ conditions=conditions,
111
+ prediction=prediction,
112
+ confidence=anchor_exp.precision() if hasattr(anchor_exp, 'precision') else None
113
+ )
114
+
115
+ computation_time = time.time() - start_time
116
+
117
+ # Get metrics from Anchors
118
+ precision = anchor_exp.precision() if hasattr(anchor_exp, 'precision') else 1.0
119
+ coverage = anchor_exp.coverage() if hasattr(anchor_exp, 'coverage') else 0.0
120
+
121
+ return Explanation(
122
+ instance=x,
123
+ rule=rule,
124
+ prediction=prediction,
125
+ precision=precision,
126
+ coverage=coverage,
127
+ method="Anchors",
128
+ computation_time=computation_time,
129
+ metadata={
130
+ 'anchor_names': anchor_exp.names() if hasattr(anchor_exp, 'names') else [],
131
+ 'threshold': self.threshold
132
+ }
133
+ )
134
+
135
+ def _parse_anchor_rules(
136
+ self,
137
+ anchor_exp,
138
+ x: np.ndarray
139
+ ) -> List[Condition]:
140
+ """
141
+ Parse Anchors explanation into Conditions.
142
+
143
+ Anchors format examples:
144
+ - "feature_0 > 3.50"
145
+ - "feature_1 <= 2.00"
146
+ - "3.50 < feature_0 <= 5.00"
147
+ """
148
+ import re
149
+
150
+ conditions = []
151
+
152
+ if not hasattr(anchor_exp, 'names'):
153
+ return conditions
154
+
155
+ for rule_str in anchor_exp.names():
156
+ # Try different patterns
157
+
158
+ # Pattern: "value < feature <= value"
159
+ match = re.match(r"([\d.-]+)\s*<\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*<=\s*([\d.-]+)", rule_str)
160
+ if match:
161
+ low = float(match.group(1))
162
+ feature_name = match.group(2)
163
+ high = float(match.group(3))
164
+
165
+ try:
166
+ idx = self.feature_names.index(feature_name)
167
+ except ValueError:
168
+ continue
169
+
170
+ # Add both bounds
171
+ conditions.append(Condition(
172
+ feature_index=idx,
173
+ feature_name=feature_name,
174
+ operator='>',
175
+ threshold=low
176
+ ))
177
+ conditions.append(Condition(
178
+ feature_index=idx,
179
+ feature_name=feature_name,
180
+ operator='<=',
181
+ threshold=high
182
+ ))
183
+ continue
184
+
185
+ # Pattern: "feature <= value" or "feature > value"
186
+ match = re.match(r"([a-zA-Z_][a-zA-Z0-9_]*)\s*(<=|<|>=|>|=)\s*([\d.-]+)", rule_str)
187
+ if match:
188
+ feature_name = match.group(1)
189
+ operator = match.group(2)
190
+ threshold = float(match.group(3))
191
+
192
+ try:
193
+ idx = self.feature_names.index(feature_name)
194
+ except ValueError:
195
+ continue
196
+
197
+ # Normalize operator
198
+ if operator == '=':
199
+ operator = '=='
200
+
201
+ conditions.append(Condition(
202
+ feature_index=idx,
203
+ feature_name=feature_name,
204
+ operator=operator,
205
+ threshold=threshold
206
+ ))
207
+ continue
208
+
209
+ # Categorical pattern: "feature = category_name"
210
+ match = re.match(r"([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*([a-zA-Z_][a-zA-Z0-9_]*)", rule_str)
211
+ if match:
212
+ feature_name = match.group(1)
213
+ category = match.group(2)
214
+
215
+ try:
216
+ idx = self.feature_names.index(feature_name)
217
+ except ValueError:
218
+ continue
219
+
220
+ conditions.append(Condition(
221
+ feature_index=idx,
222
+ feature_name=feature_name,
223
+ operator='==',
224
+ threshold=category,
225
+ is_categorical=True
226
+ ))
227
+
228
+ return conditions
229
+
230
+ def explain_batch(
231
+ self,
232
+ X: np.ndarray,
233
+ verbose: bool = False
234
+ ) -> List[Explanation]:
235
+ """Generate explanations for multiple instances."""
236
+ explanations = []
237
+
238
+ for i, x in enumerate(X):
239
+ if verbose and (i + 1) % 10 == 0:
240
+ print(f"Anchors: Explaining instance {i + 1}/{X.shape[0]}")
241
+
242
+ try:
243
+ explanation = self.explain(x)
244
+ explanations.append(explanation)
245
+ except Exception as e:
246
+ # Anchors can fail for some instances
247
+ print(f"Warning: Anchors failed for instance {i}: {e}")
248
+ # Create empty explanation
249
+ explanations.append(Explanation(
250
+ instance=x,
251
+ rule=Rule(conditions=[], prediction=self.model.predict(x.reshape(1, -1))[0]),
252
+ prediction=self.model.predict(x.reshape(1, -1))[0],
253
+ method="Anchors-failed"
254
+ ))
255
+
256
+ return explanations
257
+
@@ -0,0 +1,266 @@
1
+ """
2
+ LIME Wrapper for GPE Benchmark.
3
+
4
+ Provides a unified interface for LIME explanations that matches
5
+ the GPE Explanation format.
6
+
7
+ Author: Vladyslav Dehtiarov
8
+ """
9
+
10
+ import numpy as np
11
+ import time
12
+ from typing import List, Optional, Dict, Any
13
+
14
+ # Import GPE structures
15
+ import sys
16
+ sys.path.insert(0, '..')
17
+ from gpe.explanation import Condition, Rule, Explanation
18
+
19
+
20
+ class LIMEWrapper:
21
+ """
22
+ Wrapper for LIME (Local Interpretable Model-agnostic Explanations).
23
+
24
+ Provides a unified interface that converts LIME explanations to
25
+ the GPE Explanation format for comparison.
26
+
27
+ Parameters:
28
+ model: The model to explain
29
+ X_train: Training data for LIME
30
+ feature_names: Feature names
31
+ mode: 'classification' or 'regression'
32
+ n_features: Number of features to include in explanation
33
+
34
+ Example:
35
+ >>> wrapper = LIMEWrapper(model, X_train, feature_names)
36
+ >>> explanation = wrapper.explain(x)
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ model,
42
+ X_train: np.ndarray,
43
+ feature_names: Optional[List[str]] = None,
44
+ mode: str = 'classification',
45
+ n_features: int = 10,
46
+ kernel_width: Optional[float] = None
47
+ ):
48
+ self.model = model
49
+ self.X_train = X_train
50
+ self.mode = mode
51
+ self.n_features = n_features
52
+ self.kernel_width = kernel_width
53
+
54
+ # Set feature names
55
+ if feature_names is None:
56
+ self.feature_names = [f"feature_{i}" for i in range(X_train.shape[1])]
57
+ else:
58
+ self.feature_names = feature_names
59
+
60
+ # Initialize LIME
61
+ self._init_lime()
62
+
63
+ def _init_lime(self):
64
+ """Initialize the LIME explainer."""
65
+ try:
66
+ import lime
67
+ import lime.lime_tabular
68
+ except ImportError:
69
+ raise ImportError(
70
+ "LIME is not installed. Install with: pip install lime"
71
+ )
72
+
73
+ if self.mode == 'classification':
74
+ self.explainer = lime.lime_tabular.LimeTabularExplainer(
75
+ self.X_train,
76
+ feature_names=self.feature_names,
77
+ mode='classification',
78
+ kernel_width=self.kernel_width
79
+ )
80
+ else:
81
+ self.explainer = lime.lime_tabular.LimeTabularExplainer(
82
+ self.X_train,
83
+ feature_names=self.feature_names,
84
+ mode='regression',
85
+ kernel_width=self.kernel_width
86
+ )
87
+
88
+ def explain(
89
+ self,
90
+ x: np.ndarray,
91
+ **kwargs
92
+ ) -> Explanation:
93
+ """
94
+ Generate an explanation using LIME.
95
+
96
+ Args:
97
+ x: Instance to explain
98
+ **kwargs: Additional arguments passed to LIME
99
+
100
+ Returns:
101
+ Explanation object in GPE format
102
+ """
103
+ start_time = time.time()
104
+
105
+ x = np.asarray(x).flatten()
106
+
107
+ # Get LIME explanation
108
+ if self.mode == 'classification':
109
+ lime_exp = self.explainer.explain_instance(
110
+ x,
111
+ self.model.predict_proba,
112
+ num_features=self.n_features,
113
+ **kwargs
114
+ )
115
+ prediction = self.model.predict(x.reshape(1, -1))[0]
116
+ else:
117
+ lime_exp = self.explainer.explain_instance(
118
+ x,
119
+ self.model.predict,
120
+ num_features=self.n_features,
121
+ **kwargs
122
+ )
123
+ prediction = self.model.predict(x.reshape(1, -1))[0]
124
+
125
+ # Convert LIME explanation to GPE format
126
+ conditions = []
127
+
128
+ for feature_weight in lime_exp.as_list():
129
+ # Parse LIME feature string (e.g., "3.50 < feature_0 <= 5.00")
130
+ feature_str = feature_weight[0]
131
+ weight = feature_weight[1]
132
+
133
+ condition = self._parse_lime_condition(feature_str, x)
134
+ if condition is not None:
135
+ conditions.append(condition)
136
+
137
+ # Create rule
138
+ rule = Rule(
139
+ conditions=conditions,
140
+ prediction=prediction
141
+ )
142
+
143
+ computation_time = time.time() - start_time
144
+
145
+ # Calculate approximate precision/coverage
146
+ precision, coverage = self._estimate_metrics(rule, prediction)
147
+
148
+ return Explanation(
149
+ instance=x,
150
+ rule=rule,
151
+ prediction=prediction,
152
+ precision=precision,
153
+ coverage=coverage,
154
+ method="LIME",
155
+ computation_time=computation_time,
156
+ metadata={
157
+ 'lime_score': lime_exp.score if hasattr(lime_exp, 'score') else None,
158
+ 'n_features': self.n_features
159
+ }
160
+ )
161
+
162
+ def _parse_lime_condition(
163
+ self,
164
+ feature_str: str,
165
+ x: np.ndarray
166
+ ) -> Optional[Condition]:
167
+ """
168
+ Parse a LIME feature string into a Condition.
169
+
170
+ LIME format examples:
171
+ - "feature_0 <= 3.50"
172
+ - "3.50 < feature_0 <= 5.00"
173
+ - "feature_1 > 2.00"
174
+ """
175
+ import re
176
+
177
+ # Try to match patterns
178
+ # Pattern 1: "feature <= value" or "feature > value"
179
+ pattern1 = r"([a-zA-Z_][a-zA-Z0-9_]*)\s*(<=|<|>=|>)\s*([\d.-]+)"
180
+ # Pattern 2: "value < feature <= value"
181
+ pattern2 = r"([\d.-]+)\s*<\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*<=\s*([\d.-]+)"
182
+
183
+ match2 = re.match(pattern2, feature_str)
184
+ if match2:
185
+ low = float(match2.group(1))
186
+ feature_name = match2.group(2)
187
+ high = float(match2.group(3))
188
+
189
+ # Find feature index
190
+ try:
191
+ feature_idx = self.feature_names.index(feature_name)
192
+ except ValueError:
193
+ return None
194
+
195
+ # Return the more restrictive condition based on instance value
196
+ value = x[feature_idx]
197
+ if abs(value - low) < abs(value - high):
198
+ return Condition(
199
+ feature_index=feature_idx,
200
+ feature_name=feature_name,
201
+ operator='>',
202
+ threshold=low
203
+ )
204
+ else:
205
+ return Condition(
206
+ feature_index=feature_idx,
207
+ feature_name=feature_name,
208
+ operator='<=',
209
+ threshold=high
210
+ )
211
+
212
+ match1 = re.match(pattern1, feature_str)
213
+ if match1:
214
+ feature_name = match1.group(1)
215
+ operator = match1.group(2)
216
+ threshold = float(match1.group(3))
217
+
218
+ try:
219
+ feature_idx = self.feature_names.index(feature_name)
220
+ except ValueError:
221
+ return None
222
+
223
+ return Condition(
224
+ feature_index=feature_idx,
225
+ feature_name=feature_name,
226
+ operator=operator,
227
+ threshold=threshold
228
+ )
229
+
230
+ return None
231
+
232
+ def _estimate_metrics(
233
+ self,
234
+ rule: Rule,
235
+ prediction: Any
236
+ ) -> tuple:
237
+ """Estimate precision and coverage from training data."""
238
+ mask = rule.evaluate_batch(self.X_train)
239
+
240
+ if mask.sum() == 0:
241
+ return 1.0, 0.0
242
+
243
+ coverage = mask.mean()
244
+
245
+ y_pred = self.model.predict(self.X_train[mask])
246
+ precision = (y_pred == prediction).mean()
247
+
248
+ return precision, coverage
249
+
250
+ def explain_batch(
251
+ self,
252
+ X: np.ndarray,
253
+ verbose: bool = False
254
+ ) -> List[Explanation]:
255
+ """Generate explanations for multiple instances."""
256
+ explanations = []
257
+
258
+ for i, x in enumerate(X):
259
+ if verbose and (i + 1) % 10 == 0:
260
+ print(f"LIME: Explaining instance {i + 1}/{X.shape[0]}")
261
+
262
+ explanation = self.explain(x)
263
+ explanations.append(explanation)
264
+
265
+ return explanations
266
+