使用BERT进行社会偏见命名实体识别(NER)

在上一篇博客中,我们了解到将整个句子分类为“有偏见”或“公平”可能过于抽象,不利于有效训练。相反,如果我们将词语标注为语义词性,例如:概括、不公平和刻板印象,会怎么样?
在本文中,我将介绍如何构建一个用于社会偏见实体的命名实体识别(NER)模型,这是我们Ethical Spectacle Research GUS-Net论文的核心贡献之一,该论文将于2024年9月发表 ;)。
笔记本:✍️合成数据标注ipynb | 🏫️模型训练ipynb 相关活动:📅社会偏见黑客马拉松(24年9月) | ☕️编程工作坊
合成数据集
为了训练BERT将词语/标记分类为概括、不公平或刻板印象,我们需要一个词语已被标记为我们实体的 S 数据集……问题是,目前不存在包含我们实体的数据集。
为了从头开始构建我们的数据集,我们必须构建一个标注管道。过去,这可能需要人工完成,但多亏了像 LangChain 或 Stanford DSPy 这样的语言模型框架,我们可以构建一个类似的标注代理团队。
对于我们的任务,我们将使用 DSPy 创建语言模型程序(即代理),以便单独标注句子中的每个实体。然后,我们将它们聚合到一个列表的列表中,其中每个子列表包含每个词的实体标签。以下是我们合成数据管道输出的标签示例
我们已标注了3.5k条记录(句子,包含不同的偏见/公平情感和不同的目标),其标签如上图所示。现在我们可以继续构建一个模型,该模型可以从我们的数据集中学习,并用我们的实体标记未见句子中的词语。
注意:我们仍在犹豫是否发布此数据集,因为它可能会被滥用。相反,您可以使用我们的管道在此笔记本中标注您自己的数据集。它使用了已编译的DSPy模块,这些模块在每个提示中都有实体标签示例,并有护栏(建议/重试逻辑)。
模型和训练架构
要查看我们用于训练此模型的代码,请打开此笔记本。请注意,如果您不加载数据集(请参阅上一节),则无法运行它。
我们将训练 HuggingFace transformers 库中的 BertForTokenClassification 模块。它将加载一个模型并允许我们将其用作 PyTorch.nn 模块,并根据我们指定的类别数量(每个实体2个类别 + 1个“O”类别)自动配置。
标记分类:构成每个文本序列的标记列表将传递给 BertForTokenClassification,它将并行处理每个标记的单独分类。本质上,您可以将其视为对序列的每个标记进行多标签分类(其中每个标记可以属于多个类别)。BERT 为每个标记创建的编码仍然包含两侧标记(即上下文)的信息。值得注意的是,通常情况下,标记分类是通过多类别分类完成的,其中每个标记只能分配一个标签。
损失函数:这里开始变得有点奇怪。我们用多个潜在标签的列表来标记每个标记。这意味着在处理每个序列时,我们的预测输出实际上是一个列表的列表(二维张量),看起来像这样
- 分词文本:['nursing', 'homes', 'are', ...]
- 标签:[[0,0,0,1,0,0,0,0,0], [0,0,0,0,1,0,0,0,1], [1,0,0,0,0,0,0,0,0],...]
为了计算损失,我们将预测标签张量和真实标签张量都降维到一维,像这样
- 标签:[0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,0...]
由于它们的长度仍然相等,我们可以应用二元交叉熵并计算整个序列的损失。
值得了解的是:BERT 的分词器会应用填充标记,使 BERT 的每个输入都是固定大小的向量(在我们的例子中,长度为 128 个标记)。我们还需要将真实标签的第一个维度填充到相同大小,但用 -100 填充,以便在损失计算中忽略。这是在“分词和对齐标签”预处理步骤中完成的。
优化器
我们将使用 AdamW,这在训练 BERT 时很常见。它会将我们的学习率应用于从损失计算出的梯度(在反向传播期间),以便在每次训练批次后更新我们模型的权重。它还在计算权重时实现了权重衰减和动量平滑。
学习率调度器
通过使用带有预热的线性调度,我们将避免在模型看到训练数据之前对权重进行大的调整,并且我们将在训练过程中对权重进行更精确的更改。我们最终将得到一个看起来像这样的学习率调度器
评估指标
评估这个多标签 NER 模型不像序列分类模型那么直接,后者我们有两个二元数据点(即真实标签、预测标签)进行比较。现在,我们有更多的选择……
- 标签级别:在之前的步骤中,我们将模型的输出张量扁平化为一维,这实际上也使其适用于二元评估指标,如准确率。问题在于,这将导致评估数据集中真实负例的数量远超真实正例。我们希望关注真实正例的评估指标,因为大多数时候,并非所有(或任何)实体都会出现。
- 标记级别:相反,我们可以将真实和预测张量保持为二维(即列表的列表),然后精确比较每个列表。这将避免扁平化偏差(大量真实负例),但如果两个标签列表不完全匹配,则被视为失败(即,即使接近也无法获得分数)。
- 实体级别:或者我们可以根据实体的完整性检查实体。通常,我们的实体会跨越多个词,通过解释B-和I-标签,我们可以检查实体是否跨越了正确数量的标记/词。在这种情况下,如果标签没有跨越完整的实体,预测将是不正确的,即使接近也无法获得分数。
为了做出选择,我们首先需要确定一个目标。我们是否关心接近程度?在某些应用中,假阳性非常危险。在我们的例子中,我们可以结合使用多种指标来了解每个级别的情况。
在多标签 NER 中,我们最感兴趣的是测量每个标记中正确预测的标签部分。这为“接近”的预测提供了分数。
汉明损失
我认为汉明损失是标签级别和标记级别评估的混合。它比较每个标记的两个标签列表,返回不正确的标签部分。在这种情况下,汉明损失低于我们精确比较列表的情况。
- 真实值:[0,0,0,0,0,1,0,1,0]
- 预测值:[0,0,0,0,0,1,0,0,0] # 9个标签中有1个不正确
- 精确匹配损失:1 (100% 不正确),汉明损失:0.1111 (11% 不正确) 我喜欢用汉明损失进行此评估,因为它与实体完整性间接相关,就像实体级别评估一样(但无需处理 BIO 格式)。
请记住,我们正在比较文本序列中每个标记的标签列表,就像上面所示的那些(想象它一次处理一个词的句子)。幸运的是,汉明损失可以处理这个问题,并将提供序列中所有标记产生的损失的宏观平均值。
精确率、召回率、F1分数
幸运的是,torchmetrics 拥有多标签指标,可以同时计算我们所有单个实体的召回率、精确率和 F1 分数。我们可以分离出我们潜在标签列表中每个实体的指标,或将它们平均在一起。
- 精确率 = TP / TP + FP - 正向预测中实际正确的比例。
- 召回率:TP / TP + FN - 模型正确预测的真实正例的比例。
- F1 分数:2 x ((精确率 x 召回率) / (精确率 + 召回率)) - 调和平均值,可以将其视为平衡真实负例和真实正例的准确率。
这些分数将很好地帮助我们理解模型的实体预测效果(而不是不预测实体的效果)。在评估中,我们将查看整个模型(所有实体平均)的宏观分数。
混淆矩阵
主要因为它们很酷,但也能帮助我们直观地比较每个实体的个体表现,我们将在评估期间制作混淆矩阵。我将合并 B- 和 I- 标签的输入,这样我们将只有 4 个矩阵:“O”、“GEN”、“UNFAIR”、“STEREO”。每个矩阵将描绘真实正例和负例的比率,尽管我们应该预期会有很多真实负例。这是一个混淆矩阵的分解键
训练模型
预处理:
分词:在开始训练之前,我们必须将训练文本序列预处理成 BERT 已经训练过的词元列表。分词器还会将输入填充到设定的最大词元长度,从而使 BERT 的每个句子都具有相同的长度。我们正在构建一个句子级别的 NER 模型,因此我们将使用 128 作为最大长度。
对齐标签:我们训练集中的标签是针对每个单词的。然而,在分词过程中,许多单词被分割成子词。我们还必须正确解析训练集中的标签列表,确保作为子词一部分的标记继承其父单词的标签。
然后我们将这些重新定义为我们上一步看到的列表的列表,其中序列中的每个标记都具有一个固定大小的标签向量。最后,我们需要通过添加与填充标记对齐的填充标签,使标签向量的长度与标记的固定大小向量相同。我们可以使用 -100 的标签向量,这将在损失计算期间被忽略。
值得注意:您需要小心每个序列的第一个标记 CLS(由 BERT 分词器添加)。如果您不在标签前添加 -100 的向量,它可能会使您的标签不对齐。笔记本根据 word_ids(由分词器输出)对齐标记和标签,word_ids 以相同的方式表示 CLS 和填充标记:None。这可以用作掩码,用于为我们希望忽略的标记添加 -100 的列表。
基线:
我所见过的最相似的 NER 模型是 Nbias,它将标记标记为单个实体:BIAS。这与我们的模型不够相似,无法有效比较,因此我将猜测一些训练参数,并将其结果用作我们可以继续优化的基线指标。
结果解读:开局不错!我们的关键指标汉明损失,基线值很低,为 0.1137(11% 的预测不正确)。然而,我们可以看到这并不意味着我们完全准确地预测了实体。我们的模型检测到了 43% 的潜在存在(即召回率)。另一方面,当它标记为“存在”时,其中 65% 是正确的(即精确率)。总而言之,这些分数还不算差。一个重要的地方是混淆矩阵。正如预期,它们严重偏向于“不存在”(不包括“O”)。也许可以预见,它们偏向于不预测存在。所有新实体都有更多的假阴性而非假阳性。事实上,UNFAIR 实体甚至没有一个正向预测!!
促进实体预测
增加正向分类的明显方法是降低概率变为标签的阈值。我将节省您一些时间,我尝试了几个不同的阈值,0.5 似乎是最好的。降低阈值并没有提高精确率或召回率,它增加了一些真阳性,但增加了更多的假阳性。简单的阈值更改无法解决问题。
相反,我们可以修改损失函数。我首先创建了一个忽略掩码(一个与要忽略的标记/标签对应的列表),并将其定义为忽略训练集中所有带有“O”标签的标记。这导致实体预测(不包括“O”)急剧增加,但假阳性仍然过多,最终导致整体指标更差。我们还有另一个巧妙的选择:Focal Loss。
Focal Loss 将应用二元交叉熵,但带有一个修正因子,该因子根据预测概率调整损失。自信的预测对总损失的影响较小,反之,低自信的预测(无论是正确还是错误)对损失的影响较大。
Focal Loss 的实现
在笔记本中,您会找到一个我从 pytorch 论坛上的几个示例中拼凑出来的 focal loss 实现。我们的实现
- 创建活跃标记/标签的掩码(即,非 CLS/SEP/PAD/-100)。
- 执行 BCE(预测 logits 对比真实标签)。
- 将预测 logits 转换为概率。
- 计算每个类别存在的概率(p_t)——用于识别哪些标签
- 是最难预测的。
- 计算调制因子 alpha_t 和焦点调制。
- 将 alpha_t、焦点调制和 BCE 损失相乘。
- 将 CLS、SEP、PAD 或 -100 标记的损失归零(使用掩码)。
- 对活跃损失值求和。
我们可以通过这两个参数调整我们的焦点损失
- alpha - 强调正向预测。
- gamma - 强调“更难”的实体(较低概率)。
尝试了几个不同的 alpha 和 gamma 值后,以下是使用 alpha = 0.75 和 gamma = 3 进行训练的结果
结果解读:成功了!!全面都有很大的改进。我们检测到总存在量的 79%,而且我们的正向预测有 73% 是正确的!!!对我来说,这是一个很好的迹象,表明我们找到了正确的损失函数。然而,我们仍然无法准确预测“UNFAIR”实体,因此可能值得重新审视 gamma,并可能增加它。
求和与平均池化损失
如果我们不将所有损失值相加,而是找到每个标签的损失平均值,会怎么样?这仍然会编码相同的信息,但会平滑异常值,并使损失在批次之间保持一致的比例。这在损失方差较高的情况下尤其重要,例如在使用焦点损失或多标签分类时常见的情况。切换到平均池化(所有训练参数相同)后,准确率再次提高!
训练备注:
我只是通过在几个句子上手动尝试模型,就注意到我们经常为同一个标记同时预测 B- 和 I- 标签实体。这让我思考,模型是否找到了一个巧妙的方法,通过为实体预测两种标签来达到最低损失分布。我将 alpha 降低到 0.65,这似乎清除了重复的标签。
作为一项健全性检查,我尝试了几个不同的批量大小和学习率。没有一项改进,但更高的学习率显示出与我最终使用的 5e-5 一样有效的潜力。
结论
在以上章节中,我们
- 构建了一个合成标注的数据集,包含社会偏见实体:概括(GEN)、不公平(UNFAIR)和刻板印象(STEREO)。
- 优化了 BERT 的多标签标记分类的训练和评估。
- 使用焦点损失训练模型,以解决训练数据中标签表示不均衡和我们的多标签方法的问题。
成功了!!我们的模型很好地拟合了训练数据,测试集上只有 6.6% 的标签预测不正确(汉明损失)!更重要的是,它检测到了 76% 的潜在实体存在(召回率)。最后,当模型将一个实体标记为存在时,其准确率为 82%(精确率)。
尽管它增加了训练过程的复杂性,但进行多标签标记分类(而不是多类别)为我们带来了一些独特的结果。我们可以看到嵌套的实体和模式的出现。我注意到刻板印象通常包含嵌套的概括,并被赋予一些不公平的特质。这是一个明确偏见的语句示例。
资源:
构建您自己的 NER 模型:您可以按照本指南训练您自己的实体模型,或您自己的定义模型。这是构建数据集的笔记本💻,这是训练的笔记本💻。[🎨HuggingFace Space](https://huggingface.co/spaces/maximuspowers/bias-detection-ner) | 🔬模型库 | ⚠️偏见的二元序列分类