Aller au contenu

Classification d'intentions par LLM et gestion de l'incertitude

Classifier des intentions avec un LLM, la baseline avant tout

Classifier des messages en intentions avec un LLM commence par une baseline, pas par un finetuning. Un petit modèle open source (SLM) de moins de 500 millions de paramètres, un prompt strict avec quelques exemples et une sortie structurée donnent souvent une première mesure exploitable. Le vrai piège n'est pas de forcer un JSON valide en sortie, c'est de savoir quand ne pas lui faire confiance.

J'ai eu ce projet récemment, ranger des messages utilisateur dans huit classes d'intention, avec un modèle assez petit pour tourner en production sans exploser la latence ni la facture. Le réflexe du prompt strict et de la sortie structurée a bien fonctionné dès le départ. Ce qui m'a pris plus de temps, c'est de comprendre pourquoi certaines prédictions confiantes en apparence ne l'étaient pas du tout.

Cet article détaille la démarche dans l'ordre où elle s'est présentée, de la baseline au piège du constrained decoding, puis le scoring par log-probabilité, la décision produit sur les cas incertains, la discipline d'évaluation, et la question du finetuning. Si vous hésitez encore entre un petit et un gros modèle pour ce genre de tâche, j'ai posé la grille de décision dans SLM vs LLM, quand choisir un petit modèle ?.

Le piège du constrained decoding

Le constrained decoding garantit un format de sortie valide, mais force toujours une réponse, même quand aucune n'est bonne. C'est le piège central de la classification par LLM. Un JSON syntaxiquement parfait peut cacher une prédiction que le modèle n'a jamais vraiment choisie.

Techniquement, le constrained decoding (aussi appelé grammar-constrained generation) restreint à chaque étape les tokens que le modèle peut générer. On calcule un masque sur le vocabulaire, où les tokens qui violeraient le schéma attendu, le JSON, la liste des labels autorisés, voient leur logit mis à moins l'infini, puis on échantillonne parmi ce qui reste.

C'est le mécanisme derrière les sorties structurées qui garantissent qu'une réponse respecte toujours le schéma fourni, ou derrière les grammaires GBNF de llama.cpp et les schémas JSON d'Outlines en local.

Le problème, c'est que ce masquage ne regarde que la validité du format, jamais la pertinence du contenu. Si un message est ambigu ou hors sujet, les huit labels autorisés restent tous candidats. Le modèle en choisit un quand même, celui qui a le score le plus haut parmi ce qui reste, même si ce score est faible dans l'absolu. Rien dans la sortie ne dit "je ne sais pas".

Concrètement, un message hors sujet ou mal formulé se voit quand même coller un label avec une belle confiance apparente dans le JSON. C'est un faux positif silencieux. Rien, dans la structure de la réponse, ne distingue une vraie décision d'une réponse imposée par la contrainte de format.

Cette tension entre format contraint et qualité de la réponse est documentée au-delà de la classification. Une étude de 2024 (arXiv 2408.02442) montre que forcer un LLM à respecter un format strict (JSON, XML...) dégrade ses capacités de raisonnement par rapport à une réponse libre, et que la dégradation s'accentue avec la sévérité de la contrainte.

Sur de la classification simple, l'effet est plus limité que sur du raisonnement à plusieurs étapes, mais le principe reste le même. Contraindre la forme a un coût, et ce coût est rarement visible dans la sortie elle-même.

Scorer les labels par log-probabilité et lire la marge de confiance

La solution que j'applique n'est pas d'abandonner la sortie structurée, mais de ne plus laisser le modèle "choisir" tout seul. On récupère la vraisemblance de chaque label autorisé via les logits du modèle, et on lit l'écart entre les deux meilleurs comme signal de confiance.

Le score top-1 par logprob

Un modèle de langage causal donne, à chaque position, une distribution de probabilité sur tout son vocabulaire pour le prochain token. Plutôt que de laisser le modèle générer librement puis parser sa réponse, on lit directement cette distribution aux positions qui nous intéressent, celles des labels candidats.

La façon la plus simple de faire ça avec un petit nombre de classes consiste à associer chaque label à une lettre unique (A, B, C...) dans le prompt, puis à ne lire les logits qu'à ces quelques positions du vocabulaire. C'est la même mécanique que les benchmarks à choix multiples type MMLU, et elle évite d'avoir à sommer les logprobs sur plusieurs tokens pour des labels composés de plusieurs mots.

La marge top-1 contre top-2

Le score top-1 seul ne suffit pas. Un logprob de -0,3 ne veut rien dire dans l'absolu, il dépend du modèle, du prompt et du nombre de classes. Ce qui compte, c'est l'écart entre le meilleur label et le deuxième.

Une marge large signifie que le modèle a une préférence nette. Une marge faible signifie qu'il hésite entre deux classes, souvent parce que le message est ambigu, hors périmètre, ou mal couvert par les exemples few-shot. C'est exactement le signal qu'un JSON valide, à lui seul, ne donne jamais.

Voici la mécanique simplifiée, sur un modèle causal chargé avec Transformers.

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

model = AutoModelForCausalLM.from_pretrained("mon-slm-0.5b")
tokenizer = AutoTokenizer.from_pretrained("mon-slm-0.5b")

LABELS = {"A": "facturation", "B": "resiliation", "C": "support_technique", "D": "autre"}

def classify_with_margin(prompt: str) -> tuple[str, float]:
    inputs = tokenizer(prompt, return_tensors="pt")
    with torch.no_grad():
        logits = model(**inputs).logits[0, -1]
    log_probs = torch.log_softmax(logits, dim=-1)

    scores = {
        letter: log_probs[tokenizer.encode(letter, add_special_tokens=False)[0]].item()
        for letter in LABELS
    }
    top1, top2 = sorted(scores.items(), key=lambda kv: kv[1], reverse=True)[:2]
    margin = top1[1] - top2[1]

    return LABELS[top1[0]], margin

Le seuil de marge en dessous duquel on ne fait plus confiance à la prédiction se règle sur le jeu de validation, pas au doigt mouillé. Cette approche rejoint des retours d'expérience publiés sur les logprobs pour la classification, où le score de confiance se construit à partir de l'écart entre labels concurrents plutôt que du seul label prédit.

Other, Unknown, Low Confidence, Parsing Error, quatre catégories d'incertitude à ne pas confondre

Une fois la marge calculée, la tentation est de tout ranger dans un fourre-tout "incertain". C'est une erreur. Ces cas n'ont pas la même cause, et les confondre casse le diagnostic.

  • Other, le message a une intention réelle, mais elle ne fait pas partie des huit classes définies. C'est un signal produit, peut-être un nouveau besoin à couvrir.
  • Unknown, le message est trop court, trop vague ou hors sujet pour qu'aucune classe n'ait vraiment de sens. Ce n'est pas un défaut du modèle, c'est la nature du message.
  • Low Confidence, le modèle a rendu une prédiction, mais la marge top-1/top-2 est sous le seuil retenu. Une classe a probablement raison, le modèle hésite juste entre deux candidates proches.
  • Parsing Error, la sortie n'a pas pu être parsée du tout. C'est un problème d'infrastructure ou de prompt, pas un problème de compréhension du message.

Distinguer ces quatre cas change la décision produit. Mieux vaut ne pas comptabiliser un événement incertain que polluer les KPI avec un faux label. Un Low Confidence peut déclencher une escalade vers un humain ou un modèle plus gros, un Parsing Error doit alerter sur l'infrastructure, un Other doit remonter comme signal produit. Les mélanger revient à perdre trois informations différentes dans une seule case "erreur".

La discipline d'évaluation, holdout, F1 par classe et matrice de confusion

Aucun scoring de confiance ne remplace une évaluation sérieuse. Avant de faire confiance à un seuil de marge, on le calibre et on le valide sur des données qu'on n'a jamais utilisées pour le régler.

La règle que j'applique partout tient en trois jeux séparés, entraînement (ou exemples few-shot), validation, et un jeu de test holdout qu'on ne touche jamais avant la fin. Le seuil de marge se règle sur la validation, jamais sur le holdout, sinon la mesure finale ne veut plus rien dire.

Sur une classification à plusieurs classes, la métrique globale (accuracy) cache toujours quelque chose. Je regarde le F1 par classe, qui révèle si une intention rare est systématiquement mal reconnue même quand le score global a l'air bon, et la matrice de confusion, qui montre précisément entre quelles classes le modèle hésite. Ce sont souvent les mêmes paires qui reviennent dans les cas de marge faible, ce qui confirme que le signal de confiance capture bien quelque chose de réel. Une fois le finetuning fait, la question devient de savoir s'il tient vraiment la route avant la mise en prod : je détaille le protocole complet, holdout scellé, canary set et delta chiffré, dans comment évaluer un finetuning avant la mise en prod.

Quand finetuner un SLM pour classifier des intentions

La baseline (prompt strict, few-shot, scoring par logprob) suffit dans une bonne partie des cas mesurés. On ne passe au finetuning que si elle ne suffit pas, jamais par réflexe.

Le signal qui justifie de finetuner n'est pas une impression, ce sont les métriques du holdout. Un F1 par classe trop bas sur des intentions précises, ou trop de cas Low Confidence sur des messages qui devraient pourtant être clairs. Dans ce cas, un fine-tuning LoRA/QLoRA sur quelques centaines d'exemples annotés, tirés justement des cas mal classés, referme l'essentiel de l'écart. Je détaille la méthode et les hyperparamètres dans comment entraîner un SLM.

On gagne le droit de finetuner, on ne se l'accorde pas par défaut. Et même après un finetuning, je garde le même scoring par log-probabilité. Un modèle spécialisé peut lui aussi se tromper sur un message qui sort de son périmètre d'entraînement, holdout mis à part.

FAQ sur la classification d'intentions par LLM

Comment classifier du texte avec un LLM ? Commencez par une baseline avant de finetuner quoi que ce soit. Prenez un SLM de moins d'un milliard de paramètres, un prompt strict, quelques exemples en few-shot et une sortie structurée. Mesurez cette baseline sur un jeu de test gardé de côté avant d'envisager un entraînement.

Qu'est-ce que le constrained decoding ? Une technique qui restreint, à chaque étape de génération, les tokens que le modèle peut produire, en masquant à moins l'infini le logit des tokens qui violeraient le format attendu. C'est ce qui garantit qu'une sortie structurée respecte toujours le schéma demandé.

Pourquoi une sortie structurée valide n'est-elle pas toujours fiable ? Parce que le masquage de tokens ne juge que la validité du format, jamais la pertinence du contenu. Face à un message ambigu, le modèle choisit quand même un label parmi ceux qui restent autorisés, même avec un score faible. Rien dans le JSON final ne distingue une vraie décision d'une réponse imposée.

Comment mesurer la confiance d'un LLM sur une prédiction de classification ? En lisant les logprobs des labels candidats plutôt qu'en se fiant au seul label généré. Le signal utile est la marge entre le score du premier et celui du deuxième candidat. Une marge large signale une décision nette, une marge faible signale une hésitation.

Que faire d'une prédiction incertaine dans un système de classification ? Mieux vaut ne pas comptabiliser un événement incertain que polluer les KPI avec un faux label. Distinguez Other (intention hors périmètre), Unknown (aucune classe ne correspond), Low Confidence (marge faible) et Parsing Error (sortie illisible), car les confondre fait perdre le diagnostic.

Faut-il finetuner un SLM pour classifier des intentions ? Seulement si la baseline mesurée ne suffit pas, quand le F1 par classe reste trop bas, ou qu'il y a trop de cas Low Confidence sur des messages qui devraient être clairs. Dans ce cas, un fine-tuning LoRA/QLoRA sur quelques centaines d'exemples annotés comble l'essentiel de l'écart.

Peut-on classifier du texte en français avec un petit modèle open source ? Oui, à condition de vérifier son support du français en few-shot et de mesurer sur un jeu de test représentatif du vocabulaire réel de vos utilisateurs. La robustesse varie selon le modèle et la classe d'intention testée.

Pour aller plus loin

Si mes articles vous intéressent, que vous avez des questions ou simplement envie de discuter de vos propres défis liés à l'IA, n'hésitez pas à m'écrire à anas@tensoria.fr, j'adore échanger sur ces sujets !

Vous avez un projet de classification ou de routage d'intentions à mettre en production ? Découvrez mon activité de conseil sur tensoria.fr.

Vous pouvez aussi réserver un créneau d'échange ou vous abonner à ma newsletter :)


À propos de moi

Je suis Anas Rabhi, consultant Data Scientist freelance. J'accompagne les entreprises dans leur stratégie et mise en œuvre de solutions d'IA (RAG, Agents, NLP).

Découvrez mes services sur tensoria.fr ou testez notre solution d'agents IA heeya.fr.