Phase 2: Add SOTA visual diagnostics (13 types) with Trackio image logging
Browse filesVisual diagnostics added:
1. PCA Feature Maps (V-JEPA 2.1 style, arxiv:2603.14482)
2. Multi-head Attention Heatmaps (DINO style, arxiv:2104.14294)
3. RankMe Score anti-collapse monitoring (arxiv:2210.02885)
4. Per-dimension Variance bars (VICReg style, arxiv:2105.04906)
5. Latent Trajectory PCA (z₀→z₁→z₂→z₃)
6. Temporal Straightness metric (LeWorldModel Eq.9, arxiv:2603.19312)
7. Evidence Gate Activation Heatmaps
8. Token Norm Maps (DINOv2+Registers, arxiv:2309.16588)
9. Cross-Attention Weights (Perceiver Resampler)
10. Eigenspectrum Plot (singular value distribution)
11. Feature Similarity Heatmap overlay on input images
12. Rollout PCA per step
13. Rollout Comparison Grid (image vs PCA per step)
All logged via trackio.Image(fig_to_pil(fig)) - tested and verified.
- train_phase2.py +706 -67
|
@@ -1,14 +1,23 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
MR-JEPA Phase 2 Training — Perception Fine-tuning
|
| 4 |
|
| 5 |
Loads the best Phase 1 checkpoint and unfreezes:
|
| 6 |
- Last 6 DINOv3-L layers (LR: 1e-5)
|
| 7 |
- Last 4 Qwen3-Embedding layers (LR: 1e-5)
|
| 8 |
- Reasoning core continues at 1e-4
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
Usage:
|
| 14 |
python train_phase2.py --checkpoint checkpoints/hybrid_main_best.pt
|
|
@@ -34,30 +43,664 @@ import torch.nn.functional as F
|
|
| 34 |
from torch.optim import AdamW
|
| 35 |
from torch.utils.data import DataLoader
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s", datefmt="%H:%M:%S")
|
| 38 |
log = logging.getLogger("mrjepa-p2")
|
| 39 |
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
def download_phase1_checkpoint(hub_model_id: str, run_name: str = "hybrid_main"):
|
| 42 |
-
"""Download Phase 1 best checkpoint from Hub."""
|
| 43 |
from huggingface_hub import hf_hub_download
|
| 44 |
-
path = hf_hub_download(
|
| 45 |
-
repo_id=hub_model_id,
|
| 46 |
-
filename=f"checkpoints/{run_name}_best.pt",
|
| 47 |
-
repo_type="model",
|
| 48 |
-
)
|
| 49 |
log.info(f"Downloaded checkpoint: {path}")
|
| 50 |
return path
|
| 51 |
|
| 52 |
|
| 53 |
def main():
|
| 54 |
parser = argparse.ArgumentParser(description="MR-JEPA Phase 2 Training")
|
| 55 |
-
parser.add_argument("--checkpoint", type=str, default=None
|
| 56 |
-
help="Local path to Phase 1 checkpoint. If not given, downloads from Hub.")
|
| 57 |
parser.add_argument("--hub_model_id", default="JorgeAV/MR-JEPA")
|
| 58 |
parser.add_argument("--run_name", default="hybrid_main_phase2")
|
| 59 |
-
parser.add_argument("--phase1_run", default="hybrid_main"
|
| 60 |
-
help="Name of the Phase 1 run (for checkpoint download)")
|
| 61 |
parser.add_argument("--epochs", type=int, default=10)
|
| 62 |
parser.add_argument("--batch_size", type=int, default=16)
|
| 63 |
parser.add_argument("--grad_accum", type=int, default=8)
|
|
@@ -67,23 +710,18 @@ def main():
|
|
| 67 |
parser.add_argument("--unfreeze_visual_layers", type=int, default=6)
|
| 68 |
parser.add_argument("--unfreeze_text_layers", type=int, default=4)
|
| 69 |
parser.add_argument("--max_eval_samples", type=int, default=500)
|
|
|
|
| 70 |
parser.add_argument("--output_dir", default="./outputs/mrjepa_phase2")
|
| 71 |
args = parser.parse_args()
|
| 72 |
|
| 73 |
-
# ── Import Phase 1 module (download from Hub) ──
|
| 74 |
log.info("Downloading Phase 1 training script...")
|
| 75 |
from huggingface_hub import hf_hub_download
|
| 76 |
-
p1_script = hf_hub_download(
|
| 77 |
-
repo_id=args.hub_model_id,
|
| 78 |
-
filename="train_mrjepa.py",
|
| 79 |
-
repo_type="model",
|
| 80 |
-
)
|
| 81 |
import importlib.util
|
| 82 |
spec = importlib.util.spec_from_file_location("train_mrjepa", p1_script)
|
| 83 |
p1 = importlib.util.module_from_spec(spec)
|
| 84 |
spec.loader.exec_module(p1)
|
| 85 |
|
| 86 |
-
# ── Load checkpoint ──
|
| 87 |
if args.checkpoint and os.path.exists(args.checkpoint):
|
| 88 |
ckpt_path = args.checkpoint
|
| 89 |
else:
|
|
@@ -92,14 +730,12 @@ def main():
|
|
| 92 |
log.info(f"Loading Phase 1 checkpoint: {ckpt_path}")
|
| 93 |
ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=False)
|
| 94 |
|
| 95 |
-
# ── Reconstruct config ──
|
| 96 |
saved_cfg = ckpt["config"]
|
| 97 |
cfg = p1.Config()
|
| 98 |
for k, v in saved_cfg.items():
|
| 99 |
if hasattr(cfg, k):
|
| 100 |
setattr(cfg, k, v)
|
| 101 |
|
| 102 |
-
# Phase 2 overrides
|
| 103 |
cfg.phase = 2
|
| 104 |
cfg.epochs = args.epochs
|
| 105 |
cfg.batch_size = args.batch_size
|
|
@@ -108,7 +744,7 @@ def main():
|
|
| 108 |
cfg.backbone_lr = args.backbone_lr
|
| 109 |
cfg.output_dir = args.output_dir
|
| 110 |
cfg.run_name = args.run_name
|
| 111 |
-
cfg.freeze_backbone = True
|
| 112 |
cfg.freeze_text = True
|
| 113 |
cfg.max_eval_samples = args.max_eval_samples
|
| 114 |
cfg.resolve()
|
|
@@ -117,15 +753,22 @@ def main():
|
|
| 117 |
log.info(f"Device: {device}")
|
| 118 |
os.makedirs(cfg.output_dir, exist_ok=True)
|
| 119 |
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
-
# ── Build model & load weights ──
|
| 129 |
log.info("Building model...")
|
| 130 |
model = p1.MRJEPAModel(cfg)
|
| 131 |
model.evidence.load_state_dict(ckpt["evidence"])
|
|
@@ -135,7 +778,6 @@ def main():
|
|
| 135 |
model.target.t_ro.load_state_dict(ckpt["target_ro"])
|
| 136 |
log.info(f"Loaded Phase 1 weights (epoch={ckpt.get('epoch','?')}, eval_acc={ckpt.get('eval_acc','?')}%)")
|
| 137 |
|
| 138 |
-
# ── Unfreeze perception ──
|
| 139 |
log.info(f"Unfreezing last {args.unfreeze_visual_layers} visual layers, "
|
| 140 |
f"last {args.unfreeze_text_layers} text layers")
|
| 141 |
model.vis.unfreeze_last(args.unfreeze_visual_layers)
|
|
@@ -145,8 +787,9 @@ def main():
|
|
| 145 |
total_p = sum(p.numel() for p in model.parameters())
|
| 146 |
train_p = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 147 |
log.info(f"Total: {total_p:,} | Trainable: {train_p:,} ({100*train_p/total_p:.1f}%)")
|
|
|
|
|
|
|
| 148 |
|
| 149 |
-
# ── Data ──
|
| 150 |
transform = model.vis.get_transform()
|
| 151 |
tokenizer = model.txt.tokenizer
|
| 152 |
train_ds = p1.ScienceQADataset("train", transform=transform, tokenizer=tokenizer,
|
|
@@ -160,10 +803,8 @@ def main():
|
|
| 160 |
eval_dl = DataLoader(eval_ds, batch_size=cfg.batch_size, shuffle=False,
|
| 161 |
num_workers=2, collate_fn=coll, pin_memory=True)
|
| 162 |
|
| 163 |
-
# ── 3-group optimizer ──
|
| 164 |
backbone_params = [p for p in model.vis.parameters() if p.requires_grad]
|
| 165 |
text_params = [p for p in model.txt.parameters() if p.requires_grad]
|
| 166 |
-
# Core = everything trainable that's not backbone or text
|
| 167 |
bb_txt_ids = {id(p) for p in backbone_params + text_params}
|
| 168 |
core_params = [p for p in model.parameters() if p.requires_grad and id(p) not in bb_txt_ids]
|
| 169 |
|
|
@@ -188,8 +829,11 @@ def main():
|
|
| 188 |
|
| 189 |
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)
|
| 190 |
|
| 191 |
-
|
|
|
|
|
|
|
| 192 |
log.info(f"Phase 2: {cfg.epochs} epochs, {len(train_dl)} batches/epoch, ga={cfg.grad_accum}")
|
|
|
|
| 193 |
global_step = 0
|
| 194 |
best_acc = ckpt.get("eval_acc", 0.0)
|
| 195 |
amp_dtype = torch.bfloat16 if cfg.bf16 else torch.float32
|
|
@@ -204,36 +848,26 @@ def main():
|
|
| 204 |
|
| 205 |
for batch_idx, batch in enumerate(train_dl):
|
| 206 |
batch = {k: v.to(device) if isinstance(v, torch.Tensor) else v for k, v in batch.items()}
|
| 207 |
-
|
| 208 |
-
# Phase 2: backbone is unfrozen, so no torch.no_grad() wrapper
|
| 209 |
-
# The model.forward() still wraps vis/txt in no_grad — we need to override
|
| 210 |
-
# We do this by temporarily disabling the no_grad in forward
|
| 211 |
-
# Actually, looking at model.forward(), vis and txt are inside torch.no_grad()
|
| 212 |
-
# We need to patch this for Phase 2. Let's override forward behavior:
|
| 213 |
vis_tok = model.vis(batch["pixel_values"]).float()
|
| 214 |
txt_tok = model.txt(batch["input_ids"], batch["attention_mask"]).float()
|
| 215 |
|
| 216 |
with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=cfg.bf16 and device.type == "cuda"):
|
| 217 |
evidence, _, ev_mask = model.evidence(vis_tok, txt_tok, batch["attention_mask"])
|
| 218 |
-
|
| 219 |
if model._use_rollout:
|
| 220 |
traj, z_final, z_proj = model.rollout(evidence)
|
| 221 |
else:
|
| 222 |
B = batch["batch_size"]
|
| 223 |
z0 = model.rollout.init_tokens.expand(B, -1, -1) + \
|
| 224 |
model.rollout.z0_proj(F.adaptive_avg_pool1d(
|
| 225 |
-
evidence.permute(0,2,1), model.rollout.num_tokens
|
| 226 |
-
).permute(0,2,1))
|
| 227 |
z_final = z0
|
| 228 |
z_proj = model.rollout.out_proj(z0).unsqueeze(1)
|
| 229 |
|
| 230 |
-
# Target (still no_grad)
|
| 231 |
if model._use_jepa:
|
| 232 |
target_proj = model.target(vis_tok.detach(), txt_tok.detach(), batch["attention_mask"].detach())
|
| 233 |
else:
|
| 234 |
target_proj = None
|
| 235 |
|
| 236 |
-
# Options
|
| 237 |
opt_emb = model.encode_options(batch["opt_input_ids"], batch["opt_attention_mask"])
|
| 238 |
opt_emb = opt_emb.view(batch["batch_size"], cfg.max_options, -1)
|
| 239 |
logits = model.disc(z_final, opt_emb, batch["opt_mask"])
|
|
@@ -243,18 +877,19 @@ def main():
|
|
| 243 |
losses = model.jepa_loss(z_proj, target_proj, task_loss)
|
| 244 |
else:
|
| 245 |
losses = {"total": task_loss, "jepa": torch.tensor(0.0), "task": task_loss, "reg": torch.tensor(0.0)}
|
| 246 |
-
|
| 247 |
loss = losses["total"] / cfg.grad_accum
|
| 248 |
|
| 249 |
loss.backward()
|
| 250 |
|
| 251 |
if (batch_idx + 1) % cfg.grad_accum == 0:
|
| 252 |
nn.utils.clip_grad_norm_(trainable, cfg.max_grad_norm)
|
| 253 |
-
optimizer.step()
|
| 254 |
-
scheduler.step()
|
| 255 |
-
optimizer.zero_grad()
|
| 256 |
model.update_target(global_step, total_steps)
|
| 257 |
global_step += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
|
| 259 |
preds = logits.argmax(dim=-1)
|
| 260 |
for k, v in losses.items():
|
|
@@ -270,32 +905,36 @@ def main():
|
|
| 270 |
log.info(f"P2 E{epoch} B{batch_idx}/{len(train_dl)} | "
|
| 271 |
f"loss={avg.get('total',0):.4f} jepa={avg.get('jepa',0):.4f} "
|
| 272 |
f"task={avg.get('task',0):.4f} | acc={acc:.1f}%")
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
# Eval
|
| 285 |
eval_acc = p1.evaluate(model, eval_dl, device, cfg)
|
| 286 |
train_acc = epoch_correct / max(epoch_total, 1) * 100
|
| 287 |
log.info(f"=== Phase 2 Epoch {epoch} | Train: {train_acc:.1f}% | Eval: {eval_acc:.1f}% ===")
|
|
|
|
|
|
|
| 288 |
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
|
|
|
| 293 |
|
| 294 |
if eval_acc > best_acc:
|
| 295 |
best_acc = eval_acc
|
| 296 |
p1.save_checkpoint(model, cfg, epoch, eval_acc, is_best=True)
|
|
|
|
| 297 |
|
| 298 |
log.info(f"Phase 2 complete. Best eval accuracy: {best_acc:.1f}%")
|
|
|
|
|
|
|
| 299 |
if cfg.push_to_hub:
|
| 300 |
p1.push_results(cfg, best_acc)
|
| 301 |
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
+
MR-JEPA Phase 2 Training — Perception Fine-tuning + SOTA Visual Diagnostics
|
| 4 |
|
| 5 |
Loads the best Phase 1 checkpoint and unfreezes:
|
| 6 |
- Last 6 DINOv3-L layers (LR: 1e-5)
|
| 7 |
- Last 4 Qwen3-Embedding layers (LR: 1e-5)
|
| 8 |
- Reasoning core continues at 1e-4
|
| 9 |
|
| 10 |
+
Visual diagnostics logged to Trackio (state-of-the-art for JEPA):
|
| 11 |
+
1. PCA Feature Maps (V-JEPA 2.1 style) — patch features → RGB via PCA
|
| 12 |
+
2. Multi-head Attention Heatmaps (DINO style) — CLS attention per head overlaid on image
|
| 13 |
+
3. RankMe Score (anti-collapse) — effective rank of embedding matrix
|
| 14 |
+
4. Per-dimension Variance (VICReg style) — collapse detection per dim
|
| 15 |
+
5. Latent Trajectory PCA — z₀→z₁→z₂→z₃ projected to 2D
|
| 16 |
+
6. Temporal Straightness (LeWM Eq. 9) — trajectory coherence metric
|
| 17 |
+
7. Evidence Gate Activation Heatmaps — gate values per rollout step
|
| 18 |
+
8. Token Norm Maps (DINOv2-Reg style) — artifact detection
|
| 19 |
+
9. Cross-Attention Weights in Perceiver — which evidence each query attends to
|
| 20 |
+
10. Eigenspectrum Plot — singular value distribution of latent space
|
| 21 |
|
| 22 |
Usage:
|
| 23 |
python train_phase2.py --checkpoint checkpoints/hybrid_main_best.pt
|
|
|
|
| 43 |
from torch.optim import AdamW
|
| 44 |
from torch.utils.data import DataLoader
|
| 45 |
|
| 46 |
+
# ── Matplotlib non-interactive backend (MUST be before pyplot import) ──
|
| 47 |
+
import matplotlib
|
| 48 |
+
matplotlib.use("Agg")
|
| 49 |
+
import matplotlib.pyplot as plt
|
| 50 |
+
import matplotlib.colors as mcolors
|
| 51 |
+
import seaborn as sns
|
| 52 |
+
from sklearn.decomposition import PCA as SklearnPCA
|
| 53 |
+
|
| 54 |
+
from PIL import Image as PILImage
|
| 55 |
+
import io
|
| 56 |
+
|
| 57 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s", datefmt="%H:%M:%S")
|
| 58 |
log = logging.getLogger("mrjepa-p2")
|
| 59 |
|
| 60 |
|
| 61 |
+
def fig_to_pil(fig: plt.Figure) -> PILImage.Image:
|
| 62 |
+
"""Convert matplotlib figure to PIL Image for Trackio logging.
|
| 63 |
+
Trackio accepts PIL.Image but NOT matplotlib.figure.Figure directly."""
|
| 64 |
+
buf = io.BytesIO()
|
| 65 |
+
fig.savefig(buf, format='png', dpi=120, bbox_inches='tight', pad_inches=0.1)
|
| 66 |
+
buf.seek(0)
|
| 67 |
+
img = PILImage.open(buf).copy()
|
| 68 |
+
buf.close()
|
| 69 |
+
return img
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# ══════════════════════════════════════════════════════════════════════════
|
| 73 |
+
# SOTA VISUALIZATION FUNCTIONS (Papers: I-JEPA, V-JEPA 2.1, DINO,
|
| 74 |
+
# LeWorldModel, RankMe, VICReg, DINOv2+Registers)
|
| 75 |
+
# ══════════════════════════════════════════════════════════════════════════
|
| 76 |
+
|
| 77 |
+
def rankme(Z: torch.Tensor, epsilon: float = 1e-7) -> float:
|
| 78 |
+
"""
|
| 79 |
+
RankMe: effective rank of embedding matrix via Shannon entropy of singular values.
|
| 80 |
+
Source: "RankMe: Assessing the Downstream Performance of Pretrained Self-Supervised
|
| 81 |
+
Representations by Their Rank" (arxiv:2210.02885)
|
| 82 |
+
|
| 83 |
+
Z: (N, D) — batch of embeddings
|
| 84 |
+
Returns scalar effective rank (higher = less collapsed)
|
| 85 |
+
"""
|
| 86 |
+
if Z.dim() == 3:
|
| 87 |
+
Z = Z.reshape(-1, Z.size(-1))
|
| 88 |
+
Z_centered = Z - Z.mean(0, keepdim=True)
|
| 89 |
+
try:
|
| 90 |
+
_, S, _ = torch.linalg.svd(Z_centered.float(), full_matrices=False)
|
| 91 |
+
p = S / (S.sum() + epsilon) + epsilon
|
| 92 |
+
return torch.exp(-torch.sum(p * torch.log(p))).item()
|
| 93 |
+
except:
|
| 94 |
+
return 0.0
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def vicreg_collapse_stats(Z: torch.Tensor) -> dict:
|
| 98 |
+
"""
|
| 99 |
+
VICReg-style per-dimension variance monitoring.
|
| 100 |
+
Source: "VICReg: Variance-Invariance-Covariance Regularization" (arxiv:2105.04906)
|
| 101 |
+
|
| 102 |
+
Z: (N, D) — monitor each dimension's std
|
| 103 |
+
Collapsed dims have std < 0.1
|
| 104 |
+
"""
|
| 105 |
+
if Z.dim() == 3:
|
| 106 |
+
Z = Z.reshape(-1, Z.size(-1))
|
| 107 |
+
std_per_dim = Z.float().std(0) # (D,)
|
| 108 |
+
return {
|
| 109 |
+
"min_std": std_per_dim.min().item(),
|
| 110 |
+
"mean_std": std_per_dim.mean().item(),
|
| 111 |
+
"max_std": std_per_dim.max().item(),
|
| 112 |
+
"collapsed_dims": (std_per_dim < 0.1).sum().item(),
|
| 113 |
+
"total_dims": std_per_dim.size(0),
|
| 114 |
+
"std_values": std_per_dim.detach().cpu().numpy(),
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def temporal_straightness(z_seq: torch.Tensor) -> float:
|
| 119 |
+
"""
|
| 120 |
+
Temporal straightness metric from LeWorldModel (Eq. 9, Appendix H, Fig. 17).
|
| 121 |
+
Source: "Le World Model" (arxiv:2603.19312)
|
| 122 |
+
|
| 123 |
+
Measures geometric coherence of latent trajectory.
|
| 124 |
+
z_seq: (B, K+1, D) or (B, K+1, N, D) — latent trajectory
|
| 125 |
+
Returns mean cosine similarity between consecutive velocity vectors.
|
| 126 |
+
Higher = more coherent (straighter) trajectory.
|
| 127 |
+
"""
|
| 128 |
+
if z_seq.dim() == 4:
|
| 129 |
+
z_seq = z_seq.mean(dim=2) # Pool tokens → (B, K+1, D)
|
| 130 |
+
v = z_seq[:, 1:] - z_seq[:, :-1] # (B, K, D)
|
| 131 |
+
if v.size(1) < 2:
|
| 132 |
+
return 0.0
|
| 133 |
+
v_norm = F.normalize(v.float(), dim=-1)
|
| 134 |
+
cos_sim = (v_norm[:, :-1] * v_norm[:, 1:]).sum(-1) # (B, K-1)
|
| 135 |
+
return cos_sim.mean().item()
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def pca_feature_map(patch_features: torch.Tensor, grid_h: int, grid_w: int) -> np.ndarray:
|
| 139 |
+
"""
|
| 140 |
+
PCA RGB feature map visualization (V-JEPA 2.1 style).
|
| 141 |
+
Source: "Revisiting Feature Prediction for Learning Visual Representations
|
| 142 |
+
from Video" (arxiv:2603.14482), Section 2.2, Figs. 1, 3.
|
| 143 |
+
|
| 144 |
+
Maps first 3 principal components of patch features → RGB channels.
|
| 145 |
+
Semantic objects share PCA colors regardless of position.
|
| 146 |
+
|
| 147 |
+
patch_features: (N_patches, D)
|
| 148 |
+
Returns: (grid_h, grid_w, 3) uint8 array suitable for display
|
| 149 |
+
"""
|
| 150 |
+
feats = patch_features.float().detach().cpu().numpy()
|
| 151 |
+
n_components = min(3, feats.shape[0], feats.shape[1])
|
| 152 |
+
if n_components < 3:
|
| 153 |
+
return np.zeros((grid_h, grid_w, 3), dtype=np.uint8)
|
| 154 |
+
pca = SklearnPCA(n_components=3)
|
| 155 |
+
pca_feats = pca.fit_transform(feats) # (N, 3)
|
| 156 |
+
# Normalize each component to [0, 1]
|
| 157 |
+
for c in range(3):
|
| 158 |
+
vmin, vmax = pca_feats[:, c].min(), pca_feats[:, c].max()
|
| 159 |
+
if vmax - vmin > 1e-8:
|
| 160 |
+
pca_feats[:, c] = (pca_feats[:, c] - vmin) / (vmax - vmin)
|
| 161 |
+
else:
|
| 162 |
+
pca_feats[:, c] = 0.5
|
| 163 |
+
n_patches = pca_feats.shape[0]
|
| 164 |
+
if n_patches != grid_h * grid_w:
|
| 165 |
+
actual_side = int(math.sqrt(n_patches))
|
| 166 |
+
grid_h = grid_w = actual_side
|
| 167 |
+
pca_img = (pca_feats[:grid_h * grid_w].reshape(grid_h, grid_w, 3) * 255).astype(np.uint8)
|
| 168 |
+
return pca_img
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def eigenspectrum_plot(Z: torch.Tensor, title: str = "Eigenspectrum") -> plt.Figure:
|
| 172 |
+
"""
|
| 173 |
+
Plot singular value spectrum of embedding matrix.
|
| 174 |
+
Source: "RankMe" (arxiv:2210.02885), used in DINO, I-JEPA for collapse monitoring.
|
| 175 |
+
|
| 176 |
+
Healthy representations show flat spectrum; collapsed ones show sharp decay.
|
| 177 |
+
"""
|
| 178 |
+
if Z.dim() == 3:
|
| 179 |
+
Z = Z.reshape(-1, Z.size(-1))
|
| 180 |
+
Z_c = Z.float() - Z.float().mean(0, keepdim=True)
|
| 181 |
+
try:
|
| 182 |
+
_, S, _ = torch.linalg.svd(Z_c, full_matrices=False)
|
| 183 |
+
s = S.detach().cpu().numpy()
|
| 184 |
+
except:
|
| 185 |
+
s = np.ones(10)
|
| 186 |
+
fig, ax = plt.subplots(figsize=(6, 4))
|
| 187 |
+
ax.semilogy(s[:min(100, len(s))], 'b-', linewidth=1.5)
|
| 188 |
+
ax.fill_between(range(min(100, len(s))), s[:min(100, len(s))], alpha=0.15, color='blue')
|
| 189 |
+
ax.set_xlabel("Singular Value Index", fontsize=10)
|
| 190 |
+
ax.set_ylabel("Singular Value (log)", fontsize=10)
|
| 191 |
+
ax.set_title(title, fontsize=12, fontweight='bold')
|
| 192 |
+
ax.grid(True, alpha=0.3)
|
| 193 |
+
fig.tight_layout()
|
| 194 |
+
return fig
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def per_dim_variance_plot(std_values: np.ndarray, title: str = "Per-Dimension Std") -> plt.Figure:
|
| 198 |
+
"""
|
| 199 |
+
VICReg-style per-dimension standard deviation bar plot.
|
| 200 |
+
Source: "VICReg" (arxiv:2105.04906), Section 4.
|
| 201 |
+
|
| 202 |
+
Each bar = std of one embedding dimension across the batch.
|
| 203 |
+
Target: all dims near γ=1.0. Dims with std→0 are collapsed.
|
| 204 |
+
"""
|
| 205 |
+
fig, ax = plt.subplots(figsize=(8, 3))
|
| 206 |
+
n = min(len(std_values), 200)
|
| 207 |
+
colors = ['red' if v < 0.1 else ('orange' if v < 0.3 else 'steelblue') for v in std_values[:n]]
|
| 208 |
+
ax.bar(range(n), std_values[:n], color=colors, width=1.0, edgecolor='none')
|
| 209 |
+
ax.axhline(y=1.0, color='green', linestyle='--', alpha=0.7, label='Target γ=1.0')
|
| 210 |
+
ax.axhline(y=0.1, color='red', linestyle='--', alpha=0.5, label='Collapse threshold')
|
| 211 |
+
ax.set_xlabel("Dimension Index", fontsize=9)
|
| 212 |
+
ax.set_ylabel("Std", fontsize=9)
|
| 213 |
+
ax.set_title(title, fontsize=11, fontweight='bold')
|
| 214 |
+
ax.legend(fontsize=8)
|
| 215 |
+
ax.set_xlim(-0.5, n - 0.5)
|
| 216 |
+
fig.tight_layout()
|
| 217 |
+
return fig
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def trajectory_pca_plot(trajectory: torch.Tensor, title: str = "Latent Trajectory") -> plt.Figure:
|
| 221 |
+
"""
|
| 222 |
+
Latent trajectory visualization via PCA projection.
|
| 223 |
+
Source: I-JEPA + LeWorldModel trajectory analysis.
|
| 224 |
+
|
| 225 |
+
trajectory: (K+1, N_tokens, D) — single sample trajectory
|
| 226 |
+
Projects step centroids to 2D, draws arrows showing reasoning evolution.
|
| 227 |
+
"""
|
| 228 |
+
K_plus_1, N_s, D = trajectory.shape
|
| 229 |
+
centroids = trajectory.mean(dim=1).float().detach().cpu().numpy()
|
| 230 |
+
|
| 231 |
+
if K_plus_1 < 2:
|
| 232 |
+
fig, ax = plt.subplots(figsize=(5, 5))
|
| 233 |
+
ax.set_title(title)
|
| 234 |
+
return fig
|
| 235 |
+
|
| 236 |
+
centered = centroids - centroids.mean(axis=0)
|
| 237 |
+
try:
|
| 238 |
+
pca = SklearnPCA(n_components=2)
|
| 239 |
+
coords = pca.fit_transform(centered)
|
| 240 |
+
except:
|
| 241 |
+
coords = centered[:, :2]
|
| 242 |
+
|
| 243 |
+
fig, ax = plt.subplots(figsize=(6, 6))
|
| 244 |
+
colors = plt.cm.viridis(np.linspace(0, 1, K_plus_1))
|
| 245 |
+
|
| 246 |
+
for k in range(K_plus_1 - 1):
|
| 247 |
+
ax.annotate("", xy=coords[k+1], xytext=coords[k],
|
| 248 |
+
arrowprops=dict(arrowstyle="->", color=colors[k], lw=2.5))
|
| 249 |
+
|
| 250 |
+
for k in range(K_plus_1):
|
| 251 |
+
ax.scatter(coords[k, 0], coords[k, 1], c=[colors[k]], s=150,
|
| 252 |
+
zorder=5, edgecolors='black', linewidth=1.5)
|
| 253 |
+
label = 'z₀' if k == 0 else f'z_{k}'
|
| 254 |
+
ax.annotate(label, (coords[k, 0], coords[k, 1]),
|
| 255 |
+
textcoords="offset points", xytext=(10, 10),
|
| 256 |
+
fontsize=12, fontweight='bold', color=colors[k])
|
| 257 |
+
|
| 258 |
+
ax.set_xlabel("PC1", fontsize=10)
|
| 259 |
+
ax.set_ylabel("PC2", fontsize=10)
|
| 260 |
+
ax.set_title(title, fontsize=12, fontweight='bold')
|
| 261 |
+
ax.grid(True, alpha=0.3)
|
| 262 |
+
fig.tight_layout()
|
| 263 |
+
return fig
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def attention_heatmap_overlay(
|
| 267 |
+
attn_weights: torch.Tensor,
|
| 268 |
+
original_image: np.ndarray,
|
| 269 |
+
grid_h: int = 16, grid_w: int = 16,
|
| 270 |
+
title: str = "Attention Heatmap",
|
| 271 |
+
num_heads_to_show: int = 4,
|
| 272 |
+
) -> plt.Figure:
|
| 273 |
+
"""
|
| 274 |
+
DINO-style multi-head self-attention heatmap overlay.
|
| 275 |
+
Source: "Emerging Properties in Self-Supervised Vision Transformers"
|
| 276 |
+
(arxiv:2104.14294), Section 4.2.2, Fig. 3.
|
| 277 |
+
"""
|
| 278 |
+
if attn_weights.dim() == 2:
|
| 279 |
+
attn_weights = attn_weights.unsqueeze(0)
|
| 280 |
+
|
| 281 |
+
n_heads = min(attn_weights.size(0), num_heads_to_show)
|
| 282 |
+
fig, axes = plt.subplots(1, n_heads + 1, figsize=(4 * (n_heads + 1), 4))
|
| 283 |
+
if n_heads + 1 == 1:
|
| 284 |
+
axes = [axes]
|
| 285 |
+
|
| 286 |
+
axes[0].imshow(original_image)
|
| 287 |
+
axes[0].set_title("Input Image", fontsize=10)
|
| 288 |
+
axes[0].axis('off')
|
| 289 |
+
|
| 290 |
+
head_colors = ['Reds', 'Blues', 'Greens', 'Purples', 'Oranges', 'YlOrRd']
|
| 291 |
+
|
| 292 |
+
for h in range(n_heads):
|
| 293 |
+
attn = attn_weights[h].float().detach().cpu().numpy()
|
| 294 |
+
if attn.ndim == 2:
|
| 295 |
+
attn_map = attn[0]
|
| 296 |
+
else:
|
| 297 |
+
attn_map = attn
|
| 298 |
+
n_tokens = attn_map.shape[0]
|
| 299 |
+
side = int(math.sqrt(n_tokens))
|
| 300 |
+
if side * side != n_tokens:
|
| 301 |
+
side = grid_h
|
| 302 |
+
attn_2d = attn_map[:side*side].reshape(side, side)
|
| 303 |
+
attn_2d = (attn_2d - attn_2d.min()) / (attn_2d.max() - attn_2d.min() + 1e-8)
|
| 304 |
+
attn_resized = np.array(PILImage.fromarray((attn_2d * 255).astype(np.uint8)).resize(
|
| 305 |
+
(original_image.shape[1], original_image.shape[0]), PILImage.BILINEAR)) / 255.0
|
| 306 |
+
axes[h + 1].imshow(original_image)
|
| 307 |
+
axes[h + 1].imshow(attn_resized, cmap=head_colors[h % len(head_colors)], alpha=0.6)
|
| 308 |
+
axes[h + 1].set_title(f"Head {h}", fontsize=10)
|
| 309 |
+
axes[h + 1].axis('off')
|
| 310 |
+
|
| 311 |
+
fig.suptitle(title, fontsize=12, fontweight='bold')
|
| 312 |
+
fig.tight_layout()
|
| 313 |
+
return fig
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
def evidence_gate_heatmap(gate_values: list, title: str = "Evidence Gate Activations") -> plt.Figure:
|
| 317 |
+
"""Evidence gate activation visualization per rollout step."""
|
| 318 |
+
K = len(gate_values)
|
| 319 |
+
if K == 0:
|
| 320 |
+
fig, ax = plt.subplots()
|
| 321 |
+
ax.text(0.5, 0.5, "No gates recorded", ha='center', va='center')
|
| 322 |
+
return fig
|
| 323 |
+
|
| 324 |
+
fig, axes = plt.subplots(1, K, figsize=(4 * K, 4))
|
| 325 |
+
if K == 1:
|
| 326 |
+
axes = [axes]
|
| 327 |
+
|
| 328 |
+
for k, gv in enumerate(gate_values):
|
| 329 |
+
if isinstance(gv, torch.Tensor):
|
| 330 |
+
gv = gv.float().detach().cpu().numpy()
|
| 331 |
+
if gv.ndim == 3:
|
| 332 |
+
gv = gv.mean(0)
|
| 333 |
+
mean_gate = gv.mean(axis=-1) if gv.ndim == 2 else gv
|
| 334 |
+
im = axes[k].imshow(mean_gate.reshape(1, -1) if mean_gate.ndim == 1 else mean_gate,
|
| 335 |
+
cmap='YlOrRd', aspect='auto', vmin=0, vmax=1)
|
| 336 |
+
axes[k].set_title(f'Step {k+1}: μ={mean_gate.mean():.3f}', fontsize=10)
|
| 337 |
+
axes[k].set_xlabel("Token Index")
|
| 338 |
+
fig.colorbar(im, ax=axes[k], fraction=0.046, pad=0.04)
|
| 339 |
+
|
| 340 |
+
fig.suptitle(title, fontsize=12, fontweight='bold')
|
| 341 |
+
fig.tight_layout()
|
| 342 |
+
return fig
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
def token_norm_map(tokens: torch.Tensor, grid_h: int = 16, grid_w: int = 16,
|
| 346 |
+
title: str = "Token Norm Map") -> plt.Figure:
|
| 347 |
+
"""
|
| 348 |
+
Token norm map for artifact detection (DINOv2+Registers style).
|
| 349 |
+
Source: "Vision Transformers Need Registers" (arxiv:2309.16588).
|
| 350 |
+
"""
|
| 351 |
+
if tokens.dim() == 3:
|
| 352 |
+
tokens = tokens[0]
|
| 353 |
+
norms = tokens.float().norm(dim=-1).detach().cpu().numpy()
|
| 354 |
+
n_tokens = len(norms)
|
| 355 |
+
side = int(math.sqrt(n_tokens))
|
| 356 |
+
if side * side != n_tokens:
|
| 357 |
+
side = grid_h
|
| 358 |
+
norm_map = norms[:side*side].reshape(side, side)
|
| 359 |
+
|
| 360 |
+
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
|
| 361 |
+
im = axes[0].imshow(norm_map, cmap='hot', aspect='auto')
|
| 362 |
+
axes[0].set_title(f"{title}\nμ={norms.mean():.2f}, σ={norms.std():.2f}", fontsize=10)
|
| 363 |
+
fig.colorbar(im, ax=axes[0])
|
| 364 |
+
axes[1].hist(norms, bins=50, color='steelblue', edgecolor='white', alpha=0.8)
|
| 365 |
+
axes[1].axvline(norms.mean(), color='red', linestyle='--', label=f'Mean: {norms.mean():.2f}')
|
| 366 |
+
axes[1].axvline(norms.mean() + 2*norms.std(), color='orange', linestyle='--',
|
| 367 |
+
label=f'+2σ: {norms.mean() + 2*norms.std():.2f}')
|
| 368 |
+
axes[1].set_title("Norm Distribution", fontsize=10)
|
| 369 |
+
axes[1].set_xlabel("||token||₂")
|
| 370 |
+
axes[1].legend(fontsize=8)
|
| 371 |
+
fig.suptitle(title, fontsize=12, fontweight='bold')
|
| 372 |
+
fig.tight_layout()
|
| 373 |
+
return fig
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
def cross_attention_weights_plot(attn_weights: torch.Tensor,
|
| 377 |
+
title: str = "Perceiver Cross-Attention") -> plt.Figure:
|
| 378 |
+
"""Cross-attention weights in the Perceiver Resampler."""
|
| 379 |
+
if attn_weights.dim() == 3:
|
| 380 |
+
attn = attn_weights.float().mean(0).detach().cpu().numpy()
|
| 381 |
+
else:
|
| 382 |
+
attn = attn_weights.float().detach().cpu().numpy()
|
| 383 |
+
fig, ax = plt.subplots(figsize=(8, 6))
|
| 384 |
+
sns.heatmap(attn, cmap='viridis', ax=ax, xticklabels=False, yticklabels=False)
|
| 385 |
+
ax.set_xlabel("Key Tokens (Evidence: Visual | Text)", fontsize=10)
|
| 386 |
+
ax.set_ylabel("Query Tokens (Latent)", fontsize=10)
|
| 387 |
+
ax.set_title(title, fontsize=12, fontweight='bold')
|
| 388 |
+
fig.tight_layout()
|
| 389 |
+
return fig
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
def rollout_comparison_grid(images: list, pca_maps: list, titles: list = None,
|
| 393 |
+
suptitle: str = "Latent Rollout PCA Maps") -> plt.Figure:
|
| 394 |
+
"""
|
| 395 |
+
Grid comparing original images with PCA feature maps at each rollout step.
|
| 396 |
+
Source: LeWorldModel Fig. 7 + V-JEPA 2.1 Fig. 3.
|
| 397 |
+
"""
|
| 398 |
+
n_samples = min(len(images), 4)
|
| 399 |
+
n_steps = len(pca_maps[0]) if pca_maps else 0
|
| 400 |
+
cols = 1 + n_steps
|
| 401 |
+
fig, axes = plt.subplots(n_samples, cols, figsize=(3 * cols, 3 * n_samples))
|
| 402 |
+
if n_samples == 1:
|
| 403 |
+
axes = axes.reshape(1, -1)
|
| 404 |
+
for i in range(n_samples):
|
| 405 |
+
axes[i, 0].imshow(images[i])
|
| 406 |
+
axes[i, 0].set_title("Input" if i == 0 else "", fontsize=9)
|
| 407 |
+
axes[i, 0].axis('off')
|
| 408 |
+
for k in range(n_steps):
|
| 409 |
+
if k < len(pca_maps[i]):
|
| 410 |
+
axes[i, k+1].imshow(pca_maps[i][k])
|
| 411 |
+
axes[i, k+1].set_title(f"z_{k}" if i == 0 else "", fontsize=9)
|
| 412 |
+
axes[i, k+1].axis('off')
|
| 413 |
+
fig.suptitle(suptitle, fontsize=13, fontweight='bold')
|
| 414 |
+
fig.tight_layout()
|
| 415 |
+
return fig
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
# ══════════════════════════════════════════════════════════════════════════
|
| 419 |
+
# HOOK-BASED DIAGNOSTIC COLLECTOR
|
| 420 |
+
# ══════════════════════════════════════════════════════════════════════════
|
| 421 |
+
|
| 422 |
+
class DiagnosticCollector:
|
| 423 |
+
"""Collects intermediate activations via hooks for visualization."""
|
| 424 |
+
def __init__(self):
|
| 425 |
+
self.gate_values = []
|
| 426 |
+
self.cross_attn_weights = []
|
| 427 |
+
self.hooks = []
|
| 428 |
+
|
| 429 |
+
def attach(self, model):
|
| 430 |
+
self.clear()
|
| 431 |
+
if hasattr(model, 'rollout') and hasattr(model.rollout, 'predictor'):
|
| 432 |
+
for i, block in enumerate(model.rollout.predictor):
|
| 433 |
+
if hasattr(block, 'gate') and block.gate is not None:
|
| 434 |
+
def make_gate_hook(layer_idx):
|
| 435 |
+
def hook(module, input, output):
|
| 436 |
+
self.gate_values.append(output.detach().cpu())
|
| 437 |
+
return hook
|
| 438 |
+
h = block.gate.proj.register_forward_hook(make_gate_hook(i))
|
| 439 |
+
self.hooks.append(h)
|
| 440 |
+
if hasattr(model, 'evidence') and hasattr(model.evidence, 'layers'):
|
| 441 |
+
last_layer = model.evidence.layers[-1]
|
| 442 |
+
if hasattr(last_layer, 'xa'):
|
| 443 |
+
def xa_hook(module, input, output):
|
| 444 |
+
if isinstance(output, tuple) and len(output) > 1:
|
| 445 |
+
self.cross_attn_weights.append(output[1].detach().cpu())
|
| 446 |
+
h = last_layer.xa.register_forward_hook(xa_hook)
|
| 447 |
+
self.hooks.append(h)
|
| 448 |
+
|
| 449 |
+
def clear(self):
|
| 450 |
+
self.gate_values = []
|
| 451 |
+
self.cross_attn_weights = []
|
| 452 |
+
|
| 453 |
+
def detach(self):
|
| 454 |
+
for h in self.hooks:
|
| 455 |
+
h.remove()
|
| 456 |
+
self.hooks = []
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
# ══════════════════════════════════════════════════════════════════════════
|
| 460 |
+
# MAIN VISUALIZATION LOGGING FUNCTION
|
| 461 |
+
# ══════════════════════════════════════════════════════════════════════════
|
| 462 |
+
|
| 463 |
+
def log_visual_diagnostics(model, batch, device, cfg, global_step, epoch,
|
| 464 |
+
diagnostics_collector=None, vis_interval=100):
|
| 465 |
+
"""
|
| 466 |
+
Generate and log all visual diagnostics to Trackio.
|
| 467 |
+
|
| 468 |
+
Implements SOTA visualizations from:
|
| 469 |
+
- I-JEPA (attention maps), V-JEPA 2.1 (PCA feature maps)
|
| 470 |
+
- LeWorldModel (trajectory, temporal straightness)
|
| 471 |
+
- RankMe (effective rank), VICReg (per-dim variance)
|
| 472 |
+
- DINOv2+Registers (token norm maps)
|
| 473 |
+
"""
|
| 474 |
+
import trackio
|
| 475 |
+
|
| 476 |
+
model.eval()
|
| 477 |
+
log_dict = {}
|
| 478 |
+
|
| 479 |
+
try:
|
| 480 |
+
with torch.no_grad():
|
| 481 |
+
vis_tok = model.vis(batch["pixel_values"].to(device)).float()
|
| 482 |
+
txt_tok = model.txt(batch["input_ids"].to(device),
|
| 483 |
+
batch["attention_mask"].to(device)).float()
|
| 484 |
+
evidence, kv, ev_mask = model.evidence(vis_tok, txt_tok,
|
| 485 |
+
batch["attention_mask"].to(device))
|
| 486 |
+
|
| 487 |
+
if model._use_rollout:
|
| 488 |
+
traj, z_final, z_proj = model.rollout(evidence)
|
| 489 |
+
else:
|
| 490 |
+
traj = evidence.unsqueeze(1)
|
| 491 |
+
z_final = evidence
|
| 492 |
+
z_proj = evidence.unsqueeze(1)
|
| 493 |
+
|
| 494 |
+
# ── RankMe Score (anti-collapse) ──
|
| 495 |
+
evidence_rank = rankme(evidence)
|
| 496 |
+
z_final_rank = rankme(z_final)
|
| 497 |
+
vis_rank = rankme(vis_tok)
|
| 498 |
+
log_dict["diagnostics/rankme_evidence"] = evidence_rank
|
| 499 |
+
log_dict["diagnostics/rankme_z_final"] = z_final_rank
|
| 500 |
+
log_dict["diagnostics/rankme_visual"] = vis_rank
|
| 501 |
+
|
| 502 |
+
# ── VICReg Collapse Stats ──
|
| 503 |
+
ev_stats = vicreg_collapse_stats(evidence)
|
| 504 |
+
zf_stats = vicreg_collapse_stats(z_final)
|
| 505 |
+
log_dict["diagnostics/evidence_min_std"] = ev_stats["min_std"]
|
| 506 |
+
log_dict["diagnostics/evidence_mean_std"] = ev_stats["mean_std"]
|
| 507 |
+
log_dict["diagnostics/evidence_collapsed_dims"] = ev_stats["collapsed_dims"]
|
| 508 |
+
log_dict["diagnostics/z_final_min_std"] = zf_stats["min_std"]
|
| 509 |
+
log_dict["diagnostics/z_final_mean_std"] = zf_stats["mean_std"]
|
| 510 |
+
log_dict["diagnostics/z_final_collapsed_dims"] = zf_stats["collapsed_dims"]
|
| 511 |
+
|
| 512 |
+
# ── Temporal Straightness (LeWM Eq. 9) ──
|
| 513 |
+
if traj.dim() == 4:
|
| 514 |
+
straightness = temporal_straightness(traj)
|
| 515 |
+
log_dict["diagnostics/temporal_straightness"] = straightness
|
| 516 |
+
centroids = traj.mean(dim=2)
|
| 517 |
+
for k in range(centroids.size(1) - 1):
|
| 518 |
+
dist = torch.norm(centroids[:, k+1] - centroids[:, k], dim=-1).mean().item()
|
| 519 |
+
log_dict[f"diagnostics/step_distance_z{k}_to_z{k+1}"] = dist
|
| 520 |
+
|
| 521 |
+
# ── PCA Feature Maps (V-JEPA 2.1) ──
|
| 522 |
+
n_vis_patches = vis_tok.size(1)
|
| 523 |
+
grid_side = int(math.sqrt(n_vis_patches))
|
| 524 |
+
|
| 525 |
+
pca_ctx = pca_feature_map(vis_tok[0], grid_side, grid_side)
|
| 526 |
+
fig_pca_ctx = plt.figure(figsize=(4, 4))
|
| 527 |
+
plt.imshow(pca_ctx); plt.title("Context Encoder PCA", fontsize=11, fontweight='bold')
|
| 528 |
+
plt.axis('off'); plt.tight_layout()
|
| 529 |
+
log_dict["visuals/pca/context_encoder"] = trackio.Image(fig_to_pil(fig_pca_ctx),
|
| 530 |
+
caption=f"step={global_step}")
|
| 531 |
+
plt.close(fig_pca_ctx)
|
| 532 |
+
|
| 533 |
+
pca_ev = pca_feature_map(evidence[0], 8, 8)
|
| 534 |
+
fig_pca_ev = plt.figure(figsize=(4, 4))
|
| 535 |
+
plt.imshow(pca_ev); plt.title("Evidence Memory PCA", fontsize=11, fontweight='bold')
|
| 536 |
+
plt.axis('off'); plt.tight_layout()
|
| 537 |
+
log_dict["visuals/pca/evidence_memory"] = trackio.Image(fig_to_pil(fig_pca_ev),
|
| 538 |
+
caption=f"step={global_step}")
|
| 539 |
+
plt.close(fig_pca_ev)
|
| 540 |
+
|
| 541 |
+
if traj.dim() == 4 and traj.size(1) > 1:
|
| 542 |
+
rollout_pcas = []
|
| 543 |
+
for k in range(traj.size(1)):
|
| 544 |
+
n_tok = traj.size(2)
|
| 545 |
+
side_k = max(int(math.sqrt(n_tok)), 1)
|
| 546 |
+
rollout_pcas.append(pca_feature_map(traj[0, k], side_k, side_k))
|
| 547 |
+
fig_rollout = plt.figure(figsize=(4 * len(rollout_pcas), 4))
|
| 548 |
+
for k, pca_k in enumerate(rollout_pcas):
|
| 549 |
+
ax = fig_rollout.add_subplot(1, len(rollout_pcas), k + 1)
|
| 550 |
+
ax.imshow(pca_k); ax.set_title(f"z_{k}", fontsize=10); ax.axis('off')
|
| 551 |
+
fig_rollout.suptitle("Rollout PCA per Step", fontsize=12, fontweight='bold')
|
| 552 |
+
fig_rollout.tight_layout()
|
| 553 |
+
log_dict["visuals/pca/rollout_steps"] = trackio.Image(fig_to_pil(fig_rollout),
|
| 554 |
+
caption=f"step={global_step}")
|
| 555 |
+
plt.close(fig_rollout)
|
| 556 |
+
|
| 557 |
+
# ── Eigenspectrum Plot ──
|
| 558 |
+
fig_eigen_ev = eigenspectrum_plot(evidence, "Evidence Eigenspectrum")
|
| 559 |
+
log_dict["visuals/eigenspectrum/evidence"] = trackio.Image(fig_to_pil(fig_eigen_ev),
|
| 560 |
+
caption=f"RankMe={evidence_rank:.1f}, step={global_step}")
|
| 561 |
+
plt.close(fig_eigen_ev)
|
| 562 |
+
|
| 563 |
+
fig_eigen_zf = eigenspectrum_plot(z_final, "z_final Eigenspectrum")
|
| 564 |
+
log_dict["visuals/eigenspectrum/z_final"] = trackio.Image(fig_to_pil(fig_eigen_zf),
|
| 565 |
+
caption=f"RankMe={z_final_rank:.1f}, step={global_step}")
|
| 566 |
+
plt.close(fig_eigen_zf)
|
| 567 |
+
|
| 568 |
+
# ── Per-Dimension Variance Plot (VICReg) ──
|
| 569 |
+
fig_vardim_ev = per_dim_variance_plot(ev_stats["std_values"],
|
| 570 |
+
f"Evidence Per-Dim Std (collapsed={ev_stats['collapsed_dims']})")
|
| 571 |
+
log_dict["visuals/collapse/evidence_std"] = trackio.Image(fig_to_pil(fig_vardim_ev),
|
| 572 |
+
caption=f"step={global_step}")
|
| 573 |
+
plt.close(fig_vardim_ev)
|
| 574 |
+
|
| 575 |
+
fig_vardim_zf = per_dim_variance_plot(zf_stats["std_values"],
|
| 576 |
+
f"z_final Per-Dim Std (collapsed={zf_stats['collapsed_dims']})")
|
| 577 |
+
log_dict["visuals/collapse/z_final_std"] = trackio.Image(fig_to_pil(fig_vardim_zf),
|
| 578 |
+
caption=f"step={global_step}")
|
| 579 |
+
plt.close(fig_vardim_zf)
|
| 580 |
+
|
| 581 |
+
# ── Latent Trajectory PCA ──
|
| 582 |
+
if traj.dim() == 4 and traj.size(1) > 1:
|
| 583 |
+
fig_traj = trajectory_pca_plot(traj[0],
|
| 584 |
+
f"Latent Trajectory (straightness={straightness:.3f})")
|
| 585 |
+
log_dict["visuals/trajectory/pca"] = trackio.Image(fig_to_pil(fig_traj),
|
| 586 |
+
caption=f"K={traj.size(1)-1}, step={global_step}")
|
| 587 |
+
plt.close(fig_traj)
|
| 588 |
+
|
| 589 |
+
# ── Token Norm Maps (DINOv2+Registers) ──
|
| 590 |
+
fig_norm_vis = token_norm_map(vis_tok[0], grid_side, grid_side, "Visual Token Norms")
|
| 591 |
+
log_dict["visuals/norms/visual_tokens"] = trackio.Image(fig_to_pil(fig_norm_vis),
|
| 592 |
+
caption=f"step={global_step}")
|
| 593 |
+
plt.close(fig_norm_vis)
|
| 594 |
+
|
| 595 |
+
fig_norm_ev = token_norm_map(evidence[0], 8, 8, "Evidence Token Norms")
|
| 596 |
+
log_dict["visuals/norms/evidence_tokens"] = trackio.Image(fig_to_pil(fig_norm_ev),
|
| 597 |
+
caption=f"step={global_step}")
|
| 598 |
+
plt.close(fig_norm_ev)
|
| 599 |
+
|
| 600 |
+
# ── Cross-Attention Weights (hooks) ──
|
| 601 |
+
if diagnostics_collector and diagnostics_collector.cross_attn_weights:
|
| 602 |
+
attn_w = diagnostics_collector.cross_attn_weights[-1]
|
| 603 |
+
if attn_w is not None and attn_w.dim() >= 2:
|
| 604 |
+
if attn_w.dim() == 4: attn_w = attn_w[0]
|
| 605 |
+
elif attn_w.dim() == 3: attn_w = attn_w[0]
|
| 606 |
+
fig_xattn = cross_attention_weights_plot(attn_w, "Perceiver Cross-Attention (Last Layer)")
|
| 607 |
+
log_dict["visuals/attention/perceiver_xattn"] = trackio.Image(fig_to_pil(fig_xattn),
|
| 608 |
+
caption=f"step={global_step}")
|
| 609 |
+
plt.close(fig_xattn)
|
| 610 |
+
|
| 611 |
+
# ── Evidence Gate Heatmaps ──
|
| 612 |
+
if diagnostics_collector and diagnostics_collector.gate_values:
|
| 613 |
+
gate_vals = [gv[0] if gv.dim() == 3 else gv
|
| 614 |
+
for gv in diagnostics_collector.gate_values[-cfg.K:]]
|
| 615 |
+
if gate_vals:
|
| 616 |
+
fig_gates = evidence_gate_heatmap(gate_vals, "Evidence Gate Activations")
|
| 617 |
+
log_dict["visuals/gates/activations"] = trackio.Image(fig_to_pil(fig_gates),
|
| 618 |
+
caption=f"step={global_step}")
|
| 619 |
+
plt.close(fig_gates)
|
| 620 |
+
for k, gv in enumerate(gate_vals):
|
| 621 |
+
if isinstance(gv, torch.Tensor):
|
| 622 |
+
log_dict[f"diagnostics/gate_mean_step{k+1}"] = gv.float().mean().item()
|
| 623 |
+
|
| 624 |
+
# ── Input Image with Feature Similarity Heatmap (DINO style) ──
|
| 625 |
+
img_tensor = batch["pixel_values"][0]
|
| 626 |
+
mean = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1)
|
| 627 |
+
std = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1)
|
| 628 |
+
img_denorm = (img_tensor.cpu() * std + mean).clamp(0, 1)
|
| 629 |
+
img_np = (img_denorm.permute(1, 2, 0).numpy() * 255).astype(np.uint8)
|
| 630 |
+
|
| 631 |
+
vis_feats = vis_tok[0]
|
| 632 |
+
mean_feat = vis_feats.mean(0, keepdim=True)
|
| 633 |
+
sim = F.cosine_similarity(vis_feats, mean_feat.expand_as(vis_feats), dim=-1).detach().cpu()
|
| 634 |
+
|
| 635 |
+
n_patches = sim.size(0)
|
| 636 |
+
side = int(math.sqrt(n_patches))
|
| 637 |
+
if side * side == n_patches:
|
| 638 |
+
fig_attn_overlay, ax = plt.subplots(1, 2, figsize=(8, 4))
|
| 639 |
+
ax[0].imshow(img_np); ax[0].set_title("Input Image", fontsize=10); ax[0].axis('off')
|
| 640 |
+
attn_resized = np.array(PILImage.fromarray(
|
| 641 |
+
((sim.numpy().reshape(side, side) - sim.min().item()) /
|
| 642 |
+
(sim.max().item() - sim.min().item() + 1e-8) * 255).astype(np.uint8)
|
| 643 |
+
).resize((img_np.shape[1], img_np.shape[0]), PILImage.BILINEAR)) / 255.0
|
| 644 |
+
ax[1].imshow(img_np); ax[1].imshow(attn_resized, cmap='jet', alpha=0.5)
|
| 645 |
+
ax[1].set_title("Feature Similarity Heatmap", fontsize=10); ax[1].axis('off')
|
| 646 |
+
fig_attn_overlay.suptitle("Visual Feature Heatmap (DINO-style)", fontsize=12, fontweight='bold')
|
| 647 |
+
fig_attn_overlay.tight_layout()
|
| 648 |
+
log_dict["visuals/attention/feature_heatmap"] = trackio.Image(fig_to_pil(fig_attn_overlay),
|
| 649 |
+
caption=f"step={global_step}")
|
| 650 |
+
plt.close(fig_attn_overlay)
|
| 651 |
+
|
| 652 |
+
# ── Rollout Comparison Grid ──
|
| 653 |
+
if traj.dim() == 4 and traj.size(1) > 1:
|
| 654 |
+
n_show = min(batch["pixel_values"].size(0), 3)
|
| 655 |
+
images_for_grid = []
|
| 656 |
+
pcas_for_grid = []
|
| 657 |
+
for i in range(n_show):
|
| 658 |
+
img_i = batch["pixel_values"][i].cpu()
|
| 659 |
+
img_i_denorm = (img_i * std + mean).clamp(0, 1)
|
| 660 |
+
images_for_grid.append((img_i_denorm.permute(1, 2, 0).numpy() * 255).astype(np.uint8))
|
| 661 |
+
step_pcas = []
|
| 662 |
+
for k in range(traj.size(1)):
|
| 663 |
+
n_tok = traj.size(2)
|
| 664 |
+
side_k = max(int(math.sqrt(n_tok)), 1)
|
| 665 |
+
step_pcas.append(pca_feature_map(traj[i, k], side_k, side_k))
|
| 666 |
+
pcas_for_grid.append(step_pcas)
|
| 667 |
+
fig_grid = rollout_comparison_grid(images_for_grid, pcas_for_grid,
|
| 668 |
+
suptitle=f"Rollout PCA Grid (K={traj.size(1)-1})")
|
| 669 |
+
log_dict["visuals/rollout/comparison_grid"] = trackio.Image(fig_to_pil(fig_grid),
|
| 670 |
+
caption=f"step={global_step}")
|
| 671 |
+
plt.close(fig_grid)
|
| 672 |
+
|
| 673 |
+
trackio.log(log_dict)
|
| 674 |
+
log.info(f"Logged {len(log_dict)} visual diagnostics at step {global_step}")
|
| 675 |
+
|
| 676 |
+
except Exception as e:
|
| 677 |
+
log.warning(f"Visual diagnostics failed: {e}")
|
| 678 |
+
import traceback
|
| 679 |
+
traceback.print_exc()
|
| 680 |
+
finally:
|
| 681 |
+
model.train()
|
| 682 |
+
plt.close('all')
|
| 683 |
+
if diagnostics_collector:
|
| 684 |
+
diagnostics_collector.clear()
|
| 685 |
+
|
| 686 |
+
|
| 687 |
+
# ══════════════════════════════════════════════════════════════════════════
|
| 688 |
+
# PHASE 2 TRAINING
|
| 689 |
+
# ══════════════════════════════════════════════════════════════════════════
|
| 690 |
+
|
| 691 |
def download_phase1_checkpoint(hub_model_id: str, run_name: str = "hybrid_main"):
|
|
|
|
| 692 |
from huggingface_hub import hf_hub_download
|
| 693 |
+
path = hf_hub_download(repo_id=hub_model_id, filename=f"checkpoints/{run_name}_best.pt", repo_type="model")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
log.info(f"Downloaded checkpoint: {path}")
|
| 695 |
return path
|
| 696 |
|
| 697 |
|
| 698 |
def main():
|
| 699 |
parser = argparse.ArgumentParser(description="MR-JEPA Phase 2 Training")
|
| 700 |
+
parser.add_argument("--checkpoint", type=str, default=None)
|
|
|
|
| 701 |
parser.add_argument("--hub_model_id", default="JorgeAV/MR-JEPA")
|
| 702 |
parser.add_argument("--run_name", default="hybrid_main_phase2")
|
| 703 |
+
parser.add_argument("--phase1_run", default="hybrid_main")
|
|
|
|
| 704 |
parser.add_argument("--epochs", type=int, default=10)
|
| 705 |
parser.add_argument("--batch_size", type=int, default=16)
|
| 706 |
parser.add_argument("--grad_accum", type=int, default=8)
|
|
|
|
| 710 |
parser.add_argument("--unfreeze_visual_layers", type=int, default=6)
|
| 711 |
parser.add_argument("--unfreeze_text_layers", type=int, default=4)
|
| 712 |
parser.add_argument("--max_eval_samples", type=int, default=500)
|
| 713 |
+
parser.add_argument("--vis_interval", type=int, default=100)
|
| 714 |
parser.add_argument("--output_dir", default="./outputs/mrjepa_phase2")
|
| 715 |
args = parser.parse_args()
|
| 716 |
|
|
|
|
| 717 |
log.info("Downloading Phase 1 training script...")
|
| 718 |
from huggingface_hub import hf_hub_download
|
| 719 |
+
p1_script = hf_hub_download(repo_id=args.hub_model_id, filename="train_mrjepa.py", repo_type="model")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 720 |
import importlib.util
|
| 721 |
spec = importlib.util.spec_from_file_location("train_mrjepa", p1_script)
|
| 722 |
p1 = importlib.util.module_from_spec(spec)
|
| 723 |
spec.loader.exec_module(p1)
|
| 724 |
|
|
|
|
| 725 |
if args.checkpoint and os.path.exists(args.checkpoint):
|
| 726 |
ckpt_path = args.checkpoint
|
| 727 |
else:
|
|
|
|
| 730 |
log.info(f"Loading Phase 1 checkpoint: {ckpt_path}")
|
| 731 |
ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=False)
|
| 732 |
|
|
|
|
| 733 |
saved_cfg = ckpt["config"]
|
| 734 |
cfg = p1.Config()
|
| 735 |
for k, v in saved_cfg.items():
|
| 736 |
if hasattr(cfg, k):
|
| 737 |
setattr(cfg, k, v)
|
| 738 |
|
|
|
|
| 739 |
cfg.phase = 2
|
| 740 |
cfg.epochs = args.epochs
|
| 741 |
cfg.batch_size = args.batch_size
|
|
|
|
| 744 |
cfg.backbone_lr = args.backbone_lr
|
| 745 |
cfg.output_dir = args.output_dir
|
| 746 |
cfg.run_name = args.run_name
|
| 747 |
+
cfg.freeze_backbone = True
|
| 748 |
cfg.freeze_text = True
|
| 749 |
cfg.max_eval_samples = args.max_eval_samples
|
| 750 |
cfg.resolve()
|
|
|
|
| 753 |
log.info(f"Device: {device}")
|
| 754 |
os.makedirs(cfg.output_dir, exist_ok=True)
|
| 755 |
|
| 756 |
+
import trackio
|
| 757 |
+
trackio.init(
|
| 758 |
+
name=args.run_name, project="MR-JEPA",
|
| 759 |
+
config={
|
| 760 |
+
"phase": 2, "epochs": args.epochs,
|
| 761 |
+
"core_lr": args.core_lr, "backbone_lr": args.backbone_lr, "text_lr": args.text_lr,
|
| 762 |
+
"batch_size": args.batch_size, "grad_accum": args.grad_accum,
|
| 763 |
+
"unfreeze_visual_layers": args.unfreeze_visual_layers,
|
| 764 |
+
"unfreeze_text_layers": args.unfreeze_text_layers,
|
| 765 |
+
"phase1_best_acc": ckpt.get("eval_acc", "unknown"),
|
| 766 |
+
"vis_interval": args.vis_interval,
|
| 767 |
+
"backbone": cfg.backbone, "K": cfg.K, "use_jepa": cfg.use_jepa, "loss_fn": cfg.loss_fn,
|
| 768 |
+
}
|
| 769 |
+
)
|
| 770 |
+
log.info("Trackio initialized with visual diagnostics")
|
| 771 |
|
|
|
|
| 772 |
log.info("Building model...")
|
| 773 |
model = p1.MRJEPAModel(cfg)
|
| 774 |
model.evidence.load_state_dict(ckpt["evidence"])
|
|
|
|
| 778 |
model.target.t_ro.load_state_dict(ckpt["target_ro"])
|
| 779 |
log.info(f"Loaded Phase 1 weights (epoch={ckpt.get('epoch','?')}, eval_acc={ckpt.get('eval_acc','?')}%)")
|
| 780 |
|
|
|
|
| 781 |
log.info(f"Unfreezing last {args.unfreeze_visual_layers} visual layers, "
|
| 782 |
f"last {args.unfreeze_text_layers} text layers")
|
| 783 |
model.vis.unfreeze_last(args.unfreeze_visual_layers)
|
|
|
|
| 787 |
total_p = sum(p.numel() for p in model.parameters())
|
| 788 |
train_p = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 789 |
log.info(f"Total: {total_p:,} | Trainable: {train_p:,} ({100*train_p/total_p:.1f}%)")
|
| 790 |
+
trackio.log({"model/total_params": total_p, "model/trainable_params": train_p,
|
| 791 |
+
"model/trainable_pct": 100 * train_p / total_p})
|
| 792 |
|
|
|
|
| 793 |
transform = model.vis.get_transform()
|
| 794 |
tokenizer = model.txt.tokenizer
|
| 795 |
train_ds = p1.ScienceQADataset("train", transform=transform, tokenizer=tokenizer,
|
|
|
|
| 803 |
eval_dl = DataLoader(eval_ds, batch_size=cfg.batch_size, shuffle=False,
|
| 804 |
num_workers=2, collate_fn=coll, pin_memory=True)
|
| 805 |
|
|
|
|
| 806 |
backbone_params = [p for p in model.vis.parameters() if p.requires_grad]
|
| 807 |
text_params = [p for p in model.txt.parameters() if p.requires_grad]
|
|
|
|
| 808 |
bb_txt_ids = {id(p) for p in backbone_params + text_params}
|
| 809 |
core_params = [p for p in model.parameters() if p.requires_grad and id(p) not in bb_txt_ids]
|
| 810 |
|
|
|
|
| 829 |
|
| 830 |
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)
|
| 831 |
|
| 832 |
+
diag_collector = DiagnosticCollector()
|
| 833 |
+
diag_collector.attach(model)
|
| 834 |
+
|
| 835 |
log.info(f"Phase 2: {cfg.epochs} epochs, {len(train_dl)} batches/epoch, ga={cfg.grad_accum}")
|
| 836 |
+
log.info(f"Visual diagnostics every {args.vis_interval} optimizer steps")
|
| 837 |
global_step = 0
|
| 838 |
best_acc = ckpt.get("eval_acc", 0.0)
|
| 839 |
amp_dtype = torch.bfloat16 if cfg.bf16 else torch.float32
|
|
|
|
| 848 |
|
| 849 |
for batch_idx, batch in enumerate(train_dl):
|
| 850 |
batch = {k: v.to(device) if isinstance(v, torch.Tensor) else v for k, v in batch.items()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 851 |
vis_tok = model.vis(batch["pixel_values"]).float()
|
| 852 |
txt_tok = model.txt(batch["input_ids"], batch["attention_mask"]).float()
|
| 853 |
|
| 854 |
with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=cfg.bf16 and device.type == "cuda"):
|
| 855 |
evidence, _, ev_mask = model.evidence(vis_tok, txt_tok, batch["attention_mask"])
|
|
|
|
| 856 |
if model._use_rollout:
|
| 857 |
traj, z_final, z_proj = model.rollout(evidence)
|
| 858 |
else:
|
| 859 |
B = batch["batch_size"]
|
| 860 |
z0 = model.rollout.init_tokens.expand(B, -1, -1) + \
|
| 861 |
model.rollout.z0_proj(F.adaptive_avg_pool1d(
|
| 862 |
+
evidence.permute(0,2,1), model.rollout.num_tokens).permute(0,2,1))
|
|
|
|
| 863 |
z_final = z0
|
| 864 |
z_proj = model.rollout.out_proj(z0).unsqueeze(1)
|
| 865 |
|
|
|
|
| 866 |
if model._use_jepa:
|
| 867 |
target_proj = model.target(vis_tok.detach(), txt_tok.detach(), batch["attention_mask"].detach())
|
| 868 |
else:
|
| 869 |
target_proj = None
|
| 870 |
|
|
|
|
| 871 |
opt_emb = model.encode_options(batch["opt_input_ids"], batch["opt_attention_mask"])
|
| 872 |
opt_emb = opt_emb.view(batch["batch_size"], cfg.max_options, -1)
|
| 873 |
logits = model.disc(z_final, opt_emb, batch["opt_mask"])
|
|
|
|
| 877 |
losses = model.jepa_loss(z_proj, target_proj, task_loss)
|
| 878 |
else:
|
| 879 |
losses = {"total": task_loss, "jepa": torch.tensor(0.0), "task": task_loss, "reg": torch.tensor(0.0)}
|
|
|
|
| 880 |
loss = losses["total"] / cfg.grad_accum
|
| 881 |
|
| 882 |
loss.backward()
|
| 883 |
|
| 884 |
if (batch_idx + 1) % cfg.grad_accum == 0:
|
| 885 |
nn.utils.clip_grad_norm_(trainable, cfg.max_grad_norm)
|
| 886 |
+
optimizer.step(); scheduler.step(); optimizer.zero_grad()
|
|
|
|
|
|
|
| 887 |
model.update_target(global_step, total_steps)
|
| 888 |
global_step += 1
|
| 889 |
+
if global_step % args.vis_interval == 0 and global_step > 0:
|
| 890 |
+
log.info(f"Generating visual diagnostics at step {global_step}...")
|
| 891 |
+
log_visual_diagnostics(model, batch, device, cfg, global_step, epoch,
|
| 892 |
+
diagnostics_collector=diag_collector, vis_interval=args.vis_interval)
|
| 893 |
|
| 894 |
preds = logits.argmax(dim=-1)
|
| 895 |
for k, v in losses.items():
|
|
|
|
| 905 |
log.info(f"P2 E{epoch} B{batch_idx}/{len(train_dl)} | "
|
| 906 |
f"loss={avg.get('total',0):.4f} jepa={avg.get('jepa',0):.4f} "
|
| 907 |
f"task={avg.get('task',0):.4f} | acc={acc:.1f}%")
|
| 908 |
+
trackio.log({
|
| 909 |
+
"train/loss": avg.get("total", 0), "train/jepa_loss": avg.get("jepa", 0),
|
| 910 |
+
"train/task_loss": avg.get("task", 0), "train/reg_loss": avg.get("reg", 0),
|
| 911 |
+
"train/accuracy": acc, "train/lr": lrs[0] if lrs else 0,
|
| 912 |
+
"train/backbone_lr": lrs[1] if len(lrs) > 1 else 0,
|
| 913 |
+
"train/text_lr": lrs[2] if len(lrs) > 2 else 0,
|
| 914 |
+
"train/ema_momentum": model.target.mom,
|
| 915 |
+
"train/epoch": epoch, "train/step": global_step,
|
| 916 |
+
})
|
| 917 |
+
|
|
|
|
|
|
|
| 918 |
eval_acc = p1.evaluate(model, eval_dl, device, cfg)
|
| 919 |
train_acc = epoch_correct / max(epoch_total, 1) * 100
|
| 920 |
log.info(f"=== Phase 2 Epoch {epoch} | Train: {train_acc:.1f}% | Eval: {eval_acc:.1f}% ===")
|
| 921 |
+
trackio.log({"eval/accuracy": eval_acc, "eval/epoch": epoch,
|
| 922 |
+
"eval/train_accuracy": train_acc, "eval/best_accuracy": max(best_acc, eval_acc)})
|
| 923 |
|
| 924 |
+
log.info(f"Generating epoch-end visual diagnostics...")
|
| 925 |
+
diag_batch = next(iter(eval_dl))
|
| 926 |
+
diag_batch = {k: v.to(device) if isinstance(v, torch.Tensor) else v for k, v in diag_batch.items()}
|
| 927 |
+
log_visual_diagnostics(model, diag_batch, device, cfg, global_step, epoch,
|
| 928 |
+
diagnostics_collector=diag_collector, vis_interval=args.vis_interval)
|
| 929 |
|
| 930 |
if eval_acc > best_acc:
|
| 931 |
best_acc = eval_acc
|
| 932 |
p1.save_checkpoint(model, cfg, epoch, eval_acc, is_best=True)
|
| 933 |
+
log.info(f"New best accuracy: {best_acc:.1f}%")
|
| 934 |
|
| 935 |
log.info(f"Phase 2 complete. Best eval accuracy: {best_acc:.1f}%")
|
| 936 |
+
diag_collector.detach()
|
| 937 |
+
trackio.log({"final/best_accuracy": best_acc, "final/phase": 2, "final/total_steps": global_step})
|
| 938 |
if cfg.push_to_hub:
|
| 939 |
p1.push_results(cfg, best_acc)
|
| 940 |
|