Tri-Netra-AI / src /kfold_validation.py
anannyavyas1's picture
Upload folder using huggingface_hub
1cf3825 verified
Raw
History Blame Contribute Delete
20.4 kB
"""
K-Fold Cross-Validation Framework for Brain Tumor Detection
"""
import numpy as np
import tensorflow as tf
from sklearn.model_selection import KFold, StratifiedKFold
from sklearn.model_selection import train_test_split
import json
import os
from pathlib import Path
import pandas as pd
from datetime import datetime
class KFoldValidator:
"""
K-Fold Cross-Validation wrapper for brain tumor detection models
"""
def __init__(
self,
model_builder,
n_splits=5,
shuffle=True,
random_state=42,
stratified=True
):
"""
Initialize K-Fold validator
Args:
model_builder: Function that builds and returns a compiled model
n_splits: Number of folds for cross-validation
shuffle: Whether to shuffle data before splitting
random_state: Random seed for reproducibility
stratified: Whether to use stratified k-fold (for classification)
"""
self.model_builder = model_builder
self.n_splits = n_splits
self.shuffle = shuffle
self.random_state = random_state
self.stratified = stratified
# Results storage
self.fold_histories = []
self.fold_metrics = []
self.best_models = []
def split_data(self, X, y=None):
"""
Split data into k folds
Args:
X: Feature data (images)
y: Labels (optional, for stratified splitting)
Returns:
List of (train_indices, val_indices) tuples
"""
if self.stratified and y is not None:
kf = StratifiedKFold(
n_splits=self.n_splits,
shuffle=self.shuffle,
random_state=self.random_state
)
return list(kf.split(X, y))
else:
kf = KFold(
n_splits=self.n_splits,
shuffle=self.shuffle,
random_state=self.random_state
)
return list(kf.split(X))
def train_fold(
self,
fold_idx,
X_train,
y_train,
X_val,
y_val,
epochs=50,
batch_size=32,
callbacks=None,
**fit_kwargs
):
"""
Train model on a single fold
Args:
fold_idx: Index of the current fold
X_train: Training features
y_train: Training labels
X_val: Validation features
y_val: Validation labels
epochs: Number of training epochs
batch_size: Batch size
callbacks: List of Keras callbacks
**fit_kwargs: Additional arguments for model.fit()
Returns:
Trained model and training history
"""
# Build and compile model
model = self.model_builder()
# Default callbacks
if callbacks is None:
callbacks = []
# Add early stopping if not provided
if not any(isinstance(c, tf.keras.callbacks.EarlyStopping) for c in callbacks):
callbacks.append(
tf.keras.callbacks.EarlyStopping(
monitor='val_loss',
patience=10,
restore_best_weights=True
)
)
# Add model checkpoint if not provided
if not any(isinstance(c, tf.keras.callbacks.ModelCheckpoint) for c in callbacks):
callbacks.append(
tf.keras.callbacks.ModelCheckpoint(
filepath=f'best_model_fold_{fold_idx}.h5',
monitor='val_loss',
save_best_only=True
)
)
# Train model
history = model.fit(
X_train,
y_train,
validation_data=(X_val, y_val),
epochs=epochs,
batch_size=batch_size,
callbacks=callbacks,
**fit_kwargs
)
# Evaluate on validation set
val_results = model.evaluate(X_val, y_val, verbose=0)
# Store results
self.fold_histories.append(history)
self.fold_metrics.append({
'fold': fold_idx,
'val_loss': val_results[0] if isinstance(val_results, list) else val_results,
'val_metrics': {
metric_name: float(val_results[i])
for i, metric_name in enumerate(model.metrics_names)
} if isinstance(val_results, list) else {'loss': float(val_results)},
'epochs_trained': len(history.history['loss'])
})
# Store best model
self.best_models.append(model)
return model, history
def cross_validate(
self,
X,
y=None,
epochs=50,
batch_size=32,
callbacks=None,
save_dir='./kfold_results',
**fit_kwargs
):
"""
Perform k-fold cross-validation
Args:
X: Feature data (images)
y: Labels (optional, for stratified splitting)
epochs: Number of training epochs per fold
batch_size: Batch size
callbacks: List of Keras callbacks
save_dir: Directory to save results
**fit_kwargs: Additional arguments for model.fit()
Returns:
Dictionary containing cross-validation results
"""
# Create save directory
os.makedirs(save_dir, exist_ok=True)
# Split data
folds = self.split_data(X, y)
# Reset results
self.fold_histories = []
self.fold_metrics = []
self.best_models = []
# Train on each fold
for fold_idx, (train_indices, val_indices) in enumerate(folds):
print(f"\n{'='*50}")
print(f"Training Fold {fold_idx + 1}/{self.n_splits}")
print(f"{'='*50}")
# Split data
X_train, X_val = X[train_indices], X[val_indices]
y_train, y_val = y[train_indices], y[val_indices] if y is not None else (None, None)
# Train fold
model, history = self.train_fold(
fold_idx,
X_train,
y_train,
X_val,
y_val,
epochs=epochs,
batch_size=batch_size,
callbacks=callbacks,
**fit_kwargs
)
print(f"Fold {fold_idx + 1} completed. Validation loss: {self.fold_metrics[-1]['val_loss']:.4f}")
# Calculate aggregate metrics
results = self.summarize_results()
# Save results
self.save_results(results, save_dir)
return results
def summarize_results(self):
"""
Summarize cross-validation results
Returns:
Dictionary containing aggregated metrics
"""
if not self.fold_metrics:
raise ValueError("No fold metrics found. Run cross_validate first.")
# Extract metrics
val_losses = [m['val_loss'] for m in self.fold_metrics]
# Get all metric names from first fold
metric_names = list(self.fold_metrics[0]['val_metrics'].keys())
metric_values = {name: [] for name in metric_names}
for m in self.fold_metrics:
for name in metric_names:
metric_values[name].append(m['val_metrics'].get(name, 0))
# Calculate statistics
summary = {
'n_splits': self.n_splits,
'fold_results': self.fold_metrics,
'aggregate_metrics': {
'val_loss': {
'mean': float(np.mean(val_losses)),
'std': float(np.std(val_losses)),
'min': float(np.min(val_losses)),
'max': float(np.max(val_losses))
}
}
}
# Add metrics statistics
for name in metric_names:
values = metric_values[name]
summary['aggregate_metrics'][name] = {
'mean': float(np.mean(values)),
'std': float(np.std(values)),
'min': float(np.min(values)),
'max': float(np.max(values))
}
return summary
def save_results(self, results, save_dir):
"""
Save cross-validation results to files
Args:
results: Results dictionary from summarize_results()
save_dir: Directory to save results
"""
# Save summary as JSON
summary_path = os.path.join(save_dir, 'kfold_summary.json')
with open(summary_path, 'w') as f:
json.dump(results, f, indent=2)
# Save detailed metrics as CSV
metrics_df = pd.DataFrame(self.fold_metrics)
metrics_df.to_csv(os.path.join(save_dir, 'fold_metrics.csv'), index=False)
# Save individual fold histories
for i, history in enumerate(self.fold_histories):
history_dict = history.history
history_df = pd.DataFrame(history_dict)
history_df.to_csv(os.path.join(save_dir, f'fold_{i}_history.csv'), index=False)
# Save models
models_dir = os.path.join(save_dir, 'models')
os.makedirs(models_dir, exist_ok=True)
for i, model in enumerate(self.best_models):
model.save(os.path.join(models_dir, f'fold_{i}_model.h5'))
print(f"Results saved to {save_dir}")
def get_ensemble_predictions(self, X_test, threshold=0.5):
"""
Get ensemble predictions from all fold models
Args:
X_test: Test data
threshold: Classification threshold (for binary classification)
Returns:
Ensemble predictions (probabilities and binary predictions)
"""
if not self.best_models:
raise ValueError("No models found. Run cross_validate first.")
# Get predictions from each model
predictions = []
for model in self.best_models:
pred = model.predict(X_test)
predictions.append(pred)
# Average predictions
avg_predictions = np.mean(predictions, axis=0)
# Binary predictions
binary_predictions = (avg_predictions >= threshold).astype(int)
return avg_predictions, binary_predictions
class SegmentationKFoldValidator(KFoldValidator):
"""
K-Fold Cross-Validation for segmentation models
"""
def __init__(
self,
model_builder,
n_splits=5,
shuffle=True,
random_state=42,
image_size=(224, 224)
):
"""
Initialize segmentation K-Fold validator
Args:
model_builder: Function that builds and returns a compiled segmentation model
n_splits: Number of folds
shuffle: Whether to shuffle data
random_state: Random seed
image_size: Size of input images
"""
super().__init__(
model_builder=model_builder,
n_splits=n_splits,
shuffle=shuffle,
random_state=random_state,
stratified=False # Segmentation typically doesn't use stratification
)
self.image_size = image_size
def create_segmentation_dataset(
self,
images,
masks,
batch_size=16,
augment=False
):
"""
Create TensorFlow dataset for segmentation
Args:
images: Array of input images
masks: Array of segmentation masks
batch_size: Batch size
augment: Whether to apply data augmentation
Returns:
TensorFlow dataset
"""
def generator():
for img, mask in zip(images, masks):
yield img, mask
def augment_fn(image, mask):
# Random flips
if tf.random.uniform(()) > 0.5:
image = tf.image.flip_left_right(image)
mask = tf.image.flip_left_right(mask)
if tf.random.uniform(()) > 0.5:
image = tf.image.flip_up_down(image)
mask = tf.image.flip_up_down(mask)
# Random rotation
k = tf.random.uniform(shape=(), minval=0, maxval=4, dtype=tf.int32)
image = tf.image.rot90(image, k=k)
mask = tf.image.rot90(mask, k=k)
return image, mask
dataset = tf.data.Dataset.from_generator(
generator,
output_signature=(
tf.TensorSpec(shape=(*self.image_size, 3), dtype=tf.float32),
tf.TensorSpec(shape=(*self.image_size, 1), dtype=tf.float32)
)
)
if augment:
dataset = dataset.map(augment_fn, num_parallel_calls=tf.data.AUTOTUNE)
dataset = dataset.batch(batch_size).prefetch(tf.data.AUTOTUNE)
return dataset
def cross_validate(
self,
images,
masks,
epochs=50,
batch_size=16,
callbacks=None,
save_dir='./kfold_segmentation_results',
augment=True
):
"""
Perform k-fold cross-validation for segmentation
Args:
images: Array of input images
masks: Array of segmentation masks
epochs: Number of training epochs
batch_size: Batch size
callbacks: List of Keras callbacks
save_dir: Directory to save results
augment: Whether to use data augmentation
Returns:
Dictionary containing cross-validation results
"""
# Create save directory
os.makedirs(save_dir, exist_ok=True)
# Split data
folds = self.split_data(images)
# Reset results
self.fold_histories = []
self.fold_metrics = []
self.best_models = []
# Train on each fold
for fold_idx, (train_indices, val_indices) in enumerate(folds):
print(f"\n{'='*50}")
print(f"Training Segmentation Fold {fold_idx + 1}/{self.n_splits}")
print(f"{'='*50}")
# Split data
X_train, X_val = images[train_indices], images[val_indices]
y_train, y_val = masks[train_indices], masks[val_indices]
# Create datasets
train_dataset = self.create_segmentation_dataset(
X_train, y_train, batch_size=batch_size, augment=augment
)
val_dataset = self.create_segmentation_dataset(
X_val, y_val, batch_size=batch_size, augment=False
)
# Train fold
model, history = self.train_fold_segmentation(
fold_idx,
train_dataset,
val_dataset,
epochs=epochs,
callbacks=callbacks
)
print(f"Fold {fold_idx + 1} completed.")
# Calculate aggregate metrics
results = self.summarize_results()
# Save results
self.save_results(results, save_dir)
return results
def train_fold_segmentation(
self,
fold_idx,
train_dataset,
val_dataset,
epochs=50,
callbacks=None
):
"""
Train segmentation model on a single fold
Args:
fold_idx: Fold index
train_dataset: Training dataset
val_dataset: Validation dataset
epochs: Number of epochs
callbacks: List of callbacks
Returns:
Trained model and history
"""
# Build model
model = self.model_builder()
# Default callbacks
if callbacks is None:
callbacks = []
# Add early stopping
if not any(isinstance(c, tf.keras.callbacks.EarlyStopping) for c in callbacks):
callbacks.append(
tf.keras.callbacks.EarlyStopping(
monitor='val_loss',
patience=10,
restore_best_weights=True
)
)
# Add model checkpoint
if not any(isinstance(c, tf.keras.callbacks.ModelCheckpoint) for c in callbacks):
callbacks.append(
tf.keras.callbacks.ModelCheckpoint(
filepath=f'best_segmentation_model_fold_{fold_idx}.h5',
monitor='val_loss',
save_best_only=True
)
)
# Train
history = model.fit(
train_dataset,
validation_data=val_dataset,
epochs=epochs,
callbacks=callbacks
)
# Evaluate
val_results = model.evaluate(val_dataset, verbose=0)
# Store results
self.fold_histories.append(history)
self.fold_metrics.append({
'fold': fold_idx,
'val_loss': val_results[0] if isinstance(val_results, list) else val_results,
'val_metrics': {
metric_name: float(val_results[i])
for i, metric_name in enumerate(model.metrics_names)
} if isinstance(val_results, list) else {'loss': float(val_results)},
'epochs_trained': len(history.history['loss'])
})
self.best_models.append(model)
return model, history
def prepare_data_for_kfold(
image_paths,
label_paths=None,
image_size=(224, 224),
test_size=0.2,
random_state=42
):
"""
Prepare data for k-fold cross-validation
Args:
image_paths: List of paths to image files
label_paths: List of paths to label/mask files (for segmentation)
image_size: Size to resize images to
test_size: Proportion of data to hold out for final testing
random_state: Random seed
Returns:
Arrays of images and labels/masks
"""
import cv2
from tqdm import tqdm
# Load images
images = []
for path in tqdm(image_paths, desc="Loading images"):
img = cv2.imread(str(path))
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, image_size)
images.append(img)
images = np.array(images)
# Load labels/masks if provided
if label_paths is not None:
labels = []
for path in tqdm(label_paths, desc="Loading labels"):
mask = cv2.imread(str(path), cv2.IMREAD_GRAYSCALE)
mask = cv2.resize(mask, image_size)
mask = mask.astype(np.float32) / 255.0
mask = np.expand_dims(mask, axis=-1)
labels.append(mask)
labels = np.array(labels)
# Split into train and test
if test_size > 0:
X_train, X_test, y_train, y_test = train_test_split(
images, labels, test_size=test_size, random_state=random_state
)
return X_train, y_train, X_test, y_test
return images, labels
return images