Bloom嵌入:内存高效NLP技术深度解析

2 阅读12分钟

Bloom嵌入与未见实体的故事

Bloom嵌入简介

Bloom嵌入提供了一种内存高效的方式来表示大量词元。为了对其进行测试,我们在多种语言的命名实体识别数据集上,将Bloom嵌入与传统嵌入进行了同类对比实验。我们撰写了一篇技术报告,本文将重点介绍其中的一些实验结果,并特别关注NER领域的一个关键问题:训练集中不存在的实体,即未见实体。

如果您主要是来寻找实用工具的,可以直接查看 span-labeling-datasets 项目。它提供了下载和预处理多个数据集的命令,以便与 nerspancat 组件一起使用。在此基础上,您还可以查看 ner-embeddings 项目,它允许您运行我们在技术报告中用 ner 进行的所有实验。本文将介绍这些项目的功能特性。

Bloom嵌入原理解析

首先,我们快速介绍一下某机构库中的嵌入架构。传统嵌入为词表中的每个唯一符号分配一个专属向量。通常,我们会有一个类似Python字典的词表 Dict[str, int],将词元映射为整数索引。这些整数用于索引一个向量表 E,该表有 len(词表) 行,对应词表中的每个词。对于没有对应向量的词元,我们统一映射到一个未知词元向量上。

Bloom嵌入的目标是减少 E 的行数,从而降低嵌入矩阵的内存占用。为此,它借鉴了Bloom过滤器(Bloom filter)的思想——因此得名🌸。不是查找单个向量,而是将每个符号哈希四次,并将得到的嵌入向量求和。假设有一个嵌入矩阵 B,那么一个词元的Bloom嵌入计算方式如下:

height, width = B.shape
result = np.zeros((width, ))
for hash in hash_funcs:
    idx = hash(token) % height
    result += B[idx]

通过这种方式,我们减少了表的行数,同时仍有极大概率能为每个词元计算出唯一的向量。关于碰撞概率的分析,请参阅技术报告的第3.2节。

常规嵌入 vs. 哈希嵌入

除了使用哈希技巧,某机构库中嵌入层的另一个特点是:它们并不直接嵌入词元的原始拼写形式,而是嵌入其归一化形式(NORM)、首字符(PREFIX)、最后四个字符(SUFFIX)和形状特征(SHAPE)的组合。完整的嵌入架构被称为 MultiHashEmbedding

为了实现同类对比,ner-embeddings 项目包含了一个名为 MultiEmbed 的架构,它是 MultiHashEmbed 的一个版本,保留了 MultiHashEmbed 中的所有正交特征,但将Bloom嵌入替换为了传统嵌入。

如果您有兴趣尝试,首先需要在数据集上运行 make-tables 命令来创建查找表。这是设置流程的一部分,可以通过 python -m spacy project run setup 运行。表创建完成后,可以通过在配置文件中将其添加为嵌入器,将 MultiEmbed 引入到流程中:

[components.tok2vec.model.embed]
@architectures = "某机构.MultiEmbed.v1"
attrs = ["NORM","PREFIX","SUFFIX","SHAPE"]
width = ${components.tok2vec.model.encode.width}
include_static_vectors = true
unk = 0

在初始化模型之前,还需要告知某机构库包含映射表:

[initialize.before_init]
@callbacks = "set_attr"
path = ${paths.tables}
component = "tok2vec"
layer = "multiembed"
attr = "tables"

set_attr 回调函数定义在 set_attr.py 中,可用于在组件创建之后、初始化之前将数据注入某机构库组件。

ner-embedding 项目还包含了另一个嵌入层变体 MultiFewerHashEmbed,我们用它来改变嵌入层可用哈希函数数量进行实验。本文不包含这些结果,您可以在技术报告的第5.4节中查看。

便捷的实验运行

本文及技术报告中报告的所有性能指标,都是使用三个随机种子运行结果的平均值。随机性通过随机初始化、Dropout掩码的随机生成、数据的随机排序等方式进入深度学习架构的训练过程。我们发现方差非常低。尽管如此,为了增强结果的稳健性,我们建议在可能的情况下使用多个种子。

为了方便起见,我们在 ner-embeddings 项目中实现了 run_experiments.pycollate_results.py,以便能够使用单个脚本,在多个数据集上、跨多个随机种子运行具有多种超参数组合的多种架构,并在运行后整理所有结果以供检查。我们推荐您查看这些实用工具,并希望它们能通过精确的实验帮助您更好地了解您的流程的性能!

顺便提一下:我们未来可能会在 thinc 中添加一个确定性初始化方案,但这一方向的研究仍处于早期阶段。关于随机性在计算机视觉中影响的一篇有趣的论文,"Torch.manual_seed(3407) is all you need" 是一篇适合早晨喝咖啡时阅读的轻松文章。

内存优化能到何种程度?

为了探究Bloom嵌入在节省内存方面的潜力,我们在内存受限的情况下,将 MultiHashEmbedMultiEmbed 在西班牙语CoNLL 2002和荷兰语考古学NER数据集上进行了直接对比。我们报告了不同设置下的结果:

  • “Bloom”列使用某机构库中的默认行数。
  • “Traditional”列使用传统嵌入,为语料库中出现至少10次的每个符号分配一个向量。
  • 在“Bloom 10%”和“Bloom 20%”列中,我们仅使用传统嵌入可用向量数量的报告百分比。

不同架构下,每个数据集上每个特征对应的向量数量如下表所示。前两行显示了 MultiEmbed 在不同数据集上的向量表行数。MultiHashEmbed 行显示了我们用于技术报告的某机构库3.4版本中的默认行数。

数据集NORMPREFIXSUFFIXSHAPE
西班牙语CoNLL263580114788
荷兰语考古学31321041500174
MultiHashEmbed5000250025002500

下表显示了使用不同嵌入架构和嵌入数量训练的相同NER流程的F1分数:

数据集BloomTraditionalBloom 20%Bloom 10%
西班牙语CoNLL0.770.790.780.78
荷兰语考古学0.830.830.820.80

我们看到,在西班牙语CoNLL上,结果基本不受影响;在荷兰语考古学上,当仅使用10%的向量时,模型性能似乎略有下降。总体而言,即使使用的向量数量减少十倍,基于Bloom嵌入构建的NER流程在与使用传统嵌入的流程竞争时仍保持竞争力。这一结果与我们之前比较 floretfastText 向量时的发现一致。事实证明,你可以将数量降得非常低!

尽管对于如此小的词表大小,内存使用已经非常小,但为了完整起见,我们还是来计算一下。在GPU上训练时,我们使用 float32,在CPU上使用 float64。嵌入表的宽度默认为96。我们可以使用 numpy 来计算数组占用的兆字节数。例如,对于 MultiEmbed

下表报告了在荷兰语考古学数据集上,各种架构的内存使用情况:

嵌入器float32float64
MultiEmbed1.8853.771
Bloom4.89.6
Bloom 20%0.3770.754
Bloom 10%0.1890.377

节省几兆字节并不算什么大事,但在 floret 中使用相同的技术,可以将 fastext 约3GB的内存消耗降低到约300MB。

然而,我们在NER实验期间注意到的一个相关细节是,对于我们考虑的数据集和语言,MultiHashEmbed 的默认行数太多了。由于Bloom嵌入似乎对行数具有鲁棒性,从某机构库v.3.5开始,默认的 tok2vec 架构 HashEmbedCNN 简化了设置,为所有四个特征使用2000行。这可能看起来仍然太多,但像中文这样的语言有大量前缀,而基于社交媒体数据的数据集可能有大量形状,例如:WNUT2017基准测试有2103个形状。

警告:未见实体!

评估NER流程的标准方法是创建一个大型数据集,然后随机将其分割为训练集、开发集和测试集。然而,这种场景可能会高估真实的泛化能力,尤其是在未见实体上,即训练集中不存在的实体。这是评估NER流程的一个重要方面,因为许多现实世界的命名实体识别系统的主要目标是识别新的实体。

为了更好地了解不同数据集上的NER性能,我们选取了原始测试集,并创建了一个仅包含训练集中出现的实体的“已见”部分,以及一个互补的“未见”部分。下表展示了我们基于Bloom嵌入的流程,并添加了 en_core_web_lg 流程中的预训练向量后,得到的一部分结果(F1分数):

数据集全部未见
荷兰语CoNLL0.830.70
AnEM0.610.35
OntoNotes0.840.65

总的来说,我们发现Bloom嵌入的F1分数显著下降,并且我们在传统嵌入中也观察到了相同的模式:

数据集全部未见
荷兰语CoNLL0.840.73
AnEM0.650.32
OntoNotes0.830.66

这是NER系统中普遍存在的模式:即使在BioBERT时代,生物医学命名实体识别器也更擅长记忆而不是泛化。然而,值得注意的是,在某些数据集中,仅依赖上下文线索时,人类似乎也难以正确分类实体。

为了帮助避免意外并更好地评估NER流程,我们在 span-labeling-datasets 项目中包含了 generate-unseen 命令,我们用它来创建已见和未见的诊断集。

正交特征的影响

我们感兴趣的是,添加多个正交特征是否会转化为命名实体识别性能的提升,尤其是在处理未见实体时。这里我们以荷兰语CoNLL数据集为例,报告当我们移除特征时,以百分点衡量的相对错误率增加。这是一种常见的方式,用于比较完整模型(第一行)与消融模型(其余行)的性能。我们计算每个模型的F1分数,并报告 -(消融模型 - 完整模型) / (1 - 完整模型)。例如,如果完整模型的分数是83% F1,而消融模型只达到73% F1,那么相对错误率增加是58%。“全部”列报告了在整个数据集上的结果,而“已见”和“未见”列分别仅针对训练期间已见或未见的实体。

ORTHNORMPREFIXSUFFIXSHAPE全部已见未见
----
17%0%15%
30%80%28%
47%100%68%
50%160%62%

上表第一行是某机构库嵌入层的默认设置,利用了所有四个特征。接下来的三行逐一移除了每个特征,而最后一行使用了词元的原始拼写表面形式。最后一行对应于仅使用原始拼写形式。

我们发现,SHAPE 信息似乎主要对未见实体有益,而其余特征对已见和未见实体都至关重要。总的来说,随着我们移除特征,我们确实看到已见实体的错误率增加更多,因为这些是模型首先倾向于捕获的特征。

预训练向量的价值

让我们研究一下预训练向量在多大程度上可以缓解未见实体问题。以下是我们实验中的发现(F1分数):

数据集全部实体全部实体 + lg未见实体未见实体 + lg
荷兰语CoNLL73%83%59%70%
西班牙语CoNLL77%82%60%74%
荷兰语考古学82%83%35%40%
AnEM54%61%21%35%
OntoNotes81%84%56%65%

总的来说,我们观察到在流程中包含 en_core_web_lg 向量的好处。当考虑未见实体时,性能差异更为显著,尤其是在较大的荷兰语考古学和OntoNotes数据集上。因此,我们建议在可能的情况下,为NER流程使用预训练嵌入。

结语

如果您有兴趣了解更多,可以在我们的技术报告中找到其余结果。您也可以通过 span-labeling-datasets,在我们使用的数据集上尝试您自己的流程。它包含将所有数据集预处理为 .spacy 格式并创建已见/未见诊断分割的实用工具。要获取关于这些数据集的更多信息,可以查看 analyze.py。遗憾的是,由于许可证不允许再分发,我们无法在项目中包含论文中使用的OntoNotes数据集。我们添加了一个技术报告中未使用的数据集,名为MIT Restaurant Reviews。我们很好奇您会在这个数据集上发现什么!ner-embeddings 项目实现了我们用于比较的所有附加架构,以及帮助我们批量运行实验的脚本。

为了更深入地检验命名实体识别器在未见实体之外的稳健性,我们的同事Lj进行了创建具有挑战性的分割和数据扰动的实验,并撰写了一篇关于结果的博客文章。相应的 vs-split 库仍在开发中!

希望您在开发自己的流程或熟悉NER领域时,发现我们的报告和附加工具对您有所帮助。祝您有愉快的一天,并小心那些未见实体!FINISHED