Parte 2: criar a detecção de toxicidade da IA do lado do cliente

Maud Nalpas
Maud Nalpas

Publicado em 13 de novembro de 2024

Discurso de ódio, assédio e abuso on-line se tornaram um problema generalizado. Os comentários tóxicos silenciam vozes importantes e afastar usuários e clientes. A detecção de toxicidade protege seus usuários e cria um ambiente on-line mais seguro.

Nesta série de dois episódios, vamos mostrar como usar a IA para detectar e reduzir a toxicidade na fonte: os teclados dos usuários.

Na parte um, discutimos os casos de uso e os benefícios dessa abordagem.

Nesta segunda parte, vamos nos aprofundar na implementação, incluindo exemplos de código e dicas de UX.

Demonstração e código

Teste nossa demonstração e investigue o código no GitHub.

Demonstração de postagem de comentários.
Quando o usuário para de digitar, analisamos a toxicidade do comentário. Mostramos um aviso em tempo real se o comentário for classificado como tóxico.

Suporte ao navegador

Nossa demonstração é executada nas versões mais recentes do Safari, Chrome, Edge e Firefox.

Selecionar um modelo e uma biblioteca

Usamos a biblioteca Transformers.js do Hugging Face, que oferece ferramentas para trabalhar com modelos de aprendizado de máquina no navegador. Nosso código de demonstração é derivado deste exemplo de classificação de texto.

Escolhemos o modelo toxic-bert, um modelo pré-treinado projetado para identificar padrões de linguagem tóxicos. É uma versão compatível com a Web de unitary/toxic-bert. Para mais detalhes sobre os rótulos do modelo e a classificação de ataques de identidade, consulte a página do modelo de rosto abraço apertado.

O tamanho de download de toxic-bert é de 111 MB.

Depois que o modelo é transferido por download, a inferência é rápida.

Por exemplo, normalmente leva menos de 500 milissegundos no Chrome em execução em um dispositivo Android de gama média que testamos (um smartphone Pixel 7 normal, não o modelo Pro de maior desempenho). Execute seus próprios comparativos que representem sua base de usuários.

Implementação

Estas são as principais etapas da nossa implementação:

Definir um limite de toxicidade

Nosso classificador de toxicidade fornece pontuações de toxicidade entre 0 e 1. Nesse intervalo, precisamos definir um limite para determinar o que constitui um comentário tóxico. Um limite comumente usado é 0.9. Isso permite que você detecte comentários claramente tóxicos, evitando a sensibilidade excessiva, que pode levar a muitos falsos positivos, ou seja, comentários inofensivos categorizados como tóxicos.

export const TOXICITY_THRESHOLD = 0.9

Importar os componentes

Começamos importando os componentes necessários da biblioteca @xenova/transformers. Também importamos constantes e valores de configuração, incluindo o limite de toxicidade.

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

Carregar o modelo e se comunicar com a linha de execução principal

Carregue o modelo de detecção de toxicidade toxic-bert e use-o para preparar nosso classificador. A versão menos complexa é const classifier = await pipeline('text-classification', MODEL_NAME);

Criar um pipeline, como no código de exemplo, é a primeira etapa para executar tarefas de inferência.

A função do pipeline usa dois argumentos: a tarefa ('text-classification') e o modelo (Xenova/toxic-bert).

Termo-chave: em Transformers.js, um pipeline é uma API de alto nível que simplifica o processo de execução de modelos de ML. Ele processa tarefas como carregamento de modelo, tokenização e pós-processamento.

Nosso código de demonstração faz um pouco mais do que apenas preparar o modelo, porque transferimos as etapas de preparação de modelo computacionalmente caras para um worker da Web. Isso permite que a linha de execução principal continue responsiva. Saiba mais sobre como transferir tarefas caras para um worker da Web.

Nosso worker precisa se comunicar com a linha de execução principal, usando mensagens para indicar o status do modelo e os resultados da avaliação de toxicidade. Confira os códigos de mensagem que criamos e que correspondem a diferentes status do ciclo de vida de preparação e inferência do 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 });
  }
})();

Classificar a entrada do usuário

Na função classify, usamos o classificador criado anteriormente para analisar um comentário do usuário. Retornamos a saída bruta do classificador de toxicidade: rótulos e pontuações.

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

Chamamos nossa função de classificação quando a linha de execução principal pede que o worker faça isso. Na nossa demonstração, ativamos o classificador assim que o usuário para de digitar (consulte TYPING_DELAY). Quando isso acontece, nossa linha de execução principal envia uma mensagem para o worker que contém a entrada do usuário para classificação.

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

Processar a saída

Verificamos se as pontuações de saída do classificador excedem nosso limite. Se esse for o caso, vamos observar o rótulo em questão.

Se algum dos rótulos de toxicidade for listado, o comentário será sinalizado 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,
  });
};

Mostrar uma dica

Se isToxic for verdadeiro, vamos mostrar uma dica para o usuário. Na nossa demonstração, não usamos o tipo de toxicidade mais refinado, mas o disponibilizamos para a linha de execução principal, se necessário (toxicityTypeList). Ele pode ser útil para seu caso de uso.

Experiência do usuário

Na nossa demonstração, fizemos as seguintes escolhas:

  • Sempre permitir a postagem. Nossa dica de toxicidade do lado do cliente não impede que o usuário publique. Na nossa demonstração, o usuário pode postar um comentário mesmo que o modelo não tenha sido carregado (e, portanto, não ofereça uma avaliação de toxicidade) e mesmo que o comentário seja detectado como tóxico. Conforme recomendado, você precisa ter um segundo sistema para detectar comentários tóxicos. Se fizer sentido para seu app, informe ao usuário que o comentário foi enviado ao cliente, mas foi sinalizado no servidor ou durante a inspeção humana.
  • Evite falsos negativos. Quando um comentário não é classificado como tóxico, nossa demonstração não oferece feedback (por exemplo, "Bom comentário!"). Além de ser barulhento, oferecer feedback positivo pode enviar o sinal errado, porque nosso classificador perde alguns comentários tóxicos ocasionalmente, mas inevitavelmente.
Demonstração de postagem de comentários.
O botão Postar está sempre ativado: na nossa demonstração, o usuário ainda pode publicar o comentário, mesmo que ele seja classificado como tóxico. Mesmo que um comentário não seja classificado como tóxico, não mostramos feedback positivo.

Melhorias e alternativas

Limitações e aprimoramentos futuros

  • Idiomas: o modelo que estamos usando oferece suporte principalmente ao inglês. Para oferecer suporte a vários idiomas, você precisa fazer ajustes. Vários modelos de toxicidade listados no Hugging Face oferecem suporte a idiomas diferentes do inglês (russo, holandês), mas não são compatíveis com o Transformers.js no momento.
  • Nuance: embora o toxic-bert detecte efetivamente a toxicidade explícita, ele pode ter dificuldades com casos mais sutis ou dependentes do contexto (ironia, sarcasmo). A toxicidade pode ser altamente subjetiva e sutil. Por exemplo, talvez você queira classificar determinados termos ou até emojis como tóxicos. O ajuste fino pode ajudar a melhorar a precisão nessas áreas.

Em breve, vamos publicar um artigo sobre como ajustar um modelo de toxicidade.

Alternativas

Conclusão

A detecção de toxicidade do lado do cliente é uma ferramenta poderosa para melhorar as comunidades on-line.

Ao usar modelos de IA, como o toxic-bert, que são executados no navegador com o Transformers.js, é possível implementar mecanismos de feedback em tempo real que desencorajam comportamentos tóxicos e reduzem a carga de classificação de toxicidade nos servidores.

Essa abordagem do lado do cliente já funciona em todos os navegadores. No entanto, lembre-se das limitações, principalmente em termos de custos de veiculação de modelos e tamanho de download. Aplique as práticas recomendadas de desempenho para a IA do lado do cliente e armazene o modelo em cache.

Para uma detecção abrangente de toxicidade, combine as abordagens do lado do cliente e do servidor.