https://getupnote.com/share/notes/bUuTlgcsHYPa2JhSJMo36Rw0gIH2/01a07387-e58b-736c-b843-9e46b23056b3)
Transformer 的原始结构
Transformer 架构最初是为翻译而设计的。在训练期间,编码器接收特定语言的输入(句子),而解码器需要输出对应语言的翻译。在编码器中,注意力层可以使用一个句子中的所有单词(正如我们刚才看到的,给定单词的翻译可以取决于它在句子中的其他单词)。然而,解码器是按顺序工作的,并且只能注意它已经翻译过的句子中的单词。例如,当我们预测了翻译目标的前三个单词时,我们将它们提供给解码器,然后解码器使用编码器的所有输出来尝试预测第四个单词。
为了在训练过程中加快速度(当模型可以访问目标句子时),会将整个目标输入解码器,但不允许获取到要翻译的单词(如果它在尝试预测位置 2 的单词时可以访问位置 2 的单词,解码器就会偷懒,直接输出那个单词,从而无法学习到正确的语言关系!)。例如,当试图预测第 4 个单词时,注意力层只能获取位置 1 到 3 的单词。
最初的 Transformer 架构如下所示,编码器位于左侧,解码器位于右侧:
注意,解码器块中的第一个注意力层关联到解码器的所有(过去的)输入,但是第二个注意力层只使用编码器的输出。因此,它在预测当前单词时,可以使用整个句子的信息。这是非常有用的,因因为不同的语言可以有把词放在不同顺序的语法规则,或者句子后面提供的一些上下文可能有助于确定给定单词的最佳翻译。
也可以在编码器/解码器中使用_attention mask(注意力掩码层)_,以防止模型关注到某些特殊单词。例如,用于在批量处理句子时使所有输入长度一致的特殊填充词。
architecture(架构)与 checkpoints(权重参数又称检查点)
在本课程中,当我们深入探讨 Transformers 模型时,你将看到架构、参数和模型。这些术语的含义略有不同:
- architecture(架构):这是模型的骨架 —— 每个层的定义以及模型中发生的每个操作。
- Checkpoints(权重参数又称检查点):这些是将在给架构中结构中加载的权重参数,是一些具体的数值。
- Model(模型):这是一个笼统的术语,没有“架构”或“参数”那么精确:它可以指两者。为了避免歧义,本课程使用将使用架构和参数。
例如,BERT 是一个架构,而 bert-base-cased ,这是谷歌团队为 BERT 的第一个版本训练的一组权重参数,是一个参数。我们可以说“BERT 模型”和“ bert-base-cased 模型。”
编码器模型仅使用 Transformer 模型的编码器部分。在每次计算过程中,注意力层都能访问整个句子的所有单词,这些模型通常具有“双向”(向前/向后)注意力,被称为自编码模型。
这些模型的预训练通常会使用某种方式破坏给定的句子(例如:通过随机遮盖其中的单词),并让模型寻找或重建给定的句子。
“编码器”模型适用于需要理解完整句子的任务,例如:句子分类、命名实体识别(以及更普遍的单词分类)和阅读理解后回答问题。
该系列模型的典型代表有:
“解码器”模型仅使用 Transformer 模型的解码器部分。在每个阶段,对于给定的单词,注意力层只能获取到句子中位于将要预测单词前面的单词。这些模型通常被称为自回归模型。
“解码器”模型的预训练通常围绕预测句子中的下一个单词进行。
这些模型最适合处理文本生成的任务。
该系列模型的典型代表有:
编码器-解码器模型(也称为序列到序列模型)同时使用 Transformer 架构的编码器和解码器两个部分。在每个阶段,编码器的注意力层可以访问输入句子中的所有单词,而解码器的注意力层只能访问位于输入中将要预测单词前面的单词。
这些模型的预训练可以使用训练编码器或解码器模型的方式来完成,但通常会更加复杂。例如, T5 通过用单个掩码特殊词替换随机文本范围(可能包含多个词)进行预训练,然后目标是预测被遮盖单词原始的文本。
序列到序列模型最适合于围绕根据给定输入生成新句子的任务,如摘要、翻译或生成性问答。
该系列模型的典型代表有:
如果你打算在正式的项目中使用经过预训练或经过微调的模型。请注意:虽然这些模型是很强大,但它们也有局限性。其中最大的一个问题是,为了对大量数据进行预训练,研究人员通常会搜集所有他们能找到的所有文字内容,中间可能夹带一些意识形态或者价值观的刻板印象。
为了快速解释清楚这个问题,让我们回到一个使用 BERT 模型的 pipeline 的例子:
Copied
from transformers import pipeline
unmasker = pipeline("fill-mask", model="bert-base-uncased")
result = unmasker("This man works as a [MASK].")
print([r["token_str"] for r in result])
result = unmasker("This woman works as a [MASK].")
print([r["token_str"] for r in result])
Copied
['lawyer', 'carpenter', 'doctor', 'waiter', 'mechanic']
['nurse', 'waitress', 'teacher', 'maid', 'prostitute']
当要求模型填写这两句话中缺少的单词时,模型给出的答案中,只有一个与性别无关(服务员/女服务员)。其他职业通常与某一特定性别相关,妓女最终进入了模型中与“女人”和“工作”相关的前五位。尽管 BERT 是少数使用经过筛选和清洗后,并且看似中立的数据集上建立的的 Transformer 模型,而不是通过从互联网上搜集数据(它使用的是 Wikipedia 英文 和 BookCorpus 数据集),但是这种情况还是发生了。
因此,当你使用这些工具时,你需要记住,使用的原始模型的时候,很容易生成性别歧视、种族主义或恐同内容。这种固有偏见不会随着微调模型而消失。
让我们从一个完整的示例开始,看看在 第一章 中执行以下代码时在幕后发生了什么
Copied
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!",
]
)
获得如下输出:
Copied
[{'label': 'POSITIVE', 'score': 0.9598047137260437},
{'label': 'NEGATIVE', 'score': 0.9994558095932007}]
正如我们在 第一章 中看到的,这个 pipeline 集成了三个步骤:预处理、模型计算和后处理:
让我们先简单了解一下这三个步骤。
使用 tokenizer( tokenizer )进行预处理
与其他神经网络一样,Transformer 模型无法直接处理原始文本,因此我们管道的第一步是将文本输入转换为模型能够理解的数字。为此,我们使用 tokenizer( tokenizer ),它将负责:
- 将输入拆分为单词、子单词或符号(如标点符号),称为 token(标记)
- 将每个标记(token)映射到一个数字,称为 input ID(inputs ID)
- 添加模型需要的其他输入,例如特殊标记(如
[CLS]和[SEP])- 位置编码:指示每个标记在句子中的位置。
- 段落标记:区分不同段落的文本。
- 特殊标记:例如 [CLS] 和 [SEP] 标记,用于标识句子的开头和结尾。
在使用模型时所有这些预处理都需要与模型预训练时的方式完全相同,因此我们首先需要从 Model Hub 中下载这些信息。为此,我们使用 AutoTokenizer 类和它的 from_pretrained() 方法,并输入我们模型 checkpoint 的名称,它将自动获取与模型的 tokenizer 相关联的数据,并对其进行缓存(因此只有在你第一次运行下面的代码时才会下载)。
sentiment-analysis (情绪分析)管道默认的 checkpoint 是 distilbert-base-uncased-finetuned-sst-2-english (你可以在 这里 )看到它的模型卡片,我们运行以下代码:
Copied
from transformers import AutoTokenizer
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
当我们有了 tokenizer,我们就可以直接将我们的句子传递给它,我们就会得到一个 input ID(inputs ID) 的列表!剩下要做的唯一一件事就是将 input ID 列表转换为 tensor(张量)。
你在使用🤗 Transformers 时,您无需担心它背后的机器学习框架;它可能是 PyTorch 或 TensorFlow,或 Flax。但是,Transformers 模型只接受 tensor(张量) 作为输入。如果这是你第一次听说 tensor,你可以把它们想象成 NumPy 数组。NumPy 数组可以是标量(0D)、向量(1D)、矩阵(2D)或具有更多维度。这些都可以称为 tensor;其他 ML 框架的 tensor 使用方法也类似,通常与 NumPy 数组一样容易创建。
我们可以使用 return_tensors 参数指定我们想要得到的 tensor 的类型(PyTorch、TensorFlow 或纯 NumPy),
Copied
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)
现在不要担心 padding(填充)和 truncation(截断);我们稍后会解释这些。这里要记住的是,你可以传递一个句子或一组句子,还可以指定要返回的 tensor 类型(如果没有传递类型,默认返回的是 python 中的 list 格式)。
以下是 PyTorch 张量的结果:
Copied
{
'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 的 ID。我们将在本章后面解释什么是 attention_mask 。
探索模型
我们可以像使用 tokenizer 一样下载预训练模型。Transformers 提供了一个 AutoModel 类,它也有一个 from_pretrained() 方法:
Copied
from transformers import AutoModel
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModel.from_pretrained(checkpoint)
在这段代码中,我们将之前在 pipeline 中使用的 checkpoint(实际上应该已经被缓存了)下载下来,并用它实例化了一个模型。
这个模型只包含基本的 Transformer 模块:输入一些句子,它输出我们将称为 hidden states(隐状态) ,也被称为特征。每个输入,我们都可以获取一个高维向量,代表 Transformer 模型对该输入的上下文理解。
如果这有些难以理解,不要担心。我们以后再解释。
这些隐状态本身就很有用,它们被称为模型头(head),通常是模型另一部分的输入。在 第一章 中,可以使用相同的体系结构的模型执行不同的任务,这是因为每个任务都有一个对应的模型头。
高维向量?
Transformers 模块的矢量输出通常较大。它通常有三个维度:
- Batch size(批次大小):一次处理的序列数(在我们的示例中为 2)。
- Sequence length(序列长度):表示序列(句子)的长度(在我们的示例中为 16)。
- Hidden size(隐藏层大小):每个模型输入的向量维度。
之所以说它是“高维度”的,是因为最后一个维度值。隐藏层维度可以非常大(对于较小的模型,常见的是 768,对于较大的模型,这个数字可以达到 3072 或更多)。
如果我们将预处理的之后的值输入到模型中,我们会得到以下输入:
Copied
outputs = model(**inputs)
print(outputs.last_hidden_state.shape)
Copied
torch.Size([2, 16, 768])
注意,🤗 Transformers 模型的输出有些像 namedtuple 或词典。你可以通过使用“.”+属性(就像我们在上面示例中所做的那样)或键( outputs["last_hidden_state"] )访问元素,如果你确切知道要查找的内容的位置( outputs[0] ),也可以通过索引访问元素。
模型头:理解数字的意义
Transformers 模型的输出会直接发送到模型头进行处理。
模型头通常由一个或几个线性层组成,它的输入是隐状态的高维向量,它会并将其投影到不同的维度。
在此图中,模型由其嵌入层和后续层表示。嵌入层将 tokenize 后输入中的每个 inputs ID 转换为表示关联 token 的向量。后续层使用注意机制操纵这些向量,生成句子的最终表示。
Transformers 中有许多不同的体系结构,每种体系结构都是围绕处理特定任务而设计的。以下是一个非详尽的列表:
*Model(隐状态检索)*ForCausalLM*ForMaskedLM*ForMultipleChoice*ForQuestionAnswering*ForSequenceClassification*ForTokenClassification- 以及其他 🤗
以情感分类为例,我们需要一个带有序列分类头的模型(能够将句子分类为积极或消极)。因此,我们不选用 AutoModel 类,而是使用 AutoModelForSequenceClassification 。也就是说前面写的 model = AutoModel.from_pretrained(checkpoint) 并不能得到情感分类任务的结果,因为没有加载 Model head。 AutoModelForSequenceClassification 类在 AutoModel 的基础上添加了一个序列分类头部,可以 将文本分类为不同的类别。
Copied
from transformers import AutoModelForSequenceClassification
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
outputs = model(**inputs)
如果我们看一下现在输出的形状,其维度会降低很多:模型头接收我们之前看到的高维向量作为输入,并输出包含两个值(每种标签一个)的向量。正如您所见,输出向量的尺寸与输入向量相比要小得多。这是因为模型头将输入向量中的信息压缩成两个值,每个标签一个 264 python 265 python:
Copied
print(outputs.logits.shape)
Copied
torch.Size([2, 2])
由于我们只有两个句子和两种标签,所以我们从模型中得到的结果的形状是 2 x 2。
对输出进行后序处理
我们从模型中得到的输出值本身并不一定有意义。我们来看看,
Copied
print(outputs.logits)
Copied
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)与实际的损失函数(如交叉熵)融合):
Copied
import torch
predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
print(predictions)
Copied
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 属性(下一节将对此进行详细介绍),该属性将每个模型输出的 ID 映射到相应的标签:
Copied
model.config.id2label
Copied
{0: 'NEGATIVE', 1: 'POSITIVE'}
现在我们可以得出结论,模型预测如下:
- 第一句:消极的概率:0.0402,积极的概率:0.9598
- 第二句:消极的概率:0.9995,积极的概率:0.0005
我们已经成功地复刻了管道的三个步骤:使用 tokenizer 进行预处理、通过模型传递输入以及后处理!接下来,让我们花一些时间深入了解这些步骤中的每一步。
在本节中,我们将更详细地了解如何创建和使用模型。我们将使用 AutoModel 类,当你希望从 checkpoint 实例化任何模型时,使用它非常方便。
AutoModel 类及其所有的相关类其实就是对库中可用的各种模型的简单包装。它是一个智能的包装,因为它可以自动猜测你的 checkpoint 适合的模型架构,然后实例化一个具有相同架构的模型。
然而,如果你知道要使用模型的类型,你可以直接使用其架构相对应的模型类。让我们看看如何使用 BERT 模型。
创建 Transformer 模型
初始化 BERT 模型需要做的第一件事是加载 Config 对象:
Copied
from transformers import BertConfig, BertModel
# 初始化 Config 类
config = BertConfig()
# 从 Config 类初始化模型
model = BertModel(config)
config 中包含许多用于构建模型的属性:
Copied
print(config)
Copied
BertConfig {
[...]
"hidden_size": 768,
"intermediate_size": 3072,
"max_position_embeddings": 512,
"num_attention_heads": 12,
"num_hidden_layers": 12,
[...]
}
虽然可能你还不知道这些属性的含义,但其中一部分应该比较眼熟: hidden_size 属性定义了 hidden_states(隐状态) 向量的大小,而 num_hidden_layers 定义了 Transformer 模型的层数。
使用不同的加载方式
使用默认配置创建模型会使用随机值对其进行初始化:
Copied
from transformers import BertConfig, BertModel
config = BertConfig()
model = BertModel(config)
# 模型已随机初始化!
这个模型是可以运行并得到结果的,但它会输出胡言乱语;它需要先进行训练才能正常使用。我们可以根据手头的任务从头开始训练模型,但正如你在 第一章 中看到的,这将需要很长的时间和大量的数据,并且产生的碳足迹会对环境产生不可忽视的影响。为了避免不必要的重复工作,能够共享和复用已经训练过的模型是非常重要的。
加载已经训练过的 Transformers 模型很简单——我们可以使用 from_pretrained() 方法:
Copied
from transformers import BertModel
model = BertModel.from_pretrained("bert-base-cased")
正如你在上一小节看到的,从现在开始,我们会将 BertModel 替换为等效的 AutoModel 类,这样可以摆脱对 checkpoint 的依赖;如果你的代码适用于一个 checkpoint 那么它就可以在另一个 checkpoint 无缝地工作。即使体系结构不同,这也适用,只要 checkpoint 是针对同类的任务(例如,情绪分析任务)训练的。
在上述代码示例中,我们没有使用 BertConfig ,而是通过 bert-base-cased 标签加载了一个预训练模型。这是一个由 BERT 的作者训练的模型 checkpoint 权重;你可以在其 模型卡片 中查看更多详细信息。
现在,此模型已经用 checkpoint 的所有权重进行了初始化。它可以直接用于推理它训练过的任务,也可以在新任务上进行微调。通过使用预训练的权重进行训练,相比于从头开始训练,我们可以迅速获得比较好的结果。
权重已下载并缓存在缓存文件夹中(因此,未来调用 from_pretrained() 方法的调用将不会重新下载它们)默认为 ~/.cache/huggingface/transformers 。你可以通过设置 HF_HOME 环境变量来自定义缓存文件夹。
加载模型的标识符可以是 Model Hub 上任何模型的标签,只要它与 BERT 架构兼容。可用的 BERT checkpoint 的完整列表可以在 这里 找到。
保存模型
保存模型和加载模型一样简单—我们使用 save_pretrained() 方法,该方法类似于 from_pretrained() 方法:
Copied
model.save_pretrained("directory_on_my_computer")
这会将两个文件保存到磁盘:
Copied
ls directory_on_my_computer
config.json model.safetensors
如果你看一下 config.json 文件,你会认出构建模型架构所需的属性。这个文件还包含一些元数据,例如 checkpoint 的来源,以及你上次保存 checkpoint 时所使用的 🤗 Transformers 版本。
pytorch_model.safetensors 文件被称为 state dictionary(状态字典) ;它包含了你的模型的所有权重。这两个文件是相辅相成的;配置文件是构建你的模型架构所必需的,而模型权重就是你的模型参数。
使用 Transformers 模型进行推理
既然你知道了如何加载和保存模型,那么让我们尝试使用它进行一些预测。Transformer 模型只能处理数字——由 tokenizer 转化后的数字。但在我们讨论 tokenizer 之前,让我们探讨一下模型可以接受的输入是什么。
可以将输入转换为适当的框架张量,但为了帮助你了解发生了什么,我们将快速了解在将输入发送到模型之前必须做什么。
假设我们有几个句子:
Copied
sequences = ["Hello!", "Cool.", "Nice!"]
tokenizer 将这些转换为词汇表索引,通常称为 input IDs 。每个句子现在都是一个数字列表!结果输出是:
Copied
encoded_sequences = [
[101, 7592, 999, 102],
[101, 4658, 1012, 102],
[101, 3835, 999, 102],
]
这是一个编码序列列表:一个列表列表。张量只接受矩形(形状规则的的列表:每一列元素的数量都相同)。这个数组已经是矩形了,因此将其转换为张量很容易:
Copied
import torch
model_inputs = torch.tensor(encoded_sequences)
使用张量作为模型的输入
将张量输入给模型非常简单 —— 我们只需调用模型并输入:
Copied
output = model(model_inputs)
虽然模型接受很多不同的参数,但只有 input IDs 是必需的。我们稍后会解释其他参数的作用以及何时需要它们,但首先我们需要仔细研究一下如何构建 Transformer 模型能理解的输入。
tokenizer 是 NLP 管道的核心组件之一。它们有一个非常明确的目的:将文本转换为模型可以处理的数据。模型只能处理数字,因此 tokenizer 需要将我们的文本输入转换为数字。在本节中,我们将确切地探讨 tokenization 管道中发生的事情。
在 NLP 任务中,通常处理的原始数据是文本。这里是一个例子:
Copied
Jim Henson was a puppeteer
但是,模型只能处理数字,因此我们需要找到一种将原始文本转换为数字的方法。这就是 tokenizer 所做的,并且有很多方法可以解决这个问题。目标是找到最有意义的表达方式 —— 即对模型来说最有意义的方式 —— 如果可能,还要找到最简洁的表达方式。
让我们看一下 tokenization 算法的一些示例,并尝试回答一些你可能对 tokenization 有的疑问。
基于单词(Word-based)的 tokenization
想到的第一种 tokenizer 是基于词(word-based)的 tokenization。它通常很容易配置和使用,只需几条规则,并且通常会产生不错的结果。例如,在下图中,目标是将原始文本拆分为单词并为每个单词找到一个数字表示:
有多种方法可以拆分文本。例如,我们可以通过使用 Python 的 split() 函数,使用空格将文本分割为单词:
Copied
tokenized_text = "Jim Henson was a puppeteer".split()
print(tokenized_text)
Copied
['Jim', 'Henson', 'was', 'a', 'puppeteer']
此外,还有一些基于单词的 tokenizer 的变体,对标点符号有额外的规则。使用这类 tokenizer,我们最终可以得到一些非常大的“词汇表(vocabulary)”,其中词汇表的大小由我们在语料库中拥有的独立 tokens 的总数确定。
每个单词都分配了一个 ID,从 0 开始一直到词汇表的大小。模型使用这些 ID 来识别每个词。
如果我们想用基于单词的 tokenizer 完全覆盖一种语言,我们需要为语言中的每个单词设置一个标识符,这将生成大量的 tokens。例如,英语中有超过 500,000 个单词,因此要构建从每个单词到 ID 的映射,我们需要跟踪这么多 ID。此外,像“dog”这样的词与“dogs”这样的词的表示方式不同,模型最初无法知道“dog”和“dogs”是相似的:它会将这两个词识别为不相关。这同样适用于其他相似的词,例如“run”和“running”,模型最初也不会看到它们的相似性。
最后,我们需要一个自定义 token 来表示不在我们词汇表中的单词。这被称为“unknown” token,通常表示为“[UNK]”或“
减少未知 tokens 数量的一种方法是使用更深一层的 tokenizer 即基于字符(character-based)的 tokenizer
基于字符(Character-based)的 tokenization
基于字符的 tokenizer 将文本拆分为字符,而不是单词。这有两个主要好处:
- 词汇量要小得多。
- unknown tokens (out-of-vocabulary)要少得多,因为每个单词都可以由字符构建。
但在此过程中也有一些问题,关于空格和标点符号:
这种方法也不是完美的。由于现在表示是基于字符而不是单词,因此人们可能会争辩说,从直觉上讲,它的意义不大:每个字符本身并没有多大意义,但是单词则不然。然而,这又因语言而异;例如,在中文中,每个字符比拉丁语言中的字符包含更多的信息。
另一件要考虑的因素是,这样做会导致我们的模型需要处理大量的 tokens:虽然一个单词在基于单词的 tokenizer 中只是一个 token,但当它被转换为字符时,很可能就变成了 10 个或更多的 tokens
为了两全其美,我们可以使用结合这两种方法的第三种技术:基于子词(subword)的 tokenization。
基于子词(subword)的 tokenization
基于子词(subword)的 tokenization 算法依赖于这样一个原则:常用词不应被分解为更小的子词,但罕见词应被分解为有意义的子词。
例如,“annoyingly”可能被视为一个罕见的词,可以分解为“annoying”和“ly”。这两者都可能作为独立的子词并且出现得更频繁,同时“annoyingly”的含义通过“annoying”和“ly”的复合含义得以保留。
这里有一个例子,展示了基于子词的 tokenization 算法如何将序列“Let’s do tokenization!”分词:
这些子词最终提供了大量的语义信息:例如,在上面的例子中,“tokenization”被分割成“token”和“ization”,这两个 tokens 在保持空间效率的同时具有语义意义(只需要两个 tokens 就能表示一个长词)。这让我们能够在词汇量小的情况下获得相对良好的覆盖率,并且几乎没有未知的 token。
这种方法在土耳其语等粘着型语言(agglutinative languages)中特别有用,你可以通过将子词串在一起来形成(几乎)任意长的复杂词。
还有更多!
不出所料,还有更多的技术。仅举几例:
- Byte-level BPE,用于 GPT-2
- WordPiece,用于 BERT
- SentencePiece or Unigram,用于多个多语言模型
你现在应该对 tokenizer 的工作原理有足够的了解,可以开始使用 API 了。
加载和保存
加载和保存 tokenizer 就像使用模型一样简单。实际上,它基于相同的两种方法: from_pretrained() 和 save_pretrained() 。这些方法会加载或保存分词器使用的算法(有点像模型的架构(architecture))以及其词汇表(有点像模型的权重(weights))。
加载使用与 BERT 相同的 checkpoint 训练的 BERT tokenizer 与加载模型的方式相同,只是换成了 Bert tokenizer 类:
Copied
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained("bert-base-cased")
如同 AutoModel , AutoTokenizer 类将根据 checkpoint 名称在库中获取正确的 tokenizer 类,并且可以直接与任何 checkpoint 一起使用:
Copied
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
现在我们可以像在上一节中显示的那样使用 tokenizer:
Copied
tokenizer("Using a Transformer network is simple")
Copied
{'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 与保存模型完全相同:
Copied
tokenizer.save_pretrained("directory_on_my_computer")
我们将在 第三章 中将更多地谈论 token_type_ids ,稍后我们将解释 attention_mask 。首先,让我们看看如何生成 input_ids 。为此,我们需要查看 tokenizer 的内部是如何实现的。
编码
将文本翻译成数字被称为编码(encoding)。编码分两步完成:分词,然后转换为 inputs ID。
正如我们所见,第一步是将文本拆分为单词(或部分单词、标点符号等),通常称为 tokens 不同的分词器使用的算法也不一样,这就是为什么我们需要使用模型名称来实例化 tokenizer,以确保我们使用模型预训练时使用的相同的算法。
第二步是将这些 tokens 转换为数字,这样我们就可以用它们构建一个张量并将它们提供给模型。为此,tokenizer 有一个词汇表(vocabulary),这是我们在使用 from_pretrained() 方法实例化它时下载的部分。同样,我们需要使用与预训练模型时相同的词汇表。
为了更好地理解这两个步骤,我们将分别探讨它们。请注意,我们将单独执行部分 tokenization 管道的方法来向你展示这些步骤的中间结果,但在实践中,你应该直接在你的输入上调用 tokenizer(如第 2 小节所示)。
tokenization
tokenization 过程由 tokenizer 的 tokenize() 方法实现:
Copied
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
Copied
['Using', 'a', 'transform', '##er', 'network', 'is', 'simple']
这个 tokenizer 是一个基于子词的 tokenizer:它对词进行拆分,直到获得可以用其词汇表表示的 tokens。以 transformer 为例,它分为两个 tokens transform 和 ##er 。
从 tokens 到 inputs ID
inputs ID 的转换由 tokenizer 的 convert_tokens_to_ids() 方法实现:
Copied
ids = tokenizer.convert_tokens_to_ids(tokens)
print(ids)
Copied
[7993, 170, 11303, 1200, 2443, 1110, 3014]
这些输出,一旦转换为适当的框架张量,就可以用作模型的输入,如本章前面所示。
✏️ 试试看! 请将我们在第 2 节中使用的输入句子(“I’ve been waiting for a HuggingFace course my whole life.”和“I hate this so much!”)执行最后两个步骤(分词和转换为 inputs ID)。检查你获得的 inputs ID 是否与我们在第二节中获得的一致!
解码
解码(Decoding) 正好相反:从 inputs ID 到一个字符串。这以通过 decode() 方法实现:
Copied
decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)
Copied
'Using a Transformer network is simple'
请注意, decode 方法不仅将索引转换回 tokens,还将属于相同单词的 tokens 组合在一起以生成可读的句子。当我们使用预测新文本的模型(根据提示生成的文本,或序列到序列问题(如翻译或摘要))时,这样的功能将非常有用。
到现在为止,你应该了解 tokenizer 可以处理的原子操作:分词、转换为 ID 以及将 ID 转换回字符串。然而,我们只是瞥到了冰山一角。在下一节中,我们将继续探讨它能力的极限,并看看如何克服它们。
在上一节中,我们探讨了最简单的案例:对一个较短的句子进行推理。然而,一些问题已经出现:
- 我们如何处理多个句子?
- 我们如何处理不同长度的多个句子?
- 词汇索引是唯一可以让模型运行的输入吗?
- 是否存在句子太长的问题?
让我们看看这些问题会带来什么样的问题,以及如何使用🤗 Transformers API 解决它们
模型需要一批输入
在上一个练习中,你看到了句子如何转换为数字列表。让我们将此数字列表转换为张量,并将其发送到模型:
Copied
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)
# 这一行会运行失败
model(input_ids)
Copied
IndexError: Dimension out of range (expected to be in range of [-1, 0], but got 1)
哦,不!为什么失败了?“我们的确是按照第 2 节中管道的步骤一步步来做的。
问题是我们向模型发送了一个单独的句子,而🤗 Transformers 模型默认情况下需要一个句子列表。在这里,当我们试图重现 tokenizer 在输入 sequence 后在其内部进行的所有操作。但如果你仔细观察,你会发现 tokenizer 不仅仅是将 inputs ID 的列表转换为张量,它还在其上添加了一个维度:
Copied
tokenized_inputs = tokenizer(sequence, return_tensors="pt")
print(tokenized_inputs["input_ids"])
Copied
tensor([[ 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172,
2607, 2026, 2878, 2166, 1012, 102]])
让我们再试一次并添加一个新的维度:
Copied
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)
让我们打印 inputs ID 以及生成的 logits 值,以下是输出:
Copied
Input IDs: [[ 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012]]
Logits: [[-2.7276, 2.8789]]
批处理(Batching)是一次性通过模型发送多个句子的行为。如果你只有一句话,你可以构建一个只有一个句子的 batch:
Copied
batched_ids = [ids, ids]
这就是一个包含两个相同句子的 batch
✏️ 试试看! 将这个
batched_ids列表转换为张量,并通过你的模型进行处理。检查你是否得到了与之前相同的 logits 值(但是重复了两次)!
批处理支持模型在输入多个句子时工作。使用多个句子就像使用单个句子构建批一样简单。不过,还有第二个问题。当你试图将两个(或更多)句子组合在一起时,它们的长度可能不同。如果你以前使用过张量,那么你知道它们必须是矩形,因此无法将 inputs ID 列表直接转换为张量。为了解决这个问题,我们通常填充输入(Padding)。
填充输入(Padding)
以下列表不能转换为张量:
Copied
batched_ids = [
[200, 200, 200],
[200, 200]
]
为了解决这个问题,我们将使用填充使张量成为标准的矩形。Padding 通过在值较少的句子中添加一个名为 padding_id 的特殊单词来确保我们所有的句子长度相同。例如,如果你有 10 个包含 10 个单词的句子和 1 个包含 20 个单词的句子,填充能确保所有句子都包含 20 个单词。在我们的示例中,填充后的张量如下所示:
Copied
padding_id = 100
batched_ids = [
[200, 200, 200],
[200, 200, padding_id],
]
我们可以在 tokenizer.pad_token_id 中找到填充 token 的 ID。让我们使用它,分别用模型处理这两个句子,然后再尝试将这两个句子放在一起用模型批处理:
Copied
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)
Copied
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 相同,但我们得到了完全不同的值!
这是因为 Transformer 模型的关键特性:注意力层,它考虑了每个 token 的上下文信息。这具体来说,每个 token 的含义并非单独存在的,它的含义还取决于它在句子中的位置以及周围的其他 tokens。当我们使用填充(padding)来处理长度不同的句子时,我们会添加特殊的“填充 token”来使所有句子达到相同的长度。但是,注意力层会将这些填充 token 也纳入考虑,因为它们会关注序列中的所有 tokens。这就导致了一个问题:尽管填充 token 本身并没有实际的含义,但它们的存在会影响模型对句子的理解。我们需要告诉这些注意层忽略填充 token。这是通过使用注意力掩码(attention mask)层来实现的。
注意力掩码(attention mask)层
注意力掩码(attention mask)是与 inputs ID 张量形状完全相同的张量,用 0 和 1 填充:1 表示应关注相应的 tokens,0 表示应忽略相应的 tokens(即,它们应被模型的注意力层忽视)。
让我们用 attention mask 修改上一个示例:
Copied
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)
Copied
tensor([[ 1.5694, -1.3895],
[ 0.5803, -0.4125]], grad_fn=<AddmmBackward>)
现在我们得到了批处理中第二句话相同的 logits 值。
注意第二序列的最后一个值是填充 ID,其在注意力掩码中的值为 0。
✏️ **试试看!**在第二节使用的两个句子(“I’ve been waiting for a HuggingFace course my whole life.” 和 “I hate this so much!”)上手动进行 tokenize。将它们输入模型并检查你是否得到了与第二节相同的 logits 值。然后使用填充 token 将它们一起进行批处理,然后创建合适的注意力掩码。检查模型计算后是否得到了相同的结果!
更长的句子
对于 Transformers 模型,我们可以通过模型的序列长度是有限的。大多数模型处理多达 512 或 1024 个 的 tokens 序列,当使用模型处理更长的序列时,会崩溃。此问题有两种解决方案:
- 使用支持更长序列长度的模型。
- 截断你的序列。
不同的模型支持的序列长度各不相同,有些模型专门用于处理非常长的序列。例如 Longformer 和 LED 。如果你正在处理需要非常长序列的任务,我们建议你考虑使用这些模型。
另外,我们建议你通过设定 max_sequence_length 参数来截断序列:
Copied
sequence = sequence[:max_sequence_length]
在过去的几个章节中,我们已经尝试尽可能手动完成大部分工作。我们探索了 tokenizer 的运行机制,并且了解了分词、转换为 inputs ID、填充、截断以及注意力掩码的处理方式。
然而,正如我们在第二节中看到的那样,🤗 Transformers API 能够通过一个高级函数为我们处理所有这些工作,接下来我们就要深入研究这个函数。当你直接在句子上调用你的 tokenizer 时,就可以得到转换后的可以直接放入模型的数据了:
Copied
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 变量包含模型运行所需的所有数据。在 DistilBERT 中,它包括 inputs ID 和注意力掩码(attention mask)。其他接受额外输入的模型也会有对应的 tokenizer 可以将输入转化为模型所需要的输入。
正如我们将在下面的一些例子中看到的,这个函数非常强大。首先,它可以对单个句子进行处理:
Copied
sequence = "I've been waiting for a HuggingFace course my whole life."
model_inputs = tokenizer(sequence)
它还一次处理多个多个,并且 API 的用法完全一致:
Copied
sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"]
model_inputs = tokenizer(sequences)
它可以使用多种不同的方法对目标进行填充:
Copied
# 将句子序列填充到最长句子的长度
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)
它还可以对序列进行截断:
Copied
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 对象可以处理指定框架张量的转换,然后可以直接发送到模型。例如,在下面的代码示例中,我们告诉 tokenizer 返回不同框架的张量 —— "pt" 返回 PyTorch 张量, "tf" 返回 TensorFlow 张量,而 "np" 则返回 NumPy 数组:
Copied
sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"]
# 返回 PyTorch tensors
model_inputs = tokenizer(sequences, padding=True, return_tensors="pt")
# 返回 TensorFlow tensors
model_inputs = tokenizer(sequences, padding=True, return_tensors="tf")
# 返回 NumPy arrays
model_inputs = tokenizer(sequences, padding=True, return_tensors="np")
特殊的 tokens
如果我们看一下 tokenizer 返回的 inputs ID,我们会发现它们与之前的略有不同:
Copied
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)
Copied
[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]
句子的开始和结束分别增加了一个 inputs ID。我们来解码上述的两个 ID 序列,看看是怎么回事:
Copied
print(tokenizer.decode(model_inputs["input_ids"]))
print(tokenizer.decode(ids))
Copied
"[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 在开头添加了特殊单词 [CLS] ,在结尾添加了特殊单词 [SEP] 。这是因为模型在预训练时使用了这些字词,所以为了得到相同的推断结果,我们也需要添加它们。请注意,有些模型不添加特殊单词,或者添加不同的特殊单词;模型也可能只在开头或结尾添加这些特殊单词。无论如何,tokenizer 知道哪些是必需的,并会为你处理这些问题。
小结:从 tokenizer 到模型
现在我们已经看到 tokenizer 对象在处理文本时的所有步骤,让我们最后再看一次它如何通过 tokenizer API 处理多个序列(填充!),非常长的序列(截断!)以及多种类型的张量:
Copied
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)