abinazebinoy commited on
Commit
7873c43
Β·
1 Parent(s): 3015756

fix: return None for corrupt images, add collate_skip_none, remove RandomGrayscale/RandomAffine (audit #10)

Browse files
Files changed (1) hide show
  1. 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.15),
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
- # Return black image on failure β€” rare corrupt file
89
- blank = torch.zeros(3, 224, 224)
90
- return blank, torch.tensor(label, dtype=torch.float32)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)