Parte 2: Crea una detección de toxicidad de IA del cliente

Maud Nalpas
Maud Nalpas

Fecha de publicación: 13 de noviembre de 2024

El discurso de odio, el acoso y el abuso en línea se han convertido en un problema generalizado en la Web. Los comentarios tóxicos silencian voces importantes y alejan a los usuarios y clientes. La detección de toxicidad protege a tus usuarios y crea un entorno en línea más seguro.

En esta serie de dos partes, exploramos cómo usar la IA para detectar y mitigar la toxicidad en su fuente: los teclados de los usuarios.

En la primera parte, analizamos los casos de uso y los beneficios de este enfoque.

En esta segunda parte, analizamos la implementación, incluidos ejemplos de código y sugerencias de UX.

Demostración y código

Prueba nuestra demo y, luego, investiga el código en GitHub.

Demostración de la publicación de comentarios.
Cuando el usuario deja de escribir, analizamos la toxicidad de su comentario. Mostramos una advertencia en tiempo real si el comentario se clasifica como tóxico.

Navegadores compatibles

Nuestra demostración se ejecuta en las versiones más recientes de Safari, Chrome, Edge y Firefox.

Selecciona un modelo y una biblioteca

Usamos la biblioteca Transformers.js de Hugging Face, que proporciona herramientas para trabajar con modelos de aprendizaje automático en el navegador. Nuestro código de demostración se deriva de este ejemplo de clasificación de texto.

Elegimos el modelo toxic-bert, un modelo previamente entrenado diseñado para identificar patrones de lenguaje tóxico. Es una versión compatible con la Web de unitary/toxic-bert. Para obtener más detalles sobre las etiquetas del modelo y su clasificación de ataques de identidad, consulta la página del modelo de Hugging Face.

El tamaño de descarga de toxic-bert es de 111 MB.

Una vez que se descarga el modelo, la inferencia es rápida.

Por ejemplo, suele tardar menos de 500 milisegundos en Chrome que se ejecuta en un dispositivo Android de gama media en el que lo probamos (un teléfono Pixel 7 normal, no el modelo Pro de mayor rendimiento). Ejecuta tus propias comparativas que sean representativas de tu base de usuarios.

Implementación

Estos son los pasos clave de nuestra implementación:

Establece un umbral de toxicidad

Nuestro clasificador de toxicidad proporciona puntuaciones de toxicidad entre 0 y 1. Dentro de ese rango, debemos establecer un umbral para determinar qué constituye un comentario tóxico. Un umbral de uso general es 0.9. Esto te permite detectar comentarios abiertamente tóxicos y, al mismo tiempo, evitar una sensibilidad excesiva que podría generar demasiados falsos positivos (en otras palabras, comentarios inofensivos categorizados como tóxicos).

export const TOXICITY_THRESHOLD = 0.9

Importa los componentes

Comenzamos por importar los componentes necesarios de la biblioteca @xenova/transformers. También importamos constantes y valores de configuración, incluido nuestro umbral de toxicidad.

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';

Carga el modelo y comunícate con el subproceso principal

Cargamos el modelo de detección de toxicidad toxic-bert y lo usamos para preparar nuestro clasificador. La versión menos compleja de esto es const classifier = await pipeline('text-classification', MODEL_NAME);.

Crear una canalización, como en el código de ejemplo, es el primer paso para ejecutar tareas de inferencia.

La función de canalización toma dos argumentos: la tarea ('text-classification') y el modelo (Xenova/toxic-bert).

Término clave: En Transformers.js, una canalización es una API de alto nivel que simplifica el proceso de ejecutar modelos de AA. Controla tareas como la carga de modelos, la tokenización y el procesamiento posterior.

Nuestro código de demostración hace un poco más que solo preparar el modelo, ya que transfiere los pasos de preparación del modelo costosos en términos de procesamiento a un trabajador web. Esto permite que el subproceso principal siga siendo responsivo. Obtén más información para transferir tareas costosas a un trabajador web.

Nuestro trabajador debe comunicarse con el subproceso principal mediante mensajes para indicar el estado del modelo y los resultados de la evaluación de toxicidad. Observa los códigos de mensajes que creamos y que se asignan a diferentes estados del ciclo de vida de la preparación y la inferencia del modelo.

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 });
  }
})();

Clasifica la entrada del usuario

En nuestra función classify, usamos el clasificador que creamos anteriormente para analizar un comentario de un usuario. Devolvemos el resultado sin procesar del clasificador de toxicidad: etiquetas y puntuaciones.

// 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;
}

Llamamos a nuestra función de clasificación cuando el subproceso principal le pide al trabajador que lo haga. En nuestra demostración, activamos el clasificador en cuanto el usuario deja de escribir (consulta TYPING_DELAY). Cuando esto sucede, nuestro subproceso principal envía un mensaje al trabajador que contiene la entrada del usuario para clasificar.

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,
  });
};

Procesa el resultado

Verificamos si las puntuaciones de salida del clasificador superan nuestro umbral. Si es así, tomamos nota de la etiqueta en cuestión.

Si aparece alguna de las etiquetas de toxicidad, el comentario se marca como potencialmente tóxico.

// 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,
  });
};

Cómo mostrar una sugerencia

Si isToxic es verdadero, le mostramos una sugerencia al usuario. En nuestra demostración, no usamos el tipo de toxicidad más detallado, pero lo ponemos a disposición del subproceso principal si es necesario (toxicityTypeList). Es posible que te resulte útil para tu caso de uso.

Experiencia del usuario

En nuestra demostración, tomamos las siguientes decisiones:

  • Permitir siempre las publicaciones. Nuestra sugerencia de toxicidad del cliente no impide que el usuario realice la publicación. En nuestra demostración, el usuario puede publicar un comentario incluso si el modelo no se cargó (y, por lo tanto, no ofrece una evaluación de toxicidad) y, también, si el comentario se detecta como tóxico. Como se recomienda, debes tener un segundo sistema para detectar comentarios tóxicos. Si tiene sentido para tu aplicación, considera informarle al usuario que su comentario se procesó en el cliente, pero se marcó en el servidor o durante la inspección humana.
  • Ten en cuenta los falsos negativos. Cuando un comentario no se clasifica como tóxico, nuestra demo no ofrece comentarios (por ejemplo, “Buen comentario”). Además de ser ruidosos, ofrecer comentarios positivos puede enviar un indicador incorrecto, ya que nuestro clasificador, de forma ocasional, pero inevitable, omite algunos comentarios tóxicos.
Demostración de la publicación de comentarios.
El botón Publicar siempre está habilitado: en nuestra demostración, el usuario puede decidir publicar su comentario, incluso si se clasifica como tóxico. Incluso si un comentario no se clasifica como tóxico, no mostramos comentarios positivos.

Mejoras y alternativas

Limitaciones y próximas mejoras

  • Idiomas: El modelo que usamos admite principalmente el inglés. Para la asistencia en varios idiomas, debes realizar ajustes. Varios modelos de toxicidad que se enumeran en Hugging Face admiten idiomas que no son inglés (ruso y holandés), aunque no son compatibles con Transformers.js en este momento.
  • Nuance: Si bien toxic-bert detecta de manera eficaz la toxicidad manifiesta, puede tener dificultades con casos más sutiles o dependientes del contexto (ironía, sarcasmo). La toxicidad puede ser muy subjetiva y sutil. Por ejemplo, es posible que desees que ciertos términos o incluso emojis se clasifiquen como tóxicos. El ajuste fino puede ayudar a mejorar la precisión en estas áreas.

Tenemos un próximo artículo sobre cómo ajustar un modelo de toxicidad.

Alternativas

Conclusión

La detección de toxicidad del cliente es una herramienta poderosa para mejorar las comunidades en línea.

Si aprovechas los modelos de IA, como toxic-bert, que se ejecutan en el navegador con Transformers.js, puedes implementar mecanismos de comentarios en tiempo real que disuadan el comportamiento tóxico y reduzcan la carga de clasificación de toxicidad en tus servidores.

Este enfoque del cliente ya funciona en todos los navegadores. Sin embargo, ten en cuenta las limitaciones, especialmente en términos de costos de entrega de modelos y tamaño de descarga. Aplica las prácticas recomendadas de rendimiento para la IA del cliente y cachéalo.

Para obtener una detección de toxicidad integral, combina los enfoques del cliente y del servidor.