Argumentation basée sur les valeurs (VAF, Bench-Capon 2003)

← Dung_AF_Semantics | ↑ Argument_Analysis | Ranking_Semantics →

Pourquoi ce notebook

Le notebook Argumentation-03-Dung-AF-Semantics-Python reconstruit l’argumentation abstraite de Dung (1995) : des arguments s’attaquent, des sémantiques (grounded / preferred / stable) décident lesquels survivent. Mais dans ce cadre, toutes les attaques réussissent — un argument qui en attaque un autre le défait, un point c’est tout.

La réalité du débat est différente. Considérez : « Il faut réduire les impôts pour relancer la consommation » (argument A, valeur croissance) attaque « Il faut maintenir les impôts pour financer les services publics » (argument B, valeur solidarité). L’attaque de A sur B doit-elle réussir ? Cela dépend de ce que valorise l’auditoire : un auditoire qui préfère la croissance à la solidarité laissera A défaire B ; un auditoire inverse laissera B résister à A. Le même graphe d’attaques produit des conclusions différentes selon l’audience.

C’est le saut conceptuel de Trevor Bench-Capon dans « A Value-Based Argumentation Framework » (2003) : enrichir le cadre de Dung par des valeurs (ce que chaque argument promeut) et une audience (un ordre de préférence sur ces valeurs). Une attaque ne défait sa cible que si la valeur de l’attaquant est au moins aussi préférée que celle de l’attaqué, aux yeux de l’audience.

Ce notebook reconstruit le cadre VAF de zéro en pur Python (stdlib), à la suite de Dung_AF_Semantics dont il reprend les primitives. Objectif pédagogique : voir concrètement comment trois audiences différentes sur un même graphe d’attaques en cycle produisent trois conclusions différentes — et pourquoi ce phénomène connecte l’argumentation au choix social (préférences agrégées).

Le cadre VAF — définition

Un Value-based Argumentation Framework (VAF) est un quintuplet \(\langle \mathcal{A}, \mathcal{R}, V, \eta, \succcurlyeq_a \rangle\) où :

  • \(\mathcal{A}\) est un ensemble d’arguments (comme chez Dung) ;
  • \(\mathcal{R} \subseteq \mathcal{A} \times \mathcal{A}\) est la relation d’attaque (comme chez Dung) ;
  • \(V\) est un ensemble de valeurs ;
  • \(\eta : \mathcal{A} \to V\) associe à chaque argument la valeur qu’il promeut ;
  • \(\succcurlyeq_a\) est l’audience : un ordre total (préférence) sur \(V\).

Défaite dépendante de l’audience. L’argument \(x\) défait \(y\) (l’attaque \(x \to y\) réussit) si et seulement si :

\[\eta(x) \succcurlyeq_a \eta(y)\]

Sinon, \(y\) résiste à \(x\) : l’attaque échoue, \(y\) n’est pas défait par \(x\).

Une fois cette relation de défaite audience-dépendante fixée, on réapplique les sémantiques de Dung (grounded, preferred, stable) dessus. Le résultat dépend de l’audience : changer l’ordre de préférence sur \(V\) change qui défait qui, donc change les extensions.

# Primitives de Dung (auto-contenues, comme Dung_AF_Semantics).
from itertools import chain, combinations


class AF:
    """Cadre d'argumentation abstrait : arguments + relation d'attaque.

    Ici `attacks` = relation de DEFAITE deja resolue (cf. af_from_audience
    plus bas). On construit un AF a partir d'un VAF + une audience, puis on
    lui applique les semantiques de Dung.
    """

    def __init__(self, args, attacks):
        self.args = set(args)
        self.attacks = set(attacks)  # couples (attaquant, attaque)

    def attackers(self, x):
        return {a for (a, b) in self.attacks if b == x}

    def __repr__(self):
        att = ", ".join(f"{a}->{b}" for (a, b) in sorted(self.attacks))
        return f"AF(args={sorted(self.args)}, attacks=[{att}])"


def defeats(af, S, x):
    """True si S defait x (au moins un membre de S attaque x)."""
    return any((a, x) in af.attacks for a in S)


def defends(af, S, x):
    """True si S defend x : tout attaquant de x est defait par S."""
    return all(defeats(af, S, b) for b in af.attackers(x))


def powerset(iterable):
    s = list(iterable)
    return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1))


def grounded(af):
    """Extension grounded : plus petit point fixe de la fonction de defense."""
    E = set()
    while True:
        E_new = E | {x for x in af.args if defends(af, E, x)}
        if E_new == E:
            return E
        E = E_new


print("Primitives de Dung chargees. Smoke test:")
af_test = AF({"a", "b"}, {("a", "b")})
print(f"  af_test = {af_test}")
print(f"  grounded(af_test) = {grounded(af_test)}  # a defendu, b defait par a")
Primitives de Dung chargees. Smoke test:
  af_test = AF(args=['a', 'b'], attacks=[a->b])
  grounded(af_test) = {'a'}  # a defendu, b defait par a
# --- Le cadre VAF (Bench-Capon 2003) ---


class VAF:
    """Value-based Argumentation Framework.

    Attributs :
        args     : ensemble d'arguments
        attacks  : relation d'attaque (couples), COMME chez Dung
        values   : mapping arg -> valeur (eta)
        value_set: l'ensemble des valeurs V
    """

    def __init__(self, args, attacks, values):
        self.args = set(args)
        self.attacks = set(attacks)
        self.values = dict(values)            # arg -> valeur
        self.value_set = set(self.values.values())

    def value_of(self, x):
        return self.values[x]

    def __repr__(self):
        v = ", ".join(f"{a}:{val}" for a, val in sorted(self.values.items()))
        att = ", ".join(f"{a}->{b}" for (a, b) in sorted(self.attacks))
        return f"VAF(args={sorted(self.args)}, attacks=[{att}], values={{{v}}})"


def audience_rank(audience, v):
    """Rang d'une valeur v dans l'audience (0 = la plus preferee).

    `audience` = liste ordonnee des valeurs, de la plus preferee a la moins preferee.
    Plus le rang est PETIT, plus la valeur est preferee.
    """
    return audience.index(v)


def defeats_under_audience(vaf, audience, x, y):
    """True si l'attaque x->y reussit sous cette audience.

    L'attaque reussit ssi value(x) est au moins aussi preferee que value(y).
    On compare les RANGS : rang plus petit = plus prefere.
    """
    if (x, y) not in vaf.attacks:
        return False
    return audience_rank(audience, vaf.value_of(x)) <= audience_rank(audience, vaf.value_of(y))


def af_from_audience(vaf, audience):
    """Construit le AF (Dung) correspondant a un VAF sous une audience donnee.

    On ne garde que les attaques qui REUSSISSENT sous l'audience. Les attaques
    qui echouent (valeur de l'attaquant moins preferee) disparaissent : la
    cible resiste.
    """
    surviving = {(x, y) for (x, y) in vaf.attacks
                 if defeats_under_audience(vaf, audience, x, y)}
    return AF(vaf.args, surviving)


# Smoke test : un VAF minimal a 2 arguments, 1 valeur commune.
vaf_test = VAF({"a", "b"}, {("a", "b")}, {"a": "v", "b": "v"})
aud_test = ["v"]
af_t = af_from_audience(vaf_test, aud_test)
print(f"VAF test: {vaf_test}")
print(f"  audience {aud_test} -> AF survived attacks: {af_t.attacks}")
print(f"  grounded = {grounded(af_t)}  # valeurs egales -> attaque reussit -> a defait b")
VAF test: VAF(args=['a', 'b'], attacks=[a->b], values={a:v, b:v})
  audience ['v'] -> AF survived attacks: {('a', 'b')}
  grounded = {'a'}  # valeurs egales -> attaque reussit -> a defait b

Exemple canonique : le cycle à trois valeurs

Voici le moment pédagogique clé. Prenons trois arguments en cycle (\(a \to b \to c \to a\)), chacun promouvant une valeur distincte :

Argument Valeur promue
\(a\) verite
\(b\) confiance
\(c\) solidarite

Chez Dung (sans valeurs), un cycle de longueur impaire n’a pas d’extension stable et son extension grounded est vide (personne n’est défendable : chacun est attaqué par un argument non défait). Mais avec des valeurs, l’audience décide quelles attaques réussissent, ce qui brise la symétrie du cycle.

Construisons ce VAF et calculons ses extensions sous trois audiences distinctes.

# L'exemple canonique : cycle a->b->c->a, 3 valeurs distinctes.
args = {"a", "b", "c"}
attacks = {("a", "b"), ("b", "c"), ("c", "a")}
values = {"a": "verite", "b": "confiance", "c": "solidarite"}

vaf = VAF(args, attacks, values)
print("VAF canonique:")
print(f"  {vaf}")
print(f"  Valeurs: {sorted(vaf.value_set)}")
print()

# Trois audiences = trois ordres totaux differents, chacun preferant une
# valeur differente en tete.
audiences = {
    "audience-1 (verite > confiance > solidarite)": ["verite", "confiance", "solidarite"],
    "audience-2 (confiance > solidarite > verite)": ["confiance", "solidarite", "verite"],
    "audience-3 (solidarite > verite > confiance)": ["solidarite", "verite", "confiance"],
}

print("Extensions grounded sous chaque audience :")
print("-" * 60)
for label, aud in audiences.items():
    af_aud = af_from_audience(vaf, aud)
    g = grounded(af_aud)
    survived = sorted(f"{x}->{y}" for (x, y) in af_aud.attacks)
    excluded = sorted(vaf.args - g)
    print(f"{label}")
    print(f"    attaques reussissant: {survived}")
    print(f"    grounded = {sorted(g)}  (exclu: {excluded})")
    print()
VAF canonique:
  VAF(args=['a', 'b', 'c'], attacks=[a->b, b->c, c->a], values={a:verite, b:confiance, c:solidarite})
  Valeurs: ['confiance', 'solidarite', 'verite']

Extensions grounded sous chaque audience :
------------------------------------------------------------
audience-1 (verite > confiance > solidarite)
    attaques reussissant: ['a->b', 'b->c']
    grounded = ['a', 'c']  (exclu: ['b'])

audience-2 (confiance > solidarite > verite)
    attaques reussissant: ['b->c', 'c->a']
    grounded = ['a', 'b']  (exclu: ['c'])

audience-3 (solidarite > verite > confiance)
    attaques reussissant: ['a->b', 'c->a']
    grounded = ['b', 'c']  (exclu: ['a'])

Ce que la divergence nous apprend

Les trois audiences produisent trois extensions grounded différentes sur le même graphe d’attaques :

Audience (valeur en tête) Attaques qui réussissent Extension grounded
verite en tête \(a \to b\), \(b \to c\) (mais \(c \not\to a\)) {a, c} (b exclu)
confiance en tête \(b \to c\), \(c \to a\) (mais \(a \not\to b\)) {a, b} (c exclu)
solidarite en tête \(c \to a\), \(a \to b\) (mais \(b \not\to c\)) {b, c} (a exclu)

Lecture : sur ce cycle, le grounded contient toujours deux arguments sur trois — mais l’argument exclu change avec l’audience (\(b\), puis \(c\), puis \(a\)). Le mécanisme : l’argument dont la valeur est la plus préférée voit l’attaque dirigée contre lui échouer (son attaquant est moins préféré), donc il survit ; et comme sa propre attaque vers le suivant réussit, il élimine ce suivant. Le troisième argument survit alors par défense (le survivant principal défait l’attaquant du troisième). En résumé : mettre une valeur en tête d’audience condamne l’argument que cette valeur attaque dans le cycle.

C’est le résultat central de Bench-Capon : la conclusion d’un débat n’est pas déterminée par le graphe d’arguments seul, mais par le graphe ET les valeurs de l’audience. Trois auditoires de valeurs différentes, confrontés aux mêmes arguments, excluent légitimement des arguments différents. Cela relie l’argumentation à la théorie du choix social : si plusieurs individus ont des audiences différentes, agréger leurs préférences (cf. Tweety-9, GameTheory SocialChoice) devient la question centrale pour décider collectivement.

Sweep d’audiences : combien de conclusions distinctes ?

Sur un VAF à \(|V|\) valeurs, il y a \(|V|!\) audiences possibles (tous les ordres totaux). Une question naturelle : parmi toutes ces audiences, combien d’extensions grounded distinctes obtient-on ? Si la réponse est 1, les valeurs sont décoratives (elles ne changent rien). Si la réponse est grande, le débat est sensible à l’audience — ce qui est précisément la situation où l’argumentation basée sur les valeurs apporte quelque chose que Dung pur ne peut pas exprimer.

Énumérons toutes les audiences sur notre VAF à 3 valeurs et comptons les outcomes.

from itertools import permutations

# Sweep : toutes les audiences (ordres totaux sur les valeurs).
print("Sweep complet des audiences sur le VAF canonique (3! = 6 ordres):")
print("-" * 65)
distinct_outcomes = {}
for aud in permutations(sorted(vaf.value_set)):
    af_aud = af_from_audience(vaf, list(aud))
    g = grounded(af_aud)
    key = tuple(sorted(g)) if g else "()"
    distinct_outcomes.setdefault(key, []).append(list(aud))
    g_str = set(g) if g else "{}"
    print(f"  audience {list(aud)} -> grounded {g_str}")

print("-" * 65)
print(f"Distinct grounded outcomes: {len(distinct_outcomes)}")
for outcome, auds in distinct_outcomes.items():
    print(f"  {outcome} atteint par {len(auds)} audience(s)")
Sweep complet des audiences sur le VAF canonique (3! = 6 ordres):
-----------------------------------------------------------------
  audience ['confiance', 'solidarite', 'verite'] -> grounded {'b', 'a'}
  audience ['confiance', 'verite', 'solidarite'] -> grounded {'b', 'a'}
  audience ['solidarite', 'confiance', 'verite'] -> grounded {'b', 'c'}
  audience ['solidarite', 'verite', 'confiance'] -> grounded {'b', 'c'}
  audience ['verite', 'confiance', 'solidarite'] -> grounded {'a', 'c'}
  audience ['verite', 'solidarite', 'confiance'] -> grounded {'a', 'c'}
-----------------------------------------------------------------
Distinct grounded outcomes: 3
  ('a', 'b') atteint par 2 audience(s)
  ('b', 'c') atteint par 2 audience(s)
  ('a', 'c') atteint par 2 audience(s)

Résultat : sur 6 audiences possibles, on obtient 3 conclusions distinctes — {a, b}, {a, c}, {b, c} — chacune atteinte par 2 audiences. (Les deux audiences qui mettent la même valeur en tête convergent vers la même extension : seul compte quel argument est « en tête ».) Le débat est donc fortement sensible à l’audience : il n’y a pas de verdict universel, la conclusion dépend de qui écoute.

C’est un diagnostic actionnable : si un médiateur veut faire converger un débat argumentatif bloqué en cycle, il doit d’abord révéler les valeurs des parties et chercher un compromis sur l’ordre de préférence — pas seulement sur les arguments eux-mêmes.

Exercices

Les trois exercices suivants approfondissent le cadre. Chaque stub est à compléter — le notebook s’exécute de bout en bout même non complété (les exercices renvoient None et affichent un message).

Exercice 1 — Choisir une audience pour rejeter un argument donné

On a vu que chaque audience exclut un argument différent du grounded. Objectif : écrire audience_qui_rejette(vaf, cible) qui renvoie une audience rendant cible absent du grounded (rejeté).

Indice : d’après la lecture ci-dessus, l’argument exclu est celui attaqué par l’argument dont la valeur est la plus préférée. Donc, pour rejeter cible, il faut mettre en tête d’audience la valeur de son attaquant dans le graphe.

# Exercice 1 : a completer
def audience_qui_rejette(vaf, cible):
    """Renvoie une audience (liste ordonnee de valeurs) telle que `cible`
    soit ABSENT de l'extension grounded (rejete).

    Indice : identifier l'attaquant de `cible` dans le graphe d'attaque,
    puis mettre la valeur de cet attaquant en tete d'audience.
    Etape 1 : trouver x tel que (x, cible) in vaf.attacks.
    Etape 2 : construire une audience avec value(x) en tete, puis le reste.
    """
    # TODO etudiant
    return None


# Verification (decommenter apres completion) :
# aud = audience_qui_rejette(vaf, "b")
# if aud is not None:
#     af_aud = af_from_audience(vaf, aud)
#     g = grounded(af_aud)
#     print(f"Audience trouvee: {aud} -> grounded = {sorted(g)}")
#     assert "b" not in g, f"Echec: b devrait etre exclu, grounded = {g}"
#     print("SUCCES : b est rejete sous cette audience.")
# else:
#     print("Exercice a completer.")
print("Exercice 1 a completer.")
Exercice 1 a completer.

Exercice 2 — Quand le VAF dégénère en Dung pur

Sur le VAF canonique, les valeurs sont distinctes et l’audience brise le cycle. Mais que se passe-t-il si tous les arguments partagent la même valeur ? Alors toute attaque a un attaquant et une cible de valeur égale → toutes les attaques réussissent, et l’on retrouve exactement le cycle de Dung pur : le grounded est vide.

Objectif : construire un VAF (cycle \(a \to b \to c \to a\)) où tous les arguments ont la même valeur, choisir une audience triviale, et vérifier que le grounded est bien vide — confirmant que le VAF contient Dung comme cas particulier (valeurs dégénérées).

# Exercice 2 : a completer
def vaf_degenere_en_dung():
    """Construisez un VAF (cycle a->b->c->a) ou tous les arguments partagent
    la MEME valeur, et renvoyez (vaf, audience) tels que TOUTES les attaques
    reussissent. Renvoie (None, None) tant que non complete.

    Indice : values = {"a":"v","b":"v","c":"v"} ; audience = ["v"].
    Verifiez ensuite que grounded est vide (comme un cycle de Dung impair).
    """
    # TODO etudiant
    return None, None


# vaf2, aud2 = vaf_degenere_en_dung()
# if vaf2 is not None:
#     af2 = af_from_audience(vaf2, aud2)
#     g2 = grounded(af2)
#     print(f"Attaques reussissant: {sorted(af2.attacks)}")
#     print(f"Grounded: {sorted(g2) if g2 else '{} (vide)'}")
#     assert len(g2) == 0, "Le grounded devrait etre vide (cycle impair de Dung)"
#     print("SUCCES : VAF degenere en Dung, grounded vide.")
# else:
#     print("Exercice a completer.")
print("Exercice 2 a completer.")
Exercice 2 a completer.

Exercice 3 — Cartographier toutes les conclusions d’un VAF plus large

Généralisez le sweep : écrivez toutes_les_conclusions(vaf) qui renvoie un dictionnaire {extension_grounded: [liste des audiences qui la produisent]} pour toutes les audiences possibles.

Objectif : appliquer à un VAF à 4 arguments et 3 valeurs (certains arguments partageant une valeur), et interpréter pourquoi le nombre de conclusions distinctes est (généralement) inférieur au nombre d’audiences.

# Exercice 3 : a completer
def toutes_les_conclusions(vaf):
    """Renvoie {tuple(sorted(grounded)): [audiences productrices]} pour toutes
    les audiences possibles (tous les ordres totaux sur value_set).

    Indice : itertools.permutations sur value_set ; pour chacune, calculer le
    grounded via af_from_audience + grounded. Grouper par outcome. Une audience
    vide () represente un grounded vide.
    """
    # TODO etudiant
    return {}


# Test sur le VAF canonique a 3 valeurs :
# conclu = toutes_les_conclusions(vaf)
# print(f"Le VAF canonique a {len(conclu)} conclusion(s) distincte(s):")
# for outcome, auds in conclu.items():
#     lab = sorted(outcome) if outcome else "{} (vide)"
#     print(f"  {lab} <- {len(auds)} audience(s)")
# print()
# # VAF plus large : 4 arguments, 3 valeurs (a et d partagent 'verite')
# vaf_large = VAF({"a","b","c","d"}, {("a","b"),("b","c"),("c","d"),("d","a")},
#                 {"a":"verite","b":"confiance","c":"solidarite","d":"verite"})
# conclu_large = toutes_les_conclusions(vaf_large)
# print(f"Le VAF large (4 args, 3 valeurs) a {len(conclu_large)} conclusion(s) distincte(s).")
print("Exercice 3 a completer.")
Exercice 3 a completer.

Conclusion

Ce que vous avez appris

Le cadre de Dung (1995) dit qui attaque qui. Le cadre de Bench-Capon (2003) ajoute une couche critique : qui défait qui dépend de ce que valorise l’audience. Concrètement :

  • Une attaque \(x \to y\) n’est pas automatiquement une défaite : elle ne réussit que si la valeur promue par \(x\) est au moins aussi préférée (pour l’audience) que celle promue par \(y\).
  • Changer l’audience change la relation de défaite, donc change les extensions (grounded / preferred / stable). Sur le cycle canonique, trois audiences excluent trois arguments différents — la signature d’un débat sensible à l’audience.
  • Cas limite : si tous les arguments partagent une même valeur, le VAF dégénère en cadre de Dung pur (toutes les attaques réussissent). Le cadre de Dung est donc un cas particulier du VAF.

Prochaines étapes

  • Agrégation d’audiences → choix social : si chaque individu a sa propre audience, comment décider collectivement ? C’est le pont vers Tweety-9 (préférences) et GameTheory / SocialChoice (théorèmes d’impossibilité d’Arrow, votations). La question « existe-t-il une agrégation d’audiences qui préserve la rationalité ? » est un analogue direct du paradoxe de Condorcet.
  • Argumentation graduée : plutôt qu’un ordre total sur les valeurs, Argumentation-03c-Ranking-Semantics-Python attribue une force numérique à chaque argument — une autre façon de départager ce que Dung laisse indécidé.
  • Bipolarité : les VAF gèrent les préférences, mais restent attack-only. Les cadres bipolaires (Cayrol & Lagasquie-Schiex, 2005) ajoutent une relation de support — un prolongement naturel au-delà de ce notebook.

Référence

  • Trevor J. M. Bench-Capon, « A Value-Based Argumentation Framework », dans Proceedings of the European Conference on Logics in Artificial Intelligence (JELIA), 2003 — le papier fondateur du cadre VAF reconstruit ici.

Notebook pédagogique — pur stdlib Python, déterministe, sans LLM ni solveur externe. Conforme C.1 (stubs sans erreur volontaire), C.2 (sorties réelles), exercices à compléter.

Retour au sommet