| name | machine-learning |
| description | machine_learning skill Use when this capability is needed. |
| metadata | {"author":"brolag"} |
Machine_Learning
Machine Learning
Overview
PathML provides comprehensive machine learning capabilities for computational pathology, including pre-built models for nucleus detection and segmentation, PyTorch-integrated training workflows, public dataset access, and ONNX-based inference deployment. The framework seamlessly bridges image preprocessing with deep learning to enable end-to-end pathology ML pipelines.
Pre-Built Models
PathML includes state-of-the-art pre-trained models for nucleus analysis:
HoVer-Net
HoVer-Net (Horizontal and Vertical Network) performs simultaneous nucleus instance segmentation and classification.
Architecture:
- Encoder-decoder structure with three prediction branches:
- Nuclear Pixel (NP) - Binary segmentation of nuclear regions
- Horizontal-Vertical (HV) - Distance maps to nucleus centroids
- Classification (NC) - Nucleus type classification
Nucleus types:
- Epithelial
- Inflammatory
- Connective/Soft tissue
- Dead/Necrotic
- Background
Usage:
from pathml.ml import HoVerNet
import torch
model = HoVerNet(
num_types=5,
mode='fast',
pretrained=True
)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
tile_image = torch.from_numpy(tile.image).permute(2, 0, 1).unsqueeze(0).float()
tile_image = tile_image.to(device)
with torch.no_grad():
output = model(tile_image)
Post-processing:
from pathml.ml import hovernet_postprocess
instance_map, type_map = hovernet_postprocess(
np_pred=output['np'],
hv_pred=output['hv'],
nc_pred=output['nc']
)
HACTNet
HACTNet (Hierarchical Cell-Type Network) performs hierarchical nucleus classification with uncertainty quantification.
Features:
- Hierarchical classification (coarse to fine-grained types)
- Uncertainty estimation for predictions
- Improved performance on imbalanced datasets
from pathml.ml import HACTNet
model = HACTNet(
num_classes_coarse=3,
num_classes_fine=8,
pretrained=True
)
output = model(tile_image)
coarse_pred = output['coarse']
fine_pred = output['fine']
uncertainty = output['uncertainty']
Training Workflows
Dataset Preparation
PathML provides PyTorch-compatible dataset classes:
TileDataset:
from pathml.ml import TileDataset
from pathml.core import SlideDataset
tile_dataset = TileDataset(
slide_dataset,
tile_size=256,
transform=None
)
image, label = tile_dataset[0]
DataModule Integration:
from pathml.ml import PathMLDataModule
data_module = PathMLDataModule(
train_dataset=train_tile_dataset,
val_dataset=val_tile_dataset,
test_dataset=test_tile_dataset,
batch_size=32,
num_workers=4
)
trainer = pl.Trainer(max_epochs=100)
trainer.fit(model, data_module)
Training HoVer-Net
Complete workflow for training HoVer-Net on custom data:
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from pathml.ml import HoVerNet
from pathml.ml.datasets import PanNukeDataModule
data_module = PanNukeDataModule(
data_dir='path/to/pannuke',
batch_size=8,
num_workers=4,
tissue_types=['Breast', 'Colon']
)
model = HoVerNet(
num_types=5,
mode='fast',
pretrained=False
)
class HoVerNetLoss(nn.Module):
def __init__(self):
super().__init__()
self.mse_loss = nn.MSELoss()
self.bce_loss = nn.BCEWithLogitsLoss()
self.ce_loss = nn.CrossEntropyLoss()
def forward(self, output, target):
np_loss = self.bce_loss(output['np'], target['np'])
hv_loss = self.mse_loss(output['hv'], target['hv'])
nc_loss = .ce_loss(output[], target[])
total_loss = np_loss + hv_loss + * nc_loss
total_loss, {: np_loss, : hv_loss, : nc_loss}
criterion = HoVerNetLoss()
optimizer = torch.optim.Adam(
model.parameters(),
lr=,
weight_decay=
)
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer,
mode=,
factor=,
patience=
)
device = torch.device( torch.cuda.is_available() )
model = model.to(device)
num_epochs =
epoch (num_epochs):
model.train()
train_loss =
batch data_module.train_dataloader():
images = batch[].to(device)
targets = {
: batch[].to(device),
: batch[].to(device),
: batch[].to(device)
}
optimizer.zero_grad()
outputs = model(images)
loss, loss_dict = criterion(outputs, targets)
loss.backward()
optimizer.step()
train_loss += loss.item()
model.()
val_loss =
torch.no_grad():
batch data_module.val_dataloader():
images = batch[].to(device)
targets = {
: batch[].to(device),
: batch[].to(device),
: batch[].to(device)
}
outputs = model(images)
loss, _ = criterion(outputs, targets)
val_loss += loss.item()
scheduler.step(val_loss)
()
()
()
(epoch + ) % == :
torch.save({
: epoch,
: model.state_dict(),
: optimizer.state_dict(),
: val_loss,
}, )
PyTorch Lightning Integration
PathML models integrate with PyTorch Lightning for streamlined training:
import pytorch_lightning as pl
from pathml.ml import HoVerNet
from pathml.ml.datasets import PanNukeDataModule
class HoVerNetModule(pl.LightningModule):
def __init__(self, num_types=5, lr=1e-4):
super().__init__()
self.model = HoVerNet(num_types=num_types, pretrained=True)
self.lr = lr
self.criterion = HoVerNetLoss()
def forward(self, x):
return self.model(x)
def training_step(self, batch, batch_idx):
images = batch['image']
targets = {
'np': batch['np_map'],
'hv': batch['hv_map'],
'nc': batch['type_map']
}
outputs = self(images)
loss, loss_dict = self.criterion(outputs, targets)
self.log('train_loss', loss, prog_bar=True)
for key, val in loss_dict.items():
self.log(f'train_{key}_loss', val)
return loss
():
images = batch[]
targets = {
: batch[],
: batch[],
: batch[]
}
outputs = (images)
loss, loss_dict = .criterion(outputs, targets)
.log(, loss, prog_bar=)
key, val loss_dict.items():
.log(, val)
loss
():
optimizer = torch.optim.Adam(.parameters(), lr=.lr)
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode=, factor=, patience=
)
{
: optimizer,
: {
: scheduler,
:
}
}
data_module = PanNukeDataModule(data_dir=, batch_size=)
model = HoVerNetModule(num_types=, lr=)
trainer = pl.Trainer(
max_epochs=,
accelerator=,
devices=,
callbacks=[
pl.callbacks.ModelCheckpoint(monitor=, mode=),
pl.callbacks.EarlyStopping(monitor=, patience=)
]
)
trainer.fit(model, data_module)
Public Datasets
PathML provides convenient access to public pathology datasets:
PanNuke Dataset
PanNuke contains 7,901 histology image patches from 19 tissue types with nucleus annotations for 5 cell types.
from pathml.ml.datasets import PanNukeDataModule
pannuke = PanNukeDataModule(
data_dir='path/to/pannuke',
batch_size=16,
num_workers=4,
tissue_types=None,
fold='all'
)
train_loader = pannuke.train_dataloader()
val_loader = pannuke.val_dataloader()
test_loader = pannuke.test_dataloader()
for batch in train_loader:
images = batch['image']
inst_map = batch['inst_map']
type_map = batch['type_map']
np_map = batch['np_map']
hv_map = batch['hv_map']
tissue_type = batch['tissue_type']
Tissue types available:
Breast, Colon, Prostate, Lung, Kidney, Stomach, Bladder, Esophagus, Cervix, Liver, Thyroid, Head & Neck, Testis, Adrenal, Pancreas, Bile Duct, Ovary, Skin, Uterus
TCGA Datasets
Access The Cancer Genome Atlas datasets:
from pathml.ml.datasets import TCGADataModule
tcga = TCGADataModule(
data_dir='path/to/tcga',
cancer_type='BRCA',
batch_size=32,
tile_size=224
)
Custom Dataset Integration
Create custom datasets for PathML workflows:
from torch.utils.data import Dataset
import numpy as np
from pathlib import Path
class CustomPathologyDataset(Dataset):
def __init__(self, data_dir, transform=None):
self.data_dir = Path(data_dir)
self.image_paths = list(self.data_dir.glob('images/*.png'))
self.transform = transform
def __len__(self):
return len(self.image_paths)
def __getitem__(self, idx):
image_path = self.image_paths[idx]
image = np.array(Image.open(image_path))
annot_path = self.data_dir / 'annotations' / f'{image_path.stem}.npy'
annotation = np.load(annot_path)
if self.transform:
image = self.transform(image)
return {
'image': torch.from_numpy(image).permute(2, 0, 1).(),
: torch.from_numpy(annotation).long(),
: (image_path)
}
dataset = CustomPathologyDataset()
dataloader = DataLoader(dataset, batch_size=, shuffle=, num_workers=)
Data Augmentation
Apply augmentations to improve model generalization:
import albumentations as A
from albumentations.pytorch import ToTensorV2
train_transform = A.Compose([
A.RandomRotate90(p=0.5),
A.Flip(p=0.5),
A.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1, p=0.5),
A.GaussianBlur(blur_limit=(3, 7), p=0.3),
A.ElasticTransform(alpha=1, sigma=50, alpha_affine=50, p=0.3),
A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
ToTensorV2()
])
val_transform = A.Compose([
A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
ToTensorV2()
])
train_dataset = TileDataset(slide_dataset, transform=train_transform)
val_dataset = TileDataset(val_slide_dataset, transform=val_transform)
Model Evaluation
Metrics
Evaluate model performance with pathology-specific metrics:
from pathml.ml.metrics import (
dice_coefficient,
aggregated_jaccard_index,
panoptic_quality
)
dice = dice_coefficient(pred_mask, true_mask)
aji = aggregated_jaccard_index(pred_inst, true_inst)
pq, sq, rq = panoptic_quality(pred_inst, true_inst, pred_types, true_types)
print(f"Dice: {dice:.4f}")
print(f"AJI: {aji:.4f}")
print(f"PQ: {pq:.4f}, SQ: {sq:.4f}, RQ: {rq:.4f}")
Evaluation Loop
from pathml.ml.metrics import evaluate_hovernet
model.eval()
all_preds = []
all_targets = []
with torch.no_grad():
for batch in test_loader:
images = batch['image'].to(device)
outputs = model(images)
for i in range(len(images)):
inst_pred, type_pred = hovernet_postprocess(
outputs['np'][i],
outputs['hv'][i],
outputs['nc'][i]
)
all_preds.append({'inst': inst_pred, 'type': type_pred})
all_targets.append({
'inst': batch['inst_map'][i],
'type': batch['type_map'][i]
})
results = evaluate_hovernet(all_preds, all_targets)
print(f"Detection F1: {results['detection_f1']:.4f}")
print(f"Classification Accuracy: {results['classification_acc']:.4f}")
print(f"Panoptic Quality: {results['pq']:.4f}")
ONNX Inference
Deploy models using ONNX for production inference:
Export to ONNX
import torch
from pathml.ml import HoVerNet
model = HoVerNet(num_types=5, pretrained=True)
model.eval()
dummy_input = torch.randn(1, 3, 256, 256)
torch.onnx.export(
model,
dummy_input,
'hovernet_model.onnx',
export_params=True,
opset_version=11,
input_names=['input'],
output_names=['np_output', 'hv_output', 'nc_output'],
dynamic_axes={
'input': {0: 'batch_size'},
'np_output': {0: 'batch_size'},
'hv_output': {0: 'batch_size'},
'nc_output': {0: 'batch_size'}
}
)
ONNX Runtime Inference
import onnxruntime as ort
import numpy as np
session = ort.InferenceSession('hovernet_model.onnx')
input_name = session.get_inputs()[0].name
tile_image = preprocess_tile(tile)
outputs = session.run(None, {input_name: tile_image})
np_output, hv_output, nc_output = outputs
inst_map, type_map = hovernet_postprocess(np_output, hv_output, nc_output)
Batch Inference Pipeline
from pathml.core import SlideData
from pathml.preprocessing import Pipeline
import onnxruntime as ort
def run_onnx_inference_pipeline(slide_path, onnx_model_path):
wsi = SlideData.from_slide(slide_path)
wsi.generate_tiles(level=1, tile_size=256, stride=256)
session = ort.InferenceSession(onnx_model_path)
input_name = session.get_inputs()[0].name
results = []
for tile in wsi.tiles:
tile_array = preprocess_tile(tile.image)
outputs = session.run(None, {input_name: tile_array})
inst_map, type_map = hovernet_postprocess(*outputs)
results.append({
'coords': tile.coords,
'instance_map': inst_map,
'type_map': type_map
})
return results
results = run_onnx_inference_pipeline('slide.svs', 'hovernet_model.onnx')
Transfer Learning
Fine-tune pre-trained models on custom datasets:
from pathml.ml import HoVerNet
model = HoVerNet(num_types=5, pretrained=True)
for name, param in model.named_parameters():
if 'encoder' in name:
param.requires_grad = False
optimizer = torch.optim.Adam(
filter(lambda p: p.requires_grad, model.parameters()),
lr=1e-4
)
train_for_n_epochs(model, train_loader, optimizer, num_epochs=10)
for param in model.parameters():
param.requires_grad = True
optimizer = torch.optim.Adam(model.parameters(), lr=1e-5)
train_for_n_epochs(model, train_loader, optimizer, num_epochs=50)
Best Practices
-
Use pre-trained models when available:
- Start with pretrained=True for better initialization
- Fine-tune on domain-specific data
-
Apply appropriate data augmentation:
- Rotate, flip for orientation invariance
- Color jitter to handle staining variations
- Elastic deformation for biological variability
-
Monitor multiple metrics:
- Track detection, segmentation, and classification separately
- Use domain-specific metrics (AJI, PQ) beyond standard accuracy
-
Handle class imbalance:
- Weighted loss functions for rare cell types
- Oversampling minority classes
- Focal loss for hard examples
-
Validate on diverse tissue types:
- Ensure generalization across different tissues
- Test on held-out anatomical sites
-
Optimize for inference:
- Export to ONNX for faster deployment
- Batch tiles for efficient GPU utilization
- Use mixed precision (FP16) when possible
-
Save checkpoints regularly:
- Keep best model based on validation metrics
- Save optimizer state for training resumption
Common Issues and Solutions
Issue: Poor segmentation at nucleus boundaries
- Use HV maps (horizontal-vertical) to separate touching nuclei
- Increase weight of HV loss term
- Apply morphological post-processing
Issue: Misclassification of similar cell types
- Increase classification loss weight
- Add hierarchical classification (HACTNet)
- Augment training data for confused classes
Issue: Training unstable or not converging
- Reduce learning rate
- Use gradient clipping:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
- Check for data preprocessing issues
Issue: Out of memory during training
- Reduce batch size
- Use gradient accumulation
- Enable mixed precision training:
torch.cuda.amp
Issue: Model overfits to training data
- Increase data augmentation
- Add dropout layers
- Reduce model capacity
- Use early stopping based on validation loss
Additional Resources
Usage
Invoke this skill with:
$machine_learning [arguments]
Or let Codex auto-select based on your prompt.
Source: brolag/claude-code-templates — distributed by TomeVault.