חלק 2: פיתוח זיהוי רעילות באמצעות AI בצד הלקוח

Maud Nalpas
Maud Nalpas

פורסם: 13 בנובמבר 2024

דברי שטנה, הטרדה והתעללות באינטרנט הפכו לבעיה נפוצה באינטרנט. תגובות רעילות משתיקות קולות חשובים ומרחיקות משתמשים ולקוחות. זיהוי התנהגות רעילה מגן על המשתמשים שלכם ויוצר סביבה בטוחה יותר באינטרנט.

בסדרה הזו בת שני החלקים, אנחנו מסבירים איך להשתמש ב-AI כדי לזהות ולצמצם התנהגות רעילה במקור שלה: המקלדות של המשתמשים.

בחלק הראשון, דנו בתרחישי השימוש ובאופן שבו הגישה הזו יכולה להועיל.

בחלק השני של המדריך, אנחנו מתעמקים בהטמעה, כולל דוגמאות קוד וטיפים לחוויית משתמש.

הדגמה וקוד

אתם יכולים להתנסות בהדמו שלנו ולעיין בקוד ב-GitHub.

הדגמה של פרסום תגובה.
כשהמשתמש מפסיק להקליד, אנחנו מנתחים את הרעילות של התגובה שלו. אם התגובה מסווגת כרעילה,
מוצגת אזהרה בזמן אמת.

תמיכה בדפדפנים

הדמו שלנו פועל בגרסאות העדכניות ביותר של Safari,‏ Chrome,‏ Edge ו-Firefox.

בחירת דגם וספרייה

אנחנו משתמשים בספריית Transformers.js של Hugging Face, שמספקת כלים לעבודה עם מודלים של למידת מכונה בדפדפן. קוד ההדגמה שלנו נגזר מהדוגמה הזו לסיווג טקסט.

אנחנו בוחרים במודל toxic-bert, מודל שאומן מראש ומיועד לזיהוי דפוסי שפה רעילים. זו גרסה של unitary/toxic-bert שתואמת לאינטרנט. פרטים נוספים על התוויות של המודל ועל הסיווג שלו של מתקפות גניבת זהות זמינים בדף המודל ב-Hugging Face.

גודל ההורדה של toxic-bert הוא 111MB.

אחרי שמורידים את המודל, ההסקה מהירה.

לדוגמה, בדרך כלל זה לוקח פחות מ-500 אלפיות השנייה ב-Chrome שפועל במכשיר Android ברמת ביניים שנבדק (טלפון Pixel 7 רגיל, לא דגם Pro עם ביצועים טובים יותר). מריצים השוואות ביצועים משלכם שמייצגות את בסיס המשתמשים שלכם.

הטמעה

אלה השלבים העיקריים בהטמעה שלנו:

הגדרת סף רעילות

מסווג הרעילות שלנו מספק ציוני רעילות בין 0 ל-1. בתוך הטווח הזה, אנחנו צריכים להגדיר סף כדי לקבוע מה נחשב לתגובה רעילה. סף נפוץ הוא 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';

טעינת המודל ותקשורת עם ה-thread הראשי

אנחנו טוענים את מודל זיהוי הרעילות toxic-bert ומשתמשים בו כדי להכין את המסווג שלנו. הגרסה הכי פשוטה של זה היא const classifier = await pipeline('text-classification', MODEL_NAME);

יצירת צינור, כמו בקוד לדוגמה, היא השלב הראשון בהרצת משימות של הסקת מסקנות.

פונקציית הצינור מקבלת שני ארגומנטים: המשימה ('text-classification') והמודל (Xenova/toxic-bert).

מונח חשוב: ב-Transformers.js, ‏ pipeline הוא API ברמה גבוהה שמפשט את התהליך של הפעלת מודלים של למידת מכונה. הוא מטפל במשימות כמו טעינת מודלים, טוקניזציה ועיבוד שלאחר מכן.

קוד ההדגמה שלנו עושה קצת יותר מאשר רק הכנת המודל, כי אנחנו מעבירים את השלבים של הכנת המודל שדורשים הרבה משאבי מחשוב לעובד אינטרנט. כך השרשור הראשי נשאר רספונסיבי. מידע נוסף על העברת משימות יקרות ל-Web Worker

ה-Worker צריך לתקשר עם ה-thread הראשי באמצעות הודעות כדי לציין את הסטטוס של המודל ואת התוצאות של הערכת הרעילות. כדאי לעיין בקודי ההודעות שיצרנו, שמשויכים לסטטוסים שונים של הכנת המודל ומחזור החיים של ההסקה.

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

אנחנו קוראים לפונקציית הסיווג שלנו כשהשרשור הראשי מבקש מה-worker לעשות זאת. בהדגמה שלנו, אנחנו מפעילים את המסווג ברגע שהמשתמש מפסיק להקליד (ראו TYPING_DELAY). כשזה קורה, השרשור הראשי שלנו שולח הודעה ל-worker שמכילה את קלט המשתמש לסיווג.

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). יכול להיות שהוא יהיה שימושי לתרחיש השימוש שלכם.

חוויית משתמש

בהדגמה שלנו, בחרנו באפשרויות הבאות:

  • ללא הגבלת שימוש ההצעה שלנו לזיהוי רעילות בצד הלקוח לא מונעת מהמשתמש לפרסם. בהדגמה שלנו, המשתמש יכול לפרסם תגובה גם אם המודל לא נטען (ולכן לא מציע הערכה של רמת הרעילות), וגם אם התגובה מזוהה כרעילה. כמומלץ, כדאי להשתמש במערכת נוספת לזיהוי תגובות רעילות. אם זה מתאים לאפליקציה שלכם, כדאי להודיע למשתמש שהתגובה שלו עברה בצד הלקוח, אבל סומנה בשרת או במהלך בדיקה אנושית.
  • שימו לב לתוצאות שליליות מטעות. אם תגובה לא מסווגת כרעילה, ההדגמה שלנו לא מציעה משוב (לדוגמה, 'תגובה נחמדה!'). בנוסף לכך שהמשוב החיובי עלול להיות מטעה, כי המסווג שלנו מפספס מדי פעם תגובות רעילות.
הדגמה של פרסום תגובה.
הכפתור פרסום תמיד מופעל: בהדגמה שלנו, המשתמש יכול עדיין להחליט לפרסם את התגובה שלו, גם אם היא מסווגת כרעילה. גם אם תגובה לא מסווגת כרעילה, אנחנו לא מציגים משוב חיובי.

שיפורים ואפשרויות חלופיות

מגבלות ושיפורים עתידיים

  • שפות: המודל שבו אנחנו משתמשים תומך בעיקר באנגלית. כדי לקבל תמיכה בכמה שפות, צריך לבצע כוונון עדין. יש כמה מודלים לזיהוי רעילות שמופיעים ב-Hugging Face שתומכים בשפות שאינן אנגלית (רוסית, הולנדית), אבל הם לא תואמים ל-Transformers.js בשלב הזה.
  • ניואנסים: למרות ש-toxic-bert מזהה ביעילות רעלנות גלויה, יכול להיות שהוא יתקשה לזהות מקרים מורכבים יותר או מקרים שתלויים בהקשר (אירוניה, סרקזם). רמת הרעילות יכולה להיות סובייקטיבית ועדינה מאוד. לדוגמה, יכול להיות שתרצו שמונחים מסוימים או אפילו אמוג'י יסווגו כרעילים. כוונון עדין יכול לעזור לשפר את הדיוק בתחומים האלה.

בקרוב נפרסם מאמר בנושא כוונון עדין של מודל רעילות.

אפשרויות אחרות

סיכום

זיהוי רעילות בצד הלקוח הוא כלי יעיל לשיפור הקהילות באינטרנט.

אם משתמשים במודלים של AI כמו toxic-bert שפועלים בדפדפן עם Transformers.js, אפשר להטמיע מנגנוני משוב בזמן אמת שמרתיעים התנהגות רעילה ומפחיתים את העומס של סיווג הרעילות בשרתים.

הגישה הזו בצד הלקוח כבר פועלת בדפדפנים שונים. עם זאת, חשוב לזכור את המגבלות, במיוחד מבחינת עלויות הצגת המודל וגודל ההורדה. מיישמים שיטות מומלצות לשיפור הביצועים של AI בצד הלקוח ושומרים את המודל במטמון.

כדי לזהות רעילות באופן מקיף, מומלץ לשלב בין גישות בצד הלקוח ובצד השרת.