精通 Transformers(二)

原文:zh.annas-archive.org/md5/a6f6476d556185fc0ddb2dae043823f3

译者:飞龙

协议:CC BY-NC-SA 4.0

第四章:自回归模型与其他语言模型

我们研究了text2text 应用的细节,例如总结,释义和机器翻译。

本章将涵盖以下主题:

  • 使用 AR 模型工作

  • 使用序列到序列Seq2Seq)模型工作

  • AR 语言模型训练

  • 使用 AR 模型进行自然语言生成

  • 使用simpletransformers进行总结和机器翻译微调

技术要求

为成功完成本章,需要以下库/软件包:

  • Anaconda

  • transformers 4.0.0

  • pytorch 1.0.2

  • tensorflow 2.4.0

  • datasets 1.4.1

  • tokenizers

  • simpletransformers 0.61

所有具有编码练习的笔记本将在以下 GitHub 链接上提供:github.com/PacktPublishing/Mastering-Transformers/tree/main/CH04

点击以下链接查看代码运行的实例:bit.ly/3yjn55X

使用 AR 模型工作

Transformers架构最初旨在对诸如机器翻译或总结之类的 Seq2Seq 任务有效,但随后它已被用于各种 NLP 问题,从标记分类到指代消解。随后的作品开始分别并更有创意地使用架构的左部分和右部分。在预训练阶段使用的目标,也被称为[MASK]符号,在微调阶段的数据中不存在,导致预训练和微调之间存在不一致。其次,BERT 模型可以说假定遮盖的标记是彼此独立的。

另一方面,AR 模型避免了关于独立性的假设,并且不会受到预训练和微调不一致引起的困扰,因为它们依赖于对先前标记的下一个标记的预测而不对其进行遮盖。它们仅利用具有遮盖自我注意力的解码器部分的Transformers。它们阻止模型向当前词的右侧(或向当前词的左侧)的字词进行前向方向(或后向方向)的访问,这称为单向性。它们也称为因果语言模型CLMs),因为它们是单向的。

AE 和 AR 模型之间的区别在这里简单地描述了:

图 4.1 - AE 与 AR 语言模型

图 4.1 - AE 与 AR 语言模型

GPT 及其两个后继者(GPT-2,GPT-3),Transformer-XLXLNet是文献中流行的 AR 模型之一。尽管 XLNet 基于自回归,但它以某种方式成功地以双向方式利用了单词的上下文,借助基于排列的语言目标。现在,我们开始介绍它们,并展示如何训练这些模型以进行各种实验。让我们先看一下 GPT。

介绍以及使用 GPT 训练模型

AR 模型由多个 transformer 块组成。每个块包含一个掩码多头自注意力层和一个逐点前馈层。最后一个 transformer 块中的激活被馈入 softmax 函数,产生整个词汇表上的单词概率分布,以预测下一个单词。

在原始的 GPT 论文《通过生成预训练来改善语言理解》(2018)中,作者们解决了传统的基于机器学习ML)的自然语言处理NLP)流水线面临的几个瓶颈。例如,这些流水线首先需要大量的任务特定数据和任务特定的架构。其次,很难应用具有最小架构更改的任务感知输入转换到预训练模型中。由 OpenAI 团队设计的原始 GPT 及其后继者(GPT-2 和 GPT-3)专注于缓解这些瓶颈。原始 GPT 研究的主要贡献是,预训练模型不仅为单一任务取得了令人满意的结果,而且为各种任务取得了令人满意的结果。通过从未标记的数据中学习生成模型,这称为无监督预训练,该模型只需通过相对少量的任务特定数据进行监督微调,这称为监督微调。这种两阶段方案在其他 transformer 模型中被广泛使用,其中无监督预训练后跟着监督微调。

为了尽可能使 GPT 架构尽可能通用,只有输入被以任务特定的方式转换,而整个架构保持几乎不变。这种遍历式方法将文本输入转换为根据任务的有序序列,以便预训练模型能够从中理解任务。图 4.2 的左侧(启发自原始论文)说明了原始 GPT 工作中使用的 transformer 架构和训练目标。右侧显示了如何为几个任务进行微调时转换输入。

简单来说,对于单序列任务,如文本分类,输入原样通过网络,并且线性层采用最后的激活来做出决策。对于句对任务,如文本蕴涵,由两个序列组成的输入用分隔符标记,如 图 4.2 中的第二个示例所示。在这两种情况下,架构看到统一的令牌序列被预训练模型处理。在文本蕴涵的情况下,这种转换中使用的分隔符帮助预训练模型知道哪一部分是前提或假设。多亏了输入转换,我们不必在任务之间对架构进行重大更改。

你可以在这里看到输入转换的表示:

 图 4.2 – 输入转换(启发自论文)

图 4.2 - 输入转换(灵感来自论文)

GPT 及其两个后继模型主要关注寻找特定的架构设计,其中不需要进行微调阶段。基于这个想法,一个模型可以非常熟练,也就是说,它在预训练阶段可以学习到关于语言的大部分信息,微调阶段所需的工作很少。因此,大多数任务的微调过程可以在三个时期内完成,并且对于大部分任务,相对较小的示例即可。极端情况下,零样本学习旨在禁用微调阶段。其基本思想是,模型可以在预训练阶段学习到关于语言的大量信息。对于所有基于 Transformer 的模型来说尤其如此。

原始 GPT 的后继模型

GPT-2(参见论文语言模型是无监督多任务学习者(2019))是原始 GPT-1 的后继者,它是在比原始模型更多的训练数据 WebText 上训练的一个更大的模型。它在没有进行微调的零样本设置中,在八个任务中的七个任务上取得了最新的结果,但在一些任务中成功有限。在测量长程依赖性的较小数据集上取得了可比较的结果。GPT-2 的作者认为语言模型不一定需要明确的监督来学习一个任务。相反,当它们在一个巨大而多样的网页数据集上进行训练时,它们可以学习这些任务。GPT-2 被认为是一个通用系统,用P(output|input, task-i)替换了原始 GPT 中的学习目标P(output|input),其中模型在相同输入情况下,根据特定任务来生成不同的输出,也就是说,GPT-2 通过训练相同的无监督模型学习了多个任务。一个单独的预训练模型通过学习目标便能学习到不同的能力。我们在其他研究中也看到了类似的多任务和元任务设置。这种转变到多任务学习MTL)使得可以对同一个输入进行多种不同的任务。但是这些模型是如何确定执行哪个任务呢?他们通过零样本任务转移来实现。

与原始 GPT 相比,GPT-2 没有具体任务的微调,并且能够在零样本任务转移的设置中工作,在这个设置中,所有下游任务都是预测条件概率的一部分。任务在输入中以某种方式组织,模型应该理解下游任务的性质,并相应地提供答案。例如,对于英文到土耳其语的 MT 任务,它不仅取决于输入,还取决于任务。输入被排列成英文句子后跟随土耳其句子,有一个分隔符,模型可以从中了解到任务是英文到土耳其语的翻译。

OpenAI 团队使用了 1750 亿个参数训练了 GPT-3 模型(参见论文语言模型是几次学习者(2020)),比 GPT-2 大 100 倍。GPT-2 和 GPT-3 的架构类似,主要区别通常在于模型大小和数据集数量/质量。由于数据集中的大量数据和所训练的大量参数,它在零次学习、一次学习和少次学习(K=32)设置下在许多下游任务上取得了更好的结果,而无需进行任何基于梯度的微调。团队表明,对于许多任务,包括翻译、问答QA)和掩码标记任务,随着参数大小和示例数量的增加,模型性能也提高了。

Transformer-XL

由于在最初的设计和上下文分段中缺乏循环,Transformers模型受固定长度上下文的困扰,尽管它们能够学习长期依赖关系。大多数Transformers将文档分成一系列固定长度(主要为 512)的段,其中跨段的任何信息流均不可能。因此,语言模型无法捕捉超出这个固定长度限制之外的长期依赖性。此外,分段过程不考虑句子边界。一个段可能荒谬地由一个句子的后半部分和它的后继者的前半部分组成,因此在预测下一个标记时,语言模型可能会缺少必要的上下文信息。这一问题被研究称为上下文分段问题。

为了解决和克服这些问题,Transformers-XL 作者(参见论文Transformers-XL:超越固定长度上下文的专注语言模型(2019))提出了一种新的Transformers架构,包括一个段级别的循环机制和一种新的位置编码方案。这种方法启发了许多后续模型。它不仅限于两个连续的段,因为有效的上下文可以延伸到两个段之外。循环机制在每两个连续段之间起作用,导致对若干段的跨度到一定程度。模型可以处理的最大可能依赖长度受层数和段长度的限制。

XLNet

屏蔽语言建模MLM)主导了基于Transformers的体系结构的预训练阶段。然而,它在过去面临过批评,因为在预训练阶段存在屏蔽标记,但在微调阶段不存在,这导致了预训练和微调之间的差异。由于这种缺失,模型可能无法利用在预训练阶段学到的所有信息。 XLNet(参见论文XLNet: Generalized Autoregressive Pretraining for Language Understanding(2019))用置换语言建模PLM)取代了 MLM,这是输入标记的随机排列,以克服这一瓶颈。排列语言建模使每个标记位置利用来自所有位置的上下文信息,从而捕捉到双向上下文。客观函数只是对因子化顺序进行排列,并定义标记预测的顺序,但不改变序列的自然位置。简而言之,模型在置换后选择一些标记作为目标,然后在其余标记和目标的自然位置的条件下进一步尝试预测它们。这使得可以双向使用 AR 模型。

XLNet 利用了 AE 和 AR 模型的优势。它确实是一个广义的 AR 模型,然而,由于基于排列的语言建模,它可以关注来自左右上下文的标记。除了其客观函数,XLNet 由两个重要机制组成:它将 Transformer-XL 的段级循环机制集成到其框架中,并包括了针对目标感知表示的两流注意力机制的慎重设计。

让我们在接下来的部分讨论这些模型,使用Transformers的两部分。

与 Seq2Seq 模型一起工作

Transformers的左编码器和右解码器部分通过交叉注意力相连,这有助于每个解码器层关注最终编码器层。这自然地促使模型产生与原始输入紧密相关的输出。原始Transformers——Seq2Seq 模型通过以下方案实现了这一点:

输入标记-> 嵌入-> 编码器-> 解码器-> 输出标记

Seq2Seq 模型保留了Transformers的编码器和解码器部分。T5,双向和自回归TransformersBART)和用于抽象摘要序列到序列模型的提取间隙句子进行预训练PEGASUS)是流行的 Seq2Seq 模型之一。

T5

大多数 NLP 架构,从 Word2Vec 到Transformers,通过预测使用上下文(相邻)词语的掩码词来学习嵌入和其他参数。我们将 NLP 问题视为词预测问题。一些研究将几乎所有 NLP 问题都视为问答或令牌分类。同样,T5(参见 2019 年的论文探索统一文本到文本Transformers的迁移学习极限)提出了一个统一框架,通过将它们转换为文本到文本问题来解决许多任务。T5 的潜在理念是将所有 NLP 任务都转换为文本到文本(Seq2Seq)问题,其中输入和输出都是标记列表,因为已发现文本到文本框架有益于将相同模型应用于从问答到文本摘要等不同的 NLP 任务中。

下图,受原始论文启发,展示了 T5 如何在一个统一框架内解决四种不同的 NLP 问题—MT,语言可接受性,语义相似性和摘要:

图 4.3 – T5 框架图

图 4.3 – T5 框架图

T5 模型大致遵循原始编码器-解码器Transformers模型。修改是在层归一化和位置嵌入方案中进行的。T5 使用相对位置嵌入,而不是使用正弦位置嵌入或学习嵌入,这在Transformers架构中变得越来越普遍。T5 是一个单一模型,可以处理各种任务,例如语言生成。更重要的是,它将任务转换为文本格式。该模型接受由任务前缀和附加的输入组成的文本。我们将标记的文本数据集转换为{'inputs': '....', 'targets': ...'}格式,其中我们将目的作为前缀插入输入。然后,我们用标记数据训练模型,使其学会做什么以及如何做。如前图所示,在英语-德语翻译任务中,输入"translate English to German: That is good."会产生"das is gut."。同样,任何带有"summarize:"前缀的输入都将由该模型汇总。

介绍 BART

与 XLNet 一样,BART 模型(参见 2019 年的论文BART:用于自然语言生成、翻译和理解的去噪序列到序列预训练)利用了 AE 和 AR 模型的方案。它使用标准 Seq2Seq Transformers架构,稍作修改。BART 是一个使用各种破坏性方法来破坏文档的预训练模型。该研究对该领域的主要贡献在于它允许我们应用多种类型的创造性破坏方案,如下图所示:

图 4.4 – 受原始 BART 论文启发的图

图 4.4 – 受原始 BART 论文启发的图

我们将详细查看每个方案,如下所示:

  • [MASK] 符号,与 BERT 模型相同。

  • 标记删除:从文档中随机删除标记。模型被迫确定哪些位置被删除。

  • [MASK] 标记。还有 [MASK] 标记插入。

  • 句子重排:输入中的句子被分割并随机打乱顺序。

  • 文档旋转:文档被旋转,以便以随机选择的标记(在前面的图表中为 C)开头。目标是找到文档的起始位置。

BART 模型可以通过多种方式进行微调,用于下游应用,如 BERT。对于序列分类任务,输入经过编码器和解码器,并将解码器的最终隐藏状态视为学习到的表示。然后,简单的线性分类器可以进行预测。同样,对于标记分类任务,整个文档被馈送到编码器和解码器中,并且最终解码器的最后状态是每个标记的表示。基于这些表示,我们可以解决标记分类问题,我们将在第六章中讨论,用于标记分类的微调语言模型命名实体识别NER)和 词性标注POS)任务可以使用这个最终表示来解决,其中 NER 识别文本中的人物和组织等实体,而 POS 将每个标记与它们的词汇类别联系起来,比如名词、形容词等。

对于序列生成,BART 模型的解码器块,即 AR 解码器,可以直接微调用于生成序列的任务,例如抽象 QA 或摘要。BART 的作者(Lewis,Mike 等)使用了两个标准摘要数据集:CNN/DailyMail 和 XSum 来训练模型。作者还表明,可以使用编码器部分——消耗源语言——和解码器部分——生成目标语言中的词语——作为单个预训练解码器用于 MT。他们用一个新的随机初始化的编码器替换了编码器嵌入层,以学习源语言中的词语。然后,模型以端到端的方式进行训练,该方式训练新编码器将外语单词映射到 BART 可以去噪到目标语言的输入。新编码器可以使用单独的词汇表,包括原始 BART 模型中的外语。

在 HuggingFace 平台上,我们可以通过以下代码行访问原始预训练的 BART 模型:

AutoModel.from_pretrained('facebook/bart-large')

当我们调用transformers库中的标准summarization流水线时,如下代码行所示,将加载一个经过精简预训练的 BART 模型。此调用隐式加载了"sshleifer/distilbart-cnn-12-6"模型及其相应的分词器,如下所示:

summarizer = pipeline("summarization")

以下代码明确加载相同的模型和相应的分词器。代码示例接受要进行总结的文本并输出结果:

from transformers import BartTokenizer, BartForConditionalGeneration, BartConfig
from transformers import pipeline
model = \
BartForConditionalGeneration.from_pretrained('sshleifer/distilbart-cnn-12-6')
tokenizer = BartTokenizer.from_pretrained('sshleifer/distilbart-cnn-12-6')
nlp=pipeline("summarization", model=model, tokenizer=tokenizer)
text='''
We order two different types of jewelry from this
company the other jewelry we order is perfect.
However with this jewelry I have a few things I
don't like. The little Stone comes out of these
and customers are complaining and bringing them
back and we are having to put new jewelry in their
holes. You cannot sterilize these in an autoclave
as well because it heats up too much and the glue
does not hold up so the second group of these that
we used I did not sterilize them that way and the
stones still came out. When I use a dermal clamp
to put the top on the stones come out immediately.
DO not waste your money on this particular product
buy the three mm. that has the claws that hold the
jewelry in those are perfect. So now I'm stuck
with jewelry that I can't sell not good for
business.'''
q=nlp(text)
import pprint
pp = pprint.PrettyPrinter(indent=0, width=100)
pp.pprint(q[0]['summary_text'])
(' The little Stone comes out of these little stones and customers are complaining and bringing ' 'them back and we are having to put new jewelry in their holes . You cannot sterilize these in an ' 'autoclave because it heats up too much and the glue does not hold up so the second group of ' 'these that we used I did not sterilize them that way and the stones still came out .')

在下一节中,我们将动手学习如何训练这样的模型。

AR 语言模型训练

在本节中,您将了解如何训练自己的 AR 语言模型。我们将从 GPT-2 开始,并深入了解其用于训练的不同功能,使用transformers库。

您可以找到任何特定的语料库来训练您自己的 GPT-2,但是在本示例中,我们使用了简·奥斯汀的《爱玛》,这是一部浪漫小说。强烈建议在更大的语料库上进行训练,以获得更一般的语言生成。

在我们开始之前,值得注意的是,我们使用了 TensorFlow 的本地训练功能来展示所有 Hugging Face 模型都可以直接在 TensorFlow 或 PyTorch 上进行训练,如果您愿意的话。请按照以下步骤:

  1. 您可以使用以下命令下载Emma小说的原始文本:

    wget https://raw.githubusercontent.com/teropa/nlp/master/resources/corpora/gutenberg/austen-emma.txt
    
  2. 第一步是在你打算训练 GPT-2 的语料库上训练BytePairEncoding分词器。以下代码将从tokenizers库导入BPE分词器:

    from tokenizers.models import BPE
    from tokenizers import Tokenizer
    from tokenizers.decoders import ByteLevel as ByteLevelDecoder
    from tokenizers.normalizers import Sequence, Lowercase
    from tokenizers.pre_tokenizers import ByteLevel
    from tokenizers.trainers import BpeTrainer
    
  3. 如您所见,在此示例中,我们打算通过添加更多功能(如Lowercase规范化)来训练更高级的分词器。要创建一个tokenizer对象,可以使用以下代码:

    tokenizer = Tokenizer(BPE())
    tokenizer.normalizer = Sequence([
        Lowercase()
    ])
    tokenizer.pre_tokenizer = ByteLevel()
    tokenizer.decoder = ByteLevelDecoder()
    

    第一行从BPE分词器类创建一个分词器。对于规范化部分,已添加Lowercase,并将pre_tokenizer属性设置为ByteLevel以确保我们的输入为字节。decoder属性也必须设置为ByteLevelDecoder以能够正确解码。

  4. 接下来,将使用50000的最大词汇量和来自ByteLevel的初始字母训练分词器,如下所示:

    trainer = BpeTrainer(vocab_size=50000, inital_alphabet=ByteLevel.alphabet(), special_tokens=[
                "<s>",
                "<pad>",
                "</s>",
                "<unk>",
                "<mask>"
            ])
    tokenizer.train(["austen-emma.txt"], trainer)
    
  5. 还需要添加特殊的标记以进行考虑。为了保存分词器,需要创建一个目录,如下所示:

    !mkdir tokenizer_gpt
    
  6. 您可以通过运行以下命令保存分词器:

    tokenizer.save("tokenizer_gpt/tokenizer.json")
    
  7. 现在分词器已保存,是时候预处理语料库并使其准备好进行 GPT-2 训练了,但首先,重要的导入不能被遗忘。执行导入的代码如下所示:

    from transformers import GPT2TokenizerFast, GPT2Config, TFGPT2LMHeadModel
    
  8. 分词器可以通过使用GPT2TokenizerFast加载,如下所示:

    tokenizer_gpt = GPT2TokenizerFast.from_pretrained("tokenizer_gpt")
    
  9. 还必须添加特殊标记及其标记,如下所示:

    tokenizer_gpt.add_special_tokens({
      "eos_token": "</s>",
      "bos_token": "<s>",
      "unk_token": "<unk>",
      "pad_token": "<pad>",
      "mask_token": "<mask>"
    })
    
  10. 您还可以通过运行以下代码来双重检查是否一切都正确:

    tokenizer_gpt.eos_token_id
    >> 2
    

    此代码将输出当前分词器的2

  11. 您也可以通过执行以下代码对一个句子进行测试:

    tokenizer_gpt.encode("<s> this is </s>")
    >> [0, 265, 157, 56, 2]
    

    对于此输出,0是句子的开头,26515756与句子本身相关,EOS 被标记为2,即</s>

  12. 创建配置对象时必须使用这些设置。以下代码将创建一个config对象和 GPT-2 模型的 TensorFlow 版本:

    config = GPT2Config(
      vocab_size=tokenizer_gpt.vocab_size,
      bos_token_id=tokenizer_gpt.bos_token_id,
      eos_token_id=tokenizer_gpt.eos_token_id
    )
    model = TFGPT2LMHeadModel(config)
    
  13. 在运行config对象时,您可以看到配置以字典格式显示,如下所示:

    config
    >> GPT2Config {  "activation_function": "gelu_new",  "attn_pdrop": 0.1,  "bos_token_id": 0,  "embd_pdrop": 0.1,  "eos_token_id": 2,  "gradient_checkpointing": false,  "initializer_range": 0.02,  "layer_norm_epsilon": 1e-05,  "model_type": "gpt2",  "n_ctx": 1024,  "n_embd": 768,  "n_head": 12,  "n_inner": null,  "n_layer": 12,  "n_positions": 1024,  "resid_pdrop": 0.1,  "summary_activation": null,  "summary_first_dropout": 0.1,  "summary_proj_to_labels": true,  "summary_type": "cls_index",  "summary_use_proj": true,  "transformers_version": "4.3.2",  "use_cache": true,  "vocab_size": 11750}
    

    如您所见,其他设置未被触及,有趣的部分是vocab_size被设置为11750。背后的原因是我们将最大词汇量设置为50000,但语料库较少,只有11750

  14. 现在,您可以准备好为预训练做好语料库的准备,如下所示:

    with open("austen-emma.txt", "r", encoding='utf-8') as f:
        content = f.readlines()
    
  15. 现在,内容将包括原始文件中的所有原始文本,但需要删除每一行的'\n'并删除少于10个字符的行,如下所示:

    content_p = []
    for c in content:
        if len(c)>10:
            content_p.append(c.strip())
    content_p = " ".join(content_p)+tokenizer_gpt.eos_token
    
  16. 删除短行将确保模型在长序列上进行训练,以生成更长的序列。在前述代码段的末尾,content_p具有经连接的原始文件,并在末尾添加了eos_token。但您也可以采取不同的策略—例如,您可以通过在每一行后添加</s>来将每一行分开,这将帮助模型识别句子何时结束。但我们的目的是使其能够更长的序列而无需遇到 EOS。代码见如下片段:

    tokenized_content = tokenizer_gpt.encode(content_p)
    

    前述代码片段中的 GPT 标记生成器将对整个文本进行标记化,使其成为一个整体的长令牌 ID 序列。

  17. 现在,是时候为训练制作样本了,如下所示:

    sample_len = 100
    examples = []
    for i in range(0, len(tokenized_content)):
        examples.append(tokenized_content[i:i + sample_len])
    
  18. 前述代码使examples从给定文本的特定部分开始,每个大小为100,并在100个令牌后结束:

    train_data = []
    labels = []
    for example in examples:
        train_data.append(example[:-1])
        labels.append(example[1:])
    

    train_data中,将会有一系列从头到第 99 个令牌的大小为99的序列,标签将具有从1100的令牌序列。

  19. 为了加快训练速度,需要将数据制作成 TensorFlow 数据集的形式,如下所示:

    Import tensorflow as tf
    buffer = 500
    batch_size = 16   
    dataset = tf.data.Dataset.from_tensor_slices((train_data, labels))
    dataset = dataset.shuffle(buffer).batch(batch_size, drop_remainder=True)
    

    buffer是用于对数据进行洗牌的缓冲区大小,batch_size是训练的批量大小。drop_remainder用于丢弃余数,如果余数小于16,则丢弃。

  20. 现在,您可以指定您的optimizerlossmetrics属性,如下所示:

    optimizer = tf.keras.optimizers.Adam(learning_rate=3e-5, epsilon=1e-08, clipnorm=1.0)
    loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
    metric = tf.keras.metrics.SparseCategoricalAccuracy('accuracy')
    model.compile(optimizer=optimizer, loss=[loss, *[None] * model.config.n_layer], metrics=[metric])
    
  21. 你希望,该模型已经编译并准备好进行训练的周期数量,如下所示:

    epochs = 10
    model.fit(dataset, epochs=epochs)
    

    您将看到类似于这样的输出:

图 4.5 – 使用 TensorFlow/Keras 进行 GPT-2 训练

图 4.5 – 使用 TensorFlow/Keras 进行 GPT-2 训练

现在,我们将看看如何使用 AR 模型进行 NLG。现在你已经保存了模型,它将用于在下一节中生成句子。

到目前为止,你已经学会了如何训练自己的 NLG 模型。在下一节中,我们描述如何利用 NLG 模型进行语言生成。

使用 AR 模型进行 NLG

在上一节中,你已经学会了如何在自己的语料库上训练 AR 模型。结果,你已经训练了自己的 GPT-2 版本。但对于问题“我该如何使用它?”的缺失答案仍然存在。为了回答这个问题,让我们继续如下进行:

  1. 让我们从您刚刚训练的模型开始生成句子,如下所示:

    def generate(start, model): 
        input_token_ids = tokenizer_gpt.encode(start, return_tensors='tf') 
        output = model.generate( 
            input_token_ids, 
            max_length = 500, 
            num_beams = 5, 
            temperature = 0.7, 
            no_repeat_ngram_size=2, 
            num_return_sequences=1 
        ) 
        return tokenizer_gpt.decode(output[0])
    

    在前面代码片段中定义的generate函数接受一个start字符串,并生成遵循该字符串的序列。您可以更改参数,例如将max_length设置为较小的序列大小或将num_return_sequences设置为不同的生成。

  2. 让我们尝试一个空字符串,如下所示:

    generate(" ", model)
    

    我们得到以下输出:

    图 4.6 – GPT-2 文本生成示例

    图 4.6 – GPT-2 文本生成示例

    正如您从之前的输出中所看到的那样,即使文本的语义不太动人,但在许多情况下,语法几乎是正确的。

  3. 现在,让我们尝试不同的开始,将max_length设置为较低值,比如30,如下所示:

    generate("wetson was very good")
    >> 'wetson was very good; but it, that he was a great must be a mile from them, and a miss taylor in the house;'
    

    正如您回忆西斯顿是小说中的一个角色。

  4. 要保存模型,您可以使用以下代码使其可重复使用以发布或在不同应用程序中使用:

    model.save_pretrained("my_gpt-2/")
    
  5. 为确保您的模型正确保存,您可以尝试加载它,如下所示:

    model_reloaded = TFGPT2LMHeadModel.from_pretrained("my_gpt-2/")
    

    两个文件被保存——一个config文件和一个model.h5文件,用于 TensorFlow 版本。我们可以在以下截图中看到这两个文件:

    图 4.7 – 语言模型 save_pretrained 输出

    图 4.7 – 语言模型 save_pretrained 输出

  6. Hugging Face 还有一个必须使用的标准文件名标准——这些标准文件名可通过以下导入获得:

    from transformers import WEIGHTS_NAME, CONFIG_NAME, TF2_WEIGHTS_NAME
    

    但是,在使用save_pretrained函数时,不需要放置文件名,只需目录即可。

  7. Hugging Face 还有AutoModelAutoTokenizer类,正如您从之前的章节中所见。您也可以使用这个功能来保存模型,但在这之前仍然需要手动完成一些配置。第一件事就是将分词器以适当的格式保存以供AutoTokenizer使用。您可以通过使用save_pretrained来实现这一点,如下所示:

    tokenizer_gpt.save_pretrained("tokenizer_gpt_auto/")
    

    这是输出:

    图 4.8 – Tokenizer save_pretrained 输出

    图 4.8 – Tokenizer save_pretrained 输出

  8. 在您指定的目录中显示了文件列表,但tokenizer_config必须手动更改以便可用。首先,您应该将其重命名为config.json,其次,您应该在model_type属性中添加一个属性为gpt2,如下所示:

    {"model_type":"gpt2",
    ...
    }
    
  9. 现在,一切准备就绪,您可以简单地使用这两行代码加载modeltokenizer

    model = AutoModel.from_pretrained("my_gpt-2/", from_tf=True)
    tokenizer = AutoTokenizer.from_pretrained("tokenizer_gpt_auto")
    

    然而,不要忘记将from_tf设置为True,因为您的模型是以 TensorFlow 格式保存的。

到目前为止,您已经学会了如何使用tensorflowtransformers预训练和保存自己的文本生成模型。您还学会了如何保存一个预训练模型并准备将其用作自动模型。在接下来的部分,您将学习如何使用其他模型的基础知识。

使用 simpletransformers 进行摘要和 MT 微调

到目前为止,你已经学会了从头开始训练语言模型的基础知识和高级方法,但是从头开始训练自己的语言模型并不总是可行的,因为有时会存在低计算能力等障碍。在本节中,你将学习如何针对特定的 MT 和摘要任务对自己的数据集进行语言模型的微调。请按照以下步骤进行操作:

  1. 要开始,你需要安装simpletransformers库,如下所示:

    pip install simpletransformers
    
  2. 下一步是下载包含你的平行语料库的数据集。这个平行语料库可以是任何类型的 Seq2Seq 任务。对于本例,我们将使用 MT 示例,但你可以使用任何其他数据集来完成其他任务,比如释义、摘要,甚至将文本转换为 SQL

    你可以从 www.kaggle.com/seymasa/turkish-to-english-translation-dataset/version/1 下载数据集。

  3. 下载并解压数据后,需要为列标题添加 ENTR,以便更容易使用。你可以使用pandas加载数据集,如下所示:

    import pandas as pd
    df = pd.read_csv("TR2EN.txt",sep="\t").astype(str)
    
  4. 必须向数据集添加 T5 特定的命令,以使其了解正在处理的命令。你可以使用以下代码完成这项工作:

    data = []
    for item in digitrons():
        data.append(["translate english to turkish", item[1].EN, item[1].TR])
    
  5. 然后,你可以重组 DataFrame,就像这样:

    df = pd.DataFrame(data, columns=["prefix", "input_text", "target_text"])
    

    结果如下截图所示:

    图 4.9 – 英-土机器翻译平行语料库

    图 4.9 – 英-土机器翻译平行语料库

  6. 接下来,运行以下代码导入所需的类:

    from simpletransformers.t5 import T5Model, T5Args
    
  7. 使用以下代码定义训练参数:

    model_args = T5Args()
    model_args.max_seq_length = 96
    model_args.train_batch_size = 20
    model_args.eval_batch_size = 20
    model_args.num_train_epochs = 1
    model_args.evaluate_during_training = True
    model_args.evaluate_during_training_steps = 30000
    model_args.use_multiprocessing = False
    model_args.fp16 = False
    model_args.save_steps = -1
    model_args.save_eval_checkpoints = False
    model_args.no_cache = True
    model_args.reprocess_input_data = True
    model_args.overwrite_output_dir = True
    model_args.preprocess_inputs = False
    model_args.num_return_sequences = 1
    model_args.wandb_project = "MT5 English-Turkish Translation"
    
  8. 最后,你可以加载任何你想微调的模型。以下是我们选择的一个:

    model = T5Model("mt5", "google/mt5-small", args=model_args, use_cuda=False)
    

    如果你的 CUDA 内存不足以容纳 mT5,不要忘记将use_cuda设置为False

  9. 使用以下代码可以拆分 traineval DataFrames:

    train_df = df[: 470000]
    eval_df = df[470000:]
    
  10. 最后一步是使用以下代码开始训练:

    model.train_model(train_df, eval_data=eval_df)
    

    训练结果将如下所示:

    图 4.10 – mT5 模型评估结果

    图 4.10 – mT5 模型评估结果

    这表明了评估和训练损失。

  11. 你可以使用以下代码简单加载和使用模型:

    model_args = T5Args()
    model_args.max_length = 512
    model_args.length_penalty = 1
    model_args.num_beams = 10
    model = T5Model("mt5", "outputs", args=model_args, use_cuda=False)
    

    现在可以使用model_predict函数进行从英语到土耳其语的翻译。

simpletransformers库使得从序列标记到 Seq2Seq 模型的训练变得非常简单和可用。

干得漂亮!我们已经学会了如何训练自己的 AR 模型,并且到达了本章的结束。

摘要

在本章中,我们学习了关于 AR 语言模型的各个方面,从预训练到微调。我们通过训练生成语言模型和在诸如 MT 等任务上进行微调来查看此类模型的最佳特征。我们了解了更复杂模型如 T5 的基础知识,并使用这种模型执行了 MT。我们还使用了simpletransformers库。我们在自己的语料库上训练了 GPT-2,并使用它生成了文本。我们学会了如何保存它,并使用AutoModel。我们还深入研究了如何训练和使用 BPE,使用tokenizers库。

在下一章中,我们将看到如何为文本分类微调模型。

参考文献

这里有一些参考资料,你可以用来扩展我们在本章学到的内容:

  • Radford, A., Wu, J., Child, R., Luan, D., Amodei, D.Sutskever, I.2019)。语言模型是无监督多任务学习者OpenAI 博客1(8)9

  • Lewis, M., Liu, Y., Goyal, N., Ghazvininejad, M., Mohamed, A.,Levy, O.Zettlemoyer, L.2019)。BART: 用于自然语言生成、翻译和理解的去噪序列到序列预训练arXiv 预印本 arXiv:1910.13461

  • Xue, L., Constant, N., Roberts, A., Kale, M., Al-Rfou, R.,Siddhant, A.Raffel, C.2020)。mT5: 一个大规模多语言预训练文本到文本转换器arXiv 预印本 arXiv:2010.11934

  • Raffel, C.Shazeer, N.Roberts, A.Lee, K.Narang, S.Matena, M. 和 Liu, P. J.2019)。探索统一文本到文本转换器的迁移学习极限arXiv 预印本 arXiv:1910.10683

  • Yang, Z.Dai, Z.Yang, Y.Carbonell, J.Salakhutdinov, R.Le, Q. V.2019)。XLNet: 用于语言理解的广义自回归预训练arXiv 预印本 arXiv:1906.08237

  • Dai, Z., Yang, Z., Yang, Y., Carbonell, J.,Le, Q. V.Salakhutdinov, R.2019)。Transformer-xl: 超越固定长度上下文的关注语言模型arXiv 预印本 arXiv:1901.02860

第五章:文本分类的语言模型微调

在本章中,我们将学习如何配置预训练模型进行文本分类,并如何对其进行微调以适应任何文本分类的下游任务,例如情感分析或多类分类。我们还将讨论如何处理句对和回归问题,涵盖一个实现。我们将使用 GLUE 等知名数据集,以及我们自己的定制数据集。然后,我们将利用 Trainer 类,该类处理了训练和微调过程的复杂性。

首先,我们将学习如何使用 Trainer 类进行单句二元情感分类微调。然后,我们将使用原生 PyTorch 进行情感分类的训练,而不使用 Trainer 类。在多类分类中,将考虑超过两个类别。我们将有七个类别分类微调任务要执行。最后,我们将训练一个文本回归模型,以预测具有句子对的数值。

本章将涵盖以下主题:

  • 文本分类简介

  • 对单句二元分类微调 BERT 模型

  • 使用原生 PyTorch 训练分类模型

  • 使用自定义数据集对 BERT 进行多类分类微调

  • 对句对回归进行 BERT 的微调

  • 利用 run_glue.py 对模型进行微调

技术要求

我们将使用 Jupyter Notebook 运行我们的编程练习。您需要 Python 3.6+。确保已安装以下软件包:

  • sklearn

  • Transformers 4.0+

  • datasets

所有本章编程练习的笔记本将在以下 GitHub 链接上提供:github.com/PacktPublishing/Mastering-Transformers/tree/main/CH05

查看以下链接以观看代码演示视频:

bit.ly/3y5Fe6R

文本分类简介

文本分类(也称为文本分类)是将文档(句子、Twitter 帖子、书籍章节、电子邮件内容等)映射到预定义列表(类别)中的一种方式。在具有正负标签的两类情况下,我们称之为二元分类 - 更具体地说,是情感分析。对于多于两个类别的情况,我们称之为多类分类,其中类别是相互排斥的,或者称之为多标签分类,其中类别不是相互排斥的,这意味着一个文档可以获得多个标签。例如,一篇新闻文章的内容可能同时涉及体育和政治。除了这种分类之外,我们可能希望对文档进行范围为[-1,1]的评分或在[1-5]范围内对其进行排名。我们可以用回归模型解决这种问题,其中输出的类型是数值而不是分类。

幸运的是,变换器架构使我们能够高效地解决这些问题。对于句对任务,如文档相似性或文本蕴涵,输入不是单一句子,而是两个句子,如下图所示。我们可以评分两个句子在语义上相似的程度,或者预测它们是否在语义上相似。另一个句对任务是文本蕴涵,其中问题定义为多类分类。在 GLUE 基准测试中,两个序列被消耗:蕴含/矛盾/中性:

图 5.1 – 文本分类方案

让我们通过微调预训练的 BERT 模型开始我们的训练过程,针对一个常见问题:情感分析。

为单句二元分类微调 BERT 模型

在本节中,我们将讨论如何使用流行的IMDb 情感数据集,通过微调预训练的 BERT 模型进行情感分析。使用 GPU 可以加快我们的学习过程,但如果您没有这样的资源,您也可以通过 CPU 进行微调。让我们开始吧:

  1. 要了解并保存当前设备的信息,我们可以执行以下代码行:

    from torch import cuda
    device = 'cuda' if cuda.is_available() else 'cpu'
    
  2. 我们将在这里使用 DistilBertForSequenceClassification 类,它是从 DistilBert 类继承而来,顶部有一个特殊的序列分类头。我们可以利用这个分类头来训练分类模型,其中默认类别数为2

    from transformers import DistilBertTokenizerFast, DistilBertForSequenceClassification
    model_path= 'distilbert-base-uncased'
    tokenizer = DistilBertTokenizerFast.from_pre-trained(model_path)
    model = \ DistilBertForSequenceClassification.from_pre-trained(model_path, id2label={0:"NEG", 1:"POS"}, label2id={"NEG":0, "POS":1})
    
  3. 注意传递给模型的两个参数称为id2labellabel2id,用于推理。或者,我们可以实例化一个特定的config对象并将其传递给模型,如下所示:

    config = AutoConfig.from_pre-trained(....)
    SequenceClassification.from_pre-trained(.... config=config)
    
  4. 现在,让我们选择一个名为IMDB Dataset的流行情感分类数据集。原始数据集包含两组数据:25,000 个训练示例和 25 个测试示例。我们将数据集分成测试集和验证集。请注意,数据集的前一半示例为正面,而后一半的示例都为负面。我们可以按以下方式分布示例:

    from datasets import load_dataset
    imdb_train= load_dataset('imdb', split="train")
    imdb_test= load_dataset('imdb', split="test[:6250]+test[-6250:]")
    imdb_val= \
    load_dataset('imdb', split="test[6250:12500]+test[-12500:-6250]")
    
  5. 让我们检查数据集的形状:

    >>> imdb_train.shape, imdb_test.shape, imdb_val.shape
    ((25000, 2), (12500, 2), (12500, 2))
    
  6. 您可以根据计算资源的情况从数据集中取出一小部分。对于较小的部分,您应该运行以下代码,选择 4,000 个示例进行训练,1,000 个进行测试,以及 1,000 个进行验证,如下所示:

    imdb_train= load_dataset('imdb', split="train[:2000]+train[-2000:]")
    imdb_test= load_dataset('imdb', split="test[:500]+test[-500:]")
    imdb_val= load_dataset('imdb', split="test[500:1000]+test[-1000:-500]")
    
  7. 现在,我们可以将这些数据集通过tokenizer模型,使它们准备好进行训练:

    enc_train = imdb_train.map(lambda e: tokenizer( e['text'], padding=True, truncation=True), batched=True, batch_size=1000) 
    enc_test =  imdb_test.map(lambda e: tokenizer( e['text'], padding=True, truncation=True), batched=True, batch_size=1000) 
    enc_val =   imdb_val.map(lambda e: tokenizer( e['text'], padding=True, truncation=True), batched=True, batch_size=1000)
    
  8. 让我们看看训练集的样子。注意力掩码和输入 ID 是由分词器添加到数据集中的,以便 BERT 模型进行处理:

    import pandas as pd
    pd.DataFrame(enc_train)
    

    输出如下:

    图 5.2 – 编码后的训练数据集

    此时,数据集已准备好用于训练和测试。Trainer类(TFTrainer用于 TensorFlow)和TrainingArguments类(TFTrainingArguments用于 TensorFlow)将帮助我们处理训练的许多复杂性。我们将在TrainingArguments类中定义我们的参数集,然后将其传递给Trainer对象。

    让我们定义每个训练参数的作用:

    表 1 - 不同训练参数定义表

  9. 若要获取更多信息,请查看TrainingArguments的 API 文档,或在 Python notebook 中执行以下代码:

    TrainingArguments?
    
  10. 虽然像 LSTM 这样的深度学习架构需要许多 epoch,有时超过 50 个,但对于基于 transformer 的微调,由于迁移学习,我们通常会满足于 3 个 epoch 的数量。大部分时间,这个数量已经足够进行微调,因为预训练模型在预训练阶段已经学到了很多关于语言的知识,通常需要大约 50 个 epoch。要确定正确的 epoch 数量,我们需要监控训练和评估损失。我们将学习如何在第十一章中跟踪训练,注意力可视化和实验追踪

  11. 对于许多下游任务问题,这将足够用。在训练过程中,我们的模型检查点将被保存在./MyIMDBModel文件夹中,每 200 步保存一次:

    from transformers import TrainingArguments, Trainer
    training_args = TrainingArguments(
        output_dir='./MyIMDBModel', 
        do_train=True,
        do_eval=True,
        num_train_epochs=3,              
        per_device_train_batch_size=32,  
        per_device_eval_batch_size=64,
        warmup_steps=100,                
        weight_decay=0.01,
        logging_strategy='steps',               
        logging_dir='./logs',            
        logging_steps=200,
        evaluation_strategy= 'steps',
             fp16= cuda.is_available(),
        load_best_model_at_end=True
    )
    
  12. 在实例化Trainer对象之前,我们将定义compute_metrics()方法,它可以帮助我们监控训练过程中特定指标的进展,如 Precision、RMSE、Pearson 相关性、BLEU 等。文本分类问题(如情感分类或多类分类)大多使用微平均宏平均 F1 进行评估。而宏平均方法平等对待每个类别,微平均对每个文本或每个标记的分类决策平等对待。微平均等于模型正确决策的次数与总决策次数的比率。而宏平均方法计算每个类别的 Precision、Recall 和 F1 的平均分数。对于我们的分类问题,宏平均更方便进行评估,因为我们希望给每个标签平等的权重,如下所示:

    from sklearn.metrics import accuracy_score, Precision_Recall_fscore_support
    def compute_metrics(pred):
        labels = pred.label_ids
        preds = pred.predictions.argmax(-1)
        Precision, Recall, f1, _ = \                        
        Precision_Recall_fscore_support(labels, preds, average='macro')
        acc = accuracy_score(labels, preds)
        return {
            'Accuracy': acc,
            'F1': f1,
            'Precision': Precision,
            'Recall': Recall
        }
    
  13. 我们几乎已经准备好开始训练过程。现在,让我们实例化Trainer对象并启动它。Trainer类是一个非常强大和优化的工具,用于组织 PyTorch 和 TensorFlow(TFTrainer用于 TensorFlow)的复杂训练和评估过程,这得益于transformers库:

    trainer = Trainer(
        model=model,                     
        args=training_args,                 
        train_dataset=enc_train,         
        eval_dataset=enc_val,            
        compute_metrics= compute_metrics
    )
    
  14. 最后,我们可以开始训练过程:

    results=trainer.train()
    

    前面的调用开始记录指标,我们将在第十一章注意力可视化和实验跟踪中更详细地讨论这些内容。整个 IMDb 数据集包括 25,000 个训练示例。使用批量大小为 32,我们有 25K/32 约等于 782 个步骤,并且对于 3 个时期还有 2,346 个步骤(782 x 3),如下所示的进度条显示:

    图 5.3 – Trainer 对象生成的输出

  15. Trainer对象保留了验证损失最小的检查点。它选择了步骤 1,400 处的检查点,因为该步骤的验证损失最小。让我们在三个(训练/测试/验证)数据集上评估最佳检查点:

    >>> q=[trainer.evaluate(eval_dataset=data) for data in [enc_train, enc_val, enc_test]]
    >>> pd.DataFrame(q, index=["train","val","test"]).iloc[:,:5]
    

    输出如下:

    图 5.4 – 分类模型在训练/验证/测试数据集上的性能

  16. 干得好!我们成功完成了训练/测试阶段,并获得了 92.6 的准确度和 92.6 的宏平均 F1 值。为了更详细地监视您的训练过程,您可以调用高级工具,如 TensorBoard。这些工具会解析日志,并使我们能够跟踪各种指标以进行全面分析。我们已经在./logs文件夹下记录了性能和其他指标。只需在我们的 Python 笔记本中运行tensorboard函数就足够了,如下面的代码块所示(我们将在第十一章中详细讨论 TensorBoard 和其他监控工具的可视化和实验跟踪):

    %reload_ext tensorboard
    %tensorboard --logdir logs
    
  17. 现在,我们将使用模型进行推理以检查其是否正常工作。让我们定义一个预测函数来简化预测步骤,如下所示:

    def get_prediction(text):
        inputs = tokenizer(text, padding=True,truncation=True,
        max_length=250, return_tensors="pt").to(device)
        outputs = \ model(inputs["input_ids"].to(device),inputs["attention_mask"].to(device))
        probs = outputs[0].softmax(1)
        return probs, probs.argmax() 
    
  18. 现在,运行模型进行推理:

    >>> text = "I didn't like the movie it bored me "
    >>> get_prediction(text)[1].item()
    0 
    
  19. 我们在这里得到的是0,表示的是负面。我们已经定义了哪个 ID 表示哪个标签。我们可以使用这种映射方案来获取标签。或者,我们可以将所有这些乏味的步骤简单地传递给一个专用的 API,即 Pipeline,这是我们已经熟悉的。在实例化之前,让我们保存最佳模型以进行进一步的推理:

    model_save_path = "MyBestIMDBModel"
    trainer.save_model(model_save_path)
    tokenizer.save_pre-trained(model_save_path)
    

    Pipeline API 是使用预训练模型进行推理的简便方法。我们从保存模型的位置加载模型并将其传递给 Pipeline API,其余工作由其完成。我们可以跳过保存步骤,而是直接将modeltokenizer对象在内存中传递给 Pipeline API。如果这样做,将获得相同的结果。

  20. 如下面的代码所示,当我们执行二元分类时,需要将 Pipeline 的任务名称参数指定为sentiment-analysis

    >>> from transformers import pipeline, \ DistilBertForSequenceClassification, DistilBertTokenizerFast
    >>> model = \ DistilBertForSequenceClassification.from_pre-trained("MyBestIMDBModel")
    >>> tokenizer= \ DistilBertTokenizerFast.from_pre-trained("MyBestIMDBModel")
    >>> nlp= pipeline("sentiment-analysis", model=model, tokenizer=tokenizer)
    >>> nlp("the movie was very impressive")
    Out:  [{'label': 'POS', 'score': 0.9621992707252502}]
    >>> nlp("the text of the picture was very poor")
    Out:  [{'label': 'NEG', 'score': 0.9938313961029053}]
    

    Pipeline 知道如何处理输入,并某种方式学会了哪个 ID 表示哪个(POSNEG)标签。它还产生类别概率。

    干得好!我们已经使用Trainer类为 IMDb 数据集微调了情感预测模型。在接下来的部分中,我们将使用原生 PyTorch 进行相同的二元分类培训。我们还将使用其他数据集。

使用原生 PyTorch 训练分类模型

Trainer类非常强大,我们要感谢 HuggingFace 团队提供了这样一个有用的工具。然而,在本节中,我们将从头开始微调预训练模型,以了解其内部运行原理。让我们开始吧:

  1. 首先,让我们加载用于微调的模型。我们将在这里选择DistilBERT,因为它是 BERT 的一个小型、快速和廉价版本:

    from transformers import DistilBertForSequenceClassification
    model = DistilBertForSequenceClassification.from_pre-trained('distilbert-base-uncased')
    
  2. 要对任何模型进行微调,我们需要将其设置为训练模式,如下所示:

    model.train()
    
  3. 现在,我们必须加载分词器:

    from transformers import DistilBertTokenizerFast
    tokenizer = DistilBertTokenizerFast.from_pre-trained('bert-base-uncased')
    
  4. 由于Trainer类已经为我们组织好了整个过程,我们在之前的 IMDb 情感分类练习中没有处理优化和其他训练设置。现在,我们需要自己实例化优化器。在这里,我们必须选择AdamW,它是 Adam 算法的一个实现,但修复了权重衰减。最近的研究表明,AdamW产生的训练损失和验证损失比使用 Adam 训练的模型更好。因此,在许多 transformer 训练过程中,它是一个广泛使用的优化器:

    from transformers import AdamW
    optimizer = AdamW(model.parameters(), lr=1e-3)
    

    要从头开始设计微调过程,我们必须了解如何实现单步前向传播和反向传播。我们可以通过 transformer 层传递一个批次并获得输出,该输出由分词器生成的input_idsattention_mask组成,并使用真实标签计算损失。正如我们所看到的,输出包含losslogits两部分。现在,loss.backward()通过使用输入和标签评估模型来计算张量的梯度。optimizer.step()执行单个优化步骤并使用计算的梯度更新权重,这称为反向传播。当我们很快将所有这些行放入一个循环中时,我们还将添加optimizer.zero_grad(),它清除所有参数的梯度。在循环开始时调用这一点非常重要;否则,我们可能会积累多个步骤的梯度。输出的第二个张量是logits。在深度学习的上下文中,logits(logistic units 的缩写)是神经架构的最后一层,由实数作为预测值组成。在分类的情况下,logits 需要通过 softmax 函数转换为概率。否则,它们只是用于回归的标准化值。

  5. 如果我们想要手动计算损失,我们就不能将标签传递给模型。由于这个原因,模型只产生 logits,而不计算损失。在下面的示例中,我们正在手动计算交叉熵损失:

    from torch.nn import functional
    labels = torch.tensor([1,0,1])
    outputs = model(input_ids, attention_mask=attention_mask)
    loss = functional.cross_entropy(outputs.logits, labels)
    loss.backward()
    optimizer.step()
    loss
    Output: tensor(0.6101, grad_fn=<NllLossBackward>)
    
  6. 有了这个,我们学会了如何将批量输入通过网络的前向方向在单个步骤中进行传递。现在,是时候设计一个循环,以批量迭代整个数据集来训练模型进行多个 epochs。为此,我们将首先设计Dataset类。它是torch.Dataset的子类,继承成员变量和函数,并实现__init__()__getitem()__抽象函数:

    from torch.utils.data import Dataset
    class MyDataset(Dataset):
        def __init__(self, encodings, labels):
            self.encodings = encodings
            self.labels = labels
        def __getitem__(self, idx):
            item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}
            item['labels'] = torch.tensor(self.labels[idx])
            return item
        def __len__(self):
            return len(self.labels) 
    
  7. 通过拿取另一个情感分析数据集 SST-2 数据集,即斯坦福情感树库 v2SST2)来对情感分析的模型进行微调。我们还将加载 SST-2 的相应度量进行评估,如下所示:

    import datasets
    from datasets import load_dataset
    sst2= load_dataset("glue","sst2")
    from datasets import load_metric
    metric = load_metric("glue", "sst2")
    
  8. 我们将相应地提取句子和标签:

    texts=sst2['train']['sentence']
    labels=sst2['train']['label']
    val_texts=sst2['validation']['sentence']
    val_labels=sst2['validation']['label']
    
  9. 现在,我们可以通过标记器传递数据集并实例化MyDataset对象,使 BERT 模型可以与它们一起工作:

    train_dataset= MyDataset(tokenizer(texts, truncation=True, padding=True), labels)
    val_dataset=  MyDataset(tokenizer(val_texts, truncation=True, padding=True), val_labels)
    
  10. 让我们实例化一个Dataloader类,它提供了通过加载顺序迭代数据样本的接口。这也有助于批处理和内存固定:

    from torch.utils.data import DataLoader
    train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True)
    val_loader =  DataLoader(val_dataset, batch_size=16, shuffle=True)
    
  11. 以下行检测设备并适当地定义AdamW优化器:

    from transformers import  AdamW 
    device = \
    torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
    model.to(device)
    optimizer = AdamW(model.parameters(), lr=1e-3)
    

    到目前为止,我们知道如何实现前向传播,这是我们处理一批示例的地方。在这里,批量数据通过神经网络的前向方向进行传递。在单个步骤中,每层从第一层到最后一层都由批量数据处理,根据激活函数,传递到下一层。为了在多个 epochs 中遍历整个数据集,我们设计了两个嵌套循环:外部循环是为了 epochs,而内部循环是为了每批次的步骤。内部部分由两个块组成;一个用于训练,另一个用于评估每个 epochs。您可能已经注意到,我们在第一个训练循环中调用了model.train(),当移动第二个评估块时,我们调用了model.eval()。这很重要,因为我们使模型处于训练和推理模式。

  12. 我们已经讨论了内部块。注意,我们通过相应的metric对象跟踪模型的性能:

    for epoch in range(3):
        model.train()
        for batch in train_loader:
            optimizer.zero_grad()
            input_ids = batch['input_ids'].to(device)
            attention_mask = batch['attention_mask'].to(device)
            labels = batch['labels'].to(device)
            outputs = \
    model(input_ids, attention_mask=attention_mask, labels=labels)
            loss = outputs[0]
            loss.backward()
            optimizer.step()
        model.eval()
        for batch in val_loader:
            input_ids = batch['input_ids'].to(device)
            attention_mask = batch['attention_mask'].to(device)
            labels = batch['labels'].to(device)
            outputs = \
    model(input_ids, attention_mask=attention_mask, labels=labels)
            predictions=outputs.logits.argmax(dim=-1)  
            metric.add_batch(
                    predictions=predictions,
                    references=batch["labels"],
                )
        eval_metric = metric.compute()
        print(f"epoch {epoch}: {eval_metric}")
    OUTPUT:
    epoch 0: {'accuracy': 0.9048165137614679} 
    epoch 1: {'accuracy': 0.8944954128440367} 
    epoch 2: {'accuracy': 0.9094036697247706}
    

    做得好!我们已经对模型进行了微调,并获得了大约 90.94 的准确度。剩下的流程,如保存、加载和推理,将类似于我们在Trainer类中所做的。

有了这个,我们已经完成了二元分类。在下一节中,我们将学习如何为非英语语言实现多类分类模型。

用自定义数据集对多类分类进行 BERT 微调

在本节中,我们将对土耳其 BERT,即BERTurk,进行多类分类下游任务的微调,其自定义数据集包含从土耳其报纸编制的七个类别。我们将从获取数据集开始。或者,您可以在本书的 GitHub 存储库中找到它,或者从www.kaggle.com/savasy/ttc4900 获取它:

  1. 首先,在 Python 笔记本中运行以下代码以获取数据:

    !wget https://raw.githubusercontent.com/savasy/TurkishTextClassification/master/TTC4900.csv
    
  2. 从加载数据开始:

    import pandas as pd
    data= pd.read_csv("TTC4900.csv")
    data=data.sample(frac=1.0, random_state=42)
    
  3. 让我们用id2labellabel2id组织 ID 和标签,以使模型弄清楚哪个 ID 指代哪个标签。我们还将NUM_LABELS的数量传递给模型,以指定 BERT 模型顶部薄分类头层的大小:

    labels=["teknoloji","ekonomi","saglik","siyaset","kultur","spor","dunya"]
    NUM_LABELS= len(labels)
    id2label={i:l for i,l in enumerate(labels)}
    label2id={l:i for i,l in enumerate(labels)}
    data["labels"]=data.category.map(lambda x: label2id[x.strip()])
    data.head()
    

    输出如下:

    图 5.5 – 文本分类数据集 – TTC 4900

  4. 让我们使用 pandas 对象计算并绘制类别数量:

    data.category.value_counts().plot(kind='pie')
    

    如下图所示,数据集的类别已经被相当分配:

    图 5.6 – 类别分布

  5. 以下执行实例化一个序列分类模型,带有标签数量(7)、标签 ID 映射和一个土耳其 BERT 模型(dbmdz/bert-base-turkish-uncased),即 BERTurk。要检查这一点,请执行以下操作:

    >>> model
    
  6. 输出将是模型的摘要,太长了,无法在此处显示。相反,让我们通过以下代码关注最后一层:

    (classifier): Linear(in_features=768, out_features=7, bias=True)
    
  7. 你可能已经注意到我们没有选择DistilBert,因为没有预先训练好的uncased DistilBert适用于土耳其语:

    from transformers import BertTokenizerFast
    tokenizer = BertTokenizerFast.from_pre-trained("dbmdz/bert-base-turkish-uncased", max_length=512)
     from transformers import BertForSequenceClassification
    model = BertForSequenceClassification.from_pre-trained("dbmdz/bert-base-turkish-uncased", num_labels=NUM_LABELS, id2label=id2label, label2id=label2id)
    model.to(device)
    
  8. 现在,让我们准备训练(%50)、验证(%25)和测试(%25)数据集,如下所示:

    SIZE= data.shape[0]
    ## sentences
    train_texts= list(data.text[:SIZE//2])
    val_texts=   list(data.text[SIZE//2:(3*SIZE)//4 ])
    test_texts=  list(data.text[(3*SIZE)//4:])
    ## labels
    train_labels= list(data.labels[:SIZE//2])
    val_labels=   list(data.labels[SIZE//2:(3*SIZE)//4])
    test_labels=  list(data.labels[(3*SIZE)//4:])
    ## check the size
    len(train_texts), len(val_texts), len(test_texts)
    (2450, 1225, 1225)
    
  9. 以下代码将三个数据集的句子进行标记化,并将它们的标记转换为整数(input_ids),然后将它们输入 BERT 模型:

    train_encodings = tokenizer(train_texts, truncation=True, padding=True)
    val_encodings  = tokenizer(val_texts, truncation=True,
    padding=True)
    test_encodings = tokenizer(test_texts, truncation=True, padding=True)
    
  10. 我们已经实现了MyDataset类(请参阅第 14 页)。该类继承自抽象的Dataset类,通过重写__getitem____len__()方法来使用任何数据加载器返回数据集的项目和大小,分别预期返回:

    train_dataset = MyDataset(train_encodings, train_labels)
    val_dataset = MyDataset(val_encodings, val_labels)
    test_dataset = MyDataset(test_encodings, test_labels)
    
  11. 由于我们有一个相对较小的数据集,我们将保持批处理大小为16。请注意,TrainingArguments的其他参数几乎与之前的情感分析实验相同:

    from transformers import TrainingArguments, Trainer
    training_args = TrainingArguments(
        output_dir='./TTC4900Model', 
        do_train=True,
        do_eval=True,
        num_train_epochs=3,              
        per_device_train_batch_size=16,  
        per_device_eval_batch_size=32,
        warmup_steps=100,                
        weight_decay=0.01,
        logging_strategy='steps',                 
        logging_dir='./multi-class-logs',            
        logging_steps=50,
        evaluation_strategy="steps",
        eval_steps=50,
        save_strategy="epoch", 
        fp16=True,
        load_best_model_at_end=True
    ) 
    
  12. 情感分析和文本分类是相同评估指标的对象;即宏平均的宏平均 F1、精度和召回率。因此,我们不会重新定义compute_metric()函数。以下是实例化Trainer对象的代码:

    trainer = Trainer(
        model=model,                     
        args=training_args,                 
        train_dataset=train_dataset,         
        eval_dataset=val_dataset,            
        compute_metrics= compute_metrics
    )
    
  13. 最后,让我们开始训练过程:

    trainer.train()
    

    输出如下:

    图 5.7 – 文本分类的 Trainer 类的输出

  14. 要检查训练好的模型,我们必须在三个数据集拆分上评估微调的模型,如下所示。我们的最佳模型是在步骤 300 微调的,损失为 0.28012:

    q=[trainer.evaluate(eval_dataset=data) for data in [train_dataset, val_dataset, test_dataset]]
    pd.DataFrame(q, index=["train","val","test"]).iloc[:,:5]
    

    输出如下:

    图 5.8 – 文本分类模型在训练/验证/测试数据集上的性能

    图 5.8 – 文本分类模型在训练/验证/测试数据集上的性能

    分类准确率约为 92.6,而 F1 宏平均约为 92.5。在文献中,许多方法都在这个土耳其基准数据集上进行了测试。它们大多采用 TF-IDF 和线性分类器、word2vec 嵌入,或基于 LSTM 的分类器,最好的 F1 也达到了 90.0。与这些方法相比,除了 transformer,微调的 BERT 模型表现更佳。

  15. 与任何其他实验一样,我们可以通过 TensorBoard 跟踪实验:

    %load_ext tensorboard
    %tensorboard --logdir multi-class-logs/
    
  16. 让我们设计一个运行推理模型的函数。如果你想看到真实标签而不是 ID,你可以使用我们模型的config对象,如下面的predict函数所示:

    def predict(text):
        inputs = tokenizer(text, padding=True, truncation=True, max_length=512, return_tensors="pt").to("cuda")
        outputs = model(**inputs)
        probs = outputs[0].softmax(1)
        return probs, probs.argmax(),model.config.id2label[probs.argmax().item()]
    
  17. 现在,我们准备调用predict函数进行文本分类推理。以下代码对一个关于足球队的句子进行分类:

    text = "Fenerbahçeli futbolcular kısa paslarla hazırlık çalışması yaptılar"
    predict(text)
    (tensor([[5.6183e-04, 4.9046e-04, 5.1385e-04, 9.9414e-04, 3.4417e-04, 9.9669e-01, 4.0617e-04]], device='cuda:0', grad_fn=<SoftmaxBackward>), tensor(5, device='cuda:0'), 'spor') 
    
  18. 正如我们所看到的,该模型正确地预测了句子为体育(spor)。现在,是时候保存模型并使用from_pre-trained()函数重新加载它了。以下是代码:

    model_path = "turkish-text-classification-model"
    trainer.save_model(model_path)
    tokenizer.save_pre-trained(model_path)
    
  19. 现在,我们可以重新加载已保存的模型,并借助pipeline类进行推理:

    model_path = "turkish-text-classification-model"
    from transformers import pipeline, BertForSequenceClassification, BertTokenizerFast
    model = BertForSequenceClassification.from_pre-trained(model_path)
    tokenizer= BertTokenizerFast.from_pre-trained(model_path)
    nlp= pipeline("sentiment-analysis", model=model, tokenizer=tokenizer)
    
  20. 您可能已经注意到任务的名称是sentiment-analysis。这个术语可能令人困惑,但这个参数实际上会返回最终的TextClassificationPipeline。让我们运行 pipeline:

    >>> nlp("Sinemada hangi filmler oynuyor bugün")
    [{'label': 'kultur', 'score': 0.9930670261383057}]
    >>> nlp("Dolar ve Euro bugün yurtiçi piyasalarda yükseldi")
    [{'label': 'ekonomi', 'score': 0.9927696585655212}]
    >>> nlp("Bayern Münih ile Barcelona bugün karşı karşıya geliyor. Maçı İngiliz hakem James Watts yönetecek!")
    [{'label': 'spor', 'score': 0.9975664019584656}]
    

    这就是我们的模型!它已经成功预测了。

到目前为止,我们已经实现了两个单句任务;即情感分析和多类分类。在下一节,我们将学习如何处理句对输入,以及如何使用 BERT 设计回归模型。

为句对回归微调 BERT 模型

回归模型被认为是用于分类的,但最后一层只包含一个单元。这不是通过 softmax logistic 回归进行处理,而是进行了归一化。为了指定模型并在顶部放置单单元头层,我们可以直接通过BERT.from_pre-trained()方法传递num_labels=1参数,或者通过Config对象传递此信息。最初,这需要从预训练模型的config对象中复制,如下所示:

from transformers import DistilBertConfig, DistilBertTokenizerFast, DistilBertForSequenceClassification
model_path='distilbert-base-uncased'
config = DistilBertConfig.from_pre-trained(model_path, num_labels=1)
tokenizer = DistilBertTokenizerFast.from_pre-trained(model_path)
model = \
DistilBertForSequenceClassification.from_pre-trained(model_path, config=config)

嗯,我们的预训练模型由于num_labels=1参数具有单单元头层。现在,我们准备用我们的数据集对模型进行微调。在这里,我们将使用语义文本相似性基准STS-B),它是从各种内容中引用的句对集合,如新闻标题等。每对句子都被注释了从 1 到 5 的相似度分数。我们的任务是微调 BERT 模型以预测这些分数。我们将遵循文献,使用皮尔逊/斯皮尔曼相关系数评估模型。让我们开始吧:

  1. 以下代码加载了数据。原始数据被分成了三部分。然而,测试分组没有标签,因此我们可以将验证数据分为两部分,如下所示:

    import datasets
    from datasets import load_dataset
    stsb_train= load_dataset('glue','stsb', split="train")
    stsb_validation = load_dataset('glue','stsb', split="validation")
    stsb_validation=stsb_validation.shuffle(seed=42)
    stsb_val= datasets.Dataset.from_dict(stsb_validation[:750])
    stsb_test= datasets.Dataset.from_dict(stsb_validation[750:])
    
  2. 让我们通过 pandas 将stsb_train训练数据整理整齐:

    pd.DataFrame(stsb_train)
    

    训练数据如下所示:

    图 5.9 – STS-B 训练数据集

    图 5.9 – STS-B 训练数据集

  3. 运行以下代码以检查三个集合的形状:

    stsb_train.shape, stsb_val.shape, stsb_test.shape
    ((5749, 4), (750, 4), (750, 4))
    
  4. 运行以下代码对数据集进行分词:

    enc_train = stsb_train.map(lambda e: tokenizer( e['sentence1'],e['sentence2'], padding=True, truncation=True), batched=True, batch_size=1000) 
    enc_val =   stsb_val.map(lambda e: tokenizer( e['sentence1'],e['sentence2'], padding=True, truncation=True), batched=True, batch_size=1000) 
    enc_test =  stsb_test.map(lambda e: tokenizer( e['sentence1'],e['sentence2'], padding=True, truncation=True), batched=True, batch_size=1000)
    
  5. 分词器使用 [SEP] 分隔符合并两个句子,并为句对生成单个 input_ids 和一个 attention_mask,如下所示:

    pd.DataFrame(enc_train)
    

    输出如下:

    图 5.10 – 编码训练数据集

    from transformers import TrainingArguments, Trainer
    training_args = TrainingArguments(
        output_dir='./stsb-model', 
        do_train=True,
        do_eval=True,
        num_train_epochs=3,              
        per_device_train_batch_size=32,  
        per_device_eval_batch_size=64,
        warmup_steps=100,                
        weight_decay=0.01,
        logging_strategy='steps',                
        logging_dir='./logs',            
        logging_steps=50,
        evaluation_strategy="steps",
        save_strategy="epoch",
        fp16=True,
        load_best_model_at_end=True
    )
    
  6. 当前回归任务与以前的分类任务之间的另一个重要区别是 compute_metrics 的设计。在这里,我们的评估指标将基于皮尔逊相关系数斯皮尔曼秩相关,遵循文献中提供的通用做法。我们还提供了均方误差MSE)、均方根误差RMSE)和平均绝对误差MAE)等常用的度量标准,特别是对于回归模型:

    import numpy as np
    from scipy.stats import pearsonr
    from scipy.stats import spearmanr
    def compute_metrics(pred):
        preds = np.squeeze(pred.predictions) 
        return {"MSE": ((preds - pred.label_ids) ** 2).mean().item(),
                "RMSE": (np.sqrt ((  (preds - pred.label_ids) ** 2).mean())).item(),
                "MAE": (np.abs(preds - pred.label_ids)).mean().item(),
         "Pearson" : pearsonr(preds,pred.label_ids)[0],
         "Spearman's Rank":spearmanr(preds,pred.label_ids)[0]
                }
    
  7. 现在,让我们实例化 Trainer 对象:

    trainer = Trainer(
            model=model,
            args=training_args,
            train_dataset=enc_train,
            eval_dataset=enc_val,
            compute_metrics=compute_metrics,
            tokenizer=tokenizer
        )
    

    运行训练,像这样:

    train_result = trainer.train()
    

    输出如下:

    图 5.11 – 文本回归的训练结果

    图 5.11 – 文本回归的训练结果

  8. 计算的最佳验证损失为 0.544973,在步骤 450。让我们在该步骤评估最佳检查点模型,如下所示:

    q=[trainer.evaluate(eval_dataset=data) for data in [enc_train, enc_val, enc_test]]
    pd.DataFrame(q, index=["train","val","test"]).iloc[:,:5]
    

    输出如下:

    图 5.12 – 训练/验证/测试数据集上的回归性能

    图 5.12 – 训练/验证/测试数据集上的回归性能

    在测试数据集上,皮尔逊和斯皮尔曼相关分数分别约为 87.54 和 87.28。我们没有得到 SoTA 结果,但基于 GLUE Benchmark 排行榜,我们得到了一个可比较的 STS-B 任务结果。请查看排行榜!

  9. 现在我们准备好进行推断模型了。让我们来看以下两个意思相同的句子,并将它们传递给模型:

    s1,s2="A plane is taking off.","An air plane is taking off."
    encoding = tokenizer(s1,s2, return_tensors='pt', padding=True, truncation=True, max_length=512)
    input_ids = encoding['input_ids'].to(device)
    attention_mask = encoding['attention_mask'].to(device)
    outputs = model(input_ids, attention_mask=attention_mask)
    outputs.logits.item()
    OUTPUT: 4.033723831176758
    
  10. 以下代码使用了负面句对,这意味着句子在语义上是不同的:

    s1,s2="The men are playing soccer.","A man is riding a motorcycle."
    encoding = tokenizer("hey how are you there","hey how are you", return_tensors='pt', padding=True, truncation=True, max_length=512)
    input_ids = encoding['input_ids'].to(device)
    attention_mask = encoding['attention_mask'].to(device)
    outputs = model(input_ids, attention_mask=attention_mask)
    outputs.logits.item()
    OUTPUT: 2.3579328060150146
    
  11. 最后,我们将保存模型,如下所示:

    model_path = "sentence-pair-regression-model"
    trainer.save_model(model_path)
    tokenizer.save_pre-trained(model_path)
    

干得好!我们可以祝贺自己,因为我们成功完成了三项任务:情感分析、多类别分类和句对回归。

利用 run_glue.py 对模型进行微调

我们目前已经使用原生 PyTorch 和 Trainer 类从头设计了一个微调架构。HuggingFace 社区还提供了另一个强大的脚本,称为 run_glue.py,用于 GLUE 基准测试和 GLUE 类似的分类下游任务。这个脚本可以为我们处理和组织整个训练/验证过程。如果你想进行快速原型设计,应该使用这个脚本。它可以微调 HuggingFace hub 上的任何预训练模型。我们也可以用任何格式的自己的数据来提供给它。

请转到以下链接获取脚本并了解更多信息:github.com/huggingface/transformers/tree/master/examples

该脚本可以执行九种不同的 GLUE 任务。通过该脚本,我们可以做到目前为止我们使用Trainer类所做的一切。任务名称可以是以下 GLUE 任务之一:colasst2mrpcstsbqqpmnliqnlirtewnli

以下是微调模型的脚本方案:

export TASK_NAME= "My-Task-Name" 
python run_glue.py \  
 --model_name_or_path bert-base-cased \
 --task_name $TASK_NAME \
 --do_train \  --do_eval \
 --max_seq_length 128 \ 
 --per_device_train_batch_size 32 \
 --learning_rate 2e-5 \  
 --num_train_epochs 3 \
 --output_dir /tmp/$TASK_NAME/

社区提供了另一个名为run_glue_no_trainer.py的脚本。与原始脚本的主要区别在于,这个无 Trainer 的脚本给了我们更多改变优化器选项或添加任何自定义的机会。

总结

在本章中,我们讨论了如何针对任何文本分类的下游任务对预训练模型进行微调。我们使用情感分析、多类别分类和句子对分类(具体而言,句子对回归)对模型进行了微调。我们使用了一个著名的 IMDb 数据集和我们自己的自定义数据集来训练模型。虽然我们利用了Trainer类来处理训练和微调过程的复杂性,但我们学会了如何使用原生库从头开始训练,以了解transformers库中的前向传播和反向传播。总而言之,我们讨论并进行了使用 Trainer 进行微调单句分类、使用原生 PyTorch 进行情感分类、单句多类别分类以及微调句子对回归。

在下一章中,我们将学习如何针对任何标记分类的下游任务(如词性标注或命名实体识别)对预训练模型进行微调。

第六章:为标记分类对语言模型进行微调

在本章中,我们将学习为标记分类对语言模型进行微调。本章探讨了诸如命名实体识别NER)、词性POS)标注和问答QA)等任务。我们将学习如何将特定语言模型微调用于此类任务。我们将更多地关注 BERT,而不是其他语言模型。您将学习如何使用 BERT 应用 POS、NER 和 QA。您将熟悉这些任务的理论细节,如它们各自的数据集以及如何执行它们。完成本章后,您将能够使用 Transformers 执行任何标记分类。

在本章中,我们将为以下任务微调 BERT:为 NER 和 POS 等标记分类问题微调 BERT,为 NER 问题微调语言模型,并将 QA 问题视为起始/终止标记分类。

本章将涵盖以下主题:

  • 介绍标记分类

  • 为 NER 进行语言模型微调

  • 使用标记分类进行问答

技术要求

我们将使用 Jupyter Notebook 运行我们的编码练习,并且需要安装 Python 3.6+ 和以下软件包:

  • sklearn

  • transformers 4.0+

  • 数据集

  • seqeval

所有带有编码练习的笔记本都将在以下 GitHub 链接中提供:github.com/PacktPublishing/Mastering-Transformers/tree/main/CH06

查看以下链接以查看实际代码视频:bit.ly/2UGMQP2

介绍标记分类

将标记序列中的每个标记分类的任务称为标记分类。该任务要求特定模型能够将每个标记分类到一个类别中。POS 和 NER 是这一标准中最知名的两个任务。然而,QA 也是另一个属于这一类别的重要 NLP 任务。我们将在以下章节讨论这三个任务的基础知识。

理解 NER

在标记分类类别中一个著名的任务是 NER - 将每个标记识别为实体或非实体,并识别每个检测到的实体的类型。例如,文本可以同时包含多个实体 - 人名、地名、组织名和其他类型的实体。以下文本是 NER 的明显示例:

乔治·华盛顿是美利坚合众国的总统之一。

乔治·华盛顿是一个人名,而美利坚合众国是一个地名。序列标注模型应该能够以标签的形式标记每个单词,每个标签都包含有关该标签的信息。BIO 的标签是标准 NER 任务中通用的标签。

以下表格是标签及其描述的列表:

表 1 – BIOS 标签及其描述表

表 1 – BIOS 标签及其描述表

从这个表格可以看出,B 表示标记的开始,I 表示标记的内部,而 O 则表示实体的外部。这就是为什么这种类型的标注被称为 BIO。例如,前面显示的句子可以使用 BIO 进行标注:

[B-PER|George] [I-PER|Washington] [O|is] [O|one] [O|the] [O|presidents] [O|of] [B-LOC|United] [I-LOC|States] [I-LOC|of] [I-LOC|America] [O|.]

因此,序列必须以 BIO 格式进行标记。一个样本数据集可以使用如下格式:

图 6.1 – CONLL2003 数据集

图 6.1 – CONLL2003 数据集

除了我们见过的 NER 标签外,该数据集还包含了 POS 标签

理解 POS 标记

POS 标记,或语法标记,是根据给定文本中的各自词的词性对其进行标注。举个简单的例子,在给定文本中,识别每个词的角色,如名词、形容词、副词和动词都被认为是词性标注。然而,从语言学角度来看,除了这四种角色外还有很多其他角色。

在 POS 标签的情况下,有各种变化,但是宾州树库的 POS 标签集是最著名的之一。下面的截图显示了这些角色的摘要和相应的描述:

图 6.2 – 宾州树库 POS 标签

图 6.2 – 宾州树库 POS 标签

POS 任务的数据集如 图 6.1 所示进行了标注。

这些标签的标注在特定的 NLP 应用中非常有用,是许多其他方法的基石之一。Transformers和许多先进模型在其复杂的结构中某种程度上能理解单词之间的关系。

理解 QA

QA 或阅读理解任务包括一组阅读理解文本,并相应地提出问题。这个范围内的示例数据集包括 SQUAD斯坦福问答数据集。该数据集由维基百科文本和关于它们提出的问题组成。答案以原始维基百科文本的片段形式给出。

以下截图显示了这个数据集的一个示例:

图 6.3 – SQUAD 数据集示例

图 6.3 – SQUAD 数据集示例

突出显示的红色部分是答案,每个问题的重要部分用蓝色突出显示。要求一个良好的 NLP 模型按照问题对文本进行分割,这种分割可以通过序列标注的形式进行。模型会将答案的开始和结束部分标记为答案的起始和结束部分。

到目前为止,你已经学会了现代 NLP 序列标注任务的基础知识,如 QA、NER 和 POS。在接下来的部分,你将学习如何对这些特定任务进行 BERT 微调,并使用 datasets 库中相关的数据集。

为 NER 微调语言模型

在本节中,我们将学习如何为 NER 任务微调 BERT。我们首先从 datasets 库开始,并加载 conll2003 数据集。

数据集卡片可在 huggingface.co/datasets/conll2003 上访问。以下截图显示了来自 HuggingFace 网站的此模型卡片:

图 6.4 – 来自 HuggingFace 的 CONLL2003 数据集卡片

图 6.4 – 来自 HuggingFace 的 CONLL2003 数据集卡片

从此截图中可以看出,模型是在此数据集上进行训练的,目前可用,并在右侧面板中列出。但是,还有关于数据集的描述,例如其大小和特征:

  1. 要加载数据集,使用以下命令:

    import datasets
    conll2003 = datasets.load_dataset("conll2003")
    

    将出现一个下载进度条,下载和缓存完成后,数据集将准备好供使用。以下截图显示了进度条:

    图 6.5 – 下载和准备数据集

    图 6.5 – 下载和准备数据集

  2. 您可以通过使用以下命令访问训练样本轻松地检查数据集:

    >>> conll2003["train"][0]
    

    下图显示了结果:

    图 6.6 – 从 datasets 库获取的 CONLL2003 训练样本

    图 6.6 – 从 datasets 库获取的 CONLL2003 训练样本

  3. 前述截图显示了 POS 和 NER 的相应标签。我们将仅使用此部分的 NER 标签。您可以使用以下命令获取此数据集中可用的 NER 标签:

    >>> conll2003["train"].features["ner_tags"]
    
  4. 结果也显示在 图 6.7 中。所有 BIO 标签都显示在此处,共有九个标签:

    >>> Sequence(feature=ClassLabel(num_classes=9, names=['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC'], names_file=None, id=None), length=-1, id=None)
    
  5. 下一步是加载 BERT 分词器:

    from transformers import BertTokenizerFast
    tokenizer = BertTokenizerFast.from_pretrained("bert-base-uncased")
    
  6. tokenizer 类也可以处理基于空格的分词句子。我们需要启用我们的分词器以处理基于空格的分词句子,因为命名实体识别任务为每个标记有一个基于标记的标签。在这个任务中,标记通常是基于空格分词的单词,而不是 BPE 或任何其他分词器的标记。根据所说的内容,让我们看看 tokenizer 如何与基于空格的分词句子一起使用:

    >>> tokenizer(["Oh","this","sentence","is","tokenized","and", "splitted","by","spaces"], is_split_into_words=True)
    

    正如您所见,仅需将 is_split_into_words 设置为 True,问题就解决了。

  7. 在使用数据进行训练之前,必须对数据进行预处理。为此,我们必须使用以下函数并将其映射到整个数据集中:

    def tokenize_and_align_labels(examples):
        tokenized_inputs = tokenizer(examples["tokens"], 
               truncation=True, is_split_into_words=True)
        labels = []
        for i, label in enumerate(examples["ner_tags"]):
            word_ids = \   
             tokenized_inputs.word_ids(batch_index=i)
            previous_word_idx = None
            label_ids = []
            for word_idx in word_ids:
                if word_idx is None:
                    label_ids.append(-100)
                elif word_idx != previous_word_idx:
                     label_ids.append(label[word_idx])
                else:
                     label_ids.append(label[word_idx] if label_all_tokens else -100)
                previous_word_idx = word_idx
            labels.append(label_ids)
        tokenized_inputs["labels"] = labels
        return tokenized_inputs
    
  8. 此函数将确保我们的标记和标签正确对齐。此对齐是必需的,因为标记是分割成片段的,但单词必须是一个整体。要测试并查看此函数的工作原理,您可以将单个样本提供给它并运行它:

    q = tokenize_and_align_labels(conll2003['train'][4:5])
    print(q)
    

    结果如下所示:

    >>> {'input_ids': [[101, 2762, 1005, 1055, 4387, 2000, 1996, 2647, 2586, 1005, 1055, 15651, 2837, 14121, 1062, 9328, 5804, 2056, 2006, 9317, 10390, 2323, 4965, 8351, 4168, 4017, 2013, 3032, 2060, 2084, 3725, 2127, 1996, 4045, 6040, 2001, 24509, 1012, 102]], 'token_type_ids': [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], 'labels': [[-100, 5, 0, -100, 0, 0, 0, 3, 4, 0, -100, 0, 0, 1, 2, -100, -100, 0, 0, 0, 0, 0, 0, 0, -100, -100, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, -100]]}
    
  9. 但是这个结果是不可读的,所以您可以运行以下代码以获得可读版本:

    for token, label in zip(tokenizer.convert_ids_to_tokens(q["input_ids"][0]),q["labels"][0]):
        print(f"{token:_<40} {label}")
    

    结果如下所示:

    图 6.7 – tokenize 和 align 函数的结果

    图 6.7 – tokenize 和 align 函数的结果

  10. 这个函数与数据集的映射可以通过datasets库的map函数完成:

    >>> tokenized_datasets = \ conll2003.map(tokenize_and_align_labels, batched=True)
    
  11. 接下来,需要加载具有相应标签数量的 BERT 模型:

    from transformers import\ AutoModelForTokenClassification
    model = AutoModelForTokenClassification.from_pretrained("bert-base-uncased", num_labels=9)
    
  12. 模型将被加载并准备好进行训练。在接下来的步骤中,我们必须准备好训练器和训练参数:

    from transformers import TrainingArguments, Trainer
    args = TrainingArguments(
    "test-ner",
    evaluation_strategy = "epoch",
    learning_rate=2e-5,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=16,
    num_train_epochs=3,
    weight_decay=0.01,
    )
    
  13. 需要准备数据收集器。它将在训练数据集上应用批量操作,以使用更少的内存并执行更快。您可以像下面这样做:

    from transformers import \ DataCollatorForTokenClassification
    data_collator = \ DataCollatorForTokenClassification(tokenizer)
    
  14. 为了能够评估模型的性能,在 HuggingFace 的datasets库中有许多任务的许多指标可用。我们将使用用于 NER 的序列评估指标。seqeval 是一个用于评估序列标记算法和模型的良好 Python 框架。需要安装seqeval库:

    pip install seqeval
    
  15. 之后,您可以加载指标:

    >>> metric = datasets.load_metric("seqeval")
    
  16. 通过以下代码,很容易看出指标是如何工作的:

    example = conll2003['train'][0] 
    label_list = \ conll2003["train"].features["ner_tags"].feature.names
    labels = [label_list[i] for i in example["ner_tags"]]
    metric.compute(predictions=[labels], references=[labels])
    

    结果如下:

    图 6.8 – seqeval 指标的输出

    图 6.8 – seqeval 指标的输出

    对于样本输入,计算各种指标,如准确率、F1 分数、精确度和召回率。

  17. 以下函数用于计算指标:

    import numpy as np def compute_metrics(p):
        predictions, labels = p
        predictions = np.argmax(predictions, axis=2)
        true_predictions = [
            [label_list[p] for (p, l) in zip(prediction, label) if l != -100]
            for prediction, label in zip(predictions, labels)    ]
        true_labels = [
        [label_list[l] for (p, l) in zip(prediction, label) if l != -100]
           for prediction, label in zip(predictions, labels)
       ]
       results = \ 
           metric.compute(predictions=true_predictions,  
           references=true_labels)
       return {
       "precision": results["overall_precision"],
       "recall": results["overall_recall"],
       "f1": results["overall_f1"],
      "accuracy": results["overall_accuracy"],
      }
    
  18. 最后一步是制作训练器并相应地对其进行训练:

    trainer = Trainer(
        model,
        args,
       train_dataset=tokenized_datasets["train"],
       eval_dataset=tokenized_datasets["validation"],
       data_collator=data_collator,
       tokenizer=tokenizer,
       compute_metrics=compute_metrics
    )
    trainer.train()
    
  19. 运行trainertrain函数后,结果如下所示:图 6.9-运行 train 后的 Trainer 结果

    图 6.9-运行 train 后的 Trainer 结果

  20. 在训练后,必须保存模型和分词器:

    model.save_pretrained("ner_model")
    tokenizer.save_pretrained("tokenizer")
    
  21. 如果您希望使用管道(pipeline)使用模型,则必须读取配置文件,并根据label_list对象中使用的标签正确分配label2idid2label:

    id2label = {
    str(i): label for i,label in enumerate(label_list)
    }
    label2id = {
    label: str(i) for i,label in enumerate(label_list)
    }
    import json
    config = json.load(open("ner_model/config.json"))
    config["id2label"] = id2label
    config["label2id"] = label2id
    json.dump(config, open("ner_model/config.json","w"))
    
  22. 之后,您可以像以下示例一样轻松使用模型:

    from transformers import pipeline
    model = \ AutoModelForTokenClassification.from_pretrained("ner_model")
    nlp = \
    pipeline("ner", model=mmodel, tokenizer=tokenizer)
    example = "I live in Istanbul"
    ner_results = nlp(example)
    print(ner_results)
    

    然后结果将如下所示:

    [{'entity': 'B-LOC', 'score': 0.9983942, 'index': 4, 'word': 'istanbul', 'start': 10, 'end': 18}] 
    

到目前为止,您已经学会了如何使用 BERT 应用 POS。您了解了如何使用 Transformers 训练自己的 POS 标注模型,并对模型进行了测试。在接下来的部分,我们将专注于 QA。

使用令牌分类进行问答

一个QA问题通常被定义为一个 NLP 问题,给定一个文本和一个问题,需要 AI 返回一个答案。通常,这个答案可以在原始文本中找到,但对于这个问题存在不同的方法。在视觉问答VQA)的情况下,问题涉及的是视觉实体或视觉概念,而不是文本,但问题本身是以文本形式呈现的。

一些 VQA 的示例如下:

图 6.10 – VQA 示例

图 6.10 – VQA 示例

VQA 中大多数模型都是多模态模型,可以理解视觉上下文以及问题,并能正确生成答案。然而,单模全文本 QA 或者仅 QA 是基于文本上下文和文本问题以及相应的文本答案:

  1. SQUAD 是问答领域中最知名的数据集之一。要查看 SQUAD 的示例并对其进行检查,您可以使用以下代码:

    from pprint import pprint
    from datasets import load_dataset
    squad = load_dataset("squad")
    for item in squad["train"][1].items():
        print(item[0])
        pprint(item[1])
        print("="*20)
    

    以下是结果:

    answers
    {'answer_start': [188], 'text': ['a copper statue of Christ']}
    ====================
    Context
    ('Architecturally, the school has a Catholic character. Atop the Main ' "Building's gold dome is a golden statue of the Virgin Mary. Immediately in " 'front of the Main Building and facing it, is a copper statue of Christ with ' 'arms upraised with the legend "Venite Ad Me Omnes". Next to the Main ' 'Building is the Basilica of the Sacred Heart. Immediately behind the ' 'basilica is the Grotto, a Marian place of prayer and reflection. It is a ' 'replica of the grotto at Lourdes, France where the Virgin Mary reputedly ' 'appeared to Saint Bernadette Soubirous in 1858\. At the end of the main drive ' '(and in a direct line that connects through 3 statues and the Gold Dome), is ' 'a simple, modern stone statue of Mary.')
    ====================
    Id
    '5733be284776f4190066117f'
    ====================
    Question
    'What is in front of the Notre Dame Main Building?'
    ====================
    Title
    'University_of_Notre_Dame'
    ====================
    

    但是,SQUAD 数据集还有第 2 版,其中有更多的训练样本,并且强烈建议使用它。为了全面了解如何为 QA 问题训练模型的可能性,我们将重点放在解决这个问题的当前部分上。

  2. 要开始,使用以下代码加载 SQUAD 第 2 版:

    from datasets import load_dataset
    squad = load_dataset("squad_v2")
    
  3. 在加载 SQUAD 数据集之后,您可以通过使用以下代码查看此数据集的详细信息:

    >>> squad
    

    结果如下:

    图 6.11 – SQUAD 数据集(第 2 版)详细信息

    图 6.11 – SQUAD 数据集(第 2 版)详细信息

    SQUAD 数据集的详细信息将显示在图 6.11中。正如您所看到的,有超过 130,000 个训练样本和超过 11,000 个验证样本。

  4. 就像我们对 NER 所做的那样,我们必须预处理数据,使其具有适合模型使用的正确形式。为此,您必须首先加载您的分词器,只要您使用预训练模型并希望为 QA 问题进行微调:

    from transformers import AutoTokenizer
    model = "distilbert-base-uncased"
    tokenizer = AutoTokenizer.from_pretrained(model)
    

    正如您所看到的,我们将使用distillBERT模型。

    根据我们的 SQUAD 示例,我们需要向模型提供不止一个文本,一个用于问题,一个用于上下文。因此,我们的分词器需要将这两个文本并排放在一起,并使用特殊的[SEP]标记将它们分开,因为distillBERT是基于 BERT 的模型。

    在 QA 范围内还有另一个问题,即上下文的大小。上下文的大小可以比模型输入大小长,但我们不能将其缩减到模型接受的大小。对于某些问题,我们可能可以这样做,但在 QA 中,答案可能在被截断的部分中。我们将向您展示一个示例,展示我们如何使用文档步幅来解决此问题。

  5. 以下是一个示例,展示了如何使用tokenizer

    max_length = 384
    doc_stride = 128
    example = squad["train"][173]
    tokenized_example = tokenizer(
    example["question"],
    example["context"],
    max_length=max_length,
    truncation="only_second",
    return_overflowing_tokens=True,
    stride=doc_stride
    )
    
  6. 步幅是用于返回第二部分的窗口的文档步幅,而return_overflowing_tokens标志向模型提供有关是否应返回额外标记的信息。tokenized_example的结果不止一个标记化输出,而是有两个输入 ID。在以下,您可以看到结果:

    >>> len(tokenized_example['input_ids'])
    >>> 2
    
  7. 因此,您可以通过运行以下for循环看到完整的结果:

    for input_ids in tokenized_example["input_ids"][:2]:
        print(tokenizer.decode(input_ids))
        print("-"*50)
    

    结果如下:

    [CLS] beyonce got married in 2008 to whom? [SEP] on april 4, 2008, beyonce married jay z. she publicly revealed their marriage in a video montage at the listening party for her third studio album, i am... sasha fierce, in manhattan's sony club on october 22, 2008\. i am... sasha fierce was released on november 18, 2008 in the united states. the album formally introduces beyonce's alter ego sasha fierce, conceived during the making of her 2003 single " crazy in love ", selling 482, 000 copies in its first week, debuting atop the billboard 200, and giving beyonce her third consecutive number - one album in the us. the album featured the number - one song " single ladies ( put a ring on it ) " and the top - five songs " if i were a boy " and " halo ". achieving the accomplishment of becoming her longest - running hot 100 single in her career, " halo "'s success in the us helped beyonce attain more top - ten singles on the list than any other woman during the 2000s. it also included the successful " sweet dreams ", and singles " diva ", " ego ", " broken - hearted girl " and " video phone ". the music video for " single ladies " has been parodied and imitated around the world, spawning the " first major dance craze " of the internet age according to the toronto star. the video has won several awards, including best video at the 2009 mtv europe music awards, the 2009 scottish mobo awards, and the 2009 bet awards. at the 2009 mtv video music awards, the video was nominated for nine awards, ultimately winning three including video of the year. its failure to win the best female video category, which went to american country pop singer taylor swift's " you belong with me ", led to kanye west interrupting the ceremony and beyonce [SEP]
    --------------------------------------------------
    [CLS] beyonce got married in 2008 to whom? [SEP] single ladies " has been parodied and imitated around the world, spawning the " first major dance craze " of the internet age according to the toronto star. the video has won several awards, including best video at the 2009 mtv europe music awards, the 2009 scottish mobo awards, and the 2009 bet awards. at the 2009 mtv video music awards, the video was nominated for nine awards, ultimately winning three including video of the year. its failure to win the best female video category, which went to american country pop singer taylor swift's " you belong with me ", led to kanye west interrupting the ceremony and beyonce improvising a re - presentation of swift's award during her own acceptance speech. in march 2009, beyonce embarked on the i am... world tour, her second headlining worldwide concert tour, consisting of 108 shows, grossing $ 119\. 5 million. [SEP]
    --------------------------------------------------
    

    正如您可以从前面的输出中看到的那样,使用 128 个标记的窗口,剩余的上下文再次复制到了第二个输出的输入 ID 中。

    另一个问题是结束跨度,在数据集中不可用,而是给出了答案的开始跨度或开始字符。很容易找到答案的长度并将其添加到起始跨度,这将自动产生结束跨度。

  8. 现在,我们已经了解了该数据集的所有细节以及如何处理它们,我们可以轻松地将它们组合在一起,制作一个预处理函数(链接:github.com/huggingface/transformers/blob/master/examples/pytorch/question-answering/run_qa.py):

    def prepare_train_features(examples):
        # tokenize examples
        tokenized_examples = tokenizer(
            examples["question" if pad_on_right else "context"],
            examples["context" if pad_on_right else "question"],
            truncation="only_second" if pad_on_right else "only_first",
            max_length=max_length,
            stride=doc_stride,
            return_overflowing_tokens=True,
            return_offsets_mapping=True,
            padding="max_length",
        )
        # map from a feature to its example
        sample_mapping = \ tokenized_examples.pop("overflow_to_sample_mapping")
        offset_mapping = \ tokenized_examples.pop("offset_mapping")
        tokenized_examples["start_positions"] = []
        tokenized_examples["end_positions"] = []
        # label impossible answers with CLS
        # start and end token are the answers for each one
        for i, offsets in enumerate(offset_mapping):
            input_ids = tokenized_examples["input_ids"][i]
            cls_index = \ input_ids.index(tokenizer.cls_token_id)
            sequence_ids = \ tokenized_examples.sequence_ids(i)
            sample_index = sample_mapping[i]
            answers = examples["answers"][sample_index]
            if len(answers["answer_start"]) == 0:
                tokenized_examples["start_positions"].\ append(cls_index)
                tokenized_examples["end_positions"].\ append(cls_index)
            else:
                start_char = answers["answer_start"][0]
                end_char = \                        
                   start_char + len(answers["text"][0])
                token_start_index = 0
                while sequence_ids[token_start_index] != / (1 if pad_on_right else 0):
                    token_start_index += 1
                token_end_index = len(input_ids) - 1
                while sequence_ids[token_end_index] != (1 if pad_on_right else 0):
                    token_end_index -= 1
                if not (offsets[token_start_index][0] <= start_char and offsets[token_end_index][1] >= end_char):
                    tokenized_examples["start_positions"].append(cls_index)
                    tokenized_examples["end_positions"].append(cls_index)
                else:
                    while token_start_index < len(offsets) and offsets[token_start_index][0] <= start_char:
                        token_start_index += 1
                    tokenized_examples["start_positions"].append(token_start_index - 1)
                    while offsets[token_end_index][1] >= end_char:
                        token_end_index -= 1
                    tokenized_examples["end_positions"].append(token_end_index + 1)
        return tokenized_examples
    
  9. 将此函数映射到数据集将应用所有所需的更改:

    >>> tokenized_datasets = squad.map(prepare_train_features, batched=True, remove_columns=squad["train"].column_names)
    
  10. 就像其他示例一样,您现在可以加载预训练的模型进行微调:

    from transformers import AutoModelForQuestionAnswering, TrainingArguments, Trainer
    model = AutoModelForQuestionAnswering.from_pretrained(model)
    
  11. 下一步是创建训练参数:

    args = TrainingArguments(
    "test-squad",
    evaluation_strategy = "epoch",
    learning_rate=2e-5,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=16,
    num_train_epochs=3,
    weight_decay=0.01,
    )
    
  12. 如果我们不打算使用数据收集器,我们将为模型训练器提供一个默认的数据收集器:

    from transformers import default_data_collator
    data_collator = default_data_collator
    
  13. 现在,一切准备就绪,可以制作训练器:

    trainer = Trainer(
    model,
    args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    data_collator=data_collator,
    tokenizer=tokenizer,
    )
    
  14. 训练器可以与train函数一起使用:

    trainer.train()
    

    结果将如下所示:

    图 6.12 - 训练结果

    图 6.12 - 训练结果

    如您所见,该模型使用三个 epochs 进行训练,并报告验证和训练中的损失输出。

  15. 像其他模型一样,您可以轻松地使用以下函数保存此模型:

    >>> trainer.save_model("distillBERT_SQUAD")
    

    如果您想使用您保存的模型或任何在 QA 上训练的其他模型,则transformers库提供了一个易于使用和实施的管道,无需额外努力。

  16. 通过使用这个管道功能,您可以使用任何模型。以下是使用 QA 管道的示例:

    from transformers import pipeline
    qa_model = pipeline('question-answering', model='distilbert-base-cased-distilled-squad', tokenizer='distilbert-base-cased')
    

    管道只需要两个输入,即模型和 tokenizer,即可使模型准备就绪。但是,在给定示例中,您还需要给它一个管道类型,即 QA。

  17. 下一步是给它提供所需的输入,即contextquestion

    >>> question = squad["validation"][0]["question"]
    >>> context = squad["validation"][0]["context"]
    The question and the context can be seen by using following code:
    >>> print("Question:")
    >>> print(question)
    >>> print("Context:")
    >>> print(context)
    Question:
    In what country is Normandy located?
    Context:
    ('The Normans (Norman: Nourmands; French: Normands; Latin: Normanni) were the ' 'people who in the 10th and 11th centuries gave their name to Normandy, a ' 'region in France. They were descended from Norse ("Norman" comes from ' '"Norseman") raiders and pirates from Denmark, Iceland and Norway who, under ' 'their leader Rollo, agreed to swear fealty to King Charles III of West ' 'Francia. Through generations of assimilation and mixing with the native ' 'Frankish and Roman-Gaulish populations, their descendants would gradually ' 'merge with the Carolingian-based cultures of West Francia. The distinct ' 'cultural and ethnic identity of the Normans emerged initially in the first ' 'half of the 10th century, and it continued to evolve over the succeeding ' 'centuries.')
    
  18. 该模型可以使用以下示例:

    >>> qa_model(question=question, context=context)
    

    结果如下所示:

    {'answer': 'France', 'score': 0.9889379143714905, 'start': 159, 'end': 165,}
    

到目前为止,您已经学会了如何在想要的数据集上进行训练。您还学会了如何使用管道使用训练好的模型。

概述

在本章中,我们讨论了如何对预训练模型进行微调以适用于任何令牌分类任务。我们探讨了在 NER 和 QA 问题上微调模型的方法。使用预训练和微调后的模型在特定任务中使用管道进行详细说明,并给出了示例。我们还了解了这两个任务的各种预处理步骤。保存在特定任务上微调的预训练模型是本章的另一个重点学习内容。我们还看到了如何将具有比模型输入更长序列大小的 QA 等任务的有限输入大小的模型进行训练的可能性。在本章中,更高效地使用标记器以具有文档间距和文档步幅的文档分割也是另一个重要内容。

在下一章中,我们将讨论使用 Transformer 进行文本表示的方法。通过学习本章,您将学习如何执行零/少量样本学习和语义文本聚类。

posted @ 2024-04-30 11:40  绝不原创的飞龙  阅读(65)  评论(0编辑  收藏  举报