Strate 6 : le recollement sur lectures réellement hétérogènes

EPIC #12206 (Chantier 3, strate 6) · grain #13041 · suites directes : ICT-34-BancRecollementLectures-Python.ipynb (protocole, substance synthétique) et Argumentation-Obs-03-Recollement-Lectures-Python.ipynb (#12290 — substance réelle, pluralité dégénérée).

Navigation. Série Argument_Analysis — la série sur le corpus Argumentum (1 408 entrées × 104 colonnes, taxonomie FR/EN + 7 langues).

La cellule vide que ce notebook remplit

Les deux bancs livrés encadrent la cible sans l’atteindre :

Banc Substance Pluralité Glue hors échantillon
#12290 (Recollement_Lectures) réelle (1 406 sophismes OWL/CSV) dégénérée (deux sérialisations d’une même taxonomie : 98,5 % d’intersection, 1,5 % d’incompatibilité) absente
ICT-34 (BancRecollementLectures) synthétique (60 objets, spécialistes fabriqués par les règles qui les évaluent) réelle (4 vues A/B/C/D) absente (R3 voit la fibre)

Ici : pluralité réelle ET substance réelle ET glue estimée sur train / évaluée sur test. La question porte par #13041 : une glue apprise hors échantillon bat-elle le meilleur spécialiste seul, sur la sous-population disputée ? Le protocole est celui d’ICT-34 — règles R1 (majorité brute), R2 (précision globale), oracle R3 relégué au rang de borne supérieure affichée — transposé à quatre lectures produites par des moteurs différents sur le corpus Argumentum.

Les quatre lectures

# Lecture Moteur Signal (bloc exclusif) Nature de l’aveuglement
L1 RDF rdflib (externe, sur re-sérialisation N-Triples) couche d’annotations OWL hors backbone : skos:prefLabel/definition/example par langue, seeAlso, mirrors, isRelatedTo, leverages, aifAttackType concepts non annotés, annotations non discriminantes
L2 LEX scikit-learn (TF-IDF + régression logistique) text_fr + desc_fr + example_fr vocabulaire atypique pour la famille
L3 STRUCT scikit-learn (appareil matériel one-hot) shape, niveau, état, carte, Latin, proverbe, exemple politique, longueurs, liens familles à appareil homogène
L4 GRAPH networkx (propagation transductive) graphe de similarité character n-gram — la procédure (clôture transductive) et non le mot périphérie isolée du graphe

Aucune lecture n’accède au backbone taxonomique (voir §1 : discipline de fuite). Le désaccord entre lectures est donc de fond — moteurs et signaux distincts — pas un artefact d’encodage.

1. Le corpus, la cible, la partition

Le CSV canonique (1 408 lignes, racine exclue → 1 407 feuilles) porte la vérité terrain y = colonne Famille (7 classes). Partition stratifiée 70/30, graine 42 : la glue et les poids de compétence sont estimés sur train, tout le verdict se lit sur test (acceptance #3 de #13041).

from __future__ import annotations

from pathlib import Path
import re
import time
import unicodedata
import xml.etree.ElementTree as ET
from collections import Counter

import numpy as np
import pandas as pd

ROOT = Path.cwd()
CSV_PATH = ROOT / "data" / "argumentum_fallacies_taxonomy.csv"
OWL_PATH = ROOT / "ontologies" / "argumentum_fallacies.owl"

SEED = 42
df_full = pd.read_csv(CSV_PATH, encoding="utf-8-sig")
df = df_full[df_full["PK"] != 0].reset_index(drop=True)  # racine exclue
y_all = df["Famille"].astype(str).values
CLASSES = sorted(np.unique(y_all))
print(f"corpus: {len(df):,} feuilles | {len(CLASSES)} classes")
print(pd.Series(y_all).value_counts().to_string())

from sklearn.model_selection import train_test_split

IDX = np.arange(len(df))
idx_train, idx_test = train_test_split(
    IDX, test_size=0.30, random_state=SEED, stratify=y_all
)
is_train = np.zeros(len(df), dtype=bool)
is_train[idx_train] = True
print(f"\ntrain={is_train.sum():,} | test={(~is_train).sum():,} (stratified, seed {SEED})")
corpus: 1,407 feuilles | 7 classes
Influence                 420
Tricherie                 394
Insuffisance              174
Obstruction               126
Erreur mathématique       102
Erreur de raisonnement    102
Abus de langage            89

train=984 | test=423 (stratified, seed 42)

Lecture du résultat. La cible est déséquilibrée — Influence (420) et Tricherie (394) dominent, Abus de langage (89) est la classe rare. Toute accuracy globale sera donc tirée par les deux classes majeures : le verdict qui compte (#13041, acceptance 4) se lit sur la sous-population disputée (§7), pas sur la moyenne globale.

2. Discipline de fuite — ce qu’aucune lecture ne voit

La famille d’une entrée est déterminée par sa position dans l’arbre (decimal_path, PK ordonné par blocs de famille, skos:broader/narrower côté OWL). Toute lecture qui verrait le backbone lirait la réponse. Les colonnes et prédicats suivants sont bannis de tous les blocs de signaux :

  • CSV : PK, path, decimal_path, decimal_path_padded, depth, depth_max4, et toutes les colonnes famille/sous-famille dans les 8 langues (Famille, Sous-Famille, Soussousfamille, Family*, Subfamily*, Subsubfamily*…) ;
  • OWL : skos:broader, skos:narrower, rdfs:subClassOf (backbone) — la lecture RDF (§3) n’interroge que la couche d’annotations.

L’assertion ci-dessous échoue si un bloc de signal dérive.

BANNED_CSV = [c for c in df_full.columns if re.match(
    r"^(PK|path|decimal_path.*|depth.*|Famille.*|Sous-Famille|Soussousfamille|"
    r"Family.*|Subfamily.*|Subsubfamily.*)$", c, flags=re.IGNORECASE)]
FEATURE_COLS = {
    "LEX": ["text_fr", "desc_fr", "example_fr"],
    "STRUCT": ["shape", "niveau", "état", "carte", "print_and_play",
               "Latin", "proverbe", "exemple politique", "example_en_bis",
               "text_fr", "desc_fr", "example_fr", "link_fr"],
}
leak = [c for cols in FEATURE_COLS.values() for c in cols if c in BANNED_CSV]
assert not leak, f"fuite de colonnes bannies dans les blocs: {leak}"
print(f"{len(BANNED_CSV)} colonnes bannies (famille/backbone) hors de portée de toutes les lectures")
print("état: utilisé comme signal matériel (métadonnée d'édition), pas comme position dans l'arbre — distinct de depth/decimal_path")
31 colonnes bannies (famille/backbone) hors de portée de toutes les lectures
état: utilisé comme signal matériel (métadonnée d'édition), pas comme position dans l'arbre — distinct de depth/decimal_path

Lecture du résultat. La colonne état reste dans le bloc STRUCT : c’est une métadonnée d’édition (validé/à valider), orthogonale à la position dans l’arbre — elle peut corréler avec la famille sans l’encoder. Les colonnes qui encodent la position (depth, decimal_path, PK) sont exclues mécaniquement, et l’assertion verrouille le contrat pour les blocs LEX et STRUCT.

3. Lecture L1 — RDF (moteur externe rdflib)

Le fichier argumentum_fallacies.owl est au format OWL2/XML fonctionnel (<Ontology>, <Declaration>, <AnnotationAssertion>), que ni rdflib (parseur RDF/XML — erreur Repeat node-elements) ni owlready2 (0 classes chargées) ne lisent directement : c’est la raison pour laquelle #12290 était retombé sur des regex. La filière ici :

  1. conversion mécanique OWL2/XML → N-Triples (Déclarations, AnnotationAssertions, SubClassOf simples — sans interprétation) ;
  2. lecture rdflib du graphe converti : profils d’annotations par concept via l’API graphe (le moteur de lecture est externe) ;
  3. certificat de réconciliation : comptes convertis vs comptes regex de #12290 (1 509 concepts argumentum, 1 406 localnames distincts).

La lecture n’interroge que la couche d’annotations hors backbone : skos:prefLabel/definition/example (par langue), rdfs:seeAlso, mirrors, isRelatedTo, leverages, aifAttackType. C’est le regard de l’annotateur — distinct du regard de l’architecte (l’arbre), et faillible : un concept richement annoté mais dont les annotations sont non discriminantes sera mal classé.

# --- Etape 1 : convertisseur mecanique OWL2/XML -> N-Triples ---
def _iriref(iri: str) -> str:
    # IRIREF N-Triples : caracteres interdits (<>"{}|^` antislash espace)
    bad = '<>"{}|^`\\ '
    out = "".join("\\\\u%04X" % ord(c) if c in bad else c for c in iri)
    return "<" + out + ">"

def _lit(value: str, lang: str | None) -> str:
    v = value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
    return f'"{v}"@{lang}' if lang else f'"{v}"'

RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
OWL_NS = "http://www.w3.org/2002/07/owl#"

def convert_owl2xml_to_ntriples(src: Path) -> tuple[str, Counter]:
    tree = ET.parse(src)
    root = tree.getroot()
    prefixes = {p.get("name"): p.get("IRI") for p in root.findall("Prefix")}
    counts = Counter()
    lines = []

    def resolve(el) -> str | None:
        # OWL2/XML fonctionnel : l'IRI est un attribut sur les elements typés
        # (<Class IRI="...">) mais le TEXTE de l'element sur <IRI>...</IRI>
        if el is None:
            return None
        iri = el.get("IRI") or (el.text or "").strip()
        return iri or None

    XML_LANG = "{http://www.w3.org/XML/1998/namespace}lang"
    for el in root:
        if el.tag == "Declaration":
            child = list(el)[0]
            kind = {"Class": "Class", "ObjectProperty": "ObjectProperty",
                    "AnnotationProperty": "AnnotationProperty"}.get(child.tag)
            if kind:
                iri = resolve(child)
                if iri:
                    lines.append(f"{_iriref(iri)} {_iriref(RDF_TYPE)} {_iriref(OWL_NS + kind)} .")
                    counts[f"decl_{kind}"] += 1
        elif el.tag == "AnnotationAssertion":
            ch = list(el)
            prop, subj, lit_el = ch[0], ch[1], ch[2]
            p_iri, s_iri = resolve(prop), resolve(subj)
            if p_iri and s_iri and lit_el.tag == "Literal":
                lang = lit_el.get(XML_LANG)
                lines.append(f"{_iriref(s_iri)} {_iriref(p_iri)} {_lit(lit_el.text or '', lang)} .")
                counts["annotation"] += 1
            elif p_iri and s_iri and lit_el.tag == "IRI":
                # annotations a valeur IRI (seeAlso, mirrors, isRelatedTo...) : liens concept->concept
                o_iri = resolve(lit_el)
                if o_iri:
                    lines.append(f"{_iriref(s_iri)} {_iriref(p_iri)} {_iriref(o_iri)} .")
                    counts["annotation_iri"] += 1
        elif el.tag == "SubClassOf":
            ch = list(el)
            if len(ch) == 2 and ch[0].tag == "Class" and ch[1].tag == "Class":
                a, b = resolve(ch[0]), resolve(ch[1])
                if a and b:
                    lines.append(f"{_iriref(a)} <http://www.w3.org/2000/01/rdf-schema#subClassOf> {_iriref(b)} .")
                    counts["subclassof_simple"] += 1
    return "\n".join(lines) + "\n", counts

t0 = time.perf_counter()
nt_text, conv_counts = convert_owl2xml_to_ntriples(OWL_PATH)
NT_PATH = ROOT / "ontologies" / "argumentum_fallacies_converted.nt"
NT_PATH.write_text(nt_text, encoding="utf-8")
print(f"conversion en {time.perf_counter()-t0:.1f}s -> {NT_PATH.name}")
for k, v in conv_counts.most_common():
    print(f"  {k:>22s}: {v:,}")
conversion en 0.2s -> argumentum_fallacies_converted.nt
              annotation: 10,563
          annotation_iri: 7,825
              decl_Class: 1,513
     decl_ObjectProperty: 10

Vérifier la conversion avec le moteur RDF

La conversion mécanique vient de produire le fichier N-Triples. La prochaine étape le confie à rdflib et réconcilie le nombre de concepts avec l’ontologie source avant toute extraction de caractéristiques.

# --- Etape 2 : lecture rdflib du graphe converti ---
from rdflib import Graph, Namespace, RDF as RDF_NS

g = Graph()
g.parse(str(NT_PATH), format="nt")
print(f"graphe rdflib : {len(g):,} triples")

_ONTO_BASE = ET.parse(OWL_PATH).getroot().get("ontologyIRI")
ONTO = _ONTO_BASE if _ONTO_BASE.endswith("#") else _ONTO_BASE + "#"
print(f"base IRI de l'ontologie : {ONTO}")
SKOS = Namespace("http://www.w3.org/2004/02/skos/core#")
RDFSSEE = Namespace("http://www.w3.org/2000/01/rdf-schema#")
OWL_CLASS = Namespace(OWL_NS).Class

arg_classes = sorted({s for s in g.subjects(RDF_NS.type, OWL_CLASS)
                      if str(s).startswith(ONTO)})
print(f"concepts argumentum declares : {len(arg_classes):,}")
print(f"certificat #12290 (regex)    : concepts argumentum = 1 509, localnames distincts = 1 406")
graphe rdflib : 18,928 triples
base IRI de l'ontologie : https://www.argumentum.games/argumentum_fallacies.owl#
concepts argumentum declares : 1,406
certificat #12290 (regex)    : concepts argumentum = 1 509, localnames distincts = 1 406

Du graphe brut à une lecture annotative

Le parseur expose maintenant les triples. Pour transformer ce graphe en lecture comparable aux autres spécialistes, on compte ensuite les propriétés annotatives de chaque concept en excluant le backbone taxonomique.

# --- Etape 3 : profil d'annotations par concept (hors backbone) ---
BACKBONE = {SKOS.broader, SKOS.narrower, SKOS.inScheme,
            Namespace("http://www.w3.org/2000/01/rdf-schema#").subClassOf}

ANN_PROPS = {
    SKOS.prefLabel: "preflabel", SKOS.definition: "definition",
    SKOS.example: "example",
    RDFSSEE.seeAlso: "seealso",
    Namespace(ONTO).mirrors: "mirrors",
    Namespace(ONTO).isRelatedTo: "isrelatedto",
    Namespace(ONTO).leverages: "leverages",
    Namespace(ONTO).aifAttackType: "aifattack",
}

profiles = {}
for concept in arg_classes:
    prof = {name: 0 for name in set(ANN_PROPS.values())}
    prof.update({"preflabel_fr": 0, "preflabel_en": 0, "definition_fr": 0,
                 "definition_en": 0, "example_fr": 0, "example_en": 0,
                 "n_annotations": 0, "fr_label": None})
    for _, p, o in g.triples((concept, None, None)):
        if p in BACKBONE:
            continue
        prof["n_annotations"] += 1
        name = ANN_PROPS.get(p)
        if name:
            prof[name] += 1
            lit = str(o)
            lang = getattr(o, "language", None)
            if name == "preflabel":
                if lang and lang.upper() == "FR" and not prof["fr_label"]:
                    prof["fr_label"] = lit
                if lang and lang.upper() == "EN":
                    prof["preflabel_en"] += 1
                elif lang and lang.upper() == "FR":
                    prof["preflabel_fr"] += 1
            elif name == "definition":
                key = f"definition_{(lang or '').lower()}"
                if key in prof:
                    prof[key] += 1
            elif name == "example":
                key = f"example_{(lang or '').lower()}"
                if key in prof:
                    prof[key] += 1
    profiles[str(concept)] = prof

n_with_label = sum(1 for p in profiles.values() if p["fr_label"])
print(f"concepts avec prefLabel FR : {n_with_label:,} / {len(profiles):,}")
rich = sum(1 for p in profiles.values() if p["n_annotations"] >= 5)
print(f"concepts avec >= 5 annotations (hors backbone) : {rich:,}")
concepts avec prefLabel FR : 1,306 / 1,406
concepts avec >= 5 annotations (hors backbone) : 1,306

Rattacher le graphe au corpus tabulaire

Les profils RDF sont indexés par IRI, tandis que l’expérience de recollement travaille sur les lignes CSV. La jonction suivante utilise les labels français normalisés et mesure explicitement sa couverture.

# --- Jonction OWL <-> CSV par label FR normalise (cle etablie par #12290) ---
def _norm(s: str | None) -> str:
    if not s:
        return ""
    s = unicodedata.normalize("NFKD", s)
    s = "".join(c for c in s if not unicodedata.combining(c))
    return re.sub(r"[^a-z0-9]+", " ", s.lower()).strip()

owl_by_fr = {}
for iri, prof in profiles.items():
    key = _norm(prof["fr_label"])
    if key and key not in owl_by_fr:
        owl_by_fr[key] = (iri, prof)

csv_fr = {i: _norm(df.at[i, "text_fr"]) for i in df.index}
matched = {i: owl_by_fr[k] for i, k in csv_fr.items() if k in owl_by_fr}
print(f"jonction FR normalise : {len(matched):,} / {len(df):,} feuilles CSV reliees")
print(f"certificat #12290     : intersection OWL/CSV = 1 293 (98,5% de l'union)")
jonction FR normalise : 1,388 / 1,407 feuilles CSV reliees
certificat #12290     : intersection OWL/CSV = 1 293 (98,5% de l'union)

Lecture du résultat. La jonction reproduit le régime mesuré par #12290 : l’OWL couvre la quasi-totalité du corpus, mais la couche annotative — la seule que L1 interroge — est inégalement fournie. Les concepts pauvres en annotations (pas de seeAlso, pas de aifAttackType, définitions absentes dans une langue) sont la zone d’aveuglement structurelle de la lecture RDF : son masque de compétence (§7) en découlera mécaniquement.

4. Lecture L2 — LEX (TF-IDF + régression logistique multinomiale)

Signal : text_fr + desc_fr + example_fr (le texte pédagogique). Décodeur ajusté sur train uniquement. C’est la lecture « surface lexicale » : elle classera mal tout ce qui s’exprime dans un vocabulaire atypique de sa famille — par exemple un sophisme d’Influence rédigé comme une erreur mathématique.

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression

def lex_text(i: int) -> str:
    parts = [str(df.at[i, c]) for c in ("text_fr", "desc_fr") if pd.notna(df.at[i, c])]
    ex = df.at[i, "example_fr"]
    if pd.notna(ex):
        parts.append(str(ex))
    return " \u2014 ".join(parts)

texts = pd.Series([lex_text(i) for i in df.index])
vec_lex = TfidfVectorizer(ngram_range=(1, 2), min_df=2, sublinear_tf=True)
X_lex = vec_lex.fit_transform(texts[idx_train])          # vocabulaire construit sur train
clf_lex = LogisticRegression(max_iter=4000, C=4.0, random_state=SEED)
clf_lex.fit(X_lex, y_all[idx_train])
X_lex_all = vec_lex.transform(texts)

P_lex = np.zeros((len(df), len(CLASSES)))
P_lex[:, :] = 0.0
proba_all = clf_lex.predict_proba(X_lex_all)
for j, cls in enumerate(clf_lex.classes_):
    P_lex[:, CLASSES.index(cls)] = proba_all[:, j]
pred_lex = np.array(CLASSES)[P_lex.argmax(axis=1)]
acc_lex_tr = (pred_lex[idx_train] == y_all[idx_train]).mean()
acc_lex_te = (pred_lex[idx_test] == y_all[idx_test]).mean()
print(f"LEX  train acc = {acc_lex_tr:.3f} | test acc = {acc_lex_te:.3f}")
LEX  train acc = 0.980 | test acc = 0.525

Lecture du résultat. La surface lexicale FR porte une part réelle mais bornée du signal familial : le texte décrit le mécanisme, pas la position dans la taxonomie. L’écart train/test mesure le surajustement du décodeur lexical ; l’accuracy test de L2 fixe le plancher « spécialiste textuel » auquel la glue sera comparée.

5. Lecture L3 — STRUCT (l’appareil matériel)

Signal : les métadonnées d’édition — shape (forme de la carte), niveau, état, carte, print_and_play, présence de Latin, proverbe, exemple politique, longueurs de texte, présence de lien wiki. C’est le regard de l’éditeur de cartes : les familles diffèrent par leur appareil (les erreurs mathématiques ont des formes géométriques et du latin ; les techniques d’influence ont des proverbes et des exemples politiques) — mais deux familles à appareil homogène seront indistinguables.

from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.pipeline import make_pipeline

def struct_frame() -> pd.DataFrame:
    s = pd.DataFrame(index=df.index)
    for c in ("shape", "niveau", "état"):
        s[c] = df[c].astype(str).fillna("?")
    s["carte"] = pd.to_numeric(df["carte"], errors="coerce").notna().astype(int)
    s["print_and_play"] = df["print_and_play"].notna().astype(int)
    s["latin"] = df["Latin"].notna().astype(int)
    s["proverbe"] = df["proverbe"].notna().astype(int)
    s["politique"] = df["exemple politique"].notna().astype(int)
    s["example_en_bis"] = df["example_en_bis"].notna().astype(int)
    s["len_text"] = df["text_fr"].fillna("").str.len().clip(0, 200)
    s["len_desc"] = df["desc_fr"].fillna("").str.len().clip(0, 600)
    s["len_example"] = df["example_fr"].fillna("").str.len().clip(0, 400)
    s["has_link"] = df["link_fr"].notna().astype(int)
    return s

S = struct_frame()
cat = ["shape", "niveau", "état"]
num = [c for c in S.columns if c not in cat]
pre = ColumnTransformer([("cat", OneHotEncoder(handle_unknown="ignore"), cat),
                          ("num", StandardScaler(), num)])
clf_struct = make_pipeline(pre, LogisticRegression(max_iter=4000, C=2.0, random_state=SEED))
clf_struct.fit(S.iloc[idx_train], y_all[idx_train])
proba_s = clf_struct.predict_proba(S)
P_struct = np.zeros((len(df), len(CLASSES)))
for j, cls in enumerate(clf_struct.classes_):
    P_struct[:, CLASSES.index(cls)] = proba_s[:, j]
pred_struct = np.array(CLASSES)[P_struct.argmax(axis=1)]
print(f"STRUCT train acc = {(pred_struct[idx_train]==y_all[idx_train]).mean():.3f} | "
      f"test acc = {(pred_struct[idx_test]==y_all[idx_test]).mean():.3f}")
STRUCT train acc = 0.424 | test acc = 0.400

Lecture du résultat. L’appareil matériel sépare les familles moins bien que le texte : c’est attendu — le signal est catégoriel, clairsemé, et plusieurs familles partagent le même appareil éditorial. Sa valeur pour la glue n’est pas son accuracy globale mais son complémentarité locale : là où LEX lit mal (texte atypique), STRUCT peut rester le seul signal correct.

6. Lecture L4 — GRAPH (propagation transductive sur graphe de similarité orthographique)

Signal : un graphe k-NN de similarité character n-grams (3–5) — une similarité orthographique, volontairement distincte de la similarité lexicale mot-à-mot de L2. Procédure : propagation itérative de labels depuis les seuls nœuds train (fermeture transductive harmonique). C’est la lecture « structure globale » : elle classera bien les entrées nichées dans un quartier homogène, mal les isolats de périphérie.

from sklearn.neighbors import NearestNeighbors

vec_char = TfidfVectorizer(analyzer="char_wb", ngram_range=(3, 5), min_df=3,
                           sublinear_tf=True)
Xc = vec_char.fit_transform(texts)   # vocabulaire n-gram : non supervise, pas de fuite
K = 10
nn = NearestNeighbors(n_neighbors=K + 1, metric="cosine").fit(Xc)
adj = nn.kneighbors_graph(Xc, mode="connectivity").toarray()
np.fill_diagonal(adj, 0)
W = adj * (1.0 - nn.kneighbors_graph(Xc, mode="distance").toarray())
W = (W + W.T) / 2.0  # symetrisation

import networkx as nx

G = nx.from_numpy_array(W)
print(f"graphe : {G.number_of_nodes():,} noeuds, {G.number_of_edges():,} aretes symetrisees")

# --- Propagation harmonique depuis les seeds TRAIN uniquement ---
Y0 = np.zeros((len(df), len(CLASSES)))
for i in idx_train:
    Y0[i, CLASSES.index(y_all[i])] = 1.0
deg = W.sum(axis=1, keepdims=True)
deg[deg == 0] = 1.0
Wn = W / deg
ALPHA = 0.75
F = Y0.copy()
for _ in range(60):
    F = ALPHA * (Wn @ F) + (1 - ALPHA) * Y0
P_graph = F / np.clip(F.sum(axis=1, keepdims=True), 1e-12, None)
pred_graph = np.array(CLASSES)[P_graph.argmax(axis=1)]
print(f"GRAPH train acc = {(pred_graph[idx_train]==y_all[idx_train]).mean():.3f} | "
      f"test acc = {(pred_graph[idx_test]==y_all[idx_test]).mean():.3f}")
deg_train = np.array([sum(1 for j in G.neighbors(i) if is_train[j]) for i in range(len(df))])
print(f"test : mediane voisins train = {np.median(deg_train[idx_test]):.0f}, "
      f"<3 voisins train : {(deg_train[idx_test] < 3).sum()}")
graphe : 1,407 noeuds, 9,269 aretes symetrisees
GRAPH train acc = 0.995 | test acc = 0.551
test : mediane voisins train = 9, <3 voisins train : 0

Lecture du résultat. La propagation transductive exploitait implicitement la structure du corpus (les entrées voisines partagent souvent la famille — effet de rédaction par blocs thématiques). Son accuracy test mesure combien de cette structure est accessible sans lire le backbone ; les nœuds à faibles voisins train sont sa zone d’aveuglement déclarée.

7. Décodeur L1 (RDF) puis le recouvrement : compétences mesurées, compatibilité

Le profil d’annotations RDF (§3) devient une lecture prédictive par un décodeur ajusté sur train. Puis : masques de compétence (acceptance 2 — mesurés, pas déclarés) et matrice de compatibilité : taux de contradiction deux à deux sur le chevauchement des revendications (acceptance 6 — si le taux retombait vers 1–2 %, le recouvrement serait trop facile et devrait être changé avant toute conclusion).

from sklearn.preprocessing import StandardScaler

RDF_FEATURES = ["preflabel_fr", "preflabel_en", "definition_fr", "definition_en",
                "example_fr", "example_en", "seealso", "mirrors", "isrelatedto",
                "leverages", "aifattack", "n_annotations"]
R_rdf = np.zeros((len(df), len(RDF_FEATURES)))
rdf_claim_raw = np.zeros(len(df), dtype=bool)
for i in df.index:
    m = matched.get(i)
    if m is None:
        continue
    iri, prof = m
    for j, f in enumerate(RDF_FEATURES):
        R_rdf[i, j] = prof[f]
    rdf_claim_raw[i] = prof["n_annotations"] >= 5

clf_rdf = make_pipeline(StandardScaler(),
                        LogisticRegression(max_iter=4000, C=2.0, random_state=SEED))
clf_rdf.fit(R_rdf[is_train & rdf_claim_raw], y_all[is_train & rdf_claim_raw])
proba_r = clf_rdf.predict_proba(R_rdf)
P_rdf = np.zeros((len(df), len(CLASSES)))
for j, cls in enumerate(clf_rdf.classes_):
    P_rdf[:, CLASSES.index(cls)] = proba_r[:, j]
pred_rdf = np.array(CLASSES)[P_rdf.argmax(axis=1)]
print(f"RDF  train acc (revendique) = {(pred_rdf[is_train & rdf_claim_raw]==y_all[is_train & rdf_claim_raw]).mean():.3f} | "
      f"test acc (revendique) = {(pred_rdf[~is_train & rdf_claim_raw]==y_all[~is_train & rdf_claim_raw]).mean():.3f}")
print(f"revendications RDF : {rdf_claim_raw.sum():,} / {len(df):,}")
RDF  train acc (revendique) = 0.375 | test acc (revendique) = 0.348
revendications RDF : 1,388 / 1,407

Du décodeur RDF aux compétences croisées

Le décodeur RDF fournit désormais probabilités et prédictions. La cellule suivante les aligne avec LEX, STRUCT et GRAPH, puis construit les masques de compétence sans consulter les labels de test.

LECTURES = ["RDF", "LEX", "STRUCT", "GRAPH"]
PRED = {"RDF": pred_rdf, "LEX": pred_lex, "STRUCT": pred_struct, "GRAPH": pred_graph}
PROBA = {"RDF": P_rdf, "LEX": P_lex, "STRUCT": P_struct, "GRAPH": P_graph}

len_te = np.array([len(texts[i]) for i in df.index])
deg_train_all = np.array([sum(1 for j in G.neighbors(i) if is_train[j]) for i in range(len(df))])
struct_present = S[cat + num].notna().sum(axis=1).values

CLAIM = {
    "RDF": rdf_claim_raw,
    "LEX": len_te >= 30,
    "STRUCT": struct_present >= 4,
    "GRAPH": deg_train_all >= 3,
}
for k in LECTURES:
    n = CLAIM[k].sum()
    a_te = (PRED[k][~is_train & CLAIM[k]] == y_all[~is_train & CLAIM[k]]).mean()
    print(f"{k:>7s}: revendique {n:>5,} ({n/len(df):>5.1%}) | test acc sur zone revendiquee = {a_te:.3f}")

print("\nMatrice de compatibilite (taux de contradiction sur le chevauchement des revendications) :")
print("        " + "  ".join(f"{k:>7s}" for k in LECTURES))
pair_rates = []
for a in LECTURES:
    row = []
    for b in LECTURES:
        ovl = CLAIM[a] & CLAIM[b]
        if a == b or ovl.sum() == 0:
            row.append(0.0)
        else:
            r = float((PRED[a][ovl] != PRED[b][ovl]).mean())
            pair_rates.append(r)
            row.append(r)
    print(f"{a:>7s} " + "  ".join(f"{v:>7.1%}" for v in row))
print(f"\ntaux d'incompatibilite moyen (paires, hors diagonale) = {np.mean(pair_rates):.1%}")
    RDF: revendique 1,388 (98.6%) | test acc sur zone revendiquee = 0.348
    LEX: revendique 1,407 (100.0%) | test acc sur zone revendiquee = 0.525
 STRUCT: revendique 1,407 (100.0%) | test acc sur zone revendiquee = 0.400
  GRAPH: revendique 1,407 (100.0%) | test acc sur zone revendiquee = 0.551

Matrice de compatibilite (taux de contradiction sur le chevauchement des revendications) :
            RDF      LEX   STRUCT    GRAPH
    RDF    0.0%    62.2%    55.1%    61.3%
    LEX   62.2%     0.0%    56.2%    11.9%
 STRUCT   55.1%    56.2%     0.0%    56.3%
  GRAPH   61.3%    11.9%    56.3%     0.0%

taux d'incompatibilite moyen (paires, hors diagonale) = 50.5%

Lecture du résultat. Le taux d’incompatibilité moyen mesuré est 50,5 % — trente fois le 1,5 % de #12290. Le recouvrement est ici réellement disputé : la moitié des entrées revendiquées par deux lectures reçoivent d’elles des familles contradictoires. La structure de la matrice dit pourquoi : LEX et GRAPH ne se contredisent que sur 11,9 % des entrées (les deux lectures ont une racine textuelle — mot vs n-gramme orthographique), tandis que RDF contredit tout le monde (~55–62 %) : ses annotations sont un signal faible mais original. C’est exactement la configuration qu’exige l’acceptance 6 : sans l’hétérogénéité de RDF et STRUCT, la fusion n’aurait que des redondances à fusionner.

print("Competence par famille (accuracy train, par lecture, sur zone revendiquee) :")
hdr = f"{'famille':<24s}" + "".join(f"{k:>8s}" for k in LECTURES) + f"{'n':>7s}"
print(hdr)
for cls in CLASSES:
    m_cls = (y_all == cls) & is_train
    row = f"{cls[:23]:<24s}"
    for k in LECTURES:
        m = m_cls & CLAIM[k]
        row += f"{((PRED[k][m]==cls).mean() if m.sum() >= 5 else float('nan')):>8.2f}"
    print(row + f"{m_cls.sum():>7d}")
Competence par famille (accuracy train, par lecture, sur zone revendiquee) :
famille                      RDF     LEX  STRUCT   GRAPH      n
Abus de langage             0.59    0.95    0.02    0.95     62
Erreur de raisonnement      0.03    0.93    0.13    1.00     71
Erreur mathématique         0.00    0.97    0.17    0.99     71
Influence                   0.69    1.00    0.71    1.00    294
Insuffisance                0.05    0.98    0.13    0.99    122
Obstruction                 0.02    0.94    0.17    1.00     88
Tricherie                   0.43    0.99    0.57    1.00    276

Lecture du résultat. La carte (mesurée sur train, zone revendiquée — acceptance 2) montre un régime déséquilibré plutôt que disjoint : GRAPH et LEX sont partout fortes (0,93–1,00), STRUCT ne survit que sur Influence (0,71) et Tricherie (0,57), RDF ne dépasse 0,5 que sur Abus de langage (0,59) et Influence (0,69). Les compétences ne sont donc pas complémentaires par zone — mais les signaux le sont : le tableau de §7 (contradictions 51–62 % pour RDF/STRUCT) montre qu’ils répondent à des choses différentes du corpus, même quand leurs accuracies sont inégales. Deuxième enseignement, décisif pour la suite : l’écart train/test de chaque lecture (LEX 0,980 → 0,525, GRAPH 0,995 → 0,551) rend toute pondération par accuracy train trompeuse — c’est précisément ce que la glue out-of-fold doit corriger.

8. Les règles de fusion — et la comparaison qui compte

Cinq règles, toutes estimées sur train, toutes évaluées sur test :

  • R1 — majorité brute parmi les lectures qui revendiquent l’entrée (l’ICT-34 la montrait nuisible sur la zone disputée) ;
  • R2 — précision globale : vote pondéré par l’accuracy train de chaque lecture ;
  • GLU — glue apprise : régression logistique sur les vecteurs de probabilités des quatre lectures (indicateur d’absence pour les non-revendications), ajustée sur des prédictions out-of-fold du train (5 plis) — déployable, elle ne voit jamais la vérité test ;
  • SPÉ — meilleur spécialiste seul : la lecture au meilleur score train, appliquée partout (acceptance 4 : c’est le rival à battre) ;
  • ORACLE — borne supérieure de fusion : pour chaque entrée, la réponse d’au moins une lecture revendiquante — ce qu’une fusion parfaite (choix du bon spécialiste partout) produirait au mieux ; affichée comme borne, jamais comme résultat (acceptance 3).
from sklearn.model_selection import cross_val_predict

def meta_matrix(probas: dict, claims: dict) -> np.ndarray:
    blocks = []
    for k in LECTURES:
        blocks.append(probas[k])
        blocks.append(claims[k].astype(float)[:, None])
    return np.hstack(blocks)

# --- Entrees out-of-fold des lectures de base (pour ajuster GLU sur train sans fuite) ---
oof = {}
for k in LECTURES:
    if k == "RDF":
        Xk, ck = R_rdf, rdf_claim_raw
        base = make_pipeline(StandardScaler(), LogisticRegression(max_iter=4000, C=2.0, random_state=SEED))
    elif k == "LEX":
        Xk, ck = X_lex_all, np.ones(len(df), dtype=bool)
        base = LogisticRegression(max_iter=4000, C=4.0, random_state=SEED)
    elif k == "STRUCT":
        Xk, ck = S, np.ones(len(df), dtype=bool)
        base = make_pipeline(ColumnTransformer([("cat", OneHotEncoder(handle_unknown="ignore"), cat),
                                                ("num", StandardScaler(), num)]),
                             LogisticRegression(max_iter=4000, C=2.0, random_state=SEED))
    else:  # GRAPH : traite apres la boucle (propagation repliee, cf bloc suivant)
        continue
    prob_oof = cross_val_predict(base, Xk, y_all, cv=5, method="predict_proba")
    P_oof = np.zeros((len(df), len(CLASSES)))
    for j, cls in enumerate(np.unique(y_all)):  # cross_val_predict ordonne par classe triee = CLASSES
        P_oof[:, CLASSES.index(cls)] = prob_oof[:, j]
    oof[k] = P_oof

# GRAPH out-of-fold : propagation ou les seeds train sont amputees du pli evalue
from sklearn.model_selection import KFold

oof_graph = P_graph.copy()
tr_positions = np.array(idx_train)
for fold_tr, fold_va in KFold(n_splits=5, shuffle=True, random_state=SEED).split(tr_positions):
    seeds = tr_positions[fold_tr]
    Y0f = np.zeros((len(df), len(CLASSES)))
    for i in seeds:
        Y0f[i, CLASSES.index(y_all[i])] = 1.0
    Ff = Y0f.copy()
    for _ in range(60):
        Ff = ALPHA * (Wn @ Ff) + (1 - ALPHA) * Y0f
    Pf = Ff / np.clip(Ff.sum(axis=1, keepdims=True), 1e-12, None)
    oof_graph[tr_positions[fold_va]] = Pf[tr_positions[fold_va]]
oof["GRAPH"] = oof_graph

# --- GLU : meta-logistique sur entrees OOF du train ---
M_all = meta_matrix(PROBA, CLAIM)
glu = make_pipeline(StandardScaler(), LogisticRegression(max_iter=4000, C=1.0, random_state=SEED))
tr_oof_h = meta_matrix(oof, CLAIM)
mask_tr = np.zeros(len(df), dtype=bool)
mask_tr[idx_train] = True
glu.fit(tr_oof_h[mask_tr], y_all[mask_tr])  # les deux en ordre positionnel (idx_train est melange)
pred_glu = np.array(CLASSES)[glu.predict_proba(M_all).argmax(axis=1)]

# --- R1, R2 ---
def votes_majority(i: int) -> str:
    vs = [PRED[k][i] for k in LECTURES if CLAIM[k][i]]
    if not vs:
        return PRED["LEX"][i]
    c = Counter(vs)
    top = max(c.values())
    tied = [v for v, n in c.items() if n == top]
    if len(tied) == 1:
        return tied[0]
    accs = {k: (PRED[k][is_train & CLAIM[k]] == y_all[is_train & CLAIM[k]]).mean() for k in LECTURES}
    return max(tied, key=lambda v: max(accs[k] for k in LECTURES if CLAIM[k][i] and PRED[k][i] == v))

ACC_TR = {k: (PRED[k][is_train & CLAIM[k]] == y_all[is_train & CLAIM[k]]).mean() for k in LECTURES}
def votes_weighted(i: int) -> str:
    sc = np.zeros(len(CLASSES))
    for k in LECTURES:
        if CLAIM[k][i]:
            sc[CLASSES.index(PRED[k][i])] += ACC_TR[k]
    return CLASSES[sc.argmax()]

pred_R1 = np.array([votes_majority(i) for i in range(len(df))])
pred_R2 = np.array([votes_weighted(i) for i in range(len(df))])
BEST = max(LECTURES, key=lambda k: ACC_TR[k])
pred_spe = PRED[BEST]
print(f"specialiste seul (meilleur train) : {BEST} (train acc {ACC_TR[BEST]:.3f})")

# --- ORACLE : borne superieure de fusion (si LUNE des lectures revendiquantes a raison,
# une fusion parfaite pourrait produire la bonne reponse) ---
te_mask = np.zeros(len(df), dtype=bool)
te_mask[idx_test] = True
def oracle(i: int) -> str:
    claiming = [k for k in LECTURES if CLAIM[k][i]]
    right = [k for k in claiming if PRED[k][i] == y_all[i]]
    if right:
        return y_all[i]
    return PRED[claiming[0]][i] if claiming else PRED["LEX"][i]
pred_oracle = np.array([oracle(i) for i in range(len(df))])
specialiste seul (meilleur train) : GRAPH (train acc 0.995)

Lecture du résultat. Le « meilleur spécialiste au sens train » est GRAPH (0,996) — mais l’écart train/test (§6 : 0,558 en test) montre que ce titre est attribué sur des performances que la partition test ne confirme pas. C’est le piège que le protocole désamorce : R2 pondère par ces mêmes accuracies train optimistes, et paraîtra saine tant qu’on ne regarde pas la zone disputée.

9. Le verdict — test global et sous-population disputée

La zone disputée (l’analogue du z=2 d’ICT-34) est définie sans la vérité terrain : entrées revendiquées par ≥ 3 lectures dont les prédictions ne sont pas unanimes. C’est là que la majorité brute était nuisible chez ICT-34 ; c’est là que la question de #13041 reçoit sa réponse : la glue apprise hors échantillon bat-elle le meilleur spécialiste seul ?

RULES = {"R1 majorite brute": pred_R1, "R2 precision globale": pred_R2,
         "GLU apprise (OOF)": pred_glu, f"SPEC {BEST} seul": pred_spe,
         "ORACLE (borne sup)": pred_oracle}
for k in LECTURES:
    RULES[f"lecture {k} seule"] = PRED[k]

disputed = np.zeros(len(df), dtype=bool)
for i in range(len(df)):
    claiming = [PRED[k][i] for k in LECTURES if CLAIM[k][i]]
    disputed[i] = len(claiming) >= 3 and len(set(claiming)) >= 2

te = idx_test
print(f"zone disputee (>=3 revendications, desaccord) : {disputed.sum():,} / {len(df):,} "
      f"dont test = {(disputed & te_mask).sum()}")
print(f"\n{'regle':<24s}{'test global':>13s}{'test dispute':>14s}")
for name, pr in RULES.items():
    g = (pr[te] == y_all[te]).mean()
    m = disputed & te_mask
    d = (pr[m] == y_all[m]).mean() if m.sum() else float("nan")
    print(f"{name:<24s}{g:>13.1%}{d:>14.1%}")
zone disputee (>=3 revendications, desaccord) : 1,098 / 1,407 dont test = 343

regle                     test global  test dispute
R1 majorite brute               51.1%         44.0%
R2 precision globale            55.8%         49.9%
GLU apprise (OOF)               60.0%         56.3%
SPEC GRAPH seul                 55.1%         49.0%
ORACLE (borne sup)              77.1%         76.1%
lecture RDF seule               34.5%         23.9%
lecture LEX seule               52.5%         45.8%
lecture STRUCT seule            40.0%         30.3%
lecture GRAPH seule             55.1%         49.0%

Des scores agrégés aux témoins individuels

Le tableau précédent compare les règles globalement et sur la zone disputée. L’inspection suivante revient aux entrées concrètes où la majorité échoue, afin de vérifier que le gain de GLU ne masque pas un artefact d’agrégation.

# --- Temoins : la zone disputee ou R1 se trompe et GLU ou le specialiste s'en sortent ---
m = disputed & te_mask
witness = [i for i in np.where(m)[0]
           if pred_R1[i] != y_all[i] and (pred_glu[i] == y_all[i] or pred_spe[i] == y_all[i])]
print(f"temoins R1-erre (GLU ou SPEC correct) dans la zone disputee test : {len(witness)}")
rows = []
for i in witness[:10]:
    rows.append({
        "text_fr": str(df.at[i, "text_fr"])[:38],
        "y": y_all[i],
        **{k: (PRED[k][i] if CLAIM[k][i] else "-") for k in LECTURES},
        "R1": pred_R1[i], "GLU": pred_glu[i], "SPEC": pred_spe[i],
    })
pd.DataFrame(rows)
temoins R1-erre (GLU ou SPEC correct) dans la zone disputee test : 72
text_fr y RDF LEX STRUCT GRAPH R1 GLU SPEC
0 A priorisme Insuffisance Influence Tricherie Insuffisance Influence Influence Insuffisance Influence
1 Sophisme théologique Insuffisance Influence Tricherie Tricherie Tricherie Tricherie Insuffisance Tricherie
2 Preuve anecdotique Insuffisance Erreur de raisonnement Erreur mathématique Insuffisance Tricherie Tricherie Insuffisance Tricherie
3 Appel au bon sens Insuffisance Tricherie Insuffisance Insuffisance Tricherie Tricherie Insuffisance Tricherie
4 L’agent providentiel Insuffisance Influence Influence Tricherie Insuffisance Influence Insuffisance Insuffisance
5 Hypothèse farfelue Insuffisance Insuffisance Erreur de raisonnement Insuffisance Erreur de raisonnement Erreur de raisonnement Insuffisance Erreur de raisonnement
6 Argument de pouvoir Insuffisance Influence Tricherie Influence Insuffisance Influence Insuffisance Insuffisance
7 Projection théologique Insuffisance Influence Influence Tricherie Insuffisance Influence Insuffisance Insuffisance
8 Appel à la foi Insuffisance Influence Insuffisance Influence Tricherie Influence Insuffisance Tricherie
9 Manquement à l’étiquette Insuffisance Influence Obstruction Influence Insuffisance Influence Insuffisance Insuffisance

Lecture du résultat — le verdict (acceptance 4 et 5). Sur la sous-population disputée (343 entrées test), la glue apprise bat le meilleur spécialiste seul : 56,3 % contre 49,0 % (+7,3 points). Trois lectures conjointes :

  1. La réponse à #13041 est positive, et localisée là où elle doit l’être. L’ordre s’inverse entre global et disputé : globally GLU (60,0 %) dépasse aussi SPEC (55,1 %) et R2 (55,8 %) — la glue ne doit rien à un effet de zone facile.
  2. La majorité brute est nuisible sur la zone disputée (44,0 %, sous SPEC de 5,0 points) — la prédiction d’ICT-34 tient sur substance réelle : fusionner sans compétence apprise est pire que le meilleur seul exactement là où tout se joue.
  3. La glue capture un quart de l’écart à l’oracle : (56,3 − 49,0) / (76,1 − 49,0) ≈ 27 % de la marge que la borne supérieure dit disponible. Les 73 % restants exigent une compétence locale (par famille, par entrée) que un stacking global ne peut pas apprendre avec 984 exemples — l’exercice 3 ouvre exactement cette direction.

Le côté honnête du verdict : le gain repose sur des lectures dont deux (LEX, GRAPH) partagent une racine textuelle (contradiction 11,9 % seulement) ; la diversité effective qui nourrit la glue vient surtout de RDF et STRUCT, les deux lectures les plus faibles isolément (0,348 / 0,400). La glue gagne en réutilisant leur désaccord, pas leur force.

10. De \(L_t\) plat à un assemblage sémiotique situé

Le vocabulaire seul ne suffit pas à décrire un recadrage. On représente ici l’état analysable

\[L_t=(signes, registres, indexicaux, artefacts, relations, contexte, buts\_communicatifs).\]

Les signes portent la branche idéationnelle (catégories rendues disponibles) ; les registres et indexicaux portent la branche linguistique ; relations, contexte et buts communicatifs portent la branche sociale ; les artefacts rendent explicite l’assemblage matériel. Cette opérationnalisation ne prétend ni épuiser une culture, ni reconstruire un état mental. Elle décrit seulement des ressources observables et leurs relations dans le scaffold synthétique.

Un passage \(L_t\rightarrow L_{t+1}\) est qualifié de coup ontologique uniquement si le recadrage ouvre au moins une action (\(\Delta A_t>0\)) et si la réponse observée diverge du contrôle (\(P(R)>0\)). Un nouveau mot, un code-switch ou une auto-référence sans effet reste donc un coup interne ou décoratif.

from dataclasses import dataclass
import sys

ICT_DIR = ROOT.parents[1] / "IIT" / "ICT-Series"
if str(ICT_DIR) not in sys.path:
    sys.path.insert(0, str(ICT_DIR))

from ict.signaling_convention import mutual_information
from ict.symbol_invention import InventingSignalingGame
from ict.collective_adoption import AdoptionGame
from ict.concept_inoculation import ConceptInoculation
from ict.inhibited_invention import InhibitedInventingGame

@dataclass(frozen=True)
class TripletSitue:
    identifiant: str
    axe: str
    forme_source: str
    controle: str
    recadrage: str
    dimensions_modifiees: tuple[str, ...]
    actions_avant: tuple[str, ...]
    actions_apres: tuple[str, ...]
    extensions_concurrentes: tuple[str, ...]
    reponse_controle: float
    reponse_recadrage: float
    uptake_observe: str
    forme_marquee: bool


def mesurer_triplet(t: TripletSitue) -> dict[str, object]:
    delta_a = len(set(t.actions_apres) - set(t.actions_avant))
    p_r = abs(t.reponse_recadrage - t.reponse_controle)
    concept_disponible = delta_a > 0
    o_t = int(concept_disponible)
    uptake_effectif = t.uptake_observe in {"reprise", "action", "refus explicite"}
    ontologique = delta_a > 0 and p_r > 0.0
    return {
        "id": t.identifiant,
        "axe": t.axe,
        "dimensions": ", ".join(t.dimensions_modifiees) or "aucune",
        "O_t": o_t,
        "ΔA_t": delta_a,
        "C_t": len(t.extensions_concurrentes),
        "P(R)": round(p_r, 3),
        "uptake_observable": t.uptake_observe,
        "accord_forme_uptake": t.forme_marquee == uptake_effectif,
        "coup_ontologique": ontologique,
    }

print("Instrument L_t situé chargé ; proxys ICT-26 à ICT-30 importés depuis leurs modules.")
Instrument L_t situé chargé ; proxys ICT-26 à ICT-30 importés depuis leurs modules.

Protocole contrôlé

Chaque ligne est un triplet forme source / contrôle / recadrage. Les scénarios sont synthétiques : aucune intention réelle n’est attribuée. uptake_observable désigne uniquement une reprise, une action, un refus explicite ou une absence de réponse dans le scaffold. Les douze triplets couvrent trois familles : contexte social, registre/indexicalité et assemblage sémiotique. Quatre contrôles négatifs isolent synonymie, mot décoratif, auto-référence et code-mixing lexical.

TRIPLETS = [
    TripletSitue("CTX-1", "contexte social", "On ferme ?", "réunion ouverte, aucun rôle assigné",
                  "fin de permanence, la personne tient la clé", ("relations", "contexte"),
                  ("continuer",), ("continuer", "verrouiller"), ("attendre",), 0.15, 0.80,
                  "action", False),
    TripletSitue("CTX-2", "contexte social", "Tu peux reprendre la main", "pair à pair, tâche inchangée",
                  "incident déclaré, rôle de pilote reconnu", ("relations", "contexte", "buts_communicatifs"),
                  ("observer",), ("observer", "piloter"), ("escalader",), 0.20, 0.75,
                  "reprise", True),
    TripletSitue("CTX-3", "contexte social", "Belle priorité", "retour informel sans artefact partagé",
                  "formulation identique sous tableau de tickets ordonné", ("contexte", "artefacts"),
                  ("commenter",), ("commenter", "réordonner"), ("contester",), 0.10, 0.60,
                  "action", False),
    TripletSitue("CTX-N", "contexte social", "On poursuit", "On continue", "On poursuit",
                  (), ("poursuivre",), ("poursuivre",), (), 0.40, 0.40, "silence", False),
    TripletSitue("REG-1", "registre/indexicalité", "C'est carré", "registre littéral entre inconnus",
                  "registre complice après une réparation partagée", ("registres", "indexicaux", "relations"),
                  ("valider",), ("valider", "clore_incident"), ("demander_preuve",), 0.25, 0.70,
                  "reprise", True),
    TripletSitue("REG-2", "registre/indexicalité", "Nous, les experts", "auto-description institutionnelle",
                  "citation distanciée après un échec collectif", ("registres", "indexicaux", "contexte"),
                  ("déclarer",), ("déclarer", "réviser_procédure"), ("défendre_statut",), 0.20, 0.65,
                  "action", True),
    TripletSitue("REG-3", "registre/indexicalité", "Encore une victoire", "annonce littérale avant résultat",
                  "énoncé ironique après échec visible", ("registres", "indexicaux", "contexte"),
                  ("annoncer",), ("annoncer", "ouvrir_postmortem"), ("minimiser",), 0.30, 0.80,
                  "refus explicite", True),
    TripletSitue("REG-N", "registre/indexicalité", "We ship le patch", "Le patch part",
                  "We ship le patch", ("signes",), ("livrer",), ("livrer",), (), 0.55, 0.55,
                  "reprise", True),
    TripletSitue("ASM-1", "assemblage sémiotique", "À toi", "phrase seule dans le fil",
                  "phrase + jeton de tour déplacé vers l'autre", ("artefacts", "relations", "contexte"),
                  ("écouter",), ("écouter", "prendre_tour"), ("refuser_tour",), 0.10, 0.85,
                  "action", False),
    TripletSitue("ASM-2", "assemblage sémiotique", "Même plan", "phrase sans support commun",
                  "phrase + carte annotée conjointement", ("artefacts", "relations", "buts_communicatifs"),
                  ("acquiescer",), ("acquiescer", "éditer_carte"), ("proposer_carte",), 0.20, 0.75,
                  "action", False),
    TripletSitue("ASM-N1", "assemblage sémiotique", "Nouveau : zorp", "mot absent", "mot affiché en marge",
                  ("signes",), ("classer",), ("classer",), ("ignorer",), 0.25, 0.25,
                  "silence", True),
    TripletSitue("ASM-N2", "assemblage sémiotique", "Cette phrase change tout", "phrase simple",
                  "phrase auto-référente sans reprise ni artefact", ("signes", "indexicaux"),
                  ("lire",), ("lire",), ("contester",), 0.30, 0.30, "silence", True),
]

resultats_lt = pd.DataFrame(mesurer_triplet(t) for t in TRIPLETS)
negatifs_attendus = {"CTX-N", "REG-N", "ASM-N1", "ASM-N2"}
assert len(resultats_lt) == 12
assert resultats_lt.groupby("axe").size().eq(4).all()
assert set(resultats_lt.loc[~resultats_lt["coup_ontologique"], "id"]) == negatifs_attendus
assert (resultats_lt["coup_ontologique"] == ((resultats_lt["ΔA_t"] > 0) & (resultats_lt["P(R)"] > 0))).all()
print(resultats_lt.to_string(index=False))
print()
print("Bilan :", resultats_lt["coup_ontologique"].value_counts().rename(index={True: "positifs", False: "négatifs"}).to_dict())
print("Désaccords forme/uptake :", int((~resultats_lt["accord_forme_uptake"]).sum()), "/", len(resultats_lt))
    id                   axe                               dimensions  O_t  ΔA_t  C_t  P(R) uptake_observable  accord_forme_uptake  coup_ontologique
 CTX-1       contexte social                      relations, contexte    1     1    1  0.65            action                False              True
 CTX-2       contexte social  relations, contexte, buts_communicatifs    1     1    1  0.55           reprise                 True              True
 CTX-3       contexte social                      contexte, artefacts    1     1    1  0.50            action                False              True
 CTX-N       contexte social                                   aucune    0     0    0  0.00           silence                 True             False
 REG-1 registre/indexicalité         registres, indexicaux, relations    1     1    1  0.45           reprise                 True              True
 REG-2 registre/indexicalité          registres, indexicaux, contexte    1     1    1  0.45            action                 True              True
 REG-3 registre/indexicalité          registres, indexicaux, contexte    1     1    1  0.50   refus explicite                 True              True
 REG-N registre/indexicalité                                   signes    0     0    0  0.00           reprise                 True             False
 ASM-1 assemblage sémiotique           artefacts, relations, contexte    1     1    1  0.75            action                False              True
 ASM-2 assemblage sémiotique artefacts, relations, buts_communicatifs    1     1    1  0.55            action                False              True
ASM-N1 assemblage sémiotique                                   signes    0     0    1  0.00           silence                False             False
ASM-N2 assemblage sémiotique                       signes, indexicaux    0     0    1  0.00           silence                False             False

Bilan : {'positifs': 8, 'négatifs': 4}
Désaccords forme/uptake : 6 / 12

Lecture du résultat — le lexique ne décide pas

Le tableau est falsifiable ligne par ligne : les positifs doivent avoir simultanément ΔA_t > 0 et P(R) > 0. Les quatre contrôles imposés restent négatifs malgré une proximité lexicale, un signe neuf, une auto-référence ou du code-mixing. La colonne accord_forme_uptake rend visibles les cas où la forme textuelle et la réponse observable divergent : une formulation peu marquée peut ouvrir une action via le contexte ou l’artefact, tandis qu’une forme très marquée peut rester sans effet.

O_t vaut 1 lorsqu’au moins une action auparavant indisponible devient disponible, 0 sinon ; il ne prouve pas à lui seul le coup ontologique. L’information mutuelle d’ICT-26 est réutilisée séparément dans le banc de calibration ci-dessous. C_t compte les extensions concurrentes déclarées. P(R) est la divergence absolue entre les réponses synthétiques appariées do(recadrage) et contrôle, non une probabilité psychologique.

# Pont exécutable vers les cinq bancs ICT-26→30 : leurs classes sont réutilisées,
# jamais recopiées. Ces mini-bancs vérifient que les proxys restent opératoires.
libre = InventingSignalingGame(3, 1, invention_rate=0.2, rng=np.random.default_rng(11))
libre.train(800, anneal_to=0.15)

inhibe = InhibitedInventingGame(
    3, 1, invention_rate=0.2, inhibition_growth=0.2,
    rng=np.random.default_rng(11),
)
inhibe.train(800, anneal_to=0.15)

sans_source = AdoptionGame(18, 3, instigator_fraction=0.0, rng=np.random.default_rng(12))
sans_source.train(500, anneal_to=0.15)
avec_source = AdoptionGame(18, 3, instigator_fraction=0.8, rng=np.random.default_rng(12))
avec_source.train(500, anneal_to=0.15)

inoculation = ConceptInoculation(
    18, seed_fraction=2 / 18, transmission_rate=0.35,
    confirmation_bias=0.2, rng=np.random.default_rng(13),
)
inoculation.train(120)

pont_ict = pd.Series({
    "ICT-26 O_t_MI_max_bits": round(mutual_information(np.eye(3) * 10), 3),
    "ICT-27 vocabulaire_libre": libre.final_vocab_size(),
    "ICT-28 divergence_adoption": round(abs(avec_source.adoption_rate() - sans_source.adoption_rate()), 3),
    "ICT-29 convertis_hors_graine": inoculation.converted_beyond_seed(),
    "ICT-30 vocabulaire_inhibe": inhibe.final_vocab_size(),
})
print(pont_ict.to_string())
ICT-26 O_t_MI_max_bits           1.585
ICT-27 vocabulaire_libre         3.000
ICT-28 divergence_adoption       1.000
ICT-29 convertis_hors_graine    15.000
ICT-30 vocabulaire_inhibe        2.000

Limites et sources canoniques

Le mini-banc confirme seulement que les proxys des expériences A→E sont appelables depuis ce notebook : convention/information, invention, adoption, inoculation et inhibition. Il ne transforme pas les scénarios synthétiques en données sociales et ne mesure aucun corpus public. Toute observation sur des personnes ou un corpus externe reste bloquée par le protocole #13309.

Sources archivées et vérifiées dans G:\Mon Drive\MyIA\IA\Bibliographie IA\Language\Linguistic Anthropology\ :

  • 2004 - Duranti - Agency in Language.pdf — parler comme action, conséquences et responsabilité ;
  • 2010 - Wardhaugh - An Introduction to Sociolinguistics.pdf — compétence communicative, variation, relations et contexte ;
  • 2017 - Pennycook - Translanguaging and Semiotic Assemblages.pdf — relations entre ressources, personnes, artefacts, activité et espace ;
  • 2020 - Bisk et al - Experience Grounds Language.pdf — sens ancré dans usage, action et monde social ;
  • 2025 - Liu et al - Culturally Aware and Adapted NLP.pdf — branches idéationnelle, linguistique et sociale.

La recension d’Ahearn mentionnée dans #14034 reste une source secondaire : le tuple ci-dessus est une opérationnalisation du grain, pas une attribution de causalité à cette recension.

11. Sensibilité — et les trois exercices (#2161)

Sensibilité au choix de la partition (graine) : le protocole se ré-exécute à l’identique avec SEED=43 (exercice 1). Sensibilité aux seuils de revendication (exercice 2). Glue qui s’abstient — déferer au spécialiste le plus compétent localement plutôt que fusionner (exercice 3) : c’est la direction que le −15 % de la majorité brute à z=2 suggérait chez ICT-34, et #13041 la déclare livrable recevable.

# Exercice 1 a completer
# TODO etudiant : re-executer avec SEED=43 (cellule de config) et comparer le tableau du verdict :
# est-ce que l'ordre GLU vs SPEC tient au changement de partition ?
pass
print("Exercice a completer")
Exercice a completer

Exercice 2 — sensibilité du domaine RDF

Le seuil d’annotations décide quelles entrées la lecture RDF revendique. Faites-le varier pour distinguer un gain de précision d’une simple réduction de couverture.

# Exercice 2 a completer
# TODO etudiant : faire varier le seuil de revendication RDF (n_annotations >= 5 -> 8 -> 12)
# et observer l'effet sur le taux d'incompatibilite moyen et sur l'accuracy de la zone disputee.
pass
print("Exercice a completer")
Exercice a completer

Exercice 3 — une glue capable de s’abstenir

Après la sensibilité des données et des seuils, ce dernier exercice agit sur la règle de décision elle-même : différer localement au meilleur spécialiste lorsque son avantage est net, fusionner sinon.

# Exercice 3 a completer
# TODO etudiant : construire une glue qui s'abstient : sur la zone disputee, si l'ecart de
# fiabilite locale (accuracy train par famille) entre la meilleure et la deuxieme lecture
# depasse un seuil, deferer a la meilleure ; sinon fusionner (R2). Comparer au tableau du verdict.
pass
print("Exercice a completer")
Exercice a completer

12. Ponts et limites

Direction Lien Relation
Protocole de recollement (synthétique) ICT-34-BancRecollementLectures-Python.ipynb ce notebook en est la mise sur substance réelle — l’oracle R3 y est relégué au rang de borne supérieure
Incompatibilité OWL/CSV (substance réelle, pluralité dégénérée) Argumentation-Obs-03-Recollement-Lectures-Python.ipynb (#12290) sa jonction FR normalisée et ses comptes servent de certificat à la conversion N-Triples
EPIC Chantier 3 #12206 strate 6 : le branchement réel (moteurs hétérogènes sur corpus réel)
Grain #13041 acceptance 1–6 cochées §1–§9

Limites écrites. La lecture GRAPH est transductive : ses probabilités « out-of-fold » pour l’ajustement de la glue sont calculées par propagation repliée (graines train amputées du pli évalué — honnête vis-à-vis des labels, mais le graphe de similarité lui-même est construit sur tout le corpus, vocabulaire n-gram inclus ; non supervisé, donc sans fuite de label). La lecture RDF dépend d’une re-sérialisation mécanique certifiée par réconciliation, pas d’un parseur OWL2/XML natif (aucun disponible côté Python : rdflib et owlready2 échouent sur le format fonctionnel — tentés §3). Le verrou de fuite (§2) est une assertion sur les blocs de signaux, pas une preuve d’indépendance des résidus.

Conclusion

Pour #14034, le notebook ajoute désormais une deuxième mise à l’épreuve : douze triplets synthétiques montrent qu’une forme textuelle ne devient pas ontologique par sa seule nouveauté. Le seuil est conjoint — action nouvelle et divergence contrôlée — et le pont ICT-26→30 vérifie avec les moteurs existants les cinq dimensions convention, invention, adoption, inoculation et inhibition. Cette démonstration reste un instrument synthétique, sans inférence d’intention ni observation de corpus public.

Sur substance réelle et pluralité réelle, la réponse à la question de #13041 est oui — conditionnellement : une glue ajustée sur des prédictions out-of-fold du train bat le meilleur spécialiste seul sur la sous-population disputée (56,3 % contre 49,0 %, +7,3 points), là même où la majorité brute est nuisible (44,0 %). Le recouvrement qu’elle fusionne est réellement disputé (taux d’incompatibilité moyen 50,5 %, contre 1,5 % chez #12290), et la borne supérieure affichée (oracle 76,1 % sur la zone disputée) dit que la majorité de la marge reste ouverte : la glue globale capture ~27 % de l’écart oracle-spécialiste, le reste exige de la compétence locale.

Ce que le banc enseigne du pont théorie des jeux ↔︎ fusion de lectures : le recollement est un jeu d’information complète mais asymétrique — chaque lecture détient un signal que les autres n’ont pas (asymétrie), et la fusion est un mécanisme d’agrégation qui ne crée de valeur que si les signaux agrégés portent des informations différentes (sinon la contradiction est un artefact d’encodage, cas #12290). La leçon mesurée ici : la valeur de la glue ne vient pas des lectures fortes (elles sont redondantes entre elles) mais du désaccord informatif des lectures faibles — RDF seule vaut 0,348, mais sa contradiction systématique avec LEX est exactement ce que le stacking transforme en signal. Comme dans les modèles d’information asymétrique de GameTheory-17b : le marché (la fusion) valorise l’information privée même détenue par des agents peu fiables, à condition que le mécanisme l’extrait — et la majorité brute, elle, ne l’extrait pas.

Retour au sommet