Training a model¶
This page walks through a complete unsupervised training loop with the practices used across the MIR tutorials. The ready-to-run version for brain MRI is tutorials/lumir25_TransMorph.
1. Data¶
Training pairs are drawn at random: each moving image is paired with a different, randomly chosen fixed
image every epoch, so a cohort of N images gives N(N-1) possible pairs.
class PairDataset(torch.utils.data.Dataset):
def __init__(self, paths):
self.paths = paths
def __getitem__(self, i):
j = random.choice([k for k in range(len(self.paths)) if k != i])
load = lambda p: torch.from_numpy(nib.load(p).get_fdata()[None] / 255.).float()
return load(self.paths[i]), load(self.paths[j])
def __len__(self):
return len(self.paths)
loader = DataLoader(PairDataset(train_paths), batch_size=1, shuffle=True, num_workers=4, pin_memory=True)
2. Model, losses, optimizer¶
from MIR.models import TransMorphTVF, SpatialTransformer
from MIR.image_similarity import NCC_vxm
from MIR.deformation_regularizer import Grad3d
from MIR import RandomMultiContrastRemap
import MIR.models.configs_TransMorph as configs_TransMorph
H, W, D = 160, 224, 192
config = configs_TransMorph.get_3DTransMorph3Lvl_config()
config.img_size = (H // 2, W // 2, D // 2) # the network runs at half resolution
config.window_size = (H // 64, W // 64, D // 64)
model = TransMorphTVF(config, time_steps=7).cuda()
warp = SpatialTransformer((H, W, D)).cuda()
similarity, smoothness = NCC_vxm(), Grad3d(penalty='l2')
augment = RandomMultiContrastRemap(p=0.85) # contrast-agnostic training
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=0, amsgrad=True)
3. One training step, in both directions¶
The network predicts at half resolution; the flow is upsampled and the loss is computed on the full-resolution images. Each pair is used in both directions.
def step(mov, fix):
mov_half, fix_half = F.avg_pool3d(augment(mov, mov > 0), 2), F.avg_pool3d(augment(fix, fix > 0), 2)
flow = model((mov_half, fix_half))
flow = F.interpolate(flow, scale_factor=2, mode='trilinear', align_corners=False) * 2
loss = similarity(warp(mov, flow), fix) + 1.0 * smoothness(flow, fix)
optimizer.zero_grad()
loss.backward()
optimizer.step()
return loss.item()
for epoch in range(max_epoch):
for g in optimizer.param_groups: # polynomial learning-rate decay
g['lr'] = 1e-4 * (1 - epoch / max_epoch) ** 0.9
for mov, fix in loader:
mov, fix = mov.cuda(), fix.cuda()
step(mov, fix)
step(fix, mov)
4. Validation and checkpoints¶
Validate on labeled pairs with the nearest-neighbour transformer and keep the best checkpoints.
from MIR.accuracy_measures import dice_val_VOI
warp_nn = SpatialTransformer((H, W, D), mode='nearest').cuda()
model.eval()
with torch.no_grad():
scores = []
for mov, fix, mov_seg, fix_seg in val_loader:
flow = model((F.avg_pool3d(mov.cuda(), 2), F.avg_pool3d(fix.cuda(), 2)))
flow = F.interpolate(flow, scale_factor=2, mode='trilinear', align_corners=False) * 2
scores.append(dice_val_VOI(warp_nn(mov_seg.cuda(), flow).long(), fix_seg.cuda().long(),
num_clus=n_labels).item())
torch.save({'epoch': epoch, 'state_dict': model.state_dict(), 'optimizer': optimizer.state_dict()},
f'experiments/dsc{np.mean(scores):.4f}.pth.tar')
5. Variations¶
To add … |
Change |
|---|---|
Diffeomorphism |
|
Spatially varying regularization |
an SPR model, |
Label supervision |
a Dice term on warped one-hot labels (Losses and metrics). |
Multi-modal data |
|
Warm start |
load pretrained weights with |
Per-case refinement |
instance optimization: fine-tune a copy of the network for a few dozen steps on one pair at test time. |
Tip
MIR.utils.Logger mirrors print to a log file and AverageMeter tracks running means, as in
the tutorial scripts.