Domain Adaptation: Medical Imaging with Scarce Labels
Learners will apply transfer learning from ImageNet to a chest X-ray dataset, implement class-weighted loss for imbalanced pathologies, and evaluate AUC-ROC.
Domain Adaptation: Medical Imaging with Scarce Labels is a free Machine Learning Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Machine Learning Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
The Medical Imaging Challenge
Medical imaging presents unique transfer learning challenges. Unlike natural photographs, chest X-rays, MRI scans, and histology slides look nothing like ImageNet images: they are grayscale or have domain-specific colour patterns, the features that matter (lesions, nodules, calcifications) are subtle and domain-specific, and labelled data requires expert radiologists — making large labelled datasets expensive and rare.
Despite these challenges, ImageNet pre-trained models consistently outperform training from scratch on medical imaging tasks, even when the visual appearance differs substantially. Universal low-level features (edge detectors, texture filters) transfer across domains, providing a strong initialisation that speeds convergence and improves generalisation with scarce labels.
The CheXpert Dataset: Multi-Label X-Ray Classification
CheXpert is a benchmark chest X-ray dataset with 224,316 images and 14 labels (Cardiomegaly, Pleural Effusion, Pneumonia, Atelectasis, etc.). In our scarce-label scenario, we simulate using only a small fraction — say 1% (about 2,243 images) — to mimic real-world clinical settings where annotation budget is limited.
This is a multi-label classification problem: each image can have multiple pathologies simultaneously, unlike single-label classification. The target is a vector of 14 binary values, and we use Binary Cross-Entropy with Logits (BCE) applied element-wise. Evaluation uses AUC-ROC per pathology, averaged across all 14 labels.
# Dataset setup (pseudo-code for illustration)
import torch
from torch.utils.data import Dataset
from PIL import Image
import pandas as pd
class CheXpertDataset(Dataset):
def __init__(self, csv_path, img_dir, transform=None):
self.df = pd.read_csv(csv_path)
self.img_dir = img_dir
self.transform = transform
self.labels = ['Atelectasis', 'Cardiomegaly',
'Consolidation', 'Edema', 'Pleural Effusion']
def __len__(self):
return len(self.df)
def __getitem__(self, idx):
img_path = self.df.iloc[idx]['Path']
img = Image.open(img_path).convert('RGB') # Convert grayscale to 3-ch
label = torch.tensor(self.df.iloc[idx][self.labels].values.astype(float))
if self.transform:
img = self.transform(img)
return img, labelConverting Grayscale to RGB for Pre-trained Models
Most medical images (X-rays, CT scans) are grayscale (1 channel), but ImageNet pre-trained models expect 3-channel RGB inputs. The simplest solution is Image.convert('RGB') which replicates the single channel three times, or transforms.Grayscale(num_output_channels=3) in the transform pipeline.
This is slightly wasteful — the three channels are identical — but in practice it works well because the model simply learns to weight all three channels equally. An alternative is to replace the first convolutional layer with a new Conv2d(1, 64, kernel_size=7, ...) and initialise it by averaging the three input channel weights. This is more principled but adds training complexity.
from torchvision import transforms
# Method 1: Replicate channel at loading time (simplest)
# img = Image.open(path).convert('RGB')
# Method 2: Use transforms
medical_transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.Grayscale(num_output_channels=3), # 1 -> 3 channels
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
# Method 3: Modify first conv layer for single-channel input
import torchvision.models as models
model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1)
# Average the 3-channel weights into 1 channel
w = model.conv1.weight.mean(dim=1, keepdim=True)
model.conv1 = torch.nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)
model.conv1.weight.data = wMulti-Label Loss: BCE with Logits
For multi-label classification, each label is an independent binary prediction. We use nn.BCEWithLogitsLoss() applied to a vector of 14 logits. The loss is computed element-wise and averaged across both the 14 classes and the batch size.
Imbalanced labels are a major challenge in medical imaging: only 5-10% of images have Pneumonia or Consolidation, while 40-60% have Pleural Effusion. Pass pos_weight to BCEWithLogitsLoss to upweight the rare positive class: a pos_weight of 10 makes the model pay 10× more attention to positive examples of that pathology.
import torch
import torch.nn as nn
# Multi-label BCE loss
criterion = nn.BCEWithLogitsLoss()
# With class weighting for imbalanced pathologies
# Compute pos_weight from training data frequencies
pos_counts = train_labels.sum(dim=0) # Positives per class
neg_counts = len(train_labels) - pos_counts
pos_weight = (neg_counts / pos_counts.clamp(min=1)).clamp(max=20) # Cap at 20
criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)
print('pos_weight per label:', pos_weight)
# High values mean that class is rare and needs more emphasisModel Architecture for Multi-Label Output
Replace the ResNet-50 classification head with a linear layer that outputs 14 logits (one per pathology), rather than the default 1000 for ImageNet. We do not apply sigmoid in the forward pass — the BCEWithLogitsLoss applies it internally for numerical stability. At inference time, apply sigmoid manually to get probabilities.
Adding a dropout layer before the final linear layer is especially important with scarce data, as regularisation prevents the small head from overfitting. A dropout probability of 0.3-0.5 is typical for medical imaging fine-tuning.
import torchvision.models as models
import torch.nn as nn
NUM_CLASSES = 14 # One per CheXpert pathology label
model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1)
# Replace with multi-label head
model.fc = nn.Sequential(
nn.Dropout(p=0.4),
nn.Linear(model.fc.in_features, NUM_CLASSES)
)
# No sigmoid here - BCEWithLogitsLoss handles it internally
print('Model output shape for batch of 16:',
model(torch.randn(16, 3, 224, 224)).shape) # (16, 14)AUC-ROC: The Right Metric for Medical Tasks
Accuracy is useless for imbalanced medical datasets. If only 5% of patients have Pneumonia, a model that always predicts 'no pneumonia' achieves 95% accuracy while being completely useless clinically. AUC-ROC (Area Under the ROC Curve) measures discrimination ability across all thresholds.
We compute AUC-ROC for each of the 14 pathologies separately and report the mean AUC across all labels. AUC of 0.5 is random chance; 0.7 is acceptable; 0.85+ is clinical grade; 0.9+ often approaches radiologist-level performance. scikit-learn's roc_auc_score computes this efficiently.
import numpy as np
from sklearn.metrics import roc_auc_score
import torch
def evaluate_auc(model, loader, device):
model.eval()
all_logits, all_labels = [], []
with torch.no_grad():
for images, labels in loader:
logits = model(images.to(device))
all_logits.append(torch.sigmoid(logits).cpu().numpy())
all_labels.append(labels.numpy())
probs = np.vstack(all_logits) # (N, 14)
targets = np.vstack(all_labels) # (N, 14)
# AUC per class, then average
aucs = [roc_auc_score(targets[:, i], probs[:, i])
for i in range(targets.shape[1])]
return np.mean(aucs), aucsTraining with Scarce Labels: Key Techniques
When you have only hundreds or a few thousand labelled medical images, several techniques help maximally exploit the available data. Strong data augmentation is the most important: random flips, rotations, contrast and brightness jitter all help, while keeping augmentations clinically plausible (a chest X-ray should not be vertically flipped — that would never occur in clinical practice).
Progressive resizing (training initially at lower resolution, then increasing) is another effective technique. Start at 128×128 to quickly iterate, then fine-tune at 224×224 or even 320×320 for final accuracy. This is much faster than always training at full resolution and often matches full-resolution accuracy.
from torchvision import transforms
# Clinically appropriate augmentations for chest X-rays
medical_aug = transforms.Compose([
transforms.RandomResizedCrop(224, scale=(0.85, 1.0)), # Mild crop
transforms.RandomHorizontalFlip(p=0.5), # OK: X-rays can be flipped
# transforms.RandomVerticalFlip(p=0.5), # NOT OK: clinically invalid
transforms.ColorJitter(brightness=0.2, contrast=0.3), # Simulate scan variation
transforms.RandomRotation(degrees=10), # Slight mis-alignment
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])
])Self-Supervised Pre-Training for Medical Imaging
When ImageNet features do not transfer well enough, self-supervised pre-training on unlabelled medical images is a powerful alternative. Methods like SimCLR, MoCo, and DINO learn representations by training the model to recognise that two augmented versions of the same image are similar, without any labels.
The workflow: (1) pre-train on large unlabelled medical datasets (CheXpert has 224K unlabelled images), (2) fine-tune with the small labelled set. This consistently outperforms ImageNet transfer on medical tasks because the model learns features specific to the medical domain rather than general object recognition features.
# Self-supervised pre-training concept (SimCLR-style)
# No labels needed during this phase
class SimCLRLoss(torch.nn.Module):
def __init__(self, temperature=0.07):
super().__init__()
self.temperature = temperature
def forward(self, z_i, z_j):
# z_i, z_j: augmented views of the same images
# Maximise agreement between paired views
# Minimise agreement between all other pairs in batch
z = torch.cat([z_i, z_j], dim=0)
z = torch.nn.functional.normalize(z, dim=1)
sim = torch.matmul(z, z.T) / self.temperature
# Contrastive loss computation...
return sim # Simplified illustrationUncertainty Quantification in Medical AI
In medical decision support, knowing how confident a model is is as important as the prediction itself. A model that says 'Pneumonia: 95% probability' should be more trusted than one saying '52% probability'. Standard softmax probabilities are often overconfident and do not represent true uncertainty.
Monte Carlo Dropout (MC Dropout) approximates Bayesian uncertainty by keeping dropout active at inference time and running forward pass multiple times. The variance of predictions across runs estimates uncertainty. Predictions with high variance should be flagged for human review rather than acted upon automatically.
import torch
def mc_dropout_predict(model, x, n_samples=30):
model.train() # Keep dropout active during inference
predictions = []
with torch.no_grad():
for _ in range(n_samples):
logits = model(x)
probs = torch.sigmoid(logits)
predictions.append(probs)
preds = torch.stack(predictions) # (n_samples, batch, num_classes)
mean_pred = preds.mean(dim=0) # Average prediction
uncertainty = preds.std(dim=0) # Std = uncertainty estimate
return mean_pred, uncertainty
# High uncertainty cases should be reviewed by a radiologist
MEAN_THRESHOLD = 0.5
UNCERTAINTY_THRESHOLD = 0.15Ethical Considerations in Medical ML
Deploying ML models in medical settings carries serious ethical responsibilities. Dataset bias is a critical concern: models trained predominantly on images from one hospital's scanners, patient demographics, or scanner manufacturers may fail on images from different settings — a known problem in radiology AI.
Before deployment, always evaluate model performance across demographic subgroups (age, sex, race) and across different scanning equipment. Regulatory frameworks like the FDA's AI/ML-based Software as a Medical Device (SaMD) guidelines in the US and the EU Medical Device Regulation (MDR) require rigorous clinical validation before deployment. Models should assist radiologists, not replace them — especially on high-stakes pathologies where errors have life-or-death consequences.
Quick Check
Test your understanding of domain adaptation for medical imaging from this lesson.
Lesson Recap
In this lesson you learned: medical imaging transfer learning applies ImageNet pre-trained models to domains with grayscale images and scarce labels by converting channels and replacing the classification head, multi-label BCE loss with pos_weight handles the extreme class imbalance typical of pathology datasets, and AUC-ROC is the correct evaluation metric that measures discrimination ability independent of class balance. With these transfer learning skills mastered, you are ready to explore NLP with BERT in the next course.
Frequently asked questions
Is the “Domain Adaptation: Medical Imaging with Scarce Labels” lesson free?
Yes — the full text of “Domain Adaptation: Medical Imaging with Scarce Labels” is free to read here on the web, and the Machine Learning Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Machine Learning Academy course, upgrade to CoddyKit PRO.
What will I learn in “Domain Adaptation: Medical Imaging with Scarce Labels”?
Learners will apply transfer learning from ImageNet to a chest X-ray dataset, implement class-weighted loss for imbalanced pathologies, and evaluate AUC-ROC. You practise Machine Learning Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Machine Learning Academy?
No prior experience is required. Machine Learning Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Domain Adaptation: Medical Imaging with Scarce Labels” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Machine Learning Academy lesson?
Yes. Every Machine Learning Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Pre-trained Models in torchvision: ResNet, EfficientNet, and ViT
- Feature Extraction: Freezing the Backbone
- Fine-Tuning: Unfreezing and Low Learning Rates
- Domain Adaptation: Medical Imaging with Scarce Labels