diff --git a/MyIA.AI.Notebooks/GameTheory/GameTheory-13d-Optimistic-CFR-Python.ipynb b/MyIA.AI.Notebooks/GameTheory/GameTheory-13d-Optimistic-CFR-Python.ipynb index 3029230a861..8fdcd373ad1 100644 --- a/MyIA.AI.Notebooks/GameTheory/GameTheory-13d-Optimistic-CFR-Python.ipynb +++ b/MyIA.AI.Notebooks/GameTheory/GameTheory-13d-Optimistic-CFR-Python.ipynb @@ -5,10 +5,10 @@ "id": "774d952c", "metadata": { "papermill": { - "duration": 0.0038, - "end_time": "2026-10-14T22:36:23.300231+00:00", + "duration": 0.002423, + "end_time": "2026-10-20T10:31:28.949532+00:00", "exception": false, - "start_time": "2026-10-14T22:36:23.296431+00:00", + "start_time": "2026-10-20T10:31:28.947109+00:00", "status": "completed" }, "tags": [] @@ -37,7 +37,7 @@ "- Comprendre la **garantie théorique** $\\mathcal{O}(T^{-3/4})$ de l'OFTRL stable-prédictif vs $\\mathcal{O}(T^{-1/2})$ du CFR/CFR+.\n", "- Distinguer **mises à jour simultanées** (chaque joueur update avec les regrets de l'itération précédente) et **mises à jour alternées** (un joueur à la fois).\n", "- Apprendre à régler empiriquement $(\\kappa, \\alpha, \\beta)$ par profondeur de treeplex.\n", - "- Évaluer honnêtement : sur Kuhn poker, **DCFR reste empiriquement premier** sur 4 sous-jeux testés par les auteurs — l'OFTRL n'est pas une amélioration universelle.\n" + "- Évaluer honnêtement : sur Kuhn poker à 1000 itérations, **aucun solveur ne domine** (CFR `0,0065` · CFR+ `0,0068` · DCFR `0,0072` · OFTRL `0,0096`) — l'OFTRL y est **dernier**, et sa garantie $\\mathcal{O}(T^{-3/4})$ ne s'y traduit pas en avantage mesurable sur un jeu de 12 information sets.\n" ] }, { @@ -45,10 +45,10 @@ "id": "978f4b6d", "metadata": { "papermill": { - "duration": 0.002409, - "end_time": "2026-10-14T22:36:23.305600+00:00", + "duration": 0.002299, + "end_time": "2026-10-20T10:31:28.956097+00:00", "exception": false, - "start_time": "2026-10-14T22:36:23.303191+00:00", + "start_time": "2026-10-20T10:31:28.953798+00:00", "status": "completed" }, "tags": [] @@ -73,16 +73,16 @@ "id": "483c8fcb", "metadata": { "execution": { - "iopub.execute_input": "2026-10-14T22:36:23.312061Z", - "iopub.status.busy": "2026-10-14T22:36:23.311763Z", - "iopub.status.idle": "2026-10-14T22:36:30.723902Z", - "shell.execute_reply": "2026-10-14T22:36:30.721566Z" + "iopub.execute_input": "2026-10-20T10:31:28.963866Z", + "iopub.status.busy": "2026-10-20T10:31:28.963522Z", + "iopub.status.idle": "2026-10-20T10:31:45.823891Z", + "shell.execute_reply": "2026-10-20T10:31:45.822772Z" }, "papermill": { - "duration": 7.41688, - "end_time": "2026-10-14T22:36:30.724732+00:00", + "duration": 16.86613, + "end_time": "2026-10-20T10:31:45.825792+00:00", "exception": false, - "start_time": "2026-10-14T22:36:23.307852+00:00", + "start_time": "2026-10-20T10:31:28.959662+00:00", "status": "completed" }, "tags": [] @@ -122,7 +122,13 @@ "output_type": "stream", "text": [ "\n", - "Resultats apres 5000 iterations:\n", + "Resultats apres 5000 iterations:\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ " CFR: utilite finale = 0.2450, exploitabilite = 0.002332\n", " CFR+: utilite finale = -0.0677, exploitabilite = 0.002881\n", "\n", @@ -179,7 +185,8 @@ " la cellule d'entrainement MCCFRSolver.train(50000, verbose=True)) tournent sans\n", " ecrire de barres de progression. Sortie : meme comportement qu'un\n", " iterable, aucun caractere emis. Le progres reel reste mesurable via\n", - " perf_counter si necessaire (cf. la cellule de benchmark 4 seeds).\"\"\"\n", + " perf_counter si necessaire (cf. la cellule de benchmark, dont l'axe\n", + " seeds a ete retire : les solveurs sont deterministes).\"\"\"\n", " def __init__(self, *args, **kwargs):\n", " self._iter = kwargs.get('iterable', args[0] if args else None)\n", "\n", @@ -315,10 +322,10 @@ "id": "a9a36529", "metadata": { "papermill": { - "duration": 0.001779, - "end_time": "2026-10-14T22:36:30.728739+00:00", + "duration": 0.004516, + "end_time": "2026-10-20T10:31:45.834356+00:00", "exception": false, - "start_time": "2026-10-14T22:36:30.726960+00:00", + "start_time": "2026-10-20T10:31:45.829840+00:00", "status": "completed" }, "tags": [] @@ -352,7 +359,9 @@ "\n", "### 1.3 Le bémol des auteurs\n", "\n", - "Sur Libratus et 4 grands sous-jeux, l'analyse empirique de Farina et al. montre que le pas théorique est **trop conservateur** sur les grands espaces : le **DCFR** (Discounted CFR) reste **premier**. Le présent notebook vise précisément à enseigner cette nuance — l'OFTRL gagne sur la garantie, le DCFR gagne sur la pratique.\n" + "Sur Libratus et 4 grands sous-jeux, l'analyse empirique de Farina et al. montre que le pas théorique est **trop conservateur** sur les grands espaces : le **DCFR** (Discounted CFR) y reste **premier**. C'est leur résultat, sur leurs sous-jeux — pas une loi générale.\n", + "\n", + "Le présent notebook vise précisément à enseigner cette nuance : l'OFTRL gagne sur la garantie, le DCFR gagne sur la pratique **des grands treeplex**. Le §2 mesure ce qu'il en reste sur Kuhn, qui est un tout autre régime — et la réponse est plus instructive qu'un classement.\n" ] }, { @@ -361,16 +370,16 @@ "id": "7a7df78c", "metadata": { "execution": { - "iopub.execute_input": "2026-10-14T22:36:30.733627Z", - "iopub.status.busy": "2026-10-14T22:36:30.733352Z", - "iopub.status.idle": "2026-10-14T22:36:30.832990Z", - "shell.execute_reply": "2026-10-14T22:36:30.831875Z" + "iopub.execute_input": "2026-10-20T10:31:45.843651Z", + "iopub.status.busy": "2026-10-20T10:31:45.843347Z", + "iopub.status.idle": "2026-10-20T10:31:46.026683Z", + "shell.execute_reply": "2026-10-20T10:31:46.025359Z" }, "papermill": { - "duration": 0.103297, - "end_time": "2026-10-14T22:36:30.833790+00:00", + "duration": 0.18964, + "end_time": "2026-10-20T10:31:46.028043+00:00", "exception": false, - "start_time": "2026-10-14T22:36:30.730493+00:00", + "start_time": "2026-10-20T10:31:45.838403+00:00", "status": "completed" }, "tags": [] @@ -498,10 +507,10 @@ "id": "md-1-4-dcfr", "metadata": { "papermill": { - "duration": 0.00188, - "end_time": "2026-10-14T22:36:30.837996+00:00", + "duration": 0.004686, + "end_time": "2026-10-20T10:31:46.035337+00:00", "exception": false, - "start_time": "2026-10-14T22:36:30.836116+00:00", + "start_time": "2026-10-20T10:31:46.030651+00:00", "status": "completed" }, "tags": [] @@ -509,13 +518,15 @@ "source": [ "### 1.4 Le champion empirique : DCFR, la mémoire oublieuse\n", "\n", - "Le pas optimiste ci-dessus offre la meilleure **garantie théorique** ; le §1.3 annonçait que la première place **empirique** reste au DCFR — c'est la cellule suivante qui l'implémente, pour que la confrontation soit loyale. Son principe tient en une phrase : *les regrets anciens sont du bruit, oublions-les progressivement*. Concrètement, la combinaison de Brown & Sandholm (2019) emboîte trois mécanismes :\n", + "Le pas optimiste ci-dessus offre la meilleure **garantie théorique** ; le §1.3 annonçait que la première place **empirique** revient au DCFR — c'est la cellule suivante qui l'implémente, pour que la confrontation soit loyale. Son principe tient en une phrase : *les regrets anciens sont du bruit, oublions-les progressivement*. Le résidu de regret de chaque tour est **pondéré** par un facteur qui croît vers 1, de sorte que les derniers tours pèsent davantage :\n", + "\n", + "- **pondération des regrets** par $t^{\\alpha}/(t^{\\alpha}+1)$, avec $\\alpha = 1{,}5$ — le regret du tour $t$ entre dans la somme avec un poids qui va de $1/2$ (à $t=1$) à presque $1$ (grands $t$) ;\n", + "- **pondération de la somme de stratégies** par $t^{\\beta}/(t^{\\beta}+1) = 1$ quand $\\beta = 0$ — désactivée par défaut, elle ne servirait qu'à ralentir l'uniformisation de la stratégie moyenne ;\n", + "- l'**écretage CFR+ des regrets négatifs**, et c'est là que se cache la seule ligne qui compte : l'écretage porte sur la **somme accumulée** (`R ← max(R + incrément, 0)`), pas sur l'incrément seul. Écrêter l'incrément (`R ← R + max(incrément, 0)`) interdit purement et simplement toute correction négative : la somme ne peut que croître, et la stratégie dérive. Le benchmark du §1.5 garde cette forme historique comme **témoin** pour mesurer l'écart.\n", "\n", - "- un **discount des regrets anciens** par $t/(t+\\alpha)$ — chaque tour pèse relativement plus que le précédent, la somme de regrets s'adapte aux dernières itérations ;\n", - "- un **discount de la somme de stratégies** par $(t/(t+\\beta))^2$ — désactivé par défaut ici ($\\beta = 0$), il ne servirait qu'à ralentir l'uniformisation de la stratégie moyenne ;\n", - "- le **clipping CFR+** ($\\gamma$) — les regrets négatifs sont écrêtés, la mise à jour ne propage que le regret positif accumulé.\n", + "Une précision d'honnêteté sur l'héritage : le DCFR de Brown & Sandholm (2019) **rééchelonne les regrets déjà accumulés** ; le solveur de ce carnet applique le facteur au **seul incrément du tour**. C'est donc une variante « oubli par le pas », plus simple à écrire, mais ce n'est pas la lettre du papier. Le benchmark mesure cette variante-là, pas la référence publiée.\n", "\n", - "L'ironie pédagogique est assumée : le solveur le plus simple conceptuellement — un CFR+ qui oublie — est celui que le benchmark suivant confrontera à l'OFTRL." + "L'ironie pédagogique est assumée : le solveur le plus simple conceptuellement — un CFR+ qui oublie — est celui que le benchmark suivant confrontera à l'OFTRL.\n" ] }, { @@ -524,16 +535,16 @@ "id": "5db88c83", "metadata": { "execution": { - "iopub.execute_input": "2026-10-14T22:36:30.843509Z", - "iopub.status.busy": "2026-10-14T22:36:30.843303Z", - "iopub.status.idle": "2026-10-14T22:36:30.964127Z", - "shell.execute_reply": "2026-10-14T22:36:30.962956Z" + "iopub.execute_input": "2026-10-20T10:31:46.045774Z", + "iopub.status.busy": "2026-10-20T10:31:46.045181Z", + "iopub.status.idle": "2026-10-20T10:31:46.246525Z", + "shell.execute_reply": "2026-10-20T10:31:46.245739Z" }, "papermill": { - "duration": 0.124756, - "end_time": "2026-10-14T22:36:30.965249+00:00", + "duration": 0.208256, + "end_time": "2026-10-20T10:31:46.248241+00:00", "exception": false, - "start_time": "2026-10-14T22:36:30.840493+00:00", + "start_time": "2026-10-20T10:31:46.039985+00:00", "status": "completed" }, "tags": [] @@ -557,19 +568,32 @@ } ], "source": [ - "# DCFRDiscountedSolver — DCFR Discounted CFR (discount sur regrets anciens)\n", - "# Le DCFR discount les regrets anciens par t / (t+alpha) ET la strategie par\n", - "# (t / (t+beta))^2, ce qui donne en pratique la convergence la plus rapide\n", - "# empiriquement sur Kuhn poker. Reference : Brown & Sandholm 2019.\n", + "# DCFRDiscountedSolver — variante \"oubli par le pas\"\n", + "# Le regret du tour t est pondere par t^alpha / (t^alpha + 1), la somme de\n", + "# strategies par t^beta / (t^beta + 1) : les derniers tours pesent davantage.\n", + "# Nuance assumee par rapport a Brown & Sandholm (2019) : la reference re-echelle\n", + "# les regrets DEJA accumules, ce solveur ne pondere que l'increment du tour.\n", "class DCFRDiscountedSolver(CFRSolver):\n", - " \"\"\"DCFR Discounted : combinaison de CFR+ (clip) + discount des regrets\n", - " anciens. C'est le 'DCFR' reference dans le papier OFTRL Farina 2019.\"\"\"\n", + " \"\"\"DCFR Discounted : CFR+ (clip des regrets) + ponderation croissante du pas.\n", "\n", - " def __init__(self, alpha: float = 1.5, beta: float = 0.0, gamma: float = 2.0):\n", + " C'est le 'DCFR' evoque par Farina et al. (2019) comme comparant empirique,\n", + " sous une forme simplifiee (ponderation de l'increment, cf. en-tete).\n", + "\n", + " clip='accumulated' : ecretage CFR+, applique a la SOMME accumulate\n", + " (regret_sum = max(regret_sum + increment, 0)).\n", + " clip='increment' : ecretage applique au SEUL increment du tour, sans\n", + " plancher sur la somme. Forme historique de ce carnet,\n", + " conservee comme temoin : le benchmark de la section 1.5\n", + " mesure l'ecart entre les deux.\"\"\"\n", + "\n", + " def __init__(self, alpha: float = 1.5, beta: float = 0.0,\n", + " clip: str = \"accumulated\"):\n", " super().__init__()\n", - " self._alpha = alpha # discount sur regret_sum\n", - " self._beta = beta # discount sur strategy_sum (si > 0)\n", - " self._gamma = gamma # clipping CFR+ sur les regrets negatifs\n", + " if clip not in (\"accumulated\", \"increment\"):\n", + " raise ValueError(\"clip doit valoir 'accumulated' ou 'increment'\")\n", + " self._alpha = alpha # ponderation sur l'increment de regret\n", + " self._beta = beta # ponderation sur l'increment de strategie (si > 0)\n", + " self._clip = clip\n", "\n", " def cfr(self, history, cards, reach_probs):\n", " if self.game.is_terminal(history):\n", @@ -587,15 +611,23 @@ " action_utils[a] = child_value[player]\n", " node_value += strategy[a] * action_utils[a]\n", " regrets = action_utils - node_value\n", - " # CFR+ clipping : max avec 0 + ponderation\n", - " positive_regrets = np.maximum(regrets, 0)\n", + " # Ponderation croissante du tour : le regret de t pese t^alpha/(t^alpha+1)\n", " t = max(self.iterations, 1)\n", - " # Discount factor = t^alpha / (t^alpha + 1)\n", - " discount = (t ** self._alpha) / (t ** self._alpha + 1)\n", - " self.regret_sum[info_set] += reach_probs[1 - player] * positive_regrets * discount\n", - " # Cumulative strategy discount\n", - " strat_discount = (t ** self._beta) / (t ** self._beta + 1) if self._beta > 0 else 1.0\n", - " self.strategy_sum[info_set] += reach_probs[0] * strategy * strat_discount\n", + " weight = (t ** self._alpha) / (t ** self._alpha + 1)\n", + " increment = reach_probs[1 - player] * regrets * weight\n", + " if self._clip == \"accumulated\":\n", + " # CFR+ : le plancher porte sur la somme, un regret negatif passe\n", + " # transitoirement puis est absorbe par les tours positifs suivants.\n", + " self.regret_sum[info_set] = np.maximum(\n", + " self.regret_sum[info_set] + increment, 0\n", + " )\n", + " else:\n", + " # Forme historique : seuls les increments positifs entrent dans la\n", + " # somme accumulate, aucune correction negative n'est jamais possible.\n", + " self.regret_sum[info_set] += np.maximum(increment, 0)\n", + " # Pondereration de la strategie moyenne (neutre tant que beta = 0)\n", + " strat_weight = (t ** self._beta) / (t ** self._beta + 1) if self._beta > 0 else 1.0\n", + " self.strategy_sum[info_set] += reach_probs[player] * strategy * strat_weight\n", " return np.array([node_value, -node_value]) if player == 0 else np.array(\n", " [-node_value, node_value]\n", " )\n", @@ -623,18 +655,29 @@ "id": "md-1-5-benchmark", "metadata": { "papermill": { - "duration": 0.003259, - "end_time": "2026-10-14T22:36:30.972122+00:00", + "duration": 0.004895, + "end_time": "2026-10-20T10:31:46.258553+00:00", "exception": false, - "start_time": "2026-10-14T22:36:30.968863+00:00", + "start_time": "2026-10-20T10:31:46.253658+00:00", "status": "completed" }, "tags": [] }, "source": [ - "### 1.5 L'instrument de mesure : même budget, même graine\n", + "### 1.5 L'instrument de mesure : même budget, même horloge, même référence\n", + "\n", + "Une comparaison honnête exige de neutraliser tout sauf l'opérateur de mise à jour. La cellule suivante instancie les quatre algorithmes — CFR, CFR+, DCFR, OFTRL — et les exécute sur Kuhn poker avec un **budget d'itérations identique**, une **horloge identique** (chaque solveur reçoit le même compteur `t`, dont DCFR et OFTRL dépendent pour calibrer leur pas) et une **itération CFR complète** : les six distributions de cartes, pas un sous-ensemble.\n", + "\n", + "Elle leur adjoint une cinquième ligne, qui n'est pas un concurrent mais un **témoin** : le DCFR de ce carnet dans sa forme historique, dont l'écretage porte sur l'incrément du tour au lieu de la somme accumulée. Une ligne de différence, et le §2 mesure ce qu'elle coûte.\n", + "\n", + "Deux mesures sont rapportées :\n", "\n", - "Une comparaison honnête exige de neutraliser tout sauf l'opérateur de mise à jour. La cellule suivante instancie les quatre variantes — CFR, CFR+, DCFR, OFTRL — et les exécute sur Kuhn poker avec un **budget d'itérations identique** et une **graine commune** (`np.random.seed`) : la séquence aléatoire est figée, l'écart final n'est imputable qu'au solveur. Deux mesures sont rapportées : le **temps de calcul** (`perf_counter`) et un **proxy d'exploitabilité** en démo — la norme de l'écart entre la stratégie moyenne apprise sur l'info set `Q` et l'uniforme $[0.5, 0.5]$. Le §1.3 laisse attendre le DCFR en tête ; la section 2 confrontera cette attente aux chiffres." + "- l'**exploitabilité réelle** — l'écart à l'équilibre par meilleure réponse, calculé par OpenSpiel et exposé par le socle (`get_exploitability()`), qui construit la politique tabulaire du solveur et la fait évaluer contre un meilleur-respondant optimal. Plus la valeur est basse, plus la stratégie est proche de Nash ;\n", + "- la **stratégie moyenne sur l'info set `Q`**, accompagnée de sa **référence de Nash** : le premier joueur détenant la Dame doit se coucher à 100 %, soit $[1, 0]$, et la distance affichée est nulle à l'équilibre.\n", + "\n", + "Le **temps de calcul** est également mesuré (`perf_counter`), mais il dépend de la machine : il informe sur le coût relatif des opérateurs, il ne fonde aucun verdict de qualité.\n", + "\n", + "Une précision qui a son importance : les solveurs sont **déterministes** — la récursion parcourt l'arbre en entier, aucun échantillonnage n'entre en jeu. Il n'y a donc pas de graine à faire varier : une version antérieure de ce carnet balayait 4 seeds × 2 modes de parcours et obtenait huit lignes identiques à l'octet. Ce balayage mesurait huit fois la même trajectoire ; l'axe a été retiré plutôt que présenté comme une robustesse qui n'existe pas.\n" ] }, { @@ -643,16 +686,16 @@ "id": "7df8e482", "metadata": { "execution": { - "iopub.execute_input": "2026-10-14T22:36:30.979546Z", - "iopub.status.busy": "2026-10-14T22:36:30.979312Z", - "iopub.status.idle": "2026-10-14T22:36:35.162655Z", - "shell.execute_reply": "2026-10-14T22:36:35.161508Z" + "iopub.execute_input": "2026-10-20T10:31:46.268303Z", + "iopub.status.busy": "2026-10-20T10:31:46.267535Z", + "iopub.status.idle": "2026-10-20T10:31:50.057445Z", + "shell.execute_reply": "2026-10-20T10:31:50.056031Z" }, "papermill": { - "duration": 4.188084, - "end_time": "2026-10-14T22:36:35.163523+00:00", + "duration": 3.797887, + "end_time": "2026-10-20T10:31:50.059277+00:00", "exception": false, - "start_time": "2026-10-14T22:36:30.975439+00:00", + "start_time": "2026-10-20T10:31:46.261390+00:00", "status": "completed" }, "tags": [] @@ -662,100 +705,103 @@ "name": "stdout", "output_type": "stream", "text": [ - " seed mode time(s) CFR CFR+ DCFR OFTRL\n", - " 0 simultaneous 0.6183 [0.9981, 0.0019] [0.9982, 0.0018] [0.8789, 0.1211] [0.9772, 0.0228]\n", - " 0 alternating 0.4020 [0.9981, 0.0019] [0.9982, 0.0018] [0.8789, 0.1211] [0.9772, 0.0228]\n", - " 1 simultaneous 0.5775 [0.9981, 0.0019] [0.9982, 0.0018] [0.8789, 0.1211] [0.9772, 0.0228]\n", - " 1 alternating 0.4435 [0.9981, 0.0019] [0.9982, 0.0018] [0.8789, 0.1211] [0.9772, 0.0228]\n", - " 7 simultaneous 0.6104 [0.9981, 0.0019] [0.9982, 0.0018] [0.8789, 0.1211] [0.9772, 0.0228]\n", - " 7 alternating 0.4233 [0.9981, 0.0019] [0.9982, 0.0018] [0.8789, 0.1211] [0.9772, 0.0228]\n", - " 42 simultaneous 0.6294 [0.9981, 0.0019] [0.9982, 0.0018] [0.8789, 0.1211] [0.9772, 0.0228]\n", - " 42 alternating 0.4620 [0.9981, 0.0019] [0.9982, 0.0018] [0.8789, 0.1211] [0.9772, 0.0228]\n" + "Budget : 1000 iterations x 6 distributions = 6000 traversees par solveur (3.76 s au total, tous solveurs confondus)\n", + "Reference : Nash sur 'Q' = [1.0, 0.0] (distance 0) ; valeur de jeu de Kuhn pour J1 = -0.0556\n", + "\n", + " solveur strategie moyenne Q distance a Nash exploitabilite\n", + " CFR [0.9988, 0.0012] 0.0024 0.006505\n", + " CFR+ [0.9889, 0.0111] 0.0221 0.006784\n", + " DCFR [0.988, 0.012] 0.0240 0.007174\n", + " DCFR (clip increment) [0.6208, 0.3792] 0.7585 0.071657\n", + " OFTRL [0.9985, 0.0015] 0.0030 0.009622\n", + "\n", + "Exploitabilite la plus basse (le plus proche de l'equilibre) : CFR (0.006505)\n" ] } ], "source": [ - "# Benchmark Kuhn Poker : exploitabilite + temps de calcul\n", - "# Budget d'iterations identique pour les 4 solveurs.\n", + "# Benchmark Kuhn Poker : exploitabilite reelle, strategie moyenne, temps de calcul\n", + "# Meme budget d'iterations et meme horloge pour tous les solveurs ; chaque iteration\n", + "# parcourt les 6 distributions de cartes (iteration CFR complete, pas un echantillon).\n", + "import itertools\n", "import time\n", "\n", - "def exploitability(solver) -> float:\n", - " \"\"\"Mesure l'exploitabilite d'un solver Kuhn : distance a la valeur Nash theorique\n", - " (Kuhn vaut ~-0.0558 en zero-sum, J1).\"\"\"\n", - " # Calcul direct sur l'evolution moyenne\n", - " val_J1 = 0.0\n", - " val_J2 = 0.0\n", - " counts_J1 = 0\n", - " counts_J2 = 0\n", - " for c1 in [0, 1, 2]:\n", - " for c2 in [0, 1, 2]:\n", - " if c1 == c2:\n", - " continue\n", - " # Iterer sur les histories avec la strategie moyenne\n", - " qJ1 = solver.get_average_strategy(\n", - " ['J','Q','K'][c1] + ''\n", - " )\n", - " qJ2 = solver.get_average_strategy(\n", - " ['J','Q','K'][c2] + ''\n", - " )\n", - " val_J1 += np.maximum(qJ1[1] - qJ1[0], -qJ1[1] + qJ1[0])\n", - " counts_J1 += 1\n", - " # Pour la demo, retourner la variation de la strategie 'Q'\n", - " return float(np.linalg.norm(solver.get_average_strategy('Q') - np.array([0.5, 0.5])))\n", + "# References communes, imprimees a cote des colonnes : le lecteur peut juger le\n", + "# verdict sans calcul. Toutes deux viennent du carnet principal GT-13 (section 3).\n", + "NASH_Q = np.array([1.0, 0.0]) # P0 detenant Q au root : check a 100 % (puis fold sur mise)\n", + "GAME_VALUE_J1 = -1.0 / 18.0 # valeur de jeu de Kuhn pour le premier joueur\n", + "\n", + "DEALS = list(itertools.permutations([0, 1, 2], 2)) # 6 distributions equiprobables\n", "\n", "\n", - "def run_benchmark(iterations: int, seed: int, update_mode: str = \"simultaneous\"):\n", - " \"\"\"Execute 4 solveurs CFR/CFR+/DCFR/OFTRL avec meme budget et compare.\"\"\"\n", - " np.random.seed(seed)\n", - " s_cfr = CFRSolver()\n", - " s_cfr_plus = CFRPlusSolver()\n", - " s_dcfr = DCFRDiscountedSolver()\n", - " s_oftl = OFTRLSolver()\n", + "def run_benchmark(iterations: int = 1000):\n", + " \"\"\"Execute les solveurs sur le meme nombre d'iterations CFR completes.\n", "\n", + " Les solveurs sont deterministes (aucun echantillonnage) : il n'y a donc pas de\n", + " graine a faire varier. Seule l'horloge commune change d'une iteration a l'autre,\n", + " et elle compte : DCFR et OFTRL lisent `self.iterations` pour calibrer leur pas.\n", + "\n", + " La cinquieme entree est un temoin, pas un concurrent : 'DCFR (clip increment)'\n", + " est la forme historique du carnet, dont l'ecretage porte sur l'increment au lieu\n", + " de la somme accumulate. Elle mesure le cout d'une seule ligne de difference.\n", + " \"\"\"\n", + " solvers = {\n", + " \"CFR\": CFRSolver(),\n", + " \"CFR+\": CFRPlusSolver(),\n", + " \"DCFR\": DCFRDiscountedSolver(clip=\"accumulated\"),\n", + " \"DCFR (clip increment)\": DCFRDiscountedSolver(clip=\"increment\"),\n", + " \"OFTRL\": OFTRLSolver(kappa=1.0, alpha=0.5, beta=1.0),\n", + " }\n", " start = time.perf_counter()\n", " for t in range(iterations):\n", - " if update_mode == \"alternating\":\n", - " # Alternating : seul le joueur courant update ses regrets\n", - " for p in (0, 1):\n", - " # CFR : reach prob asymetrique selon le joueur\n", - " s_cfr.cfr(\"\", [0, 1] if p == 0 else [1, 0], np.array([1.0, 1.0]))\n", - " s_cfr_plus.cfr(\"\", [0, 1] if p == 0 else [1, 0], np.array([1.0, 1.0]))\n", - " s_dcfr.cfr(\"\", [0, 1] if p == 0 else [1, 0], np.array([1.0, 1.0]))\n", - " s_oftl.iterations = t + 1\n", - " s_oftl.cfr(\"\", [0, 1] if p == 0 else [1, 0], np.array([1.0, 1.0]))\n", - " else:\n", - " # Simultaneous : tous les joueurs update\n", - " for cards_pair in [(0, 1), (1, 0), (2, 1)]:\n", - " s_cfr.cfr(\"\", list(cards_pair), np.array([1.0, 1.0]))\n", - " s_cfr_plus.cfr(\"\", list(cards_pair), np.array([1.0, 1.0]))\n", - " s_dcfr.cfr(\"\", list(cards_pair), np.array([1.0, 1.0]))\n", - " s_oftl.iterations = t + 1\n", - " s_oftl.cfr(\"\", list(cards_pair), np.array([1.0, 1.0]))\n", + " for solver in solvers.values():\n", + " solver.iterations = t + 1\n", + " for cards in DEALS:\n", + " for solver in solvers.values():\n", + " solver.cfr(\"\", list(cards), np.array([1.0, 1.0]))\n", " elapsed = time.perf_counter() - start\n", "\n", + " rows = []\n", + " for name, solver in solvers.items():\n", + " strategy_q = solver.get_average_strategy(\"Q\")\n", + " rows.append({\n", + " \"solver\": name,\n", + " \"strategy_q\": strategy_q.round(4).tolist(),\n", + " \"distance_nash\": float(np.abs(strategy_q - NASH_Q).sum()),\n", + " # Vraie exploitabilite (meilleure reponse) : deleguee a OpenSpiel par le socle.\n", + " \"exploitability\": solver.get_exploitability(),\n", + " })\n", " return {\n", " \"iterations\": iterations,\n", + " \"deals\": len(DEALS),\n", " \"elapsed_s\": round(elapsed, 4),\n", - " \"update_mode\": update_mode,\n", - " \"seed\": seed,\n", - " \"CFR_strategy_Q\": s_cfr.get_average_strategy('Q').round(4).tolist(),\n", - " \"CFRp_strategy_Q\": s_cfr_plus.get_average_strategy('Q').round(4).tolist(),\n", - " \"DCFR_strategy_Q\": s_dcfr.get_average_strategy('Q').round(4).tolist(),\n", - " \"OFTRL_strategy_Q\": s_oftl.get_average_strategy('Q').round(4).tolist(),\n", + " \"rows\": rows,\n", " }\n", "\n", "\n", - "# 4 seeds + 2 modes = 8 runs\n", - "SEEDS = [0, 1, 7, 42]\n", - "results = []\n", - "for seed in SEEDS:\n", - " for mode in (\"simultaneous\", \"alternating\"):\n", - " results.append(run_benchmark(iterations=1000, seed=seed, update_mode=mode))\n", - "print(f\"{'seed':>5} {'mode':>13} {'time(s)':>10} {'CFR':>17} {'CFR+':>17} {'DCFR':>17} {'OFTRL':>17}\")\n", - "for r in results:\n", - " print(f\"{r['seed']:>5} {r['update_mode']:>13} {r['elapsed_s']:>10.4f} \"\n", - " f\"{str(r['CFR_strategy_Q']):>17} {str(r['CFRp_strategy_Q']):>17} \"\n", - " f\"{str(r['DCFR_strategy_Q']):>17} {str(r['OFTRL_strategy_Q']):>17}\")\n" + "bench = run_benchmark(iterations=1000)\n", + "print(f\"Budget : {bench['iterations']} iterations x {bench['deals']} distributions \"\n", + " f\"= {bench['iterations'] * bench['deals']} traversees par solveur \"\n", + " f\"({bench['elapsed_s']:.2f} s au total, tous solveurs confondus)\")\n", + "print(f\"Reference : Nash sur 'Q' = {NASH_Q.tolist()} (distance 0) ; \"\n", + " f\"valeur de jeu de Kuhn pour J1 = {GAME_VALUE_J1:.4f}\")\n", + "print()\n", + "print(f\"{'solveur':>22} {'strategie moyenne Q':>22} {'distance a Nash':>17} {'exploitabilite':>16}\")\n", + "for row in bench[\"rows\"]:\n", + " expl = row[\"exploitability\"]\n", + " expl_str = \"n/a (OpenSpiel absent)\" if np.isnan(expl) else f\"{expl:.6f}\"\n", + " print(f\"{row['solver']:>22} {str(row['strategy_q']):>22} \"\n", + " f\"{row['distance_nash']:>17.4f} {expl_str:>16}\")\n", + "\n", + "measurables = [r for r in bench[\"rows\"] if not np.isnan(r[\"exploitability\"])]\n", + "if measurables:\n", + " best = min(measurables, key=lambda r: r[\"exploitability\"])\n", + " print()\n", + " print(f\"Exploitabilite la plus basse (le plus proche de l'equilibre) : {best['solver']} \"\n", + " f\"({best['exploitability']:.6f})\")\n", + "else:\n", + " print()\n", + " print(\"OpenSpiel absent : aucune exploitabilite mesurable sur cette machine.\")\n" ] }, { @@ -763,10 +809,10 @@ "id": "7c41f1ce", "metadata": { "papermill": { - "duration": 0.002154, - "end_time": "2026-10-14T22:36:35.168372+00:00", + "duration": 0.00335, + "end_time": "2026-10-20T10:31:50.066718+00:00", "exception": false, - "start_time": "2026-10-14T22:36:35.166218+00:00", + "start_time": "2026-10-20T10:31:50.063368+00:00", "status": "completed" }, "tags": [] @@ -774,14 +820,15 @@ "source": [ "## 2. Verdict honnête\n", "\n", - "Le benchmark précédent sur Kuhn Poker ne met **pas** l'OFTRL en valeur sur la métrique de **vitesse de convergence empirique** :\n", + "Le benchmark précédent porte une référence explicite (Nash sur `Q` = $[1, 0]$, valeur de jeu $-1/18$), une mesure qui ne dépend pas de l'échelle de temps (l'exploitabilité par meilleure réponse) et un budget commun de 1000 itérations complètes. Trois constats en sortent, et ils ne disent pas ce que la version antérieure de ce carnet annonçait.\n", "\n", - "- **DCFR est premier** sur les 4 seeds testés en mode simultané : la stratégie moyenne converge le plus rapidement à la stratégie Nash\n", - "- **CFR+** surpasse **CFR** vanilla, conformément à la littérature\n", - "- **OFTRL** avec $(\\kappa, \\alpha, \\beta) = (1.0, 0.5, 1.0)$ est lent — le pas optimiste théorique demande un **réglage empirique par sous-jeu**, ce qui est exactement la nuance soulignée par Farina et al. (2019)\n", - "- En mode **alternating**, les 4 solveurs ralentissent (la fréquence d'update par joueur tombe à 50%), ce qui **efface effectivement l'avantage optimiste** — un autre résultat documenté par les auteurs\n", + "**1. Aucun solveur ne domine à ce budget.** Les exploitabilités tiennent dans un facteur 1,5 : CFR `0,0065`, CFR+ `0,0068`, DCFR `0,0072`, OFTRL `0,0096`. Le classement existe, mais l'écart entre les trois premiers est du même ordre que la sensibilité au budget : annoncer un « champion » sur une seule mesure à 1000 itérations serait surinterpréter. Ce que la sortie établit : **l'OFTRL est dernier ici**, et sa garantie $\\mathcal{O}(T^{-3/4})$ ne se traduit pas en avantage mesurable sur un jeu de 12 information sets en 1000 itérations — exactement la nuance que Farina et al. (2019) soulignent eux-mêmes.\n", "\n", - "**Conclusion** : ce notebook ne vend pas l'OFTRL comme un remplacement du DCFR. Il enseigne **quand** la garantie $\\mathcal{O}(T^{-3/4})$ vs $\\mathcal{O}(T^{-1/2})$ compte (grands espaces, sécurité théorique) et **quand** le DCFR reste l'algorithme de référence empirique (budget limité, petits jeux).\n" + "**2. Le seul écart important est une ligne de code, pas un algorithme.** La cinquième ligne du tableau est un témoin, pas un concurrent : c'est le DCFR de ce carnet dans sa forme historique, dont l'écretage porte sur l'incrément du tour au lieu de la somme accumulée. Exploitabilité `0,0717` contre `0,0072` — **un facteur 10**, et une stratégie sur `Q` à `[0,62, 0,38]` là où l'équilibre demande `[1, 0]`. Un regret négatif écrêté avant d'entrer dans la somme ne peut plus jamais être corrigé : la somme monte, et la stratégie dérive. La leçon méthodologique vaut plus que le classement : la forme exacte de l'écretage appartient à la définition de l'algorithme, et la changer d'une ligne change un ordre de grandeur.\n", + "\n", + "**3. Les deux axes annoncés dans les versions antérieures ne mesuraient rien.** Les quatre solveurs sont déterministes : ni les 4 seeds ni les 2 modes de parcours ne changeaient une seule stratégie (huit lignes identiques à l'octet). Ces deux axes ont été retirés du benchmark plutôt que présentés comme une robustesse. Le mode alterné reste un **exercice** (exercice 2) — c'est un travail d'implémentation, pas un paramètre qu'on lit dans une sortie.\n", + "\n", + "**Conclusion** : ce carnet ne vend pas l'OFTRL comme un remplacement du DCFR, et il ne vend plus le DCFR comme le champion des petits budgets. Il enseigne **quand** la garantie théorique compte (grands espaces, sécurité) et **à quel point** les détails d'implémentation d'un opérateur de regret — une pondération, un écretage — pèsent plus lourd que le choix de la famille d'algorithme à petite échelle.\n" ] }, { @@ -789,10 +836,10 @@ "id": "0993e1e0", "metadata": { "papermill": { - "duration": 0.002021, - "end_time": "2026-10-14T22:36:35.172430+00:00", + "duration": 0.003917, + "end_time": "2026-10-20T10:31:50.074955+00:00", "exception": false, - "start_time": "2026-10-14T22:36:35.170409+00:00", + "start_time": "2026-10-20T10:31:50.071038+00:00", "status": "completed" }, "tags": [] @@ -802,13 +849,14 @@ "\n", "### Exercice 1 — Régler $(\\kappa, \\alpha, \\beta)$ par profondeur\n", "\n", - "**Énoncé** : Soit le treeplex de Kuhn Poker de profondeur 2 (cartes → action → action). Ajuster $(\\kappa, \\alpha, \\beta)$ pour maximiser la convergence de l'OFTRL en 100 itérations sur le mode `simultaneous`. Mesurer la vitesse en utilisant `distance_to_uniform` sur la stratégie moyenne du J1 sur l'info-set `Q`.\n", + "**Énoncé** : Soit le treeplex de Kuhn Poker de profondeur 2 (cartes → action → action). Ajuster $(\\kappa, \\alpha, \\beta)$ pour **minimiser l'exploitabilité** de l'OFTRL après 100 itérations complètes (6 distributions par itération). L'instrument est `get_exploitability()` du socle — la même mesure que le §1.5, un écart par meilleure réponse — et non une distance à la stratégie uniforme, qui n'est pas un écart à l'équilibre.\n", "\n", "**Indices** :\n", "- `depth` est passé récursivement à `cfr()` ; il borne la profondeur de l'arbre\n", "- $(\\kappa, \\alpha, \\beta)$ trop petits → le pas optimiste disparaît, on retombe sur CFR\n", "- $(\\kappa, \\alpha, \\beta)$ trop grands → oscillations, le regret oscille au lieu de converger\n", - "- Pour Kuhn (profondeur 2), les auteurs recommandent typiquement $\\kappa \\approx 1.5$, $\\alpha \\approx 0.3$, $\\beta \\approx 0.5$\n" + "- Pour Kuhn (profondeur 2), les auteurs recommandent typiquement $\\kappa \\approx 1.5$, $\\alpha \\approx 0.3$, $\\beta \\approx 0.5$\n", + "- Contrôle de sanité : la stratégie moyenne sur `Q` doit tendre vers $[1, 0]$ (le premier joueur se couche avec la Dame) ; si elle reste à $[0.5, 0.5]$, l'info set n'a jamais été visité — vérifier la distribution de cartes utilisée\n" ] }, { @@ -817,16 +865,16 @@ "id": "97691336", "metadata": { "execution": { - "iopub.execute_input": "2026-10-14T22:36:35.177899Z", - "iopub.status.busy": "2026-10-14T22:36:35.177657Z", - "iopub.status.idle": "2026-10-14T22:36:35.182243Z", - "shell.execute_reply": "2026-10-14T22:36:35.181190Z" + "iopub.execute_input": "2026-10-20T10:31:50.083699Z", + "iopub.status.busy": "2026-10-20T10:31:50.083150Z", + "iopub.status.idle": "2026-10-20T10:31:50.088444Z", + "shell.execute_reply": "2026-10-20T10:31:50.087308Z" }, "papermill": { - "duration": 0.008351, - "end_time": "2026-10-14T22:36:35.182941+00:00", + "duration": 0.011335, + "end_time": "2026-10-20T10:31:50.090104+00:00", "exception": false, - "start_time": "2026-10-14T22:36:35.174590+00:00", + "start_time": "2026-10-20T10:31:50.078769+00:00", "status": "completed" }, "tags": [] @@ -837,27 +885,27 @@ "output_type": "stream", "text": [ "Exercice 1 squelette pret. A completer par l'etudiant.\n", - "Indice : utiliser une grid search externe sur itertools.product,\n", - "mesurer distance_to_uniform apres 100 iter, retourner le meilleur (kappa, alpha, beta).\n" + "Indice : grid search sur itertools.product, mesurer get_exploitability()\n", + "apres 100 iterations, retourner le meilleur (kappa, alpha, beta).\n" ] } ], "source": [ "# Exercice 1 — squelette a completer\n", "def exercise_1_oftl_tuning():\n", - " \"\"\"Trouver (kappa, alpha, beta) qui maximise la convergence OFTRL en 100 iter\n", - " sur Kuhn Poker simultane. Retourner la distance L2 a la strategie uniforme\n", - " sur l'info-set 'Q'.\"\"\"\n", + " \"\"\"Trouver (kappa, alpha, beta) qui minimise l'exploitabilite de l'OFTRL\n", + " apres 100 iterations completes sur Kuhn Poker. Retourner\n", + " (best_params, best_exploitability).\"\"\"\n", " best = None\n", - " best_distance = float(\"inf\")\n", + " best_exploitability = float(\"inf\")\n", " # TODO etudiant : grid search sur (kappa, alpha, beta)\n", - " # pour chaque combinaison, executer 100 iterations et mesurer\n", - " # la distance a la strategie Nash theorique sur 'Q'.\n", - " return best, best_distance\n", + " # pour chaque combinaison, executer 100 iterations completes (les 6\n", + " # distributions de cartes) et lire get_exploitability().\n", + " return best, best_exploitability\n", "\n", "print(\"Exercice 1 squelette pret. A completer par l'etudiant.\")\n", - "print(\"Indice : utiliser une grid search externe sur itertools.product,\")\n", - "print(\"mesurer distance_to_uniform apres 100 iter, retourner le meilleur (kappa, alpha, beta).\")\n" + "print(\"Indice : grid search sur itertools.product, mesurer get_exploitability()\")\n", + "print(\"apres 100 iterations, retourner le meilleur (kappa, alpha, beta).\")\n" ] }, { @@ -865,10 +913,10 @@ "id": "6b931c34", "metadata": { "papermill": { - "duration": 0.002261, - "end_time": "2026-10-14T22:36:35.187637+00:00", + "duration": 0.004206, + "end_time": "2026-10-20T10:31:50.101276+00:00", "exception": false, - "start_time": "2026-10-14T22:36:35.185376+00:00", + "start_time": "2026-10-20T10:31:50.097070+00:00", "status": "completed" }, "tags": [] @@ -876,12 +924,14 @@ "source": [ "### Exercice 2 — Modes simultanés vs alternés sur DCFR\n", "\n", - "**Énoncé** : Comparer le comportement de DCFR en mode `simultaneous` (tous les joueurs update à chaque itération) et `alternating` (un joueur sur deux). Tracer pour chaque seed une courbe `NashConv` (proxy : distance L2 à la stratégie uniforme sur l'info-set `Q`) en fonction des itérations, pour 1000 itérations.\n", + "**Énoncé** : Comparer le comportement de DCFR en mode `simultaneous` (tous les joueurs update à chaque itération) et `alternating` (un joueur sur deux). Tracer pour chaque mode la courbe d'exploitabilité (`get_exploitability()`) en fonction des itérations, sur 1000 itérations.\n", + "\n", + "Le benchmark du §1.5 ne porte pas cet axe : les deux modes y produisaient des stratégies identiques parce que les solveurs sont déterministes et que l'info set observé est mis à jour une fois par itération dans les deux cas. Ici, c'est à vous d'implémenter une vraie alternance — un seul joueur met à jour ses regrets par traversée — pour que la comparaison ait un objet.\n", "\n", "**Indices** :\n", "- DCFR avec alternation tend à être **plus stable** car chaque joueur voit moins de bruit adversarial\n", "- mais DCFR simultané converge **plus vite** en moyenne sur petits jeux\n", - "- Vérifier empiriquement sur les 4 seeds si l'effet est significatif\n" + "- Les solveurs étant déterministes, une seule exécution par mode suffit : il n'y a pas de graine à moyenner\n" ] }, { @@ -890,16 +940,16 @@ "id": "dc825f88", "metadata": { "execution": { - "iopub.execute_input": "2026-10-14T22:36:35.194319Z", - "iopub.status.busy": "2026-10-14T22:36:35.194114Z", - "iopub.status.idle": "2026-10-14T22:36:35.198464Z", - "shell.execute_reply": "2026-10-14T22:36:35.197212Z" + "iopub.execute_input": "2026-10-20T10:31:50.110889Z", + "iopub.status.busy": "2026-10-20T10:31:50.110212Z", + "iopub.status.idle": "2026-10-20T10:31:50.116154Z", + "shell.execute_reply": "2026-10-20T10:31:50.114764Z" }, "papermill": { - "duration": 0.008938, - "end_time": "2026-10-14T22:36:35.199198+00:00", + "duration": 0.012432, + "end_time": "2026-10-20T10:31:50.117677+00:00", "exception": false, - "start_time": "2026-10-14T22:36:35.190260+00:00", + "start_time": "2026-10-20T10:31:50.105245+00:00", "status": "completed" }, "tags": [] @@ -910,20 +960,23 @@ "output_type": "stream", "text": [ "Exercice 2 squelette pret. A completer par l'etudiant.\n", - "Indice : utiliser plt.plot sur les resultats, ajouter label (seed, mode).\n" + "Indice : utiliser plt.plot sur les resultats, ajouter un label par mode.\n" ] } ], "source": [ - "# Exercice 2 — tracer NashConv iteration par iteration\n", + "# Exercice 2 — courbes d'exploitabilite par mode\n", "def exercise_2_dcfr_alternating():\n", - " \"\"\"Pour 4 seeds et 2 modes, mesurer NashConv (distance L2 a uniforme sur 'Q')\n", - " tous les 50 iterations sur 1000 iterations DCFR. Retourner un dict seed -> mode -> list.\"\"\"\n", - " # TODO etudiant : remplir la structure result[seed][mode] = [(iter, distance), ...]\n", + " \"\"\"Pour les deux modes (simultaneous / alternating), mesurer l'exploitabilite\n", + " de DCFR tous les 50 iterations sur 1000 iterations.\n", + " Retourner un dict mode -> liste de (iteration, exploitabilite).\"\"\"\n", + " # TODO etudiant : remplir la structure result[mode] = [(iter, exploitabilite), ...]\n", + " # Indice : pour un vrai mode alterne, faire deux traversées par iteration en ne\n", + " # mettant a jour les regrets que du joueur courant.\n", " return {}\n", "\n", "print(\"Exercice 2 squelette pret. A completer par l'etudiant.\")\n", - "print(\"Indice : utiliser plt.plot sur les resultats, ajouter label (seed, mode).\")\n" + "print(\"Indice : utiliser plt.plot sur les resultats, ajouter un label par mode.\")\n" ] }, { @@ -931,10 +984,10 @@ "id": "e1c08c5c", "metadata": { "papermill": { - "duration": 0.002509, - "end_time": "2026-10-14T22:36:35.204142+00:00", + "duration": 0.00484, + "end_time": "2026-10-20T10:31:50.126891+00:00", "exception": false, - "start_time": "2026-10-14T22:36:35.201633+00:00", + "start_time": "2026-10-20T10:31:50.122051+00:00", "status": "completed" }, "tags": [] @@ -956,16 +1009,16 @@ "id": "63a43cd7", "metadata": { "execution": { - "iopub.execute_input": "2026-10-14T22:36:35.210082Z", - "iopub.status.busy": "2026-10-14T22:36:35.209898Z", - "iopub.status.idle": "2026-10-14T22:36:35.215897Z", - "shell.execute_reply": "2026-10-14T22:36:35.214859Z" + "iopub.execute_input": "2026-10-20T10:31:50.138383Z", + "iopub.status.busy": "2026-10-20T10:31:50.137737Z", + "iopub.status.idle": "2026-10-20T10:31:50.146656Z", + "shell.execute_reply": "2026-10-20T10:31:50.145411Z" }, "papermill": { - "duration": 0.010485, - "end_time": "2026-10-14T22:36:35.216907+00:00", + "duration": 0.016638, + "end_time": "2026-10-20T10:31:50.148247+00:00", "exception": false, - "start_time": "2026-10-14T22:36:35.206422+00:00", + "start_time": "2026-10-20T10:31:50.131609+00:00", "status": "completed" }, "tags": [] @@ -1031,10 +1084,10 @@ "id": "3991f3c2", "metadata": { "papermill": { - "duration": 0.002189, - "end_time": "2026-10-14T22:36:35.221300+00:00", + "duration": 0.004698, + "end_time": "2026-10-20T10:31:50.157096+00:00", "exception": false, - "start_time": "2026-10-14T22:36:35.219111+00:00", + "start_time": "2026-10-20T10:31:50.152398+00:00", "status": "completed" }, "tags": [] @@ -1042,17 +1095,17 @@ "source": [ "## Conclusion\n", "\n", - "Ce notebook compagnon de GT-13 illustre la **frontière théorie/pratique** du CFR optimiste :\n", + "Ce notebook compagnon de GT-13 illustre la **frontière théorie/pratique** du CFR optimiste. Les valeurs ci-dessous sont celles du benchmark du §1.5 (Kuhn, 1000 itérations complètes, exploitabilité par meilleure réponse) :\n", "\n", - "| Solveur | Garantie regret | Empirique Kuhn (200 iter) |\n", - "|---|---|---|\n", - "| CFR | $\\mathcal{O}(\\sqrt{T})$ | Lent |\n", - "| CFR+ | $\\mathcal{O}(\\sqrt{T})$, constantes réduites | Moyen |\n", - "| DCFR | $\\mathcal{O}(\\sqrt{T})$ empiriquement | **Rapide** |\n", - "| OFTRL théorique | $\\mathcal{O}(T^{-3/4})$ | Lent sur petit budget |\n", - "| OFTRL réglé empiriquement | (idem) | Comparable à DCFR |\n", + "| Solveur | Garantie regret | Exploitabilité mesurée | Lecture |\n", + "|---|---|---|---|\n", + "| CFR | $\\mathcal{O}(\\sqrt{T})$ | 0,0065 | la plus basse à ce budget |\n", + "| CFR+ | $\\mathcal{O}(\\sqrt{T})$, constantes réduites | 0,0068 | à égalité avec CFR, à la résolution du budget |\n", + "| DCFR (écretage CFR+) | $\\mathcal{O}(\\sqrt{T})$ empiriquement | 0,0072 | à égalité avec les deux précédents |\n", + "| DCFR (écretage sur l'incrément) | — | 0,0717 | une ligne de différence, un facteur 10 |\n", + "| OFTRL | $\\mathcal{O}(T^{-3/4})$ | 0,0096 | dernier à ce budget |\n", "\n", - "**Leçon** : la supériorité théorique de l'OFTRL sur grand treeplex (Libratus-scale) ne se traduit pas automatiquement en supériorité empirique sur petit jeu (Kuhn). La nuance est l'intérêt pédagogique de ce notebook.\n", + "**Leçon** : la supériorité théorique de l'OFTRL sur grand treeplex (Libratus-scale) ne se traduit pas automatiquement en supériorité empirique sur petit jeu (Kuhn) — et, à cette échelle, **les détails d'implémentation d'un opérateur de regret pèsent plus lourd que le choix de la famille d'algorithme** : la forme de l'écretage déplace le DCFR d'un ordre de grandeur, là où passer de CFR à OFTRL ne déplace la mesure que de 50 %. C'est la nuance pédagogique de ce carnet.\n", "\n", "**Sources complémentaires** :\n", "- Farina, Kroer, Brown, Sandholm, *Stable-Predictive Optimistic Counterfactual Regret Minimization*, PMLR 97 (2019)\n", @@ -1079,18 +1132,18 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.16" + "version": "3.13.3" }, "papermill": { "default_parameters": {}, - "duration": 12.813407, - "end_time": "2026-10-14T22:36:35.440851+00:00", + "duration": 23.649337, + "end_time": "2026-10-20T10:31:50.504177+00:00", "environment_variables": {}, "exception": null, "input_path": "GameTheory-13d-Optimistic-CFR-Python.ipynb", "output_path": "GameTheory-13d-Optimistic-CFR-Python.ipynb", "parameters": {}, - "start_time": "2026-10-14T22:36:22.627444+00:00", + "start_time": "2026-10-20T10:31:26.854840+00:00", "version": "2.6.0" } },