说明:有的文章将token、Tokenizer、Tokenization翻译为令牌、令牌器和令牌化。虽然从意义上来说更加准确,但是笔者感觉还是不够简单直接,不够形象。所以文中有些地方会翻译成分词、分词器和分词,有些地方又保留英文(有可能google翻译成标记、标记化没注意到)。有其它疑问可以留言或者查看原文。
在第 1 章中,我们使用pipeline API 将 Transformer 模型用于不同的任务。 尽管此 API 功能强大且方便,但了解其内部工作原理很重要,这样我们才能灵活地解决其他问题。
在本章中,您将学习:
(Transformers 库API可以加载、训练和保存任何 Transformer 模型,模型的前向传递完全定义在单个-文件中(All in one file)。模型更易于理解,且一个模型上进行试验,不会影响其他模型
本章将end-to-end为例,使用一个模型和一个tokenizer 来重现第 1 章中介绍的管道 API)
Transformer 模型通常非常大。拥有数百万到数百亿的参数,训练和部署这些模型是一项复杂的工作。此外,由于几乎每天都会发布新模型,并且每个模型都有自己的实现,因此尝试所有这些模型并非易事。
Transformers 库目标是提供一个单一的 API,通过它可以加载、训练和保存任何 Transformer 模型。其主要特点是:
最后一个功能使 Transformers 与其他 ML 库完全不同。模型不是建立在跨文件共享的模块上的;相反,每个模型都有自己的层。除了使模型更易于理解之外,这还使您可以轻松地在一个模型上进行试验,而不会影响其他模型。
本章将从一个end-to-end的例子开始,我们使用一个模型和一个tokenizer 来复制第 1 章中介绍的管道 API。
为了从 Model Hub 和 Transformers 提供的所有功能中受益,我们建议您创建一个帐户。
本节代码Open in Colab (PyTorch)
YouTube视频:what happend inside the pipeline function
让我们从一个完整的例子开始,看看当我们在第 1 章中执行以下代码时,幕后发生了什么:
from transformers import pipeline
classifier = pipeline("sentiment-analysis")
classifier([
"I've been waiting for a HuggingFace course my whole life.",
"I hate this so much!",
])
[{'label': 'POSITIVE', 'score': 0.9598047137260437},
{'label': 'NEGATIVE', 'score': 0.9994558095932007}]
正如我们在第 1 章中看到的,这个管道将三个步骤组合在一起:预处理、通过模型传递输入和后处理:
让我们快速浏览一下这些内容。
与其他神经网络一样,Transformer 模型不能直接处理原始文本,因此我们管道的第一步是将文本输入转换为模型可以理解的数字。为此,我们使用了一个分词器tokenizer,它将负责:
使用 AutoTokenizer 类及其 from_pretrained 方法,以保证所有这些预处理都以与模型预训练时完全相同的方式完成。设定模型的 checkpoint名称,它会自动获取与模型的Tokenizer关联的数据并缓存它(所以它只在你第一次运行下面的代码时下载)。
由于情感分析管道的默认检查点是 distilbert-base-uncased-finetuned-sst-2-english我们运行以下命令:
from transformers import AutoTokenizer
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
将我们的句子传递给分词器Tokenizer,它会输出一个字典,准备提供给我们模型! 唯一要做的就是将输入 ID 列表转换为张量tensors。Transformer 模型只接受tensors作为输入。
您可以使用 Transformers,而不必担心使用哪个 ML 框架作为后端; 对于某些模型,它可能是 PyTorch 或 TensorFlow,或 Flax。
要指定我们想要返回的张量类型(PyTorch、TensorFlow 或普通 NumPy),我们使用 return_tensors 参数:
raw_inputs = [
"I've been waiting for a HuggingFace course my whole life.",
"I hate this so much!",
]
inputs = tokenizer(raw_inputs, padding=True, truncation=True, return_tensors="pt")
print(inputs)
你可以传递一个句子或一个句子列表,以及指定你想要返回的张量类型(如果没有传递类型,你将得到一个list of lists作为结果)。(稍后会解释填充和截断)
以下是 PyTorch 张量的结果:
{
'input_ids': tensor([
[ 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102],
[ 101, 1045, 5223, 2023, 2061, 2172, 999, 102, 0, 0, 0, 0, 0, 0, 0, 0]
]),
'attention_mask': tensor([
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
])
}
输出的字典包含两个键:
我们可以像使用分词器一样下载我们的预训练模型。 Transformers 提供了一个 AutoModel 类,它也有一个 from_pretrained 方法:
from transformers import AutoModel
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModel.from_pretrained(checkpoint)
在此代码片段中,我们下载了之前在管道中使用的相同checkpoint(它实际上应该已经被缓存)并用它实例化了一个模型。
这个架构只包含基本的 Transformer 模块:给定一些输入,它输出我们称之为隐藏状态 hidden states的东西,也称为特征。对于每个模型输入,我们将检索一个高维向量,表示 Transformer 模型对该输入的上下文理解。
不同的任务有不同的head。这些隐藏状态本身就很有用,但它们通常是作为head的输入。这样就可以使用相同的体系结构执行不同的任务。
Transformer 模块输出的向量通常很大。 它一般具有三个维度:
将预处理过的输入传入模型,得到:
outputs = model(**inputs)
print(outputs.last_hidden_state.shape)
torch.Size([2, 16, 768])
Transformers 模型的输出行为类似于命名元组或字典。 您可以通过属性(outputs.last_hidden_state)或键(outputs[“last_hidden_state”])或索引(outputs[0])访问元素。
Model heads:将隐藏状态的高维向量作为输入,并将它们投影到不同的维度上。 它们通常由一个或几个线性层组成:
在此图中,模型由其embeddings layer和后续层表示。embeddings layer将输入进行预处理,每个输入 ID转换为表示对应token的向量。 随后的层使用注意力机制操纵这些向量以产生句子的最终表示。
Transformers 中有许多不同的架构可用,每一种架构都围绕着处理特定任务而设计。 这是一个非详尽列表:
from transformers import AutoModelForSequenceClassification
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
outputs = model(**inputs)
model head将我们之前看到的高维向量作为输入,并输出包含两个值(每个标签一个)的向量:
print(outputs.logits.shape)
torch.Size([2, 2])
由于我们只有两个句子和两个标签,因此我们从模型中得到的结果是 2 x 2 的形状。
Postprocessing the output后处理
我们从模型中获得的作为输出的值本身并不一定有意义。 让我们来看看:
print(outputs.logits)
tensor([[-1.5607, 1.6123],
[ 4.1692, -3.3464]], grad_fn=<AddmmBackward>)
我们的模型预测了第一个句子结果 [-1.5607, 1.6123] 和第二个句子的结果 [4.1692, -3.3464]。 这些不是概率,而是 logits,即模型最后一层输出的原始非标准化分数。 要转换为概率,它们需要经过一个 SoftMax 层(==所有 Transformers 模型都输出 logits,因为训练的损失函数一般会融合最后一个激活函数(比如SoftMax,和实际的交叉熵损失函数):
import torch
predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
print(predictions)
tensor([[4.0195e-02, 9.5980e-01],
[9.9946e-01, 5.4418e-04]], grad_fn=<SoftmaxBackward>)
这次输出是可识别的概率分数。
要获得每个位置对应的标签,我们可以检查模型配置的 id2label 属性(下一节将详细介绍):
model.config.id2label
{0: 'NEGATIVE', 1: 'POSITIVE'}
现在我们可以得出结论,该模型预测了以下内容:
第一句:NEGATIVE:0.0402,POSITIVE:0.9598
第二句:NEGATIVE:0.9995,POSITIVE:0.0005
我们已经成功地重现了管道的三个步骤:
现在让我们花点时间深入了解每个步骤。
✏️快来试试吧! 选择两个(或更多)你自己的文本,并通过sentiment-analysis pipeline运行它们。 然后自己复制您在本节看到的步骤,并检查您是否获得了相同的结果!
在本节中,我们将仔细研究创建和使用模型。 我们将使用 AutoModel 类从checkpoint实例化任何模型。
AutoModel 类及其所有相关类实际上是库中各种可用模型的简单包装器。 它可以自动为您的checkpoint猜测合适的模型架构,然后使用该架构实例化模型。
但是,如果您知道要使用的模型类型,则可以直接使用定义其架构的类。 让我们来看看它如何与 BERT 模型配合使用。
初始化 BERT 模型需要做的第一件事是加载配置对象:
from transformers import BertConfig, BertModel
# Building the config
config = BertConfig()
# Building the model from the config
model = BertModel(config)
config配置包含了许多用于构建模型的属性:
print(config)
BertConfig {
[...]
"hidden_size": 768, #hidden_states 向量的大小
"intermediate_size": 3072, #FFN第一层神经元个数,即attention层传入第一层全连接会扩维4倍
"max_position_embeddings": 512,#最大序列长度512
"num_attention_heads": 12,
"num_hidden_layers": 12,
[...]
}
hidden_size : hidden_states 向量的大小
num_hidden_layers :Transformer 模型的层数
从默认配置创建模型会使用随机值对其进行初始化:
from transformers import BertConfig, BertModel
config = BertConfig()
model = BertModel(config)
# 模型已经随机初始化了
模型可以在这种状态下使用,但是会输出乱码; 它需要先训练。 我们可以根据手头的任务从头开始训练模型,这将需要很长时间和大量数据,并且会对环境产生不可忽视的影响。 为了避免不必要和重复的工作,必须能够共享和重用已经训练过的模型。
使用 from_pretrained 方法来加载一个已经训练过的 Transformer 模型:
from transformers import BertModel
model = BertModel.from_pretrained("bert-base-cased")
正如您之前看到的,我们可以用 AutoModel 类替换 BertModel,效果是一样的。后面我们会使用AutoModel类,这样做的好处是设定模型结构的部分可以不影响checkpoint。如果您的代码适用于一个checkpoint,那么也可以用于另一个checkpoint。甚至即使模型结构不同,只要checkpoint是针对类似任务训练的,也适用(例如情感分析任务)。
在上面的代码示例中,我们没有使用 BertConfig,而是通过 bert-base-cased 标识符加载了一个预训练模型的checkpoint,这个checkpoint由 BERT 的作者自己训练;您可以在其model card中找到有关它的更多详细信息。
该模型现在已使用checkpoint的所有权重进行初始化。它可以直接用于对训练过的任务进行推理,也可以在新任务上进行微调。通过使用预先训练好的权重进行训练,而不是从头开始,我们可以快速取得良好的效果。
权重已下载并缓存在缓存文件夹中(因此以后对 from_pretrained 方法的调用不会重新下载它们),该文件夹默认为 ~/.cache/huggingface/transformers。您可以通过设置 HF_HOME 环境变量来自定义缓存文件夹。
用于加载模型的标识符可以是 Model Hub 上任何模型的标识符,只要它与 BERT 架构兼容即可。 可以在此处找到 BERT 检查点的完整列表。
保存模型就像加载模型一样简单——我们使用 save_pretrained 方法,它类似于 from_pretrained 方法:
model.save_pretrained("directory_on_my_computer")
这会将两个文件保存到您的磁盘:
ls directory_on_my_computer
config.json pytorch_model.bin
如果您查看 config.json 文件,您将认识到构建模型架构所需的属性。 该文件还包含一些元数据,例如检查点的来源以及您上次保存检查点时使用的 Transformers 版本。
pytorch_model.bin 文件被称为状态字典; 它包含您模型的所有权重。 这两个文件齐头并进; 配置configuration是了解模型架构所必需的,而模型权重model weights是模型的参数。
现在您知道如何加载和保存模型,让我们尝试使用它进行一些预测。 Transformer 模型只能处理数字——tokenizer生成的数字。 但在我们讨论分词器之前,让我们探讨一下模型接受哪些输入。
tokenizer可以负责将输入转换为适当框架的张量,但为了帮助您了解发生了什么,我们将快速了解在将输入发送到模型之前必须完成的操作。
假设我们有几个序列:
sequences = [
"Hello!",
"Cool.",
"Nice!"
]
分词器tokenizer将这些转换为词汇索引,通常称为输入 ID。 每个序列现在都是一个数字列表! 结果输出是:
encoded_sequences = [
[ 101, 7592, 999, 102],
[ 101, 4658, 1012, 102],
[ 101, 3835, 999, 102]
]
这是一个编码序列列表。 张量只接受矩形(想想矩阵)。 这个“数组”已经是矩形的,所以将它转换为张量很容易:
import torch
model_inputs = torch.tensor(encoded_sequences)
使用张量作为模型的输入
在模型中使用张量非常简单——我们只用输入调用模型:
output = model(model_inputs)
虽然模型接受许多不同的参数,但只有输入 ID是必需的。我们需要仔细研究,构建出 Transformer 模型可以理解的tokenizers。 (稍后将解释其他参数的作用以及何时需要它们)
Tokenizers是 NLP 管道的核心组件之一。 模型只能处理数字,因此分词器需要将我们的文本输入转换为数值型的数据。 在本节中,我们将探讨标记化管道tokenization pipeline中发生的事情。
在 NLP 任务中,通常处理的数据是原始文本。 这是此类文本的示例:
Jim Henson was a puppeteer
但是,模型只能处理数字,因此我们需要找到一种将原始文本转换为数字的方法。 这就是分词器所做的,并且有很多方法可以做到这一点。我们的目标是找到对模型最有意义的表示。如果可能的话,找到最短的表示。
让我们看一下tokenization算法的一些示例,并尝试回答您可能对tokenization提出的一些问题。
想到的第一种标记器是基于单词的。 它通常很容易设置和使用,只需几条规则就会产生不错的结果。 在下图中,目标是将原始文本拆分为单词并为每个单词找到一个数字表示:
有多种方法可以拆分文本。 例如,我们可以通过应用 Python 的 split 函数使用空格将文本标记为单词:
tokenized_text = "Jim Henson was a puppeteer".split()
print(tokenized_text)
['Jim', 'Henson', 'was', 'a', 'puppeteer']
还有一些单词tokenizers的变体,它们具有额外的标点符号规则。使用这种tokenizers,我们最终可以得到一些非常大的“词汇表vocabularies”,其中vocabularies由我们在语料库中拥有的独立tokens的总数定义。
每个单词都分配了一个 ID,从 0 开始一直到词汇表的大小。模型使用这些 ID 来识别每个单词。
如果我们想用基于单词的tokenizer完全覆盖一种语言,我们需要为语言中的每个单词对应一个标识符,这将生成大量的标记。
例如,英语中有超过 500,000 个单词,因此要构建从每个单词到输入 ID 的映射,我们需要跟踪这么多 ID。此外,像“dog”这样的词与“dogs”这样的词的表示方式不同,模型最初无法知道“dog”和“dogs”是相似的:它会将这两个词识别为不相关。又例如“run”和“running”。
最后,我们需要一个自定义标记custom token来表示不在我们词汇表中的单词。这被称为“未知”标记“unknown” token,通常表示为“[UNK]”或“”。如果您看到 tokenizer 正在生成大量这样的tokens,这通常是一个不好的迹象,因为它无法检索单词的合理表示,并且您在此过程中会丢失信息。制作词汇表的目标是使 tokenizer尽可能少的将单词分词成unknown token。
减少“unknown” token数量的一种方法是使用基于字符的tokenizer。
基于字符的tokenizer将文本拆分为字符characters,而不是单词。 这有两个主要好处:
但是这里也出现了一些关于空格和标点符号的问题:
这种方法也不是完美的。 每个字符本身并没有多大意义,英文单词就是这种情况。 但是,这又因语言而异。 在中文中,每个字符比拉丁语言中的字符包含更多信息。
使用基于单词的分词器,每个token是一个单词,但对于Character-based tokenizer,一个单词会很容易变成 10 个或更多的tokens。这使得我们的模型最终会处理大量的tokens 。
为了两全其美,我们可以使用结合这两种方法的第三种技术:子词分词subword tokenization。
Subword tokenization子词分词
子词分词算法原则:常用词不会拆分为更小的子词,而是将少见词(低频词)分解为有意义的子词。
例如,“annoyingly”可能被认为是一个罕见的词,可以分解为“annoying”和“ly”。 这两个词都可能更频繁地作为独立的子词出现,同时“annoyingly”的含义由“annoying”和“ly”的复合含义保留。
这是一个示例,展示了子词分词算法如何对序列“Let’s do tokenization!!”进行分词:
这些子词最终提供了很多语义含义:例如,在上面的示例中,“tokenization”被拆分为“token”和“ization”,这两个子词都具有语义,同时节省空间(一个较长的单词只需要两个tokens就可以表示)。 这使我们能够以较小的词汇表进行相对较好的覆盖,并且几乎没有未知的tokens。
这种方法在土耳其语等agglutinative语言中特别有用,您可以通过将子词串在一起来形成(几乎)任意长的复杂词。
另外除了上述方法,还有更多的分词技术。 例如:
字节级 BPE(Byte-level BPE),用于 GPT-2
WordPiece,用于 BERT
SentencePiece 或 Unigram,用于多个多语言模型
您现在应该对tokenizer的工作原理有足够的了解,可以开始使用tokenizer API。
加载和保存tokenizer和使用模型一样简单,基于相同的两种方法:from_pretrained 和 save_pretrained。 这些方法将加载或保存tokenizer使用的算法(有点像模型的架构)及其词汇表(有点像模型的权重)。
和加载BERT 模型一样,我们也加载和使用BERT的checkpoint来训练 BERT tokenizer,只不过这里使用 BertTokenizer 类:
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained("bert-base-cased")
与 AutoModel 类似,AutoTokenizer 类将根据检查点名称,在库中选取适当的Tokenizer类,并且可以直接与任何检查点一起使用:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
跟上节一样使用tokenizer:
tokenizer("Using a Transformer network is simple")
{'input_ids': [101, 7993, 170, 11303, 1200, 2443, 1110, 3014, 102],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]}
保存tokenizer 和保存model方式一样:
tokenizer.save_pretrained("directory_on_my_computer")
先让我们看看 input_ids 是如何生成的。 为此,我们需要查看分词器的中间方法。(第 3 章中会详细讲解 token_type_ids,稍后我们将讲解 attention_mask 键)。
将文本转换为数字称为编码。编码分两步完成:分词tokenization,然后转换为输入ID。
有多个规则可以管理分词过程,这就是为什么我们需要使用模型名称来实例化tokenizer,以确保我们使用模型预训练时使用的相同规则。
第二步是将这些标记转换为数字,然后就可以将它们转换张量来提供给模型。为此,tokenizer有一个词汇表vocabulary,使用 from_pretrained 方法实例化tokenizer时会下载vocabulary。因为我们需要使用模型预训练时使用的相同词汇。
为了更好地理解这两个步骤,我们使用一些单独执行部分tokenization pipeline的方法,来向您展示这些步骤的中间结果(实际中是使用tokenizer直接处理输入,如第 2 节所示)。
分词Tokenization
分词过程由tokenizer的tokenize方法完成:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
sequence = "Using a Transformer network is simple"
tokens = tokenizer.tokenize(sequence)
print(tokens)
此方法的输出是一个字符串列表或tokens:
['Using', 'a', 'transform', '##er', 'network', 'is', 'simple']
这个分词器是一个子词分词器:它对词进行拆分,得到可以用其词汇表表示的tokens。 这就是 Transformer 的情况,它被分成两个标记:transform 和 ##er。(##表示er的前面还有子词,在子词转换回单词时会去掉)
从tokens 到 input IDs
使用tokenizer的convert_tokens_to_ids 方法将rokens转换为 input IDs:
ids = tokenizer.convert_tokens_to_ids(tokens)
print(ids)
[7993, 170, 11303, 1200, 2443, 1110, 3014]
这些输出转换为合适结构的张量,就可以作为模型的输入。
✏️快来试试吧! 在我们在第 2 节中使用的输入句子(“I’ve been waiting for a HuggingFace course my whole life.” and “I hate this so much!”)上复制最后两个步骤(tokenization 和转换为输入 ID)。 检查您得到的input IDs是否与我们之前得到的相同!
解码是相反的,通过decode方法,可以将词汇索引转化为字符串文本。 如下所示:
decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)
'Using a Transformer network is simple'
decode 方法不仅将索引转换回tokens,还将同一个单词的subword组合在一起以生成可读的句子。 当我们使用预测新文本的模型(从提示生成的文本,或序列到序列问题(如翻译或摘要))时,这种行为将非常有用。
到现在为止,您应该了解分词器可以处理的操作:分词、转换为input IDs以及将input IDs转换回字符串文本。 然而,我们只是看到了冰山一角。 在下一节中,我们了解tokenizer的限制,并看看如何克服这些限制。
在上一节中,我们探讨了最简单的用例:对单个小长度序列进行处理。 然而,实际中还有一些其他问题:
上节中,我们将序列转换为数字列表。 现在让我们将这些数字列表转换为张量并发送到模型:
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
sequence = "I've been waiting for a HuggingFace course my whole life."
tokens = tokenizer.tokenize(sequence)
ids = tokenizer.convert_tokens_to_ids(tokens)
input_ids = torch.tensor(ids)
# T下行会报错
model(input_ids)
IndexError: Dimension out of range (expected to be in range of [-1, 0], but got 1)
我们使用了第2节中pipeline一样的处理步骤,为什么还会失败?
这是因为我们给模型输入了单个序列,而 Transformers 模型默认需要多个句子。仔细观察,您会发现tokenizer不仅将input IDs列表转换为张量,它还给input IDs添加了一个维度(和首尾特殊字符):
```python
tokenized_inputs = tokenizer(sequence, return_tensors="pt")
print(tokenized_inputs["input_ids"])
tensor([[ 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172,
2607, 2026, 2878, 2166, 1012, 102]])
让我们添加一个维度再试试:
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
sequence = "I've been waiting for a HuggingFace course my whole life."
tokens = tokenizer.tokenize(sequence)
ids = tokenizer.convert_tokens_to_ids(tokens)
input_ids = torch.tensor([ids])
print("Input IDs:", input_ids)
output = model(input_ids)
print("Logits:", output.logits)
我们打印出input_ids和生成的 logits向量:
Input IDs: [[ 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012]]
Logits: [[-2.7276, 2.8789]]
批处理是一次给模型输入多个序列。 如果你只有一个序列,那就构建一个batch为一个序列:
batched_ids = [ids, ids]
这个batch含有两个相同的输入序列。
✏️快来试试吧! 将此 batched_ids 列表转换为张量并将其传递给您的模型。 检查您是否得到了与以前一样的 logits(但是logits出现两次)!
批处理可以使模型一次处理多个序列。 这导致了第二个问题:多个句子组合时,它们的长度可能不同。 而作为模型的输入,同批次的各张量必须是同一长度。因此您将无法直接将input_ids列表转换为张量。 为了解决这个问题,我们通常填充输入(pad the inputs)。
下面的列表不能转换为张量:
batched_ids = [[200, 200, 200],[200, 200]]
为了解决这个问题,将不够长的张量padding到相同长度。 Padding过程一般是通过将padding token(fewer values)添加到不够长的序列后面。 在上面的示例中,Padding生成的张量如下所示:
padding_id = 100
batched_ids = [[200, 200, 200],[200, 200, padding_id]]
padding token ID 可以在 tokenizer.pad_token_id 中找到。 我们使用它将两个句子一起批处理:
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
sequence1_ids = [[200, 200, 200]]
sequence2_ids = [[200, 200]]
batched_ids = [[200, 200, 200], [200, 200, tokenizer.pad_token_id]]
print(model(torch.tensor(sequence1_ids)).logits)
print(model(torch.tensor(sequence2_ids)).logits)
print(model(torch.tensor(batched_ids)).logits)
tensor([[ 1.5694, -1.3895]], grad_fn=<AddmmBackward>)
tensor([[ 0.5803, -0.4125]], grad_fn=<AddmmBackward>)
tensor([[ 1.5694, -1.3895],
[ 1.3373, -1.2163]], grad_fn=<AddmmBackward>)
上面预测的 logits 有问题:sequence2_ids应该与batched_ids的第二个logits相同,但我们得到了完全不同的值!
这是因为 Transformer 模型的关键特征是attention序列中的每个tokens。所以也会关注到padding token 。 为了使padding token后的序列得到与原来同样的logits,需要屏蔽对padding token的关注。 我们可以使用attention mask来完成。
Attention masks也是一个张量,它的值全部是0和1,形状与input IDs 张量完全相同。1 表示对应的token应该被关注,0表示对应的token不应该被关注(直接忽略,attention score为0)。
让我们用一个Attention masks来完成前面的例子:
batched_ids = [[200, 200, 200],[200, 200, padding_id]]
attention_mask = [[1, 1, 1],[1, 1, 0]]
outputs = model((torch.tensor(batched_ids), attention_mask=torch.tensor(attention_mask))
print(outputs.logits)
tensor([[ 1.5694, -1.3895],
[ 0.5803, -0.4125]], grad_fn=<AddmmBackward>)
现在批处理的第二个句子得到了和 padding之前一样的logits值。
✏️快来试试吧! 在第 2 节中使用的两个句子上手动tokenization(“I’ve been waiting for a HuggingFace course my whole life.” and “I hate this so much!”)。 将它们传递给模型并检查您是否获得了与第 2 节中相同的 logits。现在使用padding token将它们组合在一起,然后创建适当的attention mask。 看看最后是否获得了相同的结果!
对于 Transformer 模型,我们可以传递模型的序列长度是有限制的。 大多数模型输入是512维或1024 维的序列,处理更长的序列时会崩溃。 这个问题有两种解决方案:
不同模型支持不同的序列长度,有些模型专门处理很长的序列。例如Longformer和LED。如果您要处理超长序列可以看看这两个模型。
否则,我们建议您通过指定 max_sequence_length 参数来截断序列:
sequence = sequence[:max_sequence_length]
在最后几节中,我们一直在尽最大努力手工完成大部分工作。 我们已经探索了tokenizers的工作原理,并研究了分词tokenization、转换为input IDs、padding、截断和注意力掩码attention mask。
然而,正如我们在第 2 节中看到的, Transformers API 可以通过处理以上所有步骤。 当你直接在句子上调用你的tokenizers时,得到的结果可以直接输入模型:
from transformers import AutoTokenizer
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
sequence = "I've been waiting for a HuggingFace course my whole life."
model_inputs = tokenizer(sequence)
#model_inputs输出一个含有两个键值对的字典
{'input_ids': [101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
model_inputs 变量包含模型正常运行所需的一切。 对于 DistilBERT,这包括input IDs 和attention mask。
这种方法非常强大。可以处理单个或多个输入序列,而不需要修改API:
sequence = "I've been waiting for a HuggingFace course my whole life."
model_inputs = tokenizer(sequence)
sequences = ["I've been waiting for a HuggingFace course my whole life.",
"So have I!"]
model_inputs = tokenizer(sequences)
还可以设定不同的padding长度:
# 将序列填充到最长序列长度
model_inputs = tokenizer(sequences, padding="longest")
# 将序列填充到模型最大长度
# (512 for BERT or DistilBERT)
model_inputs = tokenizer(sequences, padding="max_length")
# 将序列填充到指定的最大长度
model_inputs = tokenizer(sequences, padding="max_length", max_length=8)
也可以截断序列:
sequences = [
"I've been waiting for a HuggingFace course my whole life.",
"So have I!"]
# 截断比模型的最大输入长度还长的序列
# (512 for BERT or DistilBERT)
model_inputs = tokenizer(sequences, truncation=True)
# 截断长度超过指定最大长度的序列
model_inputs = tokenizer(sequences, max_length=8, truncation=True)
tokenizer可以将处理后的结果转换为不同框架需要的张量,然后就可以将其直接发送到模型。 “pt”返回 PyTorch 张量,“tf”返回 TensorFlow 张量,“np”返回 NumPy 数组:
sequences = [
"I've been waiting for a HuggingFace course my whole life.",
"So have I!"]
# Returns PyTorch tensors
model_inputs = tokenizer(sequences, padding=True, return_tensors="pt")
# Returns TensorFlow tensors
model_inputs = tokenizer(sequences, padding=True, return_tensors="tf")
# Returns NumPy arrays
model_inputs = tokenizer(sequences, padding=True, return_tensors="np")
如果我们查看tokenizer返回的input IDs,我们会发现它们与我们之前的有一点不同:
sequence = "I've been waiting for a HuggingFace course my whole life."
model_inputs = tokenizer(sequence)
print(model_inputs["input_ids"])
tokens = tokenizer.tokenize(sequence)
ids = tokenizer.convert_tokens_to_ids(tokens)
print(ids)
[101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102]
[1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012]
头尾各添加了一个token ID。让我们解码这两个ID,看看会得到什么:
print(tokenizer.decode(model_inputs["input_ids"]))
print(tokenizer.decode(ids))
"[CLS] i've been waiting for a huggingface course my whole life. [SEP]"
"i've been waiting for a huggingface course my whole life."
分词器在开头添加了特殊词 [CLS],在末尾添加了特殊词 [SEP]。添加这两个特殊字符,是为了使模型在进行预训练时,获得相同的inference结果。 请注意,有些模型不添加特殊token,或者添加不同的token;或者仅在开头或仅在结尾添加。 任何情况下,分词器都知道模型具体的特殊tokens情况,并将为您处理。
现在我们已经看到了tokenizer对象在处理文本时的每一步(单独步骤),让我们最后一次看看它如何处理多个序列(padding!)、超长序列(截断!)、多类型张量及其主要API:
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
sequences = [
"I've been waiting for a HuggingFace course my whole life.",
"So have I!"
]
tokens = tokenizer(sequences, padding=True, truncation=True, return_tensors="pt")
output = model(**tokens)
回顾本章,您:
从现在开始,您应该可以自由浏览 Transformers 文档:这些文档里的词汇听起来很熟悉,并且您会看到文档里使用的方法,大部分在前面课程已经使用过。
章节测试