Spaces:
Sleeping
Sleeping
Upload src\interpretability.py with huggingface_hub
Browse files- 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']}")
|