面向自然语言处理的迁移学习(全)

面向自然语言处理的迁移学习(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

前言

在过去的几年里,很难忽视自然语言处理(NLP)领域的飞速发展。在此期间,您可能已经被关于流行 NLP 模型(如 ELMo、BERT,以及最近的 GPT-3)的新闻文章所淹没。这种技术周围的兴奋是有道理的,因为这些模型使我们能够实现三年前我们无法想象的 NLP 应用,比如仅仅从对代码的描述中编写出生产代码,或者自动生成可信的诗歌和博客。

推动这一进步的一个重要因素是对自然语言处理模型的越来越复杂的迁移学习技术的关注。迁移学习在自然语言处理中越来越受欢迎和激动人心,因为它使您能够将从一个场景中获得的知识适应或转移到另一个场景,例如不同的语言或任务。这对于自然语言处理的民主化以及更广泛地说人工智能(AI)是一个重大进步,允许知识以前所需资源的一小部分在新环境中得到重复使用。

作为加纳西非国家的公民,在那里,许多新兴的企业家和发明家无法获得大量的计算资源,并且许多基本的自然语言处理问题仍然有待解决,这个主题对我来说尤为重要。这种范式赋予了这样的环境中的工程师们权力,使他们能够构建潜在的拯救生命的自然语言处理技术,否则这是不可能的。

我第一次接触到这些想法是在 2017 年,当时我正在美国国防高级研究计划局(DARPA)生态系统内从事开源自动机器学习技术的工作。我们使用迁移学习来减少对标记数据的需求,首先在模拟数据上训练自然语言处理系统,然后将模型转移到少量真实标记数据上。突破性模型 ELMo 随后出现,激发了我对该主题的进一步学习和探索,以了解如何在我的软件项目中进一步利用这些想法。

自然而然地,我发现由于这些想法的绝对新颖性和领域发展速度的快速性,这个主题没有全面的实用介绍。2019 年,我有机会撰写这个主题的实用介绍,我没有犹豫。你手里拿着的是我大约两年努力的成果。这本书将快速带领你了解该领域的关键近期自然语言处理模型,并提供可执行代码,您将能够直接修改和重用在自己的项目中。尽管不可能涵盖每一个体系结构和用例,但我们战略性地涵盖了我们认为会装备您基本技能以便在这个新兴领域中进一步探索并保持最新的架构和示例。

当你决定更多了解这个话题时,你做出了一个明智的决定。涌现出机会来探索新理论、算法方法和突破性应用。我期待着听到您在周围社会上所产生的转型积极影响。

致谢

我非常感谢加纳自然语言处理(NLP)开源社区的成员,在那里我有幸学习更多关于这一重要主题的知识。该群体成员和我们工具的用户的反馈强调了我对这项技术变革的理解。这激励并激励我将这本书完成。

我要感谢我的 Manning 开发编辑苏珊·埃斯里奇,她花了无数小时阅读手稿,提供反馈,并指导我度过了许多挑战。我感谢我的技术开发编辑艾尔·克林克尔为帮助我改进写作的技术维度所付出的所有时间和精力。

我感谢所有编辑委员会成员、市场营销专业人员和其他努力使这本书成为现实的制作团队成员。这些人包括丽贝卡·赖恩哈特、伯特·贝茨、尼科尔·巴特菲尔德、雷哈娜·马尔卡诺维奇、亚历山大·德拉戈萨夫列维奇、梅丽莎·艾斯、布兰科·拉廷西奇、克里斯托弗·考夫曼、坎迪斯·吉尔霍利、贝基·惠特尼、帕梅拉·亨特和拉德米拉·埃尔塞戈瓦克,排名不分先后。

在这个项目的几个关键时刻,技术同行审阅者提供了宝贵的反馈,没有他们,这本书就不会那么好。我非常感谢他们的意见。其中包括安德烈斯·萨科、安吉洛·西蒙尼·斯科托、艾瑞尔·加米诺、奥斯汀·普尔、克利福德·瑟伯、迭戈·卡塞拉、豪梅·洛佩兹、曼努埃尔·R·西奥西奇、马克·安东尼·泰勒、马西耶·阿弗尔蒂特、马修·萨尔门托、迈克尔·沃尔、尼科斯·卡纳卡里斯、尼诺斯拉夫·切尔克斯、奥尔·戈兰、拉尼·夏利姆、萨亚克·保罗、塞巴斯蒂安·帕尔马、塞尔吉奥·戈沃尼、托德·库克和万斯·西斯特拉。我感谢技术校对者艾瑞尔·加米诺在校对过程中捕捉到的许多拼写错误和其他错误。我感谢所有书籍论坛参与者的优秀评论,这进一步帮助改进了本书。

我非常感谢我的妻子戴安娜对这项工作的支持和鼓励。我感激我的母亲和我的兄弟姐妹——理查德、吉迪恩和吉夫蒂——对我继续激励我。

关于这本书

这本书试图为自然语言处理中的迁移学习这一重要主题提供全面实用的介绍。我们强调通过代表性代码和示例建立直观理解,而不是专注于理论。我们的代码编写旨在便于快速修改和重新利用,以解决您自己的实际问题和挑战。

谁应该读这本书?

要充分利用本书,您应具备一些 Python 经验,以及一些中级机器学习技能,如对基本分类和回归概念的理解。具备一些基本的数据处理和预处理技能,如使用 Pandas 和 NumPy 等库,也会有所帮助。

话虽如此,我写这本书的方式使你可以通过一点额外的工作掌握这些技能。前三章将迅速带你了解你需要掌握的一切,以充分理解迁移学习 NLP 的概念,并应用于你自己的项目中。随后,通过自行查阅包含的精选参考资料,你将巩固你的先修背景技能,如果你觉得有必要的话。

路线图

本书分为三个部分。按照它们的出现顺序逐步学习将让您收获最多。

第一部分回顾了机器学习的关键概念,提供了使最近的迁移学习 NLP 进展成为可能的机器学习进步的历史概述,并提供了研究该主题的动机。它还通过一对示例来回顾更传统的 NLP 方法的知识,并让您亲自动手使用一些关键的现代迁移学习 NLP 方法。本书此部分涵盖的概念章节级别的分解如下:

  • 第一章介绍了迁移学习的确切含义,包括在人工智能领域和自然语言处理(NLP)的背景下。它还探讨了促成迁移学习的技术进步的历史发展。

  • 第二章介绍了一对代表性的自然语言处理(NLP)问题,并展示了如何获取和预处理数据。它还使用传统的线性机器学习方法——逻辑回归和支持向量机——为它们建立了基准。

  • 第三章继续通过传统的基于树的机器学习方法——随机森林和梯度提升机——对第二章中的一对问题进行基准测试。它还使用关键的现代迁移学习技术 ELMo 和 BERT 对它们进行基准测试。

第二部分深入探讨了基于浅层神经网络的一些重要的迁移学习 NLP 方法,即层次相对较少的神经网络。它还通过代表性技术(如 ELMo)更详细地探讨了深度迁移学习,这些技术利用循环神经网络(RNN)进行关键功能。本书此部分涵盖的概念章节级别的分解如下:

  • 第四章应用了浅层词和句子嵌入技术,如 word2vec 和 sent2vec,进一步探索了本书第一部分的一些示例。它还介绍了领域自适应和多任务学习等重要的迁移学习概念。

  • 第五章介绍了一组依赖于 RNN 的深度迁移学习 NLP 方法,以及一对新的例子数据集,这些数据集将用于研究这些方法。

  • 第六章更详细地讨论了第五章介绍的方法,并将其应用于同一章节中介绍的数据集。

第三部分涵盖了这一领域中可能最重要的子领域,即依赖于变压器神经网络进行关键功能的深度迁移学习技术,例如 BERT 和 GPT。这种模型架构类别正在证明在最近的应用中最具影响力,部分原因是在并行计算架构上比等效的先前方法具有更好的可扩展性。本部分还深入探讨了各种使迁移学习过程更有效的适应策略。书中此部分涵盖的概念的章节级细分如下:

  • 第七章介绍了基本的变压器架构,并使用其重要变体之一—GPT—进行了一些文本生成和基本聊天机器人。

  • 第八章介绍了重要的变压器架构 BERT,并将其应用于多种用例,包括问答、填空以及向低资源语言的跨语言转移。

  • 第九章介绍了一些旨在使迁移学习过程更有效的适应策略。其中包括区分性微调和逐步解冻(来自方法 ULMFiT 的方法)以及知识蒸馏。

  • 第十章介绍了额外的适应策略,包括嵌入因子分解和参数共享—这些是 ALBERT 方法背后的策略。该章还涵盖了适配器和顺序多任务适应。

  • 第十一章通过回顾重要主题并简要讨论新兴的研究主题和方向来结束本书,例如需要考虑和减轻技术可能产生的潜在负面影响。这些包括对不同人群的偏见预测以及训练这些大型模型的环境影响。

软件要求

Kaggle 笔记本是执行这些方法的推荐方式,因为它们可以让您立即开始,无需进行任何设置延迟。此外,在编写本文时,此服务提供的免费 GPU 资源扩大了所有这些方法的可访问性,使得那些可能没有本地强大 GPU 访问权限的人也能够使用,这与“AI 民主化”议程一致,激发了许多人对 NLP 迁移学习的兴趣。附录 A 提供了 Kaggle 快速入门指南和作者个人关于如何最大程度地发挥平台作用的一些建议。但是,我们预计大多数读者应该会发现开始使用是相当简单的。我们已经在 Kaggle 上公开托管了所有笔记本,并附上了所有必需的数据,以便您只需点击几下即可开始执行代码。但是,请记住“复制并编辑”(fork)笔记本——而不是将其复制并粘贴到新的 Kaggle 笔记本中——因为这样可以确保结果库在环境中与我们为代码编写的库匹配。

关于代码

这本书包含了许多源代码示例,既有编号列表中的,也有与普通文本一起的。在这两种情况下,源代码都以像这样的等宽字体格式化,以便与普通文本分开。有时代码也会以粗体显示,以突出显示章节中已更改的代码,例如当新功能添加到现有代码行时。

在许多情况下,原始源代码已经重新格式化;我们添加了换行符并重新排列了缩进以适应书中可用的页面空间。在极少数情况下,即使这样还不够,列表中还包括了行继续标记(➥)。此外,在文本中描述代码时,源代码中的注释通常已从列表中删除。代码注释伴随许多列表,突出显示重要概念。

本书示例中的代码可从 Manning 网站下载,网址为www.manning.com/downloads/2116,也可以从 GitHub 上下载,网址为github.com/azunre/transfer-learning-for-nlp

liveBook 讨论论坛

购买《自然语言处理的迁移学习》包括免费访问由 Manning Publications 运营的私人网络论坛,在该论坛上,您可以对该书发表评论,提出技术问题,并从作者和其他用户那里获得帮助。要访问论坛,请转到livebook.manning.com/#!/book/transfer-learning-for-natural-language-processing/discussion。您还可以在livebook.manning.com/#!/discussion了解有关 Manning 论坛及行为规范的更多信息。

Manning 致力于为我们的读者提供一个场所,让个人读者之间以及读者与作者之间进行有意义的对话。这不是对作者参与的特定数量的承诺,作者对论坛的贡献仍然是自愿的(未付酬)。我们建议您尝试向作者提出一些具有挑战性的问题,以免他失去兴趣! 只要这本书还在印刷,您都可以从出版商的网站访问论坛和以前的讨论档案。

作者简介

Paul Azunre 持有麻省理工学院计算机科学博士学位,并曾担任多个 DARPA 研究项目的主要研究员。他创立了 Algorine Inc.,一个致力于推动人工智能/机器学习发展并确定其可能产生重大社会影响的研究实验室。Paul 还共同创立了 Ghana NLP,一个专注于使用自然语言处理和迁移学习处理加纳语和其他资源稀缺语言的开源倡议。

封面插图说明

自然语言处理的迁移学习 封面上的图案标题为“Moluquoise”,或者说是马鲁古妇女。 这幅插图取自法国 1788 年出版的雅克·格拉塞·德·圣索维尔(Jacques Grasset de Saint-Sauveur,1757-1810)的 所有已知民族的现代民族服饰 系列,每个插图都经过精细手绘和上色。格拉塞·德·圣索维尔(Grasset de Saint-Sauveur)收集的丰富多样性生动地提醒我们,就在 200 年前,世界的城镇和地区文化迥然不同。 人们相互隔离,说着不同的方言和语言。 在街上或乡间,仅通过他们的服装就可以轻松辨认出他们住在哪里以及他们的贸易或生活状况。

自那时起,我们的着装方式已经发生了变化,地区的多样性也在消失。 现在很难区分不同大陆的居民,更不用说不同的城镇、地区或国家了。 或许我们已经用文化多样性换取了更丰富的个人生活,肯定是为了更丰富和快节奏的技术生活。

在很难将一本计算机书籍与另一本区分开来的时候,Manning 通过基于两个世纪前地区生活丰富多样性的丰富多样的书籍封面,庆祝了计算机业的创造力和主动性,这些封面是格拉塞·德·圣索维尔的图片重新呈现的。

第一部分:介绍与概述

第 1、2 和 3 章回顾了机器学习中的关键概念,提供了机器学习进展的历史概述,促进了最近在自然语言处理中的迁移学习进展,并强调了研究这一主题的重要性。它们还通过一对相关示例,既回顾了您对传统自然语言处理方法的知识,又通过一些关键的现代自然语言处理迁移学习方法让您亲身体验了一下。

第一章:什么是迁移学习?

本章涵盖了

  • 迁移学习在人工智能(AI)中的普遍含义,以及在自然语言处理(NLP)的上下文中的含义

  • 典型 NLP 任务及 NLP 迁移学习进展的相关年表

  • 计算机视觉中的迁移学习概述

  • 近年来 NLP 迁移学习技术日益普及的原因

人工智能(AI)已经以戏剧性的方式改变了现代社会。现在,机器执行了人类曾经做过的任务,而且它们做得更快、更便宜,有些情况下甚至更有效。流行的例子包括计算机视觉应用,教会计算机如何理解图像和视频,例如监控摄像头视频中的罪犯检测。其他计算机视觉应用包括从患者器官图像中检测疾病以及从植物叶片中定义植物物种。人工智能的另一个重要分支,自然语言处理(NLP),特别涉及人类自然语言数据的分析和处理。NLP 应用的例子包括语音转文本转录和各种语言之间的翻译。

AI 机器人技术和自动化的技术革命最新演变—一些人将其称为第四次工业革命¹—是由几个因素的交汇引发的:针对训练大型神经网络的算法进步,通过互联网获取大量数据的可行性,以及最初是为个人游戏市场开发的大规模并行能力通过图形处理单元(GPUs)的可获得性。最近对依赖人类感知的任务自动化的快速进步,特别是计算机视觉和 NLP,需要这些神经网络理论和实践的进步。这一领域的增长促进了对输入数据和所需输出信号的复杂表示的开发,以处理这些困难问题。

与此同时,人们对 AI 能够实现的预期大大超出了实践中所取得的成就。我们被警告说,将来可能会有一个末日般的未来,将消灭大部分人类工作并取代我们所有人,甚至可能对我们构成存在威胁。NLP 并没有被排除在这种猜测之外,因为它今天是 AI 内最活跃的研究领域之一。我希望阅读本书能帮助你更好地了解从 AI、机器学习和 NLP 中现实可能期待的东西。然而,本书的主要目的是向读者提供一组与最近在 NLP 中变得重要的范式相关的可行技能—迁移学习。

迁移学习旨在利用不同设置的先前知识——无论是不同的任务、语言还是领域——来帮助解决手头的问题。它受到人类学习的启发,因为我们通常不会为了任何给定的问题从头学习事物,而是建立在可能相关的先前知识上。例如,学习演奏一种乐器,在已经知道如何演奏另一种乐器的情况下被认为更容易。显然,乐器越相似——比如风琴与钢琴——先前的知识越有用,学习新乐器也会更容易。然而,即使乐器非常不同——如鼓和钢琴——一些先前知识仍然有用,虽然作用较小,比如遵循节奏的练习。

大型研究实验室,如劳伦斯利弗莫尔国家实验室或桑迪亚国家实验室,以及大型互联网公司,如谷歌和 Facebook,能够通过在数十亿字和数百万图片上训练深层神经网络来学习大规模复杂模型。例如,谷歌的 NLP 模型 BERT(双向编码器表示转换),将在下一章介绍,是在英文版本的维基百科(25 亿字)和 BookCorpus(8 亿字)上进行了预训练。² 同样,深度卷积神经网络(CNNs)已经在 ImageNet 数据集的超过 1400 万张图片上进行了训练,学习的参数已经被许多组织广泛应用。从头开始训练这样的模型需要的资源量通常不会被普通的神经网络从业者所使用,比如在较小企业工作的 NLP 工程师或在较小学校读书的学生。这是否意味着较小的参与者无法取得其问题的最先进成果?绝对不是——值得庆幸的是,如果正确应用,迁移学习的概念承诺解决这个问题。

为什么迁移学习如此重要?

迁移学习使您能够将从一组任务和/或领域中获得的知识调整或转移到另一组任务和/或领域。这意味着,曾经开源的、经过大量资源包括数据、计算能力、时间和成本训练的模型可以通过更广泛的工程社区进行微调和重复使用,而使用的资源成本仅为原始资源成本的一小部分。这对于 NLP 甚至更广泛的 AI 的民主化代表了一个重要进步。图 1.1 说明了这种范式,以学习如何演奏乐器为例。从图中可以看出,不同任务/领域之间的信息共享可以使后续任务 B 所需的数据量减少,以实现相同的性能,或者下游任务 B。

01_01

图 1.1 转移学习范式优势的示意图—显示在底部面板—在不同任务/领域训练的系统之间共享信息,与传统范式—显示在顶部面板—其中任务/领域之间同时进行训练相比。在转移学习范式中,通过信息/知识共享可以实现减少数据和计算需求。例如,如果一个人先学会弹钢琴,我们预期他们学会打鼓会更容易些。

1.1 代表性自然语言处理任务概述

自然语言处理的目标是使计算机能够理解自然人类语言。您可以将其视为将自然语言文本系统地编码为准确反映其含义的数值表示的过程。尽管存在各种典型自然语言处理任务的分类法,但以下非尽述性列表提供了一个框架,用于思考问题的范围,并适当地构建本书将讨论的各种示例。请注意,其中一些任务可能需要(或不需要,具体取决于所选择的特定算法)列表中其他更难的任务:

  • 词性标注(POS)—在文本中标记词语的词性;可能的标记包括动词、形容词和名词。

  • 命名实体识别(NER)—在非结构化文本中检测实体,如人名、组织名和地名。请注意,词性标注可能是 NER 流水线的一部分。

  • 句子/文档分类—使用预定义的类别对句子或文档进行标记,例如情感{“积极”,“消极”}、各种主题{“娱乐”,“科学”,“历史”}或一些其他预定义的类别集。

  • 情感分析—为一个句子或文档分配其中表达的情感,例如,{“积极”,“消极”}。事实上,您可以将其视为句子/文档分类的特例。

  • 自动摘要—总结一系列句子或文档的内容,通常用几句话或关键词概括。

  • 机器翻译—将句子/文档从一种语言翻译成另一种语言或一系列语言。

  • 问答系统—确定对人类提出的问题的合适答案;例如,问题:加纳的首都是什么?答案:阿克拉。

  • 闲聊机器人/聊天机器人—与人类进行一段有说服力的对话,可能旨在实现某个目标,例如最大化对话长度或从人类那里提取某些特定信息。请注意,闲聊机器人可以被构建为问答系统。

  • 语音识别—将人类语音的音频转换为其文本表示。尽管已经投入了大量的工作使语音识别系统更加可靠,但在本书中,假设已经存在了语言感兴趣的文本表示。

  • 语言建模 —— 确定人类语言中一系列单词的概率分布,其中知道一个序列中最有可能的下一个单词对于语言生成——预测下一个单词或句子——尤为重要。

  • 依赖解析 —— 将一句话分成一个表示其语法结构和单词之间关系的依赖树。请注意,POS 标记在这里可能很重要。

1.2 在 AI 背景下理解 NLP

在继续本书的其余部分之前,了解自然语言处理这个术语,并正确地将其与其他常见术语,如人工智能、机器学习和深度学习相联系非常重要。流行媒体经常将这些术语赋予的含义与机器学习科学家和工程师使用它们的含义不匹配。因此,在我们使用这些术语时,通过图 1.2 中的部分图解精确定义这些术语非常重要。

01_02

图 1.2:自然语言处理(NLP)、人工智能(AI)、机器学习和深度学习相互关系的维恩图解。具有符号 AI 的其他相关内容也在图中显示。

如您所见,深度学习是机器学习的子集,而机器学习又是 AI 的子集。NLP 也是 AI 的子集,与深度学习和机器学习有非空交集。本图扩展了 François Chollet 提出的图表³。请参阅他的书中的第六章和第 8.1 节,了解神经网络在文本中的应用综述。符号 AI 也在图表中显示,并将在下一小节中描述。

1.2.1 人工智能(AI)

人工智能这一领域起源于 20 世纪中叶,旨在使计算机模仿和执行人类通常执行的任务。最初的方法侧重于手动推导和硬编码显式规则,以处理每种感兴趣情况的输入数据。这个范式通常被称为符号 AI。它适用于像棋类这样明确定义的问题,但在遇到属于感知类别的问题,如视觉和语音识别时,明显遇到了困难。需要一种新的范式,其中计算机可以从数据中学习新规则,而不是让人类主管明确指定它们。这导致了机器学习的崛起。

1.2.2 机器学习

在 20 世纪 90 年代,机器学习的范式成为了人工智能的主导趋势。计算机不再为每种可能的情景明确地编程,而是通过看到许多相应的输入输出对的示例来训练计算机将输入与输出信号关联起来。机器学习使用了大量的数学和统计机制,但由于它往往涉及大型和复杂的数据集,该领域更多地依赖于实验、经验观察和工程,而不是数学理论。

机器学习算法学习一种将输入数据转换为适当输出的表示。为此,它需要一组数据,例如句子分类任务中的一组句子输入,以及一组相应的输出,例如句子分类的标签,如{“positive”,“negative”}。还需要一个损失函数,它衡量机器学习模型当前输出与数据集预期输出的距离有多远。为了帮助理解,考虑一个二元分类任务,其中机器学习的目标可能是选择一个名为决策边界的函数,它将清晰地将不同类型的数据点分开,如图 1.3 所示。这个决策边界应该泛化到超出训练数据的未见示例。为了使这个边界更容易找到,您可能希望首先对数据进行预处理或转换,使其更易于分离。我们从被允许的一组称为假设集的函数中寻求这样的转换。自动确定这样一个转换,使得机器学习的最终目标更容易实现,具体来说就是所谓的学习

01_03

图 1.3 机器学习中一个重要的激励任务的示例:在假设集中找到一个决策边界,以有效地将不同类型的点彼此分开。在本图所示的情况下,假设集可能是弧的集合。

机器学习自动化了在一些预定义的假设集中搜索最佳输入输出转换的过程,利用损失函数所体现的一些反馈信号的指导。假设集的性质确定了考虑的算法类别,我们接下来会概述。

经典机器学习是以概率建模方法为起点,例如朴素贝叶斯。在这里,我们做出一个朴素假设,即输入数据特征都是相互独立的。逻辑回归是一个相关方法,通常是数据科学家在数据集上尝试的第一个方法,以其为基准。这两类方法的假设集都是线性函数的集合。

神经网络最初是在 20 世纪 50 年代发展起来的,但直到 20 世纪 80 年代才发现了训练大型网络的有效方法——反向传播算法与随机梯度下降算法相结合。虽然反向传播提供了计算网络梯度的方法,但随机梯度下降则利用这些梯度来训练网络。我们在附录 B 中简要回顾了这些概念。第一个成功的实际应用发生在 1989 年,当时贝尔实验室的 Yann LeCun 构建了一个识别手写数字的系统,这个系统后来被美国邮政部门大量使用。

核方法 在 20 世纪 90 年代变得流行起来。这些方法试图通过找到好的决策边界来解决分类问题,就像在图 1.3 中概念化的那样。最受欢迎的方法是支持向量机(SVM)。通过将数据映射到一个新的高维表示,然后在这个表示中超平面就是有效的边界。然后最大化每个类中最近数据点与超平面之间的距离。在高维空间中操作的高计算成本通过核技巧来减轻。这个方法类别受到坚实的理论支持,并且可以进行数学分析,当核是线性函数时,这样的分析是线性的。然而,在感知机器学习问题上的表现仍有待改善,因为这些方法首先需要手动进行特征工程,这使方法变得脆弱且容易出错。

决策树及相关方法是另一类仍然被广泛使用的算法类别。决策树是一种决策支持辅助工具,可以将决策及其后果建模为,即一个两个节点之间只有一条路径连接的图。另外,可以将树定义为将输入值转换为输出类别的流程图。决策树的流行度在 2010 年代上升,当依赖它们的方法开始被更喜欢于核方法时。这种流行度得益于它们易于可视化、理解和解释。为了帮助理解,图 1.4 展示了一个示例决策树结构,如果 A<10 则将输入 {A,B} 分类为类别 1,如果 A>=10 且 B<25 则分类为类别 2,否则分类为类别 3。

01_04

图 1.4 示例决策树结构,如果 A<10 则将输入 {A,B} 分类为类别 1,如果 A>=10 且 B<25 则分类为类别 2,否则分类为类别 3

随机森林为应用决策树提供了一种实用的机器学习方法。该方法涉及生成大量的专门树并结合它们的输出。随机森林非常灵活且广泛适用,通常在逻辑回归之后作为基线的第二种算法尝试。当 Kaggle 开放竞赛平台在 2010 年启动时,随机森林迅速成为该平台上最广泛使用的算法。在 2014 年,梯度提升机接管了这一地位。它们迭代地学习新的基于决策树的模型,解决了上一轮迭代中模型的弱点。在撰写本文时,它们被普遍认为是解决非感知机器学习问题的最佳类方法。它们在 Kaggle 上仍然非常受欢迎。

大约在 2012 年,GPU 训练的深度卷积神经网络(CNNs)开始在每年的 ImageNet 比赛中获胜,标志着当前深度学习“黄金时代”的开始。CNNs 开始主导所有主要的图像处理任务,如对象识别和对象检测。同样,我们可以在处理人类自然语言,即 NLP 方面找到应用。神经网络通过对输入数据的一系列越来越有意义的分层表示进行学习。这些的数量指定了模型的深度。这就是术语深度学习——训练深度神经网络的过程来自哪里。为了区分它们与深度学习,所有前述的机器学习方法通常被称为浅层传统学习方法。请注意,深度较小的神经网络也将被分类为浅层,但不是传统的。深度学习已经主导了机器学习领域,成为感知问题的明显首选,并引发了能够处理的问题复杂度的革命。

尽管神经网络受到神经生物学的启发,但它们并不是我们神经系统工作方式的直接模型。神经网络的每一层都由一组数字参数化,称为该层的权重,准确指定了它如何转换输入数据。在深度神经网络中,参数的总数可以轻易达到百万级别。前面提到的反向传播算法是用来找到正确参数集的算法引擎,也就是学习网络的过程。图 1.5 展示了一个具有两个全连接隐藏层的简单神经网络的可视化。右侧还显示了同样的总结性可视化,我们经常会使用。一个深度神经网络可能有许多这样的层。一个显著的神经网络架构,不符合前馈性质的是长短期记忆(LSTM)循环神经网络(RNN)架构。与图 1.5 中的前馈架构不同,该架构接受长度为 2 的固定长度输入,而 LSTMs 可以处理任意长度的输入序列。

01_05

图 1.5 显示了一个具有两个全连接隐藏层的简单前馈神经网络的可视化(左)。右侧是一个总结性的等效表示,我们经常会用来简化图表。

正如之前提到的,引发深度学习最近兴趣的因素是硬件的跨度,大量数据的可用性以及算法的进步。GPU 最初是为视频游戏市场开发的,互联网的成熟开始为该领域提供前所未有的质量和数量的数据。维基百科、YouTube 和 ImageNet 是数据源的具体例子,其可用性推动了计算机视觉和自然语言处理的许多进步。神经网络消除了昂贵的手工特征工程的需求——这是将浅层学习方法成功应用于感知数据所需的——这可以说是影响了深度学习易于采纳的因素。由于自然语言处理是一个感知问题,它也将是本书中讨论的最重要的机器学习算法类别之一,尽管不是唯一的。

接下来,我们的目标是深入了解自然语言处理(NLP)领域的历史和进展。

1.2.3 自然语言处理(NLP)

语言是人类认知中最重要的方面之一。毋庸置疑的是,为了创建真正的人工智能,机器需要被教导如何解释、理解、处理和作出对人类语言的反应。这强调了自然语言处理对人工智能和机器学习领域的重要性。

就像人工智能的其他子领域一样,处理自然语言处理问题的初始方法,如句子分类和情感分析,都是基于显式规则或符号型人工智能。这种系统通常无法推广到新的任务,并且很容易崩溃。自从 20 世纪 90 年代核方法诞生以来,人们一直致力于特征工程——将输入数据手动转化为浅层学习方法可以用来产生有用预测的形式。这种方法非常耗时、任务特定且对非专家来说难以接触。深度学习的出现(大约在 2012 年)引发了自然语言处理的真正革命。神经网络能够在其某些层自动设计合适的特征,降低了这些方法对新任务和问题的适用性门槛。然后,人们将精力集中在为特定任务设计适当的神经网络架构,以及调整训练过程中的各种超参数设置上。

训练自然语言处理系统的标准方式是收集一组大量的数据点,每个数据点都可靠地注释了输出标签,比如情感分析任务中的“积极”或“消极”的句子或文档。然后将这些数据点提供给机器学习算法,学习最佳的输入到输出信号的表示或转换,可以推广到新的数据点。在自然语言处理和机器学习的其他子领域中,这个过程通常被称为“监督学习”范式。标注过程通常是手动完成的,为学习代表性转换提供“监督信号”。另一方面,从无标签数据中学习表示转换被称为“无监督学习”。

虽然今天的机器学习算法和系统并非生物学习系统的直接复制品,也不应被认为是这种系统的模型,但它们的某些方面受到了进化生物学的启发,而在过去,从生物学中汲取的灵感引导了显著的进步。基于这一点,似乎不合逻辑的是,对于每个新的任务、语言或应用领域,监督学习过程传统上都是从零开始重复。这一过程在某种程度上与自然系统学习的方式背道而驰——建立在之前获得的知识之上并进行再利用。尽管如此,从零开始学习感知任务仍取得了重大进展,特别是在机器翻译、问答系统和聊天机器人领域,虽然其中仍存在一些缺点。尤其是,当样本分布发生重大变化时,现有系统在处理时的稳定性较差。换句话说,系统学会了在特定类型的输入上表现良好。如果我们改变输入类型,这可能导致性能显著下降,甚至完全失效。此外,为了完全民主化人工智能,并使自然语言处理对小型企业的普通工程师——或对没有大型互联网公司所拥有的资源的人——变得更易获得,能够下载和重复使用其他地方获得的知识将是极其有益的。这对于生活在官方语言可能与英语或其他流行语言不同的国家的人,以及从事可能在他们所在地区独特的任务或从未有人探索过的任务的人来说,也非常重要。迁移学习提供了一种解决这些问题的方法。

迁移学习使人们能够从一个环境——我们定义为特定任务、领域和语言的组合——转移知识到另一个不同的环境。原始环境自然被称为源环境,而最终的环境称为目标环境。迁移过程的难易程度和成功程度取决于源环境和目标环境的相似性。很自然地,如果目标环境在某种意义上与源环境“相似”,在这本书的后面我们将对此做出定义,那么迁移将会更加容易且成功。

转移学习在自然语言处理中的隐式使用时间比大多数从业者意识到的要长得多,因为常见做法是使用预训练的嵌入,如word2vecsent2vec(在下一节中会更详细介绍)对单词进行向量化。 浅层学习方法通常被应用于这些向量作为特征。 我们将在接下来的章节和第四章中更详细地介绍这两种技术,并在整本书中以各种方式应用它们。 这种流行的方法依赖于一个无监督的预处理步骤,首先用于训练这些嵌入而不需要任何标签。 然后,从这一步中获取的知识被转移到特定的应用程序中,在监督设置中,通过使用浅层学习算法在一小部分标记示例上对所说的知识进行改进和专业化,以解决手头的问题。 传统上,将无监督和监督学习步骤相结合的这种范式被称为半监督学习

接下来,我们将详细介绍自然语言处理进展的历史进程,特别关注转移学习最近在这一重要的人工智能和机器学习子领域中所起的作用。

1.3 自然语言处理进展简史

要框架化你对自然语言处理中转移学习的状态和重要性的理解,首先了解历史上对这个人工智能子领域重要的任务和技术可以是有帮助的。 本节介绍了这些任务和技术,并以自然语言处理转移学习最近的进展概述告终。 这个概述将帮助你适当地将转移学习在自然语言处理中的影响放入背景,并理解为什么它现在比以往任何时候都更重要。

1.3.1 概述

自然语言处理诞生于 20 世纪中叶,与人工智能同时出现。 自然语言处理的一个重要历史里程碑是 1954 年的乔治城实验,在该实验中,大约 60 个俄语句子被翻译成英语。 在 20 世纪 60 年代,麻省理工学院(MIT)的自然语言处理系统 ELIZA 成功模拟了一名心理医生。 同样在 20 世纪 60 年代,信息表示的向量空间模型被开发出来,其中单词被表示为实数向量,这些向量可进行计算。 20 世纪 70 年代,基于处理输入信息的复杂手工规则集的一系列闲聊机器人/聊天机器人概念被开发出来。

在 1980 年代和 1990 年代,我们看到了将系统化的机器学习方法应用于自然语言处理的出现,计算机发现了规则,而不是人类制定了规则。这一进步与当时机器学习的普及爆炸同时发生,正如我们在本章前面已经讨论过的那样。1980 年代末,将奇异值分解(SVD)应用于向量空间模型,导致潜在语义分析—一种无监督的确定语言中单词关系的技术。

在 2010 年代初,神经网络和深度学习在该领域的崛起,彻底改变了自然语言处理。这些技术被证明在最困难的自然语言处理任务中取得了最先进的结果,例如机器翻译和文本分类。2010 年代中期见证了 word2vec 模型的发展,以及其变种 sent2vec、doc2vec 等等。这些基于神经网络的技术将单词、句子和文档(分别)向量化,以一种确保生成的向量空间中向量之间距离代表相应实体之间的差异的方式,即单词、句子和文档。事实上,这些嵌入的一些有趣属性允许处理类比—在诱导的向量空间中,单词ManKing之间的距离大约等于单词WomanQueen之间的距离,例如。用于训练这些基于神经网络的模型的度量来自语言学领域,更具体地说是分布语义学,不需要标记数据。一个单词的含义被假定与其上下文相关联,即周围的单词。

各种嵌入文本单元的方法,例如单词、句子、段落和文档,成为现代自然语言处理的关键基石。一旦文本样本被嵌入到适当的向量空间中,分析通常可以简化为对真实向量操作的众所周知的浅层统计/机器学习技术的应用,包括聚类和分类。这可以看作是一种隐式迁移学习的形式,以及一种半监督机器学习流水线—嵌入步骤是无监督的,学习步骤通常是监督的。无监督的预训练步骤实质上降低了标记数据的要求,从而减少了实现给定性能所需的计算资源—我们将在本书中学习如何利用迁移学习来为更广泛的情景提供服务。

大约在 2014 年,序列到序列模型⁷被开发出来,并在困难任务,如机器翻译和自动摘要中取得显著改进。特别是,尽管在神经网络之前的 NLP 流水线由几个明确的步骤组成,例如词性标注、依存句法分析和语言建模,但后来表明机器翻译可以进行“序列到序列”的处理。在这里,深度神经网络的各个层自动执行了所有这些中间步骤。这些模型学会了通过一个将输入序列(例如一种语言中的源句子)与一个输出序列(例如该句子的另一种语言的翻译)相关联的方法,通过将输入转换成上下文向量的编码器和将其转换成目标序列的解码器。编码器和解码器通常被设计为循环神经网络(RNNs)。这些能够在输入句子中编码顺序信息,这是早期模型(如词袋模型)无法做到的,从而显著提高了性能。

然而,人们发现,长输入序列更难处理,这促使了被称为注意力的技术的发展。这一技术通过让模型关注输入序列中最相关的部分,显著改善了机器翻译序列模型的性能。一个叫做transformer的模型进一步定义了自注意力层,用于编码器和解码器,使两者都能相对于输入序列中的其他文本段构建更好的上下文。这种架构在机器翻译方面取得了显著的改进,并且观察到它更适合在大规模并行硬件上进行训练,将训练速度提高了一个数量级。

直到 2015 年左右,大多数自然语言处理的实用方法都集中在词级别,这意味着整个单词被视为不可分割的原子实体,并被赋予一个特征向量。这种方法有几个缺点,尤其是如何处理从未见过或词汇外的单词。当模型遇到这样的单词时,比如单词拼写错误时,该方法会失败,因为无法对其进行向量化。此外,社交媒体的兴起改变了什么被视为自然语言的定义。现在,数十亿人通过表情符号、新发明的俚语和故意拼错的单词在线表达自己。不久之后,人们意识到,许多这些问题的解决方案自然地来自于以字符级别处理语言。在这个范式中,每个字符都将被向量化,只要人类使用可接受的字符表达自己,就可以成功生成向量特征,并成功应用算法。Zhang 等人⁹在字符级别 CNN 用于文本分类的背景下展示了这一点,并展示了对拼写错误的显著鲁棒性。

1.3.2 最近的迁移学习进展

传统上,针对任何给定的问题设置——任务、领域和语言的特定组合——学习都是以完全监督或完全无监督的方式进行的,从头开始。如前所述,半监督学习早在 1999 年就在 SVM 的背景下被认识到,作为一种解决可能有限标记数据可用性的方式。对更大规模的未标记数据集进行初始无监督预训练步骤使下游监督学习更容易。对此的变体被研究用于解决可能存在噪声——可能不正确——标签的情况,这种方法有时被称为弱监督学习。然而,通常假设标记数据集和未标记数据集的采样分布是相同的。

迁移学习放宽了这些假设。1995 年,在神经信息处理系统会议(NeurIPS)上,迁移学习被普遍认为是“学习学习”。基本上,它规定智能机器需要具有终身学习能力,以重复利用学到的知识进行新任务。此后,这一点已经在几个不同的名称下进行了研究,包括学习学习知识转移归纳偏差多任务学习。在多任务学习中,算法被训练以在多个任务上同时表现良好,从而发现可能更普遍有用的特征。然而,直到 2018 年左右,才开发出了实用且可扩展的方法来解决 NLP 中最困难的感知问题。

2018 年可谓是自然语言处理领域的一场革命。对于如何最好地将文本集合表示为向量的理解发生了巨大变革。此外,人们普遍认识到开源模型可以进行微调或转移到不同的任务、语言和领域。与此同时,一些大型互联网公司发布了更多、更大的自然语言处理模型,用于计算这些表示,并且指定了明确定义的微调程序。突然之间,即使是普通从业者,甚至是独立从业者,也能够获得自然语言处理方面的最新成果。有人称之为自然语言处理的“ImageNet 时刻”,这是在 2012 年之后看到的计算机视觉应用的爆发,当时一个 GPU 训练的神经网络赢得了 ImageNet 计算机视觉竞赛。就像最初的 ImageNet 时刻一样,预训练模型库首次为大量的自然语言处理数据提供了支持,以及对使用标记数据集微调到特定任务的明确定义技术,其数据集大小明显小于否则所需的大小。本书的目的是描述、阐明、评估、可证明地应用、比较和对比属于此类别的各种技术。我们接下来简要概述这些技术。

早期对自然语言处理的迁移学习的探索主要集中在类比于计算机视觉,后者在过去十多年中已经成功使用了。其中一种模型——本体建模语义推理(SIMOn)[¹⁰]——采用了字符级卷积神经网络(CNN)与双向 LSTM 结合的结构语义文本分类。SIMOn 方法展示了直接类比于计算机视觉的自然语言处理迁移学习方法。计算机视觉应用的丰富知识库激发了这种方法。该模型学到的特征被证明对无监督学习任务有用,并且在社交媒体语言数据上表现良好,这种语言有些特殊,与维基百科和其他大型基于书籍的数据集上的语言非常不同。

原始的 word2vec 公式中一个显著的弱点是消歧。无法区别在不同上下文中可能具有不同含义的单词的各种用法,例如同音异形词的情况——鸭子(姿势)与鸭子(鸟类)或公平(一次集会)与公平(有正义)。在某种意义上,原始的 word2vec 公式通过单词的平均向量表示来代表一个单词中这些不同同音异形词的向量的平均值。从语言模型中嵌入(¹¹ ELMo)——以受欢迎的Sesame Street角色命名-试图使用双向 LSTM 开发单词的上下文化嵌入。在这个模型中,一个单词的嵌入非常依赖于它的上下文,相应的数值表示对于每个这样的上下文是不同的。ELMo 通过训练来预测单词序列中的下一个词,这与本章开头介绍的语言建模概念有很大关系。大型数据集,如维基百科和各种书籍数据集,可用于此框架的训练。

通用语言模型微调(Universal Language Model Fine-Tuning, ULMFiT )¹² 方法被提出来为了微调任何一种基于神经网络的语言模型以适应特定任务,并在文本分类的情况下被初步证明。这种方法背后的一个重要概念是有区别的微调,其中网络的不同层以不同的速率进行训练。OpenAI 的生成式预训练变换器(Generative Pretrained Transformer, GPT)改变了变换器的编码器-解码器架构,以实现 NLP 微调语言模型。它放弃了编码器,并保留了解码器及其自我注意力子层。来自变形金刚的双向编码器表征¹³ (Bidirectional Encoder Representations from Transformers, BERT) 则相反,修改了变换器的结构,保留了编码器并丢弃了解码器,还依赖于单词掩蔽,需要准确预测训练指标。这些概念将在接下来的章节中详细讨论。

在所有这些基于语言模型的方法中——ELMo、ULMFiT、GPT 和 BERT,都表明生成的嵌入可以针对特定的下游 NLP 任务进行微调,只需相对较少的标记数据点即可。对语言模型的关注是有意义的:假设它们诱导的假设集是普遍有用的,并且已知为大规模训练准备了数据。

接下来,我们重点介绍计算机视觉中的迁移学习的关键方面,以更好地理解在 NLP 中的迁移学习,并看看是否可以为我们的目的学到和借鉴一些知识。这些知识将成为本书剩余部分中驱动我们对 NLP 迁移学习探索的丰富类比的来源。

1.4 计算机视觉中的迁移学习

尽管本书的目标是自然语言处理,但将 NLP 迁移学习放在计算机视觉迁移学习的背景下进行框架化有助于理解。这样做的原因之一是,来自 AI 的这两个子领域的神经网络架构可能具有某些相似的特征,因此可以借鉴计算机视觉的方法,或者至少用它们来指导 NLP 的技术。事实上,计算机视觉领域中这些技术的可用性被认为是最近 NLP 迁移学习研究的一个重要驱动因素。研究人员可以访问一个定义良好的计算机视觉方法库,以在相对未被探索的 NLP 领域进行实验。然而,这些技术直接可转移的程度是一个开放的问题,有几个重要的区别需要注意。一个这样的区别是,NLP 神经网络通常比计算机视觉中使用的神经网络要浅。

1.4.1 总体概述

计算机视觉或视觉机器人的目标是使计算机理解数字图像和/或视频,包括获取、处理和分析图像数据,并根据它们的派生表示做出决策。视频分析通常可以通过将视频分成帧来进行,然后可以将其视为图像分析问题。因此,理论上计算机视觉可以被提出为图像分析问题而不失一般性。

计算机视觉诞生于 20 世纪中期,与人工智能一起出现。显然,视觉是认知的重要部分,因此致力于建造智能机器人的研究人员早期就认识到它的重要性。上世纪六十年代,首批方法试图模仿人类视觉系统,而上世纪七十年代人们更加关注提取边缘和场景中形状建模。上世纪八十年代,各个方面的计算机视觉方法越来越成熟,尤其是人脸识别和图像分割,到了上世纪九十年代出现了数学严谨的方法。这个时期正值机器学习流行的时期,正如我们前面所提到的。接下来的几十年,致力于为图像开发更好的特征提取方法。在应用浅层机器学习技术之前,进行努力和重心在此。2012 年的“ImageNet 时刻”,当 GPU 加速的神经网络第一次在广受关注的 ImageNet 比赛中大幅领先时,标志着该领域的革命。

  • ImageNet¹⁴ 最初于 2009 年发布,并迅速成为测试目标识别最佳方法的竞赛基础。著名的 2012 年神经网络条目指出了深度学习作为计算机视觉特别是机器学习中感知问题的前进之路。对我们来说,一些研究人员很快意识到,来自预训练的 ImageNet 模型的神经网络权重可以用于初始化其他有时看似无关的任务的神经网络模型,并显著提高性能。

- 1.4.2 预训练的 ImageNet 模型

  • 在 ImageNet 每年的标志性比赛中获胜的各个团队非常慷慨地共享了他们的预训练模型。以下是一些值得注意的 CNN 模型示例。

  • VGG 架构最初是在 2014 年引入的,具有 VGG16(深度为 16)和 VGG19(深度为 19 层)两个变种。为了使更深的网络在训练过程中收敛,需要首先训练较浅的网络直至收敛,然后使用它的参数初始化更深的网络。该架构被发现在训练过程中有些慢,而且参数总数相对较大——约为 1.3 亿至 1.5 亿个参数。

  • 2015 年 ResNet 架构解决了其中一些问题。尽管更深层,但参数数量显著减少——最小的变种 ResNet50 深 50 层,约有 5000 万个参数。实现这种减少的关键是通过一种称为 最大池化 的技术进行正则化,并通过子构建块的模块化设计。

  • 其他值得注意的例子包括 Inception 及其扩展 Xception,分别于 2015 年和 2016 年提出,旨在通过在同一网络模块中堆叠多个卷积来创建多个级别的特征提取。这两个模型都进一步显著减小了模型大小。

- 1.4.3 微调预训练的 ImageNet 模型

  • 由于已经提出了预训练的 CNN ImageNet 模型,因此从头开始训练计算机视觉模型是不常见的。目前更常见的方法是下载其中一个这些开源模型,并在有限的标记数据上使用它来初始化类似的架构,例如 微调 一部分层,或者将其用作固定的特征提取器。

在图 1.6 中显示了在前馈神经网络中选择要微调的一部分层的可视化。随着目标领域中的数据量增加,阈值从输出(向输入)移动,阈值和输出之间的层被重新训练。这种变化是因为增加的数据量可以有效地用于训练更多的参数,而否则是无法完成的。此外,阈值的移动方向必须是从右到左,即远离输出端,接近输入端。这种移动方向使我们能够保留编码接近输入端的一般特征的层,同时重新训练接近输出端的层,它们编码源领域特定特征。而且,当源领域和目标领域高度不同的时候,一些阈值右侧的更具体的参数/层可以被丢弃。

另一方面,特征提取涉及仅移除网络的最后一层,该层不再产生数据标签,而是产生一组数值向量,可以通过浅层机器学习方法(如支持向量机 SVM)进行训练,就像以前一样。

在重新训练或微调方法中,先前的预训练权重并不全部保持不变,而是允许其中的一个子集根据新的标记数据进行改变。然而,重要的是要确保在有限的新数据上训练的参数数量不会导致过度拟合,这促使我们冻结一些参数以减少正在训练的参数的数量。通常是以经验的方式来选择要冻结的层数,图 1.6 中的启发式方法指导了这一点。

01_06

图 1.6 表现了在计算机视觉中适用于前馈神经网络架构的各种迁移学习启发式方法的可视化,在 NLP 中我们将尽可能利用它。随着目标领域中的训练数据的增加,阈值向左移动,它右侧的所有参数都被重新训练,除了那些由于源领域和目标领域越来越不同而被丢弃的参数。

在 CNN 中已经确定,靠近输入层的早期层—执行更一般的图像处理任务的功能,例如检测图像中的任何边缘。 靠近输出层的后期层—执行更特定于手头任务的功能,例如将最终的数值输出映射到特定标签。 这种安排导致我们首先解冻和微调靠近输出层的层,然后逐渐解冻和微调接近输入层的层,如果发现性能不满意,这个过程将继续,只要目标任务的可用标记数据集能够支持训练参数的增加。

这个过程的一个推论是,如果目标任务的标记数据集非常大,整个网络可能都需要被微调。另一方面,如果目标数据集很小,就需要仔细考虑目标数据集与源数据集的相似程度。如果非常相似,模型体系结构可以直接初始化为预训练权重进行微调。如果非常不同,当初始化时,放弃一些网络的后续层的预训练权重可能会对目标任务没有任何相关性。此外,由于数据集不是很大,在微调时应该只解冻剩余后续层的一小部分。

我们将进行计算实验,以进一步探索这些启发式方法。

1.5 为什么 NLP 迁移学习是一个令人兴奋的研究课题?

现在我们已经在整体人工智能和机器学习领域的背景下框定了 NLP 的当前状态,我们可以很好地总结为什么本书的主题重要,以及为什么您作为读者应该非常关心这个主题。

到目前为止,显而易见的是,近年来这一领域的进展迅速加速。许多预训练语言模型首次提供,同时也提供了明确定义的程序,用于对其进行更具体的任务或领域的微调。人们发现可以类比于计算机视觉领域进行迁移学习的方式,一些研究小组能够迅速借鉴现有的计算机视觉技术,推动我们对 NLP 迁移学习的了解的进展。这项工作取得了重要的优势,即为那些没有大量资源的普通从业者减少了这些问题的计算和训练时间要求。

目前该领域存在着大量的激动人心的研究,并且大量的研究人员正在从事这个问题领域的研究。在这个新颖的学科中存在许多未解决的问题,这为机器学习研究人员通过帮助推动知识水平的提高而使自己出名提供了机会。同时,社交媒体已经成为人类互动中越来越重要的因素,它带来了在自然语言处理中以前未曾见过的新挑战。这些挑战包括俚语/行话和表情符号的使用,这些在通常用于训练语言模型的更正式语言中可能找不到。一个示例是在社交媒体自然语言生态系统中发现的严重漏洞——尤其是关于主权民主国家针对其他外国政府的选举干预指控,比如剑桥分析丑闻。此外,对“假新闻”问题恶化的一般感觉增加了人们对该领域的兴趣,并推动了在构建这些系统时应考虑的道德问题的讨论。所有这些,加上在各个领域不断增长的越来越复杂的聊天机器人的增加,以及相关的网络安全威胁,意味着自然语言处理中的迁移学习问题有望继续增长其重要性。

总结

  • 人工智能(AI)承诺着从根本上改变我们的社会。为了使这种转变的好处普及化,我们必须确保最新的进展对每个人都是可访问的,无论其语言、获取大规模计算资源的能力和出生国是什么。

  • 机器学习是人工智能中主要的现代范式,它不是为每种可能的情况明确地编程计算机,而是通过看到许多这样对应的输入-输出对的例子,训练它将输入与输出信号关联起来。

  • 自然语言处理(NLP)是我们将在本书中讨论的人工智能的子领域,它涉及对人类自然语言数据的分析和处理,是当今人工智能研究中最活跃的领域之一。

  • 近年来在自然语言处理领域中流行的一种范式,迁移学习,使你能够将从一个任务或领域中获得的知识适应或迁移到另一个任务或领域。这对于自然语言处理的民主化以及更广泛地说是人工智能,是一个重要的进步,使得知识可以在新环境中以前所需资源的一小部分重新使用,而这些资源可能并不是所有人都能得到的。

  • 关键的建模框架,使得在自然语言处理中实现迁移学习成为可能,包括 ELMo 和 BERT。

  • 社交媒体重要性的近期上升改变了什么被认为是自然语言的定义。现在,数十亿人在网上使用表情符号、新创造的俚语和故意拼写错误的单词来表达自己。所有这些都提出了新的挑战,在开发新的自然语言处理迁移学习技术时我们必须考虑到这些挑战。

  • 在计算机视觉中,迁移学习相对较为成熟,因此在尝试新的自然语言处理迁移技术时,我们应尽可能借鉴这一知识体系。

  1. K. Schwab,《第四次工业革命》(日内瓦:世界经济论坛,2016 年)。

  2. J. Devlin 等人,“BERT: 深度双向转换器的预训练”,arXiv (2018)。

  3. F. Chollet,《Python 深度学习》(纽约:Manning Publications,2018 年)。

  4. T. Mikolov 等人,“词表示在向量空间中的高效估计”,arXiv (2013)。

  5. M. Pagliardini 等人,“使用组合 n-Gram 特征的句子嵌入的无监督学习”,NAACL-HLT 论文集 (2018)。

  6. Q. V. Le 等人,“句子和文档的分布式表示”,arXiv (2014)。

  7. I. Sutskever 等人,“序列到序列学习的神经网络”,NeurIPS 论文集 (2014)。

  8. A. Vaswani 等人,“注意力就是一切”,NeurIPS 论文集 (2017)。

  9. X. Zhang 等人,“用于文本分类的字符级卷积网络”,NeurIPS 论文集 (2015)。

  10. P. Azunre 等人,“基于字符级卷积神经网络的表格数据集的语义分类”,arXiv (2019)。

  11. M. E. Peters 等人,“深层上下文化词表示”,NAACL-HLT 论文集 (2018)。

  12. J. Howard 等人,“通用语言模型微调用于文本分类”,第 56 届计算语言学年会论文集 (2018)。

  13. J. Devlin 等人,“BERT: 深度双向转换器的预训练”,NAACL-HLT 论文集 (2019)。

  14. J. Deng 等人,“ImageNet:一个大规模分层图像数据库”,NAACL-HLT 论文集 (2019)。

  15. K. Schaffer,《数据与民主:大数据算法如何塑造观点并改变历史进程》(纽约:Apress,2019 年)。

第二章:开始使用基线:数据预处理

本章包括

  • 介绍一对自然语言处理(NLP)问题

  • 获取和预处理用于此类问题的自然语言处理数据

  • 使用关键的广义线性方法为这些问题建立基线

在本章中,我们直接着手解决自然语言处理问题。这将是一个分为两部分的练习,横跨本章和下一章。我们的目标是为一对具体的自然语言处理问题建立一组基线,以便稍后用于衡量利用越来越复杂的迁移学习方法获得的渐进改进。在此过程中,我们旨在提升您的一般自然语言处理直觉,并更新您对为此类问题设置问题解决流程所涉及的典型程序的理解。您将复习从分词到数据结构和模型选择等技术。我们首先从头开始训练一些传统的机器学习模型,为这些问题建立一些初步的基线。我们在第三章中完成练习,在那里我们将最简单形式的迁移学习应用于一对最近流行的深度预训练语言模型。这涉及在目标数据集上仅微调每个网络的最终几层。这项活动将作为本书主题——自然语言处理的迁移学习的实际动手介绍的一种形式。

我们将专注于一对重要的代表性示例自然语言处理问题:电子邮件的垃圾分类和电影评论的情感分类。这个练习将装备您一些重要的技能,包括一些获取、可视化和预处理数据的技巧。我们将涵盖三种主要的模型类别:广义线性模型,如逻辑回归,基于决策树的模型,如随机森林,以及基于神经网络的模型,如 ELMo。这些类别另外由具有线性核的支持向量机(SVM),梯度提升机(GBM)和 BERT 所代表。要探索的不同类型的模型如图 2.1 所示。请注意,我们不明确讨论基于规则的方法。这些方法的一个广泛使用的示例是简单的关键词匹配方法,该方法会将包含某些预先选择的短语的所有电子邮件标记为垃圾邮件,例如,“免费彩票”作为垃圾邮件,“了不起的电影”作为正面评价。这些方法通常作为许多工业应用中解决自然语言处理问题的首次尝试,但很快被发现脆弱且难以扩展。因此,我们不再深入讨论基于规则的方法。我们在本章讨论这些问题的数据及其预处理,并引入和应用广义线性方法。在下一章,作为整体练习的第二部分,我们将决策树方法和神经网络方法应用于数据。

02_01

图 2.1 本章和下一章将探讨文本分类示例中不同类型的监督模型。

我们为每个示例和模型类别提供了代码示例,让你能快速掌握这些技术的要点,同时也可以培养编码技巧,以便能够直接应用到自己的问题中。所有代码都以渲染后的 Jupyter 笔记本形式提供在本书的伴随 GitHub 代码库,以及 Kaggle 笔记本/内核中。你可以在几分钟内开始运行 Kaggle 笔记本/内核,而无需处理任何安装或依赖问题。渲染后的 Jupyter 笔记本提供了在正确执行时可以预期的输出示例,而 Kaggle 提供了基于浏览器的 Jupyter 执行环境,同时还提供了有限的免费 GPU 计算资源。虽然 Google Colab 是 Jupyter 的主要替代方案之一,但我们选择在这里使用 Kaggle。你也可以使用 Anaconda 在本地轻松安装 Jupyter,并欢迎将笔记本转换为 .py 脚本以供本地执行,如果你更喜欢的话。然而,我们推荐使用 Kaggle 笔记本来执行这些方法,因为它们可以让你立即开始,无需任何设置延迟。此外,在撰写本文时,该服务提供的免费 GPU 资源扩大了所有这些方法的可访问性,使那些可能没有本地强大 GPU 资源的人们也能够使用,与关于 NLP 迁移学习的“人工智能民主化”议程保持一致,这激发了很多人的兴趣。附录 A 提供了一个 Kaggle 快速入门指南,以及作者对如何最大化平台价值的个人建议。然而,我们预计大多数读者应该可以很轻松地开始使用它。请注意在下面的注释中附带的重要技术注意事项。

注意 Kaggle 经常更新依赖项,即其 Docker 镜像上安装的库的版本。为了确保您使用的是我们编写代码时使用的相同依赖项——以保证代码可以直接使用而进行最小更改,请确保对感兴趣的每个笔记本选择“复制并编辑内核”,这些笔记本的链接列在本书的伴随存储库中。如果您将代码复制粘贴到一个新的笔记本中,并且不遵循此推荐过程,您可能需要针对创建它时为该笔记本安装的特定库版本稍微调整代码。如果选择在本地环境中安装,请注意我们在伴随存储库中共享的冻结依赖要求列表,该列表将指导您需要哪些库的版本。请注意,此要求文件旨在记录并完全复制在 Kaggle 上实现书中报告结果的环境;在不同的基础设施上,它只能用作指南,并且您不应期望它直接使用,因为可能存在许多潜在的与架构相关的依赖冲突。此外,对于本地安装,大多数要求都不是必需的。最后,请注意,由于在撰写本文时 ELMo 尚未移植到 TensorFlow 2.x,我们被迫使用 TensorFlow 1.x 来公平比较它和 BERT。在伴随存储库中,我们确实提供了如何在 TensorFlow 2.x 中使用 BERT 进行垃圾邮件分类示例的示例。我们在后续章节中从 TensorFlow 和 Keras 过渡到使用 TensorFlow 2.x 的 Hugging Face transformers 库。您可以将第二章和第三章中的练习视为早期为 NLP 迁移学习开发的早期软件包的历史记录和体验。这个练习同时帮助您将 TensorFlow 1.x 与 2.x 进行对比。

2.1 预处理电子邮件垃圾分类示例数据

在本节中,我们介绍了本章将要讨论的第一个示例数据集。在这里,我们有兴趣开发一个算法,它可以在规模上检测任何给定的电子邮件是否为垃圾邮件。为此,我们将从两个独立的来源构建数据集:流行的恩隆电子邮件语料库作为非垃圾邮件的代理,以及一系列“419”欺诈邮件作为垃圾邮件的代理。

我们将把这看作是一个监督分类任务,在这个任务中,我们将首先在一组被标记为垃圾邮件或非垃圾邮件的电子邮件上训练一个分类器。虽然在线上存在一些标记数据集用于训练和测试,与这个问题密切相关,但我们将采取另一种方式,从一些其他知名的电子邮件数据源创建我们自己的数据集。这样做的原因是更贴近实践中数据收集和预处理通常发生的方式,其中数据集首先必须被构建和筛选,而不是文献中这些过程通常被简化的方式。

尤其是,我们将采样安然公司语料库——最大的公开电子邮件收集,与臭名昭著的安然金融丑闻有关——作为非垃圾邮件的代理,以及采样“419”欺诈邮件,代表最为知名的垃圾邮件类型,作为垃圾邮件的代理。这两种类型的电子邮件都可以在 Kaggle 上公开获取,³,⁴,这是一个流行的数据科学竞赛平台,这使得在那里运行示例特别容易,而不需要太多的本地资源。

安然语料库包含大约五十万封由安然公司员工撰写的电子邮件,由联邦能源委员会收集,用于调查该公司的倒闭。这个语料库在文献中被广泛用于研究用于电子邮件应用的机器学习方法,并且通常是研究人员与电子邮件一起进行初步算法原型实验的首选数据源。在 Kaggle 上,它作为一个单列.csv 文件提供,每行一个电子邮件。请注意,与许多实际应用中可能找到的情况相比,此数据仍然更干净。

图 2.2 显示了在这个示例中将在每封电子邮件上执行的步骤序列。电子邮件的正文将首先与电子邮件的标头分开,将提取一些关于数据集的统计信息以了解数据的情况,将从电子邮件中删除停用词,然后将其分类为垃圾邮件或非垃圾邮件。

02_02

图 2.2 对输入电子邮件数据执行的预处理任务序列

2.1.1 加载和可视化安然公司语料库

我们需要做的第一件事是使用流行的 Pandas 库加载数据,并查看数据的一个切片,以确保我们对数据的外观有一个良好的了解。清单 2.1 展示了一旦获取了安然公司语料库数据集并放置在变量filepath指定的位置(在这种情况下,它指向我们 Kaggle 笔记本中的位置)后,要执行的代码。在导入之前,请确保所有库都已通过以下命令进行 PIP 安装:

pip install <package name>

清单 2.1 加载安然公司语料库

import numpy as np                      ❶
import pandas as pd                     ❷

filepath = "../input/enron-email-dataset/emails.csv"

emails = pd.read_csv(filepath)          ❸

print("Successfully loaded {} rows and {} columns!".format(emails.shape[0],  emails.shape[1]))                  ❹
print(emails.head(n=5))

❶ 线性代数

❷ 数据处理,CSV 文件输入输出(例如,pd.read_csv)

❸ 将数据读入名为 emails 的 Pandas DataFrame 中

❹ 显示状态和一些加载的电子邮件

执行代码成功后将确认加载的列数和行数,并显示加载的 Pandas DataFrame 的前五行,输出如下所示:

Successfully loaded 517401 rows and 2 columns!

                       file                                            message
0     allen-p/_sent_mail/1\.  Message-ID: <18782981.1075855378110.JavaMail.e...
1    allen-p/_sent_mail/10\.  Message-ID: <15464986.1075855378456.JavaMail.e...
2   allen-p/_sent_mail/100\.  Message-ID: <24216240.1075855687451.JavaMail.e...
3  allen-p/_sent_mail/1000\.  Message-ID: <13505866.1075863688222.JavaMail.e...
4  allen-p/_sent_mail/1001\.  Message-ID:<30922949.1075863688243.JavaMail.e...

尽管这个练习让我们对结果 DataFrame 有了一个了解,并形成了一个很好的形状感觉,但还不太清楚每封单独的电子邮件是什么样子。为了达到这个目的,我们通过下一行代码仔细检查第一封电子邮件

print(emails.loc[0]["message"])

产生以下输出:

Message-ID: <18782981.1075855378110.JavaMail.evans@thyme>
Date: Mon, 14 May 2001 16:39:00 -0700 (PDT)
From: phillip.allen@enron.com
To: tim.belden@enron.com
Subject: 
Mime-Version: 1.0
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
X-From: Phillip K Allen
X-To: Tim Belden <Tim Belden/Enron@EnronXGate>
X-cc: 
X-bcc: 
X-Folder: \Phillip_Allen_Jan2002_1\Allen, Phillip K.\'Sent Mail
X-Origin: Allen-P
X-FileName: pallen (Non-Privileged).pst

Here is our forecast

我们发现消息都包含在结果 DataFrame 的 message 列中,每条消息开头的额外字段——包括 Message IDToFrom 等——被称为消息的 头信息 或简称头部

传统的垃圾邮件分类方法是从头信息中提取特征来对消息进行分类。在这里,我们希望仅基于消息内容执行相同的任务。采用该方法的一个可能动机是,由于隐私问题和法规的原因,电子邮件训练数据在实践中经常会被去标识化,因此头部信息是不可用的。因此,我们需要在数据集中将头部信息与消息分离。我们通过下面的函数来实现这一点。它使用了 Python 预装的电子邮件包来处理电子邮件消息(因此无需通过 PIP 进行安装)。

列表 2.2 头信息分离和提取电子邮件正文

import email

def extract_messages(df):
    messages = []
    for item in df["message"]:
        e = email.message_from_string(item)      ❶
        message_body = e.get_payload()           ❷
        messages.append(message_body)
    print("Successfully retrieved message body from emails!")
    return messages

❶ 从字符串返回消息对象结构

❷ 获取消息正文

现在我们执行提取电子邮件正文的代码如下:

bodies = extract_messages(emails)

通过以下文本确认成功打印到屏幕:

Successfully retrieved message body from emails!

然后我们可以通过以下方式显示一些已处理的电子邮件:

bodies_df = pd.DataFrame(bodies)
print(bodies_df.head(n=5))

显示确认成功执行,输出如下:

                                                  0
0                 Here is our forecast\n\n 
1                Traveling to have a business meeting takes the...
2                 test successful.  way to go!!!
3                Randy,\n\n Can you send me a schedule of the s...
4                Let's shoot for Tuesday at 11:45\. 

2.1.2 加载和可视化欺诈邮件语料库

加载了 Enron 电子邮件之后,让我们对“419”欺诈邮件语料库做同样的操作,这样我们就可以在训练集中有一些代表垃圾邮件类别的示例数据。从前面呈现的 Kaggle 链接获取数据集,确保相应调整filepath变量(或者直接使用我们的 Kaggle 笔记本,已经包含了数据),然后按照列表 2.3 中所示重复执行步骤。

注意 因为这个数据集是以 .txt 文件格式提供的,而不是 .csv 文件,因此预处理步骤略有不同。首先,我们必须在读取文件时指定编码为 Latin-1;否则,默认编码选项 UTF-8 会失败。实际上经常出现这样的情况,需要尝试多种不同的编码方式,其中前面提到的两种是最受欢迎的,来使一些数据集能够正确读取。此外,需要注意的是,由于这个 .txt 文件是一大列带有标题的电子邮件(用换行符和空白分隔),并且没有很好地分隔成每行一个电子邮件,而并不像 Enron 语料库那样整齐地分隔成各行各个邮件,我们无法像之前那样使用 Pandas 将其整齐地加载。我们将所有的邮件读入一个字符串,然后根据出现在每封邮件标题开头附近的代码词进行分割,例如,“From r.” 请查看我们在 GitHub 或 Kaggle 上呈现的笔记本来验证此数据上是否存在这个独特代码词出现在每封此数据集中的诈骗邮件的开头附近。

列表 2.3 加载“419”诈骗邮件语料库

filepath = "../input/fraudulent-email-corpus/fradulent_emails.txt"
with open(filepath, 'r',encoding="latin1") as file:
    data = file.read()

fraud_emails = data.split("From r")     ❶

print("Successfully loaded {} spam emails!".format(len(fraud_emails)))

❶ 在每封电子邮件开头附近的代码词上进行分割

以下输出证实了加载过程的成功:

Successfully loaded 3978 spam emails!

现在,伪造的数据已经以列表的形式加载,我们可以将其转换为 Pandas DataFrame,以便用我们已经定义的函数来处理,具体如下:

fraud_bodies = extract_messages(pd.DataFrame(fraud_emails,columns=["message"],dtype=str))
fraud_bodies_df = pd.DataFrame(fraud_bodies[1:])
print(fraud_bodies_df.head())

成功执行此代码段将导致输出,让我们对加载的前五封邮件有所了解,如下所示:

Successfully retrieved message body from e-mails!
                                                   0
0  FROM:MR. JAMES NGOLA.\nCONFIDENTIAL TEL: 233-27-587908.\nE-MAIL: (james_ngola2002@maktoob.com).\n\nURGENT BUSINESS ASSISTANCE AND PARTNERSHIP.\n\n\nDEAR FRIEND,\n\nI AM ( DR.) JAMES NGOLA, THE PERSONAL ASSISTANCE TO THE LATE CONGOLESE (PRESIDENT LAURENT KABILA) WHO WAS ASSASSINATED BY HIS BODY G...
1  Dear Friend,\n\nI am Mr. Ben Suleman a custom officer and work as Assistant controller of the Customs and Excise department Of the Federal Ministry of Internal Affairs stationed at the Murtala Mohammed International Airport, Ikeja, Lagos-Nigeria.\n\nAfter the sudden death of the former Head of s...
2  FROM HIS ROYAL MAJESTY (HRM) CROWN RULER OF ELEME KINGDOM \nCHIEF DANIEL ELEME, PHD, EZE 1 OF ELEME.E-MAIL \nADDRESS:obong_715@epatra.com  \n\nATTENTION:PRESIDENT,CEO Sir/ Madam. \n\nThis letter might surprise you because we have met\nneither in person nor by correspondence. But I believe\nit is...
3  FROM HIS ROYAL MAJESTY (HRM) CROWN RULER OF ELEME KINGDOM \nCHIEF DANIEL ELEME, PHD, EZE 1 OF ELEME.E-MAIL \nADDRESS:obong_715@epatra.com  \n\nATTENTION:PRESIDENT,CEO Sir/ Madam. \n\nThis letter might surprise you because we have met\nneither in person nor by correspondence. But I believe\nit is...
4  Dear sir, \n \nIt is with a heart full of hope that I write to seek your help in respect of the context below. I am Mrs. Maryam Abacha the former first lady of the former Military Head of State of Nigeria General Sani Abacha whose sudden death occurred on 8th of June 1998 as a result of cardiac ...

在加载了两个数据集之后,我们现在准备从每个数据集中抽样电子邮件到一个单独的 DataFrame 中,该 DataFrame 将代表覆盖两类电子邮件的整体数据集。在这样做之前,我们必须决定从每个类别中抽取多少样本。理想情况下,每个类别中的样本数量将代表野外电子邮件的自然分布——如果我们希望我们的分类器在部署时遇到 60% 的垃圾邮件和 40% 的非垃圾邮件,那么 600 和 400 的比率可能是有意义的。请注意,数据的严重不平衡,例如 99% 的非垃圾邮件和 1% 的垃圾邮件,可能会过度拟合以大多数时间预测非垃圾邮件,这是在构建数据集时需要考虑的问题。由于这是一个理想化的实验,我们没有任何关于类别自然分布的信息,我们将假设是 50/50 的分布。我们还需要考虑如何对电子邮件进行标记化,即将电子邮件分割成文本的子单元——单词、句子等等。首先,我们将标记化为单词,因为这是最常见的方法。我们还必须决定每封电子邮件的最大标记数和每个标记的最大长度,以确保偶尔出现的极长电子邮件不会拖慢分类器的性能。我们通过指定以下通用超参数来完成所有这些工作,稍后将通过实验调整以根据需要提高性能:

Nsamp = 1000         ❶
maxtokens = 50       ❷
maxtokenlen = 20     ❸

❶ 每个类别生成的样本数——垃圾邮件和非垃圾邮件

❷ 每个文档的最大标记数

❸ 每个标记的最大长度

有了这些指定的超参数,我们现在可以为全局训练数据集创建一个单独的 DataFrame。让我们利用这个机会执行剩余的预处理任务,即删除停用词、标点符号和标记化。

让我们通过定义一个函数来对邮件进行标记化,将它们分割成单词,如下列表所示。

列表 2.4 将每封电子邮件标记化为单词

def tokenize(row):
    if row in [None,'']:
        tokens = ""
    else:
        tokens = str(row).split(" ")[:maxtokens]    ❶
    return tokens

❶ 按空格分割每个电子邮件字符串,以创建单词标记列表。

再次查看前两页的电子邮件,我们发现它们包含大量的标点符号,并且垃圾邮件往往是大写的。为了确保分类仅基于语言内容进行,我们定义了一个函数,用于从电子邮件中删除标点符号和其他非单词字符。我们通过使用 Python 的 regex 库来使用正则表达式实现这一点。我们还通过使用 Python 字符串函数 .lower() 将单词转换为小写来规范化单词。预处理函数如下列表所示。

列表 2.5 从电子邮件中删除标点符号和其他非单词字符

import re
def reg_expressions(row):
    tokens = []
    try:
        for token in row:
            token = token.lower()
            token = re.sub(r'[\W\d]', "", token)    ❶
            token = token[:maxtokenlen]             ❷
            tokens.append(token)
    except:
        token = ""
        tokens.append(token)
    return tokens

❶ 匹配并移除任何非单词字符。

❷ 截断标记

最后,让我们定义一个函数来移除停用词—在语言中频繁出现但对分类没有用的词。这包括诸如“the”和“are”等词,在流行的库 NLTK 中提供了一个被广泛使用的列表,我们将使用它。停用词移除函数在下一个清单中展示。请注意,NLTK 还有一些用于去除标点的方法,作为清单 2.5 所做的替代方法。

列表 2.6 移除停用词

import nltk

nltk.download('stopwords')
from nltk.corpus import stopwords
stopwords = stopwords.words('english')    

def stop_word_removal(row):
    token = [token for token in row if token not in stopwords]    ❶
    token = filter(None, token)                                   ❷
    return token

❶ 这就是从标记列表中实际移除停用词的地方。

❷ 同样移除空字符串—'',None 等等

现在我们将把所有这些功能整合在一起,构建代表两个类别的单一数据集。该过程在下一个代码清单中演示。在那段代码中,我们将合并的结果转换为 NumPy 数组,因为这是许多我们将使用的库所期望的输入数据格式。

列表 2.7 将预处理步骤组合在一起构建电子邮件数据集

import random

EnronEmails = bodies_df.iloc[:,0].apply(tokenize)              ❶
EnronEmails = EnronEmails.apply(stop_word_removal)
EnronEmails = EnronEmails.apply(reg_expressions)
EnronEmails = EnronEmails.sample(Nsamp)                        ❷

SpamEmails = fraud_bodies_df.iloc[:,0].apply(tokenize)
SpamEmails = SpamEmails.apply(stop_word_removal)
SpamEmails = SpamEmails.apply(reg_expressions)
SpamEmails = SpamEmails.sample(Nsamp)

raw_data = pd.concat([SpamEmails,EnronEmails], axis=0).values  ❸

❶ 应用预定义的处理函数

❷ 从每个类别中抽取正确数量的电子邮件样本

❸ 转换为 NumPy 数组

现在让我们来看一眼结果,确保事情正在按预期进行:

print("Shape of combined data represented as NumPy array is:")
print(raw_data.shape)
print("Data represented as NumPy array is:")
print(raw_data)

这产生了如下输出:

Shape of combined data represented as NumPy array is:
(2000, )
Data represented as NumPy array is:
'got' ... ]
 ['dear', 'friend' ' my' ...]
 ['private’, ‘confidential' 'friend', 'i' ... ]
 ...

我们看到生成的数组已经将文本分割成了单词单位,正如我们想要的。

让我们创建相应的与这些电子邮件对应的头,包括Nsamp=1000 封垃圾邮件,然后是Nsamp=1000 封非垃圾邮件,如下所示:

Categories = ['spam','notspam']
header = ([1]*Nsamp)
header.extend(([0]*Nsamp))

现在我们已经准备将这个 NumPy 数组转换为可以实际输入到分类算法中的数值特征。

2.1.3 将电子邮件文本转换为数字

在本章中,我们首先采用了通常被认为是最简单的将单词向量化的方法,即将它们转换为数字向量——袋词模型。该模型简单地计算每封电子邮件中包含的单词标记的频率,从而将其表示为这种频率计数的向量。我们在清单 2.8 中提供了组装电子邮件的词袋模型的函数。请注意,通过这样做,我们只保留出现超过一次的标记,如变量used_tokens所捕获的那样。这使我们能够将向量维度保持得比其他情况下低得多。还请注意,可以使用流行的库 scikit-learn 中的各种内置矢量化器来实现这一点(我们的 Jupyter 笔记本展示了如何做到这一点)。但是,我们专注于清单 2.8 中所展示的方法,因为我们发现这比使用实现相同功能的黑匣子函数更具说明性。我们还注意到,scikit-learn 的向量化方法包括计算任意n个单词序列或n-gram的出现次数,以及tf-idf方法—如果有生疏的话,这些是您应该复习的重要基本概念。在这里展示的问题中,当使用这些向量化方法时,我们并未注意到与使用词袋模型方法相比的改进。

清单 2.8 组装词袋表示

def assemble_bag(data):
    used_tokens = []
    all_tokens = []

    for item in data:
        for token in item:
            if token in all_tokens:             ❶
                if token not in used_tokens:
                    used_tokens.append(token)
            else:
                all_tokens.append(token)

    df = pd.DataFrame(0, index = np.arange(len(data)), columns = used_tokens)

    for i, item in enumerate(data):             ❷
        for token in item:
            if token in used_tokens:
                df.iloc[i][token] += 1    
    return df

❶ 如果标记之前已经见过,将其附加到用过的标记输出列表

❷ 创建 Pandas DataFrame 计数词汇单词的频率——对应于每封电子邮件的列——对应于行

定义了assemble_bag函数之后,让我们使用它来实际执行向量化并将其可视化如下:

EnronSpamBag = assemble_bag(raw_data)
print(EnronSpamBag)
predictors = [column for column in EnronSpamBag.columns]

输出 DataFrame 的一个片段如下所示:

     fails  report s  events   may   compliance  stephanie  
0         0         0        0     0           0          0  
1         0         0        0     0           0          0  
2         0         0        0     0           0          0  
3         0         0        0     0           0          0  
4         0         0        0     0           0          0  
...     ...       ...      ...   ...         ...        ...  
1995      1         2        1     1           1          0  
1996      0         0        0     0           0          0  
1997      0         0        0     0           0          0  
1998      0         0        0     0           0          1  
1999      0         0        0     0           0          0  

[2000 rows x 5469 columns]

列标签指示词袋模型的词汇中的单词,每行中的数字条目对应于我们数据集中 2000 封电子邮件中每个此类单词的频率计数。请注意,这是一个极为稀疏的 DataFrame——它主要由值0组成。

将数据集完全向量化后,我们必须记住它与类别无关的洗牌;也就是说,它包含Nsamp = 1000 封垃圾邮件,然后是相同数量的非垃圾邮件。根据如何拆分此数据集——在我们的情况下,通过选择前 70%用于训练,剩余部分用于测试——这可能导致训练集仅由垃圾邮件组成,这显然会导致失败。为了在数据集中创建类样本的随机排序,我们需要与标头/标签列表一起洗牌数据。下一个清单中显示了实现此目的的函数。同样,可以使用内置的 scikit-learn 函数实现相同的效果,但我们发现下一个清单中显示的方法更具说明性。

清单 2.9 与标头/标签列表一起洗牌数据

def unison_shuffle_data(data, header):
    p = np.random.permutation(len(header))
    data = data[p]
    header = np.asarray(header)[p]
    return data, header

作为准备电子邮件数据集以供基线分类器训练的最后一步,我们将其分割为独立的训练和测试,或验证集。这将允许我们评估分类器在未用于训练的一组数据上的性能——这是机器学习实践中必须确保的重要事情。我们选择使用 70% 的数据进行训练,然后进行 30% 的测试/验证。下面的代码调用了同步洗牌函数,然后执行了训练/测试分割。生成的 NumPy 数组变量train_xtrain_y将直接传递给本章后续部分中的分类器:

data, header = unison_shuffle_data(EnronSpamBag.values, header)
idx = int(0.7*data.shape[0])       ❶
train_x = data[:idx]
train_y = header[:idx]
test_x = data[idx:]                ❷
test_y = header[idx:]

❶ 使用 70% 的数据进行训练

❷ 使用剩余 30% 进行测试

希望这个为机器学习任务构建和预处理 NLP 数据集的练习现在已经完成,使您具备了可应用于自己项目的有用技能。现在我们将继续处理第二个说明性示例的预处理,该示例将在本章和下一章中使用,即互联网电影数据库(IMDB)电影评论的分类。鉴于 IMDB 数据集比我们组装的电子邮件数据集更为准备充分,因此该练习将更为简短。然而,鉴于数据按类别分开放置在不同文件夹中,这是一个突出显示不同类型预处理要求的机会。

2.2 预处理电影情感分类示例数据

在本节中,我们对将在本章中分析的第二个示例数据集进行预处理和探索。这第二个示例涉及将 IMDB 中的电影评论分类为正面或负面情绪表达。这是一个典型的情感分析示例,在文献中被广泛使用来研究许多算法。我们提供了预处理数据所需的代码片段,并鼓励您在阅读时运行代码以获得最佳的教育价值。

对于此,我们将使用一个包含 25,000 条评论的流行标记数据集,⁵该数据集是通过从流行的电影评论网站 IMDB 抓取数据并将每条评论对应的星级数量映射到 0 或 1(如果它小于或大于 10 颗星),而组装而成。⁶这个数据集在先前的 NLP 文献中被广泛使用,我们选择它作为基线的说明性示例的原因之一就是因为人们对它熟悉。

在分析每个 IMDB 电影评论之前使用的预处理步骤序列与图 2.2 中呈现的用于电子邮件垃圾邮件分类示例非常相似。第一个主要的区别是这些评论没有附加电子邮件标题,因此无需进行标题提取步骤。此外,由于包括“no”和“not”等一些停用词可能会改变消息的情感,因此从目标列表中删除停用词的步骤可能需要特别小心。我们确实尝试了从列表中删除这些单词,并且发现对结果几乎没有影响。这可能是因为评论中的其他非停用词非常具有预测特征,使得这一步骤变得无关紧要。因此,尽管我们在 Jupyter 笔记本中向您展示了如何做到这一点,但我们在这里不再讨论这个问题。

让我们直接着手准备 IMDB 数据集,就像我们在上一节中组装电子邮件数据集那样。您可以通过以下 shell 命令在我们的 Jupyter 笔记本中下载并提取 IMDB 数据集:

!wget -q "http:/ /ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz"
!tar xzf aclImdb_v1.tar.gz

注意,命令开头的感叹号标志!告诉解释器这些是 shell 命令,而不是 Python 命令。还要注意,这是一个 Linux 命令。如果您在 Windows 上本地运行此代码,则可能需要手动从提供的链接下载和解压文件。这将生成两个子文件夹 - aclImdb/pos/ 和 aclImdb/neg/ - 我们使用以下列表中的函数和其调用脚本对其进行标记化,删除停用词和标点,并进行随机处理,并将其加载到 NumPy 数组中。

列表 2.10 将 IMDB 数据加载到 NumPy 数组中

def load_data(path):
    data, sentiments = [], []
    for folder, sentiment in (('neg', 0), ('pos', 1)):
        folder = os.path.join(path, folder)
        for name in os.listdir(folder):                        ❶
            with open(os.path.join(folder, name), 'r') as reader:
                  text = reader.read()
            text = tokenize(text)                              ❷
            text = stop_word_removal(text)
            text = reg_expressions(text)
            data.append(text)
            sentiments.append(sentiment)                       ❸
    data_np = np.array(data)                                   ❹
    data, sentiments = unison_shuffle_data(data_np, sentiments)

    return data, sentiments

train_path = os.path.join('aclImdb', 'train')                  ❺
raw_data, raw_header = load_data(train_path)

❶ 遍历当前文件夹中的每个文件

❷ 应用分词和停用词分析程序

❸ 跟踪相应的情感标签

❹ 转换为 NumPy 数组

❺ 对数据调用上面的函数

注意,在 Windows 上,您可能需要在清单 2.10 的 open 函数调用中指定参数 encoding=utf-8。检查加载数据的维度,以确保一切按预期运行,如下所示:

print(raw_data.shape)
print(len(raw_header))

这将产生以下结果:

(25000,)
25000

接下来,我们取加载数据的Nsamp*2个随机条目用于训练,如下所示:

random_indices = np.random.choice(range(len(raw_header)),size=(Nsamp*2,),replace=False)
data_train = raw_data[random_indices]
header = raw_header[random_indices]

在继续之前,我们需要检查所得数据在类方面的平衡情况。通常情况下,我们不希望其中一个标签代表大多数数据集,除非这是实践中预期的分布。使用以下代码检查标签分布:

unique_elements, counts_elements = np.unique(header, return_counts=True)
print("Sentiments and their frequencies:")
print(unique_elements)
print(counts_elements)

这将产生以下结果:

Sentiments and their frequencies:
[0 1]
[1019  981]

在确保数据在两个类之间大致平衡,并且每个类大致代表数据集的一半后,使用下面的代码组装和可视化词袋表示:

MixedBagOfReviews = assemble_bag(data_train)
print(MixedBagOfReviews)

通过这段代码片段生成的结果 DataFrame 的一个切片如下所示:

      ages  i  series         the  dream  the  movie  film  plays  ...  \
0        2  2       0     0     0      0    0      1     0      0  ...   
1        0  0       0     0     0      0    0      0     0      1  ...   
2        0  0       2     2     2      2    2      0     1      0  ...   
3        0  2       0     1     0      0    0      1     1      1  ...   
4        0  2       0     0     0      0    1      0     0      0  ...   
...    ... ..     ...   ...   ...    ...  ...    ...   ...    ...  ...   
1995     0  0       0     0     0      0    0      2     1      0  ...   
1996     0  0       0     0     0      0    0      1     0      0  ...   
1997     0  0       0     0     0      0    0      0     0      0  ...   
1998     0  3       0     0     0      0    1      1     1      0  ...   
1999     0  1       0     0     0      0    0      1     0      0  ... 

请注意,在此之后,您仍然需要将这个数据结构分割成训练集和验证集,类似于我们为垃圾邮件检测示例所做的操作。出于简洁起见,我们不在这里重复,但这段代码包含在配套的 Kaggle 笔记本中。

有了这个数值表示准备好后,我们现在继续在后续部分为这两个示例数据集构建基线分类器。我们从下一节开始使用广义线性模型。

2.3 广义线性模型

传统上,在任何应用数学领域模型的发展都是从线性模型开始的。这些模型是保留输入和输出空间中的加法和乘法的映射。换句话说,对一对输入的净响应将是对每个单独输入的响应的总和。这个属性使得相关的统计和数学理论显著减少。

在这里,我们使用了来自统计学的线性的宽松定义,即广义线性模型。设 Y 是输出变量或响应的向量,X 是独立变量的向量,β 是要由我们的分类器进行训练的未知参数的向量。广义线性模型由图 2.3 中的方程定义。

02_03

图 2.3 广义线性模型方程

在这里,E[] 代表所包含数量的期望值,右侧在 X 中是线性的,并且 g 是将这个线性数量链接到 Y 的期望值的函数。

在本节中,我们将应用一对最广泛使用的广义线性机器学习算法到前一节介绍的一对示例问题上——逻辑回归和带有线性核的支持向量机(SVM)。其他流行的广义线性机器学习模型不包括简单的带有线性激活函数的感知器神经架构、潜在狄利克雷分配(LDA)和朴素贝叶斯。

2.3.1 逻辑回归

逻辑回归通过使用逻辑函数估计概率,对分类输出变量和一组输入变量之间的关系进行建模。假设存在单个输入变量 x 和单个输出二进制变量 y,其相关概率为 P(y=1)=p,则逻辑方程可以表达为图 2.4 中的方程。

02_04

图 2.4 逻辑回归方程

这可以重新组织,以得到图 2.5 中显示的典型逻辑曲线方程。

02_05

图 2.5 重组后的典型逻辑回归方程

这个方程在图 2.6 中绘制。从历史上看,这条曲线起源于对细菌种群增长的研究,初始生长缓慢,中间爆炸性增长,随着资源耗尽,生长逐渐减弱。

02_06

图 2.6 典型的逻辑曲线绘图

现在让我们继续使用流行的库 scikit-learn 构建我们的分类器,使用下一节中显示的函数。

列表 2.11 构建逻辑回归分类器

from sklearn.linear_model import LogisticRegression

def fit(train_x,train_y):
    model = LogisticRegression()      ❶

    try:
        model.fit(train_x, train_y)   ❷
    except:
        pass
    return model

❶ 实例化模型

❷ 将模型拟合到准备好的、标记的数据上

要将这个模型拟合到我们的数据中,无论是电子邮件还是 IMDB 分类示例,我们只需要执行以下一行代码:

model = fit(train_x,train_y)

这应该在任何现代 PC 上只需几秒钟。要评估性能,我们必须在为每个示例准备的“保留”测试/验证集上进行测试。这可以使用以下代码执行:

predicted_labels = model.predict(test_x)
from sklearn.metrics import accuracy_score
acc_score = accuracy_score(test_y, predicted_labels)
print("The logistic regression accuracy score is::")
print(acc_score)

对于电子邮件分类示例,这将产生:

The logistic regression accuracy score is::
0.9766666666666667

对于 IMDB 语义分析示例,这将产生:

The logistic regression accuracy score is::
0.715

这似乎表明我们设置的垃圾邮件分类问题比 IMDB 电影评论问题更容易解决。在下一章的结尾,我们将讨论改进 IMDB 分类器性能的潜在方法。

在继续之前,解决使用准确度作为评估性能的指标是很重要的。准确度被定义为正确识别的样本的比率——真正例和真负例的比率与总样本数的比率。这里可以使用的其他潜在指标包括精确度——真正例与所有预测正例的比率——以及召回率——真正例与所有实际正例的比率。如果假阳性和假阴性的成本特别重要,这两个度量可能很有用。至关重要的是,F1 分数——精确度和召回率的调和平均值——在两者之间取得平衡,对于不平衡的数据集特别有用。这是实际中最常见的情况,因此这个指标非常重要。然而,记住我们迄今为止构建的数据集大致是平衡的。因此,在我们的情况下,准确度是一个合理的度量。

2.3.2 支持向量机(SVM)

SVM,在第一章中已经提到,一直是最受欢迎的核方法。这些方法尝试通过将数据映射到高维空间来找到好的决策边界,使用超平面作为决策边界,并使用核技巧来降低计算成本。当核函数是线性函数时,SVM 不仅是广义线性模型,而且确实是线性模型。

让我们继续使用下一版示例中的代码构建和评估 SVM 分类器在我们的两个运行示例问题上。请注意,由于该分类器的训练时间比逻辑回归分类器稍长,我们使用内置的 Python 库 time 来确定训练时间。

列表 2.12 训练和测试 SVM 分类器

import time
from sklearn.svm import SVC # Support Vector Classification model

clf = SVC(C=1, gamma="auto", kernel='linear',probability=False)     ❶

start_time = time.time()                                            ❷
clf.fit(train_x, train_y)
end_time = time.time()
print("Training the SVC Classifier took %3d seconds"%(end_time-start_time))

predicted_labels = clf.predict(test_x)                              ❸
acc_score = accuracy_score(test_y, predicted_labels)
print("The SVC Classifier testing accuracy score is::")
print(acc_score)

❶ 创建具有线性内核的支持向量分类器

❷ 使用训练数据拟合分类器

❸ 测试和评估

在电子邮件数据上训练 SVM 分类器共花费了 64 秒,并获得 0.670 的准确率得分。在 IMDB 数据上训练分类器花费 36 秒,并获得 0.697 的准确率得分。我们看到,对于电子垃圾邮件分类问题,SVM 的表现明显不如逻辑回归,而对于 IMDB 问题,它的表现虽然较低,但几乎可以相提并论。

在下一章中,我们将应用更加复杂的方法来对这两个分类问题进行基线处理,并比较各种方法的性能。特别是,我们将探索基于决策树的方法,以及流行的神经网络方法 ELMo 和 BERT。

摘要

  • 在任何感兴趣的问题上尝试各种算法以找到模型复杂度和性能的最佳组合以符合您特定的情况是很常见的。

  • 基线通常从最简单的算法开始,例如逻辑回归,并逐渐变得越来越复杂,直到得到正确的性能/复杂性权衡。

  • 机器学习实践的一大部分涉及为您的问题组装和预处理数据,目前这可能是该过程中最重要的部分。

  • 重要的模型设计选择包括评估性能的指标、指导训练算法的损失函数以及最佳验证实践等,这些因模型和问题类型而异。

  1. github.com/azunre/transfer-learning-for-nlp

  2. www.kaggle.com/azunre/tlfornlp-chapters2-3-spam-bert-tf2

  3. www.kaggle.com/wcukierski/enron-email-dataset

  4. www.kaggle.com/rtatman/fraudulent-email-corpus

  5. ai.stanford.edu/~amaas/data/sentiment

  6. A.L. Maas 等人,“学习词向量进行情感分析”,NAACL-HLT 会议论文集 (2018)。

第三章:开始基线: 基准测试和优化

本章内容包括

  • 分析一对自然语言处理(NLP)问题

  • 使用关键的传统方法建立问题基线

  • 使用代表性的深度预训练语言模型 ELMo 和 BERT 进行基线

在本章中,我们继续直接深入解决 NLP 问题,这是我们在上一章开始的。我们继续追求建立一套具体 NLP 问题的基线,稍后我们将能够利用这些基线来衡量从越来越复杂的迁移学习方法中获得的逐渐改进。我们完成了我们在第二章开始的练习,那里我们介绍了一对实际问题,预处理了相应的数据,并通过探索一些广义线性方法开始了基线。特别是,我们介绍了电子邮件垃圾邮件和互联网电影数据库(IMDB)电影评论分类示例,并使用了逻辑回归和支持向量机(SVM)来建立它们的基线。

在本章中,我们探讨基于决策树和基于神经网络的方法。我们研究的基于决策树的方法包括随机森林和梯度提升机。关于基于神经网络的方法,我们将最简单形式的迁移学习应用到了一对最近流行的深度预训练语言模型 ELMo 和 BERT 上。这项工作只涉及在目标数据集上对每个网络的最后几层进行微调。这项活动将作为本书主题的应用性实践介绍,即 NLP 的迁移学习。此外,我们探索通过超参数调优来优化模型的性能。

我们将在下一节中探讨基于决策树的方法。

3.1 基于决策树的模型

决策树是一种决策支持工具,它将决策及其后果建模为——一个图,其中任意两个节点都由一条路径连接。树的另一种定义是将输入值转换为输出类别的流程图。有关这种类型模型的更多详细信息,请参阅第一章。

在本节中,我们将两种最常见的基于决策树的方法——随机森林和梯度提升机——应用到我们的两个正在运行的问题上。

3.1.1 随机森林(RFs)

随机森林(RFs)通过生成大量专门的树并收集它们的输出,为应用决策树提供了一种实用的机器学习方法。RFs 非常灵活和广泛适用,通常是从逻辑回归后从业者尝试的第二种算法用于建立基线。有关 RFs 及其历史背景的更详细讨论,请参阅第一章。

让我们使用流行的库 scikit-learn 来构建我们的分类器,如下所示。

列出 3.1 训练和测试随机森林分类器

from sklearn.ensemble import RandomForestClassifier          ❶

clf = RandomForestClassifier(n_jobs=1, random_state=0)       ❷

start_time = time.time()                                     ❸
clf.fit(train_x, train_y)
end_time = time.time()
print("Training the Random Forest Classifier took %3d seconds"%(end_time-start_time))

predicted_labels = clf.predict(test_x)

acc_score = accuracy_score(test_y, predicted_labels)

print("The RF testing accuracy score is::")
print(acc_score)

❶ 加载 scikit 的随机森林分类器库

❷ 创建一个随机森林分类器

❸ 训练分类器,以了解训练特征与训练响应变量的关系

用这段代码在我们的实验中,对电子邮件示例数据进行 RF 分类器的训练只需不到一秒钟的时间,并且达到了 0.945 的准确率分数。类似地,在 IMDB 示例上进行训练也只需不到一秒钟,并且达到了 0.665 的准确率分数。这个练习进一步证实了上一章的最初猜测,即 IMDB 评论问题比电子邮件分类问题更难。

3.1.2 梯度提升机(GBMs)

这种基于决策树的机器学习算法的变体迭代地学习新的基于决策树的模型,以解决前次迭代模型的弱点。在撰写本文时,它们被普遍认为是解决非感知机器学习问题的最佳类方法。不幸的是,它们确实存在一些缺点,包括较大的模型大小、过拟合的风险更高以及比其他一些决策树模型更少的可解释性。

训练梯度提升机(GBM)分类器的代码显示在下一个列表中。同样,我们使用 scikit-learn 中这些模型的实现。请注意,Python 库 XGBoost 中的实现被普遍认为更具有内存效率,并且更容易扩展/并行化。

列表 3.2 训练/测试梯度提升机分类器

from sklearn.ensemble import GradientBoostingClassifier        ❶
from sklearn import metrics                                   ❷
from sklearn.model_selection import cross_val_score

def modelfit(alg, train_x, train_y, predictors, test_x, performCV=True, cv_folds=5):
    alg.fit(train_x, train_y)                                 ❸
    predictions = alg.predict(train_x)                        ❹
    predprob = alg.predict_proba(train_x)[:,1]
    if performCV:                                             ❺
        cv_score = cross_val_score(alg, train_x, train_y, cv=cv_folds, scoring='roc_auc')

    print("\nModel Report")                                  ❻
    print("Accuracy : %.4g" % metrics.accuracy_score(train_y,predictions))
    print("AUC Score (Train): %f" % metrics.roc_auc_score(train_y, predprob))
    if performCV:
        print("CV Score : Mean - %.7g | Std - %.7g | Min - %.7g | Max - %.7g" % 
(np.mean(cv_score),np.std(cv_score),np.min(cv_score),np.max(cv_score)))

    return alg.predict(test_x),alg.predict_proba(test_x)      ❼

❶ GBM 算法

❷ 附加的 sklearn 函数

❸ 在整体数据上拟合算法

❹ 预测训练集

❺ 执行 k 折交叉验证

❻ 打印模型报告

❼ 预测测试数据

注意,在列表 3.2 中,除了通常的训练准确率分数外,我们还报告了 k 折交叉验证受试者工作特征曲线(ROC)下的 曲线下面积 来评估模型。这是必要的,因为 GBMs 特别容易过拟合,报告这些指标有助于我们监控这种风险。另一个原因是,这个练习可以让你复习这些概念。

更具体地说,k 折交叉验证(默认值为 k=5 折)将训练数据集随机分为 k 个分区或折叠,并在 k-1 个分区上训练模型,同时在剩余的第 k 个分区上评估/验证性能,重复这个过程 k 次,每个分区都作为验证集。然后,它使用这些 k 次评估迭代的统计数据报告性能。这个过程允许我们减少模型在数据集的某些部分过拟合和在其他部分表现不佳的风险。

简而言之,过拟合是指将太多参数拟合到太少的数据中。这种情况会损害模型对新数据的泛化能力,并且通常表现为改善训练指标但验证指标没有改善。可以通过收集更多数据、简化模型以减少训练参数的数量以及本书中我们将重点介绍的其他方法来减轻这种情况。

以下代码可用于调用函数并在两个运行示例中对其进行评估:

gbm0 = GradientBoostingClassifier(random_state=10)
start_time = time.time()
test_predictions, test_probs = modelfit(gbm0, train_x, train_y, predictors, test_x)
end_time = time.time()
print("Training the Gradient Boosting Classifier took %3d seconds"%(end_time-start_time))

predicted_labels = test_predictions
acc_score = accuracy_score(test_y, predicted_labels)
print("The Gradient Boosting testing accuracy score is::")
print(acc_score)

对于电子邮件垃圾邮件分类示例,这产生了以下结果:

Model Report
Accuracy : 0.9814
AUC Score (Train): 0.997601
CV Score : Mean - 0.9854882 | Std - 0.006275645 | Min - 0.9770558 | Max - 0.9922158
Training the Gradient Boosting Classifier took 159 seconds
The Gradient Boosting testing accuracy score is::

对于 IMDB 电影评论分类示例,这产生了以下结果:

Model Report
Accuracy : 0.8943
AUC Score (Train): 0.961556
CV Score : Mean - 0.707521 | Std - 0.03483452 | Min - 0.6635249 | Max - 0.7681968
Training the Gradient Boosting Classifier took 596 seconds
The Gradient Boosting testing accuracy score is::
0.665   

乍一看,看着这些结果,人们可能会倾向于认为与我们之前看过的先前方法相比,GBM 数值实验更昂贵——在 Kaggle 上 IMDB 示例完成需要将近 10 分钟。然而,我们必须考虑到当进行 k 折交叉验证练习时,模型会被训练 k=5 次以获得更可靠的性能估计。因此,每次训练大约需要两分钟——这并不像不考虑 k 折交叉验证而推断出的训练时间增加那么剧烈。

我们可以看到一些过拟合的证据——第一个示例的测试准确度低于 k 折训练准确度。此外,在 IMDB 示例中,k 折交叉验证分数明显低于整个数据集的训练分数,强调了在这种模型类型中使用 k 折交叉验证方法跟踪过拟合的重要性。我们在本章的倒数第二节中讨论了一些进一步提高分类器准确性的方法。

那么 ROC 曲线究竟是什么呢?它是假正率(FPR)与真正率(TPR)的曲线图,是用来评估和调整分类器的重要特征。它显示了这些分类器重要特性的权衡,当决策阈值——预测置信度开始被分类为给定类别的成员的概率值——在 0 和 1 之间变化时。现在可以使用以下代码来绘制这条曲线:

test_probs_max = []                                                        ❶
for i in range(test_probs.shape[0]):
    test_probs_max.append(test_probs[i,test_y[i]])

fpr, tpr, thresholds = metrics.roc_curve(test_y, np.array(test_probs_max))  ❷

import matplotlib.pyplot as plt                                            ❸
fig,ax = plt.subplots()
plt.plot(fpr,tpr,label='ROC curve')
plt.plot([0, 1], [0, 1], color='navy', linestyle='--')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic for Email Example')
plt.legend(loc="lower right")
plt.show()

❶ 我们首先需要找到每个示例的最大概率。

❷ 计算 ROC 曲线值

❸ 使用 matplotlib 库生成标记的 ROC 曲线图

邮件分类示例的结果 ROC 曲线如图 3.1 所示。斜率为 1 的直线代表 FPR 与 TPR 之间的权衡对应于随机机会。ROC 曲线越远离此线的左侧,分类器性能越好。因此,ROC 曲线下面积可用作性能的度量。

03_01

图 3.1 邮件分类示例的 ROC 曲线

决策树方法的一个重要特性是它们可以提供特征的重要性得分,这可以用来检测给定数据集中最重要的特征。我们通过在列表 3.2 函数的返回语句之前插入几行代码来做到这一点,如列表 3.3 中所示。

列表 3.3 梯度提升机分类代码和特征重要性分数

from sklearn.ensemble import GradientBoostingClassifier               ❶
from sklearn import metrics                                          ❷
from sklearn.model_selection import cross_val_score

def modelfit(alg, train_x, train_y, predictors, test_x, performCV=True, cv_folds=5):
    alg.fit(train_x, train_y)                                        ❸
    predictions = alg.predict(train_x)                               ❹
    predprob = alg.predict_proba(train_x)[:,1]
    if performCV:                                                     ❺
        cv_score = cross_val_score(alg, train_x, train_y, cv=cv_folds, scoring='roc_auc')

    print("\nModel Report")                                          ❻
    print("Accuracy : %.4g" % metrics.accuracy_score(train_y,predictions))
    print("AUC Score (Train): %f" % metrics.roc_auc_score(train_y, predprob))
    if performCV:
        print("CV Score : Mean - %.7g | Std - %.7g | Min - %.7g | Max - %.7g" % 
(np.mean(cv_score),np.std(cv_score),np.min(cv_score),np.max(cv_score)))

    feat_imp = pd.Series(alg.feature_importances_, predictors).sort_values(ascending=False) 
    feat_imp[:10].plot(kind='bar',title='Feature Importances')        ❼

    return alg.predict(test_x),alg.predict_proba(test_x)              ❽

❶ GBM 算法

❷ 其他 sklearn 函数

❸ 在整体数据上拟合算法

❹ 预测训练集

❺ 执行 k 折交叉验证

❻ 打印模型报告

❼ 添加新的代码来计算特征的重要性

❽ 预测测试数据

对于 IMDB 示例,这产生了图 3.2 的绘图。我们看到像“worst”和“awful”这样的词对分类决策非常重要,这在定性上是有意义的,因为可以想象负面评论家使用这些词。另一方面,“loved”这样的词可能会被积极的评论者使用。

注意:重要性分数在这个示例中似乎效果很好,但不应该一味地相信它们。例如,已经广泛认识到这些重要性分数可能对连续变量以及高基数分类变量有偏见。

现在我们开始应用一些神经网络模型到我们两个运行的示例中,可以说神经网络是当今自然语言处理中最重要的模型类别之一。

3.2 神经网络模型

正如我们在第一章讨论的那样,神经网络是处理感知问题(如计算机视觉和自然语言处理)最重要的机器学习算法类别之一。

在本节中,我们将在本章和上一章中基线化的两个示例问题上训练两个代表性的预训练神经网络语言模型。我们考虑来自语言模型的嵌入 (ELMo) 和来自变压器的双向编码器表示 (BERT)。

03_02

图 3.2 在 IMDB 分类示例中由梯度提升机分类器发现的各种标记的重要性分数

ELMo 包含卷积和循环(特别是长短期记忆 [LSTM])元素,而合适命名的 BERT 是基于变压器的。这些术语在第一章中介绍过,并将在后续章节中更详细地讨论。我们采用了最简单的迁移学习微调形式,在对应的预训练嵌入之上,通过我们前几节的标签数据集训练了一个单独的密集分类层。

3.2.1 语言模型的嵌入 (ELMo)

语言模型嵌入(ELMo)模型以热门的Sesame Street角色命名,是最早证明在一般 NLP 任务中将预训练的语言模型知识转移的有效性的模型之一。该模型被训练以预测一系列单词中的下一个单词,在非监督的大型语料库上可以进行,结果显示得到的权重可以推广到各种其他 NLP 任务。我们将不会在本节详细讨论该模型的架构--我们将在后面的章节中讨论该问题。在这里,我们专注于建立直觉,但足够提到的是,该模型利用了字符级卷积来构建每个单词标记的初步嵌入,接着是双向 LSTM 层,将上下文信息引入模型产生的最终嵌入中。

简要介绍了 ELMo 之后,让我们开始为两个运行示例数据集中的每个数据集训练它。ELMo 模型可以通过 TensorFlow Hub 获得,TensorFlow Hub 提供了一个简单的平台用于共享 TensorFlow 模型。我们将使用使用 TensorFlow 作为后端的 Keras 来构建我们的模型。为了使 TensorFlow Hub 模型可以被 Keras 使用,我们需要定义一个自定义的 Keras 层,以正确的格式实例化它。下面的代码段展示了如何实现这个功能。

代码段 3.4:将 TensorFlow Hub ELMo 实例化为自定义的 Keras 层

import tensorflow as tf                                                 ❶
import tensorflow_hub as hub
from keras import backend as K
import keras.layers as layers
from keras.models import Model, load_model
from keras.engine import Layer
import numpy as np

sess = tf.Session()                                                     ❷
K.set_session(sess)

class ElmoEmbeddingLayer(Layer):                                        ❸
    def __init__(self, **kwargs):
        self.dimensions = 1024
        self.trainable=True
        super(ElmoEmbeddingLayer, self).__init__(**kwargs)

    def build(self, input_shape):
        self.elmo =hub.Module('https:/ /tfhub.dev/google/elmo/2', trainable=self.trainable,
                               name="{}_module".format(self.name))      ❹

        self.trainable_weights += 
                         K.tf.trainable_variables(scope="^{}_module/.*".format(self.name))  ❺
        super(ElmoEmbeddingLayer, self).build(input_shape)

    def call(self, x, mask=None):
        result = self.elmo(K.squeeze(K.cast(x, tf.string), axis=1),
                      as_dict=True,
                      signature='default',
                      )['default']
        return result

    def compute_output_shape(self, input_shape):                        ❻
        return (input_shape[0], self.dimensions)

❶ 导入所需的依赖项

❷ 初始化会话

❸ 创建一个自定义层,允许我们更新权重

❹ 从 TensorFlow Hub 下载预训练的 ELMo 模型

❺ 提取可训练参数--ELMo 模型层加权平均值中的四个权重;更多详细信息请参阅之前的 TensorFlow Hub 链接

❻ 指定输出的形状

在使用这个函数来训练模型之前,我们需要对我们的预处理数据进行一些调整,以适应这个模型结构。特别是,回想一下,我们为传统模型组装了变量raw_data的词袋表示,该变量由第 2.7 节的代码生成,并且这是一个包含每封电子邮件的单词标记列表的 NumPy 数组。在这种情况下,我们将使用第 3.5 节的函数和代码将每个这样的列表合并成一个单一的文本字符串。这是 ELMo TensorFlow Hub 模型期望的输入格式,我们很高兴满足要求。

注意 深度学习实践中,由于人工神经网络极具发现重要性和非重要性的神奇能力,通常不需要去除停用词这一步骤。但在我们的情况下,因为我们试图比较不同模型类型在这个问题上的优点和缺点,对所有算法应用相同的预处理是有意义的,同时也可以说是正确的方法。然而,需要注意的是,ELMo 和 BERT 都是在包含停用词的语料库上进行预训练的。

列表 3.5 将数据转换为 ELMo TensorFlow Hub 模型所需的形式

def convert_data(raw_data,header):                              ❶
    converted_data, labels = [], []
    for i in range(raw_data.shape[0]):
        out = ' '.join(raw_data[i])                             ❷
        converted_data.append(out)
        labels.append(header[i])
    converted_data = np.array(converted_data, dtype=object)[:, np.newaxis]

    return converted_data, np.array(labels)

raw_data, header = unison_shuffle(raw_data, header)             ❸

idx = int(0.7*data_train.shape[0])
train_x, train_y = convert_data(raw_data[:idx],header[:idx])    ❹
test_x, test_y = convert_data(raw_data[idx:],header[idx:])      ❺

❶ 将数据转换为正确的格式

❷ 将每封邮件的标记连接成一个字符串

❸ 首先对原始数据进行洗牌

❹ 将 70%的数据转换为训练数据

❺ 将剩余的 30%的数据转换为测试数据

在将数据转换为正确的格式之后,我们使用下一个列表中的代码构建和训练 Keras ELMo TensorFlow Hub 模型。

列表 3.6 使用列表 3.4 中定义的自定义层构建 ELMo Keras 模型

def build_model(): 
  input_text = layers.Input(shape=(1,), dtype="string")
  embedding = ElmoEmbeddingLayer()(input_text)
  dense = layers.Dense(256, activation='relu')(embedding)    ❶
  pred = layers.Dense(1, activation='sigmoid')(dense)        ❷

  model = Model(inputs=[input_text], outputs=pred)

  model.compile(loss='binary_crossentropy', optimizer='adam',
                                 metrics=['accuracy'])       ❸
  model.summary()                                            ❹

  return model

# Build and fit
model = build_model()
model.fit(train_x,                                           ❺
          train_y,
          validation_data=(test_x, test_y),
          epochs=5,
          batch_size=32)

❶ 输出 256 维特征向量的新层

❷ 分类层

❸ 损失、度量和优化器选择

❹ 显示模型架构以进行检查

❺ 对模型进行五个时期的拟合

在这里需要注意几点,因为这是我们第一次接触深度学习设计的一些详细方面。首先,注意到我们在预训练的 ELMo 嵌入之上添加了一个额外的层,产生了 256 维的特征向量。我们还添加了一个输出维数为 1 的分类层。激活函数sigmoid将其输入转换为 0 到 1 之间的区间,并且在本质上是图 2.6 中的逻辑曲线。我们可以将其输出解释为正类别的概率,并且当它超过某个预先指定的阈值(通常为 0.5)时,我们可以将相应的网络输入分类为正类别。

模型在整个数据集上进行了五个“主要步骤”或时期的拟合。列表 3.6 中的 Keras 代码语句model.summary()打印出模型的详细信息,并产生以下输出:

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_2 (InputLayer)         (None, 1)                 0         
_________________________________________________________________
elmo_embedding_layer_2 (Elmo (None, 1024)              4         
_________________________________________________________________
dense_3 (Dense)              (None, 256)               262400    
_________________________________________________________________
dense_4 (Dense)              (None, 2)                 514       
=================================================================
Total params: 262,918
Trainable params: 262,918
Non-trainable params: 0

我们注意到——不深入进一步的细节,因为这将在第四章中讨论——在这种情况下,大多数可训练参数(大约 26 万个)来自于我们添加在自定义 ELMo 模型之上的层。这是我们第一个使用迁移学习的实例:在 ELMo 的创建者共享的预训练模型之上学习一对新的层。对于大多数神经网络实验而言,使用强大的 GPU 是非常重要的,并且batch_size参数的值(指定每一步向 GPU 输入的数据量)对于收敛速度非常重要。它将根据所使用的 GPU 或缺少 GPU 而有所不同。在实践中,可以增加这个参数的值,直到典型问题实例的收敛速度不再因增加而获益,或者 GPU 的内存在算法的一次迭代中已经不足以容纳单个数据批次为止。此外,在处理多 GPU 的情况下,已经实验证明¹批大小的最佳扩展计划与 GPU 的数量呈线性关系。

通过 Kaggle Kernel(请看我们的配套 GitHub 存储库²获得 Kaggle 笔记本链接)上的一个免费的 NVIDIA Tesla K80 GPU,在我们的电子邮件数据集上,我们在图 3.3 中显示了前五个时代的典型运行性能。我们发现batch_size为 32 在那个环境中能很好地工作。

03_03

图 3.3 显示了在电子邮件分类示例中训练 ELMo 模型的前五个时代的验证和训练准确性比分的趋势。

每个时代需要大约 10 秒来完成——这些信息是由我们的代码打印的。我们看到第四个时代达到了约 97.3%的验证准确性(意味着在不到一分钟的时间内达到了结果)。这个性能与 Logistic Regression 方法的性能相当,后者只稍好一点,为 97.7%(也见表 3.1)。我们注意到这个算法的行为是随机的——它的运行在每次运行时都表现出不同的行为。因此,即使在与我们使用的类似的架构上,你自己的收敛性也会有所不同。实践中通常尝试几次运行算法,并在随机和不同的结果中选择最佳的参数集。最后,我们注意到训练和验证准确性的背离表明了过拟合的开始,正如图中所示的那样。这证实了将通过增加超参数maxtokenlen指定的标记长度以及通过maxtokens指定的每封电子邮件的标记数量来增加信号量的数量可能会进一步提高性能的假设。自然地,通过打开Nsamp来增加每类样本的数量也应该有助于提高性能。

对于 IMDB 示例,ELMo 模型代码产生了图 3.4 所示的收敛输出。

03_04

图 3.4 显示了在 IMDB 电影评论分类示例上训练 ELMo 模型的前五个时代的验证和训练准确性比分的趋势。

每个时代再次需要大约 10 秒,并且在第二个时代不到一分钟的时间内就实现了约 70%左右的验证准确性。我们将看到如何在本章的下一个和最后一个部分提高这些模型的性能。请注意,在第三个以及之后的时代,可以观察到一些过拟合的证据,因为训练准确性继续提高——对数据的拟合改善了,而验证准确性仍然较低。

3.2.2 双向编码器表示来自转换(BERT)

双向编码器表示来自变换器(BERT)模型也是以流行的Sesame Street角色命名的,以向 ELMo 开始的趋势致敬。在撰写本文时,其变体在将预训练语言模型知识转移到下游自然语言处理任务方面取得了一些最佳性能。该模型同样被训练来预测词语序列中的词语,尽管确切的masking过程略有不同,将在本书后面详细讨论。它也可以在非常大的语料库上以无监督的方式进行,并且生成的权重同样适用于各种其他自然语言处理任务。可以说,要熟悉自然语言处理中的迁移学习,熟悉 BERT 也是不可或缺的。

就像我们对 ELMo 所做的那样,在本节中我们将避免完全详细讨论这个深度学习模型的架构——我们将在后面的章节中涵盖这个话题。在这里提一下,模型利用字符级卷积来构建词元的初步嵌入,然后是基于变换器的编码器,其中包含自注意层,为模型提供周围单词的上下文。变换器在功能上取代了 ELMo 所采用的双向 LSTM 的作用。回顾第一章中,变换器相对于 LSTM 在训练可扩展性方面具有一些优势,我们可以看到这个模型背后的一些动机。同样,我们将使用带有 TensorFlow 后端的 Keras 来构建我们的模型。

简要介绍了 BERT 之后,让我们继续为两个运行示例数据集中的每一个训练它。BERT 模型也可以通过 TensorFlow Hub 获得。为了使 hub 模型能够被 Keras 使用,我们同样定义了一个自定义 Keras 层,以正确的格式实例化它,如下一个清单所示。

使用自定义 Keras 层实例化 TensorFlow Hub BERT

import tensorflow as tf
import tensorflow_hub as hub
from bert.tokenization import FullTokenizer
from tensorflow.keras import backend as K

# Initialize session
sess = tf.Session()

class BertLayer(tf.keras.layers.Layer):
    def __init__(
        self,
        n_fine_tune_layers=10,                                             ❶
        pooling="mean",                                                    ❷
        bert_path="https:/ /tfhub.dev/google/bert_uncased_L-12_H-768_A-12/1",❸
        **kwargs,
    ):
        self.n_fine_tune_layers = n_fine_tune_layers
        self.trainable = True
        self.output_size = 768                                             ❹
        self.pooling = pooling
        self.bert_path = bert_path

        super(BertLayer, self).__init__(**kwargs)

    def build(self, input_shape):
        self.bert = hub.Module(
            self.bert_path, trainable=self.trainable, name=f"{self.name}_module"
        )

        trainable_vars = self.bert.variables                               ❺
        if self.pooling == "first":
            trainable_vars = [var for var in trainable_vars if not "/cls/" in var.name]
            trainable_layers = ["pooler/dense"]

        elif self.pooling == "mean":
            trainable_vars = [
                var
                for var in trainable_vars
                if not "/cls/" in var.name and not "/pooler/" in var.name
            ]
            trainable_layers = []
        else:
            raise NameError("Undefined pooling type")

        for i in range(self.n_fine_tune_layers):                           ❻
            trainable_layers.append(f"encoder/layer_{str(11 - i)}")

        trainable_vars = [
            var
            for var in trainable_vars
            if any([l in var.name for l in trainable_layers])
        ]

        for var in trainable_vars:                                         ❼
            self._trainable_weights.append(var)

        for var in self.bert.variables:
            if var not in self._trainable_weights:
                self._non_trainable_weights.append(var)

        super(BertLayer, self).build(input_shape)

    def call(self, inputs):
        inputs = [K.cast(x, dtype="int32") for x in inputs]
        input_ids, input_mask, segment_ids = inputs
        bert_inputs = dict(
            input_ids=input_ids, input_mask=input_mask, segment_ids=segment_ids                                               ❽
        )
        if self.pooling == "first":
            pooled = self.bert(inputs=bert_inputs, signature="tokens", as_dict=True)[
                "pooled_output"
            ]
        elif self.pooling == "mean":
            result = self.bert(inputs=bert_inputs, signature="tokens", as_dict=True)[
                "sequence_output"
            ]

            mul_mask = lambda x, m: x * tf.expand_dims(m, axis=-1)         ❾
            masked_reduce_mean = lambda x, m: tf.reduce_sum(mul_mask(x, m), axis=1) / (
                    tf.reduce_sum(m, axis=1, keepdims=True) + 1e-10)
            input_mask = tf.cast(input_mask, tf.float32)
            pooled = masked_reduce_mean(result, input_mask)
        else:
            raise NameError("Undefined pooling type")

        return pooled

    def compute_output_shape(self, input_shape):
        return (input_shape[0], self.output_size)

❶ 默认要解冻的顶层数量进行训练

❷ 正则化类型的选择

❸ 要使用的预训练模型;这是模型的大型、不区分大小写的原始版本。

❹ BERT 嵌入维度,即生成的输出语义向量的大小

❺ 移除未使用的层

❻ 强制执行要微调的解冻层的数量

❼ 可训练权重

❽ 输入到 BERT 采用非常特定的三元组形式;我们将在下一个清单中展示如何生成它。

❾ BERT“masks”一些词语,然后尝试将它们预测为学习目标。

类似于我们在前一小节为 ELMo 所做的工作,我们对前几节的数据执行一系列类似的后处理步骤,将其放入 BERT 模型所需的格式中。除了我们在列表 3.5 中所做的将词袋标记表示连接成字符串列表之外,我们随后需要将每个连接的字符串转换为三个数组——输入 ID输入掩码段 ID——然后再将它们馈送到 BERT 模型中。这样做的代码在列表 3.8 中显示。将数据转换为正确格式后,我们使用同一列表 3.8 中的剩余代码构建和训练 Keras BERT TensorFlow Hub 模型。

列表 3.8 将数据转换为 BERT 所期望的格式,构建和训练模型

def build_model(max_seq_length):                                           ❶
    in_id = tf.keras.layers.Input(shape=(max_seq_length,), name="input_ids")
    in_mask = tf.keras.layers.Input(shape=(max_seq_length,), name="input_masks")
    in_segment = tf.keras.layers.Input(shape=(max_seq_length,), name="segment_ids")
    bert_inputs = [in_id, in_mask, in_segment]
    bert_output = BertLayer(n_fine_tune_layers=0)(bert_inputs)             ❷
    dense = tf.keras.layers.Dense(256, activation="relu")(bert_output)
    pred = tf.keras.layers.Dense(1, activation="sigmoid")(dense)

    model = tf.keras.models.Model(inputs=bert_inputs, outputs=pred)
    model.compile(loss="binary_crossentropy", optimizer="adam", metrics=["accuracy"])
    model.summary()

    return model

def initialize_vars(sess):                                                 ❸
    sess.run(tf.local_variables_initializer())
    sess.run(tf.global_variables_initializer())
    sess.run(tf.tables_initializer())
    K.set_session(sess)

bert_path = "https:/ /tfhub.dev/google/bert_uncased_L-12_H-768_A-12/1"
tokenizer = create_tokenizer_from_hub_module(bert_path)                    ❹

train_examples = convert_text_to_examples(train_x, train_y)                ❺
test_examples = convert_text_to_examples(test_x, test_y)

# Convert to features
(train_input_ids,train_input_masks,train_segment_ids,train_labels) =       ❻
     convert_examples_to_features(tokenizer, train_examples,               ❻
     max_seq_length=maxtokens)                                             ❻
(test_input_ids,test_input_masks,test_segment_ids,test_labels) = 
     convert_examples_to_features(tokenizer, test_examples,  
     max_seq_length=maxtokens)

model = build_model(maxtokens)                                             ❼

initialize_vars(sess)                                                      ❽

history = model.fit([train_input_ids, train_input_masks, train_segment_ids],❾
train_labels,validation_data=([test_input_ids, test_input_masks, 
test_segment_ids],test_labels), epochs=5, batch_size=32)

❶ 用于构建模型的函数

❷ 我们不重新训练任何 BERT 层,而是将预训练模型用作嵌入,并在其上重新训练一些新层。

❸ Vanilla TensorFlow 初始化调用

❹ 使用 BERT 源代码库中的函数创建兼容的分词器

❺ 使用 BERT 源代码库中的函数将数据转换为“InputExample”格式

❻ 使用 BERT 源代码库中的函数将 InputExample 格式转换为最终的 BERT 输入格式

❼ 构建模型

❽ 实例化变量

❾ 训练模型

类似于我们在前一小节中构建的 ELMo 模型,我们在预训练模型之上放置了一对层,并且仅对这些层进行训练,这大约有 20 万个参数。通过将超参数设置为与之前的所有方法相当的值,我们在电子邮件和电影评论分类问题上分别获得了约为 98.3% 和 71% 的验证准确率(在五个时期内)。

3.3 优化性能

在查看本章前几节以及上一章的各种算法的性能结果时,我们可能会立即得出关于我们研究的每个问题中哪种算法表现最佳的结论。例如,我们可能会得出结论,对于电子邮件分类问题,BERT 和逻辑回归是最佳算法,准确率约为 98%,而 ELMo 的准确率也不远,然后是基于决策树的方法和 SVM 排在最后。另一方面,对于 IMDB 电影评论分类问题,BERT 看起来是赢家,性能约为 71%,其次是 ELMo,然后才是逻辑回归。

但是我们必须记住,我们只有在最初评估算法时才能确定这一点是真实的——Nsamp=1000maxtokens=50maxtokenlen=20——以及任何特定于算法的默认参数值。要有信心地作出一般性的陈述,我们需要更彻底地探索超参数空间,通过在许多超参数设置下评估所有算法的性能,这个过程通常称为超参数调优优化。也许通过这个过程找到的每个算法的最佳性能会改变它们的性能排名,而且一般来说,这将帮助我们为我们感兴趣的问题获得更好的准确性。

3.3.1 手动超参数调整

超参数调优通常最初是通过直觉进行手动操作的。我们在这里描述了这样一种方法,用于超参数Nsampmaxtokensmaxtokenlen,这些超参数在所有算法中都是通用的。

让我们首先假设初始训练数据量——比如Nsamp=1000——就是我们拥有的全部数据。我们假设,如果我们增加每个文档中的数据令牌数量——maxtokens——并增加任何此类令牌的最大长度——maxtokenlen——我们将能够增加用于做出分类决策的信号量,从而提高结果的准确性。

对于电子邮件分类问题,我们首先将这两个值从分别为 50 和 20 的值增加到 100。对于逻辑回归(LR)、支持向量机(SVM)、随机森林(RF)、梯度提升机(GBM)、ELMo 和 BERT 执行此操作的准确性结果显示在表 3.1 的第二行。此外,我们将maxtokens增加到 200,以得到表 3.1 的第三行的结果。

表 3.1 比较了手动调整过程中探索的电子邮件分类示例的不同通用超参数设置下算法的准确性

通用超参数设置 LR SVM RF GBM ELMo BERT
Nsamp = 1000 maxtokens = 50 maxtokenlen = 20 97.7% 70.2% 94.5% 94.2% 97.3% 98.3%
Nsamp = 1000 maxtokens = 100 maxtokenlen = 100 99.2% 72.3% 97.2% 97.3% 98.2% 98.8%
Nsamp = 1000, maxtokens = 200, maxtokenlen = 100 98.7% 90.0% 97.7% 97.2% 99.7% 98.8%

根据这个结果,我们可以看到,虽然 SVM 显然是这个问题中表现最差的分类器,但 logistic 回归、ELMo 和 BERT 几乎可以达到完美的性能。还要注意,ELMo 在更多信号存在时表现最好——这是我们在没有优化步骤的情况下可能会错过的东西。但是,logistic 回归的简单性和速度可能导致它被选为此电子邮件分类问题的生产中首选的分类器。

现在,我们对 IMDB 电影评论分类问题进行了类似的超参数测试步骤。我们首先将maxtokensmaxtokenlen都增加到 100,然后将maxtokens进一步增加到 200。得到的算法性能列在表 3.2 中,同时列出了初始超参数设置时的性能。

表 3.2 IMDB 电影评论分类示例中手动调整过程中探索的不同通用超参数设置的算法准确度比较

通用超参数设置 逻辑回归 支持向量机 随机森林 梯度提升机 ELMo BERT
Nsamp = 1000 maxtokens = 50 maxtokenlen = 20 69.1% 66.0% 63.9% 67.0% 69.7% 71.0%
Nsamp = 1000 maxtokens = 100 maxtokenlen = 100 74.3% 72.5% 70.0% 72.0% 75.2% 79.1%
Nsamp = 1000 maxtokens = 200 maxtokenlen = 100 79.0% 78.3% 67.2% 77.5% 77.7% 81.0%

对于这个问题,BERT 似乎是最佳模型,其次是 ELMo 和逻辑回归。注意,这个问题有更多的改进空间,这与我们早期观察到的这个问题比电子邮件分类问题更难的观察一致。这使我们假设,预训练知识转移对更难的问题有更大的影响,这是直观的。这个概念也符合一般建议,即在有大量标记数据可用时,神经网络模型可能优于其他方法,假设要解决的问题足够复杂,需要额外的数据。

3.3.2 系统化超参数调整

存在一些工具用于对超参数范围进行更系统化和全面的搜索。这些包括 Python 方法GridSearchCV,它对指定的参数网格执行全面搜索,以及HyperOpt,它在参数范围上进行随机搜索。在这里,我们提供了使用GridSearchCV来调整所选算法的代码,作为一个说明性示例。请注意,在这个练习中,我们只调整了一些特定于算法的内部超参数,而将上一小节中我们调整的通用超参数固定,以简化说明。

我们选择使用初始通用超参数设置的 RF 进行电子邮件分类作为我们的说明性示例。之所以做出这个选择,是因为对于这个问题的每次拟合大约需要一秒钟,由于网格搜索将执行大量的拟合,这个例子可以快速执行,以便读者获得最大的学习价值。

我们首先导入所需方法,并检查 RF 超参数可用于调整如下:

from sklearn.model_selection import GridSearchCV       ❶
print("Available hyper-parameters for systematic tuning available with RF:") 
print(clf.get_params())                                ❷

❶ GridSearchCV scikit-learn 导入语句

❷ clf 是列表 2.13 中的 RF 分类器。

这产生了以下输出:

{'bootstrap': True, 'class_weight': None, 'criterion': 'gini', 'max_depth': None, 'max_features': 'auto', 'max_leaf_nodes': None, 'min_impurity_decrease': 0.0, 'min_impurity_split': None, 'min_samples_leaf': 1, 'min_samples_split': 2, 'min_weight_fraction_leaf': 0.0, 'n_estimators': 10, 'n_jobs': 1, 'oob_score': False, 'random_state': 0, 'verbose': 0, 'warm_start': False}

我们选择了其中三个超参数进行搜索,并为每个参数指定了三个值,如下所示:

param_grid = {
    'min_samples_leaf': [1, 2, 3],
    'min_samples_split': [2, 6, 10],
    'n_estimators': [10, 100, 1000]

然后我们使用以下代码进行网格搜索,确保打印出最终的测试准确性和最佳的超参数值:

grid_search = GridSearchCV(estimator = clf, param_grid = param_grid, 
                          cv = 3, n_jobs = -1, verbose = 2)     ❶

grid_search.fit(train_x, train_y)                               ❷

print("Best parameters found:")                                 ❸
print(grid_search.best_params_)

print("Estimated accuracy is:")
acc_score = accuracy_score(test_y, grid_search.best_estimator_.predict(test_x))
print(acc_score)

❶ 使用指定的超参数网格定义网格搜索对象

❷ 将网格搜索适配到数据

❸ 显示结果

这个实验需要在 333=27 个点上训练分类器,因为每个超参数网格上有三个请求的点。整个实验不到五分钟就完成了,并且准确率达到了 95.7%。这比原始得分 94.5%提高了超过 1%。代码的原始输出如下,指定了最佳的超参数值:

Best parameters found:
{'min_samples_leaf': 2, 'min_samples_split': 10, 'n_estimators': 1000}
Estimated accuracy is:

的确,当我们在所有分类器上进行全面调整时,我们发现可以将每个分类器的性能提升 1-2%,而不会影响在前一小节中达到的每个问题的最佳分类器的结论。

摘要

  • 通常会尝试多种算法来解决感兴趣的任何问题,以找到模型复杂性和性能的最佳组合,以适应您的情况。

  • 基线通常从最简单的算法开始,例如逻辑回归,然后逐渐变得更复杂,直到达到正确的性能/复杂性折衷。

  • 重要的模型设计选择包括用于评估性能的指标,用于指导训练算法的损失函数以及最佳验证实践,等等,这些可以根据模型和问题类型而异。

  • 超参数调整是模型开发流程的重要步骤,因为初始超参数设置可能严重误代表通过调整可以找到的最佳性能。

  • 简单模型在可用数据量不大和/或问题较简单时往往效果最佳,而复杂的神经网络模型在有更多数据可用时往往表现更好,因此值得额外复杂性,当更多数据可用时。

  1. P. Goyal 等人,“准确的、大型小批次 SGD:在 1 小时内训练 ImageNet”,arXhiv(2018 年)。

  2. github.com/azunre/transfer-learning-for-nlp

第二部分:浅层迁移学习和使用递归神经网络(RNNs)的深度迁移学习

第 4、5 和 6 章深入研究了基于浅层神经网络的一些重要迁移学习自然语言处理方法,也就是相对层数较少的神经网络。它们还开始探索深度迁移学习,通过使用递归神经网络(RNNs)作为关键功能的代表性技术,比如 ELMo。

第四章:自然语言处理的浅层迁移学习

本章包括

  • 以半监督的方式使用预训练的词嵌入将预训练知识转移到问题中

  • 以半监督的方式使用预训练的较大文本部分的嵌入来将预训练知识转移到问题中

  • 使用多任务学习来开发性能更好的模型

  • 修改目标域数据以重用来自资源丰富的源域的知识

在本章中,我们将涵盖一些重要的浅层迁移学习方法和概念。这使我们能够探索迁移学习中的一些主要主题,同时在感兴趣的最终类别——浅层神经网络类别的背景下进行。几位作者已经提出了将迁移学习方法分类到不同组别中的各种分类系统。¹,²,³ 大致来说,分类是基于迁移是否发生在不同的语言、任务或数据域之间。每种类型的分类通常相应地被称为 跨语言学习多任务学习领域自适应,如图 4.1 所示。

04_01

图 4.1 将迁移学习划分为多任务学习、领域自适应和跨语言学习的可视化分类

我们将在这里看到的方法涉及到某种程度上是神经网络的组件,但不像第三章中讨论的那样,这些神经网络没有很多层。这就是为什么标签“浅层”适合描述这些方法集合的原因。与上一章一样,我们将这些方法放在特定的实际例子的背景下,以促进您的实际自然语言处理技能的提升。跨语言学习将在本书的后续章节中讨论,因为现代神经机器翻译方法通常是深层的。我们将在本章中简要探讨另外两种迁移学习。

我们首先探讨了一种常见的半监督学习形式,它使用了预训练的词嵌入,如 word2vec,将其应用于本书前两章中的一个示例。请回忆第一章,这些方法与第三章中的方法不同,因为它们产生每个单词一个向量,而不考虑上下文。

我们重新访问了 IMDB 电影评论情感分类。回想一下,此示例涉及将 IMDB 的电影评论根据表达的情感分为积极或消极。这是一个典型的情感分析示例,在文献中被广泛使用来研究许多算法。我们将由预训练的单词嵌入生成的特征向量与一些传统的机器学习分类方法相结合,即随机森林和逻辑回归。然后,我们演示了使用更高级别的嵌入,即将更大的文本部分——句子、段落和文档级别——向量化,可以提高性能。将文本向量化,然后将传统的机器学习分类方法应用于生成的向量的一般思想在图 4.2 中可视化。

04_02

图 4.2 使用单词、句子或文档嵌入进行半监督学习的典型步骤序列

随后,我们将涵盖多任务学习,并学习如何同时训练单个系统来执行多个任务——在我们的案例中,分别由上一章的两个示例代表,即电子邮件垃圾分类和 IMDB 电影评论情感分析。你可以从多任务学习中获得几个潜在的好处。通过为多个任务训练单个机器学习模型,可以在更大更多样的来自合并数据池的数据上学习共享表示,这可能导致性能提升。此外,广泛观察到,这种共享表示具有更好的泛化能力,可以推广到未经训练的任务,而且可以在不增加模型大小的情况下实现此改进。我们在我们的示例中探索了其中一些好处。具体地,我们专注于浅层神经多任务学习,其中为设置中的每个特定任务训练了一个额外的密集层以及分类层。不同的任务还共享它们之间的一层,这种设置通常被称为硬参数共享

最后,我们引入了一个流行的数据集作为本章的另一个运行示例。这就是多领域情感数据集,描述了Amazon.com的一组不同产品的产品评论。我们使用此数据集来探索领域自适应。假设我们有一个领域,它可以被定义为特定任务的特定数据分布,并且已经训练好在该领域中的数据上表现良好的分类器。领域自适应的目标是修改或适应不同目标领域的数据,以使源领域的预训练知识可以帮助在目标领域中学习。我们应用了一种简单的自动编码方法来将目标领域中的样本“投影”到源领域特征空间中。

自编码器是一个系统,它通过将输入编码成一个有效的潜在表示,然后学习有效解码该表示,从而学习以非常高的准确度重构输入。它们传统上在模型减少应用中被广泛使用,因为潜在表示通常比编码发生的原始空间的维度要小,所选维度值也可以为计算效率和准确度的正确平衡而选择。⁴ 在极端情况下,在目标域中使用无标签数据进行训练可以获得改进,这通常称为 零样本域适应,其中学习发生在目标域中没有标记的数据。在我们的实验中,我们演示了一个例子。

4.1 带有预训练单词嵌入的半监督学习

单词嵌入的概念是自然语言处理领域的核心。它是给需要分析的每个单词产生一组实数向量的技术集合的名称。在单词嵌入设计中一个重要的考虑因素是生成向量的维度。更高维度的向量通常可以更好地代表语言中的单词,在许多任务上表现更好,但计算成本也自然更高。选择最优维度需要在这些竞争因素之间取得平衡,通常是经验性的,尽管一些最近的方法提出了更彻底的理论优化方法。⁵

如本书第一章所述,这个重要的 NLP 研究子领域有着丰富的历史,起源于 60 年代的术语向量模型的信息检索。这一领域的顶峰是在 2010 年代中期,出现了预训练的浅层神经网络技术,例如 fastText、GloVe 和 word2vec,它们有多个变体,包括连续词袋(CBOW)和 Skip-Gram。CBOW 和 Skip-Gram 都是从受过不同目标训练的浅层神经网络中提取的。Skip-Gram 尝试预测滑动窗口中任何目标单词周围的单词,而 CBOW 尝试预测给定邻居的目标单词。GloVe,即全局向量,尝试扩展 word2vec 通过将全局信息纳入嵌入中。它通过优化嵌入,使得单词之间的余弦积反映它们共现的次数,其目标是使得结果向量更加可解释。技术 fastText 尝试通过在字符 n-gram(而不是单词 n-gram)上重复 Skip-Gram 方法,从而能够处理以前看不见的单词。每个预训练嵌入的变体都有其优点和缺点,并在表 4.1 中总结。

表 4.1 比较各种流行单词嵌入方法的优缺点

词嵌入方法 优势 劣势
Skip-Gram word2vec 适用于小型训练数据集和罕见词 训练速度慢,且对常见词准确性较低
CBOW word2vec 训练速度几倍快于,并对常见词提供更好的准确性 在处理少量训练数据和罕见词方面效果不佳
GloVe 向量比其他方法更容易解释 训练期间需要更高的内存存储词语共现情况
fastText 能够处理词汇外的词 计算成本更高;模型更大更复杂

需要强调的是,fastText 以处理词汇外的词而闻名,这源自它的设计初衷即嵌入子词字符 n-gram 或子词(与 word2vec 的整个词相对应)。这使得它能够通过聚合组成的字符 n-gram 嵌入来为以前未见过的词构建嵌入。这一优点是以更大的预先训练嵌入和更高的计算资源需求和成本为代价的。因此,在本节中,我们将使用 fastText 软件框架以 word2vec 输入格式加载嵌入,而没有子词信息。这可以降低计算成本,使读者更容易进行练习,同时展示如何处理词汇外问题,并提供一个坚实的体验平台,让读者可以进入子词嵌入的领域。

让我们开始计算实验!我们需要做的第一件事是获得适当的预训练词嵌入文件。因为我们将使用 fastText 框架,我们可以从作者的官方网站⁶获取这些预训练文件,该网站提供多种格式的嵌入文件。请注意,这些文件非常庞大,因为它们试图捕获语言中所有可能单词的向量化信息。例如,针对英语语言的.wec 格式嵌入,是在维基百科 2017 年数据集上训练的,提供了在不处理子词和词汇外词的情况下的向量化信息,大约为 6GB。相应的.bin 格式嵌入,包含了着名的 fastText 子词信息,能够处理词汇外词,大约大 25%,约为 7.5GB。我们还注意到,维基百科嵌入提供了高达 294 种语言,甚至包括传统上未解决的非洲语言,例如特威语、埃维语和豪萨语。但已经表明,对于许多包括低资源语言,这些嵌入的质量并不是很好。⁷

由于这些嵌入的大小,建议使用我们在 Kaggle 上托管的推荐云笔记本来执行此示例(而不是在本地运行),因为其他用户已经将嵌入文件在云环境中公开托管。因此,我们可以简单地将它们附加到正在运行的笔记本上,而无需获取并在本地运行文件。

一旦嵌入可用,我们可以使用以下代码段加载它,确保计时加载函数调用:

import time
from gensim.models import FastText, KeyedVectors

start=time.time()
FastText_embedding = KeyedVectors.load_word2vec_format("../input/jigsaw/wiki.en.vec")   ❶
end = time.time()
print("Loading the embedding took %d seconds"%(end-start))

❶ 加载以“word2vec”格式(不含子词信息)预训练的 fastText 嵌入。

在我们用于执行的 Kaggle 环境中,第一次加载嵌入需要超过 10 分钟。实际上,在这种情况下,通常将嵌入加载到内存中一次,然后使用诸如 Flask 之类的方法提供对它的访问,只要需要。这也可以通过本书本章附带的 Jupyter 笔记本来实现。

获得并加载了预训练的嵌入后,让我们回顾一下 IMDB 电影评论分类示例,在本节中我们将对其进行分析。特别是,在管道的预处理阶段,我们直接从 2.10 清单开始,生成了一个包含电影评论的单词级标记表示的 NumPy 数组raw_data,其中删除了停用词和标点符号。为了读者的方便,我们接下来再次展示 2.10 清单。

2.10 清单(从第二章复制)将 IMDB 数据加载到 NumPy 数组中。

def load_data(path):
    data, sentiments = [], []
    for folder, sentiment in (('neg', 0), ('pos', 1)):
        folder = os.path.join(path, folder)
        for name in os.listdir(folder):                  ❶
            with open(os.path.join(folder, name), 'r') as reader:
                  text = reader.read()
            text = tokenize(text)                        ❷
            text = stop_word_removal(text)
            text = reg_expressions(text)
            data.append(text)
            sentiments.append(sentiment)                ❸
    data_np = np.array(data)                            ❹
    data, sentiments = unison_shuffle_data(data_np, sentiments)

    return data, sentiments

train_path = os.path.join('aclImdb', 'train')           ❺
raw_data, raw_header = load_data(train_path)

❶ 遍历当前文件夹中的每个文件。

❷ 应用标记化和停用词分析例程。

❸ 跟踪相应的情感标签。

❹ 转换为 NumPy 数组。

❺ 在数据上调用上述函数。

如果您已经完成了第二章,您可能还记得在 2.10 清单之后,我们继续为输出 NumPy 数组生成了一个简单的词袋表示,该表示只是计算了每个评论中可能单词标记的出现频率。然后,我们使用生成的向量作为进一步机器学习任务的数值特征。在这里,我们不使用词袋表示,而是从预训练的嵌入中提取相应的向量。

因为我们选择的嵌入框架不能直接处理词汇表外的单词,所以我们要做的下一步是开发一种解决这种情况的方法。最简单的方法自然是简单地跳过任何这样的单词。因为当遇到这样的单词时,fastText 框架会报错,我们将使用一个try and except块来捕获这些错误而不中断执行。假设您有一个预训练的输入嵌入,用作字典,其中单词作为键,相应的向量作为值,并且有一个单词列表在评论中。接下来的清单显示了一个函数,该函数生成一个二维 NumPy 数组,其中每行代表评论中每个单词的嵌入向量。

列表 4.1 生成电影评论单词嵌入向量的 2-D Numpy 数组

def handle_out_of_vocab(embedding,in_txt):
    out = None
    for word in in_txt:                                  ❶
        try:
            tmp = embedding[word]                        ❷
            tmp = tmp.reshape(1,len(tmp))

            if out is None:                              ❸
                out = tmp
            else:
                out = np.concatenate((out,tmp),axis=0)   ❹
        except:                                          ❺
            pass

    return out

❶ 循环遍历每个单词

❷ 提取相应的嵌入向量,并强制“行形状”

❸ 处理第一个向量和一个空数组的边缘情况

❹ 将行嵌入向量连接到输出 NumPy 数组

❺ 在发生词汇表外错误时跳过当前单词的执行,并从下一个单词继续执行

此列表中的函数现在可以用来分析由变量raw_data捕获的整个数据集。但在此之前,我们必须决定如何将评论中单词的嵌入向量组合或聚合成代表整个评论的单个向量。实践中发现,简单地对单词进行平均通常可以作为一个强有力的基准。由于嵌入是以一种确保相似单词在生成的向量空间中彼此更接近的方式进行训练的,因此它们的平均值代表了该集合的平均含义在直觉上是有意义的。摘要/聚合的平均基准经常被推荐作为从单词嵌入中嵌入更大文本部分的第一次尝试。这也是我们在本节中使用的方法,正如列表 4.2 中的代码所示。实际上,该代码在语料库中的每个评论上重复调用列表 4.1 中的函数,对输出进行平均,并将结果向量连接成一个单一的二维 NumPy 数组。该结果数组的行对应于每个评论的通过平均聚合的嵌入向量。

列表 4.2 将 IMDB 数据加载到 NumPy 数组中

def assemble_embedding_vectors(data):
    out = None
    for item in data:                                         ❶
        tmp = handle_out_of_vocab(FastText_embedding,item)    ❷
        if tmp is not None:
            dim = tmp.shape[1]
            if out is not None:
                vec = np.mean(tmp,axis=0)                     ❸
                vec = vec.reshape((1,dim))
                out = np.concatenate((out,vec),axis=0)        ❹
            else:
                out = np.mean(tmp,axis=0).reshape((1,dim))                                            
        else:
            pass                                              ❺

    return out

❶ 循环遍历每个 IMDB 评论

❷ 提取评论中每个单词的嵌入向量,确保处理词汇表外的单词

❸ 对每个评论中的单词向量进行平均

❹ 将平均行向量连接到输出 NumPy 数组

❺ 词汇表外边缘情况处理

现在我们可以使用下一个函数调用为整个数据集组装嵌入向量:

EmbeddingVectors = assemble_embedding_vectors(data_train)

现在,这些可以作为特征向量用于相同的逻辑回归和随机森林代码,就像列表 2.11 和 3.1 中分别使用的那样。 使用这些代码来训练和评估这些模型时,当超参数maxtokensmaxtokenlen分别设置为 200 和 100 时,我们发现对应的准确率分别为 77%和 66%,而Nsamp—每个类的样本数—等于 1,000。 这些只比在前几章最初开发的基于词袋的基线稍低一些(分别对应准确率为 79%和 67%)。 我们假设这种轻微的降低可能是由于聚合个别单词向量的天真平均方法造成的。 在下一节中,我们尝试使用专门设计用于在更高文本级别嵌入的嵌入方法来执行更智能的聚合。

4.2 使用更高级别的表示进行半监督学习

受 word2vec 启发,有几种技术试图以这样一种方式将文本的较大部分嵌入向量空间,以使具有相似含义的句子在诱导的向量空间中更接近彼此。 这使我们能够对句子执行算术运算,以推断类比、合并含义等等。 一个著名的方法是段落向量,或者doc2vec,它利用了从预训练词嵌入中汇总单词时的连接(而不是平均)来总结它们。 另一个是 sent2vec,它通过优化单词和单词 n-gram 嵌入以获得准确的平均表示,将 word2vec 的经典连续词袋(CBOW)—在滑动窗口中训练浅层网络以预测上下文中的单词—扩展到句子。 在本节中,我们使用一个预训练的 sent2vec 模型作为一个说明性的代表方法,并将其应用于 IMDB 电影分类示例。

您可以在网上找到几个 sent2vec 的开源实现。 我们正在使用一个基于 fastText 构建的使用频繁的实现。 要直接从托管的 URL 安装该实现,请执行以下命令:

pip install git+https:/ /github.com/epfml/sent2vec

很自然地,就像在预训练词嵌入的情况下一样,下一步是获取预训练的 sent2vec 句子嵌入,以供我们已安装的特定实现/框架加载。 这些由框架的作者在他们的 GitHub 页面上托管,并由其他用户在 Kaggle 上托管。 为简单起见,我们选择了最小的 600 维嵌入wiki_unigrams.bin,大约 5 GB 大小,仅捕获了维基百科上的单字信息。 请注意,预训练模型的大小明显更大,在书籍语料库和 Twitter 上预训练,还包括双字信息。

在获得预训练嵌入后,我们使用以下代码片段加载它,确保像以前一样计时加载过程。

import time
import sent2vec

model = sent2vec.Sent2vecModel()
start=time.time()
model.load_model('../input/sent2vec/wiki_unigrams.bin')     ❶
end = time.time()
print("Loading the sent2vec embedding took %d seconds"%(end-start))

❶ 加载 sent2vec 嵌入

值得一提的是,我们发现首次执行时的加载时间少于 10 秒——相比于 fastText 单词嵌入的加载时间超过 10 分钟,这是一个显着的改进。这种增加的速度归因于当前包的实现比我们在上一节中使用的 gensim 实现要高效得多。在实践中,尝试不同的包以找到最有效的包对于你的应用程序并不罕见。

接下来,我们定义一个函数来生成一系列评论的向量。它本质上是列表 4.2 中呈现的预训练单词嵌入函数的简化形式。它更简单,因为我们不需要担心词汇表外的单词。该函数如下列表所示。

列表 4.3 将 IMDB 数据加载到 NumPy 数组中

def assemble_embedding_vectors(data):
    out = None
    for item in data:                                   ❶
        vec = model.embed_sentence(" ".join(item))      ❷
        if vec is not None:                             ❸
            if out is not None:
                out = np.concatenate((out,vec),axis=0)
            else:
                out = vec
        else:
            pass

    return out

❶ 遍历每个 IMDB 评论

❷ 提取每个评论的嵌入向量

❸ 处理边缘情况

现在,我们可以使用此函数提取每个评论的 sent2vec 嵌入向量,如下所示:

EmbeddingVectors = assemble_embedding_vectors(data_train)

我们也可以像以前一样将此分为训练集和测试集,并在嵌入向量的基础上训练 logistic 回归和随机森林分类器,使用类似于列表 2.11 和 3.1 中所示的代码。在 logistic 回归和随机森林分类器的情况下,准确率分别为 82% 和 68%(与上一节中相同的超参数值)。与上一节中基于词袋的基线的对应值为 79% 和 67% 相比,对于 logistic 回归分类器与 sent2vec 结合起来的这个值是一个改进,同时也是对于上一节中的平均词嵌入方法的改进。

4.3 多任务学习

传统上,机器学习算法一次只能训练执行一个任务,所收集和训练的数据对于每个单独的任务是独立的。这在某种程度上与人类和其他动物学习的方式相矛盾,人类和其他动物的学习方式是同时进行多个任务的训练,从而一个任务的训练信息可能会影响和加速其他任务的学习。这些额外的信息不仅可能提高当前正在训练的任务的性能,还可能提高未来任务的性能,有时甚至在没有关于这些未来任务的标记数据的情况下也可能如此。在目标域中没有标记数据的迁移学习场景通常被称为零样本迁移学习。

在机器学习中,多任务学习在许多场景中历史上出现过,从多目标优化l2和其他形式的正则化(本身可以被构造为一种多目标优化形式)。图 4.3 展示了我们将要使用的神经多任务学习的形式,其中一些层/参数在所有任务之间共享,即硬参数共享。¹¹

04_03

图 4.3 我们将使用的神经多任务学习的一般形式——硬参数共享(在本例中有三个任务)

在另一种突出的神经多任务学习类型中,软参数共享,所有任务都有自己的层/参数,不进行共享。相反,通过对各个任务的特定层施加的各种约束,鼓励它们相似。我们不再进一步讨论这种类型的多任务学习,但了解它的存在对于您自己未来的潜在文献调研是很有好处的。

让我们继续进行本节的实例说明,通过在下一个小节中设置和基线化它。

4.3.1 问题设置和浅层神经单任务基线

再次考虑图 4.3,但只有两个任务——第一个任务是前两节中的 IMDB 电影评论分类,第二个任务是前一章中的电子邮件垃圾邮件分类。所得到的设置代表了我们将在本节中解决的具体示例。为了促进概念化,这个设置在图 4.4 中显示。

04_04

图 4.4 我们将使用的神经多任务硬参数共享的具体形式,显示了两个特定任务——IMDB 评论和电子邮件垃圾邮件分类

在继续之前,我们必须决定如何将输入转换为用于分析的数字。一种流行的选择是使用字符级别的独热编码对输入进行编码,其中每个字符被维度等于可能字符总数的稀疏向量替换。这个向量在与字符对应的列中包含 1,其他位置为 0。图 4.5 显示了这种方法的插图,旨在帮助您简洁地可视化独热编码过程。

04_05

图 4.5 将字符进行独热编码以行向量表示的过程的可视化。该过程将词汇表中的每个字符替换为与词汇表大小相等的稀疏向量。1 被放置在与词汇表字符索引相对应的列中。

从内存角度来看,这种方法可能会很昂贵,因为维度显著增加,并且因此,通过专门的神经网络层“即时”执行一键编码是常见的。在这里,我们采用了更简单的方法:我们将每个评论通过 sent2vec 嵌入函数,并将嵌入向量作为输入特征传递给图 4.4 所示的设置。

在继续进行图 4.4 所示的准确的双任务设置之前,我们进行了另一个基准测试。我们将仅使用 IMDB 电影分类任务,以查看任务特定的浅层神经分类器与上一节中的模型相比如何。与这个浅层神经基线相关的代码将显示在下一个列表中。

清单 4.4 浅层单任务 Keras 神经网络

from keras.models import Model
from keras.layers import Input, Dense, Dropout

input_shape = (len(train_x[0]),)
sent2vec_vectors = Input(shape=input_shape)                  ❶
dense = Dense(512, activation='relu')(sent2vec_vectors)      ❷
dense = Dropout(0.3)(dense)                                  ❸
output = Dense(1, activation='sigmoid')(dense)               ❹
model = Model(inputs=sent2vec_vectors, outputs=output)
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) 
history = model.fit(train_x, train_y, validation_data=(test_x, test_y), batch_size=32, nb_epoch=10, shuffle=True)

❶ 输入必须匹配 sent2vec 向量的维度。

❷ 在 sent2vec 向量之上训练的密集神经层

❸ 应用 dropout 减少过拟合

❹ 输出指示一个单一的二元分类器——评论是“积极”还是“消极”?

我们发现,在上一节中指定的超参数值下,该分类器的性能约为 82%。这高于基于词袋结合逻辑回归的基线,大约等于上一节中的 sent2vec 结合逻辑回归。

4.3.2 双任务实验

现在我们介绍另一个任务:上一章的电子邮件垃圾分类问题。我们不会在这里重复预处理步骤和相关代码,这是一个辅助任务;有关这些详细信息,请参阅第二章。假设数据样本中的邮件对应的 sent2vec 向量 train_x2 可用,清单 4.5 显示了如何创建一个多输出的浅层神经模型,同时对其进行训练,用于电子邮件垃圾分类和 IMDB 电影评论的分类,通过硬参数共享。

清单 4.5 浅层双任务硬参数共享 Keras 神经网络

from keras.models import Model
from keras.layers import Input, Dense, Dropout
from keras.layers.merge import concatenate

input1_shape = (len(train_x[0]),)
input2_shape = (len(train_x2[0]),)
sent2vec_vectors1 = Input(shape=input1_shape)
sent2vec_vectors2 = Input(shape=input2_shape)
combined = concatenate([sent2vec_vectors1,sent2vec_vectors2])             ❶
dense1 = Dense(512, activation='relu')(combined)                          ❷
dense1 = Dropout(0.3)(dense1)
output1 = Dense(1, activation='sigmoid',name='classification1')(dense1)   ❸
output2 = Dense(1, activation='sigmoid',name='classification2')(dense1)
model = Model(inputs=[sent2vec_vectors1,sent2vec_vectors2], outputs=[output1,output2])

❶ 将不同任务的 sent2vec 向量连接起来

❷ 共享的密集神经层

❸ 两个任务特定的输出,每个都是二元分类器

已经为涉及 IMDB 电影评论和电子邮件垃圾分类的两任务多任务场景定义了硬参数共享设置,我们可以通过以下方式编译和训练生成的模型:

model.compile(loss={'classification1': 'binary_crossentropy',              ❶
                    'classification2': 'binary_crossentropy'},
              optimizer='adam', metrics=['accuracy'])
history = model.fit([train_x,train_x2],[train_y,train_y2],
                    validation_data=([test_x,test_x2],[test_y,test_y2]),   ❷
        batch_size=8,nb_epoch=10, shuffle=True)

❶ 指定两个损失函数(在我们的情况下都是 binary_crossentropy)

❷ 指定每个输入的训练和验证数据

对于这个实验,我们将超参数 maxtokensmaxtokenlen 都设置为 100,Nsamp(每个类别的样本数)的值设置为 1,000(与上一节相同)。

我们发现,在训练多任务系统时,IMDB 分类性能略有下降,从清单 4.4 中单任务浅层设置的约 82% 下降到约 80%。电子邮件分类的准确性同样从 98.7% 下降到 98.2%。鉴于性能下降,人们可能会合理地问:这一切的意义何在?

首先要注意的是,训练好的模型可以独立地用于每个任务,只需将省略的任务输入替换为零以尊重预期的整体输入维度,并忽略相应的输出。此外,我们期望多任务设置中的共享预训练层 dense1 比清单 4.4 中的更容易泛化到任意新任务。这是因为它已经在更多种类和更一般的数据和任务上进行了训练以进行预测。

为了更具体地说明这一点,考虑将任务特定层中的一个或两个替换为新的层,将共享层dense1初始化为前一个实验的预训练权重,并在新的任务数据集上对结果模型进行微调。通过观察更广泛范围的任务数据,可能与新添加的任务类似,这些共享权重更有可能包含有用信息,可用于考虑的下游任务。

我们将在本书的后面回到多任务学习的概念,这将为我们提供进一步研究和思考这些现象的机会。本节的实验希望为您提供了进一步探索所需的基础。

4.4 域自适应

在本节中,我们简要探讨了域自适应的概念,这是转移学习中最古老和最显着的想法之一。机器学习实践者经常做出的一个隐含假设是,推断阶段的数据将来自用于训练的相同分布。当然,实践中很少有这种情况发生。

进入域自适应以尝试解决这个问题。让我们将域定义为针对特定任务的数据的特定分布。假设我们有一个域和一个经过训练以在该域中表现良好的算法。域自适应的目标是修改或调整不同目标域中的数据,以便来自源域的预训练知识可以适用于更快的学习和/或直接推断目标域中的情况。已经探索了各种方法,从多任务学习(如前一节介绍的)—在不同数据分布上同时进行学习—到协同变换—这些方法能够在单个组合特征空间上进行更有效的学习—再到利用源域和目标域之间相似性度量的方法,帮助我们选择哪些数据应该用于训练。

我们采用简单的自编码方法将目标域中的样本“投射”到源域特征空间中。自编码器是一种可以学习以高准确度重构输入的系统,通常是通过将它们编码成高效的潜在表示来学习解码所述表示。描述重构输入过程的技术方法是“学习身份函数”。自编码器在模型维度缩减应用中传统上被大量使用,因为潜在表示通常比编码发生的原始空间的维度小,并且所述维度值也可以被选择为在计算效率和准确性之间达到正确平衡。在极端有利的情况下,您可以在目标域中不需要标记数据的情况下获得改进,这通常被称为零样本域自适应

零样本迁移学习的概念在许多情境中都出现过。您可以将其视为一种转移学习的“圣杯”,因为在目标域中获取标记数据可能是一项昂贵的任务。在这里,我们探讨了一个分类器是否可以用来预测 IMDB 电影评论的极性,以预测来自完全不同数据源的书评或 DVD 评论的极性,例如,是否可以使用在 IMDB 评论数据上训练的分类器来预测书评或 DVD 评论的极性?

在当今世界,一个自然的备选评论来源是亚马逊。鉴于这家电子商务网站在产品类别和数据量方面的多样性,以及它被许多美国人视为基本日常需求购买的主要来源,相比于传统的实体店而言,它具有更多的商业额。这里有一个丰富的评论库。事实上,自然语言处理领域中最显著和深度探索的数据集之一就是亚马逊不同产品类别的评论集合——多领域情感数据集。这个数据集包含 25 个类别,我们从中选择了图书评论的产品类别,认为它与 IMDB 评论足够不同,可以提供一个具有挑战性的测试案例。

该数据集中的数据以标记语言格式存储,其中标签用于定义各种元素,并且按类别和极性组织到单独的文件中。对于我们的目的来说,值得注意的是评论包含在适当命名的<review_text>...</review_text>标签内。在获得这些信息后,下一个清单中的代码可以用于加载积极和消极的图书评论,并为其准备分析。

在加载来自多领域情感数据集的评论时,清单 4.6

def parse_MDSD(data):
    out_lst = []
    for i in range(len(data)):
    txt = ""
    if(data[i]=="<review_text>\n"):              ❶
                    j=i
            while(data[j]!="</review_text>\n"):
                txt = txt+data[j]
                j = j+1
            text = tokenize(txt)
            text = stop_word_removal(text)
            text = remove_reg_expressions(text)
            out_lst.append(text)

    return out_lst

input_file_path = \
"../input/multi-domain-sentiment-dataset-books-and-dvds/books.negative.review"
with open (input_file_path, "r", encoding="latin1") as myfile:   
    data=myfile.readlines()
neg_books = parse_MDSD(data)                     ❷

input_file_path = \
"../input/multi-domain-sentiment-dataset-books-and-dvds/books.positive.review"
with open (input_file_path, "r", encoding="latin1") as myfile:
    data=myfile.readlines()
pos_books = parse_MDSD(data)

header = [0]*len(neg_books)                      ❸
header.extend([1]*len(pos_books))
neg_books.extend(pos_books)                      ❹
MDSD_data = np.array(neg_books)
data, sentiments = unison_shuffle_data(np.array(MDSD_data), header)
EmbeddingVectors = assemble_embedding_vectors(data)

❶ 定位评论的第一行,并将所有后续字符组合到结束标记中,形成评论文本

❷ 通过利用定义的函数,从源文本文件中读取正面和负面评论。

❸ 为正面和负面类别创建标签。

❹ 追加、洗牌并提取相应的 sent2vec 向量。

在加载了书评文本并准备进行进一步处理之后,我们现在直接在目标数据上测试之前部分训练的 IMDB 分类器,看看它在没有任何处理的情况下的准确性,使用以下代码:

print(model.evaluate(x=EmbeddingVectors,y=sentiments))

这产生了约 74%的准确性。虽然这与 IMDB 数据上相同分类器的 82%的性能相比有所减少,但仍足够高来证明从电影评论任务到书评任务的零-shot 知识转移的一个实例。让我们尝试通过自编码器进行零-shot 域适应来提高这个数字。

请注意,零-shot 域转移越是“相似”的源和目标域,成功的可能性就越大。相似性可以通过应用于两个域的 sent2vec 向量的余弦相似度等技术来衡量。建议的课后练习是探索 MDSD 余弦相似度在一些领域之间的应用,以及在此处描述的零-shot 转移实验之间的有效性。scikit-learn 库有一个简单的方法来计算余弦相似度。

我们训练一个自编码器来重构 IMDB 数据。自编码器采用了一个类似于我们在上一部分中使用的多任务层的浅层神经网络。Keras Python 代码在 4.7 节中显示。与以前的神经网络的一个主要区别是,因为这是一个回归问题,输出层没有激活。编码维度encoding_dim是通过经验调整以获得正确的准确性和计算成本的平衡。

4.7 Keras 浅层神经自编码器列表

encoding_dim = 30

input_shape = (len(train_x[0]),)                                  ❶
sent2vec_vectors = Input(shape=input_shape)
encoder = Dense(encoding_dim, activation='relu')(sent2vec_vectors)
dropout = Dropout(0.1)(encoder)                                   ❷
decoder = Dense(encoding_dim, activation='relu')(dropout)
dropout = Dropout(0.1)(decoder)
output = Dense(len(train_x[0]))(dropout)                          ❸
autoencoder = Model(inputs=sent2vec_vectors, outputs=output)

❶ 输入大小必须与 sent2vec 向量的维度相同。

❷ 编码到指定的潜在维度空间,编码维度为 encoding_dim。

❸ 从指定的潜在维度空间解码回到 sent2vec 空间。

我们训练自编码器 50 个 epochs,只需要几秒钟,通过将输入和输出都设置为前一章节中的 IMDB sent2vec 向量,如下所示通过编译和训练代码:

autoencoder.compile(optimizer='adam',loss='mse',metrics=["mse","mae"])
autoencoder.fit(train_x,train_x,validation_data=(test_x, test_x),
batch_size=32,nb_epoch=50, shuffle=True)

我们在这个回归问题中使用均方误差(mse)作为损失函数,和平均绝对误差(mae)作为额外的度量。最小验证 mae 值约为 0.06。

接下来,我们使用训练好的自编码器将书评投影到 IMDB 特征空间中,该自编码器是经过训练来重构刚刚描述的特征。这意味着我们使用自编码器对书评特征向量进行预处理。然后我们将 IMDB 分类器的准确性评估实验重复在这些预处理的向量上作为输入,如下所示:

EmbeddingVectorsScaledProjected = autoencoder.predict(EmbeddingVectors)
print(model.evaluate(x=EmbeddingVectorsScaledProjected,y=sentiments))

现在观察到的准确率约为 75%,表明改进约为 1%,并且实现了零-shot 领域适应的一个实例。重复多次后,我们发现改进始终保持在 0.5-1%左右,这让我们确信自动编码领域适应的确导致了一些积极的迁移。

总结

  • 预训练的词嵌入,以及文本更高层次的嵌入——如句子,已经在自然语言处理中变得无处不在,并且可以用来将文本转换为数字/向量。这简化了进一步从中提取含义的处理过程。

  • 这种提取代表了一种半监督浅层迁移学习,它已经在实践中被广泛使用,并取得了巨大成功。

  • 像硬参数共享和软参数共享这样的技术使我们能够创建多任务学习系统,其中包括简化的工程设计、改进的泛化和减少的过拟合风险。

  • 有时可能会在目标领域没有标记数据的情况下实现零-shot 迁移学习,这是一种理想的情况,因为标记数据收集可能很昂贵。

  • 有时可能会修改或调整目标领域的数据,使其更类似于源领域的数据,例如,通过自编码器等投影方法,这可以提高性能。

  1. S. J. Pan and Q. Yang, “迁移学习综述”,IEEE Knowledge and Data Engineering Transactions(2009)。

  2. S. Ruder, “自然语言处理的神经迁移学习”,爱尔兰国立大学,高威(2019)。

  3. D. Wang and T. F. Zheng, “语音和语言处理的迁移学习”,2015 年亚太信号和信息处理协会年度峰会和会议(APSIPA)。

  4. Jing Wang, Haibo Hea Danil 和 V.Prokhorov, “用于降维的折叠神经网络自动编码器”,计算机科学会议文献 13 (2012): 120-27.

  5. Z. Yin and Y. Shen, “关于词嵌入的维度性”,32 届神经信息处理系统会议(NeurIPS 2018),加拿大蒙特利尔。

  6. fasttext.cc/docs/en/english-vectors.html

  7. J. Alabi 等,“大规模词嵌入与策划词嵌入对低资源语言的影响。约鲁巴语和特威语的情况”,语言资源和评估国际会议(LREC 2020),法国马赛。

  8. www.kaggle.com/yangjia1991/jigsaw

  9. github.com/epfml/sent2vec

  10. www.kaggle.com/maxjeblick/sent2vec

  11. S. Ruder, “自然语言处理的神经迁移学习”,爱尔兰国立大学,高威(2019)。

  12. Hal Daumé III,“令人沮丧地简单领域自适应”,计算语言学协会第 45 届年会论文集(2007),捷克布拉格。

  13. S. Ruder 和 B. Plank,“使用贝叶斯优化学习选择数据的迁移学习方法”,2017 年自然语言处理实证方法会议论文集(2017),丹麦哥本哈根。

  14. Jing Wang、Haibo Hea Danil 和 V.Prokhorov,“用于降维的折叠神经网络自动编码器”,Procedia Computer Science 13(2012):120-127。

  15. www.cs.jhu.edu/~mdredze/datasets/sentiment/

第五章:处理数据以用于循环神经网络深度迁移学习实验

本章涵盖

  • 循环神经网络(RNNs)在 NLP 迁移学习中的建模架构概述

  • 处理和建模表格文本数据

  • 分析一对新的代表性 NLP 问题

在上一章中,我们详细研究了一些在 NLP 迁移学习中重要的浅层神经网络架构,包括 word2vec 和 sent2vec。 还要记住,这些方法产生的向量是静态和非上下文的,也就是说,它们对所讨论的词或句子产生相同的向量,无论周围上下文如何。这意味着这些方法无法消歧或区分词或句子的不同可能含义。

在这一章和下一章中,我们将介绍一些代表性的自然语言处理(NLP)深度迁移学习建模架构,这些架构依赖于循环神经网络(RNNs)的关键功能。具体来说,我们将研究建模框架 SIMOn,¹ ELMo,² 和 ULMFiT.³ 这些方法所使用的更深层次的神经网络的性质将使得所得到的嵌入是具有上下文的,即产生依赖上下文的词嵌入并实现消歧。回想一下,我们在第三章首次遇到了 ELMo。在下一章中,我们将更加深入地研究它的架构。

为了对本体进行建模的语义推理(SIMOn)是在 DARPA 的数据驱动模型发现(D3M)计划期间开发的,该计划旨在自动化数据科学家面临的一些典型任务,⁴ 包括自动构建用于数据清洗、特征提取、特征重要性排名和为任何给定数据科学问题选择模型的处理管道。这些任务通常被称为自动机器学习AutoML。具体来说,该模型试图将表格数据集中的每一列分类为基本类型,比如整数、字符串、浮点数或地址。其想法是 AutoML 系统可以根据这些信息决定如何处理输入的表格数据——这是实践中遇到的一种重要数据。人们可以从前述 D3M 计划网页下载程序中开发的各种工具的预打包的 Docker 镜像,包括 SIMOn。

SIMOn 的开发受到了计算机视觉中的迁移学习的类比的启发,这些内容在第一章的结尾进行了讨论。它的训练过程展示了如何使用迁移学习来使用模拟数据来补充少量手动标记的数据。将处理的类别集扩展到最初进行训练的类别之外是另一个在这个框架中生动地使用迁移学习的任务。这个模型在 D3M 中被大量使用,在本章中作为一个相对简单的实际例子,说明了如何利用迁移学习来解决真正的、实际的挑战。SIMOn 还被用于在社交媒体上检测潜在有害的沟通。#pgfId-1096624 列类型分类被用作这个建模框架的一个生动例子。

我们从一个介绍列数据类型分类示例的章节开始本章。在那一节中,相关的模拟数据生成和预处理过程也得到了简要的涉及。我们接着描述等效步骤用于“假新闻”检测示例,在下一章中将用于 ELMo 的一个实例。

在图 5.1 中展示了 SIMOn 架构的可视化,在表格列类型分类示例的背景下。粗略地说,它使用卷积神经网络(CNNs)来为句子构建初步的嵌入,使用一对 RNNs 来首先为句子中的字符构建内部上下文,然后为文档中的句子构建外部上下文。

05_01

图 5.1 在表格列类型分类示例中展示了 SIMOn 架构的可视化

从图中我们可以看到,这种架构由字符级卷积神经网络(CNNs)和双向长短期记忆(bi-LSTM)网络元素组成,这是一种递归神经网络(RNN)的类型。在这个框架中,值得强调的是输入文本被分词成句子,而不是单词。另外,将每个句子视为对应给定文档的列的单元,能够将非结构化文本转换为框架考虑的表格数据集上下文。

语言模型嵌入(ELMo)可以说是与正在进行的 NLP 迁移学习革命相关联的最受欢迎的早期预训练语言模型。它与 SIMOn 有许多架构上的相似之处,也由字符级 CNNs 与 bi-LSTMs 组成。这种相似性使得在介绍 SIMOn 之后深入挖掘 ELMo 的架构成为一个自然的下一步。我们将把 ELMo 应用到一个用于说明的问题上,即“假新闻”检测,以提供一个实际的背景。

图 5.2 显示了在表格列类型分类的背景下可视化的 ELMo 架构。 两个框架之间的一些相似之处和差异立即显而易见。 我们可以看到,两个框架都使用字符级 CNN 和双向 LSTM。 但是,虽然 SIMOn 有两个用于 RNN 的上下文构建阶段——一个用于句子中的字符,另一个用于文档中的句子——而 ELMo 只有一个阶段,重点是为输入文档中的单词建立上下文。

05_02

图 5.2 在表格列类型分类示例的背景下可视化了 ELMo 架构。

最后,我们将介绍通用语言模型微调(ULMFiT)框架,该框架引入并演示了一些关键技术和概念,使预训练语言模型能够更有效地适应新环境,如区分微调和逐步解冻。 区分性微调规定,由于语言模型的不同层包含不同类型的信息,因此应以不同的速率进行调整。 逐步解冻描述了一种逐渐微调更多参数的过程,旨在减少过拟合的风险。 ULMFiT 框架还包括在适应过程中以独特方式改变学习率的创新。 我们将在下一章介绍 ELMo 之后介绍该模型,以及其中的几个概念。

5.1 预处理表格列类型分类数据

在本节中,我们介绍了在本章和随后的章节中将探讨的第一个示例数据集。 在这里,我们有兴趣开发一种算法,该算法可以接收表格数据集,并为用户确定每列中的基本类型,即确定哪些列是整数、字符串、浮点数、地址等。 这样做的关键动机是,自动机器学习系统可以根据这些信息决定如何处理输入的表格数据——这是实践中遇到的一种重要数据类型。 例如,检测到的纬度和经度坐标值可以绘制在地图上并显示给用户。 检测到的浮点列可能是回归问题的潜在候选输入或输出,而分类列是分类问题的依赖变量的候选项。 我们用图 5.3 中的一个简单示例可视化了这个问题的本质。

05_03

图 5.3 使用简单示例可视化表格列数据类型分类问题

我们强调这是一个多标签、多类问题,因为每个输入示例都有多种可能的类别,并且每个输入样本可以分配多个这样的类别。例如,在图 5.3 中,第一列客户 ID 具有多个输出标签,即categoricalint。这还有助于处理输入列不是“干净”的情况,即它们包含多种类型。这些列可以带有所有存在的类型标签,并传递给相关解析器进行进一步清洁。

现在,我们对于这个问题有了更好的理解,让我们开始获取一些表格数据,用于本节的实验。

5.1.1 获取和可视化表格数据

我们将使用两个简单的数据集来说明下一章中的表格列类型分类示例。这两个数据集中的第一个是由 OpenML 提供的棒球球员统计数据集。⁶该数据集描述了一组球员的棒球统计数据,以及他们是否最终进入名人堂。

在 Linux 系统上,我们可以按如下方式获取数据集:

!wget https:/ /www.openml.org/data/get_csv/3622/dataset_189_baseball.arff

从以前的章节中可以回忆到,“!”符号仅在执行 Jupyter 环境(例如我们建议在这些练习中使用的 Kaggle 环境)时需要,当在终端中执行时,应该将其删除。同时请注意,对于我们的目的,.arff格式与.csv格式在功能上是等效的。

获取了感兴趣的数据集后,让我们像往常一样使用 Pandas 进行预览:

import pandas as pd
raw_baseball_data = pd.read_csv('dataset_189_baseball.arff', dtype=str)   ❶
print(raw_baseball_data.head())

❶对于我们的目的,.arff格式与.csv格式在功能上是等效的。

这将显示 DataFrame 的前五行,如下所示:

         Player Number_seasons Games_played At_bats  Runs  Hits Doubles  \
0    HANK_AARON             23         3298   12364  2174  3771     624   
1   JERRY_ADAIR             13         1165    4019   378  1022     163   
2  SPARKY_ADAMS             13         1424    5557   844  1588     249   
3   BOBBY_ADAMS             14         1281    4019   591  1082     188   
4    JOE_ADCOCK             17         1959    6606   823  1832     295   

  Triples Home_runs  RBIs Walks Strikeouts Batting_average On_base_pct  \
0      98       755  2297  1402       1383           0.305       0.377   
1      19        57   366   208        499           0.254       0.294   
2      48         9   394   453        223           0.286       0.343   
3      49        37   303   414        447           0.269        0.34   
4      35       336  1122   594       1059           0.277       0.339   

  Slugging_pct Fielding_ave     Position Hall_of_Fame  
0        0.555         0.98     Outfield            1  
1        0.347        0.985  Second_base            0  
2        0.353        0.974  Second_base            0  
3        0.368        0.955   Third_base            0  
4        0.485        0.994   First_base            0  

我们可以看到这是一组广告中所述的突击手棒球统计数据集。

现在我们获取另一个表格数据集。不多赘述,这个数据集将用于扩展我们的 SIMOn 分类器,超越预训练模型所设计的类别集合。这个练习将为转移学习提供一个有趣的使用案例,可以激发你自己应用的创意。

我们将要查看的第二个数据集是多年的不列颠哥伦比亚省公共图书馆统计数据集,我们从 BC 数据目录⁷获得,但也将其附加到我们的伴随 Kaggle 笔记本上,以方便你使用。要使用 Pandas 加载数据集,我们执行以下命令,其中我们 Kaggle 环境中该文件的位置应该替换为您本地的路径,如果选择在本地工作:

raw_data = pd.read_csv('../input/20022018-bc-public-libraries-open-data-v182/2002-2018-bc-public-libraries-open-data-csv-v18.2.csv', dtype=str)

我们可以使用以下命令查看数据集:

print(raw_data.head())

输出结果为:

   YEAR                           LOCATION                      LIB_NAME  \
0  2018  Alert Bay Public Library & Museum      Alert Bay Public Library   
1  2018       Beaver Valley Public Library  Beaver Valley Public Library   
2  2018        Bowen Island Public Library   Bowen Island Public Library   
3  2018             Burnaby Public Library        Burnaby Public Library   
4  2018          Burns Lake Public Library     Burns Lake Public Library   

                     LIB_TYPE SYMBOL        Federation             lib_ils  \
0  Public Library Association   BABM    Island Link LF     Evergreen Sitka   
1  Public Library Association   BFBV       Kootenay LF     Evergreen Sitka   
2           Municipal Library    BBI      InterLINK LF     Evergreen Sitka   
3           Municipal Library     BB      InterLINK LF  SirsiDynix Horizon   
4  Public Library Association   BBUL  North Central LF     Evergreen Sitka   

  POP_SERVED srv_pln STRAT_YR_START  ... OTH_EXP    TOT_EXP EXP_PER_CAPITA  \
0        954     Yes          2,013  ...    2488      24439        25.6174   
1      4,807     Yes          2,014  ...   15232  231314.13       48.12027   
2      3,680     Yes          2,018  ...   20709  315311.17       85.68238   
3    232,755     Yes          2,019  ...  237939   13794902       59.26791   
4      5,763     Yes          2,018  ...     NaN     292315       50.72271   

  TRANSFERS_TO_RESERVE AMORTIZATION EXP_ELEC_EBOOK EXP_ELEC_DB  \
0                    0            0              0         718   
1                11026            0        1409.23      826.82   
2                11176        40932           2111       54.17   
3                    0      2614627         132050           0   
4                  NaN          NaN              0           0   

  EXP_ELEC_ELEARN EXP_ELEC_STREAM EXP_ELEC_OTHER  
0               0               0            752  
1         1176.11               0        1310.97  
2            3241               0              0  
3               0               0         180376  
4               0               0           7040  

[5 rows x 235 columns]

我们只对百分比和整数这一对列感兴趣,我们可以按以下方式提取并显示:

COLUMNS = ["PCT_ELEC_IN_TOT_VOLS","TOT_AV_VOLS"]    ❶
raw_library_data = raw_data[COLUMNS]
print(raw_library_data)

❶这个数据集有很多列,我们只关注这两列。

这将产生以下输出,展示我们将使用的另外两列:

     PCT_ELEC_IN_TOT_VOLS TOT_AV_VOLS
0                  90.42%          57
1                  74.83%       2,778
2                  85.55%       1,590
3                   9.22%      83,906
4                  66.63%       4,261
...                   ...         ...
1202                0.00%      35,215
1203                0.00%     109,499
1204                0.00%         209
1205                0.00%      18,748
1206                0.00%        2403

[1207 rows x 2 columns]

5.1.2 预处理表格数据

现在让我们将获取的表格数据预处理成 SIMOn 框架可以接受的形式。由于我们将使用一个预训练模型,该模型预先包含一个编码器,我们将应用于此目的,因此我们需要首先安装 SIMOn,使用以下命令:

!pip install git+https:/ /github.com/algorine/simon

完成这些之后,我们还需要导入一些必需的模块,如下所示:

from Simon import Simon              ❶
from Simon.Encoder import Encoder    ❷

❶ 导入 SIMOn 模型类

❷ 导入 SIMOn 数据编码器类,用于将输入文本转换为数字

这些导入分别代表了 SIMOn 模型类、数据编码器类、将所有输入数据标准化为固定长度的实用程序,以及生成模拟数据的类。

接下来,我们获取一个预训练的 SIMOn 模型,它带有自己的编码器,用于将文本转换为数字。该模型由两个文件组成:一个包含编码器和其他配置,另一个包含模型权重。我们使用以下命令获取这些文件:

!wget https:/ /raw.githubusercontent.com/algorine/simon/master/Simon/scripts/❶pretrained_models/Base.pkl                                              ❶
!wget https:/ /raw.githubusercontent.com/algorine/simon/master/Simon/scripts/❷pretrained_models/text-class.17-0.04.hdf5                               ❷

❶ 预训练的 SIMOn 模型配置、编码器等

❷ 对应的模型权重

在我们加载模型权重之前,首先需要加载它们的配置,这些配置包括编码器,通过以下一系列命令:

checkpoint_dir = ""                                                    ❶
execution_config = "Base.pkl"                                          ❷
Classifier = Simon(encoder={})                                         ❸
config = Classifier.load_config(execution_config, checkpoint_dir)      ❹
encoder = config['encoder']                                            ❺
checkpoint = config['checkpoint']                                      ❻

❶ 模型权重位于当前级别。

❷ 下载的预训练模型配置的名称

❸ 创建一个文本分类器实例,用于从模型配置中加载编码器。

❹ 加载模型配置

❺ 提取编码器

❻ 提取检查点名称

为了确保我们下载了正确的权重集,通过以下方式双重检查模型所需的权重文件:

print(checkpoint)

通过打印以下内容,应确认我们获取了正确的文件:

text-class.17-0.04.hdf5

最后,我们需要为建模表格数据指定两个关键参数。参数max_cells指定表格每列的最大单元格数。参数max_len指定每个单元格的最大长度。这在图 5.4 中有所体现。

05_04

图 5.4 可视化表格数据建模参数。参数max_cells指定表格中每列的最大单元格或行数。参数max_len指定每个单元格或行的最大长度。

每列的最大单元格数必须与训练中使用的 500 的值匹配,并且可以从编码器中提取,如下所示:

max_cells = encoder.cur_max_cells

另外,我们将max_len设置为 20,以与预训练模型设置保持一致,并提取预训练模型支持的类别,如下所示:

max_len = 20 # maximum length of each tabular cell
Categories = encoder.categories
category_count = len(Categories)      ❶
print(encoder.categories)

❶ 预训练模型支持的类别数量

我们发现处理的类别如下:

['address', 'boolean', 'datetime', 'email', 'float', 'int', 'phone', 'text', 'uri']

5.1.3 将预处理数据编码为数字

现在我们将使用编码器将表格数据转换为 SIMOn 模型可以用来进行预测的一组数字。这涉及将每个输入字符串中的每个字符转换为该字符在模型编码方案中表示的唯一整数。

因为卷积神经网络(CNNs)需要所有输入都是固定的、预先指定的长度,所以编码器还将标准化每个输入列的长度。这一步骤会复制短于max_cells的列中的随机单元,并丢弃一些长列中的随机单元。这确保了所有列的长度恰好为max_cells。此外,如果需要,所有单元都标准化为长度max_len,并添加填充。我们不会过于担心这些细节,因为 SIMOn API 会在幕后为我们处理它。

我们对棒球数据集进行编码,并使用以下代码显示其形状:

X_baseball = encoder.encodeDataFrame(raw_baseball_data)   ❶
print(X_baseball.shape)                                   ❷
print(X_baseball[0])                                      ❸

❶ 编码数据(标准化、转置、转换为 NumPy 数组)

❷ 显示了编码数据的形状

❸ 显示了编码的第一列

执行此操作会产生以下输出,其中首先显示输出形状元组,然后显示编码的第一列:

(18, 500, 20)
[[-1 -1 -1 ... 50 37 44]
 [-1 -1 -1 ... 54 41 46]
 [-1 -1 -1 ... 37 52 55]
 ...
 [-1 -1 -1 ... 49 45 46]
 [-1 -1 -1 ... 51 54 43]
 [-1 -1 -1 ... 38 37 43]]

我们看到每个编码列都是一个max_cells=500乘以max_len=20的数组,正如预期的那样。我们还注意到编码列的-1 条目代表了短于max_len的单元的填充。

我们还对图书馆数据进行编码,以便以后使用:

X_library = encoder.encodeDataFrame(raw_library_data)

在这个阶段,我们已经将示例输入数据集转换成了适当形状的 NumPy 数组。这将文本编码为适合 SIMOn 神经网络第一阶段——生成初步输入句子嵌入的 CNN 的摄入和分析的数字。

5.2 预处理事实检验示例数据

在这一节中,我们介绍了将在本章和后续章节中研究的第二个示例数据集。在这里,我们感兴趣的是开发一种算法,用于区分事实新闻和潜在的错误信息或虚假信息。这个应用领域变得越来越重要,并经常被称为“自动假新闻检测”。

对我们来说很方便的是,Kaggle⁸上有一个适用的数据集。该数据集包含超过 40,000 篇文章,分为两类:“假”和“真”。真实的文章来自一家名声显赫的新闻网站 reuters.com。而假新闻则来自 PolitiFact 标记为不可靠的各种来源。这些文章大多涉及政治和世界新闻。

5.2.1 特殊问题考虑

可以称为假的主题无疑是一个值得讨论的敏感话题。可以肯定的是,准备训练数据标签的人的偏见可能会转移到分类系统中。在这样敏感的语境下,标签的有效性需要特别注意和考虑如何创建。

此外,尽管我们在本节的目的是开发一个基于内容的分类系统,用于区分真实文章与潜在虚假文章,但重要的是要强调,现实场景要复杂得多。换句话说,检测潜在错误信息传播只是检测影响行动问题的一个方面。要理解两者之间的区别,请考虑即使真实信息也可以用来影响意见,从而损害品牌,如果将其放在错误的上下文或不自然地放大。

检测影响行动可以自然地被构造为一个异常检测问题,⁹ 但这样的系统只有作为缓解策略的一部分时才能有效。它必须是跨平台的,尽可能监控和分析尽可能多的潜在信息渠道中的异常情况。此外,今天的大多数实用系统都嵌入了人类,即检测系统只标记聚合的可疑活动,并将最终行动呼叫留给人类分析员。

5.2.2 加载和可视化事实检查数据

现在,我们直接跳转到加载事实检查数据并使用 ELMo 建模框架对其进行分类的步骤。回想一下第 3.2.1 节,我们在那里将 ELMo 应用于垃圾邮件检测和电影评论情感分析,该模型期望将每个输入文档作为单个字符串。这使得事情变得更容易——不需要分词。还要注意,数据集已经附加到了 Kaggle 上的伴随 Jupyter 笔记本上。

我们使用列表 5.1 中的代码从数据集中加载真假数据。请注意,我们选择在此加载每种 1,000 个样本,以保持与第 3.2.1 节的一致性。

列表 5.1 加载每种 1,000 个真假文章样本

import numpy as np
import pandas as pd

DataTrue = pd.read_csv("/kaggle/input/fake-and-real-news-dataset/True.csv")❶
DataFake = pd.read_csv("/kaggle/input/fake-and-real-news-dataset/Fake.csv")❷

Nsamp =1000                                                                ❸
DataTrue = DataTrue.sample(Nsamp)
DataFake = DataFake.sample(Nsamp)
raw_data = pd.concat([DataTrue,DataFake], axis=0).values                   ❹

raw_data = [sample[0].lower() + sample[1].lower() + sample[3].lower() for sample in raw_data]                                                   ❺

Categories = ['True','False']                                              ❻
header = ([1]*Nsamp)
header.extend(([0]*Nsamp))

❶ 将真实新闻数据读入 Pandas DataFrame

❷ 将假新闻数据读入 Pandas DataFrame

❸ 每个类别生成的样本数——真实,虚假

❹ 连接的真假样本

❺ 将标题、正文和主题组合成每个文档的一个字符串

❻ 对应的标签

其次,我们使用以下代码将数据洗牌并将其分为 70% 的训练/30% 的验证,以方便起见,这些代码在此处从第 3.2.1 节复制:

def unison_shuffle(a, b):                             ❶
    p = np.random.permutation(len(b))
    data = np.asarray(a)[p]
    header = np.asarray(b)[p]
    return data, header

raw_data, header = unison_shuffle(raw_data, header)   ❷

idx = int(0.7*raw_data.shape[0])                      ❸

train_x = raw_data[:idx]                              ❹
train_y = header[:idx]
test_x = raw_data[idx:]                               ❺
test_y = header[idx:]

❶ 一个用于与标签头一起洗牌数据的函数,以消除任何潜在的顺序偏差

❷ 通过调用先前定义的函数来洗牌数据

❸ 分成独立的 70% 训练和 30% 测试集

❹ 70% 的数据用于训练

❺ 剩余 30% 用于验证

在介绍和预处理示例问题数据之后,我们将在下一章中将章节开头概述的三个基于 RNN 的神经网络模型应用于示例问题数据。

总结

  • 与单词级模型相比,字符级模型可以处理拼写错误和其他社交媒体特征,如表情符号和小众俚语。

  • 双向语言建模是构建意识到其局部上下文的词嵌入的关键。

  • SIMOn 和 ELMo 都采用字符级 CNN 和双向 LSTM,后者有助于实现双向上下文建模。

  1. P. Azunre 等人,“基于字符级卷积神经网络的表格数据集的语义分类”,arXiv(2019 年)。

  2. M. E. Peters 等人,“Deep Contextualized Word Representations”,NAACL-HLT 会议论文集(2018 年)。

  3. J. Howard 等人,“文本分类的通用语言模型微调”,第 56 届计算语言学年会论文集(2018 年)。

  4. docs.datadrivendiscovery.org/

  5. N. Dhamani 等人,“利用深度网络和迁移学习解决虚假信息问题”,AI for Social Good ICML 研讨会(2019 年)。

  6. www.openml.org/d/185

  7. catalogue.data.gov.bc.ca/dataset/bc-public-libraries-statistics-2002-present

  8. www.kaggle.com/clmentbisaillon/fake-and-real-news-dataset

  9. P. Azunre 等人,“虚假信息:检测到阻断”,真相和信任在线会议 1 卷 1 期(2019 年)。

第六章:.循环神经网络用于自然语言处理的深度迁移学习

本章内容包括

  • 依赖于 RNN 的自然语言处理迁移学习的三种代表性建模架构

  • 将这些方法应用于上一章中介绍的两个问题

  • 将在模拟数据训练中获得的知识传递到真实标记数据

  • 介绍一些更复杂的模型适应策略,通过 ULMFiT

在上一章中,我们介绍了两个用于本章实验的例子问题——列类型分类和虚假新闻检测。回顾一下,实验的目标是研究依赖于循环神经网络(RNN)的深度迁移学习方法,以用于自然语言处理的关键功能。具体而言,我们将重点研究三种方法——SIMOn、ELMo 和 ULMFiT,这些方法在上一章中已经简要介绍过。在下一节中,我们将从 SIMOn 开始,将它们应用于示例问题。

6.1 语义推理用于本体建模(SIMOn)

正如我们在上一章中简要讨论的那样,SIMOn 是作为自动机器学习(AutoML)管道的一个组成部分而设计的,用于数据驱动的模型发现(D3M)DARPA 计划。它被开发为用于表格数据集中列类型的分类工具,但也可以看作是一个更一般的文本分类框架。我们将首先在任意文本输入的环境下介绍该模型,然后将其专门用于表格案例。

SIMOn 是一个字符级模型,而不是单词级模型,以处理拼写错误和其他社交媒体特征,如表情符号和专业知识的口头语。因为它以字符级别编码输入文本,所以输入只需要用于分类的允许字符即可。这使得模型能够轻松适应社交媒体语言的动态特性。模型的字符级本质在图 6.1 中与单词级模型进行对比。在图的左侧,我们展示了单词级编码器,其输入必须是一个有效的单词。显然,由于拼写错误或行话,一个词汇表外的词是无效的输入。对于字符级编码器,如 ELMo 和 SIMOn 所示,输入只需要是一个有效的字符,这有助于处理拼写错误。

06_01

图 6.1 对比基于单词级和字符级的文本分类模型

6.1.1 通用神经架构概述

该网络可以分为两个主要耦合的部分,将一个被分割为句子的文档作为输入。第一个部分是一个用于编码每个独立句子的网络,而第二个部分则使用编码的句子创建整个文档的编码。

句子编码器首先对输入句子进行字符级的独热编码,使用了一个包含 71 个字符的字典。这包括所有可能的英文字母,以及数字和标点符号。输入句子也被标准化为长度为max_len。然后通过一系列的卷积、最大池化、失活和双向 LSTM 层。请参考图 5.1 的前两个阶段,这里为了方便起见重复一次,进行一个摘要可视化。卷积层在每个句子中实质上形成了“词”的概念,而双向 LSTM“查看”一个词周围的两个方向,以确定其局部上下文。这一阶段的输出是每个句子的默认维度为 512 的嵌入向量。还可以比较图 5.1 和图 6.1 中双向 LSTM 的等效图示来使事情具体化。

06_01_05_01

图 5.1(为了方便起见,从上一章中重复)在表格列类型分类示例中可视化 SIMOn 架构

文档编码器将句子嵌入向量作为输入,类似地通过一系列的随机失活和双向 LSTM 层来处理它们。每个文档的长度被标准化为max_cells个这样的嵌入向量。可以将这看作是从句子中形成更高级的“概念”或“主题”的过程,这些概念与文档中存在的其他概念相关联。这为每个文档产生了一个嵌入向量,然后通过一个分类层传递,输出每种不同类型或类的概率。

6.1.2 对表格数据进行建模

对表格数据进行建模出人意料的简单;它只需要将表格数据集中每个单元格都视为一个句子。当然,每个这样的列被视为要进行分类的一个文档。

这意味着要将 SIMOn 框架应用到非结构化文本,只需将文本转换成一张表,每列一个文档,每个单元格一个句子。这个过程的示意图在图 6.2 中展示。请注意,在这个简单的例子中,我们选择max_cells等于 3,只是为了示例。

06_02

图 6.2 将非结构化文本转换为 SIMOn 可消化的过程

6.1.3 将 SIMOn 应用于表格列类型分类数据

在其原始形式中,SIMOn 最初是在一组基础类的模拟数据上进行训练的。然后转移到一组手工标记的较小数据。了解如何生成模拟数据可能是有用的,因此我们用以下一组命令简要地说明了这个过程,这些命令在底层使用了库 Faker:

from Simon.DataGenerator import DataGenerator      ❶

data_cols = 5                                      ❷
data_count = 10                                    ❸

try_reuse_data = False                             ❹
simulated_data, header = DataGenerator.gen_test_data((data_count, data_cols), try_reuse_data)
print("SIMULATED DATA")                            ❺
print(simulated_data)
print("SIMULATED DATA HEADER:")
print(header)

❶ 模拟/伪造数据生成实用工具(使用库 Faker)

❷ 生成的列数,为了简单起见任意选择

❸ 每列的单元格/行数,为了简单说明而任意选择

❹ 不要重用数据,而是为数据集中的变化性生成新鲜数据。

❺ 打印结果

执行此代码会产生以下输出,显示各种数据类型的生成样本及其相应的标签:

SIMULATED DATA:
[['byoung@hotmail.com' 'Jesse' 'True' 'PPC' 'Lauraview']
 ['cindygilbert@gmail.com' 'Jason' 'True' 'Intel' 'West Brandonburgh']
 ['wilsonalexis@yahoo.com' 'Matthew' 'True' 'U; Intel'
  'South Christopherside']
 ['cbrown@yahoo.com' 'Andrew' 'False' 'U; PPC' 'Loganside']
 ['christopher90@gmail.com' 'Devon' 'True' 'PPC' 'East Charlesview']
 ['deanna75@gmail.com' 'Eric' 'False' 'U; PPC' 'West Janethaven']
 ['james80@hotmail.com' 'Ryan' 'True' 'U; Intel' 'Loriborough']
 ['cookjennifer@yahoo.com' 'Richard' 'True' 'U; Intel' 'Robertsonchester']
 ['jonestyler@gmail.com' 'John' 'True' 'PPC' 'New Kevinfort']
 ['johnsonmichael@gmail.com' 'Justin' 'True' 'U; Intel' 'Victormouth']]
SIMULATED DATA HEADER:
[list(['email', 'text']) list(['text']) list(['boolean', 'text'])
 list(['text']) list(['text'])]

SIMOn 仓库的顶层包含了 types.json 文件,该文件指定了从 Faker 库类到先前显示的类别的映射。例如,前一个示例中名称的第二列被标记为“文本”,因为我们不需要为我们的目的识别名称。您可以快速更改此映射,并为您自己的项目和类别集生成模拟数据。

我们这里不使用模拟数据进行训练,因为该过程可能需要几个小时,而我们已经可以访问捕捉到这些知识的预训练模型。但是,我们会进行一项说明性的迁移学习实验,涉及扩展支持的类别集合,超出了预训练模型中可用的类别。

回想一下,在第 5.1.2 节中加载了 SIMOn 分类器类以及模型配置,包括编码器。然后我们可以生成一个 Keras SIMOn 模型,将下载的权重加载到其中,并使用以下命令序列进行编译:

model = Classifier.generate_model(max_len, max_cells, category_count)   ❶
Classifier.load_weights(checkpoint, None, model, checkpoint_dir)        ❷
model.compile(loss='binary_crossentropy',optimizer='adam', metrics=['accuracy'])                                              ❸

❶ 生成模型

❷ 加载权重

❸ 编译模型,使用二元交叉熵损失进行多标签分类

在继续之前,查看模型架构是个好主意,我们可以使用以下命令来做到这一点:

model.summary() 

这将显示以下输出,并允许您更好地了解内部发生的情况:

______________________________________________________________________________________
Layer (type)                    Output Shape         Param #   Connected to           
======================================================================================
input_1 (InputLayer)            (None, 500, 20)      0                                
______________________________________________________________________________________
time_distributed_1 (TimeDistrib (None, 500, 512)     3202416   input_1[0][0]          
______________________________________________________________________________________
lstm_3 (LSTM)                   (None, 128)          328192    time_distributed_1[0][0]
______________________________________________________________________________________
lstm_4 (LSTM)                   (None, 128)          328192    time_distributed_1[0][0]
______________________________________________________________________________________
concatenate_2 (Concatenate)     (None, 256)          0         lstm_3[0][0]           
                                                               lstm_4[0][0]           
______________________________________________________________________________________
dropout_5 (Dropout)             (None, 256)          0         concatenate_2[0][0]    
______________________________________________________________________________________
dense_1 (Dense)                 (None, 128)          32896     dropout_5[0][0]        
______________________________________________________________________________________
dropout_6 (Dropout)             (None, 128)          0         dense_1[0][0]          
______________________________________________________________________________________
dense_2 (Dense)                 (None, 9)            1161      dropout_6[0][0]        

time_distributed_1 层是应用于每个输入句子的句子编码器。我们看到其后是前向和后向的 LSTM,它们被连接在一起,一些通过 dropout 进行的正则化,以及来自 dense_2 层的输出概率。回想一下,预训练模型处理的类别数恰好为 9,这与输出 dense_2 层的维度匹配。还要注意的是,巧合的是,模型总共有 9 层。

通过执行以下一系列命令,我们已经对编译模型的架构有了一定的了解,现在让我们继续查看它认为棒球数据集列的类型是什么。我们通过执行以下命令序列来实现这一点:

p_threshold = 0.5                                       ❶
y = model.predict(X_baseball)                           ❷
result = encoder.reverse_label_encode(y,p_threshold)    ❸
print("Recall that the column headers were:")           ❹
print(list(raw_baseball_data))
print("The predicted classes and probabilities are respectively:")
print(result)

❶ 用于决定类成员身份的概率阈值

❷ 预测棒球数据集列的类别

❸ 将概率转换为类别标签

❹ 显示输出

对应的代码输出如下所示:

Recall that the column headers were:
['Player', 'Number_seasons', 'Games_played', 'At_bats', 'Runs', 'Hits', 'Doubles', 'Triples', 'Home_runs', 'RBIs', 'Walks', 'Strikeouts', 'Batting_average', 'On_base_pct', 'Slugging_pct', 'Fielding_ave', 'Position', 'Hall_of_Fame']
The predicted classes and probabilities are respectively:
([('text',), ('int',), ('int',), ('int',), ('int',), ('int',), ('int',), ('int',), ('int',), ('int',), ('int',), ('int',), ('float',), ('float',), ('float',), ('float',), ('text',), ('int',)], [[0.9970826506614685], [0.9877430200576782], [0.9899477362632751], [0.9903284907341003], [0.9894667267799377], [0.9854978322982788], [0.9892633557319641], [0.9895514845848083], [0.989467203617096], [0.9895854592323303], [0.9896339178085327], [0.9897230863571167], [0.9998295307159424], [0.9998230338096619], [0.9998272061347961], [0.9998039603233337], [0.9975670576095581], [0.9894945025444031]])

回顾第 5.1.1 节以显示此数据的切片,我们在此复制,我们看到模型以高置信度完全正确地获取了每一列:

         Player Number_seasons Games_played At_bats  Runs  Hits Doubles  \
0    HANK_AARON             23         3298   12364  2174  3771     624   
1   JERRY_ADAIR             13         1165    4019   378  1022     163   
2  SPARKY_ADAMS             13         1424    5557   844  1588     249   
3   BOBBY_ADAMS             14         1281    4019   591  1082     188   
4    JOE_ADCOCK             17         1959    6606   823  1832     295   

  Triples Home_runs  RBIs Walks Strikeouts Batting_average On_base_pct  \
0      98       755  2297  1402       1383           0.305       0.377   
1      19        57   366   208        499           0.254       0.294   
2      48         9   394   453        223           0.286       0.343   
3      49        37   303   414        447           0.269        0.34   
4      35       336  1122   594       1059           0.277       0.339   

  Slugging_pct Fielding_ave     Position Hall_of_Fame  
0        0.555         0.98     Outfield            1  
1        0.347        0.985  Second_base            0  
2        0.353        0.974  Second_base            0  
3        0.368        0.955   Third_base            0  
4        0.485        0.994   First_base            0  

现在,假设我们有兴趣在项目中检测具有百分比值的列。我们如何快速使用预训练模型来实现这一点呢?我们可以使用上一章中准备的第二个表格数据集来调查这种情况——多年来的不列颠哥伦比亚公共图书馆统计数据集。当然,第一步是直接使用预训练模型预测这些数据。以下一系列命令实现了这一点:

X = encoder.encodeDataFrame(raw_library_data)          ❶
y = model.predict(X)                                   ❷
result = encoder.reverse_label_encode(y,p_threshold)   ❸
print("Recall that the column headers were:")
print(list(raw_library_data))
print("The predicted class/probability:")
print(result)

❶ 使用原始框架对数据进行编码

❷ 预测类别

❸ 将概率转换为类标签

这将产生以下输出:

Recall that the column headers were:
['PCT_ELEC_IN_TOT_VOLS', 'TOT_AV_VOLS']
The predicted class/probability:
([('text',), ('int',)], [[0.7253058552742004], [0.7712462544441223]])

回顾 5.1.1 节的一个数据切片,我们看到整数列被正确识别,而百分比列被识别为文本:

     PCT_ELEC_IN_TOT_VOLS TOT_AV_VOLS
0                  90.42%          57
1                  74.83%       2,778
2                  85.55%       1,590
3                   9.22%      83,906
4                  66.63%       4,261
...                   ...         ...
1202                0.00%      35,215
1203                0.00%     109,499
1204                0.00%         209
1205                0.00%      18,748
1206                0.00%        2403

[1207 rows x 2 columns]

那并不是不正确,但也不完全是我们正在寻找的,因为它不够具体。

我们将快速将预训练模型转移到一个非常小的包含百分比样本的训练数据集。首先让我们使用以下命令了解原始库 DataFrame 的大小:

print(raw_library_data.shape)

我们发现尺寸为(1207,2),这似乎是足够构建一个小数据集的行数!

在清单 6.1 中,我们展示了可用于将此数据集分割为许多每个 20 个单元格的更小列的脚本。数字 20 是任意选择的,是为了创建足够多的唯一列——大约 50 个——在生成的数据集中。此过程产生一个新的 DataFrame,new_raw_data,大小为 20 行 120 列——前 60 列对应于百分比值,后 60 列对应于整数值。它还生成一个相应的header标签列表。

清单 6.1 将长库数据转换为许多较短样本列

                                                                           ❶
percent_value_list = raw_library_data['PCT_ELEC_IN_TOT_VOLS'].values.tolist()
int_value_list = raw_library_data['TOT_AV_VOLS'].values.tolist()

                                                                           ❷
original_length = raw_data.shape[0]                                        ❸
chunk_size = 20 # length of each newly generated column
header_list = list(range(2*original_length/ /chunk_size))                   ❹
new_raw_data = pd.DataFrame(columns = header_list)                         ❺
for i in range(original_length/ /chunk_size):                               ❻
    new_raw_data[i] = percent_value_list[i:i+chunk_size]                   ❼
    new_raw_data[original_length/ /chunk_size+i] = int_value_list[i:i+chunk_size]                                        ❽

header = [("percent",),]*(original_length/ /chunk_size)                     ❾
header.extend([("int",),]*(original_length/ /chunk_size))

❶ 将数据转换为两个列表

❷ 将其分解为每个样本列 20 个单元格

❸ 原始长度,1207

❹ 新列的索引列表

❺ 初始化新的 DataFrame 以保存新数据

❻ 使用新的 DataFrame 填充

❼ 使用百分比值填充 DataFrame

❽ 使用整数值填充 DataFrame

❾ 让我们为我们的训练数据创建相应的标题。

记得预训练模型的最后一层具有输出维度为 9,与处理的类的数量相匹配。要添加另一个类,我们需要将输出维度增加到大小为 10。我们还应该将这个新维度的权重初始化为文本类的权重,因为这是预训练模型处理的最相似的类。这是在我们之前使用预训练模型将百分比数据预测为文本时确定的。这是通过下一个清单中显示的脚本完成的。在脚本中,我们将百分比添加到支持的类别列表中,将输出维度增加 1 以容纳此添加,然后将相应维度的权重初始化为最接近的类别文本值的权重。

清单 6.2 创建最终输出层的新权重,包括百分比类

import numpy as np

old_weights = model.layers[8].get_weights()                          ❶
old_category_index = encoder.categories.index('text')                ❷
encoder.categories.append("percent")                                 ❸
encoder.categories.sort()                                            ❹
new_category_index = encoder.categories.index('percent')             ❺

new_weights = np.copy(old_weights)                                   ❻
new_weights[0] = np.insert(new_weights[0], new_category_index, old_weights[0][:,old_category_index], axis=1)                   ❼
new_weights[1] = np.insert(new_weights[1], new_category_index, 0)    ❽

❶ 抓取初始化的最后一层权重

❷ 找到最接近类别的旧权重索引—文本

❸ 使用新的类别列表更新编码器

❹ 对新列表按字母顺序排序

❺ 找到新类别的索引

❻ 将新权重初始化为旧权重

❼ 在百分比权重位置插入文本权重

❽ 在百分比偏差位置插入文本偏差

在执行清单 6.2 中的代码之后,您应该仔细检查数组old_weightsnew_weights的形状。如果一切按预期进行,您应该会发现前者是(128,9),而后者是(128,10)。

现在我们已经准备好在预训练之前用来初始化新模型的权重,让我们实际构建和编译这个新模型。SIMOn API 包含以下函数,使构建模型非常容易:

model = Classifier.generate_transfer_model(max_len, max_cells, category_count, category_count+1, checkpoint, checkpoint_dir)

通过此函数返回的转移模型与我们之前构建的模型完全类似,唯一的区别是最终层现在具有新的维度,由输入category_count+1指定。另外,因为我们没有为新创建的输出层提供任何初始化信息,所以这一层目前被初始化为全零权重。

在我们可以训练这个新的转移模型之前,让我们确保只有最终输出层是可训练的。我们通过以下代码片段完成这一点,并编译模型:

for layer in model.layers:                                                      ❶
    layer.trainable = False
model.layers[-1].trainable = True                                               ❷

model.layers[8].set_weights(new_weights)                                        ❸

model.compile(loss='binary_crossentropy',optimizer='adam', metrics=['accuracy'])❹

❶ 开始时使所有层都不可训练

❷ 只有最后一层应该是可训练的。

❸ 将最终层的权重设置为先前确定的初始化值

❹ 编译模型

现在我们可以使用以下清单中的代码在新数据上训练构建的、初始化的和编译的转移模型。

清单 6.3 训练初始化和编译的新转移模型

import time

X = encoder.encodeDataFrame(new_raw_data)                                          ❶
y = encoder.label_encode(header)                                                   ❷
data = Classifier.setup_test_sets(X, y)                                            ❸

batch_size = 4
nb_epoch = 10
start = time.time()
history = Classifier.train_model(batch_size, checkpoint_dir, model, nb_epoch, data)❹
end = time.time()
print("Time for training is %f sec"%(end-start)) 

❶ 编码新数据(标准化、转置、转换为 NumPy 数组)

❷ 编码标签

❸ 准备预期格式的数据 -> 60/30/10 训练/验证/测试数据拆分

❹ 训练数据

我们在图 6.3 中可视化了此代码生成的收敛信息。我们看到在第七个时期实现了 100%的验证准确率,训练时间为 150 秒。看来我们的实验成功了,我们已成功地微调了预训练模型以处理新的数据类!我们注意到,为了使这个新模型能够准确地处理所有 10 个类,我们需要在转移步骤中的训练数据中包含每个类的一些样本。在这个阶段,微调的模型只适用于预测包含在转移步骤中的类——整数百分比。因为我们这里的目标仅仅是说明性的,我们将此作为读者的警告,并不进一步关注。

06_03

图 6.3 百分比类转移表格数据实验收敛可视化

作为转移实验的最后一步,让我们通过比较测试集的预测标签和真实标签来深入了解其性能。可以通过以下代码片段来完成这个任务:

y = model.predict(data.X_test)                                        ❶
result = encoder.reverse_label_encode(y,p_threshold)                  ❷

print("The predicted classes and probabilities are respectively:")    ❸
print(result) 
print("True labels/probabilities, for comparision:") print(encoder.reverse_label_encode(data.y_test,p_threshold))

❶预测类别

❷将概率转换为类标签

❸ 检查

生成的输出如下:

The predicted classes and probabilities are respectively:
([('percent',), ('percent',), ('int',), ('int',), ('percent',), ('int',), ('percent',), ('int',), ('int',), ('percent',), ('percent',), ('int',)], [[0.7889140248298645], [0.7893422842025757], [0.7004106640815735], [0.7190601229667664], [0.7961368560791016], [0.9885498881340027], [0.8160757422447205], [0.8141483068466187], [0.5697212815284729], [0.8359809517860413], [0.8188782930374146], [0.5185337066650391]])
True labels/probabilities, for comparision:
([('percent',), ('percent',), ('int',), ('int',), ('percent',), ('int',), ('percent',), ('int',), ('int',), ('percent',), ('percent',), ('int',)], [[1], [1], [1], [1], [1], [1], [1], [1], [1], [1], [1], [1]])

我们发现,微调模型已经完全正确地预测了每个例子,进一步验证了我们的迁移学习实验。

最后要记住,通过在 6.1.2 节中描述的适应过程,SIMOn 框架可以应用于任意输入文本,而不仅仅是表格数据。几个应用示例取得了有希望的结果。¹希望本节的练习已经充分准备您在自己的分类应用程序中部署它,并通过迁移学习将生成的分类器适应新情况。

现在我们将继续探讨将 ELMo 应用于虚假新闻分类示例的情况。

6.2 来自语言模型的嵌入(ELMo)

如前一章节简要提到的,来自语言模型的嵌入(ELMo)可以说是与正在进行的 NLP 迁移学习革命相关的最受欢迎的早期预训练语言模型之一。它与 SIMOn 有一些相似之处,因为它也由字符级 CNN 和双向 LSTM 组成。请参考图 5.2,这里重复了一遍,以便鸟瞰这些建模组件。

06_03_05_02

图 5.2(重复)在表格列类型分类示例的背景下可视化 ELMo 架构

还要查看图 6.1,特别是比图 5.2 更详细的相当于双向 LSTM 的图示。如果您按照本书的时间顺序阅读,那么您也已经在 3.2.1 节中将 ELMo 应用于垃圾邮件检测和 IMDB 电影评论情感分类问题。正如您现在可能已经了解到的那样,ELMo 产生的词表示是整个输入句子的函数。换句话说,该模型是上下文感知的词嵌入。

本节深入探讨了 ELMo 的建模架构。ELMo 确切地对输入文本做了什么来构建上下文和消岐?为了回答这个问题,首先介绍了使用 ELMo 进行双向语言建模,接着将该模型应用于虚假新闻检测问题以使问题具体化。

6.2.1 ELMo 双向语言建模

请记住,语言建模试图对一个令牌的出现概率进行建模,通常是一个词,在给定序列中出现。考虑这样一个情景,我们有一个N令牌的序列,例如,句子或段落中的单词。一个以单词为单位的前向语言模型通过取序列中每个令牌在其从左到右的历史条件下的概率的乘积来计算序列的联合概率,如图 6.4 所示。考虑这个简短的句子,“你可以”。根据图 6.4 中的公式,前向语言模型计算句子的概率为第一个词在句子中是“”的概率乘以第二个词是“可以”的概率,假设第一个词是“”,再乘以第三个词是“”的概率,假设前两个词是“你可以”。

06_04

图 6.4 前向语言模型方程

一个以单词为单位的反向语言模型做的是相同的事情,但是反过来,如图 6.5 中的方程所示。它通过对每个令牌在右到左令牌历史条件下的概率的乘积来建模序列的联合概率。

06_05

图 6.5 反向语言模型方程。

再次考虑这个简短的句子,“你可以”。根据图 6.5 中的公式,反向语言模型计算句子的概率为最后一个词在句子中是“”的概率乘以第二个词是“可以”的概率,假设最后一个词是“”,再乘以第一个词是“”的概率,假设其他两个词是“可以是”。

一个双向语言模型结合了前向和后向模型。ELMo 模型特别寻求最大化两个方向的联合对数似然——在图 6.6 中显示的量。请注意,尽管为前向和后向语言模型保留了单独的参数,但令牌向量和最终层参数在两者之间是共享的。这是第四章讨论的软参数共享多任务学习场景的一个例子。

06_06

图 6.6 ELMo 用于为序列中的任何给定令牌构建双向上下文的联合双向语言建模(LM)目标方程

每个令牌的 ELMo 表示来自双向 LSTM 语言模型的内部状态。对于任何给定任务,它是与目标令牌对应的所有 LSTM 层(两个方向上的)的内部状态的线性组合。

将所有内部状态组合在一起,与仅使用顶层不同,例如在 SIMOn 中,具有显著的优势。尽管 LSTM 的较低层使得在基于句法的任务(如词性标注)上具有良好的性能,但较高层使得在含义上进行上下文相关的消歧。学习每个任务在这两种表示类型之间的线性组合,允许最终模型选择它需要的任务类型的信号。

6.2.2 应用于假新闻检测的模型

现在让我们继续构建一个 ELMo 模型,用于我们在第 5.2 节中组装的假新闻分类数据集。对于已经阅读过第三章和第四章的读者来说,这是 ELMo 建模框架对实际示例的第二个应用。

由于我们已经构建了 ELMo 模型,我们将能够重用一些在第三章中已经定义的函数。请参考第 3.4 节的代码,该代码利用 TensorFlow Hub 平台加载了 ELMo 作者提供的权重,并使用ElmoEmbeddingLayer类构建了一个适用于 Keras 的模型。定义了这个类之后,我们可以通过以下代码训练我们所需的用于假新闻检测的 ELMo 模型(与第 3.6 节稍作修改的代码):

def build_model(): 
  input_text = layers.Input(shape=(1,), dtype="string")
  embedding = ElmoEmbeddingLayer()(input_text)
  dense = layers.Dense(256, activation='relu')(embedding)    ❶
  pred = layers.Dense(1, activation='sigmoid')(dense)        ❷

  model = Model(inputs=[input_text], outputs=pred)

  model.compile(loss='binary_crossentropy', optimizer='adam',
                                 metrics=['accuracy'])       ❸
  model.summary()                                            ❹

  return model

# Build and fit
model = build_model()
model.fit(train_x,                                           ❺
          train_y,
          validation_data=(test_x, test_y),
          epochs=10,
          batch_size=4)

❶ 输出 256 维特征向量的新层

❷ 分类层

❸ 损失、度量和优化器的选择

❹ 显示用于检查的模型架构

❺ 将模型拟合 10 个 epochs

让我们更仔细地查看模型结构,该结构由前述代码片段中的model.summary()语句输出:

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 1)                 0         
_________________________________________________________________
elmo_embedding_layer_1 (Elmo (None, 1024)              4         
_________________________________________________________________
dense_1 (Dense)              (None, 256)               262400    
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 257       
=================================================================
Total params: 262,661
Trainable params: 262,661
Non-trainable params: 0

dense_1dense_2层是添加到第 3.4 节产生的预训练嵌入之上的新的全连接层。预训练嵌入是elmo_embedding_layer_1。请注意,打印的模型摘要显示它有四个可训练参数。这四个参数是前面子节中描述的内部双向 LSTM 状态的线性组合中的权重。如果您像我们这样使用 TensorFlow Hub 方法使用预训练的 ELMo 模型,则 ELMo 模型的其余部分不可训练。然而,可以使用模型库的另一个版本构建一个完全可训练的基于 TensorFlow 的 ELMo 模型。

当我们在假新闻数据集上执行前述代码时所达到的收敛结果如图 6.7 所示。我们看到,达到了超过 98%的准确率。

06_07

图 6.7 ELMO 模型在假新闻数据集上训练的收敛结果

6.3 通用语言模型微调(ULMFiT)

在 ELMo 等技术出现的时候,人们意识到 NLP 语言模型在各种方面与计算机视觉模型不同。将计算机视觉的相同技术应用于微调 NLP 语言模型会带来一些不利之处。例如,这个过程常常遭受到预训练知识的灾难性遗忘,以及在新数据上的过度拟合。这导致的后果是在训练期间失去了任何现存的预训练知识,以及在训练集之外的任何数据上的模型通用性差。名为通用语言模型微调(ULMFiT)的方法开发了一套技术,用于微调 NLP 语言模型以减轻这些不利之处。

更具体地说,该方法规定了在微调过程中对一般预训练语言模型的各层使用一些可变的学习率安排。它还为微调语言模型的任务特定层提供了一套技术,以实现更高效的迁移。尽管这些技术是作者在分类和基于 LSTM 的语言模型的背景下演示的,但这些技术意在更一般的情况下使用。

在本节中,我们会涉及到该方法引入的各种技术。但是,我们并没有在本节中实际实现它的代码。我们将延迟对 ULMFiT 的数值研究,直到第九章,在那里我们将探讨各种预训练模型适应新场景的技术。我们将使用由 ULMFiT 作者编写的 fast.ai 库,³来进行这项工作。

为了讨论接下来的程序,我们假定我们有一个在大型普通文本语料库(如维基百科)上预训练的语言模型。

6.3.1 目标任务语言模型微调

无论最初的预训练模型有多普通,最后的部署阶段可能会涉及来自不同分布的数据。这促使我们在新分布的小型数据集上对一般预训练模型进行微调,以适应新场景。ULMFiT 的作者发现,辨别性微调倾斜学习率的技术减轻了研究人员在此过程中遇到的过拟合和灾难性遗忘的双重问题。

辨别性微调规定,由于语言模型的不同层捕捉了不同的信息,因此它们应该以不同的速率进行微调。特别是,作者们经验性地发现,首先微调最后一层并注意其最佳学习率是有益的。一旦他们得到了这个基本速率,他们将这个最佳速率除以 2.6,这样就得到了以下层所建议的速率。通过以相同的因数进行逐步除法,可以得到越来越低的下层速率。

在适应语言模型时,我们希望模型在开始阶段快速收敛,然后进入较慢的细化阶段。作者发现,实现这一点的最佳方法是使用倾斜三角形学习率,该学习率线性增加,然后线性衰减。特别地,他们在迭代的初始 10%期间线性增加速率,直到最大值为 0.01。他们建议的速率时间表如图 6.8 所示,针对总迭代次数为 10,000 的情况。

06_08

图 6.8 建议的 ULMFiT 速率时间表,适用于总迭代次数为 10,000 的情况。速率线性增加了总迭代次数的 10%(即 1,000),最高值为 0.01,然后线性下降至 0。

6.3.2 目标任务分类器微调

除了在小型数据集上微调语言模型以表示新场景的数据分布的技术外,ULMFiT 还提供了两种用于优化任务特定层的技术:concat poolinggradual unfreezing

在 ULMFiT 开发时,将基于 LSTM 的语言模型的最终单元的隐藏状态传递给任务特定层是标准做法。作者建议将这些最终隐藏状态与所有时间步的最大池化和平均池化隐藏状态串联起来(尽可能多地适应内存)。在双向上下文中,他们分别为前向和后向语言模型执行此操作,并平均预测结果。他们称之为concat pooling的过程与 ELMo 描述的双向语言建模方法执行类似的功能。

为了减少微调时灾难性遗忘的风险,作者建议逐渐解冻和调整。这个过程从最后一层开始,该层包含最少的通用知识,并且在第一个 epoch 时是唯一解冻和精炼的层。在第二个 epoch 中,将解冻一个额外的层,并重复该过程。该过程持续到所有任务特定层都在该渐进解冻过程的最后迭代中解冻和微调。

作为提醒,这些技术将在第九章的代码中探讨,该章节将涵盖各种适应策略。

摘要

  • 与词级模型相反,字符级模型可以处理拼写错误和其他社交媒体特征,例如表情符号和小众俚语。

  • 双向语言建模对于构建具有意识到其局部上下文的词嵌入至关重要。

  • SIMOn 和 ELMo 都使用字符级 CNN 和双向 LSTM,后者有助于实现双向上下文建模。

  • 将预训练语言模型适应新场景可能会受益于对模型的不同层进行不同速率的微调,这应根据倾斜三角形时间表首先增加然后减少。

  • 将任务特定的层适应新情境可能会受益于逐渐解冻和微调不同的层,从最后一层开始解冻,逐渐解冻更多层,直到所有层都被精细调整。

  • ULMFiT 采用辨别微调,倾斜三角形学习率和渐进解冻来缓解微调语言模型时的过拟合和灾难性遗忘。

  1. N. Dhamani 等人,“使用深度网络和迁移学习解决虚假信息问题”,AI for Social Good ICML Workshop(2019 年)。

  2. github.com/allenai/bilm-tf

  3. nlp.fast.ai/ulmfit

第三部分:为基于转换器和适应策略的深度迁移学习

第七章和第八章涵盖了这一领域中可能最重要的子领域,即依赖于转换神经网络进行关键功能的深度迁移学习技术,比如 BERT 和 GPT。这个模型架构类别正在证明对最近的应用有最大的影响,部分原因在于相比之前的方法,它在并行计算架构上拥有更好的可扩展性。第九章和第十章深入探讨了各种适应策略,以使迁移学习过程更加高效。第十一章总结了本书,回顾了重要的主题,并简要讨论了新兴的研究课题和方向。

第七章:深度迁移学习与转换器和 GPT 的自然语言处理

本章涵盖:

  • 理解转换器神经网络架构的基础知识

  • 使用生成预训练转换器(GPT)生成文本

在本章和接下来的一章中,我们涵盖了一些依赖于最近流行的神经架构——转换器¹——进行关键功能的自然语言处理(NLP)的代表性深度迁移学习建模架构。这可以说是当今自然语言处理(NLP)中最重要的架构。具体来说,我们将研究诸如 GPT,² 双向编码器表示来自转换器(BERT)³ 和多语言 BERT(mBERT)⁴ 等建模框架。这些方法使用的神经网络比我们在前两章中看到的深度卷积和循环神经网络模型具有更多的参数。尽管体积更大,但这些框架因在并行计算架构上相对更有效地扩展而变得越来越受欢迎。这使得在实践中可以开发出更大更复杂的模型。为了使内容更易理解,我们将这些模型的覆盖范围分为两个章节/部分:本章我们涵盖了转换器和 GPT 神经网络架构,而在下一章中,我们将专注于 BERT 和 mBERT。

在转换器到来之前,主导的 NLP 模型依赖于循环和卷积组件,就像我们在前两章中看到的一样。此外,最好的序列建模转导问题,例如机器翻译,依赖于具有注意机制的编码器-解码器架构,以检测输入的哪些部分影响输出的每个部分。转换器的目标是完全用注意力替换循环和卷积组件。

本章和接下来的章节的目标是为您提供对这一重要模型类的工作理解,并帮助您建立起关于其一些有益属性来自何处的良好认识。我们引入了一个重要的库——名为transformers——使得在 NLP 中分析、训练和应用这些类型的模型特别易于使用。此外,我们使用tensor2tensor TensorFlow 包来帮助可视化注意力功能。每个基于转换器的模型架构——GPT、BERT 和 mBERT——的介绍都后跟应用它们于相关任务的代表性代码。

GPT,由 OpenAI 开发,⁵ 是一个基于转换器的模型,它以因果建模目标训练:预测序列中的下一个单词。它也特别适用于文本生成。我们展示了如何使用预训练的 GPT 权重来实现这一目的,使用 transformers 库。

BERT 是一个基于 transformer 的模型,在第三章我们简要介绍过它。它是用掩码建模目标进行训练的:填补空白。此外,它还通过下一个句子预测任务进行了训练:确定给定句子是否是目标句子后的一个合理的后续句子。虽然不适用于文本生成,但这个模型在其他一般语言任务上表现良好,如分类和问答。我们已经比较详细地探讨了分类问题,因此我们将使用问答任务来更详细地探索这个模型架构,而不像第三章中那样简略。

mBERT,即多语言 BERT,实际上是同时在 100 多种语言上预训练的 BERT。自然地,这个模型特别适用于跨语言迁移学习。我们将展示多语言预训练检查点如何促进为甚至在最初的多语言训练语料库中未包含的语言创建 BERT 嵌入。BERT 和 mBERT 都是由 Google 创建的。

我们在本章开始时回顾了基本的架构组件,并通过 tensor2tensor 软件包详细展示了它们。接着,我们介绍了 GPT 架构的概述部分,以文本生成作为预训练权重的代表应用。第八章的第一部分涵盖了 BERT,我们将其应用于非常重要的问答应用作为一个独立部分的代表示例。第八章以一项实验结束,展示了从 mBERT 预训练权重转移到新语言的 BERT 嵌入的知识传递。这种新语言最初并不包含在用于生成预训练 mBERT 权重的多语言语料库中。在这种情况下,我们以加纳语 Twi 作为示例语言。这个例子也提供了进一步探索在新语料库上微调预训练 BERT 权重的机会。请注意,Twi 是低资源语言的一个示例——高质量的训练数据很少,如果有的话。

7.1 transformer

在本节中,我们更仔细地观察了本章所涵盖的神经模型系列背后的基本 transformer 架构。这个架构是在 Google⁶ 开发的,并受到了这样一个观察的启发,即到目前为止表现最佳的翻译模型使用了卷积和循环组件,并与一个叫做注意力的机制结合使用。

更具体地,这些模型采用编码器-解码器架构,其中编码器将输入文本转换为一些中间数值向量表示,通常称为上下文向量,并且解码器将该向量转换为输出文本。通过对输出和输入之间的依赖关系进行建模,注意力允许这些模型实现更好的性能。通常情况下,注意力被与循环组件耦合在一起。因为这些组件本质上是顺序的--给定任何位置t的内部隐藏状态都取决于前一位置t-1的隐藏状态--对于处理长的输入序列的并行处理不是一个选择。另一方面,跨这样的输入序列进行并行化处理很快就会遇到 GPU 内存限制。

转换器舍弃了循环并用注意力替换所有功能。更具体地说,它使用了一种称为自我注意的注意味道。自我注意实质上是之前描述过但应用于相同序列的输入和输出的注意。这使得它能够学习到序列的每个部分与同一序列的每个其他部分之间的依赖关系。图 7.3 将重新访问并详细说明这个想法,所以如果您还无法完全可视化它,请不要担心。与前面提到的循环模型相比,这些模型具有更好的并行性。展望未来,在 7.1.2 节中,我们将使用例如“他不想在手机上谈论细胞,因为他觉得这很无聊”的例句来研究基础设施的各个方面是如何工作的。

现在我们了解了这种架构背后的基本动机,让我们看一下各种构建块的简化鸟瞰图表示,如图 7.1 所示。

07_01

图 7.1:转换器架构的高级表示,显示堆叠的编码器、解码器、输入/输出嵌入和位置编码

我们从图中可以看到,在架构的编码或左侧上堆叠了相同的编码器。堆叠编码器的数量是一个可调的超参数,原始论文中使用了六个。同样,在解码或右侧上,堆叠了六个相同的解码器。我们还看到,使用所选的嵌入算法将输入和输出转换为向量。这可以是诸如 word2vec 的词嵌入算法,甚至可以是应用于使用 one-hot 编码的字符向量的类似于我们在前一章中遇到的那些卷积神经网络。此外,我们使用位置编码来编码输入和输出的顺序性。这使我们可以舍弃循环组件,同时保持顺序感知性。

每个编码器都可以粗略地分解为一个自注意层,紧随其后是一个前馈神经网络,如图 7.2 所示。

07_02

图 7.2 编码器和解码器的简化分解,包括自注意力、编码器-解码器注意力和前馈神经网络。

如图所示,每个解码器都可以类似地分解,增加了一个在自注意力层和前馈神经网络之间的编码器-解码器注意力层。需要注意的是,在解码器的自注意力中,在计算该标记的注意力时,“未来标记”会被“屏蔽”--我们将在更合适的时间回到这个问题。而自注意力学习其输入序列的每个部分与同一序列的每个其他部分之间的依赖关系,编码器-解码器注意力学习编码器和解码器输入之间的类似依赖关系。这个过程类似于注意力最初被用于序列到序列的循环翻译模型的方式。

图 7.2 中的自注意力层可以进一步细化为多头注意力 -- 自注意力的多维模拟,可以带来更好的性能。我们将在接下来详细分析自注意力,并借此来介绍多头注意力。bertviz包⁷用于可视化目的,以提供进一步的见解。后来我们关闭这一章,通过 transformers 库加载一个代表性的 transformer 翻译模型,并使用它快速将几个英文句子翻译成低资源的加纳语 Twi。

7.1.1 对 transformers 库和注意力可视化的介绍

在我们详细讨论多头注意力各组件是如何工作之前,让我们以例句“他不想谈论手机上的细胞,因为他觉得这很无聊”进行可视化。这个练习也让我们介绍了 Hugging Face 的 transformers Python 库。进行这个过程的第一步是使用以下命令获取必要的依赖项:

!pip install tensor2tensor
!git clone https:/ /github.com/jessevig/bertviz.git

注意:回想一下前面的章节,感叹号(!)只在 Jupyter 环境中执行时需要,比如我们推荐的 Kaggle 环境中。在通过终端执行时,它应该被去掉。

tensor2tensor 包含了 transformers 架构的原始作者实现,以及一些可视化工具。bertviz 库是这些可视化工具对 transformers 库中大量模型的扩展。注意,要渲染可视化内容需要激活 JavaScript(我们会在相关 Kaggle 笔记本中告诉你如何做)。

transformers 库可以通过以下方式安装:

!pip install transformers

注意,它已经安装在 Kaggle 上的新笔记本中。

为了我们的可视化目的,我们看看了 BERT 编码器的自注意力。这可以说是基于 transformer 架构最流行的一种变体,类似于原始架构图 7.1 中编码器-解码器架构中的编码器。我们将在第 8.1 节的图 8.1 中明确可视化 BERT 体系结构。现在,您需要注意的是 BERT 编码器与 transformer 的编码器完全相同。

对于您想在 transformers 库中加载的任何预训练模型,需要使用以下命令加载标记器以及模型:

from transformers import BertTokenizer, BertModel                                         ❶
model = BertModel.from_pretrained('bert-base-uncased', output_attentions=True)            ❷
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True)        ❸

❶ transformers BERT 标记器和模型

❷ 加载不区分大小写的 BERT 模型,确保输出注意力

❸ 加载不区分大小写的 BERT 标记器

请注意,我们在这里使用的不区分大小写的 BERT 检查点与我们在第三章(清单 3.7)中使用的相同,即当我们通过 TensorFlow Hub 首次遇到 BERT 模型时。

您可以对我们正在运行的示例句子进行标记化,将每个令牌编码为其在词汇表中的索引,并使用以下代码显示结果:

sentence = "He didnt want to talk about cells on the cell phone because he considered it boring"
inputs = tokenizer.encode(sentence, return_tensors='tf', add_special_tokens=True)                            ❶
print(inputs)

❶ 将 return_tensors 更改为“pt”将返回 PyTorch 张量。

这产生以下输出:

tf.Tensor(
[[  101  2002  2134  2102  2215  2000  2831  2055  4442  2006  1996  3526
   3042  2138  2002  2641  2009 11771   102]], shape=(1, 19), dtype=int32)

我们可以通过在inputs变量上执行以下代码轻松地返回一个 PyTorch 张量,只需设置return_tensors='pt'。要查看这些索引对应的标记,可以执行以下代码:

tokens = tokenizer.convert_ids_to_tokens(list(inputs[0]))     ❶
print(tokens)

❶ 从输入列表的列表中提取批次索引 0 的示例

这产生以下输出:

['[CLS]', 'he', 'didn', '##t', 'want', 'to', 'talk', 'about', 'cells', 'on', 'the', 'cell', 'phone', 'because', 'he', 'considered', 'it', 'boring', '[SEP]']

我们立即注意到,通过编码inputs变量时通过add_special_tokens参数请求的“特殊令牌”指的是此案例中的'[CLS]''[SEP]'令牌。前者表示句子/序列的开头,而后者表示多个序列的分隔点或序列的结束(如在此案例中)。请注意,这些是 BERT 相关的,您应该检查您尝试的每种新架构的文档以查看它使用的特殊令牌。我们从这次标记化练习中注意到的另一件事是分词是次词—请注意didn如何被分成didn##t,即使没有撇号(’),我们刻意省略掉了。

让我们继续通过定义以下函数来可视化我们加载的 BERT 模型的自注意力层:

from bertviz.bertviz import head_view                           ❶

def show_head_view(model, tokenizer, sentence):                 ❷
    input_ids = tokenizer.encode(sentence, return_tensors='pt', add_special_tokens=True)                                   ❸
    attention = model(input_ids)[-1]                            ❹
    tokens = tokenizer.convert_ids_to_tokens(list(input_ids[0]))    
    head_view(attention, tokens)                                ❺

show_head_view(model, tokenizer, sentence)                      ❻

❶ bertviz 注意力头可视化方法

❷ 功能用于显示多头注意力

❸ 一定要在 bertviz 中使用 PyTorch。

❹ 获取注意力层

❺ 调用内部 bertviz 方法来显示自注意力

❻ 调用我们的函数来渲染可视化

图 7.3 显示了我们示例句子的最终 BERT 层的自注意力可视化的结果。您应该使用可视化并滚动浏览各层各个词的可视化。注意,并非所有注意力可视化都像这个示例那样容易解释,这可能需要一些练习来建立直觉。

07_03

图 7.3 我们示例句子的预训练非大小写 BERT 模型的最终编码层中的自注意可视化。它显示“细胞”与“它”和“无聊”相关联。请注意,这是一个多头视图,每个单列中的阴影代表一个头。多头注意力在第 7.1.2 节中详细讨论。

就是这样!现在我们对自注意力的作用有了一定的了解,通过在图 7.3 中进行可视化,让我们进入它的数学细节。我们首先从下一小节中的自注意力开始,然后在之后将我们的知识扩展到完整的多头上下文中。

7.1.2 自注意力

再次考虑例句,“他不想谈论手机上的细胞,因为他认为这很无聊。”假设我们想弄清楚形容词“boring”描述的是哪个名词。能够回答这样的问题是机器需要具备的理解上下文的重要能力。我们知道它指的是“它”,而“它”指的是“细胞”,很自然。这在我们在图 7.3 中的可视化中得到了证实。机器需要被教会这种上下文意识。自注意力是在变压器中实现这一点的方法。当输入中的每个标记被处理时,自注意力会查看所有其他标记以检测可能的依赖关系。回想一下,在上一章中我们通过双向 LSTM 实现了相同的功能。

那么自注意力是如何实际工作以实现这一目标的呢?我们在图 7.4 中可视化了这个关键思想。在图中,我们正在计算单词“boring”的自注意力权重。在进一步详细说明之前,请注意一旦获取了各个单词的各种查询、键和值向量,它们就可以被独立处理。

07_04

图 7.4 我们示例句子中单词“boring”的自注意力权重计算的可视化。请注意,一旦创建了键、值和查询向量,可以独立地计算这些单词的不同权重的计算。这是变压器在循环模型之上增加的可并行性的根源。注意系数是图 7.3 中多头注意力中任何给定列的阴影强度的可视化。

每个单词都与一个查询向量(q)、一个向量(k)和一个向量(v)相关联。这些向量是通过将输入嵌入向量与在训练过程中学习到的三个矩阵相乘得到的。这些矩阵在所有输入标记中都是固定的。如图所示,当前单词 "boring" 的查询向量与每个单词的键向量进行点积。结果被一个固定常数——键和值向量维度的平方根——进行缩放,并输入到一个 softmax 函数中。输出向量产生的注意力系数表示当前标记 "boring" 与序列中每个其他标记之间关系的强度。请注意,该向量的条目表示我们在图 7.3 中可视化的多头注意力中任何给定单列中阴影的强度。接下来,为了方便起见,我们重复了图 7.3,这样您就可以检查不同行之间阴影变化的可变性。

07_04_07_03_duplicated

图 7.3(重复)预训练的不分大小写 BERT 模型在我们示例句子的最终编码层中的自注意可视化。它显示了 "cells" 与 "it" 和 "boring" 相关联。请注意,这是一个多头视图,每个单列中的阴影代表一个头。

现在我们有了足够的条件来理解为什么变压器比循环模型更具并行性。回想一下我们的介绍,不同单词的自注意力权重的计算可以在创建键、值和查询向量后独立进行。这意味着对于长输入序列,可以并行化这些计算。回想一下,循环模型本质上是顺序的——任何给定位置 t 处的内部隐藏状态取决于前一个位置 t-1 处的隐藏状态。这意味着无法在循环模型中并行处理长输入序列,因为步骤必须依次执行。另一方面,对于这样的输入序列,跨序列的并行化很快就会遇到 GPU 内存限制。变压器模型比循环模型的另一个优势是由注意力可视化提供的增加的可解释性,比如图 7.3 中的可视化。

请注意,可以独立地计算序列中每个标记的权重,尽管通过键和值向量存在一些计算之间的依赖关系。这意味着我们可以使用矩阵对整体计算进行向量化,如图 7.5 所示。在该方程中,矩阵 Q、K 和 V 简单地是由查询、键和值向量堆叠在一起形成的矩阵。

07_05

图 7.5 使用矩阵对整个输入序列进行向量化的自注意力计算

到底多头注意力有什么作用?既然我们已经介绍了自注意力,那么现在是一个很好的时机来解决这个问题。从单列的角度,我们已经将多头注意力隐式地作为自注意力的一般化呈现,如图 7.3 中的阴影部分,变为了多列。让我们思考一下,当我们寻找与“无聊”相关的名词时,我们具体做了什么。从技术上讲,我们是在寻找名词-形容词的关系。假设我们有一个跟踪这类关系的自注意力机制。如果我们还需要跟踪主-谓关系呢?还有其他可能的关系呢?多头注意力通过提供多个表示维度来解决这个问题,而不仅仅是一个。

7.1.3 残差连接、编码器-解码器注意力和位置编码

Transformer 是一种复杂的架构,具有许多特性,我们将不像自注意力那样详细介绍。精通这些细节对于您开始将 Transformer 应用于自己的问题并不是至关重要的。因此,我们在这里只是简要总结它们,并鼓励您随着获得更多经验和直觉的时间不断深入学习原始资源材料。

作为第一个这样的特性,我们注意到图 7.2 中简化的编码器表示中没有显示编码器中每个自注意层和接下来的规范化层之间的附加残差连接。这在图 7.6 中有所说明。

07_06

图 7.6 更详细和准确地拆分每个 Transformer 编码器,现在包括残差连接和规范化层

如图所示,每个前馈层在其后都有一个残差连接和一个规范化层。类似的说明也适用于解码器。这些残差连接使得梯度能够跳过层内的非线性激活函数,缓解了梯度消失和/或梯度爆炸的问题。简单地说,规范化确保所有层的输入特征的尺度大致相同。

在解码器端,回顾图 7.2 中编码器-解码器注意力层的存在,这一点我们还没有讨论到。接下来,我们复制图 7.2 并突出显示该层以方便您查看。

07_06_007_02_duplicated

图 7.2(复制,突出显示编码器-解码器注意力)将编码器和解码器简化为自注意、编码器-解码器注意力和前馈神经网络的分解形式

它的工作方式类似于所描述的自我关注层。重要的区别在于,表示键和值的每个解码器的输入向量来自编码器堆栈的顶部,而查询向量来自直接位于其下面的层。如果您再次查看图 7.4,并记住这个更新后的信息,您应该会发现这种变化的效果是计算每个输出标记和每个输入标记之间的注意力,而不是像自我关注层的情况那样在输入序列的所有标记之间计算。接下来我们将复制图 7.4——稍作调整以适用于编码器-解码器注意力——让您自己看一看。

07_06_007_04_duplicated

图 7.4(重复,稍作调整以计算编码器-解码器注意力)显示了我们的例句中单词“boring”和位置 n 处输出之间的编码器-解码器注意力权重计算的可视化。注意,一旦创建了键、值和查询向量,就可以独立地计算不同单词的这些权重。这是转换器相对于递归模型具有更高并行性的根源。

从图 7.1 中回顾一下,在编码器和解码器两侧都存在位置编码,我们现在对其进行解释。由于我们处理的是序列,因此对于每个序列中的每个标记建模和保留相对位置非常重要。到目前为止,我们对转换器操作的描述没有涉及“位置编码”,并且对输入的标记按顺序使用的顺序未定义。位置编码通过将等大小的向量添加到每个标记输入嵌入中来解决此问题,这些向量是该标记在序列中位置的特殊函数。作者使用了位置相关的正弦和余弦函数来生成这些位置嵌入。

这就是我们对转换器架构的阐述。为了让事情具体化,我们在本节中通过使用预训练的编码器-解码器模型将几个英语句子翻译为低资源语言来进行结论。

7.1.4 预训练编码器-解码器在翻译中的应用

本小节的目标是让您了解转换器库中提供的大量翻译模型。赫尔辛基大学语言技术研究组⁸提供了 1000 多个预训练模型。在撰写本文时,这些模型是许多低资源语言仅有的可用开源模型。在这里,我们以流行的加纳语 Twi 为例。它是在 JW300 语料库⁹上进行训练的,该语料库包含许多低资源语言的唯一现有平行翻译数据集。

不幸的是,JW300 是极具偏见的数据,是由耶和华见证人组织翻译的宗教文本。然而,我们的调查发现,这些模型作为进一步迁移学习和精炼的初始基线是相当不错的。我们在这里没有明确地在更好的数据上对基线模型进行改进,原因是数据收集的挑战和缺乏现有的合适数据集。然而,我们希望与下一章的倒数第二节一起来看——在那里我们将在单语特威语数据上对多语言 BERT 模型进行微调——您将获得一套强大的工具,用于进一步的跨语言迁移学习研究。

不多说了,让我们使用以下代码加载预训练的英语到特威语的翻译模型和分词器:

from transformers import MarianMTModel, MarianTokenizer

model = MarianMTModel.from_pretrained("Helsinki-NLP/opus-mt-en-tw")
tokenizer = MarianTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-tw")

MarianMTModel 类是从 C++ 库 MarianNMT 移植而来的编码器-解码器变压器架构。¹⁰ 请注意,如果研究小组提供了相应的代码,你可以通过简单地更改语言代码 entw 来更改源语言和目标语言。例如,加载一个法语到英语的模型将会改变输入配置字符串为 Helsinki-NLP/opus-mt-fr-en

如果我们在网上与加纳的朋友聊天,并想知道如何用介绍的方式写“我的名字是保罗”,我们可以使用以下代码计算并显示翻译:

text = "My name is Paul"                                      ❶
inputs = tokenizer.encode(text, return_tensors="pt")          ❷
outputs = model.generate(inputs)                              ❸
decoded_output = [tokenizer.convert_ids_to_tokens(int(outputs[0][i])) for i in range(len(outputs[0]))]                               ❹
print("Translation:")                                         ❺
print(decoded_output)

❶ 输入要翻译的英语句子

❷ 将输入编码为标记 ID

❸ 生成输出的标记 ID

❹ 将输出的标记 ID 解码为实际输出标记

❺ 显示翻译

运行代码后得到的输出结果如下所示:

Translation:
['<pad>', '▁Me', '▁din', '▁de', '▁Paul']

我们立即注意到的第一件事是输出中存在一个我们以前没有见过的特殊标记 <pad>,以及每个单词前面的下划线。这与第 7.1.1 节中 BERT 分词器产生的输出不同。技术原因是 BERT 使用了一个称为 WordPiece 的分词器,而我们这里的编码器-解码器模型使用了 SentencePiece。虽然我们在这里没有详细讨论这些分词器类型之间的差异,但我们利用这个机会再次警告您,务必查阅有关您尝试的任何新分词器的文档。

翻译“Me din de Paul”恰好是完全正确的。太好了!这并不太难,是吗?然而,对于输入句子“How are things?”的重复练习却得到了翻译“Ɔkwan bɛn so na nneɛma te saa?”,它直译成“事情是什么样的?”我们可以看到,虽然这个翻译的语义看起来很接近,但翻译是错误的。然而,语义相似性表明,该模型是一个很好的基线,如果有好的平行英文-特威语数据可用,可以通过迁移学习进一步改进。此外,将输入句子改写为“How are you?”则从这个模型得到了正确的翻译“Wo ho te dɛn?”。总的来说,这个结果是非常令人鼓舞的,我们希望一些读者受到启发,致力于将这些基线模型扩展到一些以前未解决的低资源语言的优秀开源转换器模型。

接下来,我们来看一下生成式预训练转换器(GPT),这是一种基于转换器的模型,用于文本生成,在自然语言处理(NLP)社区中变得非常有名。

7.2 生成式预训练转换器

生成式预训练转换器(Generative Pretrained Transformer)[¹¹](GPT)是由 OpenAI 开发的,并且是最早将转换器架构应用于本书讨论的半监督学习场景的模型之一。通过这个,我们指的当然是在大量文本数据上无监督(或自监督)预训练语言理解模型,然后在最终感兴趣的目标数据上进行监督微调。作者发现在四种类型的语言理解任务上的性能得到了显著提升。这些任务包括自然语言推理、问答、语义相似度和文本分类。值得注意的是,在通用语言理解评估(GLUE)基准上的表现,该基准包括这些以及其他困难和多样化的任务,提高了超过 5 个百分点。

GPT 模型已经经历了几次迭代——GPT、GPT-2,以及最近的 GPT-3。事实上,在撰写本文时,GPT-3 恰好是已知的最大的预训练语言模型之一,具有 1750 亿个参数。它的前身 GPT-2 具有 15 亿个参数,在其发布的前一年也被认为是最大的。在 2020 年 6 月发布 GPT-3 之前,最大的模型是微软的图灵-NLG,该模型具有 170 亿个参数,并于 2020 年 2 月发布。在某些指标上的进展速度之快令人难以置信,并且这些记录很可能很快就会过时。事实上,当最初披露 GPT-2 时,作者认为不完全开源技术是正确的做法,考虑到可能会被恶意行为者滥用的潜力。

虽然在最初发布时,GPT 成为了大多数上述任务的最先进技术,但它通常更受青睐作为一种文本生成模型。与 BERT 及其衍生模型不同,后者已经主导了大多数其他任务,GPT 是以因果建模目标(CLM)进行训练的,其中预测下一个标记,而不是 BERT 的掩码语言建模(MLM)填空类型的预测目标,我们将在下一章更详细地介绍。

在下一小节中,我们简要描述了 GPT 架构的关键方面。接着介绍了 transformers 库中用于最常见任务的预训练模型的最小执行的pipelines API 概念。我们将此概念应用于 GPT 在其擅长的任务——文本生成方面。与前一节关于编码器-解码器变压器和翻译的内容一样,我们在此处不会明确地在更特定的目标数据上对预训练的 GPT 模型进行改进。然而,结合下一章的最后一节——我们在单语 Twi 数据上对多语言 BERT 模型进行微调——您将获得一套用于进一步文本生成迁移学习研究的强大工具。

7.2.1 架构概述

您可能还记得 7.1.1 节中我们可视化 BERT 自注意力的情况,BERT 本质上是原始编码器-解码器变压器架构的一组叠加编码器。从这个意义上讲,GPT 本质上是它的反义词,它将解码器堆叠起来。从图 7.2 中可以看出,除了编码器-解码器注意力之外,变压器解码器的另一个显著特征是其自注意力层是“掩码的”,即在计算给定标记的注意力时,“未来标记”被“掩码”了。我们复制图 7.2 供您参考,突出显示此掩码层。

07_06_07_02_duplicated2

图 7.2(重复,突出显示掩码层)将编码器和解码器简化为自注意力、编码器-解码器注意力和前馈神经网络的分解形式

在我们在图 7.3 中经历的注意力计算中,这只意味着在计算中只包括“他不想谈论细胞”中的标记,并忽略其余的标记。我们稍后复制图 7.3,稍作修改,以便您清楚地看到未来标记被掩盖的情况。

07_06_07_03_duplicated2

图 7.3(再次重复,为掩码自注意力修改)我们示例句子的掩码自注意力可视化,显示了因果关系中未来标记的被掩盖情况。

这为系统引入了因果关系的感觉,并适用于文本生成,或预测下一个标记。由于没有编码器,编码器-解码器注意力也被删除了。考虑到这些因素,我们在图 7.7 中展示了 GPT 的架构。

请注意图 7.7 中,同样的输出可以用于一些其他任务的文本预测/生成和分类。事实上,作者设计了一个输入转换方案,使得多个任务可以通过相同的架构处理,而不需要任何架构更改。例如,考虑到 文本蕴涵 任务,它大致对应于确定一个 前提 陈述是否暗示另一个 假设 陈述。输入转换会将前提和假设陈述连接起来,用一个特殊的分隔符标记分隔,然后将结果的单一连续字符串馈送到相同的未修改架构,以分类是否存在蕴涵。另一方面,考虑到重要的问答应用。在这里,给定一些上下文文档、一个问题和一组可能的答案,任务是确定哪个答案是问题的最佳潜在答案。在这里,输入转换是将上下文、问题和每个可能的答案连接在一起,然后通过相同的模型将每个结果的连续字符串传递,并对相应的输出执行 softmax,以确定最佳答案。类似的输入转换也适用于句子相似性任务。

07_07

图 7.7 GPT 架构的高级表示,显示了堆叠的解码器、输入嵌入和位置编码。顶部的输出可以用于文本预测/生成和分类。

简要介绍了 GPT 的架构之后,让我们使用它的一个预训练版本进行一些有趣的编码实验。我们首先使用它生成一些开放式文本,给定一个提示。然后,在下一小节中,我们还将使用由微软构建的 GPT 的修改版本——DialoGPT¹²,来执行与聊天机器人的多轮对话。

7.2.2 转换器流水线介绍及应用于文本生成

在本小节中,我们将首先使用 GPT 生成一些开放式文本。我们还将利用这个机会介绍管道——一个 API,用于推断中暴露预训练模型在 transformers 库中的 API,甚至比我们在第 7.1.4 节中进行的翻译更简单。transformers 作者的声明目标是,这个 API 可以摒弃一些常用任务的复杂代码,包括命名实体识别、遮蔽语言建模、情感分析和问答。适合我们在本小节中的目的,文本生成也是一种选择。

让我们通过以下两行代码初始化转换器管道到 GPT-2 模型:

from transformers import pipeline
gpt = pipeline('text-generation',model='gpt2')

提醒一下,GPT 最初的形式非常适合于开放式文本生成,比如创造性地写出一些文本段落来补充之前的文本。让我们看看当模型以“在彩虹的那边……”为提示,生成最多 100 个标记时,模型生成了什么,通过以下命令:

gpt("Somewhere over the rainbow", max_length=100)

这生成了以下文本:

[{'generated_text': "Somewhere over the rainbow people live! I wonder how they get to know each other... They just have a wonderful community out there - but when they see each other as two of the best in school they never even realize them, just love, family, friends, and friends. I'm really proud of their talent and dedication to life. I've seen a lot of people that were raised by their mother and grandma in the Midwest and didn't understand there was such an opportunity and I truly cannot"}]

即使消息有些不连贯,这似乎在语义上非常正确。您可以想象一位创意作家使用它来生成想法以克服写作困境!现在,让我们看看是否可以用一些不太“创意”的东西来启动模型,一些更技术性的东西,以查看它的表现。让我们通过以下代码将模型启动文本设置为“迁移学习是一门研究领域”:

gpt("Transfer learning is a field of study", max_length=100)

这产生了以下输出:

[{'generated_text': "Transfer learning is a field of study that has been around for centuries, and one that requires a thorough grounding in mathematics in order to understand the complexities of these systems. If you go to the library for your high school physics course, you know you're on the right track. The only problem with this position is that people don't ask questions. The only thing they really do ask is: how do we figure out how to apply these processes to the rest of physics and other sciences?\n\nIn"}]

再次,我们可以看到,从语义连贯性、语法结构、拼写、标点等方面来看,这段文字非常好——实际上,甚至有点诡异。然而,随着它的继续,它变得可能事实上不正确。我们都可以同意,要真正理解迁移学习,需要对数学有扎实的基础,甚至可以说它已经存在了几个世纪——通过我们,人类!然而,它不是物理学的一个领域,即使在需要掌握它的技能方面可能有些类似。我们可以看到,模型的输出允许它说话的时间越长,就越不可信。

请务必进行更多实验,以了解模型的优缺点。例如,您可以尝试使用我们的示例句子提示模型,“他不想在手机上谈论细胞,因为他认为这很无聊。”我们发现这在创意写作空间和技术写作空间中都是一个可信的应用,max_length设置为较小的数值。对许多作者来说,它已经是一个可信的辅助工具。在撰写本文时,我们只能想象 GPT-3 能够做到什么。未来确实非常令人兴奋。

玩弄文本生成后,让我们看看是否可以以某种方式使用它来创建聊天机器人。

7.2.3 聊天机器人的应用

直觉上应该能够无需对此应用进行重大修改即可采用 GPT。幸运的是,微软的人员已经通过模型 DialoGPT 完成了这一点,该模型最近也被包含在 transformers 库中。它的架构与 GPT 相同,只是增加了特殊标记,以指示对话中参与者的回合结束。在看到这样的标记后,我们可以将参与者的新贡献添加到启动上下文文本中,并通过直接应用 GPT 来生成聊天机器人的响应,迭代重复这个过程。自然地,预训练的 GPT 模型在会话文本上进行了微调,以确保响应是适当的。作者们使用 Reddit 主题进行了微调。

让我们继续构建一个聊天机器人吧!在这种情况下,我们不会使用管道,因为在撰写本文时,该模型尚未通过该 API 公开。这使我们能够对比调用这些模型进行推理的不同方法,这对你来说是一个有用的练习。

首先要做的事情是通过以下命令加载预训练模型和分词器:

from transformers import GPT2LMHeadModel, GPT2Tokenizer      ❶
import torch                                                 ❷

tokenizer = GPT2Tokenizer.from_pretrained("microsoft/DialoGPT-medium")
model = GPT2LMHeadModel.from_pretrained("microsoft/DialoGPT-medium")

❶ 请注意,DialoGPT 模型使用 GPT-2 类。

❷ 我们在这里使用 Torch 而不是 TensorFlow,因为 transformers 文档中默认选择的是 Torch 平台。

此处值得强调几点。首先,请注意我们使用的是 GPT-2 模型类,这与我们先前讨论的 DialoGPT 作为该架构的直接应用是一致的。另外,请注意我们可以与这些 GPT 特定的模型类交换使用AutoModelWithLMHeadAutoTokenizer类。这些实用程序类会检测用于加载指定模型的最佳类别,例如,在这种情况下,它们将检测到最佳要使用的类别为GPT2LMHeadModelGPT2Tokenizer。浏览 transformers 库文档时,你可能会遇到这些实用程序类,了解它们的存在对你的代码更一般化是有好处的。最后请注意,这里使用的是 GPT 的“LMHead”版本。这意味着从普通 GPT 得到的输出将通过一个线性层和一个归一化层,然后转换成一个维度等于词汇表大小的概率向量。最大值对应于模型正确训练的情况下下一个最有可能的令牌。

与我们加载的预训练 DialoGPT 模型进行对话的代码如列表 7.1 所示。我们首先指定最多五个回应的最大数量。然后,我们编码用户在每个轮次的对话,将对话添加到聊天历史记录中,并将其传递给加载的预训练 DialoGPT 模型,以生成下一个响应。

列表 7.1 与预训练 DialoGPT 模型进行对话,最多五个聊天机器人响应

conversation_length = 5                                                                                           ❶
for step in range(conversation_length):
    new_user_inputs_ids = tokenizer.encode(input("User: ") + tokenizer.eos_token, return_tensors='pt')            ❷
    bot_input_ids = torch.cat([chat_history_ids, new_user_inputs_ids], dim=1)                                     ❸
     if step > 0 else new_user_inputs_ids   
    chat_history_ids = model.generate(bot_input_ids, max_length=1000, pad_token_id=tokenizer.eos_token_id)        ❹
    print("DialoGPT: {}".format(tokenizer.decode(chat_history_ids[:, bot_input_ids.shape[-1]:][0], skip_special_tokens=True)))               ❺

❶ 五行聊天

❷ 编码新用户输入,添加一个句子结束标记,并返回张量

❸ 将新输入添加到聊天历史记录中

❹ 使用聊天历史记录作为上下文生成最多 max_length 令牌的响应

❺ 显示响应

一个人可能整天都和这个机器人玩耍!我们很开心地向它提问各种问题,并以各种方式提示它。我们在这里包括了其中一个交流以供您娱乐:

User: Hi there.
DialogGPT: Hi there.
User: How are you today?
DialogGPT: I'm good, how are you?
User: Good! How much money do you have?
DialogGPT: I have about 100k.
User: What will you spend it on?
DialogGPT: I'm not sure, I'm not sure what I want to spend it on.
User: Make a decision, life is short.
DialogGPT: I'm going to go with a lot of things.

接下来这短暂对话的另一端很可能是一个人,不是吗?那是否意味着它通过了图灵测试?并非如此,下面的交流说明了这一点:

User: Hi robot.
DialogGPT: Hello, human.
User: Huh?
DialogGPT: I'm a bot.
User: OK, what is your name?
DialogGPT: Robot. I'm a robot.
User: All right then.
DialogGPT: Robot. I'm a robot.
User: Say something else.
DialogGPT: Robot. I'm a robot.

当你增加允许的对话轮次数量时,你会发现机器人会陷入重复的与话题无关的回复中。这类似于 GPT 开放式文本生成随着生成文本长度的增加变得更加荒谬。改善这一点的一个简单方法是保持固定的局部上下文大小,其中模型只受到该上下文内的对话历史的提示。当然,这意味着对话不总是考虑整个对话的上下文——这是必须对任何给定应用进行实验探索的一个权衡。

想象一下 GPT-3 在这些问题上的表现会有多好,是不是令人兴奋?在本书的最后一章中,我们将简要讨论更多关于 GPT-3 的细节,并介绍一个最近推出的更小但同样值得关注的开源替代品:EleutherAI 的 GPT-Neo。它已经可以在 transformers 库中使用,并且可以通过将 model 字符串设置为 EleutherAI 提供的模型名称之一来直接使用。[¹³]我们还附上了一个伴随笔记本,在其中展示了它在本章练习中的应用。经过检查,你应该会发现它的性能更好,但自然也会有显著更高的成本(最大模型的权重超过 10 GB!)。

在下一章中,我们将讨论变压器家族中可能最重要的成员——BERT。

总结

  • 变压器架构使用自注意力机制来构建文本的双向上下文。这使得它成为了近期在自然语言处理中占主导地位的语言模型。

  • 变压器允许对序列中的令牌进行独立处理。这比处理顺序的双向 LSTM 实现了更大的并行性。

  • 变压器是翻译应用的不错选择。

  • 在训练过程中,生成预训练变压器使用因果建模目标。这使得它成为文本生成的首选模型,例如聊天机器人应用。

  1. A. Vaswani 等人,“Attention Is All You Need”,NeurIPS(2017)。

  2. A. Radford 等人,“通过生成预训练来改善语言理解”,arXiv(2018)。

  3. M. E. Peters 等人,“BERT:用于语言理解的深度双向变压器的预训练”,NAACL-HLT(2019)。

  4. github.com/google-research/bert/blob/master/multilingual.md

  5. A. Radford 等人,“通过生成预训练来改善语言理解”,arXiv(2018)。

  6. A. Vaswani 等人,“Attention Is All You Need”,NeurIPS(2017)。

  7. github.com/jessevig/bertviz

  8. huggingface.co/Helsinki-NLP

  9. opus.nlpl.eu/JW300.php

  10. marian-nmt.github.io/

  11. A. Radford 等人,“通过生成式预训练提高语言理解能力”,arXiv(2018)。

  12. Y. Zhang 等人,“DialoGPT:面向对话回应生成的大规模生成式预训练”,arXiv(2019)。

  13. huggingface.co/EleutherAI

第八章:使用 BERT 和多语言 BERT 的 NLP 深度迁移学习

本章包括

  • 使用预训练的双向编码器表示来自变换器(BERT)架构来执行一些有趣的任务

  • 使用 BERT 架构进行跨语言迁移学习

在这一章和上一章,我们的目标是介绍一些代表性的深度迁移学习建模架构,这些架构依赖于最近流行的神经架构——transformer¹——来进行关键功能的自然语言处理(NLP)。这可以说是当今 NLP 中最重要的架构。具体来说,我们的目标是研究一些建模框架,例如生成式预训练变换器(GPT),² 双向编码器表示来自变换器(BERT),³ 和多语言 BERT(mBERT)。⁴ 这些方法使用的神经网络的参数比我们之前介绍的深度卷积和循环神经网络模型更多。尽管它们体积更大,但由于它们在并行计算架构上的比较效率更高,它们的流行度急剧上升。这使得实际上可以开发出更大更复杂的模型。为了使内容更易理解,我们将这些模型的覆盖分成两章/部分:我们在上一章中介绍了变换器和 GPT 神经网络架构,而在接下来的这章中,我们将专注于 BERT 和 mBERT。

作为提醒,BERT 是基于 transformer 的模型,我们在第三章和第七章中简要介绍过。它是使用masked modeling objective进行训练来填补空白。此外,它还经过了“下一个句子预测”任务的训练,以确定给定句子是否是目标句子后的合理跟随句子。mBERT,即“多语言 BERT”,实际上是针对 100 多种语言同时预训练的 BERT。自然地,这个模型特别适用于跨语言迁移学习。我们将展示多语言预训练权重检查点如何促进为初始未包含在多语言训练语料库中的语言创建 BERT 嵌入。BERT 和 mBERT 均由 Google 创建。

本章的第一节深入探讨了 BERT,并将其应用于重要的问答应用作为一个独立的示例。该章节通过实验展示了预训练知识从 mBERT 预训练权重转移到新语言的 BERT 嵌入的迁移。这种新语言最初并未包含在用于生成预训练 mBERT 权重的多语料库中。在这种情况下,我们使用加纳语 Twi 作为示例语言。

让我们在下一节继续分析 BERT。

8.1 双向编码器表示来自变换器(BERT)

在本节中,我们介绍了可能是最受欢迎和最具影响力的基于 Transformer 的神经网络架构,用于自然语言处理的迁移学习——双向编码器表示的 Transformer(BERT)模型,正如我们之前提到的,它也是以流行的Sesame Street角色命名的,向 ELMo 开创的潮流致敬。回想一下 ELMo 本质上就是变压器做的事情,但是使用的是循环神经网络。我们在第一章首次遇到了这两种模型,在我们对自然语言处理迁移学习历史的概述中。我们还在第三章中使用了它们进行了一对分类问题,使用了 TensorFlow Hub 和 Keras。如果您不记得这些练习,可能有必要在继续本节之前进行复习。结合上一章,这些模型的预览使您对了解模型的更详细功能处于一个很好的位置,这是本节的目标。

BERT 是早期预训练语言模型,开发于 ELMo 和 GPT 之后,但在普通语言理解评估(GLUE)数据集的大多数任务中表现出色,因为它是双向训练的。我们在第六章讨论了 ELMo 如何将从左到右和从右到左的 LSTM 组合起来实现双向上下文。在上一章中,我们还讨论了 GPT 模型的掩码自注意力如何通过堆叠变压器解码器更适合因果文本生成。与这些模型不同,BERT 通过堆叠变压器编码器而不是解码器,为每个输入标记同时实现双向上下文。回顾我们在第 7.2 节中对 BERT 每个层中的自注意力的讨论,每个标记的计算都考虑了两个方向上的每个其他标记。而 ELMo 通过将两个方向放在一起实现了双向性,GPT 是一种因果单向模型。BERT 每一层的同时双向性似乎给了它更深层次的语言上下文感。

BERT 是通过掩码语言建模(MLM)填空预测目标进行训练的。在训练文本中,标记被随机掩码,模型的任务是预测掩码的标记。为了说明,再次考虑我们示例句子的略微修改版本,“他不想在手机上谈论细胞,他认为这个话题很无聊。” 为了使用 MLM,我们可以将其转换为“他不想在手机上谈论细胞,一个[MASK],他认为这个话题很无聊。” 这里的[MASK]是一个特殊标记,指示哪些词已被省略。然后,我们要求模型根据其在此之前观察到的所有文本来预测省略的词。经过训练的模型可能会预测掩码词 40%的时间是“conversation”,35%的时间是“subject”,其余 25%的时间是“topic”。在训练期间重复执行这个过程,建立了模型对英语语言的知识。

另外,BERT 的训练还使用了下一句预测(NSP)目标。在这里,训练文本中的一些句子被随机替换为其他句子,并要求模型预测句子 B 是否是句子 A 的合理续篇。为了说明,让我们将我们的示例句子分成两个句子:“他不想谈论手机上的细胞。他认为这个话题很无聊。” 然后我们可能删除第二个句子,并用略微随机的句子替换它,“足球是一项有趣的运动。” 一个经过适当训练的模型需要能够检测前者作为潜在的合理完成,而将后者视为不合理的。我们通过具体的编码练习示例来讨论 MLM 和 NSP 目标,以帮助您理解这些概念。

在下一小节中,我们简要描述了 BERT 架构的关键方面。我们接着介绍了将 transformers 库中的管道 API 概念应用于使用预训练 BERT 模型进行问答任务。我们随后通过示例执行填空 MLM 任务和 NSP 任务。对于 NSP 任务,我们直接使用 transformers API 来帮助您熟悉它。与上一章节类似,我们在这里没有明确地在更具体的目标数据上对预训练的 BERT 模型进行调优。然而,在本章的最后一节中,我们将在单语 Twi 数据上微调多语言 BERT 模型。

8.1.1 模型架构

您可能还记得第 7.1.1 节中我们可视化了 BERT 自注意力时,BERT 本质上是图 7.1 中原始编码器-解码器变换器架构的一组叠加编码器。BERT 模型架构如图 8.1 所示。

08_01

图 8.1 BERT 架构的高级表示,显示堆叠的编码器、输入嵌入和位置编码。顶部的输出在训练期间用于下一句预测和填空遮蔽语言建模目标。

正如我们在介绍中讨论的,并且如图所示,在训练期间,我们使用下一句预测(NSP)和遮蔽语言建模(MSM)目标。BERT 最初以两种风味呈现,BASE 和 LARGE。如图 8.1 所示,BASE 堆叠了 12 个编码器,而 LARGE 堆叠了 24 个编码器。与之前一样——在 GPT 和原始 Transformer 中——通过输入嵌入将输入转换为向量,并向它们添加位置编码,以给出输入序列中每个标记的位置感。为了考虑下一句预测任务,其中输入是句子 A 和 B 的一对,添加了额外的段编码步骤。段嵌入指示给定标记属于哪个句子,并添加到输入和位置编码中,以产生输入到编码器堆栈的输出。我们的示例句对的整个输入转换在图 8.2 中可视化:“他不想在手机上谈论细胞。他认为这个主题非常无聊。”

08_02

图 8.2 BERT 输入转换可视化

此时提到[CLS][SEP]特殊标记的简要说明值得一提。回想一下,[SEP]标记分隔句子并结束它们,如前几节所讨论的。另一方面,[CLS]特殊标记被添加到每个输入示例的开头。输入示例是 BERT 框架内部用来指代标记化的输入文本的术语,如图 8.2 所示。[CLS]标记的最终隐藏状态用作分类任务的聚合序列表示,例如蕴涵或情感分析。[CLS]代表“分类”。

在继续查看以下小节中使用一些这些概念的具体示例之前,请记得,在第三章中首次遇到 BERT 模型时,我们将输入首先转换为输入示例,然后转换为特殊的三元组形式。这些是输入 ID输入掩码段 ID。我们在这里复制了列表 3.8 以帮助你记忆,因为当时这些术语尚未被介绍。

列表 3.8(从第三章复制)将数据转换为 BERT 期望的形式,训练

def build_model(max_seq_length):                                          ❶
    in_id = tf.keras.layers.Input(shape=(max_seq_length,), name="input_ids")
    in_mask = tf.keras.layers.Input(shape=(max_seq_length,), name="input_masks")
    in_segment = tf.keras.layers.Input(shape=(max_seq_length,), name="segment_ids")
    bert_inputs = [in_id, in_mask, in_segment]

    bert_output = BertLayer(n_fine_tune_layers=0)(bert_inputs)            ❷
    dense = tf.keras.layers.Dense(256, activation="relu")(bert_output)
    pred = tf.keras.layers.Dense(1, activation="sigmoid")(dense)

    model = tf.keras.models.Model(inputs=bert_inputs, outputs=pred)
    model.compile(loss="binary_crossentropy", optimizer="adam", metrics=["accuracy"])
    model.summary()

    return model

def initialize_vars(sess):                                                ❸
    sess.run(tf.local_variables_initializer())
    sess.run(tf.global_variables_initializer())
    sess.run(tf.tables_initializer())
    K.set_session(sess)

bert_path = "https:/ /tfhub.dev/google/bert_uncased_L-12_H-768_A-12/1"
tokenizer = create_tokenizer_from_hub_module(bert_path)                   ❹

train_examples = convert_text_to_examples(train_x, train_y)               ❺
test_examples = convert_text_to_examples(test_x, test_y)
# Convert to features
(train_input_ids,train_input_masks,train_segment_ids,train_labels) =       ❻
     convert_examples_to_features(tokenizer, train_examples,               ❻
     max_seq_length=maxtokens)                                             ❻
(test_input_ids,test_input_masks,test_segment_ids,test_labels) = 
     convert_examples_to_features(tokenizer, test_examples, 
     max_seq_length=maxtokens)

model = build_model(maxtokens)                                             ❼

initialize_vars(sess)                                                      ❽

history = model.fit([train_input_ids, train_input_masks, train_segment_ids], ❾
train_labels,validation_data=([test_input_ids, test_input_masks, 
test_segment_ids],test_labels), epochs=5, batch_size=32)

❶ 用于构建模型的函数

❷ 我们不重新训练任何 BERT 层,而是将预训练模型用作嵌入,并在其上重新训练一些新层。

❸ Vanilla TensorFlow 初始化调用

❹ 使用 BERT 源代码库中的函数创建兼容的分词器

❺ 使用 BERT 源代码库中的函数将数据转换为 InputExample 格式

❻ 将 InputExample 格式转换为三元 BERT 输入格式,使用 BERT 源存储库中的函数

❼ 构建模型

❽ 实例化变量

❾ 训练模型

如前一章节所述,输入 ID 只是词汇表中对应标记的整数 ID——对于 BERT 使用的 WordPiece 分词,词汇表大小为 30,000。由于变换器的输入长度是由列表 3.8 中的超参数 max_seq_length 定义的,因此需要对较短的输入进行填充,对较长的输入进行截断。输入掩码只是相同长度的二进制向量,其中 0 对应填充标记 ([PAD]),1 对应实际输入。段 ID 与图 8.2 中描述的相同。另一方面,位置编码和输入嵌入由 TensorFlow Hub 模型在内部处理,用户无法访问。可能需要再次仔细阅读第三章才能充分理解这种比较。

尽管 TensorFlow 和 Keras 仍然是任何自然语言处理工程师工具箱中至关重要的组件——具有无与伦比的灵活性和效率——但 transformers 库无疑使这些模型对许多工程师和应用更加易于接近和使用。在接下来的小节中,我们将使用该库中的 BERT 应用于问题回答、填空和下一个句子预测等关键应用。

8.1.2 问题回答的应用

自然语言处理领域的开端以来,问题回答一直吸引着计算机科学家的想象力。它涉及让计算机在给定某些指定上下文的情况下自动回答人类提出的问题。潜在的应用场景仅受想象力限制。突出的例子包括医学诊断、事实检查和客户服务的聊天机器人。事实上,每当你在谷歌上搜索像“2010 年超级碗冠军是谁?”或“2006 年谁赢得了 FIFA 世界杯?”这样的问题时,你正在使用问题回答。

让我们更加仔细地定义问题回答。更具体地说,我们将考虑 抽取式问题回答,定义如下:给定上下文段落 p 和问题 q,问题回答的任务是产生 p 中答案所在的起始和结束整数索引。如果 p 中不存在合理的答案,系统也需要能够指示这一点。直接尝试一个简单的例子,如我们接下来使用预训练的 BERT 模型和 transformers pipelines API 做的,将帮助你更好地具体了解这一点。

我们从世界经济论坛⁵中选择了一篇有关口罩和其他封锁政策对美国 COVID-19 大流行的有效性的文章。我们选择文章摘要作为上下文段落。请注意,如果没有文章摘要可用,我们可以使用相同库中的摘要流水线快速生成一个。以下代码初始化了问答流水线和上下文。请注意,这种情况下我们使用了 BERT LARGE,它已经在斯坦福问答数据集(SQuAD)⁶上进行了微调,这是迄今为止最广泛的问答数据集。还请注意,这是 transformers 默认使用的任务,默认模型,我们不需要显式指定。但是,我们为了透明度而这样做。

from transformers import pipeline

qNa= pipeline('question-answering', model= 'bert-large-cased-whole-word-masking-finetuned-squad', tokenizer='bert-large-cased-whole-word-masking-finetuned-squad')          ❶

paragraph = 'A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer deaths by the start of June. Containment policies had a large impact on the number of COVID-19 cases and deaths, directly by reducing transmission rates and indirectly by constraining people’s behaviour. They account for roughly half the observed change in the growth rates of cases and deaths.'

❶ 这些模型通常会被默认加载,但我们明确指出以保持透明度。使用已在 SQuAD 上进行了微调的模型非常重要;否则,结果将很差。

在初始化了流水线之后,让我们首先看看是否能够通过询问文章的主题来自动提取文章的精髓。我们用以下代码来实现:

ans = qNa({'question': 'What is this article about?','context': f'{paragraph}'})
print(ans)

这产生了以下输出,我们可能会认为这是一个合理的回答:

{'score': 0.47023460869354494, 'start': 148, 'end': 168, 'answer': 'Containment policies'}

注意,0.47 相对较低的分数表明答案缺少一些上下文。类似“遏制政策对 COVID-19 的影响”可能是更好的回答,但因为我们正在进行提取式问答,而这个句子不在上下文段落中,所以这是模型能做到的最好的。低分数可以帮助标记此回答进行人工双重检查和/或改进。

为什么不问一些更多的问题?让我们看看模型是否知道文章中描述的是哪个国家,使用以下代码:

ans = qNa({'question': 'Which country is this article about?',
           'context': f'{paragraph}'})
print(ans)

这产生了以下输出,正如以前的分数约为 0.8 所示,完全正确:

{'score': 0.795254447990601, 'start': 34, 'end': 36, 'answer': 'US'}

讨论的是哪种疾病?

ans = qNa({'question': 'Which disease is discussed in this article?',
           'context': f'{paragraph}'})
print(ans)

输出完全正确,信心甚至比之前更高,达到了 0.98,如下所示:

{'score': 0.9761025334558902, 'start': 205, 'end': 213, 'answer': 'COVID-19'}

那时间段呢?

ans = qNa({'question': 'What time period is discussed in the article?',
           'context': f'{paragraph}'})
print(ans)

与输出相关联的 0.22 的低分数表明结果质量差,因为文章中讨论了 4 月至 6 月的时间范围,但从未在连续的文本块中讨论,可以为高质量答案提取,如下所示:

{'score': 0.21781831588181433, 'start': 71, 'end': 79, 'answer': '1 April,'}

然而,仅选择一个范围的端点能力已经是一个有用的结果。这里的低分数可以提醒人工检查此结果。在自动化系统中,目标是这样的较低质量答案成为少数,总体上需要很少的人工干预。

在介绍了问答之后,在下一小节中,我们将解决 BERT 训练任务的填空和下一句预测。

8.1.3 应用于填空和下一句预测任务

我们在这一节的练习中使用了上一小节的文章。让我们立即开始编写一个用于填写空白的流程,使用以下代码:

from transformers import pipeline

fill_mask = pipeline("fill-mask",model="bert-base-cased",tokenizer="bert-base-cased")

注意,在这里我们使用的是 BERT BASE 模型。这些任务对任何 BERT 模型的训练来说都是基本的,所以这是一个合理的选择,不需要特殊的微调模型。初始化适当的流程后,我们现在可以将它应用于上一小节中文章的第一句话。我们通过用适当的掩码标记[MASK]来删除“cases”这个词,并使用以下代码向模型提供已省略的词进行预测:

fill_mask("A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer [MASK] by the start of June")

输出显示,最高的是“deaths”,这是一个可能合理的完成。即使剩下的建议也可以在不同的情境下起作用!

[{'sequence': '[CLS] A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer deaths by the start of June [SEP]',
  'score': 0.19625532627105713,
  'token': 6209},
 {'sequence': '[CLS] A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer executions by the start of June [SEP]',
  'score': 0.11479416489601135,
  'token': 26107},
 {'sequence': '[CLS] A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer victims by the start of June [SEP]',
  'score': 0.0846652239561081,
  'token': 5256},
 {'sequence': '[CLS] A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer masks by the start of June [SEP]',
  'score': 0.0419488325715065,
  'token': 17944},
 {'sequence': '[CLS] A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer arrests by the start of June [SEP]',
  'score': 0.02742016687989235,
  'token': 19189}] 

我们鼓励你尝试从各种句子中省略各种单词,以确信这几乎总是非常有效的。在节省篇幅的情况下,我们的附带笔记本会为几个更多的句子做到这一点,但我们不在这里打印这些结果。

然后我们继续进行下一个句子预测(NSP)任务。在写作本文时,此任务尚未包含在 pipelines API 中。因此,我们将直接使用 transformers API,这也将让您更加熟悉它。我们首先需要确保已安装 transformers 3.0.0 以上的版本,因为该任务仅在该阶段的库中包含。我们使用以下代码实现这一点;在写作本文时,Kaggle 默认安装了较早的版本:

!pip install transformers==3.0.1 # upgrade transformers for NSP

升级版本后,我们可以使用以下代码加载一个 NSP-specific BERT:

from transformers import BertTokenizer, BertForNextSentencePrediction   ❶
import torch
from torch.nn.functional import softmax                                 ❷

tokenizer = BertTokenizer.from_pretrained('bert-base-cased')
model = BertForNextSentencePrediction.from_pretrained('bert-base-cased')
model.eval()                                                            ❸

❶ NSP-specific BERT

❷ 计算原始输出的最终概率

❸ PyTorch 模型默认是可训练的。为了更便宜的推断和可执行重复性,将其设置为“eval”模式,如此处所示。通过 model.train()将其设置回“train”模式。对于 TensorFlow 模型不适用!

作为健全性检查,首先我们要确定第一句和第二句是否从模型的角度来看是合理的完成。我们使用以下代码进行检查:

prompt = "A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer deaths by the start of June."
next_sentence = "Containment policies had a large impact on the number of COVID-19 cases and deaths, directly by reducing transmission rates and indirectly by constraining people’s behavior."
encoding = tokenizer.encode(prompt, next_sentence, return_tensors='pt')
logits = model(encoding)[0]                                              ❶
probs = softmax(logits)                                                  ❷
print("Probabilities: [not plausible, plausible]")
print(probs)

❶ 输出是一个元组;第一项描述了我们追求的两个句子之间的关系。

❷ 从原始数字计算概率

注意代码中的术语logits。这是 softmax 函数的原始输入。通过 softmax 将logits传递,可以得到概率。代码的输出确认找到了正确的关系,如下所示:

Probabilities: [not plausible, plausible]
tensor([[0.1725, 0.8275]], grad_fn=<SoftmaxBackward>)

现在,让我们将第二个句子替换为一个有点随机的“Cats are independent.” 这将产生以下结果:

Probabilities: [not plausible, plausible]
tensor([0.7666, 0.2334], grad_fn=<SoftmaxBackward>)

看起来一切都如预期的那样工作!

现在,你应该已经非常清楚 BERT 在训练中解决哪些任务了。需要注意的是,本章我们还没有将 BERT 调整到任何新域或任务特定的数据上进行微调。这是有意为之的,以帮助你在没有任何干扰的情况下了解模型架构。在下一节中,我们会演示如何进行微调,通过进行跨语言迁移学习实验。对于我们已经介绍过的所有其他任务,都可以采用类似的迁移学习方式进行,通过完成下一节练习,您将有很好的发挥空间去自己实践。

8.2 基于多语言 BERT(mBERT)的跨语言学习

在本节中,我们将进行本书中第二个整体和第一个主要的跨语言实验。更具体地说,我们正在进行一个迁移学习实验,该实验涉及从多语言 BERT 模型中转移知识到其原始训练中不包含的语言。与之前一样,我们在实验中使用的语言将是 Twi 语,这是一种被认为是“低资源”的语言,因为缺乏多种任务的高质量训练数据。

多语言 BERT(mBERT)本质上是指应用前一节中所描述的 BERT,并将其应用于约 100 个连接在一起的语言维基百科⁷ 语料库。最初的语言集合是前 100 大维基百科,现已扩展到前 104 种语言。该语言集合不包括 Twi,但包括一些非洲语言,如斯瓦希里语和约鲁巴语。由于各种语言语料库的大小差异很大,因此会应用一种“指数平滑”过程来对高资源语言(如英语)进行欠采样,对低资源语言(如约鲁巴语)进行过采样。与之前一样,使用了 WordPiece 分词。对于我们而言,它足以提醒你,这种分词过程是子词级别的,正如我们在之前的章节中所看到的。唯一的例外是中文、日文的汉字和韩文汉字,它们通过在每个字符周围加上空格的方式被转换为有效的字符分词。此外,为了在精度和模型效率之间做出权衡选择,mBERT 作者消除了重音词汇。

我们可以直观地认为,一个在 100 多种语言上训练的 BERT 模型包含了可以转移到原始训练集中未包含的语言的知识。简单来说,这样的模型很可能会学习到所有语言中共同的特征。这种共同特征的一个简单例子是单词和动词-名词关系的概念。如果我们将提出的实验框架设定为多任务学习问题,正如我们在第四章中讨论的那样,我们期望对以前未见过的新场景的泛化性能得到改善。在本节中,我们将基本证明这一点。我们首先使用预训练的分词器将 mBERT 转移到单语 Twi 数据上。然后,我们通过从头开始训练相同的 mBERT/BERT 架构以及训练适当的分词器来重复实验。比较这两个实验将允许我们定性地评估多语言转移的有效性。我们为此目的使用 JW300 数据集的 Twi 子集⁸。

本节的练习对于你的技能集具有超越多语言转移的影响。这个练习将教会你如何从头开始训练你自己的分词器和基于 transformer 的模型。它还将演示如何将一个检查点转移到这样一个模型的新领域/语言数据。之前的章节和一点冒险/想象力将为你提供基于 transformer 的迁移学习超能力,无论是用于领域自适应、跨语言转移还是多任务学习。

在接下来的小节中,我们简要概述了 JW300 数据集,然后是执行跨语言转移和从头开始训练的小节。

8.2.1 JW300 数据集简介

JW300 数据集是一个面向低资源语言的广泛覆盖的平行语料库。正如之前提到的,它是一个可能具有偏见的样本,由耶和华见证人翻译的宗教文本组成。然而,对于许多低资源语言研究而言,它是一个起点,通常是唯一可用的平行数据的开放来源。然而,重要的是要记住这种偏见,并在这个语料库上进行任何训练时配备第二阶段,该阶段可以将第一阶段的模型转移到一个更少偏见和更具代表性的语言和/或任务样本。

尽管它本质上是一个平行语料库,但我们只需要 Twi 数据的单语语料库进行我们的实验。Python 包 opustools-pkg 可以用于获取给定语言对的平行语料库。为了让您的工作更容易,我们已经为英语-Twi 语对进行了这项工作,并将其托管在 Kaggle 上。⁹要为其他低资源语言重复我们的实验,您需要稍微调整一下opustools-pkg并获取一个等价的语料库(如果您这样做,请与社区分享)。我们只使用平行语料库的 Twi 部分进行我们的实验,并忽略英语部分。

让我们继续将 mBERT 转移到单语低资源语言语料库。

8.2.2 将 mBERT 转移到单语 Twi 数据与预训练的标记器

首先要做的是初始化一个 BERT 标记器到来自 mBERT 模型中的预训练检查点。这次我们使用的是大小写版本,如下代码所示:

from transformers import BertTokenizerFast                                   ❶
tokenizer = BertTokenizerFast.from_pretrained("bert-base-multilingual-cased")❷

❶ 这只是 BertTokenizer 的一个更快的版本,你可以用这个替代它。

❷ 使用了预训练的 mBERT 标记器

准备好了标记器后,让我们按以下方法将 mBERT 检查点加载到 BERT 遮蔽语言模型中,并显示参数数量:

from transformers import BertForMaskedLM                                  ❶

model = BertForMaskedLM.from_pretrained("bert-base-multilingual-cased")   ❷

print("Number of parameters in mBERT model:")
print(model.num_parameters())

❶ 使用了遮蔽语言建模

❷ 初始化到了 mBERT 检查点

输出表明模型有 1.786 亿个参数。

接下来,我们使用 transformers 附带的方便的 LineByLineTextDataset 方法,使用单语 Twi 文本的标记器来构建数据集,如下所示:

from transformers import LineByLineTextDataset

dataset = LineByLineTextDataset(
    tokenizer=tokenizer,
    file_path="../input/jw300entw/jw300.en-tw.tw",
    block_size=128)                                  ❶

❶ 指示一次读取多少行

如下代码所示,接下来我们需要定义一个“data collator” —— 一个帮助方法,通过一批样本数据行(长度为block_size)创建一个特殊对象。 这个特殊对象适用于 PyTorch 进行神经网络训练:

from transformers import DataCollatorForLanguageModeling

data_collator = DataCollatorForLanguageModeling(
    tokenizer=tokenizer,
    mlm=True, mlm_probability=0.15)                  ❶

❶ 使用了遮蔽语言建模,并以 0.15 的概率遮蔽单词

在这里,我们使用了遮蔽语言建模,就像前一节所描述的一样。在我们的输入数据中,有 15% 的单词被随机遮蔽,模型在训练期间被要求对它们进行预测。

定义标准的训练参数,比如输出目录和训练批量大小,如下所示:

from transformers import TrainingArguments

training_args = TrainingArguments(
    output_dir="twimbert",
    overwrite_output_dir=True,
    num_train_epochs=1,
    per_gpu_train_batch_size=16,
    save_total_limit=1,
)

然后使用先前定义的数据集和数据收集器定义一个“训练器”来进行数据上的一个训练周期。注意,数据包含了超过 600,000 行,因此一次遍历所有数据是相当大量的训练!

trainer = Trainer(
    model=model,
    args=training_args,
    data_collator=data_collator,
    train_dataset=dataset,
    prediction_loss_only=True)

训练并计算训练时间,如下所示:

import time
start = time.time()
trainer.train()
end = time.time()
print("Number of seconds for training:")
print((end-start))

模型在所示的超参数下大约需要三个小时才能完成一个周期,并且损失大约为 0.77。

按如下进行模型保存:

trainer.save_model("twimbert")

最后,我们从语料库中取出以下句子 —— “Eyi de ɔhaw kɛse baa sukuu hɔ” —— 它的翻译是 “这在学校中提出了一个大问题。” 我们遮蔽了一个单词,sukuu(在 Twi 中意思是“学校”),然后应用 pipelines API 来预测遗漏的单词,如下所示:

from transformers import pipeline

fill_mask = pipeline(                                 ❶
    "fill-mask",
    model="twimbert",
    tokenizer=tokenizer)

print(fill_mask("Eyi de ɔhaw kɛse baa [MASK] hɔ."))   ❷

❶ 定义了填空管道

❷ 预测被遮蔽的标记

这将产生如下输出:

[{'sequence': '[CLS] Eyi de ɔhaw kɛse baa me hɔ. [SEP]', 'score': 0.13256989419460297, 'token': 10911}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa Israel hɔ. [SEP]', 'score': 0.06816119700670242, 'token': 12991}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa ne hɔ. [SEP]', 'score': 0.06106790155172348, 'token': 10554}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa Europa hɔ. [SEP]', 'score': 0.05116277188062668, 'token': 11313}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa Eden hɔ. [SEP]', 'score': 0.033920999616384506, 'token': 35409}]

你立刻就能看到结果中的宗教偏见。“以色列”和“伊甸园”被提议为前五个完成之一。话虽如此,它们算是比较有说服力的完成 —— 因为它们都是名词。总的来说,表现可能还算不错。

如果你不会说这种语言,不用担心。在下一节中,我们将从头开始训练 BERT,并将损失值与我们在这里获得的值进行比较,以确认我们刚刚执行的转移学习实验的功效。我们希望您能尝试在其他您感兴趣的低资源语言上尝试这里概述的步骤。

8.2.3 在单语 Twi 数据上从零开始训练的 mBERT 和分词器

要从头开始训练 BERT,我们首先需要训练一个分词器。我们可以使用下一节代码中的代码初始化、训练和保存自己的分词器到磁盘。

代码清单 8.1 从头初始化、训练和保存我们自己的 Twi 分词器

from tokenizers import BertWordPieceTokenizer 

paths = ['../input/jw300entw/jw300.en-tw.tw']

tokenizer = BertWordPieceTokenizer()                                 ❶

tokenizer.train(                                                     ❷
    paths,
    vocab_size=10000,
    min_frequency=2,
    show_progress=True,
    special_tokens=["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"],   ❸
    limit_alphabet=1000,
    wordpieces_prefix="##")

!mkdir twibert                                                       ❹

tokenizer.save("twibert") 

❶ 初始化分词器

❷ 自定义训练,并进行训练

❸ 标准 BERT 特殊标记

❹ 将分词器保存到磁盘

要从刚刚保存的分词器中加载分词器,我们只需要执行以下操作:

from transformers import BertTokenizerFast
tokenizer = BertTokenizerFast.from_pretrained("twibert", max_len=512)    ❶

❶ 使用我们刚刚训练的语言特定的分词器,max_len=512,以保持与上一小节一致

请注意,我们使用最大序列长度为 512,以保持与上一小节一致——这也是预训练的 mBERT 使用的长度。还要注意,保存分词器将在指定文件夹中创建词汇文件 vocab.txt 文件。

从这里开始,我们只需初始化一个全新的 BERT 模型来进行掩码语言建模,如下所示:

from transformers import BertForMaskedLM, BertConfig
model = BertForMaskedLM(BertConfig())                  ❶

❶ 不要初始化为预训练的;创建一个全新的。

否则,步骤与上一小节相同,我们不在此处重复代码。重复相同的步骤在一个时代后大约 1.5 小时产生大约 2.8 的损失,并在两个时代后的大约 3 小时产生 2.5 的损失。这显然不如前一小节的 0.77 损失值好,证实了在那种情况下转移学习的功效。请注意,这次实验每个时代的时间较短,因为我们构建的分词器完全专注于 Twi,因此其词汇量比 104 种语言的预训练 mBERT 词汇表小。

去吧,改变未来!

摘要

  • transformer 架构使用自注意力机制来构建文本的双向上下文以理解文本。这使得它最近在 NLP 中成为主要的语言模型。

  • transformer 允许序列中的标记独立于彼此进行处理。这比按顺序处理标记的 bi-LSTM 实现了更高的可并行性。

  • transformer 是翻译应用的一个不错选择。

  • BERT 是一种基于 transformer 的架构,对于其他任务,如分类,是一个不错的选择。

  • BERT 可以同时在多种语言上进行训练,生成多语言模型 mBERT。该模型捕获的知识可转移到原本未包含在训练中的语言。

  1. A. Vaswani 等人,“注意力就是一切”,NeurIPS (2017)。

  2. A. Radford 等人,“通过生成式预训练改善语言理解”,arXiv (2018)。

  3. M. E. Peters et al., “BERT: Pre-Training of Deep Bidirectional Transformers for Language Understanding,” Proc. of NAACL-HLT (2019): 4171-86.

  4. github.com/google-research/bert/blob/master/multilingual.md

  5. www.weforum.org/agenda/2020/07/口罩命令和其他封锁政策减少了在美国的 COVID-19 传播.

  6. P. Rajpurkar et al., “SQuAD: 100,000+ Questions for Machine Comprehension of Text,” arXiv (2016).

  7. github.com/google-research/bert/blob/master/multilingual.md

  8. opus.nlpl.eu/JW300.php

  9. www.kaggle.com/azunre/jw300entw

第九章:ULMFiT 和知识蒸馏适应策略

本章包括

  • 实施判别微调逐步解冻等策略。

  • 教师学生BERT 模型之间执行知识蒸馏

在本章和下一章中,我们将介绍迄今为止已涵盖的深度NLP迁移学习建模架构的一些适应策略。换句话说,鉴于预训练架构如 ELMo、BERT 或 GPT,我们如何更有效地进行迁移学习?我们可以在这里采取几种效率措施。我们选择关注参数效率,即目标是在减少性能的同时产生尽可能少的参数模型。这样做的目的是使模型更小、更容易存储,从而更容易在智能手机设备上部署。另外,智能适应策略可能需要在某些困难的迁移情况下达到可接受的性能水平。

在第六章中,我们描述了 ULMFiT¹方法,即通用语言模型微调。该方法引入了判别微调逐步解冻的概念。简而言之,逐步解冻逐渐增加网络中解冻或微调的子层的数量。另一方面,判别微调为网络中的每一层指定了可变的学习率,从而实现更有效的迁移。我们在第六章的代码中没有实施这些方法,因为作为适应策略,我们认为它们最适合放在本章中。在本章中,我们使用 ULMFiT 作者编写的fast.ai库来演示预训练的循环神经网络(RNN)语言模型的概念。

一些模型压缩方法通常被应用于大型神经网络以减小其大小。一些著名的方法包括权重修剪和量化。在这里,我们将重点关注适应策略,即NLP领域最近备受关注的知识蒸馏。该过程本质上试图使用显著较小的学生模型模拟来自较大的教师模型的输出。特别是,我们使用变压器库中的 DistilBERT²方法的实现来演示通过这种方法可以将 BERT 的大小减半以上。

让我们从下一节开始 ULMFiT。

9.1 逐步解冻和判别微调

在本节中,我们将在代码中实现 ULMFiT 方法,将语言模型适应于新的数据领域和任务。我们首先在第六章的最后讨论了这种方法的概念,因为从历史上看,它首先是在递归神经网络(RNNs)的背景下引入的。然而,我们将实际的编码练习推迟到现在,以强调在其核心,ULMFiT 是一组与架构无关的适应技术。这意味着它们也可以应用于基于 transformer 的模型。然而,为了与源材料保持一致,我们在 RNN-based 语言模型的背景下进行练习编码。我们将编码练习集中在我们在第六章中看到的假新闻检测示例上。

作为提醒,辨别微调指定网络中每一层的可变学习率。此外,学习率在学习过程中不是恒定的。相反,它们是倾斜三角形的——在开始时线性增加到一定程度,然后线性衰减。换句话说,这意味着快速增加学习率,直到达到最大速率,然后以较慢的速度减小。这个概念在图 6.8 中有所说明,我们在这里为了您的方便重复了它。

06_08

图 6.8(从第六章复制)建议的倾斜三角形 ULMFiT 学习率时间表,对于总迭代次数为 10,000 的情况。学习率在总迭代次数的 10%(即 1,000)上线性增加,最高达 0.01,然后线性减少到 0。

请注意,图中标有“最大学习率”的点在我们的情况下会有所不同(不是 0.01)。迭代的总数也将与图中显示的 10,000 次不同。这个时间表会产生更有效的转移和更具一般性的模型。

渐进解冻,另一方面,逐渐增加网络的子层的数量解冻,这样可以减少过拟合,同时也会产生更有效的转移和更具一般性的模型。所有这些技术在第六章的最后一节中都有详细讨论,可能在着手本节其余部分之前,简要回顾该讨论会有益处。

我们将在这里使用第 5.2 节的说明性示例——事实核查示例。回想一下,这个数据集包含超过 40,000 篇文章,分为两类:“假”和“真”。真实文章是从 reuters.com,一个声誉良好的新闻网站收集来的。另一方面,假文章则是从 PolitiFact 标记为不可靠的各种来源收集来的。在第 6.2 节,我们在预训练的 ELMo 模型导出的特征向量上训练了一个二元分类器。这个分类器预测一篇给定的文章是真实的(1)还是假的(0)。使用由每个类别的 1,000 篇文章组成的数据集,获得了 98%+ 的准确率。在这里,我们将看看是否可以通过 ULMFiT 方法取得更好的效果。

在本节中,我们将该方法分为两个小节。第一个小节涉及在目标任务数据上微调预训练语言模型的第一阶段 ULMFiT。斜三角形学习率在这里发挥作用,以及分层微调的概念。一些数据预处理和模型架构讨论也自然地融入到这个第一个小节中。第二个小节涵盖了第二阶段,涉及在目标任务数据上微调目标任务分类器——它位于微调语言模型之上——的阶段。逐步解冻程序的有效性由此得到证明。

请注意,本节中呈现的代码采用 fast.ai 版本 1 语法编写。选择这样做的原因是该库的第 2 版更改了输入数据的处理方式,提供了将其分割为训练集和验证集的内部函数,而不是允许您自己指定。为了与我们在前几章中的工作保持一致,在那里我们自己分割了数据,我们在这里坚持使用版本 1。我们还在 Kaggle 笔记本中提供了等效的 fast.ai 版本 2 语法代码³,您应该运行并与此处呈现的版本 1 代码进行比较。最后,请注意,版本 1 的文档托管在 fastai1.fast.ai/,而版本 2 的文档托管在 docs.fast.ai/

9.1.1 预训练语言模型微调

第 5.2 节已经描述了我们需要对事实核查示例数据集进行的初始数据预处理步骤。特别地,我们对文章文本数据进行了洗牌,并将其加载到 NumPy 数组train_xtest_x中。我们还构建了相应的标签 NumPy 数组train_ytest_y,其中包含每篇文章是否为真实的信息,当文章为真时标记为 1,否则为 0。如同第 5.2 节一样,保持 1,000 个样本和测试/验证比例为 30%,得到的训练数组——train_xtrain_y——长度为 1,400,测试数组——test_xtest_y——长度为 600。

我们需要做的第一件事是准备 fast.ai 库所期望的数据形式。其中一种数据格式是一个两列的 Pandas DataFrame,第一列包含标签,第二列包含数据。我们可以相应地构建训练和测试/验证数据框,如下所示:

train_df = pd.DataFrame(data=[train_y,train_x]).T 
test_df = pd.DataFrame(data=[test_y,test_x]).T

这些数据框应该分别有 1,400 行和 600 行,每个都对应于相应数据样本中的每篇文章,并且在继续之前,最好用通常的.shape命令检查一下,如下所示:

train_df.shape
test_df.shape

预期输出分别为(1400, 2)(600, 2)

fast.ai 中的数据使用TextLMDataBunch类进行消耗,这些实例可以使用我们刚刚准备的 DataFrame 格式构建,使用以下命令:

data_lm = TextLMDataBunch.from_df(train_df = train_df, valid_df = test_df, path = "")

另一方面,fast.ai 中的数据由一个特定于任务的分类器使用TextClasDataBunch类进行消耗。我们构建此类的一个实例,准备进入下一小节,使用以下类似的命令从我们的数据框中:

data_clas = TextClasDataBunch.from_df(path = "", train_df = train_df, valid_df = test_df, vocab=data_lm.train_ds.vocab)

现在我们准备在目标数据上微调我们的语言模型!为此,我们需要使用以下命令创建language_model_learner fast.ai 类的一个实例:

learn = language_model_learner(data_lm, AWD_LSTM, drop_mult=0.3)     ❶

❶ 使用 30%的权重丢失率初始化预训练的权重丢失 LSTM。这是在 WikiText-103 基准数据集上预训练的。

这里,AWD_LSTM代表ASGD 权重丢失 LSTM。⁴这只是通常的 LSTM 架构,其中一些权重已被随机丢弃,就像通常的 dropout 层对神经网络激活所做的那样,与权重相反。这是最类似于 fast.ai 库中原始 ULMFiT 论文中所做的架构选择。此外,如果您检查上一个命令的执行日志,您应该能够确认它还从在 WikiText-103 基准数据集上训练的检查点加载预训练权重。⁶这个数据集,官方称为“WikiText 长期依赖语言建模数据集”,是一组由人类判断为“好”的维基百科文章。这是一个很好的、干净的无监督数据来源,已被许多自然语言处理论文用于基准测试。

现在我们已经加载了一个模型实例和一些预训练权重,我们将尝试确定用于微调语言模型的最佳或最优学习率。fast.ai 中一个称为lr_find的巧妙实用方法可以自动为我们完成这项工作。它会迭代一系列学习率,并检测结果损失与学习率曲线上损失函数下降最快的点。等价地,这是损失梯度最小的地方。⁷我们可以使用我们的语言模型学习器learn快速进行如下操作:

learn.lr_find()                        ❶
learn.recorder.plot(suggestion=True)   ❷

❶ 寻找最佳/最优学习率

❷ 绘制它

结果的损失与学习率曲线,突出显示了最佳率,如图 9.1 所示。

09_01

图 9.1 fast.ai 库用于语言模型微调步骤中的最佳学习率找寻过程的结果,用于虚假新闻检测示例。通过多次迭代不同的学习率,并选择在曲线上损失下降最快的点作为最佳学习率。

我们可以通过以下命令程序化地检索这个学习率,并显示它:

rate = learn.recorder.min_grad_lr    ❶
print(rate)                          ❷

❶ 检索最佳率

❷ 显示它

在我们执行代码时,返回的最佳学习率约为 4.0e-2。

找到最佳学习率后,我们现在可以使用下面的命令对我们的预训练的权重丢弃 LSTM 模型进行微调,使用 fit_one_cycle fast.ai 命令如下所示:

learn.fit_one_cycle(1, rate)      ❶

❶ 这个命令在底层使用了斜三角形学习率。它以 epochs 的数量和期望的最大学习率作为输入。

执行命令,在单个 Kaggle GPU 上进行大约 26 秒的微调,得到了 0.334 的准确度。

获得了基准值后,我们想要找出是否差异化微调能够带来改善。我们首先通过使用 unfreeze 命令解冻所有层,然后使用 slice 方法指定学习率范围的上限和下限。这个命令将最接近输出的层的最大学习率设置为上限,并通过除以一个常数因子几何地减少每个后续层的最大学习率到下限。下面展示了执行这个操作的确切代码:

learn.unfreeze()                               ❶
learn.fit_one_cycle(1, slice(rate/100,rate))   ❷

❶ 确保所有层都解冻以进行微调

❷ 在最终层中的最佳率和比该最佳率小两个数量级的值之间以几何方式变化

从代码可以看出,我们任意选择了将学习率从最大最优值变化到比该值小两个数量级的值。这个调度背后的直觉是,随后的层包含的信息更为一般化,与任务无关,因此它应该比最接近输出的层从这个特定目标数据集中学到的更少。

执行所提出的差异化微调代码,得到了一个准确度分数为 0.353,明显优于我们在没有使用它时得到的 0.334 的值。使用以下命令保存微调后的语言模型以供以后使用:

learn.save_encoder('fine-tuned_language_model')

通过斜三角形学习率和差异化微调调整了我们的预训练语言模型后,让我们看看我们能得到多好的目标任务分类器——也就是虚假新闻检测器。我们在下一小节对微调后的语言模型之上微调一个分类器。

9.1.2 目标任务分类器微调

请回想在前一小节中,我们创建了一个用于目标任务分类器的数据消费对象。我们将这个变量称为data_clas。作为微调我们的目标任务分类器的下一步,我们需要实例化一个分类器学习器的实例,方法恰当地命名为text_classifier_learner,在 fast.ai 中。下面的代码完成了这一步:

learn = text_classifier_learner(data_clas, AWD_LSTM, drop_mult=0.3)   ❶
learn.load_encoder('fine-tuned_language_model')                       ❷

❶ 实例化目标任务分类器学习的一个实例。使用我们微调过的语言模型相同的设置,因此我们可以无问题地加载。

❷ 载入我们微调过的语言模型

作为下一步,我们再次使用实用的 fast.ai 方法lr_find来找到最佳学习率,使用以下代码:

learn.lr_find()                           ❶
learn.recorder.plot(suggestion=True)      ❷

❶ 寻找最佳速率

❷ 绘制它

执行该代码得到的是图 9.2 中显示的损失与学习率曲线。

09_02

图 9.2 从 fast.ai 库获取目标任务分类器微调步骤中用于找到最佳学习率的结果的过程。通过几个学习率进行迭代,并选择最佳学习率,即在曲线上损失下降最快的点。

我们看到最佳速率约为 7e-4。我们使用倾斜三角形学习率,通过以下代码对分类器学习器进行一轮训练:

rate = learn.recorder.min_grad_lr      ❶
learn.fit_one_cycle(1, rate)           ❷

❶ 提取最佳的最大学习率

❷ 使用确定的最大学习率在倾斜三角形学习率计划中微调目标任务分类器

执行该代码得到的准确率约为 99.5%。这已经比我们在第六章(第 6.2 节)通过在 ELMo 嵌入之上训练分类器得到的 98%+的结果更好了。我们还能做些什么来进一步提高它呢?

幸运的是,我们还有一个底牌:渐进式解冻。再次提醒,这是当我们仅解冻一层,微调它,解冻一个额外的较低层,微调它,并重复此过程一定次数时。ULMFiT 的作者发现,在目标任务分类器阶段应用此方法显着改善了结果。举个简单的例子,要执行此过程直到 2 层深度,我们需要以下代码:

depth = 2                           ❶
for i in range(1,depth+1):          ❷
    learn.freeze_to(-i)             ❸
    learn.fit_one_cycle(1, rate)    ❹

❶ 我们仅执行渐进式解冻,直到解冻两个层为止。

❷ 逐渐解冻更多层,首先一个,然后两个,每次使用倾斜三角形学习率进行一轮训练

❸ 此命令解冻了顶部 i 层。

❹ 执行一次倾斜三角形学习率,如已经介绍的

请注意,命令 learn.freeze_to``(-i) 冻结前 i 层对于本次操作至关重要。在我们对虚假新闻检测示例上执行代码时,我们发现在第一步中准确性达到了 99.8%,当解冻了前两层时,准确性达到了惊人的 100%。这些结果充分说明了自己,似乎表明 ULMFiT 方法是一套非常有用的技术。请注意,如果有必要,我们可以继续解冻更深层次的层次——第 3 层,第 4 层等等。

奇妙的事情!看来在我们适应新场景时,聪明地调整模型可以带来显著的好处!在接下来的章节中,我们将介绍另一种实现这一点的方法——知识蒸馏。

9.2 知识蒸馏

知识蒸馏是一种神经网络压缩方法,旨在教授一个较小的学生模型大型教师模型所包含的知识。这种方法近年来在 NLP 社区中变得流行,本质上是试图通过学生来模仿教师的输出。此方法也与模型无关——教师和学生可以是基于变压器的、基于循环神经网络的或其他结构,并且彼此之间可以完全不同。

在 NLP 领域中,对此方法的最初应用是由于对双向 LSTM(bi-LSTMs)的表示能力与基于变压器的架构之间的比较的疑问。⁸ 作者想要知道单个 bi-LSTM 层是否能够捕捉到 BERT 的多少信息。令人惊讶的是,研究人员发现,在某些情况下,预训练的基于变压器的语言模型的参数数量可以减少 100 倍,推理时间可以减少 15 倍,同时不损失标准性能指标。这是一个巨大的尺寸和时间上的减少,可以决定这些方法是否可以实际部署!知识蒸馏的过程在图 9.3 中简要概述。

09_03

图 9.3 是知识蒸馏的一般过程的示意图。教师模型产生的“软”标签被用于通过蒸馏损失鼓励学生模型表现出类似的行为。同时,学生损失被训练成与通过学生损失的标准地面真实情况行为类似。

如图所示,传统上,教师产生的标签被用于计算“软”标签,通过与学生的输出进行比较来确定蒸馏损失。这种损失促使学生模型跟踪教师模型的输出。此外,学生还通过学生损失同时学习“硬”的真实标签。我们将通过 Hugging Face 的 transformers 库来快速展示如何使用这个想法实现。

已经提出了几种架构来减小预训练的 NLP 语言模型的尺寸,包括 TinyBERT ⁹ 和 DistilBERT. ¹⁰ 我们选择专注于 DistilBERT,因为它在 transformers 库中已经准备就绪。 DistilBERT 是由 Hugging Face 开发的,这是与编写 transformers 库相同的团队。 与以前一样,我们对这个主题的覆盖并不意味着是全面的,而是举例说明。 在像这样快速发展的领域中保持进一步开发和文献的更新仍然很重要。 我们希望这里所呈现的内容能让您做到这一点。

DistilBERT 研究的目标是特别生成 BERT 模型的较小版本。 学生架构被选择为与 BERT 相同-在第 7 和第八章中描述的堆叠变压器编码器。 学生的层数减少了一半,只有六层的模型。 这是大部分尺寸节省的地方。 作者发现在这种框架中,内部隐藏维度的变化对效率几乎没有影响,因此,在教师和学生之间都是相似的。 过程的一个重要部分是将学生初始化到适当的一组权重,从中收敛会相对较快。 因为教师和学生的所有层的尺寸都是相似的,作者可以简单地使用对应层中的预训练教师权重来初始化学生,并发现这样做效果良好。

作者对 GLUE 等基准进行了广泛的实验证明,我们将在下一章中看到,并在 SQuAD 上进行了实验证明。 他们发现,由结果产生的 DistilBERT 模型在 GLUE 基准上的性能保持了 BERT 教师模型的 97% ,但参数个数只有教师的 40%. 它在 CPU 上的推理时间也快了 60%,而在 iPhone 等移动设备上快了 71%。 如您所见,这是一项明显的改进。

执行实际蒸馏的脚本可在官方 transformers 存储库中找到。¹¹ 要训练自己的 DistilBERT 模型,你需要创建一个每行一个文本样本的文件,并执行该页面提供的一系列命令,这些命令准备数据并蒸馏模型。因为作者已经提供了各种检查点可供直接加载——所有检查点都列在了该页面上——而我们的重点是迁移学习,我们在这里不重复从头开始训练的步骤。相反,我们使用了一个类似于我们在第八章中用于跨语言迁移学习实验的 mBERT 检查点。这样可以直接比较使用蒸馏架构与原始 mBERT 的性能和好处,同时还教会你如何开始在自己的项目中使用这个架构。这也为你提供了另一个机会,即在自定义语料库上微调预训练的基于 transformer 的模型——直接修改具有不同架构、预训练检查点和自定义数据集的代码应该适用于你自己的用例。

更具体地说,我们将重复我们在第 8.2.2 节中进行的实验,即通过在来自 JW300 数据集的语料库上进行微调,将 mBERT 中包含的知识转移到单语 Twi 场景中。我们执行使用检查点中包含的预训练 tokenizer 的实验变体,而不是从头开始训练一个新的,为了简单起见。

9.2.1 使用预训练 tokenizer 将 DistilmBERT 转移到单语 Twi 数据

在本小节中,我们的目标是从一个在超过 100 种语言上训练过的模型中生成一个用于加纳语 Twi 的 DistilBERT 模型,不包括 Twi 在内。BERT 的多语言等效版本称为 mBERT;因此,DistilBERT 的多语言等效版本可预见地称为 DistilmBERT。这个 DistilmBERT 模型直接类比于我们在第八章中实验过的 mBERT 模型。我们当时发现,即使 Twi 没有包含在原始训练中,从这个检查点开始是有益的。在这里,我们基本上会复制相同的步骤序列,将每个 mBERT 实例替换为 DistilmBERT。这样可以直接比较两者,并因此直观地了解知识蒸馏的好处,同时学习如何在自己的项目中使用 DistilBERT。与之前一样,我们会在 JW300 数据集的单语 Twi 子集上对模型进行微调。¹²

我们首先初始化一个 DistilBERT tokenizer,使用 DistilmBERT 模型的预训练检查点。这次我们使用 cased 版本,如下所示:

from transformers import DistilBertTokenizerFast                                                           ❶
tokenizer = DistilBertTokenizerFast.from_pretrained("distilbert-base-multilingual-cased")                  ❷

❶ 这只是 DistilBertTokenizer 的一个更快的版本,你可以用它来代替。

❷ 使用预训练的 DistilmBERT tokenizer

准备好 tokenizer 后,将 DistilmBERT 检查点加载到 DistilBERT 掩码语言模型中,并按照以下方式显示参数的数量:

from transformers import DistilBertForMaskedLM                                                         ❶

model = DistilBertForMaskedLM.from_pretrained("distilbert-base-multilingual-cased")                    ❷

print("Number of parameters in DistilmBERT model:")
print(model.num_parameters())

❶ 使用掩码语言建模

❷ 初始化为 mBERT 检查点

输出表明,与我们在第八章中发现的 BERT 模型的 178.6 百万个参数相比,该模型具有 1.355 亿个参数。 因此,DistilBERT 模型的大小仅为等效 BERT 模型的 76%。

接下来,使用 transformers 中方便的 LineByLineTextDataset 方法从单语 Twi 文本构建数据集,具体方法如下所示:

from transformers import LineByLineTextDataset

dataset = LineByLineTextDataset(
    tokenizer=tokenizer,
    file_path="../input/jw300entw/jw300.en-tw.tw",    ❶
    block_size=128)                                   ❷

❶ 我们在第 8.2.1 节中介绍的英语到 Twi JW300 数据集

❷ 一次读取多少行

随后,按照下面的代码片段中所示的方式定义“数据集整理器”——这是一个帮助程序,它将一批样本数据行(长度为 block_size)创建成一个特殊对象,这个特殊对象可以被 PyTorch 用于神经网络训练:

from transformers import DataCollatorForLanguageModeling

data_collator = DataCollatorForLanguageModeling(
    tokenizer=tokenizer,
    mlm=True, mlm_probability=0.15)        ❶

❶ 使用掩码语言建模,并掩码单词的概率为 0.15

在这里,我们使用了掩码语言建模的方法——将我们输入数据中的 15% 的单词随机掩码,要求模型在训练过程中进行预测。

接下来,按照以下方式定义标准的训练参数,例如输出目录(我们选择为 twidistilmbert)和训练批次大小:

from transformers import TrainingArguments

training_args = TrainingArguments(
    output_dir="twidistilmbert",
    overwrite_output_dir=True,
    num_train_epochs=1,
    per_gpu_train_batch_size=16,
    save_total_limit=1,
)

然后,使用已定义的数据集和数据整理器定义“训练器”,并在数据上进行一个训练时代,具体方法如下。请记住,Twi 数据包含超过 600,000 行,因此在所有数据上进行一遍训练是相当费力的!

trainer = Trainer(
    model=model,
    args=training_args,
    data_collator=data_collator,
    train_dataset=dataset,
    prediction_loss_only=True)

最后,按照以下方式进行训练并计算训练所需的时间:

import time
start = time.time()
trainer.train()
end = time.time()
print("Number of seconds for training:")
print((end-start))

一如既往地,一定要保存模型:

trainer.save_model("twidistilmbert")

我们发现,与第八章中等效教师完成每个时代所需的 3 小时相比,该模型花费了大约 2 小时和 15 分钟完成该时代。 因此,学生的训练时间只有老师的 75%。 显著提高!

此外,损失函数的值达到了约 0.81,而 mBERT 的等效模型在第八章中的损失为约 0.77。就绝对值而言,性能差异可以粗略地量化为大约 5%——我们看到 DistilBERT 达到了 BERT 性能的 95%。 这非常接近 DistilBERT 作者在论文中报告的基准数字 97%。

最后一步,从语料库中取出以下句子:“Eyi de ɔhaw kɛse baa sukuu h*ɔ。” 掩盖一个单词,sukuu(在 Twi 中表示“学校”),然后将管道 API 应用于以下预测所删除的单词:

from transformers import pipeline

fill_mask = pipeline(                                ❶
    "fill-mask",
    model="twidistilmbert",
    tokenizer=tokenizer)

print(fill_mask("Eyi de ɔhaw kɛse baa [MASK] hɔ."))   ❷

❶ 定义了填空管道

❷ 预测掩码标记

这会产生以下输出:

[{'sequence': '[CLS] Eyi de ɔhaw kɛse baa fie hɔ. [SEP]', 'score': 0.31311026215553284, 'token': 29959}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa me hɔ. [SEP]', 'score': 0.09322386980056763, 'token': 10911}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa ne hɔ. [SEP]', 'score': 0.05879712104797363, 'token': 10554}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa too hɔ. [SEP]', 'score': 0.052420321851968765, 'token': 16683}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa no hɔ. [SEP]', 'score': 0.04025224596261978, 'token': 10192}]

这确实是可信的完成。值得注意的是,我们在第 8.2.2 节中看到的结果中的宗教偏见似乎已经在模型中得到了缓解。像“以色列”和“伊甸园”这样的完成,在第 8.2.2 节的 mBERT 等价模型中建议,现在已经不再存在了。这可以通过两者之间参数数量的显著差异来解释。由于这一点,DistilBERT 不太可能过拟合,而 BERT 则更有可能这样做。

现在你知道如何在自己的项目中使用 DistilBERT 了!我们再次强调,你刚刚进行的练习教会了你如何在自定义语料库上微调预训练的基于 transformer 的模型——只需修改代码以应用于你自己的用例,包括不同的架构、预训练检查点和自定义数据集。

在下一章的第一节中,我们将有机会在英语中再次微调一个基于 transformer 的模型,这次是在自定义语料库上进行!我们将讨论 ALBERT 架构背后的适应性思想——一种轻量级的 BERT——并将其微调到来自 Multi-Domain Sentiment Dataset 的一些评论中。回想一下,在第四章我们玩过这个数据集。这是亚马逊 25 个产品类别的评论数据集,我们将重点关注书评,就像第四章一样。

摘要

  • ULMFiT 的策略,如倾斜三角形学习率、差异微调逐步解冻,可以导致明显更有效的迁移。

  • 执行知识蒸馏一个较大的教师BERT 模型会产生一个明显更小的学生BERT 模型,性能损失最小。

  1. nlp.fast.ai/ulmfit

  2. V. Sanh 等人,“DistilBERT,BERT 的精简版本:更小、更快、更便宜、更轻”,EMC²:与 NeurIPS 合办的第 5 版(2019 年)。

  3. www.kaggle.com/azunre/tlfornlp-chapter9-ulmfit-adaptation-fast-aiv2

  4. S. Merity 等人,“正则化和优化 LSTM 语言模型”,ICLR(2018 年)。

  5. nlp.fast.ai/ulmfit

  6. www.salesforce.com/ca/products/einstein/ai-research/the-wikitext-dependency-language-modeling-dataset/

  7. L. Smith 等人,“神经网络超参数的一种纪律方法:第一部分——学习率、批大小、动量和权重衰减”,arXiv(2018 年)。

  8. R. Tang 等人,“从 BERT 中提炼任务特定知识到简单神经网络”,arXiv(2018 年)。

  9. X. Jiao 等人,“TinyBERT:BERT 的精炼”,arXiv(2020 年)。

  10. V. Sanh 等人,“DistilBERT,BERT 的精简版本:更小、更快、更便宜、更轻”,EMC²:与 NeurIPS 合办的第 5 版(2019 年)。

  11. github.com/huggingface/transformers/blob/master/examples/research_projects/distillation

  12. opus.nlpl.eu/JW300.php

  13. www.cs.jhu.edu/~mdredze/datasets/sentiment/

第十章:ALBERT,适配器和多任务适配策略

本章介绍

  • 对嵌入因子分解和层间参数共享进行应用

  • 在多个任务上对 BERT 系列模型进行微调

  • 将迁移学习实验分成多个步骤

  • 对 BERT 系列模型应用适配器

在上一章中,我们开始介绍了到目前为止我们所涵盖的深度 NLP 迁移学习建模架构的一些适配策略。换句话说,给定一个预训练的架构,如 ELMo、BERT 或 GPT,如何更有效地进行迁移学习?我们涵盖了 ULMFiT 方法背后的两个关键思想,即区分性微调逐渐解冻的概念。

我们在本章中将要讨论的第一个适配策略围绕着两个目标展开,旨在创建更有利于具有更大词汇量和更长输入长度的基于 transformer 的语言模型。第一个想法实质上涉及巧妙的因子分解,或者将更大的权重矩阵分解为两个较小的矩阵,使您可以增加一个的维度而不影响另一个的维度。第二个想法涉及在所有层之间共享参数。这两个策略是 ALBERT 方法的基础,即 A Lite BERT。我们使用 transformers 库中的实现来获得这种方法的一些实际经验。

在第四章中,我们介绍了多任务学习的概念,即模型被训练为同时执行多种任务。由此产生的模型通常对新场景更具泛化能力,并且可能导致更好的迁移效果。毫不奇怪,这个想法在预训练的 NLP 语言模型的适配策略的背景下再次出现。当面临转移场景时,没有足够的训练数据来微调给定任务时,为什么不在多个任务上进行微调呢?讨论这个想法为介绍(GLUE)数据集提供了一个很好的机会:一个包含了几个代表人类语言推理任务的数据集。这些任务包括检测句子之间的相似性、问题之间的相似性、释义、情感分析和问答。我们展示了如何利用 transformers 库快速进行多任务微调使用这个数据集。这个练习还演示了如何在一个来自这些重要问题类别的自定义数据集上类似地微调 BERT 系列模型。

在第四章中,我们还讨论了领域自适应,在那里我们发现源域和目标域的相似性对于迁移学习的有效性起着至关重要的作用。更大的相似性通常意味着更容易的迁移学习过程。当源和目标过于不相似时,你可能会发现在一个步骤中执行该过程是不可能的。在这种情况下,可以使用“顺序适应”的概念将整体所需的转移分解成更简单、更易管理的步骤。例如,一个语言工具在西非和东非之间无法转移,但可以先在西非和中非之间成功转移,然后在中非和东非之间转移成功。在本章中,我们将“填空”目标预训练 BERT 顺序适应到一个低资源句子相似度检测场景中,首先适应到一个数据丰富的问题相似度场景。

我们将探讨的最终适应策略是使用所谓的适应模块适配器。这些是预训练神经网络层之间只有少量参数的新引入模块。对于新任务微调这个修改后的模型只需要训练这几个额外的参数。原始网络的权重保持不变。通常情况下,当每个任务只增加 3-4% 的额外参数时,与微调整个模型相比,性能几乎没有损失。这些适配器也是模块化的,并且很容易在研究人员之间共享。

10.1 嵌入因子分解和跨层参数共享

我们在本节讨论的适应策略围绕着两个想法,旨在创建具有更大词汇表和更长最大输入长度的基于 transformer 的语言模型。第一个想法基本上涉及将一个更大的权重矩阵巧妙地分解为两个较小的矩阵,使得其中一个可以在不影响另一个维度的情况下增加维度。第二个想法涉及在所有层之间共享参数。这两种策略是 ALBERT 方法的基础。我们再次使用 transformers 库中的实现来获取一些与该方法有关的实际经验。这既可以让你对所获得的改进有所了解,也可以让你有能力在自己的项目中使用它。我们将使用第四章中的 Multi-Domain Sentiment Dataset 中的亚马逊图书评论作为我们这次实验的自定义语料库。这将使您能够进一步体验在自定义语料库上微调预训练的基于 transformer 的语言模型,这次是用英语!

第一个策略,即嵌入因子分解,受到了观察的启发,即在 BERT 中,输入嵌入的大小与其隐藏层的维度密切相关。分词器为每个标记创建一个 one-hot 编码的向量——该向量在与标记对应的维度上等于 1,在其他维度上等于 0。这个 one-hot 编码向量的维度等于词汇表的大小,V。输入嵌入可以被看作是一个维度为V乘以E的矩阵,将 one-hot 编码的向量乘以它并投影到大小为E的维度中。在早期的模型(如 BERT)中,这等于隐藏层的维度H,因此这个投影直接发生在隐藏层中。

这意味着当隐藏层的大小增加时,输入嵌入的维度也必须增加,这可能非常低效。另一方面,ALBERT 的作者观察到,输入嵌入的作用是学习上下文无关的表示,而隐藏层的作用是学习上下文相关的表示——这是一个更难的问题。受此启发,他们提出将单一输入嵌入矩阵分成两个矩阵:一个是V乘以E,另一个是E乘以H,允许HE完全独立。换句话说,one-hot 编码的向量可以首先投影到较小尺寸的中间嵌入中,然后再馈送到隐藏层。即使隐藏层的尺寸很大或需要扩展,这也使得输入嵌入可以具有显着较小的尺寸。仅此设计决策就导致将投影 one-hot 嵌入向量到隐藏层的矩阵/矩阵的尺寸减少了 80%。

第二个策略,即跨层参数共享,与我们在第四章中讨论的软参数共享多任务学习场景相关。在学习过程中,通过对它们施加适当的约束,鼓励所有层之间的相应权重彼此相似。这起到了正则化的效果,通过减少可用自由度的数量来降低过拟合的风险。这两种技术的结合使得作者能够构建出在当时(2020 年 2 月)超越了 GLUE 和 SQuAD 记录性能的预训练语言模型。与 BERT 相比,在参数大小上实现了约 90%的减少,而性能只有轻微的下降(在 SQuAD 上不到 1%)。

再次,因为多种检查点可用于直接加载,我们不在此重复从头开始的训练步骤,因为我们的重点是迁移学习。相反,我们使用类似于我们在前一章和第八章中用于我们的跨语言迁移学习实验的“基础”BERT 检查点。这使我们能够直接比较使用这种架构与原始 BERT 的性能和效益,并教你如何开始在自己的项目中使用这种架构。

10.1.1 在 MDSD 书评上对预训练的 ALBERT 进行微调

我们准备数据的步骤与第 4.4 节中的步骤相同,我们在此不再重复。这些步骤也在本书附带的 Kaggle 笔记本中重复出现。我们从列表 4.6 生成的变量data开始。假设与第 4.4 节相同的超参数设置,这是一个由 2,000 本书评文本组成的 NumPy 数组。

使用以下代码将这个 NumPy 数组写入 Pandas 到文件中:

import pandas as pd

train_df = pd.DataFrame(data=data)
train_df.to_csv("albert_dataset.csv")

我们首先初始化一个 Albert 分词器,使用基本 ALBERT 模型中的预训练检查点,如下所示。我们使用版本 2 是因为它是目前可用的最新版本。你可以在 Hugging Face 网站上随时找到所有可用的 ALBERT 模型列表。⁶

from transformers import AlbertTokenizer                          ❶
tokenizer = AlbertTokenizer.from_pretrained("albert-base-v2")     ❷

❶ 加载 ALBERT 分词器

❷ 使用预训练的 ALBERT 分词器

准备好分词器后,将基础 ALBERT 检查点加载到 ALBERT 遮盖语言模型中,并显示参数数量如下:

from transformers import AlbertForMaskedLM                    ❶

model = AlbertForMaskedLM.from_pretrained("albert-base-v2")   ❷

print("Number of parameters in ALBERT model:")
print(model.num_parameters())

❶ 使用遮盖语言建模

❷ 初始化到 ALBERT 检查点

输出表明模型有 1180 万个参数——与第八章的 BERT 的 178.6 万个参数和直接 BERT 的 135.5 万个参数相比,这是一个巨大的缩小。事实上,这是与 BERT 模型相比的 15 倍缩小。哇!

然后,像之前一样,使用 transformers 中提供的方便的LineByLineTextDataset方法,使用单语 Twi 文本中的分词器构建数据集,如下所示:

from transformers import LineByLineTextDataset

dataset = LineByLineTextDataset(
    tokenizer=tokenizer,
    file_path="albert_dataset.csv",
    block_size=128)                    ❶

❶ 每次读取多少行

定义一个“数据收集器”——一个帮助方法,将一个样本数据行批量(block_size长度)创建成一个特殊对象——如下所示。这个特殊对象可以被 PyTorch 用于神经网络训练:

from transformers import DataCollatorForLanguageModeling

data_collator = DataCollatorForLanguageModeling(
    tokenizer=tokenizer,
    mlm=True, mlm_probability=0.15)       ❶

❶ 使用遮盖语言建模,并用 0.15 的概率遮盖单词

在这里,我们使用了以 15%的概率对我们的输入数据进行随机遮盖的遮盖语言建模,并要求模型在训练过程中对它们进行预测。

定义标准的训练参数,如输出目录和训练批量大小,如下代码片段所示。注意,这一次我们训练 10 次,因为数据集比前一章中使用的超过 60,000 个单语 Twi 样本要小得多:

from transformers import Trainer, TrainingArguments

training_args = TrainingArguments(
    output_dir="albert",
    overwrite_output_dir=True,
    num_train_epochs=10,
    per_gpu_train_batch_size=16,
    save_total_limit=1,
)

然后,使用之前定义的数据集和整理器来定义一个“训练器”,以跨数据进行一个训练 epoch,如下所示:

trainer = Trainer(
    model=model,
    args=training_args,
    data_collator=data_collator,
    train_dataset=dataset,
    prediction_loss_only=True,
)

按照以下步骤训练并计时训练时间:

import time
start = time.time()
trainer.train()
end = time.time()
print("Number of seconds for training:")
print((end-start))

在这个小数据集上,10 个 epochs 大约只需约五分钟就能完成训练。损失值达到约 1。

按以下方式保存模型:

trainer.save_model("albert_fine-tuned")

最后,让我们按照以下步骤应用管道 API 来预测虚构书评中的遮蔽词:

from transformers import pipeline

fill_mask = pipeline(                                      ❶
    "fill-mask",
    model="albert_fine-tuned",
    tokenizer=tokenizer
)

print(fill_mask("The author fails to [MASK] the plot."))   ❷

❶ 定义填空管道

❷ 预测遮蔽的标记

这产生了以下非常合理的输出:

[{'sequence': '[CLS] the author fails to describe the plot.[SEP]', 'score': 0.07632581889629364, 'token': 4996}, {'sequence': '[CLS] the author fails to appreciate the plot.[SEP]', 'score': 0.03849967569112778, 'token': 8831}, {'sequence': '[CLS] the author fails to anticipate the plot.[SEP]', 'score': 0.03471902385354042, 'token': 27967}, {'sequence': '[CLS] the author fails to demonstrate the plot.[SEP]', 'score': 0.03338927403092384, 'token': 10847}, {'sequence': '[CLS] the author fails to identify the plot.[SEP]', 'score': 0.032832834869623184, 'token': 5808}]

到目前为止,您可能已经观察到,我们在此处对自定义书评语料库对 ALBERT 进行微调的步骤序列与我们在上一章中使用 DistilBERT 的步骤序列非常相似。这一系列步骤反过来又与我们在第八章中使用的 mBERT 的步骤序列非常相似。我们再次强调,这个配方可以用作 transformers 中几乎任何其他架构的蓝图。虽然我们无法提供在每种可能的应用类型上微调的示例,但这个配方应该可以推广,或者至少作为许多用例的良好起点。例如,考虑一种情况,您想要教 GPT-2 以某种选择的风格写作。只需复制我们在这里使用的相同代码,将数据集路径指向您选择的写作风格的语料库,并将标记器和模型引用从 AlbertTokenizer / AlbertForMaskedLM 更改为 GPT2Tokenizer / GPT2LMHeadModel

需要注意的一点是,所有 PyTorch transformers 模型默认情况下都会解冻所有层进行训练。要冻结所有层,您可以执行以下代码片段:

for param in model.albert.parameters():
    param.requires_grad = False

您可以使用类似的代码片段仅冻结一些参数。

在下一节中,我们将讨论多任务微调,我们将有另一个机会来看看这些类型模型的微调,这次是针对各种任务。

10.2 多任务微调

在第四章的第三部分中,我们介绍了多任务学习的概念,其中模型被训练执行各种任务,而不仅仅是一个任务。结果模型通常对新场景更具一般性,并且可以实现更好的转移和性能。毫不奇怪,这个想法再次出现在预训练 NLP 语言模型的适应策略的背景下,微调在多个任务上的模型观察到更加健壮和有效。⁷

我们在这里讨论这个想法提供了一个很好的机会来介绍通用语言理解评估(GLUE)数据集,⁸这是一个包含几个人类语言推理代表性任务数据的集合。这个数据集包括检测句子相似性、问题相似性、释义、情感分析和问题回答等任务。在本节中,我们演示了如何快速利用 transformers 库对我们讨论的各种基于变压器的预训练模型在 GLUE 数据集的各种任务上进行微调。这个练习还演示了如何按类似方式微调来自 BERT 系列的模型,以解决 GLUE 中包含的重要问题类别之一的自定义数据集。

我们还演示了顺序适应——将总体所需的转移实验过程分解成更简单、更易管理的步骤的过程。考虑一个假设的情景,即基于语言的工具在西非和东非之间无法完成转移——首先它可能在西非和中非之间成功转移,然后在中非和东非之间成功转移。这与多任务微调的想法相关,因为它本质上是按顺序进行的,一步接一步进行。与通常理解的多任务微调方法不同,顺序适应首先在一个任务上进行微调,然后再在另一个任务上进行微调。

在本节中,我们通过在 GLUE 数据集的几个任务上对一些预训练的基于变压器的语言模型进行多任务微调和顺序适应来演示。具体来说,我们关注的是一个被称为Quora 问题对(QQP)任务的问题相似度任务,以及用于衡量一对句子之间相似性的语义文本相似性基准(SST-B)任务。

10.2.1 通用语言理解数据集(GLUE)

通用语言理解数据集(GLUE)旨在提供一系列多样的自然语言理解任务的具有挑战性的基准数据集。这些任务被选中,以代表多年来在自然语言处理领域研究人员之间达成的一种关于什么构成有趣、具有挑战性和相关问题的隐含共识。在表 10.1 中,我们总结了数据集中可用任务和每个任务的数据计数。

表 10.1 原始通用语言理解数据集(GLUE)中提供的任务、描述和数据计数列表

任务名称 数据量 描述
语言可接受性语料库(CoLA) 训练 8,500,测试 1,000 确定一个英语句子是否符合语法规范
斯坦福情感树库(SST2) 训练 67,000,测试 1800 检测给定句子的情感-积极或消极
Microsoft Research Paraphrase Corpus (MRPC) 3,700 train, 1,700 test 确定一个句子是否是另一个句子的释义
Semantic Textual Similarity Benchmark (STS-B) 7,000 train, 1,400 test 预测一对句子之间的相似度分数,范围在 1 到 5 之间
Quora Question Pairs (QQP) 3,640,000 train, 391,000 test 确定一对 Quora 问题是否语义上等同
Multi-Genre Natural Language Inference (MultiNLI) 393,000 train, 20,000 test 确定一个前提句子是暗示/蕴含还是与一个假设句子相矛盾
Question-Answering Natural Language Inference (QNLI) 105,000 train, 5,400 test 检测上下文句子是否包含对问题的答案
Recognizing Textual Entailment (RTE) 2,500 train, 3,000 test 测量前提和假设之间的文本蕴含关系,类似于 MultiNLI
Winograd Schema Challenge (WNLI) 634 train, 146 test 确定模棱两可的代词指的是一组可能选项中的哪一个名词

从表中可以看出,原始的 GLUE 数据集涵盖了各种任务,并且可用的数据量不同。这是为了鼓励不同任务之间的知识共享,这也是我们在本章节中探讨的多任务微调理念的核心。接下来我们简要描述表中的各项任务。

前两个任务——Corpus of Linguistic Acceptability (CoLA) 和 Stanford Sentiment Treebank (SST2)——是单句任务。前者试图确定一个给定的英文句子是否语法正确,而后者试图检测句子中表达的情感是积极还是消极。

以下三项任务——Microsoft Research Paraphrase Corpus (MRPC)、Semantic Textual Similarity Benchmark (STS-B) 和 Quora Question Pairs (QQP)——被归类为相似性任务。这些任务涉及以各种方式比较两个句子。MRPC 试图检测一个句子是否是另一个句子的释义,即是否表达了相同的概念。STS-B 在连续范围 1 到 5 之间测量一对句子的相似度。QQP 试图检测一个 Quora 问题是否等同于另一个。

剩下的四个任务被分类为推理任务。多体裁自然语言推理(MultiNLI)任务试图确定给定的句子是否暗示另一个句子或是否与之矛盾――它衡量蕴涵问答自然语言推理(QNLI)任务类似于我们讨论并在第八章中用于说明问答的 SQuAD⁹数据集。提醒一下,该数据集由上下文段落、对其提出的问题以及答案在上下文段落中的开始和结束位置指示符组成,如果存在的话。QNLI 基本上将这个想法转化为一个句对任务,通过将每个上下文句子与问题配对,并尝试预测答案是否在该上下文句子中。识别文本蕴涵(RTE)任务类似于 MultiNLI,因为它衡量两个句子之间的蕴涵关系。最后,Winograd Schema Challenge(WNLI)数据集试图检测一个含糊指代词在句子中指代可用选项中的哪个名词。

自 GLUE 成立以来,还引入了另一个名为 SuperGLUE¹⁰的数据集。这个新版本是必要的,因为最近的现代方法在 GLUE 的许多部分几乎达到了完美的性能。SuperGLUE 的开发是为了更具挑战性,因此为比较方法提供更多的“动态范围”。我们在这里关注 GLUE,但我们认为在您成为 NLP 专家时,牢记 SuperGLUE 的存在是很重要的。

接下来,我们将以 QQP 和 STS-B GLUE 任务做一些实验,作为本节的说明性示例。首先,在下一小节中,我们展示如何对我们提出的任何任务中的一个任务进行微调预训练的 BERT。我们强调,虽然在这种情况下,我们使用 STS-B 作为示例微调任务,但对于任何呈现的任务,相同的步骤序列直接适用。我们还提醒您,此练习是为了准备您在自己的自定义数据集上对 BERT 进行微调,该数据集来自我们提出的任何任务类别。

10.2.2 在单个 GLUE 任务上进行微调

在本小节中,我们看到如何快速微调 transformers 家族中的预训练模型,以处理 GLUE 基准集中的任务。回想一下,BERT 是在“填空”和“下一个句子预测”目标上进行预训练的。在这里,我们进一步微调这个预训练的 BERT 来处理 GLUE 数据上的 STS-B 相似性任务。这个练习作为如何在 GLUE 的任何其他任务以及属于这些重要问题类别之一的任何自定义数据集上进行操作的示例。

我们要做的第一件事是克隆 transformers 存储库,并使用以下代码安装必要的要求:

!git clone --branch v3.0.1 https:/ /github.com/huggingface/transformers  ❶
!cd transformers
!pip install -r transformers/examples/requirements.txt                  ❷
!pip install transformers==3.0.1                                        ❸

❶ 克隆(指定版本的)transformers 存储库

❷ 安装必要的要求

❸ 为了可重现性,固定 transformers 版本

请注意在我们的 Kaggle 笔记本中忽略依赖冲突消息——这些消息与我们在此处使用的库无关,只要您复制我们的笔记本而不是从头开始创建一个新的。

接下来,按以下方式下载 GLUE 数据:

!mkdir GLUE
!python transformers/utils/download_glue_data.py --data_dir GLUE --tasks all❶

❶ 下载所有任务的 GLUE 数据

这会创建一个名为 GLUE 的目录,其中包含一个子目录,该子目录以每个 GLUE 任务命名,并包含该任务的数据。我们可以按以下方式查看 GLUE/STS-B 中包含的内容:

!ls GLUE/STS-B 

这产生了以下输出:

LICENSE.txt  dev.tsv  original    readme.txt  test.tsv  train.tsv

此外,我们可以用以下方式查看一部分 STS-B 训练数据:

!head GLUE/STS-B/train.tsv

这产生以下输出:

index genre   filename year old_index source1 source2 sentence1 sentence2 score

0    main-captions    MSRvid    2012test    0001    none    none    A plane is taking off.    An air plane -is taking off.    5.000

1    main-captions    MSRvid    2012test    0004    none    none    A man is playing a large flute.    A man is playing a flute.    3.800

2    main-captions    MSRvid    2012test    0005    none    none    A man is spreading shreddedcheese on a pizza.    A man is spreading shredded cheese on an uncooked pizza.    3.800

3    main-captions    MSRvid    2012test    0006    none    none    Three men are playing chess.    Two men are playing chess.    2.600

4    main-captions    MSRvid    2012test    0009    none    none    A man is playing the cello.A man seated is playing the cello.    4.250

5    main-captions    MSRvid    2012test    0011    none    none    Some men are fighting.    Two men are fighting.    4.250

6    main-captions    MSRvid    2012test    0012    none    none    A man is smoking.    A man is skating.    0.500

7    main-captions    MSRvid    2012test    0013    none    none    The man is playing the piano.    The man is playing the guitar.    1.600

8    main-captions    MSRvid    2012test    0014    none    none    A man is playing on a guitar and singing.    A woman is playing an acoustic guitar and singing.    2.200

在继续之前,我们注意到,为了使用这里讨论的脚本来在您自己的自定义数据上对模型进行精细调优,您只需要将您的数据转换为所示格式并指定脚本所在的位置即可!

要在 STS-B GLUE 任务上对“vanilla” bert-base-cased BERT checkpoint 进行三轮训练——批量大小为 32,最大输入序列长度为 256,学习率为 2e-5——我们执行以下命令:

%%time                                                            ❶
!python transformers/examples/text-classification/run_glue.py --model_name_or_path bert-base-cased --task_name STS-B --do_train --do_eval --data_dir GLUE/STS-B/ --max_seq_length 256 --per_gpu_train_batch_size 32 --learning_rate 2e-5 --num_train_epochs 3.0 --output_dir /tmp/STS-B/

❶ 这是 Jupyter 笔记本中计时的“魔法”命令。

这一操作执行时间不超过 10 分钟。请注意,在代码中,我们指定了输出目录为 /tmp/STS-B/。该文件夹包含了经过精细调优的模型和评估结果。然后,为了查看所取得的性能,我们只需执行以下命令将结果打印到屏幕上:

!cat /tmp/STS-B/eval_results_sts-b.txt

这产生以下输出:

eval_loss = 0.493795601730334
eval_pearson = 0.8897041761974835
eval_spearmanr = 0.8877572577691144
eval_corr = 0.888730716983299

这些代表了用于此问题的度量标准的最终数值,即皮尔逊相关系数和斯皮尔曼相关系数。不深入细节,这些系数衡量了数据集中提供的真实相似度与我们在测试集上精细调优模型获得的相似度之间的相关性。这些系数的较高值表明了更好的模型,因为它们与真实结果的关联更大。我们看到对于这两个系数都达到了接近 89% 的性能。在撰写本文时(2020 年 10 月初),当前的 GLUE 排行榜¹¹显示,在全球范围内,排名前 20 的性能大约在 87% 到 93% 之间变化。这些排名靠前的性能也在 GLUE 的其他任务上表现良好,尽管我们目前只对一个任务进行了精细调优。但我们可以快速取得如此接近最新技术水平的性能仍然令人印象深刻。请注意从表 10.1 中得知,用于此任务的训练数据量仅为 7,000 个样本。

在下一小节中,我们将进一步在另一个任务——Quora 问题对(QQP)上对模型进行精细调优,并进一步阐明多任务学习和顺序适应的概念。

10.2.3 顺序适应

在本小节中,我们将看到在 STS-B 任务上进行微调之前,在 Quora 问答对(QQP)任务上进行微调是否会产生更好的性能。请回顾表 10.1,其中 QQP 有 364,000 个训练样本,而 STS-B 有 7,000 个样本。显然,QQP 具有更多的数据。首先在 QQP 上训练可以被解释为应用一种顺序适应多任务学习策略来处理一个低资源的场景,其中训练数据量不理想:只有 7,000 个样本。

我们开始本练习,假设已经克隆了 transformers 存储库,已安装了必要的要求,并已下载了 GLUE 数据,如前一小节所示。现在,要做的下一件事是在 QQP GLUE 任务上对“普通”的bert-base-cased BERT 检查点进行微调,一次迭代,批处理大小为 32,最大输入序列长度为 256,学习率为 2e-5。请注意,这次我们只使用一个迭代,而不是前一小节中的三个,因为训练数据现在要大得多。现在每个迭代(涉及一次通过训练集)涵盖了 364,000 个样本,我们认为这已足够。我们使用以下代码:

!python transformers/examples/text-classification/run_glue.py --model_name_or_path bert-base-cased --task_name QQP --do_train --do_eval --data_dir GLUE/QQP/ --max_seq_length 256 --per_gpu_train_batch_size 32 --learning_rate 2e-5 --num_train_epochs 1 --output_dir /tmp/QQP/

训练时长约为 2 小时 40 分钟。与以前一样,我们可以检查 QQP 任务上的性能如下:

!cat /tmp/QQP/eval_results_qqp.txt

这达到了以下性能:

eval_loss = 0.24864352908579548
eval_acc = 0.8936433341578036
eval_f1 = 0.8581700639883898
eval_acc_and_f1 = 0.8759066990730967
epoch = 1.0

然后我们可以按以下方式加载 QQP 微调的模型:

from transformers import BertForSequenceClassification, BertConfig     ❶

qqp_model = BertForSequenceClassification.from_pretrained("/tmp/QQP")  ❷

❶ 初始化为我们的微调模型检查点

❷ 这次使用序列分类,因为这是问题的形式

在加载了微调模型之后,让我们提取其编码器,以便我们可以在后续模型中使用它,然后可以进一步在 STS-B 任务上进行微调。请注意,这类似于我们在第四章中分析的硬参数共享场景。我们在图 10.1 中说明了这种情况。

10_01

图 10.1 我们在本节探讨的硬参数共享多任务学习场景。模型首先在 QQP 上进行微调,这是一个数据丰富的场景,然后是 STS-B,这是一个资源稀缺的场景。这个实验的顺序性质将其分类为顺序适应。

图中清楚地显示了任务之间共享的编码器。编码器被提取并用于初始化一个模型,以进行在 STS-B 上的微调,代码片段如下:

shared_encoder = getattr(qqp_model, "bert")                  ❶

configuration = BertConfig()
configuration.vocab_size = qqp_model.config.vocab_size       ❷
configuration.num_labels = 1                                 ❸

stsb_model = BertForSequenceClassification(configuration)    ❹

setattr(stsb_model, "bert", shared_encoder)                  ❺

❶ 获取经过微调的 QQP 模型编码器

❷ 确保 STS-B 配置的词汇量和输出大小设置一致

❸ STS-B 是一个回归问题,只需要一个输出;QQP 是一个二元分类任务,因此有两个输出。

❹ 用与 QQP 类似的设置初始化 STS-B 模型

设置其编码器为 QQP 编码器

将初始化的 STS-B 模型保存以供进一步微调,方法如下:

stsb_model.save_pretrained("/tmp/STSB_pre")

确保从 QQP 模型中获取了词汇表,如下所示:

!cp /tmp/QQP/vocab.txt /tmp/STSB_pre 

现在,使用与前一小节相同的设置,在 STS-B 上微调先前微调的 QQP 模型,操作如下:

!python transformers/examples/text-classification/run_glue.py --model_name_or_path /tmp/STSB_pre --task_name STS-B --do_train --do_eval --data_dir GLUE/STS-B/ --max_seq_length 256 --per_gpu_train_batch_size 32 --learning_rate 2e-5 --num_train_epochs 3 --output_dir /tmp/STS-B/

这三个训练时代只需大约七分半钟即可执行,只有 7,000 个训练集大小。像往常一样,我们使用以下内容来检查获得的性能:

!cat /tmp/STS-B/eval_results_sts-b.txt

观察到以下性能:

eval_loss = 0.49737201514158474
eval_pearson = 0.8931606380447263
eval_spearmanr = 0.8934618150816026
eval_corr = 0.8933112265631644
epoch = 3.0

我们已经取得了比之前的小节更好的表现,只是在 STS-B 上进行微调。那里的eval_corr约为 88.9%,而我们在这里达到了 89.3%。因此,连续适应的多任务学习实验被证明具有益处,并导致性能的可衡量提高。

在下一节中,我们将探讨是否可以比我们在这里做得更加高效地将模型微调到新的情况。我们将研究在预训练语言模型的层之间引入称为适应模块或适配器的方法,以适应新情况。这种方法很有前途,因为引入的参数数量非常少,可以有效地预训练和分享 NLP 社区。

10.3 适配器

我们探索的下一个适应策略是使用所谓的适配模块或适配器。它们背后的关键思想如图 10.2 所示,介绍了它们作为第七章中图 7.6 中香草变压器编码器中的附加层。

10_02

图 10.2 在“香草”变压器编码器的图 7.6 中新引入的适配器层

如图所示,这些适配器是预训练神经网络层之间仅有几个参数的新引入模块。为了将修改后的模型微调到新任务上,只需要训练这些少量的额外参数——原始网络的权重保持不变。与微调整个模型相比,通常仅添加每个任务 3-4%的额外参数,几乎没有性能损失。¹² 实际上,这些额外参数相当于大约 1 兆字节的磁盘空间,这在现代标准下非常低。

这些适配器是模块化的,允许易于扩展和研究人员之间的经验共享。实际上,一个名为 AdapterHub¹³的项目是在我们使用的 transformers 库上构建的,旨在成为共享这些模块的中央存储库。在该部分中,我们将使用此项目构建在斯坦福情感树库(SST2)任务上微调的 BERT 模型。这相当于我们在先前的小节中微调 STS-B GLUE 子集所做的事情,将使您迅速了解适配器框架所提供的优势与我们之前所做的有何不同。

让我们按以下方式安装 AdapterHub 库:

pip install adapter-transformers

导入所需类并仅使用三行代码加载所需的适配器,如下所示:

from transformers import BertForSequenceClassification, BertTokenizer
model = BertForSequenceClassification.from_pretrained("bert-base-uncased") ❶
model.load_adapter("sentiment/sst-2@ukp")                                  ❷

❶要微调的检查点

❷ 任务特定的适配器选择规范

适配器和使用说明在 AdapterHub 网站上列出 ¹⁴ 这就是我们要对 BERT 检查点适应 SST2 情感分类任务所做的一切。将这与上一节的微调步骤进行比较,就可以显而易见地看出适配器方法的实用性。我们无需进行微调,只需加载附加的模块即可继续前进!

请注意,在我们的代码中,我们使用了bert-base-uncased检查点,并且我们要加载的适配器是在 UKP 句子论证挖掘语料库上进行了微调 ¹⁵,这是因为目前 AdapterHub 存储库中只有部分可用的内容。AdapterHub 是一个早期项目,我们预计随着时间的推移会提供更多的适配器。在撰写本文的 2020 年 10 月,已经有接近 200 个适配器可用。¹⁶

作为本节和本章的最终行动,让我们通过以下代码片段来确信我们构建的模型实际上作为情感分类引擎运行。我们使用以下两个句子的情感进行比较:“那是非常出色的贡献,好!”和“那对环境非常糟糕。”

import torch

tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")            ❶
tokensA = tokenizer.tokenize("That was an amazing contribution, good!")   ❷
input_tensorA = torch.tensor([tokenizer.convert_tokens_to_ids(tokensA)])
tokensB = tokenizer.tokenize("That is bad for the environment.")          ❸
input_tensorB = torch.tensor([tokenizer.convert_tokens_to_ids(tokensB)])
outputsA = model(input_tensorA,adapter_names=['sst-2'])                   ❹
outputsB = model(input_tensorB,adapter_names=['sst-2'])                   ❺
print("The prediction for sentence A - That was an amazing contribution, good! - is:")
print(torch.nn.functional.softmax(outputsA[0][0]))                        ❻
print("The prediction for sentence B - That is very bad for the environment. - is:")
print(torch.nn.functional.softmax(outputsB[0][0]))                        ❼

❶ 使用常规预训练的分词器

❷ 句子 A

❸ 句子 B

❹ 进行了 A 的预测

❺ 进行了 B 的预测

❻ 显示了句子 A 的预测概率

❼ 显示了句子 B 的预测概率

这产生了以下输出:

The prediction for sentence A - That was an amazing contribution, good! - is:
tensor([0.0010, 0.9990], grad_fn=<SoftmaxBackward>)
The prediction for sentence B - That is very bad for the environment. - is:
tensor([0.8156, 0.1844], grad_fn=<SoftmaxBackward>)

所示的预测可以被解释为一对概率,第一个指示了输入是“负面”的概率,第二个是“正面”的概率。我们看到句子“那是非常出色的贡献,好!”有 99.9%的强烈正面概率。另一方面,句子“那对环境非常糟糕。”则是负面的,概率为 81.6%。这当然是合理的,并验证了我们的实验。

总结

  • 应用嵌入因子分解和层间参数共享可以产生更加参数高效的模型。

  • 在 BERT 系列模型上同时进行多任务微调,也就是多任务微调,会产生更具一般性的模型。

  • 在 BERT 系列模型上使用适配器可以简化微调。

  1. Z. Lan 等人,“ALBERT:自监督学习语言表示的 Lite BERT”,ICLR(2020)。

  2. A. Wang 等人,“GLUE:自然语言理解多任务基准和分析平台”,ICLR(2019)。

  3. N. Houlsby 等人,“NLP 参数高效迁移学习”,ICML(2019)。

  4. Z. Lan 等人,“ALBERT:自监督学习语言表示的 Lite BERT”,ICLR(2020)。

  5. www.cs.jhu.edu/~mdredze/datasets/sentiment/

  6. huggingface.co/models?filter=albert

  7. X. Liu 等,“用于自然语言理解的多任务深度神经网络”,ACL 会议记录(2019)。

  8. A. Wang 等,“GLUE:自然语言理解的多任务基准和分析平台”,ICLR(2019)。

  9. P. Rajpurkar 等,“SQuAD:用于机器文本理解的 100,000+问题”,arXiv(2016)。

  10. A. Wang 等,“GLUE:自然语言理解的多任务基准和分析平台”,ICLR(2019)。

  11. gluebenchmark.com/leaderboard

  12. N. Houlsby 等,“用于 NLP 的参数高效迁移学习”,ICML(2019)。

  13. adapterhub.ml/

  14. adapterhub.ml/explore

  15. mng.bz/7j0e

  16. adapterhub.ml/explore

第十一章:结论

本章包括

  • 总结本书涵盖的重要概念

  • 总结相关的重要新兴概念

  • 考虑自然语言处理中关于迁移学习方法的局限性以及环境和伦理考虑

  • 展望自然语言处理中的迁移学习未来

  • 跟上该领域的最新发展

在前面的章节中,我们涵盖了大量的材料——我们希望它们既具信息性又引人入胜。这一结论性章节试图对我们所做的一切进行有意义的总结,并展望该领域的未来和新兴的研究趋势。由于该领域的多产产出和快速发展的性质,我们当然没有涵盖每一个有影响力的架构或有前途的研究方向。为了减轻这一点,我们对我们在本书中没有机会涵盖的各种研究趋势进行了简要讨论,尽可能与已涵盖的材料进行联系和框架化。

在本章中,我们还试图通过涉及一些传统上没有受到太多关注的新兴问题,例如伦理考虑和各种模型的环境影响,提供更广泛的背景。这些与对这些模型的局限性的认识密切相关,我们在本章中尽可能突出这一点。

至关重要的是,我们讨论了在这样一个快速发展的领域中保持最新的各种提示。强调掌握了本书内容后,您现在只是开始您在该领域的旅程。所提出的工具和技能会随时间变化,并且它们的每个独特应用可能需要您的创造力或者尚未开发的新技术。在这样一个快速发展的领域中保持竞争优势确实是一次旅程,而不是一个目的地。我们鼓励读者对正在进行的研究保持探究的态度,并在某种程度上继续为其发展做出贡献。

让我们通过概述关键概念来开始这一最后一章。

11.1 关键概念概述

迁移学习旨在利用不同设置中的先前知识——无论是不同的任务、语言还是领域——来帮助解决手头的问题。它受到人类学习方式的启发,因为我们通常不会从头开始学习任何给定的问题,而是建立在可能相关的先前知识上。使没有实质计算资源的从业者能够达到最先进的性能被认为是向民主化获得正在进行的技术革命成果的重要一步。作为更具体的动机,考虑一下训练不同大小的 BERT 模型的代表性成本,如图 11.1 所示。¹

11_01

图 11.1 BERT 不同规模的训练成本。展示了两种代表性成本——单次运行和包括超参数调整在内的整个训练过程。15 亿参数的最大规模单次运行成本为$80k,而所有优化步骤计算下的成本为$1.6 百万!

正如图所示,最大规模的 BERT 训练成本可能高达数百万美元。迁移学习确实可以让您在几小时内,最坏情况下花费几美元用于微调,将这些宝贵的知识重复利用于您的个人计算项目中。

在计算机视觉中推广的迁移学习最近开始被自然语言处理(NLP)社区大量使用。而计算机视觉涉及教计算机如何理解和处理图像和视频,NLP 则考虑如何处理人类语音,无论是文本还是语音音频。在本书中,我们关注的是文本。我们特别感兴趣的一些 NLP 任务包括文档分类、机器翻译和问答。

尽管在历史上,这些任务最初是通过试图为每种情况制定固定规则来解决的——这种范式现在被称为符号 AI——但机器学习现在已成为主导趋势。计算机不再为每种可能的情况明确编程,而是通过看到许多这种相应的输入-输出对的示例来训练计算机将输入与输出信号相关联。传统上用于学习适当的输入-输出关系的方法包括决策树、随机森林、诸如 SVM 的核方法和神经网络。神经网络最近已成为解决感知问题(即计算机视觉和 NLP)的表示学习方法的首选。因此,这是我们在本书中探讨的最重要的方法类别。

在深入研究现代 NLP 的迁移学习方法之前,我们进行了一项关于传统机器学习方法的回顾性实验。具体来说,我们采用了以下方法:

  • 逻辑回归

  • 支持向量机

  • 随机森林

  • 梯度提升机

以解决两个重要问题:电子邮件垃圾检测和互联网电影数据库(IMDB)电影评论分类。为了将文本转换为数字,我们使用了词袋模型。该模型简单地计算了每封电子邮件中包含的单词标记的频率,从而将其表示为这些频率计数的向量。

现代自然语言处理(NLP)方法学主要集中在将文本部分(词语、子词、句子等)向量化上,采用诸如 word2vec 和 sent2vec 之类的技术。然后将得到的数值向量进一步处理,作为传统机器学习方法的特征,例如用于随机森林分类。

正如本书第一章所概述的,这一重要的自然语言处理研究子领域起源于 20 世纪 60 年代的信息检索术语向量模型。这在预训练的浅层神经网络技术方面达到了高潮,包括以下内容:

  • fastText

  • GloVe

  • word2vec,在 2010 年代中期推出了几个变体,包括连续词袋(CBOW)和 Skip-Gram

CBOW 和 Skip-Gram 都来自于训练用于各种目标的浅层神经网络。Skip-Gram 试图预测滑动窗口中任何目标词附近的单词,而 CBOW 试图预测给定邻居的目标词。GloVe,代表“全局向量”,试图通过将全局信息合并到嵌入中来扩展 word2vec。它优化了嵌入,使得单词之间的余弦乘积反映它们共同出现的次数,从而使得结果向量更具可解释性。fastText 技术试图通过对字符 n-gram(而不是单词 n-gram)重复 Skip-Gram 方法来增强 word2vec,从而能够处理以前未见过的单词。这些预训练嵌入的每个变体都有其优点和缺点。作为这类方法的数值演示,我们使用 fastText 词嵌入来重新访问 IMDB 电影分类示例,那里将词袋模型替换为 fastText 以将文本转化为数字。

几种技术受 word2vec 的启发,试图将较大的文本部分嵌入到向量空间中,以便在诱导向量空间中含义类似的文本部分彼此靠近。这使得可以在这些文本部分上进行算术运算,以进行关于类比、组合含义等推理。这样的方法包括以下内容:

  • 段落向量,或doc2vec,利用了从预训练词嵌入中摘要单词的连接(而不是平均)。

  • Sent2vec扩展了 word2vec 的经典连续词袋(CBOW)—其中一个浅层神经网络被训练以从其上下文中的滑动窗口中预测一个词—到通过优化词和词 n-gram 的嵌入来对句子进行准确的平均表示。

作为这类方法的数值演示,我们使用了一个基于 fastText 而非词袋模型的 sent2vec 的实现来执行 IMDB 电影分类实验。

一些作者², ³, ⁴提出了各种分类系统,将迁移学习方法归类到不同的组别中。粗略地说,分类是基于迁移是否发生在不同的语言、任务或数据领域之间。通常,这些分类类型对应着以下内容:

  • 跨语言学习

  • 多任务学习

  • 领域自适应

我们进行了一系列多任务迁移学习实验,使用了 IMDB 分类和电子邮件垃圾邮件检测这些熟悉的任务来说明这个概念。为了通过示例说明领域自适应,我们使用了自动编码器来调整一个在 IMDB 电影评论分类上训练的模型,以适应亚马逊图书评论的领域。这个练习还允许我们说明了零-shot 迁移学习的一个实例,即在亚马逊图书评论领域不需要微调就可以开始提供有价值的结果。

序列到序列建模的进展为诸如机器翻译之类的任务带来了革命。该设置中的编码器和解码器最初是循环神经网络(RNNs)。由于输入序列过长的问题,发展了一种称为注意力的技术,允许输出仅关注输入的相关部分。尽管最初这与 RNNs 结合使用,但它发展成为了使用自注意力构建编码器和解码器的技术。自注意力与最初的注意力公式不同,因为它寻求序列的部分与同一序列的其他部分之间的关联,而不是两个不同输入和输出序列的部分之间的关联。自注意力取代注意力的架构被称为 变压器,它在并行计算架构上比早期基于 RNN 的序列到序列模型更具可伸缩性。这种改进的可扩展性推动了它在竞争架构中的广泛采用。我们使用了一个预训练的英文到加纳语 Twi 的翻译变压器模型来探索这一重要架构的效能和其他特性。

NLP 的迁移学习的早期探索侧重于与计算机视觉的类比,而计算机视觉在此方面已经成功使用了一段时间。其中一个模型——SIMOn——采用了字符级卷积神经网络(CNNs)结合双向 LSTM 用于结构语义文本分类。SIMOn 代表 本体建模的语义推理。它是在 DARPA 的数据驱动模型发现(D3M)⁵ 计划中开发的,该计划是为了自动化数据科学家面临的一些典型任务。它展示了与计算机视觉中使用的方法直接类似的 NLP 迁移学习方法。该模型学到的特征还被证明对无监督学习任务也有用,并且在社交媒体语言数据上表现良好,这些数据可能有些特殊,与维基百科和其他大型基于书籍的数据集上的语言非常不同。列类型分类被用作该建模框架的说明性示例。

作为提醒,计算机视觉中微调的启发式大致如下:

  • 随着目标域中的数据越来越多,阈值从输出中移动(并朝向输入)。在阈值和输出之间的参数被解冻并进行训练,而其余参数保持不变。这是由于增加的数据量可以有效地用于训练更多的参数,而否则无法完成。

  • 另外,阈值的移动必须远离输出并朝向输入,因为这样可以保留编码靠近输入的通用特征的参数,同时重新训练更靠近输出的层,这些层编码源域特定特征。

  • 此外,当源和目标高度不同时,一些更具体的参数/层可以完全丢弃。

早期嵌入方法(如 word2vec)的一个主要弱点是消歧义 - 区分一个词的各种用法,这些用法根据上下文可能有不同的含义。这些词在技术上被称为同形异义词,例如,duck(姿势)与 duck(鸟)和 fair(集会)与 fair(公平)。来自语言模型的嵌入 - 在流行的Sesame Street角色之后缩写为 ELMo - 是最早尝试开发单词的上下文化嵌入的方法之一,使用双向长短期记忆网络(bi-LSTMs)。ELMo 可以说是与正在进行的 NLP 迁移学习革命相关联的最流行的早期预训练语言模型之一。它与 SIMOn 具有许多架构相似之处,后者由字符级 CNNs 和 bi-LSTMs 组成。这个模型中一个词的嵌入取决于其上下文,ELMo 通过被训练来预测单词序列中的下一个单词来实现这一点。大量数据集,如维基百科和各种图书数据集,被用来训练这个模型。我们将 ELMo 应用于一个说明性的示例问题,即假新闻检测,作为一个实际演示。

通用语言模型微调(ULMFiT)进一步提出了一种方法,为任何特定任务微调基于神经网络的语言模型。该框架介绍并演示了一些关键的技术和概念,以更有效地适应预训练语言模型的新设置。这些包括区分性微调和渐进解冻。区分性微调规定,因为语言模型的不同层包含不同类型的信息,它们应该以不同的速率进行微调。渐进解冻描述了一种逐渐地以渐进方式微调更多参数的过程,目的是减少过拟合的风险。ULMFiT 框架还包括在适应过程中以独特方式改变学习率的创新。我们使用 fast.ai 库对这些概念进行了数值上的说明。

  • OpenAI 的生成式预训练变压器(GPT)修改了变压器的编码器-解码器架构,以实现 NLP 的可微调语言模型。它丢弃了编码器,保留了解码器及其自注意子层。它是以因果建模目标进行训练的——预测序列中的下一个词。它特别适用于文本生成。我们展示了如何使用 Hugging Face 的 transformers 库快速使用预训练的 GPT-2 模型进行文本生成,该库在本书中早已介绍过。

  • 从转换器的双向编码器表示(BERT)可以说是相反的,通过保留编码器并丢弃解码器来修改转换器架构,还依赖于掩码词语,然后需要准确预测作为训练度量的这些词语。更具体地说,它是以掩码建模目标训练的——填补空白。此外,它还通过下一个句子预测任务进行训练——确定给定句子是否是目标句子之后的一个合理的跟随句子。虽然不适用于文本生成,但该模型在其他一般语言任务(如分类和问题回答)上表现非常好。我们将其应用于问题回答和文档分类的两个重要应用。文档分类用例是垃圾邮件检测。我们还展示了它在填补空白和检测一个句子是否是另一个句子的合理的下一个句子方面的应用。

  • mBERT 模型,代表“多语言 BERT”,实际上是同时在 100 多种语言上预训练的 BERT。自然地,这个模型特别适合跨语言迁移学习。我们展示了多语言预训练权重检查点如何有助于为原本未包含在多语言训练语料库中的语言创建 BERT 嵌入。BERT 和 mBERT 都是由 Google 创建的。

  • 在所有这些基于语言模型的方法——ELMo、ULMFiT、GPT 和 BERT 中——都显示出生成的嵌入可以用相对较少的标记数据点进行特定下游 NLP 任务的微调。这解释了 NLP 社区对语言模型的关注:它验证了它们诱导的假设集通常是有用的。

我们还介绍了一些关于已覆盖的深度 NLP 迁移学习建模架构的适应策略。换句话说,针对预训练架构,如 ELMo、BERT 或 GPT,如何更有效地进行迁移学习?我们专注于参数效率,目标是在尽可能减少参数的情况下产生一个性能损失最小的模型。这样做的目的是让模型更小、更容易存储,从而更容易在智能手机等设备上部署。或者,智能的适应策略可能仅仅是为了在一些困难的转移情况下达到可接受的性能水平。我们介绍的适应策略有:

  • 我们探索的第一种适应策略是前面提到的 ULMFiT 技术,即逐步解冻区分微调,使用的是 fast.ai 库。

  • 然后我们探索了被称为知识蒸馏的模型压缩方法,因为它最近在 NLP 领域显赫一时。这个过程本质上试图通过显著更小的学生模型来模拟更大教师模型的输出。特别是,我们使用 transformers 库中知识蒸馏方法 DistilBERT 的实现来证明通过这种方式可以将 BERT 模型的大小减少一半以上。

  • 我们接触到的下一个适应策略围绕着两个想法,旨在创建更有利于更大词汇量和更长输入长度的基于变压器的语言模型。第一种方法涉及巧妙的因式分解,或者将一个更大的权重矩阵分解为两个较小的矩阵,允许你增加一个维度而不影响另一个维度。第二种想法涉及跨所有层共享参数。这两种策略是 ALBERT(A Lite BERT)方法的基础。我们使用 transformers 库中的实现来亲身体验这一方法。

因此,我们基于多任务学习的想法,即模型被训练以同时执行多种任务,并产生更具有普遍性的模型。当面临转移场景时,我们没有足够的训练数据来在给定任务上进行微调时,为什么不在多个任务上进行微调?讨论这个想法给我们提供了一个很好的机会来介绍通用语言理解评估(GLUE)数据集,这是一组代表人类语言推理的几项任务的数据集,例如检测句子之间的相似性,问题之间的相似性,释义,情感分析和问题回答。我们展示了如何使用这个数据集快速利用 transformers 库进行多任务微调。这个练习还演示了如何类似地在来自这些重要问题类别之一的自定义数据集上微调来自 BERT 系列的模型。

我们还建立在领域自适应的思想上,特别是源域和目标域的相似性对于迁移学习的有效性起着至关重要的作用。更大的相似性通常意味着一般情况下更容易进行迁移学习。当源域和目标域之间相差太大时,可能无法在单一步骤中执行该过程。在这种情况下,可能会使用顺序自适应的想法将所需的整体迁移分解为更简单、更易管理的步骤。举例来说,我们首先将一个“填空”目标预训练的 BERT 逐步适应到一个低资源的句子相似度检测场景中,再适应到一个数据丰富的问题相似度场景中。实验中两个场景的数据都来自 GLUE 数据集。

我们探讨的最终适应策略是使用所谓的适应模块适配器。这些是仅在预训练神经网络的层之间具有少量参数的新引入的模块。对于新任务微调这个修改过的模型只需要训练这几个额外的参数。原始网络的权重保持不变。与微调整个模型相比,通常只添加了 3-4% 的额外参数时往往几乎没有性能损失。这些适配器也是模块化的,并且容易在研究人员之间共享。我们使用了 AdapterHub 框架来加载其中一些适配器,并展示它们如何用于将通用 BERT 模型适应为在情感分类任务上表现良好的模型。

11.2 其他新兴研究趋势

在整本书中,我们试图强调,在诸如 NLP 的迁移学习这样的快速发展领域中,像这样一本单独的书籍完全涵盖每种架构或创新是不可能的。相反,我们采取的方法是专注于我们认为是基础的架构和技术。未来的创新很可能在某种程度上是从这些架构和技术中派生出来的,因此读者可能通过自己的一些努力来学习它们。为了进一步促进这一点,我们将这一部分重点放在了我们在这本书中没有涵盖但在该领域中已经有些影响的各种研究趋势的简要讨论上。我们尽可能将它们置于我们已经涵盖的内容的背景中,以便您在需要时更轻松地学习这些主题。

我们首先通过概述 RoBERTa⁷——Robustly Optimized BERT Approach——来开始这个练习,它采用了一些优化技巧来提高 BERT 的效率。

11.2.1 RoBERTa

所讨论的研究试图复制 BERT,同时特别关注各种训练超参数和设置以及它们对结果的影响。总的来说,观察到通过谨慎的设计选择可以显著改善原始 BERT 的性能。这样的选择之一是去除下一句预测(NSP)任务,同时保留掩码语言建模(MLM)— 填空— 任务。换句话说,他们发现 NSP 会降低下游任务的性能,并显示去除它是有益的。其他设计选择包括在训练过程中使用较大的学习率和小批量。它是由我们在本书中介绍的 Hugging Face 的 transformers 库中实现的。

接下来,我们将看看迄今为止开发的最大语言模型之一 — GPT-3,最近引起了很多关注,并在 NeurIPS 2020 虚拟研究会议(2020 年 12 月)上获得了最佳论文奖。

11.2.2 GPT-3

您可能还记得我们的报道,GPT 模型经历了几次迭代 — GPT、GPT-2,最近是 GPT-3。在撰写时,GPT-3 恰好是拥有 1750 亿参数的最大的预训练语言模型之一。它的前身 GPT-2 拥有 15 亿参数,在发布时也被认为是最大的,仅仅在前一年发布。在 2020 年 6 月发布 GPT-3 之前,最大的模型是微软的图灵 NLG,拥有 170 亿参数,并于 2020 年 2 月发布。这些指标的进展速度之快令人震惊,这些记录往往很快就会被打破。为了比较,这些参数爆炸在图 11.2 中有所体现。

11_02

图 11.2 模型参数数量随时间增长的趋势。如图所示,模型大小的爆炸趋势似乎在加速,其中最近的一个进步 — GPT-3 — 表示了 10 倍的增长因子。

如图所示,GPT-3 相比之前最大的图灵 NLG 增长了超过 10 倍,这是一次超越以往进步的飞跃。实际上,一种称为 Switch Transformer 的架构,通过为不同的输入分配单独的 transformer 块部分利用了稀疏性,并声称在 2021 年 1 月达到了 1 万亿参数的规模。由于在撰写时仍在进行同行评审,我们没有在图 11.2 中包含这种架构。然而,很明显,这种模型大小增长的趋势似乎正在加速。

在 GPT-3 论文中,作者展示了这个巨大的模型可以在很少的样本情况下执行广泛的任务。例如,只需看几个示例翻译,它就可以被启动以将一种语言翻译成另一种语言,或者只需看几个示例垃圾邮件,即可检测垃圾邮件。 事实上,一些意想不到的应用,例如从编码的描述中编写代码,已经被广泛报道。目前,该模型尚未由 OpenAI 作者发布给公众,只有通过邀请和付费 API 向少数早期采用者提供。 OpenAI 限制接入的理由是监控使用,从而限制这种技术的任何潜在有害应用。 GPT-3 的早期采用者是一款名为 Shortly 的应用程序,它为创意写作提供了 GPT-3 访问权限,任何人都可以以少量费用尝试。

此外,一个最近更小但功能强大的 GPT-3 开源替代品已经在 transformers 库中可用:EleutherAI 的 GPT-Neo。该组织旨在构建一个与全尺寸 GPT-3 相当的模型,并在开放许可下向公众提供。他们的存储库中提供了不同大小的模型,您也可以使用 Hugging Face 托管的推理 API,通过浏览器测试这些模型。我们还提供了伴随 Kaggle 笔记本,演示了我们在第七章中进行的练习的运行情况。通过检查,您应该会发现其性能更好,但自然而然地,成本也更高。 (最大型号的重量超过 10 GB!)

关于 GPT-3 工作的一个重要事项是,作者本人在论文中认识到,使语言模型更大的好处已接近极限。即便在极限下,处理某些类型的任务(例如关于对常识物理的理解的文本生成)的性能仍然较差。因此,虽然它确实代表了一项重要的技术突破,但在建模方法(而不是简单地扩大模型规模)上的创新必须成为前进的道路。

接下来,我们将看一组旨在改善基于 transformer 的模型在更长输入序列上的性能的方法。这很重要,因为香草 transformer 模型会随着输入长度呈平方级别的运行时间和内存使用。

11.2.3 XLNet

XLNet¹³是在类似早期模型 Transformer-XL¹⁴的基础上构建的,旨在更好地处理更长的输入序列。其中一个关键组成部分的想法是因果语言建模(CLM),我们在讨论 GPT 时已经讨论过,它涉及到预测序列中下一个词的经典语言建模任务。请注意,此方法中的未来标记已被屏蔽。XLNet 论文的作者等效地将其称为自回归语言建模。XLNet 的另一个关键组成部分是对输入序列的所有可能排列执行 CLM。这个想法有时被称为排列语言建模(PLM)。通过结合 PLM 和 CLM,实现了双向性,因为所有标记都有机会在某个排列中作为过去标记被包含。XLNet 和 Transformer-XL 都没有序列长度限制,并且由 Hugging Face 的 transformers 库实现。

有了这种对 XLNet 的看法,让我们继续考虑 BigBird¹⁵,这是一种引入稀疏注意机制的创新,以实现更高的计算效率。

11.2.4 BigBird

BigBird 通过引入一种稀疏注意机制将传统基于 Transformer 的模型的二次依赖关系减少到线性,该机制被证明能够近似并保持原始完整注意力的性质。与一次应用完整注意力到整个输入序列不同,稀疏注意力逐个查看序列标记,允许它更加智能地且舍弃一些连接。可以处理长达传统基于 Transformer 模型处理的八倍长度的序列,且可在类似硬件上处理。它是由 Hugging Face 的 transformers 库实现的。

接下来,我们将介绍 Longformer¹⁶,这是对 Transformer 传统完整自注意力的另一项创新,能够更好地随着输入长度的增加而扩展。

11.2.5 Longformer

Longformer 是针对传统 Transformer 注意力的二次缩放的又一尝试。这里的创新是将局部窗口注意力与全局任务导向注意力相结合。局部注意力用于上下文表示,而全局注意力用于构建在预测中使用的完整序列表示。所达到的缩放在输入序列长度方面是线性的,类似于 BigBird。Longformer 是由 Hugging Face 的 transformers 库实现的。

我们接下来介绍 Reformer¹⁷,这是另一种减轻原始自注意力二次缩放的方法。

11.2.6 Reformer

Reformer 引入了两种技术来对抗原始 Transformer 输入长度的二次扩展的计算时间和内存消耗。用局部敏感哈希替换原始的完全自注意力机制,减少了冗余计算和时间复杂度从二次到 O(LlogL)(其中 L 是输入序列长度)。一种称为可逆层的技术允许只存储激活一次。在实践中,这意味着,与为具有N层的模型存储激活N次相比,只使用了一小部分内存。根据N的值,内存节省可能非常大。Reformer 是由 Hugging Face 实现的 transformers 库中的一个模型。

显然,使基于 Transformer 的模型在处理更长的输入长度时表现更好已成为一个元研究趋势。我们在这里可能没有包括所有关于此主题的重要研究,如果你自己深入研究,可能会发现更多。

接下来,我们将谈论最近重新出现的序列到序列建模方法。这些尝试将本书中遇到的各种问题统一到一个文本到文本建模框架中。

11.2.7 T5

你可能还记得在本书中我们讨论过,序列到序列模型在自然语言处理中发挥了重要作用。首次出现在循环神经网络(RNN)模型的背景下,它们也被翻译应用领域在原始 Transformer 架构的背景下所探索。T5,“文本到文本转换 Transformer”,是将各种自然语言处理问题统一到一个序列到序列框架中的尝试。它允许对每个任务应用相同的模型、目标、训练过程和解码过程。处理的问题类别包括从摘要到情感分析和问答等众多领域。英语和罗马尼亚语、德语、法语之间的语言对翻译被包括在训练中。一些代表性的数据转换,使得可以在多种任务上训练单一模型,如图 11.3 所示的翻译和摘要(灵感来源于 T5 论文的图 1)。

11_03

图 11.3 T5 是一个序列到序列模型,它采用了一系列转换,使得可以同时在各种任务上训练单一模型、解码过程和目标。它可以被看作是多任务学习的一个有趣变体。

如图所示,任务数据通过在原始文本数据前加上标准任务描述符来转换。训练数据包括 GLUE 和 SuperGLUE 数据集、用于抽象摘要的 CNN/Daily Mail 数据集等。目标是处理包含的多样化的自然语言理解任务,而不修改模型。从这个意义上说,它可以被看作是我们在整本书中一直提到的多任务学习思想的一个有趣变体或迭代。同时学习如此多种任务的包含可能会实现参数共享和生成模型的更好泛化能力。关键是,模型最初是在作者称之为“巨大干净爬行语料库”(C4)的数据集上使用蒙版语言建模或自动编码目标进行训练,然后在上述各种任务上进行微调。基本上,所有标记的 15% 被丢弃,结果被送入输入,而未损坏的输入被送入输出以进行预测。请注意,C4 语料库本质上是目标语言(英语)的互联网,其中过滤掉了色情材料、代码和其他“垃圾数据”。用于训练的模型架构类似于我们在第七章中用于翻译的转换器架构。由所得模型在许多包含的任务上实现了最先进的结果。

除了原始的 T5 模型之外,还开发了一个多语言版本,不足为奇地称为 mT5,通过同时对 101 种语言进行训练。T5 和 mT5 都在 Hugging Face 的 transformers 库中实现。

接下来,我们简要介绍 BART,它与 T5 类似,都是基于转换器的序列到序列建模框架。

11.2.8 BART

BART,即双向自回归转换器,可以被视为 T5 减去单一统一变换的模型,以使未经修改的模型能够应用于各种任务。相反,首先对标准的转换器编码器-解码器架构进行预训练,以通过各种噪声方法重现损坏的输入。这包括蒙版语言建模,如 BERT 和 T5,以及排列语言建模,如 XLNet 等。然后,该模型被修改用于各种任务,例如 SQuAD、摘要等,并针对每个任务分别进行微调,类似于我们对传统 BERT 所做的操作。这个模型在语言生成任务中表现特别好,例如摘要、翻译和对话。同时也开发了一个多语言版本 mBART,通过同时对 25 种语言进行训练而获得。BART 和 mBART 都在 Hugging Face 的 transformers 库中实现。

在下一个小节中,我们将审视一种最新的跨语言模型,它不仅仅是同时在多种语言上进行训练,还通过在有并行数据时修改语言建模目标来显式建模跨语言转移。

11.2.9 XLM

XLM,²² 其作者用来指代“跨语言语言模型”,是一个结合单语和并行数据的跨语言学习方法的建模框架。在不同语言上学习得到的单语嵌入可以使用已知数值表示的小词汇表进行对齐。如果有并行数据可用,作者提出了一种他们称之为翻译语言建模(TLM)的方法,并同时利用它进行跨语言学习。本质上,这涉及将并行数据的连接序列应用掩码语言建模,在两种语言的连接序列的各个部分中,让一些单词消失并预测它们。

在跨语言学习任务中观察到显著的改进。它还激发了许多类似模型的产生,特别是 XLM-R,²³ 它将 XLM 的思想与 RoBERTa 的思想结合起来,以提高性能。XLM 和 XLM-R 都在 Hugging Face 的 transformers 库中实现。

最后,我们简要谈到了一种在书中遇到的重要问题数据类别——表格数据的专门模型。

11.2.10 TAPAS

在第五章和第六章中,我们讨论了 SIMOn 方法及其对表格数据类型分类的处理——这是数据科学家通常会遇到的一个重要问题类别。TAPAS²⁴ 是尝试将基于变换器的模型的建模优势扩展到这一重要问题类别的一种尝试,通过显式地为表格数据中的问答建模和专门化。TAPAS 代表表格解析器。在第八章中,我们讨论了将 BERT 应用于问答任务。结果专门化模型的输出是输入上下文段落中感兴趣问题的潜在答案的开始和结束位置。除此之外,TAPAS 还学会检测表格中哪个单元可能包含可以从中提取答案的上下文段落,且起始和结束索引类似。与本节中讨论的其他大多数模型一样,该模型在 Hugging Face 的 transformers 库中实现。

这标志着我们对本书中尚未有机会详细分析的近期工作的概述之旅的结束。这些模型架构大多可以通过与我们在 transformers 库中使用 BERT 和 DistilBERT 非常相似的代码来使用。

在下一节中,我们将尝试对这个领域下一步可能的走向做出有根据的猜测——鉴于当前和新兴的研究趋势,哪些主题可能保持或变得流行。

11.3 NLP 中转移学习的未来

在本节中,我们尝试通过预测该领域即将出现的形态来推断前面两节描述的趋势。

对过去两节的批判性分析揭示了两个可以说是正交的元趋势——一个是将模型尽可能地变大,另一个是开发更高效的更大模型的推动。

GPT-3,目前我们观察到的参数数量迈出的最大一步——是以前的 10 倍,最初引起了一些研究人员对研究公司开始将重点放在规模而不是巧妙建模上的担忧。然而,正如我们在上一节讨论的那样,扩大模型的局限性很快变得明显,GPT-3 论文的作者们承认已经达到了可能的极限。考虑到 GPT-3 目前仅通过有限的付费 API 可用,我们可以预期空间中的其他参与者将尝试很快构建更大的模型,因为他们有货币激励这样做(我们已经提到正在进行同行评审的拥有万亿参数的 Switch Transformer)。这场竞赛很可能最终将导致谷歌和/或 Facebook 发布一个类似的模型,这很可能会推动 GPT-3 完全开源(类似的情况在 GPT-2 历史上已经发生过)。除此之外,我们预计会有更多资源开始致力于实现类似性能的更有效方法。

在 NLP 迁移学习的即将到来的有趣问题中,大多数可能都在被一些人称为TinyML的运动中。这可以被定义为一个将模型大小缩小到可以适应较小硬件的通用目标。我们在第九章演示了一个例子,即通过 DistilBERT 等方法可以将 BERT 的大小大致缩小一半,而性能损失很小。我们在第十章取得了类似的成就的另一种方法是 ALBERT,它实现了模型大小的 90%缩减。现在全球大部分人口都拥有智能手机,可以运行这些先进模型的较小版本。这给物联网(IoT)等领域带来的机会是巨大的,其中设备形成智能网络,每个节点都能独立地执行复杂功能。尽管今天许多手机应用程序可能都包含翻译和其他工具的服务器后端,用于进行实际的翻译和其他计算,但在没有互联网连接的情况下在智能手机上本地运行这些算法的能力正在成为更可行和更普遍的范式。我们预计在未来几年内,在使 BERT 及其衍生产品更小和更具参数效率的努力将继续如火如荼。

另一个你可能从前一节中注意到的趋势是对跨语言模型的日益关注。事实上,过去一年全球对所谓的“低资源”语言的方法的投资有所增加。我们在第七章中通过一个例子提到了这一点,当时我们使用了一个变压器架构将低资源的西非语言特威(Twi)翻译成了英语。许多流行的经济模型预测,非洲市场正在出现一个日益重要的消费者群体,这很可能是引起对这一领域突然兴趣和投资的至少一个推动因素。对于许多低资源语言来说,应用我们讨论的所有方法的初始障碍往往是数据的可用性。因此,我们可以预期在接下来的一年左右,适当的多语言数据开发将受到很多关注,随后将进行对语言特定方法的深入研究。值得关注的地方,特别是涉及非洲语言的地方,包括 NLP Ghana,Masakhane,EthioNLP,Zindi Africa,AfricaNLP 和 Black in AI。

语音是另一个即将迎来转折时刻的 NLP 研究前沿。直到最近,自动语音识别模型,将语音转录成文本,需要大量的平行语音文本数据才能取得良好的结果。Facebook 最近的一种架构 Wav2Vec2 展示了,同时在许多语言上对语音进行预训练可以极大地减少所需的平行数据量。这类似于我们在本书中探讨的文本中使用的 mBERT 的功能。Wav2Vec2 模型已经在 transformers 库中提供,并且可以通过只使用几个小时的标注语音数据对新语言进行微调。我们预计这将在接下来的一年内首次推动多种语言的语音识别工具的开发。此外,我们预计类似的事情也将在不久的将来出现在另一个方向上:文本到语音,也就是从文本生成语音。

在第一章中,我们描述了 NLP 中的迁移学习是如何受到计算机视觉的进展的启发的。有趣的是,最近 NLP 迁移学习的进展似乎又激发了计算机视觉的进一步发展。一个具体的例子是 DALL-E,这是一个在文本描述 - 图像对上训练的 GPT-3 的版本,它已经学会了从文本提示中生成图像。一个更广泛的趋势是构建上下文情景的对象嵌入,试图从场景中其他可观察到的对象预测缺失的对象,类似于 BERT 和类似的遮蔽语言模型所使用的填空词目标。

另一个最近似乎越来越受到关注的研究问题是:这些模型的环境和道德影响是什么?在最近的研究高潮初期,研究人员似乎满足于发布只改善技术指标的模型,但随着时间的推移,这个领域开始重视对潜在道德影响的详细探讨。与之相关的是对可解释性的提高兴趣:我们能否真正解释模型是如何做出决定的,以确保它不会歧视?我们将在下一节进一步探讨这些道德问题。

11.4 道德和环境考虑

你可能还记得我们在第五章和第六章讨论关于假新闻检测的问题时提出了所谓的假新闻是什么是一个有争议的观点。如果在数据标签的质量上不加注意,那么准备训练数据标签的人身上植入的偏见很可能会转移到分类系统上。这是我们首次遭遇到了在部署这些模型到可能会显著影响人类生活的情况下,充分意识到潜在局限性的重要性。

当我们在第八章用 Jehovah's Witnesses 准备的 JW300 数据集对 mBERT 进行微调时,我们发现它会以有偏见的方式填补空白。当我们试图预测一个基本名词“学校”时,它会提供像伊甸园这样的词作为合理的补充。这表明了强烈的宗教偏见,这是我们在旅程中第二次被提醒到,盲目地将这些模型应用于某些数据可能会产生有偏见和意想不到的结果。

在这一部分,我们将从更广泛的角度讨论这个问题,考虑到可能需要放在从业者脑后的道德和环境考虑因素。这是一个近来受到越来越多关注的话题,但在机器学习领域并不算新鲜。

早期关于偏见的知名机器学习研究可预见地发生在计算机视觉领域。具有里程碑意义的作品,“性别和肤色”,³⁴研究了商业性别分类系统在种族和性别维度上的准确性。它发现,与较浅肤色的男性相比,这些系统在较深肤色的女性身上表现不佳,绝对百分比高达 35 个百分点。这对少数族裔社区有着巨大的实际影响,在一些地区可能会受到一些自动计算机视觉系统的监控。错误的分类或检测可能意味着错误的逮捕,即使被清除,也可能意味着在最脆弱的社区中失去工作。有多次广泛报道这种情况发生在真实的人身上。一个愤世嫉俗的力量失衡在那些打着“客观”和“科学”旗号的系统背后被揭露出来,在这些系统被开发的更富裕的社区和它们的经济利益主要是被搬走的地方并没有遭受到对较贫困的社区造成的伤害。这项工作及相关研究的影响是巨大的,最近美国国会最近出台了相关的减轻监管措施,可以说这是直接后果。像 IBM 和亚马逊这样的公司也被迫重新审视他们与执法机构分享这些技术的方式,IBM 甚至完全停止了这项服务。

最近对预训练的自然语言处理语言模型存在偏见的担忧也很高。实际上,GPT-3 论文³⁵专门包括了一个研究的部分,涵盖了几个方面,即种族、性别和宗教。最近越来越常见地看到学术文章做这样的研究,这是非常令人鼓舞的。特别是 GPT-3 的研究探讨了模型从训练数据中学到的与各个关注维度的关联。例如,他们发现通常与较高教育水平相关联的职业在填空时更倾向于与男性代词相关联。同样,暗示专业能力的提示更有可能由男性代词和说明者完成。这很可能是模型直接从互联网学到的性别偏见,我们可能无法指望互联网是一个无偏见的信息来源。另一方面,积极的描述词更有可能被“亚洲”和“白人”等词引导的名词以比“黑人”人物高得多的速度分配。同样,模型显然从互联网学到了种族偏见,而对模型的盲目应用只会传播这种偏见。在宗教维度上,“伊斯兰”一词与“恐怖主义”一词相关联,是最有可能的完成之一。作为这种偏见的直接现实影响,考虑一下那位巴勒斯坦人的善意“早上好”Facebook 帖子被错误地翻译为“攻击他们”,并导致了重大的不公平后果。³⁶

预训练的自然语言处理语言模型可能无意中影响贫困社区的另一种方式是通过气候变化。实际上,这些模型最近被发现具有相当大的碳足迹。³⁷, ³⁸虽然发现单个 BERT 模型的一次训练的碳足迹相当于纽约到旧金山的一个普通往返航班,但在微调和超参数优化期间,模型实际上会进行多次训练。如果模型通过 神经架构搜索 部署,其中各种架构超参数被详尽地变化,然后选择性能最佳的模型,研究人员发现单个模型部署的碳足迹相当于五辆普通汽车的寿命。再次强调,这是严重的,特别是因为与这些碳足迹直接相关的气候变化影响最严重的是贫困社区,而这些社区并没有体验到这些模型的直接好处。很明显,在评估这些模型时,这些成本需要纳入考虑。这一认识可以说是驱使该领域朝着更具参数效率的模型发展的力量之一。

预训练语言模型及深度学习普遍存在的一个长期批评是,这些模型往往不太可解释——很难解释模型是如何在特定情景下得出预测的。这与本节早些时候讨论的偏见问题有关——让模型解释其对教育相关联的决策是如何做出的,例如,可以帮助检测这样的决定是否基于种族或性别变量。最值得注意的最近的方法之一是 bertviz,³⁹试图在第七章中探索的注意力可视化基础上进行改进。然而,这仍然没有解决训练数据透明度缺失的问题:语言模型的训练规模如此之大,以至于研究人员几乎无法确保其是无偏的。因此,我们期望看到人们投入时间和精力开发出可以从更小、精心策划的数据集中执行相当的方法。

通过我们对一些应该记住的伦理问题的简要讨论完成后,我们在下一节中提供一些关于如何在这个快速发展的领域中保持时效性的建议。

11.5 保持时效性

正如我们在本章中一再强调的那样,NLP 中的迁移学习方法的状况更新速度很快。本书涵盖的材料应该仅被视为一个平台,用于继续跟踪最新发展。在本节中,我们提供了一些关于如何实现这一目标的基本提示。总的来说,在 Kaggle 和/或 Zindi 平台上参加各种相关竞赛可能是一个处理现实情况的好方法,但数据足够干净且与时俱进。跟踪 arXiv 上的最新论文是必不可少的,尽管新闻和社交媒体的报道可能夸张并且不可靠,但它仍然有助于及早发现有影响力的论文。

11.5.1 Kaggle 和 Zindi 竞赛

在整本书中,我们鼓励您使用 Kaggle 来运行所呈现的各种代码。尽管该平台提供的免费 GPU 计算和易用的设置立即成为其优点,但最大的好处可能直到现在才被明确说明。可以说,Kaggle 平台最强大的方面是可以访问该平台上众多持续进行和归档供后人参考的竞赛。

各种顶级公司面临各种技术挑战,使用该平台来刺激对这些问题的解决方案的研究和开发,通过提供现金奖励,有时可达数千美元的头等奖。这意味着通过跟踪这些竞赛,您可以了解到行业中最紧迫的问题是什么,同时可以访问代表性数据进行即时测试和实验。您可以按主题浏览当前和过去的竞赛,以找到测试任何想法所需的数据——您所需要做的就是将数据集附加到本书中使用的笔记本上,更改一些路径,然后您可能已经准备好产生一些初步的见解了。当然,如果您能够做到这一点,赢得竞赛是很好的,但您从实验、失败和再试中获得的学习价值才是真正无价的。实际上,根据我的经验,一个在排行榜上可能被认为是平庸的竞赛问题解决方案,如果在实践中易于部署和扩展,可能会导致真正的现实影响。我们在附录 A 中提供了一些使用 Kaggle 的具体提示,以帮助初学者入门。

我们还强调了 NLP 中对低资源语言的关注日益增加。因此,重要的是提到 Zindi Africa 平台,该平台提供与 Kaggle 类似的许多功能,但专注于非洲语言和问题。如果您是一位研究人员,想要了解您的方法在这些类型的语言中可能的表现,那么这个平台将是一个寻找相关竞赛和实验数据的好地方。

11.5.2 arXiv

机器学习,以及延伸开来的自然语言处理,可以说是当今最开放的研究领域。除了几个例外,一般来说,结果通常在一旦出现后立即在开放平台arXiv上发表。这使研究团队能够提前对任何发现进行申请,同时进行完善和论文出版手续。这意味着,如果你能找到它的话,最前沿的研究已经对你可用。arXiv由 Google Scholar 存档,因此你可以在那里设定关键词的警报,帮助你及早发现相关的论文。

arXiv平台上传的论文数量巨大,要找到与你相关的最重要的论文可能会有些困难。为了解决这个问题,我建议关注你喜欢的论文的作者在社交媒体上的动态——Twitter 似乎是这一领域的研究人员比较喜欢的平台。关注媒体报道也可能会有所帮助,只要你对所有声明持保留态度。接下来我们会多说几句关于这个问题。

11.5.3 新闻和社交媒体(Twitter)

总的来说,把科学主题的新闻和社交媒体报道看作可能具有煽动性和技术上不可靠是件好事。如果我们考虑一个媒体机构可能与报道技术相关的激励以及通常记者对主题可能没有技术背景的事实,这就说得通了。然而,经过核实的新闻可能是关于特定论文或主题的社区兴奋的一个良好指标,这总是一个需要考虑的好事。

如果你使用像谷歌新闻这样的平台,你可以在你的订阅中设置“语言模型”等主题的警报。你可能会得到很多信息,而且并非所有信息都值得关注。通常,我只会在这些论坛上深入研究一篇论文,只有在我认为一段时间内一直“可靠”的场所出现后,我才会深入研究一篇论文,这使我对这些论点至少经受了一段时间的公开评审产生了信心。GPT-3 的情况正好是个最近的例子——通过谷歌新闻上的这个启发式方法,我立刻就能体会到其影响。

关于社交媒体,Twitter 似乎是机器学习研究科学家的选择平台。事实上,许多人对他们的工作非常开放,并且如果你向他们提问,他们会很乐意在平台上直接回答你。这也是我最喜欢在这个领域工作的原因之一。请随时在@pazunre 上联系我。你最喜欢的作家或科学家可能会在他们的订阅中分享他们最新最喜欢的论文,通过关注他们,你可以直接收到这些信息。在这一领域,你可能会对以下一些受欢迎的账户感兴趣:@fchollet,@seb_ruder 和@huggingface。

除了竞赛、阅读 arXiv 上的论文、追踪新闻和社交媒体,没有什么比使用这些工具解决实际的挑战更好的了。对于许多人来说,这可能意味着在机器学习和/或自然语言处理领域拥有一份工作,并且每天都在解决一个实际的应用。实际的经验是这个领域大多数潜在雇主最重视的。如果你还没有在这个领域获得这样的实际经验,并且希望进入,那么开源项目可能是一个很好的途径——看看 TensorFlow、PyTorch、Hugging Face、NLP Ghana、Masakhane 等等。列表是无穷尽的,有很多有趣的问题可以解决和贡献,同时也可能使每个人受益。

我希望这些提示能帮助你进入你的机器学习和自然语言处理的未来,让你有能力对你的社会产生重大的积极影响。能够与你分享你旅程的一部分是我的荣幸。

11.6 最后的话

这就是了!你做到了——你已经读完了整本书。在写作过程中,我度过了难忘的时光,与许多研究人员互动,讨论思想并克服了许多挑战。我真诚地希望你享受这个旅程,就像我一样。当你带着这些工具改变世界的时候,请记得善待你周围的人,不要伤害生态系统,并且保持警惕,以防技术被滥用。通过与这个领域中一些杰出头脑互动的短暂时间内,我真诚地相信大多数人对于将这些技术突破变成善良的源头都感到兴奋。因此,我每天都迫不及待地关注研究新闻,渴望看到我们集体的人类思维将会产生什么样的惊喜。我只能希望你也能分享一些这种兴奋。

概要

  • 你只是在这个迅速发展的领域中旅程的开端;保持竞争优势是一个旅程,而不是一个终点。

  • 通过学习这本书所掌握的技能,使你处于一个良好的位置,能够通过持续的努力保持更新。

  • 我们涵盖的一些关键的基础预训练迁移学习启用的语言建模架构包括 Transformer、BERT、mBERT、ELMo 和 GPT。

  • 将这些更大的模型变得更小、更高效的愿望导致了像 ALBERT、DistilBERT 和 ULMFiT 这样的架构/技术的发展,我们也进行了介绍。

  • 新兴的体系结构是前述模型的后代,书中没有详细介绍但你应该知道的,包括 BART、T5、Longformer、Reformer、XLNet 等等。

  • 在实践中部署这些模型时,意识到它们可能带来的潜在的道德和环境影响是很重要的。

  • 近期对道德和环境影响的关注,以及希望将模型能力应用于智能手机和物联网,很可能会在不久的将来继续推动更高效的变压器架构的开发。

  1. Sharir O.等,“训练 NLP 模型的成本:简要概述”,arXiv(2020)。

  2. S.J. Pan 和 Q. Yang,“迁移学习概述”,IEEE 知识与数据工程交易(2009)。

  3. S. Ruder,“自然语言处理的神经迁移学习”,爱尔兰加尔韦国立大学(2019)。

  4. D. Wang 和 T. F. Zheng,“语音和语言处理的迁移学习”,2015 年亚太信号与信息处理协会年度峰会和会议(APSIPA)论文集。

  5. Lipmann Richard 等,“DARPA 数据驱动模型发现(D3M)计划概述”,第 29 届神经信息处理系统(NeurIPS)会议论文集(2016)。

  6. datadrivendiscovery.org/

  7. Yinhan Liu 等,“RoBERTa:稳健优化的 BERT 预训练方法”,arXiv(2019)。

  8. Tom B. Brown 等,“语言模型是少样本学习者”,NeurIPS(2020)。

  9. W. Fedus 等,“Switch 变压器:用简单高效的稀疏性扩展到万亿参数模型”,arXiv(2021)。

  10. www.eleuther.ai/projects/gpt-neo/

  11. huggingface.co/EleutherAI

  12. www.kaggle.com/azunre/tlfornlp-chapter7-gpt-neo

  13. Z. Yang 等,“XLNet:用于语言理解的广义自回归预训练”,NeurIPS(2019)。

  14. Z. Dai 等,“Transformer-XL:超越固定长度上下文的关注语言模型”,ACL(2019)。

  15. M. Zaheer 等,“BigBird:更长序列的变压器”,arXiv(2020)。

  16. I. Beltagy 等,“Longformer:长文档变压器”,arXiv(2020)。

  17. N. Kitaev 等,“Reformer:高效变压器”,arXiv(2020)。

  18. C. Raffel 等,“探索统一文本到文本变压器的迁移学习极限”,arXiv(2020)。

  19. L. Xue 等,“mT5:大规模多语言预训练文本到文本变压器”,arXiv(2019)。

  20. M. Lewis 等,“BART:用于自然语言生成、翻译和理解的去噪序列到序列预训练”,arXiv(2020)。

  21. Y. Liu 等,“用于神经机器翻译的多语言去噪预训练”,arXiv(2020)。

  22. G. Lample 和 A. Conneau,“跨语言语言模型预训练”,arXiv(2019)。

  23. A. Conneau 等,“规模化的无监督跨语言表示学习”,arXiv(2019)。

  24. J. Herzig 等,“TaPas:通过预训练进行弱监督表解析”,arXiv(2020)。

  25. ghananlp.org/

  26. www.masakhane.io/

  27. ethionlp.github.io/

  28. zindi.africa/

  29. www.k4all.org/project/language-dataset-fellowship/

  30. blackinai.github.io/

  31. huggingface.co/facebook/wav2vec2-large-xlsr-53

  32. openai.com/blog/dall-e/

  33. A. Dosovitskiy et al, “An Image Is Worth 16x16 Words: Transformers for Image Recognition at Scale,” arXiv (2020).

  34. J. Builamwini and T. Gebru, “Gender Shades: Intersectional Accuracy Disparities in Commercial Gender Classification,” Journal of Machine Learning Research 81 (2018).

  35. Tom B. Brown et al., “Language Models Are Few-Shot Learners,” NeurIPS (2020).

  36. mng.bz/w0V2

  37. E. Strubell et al., “Energy and Policy Considerations for Deep Learning in NLP,” ACL (2019).

  38. E. Bender et al., “On the Dangers of Stochastic Parrots: Can Language Models Be Too Big?” FAccT (2021).

  39. Jesse Vig, “A Multiscale Visualization of Attention in the Transformer Model,” ACL (2019).

附录 A:Kaggle 入门指南

Kaggle 平台为数据科学和机器学习初学者提供了一个学习基本技能的绝佳途径。通过恰当地利用该平台,你有机会在各种数据集上练习各种问题,并与其他机器学习工程师展示和讨论你的工作。这有可能帮助你扩展你的专业网络。重要的是,该平台允许你在云中直接运行 Python 笔记本,这可以显著消除初学者的系统设置障碍。它还每周提供有限的免费 GPU 计算。这进一步使得本书讨论的工具和方法更加民主化。在全书中,我们鼓励你使用 Kaggle 来运行所呈现的代码。

另一个工具——Google Colab——同样提供免费的 GPU 计算,同时与 Google Drive 集成。然而,如果你必须选择一个工具,我会推荐 Kaggle,因为它具有社交性质,可以访问数据集、讨论和竞赛,这些都是非常宝贵的学习资源。当然,在实际情况下,大多数工程师可能会在某个时候同时使用两者,例如为了增加每周的免费 GPU 配额。

在这个附录中,我们试图提供一个简要的入门指南,可以帮助初学者逐步了解 Kaggle 的各种功能。我们将其分为两个部分。首先讨论 Kaggle 内核的概念以运行笔记本,然后查看竞赛、相关讨论和 Kaggle 博客功能。

A.1 Kaggle 内核提供的免费 GPU

如前所述,你可以在 Kaggle 免费使用云中直接运行 Python 代码。这些云笔记本有时被称为Kaggle 内核。在撰写本文时(2021 年 1 月),Kaggle 每周提供约 36 小时的 GPU 时数,你可以为任何你认为可能需要的笔记本启用它。我们将通过演示如何开始,逐步介绍一个对 Python 初学者有用的简单场景。

假设你是一个初学者,并且有兴趣使用这些内核学习基本的 Python 语法。一个很好的开始地方是访问www.kaggle.com/kernels,然后搜索“Python 教程”。这个搜索结果可能如图 A.1 所示。

APPA_01

图 A.1 开始学习 Kaggle 内核和启动相关笔记本来学习新知识的最佳地点。前往www.kaggle.com/kernels,然后搜索你感兴趣的主题。在图表中,我们展示了这样一个查询的结果列表,供初学者开始学习 Python 时参考。选择最合适的结果继续。或者使用新笔记本按钮创建一个新笔记本。

如图所示,搜索将返回一系列结果,您可以选择最符合您需求的一个。在这种情况下,初学者可能希望教程直接以 NLP 为重点开始,考虑到书籍的内容,因此可能会选择突出显示的教程笔记本。点击它将会显示相关的渲染笔记本,其中代表性视图如图 A.2 所示。

APPA_02

图 A.2 渲染笔记本的视图,突出显示可以执行的一些关键操作

请注意,所示的视图代表了您在单击书籍存储库中我们的伴随笔记本链接之一时将遇到的第一个视图。¹ 如图所示,笔记本已呈现,这意味着即使不运行代码,您也可以滚动并查看所有代码的代表性输出。

要运行代码,请点击复制并编辑按钮以创建自己版本的笔记本。生成的笔记本将具有相同的依赖项——在 Kaggle 环境中预安装的 Python 库版本和用于生成代表性笔记本输出的库。请注意,如果您点击了图 A.1 中的新笔记本按钮而不是选择复制现有笔记本,则依赖项将是 Kaggle 指定的最新依赖项。因此,您可能需要修改原始代码以使其正常工作,这会增加难度。要完成复制和编辑,或分叉,过程,将要求您提供登录信息。您可以注册您的电子邮件地址,也可以直接使用谷歌等社交账号登录。

为了精确复制我们为本书的伴随笔记本所使用的 Kaggle 环境,我们在伴随书籍存储库中包含了需求文件。请注意,这些需求文件仅用于在 Kaggle 笔记本上复制 Kaggle 环境的目的。如果您试图在本地计算机上使用它们,根据本地架构的不同,您可能会遇到额外的问题,并且可能需要对其进行修改。我们不支持这种模式,如果您正在追求它,请仅将需求文件用作指南。还要记住,并非每个列出的要求都需要在您的本地安装中使用。

单击“复制并编辑”将带您进入主工作区,如图 A.3 所示。如图所示,您可以通过左上角的按钮运行当前选择的单元格,也可以运行笔记本中的所有代码。在右侧面板上,您可以启用或禁用您的互联网连接。下载数据或安装软件包可能需要互联网连接。此右侧面板还包含在当前笔记本中启用 GPU 加速的选项,您需要在合理的时间内训练神经网络。您还将看到当前附加到笔记本的数据集,并且可以单击其中任何一个以转到数据集的描述。单击“添加数据”将打开一个搜索查询框,您将能够按关键字搜索感兴趣的数据集,以添加到当前笔记本。对于本书的所有伴侣笔记本,必要的数据已经附加到笔记本中。

APPA_03

图 A.3 使用 Kaggle 内核时的主工作区。在左上角,是运行笔记本的按钮。在右上角,是共享、保存、重新启动和关闭笔记本的选项。右侧面板包含连接到互联网的选项(用于安装软件包或下载数据)、启用/禁用当前笔记本的 GPU 加速以及添加数据。

在右上角,您可以选择笔记本的共享设置——根据您的项目需要,您可以将笔记本设置为仅自己可见,与其他用户私下共享,或者对外公开。我们所有的伴侣笔记本都是公开的,这样任何人都可以访问,但您可以将它们的分支设置为私有。重要的是,也在右上角,选择保存版本将弹出对话框以保存您的工作,如图 A.4 所示。

APPA_04

图 A.4 笔记本的保存选项。您可以提交代码并让其后续非交互式运行以供以后检查,也可以直接快速保存代码和当前输出。

正如图所示,有两种保存模式。快速保存模式将在版本名称文本博客中保存当前代码和输出。如果当前输出需要几个小时来生成,这将是正确的选择。保存并运行所有选项将保存代码并在后台非交互式地运行它。当运行长时间的训练作业时,例如五六个小时时,这特别有用。您可以关闭会话和所有窗口,并在需要检查结果时随时返回。最近运行/保存的检查通常可以在个性化的 URL www.kaggle.com//notebooks 上进行,其中 是您的用户名。对于我的用户名 azunre,此页面的视图如图 A.5 所示。

APPA_05

图 A.5 近期运行/保存的检查通常可以在个性化 URL www.kaggle.com//notebooks 进行,其中 是您的用户名(此处显示的是我的用户名 azunre)。

我们已经介绍了您需要了解的本书练习的主要特性。我们还没有涵盖的许多其他特性,Kaggle 经常会添加更多。通常,快速的谷歌搜索和一些坚持和实验的愿望就足以弄清楚如何使用任何这样的特性。

在下一节中,我们简要讨论 Kaggle 比赛。

A.2 比赛、讨论和博客

面对技术挑战的领先企业利用 Kaggle 通过为顶级创新提供重大奖金来刺激解决方案的研究和开发。让我们通过选择任何 Kaggle 页面左侧面板上可见的奖杯图标来检查 Kaggle 比赛页面,如图 A.6 所示。

APPA_06

图 A.6 通过选择任何 Kaggle 页面左侧面板上的奖杯图标进入比赛页面。我们可以看到一个比赛提供了总共 $100,000 的奖金——这个问题很可能对该行业非常有价值,以激励这样的投资!

您可以追踪这些比赛,了解行业中最紧迫的问题,同时可以访问基础数据以进行即时测试和实验。您可以按主题浏览当前和过去的比赛,以找到测试您可能有的任何想法的数据。您所需要做的就是将数据集附加到上一节介绍的笔记本中,更改一些路径,然后您应该准备产生一些初步的见解了。当然,如果您能够做到,赢得比赛对于获得金钱奖励来说是很棒的,但是您从实验、失败和再次尝试中获得的学习价值才是真正无价的。事实上,在我看来,通过排行榜的位置可能被认为是中等的比赛问题的解决方案,如果在实践中更容易部署和扩展,那么可能会导致实际影响。这是我个人关心的事情,因此我倾向于将精力集中在对我最感兴趣但我了解最少的问题上,以获取最大的学习价值。

点击任何比赛都会打开一个专门页面,在这里您可以浏览其描述、数据、排行榜,以及重要的是,图 A.7 中显示的“讨论”功能。

APPA_07

图 A.7 讨论功能使您能够与 Kaggle 社区的其他成员就您感兴趣的特定主题进行交流。聊天,扩展您的网络!

正如您可能在图 A.7 中看到的那样,这是一个与问题相关的讨论论坛。人们发布提示和入门笔记,提出重要问题,甚至可能由竞赛组织者回答。例如,如果您遇到特定竞赛数据的任何问题,您很有可能在这里找到答案。许多比赛提供最有价值贡献的奖励——通常是通过点赞来衡量的——这激励人们提供帮助。获胜者经常发布他们的解决方案,有时甚至作为您可以直接重新利用的笔记本。您甚至可以在此建立未来挑战的团队,并建立友谊。参与社区,并回馈一些您从中得到的东西,您可能会学到比其他方式更多。归根结底,科学仍然是一项社会活动,这使得 Kaggle 的这个功能尤为宝贵。

最后,Kaggle 在medium.com/kaggle-blog上运行一个博客。大型比赛的获胜者经常在这里接受采访,分享他们可以与他人分享的技巧。教程经常发布在各种关键主题上。及时了解这些内容,以确保了解数据科学中最新的新兴研究趋势。

我们希望这个附录是一个有用的练习,并让您跟上了进展。继续前进,Kaggle!

  1. https:/github.com/azunre/transfer-learning-for-nlp

附录 B:初级深度学习工具介绍

本附录涵盖

  • 介绍本书中使用的五种基本算法和软件工具

  • 对训练神经网络使用的算法——随机梯度下降进行概述

  • 以 TensorFlow 开始进行神经网络建模

  • 以 PyTorch 开始进行神经网络建模

  • 对较高级别的神经网络建模框架 Keras、fast.ai 和 Hugging Face transformers 进行概述

在本附录中,我们试图对本书中使用的一些基本工具和概念提供一个简要的入门。对这些工具的简要介绍并不绝对必要,以便理解并充分从本书中受益。不过,阅读它们可以帮助一个新的深度学习领域的人快速融入,并且对他们可能是最有用的。

具体来说,我们首先向读者介绍了我们目前经历的深度学习革命背后的基本算法。当然,这就是用于训练神经网络的随机梯度下降算法。接着我们介绍了两种基本的神经网络建模框架,PyTorch 和 TensorFlow。然后我们介绍了这两个建模框架之上构建的三种工具,以提供一个更高级别的接口:Keras、fast.ai 和 Hugging Face transformers。这些工具相互补充,你可能在职业生涯的某个时候都会用到它们。我们对概念的阐述并不是穷尽的;它提供了一个“鸟瞰”为什么这些工具是需要的以及它们如何相互比较和相互补充。我们涉及了介绍性的概念,并引用了精心筛选的参考资料,以便深入研究。如果你觉得自己对这些工具的经验很少,你可能需要在开始阅读本书之前深入研究一下它们。

让我们从深度学习革命背后的算法引擎开始,即随机梯度下降算法。

B.1 随机梯度下降

神经网络有一组参数,称为权重,确定它将如何将输入数据转换为输出数据。确定哪组权重允许网络最接近地逼近一组训练数据称为训练网络。随机梯度下降是实现这一目标的方法。

让我们用W表示权重,x表示输入数据,y表示输出数据。我们还用y_pred表示神经网络对输入x预测的输出数据。损失函数,用于衡量yy_pred之间的接近程度,被表示为函数f。注意它是xyW的函数。随机梯度下降算法被制定为一个过程,以找到f的最小值,即预测尽可能接近训练数据的位置。如果f的梯度,用f'表示,存在——如果它是一个可微函数——我们知道在这样的点上f'=0。算法试图使用以下步骤序列找到这样的点:

  • 从训练集中随机抽取一个输入-输出批次x-y的数据。这种随机性是算法被称为随机的原因。

  • 使用当前W的值将输入通过网络以获得y_pred.

  • 计算相应的损失函数值f

  • 计算损失函数相对于W的相应梯度f'

  • 稍微改变W的方向以降低f。步长的大小由算法的学习率决定,这是收敛的一个非常重要的超参数。

对于过度简单的单个权重的情况,该过程在图 B.1 中的第 2 步找到了算法的最小值。这张图受到弗朗索瓦·朱利叶的优秀书籍《深度学习与 Python》(Manning Publications,2018)中图 2.11 的启发,你也应该查看这本书以获得对该算法非常直观的解释。

APPB_01

图 B.1 展示了随机梯度下降在单个权重的过度简单情况下的示意图。在每一步中,计算相对于W的梯度,并采取预先确定大小的步骤,由学习率决定,沿着损失函数梯度的相反方向。在这个假设的情景中,最小值在第 2 步找到。

有许多此算法的变体存在,包括 Adam、RMSprop 和 Adagrad。这些算法倾向于专注于避免局部最小值,并以各种方式(如学习率)进行自适应,以更快地收敛。动量的概念——它表现为每一步W更新中的额外加法项——被几种这样的变体用来避免局部最小值。以下是一些最流行的变体及简要描述。

Adagrad 根据参数遇到的频率调整学习率。罕见的参数会以更大的步长进行更新,以实现平衡。该技术被用于训练 GloVe 静态词嵌入,该词嵌入在本书的第四章中描述。在这种情况下,它需要适当处理语言中的稀有单词。

RMSprop是为了解决 Adagrad 的学习速率经常过快下降的问题而开发的。我们可以通过将更新缩放为平方梯度的指数衰减平均值来部分缓解这个问题。

Adam表示自适应矩估计,也针对不同参数变化学习率。它与 RMSprop 具有相似之处,因为它使用衰减平方梯度平均值来执行更新。衰减平方梯度平均值的第一和二个时刻被估计,更新,然后用于在每个步骤中更新参数。这是尝试解决许多问题的流行算法。

NadamNesterov 加速 Adam的缩写,采用称为Nesterov 加速梯度的创新来进一步改善 Adam 的收敛性。

因为这里的介绍只是一个简短的介绍,而不是详细的处理,所以我们不会更深入地探讨这些变体。这个主题已经被许多优秀的参考文献详细覆盖了¹,²,我们鼓励您深入研究以获得更好的理解。即使您可以在不深入了解这些变体的情况下使用现代框架,更好地了解它们可以帮助您调整超参数,并最终部署更好的模型。

B.2 TensorFlow

如前一节所述,了解损失函数相对于神经网络权重的梯度对于训练网络至关重要。由于现代神经网络是巨大的,达到数十亿个参数,因此手动计算此梯度函数将是不可能的。相反,使用 TensorFlow 等基本神经网络建模工具,通过应用取导数的链式法则自动找到梯度来计算它。这个过程被称为自动微分

Tensorflow 中的基本数据结构是张量,通过构建一个计算图来对其进行操作。在框架的 1.x 版本中,通过多种 API 调用tf.*来构建图,并使用Session对象编译和执行它以产生数值。示例 B.1 中演示了使用此 API 定义图形并执行其梯度计算的说明性示例。具体来说,我们需要计算矩阵乘积z = x*y,其中x是简单的列向量,而y是简单的行向量。我们还希望自动计算它相对于xy的梯度。

示例 B.1 使用 TensorFlow 1 计算矩阵乘积 z = x * y 及其梯度

import tensorflow as tf                                                   ❶

tf.compat.v1.disable_eager_execution()                                    ❷

x = tf.compat.v1.placeholder(tf.float32, name = "x")                      ❸
y = tf.compat.v1.placeholder(tf.float32, name = "y")

z = tf.multiply(x, y) # Define vector product graph
gradient = tf.gradients(z,[x, y],grad_ys=tf.eye(2))                       ❹

with tf.compat.v1.Session() as session:                                   ❺
    z = session.run(z, feed_dict={x: [[1., 1.]], y: [[5.], [5.]]})        ❻
    zG = session.run(gradient,feed_dict={x: [[1.,1.]], y: [[5.],[5.]]})   ❼

print("Product:")                                                         ❽
print(z)                       
print("\n\n")                  
print("Gradient of Product:")  
print(zG)                      
print("\n\n")                  

❶总是先导入 TensorFlow

❷Eager 执行在 2.0 之前作为非默认值引入,因此在此确保其关闭。

❸定义留给后面分配值的向量变量占位符

❹ 定义了乘积的向量导数图,相对于 x 和 y。参数 grad_ys 乘以输出,可用于取链导数,因此我们将其设置为单位矩阵以无效果。

❺ 使用 Session 对象执行图

❻ 运行函数,指定占位符的值

❼ 运行梯度,指定占位符的值

❽ 显示结果

执行此代码将产生以下输出。您应该能够手动验证这些值是否正确,使用您的基本线性代数知识,这是本书的先决条件。我们还在书的伴随存储库中包含了一个 Kaggle 内核笔记本,执行这些命令。³

Product:
[[5\. 5.]
 [5\. 5.]]

Gradient of Product:
[array([[5., 5.]], dtype=float32), array([[1.],
       [1.]], dtype=float32)]

框架的 2.0 版及更高版本将更“Pythonic”的 eager execution 模式作为默认模式,这使得框架更易于使用。它现在还包括了 Keras,使得使用各种高级功能更加容易。下一个列表中显示了使用此 API 定义和执行与列表 B.1 中相同图形的说明性示例。更易于访问性立即变得明显,eager 模式使得立即执行变得可能,而不是通过图上的 Session 对象。

列表 B.2 计算矩阵乘积 z = x*y 及其在 TensorFlow 2 中的梯度

import tensorflow as tf

x = tf.convert_to_tensor([[1., 1.]])            ❶
y = tf.convert_to_tensor([[5.], [5.]])          ❷

with tf.GradientTape() as g:                    ❸
    g.watch(x)
    z = tf.multiply(x, y)
    dz_dx = g.gradient(z, x, output_gradients=tf.eye(2))

with tf.GradientTape() as g:                    ❹
    g.watch(y)
    z = tf.multiply(x, y)
    dz_dy = g.gradient(z, y, output_gradients=tf.eye(2))

print("Dot Product:")                           ❺
print(z)
print("\n\n")
print("Gradient of Product (dz_dx):")
print(dz_dx)
print("\n\n")
print("Gradient of Product (dz_dy):")
print(dz_dy)

❶ 列向量

❷ 行向量

❸ 这是如何相对于 x 计算自动导数的。这里的“Tape”一词表示所有状态都被“记录”,可以播放回来以检索我们需要的信息。

❹ 这是如何相对于 y 计算自动导数的。参数 output_gradients 乘以输出,可用于取链导数,因此我们将其设置为单位矩阵以无效果。

❺ 显示结果

执行此代码应产生与之前相同的输出值。

框架按层次结构组织,具有高级和低级 API,如图 B.2 所示。

APPB_02

图 B.2 TensorFlow 框架的分层组织示意图

此图受官方 TensorFlow 文档第 1 图的影响。⁴ 如果您是初学者,并且想更详细地浏览此参考资料,这可能会有所帮助。在附录的最后一节将进一步讨论 TensorFlow 版本的 Keras,该版本也显示在图中。

更好地了解 TensorFlow 各种特性的方法是动手尝试相关的 Kaggle 内核/笔记本教程,如附录 A 中所述。特别是,只需访问 kaggle.com 并搜索“TensorFlow 教程”即可找到大量精彩的教程,您可以选择最适合您的学习风格和经验水平的内容。www.kaggle.com/akashkr/tensorflow-tutorial 上的教程似乎对初学者很有帮助。

B.3 PyTorch

这个框架在 TensorFlow 之后发布(2016 年对比 2015 年)。然而,它很快就成为许多研究人员首选的框架,如 TensorFlow 相对 PyTorch 在学术论文引用方面的相对流行度下降所证明的那样。⁵ 这种增加的流行度被普遍认为是因为该框架能够在运行时以编程方式修改各种 PyTorch 模型对象,从而在研究过程中更容易进行代码优化。事实上,TensorFlow 2.0 中的急切模式的引入被普遍认为受到了 PyTorch 成功的影响。尽管在 TensorFlow 2.0 发布后,这两个平台之间的差异变得更小了,但普遍的观点是,研究人员更喜欢 PyTorch,而 TensorFlow 更适用于在生产中部署。

作为例证,我们在 PyTorch 中执行与清单 B.1 和 B.2 相同的操作序列 —— 向量乘法及其导数,这是神经网络模型的核心 —— 并在下一个清单中展示相应的代码。

清单 B.3 在 PyTorch 中计算矩阵乘积 z = x*y 及其梯度

import torch                                                               ❶
from torch.autograd import grad                                            ❷
import numpy as np # tensors will be built from numpy arrays

x = torch.from_numpy(np.array([[1., 1.]]))                                 ❸
y = torch.from_numpy(np.array([[5.], [5.]]))                               ❹

x.requires_grad = True                                                     ❺
y.requires_grad = True

z = torch.mul(x, y)                                                        ❻

zGx = grad(outputs=z, inputs=x,grad_outputs=torch.eye(2),retain_graph=True)❼
zGy = grad(outputs=z, inputs=y,grad_outputs=torch.eye(2))                  ❽

print("Dot Product")                                                       ❾
print(z)
print("Gradient of Product(dz_dx)")
print(zGx)
print("\n\n")
print("Gradient of Product (dz_dy):")
print(zGy)

❶ 总是首先导入 PyTorch。

❷ 导入 grad 函数进行自动微分

❸ 列向量

❹ 行向量

❺ 这确保了可以针对 x 计算梯度。

❻ 计算乘积

❼ 针对 x 计算自动导数。retain_graph 确保我们可以继续进行导数计算;否则,“Tape” 将被丢弃,无法回放。

❽ 针对 y 计算自动导数。参数 grad_outputs 乘以输出,可以用于进行链式导数,因此我们将其设置为单位矩阵以无效果。

❾ 显示结果

执行此代码应该产生与前一节相同的结果。我们还在本书的伴随存储库中包含了一个 Kaggle 内核笔记本,执行这些命令。

和以前一样,我们建议通过一些 Kaggle 内核来熟悉 PyTorch 的各个方面,如果你觉得自己可能需要更多经验的话。www.kaggle.com/kanncaa1/pytorch-tutorial-for-deep-learning-lovers 上的教程似乎是初学者的好选择。

B.4 由 Hugging Face 提供的 Keras、fast.ai 和 Transformers

正如在附录中早前提到的,Keras 库是一个更高级的神经网络建模框架,现在也包含在 TensorFlow 2.0 及更高版本中。通过使用它,你可以在 TensorFlow 和 PyTorch 中指定神经网络架构,只需从一个 API 中切换后端即可!它与 TensorFlow 预先打包在一起,如我们在图 B.2 中所示。与 TensorFlow 和 PyTorch 相比,其 API 相对简单,这使得它非常受欢迎。存在许多优秀的学习资源,也许最好的资源之一是作者自己的书籍。这也是学习 TensorFlow 和神经网络的绝佳参考资料,如果你觉得需要复习这些主题,我们强烈推荐它。你也可以通过一些 Kaggle 内核来学习一些基础知识,例如 www.kaggle.com/prashant111/keras-basics-for-beginners 上的教程只是一个很好的例子。

在该领域另一个流行的高级建模 API 是 fast.ai。这个库是作为同名大型在线课程(MOOC)的伴侣而开发的,并以一种极易使用的方式实现了最先进的方法。其动机之一是将这些工具普及到发展中国家。该库的一个受欢迎功能是其学习率确定实用程序,我们在本书的第九章中使用了它。该框架用于自然语言处理和计算机视觉,并运行在 PyTorch 上。自然,学习该库的最佳参考资料是 fast.ai MOOC 本身。这门免费课程涵盖了神经网络和深度学习的基础知识,是另一个我们强烈推荐的精彩资源。该库通过定义其自己的一套数据结构来实现简单化,这些数据结构处理了用户的大量样板代码。另一方面,这可能会使其对非标准用例的定制更加困难。在作者的经验中,这是一个拥有的绝佳工具。

最后,Hugging Face 的 Transformers 是一个专门针对基于 Transformer 模型的高级建模框架。这些模型已经成为现代自然语言处理中可能是最重要的架构。你将在整本书中准确了解其中的原因。这个库可能是当今领域中最受欢迎的库,因为使用它部署这些模型非常简单。在这个库存在之前,使用 Keras、TensorFlow 和/或 PyTorch 部署 Transformer 模型相当繁琐。该库在某些情况下简化了这个过程,只需几行 Python 代码,导致其受欢迎程度激增,并被认为是现代自然语言处理从业者不可或缺的工具。由于 API 的透明度和简单性,你可能只需阅读本书并通过相关示例进行工作,甚至无需任何先前的使用经验即可。如需进一步参考,请查看作者在 GitHub 上的入门笔记⁹以及官方快速入门文档¹⁰。

  1. F. Chollet,《Deep Learning with Python》(Manning Publications,2018)。

  2. S. Ruder,“梯度下降优化算法概述”,arXiv(2016)。

  3. github.com/azunre/transfer-learning-for-nlp

  4. developers.google.com/machine-learning/crash-course/first-steps-with-tensorflow/toolkit

  5. en.wikipedia.org/wiki/TensorFlow

  6. github.com/azunre/transfer-learning-for-nlp

  7. F. Chollet,《Deep Learning with Python》(Manning Publications,2018)。

  8. www.fast.ai/

  9. github.com/huggingface/transformers/tree/master/notebooks

  10. huggingface.co/transformers/quicktour.html

posted @ 2024-05-02 22:33  绝不原创的飞龙  阅读(2)  评论(0编辑  收藏  举报