huggingface NLP工具包教程2:使用Transformers

huggingface NLP工具包教程2:使用Transformers

引言

Transformer 模型通常非常大,由于有数百万到数百亿个参数,训练和部署这些模型是一项复杂的任务。此外,由于几乎每天都有新模型发布,而且每个模型都有自己的实现,所以使用所有这些模型比较麻烦。

transformers 库就是为了解决这个问题而创建的。目标是提供一个 API,通过它可以加载、训练和保存任何 Transformer 模型。主要特点是:

  • 易用性:下载、加载和使用最新的 NLP 模型进行推理只需两行代码即可完成。
  • 灵活性:所有模型的核心都是简单的 PyTorch 中的 nn.Module 或 TensorFlow 中的 tf.keras.Model 类或其他框架中的模型一样处理。
  • 简单性:库中几乎没有任何抽象。核心概念是 “All-in-one file”:模型的前向传递完全在一个文件中定义,这样代码本身就很容易理解和魔改。

Transformers 库还有一点与其他机器学习框架不同。用于构建模型的各个模块并不是在不同文件之间共享的,每个模型有自己的层。这使得模型更容易理解,并且可以方便地在一个模型上进行实验,而不会影响到其他模型。

本章将会介绍一个端到端的例子,使用 model、tokenizer 来实现第一章中 pipeline() 方法中的流程。对于 model API,我们将深入 model 类和 configuration 类,并展示如何加载一个模型、它如何处理数值输入并输出结果。然后介绍 tokenizer API,它负责第一个和最后一个处理步骤,处理从文本到神经网络数值输入的转换,并在需要时转换回文本。最后,将介绍如何通过高层 tokenizer() 方法在一个批次内处理多个句子。

pipeline背后

我们从一个完整的例子开始,下面的例子在第一章中已经介绍过,现在来看一下在它执行的背后发生了什么:

from transformers import pipelineclassifier = 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}]

下面这张图介绍了 pipeline 方法中具体做的事情,下面将逐步看一下。

在这里插入图片描述

tokenizer预处理

Transformer 模型肯定不能直接处理原始文本数据,所以 pipeline 的第一步就是将原始文本数据转换为数值数据。tokenizer 负责完成这件事情:

  • 将输入分割成单词、子词(subword)或者符号(比如根据标点),这些分割结果成为 tokens
  • 将每个 token 映射为一个整数
  • 添加一些额外的输入

所有这些预训练步骤,在模型预训练、微调、使用(推理)时都应该保持完全一致,因此我们在 Model Hub 中下载模型时也需要下载这些信息。为了加载给定的预处理步骤,我们需要使用 AutoTokenizer 类和它的 from_pretrained() 方法。使用指定模型权重的名称时,将会自动获取与该模型对应的 tokenizer 并将其缓存(所以仅在第一次使用时需要下载)。

from transformers import AutoTokenizercheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)

当我们得到 tokenizer 之后,就可以直接将原始文本送入其中就可以得到可以直接输入给模型的整型值。剩下要做的事情就是将这些整型值转换为 tensor。

transformers 库支持多数机器学习框架,如 PyTorch、TensorFlow、Flax(部分模型支持)。我们知道,在机器学习框架中,模型需要接收张量(tensor)类型作为输入。我们可以通过 return_tensors 参数来指定返回张量类型所属的框架(如 Pytorch、TensorFlow、NumPy)。

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)

paddingtruncation 参数先不用管,后面会介绍。现在要知道的是,我们需要传入一个或一组句子,并指定返回张量所属的框架,如果未指定,则返回列表。以下是以 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]])
}

可以看到,返回结果是一个字典,其中有两个键:input_idsattention_mask 。其中 input_ids 由两行整型值组成(每一行对应一个输入句子),每行中的元素由独特的整型值表示一个 token。attention_mask 后面会介绍。

model处理过程

我们同样可以下载预训练模型(model)。transformers 库中有 AutoModel 类,与 AutoTokenizer 类似,它同样有 from_pretrained() 方法。

from transformers import AutoModelcheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModel.from_pretrained(checkpoint)

在这段代码中,我们下载了一个与之前 pipeline 中同样的模型权重(如果运行过 pipeline 方法,这里不会重新下载,而是使用缓存的权重),并使用该权重实例化了一个模型。这个架构只包含基本的 Transformer 模块:给定一些输入,输出隐藏状态,也称为特征。对于每个模型输入,也将得到一个高维向量,表示Transformer 模型对该输入的整体表示。虽然这些隐藏状态本身也可以直接用,但它们通常是模型另一部分(head)的输入。在第 1 章中,不同的任务可以用相同的体系结构来执行,但是这些任务中的每一个都有不同的 head 部分。

高维向量表示

Transformer 模型的输出通常非常巨大,一般来说它包含三个维度:

  • 批尺寸(batch size):同时处理的序列个数(上例中为 2);
  • 序列长度(sequence length):用于表示一个序列数值表示的长度(上例中为 16);
  • 隐藏状态尺寸(hidden size):高维向量和隐藏状态的尺寸

隐藏状态之所以被称为高维向量表示是因为它的尺寸通常非常大,在较小的 Transformer 模型中通常为 768,在大模型中可以达到 3072 甚至更大。可以通过以下方法查看它的尺寸:

outputs = model(**inputs)
print(outputs.last_hidden_state.shape)# 输出:
torch.Size([2, 16, 768])

Transformer 模型的输出像一个命名元组(namedtuple)或字典。可以通过属性(outputs.last_hidden_state)、键(outputs['last_hidden_state'])、索引(outputs[0])三种形式访问 。

模型输出头

模型头(head)通常由几层全连接层组成,接收 Transformer 模型输出的隐藏状态高维向量作为输入,并将其映射到另外一个维度。在模型中的例程如下图所示。Transformer 模型由其嵌入层(embeddings)和后续层表示。嵌入层将 token 输入中的每个输入 ID 转换为表示相关 token 的向量。随后的层使用注意力机制处理这些向量,以产生句子的最终表示。然后送到模型头中进行处理,输出最终的预测结果。

在这里插入图片描述

Transformers 中有许多不同的架构,每一种架构都是围绕解决特定任务而设计的。以下列表展示了其中一部分:

  • *Model (获取模型的隐藏状态)
  • *ForCausalLM
  • *ForMaskedLM
  • *ForMultipleChoice
  • *ForQuestionAnswering
  • *ForSequenceClassification
  • *ForTokenClassification
  • 其他

在我们的例子中,我们的模型需要有一个序列分类头,来对句子是正向还是负向进行分类。所以实际上我们这里使用的不是 AutoModel 类,而是 AutoModelForSequenceClassification 类。

from transformers import AutoModelForSequenceClassificationcheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
outputs = model(**inputs)

现在再来看模型的输出,维度数明显低了很多,因为在序列分类模型中,输出是模型头将 Transformer 网络输出的高维向量处理之后的结果。最终的输出向量只有两维,分别对应两个类别正向、负向。

print(outputs.logits.shape)
# 输出:
torch.Size([2, 2])

后处理

经过 Transformer 和模型头之后得到的输出本身没有实际意义。

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 进行一步后处理,得到每个类别对应的概率。(所有的 transformer 模型输出的都是 logits,因为在计算损失函数(如交叉熵)时,会包含计算 softmax 的步骤。)

import torchpredictions = 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>)

现在看到最终的输出 [0.0402, 0.9598][0.9995, 0.0005] 就已经是对应标签的概率值了。可以通过 id2label 属性来确定该模型配置中输出的每一维度所对应的标签。

print(model.config.id2label)# 输出:
{0: 'NEGATIVE', 1: 'POSITIVE'}

最终我们得到模型的预测如下:

  • 句子一: 负向: 0.0402, 正向: 0.9598
  • 句子而: 负向: 0.9995, 正向: 0.0005

至此,我们已经成功复现了 pipeline 方法中的三个步骤:使用 tokenizer 进行前处理、将输入送入模型进行计算和后处理。下面我们会深入这些步骤的具体细节。

Models

本节将详细地讨论如何创建并使用一个模型。我们会用 AutoModel 类,它可以方便地通过权重文件来实例化一个模型。AutoModel 类及其所有相关类实际上是库中各种可用模型的简单封装。它可以自动猜测适合给定权重文件的模型架构,然后使用该架构实例化模型。但是,如果您知道要使用的模型架构,可以直接使用定义该架构的类。接下来以 BERT 为例进行介绍。

创建模型

首先需要初始化一个 BERT 模型并加载一个配置对象。

from transformers import BertConfig, BertModel# Building the config
config = BertConfig()# Building the model from the config
model = BertModel(config)

配置对象包含了模型的许多属性:

print(config)# 输出:
BertConfig {[...]"hidden_size": 768,"intermediate_size": 3072,"max_position_embeddings": 512,"num_attention_heads": 12,"num_hidden_layers": 12,[...]
}

不同的加载方法

这种从默认配置中进行加载的方法会对模型权重进行随机初始化:

from transformers import BertConfig, BertModelconfig = BertConfig()
model = BertModel(config)# Model is randomly initialized!

此时模型可以正常使用,但它的输出是完全混乱的,它需要先训练。我们可以根据手头的任务从头开始训练模型,但正如在第 1 章中提到的,这将需要很长时间和大量数据,并且会对环境产生影响。为了避免不必要的重复工作,需要共享和复用已经预训练过的模型。

加载预训练的模型十分简单,使用 from_pretrained() 方法即可:

from transformers import BertModelmodel = BertModel.from_pretrained("bert-base-cased")

在前面一节的介绍中,我们用的是 AutoModel 类,而非 BertModel 类。后面我们也将这样做,这会称为权重无关代码;即如果代码适用于一个权重,那么它也适用于加载其他权重文件。即使架构不同,只要权重针对类似任务(例如,情感分析任务)训练的,就适用。在上面的代码示例中,我们没有使用 BertConfig,而是通过 'bert-base-cased' 标识符加载了一个预训练模型。该权重由 BERT 原作者自己训练;可以在它的 model card 中查看更多细节。

现在该模型由指定权重文件初始化,它可以直接用于在本身的预训练任务上进行推理,也可以用于在新任务上进行微调。相比于从头训练,在预训练权重上进行微调能够更快地得到更好的结果。

模型权重默认缓存在 ~/.cache/huggingface/transformers 目录下,可以通过修改 HF_HOME 环境变量来更改缓存目录。

标识符(如上面的 'bert-base-cased' )可以用来指定加载 Model Hub 中的任何兼容 BERT 架构的模型。目前可用的 BERT 权重在这里查看。

保存方法

保存一个模型与加载模型同样简单,使用 save_pretrained() 方法:

model.save_pretrained("directory_on_my_computer")

这将在本地磁盘上保存两个文件:

ls directory_on_my_computerconfig.json pytorch_model.bin

其中 config.json 文件中保存了构建模型必要的一些属性配置,还有一些元数据(metadata),比如保存该权重文件时的 transformers 库版本等。而 pytorch_model.bin 保存了模型的权重参数 。两份文件配合起来完整地保存了一个 Transformer 模型结构、权重的相关信息。

使用transformer模型进行推理

现在我们已经介绍过如何加载和保存模型。接下来将使用模型进行推理。前面提到过,Transformer 模型只能处理经 tokenizer 得到的数值输入,在详细介绍 tokenizer 之前,我们先来看一下模型接收的输入。

tokenizer 负责将输入文本转换为对应框架的张量,关于 tokenizer 后面会详细介绍,这里先快速看一下从原始文本到模型输入需要哪些步骤。

我们有一组原始文本序列:

sequences = ["Hello!", "Cool.", "Nice!"]

tokenizer 将这些序列转换为词表索引(通常称为 input IDs),每个序列是一组数字,输出如下:

encoded_sequences = [[101, 7592, 999, 102],[101, 4658, 1012, 102],[101, 3835, 999, 102],
]

这是一组序列(列表的列表),要将多维数组数据转换为 tensor 格式,该多维数组必须是矩形的形状(即每个维度的长度需要一致),这里刚好满足(不满足的话一般需要填充),可以直接转换为 tensor:

import torchmodel_inputs = torch.tensor(encoded_sequences)

接下来就可以直接将 tensor 格式的数值数据输入给模型:

output = model(model_inputs)

模型接收许多参数,只有 input IDs 是必须得,其他参数将会再后面介绍。现在我们先来看一下 tokenizer 是如何将原始文本输入转换为数值输入的。

tokenizers

分词方式简介

tokenizers 是 NLP pipeline 中的一个关键组件,它的作用是:将文本转换为模型能够处理的数值型输入。本节将介绍 tokenization 的过程。

在 NLP 任务中,输入的是原始文本数据,如:

Jim Henson was a puppeteer

然而,模型只能处理数值型数据,所以我们需要 tokenizer 来将原始文本转换为数值型数据,有许多种分词方法能够实现这样的转换,核心的目标是找到最有意义的表示,即最方便模型理解的表示方法,然后,还要尽可能得小。以下将介绍几种常用的分词方法。

word-based

首先能够想到的就是基于词(word-based)的分词方法。它通常易于实现,并且效果也不错。如下图所示,按照 word-based 方法进行分词,并得到它们的数值表示:

在这里插入图片描述

word-based 分割原始文本也有不同的具体方法。比如最直接地按照空格来分词,是用 Python 中字符串的 split() 方法即可实现:

tokenized_text = "Jim Henson was a puppeteer".split()
print(tokenized_text)# 输出:
['Jim', 'Henson', 'was', 'a', 'puppeteer']

也有一些加入标点的 word-based 分词方法的变体,通过这一类的 tokenizer,最终会得到一个相当大的词表,其中包含所有在语料库中出现过的 token。每个单词会被分配到一个 ID,ID 的值从 0 开始,一直到词表的总大小结束。模型使用这些 ID 来表示每一个单词。

如果想用 word-based 的 tokenizer 来完全覆盖一种语言,需要为语言中的每个单词都有一个 token,这将会有大量的 token。例如,英语中有超过 500000 个单词,要建立从每个单词到输入 ID 的映射,需要跟踪这么多ID。此外,像 “dog” 这样的词与 “dogs” 一样的词的表达方式不同,模型最初无法知道 “dog” 和 “dogs” 是相似的:它会将这两个词识别为不相关。这同样适用于其他类似的词,如 “run” 和 “running”,模型在学习开始前也不会将其视为相似。

另外,我们还要需要一个自定义标记来表示词汇表中没有的单词。这被称为 “unknown” token,通常表示为 “[UNK] ”或 “”。如果我们的 tokenizer 生成了大量的 unknown token,这通常是一个不好的迹象,因为这表明它无法检索一个单词的合理表示,并且在这一过程中丢失了信息。制定词表需要 tokenizer 产生尽可能少的 unknown token。

解决过多 unknown token 的方法是按照更深一层进行分词,即 character-based tokenizer。

character-based

character-based tokenizer 将文本划分为字符,这样做有两个显然的好处:

  • 词表会小得多
  • unknown token 会少得多,缓解 oov(out of vocabulary)问题,因为每个单词都是由英文字母组成

在这里插入图片描述

这种方法同样不完美,由于现在的表示是基于字符而不是单词,很明显每个字母是没有什么意义的。但这也因语言而异;例如,在汉语中,每个字符比拉丁语中的一个字符包含更多的信息。

另一个问题是,这种方法会需要大量的 token:一个单词使用 word-based 的 tokenizer 只需要一个 token,但当使用 character-based 方法时,则需要 10 个甚至更多 token,

为了两全其美,我们一般使用结合了这两种方法的技术:subword tokenization

subword tokenization

子单词标记化算法依赖于这样一个原则:频繁使用的单词不应该被拆分成更小的子单词,而少见的单词可以被分解成有意义的子词。

例如,“annoyingly” 可能被认为是一个少见的词,可以分解为 “annoying” 和“ly”。这两个词都可能更频繁地作为独立的子词出现,同时 “annoyingly” 的含义可以被 “annoying” 和 “ly” 的复合所表示。

下面是一个使用子词表示法进行分词的例子:

在这里插入图片描述

subword 分词法最终可以得到一个具有丰富语义的词表,比如上例中的 “tokenization” 被分为了 “token” 和 “ization”,这两个 token 都是有明确含义的。而且在空间上也比较高效,只需要两个 token 就可以组成一个长单词。

这种方法在土耳其语等语言中尤其有用,在这些语言中,通过将子词串在一起,可以形成(几乎)任意长的复杂单词。

这里再介绍几种具体的目前常用分词方法:

  • Byte-level BPE, GPT-2 的分词方式
  • WordPiece, BERT 的分词方式
  • SentencePiece和Unigram, 支持多语言模型

现在大家应该对常见分词方式有了一定的了解,接下来将介绍相关的 API。

保存和加载

保存和加载 tokenizers 与保存/加载 models 一样简单。还是使用 from_pretrained()save_pretrained() 两个方法。这两个方法会加载或保存 tokenizer 使用的算法(类似于保存模型的架构)和它生成的词表(类似于保存模型的权重参数)。

加载 BERT 的 tokenizer 与加载 BERT 权重的方式类似,只需要替换为 BertTokenizer 类就可以了:

from transformers import BertTokenizertokenizer = BertTokenizer.from_pretrained("bert-base-cased")

AutoModel 类似,AutoTokenizer 可以自动地选择库中合适的 tokenizer 类:

from transformers import AutoTokenizertokenizer = 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")

我们将在之后讨论 token_type_ids 和 attention_mask。现在,我们先来看看 input_ids 是如何生成的。为此,我们需要查看 tokenizer 的中间方法。

Encoding

将文本转换为数字的过程称为 encoding,它由两步组成:分词、将分词结果转换为输入 ID。

第一步就是将文本分割为单词(或者子词、标点等),即前面介绍的分词,得到 tokens。有多种规则可以实现该过程,这就是为什么需要使用模型的名称来实例化 tokenizer,是为了确保使用的规则与预训练模型时使用的规则相同。

第二步是将这些 tokens 转换为数字,然后可以将它们转换为张量并输入到模型中。tokenizer 有一张词表来记录 token 到数值的映射,在推理或微调时,需要保证与预训练时的词表是一致的。

接了更高的理解这两个步骤,我们将分别看一下两个步骤的过程。注意在实际使用中并不需要这样做,直接调用 tokenizer 并将你的输入传入即可。

分词

分词的过程由 tokenizer 的 tokenize() 方法完成:

from transformers import AutoTokenizertokenizer = AutoTokenizer.from_pretrained("bert-base-cased")sequence = "Using a Transformer network is simple"
tokens = tokenizer.tokenize(sequence)print(tokens)# 输出:
['Using', 'a', 'transform', '##er', 'network', 'is', 'simple']

上例中的 tokenizer 是一个 subword tokenizer,它不断地将单词进行拆分,直至可以完全被词表中的 tokens 表示。例子中的 transformer 被拆分为 transform 和 ##er。

转换为数值

将分词结果转换为 input IDs 的步骤由 convert_tokens_to_ids() 方法来实现:

ids = tokenizer.convert_tokens_to_ids(tokens)print(ids)
# 输出:
[7993, 170, 11303, 1200, 2443, 1110, 3014]

这些输出,在转换为对应框架的张量后,可以直接作为模型的输入。

Decoding

decoding 与 encoding 相反:根据词表索引,得到一个字符串。可以由 decode() 方法实现:

decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)# 输出:
'Using a Transformer network is simple'

decode() 方法不仅将索引转换回 tokens,还将属于同一单词的 token 合并在一起,生成可读的句子。

现在,已经介绍了 tokenizer 的各种原子操作:分词、转换为 ID,ID 转回字符串。然而,这只是冰山一角,接下来将介绍它的限制以及如何克服。

处理多序列

在上一节中,我们探讨了最简单的用例:对单个长度较小的序列进行推理。然而,实际中有很多新问题:

  • 如何处理多个序列?
  • 如何处理不同长度的多个序列?
  • 模型只能接收词表索引作为输入吗?
  • 是否存在序列过长的问题?

让我们看一下怎么使用 Transformer API 来处理这些问题。

批量输入模型

在之前已经介绍过如何将序列转换为数字列表。现在把这个数字列表转换为张量,并将其送入到模型:

import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassificationcheckpoint = "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)
# 这一行会报错
model(input_ids)# 报错:
IndexError: Dimension out of range (expected to be in range of [-1, 0], but got 1)

为什么会报错呢?是因为我们这里将单个序列送入了模型,而 Transformer 模型默认接收多个序列。这里我们试图按照上节的介绍手动实现 tokenizer 的内部步骤,但发现行不通。实际上 tokenizer 不止是将文本转换为序列张量,而且还在张量上添加了一个维度。

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, AutoModelForSequenceClassificationcheckpoint = "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: [[ 1045,  1005,  2310,  2042,  3403,  2005,  1037, 17662, 12172,  2607, 2026,  2878,  2166,  1012]]
Logits: [[-2.7276,  2.8789]]

batching ,是指同时向模型输入多个句子。如果只有一个句子,也可以将它们打包起来:

batched_ids = [ids, ids]

这个 batch 包含两个相同的句子。

batching 使得模型在接收多个输入序列时能够正常工作。同时处理多个序列还有一个问题:当将多个序列打包在一起时,它们的长度可能会不同。而要转换为张量,必须是矩形的数组。我们通常采用填充来解决这个问题。

填充输入

下面这种非矩形的数组无法直接转换为张量

batched_ids = [[200, 200, 200],[200, 200]
]

为了解决这个问题,我们对输入进行填充,来使得它们有相同的长度,即得到矩形的数组。具体来说,我们在每个长度较短的序列后面添加一种特殊的 token:padding token。如果我们有 10 个 10 个 tokens 的句子和 1 个 20 个 tokens 的句子,就要通过填充将它们全部填充到 20 个 token。在上面的例子中,应该填充成下面这样:

padding_id = 100batched_ids = [[200, 200, 200],[200, 200, padding_id],
]

padding token id 可以通过 tokenizer.pad_token_id 查看。

现在,我们将单个句子和两个组成 batch 的句子分别送入模型看一下结果是否相同:

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>)

可以看到,出现了一些问题。我们期望的结果是将两个序列一同送入时每个句子的输出结果与单个句子分别送入时的输出结果一致,但是看第二个句子,输出结果明显是不一样的。

这是因为 Transformer 模型的自注意力机制会关注到序列中的每一个 token,这当然会将我们填充的 token 也计算在内。当输入不同长度的句子经过填充统一长度时,我们需要告诉模型每个句子的真实长度,即哪些是实际的数据 token,哪些是为了保持长度一致填充的 token。实际中我们会通过传入 attention masks 来实现这一点。

attention masks

attention masks 是与输入张量同样形状的一个张量,由 0 和 1 组成。1 表示对应的 token 是实际的数据 token 需要计算注意力,而 0 表示对应的 token 是填充的 token 不需要计算注意力。

上面的例子应该修改为这样:

batched_ids = [[200, 200, 200],[200, 200, tokenizer.pad_token_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>)

现在我们的第二个句子就得到了预期中的结果,

更长的序列

在 Transformer 模型中,我们可以传递给模型的序列长度是有限制的。大部分模型可以处理 最多512 或 1024 个 token 的序列,再长就无能为力了。如果需要处理更长的序列,有两种方案:

  • 使用一个支持更长序列的模型
  • 对输入序列进行截断

不同的模型能够处理的最长序列长度不同,有些模型擅长处理长序列。比如 Longformer 和 LED 等。如果你的任务中都是非常长的序列,那么推荐使用这些模型,否则,推荐使用 max_sequence_length 对个别超出长度上限的序列进行截断。

sequence = sequence[:max_sequence_length]

整合到一起

在之前几节,我们已经分别介绍了 tokenizer 的几个步骤,包括分词、转换到 ID、填充、截断、注意力掩码。然而 Transformer API 可以通过一个高层的函数处理所有这些过程。当我们调用 tokenizer 并传入句子时,得到的返回值就是可以直接传入模型的输入。

from transformers import AutoTokenizercheckpoint = "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 包含了所有模型所需要的输入。对于 distilBERT,这包括 input IDsattention mask 。对于其他模型,可能会有其他所需要的输入,同样会由 tokenizer 对象返回出来。

如下面例子所示,这个方法十分强大。首先,它可以处理单个序列:

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")# 将序列填充到模型支持的最大长度(对于BERT或distilBERT,为512)
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!"]# 将序列截断到模型支持的最大长度(对于BERT或distilBERT,为512)
model_inputs = tokenizer(sequences, truncation=True)# 将大于最大长度的序列进行截断
model_inputs = tokenizer(sequences, max_length=8, truncation=True)

tokenizer 还可以将数值转换到特定框架的张量类型,可以直接输入到模型中:

sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"]# PyTorch
model_inputs = tokenizer(sequences, padding=True, return_tensors="pt")# TensorFlow
model_inputs = tokenizer(sequences, padding=True, return_tensors="tf")# NumPy
model_inputs = tokenizer(sequences, padding=True, return_tensors="np")

特殊token

查看 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,我们可以对上述序列进行 decode:

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."

tokenizer 在开始前添加了一个特殊 token [CLS] ,在结束后添加了一个特殊 token [SEP] 。这是因为 BERT 模型在预训练时任务中需要这两个额外的特殊 token,所以为了在推理时保持结果一致,也需要添加。不同的模型可能会需要添加不同的特殊 token,有的模型只在后面加,有的只在前面加,有的不需要加。总之,tokenizer 都会知道需要怎么加,并帮你做好。

总结:从tokenizer到model

现在我们已经介绍完了 tokenizer 处理文本时的全部步骤,让我们最后看一下它是如何处理多序列(填充)、如何处理长序列(截断)以及如何返回不同框架的张量类型:

import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassificationcheckpoint = "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)

概括一下,在本章中,我们:

  • 学习了 Transformer 模型的基本构建块。
  • 了解了 tokenizer pipeline 的组成。
  • 了解如何在实践中使用 Transformer 模型。
  • 学习了如何利用标 tokenizer 将文本转换为模型可以理解的张量。
  • 同时设置 tokenizer 和 model,从文本到预测。
  • 学习了输入 ID 的限制,并学习了注意力掩码。
  • 使用多功能和可配置的 tokenizer 方法。

从现在起,你应该能够自由地浏览 Transformer 的文档:其中术语会听起来很熟悉,你已经看到了大多数时候会用到的方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/532370.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mysql精讲_Mysql 索引精讲

开门见山&#xff0c;直接上图&#xff0c;下面的思维导图即是现在要讲的内容&#xff0c;可以先有个印象&#xff5e;常见索引类型(实现层面)索引种类(应用层面)聚簇索引与非聚簇索引覆盖索引最佳索引使用策略1.常见索引类型(实现层面)首先不谈Mysql怎么实现索引的,先马后炮一…

RT-Smart 官方 ARM 32 平台 musl gcc 工具链下载

前言 RT-Smart 的开发离不开 musl gcc 工具链&#xff0c;用于编译 RT-Smart 内核与用户态应用程序 RT-Smart musl gcc 工具链代码当前未开源&#xff0c;但可以下载到 RT-Thread 官方编译好的最新的 musl gcc 工具链 ARM 32位 平台 比如 RT-Smart 最好用的 ARM32 位 qemu 平…

OpenAI Whisper论文笔记

OpenAI Whisper论文笔记 OpenAI 收集了 68 万小时的有标签的语音数据&#xff0c;通过多任务、多语言的方式训练了一个 seq2seq &#xff08;语音到文本&#xff09;的 Transformer 模型&#xff0c;自动语音识别&#xff08;ASR&#xff09;能力达到商用水准。本文为李沐老师…

【经典简读】知识蒸馏(Knowledge Distillation) 经典之作

【经典简读】知识蒸馏(Knowledge Distillation) 经典之作 转自&#xff1a;【经典简读】知识蒸馏(Knowledge Distillation) 经典之作 作者&#xff1a;潘小小 知识蒸馏是一种模型压缩方法&#xff0c;是一种基于“教师-学生网络思想”的训练方法&#xff0c;由于其简单&#xf…

深度学习三大谜团:集成、知识蒸馏和自蒸馏

深度学习三大谜团&#xff1a;集成、知识蒸馏和自蒸馏 转自&#xff1a;https://mp.weixin.qq.com/s/DdgjJ-j6jHHleGtq8DlNSA 原文&#xff08;英&#xff09;&#xff1a;https://www.microsoft.com/en-us/research/blog/three-mysteries-in-deep-learning-ensemble-knowledge…

在墙上找垂直线_墙上如何快速找水平线

在装修房子的时候&#xff0c;墙面的面积一般都很大&#xff0c;所以在施工的时候要找准水平线很重要&#xff0c;那么一般施工人员是如何在墙上快速找水平线的呢&#xff1f;今天小编就来告诉大家几种找水平线的方法。一、如何快速找水平线1、用一根透明的软管&#xff0c;长度…

Vision Transformer(ViT)PyTorch代码全解析(附图解)

Vision Transformer&#xff08;ViT&#xff09;PyTorch代码全解析 最近CV领域的Vision Transformer将在NLP领域的Transormer结果借鉴过来&#xff0c;屠杀了各大CV榜单。本文将根据最原始的Vision Transformer论文&#xff0c;及其PyTorch实现&#xff0c;将整个ViT的代码做一…

Linux下的ELF文件、链接、加载与库(含大量图文解析及例程)

Linux下的ELF文件、链接、加载与库 链接是将将各种代码和数据片段收集并组合为一个单一文件的过程&#xff0c;这个文件可以被加载到内存并执行。链接可以执行与编译时&#xff0c;也就是在源代码被翻译成机器代码时&#xff1b;也可以执行于加载时&#xff0c;也就是被加载器加…

java 按钮 监听_Button的四种监听方式

Button按钮设置点击的四种监听方式注&#xff1a;加粗放大的都是改变的代码1.使用匿名内部类的形式进行设置使用匿名内部类的形式&#xff0c;直接将需要设置的onClickListener接口对象初始化&#xff0c;内部的onClick方法会在按钮被点击的时候执行第一个活动的java代码&#…

linux查看java虚拟机内存_深入理解java虚拟机(linux与jvm内存关系)

本文转载自美团技术团队发表的同名文章https://tech.meituan.com/linux-jvm-memory.html一, linux与进程内存模型要理解jvm最重要的一点是要知道jvm只是linux的一个进程,把jvm的视野放大,就能很好的理解JVM细分的一些概念下图给出了硬件系统进程三个层面内存之间的关系.从硬件上…

java function void_Java8中你可能不知道的一些地方之函数式接口实战

什么时候可以使用 Lambda&#xff1f;通常 Lambda 表达式是用在函数式接口上使用的。从 Java8 开始引入了函数式接口&#xff0c;其说明比较简单&#xff1a;函数式接口(Functional Interface)就是一个有且仅有一个抽象方法&#xff0c;但是可以有多个非抽象方法的接口。 java8…

java jvm内存地址_JVM--Java内存区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域&#xff0c;如图&#xff1a;1.程序计数器可以看作是当前线程所执行的字节码的行号指示器&#xff0c;通俗的讲就是用来指示执行哪条指令的。为了线程切换后能恢复到正确的执行位置Java多线程是…

java情人节_情人节写给女朋友Java Swing代码程序

马上又要到情人节了&#xff0c;再不解风情的人也得向女友表示表示。作为一个程序员&#xff0c;示爱的时候自然也要用我们自己的方式。这里给大家上传一段我在今年情人节的时候写给女朋友的一段简单的Java Swing代码&#xff0c;主要定义了一个对话框&#xff0c;让女友选择是…

java web filter链_filter过滤链:Filter链是如何构建的?

在一个Web应用程序中可以注册多个Filter程序&#xff0c;每个Filter程序都可以针对某一个URL进行拦截。如果多个Filter程序都对同一个URL进行拦截&#xff0c;那么这些Filter就会组成一个Filter链(也叫过滤器链)。Filter链用FilterChain对象来表示&#xff0c;FilterChain对象中…

java final static_Java基础之final、static关键字

一、前言关于这两个关键字&#xff0c;应该是在开发工作中比较常见的&#xff0c;使用频率上来说也比较高。接口中、常量、静态方法等等。但是&#xff0c;使用频繁却不代表一定是能够清晰明白的了解&#xff0c;能说出个子丑演卯来。下面&#xff0c;对这两个关键字的常见用法…

java语言错误的是解释运行的_Java基础知识测试__A卷_答案

考试宣言:同学们, 考试考多少分不是我们的目的! 排在班级多少的名次也不是我们的初衷!我的考试的目的是要通过考试中的题目,检查大家在这段时间的学习中,是否已经把需要掌握的知识掌握住了,如果哪道题目你不会做,又或者做错了, 那么不用怕, 考完试后, 导师讲解的时候你要注意听…

java 持续集成工具_Jenkins-Jenkins(持续集成工具)下载 v2.249.2官方版--pc6下载站

Jenkins是一款基于java开发的持续集成工具&#xff0c;是一款开源软件&#xff0c;主要用于监控持续重复的工作&#xff0c;为开发者提供一个开发易用的软件平台&#xff0c;使软件的持续集成变成可能。。相关软件软件大小版本说明下载地址Jenkins是一款基于java开发的持续集成…

java中线程调度遵循的原则_深入理解Java多线程核心知识:跳槽面试必备

多线程相对于其他 Java 知识点来讲&#xff0c;有一定的学习门槛&#xff0c;并且了解起来比较费劲。在平时工作中如若使用不当会出现数据错乱、执行效率低(还不如单线程去运行)或者死锁程序挂掉等等问题&#xff0c;所以掌握了解多线程至关重要。本文从基础概念开始到最后的并…

java类构造方法成员方法练习_面向对象方法论总结 练习(一)

原标题&#xff1a;面向对象方法论总结 & 练习(一)学习目标1.面向对象与面向过程2.类与对象的概念3.类的定义&#xff0c;对象的创建和使用4.封装5.构造方法6.方法的重载内容1.面向对象与面向过程为什么会出现面向对象反分析方法&#xff1f;因为现实世界太复杂多变&#x…

mysql 统计查询不充电_MySql查询语句介绍,单表查询,来充电吧

mysql在网站开发中&#xff0c;越来越多人使用了&#xff0c;方便部署&#xff0c;方便使用。我们要掌握mysql,首先要学习查询语句。查询单个表的数据&#xff0c;和多个表的联合查询。下面以一些例子来先简单介绍下单表查询。操作方法01首先看下我们例子用到的数据表&#xff…