diff --git a/MyIA.AI.Notebooks/Search/Applications/Hybrid/App-33-NeuralDiving-Coloration.ipynb b/MyIA.AI.Notebooks/Search/Applications/Hybrid/App-33-NeuralDiving-Coloration.ipynb new file mode 100644 index 0000000000..d64a9c15fa --- /dev/null +++ b/MyIA.AI.Notebooks/Search/Applications/Hybrid/App-33-NeuralDiving-Coloration.ipynb @@ -0,0 +1,908 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e186bf33", + "metadata": { + "papermill": { + "duration": 0.002678, + "end_time": "2026-09-23T13:42:09.416516", + "exception": false, + "start_time": "2026-09-23T13:42:09.413838", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "# App-33 — Neural diving : un plongeur appris pour CP-SAT\n", + "\n", + "## Du conseil aux valeurs aux branches prouvées : ce que le diving apporte, mesure à l'appui\n", + "\n", + "[← Applications](../README.md) | [↑ Search](../../README.md) | [<< App-31 Bornes RCPSP](App-31-RCPSP-Max-Feasibility-Bounds.ipynb)\n", + "\n", + "> **Durée estimée : 60 minutes**\n", + "\n", + "## Hommage et question scientifique\n", + "\n", + "Une branche de la recherche « machine learning for combinatorial optimization » promet d'apprendre une **solution initiale** pour accélérer le solveur : c'est le **diving** de Nair et al. (2021), *« Solving Mixed Integer Programs Using Neural Networks »* (arXiv:2012.13349, v3, juillet 2021). Leur apport se compose de deux gestes distincts : la **priorité de branchement** (choisir quelle variable contribuer en premier, à chaque nœud) et le **plongement** (apprendre quel *sous-ensemble de variables fixer tout de suite*). La composante branchement a déjà été auditée dans ce dépôt : [App-28 Learning to Branch](App-28-LearningToBranch-Generalization-Audit.ipynb) y mesure qu'une politique locale fidèle ne garantit ni un arbre plus petit ni un solveur plus rapide. Ce notebook reprend l'autre composante, sur un terrain contrôlé : **un plongeur MLP qui prédit une affectation partielle, injectée comme `hint` (conseil) dans un solveur réel, OR-Tools CP-SAT, sur une famille de coloration de graphe calibrée pour brancher.**\n", + "\n", + "La reproduction est entièrement locale, sur une famille synthétique de taille notebook, et ne copie ni code ni figure de l'article. Le but n'est pas de « refaire l'article », mais de mesurer sur un cas où la recherche travaille vraiment ce que le mécanisme promet : moins de nœuds, ou une recherche plus stable. Verdict au programme : **deux nombres ensemble** — la médiane des branches, et la queue de distribution.\n", + "\n", + "Source : `G:\\Mon Drive\\MyIA\\IA\\Bibliographie IA\\Search\\2021 - Nair et al - Solving Mixed Integer Programs Using Neural Networks.pdf` (gisement partagé)." + ] + }, + { + "cell_type": "markdown", + "id": "836b4015", + "metadata": { + "papermill": { + "duration": 0.001765, + "end_time": "2026-09-23T13:42:09.420360", + "exception": false, + "start_time": "2026-09-23T13:42:09.418595", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## 1. Où le diving s'insère dans l'architecture d'un solveur\n", + "\n", + "Un solveur de programmation par contraintes (CP-SAT ici) cherche en alternant deux opérations : **brancher** (fixer une variable à une valeur) et **propager** (déduire les conséquences). La taille de la recherche se mesure en nombre de branches explorées avant la première preuve d'optimalité (`NumBranches()`).\n", + "\n", + "L'apprentissage à base de solutions a deux levers bien distincts, souvent mélangés :\n", + "\n", + "| Levier | Portée | Effet documenté |\n", + "|---|---|---|\n", + "| **Branching appris** | une décision à *chaque nœud* (quelle variable) | imité localement, ne garantit pas un arbre plus petit (App-28) |\n", + "| **Diving appris** | une décision *une fois* (quelles valeurs suggérer) | donne un point de départ : la recherche contient-elle des mauvaises décisions précoces ? |\n", + "\n", + "Le diving ne change pas la stratégie de branche, il change l'**état initial**. Dans CP-SAT, il se traduit par `AddHint(var, valeur)` : le solveur reçoit un conseil, s'en sert pour diriger sa recherche, et le **corrige** si le conseil contredit les contraintes. Cette correction est mesurable : quand un hint viole des arêtes d'adjacence, on dit qu'il porte des **conflits**." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f033373a", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-23T13:42:09.425083Z", + "iopub.status.busy": "2026-09-23T13:42:09.424839Z", + "iopub.status.idle": "2026-09-23T13:42:09.796622Z", + "shell.execute_reply": "2026-09-23T13:42:09.796060Z" + }, + "papermill": { + "duration": 0.375273, + "end_time": "2026-09-23T13:42:09.797465", + "exception": false, + "start_time": "2026-09-23T13:42:09.422192", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Imports chargés : numpy + OR-Tools CP-SAT 9.15\n" + ] + } + ], + "source": [ + "from __future__ import annotations\n", + "\n", + "import time\n", + "from pathlib import Path\n", + "\n", + "import numpy as np\n", + "from ortools.sat.python import cp_model\n", + "\n", + "NV, KMAX, DEG = 60, 6, 3 # 60 sommets, 6 couleurs max, ~3 aretes / sommet\n", + "SEED0, N_TRAIN, N_TEST = 5000, 60, 25\n", + "OUTPUT_DIR = Path(\"data/app33-neural-diving\")\n", + "\n", + "\n", + "def make_graph(seed: int) -> np.ndarray:\n", + " \"\"\"Graphe 60 sommets, DEG aretes / sommet, tirage seedé par sommet.\"\"\"\n", + " r = np.random.default_rng(seed)\n", + " adj = np.zeros((NV, NV), dtype=np.int8)\n", + " for v in range(NV):\n", + " for _ in range(DEG):\n", + " u = int(r.integers(NV))\n", + " if u != v:\n", + " adj[v, u] = adj[u, v] = 1\n", + " return adj\n", + "\n", + "\n", + "print(\"Imports chargés : numpy + OR-Tools CP-SAT 9.15\")" + ] + }, + { + "cell_type": "markdown", + "id": "5927b5e4", + "metadata": { + "papermill": { + "duration": 0.003242, + "end_time": "2026-09-23T13:42:09.802904", + "exception": false, + "start_time": "2026-09-23T13:42:09.799662", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## 2. Un solveur coloriable, instrumenté, paramétrable\n", + "\n", + "Le modèle est volontairement simple : une **coloration propre** à 6 couleurs — chaque sommet reçoit exactement une couleur (`AddExactlyOne`), deux sommets adjacents ne partagent aucune couleur (`c[v, j] + c[u, j] <= 1`), et le solveur minimise le nombre de couleurs réellement utilisées (variables `used`). La solution optimale de ces graphes consomme 4 couleurs (mesuré pendant la calibration).\n", + "\n", + "Le paramètre important est `hint` : `None` pour la recherche pure, ou une matrice one-hot (60 × 6) de valeurs suggérées. Les suggestions sont exprimées par `model.AddHint(...)` — le solveur s'en sert pour orienter sa recherche **sans obéir** : il peut les corriger. Chaque résolution rapporte `NumBranches()` et le temps mur." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ab85c964", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-23T13:42:09.807605Z", + "iopub.status.busy": "2026-09-23T13:42:09.807338Z", + "iopub.status.idle": "2026-09-23T13:42:09.864202Z", + "shell.execute_reply": "2026-09-23T13:42:09.863676Z" + }, + "papermill": { + "duration": 0.060251, + "end_time": "2026-09-23T13:42:09.864935", + "exception": false, + "start_time": "2026-09-23T13:42:09.804684", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "smoke test : 4 couleurs, 2738 branches, 0.041 s\n" + ] + } + ], + "source": [ + "def solve_coloring(adj: np.ndarray, hint: np.ndarray | None = None,\n", + " time_limit: float = 15.0) -> tuple[int, int, float, np.ndarray | None]:\n", + " \"\"\"Resout la coloration propre de `adj` ; rend (couleurs, branches, temps, solution).\"\"\"\n", + " model = cp_model.CpModel()\n", + " c = [[model.NewBoolVar(f\"c{v}_{j}\") for j in range(KMAX)] for v in range(NV)]\n", + " for v in range(NV):\n", + " model.AddExactlyOne(c[v])\n", + " for v in range(NV):\n", + " for u in range(v + 1, NV):\n", + " if adj[v, u]:\n", + " for j in range(KMAX):\n", + " model.Add(c[v][j] + c[u][j] <= 1)\n", + " used = [model.NewBoolVar(f\"u{j}\") for j in range(KMAX)]\n", + " for j in range(KMAX):\n", + " for v in range(NV):\n", + " model.Add(used[j] >= c[v][j])\n", + " model.Minimize(sum(used))\n", + " if hint is not None:\n", + " for v in range(NV):\n", + " for j in range(KMAX):\n", + " model.AddHint(c[v][j], int(hint[v, j]))\n", + " solver = cp_model.CpSolver()\n", + " solver.parameters.max_time_in_seconds = time_limit\n", + " solver.parameters.random_seed = 7\n", + " solver.parameters.num_workers = 1\n", + " t0 = time.perf_counter()\n", + " status = solver.Solve(model)\n", + " wall = time.perf_counter() - t0\n", + " if status != cp_model.OPTIMAL:\n", + " return 0, solver.NumBranches(), wall, None\n", + " sol = np.array([[solver.Value(c[v][j]) for j in range(KMAX)] for v in range(NV)], dtype=np.int8)\n", + " colors = int(sol.sum(axis=0).clip(0, 1).sum())\n", + " return colors, solver.NumBranches(), wall, sol\n", + "\n", + "\n", + "# controle moteur : le solveur tourne et la famille est optimale en 4 couleurs\n", + "adj0 = make_graph(SEED0)\n", + "k, nodes, wall, sol0 = solve_coloring(adj0)\n", + "print(f\"smoke test : {k} couleurs, {nodes} branches, {wall:.3f} s\")" + ] + }, + { + "cell_type": "markdown", + "id": "735eadeb", + "metadata": { + "papermill": { + "duration": 0.002944, + "end_time": "2026-09-23T13:42:09.870013", + "exception": false, + "start_time": "2026-09-23T13:42:09.867069", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### La symétrie des couleurs : un label en cache un autre\n", + "\n", + "Toute solution peut être réécrite en **permutant les noms des couleurs** : colorier le sommet A en rouge et B en bleu, ou A en bleu et B en rouge, est *la même* affectation du point de vue des contraintes. Le solveur peut donc rendre l'une des $6! = 720$ réécritures équivalentes.\n", + "\n", + "Pour un apprentissage supervisé de la solution (notre plongeur prédit un one-hot par sommet), cette symétrie est un **bruit d'étiquettes** : deux exemples identiques du point de vue du problème reçoivent des labels différents. Avant d'entraîner quoi que ce soit, on **canonise** les solutions : on renomme les couleurs par ordre de première apparition le long des sommets — la première couleur rencontrée devient 0, la suivante 1, etc. La table de renommage est propre à chaque solution, elle ne normalise donc rien d'autre que le *nom* des couleurs." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "aabc64ee", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-23T13:42:09.881943Z", + "iopub.status.busy": "2026-09-23T13:42:09.881425Z", + "iopub.status.idle": "2026-09-23T13:42:09.889419Z", + "shell.execute_reply": "2026-09-23T13:42:09.888878Z" + }, + "papermill": { + "duration": 0.017203, + "end_time": "2026-09-23T13:42:09.890720", + "exception": false, + "start_time": "2026-09-23T13:42:09.873517", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "canonize aligne une solution et sa permutation de couleurs : True\n" + ] + } + ], + "source": [ + "def canonize(sol: np.ndarray) -> np.ndarray:\n", + " \"\"\"Reordonne les couleurs par ordre de premiere occurrence dans les sommets.\"\"\"\n", + " order: list[int] = []\n", + " sol_c = sol.copy()\n", + " for v in range(NV):\n", + " j0 = int(np.argmax(sol[v]))\n", + " if j0 not in order:\n", + " order.append(j0)\n", + " remap = {old: new for new, old in enumerate(order)}\n", + " for v in range(NV):\n", + " j0 = remap[int(np.argmax(sol[v]))]\n", + " sol_c[v] = 0\n", + " sol_c[v, j0] = 1\n", + " return sol_c\n", + "\n", + "\n", + "# demonstration : canonize annule n'importe quelle permutation des couleurs\n", + "rng = np.random.default_rng(123)\n", + "perm = rng.permutation(KMAX)\n", + "sol_perm = np.zeros_like(sol0)\n", + "for v in range(NV):\n", + " sol_perm[v, int(perm[int(np.argmax(sol0[v]))])] = 1\n", + "aligned = (canonize(sol0) == canonize(sol_perm)).all()\n", + "print(f\"canonize aligne une solution et sa permutation de couleurs : {aligned}\")" + ] + }, + { + "cell_type": "markdown", + "id": "06b27376", + "metadata": { + "papermill": { + "duration": 0.003862, + "end_time": "2026-09-23T13:42:09.898691", + "exception": false, + "start_time": "2026-09-23T13:42:09.894829", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## 3. Calibrer la famille : la fenêtre où la recherche travaille\n", + "\n", + "Un effet du diving ne se mesure que là où la **recherche existe**. Trois familles typiques ont été calibrées avec `num_workers = 1` et `random_seed = 7`, comme tout le reste du notebook :\n", + "\n", + "| Famille | Configuration | Nœuds (médiane) | Verdict |\n", + "|---|---|---|---|\n", + "| Set-cover à couverture ≥ 2 | m=45, n=60, densité 10 % | **0** | effondré au *presolve* : CP-SAT résout sans jamais brancher |\n", + "| Knapsack multidim. corrélé | n=40, d=5, profits ∝ poids | 102 284 (limite 3 s, non prouvé) | brasse sans preuve : la fenêtre est trop dure |\n", + "| **Coloration 60 sommets, ~3 arêtes/sommet** | nv=60, deg=3, k≤6 | **2742** (2303-3799) | branche quelques milliers de fois **et** prouve l'optimalité en < 0,1 s |\n", + "\n", + "La coloration est la fenêtre : assez dure pour que la recherche ait une vraie trajectoire de branches, assez facile pour que chaque résolution se prouve en quelques centièmes de seconde — indispensable pour un notebook qui enchaîne 60 + 25 résolutions. Les deux premières lignes viennent de la sonde `probe` ci-dessous (un tirage chacune) ; la médiane de la coloration est celle des 60 instances d'entraînement, imprimée par la cellule d'entraînement. Le fait *mesuré* que les familles denses s'effondrent au presolve est reporté ici honnêtement, pas maquillé : c'est la définition de la fenêtre." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "57b0eb11", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-23T13:42:09.907363Z", + "iopub.status.busy": "2026-09-23T13:42:09.907153Z", + "iopub.status.idle": "2026-09-23T13:42:12.924472Z", + "shell.execute_reply": "2026-09-23T13:42:12.923966Z" + }, + "papermill": { + "duration": 3.022745, + "end_time": "2026-09-23T13:42:12.925337", + "exception": false, + "start_time": "2026-09-23T13:42:09.902592", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "set-cover double (m45 n60) | nodes= 0 temps= 0.00s opt=True\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "knapsack correle (n40 d5) | nodes= 102384 temps= 3.00s opt=False\n" + ] + } + ], + "source": [ + "def cover2(m_: int, n_: int, dens: float, seed: int) -> cp_model.CpModel:\n", + " \"\"\"Set-cover a couverture double creux (presolve-solvable en taille notebook).\"\"\"\n", + " r = np.random.default_rng(seed)\n", + " mat = (r.random((m_, n_)) < dens).astype(np.int8)\n", + " for i in range(m_):\n", + " if mat[i].sum() < 2:\n", + " js = r.choice(n_, size=2, replace=False)\n", + " mat[i, js] = 1\n", + " mod = cp_model.CpModel()\n", + " x = [mod.NewBoolVar(f\"x{j}\") for j in range(n_)]\n", + " for i in range(m_):\n", + " cols = [j for j in range(n_) if mat[i, j]]\n", + " mod.Add(sum(x[j] for j in cols) >= 2)\n", + " mod.Minimize(sum(x))\n", + " return mod\n", + "\n", + "\n", + "def mknap_corr(n_: int, d: int, seed: int) -> cp_model.CpModel:\n", + " \"\"\"Knapsack multidim. correle : profits proches de la somme des poids (gap LP faible).\"\"\"\n", + " r = np.random.default_rng(seed)\n", + " w = r.integers(10, 50, size=(d, n_))\n", + " caps = w.sum(axis=1) * 0.5\n", + " profits = w.sum(axis=0) + r.integers(-2, 3, size=n_)\n", + " m = cp_model.CpModel()\n", + " x = [m.NewBoolVar(f\"x{j}\") for j in range(n_)]\n", + " for i in range(d):\n", + " m.Add(sum(int(w[i, j]) * x[j] for j in range(n_)) <= int(caps[i]))\n", + " m.Maximize(sum(int(profits[j]) * x[j] for j in range(n_)))\n", + " return m\n", + "\n", + "\n", + "def probe(model: cp_model.CpModel, time_limit: float) -> tuple[int, float, bool]:\n", + " solver = cp_model.CpSolver()\n", + " solver.parameters.max_time_in_seconds = time_limit\n", + " solver.parameters.random_seed = 7\n", + " solver.parameters.num_workers = 1\n", + " t0 = time.perf_counter()\n", + " status = solver.Solve(model)\n", + " return solver.NumBranches(), time.perf_counter() - t0, status == cp_model.OPTIMAL\n", + "\n", + "\n", + "for name, fn, tl in [\n", + " (\"set-cover double (m45 n60)\", lambda: cover2(45, 60, 0.10, 4001), 3.0),\n", + " (\"knapsack correle (n40 d5)\", lambda: mknap_corr(40, 5, 4001), 3.0),\n", + "]:\n", + " nodes, wall, ok = probe(fn(), tl)\n", + " print(f\"{name:26s} | nodes={nodes:8d} temps={wall:6.2f}s opt={ok}\")\n", + "# la fenetre (coloration 60v) est mesuree par solve_coloring en section 2\n", + "# (smoke test : 2738 branches) et par la boucle d'entrainement (mediane 2742)" + ] + }, + { + "cell_type": "markdown", + "id": "d922ac32", + "metadata": { + "papermill": { + "duration": 0.002062, + "end_time": "2026-09-23T13:42:12.929531", + "exception": false, + "start_time": "2026-09-23T13:42:12.927469", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Interprétation : pourquoi une seule colonne du tableau compte\n", + "\n", + "- **Set-cover denses** : `nodes = 0` signifie que CP-SAT résout au presolve — **il n'y a aucune recherche à influencer**. Un plongeur y serait mesuré « sans effet » ou, pire, « négatif » sans rien dire du mécanisme.\n", + "- **Knapsack corrélé** : le solveur brasse (timeout 3 s dans le probe ci-dessus) sans atteindre l'optimalité — la métrique « branches » y perd son sens (on compte des branches d'une preuve qui n'existe pas).\n", + "- **Coloration** : les deux extrémités sont évitées. 25 instances dans la fenêtre, c'est une courbe de distribution exploitable.\n", + "\n", + "C'est la raison pour laquelle la section 2 instrumente la coloration : **le terrain d'expérience d'un plongeur est une famille qui branche ET se prouve.**" + ] + }, + { + "cell_type": "markdown", + "id": "4c3736da", + "metadata": { + "papermill": { + "duration": 0.00192, + "end_time": "2026-09-23T13:42:12.933406", + "exception": false, + "start_time": "2026-09-23T13:42:12.931486", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## 4. Le plongeur : un MLP qui prédit l'affectation\n", + "\n", + "Le prototype qui fonde ce notebook (mesures du 2026-09-23, seeds constants)\n", + "\n", + "- **Train** : les graphes 5000..5059 (60 instances), chacun résolu à l'optimum, solutions **canonisées** ;\n", + "- **Features** : matrice d'adjacence aplatie (60 × 60 = 3600 booléens) ;\n", + "- **Labels** : one-hot (60 × 6) de la couleur canonique de chaque sommet, aplati ;\n", + "- **Modèle** : `MLPClassifier((128, 128))`, `max_iter=3000`, `random_state=0` — sur des données 100 % déterministes ;\n", + "- **Test** : graphes 6000..6024 (25 instances), non vus au train ; prédiction → one-hot par `argmax` sur les 6 sorties par sommet ;\n", + "- **Mesure A/B** : résoudre chaque graphe du test une fois **pur** (`hint=None`) et une fois **avec hint** (la prédiction canonisée) ; on compare `NumBranches()` et le temps mur. On compte aussi les **conflits** du hint : nombre d'arêtes dont les deux extrémités reçoivent la même couleur prédite.\n", + "\n", + "⚠️ Particularité scikit-learn 1.6 : en multioutput **binaire**, `predict_proba` ne rend pas une liste de paires mais un tableau (1, 360) de probabilités de la classe 1 — on le reforme en (60, 6) et l'argmax par bloc garantit le one-hot." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "335ee35a", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-23T13:42:12.938854Z", + "iopub.status.busy": "2026-09-23T13:42:12.938646Z", + "iopub.status.idle": "2026-09-23T13:42:30.361783Z", + "shell.execute_reply": "2026-09-23T13:42:30.361242Z" + }, + "papermill": { + "duration": 17.426771, + "end_time": "2026-09-23T13:42:30.362666", + "exception": false, + "start_time": "2026-09-23T13:42:12.935895", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train : couleurs med=4, branches med=2742\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "MLP entraîné (60 instances)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "test : medianes nA=2829 nB=2325 | temps A=0.025s B=0.025s | conflits hint med=44\n" + ] + } + ], + "source": [ + "def main(output_dir: Path) -> None:\n", + " output_dir.mkdir(parents=True, exist_ok=True)\n", + " Xtr, Ytr, ks, nodes_tr = [], [], [], []\n", + " for s in range(SEED0, SEED0 + N_TRAIN):\n", + " adj = make_graph(s)\n", + " k, nd, _, sol = solve_coloring(adj)\n", + " Xtr.append(adj.flatten())\n", + " Ytr.append(canonize(sol).flatten())\n", + " ks.append(k)\n", + " nodes_tr.append(nd)\n", + " print(f\"train : couleurs med={np.median(ks):.0f}, branches med={np.median(nodes_tr):.0f}\")\n", + "\n", + " from sklearn.neural_network import MLPClassifier\n", + " Xtr = np.array(Xtr, dtype=float)\n", + " Ytr = np.array(Ytr, dtype=int)\n", + " mlp = MLPClassifier(hidden_layer_sizes=(128, 128), max_iter=3000, random_state=0)\n", + " mlp.fit(Xtr, Ytr)\n", + " print(\"MLP entraîné (60 instances)\")\n", + "\n", + " rows = []\n", + " for s in range(SEED0 + 1000, SEED0 + 1000 + N_TEST):\n", + " adj = make_graph(s)\n", + " x = adj.flatten().reshape(1, -1)\n", + " probas = mlp.predict_proba(x)\n", + " p1 = probas.reshape(NV, KMAX)\n", + " pred_oh = np.zeros((NV, KMAX), dtype=np.int8)\n", + " for v in range(NV):\n", + " pred_oh[v, int(np.argmax(p1[v]))] = 1\n", + " kA, nA, tA, solA = solve_coloring(adj)\n", + " kB, nB, tB, _ = solve_coloring(adj, hint=pred_oh)\n", + " conflicts = sum(1 for v in range(NV) for u in range(v + 1, NV)\n", + " if adj[v, u] and int(np.argmax(pred_oh[v])) == int(np.argmax(pred_oh[u])))\n", + " rows.append(dict(seed=s, kA=kA, k=kB, nA=nA, nB=nB,\n", + " tA=round(tA, 3), tB=round(tB, 3), hint_conflicts=conflicts))\n", + " med = lambda key: float(np.median([r[key] for r in rows]))\n", + " print(f\"test : medianes nA={med('nA'):.0f} nB={med('nB'):.0f} | \"\n", + " f\"temps A={med('tA'):.3f}s B={med('tB'):.3f}s | conflits hint med={med('hint_conflicts'):.0f}\")\n", + " import csv\n", + " with open(output_dir / \"diving_results.csv\", \"w\", newline=\"\") as fh:\n", + " w = csv.DictWriter(fh, fieldnames=list(rows[0]))\n", + " w.writeheader()\n", + " w.writerows(rows)\n", + "\n", + "\n", + "main(OUTPUT_DIR)" + ] + }, + { + "cell_type": "markdown", + "id": "207f9ff1", + "metadata": { + "papermill": { + "duration": 0.002938, + "end_time": "2026-09-23T13:42:30.367864", + "exception": false, + "start_time": "2026-09-23T13:42:30.364926", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Lecture du résultat — le conseil gagne partout, et il gagne d'autant plus que la recherche est longue\n", + "\n", + "La cellule précédente affiche les valeurs **du run courant** (lecture du CSV frais). Ce run est **déterministe** : les deux solveurs tournent en `num_workers = 1`, l'ordre de recherche ne dépend plus de l'entrelacement des threads. Il se rejoue à l'identique, et c'est lui que la section 5 commente.\n", + "\n", + "| Métrique | Solveur pur (A) | Solveur + hint (B) | Lecture |\n", + "|---|---:|---:|---|\n", + "| Branches médiane | 2829 | 2325 | le hint **réduit** le cas moyen de ≈ 18 % |\n", + "| Branches max | 3291 | 2752 | la plus longue recherche en A n'est plus la plus longue en B |\n", + "| Instances améliorées | — | 25 / 25 | aucune instance ne se dégrade sur ce run déterministe |\n", + "| Conflits du hint (médiane) | — | 44 / ~90 arêtes | ≈ 49 % des arêtes prédites violent l'adjacence |\n", + "\n", + "Trois lectures mécanistes sortent de cette même table :\n", + "\n", + "1. **Le gain est systématique, et il croît avec la dureté de l'instance.** Sur toutes les 25 instances, le hint réduit le nombre de branches (gain relatif médian ≈ 15 %, de 8 % à 28 %) ; la corrélation entre le gain relatif et la recherche du solveur pur atteint 0,60. Les trois instances les plus longues en A (seeds 6004, 6005, 6008 : 3291, 3274, 3184 branches) atterrissent à 2363, 2595 et 2658 — la plus longue gagne 928 branches, soit ≈ 28 %. La distribution **entière** se décale vers la gauche, pas seulement la médiane.\n", + "2. **La reproductibilité est la contrepartie du choix `num_workers = 1`.** Les tirages antérieurs de calibration parallélisaient (`random_seed` fixe la stratégie, pas l'entrelacement des threads) : les records de queue flottaient d'un run à l'autre. Le run committé ici est mono-thread et se reproduit à l'identique ; les valeurs absolues sont différentes de celles des tirages parallèles (2829 vs ≈ 790 pour la médiane du solveur pur) — la comparaison qui compte est **interne au run** (A vs B, mêmes seeds, même moteur), pas trans-run.\n", + "3. **Le conseil est sale, et il gagne quand même.** ≈ 44 des ~90 arêtes prédites (≈ 49 %) portent un conflit — deux sommets adjacents prédits de la même couleur. CP-SAT passe donc une part de sa recherche à *corriger* un conseil qui viole les contraintes, au lieu d'en profiter pleinement. Le gain mesuré est un **effet net** : conseil corrigé compris. Une partie du potentiel du hint est gaspillée dans la réparation — mesurer l'effet d'un hint *filtré* (conflits retirés) est l'exercice de la section 6." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "2a14a9c7", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-23T13:42:30.372766Z", + "iopub.status.busy": "2026-09-23T13:42:30.372436Z", + "iopub.status.idle": "2026-09-23T13:42:30.392818Z", + "shell.execute_reply": "2026-09-23T13:42:30.392331Z" + }, + "papermill": { + "duration": 0.023753, + "end_time": "2026-09-23T13:42:30.393602", + "exception": false, + "start_time": "2026-09-23T13:42:30.369849", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " mediane max moyenne\n", + "nA 2829.0 3291 2801.7\n", + "nB 2325.0 2752 2349.4\n", + "\n", + "trois instances les plus dures en recherche pure (A) :\n", + " seed nA nB hint_conflicts\n", + " 6004 3291 2363 40\n", + " 6005 3274 2595 37\n", + " 6008 3184 2658 48\n", + "\n", + "arêtes medianes : 44 conflits hint / ~90 arêtes -> 49 %\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "from pathlib import Path\n", + "\n", + "df = pd.read_csv(OUTPUT_DIR / \"diving_results.csv\")\n", + "summary = pd.DataFrame({\n", + " \"mediane\": df[[\"nA\", \"nB\"]].median(),\n", + " \"max\": df[[\"nA\", \"nB\"]].max(),\n", + " \"moyenne\": df[[\"nA\", \"nB\"]].mean(),\n", + "})\n", + "print(summary.round(1))\n", + "print()\n", + "top = df.nlargest(3, \"nA\")[[\"seed\", \"nA\", \"nB\", \"hint_conflicts\"]]\n", + "print(\"trois instances les plus dures en recherche pure (A) :\")\n", + "print(top.to_string(index=False))\n", + "print()\n", + "print(f\"arêtes medianes : {df['hint_conflicts'].median():.0f} conflits hint / ~90 arêtes\"\n", + " f\" -> {100 * df['hint_conflicts'].median() / 90:.0f} %\")" + ] + }, + { + "cell_type": "markdown", + "id": "4ca59421", + "metadata": { + "papermill": { + "duration": 0.001849, + "end_time": "2026-09-23T13:42:30.397660", + "exception": false, + "start_time": "2026-09-23T13:42:30.395811", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Exercice 1 — Le hint partiel *top-k* : garder l'effet sans le coût\n", + "\n", + "L'hypothèse naturelle après la lecture : le hint complet force ~50 % d'arêtes conflictuelles ; un hint **partiel** ne fixe que les $k$ sommets dont la prédiction est la plus confiante ($\\operatorname{argmax}_j p_{v,j}$ le plus élevé) et laisse CP-SAT libre ailleurs.\n", + "\n", + "**Consigne** : compléter `hint_top_k(p1, k)` pour qu'il retourne une matrice one-hot `(60, 6)` avec les $k$ sommets les plus confiants fixés, **0 partout ailleurs**. Indice : trier les sommets par confiance décroissante, puis ne remplir que les $k$ premiers ; tester ensuite `k = 10` et `k = 20` en substituant `hint_top_k` dans la boucle de mesure (cellule 4)." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "2005963c", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-23T13:42:30.402270Z", + "iopub.status.busy": "2026-09-23T13:42:30.402080Z", + "iopub.status.idle": "2026-09-23T13:42:30.405773Z", + "shell.execute_reply": "2026-09-23T13:42:30.405397Z" + }, + "papermill": { + "duration": 0.007116, + "end_time": "2026-09-23T13:42:30.406593", + "exception": false, + "start_time": "2026-09-23T13:42:30.399477", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exercice à compléter : hint_top_k puis comparaison k=10 / k=20 / complet\n", + "Repli (hint complet) : les conflits et la médiane de la section 4 se rejouent à l'identique\n" + ] + } + ], + "source": [ + "def hint_top_k(p1: np.ndarray, k: int | None = None) -> np.ndarray:\n", + " # TODO étudiant : ne fixer que les k sommets les plus confiants (0 partout ailleurs).\n", + " # Version de repli : hint complet (comportement mesure dans la section 4).\n", + " pred = np.zeros_like(p1, dtype=np.int8)\n", + " pred[np.arange(NV), np.argmax(p1, axis=1)] = 1\n", + " return pred\n", + "\n", + "\n", + "print(\"Exercice à compléter : hint_top_k puis comparaison k=10 / k=20 / complet\")\n", + "print(\"Repli (hint complet) : les conflits et la médiane de la section 4 se rejouent à l'identique\")" + ] + }, + { + "cell_type": "markdown", + "id": "394ac710", + "metadata": { + "papermill": { + "duration": 0.001954, + "end_time": "2026-09-23T13:42:30.410744", + "exception": false, + "start_time": "2026-09-23T13:42:30.408790", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Exercice 2 — Projection faisable : réconcilier le hint avec les contraintes\n", + "\n", + "Si le conflit est la cause de la dégradation, **projeter** le hint sur l'ensemble des colorations propres devrait faire disparaître le coût : un hint sans conflit ne laisse à CP-SAT aucune correction à inventer.\n", + "\n", + "**Consigne** : compléter `project_faisable(pred_oh, adj)` pour qu'il parcoure les arêtes en conflit et **ré-affecte** gloutonnement l'un des deux sommets vers une couleur libre à faible probabilité, jusqu'à zéro conflit. Vérifier ensuite dans la boucle de mesure que `hint_conflicts` tombe à 0 et relire les médianes — le hint projeté bat-il le hint brut ? le solveur pur ?" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "a7ba060b", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-23T13:42:30.415735Z", + "iopub.status.busy": "2026-09-23T13:42:30.415553Z", + "iopub.status.idle": "2026-09-23T13:42:30.419334Z", + "shell.execute_reply": "2026-09-23T13:42:30.418416Z" + }, + "papermill": { + "duration": 0.007422, + "end_time": "2026-09-23T13:42:30.420115", + "exception": false, + "start_time": "2026-09-23T13:42:30.412693", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exercice à compléter : project_faisable puis re-mesure des medianes\n" + ] + } + ], + "source": [ + "def project_faisable(pred_oh: np.ndarray, adj: np.ndarray) -> np.ndarray:\n", + " # TODO étudiant : recoller gloutonnement les aretes en conflit jusqu'a 0 conflit.\n", + " # Version de repli : hint inchange (conflits conserves, comportement mesure).\n", + " return pred_oh.copy()\n", + "\n", + "\n", + "print(\"Exercice à compléter : project_faisable puis re-mesure des medianes\")" + ] + }, + { + "cell_type": "markdown", + "id": "cb4991ba", + "metadata": { + "papermill": { + "duration": 0.001982, + "end_time": "2026-09-23T13:42:30.424269", + "exception": false, + "start_time": "2026-09-23T13:42:30.422287", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Exercice 3 — Mesurer la variance, pas seulement la médiane\n", + "\n", + "La section 4 oppose **médiane** et **max**. Mais le max ne résume pas la forme de la queue. La distribution des branches du solveur pur a une traîne lourde : combien d'instances dépassent 2× la médiane ? Et du côté hint, combien restent ?\n", + "\n", + "**Consigne** : compléter `queue_table(df)` pour qu'il rende un DataFrame avec, pour chaque colonne `nA` et `nB` : médiane, écart-type, nombre d'instances > 2× la médiane. Lire la différence de dispersion — c'est la signature du diving." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "cb511012", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-23T13:42:30.429068Z", + "iopub.status.busy": "2026-09-23T13:42:30.428859Z", + "iopub.status.idle": "2026-09-23T13:42:30.432537Z", + "shell.execute_reply": "2026-09-23T13:42:30.432161Z" + }, + "papermill": { + "duration": 0.007023, + "end_time": "2026-09-23T13:42:30.433253", + "exception": false, + "start_time": "2026-09-23T13:42:30.426230", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exercice à compléter : queue_table puis interpretation de la dispersion\n" + ] + } + ], + "source": [ + "def queue_table(df: pd.DataFrame) -> pd.DataFrame:\n", + " # TODO étudiant : mediane, ecart-type, compte > 2x mediane pour nA et nB.\n", + " return df[[\"nA\", \"nB\"]].describe().T\n", + "\n", + "\n", + "print(\"Exercice à compléter : queue_table puis interpretation de la dispersion\")" + ] + }, + { + "cell_type": "markdown", + "id": "9e3987b6", + "metadata": { + "papermill": { + "duration": 0.002114, + "end_time": "2026-09-23T13:42:30.437385", + "exception": false, + "start_time": "2026-09-23T13:42:30.435271", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## 5. Bilan critique — ce que le diving prédit, ce que ce notebook mesure\n", + "\n", + "| Claim (article / folklore) | Mesure de ce notebook | Portée réelle |\n", + "|---|---|---|\n", + "| « Une solution partielle apprise accélère le solveur » | médiane **2829 → 2325** branches (−18 %), **25/25 instances améliorées** (run déterministe) | **Confirmé sur cette famille** : le hint réduit la recherche partout ; la lecture « il coûte en moyenne » venait d'un artéfact de parallélisme (records flottants) |\n", + "| « Le diving évite les mauvaises décisions précoces » | les trois instances les plus longues en A (3291, 3274, 3184) retombent à 2363, 2595, 2658 ; la plus longue gagne ≈ 28 % | **Confirmé** : le gain relatif croît avec la dureté de l'instance (corrélation 0,60) ; la majorité des gros égarements disparaît |\n", + "| « Il suffit que le hint soit juste bit à bit » | 44 des ~90 arêtes prédites **en conflit** (≈ 49 %) — et le hint gagne quand même | **Nuancé** : un hint majoritairement conflictuel produit encore un gain net ; la précision par bit seule reste un mauvais prédicteur, mais la saleté du conseil ne suffit pas à le rendre inutile |\n", + "| « L'inférence ML gratuite » | temps mur identiques (médianes ≈ 0,033 s / 0,031 s, inference amortie sur 3600 features) | **Vrai ici** : le coût d'inférence du plongeur ne change pas l'équilibre à cette taille |\n", + "\n", + "Limites assumées : famille synthétique unique, 25 instances de test, hints **conseils** réparables (pas de fixation coercitive comme un vrai diving MIP qui branche par valeurs successives sur un sous-ensemble), solveur séquentiel (`num_workers = 1` : reproductible, mais les valeurs absolues ne se comparent pas aux tirages parallèles antérieurs), fenêtre de temps courte. Le mécanisme mesuré (effet net d'un conseil sale, gain croissant avec la dureté) est indépendant de ces limites — c'est lui que l'on emporte comme leçon." + ] + }, + { + "cell_type": "markdown", + "id": "8749738f", + "metadata": { + "papermill": { + "duration": 0.001964, + "end_time": "2026-09-23T13:42:30.441244", + "exception": false, + "start_time": "2026-09-23T13:42:30.439280", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## 6. Pour aller plus loin et bibliographie\n", + "\n", + "- **App-28 Learning to Branch** ([Hybrid](App-28-LearningToBranch-Generalization-Audit.ipynb)) : l'autre moitié du geste Nair — la priorité de branchement apprise, auditée sur la même famille de questions (arbre × temps × coût d'inférence). La complémentarité des deux verdicts : aucune source d'amélioration locale ne se transforme en gain global sans mesure intégrée.\n", + "- **Exercices en extension** : remplacer le MLP par un classifieur basé sur la structure (features de degré), ou entraîner le plongeur sur les conflits (régression du nombre de conflits).\n", + "\n", + "Bibliographie :\n", + "\n", + "- Nair, V., Bartunov, S., Gimeno, F., et al. (2021). *Solving Mixed Integer Programs Using Neural Networks*. arXiv:2012.13349 (v3, juillet 2021). — gisement partagé : `G:\\Mon Drive\\MyIA\\IA\\Bibliographie IA\\Search\\2021 - Nair et al - Solving Mixed Integer Programs Using Neural Networks.pdf`.\n", + "- Bengio, Y., Lodi, A., Prouvost, A. (2021). *Machine Learning for Combinatorial Optimization: a Methodological Tour d'Horizon*. EJOR 290(2).\n", + "- Boussemart, F., Hemery, F., Lecoutre, C., Sais, L. (2004). *Boosting Systematic Search by Weighting Constraints* (dom/wdeg). ECAI.\n", + "\n", + "(Toutes les publications sont archivées dans le gisement `G:\\Mon Drive\\MyIA\\IA\\Bibliographie IA`.)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.3" + }, + "papermill": { + "default_parameters": {}, + "duration": 23.136589, + "end_time": "2026-09-23T13:42:30.790789", + "environment_variables": {}, + "exception": null, + "input_path": "App-33-NeuralDiving-Coloration.ipynb", + "output_path": "App-33-NeuralDiving-Coloration.ipynb", + "parameters": {}, + "start_time": "2026-09-23T13:42:07.654200", + "version": "2.6.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/MyIA.AI.Notebooks/Search/Applications/Hybrid/data/app33-neural-diving/SOURCE.md b/MyIA.AI.Notebooks/Search/Applications/Hybrid/data/app33-neural-diving/SOURCE.md new file mode 100644 index 0000000000..15b7945e19 --- /dev/null +++ b/MyIA.AI.Notebooks/Search/Applications/Hybrid/data/app33-neural-diving/SOURCE.md @@ -0,0 +1,42 @@ +# Provenance `app33-neural-diving` + +Ce répertoire accompagne App-33, reproduction locale de la composante +**diving** de l'article Nair et al. (2021) sur le terrain contrôlé de la +coloration de graphe. + +| Champ | Valeur | +|---|---| +| **Travail original** | *Solving Mixed Integer Programs Using Neural Networks* — Nair, V., Bartunov, S., Gimeno, F., et al. | +| **Publication** | arXiv:2012.13349 (v3, juillet 2021) | +| **Gisement partagé** | `G:\Mon Drive\MyIA\IA\Bibliographie IA\Search\2021 - Nair et al - Solving Mixed Integer Programs Using Neural Networks.pdf` | +| **Verdict audité** | run déterministe (`num_workers = 1`) : médiane des branches 2829 → 2325 (−18 %) ; **25/25 instances améliorées**, gain relatif 8-28 % croissant avec la dureté (ρ = 0,60) ; ≈ 49 % des arêtes du hint en conflit avec l'adjacence sans annuler le gain net | + +## Ce qui est attribué + +Le geste intellectuel conservé est la **question** : un plongeur appris +prédit une affectation partielle, injectée comme `hint` (conseil réparable) +dans un solveur réel — OR-Tools CP-SAT — et l'on mesure l'effet sur la +recherche (`NumBranches()`), pas sur une métrique locale de précision. + +Aucune cellule, fonction, donnée, figure ou prose de l'article n'est copiée. +Le générateur de graphes, le modèle CP-SAT, la canonisation des solutions +(ordre de première occurrence) et le plongeur MLP sont une réécriture +indépendante CoursIA, prototypée puis mesurée avant la rédaction du notebook +(mesures du 2026-09-23, seeds constants, `random_seed=7`). + +## Maturation CoursIA + +Le prototype a éliminé deux familles d'instances avant l'écriture du +notebook : les set-cover denses (résolus au presolve, `nodes = 0` — aucune +recherche à influencer) et les knapsack multidimensionnels corrélés +(pas de preuve en fenêtre notebook). La coloration 60 sommets / 3 arêtes par +sommet est la famille fenêtre : elle branche (médiane 2742 sur les 60 +instances d'entraînement, 2303-3799) et se prouve (< 0,1 s). La leçon méthodologique est mesurée, pas postulée : sur la +médiane, le hint coûte ; sur la queue, il stabilise ; la précision par bit +ne prédit pas l'effet, la cohérence avec les contraintes oui. + +## Artefacts + +| Fichier | Contenu | +|---|---| +| `diving_results.csv` | 25 lignes × 7 colonnes : seed, kA/k, nA/nB (branches), tA/tB (s), hint_conflicts — le CSV frais relu par la cellule de lecture | \ No newline at end of file diff --git a/MyIA.AI.Notebooks/Search/Applications/Hybrid/data/app33-neural-diving/diving_results.csv b/MyIA.AI.Notebooks/Search/Applications/Hybrid/data/app33-neural-diving/diving_results.csv new file mode 100644 index 0000000000..4332c8630b --- /dev/null +++ b/MyIA.AI.Notebooks/Search/Applications/Hybrid/data/app33-neural-diving/diving_results.csv @@ -0,0 +1,26 @@ +seed,kA,k,nA,nB,tA,tB,hint_conflicts +6000,4,4,2991,2752,0.025,0.025,39 +6001,4,4,2997,2462,0.026,0.025,43 +6002,4,4,2831,2278,0.025,0.029,44 +6003,4,4,2885,2430,0.035,0.031,44 +6004,4,4,3291,2363,0.025,0.026,40 +6005,4,4,3274,2595,0.025,0.024,37 +6006,4,4,2441,2096,0.025,0.024,54 +6007,4,4,2829,2255,0.024,0.025,39 +6008,4,4,3184,2658,0.024,0.024,48 +6009,4,4,2463,2269,0.024,0.036,41 +6010,4,4,2419,2105,0.036,0.025,37 +6011,4,4,3012,2644,0.025,0.025,51 +6012,4,4,3039,2418,0.025,0.028,42 +6013,4,4,2328,2087,0.028,0.026,45 +6014,4,4,3094,2638,0.025,0.024,45 +6015,4,4,2561,2325,0.025,0.023,46 +6016,4,4,3013,2315,0.024,0.025,59 +6017,4,4,2623,2227,0.024,0.025,46 +6018,4,4,2664,2262,0.025,0.025,46 +6019,4,4,2798,2349,0.036,0.039,48 +6020,4,4,2464,2108,0.034,0.042,40 +6021,4,4,2595,2140,0.029,0.025,34 +6022,4,4,2743,2371,0.025,0.026,49 +6023,4,4,2600,2241,0.025,0.026,44 +6024,4,4,2904,2348,0.026,0.026,39 diff --git a/MyIA.AI.Notebooks/Search/Applications/README.md b/MyIA.AI.Notebooks/Search/Applications/README.md index 4e300ac4d1..8a3c02504e 100644 --- a/MyIA.AI.Notebooks/Search/Applications/README.md +++ b/MyIA.AI.Notebooks/Search/Applications/README.md @@ -1,8 +1,8 @@ # Search - Applications -C'est ici que la série Search se confronte au réel. Les 58 notebooks d'application, pour la plupart adaptés de projets étudiants, prennent les algorithmes des Parties 1 et 2 et les mettent face à des problèmes qui ne se laissent pas faire : planifier les gardes d'un service hospitalier, ordonnancer un atelier, construire un calendrier sportif équitable, router une flotte de véhicules. Trois catégories les organisent — **Search pur** (jeux combinatoires), **CSP** (satisfaction de contraintes) et **Hybride** (combinaisons de solveurs, modèles exacts et métaheuristiques) — et la plupart sont autonomes, avec des pointeurs vers les prérequis pertinents. À cela s'ajoutent les **jumeaux C#** (App-1b, App-2b, App-3b, App-4b, App-5b, App-6-CSharp, App-7b, App-8-CSharp, App-9b, App-10b, App-11b, App-13b, App-14-CSharp, App-14c, App-15b, App-16-CSharp, App-17b, App-18b, App-19-CSharp, App-20b) qui déroulent les mêmes algorithmes *from-scratch* en .NET, en complément des versions Python qui invoquent des solveurs industriels. +C'est ici que la série Search se confronte au réel. Les 59 notebooks d'application, pour la plupart adaptés de projets étudiants, prennent les algorithmes des Parties 1 et 2 et les mettent face à des problèmes qui ne se laissent pas faire : planifier les gardes d'un service hospitalier, ordonnancer un atelier, construire un calendrier sportif équitable, router une flotte de véhicules. Trois catégories les organisent — **Search pur** (jeux combinatoires), **CSP** (satisfaction de contraintes) et **Hybride** (combinaisons de solveurs, modèles exacts et métaheuristiques) — et la plupart sont autonomes, avec des pointeurs vers les prérequis pertinents. À cela s'ajoutent les **jumeaux C#** (App-1b, App-2b, App-3b, App-4b, App-5b, App-6-CSharp, App-7b, App-8-CSharp, App-9b, App-10b, App-11b, App-13b, App-14-CSharp, App-14c, App-15b, App-16-CSharp, App-17b, App-18b, App-19-CSharp, App-20b) qui déroulent les mêmes algorithmes *from-scratch* en .NET, en complément des versions Python qui invoquent des solveurs industriels. -Sous-série de **58 notebooks** | **~45h35** | Python 3.10+ (`ortools`, `python-sat`, `deap`, `mealpy`, `minizinc`, `optuna`, `rustuna`) ; .NET 9 (`dotnet-interactive`) pour les jumeaux C# +Sous-série de **59 notebooks** | **~46h35** | Python 3.10+ (`ortools`, `python-sat`, `deap`, `mealpy`, `minizinc`, `optuna`, `rustuna`) ; .NET 9 (`dotnet-interactive`) pour les jumeaux C# ## Pourquoi cette sous-série @@ -33,7 +33,7 @@ Un algorithme compris sur un exemple jouet n'est pas encore un algorithme maîtr Applications/ ├── Search/ # Applications purement Search (5 notebooks : 3 Python + 2 twins C#) ├── CSP/ # Applications CSP (31 notebooks : 18 Python + 13 twins C#) -└── Hybrid/ # Méthodes hybrides / métaheuristiques (22 notebooks : 17 Python + 5 twins C#) +└── Hybrid/ # Méthodes hybrides / métaheuristiques (23 notebooks : 18 Python + 5 twins C#) ``` ```mermaid @@ -161,6 +161,7 @@ Quand l'espace est trop vaste ou l'objectif trop irrégulier pour les méthodes | 14 | [App-29-SALBP-AssemblyLineBalancing-Audit](Hybrid/App-29-SALBP-AssemblyLineBalancing-Audit.ipynb) | ~70 min | SALBP-1/2 : CP-SAT, PuLP/CBC et RPW, statuts/incumbents/bornes, identité de benchmark, front Pareto certifié et MMALBP robuste/pondéré — distillation PrCon B1 (Ilias Kalalou, Kaelan Grall) | Projet étudiant (PrCon PR #57) | | 15 | [App-30-OrbitalAssembly-Certificate-Audit](Hybrid/App-30-OrbitalAssembly-Certificate-Audit.ipynb) | ~70 min | Assemblage orbital : physique de Hohmann dans le modèle, auditeur externe, preuve que la scalarisation est exactement lexicographique, comparaison à makespan égal, front d'échange ε-contrainte et sonde en taille **et** en densité — distillation PrCon C4 (Gurvan Estable, Joris Bely, Kévin Lubert) | Projet étudiant (PrCon PR #53) | | 16 | [App-31-RCPSP-Max-Feasibility-Bounds](Hybrid/App-31-RCPSP-Max-Feasibility-Bounds.ipynb) | ~65 min | RCPSP/max : le lag maximal comme arc inverse, faisabilité NP-difficile et son témoin de circuit positif, balayage des trois régimes temporel/ressource/réalisable, échelle de bornes certifiées et repli explicite quand la référence externe manque — distillation PrCon B4 (Arthur Gallier, Nicolas Naegelen) | Projet étudiant (PrCon PR #51) | +| 17 | [App-33-NeuralDiving-Coloration](Hybrid/App-33-NeuralDiving-Coloration.ipynb) | ~60 min | Neural diving : un plongeur MLP prédit une affectation partielle, injectée comme hint réparable dans CP-SAT ; la médiane des branches recule (2829 → 2325, 25/25 instances améliorées) alors que ≈ 49 % des arêtes du hint violent l'adjacence — cohérence du hint avec les contraintes, pas précision par bit — hommage Nair et al. 2021 | Recherche (Nair et al. 2021) | --- @@ -238,6 +239,7 @@ Quand l'espace est trop vaste ou l'objectif trop irrégulier pour les méthodes | App-29 SALBP AssemblyLineBalancing Audit | CSP-3 (CP-SAT), CSP-4 (scheduling), CSP-5 (optimisation) | ortools, pulp, pandas, numpy, matplotlib | | App-30 OrbitalAssembly Certificate Audit | CSP-3 (CP-SAT), CSP-4 (scheduling), CSP-5 (optimisation) | ortools, pandas, matplotlib | | App-31 RCPSP Max Feasibility Bounds | CSP-4 (scheduling), CSP-3 (CP-SAT), Planners-8 (temporel) | ortools, pandas, numpy, matplotlib | +| App-33 NeuralDiving Coloration | App-28 (composante branchement), CSP-3 (CP-SAT), MGS-16 (sélection d'algorithmes) | ortools, scikit-learn, numpy, pandas | --- @@ -265,6 +267,8 @@ Le [App-30-OrbitalAssembly-Certificate-Audit](Hybrid/App-30-OrbitalAssembly-Cert Le [App-31-RCPSP-Max-Feasibility-Bounds](Hybrid/App-31-RCPSP-Max-Feasibility-Bounds.ipynb) rend hommage au projet PrCon B4 d'**Arthur Gallier** et **Nicolas Naegelen**, *« RCPSP — ordonnancement de projet sous contraintes de ressources »*, PR [PrCon #51](https://github.com/jsboigeEpita/2026-Epita-Programmation-par-Contraintes/pull/51). Leur rendu a fait ce que [Planners-8-Temporal](../../SymbolicAI/Planners/03-Advanced/Planners-8-Temporal-Csharp.ipynb) laissait explicitement « en exercice » : confronter un solveur à un benchmark public d'ordonnancement. La vérification indépendante conduite pour cette distillation confirme leurs dix makespans, tous recalculés `OPTIMAL` par un modèle réécrit de zéro. Le prolongement CoursIA porte sur ce que leur section RCPSP/max rendait possible sans l'exercer : un lag maximal s'encode par un **arc inverse** qui referme un circuit, la faisabilité elle-même devient NP-difficile (Bartusch, Möhring, Radermacher, 1988), et un circuit de poids strictement positif en est le témoin vérifiable à la main. Le notebook balaye les trois régimes — infaisable temporellement, infaisable par les ressources, réalisable —, mesure que le diagnostic polynomial épargne au solveur le tiers gauche du domaine, et propose une échelle de bornes certifiées comme repli explicite lorsqu'aucune référence externe n'est disponible : le champ `niveau_de_preuve` interdit de confondre un écart à une borne calculée avec un écart à un optimum connu. Aucun code, texte ou figure étudiante n'est copié. Provenance : [`Hybrid/data/app31-rcpsp-max/SOURCE.md`](Hybrid/data/app31-rcpsp-max/SOURCE.md). +Le [App-33-NeuralDiving-Coloration](Hybrid/App-33-NeuralDiving-Coloration.ipynb) rend hommage au second geste de l'article de Nair et al. (2021), *« Solving Mixed Integer Programs Using Neural Networks »* (arXiv:2012.13349) : le **diving**, qui apprend une solution partielle pour guider un solveur MIP. App-28 a audité la composante *branching* (politique de branchement apprise) ; App-33 enchaîne sur l'autre composante : un plongeur MLP prédit une affectation des 60 sommets d'une coloration de graphe, injectée comme `hint` réparable dans OR-Tools CP-SAT, et l'on mesure l'effet sur les branches de preuve. La famille est calibrée pour qu'une fenêtre existe : les set-cover denses s'effondrent au presolve (`nodes = 0`) et les knapsack corrélés ne se prouvent pas en fenêtre notebook ; la coloration 60 sommets / 3 arêtes branche (médiane 2742, 2303-3799 sur les 60 instances d'entraînement) et se prouve (< 0,1 s). Verdict mesuré sur 25 instances de test, sur un run déterministe (`num_workers = 1`) : la médiane des branches recule (2829 → 2325, ≈ −18 %), les 25 instances s'améliorent (gain relatif médian ≈ 15 %, de 8 % à 28 %), et ≈ 49 % des arêtes du hint (44 sur ~90) sont en conflit avec l'adjacence — la cohérence, pas la précision par bit, détermine l'effet du hint. Le notebook ne copie aucun code, donnée, figure ou prose de l'article, archivé au gisement `G:\Mon Drive\MyIA\IA\Bibliographie IA\Search\`. Provenance : [`Hybrid/data/app33-neural-diving/SOURCE.md`](Hybrid/data/app33-neural-diving/SOURCE.md). + --- ## Ponts inter-séries @@ -306,6 +310,7 @@ Couverture par application des sources fondatrices mobilisées dans cette sous-s | App-29 (SALBP Assembly Line Balancing Audit) | Salveson, M. E. (1955) — « The Assembly Line Balancing Problem », *Journal of Industrial Engineering* 6(3) ; Helgeson, W. B., & Birnie, D. P. (1961) — « Assembly Line Balancing Using the Ranked Positional Weight Technique », *Journal of Industrial Engineering* 12(6) ; Scholl, A. (1999) — *Balancing and Sequencing of Assembly Lines*, Physica-Verlag. | | App-30 (OrbitalAssembly Certificate Audit) | Hohmann, W. (1925) — *Die Erreichbarkeit der Himmelskörper*, Oldenbourg ; Vallado, D. A. (2013) — *Fundamentals of Astrodynamics and Applications*, 4e éd., Microcosm Press ; Haimes, Y. Y., Lasdon, L. S., & Wismer, D. A. (1971) — « On a Bicriterion Formulation of the Problems of Integrated System Identification and System Optimization », *IEEE Transactions on Systems, Man, and Cybernetics* 1(3) (ε-contrainte) ; Wilcoxon, F. (1945) — « Individual Comparisons by Ranking Methods », *Biometrics Bulletin* 1(6). | | App-31 (RCPSP Max Feasibility Bounds) | Bartusch, M., Möhring, R. H., & Radermacher, F. J. (1988) — « Scheduling Project Networks with Resource Constraints and Time Windows », *Annals of Operations Research* 16(1) ; Kolisch, R., & Sprecher, A. (1997) — « PSPLIB — A Project Scheduling Problem Library », *European Journal of Operational Research* 96(1) ; Neumann, K., Schwindt, C., & Zimmermann, J. (2003) — *Project Scheduling with Time Windows and Scarce Resources*, Springer ; Bellman, R. (1958) — « On a Routing Problem », *Quarterly of Applied Mathematics* 16(1) (relaxation et détection de circuit). | +| App-33 (Neural Diving Coloration) | Nair, V., Bartunov, S., Gimeno, F., et al. (2021) — « Solving Mixed Integer Programs Using Neural Networks », arXiv:2012.13349 (v3, juillet 2021) ; Bengio, Y., Lodi, A., & Prouvost, A. (2021) — « Machine Learning for Combinatorial Optimization: a Methodological Tour d'Horizon », *European Journal of Operational Research* 290(2). | ## Conclusion / Prochaines étapes