import argparse import os import sys from pathlib import Path import numpy as np import tensorflow as tf import matplotlib.pyplot as plt root = Path(__file__).resolve().parents[1] sys.path.append(str(root)) from src.data import get_datasets, prepare_dataset from src.models import get_model from src.utils import make_gradcam_heatmap, overlay_heatmap def parse_args(): parser = argparse.ArgumentParser(description='Generate explainability outputs for brain tumor models') parser.add_argument('--model', choices=['cnn', 'transfer', 'vit'], default='cnn') parser.add_argument('--dataset', default='dataset') parser.add_argument('--weights', required=True) parser.add_argument('--batch_size', type=int, default=8) parser.add_argument('--output', default='artifacts') parser.add_argument('--examples', type=int, default=4) return parser.parse_args() def _get_default_conv_layer(model_type): if model_type == 'cnn': return 'conv_block_3' if model_type == 'transfer': return 'conv5_block3_out' return None def _get_sample_images(dataset, max_examples=4): images = [] labels = [] for batch, (x, y) in enumerate(dataset): for i in range(x.shape[0]): if len(images) >= max_examples: return np.array(images), np.array(labels) images.append(x[i].numpy()) labels.append(int(y[i].numpy())) if len(images) >= max_examples: break return np.array(images), np.array(labels) def _plot_image(image, title, save_path): plt.figure(figsize=(5, 5)) plt.imshow(image.astype('uint8')) plt.title(title) plt.axis('off') plt.tight_layout() plt.savefig(save_path) plt.close() def vit_patch_saliency(model, image, image_size=(224, 224)): """Saliency for the hybrid ResNet50+ViT classifier defined in src/models.py. The hybrid model projects the ResNet50 feature map (7x7) into patch tokens via a 1x1 Conv2D named 'hybrid_patch_projection', then reshapes to a sequence and passes it through transformer blocks. We compute gradient-based saliency on the patch token sequence (after position embedding) and reshape it back to a 7x7 grid before resizing to the input resolution. """ try: token_layer = model.get_layer('hybrid_patch_tokens') except ValueError: token_layer = model.get_layer('hybrid_patch_projection') token_model = tf.keras.Model(inputs=model.inputs, outputs=token_layer.output) image_batch = tf.expand_dims(image, axis=0) with tf.GradientTape() as tape: tokens = token_model(image_batch) tape.watch(tokens) predictions = model(image_batch) loss = predictions[:, 0] grads = tape.gradient(loss, tokens) importance = tf.reduce_mean(tf.abs(grads * tokens), axis=-1) importance = tf.squeeze(importance).numpy() num_tokens = importance.shape[0] if importance.ndim == 1 else importance.size side = int(round(num_tokens ** 0.5)) if side * side != num_tokens: side = max(1, int(np.floor(num_tokens ** 0.5))) importance = importance[: side * side] importance = importance.reshape(side, side) importance = importance / (importance.max() + 1e-8) importance = tf.expand_dims(importance, axis=-1) importance = tf.image.resize(importance, image_size, method='bilinear').numpy() return np.squeeze(importance) def explain_examples(model, model_type, images, labels, output_dir): os.makedirs(output_dir, exist_ok=True) conv_layer = _get_default_conv_layer(model_type) for idx, (image, label) in enumerate(zip(images, labels)): title = f'Label={label}' sample_image = image.astype('uint8') if image.dtype != 'uint8' else image _plot_image(sample_image, f'Input {idx} ({title})', os.path.join(output_dir, f'input_{idx}.png')) if model_type in ['cnn', 'transfer']: heatmap = make_gradcam_heatmap(tf.expand_dims(image, axis=0), model, conv_layer) overlay = overlay_heatmap(sample_image, heatmap) _plot_image(overlay, f'Grad-CAM {idx} ({title})', os.path.join(output_dir, f'gradcam_{idx}.png')) if model_type == 'vit': heatmap = vit_patch_saliency(model, image) overlay = overlay_heatmap(sample_image, heatmap) _plot_image(overlay, f'ViT Patch Saliency {idx} ({title})', os.path.join(output_dir, f'vit_saliency_{idx}.png')) prediction = model.predict(tf.expand_dims(image, axis=0), verbose=0)[0][0] print(f'Example {idx}: true={label}, score={prediction:.4f}') def main(): args = parse_args() train_ds, val_ds, test_ds = get_datasets(args.dataset, batch_size=args.batch_size) eval_ds = test_ds if test_ds is not None else val_ds if eval_ds is None: raise ValueError('No validation or test dataset available for explanation.') eval_ds = prepare_dataset(eval_ds) model = get_model(args.model) model.load_weights(args.weights) images, labels = _get_sample_images(eval_ds, max_examples=args.examples) explain_dir = os.path.join(args.output, args.model, 'explain') explain_examples(model, args.model, images, labels, explain_dir) print(f'Explainability outputs saved to {explain_dir}') if __name__ == '__main__': main()