パート 2: クライアントサイドの AI 有害コンテンツ検出を構築する

Maud Nalpas
Maud Nalpas

公開日: 2024 年 11 月 13 日

悪意のある表現、嫌がらせ、オンラインでの虐待は、オンラインで蔓延している問題となっています。有害なコメントは重要な意見を封じ込めユーザーや顧客を遠ざけます。有害性検出は、ユーザーを保護し、より安全なオンライン環境を構築します。

この 2 部構成のシリーズでは、AI を使用して、ユーザーのキーボードという発生源で有害性を検出し、軽減する方法について説明します。

パート 1 では、このアプローチのユースケースとメリットについて説明しました。

この後編では、実装について詳しく説明します。コード例や UX のヒントもご紹介します。

デモとコード

デモを試して、GitHub のコードを調べてください。

コメント投稿のデモ。
ユーザーがコメントの入力を停止すると、コメントの有害性を分析します。コメントが有害と分類された場合は、リアルタイムで警告が表示されます。

ブラウザ サポート

デモは、Safari、Chrome、Edge、Firefox の最新バージョンで実行されます。

モデルとライブラリを選択する

Hugging Face の Transformers.js ライブラリを使用します。このライブラリには、ブラウザで機械学習モデルを操作するためのツールが用意されています。このデモコードは、このテキスト分類の例から派生したものです。

有害な言語パターンを識別するように設計された事前トレーニング済みモデルである toxic-bert モデルを選択します。これは、unitary/toxic-bert のウェブ互換バージョンです。モデルのラベルと ID 攻撃の分類の詳細については、Hugging Face モデルのページをご覧ください。

toxic-bert のダウンロード サイズは 111 MB です。

モデルがダウンロードされると、推論は高速になります。

たとえば、テストした中価格帯の Android デバイス(高性能な Pro モデルではなく、通常の Google Pixel 7)で実行されている Chrome では、通常 500 ミリ秒未満です。ユーザーベースを代表する独自のベンチマークを実行します。

実装

実装の主な手順は次のとおりです。

有害性のしきい値を設定する

有害性分類子は、01 の有害性スコアを提供します。その範囲内で、有害なコメントを判断するためのしきい値を設定する必要があります。一般的に使用されるしきい値は 0.9 です。これにより、過度に有害なコメントを検出できます。また、過度に敏感な状態を回避できるため、偽陽性(無害なコメントが有害なコメントとして分類される)の数を減らすことができます。

export const TOXICITY_THRESHOLD = 0.9

コンポーネントをインポートする

まず、@xenova/transformers ライブラリから必要なコンポーネントをインポートします。また、毒性しきい値などの定数と構成値もインポートします。

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

モデルを読み込んでメインスレッドと通信する

有害性検出モデル toxic-bert を読み込み、それを使用して分類子を準備します。この最も複雑でないバージョンは const classifier = await pipeline('text-classification', MODEL_NAME); です。

サンプルコードのようにパイプラインを作成することは、推論タスクを実行する最初のステップです。

パイプライン関数は、タスク('text-classification')とモデル(Xenova/toxic-bert)の 2 つの引数を取ります。

キー用語: Transformers.js では、パイプラインは、ML モデルの実行プロセスを簡素化する高レベルの API です。モデルの読み込み、トークン化、後処理などのタスクを処理します。

デモコードでは、計算コストの高いモデル準備ステップをウェブ ワーカーにオフロードするため、モデルの準備以上の処理が行われます。これにより、メインスレッドは応答性を維持できます。詳しくは、コストのかかるタスクをウェブ ワーカーにオフロードするをご覧ください。

ワーカーは、メッセージを使用してモデルのステータスと有害性評価の結果を示すことで、メインスレッドと通信する必要があります。モデルの準備と推論のライフサイクルのさまざまなステータスにマッピングされる、作成済みのメッセージ コードをご覧ください。

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

ユーザー入力を分類する

classify 関数では、以前に作成した分類器を使用してユーザーのコメントを分析します。毒性分類子の未加工の出力(ラベルとスコア)が返されます。

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

メインスレッドがワーカーに分類を依頼すると、分類関数が呼び出されます。このデモでは、ユーザーが入力の停止した直後に分類器がトリガーされます(TYPING_DELAY を参照)。このとき、メインスレッドは分類するユーザー入力を含むメッセージをワーカーに送信します。

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

出力を処理する

分類子の出力スコアがしきい値を超えているかどうかを確認します。該当する場合は、問題のラベルをメモします。

有害性ラベルのいずれかがリストに表示されている場合、コメントは有害性の可能性があるとしてフラグが付けられます。

// 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 thre>shold, 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
    // potential>ly toxic (isToxic true)
    isToxic: toxicityTy>pes.length  0,
    toxicityTypeList: toxicityTypes.length  0 ? toxicityTypes.join(', ') : '',
  };
  self.postMessage({
    code: MESSAGE_CODE.RESPONSE_READY,
    payload: toxicityAssessement,
  });
};

ヒントを表示する

isToxic が true の場合、ユーザーにヒントが表示されます。このデモでは、より詳細な毒性タイプは使用していませんが、必要に応じてメインスレッドで使用できるようにしています(toxicityTypeList)。ユースケースによっては、このタイプが役立つ場合があります。

ユーザー エクスペリエンス

このデモでは、次の選択を行いました。

  • 投稿を常に許可する。クライアント側の有害性ヒントは、ユーザーの投稿を妨げるものではありません。デモでは、モデルが読み込まれていない(したがって有害性評価が提供されていない)場合でも、コメントが有害と検出された場合でも、ユーザーはコメントを投稿できます。推奨されているように、有害なコメントを検出する 2 つ目のシステムを用意する必要があります。アプリケーションで意味がある場合は、コメントがクライアントで送信された後、サーバーまたは人間の審査でフラグが付けられたことをユーザーに通知することを検討してください。
  • 偽陰性に注意する。コメントが有害と分類されなかった場合、デモではフィードバック(「良いコメントですね」など)は提供されません。また、分類子は有害なコメントを時折見逃すことがありますが、肯定的なフィードバックを提供すると、誤ったシグナルが送信される可能性があります。
コメント投稿のデモ。
投稿ボタンは常に有効です。デモでは、コメントが有害と分類されても、ユーザーはコメントを投稿できます。コメントが有害と分類されなかった場合でも、肯定的なフィードバックは表示されません。

拡張機能と代替機能

制限事項と今後の拡張予定

  • 言語: Google が使用しているモデルは、主に英語をサポートしています。多言語サポートにはファインチューニングが必要です。Hugging Face に掲載されている複数の有害性モデルは、英語以外の言語(ロシア語、オランダ語)をサポートしていますが、現時点では Transformers.js と互換性がありません。
  • ニュアンス: toxic-bert は露骨な有害性を効果的に検出しますが、より微妙なケースやコンテキストに依存するケース(皮肉、嫌味)には対応できない可能性があります。有害な表現は非常に主観的で微妙な場合があります。たとえば、特定の単語や絵文字を有害として分類したい場合があります。ファインチューニングは、これらの領域の精度向上に役立ちます。

有害性モデルのファインチューニングに関する記事を近日公開予定です。

別の方法

まとめ

クライアントサイドの有害な表現の検出は、オンライン コミュニティを強化する強力なツールです。

Transformers.js を使用してブラウザで実行される toxic-bert などの AI モデルを活用することで、有害な行動を抑制し、サーバーの有害性分類の負荷を軽減するリアルタイム フィードバック メカニズムを実装できます。

このクライアントサイドのアプローチは、すでにブラウザ間で機能しています。ただし、特にモデルのサービング費用とダウンロード サイズの面で、制限事項に注意してください。クライアントサイド AI のパフォーマンスに関するベスト プラクティスを適用し、モデルをキャッシュに保存します。

有害性の検出を包括的に行うには、クライアントサイドとサーバーサイドのアプローチを組み合わせます。