Skip to content
Guides6 min read

Au-delà du recouvrement total : une couverture adaptative des annotateurs pour les grands jeux de données

Tout double-annoter coûte cher ; tout annoter une seule fois vous laisse aveugle. Potato 2.6 vous permet d'affecter un annotateur à la plupart des items et trois à un échantillon stratifié, avec des renforcements adaptatifs et un routage automatique vers l'arbitrage.

Potato Team

Tout projet d'annotation d'une certaine ampleur connaît une tension classique. Si chaque item reçoit deux ou trois annotateurs, vous pouvez mesurer l'accord et faire confiance à vos étiquettes, mais vous venez de multiplier votre budget par deux ou par trois. Si chaque item ne reçoit qu'un seul annotateur, vous annotez trois fois plus de données pour le même prix et vous n'avez aucune idée de la fiabilité de quoi que ce soit.

Le compromis habituel est bien connu de quiconque a mené une étude : annoter une seule fois la majeure partie du corpus et double- ou triple-annoter un petit échantillon pour garder un œil sur la qualité. La difficulté a toujours été d'amener l'outillage à faire cela proprement, puis de faire quelque chose du recouvrement une fois qu'on l'a. Potato 2.6 intègre cette conception en natif, à travers deux blocs de configuration (num_annotators_per_item et per_annotator_quota) auxquels s'ajoutent les renforcements adaptatifs et le routage vers l'arbitrage.

Cet article parcourt la conception de la couverture, du cas simple au cas adaptatif. La documentation sur la couverture hétérogène en donne la référence complète.

Couverture individuelle par défaut, un échantillon de recouvrement stratifié à trois annotateurs, des renforcements adaptatifs en cas de désaccord et un routage vers l'arbitrageCouverture adaptative des annotateurs dans Potato

Plafonds par item avec un échantillon de recouvrement

num_annotators_per_item accepte un entier unique pour un plafond uniforme, ou un mapping structuré lorsque vous voulez couvrir différemment des items différents. La forme courante est une valeur par défaut de un, avec un échantillon stratifié porté à trois :

yaml
num_annotators_per_item:
  default: 1
  overlap_sample:
    fraction: 0.1
    count: 3
    stratify_by: domain
    seed: 42
  min: 1

Le bloc overlap_sample relève le plafond sur un sous-ensemble déterministe d'items. L'échantillonnage a lieu une seule fois au démarrage, et les items choisis sont marqués en interne de sorte que la logique d'affectation les traite ensuite comme à forte couverture. Les champs sont simples : fraction est la proportion échantillonnée, count est le plafond relevé (il doit dépasser la valeur par défaut) et seed rend le choix reproductible d'un redémarrage à l'autre.

Le détail sur lequel il vaut la peine de s'attarder est stratify_by. Pointez-le sur un champ de vos données (domain ici) et la proportion s'applique par strate plutôt que sur l'ensemble du pool. Chaque catégorie contribue à l'échantillon de recouvrement de façon proportionnelle, si bien que vous ne mesurez pas l'accord sur un échantillon qui se trouve composé à 90 % d'un seul domaine. Si votre corpus mêle actualités, réseaux sociaux et textes cliniques, chacun apparaît dans l'échantillon de qualité à proportion de sa taille.

Renforcement adaptatif : dépenser plus là où c'est difficile

Un échantillon de recouvrement fixe est choisi à l'aveugle, avant que quiconque ait annoté quoi que ce soit. Or les items qui ont le plus besoin d'un deuxième et d'un troisième regard sont ceux sur lesquels les annotateurs sont réellement en désaccord, et vous n'apprenez lesquels qu'après la première passe. Le renforcement adaptatif règle exactement ce point :

yaml
num_annotators_per_item:
  default: 1
  adaptive:
    enabled: true
    disagreement_threshold: 0.5
    boost_to: 3

Dès qu'un item a au moins deux annotations et que son score de désaccord franchit disagreement_threshold, son plafond est porté à boost_to et l'item revient dans la file d'affectation pour une passe supplémentaire. Le renforcement est unique par item, de sorte qu'un item litigieux reçoit une seule escalade plutôt que de s'emballer. C'est une couverture qui suit la difficulté des données au lieu de la deviner d'avance.

Quotas par annotateur

Les plafonds de couverture déterminent combien d'annotateurs reçoit chaque item. Un bloc distinct détermine combien d'items reçoit chaque annotateur, ce que l'on veut généralement faire varier selon l'expertise ou le contrat :

yaml
per_annotator_quota:
  default: 100
  by_user:
    alice: 30
  by_user_role:
    expert: 30
    novice: 200
 
user_roles:
  alice: expert
  carol: novice

La résolution va du plus spécifique au plus général : d'abord by_user[uid], puis by_user_role[user_roles[uid]], enfin default. Vous pouvez ainsi plafonner un expert précis à 30 items, tout autre expert à 30 par rôle et les novices à 200, sans que les deux systèmes interfèrent avec les plafonds par item ci-dessus.

Transformer le recouvrement en décision

Recueillir du recouvrement n'est que la moitié du travail ; l'enjeu est d'agir sur les désaccords. Lorsque le bloc d'arbitrage est activé, les items de l'échantillon de recouvrement qui atteignent leur plafond sont notés automatiquement et poussés dans une file d'arbitrage dès que l'accord passe sous votre seuil :

yaml
adjudication:
  enabled: true
  adjudicator_users: [admin]
  min_annotations: 2
  agreement_threshold: 0.75

Résultat : les items à faible accord remontent à la surface au moment même où l'échantillon sature, au lieu d'attendre que quelqu'un pense à reconstruire la file à la main. Un arbitre ouvre la file et voit les items réellement contestés, déjà filtrés de la masse sur laquelle tout le monde était d'accord.

Lire l'accord

Une fois les items de l'échantillon de recouvrement saturés, les statistiques d'accord sont disponibles à l'adresse /admin/iaa. Le endpoint calcule la métrique adaptée au type de chaque schéma plutôt que d'imposer un chiffre unique à tout : kappa de Cohen et de Fleiss pour les schémas nominaux, weighted kappa pour les schémas ordinaux, et kappa au niveau du token plus span F1 pour les spans. Cela compte, car un κ calculé comme si vos notations Likert ordinales étaient des catégories non ordonnées sous-estimera l'accord réel.

Pour l'essayer

Une démonstration exécutable est livrée avec la version. Depuis la racine du dépôt :

bash
python potato/flask_server.py start examples/advanced/heterogeneous-coverage/config.yaml -p 8000

Elle utilise 20 items répartis sur deux domaines, échantillonne 20 % pour un recouvrement à trois annotateurs stratifié par domaine, active un renforcement adaptatif au seuil de 0,5, définit deux paliers d'expertise et route les items à faible accord vers l'arbitrage : toute la conception ci-dessus, de bout en bout.

La forme d'un bon plan de couverture

Mis bout à bout, le dispositif vous laisse décider où va votre budget d'annotation au lieu de l'étaler uniformément. La plupart des items reçoivent une passe. Une tranche stratifiée en reçoit trois, ce qui vous permet de rendre compte de la fiabilité sur l'ensemble du corpus, et non sur un seul de ses coins. Les items qui se révèlent réellement difficiles sont escaladés automatiquement, et les items contestés sont routés vers un arbitre. Vous dépensez le plus sur les données les plus incertaines, et vous pouvez défendre chaque décision de couverture dans une section méthodes.

Combien d'annotateurs il vous faut réellement pour une tâche donnée est une question à part ; l'article combien d'annotateurs vous faut-il passe en revue les règles empiriques. Cette version vise à rendre facile à exprimer la réponse, quelle qu'elle soit, à laquelle vous parvenez. La couverture hétérogène est livrée dans Potato 2.6 ; consultez la documentation sur la couverture hétérogène et la référence sur l'affectation des tâches pour tout ce que les blocs ci-dessus permettent de faire.