MesseMMP
Normalize and trim comments
75c3625
Raw
History Blame Contribute Delete
13.5 kB
import os
import glob
from typing import Any
import click
import lightning.pytorch as pl
import torch
from lightning.pytorch.callbacks import LearningRateMonitor, ModelCheckpoint
from lightning.pytorch.loggers import TensorBoardLogger
from pytorchvideo.transforms import Normalize, Permute, RandAugment
from torch.utils.data import DataLoader, WeightedRandomSampler
from torchvision.transforms import transforms as T
from torchvision.transforms._transforms_video import ToTensorVideo
from torchvision.transforms import InterpolationMode
from full_model.rnn_dataset import SyntaxDataset
from full_model.rnn_model import SyntaxLightningModule
torch.set_float32_matmul_precision("medium")
def get_transforms(video_size, imagenet_mean, imagenet_std, train: bool = True):
"""Augmentations and preprocessing for clips."""
interpolation_choices = [InterpolationMode.BILINEAR, InterpolationMode.BICUBIC]
if train:
return T.Compose([
ToTensorVideo(),
Permute(dims=[1, 0, 2, 3]),
RandAugment(magnitude=10, num_layers=2),
T.RandomHorizontalFlip(),
Permute(dims=[1, 0, 2, 3]),
T.RandomChoice([
T.Resize(size=video_size, interpolation=interp, antialias=True)
for interp in interpolation_choices
]),
Normalize(mean=imagenet_mean, std=imagenet_std),
])
else:
return T.Compose([
ToTensorVideo(),
T.Resize(size=video_size, interpolation=InterpolationMode.BICUBIC, antialias=True),
Normalize(mean=imagenet_mean, std=imagenet_std),
])
def make_dataloader(dataset, batch_size: int, num_workers: int, use_weighted_sampler: bool):
"""DataLoader with an optional score-based WeightedRandomSampler."""
if use_weighted_sampler:
sample_weights = dataset.get_sample_weights().cpu()
sampler = WeightedRandomSampler(sample_weights, num_samples=len(dataset), replacement=True)
shuffle = False
else:
sampler = None
shuffle = False
return DataLoader(
dataset,
batch_size=batch_size,
num_workers=num_workers,
sampler=sampler,
shuffle=shuffle,
drop_last=True,
pin_memory=True,
persistent_workers=(num_workers > 0),
)
def make_model(
num_classes: int,
lr: float,
variant: str,
weight_decay: float,
max_epochs: int,
weight_path: str | None = None,
pl_weight_path: str | None = None,
pt_weights_format: bool = False,
) -> SyntaxLightningModule:
"""
Create the head model.
weight_path — pretrained backbone weights (r3d_18), .pt or .ckpt.
pl_weight_path — full head-model checkpoint (Lightning .ckpt or raw .pt).
pt_weights_format=True → pl_weight_path is a raw state_dict (.pt).
pt_weights_format=False → pl_weight_path is a Lightning .ckpt with 'state_dict'.
"""
return SyntaxLightningModule(
num_classes=num_classes,
lr=lr,
variant=variant,
weight_decay=weight_decay,
max_epochs=max_epochs,
weight_path=weight_path,
pl_weight_path=pl_weight_path,
yulie_model=pt_weights_format,
)
def make_callbacks(phase: str):
"""Callbacks: LR monitor plus ModelCheckpoint on val_rmse."""
lr_monitor = LearningRateMonitor(logging_interval="epoch")
if phase == "pre":
checkpoint = ModelCheckpoint(
monitor="val_rmse",
save_top_k=1,
mode="min",
filename="rnn_model-{epoch:02d}-{val_rmse:.3f}",
save_last=True,
)
elif phase == "full":
checkpoint = ModelCheckpoint(
monitor="val_rmse",
save_top_k=3,
mode="min",
filename="rnn_model-{epoch:02d}-{val_rmse:.3f}",
save_last=True,
)
else:
raise ValueError(f"Unknown phase '{phase}', expected 'pre' or 'full'")
return [lr_monitor, checkpoint]
def make_trainer(max_epochs: int, logdir: str, logger_name: str, devices: list[int], precision: str, callbacks):
"""Create a Trainer with a TensorBoard logger."""
logger = TensorBoardLogger(save_dir=logdir, name=logger_name)
strategy = "ddp_find_unused_parameters_true" if len(devices) > 1 else "auto"
trainer = pl.Trainer(
max_epochs=max_epochs,
accelerator="gpu" if torch.cuda.is_available() else "cpu",
devices=devices,
strategy=strategy,
precision=precision,
callbacks=callbacks,
log_every_n_steps=10,
logger=logger,
)
return trainer
def find_backbone_ckpt_lightning(backbone_logdir: str, artery: str, fold: int, phase: str = "full") -> str:
"""
Find a backbone Lightning checkpoint in the log directory.
Expected structure:
backbone_logdir/
{artery}BinSyntax_R3D_{phase}_foldXX/version_*/checkpoints/*.ckpt
"""
logger_name = f"{artery}BinSyntax_R3D_{phase}_fold{fold:02d}"
pattern = os.path.join(backbone_logdir, logger_name, "version_*/checkpoints", "*.ckpt")
ckpts = glob.glob(pattern)
if not ckpts:
raise FileNotFoundError(
f"No backbone Lightning checkpoints found for\n"
f" artery={artery}, fold={fold}, phase={phase}\n"
f" in '{backbone_logdir}' (pattern: {pattern})"
)
best = max(ckpts, key=os.path.getctime)
print(f"[Backbone] Using Lightning checkpoint: {best}")
return best
def build_backbone_pt_path(backbone_pt_dir: str, artery: str, fold: int) -> str:
"""
Build the backbone .pt path using the naming convention:
rightBinSyntax_R3D_full_fold00.pt
leftBinSyntax_R3D_full_fold00.pt
...
"""
fname = f"{artery}BinSyntax_R3D_full_fold{fold:02d}.pt"
path = os.path.join(backbone_pt_dir, fname)
if not os.path.exists(path):
raise FileNotFoundError(
f"Backbone .pt not found for artery={artery}, fold={fold} in '{backbone_pt_dir}'\n"
f"Expected file: {fname}"
)
print(f"[Backbone] Using .pt file: {path}")
return path
@click.command()
@click.option(
"-r",
"--dataset-root",
type=click.Path(exists=True),
default=".",
show_default=True,
help="Dataset root (JSON and DICOM paths are resolved relative to it).",
)
@click.option("--fold", type=int, default=4, show_default=True, help="Fold number.")
@click.option(
"-a",
"--artery",
type=str,
default="right",
show_default=True,
help="Artery: left or right.",
)
@click.option(
"--variant",
type=str,
default="lstm_mean",
show_default=True,
help="Head-model variant: mean_out, mean, lstm_mean, lstm_last, gru_mean, gru_last, bert_mean, bert_cls, bert_cls2.",
)
@click.option("-nc", "--num-classes", type=int, default=2, show_default=True,
help="Number of head-model outputs (clf + reg).")
@click.option("-b", "--batch-size", type=int, default=8, show_default=True, help="Batch size.")
@click.option("-f", "--frames-per-clip", type=int, default=32, show_default=True,
help="Frames per clip.")
@click.option(
"-v",
"--video-size",
type=click.Tuple([int, int]),
default=(256, 256),
show_default=True,
help="Frame size (H, W).",
)
@click.option("--max-epochs", type=int, default=10, show_default=True, help="Number of full-train epochs.")
@click.option("--num-workers", type=int, default=16, show_default=True, help="DataLoader workers.")
@click.option(
"--devices",
type=list[int],
multiple=True,
default=[0],
show_default=True,
help="List of GPU ids",
)
@click.option("--precision", type=str, default="bf16-mixed", show_default=True, help="Numeric precision mode.")
@click.option(
"--logdir",
type=click.Path(),
default="./logs/rnn",
show_default=True,
help="Log and checkpoint directory for the head model.",
)
@click.option(
"--backbone-logdir",
type=click.Path(exists=True),
default=None,
help="Directory with backbone logs (Lightning .ckpt files).",
)
@click.option(
"--backbone-pt-dir",
type=click.Path(exists=True),
default="backbone_weights",
show_default=True,
help="Directory with backbone .pt files (rightBinSyntax_R3D_full_foldXX.pt, leftBinSyntax_R3D_full_foldXX.pt).",
)
@click.option(
"--backbone-from-pt",
is_flag=True,
default=True,
show_default=True,
help="When enabled, load the backbone from .pt files in backbone-pt-dir; otherwise use Lightning logs in backbone-logdir.",
)
@click.option(
"--rnn-folds-dir",
type=click.Path(),
default="rnn_folds",
show_default=True,
help="Directory with rnn_folds (relative to dataset_root).",
)
@click.option(
"--use-weighted-sampler",
is_flag=True,
default=False,
show_default=True,
help="Use a WeightedRandomSampler by score.",
)
@click.option(
"--pt-weights-format",
is_flag=True,
default=False,
show_default=True,
help="pl_weight_path format for full training: True uses .pt raw state_dict, False uses Lightning .ckpt.",
)
@click.option("--seed", type=int, default=42, show_default=True, help="Random seed.")
def main(
dataset_root: str,
fold: int,
artery: str,
variant: str,
num_classes: int,
batch_size: int,
frames_per_clip: int,
video_size: Any,
max_epochs: int,
num_workers: int,
devices: int,
precision: str,
logdir: str,
backbone_logdir: str | None,
backbone_pt_dir: str | None,
backbone_from_pt: bool,
rnn_folds_dir: str,
use_weighted_sampler: bool,
pt_weights_format: bool,
seed: int,
):
"""Train the RNN head on top of the backbone."""
VARIANTS = "mean_out mean lstm_mean lstm_last gru_mean gru_last bert_mean bert_cls bert_cls2".split()
if variant not in VARIANTS:
raise ValueError(f"Unknown variant '{variant}', expected one of: {VARIANTS}")
artery = artery.lower()
if artery not in ("left", "right"):
raise ValueError(f"Unknown artery '{artery}', expected 'left' or 'right'")
pl.seed_everything(seed)
imagenet_mean = [0.485, 0.456, 0.406]
imagenet_std = [0.229, 0.224, 0.225]
train_meta = os.path.join(rnn_folds_dir, f"rnn_fold{fold:02d}_train.json")
eval_meta = os.path.join(rnn_folds_dir, f"rnn_fold{fold:02d}_eval.json")
train_set = SyntaxDataset(
root=dataset_root,
meta=train_meta,
train=True,
length=frames_per_clip,
label=f"syntax_{artery}",
artery=artery,
inference=False,
validation=True,
transform=get_transforms(video_size, imagenet_mean, imagenet_std, train=True),
)
val_set = SyntaxDataset(
root=dataset_root,
meta=eval_meta,
train=False,
length=frames_per_clip,
label=f"syntax_{artery}",
artery=artery,
inference=False,
validation=True,
transform=get_transforms(video_size, imagenet_mean, imagenet_std, train=False),
)
train_loader_pre = make_dataloader(train_set, batch_size * 2, num_workers, use_weighted_sampler)
train_loader_post = make_dataloader(train_set, batch_size, num_workers, use_weighted_sampler)
val_loader = make_dataloader(val_set, 1, num_workers, use_weighted_sampler=False)
x, *_ = next(iter(train_loader_pre))
video_shape = x.shape[2:]
print(f"RNN head input per clip: {video_shape}")
if backbone_from_pt:
if backbone_pt_dir is None:
raise ValueError("backbone-from-pt=True, but backbone-pt-dir is not set.")
backbone_weight_path = build_backbone_pt_path(backbone_pt_dir, artery=artery, fold=fold)
else:
if backbone_logdir is None:
raise ValueError("backbone-from-pt=False, but backbone-logdir is not set.")
backbone_weight_path = find_backbone_ckpt_lightning(
backbone_logdir=backbone_logdir,
artery=artery,
fold=fold,
phase="full",
)
callbacks_pre = make_callbacks(phase="pre")
model_pre = make_model(
num_classes=num_classes,
lr=1e-4,
variant=variant,
weight_decay=0.01,
max_epochs=max_epochs,
weight_path=backbone_weight_path,
pl_weight_path=None,
pt_weights_format=False,
)
trainer_pre = make_trainer(
max_epochs=max_epochs,
logdir=logdir,
logger_name=f"{artery}BinSyntax_R3D_fold{fold:02d}_{variant}_pre",
devices=devices,
precision=precision,
callbacks=callbacks_pre,
)
trainer_pre.fit(model_pre, train_dataloaders=train_loader_pre, val_dataloaders=val_loader)
callbacks_full = make_callbacks(phase="full")
model_full = make_model(
num_classes=num_classes,
lr=2e-5,
variant=variant,
weight_decay=0.01,
max_epochs=max_epochs,
weight_path=None,
pl_weight_path=trainer_pre.checkpoint_callback.best_model_path,
pt_weights_format=pt_weights_format,
)
trainer_full = make_trainer(
max_epochs=max_epochs,
logdir=logdir,
logger_name=f"{artery}BinSyntax_R3D_fold{fold:02d}_{variant}_post",
devices=devices,
precision=precision,
callbacks=callbacks_full,
)
trainer_full.fit(model_full, train_dataloaders=train_loader_post, val_dataloaders=val_loader)
if __name__ == "__main__":
main()