Multispectral 7 Tesla MRI as a potential predictor of dopamine transporter deficiency in Parkinson's disease.
The 9 matches
- [1] § Methods › Contrastive embedder ↔ embedder/trainer.py, lines 138–165 · score 0.94 · additive Gaussian noise, monotone gamma, channel gain, low rank, feature dropout, shifts
- [2] § Methods › DaT predictor ↔ nt_mlp/trainer.py, lines 16–64 · score 0.83 · cosine annealing scheduler, mixed precision, AdamW, weight decay, batch, train
- [3] § Methods › Contrastive embedder ↔ embedder/model.py, lines 84–100 · score 0.72 · ReLU, layer normalization, predicts subject, subject head, hidden, dropout
- [4] § Methods › Contrastive embedder ↔ nt_mlp/trainer.py, lines 16–64 · score 0.69 · mixed precision, AdamW, weight decay, batch, losses, train
- [5] § Methods › Contrastive embedder ↔ embedder/trainer.py, lines 22–103 · score 0.69 · weight decay, adversarial losses, mixed precision, adversarial heads, encoder, batch
- [6] § Methods › Contrastive embedder ↔ embedder/trainer.py, lines 22–103 · score 0.63 · hidden layers, adversarial heads, coordinate head, architectural, dimensional, encoder
- [7] § Methods › Contrastive embedder ↔ embedder/model.py, lines 190–229 · score 0.60 · ReLU, layer normalization, vectors, hidden, MLP, encoder
- [8] § Methods › Contrastive embedder ↔ nt_mlp/core_model.py, lines 25–67 · score 0.58 · ReLU, layer normalization, activation, MLP, hidden, dropout
- [9] § Results › Quantitative ROI analysis ↔ nt_mlp/cv_evaluator.py, lines 887–1006 · score 0.52 · predicted binding potentials, boxplots, FDR, fold, striatum, HC
Paper
Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC
The paper is loaded when this pane is shown.
The authors' code
Python · 637 lines · 25 KB · MIT · 3 matches
- import torch
- import torch.optim as optim
- from torch.utils.data import DataLoader
- import torch.nn.functional as F
- import numpy as np
- from pathlib import Path
- import json
- import argparse
- import math
- import matplotlib.pyplot as plt
- from augs import (
- Compose, RandomGlobalGainShift, RandomChannelLowRankGain,
- BatchPartitionGain, RandomMonotoneGammaWarp, AdditiveGaussianNoise,
- FeatureDropout
- )
- from model import *
- from data_handler import ContrastiveDataHandler
- def parse_args():
- """Parse command-line arguments for contrastive learning training.
- Defaults are set to match the values used in run_experiment.py.
- """
- parser = argparse.ArgumentParser(description='Contrastive Learning for MRI Voxels')
- # Required arguments
- parser.add_argument('--subjects', nargs='+', required=True,
- help='List of training subject IDs')
- parser.add_argument('--exp_name', type=str, required=True,
- help='Experiment name for saving outputs')
- # Data settings
- parser.add_argument('--test_subjects', nargs='+', default=None,
- help='Test subject IDs (for visualization only)')
- parser.add_argument('--fname', type=str, default="train_data.npy",
- help='Data filename to load from each subject directory')
- # Training hyperparameters (defaults from run_experiment.py)
- parser.add_argument('--steps', type=int, default=4000,
- help='Number of training steps')
- parser.add_argument('--batch_vox', type=int, default=16384,
- help='Number of voxels per batch')
- parser.add_argument('--lr', type=float, default=1e-4,
- help='Learning rate')
- parser.add_argument('--weight_decay', type=float, default=1e-2,
- help='Weight decay (L2 regularization)')
- parser.add_argument('--temperature', type=float, default=0.07,
- help='Temperature for contrastive loss')
- parser.add_argument('--random_seed', type=int, default=66,
- help='Random seed for reproducibility')
- # Model architecture (defaults from run_experiment.py)
- parser.add_argument('--embedding_dim', type=int, default=16,
- help='Embedding dimension')
- parser.add_argument('--hidden_dims', nargs='+', type=int, default=[2048, 2048, 2048, 2048],
- help='Hidden layer dimensions')
- parser.add_argument('--dropout', type=float, default=0.3,
- help='Dropout rate for encoder')
- parser.add_argument('--hidden_dim_adv', type=int, default=512,
- help='Hidden dimension for adversarial heads')
- parser.add_argument('--adv_dropout', type=float, default=0.3,
- help='Dropout rate for adversarial heads')
- # Learning rate warmup
- parser.add_argument('--warmup_steps', type=int, default=0,
- help='Linear LR warmup steps (0 disables warmup)')
- parser.add_argument('--warmup_start_factor', type=float, default=0.1,
- help='Warmup start factor relative to lr')
- # Loss weights (defaults from run_experiment.py)
- parser.add_argument('--w_inst', type=float, default=1.0,
- help='Weight for instance contrastive loss')
- parser.add_argument('--w_coord_adv', type=float, default=20.0,
- help='Weight for coordinate adversary loss')
- parser.add_argument('--w_subj_adv', type=float, default=20.0,
- help='Weight for subject adversary loss')
- # Adversarial training (defaults from run_experiment.py)
- parser.add_argument('--use_coord_adv', action='store_true',
- help='Use coordinate adversary (enabled in run_experiment)')
- parser.add_argument('--use_subject_adv', action='store_true',
- help='Use subject adversary (enabled in run_experiment)')
- parser.add_argument('--coordHeadBins', type=int, default=16,
- help='Number of bins for coordinate head')
- # Augmentation (defaults from run_experiment.py)
- parser.add_argument('--use_weak_strong_aug', action='store_true',
- help='Use weak vs strong augmentations (enabled in run_experiment)')
- parser.add_argument('--weak_strength', type=float, default=0.30,
- help='Weak view magnitude in [0,1] (scaled into each op range)')
- parser.add_argument('--strong_strength', type=float, default=0.75,
- help='Strong view magnitude in [0,1] (scaled into each op range)')
- parser.add_argument('--aug_ramp', action='store_true',
- help='Cosine-ramp augmentation strength (enabled in run_experiment)')
- # Training options
- parser.add_argument('--amp_scaler', action='store_true',
- help='Use mixed precision training (enabled in run_experiment)')
- return parser.parse_args()
- def setup_experiment_dir(exp_name):
- """Create experiment directory and return path."""
- exp_dir = Path(f"experiments/{exp_name}")
- exp_dir.mkdir(parents=True, exist_ok=True)
- return exp_dir
- def save_config(exp_dir, args):
- """Save experiment configuration."""
- config = vars(args)
- with open(exp_dir / "config.json", 'w') as f:
- json.dump(config, f, indent=2)
- def digitize(v, edges, bins):
- """Discretize continuous values into bins.
- Args:
- v: Continuous values to discretize
- edges: Bin edges
- bins: Number of bins
- Returns:
- Integer bin indices in [0, bins-1]
- """
- idx = torch.bucketize(v, edges[1:-1]) # exclude first/last edge
- return idx.clamp_(0, bins-1)
- def _lerp(a, b, t):
- return a + (b - a) * t
- def cosine_ramp(t: float) -> float:
- t = max(0.0, min(1.0, t))
- return 0.5 * (1.0 - math.cos(math.pi * t))
- def build_augment_from_strength(m: float) -> Compose:
- """Magnitude m in [0,1] controls intensity per operator via interpolation."""
- # Global/channel gain-shift
- gain_logstd = _lerp(0.05, 0.30, m)
- bias_std = _lerp(0.02, 0.1, m)
- rank = int(round(_lerp(6, 18, m)))
- gain_std = _lerp(0.12, 0.30, m)
- # Batch/domain randomization
- n_groups = int(round(_lerp(4, 12, m)))
- part_logstd = _lerp(0.12, 0.30, m)
- # Monotone gamma warp
- delta = _lerp(0.15, 0.40, m)
- gamma_p = _lerp(0.6, 1.0, m)
- # Noise + feature dropout
- sigma = _lerp(0.03, 0.10, m)
- p_drop = _lerp(0.1, 0.50, m)
- return Compose(
- RandomGlobalGainShift(gain_logstd=gain_logstd, bias_std=bias_std, p=0.75),
- RandomChannelLowRankGain(feat_dim=349, rank=rank, gain_std=gain_std, p=0.75),
- BatchPartitionGain(n_groups=n_groups, logstd=part_logstd, p=0.75),
- RandomMonotoneGammaWarp(delta=delta, per_feature=True, p=0.75, eps=1e-4),
- AdditiveGaussianNoise(sigma=sigma, p=1.0),
- FeatureDropout(p_drop=p_drop),
- )
- def softmax_top1_acc(logits, y):
- return (logits.argmax(dim=1) == y).float().mean()
- def q_edges(v, bins):
- qs = torch.linspace(0, 1, bins+1).to(v.device)
- e = torch.quantile(v, qs, interpolation="linear")
- e[0], e[-1] = 0.0, 1.0 # clamp ends
- # enforce strict monotonicity to avoid bucketize ties
- for i in range(1, e.numel()):
- if e[i] <= e[i-1]:
- e[i] = min(1.0, e[i-1] + 1e-6)
- return e
- def main():
- args = parse_args()
- set_random_seeds(args.random_seed)
- print(f"Random seed set to: {args.random_seed}")
- # Setup experiment
- exp_dir = setup_experiment_dir(args.exp_name)
- save_config(exp_dir, args)
- print(f"Experiment directory: {exp_dir}")
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
- print(f"Using device: {device}")
- # Enable TF32 for additional speed on Ampere+ (safe for training)
- if torch.cuda.is_available():
- torch.backends.cuda.matmul.allow_tf32 = True
- torch.backends.cudnn.allow_tf32 = True
- # Decide AMP dtype based on hardware support (prefer BF16)
- bf16_ok = False
- if torch.cuda.is_available():
- if hasattr(torch.cuda, "is_bf16_supported") and torch.cuda.is_bf16_supported():
- bf16_ok = True
- else:
- try:
- major, _ = torch.cuda.get_device_capability()
- if major >= 8:
- bf16_ok = True
- except Exception:
- pass
- autocast_dtype = torch.bfloat16 if bf16_ok else torch.float16
- print(f"AMP: enabled={bool(args.amp_scaler)} | dtype={autocast_dtype} | bf16_supported={bf16_ok}")
- # !!! augs assume z-scored features
- # Pre-build static augment if not using weak/strong policy
- use_ws = args.use_weak_strong_aug
- if not use_ws:
- augment = Compose(
- AdditiveGaussianNoise(sigma=0.05, p=1.0),
- FeatureDropout(p_drop=0.3),
- )
- else:
- augment = None # will build per-step weak/strong aug below
- # Data setup
- dataset = ContrastiveDataHandler(subject_ids=args.subjects,
- batch_vox=args.batch_vox,
- fname=args.fname, augment=augment)
- # Model setup
- base_encoder = VoxelEncoder(
- in_dim=349,
- hidden_dims=args.hidden_dims,
- embedding_dim=args.embedding_dim,
- dropout=args.dropout
- ).to(device)
- # Wrap with adversarial heads for GRL training
- model = VoxelEncoderAdv(
- base_encoder=base_encoder,
- embedding_dim=args.embedding_dim,
- use_coord_adv=args.use_coord_adv,
- use_subject_adv=args.use_subject_adv,
- n_subjects=dataset.n_subj,
- adv_hidden=args.hidden_dim_adv,
- adv_dropout=args.adv_dropout,
- coordHeadBins=args.coordHeadBins,
- ).to(device)
- # Optional compile for speed (PyTorch 2.x)
- if hasattr(torch, "compile"):
- try:
- model = torch.compile(model)
- print("Compiled model with torch.compile (max-autotune)")
- except Exception:
- print("torch.compile not available or failed; continuing without compile")
- crit = WeightedSupConLoss(temperature=args.temperature)
- try:
- optimizer = optim.AdamW(model.parameters(), lr=args.lr, weight_decay=args.weight_decay, fused=True)
- print("Using fused AdamW optimizer")
- except TypeError:
- optimizer = optim.AdamW(model.parameters(), lr=args.lr, weight_decay=args.weight_decay)
- #optimizer = optim.AdamW(model.parameters(), lr=args.lr, weight_decay=args.weight_decay)
- # Learning-rate schedule with optional linear warmup into cosine decay
- if args.warmup_steps > 0:
- total_after_warmup = max(1, args.steps - args.warmup_steps)
- warmup = optim.lr_scheduler.LinearLR(
- optimizer,
- start_factor=max(1e-6, min(1.0, args.warmup_start_factor)),
- total_iters=args.warmup_steps,
- )
- cosine = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=total_after_warmup)
- sched = optim.lr_scheduler.SequentialLR(
- optimizer,
- schedulers=[warmup, cosine],
- milestones=[args.warmup_steps],
- )
- print(f"Using LR warmup: {args.warmup_steps} steps, start_factor={args.warmup_start_factor}")
- else:
- sched = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=args.steps)
- print(f"Training with {len(args.subjects)} subjects: {args.subjects}")
- # Test BEFORE training
- print("=== BEFORE TRAINING ===")
- # Training loop
- loss_history = []
- # Track individual loss components for plotting
- contr_history = []
- coord_history = []
- subj_history = []
- log_steps = []
- print(f"\n=== TRAINING FOR {args.steps} STEPS ===")
- loader = torch.utils.data.DataLoader(
- dataset,
- batch_size=None, # iterable yields whole batches
- num_workers=8, # start with 4–8, tune
- pin_memory=True,
- persistent_workers=True,
- prefetch_factor=2
- )
- data_iter = iter(loader)
- edges_x = edges_y = edges_z = None
- if args.use_coord_adv:
- #average of some batches for stable bin edges
- with torch.no_grad():
- all_coords = []
- n_avg = 20
- for _ in range(n_avg):
- try:
- _, coords, _ = next(data_iter)
- except StopIteration:
- data_iter = iter(loader)
- _, coords, _ = next(data_iter)
- all_coords.append(coords)
- all_coords = torch.cat(all_coords,0)
- edges_x = q_edges(all_coords[:,0], args.coordHeadBins).to(device)
- edges_y = q_edges(all_coords[:,1], args.coordHeadBins).to(device)
- edges_z = q_edges(all_coords[:,2], args.coordHeadBins).to(device)
- #reset data_iter to start of epoch
- data_iter = iter(loader)
- warmup_steps = max(1, int(args.steps * 0)) # avoid div by zero
- model.set_grl_lambdas(coord=1.0 if args.use_coord_adv else 0.0,
- subj=1.0 if args.use_subject_adv else 0.0)
- for step in range(args.steps):
- # ramp *before* forward so this step uses current weights
- ramp = min(1.0, step / warmup_steps)
- # Backward-compat: map deprecated lambda_* to weights if new weights are unset
- w_coord = args.w_coord_adv if (args.w_coord_adv is not None) else args.lambda_coord
- w_subj = args.w_subj_adv if (args.w_subj_adv is not None) else args.lambda_subject
- # Effective ramped weights
- eff_w_coord = (w_coord if args.use_coord_adv else 0.0) * ramp
- eff_w_subj = (w_subj if args.use_subject_adv else 0.0) * ramp
- # Set GRL λ to 1.0 (when enabled) so strength is controlled via loss weights
- optimizer.zero_grad(set_to_none=True)
- try:
- feats, coords, sids = next(data_iter)
- except StopIteration:
- data_iter = iter(loader)
- feats, coords, sids = next(data_iter)
- # Transfer to GPU with non-blocking for async copy
- feats = feats.to(device, non_blocking=True)
- coords = coords.to(device, non_blocking=True)
- sids = sids.to(device, non_blocking=True)
- # Apply augmentations; either static (HA presets) or dynamic weak/strong
- if augment is not None:
- x1 = augment(feats)
- x2 = augment(feats)
- else:
- # dynamic policy
- if args.use_weak_strong_aug:
- t = step / float(max(1, args.steps - 1))
- ramp_factor = max(cosine_ramp(t) , 0.2) if args.aug_ramp else 1.0
- m_w = float(np.clip(args.weak_strength * ramp_factor, 0.0, 1.0))
- m_s = float(np.clip(args.strong_strength * ramp_factor, 0.0, 1.0))
- aug_w = build_augment_from_strength(m_w)
- aug_s = build_augment_from_strength(m_s)
- x1 = aug_w(feats)
- x2 = aug_s(feats)
- else:
- x1 = feats
- x2 = feats
- # Forward pass under autocast (compute in BF16/FP16)
- with torch.amp.autocast('cuda', enabled=bool(args.amp_scaler), dtype=autocast_dtype):
- compute_heads = (args.use_coord_adv or args.use_subject_adv)
- z1, coord1, subj1 = model(x1, compute_heads=compute_heads)
- z2, coord2, subj2 = model(x2, compute_heads=compute_heads)
- # Compute all losses in float32 for numerical stability
- z1f, z2f = z1.float(), z2.float()
- contr_loss = crit(z1f, z2f)
- # Start with contrastive loss
- loss = contr_loss
- if args.use_coord_adv:
- bins = args.coordHeadBins
- cx = digitize(coords[:, 0].contiguous(), edges_x, bins)
- cy = digitize(coords[:, 1].contiguous(), edges_y, bins)
- cz = digitize(coords[:, 2].contiguous(), edges_z, bins)
- logit_x1, logit_y1, logit_z1 = coord1
- logit_x2, logit_y2, logit_z2 = coord2
- # CE on float32 logits for stability
- coord_loss1 = (
- F.cross_entropy(logit_x1.float(), cx)
- + F.cross_entropy(logit_y1.float(), cy)
- + F.cross_entropy(logit_z1.float(), cz)
- ) / 3.0
- coord_loss2 = (
- F.cross_entropy(logit_x2.float(), cx)
- + F.cross_entropy(logit_y2.float(), cy)
- + F.cross_entropy(logit_z2.float(), cz)
- ) / 3.0
- coord_loss = (coord_loss1 + coord_loss2) / 2.0
- else:
- coord_loss = torch.tensor(0.0, device=device)
- if args.use_subject_adv:
- subj_loss1 = (
- F.cross_entropy(subj1.float(), sids)
- if subj1 is not None
- else torch.tensor(0.0, device=device)
- )
- subj_loss2 = (
- F.cross_entropy(subj2.float(), sids)
- if subj2 is not None
- else torch.tensor(0.0, device=device)
- )
- subj_loss = (subj_loss1 + subj_loss2) / 2.0
- else:
- subj_loss = torch.tensor(0.0, device=device)
- # Include head losses so heads train; scale by effective weights; encoder protection via --stop_grad_from_heads
- if args.use_coord_adv:
- loss = loss + eff_w_coord * coord_loss
- if args.use_subject_adv:
- loss = loss + eff_w_subj * subj_loss
- # Backward + step (no GradScaler for BF16 path)
- loss.backward()
- optimizer.step()
- sched.step()
- if step % 250 == 0 or step == args.steps - 1:
- loss_history.append(loss.item())
- # Store component losses and the step for plotting
- contr_history.append(float(contr_loss.item()))
- coord_history.append(float(coord_loss.item()))
- subj_history.append(float(subj_loss.item()))
- log_steps.append(int(step))
- # Lightweight monitoring metrics for logging cadence
- chance_logs = []
- with torch.no_grad():
- if args.use_subject_adv and (subj1 is not None and subj2 is not None):
- K = dataset.n_subj
- subj_acc1 = softmax_top1_acc(subj1, sids).item()
- subj_acc2 = softmax_top1_acc(subj2, sids).item()
- subj_acc = 0.5*(subj_acc1 + subj_acc2)
- subj_ch_acc = 1.0 / K
- subj_ch_ce = np.log(K)
- chance_logs.append(f"SubjAcc {subj_acc:.3f} (~{subj_ch_acc:.3f}), CE~{subj_ch_ce:.3f}")
- if args.use_coord_adv and (coord1 is not None and coord2 is not None):
- bins = args.coordHeadBins
- cx = digitize(coords[:, 0].contiguous(), edges_x, bins)
- cy = digitize(coords[:, 1].contiguous(), edges_y, bins)
- cz = digitize(coords[:, 2].contiguous(), edges_z, bins)
- (logit_x1, logit_y1, logit_z1) = coord1
- (logit_x2, logit_y2, logit_z2) = coord2
- ax_accs = []
- for (lx, ly, lz), (cx_, cy_, cz_) in [((logit_x1,logit_y1,logit_z1),(cx,cy,cz)),
- ((logit_x2,logit_y2,logit_z2),(cx,cy,cz))]:
- ax_accs.append((
- softmax_top1_acc(lx, cx_).item(),
- softmax_top1_acc(ly, cy_).item(),
- softmax_top1_acc(lz, cz_).item()
- ))
- ax_acc = tuple(np.mean([a[i] for a in ax_accs]) for i in range(3))
- coord_ch_acc = 1.0 / bins
- coord_ch_ce = np.log(bins)
- chance_logs.append(f"CoordAcc x/y/z {ax_acc[0]:.3f}/{ax_acc[1]:.3f}/{ax_acc[2]:.3f} (~{coord_ch_acc:.3f}), CE~{coord_ch_ce:.3f}")
- chance_str = " | ".join(chance_logs)
- print(
- f"Step {step:5d} | ramp {ramp:0.2f} | "
- f"Wc {eff_w_coord:0.2f} Ws {eff_w_subj:0.2f} | "
- f"ContLoss: {contr_loss.item():.4f} | "
- f"CoordLoss: {coord_loss.item():.4f} | "
- f"SubjLoss: {subj_loss.item():.4f} | "
- f"loss {loss.item():.4f} |"
- f"Alignment: {alignment(z1, z2):.4f} |"
- f"Uniformity z1: {uniformity(z1):.4f} |"
- f"Uniformity z2: {uniformity(z2):.4f} |"
- f"{chance_str}"
- )
- # Plot component losses + dotted sum line with legend
- save_loss_plot(exp_dir, log_steps, contr_history, coord_history, subj_history)
- # Save checkpoint and quick similarity stats
- torch.save({
- 'adv_model_state_dict': model.state_dict(),
- 'base_encoder_state_dict': base_encoder.state_dict(),
- 'optimizer_state_dict': optimizer.state_dict(),
- 'args': args,
- 'loss_history': loss_history
- }, exp_dir / "model_checkpoint.pth")
- with torch.no_grad():
- pos = (z1f * z2f).sum(dim=1) # cosine if z’s are L2-normalized of positive pairs
- idx = torch.randperm(z1f.size(0))[:2048] # subset for negatives
- z1s, z2s = z1f[idx], z2f[idx]
- neg = (z1s @ z2s.t())
- neg = neg[~torch.eye(neg.size(0), dtype=bool, device=neg.device)]
- print(f"Pos cos μ={pos.mean():.3f}, σ={pos.std():.3f} | Neg cos μ={neg.mean():.3f}, σ={neg.std():.3f}")
- S = z1f @ z2f.t()
- top1 = S.argmax(dim=1)
- inst_r1 = (top1 == torch.arange(S.size(0), device=S.device)).float().mean().item()
- print(f"Instance R@1: {inst_r1:.3f}")
- # Save loss history
- np.save(exp_dir / "loss_history.npy", np.array(loss_history))
- print(f"\nExperiment completed! Results saved to: {exp_dir}")
- def save_loss_plot(exp_dir: Path, steps: list, contr_hist: list, coord_hist: list, subj_hist: list):
- """Save a plot with separate loss components and a dotted sum line.
- This version avoids stretching artefacts in SVG viewers by:
- - keeping annotations inside the axes range
- - using a slightly more square aspect ratio
- - relaxing the tight bounding box so the axes, not labels, define width.
- Args:
- exp_dir: Output directory where the plot will be saved.
- steps: Logged training steps.
- contr_hist: Contrastive loss values.
- coord_hist: Coordinate loss values.
- subj_hist: Subject loss values.
- """
- steps_np = np.asarray(steps, dtype=float)
- if steps_np.size == 0:
- return
- contr_np = np.asarray(contr_hist, dtype=float)
- coord_np = np.asarray(coord_hist, dtype=float)
- subj_np = np.asarray(subj_hist, dtype=float)
- total_np = contr_np + coord_np + subj_np
- # Use a restrained, publication-friendly palette (ColorBrewer-inspired)
- colors = {
- "Contrastive": "#1b9e77",
- "Coordinate": "#d95f02",
- "Subject": "#7570b3",
- "Total": "#000000",
- }
- # Slightly more square figure for better appearance in wide editors
- fig, ax = plt.subplots(figsize=(5.0, 3.8), constrained_layout=True)
- # Plot individual components
- ax.plot(steps_np, contr_np, label="Contrastive", color=colors["Contrastive"], lw=1.8)
- ax.plot(steps_np, coord_np, label="Coordinate", color=colors["Coordinate"], lw=1.8)
- ax.plot(steps_np, subj_np, label="Subject", color=colors["Subject"], lw=1.8)
- # Dotted total line (subtle)
- ax.plot(steps_np, total_np, label="Total", color=colors["Total"], lw=1.4,
- linestyle=(0, (3, 2)))
- # Axis styling: remove clutter, subtle horizontal grid only
- ax.spines['top'].set_visible(False)
- ax.spines['right'].set_visible(False)
- ax.grid(axis='y', color="#dddddd", linewidth=0.6, alpha=0.6)
- ax.grid(axis='x', alpha=0.0)
- ax.set_xlabel("Step", labelpad=6)
- ax.set_ylabel("Loss", labelpad=6)
- ax.set_title("Training Loss Components", pad=10)
- # Ticks: modest sizing
- ax.tick_params(axis='both', labelsize=10, width=0.8, length=4)
- # Annotate last value of each component *inside* the axes to avoid
- # expanding the SVG width excessively when viewing the figure.
- if steps_np.size > 1:
- x_last = steps_np[-1]
- x_prev = steps_np[-2]
- # small offset but stay within [x_prev, x_last]
- x_off = 0.25 * (x_last - x_prev)
- x_anno = x_last - x_off
- else:
- x_anno = steps_np[0]
- for arr, name in [(contr_np, "Contrastive"), (coord_np, "Coordinate"), (subj_np, "Subject")]:
- if arr.size:
- ax.text(x_anno, arr[-1], f"{arr[-1]:.3g}",
- color=colors[name], fontsize=9, va='center', ha='right')
- # Legend without frame
- ax.legend(frameon=False, fontsize=10, handlelength=3, loc='upper right')
- # Save without an overly tight bbox so labels don't dominate width
- out_path = Path(exp_dir) / "loss_plot.svg"
- fig.savefig(out_path, dpi=300)
- plt.close(fig)
- if __name__ == "__main__":
- main()
trainer.py at commit a229605, under MIT · at the source
Overview
- Department of Computer Science, Friedrich-Alexander-Universität Erlangen-Nürnberg, Erlangen, Germany
- Department of Molecular Neurology, University Hospital Erlangen, Erlangen, Germany
- Institute of Neuroradiology, University Hospital Erlangen, Erlangen, Germany
- Institute of Radiology, University Hospital Erlangen, Erlangen, Germany
Abstract
The neuropathological hallmark of Parkinson’s disease (PD) is a progressive degeneration of dopaminergic neurons in the substantia nigra resulting in a reduced striatal dopamine transporter (DaT) concentration. Radioligands can detect degeneration of nigrostriatal projections early in the disease course, but their clinical use is constrained by ionizing radiation exposure and the need for radiotracer production and associated logistics. In this study, we investigate whether a healthy DaT atlas can be predicted directly from native 7 Tesla (7T) multicontrast magnetic resonance imaging (MRI) using a normative atlas as the supervised target for voxel-wise DaT density. Based on these results, we estimate the deviation of sporadic PD patients from the normative DaT atlas. Using this approach, we found that estimated DaT distributions in the putamen differ between PD patients and healthy controls, providing preliminary evidence that high-field advanced multispectral MRI could inform on neurochemical alterations in PD.
Reproduced under the paper's license (CC BY), from the paper cited above.
Repository
Its files are read in the Code ↔ Paper reader above, with 9 matches between paragraphs and lines of code.
mert-o/dat_prediction
a229605f6bc0b7220a6c5e52bcaa13473de3ff74, 28 January 2026Availability: 1 check, the latest on 28 September 2026: the link answers
- 28 September 2026: the link answers
15 files
- embedder/
augs.py , Python, 159 lines - embedder/
data_handler.py , Python, 183 lines - embedder/
embedder.py , Python, 227 lines - embedder/
model.py , Python, 311 lines, 2 matches - embedder/
trainer.py , Python, 637 lines, 3 matches - nt_mlp/
core_model.py , Python, 121 lines, 1 match - nt_mlp/
cv_evaluator.py , Python, 1,332 lines, 1 match - nt_mlp/
data_loader.py , Python, 326 lines - nt_mlp/
evaluate_simple.py , Python, 157 lines - nt_mlp/
evaluator.py , Python, 252 lines - nt_mlp/
train_simple.py , Python, 226 lines - nt_mlp/
trainer.py , Python, 292 lines, 2 matches - run_experiment.py, Python, 171 lines
- LICENSE, License, 21 lines
- README.md, Text, 136 lines
The paper's code and data availability statement is in the Data section.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 13 scripts, each with its path and the digest of its content;
- 9 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
No dataset and no data link were found in the paper.
Data and Code Availability
The data used in this study are available from the corresponding authors upon reasonable request. The preprocessing, training, and evaluation code are available at: https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 2, 28 September 2026
- Funding: added Deutsche Forschungsgemeinschaft: 500888779, 505539112, RU5534, KFO5024
Version 1, 28 September 2026: the first record
Recorded: type, language, journal, volume, pages, dates, 9 authors, 5 keywords, 54 references.
Cite
This paper
Özer, M., Egger, B., Mennecke, A., Nagel, A., Zaiss, M., Laun, F. B., Dörfler, A., Winkler, J., & German, A. (2026). Multispectral 7 Tesla MRI as a potential predictor of dopamine transporter deficiency in Parkinson's disease. Imaging neuroscience (Cambridge, Mass.), 4, IMAG.a.1241. https://
BibTeX
@article{ozer2026multisp
author = {Özer, Mert and Egger, Bernhard and Mennecke, Angelika and Nagel, Armin and Zaiss, Moritz and Laun, Frederik Bernd and Dörfler, Arnd and Winkler, Jürgen and German, Alexander},
title = {{Multispectral 7 Tesla MRI as a potential predictor of dopamine transporter deficiency in Parkinson's disease}},
journal = {Imaging neuroscience (Cambridge, Mass.)},
year = {2026},
month = may,
volume = {4},
pages = {IMAG.a.1241},
publisher = {MIT Press},
issn = {2837-6056},
doi = {10.1162/
url = {https://
pmid = {42212222},
pmcid = {PMC13214570}
}
RIS
TY - JOUR
AU - Özer, Mert
AU - Egger, Bernhard
AU - Mennecke, Angelika
AU - Nagel, Armin
AU - Zaiss, Moritz
AU - Laun, Frederik Bernd
AU - Dörfler, Arnd
AU - Winkler, Jürgen
AU - German, Alexander
TI - Multispectral 7 Tesla MRI as a potential predictor of dopamine transporter deficiency in Parkinson's disease
T2 - Imaging neuroscience (Cambridge, Mass.)
J2 - Imaging Neurosci (Camb)
PY - 2026
DA - 2026/
VL - 4
SP - IMAG.a.1241
SN - 2837-6056
PB - MIT Press
DO - 10.1162/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1162/
"type": "article-journal",
"title": "Multispectral 7 Tesla MRI as a potential predictor of dopamine transporter deficiency in Parkinson's disease",
"container-title": "Imaging neuroscience (Cambridge, Mass.)",
"author": [
{
"family": "Özer",
"given": "Mert"
},
{
"family": "Egger",
"given": "Bernhard"
},
{
"family": "Mennecke",
"given": "Angelika"
},
{
"family": "Nagel",
"given": "Armin"
},
{
"family": "Zaiss",
"given": "Moritz"
},
{
"family": "Laun",
"given": "Frederik Bernd"
},
{
"family": "Dörfler",
"given": "Arnd"
},
{
"family": "Winkler",
"given": "Jürgen"
},
{
"family": "German",
"given": "Alexander"
}
],
"container-title-short":
"volume": "4",
"page": "IMAG.a.1241",
"DOI": "10.1162/
"PMID": "42212222",
"PMCID": "PMC13214570",
"ISSN": "2837-6056",
"publisher": "MIT Press",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
26
]
]
}
}
The tracing map gets a citation of its own once an author has validated it and it has a DOI.
Similar papers
The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.
- [1] doi:10.1371/journal.pbio.3003856 [code]
- Aging and metabolism contribute separately to brain-body health.Journal: PLoS biologyIn common: Nilearn, NiBabel, statsmodels, 6 other tools, structural MRI / diffusion, 1 reference
- [2] doi:10.1007/s00234-026-04103-8 [code]
- Enhanced detection of subtle cortical abnormalities in focal epilepsy using 7 T MRI surface-based models and graph neural networks.Journal: NeuroradiologyIn common: Nilearn, NiBabel, statsmodels, 6 other tools, structural MRI / diffusion, 1 reference
- [3] doi:10.1002/agm2.70073 [code]
- Foreign Language Learning in Older Adults Modifies Resting-State Functional Connectivity Between the Subcortical Structures and the Cortex.Journal: Aging medicine (Milton (N.S.W))In common: Nilearn, NiBabel, statsmodels, 6 other tools, 1 reference
- [4] doi:10.1038/s41467-026-76452-0 [code]
- Music evokes shared neural representations of imagined narratives across sensory modalities.Journal: Nature communicationsIn common: Nilearn, NiBabel, statsmodels, 6 other tools, 1 reference
- [5] doi:10.1016/j.isci.2026.117180 [code]
- Developmental changes in similarity between neural representations of mental arithmetic and artificial neural networks.Journal: iScienceIn common: Nilearn, NiBabel, statsmodels, 6 other tools, 1 reference
- [6] doi:10.1162/imag.a.1256 [code]
- Gamer in the scanner: Event-related analysis of fMRI activity during retro videogame play guided by automated annotations of game content.Journal: Imaging neuroscience (Cambridge, Mass.)In common: Nilearn, NiBabel, statsmodels, 6 other tools, 1 reference
- [7] doi:10.7554/elife.107933 [code]
- Modality-agnostic decoding of vision and language from fMRI.Journal: eLifeIn common: Nilearn, NiBabel, statsmodels, 6 other tools, 1 reference
- [8] doi:10.1126/sciadv.adu9309 [code]
- Variations of global brain asymmetry are associated with aging and related diseases.Journal: Science advancesIn common: Nilearn, NiBabel, PyTorch, 4 other tools, Parkinson's, 2 references
- [9] doi:10.1038/s41531-026-01354-3 [code]
- Neuromodulation-induced normalization of cortical metastable dynamics signatures in Parkinson's disease.Journal: NPJ Parkinson's diseaseIn common: Nilearn, NiBabel, statsmodels, 5 other tools, Parkinson's, 1 reference
- [10] doi:10.64898/2026.08.18.26360725 [code]
- Temporal pole blurring in hippocampal sclerosis reflects seizure-disrupted myelinationJournal: medRxiv (preprint)In common: Nilearn, NiBabel, statsmodels, 6 other tools, structural MRI / diffusion
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 13 scripts, and 9 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:8507f7d9f3bd14c1…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.
Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
