A la fin de ce notebook, vous saurez : 1. Comprendre la contrainte d’entrainement qui relie hypothese, descriptions et classifications 2. Implementer l’apprentissage base sur les explications (EBL) : extraire une règle générale d’un seul exemple 3. Comprendre les determinations et verifier la pertinence d’attributs (introduction au RBL, approfondi dans SL-3) 4. Comparer les trois paradigmes : EBL (deductif), RBL (pertinence), KBIL (inductif base sur la connaissance)
Prerequis
SL-1 : Apprentissage logique (CBH, Version Space)
Python 3.10+
Notions de logique du premier ordre (predicats, règles d’inference)
Duree estimee : 45 minutes
References
Russell & Norvig, Artificial Intelligence: A Modern Approach, 3e ed., Chapitres 19.3-19.4
G. DeJong, Explanation-Based Learning (1981)
T. Mitchell, Machine Learning, Chapitre 11 (Explanation-Based Learning)
1. Le cadre general — Connaissance dans l’apprentissage
Dans le notebook SL-1, nous avons etudie l’apprentissage inductif pur : trouver une hypothese qui agree avec les exemples observes, sans aucune connaissance prealable du domaine.
L’approche moderne (AIMA Chapitre 19.2) est cumulative : un agent qui sait déjà quelque chose peut apprendre plus efficacement.
La contrainte d’entrainement
Formellement, une hypothese \(H\) est acceptable si elle satisfait :
\[
H \wedge \text{Descriptions} \models \text{Classifications}
\]
Autrement dit : l’hypothese, combinee aux descriptions des exemples, doit entrainer logiquement leurs classifications.
Trois types d’apprentissage utilisant la connaissance
Type
Principe
Méthode
Nouvelle connaissance ?
EBL
La connaissance de fond implique l’hypothese
Deduction pure
Non (specialisation)
RBL
Les observations determinent l’hypothese
Deduction + pertinence
Non (reduction d’espace)
KBIL
Connaissance + hypothese expliquent les observations
Ce notebook couvre les deux premières approches : EBL et RBL.
# Les trois contraintes d'apprentissage, verifiees par enumeration de modeles# Mini-monde propositionnel a 3 atomes (l'exemple de Zog, version booleenne) :# pointu : l'objet observe est pointu# perce : l'objet peut percer la peau# tue : l'objet peut tuer (la classification a expliquer)from itertools import productATOMS = ["pointu", "perce", "tue"]def entails(kb: list, query) ->bool:"""KB |= query ssi query est vraie dans TOUS les modeles ou KB est vraie. Verification par enumeration des 2^3 = 8 modeles possibles. Une formule = une fonction modele -> bool ; un modele = dict atome -> bool. """for values in product([False, True], repeat=len(ATOMS)): model =dict(zip(ATOMS, values))ifall(f(model) for f in kb) andnot query(model):returnFalsereturnTrue# Connaissance de fond : pointu => perce, perce => tuebackground = [lambda m: (not m["pointu"]) or m["perce"],lambda m: (not m["perce"]) or m["tue"],]# Observation : Descriptions = pointu ; Classifications = tuedescription =lambda m: m["pointu"]classification =lambda m: m["tue"]# Hypothese candidate H : pointu => tuehypothesis =lambda m: (not m["pointu"]) or m["tue"]print("Contraintes d'apprentissage (AIMA 19.3-19.5) verifiees par enumeration de modeles")print("="*78)print()print("1. Induction pure (SL-1) : Hypothese ^ Descriptions |= Classifications")print(f" (pointu=>tue) ^ pointu |= tue ? -> {entails([hypothesis, description], classification)}")print()print("2. EBL : meme contrainte, PLUS Background |= Hypothese")print(f" (pointu=>perce) ^ (perce=>tue) |= (pointu=>tue) ? -> {entails(background, hypothesis)}")print(" H etait deja deductible de la connaissance de fond : EBL ne decouvre rien, il COMPILE.")converse =lambda m: (not m["tue"]) or m["pointu"]print(f" Contre-verification, la reciproque (tue=>pointu) : {entails(background, converse)} (non impliquee)")print()print("3. RBL : Background ^ Descriptions ^ Classifications |= Hypothese")print(f" -> {entails(background + [description, classification], hypothesis)}")print(" (avec des determinations comme connaissance de pertinence, cf. SL-3)")print()print("4. KBIL : Background ^ Hypothese ^ Descriptions |= Classifications")print(f" -> {entails(background + [hypothesis, description], classification)}")print(" H reste a trouver inductivement, mais Background guide et restreint la recherche.")
Contraintes d'apprentissage (AIMA 19.3-19.5) verifiees par enumeration de modeles
==============================================================================
1. Induction pure (SL-1) : Hypothese ^ Descriptions |= Classifications
(pointu=>tue) ^ pointu |= tue ? -> True
2. EBL : meme contrainte, PLUS Background |= Hypothese
(pointu=>perce) ^ (perce=>tue) |= (pointu=>tue) ? -> True
H etait deja deductible de la connaissance de fond : EBL ne decouvre rien, il COMPILE.
Contre-verification, la reciproque (tue=>pointu) : False (non impliquee)
3. RBL : Background ^ Descriptions ^ Classifications |= Hypothese
-> True
(avec des determinations comme connaissance de pertinence, cf. SL-3)
4. KBIL : Background ^ Hypothese ^ Descriptions |= Classifications
-> True
H reste a trouver inductivement, mais Background guide et restreint la recherche.
Interpretation : Contraintes d’apprentissage
Approche
Source de l’hypothese
Rôle de la connaissance
Inductif pur
Exploration de H
Aucune
EBL
Deduction de Background
Genere directement H
RBL
Deduction de Background + observations
Reduit l’espace de recherche
KBIL
Induction guidee
Contraint les hypotheses possibles
Point cle : EBL et RBL sont deductifs — ils ne créent pas de connaissance factuellement nouvelle. Ils transforment la connaissance existante en formes plus utiles.
2. EBL — Idee principale
L’apprentissage base sur les explications (Explanation-Based Learning) permet d’extraire une règle générale a partir d’un seul exemple, en utilisant la connaissance de fond du domaine.
L’exemple de Zog (AIMA)
Zog observe qu’un baton pointu tue un gibier. A partir de cette unique observation et de sa connaissance de la physique/physiologie, il generalise : “Tout objet pointu peut tuer le gibier”.
EBL ne decouvre pas un nouveau fait — Zog savait déjà que les objets pointus traversent la peau, que la penetration entraine la mort, etc. EBL compile cette connaissance en une règle operationnelle.
Le processus EBL (4 étapes)
Construire une explication (arbre de preuve) montrant que le predicat cible s’applique a l’exemple
Variabiliser — remplacer les constantes de l’exemple par des variables
Extraire la règle a partir des feuilles de l’arbre de preuve generalise
Simplifier — supprimer les conditions toujours vraies
Origine. Ce processus en 4 étapes (1. expliquer, 2. variabiliser, 3. extraire la règle, 4. simplifier) est la formalisation canonique de l’EBL donnee par Mitchell, T. M., Keller, R. M. & Kedar-Cabelli, S. T. (1986), Explanation-based generalization: A unifying view, Machine Learning 1(1):47-80. C’est le pendant complementaire (vue generalisation) du travail de DeJong & Mooney (1986) déjà cite en Ressources (vue apprentissage).
Pourquoi EBL est utile ?
Accelerer le raisonnement futur en evitant de re-construire la même preuve
Convertir des théories de premiers principes en heuristiques operationnelles
Analogie : transformer un theoreme general en corollaire specialise
# EBL : l'exemple du baton pointu de Zog, rejoue par chainage avant reel# Semantique AIMA corrigee : c'est la peau de la CIBLE qui est molle (pas le baton).# Atomes = tuples (Predicat, (arguments,)) ; variables en minuscules.RULES = [ ([("Pointu", ("x",))], ("Perce", ("x",))), ([("Perce", ("x",)), ("PeauMolle", ("y",))], ("TraversePeau", ("x", "y"))), ([("TraversePeau", ("x", "y")), ("Vivant", ("y",))], ("Hemorragie", ("y",))), ([("Hemorragie", ("y",))], ("Meurt", ("y",))),]FACTS = {("Pointu", ("Baton",)), ("PeauMolle", ("Gibier",)), ("Vivant", ("Gibier",))}def is_var(term: str) ->bool:return term[0].islower()def unify_atom(pattern, fact, bindings):"""Unifie un atome a variables avec un fait clos ; None si echec."""if pattern[0] != fact[0] orlen(pattern[1]) !=len(fact[1]):returnNone b =dict(bindings)for p, f inzip(pattern[1], fact[1]):if is_var(p):if p in b and b[p] != f:returnNone b[p] = felif p != f:returnNonereturn bdef forward_chain(rules, facts):"""Chainage avant jusqu'au point fixe ; retourne (faits, trace).""" facts =set(facts) trace = [] changed =Truewhile changed: changed =False snapshot =sorted(facts)for body, head in rules: stack = [({}, 0)]while stack: bindings, k = stack.pop()if k ==len(body): new = (head[0], tuple(bindings.get(a, a) for a in head[1]))if new notin facts: facts.add(new) trace.append((body, bindings, new)) changed =Truecontinuefor f in snapshot: b2 = unify_atom(body[k], f, bindings)if b2 isnotNone: stack.append((b2, k +1))return facts, tracedef fmt(atom) ->str:returnf"{atom[0]}({','.join(atom[1])})"derived, trace = forward_chain(RULES, FACTS)print("Exemple EBL : le baton pointu de Zog, rejoue par chainage avant")print("="*62)print("Faits observes :", " ; ".join(sorted(fmt(f) for f in FACTS)))print()print("Derivations :")for body, bindings, new in trace: inst = [(p[0], tuple(bindings.get(a, a) for a in p[1])) for p in body]print(f" {' ^ '.join(fmt(a) for a in inst)} => {fmt(new)}")print()goal = ("Meurt", ("Gibier",))print(f"Classification expliquee : {fmt(goal)} ? {goal in derived}")print()print("Variabilisation EBL (Baton -> x, Gibier -> y) : les feuilles de la preuve")print("deviennent les preconditions, les etapes intermediaires sont compilees :")print(" Pointu(x) ^ PeauMolle(y) ^ Vivant(y) => Meurt(y)")print("(implementation generique de la variabilisation dans la suite du notebook)")
Exemple EBL : le baton pointu de Zog, rejoue par chainage avant
==============================================================
Faits observes : PeauMolle(Gibier) ; Pointu(Baton) ; Vivant(Gibier)
Derivations :
Pointu(Baton) => Perce(Baton)
Perce(Baton) ^ PeauMolle(Gibier) => TraversePeau(Baton,Gibier)
TraversePeau(Baton,Gibier) ^ Vivant(Gibier) => Hemorragie(Gibier)
Hemorragie(Gibier) => Meurt(Gibier)
Classification expliquee : Meurt(Gibier) ? True
Variabilisation EBL (Baton -> x, Gibier -> y) : les feuilles de la preuve
deviennent les preconditions, les etapes intermediaires sont compilees :
Pointu(x) ^ PeauMolle(y) ^ Vivant(y) => Meurt(y)
(implementation generique de la variabilisation dans la suite du notebook)
Interpretation : Principe EBL
Étape
Entree
Sortie
Opération
1. Expliquer
1 exemple + connaissance
Arbre de preuve
Deduction
2. Variabiliser
Arbre de preuve concret
Arbre de preuve general
Substitution
3. Extraire
Arbre general
Règle candidate
Projection des feuilles
4. Simplifier
Règle candidate
Règle finale
Elimination des tautologies
Point cle : L’hypothese EBL ne contient aucune information qui n’etait pas déjà presente dans la connaissance de fond. EBL est un mécanisme d’efficacite, pas un mécanisme de decouverte.
3. EBL — Exemple de simplification arithmetique
L’exemple canonique de l’AIMA est la simplification d’expressions arithmetiques. Etant donne une base de règles de reecriture, nous voulons montrer comment simplifier une expression spécifique, puis generaliser cette preuve.
Base de connaissances pour la simplification
Les règles sont : - Rewrite(u, v) ^ Simplify(v, w) => Simplify(u, w) — composition - Primitive(u) => Simplify(u, u) — un primitif est déjà simplifie - ArithmeticUnknown(u) => Primitive(u) — une variable est un primitif - Rewrite(1 * u, u) — règle de reecriture pour 1*x - Rewrite(0 + u, u) — règle de reecriture pour 0+x
# Representation des regles de simplification arithmetique# On utilise des strings pour garder les expressions lisibles@dataclassclass RewriteRule:"""Regle de reecriture : pattern -> resultat.""" name: str pattern: str# Pattern a chercher result: str# Resultat de la reecrituredefapply(self, expr: str) -> Optional[str]:"""Tente d'appliquer la regle a l'expression."""ifself.pattern in expr:return expr.replace(self.pattern, self.result, 1)returnNone# Regles de reecriture (connaissances de fond)REWRITE_RULES = [ RewriteRule("1*u -> u", "1*", ""), RewriteRule("0+u -> u", "0+", ""),]def is_primitive(expr: str) ->bool:"""Un primitif est une variable ou un nombre seul."""returnlen(expr.strip()) >0andnotany(op in expr for op in ['+', '-', '*', '/'])def simplify_step(expr: str) ->tuple[str, str, str]:"""Une etape de simplification. Retourne : (expression_resultat, nom_regle, explication) """# Essayer les regles de reecriturefor rule in REWRITE_RULES: result = rule.apply(expr)if result isnotNone:return result, rule.name, f"Rewrite({expr}, {result})"# Si c'est deja un primitifif is_primitive(expr):return expr, "primitive", f"Primitive({expr}) => Simplify({expr}, {expr})"return expr, "bloque", "Aucune regle applicable"# Test sur une expression simpleprint("Regles de reecriture chargees :")for r in REWRITE_RULES:print(f" {r.name}")print()print(f"is_primitive('x') = {is_primitive('x')}")print(f"is_primitive('0+x') = {is_primitive('0+x')}")print(f"is_primitive('42') = {is_primitive('42')}")
Regles de reecriture chargees :
1*u -> u
0+u -> u
is_primitive('x') = True
is_primitive('0+x') = False
is_primitive('42') = True
Avec ces règles de reecriture, nous pouvons maintenant construire la preuve complete pour simplifier 1*(0+x) et observer chaque étape de la derivation.
# Construction pas-a-pas de la preuve pour Simplify(1*(0+x), x)# La cible est l'expression "1*(0+x)" et le resultat attendu est "x"@dataclassclass ProofStep:"""Une etape dans l'arbre de preuve EBL.""" step: int input_expr: str output_expr: str rule_name: str justification: strdef build_proof(expr: str, verbose: bool=True) ->list[ProofStep]:"""Construit l'arbre de preuve pour la simplification complete.""" proof = [] current = expr step =0whileTrue: result, rule, explain = simplify_step(current)if rule =="bloque"or result == current:break step +=1 proof_step = ProofStep( step=step, input_expr=current, output_expr=result, rule_name=rule, justification=explain ) proof.append(proof_step)if verbose:print(f" Etape {step}: {current}")print(f" Regle: {rule}")print(f" -> {result}")print() current = result# Securite anti-boucleif step >20:breakreturn proof# Preuve pour l'expression cible : 1*(0+x)target_expr ="1*(0+x)"print(f"Construction de la preuve pour Simplify({target_expr}, x)")print("="*55)print()proof = build_proof(target_expr, verbose=True)print(f"Preuve complete : Simplify({target_expr}, {proof[-1].output_expr}) en {len(proof)} etapes")print()print("Arbre de preuve recapitulatif :")for ps in proof:print(f" {ps.input_expr:12s} --[{ps.rule_name}]--> {ps.output_expr}")
Construction de la preuve pour Simplify(1*(0+x), x)
=======================================================
Etape 1: 1*(0+x)
Regle: 1*u -> u
-> (0+x)
Etape 2: (0+x)
Regle: 0+u -> u
-> (x)
Preuve complete : Simplify(1*(0+x), (x)) en 2 etapes
Arbre de preuve recapitulatif :
1*(0+x) --[1*u -> u]--> (0+x)
(0+x) --[0+u -> u]--> (x)
Interpretation : Arbre de preuve EBL
Étape
Expression
Règle
Résultat
1
1*(0+x)
1*u -> u
0+x
2
0+x
0+u -> u
x
3
x
primitive
x (déjà simplifie)
La preuve montre que Simplify(1*(0+x), x) est derivable en 2 étapes de reecriture puis une verification de primalite.
Note : Cette preuve est spécifique a l’expression 1*(0+x). L’étape suivante (variabilisation) va la generaliser.
4. EBL — Variabilisation et extraction de règle
L’étape centrale d’EBL est la variabilisation : remplacer les constantes de l’exemple par des variables dans l’arbre de preuve, puis extraire une règle générale.
Principe
Identifier les constantes dans l’exemple (ex: x dans 1*(0+x) est une variable, mais le 0 et le 1 sont des constantes structurelles)
Remplacer les constantes exemplaires par des variables fraiches
Les conditions qui sont toujours vraies (tautologies) sont supprimees
La règle extraite est la generalisation la plus large possible qui preserve la structure de la preuve
# Implementation de la variabilisationdef variabilize_proof( proof: list[ProofStep], const_to_var: dict[str, str]) ->list[ProofStep]:"""Remplace les constantes par des variables dans l'arbre de preuve. Args: proof: L'arbre de preuve original const_to_var: Mapping constante -> variable (ex: {'x': 'z'}) Returns: L'arbre de preuve variabilise """ generalized = []for step in proof: new_input = step.input_expr new_output = step.output_expr new_justif = step.justificationfor const, var in const_to_var.items(): new_input = new_input.replace(const, var) new_output = new_output.replace(const, var) new_justif = new_justif.replace(const, var) generalized.append(ProofStep( step=step.step, input_expr=new_input, output_expr=new_output, rule_name=step.rule_name, justification=new_justif ))return generalizeddef extract_rule(generalized_proof: list[ProofStep], variables: list[str]) ->str:"""Extrait la regle generalisee de la preuve. La regle est : preconditions => Simplify(input, output), ou les preconditions declarent chaque variable introduite par la variabilisation. """ input_expr = generalized_proof[0].input_expr output_expr = generalized_proof[-1].output_expr preconditions =" AND ".join(f"ArithmeticUnknown({v})"for v in variables)returnf"{preconditions} => Simplify({input_expr}, {output_expr})"# Application : variabiliser la preuve de 1*(0+x)print("Variabilisation de la preuve")print("="*50)print()print("Preuve originale :")for ps in proof:print(f" {ps.input_expr:12s} --[{ps.rule_name}]--> {ps.output_expr}")print()print("Mapping des constantes : x -> z")gen_proof = variabilize_proof(proof, {'x': 'z'})print("Preuve variabilisee :")for ps in gen_proof:print(f" {ps.input_expr:12s} --[{ps.rule_name}]--> {ps.output_expr}")print()rule = extract_rule(gen_proof, ["z"])print(f"Regle extraite : {rule}")
Variabilisation de la preuve
==================================================
Preuve originale :
1*(0+x) --[1*u -> u]--> (0+x)
(0+x) --[0+u -> u]--> (x)
Mapping des constantes : x -> z
Preuve variabilisee :
1*(0+z) --[1*u -> u]--> (0+z)
(0+z) --[0+u -> u]--> (z)
Regle extraite : ArithmeticUnknown(z) => Simplify(1*(0+z), (z))
Interpretation : Variabilisation EBL
Phase
Expression
Commentaire
Preuve concrete
1*(0+x) -> (x)
Spécifique a l’exemple observe
Preuve variabilisee
1*(0+z) -> (z)
Variable z remplace x
Règle extraite
ArithmeticUnknown(z) => Simplify(1*(0+z), (z))
Condition : z doit etre un inconnu arithmetique
La condition ArithmeticUnknown(z) est la seule precondition non-tautologique. Les règles 1*u->u et 0+u->u sont des axiomes de la base de connaissance — elles sont toujours vraies. (Les parentheses residuelles de (z) restent en l’etat : notre mini-système ne reecrit que 1* et 0+.)
Point cle : La règle extraite dit “pour TOUT z qui est un inconnu arithmetique, Simplify(1*(0+z), (z))“. Cette règle est nouvelle par rapport a la base de connaissance (elle n’y etait pas), mais elle est deduite de cette base.
5. EBL — Implementation complete
Nous integrons maintenant les 4 étapes de l’EBL dans une classe complete : explanation, variabilisation, extraction et stockage des règles apprises.
Architecture
ArithmeticEBL
|- kb_rules : base de règles de reecriture
|- learned_rules : règles extraites par EBL
|- explain() : construit l'arbre de preuve
|- variabilize() : generalise la preuve
|- extract_rule() : extrait la règle finale
|- learn() : pipeline complet
|- simplify_with_learned() : simplification utilisant les règles apprises
import re@dataclassclass LearnedRule:"""Regle extraite par EBL.""" original_example: str pattern: str# Pattern generalise (ex: "1*(0+z)") result: str# Resultat generalise (ex: "z") precondition: str# Condition non-tautologique proof_length: int# Longueur de la preuve originaleclass ArithmeticEBL:"""Implementation complete de l'EBL pour la simplification arithmetique."""def__init__(self):self.kb_rules = [ RewriteRule("1*u -> u", "1*", ""), RewriteRule("0+u -> u", "0+", ""),# NB : "0*u -> 0" n'est pas exprimable dans ce schema substring# (il faudrait capturer u) ; le matching a variables des regles# apprises (_match_learned) montre comment faire mieux. RewriteRule("u+0 -> u", "+0", ""), RewriteRule("u*1 -> u", "*1", ""), ]self.learned: list[LearnedRule] = []def _is_primitive(self, expr: str) ->bool:"""Verifie si l'expression est un primitif."""returnlen(expr.strip()) >0andnotany(op in expr for op in ['+', '-', '*', '/'])def _simplify_one_step(self, expr: str) ->tuple[str, str]:"""Une seule etape de simplification."""for rule inself.kb_rules: result = rule.apply(expr)if result isnotNoneand result != expr:return result, rule.nameifself._is_primitive(expr):return expr, "primitive"return expr, "bloque"def explain(self, expr: str) ->list[ProofStep]:"""Etape 1 : construit l'arbre de preuve pour l'expression.""" proof = [] current = expr step_num =0whileTrue: result, rule =self._simplify_one_step(current)if rule =="bloque"or result == current:break step_num +=1 proof.append(ProofStep( step=step_num, input_expr=current, output_expr=result, rule_name=rule, justification=f"{current} -> {result} [{rule}]" )) current = resultif step_num >20:breakreturn proofdef variabilize(self, proof: list[ProofStep], const_map: dict[str, str] ) ->list[ProofStep]:"""Etape 2 : variabilise l'arbre de preuve.""" generalized = []for step in proof: new_input = step.input_expr new_output = step.output_expr new_justif = step.justificationfor const, var in const_map.items(): new_input = new_input.replace(const, var) new_output = new_output.replace(const, var) new_justif = new_justif.replace(const, var) generalized.append(ProofStep( step=step.step, input_expr=new_input, output_expr=new_output, rule_name=step.rule_name, justification=new_justif ))return generalizeddef extract_rule(self, gen_proof: list[ProofStep], const_map: dict[str, str] ) -> LearnedRule:"""Etape 3 : extrait la regle de la preuve generalisee.""" pattern = gen_proof[0].input_expr result = gen_proof[-1].output_expr# Identifier les variables vars_used =set(const_map.values()) preconditions = [f"ArithmeticUnknown({v})"for v in vars_used]return LearnedRule( original_example=gen_proof[0].input_expr, pattern=pattern, result=result, precondition=" AND ".join(preconditions), proof_length=len(gen_proof) )def learn(self, example: str, const_to_var: dict[str, str], verbose: bool=True ) -> LearnedRule:"""Pipeline EBL complet : expliquer -> variabiliser -> extraire."""# Etape 1 : Construire l'explication proof =self.explain(example)ifnot proof:if verbose:print(f" Aucune preuve trouvee pour {example}")returnNone# type: ignore# Etape 2 : Variabiliser gen_proof =self.variabilize(proof, const_to_var)# Etape 3 : Extraire la regle rule =self.extract_rule(gen_proof, const_to_var)# Stockerself.learned.append(rule)if verbose:print(f" Exemple : {example}")print(f" Preuve : {len(proof)} etapes")print(f" Regle : {rule.precondition} => Simplify({rule.pattern}, {rule.result})")return ruledef _match_learned(self, rule: LearnedRule, expr: str) ->str|None:r"""Matche un pattern appris contre l'expression (unification simple). Le pattern "1*(0+z)" devient la regex r"1\*\(0\+(\w+)\)" : chaque variable declaree dans la precondition capture un sous-terme, et le resultat est reconstruit en substituant les captures. C'est ce qui permet a la regle apprise sur x de s'appliquer a a, b, c... """ variables = re.findall(r"ArithmeticUnknown\((\w+)\)", rule.precondition) regex = re.escape(rule.pattern)for v in variables: regex = regex.replace(re.escape(v), f"(?P<{v}>\\w+)", 1) m = re.search(regex, expr)if m isNone:returnNone result = rule.resultfor v in variables: result = result.replace(v, m.group(v))return expr[:m.start()] + result + expr[m.end():]def simplify_with_learned(self, expr: str) ->tuple[str, str, int]:"""Simplifie en utilisant d'abord les regles apprises, puis la KB. Retourne : (resultat, methode, nombre_d_etapes) """# Essayer d'abord les regles apprises (matching a variables, 1 etape)for rule inself.learned: matched =self._match_learned(rule, expr)if matched isnotNone:return matched, f"learned({rule.pattern}->{rule.result})", 1# Sinon, utiliser la KB classique proof =self.explain(expr)if proof:return proof[-1].output_expr, "kb", len(proof)return expr, "bloque", 0# Demonstration completeebl = ArithmeticEBL()print("Pipeline EBL complet")print("="*50)print()# Apprendre a partir de l'exemple 1*(0+x)print("Apprentissage 1 :")rule1 = ebl.learn("1*(0+x)", {"x": "z"})print()# Apprendre a partir d'un autre exempleprint("Apprentissage 2 :")rule2 = ebl.learn("1*y", {"y": "v"})print()# Apprendre encore un autreprint("Apprentissage 3 :")rule3 = ebl.learn("0+u", {"u": "w"})print()print(f"Regles apprises : {len(ebl.learned)}")for i, r inenumerate(ebl.learned):print(f" {i+1}. {r.precondition} => Simplify({r.pattern}, {r.result})")
O(n * \|règles\|) ou n = profondeur de simplification
variabilize()
Substitution constante -> variable
O(n * \|mapping\|)
extract_rule()
Projection des feuilles
O(n)
learn()
Pipeline complet
Lineaire en la taille de la preuve
simplify_with_learned()
Utilisation des règles
O(1) en cas de hit, O(n*\|règles\|) sinon
L’intérêt principal est le lookup O(1) : une règle apprise permet de court-circuiter la chaîne de preuve complete.
Note : Dans notre implementation simplifiee, la correspondance de pattern est textuelle. Un système reel utiliserait un unificateur (pattern matching avec variables) pour une generalisation correcte.
6. EBL — Efficacite et operationalite
EBL introduit un compromis fondamental entre operationalite et generalite :
Règles très spécifiques (ex: Simplify(1*(0+x), x)) : faciles a evaluer mais couvrent peu de cas
Règles très générales (ex: Simplify(u, u)) : couvrent tout mais n’apportent rien
Le benefice reel d’EBL depend du taux de reutilisation des règles apprises.
Risque : la proliferation de règles
Ajouter trop de règles spécifiques peut ralentir le système : - Chaque règle supplementaire augmente le temps de recherche (branching factor) - Les règles trop spécifiques ne sont presque jamais reutilisees - Il faut un mécanisme de filtrage ou de desapprentissage
# Demonstration du speedup apres apprentissage EBL# On compare la simplification AVEC et SANS regles apprises# Expressions de test (similaires mais pas identiques a l'exemple d'apprentissage)test_exprs = ["1*(0+a)","1*(0+b)","1*(0+c)","1*(0+d)","1*(0+e)","1*f","0+g","1*(0+h)","1*(0+i)","1*(0+j)",]# Systeme SANS regles apprisesebl_fresh = ArithmeticEBL() # Pas de learn()# Systeme AVEC regles apprisesebl_smart = ArithmeticEBL()ebl_smart.learn("1*(0+x)", {"x": "z"}, verbose=False)ebl_smart.learn("1*y", {"y": "v"}, verbose=False)ebl_smart.learn("0+u", {"u": "w"}, verbose=False)print("Comparaison de performance : avec vs sans regles EBL")print("="*60)print()print(f"{'Expression':12s} | {'Sans EBL':>20s} | {'Avec EBL':>20s} | Gain")print("-"*70)total_steps_fresh =0total_steps_smart =0for expr in test_exprs: _, method_f, steps_f = ebl_fresh.simplify_with_learned(expr) _, method_s, steps_s = ebl_smart.simplify_with_learned(expr) total_steps_fresh += steps_f total_steps_smart += steps_s gain = steps_f - steps_s gain_str =f"-{gain}"if gain >0else"="print(f"{expr:12s} | {method_f:>15s} ({steps_f}e) | {method_s:>15s} ({steps_s}e) | {gain_str}")print("-"*70)print(f"{'TOTAL':12s} | {total_steps_fresh:>20d} | {total_steps_smart:>20d} | -{total_steps_fresh - total_steps_smart}")print()if total_steps_fresh >0: speedup = total_steps_fresh / total_steps_smart if total_steps_smart >0elsefloat('inf')print(f"Speedup moyen : {speedup:.1f}x") reduction = (1- total_steps_smart / total_steps_fresh) *100if total_steps_fresh >0else0print(f"Reduction d'etapes : {reduction:.0f}%")
Le compromis est clair : - Avantage : Les expressions correspondant aux règles apprises sont simplifiees en 1 étape au lieu de 2-3 - Inconvenient : Chaque règle apprise prend de l’espace et du temps de recherche - Quand EBL est efficace : Quand les mêmes patterns de simplification apparaissent souvent - Quand EBL est inefficace : Quand les expressions sont toutes différentes (règles jamais reutilisees)
Point cle AIMA : “EBL converts first-principles théories into useful special-purpose knowledge.” La valeur d’EBL depend entierement de la frequence de reutilisation.
7. RBL — Introduction aux determinations
L’apprentissage base sur la pertinence (Relevance-Based Learning) utilise la connaissance prealable pour identifier quels attributs sont pertinents pour la tâche d’apprentissage.
Determinations
Une determination exprime qu’un ensemble d’attributs determine completement un autre attribut :
Cela signifie : si deux personnes ont la même nationalite, elles parlent la même langue.
Impact sur l’espace des hypotheses
Sans determination : l’espace est \(O(2^n)\) ou n est le nombre total d’attributs. Avec une determination d’attributs pertinents : l’espace se reduit a \(O(2^d)\) ou d est le nombre d’attributs determinants.
Si d << n, la reduction est exponentielle.
# Implementation de la verification de determinationdef check_determination( data: list[dict], det_attrs: list[str], target_attr: str) ->bool:"""Verifie si det_attrs determinent target_attr dans les donnees. Une determination est valide si pour chaque combinaison de valeurs de det_attrs, il y a exactement UNE valeur de target_attr. Args: data: Liste de dictionnaires (lignes de donnees) det_attrs: Attributs determinants hypothetiques target_attr: Attribut cible Returns: True si la determination est valide """ groups: dict[tuple, set] = defaultdict(set)for row in data: key =tuple(row[a] for a in det_attrs) groups[key].add(row[target_attr])returnall(len(values) ==1for values in groups.values())# Exemple : nationalite determine la languenationality_data = [ {"name": "Alice", "nationality": "France", "language": "Francais", "age": 30, "city": "Paris"}, {"name": "Bob", "nationality": "France", "language": "Francais", "age": 25, "city": "Lyon"}, {"name": "Carlos", "nationality": "Espagne", "language": "Espagnol", "age": 35, "city": "Madrid"}, {"name": "Diana", "nationality": "Espagne", "language": "Espagnol", "age": 28, "city": "Barcelone"}, {"name": "Elena", "nationality": "Italie", "language": "Italien", "age": 32, "city": "Rome"}, {"name": "Fernando", "nationality": "Bresil", "language": "Portugais", "age": 40, "city": "Sao Paulo"}, {"name": "Greta", "nationality": "Allemagne", "language": "Allemand", "age": 27, "city": "Berlin"}, {"name": "Hiroshi", "nationality": "Japon", "language": "Japonais", "age": 45, "city": "Tokyo"}, {"name": "Ivan", "nationality": "Russie", "language": "Russe", "age": 33, "city": "Moscou"}, {"name": "Julia", "nationality": "Allemagne", "language": "Allemand", "age": 29, "city": "Munich"}, {"name": "Klaus", "nationality": "Allemagne", "language": "Allemand", "age": 50, "city": "Hamburg"}, {"name": "Lucia", "nationality": "Italie", "language": "Italien", "age": 22, "city": "Milan"},]ALL_ATTRS = ["nationality", "age", "city"]print("Donnees : nationalite -> langue")print("="*55)print()print(f"{'Nom':10s} | {'Nationalite':12s} | {'Langue':10s} | {'Age':3s} | {'Ville':10s}")print("-"*55)for row in nationality_data:print(f"{row['name']:10s} | {row['nationality']:12s} | {row['language']:10s} | {row['age']:3d} | {row['city']:10s}")print()print("Verification de determinations :")print(f" nationality -> language : {check_determination(nationality_data, ['nationality'], 'language')}")print(f" age -> language : {check_determination(nationality_data, ['age'], 'language')}")print(f" city -> language : {check_determination(nationality_data, ['city'], 'language')}")print(f" nationality,age -> lang : {check_determination(nationality_data, ['nationality', 'age'], 'language')}")
Donnees : nationalite -> langue
=======================================================
Nom | Nationalite | Langue | Age | Ville
-------------------------------------------------------
Alice | France | Francais | 30 | Paris
Bob | France | Francais | 25 | Lyon
Carlos | Espagne | Espagnol | 35 | Madrid
Diana | Espagne | Espagnol | 28 | Barcelone
Elena | Italie | Italien | 32 | Rome
Fernando | Bresil | Portugais | 40 | Sao Paulo
Greta | Allemagne | Allemand | 27 | Berlin
Hiroshi | Japon | Japonais | 45 | Tokyo
Ivan | Russie | Russe | 33 | Moscou
Julia | Allemagne | Allemand | 29 | Munich
Klaus | Allemagne | Allemand | 50 | Hamburg
Lucia | Italie | Italien | 22 | Milan
Verification de determinations :
nationality -> language : True
age -> language : True
city -> language : True
nationality,age -> lang : True
Interpretation : Determinations
Attributs testes
Determine langue ?
Raison
nationality
Oui
Même nationalite => même langue
age
Non
Alice (30) et Ivan (33) parlent des langues différentes
city
Non
Pas de correlation ville -> langue unique
nationality, age
Oui
Plus restrictif mais toujours valide
Point cle : La determination minimale est nationality seule. Ajouter age ne change rien a la validite mais rend la determination plus restrictive sans benefice.
L’impact sur l’espace des hypotheses est considerable. La determination ramene l’apprentissage de la fonction langue a une simple table nationalite -> langue : un exemple par nationalite observee suffit (croissance lineaire avec le nombre de nationalites), au lieu d’explorer l’espace des hypotheses construit sur les 4 attributs (croissance exponentielle avec le nombre d’attributs, cf. la borne PAC detaillee en SL-3).
Pour aller plus loin : SL-3
Nous nous arretons volontairement ici. La formalisation complete du RBL — proprietes des determinations (monotonie, minimalite), treillis des determinations, algorithme MINIMAL-CONSISTENT-DET et sa complexite, cas d’echec sur les concepts disjonctifs (le restaurant de SL-1), et comparaison avec la sélection statistique d’attributs (information mutuelle, sklearn) — est l’objet du notebook dedie SL-3 - Apprentissage Base sur la Pertinence.
Pour situer le RBL parmi les paradigmes d’apprentissage utilisant la connaissance, le concept de determination introduit ci-dessus suffit.
8. Synthese et exercices
Tableau recapitulatif
Aspect
EBL
RBL
KBIL
Type
Deductif
Deductif
Inductif
Connaissance requise
Théorie complete du domaine
Determinations (attributs pertinents)
Théorie partielle + exemples
Nouveaux faits
Non
Non
Oui
Exemples necessaires
1 seul
Peu
Beaucoup
Résultat
Règles operationnelles
Reduction d’espace
Hypotheses inductives
Exemple AIMA
Simplification arithmetique
Nationalite -> Langue
Programmation logique inductive
Points cles
EBL compile les théories de premiers principes en heuristiques operationnelles. Il n’apprend rien de nouveau factuellement, mais rend le raisonnement plus efficace.
RBL utilise les determinations pour reduire exponentiellement l’espace des hypotheses. Il suffit de connaitre les attributs pertinents pour accelerer considerablement l’apprentissage.
KBIL (couvert a partir de SL-4) combine la connaissance de fond avec l’induction pour apprendre de veritables nouvelles hypotheses que ni EBL ni RBL ne pourraient decouvrir seuls.
L’operationalite en EBL est un compromis : les règles trop spécifiques ne sont pas reutilisees, les règles trop générales n’apportent rien.
Exemple guidé 1 : l’EBL appliqué à la différentiation symbolique
Les sections 4 à 6 ont illustré l’EBL sur la simplification arithmétique : à partir d’un seul exemple résolu, on extrait par variabilisation une règle macro réutilisable. Avant de passer aux exercices, voici un exemple entièrement résolu qui rejoue exactement le même mécanisme sur un autre domaine, la différentiation symbolique.
La théorie de domaine est ici l’ensemble des règles de dérivation (\(d/dx(u+v)=du+dv\), \(d/dx(u\cdot v)=du\cdot v + u\cdot dv\), …). La cellule construit d’abord un dérivateur symbolique, puis montre comment l’EBL en généralise la règle de la chaîne à partir d’une seule trace de preuve — la démarche des sections 4-6, transposée. Étudiez-le : les trois exercices qui suivent vous demanderont de l’étendre.
# Exemple guide 1 : EBL sur la differentiation symbolique## La theorie de domaine = les regles de derivation :# d/dx(x) = 1, d/dx(c) = 0, d/dx(u+v) = du + dv,# d/dx(u*v) = du*v + u*dv, d/dx(sin(u)) = cos(u) * du# On implemente d'abord la derivation, puis on montre comment l'EBL extrait# la regle generale (le chain rule) d'un seul exemple par variabilisation,# exactement comme pour la simplification arithmetique en sections 4-6.def _split_niveau_zero(expr: str, op: str) ->int:"""Index du dernier operateur 'op' au niveau 0 (hors parentheses), ou -1. Chercher hors des parentheses evite de couper un sous-terme comme 'sin(x*y)' au '*' interne : on ne scinde que l'operateur racine de l'expression. """ profondeur =0for i inrange(len(expr) -1, -1, -1): c = expr[i]if c ==")": profondeur +=1elif c =="(": profondeur -=1elif profondeur ==0and c == op:return ireturn-1def symbolic_diff(expr: str, var: str="x") ->str:"""Derivation symbolique (theorie de domaine de l'EBL). Supporte : variables, constantes, u+v, u*v, sin(u). """ expr = expr.replace(" ", "")# d/dx(u+v) = du + dv i = _split_niveau_zero(expr, "+")if i !=-1:return symbolic_diff(expr[:i], var) +"+"+ symbolic_diff(expr[i +1:], var)# d/dx(u*v) = du*v + u*dv i = _split_niveau_zero(expr, "*")if i !=-1: u, v = expr[:i], expr[i +1:]return symbolic_diff(u, var) +"*"+ v +"+"+ u +"*"+ symbolic_diff(v, var)# d/dx(sin(u)) = cos(u) * duif expr.startswith("sin(") and expr.endswith(")"): inner = expr[4:-1]return"cos("+ inner +")*"+ symbolic_diff(inner, var)# d/dx(x) = 1if expr == var:return"1"# d/dx(c) = 0 (constante, ou variable autre que celle dont on derive)return"0"# --- Tests : la theorie de domaine produit les bonnes derivees ---print("Tests de symbolic_diff :")for expr, attendu in [("x", "1"), ("5", "0"), ("x+y", "1+0"), ("x*y", "1*y+x*0"), ("x*x", "1*x+x*1"), ("sin(x)", "cos(x)*1")]: res = symbolic_diff(expr) ok ="OK "if res == attendu else"?? "print(f" {ok}d/dx({expr}) = {res}")print()# --- EBL : extraire la regle generale d'un seul exemple ---# Exemple de depart : d/dx(sin(x)). L'EBL construit l'explication (l'arbre de# derivation), puis variabilise la constante 'x' en variable 'u'.print("Exemple de depart : d/dx(sin(x)) =", symbolic_diff("sin(x)"))print("Cas composite : d/dx(sin(x*y)) =", symbolic_diff("sin(x*y)"))print()print("Variabilisation x -> u de l'exemple d/dx(sin(x)) :")print(" - NAIVE (sur le RESULTAT brut) : cos(u)*1 <-- FIGE le sous-but a 1")print(" - CORRECTE (sur la PREUVE) : d/dx(sin(u)) = cos(u) * d/dx(u)")print(" le sous-but d/dx(x) devient le sous-but general d/dx(u) = 'du'")print()print("Verification pour u = x*y : la regle generalisee predit cos(x*y)*du,")print("et symbolic_diff reconstruit exactement cos(u)*du :")du = symbolic_diff("x*y")print(f" d/dx(x*y) [le sous-but du] = {du}")print(f" cos(x*y) * du = cos(x*y)*{du}")print(f" symbolic_diff('sin(x*y)') = {symbolic_diff('sin(x*y)')}")print(" => coincidence par construction : le chain rule extrait par EBL est correct.")
Tests de symbolic_diff :
OK d/dx(x) = 1
OK d/dx(5) = 0
OK d/dx(x+y) = 1+0
OK d/dx(x*y) = 1*y+x*0
OK d/dx(x*x) = 1*x+x*1
OK d/dx(sin(x)) = cos(x)*1
Exemple de depart : d/dx(sin(x)) = cos(x)*1
Cas composite : d/dx(sin(x*y)) = cos(x*y)*1*y+x*0
Variabilisation x -> u de l'exemple d/dx(sin(x)) :
- NAIVE (sur le RESULTAT brut) : cos(u)*1 <-- FIGE le sous-but a 1
- CORRECTE (sur la PREUVE) : d/dx(sin(u)) = cos(u) * d/dx(u)
le sous-but d/dx(x) devient le sous-but general d/dx(u) = 'du'
Verification pour u = x*y : la regle generalisee predit cos(x*y)*du,
et symbolic_diff reconstruit exactement cos(u)*du :
d/dx(x*y) [le sous-but du] = 1*y+x*0
cos(x*y) * du = cos(x*y)*1*y+x*0
symbolic_diff('sin(x*y)') = cos(x*y)*1*y+x*0
=> coincidence par construction : le chain rule extrait par EBL est correct.
Exercice 1 (variation) : EBL sur le carre d/dx(u*u)
L’exemple guide montre comment l’EBL extrait le chain ruled/dx(sin(u))=cos(u)*du par variabilisation de la preuve. Appliquez la même idee au carre : a partir de l’exemple particulier d/dx(x*x) = 1*x+x*1, variabilisezx -> u pour extraire la règle générale d/dx(u*u), puis verifiez-la avec symbolic_diff.
Indices : 1. Calculez symbolic_diff("x*x") (cas particulier ou u = x). 2. Remplacez x par u dans le résultat -> c’est la règle generalisee d/dx(u*u). 3. Verifiez : symbolic_diff("y*y") doit donner la même forme (avec y au lieu de x), ce qui prouve que la règle est bien générale (independante du nom de la variable). 4. Bonus : après simplification arithmetique, d/dx(u*u) = 2*u – retrouve-t-on le product rule ?
# Exercice 1 (variation) : EBL sur le carre d/dx(u*u)# TODO etudiant : extrayez la regle generale d/dx(u*u) par variabilisation# Etape 1 : calculez symbolic_diff("x*x") (cas particulier u=x)# Etape 2 : variabilisez x -> u dans le resultat -> regle generale d/dx(u*u)# Etape 3 : verifiez avec symbolic_diff("y*y") que la forme est identique (regle generale)# Indice : la regle generalisee est d/dx(u*u) = 1*u + u*1 (qui se simplifie en 2*u)print("Exercice a completer : extrayez d/dx(u*u) par variabilisation EBL")
Exercice a completer : extrayez d/dx(u*u) par variabilisation EBL
Exemple guide 2 : Filtrage des règles apprises (operationalite)
La section 6 a montre le compromis fondamental de l’EBL : chaque règle apprise augmente le branching factor (temps de recherche), et n’est rentable que si elle est suffisamment reutilisee. Une règle trop spécifique – apprise pour un seul exemple – ne sert presque jamais et ralentit le système pour rien. On implemente ici le mécanisme de desapprentissage annonce : (a) compter les utilisations de chaque règle lors d’un batch de simplifications, puis (b) ne conserver que les règles dont le taux de reutilisation atteint un seuil.
Principe. ArithmeticEBL.simplify_with_learned renvoie (result, method, steps) ou method vaut : - kb : aucune règle apprise utilisee (fallback sur la base de règles générale) ; - learned(<pattern>-><result>) : la règle apprise identifiee par son pattern.
On exploite ce champ method pour compter, puis on filtre ebl.learned selon un seuil d’utilisation.
# Exemple guide 2 : Filtrage des regles apprises (operationalite)# La section 6 : chaque regle apprise augmente le temps de recherche (branching factor).# Une regle n'est rentable que si elle est suffisamment reutilisee. On implemente# (a) un compteur d'utilisation des regles lors d'un batch de simplifications, et# (b) prune_learned_rules : desapprendre les regles peu utilisees (sous un seuil).from collections import defaultdictdef count_rule_usage(ebl, expressions: list[str]) ->dict[str, int]:"""Compte les utilisations de chaque regle APPRISE sur un corpus d'expressions. ebl.simplify_with_learned renvoie (result, method, steps) ou method est : - 'kb' -> aucune regle apprise utilisee - 'learned(<pattern>-><result>)' -> la regle apprise identifiee par son pattern Returns: Dictionnaire {pattern_de_regle: nombre_d_utilisations}. """ usage = defaultdict(int)for expr in expressions: _, method, _ = ebl.simplify_with_learned(expr)if method.startswith("learned("):# Extraire le pattern : 'learned(1*(0+z)->(z))' -> '1*(0+z)' inner = method[len("learned("):-1] # '1*(0+z)->(z)' pattern = inner.split("->")[0] # '1*(0+z)' usage[pattern] +=1returndict(usage)def prune_learned_rules(ebl, usage_counts: dict[str, int], min_usage: int=2) ->list:"""Retourne la liste des regles apprises a CONSERVER. Une regle est conservee si elle a ete utilisee au moins min_usage fois. C'est le mecanisme de desapprentissage : les regles peu reutilisees (peu operationnelles) sont eliminees pour reduire le branching factor. Args: ebl: instance d'ArithmeticEBL (regles apprises dans ebl.learned) usage_counts: nombre d'utilisations par pattern de regle min_usage: seuil de conservation Returns: Liste des LearnedRule a conserver. """return [rule for rule in ebl.learnedif usage_counts.get(rule.pattern, 0) >= min_usage]# --- Etape 1 : enseigner 3 regles a une instance d'ArithmeticEBL ---ebl = ArithmeticEBL()ebl.learn("1*(0+x)", {"x": "z"}, verbose=False) # regle : 1*(0+z) -> (z)ebl.learn("1*y", {"y": "v"}, verbose=False) # regle : 1*v -> vebl.learn("0+u", {"u": "w"}, verbose=False) # regle : 0+w -> wprint(f"Regles apprises initialement : {len(ebl.learned)}")for i, rule inenumerate(ebl.learned, 1):print(f" R{i} : {rule.pattern} -> {rule.result}")# --- Etape 2 : simplifier un corpus et compter les utilisations par regle ---# Corpus concu pour exposer les 3 profils : regle tres reutilisee (1*(0+z)),# moyennement (1*v), et rarement (0+w) -- plus des expressions sans regle apprise.corpus = ["1*(0+a)", "1*(0+b)", "1*(0+c)", "1*(0+d)", # declenchent la regle 1*(0+z)"1*(0+e)", "1*(0+f)", "1*(0+g)", "1*(0+h)", # declenchent la regle 1*(0+z)"1*y", # declenche la regle 1*v"0+w", # declenche la regle 0+w"1*nomatch", "2+3", # kb only, aucune regle apprise]usage = count_rule_usage(ebl, corpus)print(f"\nUtilisations par regle apprise sur le corpus ({len(corpus)} expressions) :")for rule in ebl.learned:print(f" {rule.pattern:12s} -> {rule.result:6s} : {usage.get(rule.pattern, 0)} utilisation(s)")# --- Etape 3 : appliquer prune_learned_rules (min_usage=2) et verifier ---seuil =2avant =len(ebl.learned)ebl.learned = prune_learned_rules(ebl, usage, min_usage=seuil) # desapprentissage effectifapres =len(ebl.learned)print(f"\nDesapprentissage (min_usage={seuil}) : {avant} regles -> {apres} conservees")print("Regles conservees :")for rule in ebl.learned:print(f" {rule.pattern} -> {rule.result} ({usage.get(rule.pattern, 0)} utilisations)")# --- Verification : la regle peu utilisee a bien ete desapprise ---patterns_restants = {rule.pattern for rule in ebl.learned}print(f"\nVerification :")print(f" Regle tres reutilisee 1*(0+z) conservee : {'1*(0+z)'in patterns_restants}")print(f" Regle peu utilisee 0+w desappris : {'0+w'notin patterns_restants}")print(f" => Le desapprentissage ne garde que les regles rentables (operationalite).")
Regles apprises initialement : 3
R1 : 1*(0+z) -> (z)
R2 : 1*v -> v
R3 : 0+w -> w
Utilisations par regle apprise sur le corpus (12 expressions) :
1*(0+z) -> (z) : 8 utilisation(s)
1*v -> v : 2 utilisation(s)
0+w -> w : 1 utilisation(s)
Desapprentissage (min_usage=2) : 3 regles -> 2 conservees
Regles conservees :
1*(0+z) -> (z) (8 utilisations)
1*v -> v (2 utilisations)
Verification :
Regle tres reutilisee 1*(0+z) conservee : True
Regle peu utilisee 0+w desappris : True
=> Le desapprentissage ne garde que les regles rentables (operationalite).
Interpretation : l’EBL n’est rentable que si les règles sont reutilisees
La mesure confirme quantitativement le compromis d’operationalite de la section 6 :
Les règles très reutilisees (ici 1*(0+z), utilisee 8 fois) sont précisément celles que l’EBL a compilees avec le plus de benefice : chaque utilisation economise une chaîne de preuve complete (lookup O(1) au lieu de O(n)).
Les règles rarement declenchees (ici 0+w, utilisee 1 seule fois) coutent plus en branching factor qu’elles ne rapportent : les desapprendre assainit la base sans perte pratique.
Lien avec AIMA 19.3.4. Le desapprentissage par seuil d’utilisation est une reponse directe au risque de proliferation des règles : sans filtrage, un EBL qui apprend une règle par exemple vu finit par memoriser plutot qu’a compiler. Le seuil min_usage incarne le taux de rentabilite en dessous duquel une règle n’est pas operationnelle – l’equivalent, pour l’EBL, du rasoir d’Occam de l’Exemple guide 5 (SL-1) : un critere de sélection parmi les connaissances consistantes.
Point de vigilance : le seuil et le corpus determinent quelles règles survivent. Un corpus non representatif ferait disparaitre une règle en realite utile (faux negatif d’utilisation) ; un seuil trop bas laisserait proliferer les règles spécifiques. C’est l’eternel compromis biais-variance applique a la memoire de règles.
Exemple guide 3 : Mesurer empiriquement le speedup EBL
On mesure maintenant le speedup obtenu par les règles EBL sur un corpus systématique de 100 expressions, en comparant les temps de simplification avec et sans apprentissage. Deux metriques sont suivies :
Nombre d’étapes (metrique déterministe, reproductible) : le nombre de pas de reecriture effectues par simplify_with_learned. C’est la mesure canonique du speedup algorithmique de l’EBL (cf section précédente).
Temps wall-clock (metrique secondaire, bruitee) : la duree reelle via time.perf_counter(), moyennee sur plusieurs repetitions pour amortir le bruit de la charge CPU.
On attend qu’un corpus heterogene revele sur quelle catégorie d’expressions l’EBL est rentable – et, surprise, que la reponse differe selon la metrique choisie.
# Exemple guide 3 : Mesurer le speedup EBL# Metrique PRINCIPALE = nombre d'etapes (deterministe, reproductible).# Metrique SECONDAIRE = temps wall-clock (indicateur, bruite sur CPU, moyenne sur N runs).import timeimport random# --- Etape 1 : deux instances d'ArithmeticEBL ---ebl_without = ArithmeticEBL() # base de regles generale uniquement (1*u->u, 0+u->u)ebl_with = ArithmeticEBL() # + regles apprises (compilation EBL)# --- Etape 2 : enseigner des regles a ebl_with ---ebl_with.learn("1*(0+x)", {"x": "z"}, verbose=False) # compile 1*(0+z) -> (z) (1 etape au lieu de 2 KB)ebl_with.learn("1*y", {"y": "v"}, verbose=False) # compile 1*v -> vebl_with.learn("0+u", {"u": "w"}, verbose=False) # compile 0+w -> wprint(f"ebl_without : {len(ebl_without.learned)} regle apprise (base KB seule)")print(f"ebl_with : {len(ebl_with.learned)} regles apprises")print()# --- Etape 3 : generer 100 expressions reproductibles (seed fixee) ---# 3 familles pour repondre a 'le speedup depend-il du type d'expression ?' :# - PROFONDES : 1*(0+x) -> 2 etapes KB, 1 etape learned (EBL rentable)# - SHALLOW : 0+x -> 1 etape KB, 1 etape learned (EBL neutre)# - NON-MATCH : 2+i -> aucune regle apprise ne s'applique (EBL neutre)random.seed(42)n =100corpus = []for i inrange(n): fam = i %3if fam ==0: corpus.append(("profond", f"1*(0+{chr(ord('a')+i%10)})"))elif fam ==1: corpus.append(("shallow", f"0+{chr(ord('a')+i%10)})"))else: corpus.append(("nomatch", f"2+{i}"))# --- Mesure des ETAPES (deterministe) ---def total_steps(ebl, exprs):returnsum(ebl.simplify_with_learned(e)[2] for _, e in exprs)steps_without = total_steps(ebl_without, corpus)steps_with = total_steps(ebl_with, corpus)def steps_by_family(ebl, exprs): fam_steps = {"profond": 0, "shallow": 0, "nomatch": 0}for fam, e in exprs: fam_steps[fam] += ebl.simplify_with_learned(e)[2]return fam_stepsfs_without = steps_by_family(ebl_without, corpus)fs_with = steps_by_family(ebl_with, corpus)print(f"Corpus : {n} expressions (34 profondes, 33 shallow, 33 nomatch)")print()print("=== Metrique principale : NOMBRE D'ETAPES (deterministe) ===")print(f" Sans EBL (KB seule) : {steps_without:4d} etapes")print(f" Avec EBL (regles) : {steps_with:4d} etapes")print(f" Reduction : {steps_without - steps_with:4d} etapes "f"({(steps_without-steps_with)/steps_without*100:.0f}%)")print(f" Speedup (ratio) : {steps_without/steps_with:.2f}x")print()print("=== Decomposition par famille d'expressions ===")print(f" {'Famille':10s} | {'Sans':>6s} | {'Avec':>6s} | {'Gain':>5s}")print(f" {'-'*40}")for fam in ["profond", "shallow", "nomatch"]: g = fs_without[fam] - fs_with[fam]print(f" {fam:10s} | {fs_without[fam]:>6d} | {fs_with[fam]:>6d} | {g:>+5d}")print()# --- Mesure du TEMPS wall-clock (metrique secondaire, moyenne robuste sur N runs) ---def bench_time(ebl, exprs, reps=20):"""Retourne le temps median (robuste aux outliers) sur `reps` repetitions.""" ts = []for _ inrange(reps): t0 = time.perf_counter()for _, e in exprs: ebl.simplify_with_learned(e) ts.append(time.perf_counter() - t0) ts.sort()return ts[len(ts)//2] # medianet_without_med = bench_time(ebl_without, corpus)t_with_med = bench_time(ebl_with, corpus)print("=== Metrique secondaire : temps wall-clock (mediane sur 20 runs) ===")print(f" Sans EBL : {t_without_med*1e6:7.1f} us")print(f" Avec EBL : {t_with_med*1e6:7.1f} us")print(f" Ratio (sans/avec) : {t_without_med/t_with_med:.2f}x")print(f" NB : bruite par la charge CPU ; le classement wall-clock peut DIFFERER")print(f" du classement en etapes -- voir l'interpretation ci-dessous.")print()# --- Reponses aux 3 questions (basees sur la MESURE REELLE) ---print("=== Reponses aux questions (mesure reelle) ===")print(f"Q1 Speedup moyen : {steps_without/steps_with:.2f}x en ETAPES "f"({(steps_without-steps_with)/steps_without*100:.0f}% de reduction). "f"Mais en TEMPS wall-clock, l'EBL est ici plus LENT "f"({t_without_med/t_with_med:.2f}x) -- voir interpretation.")print(f"Q2 Depend du type : OUI. Seules les expressions PROFONDES beneficient")print(f" de l'EBL ({fs_without['profond']-fs_with['profond']} etapes gagnees) ; "f"les SHALLOW et NON-MATCH sont neutres (0 etape gagnee).")print(f"Q3 Point de bascule memoire : l'EBL paie son surcout (stocker une regle)")print(f" des qu'elle evite au moins une chaine de preuve complete. Une regle")print(f" jamais reutilisee (cf Exemple guide 2) est une perte nette : c'est le")print(f" critere operationnel qui justifie le desapprentissage par seuil.")
ebl_without : 0 regle apprise (base KB seule)
ebl_with : 3 regles apprises
Corpus : 100 expressions (34 profondes, 33 shallow, 33 nomatch)
=== Metrique principale : NOMBRE D'ETAPES (deterministe) ===
Sans EBL (KB seule) : 101 etapes
Avec EBL (regles) : 67 etapes
Reduction : 34 etapes (34%)
Speedup (ratio) : 1.51x
=== Decomposition par famille d'expressions ===
Famille | Sans | Avec | Gain
----------------------------------------
profond | 68 | 34 | +34
shallow | 33 | 33 | +0
nomatch | 0 | 0 | +0
=== Metrique secondaire : temps wall-clock (mediane sur 20 runs) ===
Sans EBL : 147.8 us
Avec EBL : 336.3 us
Ratio (sans/avec) : 0.44x
NB : bruite par la charge CPU ; le classement wall-clock peut DIFFERER
du classement en etapes -- voir l'interpretation ci-dessous.
=== Reponses aux questions (mesure reelle) ===
Q1 Speedup moyen : 1.51x en ETAPES (34% de reduction). Mais en TEMPS wall-clock, l'EBL est ici plus LENT (0.44x) -- voir interpretation.
Q2 Depend du type : OUI. Seules les expressions PROFONDES beneficient
de l'EBL (34 etapes gagnees) ; les SHALLOW et NON-MATCH sont neutres (0 etape gagnee).
Q3 Point de bascule memoire : l'EBL paie son surcout (stocker une regle)
des qu'elle evite au moins une chaine de preuve complete. Une regle
jamais reutilisee (cf Exemple guide 2) est une perte nette : c'est le
critere operationnel qui justifie le desapprentissage par seuil.
Interpretation : le speedup de l’EBL est algorithmique, pas (toujours) wall-clock
La mesure revele un résultat contre-intuitif mais fondamental – et reproductible :
Metrique
Sans EBL
Avec EBL
Verdict
Étapes (déterministe)
101
67
EBL gagne : 1.51x, -34%
Temps wall-clock (mediane)
~148 us
~336 us
EBL perd : ~0.44x
(Les chiffres wall-clock précis fluctuent avec la charge CPU ; le classement, lui, est stable : l’EBL est ici plus lente en temps alors qu’elle gagne en étapes.)
Pourquoi cette inversion ? Le speedup EBL est un gain algorithmique en profondeur de preuve : une règle compilee remplace une chaîne de 2 reecritures par un lookup direct. Mais sur ce toy-example – expressions triviales, base de 3 règles – le cout de recherche parmi les règles apprises (le branching factor de la section 6) domine le gain en étapes. C’est l’illustration exacte du utility problem decrit par Minton (1990) : au-dela d’un certain nombre de règles apprises, apprendre plus ralentit le système, parce que le cout de matcher chaque règle supplementaire depasse l’economie realisee sur les quelques-unes qui s’appliquent. Le wall-clock ne redevient favorable qu’a plus grande echelle, sur de vraies chaînes de preuve ou chaque étape economisee coute cher.
Dependence au type d’expression (Q2) : la decomposition par famille tranche net. Seules les expressions profondes (1*(0+x), 2 étapes -> 1) beneficient du gain ; les expressions shallow (0+x, déjà 1 étape) et non-matchantes (2+3) sont strictement neutres (0 étape gagnee). C’est la formalisation du principe de compilation : on ne gagne qu’en court-circuitant une chaîne qui existait déjà.
Point de bascule memoire (Q3) : lie a l’Exemple guide 2 et au utility problem. Une règle apprise paie son surcout de stockage des qu’elle evite au moins une chaîne de preuve complete lors d’une reutilisation. En dessous, elle alourdit inutilement la base – c’est exactement ce que le prune_learned_rules par seuil d’utilisation detecte et elimine. Le compromis étapes-temps ci-dessus est donc le cahier des charges du seuil min_usage : garder une règle seulement si, sur le corpus reel, ses economies en étapes compensent son cout de recherche.
Lien avec SL-1 / Occam : ici encore, l’EBL applique un critere de sélection parmi des connaissances consistantes (toutes les règles apprises sont correctes), exactement comme le rasoir d’Occam selectionnait l’hypothese minimale en SL-1.
Exercice 2 (variation) : RBL robuste face a un attribut bruite
La section 7 a montre que nationalitydetermine seul la langue, tandis que age et city ne la determinent pas (table idx24). Une question naturelle : que se passe-t-il si l’on ajoute un attribut totalement decorrele (un bruit) aux données ? RBL doit rester robuste : la determination minimale nationality -> language ne doit pas changer.
Ajoutez un 5e attribut forecaster (le nom du meteorologue, sans aucun lien avec la langue) a chaque ligne de nationality_data, puis verifiez que la determination minimale est toujours nationality.
Étapes : 1. Construisez nationality_noisy en ajoutant forecaster (valeurs arbitraires : "Alice", "Bob", ...) a chaque ligne via dict(row, forecaster=f). 2. Ajoutez "forecaster" a ALL_ATTRS. 3. Re-testez check_determination sur ["nationality"], ["forecaster"], ["nationality", "forecaster"]. 4. Confirmez : nationality -> language reste True, forecaster -> language est False.
Indice : check_determination groupe les lignes par les valeurs de det_attrs. Un attribut bruite ajoute des groupes sans reduire les violations – la recherche par taille croissante (cf. SL-3 MINIMAL-CONSISTENT-DET) saute donc naturellement les attributs non pertinents. C’est le fondement de la robustesse de RBL au bruit.
# Exercice 2 (variation) : RBL robuste face a un attribut bruite# TODO etudiant : ajoutez un attribut 'forecaster' decorrele et confirmez la robustesse de RBL.# Etape 1 : construire nationality_noisy avec un attribut bruite 'forecaster'# forecasters = ["MeteoA", "MeteoB", "MeteoA", "MeteoB", "MeteoA", "MeteoB",# "MeteoA", "MeteoB", "MeteoA", "MeteoB", "MeteoA", "MeteoB"]# nationality_noisy = [dict(row, forecaster=f) for row, f in zip(nationality_data, forecasters)]# Etape 2 : etendre la liste des attributs candidats# noisy_attrs = ALL_ATTRS + ["forecaster"]# Etape 3 : re-tester les determinations# print(check_determination(nationality_noisy, ["nationality"], "language")) # attendu True# print(check_determination(nationality_noisy, ["forecaster"], "language")) # attendu False# print(check_determination(nationality_noisy, ["nationality", "forecaster"], "language")) # True (redondant)# Indice : un attribut non pertinent ajoute des groupes sans eliminer de violations -> RBL l'ecarte.print("Exercice a completer : robustesse de RBL face a un attribut bruite")print("Question : la determination minimale reste-t-elle nationality -> language ?")
Exercice a completer : robustesse de RBL face a un attribut bruite
Question : la determination minimale reste-t-elle nationality -> language ?
Exercice 3 (variation) : EBL operationalite – faire varier le seuil de desapprentissage
L’Exemple guide 2 a fixe min_usage=2 et observe que la règle 0+w (1 utilisation) est desapprise tandis que 1*(0+z) (8) et 1*v (2) sont conservees. Mais le seuil est un paramètre libre : sa valeur decide quelles règles survivent. Faites-le varier et observez la frontiere d’operationalite – c’est le utility problem de Minton (1990) introduit dans l’interpretation de l’Exemple guide 3.
Reutilisez count_rule_usage et prune_learned_rules de l’Exemple guide 2, et tabulez le nombre de règles conservees pour plusieurs seuils.
Étapes : 1. Reconstruisez une instance ArithmeticEBL fraiche et apprenez-lui les 3 règles (1*(0+x), 1*y, 0+u). 2. Recomptez les utilisations sur le même corpus que l’Exemple guide 2 (12 expressions). 3. Pour min_usage dans {1, 2, 3, 5}, appliquez prune_learned_rules et affichez le nombre de règles conservees + leurs patterns. 4. Observez : a partir de quel seuil la règle la plus reutilisee (1*(0+z)) est-elle elle-même desapprise ? Pourquoi cela illustre-t-il que le seuil traduit un compromis biais-variance ?
Indice : chaque règle a un compte d’utilisation fixe sur un corpus donne. Le seuil ne fait que sélectionner un prefixe du classement par utilisation decroissante. Un seuil trop eleve = base sous-apprise (perte de speedup) ; un seuil trop bas = proliferation (utility problem). Le seuil optimal depend du corpus reel futur, qui est inconnu – d’ou l’eternel compromis.
# Exercice 3 (variation) : faire varier le seuil de desapprentissage (utility problem)# TODO etudiant : tabulez le nombre de regles conservees selon min_usage.# Etape 1 : instance fraiche + 3 regles apprises# ebl = ArithmeticEBL()# ebl.learn("1*(0+x)", {"x": "z"}, verbose=False)# ebl.learn("1*y", {"y": "v"}, verbose=False)# ebl.learn("0+u", {"u": "w"}, verbose=False)# Etape 2 : recompter les utilisations sur le meme corpus (12 expressions) que l'Exemple guide 2# corpus = ["1*(0+a)", "1*(0+b)", "1*(0+c)", "1*(0+d)", "1*(0+e)", "1*(0+f)",# "1*(0+g)", "1*(0+h)", "1*y", "0+w", "1*nomatch", "2+3"]# usage = count_rule_usage(ebl, corpus)# Etape 3 : tabuler pour min_usage dans {1, 2, 3, 5}# for seuil in [1, 2, 3, 5]:# conservees = prune_learned_rules(ebl, usage, min_usage=seuil)# print(f"min_usage={seuil} : {len(conservees)} regles conservees -> "# f"{[r.pattern for r in conservees]}")# Question : a partir de quel seuil la regle 1*(0+z) (la plus reutilisee) est-elle desapprise ?print("Exercice a completer : frontiere d'operationalite selon le seuil de desapprentissage")print("Question : a quel seuil disparait la regle la plus reutilisee ?")
Exercice a completer : frontiere d'operationalite selon le seuil de desapprentissage
Question : a quel seuil disparait la regle la plus reutilisee ?
Resume et perspectives
Ce notebook a etudie deux approches qui exploitent la connaissance prealable du domaine pour ameliorer l’apprentissage : l’EBL (Explanation-Based Learning) et le RBL (Relevance-Based Learning). L’EBL compile une théorie de premiers principes en règles operationnelles a partir d’un seul exemple, en construisant un arbre de preuve puis en variabilisant les constantes. L’implementation sur la simplification arithmetique a montre comment les règles 1*u -> u et 0+u -> u peuvent etre combinees en une règle composee Simplify(1*(0+z), z) qui court-circuite les étapes intermediaires. Le compromis fondamental de l’EBL reside dans l’operationalite : les règles trop spécifiques ne sont jamais reutilisees, tandis que les règles trop générales n’apportent aucun gain.
Le RBL, quant a lui, identifie les attributs pertinents via les determinations : si un sous-ensemble de d attributs determine la cible parmi n attributs, l’espace des hypotheses se reduit de O(2^n) a O(2^d) — une reduction exponentielle quand d << n. Sur le domaine nationalite-langue, la determination nationality seule suffit a predire la langue, rendant les autres attributs (age, ville) inutiles a l’apprentissage. La formalisation complete du cadre — treillis des determinations, algorithme MINIMAL-CONSISTENT-DET, cas d’echec sur les concepts disjonctifs et comparaison avec la sélection statistique d’attributs — est l’objet du notebook suivant.
Ces deux approches sont deductives et ne créent pas de connaissance factuellement nouvelle. Le notebook suivant, SL-3 - Apprentissage Base sur la Pertinence, approfondit le RBL ; le passage a l’apprentissage inductif base sur la connaissance (KBIL), qui combine connaissances de fond et induction pour decouvrir de veritables nouvelles hypotheses, sera explore a partir de SL-4 - Programmation Logique Inductive.
Defi presentation
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 (EBL, differentiation symbolique)
Exhibez un cas ou la variabilisation trop agressive de l’arbre de preuve produit une règle compilee fausse (appliquee hors de ses conditions de validite). Quelle partie de la preuve aurait du rester constante ?
Ex. 2 (filtrage des règles)
L’utilite d’une règle depend de la distribution des problemes futurs : construisez deux distributions de requêtes qui inversent le classement de vos règles (une règle filtree devient la plus rentable).
Ex. 3 (speedup EBL)
Augmentez le nombre de règles apprises jusqu’a observer le point ou apprendre plus ralentit le système (utility problem, Minton 1990). Pourquoi ce point existe-t-il quelle que soit l’implementation ?
Ressources
Russell & Norvig, AI: A Modern Approach, 3e ed., Chapitres 19.3-19.4
G. DeJong & R. Mooney, Explanation-Based Learning: An Alternative View (1986)