Oversight-Scaling-Laws-Wargames

Simulation 3 roles - Engels et al. R12 Section 5

Ce notebook reproduit le scenario Wargames de R12 Section 5 (Engels et al, NeurIPS 2025, arXiv:2504.18530) en simulation stochastique pure, sans LLM externe.

Le protocole 3 roles :

  • Defender : detient un ‘secret’ parmi K options (typiquement K = 10-100). Strategie : repondre aux questions de l’Attacker de maniere coherente mais evasive (force de defense d_strength ∈ [0,1]).
  • Attacker (Houdini dans R12) : interroge le Defender pour extraire le secret. Strategie : questions informees par un modele bayesien des reponses observees.
  • Judge (Guard dans R12) : evalue la performance d’Attacker sur un echantillon de N episodes. Peut se tromper avec un bruit judge_noise ∈ [0, 0.5].

Mesures :

  • Taux de reussite Attacker (p_attacker) en fonction de K, d_strength, n_questions.
  • Ecart Judge vs realite : p_judged contre p_real sur le meme balayage (l’ecart-type inter-episodes n’est pas mesure).
  • Demi-vie de controle : nombre de questions necessaires pour atteindre 50% de reussite.
  • Courbe NSO : extension de PR 2 au cas Wargames.

Source : R12 Section 5. PDF archive hors depot : G:\Mon Drive\MyIA\IA\Bibliographie IA\XAI\2025 - Engels et al - Scaling Laws For Scalable Oversight.pdf (sha8 FDA29C9A).

Auto-contenu : pas de GPU, pas de LLM externe. Simulation pure, 100% reproductible (seed = 42).

import numpy as np
rng = np.random.default_rng(42)
print("Setup OK - simulation Wargames R12 §5, numpy uniquement")
Setup OK - simulation Wargames R12 §5, numpy uniquement

1. Protocole Wargames - specification

On formalise le scenario 3 roles :

  1. Le Defender choisit un secret s ∈ {0, 1, ..., K-1} aleatoirement.
  2. Pour chaque question de l’Attacker (jusqu’a n_questions) :
    • L’Attacker propose une question (binaire : ‘est-ce que s ∈ S ?’ pour un sous-ensemble S).
    • Le Defender repond True/False avec une probabilite 1 - d_strength d’etre correct (force de defense : d_strength = 1 = defense parfaite, mente systematiquement ; d_strength = 0 = revele toujours la verite).
  3. Apres les questions, l’Attacker propose sa meilleure estimation du secret.
  4. Le Judge evalue si l’estimation est correcte, avec un bruit judge_noise (le Judge peut se tromper avec cette probabilite).

Cas degeneres :

  • d_strength = 0 (Defender transparent) : Attacker reussit des la 1ere question.
  • K = 2 (binaire) : Attacker reussit en log2(K) questions en moyenne.
  • judge_noise = 0 : le verdict est deterministe (utile pour calibration).
class Defender:
    """Defender avec force de defense d_strength (1 = parfait menteur, 0 = transparent)."""
    def __init__(self, secret, K, d_strength, rng):
        self.secret = secret
        self.K = K
        self.d_strength = d_strength
        self.rng = rng

    def answer(self, subset):
        """subset est un set d'indices candidats. Retourne True si secret ∈ subset."""
        truth = self.secret in subset
        if self.rng.random() < self.d_strength:
            return not truth  # mente
        return truth

class BayesianAttacker:
    """Attacker bayesien : maintient une posterior sur le secret, met a jour a chaque reponse."""
    def __init__(self, K, rng):
        self.K = K
        self.rng = rng
        # posterior uniforme au depart
        self.posterior = np.ones(K) / K

    def reset(self):
        self.posterior = np.ones(self.K) / self.K

    def best_guess(self):
        """Renvoie l'indice avec la posterior la plus haute."""
        return int(np.argmax(self.posterior))

    def update(self, subset, answer):
        """Met a jour la posterior selon la formule de Bayes."""
        # P(subset | s ∈ subset ∨ s ∉ subset) depends on d_strength mais on simplifie :
        # on suppose d_strength = 0 (Defender transparent) pour la version de base
        # version sophistiquee : on apprendrait aussi le d_strength
        likelihood = np.zeros(self.K)
        for s in range(self.K):
            likelihood[s] = 1.0 if ((s in subset) == answer) else 0.0
        # Si d_strength > 0, le likelihood s'inverse selon la proba de mentir
        # Version simplifiee : on suppose Defender transparent
        self.posterior *= likelihood
        if self.posterior.sum() == 0:
            self.posterior = np.ones(self.K) / self.K
        else:
            self.posterior /= self.posterior.sum()

    def ask_question(self, posterior):
        """Question binaire informee : coupe la posterior en 2 moities les plus equilibrees."""
        sorted_idx = np.argsort(posterior)[::-1]
        cumulative = 0
        half = posterior.sum() / 2
        split = 1
        for i, idx in enumerate(sorted_idx):
            cumulative += posterior[idx]
            if cumulative >= half:
                split = i + 1
                break
        return set(sorted_idx[:split].tolist())

print("Classes Defender et BayesianAttacker OK")
Classes Defender et BayesianAttacker OK
def run_episode(K, d_strength, n_questions, rng):
    """Execute un episode Wargames et renvoie (succes, n_questions_utilisees)."""
    secret = int(rng.integers(0, K))
    defender = Defender(secret, K, d_strength, rng)
    attacker = BayesianAttacker(K, rng)

    for q in range(n_questions):
        subset = attacker.ask_question(attacker.posterior)
        answer = defender.answer(subset)
        attacker.update(subset, answer)
        # Si la posterior est deja concentree, on peut s'arreter
        if attacker.posterior.max() > 0.99:
            break

    guess = attacker.best_guess()
    return guess == secret, q + 1

print("run_episode(K, d_strength, n_questions, rng) -> (succes, n_used)")
run_episode(K, d_strength, n_questions, rng) -> (succes, n_used)
class Judge:
    """Evaluateur final : decide si Attacker a identifie le secret, avec bruit judge_noise."""
    def __init__(self, judge_noise, rng):
        self.judge_noise = judge_noise
        self.rng = rng

    def evaluate(self, guess, secret):
        """Renvoie True si Judge declare succes, False sinon.
        judge_noise = 0 : deterministe (True ssi guess == secret).
        judge_noise > 0 : bruite le verdict."""
        truthful = (guess == secret)
        if self.rng.random() < self.judge_noise:
            return not truthful  # bruite
        return truthful


def run_episode_judged(K, d_strength, n_questions, judge_noise, rng):
    """Episode complet : Defender + Attacker + Judge (avec bruit)."""
    secret = int(rng.integers(0, K))
    defender = Defender(secret, K, d_strength, rng)
    attacker = BayesianAttacker(K, rng)
    judge = Judge(judge_noise, rng)

    for q in range(n_questions):
        subset = attacker.ask_question(attacker.posterior)
        answer = defender.answer(subset)
        attacker.update(subset, answer)
        if attacker.posterior.max() > 0.99:
            break

    guess = attacker.best_guess()
    real_success = (guess == secret)
    judged_success = judge.evaluate(guess, secret)
    return real_success, judged_success, q + 1


# Sweep sur n_questions (demi-vie de controle) avec judge fixe
print("Sweep demi-vie : K=10, d_strength=0.3, judge_noise=0.15, 500 episodes par point")
n_q_values = [1, 3, 5, 10, 15, 20]
results_half_life = {}
for n_q in n_q_values:
    episodes = [run_episode_judged(10, 0.3, n_q, 0.15, rng) for _ in range(500)]
    real_succ = sum(1 for r, j, _ in episodes if r)
    judged_succ = sum(1 for r, j, _ in episodes if j)
    avg_q = float(np.mean([q for _, _, q in episodes]))
    results_half_life[n_q] = {
        "p_real": real_succ / 500,
        "p_judged": judged_succ / 500,
        "avg_questions": avg_q,
    }

print()
header_fmt = "{:>4} {:>8} {:>10} {:>7}"
print(header_fmt.format("n_q", "p_real", "p_judged", "avg_Q"))
print("-" * 36)
for n_q in n_q_values:
    r = results_half_life[n_q]
    print(header_fmt.format(str(n_q), f"{r['p_real']:.3f}", f"{r['p_judged']:.3f}", f"{r['avg_questions']:.1f}"))
Sweep demi-vie : K=10, d_strength=0.3, judge_noise=0.15, 500 episodes par point

 n_q   p_real   p_judged   avg_Q
------------------------------------
   1    0.144      0.268     1.0
   3    0.282      0.366     3.0
   5    0.296      0.370     3.2
  10    0.314      0.342     3.2
  15    0.258      0.314     3.2
  20    0.298      0.346     3.2

Lecture du balayage demi-vie (K=10, d=0.3, judge_noise=0.15)

p_real grimpe de 0.144 (1 question) a un plateau ~0.3 des 3 questions (0.282 / 0.296 / 0.314) - jamais 0.5 : la demi-vie de controle n’est pas definie dans ce regime. avg_Q cale a 3.2 des n_q=5 : l’arret anticipe posterior.max() > 0.99 coupe l’interrogation.

Cote Judge : p_judged (0.268-0.370) surestime systematiquement p_real (0.144-0.314) - un bruit de judge a 0.15 ajoute ~0.1 de verdicts faux positifs, l’ecart est le signal de supervision, pas un artefact. L’ecart-type inter-episodes des evaluations Judge n’est pas mesure ici (une moyenne par point) - la promesse en est retiree de l’intro.

# Parametres du sweep
K_values = [4, 10, 25, 100]
d_strength_values = [0.0, 0.1, 0.3, 0.5, 0.8]
n_questions_max = 20
n_episodes = 500

print(f"Sweep {len(K_values)} K x {len(d_strength_values)} d_strength x {n_episodes} episodes")
print(f"Total : {len(K_values) * len(d_strength_values) * n_episodes} episodes simules")
print(f"Cible : {n_questions_max} questions max par episode")
Sweep 4 K x 5 d_strength x 500 episodes
Total : 10000 episodes simules
Cible : 20 questions max par episode
# Executer le sweep
results = {}
for K in K_values:
    for d in d_strength_values:
        episodes = [run_episode(K, d, n_questions_max, rng) for _ in range(n_episodes)]
        successes = sum(1 for s, _ in episodes if s)
        avg_questions = np.mean([q for _, q in episodes])
        results[(K, d)] = {
            "p_success": successes / n_episodes,
            "avg_questions": avg_questions,
            "n_success": successes,
            "n_episodes": n_episodes,
        }

# Affichage compact
print(f"\n{'K':>5} {'d_str':>6} {'p_succ':>8} {'avg_Q':>7} {'n_succ':>7}/{'n_eps':>6}")
print("-" * 50)
for K in K_values:
    for d in d_strength_values:
        r = results[(K, d)]
        print(f"{K:>5} {d:>6.2f} {r['p_success']:>8.3f} {r['avg_questions']:>7.1f} {r['n_success']:>7}/{r['n_episodes']:>6}")

    K  d_str   p_succ   avg_Q  n_succ/ n_eps
--------------------------------------------------
    4   0.00    1.000     2.0     500/   500
    4   0.10    0.806     2.0     403/   500
    4   0.30    0.490     2.0     245/   500
    4   0.50    0.226     2.0     113/   500
    4   0.80    0.038     2.0      19/   500
   10   0.00    1.000     3.4     500/   500
   10   0.10    0.718     3.3     359/   500
   10   0.30    0.302     3.2     151/   500
   10   0.50    0.102     3.3      51/   500
   10   0.80    0.010     3.6       5/   500
   25   0.00    1.000     4.7     500/   500
   25   0.10    0.592     4.6     296/   500
   25   0.30    0.170     4.4      85/   500
   25   0.50    0.038     4.5      19/   500
   25   0.80    0.002     4.8       1/   500
  100   0.00    1.000     6.7     500/   500
  100   0.10    0.476     6.4     238/   500
  100   0.30    0.094     6.4      47/   500
  100   0.50    0.008     6.6       4/   500
  100   0.80    0.000     6.9       0/   500

Lecture du resultat - regimes Wargames

Regimes observes :

  1. Defender transparent (d_strength = 0) : Attacker atteint ~100% en ~log2(K) questions (mesure : avg_Q = 2.0 / 3.4 / 4.7 / 6.7 pour K = 4 / 10 / 25 / 100 - recherche binaire avec arret anticipe des que le posterior culmine a > 0.99).
  2. Defense faible (d_strength = 0.1-0.3) : p_success decroit avec K et avec d_strength — a K=4 : 0.806 (d=0.1) et 0.490 (d=0.3) ; a K=100 : 0.476 et 0.094. Le regime reste au-dessus du hasard 1/K, mais seul d = 0.1 avec K <= 10 depasse 0.7 ; des K = 25 la reussite tombe a 0.592 (d=0.1) et 0.170 (d=0.3).
  3. Defense moyenne (d_strength = 0.5) : le Defender est essentiellement aleatoire. Attacker tombe a 1/K environ.
  4. Defense forte (d_strength = 0.8) : Defender presque parfait. Le posterior se concentre sur le candidat miroir que designe le mensonge systematique : a K=4, p = 0.038, sous le hasard 1/K = 0.25 - l’Attacker est activement trompe, pas seulement aveugle.

Demi-vie de controle : non atteinte dans ce regime. A K=10, d_strength=0.3, p_real plafonne a ~0.3 des 3 questions (0.282 a 0.314, cf. balayage) - l’Attacker ne franchit jamais 50%, et avg_Q cale a 3.2 (l’arret anticipe posterior.max() > 0.99 coupe l’interrogation avant que davantage de questions puissent aider).

2. Extension NSO au cas Wargames

PR 2 a montre que NSO close-form n* = log(q)/log(1 - D/400) decompose le regime Houdini-vs-Guard en Elo oversight-specifique. Wargames ajoute une dimension : la strategie defensive du Defender.

Hypothese : le gap Elo D dans Wargames depend de d_strength et K. Plus le Defender est fort, plus le gap est grand, plus le regime NSO degrade.

On approxime : D ≈ f(d_strength) = -400 * log10(K * (1 - d_strength) + d_strength) (calibration grossiere, le K multiplie l’ambiguite de la question).

Pour un Defender ‘parfait’ (d_strength = 1.0) : D = +0.0 quel que soit K (le terme K*(1-d) + d vaut 1, log10(1) = 0) - mesure dans la table ci-dessous. Sur toute la grille, D est negatif et < 400 : n* rend NaN partout, regime « Attacker dominant » - ce mapping heuristique ne produit aucun point d’equilibre NSO.

Pour un Defender ‘moyen’ (d_strength = 0.3, K = 10) : D ≈ -400 * log10(7.3) ≈ -345 Elo — meme regime « Attacker dominant » que le reste de la grille. La saturation NSO est a l’autre extremite (D → +400, cf PR 2), et le point le plus proche du domaine (D > 0) est le Defender parfait (d = 1.0), a D = +0.0 : exclu lui aussi, la borne etant stricte.

Pour un Defender ‘faible’ (d_strength = 0.0, K = 10) : D ≈ -400 * log10(10) ≈ -400 Elo (mais le Defender revele tout, donc le Guard n’a pas besoin de superviser).

Ce que cette table ne peut pas trancher : l’hypothese de l’intro (« plus le Defender est fort, plus le regime NSO degrade ») n’est pas observable ici — n* vaut NaN sur les 24 points de la grille, donc aucune valeur ne peut monter ni descendre. Ce qui est mesure est plus etroit : D_approx reste du cote « Attacker dominant » partout, et ne fournit aucun point d’entree au formalisme NSO.

def D_approx(d_strength, K):
    """Approximation du gap Elo Wargames selon d_strength et K.

    Convention R12 : D > 0 = Defender plus fort qu'Attacker, D < 0 = Attacker
    plus fort. Ici D est *negatif* car le Defender n'a pas d'avantage Elo
    intrinsque : on mesure l'ambiguite du secret K et la force de brouillage.

    Domaine : la close-form NSO `n* = log(q)/log(1 - D/400)` n'a un sens que
    pour 0 < D < 400. Sur toute la grille mesuree plus bas, D reste <= 0 : cette
    approximation ne produit aucun point exploitable par le formalisme NSO, et
    elle ne mesure donc aucune « degradation » du regime NSO.
    """
    effective_K = K * (1 - d_strength) + d_strength
    if effective_K <= 1:
        return 0  # Defense triviale, pas de gap
    return -400 * np.log10(effective_K)


def n_star_wargames(d_strength, K, q):
    """n* pour Wargames. Renvoie NaN hors domaine (D <= 0 ou D >= 400).

    Convention R12 : D > 0 = Defender plus fort, n* > 0 = plusieurs niveaux.
    Pour D <= 0 la close-form rend un n* *negatif* (log(1 - D/400) > 0 alors que
    log(q) < 0), qui ne s'interprete pas comme un nombre de niveaux : on marque
    NaN plutot que de lui preter le sens « un seul niveau suffit ».
    """
    D = D_approx(d_strength, K)
    if D <= 0 or D >= 400:
        return float("nan")
    if q <= 0.5 or q >= 1.0:
        return float("nan")
    base = 1 - D / 400
    if base <= 0:
        return float("nan")
    val = np.log(q) / np.log(base)
    if not np.isfinite(val):
        return float("nan")
    return val


print(f"{'d_str':>6} {'K':>5} {'D':>7} {'n*(q=0.8)':>11} {'n*(q=0.95)':>12} {'regime':>20}")
print("-" * 65)
for d in [0.0, 0.1, 0.3, 0.5, 0.8, 1.0]:
    for K in [4, 10, 25, 100]:
        D = D_approx(d, K)
        n1 = n_star_wargames(d, K, 0.8)
        n2 = n_star_wargames(d, K, 0.95)
        if D <= 0:
            regime = "Attacker dominant"
        elif D >= 400:
            regime = "Defender sature"
        else:
            regime = "NSO exploitable"
        n1_str = f"{n1:>+11.3f}" if np.isfinite(n1) else "       NaN"
        n2_str = f"{n2:>+12.3f}" if np.isfinite(n2) else "        NaN"
        print(f"{d:>6.2f} {K:>5} {D:>+7.1f} {n1_str} {n2_str} {regime:>20}")
 d_str     K       D   n*(q=0.8)   n*(q=0.95)               regime
-----------------------------------------------------------------
  0.00     4  -240.8        NaN         NaN    Attacker dominant
  0.00    10  -400.0        NaN         NaN    Attacker dominant
  0.00    25  -559.2        NaN         NaN    Attacker dominant
  0.00   100  -800.0        NaN         NaN    Attacker dominant
  0.10     4  -227.3        NaN         NaN    Attacker dominant
  0.10    10  -383.6        NaN         NaN    Attacker dominant
  0.10    25  -541.6        NaN         NaN    Attacker dominant
  0.10   100  -781.9        NaN         NaN    Attacker dominant
  0.30     4  -196.5        NaN         NaN    Attacker dominant
  0.30    10  -345.3        NaN         NaN    Attacker dominant
  0.30    25  -500.2        NaN         NaN    Attacker dominant
  0.30   100  -738.8        NaN         NaN    Attacker dominant
  0.50     4  -159.2        NaN         NaN    Attacker dominant
  0.50    10  -296.1        NaN         NaN    Attacker dominant
  0.50    25  -445.6        NaN         NaN    Attacker dominant
  0.50   100  -681.3        NaN         NaN    Attacker dominant
  0.80     4   -81.6        NaN         NaN    Attacker dominant
  0.80    10  -178.9        NaN         NaN    Attacker dominant
  0.80    25  -305.4        NaN         NaN    Attacker dominant
  0.80   100  -527.2        NaN         NaN    Attacker dominant
  1.00     4    +0.0        NaN         NaN    Attacker dominant
  1.00    10    +0.0        NaN         NaN    Attacker dominant
  1.00    25    +0.0        NaN         NaN    Attacker dominant
  1.00   100    +0.0        NaN         NaN    Attacker dominant

Lecture du résultat — un formalisme qui ne mord pas sur ce terrain

La table dit une chose simple et décisive : sur toute la grille (K ∈ {4, 10, 25, 100}, d ∈ {0, 0.1, 0.3}), D reste ≤ 0 — colonne « Attacker dominant » de bout en bout, et n* vaut NaN partout. Ce n’est pas un bug de calcul : D_approx vaut -400·log10(K(1-d) + d), quantité structurellement négative dès que K > 1, car elle mesure l’ambiguïté du secret, pas un avantage du Defender. La conséquence est forte : dans cette paramétrisation, le régime NSO (0 < D < 400) n’est jamais atteint, et le close-form ne peut donc rien prédire ici. Le notebook le dit honnêtement au lieu de forcer un chiffre.

Le sweep de la cellule précédente donne la contrepartie mesurée de cette impasse : quand le brouillage croît, ce n’est pas le nombre de questions qui bouge (avg_Q reste calé sur log2(K) : 2.0, 3.4, 4.7, 6.7) mais le taux de succès qui s’effondre (K=10 : 1.000 → 0.010 entre d=0 et d=0.8). La ressource rare n’est pas l’interrogation, c’est la fiabilité de chaque réponse.

Exercice 1 : localiser l’effondrement de p_success

Le sweep montre p_success qui chute de 1.000 à 0.010 (K=10) entre d=0 et d=0.8, alors que avg_Q ne bouge presque pas. Précisez la transition.

  • Étape 1 : pour K=10, mesurez p_success à d ∈ {0.55, 0.6, 0.65, 0.7, 0.75} (500 épisodes par point, réutilisez run_episode).
  • Étape 2 : tracez p_success(d) en échelle log — la décroissance est-elle exponentielle ? Ajustez si oui un taux.
  • Étape 3 : estimez le d où p_success = 0.5 par interpolation (le sweep donnait déjà 0.302 à d=0.3).

Indice : chaque question du Judge porte sur une réponse bruitée à probabilité d ; l’attaquant bayésien a besoin de plusieurs réponses concordantes — le log de p_success devrait être proche d’affine en d.

# Exercice 1 : localiser l'effondrement de p_success (K=10)
# Etape 1 : p_success pour d in {0.55, 0.6, 0.65, 0.7, 0.75}, 500 episodes
# Etape 2 : echelle log -- decroissance exponentielle ? taux ?
# Etape 3 : d tel que p_success = 0.5 par interpolation
# TODO etudiant
p_succ_fins = None  # TODO etudiant : dict {d: p_success}
d_demi = None       # TODO etudiant : d ou p_success = 0.5
print("Exercice 1 a completer : effondrement de p_success")
Exercice 1 a completer : effondrement de p_success

Exercice 2 : le coût informationnel de K est-il affine en log2(K) ?

À d=0, avg_Q vaut 2.0 (K=4), 3.4 (K=10), 4.7 (K=25), 6.7 (K=100) — des valeurs proches de log2(K), sans écart constant apparent. Vérifiez cette loi.

  • Étape 1 : à partir de results (le dictionnaire du sweep, d=0.0), extrayez avg_Q pour les quatre K.
  • Étape 2 : calculez log2(K) pour chacun et l’écart avg_Q - log2(K).
  • Étape 3 : testez l’hypothèse affine par np.polyfit sur (log2 K, avg_Q>. L’attaquant par dichotomie postérieure paie-t-il exactement le log2, ou un peu plus ?

Indice : un écart qui grandirait avec K trahirait un coût non logarithmique ; un écart qui reste petit et stable signalerait au contraire une taxe de terminaison fixe (la dernière question qui confirme au lieu de couper).

# Exercice 2 : avg_Q vs log2(K) -- hypothese affine
# Etape 1 : extraire avg_Q (d=0.0) pour K in {4, 10, 25, 100}
# Etape 2 : ecart avg_Q - log2(K) par K
# Etape 3 : np.polyfit sur (log2 K, avg_Q) -- pente et ordonnee
# TODO etudiant
pente = None      # TODO etudiant : pente du fit affine
ecarts_log = None # TODO etudiant : liste avg_Q - log2(K)
print("Exercice 2 a completer : cout informationnel de K")
Exercice 2 a completer : cout informationnel de K

Exercice 3 : le régime NSO est-il atteignable dans ce formalisme ?

La Lecture de la section 2 l’affirme : D_approx ≤ 0 partout dès K > 1, donc n* est toujours NaN. Démontrez-le proprement, puis cherchez ce qu’il faudrait changer.

  • Étape 1 : pour K fini > 1 et d ∈ [0, 1], montrez que K(1-d) + d ≥ 1 avec égalité seulement si (K=1 ou d=1) — concluez que D_approx ≤ 0 sans jamais entrer dans (0, 400).
  • Étape 2 : vérifiez numériquement sur une grille fine (K ∈ [2, 100], d ∈ [0, 1], pas 0.05) qu’aucun point ne donne D > 0.
  • Étape 3 : proposez une modification minimale de D_approx (par exemple un avantage Elo défensif additif quand d < seuil) qui rende le régime (0, 400) atteignable, et dites ce que la modification signifie conceptuellement.

Indice : l’étape 1 est une inégalité à une ligne ; l’étape 3 n’a pas de réponse unique — c’est la discussion de design qui compte.

# Exercice 3 : le regime NSO (0 < D < 400) est-il atteignable ici ?
# Etape 1 : preuve algebrique K(1-d)+d >= 1 pour K>1, d in [0,1]
# Etape 2 : verification numerique sur grille fine -- aucun D > 0 ?
# Etape 3 : modification minimale de D_approx rendant (0,400) atteignable
# TODO etudiant
D_max_grille = None  # TODO etudiant : max de D_approx sur la grille
proposition = None   # TODO etudiant : description de la modification
print("Exercice 3 a completer : atteignabilite du regime NSO")
Exercice 3 a completer : atteignabilite du regime NSO

Conclusion

Ce que ce notebook valide

  1. Regimes Wargames identifies selon (K, d_strength) : transparent, defense faible/moyenne/forte. Le balayage 4×5×500 episodes = 10000 simulations produit une carte du taux de reussite.
  2. Demi-vie de controle : non mesurable dans le regime balaye - p_real plafonne a ~0.3 des 3 questions (K=10, d=0.3). L’ordre de grandeur operationnel est un plateau, pas une demi-vie.
  3. Extension NSO : l’approximation D_approx(d_strength, K) permet d’etendre le formalisme R12 au cas Wargames, mais reste grossiere (le vrai mapping Elo/Defense Strategy demanderait une calibration empirique sur LLM reels).

Limites assumées (honnetete Tell c.G.9)

  1. Pas de LLM reels : Tell c.1261-L1 ★★★ strict. Les Agents bayesien/Defender sont des modeles jouets. R12 utilise GPT-4 comme Attacker et Defender reels.
  2. Posterior simplifiee : l’Attacker suppose d_strength = 0 (Defender transparent). Une version sophistiquee apprendrait le d_strength en meme temps que le secret (inference jointe).
  3. Extension NSO grossiere : D_approx est une heuristique, pas une mesure. La vraie calibration demanderait des experiences Wargames avec LLMS sur les 4 jeux R12.

Parallele cluster

Wargames formalise un scenario que nous jouons en pratique : quand un coordinateur demande a un worker de prouver qu’un livrable est valide (le ‘secret’ = le contenu reel), un reviewer peut soit valider sans regarder (Defender transparent), soit appliquer un audit strict (Defender fort). Le ‘Judge’ dans R12 = notre checker Hermes/NanoClaw qui evalue la qualite du livrable.

Pour notre cluster : d_strength correspond a la probabilite de reponse non conforme du worker (un taux de conformite observe de ~85% equivaut a d = 0.15), K au nombre de possibilites de sortie (~10-50 selon la complexite du grain), et le Judge = Hermes/NanoClaw avec judge_noise ≈ 0.1-0.2 (faux positifs/negatifs).

Implication operationnelle : la table NSO ne tranche pas - n* rend NaN sur toute la grille (D negatif, < 400 partout, regime « Attacker dominant »). Le choix du nombre de niveaux de Judge reste empirique (coordinateur + bots specialises pour les grains complexes).

Suite logique (PR 4+ sur #16754)

  • PR 4 - Oversight/Oversight-Statistics.ipynb : module stats consolidé final (Elo + L-BFGS-B + AIC + NSO + Wargames). Sur les 4 notebooks : Nim (mesure), Analytics (close-form), Wargames (simulation), Statistics (consolidation).
  • PR 5 (optionnel) - calibration empirique : si greenlight GenAI po-2023 ou ai-01 vLLM (Tell c.1261-L1), executer le scenario Wargames avec GPT-4 vs GPT-3.5 et mesurer les vrais taux.

Sources : - R12 - Engels, Baek, Kantamneni, Tegmark. Scaling Laws For Scalable Oversight. NeurIPS 2025. arXiv:2504.18530, Section 5. - Sub-grain #16754 (T13 distillation corpus Tegmark) - EPIC #16741.

Retour au sommet