Tri-Netra-AI / src /advanced_models.py
anannyavyas1's picture
Upload folder using huggingface_hub
1cf3825 verified
Raw
History Blame Contribute Delete
34.4 kB
"""
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
@staticmethod
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
@staticmethod
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