| """
|
| Diagnose misclassification issue by analyzing predicted probabilities,
|
| finding optimal threshold, and showing false positive examples.
|
| """
|
| import joblib
|
| import numpy as np
|
| import torch
|
| from pathlib import Path
|
| from sklearn.metrics import accuracy_score, roc_auc_score, classification_report, confusion_matrix, roc_curve
|
| from torch.utils.data import DataLoader
|
| from torchvision import transforms
|
| import torchvision.transforms.functional as TF
|
| import matplotlib.pyplot as plt
|
|
|
| from train import DeepfakeFeatureFusion, ImageDataset
|
|
|
|
|
| def pad_to_min_size(img, size):
|
| w, h = img.size
|
| pad_w = max(0, size - w)
|
| pad_h = max(0, size - h)
|
| if pad_w or pad_h:
|
| left = pad_w // 2
|
| right = pad_w - left
|
| top = pad_h // 2
|
| bottom = pad_h - top
|
| img = TF.pad(img, [left, top, right, bottom], padding_mode='reflect')
|
| return img
|
|
|
|
|
| MODEL_INFO_PATH = Path('model_fusion_best.joblib_info.pkl')
|
| DATASET = Path('DeepfakeVsReal/Dataset')
|
|
|
| if not MODEL_INFO_PATH.exists():
|
| raise FileNotFoundError(f'{MODEL_INFO_PATH} not found')
|
|
|
| model_info = joblib.load(str(MODEL_INFO_PATH))
|
| state_path = model_info.get('state_dict_path')
|
| if state_path is None or not Path(state_path).exists():
|
| raise FileNotFoundError(f'State dict not found: {state_path}')
|
|
|
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| clf = DeepfakeFeatureFusion()
|
| clf.load_state_dict(torch.load(state_path, map_location='cpu'))
|
| clf.to(device)
|
| clf.eval()
|
|
|
| transform = transforms.Compose([
|
| transforms.Lambda(lambda img: pad_to_min_size(img, 224)),
|
| transforms.CenterCrop(224),
|
| transforms.ToTensor(),
|
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
|
| ])
|
|
|
| print('=' * 80)
|
| print('DEEPFAKE DETECTION DIAGNOSTIC')
|
| print('=' * 80)
|
| print(f'\n✓ Loaded fusion_improved model from {state_path}')
|
|
|
|
|
| val_root = DATASET / 'Validation'
|
| print(f'\nEvaluating on Validation set...')
|
| real_val = val_root / 'Real'
|
| fake_val = val_root / 'Fake'
|
|
|
| real_files = sorted([str(x) for x in real_val.rglob('*.jpg')] + [str(x) for x in real_val.rglob('*.png')])
|
| fake_files = sorted([str(x) for x in fake_val.rglob('*.jpg')] + [str(x) for x in fake_val.rglob('*.png')])
|
| files = real_files + fake_files
|
| labels = [0] * len(real_files) + [1] * len(fake_files)
|
|
|
| if len(files) > 0:
|
| dataset = ImageDataset(files, labels, transform=transform)
|
| dataloader = DataLoader(dataset, batch_size=8, shuffle=False, num_workers=0)
|
| all_probs, all_labels = [], []
|
| with torch.no_grad():
|
| for inputs, lbls in dataloader:
|
| inputs = inputs.to(device)
|
| outputs = clf(inputs)
|
| probs = torch.softmax(outputs, dim=1)[:, 1].cpu().numpy()
|
| all_probs.extend(probs.tolist())
|
| all_labels.extend(lbls.tolist())
|
| yv = np.array(all_labels)
|
| probs_v = np.array(all_probs)
|
|
|
| print(f' Total samples: {len(yv)}')
|
| print(f' Real images: {sum(yv == 0)}')
|
| print(f' Fake images: {sum(yv == 1)}')
|
|
|
|
|
| preds_05 = (probs_v >= 0.5).astype(int)
|
| acc_05 = accuracy_score(yv, preds_05)
|
| cm_05 = confusion_matrix(yv, preds_05)
|
| tn, fp, fn, tp = cm_05.ravel()
|
|
|
| print(f'\n--- Current Threshold: 0.5 ---')
|
| print(f' Accuracy: {acc_05:.4f}')
|
| print(f' True Negatives (Real correctly as Real): {tn}')
|
| print(f' False Positives (Real wrongly as Fake): {fp} ← PROBLEM')
|
| print(f' False Negatives (Fake wrongly as Real): {fn}')
|
| print(f' True Positives (Fake correctly as Fake): {tp}')
|
|
|
|
|
| try:
|
| roc_auc = roc_auc_score(yv, probs_v)
|
| print(f' ROC AUC: {roc_auc:.4f}')
|
| except Exception as e:
|
| print(f' ROC AUC: Error - {e}')
|
|
|
|
|
|
|
| print(f'\n--- Threshold Sweep (Finding Optimal) ---')
|
| thresholds = np.arange(0.1, 1.0, 0.05)
|
| best_threshold = 0.5
|
| best_fp_rate = 1.0
|
| best_metrics = {}
|
|
|
| for thresh in thresholds:
|
| preds = (probs_v >= thresh).astype(int)
|
| cm = confusion_matrix(yv, preds)
|
| tn, fp, fn, tp = cm.ravel()
|
|
|
|
|
| fp_rate = fp / (tn + fp) if (tn + fp) > 0 else 0
|
| acc = accuracy_score(yv, preds)
|
|
|
| status = "✓" if fp_rate < best_fp_rate else " "
|
| print(f' {status} Threshold {thresh:.2f}: Acc={acc:.4f}, FP_Rate={fp_rate:.4f} (FP={fp}, TP={tp})')
|
|
|
| if fp_rate < best_fp_rate:
|
| best_fp_rate = fp_rate
|
| best_threshold = thresh
|
| best_metrics = {'tn': tn, 'fp': fp, 'fn': fn, 'tp': tp, 'fp_rate': fp_rate, 'acc': acc}
|
|
|
| print(f'\n--- RECOMMENDATION ---')
|
| print(f'Best threshold: {best_threshold:.2f}')
|
| print(f' Expected metrics:')
|
| print(f' False Positive Rate: {best_metrics["fp_rate"]:.4f}')
|
| print(f' Accuracy: {best_metrics["acc"]:.4f}')
|
| print(f' Real→Fake (FP): {best_metrics["fp"]}')
|
| print(f' Fake→Real (FN): {best_metrics["fn"]}')
|
|
|
|
|
| preds_best = (probs_v >= best_threshold).astype(int)
|
| print(f'\n--- Classification Report (Threshold {best_threshold:.2f}) ---')
|
| print(classification_report(yv, preds_best, target_names=['Real', 'Fake']))
|
|
|
|
|
| test_root = DATASET / 'Test'
|
| if test_root.exists():
|
| print(f'\n{"=" * 80}')
|
| print(f'Evaluating on Test set...')
|
| real_test = test_root / 'Real'
|
| fake_test = test_root / 'Fake'
|
| real_t = sorted([str(x) for x in real_test.rglob('*.jpg')] + [str(x) for x in real_test.rglob('*.png')])
|
| fake_t = sorted([str(x) for x in fake_test.rglob('*.jpg')] + [str(x) for x in fake_test.rglob('*.png')])
|
| files_t = real_t + fake_t
|
| labels_t = [0] * len(real_t) + [1] * len(fake_t)
|
|
|
| if len(files_t) > 0:
|
| ds_t = ImageDataset(files_t, labels_t, transform=transform)
|
| dl_t = DataLoader(ds_t, batch_size=8, shuffle=False, num_workers=0)
|
| all_probs_t, all_labels_t = [], []
|
| with torch.no_grad():
|
| for inputs, lbls in dl_t:
|
| inputs = inputs.to(device)
|
| outputs = clf(inputs)
|
| probs = torch.softmax(outputs, dim=1)[:, 1].cpu().numpy()
|
| all_probs_t.extend(probs.tolist())
|
| all_labels_t.extend(lbls.tolist())
|
| yt = np.array(all_labels_t)
|
| probs_t = np.array(all_probs_t)
|
|
|
| print(f' Total samples: {len(yt)}')
|
| print(f' Real images: {sum(yt == 0)}')
|
| print(f' Fake images: {sum(yt == 1)}')
|
|
|
|
|
| preds_best = (probs_t >= best_threshold).astype(int)
|
| acc_best = accuracy_score(yt, preds_best)
|
| cm_best = confusion_matrix(yt, preds_best)
|
| tn, fp, fn, tp = cm_best.ravel()
|
|
|
| print(f'\nTest Results (Threshold {best_threshold:.2f}):')
|
| print(f' Accuracy: {acc_best:.4f}')
|
| print(f' Real→Fake (False Positive): {fp}')
|
| print(f' Fake→Real (False Negative): {fn}')
|
| print(f'\n{classification_report(yt, preds_best, target_names=["Real", "Fake"])}')
|
|
|
| print(f'\n{"=" * 80}')
|
| print('NEXT STEPS:')
|
| print('1. Update threshold in your prediction scripts from 0.5 to recommended value')
|
| print('2. If false positives still too high, consider:')
|
| print(' - Retraining with adjusted class weights')
|
| print(' - Adding more diverse real image samples')
|
| print(' - Checking for preprocessing mismatches')
|
| print('=' * 80)
|
|
|