OdaxAI commited on
Commit
63d7636
·
verified ·
1 Parent(s): 09f0673

Add train.py

Browse files
Files changed (1) hide show
  1. train.py +375 -0
train.py ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Train Venice-H1 re-ranker on pre-extracted DeRIS feature caches.
4
+
5
+ Matches paper Section 3.5 exactly:
6
+ - Loss: L = L_gate + λ·L_gain (λ=5)
7
+ - L_gate: focal BCE (γ=2, auto w_pos)
8
+ - L_gain: smooth-L1 IoU regression on ALL samples
9
+ - AdamW (lr=5e-4, wd=1e-4), cosine + 3-epoch warmup
10
+ - 20 epochs, batch 512, FP16
11
+ - Training time: ~3 min on single RTX 3090
12
+
13
+ Usage:
14
+ python train.py --config venice_h1/configs/default.yaml
15
+ python train.py --config venice_h1/configs/default.yaml --no_grid
16
+ """
17
+
18
+ import argparse
19
+ import os
20
+ import random
21
+ from pathlib import Path
22
+
23
+ import numpy as np
24
+ import torch
25
+ import torch.nn as nn
26
+ import torch.nn.functional as F
27
+ from torch.utils.data import DataLoader, Dataset, ConcatDataset
28
+ from torch.cuda.amp import GradScaler, autocast
29
+
30
+ try:
31
+ from torch.utils.tensorboard import SummaryWriter
32
+ _HAS_TB = True
33
+ except ImportError:
34
+ _HAS_TB = False
35
+
36
+ try:
37
+ import yaml
38
+ except ImportError:
39
+ raise SystemExit("pip install pyyaml")
40
+
41
+ try:
42
+ from sklearn.metrics import roc_auc_score
43
+ _HAS_SKLEARN = True
44
+ except ImportError:
45
+ _HAS_SKLEARN = False
46
+
47
+ from venice_h1.model.reranker import VeniceH1Reranker
48
+
49
+
50
+ def set_seed(seed: int):
51
+ random.seed(seed)
52
+ np.random.seed(seed)
53
+ torch.manual_seed(seed)
54
+ if torch.cuda.is_available():
55
+ torch.cuda.manual_seed_all(seed)
56
+
57
+
58
+ def load_config(path: str) -> dict:
59
+ with open(path) as f:
60
+ return yaml.safe_load(f)
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Dataset
65
+ # ---------------------------------------------------------------------------
66
+
67
+ class FeatureCacheDataset(Dataset):
68
+ """Loads a pre-extracted .pt feature cache from extract_features.py."""
69
+
70
+ def __init__(self, path: str, use_grid: bool = True):
71
+ data = torch.load(path, map_location="cpu")
72
+ self.use_grid = use_grid
73
+
74
+ self.query_feat = data["query_feat"].float()
75
+ self.det_scores = data["det_scores"].float()
76
+ self.query_ious = data["query_ious"].float()
77
+ self.oracle_idx = data["oracle_idx"].long()
78
+
79
+ self.mask_mean = data["mask_mean"].float()
80
+ self.mask_max = data["mask_max"].float()
81
+ self.mask_area = data["mask_area"].float()
82
+ self.mask_std = data["mask_std"].float()
83
+
84
+ if use_grid:
85
+ self.grid_mean_4 = data["grid_mean_4"].float()
86
+ self.grid_max_4 = data["grid_max_4"].float()
87
+ self.boundary_4 = data["boundary_4"].float()
88
+ self.grid_mean_8 = data["grid_mean_8"].float()
89
+ self.grid_max_8 = data["grid_max_8"].float()
90
+ self.boundary_8 = data["boundary_8"].float()
91
+ self.grid_mean_16 = data["grid_mean_16"].float()
92
+ self.grid_max_16 = data["grid_max_16"].float()
93
+ self.boundary_16 = data["boundary_16"].float()
94
+
95
+ self.failure_flag = (self.oracle_idx != 0).float()
96
+
97
+ def __len__(self):
98
+ return len(self.oracle_idx)
99
+
100
+ def __getitem__(self, idx):
101
+ N = self.query_feat.shape[1]
102
+ qf = self.query_feat[idx] # [N, D]
103
+ ds = self.det_scores[idx].unsqueeze(-1) # [N, 1]
104
+ mm = self.mask_mean[idx].unsqueeze(-1) # [N, 1]
105
+ mx = self.mask_max[idx].unsqueeze(-1) # [N, 1]
106
+ ma = self.mask_area[idx].unsqueeze(-1) # [N, 1]
107
+ ms = self.mask_std[idx].unsqueeze(-1) # [N, 1]
108
+
109
+ parts = [qf, ds, mm, mx, ma, ms] # [N, D+5]
110
+
111
+ if self.use_grid:
112
+ gm4 = self.grid_mean_4[idx] # [N, 16]
113
+ gx4 = self.grid_max_4[idx] # [N, 16]
114
+ b4 = self.boundary_4[idx].unsqueeze(-1) # [N, 1]
115
+ gm8 = self.grid_mean_8[idx] # [N, 64]
116
+ gx8 = self.grid_max_8[idx] # [N, 64]
117
+ b8 = self.boundary_8[idx].unsqueeze(-1) # [N, 1]
118
+ gm16 = self.grid_mean_16[idx] # [N, 256]
119
+ gx16 = self.grid_max_16[idx] # [N, 256]
120
+ b16 = self.boundary_16[idx].unsqueeze(-1) # [N, 1]
121
+ parts += [gm4, gx4, b4, gm8, gx8, b8, gm16, gx16, b16]
122
+
123
+ features = torch.cat(parts, dim=-1) # [N, 936]
124
+
125
+ return {
126
+ "features": features,
127
+ "det_scores": self.det_scores[idx],
128
+ "mask_means": self.mask_mean[idx],
129
+ "oracle_idx": self.oracle_idx[idx],
130
+ "failure_flag": self.failure_flag[idx],
131
+ "query_ious": self.query_ious[idx],
132
+ }
133
+
134
+
135
+ # ---------------------------------------------------------------------------
136
+ # Loss (Section 3.5)
137
+ # ---------------------------------------------------------------------------
138
+
139
+ def focal_bce_loss(pred: torch.Tensor, target: torch.Tensor,
140
+ gamma: float = 2.0, pos_weight: float | None = None):
141
+ """Focal binary cross-entropy (Eq. in Section 3.5)."""
142
+ if pos_weight is not None:
143
+ weight = torch.where(target > 0.5, pos_weight, 1.0)
144
+ else:
145
+ weight = None
146
+
147
+ bce = F.binary_cross_entropy(pred, target, reduction='none')
148
+ pt = pred * target + (1 - pred) * (1 - target)
149
+ focal = ((1 - pt) ** gamma) * bce
150
+
151
+ if weight is not None:
152
+ focal = focal * weight
153
+ return focal.mean()
154
+
155
+
156
+ def compute_loss(out: dict, batch: dict, cfg: dict) -> dict:
157
+ """L = L_gate + λ·L_gain (Section 3.5)."""
158
+ device = out["p_fail"].device
159
+ tcfg = cfg["training"]
160
+
161
+ failure_flag = batch["failure_flag"].to(device)
162
+ query_ious = batch["query_ious"].to(device)
163
+
164
+ # Auto positive weight for focal BCE
165
+ n_pos = failure_flag.sum().clamp(min=1)
166
+ n_neg = (1 - failure_flag).sum().clamp(min=1)
167
+ wpos = (n_neg / n_pos).clamp(max=50.0) if tcfg.get("auto_wpos") else None
168
+
169
+ # L_gate: focal BCE
170
+ loss_gate = focal_bce_loss(
171
+ out["p_fail"], failure_flag,
172
+ gamma=tcfg["focal_gamma"], pos_weight=wpos)
173
+
174
+ # L_gain: smooth-L1 on IoU gain (all samples, dense supervision)
175
+ gain_target = query_ious - query_ious[:, 0:1] # relative to Q0
176
+ loss_gain = F.smooth_l1_loss(out["gain_logits"], gain_target)
177
+
178
+ # Total loss
179
+ total = loss_gate + tcfg["lambda_gain"] * loss_gain
180
+
181
+ return {"total": total, "gate": loss_gate, "gain": loss_gain}
182
+
183
+
184
+ # ---------------------------------------------------------------------------
185
+ # Evaluation
186
+ # ---------------------------------------------------------------------------
187
+
188
+ @torch.no_grad()
189
+ def evaluate(model, loader, device, tau: float = 0.05) -> dict:
190
+ model.eval()
191
+ total = failures = reranked_count = correct = harmful = 0
192
+ all_pfail, all_flag = [], []
193
+
194
+ for batch in loader:
195
+ features = batch["features"].to(device)
196
+ det_scores = batch["det_scores"].to(device)
197
+ mask_means = batch["mask_means"].to(device)
198
+ oracle_idx = batch["oracle_idx"].to(device)
199
+ failure_flag = batch["failure_flag"].to(device)
200
+ query_ious = batch["query_ious"].to(device)
201
+
202
+ out = model(features, det_scores, mask_means)
203
+ selected = model.rerank(features, det_scores, mask_means, tau=tau)
204
+
205
+ B = len(oracle_idx)
206
+ total += B
207
+ fail_mask = failure_flag.bool()
208
+ failures += fail_mask.sum().item()
209
+
210
+ reranked_mask = selected != 0
211
+ reranked_count += reranked_mask.sum().item()
212
+ correct += (selected[fail_mask] == oracle_idx[fail_mask]).sum().item()
213
+
214
+ q0_iou = query_ious[:, 0]
215
+ sel_iou = query_ious.gather(1, selected.unsqueeze(1)).squeeze(1)
216
+ harmful += (reranked_mask & (sel_iou < q0_iou - 0.01)).sum().item()
217
+
218
+ all_pfail.append(out["p_fail"].cpu())
219
+ all_flag.append(failure_flag.cpu())
220
+
221
+ all_pfail = torch.cat(all_pfail).numpy()
222
+ all_flag = torch.cat(all_flag).numpy()
223
+ auc = roc_auc_score(all_flag, all_pfail) if _HAS_SKLEARN and all_flag.sum() > 0 else 0.0
224
+
225
+ return {
226
+ "failure_rate": failures / max(total, 1),
227
+ "rerank_rate": reranked_count / max(total, 1),
228
+ "correct_rerank": correct / max(failures, 1),
229
+ "harmful_switch": harmful / max(total, 1),
230
+ "gate_auc": auc,
231
+ }
232
+
233
+
234
+ # ---------------------------------------------------------------------------
235
+ # Main
236
+ # ---------------------------------------------------------------------------
237
+
238
+ def main():
239
+ parser = argparse.ArgumentParser(description="Train Venice-H1 re-ranker")
240
+ parser.add_argument("--config", default="venice_h1/configs/default.yaml")
241
+ parser.add_argument("--no_grid", action="store_true",
242
+ help="Ablation: BASE features only (Df=261)")
243
+ parser.add_argument("--tau", type=float, default=None)
244
+ args = parser.parse_args()
245
+
246
+ cfg = load_config(args.config)
247
+ if args.no_grid:
248
+ cfg["model"]["use_grid"] = False
249
+
250
+ set_seed(cfg["training"]["seed"])
251
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
252
+ print(f"Device: {device}")
253
+
254
+ use_grid = cfg["model"]["use_grid"]
255
+ tau = args.tau if args.tau is not None else cfg["model"]["tau"]
256
+
257
+ # ---- Data ----
258
+ train_paths = [p for p in cfg["data"]["train_splits"] if Path(p).exists()]
259
+ val_paths = [p for p in cfg["data"]["val_splits"] if Path(p).exists()]
260
+
261
+ if not train_paths:
262
+ print("ERROR: No training data found. Run extract_features.py first.")
263
+ print("Expected paths:", cfg["data"]["train_splits"])
264
+ return
265
+
266
+ train_ds = ConcatDataset([FeatureCacheDataset(p, use_grid) for p in train_paths])
267
+ val_ds = ConcatDataset([FeatureCacheDataset(p, use_grid) for p in val_paths]) \
268
+ if val_paths else None
269
+
270
+ bs = cfg["training"]["batch_size"]
271
+ train_loader = DataLoader(train_ds, batch_size=bs, shuffle=True,
272
+ num_workers=4, pin_memory=True, drop_last=True)
273
+ val_loader = DataLoader(val_ds, batch_size=bs, shuffle=False,
274
+ num_workers=4, pin_memory=True) if val_ds else None
275
+
276
+ print(f"Train: {len(train_ds):,} samples | "
277
+ f"Val: {len(val_ds) if val_ds else 0:,} samples")
278
+ print(f"Feature set: {'BASE+GRID (Df=936)' if use_grid else 'BASE (Df=261)'}")
279
+
280
+ # ---- Model ----
281
+ model = VeniceH1Reranker(**cfg["model"]).to(device)
282
+ print(f"Venice-H1: {model.num_parameters():,} parameters (~11.3M)")
283
+
284
+ # ---- Optimizer (Section 3.5: AdamW, lr=5e-4, wd=1e-4) ----
285
+ optimizer = torch.optim.AdamW(
286
+ model.parameters(),
287
+ lr=cfg["training"]["lr"],
288
+ weight_decay=cfg["training"]["weight_decay"])
289
+
290
+ epochs = cfg["training"]["epochs"]
291
+ warmup = cfg["training"]["warmup_epochs"]
292
+
293
+ def lr_lambda(epoch):
294
+ if epoch < warmup:
295
+ return epoch / warmup
296
+ progress = (epoch - warmup) / (epochs - warmup)
297
+ return 0.5 * (1 + np.cos(np.pi * progress))
298
+
299
+ scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)
300
+
301
+ # FP16
302
+ scaler = GradScaler() if cfg["training"].get("fp16") else None
303
+
304
+ # Logging
305
+ writer = SummaryWriter(cfg["output"]["log_dir"]) if _HAS_TB else None
306
+ ckpt_dir = cfg["output"]["checkpoint_dir"]
307
+ os.makedirs(ckpt_dir, exist_ok=True)
308
+
309
+ best_auc = 0.0
310
+
311
+ # ---- Training Loop ----
312
+ print(f"\nTraining for {epochs} epochs (batch={bs}, lr={cfg['training']['lr']})")
313
+ print("-" * 70)
314
+
315
+ for epoch in range(1, epochs + 1):
316
+ model.train()
317
+ epoch_loss = 0.0
318
+
319
+ for batch in train_loader:
320
+ features = batch["features"].to(device)
321
+ det_scores = batch["det_scores"].to(device)
322
+ mask_means = batch["mask_means"].to(device)
323
+
324
+ optimizer.zero_grad()
325
+
326
+ if scaler:
327
+ with autocast():
328
+ out = model(features, det_scores, mask_means)
329
+ losses = compute_loss(out, batch, cfg)
330
+ scaler.scale(losses["total"]).backward()
331
+ scaler.unscale_(optimizer)
332
+ nn.utils.clip_grad_norm_(model.parameters(), 1.0)
333
+ scaler.step(optimizer)
334
+ scaler.update()
335
+ else:
336
+ out = model(features, det_scores, mask_means)
337
+ losses = compute_loss(out, batch, cfg)
338
+ losses["total"].backward()
339
+ nn.utils.clip_grad_norm_(model.parameters(), 1.0)
340
+ optimizer.step()
341
+
342
+ epoch_loss += losses["total"].item()
343
+
344
+ scheduler.step()
345
+ avg_loss = epoch_loss / len(train_loader)
346
+
347
+ # Validation
348
+ metrics = evaluate(model, val_loader, device, tau=tau) if val_loader else {}
349
+
350
+ if writer:
351
+ writer.add_scalar("train/loss", avg_loss, epoch)
352
+ for k, v in metrics.items():
353
+ writer.add_scalar(f"val/{k}", v, epoch)
354
+
355
+ auc = metrics.get("gate_auc", 0)
356
+ harm = metrics.get("harmful_switch", 0) * 100
357
+ print(f" Epoch {epoch:2d}/{epochs} | loss={avg_loss:.4f} | "
358
+ f"AUC={auc:.3f} | harmful={harm:.2f}%")
359
+
360
+ if auc > best_auc:
361
+ best_auc = auc
362
+ torch.save(
363
+ {"epoch": epoch, "model": model.state_dict(),
364
+ "config": cfg, "metrics": metrics},
365
+ os.path.join(ckpt_dir, "best.pt"))
366
+
367
+ if writer:
368
+ writer.close()
369
+
370
+ print(f"\nDone. Best Gate AUC: {best_auc:.4f}")
371
+ print(f"Checkpoint: {os.path.join(ckpt_dir, 'best.pt')}")
372
+
373
+
374
+ if __name__ == "__main__":
375
+ main()