{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.6.6","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"colab":{"provenance":[],"gpuType":"T4"},"accelerator":"GPU","kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":16880,"databundleVersionId":858837,"sourceType":"competition"},{"sourceId":9146200,"sourceType":"datasetVersion","datasetId":5524489},{"sourceId":10125851,"sourceType":"datasetVersion","datasetId":6248577},{"sourceId":5380830,"sourceType":"datasetVersion","datasetId":3120670}],"dockerImageVersionId":29845,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport cv2\nimport numpy as np\nimport tensorflow as tf\nfrom glob import glob\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import roc_auc_score, average_precision_score\n\nfrom tensorflow.keras.layers import (\n    Dense, Input, Layer, GlobalAveragePooling1D, LayerNormalization, \n    Lambda, Dropout, Reshape, Concatenate\n)\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.optimizers import Adam\n\n# ============================================================\n# 1. CUSTOM LAYERS (Preserved from your working version)\n# ============================================================\n\nclass MultiHeadAttention(Layer):\n    \"\"\"Custom MHA for compatibility with older TF versions.\"\"\"\n    def __init__(self, num_heads, key_dim, **kwargs):\n        super(MultiHeadAttention, self).__init__(**kwargs)\n        self.num_heads = num_heads\n        self.key_dim = key_dim\n        self.d_model = num_heads * key_dim\n        \n        self.query_dense = Dense(self.d_model)\n        self.key_dense = Dense(self.d_model)\n        self.value_dense = Dense(self.d_model)\n        self.combine_heads = Dense(self.d_model)\n\n    def split_heads(self, x, batch_size):\n        x = tf.reshape(x, (batch_size, -1, self.num_heads, self.key_dim))\n        return tf.transpose(x, perm=[0, 2, 1, 3])\n\n    def call(self, query, value, key):\n        batch_size = tf.shape(query)[0]\n        \n        # Linear projections\n        query = self.query_dense(query)\n        key = self.key_dense(key)\n        value = self.value_dense(value)\n        \n        # Split heads\n        query = self.split_heads(query, batch_size)\n        key = self.split_heads(key, batch_size)\n        value = self.split_heads(value, batch_size)\n        \n        # Scaled dot-product attention\n        matmul_qk = tf.matmul(query, key, transpose_b=True)\n        dk = tf.cast(tf.shape(key)[-1], tf.float32)\n        scaled_attention_logits = matmul_qk / tf.math.sqrt(dk)\n        \n        attention_weights = tf.nn.softmax(scaled_attention_logits, axis=-1)\n        output = tf.matmul(attention_weights, value)\n        \n        # Reshape and combine heads\n        output = tf.transpose(output, perm=[0, 2, 1, 3])\n        output = tf.reshape(output, (batch_size, -1, self.d_model))\n        \n        return self.combine_heads(output)\n\n    def get_config(self):\n        config = super(MultiHeadAttention, self).get_config()\n        config.update({\n            \"num_heads\": self.num_heads,\n            \"key_dim\": self.key_dim,\n        })\n        return config\n\n\nclass VisionTemporalTransformer(Layer):\n    def __init__(self, patch_size=8, d_model=128, num_heads=4, spatial_layers=1, temporal_layers=1, **kwargs):\n        super(VisionTemporalTransformer, self).__init__(**kwargs)\n        self.patch_size = patch_size\n        self.d_model = d_model\n        self.num_heads = num_heads\n        self.spatial_layers = spatial_layers\n        self.temporal_layers = temporal_layers\n\n        self.dense_projection = Dense(d_model)\n        self.pos_emb = None\n\n        self.spatial_mhas = [MultiHeadAttention(num_heads=num_heads, key_dim=d_model//num_heads) for _ in range(spatial_layers)]\n        self.spatial_norm1 = [LayerNormalization() for _ in range(spatial_layers)]\n        self.spatial_ffn = [tf.keras.Sequential([Dense(d_model*4, activation='relu'), Dense(d_model)]) for _ in range(spatial_layers)]\n        self.spatial_norm2 = [LayerNormalization() for _ in range(spatial_layers)]\n\n        self.temporal_mhas = [MultiHeadAttention(num_heads=num_heads, key_dim=d_model//num_heads) for _ in range(temporal_layers)]\n        self.temporal_norm1 = [LayerNormalization() for _ in range(temporal_layers)]\n        self.temporal_ffn = [tf.keras.Sequential([Dense(d_model*4, activation='relu'), Dense(d_model)]) for _ in range(temporal_layers)]\n        self.temporal_norm2 = [LayerNormalization() for _ in range(temporal_layers)]\n\n    def build(self, input_shape):\n        H = input_shape[2]\n        W = input_shape[3]\n        ph = H // self.patch_size\n        pw = W // self.patch_size\n        num_patches = ph * pw\n        self.pos_emb = self.add_weight(shape=(1, num_patches, self.d_model), initializer='random_normal', trainable=True, name='pos_emb')\n        super(VisionTemporalTransformer, self).build(input_shape)\n\n    def call(self, inputs):\n        # 1. Handle Shapes\n        input_shape = inputs.get_shape() \n        shape = tf.shape(inputs)\n        \n        batch = shape[0]\n        frames = shape[1]\n        H = shape[2]\n        W = shape[3]\n        \n        # Attempt to get static Channel dim\n        C_static = input_shape[-1]\n        C = C_static if C_static is not None else shape[4]\n\n        # 2. Reshape\n        reshaped = tf.reshape(inputs, (-1, H, W, C))\n\n        # 3. Extract Patches\n        patches = tf.image.extract_patches(\n            images=reshaped,\n            sizes=[1, self.patch_size, self.patch_size, 1],\n            strides=[1, self.patch_size, self.patch_size, 1],\n            rates=[1,1,1,1],\n            padding='VALID'\n        )\n        \n        # 4. Flatten Patches & FORCE SHAPE\n        if C_static is not None:\n            patch_dim_static = self.patch_size * self.patch_size * C_static\n        else:\n            patch_dim_static = None\n            \n        patch_dim_dynamic = tf.shape(patches)[-1]\n        final_patch_dim = patch_dim_static if patch_dim_static is not None else patch_dim_dynamic\n        \n        patches = tf.reshape(patches, (-1, tf.shape(patches)[1] * tf.shape(patches)[2], final_patch_dim))\n\n        if patch_dim_static is not None:\n             patches.set_shape([None, None, patch_dim_static])\n\n        # 5. Projection\n        x = self.dense_projection(patches) + self.pos_emb\n\n        # 6. Spatial Transformer\n        for i in range(self.spatial_layers):\n            attn = self.spatial_mhas[i](x, value=x, key=x)\n            x = self.spatial_norm1[i](x + attn)\n            ff = self.spatial_ffn[i](x)\n            x = self.spatial_norm2[i](x + ff)\n\n        # 7. Temporal Pooling\n        x = tf.reshape(x, (batch, frames, -1, self.d_model))\n        x = tf.reduce_mean(x, axis=2)  \n\n        # FORCE STATIC SHAPE FOR TEMPORAL (Required for older TF)\n        x.set_shape([None, None, self.d_model]) \n\n        # 8. Temporal Transformer\n        for i in range(self.temporal_layers):\n            attn = self.temporal_mhas[i](x, value=x, key=x)\n            x = self.temporal_norm1[i](x + attn)\n            ff = self.temporal_ffn[i](x)\n            x = self.temporal_norm2[i](x + ff)\n\n        pooled = GlobalAveragePooling1D()(x)\n        return pooled\n\n    def get_config(self):\n        config = super(VisionTemporalTransformer, self).get_config()\n        config.update({\n            \"patch_size\": self.patch_size,\n            \"d_model\": self.d_model,\n            \"num_heads\": self.num_heads,\n            \"spatial_layers\": self.spatial_layers,\n            \"temporal_layers\": self.temporal_layers,\n        })\n        return config\n\n# ============================================================\n# 2. MODEL DEFINITION\n# ============================================================\n\ndef batch_consistency_loss(y_true, features):\n    f = tf.reshape(features, (tf.shape(features)[0], -1))\n    f_norm = tf.math.l2_normalize(f, axis=1)\n    sim_matrix = tf.matmul(f_norm, f_norm, transpose_b=True)\n    avg_sim = tf.reduce_mean(sim_matrix, axis=1)\n    return 1.0 - avg_sim\n\ndef consistency_loss_wrapper(y_true, y_pred):\n    return batch_consistency_loss(y_true, y_pred)\n\ndef build_lipinc_model(frame_shape=(8,64,144,3), residue_shape=(7,64,144,3), d_model=128):\n    frame_input = Input(shape=frame_shape, name='FrameInput')\n    residue_input = Input(shape=residue_shape, name='ResidueInput')\n\n    vt = VisionTemporalTransformer(\n        patch_size=8, d_model=d_model, num_heads=4, spatial_layers=1, temporal_layers=1\n    )\n\n    frame_feat = vt(frame_input)      \n    residue_feat = vt(residue_input) \n\n    expand1 = Lambda(lambda x: tf.expand_dims(x, axis=1))\n    q = expand1(frame_feat)\n    k = expand1(residue_feat)\n    v = k\n\n    mha = MultiHeadAttention(num_heads=4, key_dim=d_model//4)\n    attn_out = mha(q, value=v, key=k)  \n\n    squeeze = Lambda(lambda x: tf.squeeze(x, axis=1))\n    attn_out = squeeze(attn_out)\n\n    concat = Lambda(lambda t: tf.concat(t, axis=1))\n    fusion = concat([frame_feat, residue_feat, attn_out])\n\n    x = Dense(512, activation='relu')(fusion)\n    x = Dense(256, activation='relu')(x)\n\n    class_output = Dense(2, activation='softmax', name='class_output')(x)\n    features_output = Dense(d_model, activation=None, name='features_output')(x)\n\n    model = Model(\n        inputs=[frame_input, residue_input],\n        outputs=[class_output, features_output],\n        name='LIPINC_fixed'\n    )\n    return model\n\nprint(\"Model architecture loaded.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class VideoDataGenerator(tf.keras.utils.Sequence):\n    \"\"\"\n    Custom Data Generator to load videos in batches, avoiding RAM crashes.\n    \"\"\"\n    def __init__(self, video_paths, labels, batch_size=16, frame_count=8, dim=(64, 144), shuffle=True):\n        self.video_paths = video_paths\n        self.labels = labels\n        self.batch_size = batch_size\n        self.frame_count = frame_count\n        self.dim = dim\n        self.shuffle = shuffle\n        self.indexes = np.arange(len(self.video_paths))\n        self.on_epoch_end()\n\n    def __len__(self):\n        # Denotes the number of batches per epoch\n        return int(np.floor(len(self.video_paths) / self.batch_size))\n\n    def __getitem__(self, index):\n        # Generate indexes of the batch\n        indexes = self.indexes[index*self.batch_size:(index+1)*self.batch_size]\n        list_paths = [self.video_paths[k] for k in indexes]\n        list_labels = [self.labels[k] for k in indexes]\n\n        return self.__data_generation(list_paths, list_labels)\n\n    def on_epoch_end(self):\n        # Updates indexes after each epoch\n        if self.shuffle:\n            np.random.shuffle(self.indexes)\n\n    def load_video(self, path):\n        cap = cv2.VideoCapture(path)\n        frames = []\n        try:\n            while len(frames) < self.frame_count:\n                ret, frame = cap.read()\n                if not ret:\n                    break\n                # Resize to (W, H) -> cv2 uses (width, height)\n                # Model expects (64, 144) -> Height 64, Width 144\n                frame = cv2.resize(frame, (self.dim[1], self.dim[0])) \n                frames.append(frame)\n        finally:\n            cap.release()\n\n        frames = np.array(frames)\n        \n        # Handle empty/short videos\n        if len(frames) == 0:\n            return np.zeros((self.frame_count, self.dim[0], self.dim[1], 3), dtype=np.float32)\n            \n        if len(frames) < self.frame_count:\n            # Pad with zeros\n            padding = np.zeros((self.frame_count - len(frames), self.dim[0], self.dim[1], 3))\n            frames = np.concatenate([frames, padding], axis=0)\n            \n        return frames.astype(np.float32) / 255.0\n\n    def compute_residue(self, frames):\n        # Simple residue: frame[t] - frame[t-1]\n        residues = np.zeros((self.frame_count - 1, self.dim[0], self.dim[1], 3), dtype=np.float32)\n        if len(frames) > 1:\n            for i in range(1, len(frames)):\n                residues[i-1] = frames[i] - frames[i-1]\n        return residues\n\n    def __data_generation(self, list_paths, list_labels):\n        # Initialization\n        X_frames = np.empty((self.batch_size, self.frame_count, *self.dim, 3))\n        X_residues = np.empty((self.batch_size, self.frame_count-1, *self.dim, 3))\n        y = np.empty((self.batch_size, 2), dtype=int)\n        \n        # Dummy target for consistency loss\n        dummy_feats = np.zeros((self.batch_size, 128))\n\n        for i, path in enumerate(list_paths):\n            frames = self.load_video(path)\n            X_frames[i,] = frames\n            X_residues[i,] = self.compute_residue(frames)\n            y[i] = list_labels[i]\n\n        # Inputs: [FrameInput, ResidueInput]\n        # Outputs: {'class_output': y, 'features_output': dummy}\n        return [X_frames, X_residues], {'class_output': y, 'features_output': dummy_feats}","metadata":{"id":"VUf07AfNxTkD","trusted":true,"execution":{"iopub.status.busy":"2025-11-30T15:30:28.617789Z","iopub.execute_input":"2025-11-30T15:30:28.618050Z","iopub.status.idle":"2025-11-30T15:30:28.637574Z","shell.execute_reply.started":"2025-11-30T15:30:28.618013Z","shell.execute_reply":"2025-11-30T15:30:28.636838Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport json\nimport numpy as np\nfrom glob import glob\nfrom sklearn.model_selection import train_test_split\n\n# -----------------------------------------------------------\n# 1. SETUP PATHS\n# -----------------------------------------------------------\n# Dataset 1: CelebDF\nCELEBDF_REAL_PATH = \"/kaggle/input/celeb-df-v2/Celeb-real\"\nCELEBDF_FAKE_PATH = \"/kaggle/input/celeb-df-v2/Celeb-synthesis\"\n\n# Dataset 2: FaceForensics++\nFF_REAL_PATH = \"/kaggle/input/ff-c23/FaceForensics++_C23/original\"\nFF_FAKE_PATHS = [\n    \"/kaggle/input/ff-c23/FaceForensics++_C23/Deepfakes\",\n    \"/kaggle/input/ff-c23/FaceForensics++_C23/Face2Face\",\n    \"/kaggle/input/ff-c23/FaceForensics++_C23/FaceSwap\",\n    \"/kaggle/input/ff-c23/FaceForensics++_C23/NeuralTextures\"\n]\n\n# -----------------------------------------------------------\n# 2. LOAD DATASETS\n# -----------------------------------------------------------\nprint(\"Scanning Datasets...\")\n\n# --- CelebDF ---\nceleb_real = glob(os.path.join(CELEBDF_REAL_PATH, '**', '*.mp4'), recursive=True)\nceleb_fake = glob(os.path.join(CELEBDF_FAKE_PATH, '**', '*.mp4'), recursive=True)\n# Fallback\nif not celeb_real: dfd_real = glob(os.path.join(CELEBDF_REAL_PATH, '**', '*.avi'), recursive=True)\nif not celeb_fake: dfd_fake = glob(os.path.join(CELEBDF_FAKE_PATH, '**', '*.avi'), recursive=True)\n\n# --- FaceForensics++ ---\nff_real = glob(os.path.join(FF_REAL_PATH, '**', '*.mp4'), recursive=True)\nif not ff_real: ff_real = glob(os.path.join(FF_REAL_PATH, '**', '*.avi'), recursive=True)\nff_fake = []\nfor path in FF_FAKE_PATHS:\n    fakes = glob(os.path.join(path, '**', '*.mp4'), recursive=True)\n    if not fakes: fakes = glob(os.path.join(path, '**', '*.avi'), recursive=True)\n    ff_fake.extend(fakes)\n\nprint(f\"Counts -> DFD: {len(celeb_real)}/{len(celeb_fake)} | FF++: {len(ff_real)}/{len(ff_fake)}\")\n\n# -----------------------------------------------------------\n# 3. AGGREGATE & BALANCE DATA\n# -----------------------------------------------------------\n# Combine all sources\nall_real_paths = np.array(celeb_real + ff_real)\nall_fake_paths = np.array(celeb_fake + ff_fake)\n\nn_real = len(all_real_paths)\nn_fake = len(all_fake_paths)\n\nprint(f\"Only using DFD dataset for Fake Videos\")\nprint(f\"Total Available -> Real: {n_real}, Fake: {n_fake}\")\n\n# Undersample Fake to match Real\nif n_fake > n_real:\n    print(f\"Undersampling Fake videos from {n_fake} to {n_real}...\")\n    undersampled_fake_paths = np.random.choice(all_fake_paths, n_real, replace=False)\n    final_fake_paths = undersampled_fake_paths\nelse:\n    # If we have more real than fake (or 0 fakes), keep all fakes\n    final_fake_paths = all_fake_paths\n\nfinal_real_paths = all_real_paths\n\n# Create Labels\nfinal_real_labels = [[1, 0]] * len(final_real_paths)\nfinal_fake_labels = [[0, 1]] * len(final_fake_paths)\n\n# -----------------------------------------------------------\n# 4. MERGE (FIXED)\n# -----------------------------------------------------------\n# Merge Paths (Numpy arrays)\nall_paths = np.concatenate([final_real_paths, final_fake_paths])\n\n# Merge Labels (Lists -> Single Numpy Array)\nall_labels = np.array(final_real_labels + final_fake_labels)\n\nprint(f\"Final Balanced Dataset: {len(all_paths)} total videos ({len(final_real_paths)} Real, {len(final_fake_paths)} Fake)\")\n\nif len(all_paths) == 0:\n    raise ValueError(\"No videos found! Check your dataset paths.\")\n\n# -----------------------------------------------------------\n# 5. SPLIT DATA\n# -----------------------------------------------------------\nX_train_paths, X_temp_paths, y_train, y_temp = train_test_split(\n    all_paths, all_labels, test_size=0.3, random_state=42, stratify=all_labels\n)\n\nX_val_paths, X_test_paths, y_val, y_test = train_test_split(\n    X_temp_paths, y_temp, test_size=0.5, random_state=42, stratify=y_temp\n)\n\nprint(f\"Training on: {len(X_train_paths)}\")\nprint(f\"Validation on: {len(X_val_paths)}\")\nprint(f\"Testing on: {len(X_test_paths)}\")\n\n# -----------------------------------------------------------\n# 6. INSTANTIATE GENERATORS\n# -----------------------------------------------------------\nBATCH_SIZE = 16\n\ntrain_gen = VideoDataGenerator(X_train_paths, y_train, batch_size=BATCH_SIZE)\nval_gen = VideoDataGenerator(X_val_paths, y_val, batch_size=BATCH_SIZE)\ntest_gen = VideoDataGenerator(X_test_paths, y_test, batch_size=BATCH_SIZE, shuffle=False)","metadata":{"id":"FSHj-txO1Dux","outputId":"d5e39e3e-a359-4801-ca2e-f93b0844c6c9","trusted":true,"execution":{"iopub.status.busy":"2025-11-30T15:31:39.535284Z","iopub.execute_input":"2025-11-30T15:31:39.535567Z","iopub.status.idle":"2025-11-30T15:31:55.268480Z","shell.execute_reply.started":"2025-11-30T15:31:39.535528Z","shell.execute_reply":"2025-11-30T15:31:55.267664Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping\n\n# Build Model\nmodel = build_lipinc_model()\nmodel.summary()\n\n# Compile\nopt = Adam(learning_rate=1e-5) # Slightly lower LR for stability\nmodel.compile(\n    optimizer=opt,\n    loss={\n        'class_output': 'categorical_crossentropy',\n        'features_output': consistency_loss_wrapper\n    },\n    loss_weights={\n        'class_output': 1.0,\n        'features_output': 1.0\n    },\n    metrics={'class_output': 'accuracy'}\n)\n\n# -----------------------------------------------------------\n# DEFINE CALLBACKS\n# -----------------------------------------------------------\n# 1. Save only the best model (in case performance drops later)\ncheckpoint = ModelCheckpoint(\n    'best_lipinc_model.h5', \n    monitor='val_class_output_accuracy', # specifically monitor the classification accuracy\n    save_best_only=True, \n    mode='max',\n    verbose=1\n)\n\n# 2. Stop training if it stops improving for 5 epochs\nearly_stop = EarlyStopping(\n    monitor='val_class_output_loss', \n    patience=5, \n    restore_best_weights=True,\n    verbose=1\n)\n\n# -----------------------------------------------------------\n# TRAIN\n# -----------------------------------------------------------\nprint(\"Starting Training on Full Dataset...\")\nhistory = model.fit(\n    train_gen,\n    validation_data=val_gen,\n    epochs=50,\n    callbacks=[checkpoint, early_stop], # Added callbacks list\n    verbose=1\n)\n\n# Save the final state as well\nmodel.save('lipinc_full_data_final.h5')\nprint(\"Model saved.\")","metadata":{"id":"ULJkYEq7bWcA","trusted":true,"execution":{"iopub.status.busy":"2025-11-30T15:32:41.135343Z","iopub.execute_input":"2025-11-30T15:32:41.135620Z","iopub.status.idle":"2025-11-30T15:36:08.039227Z","shell.execute_reply.started":"2025-11-30T15:32:41.135581Z","shell.execute_reply":"2025-11-30T15:36:08.037778Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import roc_auc_score, average_precision_score, jaccard_score, accuracy_score\n\nprint(\"\\nEvaluating on Test Set...\")\n# 1. Standard Keras Evaluate (Loss & Accuracy)\nresults = model.evaluate(test_gen, verbose=1)\nprint(f\"Test Loss: {results[0]:.4f}\")\nprint(f\"Test Accuracy: {results[-1]:.4f}\")\n\n# 2. Detailed Metrics Calculation\nprint(\"Calculating AP, AUC, and IoU...\")\ny_true_all = []\ny_pred_all = []\n\n# Reset generator to ensure we start from the beginning\ntest_gen.on_epoch_end()\n\n# Loop through the generator to gather all predictions\nfor i in range(len(test_gen)):\n    inputs, targets = test_gen[i]\n    \n    # Extract True Labels (One-hot encoded)\n    y_true_batch = targets['class_output']\n    \n    # Get Predictions\n    preds = model.predict_on_batch(inputs)\n    # preds is a list [class_output, features_output], we need class_output (index 0)\n    y_pred_batch = preds[0] \n    \n    y_true_all.extend(y_true_batch)\n    y_pred_all.extend(y_pred_batch)\n\n# Convert to numpy arrays\ny_true_all = np.array(y_true_all)\ny_pred_all = np.array(y_pred_all)\n\n# Extract probabilities for the \"Fake\" class (Index 1)\n# y_true_all is shape (N, 2) -> [Real, Fake]\n# y_pred_all is shape (N, 2) -> [Prob_Real, Prob_Fake]\ntrue_labels = y_true_all[:, 1]\npred_probs = y_pred_all[:, 1]\n\n# Convert probabilities to hard binary labels (0 or 1) for IoU calculation\n# Threshold is usually 0.5\npred_labels = (pred_probs > 0.5).astype(int)\n\ntry:\n    # 1. ROC-AUC\n    roc_auc = roc_auc_score(true_labels, pred_probs)\n    \n    # 2. Average Precision (AP)\n    ap_score = average_precision_score(true_labels, pred_probs)\n    \n    # 3. Intersection over Union (IoU) - Equivalent to Jaccard Score for binary classification\n    # Calculates: TP / (TP + FP + FN)\n    iou_score = jaccard_score(true_labels, pred_labels, average='binary')\n\n    print(\"-\" * 30)\n    print(f\"ROC-AUC  : {roc_auc:.4f}\")\n    print(f\"AP Score : {ap_score:.4f}\")\n    print(f\"IoU Score: {iou_score:.4f}\")\n    print(\"-\" * 30)\n    \nexcept Exception as e:\n    print(\"Error calculating metrics (Check if test set has both classes):\", e)","metadata":{"id":"HPUZIzLKbXPv","trusted":true,"execution":{"iopub.status.busy":"2025-11-30T15:36:08.042688Z","iopub.status.idle":"2025-11-30T15:36:08.043059Z","shell.execute_reply":"2025-11-30T15:36:08.042876Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport seaborn as sns\nfrom sklearn.metrics import confusion_matrix, classification_report\n\n# ==========================================\n# 1. Confusion Matrix & Classification Report\n# ==========================================\n\n# Ensure variables from the previous cell are available\n# true_labels: 1D array of actual class indices (0 or 1)\n# pred_labels: 1D array of predicted class indices (0 or 1)\n\n# Generate Confusion Matrix\ncm = confusion_matrix(true_labels, pred_labels)\n\nplt.figure(figsize=(6, 5))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', \n            xticklabels=['Real', 'Fake'], yticklabels=['Real', 'Fake'])\nplt.xlabel('Predicted Label')\nplt.ylabel('True Label')\nplt.title('Confusion Matrix')\nplt.show()\n\n# Generate Classification Report\nprint(\"\\n--- Classification Report ---\")\nprint(classification_report(true_labels, pred_labels, target_names=['Real', 'Fake']))\n\n# ==========================================\n# 2. Training History Plots\n# ==========================================\n\n# We plot the specific loss and accuracy for the 'class_output' head\nacc = history.history['class_output_accuracy']\nval_acc = history.history['val_class_output_accuracy']\n\nloss = history.history['class_output_loss']\nval_loss = history.history['val_class_output_loss']\n\nepochs_range = range(len(acc))\n\nplt.figure(figsize=(14, 5))\n\n# Plot Accuracy\nplt.subplot(1, 2, 1)\nplt.plot(epochs_range, acc, label='Training Accuracy')\nplt.plot(epochs_range, val_acc, label='Validation Accuracy')\nplt.legend(loc='lower right')\nplt.title('Training and Validation Accuracy')\nplt.xlabel('Epochs')\nplt.ylabel('Accuracy')\n\n# Plot Loss\nplt.subplot(1, 2, 2)\nplt.plot(epochs_range, loss, label='Training Loss')\nplt.plot(epochs_range, val_loss, label='Validation Loss')\nplt.legend(loc='upper right')\nplt.title('Training and Validation Loss')\nplt.xlabel('Epochs')\nplt.ylabel('Loss')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-29T12:22:00.266346Z","iopub.execute_input":"2025-11-29T12:22:00.266628Z","iopub.status.idle":"2025-11-29T12:22:00.707542Z","shell.execute_reply.started":"2025-11-29T12:22:00.266587Z","shell.execute_reply":"2025-11-29T12:22:00.706801Z"}},"outputs":[],"execution_count":null}]}