{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.10.0"},"accelerator":"GPU","kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":14774,"databundleVersionId":875431,"isSourceIdPinned":false}],"dockerImageVersionId":31331,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"0","cell_type":"markdown","source":"# AMCA-ViT : Vision Transformer pour APTOS 2019\n## Diabetic Retinopathy Binary Classification\n### Architecture : ViT-B/16 (timm) + stratégie AMCA\n> **ResNet50 remplacé par Vision Transformer** avec gel adaptatif des blocs Transformer selon AMCA","metadata":{}},{"id":"1","cell_type":"code","source":"# ============================================================\n# Install dependencies (Kaggle environment)\n# ============================================================\n!pip install timm transformers -q\nprint(\"Packages installed.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-05T00:53:18.511052Z","iopub.execute_input":"2026-04-05T00:53:18.511682Z","iopub.status.idle":"2026-04-05T00:53:21.943944Z","shell.execute_reply.started":"2026-04-05T00:53:18.511655Z","shell.execute_reply":"2026-04-05T00:53:21.943124Z"}},"outputs":[],"execution_count":null},{"id":"2","cell_type":"code","source":"# ============================================================\n# Imports\n# ============================================================\nimport os, random, warnings\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom PIL import Image\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom sklearn.metrics import classification_report, confusion_matrix, roc_auc_score\nimport timm\n\nwarnings.filterwarnings(\"ignore\")\n\nSEED = 42\nrandom.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)\nif torch.cuda.is_available():\n    torch.cuda.manual_seed_all(SEED)\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Device: {DEVICE}\")\nif torch.cuda.is_available():\n    print(f\"GPU: {torch.cuda.get_device_name(0)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-05T00:53:21.945807Z","iopub.execute_input":"2026-04-05T00:53:21.946171Z","iopub.status.idle":"2026-04-05T00:53:21.954368Z","shell.execute_reply.started":"2026-04-05T00:53:21.946140Z","shell.execute_reply":"2026-04-05T00:53:21.953767Z"}},"outputs":[],"execution_count":null},{"id":"3","cell_type":"code","source":"# ============================================================\n# Configuration Kaggle APTOS 2019\n# ============================================================\n\nDATA_DIR   = \"/kaggle/input/competitions/aptos2019-blindness-detection\"\nCSV_PATH   = os.path.join(DATA_DIR, \"train.csv\")\nIMAGE_DIR  = os.path.join(DATA_DIR, \"train_images\")\nOUTPUT_DIR = \"/kaggle/working\"\n\n# ViT settings\nIMG_SIZE       = 224        # ViT-B/16 native\nBATCH_SIZE     = 16\nVIT_TIMM_NAME  = \"vit_base_patch16_224\"   # pretrained ImageNet-21k\n\n# Training epochs per phase\nEPOCHS_PHASE1 = 8   # Head + AMCA frozen blocks\nEPOCHS_PHASE2 = 8   # Unfreeze last 6 blocks\nEPOCHS_PHASE3 = 6   # Full fine-tuning\n\n# AMCA parameters\nCOMPUTE_BUDGET     = \"medium\"   # very_low / low / medium / high\nTARGET_PERFORMANCE = \"high\"     # low / medium / high / very_high\n\nUSE_CLASS_WEIGHTS = True\nUSE_AMP           = True        # Automatic Mixed Precision\n\nprint(\"Config OK\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-05T00:53:21.955192Z","iopub.execute_input":"2026-04-05T00:53:21.955442Z","iopub.status.idle":"2026-04-05T00:53:21.968239Z","shell.execute_reply.started":"2026-04-05T00:53:21.955415Z","shell.execute_reply":"2026-04-05T00:53:21.967554Z"}},"outputs":[],"execution_count":null},{"id":"4","cell_type":"code","source":"# ============================================================\n# Reduced AMCA adapté Vision Transformer\n# ViT-B/16 = 12 blocs Transformer\n# La strategie AMCA determine combien de blocs sont geles\n# ============================================================\n\nclass ReducedAMCA_ViT:\n    TOTAL_BLOCKS = 12  # ViT-B/16\n\n    def __init__(self, problem, constraints, requirements):\n        self.problem      = problem\n        self.constraints  = constraints\n        self.requirements = requirements\n\n    def _complexity(self):\n        nc  = self.problem.get(\"num_classes\", 2)\n        var = self.problem.get(\"intra_class_variability\", \"medium\")\n        if nc <= 2 and var == \"low\":   return \"simple\"\n        if nc <= 5 and var != \"high\":  return \"moderate\"\n        return \"complex\"\n\n    def strategy(self):\n        c = self.constraints.get(\"compute_budget\", \"medium\")\n        p = self.requirements.get(\"target_performance\", \"high\")\n        complexity = self._complexity()\n\n        # --- Blocs geles (cutting point ViT) ---\n        if c == \"very_low\":     frozen = 11\n        elif c == \"low\":        frozen = 10\n        elif c == \"medium\":     frozen = 6 if p in [\"high\",\"very_high\"] else 8\n        else:                   frozen = 4   # high compute\n\n        # --- Tete de classification ---\n        if complexity == \"simple\":\n            head = {\"units\": [256],       \"dropout\": [0.30]}\n        elif complexity == \"moderate\":\n            head = {\"units\": [512, 256],  \"dropout\": [0.30, 0.25]}\n        else:\n            head = {\"units\": [1024, 512], \"dropout\": [0.40, 0.30]}\n\n        # --- Learning rates ---\n        if complexity == \"simple\":\n            lr = {\"phase1\": 1e-3, \"phase2\": 5e-4, \"phase3\": 1e-5}\n        elif complexity == \"moderate\":\n            lr = {\"phase1\": 5e-4, \"phase2\": 1e-4, \"phase3\": 1e-5}\n        else:\n            lr = {\"phase1\": 1e-4, \"phase2\": 5e-5, \"phase3\": 1e-6}\n\n        # --- Nombre de phases ---\n        if c == \"very_low\" or (complexity == \"simple\" and p in [\"low\",\"medium\"]):\n            phases = 1\n        elif complexity == \"moderate\":\n            phases = 2\n        else:\n            phases = 3\n\n        return {\n            \"frozen_blocks\": frozen,\n            \"head\"         : head,\n            \"lr\"           : lr,\n            \"phases\"       : phases,\n            \"complexity\"   : complexity,\n        }\n\namca = ReducedAMCA_ViT(\n    problem      = {\"num_classes\": 2, \"intra_class_variability\": \"medium\"},\n    constraints  = {\"compute_budget\": COMPUTE_BUDGET},\n    requirements = {\"target_performance\": TARGET_PERFORMANCE},\n)\nSTRATEGY = amca.strategy()\nprint(\"=== Strategie AMCA-ViT ===\")\nfor k, v in STRATEGY.items():\n    print(f\"  {k}: {v}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-05T00:53:21.969820Z","iopub.execute_input":"2026-04-05T00:53:21.970127Z","iopub.status.idle":"2026-04-05T00:53:21.981177Z","shell.execute_reply.started":"2026-04-05T00:53:21.970106Z","shell.execute_reply":"2026-04-05T00:53:21.980642Z"}},"outputs":[],"execution_count":null},{"id":"5","cell_type":"code","source":"# ============================================================\n# Dataset APTOS 2019 (PyTorch)\n# ============================================================\n\ndef make_binary(x):\n    return 0 if int(x) == 0 else 1\n\ndef load_df():\n    df = pd.read_csv(CSV_PATH)\n    df[\"label\"] = df[\"diagnosis\"].apply(make_binary)\n    df[\"image_path\"] = df[\"id_code\"].apply(\n        lambda x: os.path.join(IMAGE_DIR, f\"{x}.png\"))\n    df = df[df[\"image_path\"].apply(os.path.exists)].reset_index(drop=True)\n    print(f\"Samples valides : {len(df)}\")\n    print(df[\"label\"].value_counts().to_string())\n    return df\n\n# ViT utilise normalisation [-1, 1]\nVIT_MEAN = [0.5, 0.5, 0.5]\nVIT_STD  = [0.5, 0.5, 0.5]\n\ntrain_transform = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1),\n    transforms.RandomRotation(15),\n    transforms.ToTensor(),\n    transforms.Normalize(VIT_MEAN, VIT_STD),\n])\n\nval_transform = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(VIT_MEAN, VIT_STD),\n])\n\nclass APTOSDataset(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):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row   = self.df.iloc[idx]\n        image = Image.open(row[\"image_path\"]).convert(\"RGB\")\n        if self.transform:\n            image = self.transform(image)\n        return image, torch.tensor(row[\"label\"], dtype=torch.float32)\n\ndf       = load_df()\ntrain_df, val_df = train_test_split(\n    df, test_size=0.2, stratify=df[\"label\"], random_state=SEED)\ntrain_df = train_df.reset_index(drop=True)\nval_df   = val_df.reset_index(drop=True)\n\ntrain_loader = DataLoader(\n    APTOSDataset(train_df, train_transform),\n    batch_size=BATCH_SIZE, shuffle=True,  num_workers=2, pin_memory=True,\n    drop_last=True)   # Evite les batches de taille 1 en fin d'epoch\nval_loader = DataLoader(\n    APTOSDataset(val_df, val_transform),\n    batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n\nprint(f\"Train: {len(train_df)} | Val: {len(val_df)}\")\nprint(f\"Batches train: {len(train_loader)} | val: {len(val_loader)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-05T00:53:21.982004Z","iopub.execute_input":"2026-04-05T00:53:21.982186Z","iopub.status.idle":"2026-04-05T00:53:23.606073Z","shell.execute_reply.started":"2026-04-05T00:53:21.982169Z","shell.execute_reply":"2026-04-05T00:53:23.605337Z"}},"outputs":[],"execution_count":null},{"id":"6","cell_type":"code","source":"# ============================================================\n# Modele AMCA-ViT\n# Backbone : ViT-B/16 (timm, pretrained ImageNet-21k -> 1k)\n# Gel adaptatif des blocs Transformer via AMCA\n# ============================================================\n\nclass AMCA_ViT(nn.Module):\n    def __init__(self, strategy, num_classes=1):\n        super().__init__()\n\n        # 1. Charger ViT-B/16 sans tete\n        self.backbone = timm.create_model(\n            VIT_TIMM_NAME,\n            pretrained  = True,\n            num_classes = 0,\n            global_pool = \"token\"   # CLS token en sortie\n        )\n        embed_dim = self.backbone.embed_dim  # 768\n\n        # 2. Gel AMCA\n        self._apply_amca_freezing(strategy[\"frozen_blocks\"])\n\n        # 3. Tete de classification\n        head_cfg = strategy[\"head\"]\n        layers   = []\n        in_dim   = embed_dim\n        for units, drop in zip(head_cfg[\"units\"], head_cfg[\"dropout\"]):\n            layers += [\n                nn.Linear(in_dim, units),\n                nn.LayerNorm(units),   # LayerNorm : fonctionne avec batch_size=1\n                nn.GELU(),\n                nn.Dropout(drop),\n            ]\n            in_dim = units\n        layers.append(nn.Linear(in_dim, num_classes))\n        self.head = nn.Sequential(*layers)\n\n        print(f\"AMCA-ViT charge | blocs geles: {strategy['frozen_blocks']}/12\")\n        self._print_trainable()\n\n    def _apply_amca_freezing(self, frozen_blocks):\n        # Geler patch embedding & position embedding\n        for p in self.backbone.patch_embed.parameters():\n            p.requires_grad = False\n        self.backbone.pos_embed.requires_grad = False\n        self.backbone.cls_token.requires_grad = False\n\n        # Geler les N premiers blocs Transformer\n        for i, block in enumerate(self.backbone.blocks):\n            freeze = i < frozen_blocks\n            for p in block.parameters():\n                p.requires_grad = not freeze\n\n        # LayerNorm finale toujours entrainable\n        for p in self.backbone.norm.parameters():\n            p.requires_grad = True\n\n    def unfreeze_last_n_blocks(self, n):\n        total = len(self.backbone.blocks)\n        start = total - n\n        for i, block in enumerate(self.backbone.blocks):\n            if i >= start:\n                for p in block.parameters():\n                    p.requires_grad = True\n        print(f\"Blocs {start}..{total-1} degeles\")\n        self._print_trainable()\n\n    def unfreeze_all(self):\n        for p in self.backbone.parameters():\n            p.requires_grad = True\n        print(\"Backbone entierement degele\")\n        self._print_trainable()\n\n    def _print_trainable(self):\n        total     = sum(p.numel() for p in self.parameters())\n        trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)\n        print(f\"  Params : {total:,} total | {trainable:,} entrainables ({100*trainable/total:.1f}%)\")\n\n    def forward(self, x):\n        feat = self.backbone(x)           # (B, 768)\n        return self.head(feat).squeeze(1) # (B,)\n\nmodel = AMCA_ViT(STRATEGY).to(DEVICE)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-05T00:53:23.607153Z","iopub.execute_input":"2026-04-05T00:53:23.607697Z","iopub.status.idle":"2026-04-05T00:53:25.160245Z","shell.execute_reply.started":"2026-04-05T00:53:23.607670Z","shell.execute_reply":"2026-04-05T00:53:25.159652Z"}},"outputs":[],"execution_count":null},{"id":"7","cell_type":"code","source":"# ============================================================\n# Boucle d'entrainement\n# ============================================================\nfrom torch.cuda.amp import GradScaler, autocast\n\ndef make_optimizer_scheduler(model, lr, steps, epochs):\n    head_params     = list(model.head.parameters())\n    backbone_params = [p for p in model.backbone.parameters() if p.requires_grad]\n    param_groups    = [\n        {\"params\": backbone_params, \"lr\": lr / 10},   # LR reduit pour backbone\n        {\"params\": head_params,     \"lr\": lr},\n    ]\n    opt  = torch.optim.AdamW(param_groups, weight_decay=1e-4)\n    sched = torch.optim.lr_scheduler.CosineAnnealingLR(\n        opt, T_max=epochs * steps, eta_min=lr / 100)\n    return opt, sched\n\ndef train_epoch(model, loader, opt, sched, crit, scaler):\n    model.train()\n    total_loss, correct, total = 0.0, 0, 0\n    for imgs, labels in loader:\n        imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)\n        opt.zero_grad()\n        with autocast(enabled=USE_AMP):\n            logits = model(imgs)\n            loss   = crit(logits, labels)\n        scaler.scale(loss).backward()\n        scaler.unscale_(opt)\n        nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n        scaler.step(opt); scaler.update(); sched.step()\n        total_loss += loss.item() * len(labels)\n        correct    += ((torch.sigmoid(logits) >= 0.5).float() == labels).sum().item()\n        total      += len(labels)\n    return total_loss / total, correct / total\n\n@torch.no_grad()\ndef val_epoch(model, loader, crit):\n    model.eval()\n    total_loss, preds_all, labels_all = 0.0, [], []\n    total = 0\n    for imgs, labels in loader:\n        imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)\n        with autocast(enabled=USE_AMP):\n            logits = model(imgs)\n            loss   = crit(logits, labels)\n        total_loss += loss.item() * len(labels)\n        preds_all.extend(torch.sigmoid(logits).cpu().numpy())\n        labels_all.extend(labels.cpu().numpy())\n        total += len(labels)\n    p = np.array(preds_all); l = np.array(labels_all)\n    acc = ((p >= 0.5).astype(int) == l).mean()\n    try:    auc = roc_auc_score(l, p)\n    except: auc = 0.0\n    return total_loss / total, acc, auc\n\ndef run_phase(model, phase_num, epochs, lr, best_auc, save_path):\n    print(f\"\\n{'='*55}\")\n    print(f\"  PHASE {phase_num}  |  LR={lr}  |  Epochs={epochs}\")\n    print(f\"{'='*55}\")\n    scaler     = GradScaler(enabled=USE_AMP)\n    opt, sched = make_optimizer_scheduler(model, lr, len(train_loader), epochs)\n    for ep in range(1, epochs + 1):\n        tl, ta = train_epoch(model, train_loader, opt, sched, criterion, scaler)\n        vl, va, vauc = val_epoch(model, val_loader, criterion)\n        marker = \"\"\n        if vauc > best_auc:\n            best_auc = vauc\n            torch.save(model.state_dict(), save_path)\n            marker = \"  [SAVED]\"\n        print(f\"Ep {ep:02d}/{epochs} | \"\n              f\"train loss={tl:.4f} acc={ta:.4f} | \"\n              f\"val loss={vl:.4f} acc={va:.4f} AUC={vauc:.4f}{marker}\")\n    return best_auc\n\nprint(\"Fonctions prets.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-05T00:53:25.161202Z","iopub.execute_input":"2026-04-05T00:53:25.161409Z","iopub.status.idle":"2026-04-05T00:53:25.174231Z","shell.execute_reply.started":"2026-04-05T00:53:25.161390Z","shell.execute_reply":"2026-04-05T00:53:25.173717Z"}},"outputs":[],"execution_count":null},{"id":"8","cell_type":"code","source":"# ============================================================\n# Class weights & criterion\n# ============================================================\n\nif USE_CLASS_WEIGHTS:\n    classes = np.array(sorted(train_df[\"label\"].unique()))\n    weights = compute_class_weight(\"balanced\", classes=classes, y=train_df[\"label\"].values)\n    pos_weight = torch.tensor([weights[1] / weights[0]], dtype=torch.float32).to(DEVICE)\n    criterion  = nn.BCEWithLogitsLoss(pos_weight=pos_weight)\n    print(f\"Class weights -> 0: {weights[0]:.3f} | 1: {weights[1]:.3f}\")\n    print(f\"pos_weight: {pos_weight.item():.3f}\")\nelse:\n    criterion = nn.BCEWithLogitsLoss()\n\nSAVE_PATH = os.path.join(OUTPUT_DIR, \"amca_vit_best.pth\")\nbest_auc  = 0.0\nlr_cfg    = STRATEGY[\"lr\"]\nphases    = STRATEGY[\"phases\"]\nprint(f\"Phases prevues : {phases}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-05T00:53:25.175148Z","iopub.execute_input":"2026-04-05T00:53:25.175629Z","iopub.status.idle":"2026-04-05T00:53:25.191051Z","shell.execute_reply.started":"2026-04-05T00:53:25.175607Z","shell.execute_reply":"2026-04-05T00:53:25.190423Z"}},"outputs":[],"execution_count":null},{"id":"9","cell_type":"code","source":"# ============================================================\n# Entrainement multi-phases AMCA-ViT\n# ============================================================\n\n# Phase 1 : Tete + blocs Transformer degeles par AMCA\nbest_auc = run_phase(model, 1, EPOCHS_PHASE1, lr_cfg[\"phase1\"], best_auc, SAVE_PATH)\n\n# Phase 2 : Degeler les 6 derniers blocs\nif phases >= 2:\n    model.unfreeze_last_n_blocks(6)\n    best_auc = run_phase(model, 2, EPOCHS_PHASE2, lr_cfg[\"phase2\"], best_auc, SAVE_PATH)\n\n# Phase 3 : Fine-tuning complet\nif phases >= 3:\n    model.unfreeze_all()\n    best_auc = run_phase(model, 3, EPOCHS_PHASE3, lr_cfg[\"phase3\"], best_auc, SAVE_PATH)\n\nprint(f\"\\nMeilleur AUC : {best_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-05T00:53:25.191724Z","iopub.execute_input":"2026-04-05T00:53:25.191966Z","iopub.status.idle":"2026-04-05T00:59:47.032748Z","shell.execute_reply.started":"2026-04-05T00:53:25.191943Z","shell.execute_reply":"2026-04-05T00:59:47.030244Z"}},"outputs":[],"execution_count":null},{"id":"10","cell_type":"code","source":"# ============================================================\n# Evaluation finale (meilleur checkpoint)\n# ============================================================\n\nmodel.load_state_dict(torch.load(SAVE_PATH, map_location=DEVICE))\nmodel.eval()\n\nall_preds, all_labels = [], []\nwith torch.no_grad():\n    for imgs, labels in val_loader:\n        imgs = imgs.to(DEVICE)\n        with autocast(enabled=USE_AMP):\n            logits = model(imgs)\n        all_preds.extend(torch.sigmoid(logits).cpu().numpy())\n        all_labels.extend(labels.numpy())\n\ny_true  = np.array(all_labels)\ny_score = np.array(all_preds)\ny_pred  = (y_score >= 0.5).astype(int)\n\nprint(\"\\n=== Rapport de classification ===\")\nprint(classification_report(y_true, y_pred, target_names=[\"No DR\", \"DR\"], digits=4))\ncm = confusion_matrix(y_true, y_pred)\nprint(\"Confusion Matrix:\")\nprint(cm)\nauc_final = roc_auc_score(y_true, y_score)\nprint(f\"\\nROC AUC final : {auc_final:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-05T00:59:47.034084Z","iopub.status.idle":"2026-04-05T00:59:47.034511Z","shell.execute_reply.started":"2026-04-05T00:59:47.034295Z","shell.execute_reply":"2026-04-05T00:59:47.034322Z"}},"outputs":[],"execution_count":null},{"id":"11","cell_type":"code","source":"# ============================================================\n# Visualisation\n# ============================================================\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nfig, axes = plt.subplots(1, 2, figsize=(13, 5))\n\n# Confusion matrix\nsns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\",\n            xticklabels=[\"No DR\", \"DR\"],\n            yticklabels=[\"No DR\", \"DR\"], ax=axes[0])\naxes[0].set_title(\"Matrice de Confusion\")\naxes[0].set_ylabel(\"Vrai label\"); axes[0].set_xlabel(\"Predit\")\n\n# Score distribution\naxes[1].hist(y_score[y_true==0], bins=30, alpha=0.65, label=\"No DR\", color=\"steelblue\")\naxes[1].hist(y_score[y_true==1], bins=30, alpha=0.65, label=\"DR\",    color=\"tomato\")\naxes[1].axvline(0.5, color=\"black\", ls=\"--\", label=\"Seuil=0.5\")\naxes[1].set_title(\"Distribution des scores\")\naxes[1].set_xlabel(\"Score sigmoid\"); axes[1].set_ylabel(\"Count\")\naxes[1].legend()\n\nplt.suptitle(f\"AMCA-ViT (ViT-B/16) | AUC = {auc_final:.4f}\", fontsize=13, fontweight=\"bold\")\nplt.tight_layout()\nout_fig = os.path.join(OUTPUT_DIR, \"amca_vit_results.png\")\nplt.savefig(out_fig, dpi=150, bbox_inches=\"tight\")\nplt.show()\nprint(f\"Figure sauvegardee : {out_fig}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-05T00:59:47.038626Z","iopub.status.idle":"2026-04-05T00:59:47.039023Z","shell.execute_reply.started":"2026-04-05T00:59:47.038813Z","shell.execute_reply":"2026-04-05T00:59:47.038834Z"}},"outputs":[],"execution_count":null}]}