{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":16880,"databundleVersionId":858837,"sourceType":"competition"},{"sourceId":846606,"sourceType":"datasetVersion","datasetId":446574}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## 1. Installation of Required Libraries","metadata":{}},{"cell_type":"code","source":"# 1. Install PyTorch-compatible MTCNN (from facenet-pytorch)\n# This library is essential for efficient GPU-accelerated face detection.\n!pip install facenet-pytorch --quiet\n\n# 2. Install EfficientNet implementation (often available via torch.hub, but good practice to install)\n!pip install efficientnet-pytorch --quiet\n\n# 3. Install headless version of OpenCV for video processing without GUI dependencies\n!pip install opencv-python-headless --quiet","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T09:44:30.628729Z","iopub.execute_input":"2025-12-18T09:44:30.629336Z","iopub.status.idle":"2025-12-18T09:47:32.998085Z","shell.execute_reply.started":"2025-12-18T09:44:30.629297Z","shell.execute_reply":"2025-12-18T09:47:32.997194Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 1.1. CRITICAL FIX: RESOLVE NUMPY/SCIKIT-LEARN CONFLICTS ---\n# Reinstalling core data packages to a known working version to fix the \n# 'ValueError: numpy.dtype size changed' error in scikit-learn/numpy.\n# This must be run before importing any scikit-learn component.\n\n!pip install numpy==1.26.4 scikit-learn==1.2.2 --force-reinstall --quiet\n!pip install facenet-pytorch efficientnet-pytorch opencv-python-headless --quiet","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T09:48:20.942091Z","iopub.execute_input":"2025-12-18T09:48:20.942393Z","iopub.status.idle":"2025-12-18T09:49:17.345264Z","shell.execute_reply.started":"2025-12-18T09:48:20.942370Z","shell.execute_reply":"2025-12-18T09:49:17.344511Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Import Libraries and Hardware Configuration","metadata":{}},{"cell_type":"code","source":"import os\nimport time\nimport json\nimport random\nfrom PIL import Image\n\n# Data processing and computation\nimport numpy as np\nimport pandas as pd\nfrom tqdm.notebook import tqdm\n\n# Video processing\nimport cv2 \n\n# PyTorch Core modules\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\n\n# MTCNN and EfficientNet implementations\nfrom facenet_pytorch import MTCNN\nfrom efficientnet_pytorch import EfficientNet\n\n# === ENVIRONMENT SETUP ===\n\n# Set a random seed for reproducibility across runs\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\ntorch.cuda.manual_seed_all(SEED)\n\n# Optimization settings for PyTorch\ntorch.backends.cudnn.deterministic = True\ntorch.backends.cudnn.benchmark = False\n\n# Determine the device (GPU or CPU)\ndevice = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')\nprint(f'Running on device: {device}')\n\n# Check for GPU details\nif device.type == 'cuda':\n    print(f'GPU Name: {torch.cuda.get_device_name(0)}')\n    print(f'Memory: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.2f} GB')\n\n# --- 2.5. MULTIPROCESSING FIX for CUDA on Linux ---\n\n# Check if the environment is suitable for changing the start method\nif torch.cuda.is_available() and device.type == 'cuda':\n    # Set the start method to 'spawn' if it hasn't been set yet\n    # 'spawn' is the safest method for CUDA multiprocessing on Linux/Kaggle\n    if torch.multiprocessing.get_start_method(allow_none=True) != 'spawn':\n        print(\"Setting multiprocessing start method to 'spawn' for CUDA compatibility.\")\n        torch.multiprocessing.set_start_method('spawn', force=True)\nelse:\n    print(\"CUDA not available or not required; not setting multiprocessing start method.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T09:49:17.346866Z","iopub.execute_input":"2025-12-18T09:49:17.347206Z","iopub.status.idle":"2025-12-18T09:49:17.470290Z","shell.execute_reply.started":"2025-12-18T09:49:17.347168Z","shell.execute_reply":"2025-12-18T09:49:17.469573Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Reading and Preparing Metadata","metadata":{}},{"cell_type":"code","source":"# Assume the Deepfake Detection Challenge or FaceForensics++ dataset is added as a Data Source.\n# The paths are typically set up like this in Kaggle:\nDATA_DIR = '../input/deepfake-detection-challenge/train_sample_videos/'\nMETADATA_PATH = os.path.join(DATA_DIR, 'metadata.json')\n\n# 1. Load metadata from the JSON file\ntry:\n    with open(METADATA_PATH, 'r') as f:\n        metadata = json.load(f)\nexcept FileNotFoundError:\n    print(f\"Error: Metadata file not found at {METADATA_PATH}. Check your Kaggle Data Sources.\")\n    # Create an empty DataFrame to prevent subsequent code errors\n    metadata_df = pd.DataFrame() \n    # NOTE: Adjust DATA_DIR and METADATA_PATH to match your chosen dataset path!\nelse:\n    # 2. Convert raw metadata dictionary into a Pandas DataFrame for easier handling\n    records = []\n    for k, v in metadata.items():\n        records.append({\n            'file_name': k,\n            'label': v['label'],\n            # The 'original' key is often missing for REAL videos, so use 'N/A' as default\n            'original': v.get('original', 'N/A') \n        })\n    metadata_df = pd.DataFrame(records)\n\n    # 3. Print dataset statistics\n    print(f\"Total videos in metadata: {len(metadata_df)}\")\n    print(\"Label distribution:\")\n    print(metadata_df['label'].value_counts())\n    \n    # 4. Filtration: Ensure we only work with video files that actually exist in the directory\n    existing_files = [f for f in os.listdir(DATA_DIR) if f.endswith('.mp4')]\n    metadata_df = metadata_df[metadata_df['file_name'].isin(existing_files)].reset_index(drop=True)\n    \n    print(f\"\\nVideos available for processing: {len(metadata_df)}\")\n\n# Display the first few rows for verification\nif not metadata_df.empty:\n    print(metadata_df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T09:49:17.471675Z","iopub.execute_input":"2025-12-18T09:49:17.472079Z","iopub.status.idle":"2025-12-18T09:49:17.564685Z","shell.execute_reply.started":"2025-12-18T09:49:17.472060Z","shell.execute_reply":"2025-12-18T09:49:17.564053Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Face Extraction and Preprocessing Module (MTCNN)","metadata":{}},{"cell_type":"code","source":"# --- 4.1. FACE PRE-EXTRACTION UTILITY SCRIPT (FIXED WITH OPENCV SAVE) ---\n# Goal: Decode videos, extract faces using MTCNN, and save faces as JPEG files \n# to resolve the RAM/multiprocessing error.\n\nimport cv2\nimport os\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\nfrom PIL import Image\nfrom facenet_pytorch import MTCNN\n\n# 1. Define Paths and Constants\nOUTPUT_DIR = './pre_extracted_faces_dataset/'\nFACE_OUTPUT_DIR = os.path.join(OUTPUT_DIR, 'faces')\nos.makedirs(FACE_OUTPUT_DIR, exist_ok=True)\n\nPRE_EXTRACT_FRAMES = 15 \nFACE_SIZE = 256 \n\ndef save_faces_from_video(video_path: str, video_name: str, label: str, frames_to_save: int, mtcnn_model):\n    \"\"\"\n    Decodes video, extracts faces using MTCNN, and saves using OpenCV to avoid PIL errors.\n    \"\"\"\n    cap = cv2.VideoCapture(video_path)\n    if not cap.isOpened():\n        return\n        \n    frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n    if frame_count <= 0:\n        cap.release()\n        return\n\n    # Safely generate indices\n    if frame_count < frames_to_save:\n        frame_indices = np.arange(frame_count)\n    else:\n        frame_indices = np.linspace(0, frame_count - 1, frames_to_save, dtype=int)\n    \n    for k, i in enumerate(frame_indices):\n        cap.set(cv2.CAP_PROP_POS_FRAMES, i)\n        ret, frame = cap.read()\n        if not ret: continue\n            \n        # Convert to RGB for MTCNN detection\n        frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)\n        frame_pil = Image.fromarray(frame_rgb)\n        \n        # 1. Detect faces\n        boxes, _ = mtcnn_model.detect(frame_pil)\n        \n        if boxes is not None and len(boxes) > 0:\n            # Take the first face\n            box = boxes[0].astype(int)\n            \n            # Ensure box coordinates are within image boundaries\n            width, height = frame_pil.size\n            x1 = max(0, box[0])\n            y1 = max(0, box[1])\n            x2 = min(width, box[2])\n            y2 = min(height, box[3])\n            \n            # Check if crop area is valid\n            if x2 > x1 and y2 > y1:\n                # 2. Crop using PIL (Safe and easy)\n                face_img_pil = frame_pil.crop((x1, y1, x2, y2))\n                \n                # 3. Resize using PIL\n                face_img_pil = face_img_pil.resize((FACE_SIZE, FACE_SIZE), Image.LANCZOS)\n                \n                # 4. Save using OpenCV (bypasses PIL error)\n                # Convert PIL (RGB) back to Numpy (RGB) then to OpenCV (BGR)\n                face_img_np = np.array(face_img_pil)\n                face_img_bgr = cv2.cvtColor(face_img_np, cv2.COLOR_RGB2BGR)\n                \n                output_filename = f\"{video_name.split('.')[0]}_{k}_{label}.jpg\"\n                output_path = os.path.join(FACE_OUTPUT_DIR, output_filename)\n                \n                # Write file using OpenCV (Quality 95)\n                cv2.imwrite(output_path, face_img_bgr, [cv2.IMWRITE_JPEG_QUALITY, 95])\n            \n    cap.release()\n\n\n# --- Main Execution for Pre-Extraction ---\n\n# Re-initialize MTCNN \nmtcnn_pre_extract = MTCNN(\n    image_size=FACE_SIZE, margin=0, min_face_size=20, \n    thresholds=[0.6, 0.7, 0.7], factor=0.709, post_process=False, device=device\n)\n\n# Combine train_df and val_df \ntry:\n    full_df = pd.concat([train_df, val_df]).reset_index(drop=True) \nexcept NameError:\n    full_df = metadata_df.copy()\n\nprint(f\"Starting face extraction for {len(full_df)} videos using OpenCV saver...\")\n\nfor index, row in tqdm(full_df.iterrows(), total=len(full_df)):\n    video_path = os.path.join(DATA_DIR, row['file_name'])\n    \n    save_faces_from_video(\n        video_path, \n        row['file_name'], \n        row['label'], \n        frames_to_save=PRE_EXTRACT_FRAMES,\n        mtcnn_model=mtcnn_pre_extract\n    )\n\nprint(\"Face extraction complete. New faces are saved in: ./pre_extracted_faces_dataset/faces/\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T10:03:44.742861Z","iopub.execute_input":"2025-12-18T10:03:44.743134Z","iopub.status.idle":"2025-12-18T10:57:41.766660Z","shell.execute_reply.started":"2025-12-18T10:03:44.743116Z","shell.execute_reply":"2025-12-18T10:57:41.766022Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Data Split and DataLoader Initialization","metadata":{}},{"cell_type":"code","source":"# --- 6. DATA SPLIT AND DATALOADER INITIALIZATION (IMAGE MODE) ---\n\nfrom sklearn.model_selection import train_test_split\nfrom torchvision import transforms # Import transforms here if not done in Cell 2\n\n# Define NEW Data Paths\nNEW_DATA_DIR = './pre_extracted_faces_dataset/faces/' \n\n# 1. Create a DataFrame from the saved images\nimage_files = os.listdir(NEW_DATA_DIR)\nimage_files = [f for f in image_files if f.endswith('.jpg')] \n\n# The file name format is: videoName_frameIndex_LABEL.jpg\nnew_metadata_df = pd.DataFrame({\n    'file_name': image_files,\n    # Extract 'REAL' or 'FAKE' from the file name (e.g., last element before .jpg)\n    'label': [f.split('_')[-1].split('.')[0] for f in image_files] \n})\n\nprint(f\"Total extracted face images: {len(new_metadata_df)}\")\nprint(\"Label distribution in image dataset:\")\nprint(new_metadata_df['label'].value_counts())\n\n# Split the image metadata into training and validation sets\ntrain_df, val_df = train_test_split(\n    new_metadata_df, \n    test_size=0.1, # Use a smaller test split if the total number of frames is huge\n    random_state=SEED, \n    stratify=new_metadata_df['label']\n)\n\n# Define Image Transformation (Normalization)\nIMG_TRANSFORM = transforms.Compose([\n    transforms.ToTensor(), # Converts to tensor and scales to [0, 1]\n    # Normalization (0.5 mean/std for range [-1, 1])\n    transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) \n])\n\n\n# Define Hyperparameters\nBATCH_SIZE = 32 # Can now be much larger\nNUM_WORKERS = 0\n\n# 2. Define the new Dataset Class (DeepFakeImageDataset)\nclass DeepFakeImageDataset(Dataset):\n    \"\"\"\n    Dataset for loading pre-extracted face images directly from disk.\n    \"\"\"\n    def __init__(self, metadata_df: pd.DataFrame, root_dir: str, transform=IMG_TRANSFORM):\n        self.metadata = metadata_df.copy()\n        self.root_dir = root_dir\n        self.transform = transform \n\n        # Convert label strings to numeric format (0 for REAL, 1 for FAKE)\n        self.metadata['label_numeric'] = self.metadata['label'].apply(\n            lambda x: 1.0 if x == 'FAKE' else 0.0 # Use 1.0/0.0 for BCE Loss\n        )\n\n    def __len__(self):\n        return len(self.metadata)\n\n    def __getitem__(self, idx: int):\n        row = self.metadata.iloc[idx]\n        file_name = row['file_name']\n        label = row['label_numeric']\n        image_path = os.path.join(self.root_dir, file_name)\n        \n        image = Image.open(image_path).convert('RGB')\n        \n        if self.transform:\n            image_tensor = self.transform(image)\n        else:\n            image_tensor = transforms.ToTensor()(image)\n            \n        return image_tensor, torch.tensor(label, dtype=torch.float32)\n\n# 3. Initialize DataLoaders\ntrain_dataset = DeepFakeImageDataset(train_df, NEW_DATA_DIR, transform=IMG_TRANSFORM)\nval_dataset = DeepFakeImageDataset(val_df, NEW_DATA_DIR, transform=IMG_TRANSFORM)\n\ntrain_loader = DataLoader(\n    train_dataset, \n    batch_size=BATCH_SIZE, \n    shuffle=True, \n    num_workers=NUM_WORKERS,\n    pin_memory=True\n)\n\nval_loader = DataLoader(\n    val_dataset, \n    batch_size=BATCH_SIZE, \n    shuffle=False, \n    num_workers=NUM_WORKERS,\n    pin_memory=True\n)\n\nprint(f\"\\nImage DataLoaders initialized successfully with NUM_WORKERS={NUM_WORKERS}.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T10:57:55.597828Z","iopub.execute_input":"2025-12-18T10:57:55.598414Z","iopub.status.idle":"2025-12-18T10:57:55.656144Z","shell.execute_reply.started":"2025-12-18T10:57:55.598386Z","shell.execute_reply":"2025-12-18T10:57:55.655531Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Model Definition (EfficientNet with Transfer Learning)\n\nThis section defines the EfficientNet architecture, loads pre-trained weights, and modifies the final fully-connected layer for binary classification (Real vs. Fake). We will use EfficientNet-B4, as it offers a good balance between performance and computational cost.","metadata":{}},{"cell_type":"code","source":"def setup_efficientnet(model_name: str = 'efficientnet-b4') -> nn.Module:\n    \"\"\"\n    Loads a pre-trained EfficientNet model and adapts its final layer \n    for binary classification.\n    \n    Args:\n        model_name (str): Name of the EfficientNet variant (e.g., 'efficientnet-b4').\n        \n    Returns:\n        nn.Module: The configured model ready for training.\n    \"\"\"\n    \n    print(f\"Loading pre-trained model: {model_name}...\")\n    \n    try:\n        # Load model weights pre-trained on ImageNet\n        # The 'from_pretrained' function handles downloading the weights\n        model = EfficientNet.from_pretrained(model_name)\n    except Exception as e:\n        print(f\"Error loading EfficientNet from PyPI/Cache. Trying local hub or custom path. Error: {e}\")\n        # Fallback: if you have a local copy of weights, load them here\n        model = EfficientNet.from_name(model_name)\n        # You may need to manually load state_dict here: model.load_state_dict(...)\n\n    # Freeze the convolutional base layers (optional, but speeds up early training)\n    # for param in model.parameters():\n    #     param.requires_grad = False\n        \n    # Get the number of input features for the final classification layer\n    num_ftrs = model._fc.in_features\n    \n    # Replace the final fully-connected layer (_fc) with a new Sequential layer\n    # Output: 1 neuron (for binary classification probability)\n    model._fc = nn.Sequential(\n        nn.Linear(num_ftrs, 1),\n        nn.Sigmoid() # Sigmoid squashes the output to [0, 1] (probability of being FAKE)\n    )\n    \n    # Move the model to the defined device (GPU)\n    model = model.to(device)\n    \n    print(\"Model loaded and adapted successfully.\")\n    return model\n\n# Initialize the model\nmodel = setup_efficientnet(model_name='efficientnet-b4')\n\n# Print the model architecture (last few layers) for verification\nprint(\"\\nFinal layer architecture:\")\nprint(model._fc)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T10:57:59.860729Z","iopub.execute_input":"2025-12-18T10:57:59.861347Z","iopub.status.idle":"2025-12-18T10:58:00.942547Z","shell.execute_reply.started":"2025-12-18T10:57:59.861300Z","shell.execute_reply":"2025-12-18T10:58:00.941751Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 8. Loss Function and Optimizer Setup\nWe define the loss function (Binary Cross-Entropy Loss) suitable for binary classification and choose the Adam optimizer for model training.","metadata":{}},{"cell_type":"code","source":"# Binary Cross-Entropy Loss (BCE) is standard for two-class probability output\ncriterion = nn.BCELoss() \n\n# Adam optimizer is a robust choice for deep learning models\n# We only pass parameters that require gradient updates (i.e., parameters that are not frozen)\nLEARNING_RATE = 1e-4 # Standard starting learning rate\noptimizer = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=LEARNING_RATE)\n\n# Optional: Learning Rate Scheduler (helps stabilize training and reach better optima)\nfrom torch.optim.lr_scheduler import ReduceLROnPlateau\n# Reduces LR if validation loss doesn't improve for 'patience' epochs\nscheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=5, verbose=True)\n\nprint(f\"Criterion: {criterion.__class__.__name__}\")\nprint(f\"Optimizer: {optimizer.__class__.__name__} with LR={LEARNING_RATE}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T10:58:11.643059Z","iopub.execute_input":"2025-12-18T10:58:11.643343Z","iopub.status.idle":"2025-12-18T10:58:11.651395Z","shell.execute_reply.started":"2025-12-18T10:58:11.643298Z","shell.execute_reply":"2025-12-18T10:58:11.650614Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 9. Training and Validation Loops\nThese functions define the core logic for one epoch of training and validation, including the crucial step of aggregating predictions from multiple video frames.","metadata":{}},{"cell_type":"code","source":"# --- 9. SIMPLIFIED TRAINING AND VALIDATION LOOPS (Image Mode) ---\n\ndef train_epoch(model: nn.Module, loader: DataLoader, criterion: nn.Module, optimizer: optim.Optimizer, device: torch.device) -> float:\n    \"\"\"Runs a single training epoch for image data.\"\"\"\n    model.train()\n    running_loss = 0.0\n    \n    for inputs, labels in tqdm(loader, desc=\"Training\"):\n        # inputs shape: (Batch_Size, C, H, W) - now images only\n        inputs = inputs.to(device)\n        labels = labels.to(device) \n\n        optimizer.zero_grad()\n        \n        # Forward pass (one image per prediction)\n        outputs = model(inputs).squeeze(1) # outputs shape: (Batch_Size)\n        \n        # Loss calculation (direct loss on batch)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        \n        running_loss += loss.item() * inputs.size(0) # Multiply by batch size\n\n    return running_loss / len(loader.dataset)\n\n\ndef validate_epoch(model: nn.Module, loader: DataLoader, criterion: nn.Module, device: torch.device) -> tuple[float, float]:\n    \"\"\"Runs a single validation epoch for image data.\"\"\"\n    model.eval()\n    running_loss = 0.0\n    correct_predictions = 0\n    total_samples = 0\n    \n    with torch.no_grad():\n        for inputs, labels in tqdm(loader, desc=\"Validation\"):\n            inputs = inputs.to(device)\n            labels = labels.to(device)\n\n            # Forward pass\n            outputs = model(inputs).squeeze(1) # outputs shape: (Batch_Size)\n\n            # Loss calculation\n            loss = criterion(outputs, labels)\n            running_loss += loss.item() * inputs.size(0)\n\n            # Accuracy calculation\n            predicted_labels = (outputs > 0.5).float() \n            correct_predictions += (predicted_labels == labels).sum().item()\n            total_samples += inputs.size(0)\n\n    avg_loss = running_loss / total_samples\n    avg_accuracy = correct_predictions / total_samples\n    return avg_loss, avg_accuracy","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T10:58:15.880612Z","iopub.execute_input":"2025-12-18T10:58:15.881085Z","iopub.status.idle":"2025-12-18T10:58:15.888184Z","shell.execute_reply.started":"2025-12-18T10:58:15.881060Z","shell.execute_reply":"2025-12-18T10:58:15.887300Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 10. Main Training Execution Block\nThis block orchestrates the training process, iterates over epochs, tracks performance, and saves the best model.","metadata":{}},{"cell_type":"code","source":"NUM_EPOCHS = 15 # Define the number of full passes over the dataset\n\nbest_val_accuracy = 0.0\nhistory = {\n    'train_loss': [],\n    'val_loss': [],\n    'val_acc': []\n}\n\nprint(f\"Starting training for {NUM_EPOCHS} epochs...\")\n\nfor epoch in range(NUM_EPOCHS):\n    start_time = time.time()\n    \n    # Training step\n    train_loss = train_epoch(model, train_loader, criterion, optimizer, device)\n    history['train_loss'].append(train_loss)\n    \n    # Validation step\n    val_loss, val_accuracy = validate_epoch(model, val_loader, criterion, device)\n    history['val_loss'].append(val_loss)\n    history['val_acc'].append(val_accuracy)\n    \n    end_time = time.time()\n    epoch_duration = end_time - start_time\n    \n    print(f\"\\nEpoch {epoch+1}/{NUM_EPOCHS} | Duration: {epoch_duration:.2f}s\")\n    print(f\"Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | Val Acc: {val_accuracy:.4f}\")\n    \n    # Step the learning rate scheduler\n    scheduler.step(val_loss)\n    \n    # Save the best model based on validation accuracy\n    if val_accuracy > best_val_accuracy:\n        best_val_accuracy = val_accuracy\n        # Save the model state\n        torch.save(model.state_dict(), f'best_deepfake_model_epoch_{epoch+1}.pth')\n        print(f\"Model saved! New best accuracy: {best_val_accuracy:.4f}\")\n        \nprint(\"\\nTraining completed.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 11. Data and Preprocessing Visualization\nThis block helps visually confirm that the face detection (MTCNN) and image processing steps work correctly, by displaying the raw input and the final processed face image.","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nfrom facenet_pytorch import MTCNN # Need MTCNN instance for visualization\n\n# Define paths for visualization (use one sample video file name) adylbeequz.mp4 aelfnikyqj\nSAMPLE_VIDEO_PATH = os.path.join(DATA_DIR, 'adylbeequz.mp4') \n\n# Re-initialize MTCNN for visualization (post_process=False is better for raw detection display)\nmtcnn_vis = MTCNN(\n    image_size=FACE_SIZE, margin=0, min_face_size=20, \n    thresholds=[0.6, 0.7, 0.7], factor=0.709, post_process=False, device=device\n)\n\ndef visualize_face_detection(video_path: str, frame_index: int = 5):\n    \"\"\"\n    Shows the original frame and the detected face crop for a given video.\n    \"\"\"\n    cap = cv2.VideoCapture(video_path)\n    cap.set(cv2.CAP_PROP_POS_FRAMES, frame_index)\n    ret, frame = cap.read()\n    cap.release()\n    \n    if not ret:\n        print(f\"Could not read frame {frame_index} from video {os.path.basename(video_path)}\")\n        return\n\n    frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)\n    frame_pil = Image.fromarray(frame_rgb)\n    \n    # 1. Detect faces (gets bounding box coordinates)\n    boxes, _ = mtcnn_vis.detect(frame_pil)\n    \n    fig, ax = plt.subplots(1, 2, figsize=(15, 7))\n    \n    # --- Left Plot: Raw Frame with Bounding Box ---\n    ax[0].imshow(frame_rgb)\n    ax[0].set_title(f\"Raw Frame {frame_index}\")\n    ax[0].axis('off')\n\n    if boxes is not None and len(boxes) > 0:\n        # Draw the bounding box for the first detected face\n        box = boxes[0].astype(int)\n        ax[0].plot([box[0], box[2], box[2], box[0], box[0]], \n                   [box[1], box[1], box[3], box[3], box[1]], \n                   color='red', linewidth=3)\n        \n        # 2. Extract and crop the face\n        face_tensor = mtcnn_vis.extract(frame_pil, boxes[0:1], save_path=None)\n        # --- Right Plot: Extracted and Processed Face ---\n        if face_tensor is not None:\n            # Denormalize tensor back to image array for display\n            # Крок 1: Permute (C, H, W -> H, W, C) і перемістити на CPU\n            face_img_array = face_tensor.permute(1, 2, 0).cpu().numpy()\n            \n            # --- КРОК 2: ЗВОРОТНА СТАНДАРТИЗАЦІЯ (ДЕНОРМАЛІЗАЦІЯ) ---\n            # Визначаємо параметри ImageNet\n            # mean = np.array([0.485, 0.456, 0.406])\n            # std = np.array([0.229, 0.224, 0.225])\n            \n            # # Зворотна стандартизація: x = x' * std + mean\n            # face_img_array = face_img_array * std + mean\n            \n            # # КРОК 3: Обрізка для гарантії [0, 1]\n            # face_img_array = np.clip(face_img_array, 0, 1)\n            if face_img_array.max() <= 1.0:\n                face_img_array = face_img_array * 255\n            # Крок 4: Перетворення до [0, 255] і цілих чисел (uint8)\n            face_img = face_img_array.astype(np.uint8)\n            \n            ax[1].imshow(face_img)\n            ax[1].set_title(f\"Extracted Face ({FACE_SIZE}x{FACE_SIZE})\")\n            ax[1].axis('off')\n        else:\n            ax[1].set_title(\"No face extracted after cropping\")\n            \n    else:\n        ax[0].set_title(f\"Raw Frame {frame_index} (No Face Detected)\")\n        ax[1].set_title(\"No Face Detected\")\n        \n    plt.tight_layout()\n    plt.savefig('face_detection_example.png')\n    plt.show()\n\n# Run the visualization utility (requires a valid video path)\nvisualize_face_detection(SAMPLE_VIDEO_PATH) \n# NOTE: Uncomment the line above and ensure SAMPLE_VIDEO_PATH is correct after the file upload.","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T11:41:43.593047Z","iopub.execute_input":"2025-12-18T11:41:43.593351Z","iopub.status.idle":"2025-12-18T11:41:45.274909Z","shell.execute_reply.started":"2025-12-18T11:41:43.593300Z","shell.execute_reply":"2025-12-18T11:41:45.274101Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 12. Model Evaluation and Confusion Matrix\nThis block defines the function to load the trained model and generate quantitative metrics (Accuracy, Confusion Matrix, and ROC-AUC) on the validation set.","metadata":{}},{"cell_type":"code","source":"# --- 13. MODEL EVALUATION AND CONFUSION MATRIX ---\n\nfrom sklearn.metrics import confusion_matrix, roc_auc_score, roc_curve, accuracy_score\nimport seaborn as sns\n\ndef evaluate_model(model: nn.Module, loader: DataLoader, model_path: str, device: torch.device):\n    \"\"\"\n    Loads the trained model, evaluates it on the DataLoader, and generates metrics.\n    \"\"\"\n    # Load the best weights saved during training\n    try:\n        model.load_state_dict(torch.load(model_path, map_location=device))\n        print(f\"Model loaded successfully from: {model_path}\")\n    except Exception as e:\n        print(f\"Error loading model weights: {e}. Ensure training completed and file exists.\")\n        return\n\n    model.eval()\n    all_labels = []\n    all_predictions = []\n    \n    with torch.no_grad():\n        for inputs, labels in tqdm(loader, desc=\"Testing\"):\n            inputs = inputs.to(device)\n            labels = labels.to(device)\n            \n            # Forward pass\n            outputs = model(inputs).squeeze(1) # Probability outputs (0 to 1)\n\n            all_labels.extend(labels.cpu().tolist())\n            all_predictions.extend(outputs.cpu().tolist())\n\n    # --- Metrics Calculation ---\n    \n    # Binary predictions (using 0.5 threshold)\n    binary_predictions = (np.array(all_predictions) > 0.5).astype(int)\n    \n    # 1. Accuracy\n    acc = accuracy_score(all_labels, binary_predictions)\n    print(f\"\\nFinal Validation Accuracy: {acc:.4f}\")\n    \n    # 2. Confusion Matrix\n    cm = confusion_matrix(all_labels, binary_predictions)\n    plt.figure(figsize=(6, 5))\n    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', cbar=False,\n                xticklabels=['REAL (0)', 'FAKE (1)'], yticklabels=['REAL (0)', 'FAKE (1)'])\n    plt.xlabel('Predicted Label')\n    plt.ylabel('True Label')\n    plt.title('Confusion Matrix')\n    plt.savefig('confusion_matrix.png')\n    plt.show()\n    \n    # 3. ROC-AUC Score\n    try:\n        roc_auc = roc_auc_score(all_labels, all_predictions)\n        print(f\"ROC AUC Score: {roc_auc:.4f}\")\n        \n        # 4. ROC Curve Plot\n        fpr, tpr, _ = roc_curve(all_labels, all_predictions)\n        plt.figure(figsize=(6, 6))\n        plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (AUC = {roc_auc:.4f})')\n        plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')\n        plt.xlim([0.0, 1.0])\n        plt.ylim([0.0, 1.05])\n        plt.xlabel('False Positive Rate')\n        plt.ylabel('True Positive Rate')\n        plt.title('Receiver Operating Characteristic (ROC) Curve')\n        plt.legend(loc=\"lower right\")\n        plt.savefig('roc_curve.png')\n        plt.show()\n    except ValueError:\n        print(\"ROC AUC calculation skipped (not enough samples in one class).\")\n\n# --- Execution ---\n# NOTE: Replace 'best_deepfake_model_epoch_X.pth' with the actual file name saved in cell 10.\nBEST_MODEL_PATH = 'best_deepfake_model_epoch_9.pth' \nevaluate_model(model, val_loader, BEST_MODEL_PATH, device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T11:41:57.108052Z","iopub.execute_input":"2025-12-18T11:41:57.108356Z","iopub.status.idle":"2025-12-18T11:42:02.082677Z","shell.execute_reply.started":"2025-12-18T11:41:57.108304Z","shell.execute_reply":"2025-12-18T11:42:02.081895Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 14. Training History Plot\nThis block visualizes the model's performance over the training epochs, helping to diagnose overfitting or underfitting.","metadata":{}},{"cell_type":"code","source":"def plot_training_history(history: dict):\n    \"\"\"\n    Plots the loss and accuracy curves based on the saved history dictionary.\n    \"\"\"\n    epochs = range(1, len(history['train_loss']) + 1)\n    \n    # 1. Loss Plot\n    plt.figure(figsize=(12, 5))\n    plt.subplot(1, 2, 1)\n    plt.plot(epochs, history['train_loss'], 'b-', label='Training Loss')\n    plt.plot(epochs, history['val_loss'], 'r-', label='Validation Loss')\n    plt.title('Training and Validation Loss')\n    plt.xlabel('Epochs')\n    plt.ylabel('Loss (BCE)')\n    plt.legend()\n    \n    # 2. Accuracy Plot\n    plt.subplot(1, 2, 2)\n    plt.plot(epochs, history['val_acc'], 'g-', label='Validation Accuracy')\n    plt.title('Validation Accuracy')\n    plt.xlabel('Epochs')\n    plt.ylabel('Accuracy')\n    plt.legend()\n    \n    plt.tight_layout()\n    plt.savefig('training_history.png')\n    plt.show()\n\n# --- Execution ---\n# NOTE: This requires the 'history' dictionary from cell 10 to be accessible.\nplot_training_history(history)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T11:43:13.191741Z","iopub.execute_input":"2025-12-18T11:43:13.192257Z","iopub.status.idle":"2025-12-18T11:43:13.787387Z","shell.execute_reply.started":"2025-12-18T11:43:13.192234Z","shell.execute_reply":"2025-12-18T11:43:13.786633Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"code","source":"def visualize_comparative_predictions(model, dataloader, device, num_images=5):\n    \"\"\"\n    Візуалізує та порівнює зображення, класифіковані моделлю як FAKE та REAL.\n    \n    Args:\n        model: Навчена модель (EfficientNet).\n        dataloader: DataLoader з валідаційними даними.\n        device: Пристрій (cuda або cpu).\n        num_images: Кількість зображень кожного типу для показу.\n    \"\"\"\n    model.eval()\n    \n    # Списки для збереження прикладів\n    pred_fake_imgs = []\n    pred_fake_infos = [] # (True Label, Probability)\n    \n    pred_real_imgs = []\n    pred_real_infos = []\n    \n    print(\"Збір прикладів для візуалізації...\")\n    \n    with torch.no_grad():\n        for inputs, labels in dataloader:\n            inputs = inputs.to(device)\n            # labels: 1.0 = FAKE, 0.0 = REAL\n            \n            outputs = model(inputs)\n            probs = torch.sigmoid(outputs).view(-1) # Отримуємо ймовірності [0, 1]\n            preds = (probs > 0.5).float()           # Порогова класифікація\n            \n            # Проходимо по батчу\n            for i in range(len(preds)):\n                # Якщо назбирали достатньо обох типів, виходимо\n                if len(pred_fake_imgs) >= num_images and len(pred_real_imgs) >= num_images:\n                    break\n                \n                # Денормалізація зображення для показу\n                # Зворотна формула до transforms.Normalize([0.5...], [0.5...])\n                # pixel = input * 0.5 + 0.5\n                img_tensor = inputs[i].cpu()\n                img_np = img_tensor.permute(1, 2, 0).numpy()\n                img_np = img_np * 0.5 + 0.5\n                img_np = np.clip(img_np, 0, 1) # Гарантуємо діапазон [0, 1]\n                \n                prob = probs[i].item()\n                true_label = labels[i].item()\n                \n                # Розподіляємо за ПЕРЕДБАЧЕННЯМ моделі\n                if preds[i] == 1.0: # Модель каже: FAKE\n                    if len(pred_fake_imgs) < num_images:\n                        pred_fake_imgs.append(img_np)\n                        pred_fake_infos.append((true_label, prob))\n                else: # Модель каже: REAL\n                    if len(pred_real_imgs) < num_images:\n                        pred_real_imgs.append(img_np)\n                        pred_real_infos.append((true_label, prob))\n            \n            if len(pred_fake_imgs) >= num_images and len(pred_real_imgs) >= num_images:\n                break\n    \n    # --- Побудова графіків ---\n    fig, axes = plt.subplots(2, num_images, figsize=(3 * num_images, 8))\n    plt.subplots_adjust(wspace=0.3, hspace=0.3)\n    \n    # Рядок 1: Зображення, передбачені як FAKE\n    for i in range(num_images):\n        ax = axes[0, i]\n        if i < len(pred_fake_imgs):\n            ax.imshow(pred_fake_imgs[i])\n            \n            true_lbl = pred_fake_infos[i][0]\n            prob = pred_fake_infos[i][1]\n            \n            # Колір заголовка: Зелений, якщо прогноз вірний, Червоний - якщо ні\n            # Прогноз тут завжди FAKE (1.0). Якщо True Label == 1.0, то вірно.\n            is_correct = (true_lbl == 1.0)\n            color = 'green' if is_correct else 'red'\n            true_str = \"FAKE\" if true_lbl == 1.0 else \"REAL\"\n            \n            ax.set_title(f\"Pred: FAKE\\nTrue: {true_str}\\nProb: {prob:.2f}\", color=color, fontweight='bold')\n        else:\n            ax.text(0.5, 0.5, \"Not found\", ha='center')\n        ax.axis('off')\n    \n    axes[0, 0].set_ylabel(\"Predicted: FAKE\", rotation=90, size='large', labelpad=10)\n\n    # Рядок 2: Зображення, передбачені як REAL\n    for i in range(num_images):\n        ax = axes[1, i]\n        if i < len(pred_real_imgs):\n            ax.imshow(pred_real_imgs[i])\n            \n            true_lbl = pred_real_infos[i][0]\n            prob = pred_real_infos[i][1]\n            \n            # Прогноз тут завжди REAL (0.0). Якщо True Label == 0.0, то вірно.\n            is_correct = (true_lbl == 0.0)\n            color = 'green' if is_correct else 'red'\n            true_str = \"FAKE\" if true_lbl == 1.0 else \"REAL\"\n            \n            ax.set_title(f\"Pred: REAL\\nTrue: {true_str}\\nProb: {prob:.2f}\", color=color, fontweight='bold')\n        else:\n            ax.text(0.5, 0.5, \"Not found\", ha='center')\n        ax.axis('off')\n\n    axes[1, 0].set_ylabel(\"Predicted: REAL\", rotation=90, size='large', labelpad=10)\n    \n    plt.suptitle(\"Model Predictions Comparison: FAKE vs REAL\", fontsize=16)\n    plt.show()\n\n# --- ЗАПУСК ВІЗУАЛІЗАЦІЇ ---\n# Переконайтеся, що 'model' та 'val_loader' вже визначені та модель навчена\ntry:\n    visualize_comparative_predictions(model, val_loader, device, num_images=5)\nexcept NameError:\n    print(\"Помилка: Переконайтеся, що ви запустили всі попередні комірки (визначення моделі, dataloader тощо).\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T12:08:42.613622Z","iopub.execute_input":"2025-12-18T12:08:42.613917Z","iopub.status.idle":"2025-12-18T12:08:47.761953Z","shell.execute_reply.started":"2025-12-18T12:08:42.613897Z","shell.execute_reply":"2025-12-18T12:08:47.761180Z"}},"outputs":[],"execution_count":null}]}