AMontiB
Add utilities and CLI for PGC pipeline
c55595d
Raw History Blame Contribute Delete
11.6 kB
import argparse
import os
from typing import List, Tuple
import torch
# Choices for ``--dino_variant``. Centralised so train and test cannot drift.
DINO_CHOICES = [
"dinov2-base",
"dinov2-large",
"dinov2-giant",
"dinov3-vitb16-pretrain-lvd1689m",
]
# Choices for ``--optim``.
OPTIM_CHOICES = ["adam", "sgd"]
def build_train_parser() -> argparse.ArgumentParser:
"""Construct the training CLI parser.
Every option used anywhere in the training pipeline is registered here
with a sensible default; downstream modules access them directly via
attribute lookup.
"""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description="PGC (Peak-Guided Calibration) training CLI",
)
# ----- Identity / I/O -------------------------------------------------
parser.add_argument(
"--name",
type=str,
default="pgc_train",
help="Experiment name (also used as log filename stem).",
)
parser.add_argument(
"--checkpoints_dir",
type=str,
default="checkpoints",
help="Directory under which logs and checkpoints are written.",
)
parser.add_argument(
"--devices",
type=str,
default="3",
help=(
"CUDA devices, e.g. '0' or '0,1'; '-1' for CPU. "
"RTX 4090 #3 is the test box."
),
)
parser.add_argument(
"--num_threads",
type=int,
default=8,
help="Number of DataLoader worker processes for training.",
)
parser.add_argument(
"--batch_size",
type=int,
default=32,
help="Training batch size.",
)
# ----- DINO backbone & LoRA -------------------------------------------
parser.add_argument(
"--dino_variant",
type=str,
default="dinov2-large",
choices=DINO_CHOICES,
help="DINO backbone variant.",
)
parser.add_argument(
"--dino_pretrained_root",
type=str,
default=None,
help=(
"Root directory containing local DINO checkpoints. "
"When set, the loader expects subfolders named after the variant. "
"Required for training; entry-point validates explicitly."
),
)
parser.add_argument(
"--lora_rank",
type=int,
default=8,
help="LoRA rank for adapter layers.",
)
parser.add_argument(
"--lora_alpha",
type=float,
default=1.0,
help="LoRA scaling factor (alpha / rank determines effective scale).",
)
parser.add_argument(
"--lora_dropout",
type=float,
default=0.1,
help="Dropout probability inside LoRA adapter layers.",
)
# ----- Training data ---------------------------------------------------
# ``--real_image_dir`` and ``--fake_image_dir`` both accept *one or more*
# root directories (``nargs='+'``). ``RealFakeDataset`` scans every
# supplied directory, concatenates the samples and performs a global
# ``random.shuffle`` so that every mini-batch can freely mix sources.
parser.add_argument(
"--real_image_dir",
type=str,
nargs="+",
default=None,
help=(
"One or more root directories of real training images. "
"Example: --real_image_dir /path/A /path/B. All directories are "
"scanned recursively and the resulting samples are merged + "
"shuffled before training."
),
)
parser.add_argument(
"--fake_image_dir",
type=str,
nargs="+",
default=None,
help=(
"One or more root directories of fake / generated training "
"images. Example: --fake_image_dir /path/A /path/B. All "
"directories are scanned recursively and the resulting samples "
"are merged + shuffled before training."
),
)
parser.add_argument(
"--cropSize",
type=int,
default=224,
help="Pad-then-crop input resolution (square).",
)
# ----- Mid-training evaluation ----------------------------------------
parser.add_argument(
"--test_root",
type=str,
default=None,
help=(
"Optional root directory of the evaluation dataset "
"(UniversalFakeDetect layout: ``<root>/<subset>/{0_real,1_fake}``). "
"If supplied the trainer evaluates on it periodically."
),
)
parser.add_argument(
"--eval_batch_size",
type=int,
default=32,
help="Batch size used during mid-training evaluation.",
)
parser.add_argument(
"--eval_num_threads",
type=int,
default=8,
help="DataLoader workers for mid-training evaluation.",
)
# ----- Optimization ----------------------------------------------------
parser.add_argument(
"--niter",
type=int,
default=100,
help="Total number of training epochs.",
)
parser.add_argument(
"--lr",
type=float,
default=5e-5,
help="Initial learning rate.",
)
parser.add_argument(
"--optim",
type=str,
default="adam",
choices=OPTIM_CHOICES,
help="Optimizer family.",
)
parser.add_argument(
"--accumulation_steps",
type=int,
default=4,
help="Gradient accumulation steps.",
)
parser.add_argument(
"--weight_decay",
type=float,
default=0.05,
help="L2 regularization strength.",
)
parser.add_argument(
"--label_smoothing",
type=float,
default=0.1,
help="Label-smoothing epsilon for binary cross-entropy.",
)
# ----- PGCM (Peak-Guided Calibration Module) hyper-parameters ---------
parser.add_argument(
"--tau_rgb",
type=float,
default=0.5,
help="Temperature for the RGB-stream peak aggregation in PGCM.",
)
parser.add_argument(
"--tau_res",
type=float,
default=0.5,
help="Temperature for the residual-stream peak aggregation in PGCM.",
)
# ----- Stability / reproducibility ------------------------------------
parser.add_argument(
"--max_grad_norm",
type=float,
default=1.0,
help="Maximum L2 norm for gradient clipping (0 to disable).",
)
parser.add_argument(
"--seed",
type=int,
default=42,
help="Random seed for Python, NumPy and PyTorch.",
)
# ----- Bookkeeping -----------------------------------------------------
parser.add_argument(
"--eval_every_steps",
type=int,
default=200,
help=(
"Run mid-training evaluation every N optimization steps. "
"A checkpoint named "
"``step{N}_acc{int}_ap{int}.pth`` is also saved right after "
"each such evaluation (in addition to the best-AP ``best.pth``)."
),
)
# Internal flag used by ``finalize_opt`` to know which parser produced
# the namespace. Not exposed as a CLI option (no ``add_argument``).
parser.set_defaults(isTrain=True)
return parser
def build_test_parser() -> argparse.ArgumentParser:
"""Construct the inference / evaluation CLI parser."""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description="PGC (Peak-Guided Calibration) test CLI",
)
parser.add_argument(
"--checkpoint",
type=str,
required=True,
help="Path to the trained model checkpoint (.pth).",
)
parser.add_argument(
"--test_root",
type=str,
required=True,
help="Root directory of the evaluation dataset.",
)
parser.add_argument(
"--checkpoints_dir",
type=str,
default="checkpoints",
help="Directory under which the test log file is written.",
)
parser.add_argument(
"--name",
type=str,
default="pgc_test",
help="Experiment name used to derive the test log filename.",
)
parser.add_argument(
"--batch_size",
type=int,
default=32,
help="Evaluation batch size.",
)
parser.add_argument(
"--num_workers",
type=int,
default=8,
help="DataLoader worker processes for evaluation.",
)
parser.add_argument(
"--devices",
type=str,
default="3",
help=(
"CUDA devices, e.g. '0' or '0,1'; '-1' for CPU. "
"RTX 4090 #3 is the test box."
),
)
parser.add_argument(
"--cropSize",
type=int,
default=224,
help="Pad-then-crop input resolution (must match training).",
)
parser.add_argument(
"--dino_variant",
type=str,
default="dinov2-large",
choices=DINO_CHOICES,
help="DINO backbone variant (must match training).",
)
parser.add_argument(
"--lora_rank",
type=int,
default=8,
help="LoRA rank (must match training).",
)
parser.add_argument(
"--lora_alpha",
type=float,
default=1.0,
help="LoRA alpha (must match training).",
)
parser.add_argument(
"--lora_dropout",
type=float,
default=0.1,
help="LoRA dropout (must match training).",
)
parser.add_argument(
"--dino_pretrained_root",
type=str,
default=None,
help="Root directory containing local DINO checkpoints.",
)
parser.add_argument(
"--tau_rgb",
type=float,
default=0.5,
help="PGCM RGB-stream temperature (must match training).",
)
parser.add_argument(
"--tau_res",
type=float,
default=0.5,
help="PGCM residual-stream temperature (must match training).",
)
parser.set_defaults(isTrain=False)
return parser
def parse_devices(devices_str: str) -> Tuple[List[int], torch.device]:
str_ids = [s for s in devices_str.split(",") if s.strip() != ""]
parsed_ids = [int(x) for x in str_ids]
valid_ids = [i for i in parsed_ids if i >= 0]
if torch.cuda.is_available() and len(valid_ids) > 0:
return valid_ids, torch.device(f"cuda:{valid_ids[0]}")
return [], torch.device("cpu")
def cli_message_for_opt(
parser: argparse.ArgumentParser, opt: argparse.Namespace
) -> str:
message = "----------------- Options ---------------\n"
for k, v in sorted(vars(opt).items()):
if k.startswith("_"):
continue
default = parser.get_default(k)
comment = f"\t[default: {default}]" if v != default else ""
message += "{:>25}: {:<30}{}\n".format(str(k), str(v), comment)
message += "----------------- End -------------------"
return message
def finalize_opt(opt: argparse.Namespace) -> str:
parser = build_train_parser() if opt.isTrain else build_test_parser()
message = cli_message_for_opt(parser, opt)
expr_dir = os.path.join(opt.checkpoints_dir, opt.name)
os.makedirs(expr_dir, exist_ok=True)
with open(os.path.join(expr_dir, "opt.txt"), "wt") as opt_file:
opt_file.write(message + "\n")
devices_list, device = parse_devices(opt.devices)
opt.devices = devices_list
opt.device = device
if torch.cuda.is_available() and len(devices_list) > 0:
torch.cuda.set_device(devices_list[0])
return message