Spaces:
Runtime error
Runtime error
| """ | |
| Advanced Models: 3D MRI Transformer, Federated Learning, and Self-Supervised Pre-training | |
| """ | |
| import numpy as np | |
| import tensorflow as tf | |
| from tensorflow.keras import layers, Model | |
| import json | |
| import os | |
| from typing import List, Dict, Tuple, Optional | |
| from pathlib import Path | |
| import pickle | |
| from datetime import datetime | |
| class MRI3DTransformer: | |
| """ | |
| 3D Vision Transformer for MRI volume analysis | |
| """ | |
| def __init__( | |
| self, | |
| input_shape=(128, 128, 128, 1), | |
| patch_size=16, | |
| num_layers=12, | |
| num_heads=12, | |
| embedding_dim=768, | |
| mlp_dim=3072, | |
| dropout_rate=0.1, | |
| num_classes=1000 | |
| ): | |
| """ | |
| Initialize 3D MRI Transformer | |
| Args: | |
| input_shape: Shape of input 3D volumes (depth, height, width, channels) | |
| patch_size: Size of patches to extract | |
| num_layers: Number of transformer layers | |
| num_heads: Number of attention heads | |
| embedding_dim: Dimension of patch embeddings | |
| mlp_dim: Dimension of MLP hidden layer | |
| dropout_rate: Dropout rate | |
| num_classes: Number of output classes | |
| """ | |
| self.input_shape = input_shape | |
| self.patch_size = patch_size | |
| self.num_layers = num_layers | |
| self.num_heads = num_heads | |
| self.embedding_dim = embedding_dim | |
| self.mlp_dim = mlp_dim | |
| self.dropout_rate = dropout_rate | |
| self.num_classes = num_classes | |
| self.model = None | |
| def patch_embedding(self, inputs): | |
| """Extract 3D patches with spatial locality preserved and project to embeddings. | |
| Uses a strided Conv3D whose kernel and stride equal patch_size, then flattens | |
| the resulting spatial grid to a token sequence. This is the same construction | |
| as the standard 3D ViT (analogous to the 2D ViT's Conv2D patch projection). | |
| Previously this method used a single Reshape over the whole flattened volume, | |
| which destroyed spatial structure and was not a real 3D ViT patcher. | |
| """ | |
| depth_patches = self.input_shape[0] // self.patch_size | |
| height_patches = self.input_shape[1] // self.patch_size | |
| width_patches = self.input_shape[2] // self.patch_size | |
| num_patches = depth_patches * height_patches * width_patches | |
| x = layers.Conv3D( | |
| filters=self.embedding_dim, | |
| kernel_size=self.patch_size, | |
| strides=self.patch_size, | |
| padding='valid', | |
| name='patch_projection_conv3d', | |
| )(inputs) | |
| patch_projection = layers.Reshape((num_patches, self.embedding_dim), name='patch_projection')(x) | |
| return patch_projection, num_patches | |
| def transformer_encoder(self, x): | |
| """ | |
| Build transformer encoder layers | |
| Args: | |
| x: Input embeddings | |
| Returns: | |
| Encoded representations | |
| """ | |
| for i in range(self.num_layers): | |
| # Layer normalization | |
| x_norm = layers.LayerNormalization(epsilon=1e-6, name=f'layer_norm_{i}')(x) | |
| # Multi-head attention | |
| attention_output = layers.MultiHeadAttention( | |
| num_heads=self.num_heads, | |
| key_dim=self.embedding_dim // self.num_heads, | |
| dropout=self.dropout_rate, | |
| name=f'attention_{i}' | |
| )(x_norm, x_norm) | |
| # Add & norm | |
| x = layers.Add(name=f'add_{i}')([x, attention_output]) | |
| # MLP block | |
| x_norm = layers.LayerNormalization(epsilon=1e-6, name=f'layer_norm_{i}_mlp')(x) | |
| mlp_output = layers.Dense(self.mlp_dim, activation='gelu', name=f'mlp_dense1_{i}')(x_norm) | |
| mlp_output = layers.Dropout(self.dropout_rate, name=f'mlp_dropout1_{i}')(mlp_output) | |
| mlp_output = layers.Dense(self.embedding_dim, name=f'mlp_dense2_{i}')(mlp_output) | |
| mlp_output = layers.Dropout(self.dropout_rate, name=f'mlp_dropout2_{i}')(mlp_output) | |
| # Add & norm | |
| x = layers.Add(name=f'add_mlp_{i}')([x, mlp_output]) | |
| return x | |
| def build_model(self): | |
| """ | |
| Build the 3D MRI Transformer model | |
| Returns: | |
| Compiled Keras model | |
| """ | |
| inputs = layers.Input(shape=self.input_shape, name='mri_volume') | |
| # Patch embedding | |
| patches, num_patches = self.patch_embedding(inputs) | |
| # Positional encoding | |
| pos_encoding = self.get_positional_encoding(num_patches, self.embedding_dim) | |
| x = layers.Add()([patches, pos_encoding]) | |
| x = layers.Dropout(self.dropout_rate)(x) | |
| # Transformer encoder | |
| x = self.transformer_encoder(x) | |
| # Global average pooling | |
| x = layers.GlobalAveragePooling1D()(x) | |
| x = layers.LayerNormalization(epsilon=1e-6)(x) | |
| # Classification head | |
| x = layers.Dense(self.embedding_dim, activation='tanh')(x) | |
| x = layers.Dropout(self.dropout_rate)(x) | |
| outputs = layers.Dense(self.num_classes, activation='softmax')(x) | |
| self.model = Model(inputs=inputs, outputs=outputs, name='mri_3d_transformer') | |
| return self.model | |
| def get_positional_encoding(self, num_patches, embedding_dim): | |
| """ | |
| Generate positional encoding | |
| Args: | |
| num_patches: Number of patches | |
| embedding_dim: Embedding dimension | |
| Returns: | |
| Positional encoding tensor | |
| """ | |
| positions = tf.range(start=0, limit=num_patches, delta=1) | |
| position_embedding = layers.Embedding( | |
| input_dim=num_patches, | |
| output_dim=embedding_dim, | |
| name='positional_encoding' | |
| )(positions) | |
| return position_embedding | |
| def compile_model(self, learning_rate=1e-4): | |
| """ | |
| Compile the model | |
| Args: | |
| learning_rate: Learning rate for optimizer | |
| """ | |
| if self.model is None: | |
| self.build_model() | |
| self.model.compile( | |
| optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate), | |
| loss='sparse_categorical_crossentropy', | |
| metrics=['accuracy'] | |
| ) | |
| return self.model | |
| def train(self, X_train, y_train, X_val=None, y_val=None, epochs=100, batch_size=32, callbacks=None): | |
| """ | |
| Train the model | |
| Args: | |
| X_train: Training volumes | |
| y_train: Training labels | |
| X_val: Validation volumes | |
| y_val: Validation labels | |
| epochs: Number of training epochs | |
| batch_size: Batch size | |
| callbacks: List of Keras callbacks | |
| Returns: | |
| Training history | |
| """ | |
| if self.model is None: | |
| self.compile_model() | |
| # Default callbacks | |
| if callbacks is None: | |
| callbacks = [] | |
| callbacks.extend([ | |
| tf.keras.callbacks.EarlyStopping( | |
| monitor='val_loss' if X_val is not None else 'loss', | |
| patience=15, | |
| restore_best_weights=True | |
| ), | |
| tf.keras.callbacks.ReduceLROnPlateau( | |
| monitor='val_loss' if X_val is not None else 'loss', | |
| factor=0.5, | |
| patience=5, | |
| min_lr=1e-7 | |
| ) | |
| ]) | |
| # Train | |
| history = self.model.fit( | |
| X_train, y_train, | |
| validation_data=(X_val, y_val) if X_val is not None else None, | |
| epochs=epochs, | |
| batch_size=batch_size, | |
| callbacks=callbacks, | |
| verbose=1 | |
| ) | |
| return history | |
| def save_model(self, save_path): | |
| """Save model""" | |
| if self.model is not None: | |
| self.model.save(save_path) | |
| def load_model(self, model_path): | |
| """Load model""" | |
| self.model = tf.keras.models.load_model(model_path) | |
| return self.model | |
| class FederatedLearningClient: | |
| """ | |
| Client for federated learning | |
| """ | |
| def __init__(self, client_id, model, data, batch_size=32): | |
| """ | |
| Initialize federated learning client | |
| Args: | |
| client_id: Unique client identifier | |
| model: Model architecture (uncompiled) | |
| data: Tuple of (X, y) for this client | |
| batch_size: Batch size for training | |
| """ | |
| self.client_id = client_id | |
| self.model = model | |
| self.X, self.y = data | |
| self.batch_size = batch_size | |
| self.local_epochs = 1 | |
| def set_weights(self, weights): | |
| """Set model weights from server""" | |
| self.model.set_weights(weights) | |
| def get_weights(self): | |
| """Get model weights to send to server""" | |
| return self.model.get_weights() | |
| def train_local(self, epochs=1, batch_size=32): | |
| """ | |
| Train model locally | |
| Args: | |
| epochs: Number of local training epochs | |
| batch_size: Batch size | |
| Returns: | |
| Updated weights | |
| """ | |
| self.model.compile( | |
| optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4), | |
| loss='sparse_categorical_crossentropy', | |
| metrics=['accuracy'] | |
| ) | |
| history = self.model.fit( | |
| self.X, self.y, | |
| epochs=epochs, | |
| batch_size=batch_size, | |
| verbose=0 | |
| ) | |
| return self.get_weights(), history.history | |
| class FederatedLearningServer: | |
| """ | |
| Server for federated learning coordination | |
| """ | |
| def __init__(self, model_architecture, input_shape, num_classes): | |
| """ | |
| Initialize federated learning server | |
| Args: | |
| model_architecture: Function that creates model architecture | |
| input_shape: Shape of input data | |
| num_classes: Number of output classes | |
| """ | |
| self.model_architecture = model_architecture | |
| self.input_shape = input_shape | |
| self.num_classes = num_classes | |
| self.global_model = self.create_model() | |
| self.clients = [] | |
| self.round_history = [] | |
| def create_model(self): | |
| """Create global model""" | |
| model = self.model_architecture( | |
| input_shape=self.input_shape, | |
| num_classes=self.num_classes | |
| ) | |
| model.compile( | |
| optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4), | |
| loss='sparse_categorical_crossentropy', | |
| metrics=['accuracy'] | |
| ) | |
| return model | |
| def add_client(self, client): | |
| """Add client to federation""" | |
| self.clients.append(client) | |
| def aggregate_weights(self, client_weights, client_data_sizes): | |
| """ | |
| Aggregate client weights using FedAvg | |
| Args: | |
| client_weights: List of client weight sets | |
| client_data_sizes: List of client data sizes | |
| Returns: | |
| Aggregated weights | |
| """ | |
| total_size = sum(client_data_sizes) | |
| aggregated_weights = [] | |
| # For each layer | |
| for layer_weights in zip(*client_weights): | |
| # Weighted average | |
| aggregated = np.zeros_like(layer_weights[0]) | |
| for weight, size in zip(layer_weights, client_data_sizes): | |
| aggregated += weight * (size / total_size) | |
| aggregated_weights.append(aggregated) | |
| return aggregated_weights | |
| def set_eval_data(self, X_eval, y_eval): | |
| """Register a held-out evaluation set for the global model. | |
| FedAvg evaluation should NOT use the clients' own training data, as that | |
| leaks training samples into the metric. Use this method to register a | |
| separate held-out dataset that the server uses each round. | |
| """ | |
| self._X_eval = X_eval | |
| self._y_eval = y_eval | |
| def run_federation_round(self, local_epochs=1): | |
| """Run one round of federated learning. | |
| Returns round metrics computed on the held-out global eval set (if | |
| registered via set_eval_data) rather than on each client's own training | |
| data (the prior behaviour, which leaked train data into the metric). | |
| """ | |
| # Get current global weights | |
| global_weights = self.global_model.get_weights() | |
| # Send weights to clients and train locally | |
| client_weights = [] | |
| client_data_sizes = [] | |
| for client in self.clients: | |
| client.set_weights(global_weights) | |
| weights, _history = client.train_local(epochs=local_epochs) | |
| client_weights.append(weights) | |
| client_data_sizes.append(len(client.X)) | |
| # Aggregate weights | |
| aggregated_weights = self.aggregate_weights(client_weights, client_data_sizes) | |
| self.global_model.set_weights(aggregated_weights) | |
| # Evaluate global model on the held-out set (if provided) instead of | |
| # each client's training data. Falls back to per-client evaluation with | |
| # a loud warning when no held-out set is registered. | |
| if getattr(self, '_X_eval', None) is not None and getattr(self, '_y_eval', None) is not None: | |
| results = self.global_model.evaluate(self._X_eval, self._y_eval, verbose=0) | |
| if isinstance(results, (list, tuple)): | |
| names = self.global_model.metrics_names | |
| metrics_by_name = dict(zip(names, results)) | |
| accuracy = float(metrics_by_name.get('accuracy', metrics_by_name.get('compile_metrics', 0.0))) | |
| loss = float(metrics_by_name.get('loss', results[0])) | |
| else: | |
| accuracy = float(results) | |
| loss = float(results) | |
| round_metrics = { | |
| 'round': len(self.round_history) + 1, | |
| 'eval_accuracy': accuracy, | |
| 'eval_loss': loss, | |
| 'note': 'evaluated on held-out global eval set', | |
| } | |
| else: | |
| import warnings | |
| warnings.warn( | |
| 'FederatedLearningServer has no held-out eval set; falling back to ' | |
| 'per-client evaluation on training data. Call set_eval_data() with ' | |
| 'a held-out (X, y) to avoid train/test leakage.' | |
| ) | |
| client_accuracies = [] | |
| for client in self.clients: | |
| results = self.global_model.evaluate(client.X, client.y, verbose=0) | |
| if isinstance(results, (list, tuple)): | |
| names = self.global_model.metrics_names | |
| metrics_by_name = dict(zip(names, results)) | |
| client_accuracies.append(float(metrics_by_name.get('accuracy', results[-1]))) | |
| else: | |
| client_accuracies.append(float(results)) | |
| round_metrics = { | |
| 'round': len(self.round_history) + 1, | |
| 'client_accuracies': client_accuracies, | |
| 'mean_accuracy': float(np.mean(client_accuracies)), | |
| 'std_accuracy': float(np.std(client_accuracies)), | |
| 'note': 'evaluated on client training data (no held-out set registered)', | |
| } | |
| self.round_history.append(round_metrics) | |
| return round_metrics | |
| def run_federation(self, num_rounds=10, local_epochs=1): | |
| """ | |
| Run multiple rounds of federated learning | |
| Args: | |
| num_rounds: Number of federation rounds | |
| local_epochs: Number of local training epochs per round | |
| Returns: | |
| List of round metrics | |
| """ | |
| for round_num in range(num_rounds): | |
| print(f"\nRound {round_num + 1}/{num_rounds}") | |
| metrics = self.run_federation_round(local_epochs) | |
| print(f"Mean accuracy: {metrics['mean_accuracy']:.4f} ± {metrics['std_accuracy']:.4f}") | |
| return self.round_history | |
| def save_results(self, save_dir='./federated_results'): | |
| """Save federation results""" | |
| os.makedirs(save_dir, exist_ok=True) | |
| # Save round history | |
| with open(os.path.join(save_dir, 'federation_history.json'), 'w') as f: | |
| json.dump(self.round_history, f, indent=2) | |
| # Save global model | |
| self.global_model.save(os.path.join(save_dir, 'global_model.h5')) | |
| # Save client configurations | |
| client_configs = [] | |
| for client in self.clients: | |
| client_configs.append({ | |
| 'client_id': client.client_id, | |
| 'data_size': len(client.X) | |
| }) | |
| with open(os.path.join(save_dir, 'client_configs.json'), 'w') as f: | |
| json.dump(client_configs, f, indent=2) | |
| class SelfSupervisedPretrainer: | |
| """ | |
| Self-supervised pre-training for medical images | |
| """ | |
| def __init__(self, model_architecture, input_shape, projection_dim=128): | |
| """ | |
| Initialize self-supervised pretrainer | |
| Args: | |
| model_architecture: Function that creates encoder architecture | |
| input_shape: Shape of input images | |
| projection_dim: Dimension of projection head output | |
| """ | |
| self.model_architecture = model_architecture | |
| self.input_shape = input_shape | |
| self.projection_dim = projection_dim | |
| self.encoder = None | |
| self.pretext_model = None | |
| def create_encoder(self): | |
| """Create encoder model""" | |
| inputs = layers.Input(shape=self.input_shape) | |
| # Base encoder (e.g., ResNet, U-Net encoder) | |
| x = self.model_architecture(inputs) | |
| # Global average pooling | |
| x = layers.GlobalAveragePooling2D()(x) | |
| # Projection head | |
| x = layers.Dense(256, activation='relu')(x) | |
| x = layers.BatchNormalization()(x) | |
| x = layers.Dense(self.projection_dim)(x) | |
| self.encoder = Model(inputs=inputs, outputs=x, name='encoder') | |
| return self.encoder | |
| def create_pretext_model(self, pretext_task='rotation'): | |
| """ | |
| Create pretext task model | |
| Args: | |
| pretext_task: 'rotation', 'jigsaw', or 'contrastive' (SimCLR-style) | |
| Returns: | |
| Pretext task model. For contrastive, returns the projection model | |
| (encoder + projection head, no classification head); train with the | |
| NT-Xent loss via SelfSupervisedPretrainer.pretrain(...). | |
| """ | |
| if self.encoder is None: | |
| self.create_encoder() | |
| inputs = layers.Input(shape=self.input_shape) | |
| x = self.encoder(inputs) | |
| if pretext_task == 'rotation': | |
| outputs = layers.Dense(4, activation='softmax', name='rotation_head')(x) | |
| elif pretext_task == 'jigsaw': | |
| num_permutations = 10 | |
| outputs = layers.Dense(num_permutations, activation='softmax', name='jigsaw_head')(x) | |
| elif pretext_task == 'contrastive': | |
| # SimCLR-style projection head producing an L2-normalised embedding; | |
| # trained with NT-Xent (see _nt_xent_loss). Previously this branch | |
| # used Dense(1, sigmoid) with sparse_categorical_crossentropy, which | |
| # is an incoherent loss/head combo and would not train. | |
| x = layers.Dense(self.projection_dim, activation='relu', name='proj_hidden')(x) | |
| x = layers.Dense(self.projection_dim, name='proj_out')(x) | |
| outputs = layers.Lambda( | |
| lambda t: tf.math.l2_normalize(t, axis=-1), name='projection_l2' | |
| )(x) | |
| else: | |
| raise ValueError(f"Unknown pretext task: {pretext_task}") | |
| self.pretext_model = Model(inputs=inputs, outputs=outputs, name=f'pretext_{pretext_task}') | |
| return self.pretext_model | |
| def _nt_xent_loss(temperature=0.5): | |
| """SimCLR NT-Xent contrastive loss for two augmented views per sample. | |
| Expects the model output to be an L2-normalised projection of a batch of | |
| shape (2N, D), where rows 0..N-1 are the first view of each sample and | |
| rows N..2N-1 are the second view (paired in order). | |
| """ | |
| def loss_fn(_y_true_unused, z): | |
| batch_size_2 = tf.shape(z)[0] | |
| batch_size = batch_size_2 // 2 | |
| # Cosine similarity matrix (already L2 normed) | |
| sim = tf.matmul(z, z, transpose_b=True) / temperature | |
| # Mask out self-similarity | |
| mask_self = tf.eye(batch_size_2, dtype=tf.bool) | |
| sim = tf.where(mask_self, -1e9 * tf.ones_like(sim), sim) | |
| # Positive pair indices: i -> i+N for i in [0,N), and i -> i-N for i in [N,2N) | |
| ar = tf.range(batch_size_2) | |
| positives = tf.where(ar < batch_size, ar + batch_size, ar - batch_size) | |
| log_softmax = tf.nn.log_softmax(sim, axis=1) | |
| loss = -tf.gather(log_softmax, positives, batch_dims=1) | |
| return tf.reduce_mean(loss) | |
| return loss_fn | |
| def make_two_view_batch(X, augment_fn=None): | |
| """Build a SimCLR-style two-view batch (2N, H, W, C) from N images. | |
| If augment_fn is None, applies random horizontal/vertical flips and a | |
| random 90-degree rotation as a sensible default for medical images. | |
| """ | |
| import tensorflow as _tf | |
| def _default_aug(img): | |
| img = _tf.image.random_flip_left_right(img) | |
| img = _tf.image.random_flip_up_down(img) | |
| k = _tf.random.uniform(shape=(), minval=0, maxval=4, dtype=_tf.int32) | |
| img = _tf.image.rot90(img, k=k) | |
| img = _tf.image.random_brightness(img, max_delta=0.1) | |
| img = _tf.image.random_contrast(img, lower=0.9, upper=1.1) | |
| return img | |
| aug = augment_fn or _default_aug | |
| view1 = _tf.stack([aug(_tf.convert_to_tensor(x)) for x in X]) | |
| view2 = _tf.stack([aug(_tf.convert_to_tensor(x)) for x in X]) | |
| return _tf.concat([view1, view2], axis=0).numpy() | |
| def prepare_rotation_data(self, X): | |
| """ | |
| Prepare data for rotation prediction pretext task | |
| Args: | |
| X: Input images | |
| Returns: | |
| Rotated images and rotation labels | |
| """ | |
| rotated_images = [] | |
| rotation_labels = [] | |
| for image in X: | |
| # Generate 4 rotated versions | |
| for rotation_angle in [0, 90, 180, 270]: | |
| rotated = np.rot90(image, k=rotation_angle // 90, axes=(0, 1)) | |
| rotated_images.append(rotated) | |
| rotation_labels.append(rotation_angle // 90) | |
| return np.array(rotated_images), np.array(rotation_labels) | |
| def prepare_jigsaw_data(self, X, grid_size=3): | |
| """ | |
| Prepare data for jigsaw puzzle pretext task | |
| Args: | |
| X: Input images | |
| grid_size: Size of jigsaw grid | |
| Returns: | |
| Permuted images and permutation labels | |
| """ | |
| # Simple implementation: just shuffle patches | |
| permuted_images = [] | |
| permutation_labels = [] | |
| for image in X: | |
| # Divide image into patches | |
| h, w = image.shape[0] // grid_size, image.shape[1] // grid_size | |
| patches = [] | |
| for i in range(grid_size): | |
| for j in range(grid_size): | |
| patch = image[i*h:(i+1)*h, j*w:(j+1)*w, :] | |
| patches.append(patch) | |
| # Create a few random permutations | |
| for _ in range(5): # 5 random permutations per image | |
| perm = np.random.permutation(len(patches)) | |
| permuted = np.concatenate([ | |
| np.concatenate([patches[perm[i*grid_size + j]] for j in range(grid_size)], axis=1) | |
| for i in range(grid_size) | |
| ], axis=0) | |
| permuted_images.append(permuted) | |
| permutation_labels.append(hash(tuple(perm)) % 10) # Simplified | |
| return np.array(permuted_images), np.array(permutation_labels) | |
| def pretrain(self, X, pretext_task='rotation', epochs=50, batch_size=32, temperature=0.5): | |
| """ | |
| Perform self-supervised pre-training. | |
| - rotation: 4-way rotation prediction (sparse_categorical_crossentropy). | |
| - jigsaw: permutation classification (sparse_categorical_crossentropy). | |
| - contrastive: SimCLR-style NT-Xent over two augmented views per image. | |
| (Previously this branch trained a sigmoid head with sparse-categorical | |
| loss, which would not learn anything meaningful.) | |
| """ | |
| # Create pretext model | |
| self.create_pretext_model(pretext_task) | |
| if pretext_task == 'rotation': | |
| X_pretext, y_pretext = self.prepare_rotation_data(X) | |
| self.pretext_model.compile( | |
| optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4), | |
| loss='sparse_categorical_crossentropy', | |
| metrics=['accuracy'], | |
| ) | |
| history = self.pretext_model.fit( | |
| X_pretext, y_pretext, | |
| epochs=epochs, batch_size=batch_size, validation_split=0.2, verbose=1, | |
| ) | |
| return history | |
| if pretext_task == 'jigsaw': | |
| X_pretext, y_pretext = self.prepare_jigsaw_data(X) | |
| self.pretext_model.compile( | |
| optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4), | |
| loss='sparse_categorical_crossentropy', | |
| metrics=['accuracy'], | |
| ) | |
| history = self.pretext_model.fit( | |
| X_pretext, y_pretext, | |
| epochs=epochs, batch_size=batch_size, validation_split=0.2, verbose=1, | |
| ) | |
| return history | |
| if pretext_task == 'contrastive': | |
| self.pretext_model.compile( | |
| optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), | |
| loss=self._nt_xent_loss(temperature=temperature), | |
| ) | |
| # Custom batch loop: for each step we sample N images and form a | |
| # (2N, H, W, C) two-view batch in-memory. | |
| n = len(X) | |
| steps_per_epoch = max(1, n // batch_size) | |
| history = {'loss': []} | |
| rng = np.random.default_rng(seed=42) | |
| for epoch in range(epochs): | |
| epoch_losses = [] | |
| perm = rng.permutation(n) | |
| for step in range(steps_per_epoch): | |
| idx = perm[step * batch_size : (step + 1) * batch_size] | |
| if len(idx) == 0: | |
| continue | |
| batch_imgs = X[idx] | |
| two_view = self.make_two_view_batch(batch_imgs) | |
| # NT-Xent ignores y_true, but Keras requires a target tensor. | |
| dummy_y = np.zeros((two_view.shape[0],), dtype=np.float32) | |
| loss = self.pretext_model.train_on_batch(two_view, dummy_y) | |
| epoch_losses.append(float(loss)) | |
| mean_loss = float(np.mean(epoch_losses)) if epoch_losses else float('nan') | |
| history['loss'].append(mean_loss) | |
| print(f'[SSL contrastive] epoch {epoch + 1}/{epochs} loss={mean_loss:.4f}') | |
| return history | |
| raise ValueError(f'Unknown pretext task: {pretext_task}') | |
| def get_pretrained_encoder(self): | |
| """Get the pretrained encoder""" | |
| if self.encoder is None: | |
| raise ValueError("Encoder not created. Call pretrain() first.") | |
| return self.encoder | |
| def save_pretrained_encoder(self, save_path): | |
| """Save pretrained encoder""" | |
| if self.encoder is not None: | |
| self.encoder.save(save_path) | |
| def load_pretrained_encoder(self, model_path): | |
| """Load pretrained encoder""" | |
| self.encoder = tf.keras.models.load_model(model_path) | |
| return self.encoder | |
| class MedicalImageDataset: | |
| """ | |
| Dataset class for medical images with support for various modalities | |
| """ | |
| def __init__(self, data_dir, modality='mri', image_size=(224, 224)): | |
| """ | |
| Initialize medical image dataset | |
| Args: | |
| data_dir: Directory containing medical images | |
| modality: Imaging modality ('mri', 'ct', 'xray') | |
| image_size: Target image size | |
| """ | |
| self.data_dir = Path(data_dir) | |
| self.modality = modality | |
| self.image_size = image_size | |
| self.images = [] | |
| self.labels = [] | |
| self.metadata = [] | |
| def load_data(self): | |
| """Load medical images from directory""" | |
| # Placeholder - implement based on your data structure | |
| pass | |
| def augment(self, image, label): | |
| """Apply medical image-specific augmentations""" | |
| # Random rotations (medical images often have consistent orientation) | |
| if np.random.random() > 0.5: | |
| angle = np.random.uniform(-15, 15) | |
| image = tf.image.rot90(image, k=np.random.randint(0, 4)) | |
| # Random flips (only if anatomically appropriate) | |
| if np.random.random() > 0.5: | |
| image = tf.image.flip_left_right(image) | |
| # Random brightness/contrast adjustments | |
| image = tf.image.random_brightness(image, max_delta=0.1) | |
| image = tf.image.random_contrast(image, lower=0.9, upper=1.1) | |
| return image, label | |
| def create_dataset(self, batch_size=32, shuffle=True, augment=False): | |
| """Create TensorFlow dataset""" | |
| dataset = tf.data.Dataset.from_tensor_slices((self.images, self.labels)) | |
| if shuffle: | |
| dataset = dataset.shuffle(buffer_size=len(self.images)) | |
| if augment: | |
| dataset = dataset.map( | |
| lambda x, y: self.augment(x, y), | |
| num_parallel_calls=tf.data.AUTOTUNE | |
| ) | |
| dataset = dataset.batch(batch_size).prefetch(tf.data.AUTOTUNE) | |
| return dataset | |
| def create_medical_vision_transformer(input_shape=(224, 224, 3), num_classes=1000, patch_size=16): | |
| """ | |
| Create a Vision Transformer for 2D medical images | |
| Args: | |
| input_shape: Input image shape | |
| num_classes: Number of output classes | |
| patch_size: Size of patches | |
| Returns: | |
| ViT model | |
| """ | |
| inputs = layers.Input(shape=input_shape) | |
| # Patch extraction and embedding | |
| x = layers.Rescaling(1.0 / 255)(inputs) | |
| patches = layers.Conv2D( | |
| filters=64, | |
| kernel_size=patch_size, | |
| strides=patch_size, | |
| padding='same', | |
| activation='relu' | |
| )(x) | |
| # Reshape to sequence | |
| patch_shape = patches.shape[1:] | |
| patches = layers.Reshape((patch_shape[0] * patch_shape[1], patch_shape[2]))(patches) | |
| # Positional encoding | |
| num_patches = patch_shape[0] * patch_shape[1] | |
| positions = tf.range(start=0, limit=num_patches, delta=1) | |
| position_embedding = layers.Embedding(input_dim=num_patches, output_dim=64)(positions) | |
| patches = layers.Add()([patches, position_embedding]) | |
| # Transformer blocks | |
| for i in range(6): | |
| # Multi-head attention | |
| x = layers.LayerNormalization()(patches) | |
| attention_output = layers.MultiHeadAttention( | |
| num_heads=4, | |
| key_dim=64 | |
| )(x, x) | |
| x = layers.Add()([x, attention_output]) | |
| # MLP | |
| x = layers.LayerNormalization()(x) | |
| x = layers.Dense(128, activation='relu')(x) | |
| x = layers.Dense(64)(x) | |
| patches = layers.Add()([x, patches]) | |
| # Classification head | |
| x = layers.GlobalAveragePooling1D()(patches) | |
| x = layers.Dense(128, activation='relu')(x) | |
| outputs = layers.Dense(num_classes, activation='softmax')(x) | |
| return Model(inputs=inputs, outputs=outputs, name='medical_vit') | |
| def create_self_supervised_model(input_shape=(224, 224, 3), projection_dim=128): | |
| """ | |
| Create a model for self-supervised learning (SimCLR-style) | |
| Args: | |
| input_shape: Input image shape | |
| projection_dim: Dimension of projection head | |
| Returns: | |
| Base encoder model and projection model | |
| """ | |
| # Base encoder (ResNet-like) | |
| inputs = layers.Input(shape=input_shape) | |
| x = layers.Rescaling(1.0 / 255)(inputs) | |
| # Simple CNN encoder | |
| x = layers.Conv2D(32, 3, activation='relu', padding='same')(x) | |
| x = layers.MaxPooling2D()(x) | |
| x = layers.Conv2D(64, 3, activation='relu', padding='same')(x) | |
| x = layers.MaxPooling2D()(x) | |
| x = layers.Conv2D(128, 3, activation='relu', padding='same')(x) | |
| x = layers.MaxPooling2D()(x) | |
| x = layers.Conv2D(256, 3, activation='relu', padding='same')(x) | |
| x = layers.GlobalAveragePooling2D()(x) | |
| # Representation (for downstream tasks) | |
| representation = layers.Dense(256, activation='relu', name='representation')(x) | |
| # Projection head (for contrastive loss) | |
| projection = layers.Dense(projection_dim, name='projection')(representation) | |
| # Create models | |
| encoder = Model(inputs=inputs, outputs=representation, name='encoder') | |
| projection_model = Model(inputs=inputs, outputs=projection, name='projection_model') | |
| return encoder, projection_model |