Spaces:
Runtime error
Runtime error
| """Explainability helpers for sequential sequence-classification models.""" | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from typing import Dict, List, Optional | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| import torch | |
| from src.models.base_sequential_models import LSTMModel, TransformerModel | |
| class SequenceExplanation: | |
| """Container for a model explanation artifact.""" | |
| predicted_class: int | |
| confidence: float | |
| frame_importance: np.ndarray | |
| attention_weights: Optional[np.ndarray] = None | |
| def _to_tensor(sequence: np.ndarray, device: str) -> torch.Tensor: | |
| tensor = torch.as_tensor(sequence, dtype=torch.float32, device=device) | |
| if tensor.ndim == 2: | |
| tensor = tensor.unsqueeze(0) | |
| if tensor.ndim != 3: | |
| raise ValueError( | |
| f"Expected a (seq_len, features) or (batch, seq_len, features) array, got {tuple(tensor.shape)}" | |
| ) | |
| return tensor | |
| def _gradients_to_importance(sequence_grad: torch.Tensor) -> np.ndarray: | |
| importance = sequence_grad.detach().abs().mean(dim=-1).squeeze(0) | |
| return importance.cpu().numpy().astype(np.float32) | |
| def explain_sequence_model( | |
| model: torch.nn.Module, | |
| sequence: np.ndarray, | |
| device: str = "cpu", | |
| ) -> SequenceExplanation: | |
| """Return class prediction plus a per-frame importance vector.""" | |
| model = model.to(device) | |
| # First run a deterministic forward in eval mode to get the predicted class | |
| with torch.no_grad(): | |
| model.eval() | |
| eval_inputs = _to_tensor(sequence, device) | |
| logits_eval = model(eval_inputs) | |
| probabilities = torch.softmax(logits_eval, dim=1) | |
| confidence, prediction = torch.max(probabilities, dim=1) | |
| predicted_index = int(prediction.item()) | |
| confidence_value = float(confidence.item()) | |
| # For backward on cuDNN RNNs, the model must be in training mode. | |
| # Re-run the forward with grad enabled in training mode, then backprop. | |
| model.train() | |
| inputs = _to_tensor(sequence, device) | |
| inputs.requires_grad_(True) | |
| model.zero_grad(set_to_none=True) | |
| logits = model(inputs) | |
| logits[0, predicted_index].backward() | |
| frame_importance = _gradients_to_importance(inputs.grad) | |
| attention_weights: Optional[np.ndarray] = None | |
| if isinstance(model, LSTMModel): | |
| with torch.no_grad(): | |
| lstm_out, _ = model.lstm(inputs.detach()) | |
| _, weights = model.attention(lstm_out) | |
| attention_weights = weights.squeeze(0).squeeze(-1).cpu().numpy().astype(np.float32) | |
| return SequenceExplanation( | |
| predicted_class=predicted_index, | |
| confidence=confidence_value, | |
| frame_importance=frame_importance, | |
| attention_weights=attention_weights, | |
| ) | |
| def save_sequence_explanation( | |
| explanation: SequenceExplanation, | |
| output_path: str | Path, | |
| title: str = "Sequence explanation", | |
| ) -> Path: | |
| """Save a compact bar plot showing frame importance and optional attention.""" | |
| output_path = Path(output_path) | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| frames = np.arange(len(explanation.frame_importance)) | |
| figure, axis = plt.subplots(figsize=(10, 4)) | |
| axis.bar(frames, explanation.frame_importance, color="#2f6fed", alpha=0.9, label="Gradient saliency") | |
| if explanation.attention_weights is not None: | |
| axis.plot(frames, explanation.attention_weights, color="#ff7a59", linewidth=2.0, marker="o", label="Attention") | |
| axis.set_title(f"{title} | class={explanation.predicted_class} | conf={explanation.confidence:.3f}") | |
| axis.set_xlabel("Frame index") | |
| axis.set_ylabel("Importance") | |
| axis.legend(loc="upper right") | |
| figure.tight_layout() | |
| figure.savefig(output_path, dpi=180, bbox_inches="tight") | |
| plt.close(figure) | |
| return output_path | |
| def summarize_sequence_explanation(explanation: SequenceExplanation) -> Dict[str, List[float] | int | float]: | |
| """Convert an explanation into a JSON-serializable dictionary.""" | |
| summary: Dict[str, List[float] | int | float] = { | |
| "predicted_class": explanation.predicted_class, | |
| "confidence": explanation.confidence, | |
| "frame_importance": explanation.frame_importance.tolist(), | |
| } | |
| if explanation.attention_weights is not None: | |
| summary["attention_weights"] = explanation.attention_weights.tolist() | |
| return summary |