Asmitha-28 commited on
Commit
b4b4d46
·
verified ·
1 Parent(s): 6f14c50

Upload src\interpretability.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. src//interpretability.py +137 -0
src//interpretability.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/interpretability.py
2
+ # Model Interpretability Module — SHAP Explanations
3
+ # SupportMind v1.0 — Asmitha
4
+
5
+ import torch
6
+ import shap
7
+ import numpy as np
8
+ from transformers import pipeline
9
+ import logging
10
+ from typing import Dict, List, Any
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class SupportMindExplainer:
15
+ """
16
+ Provides SHAP-based explanations for DistilBERT predictions.
17
+
18
+ Helps support agents understand WHY a ticket was routed to a specific
19
+ category by highlighting the most influential words.
20
+ """
21
+
22
+ def __init__(self, model, tokenizer, device='cpu'):
23
+ self.model = model
24
+ self.tokenizer = tokenizer
25
+ self.device = device
26
+
27
+ # Create a transformers pipeline for SHAP
28
+ self.pipe = pipeline(
29
+ "text-classification",
30
+ model=self.model,
31
+ tokenizer=self.tokenizer,
32
+ device=0 if device == 'cuda' else -1,
33
+ top_k=None, # Get all class probabilities
34
+ framework="pt" # Force PyTorch
35
+ )
36
+
37
+
38
+ # Initialize SHAP explainer
39
+ # We use a simple wrap to make it compatible with SHAP's expectations
40
+ def predictor(texts):
41
+ # Convert numpy array to list if necessary for transformers pipeline
42
+ if isinstance(texts, np.ndarray):
43
+ texts = texts.tolist()
44
+ outputs = self.pipe(texts, batch_size=32)
45
+ # SHAP expects a matrix of [num_samples, num_classes]
46
+ # Outputs is a list of lists of dicts: [[{'label': 'LABEL_0', 'score': 0.1}, ...], ...]
47
+ # We need to ensure the order matches the CATEGORY_MAP
48
+ from confidence_router import CATEGORY_MAP, CATEGORY_REVERSE
49
+
50
+ num_classes = len(CATEGORY_MAP)
51
+ results = np.zeros((len(texts), num_classes))
52
+
53
+ for i, out in enumerate(outputs):
54
+ for item in out:
55
+ # HF labels are usually 'LABEL_N' or the actual category names
56
+ label = item['label']
57
+ score = item['score']
58
+
59
+ # If label is 'LABEL_N'
60
+ if label.startswith('LABEL_'):
61
+ idx = int(label.split('_')[1])
62
+ results[i, idx] = score
63
+ # If label is the category name
64
+ elif label in CATEGORY_REVERSE:
65
+ idx = CATEGORY_REVERSE[label]
66
+ results[i, idx] = score
67
+
68
+ return results
69
+
70
+ # SHAP Explainer for text
71
+ # Using a small masker for performance
72
+ self.explainer = shap.Explainer(predictor, self.tokenizer)
73
+
74
+ def explain(self, text: str, target_class_idx: int = None) -> Dict[str, Any]:
75
+ """
76
+ Generate SHAP values for a single ticket.
77
+
78
+ Args:
79
+ text: The ticket text.
80
+ target_class_idx: The class index to explain. If None, uses the predicted class.
81
+
82
+ Returns:
83
+ Dictionary with tokens and their corresponding SHAP values.
84
+ """
85
+ try:
86
+ # Generate SHAP values
87
+ # This can be slow for long texts, but for tickets (~128 tokens) it's manageable.
88
+ # Capped max_evals to 500 to ensure fast response times during demos.
89
+ shap_values = self.explainer([text], max_evals=500)
90
+
91
+ # If target_class_idx is not provided, use the one with highest mean SHAP value
92
+ if target_class_idx is None:
93
+ # shap_values.values has shape [samples, tokens, classes]
94
+ # We take the class that has the highest average value for this sample
95
+ target_class_idx = np.argmax(np.mean(np.abs(shap_values.values[0]), axis=0))
96
+
97
+ # Extract tokens and values for the target class
98
+ # shap_values[sample_idx, :, class_idx]
99
+ values = shap_values.values[0, :, target_class_idx]
100
+ base_value = float(shap_values.base_values[0, target_class_idx])
101
+
102
+ # SHAP returns tokens as they are produced by the tokenizer (e.g. '##ing', ' [CLS]')
103
+ # We want to map these back to something readable if possible, but raw tokens are okay for highlighting
104
+ tokens = self.tokenizer.convert_ids_to_tokens(self.tokenizer.encode(text))
105
+
106
+ # Filter out special tokens like [CLS], [SEP], [PAD] for the final output
107
+ # but keep the alignment
108
+ result_tokens = []
109
+ result_values = []
110
+
111
+ for token, val in zip(tokens, values):
112
+ if token in [self.tokenizer.cls_token, self.tokenizer.sep_token, self.tokenizer.pad_token]:
113
+ continue
114
+ result_tokens.append(token)
115
+ result_values.append(float(val))
116
+
117
+ return {
118
+ 'tokens': result_tokens,
119
+ 'values': result_values,
120
+ 'base_value': base_value,
121
+ 'target_class': target_class_idx,
122
+ 'prediction_value': float(base_value + np.sum(values))
123
+ }
124
+ except Exception as e:
125
+ logger.error(f"SHAP explanation failed: {e}")
126
+ return {'error': str(e)}
127
+
128
+ if __name__ == '__main__':
129
+ # Test
130
+ from confidence_router import ConfidenceGatedRouter
131
+ router = ConfidenceGatedRouter()
132
+ explainer = SupportMindExplainer(router.model, router.tokenizer)
133
+
134
+ test_text = "My invoice is wrong, please fix the billing error."
135
+ res = explainer.explain(test_text)
136
+ print(f"Tokens: {res['tokens']}")
137
+ print(f"Values: {res['values']}")