转载自:https://huggingface.co/learn/nlp-course/zh-CN/

原中文文档有很多地方翻译的太敷衍了,因此才有此系列文章。

NLP课程(二)- Transformers的使用

pipline的内部

示例:

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

pipeline函数相当于整合了下图三个步骤

1.使用分词器进行预处理

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

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

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

因为sentiment-analysis(情绪分析)管道的默认检查点是distilbert-base-uncased-finetuned-sst-2-english(你可以看到它的模型卡here),我们运行以下程序:

1
2
3
4
from transformers import AutoTokenizer

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

一旦我们有了标记器,我们就可以直接将我们的句子传递给它,然后我们就会得到一本字典,它可以提供给我们的模型!剩下要做的唯一一件事就是将输入ID列表转换为张量。

要指定要返回的张量类型(PyTorch、TensorFlow或plain 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)

可以传递一个句子或一组句子,还可以指定要返回的张量类型(如果没有传递类型,您将得到一组列表)

输出:

101对应[‘cls’],1045对应[‘I’]

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_idsattention_maskinput_ids包含两行整数(每个句子一行),它们是每个句子中标记的唯一标记(token)

2.模型处理

不带序列头单纯用AutoModel,只包括Transformer架构,它的输出是隐藏状态。

注意:transformer架构只包含基本转换器模块:给定一些输入,它输出我们将调用的内容<隐藏状态(hidden states),亦称特征(features)。对于每个模型输入,我们将检索一个高维向量,表示Transformer模型对该输入的上下文理解。**隐藏状态(hidden states)**是head的输入。

Transformers模块的矢量输出通常较大。它通常有三个维度:

  • Batch size: 一次处理的序列数(在我们的示例中为2)。
  • Sequence length: 序列的数值表示的长度(在我们的示例中为16)。
  • Hidden size: 每个模型输入的向量维度。
1
2
3
4
5
6
7
8
9
10
11
from transformers import AutoModel

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

outputs = model(**inputs)
print(outputs.last_hidden_state.shape)

'''
torch.Size([2, 16, 768])
'''

注意:Transformers模型的输出与namedtuple或词典相似。您可以通过属性(就像我们所做的那样)或键(输出["last_hidden_state"])访问元素,甚至可以通过索引访问元素

带head的类型,输出是logits

  • *Model (retrieve the hidden states)
  • *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
2
print(outputs.logits.shape)
torch.Size([2, 2])

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

3.输出后处理

1
2
3
4
print(outputs.logits)

tensor([[-1.5607, 1.6123],
[ 4.1692, -3.3464]], grad_fn=<AddmmBackward>)

输出的不是概率,而是logits,即模型最后一层输出的原始非标准化分数。要转换为概率,它们需要经过SoftMax层(所有🤗Transformers模型输出logits,因为用于训练的损耗函数通常会将最后的激活函数(如SoftMax)与实际损耗函数(如交叉熵)融合)

1
2
3
4
5
6
7
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>)

为了获得每个位置对应的标签,我们可以检查模型配置的id2label属性

1
2
3
model.config.id2label

{0: 'NEGATIVE', 1: 'POSITIVE'}

模型

BERT模型

1.加载配置对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from transformers import BertConfig, BertModel

# Building the config
config = BertConfig()

# Building the model from the config
model = BertModel(config)

print(config)

'''
BertConfig {
"attention_probs_dropout_prob": 0.1,
"classifier_dropout": null,
"hidden_act": "gelu",
"hidden_dropout_prob": 0.1,
"hidden_size": 768, # hidden状态向量的大小
"initializer_range": 0.02,
"intermediate_size": 3072,
"layer_norm_eps": 1e-12,
"max_position_embeddings": 512,
"model_type": "bert",
"num_attention_heads": 12,
"num_hidden_layers": 12, # Transformer模型的层数
"pad_token_id": 0,
"position_embedding_type": "absolute",
"transformers_version": "4.42.4",
"type_vocab_size": 2,
"use_cache": true,
"vocab_size": 30522
}
'''

从默认配置创建模型会使用随机值对其进行初始化.该模型可以在这种状态下使用,但会输出胡言乱语,因为没有对其进行训练。

所以更好的方式是使用已经训练过的模型。

2.加载训练过的模型

1
2
3
4
5
from transformers import BertModel, AutoModel

model = BertModel.from_pretrained("bert-base-cased")

model = AutoModel.from_pretrained("bert-base-cased")

在上面的代码示例中,我们没有使用BertConfig,而是通过Bert base cased 标识符加载了一个预训练模型。这是一个模型检查点,由BERT的作者自己训练;在加载时权重会被下载并缓存在缓存文件夹中。用于加载模型的标识符可以是模型中心Hub上任何模型的标识符,只要它与BERT体系结构兼容

3.保存模型

1
model.save_pretrained("directory_on_my_computer")

这会将两个文件保存到目录directory_on_my_computer

  • config.json
  • pytorch_model.bin

config.json 文件中包括构建模型体系结构所需的属性。该文件还包含一些元数据,例如检查点的来源以及上次保存检查点时使用的Transformers版本。

pytorch_model.bin 文件是众所周知的state dictionary; 它包含模型的所有权重。

4.使用张量作为模型的输入

1
2
3
import torch

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

分词器将这些转换为词汇表索引,通常称为 input IDs . 每个序列现在都是一个数字列表!结果是:

1
2
3
4
5
encoded_sequences = [
[101, 7592, 999, 102],
[101, 4658, 1012, 102],
[101, 3835, 999, 102],
]
1
2
3
model_inputs = torch.tensor(encoded_sequences)

output = model(model_inputs)

Tokenizer

1.分类

  • 基于词

1.每个单词都分配了一个 ID,从 0 开始一直到词汇表的大小。该模型使用这些 ID 来识别每个单词。

2.如果我们想用基于单词的标记器(tokenizer)完全覆盖一种语言,我们需要为语言中的每个单词都有一个标识符,这将生成大量的标记

3.最后,我们需要一个自定义标记(token)来表示不在我们词汇表中的单词。这被称为“未知”标记(token),通常表示为“[UNK]”或”“。如果你看到标记器产生了很多这样的标记,这通常是一个不好的迹象,因为它无法检索到一个词的合理表示,并且你会在这个过程中丢失信息。

4.减少未知标记数量的一种方法是使用更深一层的标记器(tokenizer),即基于字符的(character-based)标记器(tokenizer)

  • 基于字符

1.考虑的事情是,我们的模型最终会处理大量的词符(token):虽然使用基于单词的标记器(tokenizer),单词只会是单个标记,但当转换为字符时,它很容易变成 10 个或更多的词符(token)。

2.为了两全其美,我们可以使用结合这两种方法的第三种技术:子词标记化(subword tokenization)

  • 子词标记化

1.子词分词算法依赖于这样一个原则,即不应将常用词拆分为更小的子词,而应将稀有词分解为有意义的子词。

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

2.加载与保存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from transformers import BertTokenizer, AutoTokenizer

tokenizer = BertTokenizer.from_pretrained("bert-base-cased")

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")

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.save_pretrained("directory_on_my_computer")

3.编码

**第一步:**将文本拆分为单词(或单词的一部分、标点符号等),通常称为标记(token)。有多个规则可以管理该过程,这就是为什么我们需要使用模型名称来实例化标记器(tokenizer),以确保我们使用模型预训练时使用的相同规则。

标记化过程由标记器(tokenizer)的tokenize() 方法实现

1
2
3
4
5
6
7
8
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")

sequence = "Using a Transformer network is simple"
tokens = tokenizer.tokenize(sequence)

print(tokens)

此方法的输出是一个字符串列表或标记(token):

1
['Using', 'a', 'transform', '##er', 'network', 'is', 'simple']

**第二步:**将这些标记转换为数字,这样我们就可以用它们构建一个张量并将它们提供给模型。为此,标记器(tokenizer)有一个词汇(vocabulary),这是我们在实例化它时下载的部分 from_pretrained() 方法。

1
2
3
4
ids = tokenizer.convert_tokens_to_ids(tokens)

print(ids)
[7993, 170, 11303, 1200, 2443, 1110, 3014]

解码:

从词汇索引中,我们想要得到一个字符串。这可以通过 decode() 方法实现

decode 方法不仅将索引转换回标记(token),还将属于相同单词的标记(token)组合在一起以生成可读的句子。当

1
2
3
decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)
'Using a Transformer network is simple'

处理多个序列

1.模型需要一批输入

意思是输入model的input_ids是二维的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)
# This line will fail.
model(input_ids)

若按上述执行操作,最后一行代码会报错IndexError: Dimension out of range (expected to be in range of [-1, 0], but got 1)

若用tokenizer简化如下部分代码

tokens = tokenizer.tokenize(sequence)
ids = tokenizer.convert_tokens_to_ids(tokens)
input_ids = torch.tensor(ids)

可以看到tokenizer不仅将输入ID列表转换为张量,还在其顶部添加了一个维度:这是为了处理多个序列而新增的一个维度

1
2
3
4
5
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]])

2.填充输入

当你试图将两个(或更多)句子组合在一起时,它们的长度可能不同。为了解决这个问题,我们通常填充输入。

可以在tokenizer.pad_token_id中找到填充令牌ID. 让我们使用它,将我们的两句话分别发送到模型中,并分批发送到一起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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有点问题:第二行应该与第二句的logits相同,但我们得到了完全不同的值!

这是因为Transformer模型的关键特性是关注层,它将每个标记上下文化。这些将考虑填充标记,因为它们涉及序列中的所有标记。为了在通过模型传递不同长度的单个句子时,或者在传递一批应用了相同句子和填充的句子时获得相同的结果,我们需要告诉这些注意层忽略填充标记。这是通过使用 attention mask来实现的。

3.attention mask

Attention masks是与输入ID张量形状完全相同的张量,用0和1填充:1s表示应注意相应的标记,0s表示不应注意相应的标记(即,模型的注意力层应忽略它们)。

让我们用attention mask完成上一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)
tensor([[ 1.5694, -1.3895],
[ 0.5803, -0.4125]], grad_fn=<AddmmBackward>)

现在我们得到了该批中第二个句子的相同登录。

请注意,第二个序列的最后一个值是一个填充ID,它在attention mask中是一个0值。

4.序列截断

如果不是要处理一项需要很长序列的任务

建议通过指定max_sequence_length参数:

1
sequence = sequence[:max_sequence_length]

结合

1.tokenizer

1
2
3
4
5
6
7
8
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 = tokenizer(sequence)是一个很强大的api

它可以处理多序列且不用变更代码

它也可以根据几个目标进行填充:

1
2
3
4
5
6
7
8
9
# Will pad the sequences up to the maximum sequence length
model_inputs = tokenizer(sequences, padding="longest")

# Will pad the sequences up to the model max length
# (512 for BERT or DistilBERT)
model_inputs = tokenizer(sequences, padding="max_length")

# Will pad the sequences up to the specified max length
model_inputs = tokenizer(sequences, padding="max_length", max_length=8)

它还可以截断序列:

1
2
3
4
5
6
7
8
sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"]

# Will truncate the sequences that are longer than the model max length
# (512 for BERT or DistilBERT)
model_inputs = tokenizer(sequences, truncation=True)

# Will truncate the sequences that are longer than the specified max length
model_inputs = tokenizer(sequences, max_length=8, truncation=True)

还可以处理到特定框架张量的转换,然后可以直接发送到模型。"pt"返回Py Torch张量,"tf"返回TensorFlow张量,"np"返回NumPy数组:

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

2.特殊字符

标记器返回的输入 ID,我们会发现它们与之前的略有不同:

1
2
3
4
5
6
7
8
9
10
11
12
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,一个在结束时添加了一个标记(token) ID。让我们解码上面的两个ID序列,看看这是怎么回事:

1
2
3
4
5
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]。这是因为模型是用这些数据预训练的,所以为了得到相同的推理结果,我们还需要添加它们。请注意,有些模型不添加特殊单词,或者添加不同的单词;模型也可能只在开头或结尾添加这些特殊单词。在任何情况下,标记器都知道需要哪些词符,并将为您处理这些词符。

3.处理多序列的完整代码

1
2
3
4
5
6
7
8
9
10
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)