Spaces:
Runtime error
Runtime error
| """ | |
| Ablation Study Framework for Brain Tumor Detection Models | |
| """ | |
| import numpy as np | |
| import tensorflow as tf | |
| import json | |
| import os | |
| import pandas as pd | |
| from datetime import datetime | |
| from pathlib import Path | |
| import matplotlib.pyplot as plt | |
| import seaborn as sns | |
| class AblationStudy: | |
| """ | |
| Framework for conducting ablation studies on brain tumor detection models | |
| """ | |
| def __init__(self, base_config, results_dir='./ablation_results'): | |
| """ | |
| Initialize ablation study | |
| Args: | |
| base_config: Base configuration dictionary | |
| results_dir: Directory to save results | |
| """ | |
| self.base_config = base_config | |
| self.results_dir = results_dir | |
| self.results = {} | |
| self.study_metadata = { | |
| 'start_time': datetime.now().isoformat(), | |
| 'base_config': base_config, | |
| 'experiments': [] | |
| } | |
| os.makedirs(results_dir, exist_ok=True) | |
| def add_experiment(self, name, config_modification, description=""): | |
| """ | |
| Add an experiment configuration | |
| Args: | |
| name: Name of the experiment | |
| config_modification: Dictionary of config modifications | |
| description: Description of what this experiment tests | |
| """ | |
| config = self.base_config.copy() | |
| config.update(config_modification) | |
| self.study_metadata['experiments'].append({ | |
| 'name': name, | |
| 'config': config, | |
| 'description': description, | |
| 'modifications': config_modification | |
| }) | |
| def run_experiment(self, experiment_idx, model_builder, data, metrics_calculator): | |
| """ | |
| Run a single ablation experiment | |
| Args: | |
| experiment_idx: Index of experiment in study_metadata['experiments'] | |
| model_builder: Function to build model with given config | |
| data: Tuple of (X_train, y_train, X_val, y_val) | |
| metrics_calculator: Function to calculate metrics | |
| Returns: | |
| Results dictionary | |
| """ | |
| experiment = self.study_metadata['experiments'][experiment_idx] | |
| config = experiment['config'] | |
| name = experiment['name'] | |
| print(f"\n{'='*60}") | |
| print(f"Running Ablation Experiment: {name}") | |
| print(f"{'='*60}") | |
| print(f"Description: {experiment['description']}") | |
| print(f"Modifications: {experiment['modifications']}") | |
| # Build model | |
| model = model_builder(config) | |
| # Train model | |
| history = self._train_model(model, data, config) | |
| # Evaluate | |
| metrics = metrics_calculator(model, data) | |
| # Store results | |
| self.results[name] = { | |
| 'metrics': metrics, | |
| 'history': history.history if hasattr(history, 'history') else history, | |
| 'config': config | |
| } | |
| print(f"Experiment {name} completed. Metrics: {metrics}") | |
| return self.results[name] | |
| def _train_model(self, model, data, config): | |
| """ | |
| Train model with given configuration | |
| Args: | |
| model: Model to train | |
| data: Training data tuple | |
| config: Training configuration | |
| Returns: | |
| Training history | |
| """ | |
| X_train, y_train, X_val, y_val = data | |
| # Compile model | |
| model.compile( | |
| optimizer=tf.keras.optimizers.Adam(learning_rate=config.get('learning_rate', 1e-4)), | |
| loss=config.get('loss_fn', 'binary_crossentropy'), | |
| metrics=config.get('metrics', ['accuracy']) | |
| ) | |
| # Callbacks | |
| callbacks = [ | |
| tf.keras.callbacks.EarlyStopping( | |
| monitor='val_loss', | |
| patience=config.get('patience', 10), | |
| restore_best_weights=True | |
| ), | |
| tf.keras.callbacks.ReduceLROnPlateau( | |
| monitor='val_loss', | |
| factor=0.5, | |
| patience=5, | |
| min_lr=1e-7 | |
| ) | |
| ] | |
| # Train | |
| history = model.fit( | |
| X_train, | |
| y_train, | |
| validation_data=(X_val, y_val), | |
| epochs=config.get('epochs', 50), | |
| batch_size=config.get('batch_size', 32), | |
| callbacks=callbacks, | |
| verbose=1 | |
| ) | |
| return history | |
| def run_all_experiments(self, model_builder, data, metrics_calculator): | |
| """ | |
| Run all experiments in the study | |
| Args: | |
| model_builder: Function to build model with given config | |
| data: Training data tuple | |
| metrics_calculator: Function to calculate metrics | |
| """ | |
| for i in range(len(self.study_metadata['experiments'])): | |
| self.run_experiment(i, model_builder, data, metrics_calculator) | |
| # Save results | |
| self.save_results() | |
| return self.results | |
| def save_results(self): | |
| """Save ablation study results""" | |
| # Save summary | |
| summary_path = os.path.join(self.results_dir, 'ablation_summary.json') | |
| with open(summary_path, 'w') as f: | |
| json.dump({ | |
| 'study_metadata': self.study_metadata, | |
| 'results': self.results | |
| }, f, indent=2) | |
| # Save detailed results as CSV | |
| results_data = [] | |
| for name, result in self.results.items(): | |
| row = {'experiment': name} | |
| row.update(result['metrics']) | |
| results_data.append(row) | |
| results_df = pd.DataFrame(results_data) | |
| results_df.to_csv(os.path.join(self.results_dir, 'ablation_results.csv'), index=False) | |
| # Save plots | |
| self.plot_results() | |
| print(f"Results saved to {self.results_dir}") | |
| def plot_results(self): | |
| """Plot ablation study results""" | |
| if not self.results: | |
| return | |
| # Extract metrics | |
| experiments = list(self.results.keys()) | |
| metrics_names = list(list(self.results.values())[0]['metrics'].keys()) | |
| # Create subplots for each metric | |
| fig, axes = plt.subplots(1, len(metrics_names), figsize=(6*len(metrics_names), 5)) | |
| if len(metrics_names) == 1: | |
| axes = [axes] | |
| for ax, metric_name in zip(axes, metrics_names): | |
| values = [self.results[exp]['metrics'][metric_name] for exp in experiments] | |
| # Create bar plot | |
| bars = ax.bar(experiments, values, color=plt.cm.Set3(np.linspace(0, 1, len(experiments)))) | |
| ax.set_title(metric_name.replace('_', ' ').title()) | |
| ax.set_ylabel(metric_name) | |
| ax.tick_params(axis='x', rotation=45) | |
| # Add value labels on bars | |
| for bar, value in zip(bars, values): | |
| ax.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.001, | |
| f'{value:.4f}', ha='center', va='bottom', fontsize=9) | |
| plt.tight_layout() | |
| plt.savefig(os.path.join(self.results_dir, 'ablation_results.png'), dpi=300, bbox_inches='tight') | |
| plt.close() | |
| # Plot training histories | |
| if len(experiments) > 0 and 'history' in self.results[experiments[0]]: | |
| self._plot_training_histories(experiments) | |
| def _plot_training_histories(self, experiments): | |
| """Plot training histories for all experiments""" | |
| fig, axes = plt.subplots(1, 2, figsize=(15, 5)) | |
| for exp in experiments: | |
| history = self.results[exp]['history'] | |
| if 'loss' in history: | |
| axes[0].plot(history['loss'], label=f'{exp} - train') | |
| if 'val_loss' in history: | |
| axes[0].plot(history['val_loss'], label=f'{exp} - val', linestyle='--') | |
| if 'accuracy' in history: | |
| axes[1].plot(history['accuracy'], label=f'{exp} - train') | |
| if 'val_accuracy' in history: | |
| axes[1].plot(history['val_accuracy'], label=f'{exp} - val', linestyle='--') | |
| axes[0].set_title('Loss') | |
| axes[0].set_xlabel('Epoch') | |
| axes[0].set_ylabel('Loss') | |
| axes[0].legend(bbox_to_anchor=(1.05, 1), loc='upper left') | |
| axes[1].set_title('Accuracy') | |
| axes[1].set_xlabel('Epoch') | |
| axes[1].set_ylabel('Accuracy') | |
| axes[1].legend(bbox_to_anchor=(1.05, 1), loc='upper left') | |
| plt.tight_layout() | |
| plt.savefig(os.path.join(self.results_dir, 'training_histories.png'), dpi=300, bbox_inches='tight') | |
| plt.close() | |
| def get_comparison_table(self): | |
| """ | |
| Get comparison table of all experiments | |
| Returns: | |
| Pandas DataFrame with comparison results | |
| """ | |
| if not self.results: | |
| raise ValueError("No results available. Run experiments first.") | |
| rows = [] | |
| for name, result in self.results.items(): | |
| row = {'Experiment': name} | |
| row.update(result['metrics']) | |
| row['Description'] = next( | |
| (exp['description'] for exp in self.study_metadata['experiments'] | |
| if exp['name'] == name), '' | |
| ) | |
| rows.append(row) | |
| return pd.DataFrame(rows) | |
| class SegmentationAblationStudy(AblationStudy): | |
| """ | |
| Ablation study framework specifically for segmentation models | |
| """ | |
| def __init__(self, base_config, results_dir='./segmentation_ablation_results'): | |
| super().__init__(base_config, results_dir) | |
| def add_segmentation_experiment(self, name, model_config, training_config, description=""): | |
| """ | |
| Add a segmentation experiment | |
| Args: | |
| name: Experiment name | |
| model_config: Model configuration modifications | |
| training_config: Training configuration modifications | |
| description: Description of the experiment | |
| """ | |
| config = { | |
| **self.base_config, | |
| **model_config, | |
| **training_config | |
| } | |
| self.study_metadata['experiments'].append({ | |
| 'name': name, | |
| 'config': config, | |
| 'description': description, | |
| 'model_modifications': model_config, | |
| 'training_modifications': training_config | |
| }) | |
| def run_segmentation_experiment(self, experiment_idx, model_builder, data, metrics_calculator): | |
| """ | |
| Run a segmentation experiment | |
| Args: | |
| experiment_idx: Index of experiment | |
| model_builder: Function to build model | |
| data: Tuple of (X_train, y_train, X_val, y_val) where y are masks | |
| metrics_calculator: Function to calculate segmentation metrics | |
| Returns: | |
| Results dictionary | |
| """ | |
| experiment = self.study_metadata['experiments'][experiment_idx] | |
| config = experiment['config'] | |
| name = experiment['name'] | |
| print(f"\n{'='*60}") | |
| print(f"Running Segmentation Ablation: {name}") | |
| print(f"{'='*60}") | |
| # Build model | |
| model = model_builder(config) | |
| # Compile | |
| model.compile( | |
| optimizer=tf.keras.optimizers.Adam(learning_rate=config.get('learning_rate', 1e-4)), | |
| loss=config.get('loss_fn', 'binary_crossentropy'), | |
| metrics=[ | |
| tf.keras.metrics.MeanIoU(num_classes=2), | |
| 'accuracy' | |
| ] | |
| ) | |
| # Train | |
| X_train, y_train, X_val, y_val = data | |
| callbacks = [ | |
| tf.keras.callbacks.EarlyStopping( | |
| monitor='val_loss', | |
| patience=config.get('patience', 10), | |
| restore_best_weights=True | |
| ) | |
| ] | |
| history = model.fit( | |
| X_train, | |
| y_train, | |
| validation_data=(X_val, y_val), | |
| epochs=config.get('epochs', 100), | |
| batch_size=config.get('batch_size', 16), | |
| callbacks=callbacks | |
| ) | |
| # Evaluate with custom metrics | |
| metrics = metrics_calculator(model, (X_val, y_val)) | |
| # Store results | |
| self.results[name] = { | |
| 'metrics': metrics, | |
| 'history': history.history, | |
| 'config': config | |
| } | |
| print(f"Segmentation ablation {name} completed. Metrics: {metrics}") | |
| return self.results[name] | |
| def create_attention_ablation_study(base_config, results_dir='./attention_ablation'): | |
| """ | |
| Create an ablation study for attention mechanisms | |
| Args: | |
| base_config: Base configuration | |
| results_dir: Directory to save results | |
| Returns: | |
| AblationStudy instance with experiments added | |
| """ | |
| study = AblationStudy(base_config, results_dir) | |
| # Baseline without attention | |
| study.add_experiment( | |
| name='baseline_no_attention', | |
| config_modification={'use_attention': False}, | |
| description='Baseline model without any attention mechanisms' | |
| ) | |
| # With attention gates in skip connections | |
| study.add_experiment( | |
| name='attention_skip_connections', | |
| config_modification={'use_attention': True}, | |
| description='Model with attention gates in U-Net skip connections' | |
| ) | |
| # With channel attention | |
| study.add_experiment( | |
| name='channel_attention', | |
| config_modification={'attention_type': 'channel'}, | |
| description='Model with channel-wise attention mechanism' | |
| ) | |
| # With spatial attention | |
| study.add_experiment( | |
| name='spatial_attention', | |
| config_modification={'attention_type': 'spatial'}, | |
| description='Model with spatial attention mechanism' | |
| ) | |
| # With both channel and spatial attention | |
| study.add_experiment( | |
| name='cbam_attention', | |
| config_modification={'attention_type': 'cbam'}, | |
| description='Model with combined channel and spatial attention (CBAM)' | |
| ) | |
| return study | |
| def create_architecture_ablation_study(base_config, results_dir='./architecture_ablation'): | |
| """ | |
| Create an ablation study for architecture variations | |
| Args: | |
| base_config: Base configuration | |
| results_dir: Directory to save results | |
| Returns: | |
| AblationStudy instance with experiments added | |
| """ | |
| study = AblationStudy(base_config, results_dir) | |
| # Baseline | |
| study.add_experiment( | |
| name='baseline', | |
| config_modification={}, | |
| description='Baseline architecture' | |
| ) | |
| # Different depths | |
| study.add_experiment( | |
| name='shallow_network', | |
| config_modification={'num_layers': 3}, | |
| description='Shallower network with fewer layers' | |
| ) | |
| study.add_experiment( | |
| name='deep_network', | |
| config_modification={'num_layers': 6}, | |
| description='Deeper network with more layers' | |
| ) | |
| # Different filter sizes | |
| study.add_experiment( | |
| name='smaller_filters', | |
| config_modification={'base_filters': 32}, | |
| description='Network with smaller base number of filters' | |
| ) | |
| study.add_experiment( | |
| name='larger_filters', | |
| config_modification={'base_filters': 128}, | |
| description='Network with larger base number of filters' | |
| ) | |
| # With residual connections | |
| study.add_experiment( | |
| name='residual_connections', | |
| config_modification={'use_residual': True}, | |
| description='Network with residual connections' | |
| ) | |
| # With dense connections | |
| study.add_experiment( | |
| name='dense_connections', | |
| config_modification={'use_dense': True}, | |
| description='Network with dense connections' | |
| ) | |
| return study | |
| def create_loss_ablation_study(base_config, results_dir='./loss_ablation'): | |
| """ | |
| Create an ablation study for different loss functions | |
| Args: | |
| base_config: Base configuration | |
| results_dir: Directory to save results | |
| Returns: | |
| AblationStudy instance with experiments added | |
| """ | |
| study = AblationStudy(base_config, results_dir) | |
| # Baseline cross-entropy | |
| study.add_experiment( | |
| name='cross_entropy', | |
| config_modification={'loss_fn': 'binary_crossentropy'}, | |
| description='Standard binary cross-entropy loss' | |
| ) | |
| # Dice loss | |
| study.add_experiment( | |
| name='dice_loss', | |
| config_modification={'loss_fn': 'dice_loss'}, | |
| description='Dice loss for better handling of class imbalance' | |
| ) | |
| # Combined loss | |
| study.add_experiment( | |
| name='combined_dice_bce', | |
| config_modification={'loss_fn': 'combined_dice_bce', 'loss_weights': [0.5, 0.5]}, | |
| description='Combined Dice and BCE loss' | |
| ) | |
| # Focal loss | |
| study.add_experiment( | |
| name='focal_loss', | |
| config_modification={'loss_fn': 'focal_loss', 'focal_gamma': 2.0}, | |
| description='Focal loss for hard example mining' | |
| ) | |
| # Tversky loss | |
| study.add_experiment( | |
| name='tversky_loss', | |
| config_modification={'loss_fn': 'tversky_loss', 'alpha': 0.5, 'beta': 0.5}, | |
| description='Tversky loss for imbalanced segmentation' | |
| ) | |
| return study | |
| def create_data_augmentation_ablation_study(base_config, results_dir='./augmentation_ablation'): | |
| """ | |
| Create an ablation study for data augmentation strategies | |
| Args: | |
| base_config: Base configuration | |
| results_dir: Directory to save results | |
| Returns: | |
| AblationStudy instance with experiments added | |
| """ | |
| study = AblationStudy(base_config, results_dir) | |
| # No augmentation | |
| study.add_experiment( | |
| name='no_augmentation', | |
| config_modification={'use_augmentation': False}, | |
| description='No data augmentation' | |
| ) | |
| # Basic augmentation | |
| study.add_experiment( | |
| name='basic_augmentation', | |
| config_modification={ | |
| 'use_augmentation': True, | |
| 'augmentation': ['flip', 'rotation'] | |
| }, | |
| description='Basic augmentation: flips and rotations' | |
| ) | |
| # Advanced augmentation | |
| study.add_experiment( | |
| name='advanced_augmentation', | |
| config_modification={ | |
| 'use_augmentation': True, | |
| 'augmentation': ['flip', 'rotation', 'zoom', 'contrast', 'brightness'] | |
| }, | |
| description='Advanced augmentation with multiple transformations' | |
| ) | |
| # With MixUp | |
| study.add_experiment( | |
| name='mixup_augmentation', | |
| config_modification={ | |
| 'use_augmentation': True, | |
| 'augmentation': ['flip', 'rotation', 'mixup'], | |
| 'mixup_alpha': 0.2 | |
| }, | |
| description='Augmentation with MixUp strategy' | |
| ) | |
| # With CutMix | |
| study.add_experiment( | |
| name='cutmix_augmentation', | |
| config_modification={ | |
| 'use_augmentation': True, | |
| 'augmentation': ['flip', 'rotation', 'cutmix'], | |
| 'cutmix_alpha': 1.0 | |
| }, | |
| description='Augmentation with CutMix strategy' | |
| ) | |
| return study | |
| def calculate_segmentation_metrics(model, data, thresholds=None): | |
| """ | |
| Calculate comprehensive segmentation metrics | |
| Args: | |
| model: Trained segmentation model | |
| data: Tuple of (X_val, y_val) | |
| thresholds: List of thresholds for binary classification | |
| Returns: | |
| Dictionary of metrics | |
| """ | |
| from sklearn.metrics import jaccard_score, confusion_matrix | |
| def _dice_score(y_true, y_pred, smooth=1e-6): | |
| y_true = np.asarray(y_true).ravel().astype(np.float32) | |
| y_pred = np.asarray(y_pred).ravel().astype(np.float32) | |
| intersection = float(np.sum(y_true * y_pred)) | |
| return (2.0 * intersection + smooth) / (float(np.sum(y_true) + np.sum(y_pred)) + smooth) | |
| X_val, y_val = data | |
| # Predict | |
| y_pred = model.predict(X_val) | |
| # Use threshold of 0.5 by default | |
| if thresholds is None: | |
| thresholds = [0.5] | |
| metrics = {} | |
| for threshold in thresholds: | |
| y_pred_binary = (y_pred >= threshold).astype(int) | |
| # Flatten for metric calculation | |
| y_val_flat = y_val.flatten() | |
| y_pred_flat = y_pred_binary.flatten() | |
| # Dice coefficient (local implementation; sklearn has no dice_score) | |
| dice = _dice_score(y_val_flat, y_pred_flat) | |
| # IoU (Jaccard index) | |
| iou = jaccard_score(y_val_flat, y_pred_flat) | |
| # Precision, Recall, F1 | |
| tn, fp, fn, tp = confusion_matrix(y_val_flat, y_pred_flat).ravel() | |
| precision = tp / (tp + fp) if (tp + fp) > 0 else 0 | |
| recall = tp / (tp + fn) if (tp + fn) > 0 else 0 | |
| f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0 | |
| # Specificity | |
| specificity = tn / (tn + fp) if (tn + fp) > 0 else 0 | |
| metrics[f'dice_t{threshold}'] = float(dice) | |
| metrics[f'iou_t{threshold}'] = float(iou) | |
| metrics[f'precision_t{threshold}'] = float(precision) | |
| metrics[f'recall_t{threshold}'] = float(recall) | |
| metrics[f'f1_t{threshold}'] = float(f1) | |
| metrics[f'specificity_t{threshold}'] = float(specificity) | |
| return metrics |