Bloom嵌入与未见实体的故事
Bloom嵌入简介
Bloom嵌入提供了一种内存高效的方式来表示大量词元。为了对其进行测试,我们在多种语言的命名实体识别数据集上,将Bloom嵌入与传统嵌入进行了同类对比实验。我们撰写了一篇技术报告,本文将重点介绍其中的一些实验结果,并特别关注NER领域的一个关键问题:训练集中不存在的实体,即未见实体。
如果您主要是来寻找实用工具的,可以直接查看 span-labeling-datasets 项目。它提供了下载和预处理多个数据集的命令,以便与 ner 和 spancat 组件一起使用。在此基础上,您还可以查看 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.py 和 collate_results.py,以便能够使用单个脚本,在多个数据集上、跨多个随机种子运行具有多种超参数组合的多种架构,并在运行后整理所有结果以供检查。我们推荐您查看这些实用工具,并希望它们能通过精确的实验帮助您更好地了解您的流程的性能!
顺便提一下:我们未来可能会在 thinc 中添加一个确定性初始化方案,但这一方向的研究仍处于早期阶段。关于随机性在计算机视觉中影响的一篇有趣的论文,"Torch.manual_seed(3407) is all you need" 是一篇适合早晨喝咖啡时阅读的轻松文章。
内存优化能到何种程度?
为了探究Bloom嵌入在节省内存方面的潜力,我们在内存受限的情况下,将 MultiHashEmbed 与 MultiEmbed 在西班牙语CoNLL 2002和荷兰语考古学NER数据集上进行了直接对比。我们报告了不同设置下的结果:
- “Bloom”列使用某机构库中的默认行数。
- “Traditional”列使用传统嵌入,为语料库中出现至少10次的每个符号分配一个向量。
- 在“Bloom 10%”和“Bloom 20%”列中,我们仅使用传统嵌入可用向量数量的报告百分比。
不同架构下,每个数据集上每个特征对应的向量数量如下表所示。前两行显示了 MultiEmbed 在不同数据集上的向量表行数。MultiHashEmbed 行显示了我们用于技术报告的某机构库3.4版本中的默认行数。
| 数据集 | NORM | PREFIX | SUFFIX | SHAPE |
|---|---|---|---|---|
| 西班牙语CoNLL | 2635 | 80 | 1147 | 88 |
| 荷兰语考古学 | 3132 | 104 | 1500 | 174 |
| MultiHashEmbed | 5000 | 2500 | 2500 | 2500 |
下表显示了使用不同嵌入架构和嵌入数量训练的相同NER流程的F1分数:
| 数据集 | Bloom | Traditional | Bloom 20% | Bloom 10% |
|---|---|---|---|---|
| 西班牙语CoNLL | 0.77 | 0.79 | 0.78 | 0.78 |
| 荷兰语考古学 | 0.83 | 0.83 | 0.82 | 0.80 |
我们看到,在西班牙语CoNLL上,结果基本不受影响;在荷兰语考古学上,当仅使用10%的向量时,模型性能似乎略有下降。总体而言,即使使用的向量数量减少十倍,基于Bloom嵌入构建的NER流程在与使用传统嵌入的流程竞争时仍保持竞争力。这一结果与我们之前比较 floret 和 fastText 向量时的发现一致。事实证明,你可以将数量降得非常低!
尽管对于如此小的词表大小,内存使用已经非常小,但为了完整起见,我们还是来计算一下。在GPU上训练时,我们使用 float32,在CPU上使用 float64。嵌入表的宽度默认为96。我们可以使用 numpy 来计算数组占用的兆字节数。例如,对于 MultiEmbed:
下表报告了在荷兰语考古学数据集上,各种架构的内存使用情况:
| 嵌入器 | float32 | float64 |
|---|---|---|
| MultiEmbed | 1.885 | 3.771 |
| Bloom | 4.8 | 9.6 |
| Bloom 20% | 0.377 | 0.754 |
| Bloom 10% | 0.189 | 0.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分数):
| 数据集 | 全部 | 未见 |
|---|---|---|
| 荷兰语CoNLL | 0.83 | 0.70 |
| AnEM | 0.61 | 0.35 |
| OntoNotes | 0.84 | 0.65 |
总的来说,我们发现Bloom嵌入的F1分数显著下降,并且我们在传统嵌入中也观察到了相同的模式:
| 数据集 | 全部 | 未见 |
|---|---|---|
| 荷兰语CoNLL | 0.84 | 0.73 |
| AnEM | 0.65 | 0.32 |
| OntoNotes | 0.83 | 0.66 |
这是NER系统中普遍存在的模式:即使在BioBERT时代,生物医学命名实体识别器也更擅长记忆而不是泛化。然而,值得注意的是,在某些数据集中,仅依赖上下文线索时,人类似乎也难以正确分类实体。
为了帮助避免意外并更好地评估NER流程,我们在 span-labeling-datasets 项目中包含了 generate-unseen 命令,我们用它来创建已见和未见的诊断集。
正交特征的影响
我们感兴趣的是,添加多个正交特征是否会转化为命名实体识别性能的提升,尤其是在处理未见实体时。这里我们以荷兰语CoNLL数据集为例,报告当我们移除特征时,以百分点衡量的相对错误率增加。这是一种常见的方式,用于比较完整模型(第一行)与消融模型(其余行)的性能。我们计算每个模型的F1分数,并报告 -(消融模型 - 完整模型) / (1 - 完整模型)。例如,如果完整模型的分数是83% F1,而消融模型只达到73% F1,那么相对错误率增加是58%。“全部”列报告了在整个数据集上的结果,而“已见”和“未见”列分别仅针对训练期间已见或未见的实体。
| ORTH | NORM | PREFIX | SUFFIX | SHAPE | 全部 | 已见 | 未见 |
|---|---|---|---|---|---|---|---|
| ✅ | ✅ | ✅ | ✅ | - | - | - | - |
| ✅ | ✅ | ✅ | ✅ | 17% | 0% | 15% | |
| ✅ | ✅ | 30% | 80% | 28% | |||
| ✅ | 47% | 100% | 68% | ||||
| ✅ | 50% | 160% | 62% |
上表第一行是某机构库嵌入层的默认设置,利用了所有四个特征。接下来的三行逐一移除了每个特征,而最后一行使用了词元的原始拼写表面形式。最后一行对应于仅使用原始拼写形式。
我们发现,SHAPE 信息似乎主要对未见实体有益,而其余特征对已见和未见实体都至关重要。总的来说,随着我们移除特征,我们确实看到已见实体的错误率增加更多,因为这些是模型首先倾向于捕获的特征。
预训练向量的价值
让我们研究一下预训练向量在多大程度上可以缓解未见实体问题。以下是我们实验中的发现(F1分数):
| 数据集 | 全部实体 | 全部实体 + lg | 未见实体 | 未见实体 + lg |
|---|---|---|---|---|
| 荷兰语CoNLL | 73% | 83% | 59% | 70% |
| 西班牙语CoNLL | 77% | 82% | 60% | 74% |
| 荷兰语考古学 | 82% | 83% | 35% | 40% |
| AnEM | 54% | 61% | 21% | 35% |
| OntoNotes | 81% | 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