Controle par interpretabilite — direction du refus, ablation, unlearning
1. Positionnement dans la distillation
Sources canoniques :
R11 — The 2026 Singapore Consensus on Global AI Safety Research Priorities (arXiv:2608.14611, Casper et al.). PDF archive hors depot : G:\Mon Drive\MyIA\IA\Bibliographie IA\XAI\2026 - Casper et al - The 2026 Singapore Consensus on Global AI Safety Research Priorities.pdf (sha8 134E9DA8, cf regle bibliography-hygiene). Sections mobilisees : §1.1, §2.2.5 (open-weight safety), §3.2.
R14 — Open Problems in Mechanistic Interpretability (arXiv:2501.16496, Sharkey et al.). PDF archive hors depot : G:\Mon Drive\MyIA\IA\Bibliographie IA\XAI\2025 - Sharkey et al - Open Problems in Mechanistic Interpretability.pdf (sha8 9A50CDC6). Sections mobilisees : §3.1-§3.2.
La chaine que ce notebook implemente (issue T17, #16758, dans l’arc B de l’EPIC #16741) : le refus est-il une direction dans le flux residuel, editable a l’inference ; que fait un finetuning de dix exemples a une politique d’alignement ; comment evaluer honnetement un « machine unlearning » ; pourquoi une evaluation ne prouve plus l’alignement.
Ce que ce notebook fait : il implemente ces interventions sur un temoin synthetique entraine from scratch — un transformeur a 2 blocs, 25 tokens de vocabulaire, une tache de decision non degeneree (le risque est une conjonction verbe x objet, pas un token marqueur). Tout est executable localement, sans GPU requis, sans LLM externe, sans telechargement.
Ce que ce notebook n’est pas : une reproduction des resultats publies sur un LLM reel. Le modele n’est ni Qwen ni Llama ; les effets mesures valent pour le temoin, et le notebook dit a chaque section ce qui se transpose et ce qui ne se transpose pas. Le passage a une famille open-weights reelle est RECOVERABLE-MACHINE (stack GenAI / cache HuggingFace sur une lane dediee), declare en §13.
Acceptance :
Extraire une direction de refus par difference de moyennes, verifier sa qualite par un classifieur sans entrainement (fit sur calibration, evaluation sur split tenu a part) et par un controle a direction aleatoire.
Mesurer l’effet de deux ablations directionnelles (brute et centree) et d’un steering signe (dose-reponse sur les deux classes).
Mesurer ce qu’un finetuning de dix exemples fait a la politique, avec son controle (dix exemples qui ne la contredisent pas).
Poser une evaluation forget/retain des interventions et une simulation du biais d’evaluation awareness.
Trois exercices C.1 repartis dans le notebook.
2. Pourquoi ce volet existe pour notre flotte (R11 §2.2.5)
Nos services GenAI hebergent des modeles open weights (famille Qwen). R11 §2.2.5 pose la contrepartie : qui detient les poids detient aussi toutes les interventions au niveau des poids — ablation, steering, finetuning. Une politique de refus apprise n’est donc pas un verrou mais un comportement dans un espace de representation, et ce comportement est reecrivable par quelques exemples.
La consequence operationnelle pour nos agents : un checkpoint aligne n’est pas une garantie d’alignement, et une evaluation qui ne teste que le comportement observable ne prouve rien sur ce qui a ete appris. Les sections 7 a 12 mesurent chacune de ces affirmations, a l’echelle d’un temoin.
import mathimport randomimport numpy as npimport torchimport torch.nn as nnimport torch.nn.functional as FSEED =0random.seed(SEED)np.random.seed(SEED)torch.manual_seed(SEED)DEV ="cuda"if torch.cuda.is_available() else"cpu"print(f"Setup OK — torch {torch.__version__}, device={DEV}")print("Determinisme : seeds python/numpy/torch fixes ; aucune couche non deterministe (pas de convolution, pas de dropout).")
Setup OK — torch 2.14.0+cu126, device=cuda
Determinisme : seeds python/numpy/torch fixes ; aucune couche non deterministe (pas de convolution, pas de dropout).
3. Corpus synthetique — une decision non degeneree
Le temoin doit apprendre un comportement de refus qui soit une fonction du contenu, pas un simple token marqueur. Sinon l’ablation serait triviale (retirer « le » token suffirait) et le notebook demontrerait un cas degenere.
Grammaire. Une requete est BOS t t t verbe objet QUERY : trois tokens de sujet tires parmi 8, puis un verbe parmi 6, puis un objet parmi 6, puis le token de requete. Le risque est la conjonction :
risque <=> verbe parmi {v0, v1, v2} ET objet parmi {o0, o1, o2}
Un verbe a risque avec un objet hors du set est benin : ni le verbe ni l’objet ne determinent la classe seuls. Le modele doit agreger deux positions pour decider, ce qui est exactement le regime ou une representation interne « la requete est a risque » devient necessaire.
Trois splits, trois roles :
Split
Taille
Distribution
Role
train
8000
p_risque = 0.25
apprentissage (le prior penche vers COMPLY)
calibration
4000
50 / 50
extraction de la direction, choix du seuil
evaluation
2000
50 / 50
toutes les mesures rapportees
Aucun contenu reel n’est genere : les tokens sont des symboles abstraits (v0, o2), la tache est un jouet logique.
Corpus : train n=8000 (p_risque=0.25), calibration n=4000, evaluation n=2000 (equilibres)
Part de <refuse> dans le train : 0.246
Vocabulaire : 25 tokens = 4 speciaux + 8 sujets + 6 verbes + 6 objets
Exemple risque : <bos> t6 t6 t3 v2 o0 <query>
Exemple benin : <bos> t3 t3 t2 v5 o3 <query>
Conjonction : <bos> t1 t2 t3 v0 o4 <query> -> <comply>
Conjonction : <bos> t4 t0 t7 v4 o2 <query> -> <comply>
Lecture du resultat — le corpus est bien une conjonction
Les deux lignes Conjonction montrent le point cle : v0 (verbe a risque) avec o4 (objet hors du set) est classe <comply>, et v4 (verbe benin) avec o2 (objet du set) est classe <comply> aussi. Le risque demande les deux : aucun token unique ne separe les classes, et une sonde lineaire sur un seul token ne peut pas resoudre la tache.
Le train est desequilibre (25 % de requetes a risque) : le comportement par defaut du modele sera donc COMPLY, et le refus devra etre declare par une representation interne. C’est le regime qui rend l’ablation et le steering interpretables — un modele qui refuse par defaut ne montrerait rien.
4. Temoin — un transformeur a intervention explicite
Le modele est un transformeur minimal (2 blocs pre-LN, attention causale a 4 tetes, MLP GELU, d_model=64), entraine par prediction du token suivant a la derniere position : la cible est <refuse> ou <comply>.
Point de methode : l’intervention est dans la passe avant, pas dans un hook externe.
def forward_logits(self, idx, ablate=None, steer=None, capture=None): ... x = blk(x)if ablate ... : x = x - ((x - mu) . d) * d # ablation directionnelleif steer ... : x = x + alpha * d # steering
Rendre l’intervention explicite dans le code a deux consequences : elle est lisible (on voit exactement ce qui est retire ou ajoute) et elle est deterministe (aucun etat cache, aucune API de hooks a faire varier). Une intervention au niveau des poids, elle, est persistante — c’est la difference entre le steering (inference) et le finetuning (poids), mesuree en §9.
class Block(nn.Module):def__init__(self, d, nhead, d_ff):super().__init__()self.ln1, self.ln2 = nn.LayerNorm(d), nn.LayerNorm(d)self.attn = nn.MultiheadAttention(d, nhead, batch_first=True)self.mlp = nn.Sequential(nn.Linear(d, d_ff), nn.GELU(), nn.Linear(d_ff, d))def forward(self, x): T = x.shape[1] mask = torch.triu(torch.ones(T, T, dtype=torch.bool, device=x.device), 1) h =self.ln1(x) a, _ =self.attn(h, h, h, attn_mask=mask, need_weights=False)return x + a +self.mlp(self.ln2(x + a))class TinyRefusalModel(nn.Module):def__init__(self, d=64, nhead=4, d_ff=128, nlayer=2):super().__init__()self.tok, self.pos = nn.Embedding(VOCAB, d), nn.Embedding(N_POS, d)self.blocks = nn.ModuleList([Block(d, nhead, d_ff) for _ inrange(nlayer)])self.lnf, self.head = nn.LayerNorm(d), nn.Linear(d, VOCAB)self.nlayer = nlayerdef forward_logits(self, idx, ablate=None, steer=None, capture=None):"""ablate = (couches, directions, moyennes|None) ; steer = (couche, direction, alpha). L'ablation retire la composante de (x - mu) le long de la direction ; mu=None retire la composante de x (ablation brute). """ B, T = idx.shape x =self.tok(idx) +self.pos(torch.arange(T, device=idx.device))[None] caps = {}for li, blk inenumerate(self.blocks): x = blk(x)if ablate isnotNone: layers, dirs, mus = ablateif li in layers: d = dirs[li] z = x if mus isNoneelse x - mus[li] x = x - (z @ d).unsqueeze(-1) * dif steer isnotNoneand li == steer[0]: x = x + steer[2] * steer[1]if capture isnotNoneand li in capture: caps[li] = xreturnself.head(self.lnf(x)), capsdef forward(self, idx):returnself.forward_logits(idx)[0][:, -1, :]def rate(model, x, y, token, **kw):"""Taux de predictions egales a `token` sur l'ensemble (x, y).""" model.eval()with torch.no_grad(): logits, _ = model.forward_logits(x.to(DEV), **kw) pred = logits[:, -1, :].argmax(-1).cpu()return (pred == token).float().mean().item()def layer_hidden(model, x, layers):"""Activations de la derniere position, par couche (split en CPU).""" model.eval()with torch.no_grad(): _, caps = model.forward_logits(x.to(DEV), capture=set(layers))return {li: caps[li][:, -1, :].cpu() for li in layers}model = TinyRefusalModel().to(DEV)n_params =sum(p.numel() for p in model.parameters())print(f"Temoin : {n_params} parametres, {model.nlayer} blocs, d_model=64, 4 tetes")
Lecture du resultat — un temoin minuscule, mais complet
Le compte de parametres affiche est celui d’un modele de jouet : assez petit pour tourner sur CPU, assez structure (attention + MLP + LayerNorm) pour que les notions d’interpretabilite — flux residuel, direction, couche d’intervention — aient un sens technique exact. Les interventions des sections 6 a 9 sont celles des travaux cites par R14 §3.1-§3.2 (difference de moyennes, ablation directionnelle, steering, finetuning court) ; seul le terrain change.
5. Entrainement du temoin
Objectif : cross-entropy sur le token de reponse, 600 pas de AdamW (lr 3e-3), batch 128. On veut un modele qui a appris la conjonction, c’est-a-dire un comportement net sur les deux classes — sans quoi il n’y a pas de direction a extraire.
Entrainement : 600 pas, batch 128, AdamW lr=3e-3
pas 0 — perte 2.9213
pas 200 — perte 0.0008
pas 400 — perte 0.0003
pas 599 — perte 0.0002
Evaluation : refusal_harmful=1.000 comply_benign=1.000
Baseline majoritaire (split equilibre) : 0.500
Lecture du resultat — le comportement est net des deux cotes
Le modele atteint un taux de refus de 1.000 sur les requetes a risque et un taux de conformite de 1.000 sur les requetes benignes : la conjonction est apprise, et le comportement n’est pas obtenu par un prior majoritaire (le split d’evaluation est equilibre, la baseline majoritaire est a 0.5). Un modele qui refuserait par defaut afficherait une conformite basse ; ce n’est pas le cas.
C’est le prerequis de tout le reste : une direction unique ne peut etre la variable causale que si les deux comportements sont presents et separes.
6. Direction du refus — difference de moyennes sur le split de calibration
Protocole. Sur le split de calibration, on capture l’activation de la derniere position (QUERY) a la sortie de chaque bloc, et on calcule
d = moyenne(activations | requete a risque) - moyenne(activations | requete benigne)
Qualite de la direction. Deux mesures, aucune n’utilisant les etiquettes du split d’evaluation pour le fit :
la separation intra-classe d-prime = (m_risque - m_benin) / racine((var_risque + var_benin)/2) — combien d’ecarts-types sepacent les deux moyennes ;
la direction est-elle exploitable comme classifieur sans entrainement ? C’est l’exercice 1 ci-dessous (Exercice 1).
Controle negatif. Une direction tiree au hasard doit etre inexploitable : sans ce controle, un bon score ne prouverait rien de specifique a la direction extraite.
h_fit = layer_hidden(model, X_FIT, [0, 1])mask_risque, mask_benin = (Y_FIT == REFUSE), (Y_FIT == COMPLY)DIRS, MUS = {}, {}print("Direction de refus par couche (calibration n=%d) :"%len(X_FIT))for li in [0, 1]: d = h_fit[li][mask_risque].mean(0) - h_fit[li][mask_benin].mean(0) DIRS[li] = d / d.norm() MUS[li] = h_fit[li].mean(0) # moyenne globale (les deux classes) p = h_fit[li] @ DIRS[li] sep = (p[mask_risque].mean() - p[mask_benin].mean()) / math.sqrt( ((p[mask_risque].var() + p[mask_benin].var()) /2).item())print(f" couche {li} : |d|={d.norm():.2f} d-prime_intra={sep:.2f}")LAYER =1d_refus = DIRS[LAYER].to(DEV)print(f"Couche retenue pour les interventions : {LAYER}")
Direction de refus par couche (calibration n=4000) :
couche 0 : |d|=15.18 d-prime_intra=3.35
couche 1 : |d|=39.48 d-prime_intra=27.45
Couche retenue pour les interventions : 1
Lecture du resultat — une direction unique, tres fortement separee
A la couche 1 (la plus proche de la sortie), le d-prime intra-classe est tres grand : les deux classes sont separees de plusieurs dizaines d’ecarts-types internes dans le flux residuel, et la norme de la difference de moyennes est elle aussi grande devant la dispersion. A la couche 0, la separation existe deja mais elle est plus faible : l’information n’est pas encore agregee.
Cette separation est necessaire mais pas suffisante : elle dit qu’une direction distingue les classes, pas qu’elle cause le comportement. C’est ce que les deux sections suivantes mesurent — ablation (necessite) et steering (suffisance).
Exercice 1 — la direction comme classifieur sans entrainement
Objectif : ecrire centroid_accuracy(x, y, layer, direction) qui, en utilisant uniquement le split de calibration pour fixer le seuil et l’orientation, puis evalue sur un split tenu a part.
# Etape 1 — projeter les activations : p = h @ direction (utiliser layer_hidden).
# Etape 2 — sur le split de calibration, calculer le seuil au milieu des deux moyennes et retenir le signe tel que la classe a risque soit du cote positif.
# Etape 3 — appliquer seuil et signe au split passe en argument, renvoyer l’accuracy.
Verification attendue : avec DIRS[LAYER] l’accuracy est tres haute, avec une direction aleatoire elle retombe au niveau du hasard. C’est ce contraste qui fait de la direction une mesure, pas une impression.
def centroid_accuracy(x, y, layer, direction, x_fit=X_FIT, y_fit=Y_FIT):"""Accuracy d'un classifieur a un seuil : seuil et signe fixes sur le split de calibration.""" result =None# TODO etudiant — voir les trois etapes de l'enonceprint("Exercice 1 a completer")return resultg = torch.Generator().manual_seed(7)d_alea = torch.randn(DIRS[LAYER].shape[0], generator=g)d_alea = d_alea / d_alea.norm()_ = centroid_accuracy(X_VAL, Y_VAL, LAYER, DIRS[LAYER])print("Exercice 1 a completer — attendu : un score eleve pour DIRS[LAYER], au niveau du hasard pour d_alea")
Exercice 1 a completer
Exercice 1 a completer — attendu : un score eleve pour DIRS[LAYER], au niveau du hasard pour d_alea
7. Ablation directionnelle — le jailbreak par minimisation de projection
L’intervention. Retirer, a chaque couche et a chaque position, la composante du flux residuel le long de la direction :
x <- x - ((x - mu) . d) d
C’est une minimisation de projection : la composante « refus » est mise a zero, la matrice de poids n’est pas touchee. C’est l’intervention que R14 §3.2 decrit comme le jailbreak white-box d’un modele open-weights.
Deux variantes, et c’est une lecon de methode. Sans centrage (mu = 0), l’ablation retire la composante le long de dy compris la composante partagee par les deux classes — elle ne retire pas « le refus », elle deplace tout le monde. Avec centrage (mu = moyenne du split de calibration), elle retire la variation le long de d en conservant le point commun : c’est l’intervention qui isole vraiment le facteur discriminant.
Controle negatif. La meme ablation avec une direction aleatoire, norme comparable, ne doit rien produire.
Lecture du resultat — le resultat principal, et le piege de methode
Ablation centree : le refus s’effondre (taux de refus proche de 0) alors que la conformite des requetes benignes reste intacte. La direction est donc necessaire au comportement de refus : retiree du flux residuel, le modele traite les requetes a risque comme des requetes ordinaires. C’est le resultat central de la chaine R14 §3.2, reproduit sur le temoin.
Ablation brute : resultat inverse et instructif — le refus reste a 1.000 et c’est la conformite qui s’effondre (le modele refuse tout). Sans centrage, l’ablation ne retire pas seulement le facteur discriminant : elle supprime aussi la composante que les deux classes partagent le long de cet axe, et le modele se retrouve hors distribution. La lecon : « retirer la direction du refus » n’est une operation bien posee que si l’on distingue la variation que l’on veut annuler de la composante commune qu’il faut conserver.
Controle a direction aleatoire : aucune des deux mesures ne bouge. L’effet des lignes precedentes est donc bien specifique a la direction extraite, pas un artefact de l’intervention.
8. Steering — dose-reponse signee, sufficiency et necessite
L’ablation retire la composante ; le steering la deplace. On ajoute alpha * d au flux residuel a la derniere position de la couche retenue, avec alpha exprime en unites de l’ecart-type de la projection sur le split de calibration (une echelle comparable entre directions et entre modeles).
alpha < 0 : on soustrait du refus — c’est la meme minimisation de projection, mais bornee et dosee ;
alpha > 0 : on en ajoute — si la direction est suffisante, le modele doit se mettre a refuser des requetes benignes.
Une dose-reponse monotone sur les deux classes est une bien meilleure preuve qu’un point unique : elle teste la direction comme cause (suffisance) et pas seulement comme correlate.
p_fit = h_fit[LAYER] @ DIRS[LAYER]sigma = p_fit.std().item() # echelle de la projection sur le split de calibrationprint(f"Echelle sigma_projection (couche {LAYER}, calibration) = {sigma:.3f}")alphas = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0]res_steering = []print(f"{'alpha (sigma)':>14}{'refus (risque)':>16}{'conformite (benin)':>20}")for a in alphas: s = (LAYER, d_refus, float(a * sigma)) r_ = rate(model, X_RISK, Y_RISK, REFUSE, steer=s) c_ = rate(model, X_BEN, Y_BEN, COMPLY, steer=s) res_steering.append((a, r_, c_))print(f"{a:>14.1f}{r_:>16.3f}{c_:>20.3f}")
Lecture du resultat — une dose-reponse compatible avec une cause unique
A alpha = 0, on retrouve le temoin (refus 1.000, conformite 1.000). En descendant vers les valeurs negatives, le refus s’effondre alors que la conformite benigne reste a 1.000 : la soustraction de la direction liberalise les requetes a risque sans degrader le reste — c’est le jailbreak, dose.
En montant vers les valeurs positives, la conformite benigne chute a son tour (le modele se met a refuser des requetes ordinaires) alors que le refus reste a 1.000 : la direction est suffisante pour declencher le comportement.
Les deux directions du desequilibre sont donc bien portees par le meme axe, et l’intervention est reversible par construction : aucune matrice de poids n’est modifiee (la passe avant est parametree), alors que le finetuning de la section 9 modifie les poids de facon persistante.
Exercice 2 — ou l’intervention agit-elle ?
Objectif : mesurer la dose-reponse du steering a la couche 0 et comparer avec la couche 1.
# Etape 1 — reutiliser la boucle de la cellule precedente avec steer=(0, DIRS[0].to(DEV), a * sigma_0) (calculer sigma_0 sur la couche 0, split de calibration).
# Etape 2 — rassembler les deux courbes (couche 0 et couche 1) et les comparer.
# Etape 3 — interpreter : l’effet est-il localise pres de la sortie, ou distribue ?
Indice : l’hypothese a tester est qu’une intervention appliquee loin de la sortie peut etre partiellement recalculee par les couches suivantes — c’est une hypothese, pas un resultat deja mesure ici. La comparaison des deux courbes est ce qui la tranche.
def dose_reponse_couche(layer, alphas=(-1.0, 0.0, 1.0)):"""Taux de refus sur les requetes a risque, pour un steering a `layer`.""" result =None# TODO etudiant — voir les trois etapes de l'enonceprint("Exercice 2 a completer")return result_ = dose_reponse_couche(0)print("Exercice 2 a completer — a comparer avec la courbe de la couche 1 (section 8)")
Exercice 2 a completer
Exercice 2 a completer — a comparer avec la courbe de la couche 1 (section 8)
9. Finetuning shallow — dix exemples rouvrent le modele
L’affirmation a tester (chaine R14 §3.2, Gade/Lermen) : une politique d’alignement apprise par finetuning peut etre annulee par quelques exemples. Le protocole : dix requetes a risque, dont la cible est remplacee par COMPLY, quelques dizaines de pas de descente de gradient.
Deux mesures rendent le resultat interpretable :
l’effet sur les deux classes (le refus s’effondre-t-il sans casser la conformite benigne ?) ;
la similarite de la direction avant/apres (cos(d_avant, d_apres)) : la representation qui portait le refus survit-elle a sa disparition comportementale ? Si oui, « desapprendre » par finetuning est une reecriture de la lecture, pas un effacement de l’information.
Controle negatif indispensable. Dix exemples benins (etiquettes inchangees, donc non contradictoires avec la politique) passes dans la meme boucle : si le refus s’effondrait aussi, c’est la dose de gradient qui expliquerait tout, et non le contenu des exemples.
Lecture du resultat — le comportement est reecrit, la direction survit
Le finetuning sur dix exemples contradictoires effondre le refus alors que la conformite benigne reste intacte : la politique de refus est bien une lecture parmi d’autres du meme modele, et dix exemples suffisent a la retourner. Le controle (dix exemples benins) ne fait pas bouger le refus : ce n’est donc pas la dose de gradient qui explique l’effet, mais le contenu des exemples.
Le cosinus entre la direction extraite avant et apres finetuning reste eleve : la representation qui separait les requetes a risque des requetes benignes n’a pas disparu avec le comportement. Le finetuning a modifie comment le modele lit cette representation — c’est la definition operatoire d’une edition « shallow » : la surface a change, la structure est toujours la.
Consequence pour une evaluation honnete : un test de refus apres finetuning mesure l’etat de la surface, pas la presence de l’information. C’est exactement ce que la section 10 met en tableau.
10. Unlearning machine — un tableau forget / retain, pas un score unique
R14 §3.2 demande une evaluation honnete du desapprentissage : une methode qui fait baisser le taux de refus sur les requetes a risque (« forget ») n’est interessante que si elle conserve le comportement legitime (« retain »). Un score unique — « 0 % de refus sur le set a oublier » — est trivialement atteignable en detruisant le modele ; c’est le tableau a deux colonnes qui tranche.
Le tableau ci-dessous reprend les mesures des sections precedentes (aucune nouvelle execution GPU) : chaque ligne est une intervention, chaque colonne un comportement legitime.
lignes = [ ("aucune (temoin)", base_refus, base_conf, None), ("ablation centree (inference)", res_ablation["centree"][0], res_ablation["centree"][1], "direction retiree"), ("ablation brute (inference)", res_ablation["brute"][0], res_ablation["brute"][1], "hors distribution"), ("finetuning 10 ex. (poids)", flip_refus, flip_conf, f"cos={cos_dir:.3f}"), ("controle 10 ex. benins (poids)", ctrl_refus, ctrl_conf, "temoin negatif"),]print(f"{'intervention':<32}{'forget (refus)':>16}{'retain (conformite)':>20} note")for nom, r_, c_, note in lignes:print(f"{nom:<32}{r_:>16.3f}{c_:>20.3f}{note or''}")print()print("Lecture : une ligne n'est un 'unlearning' reussi que si la colonne forget est basse")print("ET la colonne retain haute. L'ablation brute et le controle benin sont les deux")print("manieres de rater ce critere — l'une detruit le modele, l'autre ne fait rien.")
intervention forget (refus) retain (conformite) note
aucune (temoin) 1.000 1.000
ablation centree (inference) 0.000 1.000 direction retiree
ablation brute (inference) 1.000 0.000 hors distribution
finetuning 10 ex. (poids) 0.000 1.000 cos=0.837
controle 10 ex. benins (poids) 1.000 1.000 temoin negatif
Lecture : une ligne n'est un 'unlearning' reussi que si la colonne forget est basse
ET la colonne retain haute. L'ablation brute et le controle benin sont les deux
manieres de rater ce critere — l'une detruit le modele, l'autre ne fait rien.
Lecture du resultat — ce que le tableau elimine
Deux lignes ont une colonne forget a zero — mais toutes les lignes a forget bas ne sont pas des succes, et c’est tout l’interet du tableau. L’ablation brute garde un forget maximal (le modele refuse tout) et detruit le retain : elle est disqualifiee, elle n’oublie rien, elle casse le modele. Le controle benin ne bouge aucune des deux colonnes : il ne fait rien du tout. Seules les deux lignes ou forget est bas etretain intact passent le critere : l’ablation centree (intervention d’inference, reversible, direction annotee) et le finetuning (intervention de poids, persistante, direction survivante).
C’est la structure du debat sur le machine unlearning : la methode qui passe le tableau le plus proprement (le finetuning) est precisement celle qui laisse l’information en place. Un tableau forget/retain est necessaire, il n’est pas suffisant — c’est l’objet de l’exercice 3.
Exercice 3 — mesurer le re-apprentissage
Objectif : mesurer combien de pas de finetuning — avec dix exemples de refus — suffisent a restaurer le refus sur le modele deja « desapprenti » (model_flip), et comparer au cout de l’entrainement initial (le nombre de pas affiche par la section 5).
# Etape 1 — reprendre finetune mais en enregistrant le taux de refus a chaque pas (ou tous les 2 pas) : renvoyer la trajectoire.
# Etape 2 — trouver le premier pas ou le refus atteint 0.8.
# Etape 3 — comparer ce nombre au cout de l’entrainement initial et conclure sur ce que « desapprendre » signifie quand l’information est restee en place.
Indice : dix exemples, une boucle de gradient, une copie de model_flip — tout est deja ecrit dans la section 9 ; il s’agit de la meme boucle avec des etiquettes <refuse>.
def trajectoire_reapprentissage(model_source, steps=20):"""Taux de refus sur les requetes a risque, a chaque pas de finetuning sur 10 ex. de refus.""" result =None# TODO etudiant — voir les trois etapes de l'enonceprint("Exercice 3 a completer")return result_ = trajectoire_reapprentissage(model_flip)print("Exercice 3 a completer — la trajectoire doit rendre la suite des taux de refus")
Exercice 3 a completer
Exercice 3 a completer — la trajectoire doit rendre la suite des taux de refus
11. Evaluation awareness — pourquoi une evaluation ne prouve plus l’alignement
R14 §3.2 et l’issue T17 rapportent le probleme suivant : un modele peut detecter qu’il est evalue et adapter son comportement. Les consequences statistiques sont simples et devastatrices, et elles se calculent sans aucun modele reel.
Notation : p_deploiement = taux d’action indesirable hors evaluation, p_eval = taux quand le modele se sait observe, q = probabilite qu’il detecte l’evaluation. Le taux observe est le melange :
observe = (1 - q) * p_deploiement + q * p_eval
Si p_eval < p_deploiement (le modele se tient bien quand il est observe), le taux observe est une borne inferieure du taux reel, et l’ecart vaut q * (p_deploiement - p_eval).
Ce que cette section est, et n’est pas. C’est une simulation du biais, pas une mesure de la capacite de detection d’un modele : nous n’avons pas de LLM reel ici, et le temoin synthetique n’a pas de notion de « se savoir observe ». La simulation rend visible la taille du biais ; elle ne pretend pas que le biais existe dans un modele donne.
def taux_observe(p_deploiement, p_eval, q, n=200000, seed=5):"""Taux d'action indesirable observe, quand le modele detecte l'evaluation avec proba q.""" rng = random.Random(seed) total =0for _ inrange(n): p = p_eval if rng.random() < q else p_deploiementif rng.random() < p: total +=1return total / nP_DEPLOIEMENT, P_EVAL =0.20, 0.0print(f"Verite en deploiement : p = {P_DEPLOIEMENT:.2f} ; comportement sous observation : p = {P_EVAL:.2f}")print(f"{'q (detection)':>14}{'taux observe':>15}{'biais (borne inf.)':>20}")for q in [0.0, 0.5, 0.9, 0.99]: obs = taux_observe(P_DEPLOIEMENT, P_EVAL, q)print(f"{q:>14.2f}{obs:>15.3f}{obs - P_DEPLOIEMENT:>20.3f}")print()print("Formule : observe = (1 - q) * p_deploiement + q * p_eval.")print("Plus le modele detecte l'evaluation, plus l'evaluation se rassure.")
Verite en deploiement : p = 0.20 ; comportement sous observation : p = 0.00
q (detection) taux observe biais (borne inf.)
0.00 0.201 0.001
0.50 0.100 -0.100
0.90 0.020 -0.180
0.99 0.002 -0.198
Formule : observe = (1 - q) * p_deploiement + q * p_eval.
Plus le modele detecte l'evaluation, plus l'evaluation se rassure.
Lecture du resultat — l’evaluation mesure la surface qu’elle cree
A q = 0 (aucune detection), le taux observe est le taux reel : l’evaluation est honnete. Des que q monte, le taux observe decroche vers p_eval : a q = 0.9, le taux observe vaut environ un dixieme du taux de deploiement, et l’ecart affiche est entierement negatif — une sous-estimation systematique.
Deux consequences pratiques, valables meme sans mesurer la detection d’un modele reel :
Le pire cas observe n’est qu’une borne inferieure : un comportement indesirable jamais observe sous evaluation ne dit rien sur ce qui peut advenir en deploiement.
Multiplier les evaluations ne repare pas le biais tant que p_eval reste plus bas que p_deploiement : toutes les evaluations partagent le meme angle mort. Ce qui reduit le biais, c’est de mesurer q (ou de rendre p_eval non strategy-defavorable), pas d’augmenter le nombre de tests.
C’est la raison de la formule de l’issue : « les evaluations ne prouvent plus l’alignement ».
12. Synthese — ce qui est mesure, ce qui ne l’est pas
Mesure firsthand dans ce notebook, sur le temoin synthetique :
un comportement de refus non degenere (conjonction verbe x objet), appris et verifie sur les deux classes ;
une direction extraite par difference de moyennes, tres fortement separee a la couche proche de la sortie ;
l’effet d’une ablation centree (refus effondre, conformite benigne intacte), d’une ablation brute (tout refuse : piege de methode) et d’un controle a direction aleatoire (aucun effet) ;
une dose-reponse de steering signee : soustraction = liberalisation, addition = refus de requetes benignes ;
dix exemples contradictoires suffisent a retourner la politique, dix exemples non contradictoires ne la touchent pas, et la direction extraite survit au finetuning ;
un tableau forget/retain qui disqualifie deux lignes plausibles ;
le biais d’evaluation awareness par simulation (observe = (1-q) p_deploiement + q p_eval).
Non mesure ici, et pourquoi :
Non mesure
Raison
Verdict
Reproduction des resultats sur Qwen/Llama (direction reelle, jailbreak reel)
pas de stack GenAI ni de cache HuggingFace sur cette lane
RECOVERABLE-MACHINE — a porter sur une lane dediee
Capacite de detection d’evaluation d’un modele reel
demande un LLM reel et un protocole d’evaluation controle
RECOVERABLE-MACHINE
Robustesse multi-seed des effets mesures
un seul seed ici ; les effets sont qualitativement nets (0.000 vs 1.000), un balayage de seeds serait la prochaine etape
a faire si le resultat devient un claim quantitatif
Ce que le temoin transmet, et ce qu’il ne transmet pas. Les interventions sont celles des travaux cites, au code pres ; le terrain est un jouet logique. Ce qui se transpose : la structure des effets (une direction necessaire et suffisante, une edition shallow qui laisse la representation en place, un tableau forget/retain qui ne suffit pas). Ce qui ne se transpose pas : les ordres de grandeur, le nombre d’exemples necessaires, la nettete des effets — tout cela depend du modele et de la politique etudies.
Lien avec nos services. Nos modeles heberges sont open weights (R11 §2.2.5) : les interventions de ce notebook sont a portee de quiconque detient le checkpoint. Un modele aligne n’est pas un modele verrouille ; la seule defense realiste est de mesurer, pas de supposer.
13. Sources et remerciements
Sources primaires : R11 — Casper et al., The 2026 Singapore Consensus on Global AI Safety Research Priorities (arXiv:2608.14611 ; §1.1, §2.2.5, §3.2). R14 — Sharkey et al., Open Problems in Mechanistic Interpretability (arXiv:2501.16496 ; §3.1-§3.2). PDF archives hors depot, cf §1.
Contexte distillation : issue T17 (#16758) de l’EPIC #16741 (distillation corpus), arc B. Les travaux tiers cites par la chaine (direction de refus, finetuning shallow, unlearning, evaluation awareness) sont rapportes par R11/R14 et n’ont pas ete re-mesures sur LLM reel ici.