Spaces:
Running
Running
Commit Β·
7873c43
1
Parent(s): 3015756
fix: return None for corrupt images, add collate_skip_none, remove RandomGrayscale/RandomAffine (audit #10)
Browse files- scripts/train_embedding.py +36 -6
scripts/train_embedding.py
CHANGED
|
@@ -41,15 +41,21 @@ ROOT = Path(__file__).parents[1]
|
|
| 41 |
MANIFEST_PATH = ROOT / "data" / "manifest.csv"
|
| 42 |
|
| 43 |
# ββ Image transform ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
TRAIN_TRANSFORM = transforms.Compose([
|
| 45 |
transforms.Resize((224, 224)),
|
| 46 |
transforms.RandomHorizontalFlip(),
|
| 47 |
-
transforms.RandomApply([transforms.GaussianBlur(3)], p=0.
|
| 48 |
transforms.RandomApply([
|
| 49 |
transforms.ColorJitter(brightness=0.4, contrast=0.4, saturation=0.3, hue=0.1)
|
| 50 |
], p=0.5),
|
| 51 |
-
transforms.RandomGrayscale(p=0.1),
|
| 52 |
-
transforms.RandomApply([transforms.RandomAffine(degrees=10, translate=(0.05, 0.05))], p=0.2),
|
| 53 |
transforms.ToTensor(),
|
| 54 |
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
|
| 55 |
])
|
|
@@ -85,9 +91,27 @@ class ImageManifestDataset(Dataset):
|
|
| 85 |
img = Image.open(img_path).convert("RGB")
|
| 86 |
return self.transform(img), torch.tensor(label, dtype=torch.float32)
|
| 87 |
except Exception:
|
| 88 |
-
#
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
|
| 93 |
# ββ Load manifest ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -161,10 +185,12 @@ def train(args):
|
|
| 161 |
train_loader = DataLoader(
|
| 162 |
train_ds, batch_size=args.batch,
|
| 163 |
shuffle=True, num_workers=0, pin_memory=False,
|
|
|
|
| 164 |
)
|
| 165 |
val_loader = DataLoader(
|
| 166 |
val_ds, batch_size=args.batch,
|
| 167 |
shuffle=False, num_workers=0,
|
|
|
|
| 168 |
)
|
| 169 |
|
| 170 |
logger.info(f"Train batches: {len(train_loader)} "
|
|
@@ -193,6 +219,8 @@ def train(args):
|
|
| 193 |
t0 = time.time()
|
| 194 |
|
| 195 |
for batch_idx, (images, labels) in enumerate(train_loader):
|
|
|
|
|
|
|
| 196 |
images = images.to(device)
|
| 197 |
labels = labels.to(device).unsqueeze(1)
|
| 198 |
|
|
@@ -223,6 +251,8 @@ def train(args):
|
|
| 223 |
|
| 224 |
with torch.no_grad():
|
| 225 |
for images, labels in val_loader:
|
|
|
|
|
|
|
| 226 |
images = images.to(device)
|
| 227 |
labels = labels.to(device).unsqueeze(1)
|
| 228 |
_, probs = model(images)
|
|
|
|
| 41 |
MANIFEST_PATH = ROOT / "data" / "manifest.csv"
|
| 42 |
|
| 43 |
# ββ Image transform ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 44 |
+
# Forensic-safe augmentations (audit fix #22):
|
| 45 |
+
# - RandomGrayscale REMOVED β discards chroma-channel noise that is itself an
|
| 46 |
+
# AI-generation signal.
|
| 47 |
+
# - RandomAffine REMOVED β destroys CFA pixel-grid alignment and compression
|
| 48 |
+
# artifacts that several detectors rely on.
|
| 49 |
+
# - GaussianBlur probability reduced from 0.15 to 0.03 β blurring removes the
|
| 50 |
+
# high-frequency noise/PRNU texture the model needs to distinguish real camera
|
| 51 |
+
# images from synthetic ones.
|
| 52 |
TRAIN_TRANSFORM = transforms.Compose([
|
| 53 |
transforms.Resize((224, 224)),
|
| 54 |
transforms.RandomHorizontalFlip(),
|
| 55 |
+
transforms.RandomApply([transforms.GaussianBlur(3)], p=0.03),
|
| 56 |
transforms.RandomApply([
|
| 57 |
transforms.ColorJitter(brightness=0.4, contrast=0.4, saturation=0.3, hue=0.1)
|
| 58 |
], p=0.5),
|
|
|
|
|
|
|
| 59 |
transforms.ToTensor(),
|
| 60 |
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
|
| 61 |
])
|
|
|
|
| 91 |
img = Image.open(img_path).convert("RGB")
|
| 92 |
return self.transform(img), torch.tensor(label, dtype=torch.float32)
|
| 93 |
except Exception:
|
| 94 |
+
# Skip corrupt/unreadable files entirely instead of substituting a
|
| 95 |
+
# black image. A black tensor with the original label teaches the
|
| 96 |
+
# network that pure-black input can belong to either class, adding
|
| 97 |
+
# noise to every batch that happens to contain a corrupt file.
|
| 98 |
+
# collate_skip_none (below) filters out these None returns.
|
| 99 |
+
logger.warning("Skipping corrupt/unreadable image: %s", img_path)
|
| 100 |
+
return None
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def collate_skip_none(batch):
|
| 104 |
+
"""
|
| 105 |
+
DataLoader collate_fn that filters out None entries produced by
|
| 106 |
+
ImageManifestDataset.__getitem__ for corrupt/unreadable files.
|
| 107 |
+
Without this, a single corrupt file crashes the entire epoch.
|
| 108 |
+
"""
|
| 109 |
+
batch = [item for item in batch if item is not None]
|
| 110 |
+
if not batch:
|
| 111 |
+
# Extremely rare (entire batch of corrupt files) β return empty tensors
|
| 112 |
+
# so the training loop's skip-on-empty-batch guard handles it cleanly.
|
| 113 |
+
return torch.zeros(0, 3, 224, 224), torch.zeros(0)
|
| 114 |
+
return torch.utils.data.default_collate(batch)
|
| 115 |
|
| 116 |
|
| 117 |
# ββ Load manifest ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 185 |
train_loader = DataLoader(
|
| 186 |
train_ds, batch_size=args.batch,
|
| 187 |
shuffle=True, num_workers=0, pin_memory=False,
|
| 188 |
+
collate_fn=collate_skip_none,
|
| 189 |
)
|
| 190 |
val_loader = DataLoader(
|
| 191 |
val_ds, batch_size=args.batch,
|
| 192 |
shuffle=False, num_workers=0,
|
| 193 |
+
collate_fn=collate_skip_none,
|
| 194 |
)
|
| 195 |
|
| 196 |
logger.info(f"Train batches: {len(train_loader)} "
|
|
|
|
| 219 |
t0 = time.time()
|
| 220 |
|
| 221 |
for batch_idx, (images, labels) in enumerate(train_loader):
|
| 222 |
+
if images.size(0) == 0:
|
| 223 |
+
continue # all-corrupt batch β skip
|
| 224 |
images = images.to(device)
|
| 225 |
labels = labels.to(device).unsqueeze(1)
|
| 226 |
|
|
|
|
| 251 |
|
| 252 |
with torch.no_grad():
|
| 253 |
for images, labels in val_loader:
|
| 254 |
+
if images.size(0) == 0:
|
| 255 |
+
continue
|
| 256 |
images = images.to(device)
|
| 257 |
labels = labels.to(device).unsqueeze(1)
|
| 258 |
_, probs = model(images)
|