SL-2 — Apprentissage et Connaissance (EBL & RBL)

Navigation : Index | << SL-1

Objectifs d’apprentissage

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)
# Imports et configuration
from typing import Optional
from dataclasses import dataclass, field
from collections import defaultdict
import itertools
import copy
import time

# Metadata du notebook
NOTEBOOK_INFO = {
    "title": "SL-2 - Apprentissage et Connaissance (EBL & RBL)",
    "series": "SymbolicLearning",
    "aima_chapters": ["19.3", "19.4"],
    "date": "2026-05",
}

print(f"Notebook : {NOTEBOOK_INFO['title']}")
print(f"Chapitres AIMA : {', '.join(NOTEBOOK_INFO['aima_chapters'])}")
Notebook : SL-2 - Apprentissage et Connaissance (EBL & RBL)
Chapitres AIMA : 19.3, 19.4

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 Induction guidee Oui

L’equation générale du KBIL est :

\[ \text{Background} \wedge H \wedge \text{Descriptions} \models \text{Classifications} \quad \text{(19.5)} \]

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 product

ATOMS = ["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))
        if all(f(model) for f in kb) and not query(model):
            return False
    return True


# Connaissance de fond : pointu => perce, perce => tue
background = [
    lambda m: (not m["pointu"]) or m["perce"],
    lambda m: (not m["perce"]) or m["tue"],
]
# Observation : Descriptions = pointu ; Classifications = tue
description = lambda m: m["pointu"]
classification = lambda m: m["tue"]
# Hypothese candidate H : pointu => tue
hypothesis = 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)

  1. Construire une explication (arbre de preuve) montrant que le predicat cible s’applique a l’exemple
  2. Variabiliser — remplacer les constantes de l’exemple par des variables
  3. Extraire la règle a partir des feuilles de l’arbre de preuve generalise
  4. 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] or len(pattern[1]) != len(fact[1]):
        return None
    b = dict(bindings)
    for p, f in zip(pattern[1], fact[1]):
        if is_var(p):
            if p in b and b[p] != f:
                return None
            b[p] = f
        elif p != f:
            return None
    return b


def forward_chain(rules, facts):
    """Chainage avant jusqu'au point fixe ; retourne (faits, trace)."""
    facts = set(facts)
    trace = []
    changed = True
    while 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 not in facts:
                        facts.add(new)
                        trace.append((body, bindings, new))
                        changed = True
                    continue
                for f in snapshot:
                    b2 = unify_atom(body[k], f, bindings)
                    if b2 is not None:
                        stack.append((b2, k + 1))
    return facts, trace


def fmt(atom) -> str:
    return f"{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

@dataclass
class RewriteRule:
    """Regle de reecriture : pattern -> resultat."""
    name: str
    pattern: str   # Pattern a chercher
    result: str    # Resultat de la reecriture
    
    def apply(self, expr: str) -> Optional[str]:
        """Tente d'appliquer la regle a l'expression."""
        if self.pattern in expr:
            return expr.replace(self.pattern, self.result, 1)
        return None


# 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."""
    return len(expr.strip()) > 0 and not any(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 reecriture
    for rule in REWRITE_RULES:
        result = rule.apply(expr)
        if result is not None:
            return result, rule.name, f"Rewrite({expr}, {result})"
    
    # Si c'est deja un primitif
    if is_primitive(expr):
        return expr, "primitive", f"Primitive({expr}) => Simplify({expr}, {expr})"
    
    return expr, "bloque", "Aucune regle applicable"


# Test sur une expression simple
print("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"

@dataclass
class ProofStep:
    """Une etape dans l'arbre de preuve EBL."""
    step: int
    input_expr: str
    output_expr: str
    rule_name: str
    justification: str


def build_proof(expr: str, verbose: bool = True) -> list[ProofStep]:
    """Construit l'arbre de preuve pour la simplification complete."""
    proof = []
    current = expr
    step = 0
    
    while True:
        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-boucle
        if step > 20:
            break
    
    return 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

  1. 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)
  2. Remplacer les constantes exemplaires par des variables fraiches
  3. Les conditions qui sont toujours vraies (tautologies) sont supprimees
  4. La règle extraite est la generalisation la plus large possible qui preserve la structure de la preuve
# Implementation de la variabilisation

def 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.justification
        
        for 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 generalized


def 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)
    return f"{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

@dataclass
class 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 originale


class 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."""
        return len(expr.strip()) > 0 and not any(op in expr for op in ['+', '-', '*', '/'])
    
    def _simplify_one_step(self, expr: str) -> tuple[str, str]:
        """Une seule etape de simplification."""
        for rule in self.kb_rules:
            result = rule.apply(expr)
            if result is not None and result != expr:
                return result, rule.name
        if self._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 = 0
        
        while True:
            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 = result
            if step_num > 20:
                break
        return proof
    
    def 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.justification
            for 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 generalized
    
    def 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)
        if not proof:
            if verbose:
                print(f"  Aucune preuve trouvee pour {example}")
            return None  # 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)
        
        # Stocker
        self.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 rule
    
    def _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 is None:
            return None
        result = rule.result
        for 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 in self.learned:
            matched = self._match_learned(rule, expr)
            if matched is not None:
                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 complete
ebl = 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 exemple
print("Apprentissage 2 :")
rule2 = ebl.learn("1*y", {"y": "v"})
print()

# Apprendre encore un autre
print("Apprentissage 3 :")
rule3 = ebl.learn("0+u", {"u": "w"})
print()

print(f"Regles apprises : {len(ebl.learned)}")
for i, r in enumerate(ebl.learned):
    print(f"  {i+1}. {r.precondition} => Simplify({r.pattern}, {r.result})")
Pipeline EBL complet
==================================================

Apprentissage 1 :
  Exemple : 1*(0+x)
  Preuve  : 2 etapes
  Regle   : ArithmeticUnknown(z) => Simplify(1*(0+z), (z))

Apprentissage 2 :
  Exemple : 1*y
  Preuve  : 1 etapes
  Regle   : ArithmeticUnknown(v) => Simplify(1*v, v)

Apprentissage 3 :
  Exemple : 0+u
  Preuve  : 1 etapes
  Regle   : ArithmeticUnknown(w) => Simplify(0+w, w)

Regles apprises : 3
  1. ArithmeticUnknown(z) => Simplify(1*(0+z), (z))
  2. ArithmeticUnknown(v) => Simplify(1*v, v)
  3. ArithmeticUnknown(w) => Simplify(0+w, w)

Interpretation : Implementation EBL

Composant Rôle Complexite
explain() Construit l’arbre de preuve lineaire 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 apprises
ebl_fresh = ArithmeticEBL()  # Pas de learn()

# Systeme AVEC regles apprises
ebl_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 = 0
total_steps_smart = 0

for 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 > 0 else "="
    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 > 0 else float('inf')
    print(f"Speedup moyen : {speedup:.1f}x")
    reduction = (1 - total_steps_smart / total_steps_fresh) * 100 if total_steps_fresh > 0 else 0
    print(f"Reduction d'etapes : {reduction:.0f}%")
Comparaison de performance : avec vs sans regles EBL
============================================================

Expression   |             Sans EBL |             Avec EBL | Gain
----------------------------------------------------------------------
1*(0+a)      |              kb (2e) | learned(1*(0+z)->(z)) (1e) | -1
1*(0+b)      |              kb (2e) | learned(1*(0+z)->(z)) (1e) | -1
1*(0+c)      |              kb (2e) | learned(1*(0+z)->(z)) (1e) | -1
1*(0+d)      |              kb (2e) | learned(1*(0+z)->(z)) (1e) | -1
1*(0+e)      |              kb (2e) | learned(1*(0+z)->(z)) (1e) | -1
1*f          |              kb (1e) | learned(1*v->v) (1e) | =
0+g          |              kb (1e) | learned(0+w->w) (1e) | =
1*(0+h)      |              kb (2e) | learned(1*(0+z)->(z)) (1e) | -1
1*(0+i)      |              kb (2e) | learned(1*(0+z)->(z)) (1e) | -1
1*(0+j)      |              kb (2e) | learned(1*(0+z)->(z)) (1e) | -1
----------------------------------------------------------------------
TOTAL        |                   18 |                   10 | -8

Speedup moyen : 1.8x
Reduction d'etapes : 44%

Interpretation : Efficacite EBL

Metrique Sans EBL Avec EBL Amelioration
Étapes totales Variable Reduit Court-circuit de la chaîne de preuve
Lookup Recherche séquentielle Matching direct O(n) -> O(1)
Règles stockees 5 (KB) 5 + k (apprises) Memoire croissante

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 :

\[ \text{Nationalite}(x, n) > \text{Langue}(x, l) \]

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 determination

def 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])
    
    return all(len(values) == 1 for values in groups.values())


# Exemple : nationalite determine la langue
nationality_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

  1. EBL compile les théories de premiers principes en heuristiques operationnelles. Il n’apprend rien de nouveau factuellement, mais rend le raisonnement plus efficace.

  2. RBL utilise les determinations pour reduire exponentiellement l’espace des hypotheses. Il suffit de connaitre les attributs pertinents pour accelerer considerablement l’apprentissage.

  3. 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.

  4. 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 = 0
    for i in range(len(expr) - 1, -1, -1):
        c = expr[i]
        if c == ")":
            profondeur += 1
        elif c == "(":
            profondeur -= 1
        elif profondeur == 0 and c == op:
            return i
    return -1


def 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) * du
    if expr.startswith("sin(") and expr.endswith(")"):
        inner = expr[4:-1]
        return "cos(" + inner + ")*" + symbolic_diff(inner, var)

    # d/dx(x) = 1
    if 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 rule d/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, variabilisez x -> 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 defaultdict


def 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] += 1
    return dict(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.learned
            if 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 -> v
ebl.learn("0+u", {"u": "w"}, verbose=False)        # regle : 0+w -> w
print(f"Regles apprises initialement : {len(ebl.learned)}")
for i, rule in enumerate(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 = 2
avant = len(ebl.learned)
ebl.learned = prune_learned_rules(ebl, usage, min_usage=seuil)   # desapprentissage effectif
apres = 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' not in 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 time
import 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 -> v
ebl_with.learn("0+u", {"u": "w"}, verbose=False)        # compile 0+w -> w
print(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 = 100
corpus = []
for i in range(n):
    fam = i % 3
    if 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):
    return sum(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_steps

fs_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 _ in range(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]   # mediane

t_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 nationality determine 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)
  • T. Mitchell, Machine Learning, Chapitre 11
  • AIMA Python Code - Implementations de reference

Retour : Index SymbolicLearning | << SL-1 | SL-3 >>

Retour au sommet