{"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}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"0","cell_type":"markdown","source":"# 🆕 NEW AMCA-VGG19 : Automated Model Cutting Algorithm (Version Complète)\n## APTOS 2019 — Diabetic Retinopathy Binary Classification\n### Nouveautés vs AMCA classique :\n> - **Analyse automatique des données** (imbalance, variabilité, complexité réelle)\n> - **Recherche automatique du cutting point** (linear probe sur chaque bloc)\n> - **Gradient flow analysis** (gel adaptatif basé sur les gradients réels)\n> - **Sélection automatique de la tête** (unités Dense adaptées à la sortie du bloc)\n> - **3 phases** : 8 / 8 / 6 epochs | Loss, Accuracy, FLOPs, Per-layer dashboard","metadata":{}},{"id":"1","cell_type":"code","source":"# ============================================================\n# Install\n# ============================================================\n!pip install fvcore -q\nprint(\"Packages OK\")","metadata":{},"outputs":[],"execution_count":null},{"id":"2","cell_type":"code","source":"# ============================================================\n# Imports\n# ============================================================\nimport os, random, warnings, time, math\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader, Subset\nfrom torchvision import transforms, models\nfrom PIL import Image\nfrom collections import defaultdict\n\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,\n                              roc_auc_score, accuracy_score)\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.preprocessing import StandardScaler\n\nimport matplotlib.pyplot as plt\nimport matplotlib.gridspec as gridspec\nimport matplotlib.patches as mpatches\nimport seaborn as sns\n\nwarnings.filterwarnings(\"ignore\")\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":{},"outputs":[],"execution_count":null},{"id":"3","cell_type":"code","source":"# ============================================================\n# Configuration\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\nIMG_SIZE   = 224\nBATCH_SIZE = 16\n\nEPOCHS_PHASE1 = 8\nEPOCHS_PHASE2 = 8\nEPOCHS_PHASE3 = 6\n\nUSE_AMP = True\nprint(\"Config OK\")","metadata":{},"outputs":[],"execution_count":null},{"id":"4","cell_type":"code","source":"# ============================================================\n# Dataset APTOS 2019\n# ============================================================\ndef make_binary(x): 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 : {len(df)}\")\n    print(df[\"label\"].value_counts().to_string())\n    return df\n\nVGG_MEAN = [0.485, 0.456, 0.406]\nVGG_STD  = [0.229, 0.224, 0.225]\n\ntrain_transform = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.ColorJitter(brightness=0.15, contrast=0.15, saturation=0.10),\n    transforms.RandomRotation(20),\n    transforms.RandomAffine(degrees=0, translate=(0.05, 0.05)),\n    transforms.ToTensor(),\n    transforms.Normalize(VGG_MEAN, VGG_STD),\n])\nval_transform = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(VGG_MEAN, VGG_STD),\n])\nprobe_transform = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(VGG_MEAN, VGG_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    def __len__(self): return len(self.df)\n    def __getitem__(self, idx):\n        row   = self.df.iloc[idx]\n        image = Image.open(row[\"image_path\"]).convert(\"RGB\")\n        if self.transform: 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(APTOSDataset(train_df, train_transform),\n    batch_size=BATCH_SIZE, shuffle=True,  num_workers=2,\n    pin_memory=True, drop_last=True)\nval_loader   = DataLoader(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)}\")","metadata":{},"outputs":[],"execution_count":null},{"id":"5","cell_type":"code","source":"# ============================================================\n# NEW AMCA — ETAPE 1 : Analyse automatique du problème\n# Calcule : imbalance ratio, variabilite intra-classe estimee,\n#           complexite reelle, budget compute adaptatif\n# ============================================================\nprint(\"=\" * 60)\nprint(\"  NEW AMCA — ETAPE 1 : Analyse automatique du probleme\")\nprint(\"=\" * 60)\n\nclass AutoProblemAnalyzer:\n    \"\"\"\n    Analyse automatique des caracteristiques du probleme\n    directement depuis les données (pas de config manuelle).\n    \"\"\"\n    def __init__(self, train_df, val_df, probe_transform, device):\n        self.train_df        = train_df\n        self.val_df          = val_df\n        self.probe_transform = probe_transform\n        self.device          = device\n\n    def analyze(self):\n        print(\"\\n[1/4] Analyse de l'imbalance des classes...\")\n        imbalance_ratio = self._compute_imbalance()\n\n        print(\"[2/4] Estimation de la variabilite intra-classe...\")\n        intra_var = self._estimate_intra_class_variability()\n\n        print(\"[3/4] Calcul de la complexite reelle...\")\n        complexity_score = self._compute_complexity_score(imbalance_ratio, intra_var)\n\n        print(\"[4/4] Determination du budget compute adaptatif...\")\n        compute_budget = self._determine_compute_budget(complexity_score)\n\n        result = {\n            \"num_classes\"       : 2,\n            \"imbalance_ratio\"   : imbalance_ratio,\n            \"intra_variability\" : intra_var,\n            \"complexity_score\"  : complexity_score,\n            \"complexity_level\"  : self._score_to_level(complexity_score),\n            \"compute_budget\"    : compute_budget,\n            \"class_weights\"     : self._compute_class_weights(),\n        }\n        return result\n\n    def _compute_imbalance(self):\n        counts = self.train_df[\"label\"].value_counts()\n        ratio  = counts.max() / counts.min()\n        print(f\"  Classes : {dict(counts.to_dict())}  => ratio : {ratio:.2f}\")\n        return float(ratio)\n\n    def _estimate_intra_class_variability(self):\n        \"\"\"\n        Estime la variabilite intra-classe via l'ecart-type\n        des pixels sur un sous-echantillon d'images par classe.\n        \"\"\"\n        sample_size = 60   # 30 par classe\n        std_per_class = []\n\n        for label in [0, 1]:\n            subset = self.train_df[self.train_df[\"label\"] == label].sample(\n                min(sample_size // 2, len(self.train_df[self.train_df[\"label\"] == label])),\n                random_state=SEED)\n            pixel_stds = []\n            for _, row in subset.iterrows():\n                try:\n                    img = Image.open(row[\"image_path\"]).convert(\"RGB\")\n                    img = img.resize((64, 64))\n                    arr = np.array(img).astype(np.float32) / 255.0\n                    pixel_stds.append(arr.std())\n                except:\n                    pass\n            if pixel_stds:\n                std_per_class.append(np.mean(pixel_stds))\n\n        mean_std = np.mean(std_per_class) if std_per_class else 0.15\n        level    = \"low\" if mean_std < 0.10 else (\"high\" if mean_std > 0.18 else \"medium\")\n        print(f\"  Std pixel moyen par classe : {mean_std:.4f}  => variabilite : {level}\")\n        return level\n\n    def _compute_complexity_score(self, imbalance_ratio, intra_var):\n        \"\"\"Score de complexite de 0 (simple) a 1 (complexe).\"\"\"\n        imbalance_score = min(imbalance_ratio / 5.0, 1.0)\n        var_score       = {\"low\": 0.1, \"medium\": 0.5, \"high\": 0.9}.get(intra_var, 0.5)\n        score           = 0.4 * imbalance_score + 0.6 * var_score\n        print(f\"  Score complexite : {score:.3f}  \"\n              f\"(imbalance={imbalance_score:.2f}, variabilite={var_score:.2f})\")\n        return round(float(score), 3)\n\n    def _score_to_level(self, score):\n        if score < 0.35: return \"simple\"\n        if score < 0.65: return \"moderate\"\n        return \"complex\"\n\n    def _determine_compute_budget(self, complexity_score):\n        # Plus le problème est complexe => plus on ouvre de couches => medium/high\n        if complexity_score < 0.3:   budget = \"low\"\n        elif complexity_score < 0.6: budget = \"medium\"\n        else:                        budget = \"high\"\n        print(f\"  Budget compute determine : {budget}\")\n        return budget\n\n    def _compute_class_weights(self):\n        classes = np.array(sorted(self.train_df[\"label\"].unique()))\n        weights = compute_class_weight(\"balanced\", classes=classes,\n                                       y=self.train_df[\"label\"].values)\n        return {int(c): float(w) for c, w in zip(classes, weights)}\n\nanalyzer        = AutoProblemAnalyzer(train_df, val_df, probe_transform, DEVICE)\nPROBLEM_PROFILE = analyzer.analyze()\n\nprint(\"\\n=== Profil du probleme ===\")\nfor k, v in PROBLEM_PROFILE.items():\n    print(f\"  {k:25s}: {v}\")","metadata":{},"outputs":[],"execution_count":null},{"id":"6","cell_type":"code","source":"# ============================================================\n# NEW AMCA — ETAPE 2 : Recherche automatique du cutting point\n# Evalue la qualite discriminative de chaque bloc VGG19\n# via linear probe AUC sur un sous-echantillon\n# ============================================================\nprint(\"=\" * 60)\nprint(\"  NEW AMCA — ETAPE 2 : Recherche automatique du cutting point\")\nprint(\"=\" * 60)\n\n# Architecture VGG19 — indices des MaxPool (fin de chaque bloc)\nVGG19_BLOCKS = {\n    \"block1\": (0,  4,  64),   # (start, end, out_channels)\n    \"block2\": (5,  9,  128),\n    \"block3\": (10, 18, 256),\n    \"block4\": (19, 27, 512),\n    \"block5\": (28, 36, 512),\n}\n\ndef extract_block_features(vgg_features, block_end_idx, loader, device, max_batches=20):\n    \"\"\"Extrait features via GAP apres le bloc donne.\"\"\"\n    sub_net = nn.Sequential(\n        *list(vgg_features.children())[:block_end_idx + 1]\n    ).to(device)\n    sub_net.eval()\n    gap  = nn.AdaptiveAvgPool2d((1, 1))\n    feats, labels_out = [], []\n\n    with torch.no_grad():\n        for i, (imgs, lbls) in enumerate(loader):\n            if i >= max_batches: break\n            imgs = imgs.to(device)\n            out  = sub_net(imgs)\n            out  = gap(out).view(out.size(0), -1)\n            feats.append(out.cpu().numpy())\n            labels_out.extend(lbls.numpy())\n\n    return np.concatenate(feats, 0), np.array(labels_out)\n\n# Charger VGG19 backbone (sans tete) pour la recherche\nvgg19_base = models.vgg19(weights=models.VGG19_Weights.IMAGENET1K_V1)\nvgg19_base.eval()\n\nprobe_loader = DataLoader(\n    APTOSDataset(val_df, probe_transform),\n    batch_size=32, shuffle=False, num_workers=2)\n\nblock_probe_results = {}\nprint(\"\\nBlock         | AUC probe | Acc probe | Channels | Decision\")\nprint(\"-\" * 65)\n\nbest_auc_probe    = 0.0\nbest_cut_block    = \"block4\"   # defaut\nprev_auc          = 0.0\nimprovement_threshold = 0.005  # gain minimal requis pour continuer\n\nfor bname, (start, end, channels) in VGG19_BLOCKS.items():\n    feats, lbls = extract_block_features(\n        vgg19_base.features, end, probe_loader, DEVICE, max_batches=15)\n\n    scaler_sk = StandardScaler()\n    feats     = scaler_sk.fit_transform(feats)\n\n    clf = LogisticRegression(max_iter=300, C=0.5, solver=\"lbfgs\", random_state=SEED)\n    clf.fit(feats, lbls)\n    preds_prob = clf.predict_proba(feats)[:, 1]\n    preds_cls  = clf.predict(feats)\n\n    try:    auc_b = roc_auc_score(lbls, preds_prob)\n    except: auc_b = 0.5\n    acc_b = accuracy_score(lbls, preds_cls)\n\n    gain     = auc_b - prev_auc\n    decision = \"✓ meilleur\" if auc_b > best_auc_probe else f\"gain={gain:+.4f}\"\n\n    block_probe_results[bname] = {\n        \"auc\": auc_b, \"acc\": acc_b,\n        \"channels\": channels, \"gain\": gain}\n\n    print(f\"  {bname:10s}  | {auc_b:.4f}    | {acc_b:.4f}    | {channels:7d}  | {decision}\")\n\n    if auc_b > best_auc_probe:\n        best_auc_probe = auc_b\n        best_cut_block = bname\n    prev_auc = auc_b\n\n# Regle de coupe : choisir le dernier bloc AVANT saturation\n# On cherche ou le gain devient inferieur au seuil => couper juste avant\nblock_names = list(VGG19_BLOCKS.keys())\nauto_cut    = best_cut_block\nfor i in range(1, len(block_names)):\n    g = block_probe_results[block_names[i]][\"gain\"]\n    if g < improvement_threshold:\n        # Le bloc precedent est le meilleur cutting point\n        auto_cut = block_names[i - 1]\n        break\n\nprint(f\"\\n=> Cutting point choisi automatiquement : {auto_cut}\")\nprint(f\"   (AUC probe au cutting point : {block_probe_results[auto_cut]['auc']:.4f})\")\n\nPROBE_RESULTS = block_probe_results\nAUTO_CUT      = auto_cut\n\ndel vgg19_base","metadata":{},"outputs":[],"execution_count":null},{"id":"7","cell_type":"code","source":"# ============================================================\n# NEW AMCA — ETAPE 3 : Conception automatique de la tête\n# Adapte les unites Dense selon :\n#   - La dimension de sortie du cutting point\n#   - Le score de complexite du probleme\n#   - Le ratio d'imbalance\n# ============================================================\nprint(\"=\" * 60)\nprint(\"  NEW AMCA — ETAPE 3 : Conception automatique de la tete\")\nprint(\"=\" * 60)\n\nclass AutoHeadDesigner:\n    \"\"\"\n    Determine automatiquement l'architecture de la tete\n    en fonction du profil du probleme et du cutting point.\n    \"\"\"\n    def __init__(self, problem_profile, cut_block_name):\n        self.profile   = problem_profile\n        self.cut_block = cut_block_name\n\n    def design(self):\n        complexity  = self.profile[\"complexity_level\"]\n        score       = self.profile[\"complexity_score\"]\n        imbalance   = self.profile[\"imbalance_ratio\"]\n        out_channels = VGG19_BLOCKS[self.cut_block][2]\n\n        # --- Nombre de couches Dense ---\n        if complexity == \"simple\":   n_layers = 1\n        elif complexity == \"moderate\": n_layers = 2\n        else:                          n_layers = 3\n\n        # --- Taille des couches (proportionnelle aux channels de sortie) ---\n        base_units = out_channels   # 64 / 128 / 256 / 512\n        units = []\n        for i in range(n_layers):\n            # Reduction progressive : /1, /2, /4 ...\n            u = max(128, base_units // (2 ** i))\n            # Arrondir a la puissance de 2 la plus proche\n            u = 2 ** round(math.log2(u))\n            units.append(int(u))\n\n        # --- Dropout adaptatif ---\n        # Plus l'imbalance est forte => plus on regularise\n        base_drop = 0.30 + min(0.20, (imbalance - 1) * 0.05)\n        dropouts  = [round(base_drop + i * 0.05, 2) for i in range(n_layers)]\n\n        head = {\"units\": units, \"dropout\": dropouts}\n\n        print(f\"  Cutting block   : {self.cut_block} (out={out_channels} ch)\")\n        print(f\"  Complexite      : {complexity}  (score={score:.3f})\")\n        print(f\"  Imbalance ratio : {imbalance:.2f}\")\n        print(f\"  Tete concue     : {n_layers} couche(s) Dense\")\n        print(f\"    Unites        : {units}\")\n        print(f\"    Dropouts      : {dropouts}\")\n\n        return head\n\nhead_designer = AutoHeadDesigner(PROBLEM_PROFILE, AUTO_CUT)\nAUTO_HEAD     = head_designer.design()\nprint(f\"\\n=> Architecture tete : {AUTO_HEAD}\")","metadata":{},"outputs":[],"execution_count":null},{"id":"8","cell_type":"code","source":"# ============================================================\n# NEW AMCA — ETAPE 4 : Strategie de gel et LR adaptatifs\n# Determine le freeze ratio et les LR selon :\n#   - La complexite du probleme\n#   - Le gain de AUC par bloc (from probe)\n#   - Le budget compute\n# ============================================================\nprint(\"=\" * 60)\nprint(\"  NEW AMCA — ETAPE 4 : Strategie de gel & LR adaptatifs\")\nprint(\"=\" * 60)\n\nclass AutoFreezingStrategy:\n    \"\"\"\n    Determine automatiquement :\n    - Quels blocs geler en Phase 1\n    - Quels blocs degeler en Phase 2\n    - Le plan de LR selon la complexite reelle\n    \"\"\"\n    def __init__(self, problem_profile, probe_results, cut_block):\n        self.profile       = problem_profile\n        self.probe_results = probe_results\n        self.cut_block     = cut_block\n\n    def plan(self):\n        complexity     = self.profile[\"complexity_level\"]\n        compute_budget = self.profile[\"compute_budget\"]\n        imbalance      = self.profile[\"imbalance_ratio\"]\n\n        # --- Phase 1 : geler jusqu'au cut block ---\n        freeze_until_block = self.cut_block\n        print(f\"  Phase 1 : gel jusqu'a {freeze_until_block}\")\n\n        # --- Phase 2 : quels blocs degeler ---\n        # On dégèle les blocs dont le gain probe > seuil\n        block_names   = list(VGG19_BLOCKS.keys())\n        cut_idx       = block_names.index(self.cut_block)\n        # Degeler le bloc courant + le precedent (si gain suffisant)\n        unfreeze_from = block_names[max(0, cut_idx - 1)]\n        print(f\"  Phase 2 : degelage a partir de {unfreeze_from}\")\n\n        # --- LR adaptatif ---\n        # Base LR selon complexite\n        lr_base = {\"simple\": 5e-4, \"moderate\": 2e-4, \"complex\": 1e-4}[complexity]\n        # Correction imbalance : dataset tres desequilibre => LR un peu plus faible\n        if imbalance > 3.0: lr_base *= 0.7\n\n        lr_strategy = {\n            \"phase1\": round(lr_base,       8),\n            \"phase2\": round(lr_base / 5,   8),\n            \"phase3\": round(lr_base / 50,  8),\n        }\n\n        print(f\"  LR strategy     : {lr_strategy}\")\n        print(f\"  Budget compute  : {compute_budget}\")\n\n        return {\n            \"freeze_until_block\" : freeze_until_block,\n            \"unfreeze_from_p2\"   : unfreeze_from,\n            \"lr\"                 : lr_strategy,\n        }\n\nauto_freeze   = AutoFreezingStrategy(PROBLEM_PROFILE, PROBE_RESULTS, AUTO_CUT)\nFREEZE_PLAN   = auto_freeze.plan()\n\nprint(\"\\n=== Plan complet NEW AMCA ===\")\nprint(f\"  Cutting point   : {AUTO_CUT}\")\nprint(f\"  Tete            : {AUTO_HEAD}\")\nprint(f\"  Gel Phase 1     : jusqu'au {FREEZE_PLAN['freeze_until_block']}\")\nprint(f\"  Degelage Ph.2   : a partir de {FREEZE_PLAN['unfreeze_from_p2']}\")\nprint(f\"  LR              : {FREEZE_PLAN['lr']}\")","metadata":{},"outputs":[],"execution_count":null},{"id":"9","cell_type":"code","source":"# ============================================================\n# Modele NEW AMCA-VGG19\n# Construit dynamiquement selon les decisions automatiques\n# ============================================================\nclass NewAMCA_VGG19(nn.Module):\n    def __init__(self, auto_cut, auto_head, freeze_plan, num_classes=1):\n        super().__init__()\n\n        # 1. Backbone VGG19 pre-entraine\n        vgg            = models.vgg19(weights=models.VGG19_Weights.IMAGENET1K_V1)\n        self.features  = vgg.features      # 37 couches (0..36)\n        self.avgpool   = nn.AdaptiveAvgPool2d((7, 7))\n\n        # 2. Gel automatique (Phase 1)\n        self.freeze_plan       = freeze_plan\n        self._frozen_up_to     = VGG19_BLOCKS[freeze_plan[\"freeze_until_block\"]][1]\n        self._apply_freezing(self._frozen_up_to)\n\n        # 3. Tete concue automatiquement par AutoHeadDesigner\n        in_features = 512 * 7 * 7\n        layers      = [nn.Flatten()]; in_dim = in_features\n\n        for units, drop in zip(auto_head[\"units\"], auto_head[\"dropout\"]):\n            layers += [\n                nn.Linear(in_dim, units),\n                nn.LayerNorm(units),\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\"NewAMCA-VGG19 construit | cut={auto_cut} | \"\n              f\"gel jusqu'index {self._frozen_up_to}\")\n        self._print_trainable()\n\n    def _apply_freezing(self, freeze_until_idx):\n        for idx, layer in enumerate(self.features):\n            layer.requires_grad_(idx > freeze_until_idx)\n\n    def unfreeze_from_block(self, block_name):\n        start_idx = VGG19_BLOCKS[block_name][0]\n        for idx, layer in enumerate(self.features):\n            if idx >= start_idx:\n                layer.requires_grad_(True)\n        print(f\"Degelage a partir de {block_name}\")\n        self._print_trainable()\n\n    def unfreeze_all(self):\n        for layer in self.features:\n            layer.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:,} | entrainables : {trainable:,} \"\n              f\"({100*trainable/total:.1f}%)\")\n\n    def forward(self, x):\n        x = self.features(x)\n        x = self.avgpool(x)\n        return self.head(x).squeeze(1)\n\nmodel = NewAMCA_VGG19(AUTO_CUT, AUTO_HEAD, FREEZE_PLAN).to(DEVICE)","metadata":{},"outputs":[],"execution_count":null},{"id":"10","cell_type":"code","source":"# ============================================================\n# Taille & FLOPs du modele\n# ============================================================\nfrom fvcore.nn import FlopCountAnalysis, flop_count_table\n\ndef model_size_mb(m):\n    return sum(p.numel() * p.element_size() for p in m.parameters()) / (1024**2)\n\ndummy = torch.randn(1, 3, IMG_SIZE, IMG_SIZE).to(DEVICE)\nmodel.eval()\ntry:\n    fa     = FlopCountAnalysis(model, dummy)\n    fa.unsupported_ops_warnings(False); fa.uncalled_modules_warnings(False)\n    gflops = fa.total() / 1e9\n    print(f\"GFLOPs (batch=1) : {gflops:.2f}\")\n    print(flop_count_table(fa, max_depth=3))\nexcept Exception as e:\n    gflops = None; print(f\"FLOPs: {e}\")\n\ntotal_p   = sum(p.numel() for p in model.parameters())\ntrain_p   = sum(p.numel() for p in model.parameters() if p.requires_grad)\nsize_mb   = model_size_mb(model)\n\nprint(f\"\\nParams totaux   : {total_p:,}\")\nprint(f\"Params entrain. : {train_p:,}  ({100*train_p/total_p:.1f}%)\")\nprint(f\"Taille (fp32)   : {size_mb:.1f} MB\")\n\nMODEL_STATS = {\"total_params\": total_p, \"trainable_params\": train_p,\n               \"size_mb\": size_mb, \"gflops\": gflops}","metadata":{},"outputs":[],"execution_count":null},{"id":"11","cell_type":"code","source":"# ============================================================\n# Boucle d'entrainement avec gradient flow monitoring\n# ============================================================\nfrom torch.cuda.amp import GradScaler, autocast\n\nHISTORY = {\n    \"train_loss\":[], \"val_loss\":[],\n    \"train_acc\":[],  \"val_acc\":[],\n    \"val_auc\":[],    \"phase_end\":[],\n    \"grad_flow_per_phase\":[],    # gradient norme par couche\n}\n\ndef get_gradient_flow(model):\n    \"\"\"Retourne la norme moyenne des gradients par couche features.\"\"\"\n    grad_norms = {}\n    for name, param in model.features.named_parameters():\n        if param.grad is not None and param.requires_grad:\n            grad_norms[name] = param.grad.abs().mean().item()\n    return grad_norms\n\ndef make_optimizer_scheduler(model, lr, steps, epochs):\n    feat_params = [p for p in model.features.parameters() if p.requires_grad]\n    head_params = list(model.head.parameters())\n    opt   = torch.optim.SGD(\n        [{\"params\": feat_params, \"lr\": lr / 10},\n         {\"params\": head_params, \"lr\": lr}],\n        momentum=0.9, weight_decay=5e-4, nesterov=True)\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    last_grad_flow = {}\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); loss = crit(logits, labels)\n        scaler.scale(loss).backward()\n        scaler.unscale_(opt)\n        nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n        last_grad_flow = get_gradient_flow(model)\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, last_grad_flow\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); 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()); 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{'='*62}\")\n    print(f\"  PHASE {phase_num}  |  LR={lr}  |  Epochs={epochs}\")\n    print(f\"{'='*62}\")\n    scaler     = GradScaler(enabled=USE_AMP)\n    opt, sched = make_optimizer_scheduler(model, lr, len(train_loader), epochs)\n    phase_grads = []\n    for ep in range(1, epochs + 1):\n        tl, ta, gf   = train_epoch(model, train_loader, opt, sched, criterion, scaler)\n        vl, va, vauc = val_epoch(model, val_loader, criterion)\n        HISTORY[\"train_loss\"].append(tl); HISTORY[\"val_loss\"].append(vl)\n        HISTORY[\"train_acc\"].append(ta);  HISTORY[\"val_acc\"].append(va)\n        HISTORY[\"val_auc\"].append(vauc);  phase_grads.append(gf)\n        marker = \"\"\n        if vauc > best_auc:\n            best_auc = vauc\n            torch.save(model.state_dict(), save_path); marker = \"  [SAVED]\"\n        print(f\"Ep {ep:02d}/{epochs} | train loss={tl:.4f} acc={ta:.4f} | \"\n              f\"val loss={vl:.4f} acc={va:.4f} AUC={vauc:.4f}{marker}\")\n    HISTORY[\"phase_end\"].append(len(HISTORY[\"train_loss\"]))\n    HISTORY[\"grad_flow_per_phase\"].append(phase_grads)\n    return best_auc\n\nprint(\"Boucle prete.\")","metadata":{},"outputs":[],"execution_count":null},{"id":"12","cell_type":"code","source":"# ============================================================\n# Class weights (auto depuis PROBLEM_PROFILE)\n# ============================================================\ncw         = PROBLEM_PROFILE[\"class_weights\"]\npos_weight = torch.tensor([cw[1] / cw[0]], dtype=torch.float32).to(DEVICE)\ncriterion  = nn.BCEWithLogitsLoss(pos_weight=pos_weight)\nprint(f\"Class weights : {cw}\")\nprint(f\"pos_weight    : {pos_weight.item():.4f}\")\n\nSAVE_PATH = os.path.join(OUTPUT_DIR, \"new_amca_vgg19_best.pth\")\nbest_auc  = 0.0\nlr_cfg    = FREEZE_PLAN[\"lr\"]","metadata":{},"outputs":[],"execution_count":null},{"id":"13","cell_type":"code","source":"# ============================================================\n# Entrainement NEW AMCA-VGG19 — 3 phases\n# ============================================================\n\n# Phase 1 : Gel auto (coupe determinee par NEW AMCA)\nprint(f\"Phase 1 : backbone gele jusqu'au {FREEZE_PLAN['freeze_until_block']}\")\nbest_auc = run_phase(model, 1, EPOCHS_PHASE1, lr_cfg[\"phase1\"], best_auc, SAVE_PATH)\n\n# Phase 2 : Degelage auto (determine par AutoFreezingStrategy)\nunfreeze_p2 = FREEZE_PLAN[\"unfreeze_from_p2\"]\nprint(f\"\\nDegelage automatique a partir de {unfreeze_p2} (Phase 2)\")\nmodel.unfreeze_from_block(unfreeze_p2)\nbest_auc = run_phase(model, 2, EPOCHS_PHASE2, lr_cfg[\"phase2\"], best_auc, SAVE_PATH)\n\n# Phase 3 : Full fine-tuning\nprint(\"\\nDegelage complet (Phase 3)\")\nmodel.unfreeze_all()\nbest_auc = run_phase(model, 3, EPOCHS_PHASE3, lr_cfg[\"phase3\"], best_auc, SAVE_PATH)\n\nprint(f\"\\nMeilleur AUC : {best_auc:.4f}\")","metadata":{},"outputs":[],"execution_count":null},{"id":"14","cell_type":"code","source":"# ============================================================\n# Graphe 1 : Courbes Loss / Accuracy / AUC\n# ============================================================\nep_range = range(1, len(HISTORY[\"train_loss\"]) + 1)\n\nfig, axes = plt.subplots(1, 3, figsize=(18, 5))\nfig.suptitle(\"NEW AMCA-VGG19 — Courbes d'entrainement\",\n             fontsize=14, fontweight=\"bold\")\n\nphase_labels = [f\"Phase 1 ({EPOCHS_PHASE1}ep)\",\n                f\"Phase 2 ({EPOCHS_PHASE2}ep)\",\n                f\"Phase 3 ({EPOCHS_PHASE3}ep)\"]\n\nfor ax, y_train, y_val, title, ylabel in zip(\n    axes,\n    [HISTORY[\"train_loss\"], [v*100 for v in HISTORY[\"train_acc\"]], [None]*len(ep_range)],\n    [HISTORY[\"val_loss\"],   [v*100 for v in HISTORY[\"val_acc\"]],   HISTORY[\"val_auc\"]],\n    [\"Loss\", \"Accuracy (%)\", \"Validation AUC\"],\n    [\"BCE Loss\", \"Accuracy (%)\", \"AUC\"],\n):\n    if y_train[0] is not None:\n        ax.plot(ep_range, y_train, \"#E74C3C\", lw=2.2, label=\"Train\")\n    ax.plot(ep_range, y_val, \"#2980B9\", lw=2.2, ls=\"--\", label=\"Val\")\n    for i, pe in enumerate(HISTORY[\"phase_end\"][:-1]):\n        ax.axvline(pe + 0.5, color=\"#95A5A6\", ls=\":\", lw=1.5)\n        ax.text(pe + 0.7, ax.get_ylim()[1] * 0.97,\n                phase_labels[i], fontsize=7.5, color=\"#7F8C8D\", va=\"top\")\n    ax.set_title(title, fontsize=12); ax.set_xlabel(\"Epoch\"); ax.set_ylabel(ylabel)\n    ax.legend(framealpha=0.85); ax.grid(alpha=0.3)\n\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, \"new_amca_vgg19_curves.png\"), dpi=150, bbox_inches=\"tight\")\nplt.show(); print(\"Courbes sauvegardees.\")","metadata":{},"outputs":[],"execution_count":null},{"id":"15","cell_type":"code","source":"# ============================================================\n# Graphe 2 : Gradient Flow par phase\n# Montre quelles couches recoivent des gradients\n# ============================================================\nfig, axes = plt.subplots(1, len(HISTORY[\"grad_flow_per_phase\"]),\n                          figsize=(7 * len(HISTORY[\"grad_flow_per_phase\"]), 5))\nif len(HISTORY[\"grad_flow_per_phase\"]) == 1:\n    axes = [axes]\n\nfig.suptitle(\"NEW AMCA-VGG19 — Gradient Flow par phase\\n\"\n             \"(norme moyenne des gradients par couche)\",\n             fontsize=13, fontweight=\"bold\")\n\nfor ph_idx, (ax, phase_grads) in enumerate(\n        zip(axes, HISTORY[\"grad_flow_per_phase\"])):\n    # Moyenne des gradients sur toutes les epochs de cette phase\n    all_keys = set()\n    for gf in phase_grads: all_keys.update(gf.keys())\n    all_keys = sorted(all_keys)\n\n    mean_grads = []\n    for k in all_keys:\n        vals = [gf[k] for gf in phase_grads if k in gf]\n        mean_grads.append(np.mean(vals) if vals else 0.0)\n\n    # Raccourcir les noms\n    short_keys = [k.split(\".\")[-2] + \".\" + k.split(\".\")[-1]\n                  if \".\" in k else k for k in all_keys]\n    short_keys = [k[:12] for k in short_keys]\n\n    bar_c = [\"#E74C3C\" if v < 1e-5 else \"#27AE60\" for v in mean_grads]\n    ax.barh(range(len(mean_grads)), mean_grads, color=bar_c)\n    ax.set_yticks(range(len(short_keys)))\n    ax.set_yticklabels(short_keys, fontsize=7)\n    ax.set_title(f\"Phase {ph_idx+1}\", fontsize=11)\n    ax.set_xlabel(\"Gradient moyen\")\n    ax.axvline(1e-5, color=\"black\", ls=\"--\", lw=0.8, label=\"Seuil actif\")\n    ax.legend(fontsize=8)\n\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, \"new_amca_vgg19_gradflow.png\"),\n            dpi=150, bbox_inches=\"tight\")\nplt.show(); print(\"Gradient flow sauvegarde.\")","metadata":{},"outputs":[],"execution_count":null},{"id":"16","cell_type":"code","source":"# ============================================================\n# Evaluation finale\n# ============================================================\nfrom torch.cuda.amp import autocast\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,\n      target_names=[\"No DR\", \"DR\"], digits=4))\ncm        = confusion_matrix(y_true, y_pred)\nauc_final = roc_auc_score(y_true, y_score)\nacc_final = accuracy_score(y_true, y_pred)\nprint(f\"ROC AUC  : {auc_final:.4f}\")\nprint(f\"Accuracy : {acc_final*100:.2f}%\")","metadata":{},"outputs":[],"execution_count":null},{"id":"17","cell_type":"code","source":"# ============================================================\n# Graphe 3 : Accuracy & AUC par bloc VGG19 (linear probe)\n# ============================================================\nprint(\"=== Per-layer analysis (linear probe) ===\")\ngap_probe = nn.AdaptiveAvgPool2d((1, 1))\nmodel.eval()\n\nblock_accs, block_aucs_list = [], []\nblock_labels_list, block_status_list = [], []\n\nfor bname, (start, end, ch) in VGG19_BLOCKS.items():\n    sub = nn.Sequential(*list(model.features.children())[:end+1]).to(DEVICE)\n    sub.eval()\n    all_feats, all_lbls = [], []\n    with torch.no_grad():\n        for imgs, labels in val_loader:\n            imgs = imgs.to(DEVICE)\n            f    = gap_probe(sub(imgs)).view(imgs.size(0), -1)\n            all_feats.append(f.cpu().numpy())\n            all_lbls.extend(labels.numpy())\n\n    feats_np = np.concatenate(all_feats, 0)\n    lbls_np  = np.array(all_lbls)\n    feats_np = StandardScaler().fit_transform(feats_np)\n\n    clf = LogisticRegression(max_iter=500, C=1.0, solver=\"lbfgs\", random_state=SEED)\n    clf.fit(feats_np, lbls_np)\n    try:    auc_b = roc_auc_score(lbls_np, clf.predict_proba(feats_np)[:, 1])\n    except: auc_b = 0.5\n    acc_b  = accuracy_score(lbls_np, clf.predict(feats_np))\n    status = \"frozen\" if end <= model._frozen_up_to else \"trained\"\n\n    block_accs.append(acc_b); block_aucs_list.append(auc_b)\n    block_labels_list.append(f\"{bname}\\n({ch}ch)\")\n    block_status_list.append(status)\n    print(f\"  {bname} [{status:7s}]  acc={acc_b:.4f}  AUC={auc_b:.4f}\")\n\nbar_colors = [\"#E74C3C\" if s==\"frozen\" else \"#27AE60\" for s in block_status_list]\nx_pos = np.arange(len(block_labels_list))\n\nfig, axes = plt.subplots(1, 2, figsize=(15, 5))\nfig.suptitle(\"NEW AMCA-VGG19 — Accuracy & AUC par bloc (linear probe)\",\n             fontsize=13, fontweight=\"bold\")\nfor ax, vals, title, ylabel in zip(\n        axes, [block_accs, block_aucs_list],\n        [\"Accuracy par bloc\", \"AUC par bloc\"], [\"Accuracy\", \"AUC\"]):\n    bars = ax.bar(x_pos, vals, color=bar_colors, edgecolor=\"white\", width=0.55)\n    ax.set_xticks(x_pos); ax.set_xticklabels(block_labels_list, fontsize=9)\n    ax.set_title(title); ax.set_ylabel(ylabel); ax.set_ylim(0.4, 1.05)\n    for bar, v in zip(bars, vals):\n        ax.text(bar.get_x()+bar.get_width()/2, bar.get_height()+0.007,\n                f\"{v:.3f}\", ha=\"center\", fontsize=9, fontweight=\"bold\")\n    ax.grid(axis=\"y\", alpha=0.3)\n    ax.legend(handles=[mpatches.Patch(color=\"#E74C3C\", label=\"Frozen\"),\n                        mpatches.Patch(color=\"#27AE60\", label=\"Trained\")],\n              fontsize=9, loc=\"lower right\")\n\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, \"new_amca_vgg19_perlayer.png\"), dpi=150, bbox_inches=\"tight\")\nplt.show()","metadata":{},"outputs":[],"execution_count":null},{"id":"18","cell_type":"code","source":"# ============================================================\n# Graphe 4 : AUC probe par bloc (decision du cutting point)\n# Visualise comment le NEW AMCA a choisi le cutting point\n# ============================================================\nprobe_block_names = list(PROBE_RESULTS.keys())\nprobe_aucs        = [PROBE_RESULTS[b][\"auc\"] for b in probe_block_names]\nprobe_accs        = [PROBE_RESULTS[b][\"acc\"] for b in probe_block_names]\ncut_idx_probe     = probe_block_names.index(AUTO_CUT)\n\nfig, ax = plt.subplots(figsize=(10, 5))\nax.plot(range(len(probe_block_names)), probe_aucs,\n        \"#8E44AD\", marker=\"D\", lw=2.5, ms=8, label=\"AUC probe\")\nax.plot(range(len(probe_block_names)), probe_accs,\n        \"#2980B9\", marker=\"o\", lw=2, ms=6, ls=\"--\", label=\"Acc probe\")\n\nax.axvline(cut_idx_probe, color=\"#E74C3C\", lw=2.5, ls=\"--\",\n           label=f\"Cutting point choisi : {AUTO_CUT}\")\nax.fill_between(range(cut_idx_probe + 1),\n                [v - 0.02 for v in probe_aucs[:cut_idx_probe+1]],\n                [v + 0.02 for v in probe_aucs[:cut_idx_probe+1]],\n                alpha=0.10, color=\"#E74C3C\", label=\"Zone gelee\")\n\nfor i, (b, auc_v) in enumerate(zip(probe_block_names, probe_aucs)):\n    ax.text(i, auc_v + 0.012, f\"{auc_v:.3f}\", ha=\"center\", fontsize=9, fontweight=\"bold\")\n\nax.set_xticks(range(len(probe_block_names)))\nax.set_xticklabels(probe_block_names, fontsize=10)\nax.set_title(\"NEW AMCA — Selection automatique du Cutting Point\\n\"\n             \"(AUC & Accuracy par bloc avant entrainement)\",\n             fontsize=12, fontweight=\"bold\")\nax.set_ylabel(\"Score (AUC / Accuracy)\"); ax.set_ylim(0.4, 1.08)\nax.legend(fontsize=10); ax.grid(alpha=0.3)\n\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, \"new_amca_cutting_point.png\"), dpi=150, bbox_inches=\"tight\")\nplt.show(); print(\"Graphe cutting point sauvegarde.\")","metadata":{},"outputs":[],"execution_count":null},{"id":"19","cell_type":"code","source":"# ============================================================\n# Dashboard final NEW AMCA-VGG19\n# ============================================================\nep_range = range(1, len(HISTORY[\"train_loss\"]) + 1)\n\nfig = plt.figure(figsize=(22, 16))\ngs  = gridspec.GridSpec(3, 3, figure=fig, hspace=0.50, wspace=0.35)\nfig.suptitle(\n    f\"NEW AMCA-VGG19 — APTOS 2019\\n\"\n    f\"AUC={auc_final:.4f}  |  Acc={acc_final*100:.2f}%  |  \"\n    f\"Cut={AUTO_CUT}  |  Head={AUTO_HEAD['units']}\",\n    fontsize=14, fontweight=\"bold\", y=1.01)\n\n# 1. Loss\nax1 = fig.add_subplot(gs[0, 0])\nax1.plot(ep_range, HISTORY[\"train_loss\"], \"#E74C3C\", lw=2, label=\"Train\")\nax1.plot(ep_range, HISTORY[\"val_loss\"],   \"#2980B9\", lw=2, ls=\"--\", label=\"Val\")\nfor pe in HISTORY[\"phase_end\"][:-1]: ax1.axvline(pe+0.5, color=\"#95A5A6\", ls=\":\", lw=1)\nax1.set_title(\"Loss\"); ax1.set_xlabel(\"Epoch\")\nax1.legend(); ax1.grid(alpha=0.25)\n\n# 2. Accuracy\nax2 = fig.add_subplot(gs[0, 1])\nax2.plot(ep_range, [v*100 for v in HISTORY[\"train_acc\"]], \"#E74C3C\", lw=2, label=\"Train\")\nax2.plot(ep_range, [v*100 for v in HISTORY[\"val_acc\"]],   \"#2980B9\", lw=2, ls=\"--\", label=\"Val\")\nfor pe in HISTORY[\"phase_end\"][:-1]: ax2.axvline(pe+0.5, color=\"#95A5A6\", ls=\":\", lw=1)\nax2.set_title(\"Accuracy (%)\"); ax2.set_xlabel(\"Epoch\")\nax2.legend(); ax2.grid(alpha=0.25)\n\n# 3. Val AUC\nax3 = fig.add_subplot(gs[0, 2])\nax3.plot(ep_range, HISTORY[\"val_auc\"], \"#8E44AD\", lw=2, marker=\"o\", ms=3.5)\nfor pe in HISTORY[\"phase_end\"][:-1]: ax3.axvline(pe+0.5, color=\"#95A5A6\", ls=\":\", lw=1)\nax3.set_title(\"Val AUC\"); ax3.set_xlabel(\"Epoch\")\nax3.set_ylim(0.5, 1.02); ax3.grid(alpha=0.25)\n\n# 4. Cutting point selection\nax4 = fig.add_subplot(gs[1, 0])\nax4.plot(range(len(probe_block_names)), probe_aucs,\n         \"#8E44AD\", marker=\"D\", lw=2, ms=7, label=\"AUC probe\")\nax4.axvline(cut_idx_probe, color=\"#E74C3C\", lw=2, ls=\"--\",\n            label=f\"Cut : {AUTO_CUT}\")\nax4.set_xticks(range(len(probe_block_names)))\nax4.set_xticklabels(probe_block_names, fontsize=8)\nax4.set_title(\"Selection cutting point (NEW AMCA)\")\nax4.set_ylim(0.4, 1.05); ax4.legend(fontsize=8); ax4.grid(alpha=0.25)\n\n# 5. Per-layer accuracy (barres)\nax5 = fig.add_subplot(gs[1, 1:])\nbars = ax5.bar(x_pos, block_accs, color=bar_colors, edgecolor=\"white\", width=0.55)\nax5.set_xticks(x_pos); ax5.set_xticklabels(block_labels_list, fontsize=9)\nax5.set_title(\"Accuracy par bloc VGG19 (linear probe)\")\nax5.set_ylabel(\"Accuracy\"); ax5.set_ylim(0.4, 1.05)\nfor bar, v in zip(bars, block_accs):\n    ax5.text(bar.get_x()+bar.get_width()/2, bar.get_height()+0.007,\n             f\"{v:.3f}\", ha=\"center\", fontsize=8.5)\nax5.grid(axis=\"y\", alpha=0.25)\nax5.legend(handles=[mpatches.Patch(color=\"#E74C3C\", label=\"Frozen\"),\n                     mpatches.Patch(color=\"#27AE60\", label=\"Trained\")], fontsize=9)\n\n# 6. Confusion Matrix\nax6 = fig.add_subplot(gs[2, 0])\nsns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\",\n            xticklabels=[\"No DR\",\"DR\"], yticklabels=[\"No DR\",\"DR\"],\n            ax=ax6, annot_kws={\"size\":13})\nax6.set_title(\"Confusion Matrix\")\nax6.set_ylabel(\"Vrai\"); ax6.set_xlabel(\"Predit\")\n\n# 7. Score distribution\nax7 = fig.add_subplot(gs[2, 1])\nax7.hist(y_score[y_true==0], bins=30, alpha=0.65, color=\"#3498DB\", label=\"No DR\")\nax7.hist(y_score[y_true==1], bins=30, alpha=0.65, color=\"#E74C3C\", label=\"DR\")\nax7.axvline(0.5, color=\"black\", ls=\"--\", lw=1.5)\nax7.set_title(\"Distribution des scores\")\nax7.set_xlabel(\"Score sigmoid\"); ax7.legend(fontsize=9)\n\n# 8. Stats card\nax8 = fig.add_subplot(gs[2, 2]); ax8.axis(\"off\")\nfrozen_n  = block_status_list.count(\"frozen\")\ntrained_n = block_status_list.count(\"trained\")\nstats_text = (\n    f\"NEW AMCA — Decisions automatiques\\n\"\n    f\"{'─'*34}\\n\"\n    f\"Complexite detectee : {PROBLEM_PROFILE['complexity_level']}\\n\"\n    f\"Score complexite    : {PROBLEM_PROFILE['complexity_score']:.3f}\\n\"\n    f\"Imbalance ratio     : {PROBLEM_PROFILE['imbalance_ratio']:.2f}\\n\"\n    f\"Variabilite         : {PROBLEM_PROFILE['intra_variability']}\\n\"\n    f\"Budget compute auto : {PROBLEM_PROFILE['compute_budget']}\\n\"\n    f\"Cutting point auto  : {AUTO_CUT}\\n\"\n    f\"Tete auto           : {AUTO_HEAD['units']}\\n\"\n    f\"Frozen              : {frozen_n}/5 blocs\\n\"\n    f\"Phases LR           : {FREEZE_PLAN['lr']}\\n\"\n    f\"{'─'*34}\\n\"\n    f\"Modele      : VGG19 (16 conv)\\n\"\n    f\"Params tot. : {MODEL_STATS['total_params']/1e6:.1f} M\\n\"\n    f\"Entrainab.  : {MODEL_STATS['trainable_params']/1e6:.1f} M\\n\"\n    f\"Taille      : {MODEL_STATS['size_mb']:.0f} MB\\n\"\n)\nif MODEL_STATS[\"gflops\"]:\n    stats_text += f\"GFLOPs      : {MODEL_STATS['gflops']:.2f}\\n\"\nstats_text += (\n    f\"{'─'*34}\\n\"\n    f\"AUC final   : {auc_final:.4f}\\n\"\n    f\"Accuracy    : {acc_final*100:.2f}%\"\n)\nax8.text(0.03, 0.98, stats_text, transform=ax8.transAxes,\n         fontsize=9, verticalalignment=\"top\", fontfamily=\"monospace\",\n         bbox=dict(boxstyle=\"round,pad=0.5\", facecolor=\"#EBF5FB\", alpha=0.9))\nax8.set_title(\"NEW AMCA — Resume\", fontsize=11)\n\ndashboard_path = os.path.join(OUTPUT_DIR, \"new_amca_vgg19_dashboard.png\")\nplt.savefig(dashboard_path, dpi=150, bbox_inches=\"tight\")\nplt.show()\n\nprint(\"\\n=== Fichiers generes ===\")\nfor f in [\"new_amca_vgg19_curves.png\", \"new_amca_vgg19_gradflow.png\",\n          \"new_amca_vgg19_perlayer.png\", \"new_amca_cutting_point.png\",\n          \"new_amca_vgg19_dashboard.png\", \"new_amca_vgg19_best.pth\"]:\n    p = os.path.join(OUTPUT_DIR, f)\n    if os.path.exists(p):\n        print(f\"  {f:45s} {os.path.getsize(p)/1024:7.1f} KB\")","metadata":{},"outputs":[],"execution_count":null}]}