erfanasgari21 commited on
Commit
290ed08
·
1 Parent(s): 255c417
Files changed (3) hide show
  1. config/config.yaml +5 -3
  2. notebooks/exploratory.ipynb +55 -27
  3. scripts/train.py +36 -12
config/config.yaml CHANGED
@@ -59,12 +59,14 @@ model:
59
  anomaly_smooth_sigma: 1.5
60
 
61
  train:
62
- batch_size_mvtec: 20
63
- batch_size_visa: 12
 
 
64
  optimizer: "adam"
65
  lr: 0.0001
66
  epochs: 100
67
- num_workers: 4
68
  device: "cuda"
69
  seed: 42
70
  unified: true # every batch samples across ALL classes
 
59
  anomaly_smooth_sigma: 1.5
60
 
61
  train:
62
+ # Paper uses 20/12. On ~14–16GB use 2–4 + AMP; raise if you have headroom.
63
+ batch_size_mvtec: 4
64
+ batch_size_visa: 2
65
+ use_amp: true
66
  optimizer: "adam"
67
  lr: 0.0001
68
  epochs: 100
69
+ num_workers: 2
70
  device: "cuda"
71
  seed: 42
72
  unified: true # every batch samples across ALL classes
notebooks/exploratory.ipynb CHANGED
@@ -199,7 +199,7 @@
199
  "metadata": {},
200
  "outputs": [],
201
  "source": [
202
- "# تست Reconstruction Sub-network به تنهایی\n",
203
  "recon = ReconstructionNet(norm_type=cfg[\"model\"][\"reconstruction_norm\"])\n",
204
  "x = torch.randn(2, 3, 256, 256)\n",
205
  "rec_img, rec_jnd, feats_inp, feats_rec, feats_jnd = recon(x)\n",
@@ -210,7 +210,6 @@
210
  "print(\"feats_rec:\", [f.shape for f in feats_rec])\n",
211
  "print(\"feats_jnd:\", [f.shape for f in feats_jnd])\n",
212
  "\n",
213
- "# تست DiffNeck: مسیرهای (Rec,Inp), (JND,Inp), (Rec,JND,Inp)\n",
214
  "diffneck = DiffNeck(\n",
215
  " channels_list=recon.level_channels,\n",
216
  " out_ch=cfg[\"model\"][\"base_channels\"],\n",
@@ -221,7 +220,6 @@
221
  "assert desc.shape[0] == 2 and desc.shape[1] == cfg[\"model\"][\"base_channels\"]\n",
222
  "print(\"DiffNeck descriptor:\", desc.shape)\n",
223
  "\n",
224
- "# تست LocalizationNet به تنهایی\n",
225
  "loc = LocalizationNet(\n",
226
  " in_ch=3 + cfg[\"model\"][\"base_channels\"],\n",
227
  " base_ch=cfg[\"model\"][\"base_channels\"],\n",
@@ -229,16 +227,16 @@
229
  " dilation_rates=tuple(cfg[\"model\"][\"dilation_rates\"]),\n",
230
  " norm_type=cfg[\"model\"][\"localization_norm\"],\n",
231
  ")\n",
232
- "# pad descriptor to full res if needed\n",
233
- "if desc.shape[-2:] != x.shape[-2:]:\n",
234
- " import torch.nn.functional as F\n",
235
- " desc_up = F.interpolate(desc, size=x.shape[-2:], mode=\"bilinear\", align_corners=False)\n",
236
- "else:\n",
237
- " desc_up = desc\n",
238
- "loc_in = torch.cat([x, desc_up], dim=1)\n",
239
- "seg = loc(loc_in)\n",
240
  "assert seg.shape == (2, 4, 256, 256)\n",
241
- "print(\"Localization seg_logits:\", seg.shape)"
 
 
 
242
  ]
243
  },
244
  {
@@ -248,12 +246,22 @@
248
  "metadata": {},
249
  "outputs": [],
250
  "source": [
251
- "# تست کامل مدل OmniAL (forward pass end-to-end)\n",
 
 
 
252
  "model, optimizer, criterion, device = build_model_and_optimizer(cfg)\n",
253
- "x = torch.randn(2, 3, *size).to(device)\n",
254
- "out = model(x)\n",
 
 
 
 
255
  "for k, v in out.items():\n",
256
- " print(k, v.shape)"
 
 
 
257
  ]
258
  },
259
  {
@@ -271,17 +279,24 @@
271
  "metadata": {},
272
  "outputs": [],
273
  "source": [
 
 
 
 
 
274
  "batch = next(iter(train_loader))\n",
275
- "anomaly_img = batch[\"anomaly_img\"].to(device)\n",
 
276
  "targets = {\n",
277
- " \"image\": batch[\"image\"].to(device),\n",
278
- " \"jnd\": batch[\"jnd\"].to(device),\n",
279
- " \"severity\": batch[\"severity\"].to(device),\n",
280
  "}\n",
281
  "\n",
282
- "outputs = model(anomaly_img)\n",
283
- "losses = criterion(outputs, targets)\n",
284
- "losses"
 
285
  ]
286
  },
287
  {
@@ -290,7 +305,8 @@
290
  "metadata": {},
291
  "source": [
292
  "## 6. آموزش مدل (Training Loop)\n",
293
- "برای تست سریع، ابتدا `epochs` را در `config.yaml` کم بگذار (مثلا 2) تا فقط pipeline را چک کنی."
 
294
  ]
295
  },
296
  {
@@ -300,10 +316,22 @@
300
  "metadata": {},
301
  "outputs": [],
302
  "source": [
303
- "# برای تست سریع فقط چند epoch:\n",
 
304
  "cfg[\"train\"][\"epochs\"] = 2\n",
 
 
 
 
 
 
 
 
 
 
 
305
  "history = train_model(model, train_loader, optimizer, criterion, device, cfg)\n",
306
- "plot_loss_curves(history)"
307
  ]
308
  },
309
  {
@@ -388,4 +416,4 @@
388
  },
389
  "nbformat": 4,
390
  "nbformat_minor": 5
391
- }
 
199
  "metadata": {},
200
  "outputs": [],
201
  "source": [
202
+ "# Component smoke tests stay on CPU so they do not eat VRAM before the real model.\n",
203
  "recon = ReconstructionNet(norm_type=cfg[\"model\"][\"reconstruction_norm\"])\n",
204
  "x = torch.randn(2, 3, 256, 256)\n",
205
  "rec_img, rec_jnd, feats_inp, feats_rec, feats_jnd = recon(x)\n",
 
210
  "print(\"feats_rec:\", [f.shape for f in feats_rec])\n",
211
  "print(\"feats_jnd:\", [f.shape for f in feats_jnd])\n",
212
  "\n",
 
213
  "diffneck = DiffNeck(\n",
214
  " channels_list=recon.level_channels,\n",
215
  " out_ch=cfg[\"model\"][\"base_channels\"],\n",
 
220
  "assert desc.shape[0] == 2 and desc.shape[1] == cfg[\"model\"][\"base_channels\"]\n",
221
  "print(\"DiffNeck descriptor:\", desc.shape)\n",
222
  "\n",
 
223
  "loc = LocalizationNet(\n",
224
  " in_ch=3 + cfg[\"model\"][\"base_channels\"],\n",
225
  " base_ch=cfg[\"model\"][\"base_channels\"],\n",
 
227
  " dilation_rates=tuple(cfg[\"model\"][\"dilation_rates\"]),\n",
228
  " norm_type=cfg[\"model\"][\"localization_norm\"],\n",
229
  ")\n",
230
+ "import torch.nn.functional as F\n",
231
+ "desc_up = desc if desc.shape[-2:] == x.shape[-2:] else F.interpolate(\n",
232
+ " desc, size=x.shape[-2:], mode=\"bilinear\", align_corners=False\n",
233
+ ")\n",
234
+ "seg = loc(torch.cat([x, desc_up], dim=1))\n",
 
 
 
235
  "assert seg.shape == (2, 4, 256, 256)\n",
236
+ "print(\"Localization seg_logits:\", seg.shape)\n",
237
+ "\n",
238
+ "# Free unit-test nets / activations before putting OmniAL on GPU\n",
239
+ "del recon, diffneck, loc, x, rec_img, rec_jnd, feats_inp, feats_rec, feats_jnd, desc, desc_up, seg\n"
240
  ]
241
  },
242
  {
 
246
  "metadata": {},
247
  "outputs": [],
248
  "source": [
249
+ "# Full OmniAL forward (small batch). Clear leftover CUDA junk first.\n",
250
+ "if torch.cuda.is_available():\n",
251
+ " torch.cuda.empty_cache()\n",
252
+ "\n",
253
  "model, optimizer, criterion, device = build_model_and_optimizer(cfg)\n",
254
+ "n_params = sum(p.numel() for p in model.parameters()) / 1e6\n",
255
+ "print(f\"params: {n_params:.1f}M | device={device}\")\n",
256
+ "\n",
257
+ "with torch.no_grad():\n",
258
+ " x = torch.randn(2, 3, *size, device=device)\n",
259
+ " out = model(x)\n",
260
  "for k, v in out.items():\n",
261
+ " print(k, v.shape)\n",
262
+ "del x, out\n",
263
+ "if torch.cuda.is_available():\n",
264
+ " torch.cuda.empty_cache()\n"
265
  ]
266
  },
267
  {
 
279
  "metadata": {},
280
  "outputs": [],
281
  "source": [
282
+ "# Smoke-test loss on 2 samples only — NOT the full train batch (paper BS=20).\n",
283
+ "# Full BS=20 + autograd + this UNet can exceed ~14GB VRAM.\n",
284
+ "if torch.cuda.is_available():\n",
285
+ " torch.cuda.empty_cache()\n",
286
+ "\n",
287
  "batch = next(iter(train_loader))\n",
288
+ "n = 2 # keep tiny for notebook testing\n",
289
+ "anomaly_img = batch[\"anomaly_img\"][:n].to(device)\n",
290
  "targets = {\n",
291
+ " \"image\": batch[\"image\"][:n].to(device),\n",
292
+ " \"jnd\": batch[\"jnd\"][:n].to(device),\n",
293
+ " \"severity\": batch[\"severity\"][:n].to(device),\n",
294
  "}\n",
295
  "\n",
296
+ "with torch.no_grad(): # forward-only is enough to verify the loss pipeline\n",
297
+ " outputs = model(anomaly_img)\n",
298
+ " losses = criterion(outputs, targets)\n",
299
+ "losses\n"
300
  ]
301
  },
302
  {
 
305
  "metadata": {},
306
  "source": [
307
  "## 6. آموزش مدل (Training Loop)\n",
308
+ "برای تست سریع فقط ۲ epoch. **حتماً DataLoader را دوباره بساز** (batch قبلی احتمالاً هنوز ۲۰ بوده).\n",
309
+ "اگر OOM دیدی: kernel را Restart کن، بعد `batch_size_mvtec` را روی ۲ بگذار.\n"
310
  ]
311
  },
312
  {
 
316
  "metadata": {},
317
  "outputs": [],
318
  "source": [
319
+ "# Quick pipeline check (2 epochs). Rebuild loader — changing cfg alone does NOT resize an existing DataLoader.\n",
320
+ "# Restart kernel first if VRAM is already nearly full from earlier cells.\n",
321
  "cfg[\"train\"][\"epochs\"] = 2\n",
322
+ "cfg[\"train\"][\"batch_size_mvtec\"] = 2 # safe on ~14GB with this architecture\n",
323
+ "cfg[\"train\"][\"use_amp\"] = True\n",
324
+ "cfg[\"train\"][\"num_workers\"] = 2\n",
325
+ "\n",
326
+ "if torch.cuda.is_available():\n",
327
+ " torch.cuda.empty_cache()\n",
328
+ " print(\"VRAM allocated before train:\", round(torch.cuda.memory_allocated()/1e9, 2), \"GB\")\n",
329
+ "\n",
330
+ "train_loader, train_dataset = build_dataloader(cfg, dataset_name=\"mvtec\", shuffle=True)\n",
331
+ "print(\"train batch_size:\", train_loader.batch_size, \"| samples:\", len(train_dataset))\n",
332
+ "\n",
333
  "history = train_model(model, train_loader, optimizer, criterion, device, cfg)\n",
334
+ "plot_loss_curves(history)\n"
335
  ]
336
  },
337
  {
 
416
  },
417
  "nbformat": 4,
418
  "nbformat_minor": 5
419
+ }
scripts/train.py CHANGED
@@ -2,6 +2,7 @@ import os
2
  import sys
3
  import torch
4
  from torch.optim import Adam
 
5
 
6
  sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
7
  from models.model import OmniAL
@@ -18,35 +19,44 @@ def build_model_and_optimizer(cfg: dict, device: str = None):
18
  return model, optimizer, criterion, device
19
 
20
 
21
- def run_one_epoch(model, dataloader, optimizer, criterion, device, train: bool = True):
 
22
  model.train() if train else model.eval()
23
  epoch_losses = {"total": 0.0, "l2_img": 0.0, "ssim_img": 0.0,
24
  "l2_jnd": 0.0, "ssim_jnd": 0.0, "focal": 0.0}
25
  n_batches = 0
 
 
 
26
 
27
  context = torch.enable_grad() if train else torch.no_grad()
28
  with context:
29
  for batch in dataloader:
30
- anomaly_img = batch["anomaly_img"].to(device)
31
  targets = {
32
- "image": batch["image"].to(device),
33
- "jnd": batch["jnd"].to(device),
34
- "severity": batch["severity"].to(device),
35
  }
36
 
37
- outputs = model(anomaly_img)
38
- losses = criterion(outputs, targets)
 
39
 
40
  if train:
41
- optimizer.zero_grad()
42
- losses["total"].backward()
43
- optimizer.step()
 
44
 
45
  for k in epoch_losses:
46
  v = losses[k]
47
  epoch_losses[k] += v.item() if torch.is_tensor(v) else v
48
  n_batches += 1
49
 
 
 
 
50
  for k in epoch_losses:
51
  epoch_losses[k] /= max(n_batches, 1)
52
  return epoch_losses
@@ -56,19 +66,30 @@ def train_model(model, dataloader, optimizer, criterion, device, cfg: dict, val_
56
  epochs = cfg["train"]["epochs"]
57
  ckpt_dir = cfg["logging"]["ckpt_dir"]
58
  save_every = cfg["logging"]["save_every"]
 
59
  os.makedirs(ckpt_dir, exist_ok=True)
60
 
 
61
  history = {"train_total": []}
62
  if val_loader is not None:
63
  history["val_total"] = []
64
 
 
 
 
65
  for epoch in range(1, epochs + 1):
66
- train_losses = run_one_epoch(model, dataloader, optimizer, criterion, device, train=True)
 
 
 
67
  history["train_total"].append(train_losses["total"])
68
  msg = f"[Epoch {epoch}/{epochs}] train_total={train_losses['total']:.4f}"
69
 
70
  if val_loader is not None:
71
- val_losses = run_one_epoch(model, dataloader, optimizer, criterion, device, train=False)
 
 
 
72
  history["val_total"].append(val_losses["total"])
73
  msg += f" | val_total={val_losses['total']:.4f}"
74
 
@@ -79,4 +100,7 @@ def train_model(model, dataloader, optimizer, criterion, device, cfg: dict, val_
79
  torch.save(model.state_dict(), ckpt_path)
80
  print(f" -> checkpoint saved: {ckpt_path}")
81
 
 
 
 
82
  return history
 
2
  import sys
3
  import torch
4
  from torch.optim import Adam
5
+ from torch.cuda.amp import GradScaler, autocast
6
 
7
  sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
8
  from models.model import OmniAL
 
19
  return model, optimizer, criterion, device
20
 
21
 
22
+ def run_one_epoch(model, dataloader, optimizer, criterion, device, train: bool = True,
23
+ use_amp: bool = False, scaler: GradScaler = None):
24
  model.train() if train else model.eval()
25
  epoch_losses = {"total": 0.0, "l2_img": 0.0, "ssim_img": 0.0,
26
  "l2_jnd": 0.0, "ssim_jnd": 0.0, "focal": 0.0}
27
  n_batches = 0
28
+ use_amp = bool(use_amp and device != "cpu" and torch.cuda.is_available())
29
+ if scaler is None:
30
+ scaler = GradScaler(enabled=use_amp)
31
 
32
  context = torch.enable_grad() if train else torch.no_grad()
33
  with context:
34
  for batch in dataloader:
35
+ anomaly_img = batch["anomaly_img"].to(device, non_blocking=True)
36
  targets = {
37
+ "image": batch["image"].to(device, non_blocking=True),
38
+ "jnd": batch["jnd"].to(device, non_blocking=True),
39
+ "severity": batch["severity"].to(device, non_blocking=True),
40
  }
41
 
42
+ with autocast(enabled=use_amp):
43
+ outputs = model(anomaly_img)
44
+ losses = criterion(outputs, targets)
45
 
46
  if train:
47
+ optimizer.zero_grad(set_to_none=True)
48
+ scaler.scale(losses["total"]).backward()
49
+ scaler.step(optimizer)
50
+ scaler.update()
51
 
52
  for k in epoch_losses:
53
  v = losses[k]
54
  epoch_losses[k] += v.item() if torch.is_tensor(v) else v
55
  n_batches += 1
56
 
57
+ # drop refs so peak memory can reclaim between steps
58
+ del anomaly_img, targets, outputs, losses
59
+
60
  for k in epoch_losses:
61
  epoch_losses[k] /= max(n_batches, 1)
62
  return epoch_losses
 
66
  epochs = cfg["train"]["epochs"]
67
  ckpt_dir = cfg["logging"]["ckpt_dir"]
68
  save_every = cfg["logging"]["save_every"]
69
+ use_amp = bool(cfg["train"].get("use_amp", True))
70
  os.makedirs(ckpt_dir, exist_ok=True)
71
 
72
+ scaler = GradScaler(enabled=use_amp and device != "cpu" and torch.cuda.is_available())
73
  history = {"train_total": []}
74
  if val_loader is not None:
75
  history["val_total"] = []
76
 
77
+ print(f"Training: epochs={epochs}, amp={use_amp}, "
78
+ f"batch≈{getattr(dataloader, 'batch_size', '?')}, device={device}")
79
+
80
  for epoch in range(1, epochs + 1):
81
+ train_losses = run_one_epoch(
82
+ model, dataloader, optimizer, criterion, device,
83
+ train=True, use_amp=use_amp, scaler=scaler,
84
+ )
85
  history["train_total"].append(train_losses["total"])
86
  msg = f"[Epoch {epoch}/{epochs}] train_total={train_losses['total']:.4f}"
87
 
88
  if val_loader is not None:
89
+ val_losses = run_one_epoch(
90
+ model, val_loader, optimizer, criterion, device,
91
+ train=False, use_amp=use_amp, scaler=scaler,
92
+ )
93
  history["val_total"].append(val_losses["total"])
94
  msg += f" | val_total={val_losses['total']:.4f}"
95
 
 
100
  torch.save(model.state_dict(), ckpt_path)
101
  print(f" -> checkpoint saved: {ckpt_path}")
102
 
103
+ if torch.cuda.is_available():
104
+ torch.cuda.empty_cache()
105
+
106
  return history