{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"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"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":16880,"databundleVersionId":858837,"sourceType":"competition"}],"dockerImageVersionId":29844,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport glob\nimport json\nimport numpy as np\nimport cv2\nfrom collections import Counter\n\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models, applications\nfrom tensorflow.keras.utils import Sequence\nfrom tensorflow.keras.mixed_precision import experimental as mixed_precision\nfrom sklearn.model_selection import GroupShuffleSplit\nfrom sklearn.utils import class_weight","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-08T11:03:15.074393Z","iopub.execute_input":"2025-12-08T11:03:15.074621Z","iopub.status.idle":"2025-12-08T11:03:15.079557Z","shell.execute_reply.started":"2025-12-08T11:03:15.074582Z","shell.execute_reply":"2025-12-08T11:03:15.078666Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"LABELED_DATA_DIR = '/kaggle/input/deepfake-detection-challenge/train_sample_videos'\nMETA_PATH = '/kaggle/input/deepfake-detection-challenge/train_sample_videos/metadata.json'\n\nBATCH_SIZE      = 8          \nSEQUENCE_LENGTH = 20         # number of frames per video\nIMG_HEIGHT      = 160        \nIMG_WIDTH       = 160\n\nFEATURE_DIR     = '/kaggle/working/features_mnet'\nos.makedirs(FEATURE_DIR, exist_ok=True)\n\npolicy = mixed_precision.Policy('mixed_float16')\nmixed_precision.set_policy(policy)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T11:03:18.243407Z","iopub.execute_input":"2025-12-08T11:03:18.243655Z","iopub.status.idle":"2025-12-08T11:03:18.251084Z","shell.execute_reply.started":"2025-12-08T11:03:18.243617Z","shell.execute_reply":"2025-12-08T11:03:18.250222Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with open(META_PATH, 'r') as f:\n    metadata = json.load(f)\n\nall_video_paths = glob.glob(f\"{LABELED_DATA_DIR}/*.mp4\")\n\nfilenames = []\nlabels = []\ngroups = []\n\nprint(f\"Scanning {len(all_video_paths)} videos for labels...\")\n\nfor filepath in all_video_paths:\n    fname = os.path.basename(filepath)\n    if fname in metadata:\n        meta = metadata[fname]\n        filenames.append(filepath)\n        labels.append(1 if meta['label'] == 'FAKE' else 0)\n        original_group = meta.get('original')\n        groups.append(original_group if original_group is not None else fname)\n\ngss = GroupShuffleSplit(n_splits=1, test_size=0.25, random_state=42)\ntrain_idx, val_idx = next(gss.split(filenames, labels, groups))\n\ntrain_paths  = [filenames[i] for i in train_idx]\ntrain_labels = [labels[i]   for i in train_idx]\n\nval_paths    = [filenames[i] for i in val_idx]\nval_labels   = [labels[i]    for i in val_idx]\n\nprint(\"-\" * 30)\nprint(\"Train label counts:\", Counter(train_labels))\nprint(\"Val   label counts:\", Counter(val_labels))\nprint(\"train videos:\", len(train_paths), \"val videos:\", len(val_paths))\nprint(\"-\" * 30)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T11:03:21.663025Z","iopub.execute_input":"2025-12-08T11:03:21.663248Z","iopub.status.idle":"2025-12-08T11:03:21.679893Z","shell.execute_reply.started":"2025-12-08T11:03:21.663212Z","shell.execute_reply":"2025-12-08T11:03:21.679239Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_video_frames(path, frame_count, img_size):\n    \"\"\"\n    Load up to `frame_count` frames evenly from the video, resize, normalize.\n    Returns array of shape (frame_count, H, W, 3).\n    \"\"\"\n    cap = cv2.VideoCapture(path)\n    frames = []\n    try:\n        total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n        if total_frames <= 0:\n            return np.zeros((frame_count, img_size[0], img_size[1], 3), dtype=np.float32)\n\n        skip = max(int(total_frames / frame_count), 1)\n\n        for i in range(frame_count):\n            cap.set(cv2.CAP_PROP_POS_FRAMES, i * skip)\n            ret, frame = cap.read()\n            if not ret:\n                break\n            frame = cv2.resize(frame, (img_size[1], img_size[0]))\n            frame = frame.astype(np.float32) / 255.0\n            frames.append(frame)\n    finally:\n        cap.release()\n\n    frames = np.array(frames, dtype=np.float32)\n    if frames.shape[0] < frame_count:\n        pad_n = frame_count - frames.shape[0]\n        padding = np.zeros((pad_n, img_size[0], img_size[1], 3), dtype=np.float32)\n        if frames.shape[0] > 0:\n            frames = np.concatenate([frames, padding], axis=0)\n        else:\n            frames = padding\n\n    return frames   # (frame_count, H, W, 3)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T11:03:24.473876Z","iopub.execute_input":"2025-12-08T11:03:24.474099Z","iopub.status.idle":"2025-12-08T11:03:24.482496Z","shell.execute_reply.started":"2025-12-08T11:03:24.474064Z","shell.execute_reply":"2025-12-08T11:03:24.481873Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"base_cnn = applications.MobileNetV2(\n    include_top=False,\n    weights='imagenet',\n    pooling='avg',     # output shape: (features,)\n    input_shape=(IMG_HEIGHT, IMG_WIDTH, 3)\n)\nbase_cnn.trainable = False\n\nframe_input = layers.Input(shape=(IMG_HEIGHT, IMG_WIDTH, 3))\nfeat = base_cnn(frame_input)\nfeature_extractor = models.Model(frame_input, feat)\n\nfeat_dim = feature_extractor.output_shape[-1]\nprint(\"Feature dim:\", feat_dim)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T11:03:27.993232Z","iopub.execute_input":"2025-12-08T11:03:27.99349Z","iopub.status.idle":"2025-12-08T11:03:30.955842Z","shell.execute_reply.started":"2025-12-08T11:03:27.993446Z","shell.execute_reply":"2025-12-08T11:03:30.955134Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def feature_path_for_video(video_path):\n    fname = os.path.basename(video_path)\n    base, _ = os.path.splitext(fname)\n    return os.path.join(FEATURE_DIR, base + \".npy\")\n\ndef extract_features_for_list(video_paths, frame_count, img_size):\n    for i, vp in enumerate(video_paths):\n        out_path = feature_path_for_video(vp)\n        if os.path.exists(out_path):\n            continue  # already done\n        frames = load_video_frames(vp, frame_count, img_size)     # (T, H, W, 3)\n        # Run all frames through CNN at once\n        feats = feature_extractor(frames, training=False).numpy() # (T, feat_dim)\n        np.save(out_path, feats)\n        if (i + 1) % 20 == 0:\n            print(f\"Extracted features for {i+1}/{len(video_paths)} videos\")\n\nprint(\"Extracting TRAIN features...\")\nextract_features_for_list(train_paths, SEQUENCE_LENGTH, (IMG_HEIGHT, IMG_WIDTH))\nprint(\"Extracting VAL features...\")\nextract_features_for_list(val_paths,   SEQUENCE_LENGTH, (IMG_HEIGHT, IMG_WIDTH))\nprint(\"Feature extraction done.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T11:03:38.652835Z","iopub.execute_input":"2025-12-08T11:03:38.653088Z","iopub.status.idle":"2025-12-08T11:13:33.173835Z","shell.execute_reply.started":"2025-12-08T11:03:38.653052Z","shell.execute_reply":"2025-12-08T11:13:33.172917Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class FeatureGenerator(Sequence):\n    def __init__(self, video_paths, labels, batch_size, seq_len):\n        self.video_paths = list(video_paths)\n        self.labels = np.array(labels, dtype=np.int32)\n        self.batch_size = int(batch_size)\n        self.seq_len = int(seq_len)\n\n        # Load one file to infer feat_dim\n        sample_feat = np.load(feature_path_for_video(self.video_paths[0]))\n        self.feat_dim = sample_feat.shape[1]\n\n        self.indices = np.arange(len(self.video_paths))\n\n    def __len__(self):\n        return int(np.ceil(len(self.video_paths) / float(self.batch_size)))\n\n    def __getitem__(self, index):\n        idx = self.indices[index * self.batch_size:(index + 1) * self.batch_size]\n        batch_paths = [self.video_paths[i] for i in idx]\n        batch_labels = self.labels[idx]\n\n        X = np.zeros((len(batch_paths), self.seq_len, self.feat_dim), dtype=np.float32)\n        y = batch_labels.astype(np.int32)\n\n        for j, vp in enumerate(batch_paths):\n            feats = np.load(feature_path_for_video(vp))\n            T = feats.shape[0]\n\n            if T >= self.seq_len:\n                X[j] = feats[:self.seq_len]\n            else:\n                # pad if somehow shorter\n                pad_n = self.seq_len - T\n                pad = np.zeros((pad_n, self.feat_dim), dtype=np.float32)\n                X[j] = np.concatenate([feats, pad], axis=0)\n\n        return X, y\n\ntrain_gen = FeatureGenerator(train_paths, train_labels, BATCH_SIZE, SEQUENCE_LENGTH)\nval_gen   = FeatureGenerator(val_paths,   val_labels,   BATCH_SIZE, SEQUENCE_LENGTH)\n\nprint(\"Train batches:\", len(train_gen), \"Val batches:\", len(val_gen))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T11:49:00.135893Z","iopub.execute_input":"2025-12-08T11:49:00.136158Z","iopub.status.idle":"2025-12-08T11:49:00.149043Z","shell.execute_reply.started":"2025-12-08T11:49:00.13612Z","shell.execute_reply":"2025-12-08T11:49:00.148293Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_lstm_on_features(seq_len, feat_dim):\n    inp = layers.Input(shape=(seq_len, feat_dim))\n    x = layers.Conv1D(128, 3, activation=\"relu\", padding=\"same\")(inp)\n    x = layers.GlobalMaxPooling1D()(x)\n    x = layers.Dropout(0.5)(x)\n    x = layers.Dense(128, activation='relu')(x)\n    out = layers.Dense(1, activation='sigmoid', dtype='float32')(x)\n\n    model = models.Model(inp, out)\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(1e-4),\n        loss=\"binary_crossentropy\",\n        metrics=[\"accuracy\"]\n    )\n    return model\n\nmodel = build_lstm_on_features(SEQUENCE_LENGTH, train_gen.feat_dim)\nmodel.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T11:49:05.516983Z","iopub.execute_input":"2025-12-08T11:49:05.517224Z","iopub.status.idle":"2025-12-08T11:49:05.597966Z","shell.execute_reply.started":"2025-12-08T11:49:05.517188Z","shell.execute_reply":"2025-12-08T11:49:05.597232Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"y_train_arr = np.array(train_labels)\nclasses = np.unique(y_train_arr)\n\nclass_weights_arr = class_weight.compute_class_weight(\n    class_weight='balanced',\n    classes=classes,\n    y=y_train_arr\n)\n\nclass_weight_dict = {int(c): float(w) for c, w in zip(classes, class_weights_arr)}\nprint(\"Class weights:\", class_weight_dict)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T11:49:11.625691Z","iopub.execute_input":"2025-12-08T11:49:11.62597Z","iopub.status.idle":"2025-12-08T11:49:11.63181Z","shell.execute_reply.started":"2025-12-08T11:49:11.625932Z","shell.execute_reply":"2025-12-08T11:49:11.631082Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau\n\ncallbacks = [\n    # EarlyStopping(patience=5, restore_best_weights=True, monitor='val_loss'),\n    ReduceLROnPlateau(patience=2, factor=0.5, monitor='val_loss')\n]\n\nEPOCHS = 20\n\nhistory = model.fit(\n    train_gen,\n    validation_data=val_gen,\n    epochs=EPOCHS,\n    class_weight=class_weight_dict,\n    callbacks=callbacks,\n    verbose=1\n)\n\nmodel.save('deepfake_lstm_on_features.h5')\nprint(\"Model saved.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T11:49:15.508166Z","iopub.execute_input":"2025-12-08T11:49:15.508431Z","iopub.status.idle":"2025-12-08T11:49:23.564096Z","shell.execute_reply.started":"2025-12-08T11:49:15.508389Z","shell.execute_reply":"2025-12-08T11:49:23.563386Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\ndef plot_training_history(history):\n    # ---- Accuracy ----\n    plt.figure(figsize=(6,4))\n    plt.plot(history.history['accuracy'], label='Train Acc')\n    plt.plot(history.history['val_accuracy'], label='Val Acc')\n    plt.xlabel('Epoch')\n    plt.ylabel('Accuracy')\n    plt.title('Training vs Validation Accuracy')\n    plt.legend()\n    plt.grid(True)\n    plt.show()\n\n    # ---- Loss ----\n    plt.figure(figsize=(6,4))\n    plt.plot(history.history['loss'], label='Train Loss')\n    plt.plot(history.history['val_loss'], label='Val Loss')\n    plt.xlabel('Epoch')\n    plt.ylabel('Loss')\n    plt.title('Training vs Validation Loss')\n    plt.legend()\n    plt.grid(True)\n    plt.show()\n\nplot_training_history(history)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T11:49:34.875252Z","iopub.execute_input":"2025-12-08T11:49:34.875504Z","iopub.status.idle":"2025-12-08T11:49:35.440378Z","shell.execute_reply.started":"2025-12-08T11:49:34.875467Z","shell.execute_reply":"2025-12-08T11:49:35.439072Z"}},"outputs":[],"execution_count":null}]}