NLP课程(二)- Transformers的使用
转载自:https://huggingface.co/learn/nlp-course/zh-CN/
原中文文档有很多地方翻译的太敷衍了,因此才有此系列文章。
NLP课程(二)- Transformers的使用
pipline的内部
示例:
1 | from transformers import pipeline |
输出:
1 | [{'label': 'POSITIVE', 'score': 0.9598047137260437}, |
pipeline函数相当于整合了下图三个步骤
1.使用分词器进行预处理
与其他神经网络一样,Transformer模型无法直接处理原始文本,因此我们管道的第一步是将文本输入转换为模型能够理解的数字。 为此,我们使用tokenizer(标记器),负责:
- 将输入拆分为单词、子单词或符号(如标点符号),称为标记(token)
- 将每个标记(token)映射到一个整数
- 添加可能对模型有用的其他输入
所有这些预处理都需要以与模型预训练时完全相同的方式完成,因此我们首先需要从Model Hub中下载这些信息。为此,我们使用AutoTokenizer
类及其from_pretrained()
方法。使用我们模型的检查点名称,它将自动获取与模型的标记器相关联的数据,并对其进行缓存(因此只有在您第一次运行下面的代码时才会下载)。
因为sentiment-analysis
(情绪分析)管道的默认检查点是distilbert-base-uncased-finetuned-sst-2-english
(你可以看到它的模型卡here),我们运行以下程序:
1 | from transformers import AutoTokenizer |
一旦我们有了标记器,我们就可以直接将我们的句子传递给它,然后我们就会得到一本字典,它可以提供给我们的模型!剩下要做的唯一一件事就是将输入ID列表转换为张量。
要指定要返回的张量类型(PyTorch、TensorFlow或plain NumPy),我们使用return_tensors
参数:
1 | raw_inputs = [ |
可以传递一个句子或一组句子,还可以指定要返回的张量类型(如果没有传递类型,您将得到一组列表)
输出:
101对应[‘cls’],1045对应[‘I’]
1 | { |
输出本身是一个包含两个键的字典,input_ids
和attention_mask
。input_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 | from transformers import BertConfig, BertModel |
从默认配置创建模型会使用随机值对其进行初始化.该模型可以在这种状态下使用,但会输出胡言乱语,因为没有对其进行训练。
所以更好的方式是使用已经训练过的模型。
2.加载训练过的模型
1 | from transformers import BertModel, AutoModel |
在上面的代码示例中,我们没有使用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 | import torch |
分词器将这些转换为词汇表索引,通常称为 input IDs . 每个序列现在都是一个数字列表!结果是:
1 | encoded_sequences = [ |
1 | model_inputs = torch.tensor(encoded_sequences) |
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 | from transformers import BertTokenizer, AutoTokenizer |
3.编码
**第一步:**将文本拆分为单词(或单词的一部分、标点符号等),通常称为标记(token)。有多个规则可以管理该过程,这就是为什么我们需要使用模型名称来实例化标记器(tokenizer),以确保我们使用模型预训练时使用的相同规则。
标记化过程由标记器(tokenizer)的tokenize()
方法实现
1 | from transformers import AutoTokenizer |
此方法的输出是一个字符串列表或标记(token):
1 | ['Using', 'a', 'transform', '##er', 'network', 'is', 'simple'] |
**第二步:**将这些标记转换为数字,这样我们就可以用它们构建一个张量并将它们提供给模型。为此,标记器(tokenizer)有一个词汇(vocabulary),这是我们在实例化它时下载的部分 from_pretrained()
方法。
1 | ids = tokenizer.convert_tokens_to_ids(tokens) |
解码:
从词汇索引中,我们想要得到一个字符串。这可以通过 decode()
方法实现
decode
方法不仅将索引转换回标记(token),还将属于相同单词的标记(token)组合在一起以生成可读的句子。当
1 | decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014]) |
处理多个序列
1.模型需要一批输入
意思是输入model的input_ids是二维的
1 | import torch |
若按上述执行操作,最后一行代码会报错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 | tokenized_inputs = tokenizer(sequence, return_tensors="pt") |
2.填充输入
当你试图将两个(或更多)句子组合在一起时,它们的长度可能不同。为了解决这个问题,我们通常填充输入。
可以在tokenizer.pad_token_id
中找到填充令牌ID. 让我们使用它,将我们的两句话分别发送到模型中,并分批发送到一起:
1 | model = AutoModelForSequenceClassification.from_pretrained(checkpoint) |
我们批处理预测中的logits有点问题:第二行应该与第二句的logits相同,但我们得到了完全不同的值!
这是因为Transformer模型的关键特性是关注层,它将每个标记上下文化。这些将考虑填充标记,因为它们涉及序列中的所有标记。为了在通过模型传递不同长度的单个句子时,或者在传递一批应用了相同句子和填充的句子时获得相同的结果,我们需要告诉这些注意层忽略填充标记。这是通过使用 attention mask来实现的。
3.attention mask
Attention masks是与输入ID张量形状完全相同的张量,用0和1填充:1s表示应注意相应的标记,0s表示不应注意相应的标记(即,模型的注意力层应忽略它们)。
让我们用attention mask完成上一个示例:
1 | batched_ids = [ |
现在我们得到了该批中第二个句子的相同登录。
请注意,第二个序列的最后一个值是一个填充ID,它在attention mask中是一个0值。
4.序列截断
如果不是要处理一项需要很长序列的任务
建议通过指定max_sequence_length参数:
1 | sequence = sequence[:max_sequence_length] |
结合
1.tokenizer
1 | from transformers import AutoTokenizer |
model_inputs = tokenizer(sequence)
是一个很强大的api
它可以处理多序列且不用变更代码
它也可以根据几个目标进行填充:
1 | # Will pad the sequences up to the maximum sequence length |
它还可以截断序列:
1 | sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"] |
还可以处理到特定框架张量的转换,然后可以直接发送到模型。"pt"
返回Py Torch张量,"tf"
返回TensorFlow张量,"np"
返回NumPy数组:
1 | sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"] |
2.特殊字符
标记器返回的输入 ID,我们会发现它们与之前的略有不同:
1 | sequence = "I've been waiting for a HuggingFace course my whole life." |
一个在开始时添加了一个标记(token) ID,一个在结束时添加了一个标记(token) ID。让我们解码上面的两个ID序列,看看这是怎么回事:
1 | print(tokenizer.decode(model_inputs["input_ids"])) |
标记器在开头添加了特殊单词[CLS]
,在结尾添加了特殊单词[SEP]
。这是因为模型是用这些数据预训练的,所以为了得到相同的推理结果,我们还需要添加它们。请注意,有些模型不添加特殊单词,或者添加不同的单词;模型也可能只在开头或结尾添加这些特殊单词。在任何情况下,标记器都知道需要哪些词符,并将为您处理这些词符。
3.处理多序列的完整代码
1 | import torch |