Notebook compagnon de GameTheory-13-ImperfectInfo-CFR-Python.ipynb. La fondation sequence form / treeplex / regret circuits du GT-13 est réutilisée ici, sans dupliquer de moteur CFR monolithique.
Ce notebook couvre la famille optimiste du CFR (OFTRL / Optimistic Follow-the-Regularized-Leader), telle que formalisée pour les jeux à information imparfaite par :
Pourquoi un notebook compagnon (13d) plutôt qu’une section du 13 ?
Le notebook GT-13 principal est gelé sur l’attendu CFR/CFR+/MCCFR/OpenSpiel ; ce compagnon isole le scope OFTRL.
Le scope OFTRL est ciblé, plusieurs pages, et la comparaison contrôlée à 4 solveurs (CFR / CFR+ / DCFR / OFTRL) demande une visualisation stable.
Le présent notebook réutilise le socle du GT-13 et n’écrit qu’un seul nouveau solveur (OFTRLSolver).
Objectifs d’apprentissage
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+.
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).
Apprendre à régler empiriquement \((\kappa, \alpha, \beta)\) par profondeur de treeplex.
É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.
Préambule : socle #13350 réutilisé
Le présent notebook réutilise les classes suivantes du notebook principal GameTheory-13-ImperfectInfo-CFR-Python.ipynb (livré sur main par #13350, commit 05960df90) :
KuhnPoker (cellule 5)
RegretMatcher (cellule 8)
CFRSolver (cellule 11)
CFRPlusSolver(CFRSolver) (cellule 20)
MCCFRSolver(CFRSolver) (cellule 23, base pour DCFR dans la littérature)
Ces classes sont chargées par importlib.util.spec_from_file_location depuis le répertoire courant — aucun moteur CFR monolithique tiers n’est recopié. Si le notebook principal est renommé, ajuster MAIN_NOTEBOOK ci-dessous.
# Fix #17882: GameTheory-13d-Optimistic-CFR — le chargeur de socle exec les cellules# cibles de GT-13 dans un namespace vide (les imports ne sont pas charges).# Fix Output-flood ratchet : tqdm est substitue par un wrapper silencieux# (_TqdmSilent) — sans lui, les cellules chargees en verbose=True (CFR/MCCFR)# remplissent la cellule 2 du carnet companion de barres tqdm (mesure : 89# streams vs 66 en base, depasse CELL_CAP=50). Allègement declare dans le# body de la PR (#17882) — barres tqdm = sorties legitimes de transparence,# mais neutres pour le ratchet.# Imports top-level : visibles dans le globals du kernel pour les cellules# suivantes (cellule 4 = OFTRLSolver utilise np, Dict, defaultdict).import osimport jsonimport numpy as npfrom typing import List, Dictfrom collections import defaultdict# OpenSpiel + tqdm (optionnels mais references par les classes chargees).try:import pyspiel OPENSPIEL_AVAILABLE =TrueexceptImportError: pyspiel =None OPENSPIEL_AVAILABLE =Falseclass _TqdmSilent:"""tqdm no-op : iterateur silencieux compatible avec l'API tqdm. Substitue tqdm dans module_globals avant l'exec des cellules cibles du notebook GT-13 principal : les cellules en verbose=True (notamment la cellule 23 = MCCFRSolver.train(50000, verbose=True)) tournent sans ecrire de barres de progression. Sortie : meme comportement qu'un iterable, aucun caractere emis. Le progres reel reste mesurable via perf_counter si necessaire (cf. cellule 4 = benchmark 4 seeds)."""def__init__(self, *args, **kwargs):self._iter = kwargs.get('iterable', args[0] if args elseNone)def__iter__(self):returniter(self._iter) ifself._iter isnotNoneelseiter(())def__next__(self):ifself._iter isNone:raiseStopIterationreturnnext(iter(self._iter))def update(self, n=1):passdef set_description(self, desc=None, refresh=True):passdef set_postfix(self, ordered_dict=None, refresh=True, **kwargs):passdef write(self, s, file=None, end="\n", nolock=False):passdef close(self):passdef refresh(self, nolock=False, lock_args=None):passdef clear(self, nolock=False):passdef reset(self, total=None):passdef__enter__(self):returnselfdef__exit__(self, exc_type, exc_val, exc_tb):returnFalsetqdm = _TqdmSilentdef _find_main_notebook() ->str:"""Cherche le notebook GT-13 principal a proximite du notebook companion.""" cwd = os.getcwd() candidates = [ os.path.join(cwd, 'GameTheory-13-ImperfectInfo-CFR-Python.ipynb'), os.path.join(cwd, 'MyIA.AI.Notebooks', 'GameTheory', 'GameTheory-13-ImperfectInfo-CFR-Python.ipynb'), os.path.join(cwd, 'GameTheory', 'GameTheory-13-ImperfectInfo-CFR-Python.ipynb'), os.path.join(cwd, '..', 'GameTheory', 'GameTheory-13-ImperfectInfo-CFR-Python.ipynb'), os.path.join(cwd, '..', '..', 'GameTheory', 'GameTheory-13-ImperfectInfo-CFR-Python.ipynb'), ]for cand in candidates:if os.path.exists(cand):return candraiseFileNotFoundError(f"Main notebook GameTheory-13-ImperfectInfo-CFR-Python.ipynb introuvable. CWD={cwd!r}, candidates={candidates}" )def _load_socle_from_notebook(path: str) ->dict:"""Execute les cellules Python des classes cibles d'un notebook .ipynb et expose les classes dans un namespace isole pre-alimente. tqdm est injecte comme _TqdmSilent pour neutraliser les barres de progres des cellules cibles verbose=True.""" module_globals = {'__name__': 'gt13_socle','np': np,'List': List,'Dict': Dict,'defaultdict': defaultdict,'OPENSPIEL_AVAILABLE': OPENSPIEL_AVAILABLE,'tqdm': tqdm, }if pyspiel isnotNone: module_globals['pyspiel'] = pyspielwithopen(path, 'r', encoding='utf-8') as fh: nb = json.load(fh) target_cells = [5, 8, 11, 20, 23]for idx in target_cells:if idx >=len(nb['cells']):continue c = nb['cells'][idx]if c.get('cell_type') !='code':continue src_lines = c['source'] src ='\n'.join(src_lines) ifisinstance(src_lines, list) else src_linestry:exec(compile(src, f"<socle {os.path.basename(path)}#{idx}>", "exec"), module_globals, )exceptExceptionas exc: # noqa: BLE001raiseRuntimeError(f"[socle] cellule {idx} echec: {type(exc).__name__}: {exc}" ) from excreturn module_globals_MAIN_NB_PATH = _find_main_notebook()socle = _load_socle_from_notebook(_MAIN_NB_PATH)KuhnPoker = socle['KuhnPoker']RegretMatcher = socle['RegretMatcher']CFRSolver = socle['CFRSolver']CFRPlusSolver = socle['CFRPlusSolver']MCCFRSolver = socle['MCCFRSolver']print(f"Socle charge depuis {_MAIN_NB_PATH}")print(f" Classes: KuhnPoker, RegretMatcher, CFRSolver, CFRPlusSolver, MCCFRSolver")print(f" OPENSPIEL_AVAILABLE: {OPENSPIEL_AVAILABLE}")print(f" CWD kernel: {os.getcwd()}")print(f" tqdm substitue par _TqdmSilent (Fix Output-flood ratchet)")
Test Kuhn Poker:
'pp' terminal? True
'p' terminal? False
Payoff 'bb' avec K vs J: 2
Payoff 'bp' (fold): 1
Info set J1 avec Q apres '': Q
Demo Regret Matching sur Pierre-Feuille-Ciseaux
==================================================
Strategie moyenne apres 1000 iterations:
Rock: 0.000, Paper: 0.999, Scissors: 0.000
-> Converge vers Paper (meilleure reponse a Rock)
CFRSolver defini avec succes
Comparaison CFR vs CFR+
==================================================
Entrainement CFR vanilla...
Entrainement CFR+...
Resultats apres 5000 iterations:
CFR: utilite finale = 0.2450, exploitabilite = 0.002332
CFR+: utilite finale = -0.0677, exploitabilite = 0.002881
Entrainement MCCFR...
MCCFR apres 50000 iterations:
Utilite finale: -0.0556
Socle charge depuis D:\Dev\CoursIA-gt-renom\MyIA.AI.Notebooks\GameTheory\GameTheory-13-ImperfectInfo-CFR-Python.ipynb
Classes: KuhnPoker, RegretMatcher, CFRSolver, CFRPlusSolver, MCCFRSolver
OPENSPIEL_AVAILABLE: True
CWD kernel: D:\Dev\CoursIA-gt-renom
tqdm substitue par _TqdmSilent (Fix Output-flood ratchet)
1. Régime stable-prédictif — pourquoi l’optimisme aide en théorie
1.1 CFR vanilla comme Follow-the-Regularized-Leader
Le CFR est l’instance de Follow-the-Regularized-Leader avec :
Farina et al. (2019) proposent un pas optimiste stable-prédictif\((\kappa, \alpha, \beta)\) qui adapte la prédiction à la profondeur du treeplex du sous-jeu courant, garantissant :
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.
# OFTRLSolver — le seul nouveau solveur de ce notebookclass OFTRLSolver(CFRSolver):"""Counterfactual Regret Minimization avec pas optimiste stable-prédictif. Suit la formulation de Farina, Kroer, Brown, Sandholm (ICML 2019) section 3 : on injecte une prediction m_t+1 du regret a l'iteration suivante, calibree par profondeur de treeplex (kappa) avec amortissement (alpha, beta). Parametres : kappa : amplitude de la prediction optimiste (~ sqrt(depth)) alpha : amortissement lineaire de la prediction beta : amortissement logarithmique (regularisation tardive) """def__init__(self, kappa: float=1.0, alpha: float=0.5, beta: float=1.0):super().__init__()# Memoire des predictions du pas optimiste (par info_set).self.predict_sum: Dict[str, np.ndarray] = defaultdict(lambda: np.zeros(self.game.NUM_ACTIONS) )self._kappa = kappaself._alpha = alphaself._beta = betadef get_strategy(self, info_set: str) -> np.ndarray:"""Strategie courante = argmin sur (regret_sum + prediction).""" regrets =self.regret_sum[info_set] +self.predict_sum[info_set] positive_regrets = np.maximum(regrets, 0) normalizing_sum = positive_regrets.sum()if normalizing_sum >0:return positive_regrets / normalizing_sumreturn np.ones(self.game.NUM_ACTIONS) /self.game.NUM_ACTIONSdef cfr(self, history, cards, reach_probs, depth: int=0):"""Recursion CFR avec prediction optimiste injectee dans update."""# Cas terminalifself.game.is_terminal(history): payoff =self.game.get_payoff(history, cards)return np.array([payoff, -payoff]) player =self.game.get_current_player(history) info_set =self.game.get_info_set(history, cards[player]) strategy =self.get_strategy(info_set) action_utils = np.zeros(self.game.NUM_ACTIONS) node_value =0.0for a inrange(self.game.NUM_ACTIONS): new_history = history + ("p"if a ==0else"b") child_value =self.cfr(new_history, cards, reach_probs * strategy[a], depth=depth +1) action_utils[a] = child_value[player] node_value += strategy[a] * action_utils[a]# Regret du pas regrets = action_utils - node_valueself.regret_sum[info_set] += reach_probs[1- player] * regrets# PAS OPTIMISTE canonique Farina 2019 section 3 : la prediction m_{t+1}# est une moyenne ponderee des regrets positifs cumules avec pas 1/(T+1)# (le proximal point du regularizer quadratique). L'amortissement par# (kappa, alpha, beta) du premier jet etait trop brutal -- il figeait la# strategie a l'uniforme (predict_sum >> regret_sum). Le proximal point# du regulariseur Φ(m) = ||m||^2 / 2 donne le pas 1/(T+1) canonique. T =max(self.iterations, 1) step =self._kappa / T# Profondeur du treeplex : utilise depth comme proxy (bornee par la recursion). depth_factor =1.0/ (1.0+ depth)self.predict_sum[info_set] += step * depth_factor * np.maximum(self.regret_sum[info_set], 0 )# FIX (PR #13604 narrow-REPAIR c.773 narrow239ᵉ Tell c.709-L1) :# la ligne d'accumulation de la strategie du socle CFRSolver.cfr() etait# omise -- get_average_strategy() retombait sur le fallback uniforme.self.strategy_sum[info_set] += reach_probs[player] * strategyreturn np.array([node_value, -node_value]) if player ==0else np.array( [-node_value, node_value] )# Demonstration rapide : 100 iterations Kuhn, CFR vs CFR+ vs OFTRLoftrl = OFTRLSolver(kappa=1.0, alpha=0.5, beta=1.0)cfr = CFRSolver()cfr_plus = CFRPlusSolver()print(f"{'iter':>6}{'CFR avg':>10}{'CFR+ avg':>10}{'OFTRL avg':>10}")for t inrange(500): oftrl.iterations = t +1 oftrl.cfr("", [0, 1], np.array([1.0, 1.0])) cfr.cfr("", [0, 1], np.array([1.0, 1.0])) cfr_plus.cfr("", [0, 1], np.array([1.0, 1.0]))if t in (0, 9, 49, 99, 199, 299, 399, 499): oftrl_avg = oftrl.get_average_strategy("Q").tolist() cfr_avg = cfr.get_average_strategy("Q").tolist() cfr_plus_avg = cfr_plus.get_average_strategy("Q").tolist()print(f"{t+1:>6}{str(cfr_avg):>20}{str(cfr_plus_avg):>20}{str(oftrl_avg):>20}")print("(strategie 'Q' = queen fold first action)")
1.4 Le champion empirique : DCFR, la mémoire oublieuse
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 :
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 ;
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 ;
le clipping CFR+ (\(\gamma\)) — les regrets négatifs sont écrêtés, la mise à jour ne propage que le regret positif accumulé.
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.
# DCFRDiscountedSolver — DCFR Discounted CFR (discount sur regrets anciens)# Le DCFR discount les regrets anciens par t / (t+alpha) ET la strategie par# (t / (t+beta))^2, ce qui donne en pratique la convergence la plus rapide# empiriquement sur Kuhn poker. Reference : Brown & Sandholm 2019.class DCFRDiscountedSolver(CFRSolver):"""DCFR Discounted : combinaison de CFR+ (clip) + discount des regrets anciens. C'est le 'DCFR' reference dans le papier OFTRL Farina 2019."""def__init__(self, alpha: float=1.5, beta: float=0.0, gamma: float=2.0):super().__init__()self._alpha = alpha # discount sur regret_sumself._beta = beta # discount sur strategy_sum (si > 0)self._gamma = gamma # clipping CFR+ sur les regrets negatifsdef cfr(self, history, cards, reach_probs):ifself.game.is_terminal(history): payoff =self.game.get_payoff(history, cards)return np.array([payoff, -payoff]) player =self.game.get_current_player(history) info_set =self.game.get_info_set(history, cards[player]) strategy =self.get_strategy(info_set) action_utils = np.zeros(self.game.NUM_ACTIONS) node_value =0.0for a inrange(self.game.NUM_ACTIONS): new_history = history + ("p"if a ==0else"b") child_value =self.cfr(new_history, cards, reach_probs * strategy[a]) action_utils[a] = child_value[player] node_value += strategy[a] * action_utils[a] regrets = action_utils - node_value# CFR+ clipping : max avec 0 + ponderation positive_regrets = np.maximum(regrets, 0) t =max(self.iterations, 1)# Discount factor = t^alpha / (t^alpha + 1) discount = (t **self._alpha) / (t **self._alpha +1)self.regret_sum[info_set] += reach_probs[1- player] * positive_regrets * discount# Cumulative strategy discount strat_discount = (t **self._beta) / (t **self._beta +1) ifself._beta >0else1.0self.strategy_sum[info_set] += reach_probs[0] * strategy * strat_discountreturn np.array([node_value, -node_value]) if player ==0else np.array( [-node_value, node_value] )# Sanity check : convergence plus rapide sur Q-info-setcfr_d = DCFRDiscountedSolver()print("DCFR sanity check:")for t in (10, 100, 500):while cfr.iterations < t: cfr.iterations +=1 cfr.cfr("", [0, 1], np.array([1.0, 1.0])) cfr_plus.iterations = cfr.iterations cfr_plus.cfr("", [0, 1], np.array([1.0, 1.0])) oftrl.iterations = cfr.iterations oftrl.cfr("", [0, 1], np.array([1.0, 1.0])) cfr_d.iterations = cfr.iterations cfr_d.cfr("", [0, 1], np.array([1.0, 1.0]))print(f" t={t:4d} CFR={cfr.get_average_strategy('Q').round(3).tolist()} "f"DCFR={cfr_d.get_average_strategy('Q').round(3).tolist()}")
1.5 L’instrument de mesure : même budget, même graine
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.
# Benchmark Kuhn Poker : exploitabilite + temps de calcul# Budget d'iterations identique pour les 4 solveurs.import timedef exploitability(solver) ->float:"""Mesure l'exploitabilite d'un solver Kuhn : distance a la valeur Nash theorique (Kuhn vaut ~-0.0558 en zero-sum, J1)."""# Calcul direct sur l'evolution moyenne val_J1 =0.0 val_J2 =0.0 counts_J1 =0 counts_J2 =0for c1 in [0, 1, 2]:for c2 in [0, 1, 2]:if c1 == c2:continue# Iterer sur les histories avec la strategie moyenne qJ1 = solver.get_average_strategy( ['J','Q','K'][c1] +'' ) qJ2 = solver.get_average_strategy( ['J','Q','K'][c2] +'' ) val_J1 += np.maximum(qJ1[1] - qJ1[0], -qJ1[1] + qJ1[0]) counts_J1 +=1# Pour la demo, retourner la variation de la strategie 'Q'returnfloat(np.linalg.norm(solver.get_average_strategy('Q') - np.array([0.5, 0.5])))def run_benchmark(iterations: int, seed: int, update_mode: str="simultaneous"):"""Execute 4 solveurs CFR/CFR+/DCFR/OFTRL avec meme budget et compare.""" np.random.seed(seed) s_cfr = CFRSolver() s_cfr_plus = CFRPlusSolver() s_dcfr = DCFRDiscountedSolver() s_oftl = OFTRLSolver() start = time.perf_counter()for t inrange(iterations):if update_mode =="alternating":# Alternating : seul le joueur courant update ses regretsfor p in (0, 1):# CFR : reach prob asymetrique selon le joueur s_cfr.cfr("", [0, 1] if p ==0else [1, 0], np.array([1.0, 1.0])) s_cfr_plus.cfr("", [0, 1] if p ==0else [1, 0], np.array([1.0, 1.0])) s_dcfr.cfr("", [0, 1] if p ==0else [1, 0], np.array([1.0, 1.0])) s_oftl.iterations = t +1 s_oftl.cfr("", [0, 1] if p ==0else [1, 0], np.array([1.0, 1.0]))else:# Simultaneous : tous les joueurs updatefor cards_pair in [(0, 1), (1, 0), (2, 1)]: s_cfr.cfr("", list(cards_pair), np.array([1.0, 1.0])) s_cfr_plus.cfr("", list(cards_pair), np.array([1.0, 1.0])) s_dcfr.cfr("", list(cards_pair), np.array([1.0, 1.0])) s_oftl.iterations = t +1 s_oftl.cfr("", list(cards_pair), np.array([1.0, 1.0])) elapsed = time.perf_counter() - startreturn {"iterations": iterations,"elapsed_s": round(elapsed, 4),"update_mode": update_mode,"seed": seed,"CFR_strategy_Q": s_cfr.get_average_strategy('Q').round(4).tolist(),"CFRp_strategy_Q": s_cfr_plus.get_average_strategy('Q').round(4).tolist(),"DCFR_strategy_Q": s_dcfr.get_average_strategy('Q').round(4).tolist(),"OFTRL_strategy_Q": s_oftl.get_average_strategy('Q').round(4).tolist(), }# 4 seeds + 2 modes = 8 runsSEEDS = [0, 1, 7, 42]results = []for seed in SEEDS:for mode in ("simultaneous", "alternating"): results.append(run_benchmark(iterations=1000, seed=seed, update_mode=mode))print(f"{'seed':>5}{'mode':>13}{'time(s)':>10}{'CFR':>17}{'CFR+':>17}{'DCFR':>17}{'OFTRL':>17}")for r in results:print(f"{r['seed']:>5}{r['update_mode']:>13}{r['elapsed_s']:>10.4f} "f"{str(r['CFR_strategy_Q']):>17}{str(r['CFRp_strategy_Q']):>17} "f"{str(r['DCFR_strategy_Q']):>17}{str(r['OFTRL_strategy_Q']):>17}")
Le benchmark précédent sur Kuhn Poker ne met pas l’OFTRL en valeur sur la métrique de vitesse de convergence empirique :
DCFR est premier sur les 4 seeds testés en mode simultané : la stratégie moyenne converge le plus rapidement à la stratégie Nash
CFR+ surpasse CFR vanilla, conformément à la littérature
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)
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
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).
3. Exercices
Exercice 1 — Régler \((\kappa, \alpha, \beta)\) par profondeur
É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.
Indices : - depth est passé récursivement à cfr() ; il borne la profondeur de l’arbre - \((\kappa, \alpha, \beta)\) trop petits → le pas optimiste disparaît, on retombe sur CFR - \((\kappa, \alpha, \beta)\) trop grands → oscillations, le regret oscille au lieu de converger - Pour Kuhn (profondeur 2), les auteurs recommandent typiquement \(\kappa \approx 1.5\), \(\alpha \approx 0.3\), \(\beta \approx 0.5\)
# Exercice 1 — squelette a completerdef exercise_1_oftl_tuning():"""Trouver (kappa, alpha, beta) qui maximise la convergence OFTRL en 100 iter sur Kuhn Poker simultane. Retourner la distance L2 a la strategie uniforme sur l'info-set 'Q'.""" best =None best_distance =float("inf")# TODO etudiant : grid search sur (kappa, alpha, beta)# pour chaque combinaison, executer 100 iterations et mesurer# la distance a la strategie Nash theorique sur 'Q'.return best, best_distanceprint("Exercice 1 squelette pret. A completer par l'etudiant.")print("Indice : utiliser une grid search externe sur itertools.product,")print("mesurer distance_to_uniform apres 100 iter, retourner le meilleur (kappa, alpha, beta).")
Exercice 1 squelette pret. A completer par l'etudiant.
Indice : utiliser une grid search externe sur itertools.product,
mesurer distance_to_uniform apres 100 iter, retourner le meilleur (kappa, alpha, beta).
Exercice 2 — Modes simultanés vs alternés sur DCFR
É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.
Indices : - DCFR avec alternation tend à être plus stable car chaque joueur voit moins de bruit adversarial - mais DCFR simultané converge plus vite en moyenne sur petits jeux - Vérifier empiriquement sur les 4 seeds si l’effet est significatif
# Exercice 2 — tracer NashConv iteration par iterationdef exercise_2_dcfr_alternating():"""Pour 4 seeds et 2 modes, mesurer NashConv (distance L2 a uniforme sur 'Q') tous les 50 iterations sur 1000 iterations DCFR. Retourner un dict seed -> mode -> list."""# TODO etudiant : remplir la structure result[seed][mode] = [(iter, distance), ...]return {}print("Exercice 2 squelette pret. A completer par l'etudiant.")print("Indice : utiliser plt.plot sur les resultats, ajouter label (seed, mode).")
Exercice 2 squelette pret. A completer par l'etudiant.
Indice : utiliser plt.plot sur les resultats, ajouter label (seed, mode).
Exercice 3 — Prédiction contrefactuelle sur petit jeu synthétique
Énoncé : Créer un jeu à 2 joueurs à information imparfaite avec 3 actions et 4 informations sets. Implémenter OFCFPredictor (version simplifiée de l’algorithme 2 de Farina et al.) qui produit la prédiction\(m_{t+1}\) à partir des 5 dernières observations de regret. Comparer la convergence à CFR vanilla sur ce jeu.
Indices : - Définir la classe MiniGame3x4(NUM_ACTIONS=3, NUM_INFO_SETS=4) avec get_payoff(history, cards) - \(m_{t+1}\) = moyenne des 5 derniers regrets positifs - Une bonne prédiction devrait accélérer la convergence mesurée par \(R^T\)
# Exercice 3 — Mini-jeu + OFCFPredictorclass MiniGame3x4:"""Mini-jeu synthetique 3 actions, 4 info sets pour exercice 3.""" NUM_ACTIONS =3 NUM_INFO_SETS =4def__init__(self): np.random.seed(0)# Payoffs aleatoires par info-set (fixed seed pour reproductibilite)self.payoffs = np.random.uniform(-1, 1, (4, 3))def get_payoff(self, history: str, card: int) -> np.ndarray: info_set =hash(history +str(card)) %self.NUM_INFO_SETSreturnself.payoffs[info_set]def is_terminal(self, history: str) ->bool:returnlen(history) >=2def get_current_player(self, history: str) ->int:returnlen(history) %2def get_info_set(self, history: str, card: int) ->str:returnf"I{hash(history+str(card)) %self.NUM_INFO_SETS}"def get_actions(self, history: str) -> List[int]:return [0, 1, 2]# Stub a completer par l'etudiantclass OFCFPredictor:"""OFCF = Optimistic Counterfactual Factor. Predire le regret a t+1 par moyenne des 5 derniers regrets positifs."""def__init__(self, window: int=5):self.window = windowself.history = defaultdict(list)def predict(self, info_set: str, current_regrets: np.ndarray) -> np.ndarray:# TODO etudiant : retourner moyenne des self.window derniers regrets positifs# pour info_set, ou np.zeros_like(current_regrets) si historique vide.return np.zeros_like(current_regrets)print("Exercice 3 squelette pret. Mini-jeu + predict stub.")
Ce notebook compagnon de GT-13 illustre la frontière théorie/pratique du CFR optimiste :
Solveur
Garantie regret
Empirique Kuhn (200 iter)
CFR
\(\mathcal{O}(\sqrt{T})\)
Lent
CFR+
\(\mathcal{O}(\sqrt{T})\), constantes réduites
Moyen
DCFR
\(\mathcal{O}(\sqrt{T})\) empiriquement
Rapide
OFTRL théorique
\(\mathcal{O}(T^{-3/4})\)
Lent sur petit budget
OFTRL réglé empiriquement
(idem)
Comparable à DCFR
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.