Hugging Face NLP课程学习记录 - 2. 使用 Hugging Face Transformers

Hugging Face NLP课程学习记录 - 2. 使用 Hugging Face Transformers

说明:

2. 使用 Hugging Face Transformers

管道的内部(Behind the pipeline)

从例子开始:

python 复制代码
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!",
    ]
)

原始文本(Raw text) --> 分词器(Tokenizer) --> 模型(Model)--> 后处理/预测(Predictions)

使用分词器进行预处理(Preprocessing with a tokenizer)

与其他神经网络一样,Transformer模型无法直接处理原始文本, 因此我们管道的第一步是将文本输入转换为模型能够理解的数字。 为此,我们使用tokenizer,负责:

  • 将输入拆分为单词、子单词或符号(如标点符号),称为token
  • 将每个token映射到一个整数
  • 添加可能对模型有用的其他输入

我们使用AutoTokenizer类及其from_pretrained()方法获取与训练时相同的tokenizer。

python 复制代码
from transformers import AutoTokenizer

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

一旦我们有了分词器(tokenizer),我们可以直接将句子传递给它,我们会得到一个字典(dictionary),这个字典已经准备好输入到我们的模型中了!唯一剩下要做的就是将输入ID的列表转换成张量。

Transformers的后端可能是Pytorch,Tensorflow或者Flax。

Transformers模型只接受张量作为输入。

要指定要返回的张量类型(PyTorch、TensorFlow或plain NumPy),我们使用return_tensors参数:

python 复制代码
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)
{
    '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, 1, 0, 0, 0, 0, 0, 0, 0, 0]
    ])
}

输出本身是一个包含两个键的字典,分别是input_ids和attention_mask。input_ids包含两行整数(每句话一行),这些整数是每句话中词元(token)的唯一标识符。我们稍后会在本章解释attention_mask是什么。

了解模型(Go through the model)

我们可以像下载分词器(tokenizer)一样下载我们的预训练模型。

python 复制代码
from transformers import AutoModel

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

This architecture contains only the base Transformer module: given some inputs, it outputs what we'll call hidden states , also known as features . For each model input, we'll retrieve a high-dimensional vector representing the contextual understanding of that input by the Transformer model.

高维向量(A high-dimensional vector?)

Transformer输出的向量一般很大。通常有3个维度:

  • Batch size: 一次处理的序列数(在我们的示例中为2)。
  • Sequence length: 序列的数值表示的长度(在我们的示例中为16)。
  • Hidden size: 每个模型输入的向量维度。

之所以被称为高维,是因为Hidden size. Hidden size可能非常大(768通常用于较小的型号,而在较大的型号中,这可能达到3072或更大)。

如果我们将预处理的输入输入到模型中,我们可以看到这一点:

python 复制代码
outputs = model(**inputs)
print(outputs.last_hidden_state.shape)
torch.Size([2, 16, 768])

注意🤗 Transformers模型的输出与namedtuple或词典相似。您可以通过属性(就像我们所做的那样)或键(输出["last_hidden_state"])访问元素,甚至可以通过索引访问元素,前提是您确切知道要查找的内容在哪里(outputs[0])。

模型头:数字的意义(Model heads: Making sense out of numbers)

模型头将隐藏状态的高维向量作为输入,并将其投影到不同的维度。它们通常由一个或几个线性层组成:

![[en_chapter2_transformer_and_head.svg]]

Transformers模型的输出直接发送到模型头进行处理。

In this diagram, the model is represented by its embeddings layer and the subsequent layers. The embeddings layer converts each input ID in the tokenized input into a vector that represents the associated token. The subsequent layers manipulate those vectors using the attention mechanism to produce the final representation of the sentences.

Transformers中有许多不同的体系结构,每种体系结构都是围绕处理特定任务而设计的。以下是一个非详尽的列表:

  • *Model (retrieve the hidden states)
  • *ForCausalLM
  • *ForMaskedLM
  • *ForMultipleChoice
  • *ForQuestionAnswering
  • *ForSequenceClassification
  • *ForTokenClassification
  • 以及其他

对于我们的示例,我们需要一个带有序列分类头的模型(能够将句子分类为肯定或否定)。因此,我们实际上不会使用AutoModel类,而是使用AutoModelForSequenceClassification

python 复制代码
from transformers import AutoModelForSequenceClassification

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

现在,如果我们观察输出的形状,维度将低得多:模型头将我们之前看到的高维向量作为输入,并输出包含两个值的向量(每个标签一个):

python 复制代码
print(outputs.logits.shape)
torch.Size([2, 2])

对输出进行后处理( Postprocessing the output)

我们从模型中得到的输出值本身并不一定有意义。我们来看看,

python 复制代码
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)与实际损耗函数(如交叉熵)融合):

python 复制代码
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>)

现在我们可以看到,模型预测第一句为[0.0402, 0.9598],第二句为[0.9995, 0.0005]。这些是可识别的概率分数。

为了获得每个位置对应的标签,我们可以检查模型配置的id2label属性(下一节将对此进行详细介绍):

python 复制代码
model.config.id2label
{0: 'NEGATIVE', 1: 'POSITIVE'}

现在我们可以得出结论,该模型预测了以下几点:

  • 第一句:否定:0.0402,肯定:0.9598
  • 第二句:否定:0.9995,肯定:0.0005

模型(Models)

创建一个Transformer(Creating a Transformer)

初始化BERT模型需要做的第一件事是加载配置对象:

python 复制代码
from transformers import BertConfig, BertModel

# Building the config
config = BertConfig()

# Building the model from the config
model = BertModel(config)

配置包含许多用于构建模型的属性:

python 复制代码
print(conrfig)
BertConfig {
  [...]
  "hidden_size": 768,
  "intermediate_size": 3072,
  "max_position_embeddings": 512,
  "num_attention_heads": 12,
  "num_hidden_layers": 12,
  [...]
}

不同的加载方式(Different loading methods)

从默认配置创建模型会使用随机值对其进行初始化:

python 复制代码
from transformers import BertConfig, BertModel

config = BertConfig()
model = BertModel(config)

# Model is randomly initialized!

该模型可以在这种状态下使用,但会输出胡言乱语;首先需要对其进行训练。

加载已经训练过的Transformers模型很简单 - 我们可以使用from_pretrained() 方法:

python 复制代码
from transformers import BertModel

model = BertModel.from_pretrained("bert-base-cased")

正如您之前看到的,我们可以用等效的AutoModel类替换Bert模型。

区别:

  • 使用AutoModel类时,我们是不知道模型的检查点(checkpoint)的。
  • 使用BertModel.from_pretrained("bert-base-cased")时,通过指定的标识符bert-base-cased加载预训练模型,这是BERT作者训练出来的模型检查点(model checkpoint)。

保存模型(Saving methods)

我们使用 save_pretrained() 方法保存模型:

python 复制代码
model.save_pretrained("directory_on_my_computer")

使用Transformers模型进行推理(Using a Transformer model for inference)

Transformer模型只能处理数字------分词器生成的数字。但在我们讨论分词器之前,让我们先探讨模型接受哪些输入。

分词器(Tokenizer)负责将输入转换为合适框架(Pytorch,Tensorflow或Flax)的张量。

假设我们有几个序列:

python 复制代码
sequences = ["Hello!", "Cool.", "Nice!"]

分词器将他们转换为词汇表索引(vocabulary indices),通常我们称其为Input IDs. 现在每个序列都是一个数字列表!结果是:

python 复制代码
encoded_sequences = [
    [101, 7592, 999, 102],
    [101, 4658, 1012, 102],
    [101, 3835, 999, 102],
]

这是一个编码后的序列列表。将其转换为张量(tensors):

python 复制代码
import torch

model_inputs = torch.tensor(encoded_sequences)

使用张量作为模型的输入(Using the tensors as inputs to the model)

python 复制代码
output = model(model_inputs)

分词器(Tokenizers)

分词器是自然语言处理(NLP)流水线的核心组件之一。它们有一个目的:将文本转换成模型可以处理的数据。模型只能处理数字,因此分词器需要将我们的文本输入转换为数值数据。

在 NLP 任务中,通常处理的数据是原始文本。如:

Jim Henson was a puppeteer

然而,模型只能处理数字,所以我们需要找到一种方法将原始文本转换为数字。这就是分词器的作用,而且有很多方法可以实现这一点。目标是找到最有意义的表示形式------即对模型来说最有意义的那种------如果可能的话,还要找到最小的表示形式。

让我们看看一些分词器算法的例子。

基于词的(Word-based)

首先想到的分词器类型是基于单词的。它通常非常容易设置和使用,只需要一些规则,而且通常能产生不错的结果。

例如,对于原始文本Let's do tokenization!

基于空格(space)拆分:Let's do tokenization!

基于标点(Punctuation)拆分:Let 's do tokenization !

我们可以通过应用Python的split()函数,使用空格将文本分词为单词:

python 复制代码
tokenized_text = "Jim Henson was a puppeteer".split()
print(tokenized_text)
python 复制代码
['Jim', 'Henson', 'was', 'a', 'puppeteer']

有许多基于词的分词器(word tokenizers)的变体,它们有额外的关于标点的规则。

使用这种分词器,我们可能会得到一些相当大的"词汇表",其中词汇表定义为我们语料库中独立词元(token)的总数。

每个单词都被分配一个ID,从0开始,一直到词汇表的大小。模型使用这些ID来识别每个单词。

如果我们想用基于词的分词器完全覆盖一种语言,那么我们需要为该语言中的每个单词分配一个标识符,这将生成大量的词元。例如,英语中有超过50万个单词,因此为了将每个单词映射到一个输入ID,我们需要跟踪这么多的ID。此外,像"dog"这样的单词与"dogs"这样的单词表现不同,而模型最初并无法知道"dog"和"dogs"是相似的:它会将这两个单词识别为不相关。同样的情况也适用于其他类似的单词,比如"run"和"running",模型最初也不会认为它们是相似的。

最后,我们需要一个自定义的词元来表示不在词汇表中的单词。这通常称为"未知"词元,通常表示为"[UNK]"或"<unk>"。如果你发现分词器生成了大量这样的词元,通常这是一个不好的迹象,因为它无法为某个单词找到合适的表示形式,导致在此过程中丢失了信息。在构建词汇表时,目标是尽可能减少分词器将单词分词为未知词元的情况。

减少未知词元数量的一种方法是更进一步,使用基于字符的分词器(character-based tokenizer)。

基于字符的(Character-based)

基于字符的分词器将文本分割成字符,而不是单词。这有两个主要好处:

  1. 词汇表要小得多。
  2. 词汇表外(未知)的词元(token)要少得多,因为每个单词都可以由字符构建。

但在这里,同样会出现一些关于空格和标点符号的问题:

![[some questions arise concerning spaces and punctuation.png]]

这种方法也并不完美。由于表示现在基于字符而非单词,可以说从直观上看,这种方法的意义较小:每个字符本身并没有太多意义,而单词则不然。不过,这一点在不同语言中有所差异;例如,在中文中,每个字符所承载的信息比拉丁语系语言中的字符要多。

另一个需要考虑的问题是,我们的模型将需要处理大量的词元:在基于词的分词器中,一个单词只会是一个词元,而当其转换为字符时,很容易变成10个或更多的词元。

为了兼顾两种方法的优点,我们可以使用结合这两种方法的第三种技术:子词分词(subword tokenization)。

字词分词(subword tokenization)

子词分词算法基于这样一个原则:频繁使用的单词不应被拆分成更小的子词,而罕见的单词则应被分解为有意义的子词。

例如,"annoyingly"可能被认为是一个罕见的单词,可以被分解为"annoying"和"ly"。这两个部分作为独立的子词出现的频率可能更高,同时通过"annoying"和"ly"的组合含义,保留了"annoyingly"的意义。

以下是一个示例,展示了子词分词算法如何对序列"Let's do tokenization!"进行分词:

Let's</w> do</w> token ization</w> !</w>

这些子词提供了丰富的语义意义:例如,在上面的示例中,"tokenization"被拆分为"token"和"ization",这两个词元既具有语义意义,又在空间上很高效(仅需两个词元来表示一个长单词)。这使我们能够在较小的词汇表中实现相对较好的覆盖率,并几乎没有未知词元。

还有更多!(And more!)

不出所料,还有更多的技术。仅举几例:

  • Byte-level BPE, 用于 GPT-2
  • WordPiece, 用于 BERT
  • SentencePiece or Unigram, 用于多个多语言模型

加载和保存(Loading and saving)

加载和保存分词器与模型一样简单。实际上,它基于相同的两种方法:from_pretrained() 和 save_pretrained()。这些方法将加载或保存分词器使用的算法(有点类似于模型的架构)以及它的词汇表(有点类似于模型的权重)。

python 复制代码
from transformers import BertTokenizer

tokenizer = BertTokenizer.from_pretrained("bert-base-cased")

与AutoModel类似,AutoTokenizer类将根据检查点名称自动获取库中的适当分词器类,并且可以直接与任何检查点一起使用:

python 复制代码
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
python 复制代码
tokenizer("Using a Transformer network is simple")

保存分词器:

python 复制代码
tokenizer.save_pretrained("directory_on_my_computer")

编码(Encoding)

将文本转换为数字称为编码。编码是一个两步过程:首先是分词,然后是转换为input IDs。

  • 第一步是将文本拆分为单词(或单词的部分、标点符号等),通常称为词元(Token)。
  • 第二步是将这些词元转换为数字,以便我们可以根据它们构建张量并将其输入到模型中。为此,分词器有一个词汇表(vocabulary),这是我们在使用from_pretrained()方法实例化分词器时下载的部分。同样,我们需要使用在模型预训练时所使用的相同词汇表。

词元化(Tokenization)

分词过程是通过分词器的 tokenize() 方法完成的:

python 复制代码
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")

sequence = "Using a Transformer network is simple"
tokens = tokenizer.tokenize(sequence)

print(tokens)
['Using', 'a', 'Trans', '##former', 'network', 'is', 'simple']

这个分词器是一个子词分词器(subword tokenizer):它会拆分单词,直到获得可以由其词汇表表示的词元。在这里,"transformer"被拆分为两个词元:transform##er

从词元到Input IDs(From tokens to input IDs)

使用分词器(tokenizer)的 convert_tokens_to_ids() 方法进行转化:

python 复制代码
ids = tokenizer.convert_tokens_to_ids(tokens)

print(ids)
[7993, 170, 11303, 1200, 2443, 1110, 3014]

解码(Decoding)

python 复制代码
decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])

print(decoded_string)
Using a transformer network is simple

处理多个序列(Handling multiple sequences)

在上一节中,我们探讨了最简单的用例:对一个小长度的序列进行推理。然而,一些问题已经出现:

  • 我们如何处理多个序列?
  • 我们如何处理多个序列不同长度?
  • 词汇索引是让模型正常工作的唯一输入吗?
  • 是否存在序列太长的问题?

模型期望一批次输入(Models expect a batch of inputs)

在前面的练习中,你看到了序列如何被转换为数字列表。现在让我们将这个数字列表转换为张量并传递给模型:

python 复制代码
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)
# This line will fail
model(input_ids)

报错:

IndexError: Dimension out of range (expected to be in range of [-1, 0], but got 1)
python 复制代码
print(input_ids)
tensor([ 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012])
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]])
python 复制代码
print(input_ids.shape, tokenized_inputs["input_ids"].shape)
torch.Size([14]) torch.Size([1, 16])

需要添加一个新的维度:

python 复制代码
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: [[ 1045,  1005,  2310,  2042,  3403,  2005,  1037, 17662, 12172,  2607, 2026,  2878,  2166,  1012]]
Logits: [[-2.7276,  2.8789]]

批处理(Batching)是指一次性将多个句子传递给模型。如果你只有一个句子,你可以用单个序列构建一个批量。

这是一个包含两条相同序列的批量(Batches):

python 复制代码
batched_ids = [ids, ids]

批处理使得模型能够处理多个句子。使用多个序列与构建单个序列的批处理同样简单。然而,还有一个问题。当你尝试将两句(或更多句)放在一起进行批处理时,它们的长度可能不同。如果你曾经使用过张量,你就知道它们需要是矩形的形状,因此无法直接将输入ID列表转换为张量。为了解决这个问题,我们通常会对输入进行填充。

填充输入(Padding the inputs)

以下列表不能转换为张量:

python 复制代码
batched_ids = [
    [200, 200, 200],
    [200, 200]
]

为了解决这个问题,我们将使用填充(padding)来使我们的张量具有矩形形状。填充通过向较短的句子添加一个特殊的词(称为填充词元,padding token)来确保所有句子具有相同的长度。例如,如果你有10个句子,每个句子有10个单词,还有1个句子有20个单词,填充将确保所有句子都具有20个单词。在我们的示例中,生成的张量看起来像这样:

python 复制代码
padding_id = 100

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

填充词元的ID(padding token ID)可以在 tokenizer.pad_token_id 中找到。让我们使用它,并分别将两句话单独传递给模型以及一起进行批处理:

python 复制代码
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有些问题:第二行应该与第二个句子的logits相同,但我们得到了完全不同的值!

This is because the key feature of Transformer models is attention layers that contextualize each token. These will take into account the padding tokens since they attend to all of the tokens of a sequence. To get the same result when passing individual sentences of different lengths through the model or when passing a batch with the same sentences and padding applied, we need to tell those attention layers to ignore the padding tokens. This is done by using an attention mask.

这是因为 Transformer 模型的一个重要特点是注意力层(attention layer),它们对每个词元进行上下文化处理(contextualize each token)。这些层会考虑填充词元,因为它们会关注序列中的所有词元(token)。为了在将不同长度的单个句子传递给模型时,或在传递应用了填充的相同句子的批量(batch)给模型时获得相同的结果,我们需要告诉这些注意力层忽略填充词元。这是通过使用注意力掩码(attention mask)来实现的。

注意力掩码(Attention masks)

注意力掩码是与输入ID张量形状完全相同的张量,填充了0和1:1表示相应的词元应该被关注,而0表示相应的词元不应该被关注(即,它们应该被模型的注意力层忽略)。

让我们用attention mask完成上一个示例:

python 复制代码
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>)

现在我们得到了批量中第二个句子相同的逻辑值。(即[ 0.5803, -0.4125]print(model(torch.tensor(sequence2_ids)).logits)的输出保持一致)。

注意第二个序列的最后一个值是一个填充ID,在注意力掩码中是一个0值。

python 复制代码
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_1 = "I've been waiting for a HuggingFace course my whole life."
sequence_2 = "I hate this so much!"

tokens_1 = tokenizer.tokenize(sequence_1)
ids_1 = tokenizer.convert_tokens_to_ids(tokens_1)
input_ids_1 = torch.tensor([ids_1])
output_1 = model(input_ids_1)

tokens_2 = tokenizer.tokenize(sequence_2)
ids_2 = tokenizer.convert_tokens_to_ids(tokens_2)
input_ids_2 = torch.tensor([ids_2])
output_2 = model(input_ids_2)

print(input_ids_1)
print(input_ids_2)
print(output_1.logits)
print(output_2.logits)
tensor([[ 1045,  1005,  2310,  2042,  3403,  2005,  1037, 17662, 12172,  2607,
          2026,  2878,  2166,  1012]])
tensor([[1045, 5223, 2023, 2061, 2172,  999]])
tensor([[-2.7276,  2.8789]], grad_fn=<AddmmBackward0>)
tensor([[ 3.1931, -2.6685]], grad_fn=<AddmmBackward0>)
python 复制代码
batched_ids = [
    ids_1,
    ids_2 + [tokenizer.pad_token_id] * (input_ids_1.shape[1] - input_ids_2.shape[1])
]

attention_mask = [
    [1] * input_ids_1.shape[1],
    [1] * input_ids_2.shape[1] + [0] * (input_ids_1.shape[1] - input_ids_2.shape[1]),
]

outputs = model(torch.tensor(batched_ids), attention_mask=torch.tensor(attention_mask))
print(outputs.logits)
tensor([[-2.7276,  2.8789],
        [ 3.1931, -2.6685]], grad_fn=<AddmmBackward0>)

更长的序列(Longer sequences)

对于 Transformer 模型,传递给模型的序列长度是有限制的。大多数模型处理的序列长度最多为 512 或 1024 个词元,处理更长的序列时会崩溃。对此有两种解决方案:

  1. 使用支持更长序列长度的模型。
  2. 截断你的序列。

不同模型支持的序列长度各不相同,有些专门处理非常长的序列。Longformer 是一个例子,另一个是 LED。如果你正在处理需要非常长序列的任务,建议你查看这些模型。

否则,我们建议你通过指定 max_sequence_length 参数来截断你的序列:

python 复制代码
sequence = sequence[:max_sequence_length]

将所有内容放在一起(Put it all together)

当你直接调用分词器(tokenizer)处理句子时,你会得到可以直接传递给模型的输入:

python 复制代码
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)
print(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]}

正如我们将在下面的一些例子中看到的,这种方法非常强大。首先,它可以对单个序列进行分词:

python 复制代码
sequence = "I've been waiting for a HuggingFace course my whole life."

model_inputs = tokenizer(sequence)

还可以一次处理多个序列:

python 复制代码
sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"]

model_inputs = tokenizer(sequences)

It can pad according to several objectives:

python 复制代码
# Will pad the sequences up to the maximum sequence length
model_inputs = tokenizer(sequences, padding="longest")

# Will pad the sequences up to the model max length
# (512 for BERT or DistilBERT)
model_inputs = tokenizer(sequences, padding="max_length")

# Will pad the sequences up to the specified max length
model_inputs = tokenizer(sequences, padding="max_length", max_length=8)

它还可以截断序列:

python 复制代码
sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"]

# Will truncate the sequences that are longer than the model max length
# (512 for BERT or DistilBERT)
model_inputs = tokenizer(sequences, truncation=True)

# Will truncate the sequences that are longer than the specified max length
model_inputs = tokenizer(sequences, max_length=8, truncation=True)

tokenizer 对象可以处理到特定框架张量的转换,然后可以直接传递给模型。例如,在以下代码示例中,我们让 tokenizer 返回不同框架的张量 --- "pt" 返回 PyTorch 张量,"tf" 返回 TensorFlow 张量,"np" 返回 NumPy 数组:

python 复制代码
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")

特殊词元(Special tokens)

如果我们查看分词器返回的输入ID,会发现它们与之前的稍有不同:

python 复制代码
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序列,看看这是怎么回事:

python 复制代码
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]。这是因为模型在预训练时使用了这些词元,所以为了在推理时获得相同的结果,我们也需要添加它们。请注意,有些模型不会添加特殊词,或者会添加不同的特殊词;有些模型可能只在开头或结尾添加这些特殊词。不管怎样,分词器知道模型期望的特殊词,并会为你处理这一切。

总结:从分词器到模型(Wrapping up: From tokenizer to model)

现在我们已经了解了分词器对象在处理文本时所用的所有单独步骤,让我们最后一次看看它如何处理多个序列(填充!)、非常长的序列(截断!),以及使用其主要API处理多种类型的张量:

python 复制代码
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)