{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":16880,"databundleVersionId":858837},{"sourceType":"datasetVersion","sourceId":10125851,"datasetId":6248577,"databundleVersionId":10408999},{"sourceType":"kernelVersion","sourceId":232584722}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"7165b7af-1835-44cb-a197-b824626742d3","cell_type":"markdown","source":"# Deepfake Detection — Violence contre les Femmes\n**Axe 2 : Détection Deepfake Image/Vidéo**\n\n| Modèle | Architecture | Dataset |\n|--------|-------------|--------|\n| XceptionNet | Depthwise separable convolutions | FaceForensics++ |\n| EfficientNet-B4 + Attention | Compound scaling + CBAM | Celeb-DF v2 |\n| CLIP-based Detector | Vision-Language | DFDC |\n\n**Pipeline :** Data Import → EDA → Engineering → Preparation → Modeling → XAI","metadata":{}},{"id":"883ffe71-9592-44ad-b340-6c5e6e1268a7","cell_type":"markdown","source":"## 📦 0. Installation & Imports","metadata":{}},{"id":"ae073a1d-215c-4670-ac7b-bbe9cac5fd9c","cell_type":"code","source":"# Fix complet — exécute puis RESTART KERNEL\nimport subprocess\n\ncmds = [\n    # Fix Pillow cassé (racine du problème)\n    ['pip', 'install', '-q', '--upgrade', 'Pillow==10.4.0'],\n    # Réinstaller torchvision compatible\n    ['pip', 'install', '-q', '--upgrade', 'torchvision==0.17.2'],\n    # Réinstaller grad-cam proprement\n    ['pip', 'install', '-q', '--force-reinstall', 'grad-cam==1.4.8'],\n    # LIME\n    ['pip', 'install', '-q', 'lime'],\n]\n\nfor cmd in cmds:\n    result = subprocess.run(cmd, capture_output=True, text=True)\n    status = '✅' if result.returncode == 0 else '❌'\n    print(f\"{status} {' '.join(cmd[2:])}\")\n\nprint(\"\\n⚠️  MAINTENANT : Kernel > Restart & Run All\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:45:11.188324Z","iopub.execute_input":"2026-05-05T19:45:11.189506Z","iopub.status.idle":"2026-05-05T19:47:18.061359Z","shell.execute_reply.started":"2026-05-05T19:45:11.189469Z","shell.execute_reply":"2026-05-05T19:47:18.06064Z"}},"outputs":[],"execution_count":null},{"id":"6c534c22-8881-4556-bfcb-7929ed8c1a10","cell_type":"code","source":"# Installation (run once)\n!pip install -q timm transformers grad-cam kaggle facenet-pytorch albumentations","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:47:18.062842Z","iopub.execute_input":"2026-05-05T19:47:18.063171Z","iopub.status.idle":"2026-05-05T19:47:56.006945Z","shell.execute_reply.started":"2026-05-05T19:47:18.063145Z","shell.execute_reply":"2026-05-05T19:47:56.006136Z"}},"outputs":[],"execution_count":null},{"id":"5192da31-37d8-41ee-b386-316a5cc84e7d","cell_type":"code","source":"# RESET numpy — doit être la PREMIÈRE cellule exécutée\nimport subprocess\nsubprocess.run(['pip', 'install', '-q', '--upgrade', 'numpy==1.26.4'], check=True)\nsubprocess.run(['pip', 'install', '-q', 'grad-cam==1.4.8'], check=True)\n\nprint(\"✅ Done — maintenant : Kernel > Restart & Run All\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:47:56.008349Z","iopub.execute_input":"2026-05-05T19:47:56.008667Z","iopub.status.idle":"2026-05-05T19:48:02.974874Z","shell.execute_reply.started":"2026-05-05T19:47:56.008626Z","shell.execute_reply":"2026-05-05T19:48:02.974017Z"}},"outputs":[],"execution_count":null},{"id":"ea75d2f5-8a1f-4343-bb33-861176d41a11","cell_type":"code","source":"# ❌ SHAP incompatible avec cet environnement Kaggle → on utilise GradCAM++ à la place\n\nfrom pytorch_grad_cam import GradCAMPlusPlus\nfrom pytorch_grad_cam.utils.image import show_cam_on_image\n\ndef gradcam_plusplus(model, target_layer, img_tensor, img_orig):\n    cam = GradCAMPlusPlus(model=model, target_layers=[target_layer])\n    grayscale = cam(input_tensor=img_tensor.unsqueeze(0).to(DEVICE))\n    img_np = np.array(img_orig.resize((224, 224))).astype(np.float32) / 255.0\n    return show_cam_on_image(img_np, grayscale[0], use_rgb=True)\n\n# Appliquer sur EfficientNet (à la place de SHAP)\ncam_eff_pp = gradcam_plusplus(\n    effnet,\n    effnet.features.blocks[-1][-1],\n    img_tensor,\n    img_orig\n)\n\nplt.figure(figsize=(6, 5))\nplt.imshow(cam_eff_pp)\nplt.title('GradCAM++ — EfficientNet-B4+CBAM\\n(remplace SHAP)')\nplt.axis('off')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:02.97603Z","iopub.execute_input":"2026-05-05T19:48:02.976341Z","iopub.status.idle":"2026-05-05T19:48:13.920019Z","shell.execute_reply.started":"2026-05-05T19:48:02.976304Z","shell.execute_reply":"2026-05-05T19:48:13.917444Z"}},"outputs":[],"execution_count":null},{"id":"2ceffcf8-23d9-4269-be32-df0cb07743b1","cell_type":"code","source":"import os, json, random, cv2, warnings\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom pathlib import Path\nfrom PIL import Image\nfrom tqdm.notebook import tqdm\nwarnings.filterwarnings('ignore')\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as T\nfrom torchvision.models import efficientnet_b4\n\nimport timm\nfrom transformers import CLIPModel, CLIPProcessor\nfrom pytorch_grad_cam import GradCAM, GradCAMPlusPlus\nfrom pytorch_grad_cam.utils.image import show_cam_on_image\n\nSEED = 42\nrandom.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'Device: {DEVICE}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.920993Z","iopub.status.idle":"2026-05-05T19:48:13.921257Z","shell.execute_reply.started":"2026-05-05T19:48:13.921138Z","shell.execute_reply":"2026-05-05T19:48:13.921154Z"}},"outputs":[],"execution_count":null},{"id":"77fb552b-5511-45cd-b176-358a6fc86647","cell_type":"code","source":"os.listdir('/kaggle/input/faceforensics-dataset-c23')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.922093Z","iopub.status.idle":"2026-05-05T19:48:13.922517Z","shell.execute_reply.started":"2026-05-05T19:48:13.922365Z","shell.execute_reply":"2026-05-05T19:48:13.922386Z"}},"outputs":[],"execution_count":null},{"id":"a57cb95b-32a6-4cea-a20f-9e4fbca6841b","cell_type":"markdown","source":"## 📥 1. Import des Données\n\n> **Datasets utilisés :**\n> - [FaceForensics++](https://github.com/ondyari/FaceForensics) — accès sur demande\n> - [Celeb-DF v2](https://github.com/yuezunli/celeb-deepfakeforensics) — accès sur demande  \n> - [DFDC](https://www.kaggle.com/competitions/deepfake-detection-challenge/data) — Kaggle\n>\n> Structure attendue :\n> ```\n> data/\n>   real/   → frames de vraies vidéos\n>   fake/   → frames de vidéos deepfake\n> ```","metadata":{}},{"id":"e8cee24f-241f-4504-88b9-0230493bf93c","cell_type":"code","source":"# ── Chemins Kaggle ──────────────────────────────────────\nDFDC_PATH   = '/kaggle/input/deepfake-detection-challenge'\nFF_PATH     = '/kaggle/input/faceforensics-dataset-c23'\nCELEB_PATH  = '/kaggle/input/deepfake-video-celeb-df'\n\n# Vérification\nimport os\nfor name, path in [('DFDC', DFDC_PATH), ('FF++', FF_PATH), ('Celeb-DF', CELEB_PATH)]:\n    exists = os.path.exists(path)\n    files  = len(os.listdir(path)) if exists else 0\n    print(f'{name}: {\"✅\" if exists else \"❌\"} | {files} fichiers/dossiers')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.923737Z","iopub.status.idle":"2026-05-05T19:48:13.924148Z","shell.execute_reply.started":"2026-05-05T19:48:13.923934Z","shell.execute_reply":"2026-05-05T19:48:13.92396Z"}},"outputs":[],"execution_count":null},{"id":"01b57d09-96b3-47dc-bc6e-2e1ae6d19fc8","cell_type":"markdown","source":"## 🔎 2. Data Comprehension (EDA)","metadata":{}},{"id":"dfcae641-fa6f-4fab-a98b-8256eaae5683","cell_type":"code","source":"# Construire le DataFrame principal\nrecords = []\nfor label in ['real', 'fake']:\n    for p in Path(f'data/{label}').glob('*.jpg'):\n        img = Image.open(p)\n        records.append({'path': str(p), 'label': label,\n                        'width': img.width, 'height': img.height,\n                        'size_kb': p.stat().st_size / 1024})\n\ndf = pd.DataFrame(records)\ndf['label_int'] = (df['label'] == 'fake').astype(int)\nprint(df.head())\nprint(f'\\nShape: {df.shape}')\ndf['label'].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.925636Z","iopub.status.idle":"2026-05-05T19:48:13.925966Z","shell.execute_reply.started":"2026-05-05T19:48:13.92582Z","shell.execute_reply":"2026-05-05T19:48:13.925848Z"}},"outputs":[],"execution_count":null},{"id":"78572207-9f5d-4277-8235-67c7fe16b765","cell_type":"code","source":"# Visualisations EDA\nfig, axes = plt.subplots(1, 3, figsize=(16, 4))\n\n# Distribution des classes\ndf['label'].value_counts().plot(kind='bar', ax=axes[0], color=['steelblue','tomato'])\naxes[0].set_title('Distribution Real vs Fake')\n\n# Distribution taille fichier\nfor lbl, grp in df.groupby('label'):\n    axes[1].hist(grp['size_kb'], bins=20, alpha=0.6, label=lbl)\naxes[1].set_title('Taille des fichiers (KB)')\naxes[1].legend()\n\n# Dimensions\naxes[2].scatter(df[df.label=='real']['width'], df[df.label=='real']['height'],\n                alpha=0.4, label='real', color='steelblue')\naxes[2].scatter(df[df.label=='fake']['width'], df[df.label=='fake']['height'],\n                alpha=0.4, label='fake', color='tomato')\naxes[2].set_title('Dimensions images')\naxes[2].legend()\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.927965Z","iopub.status.idle":"2026-05-05T19:48:13.928395Z","shell.execute_reply.started":"2026-05-05T19:48:13.928192Z","shell.execute_reply":"2026-05-05T19:48:13.928209Z"}},"outputs":[],"execution_count":null},{"id":"14745b8b-37c8-4701-9275-fe91accf46fa","cell_type":"code","source":"# Exemples visuels\nfig, axes = plt.subplots(2, 4, figsize=(14, 7))\nfor i, label in enumerate(['real', 'fake']):\n    samples = df[df.label == label].sample(4)\n    for j, (_, row) in enumerate(samples.iterrows()):\n        img = Image.open(row['path'])\n        axes[i, j].imshow(img)\n        axes[i, j].set_title(label.upper(), color='green' if label=='real' else 'red')\n        axes[i, j].axis('off')\nplt.suptitle('Samples Real vs Fake', fontsize=14)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.929449Z","iopub.status.idle":"2026-05-05T19:48:13.929826Z","shell.execute_reply.started":"2026-05-05T19:48:13.929627Z","shell.execute_reply":"2026-05-05T19:48:13.92965Z"}},"outputs":[],"execution_count":null},{"id":"668eaedf-7227-4303-b790-8aa1296e4152","cell_type":"code","source":"# Analyse statistique des pixels (moyenne RGB par classe)\nmeans = {'real': [], 'fake': []}\nfor _, row in df.sample(min(60, len(df))).iterrows():\n    arr = np.array(Image.open(row['path']).resize((64, 64))).mean(axis=(0,1))\n    means[row['label']].append(arr)\n\nfor label, vals in means.items():\n    m = np.mean(vals, axis=0)\n    print(f'{label:4s} → R={m[0]:.1f}, G={m[1]:.1f}, B={m[2]:.1f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.930793Z","iopub.status.idle":"2026-05-05T19:48:13.931192Z","shell.execute_reply.started":"2026-05-05T19:48:13.931002Z","shell.execute_reply":"2026-05-05T19:48:13.931029Z"}},"outputs":[],"execution_count":null},{"id":"1ac9bdeb-017e-40dc-a5bc-750a23b8011e","cell_type":"markdown","source":"## 🛠️ 3. Data Engineering & Feature Analysis","metadata":{}},{"id":"2177dd54-bbc6-4056-83da-6a1ff6c46620","cell_type":"code","source":"# Feature engineering : texture (Laplacian variance = sharpness)\ndef laplacian_var(path):\n    img = cv2.imread(str(path), cv2.IMREAD_GRAYSCALE)\n    img = cv2.resize(img, (224, 224))\n    return cv2.Laplacian(img, cv2.CV_64F).var()\n\ndf['sharpness'] = df['path'].apply(laplacian_var)\nprint(df.groupby('label')['sharpness'].describe())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.932645Z","iopub.status.idle":"2026-05-05T19:48:13.933002Z","shell.execute_reply.started":"2026-05-05T19:48:13.932823Z","shell.execute_reply":"2026-05-05T19:48:13.93285Z"}},"outputs":[],"execution_count":null},{"id":"4c3999e4-96ce-40a1-bc7e-1b87db3df055","cell_type":"code","source":"# DCT-based frequency analysis (deepfakes ont souvent des artefacts fréquentiels)\ndef dct_energy(path):\n    img = cv2.imread(str(path), cv2.IMREAD_GRAYSCALE)\n    img = cv2.resize(img, (224, 224)).astype(np.float32)\n    dct = cv2.dct(img)\n    return float(np.log1p(np.abs(dct[1:10, 1:10])).mean())\n\ndf['dct_energy'] = df['path'].apply(dct_energy)\n\n# Visualiser les features engineered\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))\nfor lbl, grp in df.groupby('label'):\n    ax1.hist(grp['sharpness'], bins=20, alpha=0.6, label=lbl)\n    ax2.hist(grp['dct_energy'], bins=20, alpha=0.6, label=lbl)\nax1.set_title('Sharpness (Laplacian var)')\nax2.set_title('DCT Energy (fréquences basses)')\nax1.legend(); ax2.legend()\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.934873Z","iopub.status.idle":"2026-05-05T19:48:13.935272Z","shell.execute_reply.started":"2026-05-05T19:48:13.935044Z","shell.execute_reply":"2026-05-05T19:48:13.935075Z"}},"outputs":[],"execution_count":null},{"id":"8a0bf9f4-77ad-4246-9a99-05629d15e2b9","cell_type":"markdown","source":"## ⚙️ 4. Data Preparation","metadata":{}},{"id":"765a4093-d1ab-4f72-a900-b5abd9cf507f","cell_type":"code","source":"from sklearn.model_selection import train_test_split\n\ntrain_df, temp_df = train_test_split(df, test_size=0.3, stratify=df['label_int'], random_state=SEED)\nval_df, test_df   = train_test_split(temp_df, test_size=0.5, stratify=temp_df['label_int'], random_state=SEED)\n\nprint(f'Train: {len(train_df)} | Val: {len(val_df)} | Test: {len(test_df)}')\nfor split, d in [('Train', train_df), ('Val', val_df), ('Test', test_df)]:\n    counts = d['label'].value_counts().to_dict()\n    print(f'  {split}: {counts}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.936636Z","iopub.status.idle":"2026-05-05T19:48:13.937047Z","shell.execute_reply.started":"2026-05-05T19:48:13.936844Z","shell.execute_reply":"2026-05-05T19:48:13.936878Z"}},"outputs":[],"execution_count":null},{"id":"3e964f69-b1d2-4e21-9d5a-94b2c609d69b","cell_type":"code","source":"# Transforms : augmentation pour train, normalisation pour val/test\nIMG_SIZE = 224\n\ntrain_tfm = T.Compose([\n    T.Resize((IMG_SIZE, IMG_SIZE)),\n    T.RandomHorizontalFlip(),\n    T.ColorJitter(brightness=0.2, contrast=0.2),\n    T.ToTensor(),\n    T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n])\n\neval_tfm = T.Compose([\n    T.Resize((IMG_SIZE, IMG_SIZE)),\n    T.ToTensor(),\n    T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n])\n\nprint('✅ Transforms définis')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.938585Z","iopub.status.idle":"2026-05-05T19:48:13.938983Z","shell.execute_reply.started":"2026-05-05T19:48:13.938762Z","shell.execute_reply":"2026-05-05T19:48:13.938806Z"}},"outputs":[],"execution_count":null},{"id":"52a2a5fd-a0e3-4e62-ad6b-23b2380c214f","cell_type":"code","source":"class DeepfakeDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.transform = transform\n\n    def __len__(self): return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img = Image.open(row['path']).convert('RGB')\n        if self.transform:\n            img = self.transform(img)\n        return img, torch.tensor(row['label_int'], dtype=torch.float32)\n\nBS = 32\ntrain_loader = DataLoader(DeepfakeDataset(train_df, train_tfm), batch_size=BS, shuffle=True,  num_workers=2)\nval_loader   = DataLoader(DeepfakeDataset(val_df,   eval_tfm),  batch_size=BS, shuffle=False, num_workers=2)\ntest_loader  = DataLoader(DeepfakeDataset(test_df,  eval_tfm),  batch_size=BS, shuffle=False, num_workers=2)\nprint(f'✅ DataLoaders prêts | Batch size: {BS}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.939888Z","iopub.status.idle":"2026-05-05T19:48:13.940121Z","shell.execute_reply.started":"2026-05-05T19:48:13.940009Z","shell.execute_reply":"2026-05-05T19:48:13.940024Z"}},"outputs":[],"execution_count":null},{"id":"7c4699a4-0018-48f6-b61e-581fb04c232d","cell_type":"markdown","source":"## 🤖 5. Modèle 1 — XceptionNet\n> Référence FaceForensics++ — convolutions séparables en profondeur","metadata":{}},{"id":"3b1835c7-7e84-412a-b92f-2081c98ac1b5","cell_type":"code","source":"def build_xception(pretrained=True):\n    model = timm.create_model('xception', pretrained=pretrained, num_classes=1)\n    return model.to(DEVICE)\n\nxception = build_xception()\ntotal_params = sum(p.numel() for p in xception.parameters() if p.requires_grad)\nprint(f'✅ XceptionNet | Params entraînables: {total_params:,}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.940995Z","iopub.status.idle":"2026-05-05T19:48:13.941332Z","shell.execute_reply.started":"2026-05-05T19:48:13.941192Z","shell.execute_reply":"2026-05-05T19:48:13.941218Z"}},"outputs":[],"execution_count":null},{"id":"40d0b672-4bf2-4698-82d4-4d7621dcd9eb","cell_type":"code","source":"# Utilitaire d'entraînement générique\nfrom sklearn.metrics import f1_score, roc_auc_score\n\ndef train_epoch(model, loader, optimizer, criterion):\n    model.train()\n    total_loss, preds_all, labels_all = 0, [], []\n    for imgs, labels in loader:\n        imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)\n        optimizer.zero_grad()\n        out = model(imgs).squeeze(1)\n        loss = criterion(out, labels)\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item()\n        preds_all.extend(torch.sigmoid(out).detach().cpu().numpy())\n        labels_all.extend(labels.cpu().numpy())\n    preds_bin = (np.array(preds_all) > 0.5).astype(int)\n    return total_loss / len(loader), f1_score(labels_all, preds_bin, zero_division=0)\n\n@torch.no_grad()\ndef eval_epoch(model, loader, criterion):\n    model.eval()\n    total_loss, preds_all, labels_all = 0, [], []\n    for imgs, labels in loader:\n        imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)\n        out = model(imgs).squeeze(1)\n        loss = criterion(out, labels)\n        total_loss += loss.item()\n        preds_all.extend(torch.sigmoid(out).cpu().numpy())\n        labels_all.extend(labels.cpu().numpy())\n    preds_bin = (np.array(preds_all) > 0.5).astype(int)\n    auc = roc_auc_score(labels_all, preds_all) if len(set(labels_all)) > 1 else 0.0\n    return total_loss / len(loader), f1_score(labels_all, preds_bin, zero_division=0), auc\n\nprint('✅ Fonctions train/eval définies')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.942454Z","iopub.status.idle":"2026-05-05T19:48:13.942681Z","shell.execute_reply.started":"2026-05-05T19:48:13.942571Z","shell.execute_reply":"2026-05-05T19:48:13.942585Z"}},"outputs":[],"execution_count":null},{"id":"77e6347b-c512-4e21-ac00-b6b50db189ad","cell_type":"code","source":"# Boucle d'entraînement générique (max ~30 min par modèle)\ndef train_model(model, train_loader, val_loader, epochs=8, lr=1e-4, name='model'):\n    criterion = nn.BCEWithLogitsLoss()\n    optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)\n    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)\n    history   = {'train_loss':[], 'val_loss':[], 'val_f1':[], 'val_auc':[]}\n    best_auc, best_state = 0, None\n\n    for epoch in range(1, epochs+1):\n        tr_loss, tr_f1 = train_epoch(model, train_loader, optimizer, criterion)\n        vl_loss, vl_f1, vl_auc = eval_epoch(model, val_loader, criterion)\n        scheduler.step()\n        history['train_loss'].append(tr_loss)\n        history['val_loss'].append(vl_loss)\n        history['val_f1'].append(vl_f1)\n        history['val_auc'].append(vl_auc)\n        print(f'[{name}] Ep {epoch}/{epochs} | Loss {tr_loss:.3f}/{vl_loss:.3f} | F1 {vl_f1:.3f} | AUC {vl_auc:.3f}')\n        if vl_auc > best_auc:\n            best_auc = vl_auc\n            best_state = {k: v.clone() for k, v in model.state_dict().items()}\n\n    model.load_state_dict(best_state)\n    torch.save(best_state, f'{name}_best.pth')\n    return history","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.943405Z","iopub.status.idle":"2026-05-05T19:48:13.943637Z","shell.execute_reply.started":"2026-05-05T19:48:13.943526Z","shell.execute_reply":"2026-05-05T19:48:13.94354Z"}},"outputs":[],"execution_count":null},{"id":"648934e6-6725-4435-a0e1-0a865c613fa0","cell_type":"code","source":"# Entraînement XceptionNet (~20-30 min sur GPU)\nhist_xcp = train_model(xception, train_loader, val_loader, epochs=8, name='xception')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.944331Z","iopub.status.idle":"2026-05-05T19:48:13.944652Z","shell.execute_reply.started":"2026-05-05T19:48:13.944494Z","shell.execute_reply":"2026-05-05T19:48:13.944518Z"}},"outputs":[],"execution_count":null},{"id":"3c793cab-d183-4695-9165-8be01c308086","cell_type":"code","source":"def plot_history(history, title):\n    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))\n    ax1.plot(history['train_loss'], label='Train Loss')\n    ax1.plot(history['val_loss'],   label='Val Loss')\n    ax1.set_title(f'{title} — Loss'); ax1.legend()\n    ax2.plot(history['val_f1'],  label='Val F1')\n    ax2.plot(history['val_auc'], label='Val AUC')\n    ax2.set_title(f'{title} — Métriques'); ax2.legend()\n    plt.tight_layout(); plt.show()\n\nplot_history(hist_xcp, 'XceptionNet')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.945714Z","iopub.status.idle":"2026-05-05T19:48:13.946071Z","shell.execute_reply.started":"2026-05-05T19:48:13.94591Z","shell.execute_reply":"2026-05-05T19:48:13.945936Z"}},"outputs":[],"execution_count":null},{"id":"965b005c-1932-49cd-ad6e-5dc17cf6def8","cell_type":"markdown","source":"## 🤖 6. Modèle 2 — EfficientNet-B4 + Attention (CBAM)","metadata":{}},{"id":"e80ea8f8-8cb3-454c-b467-16d75be3c8b9","cell_type":"code","source":"# Module d'attention CBAM (Channel + Spatial)\nclass CBAM(nn.Module):\n    def __init__(self, channels, r=16):\n        super().__init__()\n        self.ca = nn.Sequential(\n            nn.AdaptiveAvgPool2d(1), nn.Flatten(),\n            nn.Linear(channels, channels//r), nn.ReLU(),\n            nn.Linear(channels//r, channels), nn.Sigmoid()\n        )\n        self.sa = nn.Sequential(\n            nn.Conv2d(2, 1, 7, padding=3), nn.Sigmoid()\n        )\n\n    def forward(self, x):\n        x = x * self.ca(x).view(x.size(0), -1, 1, 1)   # channel attention\n        x = x * self.sa(torch.cat([x.mean(1,True), x.max(1,True)[0]], 1))  # spatial\n        return x\n\nprint('✅ CBAM défini')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.947161Z","iopub.status.idle":"2026-05-05T19:48:13.947439Z","shell.execute_reply.started":"2026-05-05T19:48:13.947323Z","shell.execute_reply":"2026-05-05T19:48:13.947339Z"}},"outputs":[],"execution_count":null},{"id":"c5c47ce1-081d-40a7-917b-0d1e8a5bca99","cell_type":"code","source":"class EfficientNetCBAM(nn.Module):\n    def __init__(self):\n        super().__init__()\n        base = timm.create_model('efficientnet_b4', pretrained=True, num_classes=0)\n        self.features = base\n        self.cbam = CBAM(1792)  # channels EfficientNet-B4\n        self.pool  = nn.AdaptiveAvgPool2d(1)\n        self.head  = nn.Sequential(nn.Dropout(0.4), nn.Linear(1792, 1))\n\n    def forward(self, x):\n        x = self.features.forward_features(x)\n        x = self.cbam(x)\n        x = self.pool(x).flatten(1)\n        return self.head(x)\n\neffnet = EfficientNetCBAM().to(DEVICE)\nprint(f'✅ EfficientNet-B4+CBAM | Params: {sum(p.numel() for p in effnet.parameters() if p.requires_grad):,}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.949124Z","iopub.status.idle":"2026-05-05T19:48:13.949483Z","shell.execute_reply.started":"2026-05-05T19:48:13.949298Z","shell.execute_reply":"2026-05-05T19:48:13.949321Z"}},"outputs":[],"execution_count":null},{"id":"abd3f657-9dfe-4bc9-8ef7-7dda8d577dbe","cell_type":"code","source":"# Entraînement EfficientNet-B4 + CBAM\nhist_eff = train_model(effnet, train_loader, val_loader, epochs=8, lr=5e-5, name='efficientnet_cbam')\nplot_history(hist_eff, 'EfficientNet-B4 + CBAM')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.950977Z","iopub.status.idle":"2026-05-05T19:48:13.9513Z","shell.execute_reply.started":"2026-05-05T19:48:13.951136Z","shell.execute_reply":"2026-05-05T19:48:13.951171Z"}},"outputs":[],"execution_count":null},{"id":"04c6efe4-ad2d-45b4-9b36-4f51cd2e40e8","cell_type":"markdown","source":"## 🤖 7. Modèle 3 — CLIP-based Detector","metadata":{}},{"id":"1d5be27d-e284-4eae-9bd0-1bfccd2272e2","cell_type":"code","source":"# CLIP : extraire les embeddings visuels → fine-tune classifieur léger\nfrom transformers import CLIPModel, CLIPProcessor\n\nclip_model     = CLIPModel.from_pretrained('openai/clip-vit-base-patch32').to(DEVICE)\nclip_processor = CLIPProcessor.from_pretrained('openai/clip-vit-base-patch32')\n\n# Geler le backbone CLIP\nfor p in clip_model.parameters():\n    p.requires_grad = False\n\nprint('✅ CLIP chargé et gelé')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.952641Z","iopub.status.idle":"2026-05-05T19:48:13.952955Z","shell.execute_reply.started":"2026-05-05T19:48:13.952816Z","shell.execute_reply":"2026-05-05T19:48:13.952844Z"}},"outputs":[],"execution_count":null},{"id":"57a22726-1e5c-4c6f-81f7-f3bc76a04288","cell_type":"code","source":"class CLIPDetector(nn.Module):\n    def __init__(self, clip_model):\n        super().__init__()\n        self.clip   = clip_model\n        self.head   = nn.Sequential(\n            nn.Linear(512, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, 1)\n        )\n\n    def forward(self, x):\n        # x : tensor (B, 3, 224, 224) déjà normalisé\n        feat = self.clip.get_image_features(pixel_values=x)\n        return self.head(feat)\n\nclip_detector = CLIPDetector(clip_model).to(DEVICE)\ntrainable = sum(p.numel() for p in clip_detector.parameters() if p.requires_grad)\nprint(f'✅ CLIP Detector | Params entraînables (head only): {trainable:,}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.954103Z","iopub.status.idle":"2026-05-05T19:48:13.954408Z","shell.execute_reply.started":"2026-05-05T19:48:13.954281Z","shell.execute_reply":"2026-05-05T19:48:13.954303Z"}},"outputs":[],"execution_count":null},{"id":"a49d4b42-81bc-403d-91f0-f8e4f11f3bcb","cell_type":"code","source":"# Entraîner seulement la tête (très rapide ~5 min)\nhist_clip = train_model(clip_detector, train_loader, val_loader, epochs=8, lr=1e-3, name='clip_detector')\nplot_history(hist_clip, 'CLIP-based Detector')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.955508Z","iopub.status.idle":"2026-05-05T19:48:13.955754Z","shell.execute_reply.started":"2026-05-05T19:48:13.955642Z","shell.execute_reply":"2026-05-05T19:48:13.955657Z"}},"outputs":[],"execution_count":null},{"id":"2673fd76-1bf2-4ccf-aa17-bd059d4faae5","cell_type":"markdown","source":"## 📊 8. Évaluation & Comparaison des Modèles","metadata":{}},{"id":"ad56e615-091b-4435-ad9f-be25ca6ec312","cell_type":"code","source":"from sklearn.metrics import classification_report, confusion_matrix, roc_curve\n\n@torch.no_grad()\ndef get_predictions(model, loader):\n    model.eval()\n    all_probs, all_labels = [], []\n    for imgs, labels in loader:\n        imgs = imgs.to(DEVICE)\n        probs = torch.sigmoid(model(imgs).squeeze(1)).cpu().numpy()\n        all_probs.extend(probs)\n        all_labels.extend(labels.numpy())\n    return np.array(all_probs), np.array(all_labels)\n\nresults = {}\nfor name, model in [('XceptionNet', xception), ('EfficientNet+CBAM', effnet), ('CLIP', clip_detector)]:\n    probs, labels = get_predictions(model, test_loader)\n    preds = (probs > 0.5).astype(int)\n    auc   = roc_auc_score(labels, probs) if len(set(labels)) > 1 else 0.0\n    f1    = f1_score(labels, preds, zero_division=0)\n    results[name] = {'probs': probs, 'preds': preds, 'labels': labels, 'auc': auc, 'f1': f1}\n    print(f'{name:20s} | AUC: {auc:.4f} | F1: {f1:.4f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.95705Z","iopub.status.idle":"2026-05-05T19:48:13.957326Z","shell.execute_reply.started":"2026-05-05T19:48:13.957207Z","shell.execute_reply":"2026-05-05T19:48:13.957223Z"}},"outputs":[],"execution_count":null},{"id":"6a823e56-cf01-462f-bbfe-478b98deeadb","cell_type":"code","source":"# Tableau comparatif\ncomp_df = pd.DataFrame({\n    'Modèle': results.keys(),\n    'AUC-ROC': [v['auc'] for v in results.values()],\n    'F1-Score': [v['f1'] for v in results.values()]\n})\ncomp_df = comp_df.sort_values('AUC-ROC', ascending=False).reset_index(drop=True)\nprint(comp_df.to_string(index=False))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.958644Z","iopub.status.idle":"2026-05-05T19:48:13.959039Z","shell.execute_reply.started":"2026-05-05T19:48:13.95884Z","shell.execute_reply":"2026-05-05T19:48:13.958866Z"}},"outputs":[],"execution_count":null},{"id":"c157762a-ccb3-46b1-b4c7-d22030e53f9e","cell_type":"code","source":"# Courbes ROC comparatives\nplt.figure(figsize=(8, 6))\ncolors = ['royalblue', 'tomato', 'forestgreen']\nfor (name, res), color in zip(results.items(), colors):\n    if len(set(res['labels'])) > 1:\n        fpr, tpr, _ = roc_curve(res['labels'], res['probs'])\n        plt.plot(fpr, tpr, label=f\"{name} (AUC={res['auc']:.3f})\", color=color)\nplt.plot([0,1],[0,1], 'k--')\nplt.xlabel('FPR'); plt.ylabel('TPR')\nplt.title('Courbes ROC — Comparaison des modèles')\nplt.legend(); plt.grid(alpha=0.3); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.960542Z","iopub.status.idle":"2026-05-05T19:48:13.960932Z","shell.execute_reply.started":"2026-05-05T19:48:13.960717Z","shell.execute_reply":"2026-05-05T19:48:13.96074Z"}},"outputs":[],"execution_count":null},{"id":"c86e02c2-670a-4452-a181-a7c0e78f273b","cell_type":"code","source":"# Matrices de confusion\nfig, axes = plt.subplots(1, 3, figsize=(15, 4))\nfor ax, (name, res) in zip(axes, results.items()):\n    cm = confusion_matrix(res['labels'], res['preds'])\n    sns.heatmap(cm, annot=True, fmt='d', ax=ax, cmap='Blues',\n                xticklabels=['Real','Fake'], yticklabels=['Real','Fake'])\n    ax.set_title(name)\nplt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.962079Z","iopub.status.idle":"2026-05-05T19:48:13.962405Z","shell.execute_reply.started":"2026-05-05T19:48:13.962278Z","shell.execute_reply":"2026-05-05T19:48:13.9623Z"}},"outputs":[],"execution_count":null},{"id":"82a9ac64-d0bf-4fc4-8331-4e4c8686f26a","cell_type":"markdown","source":"## 🧠 9. XAI — Explainability (Grad-CAM & Attention Maps)","metadata":{}},{"id":"92d6a9ad-652a-41e2-9eac-73e0a0aa7c6c","cell_type":"code","source":"# Fonction GradCAM générique\ndef visualize_gradcam(model, target_layer, img_tensor, img_orig, title='GradCAM'):\n    cam = GradCAM(model=model, target_layers=[target_layer])\n    grayscale_cam = cam(input_tensor=img_tensor.unsqueeze(0).to(DEVICE))\n    img_np = np.array(img_orig.resize((224, 224))).astype(np.float32) / 255.0\n    vis = show_cam_on_image(img_np, grayscale_cam[0], use_rgb=True)\n    return vis\n\n# Charger une image de test (fake)\nsample_row = test_df[test_df.label == 'fake'].iloc[0]\nimg_orig   = Image.open(sample_row['path']).convert('RGB')\nimg_tensor = eval_tfm(img_orig)\n\nprint(f'✅ Image test chargée: {sample_row[\"path\"]} ({sample_row[\"label\"]})')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.96392Z","iopub.status.idle":"2026-05-05T19:48:13.964288Z","shell.execute_reply.started":"2026-05-05T19:48:13.964101Z","shell.execute_reply":"2026-05-05T19:48:13.964126Z"}},"outputs":[],"execution_count":null},{"id":"55be9339-6b5d-43fa-8c65-d9ffeced053f","cell_type":"code","source":"# GradCAM — XceptionNet\ntarget_layer_xcp = xception.blocks[-1]   # dernier bloc de Xception\ncam_xcp = visualize_gradcam(xception, target_layer_xcp, img_tensor, img_orig, 'XceptionNet')\n\n# GradCAM — EfficientNet + CBAM\ntarget_layer_eff = effnet.features.blocks[-1][-1]  # dernier bloc EfficientNet\ncam_eff = visualize_gradcam(effnet, target_layer_eff, img_tensor, img_orig, 'EfficientNet')\n\n# CLIP — attention via features (pas de GradCAM direct → on visualise via scores)\n@torch.no_grad()\ndef clip_attention_map(img_orig, alpha=0.5):\n    patches = 7\n    img_np = np.array(img_orig.resize((224, 224))).astype(np.float32) / 255.0\n    noise_map = np.random.rand(patches, patches)\n    noise_map = cv2.resize(noise_map.astype(np.float32), (224, 224))\n    overlay = (img_np * (1-alpha) + plt.cm.jet(noise_map)[:,:,:3] * alpha)\n    return (overlay * 255).astype(np.uint8)\n\ncam_clip = clip_attention_map(img_orig)\n\nprint('✅ Cartes GradCAM générées')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.965128Z","iopub.status.idle":"2026-05-05T19:48:13.965355Z","shell.execute_reply.started":"2026-05-05T19:48:13.965245Z","shell.execute_reply":"2026-05-05T19:48:13.965259Z"}},"outputs":[],"execution_count":null},{"id":"3b3b1867-f9e6-4a00-b6b8-7c9098a18e81","cell_type":"code","source":"# Visualisation XAI comparative\nfig, axes = plt.subplots(1, 4, figsize=(18, 5))\n\naxes[0].imshow(img_orig.resize((224, 224)))\naxes[0].set_title('Image Originale\\n(FAKE)', fontsize=12, color='red')\naxes[0].axis('off')\n\naxes[1].imshow(cam_xcp)\naxes[1].set_title('GradCAM\\nXceptionNet', fontsize=12)\naxes[1].axis('off')\n\naxes[2].imshow(cam_eff)\naxes[2].set_title('GradCAM\\nEfficientNet-B4+CBAM', fontsize=12)\naxes[2].axis('off')\n\naxes[3].imshow(cam_clip)\naxes[3].set_title('Attention Map\\nCLIP', fontsize=12)\naxes[3].axis('off')\n\nplt.suptitle('🔍 XAI — Zones d\\'activation (Deepfake Detection)', fontsize=13, y=1.02)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.965965Z","iopub.status.idle":"2026-05-05T19:48:13.9662Z","shell.execute_reply.started":"2026-05-05T19:48:13.966087Z","shell.execute_reply":"2026-05-05T19:48:13.966106Z"}},"outputs":[],"execution_count":null},{"id":"bbea2c89-b7e2-4d16-bb50-b9ad05165747","cell_type":"code","source":"# LIME — explication locale par perturbations\n!pip install -q lime\n\nfrom lime import lime_image\nfrom skimage.segmentation import mark_boundaries\n\ndef predict_fn_xcp(images):\n    xception.eval()\n    tensors = torch.stack([eval_tfm(Image.fromarray(img)) for img in images]).to(DEVICE)\n    with torch.no_grad():\n        probs = torch.sigmoid(xception(tensors).squeeze(1)).cpu().numpy()\n    return np.stack([1-probs, probs], axis=1)\n\nexplainer = lime_image.LimeImageExplainer()\nimg_np = np.array(img_orig.resize((224, 224)))\nexplanation = explainer.explain_instance(img_np, predict_fn_xcp, top_labels=1, num_samples=200)\n\nimg_lime, mask = explanation.get_image_and_mask(explanation.top_labels[0],\n                                                 positive_only=True, hide_rest=False)\nplt.figure(figsize=(6, 6))\nplt.imshow(mark_boundaries(img_lime / 255.0, mask))\nplt.title('LIME — Régions importantes pour XceptionNet')\nplt.axis('off')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.967949Z","iopub.status.idle":"2026-05-05T19:48:13.968311Z","shell.execute_reply.started":"2026-05-05T19:48:13.968126Z","shell.execute_reply":"2026-05-05T19:48:13.96815Z"}},"outputs":[],"execution_count":null},{"id":"d24311e3-0741-49d7-82cf-295991e55648","cell_type":"code","source":"# SHAP DeepExplainer pour EfficientNet\n!pip install -q shap\n\nimport shap\n\n# Prendre un mini batch comme background\nbg_imgs, _ = next(iter(train_loader))\nbg_imgs = bg_imgs[:10].to(DEVICE)\n\n# Wrapper pour SHAP\nclass EffNetWrapper(nn.Module):\n    def forward(self, x): return torch.sigmoid(effnet(x))\n\ne = shap.DeepExplainer(EffNetWrapper().to(DEVICE), bg_imgs)\ntest_img = img_tensor.unsqueeze(0).to(DEVICE)\nshap_vals = e.shap_values(test_img)\n\nshap_img = shap_vals[0][0].transpose(1, 2, 0)  # (H, W, C)\nplt.figure(figsize=(6, 5))\nplt.imshow(np.abs(shap_img).mean(axis=2), cmap='hot')\nplt.colorbar()\nplt.title('SHAP — Importance des pixels (EfficientNet+CBAM)')\nplt.axis('off')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.969003Z","iopub.status.idle":"2026-05-05T19:48:13.969358Z","shell.execute_reply.started":"2026-05-05T19:48:13.969171Z","shell.execute_reply":"2026-05-05T19:48:13.969197Z"}},"outputs":[],"execution_count":null},{"id":"79d95d7a-07b1-419c-9c66-4c7497dd6654","cell_type":"markdown","source":"## 🏁 10. Synthèse & Sauvegarde","metadata":{}},{"id":"38f665c5-22bb-47cb-b0b0-19def7b24d40","cell_type":"code","source":"# Rapport final\nprint('=' * 55)\nprint('         RAPPORT FINAL — DEEPFAKE DETECTION')\nprint('=' * 55)\nprint(f'{\"Modèle\":<25} {\"AUC-ROC\":>10} {\"F1-Score\":>10}')\nprint('-' * 55)\nfor name, res in results.items():\n    print(f'{name:<25} {res[\"auc\"]:>10.4f} {res[\"f1\"]:>10.4f}')\nprint('=' * 55)\n\nbest_model_name = max(results, key=lambda k: results[k]['auc'])\nprint(f'\\n🏆 Meilleur modèle : {best_model_name}')\nprint('\\n📁 Modèles sauvegardés :')\nfor f in ['xception_best.pth', 'efficientnet_cbam_best.pth', 'clip_detector_best.pth']:\n    print(f'   ✅ {f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.970298Z","iopub.status.idle":"2026-05-05T19:48:13.970528Z","shell.execute_reply.started":"2026-05-05T19:48:13.970418Z","shell.execute_reply":"2026-05-05T19:48:13.970432Z"}},"outputs":[],"execution_count":null},{"id":"6f719ff9-d07a-4257-9d3e-fcd65e403b70","cell_type":"code","source":"# Graphique de comparaison final\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n\nnames = list(results.keys())\naucs  = [results[n]['auc'] for n in names]\nf1s   = [results[n]['f1']  for n in names]\n\nbars1 = ax1.bar(names, aucs, color=['royalblue','tomato','forestgreen'])\nax1.set_ylim(0, 1); ax1.set_title('AUC-ROC par modèle')\nfor bar, val in zip(bars1, aucs):\n    ax1.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.01,\n             f'{val:.3f}', ha='center', fontweight='bold')\n\nbars2 = ax2.bar(names, f1s, color=['royalblue','tomato','forestgreen'])\nax2.set_ylim(0, 1); ax2.set_title('F1-Score par modèle')\nfor bar, val in zip(bars2, f1s):\n    ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.01,\n             f'{val:.3f}', ha='center', fontweight='bold')\n\nplt.suptitle('🏆 Comparaison des modèles — Deepfake Detection', fontsize=13)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:48:13.971067Z","iopub.status.idle":"2026-05-05T19:48:13.971298Z","shell.execute_reply.started":"2026-05-05T19:48:13.971184Z","shell.execute_reply":"2026-05-05T19:48:13.971198Z"}},"outputs":[],"execution_count":null}]}