当前位置: 首页 > news >正文

西安做网站需要多少钱菏泽住房和城乡建设厅网站

西安做网站需要多少钱,菏泽住房和城乡建设厅网站,推广比较好的网站,企业网站可以做商城类原文#xff1a;Natural Language Processing with Transformers 译者#xff1a;飞龙 协议#xff1a;CC BY-NC-SA 4.0 第五章#xff1a;文本生成 基于 Transformer 的语言模型最令人不安的特点之一是它们生成的文本几乎无法与人类写的文本区分开。一个著名的例子是 Ope… 原文Natural Language Processing with Transformers 译者飞龙 协议CC BY-NC-SA 4.0 第五章文本生成 基于 Transformer 的语言模型最令人不安的特点之一是它们生成的文本几乎无法与人类写的文本区分开。一个著名的例子是 OpenAI 的 GPT-2当给出以下提示时¹ 在一个令人震惊的发现中科学家们发现了一群生活在安第斯山脉偏远、以前未被探索过的山谷中的独角兽。更令研究人员感到惊讶的是这些独角兽竟然讲着一口流利的英语。 能够生成一篇关于说话的独角兽的引人入胜的新闻文章 科学家们根据它们独特的角将这一种群命名为奥维德独角兽。这种四角、银白色的独角兽以前对科学界来说是未知的。现在经过将近两个世纪的时间这一奇怪现象的起因终于得到了解决。拉巴斯大学的进化生物学家豪尔赫·佩雷斯博士和几个同伴在探索安第斯山脉时发现了一个没有其他动物或人类的小山谷。佩雷斯注意到山谷中有一个看起来像是天然喷泉的地方周围有两座岩石和银色的雪峰。佩雷斯和其他人随后进一步探索了山谷。“当我们到达一个山峰的顶部时水看起来是蓝色的上面有一些水晶”佩雷斯说。佩雷斯和他的朋友们惊讶地看到了独角兽群。这些生物可以从空中看到而不需要移动太多就能看到它们——它们离得很近可以触摸到它们的角。在检查这些奇怪的生物时科学家们发现这些生物还能说一些相当规范的英语…… 这个例子之所以如此引人注目是因为它是在没有任何明确监督的情况下生成的通过简单地学习预测数百万网页文本中的下一个单词GPT-2 及其更强大的后代如 GPT-3 能够获得广泛的技能和模式识别能力可以通过不同类型的输入提示来激活。图 5-1 显示了语言模型有时在预训练期间会接触到需要仅基于上下文预测下一个标记的任务序列比如加法、单词重组和翻译。这使它们能够在微调期间或如果模型足够大在推断期间有效地转移这些知识。这些任务并不是提前选择的而是在用于训练百亿参数语言模型的庞大语料库中自然发生的。 图 5-1。在预训练期间语言模型会接触到可以在推断期间进行调整的任务序列由汤姆·布朗提供 Transformer 生成逼真文本的能力已经导致了各种各样的应用比如InferKit、Write With Transformer、AI Dungeon以及像Google 的 Meena这样的对话代理甚至可以讲一些陈腐的笑话就像图 5-2 中所示的那样² 图 5-2。左边是米娜右边是一个人米娜正在讲一个陈腐的笑话由丹尼尔·阿迪瓦达纳和 Thang Luong 提供 在本章中我们将使用 GPT-2 来说明语言模型的文本生成工作原理并探讨不同的解码策略如何影响生成的文本。 生成连贯文本的挑战 到目前为止在本书中我们已经专注于通过预训练和监督微调的组合来解决 NLP 任务。正如我们所看到的对于诸如序列或标记分类的任务特定头部生成预测是相当简单的模型产生一些 logits我们要么取最大值得到预测类要么应用 softmax 函数以获得每个类的预测概率。相比之下将模型的概率输出转换为文本需要解码方法这引入了一些对文本生成独特的挑战 解码是迭代进行的因此涉及的计算量比简单地通过模型的前向传递一次传递输入要多得多。 生成的文本的质量和多样性取决于解码方法和相关超参数的选择。 为了理解这个解码过程是如何工作的让我们从检查 GPT-2 是如何预训练和随后应用于生成文本开始。 与其他自回归或因果语言模型一样GPT-2 被预训练来估计在给定一些初始提示或上下文序列 x 1 , x 2 , … x k的情况下估计文本中出现的一系列标记 y 1 , y 2 , … y t的概率P ( | )。由于直接获取足够的训练数据来估计P ( | )是不切实际的因此通常使用概率的链式法则将其分解为条件概率的乘积 P ( y 1 , … , y t | ) ∏ t1 N P ( y t | y t , ) 其中y t是序列y 1 , … , y t-1的简写符号。我们从这些条件概率中得到的直觉是自回归语言建模等于在给定句子中的前面单词的情况下预测每个单词这正是前面方程右边的概率描述的内容。请注意这种预训练目标与 BERT 的目标非常不同BERT 利用过去和未来上下文来预测掩码标记。 到目前为止您可能已经猜到了我们如何将下一个标记预测任务调整为生成任意长度的文本序列。如图 5-3 所示我们从一个提示开始比如“Transformer 是”然后使用模型预测下一个标记。一旦确定了下一个标记我们将其附加到提示上然后使用新的输入序列生成另一个标记。我们一直这样做直到达到特殊的序列结束标记或预定义的最大长度。 图 5-3。通过在每个步骤向输入添加一个新单词来从输入序列生成文本 注意 由于输出序列取决于输入提示的选择这种类型的文本生成通常被称为条件文本生成。 这个过程的核心是一个解码方法它确定在每个时间步骤选择哪个标记。由于语言模型头在每个步骤的词汇表中为每个标记生成一个 logit z t,i我们可以通过使用 softmax 得到下一个可能标记w i的概率分布 P ( y t w i | y t , ) softmax ( z t,i ) 大多数解码方法的目标是通过选择 ^来搜索最可能的整体序列使得 ^ argmax P ( | ) 直接找到 ^将涉及评估语言模型的每个可能序列。由于不存在可以在合理时间内执行此操作的算法因此我们依赖于近似。在本章中我们将探讨其中一些近似并逐渐构建更聪明和更复杂的算法这些算法可以用于生成高质量的文本。 贪婪搜索解码 从模型的连续输出中获得离散标记的最简单解码方法是在每个时间步骤贪婪地选择具有最高概率的标记 y ^ t argmax y t P ( y t | y t , ) 为了看看贪婪搜索是如何工作的让我们从加载带有语言建模头的 GPT-2 的 15 亿参数版本开始³ import torch from transformers import AutoTokenizer, AutoModelForCausalLMdevice cuda if torch.cuda.is_available() else cpu model_name gpt2-xl tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained(model_name).to(device)现在让我们生成一些文本虽然 Transformers 为 GPT-2 这样的自回归模型提供了 generate() 函数但我们将自己实现这种解码方法以了解底层发生了什么。为了热身我们将采用图 5-3 中显示的相同的迭代方法我们将使用“Transformers are the”作为输入提示并运行八个时间步的解码。在每个时间步我们挑选出模型对提示中最后一个标记的 logits并用 softmax 包装它们以获得概率分布。然后我们选择具有最高概率的下一个标记将其添加到输入序列中然后再次运行该过程。以下代码完成了这项工作并且还存储了每个时间步的五个最有可能的标记以便我们可以可视化替代方案 import pandas as pdinput_txt Transformers are the input_ids tokenizer(input_txt, return_tensorspt)[input_ids].to(device) iterations [] n_steps 8 choices_per_step 5with torch.no_grad():for _ in range(n_steps):iteration dict()iteration[Input] tokenizer.decode(input_ids[0])output model(input_idsinput_ids)# Select logits of the first batch and the last token and apply softmaxnext_token_logits output.logits[0, -1, :]next_token_probs torch.softmax(next_token_logits, dim-1)sorted_ids torch.argsort(next_token_probs, dim-1, descendingTrue)# Store tokens with highest probabilitiesfor choice_idx in range(choices_per_step):token_id sorted_ids[choice_idx]token_prob next_token_probs[token_id].cpu().numpy()token_choice (f{tokenizer.decode(token_id)} ({100 * token_prob:.2f}%))iteration[fChoice {choice_idx1}] token_choice# Append predicted next token to inputinput_ids torch.cat([input_ids, sorted_ids[None, 0, None]], dim-1)iterations.append(iteration)pd.DataFrame(iterations)输入选择 1选择 2选择 3选择 4选择 50Transformers are themost (8.53%)only (4.96%)best (4.65%)Transformers (4.37%)ultimate (2.16%)1Transformers are the mostpopular (16.78%)powerful (5.37%)common (4.96%)famous (3.72%)successful (3.20%)2Transformers are the most populartoy (10.63%)toys (7.23%)Transformers (6.60%)of (5.46%)and (3.76%)3Transformers are the most popular toyline (34.38%)in (18.20%)of (11.71%)brand (6.10%)line (2.69%)4Transformers are the most popular toy linein (46.28%)of (15.09%), (4.94%)on (4.40%)ever (2.72%)5Transformers are the most popular toy line inthe (65.99%)history (12.42%)America (6.91%)Japan (2.44%)North (1.40%)6Transformers are the most popular toy line in theworld (69.26%)United (4.55%)history (4.29%)US (4.23%)U (2.30%)7Transformers are the most popular toy line in the world, (39.73%). (30.64%)and (9.87%)with (2.32%)today (1.74%) 通过这种简单的方法我们能够生成句子“Transformers are the most popular toy line in the world”。有趣的是这表明 GPT-2 已经内化了一些关于变形金刚媒体特许经营的知识这是由两家玩具公司孩之宝和 Takara Tomy创造的。我们还可以看到每一步的其他可能的延续这显示了文本生成的迭代性质。与其他任务如序列分类不同在那些任务中单次前向传递就足以生成预测而在文本生成中我们需要逐个解码输出标记。 实现贪婪搜索并不太难但我们希望使用来自 Transformers 的内置 generate() 函数来探索更复杂的解码方法。为了重现我们的简单示例让我们确保抽样被关闭默认情况下关闭除非您从加载检查点的特定模型配置中另有说明并指定 max_new_tokens 为新生成标记的数量 input_ids tokenizer(input_txt, return_tensorspt)[input_ids].to(device) output model.generate(input_ids, max_new_tokensn_steps, do_sampleFalse) print(tokenizer.decode(output[0]))Transformers are the most popular toy line in the world,现在让我们尝试一些更有趣的东西我们能否重现 OpenAI 的独角兽故事与之前一样我们将使用分词器对提示进行编码并为 max_length 指定一个较大的值以生成更长的文本序列。 max_length 128 input_txt In a shocking finding, scientist discovered \ a herd of unicorns living in a remote, previously unexplored \ valley, in the Andes Mountains. Even more surprising to the \ researchers was the fact that the unicorns spoke perfect English.\n\n input_ids tokenizer(input_txt, return_tensorspt)[input_ids].to(device) output_greedy model.generate(input_ids, max_lengthmax_length,do_sampleFalse) print(tokenizer.decode(output_greedy[0]))py In a shocking finding, scientist discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English. The researchers, from the University of California, Davis, and the University of Colorado, Boulder, were conducting a study on the Andean cloud forest, which is home to the rare species of cloud forest trees. The researchers were surprised to find that the unicorns were able to communicate with each other, and even with humans. The researchers were surprised to find that the unicorns were able 嗯前几句与 OpenAI 的示例有很大不同有趣的是涉及到不同的大学被认为是发现者我们还可以看到贪婪搜索解码的一个主要缺点它倾向于产生重复的输出序列在新闻文章中显然是不可取的。这是贪婪搜索算法的一个常见问题它可能无法给出最佳解决方案在解码的上下文中它可能会错过整体概率更高的词序列只是因为高概率的词恰好是由低概率的词前导的。幸运的是我们可以做得更好——让我们来看一种被称为*束搜索解码*的流行方法。###### 注意尽管贪婪搜索解码在需要多样性的文本生成任务中很少使用但它对于生成像算术这样需要确定性和事实正确的输出的短序列是有用的。⁴ 对于这些任务您可以通过提供格式为5 8 13 \n 7 2 9 \n 1 0 的几个以换行符分隔的示例来对 GPT-2 进行条件设置。 #束搜索解码束搜索不是在每一步解码时选择具有最高概率的标记而是跟踪前*b*个最有可能的下一个标记其中*b*被称为*束*或*部分假设*的数量。下一组束是通过考虑现有集合的所有可能的下一个标记扩展并选择*b*个最有可能的扩展来选择的。这个过程重复进行直到达到最大长度或 EOS 标记并且通过根据它们的对数概率对*b*束进行排名来选择最有可能的序列。束搜索的一个示例显示在图 5-4 中。![束搜索](https://gitcode.net/OpenDocCN/ibooker-dl-zh/-/raw/master/docs/nlp-tfm/img/nlpt_0504.png)###### 图 5-4。具有两个束的束搜索为什么我们要使用对数概率而不是概率本身来对序列进行评分计算序列的总体概率math alttextupper P left-parenthesis y 1 comma y 2 comma ellipsis comma y Subscript t Baseline vertical-bar bold x right-parenthesismrowmiP/mi mo(/mo msubmiy/mi mn1/mn/msub mo,/mo msubmiy/mi mn2/mn/msub mo,/mo mo.../mo mo,/mo msubmiy/mi mit/mi/msub mo|/mo mi/mi mo)/mo/mrow/math涉及计算条件概率math alttextupper P left-parenthesis y Subscript t Baseline vertical-bar y Subscript t Baseline comma bold x right-parenthesismrowmiP/mi mo(/mo msubmiy/mi mit/mi/msub mo|/mo msubmiy/mi mrowmo/momit/mi/mrow/msub mo,/mo mi/mi mo)/mo/mrow/math的*乘积*是一个原因。由于每个条件概率通常是在[ math alttext0 comma 1mrowmn0/mn mo,/mo mn1/mn/mrow/math ]范围内的一个小数取它们的乘积可能导致总体概率很容易下溢。这意味着计算机不能准确地表示计算结果。例如假设我们有一个包含math alttextt equals 1024mrowmit/mi mo/mo mn1024/mn/mrow/math个标记的序列并慷慨地假设每个标记的概率为 0.5。这个序列的总体概率是一个极小的数py 0.5 ** 10245.562684646268003e-309这导致了数值不稳定因为我们遇到了下溢。我们可以通过计算一个相关的术语即对数概率来避免这种情况。如果我们对联合和条件概率应用对数然后借助对数的乘法规则我们得到 log P ( y 1 , … y t | ) ∑ t1 N log P ( y t | y t , ) 换句话说我们之前看到的概率乘积变成了对数概率的总和这样就不太可能遇到数值不稳定性的问题。例如计算与之前相同示例的对数概率为 import numpy as npsum([np.log(0.5)] * 1024)-709.7827128933695这是一个我们可以轻松处理的数字这种方法对于更小的数字仍然有效。因为我们只想比较相对概率所以我们可以直接使用对数概率进行比较。 让我们计算并比较贪婪搜索和束搜索生成的文本的对数概率看看束搜索是否可以提高整体概率。由于 Transformers 模型返回给定输入标记的下一个标记的非归一化 logits我们首先需要对 logits 进行归一化以创建整个词汇表上每个标记的概率分布。然后我们需要选择仅出现在序列中的标记概率。以下函数实现了这些步骤 import torch.nn.functional as Fdef log_probs_from_logits(logits, labels):logp F.log_softmax(logits, dim-1)logp_label torch.gather(logp, 2, labels.unsqueeze(2)).squeeze(-1)return logp_label这给我们了单个标记的对数概率所以要得到序列的总对数概率我们只需要对每个标记的对数概率求和 def sequence_logprob(model, labels, input_len0):with torch.no_grad():output model(labels)log_probs log_probs_from_logits(output.logits[:, :-1, :], labels[:, 1:])seq_log_prob torch.sum(log_probs[:, input_len:])return seq_log_prob.cpu().numpy()请注意我们忽略输入序列的对数概率因为它们不是模型生成的。我们还可以看到对齐 logits 和标签的重要性因为模型预测下一个标记我们不会得到第一个标签的 logit并且我们不需要最后一个 logit因为我们没有它的真实标记。 让我们首先使用这些函数来计算 OpenAI 提示中贪婪解码器的序列对数概率 logp sequence_logprob(model, output_greedy, input_lenlen(input_ids[0])) print(tokenizer.decode(output_greedy[0])) print(f\nlog-prob: {logp:.2f})In a shocking finding, scientist discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English.The researchers, from the University of California, Davis, and the University of Colorado, Boulder, were conducting a study on the Andean cloud forest, which is home to the rare species of cloud forest trees.The researchers were surprised to find that the unicorns were able to communicate with each other, and even with humans.The researchers were surprised to find that the unicorns were ablelog-prob: -87.43现在让我们将其与使用束搜索生成的序列进行比较。要使用generate()函数激活束搜索我们只需要使用num_beams参数指定束的数量。我们选择的束数越多结果可能就越好然而生成过程变得更慢因为我们为每个束生成并行序列 output_beam model.generate(input_ids, max_lengthmax_length, num_beams5,do_sampleFalse) logp sequence_logprob(model, output_beam, input_lenlen(input_ids[0])) print(tokenizer.decode(output_beam[0])) print(f\nlog-prob: {logp:.2f})In a shocking finding, scientist discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English.The discovery of the unicorns was made by a team of scientists from the University of California, Santa Cruz, and the National Geographic Society.The scientists were conducting a study of the Andes Mountains when they discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect Englishlog-prob: -55.23我们可以看到使用束搜索得到的对数概率越高越好比简单的贪婪解码要好。然而我们也可以看到束搜索也存在重复的文本问题。解决这个问题的一种方法是使用no_repeat_ngram_size参数来施加n-gram 惩罚该参数跟踪已经看到的n-gram并且如果生成的下一个标记会产生先前看到的n-gram则将其概率设置为零 output_beam model.generate(input_ids, max_lengthmax_length, num_beams5,do_sampleFalse, no_repeat_ngram_size2) logp sequence_logprob(model, output_beam, input_lenlen(input_ids[0])) print(tokenizer.decode(output_beam[0])) print(f\nlog-prob: {logp:.2f})In a shocking finding, scientist discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English.The discovery was made by a team of scientists from the University of California, Santa Cruz, and the National Geographic Society.According to a press release, the scientists were conducting a survey of the area when they came across the herd. They were surprised to find that they were able to converse with the animals in English, even though they had never seen a unicorn in person before. The researchers werelog-prob: -93.12这并不太糟糕我们成功地停止了重复并且可以看到尽管产生了较低的分数文本仍然连贯。带有n-gram 惩罚的波束搜索是一种很好的方法可以在关注高概率标记使用波束搜索的同时减少重复使用n-gram 惩罚在摘要或机器翻译等需要事实正确性的应用中通常使用。当事实正确性不如生成输出的多样性重要时例如在开放域闲聊或故事生成中另一种减少重复并提高多样性的替代方法是使用抽样。让我们通过检查一些最常见的抽样方法来完成我们对文本生成的探索。 抽样方法 最简单的抽样方法是在每个时间步从模型输出的概率分布中随机抽样整个词汇表 P ( y t w i | y t , ) softmax ( z t,i ) exp(z t,i ) ∑ j1 |V| exp(z t,j ) 其中| V |表示词汇表的基数。我们可以通过添加一个温度参数T来控制输出的多样性该参数在进行 softmax 之前重新调整 logits P ( y t w i | y t , ) exp(z t,i /T) ∑ j1 |V| exp(z t,j /T) 通过调整T我们可以控制概率分布的形状。⁵ 当T ≪ 1时分布在原点附近呈峰值罕见的标记被抑制。另一方面当T ≫ 1时分布变得平坦每个标记变得同样可能。温度对标记概率的影响在图 5-5 中显示。 图 5-5. 三个选定温度下随机生成的标记概率分布 为了看看我们如何使用温度来影响生成的文本让我们通过在generate()函数中设置temperature参数来使用T 2进行抽样我们将在下一节解释top_k参数的含义 output_temp model.generate(input_ids, max_lengthmax_length, do_sampleTrue,temperature2.0, top_k0) print(tokenizer.decode(output_temp[0]))In a shocking finding, scientist discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English.While the station aren protagonist receive Pengala nostalgiates tidbitRegarding Jenny loclonju AgreementCON irrational rite Continent seaf A jer Turner Dorbecue WILL Pumpkin mere Thatvernuildagain YoAniamond disse * Runewitingkusstemprop});b zo coachinginventorymodules deflation press Vaticanpres Wrestling chargesThingsctureddong Ty physician PET KimBi66 graz Oz at aff da temporou MD6 radi iter我们可以清楚地看到高温大多产生了胡言乱语通过突出罕见的标记我们导致模型创建了奇怪的语法和相当多的虚构词让我们看看如果我们降低温度会发生什么 output_temp model.generate(input_ids, max_lengthmax_length, do_sampleTrue,temperature0.5, top_k0) print(tokenizer.decode(output_temp[0]))In a shocking finding, scientist discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English.The scientists were searching for the source of the mysterious sound, which was making the animals laugh and cry.The unicorns were living in a remote valley in the Andes mountainsWhen we first heard the noise of the animals, we thought it was a lion or a tiger, said Luis Guzman, a researcher from the University of Buenos Aires, Argentina.But when这显然更加连贯甚至包括了另一所大学被认为是发现的引用我们可以从温度中得出的主要教训是它允许我们控制样本的质量但在连贯性低温和多样性高温之间总是存在一个权衡需要根据手头的使用情况进行调整。 调整连贯性和多样性之间的权衡的另一种方法是截断词汇的分布。这使我们能够通过温度自由调整多样性但在一个更有限的范围内排除了在上下文中太奇怪的单词即低概率单词。有两种主要方法可以做到这一点top-k和核或 top-p抽样。让我们来看看。 Top-k 和核抽样 Top-k和核top-p抽样是使用温度的两种流行的替代方法或扩展。在这两种情况下基本思想是限制我们可以在每个时间步骤从中抽样的可能标记的数量。为了了解这是如何工作的让我们首先可视化模型在T 1时的累积概率分布如图 5-6 中所示。 让我们分解这些图表因为它们包含了大量信息。在上图中我们可以看到标记概率的直方图。它在10 -8左右有一个峰值然后在10 -4左右有第二个较小的峰值接着急剧下降只有少数标记的概率在10 -2和10 -1之间。从这个图表中我们可以看到选择具有最高概率的标记在10 -1处的孤立条的概率是 10 中的 1。 图 5-6. 下一个标记预测的概率分布上和下降标记概率的累积分布下 在下图中我们按概率降序排列了标记并计算了前 10,000 个标记的累积和GPT-2 的词汇表中总共有 50,257 个标记。曲线代表选择任何前面标记的概率。例如选择具有最高概率的 1,000 个标记的概率大约为 96%。我们看到概率在 90%以上迅速上升但只有在几千个标记后才接近 100%。图表显示有 100 分之一的机会不选择任何不在前 2,000 位的标记。 尽管这些数字乍一看可能很小但它们变得重要因为在生成文本时我们每个标记只采样一次。因此即使只有 100 分之一或 1000 分之一的几率如果我们采样数百次就有很大的机会在某个时候选择到一个不太可能的标记并且在采样时选择这些标记可能会严重影响生成文本的质量。因此我们通常希望避免这些非常不太可能的标记。这就是 top-k和 top-p采样发挥作用的地方。 top-k采样的理念是通过仅从具有最高概率的k个标记中进行采样来避免低概率的选择。这对分布的长尾部分进行了固定的截断并确保我们只从可能的选择中进行采样。回到图 5-6top-k采样相当于定义一条垂直线并从左侧的标记中进行采样。同样generate()函数提供了一个使用top_k参数轻松实现这一点的方法 output_topk model.generate(input_ids, max_lengthmax_length, do_sampleTrue,top_k50) print(tokenizer.decode(output_topk[0]))In a shocking finding, scientist discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English.The wild unicorns roam the Andes Mountains in the region of Cajamarca, on the border with Argentina (Picture: Alamy/Ecole Nationale Supérieure dHistoire Naturelle)The researchers came across about 50 of the animals in the valley. They had lived in such a remote and isolated area at that location for nearly a thousand years that这可能是我们迄今为止生成的最接近人类的文本。但是我们如何选择kk的值是手动选择的并且对于序列中的每个选择都是相同的独立于实际的输出分布。我们可以通过查看一些文本质量指标来找到k的合适值我们将在下一章中探讨这些指标但是这种固定的截断可能并不令人满意。 另一种选择是使用动态截断。使用核或 top-p采样时我们不是选择固定的截断值而是设置一个截断条件。这个条件是在选择中达到一定的概率质量时截断。假设我们将该值设置为 95%。然后我们按概率降序排列所有标记并从列表顶部逐个添加标记直到所选标记的概率总和达到 95%。回到图 5-6p的值定义了累积概率图上的水平线并且我们只从线下的标记中进行采样。根据输出分布这可能只是一个非常可能的标记也可能是一百个同等可能的标记。在这一点上您可能不会感到惊讶generate()函数还提供了一个参数来激活 top-p采样。让我们试一试 output_topp model.generate(input_ids, max_lengthmax_length, do_sampleTrue,top_p0.90) print(tokenizer.decode(output_topp[0]))In a shocking finding, scientist discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English.The scientists studied the DNA of the animals and came to the conclusion that the herd are descendants of a prehistoric herd that lived in Argentina about 50,000 years ago.According to the scientific analysis, the first humans who migrated to South America migrated into the Andes Mountains from South Africa and Australia, after the last ice age had ended.Since their migration, the animals have been adapting toTop-p采样也产生了一个连贯的故事这次是关于从澳大利亚迁移到南美洲的新情节。您甚至可以结合这两种采样方法以获得两全其美。设置top_k50和top_p0.9相当于选择具有 90%概率质量的标记最多从 50 个标记中进行选择的规则。 注意 当我们使用采样时我们还可以应用束搜索。我们可以对下一批候选标记进行采样并以相同的方式构建束而不是贪婪地选择它们。 哪种解码方法最好 不幸的是没有普遍“最佳”的解码方法。哪种方法最好将取决于你为生成文本的任务的性质。如果你希望你的模型执行精确的任务比如算术或提供对特定问题的答案那么你应该降低温度或使用贪婪搜索与波束搜索的确定性方法来保证得到最可能的答案。如果你希望模型生成更长的文本甚至有点创造性那么你应该切换到采样方法增加温度或使用 top-k和核采样的混合。 结论 在本章中我们研究了文本生成这与我们之前遇到的 NLU 任务非常不同。生成文本至少需要每个生成的标记进行一次前向传递如果使用波束搜索甚至需要更多。这使得文本生成在计算上要求很高需要适当的基础设施来规模化运行文本生成模型。此外一个良好的解码策略将模型的输出概率转换为离散标记可以提高文本质量。找到最佳的解码策略需要一些实验和对生成的文本进行主观评价。 然而在实践中我们不希望仅凭直觉做出这些决定与其他 NLP 任务一样我们应该选择一个反映我们想要解决的问题的模型性能指标。毫不奇怪有很多选择我们将在下一章中遇到最常见的选择在那里我们将看看如何训练和评估文本摘要模型。或者如果你迫不及待地想学习如何从头开始训练 GPT 类型的模型你可以直接跳到第十章在那里我们收集了大量的代码数据集然后对其进行自回归语言模型的训练。 ¹ 这个例子来自 OpenAI 的GPT-2 博客文章。 ² 然而正如Delip Rao 指出的Meena 是否打算讲冷笑话是一个微妙的问题。 ³ 如果你的机器内存不足你可以通过将model_name gpt-xl替换为model_name gpt来加载一个较小的 GPT-2 版本。 ⁴ N.S. Keskar 等人“CTRL: A Conditional Transformer Language Model for Controllable Generation”2019。 ⁵ 如果你懂一些物理学你可能会发现与玻尔兹曼分布有惊人的相似之处。 第六章摘要 你可能在某个时候需要总结一篇文档无论是研究文章、财务收益报告还是一系列电子邮件。如果你仔细想想这需要一系列能力比如理解长篇文章、推理内容并产生流畅的文本其中包含原始文档的主要主题。此外准确总结新闻文章与总结法律合同大不相同因此能够做到这一点需要一定程度的领域泛化。出于这些原因文本摘要对于神经语言模型包括 Transformer 来说是一项困难的任务。尽管存在这些挑战文本摘要为领域专家提供了显著加快工作流程的可能性并被企业用于压缩内部知识、总结合同、自动生成社交媒体发布的内容等。 为了帮助您了解涉及的挑战本章将探讨如何利用预训练的 Transformer 来总结文档。摘要是一个经典的序列到序列seq2seq任务有一个输入文本和一个目标文本。正如我们在第一章中看到的这正是编码器-解码器 Transformer 擅长的地方。 在本章中我们将构建自己的编码器-解码器模型将几个人之间的对话压缩成简洁的摘要。但在开始之前让我们先来看看摘要的经典数据集之一CNN/DailyMail 语料库。 CNN/DailyMail 数据集 CNN/DailyMail 数据集包括约 30 万对新闻文章及其相应摘要由 CNN 和 DailyMail 附加到其文章的要点组成。数据集的一个重要方面是摘要是抽象的而不是提取的这意味着它们由新句子组成而不是简单的摘录。数据集可在Hub上找到我们将使用 3.0.0 版本这是为摘要设置的非匿名版本。我们可以像在第四章中看到的那样使用version关键字选择版本。所以让我们深入研究一下 from datasets import load_datasetdataset load_dataset(cnn_dailymail, version3.0.0) print(fFeatures: {dataset[train].column_names})Features: [article, highlights, id]数据集有三列文章其中包含新闻文章摘要其中包含摘要以及id用于唯一标识每篇文章。让我们来看一段文章摘录 sample dataset[train][1] print(f Article (excerpt of 500 characters, total length: {len(sample[article])}): ) print(sample[article][:500]) print(f\nSummary (length: {len(sample[highlights])}):) print(sample[highlights])Article (excerpt of 500 characters, total length: 3192):(CNN) -- Usain Bolt rounded off the world championships Sunday by claiming his third gold in Moscow as he anchored Jamaica to victory in the mens 4x100m relay. The fastest man in the world charged clear of United States rival Justin Gatlin as the Jamaican quartet of Nesta Carter, Kemar Bailey-Cole, Nickel Ashmeade and Bolt won in 37.36 seconds. The U.S finished second in 37.56 seconds with Canada taking the bronze after Britain were disqualified for a faulty handover. The 26-year-old Bolt has nSummary (length: 180): Usain Bolt wins third gold of world championship . Anchors Jamaica to 4x100m relay victory . Eighth gold at the championships for Bolt . Jamaica double up in womens 4x100m relay .我们看到与目标摘要相比文章可能非常长在这种特殊情况下差异是 17 倍。长篇文章对大多数 Transformer 模型构成挑战因为上下文大小通常限制在 1,000 个标记左右相当于几段文字。对于摘要处理这个问题的标准但粗糙的方法是简单地截断超出模型上下文大小的文本。显然文本末尾可能有重要信息供摘要使用但目前我们需要接受模型架构的这种限制。 文本摘要管道 让我们首先定性地查看前面示例的输出看看一些最受欢迎的摘要 Transformer 模型的表现如何。尽管我们将要探索的模型架构具有不同的最大输入大小但让我们将输入文本限制为 2,000 个字符以便所有模型都具有相同的输入从而使输出更具可比性 sample_text dataset[train][1][article][:2000] # Well collect the generated summaries of each model in a dictionary summaries {}摘要中的一个惯例是用换行符分隔摘要句子。我们可以在每个句号后添加一个换行符但这种简单的启发式方法对于像“U.S.”或“U.N.”这样的字符串将失败。自然语言工具包NLTK包括一个更复杂的算法可以区分句子的结束和缩写中出现的标点符号 import nltk from nltk.tokenize import sent_tokenizenltk.download(punkt)string The U.S. are a country. The U.N. is an organization. sent_tokenize(string)[The U.S. are a country., The U.N. is an organization.]警告 在接下来的几节中我们将加载几个大型模型。如果内存不足可以用较小的检查点例如“gpt”“t5-small”替换大型模型或者跳过本节转到“在 CNN/DailyMail 数据集上评估 PEGASUS”。 摘要基线 摘要新闻文章的一个常见基线是简单地取文章的前三句。使用 NLTK 的句子分词器我们可以很容易地实现这样一个基线 def three_sentence_summary(text):return \n.join(sent_tokenize(text)[:3])summaries[baseline] three_sentence_summary(sample_text)GPT-2 我们已经在第五章中看到了 GPT-2 如何根据一些提示生成文本。该模型令人惊讶的一个特点是我们还可以使用它来生成摘要只需在输入文本末尾添加“TL;DR”。表达“TL;DR”太长了没看经常在 Reddit 等平台上使用表示长篇帖子的简短版本。我们将通过使用来自 Transformers 的pipeline()函数重新创建原始论文的程序开始我们的摘要实验。我们创建一个文本生成管道并加载大型 GPT-2 模型 from transformers import pipeline, set_seedset_seed(42) pipe pipeline(text-generation, modelgpt2-xl) gpt2_query sample_text \nTL;DR:\n pipe_out pipe(gpt2_query, max_length512, clean_up_tokenization_spacesTrue) summaries[gpt2] \n.join(sent_tokenize(pipe_out[0][generated_text][len(gpt2_query) :]))在这里我们只需存储通过切割输入查询生成的摘要并将结果保存在 Python 字典中以供以后比较。 T5 接下来让我们尝试 T5 Transformer。正如我们在第三章中看到的该模型的开发人员对 NLP 中的迁移学习进行了全面研究并发现他们可以通过将所有任务制定为文本-文本任务来创建通用 Transformer 架构。T5 检查点经过混合无监督数据用于重建掩码单词和监督数据的训练包括摘要等多个任务。因此这些检查点可以直接用于执行摘要而无需使用预训练期间使用的相同提示进行微调。在该框架中模型摘要文档的输入格式为summarize: ARTICLE翻译的格式看起来像translate English to German: TEXT。如图 6-1 所示这使得 T5 非常灵活可以使用单个模型解决许多任务。 我们可以直接使用pipeline()函数加载 T5 进行摘要该函数还负责以文本-文本格式格式化输入因此我们不需要在其前面加上summarize pipe pipeline(summarization, modelt5-large) pipe_out pipe(sample_text) summaries[t5] \n.join(sent_tokenize(pipe_out[0][summary_text]))图 6-1。T5 的文本-文本框架图由 Colin Raffel 提供除了翻译和摘要之外还显示了 CoLA语言可接受性和 STSB语义相似性任务 巴特 BART 还使用了编码器-解码器架构并且经过训练以重建损坏的输入。它结合了 BERT 和 GPT-2 的预训练方案。我们将使用facebook/bart-large-ccn检查点该检查点已经在 CNN/DailyMail 数据集上进行了特定的微调 pipe pipeline(summarization, modelfacebook/bart-large-cnn) pipe_out pipe(sample_text) summaries[bart] \n.join(sent_tokenize(pipe_out[0][summary_text]))飞马座 像 BART 一样PEGASUS 是一种编码器-解码器 Transformer。如图 6-2 所示它的预训练目标是预测多句文本中的掩码句子。作者认为预训练目标与下游任务越接近效果就越好。为了找到一个比通用语言建模更接近摘要的预训练目标他们在一个非常大的语料库中自动识别了包含其周围段落大部分内容的句子使用摘要评估指标作为内容重叠的启发式并预训练 PEGASUS 模型以重建这些句子从而获得了用于文本摘要的最先进模型。 图 6-2。PEGASUS 架构图由 Jingqing Zhang 等人提供 该模型具有用于换行的特殊标记这就是为什么我们不需要sent_tokenize()函数的原因 pipe pipeline(summarization, modelgoogle/pegasus-cnn_dailymail) pipe_out pipe(sample_text) summaries[pegasus] pipe_out[0][summary_text].replace( .n, .\n)比较不同的摘要 现在我们已经用四种不同的模型生成了摘要让我们比较一下结果。请记住其中一个模型根本没有在数据集上进行训练GPT-2一个模型在这项任务以及其他任务上进行了微调T5而另外两个模型则专门在这项任务上进行了微调BART 和 PEGASUS。让我们看看这些模型生成的摘要 print(GROUND TRUTH) print(dataset[train][1][highlights]) print()for model_name in summaries:print(model_name.upper())print(summaries[model_name])print()GROUND TRUTH Usain Bolt wins third gold of world championship . Anchors Jamaica to 4x100m relay victory . Eighth gold at the championships for Bolt . Jamaica double up in womens 4x100m relay .BASELINE (CNN) -- Usain Bolt rounded off the world championships Sunday by claiming his third gold in Moscow as he anchored Jamaica to victory in the mens 4x100m relay. The fastest man in the world charged clear of United States rival Justin Gatlin as the Jamaican quartet of Nesta Carter, Kemar Bailey-Cole, Nickel Ashmeade and Bolt won in 37.36 seconds. The U.S finished second in 37.56 seconds with Canada taking the bronze after Britain were disqualified for a faulty handover.GPT2 Nesta, the fastest man in the world. Gatlin, the most successful Olympian ever. Kemar, a Jamaican legend. Shelly-Ann, the fastest woman ever. Bolt, the worlds greatest athlete. The team sport of pole vaultingT5 usain bolt wins his third gold medal of the world championships in the mens 4x100m relay . the 26-year-old anchored Jamaica to victory in the event in the Russian capital . he has now collected eight gold medals at the championships, equaling the record .BART Usain Bolt wins his third gold of the world championships in Moscow. Bolt anchors Jamaica to victory in the mens 4x100m relay. The 26-year-old has now won eight gold medals at world championships. Jamaicas women also win gold in the relay, beating France in the process.PEGASUS Usain Bolt wins third gold of world championships. Anchors Jamaica to victory in mens 4x100m relay. Eighth gold at the championships for Bolt. Jamaica also win womens 4x100m relay .通过查看模型输出我们首先注意到 GPT-2 生成的摘要与其他模型有很大不同。它不是给出文本的摘要而是总结了字符。由于它并没有明确训练生成真实摘要因此 GPT-2 模型经常“产生幻觉”或虚构事实。例如在撰写时内斯塔并不是世界上最快的人而是排名第九。将其他三个模型的摘要与真实情况进行比较我们发现 PEGASUS 的输出与之最为相似。 现在我们已经检查了一些模型让我们试着决定在生产环境中使用哪个模型。所有四个模型似乎都提供了合理的结果我们可以生成更多的例子来帮助我们决定。然而这并不是一个系统确定最佳模型的方法理想情况下我们会定义一个指标在一些基准数据集上对所有模型进行测量并选择性能最佳的模型。但是如何定义文本生成的指标呢我们看到的标准指标如准确率、召回率和精度不容易应用于这个任务。对于人类撰写的“黄金标准”摘要可能有数十种其他具有同义词、释义或稍微不同表达事实方式的摘要同样可以接受。 在接下来的部分我们将看一些用于衡量生成文本质量的常见指标。 衡量生成文本的质量 好的评估指标很重要因为我们用它们来衡量模型的性能不仅在训练时使用而且在生产中也会用到。如果我们的指标不好我们可能会对模型的退化视而不见如果它们与业务目标不一致我们可能无法创造任何价值。 在文本生成任务上衡量性能并不像标准分类任务如情感分析或命名实体识别那样容易。以翻译为例给定一个句子“我爱狗”的英文翻译成西班牙语可能有多种有效的可能性比如“¡Me encantan los perros!”或“¡Me gustan los perros!”简单地检查是否与参考翻译完全匹配并不是最佳选择即使是人类在这样的指标上表现也不佳因为我们每个人写的文本都略有不同甚至在一天或一年的不同时间也不同。幸运的是还有其他选择。 用于评估生成文本的最常见的两个指标是 BLEU 和 ROUGE。让我们看看它们是如何定义的。 BLEU BLEU 的思想很简单⁴我们不是看生成文本中有多少标记与参考文本标记完全对齐而是看单词或n-grams。BLEU 是一种基于精度的指标这意味着当我们比较两个文本时我们计算生成文本中与参考文本中出现的单词数并将其除以参考文本的长度。 然而这种基础精度存在一个问题。假设生成的文本只是一遍又一遍地重复相同的单词并且这个单词也出现在参考文本中。如果它重复的次数正好等于参考文本的长度那么我们就得到了完美的精度因此BLEU 论文的作者引入了一个轻微的修改一个单词只计算它在参考文本中出现的次数。为了说明这一点假设我们有参考文本“猫在垫子上”生成文本“猫猫猫猫猫猫”。 从这个简单的例子中我们可以计算出精度值如下 math alttext“p 下标 vanilla 基线等于六分之六” 显示“块” p v a n i l l a 6 6 math alttext“p 下标 mod 基线等于二分之六” 显示“块” p m o d 2 6 我们可以看到简单的修正产生了一个更合理的值。现在让我们通过不仅查看单词而且还查看n-克拉姆来扩展这一点。假设我们有一个生成的句子 s n t 我们想将其与一个参考句子 s n t ’ 进行比较。我们提取所有可能的n-克拉姆并进行计算得到精度 p n 数学标记“p 下标 n 基线等于开始分数 sigma-总和下标 n-克拉姆元素属于 snt 上标计数下标 clip 基线左括号 n-克拉姆右括号除以 sigma-总和下标 n-克拉姆元素属于 snt 上标计数左括号 n-克拉姆右括号结束分数” 显示“块” p n ∑ n - g r a m ∈ s n t C o u n t c l i p ( n - g r a m ) ∑ n - g r a m ∈ s n t ’ C o u n t ( n - g r a m ) 为了避免奖励重复生成分子中的计数被剪切。这意味着n-克拉姆的出现次数被限制为它在参考句子中出现的次数。还要注意这个方程中对句子的定义并不是非常严格的如果你有一个生成的跨越多个句子的文本你会将它视为一个句子。 通常情况下我们在测试集中有多个样本要评估因此我们需要稍微扩展方程对语料库C中的所有样本进行求和 数学标记“p 下标 n 基线等于开始分数 sigma-求和下标 snt 属于上 C 结束 sigma-求和下标 n-hyphen-gram 属于 snt 上 C 计数 clip 下标基线左括号 n-hyphen-gram 右括号除以 sigma-求和下标 snt prime 属于上 C 结束 sigma-求和下标 n-hyphen-gram 属于 snt prime 计数左括号 n-hyphen-gram 右括号结束分数” 显示“块” p n ∑ s n t ∈ C ∑ n - g r a m ∈ s n t C o u n t c l i p ( n - g r a m ) ∑ s n t ’ ∈ C ∑ n - g r a m ∈ s n t ’ C o u n t ( n - g r a m ) 我们快要到了。由于我们不关注召回率所有生成的序列如果短而准确都比长句子有益。因此精确度得分偏爱短生成物。为了补偿这一点BLEU 的作者引入了一个额外的术语简洁惩罚 math alttext“上 B 上 R 等于 min 左括号 1e 上标 1 减脚本 l Super Subscript r e f Superscript slash 脚本 l Super Subscript g e n Superscript Baseline 右括号” 显示“块” B R 可移动限制“真形式“前缀” min mfenced 分隔符”“打开”(“关闭”) 1 e 1 - ℓ r e f / ℓ g e n 通过取最小值我们确保这个惩罚永远不会超过 1当生成文本的长度l gen小于参考文本l ref时指数项变得指数级小。此时你可能会问为什么我们不使用类似F[1]-分数来考虑召回率呢答案是通常在翻译数据集中有多个参考句子而不只是一个因此如果我们也测量召回率我们会鼓励使用所有参考句子中的所有单词的翻译。因此最好是寻求翻译中的高精度并确保翻译和参考具有类似的长度。 最后我们可以把所有东西放在一起得到 BLEU 分数的方程 math alttext“” 显示“块” BLEU- N B R × mfenced 分隔符““打开”(“关闭”)” ∏ n 1 N p n 1 / N 最后一个术语是修改后的精度直到n-gram N的几何平均值。在实践中通常报告 BLEU-4 分数。然而您可能已经看到这个指标有许多局限性例如它不考虑同义词并且推导过程中的许多步骤似乎是临时的和相当脆弱的启发式。您可以在 Rachel Tatman 的博客文章“Evaluating Text Output in NLP: BLEU at Your Own Risk”中找到对 BLEU 缺陷的精彩阐述。 总的来说文本生成领域仍在寻找更好的评估指标克服 BLEU 等指标的局限性是一个活跃的研究领域。BLEU 指标的另一个弱点是它期望文本已经被标记化。如果没有使用完全相同的文本标记化方法这可能会导致不同的结果。SacreBLEU 指标通过内部化标记化步骤来解决这个问题因此它是用于基准测试的首选指标。 我们现在已经通过了一些理论但我们真正想做的是计算一些生成文本的分数。这是否意味着我们需要在 Python 中实现所有这些逻辑不用担心 Datasets 还提供了指标加载指标的方式与加载数据集的方式相同 from datasets import load_metricbleu_metric load_metric(sacrebleu)bleu_metric对象是Metric类的一个实例它像聚合器一样工作您可以使用add()添加单个实例也可以通过add_batch()添加整个批次。一旦添加了需要评估的所有样本然后调用compute()指标就会被计算。这将返回一个包含多个值的字典例如每个n-gram 的精度长度惩罚以及最终的 BLEU 分数。让我们看一下之前的例子 import pandas as pd import numpy as npbleu_metric.add(predictionthe the the the the the, reference[the cat is on the mat]) results bleu_metric.compute(smooth_methodfloor, smooth_value0) results[precisions] [np.round(p, 2) for p in results[precisions]] pd.DataFrame.from_dict(results, orientindex, columns[Value])值分数0.0计数[2, 0, 0, 0]总数[6, 5, 4, 3]精度[33.33, 0.0, 0.0, 0.0]bp1.0系统长度6参考长度6 注 如果有多个参考翻译BLEU 分数也适用。这就是为什么reference作为列表传递的原因。为了使n-gram 中的零计数的指标更加平滑BLEU 集成了修改精度计算的方法。一种方法是向分子添加一个常数。这样缺少的n-gram 不会导致分数自动变为零。为了解释这些值我们通过设置smooth_value0将其关闭。 我们可以看到 1-gram 的精度确实是 2/6而 2/3/4-gram 的精度都是 0。 有关个别指标的更多信息如计数和 bp请参阅SacreBLEU 存储库。这意味着几何平均值为零因此 BLEU 分数也为零。让我们看另一个几乎正确的预测示例 bleu_metric.add(predictionthe cat is on mat, reference[the cat is on the mat]) results bleu_metric.compute(smooth_methodfloor, smooth_value0) results[precisions] [np.round(p, 2) for p in results[precisions]] pd.DataFrame.from_dict(results, orientindex, columns[Value])值分数57.893007计数[5, 3, 2, 1]总数[5, 4, 3, 2]精度[100.0, 75.0, 66.67, 50.0]bp0.818731系统长度5参考长度6 我们观察到精度分数要好得多。预测中的 1-gram 都匹配只有在精度分数中我们才看到有些不对劲。对于 4-gram只有两个候选项[the, cat, is, on]和[cat, is, on, mat]后者不匹配因此精度为 0.5。 BLEU 分数被广泛用于评估文本特别是在机器翻译中因为通常更青睐精确的翻译而不是包含所有可能和适当单词的翻译。 还有其他应用比如摘要情况就不同了。在那里我们希望生成的文本中包含所有重要信息因此我们更青睐高召回率。这就是 ROUGE 分数通常被使用的地方。 ROUGE ROUGE 分数专门为像摘要这样的应用程序开发其中高召回率比精确度更重要。这种方法与 BLEU 分数非常相似我们观察不同的n-gram并比较它们在生成文本和参考文本中的出现次数。不同之处在于对于 ROUGE我们检查参考文本中有多少n-gram 也出现在生成文本中。对于 BLEU我们看生成文本中有多少n-gram 出现在参考文本中因此我们可以重新使用精确度公式只是在分子中计算生成文本中参考n-gram 的未修剪的出现次数。 ROUGE-N ∑ snt’€™∈C ∑ n-gram∈snt ‘ Count match (n-gram) ∑ snt’€™∈C ∑ n-gram∈snt ’ Count(n-gram) 这是 ROUGE 的原始提案。随后研究人员发现完全移除精确度可能会产生强烈的负面影响。回到没有修剪计数的 BLEU 公式我们可以衡量精确度并且可以将精确度和召回的 ROUGE 分数结合起来得到F[1]-score。这个分数是现在常用于 ROUGE 报告的度量标准。 ROUGE 中有一个单独的分数用于衡量最长公共子串LCS称为 ROUGE-L。LCS 可以计算任意一对字符串。例如“abab”和“abc”的 LCS 将是“ab”其长度为 2。如果我们想比较两个样本之间的这个值我们需要以某种方式对其进行归一化否则较长的文本将处于优势地位。为了实现这一点ROUGE 的发明者提出了一种类似于F -score 的方案其中 LCS 与参考文本和生成文本的长度进行了归一化然后将两个归一化分数混合在一起 R LCS LCS(X,Y) mP LCS LCS(X,Y) nF LCS (1β 2 )R LCS P LCS R LCS βP LCS , where β P LCS / R LCS 这样LCS 分数就可以得到适当的归一化并可以在样本之间进行比较。在数据集实现中计算了两种 ROUGE 的变体一种计算每个句子的得分并对摘要进行平均ROUGE-L另一种直接在整个摘要上计算ROUGE-Lsum。 我们可以按以下方式加载度量标准 rouge_metric load_metric(rouge)我们已经使用 GPT-2 和其他模型生成了一组摘要现在我们有了一个系统比较摘要的度量标准。让我们将 ROUGE 分数应用于模型生成的所有摘要 reference dataset[train][1][highlights] records [] rouge_names [rouge1, rouge2, rougeL, rougeLsum]for model_name in summaries:rouge_metric.add(predictionsummaries[model_name], referencereference)score rouge_metric.compute()rouge_dict dict((rn, score[rn].mid.fmeasure) for rn in rouge_names)records.append(rouge_dict) pd.DataFrame.from_records(records, indexsummaries.keys())rouge1rouge2rougeLrougeLsum—————基线0.3035710.0909090.2142860.232143gpt20.1875000.0000000.1250000.187500t50.4864860.2222220.3783780.486486bart0.5822780.2077920.4556960.506329pegasus0.8666670.6551720.8000000.833333 注意 在数据集库中ROUGE 度量标准还计算置信区间默认情况下为第 5 和第 95 百分位数。平均值存储在属性mid中区间可以通过low和high检索。 显然这些结果并不是非常可靠因为我们只看了一个样本但我们可以比较一下这一个例子的摘要质量。表格证实了我们的观察即我们考虑的模型中GPT-2 的表现最差。这并不奇怪因为它是这个组中唯一没有明确训练用于摘要的模型。然而令人惊讶的是简单的前三句基线与拥有大约十亿参数的 Transformer 模型相比表现并不太差PEGASUS 和 BART 是整体上最好的模型ROUGE 分数越高越好但 T5 在 ROUGE-1 和 LCS 分数上略微更好。这些结果将 T5 和 PEGASUS 列为最佳模型但再次需要谨慎对待这些结果因为我们只在一个例子上评估了模型。从 PEGASUS 论文中的结果来看我们预期 PEGASUS 在 CNN/DailyMail 数据集上的表现将优于 T5。 让我们看看我们是否可以用 PEGASUS 复现这些结果。 评估 PEGASUS 在 CNN/DailyMail 数据集上 我们现在已经准备好适当地评估模型了我们有一个来自 CNN/DailyMail 的测试集的数据集我们有一个 ROUGE 指标我们有一个摘要模型。我们只需要把这些部分组合起来。让我们首先评估三句基线的性能 def evaluate_summaries_baseline(dataset, metric,column_textarticle,column_summaryhighlights):summaries [three_sentence_summary(text) for text in dataset[column_text]]metric.add_batch(predictionssummaries,referencesdataset[column_summary])score metric.compute()return score现在我们将把这个函数应用到数据的一个子集上。由于 CNN/DailyMail 数据集的测试部分大约有 10,000 个样本为所有这些文章生成摘要需要很长时间。回想一下第五章中提到的每个生成的标记都需要通过模型进行前向传递为每个样本生成 100 个标记将需要 100 万次前向传递如果我们使用波束搜索这个数字将乘以波束的数量。为了保持计算相对快速我们将对测试集进行子采样然后在 1,000 个样本上运行评估。这应该给我们一个更加稳定的分数估计同时在单个 GPU 上不到一小时内完成 PEGASUS 模型的评估 test_sampled dataset[test].shuffle(seed42).select(range(1000))score evaluate_summaries_baseline(test_sampled, rouge_metric) rouge_dict dict((rn, score[rn].mid.fmeasure) for rn in rouge_names) pd.DataFrame.from_dict(rouge_dict, orientindex, columns[baseline]).Trouge1rouge2rougeLrougeLsum基线0.3960610.1739950.2458150.361158 大多数分数比上一个例子差但仍然比 GPT-2 取得的分数要好现在让我们实现相同的评估函数来评估 PEGASUS 模型 from tqdm import tqdm import torchdevice cuda if torch.cuda.is_available() else cpudef chunks(list_of_elements, batch_size):Yield successive batch-sized chunks from list_of_elements.for i in range(0, len(list_of_elements), batch_size):yield list_of_elements[i : i batch_size]def evaluate_summaries_pegasus(dataset, metric, model, tokenizer,batch_size16, devicedevice,column_textarticle,column_summaryhighlights):article_batches list(chunks(dataset[column_text], batch_size))target_batches list(chunks(dataset[column_summary], batch_size))for article_batch, target_batch in tqdm(zip(article_batches, target_batches), totallen(article_batches)):inputs tokenizer(article_batch, max_length1024, truncationTrue,paddingmax_length, return_tensorspt)summaries model.generate(input_idsinputs[input_ids].to(device),attention_maskinputs[attention_mask].to(device),length_penalty0.8, num_beams8, max_length128)decoded_summaries [tokenizer.decode(s, skip_special_tokensTrue,clean_up_tokenization_spacesTrue)for s in summaries]decoded_summaries [d.replace(n, ) for d in decoded_summaries]metric.add_batch(predictionsdecoded_summaries, referencestarget_batch)score metric.compute()return score让我们稍微解开一下这个评估代码。首先我们将数据集分成较小的批次然后对每个批次进行同时处理。然后对于每个批次我们对输入文章进行标记化并将它们馈送到generate()函数中使用波束搜索生成摘要。我们使用与论文中提出的相同的生成参数。长度惩罚的新参数确保模型不会生成太长的序列。最后我们解码生成的文本替换n标记并将解码的文本与参考文献一起添加到度量中。最后我们计算并返回 ROUGE 分数。现在让我们再次使用AutoModelForSeq2SeqLM类加载模型用于 seq2seq 生成任务并对其进行评估 from transformers import AutoModelForSeq2SeqLM, AutoTokenizermodel_ckpt google/pegasus-cnn_dailymail tokenizer AutoTokenizer.from_pretrained(model_ckpt) model AutoModelForSeq2SeqLM.from_pretrained(model_ckpt).to(device) score evaluate_summaries_pegasus(test_sampled, rouge_metric,model, tokenizer, batch_size8) rouge_dict dict((rn, score[rn].mid.fmeasure) for rn in rouge_names) pd.DataFrame(rouge_dict, index[pegasus])rouge1rouge2rougeLrougeLsumpegasus0.4343810.2108830.3071950.373231 这些数字非常接近已发布的结果。这里需要注意的一点是损失和每个标记的准确性在某种程度上与 ROUGE 分数是分离的。损失与解码策略无关而 ROUGE 分数与之强相关。 由于 ROUGE 和 BLEU 与人类判断更相关比损失或准确性我们应该专注于它们并在构建文本生成模型时仔细探索和选择解码策略。然而这些指标远非完美人们始终应该考虑人类判断。 现在我们有了一个评估函数是时候为我们自己的摘要模型进行训练了。 训练摘要模型 我们已经详细讨论了文本摘要和评估的许多细节现在让我们利用这些来训练一个自定义的文本摘要模型对于我们的应用我们将使用三星开发的SAMSum 数据集其中包含一系列对话以及简要摘要。在企业环境中这些对话可能代表客户与支持中心之间的互动因此生成准确的摘要可以帮助改善客户服务并检测客户请求中的常见模式。让我们加载它并查看一个示例 dataset_samsum load_dataset(samsum) split_lengths [len(dataset_samsum[split])for split in dataset_samsum]print(fSplit lengths: {split_lengths}) print(fFeatures: {dataset_samsum[train].column_names}) print(\nDialogue:) print(dataset_samsum[test][0][dialogue]) print(\nSummary:) print(dataset_samsum[test][0][summary])Split lengths: [14732, 819, 818] Features: [id, dialogue, summary]Dialogue: Hannah: Hey, do you have Bettys number? Amanda: Lemme check Hannah: file_gif Amanda: Sorry, cant find it. Amanda: Ask Larry Amanda: He called her last time we were at the park together Hannah: I dont know him well Hannah: file_gif Amanda: Dont be shy, hes very nice Hannah: If you say so.. Hannah: Id rather you texted him Amanda: Just text him Hannah: Urgh.. Alright Hannah: Bye Amanda: Bye byeSummary: Hannah needs Bettys number but Amanda doesnt have it. She needs to contact Larry.对话看起来就像你通过短信或 WhatsApp 进行聊天时所期望的那样包括表情符号和 GIF 的占位符。dialogue字段包含完整文本summary包含总结的对话。CNN/DailyMail 数据集上进行了微调的模型能够处理吗让我们找出来 在 SAMSum 上评估 PEGASUS 首先我们将使用 PEGASUS 运行相同的摘要流程看看输出是什么样的。我们可以重用用于 CNN/DailyMail 摘要生成的代码 pipe_out pipe(dataset_samsum[test][0][dialogue]) print(Summary:) print(pipe_out[0][summary_text].replace( .n, .\n))Summary: Amanda: Ask Larry Amanda: He called her last time we were at the park together. Hannah: Id rather you texted him. Amanda: Just text him .我们可以看到该模型主要通过提取对话中的关键句子来进行摘要。这在 CNN/DailyMail 数据集上可能效果相对较好但在 SAMSum 中的摘要更加抽象。让我们通过在测试集上运行完整的 ROUGE 评估来确认这一点 score evaluate_summaries_pegasus(dataset_samsum[test], rouge_metric, model,tokenizer, column_textdialogue,column_summarysummary, batch_size8)rouge_dict dict((rn, score[rn].mid.fmeasure) for rn in rouge_names) pd.DataFrame(rouge_dict, index[pegasus])rouge1rouge2rougeLrougeLsumpegasus0.2961680.0878030.2296040.229514 结果并不理想但这并不意外因为我们已经远离了 CNN/DailyMail 数据分布。尽管如此在训练之前设置评估流程有两个优点我们可以直接用指标来衡量训练的成功与否同时也有了一个很好的基准。在我们的数据集上微调模型应该会立即提高 ROUGE 指标如果不是这样我们就会知道我们的训练循环出了问题。 PEGASUS 微调 在处理训练数据之前让我们快速查看输入和输出的长度分布 d_len [len(tokenizer.encode(s)) for s in dataset_samsum[train][dialogue]] s_len [len(tokenizer.encode(s)) for s in dataset_samsum[train][summary]]fig, axes plt.subplots(1, 2, figsize(10, 3.5), shareyTrue) axes[0].hist(d_len, bins20, colorC0, edgecolorC0) axes[0].set_title(Dialogue Token Length) axes[0].set_xlabel(Length) axes[0].set_ylabel(Count) axes[1].hist(s_len, bins20, colorC0, edgecolorC0) axes[1].set_title(Summary Token Length) axes[1].set_xlabel(Length) plt.tight_layout() plt.show()我们看到大多数对话比 CNN/DailyMail 文章要短得多每个对话大约有 100-200 个标记。同样摘要也要短得多大约有 20-40 个标记平均推文长度。 在构建Trainer的数据收集器时让我们记住这些观察结果。首先我们需要对数据集进行标记化。目前我们将对话和摘要的最大长度分别设置为 1024 和 128 def convert_examples_to_features(example_batch):input_encodings tokenizer(example_batch[dialogue], max_length1024,truncationTrue)with tokenizer.as_target_tokenizer():target_encodings tokenizer(example_batch[summary], max_length128,truncationTrue)return {input_ids: input_encodings[input_ids],attention_mask: input_encodings[attention_mask],labels: target_encodings[input_ids]}dataset_samsum_pt dataset_samsum.map(convert_examples_to_features,batchedTrue) columns [input_ids, labels, attention_mask] dataset_samsum_pt.set_format(typetorch, columnscolumns)在标记化步骤中使用的新功能是tokenizer.as_target_tokenizer()上下文。一些模型需要在解码器输入中使用特殊标记因此区分编码器和解码器输入的标记化非常重要。在with语句称为上下文管理器中标记器知道它正在为解码器进行标记化并可以相应地处理序列。 现在我们需要创建数据收集器。这个函数在Trainer中在批处理通过模型之前调用。在大多数情况下我们可以使用默认的收集器它会从批处理中收集所有张量并简单地堆叠它们。对于摘要任务我们不仅需要堆叠输入还需要在解码器端准备目标。PEGASUS 是一个编码器-解码器变换器因此具有经典的 seq2seq 架构。在 seq2seq 设置中一个常见的方法是在解码器中应用“教师强制”。使用这种策略解码器接收输入标记就像在仅解码器模型如 GPT-2中一样其中包括标签向后移动一个位置的内容以及编码器输出因此在进行下一个标记的预测时解码器会得到向后移动一个位置的地面真相作为输入如下表所示 解码器输入标签步骤———1[PAD]Transformers2[PAD, Transformers]are3[PAD, Transformers, are]awesome4[PAD, Transformers, are, awesome]for5[PAD, Transformers, are, awesome, for]text6[PAD, Transformers, are, awesome, for, text]summarization 我们将其向右移动一个以便解码器只看到先前的地面真实标签而不是当前或未来的标签。仅仅移位就足够了因为解码器具有掩码自注意力可以屏蔽当前和未来的所有输入。 因此当我们准备批处理时我们通过将标签向右移动一个来设置解码器输入。之后我们确保通过将它们设置为-100 来忽略标签中的填充标记以便损失函数忽略它们。实际上我们不必手动执行此操作因为DataCollatorForSeq2Seq会帮助我们处理所有这些步骤 from transformers import DataCollatorForSeq2Seqseq2seq_data_collator DataCollatorForSeq2Seq(tokenizer, modelmodel)然后像往常一样我们为训练设置了TrainingArguments from transformers import TrainingArguments, Trainertraining_args TrainingArguments(output_dirpegasus-samsum, num_train_epochs1, warmup_steps500,per_device_train_batch_size1, per_device_eval_batch_size1,weight_decay0.01, logging_steps10, push_to_hubTrue,evaluation_strategysteps, eval_steps500, save_steps1e6,gradient_accumulation_steps16)与以前的设置不同的一件事是新的参数gradient_accumulation_steps。由于模型非常庞大我们不得不将批处理大小设置为 1。然而太小的批处理大小可能会影响收敛。为了解决这个问题我们可以使用一种称为梯度累积的巧妙技术。顾名思义我们不是一次计算整个批次的梯度而是制作更小的批次并聚合梯度。当我们聚合了足够的梯度时我们运行优化步骤。当然这比一次性完成要慢一些但它节省了大量的 GPU 内存。 现在让我们确保我们已经登录到 Hugging Face这样我们就可以在训练后将模型推送到 Hub 了 from huggingface_hub import notebook_loginnotebook_login()现在我们已经准备好使用模型、分词器、训练参数和数据整理器以及训练和评估集来初始化训练器了 trainer Trainer(modelmodel, argstraining_args,tokenizertokenizer, data_collatorseq2seq_data_collator,train_datasetdataset_samsum_pt[train],eval_datasetdataset_samsum_pt[validation])我们已经准备好进行训练。训练后我们可以直接在测试集上运行评估函数以查看模型的表现如何 trainer.train() score evaluate_summaries_pegasus(dataset_samsum[test], rouge_metric, trainer.model, tokenizer,batch_size2, column_textdialogue, column_summarysummary)rouge_dict dict((rn, score[rn].mid.fmeasure) for rn in rouge_names) pd.DataFrame(rouge_dict, index[fpegasus])rouge1rouge2rougeLrougeLsumpegasus0.4276140.2005710.3406480.340738 我们看到 ROUGE 分数比没有微调的模型有了显著的改进因此即使先前的模型也是用于摘要但它并没有很好地适应新的领域。让我们将模型推送到 Hub trainer.push_to_hub(Training complete!)在下一节中我们将使用模型为我们生成一些摘要。 提示 您还可以在训练循环中评估生成的内容使用名为Seq2Seq​Trainin⁠g​Arguments的TrainingArguments的扩展并指定predict_with_generateTrue。将其传递给专用的Trainer称为Seq2SeqTrainer然后使用generate()函数而不是模型的前向传递来创建评估预测。试试看 生成对话摘要 通过查看损失和 ROUGE 分数似乎模型在 CNN/DailyMail 上训练的原始模型相比显示了显著的改进。让我们看看从测试集中的样本生成的摘要是什么样子的 gen_kwargs {length_penalty: 0.8, num_beams:8, max_length: 128} sample_text dataset_samsum[test][0][dialogue] reference dataset_samsum[test][0][summary] pipe pipeline(summarization, modeltransformersbook/pegasus-samsum)print(Dialogue:) print(sample_text) print(\nReference Summary:) print(reference) print(\nModel Summary:) print(pipe(sample_text, **gen_kwargs)[0][summary_text])Dialogue: Hannah: Hey, do you have Bettys number? Amanda: Lemme check Hannah: file_gif Amanda: Sorry, cant find it. Amanda: Ask Larry Amanda: He called her last time we were at the park together Hannah: I dont know him well Hannah: file_gif Amanda: Dont be shy, hes very nice Hannah: If you say so.. Hannah: Id rather you texted him Amanda: Just text him Hannah: Urgh.. Alright Hannah: Bye Amanda: Bye byeReference Summary: Hannah needs Bettys number but Amanda doesnt have it. She needs to contact Larry.Model Summary: Amanda cant find Bettys number. Larry called Betty last time they were at the park together. Hannah wants Amanda to text Larry instead of calling Betty. 这看起来更像参考摘要。似乎模型已经学会将对话合成为摘要而不仅仅是提取段落。现在最终测试模型在自定义输入上的表现如何 custom_dialogue \ Thom: Hi guys, have you heard of transformers? Lewis: Yes, I used them recently! Leandro: Indeed, there is a great library by Hugging Face. Thom: I know, I helped build it ;) Lewis: Cool, maybe we should write a book about it. What do you think? Leandro: Great idea, how hard can it be?! Thom: I am in! Lewis: Awesome, lets do it together!print(pipe(custom_dialogue, **gen_kwargs)[0][summary_text])Thom, Lewis and Leandro are going to write a book about transformers. Thom helped build a library by Hugging Face. They are going to do it together.定制对话的生成摘要是有意义的。它很好地总结了讨论中所有人都想一起写书而不仅仅是提取单个句子。例如它将第三和第四行合成了一个逻辑组合。 结论 与可以作为分类任务框架的其他任务相比文本摘要提出了一些独特的挑战比如情感分析、命名实体识别或问答。传统的度量标准如准确性并不能反映生成文本的质量。正如我们所看到的BLEU 和 ROUGE 指标可以更好地评估生成的文本然而人类判断仍然是最好的衡量标准。 在使用总结模型时一个常见问题是如何总结文档其中文本长度超过模型的上下文长度。不幸的是目前还没有解决这个问题的单一策略迄今为止这仍然是一个开放和活跃的研究问题。例如OpenAI 的最新工作显示了如何通过将其递归应用于长文档并在循环中使用人类反馈来扩展总结。⁶ 在下一章中我们将讨论问答这是根据文本段落提供问题答案的任务。与总结相比对于这个任务存在处理长篇或多篇文档的良好策略我们将向您展示如何将问答扩展到数千篇文档。 ¹ A. Radford 等人“语言模型是无监督多任务学习者”OpenAI (2019)。 ² M. Lewis 等人“BART:去噪序列到序列的预训练用于自然语言生成、翻译和理解”(2019)。 ³ J. Zhang 等人“PEGASUS:使用抽取的间隙句子进行抽象总结的预训练”(2019)。 ⁴ K. Papineni 等人“BLEU:机器翻译自动评估的方法”计算语言学协会第 40 届年会论文集 (2002 年 7 月): 311–318http://dx.doi.org/10.3115/1073083.1073135。 ⁵ C-Y. Lin“ROUGE:自动摘要评估包”文本摘要分支扩展 (2004 年 7 月)https://aclanthology.org/W04-1013.pdf。 ⁶ J. Wu 等人“使用人类反馈递归总结书籍”(2021)。 第七章问答 无论您是研究人员、分析师还是数据科学家都有可能在某个时候需要浏览大量文档以找到您正在寻找的信息。更糟糕的是您不断地被谷歌和必应提醒存在更好的搜索方式例如如果我们在谷歌上搜索“玛丽·居里何时获得她的第一个诺贝尔奖”我们立即得到了“1903”这个正确答案如图 7-1 所示。 图 7-1 谷歌搜索查询和相应的答案片段 在这个例子中谷歌首先检索了大约 319,000 个与查询相关的文档然后进行了额外的处理步骤提取了带有相应段落和网页的答案片段。很容易看出这些答案片段是有用的。例如如果我们搜索一个更棘手的问题比如“哪种吉他调音是最好的”谷歌没有提供答案而是我们必须点击搜索引擎返回的网页之一来找到答案。¹ 这项技术背后的一般方法被称为问答QA。有许多种类的 QA但最常见的是抽取式 QA它涉及到问题的答案可以在文档中识别为文本段落的一部分文档可能是网页、法律合同或新闻文章。首先检索相关文档然后从中提取答案的两阶段过程也是许多现代 QA 系统的基础包括语义搜索引擎、智能助手和自动信息提取器。在本章中我们将应用这一过程来解决电子商务网站面临的一个常见问题帮助消费者回答特定的查询以评估产品。我们将看到客户评论可以作为 QA 的丰富而具有挑战性的信息来源并且在这个过程中我们将学习 transformers 如何作为强大的阅读理解模型可以从文本中提取含义。让我们从详细说明用例开始。 注 本章重点介绍抽取式 QA但其他形式的 QA 可能更适合您的用例。例如社区 QA涉及收集用户在论坛上生成的问题-答案对然后使用语义相似性搜索找到与新问题最接近的答案。还有长篇 QA旨在对开放性问题生成复杂的段落长度答案比如“天空为什么是蓝色”值得注意的是还可以对表格进行 QAtransformer 模型如TAPAS甚至可以执行聚合以生成最终答案 构建基于评论的问答系统 如果您曾经在网上购买过产品您可能会依赖客户评论来帮助您做出决定。这些评论通常可以帮助回答特定问题比如“这把吉他带吗”或者“我可以在晚上使用这个相机吗”这些问题可能很难仅通过产品描述就能回答。然而热门产品可能会有数百甚至数千条评论因此找到相关的评论可能会很麻烦。一个替代方法是在像亚马逊这样的网站提供的社区问答平台上发布您的问题但通常需要几天时间才能得到答案如果有的话。如果我们能像图 7-1 中的谷歌示例那样立即得到答案那不是挺好的吗让我们看看是否可以使用 transformers 来实现这一点 数据集 为了构建我们的 QA 系统我们将使用 SubjQA 数据集²该数据集包括英语中关于六个领域的产品和服务的 10,000 多条客户评论TripAdvisor、餐馆、电影、书籍、电子产品和杂货。如图 7-2 所示每条评论都与一个问题相关联可以使用评论中的一句或多句来回答。³ 图 7-2. 有关产品的问题和相应的评论答案范围已划线 该数据集的有趣之处在于大多数问题和答案都是主观的也就是说它们取决于用户的个人经验。图 7-2 中的示例显示了为什么这一特点使得这个任务可能比寻找对像“英国的货币是什么”这样的事实问题的答案更困难。首先查询是关于“质量差”这是主观的取决于用户对质量的定义。其次查询的重要部分根本不出现在评论中这意味着不能用关键词搜索或释义输入问题来回答。这些特点使得 SubjQA 成为一个真实的数据集可以用来对我们基于评论的 QA 模型进行基准测试因为像图 7-2 中显示的用户生成内容类似于我们可能在野外遇到的内容。 注意 QA 系统通常根据其在响应查询时可以访问的数据的领域进行分类。封闭领域QA 处理关于狭窄主题的问题例如单个产品类别而开放领域QA 处理几乎任何问题例如亚马逊的整个产品目录。一般来说封闭领域 QA 涉及搜索的文档比开放领域情况少。 为了开始让我们从Hugging Face Hub下载数据集。就像我们在第四章中所做的那样我们可以使用get_dataset_config_names()函数来找出哪些子集是可用的 from datasets import get_dataset_config_namesdomains get_dataset_config_names(subjqa) domains[books, electronics, grocery, movies, restaurants, tripadvisor]对于我们的用例我们将专注于构建电子产品领域的 QA 系统。要下载electronics子集我们只需要将该值传递给load_dataset()函数的name参数 from datasets import load_datasetsubjqa load_dataset(subjqa, nameelectronics)与 Hub 上的其他问答数据集一样SubjQA 将每个问题的答案存储为嵌套字典。例如如果我们检查answers列中的一行 print(subjqa[train][answers][1]){text: [Bass is weak as expected, Bass is weak as expected, even with EQ adjusted up], answer_start: [1302, 1302], answer_subj_level: [1, 1], ans_subj_score: [0.5083333253860474, 0.5083333253860474], is_ans_subjective: [True, True]}我们可以看到答案存储在text字段中而起始字符索引则在answer_start中提供。为了更轻松地探索数据集我们将使用flatten()方法展平这些嵌套列并将每个拆分转换为 Pandas 的DataFrame如下所示 import pandas as pddfs {split: dset.to_pandas() for split, dset in subjqa.flatten().items()}for split, df in dfs.items():print(fNumber of questions in {split}: {df[id].nunique()})Number of questions in train: 1295 Number of questions in test: 358 Number of questions in validation: 255请注意数据集相对较小总共只有 1,908 个示例。这模拟了真实世界的情况因为让领域专家标记抽取式 QA 数据集是费时费力的。例如用于法律合同抽取式 QA 的 CUAD 数据集估计价值为 200 万美元以应对标注其 13,000 个示例所需的法律专业知识⁴ SubjQA 数据集中有相当多的列但对于构建我们的 QA 系统来说最有趣的是表 7-1 中显示的那些。 表 7-1. SubjQA 数据集的列名及其描述 列名描述title与每个产品相关联的亚马逊标准识别号ASINquestion问题answers.answer_text标注者标记的评论文本范围answers.answer_start答案范围的起始字符索引context客户评论 让我们专注于这些列并查看一些训练示例。我们可以使用sample()方法选择一个随机样本 qa_cols [title, question, answers.text,answers.answer_start, context] sample_df dfs[train][qa_cols].sample(2, random_state7) sample_df标题问题答案文本答案起始上下文B005DKZTMG键盘轻便吗[这个键盘很紧凑][215]我真的很喜欢这个键盘。我给它 4 颗星因为它没有大写锁定键所以我永远不知道我的大写是否打开。但是就价格而言它确实足够作为一个无线键盘。我手很大这个键盘很紧凑但我没有抱怨。B00AAIPT76电池如何[][]我在购买了第一个备用 gopro 电池后发现它无法保持充电。我对这种产品有非常现实的期望对于充电时间和电池寿命的惊人故事我持怀疑态度但我确实希望电池至少能保持几周的充电并且充电器能像充电器一样工作。在这方面我并不失望。我是一名漂流者发现 gopro 电池很快就用完了所以这次购买解决了这个问题。电池保持了充电在短途旅行上额外的两块电池足够使用在长途旅行上我可以使用我的朋友的 JOOS Orange 来给它们充电。我刚刚购买了一个 newtrent xtreme powerpak期望能用它给这些电池充电所以我不会再用完电了。 从这些例子中我们可以做出一些观察。首先问题在语法上不正确这在电子商务网站的常见 FAQ 部分中是很常见的。其次空的answers.text条目表示“无法回答”的问题其答案无法在评论中找到。最后我们可以使用答案跨度的起始索引和长度来切出对应于答案的评论文本跨度 start_idx sample_df[answers.answer_start].iloc[0][0] end_idx start_idx len(sample_df[answers.text].iloc[0][0]) sample_df[context].iloc[0][start_idx:end_idx]this keyboard is compact接下来让我们通过计算以一些常见起始词开头的问题来了解训练集中有哪些类型的问题 counts {} question_types [What, How, Is, Does, Do, Was, Where, Why]for q in question_types:counts[q] dfs[train][question].str.startswith(q).value_counts()[True]pd.Series(counts).sort_values().plot.barh() plt.title(Frequency of Question Types) plt.show()我们可以看到以“How”、“What”和“Is”开头的问题是最常见的所以让我们看一些例子 for question_type in [How, What, Is]:for question in (dfs[train][dfs[train].question.str.startswith(question_type)].sample(n3, random_state42)[question]):print(question)How is the camera? How do you like the control? How fast is the charger? What is direction? What is the quality of the construction of the bag? What is your impression of the product? Is this how zoom works? Is sound clear? Is it a wireless keyboard?现在我们已经对数据集有了一些了解让我们深入了解 transformers 如何从文本中提取答案。 从文本中提取答案 我们的问答系统首先需要找到一种方法来识别客户评论中的潜在答案作为文本跨度。例如如果我们有一个问题“它防水吗”和评论段落是“这个手表在 30 米深处防水”那么模型应该输出“在 30 米处防水”。为了做到这一点我们需要了解如何 构建监督学习问题。 为问答任务对文本进行标记和编码。 处理超出模型最大上下文大小的长段落。 让我们从如何构建问题开始。 跨度分类 从文本中提取答案的最常见方法是将问题构建为跨度分类任务其中答案跨度的起始和结束标记作为模型需要预测的标签。这个过程在图 7-4 中有所说明。 图 7-4。QA 任务的跨度分类头部 由于我们的训练集相对较小只有 1295 个例子一个好的策略是从已经在大规模 QA 数据集如 SQuAD上进行了微调的语言模型开始。一般来说这些模型具有很强的阅读理解能力并且可以作为构建更准确系统的良好基线。这与之前章节中的方法有些不同在之前的章节中我们通常是从预训练模型开始然后自己微调特定任务的头部。例如在第二章中我们不得不微调分类头部因为类别数量与手头的数据集相关联。对于抽取式问答我们实际上可以从一个经过微调的模型开始因为标签的结构在不同数据集之间保持不变。 您可以通过导航到Hugging Face Hub并在 Models 选项卡上搜索“squad”来找到一系列抽取式 QA 模型图 7-5。 图 7-5。Hugging Face Hub 上一些抽取式 QA 模型的选择 正如您所看到的在撰写本文时有超过 350 个 QA 模型可供选择那么您应该选择哪一个呢一般来说答案取决于各种因素比如您的语料库是单语还是多语以及在生产环境中运行模型的约束。表 7-2 列出了一些提供了良好基础的模型。 表 7-2。在 SQuAD 2.0 上进行微调的基线 Transformer 模型 Transformer描述参数数量SQuAD 2.0 上的F分数MiniLM保留了 99%的性能同时运行速度是 BERT-base 的两倍66M79.5RoBERTa-baseRoBERTa 模型的性能比 BERT 模型更好并且可以在大多数 QA 数据集上使用单个 GPU 进行微调125M83.0ALBERT-XXL在 SQuAD 2.0 上表现出色但计算密集且难以部署235M88.1XLM-RoBERTa-large用于 100 种语言的多语言模型具有强大的零热门性能570M83.8 在本章的目的是我们将使用经过微调的 MiniLM 模型因为它训练速度快可以让我们快速迭代我们将要探索的技术。⁸ 像往常一样我们需要的第一件事是一个标记器来对我们的文本进行编码所以让我们看看这对于 QA 任务是如何工作的。 QA 的文本标记化 为了对我们的文本进行编码我们将像往常一样从Hugging Face Hub加载 MiniLM 模型检查点 from transformers import AutoTokenizermodel_ckpt deepset/minilm-uncased-squad2 tokenizer AutoTokenizer.from_pretrained(model_ckpt)要看到模型的实际效果让我们首先尝试从文本的简短段落中提取答案。在抽取式 QA 任务中输入以问题上下文对的形式提供因此我们将它们都传递给标记器如下所示 question How much music can this hold? context An MP3 is about 1 MB/minute, so about 6000 hours depending on \ file size. inputs tokenizer(question, context, return_tensorspt)在这里我们返回了 PyTorchTensor对象因为我们需要它们来运行模型的前向传递。如果我们将标记化的输入视为表格 input_ids10121292172218920642023…58342006537129461012102token_type_ids000000…111111attention_mask111111…111111 我们可以看到熟悉的input_ids和attention_mask张量而token_type_ids张量指示了输入的哪个部分对应于问题和上下文0 表示问题标记1 表示上下文标记。⁹ 为了了解标记器如何为 QA 任务格式化输入让我们解码input_ids张量 print(tokenizer.decode(inputs[input_ids][0]))[CLS] how much music can this hold? [SEP] an mp3 is about 1 mb / minute, so about 6000 hours depending on file size. [SEP]我们看到对于每个 QA 示例输入采用以下格式 [CLS] question tokens [SEP] context tokens [SEP]其中第一个[SEP]标记的位置由token_type_ids确定。现在我们的文本已经被标记化我们只需要用 QA 头实例化模型并通过前向传递运行输入 import torch from transformers import AutoModelForQuestionAnsweringmodel AutoModelForQuestionAnswering.from_pretrained(model_ckpt)with torch.no_grad():outputs model(**inputs) print(outputs)QuestionAnsweringModelOutput(lossNone, start_logitstensor([[-0.9862, -4.7750,-5.4025, -5.2378, -5.2863, -5.5117, -4.9819, -6.1880,-0.9862, 0.2596, -0.2144, -1.7136, 3.7806, 4.8561, -1.0546, -3.9097,-1.7374, -4.5944, -1.4278, 3.9949, 5.0390, -0.2018, -3.0193, -4.8549,-2.3107, -3.5110, -3.5713, -0.9862]]), end_logitstensor([[-0.9623,-5.4733, -5.0326, -5.1639, -5.4278, -5.5151, -5.1749, -4.6233,-0.9623, -3.7855, -0.8715, -3.7745, -3.0161, -1.1780, 0.1758, -2.7365,4.8934, 0.3046, -3.1761, -3.2762, 0.8937, 5.6606, -0.3623, -4.9554,-3.2531, -0.0914, 1.6211, -0.9623]]), hidden_statesNone, attentionsNone)在这里我们可以看到我们得到了一个QuestionAnsweringModelOutput对象作为 QA 头的输出。正如图 7-4 中所示QA 头对应于一个线性层它获取来自编码器的隐藏状态并计算开始和结束跨度的 logits。¹⁰ 这意味着我们将 QA 视为一种令牌分类类似于我们在第四章中遇到的命名实体识别。要将输出转换为答案跨度我们首先需要获取开始和结束令牌的 logits start_logits outputs.start_logits end_logits outputs.end_logits如果我们比较这些 logits 的形状和输入 ID print(fInput IDs shape: {inputs.input_ids.size()}) print(fStart logits shape: {start_logits.size()}) print(fEnd logits shape: {end_logits.size()})Input IDs shape: torch.Size([1, 28]) Start logits shape: torch.Size([1, 28]) End logits shape: torch.Size([1, 28])我们看到每个输入标记都有两个 logits一个起始和一个结束。如图 7-6 所示较大的正 logits 对应于更有可能成为起始和结束标记的候选标记。在这个例子中我们可以看到模型将最高的起始标记 logits 分配给数字“1”和“6000”这是有道理的因为我们的问题是关于数量的。同样我们看到最高 logits 的结束标记是“minute”和“hours”。 图 7-6。预测的起始和结束标记的 logits得分最高的标记以橙色标出 为了得到最终答案我们可以计算起始和结束标记 logits 的 argmax然后从输入中切片出这个范围。以下代码执行这些步骤并解码结果以便我们可以打印出结果文本 import torchstart_idx torch.argmax(start_logits) end_idx torch.argmax(end_logits) 1 answer_span inputs[input_ids][0][start_idx:end_idx] answer tokenizer.decode(answer_span) print(fQuestion: {question}) print(fAnswer: {answer})Question: How much music can this hold? Answer: 6000 hours太好了成功了在 Transformers 中所有这些预处理和后处理步骤都方便地包装在一个专用的管道中。我们可以通过传递我们的分词器和微调模型来实例化管道如下所示 from transformers import pipelinepipe pipeline(question-answering, modelmodel, tokenizertokenizer) pipe(questionquestion, contextcontext, topk3)[{score: 0.26516005396842957,start: 38,end: 48,answer: 6000 hours},{score: 0.2208300083875656,start: 16,end: 48,answer: 1 MB/minute, so about 6000 hours},{score: 0.10253632068634033,start: 16,end: 27,answer: 1 MB/minute}]除了答案管道还在score字段中返回模型的概率估计通过对 logits 进行 softmax 获得。当我们想要在单个上下文中比较多个答案时这是很方便的。我们还表明通过指定topk参数我们可以让模型预测多个答案。有时可能会有问题没有答案的情况比如 SubjQA 中空的answers.answer_start示例。在这些情况下模型将为[CLS]标记分配高的起始和结束分数管道将这个输出映射为空字符串 pipe(questionWhy is there no data?, contextcontext,handle_impossible_answerTrue){score: 0.9068416357040405, start: 0, end: 0, answer: }注意 在我们的简单示例中我们通过获取相应 logits 的 argmax 来获得起始和结束索引。然而这种启发式方法可能会产生超出范围的答案因为它选择属于问题而不是上下文的标记。在实践中管道计算最佳的起始和结束索引组合受到各种约束的限制比如在范围内要求起始索引在结束索引之前等等。 处理长段落 阅读理解模型面临的一个微妙之处是上下文通常包含的标记比模型的最大序列长度通常最多几百个标记多。如图 7-7 所示SubjQA 训练集中相当一部分包含的问题-上下文对无法适应 MiniLM 的 512 个标记的上下文大小。 图 7-7。SubjQA 训练集中每个问题-上下文对的标记分布 对于其他任务比如文本分类我们简单地截断长文本假设[CLS]标记的嵌入中包含足够的信息来生成准确的预测。然而对于 QA 来说这种策略是有问题的因为问题的答案可能位于上下文的末尾因此会被截断。如图 7-8 所示处理这个问题的标准方法是在输入上应用滑动窗口其中每个窗口包含适合模型上下文的标记段。 图 7-8。滑动窗口如何为长文档创建多个问题-上下文对——第一个条形图对应问题而第二个条形图是每个窗口中捕获的上下文 在 Transformers 中我们可以在分词器中设置return_overflowing_tokensTrue来启用滑动窗口。滑动窗口的大小由max_seq_length参数控制步幅的大小由doc_stride控制。让我们从训练集中抓取第一个例子并定义一个小窗口来说明这是如何工作的 example dfs[train].iloc[0][[question, context]] tokenized_example tokenizer(example[question], example[context],return_overflowing_tokensTrue, max_length100,stride25)在这种情况下我们现在得到了一个input_ids列表每个窗口一个。让我们检查每个窗口中的标记数 for idx, window in enumerate(tokenized_example[input_ids]):print(fWindow #{idx} has {len(window)} tokens)Window #0 has 100 tokens Window #1 has 88 tokens最后我们可以通过解码输入来看到两个窗口重叠的位置 for window in tokenized_example[input_ids]:print(f{tokenizer.decode(window)} \n)[CLS] how is the bass? [SEP] i have had koss headphones in the past, pro 4aa and qz - 99\. the koss portapro is portable and has great bass response. the work great with my android phone and can be rolled up to be carried in my motorcycle jacket or computer bag without getting crunched. they are very light and dont feel heavy or bear down on your ears even after listening to music with them on all day. the sound is [SEP][CLS] how is the bass? [SEP] and dont feel heavy or bear down on your ears even after listening to music with them on all day. the sound is night and day better than any ear - bud could be and are almost as good as the pro 4aa. they are open air headphones so you cannot match the bass to the sealed types, but it comes close. for $ 32, you cannot go wrong. [SEP]现在我们对 QA 模型如何从文本中提取答案有了一些直觉让我们看看构建端到端 QA 管道所需的其他组件。 使用 Haystack 构建 QA 管道 在我们简单的答案提取示例中我们向模型提供了问题和上下文。然而在现实中我们系统的用户只会提供有关产品的问题因此我们需要一种方法从我们语料库中的所有评论中选择相关的段落。做到这一点的一种方法是将给定产品的所有评论连接在一起并将它们作为单个长上下文输入模型。虽然简单但这种方法的缺点是上下文可能变得非常长从而为我们用户的查询引入不可接受的延迟。例如假设平均每个产品有 30 条评论每条评论需要 100 毫秒处理。如果我们需要处理所有评论来得到答案这将导致每个用户查询的平均延迟为 3 秒对于电子商务网站来说太长了 为了处理这一点现代 QA 系统通常基于检索器-阅读器架构它有两个主要组件 检索器 负责为给定查询检索相关文档。检索器通常被分类为稀疏或密集。稀疏检索器使用词频来表示每个文档和查询形成稀疏向量。¹¹然后通过计算向量的内积来确定查询和文档的相关性。另一方面密集检索器使用编码器如 transformers将查询和文档表示为上下文化的嵌入密集向量。这些嵌入编码语义含义并允许密集检索器通过理解查询的内容来提高搜索准确性。 阅读器 负责从检索器提供的文档中提取答案。阅读器通常是一个阅读理解模型尽管在本章末尾我们将看到可以生成自由形式答案的模型示例。 如图 7-9 所示还可以有其他组件对检索器获取的文档或阅读器提取的答案进行后处理。例如检索到的文档可能需要重新排名以消除可能混淆阅读器的嘈杂或无关的文档。类似地当正确答案来自长文档中的各个段落时通常需要对阅读器的答案进行后处理。 图 7-9。现代 QA 系统的检索器-阅读器架构 为了构建我们的 QA 系统我们将使用由deepset开发的Haystack库deepset 是一家专注于 NLP 的德国公司。Haystack 基于检索器-阅读器架构抽象了构建这些系统所涉及的大部分复杂性并与 Transformers 紧密集成。 除了检索器和阅读器之外在构建 Haystack 的 QA 管道时还涉及另外两个组件 文档存储 存储文档和元数据的面向文档的数据库这些文档和元数据在查询时提供给检索器 管道 将 QA 系统的所有组件结合在一起以实现自定义查询流程合并来自多个检索器的文档等 在本节中我们将看看如何使用这些组件快速构建原型 QA 管道。稍后我们将探讨如何提高其性能。 警告 本章是使用 Haystack 库的 0.9.0 版本编写的。在版本 0.10.0中重新设计了管道和评估 API以便更容易检查检索器或阅读器是否影响性能。要查看使用新 API 的本章代码是什么样子请查看GitHub 存储库。 初始化文档存储 在 Haystack 中有各种可供选择的文档存储并且每个文档存储都可以与一组专用的检索器配对。这在表 7-3 中有所说明其中显示了每个可用文档存储的稀疏TF-IDF、BM25和密集嵌入、DPR检索器的兼容性。我们将在本章后面解释所有这些首字母缩略词的含义。 表 7-3。Haystack 检索器和文档存储的兼容性 内存ElasticsearchFAISSMilvusTF-IDF是是否否BM25否是否否嵌入是是是是DPR是是是是 由于本章将探讨稀疏和密集检索器我们将使用与两种检索器类型兼容的ElasticsearchDocumentStore。Elasticsearch 是一种能够处理各种数据类型的搜索引擎包括文本、数字、地理空间、结构化和非结构化数据。它存储大量数据并能够快速进行全文搜索过滤因此特别适用于开发问答系统。它还具有成为基础设施分析行业标准的优势因此您的公司很有可能已经有一个集群可以使用。 要初始化文档存储我们首先需要下载并安装 Elasticsearch。通过按照 Elasticsearch 的指南¹² 我们可以使用wget获取 Linux 的最新版本并使用tar shell 命令解压缩它 url https://artifacts.elastic.co/downloads/elasticsearch/\ elasticsearch-7.9.2-linux-x86_64.tar.gz !wget -nc -q {url} !tar -xzf elasticsearch-7.9.2-linux-x86_64.tar.gz接下来我们需要启动 Elasticsearch 服务器。由于我们在 Jupyter 笔记本中运行本书中的所有代码我们需要使用 Python 的Popen()函数来生成一个新的进程。在此过程中让我们还使用chown shell 命令在后台运行子进程 import os from subprocess import Popen, PIPE, STDOUT# Run Elasticsearch as a background process !chown -R daemon:daemon elasticsearch-7.9.2 es_server Popen(args[elasticsearch-7.9.2/bin/elasticsearch],stdoutPIPE, stderrSTDOUT, preexec_fnlambda: os.setuid(1)) # Wait until Elasticsearch has started !sleep 30在Popen()函数中args指定我们希望执行的程序而stdoutPIPE创建一个新的管道用于标准输出stderrSTDOUT收集相同管道中的错误。preexec_fn参数指定我们希望使用的子进程的 ID。默认情况下Elasticsearch 在本地端口 9200 上运行因此我们可以通过向localhost发送 HTTP 请求来测试连接 !curl -X GET localhost:9200/?pretty{name : 96938eee37cd,cluster_name : docker-cluster,cluster_uuid : ABGDdvbbRWmMb9Umz79HbA,version : {number : 7.9.2,build_flavor : default,build_type : docker,build_hash : d34da0ea4a966c4e49417f2da2f244e3e97b4e6e,build_date : 2020-09-23T00:45:33.626720Z,build_snapshot : false,lucene_version : 8.6.2,minimum_wire_compatibility_version : 6.8.0,minimum_index_compatibility_version : 6.0.0-beta1},tagline : You Know, for Search }现在我们的 Elasticsearch 服务器已经启动运行接下来要做的事情是实例化文档存储 from haystack.document_store.elasticsearch import ElasticsearchDocumentStore# Return the document embedding for later use with dense retriever document_store ElasticsearchDocumentStore(return_embeddingTrue)默认情况下ElasticsearchDocumentStore在 Elasticsearch 上创建两个索引一个称为document用于你猜对了存储文档另一个称为label用于存储注释的答案跨度。现在我们将使用 SubjQA 评论填充document索引Haystack 的文档存储期望一个带有text和meta键的字典列表如下所示 {text: the-context,meta: {field_01: additional-metadata,field_02: additional-metadata,...} }meta中的字段可用于在检索期间应用过滤器。为了我们的目的我们将包括 SubjQA 的item_id和q_review_id列以便我们可以按产品和问题 ID 进行过滤以及相应的训练拆分。然后我们可以循环遍历每个DataFrame中的示例并使用write_documents()方法将它们添加到索引中如下所示 for split, df in dfs.items():# Exclude duplicate reviewsdocs [{text: row[context],meta:{item_id: row[title], question_id: row[id],split: split}}for _,row in df.drop_duplicates(subsetcontext).iterrows()]document_store.write_documents(docs, indexdocument)print(fLoaded {document_store.get_document_count()} documents)Loaded 1615 documents太好了我们已经将所有的评论加载到了一个索引中要搜索索引我们需要一个检索器因此让我们看看如何为 Elasticsearch 初始化一个检索器。 初始化检索器 Elasticsearch 文档存储可以与 Haystack 检索器中的任何一种配对因此让我们首先使用基于 BM25 的稀疏检索器简称“Best Match 25”。BM25 是经典的词项频率-逆文档频率TF-IDF算法的改进版本它将问题和上下文表示为可以在 Elasticsearch 上高效搜索的稀疏向量。BM25 分数衡量了匹配文本与搜索查询的相关程度并通过迅速饱和 TF 值和规范化文档长度来改进 TF-IDF以便短文档优于长文档。¹³ 在 Haystack 中默认情况下使用 BM25 检索器在ElasticsearchRetriever中因此让我们通过指定我们希望搜索的文档存储来初始化这个类 from haystack.retriever.sparse import ElasticsearchRetrieveres_retriever ElasticsearchRetriever(document_storedocument_store)接下来让我们看一下训练集中单个电子产品的简单查询。对于像我们这样基于评论的问答系统将查询限制在单个项目是很重要的否则检索器会返回与用户查询无关的产品评论。例如询问“相机质量如何”如果没有产品过滤器可能会返回关于手机的评论而用户可能是在询问特定笔记本电脑相机的情况。我们的数据集中的 ASIN 值本身有点神秘但我们可以使用在线工具如amazon ASIN或者简单地将item_id的值附加到www.amazon.com/dp/ URL 来解密它们。以下项目 ID 对应于亚马逊的 Fire 平板电脑之一所以让我们使用检索器的retrieve()方法来询问它是否适合阅读 item_id B0074BW614 query Is it good for reading? retrieved_docs es_retriever.retrieve(queryquery, top_k3, filters{item_id:[item_id], split:[train]})在这里我们已经指定了top_k参数返回多少个文档并在我们的文档的meta字段中包含的item_id和split键上应用了过滤器。retrieved_docs的每个元素都是一个 Haystack Document对象用于表示文档并包括检索器的查询分数以及其他元数据。让我们看一下其中一个检索到的文档 print(retrieved_docs[0]){text: This is a gift to myself. I have been a kindle user for 4 years and this is my third one. I never thought I would want a fire for I mainly use it for book reading. I decided to try the fire for when I travel I take my laptop, my phone and my iPod classic. I love my iPod but watching movies on the plane with it can be challenging because it is so small. Laptops battery life is not as good as the Kindle. So the Fire combines for me what I needed all three to do. So far so good., score: 6.243799, probability: 0.6857824513476455, question: None, meta: {item_id: B0074BW614, question_id: 868e311275e26dbafe5af70774a300f3, split: train}, embedding: None, id: 252e83e25d52df7311d597dc89eef9f6}除了文档的文本我们还可以看到 Elasticsearch 为其与查询的相关性计算的score较大的分数意味着更好的匹配。在幕后Elasticsearch 依赖于Lucene进行索引和搜索因此默认情况下它使用 Lucene 的practical scoring function。您可以在Elasticsearch 文档中找到得分函数背后的细节但简而言之它首先通过应用布尔测试文档是否与查询匹配来过滤候选文档然后应用基于将文档和查询表示为向量的相似度度量。 现在我们有了检索相关文档的方法接下来我们需要的是从中提取答案的方法。这就是读取器的作用让我们看看如何在 Haystack 中加载我们的 MiniLM 模型。 初始化读取器 在 Haystack 中有两种类型的读取器可以用来从给定的上下文中提取答案 FARMReader 基于 deepset 的FARM框架进行 transformers 的微调和部署。与使用 Transformers 训练的模型兼容并且可以直接从 Hugging Face Hub 加载模型。 TransformersReader 基于 Transformers 的 QA 流水线。适用于仅运行推理。 尽管两种读取器都以相同的方式处理模型的权重但在转换预测以生成答案方面存在一些差异 在 Transformers 中QA 管道使用 softmax 在每个段落中对开始和结束的 logits 进行归一化。这意味着只有在从同一段落中提取的答案之间才有意义比较得分其中概率总和为 1。例如来自一个段落的答案得分为 0.9 并不一定比另一个段落中的得分为 0.8 好。在 FARM 中logits 没有被归一化因此可以更容易地比较跨段落的答案。 TransformersReader有时会预测相同的答案两次但得分不同。如果答案跨越两个重叠的窗口这可能会发生。在 FARM 中这些重复项会被删除。 由于我们将在本章后面对读取器进行微调我们将使用FARMReader。与 Transformers 一样要加载模型我们只需要在 Hugging Face Hub 上指定 MiniLM 检查点以及一些特定于 QA 的参数 from haystack.reader.farm import FARMReadermodel_ckpt deepset/minilm-uncased-squad2 max_seq_length, doc_stride 384, 128 reader FARMReader(model_name_or_pathmodel_ckpt, progress_barFalse,max_seq_lenmax_seq_length, doc_stridedoc_stride,return_no_answerTrue)注意 也可以直接在 Transformers 中微调阅读理解模型然后加载到TransformersReader中进行推理。有关如何进行微调的详细信息请参阅库的文档中的问答教程。 在FARMReader中滑动窗口的行为由我们在标记器中看到的max_seq_length和doc_stride参数控制。这里我们使用了 MiniLM 论文中的值。现在让我们在之前的简单示例上测试一下读取器 print(reader.predict_on_texts(questionquestion, texts[context], top_k1)){query: How much music can this hold?, no_ans_gap: 12.648084878921509, answers: [{answer: 6000 hours, score: 10.69961929321289, probability: 0.3988136053085327, context: An MP3 is about 1 MB/minute, so about 6000 hours depending on file size., offset_start: 38, offset_end: 48, offset_start_in_doc: 38, offset_end_in_doc: 48, document_id: e344757014e804eff50faa3ecf1c9c75}]}太好了读取器似乎正在按预期工作——接下来让我们使用 Haystack 的一个管道将所有组件联系在一起。 将所有内容放在一起 Haystack 提供了一个Pipeline抽象允许我们将检索器、读取器和其他组件组合成一个图可以根据每个用例轻松定制。还有预定义的管道类似于 Transformers 中的管道但专门为 QA 系统设计。在我们的案例中我们对提取答案感兴趣所以我们将使用ExtractiveQAPipeline它以单个检索器-读取器对作为其参数 from haystack.pipeline import ExtractiveQAPipelinepipe ExtractiveQAPipeline(reader, es_retriever)每个Pipeline都有一个run()方法指定查询流程应如何执行。对于ExtractiveQAPipeline我们只需要传递query用top_k_retriever指定要检索的文档数量用top_k_reader指定要从这些文档中提取的答案数量。在我们的案例中我们还需要使用filters参数指定对项目 ID 的过滤器就像我们之前对检索器所做的那样。让我们再次运行一个关于亚马逊 Fire 平板电脑的简单示例但这次返回提取的答案 n_answers 3 preds pipe.run(queryquery, top_k_retriever3, top_k_readern_answers,filters{item_id: [item_id], split:[train]})print(fQuestion: {preds[query]} \n) for idx in range(n_answers):print(fAnswer {idx1}: {preds[answers][idx][answer]})print(fReview snippet: ...{preds[answers][idx][context]}...)print(\n\n)Question: Is it good for reading?Answer 1: I mainly use it for book reading Review snippet: ... is my third one. I never thought I would want a fire for I mainly use it for book reading. I decided to try the fire for when I travel I take my la...Answer 2: the larger screen compared to the Kindle makes for easier reading Review snippet: ...ght enough that I can hold it to read, but the larger screen compared to the Kindle makes for easier reading. I love the color, something I never thou...Answer 3: it is great for reading books when no light is available Review snippet: ...ecoming addicted to hers! Our son LOVES it and it is great for reading books when no light is available. Amazing sound but I suggest good headphones t...太好了现在我们有了一个端到端的 Amazon 产品评论 QA 系统这是一个很好的开始但请注意第二和第三个答案更接近实际问题。为了做得更好我们需要一些指标来量化检索器和读取器的性能。我们接下来将看一下这一点。 改进我们的 QA 管道 尽管最近关于 QA 的研究大部分集中在改进阅读理解模型上但实际上如果检索器一开始就找不到相关文档你的读取器有多好并不重要特别是检索器为整个 QA 系统的性能设定了一个上限因此确保它做得好很重要。考虑到这一点让我们首先介绍一些常见的指标来评估检索器以便我们可以比较稀疏和密集表示的性能。 评估检索器 评估检索器的常见指标是召回率它衡量了检索到的所有相关文档的比例。在这种情况下“相关”只是指答案是否出现在文本段落中因此给定一组问题我们可以通过计算答案出现在检索器返回的前k个文档中的次数来计算召回率。 在 Haystack 中有两种评估检索器的方法 使用检索器内置的eval()方法。这可以用于开放域和封闭域的问答但不能用于像 SubjQA 这样的数据集其中每个文档都与一个产品配对我们需要为每个查询按产品 ID 进行过滤。 构建一个自定义的Pipeline将检索器与EvalRetriever类结合在一起。这样可以实现自定义指标和查询流程。 注意 与召回率相辅相成的指标是平均精度mAP它奖励能够将正确答案排在文档排名中较高位置的检索器。 由于我们需要对每个产品进行召回率评估然后在所有产品中进行聚合我们将选择第二种方法。流水线图中的每个节点都代表一个通过run()方法获取一些输入并产生一些输出的类 class PipelineNode:def __init__(self):self.outgoing_edges 1def run(self, **kwargs):...return (outputs, outgoing_edge_name)这里的kwargs对应于图中前一个节点的输出在run()方法中进行操作以返回下一个节点的输出的元组以及出边的名称。唯一的其他要求是包括一个outgoing_edges属性指示节点的输出数量在大多数情况下outgoing_edges1除非流水线中有根据某些标准路由输入的分支。 在我们的情况下我们需要一个节点来评估检索器因此我们将使用EvalRetriever类其run()方法跟踪哪些文档具有与地面真相匹配的答案。有了这个类我们可以通过在代表检索器本身的节点之后添加评估节点来构建Pipeline图 from haystack.pipeline import Pipeline from haystack.eval import EvalDocumentsclass EvalRetrieverPipeline:def __init__(self, retriever):self.retriever retrieverself.eval_retriever EvalDocuments()pipe Pipeline()pipe.add_node(componentself.retriever, nameESRetriever,inputs[Query])pipe.add_node(componentself.eval_retriever, nameEvalRetriever,inputs[ESRetriever])self.pipeline pipepipe EvalRetrieverPipeline(es_retriever)注意每个节点都有一个name和一个inputs列表。在大多数情况下每个节点都有一个单独的出边因此我们只需要在inputs中包含前一个节点的名称。 现在我们有了评估流水线我们需要传递一些查询及其相应的答案。为此我们将答案添加到我们文档存储中的专用label索引。Haystack 提供了一个Label对象以标准化的方式表示答案跨度及其元数据。为了填充label索引我们将首先通过循环遍历测试集中的每个问题并提取匹配的答案和附加的元数据来创建Label对象的列表 from haystack import Labellabels [] for i, row in dfs[test].iterrows():# Metadata used for filtering in the Retrievermeta {item_id: row[title], question_id: row[id]}# Populate labels for questions with answersif len(row[answers.text]):for answer in row[answers.text]:label Label(questionrow[question], answeranswer, idi, originrow[id],metameta, is_correct_answerTrue, is_correct_documentTrue,no_answerFalse)labels.append(label)# Populate labels for questions without answerselse:label Label(questionrow[question], answer, idi, originrow[id],metameta, is_correct_answerTrue, is_correct_documentTrue,no_answerTrue)labels.append(label)如果我们查看其中一个标签 print(labels[0]){id: e28f5e62-85e8-41b2-8a34-fbff63b7a466, created_at: None, updated_at: None, question: What is the tonal balance of these headphones?, answer: I have been a headphone fanatic for thirty years, is_correct_answer: True, is_correct_document: True, origin: d0781d13200014aa25860e44da9d5ea7, document_id: None, offset_start_in_doc: None, no_answer: False, model_id: None, meta: {item_id: B00001WRSJ, question_id: d0781d13200014aa25860e44da9d5ea7}}我们可以看到问题-答案对以及一个包含唯一问题 ID 的origin字段以便我们可以根据问题过滤文档存储。我们还将产品 ID 添加到meta字段中以便我们可以按产品筛选标签。现在我们有了标签我们可以按以下方式将它们写入 Elasticsearch 上的label索引 document_store.write_labels(labels, indexlabel) print(fLoaded {document_store.get_label_count(indexlabel)} \ question-answer pairs)Loaded 358 question-answer pairs接下来我们需要建立一个映射将我们的问题 ID 和相应的答案传递给流水线。为了获取所有标签我们可以使用文档存储中的get_all_labels_aggregated()方法该方法将聚合与唯一 ID 相关联的所有问题-答案对。该方法返回一个MultiLabel对象的列表但在我们的情况下由于我们按问题 ID 进行过滤所以只会得到一个元素。我们可以按以下方式建立一个聚合标签列表 labels_agg document_store.get_all_labels_aggregated(indexlabel,open_domainTrue,aggregate_by_meta[item_id] ) print(len(labels_agg))330通过查看其中一个标签我们可以看到与给定问题相关联的所有答案都聚合在一个multiple_answers字段中 print(labels_agg[109]){question: How does the fan work?, multiple_answers: [the fan is really really good, the fan itself isnt super loud. There is an adjustable dial to change fan speed], is_correct_answer: True, is_correct_document: True, origin: 5a9b7616541f700f103d21f8ad41bc4b, multiple_document_ids: [None, None], multiple_offset_start_in_docs: [None, None], no_answer: False, model_id: None, meta: {item_id: B002MU1ZRS}}现在我们有了评估检索器的所有要素让我们定义一个函数将与每个产品相关联的每个问题-答案对传递到评估流水线中并在我们的pipe对象中跟踪正确的检索 def run_pipeline(pipeline, top_k_retriever10, top_k_reader4):for l in labels_agg:_ pipeline.pipeline.run(queryl.question,top_k_retrievertop_k_retriever,top_k_readertop_k_reader,top_k_eval_documentstop_k_retriever,labelsl,filters{item_id: [l.meta[item_id]], split: [test]})run_pipeline(pipe, top_k_retriever3) print(fRecall3: {pipe.eval_retriever.recall:.2f})Recall3: 0.95太好了它起作用了请注意我们选择了一个特定的值作为top_k_retriever以指定要检索的文档数。一般来说增加这个参数将提高召回率但会增加向读者提供更多文档并减慢端到端流程的速度。为了指导我们选择哪个值我们将创建一个循环遍历几个k值并计算每个k在整个测试集上的召回率的函数 def evaluate_retriever(retriever, topk_values [1,3,5,10,20]):topk_results {}for topk in topk_values:# Create Pipelinep EvalRetrieverPipeline(retriever)# Loop over each question-answers pair in test setrun_pipeline(p, top_k_retrievertopk)# Get metricstopk_results[topk] {recall: p.eval_retriever.recall}return pd.DataFrame.from_dict(topk_results, orientindex)es_topk_df evaluate_retriever(es_retriever)如果我们绘制结果我们可以看到随着k的增加召回率如何提高 def plot_retriever_eval(dfs, retriever_names):fig, ax plt.subplots()for df, retriever_name in zip(dfs, retriever_names):df.plot(yrecall, axax, labelretriever_name)plt.xticks(df.index)plt.ylabel(Top-k Recall)plt.xlabel(k)plt.show()plot_retriever_eval([es_topk_df], [BM25])从图中可以看出在k 5附近有一个拐点从k 10开始我们几乎可以完美地召回。现在让我们来看看使用密集向量技术检索文档。 密集通道检索 我们已经看到当我们的稀疏检索器返回k 10个文档时我们几乎可以完美地召回但在较小的k值上我们能做得更好吗这样做的优势在于我们可以向读者传递更少的文档从而减少我们问答流程的总体延迟。像 BM25 这样的稀疏检索器的一个众所周知的局限性是如果用户查询包含与评论完全不匹配的术语它们可能无法捕获相关文档。一个有前途的替代方案是使用密集嵌入来表示问题和文档当前的技术水平是一种被称为密集通道检索DPR的架构。¹⁴ DPR 背后的主要思想是使用两个 BERT 模型作为问题和段落的编码器。如图 7-10 所示这些编码器将输入文本映射到[CLS]标记的d维向量表示。 图 7-10。DPR 的双编码器架构用于计算文档和查询的相关性 在 Haystack 中我们可以像为 BM25 那样初始化 DPR 的检索器。除了指定文档存储外我们还需要选择用于问题和段落的 BERT 编码器。通过给它们提供具有相关正面段落和不相关负面段落的问题进行训练这些编码器被训练目标是学习相关的问题-段落对具有更高的相似性。对于我们的用例我们将使用已经以这种方式在 NQ 语料库上进行了微调的编码器 from haystack.retriever.dense import DensePassageRetrieverdpr_retriever DensePassageRetriever(document_storedocument_store,query_embedding_modelfacebook/dpr-question_encoder-single-nq-base,passage_embedding_modelfacebook/dpr-ctx_encoder-single-nq-base,embed_titleFalse)在这里我们还设置了embed_titleFalse因为连接文档的标题即item_id不会提供任何额外信息因为我们按产品进行过滤。一旦我们初始化了密集检索器下一步是迭代我们 Elasticsearch 索引中的所有索引文档并应用编码器来更新嵌入表示。这可以通过以下方式完成 document_store.update_embeddings(retrieverdpr_retriever)我们现在准备好了我们可以像为 BM25 那样评估密集检索器并比较前k个召回率 dpr_topk_df evaluate_retriever(dpr_retriever) plot_retriever_eval([es_topk_df, dpr_topk_df], [BM25, DPR])在这里我们可以看到DPR 在召回率上并没有比 BM25 提供提升并且在k 3左右饱和。 提示 通过使用 Facebook 的FAISS 库作为文档存储可以加速嵌入的相似性搜索。同样通过在目标领域进行微调可以提高 DPR 检索器的性能。如果您想了解如何微调 DPR请查看 Haystack 的教程。 现在我们已经探讨了检索器的评估让我们转而评估读者。 评估读者 在抽取式问答中有两个主要的指标用于评估读者 精确匹配EM 一个二进制度量如果预测答案中的字符与真实答案完全匹配则 EM 1否则 EM 0。如果不期望有答案那么如果模型预测任何文本EM 0。 F[1]-score 度量精确度和召回率的调和平均值。 让我们通过从 FARM 导入一些辅助函数并将它们应用于一个简单的例子来看看这些度量是如何工作的 from farm.evaluation.squad_evaluation import compute_f1, compute_exactpred about 6000 hours label 6000 hours print(fEM: {compute_exact(label, pred)}) print(fF1: {compute_f1(label, pred)})EM: 0 F1: 0.8在底层这些函数首先通过去除标点修复空白和转换为小写来对预测和标签进行规范化。然后对规范化的字符串进行词袋式标记化最后在标记级别计算度量。从这个简单的例子中我们可以看到 EM 比F[1]-score 严格得多向预测中添加一个标记会使 EM 为零。另一方面F[1]-score 可能无法捕捉真正不正确的答案。例如如果我们的预测答案范围是“大约 6000 美元”那么我们得到 pred about 6000 dollars print(fEM: {compute_exact(label, pred)}) print(fF1: {compute_f1(label, pred)})EM: 0 F1: 0.4因此仅依赖F[1]-score 是误导性的跟踪这两个度量是在权衡低估EM和高估F[1]-score模型性能之间的一个好策略。 一般来说每个问题可能有多个有效答案因此这些度量是针对评估集中的每个问题-答案对计算的并且从所有可能的答案中选择最佳分数。然后通过对每个问题-答案对的单独分数进行平均来获得模型的整体 EM 和F[1]分数。 为了评估阅读器我们将创建一个包含两个节点的新管道一个阅读器节点和一个用于评估阅读器的节点。我们将使用EvalReader类该类获取阅读器的预测并计算相应的 EM 和F[1]分数。为了与 SQuAD 评估进行比较我们将使用EvalAnswers中存储的top_1_em和top_1_f1度量来获取每个查询的最佳答案 from haystack.eval import EvalAnswersdef evaluate_reader(reader):score_keys [top_1_em, top_1_f1]eval_reader EvalAnswers(skip_incorrect_retrievalFalse)pipe Pipeline()pipe.add_node(componentreader, nameQAReader, inputs[Query])pipe.add_node(componenteval_reader, nameEvalReader, inputs[QAReader])for l in labels_agg:doc document_store.query(l.question,filters{question_id:[l.origin]})_ pipe.run(queryl.question, documentsdoc, labelsl)return {k:v for k,v in eval_reader.__dict__.items() if k in score_keys}reader_eval {} reader_eval[Fine-tune on SQuAD] evaluate_reader(reader)请注意我们指定了skip_incorrect_retrievalFalse。这是为了确保检索器始终将上下文传递给阅读器就像在 SQuAD 评估中一样。现在我们已经通过阅读器运行了每个问题让我们打印分数 def plot_reader_eval(reader_eval):fig, ax plt.subplots()df pd.DataFrame.from_dict(reader_eval)df.plot(kindbar, ylabelScore, rot0, axax)ax.set_xticklabels([EM, F1])plt.legend(locupper left)plt.show()plot_reader_eval(reader_eval)好吧看起来经过精细调整的模型在 SubjQA 上的表现明显比在 SQuAD 2.0 上差MiniLM 在 SQuAD 2.0 上的 EM 和F[1]分别为 76.1 和 79.5。性能下降的一个原因是客户评论与 SQuAD 2.0 数据集生成的维基百科文章非常不同它们使用的语言通常是非正式的。另一个因素可能是我们数据集固有的主观性其中问题和答案都与维基百科中包含的事实信息不同。让我们看看如何在数据集上进行精细调整以获得更好的领域自适应结果。 领域自适应 尽管在 SQuAD 上进行了精细调整的模型通常会很好地推广到其他领域但我们发现对于 SubjQA我们的模型的 EM 和F[1]分数要比 SQuAD 差得多。这种泛化失败也在其他抽取式 QA 数据集中观察到并且被认为是证明了 Transformer 模型特别擅长过度拟合 SQuAD 的证据。(15)改进阅读器的最直接方法是在 SubjQA 训练集上进一步对我们的 MiniLM 模型进行精细调整。FARMReader有一个train()方法专门用于此目的并且期望数据以 SQuAD JSON 格式提供其中所有问题-答案对都被分组在一起如图 7-11 所示。 图 7-11. SQuAD JSON 格式的可视化 这是一个非常复杂的数据格式因此我们需要一些函数和一些 Pandas 魔术来帮助我们进行转换。我们需要做的第一件事是实现一个可以创建与每个产品 ID 相关联的paragraphs数组的函数。该数组中的每个元素包含单个上下文即评论和问题-答案对的qas数组。以下是一个构建paragraphs数组的函数 def create_paragraphs(df):paragraphs []id2context dict(zip(df[review_id], df[context]))for review_id, review in id2context.items():qas []# Filter for all question-answer pairs about a specific contextreview_df df.query(freview_id {review_id})id2question dict(zip(review_df[id], review_df[question]))# Build up the qas arrayfor qid, question in id2question.items():# Filter for a single question IDquestion_df df.query(fid {qid}).to_dict(orientlist)ans_start_idxs question_df[answers.answer_start][0].tolist()ans_text question_df[answers.text][0].tolist()# Fill answerable questionsif len(ans_start_idxs):answers [{text: text, answer_start: answer_start}for text, answer_start in zip(ans_text, ans_start_idxs)]is_impossible Falseelse:answers []is_impossible True# Add question-answer pairs to qasqas.append({question: question, id: qid,is_impossible: is_impossible, answers: answers})# Add context and question-answer pairs to paragraphsparagraphs.append({qas: qas, context: review})return paragraphs现在当我们将与单个产品 ID 相关联的DataFrame的行应用于 SQuAD 格式时 product dfs[train].query(title B00001P4ZH) create_paragraphs(product)[{qas: [{question: How is the bass?,id: 2543d296da9766d8d17d040ecc781699,is_impossible: True,answers: []}],context: I have had Koss headphones ...,id: d476830bf9282e2b9033e2bb44bbb995,is_impossible: False,answers: [{text: Bass is weak as expected, answer_start: 1302},{text: Bass is weak as expected, even with EQ adjusted up,answer_start: 1302}]}],context: To anyone who hasn\t tried all ...},{qas: [{question: How is the bass?,id: 455575557886d6dfeea5aa19577e5de4,is_impossible: False,answers: [{text: The only fault in the sound is the bass,answer_start: 650}]}],context: I have had many sub-$100 headphones ...}]最后一步是将此函数应用于每个拆分的DataFrame中的每个产品 ID。以下convert_to_squad()函数可以做到这一点并将结果存储在electronics-{split}.json文件中 import jsondef convert_to_squad(dfs):for split, df in dfs.items():subjqa_data {}# Create paragraphs for each product IDgroups (df.groupby(title).apply(create_paragraphs).to_frame(nameparagraphs).reset_index())subjqa_data[data] groups.to_dict(orientrecords)# Save the result to diskwith open(felectronics-{split}.json, w, encodingutf-8) as f:json.dump(subjqa_data, f)convert_to_squad(dfs)现在我们已经将拆分格式正确让我们通过指定训练和开发拆分的位置以及保存微调模型的位置来微调我们的阅读器 train_filename electronics-train.json dev_filename electronics-validation.jsonreader.train(data_dir., use_gpuTrue, n_epochs1, batch_size16,train_filenametrain_filename, dev_filenamedev_filename)阅读器微调后让我们现在将其在测试集上的性能与我们的基线模型进行比较 reader_eval[Fine-tune on SQuAD SubjQA] evaluate_reader(reader) plot_reader_eval(reader_eval)哇领域适应将我们的 EM 分数提高了六倍以上并且F[1]-score 增加了一倍多此时您可能会想知道为什么我们不直接在 SubjQA 训练集上对预训练的语言模型进行微调。一个原因是 SubjQA 中只有 1,295 个训练示例而 SQuAD 有超过 100,000 个因此我们可能会遇到过拟合的挑战。尽管如此让我们看看天真的微调会产生什么结果。为了公平比较我们将使用与在 SQuAD 上微调基线时使用的相同语言模型。与以前一样我们将使用FARMReader加载模型 minilm_ckpt microsoft/MiniLM-L12-H384-uncased minilm_reader FARMReader(model_name_or_pathminilm_ckpt, progress_barFalse,max_seq_lenmax_seq_length, doc_stridedoc_stride,return_no_answerTrue)接下来我们进行一轮微调 minilm_reader.train(data_dir., use_gpuTrue, n_epochs1, batch_size16,train_filenametrain_filename, dev_filenamedev_filename)并在测试集上进行评估 reader_eval[Fine-tune on SubjQA] evaluate_reader(minilm_reader) plot_reader_eval(reader_eval)我们可以看到直接在 SubjQA 上对语言模型进行微调的结果比在 SQuAD 和 SubjQA 上进行微调的性能要差得多。 警告 处理小数据集时最佳做法是在评估 Transformer 时使用交叉验证因为它们可能容易过拟合。您可以在FARM 存储库中找到如何使用 SQuAD 格式的数据集执行交叉验证的示例。 评估整个 QA 管道 现在我们已经了解了如何分别评估阅读器和检索器组件让我们将它们联系在一起以衡量我们管道的整体性能。为此我们需要为我们的检索器管道增加阅读器及其评估的节点。我们已经看到在k 10时我们几乎可以完美地召回因此我们可以固定这个值并评估这对阅读器性能的影响因为现在它将收到每个查询的多个上下文而不是 SQuAD 风格的评估 # Initialize retriever pipeline pipe EvalRetrieverPipeline(es_retriever) # Add nodes for reader eval_reader EvalAnswers() pipe.pipeline.add_node(componentreader, nameQAReader,inputs[EvalRetriever]) pipe.pipeline.add_node(componenteval_reader, nameEvalReader,inputs[QAReader]) # Evaluate! run_pipeline(pipe) # Extract metrics from reader reader_eval[QA Pipeline (top-1)] {k:v for k,v in eval_reader.__dict__.items()if k in [top_1_em, top_1_f1]}然后我们可以将模型预测文档中检索器返回的答案的前 1 个 EM 和F[1]分数与图 7-12 中的进行比较。 图 7-12。EM 和F[1]分数的阅读器与整个 QA 管道的比较 从这个图中我们可以看到检索器对整体性能的影响。特别是与匹配问题-上下文对相比整体性能有所下降这是在 SQuAD 风格的评估中所做的。这可以通过增加阅读器被允许预测的可能答案数量来避免。 到目前为止我们只从上下文中提取了答案范围但一般情况下答案的各个部分可能分散在整个文档中我们希望我们的模型能够将这些片段综合成一个连贯的答案。让我们看看如何使用生成式 QA 来成功完成这项任务。 超越抽取式 QA 在文档中提取答案作为文本段的一个有趣的替代方法是使用预训练语言模型生成它们。这种方法通常被称为抽象或生成型 QA它有潜力产生更好措辞的答案综合了多个段落中的证据。虽然这种方法比抽取式 QA 不够成熟但这是一个快速发展的研究领域所以很可能在您阅读本文时这些方法已经被广泛应用于工业中在本节中我们将简要介绍当前的技术水平检索增强生成RAG。 RAG 通过将读者替换为生成器并使用 DPR 作为检索器来扩展了我们在本章中看到的经典检索器-阅读器架构。生成器是一个预训练的序列到序列变换器如 T5 或 BART它接收来自 DPR 的文档的潜在向量然后根据查询和这些文档迭代生成答案。由于 DPR 和生成器是可微分的整个过程可以端到端地进行微调如图 7-13 所示。 图 7-13。RAG 架构用于对检索器和生成器进行端到端微调由 Ethan Perez 提供 为了展示 RAG 的工作原理我们将使用之前的DPRetriever因此我们只需要实例化一个生成器。有两种类型的 RAG 模型可供选择 RAG-Sequence 使用相同的检索文档来生成完整的答案。特别是从检索器中获取的前k个文档被馈送到生成器生成器为每个文档产生一个输出序列然后对结果进行边际化以获得最佳答案。 RAG-Token 可以使用不同的文档来生成答案中的每个标记。这允许生成器从多个文档中综合证据。 由于 RAG-Token 模型往往比 RAG-Sequence 模型表现更好我们将使用在 NQ 上进行微调的标记模型作为我们的生成器。在 Haystack 中实例化生成器类似于实例化阅读器但是我们不是为上下文中的滑动窗口指定max_seq_length和doc_stride参数而是指定控制文本生成的超参数 from haystack.generator.transformers import RAGeneratorgenerator RAGenerator(model_name_or_pathfacebook/rag-token-nq,embed_titleFalse, num_beams5)这里的num_beams指定了在波束搜索中使用的波束数量文本生成在第五章中有详细介绍。与 DPR 检索器一样我们不嵌入文档标题因为我们的语料库始终根据产品 ID 进行过滤。 接下来要做的事情是使用 Haystack 的GenerativeQAPipeline将检索器和生成器联系起来 from haystack.pipeline import GenerativeQAPipelinepipe GenerativeQAPipeline(generatorgenerator, retrieverdpr_retriever)注意 在 RAG 中查询编码器和生成器都是端到端训练的而上下文编码器是冻结的。在 Haystack 中GenerativeQAPipeline使用RAGenerator的查询编码器和DensePassageRetriever的上下文编码器。 现在让我们通过输入一些关于之前的亚马逊 Fire 平板电脑的查询来尝试一下 RAG。为了简化查询我们将编写一个简单的函数该函数接受查询并打印出前几个答案 def generate_answers(query, top_k_generator3):preds pipe.run(queryquery, top_k_generatortop_k_generator,top_k_retriever5, filters{item_id:[B0074BW614]})print(fQuestion: {preds[query]} \n)for idx in range(top_k_generator):print(fAnswer {idx1}: {preds[answers][idx][answer]})好的现在我们准备好测试一下 generate_answers(query)Question: Is it good for reading?Answer 1: the screen is absolutely beautiful Answer 2: the Screen is absolutely beautiful Answer 3: Kindle fire这个答案并不算太糟糕但它确实表明问题的主观性使生成器感到困惑。让我们尝试一些更加客观的问题 generate_answers(What is the main drawback?)Question: What is the main drawback?Answer 1: the price Answer 2: no flash support Answer 3: the cost这更有意义为了获得更好的结果我们可以在 SubjQA 上对 RAG 进行端到端的微调我们将把这留作练习但如果您有兴趣探索可以在 Transformers repository中找到一些脚本来帮助您入门。 结论 好吧这是 QA 的一个风风火火的介绍你可能还有很多问题想要得到答案双关语。在本章中我们讨论了 QA 的两种方法提取式和生成式并检查了两种不同的检索算法BM25 和 DPR。在这个过程中我们看到领域适应可以是一个简单的技术可以显著提高我们的 QA 系统的性能并且我们看了一些用于评估这种系统的最常见的指标。尽管我们专注于封闭领域的 QA即电子产品的单一领域但本章中的技术可以很容易地推广到开放领域的情况我们建议阅读 Cloudera 出色的 Fast Forward QA 系列来了解其中的内容。 在野外部署 QA 系统可能是一个棘手的问题我们的经验是其中相当大一部分价值来自首先为最终用户提供有用的搜索功能然后是一个提取组件。在这方面读者可以以新颖的方式使用超出了按需回答用户查询的范围。例如Grid Dynamics的研究人员能够使用他们的读者自动提取客户目录中每种产品的优缺点。他们还表明可以通过创建查询如“什么样的相机”以零-shot 方式使用读者来提取命名实体。鉴于其幼稚和微妙的失败模式我们建议只有在其他两种方法耗尽后才探索生成式 QA。解决 QA 问题的这种“需求层次结构”在图 7-14 中有所说明。 图 7-14. QA 需求层次结构 展望未来一个令人兴奋的研究领域是多模态 QA它涉及对文本、表格和图像等多种模态进行 QA。正如 MultiModalQA 基准所描述的那样¹⁷这样的系统可以使用户回答跨越不同模态的信息的复杂问题比如“著名的两根手指的画是什么时候完成的”另一个具有实际业务应用的领域是在知识图谱上进行 QA其中图的节点对应于现实世界的实体它们的关系由边定义。通过将事实编码为主语谓语宾语三元组可以使用图来回答关于缺失元素的问题。有一个将 Transformer 与知识图谱相结合的例子请参见Haystack 教程。另一个有前途的方向是自动生成问题作为一种使用未标记数据或数据增强进行无监督/弱监督训练的方式。最近的两个例子包括 Probably Answered QuestionsPAQ基准和跨语言设置的合成数据增强的论文。¹⁸ 在本章中我们已经看到为了成功地将 QA 模型应用于实际用例我们需要应用一些技巧比如实现一个快速检索管道以便在几乎实时进行预测。尽管这听起来可能不像什么但想象一下如果您必须等待几秒钟才能获得谷歌搜索结果您的体验会有多不同——几秒钟的等待时间可能决定了您的 Transformer 应用程序的命运。在下一章中我们将看一些加速模型预测的方法。 ¹尽管在这种特殊情况下每个人都同意 Drop C 是最好的吉他调音。 ²J. Bjerva 等人“SubjQA: A Dataset for Subjectivity and Review Comprehension”2020。 ³ 正如我们将很快看到的还有一些无法回答的问题旨在产生更健壮的模型。 ⁴ D. Hendrycks 等人“CUAD: 专家注释的法律合同审查 NLP 数据集”2021。 ⁵ P. Rajpurkar 等人“SQuAD超过 10 万个文本理解问题”2016。 ⁶ P. Rajpurkar, R. Jia, and P. Liang“知道你不知道的SQuAD 的无法回答的问题”2018。 ⁷ T. Kwiatkowski 等人“自然问题问答研究的基准”计算语言学协会交易 72019 年 3 月452-466http://dx.doi.org/10.1162/tacl_a_00276。 ⁸ W. Wang 等人“MINILM: 用于任务不可知的预训练 Transformer 的深度自注意压缩”2020。 ⁹ 请注意token_type_ids并不是所有 Transformer 模型都具有的。对于 MiniLM 等类似 BERT 的模型token_type_ids在预训练期间也用于整合下一个句子预测任务。 ¹⁰ 有关如何提取这些隐藏状态的详细信息请参阅第二章。 ¹¹ 如果一个向量的大部分元素都是零那么它就是稀疏的。 ¹² 该指南还提供了 macOS 和 Windows 的安装说明。 ¹³ 有关使用 TF-IDF 和 BM25 进行文档评分的深入解释请参阅 D. Jurafsky 和 J.H. MartinPrentice Hall的《语音和语言处理》第 3 版第二十三章。 ¹⁴ V. Karpukhin 等人“用于开放领域问答的密集通道检索”2020。 ¹⁵ D. Yogatama 等人“学习和评估通用语言智能”2019。 ¹⁶ P. Lewis 等人“用于知识密集型 NLP 任务的检索增强生成”2020。 ¹⁷ A. Talmor 等人“MultiModalQA文本、表格和图像的复杂问答”2021。 ¹⁸ P. Lewis 等人“PAQ: 6500 万个可能被问到的问题及其用途”2021A. Riabi 等人“用于零样本跨语言问答的合成数据增强”2020。
http://www.zqtcl.cn/news/71444/

相关文章:

  • 信阳网站建设策划方案免费下载软件app
  • 如何更改网站的关键词做seo必须有自己网站吗
  • 哈密建设厅网站绍兴微网站建设
  • 怎么在网站上添加广告代码公司网站怎么做网站备案
  • 自己做简单网站价格老公的姐姐一直用我们的手机号码
  • 做网站电话广东世纪达建设集团有限公司官方网站
  • 视频购物网站开发方案泸县城乡住房建设厅网站
  • 革吉网站建设长沙开发网站的公司哪家好
  • 上海做网站好的公司网站建设管理
  • ipad网站制作无锡网站推
  • 怎样给网站做后台wordpress重置密码忘记
  • wordpress安装网站无法pc端微信端网站建设
  • 镇江网站建设企业网站seo收费
  • 如何注册网站主办者深圳设计展2022
  • 北京网站建设公司如何选龙岩app制作
  • 沈阳免费自助建站模板广东网站建设网
  • wordpress 网站显示加载时长湘潭网络推广公司
  • 国家为什么不禁止外包北京百度seo排名点击软件
  • 做网站字体规范本科毕业 做网站编辑
  • 提供网站制作公司哪家好消防做ccc去那个网站
  • 艺术设计教学资源网站建设标准品牌电商网站
  • 一个完整网站开发需要什么技术建e网室内设计网下载
  • 设计公司网站建设模板图世界购物网站排名
  • 网站做推广搜索引擎营销的典型案例
  • 下载贵州省建设厅网站手游制作
  • 网站建设 计入哪个科目建设020网站需要多少钱
  • 前端网站页面模板网站域名的组成
  • 龙岩做网站开发价格湖北省住房和城乡建设厅网站首页
  • 网站可信国家企业官方网站查询系统
  • 泉州高端网站建设网站建设开发有什么好处