首先介绍了 - what is NLP?
Classifying whole sentences: Getting the sentiment of a review, detecting if an email is spam, determining if a sentence is grammatically correct or whether two sentences are logically related or not
Classifying each word in a sentence: Identifying the grammatical components of a sentence (noun, verb, adjective), or the named entities (person, location, organization)
Generating text content: Completing a prompt with auto-generated text, filling in the blanks in a text with masked words
Extracting an answer from a text: Given a question and a context, extracting the answer to the question based on the information provided in the context
Generating a new sentence from an input text: Translating a text into another language, summarizing a text
NLP isn’t limited to written text though. It also tackles complex challenges in speech recognition and computer vision, such as generating a transcript of an audio sample or a description of an image.
然后介绍了transformer模型的架构和它的衍生模型:
编码器模型仅使用 Transformer 模型的编码器。在每个阶段,注意力层都可以访问初始句子中的所有单词。这些模型通常具有“双向”注意力的特征,通常称为自动编码模型。
这些模型的预训练通常围绕着以某种方式破坏给定的句子(例如,通过屏蔽其中的随机单词)并让模型找到或重建初始句子。
编码器模型最适合需要理解完整句子的任务,例如句子分类、命名实体识别(以及更一般的单词分类)和提取式问答。
一些主要的encoder模型:
解码器模型仅使用 Transformer 模型的解码器。在每个阶段,对于给定的单词,注意力层只能访问句子中位于它之前的单词。这些模型通常称为自回归模型。
解码器模型的预训练通常围绕预测句子中的下一个单词。
这些模型最适合涉及文本生成的任务。
一些主要的decoder模型:
编码器-解码器模型(也称为序列到序列模型)使用 Transformer 架构的两个部分。在每个阶段,编码器的注意力层可以访问初始句子中的所有单词,而解码器的注意力层只能访问输入中位于给定单词之前的单词。
这些模型的预训练可以使用编码器或解码器模型的目标来完成,但通常涉及更复杂的事情。例如,T5是通过用单个掩码特殊词替换随机文本跨度(可以包含多个词)来预训练的,然后目标是预测这个掩码词替换的文本。
序列到序列模型最适合围绕根据给定输入生成新句子的任务,例如摘要、翻译或生成式问答。
一些主要的seq2seq模型:
首先介绍了transformer库的一些特点:
易于使用:只需两行代码即可下载、加载和使用最先进的 NLP 模型进行推理。
灵活性:从本质上讲,所有模型都是简单的 PyTorchnn.Module或 TensorFlowtf.keras.Model类,并且可以像其各自机器学习 (ML) 框架中的任何其他模型一样进行处理。
简单性:在整个库中几乎没有任何抽象。“一体化文件”是一个核心概念:模型的前向传递完全定义在单个文件中,因此代码本身是可理解和可破解的。
然后以一些完整的例子来介绍:
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 章中看到的,这个pipeline将三个步骤组合在一起:预处理、通过模型传递输入和后处理:
与其他神经网络一样,Transformer 模型不能直接处理原始文本,因此我们pipeline的第一步是将文本输入转换为模型可以理解的数字。为此,我们使用了一个tokenizer,它将负责:
所有这些预处理都需要以与模型预训练时完全相同的方式完成,因此我们首先需要从Model Hub下载该信息。为此,我们使用AutoTokenizer类及其from_pretrained()方法。
from transformers import AutoTokenizer
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
一旦我们有了tokenizer,我们就可以直接将我们的句子传递给它,我们将得到一个准备好提供给我们模型的字典!唯一要做的就是将输入 ID 列表转换为张量。
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") #pt代表pytorch
print(inputs)
输出:
{
'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, 0, 0, 0, 0, 0, 0, 0, 0, 0]
])
}
然后载入模型:
from transformers import AutoModel
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModel.from_pretrained(checkpoint)
The vector output by the Transformer module is usually large. It generally has three dimensions:
outputs = model(**inputs)
print(outputs.last_hidden_state.shape)
输出
torch.Size([2, 16, 768])
The model heads将隐藏状态的高维向量作为输入,并将它们投影到不同的维度上。它们通常由一个或几个线性层组成:
Transformer 模型的输出直接送到模型头进行处理。
在此图中,模型由其嵌入层和后续层表示。嵌入层将标记化输入中的每个输入 ID 转换为表示相关标记的向量。随后的层使用注意力机制操纵这些向量以产生句子的最终表示。
对于我们的示例,我们需要一个带有序列分类头的模型(能够将句子分类为正面或负面)。所以,我们实际上不会使用这个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])
我们从模型中获得的作为输出的值本身并不一定有意义。让我们来看看:
print(outputs.logits)
输出:
tensor([[-1.5607, 1.6123],
[ 4.1692, -3.3464]], grad_fn=<AddmmBackward>)
我们的模型预测[-1.5607, 1.6123]了第一句话和[ 4.1692, -3.3464]第二句话。这些不是概率,而是一些实数,即模型最后一层输出的原始非标准化分数。要转换为概率,它们需要经过一个SoftMax层(所有 Transformers 模型都输出 logits,因为训练的损失函数通常会将最后一个激活函数(例如 SoftMax)与实际的损失函数(例如 cross熵):
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'}
Now we can conclude that the model predicted the following:
We have successfully reproduced the three steps of the pipeline:
preprocessing with tokenizers,
passing the inputs through the model,
and postprocessing!
Now let’s take some time to dive deeper into each of those steps.
在本节中,我们将仔细研究创建和使用模型。我们将使用AutoModel该类,当您想从检查点实例化任何模型时,该类非常方便。
本AutoModel类及其所有亲属的实际上是在各种各样的库中的现有模式的简单wrappers。这是一个聪明的wrappers,因为它可以自动为您的检查点猜测合适的模型架构,然后使用该架构实例化模型。
但是,如果您知道要使用的模型类型,则可以直接使用定义其架构的类。让我们来看看它是如何与 BERT 模型配合使用的。
初始化 BERT 模型需要做的第一件事是加载配置对象:
from transformers import BertConfig, BertModel
# Building the config
config = BertConfig()
# Building the model from the config
model = BertModel(config)
The configuration contains many attributes that are used to build the model:
>>>print(config)
>>>BertConfig {
[...]
"hidden_size": 768,
"intermediate_size": 3072,
"max_position_embeddings": 512,
"num_attention_heads": 12,
"num_hidden_layers": 12,
[...]
}
不同的载入方法:
from transformers import BertModel
model = BertModel.from_pretrained("bert-base-cased")
正如之前看到的,我们可以替换BertModel为等效的AutoModel类。我们将从现在开始这样做,因为这会产生与checkpoint无关的代码;如果您的代码适用于一个checkpoint,它应该可以与另一个无缝协作。即使架构不同,只要checkpoint是针对类似任务(例如,情感分析任务)训练的,这也适用。
model.save_pretrained("directory_on_my_computer")
将会存在该目录下:
ls directory_on_my_computer
config.json pytorch_model.bin
Tokenizers是 NLP 管道的核心组件之一。它们有一个目的:将文本转换为模型可以处理的数据。模型只能处理数字,因此Tokenizers需要将我们的文本输入转换为数字数据。在本节中,我们将确切地探讨tokenization pipeline中发生的事情。
在 NLP 任务中,通常处理的数据是原始文本。这是此类文本的示例:
Jim Henson was a puppeteer
想到的第一种标记器是基于单词的。它通常很容易设置和使用,只需几条规则,并且通常会产生不错的结果。例如,在下图中,目标是将原始文本拆分为单词并为每个单词找到一个数字表示:
>>>tokenized_text = "Jim Henson was a puppeteer".split()
>>>print(tokenized_text)
['Jim', 'Henson', 'was', 'a', 'puppeteer']
每个单词都分配了一个 ID,从 0 开始一直到词汇表的大小。该模型使用这些 ID 来识别每个单词。
如果我们想用基于单词的分词器完全覆盖一种语言,我们需要为语言中的每个词都有一个标识符,这将生成大量的标记。例如,英语中有超过 500,000 个单词,因此要构建从每个单词到输入 ID 的映射,我们需要跟踪这么多 ID。此外,像“dog”这样的词与“dogs”这样的词的表示方式不同,模型最初无法知道“dog”和“dogs”是相似的:它会将这两个词识别为不相关。这同样适用于其他相似的词,例如“run”和“running”,模型最初不会认为它们是相似的。
最后,我们需要一个自定义标记来表示不在我们词汇表中的单词。这被称为“未知”标记,通常表示为“[UNK]”或“”。如果您看到分词器正在生成大量这样的标记,这通常是一个不好的迹象,因为它无法检索单词的合理表示,并且您在此过程中会丢失信息。制作词汇表的目标是使分词器将尽可能少的单词分词成未知标记。
减少未知标记数量的一种方法是使用基于字符的标记器更深一层。
这是一种非常简单粗暴的方法,
优点是字符集会很小,词汇外(未知)标记要少得多,因为每个单词都可以从字符构建。
缺点是每个字符本身并没有多大意义,而单词就是这种情况。然而,这又因语言而异;例如,在中文中,每个字符比拉丁语言中的字符包含更多的信息。
为了两全其美,我们可以使用结合这两种方法的第三种技术:subword tokenization 子词标记化。
子词分词算法依赖于这样一个原则,即不应将常用词拆分为更小的子词,而应将稀有词分解为有意义的子词。
例如,“annoyingly”可能被认为是一个罕见的词,可以分解为“annoying”和“ly”。这两者都可能作为独立的子词出现得更频繁,同时“annoyingly”的含义由“annoying”和“ly”的复合含义保持。
这是一个示例,展示了子词标记化算法如何标记序列“让我们做标记化!:
这些子词最终提供了很多语义含义:例如,在上面的示例中,“tokenization”被拆分为“token”和“ization”,这两个具有语义意义同时节省空间的令牌(只需要两个tokens代表一个长词)。这使我们能够对较小的词汇表进行相对较好的覆盖,并且几乎没有未知的标记。
这种方法在土耳其语等粘着语言中特别有用,您可以通过将子词串在一起来形成(几乎)任意长的复杂词。
Unsurprisingly, there are many more techniques out there. To name a few:
from transformers import AutoTokenizer
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")
将文本转换为数字称为encoding。encoding分两步完成:标记化,然后转换为输入 ID。
正如我们所见,第一步是将文本拆分为单词(或单词的一部分、标点符号等),通常称为标记。有多个规则可以管理该过程,这就是为什么我们需要使用模型名称来实例化分词器,以确保我们使用模型预训练时使用的相同规则。
第二步是将这些标记转换为数字,这样我们就可以用它们构建一个张量并将它们提供给模型。为此,分词器有一个词汇表,这是我们在使用from_pretrained()方法实例化它时下载的部分。同样,我们需要使用模型预训练时使用的相同词汇。
The tokenization process is done by the tokenize() method of the tokenizer:
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的转换由convert_tokens_to_ids()tokenizer 方法处理:
>>>ids = tokenizer.convert_tokens_to_ids(tokens)
>>>print(ids)
[7993, 170, 11303, 1200, 2443, 1110, 3014]
Decoding是相反的:从词汇索引中,我们想要得到一个字符串。这可以通过以下decode()方法完成:
decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)
输出:
'Using a Transformer network is simple'
注意该decode方法不仅将索引转换回标记,还将属于相同单词的标记组合在一起以生成可读的句子。当我们使用预测新文本的模型(根据提示生成的文本,或序列到序列问题(如翻译或摘要))时,这种行为将非常有用。
到现在为止,您应该了解标记器可以处理的原子操作:标记化、转换为 ID 以及将 ID 转换回字符串。然而,我们只是刮了冰山一角。在下一节中,我们将采用我们的方法来克服它的限制,并看看如何克服它们。
在上一节中,我们探讨了最简单的用例:对单个小长度序列进行推理。然而,一些问题已经出现: