#!/usr/bin/env python # coding: utf-8 # # πŸ” Deepfake Image Detection # **Binary Classifier β€” Real vs Fake** # Model: `InceptionResnetV1` (VGGFace2 pretrained backbone, fine-tuned classifier head) # Supports: GPU training, pause & resume via checkpoints, probabilistic inference # # --- # ### Notebook Structure # | Cell | Job | # |------|-----| # | 1 | Imports & GPU Setup | # | 2 | Configuration (all hyperparams in one place) | # | 3 | Data Loaders | # | 4 | Model Definition | # | 5 | Checkpoint Utilities (Pause & Resume) | # | 6 | Training Loop | # | 7 | Validation Loop | # | 8 | β–Ά Run Training | # | 9 | Plot Training Curves | # | 10 | πŸ”Ž Single Image Inference | # | 11 | πŸ’Ύ Save Final Model | # In[2]: # ───────────────────────────────────────────────────────── # CELL 1 β€” Imports & GPU Setup # ───────────────────────────────────────────────────────── import os import random import numpy as np import torch import torch.nn as nn from torch import optim from torch.optim.lr_scheduler import CosineAnnealingLR from torch.utils.data import DataLoader from torchvision import datasets, transforms from facenet_pytorch import InceptionResnetV1, fixed_image_standardization from PIL import Image import matplotlib.pyplot as plt import matplotlib.ticker as ticker import seaborn as sns from tqdm import tqdm # ── Reproducibility ── SEED = 42 random.seed(SEED) np.random.seed(SEED) torch.manual_seed(SEED) torch.backends.cudnn.deterministic = True # Reproducible GPU ops torch.backends.cudnn.benchmark = False # Set True for speed if input size is fixed # ── Device ── device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') print(f'[OK] PyTorch : {torch.__version__}') print(f'[OK] Device : {device}') if device.type == 'cuda': print(f' GPU Name : {torch.cuda.get_device_name(0)}') print(f' VRAM : {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB') else: print('[WARN] No GPU detected β€” training will run on CPU (slower)') # In[ ]: # ───────────────────────────────────────────────────────── # CELL 2 β€” Configuration # All hyperparameters and paths live here. Adjust as needed. # ───────────────────────────────────────────────────────── CONFIG = { # ── Dataset paths ── 'train_dir' : r'C:\Users\SHINJAN\Downloads\deepfake_dataset\real_vs_fake\real-vs-fake\train', 'valid_dir' : r'C:\Users\SHINJAN\Downloads\deepfake_dataset\real_vs_fake\real-vs-fake\valid', 'test_dir' : r'C:\Users\SHINJAN\Downloads\deepfake_dataset\real_vs_fake\real-vs-fake\test', # ── Extra fake sources (modern AI images: Midjourney, SD, DALL-E, Flux etc.) ── # Drop any folder of AI-generated face images here. # Each folder just needs .jpg/.png files (no subfolder structure required). # All images are labelled fake (class 0) automatically. # Leave as [] if you have no extra fakes yet. 'extra_fake_dirs' : [], # e.g. [r'C:\path\to\midjourney_faces', r'C:\path\to\sd_faces'] # ── Training ── 'epochs' : 15, 'batch_size' : 32, 'learning_rate' : 1e-4, 'freeze_backbone' : True, 'num_workers' : 0, # 0 on Windows; increase on Linux 'use_class_weights': True, # auto-balance loss when extra fakes skew the ratio # ── Checkpoint ── 'checkpoint_path' : 'checkpoint.pt', 'checkpoint_interval' : 50, # ── Output ── 'model_save_path' : 'deepfake_model_final.pt', 'image_size' : 299, 'num_classes' : 2, } print('[CONFIG] CONFIG loaded:') for k, v in CONFIG.items(): print(f' {k:<25} = {v}') # In[ ]: # ───────────────────────────────────────────────────────── # CELL 2b β€” HuggingFace Dataset Loader # # Loads: riandika/AI-vs-Deepfake-vs-Real-Resized-Aug (~17k images) # 3 source classes --> mapped to our binary labels: # AI-generated --> fake (0) # Deepfake --> fake (0) # Real --> real (1) # # Only the 'train' split is merged into training. # Set USE_HF_DATASET = False to skip this cell entirely. # ───────────────────────────────────────────────────────── USE_HF_DATASET = True # set False to skip HF_DATASET_ID = 'riandika/AI-vs-Deepfake-vs-Real-Resized-Aug' # Populated here; consumed by Cell 3 hf_train_data = None HF_LABEL_MAP = {} if USE_HF_DATASET: try: from datasets import load_dataset as hf_load_dataset except ImportError: import subprocess print('Installing huggingface datasets...') subprocess.check_call([__import__('sys').executable, '-m', 'pip', 'install', 'datasets', '-q']) from datasets import load_dataset as hf_load_dataset print(f'Downloading {HF_DATASET_ID} ...') print('(This may take a few minutes on first run; cached locally afterwards)') hf_ds = hf_load_dataset(HF_DATASET_ID) hf_train_data = hf_ds['train'] # Inspect label names feature = hf_train_data.features['label'] print(f'Label names in HF dataset: {feature.names}') print(f'Total HF train samples: {len(hf_train_data):,}') # Anything not 'real' (case-insensitive) -> 0 (fake) HF_LABEL_MAP = { idx: (1 if name.lower() == 'real' else 0) for idx, name in enumerate(feature.names) } print('Binary label mapping:') for src_idx, name in enumerate(feature.names): mapped = 'real (1)' if HF_LABEL_MAP[src_idx] == 1 else 'fake (0)' print(f' {name:<20} --> {mapped}') else: print('HF dataset skipped (USE_HF_DATASET = False)') # In[ ]: # ───────────────────────────────────────────────────────── # CELL 3 β€” Data Loaders # # Supports two modes: # 1. Standard: single ImageFolder train/valid/test split # 2. Multi-source: merges extra_fake_dirs (modern AI images) # into the training set, labelled as fake (class 0) # # Augmentations tuned to expose diffusion-model artifacts: # β€’ RandomJPEGCompression β€” real photos have JPEG noise; # diffusion images are suspiciously clean # β€’ GaussianBlur β€” catches over-smoothed diffusion outputs # β€’ RandomGrayscale β€” prevents colour-distribution overfit # ───────────────────────────────────────────────────────── import glob import io from torch.utils.data import Dataset, ConcatDataset # ─── Custom transform: simulate JPEG compression ─────────────────────────────────── class RandomJPEGCompression: """ Randomly re-encodes a PIL image as JPEG at a low quality. Real photos naturally have JPEG noise; diffusion/AI images often do not. This teaches the model to notice that difference. """ def __init__(self, quality_range=(40, 90), p=0.4): self.quality_range = quality_range self.p = p def __call__(self, img): if random.random() > self.p: return img quality = random.randint(*self.quality_range) buf = io.BytesIO() img.save(buf, format='JPEG', quality=quality) buf.seek(0) return Image.open(buf).convert('RGB') # ─── Custom Dataset: flat folder of images β†’ all labelled fake (0) ───────────────── class FlatFakeDataset(Dataset): """ Loads all .jpg/.jpeg/.png/.webp images from a folder and assigns them label 0 (fake). Use for Midjourney, Stable Diffusion, DALL-E, Flux, or any modern AI-generated image source. """ EXTS = ('*.jpg', '*.jpeg', '*.png', '*.webp') def __init__(self, folder: str, transform=None): self.transform = transform self.paths = [] for ext in self.EXTS: self.paths.extend(glob.glob(os.path.join(folder, '**', ext), recursive=True)) if not self.paths: print(f' \u26a0\ufe0f No images found in: {folder}') else: print(f' \U0001f4c2 Extra fakes {folder} \u2192 {len(self.paths):,} images') def __len__(self): return len(self.paths) def __getitem__(self, idx): img = Image.open(self.paths[idx]).convert('RGB') if self.transform: img = self.transform(img) return img, 0 # always fake # --- HuggingFace Dataset wrapper ------------------------------------------- class HuggingFaceWrapperDataset(Dataset): """ Wraps a HuggingFace dataset split as a PyTorch Dataset. label_map: dict mapping HF integer label -> binary label (0=fake, 1=real) """ def __init__(self, hf_dataset, label_map: dict, transform=None): self.data = hf_dataset self.label_map = label_map self.transform = transform def __len__(self): return len(self.data) def __getitem__(self, idx): row = self.data[idx] img = row['image'].convert('RGB') label = self.label_map[row['label']] if self.transform: img = self.transform(img) return img, label # ─── Augmentation pipelines ───────────────────────────────────────────────────── # Train: diffusion-aware augmentation train_transform = transforms.Compose([ transforms.Resize((CONFIG['image_size'], CONFIG['image_size'])), RandomJPEGCompression(quality_range=(40, 90), p=0.4), # diffusion-aware transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.15, contrast=0.15, saturation=0.1, hue=0.05), transforms.GaussianBlur(kernel_size=3, sigma=(0.1, 1.5)), # diffusion-aware transforms.RandomGrayscale(p=0.05), # prevents colour overfit transforms.ToTensor(), fixed_image_standardization, ]) # Val / Test: no augmentation eval_transform = transforms.Compose([ transforms.Resize((CONFIG['image_size'], CONFIG['image_size'])), transforms.ToTensor(), fixed_image_standardization, ]) # ─── Base datasets (ImageFolder) ──────────────────────────────────────────────────── train_dataset_base = datasets.ImageFolder(CONFIG['train_dir'], transform=train_transform) valid_dataset = datasets.ImageFolder(CONFIG['valid_dir'], transform=eval_transform) test_dataset = datasets.ImageFolder(CONFIG['test_dir'], transform=eval_transform) CLASS_NAMES = {v: k for k, v in train_dataset_base.class_to_idx.items()} # {0:'fake', 1:'real'} # ─── Merge extra fake sources ──────────────────────────────────────────────────────── extra_fake_count = 0 extra_datasets = [] if CONFIG['extra_fake_dirs']: print('\n[BOX] Loading extra fake image sources...') for folder in CONFIG['extra_fake_dirs']: ds = FlatFakeDataset(folder, transform=train_transform) if len(ds) > 0: extra_datasets.append(ds) extra_fake_count += len(ds) train_dataset = ConcatDataset([train_dataset_base] + extra_datasets) if extra_datasets else train_dataset_base # --- Merge HuggingFace dataset (if loaded in Cell 2b) --- hf_sample_count = 0 hf_fake_count = 0 hf_real_count = 0 if USE_HF_DATASET and hf_train_data is not None and HF_LABEL_MAP: hf_torch_ds = HuggingFaceWrapperDataset( hf_train_data, HF_LABEL_MAP, transform=train_transform ) hf_sample_count = len(hf_torch_ds) # [OK] FIX: Fetch 'label' column entirely. This avoids memory/speed issues associated with HuggingFace dataset dictionary fetching hf_labels = hf_train_data['label'] hf_fake_count = sum(1 for lbl in hf_labels if HF_LABEL_MAP[lbl] == 0) hf_real_count = hf_sample_count - hf_fake_count if isinstance(train_dataset, ConcatDataset): train_dataset = ConcatDataset(list(train_dataset.datasets) + [hf_torch_ds]) else: train_dataset = ConcatDataset([train_dataset, hf_torch_ds]) print(f'HF dataset merged: {hf_sample_count:,} samples (fake={hf_fake_count:,} real={hf_real_count:,})') else: hf_torch_ds = None # ─── Class weights (inverse-frequency) ──────────────────────────────────────────────── base_fake = sum(1 for _, lbl in train_dataset_base.samples if lbl == 0) base_real = sum(1 for _, lbl in train_dataset_base.samples if lbl == 1) # Include HF dataset counts in class weight calculation total_fake = base_fake + extra_fake_count + hf_fake_count total_real = base_real + hf_real_count total_all = total_fake + total_real class_weights = torch.tensor([ total_all / (2 * total_fake) if total_fake > 0 else 1.0, # weight for fake (0) total_all / (2 * total_real) if total_real > 0 else 1.0, # weight for real (1) ], dtype=torch.float).to(device) # ─── Data loaders ───────────────────────────────────────────────────────────────── train_loader = DataLoader(train_dataset, batch_size=CONFIG['batch_size'], shuffle=True, num_workers=CONFIG['num_workers'], pin_memory=True) valid_loader = DataLoader(valid_dataset, batch_size=CONFIG['batch_size'], shuffle=False, num_workers=CONFIG['num_workers'], pin_memory=True) test_loader = DataLoader(test_dataset, batch_size=CONFIG['batch_size'], shuffle=False, num_workers=CONFIG['num_workers'], pin_memory=True) # ─── Summary ────────────────────────────────────────────────────────────────────────── print(f'\n[DIR] Classes : {train_dataset_base.class_to_idx}') print(f'[STATS] Base train : {len(train_dataset_base):,} (fake={base_fake:,} real={base_real:,})') if extra_fake_count: print(f'[+] Extra fakes : {extra_fake_count:,}') if hf_sample_count: print(f'HF dataset : {hf_sample_count:,} (fake={hf_fake_count:,} real={hf_real_count:,})') print(f'[STATS] Total train : {len(train_dataset):,}') print(f'[STATS] Valid images : {len(valid_dataset):,}') print(f'[STATS] Test images : {len(test_dataset):,}') print(f'[WEIGHT] Class weights : fake={class_weights[0]:.3f} real={class_weights[1]:.3f}') print(f'[LOOP] Batches/epoch : {len(train_loader)}') # In[ ]: # ───────────────────────────────────────────────────────── # CELL 4 β€” Model Definition # InceptionResnetV1 pretrained on VGGFace2 # We replace the final logits layer to output 2 classes. # Optionally freeze the visual backbone and only train # the new classifier head (faster, less overfitting). # ───────────────────────────────────────────────────────── def build_model(num_classes: int, freeze_backbone: bool, device: torch.device) -> nn.Module: """ Build InceptionResnetV1 with a fresh classification head. Args: num_classes : Number of output classes (2 for Real/Fake) freeze_backbone: If True, backbone weights are frozen β€” only head trains device : Target compute device Returns: model on the specified device """ model = InceptionResnetV1( classify=True, pretrained='vggface2', num_classes=num_classes ).to(device) if freeze_backbone: # Freeze all layers … for param in model.parameters(): param.requires_grad = False # … then unfreeze only the final logits layer for param in model.logits.parameters(): param.requires_grad = True return model model = build_model( num_classes=CONFIG['num_classes'], freeze_backbone=CONFIG['freeze_backbone'], device=device ) # ── Summary ── trainable = sum(p.numel() for p in model.parameters() if p.requires_grad) total_ = sum(p.numel() for p in model.parameters()) frozen = total_ - trainable print(f'[MODEL] Model : InceptionResnetV1 (VGGFace2)') print(f' Total params : {total_:,}') print(f' Trainable params: {trainable:,} ← only these update') print(f' Frozen params : {frozen:,}') # ── Note on unfreezing backbone layers ── # If you added many extra fake images (>5000), consider training more of # the backbone so it can learn newer artifact patterns. # Change freeze_backbone=False in CONFIG to unfreeze everything, or # selectively unfreeze the last few blocks: # # for name, param in model.named_parameters(): # if any(b in name for b in ('block8', 'block7', 'logits')): # param.requires_grad = True # # More trainable params β†’ more capacity to learn modern artifacts, # but needs more data to avoid overfitting. # In[ ]: # ───────────────────────────────────────────────────────── # CELL 5 β€” Checkpoint Utilities (Pause & Resume) # # To PAUSE: interrupt the kernel. The last auto-save # (every CHECKPOINT_INTERVAL batches) is kept. # To RESUME: just re-run Cell 8. It detects the checkpoint # and picks up from the right epoch + batch. # ───────────────────────────────────────────────────────── def save_checkpoint(path, model, optimizer, scheduler, epoch, batch_idx, history): """Persist training state so we can resume later.""" torch.save({ 'epoch' : epoch, 'batch_idx' : batch_idx, 'model_state_dict' : model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'scheduler_state_dict': scheduler.state_dict(), 'history' : history, }, path) def load_checkpoint(path, model, optimizer, scheduler): """ Load training state from disk. Returns (start_epoch, start_batch, history). If no checkpoint exists, returns (0, 0, default_history). """ default_history = {'train_loss': [], 'val_loss': [], 'val_acc': []} if not os.path.exists(path): print('[INFO] No checkpoint found β€” starting fresh') return 0, 0, default_history ckpt = torch.load(path, map_location=device, weights_only=False) model.load_state_dict(ckpt['model_state_dict']) optimizer.load_state_dict(ckpt['optimizer_state_dict']) scheduler.load_state_dict(ckpt['scheduler_state_dict']) start_epoch = ckpt['epoch'] start_batch = ckpt['batch_idx'] history = ckpt.get('history', default_history) print(f'[OK] Checkpoint loaded β€” resuming at Epoch {start_epoch + 1}, Batch {start_batch}') return start_epoch, start_batch, history print('[OK] Checkpoint utilities ready (save_checkpoint / load_checkpoint)') # In[ ]: # ───────────────────────────────────────────────────────── # CELL 6 β€” Training Loop # # For each batch: # 1. Forward pass β†’ model predicts labels # 2. Loss β†’ CrossEntropyLoss measures how wrong # 3. Backward pass β†’ gradients tell model which way to adjust # 4. Optimizer stepβ†’ applies the adjustments (Adam) # 5. Checkpoint β†’ saved every N batches automatically # ───────────────────────────────────────────────────────── def train_one_epoch(model, loader, loss_fn, optimizer, scheduler, epoch, start_batch, checkpoint_path, history, checkpoint_interval, device): """ Train for one epoch. Returns: average training loss for this epoch. """ model.train() running_loss = 0.0 batches_done = 0 pbar = tqdm(enumerate(loader, start=1), total=len(loader), desc=f'Epoch {epoch+1} [train]', leave=True, ascii=True) for batch_idx, (images, labels) in pbar: # ── Skip already-trained batches when resuming ── if batch_idx <= start_batch: continue images = images.to(device, non_blocking=True) labels = labels.to(device, non_blocking=True) # ── Forward ── optimizer.zero_grad() outputs = model(images) # raw logits [batch, num_classes] # ── Loss (CrossEntropy = softmax + negative log likelihood) ── loss = loss_fn(outputs, labels) # ── Backward (compute gradients) ── loss.backward() # ── Optimizer step (update weights) ── optimizer.step() running_loss += loss.item() batches_done += 1 pbar.set_postfix({'loss': f'{loss.item():.4f}', 'lr' : f'{scheduler.get_last_lr()[0]:.2e}'}) # ── Auto-save checkpoint every N batches ── if batch_idx % checkpoint_interval == 0: save_checkpoint(checkpoint_path, model, optimizer, scheduler, epoch, batch_idx, history) avg_loss = running_loss / max(batches_done, 1) return avg_loss print('[OK] Training loop function defined') # In[ ]: # ───────────────────────────────────────────────────────── # CELL 7 β€” Validation Loop # # No gradient computation here β€” pure inference. # Returns: average val loss, accuracy % # ───────────────────────────────────────────────────────── def validate(model, loader, loss_fn, device, split_name='val'): """ Evaluate model on a data loader. Returns: (avg_loss, accuracy_percent) """ model.eval() running_loss = 0.0 correct = 0 total = 0 with torch.no_grad(): # No gradients β†’ saves GPU memory + faster pbar = tqdm(loader, desc=f' [{split_name}]', leave=False, ascii=True) for images, labels in pbar: images = images.to(device, non_blocking=True) labels = labels.to(device, non_blocking=True) outputs = model(images) loss = loss_fn(outputs, labels) running_loss += loss.item() # ── Prediction = class with highest logit ── _, predicted = torch.max(outputs, dim=1) correct += (predicted == labels).sum().item() total += labels.size(0) avg_loss = running_loss / len(loader) accuracy = 100.0 * correct / total return avg_loss, accuracy print('[OK] Validation loop function defined') # In[ ]: # ───────────────────────────────────────────────────────── # CELL 8 β€” β–Ά Run Training # # This cell wires everything together: # - Builds optimizer + scheduler + loss function # - Loads checkpoint if one exists (RESUME) # - Runs the train β†’ validate loop for each epoch # - Saves checkpoint at end of each epoch too # # To PAUSE: Kernel β†’ Interrupt (Esc + I + I) # To RESUME: re-run this cell β€” it picks up automatically # ───────────────────────────────────────────────────────── # ── Optimizer: Adam (adaptive learning rate per parameter) ── optimizer = optim.Adam( filter(lambda p: p.requires_grad, model.parameters()), lr=CONFIG['learning_rate'], weight_decay=1e-4 ) # ── Scheduler: Cosine decay β€” smoothly lowers LR as training progresses ── scheduler = CosineAnnealingLR(optimizer, T_max=CONFIG['epochs'], eta_min=1e-6) # ── Loss: CrossEntropyLoss (softmax + NLL) ── # Use class weights if configured (handles imbalance when extra fakes are added) if CONFIG['use_class_weights']: loss_fn = nn.CrossEntropyLoss(weight=class_weights) print(f'[WEIGHT] Weighted loss: fake={class_weights[0]:.3f} real={class_weights[1]:.3f}') else: loss_fn = nn.CrossEntropyLoss() print('[WEIGHT] Unweighted loss') # ── Load checkpoint (resume if exists, else start fresh) ── start_epoch, start_batch, history = load_checkpoint( CONFIG['checkpoint_path'], model, optimizer, scheduler ) print(f'\n[START] Starting training for {CONFIG["epochs"]} epochs\n') # ── Main training loop ── for epoch in range(start_epoch, CONFIG['epochs']): print(f'\n══════════ Epoch {epoch + 1}/{CONFIG["epochs"]} ══════════') # Train train_loss = train_one_epoch( model, train_loader, loss_fn, optimizer, scheduler, epoch, start_batch, CONFIG['checkpoint_path'], history, CONFIG['checkpoint_interval'], device ) # After the first resumed epoch, start_batch resets for subsequent epochs start_batch = 0 # Validate val_loss, val_acc = validate(model, valid_loader, loss_fn, device) # Step the LR scheduler once per epoch scheduler.step() # Record history history['train_loss'].append(train_loss) history['val_loss'].append(val_loss) history['val_acc'].append(val_acc) print(f'\nπŸ“‰ Train Loss : {train_loss:.4f}') print(f'πŸ“‰ Val Loss : {val_loss:.4f}') print(f'[TARGET] Val Acc : {val_acc:.2f}%') print(f'[BOOKS] LR : {scheduler.get_last_lr()[0]:.2e}') # Save checkpoint at end of every epoch save_checkpoint(CONFIG['checkpoint_path'], model, optimizer, scheduler, epoch + 1, 0, history) print('\n[OK] Training complete!') # In[ ]: sns.set_theme(style='darkgrid') epochs_ran = list(range(1, len(history['train_loss']) + 1)) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5)) fig.suptitle('Deepfake Detection β€” Training Progress', fontsize=15, fontweight='bold') # ── Loss plot ── ax1.plot(epochs_ran, history['train_loss'], marker='o', label='Train Loss', color='#E74C3C') ax1.plot(epochs_ran, history['val_loss'], marker='s', label='Val Loss', color='#3498DB') ax1.set_title('Loss over Epochs') ax1.set_xlabel('Epoch') ax1.set_ylabel('Cross-Entropy Loss') ax1.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) ax1.legend() # ── Accuracy plot ── ax2.plot(epochs_ran, history['val_acc'], marker='^', color='#2ECC71', label='Val Accuracy') ax2.set_title('Validation Accuracy over Epochs') ax2.set_xlabel('Epoch') ax2.set_ylabel('Accuracy (%)') ax2.set_ylim(0, 100) ax2.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) ax2.legend() plt.tight_layout() plt.savefig('training_curves.png', dpi=150, bbox_inches='tight') plt.show() print('[STATS] Plot saved as training_curves.png') # In[ ]: import os import torchvision.transforms as transforms from facenet_pytorch import fixed_image_standardization # ─── Self-Contained Inference Dependencies ─── eval_transform = transforms.Compose([ transforms.Resize((299, 299)), transforms.ToTensor(), fixed_image_standardization, ]) # Dynamically fetch class names if possible, else default try: CLASS_NAMES = {v: k for k, v in train_dataset_base.class_to_idx.items()} except NameError: CLASS_NAMES = {0: 'fake', 1: 'real'} def predict_image(image_path: str, model: nn.Module, device: torch.device) -> dict: img = Image.open(image_path).convert('RGB') tensor = eval_transform(img).unsqueeze(0).to(device) # [1, 3, 299, 299] model.eval() with torch.no_grad(): logits = model(tensor) probs = torch.softmax(logits, dim=1)[0] class_probs = {CLASS_NAMES[i]: round(probs[i].item(), 4) for i in range(len(CLASS_NAMES))} predicted_idx = probs.argmax().item() return { 'label' : CLASS_NAMES[predicted_idx].upper(), 'confidence' : round(probs[predicted_idx].item(), 4), 'probabilities': class_probs, } TEST_IMAGE_PATH = r'C:\Users\SHINJAN\Downloads\deepfake_dataset\real_vs_fake\real-vs-fake\test\fake\00000.jpg' if os.path.exists(TEST_IMAGE_PATH): result = predict_image(TEST_IMAGE_PATH, model, device) print(f'\n[SEARCH] Image : {os.path.basename(TEST_IMAGE_PATH)}') print(f' Verdict : {result["label"]}') print(f' Confidence : {result["confidence"]*100:.1f}%') print(f' All probs : {result["probabilities"]}') img_display = Image.open(TEST_IMAGE_PATH) color = '#2ECC71' if result['label'] == 'REAL' else '#E74C3C' plt.figure(figsize=(5, 5)) plt.imshow(img_display) plt.title(f'{result["label"]} ({result["confidence"]*100:.1f}% confident)', fontsize=14, fontweight='bold', color=color) plt.axis('off') plt.tight_layout() plt.show() else: print(f'\n[WARN] Test image {TEST_IMAGE_PATH} not found. Change TEST_IMAGE_PATH to test.') # In[ ]: # ───────────────────────────────────────────────────────── # CELL 11 β€” [SAVE] Save Final Model # # Saves the trained model weights to disk. # Use this after training is fully done. # ───────────────────────────────────────────────────────── os.makedirs('models', exist_ok=True) final_path = os.path.join('models', CONFIG['model_save_path']) torch.save(model.state_dict(), final_path) print(f'[SAVE] Final model saved β†’ {final_path}') print(f' To reload later: model.load_state_dict(torch.load("{final_path}", map_location=device))')