管道背后的机制
让我们从一个完整的示例开始,看看我们在 第 1 章 中执行以下代码时,幕后发生了什么
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}]
正如我们在 第 1 章 中所见,此管道将三个步骤组合在一起:预处理、将输入传递到模型中以及后处理
让我们快速回顾一下每个步骤。
使用分词器进行预处理
与其他神经网络一样,Transformer 模型无法直接处理原始文本,因此我们管道的第一步是将文本输入转换为模型可以理解的数字。为此,我们使用一个分词器,它负责
- 将输入拆分为称为token 的单词、子词或符号(如标点符号)
- 将每个 token 映射到一个整数
- 添加可能对模型有用的其他输入
所有这些预处理都需要以与模型预训练时完全相同的方式进行,因此我们首先需要从 模型中心 下载这些信息。为此,我们使用 AutoTokenizer
类及其 from_pretrained()
方法。使用我们模型的检查点名称,它将自动获取与模型分词器相关联的数据并将其缓存(因此它只在您第一次运行以下代码时下载)。
由于 sentiment-analysis
管道的默认检查点是 distilbert-base-uncased-finetuned-sst-2-english
(您可以在此处查看其模型卡 此处),我们运行以下代码
from transformers import AutoTokenizer
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
一旦我们有了分词器,我们就可以直接将我们的句子传递给它,并且我们将获得一个可以直接馈送到模型的字典!剩下的唯一工作是将输入 ID 列表转换为张量。
您可以使用 🤗 Transformers,而无需担心哪种 ML 框架用作后端;它可能是 PyTorch 或 TensorFlow,或者对于某些模型而言是 Flax。但是,Transformer 模型只接受张量作为输入。如果这是您第一次听说张量,您可以将其视为 NumPy 数组。NumPy 数组可以是标量 (0D)、向量 (1D)、矩阵 (2D) 或具有更多维度。它实际上是一个张量;其他 ML 框架的张量行为类似,并且通常与实例化 NumPy 数组一样简单。
为了指定我们希望获取的张量类型(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)
暂时不要担心填充和截断;我们将在后面解释这些。这里要记住的主要事项是,您可以传递一个句子或一个句子列表,以及指定您希望获取的张量类型(如果未传递类型,您将得到一个列表列表作为结果)。
以下是结果在 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 的唯一标识符。我们将在本章后面解释 attention_mask
是什么。
通过模型
我们可以用与分词器相同的方式下载预训练模型。🤗 Transformers 提供了一个 AutoModel
类,它也具有 from_pretrained()
方法
from transformers import AutoModel
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModel.from_pretrained(checkpoint)
在此代码片段中,我们下载了之前在管道中使用的相同检查点(它实际上应该已经被缓存了),并使用它实例化了一个模型。
此架构只包含基本的 Transformer 模块:给定一些输入,它输出我们称之为隐藏状态的东西,也称为特征。对于每个模型输入,我们将检索一个高维向量,表示Transformer 模型对该输入的语境理解。
如果这听起来没有意义,不要担心。我们将在后面解释所有内容。
虽然这些隐藏状态本身可能很有用,但它们通常是模型另一个部分的输入,称为头部。在 第 1 章 中,不同的任务可以用相同的架构执行,但每个任务都将与一个不同的头部相关联。
高维向量?
Transformer 模块输出的向量通常很大。它通常具有三个维度
- 批次大小:一次处理的序列数量(在我们示例中为 2)。
- 序列长度:序列的数字表示的长度(在我们示例中为 16)。
- 隐藏大小:每个模型输入的向量维度。
由于最后一个值,它被称为“高维”。隐藏大小可以非常大(较小的模型通常为 768,而较大的模型可以达到 3072 或更大)。
如果我们将预处理后的输入馈送到我们的模型,我们可以看到这一点。
outputs = model(**inputs)
print(outputs.last_hidden_state.shape)
torch.Size([2, 16, 768])
请注意,🤗 Transformers 模型的输出类似于namedtuple
或字典。您可以通过属性(如我们所做的那样)或通过键(outputs["last_hidden_state"]
)访问元素,甚至可以通过索引访问(outputs[0]
),如果您确切地知道要查找的内容在哪里。
模型头部:从数字中获取意义
模型头部将高维隐藏状态向量作为输入,并将它们投影到不同的维度。它们通常由一个或几个线性层组成。
Transformer 模型的输出直接发送到模型头部进行处理。
在此图中,模型由其嵌入层和后续层表示。嵌入层将标记化输入中的每个输入 ID 转换为代表关联标记的向量。后续层使用注意力机制操作这些向量,以产生句子的最终表示。
🤗 Transformers 中提供了许多不同的架构,每种架构都是围绕解决特定任务而设计的。以下是非详尽的列表。
*模型
(检索隐藏状态)*ForCausalLM
*ForMaskedLM
*ForMultipleChoice
*ForQuestionAnswering
*ForSequenceClassification
*ForTokenClassification
- 以及其他 🤗
对于我们的示例,我们将需要一个带有序列分类头的模型(能够将句子分类为正面或负面)。因此,我们实际上不会使用AutoModel
类,而是使用AutoModelForSequenceClassification
。
from transformers import AutoModelForSequenceClassification
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
outputs = model(**inputs)
现在,如果我们查看输出的形状,维度将低得多:模型头部将我们之前看到的那些高维向量作为输入,并输出包含两个值的向量(每个标签一个)。
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
属性(下一节将详细介绍)。
model.config.id2label
{0: 'NEGATIVE', 1: 'POSITIVE'}
现在我们可以得出结论,模型预测如下:
- 第一个句子:负面:0.0402,正面:0.9598
- 第二个句子:负面:0.9995,正面:0.0005
我们已经成功地重现了管道的三个步骤:使用标记器进行预处理、将输入传递到模型以及后处理!现在让我们花一些时间深入研究这些步骤中的每一个。
✏️ 试试看! 选择您自己的两个(或更多)文本,并将其通过sentiment-analysis
管道运行。然后自己复制您在这里看到的步骤,并检查您是否获得了相同的结果!