发布时间:2024 年 11 月 13 日
仇恨言论、骚扰和网络辱骂已成为普遍存在的在线问题。有害评论让重要的声音沉默,并赶走用户和客户。毒性检测可以保护您的用户,并创建更安全的在线环境。
在本系列的两部分中,我们将探讨如何使用 AI 在源头(用户的键盘)检测和减轻毒性。
在第一部分中,我们讨论了这种方法的使用场景和优势。
在第二部分中,我们将深入探讨实现细节,包括代码示例和用户体验技巧。
演示和代码
体验我们的演示,并研究 GitHub 上的代码。

浏览器支持
我们的演示在最新版本的 Safari、Chrome、Edge 和 Firefox 中运行。
选择模型和库
我们使用 Hugging Face 的 Transformers.js 库,该库提供用于在浏览器中使用机器学习模型的工具。我们的演示代码源自此文本分类示例。
我们选择 toxic-bert 模型,这是一种预训练模型,旨在识别有害语言模式。它是 unitary/toxic-bert 的 Web 兼容版本。有关模型标签及其身份攻击分类的更多详细信息,请参阅 Hugging Face 模型页面。

模型下载后,推理速度很快。
例如,在我们测试过的中端 Android 设备(普通的 Pixel 7 手机,而不是性能更高的 Pro 型号)上运行 Chrome 时,通常不到 500 毫秒。运行您自己的基准测试,以代表您的用户群。
实现
以下是我们实现的关键步骤
设置毒性阈值
我们的毒性分类器提供 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';
加载模型并与主线程通信
我们加载毒性检测模型 toxic-bert,并使用它来准备我们的分类器。最简单的版本是 const classifier = await pipeline('text-classification', MODEL_NAME);
像示例代码中那样创建管道是运行推理任务的第一步。
管道函数接受两个参数:任务 ('text-classification'
) 和模型 (Xenova/toxic-bert
)。
关键术语:在 Transformers.js 中,管道是一个高级 API,可简化运行 ML 模型的过程。它处理模型加载、分词和后处理等任务。
我们的演示代码不仅仅是准备模型,因为我们将计算量大的模型准备步骤卸载到 Web Worker。这使主线程保持响应。了解有关将昂贵的任务卸载到 Web Worker的更多信息。
我们的 Worker 需要与主线程通信,使用消息来指示模型的状态和毒性评估的结果。查看我们创建的消息代码,这些代码映射到模型准备和推理生命周期的不同状态。
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 执行分类时,我们会调用 classify 函数。在我们的演示中,一旦用户停止输入(请参阅 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 有效地检测到明显的毒性,但它可能难以处理更微妙或依赖上下文的情况(讽刺、嘲讽)。毒性可能非常主观和微妙。例如,您可能希望将某些术语甚至表情符号归类为有害。微调可以帮助提高这些领域的准确性。
我们即将发布一篇关于微调毒性模型的文章。
替代方案
- 用于文本分类的 MediaPipe。确保使用与分类任务兼容的模型。
TensorFlow.js 毒性分类器。它提供了一个更小、获取速度更快的模型,但它已经有一段时间没有优化了,因此您可能会注意到推理速度比使用 Transformers.js 稍慢。
结论
客户端毒性检测是增强在线社区的强大工具。
通过利用像 toxic-bert 这样的 AI 模型(在浏览器中与 Transformers.js 一起运行),您可以实现实时反馈机制,以阻止有害行为并减少服务器上的毒性分类负载。
这种客户端方法已经在各种浏览器中运行。但是,请记住其局限性,尤其是在模型服务成本和下载大小方面。应用客户端 AI 的性能最佳实践并缓存模型。
为了实现全面的毒性检测,请结合使用客户端和服务器端方法。