Partie 2: Créer une détection de la toxicité de l'IA côté client

Maud Nalpas
Maud Nalpas

Publié le 13 novembre 2024

L'incitation à la haine, le harcèlement et les abus en ligne sont devenus un problème omniprésent sur Internet. Les commentaires toxiques étouffent des voix importantes et éloignent les utilisateurs et les clients. La détection de toxicité protège vos utilisateurs et crée un environnement en ligne plus sûr.

Dans cette série en deux parties, nous allons voir comment utiliser l'IA pour détecter et atténuer la toxicité à sa source : les claviers des utilisateurs.

Dans la première partie, nous avons abordé les cas d'utilisation et les avantages de cette approche.

Dans cette deuxième partie, nous allons nous pencher sur l'implémentation, y compris sur des exemples de code et des conseils sur l'expérience utilisateur.

Démonstration et code

Testez notre démonstration et examinez le code sur GitHub.

Démonstration de la publication de commentaires.
Lorsque l'utilisateur arrête de taper, nous analysons la toxicité de son commentaire. Nous affichons un avertissement en temps réel si le commentaire est classé comme toxique.

Prise en charge des navigateurs

Notre démo fonctionne avec les dernières versions de Safari, Chrome, Edge et Firefox.

Sélectionner un modèle et une bibliothèque

Nous utilisons la bibliothèque Transformers.js de Hugging Face, qui fournit des outils permettant de travailler avec des modèles de machine learning dans le navigateur. Notre code de démonstration est dérivé de cet exemple de classification de texte.

Nous choisissons le modèle toxic-bert, un modèle pré-entraîné conçu pour identifier les schémas de langage toxiques. Il s'agit d'une version compatible avec le Web de unitary/toxic-bert. Pour en savoir plus sur les libellés du modèle et sa classification des attaques d'identité, consultez la page du modèle Hugging Face.

La taille du téléchargement de toxic-bert est de 111 Mo.

Une fois le modèle téléchargé, l'inférence est rapide.

Par exemple, cela prend généralement moins de 500 millisecondes dans Chrome sur un appareil Android de milieu de gamme que nous avons testé (un téléphone Pixel 7 standard, et non le modèle Pro plus performant). Exécutez vos propres benchmarks représentatifs de votre base d'utilisateurs.

Implémentation

Voici les étapes clés de notre implémentation :

Définir un seuil de toxicité

Notre classificateur de toxicité fournit des scores de toxicité compris entre 0 et 1. Dans cette plage, nous devons définir un seuil pour déterminer ce qui constitue un commentaire toxique. Un seuil couramment utilisé est 0.9. Cela vous permet de détecter les commentaires ouvertement toxiques, tout en évitant une sensibilité excessive qui pourrait entraîner trop de faux positifs (c'est-à-dire des commentaires inoffensifs classés comme toxiques).

export const TOXICITY_THRESHOLD = 0.9

Importer les composants

Nous commençons par importer les composants nécessaires de la bibliothèque @xenova/transformers. Nous importons également des constantes et des valeurs de configuration, y compris notre seuil de toxicité.

import { env, pipeline } from '@xenova/transformers';
// Model name: 'Xenova/toxic-bert'
// Our threshold is set to 0.9
import { TOXICITY_THRESHOLD, MODEL_NAME } from './config.js';

Charger le modèle et communiquer avec le thread principal

Nous chargeons le modèle de détection de toxicité toxic-bert et l'utilisons pour préparer notre classificateur. La version la moins complexe est const classifier = await pipeline('text-classification', MODEL_NAME);.

La création d'un pipeline, comme dans l'exemple de code, est la première étape pour exécuter des tâches d'inférence.

La fonction de pipeline accepte deux arguments : la tâche ('text-classification') et le modèle (Xenova/toxic-bert).

Terme clé : dans Transformers.js, un pipeline est une API de haut niveau qui simplifie le processus d'exécution des modèles de ML. Il gère des tâches telles que le chargement de modèles, la tokenisation et le post-traitement.

Notre code de démonstration fait un peu plus que simplement préparer le modèle, car nous déchargeons les étapes de préparation du modèle gourmandes en calcul sur un nœud de calcul Web. Cela permet au thread principal de rester responsif. En savoir plus sur le déchargement des tâches coûteuses sur un Web Worker

Notre worker doit communiquer avec le thread principal, en utilisant des messages pour indiquer l'état du modèle et les résultats de l'évaluation de la toxicité. Consultez les codes de message que nous avons créés et qui correspondent aux différents états du cycle de vie de la préparation et de l'inférence du modèle.

let classifier = null;
(async function () {
  // Signal to the main thread that model preparation has started
  self.postMessage({ code: MESSAGE_CODE.PREPARING_MODEL, payload: null });
  try {
    // Prepare the model
    classifier = await pipeline('text-classification', MODEL_NAME);
    // Signal to the main thread that the model is ready
    self.postMessage({ code: MESSAGE_CODE.MODEL_READY, payload: null });
  } catch (error) {
    console.error('[Worker] Error preparing model:', error);
    self.postMessage({ code: MESSAGE_CODE.MODEL_ERROR, payload: null });
  }
})();

Classer l'entrée utilisateur

Dans notre fonction classify, nous utilisons notre classificateur créé précédemment pour analyser un commentaire d'utilisateur. Nous renvoyons le résultat brut du classificateur de toxicité : libellés et scores.

// Asynchronous function to classify user input
// output: [{ label: 'toxic', score: 0.9243140482902527 },
// ... { label: 'insult', score: 0.96187334060668945 }
// { label: 'obscene', score: 0.03452680632472038 }, ...etc]
async function classify(text) {
  if (!classifier) {
    throw new Error("Can't run inference, the model is not ready yet");
  }
  let results = await classifier(text, { topk: null });
  return results;
}

Nous appelons notre fonction de classification lorsque le thread principal demande au worker de le faire. Dans notre démo, nous déclenchons le classificateur dès que l'utilisateur a fini de saisir du texte (voir TYPING_DELAY). Dans ce cas, notre thread principal envoie un message au worker contenant la saisie de l'utilisateur à classer.

self.onmessage = async function (message) {
  // User input
  const textToClassify = message.data;
  if (!classifier) {
    throw new Error("Can't run inference, the model is not ready yet");
  }
  self.postMessage({ code: MESSAGE_CODE.GENERATING_RESPONSE, payload: null });

  // Inference: run the classifier
  let classificationResults = null;
  try {
    classificationResults = await classify(textToClassify);
  } catch (error) {
    console.error('[Worker] Error: ', error);
    self.postMessage({
      code: MESSAGE_CODE.INFERENCE_ERROR,
    });
    return;
  }
  const toxicityTypes = getToxicityTypes(classificationResults);
  const toxicityAssessement = {
    isToxic: toxicityTypes.length > 0,
    toxicityTypeList: toxicityTypes.length > 0 ? toxicityTypes.join(', ') : '',
  };
  console.info('[Worker] Toxicity assessed: ', toxicityAssessement);
  self.postMessage({
    code: MESSAGE_CODE.RESPONSE_READY,
    payload: toxicityAssessement,
  });
};

Traiter le résultat

Nous vérifions si les scores de sortie du classificateur dépassent notre seuil. Le cas échéant, nous prenons note du libellé en question.

Si l'un des libellés de toxicité est indiqué, le commentaire est signalé comme potentiellement toxique.

// input: [{ label: 'toxic', score: 0.9243140482902527 }, ...
// { label: 'insult', score: 0.96187334060668945 },
// { label: 'obscene', score: 0.03452680632472038 }, ...etc]
// output: ['toxic', 'insult']
function getToxicityTypes(results) {
  const toxicityAssessment = [];
  for (let element of results) {
    // If a label's score > our threshold, save the label
    if (element.score > TOXICITY_THRESHOLD) {
      toxicityAssessment.push(element.label);
    }
  }
  return toxicityAssessment;
}

self.onmessage = async function (message) {
  // User input
  const textToClassify = message.data;
  if (!classifier) {
    throw new Error("Can't run inference, the model is not ready yet");
  }
  self.postMessage({ code: MESSAGE_CODE.GENERATING_RESPONSE, payload: null });

  // Inference: run the classifier
  let classificationResults = null;
  try {
    classificationResults = await classify(textToClassify);
  } catch (error) {
    self.postMessage({
      code: MESSAGE_CODE.INFERENCE_ERROR,
    });
    return;
  }
  const toxicityTypes = getToxicityTypes(classificationResults);
  const toxicityAssessement = {
    // If any toxicity label is listed, the comment is flagged as
    // potentially toxic (isToxic true)
    isToxic: toxicityTypes.length > 0,
    toxicityTypeList: toxicityTypes.length > 0 ? toxicityTypes.join(', ') : '',
  };
  self.postMessage({
    code: MESSAGE_CODE.RESPONSE_READY,
    payload: toxicityAssessement,
  });
};

Afficher un indice

Si isToxic est défini sur "true", nous affichons un indice à l'utilisateur. Dans notre démo, nous n'utilisons pas le type de toxicité plus précis, mais nous l'avons mis à disposition du thread principal si nécessaire (toxicityTypeList). Vous pouvez le trouver utile pour votre cas d'utilisation.

Expérience utilisateur

Dans notre démonstration, nous avons fait les choix suivants :

  • Toujours autoriser la publication Notre indication de toxicité côté client n'empêche pas l'utilisateur de publier. Dans notre démo, l'utilisateur peut publier un commentaire même si le modèle n'est pas chargé (et ne propose donc pas d'évaluation de la toxicité) et même si le commentaire est détecté comme toxique. Comme recommandé, vous devez disposer d'un deuxième système pour détecter les commentaires toxiques. Si cela a du sens pour votre application, envisagez d'informer l'utilisateur que son commentaire a été transmis sur le client, mais qu'il a ensuite été signalé sur le serveur ou lors d'une inspection humaine.
  • Tenez compte des faux négatifs. Lorsqu'un commentaire n'est pas classé comme toxique, notre démo ne fournit pas de commentaires (par exemple, "Joli commentaire !"). En plus d'être bruyant, le fait de fournir des commentaires positifs peut envoyer le mauvais signal, car notre classificateur manque parfois, mais inévitablement, certains commentaires toxiques.
Démonstration de la publication de commentaires.
Le bouton Publier est toujours activé : dans notre démo, l'utilisateur peut toujours décider de publier son commentaire, même s'il est classé comme toxique. Même si un commentaire n'est pas classé comme toxique, nous n'affichons pas de commentaires positifs.

Améliorations et alternatives

Limitations et améliorations futures

  • Langues : le modèle que nous utilisons est principalement compatible avec l'anglais. Pour bénéficier d'une assistance multilingue, vous devez effectuer un réglage précis. Plusieurs modèles de toxicité listés sur Hugging Face sont compatibles avec les langues autres que l'anglais (russe, néerlandais), mais ils ne sont pas compatibles avec Transformers.js pour le moment.
  • Nuance : bien que toxic-bert détecte efficacement la toxicité manifeste, il peut avoir du mal à identifier les cas plus subtils ou dépendants du contexte (ironie, sarcasme). La toxicité peut être très subjective et subtile. Par exemple, vous pouvez souhaiter que certains termes ou même certains emoji soient classés comme toxiques. L'affinage peut vous aider à améliorer la précision dans ces domaines.

Nous publierons prochainement un article sur l'affinage d'un modèle de toxicité.

Autres solutions

Conclusion

La détection de la toxicité côté client est un outil puissant pour améliorer les communautés en ligne.

En exploitant des modèles d'IA comme toxic-bert, qui s'exécutent dans le navigateur avec Transformers.js, vous pouvez implémenter des mécanismes de commentaires en temps réel qui découragent les comportements toxiques et réduisent la charge de classification de la toxicité sur vos serveurs.

Cette approche côté client fonctionne déjà dans tous les navigateurs. Toutefois, gardez à l'esprit les limites, en particulier en termes de coûts de diffusion des modèles et de taille de téléchargement. Appliquez les bonnes pratiques en termes de performances pour l'IA côté client et mettez le modèle en cache.

Pour une détection complète de la toxicité, combinez les approches côté client et côté serveur.