TonalIQ-Backend / src /models /explain.py
rohan21005's picture
Upload 34 files
2486f9d verified
Raw
History Blame Contribute Delete
4.4 kB
"""
backend/src/models/explain.py
Explainable AI (XAI) engine for Speech Emotion Recognition.
Extracts final self-attention layer weights from Wav2Vec2 and plots them as an overlay on the audio waveform.
"""
import io
import base64
import torch
import numpy as np
import matplotlib
# Use non-interactive backend for server environments
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from transformers import Wav2Vec2Processor, Wav2Vec2ForSequenceClassification
from pathlib import Path
from typing import Dict, Any
from src.models.config import MODEL_NAME, SAMPLE_RATE
def generate_attention_map(waveform: np.ndarray, model: Any, processor: Any) -> str:
"""
Passes waveform through Wav2Vec2, extracts final self-attention weights,
maps attention to timeline, and generates a base64-encoded plot.
"""
try:
device = next(model.parameters()).device
# 1. Run inference & extract attentions
inputs = processor(waveform, sampling_rate=SAMPLE_RATE, return_tensors="pt")
input_values = inputs.input_values.to(device)
# Forward pass with attention tracking
with torch.no_grad():
outputs = model(input_values, output_attentions=True)
# Extract attention tuple: 12 elements (one per layer)
# Final layer index = -1
# Shape: (batch_size, num_heads, seq_len, seq_len)
attentions = outputs.attentions
if not attentions:
raise ValueError("Attentions are empty. Check if model supports output_attentions=True.")
final_layer_att = attentions[-1][0].cpu() # Get batch index 0, shape: (num_heads, seq_len, seq_len)
# 2. Compute Salience Score
# Average attention weight across all heads
mean_att = final_layer_att.mean(dim=0).numpy() # (seq_len, seq_len)
# Sum/average attention received by each target frame
salience = mean_att.mean(axis=0) # (seq_len,)
# Max-normalize the salience curve for visualization
if np.max(salience) > 0:
salience = salience / (np.max(salience) + 1e-8)
# 3. Align Timeline
# Wav2Vec2 downsamples 16kHz audio by factor of 320 (20ms frames)
time_axis_wav = np.arange(len(waveform)) / SAMPLE_RATE
time_axis_att = np.linspace(0, len(waveform) / SAMPLE_RATE, len(salience))
# 4. Generate Plot
fig, ax1 = plt.subplots(figsize=(10, 3.5), dpi=100)
# Set light minimalist style
fig.patch.set_facecolor('#fafafa')
ax1.set_facecolor('#ffffff')
# Plot raw waveform (light gray line)
ax1.plot(time_axis_wav, waveform, color='#d1d5db', alpha=0.8, label="Audio Signal")
ax1.set_xlabel("Time (seconds)", fontsize=9, color='#4b5563')
ax1.set_ylabel("Amplitude", fontsize=9, color='#4b5563')
ax1.tick_params(axis='both', colors='#4b5563', labelsize=8)
# Instantiate second y-axis for attention weight
ax2 = ax1.twinx()
# Smooth attention plot using interpolation
# Color: Stripe/Vercel purple accent
ax2.fill_between(time_axis_att, salience, color='#6366f1', alpha=0.25, label="Model Attention")
ax2.plot(time_axis_att, salience, color='#4f46e5', linewidth=1.5)
ax2.set_ylabel("Attention Salience", fontsize=9, color='#4f46e5')
ax2.tick_params(axis='y', colors='#4f46e5', labelsize=8)
ax1.spines['top'].set_visible(False)
ax2.spines['top'].set_visible(False)
plt.title("Acoustic Temporal Attention Highlight Map", fontsize=10, fontweight='bold', color='#1f2937', pad=12)
fig.tight_layout()
# Save to buffer
buf = io.BytesIO()
plt.savefig(buf, format="png", bbox_inches='tight', facecolor=fig.get_facecolor(), edgecolor='none')
plt.close(fig)
buf.seek(0)
# Encode to Base64
base64_str = base64.b64encode(buf.read()).decode("utf-8")
return f"data:image/png;base64,{base64_str}"
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"XAI saliency generation failed: {str(e)}")
return ""