A la fin de ce notebook, vous saurez : 1. Distinguer apprentissage passif (exemples imposes, SL-1) et actif (l’apprenant pose des questions) 2. Formaliser le modèle MAT (Minimally Adequate Teacher) : requêtes d’appartenance et d’equivalence 3. Implementer la table d’observation d’Angluin (fermeture, consistance) 4. Derouler l’algorithme L* complet et apprendre un DFA inconnu 5. Relier la garantie de minimalite au theoreme de Myhill-Nerode 6. Mesurer ce qui se passe quand l’oracle d’equivalence devient approximatif (echantillonnage, PAC)
Prerequis
SL-1 (espaces d’hypotheses, apprentissage passif)
Notions d’automates finis déterministes (rappel complet en section 1)
Hommage — Stearns et la théorie des automates : LL(k), régularité, hiérarchies
Richard E. Stearns (1936–2026), co-lauréat du prix Turing 1993 pour avoir fondé la théorie de la complexité computationnelle, est aussi un artisan central de la théorie des langages formels que ce notebook met en pratique :
Lewis & Stearns, « Syntax-Directed Transduction » (JACM, 1968) a défini les grammaires LL(k) — la famille de parseurs descendants que la bibliographie ACM présente comme la première méthode d’analyse que les étudiants rencontrent ; les propriétés théoriques ont été établies avec Knuth puis Rosenkrantz & Stearns (1970).
Stearns, « A Regularity Test for Pushdown Machines » (1967) : si un langage régulier possède un DPDA de taille n, il possède un DFA de taille au plus n(n(n^O(n))) — un écart super-exponentiel mais calculable, resserré depuis par Valiant (2(2O(n))). C’est l’un des rares exemples où l’écart entre deux modèles de machines dépasse l’exponentiel sans devenir indécidable.
Stearns a aussi montré que les langages hors-contexte se reconnaissent en espace O(log² n) et établi une hiérarchie en espace — le pendant spatial de la hiérarchie en temps de Hartmanis–Stearns (1965).
Hommage complet (hiérarchie de Hartmanis–Stearns, série Complexity/) : issue #15949 ; autres résonances — paradoxe d’Arrow 1959 et jeux répétés à information incomplète (voir GameTheory/SocialChoice/ et GameTheory/game_theory_lean/README.md).
Tous les algorithmes de la serie jusqu’ici sont passifs : l’apprenant recoit un jeu d’exemples qu’il n’a pas choisi (les 12 restaurants de SL-1, les triples du KG de SL-8) et doit s’en contenter. Or certains concepts sont exponentiellement plus faciles a apprendre si l’apprenant peut poser des questions.
Dana Angluin (1987) formalise cela avec le MAT (Minimally Adequate Teacher), un professeur qui repond a exactement deux types de requêtes :
Requête
Question
Reponse
Appartenance (MQ)
« le mot \(w\) est-il dans le langage cible \(L\) ? »
La cible est ici un langage regulier : un ensemble de mots reconnu par un automate fini déterministe (DFA). C’est un objet pleinement symbolique — etats, transitions, etats acceptants — et le theoreme central d’Angluin est remarquable :
Theoreme (Angluin 1987). L* apprend le DFA minimal de tout langage regulier avec un nombre de requêtes polynomial en le nombre d’etats \(n\) du DFA minimal et la longueur \(m\) du plus long contre-exemple.
A titre de comparaison, apprendre un DFA passivement (trouver le plus petit DFA consistant avec un echantillon donne) est NP-difficile (Gold 1978). Le droit de poser des questions change la classe de complexite du problème.
Commencons par la cible : un DFA jouet que L* devra decouvrir sans jamais regarder sa structure, uniquement en l’interrogeant.
// Un DFA = (etats, alphabet, transitions, etat initial, etats acceptants)using System.Collections.Generic;using System.Linq;using System.Text;publicclass Dfa{public HashSet<string> States {get;set;}public List<char> Alphabet {get;set;}public Dictionary<(string,char),string> Delta {get;set;}publicstring Start {get;set;}public HashSet<string> Accepting {get;set;}publicDfa(IEnumerable<string> states, IEnumerable<char> alphabet, IEnumerable<((string,char),string)> delta,string start, IEnumerable<string> accepting){ States = states.ToHashSet(); Alphabet = alphabet.ToList(); Delta =new Dictionary<(string,char),string>();foreach(var(k, v)in delta) Delta[k]= v; Start = start; Accepting = accepting.ToHashSet();}publicboolAccepts(string word){var q = Start;foreach(var ch in word) q = Delta[(q, ch)];return Accepting.Contains(q);}publicint StateCount => States.Count;publicoverridestringToString()=> $"Dfa({States.Count} etats, {Accepting.Count} acceptants)";}// Langage cible (cache pour L*) : mots sur {a, b} avec un nombre PAIR de 'a'// ET un nombre PAIR de 'b'. Etat = (parite des a, parite des b) -> 4 etats.List<char> ALPHABET =new List<char>{'a','b'};string[] _states ={"ee","eo","oe","oo"};// (pair/impair a, pair/impair b)var _delta =new List<((string,char),string)>();foreach(var s in _states){char pa = s[0], pb = s[1]; _delta.Add(((s,'a'),(pa =='e'?"o":"e")+ pb.ToString())); _delta.Add(((s,'b'), pa.ToString()+(pb =='e'?"o":"e")));}Dfa TARGET =newDfa(_states, ALPHABET, _delta,"ee",new[]{"ee"});// L'apprenant n'aura acces qu'a cette fonction (la requete d'appartenance) :boolMembershipQuery(string word)=> TARGET.Accepts(word);// Verification rapide sur quelques motsConsole.WriteLine($"Cible : {TARGET} (structure invisible pour l'apprenant)");Console.WriteLine($"{"mot",8} | nb_a | nb_b | dans L ?");Console.WriteLine(newstring('-',36));foreach(var w innew[]{"","a","b","ab","aabb","abab","aab","bbaa","abba"}){int na = w.Count(c => c =='a'), nb = w.Count(c => c =='b'); Console.WriteLine($"{(w == "" ? "eps" : w),8} | {na,4} | {nb,4} | {(MembershipQuery(w) ? "OUI" : "non")}");}
Cible : Dfa(4 etats, 1 acceptants) (structure invisible pour l'apprenant)
mot | nb_a | nb_b | dans L ?
------------------------------------
eps | 0 | 0 | OUI
a | 1 | 0 | non
b | 0 | 1 | non
ab | 1 | 1 | non
aabb | 2 | 2 | OUI
abab | 2 | 2 | OUI
aab | 2 | 1 | non
bbaa | 2 | 2 | OUI
abba | 2 | 2 | OUI
Pourquoi ce langage ?
Le mot vide est accepte (0 est pair), ab est rejete (1 ‘a’, 1 ‘b’), aabb et abab sont acceptes. Le DFA minimal a exactement 4 etats — le produit des deux parites — et aucune conjonction d’attributs a la SL-1 ne peut le representer : l’appartenance depend d’un compteur modulaire, pas de la presence ou l’absence d’un motif. C’est un concept hors de portee des espaces d’hypotheses de SL-1, et parfaitement dans celui de L*.
L’apprenant ne voit que membership_query : une boite noire mot -> booléen. Tout l’enjeu est de reconstruire la structure (etats, transitions) a partir de reponses oui/non bien choisies.
Schema : le DFA cible (invisible pour l’apprenant)
Le langage cible – nombre pair de aet pair de b – est reconnu par le DFA minimal a 4 etats, un par couple (parite des a, parite des b). C’est cette structure (etats + transitions) que L* doit reconstruire a partir des seules reponses oui/non de l’oracle ; elle lui est cachee. La voici :
stateDiagram-v2
direction LR
[*] --> ee
ee --> oe : a
ee --> eo : b
oe --> ee : a
oe --> oo : b
eo --> oo : a
eo --> ee : b
oo --> eo : a
oo --> oe : b
ee : ee (pair a, pair b)
oe : oe (impair a, pair b)
eo : eo (pair a, impair b)
oo : oo (impair a, impair b)
classDef accept fill:#d1e7dd,stroke:#0f5132,stroke-width:3px,color:#0a3622
class ee accept
Lecture. L’etat initial ee (pair, pair) est aussi le seul etat acceptant (en vert) : un mot est dans le langage si, en le lisant lettre par lettre, on revient en ee. Chaque a bascule la parite des a (colonne gauche/droite), chaque b bascule celle des b – d’ou la symetrie en damier. Aucune conjonction d’attributs a la SL-1 ne capture ce compteur modulaire : c’est précisément ce qui place ce concept hors de portee de l’apprentissage passif et dans celui de L*.
2. La table d’observation
L* organise ses requêtes d’appartenance dans une table d’observation\((S, E, T)\) :
\(S\) : un ensemble de prefixes d’acces (chaque \(s \in S\) est un chemin candidat vers un etat du DFA) ;
\(E\) : un ensemble de suffixes distinguants (des « expériences » qui separent les etats) ;
\(T(s \cdot e) \in \{0, 1\}\) : la reponse de l’oracle a la requête \(s \cdot e \in L\) ?
La ligne d’un prefixe \(s\) est le vecteur \(row(s) = (T(s \cdot e))_{e \in E}\). L’intuition centrale : deux prefixes qui ont la même ligne menent (provisoirement) au même etat. C’est une approximation finie de la congruence de Myhill-Nerode : \(u \equiv_L v \iff \forall w,\; uw \in L \Leftrightarrow vw \in L\) — ici on ne teste pas tous les suffixes \(w\), seulement ceux de \(E\).
Pour pouvoir construire un DFA a partir de la table, deux proprietes sont requises :
Propriete
Definition
Si violee…
Fermeture
\(\forall s \in S, a \in \Sigma\), \(row(s a)\) est la ligne d’un élément de \(S\)
la transition \(row(s) \xrightarrow{a} {?}\) sort de la table : promouvoir\(sa\) dans \(S\)
Consistance
\(row(s_1) = row(s_2) \Rightarrow \forall a, row(s_1 a) = row(s_2 a)\)
deux etats fusionnes a tort : ajouter le suffixe \(a e\) qui les separe a \(E\)
publicclass ObservationTable{public List<char> A {get;set;}public Func<string,bool> Mq {get;set;}public List<string> S {get;set;}=new List<string>{""};public List<string> E {get;set;}=new List<string>{""};public Dictionary<string,bool> T {get;set;}=new Dictionary<string,bool>();publicint NMq {get;set;}=0;publicObservationTable(IEnumerable<char> alphabet, Func<string,bool> mq){ A = alphabet.ToList(); Mq = mq;Fill();}publicboolAsk(string w){if(!T.ContainsKey(w)){ T[w]=Mq(w); NMq++;}return T[w];}publicvoidFill(){foreach(var s in S.Concat(S.SelectMany(s => A.Select(a => s + a))))foreach(var e in E)Ask(s + e);}// Ligne d'un prefixe = signature textuelle sur les colonnes E (comparable/hashable).publicstringRow(string s)=>string.Join("", E.Select(e =>Ask(s + e)?"1":"0"));publicstringFindUnclosed(){var rowsS = S.Select(s =>Row(s)).ToHashSet();foreach(var s in S)foreach(var a in A)if(!rowsS.Contains(Row(s + a)))return s + a;returnnull;}publicstringFindInconsistency(){for(int i =0; i < S.Count; i++)for(int j = i +1; j < S.Count; j++){if(Row(S[i])!=Row(S[j]))continue;foreach(var a in A)foreach(var e in E)if(Ask(S[i]+ a + e)!=Ask(S[j]+ a + e))return a + e;}returnnull;}publicvoidShow(){string eps ="eps";var cols = E.Select(e => e ==""? eps : e).ToList();int w = Math.Max(6, S.Max(s => s.Length)+2);stringLab(string s)=>(s ==""? eps : s).PadRight(w);stringRowStr(string s)=>string.Join(" ",Row(s).Select(v =>(v =='1'?"1":"0").PadLeft(4))); Console.WriteLine(newstring(' ', w)+" | "+string.Join(" ", cols.Select(c => c.PadLeft(4)))); Console.WriteLine(newstring('-', w +3+5* cols.Count));foreach(var s in S) Console.WriteLine(Lab(s)+" | "+RowStr(s)); Console.WriteLine(newstring('-', w +3+5* cols.Count));var inS = S.ToHashSet();foreach(var s in S)foreach(var a in A){var sa = s + a;if(inS.Contains(sa))continue; Console.WriteLine(Lab(sa)+" | "+RowStr(sa));}}}// Table initiale : S = {epsilon}, E = {epsilon}var table =newObservationTable(ALPHABET, MembershipQuery);Console.WriteLine("Table initiale (haut : S ; bas : les successeurs S.A) :\n");table.Show();var sa0 = table.FindUnclosed();Console.WriteLine($"\nRequetes MQ posees : {table.NMq}");Console.WriteLine($"Fermee ? {(sa0 == null ? "oui" : "NON :row(" + sa0 + ") absente de S")}");
Table initiale (haut : S ; bas : les successeurs S.A) :
| eps
--------------
eps | 1
--------------
a | 0
b | 0
Requetes MQ posees : 3
Fermee ? NON : row(a) absente de S
Lecture de la table initiale
La ligne de \(\varepsilon\) vaut \((1)\) (le mot vide est dans \(L\)), mais celles de a et b valent \((0)\) : la table n’est pas fermee — l’automate conjecture aurait une transition vers un etat qui n’existe pas encore. Le remede est mecanique : promouvoir le prefixe fautif dans \(S\), ce qui créé l’etat manquant, puis re-remplir la table. L’algorithme L* n’est rien d’autre que la repetition disciplinee de ce geste, plus le traitement des contre-exemples.
3. L’algorithme L*
L*(alphabet, MQ, EQ):
initialiser la table (S = E = {epsilon})
repeter:
tant que la table n'est pas fermee ou pas consistante:
non fermee -> promouvoir le prefixe s.a fautif dans S
non consistante -> ajouter le suffixe a.e separateur a E
H <- DFA conjecture a partir des lignes de la table
si EQ(H) repond OUI : retourner H
sinon (contre-exemple c) : ajouter TOUS les prefixes de c a S
La conjecture se lit directement dans la table : les etats sont les lignes distinctes de \(S\), l’etat initial est \(row(\varepsilon)\), un etat est acceptant si sa première colonne (\(e = \varepsilon\)) vaut 1, et la transition par \(a\) envoie \(row(s)\) sur \(row(sa)\) — la fermeture garantit que cette ligne existe, la consistance qu’elle ne depend pas du representant \(s\) choisi.
Dfa Conjecture(ObservationTable table){var reps =new Dictionary<string,string>();// ligne (signature) -> prefixe representantforeach(var s in table.S)if(!reps.ContainsKey(table.Row(s))) reps[table.Row(s)]= s;var delta =new List<((string,char),string)>();foreach(var kv in reps){var sig = kv.Key;var s = kv.Value;foreach(var a in table.A) delta.Add(((sig, a), table.Row(s + a)));}string start = table.Row("");var accepting = reps.Keys.Where(sig => sig[0]=='1').ToList();returnnewDfa(reps.Keys, table.A, delta, start, accepting);}stringFindCounterexample(Dfa hyp, Dfa target, IEnumerable<char> alphabet){var alph = alphabet.ToList();var start =(hyp.Start, target.Start);var seen =new HashSet<(string,string)>(){ start };var queue =new Queue<(string w,string qh,string qt)>(); queue.Enqueue(("", hyp.Start, target.Start));while(queue.Count>0){var(w, qh, qt)= queue.Dequeue();if(hyp.Accepting.Contains(qh)!= target.Accepting.Contains(qt))return w;foreach(var a in alph){var nxt =(hyp.Delta[(qh, a)], target.Delta[(qt, a)]);if(seen.Add(nxt)) queue.Enqueue((w + a, nxt.Item1, nxt.Item2));}}returnnull;}(Dfa hyp, ObservationTable table,int nEq)Lstar(IEnumerable<char> alphabet, Func<string,bool> mq, Func<Dfa,string> eq,bool verbose =true){var tbl =newObservationTable(alphabet, mq);int nEq =0, round_ =0;while(true){// 1. Stabiliser la tablewhile(true){var sa = tbl.FindUnclosed();if(sa !=null){ tbl.S.Add(sa); tbl.Fill();if(verbose) Console.WriteLine($" table non fermee -> S += {sa}");continue;}var ae = tbl.FindInconsistency();if(ae !=null){ tbl.E.Add(ae); tbl.Fill();if(verbose) Console.WriteLine($" table non consistante -> E += {ae}");continue;}break;}// 2. Conjecturer et interroger le professeurvar hyp =Conjecture(tbl); nEq++; round_++;var cex =eq(hyp);if(verbose) Console.WriteLine($"Conjecture {round_} : {hyp.StateCount} etats -> {(cex == null ? "ACCEPTEE" : "contre-exemple " + cex)}");if(cex ==null)return(hyp, tbl, nEq);// 3. Integrer le contre-exemple (tous ses prefixes dans S, Angluin 1987)for(int i =1; i <= cex.Length; i++)if(!tbl.S.Contains(cex[..i])) tbl.S.Add(cex[..i]); tbl.Fill();}}Console.WriteLine("L* implemente : ObservationTable + conjecture + boucle de raffinement.");
L* implemente : ObservationTable + conjecture + boucle de raffinement.
4. Exécution complete sur le langage cible
Lancons L* contre la boite noire. L’oracle d’equivalence est ici exact (BFS sur l’automate produit) : un luxe possible seulement parce que nous connaissons la cible — la section 6 le remplacera par ce qu’on a vraiment en pratique.
var(learned, finalTable, nEqRun)=Lstar(ALPHABET, MembershipQuery, h =>FindCounterexample(h, TARGET, ALPHABET));Console.WriteLine($"\nDFA appris : {learned}");Console.WriteLine($"Requetes d'appartenance : {finalTable.NMq}");Console.WriteLine($"Requetes d'equivalence : {nEqRun}");string eList =string.Join(", ", finalTable.E.Select(e =>"'"+ e +"'"));Console.WriteLine($"\nTable finale (S = {finalTable.S.Count} prefixes, E = {{ {eList} }} ) :\n");finalTable.Show();// Verification independante exhaustive jusqu'a la longueur 10IEnumerable<string>AllWords(List<char> alphabet,int maxLen){yieldreturn"";var frontier =new List<string>{""};for(int _ =0; _ < maxLen; _++){var next =new List<string>();foreach(var w in frontier)foreach(var a in alphabet) next.Add(w + a); frontier = next;foreach(var w in frontier)yieldreturn w;}}var allW =AllWords(ALPHABET,10).ToList();int mismatch = allW.Count(w => learned.Accepts(w)!= TARGET.Accepts(w));Console.WriteLine($"\nVerification exhaustive : {allW.Count - mismatch}/{allW.Count} mots (longueur <= 10) identiques");
table non fermee -> S += a
Conjecture 1 : 2 etats -> contre-exemple ba
table non consistante -> E += a
table non consistante -> E += b
Conjecture 2 : 4 etats -> ACCEPTEE
DFA appris : Dfa(4 etats, 1 acceptants)
Requetes d'appartenance : 19
Requetes d'equivalence : 2
Table finale (S = 4 prefixes, E = { '', 'a', 'b' } ) :
| eps a b
------------------------
eps | 1 0 0
a | 0 1 0
b | 0 0 1
ba | 0 0 0
------------------------
aa | 1 0 0
ab | 0 0 0
bb | 1 0 0
baa | 0 0 1
bab | 0 1 0
Verification exhaustive : 2047/2047 mots (longueur <= 10) identiques
Interpretation : que s’est-il passe ?
La première conjecture est minuscule et fausse. Avec \(S = E = \{\varepsilon\}\) stabilises, la table ne distingue que « accepte / rejette » : 2 etats. Le professeur repond par un contre-exemple court.
Le contre-exemple force la croissance. Ses prefixes entrent dans \(S\), la consistance casse, des suffixes distinguants entrent dans \(E\) — chaque rejet d’une conjecture ajoute au moins un etat a la suivante. Comme le DFA minimal a 4 etats, il y a au plus 4 conjectures : la terminaison est garantie, pas seulement esperee.
Le cout total est modeste : quelques dizaines de MQ et une poignee d’EQ pour identifier exactement un langage infini. Comparez avec SL-1 : 12 exemples imposes n’avaient pas suffi a stabiliser une simple conjonction. Le choix actif des questions est la source de l’efficacite — chaque requête de la table sert a trancher une distinction precise entre deux etats candidats.
5. Garanties : minimalite et complexite polynomiale
Le theoreme de Myhill-Nerode (1958) donne la borne inferieure structurelle : le nombre d’etats du DFA minimal de \(L\) est exactement le nombre de classes de la congruence \(u \equiv_L v \iff \forall w,\; uw \in L \Leftrightarrow vw \in L\).
Origine. L’exposition canonique du theoreme de Myhill-Nerode et de son rôle pour la minimalite des automates se trouve dans Hopcroft, J. E., Motwani, R. & Ullman, J. D., Introduction to Automata Theory, Languages, and Computation (3e ed., Pearson/Addison-Wesley). Le theoreme (Myhill 1958, Nerode 1958) caracterise un langage regulier par sa congruence syntactique : deux prefixes sont equivalents s’ils ne peuvent etre distingues par aucun suffixe.
L* s’y adosse directement :
chaque paire de lignes distinctes de la table est un certificat de non-equivalence (un suffixe de \(E\) separe les deux prefixes), donc la conjecture n’a jamais plus d’etats que le DFA minimal ;
l’oracle d’equivalence fournit des contre-exemples tant qu’elle en a moins ;
l’algorithme s’arrete donc précisément sur le DFA minimal — il ne peut pas retourner autre chose.
Cote complexite : \(O(n)\) requêtes d’equivalence et \(O(|\Sigma| \cdot m \cdot n^2)\) requêtes d’appartenance, ou \(n\) est le nombre d’etats du minimal et \(m\) la longueur du plus long contre-exemple. Verifions empiriquement la croissance sur la famille \(L_k\) = « le nombre de ‘a’ est divisible par \(k\) » (DFA minimal a \(k\) etats) :
// Verification empirique 1 : les classes de Myhill-Nerode du langage cibleDictionary<string, List<string>>NerodeClasses(Func<string,bool> mq, List<char> alphabet,int maxPrefix =4,int maxSuffix =4){var suffixes =AllWords(alphabet, maxSuffix).ToList();var sig =new Dictionary<string, List<string>>();foreach(var u inAllWords(alphabet, maxPrefix)){var key =string.Join("", suffixes.Select(w =>mq(u + w)?"1":"0"));if(!sig.ContainsKey(key)) sig[key]=new List<string>(); sig[key].Add(u);}return sig;}var classes =NerodeClasses(MembershipQuery, ALPHABET);Console.WriteLine($"Classes de Myhill-Nerode empiriques (prefixes <= 4, suffixes <= 4) : {classes.Count}");foreach(var kv in classes) Console.WriteLine($" classe de {(kv.Value[0] == "" ? "eps" : ("'" + kv.Value[0] + "'")),8} : {kv.Value.Count} prefixes");// Verification empirique 2 : croissance du cout en fonction de kConsole.WriteLine($"\n{"k",3} | etats appris | MQ | EQ");Console.WriteLine(newstring('-',34));for(int k =2; k <=6; k++){boolMqK(string word)=> word.Count(c => c =='a')% k ==0;var dkStates = Enumerable.Range(0, k).Select(i => i.ToString()).ToList();var dkDelta =new List<((string,char),string)>();foreach(var q in dkStates){ dkDelta.Add(((q,'a'),((int.Parse(q)+1)% k).ToString())); dkDelta.Add(((q,'b'), q));}var dk =newDfa(dkStates, ALPHABET, dkDelta,"0",new[]{"0"});var(h, tbl, neq)=Lstar(ALPHABET, MqK, h2 =>FindCounterexample(h2, dk, ALPHABET), verbose:false); Console.WriteLine($"{k,3} | {h.StateCount,12} | {tbl.NMq,4} | {neq,2}");}
Classes de Myhill-Nerode empiriques (prefixes <= 4, suffixes <= 4) : 4
classe de eps : 11 prefixes
classe de 'a' : 5 prefixes
classe de 'b' : 5 prefixes
classe de 'ab' : 10 prefixes
k | etats appris | MQ | EQ
----------------------------------
2 | 2 | 5 | 1
3 | 3 | 14 | 2
4 | 4 | 23 | 2
5 | 5 | 34 | 2
6 | 6 | 47 | 2
Interpretation
Les classes empiriques de Myhill-Nerode sont bien au nombre de 4 — et L* a appris un DFA a 4 etats : la garantie de minimalite n’est pas un a-cote théorique, c’est le mécanisme même de l’algorithme. Sur la famille \(L_k\), le nombre d’etats appris suit exactement \(k\) et le nombre de requêtes croit polynomialement, très loin de l’explosion exponentielle qu’exigerait l’enumeration des DFA candidats.
A noter : c’est un des rares algorithmes de la serie a venir avec une garantie exacte (identification du concept, pas approximation). Le prix est le professeur : sans oracle d’equivalence fiable, la garantie s’evapore — voyons précisément comment.
6. L’oracle d’equivalence en pratique
Dans le monde reel (retro-ingenierie d’un protocole, test d’un composant boite noire), personne ne sait repondre exactement a « ton hypothese est-elle correcte ? ». On approxime l’EQ par echantillonnage : tirer \(N\) mots au hasard, comparer l’hypothese et le système, renvoyer le premier desaccord. S’il n’y en a aucun, declarer l’hypothese correcte… en croisant les doigts.
C’est exactement le cadre PAC (SL-3) : le DFA retourne est probablement approximativement correct — mais plus exactement correct. Et la nuance n’est pas academique : elle depend entierement de la densite des desaccords entre une hypothese fausse et la cible. Mesurons-la sur deux langages aux profils opposes : le langage des parites (ou une hypothese fausse se trompe sur environ un mot sur quatre) et un langage-aiguille, « les mots contenant le facteur aabbaabb », ou les mots discriminants sont rares.
using System.Globalization;// Oracle d'equivalence approximatif : N mots aleatoires, premier desaccord.Func<Dfa,string>MakeSamplingEq(Func<string,bool> mq, List<char> alphabet,int nSamples,int maxLen =20,int seed =0){var rng =newRandom(seed);stringEq(Dfa hyp){for(int _ =0; _ < nSamples; _++){int len = rng.Next(0, maxLen +1);var sb =newStringBuilder();for(int i =0; i < len; i++) sb.Append(alphabet[rng.Next(alphabet.Count)]);var w = sb.ToString();if(hyp.Accepts(w)!=mq(w))return w;}returnnull;}return Eq;}// Le langage-aiguille : mots contenant le facteur "aabbaabb".// Son DFA minimal est l'automate de Knuth-Morris-Pratt du motif : 9 etats.Dfa FactorDfa(string pattern, List<char> alphabet){int n = pattern.Length;var delta =new List<((string,char),string)>();for(int q =0; q <= n; q++)foreach(var ch in alphabet){string target;if(q == n) target = n.ToString();// motif deja vu : etat absorbantelseif(pattern[q]== ch) target =(q +1).ToString();else{var s = pattern[..q]+ ch;// repli KMP : plus long prefixe du motif encore suffixeint best =0;for(int kk = s.Length; kk >=0; kk--)if(kk <= s.Length&& s.EndsWith(pattern[..kk])){ best = kk;break;} target = best.ToString();} delta.Add(((q.ToString(), ch), target));}var states = Enumerable.Range(0, n +1).Select(i => i.ToString());returnnewDfa(states, alphabet, delta,"0",new[]{ n.ToString()});}Dfa NEEDLE =FactorDfa("aabbaabb", ALPHABET);foreach(var(name, mq, tgt)innew(string, Func<string,bool>, Dfa)[]{("parites (desaccords DENSES)", MembershipQuery, TARGET),("contient 'aabbaabb' (desaccords RARES)", NEEDLE.Accepts, NEEDLE),}){ Console.WriteLine($"Langage {name} :"); Console.WriteLine($"{"N essais",9} | etats | DFA exact ?");foreach(var nSamples innew[]{1,10,100,1000}){var(h, _t, _e)=Lstar(ALPHABET, mq,MakeSamplingEq(mq, ALPHABET, nSamples, seed:7), verbose:false);bool exact =FindCounterexample(h, tgt, ALPHABET)==null; Console.WriteLine($"{nSamples,9} | {h.StateCount,5} | {(exact ? "OUI" : "NON")}");} Console.WriteLine();}
Interpretation : tout depend de la densite des desaccords
Sur les parites, les desaccords sont suffisamment denses pour que peu d’essais suffisent : des N=10, l’EQ trouve les 4 etats exacts. A N=1, un seul mot peut manquer le contre-exemple (l’approximation retourne un DFA a 2 etats non exact) — la garantie semble intacte, mais c’est une propriete du langage, pas de l’algorithme.
Sur le langage-aiguille, un mot aleatoire de longueur \(\le 20\) ne contient le facteur qu’avec une probabilite de quelques pourcents. A \(N = 1\) ou \(10\) essais, l’EQ approximatif ne voit jamais de desaccord et accepte la toute première conjecture : un DFA a un etat qui rejette tout. Le résultat est grossierement faux et rien dans l’exécution ne le signale — c’est le sens exact du PAC : correct avec probabilite \(1 - \delta\)sur la distribution d’echantillonnage, rien de plus. A \(N = 100\), le contre-exemple finit par sortir, et un seul suffit : ses prefixes injectent dans la table tout le materiel necessaire, et L* deroule les 9 etats sans aide supplementaire.
C’est la même lecon que SL-9, vue de l’autre cote : la-bas, un générateur faillible (le LLM) etait discipline par un oracle exact ; ici, un apprenant exact est fragilise par un oracle faillible. Dans un pipeline neuro-symbolique, identifier qui joue le rôle de l’oracle et ce qu’il garantit vraiment est la première question d’architecture.
7. Posterite : le model learning
L* n’est pas reste un résultat théorique. Sous le nom de model learning (Vaandrager, CACM 2017), ses descendants directs apprennent des machines a etats de systèmes reels, en branchant les MQ sur le système lui-même :
Retro-ingenierie de protocoles : les implementations de TLS, TCP, SSH ou des cartes bancaires ont ete apprises comme des machines de Mealy ; plusieurs violations de specification et failles ont ete trouvees en lisant le DFA appris (etats parasites, transitions interdites presentes).
LearnLib (open source, Java) industrialise L* et ses variantes modernes (TTT, Rivest-Schapire, ADT), avec des EQ approximatifs intelligents (W-method, echantillonnage dirige).
Verification : le DFA appris sert d’interface formelle d’un composant boite noire, sur laquelle un model checker peut ensuite prouver des proprietes (assume-guarantee reasoning).
Et la boucle se referme avec les LLM : des travaux recents utilisent un LLM comme professeur approximatif (repondre aux MQ sur un format textuel, proposer des contre-exemples) et L* comme apprenant exact qui en extrait un automate verifiable — la division générateur faillible / verificateur symbolique de SL-9, devenue protocole d’apprentissage.
8. Exercices
Tableau recapitulatif
Concept
Definition
Implementation
Requête MQ / EQ
\(w \in L\) ? / \(H = L\) ?
membership_query, find_counterexample
Table d’observation
\((S, E, T)\), lignes = etats candidats
ObservationTable
Fermeture
\(row(sa)\) presente dans \(S\)
find_unclosed()
Consistance
lignes egales -> successeurs egaux
find_inconsistency()
Conjecture
DFA des lignes distinctes
conjecture()
EQ approximatif
echantillonnage, garantie PAC
make_sampling_eq()
// Exemple guide 1 : Apprendre le langage "contient 'abb' comme facteur"// Etape 1 : requete d'appartenance -- une ligne suffit ("abb" in word)boolMqAbb(string word)=> word.Contains("abb");// Etape 2 : lancer L* avec un oracle d'equivalence par echantillonnagevar(learnedAbb, finalTableAbb, nEqAbb)=Lstar(ALPHABET, MqAbb,MakeSamplingEq(MqAbb, ALPHABET,2000), verbose:false);Console.WriteLine($"DFA appris : {learnedAbb}");Console.WriteLine($" Conjectures (equivalences) : {nEqAbb}");Console.WriteLine($" |S| = {finalTableAbb.S.Count}, |E| = {finalTableAbb.E.Count}, MQ posees = {finalTableAbb.NMq}");// Etape 3 : interpretation des etats (prediction theorique : 4 etats)Console.WriteLine();Console.WriteLine("Prediction theorique : 4 etats (rien vu / 'a' vu / 'ab' vu / 'abb' vu).");Console.WriteLine($"-> observe : {learnedAbb.StateCount} etats "+(learnedAbb.StateCount==4?"(confirme)":"(a commenter)"));// Etape 4 : verification sur 10 motsvar testWords =new[]{"","a","ab","abb","abbb","babb","aaabb","abab","abbabb","bbabb"};Console.WriteLine();Console.WriteLine($"{"mot",10} | DFA | vrai | OK ?");Console.WriteLine(newstring('-',34));foreach(var w in testWords){bool pred = learnedAbb.Accepts(w), vrai = w.Contains("abb"); Console.WriteLine($"{(w == "" ? "(eps)" : w),10} | {(pred ? 1 : 0),3} | {(vrai ? 1 : 0),4} | {(pred == vrai ? "OK" : "ECHEC")}");}
DFA appris : Dfa(4 etats, 1 acceptants)
Conjectures (equivalences) : 2
|S| = 16, |E| = 3, MQ posees = 67
Prediction theorique : 4 etats (rien vu / 'a' vu / 'ab' vu / 'abb' vu).
-> observe : 4 etats (confirme)
mot | DFA | vrai | OK ?
----------------------------------
(eps) | 0 | 0 | OK
a | 0 | 0 | OK
ab | 0 | 0 | OK
abb | 1 | 1 | OK
abbb | 1 | 1 | OK
babb | 1 | 1 | OK
aaabb | 1 | 1 | OK
abab | 0 | 0 | OK
abbabb | 1 | 1 | OK
bbabb | 1 | 1 | OK
Exercice 1 (variation) : Apprendre un autre langage-facteur
Reprenez la méthode de l’exemple guide pour apprendre par L* le langage des mots (sur {a, b}) qui contiennent la sous-chaîne “ba” (au lieu de “abb”). Combien d’etats le DFA minimal comporte-t-il, et pourquoi ?
Étapes : 1. Ecrire mq_ba(word) : appartenance au langage “contient ‘ba’ comme facteur” 2. Lancer L* avec make_sampling_eq(mq_ba, ALPHABET, 2000) 3. Donner le nombre d’etats du DFA appris et expliquer chacun (prediction théorique : 3 etats) 4. Verifier sur 8 mots de votre choix
// EXERCICE 1 (variation) : apprendre le langage "contient 'ba' comme facteur"// TODO etudiant : reprenez l'exemple guide ci-dessus avec le motif "ba".Console.WriteLine("Exercice a completer : apprenez le langage 'contient ba' par L*");Console.WriteLine("Etape 1 : ecrivez MqBa(word) avec .Contains(\"ba\")");Console.WriteLine("Etape 2 : lancez Lstar(ALPHABET, MqBa, MakeSamplingEq(..., 2000))");Console.WriteLine("Etape 3 : donnez le nombre d'etats et expliquez chacun");Console.WriteLine("Etape 4 : verifiez sur 8 mots");// Indice : le motif "ba" est plus court que "abb" -> moins d'etats "amorce".// Etape 1 : bool MqBa(string word) => word.Contains("ba"); // TODO etudiant// Etape 2 : var (learnedBa, _, _) = Lstar(ALPHABET, MqBa, MakeSamplingEq(MqBa, ALPHABET, 2000), verbose: false);// Etape 3 : Console.WriteLine($"DFA appris : {learnedBa} ..."); // prediction : 3 etats// Etape 4 : boucle de verification sur 8 motsDfa learnedBa =null;// TODO etudiant : le DFA appris (a remplir)Console.WriteLine($"Resultat : {(learnedBa == null ? "(a completer)" : learnedBa.ToString())}");
Exercice a completer : apprenez le langage 'contient ba' par L*
Etape 1 : ecrivez MqBa(word) avec .Contains("ba")
Etape 2 : lancez Lstar(ALPHABET, MqBa, MakeSamplingEq(..., 2000))
Etape 3 : donnez le nombre d'etats et expliquez chacun
Etape 4 : verifiez sur 8 mots
Resultat : (a completer)
L’exercice suivant quantifie ce que la section 6 a montre qualitativement : la fiabilite de l’oracle d’equivalence par echantillonnage est une variable aleatoire, qui se mesure en repetant l’expérience.
// Exemple guide 2 : Fiabilite de l'oracle d'equivalence approximatif// Etape 1 : taux de reussite sur n_seeds grainesdoubleTauxFiabilite(Func<string,bool> mq, Dfa target,int nSamples,int nSeeds =20){int corrects =0;for(int seed =0; seed < nSeeds; seed++){var eq =MakeSamplingEq(mq, ALPHABET, nSamples, seed: seed);var(h, _t, _e)=Lstar(ALPHABET, mq, eq, verbose:false);if(FindCounterexample(h, target, ALPHABET)==null) corrects++;}return(double)corrects / nSeeds;}// Etape 2 : tableau n_samples -> taux de reussite (20 runs), NEEDLE vs TARGETConsole.WriteLine("Fiabilite de l'oracle d'equivalence par echantillonnage (20 graines)");Console.WriteLine(newstring('=',64));Console.WriteLine($"{"n_samples",10} | {"NEEDLE(desaccords rares)",26} | {"TARGET parites(denses)",24}");Console.WriteLine(newstring('-',64));foreach(var nSamples innew[]{1,10,50,100,500}){double tNeedle =TauxFiabilite(NEEDLE.Accepts, NEEDLE, nSamples);double tTarget =TauxFiabilite(MembershipQuery, TARGET, nSamples); Console.WriteLine($"{nSamples,10} | {tNeedle.ToString("P0", CultureInfo.InvariantCulture),25} | {tTarget.ToString("P0", CultureInfo.InvariantCulture),23}");}Console.WriteLine();Console.WriteLine("Lecture : le seuil de fiabilite (n_samples au-dela duquel le taux tend");Console.WriteLine("vers 100%) depend fortement du langage. NEEDLE a des desaccords RARES --");Console.WriteLine("il faut beaucoup d'echantillons pour tomber sur un mot revelateur. TARGET");Console.WriteLine("(parite) a des desaccords DENSES -- donc peu d'echantillons suffisent.");
Fiabilite de l'oracle d'equivalence par echantillonnage (20 graines)
================================================================
n_samples | NEEDLE (desaccords rares) | TARGET parites (denses)
----------------------------------------------------------------
1 | 0 % | 25 %
10 | 5 % | 100 %
50 | 55 % | 100 %
100 | 80 % | 100 %
500 | 100 % | 100 %
Lecture : le seuil de fiabilite (n_samples au-dela duquel le taux tend
vers 100%) depend fortement du langage. NEEDLE a des desaccords RARES --
il faut beaucoup d'echantillons pour tomber sur un mot revelateur. TARGET
(parite) a des desaccords DENSES -- donc peu d'echantillons suffisent.
Exercice 2 (variation) : Fiabilite vs longueur des mots echantillonnes
Pour NEEDLE, le contre-exemple revelateur est un mot LONG (il faut un mot contenant “aabbaabb”). L’oracle echantillonne des mots de longueur <= max_len : que se passe-t-il si max_len < 8 (aucun mot genere ne peut contenir le motif) ? Mesurez le taux de fiabilite pour max_len dans [5, 8, 12, 20], a n_samples fixe.
Étapes : 1. Reutiliser taux_fiabilite en variant max_len (paramètre de make_sampling_eq) avec n_samples=200 fixe, sur NEEDLE 2. Tableau max_len -> taux de reussite (20 graines) 3. Pourquoi max_len < len(motif) annule-t-il la fiabilite même avec beaucoup d’echantillons ?
// EXERCICE 2 (variation) : fiabilite vs longueur des mots echantillonnes// TODO etudiant : pour NEEDLE, la fiabilite depend de max_len (longueur max des mots tires).Console.WriteLine("Exercice a completer : fiabilite vs longueur des mots echantillonnes");Console.WriteLine("Etape 1 : reutilisez TauxFiabilite en variant max_len (n_samples=200 fixe)");Console.WriteLine("Etape 2 : affichez le tableau max_len -> taux sur NEEDLE");Console.WriteLine("Etape 3 : expliquez pourquoi max_len < 8 annule la fiabilite");// Indice : si aucun mot genere ne peut contenir le motif 'aabbaabb',// l'oracle ne trouvera JAMAIS de contre-exemple, meme avec n_samples infini.// Etape 1 : double TauxFiabiliteMaxLen(int maxLen, int nSamples = 200, int nSeeds = 20) { ... }// (variation de TauxFiabilite passant maxLen a MakeSamplingEq)// Etape 2 : foreach (var ml in new[] { 5, 8, 12, 20 }) Console.WriteLine($" max_len={ml} : {TauxFiabiliteMaxLen(ml):P0}");Dictionary<int,double> resultat2 =null;// TODO etudiant : dict {max_len: taux}Console.WriteLine($"Resultat : {(resultat2 == null ? "(a completer)" : string.Join(",", resultat2.Select(kv => kv.Key + ":" + kv.Value.ToString("P0"))))}");
Exercice a completer : fiabilite vs longueur des mots echantillonnes
Etape 1 : reutilisez TauxFiabilite en variant max_len (n_samples=200 fixe)
Etape 2 : affichez le tableau max_len -> taux sur NEEDLE
Etape 3 : expliquez pourquoi max_len < 8 annule la fiabilite
Resultat : (a completer)
L’exercice suivant touche au coeur algorithmique : la maniere d’integrer le contre-exemple. Angluin ajoute tous ses prefixes a \(S\) ; la variante de Maler-Pnueli ajoute tous ses suffixes a \(E\) — même garantie, couts différents.
// Exemple guide 3 : Variante de traitement du contre-exemple (suffixes vs prefixes)// L* variante Maler-Pnueli : integre le contre-exemple par ses SUFFIXES (ajoutes a E).(Dfa hyp, ObservationTable table,int nEq)LstarSuffixes(IEnumerable<char> alphabet, Func<string,bool> mq, Func<Dfa,string> eq,bool verbose =false){var tbl =newObservationTable(alphabet, mq);int nEq =0;while(true){while(true){var sa = tbl.FindUnclosed();if(sa !=null){ tbl.S.Add(sa); tbl.Fill();continue;}var ae = tbl.FindInconsistency();if(ae !=null){ tbl.E.Add(ae); tbl.Fill();continue;}break;}var hyp =Conjecture(tbl); nEq++;var cex =eq(hyp);if(cex ==null)return(hyp, tbl, nEq);// 3. Maler-Pnueli : ajouter les SUFFIXES cex[i:] du contre-exemple a Efor(int i =0; i < cex.Length; i++){var suf = cex[i..];if(!tbl.E.Contains(suf)) tbl.E.Add(suf);} tbl.Fill();}}// Deuxieme langage de comparaison : L_5 = mots de longueur divisible par 5.var l5Delta =new List<((string,char),string)>();for(int q =0; q <5; q++)foreach(var a in ALPHABET) l5Delta.Add(((q.ToString(), a),((q +1)%5).ToString()));Dfa L5 =newDfa(Enumerable.Range(0,5).Select(i => i.ToString()), ALPHABET, l5Delta,"0",new[]{"0"});// Etape 2 : comparaison prefixes (Angluin) vs suffixes (Maler-Pnueli)Console.WriteLine("Comparaison Angluin (prefixes -> S) vs Maler-Pnueli (suffixes -> E)");Console.WriteLine(newstring('=',70));Console.WriteLine($"{"Langage",-22} | {"Variante",-13} | {"|S|",4} | {"|E|",4} | {"|S|x|E|",7} | {"MQ",5} | {"EQ",3}");Console.WriteLine(newstring('-',70));var langages =new(string nom, Func<string,bool> mq, Dfa tgt)[]{("TARGET (parites)", MembershipQuery, TARGET),("L_5 (longueur % 5)", L5.Accepts, L5),};foreach(var(nom, mq, tgt)in langages){ Func<Dfa,string> eq = h =>FindCounterexample(h, tgt, ALPHABET);foreach(var(variante, runner)innew(string, Func<IEnumerable<char>, Func<string,bool>, Func<Dfa,string>,bool,(Dfa, ObservationTable,int)>)[]{("Angluin",(a, m, e, v)=>Lstar(a, m, e, v)),("Maler-Pnueli",(a, m, e, v)=>LstarSuffixes(a, m, e, v)),}){var(h, tbl, nEq)=runner(ALPHABET, mq, eq,false);int taille = tbl.S.Count* tbl.E.Count; Console.WriteLine($"{nom,-22} | {variante,-13} | {tbl.S.Count,4} | {tbl.E.Count,4} | {taille,7} | {tbl.NMq,5} | {nEq,3}");}}Console.WriteLine();Console.WriteLine("Lecture : les deux variantes echangent |S| contre |E|. Angluin ajoute des");Console.WriteLine("PREFIXES a S ; Maler-Pnueli ajoute des SUFFIXES a E. La compacite globale");Console.WriteLine("n'avantage aucune des deux variantes en general.")
Comparaison Angluin (prefixes -> S) vs Maler-Pnueli (suffixes -> E)
======================================================================
Langage | Variante | |S| | |E| | |S|x|E| | MQ | EQ
----------------------------------------------------------------------
TARGET (parites) | Angluin | 4 | 3 | 12 | 19 | 2
TARGET (parites) | Maler-Pnueli | 4 | 3 | 12 | 19 | 2
L_5 (longueur % 5) | Angluin | 6 | 4 | 24 | 34 | 2
L_5 (longueur % 5) | Maler-Pnueli | 5 | 6 | 30 | 41 | 2
Lecture : les deux variantes echangent |S| contre |E|. Angluin ajoute des
PREFIXES a S ; Maler-Pnueli ajoute des SUFFIXES a E. La compacite globale
n'avantage aucune des deux variantes en general.
Exercice 3 (variation) : Variante Rivest-Schapire (un seul suffixe, par dichotomie)
Angluin ajoute tous les prefixes, Maler-Pnueli tous les suffixes. La variante de Rivest-Schapire (1993) est plus parcimonieuse : par recherche dichotomique sur le contre-exemple, elle identifie un seul suffixe distinguant et l’ajoute a E. Implementez cette recherche.
Étapes : 1. Soit un contre-exemple c ou l’hypothese et la cible differentent : trouvez par dichotomie un indice i tel que le suffixe c[i:] separe la ligne d’acces correspondante de celle de la cible 2. Ajoutez ce seul suffixe c[i:] a E (pas tous les suffixes) 3. Comparez le nombre total de MQ avec la variante Maler-Pnueli (tous les suffixes) sur TARGET : Rivest ajoute-t-il moins de colonnes ?
// EXERCICE 3 (variation) : variante Rivest-Schapire (un seul suffixe par dichotomie)// TODO etudiant : au lieu d'ajouter TOUS les suffixes du contre-exemple a E,// n'en ajouter qu'UN, trouve par recherche dichotomique.Console.WriteLine("Exercice a completer : variante Rivest-Schapire (un seul suffixe par dichotomie)");Console.WriteLine("Etape 1 : ecrivez SuffixeDistinguantRivest(table, cex) (recherche dichotomique)");Console.WriteLine("Etape 2 : ecrivez LstarRivest (copie de LstarSuffixes, ajout d'un seul suffixe)");Console.WriteLine("Etape 3 : comparez n_mq et |E| avec Maler-Pnueli sur TARGET");// Indice : la dichotomie exploite le fait que si la ligne d'acces hypothese != cible// mais coincide a un certain decalage, il existe un point de bascule i ; cex[i:] est// le suffixe cherche. O(log|cex|) ajout au lieu de |cex|.// Etape 1 : string SuffixeDistinguantRivest(ObservationTable table, string cex) { ... }// Etape 2 : (Dfa, ObservationTable, int) LstarRivest(...) { ... } (copie LstarSuffixes)// Etape 3 : comparer n_mq et |E| avec Maler-Pnueli sur TARGETDfa learnedRivest =null;// TODO etudiant : le DFA appris par LstarRivestConsole.WriteLine($"Resultat : {(learnedRivest == null ? "(a completer)" : learnedRivest.ToString())}");
Exercice a completer : variante Rivest-Schapire (un seul suffixe par dichotomie)
Etape 1 : ecrivez SuffixeDistinguantRivest(table, cex) (recherche dichotomique)
Etape 2 : ecrivez LstarRivest (copie de LstarSuffixes, ajout d'un seul suffixe)
Etape 3 : comparez n_mq et |E| avec Maler-Pnueli sur TARGET
Resultat : (a completer)
Dernier exercice : la robustesse. Tous les algorithmes de la serie rencontrent tot ou tard cette question, et la reponse de L* est singuliere.
// Exemple guide 4 : Oracle d'appartenance bruite// Oracle qui MENT avec probabilite p, de facon DETERMINISTE par mot (memoire par mot).Func<string,bool>MakeNoisyMq(Func<string,bool> trueMq,double p =0.05,int seed =0){var rng =newRandom(seed);var cache =new Dictionary<string,bool>();boolNoisy(string word){if(!cache.ContainsKey(word)){bool truth =trueMq(word);bool lie = rng.NextDouble()< p; cache[word]= lie ?!truth : truth;}return cache[word];}return Noisy;}// L* borne : evite la boucle infinie si mq est inconsistant (oracle bruite).(Dfa hyp, ObservationTable table,int nEq,bool stable)LstarBounded(IEnumerable<char> alphabet, Func<string,bool> mq, Func<Dfa,string> eq,int maxRounds =8,int maxSteps =60){var tbl =newObservationTable(alphabet, mq);int nEq =0; Dfa hyp =null;for(int round =1; round <= maxRounds; round++){int steps =0;while(true){var sa = tbl.FindUnclosed();if(sa !=null){ tbl.S.Add(sa); tbl.Fill();}else{var ae = tbl.FindInconsistency();if(ae !=null){ tbl.E.Add(ae); tbl.Fill();}elsebreak;} steps++;if(steps > maxSteps)break;}if(tbl.FindUnclosed()!=null|| tbl.FindInconsistency()!=null)return(hyp, tbl, nEq,false);// non-stabilise hyp =Conjecture(tbl); nEq++;var cex =eq(hyp);if(cex ==null)return(hyp, tbl, nEq,true);for(int i =1; i <= cex.Length; i++)if(!tbl.S.Contains(cex[..i])) tbl.S.Add(cex[..i]); tbl.Fill();}return(hyp, tbl, nEq,false);}// Etape 2 : L* borne sous oracle bruite, pour p = 0.05, 0.01, 0.20Console.WriteLine("Oracle d'appartenance bruite : comportement de L* (cible TARGET = 4 etats)");Console.WriteLine(newstring('=',66));foreach(var p innew[]{0.05,0.01,0.20}){var noisy =MakeNoisyMq(MembershipQuery, p, seed:0);var eqNoisy =MakeSamplingEq(noisy, ALPHABET,300, seed:1);var(hyp, _tbl, nEq, stable)=LstarBounded(ALPHABET, noisy, eqNoisy, maxRounds:8);if(hyp ==null){ Console.WriteLine($"p={p.ToString("F2", CultureInfo.InvariantCulture),4} : NON-STABILISE apres {nEq} conjectures");continue;}var cexReal =FindCounterexample(hyp, TARGET, ALPHABET);bool exact = cexReal ==null;string statut = stable ?"stable":"max_rounds";string verdict = exact ?"EXACT vs TARGET":"ecart (contre-ex "+ cexReal +")"; Console.WriteLine($"p={p.ToString("F2", CultureInfo.InvariantCulture),4} : {hyp.StateCount,2} etats | {statut,10} | {nEq,2} EQ | {verdict}");}Console.WriteLine();Console.WriteLine("Lecture : le resultat n'est PAS monotone en p -- il depend de QUELS mots");Console.WriteLine("l'oracle ment. Un apprenant EXACT comme L* n'a AUCUNE degradation gracieuse");Console.WriteLine("face au bruit : un seul mensonge memorise dans T cree un etat fantome permanent.");
Oracle d'appartenance bruite : comportement de L* (cible TARGET = 4 etats)
==================================================================
p=0.05 : 8 etats | max_rounds | 2 EQ | ecart (contre-ex abb)
p=0.01 : 4 etats | max_rounds | 3 EQ | EXACT vs TARGET
p=0.20 : 40 etats | max_rounds | 3 EQ | ecart (contre-ex aaaa)
Lecture : le resultat n'est PAS monotone en p -- il depend de QUELS mots
l'oracle ment. Un apprenant EXACT comme L* n'a AUCUNE degradation gracieuse
face au bruit : un seul mensonge memorise dans T cree un etat fantome permanent.
Exercice 4 (variation) : Robustesse par vote majoritaire
Le bruit de l’oracle d’appartenance casse l’hypothese d’exactitude de L. La parade classique : pour chaque mot, poser la requête K fois a l’oracle bruite et garder la reponse majoritaire (le vrai verdict ressort des que K est grand devant le taux de mensonges). Implementez ce robust_mq et montrez qu’il permet a L de retrouver le DFA exact.
Étapes : 1. Ecrire make_robust_mq(noisy_mq, K) : pour chaque mot, echantillonne K reponses bruitees et renvoie la majorite (memoire par mot, comme le bruite) 2. Lancer lstar_bounded avec robust_mq (K=21) sur un oracle bruite a p=0.05 3. Le DFA appris est-il exact vs TARGET ? Quel K minimal suffit pour p=0.05 ? pour p=0.20 ?
// EXERCICE 4 (variation) : robustesse par vote majoritaire// TODO etudiant : diluez le bruit en posant K fois chaque requete et en gardant la majorite.Console.WriteLine("Exercice a completer : robustesse par vote majoritaire");Console.WriteLine("Etape 1 : ecrivez MakeRobustMq(noisyMq, K) (vote majoritaire par mot)");Console.WriteLine("Etape 2 : lancez LstarBounded avec robustMq (K=21) sur un bruite p=0.05");Console.WriteLine("Etape 3 : le DFA est-il exact vs TARGET ? K minimal pour p=0.05 ? p=0.20 ?");// Indice : le vote majoritaire marche seulement si chaque appel bruite est INDEPENDANT.// Il faut un oracle bruite NON memoise pour que K tirages soient independants.// Etape 1 : Func<string,bool> MakeRobustMq(Func<string,bool> noisy, int K) { ... }// Etape 2 : var robust = MakeRobustMq(noisy, 21); var (h,_,_,_) = LstarBounded(ALPHABET, robust, eqR, maxRounds:15);// Etape 3 : exact vs TARGET ; K minimalDfa learnedRobust =null;// TODO etudiant : le DFA robuste apprisConsole.WriteLine($"Resultat : {(learnedRobust == null ? "(a completer)" : learnedRobust.ToString())}");
Exercice a completer : robustesse par vote majoritaire
Etape 1 : ecrivez MakeRobustMq(noisyMq, K) (vote majoritaire par mot)
Etape 2 : lancez LstarBounded avec robustMq (K=21) sur un bruite p=0.05
Etape 3 : le DFA est-il exact vs TARGET ? K minimal pour p=0.05 ? p=0.20 ?
Resultat : (a completer)
9. Pont vers Infer.NET : du DFA exact a la reconnaissance probabiliste de motifs
Les sections 6 et 7 ont etabli le talon d’Achille de L* : il est exact et donc fragile. L’exemple guide 4 (oracle d’appartenance bruite) l’a montre nettement – un seul mensonge memorise dans la table T créé un etat fantome permanent – et concluait par une promesse :
un apprenant EXACT comme L* n’a aucune degradation gracieuse face au bruit, la ou un classifieur statistique se degrade smoothment (loi des grands nombres).
Cette section construit ce classifieur statistique, et montre qu’il est le pendant probabiliste exact de l’automate appris par L*. C’est le pont vers la programmation probabiliste sur sequences d’Infer.NET (serie Probas/Infer).
Le changement de modèle
Automate exact (L*)
Chaîne probabiliste (Infer.NET)
Objet
DFA déterministe
automate pondere / HMM
Verdict
accepte / rejette (binaire)
P(accepte) (probabilite calibree)
Bruit
aucun modèle – un mensonge = une verite
modèle de canal explicite (epsilon)
Inference
parcours d’etat
algorithme forward (somme-produit)
Au lieu d’un oracle qui ment sur l’appartenance (section 6), on adopte le cadre naturel de la reconnaissance de motifs : un canal d’observation bruite sur les symboles eux-mêmes (pensez OCR, parole, capteurs). Le mot vrai w passe dans un canal qui retourne chaque symbole correct avec probabilite 1 - epsilon et le corrompt avec probabilite epsilon. On observe wabime et l’on cherche P(w vrai appartient au langage | observation).
// Reconnaissance probabiliste : l'algorithme forward sur les etats du DFA.// alpha_t[q] = masse des prefixes vrais finissant dans l'etat q, sachant o_1..o_t.doubleForwardAcceptProb(Dfa dfa,string observed,double epsilon){var A = dfa.Alphabet;int m = A.Count;double pFlip = m >1? epsilon /(m -1):0.0;// masse repartie sur les mauvais symbolesvar alpha = dfa.States.ToDictionary(q => q, q => q == dfa.Start?1.0:0.0);foreach(var o in observed){var nxt = dfa.States.ToDictionary(q => q, q =>0.0);foreach(var(q, mass)in alpha){if(mass ==0.0)continue;foreach(var s in A){double lik = s == o ?1.0- epsilon : pFlip;// P(observer o | vrai s) nxt[dfa.Delta[(q, s)]]+= mass *(1.0/ m)* lik;}}double Z =alphaSum(nxt);foreach(var q in dfa.States) nxt[q]= Z >0? nxt[q]/ Z :0.0; alpha = nxt;}double Ztot =alphaSum(alpha);return Ztot >0? dfa.Accepting.Sum(q => alpha[q])/ Ztot :0.0;}doublealphaSum(Dictionary<string,double> d)=> d.Values.Sum();// Garde-fou : a epsilon = 0, le canal est parfait -> le forward redonne le verdict exact.foreach(var w innew[]{"","a","ab","aabb","abab","aab","bbaa","abba"}){double p0 =ForwardAcceptProb(TARGET, w,0.0);bool mq =MembershipQuery(w);if((p0 >0.5)!= mq || Math.Abs(p0 -(mq ?1.0:0.0))>1e-9) Console.WriteLine($"ECHEC coherence epsilon=0 sur {w}");}Console.WriteLine("Coherence epsilon=0 : le forward redonne le verdict exact du DFA. OK\n");string trueW ="aabb";Console.WriteLine($"Mot vrai w = '{trueW}' (appartient a L ? {MembershipQuery(trueW)})");Console.WriteLine($"{"epsilon",7} | {"P(accepte | obs)",16} | lecture");Console.WriteLine(newstring('-',52));foreach(var eps innew[]{0.0,0.05,0.10,0.20,0.35}){double p =ForwardAcceptProb(TARGET, trueW, eps);string note = p >0.95?"certain":(p >0.5?"penche accepte":"doute"); Console.WriteLine($"{eps.ToString("F2", CultureInfo.InvariantCulture),7} | {p.ToString("F3", CultureInfo.InvariantCulture),16} | {note}");}
Coherence epsilon=0 : le forward redonne le verdict exact du DFA. OK
Mot vrai w = 'aabb' (appartient a L ? True)
epsilon | P(accepte | obs) | lecture
----------------------------------------------------
0.00 | 1.000 | certain
0.05 | 0.828 | penche accepte
0.10 | 0.705 | penche accepte
0.20 | 0.565 | penche accepte
0.35 | 0.504 | penche accepte
Lecture : une probabilite calibree, pas un meilleur verdict
A partir d’une seule observation, la decision a seuil 0.5 du recognizer probabiliste coincide avec le parcours dur du DFA sur l’observation : pour epsilon < 0.5, le symbole le plus probable reste celui observe. Ce que le modèle probabiliste apporte d’emblee n’est donc pas une meilleure exactitude sur un coup, mais une confiance calibree : P(accepte) glisse doucement vers 0.5 quand le bruit monte – jamais le basculement catastrophique de L*, qui construisait un automate faux. La degradation est gracieuse.
Le vrai gain d’exactitude vient quand on dispose de plusieurs observations bruitees du même mot (plusieurs lectures OCR, plusieurs trames audio). C’est la version probabiliste – et correcte – du vote majoritaire de l’exercice 4.
// Agregation de preuves : k observations independantes du meme mot vrai.doubleForwardAcceptProbMulti(Dfa dfa, List<string> observations,double epsilon){var A = dfa.Alphabet;int m = A.Count;double pFlip = m >1? epsilon /(m -1):0.0;int T = observations[0].Length;var alpha = dfa.States.ToDictionary(q => q, q => q == dfa.Start?1.0:0.0);for(int i =0; i < T; i++){var column = observations.Select(o => o[i]).ToList();var nxt = dfa.States.ToDictionary(q => q, q =>0.0);foreach(var(q, mass)in alpha){if(mass ==0.0)continue;foreach(var s in A){double lik =1.0;foreach(var oI in column) lik *= s == oI ?1.0- epsilon : pFlip; nxt[dfa.Delta[(q, s)]]+= mass *(1.0/ m)* lik;}}double Z = nxt.Values.Sum();if(Z ==0) Z =1.0;foreach(var q in dfa.States) nxt[q]/= Z; alpha = nxt;}double Ztot = alpha.Values.Sum();return Ztot >0? dfa.Accepting.Sum(q => alpha[q])/ Ztot :0.0;}stringCorrupt(string word,double eps, Random rng){var sb =newStringBuilder();foreach(var c in word) sb.Append(rng.NextDouble()>= eps ? c :(c =='a'?'b':'a'));return sb.ToString();}(double hard1,double probK)BatchAccuracy(double eps,int k,int nWords =3000,int seed =7){var rng =newRandom(seed);int hard1 =0, probK =0;for(int _ =0; _ < nWords; _++){int len = rng.Next(0,13);var sb =newStringBuilder();for(int i =0; i < len; i++) sb.Append(ALPHABET[rng.Next(ALPHABET.Count)]);var w = sb.ToString();bool y =MembershipQuery(w);var obs =new List<string>();for(int j =0; j < k; j++) obs.Add(Corrupt(w, eps, rng));if(TARGET.Accepts(obs[0])== y) hard1++;if((ForwardAcceptProbMulti(TARGET, obs, eps)>0.5)== y) probK++;}return((double)hard1 / nWords,(double)probK / nWords);}Console.WriteLine("Recuperation par agregation de k observations (n=3000 mots) :");Console.WriteLine($"{"k obs",6} | {"dur(1 obs)",11} | {"forward(k obs)",15}");Console.WriteLine(newstring('-',40));var ks =new[]{1,3,5,9,15,25};var curve20 =new List<double>();var curve30 =new List<double>();double hardRef =0;foreach(var k in ks){var(h20, p20)=BatchAccuracy(0.20, k);var(_h30, p30)=BatchAccuracy(0.30, k); curve20.Add(p20); curve30.Add(p30); hardRef = h20; Console.WriteLine($"{k,6} | {h20.ToString("F3", CultureInfo.InvariantCulture),11} | eps=.20 {p20.ToString("F3", CultureInfo.InvariantCulture)} eps=.30 {p30.ToString("F3", CultureInfo.InvariantCulture)}");}// Visualisation ASCII : exactitude vs k (forward eps=.20 / eps=.30 + plafond DFA dur).Console.WriteLine();Console.WriteLine("Recuperation par evidence (ASCII) : exactitude vs nombre d'observations k");int W =46, H =14;char[,] g =newchar[H, W];for(int y =0; y < H; y++)for(int x =0; x < W; x++) g[y, x]=' ';Action<List<double>,char> Plot =(curve, mark)=>{for(int i =0; i < curve.Count; i++){int x =(int)Math.Round((double)i /(curve.Count-1)*(W -1));int y =(int)Math.Round((1.0- curve[i])*(H -1)/0.3);// plage 0.70-1.00if(y >=0&& y < H) g[y, x]= mark;}};Plot(curve20,'O');Plot(curve30,'X');int yRef =(int)Math.Round((1.0- hardRef)*(H -1)/0.3);if(yRef >=0&& yRef < H)for(int x =0; x < W; x++)if(g[yRef, x]==' ') g[yRef, x]='-';Console.WriteLine($"exact {1.0.ToString("F2", CultureInfo.InvariantCulture)} +");for(int y =0; y < H; y++){ Console.Write(" |");for(int x =0; x < W; x++) Console.Write(g[y, x]); Console.WriteLine();}Console.WriteLine($"exact {0.7.ToString("F2", CultureInfo.InvariantCulture)} +{new string('-', W)} k -> {ks.Last()}");Console.WriteLine("O = forward eps=0.20 ; X = forward eps=0.30 ; - = plafond DFA dur (1 obs).");
Recuperation par agregation de k observations (n=3000 mots) :
k obs | dur (1 obs) | forward (k obs)
----------------------------------------
1 | 0.784 | eps=.20 0.784 eps=.30 0.788
3 | 0.782 | eps=.20 0.827 eps=.30 0.795
5 | 0.774 | eps=.20 0.866 eps=.30 0.793
9 | 0.782 | eps=.20 0.944 eps=.30 0.832
15 | 0.806 | eps=.20 0.985 eps=.30 0.882
25 | 0.795 | eps=.20 0.998 eps=.30 0.954
Recuperation par evidence (ASCII) : exactitude vs nombre d'observations k
exact 1.00 +
| O
| O
| O X
|
|
| X
| O
| X
| O
|X--------X--------X---------------------------
|
|
|
|
exact 0.70 +---------------------------------------------- k -> 25
O = forward eps=0.20 ; X = forward eps=0.30 ; - = plafond DFA dur (1 obs).
Le pont : Infer.NET fait exactement cela, automatiquement
Trois faits reunis ferment la boucle :
L’algorithme forward est du passage de messages. La recursion alpha_t[q'] = somme ... est l’algorithme somme-produit sur le graphe de facteurs en chaîne etat_0 -- etat_1 -- ... -- etat_T. C’est le même moteur qui calcule les marginales d’un HMM.
Infer.NET (Microsoft Research) automatise ce passage de messages. On y declare le modèle generatif – un mot vrai tire d’un prior, un canal qui le bruite, des observations – et le moteur infere la posterieure. Surtout, Infer.NET sait raisonner directement sur des distributions de sequences (StringDistribution), c’est-a-dire des automates ponderes : la version probabiliste native du DFA que L* apprend.
C’est la synthese neuro-symbolique de la serie. L* fournit la structure exacte (l’automate minimal, garanti) ; Infer.NET l’enveloppe d’une inference statistique robuste au bruit. Structure symbolique + inference probabiliste : ni l’exactitude fragile seule, ni le tout-statistique opaque.
Le canal ci-dessus est symetrique (même epsilon dans les deux sens). Beaucoup de capteurs reels sont asymetriques : confondre a en b est plus frequent que l’inverse. Reprenez forward_accept_prob avec une matrice de confusion P(observe | vrai) quelconque (2x2, lignes sommant a 1) au lieu du scalaire epsilon, puis verifiez que la coherence canal-parfait (matrice identite = verdict exact) tient toujours. Indice : seule la ligne lik = ... change.
// Exercice du pont : canal d'observation ASYMETRIQUE (matrice de confusion).// confusion[(vrai, observe)] = P(observer 'observe' | vrai symbole 'vrai').doubleForwardAcceptProbConfusion(Dfa dfa,string observed, Dictionary<(char,char),double> confusion){// Indice : recopier ForwardAcceptProb et remplacer la ligne// lik = (1 - epsilon) if s == o else p_flip// par// lik = confusion[(s, o)]// TODO etudiant : implementer la passe forward avec la matrice de confusion. Console.WriteLine("Exercice du pont a completer : forward avec matrice de confusion.");return0.0;// TODO etudiant : remplacer par la probabilite calculee}// Verification attendue une fois implemente (canal parfait = identite -> verdict exact) :// var identite = new Dictionary<(char,char),double>();// foreach (var x in ALPHABET) foreach (var y in ALPHABET) identite[(x,y)] = x == y ? 1.0 : 0.0;// foreach (var w in new[] { "aabb", "aab", "abba" }) {// double p = ForwardAcceptProbConfusion(TARGET, w, identite);// if ((p > 0.5) != MembershipQuery(w)) Console.WriteLine($"ECHEC canal identite sur {w}");// }Console.WriteLine("Exercice du pont : generalisez ForwardAcceptProb a une matrice de confusion.");
Exercice du pont : generalisez ForwardAcceptProb a une matrice de confusion.
Conclusion
Recapitulatif
Le modèle MAT (Angluin 1987) donne a l’apprenant le droit de poser des questions : appartenance (MQ) et equivalence (EQ). Ce droit change la classe de complexite du problème — l’inference passive du plus petit DFA consistant est NP-difficile, l’inference active est polynomiale.
La table d’observation\((S, E, T)\) approxime la congruence de Myhill-Nerode avec un ensemble fini de suffixes distinguants. Fermeture et consistance sont les deux conditions pour qu’elle definisse un DFA.
L* alterne stabilisation de la table, conjecture, et integration du contre-exemple. Chaque conjecture rejetee gagne au moins un etat : terminaison et minimalite sont garanties, en au plus \(n\) equivalences.
L’oracle d’equivalence exact n’existe pas en pratique : on echantillonne, et la garantie exacte devient garantie PAC. La fiabilite du résultat est plafonnee par celle du professeur.
Le model learning industrialise tout cela (LearnLib, machines de Mealy, protocoles reels) — et la variante moderne « LLM comme professeur, L* comme apprenant » re-applique la division du travail de SL-9.
Modalite du cours : chaque groupe choisit un exercice de la serie, le prepare, et le presente en seance. Resoudre l’exercice est le minimum ; ce qui distingue une presentation qui maitrise le sujet, c’est la question-twist associee ci-dessous. Elle fait partie integrante de la presentation attendue.
Exercice
Question-twist a traiter en plus
Ex. 1 (langage ‘abb’)
Verifiez que le DFA appris est minimal en exhibant, pour chaque paire d’etats, un suffixe qui les distingue (certificat de Myhill-Nerode). Pourquoi L* est-il structurellement incapable de retourner un DFA non minimal ?
Ex. 2 (fiabilite de l’EQ)
Reliez votre taux de reussite empirique a la borne PAC : combien d’echantillons la théorie exige-t-elle pour \(\varepsilon = 0.01, \delta = 0.05\) ? Construisez une distribution de tirage qui fait echouer l’echantillonnage quel que soit \(N\) raisonnable.
Ex. 3 (prefixes vs suffixes)
Exhibez le pire cas : une famille de langages ou les contre-exemples sont longs et ou la variante prefixes fait exploser $S$. La variante de Rivest-Schapire n’ajoute qu’UN suffixe trouve par dichotomie sur le contre-exemple : quel est son cout en MQ et pourquoi est-ce le bon compromis ?
Ex. 4 (oracle bruite)
Peut-on reparer L* par vote majoritaire (poser chaque MQ 2k+1 fois) ? Calculez le surcout en requêtes et la probabilite residuelle d’erreur ; comparez avec la degradation graduelle d’un classifieur statistique sous le même bruit. Que conclure sur le contrat exactitude-contre-robustesse du symbolique ?
Ressources
D. Angluin, Learning Regular Sets from Queries and Counterexamples, Information and Computation 75(2), 1987
M. Kearns & U. Vazirani, An Introduction to Computational Learning Theory, ch. 8 (MIT Press, 1994)
F. Vaandrager, Model Learning, Communications of the ACM 60(2), 2017
R. Rivest & R. Schapire, Inference of Finite Automata Using Homing Sequences, Information and Computation 103(2), 1993
LearnLib — bibliotheque de reference du model learning
E. M. Gold, Complexity of Automaton Identification from Given Data, Information and Control 37, 1978