Transformer模型

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.")
#分类

# 输出结果为——
[{'label': 'POSITIVE', 'score': 0.9598047137260437}]

我们也可以多传几句!

1
2
3
4
5
6
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选择一个特定的预训练模型,该模型已针对英语情感分析进行了微调。创建分类器对象时,将下载并缓存模型。如果您重新运行该命令,则将使用缓存的模型,无需再次下载模型。

将一些文本传递到pipeline时涉及三个主要步骤:

  1. 文本被预处理为模型可以理解的格式。
  2. 预处理的输入被传递给模型。
  3. 模型处理后输出最终人类可以理解的结果。

目前可用的一些pipeline是:
特征提取(获取文本的向量表示)填充空缺 ner(命名实体识别) 问答 情感分析
文本摘要 文本生成 翻译 零样本分类

典型任务

零样本分类

我们将首先处理一项非常具挑战性的任务,我们需要对尚未标记的文本进行分类。这是实际项目中的常见场景,因为注释文本通常很耗时并且需要领域专业知识。对于这项任务zero-shot-classificationpipeline非常强大:它允许您直接指定用于分类的标签,因此您不必依赖预训练模型的标签。下面的模型展示了如何使用这两个标签将句子分类为正面或负面——但也可以使用您喜欢的任何其他标签集对文本进行分类。

1
2
3
4
5
6
7
from transformers import pipeline

classifier = pipeline("zero-shot-classification")
classifier(
"This is a course about the Transformers library",
candidate_labels=["education", "politics", "business"],
)

文本预测生成

1
2
3
4
from transformers import pipeline

generator = pipeline("text-generation")
generator("In this course, we will teach you how to")
1
2
3
4
5
[{'generated_text': 'In this course, we will teach you how to understand and use '
'data flow and data interchange when handling user data. We '
'will be working with one or more of the most commonly used '
'data flows — data flows of various types, as seen by the '
'HTTP'}]

Mask Filling 填补空白

1
2
3
4
from transformers import pipeline

unmasker = pipeline("fill-mask")
unmasker("This course will teach you all about <mask> models.", top_k=2)
1
2
3
4
5
6
7
8
[{'sequence': 'This course will teach you all about mathematical models.',
'score': 0.19619831442832947,
'token': 30412,
'token_str': ' mathematical'},
{'sequence': 'This course will teach you all about computational models.',
'score': 0.04052725434303284,
'token': 38163,
'token_str': ' computational'}]

实体命名识别

1
2
3
4
from transformers import pipeline

ner = pipeline("ner", grouped_entities=True)
ner("My name is Sylvain and I work at Hugging Face in Brooklyn.")
1
2
3
4
[{'entity_group': 'PER', 'score': 0.99816, 'word': 'Sylvain', 'start': 11, 'end': 18}, 
{'entity_group': 'ORG', 'score': 0.97960, 'word': 'Hugging Face', 'start': 33, 'end': 45},
{'entity_group': 'LOC', 'score': 0.99321, 'word': 'Brooklyn', 'start': 49, 'end': 57}
]

在这里,模型正确地识别出 Sylvain 是一个人 (PER),Hugging Face 是一个组织 (ORG),而布鲁克林是一个位置 (LOC)。

工作机理

上面提到的所有 Transformer 模型(GPT、BERT、BART、T5 等)都被训练为语言模型。这意味着他们已经以无监督学习的方式接受了大量原始文本的训练。无监督学习是一种训练类型,其中目标是根据模型的输入自动计算的。这意味着不需要人工来标记数据!

这种类型的模型可以对其训练过的语言进行统计理解,但对于特定的实际任务并不是很有用。因此,一般的预训练模型会经历一个称为迁移学习的过程。在此过程中,模型在给定任务上以监督方式(即使用人工注释标签)进行微调。

微调他

微调方式一:因果语言建模

因果语言建模

微调方式2:遮罩语言建模

遮罩语言建模

迁移学习

预训练是 训练模型前的一个操作:随机初始化权重,在没有任何先验知识的情况下开始训练。(预训练通常是自监督,这意味着标签是根据输入自动创建的(例如:预测下一个单词或填充一些[MARSK]单词)。)

微调 是在模型经过预训练后完成的训练。要执行微调,首先需要获取一个经过预训练的语言模型,然后使用特定于任务的数据集执行额外的训练。

等等,为什么不直接为最后的任务而训练呢?有几个原因:

  • 预训练模型已经在与微调数据集有一些相似之处的数据集上进行了训练。因此,微调过程能够利用模型在预训练期间获得的知识(例如,对于NLP问题,预训练模型将对您在任务中使用的语言有某种统计规律上的理解)。
  • 由于预训练模型已经在大量数据上进行了训练,因此微调需要更少的数据来获得不错的结果
  • 出于同样的原因,获得好结果所需的时间和资源要少得多

体系结构

该模型主要由两个块组成:

  • Encoder (左侧): 编码器接收输入并构建其表示(其特征)。这意味着对模型进行了优化,以从输入中获得理解。
  • Decoder (右侧): 解码器使用编码器的表示(特征)以及其他输入来生成目标序列。这意味着该模型已针对生成输出进行了优化。

模型基本体系结构

这些部件中的每一个都可以独立使用,具体取决于任务:

  • Encoder-only models: 适用于需要理解输入的任务,如句子分类和命名实体识别。
  • Decoder-only models: 适用于生成任务,如文本生成。
  • Encoder-decoder models 或者 sequence-to-sequence models: 适用于需要根据输入进行生成的任务,如翻译或摘要。

注意力层

这一层将告诉模型在处理每个单词的表示时,要特别重视您传递给它的句子中的某些单词(并且或多或少地忽略其他单词)。

原始的结构

transformer原始架构

解码器块中的第一个注意力层关联到解码器的所有(过去的)输入,但是第二注意力层使用编码器的输出

“编码器”模型指仅使用编码器的Transformer模型。在每个阶段,注意力层都可以获取初始句子中的所有单词。这些模型通常具有“双向”注意力,被称为自编码模型。

“解码器”模型通常指仅使用解码器的Transformer模型。在每个阶段,对于给定的单词,注意力层只能获取到句子中位于将要预测单词前面的单词。这些模型通常被称为自回归模型。“解码器”模型的预训练通常围绕预测句子中的下一个单词进行。

编码器-解码器模型(也称为序列到序列模型)同时使用Transformer架构的编码器和解码器两个部分。在每个阶段,编码器的注意力层可以访问初始句子中的所有单词,而解码器的注意力层只能访问位于输入中将要预测单词前面的单词。

模型 示例 任务
编码器 ALBERT, BERT, DistilBERT, ELECTRA, RoBERTa 句子分类、命名实体识别、从文本中提取答案
解码器 CTRL, GPT, GPT-2, Transformer XL 文本生成
编码器-解码器 BART, T5, Marian, mBART 文本摘要、翻译、生成问题的回答

使用Transformer

Behind 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!",
]
)

此管道将三个步骤组合在一起:预处理、通过模型传递输入和后处理:

pipline背后的自动化步骤

使用分词器进行预处理 Tokenizer

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

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

所有这些预处理都需要以与模型预训练时完全相同的方式完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 导入 AutoTokenizer 类,用于加载预训练模型的分词器
from transformers import AutoTokenizer

# 指定预训练模型的名称或路径
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"

# 使用 AutoTokenizer 类的 from_pretrained 方法加载指定模型的分词器
tokenizer = AutoTokenizer.from_pretrained(checkpoint)

# 上述代码段的主要作用是加载了一个预训练的 DistilBERT 模型的分词器,
# 这个模型是在 SST-2 数据集上进行了微调的。
# 分词器用于将文本转换成模型可以理解的输入格式,通常包括将文本拆分成单词或子词,
# 并进行必要的标准化和编码处理。

from_pretrained 是 Hugging Face Transformers 库中的一个方法,用于从预训练的模型加载模型或相关的预处理器(如分词器)。这个方法可以根据指定的模型名称或路径自动加载对应的预训练模型或预处理器。

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

Transformers型号只接受 张量 作为输入。要指定要返回的张量类型(PyTorch、TensorFlow或plain NumPy),我们使用return_tensors参数:

hl
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)

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

以下是上面打印的input 的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_idsattention_maskinput_ids包含两行整数(每个句子一行),它们是每个句子中标记的唯一标记(token)。我们将在本章后面解释什么是attention_mask

attention_mask: 这是一个掩码张量,用于指示哪些位置是填充(padding)的,哪些位置是真实的标记。1 表示该位置是真实的标记,0 表示该位置是因为长度不够填充的。在输出中,对于每个序列,填充的位置被置为 0,而其余位置被置为 1。

浏览模型

我们可以像使用标记器一样下载预训练模型。🤗 Transformers提供了一个AutoModel类,该类还具有from_pretrained()方法:

1
2
3
4
5
from transformers import AutoModel

checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModel.from_pretrained(checkpoint)
# 我们下载了之前在管道中使用的相同检查点(它实际上应该已经被缓存),并用它实例化了一个模型。

这个架构只包含基本转换器模块:给定一些输入,它输出我们将调用的内容 隐藏状态(hidden states) ,亦称 特征(features) .这些隐藏状态本身可能很有用,但它们通常是模型另一部分(称为 头部(head) )的输入

高维向量

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

  • Batch size: 一次处理的序列数(在我们的示例中为2)。
  • Sequence length: 序列的数值表示的长度(在我们的示例中为16)。
  • Hidden size: 每个模型输入的向量维度。

由于最后一个值,它被称为“高维”。隐藏的大小可能非常大(768通常用于较小的型号,而在较大的型号中,这可能达到3072或更大)。

模型头:数字的意义

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

模型头

在深度学习中,特别是在神经网络中,”头”(head)通常指的是模型的最后一层或几层,这些层负责执行特定的任务。”头”通常与”主干”(backbone)相对应,”主干”是指模型的基本结构或主要部分,它负责提取特征。

在自然语言处理(NLP)中,”头”通常指的是与特定任务相关的层,例如情感分类、命名实体识别、语言建模等。在一个通用的预训练语言模型(如BERT、GPT)之后,我们可以添加一个或多个额外的层(即”头”),以便模型可以执行特定的任务。

在给定的上下文中,”模型头”(model head)指的可能是与序列分类任务相关的最后一层或一组层。在使用 AutoModelForSequenceClassification 加载预训练模型时,这个模型的头部通常包括用于进行序列分类的层,例如 softmax 分类器。因此,我们实际上不会使用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)

输出后处理

模型的输出值(未softmax前)为

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
5
6
7
8
9
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属性(下一节将对此进行详细介绍):

1
2
3
4
model.config.id2label

# 输出
{0: 'NEGATIVE', 1: 'POSITIVE'}

现在我们可以得出结论,该模型预测了以下几点:

  • 第一句:否定:0.0402,肯定:0.9598
  • 第二句:否定:0.9995,肯定:0.0005

模型

在本节中,我们将更详细地了解如何创建和使用模型。我们将使用 AutoModel类,当您希望从检查点实例化任何模型时,这非常方便。

这个AutoModel类及其所有相关项实际上是对库中各种可用模型的简单包装,它可以自动猜测检查点的适当模型体系结构,然后用该体系结构实例化模型。

初始化模型

初始化BERT模型需要做的第一件事是加载配置对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from transformers import BertConfig, BertModel

# Building the config
config = BertConfig()

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


print(config)
# 输出
BertConfig {
[...]
"hidden_size": 768,
"intermediate_size": 3072,
"max_position_embeddings": 512,
"num_attention_heads": 12,
"num_hidden_layers": 12,
[...]
}

hiddensize属性定义了hidden状态向量的大小,num_hidden_layers定义了Transformer模型的层数。

但是,通过上面的方式加载模型,该模型可以在这种状态下使用,但会输出胡言乱语

首先需要对其进行训练。我们可以根据手头的任务从头开始训练模型,但正如上一章 ,这将需要很长的时间和大量的数据,并将产生不可忽视的环境影响。为了避免不必要的重复工作,必须能够共享和重用已经训练过的模型。

加载已经训练过的Transformers模型很简单-我们可以使用from_pretrained() 方法:

1
2
3
from transformers import BertModel

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

在上面的代码示例中,我们没有使用BertConfig,而是通过Bert base cased标识符加载了一个预训练模型。这是一个模型检查点,由BERT的作者自己训练

我们可以用等效的AutoModel类替换Bert模型(见上一部分的浏览模型)

保存模型

保存模型和加载模型一样简单—我们使用 save_pretrained() 方法,类似于 from_pretrained() 方法:

1
model.save_pretrained("directory_on_my_computer")

将会存入config.json和pytorch_model.bin

config.json 文件描述构建模型体系结构所需的属性(即在初始化模型写到的config)。该文件还包含一些元数据,例如检查点的来源以及上次保存检查点时使用的🤗 Transformers版本。

这个 pytorch_model.bin 文件就是众所周知的 state dictionary ; 它包含模型的所有权重。这两个文件齐头并进;配置是了解模型体系结构所必需的,而模型权重是模型的参数。

推理

Transformer模型只能处理数字——分词器生成的数字。标记化器可以将输入转换为适当的框架张量,但在我们讨论标记化器之前,让我们先探讨模型接受哪些输入。

假设我们有几个序列:

1
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
import torch

model_inputs = torch.tensor(encoded_sequences)

在模型中使用张量非常简单-我们只需将输入称为模型:

1
output = model(model_inputs)

标记器

将文本转换为模型可以处理的数据。模型只能处理数字,因此标记器(Tokenizer)需要将我们的文本输入转换为数字数据

如何标记

基于词的(Word-based)& 基于字符(Character-based)

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

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

最后,我们需要一个自定义标记(token)来表示不在我们词汇表中的单词。这被称为“未知”标记(token),通常表示为“[UNK]”

这种方法也不是完美的。由于现在表示是基于字符而不是单词,因此人们可能会争辩说,从直觉上讲,它的意义不大

子词标记化

核心原则:不应将常用词拆分为更小的子词,而应将稀有词分解为有意义的子词

应用

加载和保存标记器(tokenizer)就像使用模型一样简单。实际上,它基于相同的两种方法: from_pretrained()save_pretrained()

1
2
3
4
5
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
tokenizer("Using a Transformer network is simple")
tokenizer.save_pretrained("directory_on_my_computer")

逐步理解原子操作:编码与解码

编码

将文本翻译成数字被称为编码(encoding).编码分两步完成:标记化,然后转换为输入 ID。

第一步是将文本拆分为单词(或单词的一部分、标点符号等),通常称为_标记(token)
第二步是将这些标记转换为数字,这样我们就可以用它们构建一个张量并将它们提供给模型

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

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

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

print(tokens)

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

从词符(token)到输入 ID:

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

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

解码

1
2
3
4
5
decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)

# 输出
'Using a Transformer network is simple'

处理序列

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)

我们向模型发送了一个序列,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]])

让我们重试并添加一个新维度:

hl:13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)

# 输出
Input IDs: [[ 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012]]
Logits: [[-2.7276, 2.8789]]

所以,如果打算批处理,一般把torch.tensor([ids])中的[ids]变更为batch_ids

例如:

1
batched_ids = [ids, ids]

填充输入

1
2
3
4
batched_ids = [
[200, 200, 200],
[200, 200]
]

这显然不符合矩阵要求,为了解决这个问题,我们将使用填充使张量具有矩形。

Padding通过在值较少的句子中添加一个名为Padding token的特殊单词来确保我们所有的句子长度相同。例如,如果你有10个包含10个单词的句子和1个包含20个单词的句子,填充将确保所有句子都包含20个单词。在我们的示例中,生成的张量如下所示:

1
2
3
4
5
6
7
padding_id = 100
# 但是实际上,我们一般用自带的Padding_id,可以在tokenizer.pad_token_id中找到填充令牌ID.

batched_ids = [
[200, 200, 200],
[200, 200, padding_id],
]

或者使用自带的

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

注意力面具

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

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

长序列

我们可以通过模型的序列长度是有限的。大多数模型处理多达512或1024个令牌的序列,当要求处理更长的序列时,会崩溃。此问题有两种解决方案:

  • 使用支持的序列长度较长的模型。
  • 截断序列。
1
sequence = sequence[:max_sequence_length]

合起来

Transformers API可以通过一个高级函数为我们处理所有这些(标记化器的工作原理、标记化、到输入ID的转换、填充、截断和注意掩码),我们将在这介绍一点API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 设置填充输入的API
# 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)


# 设置截断的API
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)

特殊词符(token)

为了帮助模型理解分局合起始位置,一个在开始时添加了一个标记(token) ID,一个在结束时添加了一个标记(token) ID。

1
2
"[CLS] i've been waiting for a huggingface course my whole life. [SEP]"
"i've been waiting for a huggingface course my whole life."

他们在使用tokenlizer的时候会自动的添加。

最终

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)

一起回顾一下吧

微调

预处理数据

从模型中心下载数据集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from datasets import load_dataset

raw_datasets = load_dataset("glue", "mrpc")
raw_datasets

# 输出
DatasetDict({
train: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx'],
num_rows: 3668
})
validation: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx'],
num_rows: 408
})
test: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx'],
num_rows: 1725
})
})

我们可以访问我们数据集中的每一个raw_train_dataset对象,如使用字典:

1
2
3
4
5
6
7
8
raw_train_dataset = raw_datasets["train"]
raw_train_dataset[0]
# 输出

{'idx': 0,
'label': 1,
'sentence1': 'Amrozi accused his brother , whom he called " the witness " , of deliberately distorting his evidence .',
'sentence2': 'Referring to him as only " the witness " , Amrozi accused his brother of deliberately distorting his evidence .'}

我们可以看到标签已经是整数了,所以我们不需要对标签做任何预处理。要知道哪个数字对应于哪个标签,我们可以查看raw_train_datasetfeatures. 这将告诉我们每列的类型:

1
2
3
4
5
6
7
raw_train_dataset.features

# 输出
{'sentence1': Value(dtype='string', id=None),
'sentence2': Value(dtype='string', id=None),
'label': ClassLabel(num_classes=2, names=['not_equivalent', 'equivalent'], names_file=None, id=None),
'idx': Value(dtype='int32', id=None)}

预处理训练集

为了预处理数据集,我们需要将文本转换为模型能够理解的数字。

1
2
3
4
5
6
from transformers import AutoTokenizer

checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
tokenized_sentences_1 = tokenizer(raw_datasets["train"]["sentence1"])
tokenized_sentences_2 = tokenizer(raw_datasets["train"]["sentence2"])

当然,这种方法似乎有点愚蠢,因为标记器不仅仅可以输入单个句子还可以输入一组句子,并按照我们的BERT模型所期望的输入进行处理:

1
2
3
4
5
6
7
8
9
inputs = tokenizer("This is the first sentence.", "This is the second one.")
inputs

# 输出
{
'input_ids': [101, 2023, 2003, 1996, 2034, 6251, 1012, 102, 2023, 2003, 1996, 2117, 2028, 1012, 102],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
}

类型标记ID(token_type_ids) 的作用就是告诉模型输入的哪一部分是第一句,哪一部分是第二句。如果选择其他的检查点,则不一定具有类型标记ID(token_type_ids)(例如,如果使用DistilBERT模型,就不会返回它们)。只有当它在预训练期间使用过这一层,模型在构建时依赖它们,才会返回它们。

即,我们可以这样传递

1
2
3
4
5
6
tokenized_dataset = tokenizer(
raw_datasets["train"]["sentence1"],
raw_datasets["train"]["sentence2"],
padding=True,
truncation=True,
)

这很有效,但它的缺点是返回字典(字典的键是输入词id(input_ids)注意力遮罩(attention_mask)类型标记ID(token_type_ids),字典的值是键所对应值的列表),这需要一定的内存空间。所以继续改进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def tokenize_function(example):
return tokenizer(example["sentence1"], example["sentence2"], truncation=True) # 定义了一个函数

tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
tokenized_datasets
# 输出
DatasetDict({
train: Dataset({
features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
num_rows: 3668
})
validation: Dataset({
features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
num_rows: 408
})
test: Dataset({
features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
num_rows: 1725
})
})

最后一件我们需要做的事情是,当我们一起批处理元素时,将所有示例填充到最长元素的长度——我们称之为动态填充。

动态填充

为了解决句子长度统一的问题,我们必须定义一个collate函数,该函数会将每个batch句子填充到正确的长度。

transformer库通过DataCollatorWithPadding为我们提供了这样一个函数。

1
2
3
from transformers import DataCollatorWithPadding

data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

汇总

所有的数据预处理工作,可以归结为这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from datasets import load_dataset
from transformers import AutoTokenizer, DataCollatorWithPadding

raw_datasets = load_dataset("glue", "mrpc")
checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)


def tokenize_function(example):
return tokenizer(example["sentence1"], example["sentence2"], truncation=True)


tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

使用 Trainer API 微调模型

训练

在我们定义我们的 Trainer 之前首先要定义一个 TrainingArguments 类,它将包含 Trainer用于训练和评估的所有超参数。您唯一必须提供的参数是保存训练模型的目录,以及训练过程中的检查点。对于其余的参数,您可以保留默认值,这对于基本微调应该非常有效。

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
from transformers import TrainingArguments

training_args = TrainingArguments("test-trainer")


from transformers import AutoModelForSequenceClassification

model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)



from transformers import Trainer

trainer = Trainer(
model,
training_args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
data_collator=data_collator,
tokenizer=tokenizer,
)

trainer.train() # 开始炼丹


评估

为了查看模型在每个训练周期结束的好坏,下面是我们如何使用compute_metrics()函数定义一个新的 Trainer

hl:11
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
33
def compute_metrics(eval_preds):
# 加载MRPC数据集的评估指标。这里的"glue"是指标集的名称,"mrpc"是具体的指标名称。
# "evaluate"库提供了一个统一的接口来加载和计算各种NLP任务的指标。
metric = evaluate.load("glue", "mrpc")

# eval_preds是一个元组,包含模型的输出logits和实际的标签labels。
# logits是模型输出的未归一化的概率分布,labels是实际的标签。
logits, labels = eval_preds

# 使用numpy的argmax函数来从logits中获取预测的标签。
# argmax函数返回沿给定轴的最大值的索引,这里我们假设最后一个轴是分类的维度。
# 因此,predictions将是一个包含模型预测的标签的数组。
predictions = np.argmax(logits, axis=-1)

# 使用evaluate库中的compute方法来计算预测结果和实际标签之间的指标。
# 这将返回一个包含各种性能指标的字典,如准确率、F1分数等。
return metric.compute(predictions=predictions, references=labels)



training_args = TrainingArguments("test-trainer", evaluation_strategy="epoch")
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)

trainer = Trainer(
model,
training_args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
data_collator=data_collator,
tokenizer=tokenizer,
compute_metrics=compute_metrics,
)
trainer.train()