Improve training: learnable temperature, per-group LR, warmup, augmentations
Loss: - Learnable temperature (CLIP-style logit_scale) with clamp [0.01, 0.5] - Replaces fixed cosine schedule (still available via --no-learnable-temp) - Default tau_init=0.07 Optimizer: - Per-group LR: projections 1e-4, text encoder 1e-5 (10x lower) - Learnable temperature included in projection param group Scheduler: - Linear warmup (2 epochs default) + cosine annealing - Per-step scheduling (not per-epoch) Augmentations (separate drone/satellite): - Drone: RandomResizedCrop(0.7-1.0), HFlip, Rotation(15), ColorJitter, RandomGrayscale(0.05), GaussianBlur - Satellite: RandomResizedCrop(0.7-1.0), HFlip, ColorJitter, RandomGrayscale - Eval: clean Resize+CenterCrop (no augmentation) Dataset: supports separate drone_transform/sat_transform args Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ Single InfoNCE loss: query(drone+text) vs gallery(satellite).
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -18,13 +19,18 @@ import torch
|
||||
import torch.nn as nn
|
||||
from torch.amp import GradScaler, autocast
|
||||
from torch.optim import AdamW
|
||||
from torch.optim.lr_scheduler import CosineAnnealingLR
|
||||
from torch.optim.lr_scheduler import LambdaLR
|
||||
from torch.utils.data import DataLoader
|
||||
from tqdm import tqdm
|
||||
|
||||
from src.datasets.gtauav_dataset import GTAUAVDataset, collate_gtauav_batch
|
||||
from src.losses.multi_infonce import InfoNCELoss
|
||||
from src.models.asymmetric_encoder import AsymmetricEncoder, get_dino_transform
|
||||
from src.models.asymmetric_encoder import (
|
||||
AsymmetricEncoder,
|
||||
get_dino_transform,
|
||||
get_drone_train_transform,
|
||||
get_satellite_train_transform,
|
||||
)
|
||||
|
||||
LOGGER = logging.getLogger("caption_test.train_gtauav")
|
||||
|
||||
@@ -64,19 +70,21 @@ class TrainConfigGTAUAV:
|
||||
batch_size: int = 64
|
||||
num_workers: int = 4
|
||||
learning_rate: float = 1e-4
|
||||
text_lr_factor: float = 0.1 # text encoder LR = learning_rate * factor
|
||||
weight_decay: float = 1e-4
|
||||
grad_clip: float = 1.0
|
||||
use_amp: bool = True
|
||||
eval_every: int = 2
|
||||
warmup_epochs: int = 2
|
||||
seed: int = 42
|
||||
device: str = "cuda"
|
||||
|
||||
# Loss.
|
||||
tau_init: float = 0.1
|
||||
tau_final: float = 0.01
|
||||
tau_init: float = 0.07
|
||||
label_smoothing: float = 0.1
|
||||
weight_q2g: float = 0.6
|
||||
weight_g2q: float = 0.4
|
||||
learnable_temperature: bool = True
|
||||
|
||||
|
||||
def _set_seed(seed: int) -> None:
|
||||
@@ -95,6 +103,45 @@ def _atomic_save(obj: dict, path: Path) -> None:
|
||||
tmp_path.replace(path)
|
||||
|
||||
|
||||
def _build_param_groups(
|
||||
model: AsymmetricEncoder,
|
||||
lr: float,
|
||||
text_lr_factor: float,
|
||||
) -> list[dict]:
|
||||
"""Build optimizer param groups with separate LR for text encoder."""
|
||||
text_params = []
|
||||
other_params = []
|
||||
|
||||
for name, param in model.named_parameters():
|
||||
if not param.requires_grad:
|
||||
continue
|
||||
if "text_encoder" in name:
|
||||
text_params.append(param)
|
||||
else:
|
||||
other_params.append(param)
|
||||
|
||||
groups = [{"params": other_params, "lr": lr}]
|
||||
if text_params:
|
||||
groups.append({"params": text_params, "lr": lr * text_lr_factor})
|
||||
|
||||
return groups
|
||||
|
||||
|
||||
def _cosine_warmup_schedule(
|
||||
warmup_steps: int,
|
||||
total_steps: int,
|
||||
) -> callable:
|
||||
"""Cosine annealing with linear warmup."""
|
||||
|
||||
def lr_lambda(step: int) -> float:
|
||||
if step < warmup_steps:
|
||||
return step / max(warmup_steps, 1)
|
||||
progress = (step - warmup_steps) / max(total_steps - warmup_steps, 1)
|
||||
return 0.5 * (1.0 + math.cos(math.pi * progress))
|
||||
|
||||
return lr_lambda
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def _evaluate(
|
||||
model: AsymmetricEncoder,
|
||||
@@ -186,27 +233,35 @@ def train(cfg: TrainConfigGTAUAV) -> None:
|
||||
# Loss.
|
||||
loss_fn = InfoNCELoss(
|
||||
temperature_init=cfg.tau_init,
|
||||
temperature_final=cfg.tau_final,
|
||||
label_smoothing=cfg.label_smoothing,
|
||||
weight_q2g=cfg.weight_q2g,
|
||||
weight_g2q=cfg.weight_g2q,
|
||||
learnable_temperature=cfg.learnable_temperature,
|
||||
)
|
||||
LOGGER.info(
|
||||
"🌡️ Temperature: %s (init=%.3f)",
|
||||
"learnable" if cfg.learnable_temperature else "cosine schedule",
|
||||
cfg.tau_init,
|
||||
)
|
||||
|
||||
# Data.
|
||||
transform = get_dino_transform(image_size=256)
|
||||
# Data — separate transforms for train (augmented) and eval (clean).
|
||||
drone_train_tf = get_drone_train_transform(image_size=256)
|
||||
sat_train_tf = get_satellite_train_transform(image_size=256)
|
||||
eval_tf = get_dino_transform(image_size=256)
|
||||
|
||||
train_ds = GTAUAVDataset(
|
||||
pair_json=cfg.train_json,
|
||||
rgb_root=cfg.rgb_root,
|
||||
caption_root=cfg.caption_root,
|
||||
image_transform=transform,
|
||||
drone_transform=drone_train_tf,
|
||||
sat_transform=sat_train_tf,
|
||||
filter_meta=cfg.filter_meta,
|
||||
)
|
||||
test_ds = GTAUAVDataset(
|
||||
pair_json=cfg.test_json,
|
||||
rgb_root=cfg.rgb_root,
|
||||
caption_root=cfg.caption_root,
|
||||
image_transform=transform,
|
||||
image_transform=eval_tf,
|
||||
filter_meta=cfg.filter_meta,
|
||||
)
|
||||
|
||||
@@ -230,13 +285,27 @@ def train(cfg: TrainConfigGTAUAV) -> None:
|
||||
|
||||
LOGGER.info("📦 train=%d test=%d batch=%d", len(train_ds), len(test_ds), cfg.batch_size)
|
||||
|
||||
# Optimizer.
|
||||
optimizer = AdamW(
|
||||
model.trainable_parameters(),
|
||||
lr=cfg.learning_rate,
|
||||
weight_decay=cfg.weight_decay,
|
||||
# Optimizer — per-group LR (text encoder gets lower LR).
|
||||
param_groups = _build_param_groups(model, cfg.learning_rate, cfg.text_lr_factor)
|
||||
# Include loss temperature if learnable.
|
||||
if cfg.learnable_temperature and loss_fn.logit_scale is not None:
|
||||
param_groups[0]["params"].append(loss_fn.logit_scale)
|
||||
|
||||
optimizer = AdamW(param_groups, weight_decay=cfg.weight_decay)
|
||||
|
||||
lr_info = f"proj={cfg.learning_rate:.0e}"
|
||||
if not cfg.baseline_mode:
|
||||
lr_info += f" text={cfg.learning_rate * cfg.text_lr_factor:.0e}"
|
||||
LOGGER.info("⚙️ Optimizer: AdamW LR: %s warmup=%d epochs", lr_info, cfg.warmup_epochs)
|
||||
|
||||
# Scheduler — cosine with linear warmup.
|
||||
steps_per_epoch = len(train_loader)
|
||||
total_steps = cfg.epochs * steps_per_epoch
|
||||
warmup_steps = cfg.warmup_epochs * steps_per_epoch
|
||||
scheduler = LambdaLR(
|
||||
optimizer,
|
||||
lr_lambda=_cosine_warmup_schedule(warmup_steps, total_steps),
|
||||
)
|
||||
scheduler = CosineAnnealingLR(optimizer, T_max=cfg.epochs)
|
||||
scaler = GradScaler(enabled=cfg.use_amp)
|
||||
|
||||
history: list[dict] = []
|
||||
@@ -289,6 +358,7 @@ def train(cfg: TrainConfigGTAUAV) -> None:
|
||||
)
|
||||
scaler.step(optimizer)
|
||||
scaler.update()
|
||||
scheduler.step()
|
||||
|
||||
for key, val in loss_dict.items():
|
||||
agg[key] = agg.get(key, 0.0) + float(val.item())
|
||||
@@ -296,10 +366,10 @@ def train(cfg: TrainConfigGTAUAV) -> None:
|
||||
|
||||
pbar.set_postfix(
|
||||
loss=f"{total_loss.item():.3f}",
|
||||
tau=f"{loss_dict['temperature'].item():.4f}",
|
||||
gate=f"{loss_dict['gate'].item():.3f}",
|
||||
)
|
||||
|
||||
scheduler.step()
|
||||
elapsed = time.time() - epoch_start
|
||||
|
||||
means = {k: v / max(n_batches, 1) for k, v in agg.items()}
|
||||
@@ -339,6 +409,7 @@ def train(cfg: TrainConfigGTAUAV) -> None:
|
||||
"epoch": epoch,
|
||||
"model_state": model.state_dict(),
|
||||
"optimizer_state": optimizer.state_dict(),
|
||||
"loss_state": loss_fn.state_dict(),
|
||||
},
|
||||
path=output_dir / f"ckpt_epoch{epoch:03d}.pt",
|
||||
)
|
||||
@@ -395,7 +466,15 @@ def main() -> None:
|
||||
)
|
||||
parser.add_argument(
|
||||
"--lr", type=float, default=1e-4,
|
||||
help="Learning rate.",
|
||||
help="Learning rate for projections.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--text-lr-factor", type=float, default=0.1,
|
||||
help="Text encoder LR = lr * factor (default 0.1 = 10x lower).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--warmup-epochs", type=int, default=2,
|
||||
help="Linear warmup epochs.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--init-gate", type=float, default=0.7,
|
||||
@@ -408,6 +487,8 @@ def main() -> None:
|
||||
cfg.batch_size = args.batch_size
|
||||
cfg.epochs = args.epochs
|
||||
cfg.learning_rate = args.lr
|
||||
cfg.text_lr_factor = args.text_lr_factor
|
||||
cfg.warmup_epochs = args.warmup_epochs
|
||||
cfg.init_gate = args.init_gate
|
||||
|
||||
if args.filter_meta is not None:
|
||||
|
||||
Reference in New Issue
Block a user