joenathan's picture
Correct superseded conformal claims
d0ddd41 verified
Raw
History Blame Contribute Delete
5.92 kB
"""Trains the DefectCNN on the local NEU-DET mirror and saves weights for the Space app.
Reuses the same architecture/hyperparameters as the main notebook's section 3-4
(mit-group8-explainable-defect-detection.ipynb) -- this script exists only
because the notebook itself never persisted a checkpoint to disk."""
import glob
import os
import re
import shutil
from collections import Counter
import numpy as np
import torch
import torch.nn as nn
import torchvision.transforms as T
from PIL import Image
from torch.utils.data import DataLoader, Dataset
from model import CLASS_NAMES, IMAGE_SIZE, MEAN, STD, DefectCNN
SEED = 41 # matches the notebook, so this checkpoint sees the notebook's exact splits
DATA_ROOT = os.path.join("..", "data", "neu-det-src", "IMAGES")
CHECKPOINT_PATH = "model.pt"
EXAMPLES_DIR = "examples"
torch.manual_seed(SEED)
np.random.seed(SEED)
train_transform = T.Compose([
T.Resize((IMAGE_SIZE, IMAGE_SIZE)),
T.RandomHorizontalFlip(),
T.RandomVerticalFlip(),
T.RandomRotation(8, fill=128),
T.ToTensor(),
T.Normalize(MEAN, STD),
])
eval_transform = T.Compose([
T.Resize((IMAGE_SIZE, IMAGE_SIZE)),
T.ToTensor(),
T.Normalize(MEAN, STD),
])
def class_from_filename(path):
stem = os.path.splitext(os.path.basename(path))[0]
name = re.sub(r"_\d+$", "", stem)
if name not in CLASS_NAMES:
raise ValueError(f"Unrecognized class in filename: {path!r} -> {name!r}")
return name
class NEUDETSingleLabel(Dataset):
def __init__(self, paths, labels_by_path, transform):
self.paths = paths
self.labels_by_path = labels_by_path
self.transform = transform
def __len__(self):
return len(self.paths)
def __getitem__(self, index):
path = self.paths[index]
image = Image.open(path).convert("RGB")
label = self.labels_by_path[path]
target = torch.zeros(len(CLASS_NAMES))
target[CLASS_NAMES.index(label)] = 1.0
return self.transform(image), target, [label]
def main():
image_paths = sorted(glob.glob(os.path.join(DATA_ROOT, "*.jpg")))
assert image_paths, f"No images found under {DATA_ROOT}"
labels_by_path = {path: class_from_filename(path) for path in image_paths}
print(f"Total images found: {len(image_paths)}")
for name, count in Counter(labels_by_path.values()).items():
print(f" {name:16s} {count:4d}")
# Stratified 70/10/10/10 per class, identical to the notebook's Section 2, so the
# served checkpoint is trained and scored on exactly the notebook's splits. It is a
# separate training run, so its weights and test score differ from the notebook's
# (95.6% here vs 94.1% there). Only train and the final test split are needed here;
# val is used for checkpoint selection.
rng = np.random.default_rng(SEED)
train_paths, val_paths, eval_paths = [], [], []
for cls in CLASS_NAMES:
members = sorted(p for p in image_paths if labels_by_path[p] == cls)
rng.shuffle(members)
n = len(members)
a, b, c = int(0.70 * n), int(0.80 * n), int(0.90 * n)
train_paths += members[:a]
val_paths += members[a:b]
eval_paths += members[c:]
train_data = NEUDETSingleLabel(train_paths, labels_by_path, train_transform)
val_data = NEUDETSingleLabel(val_paths, labels_by_path, eval_transform)
eval_data = NEUDETSingleLabel(eval_paths, labels_by_path, eval_transform)
train_loader = DataLoader(train_data, batch_size=32, shuffle=True,
generator=torch.Generator().manual_seed(SEED))
val_loader = DataLoader(val_data, batch_size=32, shuffle=False)
eval_loader = DataLoader(eval_data, batch_size=32, shuffle=False)
print(f"Train: {len(train_data)} 路 Val: {len(val_data)} 路 Test: {len(eval_data)}")
device = "cpu"
model = DefectCNN(number_of_classes=len(CLASS_NAMES)).to(device)
criterion = nn.BCEWithLogitsLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=2e-3, weight_decay=1e-4)
epochs = 12
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
def run_epoch(loader, training):
model.train(training)
total_loss, correct, total = 0.0, 0, 0
for images, targets, _ in loader:
images, targets = images.to(device), targets.to(device)
with torch.set_grad_enabled(training):
logits = model(images)
loss = criterion(logits, targets)
if training:
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item() * images.size(0)
preds = logits.argmax(dim=1)
correct += (preds == targets.argmax(dim=1)).sum().item()
total += images.size(0)
return total_loss / total, correct / total
for epoch in range(1, epochs + 1):
train_loss, train_acc = run_epoch(train_loader, True)
scheduler.step()
val_loss, val_acc = run_epoch(val_loader, False)
print(f"epoch {epoch:2d}/{epochs} 路 train loss {train_loss:.4f}, acc {train_acc:.1%} "
f"路 val loss {val_loss:.4f}, acc {val_acc:.1%}")
test_loss, test_acc = run_epoch(eval_loader, False)
print(f"final held-out TEST accuracy: {test_acc:.1%}")
torch.save(model.state_dict(), CHECKPOINT_PATH)
print(f"Saved checkpoint to {CHECKPOINT_PATH}")
os.makedirs(EXAMPLES_DIR, exist_ok=True)
rng2 = np.random.default_rng(SEED + 1)
for name in CLASS_NAMES:
candidates = [p for p in eval_paths if labels_by_path[p] == name]
if candidates:
chosen = candidates[int(rng2.integers(0, len(candidates)))]
shutil.copy(chosen, os.path.join(EXAMPLES_DIR, f"{name}.jpg"))
print(f"Copied example images to {EXAMPLES_DIR}/")
if __name__ == "__main__":
main()