ICT-37 - F-Lens : mode belief-state, probing lineaire et geometrie predictive held-out

Navigation : >> ICT-35 | voir aussi #15478 (mode factored-geometry, ICT-36, PR #15514)

Grain : Issue #15477 - mode belief-state de la F-Lens. Parent : Epic #15475 (toolkit multi-instrument ICT). Dependances : #15476 (ICT-trace-contract) MERGED - split/fit IDs disponibles via ict_trace.

Question scientifique

Un etat predictif suffisant du processus generateur est-il lineairement accessible dans le residual stream, et ou et quand cette accessibilite apparait-elle ?

Ce notebook repond a “quel etat predictif est accessible ?” (mode belief-state), distinct du mode factored-geometry (#15478, ICT-36) qui etudie l’organisation geometrique des facteurs.

Protocole

  • Processus synthetiques a belief state ground-truth exact : Mess3 (HMM 3 etats), RRXOR (geometrie non-reductible au next-token).
  • Activations simulees : representation dense deterministe (couches pre/post LayerNorm capturees en deux blocs distincts).
  • Probe lineaire supervisee : Ridge par equation normale regularisee, split train/validation/test gele par ID.
  • Metriques held-out : R2, RMSE, accuracy, calibration (Brier score).
  • Baselines : (a) shuffle des cibles ; (b) probe next-token ; (c) baseline majoritaire.
  • Multi-seed : 5 seeds, IC rapportees.

Livrables

  • solve_ols_ridge - probe lineaire Ridge par resolution fermee.
  • eval_heldout / brier_score - metriques held-out.
  • make_mess3_transitions / sample_mess3 - generateur HMM Mess3 + etat cache exact.
  • make_rrxor - generateur RRXOR avec belief ground-truth (parite).
  • simulate_residual - simulateurs d’activations pre/post LayerNorm.
  • 3 exemples guides (orthogonal, Mess3, RRXOR), chacun suivi d’un exercice non resolu - chacun avec verdict falsifiable.

Hypotheses falsifiables

  • H1 : le residual stream predit mieux le belief state que le controle shuffle (gap >= 0.3 accuracy held-out, ou >= 0.2 sur RRXOR binaire).
  • H2 : sur le regime orthogonal, l’accuracy pre-LayerNorm est superieure a post-LayerNorm (LayerNorm ecrase la magnitude, mais conserve la direction).
  • H3 : sur RRXOR, le probe belief performe mieux que le probe next-token par >= 0.1 accuracy (dissociation belief vs next-token).

Acceptance vs #15477

Statut épistémique — Sans verdict à ce jour : aucune ligne de la matrice de dissociations ne concerne ce notebook ; son statut épistémique sera porté par la matrice le cas échéant.

Voisins et positionnement

Même geste que le S-Lens (ICT-38) : nommer les voisins de l’instrument, et ce qui nous en sépare.

Publications fondatrices (Epic #15475, bibliothèque canonique — aucun PDF committé) : Shai et al., Transformers Represent Belief State Geometry in their Residual Stream (arXiv:2405.15943) et Transformers Learn Factored Representations (arXiv:2602.02385) — c’est ce second papier, section belief-state, que le mode de cette F-Lens instrumente.

Dépôts de référence — un mot par voisin :

  • Astera-org/simplexity : JAX/Equinox, generative_processes/ porte déjà hidden_markov_model et mixed_state_presentation. Nos make_mess3_transitions / make_rrxor les reconstruisent à la main en numpy ; ce que cette F-Lens ajoute est le probe held-out à baselines explicites (shuffle, next-token, majoritaire), que ces dépôts ne publient pas.
  • Astera-org/factored-reps : le même programme en fwh_core, sans banc de probing de belief state.
  • Astera-org/strange-loop : voisin le plus précoce, expérience centrale en stub — rien n’en est importé.

Les recoupements de modules ci-dessus sont releves au scoping de #16224 (lecture seule des depots, aucun code copie) : ils sont reportes ici comme positionnement, pas re-mesures.

Pourquoi numpy plutôt que simplexity. La règle d’architecture de la série confine ict/ au numpy-only (torch aux scripts d’extraction) ; simplexity est Hydra + JAX/Equinox. Le portage rendrait les primitives intestables sans GPU. Aucune licence détectée sur ces dépôts (preflight Epic #15475) : aucun code n’en est copié.

Renvoi interne. La notion d’état prédictif que ces hypothèses sondent est celle de la mécanique computationnelle de Crutchfield, enseignée dans la série en ICT-17 — EpsilonMachine — à lire avant ce notebook pour qui veut la construction de l’objet, celui-ci n’en testant que l’accessibilité linéaire.

# Parametres du notebook
nb_name = "ICT-37-FLens-BeliefState-Python"
N_SEEDS = 5  # multi-seed : au moins 4 seeds (acceptance #15477)
N_TRAIN = 6000  # points d'entrainement
N_TEST = 1500   # points held-out
DIM = 32        # dimension du residual stream simule
N_HIDDEN = 3    # nombre d'etats caches (Mess3)
RNG_SEEDS = [11, 23, 47, 89, 123]  # 5 seeds pour IC

print(f"=== {nb_name} ===")
print(f"N_SEEDS={N_SEEDS}, N_TRAIN={N_TRAIN}, N_TEST={N_TEST}, DIM={DIM}")
print(f"Verdict par hypothese : H1 SUPPORTED si accuracy_belief - accuracy_shuffle >= 0.3")
print(f"                       H2 SUPPORTED si accuracy_preLN > accuracy_postLN (orthogonal)")
print(f"                       H3 SUPPORTED si belief > next_token par >= 0.1 (RRXOR)")
=== ICT-37-FLens-BeliefState-Python ===
N_SEEDS=5, N_TRAIN=6000, N_TEST=1500, DIM=32
Verdict par hypothese : H1 SUPPORTED si accuracy_belief - accuracy_shuffle >= 0.3
                       H2 SUPPORTED si accuracy_preLN > accuracy_postLN (orthogonal)
                       H3 SUPPORTED si belief > next_token par >= 0.1 (RRXOR)
# Imports : numpy uniquement (acceptance #15477 primitives numpy-only)
# numpy.linalg suffit : pas de scikit-learn, pas de scipy, pas de torch.
# Rationale : autonomie CPU-only, reproductibilite, deploiement sur toute machine.
import numpy as np

print(f"numpy version: {np.__version__}")
numpy version: 2.4.6

Primitives numpy-only

Trois briques de base, toutes numpy-only :

Primitive Role
solve_ols_ridge Probe lineaire Ridge par equation normale regularisee - retourne poids et intercept
eval_heldout Metriques held-out : R2, RMSE, accuracy, Brier score
brier_score Calibration pour probabilites predites vs belief ground-truth one-hot

Ces primitives sont autonomes : aucune dependance a scikit-learn, scipy, ou torch. Le probe Ridge est resolu par l’equation normale regularisee : W_hat = (X^T X + lambda I)^-1 X^T y avec lambda = 1.0 par defaut. Cela suffit largement pour un espace de dimension 32.

def solve_ols_ridge(X, y, lam=1.0):
    """Probe lineaire Ridge par equation normale regularisee.

    X : ndarray shape (N, D) activations
    y : ndarray shape (N,) belief indices ou (N, K) probabilites
    lam : float regularisation L2
    Returns W, b.
    """
    X_ = np.column_stack([X, np.ones(X.shape[0])])  # ajout colonne biais
    D_plus = X_.shape[1]
    reg = lam * np.eye(D_plus)
    reg[-1, -1] = 0.0  # pas de regularisation sur le biais
    if y.ndim == 1:
        W_full = np.linalg.solve(X_.T @ X_ + reg, X_.T @ y.astype(float))
        return W_full[:-1], W_full[-1]
    else:
        W_full = np.linalg.solve(X_.T @ X_ + reg, X_.T @ y.astype(float))
        return W_full[:-1, :], W_full[-1, :]


def eval_heldout(y_true, y_pred, y_prob=None):
    """Metriques held-out : RMSE, accuracy, R2, Brier."""
    rmse = float(np.sqrt(np.mean((y_true.astype(float) - y_pred.astype(float)) ** 2)))
    acc = float(np.mean(y_true == y_pred))
    if y_prob is not None:
        K = y_prob.shape[1]
        onehot = np.eye(K)[y_true]
        ss_res = float(np.sum((onehot - y_prob) ** 2))
        ss_tot = float(np.sum((onehot - onehot.mean(axis=0)) ** 2))
        r2 = 1.0 - ss_res / max(ss_tot, 1e-9)
        brier = float(np.mean(np.sum((onehot - y_prob) ** 2, axis=1)))
    else:
        r2 = float("nan")
        brier = float("nan")
    return {"rmse": rmse, "accuracy": acc, "r2": r2, "brier": brier}


def brier_score(y_true, y_prob):
    """Brier score = MSE entre one-hot(y_true) et y_prob."""
    K = y_prob.shape[1]
    onehot = np.eye(K)[y_true]
    return float(np.mean(np.sum((onehot - y_prob) ** 2, axis=1)))


# Smoke test des primitives
np.random.seed(0)
X_test = np.random.randn(100, 4)
y_test = np.random.randint(0, 3, 100)
W, b = solve_ols_ridge(X_test, y_test)
y_pred_test = np.clip(np.round(X_test @ W + b).astype(int), 0, 2)
print(f"Smoke test primitives OK : W.shape={W.shape}, accuracy_test={np.mean(y_test == y_pred_test):.3f}")
Smoke test primitives OK : W.shape=(4,), accuracy_test=0.340

Generateurs de processus synthetiques (conformes #16225)

Deux processus a belief ground-truth exact, importes du module ict – plus aucune redefinition inline :

Mess3 (HMM 3 etats, emissions ternaires discretes)

Processus de Marzen & Crutchfield (2017), Nearly maximally predictive features and their dimensions (reference [20] de arXiv:2405.15943). Trois etats caches, persistance p_stay = 0.95, alphabet ternaire discret {0, 1, 2} dont l’emission ne revele pas l’etat (emission_diag = 0.5). Le belief ground-truth est la distribution filtree P(s_t | obs_0..t) – un point du 2-simplexe, pas l’etat cache.

RRXOR (Riechers & Crutchfield 2017, arXiv:1706.00883)

Le processus repete les triplets (r1, r2, r1 XOR r2) : correlations par paires nulles, spectre plat, mais contrainte de triplet deterministe. Epsilon-machine a 5 etats causaux (machine Mealy : emissions sur les aretes). Sa mixed-state presentation compte 36 croyances distinctes (31 transitoires + 5 recurrentes, litterature p. 17 Fig. 7) – et plusieurs de ces croyances partagent la meme prediction next-token : c’est la dissociation que l’exercice 3 mesure.

Les deux generateurs viennent de ict/bench_factorise.py (numpy-only) et la primitive MSP de ict/mixed_state.py (BFS sequence -> croyance).

# Racine canonique de la serie : remonte les parents depuis le dossier du
# notebook jusqu'au package ict/, puis en derive les dossiers de donnees.
# La serie reste executable depuis sa racine comme depuis le dossier d'une
# sous-serie (arbitrage #4362, preparation de l'arc A).
import sys
from pathlib import Path

ICT_ROOT = Path.cwd()
while not (ICT_ROOT / "ict" / "__init__.py").exists() and ICT_ROOT != ICT_ROOT.parent:
    ICT_ROOT = ICT_ROOT.parent
assert (ICT_ROOT / "ict" / "__init__.py").exists(), (
    f"package ict/ introuvable en remontant depuis {Path.cwd().name}")
if str(ICT_ROOT) not in sys.path:
    sys.path.insert(0, str(ICT_ROOT))
TRACES_DIR = ICT_ROOT / "traces"
RUNS_DIR = ICT_ROOT / "runs"
SCRIPTS_DIR = ICT_ROOT / "scripts"
print(f"racine ict : {ICT_ROOT.name}")
racine ict : ICT-Series
import os
import sys

# Generateurs conformes importes du module ict (plus de version inline).
from ict.bench_factorise import Mess3Canonical, RRXOR
from ict.mixed_state import msp_mess3, msp_rrxor

MESS3 = Mess3Canonical()
RRXOR_GEN = RRXOR()


def simulate_residual(B, dim, layer, rng):
    """Simule des activations a partir d'une matrice de croyances B (N, K).

    Lineaire en B : X = B @ W ou W est (K, dim). Un belief Dirac sur l'etat i
    redonne X = W_i (le regime orthogonal de l'exercice 1 est le cas
    particulier croyance synchronisee). layer : 'pre' (bruit additif fort)
    ou 'post' (normalisation L2 + bruit faible, style LayerNorm).
    """
    K = B.shape[1]
    W = rng.standard_normal((K, dim)) * 0.5
    X = B @ W
    if layer == "pre":
        X += rng.standard_normal(X.shape) * 0.3
    elif layer == "post":
        norms = np.linalg.norm(X, axis=1, keepdims=True) + 1e-6
        X = X / norms
        X += rng.standard_normal(X.shape) * 0.05
    return X


# Test rapide des generateurs conformes
rng = np.random.default_rng(42)
states_m, obs_m = MESS3.sample(100, int(rng.integers(0, 2**31 - 1)))
states_r, obs_r = RRXOR_GEN.sample(99, int(rng.integers(0, 2**31 - 1)))
print(f"Mess3 : 100 steps, etats uniques = {np.unique(states_m)}, obs uniques = {np.unique(obs_m)}")
print(f"RRXOR : 99 steps, etats d'arrivee uniques = {np.unique(states_r)}")
k = (len(obs_r) // 3) * 3
print(f"RRXOR : triplets (r1, r2, r1 XOR r2) valides : {bool(np.all(obs_r[2:k:3] == obs_r[0:k:3] ^ obs_r[1:k:3]))}")
Mess3 : 100 steps, etats uniques = [0 1 2], obs uniques = [0 1 2]
RRXOR : 99 steps, etats d'arrivee uniques = [0 1 2 3 4]
RRXOR : triplets (r1, r2, r1 XOR r2) valides : True

Mixed-state presentation : la geometrie exacte du banc

La primitive ict/mixed_state.py enumere par BFS l’arbre sequence -> croyance : cardinal par profondeur, union fermee, et le test fondateur de la dissociation — des croyances distinctes partagent la meme prediction next-token.

# Mixed-state presentation : enumeration BFS sequence -> croyance (#16225)
# Mess3 : croissance 3^k. RRXOR : union FERMEE a 36 croyances distinctes.
msp3 = msp_mess3(max_depth=4)
mspr = msp_rrxor()
print("MSP Mess3, croyances par profondeur :", [msp3.n_distinct(d) for d in range(msp3.depth)])
print("MSP RRXOR, croyances par profondeur :", [mspr.n_distinct(d) for d in range(mspr.depth)])
print(f"MSP RRXOR, union des croyances distinctes : {mspr.n_distinct_total()} (litterature : 36 = 31 transitoires + 5 recurrentes)")

# Dissociation : des croyances DISTINCTES partagent la meme prediction next-token
Wsum = RRXOR_GEN.edge_tensor().sum(axis=1)  # Wsum[s, y] = P(y | etat causal s)
groups = {}
for level in mspr.nodes:
    for b in level:
        pred = tuple(np.round(b @ Wsum, 6))
        groups.setdefault(pred, set()).add(tuple(np.round(b, 6)))
n_beliefs = sum(len(g) for g in groups.values())
print(f"{n_beliefs} croyances pour {len(groups)} predictions next-token distinctes : la carte belief -> next-token est non injective")
example = max(groups.values(), key=len)
print(f"Groupe le plus large : {len(example)} croyances distinctes -> meme next-token (uniforme 1/2, 1/2)")

ok3, f3 = msp3.verify_invariants(MESS3.beliefs)
okr, fr = mspr.verify_invariants(RRXOR_GEN.beliefs)
print(f"Invariants MSP Mess3 : {ok3} {f3}")
print(f"Invariants MSP RRXOR : {okr} {fr}")
MSP Mess3, croyances par profondeur : [1, 3, 9, 27]
MSP RRXOR, croyances par profondeur : [1, 2, 4, 8, 12, 17, 17, 17]
MSP RRXOR, union des croyances distinctes : 36 (litterature : 36 = 31 transitoires + 5 recurrentes)
36 croyances pour 11 predictions next-token distinctes : la carte belief -> next-token est non injective
Groupe le plus large : 10 croyances distinctes -> meme next-token (uniforme 1/2, 1/2)
Invariants MSP Mess3 : True []
Invariants MSP RRXOR : True []

Exemple guide 1 - Regime orthogonal (sanity check)

Hypothese : sur des activations ou chaque etat cache correspond a une direction orthogonale dans le residual stream, un probe lineaire Ridge doit atteindre une accuracy held-out proche de 1.0, et le shuffle baseline doit rester a ~1/K (1/3 = 0.333).

C’est le regime trivial : on verifie que les primitives fonctionnent et que le protocole capture bien la linearite du belief. Tout ecart significatif constitue un bug dans les primitives ou le protocole.

Verdict attendu : H1 (accuracy_belief - accuracy_shuffle >= 0.3) SUPPORTED. H2 (pre-LN > post-LN) est une question ouverte : avec l’encodage lineaire en belief (post-#16225), la normalisation post-L2 n’ecrase plus l’echelle des directions – le sens se lit sur la mesure, pas sur une attente.

results_orthogonal = {"pre": [], "post": []}
shuffle_results = []
for seed in RNG_SEEDS:
    rng = np.random.default_rng(seed)
    gen_seed = int(rng.integers(0, 2**31 - 1))
    states, obs = MESS3.sample(N_TRAIN + N_TEST, gen_seed)
    # Regime Dirac : croyance synchronisee sur l'etat (one-hot) -- sanity check
    B = np.eye(MESS3.n_states)[states]
    X_pre = simulate_residual(B, DIM, "pre", rng)
    X_post = simulate_residual(B, DIM, "post", rng)

    # Split train/test
    X_pre_train, X_pre_test = X_pre[:N_TRAIN], X_pre[N_TRAIN:]
    X_post_train, X_post_test = X_post[:N_TRAIN], X_post[N_TRAIN:]
    y_train, y_test = states[:N_TRAIN], states[N_TRAIN:]

    # Probe pre-LayerNorm
    W_pre, b_pre = solve_ols_ridge(X_pre_train, y_train, lam=1.0)
    y_pred_pre = np.clip(np.round(X_pre_test @ W_pre + b_pre).astype(int), 0, N_HIDDEN - 1)
    results_orthogonal["pre"].append(float(np.mean(y_test == y_pred_pre)))

    # Probe post-LayerNorm
    W_post, b_post = solve_ols_ridge(X_post_train, y_train, lam=1.0)
    y_pred_post = np.clip(np.round(X_post_test @ W_post + b_post).astype(int), 0, N_HIDDEN - 1)
    results_orthogonal["post"].append(float(np.mean(y_test == y_pred_post)))

    # Shuffle baseline
    rng_shuf = np.random.default_rng(seed + 1000)
    y_shuffled = rng_shuf.permutation(y_test)
    shuffle_results.append(float(np.mean(y_shuffled == y_test)))


acc_pre_mean = float(np.mean(results_orthogonal["pre"]))
acc_pre_std = float(np.std(results_orthogonal["pre"]))
acc_post_mean = float(np.mean(results_orthogonal["post"]))
acc_post_std = float(np.std(results_orthogonal["post"]))
acc_shuf_mean = float(np.mean(shuffle_results))
gap_pre = acc_pre_mean - acc_shuf_mean
gap_post = acc_post_mean - acc_shuf_mean

print("=== Exercice 1 - Regime orthogonal ===")
print(f"Accuracy belief (pre-LN)  : {acc_pre_mean:.3f} +/- {acc_pre_std:.3f}")
print(f"Accuracy belief (post-LN) : {acc_post_mean:.3f} +/- {acc_post_std:.3f}")
print(f"Accuracy shuffle baseline : {acc_shuf_mean:.3f}")
print(f"Gap (pre - shuffle)       : {gap_pre:.3f}")
print(f"Gap (post - shuffle)      : {gap_post:.3f}")

H1_orth = gap_pre >= 0.3
H2_orth = acc_pre_mean > acc_post_mean
print(f"H1 (orthogonal) : {'SUPPORTED' if H1_orth else 'NOT_SUPPORTED'}")
print(f"H2 (orthogonal) : {'SUPPORTED' if H2_orth else 'NOT_SUPPORTED'} (pre-LN > post-LN)")
=== Exercice 1 - Regime orthogonal ===
Accuracy belief (pre-LN)  : 0.998 +/- 0.001
Accuracy belief (post-LN) : 1.000 +/- 0.000
Accuracy shuffle baseline : 0.345
Gap (pre - shuffle)       : 0.653
Gap (post - shuffle)      : 0.655
H1 (orthogonal) : SUPPORTED
H2 (orthogonal) : NOT_SUPPORTED (pre-LN > post-LN)

Exercice 1 - Largeur minimale du residual stream

L’exemple guide 1 valide les primitives a DIM = 32. La limite 4 du verdict pose la question : a quelle largeur le sanity check tient-il encore ?

Enonce : faites varier la dimension du residual stream simule dans {2, 3, 5, 32} dans le regime orthogonal (croyance Dirac, probe pre-LN), et affichez l’accuracy held-out par dimension (moyenne puis ecart-type sur RNG_SEEDS). A partir de quelle largeur le verdict H1 (gap d’au moins 0.3 au-dessus du shuffle a 1/3) tient-il ?

Sortie attendue : une ligne par dimension avec accuracy et ecart-type, puis le verdict H1 par dimension.

Indices :

  • # Indice : simulate_residual(B, dim, "pre", rng) prend la dimension en argument – tout le reste du protocole de l’exemple guide 1 est inchange.
  • # Etape 1 : boucler sur les dimensions, reutiliser le corps de l’exemple guide 1 avec la dimension de la boucle a la place de DIM.
  • # Etape 2 : comparer chaque accuracy au shuffle (1/3) avec le seuil H1.
# Exercice 1 : largeur minimale du residual stream -- a completer

# TODO etudiant : sweep de la dimension dans {2, 3, 5, 32} en regime orthogonal
# (croyance Dirac, probe pre-LN) : accuracy held-out +/- ecart-type par dimension
# sur RNG_SEEDS, puis verdict H1 (gap >= 0.3 au-dessus de 1/3) par dimension.

print("Exercice a completer : accuracy du probe orthogonal par largeur de stream.")
resultat = None  # TODO etudiant
Exercice a completer : accuracy du probe orthogonal par largeur de stream.

Exemple guide 2 - Regime Mess3 canonique (emissions non revelatrices)

Le banc est desormais le Mess3 canonique (Marzen & Crutchfield 2017) : alphabet ternaire discret dont l’emission ne revele pas l’etat. Le belief ground-truth est la distribution filtree exacte P(s_t | obs_0..t) sur le 2-simplexe – calculee par MESS3.beliefs(obs) (filtration forward numpy).

Question : le probe lineaire recupere-t-il le belief vectoriel (coordonnees du simplexe) depuis le residual stream simule ?

Ce qui change vs la version obs = etat : l’ancienne equivalence next-token == belief par construction est LEVEE (c’etait l’artefact denonce par #16225 – accuracy 1.000 tautologique). Le probe next-token predira une observation stochastique : son plafond est E[max_y P(y_{t+1} | b_t)], mesure ci-dessous.

Verdict attendu : H1 SUPPORTED (accuracy argmax belief - shuffle >= 0.3).

results_mess3_belief = []
results_mess3_nexttok = []
results_mess3_r2 = []
ceiling_next_mess3 = []

for seed in RNG_SEEDS:
    rng = np.random.default_rng(seed)
    gen_seed = int(rng.integers(0, 2**31 - 1))
    # +1 pas : le probe next-token predit obs[t+1]
    states, obs = MESS3.sample(N_TRAIN + N_TEST + 1, gen_seed)
    beliefs = MESS3.beliefs(obs)  # ground-truth exact : filtration forward

    X = simulate_residual(beliefs, DIM, "pre", rng)
    X_train, X_test = X[:N_TRAIN], X[N_TRAIN:N_TRAIN + N_TEST]
    B_train, B_test = beliefs[:N_TRAIN], beliefs[N_TRAIN:N_TRAIN + N_TEST]

    # Probe belief : regression Ridge sur les 3 coordonnees du simplexe
    W_belief, b_belief = solve_ols_ridge(X_train, B_train, lam=1.0)
    B_pred = X_test @ W_belief + b_belief
    results_mess3_belief.append(float(np.mean(B_pred.argmax(axis=1) == B_test.argmax(axis=1))))
    ss_res = float(((B_pred - B_test) ** 2).sum())
    ss_tot = float(((B_test - B_test.mean(axis=0)) ** 2).sum())
    results_mess3_r2.append(1.0 - ss_res / ss_tot)

    # Probe next-token : predire l'observation suivante (ternaire, stochastique)
    y_next_train = obs[1:N_TRAIN + 1].astype(float)
    y_next_test = obs[N_TRAIN + 1:N_TRAIN + N_TEST + 1]
    W_next, b_next = solve_ols_ridge(X_train, y_next_train, lam=1.0)
    y_pred_next = np.clip(np.round(X_test @ W_next + b_next).astype(int), 0, 2)
    results_mess3_nexttok.append(float(np.mean(y_next_test == y_pred_next)))

    # Plafond next-token mesure : E[max_y P(y_{t+1} | b_t)] sur le held-out
    P_next = (B_test @ MESS3.transition_matrix()) @ MESS3.emission_matrix()
    ceiling_next_mess3.append(float(P_next.max(axis=1).mean()))


mean_belief = float(np.mean(results_mess3_belief))
std_belief = float(np.std(results_mess3_belief))
mean_r2 = float(np.mean(results_mess3_r2))
mean_next = float(np.mean(results_mess3_nexttok))
std_next = float(np.std(results_mess3_nexttok))
mean_ceiling = float(np.mean(ceiling_next_mess3))
gap = mean_belief - acc_shuf_mean

print("=== Exercice 2 - Regime Mess3 canonique (emissions non revelatrices) ===")
print(f"Accuracy belief (argmax) held-out : {mean_belief:.3f} +/- {std_belief:.3f}")
print(f"R2 belief (3 coordonnees simplexe): {mean_r2:.3f}")
print(f"Accuracy next-token held-out      : {mean_next:.3f} +/- {std_next:.3f}")
print(f"Plafond next-token mesure         : {mean_ceiling:.3f}")
print(f"Gap belief - shuffle              : {gap:.3f}")

H1_mess3 = gap >= 0.3
print(f"H1 (Mess3 belief vs shuffle) : {'SUPPORTED' if H1_mess3 else 'NOT_SUPPORTED'}")
print("Note : equivalence next-token == belief LEVEE -- l'observation ne revele plus l'etat (#16225)")
=== Exercice 2 - Regime Mess3 canonique (emissions non revelatrices) ===
Accuracy belief (argmax) held-out : 0.908 +/- 0.006
R2 belief (3 coordonnees simplexe): 0.886
Accuracy next-token held-out      : 0.349 +/- 0.019
Plafond next-token mesure         : 0.408
Gap belief - shuffle              : 0.563
H1 (Mess3 belief vs shuffle) : SUPPORTED
Note : equivalence next-token == belief LEVEE -- l'observation ne revele plus l'etat (#16225)

Exercice 2 - H2 sur le banc non trivial : pre-LN vs post-LN

L’exemple guide 1 mesurait H2 (pre-LN > post-LN) dans le regime orthogonal ; son enonce laissait la question ouverte ailleurs. Tranchez-la sur Mess3.

Enonce : pour le regime Mess3 canonique, mesurez le probe belief (accuracy argmax ET R2 sur les coordonnees du simplexe) en pre-LN et en post-LN, sur RNG_SEEDS. H2 est-elle SUPPORTED sur ce banc ?

Sortie attendue : accuracy et R2 pour chaque normalisation, puis le verdict H2 avec une phrase d’interpretation.

Indices :

  • # Indice : le changement est d’un seul argument – simulate_residual(beliefs, DIM, layer, rng) avec layer dans {“pre”, “post”}.
  • # Etape 1 : reprendre le probe belief de l’exemple guide 2 (regression Ridge sur les coordonnees du simplexe), une fois par layer.
  • # Etape 2 : calculer R2 comme dans l’exemple guide 2 (un moins le rapport des sommes de carres).
# Exercice 2 : pre-LN vs post-LN sur le probe belief Mess3 -- a completer

# TODO etudiant : accuracy argmax et R2 du probe belief Mess3 en pre-LN et en
# post-LN (RNG_SEEDS), puis verdict H2 (pre-LN > post-LN) sur ce banc.

print("Exercice a completer : le probe belief Mess3 sous les deux normalisations.")
resultat = None  # TODO etudiant
Exercice a completer : le probe belief Mess3 sous les deux normalisations.

Exemple guide 3 - RRXOR conforme : dissociation belief vs next-token

Le RRXOR de la litterature (Riechers & Crutchfield 2018) repete les triplets (r1, r2, r1 XOR r2). Sa MSP compte 36 croyances distinctes, et la cellule de demo l’a montre : la carte belief -> next-token est non injective – des croyances distinctes partagent la meme prediction. C’est exactement la structure que l’ancien banc (parite du bit precedent, 2 etats) ne pouvait pas produire.

Protocole : le residual stream simule encode lineairement le belief vecteur (5 coordonnees). Le probe belief regresse ces coordonnees ; le probe next-token predit le bit suivant, stochastique depuis G/A (uniforme) et deterministe depuis X – plafond E[max_y P(y | b_t)] ~ 2/3 en regime stationnaire, mesure ci-dessous.

Prediction : le probe belief depasse le probe next-token d’au moins 0.1 (H3) – le belief porte la phase du processus, le next-token n’en porte qu’une projection. Un H3 NOT_SUPPORTED sur CE banc serait un vrai resultat (plus d’artefact de generateur a invoquer).

results_rrxor_belief = []
results_rrxor_nexttok = []
results_rrxor_r2 = []
ceiling_next_rrxor = []

for seed in RNG_SEEDS:
    rng = np.random.default_rng(seed)
    gen_seed = int(rng.integers(0, 2**31 - 1))
    states, obs = RRXOR_GEN.sample(N_TRAIN + N_TEST + 1, gen_seed)
    beliefs = RRXOR_GEN.beliefs(obs)  # (n, 5) : filtration forward Mealy

    X = simulate_residual(beliefs, DIM, "pre", rng)
    X_train, X_test = X[:N_TRAIN], X[N_TRAIN:N_TRAIN + N_TEST]
    B_train, B_test = beliefs[:N_TRAIN], beliefs[N_TRAIN:N_TRAIN + N_TEST]

    # Probe belief : regression sur les 5 coordonnees, accuracy en argmax
    W_b, b_b = solve_ols_ridge(X_train, B_train, lam=1.0)
    B_pred = X_test @ W_b + b_b
    results_rrxor_belief.append(float(np.mean(B_pred.argmax(axis=1) == B_test.argmax(axis=1))))
    ss_res = float(((B_pred - B_test) ** 2).sum())
    ss_tot = float(((B_test - B_test.mean(axis=0)) ** 2).sum())
    results_rrxor_r2.append(1.0 - ss_res / ss_tot)

    # Probe next-token : predire le bit suivant (stochastique depuis G/A)
    y_next_train = obs[1:N_TRAIN + 1].astype(float)
    y_next_test = obs[N_TRAIN + 1:N_TRAIN + N_TEST + 1]
    W_n, b_n = solve_ols_ridge(X_train, y_next_train, lam=1.0)
    y_pred_next = np.clip(np.round(X_test @ W_n + b_n).astype(int), 0, 1)
    results_rrxor_nexttok.append(float(np.mean(y_next_test == y_pred_next)))

    # Plafond next-token mesure : E[max_y P(y_{t+1} | b_t)] sur le held-out
    Wsum = RRXOR_GEN.edge_tensor().sum(axis=1)   # P(y | etat causal s)
    P_next = B_test @ Wsum
    ceiling_next_rrxor.append(float(P_next.max(axis=1).mean()))


mean_b = float(np.mean(results_rrxor_belief))
std_b = float(np.std(results_rrxor_belief))
mean_r2_r = float(np.mean(results_rrxor_r2))
mean_n = float(np.mean(results_rrxor_nexttok))
std_n = float(np.std(results_rrxor_nexttok))
mean_ceiling_r = float(np.mean(ceiling_next_rrxor))
gap_b = mean_b - 0.5
dissociation = mean_b - mean_n

print("=== Exercice 3 - RRXOR conforme : belief vs next-token ===")
print(f"Accuracy belief (argmax) held-out : {mean_b:.3f} +/- {std_b:.3f}")
print(f"R2 belief (5 coordonnees)         : {mean_r2_r:.3f}")
print(f"Accuracy next-token held-out      : {mean_n:.3f} +/- {std_n:.3f}")
print(f"Plafond next-token mesure         : {mean_ceiling_r:.3f}")
print(f"Gap belief - 0.5 (shuffle)        : {gap_b:.3f}")
print(f"Dissociation belief - next        : {dissociation:.3f}")

H1_rrxor = gap_b >= 0.2
H3_rrxor = dissociation >= 0.1
print(f"H1 (RRXOR belief vs shuffle) : {'SUPPORTED' if H1_rrxor else 'NOT_SUPPORTED'}")
print(f"H3 (RRXOR belief > next-tok) : {'SUPPORTED' if H3_rrxor else 'NOT_SUPPORTED'}")
=== Exercice 3 - RRXOR conforme : belief vs next-token ===
Accuracy belief (argmax) held-out : 1.000 +/- 0.000
R2 belief (5 coordonnees)         : 0.931
Accuracy next-token held-out      : 0.665 +/- 0.006
Plafond next-token mesure         : 0.667
Gap belief - 0.5 (shuffle)        : 0.500
Dissociation belief - next        : 0.335
H1 (RRXOR belief vs shuffle) : SUPPORTED
H3 (RRXOR belief > next-tok) : SUPPORTED

Exercice 3 - Robustesse au bruit du probe belief RRXOR

La limite 3 du verdict le nomme : le banc n’a explore qu’un bruit additif fixe. Quantifiez la robustesse du readout.

Enonce : ajoutez au residual stream simule (regime RRXOR, pre-LN) un bruit additif d’ecart-type sigma dans {0.0, 0.3, 1.0, 1.5, 2.0}, et mesurez l’accuracy held-out du probe belief par sigma. Entre quelles valeurs consecutives de sigma l’accuracy tombe-t-elle sous 0.9 ?

Sortie attendue : accuracy et ecart-type par sigma, puis l’encadrement de la rupture.

Indices :

  • # Etape 1 : apres X = simulate_residual(beliefs, DIM, "pre", rng), ajouter X = X + rng.standard_normal(X.shape) * sigma – le bruit frappe train ET test, c’est la robustesse du readout qui est mesuree.
  • # Etape 2 : reprendre le probe belief de l’exemple guide 3, la boucle sigma en plus de la boucle seed.
  • # Indice : la rupture attendue se situe entre deux valeurs consecutives du sweep – dire lesquelles suffit.
# Exercice 3 : robustesse au bruit du probe belief RRXOR -- a completer

# TODO etudiant : sweep sigma dans {0.0, 0.3, 1.0, 1.5, 2.0} -- bruit additif sur le
# stream apres simulate_residual (regime RRXOR, pre-LN) -- accuracy held-out du probe
# belief par sigma, puis encadrement du sigma ou l'accuracy passe sous 0.9.

print("Exercice a completer : la courbe de robustesse du probe belief RRXOR.")
resultat = None  # TODO etudiant
Exercice a completer : la courbe de robustesse du probe belief RRXOR.

Verdict global

Hypothese Regime orthogonal (EG 1) Mess3 canonique (EG 2) RRXOR conforme (EG 3)
H1 belief >> shuffle SUPPORTED si gap >= 0.3 SUPPORTED si gap >= 0.3 SUPPORTED si gap >= 0.2
H2 pre-LN > post-LN applicable orthogonal - -
H3 belief >> next-token - plafond next-token mesure SUPPORTED si dissociation >= 0.1

Lecture : - Le regime orthogonal valide les primitives : le probe Ridge recupere quasi-parfaitement le belief quand l’information est lineairement encodee. - Le regime Mess3 canonique applique le probe a un HMM dont l’observation ne revele pas l’etat : le belief est un point du simplexe, recupere par regression vectorielle (R2), et le probe next-token plafonne a E[max_y P(y|b)] mesure – l’equivalence tautologique de l’ancienne version obs = etat est levee (#16225). - Le regime RRXOR conforme teste la dissociation sur la structure de la litterature : 36 croyances distinctes dont plusieurs partagent la meme prediction next-token (MSP non injective, see demo). Le probe belief doit performer au-dela du plafond next-token.

Limites

  1. Regime orthogonal est trivial : c’est un sanity check, pas un resultat scientifique. Il valide les primitives et le protocole.
  2. Banc simule, pas transformer reel : simulate_residual encode lineairement le belief ; la migration vers des activations reelles (Epic #15475) reste le test decisif.
  3. Bruit Gaussien additif : on n’a pas explore de regimes ou le bruit est structure (correle aux etats) ou non-stationnaire.
  4. Dimension 32 : suffisant pour 3-5 etats caches ; pour des HMM plus larges (10+ etats), il faudrait augmenter DIM et possiblement utiliser un probe non-lineaire (mais ce notebook reste numpy-only).

Migration future

Quand le contrat de trace v1 (Epic #15475 instrument) sera livre, ce notebook pourra charger des activations reelles (NPZ) au lieu des activations simulees. La signature simulate_residual(B, dim, layer, rng) est compatible avec un futur load_real_activations(npz_path, layer).

Références

  • arXiv:2405.15943 — Shai et al., Transformers Represent Belief State Geometry in their Residual Stream : papier fondateur du théorème de géométrie belief-state linéairement représentée dans le residual stream ; §2.2 définit la mise à jour eta' = eta T^(x) / (eta T^(x) 1) et §3.2 le RRXOR à 36 états de croyance.
  • Marzen & Crutchfield 2017 — Nearly maximally predictive features and their dimensions, Phys. Rev. E 95(5):051301(R) : origine du processus Mess3 (correction d’attribution #16225 — l’ancienne mention « singh et al. 1994 » était erronée).
  • Riechers & Crutchfield 2017 — Spectral Simplicity of Apparent Complexity, Part II (arXiv:1706.00883) : définition du RRXOR (triplets r1, r2, r1 XOR r2), epsilon-machine à 5 états, S-MSP à 36 croyances (Fig. 4 et 7).
  • arXiv:2602.02385 — Transformers Learn Factored Representations : base de la réimplémentation numpy-only (régimes orthogonal / probes linéaires).
  • Dépôt ZM — Zeinab-Mohammadi/pytorch-AI-interpretability-transformer_ZM : architecture minimale de référence, point de comparaison pour la migration future vers des activations réelles (cf. Epic #15475).
print("=" * 60)
print(f"VERDICT FINAL - {nb_name}")
print("=" * 60)

verdict_lines = []
verdict_lines.append(f"EG 1 orthogonal : H1={'SUPPORTED' if H1_orth else 'NOT_SUPPORTED'}, H2={'SUPPORTED' if H2_orth else 'NOT_SUPPORTED'}")
verdict_lines.append(f"EG 2 Mess3 canonique : H1={'SUPPORTED' if H1_mess3 else 'NOT_SUPPORTED'}, R2 belief={mean_r2:.3f}, plafond next-token={mean_ceiling:.3f}")
verdict_lines.append(f"EG 3 RRXOR conforme : H1={'SUPPORTED' if H1_rrxor else 'NOT_SUPPORTED'}, H3={'SUPPORTED' if H3_rrxor else 'NOT_SUPPORTED'}")

for line in verdict_lines:
    print(line)

if H1_orth and H1_mess3 and H1_rrxor:
    if H3_rrxor:
        print("\n-> Conclusion : SUPPORTED sur les hypotheses principales (H1, H3)")
        print("  Le probe lineaire recupere le belief state (vectoriel) de Mess3")
        print("  canonique et de RRXOR conforme ; la dissociation est SUPPORTED :")
        print("  le belief porte la phase du processus, le next-token n'en porte")
        print("  qu'une projection (plafond mesure, non un artefact du banc).")
    else:
        print("\n-> Conclusion : H1 SUPPORTED, H3 NOT_SUPPORTED")
        print(f"  Dissociation mesuree : {dissociation:.3f} (seuil 0.1).")
        print("  Sur le banc conforme #16225 (MSP 36 croyances, croyances")
        print("  distinctes a next-token identique), ce resultat est un vrai")
        print("  resultat d'experience -- il n'y a plus de defaut de generateur")
        print("  a invoquer ; la cause serait du cote de l'encodage simule.")
else:
    print("\n-> Conclusion : NOT_SUPPORTED sur au moins une hypothese - voir details ci-dessus.")
============================================================
VERDICT FINAL - ICT-37-FLens-BeliefState-Python
============================================================
EG 1 orthogonal : H1=SUPPORTED, H2=NOT_SUPPORTED
EG 2 Mess3 canonique : H1=SUPPORTED, R2 belief=0.886, plafond next-token=0.408
EG 3 RRXOR conforme : H1=SUPPORTED, H3=SUPPORTED

-> Conclusion : SUPPORTED sur les hypotheses principales (H1, H3)
  Le probe lineaire recupere le belief state (vectoriel) de Mess3
  canonique et de RRXOR conforme ; la dissociation est SUPPORTED :
  le belief porte la phase du processus, le next-token n'en porte
  qu'une projection (plafond mesure, non un artefact du banc).
Retour au sommet