Commit ·
290ed08
1
Parent(s): 255c417
oom fix
Browse files- config/config.yaml +5 -3
- notebooks/exploratory.ipynb +55 -27
- scripts/train.py +36 -12
config/config.yaml
CHANGED
|
@@ -59,12 +59,14 @@ model:
|
|
| 59 |
anomaly_smooth_sigma: 1.5
|
| 60 |
|
| 61 |
train:
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
| 64 |
optimizer: "adam"
|
| 65 |
lr: 0.0001
|
| 66 |
epochs: 100
|
| 67 |
-
num_workers:
|
| 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 |
-
"#
|
| 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 |
-
"
|
| 233 |
-
"if desc.shape[-2:]
|
| 234 |
-
"
|
| 235 |
-
"
|
| 236 |
-
"
|
| 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 |
-
"#
|
|
|
|
|
|
|
|
|
|
| 252 |
"model, optimizer, criterion, device = build_model_and_optimizer(cfg)\n",
|
| 253 |
-
"
|
| 254 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
|
|
|
| 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 |
-
"
|
| 283 |
-
"
|
| 284 |
-
"losses"
|
|
|
|
| 285 |
]
|
| 286 |
},
|
| 287 |
{
|
|
@@ -290,7 +305,8 @@
|
|
| 290 |
"metadata": {},
|
| 291 |
"source": [
|
| 292 |
"## 6. آموزش مدل (Training Loop)\n",
|
| 293 |
-
"برای تست سریع
|
|
|
|
| 294 |
]
|
| 295 |
},
|
| 296 |
{
|
|
@@ -300,10 +316,22 @@
|
|
| 300 |
"metadata": {},
|
| 301 |
"outputs": [],
|
| 302 |
"source": [
|
| 303 |
-
"#
|
|
|
|
| 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 |
-
|
| 38 |
-
|
|
|
|
| 39 |
|
| 40 |
if train:
|
| 41 |
-
optimizer.zero_grad()
|
| 42 |
-
losses["total"].backward()
|
| 43 |
-
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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
|