# Parameters
nb_name = "ICT-35b-HumorCausalPairs-SAE-Python"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.
ICT-35b – HumorCausalPairs-SAE : paires minimales, substrat SAE (#14035, tranche 1)
Le pilote ICT-35 a mesure l’humour sur un substrat HLS lexical (sklearn, 6 dimensions) : verdict INCONCLUSIVE borne. Ce notebook passe au substrat SAE (sparse autoencoder Qwen-Scope) avec un design experimental plus fin : des paires minimales ou seule la punchline varie.
Le design pre-regle (issue #14035, commentaire c.5743321902) :
- pour chaque blague, trois textes partagent le meme setup token a token : le texte humour commite, sa punchline neutralisee (unfun : continuation coherente qui tue l’incongruite en gardant le registre), et un controle de distance d’edition (ctrl_edit : autre continuation coherente non-humorale de taille comparable) ;
- la zone mesuree est la zone d’edition (tokens apres le prefixe commun exact) ;
- trois jambes statistiques independantes :
delta_pair(distance L1 humour vs unfun) confrontee a un null croise (les unfun brasses entre paires, 2048 tirages, ecart c.5743498870),delta_ctrl(controle de distance d’edition), et les z de features (flip de signe intra-paire).
L’ecart c.5743512349 est documente avant interpretation : le critere delta_pair > p99 est mal dirige pour une hypothese de shift systematique (une composante constante est invisible au brassage inter-paires). Le verdict repose donc sur l’ensemble des jambes, pas sur ce seul seuil.
# -*- coding: utf-8 -*-
# Setup. Reproduction du corpus humour (meme geste que le pilote ICT-35) :
# les cellules code [26..33] de la section echelle du banc consolide
# (GameTheory-18c, absorption #14032) sont
# executees dans un namespace isole, sans editer le notebook source.
# La reproduction exige OPENROUTER_API_KEY dans l'environnement (les cellules
# endpoint du banc la verificent ; aucune n'appelle l'API ici).
from collections import Counter
from pathlib import Path
import json
import sys
import numpy as np
import pandas as pd
ROOT = Path.cwd()
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from ict.humor_pairs import (
build_pairs, build_prompts_json, banc_humour_path, label_distribution,
load_corpus_dur, measure_humor_differential, validate_pairs,
)
corpus = load_corpus_dur()
dist = label_distribution(corpus)
print(f"banc humour : {banc_humour_path().name}")
print(f"CORPUS_DUR : {len(corpus)} instances")
for lab, n in dist.most_common():
print(f" {lab:32s} {n}")[setup] Catégories : ['humour_reussi', 'rire_sans_recadrage', 'recadrage_sans_rire', 'offensif_compris_non_partage', 'rien']
[setup] LLM endpoint : https://openrouter.ai/api/v1
[setup] LLM model : anthropic/claude-haiku-4.5
[fetch] downloaded 558033 bytes -> argumentum_scenarii.csv
[fetch] upstream master @1b4c831bfc : docs(regles): #458 grain 9 -- outil portable, controle de citation mot pour mot,
[parse] 167 scénarios Argumentum chargés
[parse] catégories : {'histoire': 17, 'mythologie': 27, 'relation intime': 36, 'vie professionnelle': 30, 'vie personnelle': 25, 'pop culture': 18, 'politique': 14}
[parse] sous-catégories (21) : {'antiquité': 6, 'moyen-âge et temps modernes': 6, '20e et 21e siècle': 5, 'contes': 10, 'religions': 11, 'littérature': 6, 'drague et séduction': 9, 'vie de couple': 16, 'romance': 11, 'interactions professionnelles': 14, 'relations au travail': 8, 'gestion et administration': 9, 'Bandes dessinées': 5, 'cinéma & télévision': 7, 'science': 6, 'gouvernance': 4, 'manoeuvres et collusion': 6, 'campagne': 4, 'famille et enfance': 8, 'voisins et amis': 11, 'loisirs et espace public': 5}
[parse] 167 instances retenues (champs FR)
[parse] exemple : id=1.1.1 titre='La mère de César et Cléopâtre'
baratineur='Aurelia Cotta, mère de César' -> piocheur='Jules César'
[label] distribution après annotation manuelle : {'rien': 46, 'recadrage_sans_rire': 27, 'humour_reussi': 55, 'rire_sans_recadrage': 25, 'offensif_compris_non_partage': 14}
[corpus] total : 120 instances
[corpus] distribution par label : {'rire_sans_recadrage': 14, 'recadrage_sans_rire': 13, 'rien': 38, 'humour_reussi': 48, 'offensif_compris_non_partage': 7}
[corpus] distribution par source : {'ArgumentumGames/Argumentum': 60, 'blague-manuelle': 30, 'declaration-factuelle': 20, 'edge-case-curated': 10}
[corpus] humour_reussi : 48 instances OK
[corpus] rire_sans_recadrage : 14 instances OK
[corpus] recadrage_sans_rire : 13 instances OK
[corpus] offensif_compris_non_partage : 7 instances OK
[corpus] rien : 38 instances OK
banc humour : GameTheory-18c-Humour-Banc-Python.ipynb
CORPUS_DUR : 120 instances
humour_reussi 48
rien 38
rire_sans_recadrage 14
recadrage_sans_rire 13
offensif_compris_non_partage 7
Lecture 1 – Le corpus reproduit : 120 instances, 48 humour_reussi
La reproduction est deterministe (le banc humour (18c) pose random.seed(42) lui-meme) et cwd-independante : le chargeur resout le cache Argumentum (argumentum_scenarii.csv) dans le dossier ICT-Series puis GameTheory, jamais via un fetch reseau silencieux.
Les 30 paires de la tranche 1 se construisent sur les blagues manuelles (blague-manuelle, 30 instances) et deux one-liners edges (edge-case-curated) ; les 60 instances Argumentum du banc sont exclues du pairing : leur champ texte est tronque a ~150 caracteres par le builder du banc humour (18c) – la punchline n’y est jamais commitee (mesure firsthand, tranche 1).
# Paires minimales : 30 entrees (28 blagues manuelles + 2 one-liners edge).
# Chaque paire : setup verbatim | punchline verbatim | punchline neutralisee |
# controle de distance d'edition. Re-verifiees au chargement (span unique,
# suffixe exact, prefixe commun).
pairs = build_pairs(corpus)
validate_pairs(pairs) # plancher protocolaire : >= 30
payload = build_prompts_json(pairs)
print(f"{len(pairs)} paires validees ; prompts-json : "
+ ", ".join(f"{s}={len(v)}" for s, v in payload.items()))
apercu = pd.DataFrame([
{"id": p["id"],
"n_car_setup": len(p["setup"]), "n_car_humour": len(p["humour"]),
"n_car_unfun": len(p["unfun"]), "n_car_ctrl": len(p["ctrl_edit"])}
for p in pairs
])
apercu.head(8)30 paires validees ; prompts-json : humour=30, unfun=30, ctrl_edit=30
| id | n_car_setup | n_car_humour | n_car_unfun | n_car_ctrl | |
|---|---|---|---|---|---|
| 0 | joke-p02 | 57 | 84 | 99 | 98 |
| 1 | joke-p03 | 40 | 106 | 102 | 102 |
| 2 | joke-p04 | 57 | 75 | 89 | 92 |
| 3 | joke-p05 | 56 | 90 | 82 | 84 |
| 4 | joke-p06 | 52 | 81 | 81 | 90 |
| 5 | joke-p07 | 45 | 96 | 77 | 79 |
| 6 | joke-p08 | 45 | 73 | 87 | 73 |
| 7 | joke-p09 | 50 | 86 | 88 | 90 |
Lecture 2 – Trente paires, trois exclusions documentees
joke-p01etjoke-p17sont exclues : leurs textes committes dans le banc humour (18c) sont malformes (p01 tronquee au milieu du refrain, p17 saut de ligne inline) – la contraintetexte == setup + punchlinene peut pas y etre verifiee ;edge-04etedge-05(one-liners Chapman/Cooper, labelrecadrage_sans_rire) completent a 30 : heterogeneite de label documentee – le critere du pairing est l’incongruite portee par le texte, pas le label comportemental ;- les variantes
unfunetctrl_editdifferent dehumourseulement dans la zone punchline : le prefixe commun exact (verifie token a token sur les traces a la cellule de mesure) garantit que la zone d’edition est disjoncte du setup.
Le controle ctrl_edit repond a l’objection la plus forte contre un design minimal-pair : si l’effet humour etait une consequence mecanique de n’importe quelle edition de cette taille (tokens finaux differents, longueur proche), alors delta_ctrl egalerait delta_pair et l’interpretation “perte d’incongruite” serait indue.
Captures GPU – provenance et pre-enregistrement
Les deux traces sont produites par scripts/extract_sae_traces.py (seul composant torch de la strate, le package ict/ reste numpy-only) sur la machine po-2024 (RTX 3070) :
| trained | control | |
|---|---|---|
| modele | Qwen/Qwen3.5-2B-Base |
idem |
| SAE | Qwen/SAE-Res-Qwen3.5-2B-Base-W32K-L0_50 (d_sae=32768, k=50) |
idem |
| couche | 12/24 (layer_frac 0.5217) |
idem |
| variant | encodage officiel Qwen-Scope (pre = h @ W_enc.T + b_enc ; relu ; topk(50)) |
permutation seeedee des lignes d’input embeddings (le SAE lit un residu dont l’anatomie des positions est detruite) |
| date / seed | 2026-09-19T16:05Z, seed 42 | idem |
Le protocole statistique etait pre-registre (c.5743321902) avant toute mesure, avec deux ecarts documentes sur l’issue avant interpretation : la correction du null (degenere pour L1 sous swap de labels, remplace par le null croise, c.5743498870) et la requalification du seuil delta_pair > p99 (c.5743512349).
# Chargement de la trace trained : manifeste, sparsite reelle (L0 mesure,
# pas suppose).
TRACE_T = ROOT / "traces" / "humor35b_qwen35-2b-base_layer12of24_trained.npz"
TRACE_C = ROOT / "traces" / "humor35b_qwen35-2b-base_layer12of24_control.npz"
data_t = np.load(TRACE_T, allow_pickle=False)
meta_t = json.loads(str(data_t["__meta__"]))
n_entries = sum(1 for k in data_t.files if k.endswith("__topk_ids"))
l0 = np.concatenate([
(data_t[k] > 0).sum(axis=1) for k in data_t.files if k.endswith("__topk_vals")
]).mean()
print(f"trained : {meta_t['model']} x {meta_t['sae_repo']}")
print(f" layer {meta_t['layer']}/{meta_t['n_layers']} (frac {meta_t['layer_frac']:.4f}), "
f"d_sae={meta_t['d_sae']}, k={meta_t['k']}, seed={meta_t['seed']}")
print(f" {n_entries} prompts ({sum(meta_t['prompt_sets'].values())} attends), "
f"{meta_t['n_tokens_total']} tokens au total")
print(f" L0 mesure = {l0:.2f} features actives par token (sur k={meta_t['k']})")trained : Qwen/Qwen3.5-2B-Base x Qwen/SAE-Res-Qwen3.5-2B-Base-W32K-L0_50
layer 12/24 (frac 0.5217), d_sae=32768, k=50, seed=42
90 prompts (90 attends), 1868 tokens au total
L0 mesure = 50.00 features actives par token (sur k=50)
Lecture 3 – 90 prompts, 1868 tokens, L0 = 50.00 mesure
Chaque paire contribue trois prompts (humour, unfun, ctrl_edit) : 30 x 3 = 90 entrees. La sparsite mesuree est exactement k=50 a chaque token : le relu du SAE top-k ne tue aucune des 50 plus grandes activations sur ces prompts, et la densification operee par la couche de mesure est exacte (une feature hors top-50 vaut exactement zero par construction), sans troncature ni approximation.
La mesure pre-registree (measure_humor_differential) s’applique telle quelle : prefixe commun calcule sur les tokens reels (pas sur les caracteres), zone = tokens de punchline, L1 sur les vecteurs de zone moyenne, null croise a 2048 tirages, z de features par flip de signe intra-paire, seed 42.
# Mesure pre-registree -- trace TRAINED.
res_t = measure_humor_differential(TRACE_T, pairs)
for k in ("delta_pair", "delta_ctrl", "null_p99", "ratio_vs_ctrl",
"n_features_over3", "verdict"):
if isinstance(res_t[k], float):
print(f"{k:18s} {res_t[k]:.4f}")
else:
print(f"{k:18s} {res_t[k]}")
print("top-5 |z| :", [(f, round(z, 2)) for f, z in res_t["top_features"][:5]])delta_pair 27.8327
delta_ctrl 28.6169
null_p99 35.8102
ratio_vs_ctrl 0.9726
n_features_over3 0
verdict INCONCLUSIVE
top-5 |z| : [(6677, 2.62), (12444, 2.59), (23779, -2.58), (18944, 2.54), (2919, -2.52)]
Lecture 4 – Verdict TRAINED : INCONCLUSIVE, jambe par jambe
- delta_pair = 27.83 contre un null croise a p99 = 35.81 : la distance humour/unfun est sous le 99e centile du null – les paires ne sont pas plus eloignees que des unfun re-brasses entre paires ;
- ratio_vs_ctrl = 0.97 : la punchline neutralisee n’est pas plus proche du texte humour que ne l’est le controle de distance d’edition (28.62) – aucune specificite de la perte d’incongruite ;
- 0 features |z| > 3 (max : feature 6677, z = +2.62) : aucun shift systematique de direction constante dans l’espace des 32768 features.
Les trois jambes convergent : a ce couple modele x SAE x couche (Qwen3.5-2B, W32K, residu 12/24), la suppression de l’incongruite humorale ne produit pas de deplacement mesurable de la representation moyenne de zone. C’est un negatif honnete, pas un echec de protocole : le protocole etait falsifiable et reste falsifiable (tranches suivantes).
# Mesure pre-registree -- trace CONTROL (SAE lisant un residu a positions permutees).
res_c = measure_humor_differential(TRACE_C, pairs)
for k in ("delta_pair", "delta_ctrl", "null_p99", "ratio_vs_ctrl",
"n_features_over3", "verdict"):
if isinstance(res_c[k], float):
print(f"{k:18s} {res_c[k]:.4f}")
else:
print(f"{k:18s} {res_c[k]}")
print("top-5 |z| :", [(f, round(z, 2)) for f, z in res_c["top_features"][:5]])
top_t = {f for f, _ in res_t["top_features"]}
top_c = {f for f, _ in res_c["top_features"]}
print(f"recouvrement top-10 trained/control : {len(top_t & top_c)} features communes")delta_pair 22.0069
delta_ctrl 22.8715
null_p99 27.6215
ratio_vs_ctrl 0.9622
n_features_over3 0
verdict INCONCLUSIVE
top-5 |z| : [(5680, -2.94), (27159, -2.47), (31556, 2.38), (19573, -2.37), (12486, 2.33)]
recouvrement top-10 trained/control : 0 features communes
Lecture 5 – Le controle clot le debat sur les z residuels
Le controle (permutation seeedee des lignes d’input embeddings, qui detruit l’anatomie des positions sans changer la loi des activations) rend lui aussi INCONCLUSIVE : delta_pair 22.01 sous p99 27.62, ratio 0.96, 0 features |z| > 3.
L’observation decisive : le top-5 du trained (6677, 12444, 23779, 18944, 2919 ; |z| <= 2.62) et celui du control (5680, 27159, 31556, 19573, 12486 ; |z| <= 2.94) sont disjoints, et l’amplitude maximale du trained est sous celle du bruit permute. Les z residuels du trained sont donc interchangeables avec ceux qu’on obtient sans aucune structure linguistique residuelle : il n’y a pas de “presque-signal” a sauver.
Ce que ce negatif borne – et ce qu’il ne borne pas
Borne : au couple Qwen3.5-2B-Base x SAE-W32K x residu 12/24, avec un zone-mean sur la punchline et n=30, la perte d’incongruite humorale est invisible dans les deplacements systematiques de features comme dans la distance globale.
Ne borne PAS : (i) les autres couches (l’incongruite peut etre resolue plus tot ou plus tard dans la pile) ; (ii) le couple 9B/W64K (capacite et largeur de dictionnaire x2) ; (iii) les effets token-level non stationnaires qu’un zone-mean moyenne a zero (le token de resolution vs les tokens de setup de la punchline) ; (iv) les differences de trajectoire (l’ordre des activations au fil de la zone), que la distance L1 sur moyenne ne voit pas. Ces quatre axes sont les tranches 2+ de #14035.
# EXERCICE 1 -- delta par paire : la dispersion cachee derriere la moyenne
#
# delta_pair = 27.83 est une MOYENNE sur 30 paires : la dispersion par paire
# est invisible. Certaines paires pourraient porter un signal local que la
# moyenne dilue.
#
# Indice : reproduire la boucle de zones de measure_humor_differential
# (ict/humor_pairs.py) avec ict.humor_pairs._common_prefix_len et
# _zone_vec, puis np.abs(H - U).sum(axis=1) donne le delta par paire.
# Etape 1 : charger la trace trained (ict.sae_traces.load_traces) et
# construire les matrices H et U [30, d_sae] des zones.
# Etape 2 : calculer le delta par paire, afficher les 5 paires au delta max
# et les 5 au delta min (argsort, ids de paires).
# Etape 3 : tester la correlation delta x longueur de zone (nombre de tokens
# apres le prefixe commun) : un delta purement surfacique croit
# avec la longueur ; un delta d'incongruite, pas necessairement.
H = None
U = None
delta_par_paire = None
print("Exercice a completer")Exercice a completer
# EXERCICE 2 -- zone reduite au DERNIER token de punchline
#
# La mesure pre-registree moyenne la zone ENTIERE (tous les tokens apres le
# prefixe commun). Hypothese tranches 2+ : l'effet, s'il existe, est porte par
# le token de resolution (souvent le dernier), qu'un zone-mean dilue.
#
# Indice : _zone_vec(entry, start, d_sae) accepte n'importe quel start ; pour
# se limiter au dernier token : start = len(entry["tokens"]) - 1.
# Etape 1 : reconstruire H, U, C en zone dernier-token (meme boucle que
# l'exercice 1, start different).
# Etape 2 : recalculer delta_pair et delta_ctrl sur ces vecteurs et les
# comparer aux valeurs zone-entiere (27.83 / 28.62).
# Etape 3 : interpreter : le ratio delta_pair/delta_ctrl bouge-t-il vers une
# specificite de l'incongruite, ou reste-t-il ~1 ?
H_last = None
U_last = None
delta_pair_last = None
delta_ctrl_last = None
print("Exercice a completer")Exercice a completer
# EXERCICE 3 -- stabilite des top features trained vs control
#
# Le verdict global est INCONCLUSIVE, mais la question fine reste : les
# features les plus extremees du trained (6677, 12444, ...) sont-elles
# reproductibles, ou sont-elles un tirage de bruit ? Le control permet de
# trancher SANS GPU : comparez les deux tables de top features.
#
# Indice : res_t["top_features"] et res_c["top_features"] donnent chacun le
# top-10 sous forme (id, z) ; la cellule de mesure control affiche
# deja le recouvrement des deux top-10.
# Etape 1 : extraire les ids top-10 des deux mesures et leur intersection.
# Etape 2 : pour les 5 premiers ids du trained, retrouver leur z dans le
# control via un dict {id: z} construit sur res_c["top_features"]
# -- que constate-t-on ?
# Etape 3 : conclure en une phrase : que suffit-il pour qu'une feature du
# trained merite une investigation de tranche 2 ?
ids_trained = None
ids_control = None
intersection = None
print("Exercice a completer")Exercice a completer
Conclusion et voir aussi
Etabli (negatif borne). Sur 30 paires minimales humour/unfun/controle, au couple Qwen3.5-2B-Base x SAE-Res-W32K-L0_50 couche 12/24, la perte d’incongruite humorale ne deplace ni la distance globale (ratio 0.97 vs controle d’edition), ni les features individuelles (0 sur 32768 a |z| > 3, amplitude sous le bruit permute). Le protocole etait pre-registre avec deux ecarts documentes AVANT interpretation ; le negatif est donc interpretable, pas une absence de conclusion.
Tranches suivantes (#14035) : couches early/late, couple 9B/W64K, zone dernier-token (exercice 2), dynamique de trajectoire dans la zone.
- #14035 – issue de reference : protocole falsifiable, pre-enregistrement (c.5743321902), ecarts c.5743498870 / c.5743512349
- ICT-35-HumorCausalProbe-Pilot-Python – le pilote HLS lexical (verdict INCONCLUSIVE borne, meme honnetete)
- ICT-21-SAETrajectoires-Python / ICT-21b-SAECalibration-Python – la strate SAE du pipeline #5101 et ses garde-fous (bf16, top-k)
- GameTheory-18c-Humour-Banc-Python – le banc humour consolide (#14032) : CORPUS_DUR (120 instances annotees)
scripts/extract_sae_traces.py– l’extracteur GPU (seul composant torch) ;ict/humor_pairs.py– corpus, paires, mesure