Spaces:
Runtime error
Runtime error
| """ | |
| Training Script for Brain Tumor Segmentation Models | |
| """ | |
| import argparse | |
| import sys | |
| import numpy as np | |
| import tensorflow as tf | |
| from pathlib import Path | |
| import json | |
| import os | |
| from datetime import datetime | |
| import matplotlib.pyplot as plt | |
| _REPO_ROOT = Path(__file__).resolve().parents[1] | |
| if str(_REPO_ROOT) not in sys.path: | |
| sys.path.append(str(_REPO_ROOT)) | |
| from src.segmentation_models import ( | |
| build_unet, | |
| build_attention_unet, | |
| build_res_unet, | |
| build_multi_modal_unet, | |
| dice_coefficient, | |
| dice_loss, | |
| combined_loss, | |
| iou_metric, | |
| ) | |
| from src.kfold_validation import SegmentationKFoldValidator, prepare_data_for_kfold | |
| from src.ablation_study import ( | |
| SegmentationAblationStudy, | |
| calculate_segmentation_metrics, | |
| create_attention_ablation_study, | |
| create_architecture_ablation_study, | |
| create_loss_ablation_study, | |
| ) | |
| def get_model(config): | |
| """ | |
| Build model based on configuration | |
| Args: | |
| config: Configuration dictionary | |
| Returns: | |
| Compiled model | |
| """ | |
| model_type = config.get('model_type', 'unet') | |
| input_shape = config.get('input_shape', (224, 224, 3)) | |
| num_classes = config.get('num_classes', 1) | |
| base_filters = config.get('base_filters', 64) | |
| dropout_rate = config.get('dropout_rate', 0.2) | |
| use_attention = config.get('use_attention', False) | |
| if model_type == 'unet': | |
| model = build_unet( | |
| input_shape=input_shape, | |
| num_classes=num_classes, | |
| base_filters=base_filters, | |
| dropout_rate=dropout_rate, | |
| use_attention=use_attention | |
| ) | |
| elif model_type == 'attention_unet': | |
| model = build_attention_unet( | |
| input_shape=input_shape, | |
| num_classes=num_classes, | |
| base_filters=base_filters, | |
| dropout_rate=dropout_rate | |
| ) | |
| elif model_type == 'res_unet': | |
| model = build_res_unet( | |
| input_shape=input_shape, | |
| num_classes=num_classes, | |
| base_filters=base_filters, | |
| dropout_rate=dropout_rate | |
| ) | |
| elif model_type == 'multi_modal_unet': | |
| input_shapes = config.get('input_shapes', [(224, 224, 3), (224, 224, 3)]) | |
| fusion_method = config.get('fusion_method', 'attention') | |
| model = build_multi_modal_unet( | |
| input_shapes=input_shapes, | |
| num_classes=num_classes, | |
| base_filters=base_filters, | |
| dropout_rate=dropout_rate, | |
| fusion_method=fusion_method | |
| ) | |
| else: | |
| raise ValueError(f"Unknown model type: {model_type}") | |
| return model | |
| def compile_model(model, config): | |
| """ | |
| Compile model with loss function and metrics | |
| Args: | |
| model: Model to compile | |
| config: Configuration dictionary | |
| """ | |
| loss_fn = config.get('loss_fn', 'dice_bce') | |
| learning_rate = config.get('learning_rate', 1e-4) | |
| # Get loss function | |
| if loss_fn == 'dice_bce': | |
| loss = combined_loss(weights=[0.5, 0.5]) | |
| elif loss_fn == 'dice': | |
| loss = dice_loss | |
| elif loss_fn == 'bce': | |
| loss = 'binary_crossentropy' | |
| elif loss_fn == 'focal': | |
| gamma = config.get('focal_gamma', 2.0) | |
| def focal_loss(y_true, y_pred): | |
| y_pred = tf.clip_by_value(y_pred, 1e-7, 1 - 1e-7) | |
| cross_entropy = -y_true * tf.math.log(y_pred) | |
| focal_weight = tf.pow(1 - y_pred, gamma) * y_true + tf.pow(y_pred, gamma) * (1 - y_true) | |
| return cross_entropy * focal_weight | |
| loss = focal_loss | |
| else: | |
| loss = 'binary_crossentropy' | |
| # Compile | |
| model.compile( | |
| optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate), | |
| loss=loss, | |
| metrics=[ | |
| dice_coefficient, | |
| iou_metric, | |
| 'accuracy' | |
| ] | |
| ) | |
| return model | |
| def load_data(config): | |
| """ | |
| Load training data | |
| Args: | |
| config: Configuration dictionary | |
| Returns: | |
| Tuple of (images, masks) or (X_train, y_train, X_val, y_val) | |
| """ | |
| data_dir = Path(config.get('data_dir', './dataset')) | |
| image_size = tuple(config.get('image_size', (224, 224))) | |
| # Check for pre-split data | |
| train_dir = data_dir / 'train' | |
| val_dir = data_dir / 'val' | |
| if train_dir.exists() and val_dir.exists(): | |
| # Load pre-split data | |
| X_train, y_train = load_images_and_masks(train_dir, image_size) | |
| X_val, y_val = load_images_and_masks(val_dir, image_size) | |
| return X_train, y_train, X_val, y_val | |
| else: | |
| # Load all data and split | |
| images, masks = load_images_and_masks(data_dir, image_size) | |
| return images, masks | |
| def load_images_and_masks(data_dir, image_size): | |
| """ | |
| Load images and masks from directory | |
| Args: | |
| data_dir: Directory containing images and masks subdirectories | |
| image_size: Size to resize images to | |
| Returns: | |
| Tuple of (images array, masks array) | |
| """ | |
| import cv2 | |
| images_dir = Path(data_dir) / 'images' | |
| masks_dir = Path(data_dir) / 'masks' | |
| if not images_dir.exists(): | |
| # Try loading directly from data_dir | |
| images_dir = Path(data_dir) | |
| masks_dir = Path(data_dir) | |
| # Get file lists | |
| image_files = sorted(list(images_dir.glob('*.jpg')) + list(images_dir.glob('*.png'))) | |
| mask_files = sorted(list(masks_dir.glob('*.jpg')) + list(masks_dir.glob('*.png'))) | |
| if len(image_files) != len(mask_files): | |
| raise ValueError(f"Mismatch between images ({len(image_files)}) and masks ({len(mask_files)})") | |
| # Load images | |
| images = [] | |
| masks = [] | |
| for img_path, mask_path in zip(image_files, mask_files): | |
| # Load image | |
| img = cv2.imread(str(img_path)) | |
| img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) | |
| img = cv2.resize(img, image_size) | |
| images.append(img) | |
| # Load mask | |
| mask = cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) | |
| mask = cv2.resize(mask, image_size) | |
| mask = mask.astype(np.float32) / 255.0 | |
| mask = np.expand_dims(mask, axis=-1) | |
| masks.append(mask) | |
| return np.array(images), np.array(masks) | |
| def create_callbacks(config, save_dir): | |
| """ | |
| Create training callbacks | |
| Args: | |
| config: Configuration dictionary | |
| save_dir: Directory to save model checkpoints | |
| Returns: | |
| List of callbacks | |
| """ | |
| callbacks = [] | |
| # Early stopping | |
| callbacks.append( | |
| tf.keras.callbacks.EarlyStopping( | |
| monitor='val_loss', | |
| patience=config.get('patience', 15), | |
| restore_best_weights=True, | |
| verbose=1 | |
| ) | |
| ) | |
| # Model checkpoint | |
| callbacks.append( | |
| tf.keras.callbacks.ModelCheckpoint( | |
| filepath=os.path.join(save_dir, 'best_model.h5'), | |
| monitor='val_loss', | |
| save_best_only=True, | |
| verbose=1 | |
| ) | |
| ) | |
| # Learning rate scheduler | |
| def lr_scheduler(epoch, lr): | |
| if epoch < 10: | |
| return lr | |
| else: | |
| return lr * tf.math.exp(-0.1) | |
| callbacks.append( | |
| tf.keras.callbacks.LearningRateScheduler(lr_scheduler, verbose=1) | |
| ) | |
| # TensorBoard | |
| if config.get('use_tensorboard', False): | |
| log_dir = os.path.join(save_dir, 'logs', datetime.now().strftime('%Y%m%d-%H%M%S')) | |
| callbacks.append( | |
| tf.keras.callbacks.TensorBoard(log_dir=log_dir, histogram_freq=1) | |
| ) | |
| # CSV logger | |
| callbacks.append( | |
| tf.keras.callbacks.CSVLogger(os.path.join(save_dir, 'training_history.csv')) | |
| ) | |
| return callbacks | |
| def train_model(config): | |
| """ | |
| Main training function | |
| Args: | |
| config: Configuration dictionary | |
| Returns: | |
| Trained model and history | |
| """ | |
| # Set random seeds | |
| np.random.seed(config.get('random_seed', 42)) | |
| tf.random.set_seed(config.get('random_seed', 42)) | |
| # Create save directory | |
| save_dir = Path(config.get('save_dir', './segmentation_models')) | |
| save_dir.mkdir(parents=True, exist_ok=True) | |
| # Save config | |
| with open(save_dir / 'config.json', 'w') as f: | |
| json.dump(config, f, indent=2) | |
| # Load data | |
| print("Loading data...") | |
| data = load_data(config) | |
| if len(data) == 2: | |
| # Need to split data | |
| images, masks = data | |
| from sklearn.model_selection import train_test_split | |
| X_train, X_val, y_train, y_val = train_test_split( | |
| images, masks, test_size=config.get('val_split', 0.2), random_state=config.get('random_seed', 42) | |
| ) | |
| else: | |
| X_train, y_train, X_val, y_val = data | |
| print(f"Training data: {X_train.shape[0]} images") | |
| print(f"Validation data: {X_val.shape[0]} images") | |
| # Build and compile model | |
| print("Building model...") | |
| model = get_model(config) | |
| model = compile_model(model, config) | |
| model.summary() | |
| # Create callbacks | |
| callbacks = create_callbacks(config, str(save_dir)) | |
| # Train | |
| print("Training model...") | |
| 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, | |
| verbose=1 | |
| ) | |
| # Save final model | |
| model.save(save_dir / 'final_model.h5') | |
| # Plot training history | |
| plot_training_history(history, save_dir / 'training_history.png') | |
| # Evaluate | |
| print("Evaluating model...") | |
| eval_results = model.evaluate(X_val, y_val, verbose=0) | |
| # Save evaluation results | |
| eval_dict = { | |
| metric_name: float(value) | |
| for metric_name, value in zip(model.metrics_names, eval_results) | |
| } | |
| with open(save_dir / 'evaluation_results.json', 'w') as f: | |
| json.dump(eval_dict, f, indent=2) | |
| print(f"Evaluation results: {eval_dict}") | |
| return model, history | |
| def train_with_kfold(config): | |
| """ | |
| Train model with k-fold cross-validation | |
| Args: | |
| config: Configuration dictionary | |
| Returns: | |
| KFoldValidator with trained models | |
| """ | |
| # Set random seeds | |
| np.random.seed(config.get('random_seed', 42)) | |
| tf.random.set_seed(config.get('random_seed', 42)) | |
| # Create save directory | |
| save_dir = Path(config.get('save_dir', './kfold_segmentation_results')) | |
| save_dir.mkdir(parents=True, exist_ok=True) | |
| # Load data | |
| print("Loading data...") | |
| images, masks = load_data(config) | |
| # Create model builder function | |
| def model_builder(): | |
| model = get_model(config) | |
| return compile_model(model, config) | |
| # Create K-fold validator | |
| validator = SegmentationKFoldValidator( | |
| model_builder=model_builder, | |
| n_splits=config.get('n_splits', 5), | |
| shuffle=config.get('shuffle', True), | |
| random_state=config.get('random_seed', 42), | |
| image_size=tuple(config.get('image_size', (224, 224))) | |
| ) | |
| # Run cross-validation | |
| results = validator.cross_validate( | |
| images=images, | |
| masks=masks, | |
| epochs=config.get('epochs', 100), | |
| batch_size=config.get('batch_size', 16), | |
| save_dir=str(save_dir), | |
| augment=config.get('use_augmentation', True) | |
| ) | |
| return validator, results | |
| def run_ablation_study(config): | |
| """ | |
| Run ablation study on segmentation models | |
| Args: | |
| config: Configuration dictionary | |
| Returns: | |
| AblationStudy with results | |
| """ | |
| # Load data | |
| print("Loading data...") | |
| images, masks = load_data(config) | |
| # Split into train/val for ablation | |
| from sklearn.model_selection import train_test_split | |
| X_train, X_val, y_train, y_val = train_test_split( | |
| images, masks, test_size=0.2, random_state=config.get('random_seed', 42) | |
| ) | |
| data = (X_train, y_train, X_val, y_val) | |
| # Create ablation study | |
| base_config = { | |
| 'input_shape': tuple(config.get('image_size', (224, 224))) + (3,), | |
| 'num_classes': 1, | |
| 'base_filters': config.get('base_filters', 64), | |
| 'dropout_rate': config.get('dropout_rate', 0.2), | |
| 'learning_rate': config.get('learning_rate', 1e-4), | |
| 'epochs': config.get('epochs', 50), | |
| 'batch_size': config.get('batch_size', 16) | |
| } | |
| # Choose ablation type | |
| ablation_type = config.get('ablation_type', 'attention') | |
| if ablation_type == 'attention': | |
| study = create_attention_ablation_study( | |
| base_config, | |
| results_dir=config.get('ablation_results_dir', './attention_ablation') | |
| ) | |
| elif ablation_type == 'architecture': | |
| study = create_architecture_ablation_study( | |
| base_config, | |
| results_dir=config.get('ablation_results_dir', './architecture_ablation') | |
| ) | |
| elif ablation_type == 'loss': | |
| study = create_loss_ablation_study( | |
| base_config, | |
| results_dir=config.get('ablation_results_dir', './loss_ablation') | |
| ) | |
| else: | |
| raise ValueError(f"Unknown ablation type: {ablation_type}") | |
| # Define model builder | |
| def model_builder(cfg): | |
| model = build_unet( | |
| input_shape=cfg.get('input_shape', (224, 224, 3)), | |
| num_classes=cfg.get('num_classes', 1), | |
| base_filters=cfg.get('base_filters', 64), | |
| dropout_rate=cfg.get('dropout_rate', 0.2), | |
| use_attention=cfg.get('use_attention', False) | |
| ) | |
| return compile_model(model, cfg) | |
| # Run ablation study | |
| results = study.run_all_experiments( | |
| model_builder=model_builder, | |
| data=data, | |
| metrics_calculator=calculate_segmentation_metrics | |
| ) | |
| return study, results | |
| def plot_training_history(history, save_path): | |
| """ | |
| Plot training history | |
| Args: | |
| history: Training history object | |
| save_path: Path to save plot | |
| """ | |
| fig, axes = plt.subplots(1, 3, figsize=(15, 5)) | |
| # Loss | |
| axes[0].plot(history.history['loss'], label='Train Loss') | |
| if 'val_loss' in history.history: | |
| axes[0].plot(history.history['val_loss'], label='Val Loss') | |
| axes[0].set_title('Loss') | |
| axes[0].set_xlabel('Epoch') | |
| axes[0].set_ylabel('Loss') | |
| axes[0].legend() | |
| # Dice coefficient | |
| if 'dice_coefficient' in history.history: | |
| axes[1].plot(history.history['dice_coefficient'], label='Train Dice') | |
| if 'val_dice_coefficient' in history.history: | |
| axes[1].plot(history.history['val_dice_coefficient'], label='Val Dice') | |
| axes[1].set_title('Dice Coefficient') | |
| axes[1].set_xlabel('Epoch') | |
| axes[1].set_ylabel('Dice') | |
| axes[1].legend() | |
| # IoU | |
| if 'iou_metric' in history.history: | |
| axes[2].plot(history.history['iou_metric'], label='Train IoU') | |
| if 'val_iou_metric' in history.history: | |
| axes[2].plot(history.history['val_iou_metric'], label='Val IoU') | |
| axes[2].set_title('Intersection over Union') | |
| axes[2].set_xlabel('Epoch') | |
| axes[2].set_ylabel('IoU') | |
| axes[2].legend() | |
| plt.tight_layout() | |
| plt.savefig(save_path, dpi=300, bbox_inches='tight') | |
| plt.close() | |
| print(f"Training history plot saved to {save_path}") | |
| def main(): | |
| parser = argparse.ArgumentParser(description='Train Brain Tumor Segmentation Models') | |
| # Data arguments | |
| parser.add_argument('--data_dir', type=str, default='./dataset', | |
| help='Directory containing training data') | |
| parser.add_argument('--image_size', type=int, nargs=2, default=[224, 224], | |
| help='Image size (height width)') | |
| # Model arguments | |
| parser.add_argument('--model_type', type=str, default='unet', | |
| choices=['unet', 'attention_unet', 'res_unet', 'multi_modal_unet'], | |
| help='Type of model to train') | |
| parser.add_argument('--base_filters', type=int, default=64, | |
| help='Number of base filters in model') | |
| parser.add_argument('--dropout_rate', type=float, default=0.2, | |
| help='Dropout rate') | |
| parser.add_argument('--use_attention', action='store_true', | |
| help='Use attention gates in U-Net') | |
| # Training arguments | |
| parser.add_argument('--epochs', type=int, default=100, | |
| help='Number of training epochs') | |
| parser.add_argument('--batch_size', type=int, default=16, | |
| help='Batch size') | |
| parser.add_argument('--learning_rate', type=float, default=1e-4, | |
| help='Learning rate') | |
| parser.add_argument('--loss_fn', type=str, default='dice_bce', | |
| choices=['dice_bce', 'dice', 'bce', 'focal'], | |
| help='Loss function') | |
| parser.add_argument('--val_split', type=float, default=0.2, | |
| help='Validation split ratio') | |
| # K-fold arguments | |
| parser.add_argument('--use_kfold', action='store_true', | |
| help='Use k-fold cross-validation') | |
| parser.add_argument('--n_splits', type=int, default=5, | |
| help='Number of folds for cross-validation') | |
| # Ablation study arguments | |
| parser.add_argument('--use_ablation', action='store_true', | |
| help='Run ablation study') | |
| parser.add_argument('--ablation_type', type=str, default='attention', | |
| choices=['attention', 'architecture', 'loss'], | |
| help='Type of ablation study') | |
| # General arguments | |
| parser.add_argument('--save_dir', type=str, default='./segmentation_models', | |
| help='Directory to save models and results') | |
| parser.add_argument('--random_seed', type=int, default=42, | |
| help='Random seed') | |
| parser.add_argument('--use_tensorboard', action='store_true', | |
| help='Use TensorBoard logging') | |
| args = parser.parse_args() | |
| config = vars(args) | |
| # Run appropriate training mode | |
| if args.use_kfold: | |
| validator, results = train_with_kfold(config) | |
| print("\nK-Fold Cross-Validation Results:") | |
| print(f"Mean validation loss: {results['aggregate_metrics']['val_loss']['mean']:.4f} ± {results['aggregate_metrics']['val_loss']['std']:.4f}") | |
| elif args.use_ablation: | |
| study, results = run_ablation_study(config) | |
| print("\nAblation Study Results:") | |
| print(study.get_comparison_table()) | |
| else: | |
| model, history = train_model(config) | |
| print("\nTraining completed!") | |
| if __name__ == '__main__': | |
| main() |