{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.10"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"d80d1ef9-a2e7-48c9-a47c-1132a5b9ea12","cell_type":"markdown","source":"# Stage 1 — AMCA (Adaptive Model Cutting Algorithm) — **MobileNetV3Large**\nNotebook de **référence**. APTOS 2019, classification binaire DR / non-DR.\n\nPipeline : **A** Baseline → **B1** Détection d'anchors (topologique) → **C** Fine-tune de tous les anchors → `pick_winner`.\n\nPour adapter à un autre backbone : **seule la CELLULE 2 (CONFIG) change** (`build_backbone`, `PREP`, `BACKBONE_NAME`).","metadata":{}},{"id":"d2372e78-9cda-4202-821a-a4f48aefc6b7","cell_type":"code","source":"# ===== CELL 1 : Imports + hardware + seeds =====\nimport os, gc, json, time, math, random\nimport numpy as np, pandas as pd\nimport tensorflow as tf\nfrom tensorflow.keras import layers, Model, Input, mixed_precision\nfrom tensorflow.keras.layers import GlobalAveragePooling2D, Dropout, Dense\nfrom tensorflow.keras.callbacks import EarlyStopping, LearningRateScheduler\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom sklearn.metrics import roc_auc_score, roc_curve, confusion_matrix\nimport matplotlib.pyplot as plt\nimport cv2\n\nSEED = 42\nrandom.seed(SEED); np.random.seed(SEED); tf.random.set_seed(SEED)\nmixed_precision.set_global_policy('mixed_float16')   # T4 = tensor cores FP16\nimport logging; tf.get_logger().setLevel('ERROR'); logging.getLogger('tensorflow').setLevel('ERROR')\n\nprint('TF', tf.__version__)\ngpus = tf.config.list_physical_devices('GPU')\nprint('GPUs:', gpus)\n# T4 x2 -> MirroredStrategy pour utiliser les 2 GPU en parallele\nstrategy = tf.distribute.MirroredStrategy() if len(gpus) > 1 else tf.distribute.get_strategy()\nN_REPLICAS = strategy.num_replicas_in_sync\nprint('Replicas:', N_REPLICAS)","metadata":{},"outputs":[],"execution_count":null},{"id":"8ea33483-8fba-43b8-842c-6fcbf64d74c9","cell_type":"code","source":"# ===== CELL 2 : CONFIG  (LA SEULE PARTIE QUI CHANGE D'UN BACKBONE A L'AUTRE) =====\nfrom tensorflow.keras.applications import MobileNetV3Large\n\nBACKBONE_NAME = 'MobileNetV3Large'\nIMG_SIZE = 224          # taille COMMUNE aux 5 backbones -> comparaison equitable\nBATCH    = 32           # batch PAR GPU\nGLOBAL_BATCH = BATCH * N_REPLICAS   # batch global (T4 x2 -> 64)\nLR_SCALE = N_REPLICAS ** 0.5        # mise a l'echelle DOUCE (sqrt) du LR -> stabilite\n\ndef build_backbone(size):\n    # MobileNetV3 integre son preprocessing -> on lui donne des pixels [0,255]\n    return MobileNetV3Large(input_shape=(size, size, 3), include_top=False,\n                            weights='imagenet', include_preprocessing=True)\n\ndef PREP(x):            # x : float [0,255]\n    return x            # pass-through (preprocessing integre au modele MNV3)\n\n# --- Preprocessing (ablation) ---\nUSE_BEN_GRAHAM = False   # False = crop+resize (defaut) ; True = + ben_graham + masque circulaire 0.9\n\n# --- Hyperparametres AMCA ---\nTAU      = 0.015        # tolerance AUC (max 1.5% perdu vs baseline)\nPHI      = 0.40         # reduction FLOPs ciblee (preference dans pick_winner)\nE_HEAD   = 4            # epoques head-only\nE_FT     = 15           # epoques fine-tune complet\nLR_HEAD  = 1e-3\nLR_FT_MAX, LR_FT_MIN = 5e-5, 1e-6\nPATIENCE = 5\nWARMUP   = 2            # epoques de warmup (fine-tune) -> stabilite\nD_MIN    = 0.30         # profondeur min d'un anchor (filtre de pertinence)\nN_MAX    = 12           # nb max d'anchors a fine-tuner (borne le cout)\nDATA_DIR = '/kaggle/input/competitions/aptos2019-blindness-detection'\nOUT_DIR  = '/kaggle/working'","metadata":{},"outputs":[],"execution_count":null},{"id":"ac8ba3fe-525e-4f03-8f82-ac86908219f3","cell_type":"code","source":"# ===== CELL 3 : Preprocessing fundus =====\n# Base (TOUJOURS)        : crop bords noirs -> resize\n# Optionnel (USE_BEN_GRAHAM) : ben_graham (GaussianBlur + addWeighted) -> masque circulaire 0.9\ndef crop_retina(img, tol=7):\n    gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)\n    mask = gray > tol\n    if mask.sum() == 0:\n        return img\n    return img[np.ix_(mask.any(1), mask.any(0))]\n\ndef ben_graham(img, sigma=None, scale=4, bias=128):\n    if sigma is None:\n        sigma = img.shape[0] / 30.0                          # ~7-8 px pour 224\n    blurred = cv2.GaussianBlur(img, (0, 0), sigma)           # estime le fond (basse frequence)\n    out = cv2.addWeighted(img, scale, blurred, -scale, bias) # 4*(img - blurred) + 128  -> passe-haut\n    return np.clip(out, 0, 255).astype(np.uint8)\n\ndef circular_mask(img, radius_ratio=0.9):\n    h, w = img.shape[:2]\n    cy, cx = h // 2, w // 2\n    r = int(min(h, w) * radius_ratio / 2)\n    Y, X = np.ogrid[:h, :w]\n    m = (X - cx) ** 2 + (Y - cy) ** 2 <= r * r\n    out = img.copy(); out[~m] = 0\n    return out\n\ndef load_image(path, size=IMG_SIZE):\n    img = cv2.imread(path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img = crop_retina(img)\n    img = cv2.resize(img, (size, size), interpolation=cv2.INTER_AREA)\n    if USE_BEN_GRAHAM:\n        img = ben_graham(img, sigma=size / 30.0)   # passe-haut (inclut GaussianBlur)\n        img = circular_mask(img, 0.9)              # enleve l'anneau de bord cree par Ben Graham\n    return img.astype(np.uint8)","metadata":{},"outputs":[],"execution_count":null},{"id":"60db2e57-0cec-4139-a3ec-cbdc1f2080dc","cell_type":"code","source":"# ===== CELL 4 : Chargement APTOS, label binaire, split stratifie, preload =====\ndf = pd.read_csv(f'{DATA_DIR}/train.csv')\ndf['binary'] = (df['diagnosis'] > 0).astype(int)        # 0 = non-DR, 1..4 = DR\nprint(df['binary'].value_counts().to_dict())\n\ntr_df, va_df = train_test_split(df, test_size=0.2, stratify=df['binary'],\n                                random_state=SEED)\nprint('train', len(tr_df), '| val', len(va_df))\n\ndef preload(frame):\n    X = np.zeros((len(frame), IMG_SIZE, IMG_SIZE, 3), dtype=np.uint8)\n    y = frame['binary'].values.astype(np.float32)\n    for i, code_ in enumerate(frame['id_code'].values):\n        X[i] = load_image(f'{DATA_DIR}/train_images/{code_}.png')\n    return X, y\n\nX_train, y_train = preload(tr_df)\nX_val,   y_val   = preload(va_df)\nprint('shapes', X_train.shape, X_val.shape)\n\ncw = compute_class_weight('balanced', classes=np.array([0, 1]), y=y_train)\nCLASS_WEIGHT = {0: float(cw[0]), 1: float(cw[1])}\nprint('class_weight', CLASS_WEIGHT)","metadata":{},"outputs":[],"execution_count":null},{"id":"bd4367a5-bf29-4711-ab86-b53b6f67655c","cell_type":"code","source":"# ===== CELL 5 : Pipeline tf.data =====\nAUTOTUNE = tf.data.AUTOTUNE\n\ndef augment(x, y):\n    x = tf.image.random_flip_left_right(x)\n    x = tf.image.random_flip_up_down(x)\n    return x, y\n\ndef make_ds(X, y, training):\n    ds = tf.data.Dataset.from_tensor_slices((X, y))\n    if training:\n        ds = ds.shuffle(2048, seed=SEED).map(augment, AUTOTUNE)\n    return ds.batch(GLOBAL_BATCH).prefetch(AUTOTUNE)\n\ntrain_ds = make_ds(X_train, y_train, True)\nval_ds   = make_ds(X_val,   y_val,   False)","metadata":{},"outputs":[],"execution_count":null},{"id":"85347c2c-3637-44d8-aa09-31beb3203c55","cell_type":"code","source":"# ===== CELL 5b : Visualization BEFORE / AFTER preprocessing =====\nn_show = 4\nfig, axv = plt.subplots(2, n_show, figsize=(4*n_show, 8))\ncodes = tr_df['id_code'].values[:n_show]\nfor j in range(n_show):\n    raw = cv2.cvtColor(cv2.imread(f'{DATA_DIR}/train_images/{codes[j]}.png'), cv2.COLOR_BGR2RGB)\n    axv[0, j].imshow(raw)\n    axv[0, j].set_title(f'BEFORE ({\"DR\" if y_train[j]==1 else \"non-DR\"})'); axv[0, j].axis('off')\n    axv[1, j].imshow(X_train[j])\n    axv[1, j].set_title('AFTER'); axv[1, j].axis('off')\nmode_prep = 'Ben Graham + crop' if USE_BEN_GRAHAM else 'crop + resize'\nplt.suptitle(f'Preprocessing: BEFORE (raw)  vs  AFTER ({mode_prep}, {IMG_SIZE}x{IMG_SIZE})')\nplt.tight_layout(); plt.savefig(f'{OUT_DIR}/preprocessing_before_after.png', dpi=120); plt.show()","metadata":{},"outputs":[],"execution_count":null},{"id":"b63dbdce-a5ad-4052-8aba-c4821e514042","cell_type":"code","source":"# ===== CELL 5c : Data augmentation - real counts + visual demo =====\n# NOTE: augmentation is applied ON-THE-FLY during training (tf.data .map).\n# It creates NO stored images -> the dataset size is unchanged. Each epoch every\n# image is shown once with a fresh random transform. There is therefore no fixed\n# \"augmented image count\": below we report the REAL counts (a) and show what the\n# augmentation actually produces on one image (b).\n\n# --- (a) real image counts ---\nn_tr, n_va = len(y_train), len(y_val)\ntr0, tr1 = int((y_train == 0).sum()), int((y_train == 1).sum())\nva0, va1 = int((y_val == 0).sum()),   int((y_val == 1).sum())\nprint(f'Train images : {n_tr}  (non-DR={tr0}, DR={tr1})')\nprint(f'Val   images : {n_va}  (non-DR={va0}, DR={va1})')\nprint('Augmentation : on-the-fly random H/V flips (nothing stored; dataset size unchanged)')\n\nfig, (axa, axb) = plt.subplots(1, 2, figsize=(13, 4.5))\nb1 = axa.bar(['Train', 'Validation'], [n_tr, n_va], color=['#4C72B0', '#C44E52'], width=0.6)\nfor b, v in zip(b1, [n_tr, n_va]):\n    axa.text(b.get_x()+b.get_width()/2, v, f'{v:,}', ha='center', va='bottom', fontweight='bold')\naxa.set_ylabel('Number of images'); axa.set_title('Dataset split (APTOS 2019, binary)')\naxa.grid(axis='y', alpha=0.3)\n\nxc = np.arange(2); w = 0.38\naxb.bar(xc - w/2, [tr0, tr1], w, label='Train', color='#4C72B0')\naxb.bar(xc + w/2, [va0, va1], w, label='Validation', color='#C44E52')\nfor k, (a, bb) in enumerate([(tr0, va0), (tr1, va1)]):\n    axb.text(k - w/2, a,  f'{a:,}',  ha='center', va='bottom', fontsize=9)\n    axb.text(k + w/2, bb, f'{bb:,}', ha='center', va='bottom', fontsize=9)\naxb.set_xticks(xc); axb.set_xticklabels(['non-DR', 'DR'])\naxb.set_ylabel('Number of images'); axb.set_title('Per-class count (train vs validation)')\naxb.legend(); axb.grid(axis='y', alpha=0.3)\nplt.suptitle('Dataset composition (augmentation is applied on-the-fly, not stored)',\n             fontweight='bold')\nplt.tight_layout(); plt.savefig(f'{OUT_DIR}/dataset_counts.png', dpi=120); plt.show()\n\n# --- (b) visual demo : one image -> several random augmentations ---\nk = 0\nbase = X_train[k]\nn_aug_show = 7\nfig, axd = plt.subplots(1, n_aug_show + 1, figsize=(2.2*(n_aug_show + 1), 2.8))\naxd[0].imshow(base); axd[0].set_title('Original'); axd[0].axis('off')\nfor j in range(1, n_aug_show + 1):\n    out, _ = augment(tf.constant(base), tf.constant(0.0))   # same augment() used in training\n    axd[j].imshow(np.asarray(out).astype('uint8'))\n    axd[j].set_title(f'Augmented #{j}'); axd[j].axis('off')\nplt.suptitle('On-the-fly data augmentation (random horizontal / vertical flips applied each epoch)',\n             fontweight='bold')\nplt.tight_layout(); plt.savefig(f'{OUT_DIR}/augmentation_examples.png', dpi=120); plt.show()","metadata":{},"outputs":[],"execution_count":null},{"id":"37a872ec-1ad3-41ab-9cdb-1460016d57f6","cell_type":"code","source":"# ===== CELL 6 : Utilitaires (FLOPs, taille, latence, head) =====\nclass CastF32(layers.Layer):\n    # Keras 3 : on ne peut plus appeler tf.cast sur un tenseur symbolique -> on l'enveloppe dans une couche\n    def call(self, x):\n        return tf.cast(x, tf.float32)\nfrom tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2\n\ndef compute_flops(model):\n    try:\n        spec = tf.TensorSpec([1] + list(model.inputs[0].shape[1:]), model.inputs[0].dtype)\n        conc = tf.function(lambda x: model(x)).get_concrete_function(spec)\n        frozen = convert_variables_to_constants_v2(conc)\n        run_meta = tf.compat.v1.RunMetadata()\n        opts = (tf.compat.v1.profiler.ProfileOptionBuilder(\n                    tf.compat.v1.profiler.ProfileOptionBuilder.float_operation())\n                .with_empty_output().build())   # pas de dump stdout\n        f = tf.compat.v1.profiler.profile(graph=frozen.graph, run_meta=run_meta,\n                                          cmd='op', options=opts)\n        return int(f.total_float_ops)\n    except Exception as e:\n        print('FLOPs warn:', e); return float('nan')\n\ndef model_size_mb(model):\n    return float(sum(np.prod(w.shape) for w in model.get_weights()) * 4 / (1024**2))\n\ndef measure_latency(model, n=50, warmup=10):\n    x = tf.zeros([1, IMG_SIZE, IMG_SIZE, 3], dtype=model.inputs[0].dtype)\n    for _ in range(warmup): _ = model(x, training=False)\n    t0 = time.time()\n    for _ in range(n): _ = model(x, training=False)\n    return (time.time() - t0) / n * 1000.0    # ms / image\n\ndef head(feat):\n    g = GlobalAveragePooling2D()(feat)\n    g = Dropout(0.3)(g)\n    return Dense(1, activation='sigmoid', dtype='float32')(g)\n\nclass OptAccCB(tf.keras.callbacks.Callback):\n    # accuracy de validation au SEUIL OPTIMAL (Youden) a chaque epoque -> courbe interpretable\n    def __init__(self, X, y):\n        super().__init__(); self.X = X; self.y = y; self.acc_opt = []\n    def on_epoch_end(self, epoch, logs=None):\n        p = self.model.predict(self.X, batch_size=GLOBAL_BATCH, verbose=0).ravel()\n        p = np.nan_to_num(p, nan=0.5)\n        try:\n            fpr, tpr, thr = roc_curve(self.y, p)\n            t = float(thr[np.argmax(tpr - fpr)])\n            self.acc_opt.append(float(((p >= t).astype(int) == self.y).mean()))\n        except Exception:\n            self.acc_opt.append(float('nan'))\n","metadata":{},"outputs":[],"execution_count":null},{"id":"f1cfb4fb-ad09-4a58-8b00-04063fcdc7ff","cell_type":"code","source":"# ===== CELL 7 : PHASE A - Baseline (ImageNet utilise ICI uniquement) =====\ntf.keras.backend.clear_session()\nwith strategy.scope():\n    backbone = build_backbone(IMG_SIZE)          # contiendra les POIDS APTOS apres entrainement\n    inp  = Input((IMG_SIZE, IMG_SIZE, 3), dtype='uint8')\n    x    = PREP(CastF32()(inp))\n    feat = backbone(x)\n    baseline = Model(inp, head(feat), name='baseline')\n\nMETRICS = ['accuracy', tf.keras.metrics.AUC(name='auc')]\nBASELINE_T0 = time.time()\n\n# Phase 1 : head-only\nbackbone.trainable = False\nwith strategy.scope():\n    baseline.compile(optimizer=tf.keras.optimizers.Adam(LR_HEAD * LR_SCALE),\n                     loss='binary_crossentropy', metrics=METRICS)\nbaseline.fit(train_ds, validation_data=val_ds, epochs=E_HEAD,\n             class_weight=CLASS_WEIGHT, verbose=2)\n\n# Phase 2 : fine-tune complet (BN gelee)\nbackbone.trainable = True\nfor l in backbone.layers:\n    if isinstance(l, layers.BatchNormalization):\n        l.trainable = False\n\ndef cosine(epoch):\n    if epoch < WARMUP:                                   # warmup lineaire -> evite l'instabilite\n        base = LR_FT_MAX * (epoch + 1) / WARMUP\n    else:\n        prog = (epoch - WARMUP) / max(1, E_FT - WARMUP)\n        base = LR_FT_MIN + 0.5*(LR_FT_MAX - LR_FT_MIN)*(1 + math.cos(math.pi*prog))\n    return base * LR_SCALE\n\nwith strategy.scope():\n    baseline.compile(optimizer=tf.keras.optimizers.Adam(LR_FT_MAX * LR_SCALE),\n                     loss='binary_crossentropy', metrics=METRICS)\nes = EarlyStopping(monitor='val_auc', mode='max', patience=PATIENCE, restore_best_weights=True)\nbase_cb = OptAccCB(X_val, y_val)\n_bh = baseline.fit(train_ds, validation_data=val_ds, epochs=E_FT, class_weight=CLASS_WEIGHT,\n                   callbacks=[es, LearningRateScheduler(cosine), base_cb], verbose=2)\nBASE_HIST = _bh.history\nBASE_ACCOPT = base_cb.acc_opt\nBASELINE_TIME = time.time() - BASELINE_T0\n\np = baseline.predict(X_val, batch_size=GLOBAL_BATCH, verbose=0).ravel()\nA0_AUC   = float(roc_auc_score(y_val, p))\nA0_ACC   = float(((p >= 0.5).astype(int) == y_val).mean())\n_fpr, _tpr, _thr = roc_curve(y_val, p)\nA0_THR   = float(_thr[np.argmax(_tpr - _fpr)])\nA0_ACC_OPT = float(((p >= A0_THR).astype(int) == y_val).mean())\nA0_FLOPS = compute_flops(Model(backbone.input, backbone.output))\nA0_LAT   = measure_latency(baseline)\nA0_SIZE  = model_size_mb(baseline)\nprint(f'BASELINE  AUC={A0_AUC:.4f}  ACC@0.5={A0_ACC:.4f}  ACC@opt={A0_ACC_OPT:.4f}  '\n      f'FLOPs={A0_FLOPS/1e6:.1f}M  lat={A0_LAT:.2f}ms  size={A0_SIZE:.1f}MB')\nBASE_W = backbone.get_weights()   # snapshot des poids APTOS PROPRES (pour fine-tunes independants)\n","metadata":{},"outputs":[],"execution_count":null},{"id":"f3533f4d-6b66-4353-b9f4-0e34d7da9f7c","cell_type":"code","source":"# ===== CELL 8 : PHASE B1 - Anchor detection (tensor liveness) =====\n# An anchor = a layer after which a SINGLE tensor is still alive (4D output).\n# Topological method, generic (no dependency on layer names).\ndef detect_anchors(model):\n    L = model.layers\n    producer = {}\n    for i, l in enumerate(L):\n        try:\n            outs = l.output if isinstance(l.output, list) else [l.output]\n        except Exception:\n            outs = []\n        for t in outs: producer[id(t)] = i\n    last_use = {}\n    for j, l in enumerate(L):\n        try:\n            ins = l.input if isinstance(l.input, list) else [l.input]\n        except Exception:\n            ins = []\n        for t in ins:\n            pid = producer.get(id(t))\n            if pid is not None:\n                last_use[pid] = max(last_use.get(pid, pid), j)\n    anchors = []\n    for i, l in enumerate(L):\n        live = [p for p in range(i + 1) if last_use.get(p, p) > i]\n        try:\n            out = l.output\n            is4d = (not isinstance(out, list)) and len(out.shape) == 4\n        except Exception:\n            is4d = False\n        if live == [i] and is4d:\n            anchors.append((i, l.name))\n    return anchors\n\nall_anchors = detect_anchors(backbone)\nn_layers = len(backbone.layers)\n\n# --- (1) ALL detected cut points ---\nprint(f'(1) ALL cut points detected: {len(all_anchors)} over {n_layers} layers')\nfor i, nm in all_anchors:\n    print(f'      [{i:3d}]  depth={i/n_layers:.2f}  {nm}')\n\n# --- (2) VALID cut points : those past the minimum-depth threshold D_MIN ---\nvalid = [(i, nm) for (i, nm) in all_anchors if i / n_layers >= D_MIN]\nprint(f'\\n(2) VALID cut points (depth >= D_MIN = {D_MIN:.2f}): {len(valid)}')\nfor i, nm in valid:\n    print(f'      [{i:3d}]  depth={i/n_layers:.2f}  {nm}')\n\n# --- (3) CHOSEN anchors : even subsampling in FLOPs-REDUCTION space (<= N_MAX) ---\n# Even spacing in layer INDEX can miss whole compression ranges on architectures\n# whose cost is not linear in depth (e.g. DenseNet121: the deep anchors cluster\n# at small FLOPs reductions). We therefore measure the FLOPs of every valid cut\n# and keep the N_MAX anchors whose reductions are evenly spread between the\n# least and the most compressed valid cut -> candidates AROUND the target PHI\n# are guaranteed for every backbone.\nprint(f'\\nMeasuring FLOPs of each valid cut ({len(valid)} cuts, ~1-2 min)...')\nred_of = {}\nfor i, nm in valid:\n    _f = compute_flops(Model(backbone.input, backbone.layers[i].output))\n    red_of[i] = 1.0 - _f / A0_FLOPS\n    print(f'      [{i:3d}]  depth={i/n_layers:.2f}  red={100*red_of[i]:5.1f}%  {nm}')\n\nfilt = valid[:]\nif len(filt) > N_MAX:\n    by_red  = sorted(filt, key=lambda a: red_of[a[0]])         # ascending reduction\n    reds    = np.array([red_of[i] for i, _ in by_red])\n    targets = np.linspace(reds.min(), reds.max(), N_MAX)       # even targets in reduction space\n    picked, used = [], set()\n    for t in targets:\n        k = int(np.argmin(np.abs(reds - t)))                   # nearest valid cut\n        while k in used:                                       # avoid duplicates\n            k = (k + 1) % len(by_red)\n        used.add(k); picked.append(by_red[k])\n    filt = sorted(picked, key=lambda a: a[0])                  # back to index order\nchosen_idx = set(i for (i, _) in filt)\nfilt_bw = list(reversed(filt))                       # deep -> shallow\nanchors_backward = [i for (i, _) in filt_bw]         # INDICES only (used downstream)\nprint(f'\\n(3) CHOSEN anchors (<= N_MAX = {N_MAX}, evenly spread in FLOPs reduction): {len(anchors_backward)}')\nfor i, nm in filt_bw:\n    print(f'      [{i:3d}]  depth={i/n_layers:.2f}  red={100*red_of[i]:5.1f}%  {nm}')\n\n# --- Visualization : depth axis (top) + FLOPs-reduction axis (bottom) ---\nfig, (ax, ax2) = plt.subplots(2, 1, figsize=(13, 5.6))\nrej  = [i/n_layers for (i, _) in all_anchors if i/n_layers <  D_MIN]\nkeep = [i/n_layers for (i, _) in valid if i not in chosen_idx]\ncho  = [i/n_layers for (i, _) in valid if i in chosen_idx]\nax.scatter(rej,  [0]*len(rej),  s=55,  c='#BBBBBB', marker='o',\n           label=f'Rejected (depth < {D_MIN:.2f}): {len(rej)}', zorder=2)\nax.scatter(keep, [0]*len(keep), s=70,  c='#4C72B0', marker='o',\n           label=f'Valid cut points: {len(valid)}', zorder=3)\nax.scatter(cho,  [0]*len(cho),  s=200, c='#C44E52', marker='*',\n           edgecolors='k', linewidths=0.6,\n           label=f'Chosen anchors (N_MAX = {N_MAX}): {len(cho)}', zorder=4)\nax.axvline(D_MIN, color='#DD8452', ls='--', lw=2,\n           label=f'Minimum-depth threshold D_MIN = {D_MIN:.2f}')\nax.set_yticks([]); ax.set_xlim(-0.02, 1.02)\nax.set_xlabel('Normalized network depth  (layer index / total layers)')\nax.set_title(f'{BACKBONE_NAME} - cut-point selection: '\n             f'{len(all_anchors)} detected -> {len(valid)} valid -> {len(anchors_backward)} chosen')\nax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.32), ncol=2, frameon=False)\nax.grid(axis='x', alpha=0.25)\nkeep_r = [red_of[i] for (i, _) in valid if i not in chosen_idx]\ncho_r  = [red_of[i] for (i, _) in valid if i in chosen_idx]\nax2.scatter(keep_r, [0]*len(keep_r), s=70,  c='#4C72B0', marker='o',\n            label='valid cuts', zorder=3)\nax2.scatter(cho_r,  [0]*len(cho_r),  s=200, c='#C44E52', marker='*',\n            edgecolors='k', linewidths=0.6,\n            label='chosen (even spacing in reduction)', zorder=4)\nax2.axvline(PHI, color='#27AE60', ls='--', lw=2, label=f'compression target PHI = {PHI:.2f}')\nax2.set_yticks([]); ax2.set_xlim(-0.02, 1.02)\nax2.set_xlabel('FLOPs reduction vs baseline')\nax2.legend(loc='upper center', bbox_to_anchor=(0.5, -0.38), ncol=3, frameon=False)\nax2.grid(axis='x', alpha=0.25)\nplt.tight_layout(); plt.savefig(f'{OUT_DIR}/anchor_cut_points.png', dpi=120, bbox_inches='tight'); plt.show()\n","metadata":{},"outputs":[],"execution_count":null},{"id":"8b6720b9-ce8c-47be-b18a-5b4f0fd7b5f3","cell_type":"code","source":"# ===== CELL 9 : PHASE C - Fine-tune de TOUS les anchors (plus de probe) =====\n# Sans sonde : on fine-tune chaque anchor detecte en B1, puis pick_winner (Phase D)\n# choisit le meilleur sur les metriques REELLES du fine-tuning.\ndef cut_backbone(idx):\n    return Model(backbone.input, backbone.layers[idx].output)\n\nto_finetune = list(anchors_backward)          # tous les anchors retenus en B1\nprint(f'Fine-tune de {len(to_finetune)} anchor(s) (tous les anchors detectes) : {to_finetune}')\n","metadata":{},"outputs":[],"execution_count":null},{"id":"e4c5670d-1937-468e-8b42-21e14e1975c3","cell_type":"code","source":"# ===== CELL 11 : PHASE D - Fine-tune des finalistes (gradual unfreezing) =====\ndef build_cut_full(idx):\n    bb = build_backbone(IMG_SIZE)          # backbone INDEPENDANT -> evite le partage de poids entre coupes\n    bb.set_weights(BASE_W)                 # poids APTOS du baseline (propres)\n    sub = Model(bb.input, bb.layers[idx].output)\n    inp_ = Input((IMG_SIZE, IMG_SIZE, 3), dtype='uint8')\n    x_   = PREP(CastF32()(inp_))\n    return Model(inp_, head(sub(x_))), sub\n\ndef finetune(idx):\n    _ft0 = time.time()\n    with strategy.scope():\n        m, sub = build_cut_full(idx)\n        sub.trainable = False                      # Phase 1 : head-only\n        m.compile(optimizer=tf.keras.optimizers.Adam(LR_HEAD * LR_SCALE),\n                  loss='binary_crossentropy', metrics=['accuracy', tf.keras.metrics.AUC(name='auc')])\n    m.fit(train_ds, validation_data=val_ds, epochs=E_HEAD, class_weight=CLASS_WEIGHT, verbose=2)\n    with strategy.scope():\n        sub.trainable = True                       # Phase 2 : complet, BN gelee sauf dernier stage\n        bns = [l for l in sub.layers if isinstance(l, layers.BatchNormalization)]\n        keep_bn = set(id(l) for l in bns[-2:])\n        for l in sub.layers:\n            if isinstance(l, layers.BatchNormalization) and id(l) not in keep_bn:\n                l.trainable = False\n        m.compile(optimizer=tf.keras.optimizers.Adam(LR_FT_MAX * LR_SCALE),\n                  loss='binary_crossentropy', metrics=['accuracy', tf.keras.metrics.AUC(name='auc')])\n    es = EarlyStopping(monitor='val_auc', mode='max', patience=PATIENCE, restore_best_weights=True)\n    cb_acc = OptAccCB(X_val, y_val)\n    h = m.fit(train_ds, validation_data=val_ds, epochs=E_FT, class_weight=CLASS_WEIGHT,\n              callbacks=[es, LearningRateScheduler(cosine), cb_acc], verbose=2)\n    p = m.predict(X_val, batch_size=GLOBAL_BATCH, verbose=0).ravel()\n    auc = float(roc_auc_score(y_val, p))\n    acc05 = float(((p >= 0.5).astype(int) == y_val).mean())\n    fpr, tpr, thr = roc_curve(y_val, p)                  # seuil optimal (Youden J)\n    thr_opt = float(thr[np.argmax(tpr - fpr)])\n    acc_opt = float(((p >= thr_opt).astype(int) == y_val).mean())\n    flops = compute_flops(cut_backbone(idx))\n    w = m.get_weights()                                  # on garde les POIDS (pas le modele) -> pas de corruption\n    lat = measure_latency(m); sz = model_size_mb(m); npar = int(m.count_params())\n    del m, sub; gc.collect()\n    ft_time = time.time() - _ft0\n    print(f'>>> cut={idx}  AUC={auc:.4f}  ACC@0.5={acc05:.4f}  ACC@opt={acc_opt:.4f}  '\n          f'(thr={thr_opt:.2f})  FLOPs_red={100*(1-flops/A0_FLOPS):.1f}%')\n    return dict(cut_idx=idx, final_auc=auc, final_acc=acc05, acc_opt=acc_opt, thr_opt=thr_opt,\n                flops=flops, flops_red=float(1 - flops / A0_FLOPS),\n                size_mb=sz, params=npar, latency_ms=lat, ft_time=ft_time, history=h.history, accopt_curve=cb_acc.acc_opt, weights=w)\n\nG = [finetune(int(i)) for i in to_finetune]\ndfG = pd.DataFrame([{k: g[k] for k in\n        ('cut_idx','final_auc','final_acc','acc_opt','thr_opt','flops_red','latency_ms','size_mb','params')}\n       for g in G]).sort_values('cut_idx').reset_index(drop=True)\ndfG.to_csv(f'{OUT_DIR}/amca_finetune.csv', index=False)\ndfG\n","metadata":{},"outputs":[],"execution_count":null},{"id":"8b3de975-fef8-45b4-9c7a-004ca84ae32f","cell_type":"code","source":"# ===== CELL 12 : pick_winner deterministe + sauvegarde =====\ndef pick_winner(G):\n    # tolerance RELATIVE AU BASELINE (reference fixe) -> coherent entre fast et exhaustif\n    W = [g for g in G if g['final_auc'] >= A0_AUC - TAU and g['acc_opt'] >= A0_ACC_OPT - TAU]\n    if not W:\n        W = [g for g in G if g['final_auc'] >= A0_AUC - TAU]\n    prefer = [g for g in W if g['flops_red'] >= PHI]\n    pool = prefer if prefer else W\n    return max(pool, key=lambda g: g['flops_red'])\n\nWIN = pick_winner(G)\n# reconstruire le modele gagnant a partir de ses POIDS sauvegardes (modele propre, non corrompu)\nWIN_model, _ = build_cut_full(WIN['cut_idx'])\nWIN_model.set_weights(WIN['weights'])\nWIN['model'] = WIN_model\nWIN_model.save_weights(f'{OUT_DIR}/stage1_final.weights.h5')\nWIN_NAME = backbone.layers[WIN['cut_idx']].name\nprint(f\"WINNER  cut={WIN['cut_idx']} ({WIN_NAME})  \"\n      f\"AUC={WIN['final_auc']:.4f}  ACC@0.5={WIN['final_acc']:.4f}  ACC@opt={WIN['acc_opt']:.4f}  \"\n      f\"(thr={WIN['thr_opt']:.2f})  FLOPs_red={100*WIN['flops_red']:.1f}%  \"\n      f\"lat={WIN['latency_ms']:.2f}ms  size={WIN['size_mb']:.1f}MB\")\n","metadata":{},"outputs":[],"execution_count":null},{"id":"42178834-1753-4d36-854f-fa4c80f0b32d","cell_type":"code","source":"# ===== CELL 12b : Final model (winner) - block map =====\n# Affiche les blocs du backbone : ce que la coupe GARDE et ce qu'elle SUPPRIME.\nimport re as _re\nfrom matplotlib.patches import Patch\n_cut = WIN['cut_idx']; _L = backbone.layers; _n = len(_L)\n\ndef _block_of(name):\n    m = _re.match(r'(.+?block[_]?\\d+)', name)        # conv4_block12, block_12, blocks_3 ...\n    if m: return m.group(1)\n    return name.split('/')[0].split('_')[0]          # sinon : prefixe de stage (conv1, stem, ...)\n\n_blocks = []\nfor _i, _l in enumerate(_L):\n    _b = _block_of(_l.name)\n    if not _blocks or _blocks[-1][0] != _b: _blocks.append([_b, _i, _i])\n    else: _blocks[-1][2] = _i\n\nprint(f\"WINNER : cut_idx={_cut}  ({_L[_cut].name})  -  {_cut+1}/{_n} layers kept\")\nprint(f\"params={WIN['params']:,}   FLOPs={WIN['flops']/1e6:.1f}M  (red={100*WIN['flops_red']:.1f}%)   \"\n      f\"size={WIN['size_mb']:.1f}MB   latency={WIN['latency_ms']:.2f}ms\\n\")\nprint(f\"{'block':<30}{'layers':>12}   status\")\nfor _b, _s, _e in _blocks:\n    if _e <= _cut:   _st = 'KEPT'\n    elif _s > _cut:  _st = 'REMOVED'\n    else:            _st = f'PARTIAL (kept up to layer {_cut})'\n    print(f\"{_b:<30}{f'{_s}-{_e}':>12}   {_st}\")\n\nfig, ax = plt.subplots(figsize=(13, 2.9))\nfor _b, _s, _e in _blocks:\n    if _s <= _cut:\n        ax.barh(0, min(_e, _cut) - _s + 1, left=_s, height=0.5,\n                color='#27AE60', edgecolor='white', linewidth=0.6)\n    if _e > _cut:\n        _rs = max(_s, _cut + 1)\n        ax.barh(0, _e - _rs + 1, left=_rs, height=0.5,\n                color='#E74C3C', alpha=0.45, edgecolor='white', linewidth=0.6)\n    ax.text((_s + _e) / 2, 0.42, _b, rotation=90, ha='center', va='bottom', fontsize=7)\nax.axvline(_cut + 0.5, color='#E67E22', lw=2.5, ls='--')\nax.text(_cut + 0.5, -0.42, f\"cut_idx={_cut}\\n{_L[_cut].name}\",\n        color='#E67E22', ha='center', va='top', fontsize=8, fontweight='bold')\nax.legend(handles=[Patch(color='#27AE60', label='kept'),\n                   Patch(color='#E74C3C', alpha=0.45, label='removed')],\n          loc='upper right', frameon=False)\nax.set_yticks([]); ax.set_xlim(0, _n); ax.set_ylim(-1.7, 2.4)\nax.set_xlabel('layer index')\nax.set_title(f'{BACKBONE_NAME} - final (winner) model : kept vs removed blocks')\nplt.tight_layout(); plt.savefig(f'{OUT_DIR}/winner_blocks.png', dpi=130, bbox_inches='tight'); plt.show()\n","metadata":{},"outputs":[],"execution_count":null},{"id":"11278c91-e03d-48b3-a2da-33d54e52df04","cell_type":"code","source":"# ===== CELL 13 : Courbes d'entrainement (baseline vs winner) =====\nh = WIN['history']\nfig, ax = plt.subplots(1, 3, figsize=(16, 4.5))\n# --- AUC ---\nif 'auc' in h:     ax[0].plot(range(1, len(h['auc'])+1), h['auc'], color='tab:orange', label='winner (train)')\nif 'val_auc' in h: ax[0].plot(range(1, len(h['val_auc'])+1), h['val_auc'], color='tab:red', label='winner (val)')\nif 'val_auc' in BASE_HIST:\n    ax[0].plot(range(1, len(BASE_HIST['val_auc'])+1), BASE_HIST['val_auc'], '--', color='gray', label='baseline (val)')\nax[0].set_title('AUC'); ax[0].set_xlabel('epoch'); ax[0].legend(); ax[0].grid(alpha=.3)\n# --- Loss ---\nif 'loss' in h:     ax[1].plot(range(1, len(h['loss'])+1), h['loss'], color='tab:orange', label='winner (train)')\nif 'val_loss' in h: ax[1].plot(range(1, len(h['val_loss'])+1), h['val_loss'], color='tab:red', label='winner (val)')\nif 'val_loss' in BASE_HIST:\n    ax[1].plot(range(1, len(BASE_HIST['val_loss'])+1), BASE_HIST['val_loss'], '--', color='gray', label='baseline (val)')\nax[1].set_title('Loss'); ax[1].set_xlabel('epoch'); ax[1].legend(); ax[1].grid(alpha=.3)\n# --- Accuracy au SEUIL OPTIMAL (pas 0.5) ---\nax[2].plot(range(1, len(WIN['accopt_curve'])+1), WIN['accopt_curve'], color='tab:red', label='winner (val)')\nax[2].plot(range(1, len(BASE_ACCOPT)+1), BASE_ACCOPT, '--', color='gray', label='baseline (val)')\nax[2].set_title('Accuracy @ seuil optimal'); ax[2].set_xlabel('epoch'); ax[2].legend(); ax[2].grid(alpha=.3)\nplt.suptitle(f\"{BACKBONE_NAME} - baseline vs winner (cut {WIN['cut_idx']})\")\nplt.tight_layout(); plt.savefig(f'{OUT_DIR}/curves_winner.png', dpi=120); plt.show()","metadata":{},"outputs":[],"execution_count":null},{"id":"73e6cb50-1bdb-4118-98a6-3d8900dcb1c1","cell_type":"code","source":"# ===== CELL 14 : Front de Pareto (metriques REELLES) + comparatif baseline vs winner =====\n# --- Front de Pareto : tous les anchors fine-tunes + winner ---\nfig, axp = plt.subplots(figsize=(7.5, 5))\naxp.scatter(dfG['flops_red']*100, dfG['final_auc'], c='lightgray', s=55, edgecolor='k',\n            label='anchors (fine-tunes)', zorder=1)\naxp.scatter([WIN['flops_red']*100], [WIN['final_auc']], marker='*', s=420, c='gold',\n            edgecolor='k', label='winner', zorder=4)\naxp.axhline(A0_AUC - TAU, ls='--', c='red', label=f'seuil AUC ({A0_AUC-TAU:.3f})')\naxp.axvline(PHI*100, ls=':', c='gray', label=f'cible FLOPs ({PHI*100:.0f}%)')\naxp.set_xlabel('FLOPs reduction (%)'); axp.set_ylabel('AUC (fine-tune)')\naxp.set_title(f'{BACKBONE_NAME} - Pareto : anchors fine-tunes, winner')\naxp.legend(); axp.grid(alpha=.3)\nplt.tight_layout(); plt.savefig(f'{OUT_DIR}/pareto.png', dpi=120); plt.show()\n\n# --- Comparatif baseline vs winner : une barre par metrique (echelles separees) ---\nmetrics = [('AUC',          A0_AUC,        WIN['final_auc']),\n           ('ACC@opt',      A0_ACC_OPT,    WIN['acc_opt']),\n           ('FLOPs (M)',    A0_FLOPS/1e6,  WIN['flops']/1e6),\n           ('Latence (ms)', A0_LAT,        WIN['latency_ms']),\n           ('Taille (MB)',  A0_SIZE,       WIN['size_mb'])]\nfig, axes = plt.subplots(1, 5, figsize=(19, 4))\nfor axb, (name, bv, wv) in zip(axes, metrics):\n    bars = axb.bar(['baseline', 'winner'], [bv, wv], color=['tab:gray', 'tab:orange'])\n    axb.set_title(name); axb.grid(alpha=.3, axis='y')\n    for rect, v in zip(bars, [bv, wv]):\n        txt = f'{v:.3f}' if v < 10 else f'{v:.1f}'\n        axb.text(rect.get_x()+rect.get_width()/2, v, txt, ha='center', va='bottom', fontsize=9)\nplt.suptitle(f'{BACKBONE_NAME} - Baseline vs Winner')\nplt.tight_layout(); plt.savefig(f'{OUT_DIR}/compare.png', dpi=120); plt.show()\n","metadata":{},"outputs":[],"execution_count":null},{"id":"5f8268ec-0693-49cb-96a8-545b85204a3b","cell_type":"code","source":"# ===== CELL 15 : ROC overlay + matrice de confusion (winner, au SEUIL OPTIMAL) =====\np_base = baseline.predict(X_val, batch_size=GLOBAL_BATCH, verbose=0).ravel()\np_win  = WIN['model'].predict(X_val, batch_size=GLOBAL_BATCH, verbose=0).ravel()\n\nfig, ax = plt.subplots(1, 2, figsize=(12, 5))\nfor p_, lab in [(p_base, 'baseline'), (p_win, 'winner')]:\n    fpr, tpr, _ = roc_curve(y_val, p_)\n    ax[0].plot(fpr, tpr, label=f'{lab} (AUC={roc_auc_score(y_val, p_):.3f})')\nax[0].plot([0, 1], [0, 1], 'k--', alpha=.4)\nax[0].set_xlabel('FPR'); ax[0].set_ylabel('TPR'); ax[0].set_title('ROC'); ax[0].legend(); ax[0].grid(alpha=.3)\n\n# Matrice de confusion au SEUIL OPTIMAL (pas 0.5) = point de fonctionnement reel\nthr = WIN['thr_opt']\npred = (p_win >= thr).astype(int)\ncm = confusion_matrix(y_val, pred)\ntn, fp, fn, tp = cm.ravel()\nsens = tp / (tp + fn) if (tp + fn) else 0.0      # sensibilite (recall DR) : ne pas rater un malade\nspec = tn / (tn + fp) if (tn + fp) else 0.0      # specificite\nax[1].imshow(cm, cmap='Blues')\nfor (r, c), v in np.ndenumerate(cm):\n    ax[1].text(c, r, str(v), ha='center', va='center', fontsize=12)\nax[1].set_xticks([0, 1]); ax[1].set_yticks([0, 1])\nax[1].set_xticklabels(['non-DR', 'DR']); ax[1].set_yticklabels(['non-DR', 'DR'])\nax[1].set_xlabel('Predit'); ax[1].set_ylabel('Reel')\nax[1].set_title(f'Confusion @ seuil {thr:.2f}\\nsensibilite={sens:.3f}  specificite={spec:.3f}')\nplt.tight_layout(); plt.savefig(f'{OUT_DIR}/roc_cm.png', dpi=120); plt.show()\nprint(f'Seuil optimal={thr:.3f}  sensibilite(DR)={sens:.3f}  specificite={spec:.3f}  FN={fn}  FP={fp}')","metadata":{},"outputs":[],"execution_count":null},{"id":"b3ee78f5-673f-4fe4-b586-53161b54c195","cell_type":"code","source":"# ===== CELL 16 : Resume JSON complet =====\nsummary = dict(\n    backbone=BACKBONE_NAME, img_size=IMG_SIZE, use_ben_graham=USE_BEN_GRAHAM,\n    baseline=dict(auc=A0_AUC, acc=A0_ACC, acc_opt=A0_ACC_OPT, thr_opt=A0_THR,\n                  flops=int(A0_FLOPS), latency_ms=A0_LAT, size_mb=A0_SIZE),\n    winner=dict(cut_idx=WIN['cut_idx'], anchor=backbone.layers[WIN['cut_idx']].name,\n                auc=WIN['final_auc'], acc=WIN['final_acc'], acc_opt=WIN['acc_opt'],\n                thr_opt=WIN['thr_opt'], flops=int(WIN['flops']),\n                flops_red=WIN['flops_red'], latency_ms=WIN['latency_ms'],\n                size_mb=WIN['size_mb'], params=WIN['params']),\n    finalists=[{k: v for k, v in g.items() if k not in ('model', 'history', 'weights', 'accopt_curve')} for g in G],\n    hyperparams=dict(tau=TAU, phi=PHI, e_head=E_HEAD, e_ft=E_FT,\n                     img_size=IMG_SIZE, d_min=D_MIN, n_max=N_MAX),\n)\nwith open(f'{OUT_DIR}/stage1_summary.json', 'w') as fjson:\n    json.dump(summary, fjson, indent=2, default=float)\nprint(json.dumps(summary['winner'], indent=2))\nprint('Fichiers produits :', [x for x in os.listdir(OUT_DIR) if x.startswith('stage1') or x.endswith(('.png','.csv'))])","metadata":{},"outputs":[],"execution_count":null},{"id":"93101e81-c494-46bf-90f4-dd4f084715e4","cell_type":"code","source":"# ===== CELL 17 : Test MANUEL photo par photo (upload une image a la fois) =====\n# Utilise le modele deja en memoire (WIN['model'] + baseline). Aucun fichier externe requis.\n# Deux usages :\n#   (A) bouton d'upload interactif  -> uploade une photo, la prediction s'affiche.\n#   (B) test_photo('/chemin/photo.jpg')  -> si l'upload ne marche pas, donne un chemin.\nimport io\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image as PILImage\n\nTHR = WIN['thr_opt']    # seuil de decision (Youden) du winner\n\ndef _preprocess_rgb(raw_rgb):\n    # meme preprocessing que l'entrainement : crop des bords -> resize (+ ben graham si actif)\n    proc = crop_retina(raw_rgb)\n    proc = cv2.resize(proc, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_AREA)\n    if USE_BEN_GRAHAM:\n        proc = ben_graham(proc, sigma=IMG_SIZE / 30.0)\n        proc = circular_mask(proc, 0.9)\n    return proc.astype(np.uint8)\n\ndef _predict_and_show(raw_rgb, title=''):\n    proc = _preprocess_rgb(raw_rgb)\n    x = proc[None, ...]\n    p_win  = float(WIN['model'].predict(x, verbose=0).ravel()[0])\n    p_base = float(baseline.predict(x,    verbose=0).ravel()[0])\n    label  = 'DR' if p_win >= THR else 'No-DR'\n    color  = '#E74C3C' if p_win >= THR else '#27AE60'\n    fig, ax = plt.subplots(1, 2, figsize=(7.4, 3.7))\n    ax[0].imshow(raw_rgb); ax[0].set_title('uploaded'); ax[0].axis('off')\n    ax[1].imshow(proc);    ax[1].set_title('model input (preprocessed)'); ax[1].axis('off')\n    plt.suptitle(f'{title}\\nWINNER : p(DR)={p_win:.3f}  ->  {label}   (thr={THR:.2f})'\n                 f'      |   baseline : p(DR)={p_base:.3f}',\n                 color=color, fontweight='bold', fontsize=11)\n    plt.tight_layout(); plt.show()\n    return dict(p_winner=p_win, p_baseline=p_base, decision=label, threshold=THR)\n\n# ---- (B) test par chemin : appelle test_photo('/kaggle/input/.../img.jpg') ----\ndef test_photo(path):\n    raw = np.array(PILImage.open(path).convert('RGB'))\n    return _predict_and_show(raw, title=path.split('/')[-1])\n\n# ---- (A) bouton d'upload interactif ----\ntry:\n    import ipywidgets as widgets\n    from IPython.display import display\n\n    def _bytes_from_uploader(up):\n        val = up.value\n        out = []\n        if isinstance(val, dict):                       # ipywidgets v7\n            for name, info in val.items():\n                out.append((name, info['content']))\n        else:                                           # ipywidgets v8 (tuple de dicts)\n            for info in val:\n                out.append((info['name'], info['content']))\n        return out\n\n    uploader = widgets.FileUpload(accept='image/*', multiple=False,\n                                  description='Upload photo')\n    out_area = widgets.Output()\n\n    def _on_change(change):\n        files = _bytes_from_uploader(uploader)\n        if not files:\n            return\n        name, content = files[-1]\n        with out_area:\n            out_area.clear_output(wait=True)\n            raw = np.array(PILImage.open(io.BytesIO(content)).convert('RGB'))\n            _predict_and_show(raw, title=name)\n\n    uploader.observe(_on_change, names='value')\n    print('Uploade une photo (fond d\\'oeil) ci-dessous - la prediction du winner s\\'affiche.')\n    print('Si le bouton ne marche pas, utilise :  test_photo(\"/chemin/vers/photo.jpg\")')\n    display(uploader, out_area)\nexcept Exception as _e:\n    print('Widget d\\'upload indisponible (', _e, ')')\n    print('Utilise plutot :  test_photo(\"/chemin/vers/photo.jpg\")')\n","metadata":{},"outputs":[],"execution_count":null}]}