当前位置: 代码迷 >> 综合 >> Hugging Face主页课程第二篇《 Using Transformers》
  详细解决方案

Hugging Face主页课程第二篇《 Using Transformers》

热度:32   发布时间:2023-12-01 02:56:06.0

Using ? Transformers

文章目录

  • Using ? Transformers
    • 本章简介
    • 1. Transformers简介
    • 2. 管道背后的故事
      • tokenizer预处理
      • Going through the model
      • 高维向量
      • Model heads: Making sense out of numbers
    • 3.Models
      • 创建一个transformer
      • 不同的加载方式
      • 保存模型
      • 使用 Transformer 模型进行推理
    • 4. Tokenizers
      • Word-based词级分词
      • Character-based基于字符的分词
      • 加载和保存
      • 编码
      • 解码
    • 5. 多序列处理
      • 模型输入是分批的(batch)
      • Padding the inputs
      • Attention masks
      • 超长序列
    • 6. 组合处理
      • 特殊tokens
      • Wrapping up: From tokenizer to model
    • 7. 总结
    • 8. 章节测试

本文翻译自 Hugging Face主页Resources下的 course

说明:有的文章将token、Tokenizer、Tokenization翻译为令牌、令牌器和令牌化。虽然从意义上来说更加准确,但是笔者感觉还是不够简单直接,不够形象。所以文中有些地方会翻译成分词、分词器和分词,有些地方又保留英文(有可能google翻译成标记、标记化没注意到)。有其它疑问可以留言或者查看原文。

本章简介

在第 1 章中,我们使用pipeline API 将 Transformer 模型用于不同的任务。 尽管此 API 功能强大且方便,但了解其内部工作原理很重要,这样我们才能灵活地解决其他问题。

在本章中,您将学习:

  • 如何使用标记器和模型来复制管道 API 的行为
  • 如何加载和保存模型和分词器
  • 不同的标记化方法,例如基于词、基于字符和基于子词
  • 如何处理不同长度的多个句子

1. Transformers简介

(Transformers 库API可以加载、训练和保存任何 Transformer 模型,模型的前向传递完全定义在单个-文件中(All in one file)。模型更易于理解,且一个模型上进行试验,不会影响其他模型
本章将end-to-end为例,使用一个模型和一个tokenizer 来重现第 1 章中介绍的管道 API

Transformer 模型通常非常大。拥有数百万到数百亿的参数,训练和部署这些模型是一项复杂的工作。此外,由于几乎每天都会发布新模型,并且每个模型都有自己的实现,因此尝试所有这些模型并非易事。

? Transformers 库目标是提供一个单一的 API,通过它可以加载、训练和保存任何 Transformer 模型。其主要特点是:

  • 易于使用:只需两行代码即可下载、加载和使用最先进的 NLP 模型进行inference。
  • 灵活性:从本质上讲,所有模型都是简单的 PyTorch nn.Module 或 TensorFlow tf.keras.Model 类,并且可以像机器学习 (ML) 框架中的任何其他模型一样进行处理。
  • 简单性:在整个库中几乎没有任何抽象。 “一体化文件(All in one file)”是一个核心概念:模型的前向传递完全定义在单个文件中,因此代码本身是可理解和可破解的。

最后一个功能使 ? Transformers 与其他 ML 库完全不同。模型不是建立在跨文件共享的模块上的;相反,每个模型都有自己的层。除了使模型更易于理解之外,这还使您可以轻松地在一个模型上进行试验,而不会影响其他模型。

本章将从一个end-to-end的例子开始,我们使用一个模型和一个tokenizer 来复制第 1 章中介绍的管道 API。

  • model API:我们将深入研究model和configuration(配置)类,并向您展示如何加载模型以及它如何处理数值输入以输出预测。
  • tokenizer API:管道的另一个主要组件。 tokenizer负责第一个和最后一个处理步骤,处理神经网络从文本到数字输入的转换,以及在需要时转换回文本
    -通过模型在一个准备好的batch中处理多个句子,然后仔细研究高级tokenizer函数来封装(wrap it all up)。

为了从 Model Hub 和 ? Transformers 提供的所有功能中受益,我们建议您创建一个帐户。

2. 管道背后的故事

本节代码Open in Colab (PyTorch)
YouTube视频:what happend inside the pipeline function
让我们从一个完整的例子开始,看看当我们在第 1 章中执行以下代码时,幕后发生了什么:


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}]

正如我们在第 1 章中看到的,这个管道将三个步骤组合在一起:预处理、通过模型传递输入和后处理:
full_nlp_pipeline
让我们快速浏览一下这些内容。

tokenizer预处理

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

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

使用 AutoTokenizer 类及其 from_pretrained 方法,以保证所有这些预处理都以与模型预训练时完全相同的方式完成。设定模型的 checkpoint名称,它会自动获取与模型的Tokenizer关联的数据并缓存它(所以它只在你第一次运行下面的代码时下载)。

由于情感分析管道的默认检查点是 distilbert-base-uncased-finetuned-sst-2-english我们运行以下命令:

from transformers import AutoTokenizercheckpoint = "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]])
}

输出的字典包含两个键:

  • input_ids 包含两行整数(每个句子一个),它们是每个句子中标记的唯一标识符。
  • attention_mask:

Going through the model

我们可以像使用分词器一样下载我们的预训练模型。 ? Transformers 提供了一个 AutoModel 类,它也有一个 from_pretrained 方法:

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

在此代码片段中,我们下载了之前在管道中使用的相同checkpoint(它实际上应该已经被缓存)并用它实例化了一个模型。

这个架构只包含基本的 Transformer 模块:给定一些输入,它输出我们称之为隐藏状态 hidden states的东西,也称为特征。对于每个模型输入,我们将检索一个高维向量,表示 Transformer 模型对该输入的上下文理解。

不同的任务有不同的head。这些隐藏状态本身就很有用,但它们通常是作为head的输入。这样就可以使用相同的体系结构执行不同的任务。

高维向量

Transformer 模块输出的向量通常很大。 它一般具有三个维度:

  • batch size:一次处理的序列数(在我们的示例中为 2)。
  • 序列长度:序列的数字表示的长度(在我们的示例中为 16)。
  • Hidden size:每个模型输入的向量维度。
    由于最后一个值,它被称为“高维”。 Hidden size可能非常大(对于较小的模型,通常为 768,在较大的模型中可以达到 3072 或更多)。

将预处理过的输入传入模型,得到:

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: Making sense out of numbers

Model heads:将隐藏状态的高维向量作为输入,并将它们投影到不同的维度上。 它们通常由一个或几个线性层组成:
transformer_and_head
在此图中,模型由其embeddings layer和后续层表示。embeddings layer将输入进行预处理,每个输入 ID转换为表示对应token的向量。 随后的层使用注意力机制操纵这些向量以产生句子的最终表示。

? Transformers 中有许多不同的架构可用,每一种架构都围绕着处理特定任务而设计。 这是一个非详尽列表:

  • Model (retrieve the hidden states)
  • ForCausalLM
  • ForMaskedLM
  • ForMultipleChoice
  • ForQuestionAnswering
  • ForSequenceClassification
  • ForTokenClassification
  • and others ?
    在我们的示例中,我们需要一个带有序列分类的模型head(能够将句子分类为正面或负面)。 因此,我们实际上不会使用 AutoModel 类,而是使用 AutoModelForSequenceClassification:
from transformers import AutoModelForSequenceClassificationcheckpoint = "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 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>)

这次输出是可识别的概率分数。

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

model.config.id2label
{
    0: 'NEGATIVE', 1: 'POSITIVE'}

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

第一句:NEGATIVE:0.0402,POSITIVE:0.9598
第二句:NEGATIVE:0.9995,POSITIVE:0.0005

我们已经成功地重现了管道的三个步骤:

  • 使用分词器进行预处理、
  • 通过模型传递输入
  • 后处理

现在让我们花点时间深入了解每个步骤。

??快来试试吧! 选择两个(或更多)你自己的文本,并通过sentiment-analysis pipeline运行它们。 然后自己复制您在本节看到的步骤,并检查您是否获得了相同的结果!

3.Models

在本节中,我们将仔细研究创建和使用模型。 我们将使用 AutoModel 类从checkpoint实例化任何模型。

AutoModel 类及其所有相关类实际上是库中各种可用模型的简单包装器。 它可以自动为您的checkpoint猜测合适的模型架构,然后使用该架构实例化模型。

但是,如果您知道要使用的模型类型,则可以直接使用定义其架构的类。 让我们来看看它如何与 BERT 模型配合使用。

创建一个transformer

初始化 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, BertModelconfig = BertConfig()
model = BertModel(config)# 模型已经随机初始化了

模型可以在这种状态下使用,但是会输出乱码; 它需要先训练。 我们可以根据手头的任务从头开始训练模型,这将需要很长时间和大量数据,并且会对环境产生不可忽视的影响。 为了避免不必要和重复的工作,必须能够共享和重用已经训练过的模型。

使用 from_pretrained 方法来加载一个已经训练过的 Transformer 模型:

from transformers import BertModelmodel = 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_computerconfig.json pytorch_model.bin

如果您查看 config.json 文件,您将认识到构建模型架构所需的属性。 该文件还包含一些元数据,例如检查点的来源以及您上次保存检查点时使用的 ? Transformers 版本。

pytorch_model.bin 文件被称为状态字典; 它包含您模型的所有权重。 这两个文件齐头并进; 配置configuration是了解模型架构所必需的,而模型权重model weights是模型的参数。

使用 Transformer 模型进行推理

现在您知道如何加载和保存模型,让我们尝试使用它进行一些预测。 Transformer 模型只能处理数字——tokenizer生成的数字。 但在我们讨论分词器之前,让我们探讨一下模型接受哪些输入。

tokenizer可以负责将输入转换为适当框架的张量,但为了帮助您了解发生了什么,我们将快速了解在将输入发送到模型之前必须完成的操作。

假设我们有几个序列:

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

分词器tokenizer将这些转换为词汇索引,通常称为输入 ID。 每个序列现在都是一个数字列表! 结果输出是:

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

这是一个编码序列列表。 张量只接受矩形(想想矩阵)。 这个“数组”已经是矩形的,所以将它转换为张量很容易:

import torchmodel_inputs = torch.tensor(encoded_sequences)

使用张量作为模型的输入
在模型中使用张量非常简单——我们只用输入调用模型:

output = model(model_inputs)

虽然模型接受许多不同的参数,但只有输入 ID是必需的。我们需要仔细研究,构建出 Transformer 模型可以理解的tokenizers。 (稍后将解释其他参数的作用以及何时需要它们)

4. Tokenizers

Tokenizers是 NLP 管道的核心组件之一。 模型只能处理数字,因此分词器需要将我们的文本输入转换为数值型的数据。 在本节中,我们将探讨标记化管道tokenization pipeline中发生的事情。

在 NLP 任务中,通常处理的数据是原始文本。 这是此类文本的示例:

Jim Henson was a puppeteer

但是,模型只能处理数字,因此我们需要找到一种将原始文本转换为数字的方法。 这就是分词器所做的,并且有很多方法可以做到这一点。我们的目标是找到对模型最有意义的表示。如果可能的话,找到最短的表示。

让我们看一下tokenization算法的一些示例,并尝试回答您可能对tokenization提出的一些问题。

Word-based词级分词

想到的第一种标记器是基于单词的。 它通常很容易设置和使用,只需几条规则就会产生不错的结果。 在下图中,目标是将原始文本拆分为单词并为每个单词找到一个数字表示:
word_based_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。

Character-based基于字符的分词

基于字符的tokenizer将文本拆分为字符characters,而不是单词。 这有两个主要好处:

  • 词汇量要小得多。
  • 词汇表外(未知)标记要少得多,因为每个单词都可以从字符构建。

但是这里也出现了一些关于空格和标点符号的问题:
character_based_tokenization.
这种方法也不是完美的。 每个字符本身并没有多大意义,英文单词就是这种情况。 但是,这又因语言而异。 在中文中,每个字符比拉丁语言中的字符包含更多信息。

使用基于单词的分词器,每个token是一个单词,但对于Character-based tokenizer,一个单词会很容易变成 10 个或更多的tokens。这使得我们的模型最终会处理大量的tokens 。

为了两全其美,我们可以使用结合这两种方法的第三种技术:子词分词subword tokenization。
Subword tokenization子词分词
子词分词算法原则:常用词不会拆分为更小的子词,而是将少见词(低频词)分解为有意义的子词。

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

这是一个示例,展示了子词分词算法如何对序列“Let’s do tokenization!!”进行分词:
bpe_subword
这些子词最终提供了很多语义含义:例如,在上面的示例中,“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 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")

先让我们看看 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的限制,并看看如何克服这些限制。

5. 多序列处理

在上一节中,我们探讨了最简单的用例:对单个小长度序列进行处理。 然而,实际中还有一些其他问题:

  • 我们如何处理不同长度的多个序列?
  • 词汇索引vocabulary indices是唯一能让模型正常工作的输入方式吗?
  • 会不会有序列太差的情况?
    让我们仔细看看这些问题,并研究如何使用 ? Transformers API 来解决它们。

模型输入是分批的(batch)

上节中,我们将序列转换为数字列表。 现在让我们将这些数字列表转换为张量并发送到模型:

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)
# 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, 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和生成的 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)。

Padding 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

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]

6. 组合处理

在最后几节中,我们一直在尽最大努力手工完成大部分工作。 我们已经探索了tokenizers的工作原理,并研究了分词tokenization、转换为input IDs、padding、截断和注意力掩码attention mask。

然而,正如我们在第 2 节中看到的,? Transformers API 可以通过处理以上所有步骤。 当你直接在句子上调用你的tokenizers时,得到的结果可以直接输入模型:

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输出一个含有两个键值对的字典
{
    '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")

特殊tokens

如果我们查看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情况,并将为您处理。

Wrapping up: From tokenizer to model

现在我们已经看到了tokenizer对象在处理文本时的每一步(单独步骤),让我们最后一次看看它如何处理多个序列(padding!)、超长序列(截断!)、多类型张量及其主要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)
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)

7. 总结

回顾本章,您:

  • 学习了 Transformer 模型的基本构建模块。
  • 了解了tokenization pipeline的组成部分。
  • 了解如何在实践中使用 Transformer 模型。
  • 学习了如何利用tokenizer将文本转换为模型可理解的张量。
  • 配置tokenizer和model,输入文本,输出预测结果。
  • 了解了input_ids 的局限性,并了解了注意力掩码attention masks。
  • 使用多功能和可配置的tokenizer方法。

从现在开始,您应该可以自由浏览 ? Transformers 文档:这些文档里的词汇听起来很熟悉,并且您会看到文档里使用的方法,大部分在前面课程已经使用过。

8. 章节测试

章节测试

  相关解决方案