前言

让我们从一个完整的例子开始,回顾一下我们在第一章执行以下代码时幕后发生了什么:

1
2
3
4
5
6
7
8
9
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!",
]
)

并得到了以下结果:

1
2
[{'label': 'POSITIVE', 'score': 0.9598047137260437},
{'label': 'NEGATIVE', 'score': 0.9994558095932007}]

正如我们在第1章看到的,这个管道将三个步骤组合在一起:预处理、将输入通过模型传递,以及后处理:

让我们快速回顾一下这些步骤。

src link: https://huggingface.co/learn/nlp-course/chapter2/2

Operating System: Ubuntu 22.04.4 LTS

参考文档

  1. NLP Course - Behind the pipeline

使用分词器的预处理

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

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

所有这些预处理需要以与模型预训练时完全相同的方式进行,因此我们首先需要从Model Hub下载这些信息。为此,我们使用AutoTokenizer类及其from_pretrained()方法。使用我们模型的检查点名称,它将自动获取与模型分词器关联的数据并将其缓存(所以只有在第一次运行下面的代码时才需要下载)。

由于情感分析管道的默认检查点是distilbert-base-uncased-finetuned-sst-2-english(您可以在其模型卡中看到),我们运行以下代码:

1
2
3
4
from transformers import AutoTokenizer

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

一旦我们有了分词器,我们可以直接将我们的句子传递给它,然后我们会得到一个准备好喂给模型的字典!剩下的唯一事情就是将输入ID列表转换为张量。

您可以使用🤗 Transformers,而不用担心后端使用的是哪个机器学习框架;可能是PyTorch或TensorFlow,或者对于一些模型来说是Flax。然而,Transformer模型只接受张量作为输入。如果您是第一次听说张量,可以将其视为NumPy数组。NumPy数组可以是标量(0D)、向量(1D)、矩阵(2D)或具有更多维度。它实际上是一个张量;其他机器学习框架的张量行为类似,并且通常与NumPy数组一样简单实例化。

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

1
2
3
4
5
6
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张量的样子:

1
2
3
4
5
6
7
8
9
10
{
'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包含两行整数(每句一个),是每个句子中标记的唯一标识符。我们将在本章后面解释attention_mask是什么。

通过模型传递

我们可以用与分词器相同的方式下载我们的预训练模型。🤗 Transformers提供了一个AutoModel类,它也有一个from_pretrained()方法:

1
2
3
4
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或更多)。

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

1
2
outputs = model(**inputs)
print(outputs.last_hidden_state.shape)
1
torch.Size([2, 16, 768])

注意,🤗 Transformers模型的输出行为类似于命名元组或字典。您可以通过属性(就像我们做的那样)或通过键(outputs[“last_hidden_state”])访问元素,如果您确切知道您要找的东西在哪里,甚至可以通过索引(outputs[0])访问。

模型头部:从数字中获取意义

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

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

在这个图中,模型由其嵌入层和随后的层表示。嵌入层将分词输入中的每个输入ID转换为一个向量,该向量代表关联的标记。随后的层使用注意力机制操纵这些向量,以产生句子的最终表示。

在🤗 Transformers中,有许多不同的架构可供选择,每一种都是围绕解决特定任务而设计的。以下是一个非详尽列表:

  • *Model(检索隐藏状态)
  • *ForCausalLM
  • *ForMaskedLM
  • *ForMultipleChoice
  • *ForQuestionAnswering
  • *ForSequenceClassification
  • *ForTokenClassification
  • 以及其他🤗

对于我们的例子,我们需要一个带有序列分类头部的模型(能够将句子分类为正面或负面)。所以,我们实际上不会使用AutoModel类,而是使用AutoModelForSequenceClassification:

1
2
3
4
5
from transformers import AutoModelForSequenceClassification

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

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

1
print(outputs.logits.shape)
1
torch.Size([2, 2])

因为我们只有两个句子和两个标签,所以我们的模型得到的结果的形状是2 x 2。

后处理输出结果

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

1
print(outputs.logits)
1
2
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)与实际损失函数(如交叉熵)融合:

1
2
3
4
import torch

predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
print(predictions)
1
2
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属性(更多内容将在下一节介绍):

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

现在我们可以得出结论,模型的预测如下:

  • First sentence: NEGATIVE: 0.0402, POSITIVE: 0.9598
  • Second sentence: NEGATIVE: 0.9995, POSITIVE: 0.0005

我们已经成功复现了管道的三个步骤:使用分词器的预处理,将输入通过模型传递,以及后处理!现在,让我们花些时间深入探讨这些步骤的每一个细节。

结语

第二百零九篇博文写完,开心!!!!

今天,也是充满希望的一天。