パート 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 ミリ秒未満で完了します。ユーザーベースを代表する独自のベンチマークを実行します。

実装

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

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

YouTube の有害性分類システムは、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 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,
  });
};

ヒントを表示する

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

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

このデモでは、次のように選択しました。

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

改善と代替案

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

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

有害性モデルのファインチューニングについては、今後の記事で説明します。

代替

まとめ

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

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

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

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