Pipeline 的内部
让我们从一个完整的示例开始,看看在 第一章 中执行以下代码时在幕后发生了什么
from transformers import pipeline
classifier = pipeline("sentiment-analysis")
classifier(
[
"I've been waiting for a HuggingFace course my whole life.",
"I hate this so much!",
]
)
获得如下输出:
[{'label': 'POSITIVE', 'score': 0.9598047137260437},
{'label': 'NEGATIVE', 'score': 0.9994558095932007}]
正如我们在 第一章 中看到的,这个 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
(你可以在 这里 )看到它的模型卡片,我们运行以下代码:
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),
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 张量的结果:
{
'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()
方法:
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 或更多)。
如果我们将预处理的之后的值输入到模型中,我们会得到以下输入:
outputs = model(**inputs)
print(outputs.last_hidden_state.shape)
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
的基础上添加了一个序列分类头部,可以
将文本分类为不同的类别。
from transformers import AutoModelForSequenceClassification
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
outputs = model(**inputs)
如果我们看一下现在输出的形状,其维度会降低很多:模型头接收我们之前看到的高维向量作为输入,并输出包含两个值(每种标签一个)的向量。正如您所见,输出向量的尺寸与输入向量相比要小得多。这是因为模型头将输入向量中的信息压缩成两个值,每个标签一个
264 python 265
python:
print(outputs.logits.shape)
torch.Size([2, 2])
由于我们只有两个句子和两钟标签,所以我们从模型中得到的结果的形状是 2 x 2。
对输出进行后序处理
我们从模型中得到的输出值本身并不一定有意义。我们来看看,
print(outputs.logits)
tensor([[-1.5607, 1.6123],
[ 4.1692, -3.3464]], grad_fn=<AddmmBackward>)
我们的模型预测第一句为 [-1.5607, 1.6123]
,第二句为 [ 4.1692, -3.3464]
。这些不是概率,而是 logits(对数几率)
,是模型最后一层输出的原始的、未标准化的分数。要转换为概率,它们需要经过 SoftMax 层(所有🤗Transformers 模型的输出都是 logits,因为训练时的损失函数通常会将最后的激活函数(如 SoftMax)与实际的损失函数(如交叉熵)融合):
import torch
predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
print(predictions)
tensor([[4.0195e-02, 9.5980e-01],
[9.9946e-01, 5.4418e-04]], grad_fn=<SoftmaxBackward>)
现在我们可以看到,模型预测第一句的输出是 [0.0402, 0.9598]
,第二句 [0.9995, 0.0005]
。这些是可直接使用的概率分数。
为了获得每个分数对应的标签,我们可以查看模型配置的 id2label
属性(下一节将对此进行详细介绍),该属性将每个模型输出的 ID 映射到相应的标签:
model.config.id2label
{0: 'NEGATIVE', 1: 'POSITIVE'}
现在我们可以得出结论,模型预测如下:
- 第一句:消极的概率:0.0402,积极的概率:0.9598
- 第二句:消极的概率:0.9995,积极的概率:0.0005
我们已经成功地复刻了管道的三个步骤:使用 tokenizer 进行预处理、通过模型传递输入以及后处理!接下来,让我们花一些时间深入了解这些步骤中的每一步。
✏️ 试试看! 选择两个(或更多)句子并分别在 sentiment-analysis
管道和自己实现的管道中运行它们。看一看是否获得的结果是不是相同的!