deepshield-api / backend /train_image.py
Venkatkalyan21
Deploy clean backend to Hugging Face
1970a5b
Raw
History Blame Contribute Delete
11.6 kB
"""
train_image.py β€” Training script for EfficientNetV2-S image deepfake detector
Uses MTCNN-preprocessed face crops for training.
Dataset structure:
data/images/
train/
real/ ← real face crops (JPG/PNG)
fake/ ← deepfake face crops (JPG/PNG)
val/
real/
fake/
Compatible datasets:
- FaceForensics++ (face crops from real/manipulated videos)
- Celeb-DF v2 (face crops)
- DFDC face crops
- Any real/fake image folder pair
Usage:
python train_image.py \
--data_dir ../data/images \
--epochs 30 \
--batch_size 32 \
--lr 1e-4 \
--save_dir ../models
"""
import argparse, os, time, random
from pathlib import Path
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from torch.optim import AdamW
from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts
from sklearn.metrics import roc_auc_score, accuracy_score
from PIL import Image
from image_detector import EfficientNetV2Detector, train_transform, inference_transform
# ─────────────────────────────────────────────────────────────────
SEED = 42
IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"}
def seed_everything(seed: int = SEED):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
# ─────────────────────────────────────────────────────────────────
# Dataset
# ─────────────────────────────────────────────────────────────────
class ImageFakeDataset(Dataset):
"""
Folder-based image dataset.
Expects: <root>/<split>/real/ and <root>/<split>/fake/
"""
def __init__(self, root: str, split: str, augment: bool = False):
self.transform = train_transform if augment else inference_transform
self.samples = []
for label, name in [(0, "real"), (1, "fake")]:
folder = Path(root) / split / name
if not folder.exists():
print(f"[WARN] Folder not found: {folder}")
continue
for f in folder.rglob("*"):
if f.suffix.lower() in IMG_EXTS:
self.samples.append((str(f), label))
random.shuffle(self.samples)
real_n = sum(1 for _, l in self.samples if l == 0)
fake_n = sum(1 for _, l in self.samples if l == 1)
print(f"[Dataset/{split}] real={real_n} fake={fake_n} total={len(self.samples)}")
def __len__(self):
return len(self.samples)
def __getitem__(self, idx):
path, label = self.samples[idx]
try:
img = Image.open(path).convert("RGB")
except Exception:
img = Image.new("RGB", (224, 224), (128, 128, 128))
tensor = self.transform(img)
return tensor, torch.tensor(label, dtype=torch.float32)
# ─────────────────────────────────────────────────────────────────
# Training helpers
# ─────────────────────────────────────────────────────────────────
def mixup(x, y, alpha=0.2):
"""MixUp augmentation for better generalisation."""
if alpha > 0:
lam = np.random.beta(alpha, alpha)
else:
lam = 1.0
idx = torch.randperm(x.size(0))
mixed = lam * x + (1 - lam) * x[idx]
y_mix = lam * y + (1 - lam) * y[idx]
return mixed, y_mix
from tqdm import tqdm
def run_epoch(model, loader, criterion, optimizer, device, training: bool, use_mixup: bool = False):
model.train(training)
total_loss, all_probs, all_labels = 0.0, [], []
desc = "Train" if training else "Val"
pbar = tqdm(loader, desc=desc, leave=False, dynamic_ncols=True)
for imgs, labels in pbar:
imgs = imgs.to(device)
labels = labels.to(device)
if training and use_mixup:
imgs, labels = mixup(imgs, labels, alpha=0.2)
with torch.set_grad_enabled(training):
logits = model(imgs).squeeze(1)
loss = criterion(logits, labels)
if training:
optimizer.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
total_loss += loss.item() * len(labels)
probs = torch.sigmoid(logits).detach().cpu().numpy()
all_probs.extend(probs.tolist())
all_labels.extend(labels.cpu().numpy().tolist())
# Update progress bar
pbar.set_postfix(loss=f"{(total_loss / max(len(all_labels), 1)):.4f}")
avg_loss = total_loss / max(len(loader.dataset), 1)
preds = [1 if p >= 0.5 else 0 for p in all_probs]
int_labels = [round(l) for l in all_labels]
acc = accuracy_score(int_labels, preds)
try:
auc = roc_auc_score(int_labels, all_probs)
except Exception:
auc = 0.5
return avg_loss, acc, auc
# ─────────────────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────────────────
def main(args):
seed_everything()
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"[Train] Device: {device} | Data: {args.data_dir}")
# ── Datasets ──────────────────────────────────────────────
train_ds = ImageFakeDataset(args.data_dir, "train", augment=True)
val_ds = ImageFakeDataset(args.data_dir, "val", augment=False)
if len(train_ds) == 0:
print("\n[WARN] No training images found!")
print("Expected structure:")
print(" data/images/train/real/*.jpg")
print(" data/images/train/fake/*.jpg")
print(" data/images/val/real/*.jpg")
print(" data/images/val/fake/*.jpg")
print("\nYou can generate face crops from FaceForensics++ using preprocess_celebdf.py")
print("Running in demo mode (no actual training).")
return
train_loader = DataLoader(
train_ds, batch_size=args.batch_size, shuffle=True,
num_workers=4, pin_memory=(device == "cuda"), drop_last=True,
)
val_loader = DataLoader(
val_ds, batch_size=args.batch_size, shuffle=False, num_workers=4,
)
# ── Model ─────────────────────────────────────────────────
model = EfficientNetV2Detector(pretrained=True)
# Freeze backbone, only train classifier head initially
for param in model.backbone.parameters():
param.requires_grad = False
model.to(device)
# ── Loss, optimiser, scheduler ────────────────────────────
real_n = sum(1 for _, l in train_ds.samples if l == 0)
fake_n = sum(1 for _, l in train_ds.samples if l == 1)
pos_weight = torch.tensor([real_n / max(fake_n, 1)], device=device)
criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)
optimizer = AdamW(
filter(lambda p: p.requires_grad, model.parameters()),
lr=args.lr, weight_decay=1e-4,
)
scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=1)
# ── Training loop β€” Phase 1 (frozen backbone) ─────────────
save_dir = Path(args.save_dir)
save_dir.mkdir(parents=True, exist_ok=True)
best_auc = 0.0
print("\n" + "=" * 65)
print(" DeepShield β€” Image Detector Training (EfficientNetV2-S)")
print("=" * 65)
print(f" Phase 1: {args.epochs} epochs with frozen backbone")
for epoch in range(1, args.epochs + 1):
t0 = time.time()
tr_loss, tr_acc, tr_auc = run_epoch(model, train_loader, criterion, optimizer, device, True, use_mixup=True)
va_loss, va_acc, va_auc = run_epoch(model, val_loader, criterion, None, device, False, use_mixup=False)
scheduler.step()
elapsed = time.time() - t0
print(
f" Epoch {epoch:03d}/{args.epochs} "
f"| train loss={tr_loss:.4f} acc={tr_acc:.3f} AUC={tr_auc:.3f}"
f" | val loss={va_loss:.4f} acc={va_acc:.3f} AUC={va_auc:.3f}"
f" | {elapsed:.1f}s"
)
if va_auc > best_auc:
best_auc = va_auc
ckpt = save_dir / "image_model_best.pth"
torch.save(model.state_dict(), ckpt)
print(f" βœ” Best model saved β†’ {ckpt} (AUC={best_auc:.4f})")
# ── Phase 2: Unfreeze + fine-tune whole network ───────────
if args.finetune_epochs > 0:
print(f"\n Phase 2: Unfreezing backbone for {args.finetune_epochs} epochs...")
for param in model.backbone.parameters():
param.requires_grad = True
optimizer2 = AdamW(model.parameters(), lr=args.lr * 0.1, weight_decay=1e-4)
scheduler2 = CosineAnnealingWarmRestarts(optimizer2, T_0=args.finetune_epochs)
for epoch in range(1, args.finetune_epochs + 1):
t0 = time.time()
tr_loss, tr_acc, tr_auc = run_epoch(model, train_loader, criterion, optimizer2, device, True)
va_loss, va_acc, va_auc = run_epoch(model, val_loader, criterion, None, device, False)
scheduler2.step()
elapsed = time.time() - t0
print(
f" [P2] Epoch {epoch:03d}/{args.finetune_epochs} "
f"| val AUC={va_auc:.3f} acc={va_acc:.3f} | {elapsed:.1f}s"
)
if va_auc > best_auc:
best_auc = va_auc
ckpt = save_dir / "image_model_best.pth"
torch.save(model.state_dict(), ckpt)
print(f" βœ” Best model updated β†’ {ckpt} (AUC={best_auc:.4f})")
torch.save(model.state_dict(), save_dir / "image_model_final.pth")
print(f"\n[Done] Final model saved β†’ {save_dir / 'image_model_final.pth'}")
print(f"[Done] Best val AUC: {best_auc:.4f}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="DeepShield Image Deepfake Detector Training")
parser.add_argument("--data_dir", type=str, default="../datasets/images")
parser.add_argument("--epochs", type=int, default=30,
help="Phase-1 epochs (frozen backbone)")
parser.add_argument("--finetune_epochs", type=int, default=10,
help="Phase-2 epochs (full fine-tune)")
parser.add_argument("--batch_size", type=int, default=32)
parser.add_argument("--lr", type=float, default=1e-4)
parser.add_argument("--save_dir", type=str, default="../models")
args = parser.parse_args()
main(args)