AGI 之 【Hugging Face】 的【多语言命名实体识别】的 [ 多语言Transformer ] / [ 多语言词元化 ] / [命名实体识别] / [ 自定义库模型 ]的简单整理
目录
AGI 之 【Hugging Face】 的【多语言命名实体识别】的 [ 多语言Transformer ] / [ 多语言词元化 ] / [命名实体识别] / [ 自定义库模型 ]的简单整理
六、自定义Hugging Face Transformers库模型类
一、简单介绍
AGI,即通用人工智能(Artificial General Intelligence),是一种具备人类智能水平的人工智能系统。它不仅能够执行特定的任务,而且能够理解、学习和应用知识于广泛的问题解决中,具有较高的自主性和适应性。AGI的能力包括但不限于自我学习、自我改进、自我调整,并能在没有人为干预的情况下解决各种复杂问题。
- AGI能做的事情非常广泛:
跨领域任务执行:AGI能够处理多领域的任务,不受限于特定应用场景。
自主学习与适应:AGI能够从经验中学习,并适应新环境和新情境。
创造性思考:AGI能够进行创新思维,提出新的解决方案。
社会交互:AGI能够与人类进行复杂的社会交互,理解情感和社会信号。
- 关于AGI的未来发展前景,它被认为是人工智能研究的最终目标之一,具有巨大的变革潜力:
技术创新:随着机器学习、神经网络等技术的进步,AGI的实现可能会越来越接近。
跨学科整合:实现AGI需要整合计算机科学、神经科学、心理学等多个学科的知识。
伦理和社会考量:AGI的发展需要考虑隐私、安全和就业等伦理和社会问题。
增强学习和自适应能力:未来的AGI系统可能利用先进的算法,从环境中学习并优化行为。
多模态交互:AGI将具备多种感知和交互方式,与人类和其他系统交互。
Hugging Face作为当前全球最受欢迎的开源机器学习社区和平台之一,在AGI时代扮演着重要角色。它提供了丰富的预训练模型和数据集资源,推动了机器学习领域的发展。Hugging Face的特点在于易用性和开放性,通过其Transformers库,为用户提供了方便的模型处理文本的方式。随着AI技术的发展,Hugging Face社区将继续发挥重要作用,推动AI技术的发展和应用,尤其是在多模态AI技术发展方面,Hugging Face社区将扩展其模型和数据集的多样性,包括图像、音频和视频等多模态数据。
在AGI时代,Hugging Face可能会通过以下方式发挥作用:
模型共享:作为模型共享的平台,Hugging Face将继续促进先进的AGI模型的共享和协作。
开源生态:Hugging Face的开源生态将有助于加速AGI技术的发展和创新。
工具和服务:提供丰富的工具和服务,支持开发者和研究者在AGI领域的研究和应用。
伦理和社会责任:Hugging Face注重AI伦理,将推动负责任的AGI模型开发和应用,确保技术进步同时符合伦理标准。
AGI作为未来人工智能的高级形态,具有广泛的应用前景,而Hugging Face作为开源社区,将在推动AGI的发展和应用中扮演关键角色。
(注意:以下代码运行,可能需要科学上网)
二、多语言命名实体识别
Hugging Face 多语言命名实体识别(NER)是利用 Hugging Face 的预训练模型和工具,对文本中的实体进行识别和分类的一项自然语言处理任务。NER 任务的目标是从文本中自动提取出命名实体,并将这些实体分类为预定义的类别(如人名、地名、组织名等)。
- 什么是命名实体识别(NER)?
命名实体识别是一种信息提取技术,旨在识别并分类文本中的特定名称实体。这些实体通常包括以下类别:
- Person (人名): 例如 "Albert Einstein"
- Organization (组织名): 例如 "Google"
- Location (地名): 例如 "New York"
- Date (日期): 例如 "2024-07-04"
- Miscellaneous (其他): 例如 "COVID-19"
NER 在各种应用中具有广泛的用途,包括文本分析、信息检索、问答系统和语义搜索等。
- Hugging Face 的多语言 NER 模型
Hugging Face 提供了许多预训练模型,这些模型能够处理不同语言的 NER 任务。多语言 NER 模型的特点是它们能够处理多种语言的文本,并在多个语言环境下表现良好。这得益于使用了如 BERT、XLM-RoBERTa 等预训练的多语言 Transformer 模型。
- 主要特性:
- 预训练模型: 利用大规模语料库进行预训练,能捕捉丰富的语言信息。
- 多语言支持: 这些模型能处理多种语言的文本,适用于跨语言的应用场景。
- 高精度: 在多种 NER 任务中表现出色,提供高精度的实体识别和分类。
我们已经应用Transformer来解决英文文本的自然语言处理任务。
但是,如果文档是用希腊语、斯瓦希里语或克林贡语写的呢?
- 一种方法是在Hugging Face Hub上寻找合适的预训练语言模型,然后对手头的任务进行微调。然而,这些预训练模型往往只适用于像德语、俄语或汉语这样的“高资源”语言,因为这些语言有大量的网络文本可用于预训练。当你使用多语言语料库时,另一个常见的挑战是对你和你的工程团队来说,在生产环境中维护多个单语言模型并不是一件有趣的事。
- 幸运的是,有一类多语言Transformer模型可以将你从这种处境中拯救出来。像BERT一样,这些模型使用掩码语言建模作为预训练目标,不过不同的是,它们是在100多种语言的文本上联合训练的。通过在多种语言的庞大语料库上进行预训练,这些多语言Transformer模型实现了零样本跨语言迁移。这意味着在一种语言上微调的模型无须进行任何进一步的训练就可以在其他语言上应用!这也使得这些模型非常适合于“代码切换”,即在单个对话的上下文中,说话者交替使用两种或更多语言或方言。
A. Conneau et al., “Unsupervised Cross-Lingual Representation Learning at Scale”(https://arxiv.org/abs/1911.02116),(2019).
在这里中,我们将讲述一种名为XLM-RoBERTa 的Transformer模型,以及如何对其进行微调以在多种语言中进行NER的。正如我们在前面看到的那样,NER是一种常见的自然语言处理(NLP)任务,该任务识别文本中的人物、组织或地点等实体。这些实体可以用于各种应用,例如从企业文档中获取洞见、增强搜索引擎的质量,或者简单地从语料库构建结构化数据库。
在这里,我们假设要为一个位于瑞士的客户进行命名实体识别,因为瑞士有四种官方语言(通常使用英语作为它们之间的桥梁)。我们第一步是获取一个适合此问题的多语言语料库。
零样本迁移或零样本学习通常指的是在一个标注集上训练模型,然后在另一个标注集上进行评估。不过在谈到Transformer模型的时候,零样本学习也有可能是指在下游任务上评估像GPT-3这样的语言模型,即使该模型并没有经过微调。
三、数据集
J. Hu et al., “XTREME: A Massively Multilingual Multi-Task Benchmark for Evaluating Cross-Lingual Generalization”(https://arxiv.org/abs/2003.11080),(2020); X. Pan et al., “Cross-Lingual Name Tagging and Linking for 282 Languages,”Proceedings of the 55th Annual Meeting of the Association for Computational Linguistics 1(July 2017): 1946-1958,http://dx.doi.org/10.18653/v1/P17-1178.
本章我们将使用跨语言传输多语言编码器(XTREME)基准测试的子集,即WikiANN,又称PAN-X 。该数据集包括多种语言的维基百科文章,其中包括瑞士使用最广泛的四种语言:德语(62.8%)、法语(22.9%)、意大利语(8.4%)和英语(5.9%)。每篇文章都用LOC(位置)、PER(人物)和ORG(组织)并以IOB2格式进行标注(https://oreil.ly/yXMUn)。在IOB2这种格式中,B-前缀表示实体的开头,位于之后的属于同一实体的连续词元则赋予I-前缀。O标记表示该词元不属于任何实体。以如下句子为例:
Jeff Dean is a computer scientist at Google In California
该句子会以IOB2格式标注,如图所示:
![](https://i-blog.csdnimg.cn/direct/2b6b6525f6704a1a90227a9aa3877d88.jpeg)
import pandas as pd
toks = "Jeff Dean is a computer scientist at Google in California".split()
lbls = ["B-PER", "I-PER", "O", "O", "O", "O", "O", "B-ORG", "O", "B-LOC"]
df = pd.DataFrame(data=[toks, lbls], index=['Tokens', 'Tags'])
df
运行结果:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
Tokens | Jeff | Dean | is | a | computer | scientist | at | in | California | |
Tags | B-PER | I-PER | O | O | O | O | O | B-ORG | O | B-LOC |
要在XTREME中加载PAN-X的一个子集,我们需要知道要传给load_dataset()函数的数据集配置。每当要处理具有多个领域的数据集时,你可以使用get_dataset_config_names()函数来查找可用的子集。
# 从 datasets 库中导入 get_dataset_config_names 函数
from datasets import get_dataset_config_names
# 获取 XTREME 数据集的所有配置名称
xtreme_subsets = get_dataset_config_names("xtreme")
# 打印 XTREME 数据集的配置数量
print(f"XTREME has {len(xtreme_subsets)} configurations")
运行结果:
有183个配置!太多了!我们需要缩小搜索范围,我们只寻找以“PAN”开头的配置:
# 过滤出 XTREME 数据集中所有以 "PAN" 开头的配置
panx_subsets = [s for s in xtreme_subsets if s.startswith("PAN")]
# 打印前 3 个 PAN-X 数据集的配置名称
panx_subsets[:3]
运行结果:
['PAN-X.af', 'PAN-X.ar', 'PAN-X.bg']
好的,看来我们已经明确了PAN-X子集的语法规则:每个子集都有一个后缀,该后缀由两个字母组成,看起来是一个ISO 639-1语言代码(https://oreil.ly/R8XNu)。这意味着,要加载德语语料库,我们需要将de代码传给load dataset()的name参数,如下所示:
from datasets import load_dataset
# 加载指定语言的 PAN-X 数据集,这里选择德语 (de)
dataset = load_dataset("xtreme", name="PAN-X.de")
运行结果:
为了创建一个真实的瑞士语语料库,我们将从PAN-X中按照前面所述的口语比例提取德语(de)、法语(fr)、意大利语(it)和英语(en)语料库。我们这么做将会造成语言类别不平衡,这在现实世界的数据集中非常普遍,其中由于缺乏熟练掌握该语言的领域专家,获取少数民族语言的标注样本可能会很昂贵。这种不平衡的数据集将能够模拟在多语言应用程序中工作时的常见情况,我们将看到如何构建一个可以处理所有语言的模型。
为了追踪每种语言,我们创建一个Python defaultdict,将语言代码作为key并将类型为DatasetDict的PAN-X语料库作为value进行存储:
from collections import defaultdict
from datasets import DatasetDict
# 定义要处理的语言和相应的比例
langs = ["de", "fr", "it", "en"]
fracs = [0.629, 0.229, 0.084, 0.059]
# 如果键不存在,则返回 DatasetDict
panx_ch = defaultdict(DatasetDict)
# 遍历语言和对应的比例
for lang, frac in zip(langs, fracs):
# 加载单语言语料库
ds = load_dataset("xtreme", name=f"PAN-X.{lang}")
# 打乱并按口语比例下采样每个分割
for split in ds:
panx_ch[lang][split] = (
ds[split]
.shuffle(seed=0)
.select(range(int(frac * ds[split].num_rows)))
)
运行结果:
这里我们使用shuffle()方法确保不会意外地偏向某个数据集拆分,而select()方法允许我们根据fracs中的值对每个语料库进行下采样。然后我们可以通过访问Dataset.num_rows属性来查看训练集中每种语言的实例数量:
import pandas as pd
# 创建一个 DataFrame 来显示每种语言的训练示例数量
training_examples_df = pd.DataFrame({lang: [panx_ch[lang]["train"].num_rows] for lang in langs},
index=["Number of training examples"])
training_examples_df
运行结果:
de | fr | it | en | |
Number of training examples | 12580 | 4580 | 1680 | 1180 |
按照前面的设计,我们在德语方面拥有比其他所有语言加起来都多的样本,所以我们将以德语作为起点,向法语、意大利语和英语进行零样本跨语言迁移。我们先从德语语料库中抽一个样本:
# 访问德语训练数据集的第一个元素
element = panx_ch["de"]["train"][0]
# 打印每个键值对
for key, value in element.items():
print(f"{key}: {value}")
运行结果:
tokens: ['2.000', 'Einwohnern', 'an', 'der', 'Danziger', 'Bucht', 'in', 'der', 'polnischen', 'Woiwodschaft', 'Pommern', '.'] ner_tags: [0, 0, 0, 0, 5, 6, 0, 0, 5, 5, 6, 0] langs: ['de', 'de', 'de', 'de', 'de', 'de', 'de', 'de', 'de', 'de', 'de', 'de']
与我们之前遇到的Dataset对象一样,我们样本的key对应于Arrow表的列名称,而value表示每个列中的条目。值得一提的是,我们看到ner_tags列对应于每个实体所映射的类别ID。人类难以一下看懂这一列所代表的具体含义,所以我们将创建一个人类能够看懂的LOC、PER和ORG标记的新列。讲到这里,值得一提的是,我们的Dataset对象有一个features属性,该属性指定了与每列相关联的底层数据类型:
# 打印德语训练数据集中每个特征的键和值
for key, value in panx_ch["de"]["train"].features.items():
print(f"{key}: {value}")
运行结果:
tokens: Sequence(feature=Value(dtype='string', id=None), length=-1, id=None) ner_tags: Sequence(feature=ClassLabel(names=['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC'], id=None), length=-1, id=None) langs: Sequence(feature=Value(dtype='string', id=None), length=-1, id=None)
Sequence类指定了该字段所包含的特征列表,对于ner_tags来说,它对应于ClassLabel特征的列表。现在我们从训练集中提取这个特征列表,具体方法如下:
# 获取德语训练数据集中命名实体识别标签的特征
tags = panx_ch["de"]["train"].features["ner_tags"].feature
print(tags)
运行结果:
ClassLabel(names=['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC'], id=None)
我们可以使用之前提到过的ClassLabel.int2str()方法,为训练集创建一个新列,该列包含每个标记的类名。我们将使用map()方法返回一个字典(dict),其中key对应新列名,value为类名列表(list):
# 定义一个函数,将标签索引转换为标签名称
def create_tag_names(batch):
return {"ner_tags_str": [tags.int2str(idx) for idx in batch["ner_tags"]]}
# 将标签索引转换为标签名称,并应用到德语数据集中
panx_de = panx_ch["de"].map(create_tag_names)
运行结果:
现在我们已经将标记转换成人类可读的格式,我们看看在训练集的第一个样本中,词元和标记的对齐结果:
de_example = panx_de["train"][0]
pd.DataFrame([de_example["tokens"], de_example["ner_tags_str"]],
['Tokens', 'Tags'])
运行结果:
我们可以看到,上述结果中的LOC标记是有用的,因为句子“2000 Einwohnern an der Danziger Bucht in der polnischen Woiwodschaft Pommern”在英语中的意思是“波兰波美拉尼亚省的格但斯克湾有2000名居民”,其中格但斯克湾是波罗的海的一个海湾,而“voivodeship”则对应于波兰的一个省。
接下来我们快速检查一下,确保标记之间没有不寻常的不平衡,我们计算每个拆分中每个实体的频率:
# 导入Counter用于计数
from collections import Counter
# 创建一个默认字典来存储不同数据集分割中每个标签类型的计数器
split2freqs = defaultdict(Counter)
# 遍历德语数据集中的每个分割和每行的ner_tags_str
for split, dataset in panx_de.items():
for row in dataset["ner_tags_str"]:
# 遍历每个标签
for tag in row:
# 如果标签以B开头,则提取标签类型并增加计数
if tag.startswith("B"):
tag_type = tag.split("-")[1]
split2freqs[split][tag_type] += 1
# 将计数结果转换为DataFrame,并以数据集分割为行索引
pd.DataFrame.from_dict(split2freqs, orient="index")
运行结果:
LOC | ORG | PER | |
train | 6186 | 5366 | 5810 |
validation | 3172 | 2683 | 2893 |
test | 3180 | 2573 | 3071 |
这看起来很不错,PER、LOC和ORG出现频率的分布在每个拆分中大致相同,因此验证集和测试集应该能够很好地度量我们的NER标注器的泛化能力。接下来,我们介绍一下几个流行的多语言Transformer以及如何对它们进行适配以解决我们的NER任务。
四、多语言Transformer
多语言Transformer采用与其单语言对应物相似的架构和训练程序,不同之处在于用于预训练的语料库包含多种语言的文档。这种方法的一个显著特点是,尽管没有接收任何明确的区分语言的信息,但生成的语言表示能够在各种下游任务中很好地进行跨语言泛化。在某些情况下,这种跨语言迁移能力可以产生与单语言模型类似的结果,从而不需要单独为每种语言训练一个模型!
为了度量NER的跨语言迁移进展,通常使用CoNLL-2002(https://oreil.ly/nYd0o)和CoNLL-2003(https://oreil.ly/sVESv)数据集作为英语、荷兰语、西班牙语和德语的基准。这个基准数据集由新闻文章组成,使用与PAN-X相同的LOC、PER和ORG类别进行标注,但还包括一个用于表示词元不属于前三个组的MISC(杂项)实体标注。多语言Transformer模型通常以三种不同的方式进行评估:
en
在英语训练数据上进行微调,然后在每种语言的测试集上进行评估。
- each
针对每种语言的测试数据进行微调和评估,以度量每种语言的性能。
- all
在所有的训练数据上进行微调,然后在每种语言的测试数据集上进行评估。
我们将采用类似的评估策略来评估我们的实体识别任务,但首先我们需要选择一个要评估的模型。最早的多语言Transformer之一是mBERT,它使用与BERT相同的架构和预训练目标,但将多种语言的维基百科文章添加到了预训练语料库中。不过现在mBERT已被XLM-RoBERTa(或简称为XLM-R)所取代了,因此我们在本章中将使用XLM-R模型。
正如我们在前面所看到的那样,XLM-R仅使用MLM作为100种语言的预训练目标,但与其先前的版本相比,其预训练语料库的规模巨大:每种语言都有维基百科转储数据和2.5TB来自网络的Common Crawl数据。该语料库比早期模型使用的语料库大几个数量级,并为像缅甸语和斯瓦希里语这样的低资源语言提供了显著的信号提升(这些语言只有少量的维基百科文章可用)。
Y. Liu et al., “RoBERTa: A Robustly Optimized BERT Pretraining Approach”(https://arxiv.org/abs/1907.11692),(2019).
T. Kudo and J. Richardson,“SentencePiece:A Simple and Language Independent Subword Tokenizer and Detokenizer for Neural Text Processing”(https://arxiv.org/abs/1808.06226),(2018).
XLM-RoBERTa模型名称中的RoBERTa部分是指其预训练方法与RoBERTa单语言模型相同。RoBERTa的开发人员改进了BERT的几个方面,特别是完全删除了下一句预测任务 。XLM-R也删除了XLM中使用的语言嵌入,并使用SentencePiece直接词元化原始文本 。除了其多语言特性外,XLM-R和RoBERTa之间的一个值得注意的区别是其各自词表的大小:XLM-R有250 000个词元,而RoBERTa只有55 000个!
综上所述,XLM-R是进行多语言自然语言理解任务的绝佳选择。在接下来的部分中,我们将探讨如何在多语言中有效进行词元化。
五、多语言词元化技术
XLM-R使用一种名为SentencePiece的词元分析器,而不是使用WordPiece词元分析器。SentencePiece词元分析器是基于所有100种语言的原始文本进行训练的。为了了解SentencePiece和WordPiece之间的比较,现在我们使用Hugging Face Transformers库加载BERT和XLM-R词元分析器。
# 从transformers库中导入AutoTokenizer
from transformers import AutoTokenizer
# 定义两个模型的名称
bert_model_name = "bert-base-cased"
xlmr_model_name = "xlm-roberta-base"
# 创建BERT和XLM-R的tokenizer实例
bert_tokenizer = AutoTokenizer.from_pretrained(bert_model_name)
xlmr_tokenizer = AutoTokenizer.from_pretrained(xlmr_model_name)
运行结果:
然后我们编码一个小文本序列“Jack Sparrow loves New York!”,我们发现词元分析器还可以检索出每个模型在预训练期间所使用的特殊词元:
# 定义输入文本
text = "Jack Sparrow loves New York!"
# 使用BERT的tokenizer对文本进行分词
bert_tokens = bert_tokenizer(text).tokens()
# 使用XLM-R的tokenizer对文本进行分词
xlmr_tokens = xlmr_tokenizer(text).tokens()
# 将BERT和XLM-R分词结果放入DataFrame中
df = pd.DataFrame([bert_tokens, xlmr_tokens], index=["BERT", "XLM-R"])
# 显示DataFrame
df
运行结果:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
BERT | [CLS] | Jack | Spa | ##rrow | loves | New | York | ! | [SEP] | None |
XLM-R | <s> | ▁Jack | ▁Spar | row | ▁love | s | ▁New | ▁York | ! | </s> |
从以上结果可以看到,XLM-R使用<s>和</s>来表示序列的起始和结束,而不是BERT用于句子分类任务的[CLS]和[SEP]词元。这些词元是在词元化的最后阶段添加的,接下来我们将讲解这一步。
1、词元分析器pipeline
到目前为止,我们将词元化视为将字符串转换为可以传给模型的整数的单个操作。这并不完全准确,如果深究,那么我们可以看到整个pipeline通常由四个步骤组成,具体如图所示。
![](https://i-blog.csdnimg.cn/direct/51b492e08506439f85130d8bbbcd9836.jpeg)
现在我们通过例句“Jack Sparrow loves New York!”更详细地讲解一下每个处理步骤:
- 规范化
这一步对原始字符串进行一系列操作(清理)。常见的操作包括去除空格和移除重音符号。Unicode规范化(https://oreil.ly/2cp3w)是另一种常见的规范化操作,许多词元分析器都应用它来解决同一个字符存在多种写法的场景。具体来说,同一抽象字符序列可能会有两个版本的表示,而诸如NFC、NFD、NFKC和NFKD这样的Unicode规范化方案则将同一字符的不同写法转换为一种标准形式。规范化的另一个例子是将字符串全部转为小写字母。因为如果模型只需要处理小写字符,那么这种技术可以用来减小所需的词表大小。在规范化后,我们的示例字符串将会变成“jack sparrow loves new york!”。
- 词元化预处理
这一步将文本拆分为较小的对象。可以将词元化预处理器视为将文本拆分为“单词”的工具,最终的词元将是这些单词的一部分。对于英语、德语、法语等印欧语言而言,字符串通常可以根据空格和标点符号进行拆分。以我们的示例为例,这一步将得到["jack","sparrow","loves","new","york","!"]。这样操作之后,这些单词后面可以更简单地在pipeline的下一步中使用字节对编码(BPE)或Unigram算法来拆分为子词。然而,将文本拆分为“单词”并非总是一个简单和确定性的操作,甚至可能不是合理的操作。例如,在中文、日文或韩文等语言中,将符号组合成语义单元(如印欧语言中的单词)可能是一个具有多个同样有效组别的非确定性操作。在这种情况下,最好使用特定于语言的库进行词元化预处理。
- 词元化模型
当输入文本规范化和词元化预处理完成之后,词元分析器对单词应用子词拆分模型。这一步需要根据你的语料库进行训练(如果使用预训练的词元分析器,则已经完成了训练)。该模型的作用是将单词拆分成更小的子词,以减小词表的大小,并尝试减少无法识别的词元数量。常见的子词拆分算法包括BPE、Unigram和WordPiece等。例如,在应用词元化模型之后,我们的示例可能被拆分为[jack,spa,rrow,loves,new,york,!]。请注意,此时我们拥有的不再是字符串列表,而是整数列表(即输入ID)。为了更加生动形象地描述这一过程,我们省略了引号。
- 后处理
这是整个pipeline的最后一步,主要是对词元列表进行一些额外的转换,例如,在输入的词元索引序列的开头或结尾添加特殊词元。例如,BERT风格的词元分析器将添加分类和分隔符词元,于是我们的示例就变成了:
[CLS,jack,spa,rrow,loves,new,york,!,SEP]。至此,这个序列(请记住,这将是整数序列,而不是你在这里看到的字符串序列)已经走完整个pipeline,可以输入模型了。
回到前面我们对XLM-R和BERT的比较,我们现在明白了SentencePiece是在后处理步骤中添加了<s>和</s>,而不是[CLS]和[SEP](方便起见,我们将在图示中继续使用[CLS]和[SEP])。现在我们再次回到SentencePiece词元分析器,看看它还有什么特殊之处。
2、SentencePiece词元分析器
SentencePiece词元分析器基于一种称为Unigram的子词分割类型,并将每个输入文本编码为Unicode字符序列。这一特点对于处理多语言语料库尤其有用,因为它能够忽略重音符号、标点符号以及许多语言(如日语)中没有空格字符的情况。此外,SentencePiece的另一个特别之处在于,它使用Unicode符号U+2581或者__字符(又称为下四分之一块字符)来表示空格。这使得SentencePiece能够无歧义地反词元化序列,也不需要依赖特定于语言的词元化预处理器。还是以我们前面的示例为例,我们可以看到WordPiece丢失了“York”和“!”之间的空格信息。而SentencePiece则保留了词元化后的文本中的空格,因此我们可以无歧义地将其转换回原始文本。这一特性尤其适用于处理多语言文本,因为它可以保持原始文本的完整性,而不会引入额外的歧义。
# 将XLM-R的分词结果连接成一个字符串,并将特殊字符替换为空格
joined_xlmr_tokens = "".join(xlmr_tokens).replace(u"\u2581", " ")
# 显示处理后的字符串
joined_xlmr_tokens
运行结果:
'<s> Jack Sparrow loves New York!</s>'
现在我们理解了SentencePiece的工作原理,我们看看如何将简单示例编码成适合NER的形式。第一件事是使用带有词元分类头的预训练模型。但是,我们不会直接从Hugging Face Transformers库中加载这个头,而是自己构建它!通过深入研究Hugging Face Transformers库API,我们可以只用几个步骤就完成这个过程。
五、命名实体识别中的 Transformers
在之前的篇章中,我们看到在文本分类中,BERT使用特殊的[CLS]词元开头来表示整个文本序列。然后,该表示经过一个全连接或密集层输出所有离散标注值的分布,如图所示。
![](https://i-blog.csdnimg.cn/direct/2e8f6863f0ca4c46b14f433b94a1377f.jpeg)
BERT和其他纯编码器Transformer模型在命令实体识别方面采用了与NER相似的方法,不同之处在于将每个输入词元的表示馈送到相同的全连接层以输出词元的实体。因此,NER通常被框定为词元分类任务。具体过程大致如下图所示。
![](https://i-blog.csdnimg.cn/direct/c90887c6a1b746f29feacfe0d12742d3.jpeg)
到目前为止,一切都很好,但在词元分类任务中,我们应该如何处理子词呢?例如,图中的名字“Christa”被词元化为子词“Chr”和“##ista”,那么应该将哪一个或哪几个子词分配为B-PER标注?
J. Devlin et al., “BERT:Pre-Training of Deep BidirectionalTransformersfor Language Understanding”(https://arxiv.org/abs/1810.04805),(2018).
在BERT论文中 ,作者将此标注分配给第一个子词(在我们的示例中则为“Chr”),并忽略后续的子词(“##ista”)。因此我们将遵循该惯例,并用IGN标注要忽略的子词。这样在稍后的后处理步骤中,我们可以轻松地将预测标注传给后续的子词。我们也可以选择包含“##ista”子词的表示,即将其分配为B-LOC标注的副本,但这样做违反了IOB2格式。
幸运的是,由于XLM-R的架构基于RoBERTa,而RoBERTa与BERT完全相同,因此我们在BERT中看到的所有架构方面都可以用于XLM-R!接下来,我们将看到如何通过轻微修改来支持Transformer模型类中的许多其他任务。
六、自定义Hugging Face Transformers库模型类
Hugging Face Transformers库是根据任务来组织和架构的。因此模型类根据不同的任务按照<Model Name>For<Task>的惯例来命名,或者当使用AutoModel类时按照AutoModelFor<Task>命名。
然而,这种方法无法包揽全部任务。假设你有一个伟大的想法,想用Transformer模型解决一直困扰你的NLP问题。所以,你安排了一次会议,通过出色的PPT演示,向老板推销说如果你能解决这个问题,就可以增加部门的收入。老板被你出色的演示和对利润增长的建议所打动,慷慨地同意给你一周时间来构建一个概念验证(PoC)。看到老板这样,你十分兴奋,立即开始工作。你启动GPU并打开notebook开始编码。可是当你执行from transformers import Bert ForTaskXY(这里的TaskXY是你想解决的假想NLP任务)后,当可怕的红色报错布满了你的屏幕时,你脸上变色了:ImportError: cannot import nameBertForTaskXY。噢,目前没有适合你这个用例的BERT模型!你只有一周时间,如果你必须要自己实现这个模型,你应该如何开始?
不要惊慌!Hugging Face Transformers库的设计初衷就是让你能够轻松地扩展现有模型以适应你的特定用例。你可以从预训练模型加载权重,然后使用特定于任务的辅助函数。这样你就可以以极小的成本为你的特定目标构建自定义模型。本节我们将讲述如何实现自己的自定义模型。
1、主体和头
能令Hugging Face Transformers库如此通用的主要思想是将架构分为主体和头(如我们在第1章中所看到的)。我们已经看到,当我们从预训练任务转换到下游任务时,我们需要用适合该任务的层替换模型的最后一层。这个最后一层被称为模型头,它是与任务相关的部分。模型的其余部分称为主体,包括词元嵌入和Transformer层,这些层是通用的、与任务无关的。这种结构表现在具体的代码实现类中,模型主体在BertModel或GPT2Model之类的类中实现,这些类将返回最后一层隐藏状态。而BertForMaskedLM或BertForSequence Classification则为与任务相关的模型类,它们基于基础模型,然后在隐藏状态之上添加必要的头,具体如下图所示。
![](https://i-blog.csdnimg.cn/direct/a50b7c992f4145eca12319ff7f2aebe6.jpeg)
正如接下来我们将看到的那样,这种将主体和头分离的方法使我们能够为任何任务构建一个自定义头,然后将其安装到预训练模型之上。
2、创建用于词元分类的自定义模型
这里我们通过建立一个适用于XLM-R的自定义词元分类头的练习来进行学习。由于XLM-R使用与RoBERTa相同的模型架构,因此我们将使用RoBERTa作为基本模型,然后在此之上增加适用于XLM-R的设置。需要注意的是,本练习出于教学目的,旨在向你展示如何为自己的任务构建自定义模型。在实际工作中,对于词元分类,Hugging Face Transformers库已经有一个名为XLMRobertaForTokenClassification的类可以使用,你可以直接导入该类。因此如果需要的话,你可以跳过本节,直接使用该类。
在开始之前,我们需要一个能够代表我们的XLM-R NER标注器的数据结构。我们需要一个配置对象来初始化模型,以及一个forward()函数来生成输出。我们按如下方式构建用于词元分类的XLM-R类。
import torch.nn as nn
from transformers import XLMRobertaConfig
from transformers.modeling_outputs import TokenClassifierOutput
from transformers.models.roberta.modeling_roberta import RobertaModel
from transformers.models.roberta.modeling_roberta import RobertaPreTrainedModel
# 自定义用于Token分类的XLM-RoBERTa模型类
class XLMRobertaForTokenClassification(RobertaPreTrainedModel):
config_class = XLMRobertaConfig
def __init__(self, config):
super().__init__(config)
self.num_labels = config.num_labels
# 加载模型主体
self.roberta = RobertaModel(config, add_pooling_layer=False)
# 设置用于Token分类的头层
self.dropout = nn.Dropout(config.hidden_dropout_prob)
self.classifier = nn.Linear(config.hidden_size, config.num_labels)
# 加载和初始化权重
self.init_weights()
def forward(self, input_ids=None, attention_mask=None, token_type_ids=None,
labels=None, **kwargs):
# 使用模型主体获取编码器表示
outputs = self.roberta(input_ids, attention_mask=attention_mask,
token_type_ids=token_type_ids, **kwargs)
# 对编码器表示应用分类器
sequence_output = self.dropout(outputs[0])
logits = self.classifier(sequence_output)
# 计算损失
loss = None
if labels is not None:
loss_fct = nn.CrossEntropyLoss()
loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
# 返回模型输出对象
return TokenClassifierOutput(loss=loss, logits=logits,
hidden_states=outputs.hidden_states,
attentions=outputs.attentions)
这里config_class确保在初始化新模型时使用标准的XLM-R设置。如果你想更改默认的参数,则可以通过覆盖配置中的默认设置来实现。使用super()方法调用RobertaPreTrainedModel类的初始化函数。这个抽象类处理预训练权重的初始化或加载。然后,我们加载我们的模型主体,即RobertaModel,并使用自己的分类头进行扩展,其中包括一个dropout和一个标准前馈层。请注意,我们设置add_pooling_layer=False,以确保所有隐藏状态都被返回,而不仅仅是与[CLS]词元相关联的状态。最后,我们通过调用从RobertaPreTrainedModel继承的init weights()方法来初始化所有权重,该方法将为模型主体加载预训练权重,并随机初始化我们的词元分类头的权重。
我们唯一需要做的是定义模型如何在forward()方法中进行前向传递。在前向传递期间,首先将数据通过模型主体进行馈送。有许多输入变量,但我们现在所需的只是input_ids和attention_mask。随后,将模型主体输出的隐藏状态馈送到dropout和分类层中。如果我们在前向传递还提供了标注,则可以直接计算损失。如果存在注意力掩码,则需要做一些额外的工作以确保我们仅计算未掩码词元的损失。最后,我们将所有输出包装在TokenClassifierOutput对象中,从而允许我们通过前几章中熟悉的命名元组来访问元素。
通过实现一个简单类的两个函数,我们可以构建自己的自定义Transformer模型。由于我们继承自PreTrainedModel,因此能立即获得Hugging Face Transformers库所有有用的Transformer工具,如from_pretrained()!接下来我们看看如何将预训练的权重加载到我们的自定义模型中。
3、加载自定义模型
现在我们准备加载词元分类模型。除了模型名称之外,我们还需要提供一些额外的信息,包括我们将用于词元每个实体的标记以及每个标记与ID之间的映射和反向映射。所有这些信息都可以从我们的tags变量中进行推导,tags变量作为一个ClassLabel对象具有一个names属性,我们可以使用它来推导映射:
# 创建索引到标签的映射字典
index2tag = {idx: tag for idx, tag in enumerate(tags.names)}
# 创建标签到索引的映射字典
tag2index = {tag: idx for idx, tag in enumerate(tags.names)}
我们将把这些映射和tags.num classes属性存储在我们在第3章遇到的AutoConfig对象中。向from pretrained()方法传关键字参数将覆盖默认值:
from transformers import AutoConfig
# 从预训练模型名称加载配置
xlmr_config = AutoConfig.from_pretrained(
xlmr_model_name, # 预训练模型的名称
num_labels=tags.num_classes, # 设置标签数量
id2label=index2tag, # 设置索引到标签的映射字典
label2id=tag2index # 设置标签到索引的映射字典
)
utoConfig类包含一个模型架构的蓝图。当我们使用AutoModel.from_pretrained(model_ckpt)载入一个模型时,与该模型相关联的配置文件将被自动下载。然而,如果我们想要修改像类别数或标注名称之类的内容,那么我们可以先载入配置文件,然后使用我们想要自定义的参数来进行修改。
现在,我们可以使用带有附加config参数的from_pretrained()函数像往常一样加载模型权重。请注意,我们的自定义模型类中没有实现加载预训练权重,我们可以通过继承RobertaPreTrainedModel来马上获得该功能。
import torch
# 检查是否有可用的GPU,如果有则使用GPU,否则使用CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 加载预训练的XLM-Roberta模型,并应用之前设置的配置,将模型移动到相应的设备(GPU或CPU)
xlmr_model = (XLMRobertaForTokenClassification
.from_pretrained(xlmr_model_name, config=xlmr_config)
.to(device))
然后我们要快速检查一下,验证我们是否正确初始化了词元分析器和模型,我们使用一个已经知道实体标注的小序列来预测结果以进行测试:
# 将文本编码为输入ID,返回的输入ID是一个PyTorch张量
input_ids = xlmr_tokenizer.encode(text, return_tensors="pt")
# 将编码后的Tokens和Input IDs放入数据框中以便查看
pd.DataFrame([xlmr_tokens, input_ids[0].numpy()], index=["Tokens", "Input IDs"])
运行结果:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
Tokens | <s> | ▁Jack | ▁Spar | row | ▁love | s | ▁New | ▁York | ! | </s> |
Input IDs | 0 | 21763 | 37456 | 15555 | 5161 | 7 | 2356 | 5753 | 38 | 2 |
正如你所看到的,起始词元<s>和结束词元</s>分别被赋予ID为0和2。
最后,我们需要将输入传给模型,并通过获取argmax来提取预测结果,以获取每个词元最有可能的类。
# 使用XLM-R模型进行预测,获取输出logits
outputs = xlmr_model(input_ids.to(device)).logits
# 预测结果为logits中概率最大的标签索引
predictions = torch.argmax(outputs, dim=-1)
# 打印序列中的token数量和输出的形状
print(f"Number of tokens in sequence: {len(xlmr_tokens)}")
print(f"Shape of outputs: {outputs.shape}")
运行结果:
Number of tokens in sequence: 10 Shape of outputs: torch.Size([1, 10, 7])
在这里,我们可以看到logit的形状为[batch_size,num_tokens,num_tags],每个词元都被赋予了7个可能的NER标记中的一个。通过枚举整个序列,我们可以快速查看预训练模型的预测结果:
# 将预测的标签索引转换为实际的标签名称
preds = [tags.names[p] for p in predictions[0].cpu().numpy()]
# 创建一个DataFrame,展示tokens和对应的预测标签
pd.DataFrame([xlmr_tokens, preds], index=["Tokens", "Tags"])
运行结果:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
Tokens | <s> | ▁Jack | ▁Spar | row | ▁love | s | ▁New | ▁York | ! | </s> |
Tags | B-LOC | B-LOC | B-LOC | B-LOC | B-LOC | B-LOC | B-LOC | B-LOC | B-LOC | B-LOC |
毫不意外,使用随机权重的词元分类层存在许多不足,我们需要使用一些标注数据进行微调以使其更好!在微调之前,我们将前面的步骤封装成一个辅助函数,以备后用:
def tag_text(text, tags, model, tokenizer):
# 获取带有特殊字符的tokens
tokens = tokenizer(text).tokens()
# 将序列编码为IDs
input_ids = xlmr_tokenizer(text, return_tensors="pt").input_ids.to(device)
# 获取7个可能类别的分布预测
outputs = model(input_ids)[0]
# 获取每个token最可能的类别
predictions = torch.argmax(outputs, dim=2)
# 将预测结果转换为DataFrame
preds = [tags.names[p] for p in predictions[0].cpu().numpy()]
return pd.DataFrame([tokens, preds], index=["Tokens", "Tags"])
在训练模型之前,我们还需要对输入进行词元化并准备标注。接下来我们将进行词元化。