赞
踩
原文:
zh.annas-archive.org/md5/da86c0033427bb250532db6d61561179
译者:飞龙
在过去的几年里,很难忽视自然语言处理(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
。
购买《自然语言处理的迁移学习》包括免费访问由 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。
图 1.1 转移学习范式优势的示意图—显示在底部面板—在不同任务/领域训练的系统之间共享信息,与传统范式—显示在顶部面板—其中任务/领域之间同时进行训练相比。在转移学习范式中,通过信息/知识共享可以实现减少数据和计算需求。例如,如果一个人先学会弹钢琴,我们预期他们学会打鼓会更容易些。
自然语言处理的目标是使计算机能够理解自然人类语言。您可以将其视为将自然语言文本系统地编码为准确反映其含义的数值表示的过程。尽管存在各种典型自然语言处理任务的分类法,但以下非尽述性列表提供了一个框架,用于思考问题的范围,并适当地构建本书将讨论的各种示例。请注意,其中一些任务可能需要(或不需要,具体取决于所选择的特定算法)列表中其他更难的任务:
词性标注(POS)—在文本中标记词语的词性;可能的标记包括动词、形容词和名词。
命名实体识别(NER)—在非结构化文本中检测实体,如人名、组织名和地名。请注意,词性标注可能是 NER 流水线的一部分。
句子/文档分类—使用预定义的类别对句子或文档进行标记,例如情感{“积极”,“消极”}、各种主题{“娱乐”,“科学”,“历史”}或一些其他预定义的类别集。
情感分析—为一个句子或文档分配其中表达的情感,例如,{“积极”,“消极”}。事实上,您可以将其视为句子/文档分类的特例。
自动摘要—总结一系列句子或文档的内容,通常用几句话或关键词概括。
机器翻译—将句子/文档从一种语言翻译成另一种语言或一系列语言。
问答系统—确定对人类提出的问题的合适答案;例如,问题:加纳的首都是什么?答案:阿克拉。
闲聊机器人/聊天机器人—与人类进行一段有说服力的对话,可能旨在实现某个目标,例如最大化对话长度或从人类那里提取某些特定信息。请注意,闲聊机器人可以被构建为问答系统。
语音识别—将人类语音的音频转换为其文本表示。尽管已经投入了大量的工作使语音识别系统更加可靠,但在本书中,假设已经存在了语言感兴趣的文本表示。
语言建模 —— 确定人类语言中一系列单词的概率分布,其中知道一个序列中最有可能的下一个单词对于语言生成——预测下一个单词或句子——尤为重要。
依赖解析 —— 将一句话分成一个表示其语法结构和单词之间关系的依赖树。请注意,POS 标记在这里可能很重要。
在继续本书的其余部分之前,了解自然语言处理这个术语,并正确地将其与其他常见术语,如人工智能、机器学习和深度学习相联系非常重要。流行媒体经常将这些术语赋予的含义与机器学习科学家和工程师使用它们的含义不匹配。因此,在我们使用这些术语时,通过图 1.2 中的部分图解精确定义这些术语非常重要。
图 1.2:自然语言处理(NLP)、人工智能(AI)、机器学习和深度学习相互关系的维恩图解。具有符号 AI 的其他相关内容也在图中显示。
如您所见,深度学习是机器学习的子集,而机器学习又是 AI 的子集。NLP 也是 AI 的子集,与深度学习和机器学习有非空交集。本图扩展了 François Chollet 提出的图表³。请参阅他的书中的第六章和第 8.1 节,了解神经网络在文本中的应用综述。符号 AI 也在图表中显示,并将在下一小节中描述。
人工智能这一领域起源于 20 世纪中叶,旨在使计算机模仿和执行人类通常执行的任务。最初的方法侧重于手动推导和硬编码显式规则,以处理每种感兴趣情况的输入数据。这个范式通常被称为符号 AI。它适用于像棋类这样明确定义的问题,但在遇到属于感知类别的问题,如视觉和语音识别时,明显遇到了困难。需要一种新的范式,其中计算机可以从数据中学习新规则,而不是让人类主管明确指定它们。这导致了机器学习的崛起。
在 20 世纪 90 年代,机器学习的范式成为了人工智能的主导趋势。计算机不再为每种可能的情景明确地编程,而是通过看到许多相应的输入输出对的示例来训练计算机将输入与输出信号关联起来。机器学习使用了大量的数学和统计机制,但由于它往往涉及大型和复杂的数据集,该领域更多地依赖于实验、经验观察和工程,而不是数学理论。
机器学习算法学习一种将输入数据转换为适当输出的表示。为此,它需要一组数据,例如句子分类任务中的一组句子输入,以及一组相应的输出,例如句子分类的标签,如{“positive”,“negative”}。还需要一个损失函数,它衡量机器学习模型当前输出与数据集预期输出的距离有多远。为了帮助理解,考虑一个二元分类任务,其中机器学习的目标可能是选择一个名为决策边界的函数,它将清晰地将不同类型的数据点分开,如图 1.3 所示。这个决策边界应该泛化到超出训练数据的未见示例。为了使这个边界更容易找到,您可能希望首先对数据进行预处理或转换,使其更易于分离。我们从被允许的一组称为假设集的函数中寻求这样的转换。自动确定这样一个转换,使得机器学习的最终目标更容易实现,具体来说就是所谓的学习。
图 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。
图 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 可以处理任意长度的输入序列。
图 1.5 显示了一个具有两个全连接隐藏层的简单前馈神经网络的可视化(左)。右侧是一个总结性的等效表示,我们经常会用来简化图表。
正如之前提到的,引发深度学习最近兴趣的因素是硬件的跨度,大量数据的可用性以及算法的进步。GPU 最初是为视频游戏市场开发的,互联网的成熟开始为该领域提供前所未有的质量和数量的数据。维基百科、YouTube 和 ImageNet 是数据源的具体例子,其可用性推动了计算机视觉和自然语言处理的许多进步。神经网络消除了昂贵的手工特征工程的需求——这是将浅层学习方法成功应用于感知数据所需的——这可以说是影响了深度学习易于采纳的因素。由于自然语言处理是一个感知问题,它也将是本书中讨论的最重要的机器学习算法类别之一,尽管不是唯一的。
接下来,我们的目标是深入了解自然语言处理(NLP)领域的历史和进展。
语言是人类认知中最重要的方面之一。毋庸置疑的是,为了创建真正的人工智能,机器需要被教导如何解释、理解、处理和作出对人类语言的反应。这强调了自然语言处理对人工智能和机器学习领域的重要性。
就像人工智能的其他子领域一样,处理自然语言处理问题的初始方法,如句子分类和情感分析,都是基于显式规则或符号型人工智能。这种系统通常无法推广到新的任务,并且很容易崩溃。自从 20 世纪 90 年代核方法诞生以来,人们一直致力于特征工程——将输入数据手动转化为浅层学习方法可以用来产生有用预测的形式。这种方法非常耗时、任务特定且对非专家来说难以接触。深度学习的出现(大约在 2012 年)引发了自然语言处理的真正革命。神经网络能够在其某些层自动设计合适的特征,降低了这些方法对新任务和问题的适用性门槛。然后,人们将精力集中在为特定任务设计适当的神经网络架构,以及调整训练过程中的各种超参数设置上。
训练自然语言处理系统的标准方式是收集一组大量的数据点,每个数据点都可靠地注释了输出标签,比如情感分析任务中的“积极”或“消极”的句子或文档。然后将这些数据点提供给机器学习算法,学习最佳的输入到输出信号的表示或转换,可以推广到新的数据点。在自然语言处理和机器学习的其他子领域中,这个过程通常被称为“监督学习”范式。标注过程通常是手动完成的,为学习代表性转换提供“监督信号”。另一方面,从无标签数据中学习表示转换被称为“无监督学习”。
虽然今天的机器学习算法和系统并非生物学习系统的直接复制品,也不应被认为是这种系统的模型,但它们的某些方面受到了进化生物学的启发,而在过去,从生物学中汲取的灵感引导了显著的进步。基于这一点,似乎不合逻辑的是,对于每个新的任务、语言或应用领域,监督学习过程传统上都是从零开始重复。这一过程在某种程度上与自然系统学习的方式背道而驰——建立在之前获得的知识之上并进行再利用。尽管如此,从零开始学习感知任务仍取得了重大进展,特别是在机器翻译、问答系统和聊天机器人领域,虽然其中仍存在一些缺点。尤其是,当样本分布发生重大变化时,现有系统在处理时的稳定性较差。换句话说,系统学会了在特定类型的输入上表现良好。如果我们改变输入类型,这可能导致性能显著下降,甚至完全失效。此外,为了完全民主化人工智能,并使自然语言处理对小型企业的普通工程师——或对没有大型互联网公司所拥有的资源的人——变得更易获得,能够下载和重复使用其他地方获得的知识将是极其有益的。这对于生活在官方语言可能与英语或其他流行语言不同的国家的人,以及从事可能在他们所在地区独特的任务或从未有人探索过的任务的人来说,也非常重要。迁移学习提供了一种解决这些问题的方法。
迁移学习使人们能够从一个环境——我们定义为特定任务、领域和语言的组合——转移知识到另一个不同的环境。原始环境自然被称为源环境,而最终的环境称为目标环境。迁移过程的难易程度和成功程度取决于源环境和目标环境的相似性。很自然地,如果目标环境在某种意义上与源环境“相似”,在这本书的后面我们将对此做出定义,那么迁移将会更加容易且成功。
转移学习在自然语言处理中的隐式使用时间比大多数从业者意识到的要长得多,因为常见做法是使用预训练的嵌入,如word2vec或sent2vec(在下一节中会更详细介绍)对单词进行向量化。 浅层学习方法通常被应用于这些向量作为特征。 我们将在接下来的章节和第四章中更详细地介绍这两种技术,并在整本书中以各种方式应用它们。 这种流行的方法依赖于一个无监督的预处理步骤,首先用于训练这些嵌入而不需要任何标签。 然后,从这一步中获取的知识被转移到特定的应用程序中,在监督设置中,通过使用浅层学习算法在一小部分标记示例上对所说的知识进行改进和专业化,以解决手头的问题。 传统上,将无监督和监督学习步骤相结合的这种范式被称为半监督学习。
接下来,我们将详细介绍自然语言处理进展的历史进程,特别关注转移学习最近在这一重要的人工智能和机器学习子领域中所起的作用。
要框架化你对自然语言处理中转移学习的状态和重要性的理解,首先了解历史上对这个人工智能子领域重要的任务和技术可以是有帮助的。 本节介绍了这些任务和技术,并以自然语言处理转移学习最近的进展概述告终。 这个概述将帮助你适当地将转移学习在自然语言处理中的影响放入背景,并理解为什么它现在比以往任何时候都更重要。
自然语言处理诞生于 20 世纪中叶,与人工智能同时出现。 自然语言处理的一个重要历史里程碑是 1954 年的乔治城实验,在该实验中,大约 60 个俄语句子被翻译成英语。 在 20 世纪 60 年代,麻省理工学院(MIT)的自然语言处理系统 ELIZA 成功模拟了一名心理医生。 同样在 20 世纪 60 年代,信息表示的向量空间模型被开发出来,其中单词被表示为实数向量,这些向量可进行计算。 20 世纪 70 年代,基于处理输入信息的复杂手工规则集的一系列闲聊机器人/聊天机器人概念被开发出来。
在 1980 年代和 1990 年代,我们看到了将系统化的机器学习方法应用于自然语言处理的出现,计算机发现了规则,而不是人类制定了规则。这一进步与当时机器学习的普及爆炸同时发生,正如我们在本章前面已经讨论过的那样。1980 年代末,将奇异值分解(SVD)应用于向量空间模型,导致潜在语义分析—一种无监督的确定语言中单词关系的技术。
在 2010 年代初,神经网络和深度学习在该领域的崛起,彻底改变了自然语言处理。这些技术被证明在最困难的自然语言处理任务中取得了最先进的结果,例如机器翻译和文本分类。2010 年代中期见证了 word2vec 模型的发展,以及其变种 sent2vec、doc2vec 等等。这些基于神经网络的技术将单词、句子和文档(分别)向量化,以一种确保生成的向量空间中向量之间距离代表相应实体之间的差异的方式,即单词、句子和文档。事实上,这些嵌入的一些有趣属性允许处理类比—在诱导的向量空间中,单词Man和King之间的距离大约等于单词Woman和Queen之间的距离,例如。用于训练这些基于神经网络的模型的度量来自语言学领域,更具体地说是分布语义学,不需要标记数据。一个单词的含义被假定与其上下文相关联,即周围的单词。
各种嵌入文本单元的方法,例如单词、句子、段落和文档,成为现代自然语言处理的关键基石。一旦文本样本被嵌入到适当的向量空间中,分析通常可以简化为对真实向量操作的众所周知的浅层统计/机器学习技术的应用,包括聚类和分类。这可以看作是一种隐式迁移学习的形式,以及一种半监督机器学习流水线—嵌入步骤是无监督的,学习步骤通常是监督的。无监督的预训练步骤实质上降低了标记数据的要求,从而减少了实现给定性能所需的计算资源—我们将在本书中学习如何利用迁移学习来为更广泛的情景提供服务。
大约在 2014 年,序列到序列模型⁷被开发出来,并在困难任务,如机器翻译和自动摘要中取得显著改进。特别是,尽管在神经网络之前的 NLP 流水线由几个明确的步骤组成,例如词性标注、依存句法分析和语言建模,但后来表明机器翻译可以进行“序列到序列”的处理。在这里,深度神经网络的各个层自动执行了所有这些中间步骤。这些模型学会了通过一个将输入序列(例如一种语言中的源句子)与一个输出序列(例如该句子的另一种语言的翻译)相关联的方法,通过将输入转换成上下文向量的编码器和将其转换成目标序列的解码器。编码器和解码器通常被设计为循环神经网络(RNNs)。这些能够在输入句子中编码顺序信息,这是早期模型(如词袋模型)无法做到的,从而显著提高了性能。
然而,人们发现,长输入序列更难处理,这促使了被称为注意力的技术的发展。这一技术通过让模型关注输入序列中最相关的部分,显著改善了机器翻译序列模型的性能。一个叫做transformer的模型进一步定义了自注意力层,用于编码器和解码器,使两者都能相对于输入序列中的其他文本段构建更好的上下文。这种架构在机器翻译方面取得了显著的改进,并且观察到它更适合在大规模并行硬件上进行训练,将训练速度提高了一个数量级。
直到 2015 年左右,大多数自然语言处理的实用方法都集中在词级别,这意味着整个单词被视为不可分割的原子实体,并被赋予一个特征向量。这种方法有几个缺点,尤其是如何处理从未见过或词汇外的单词。当模型遇到这样的单词时,比如单词拼写错误时,该方法会失败,因为无法对其进行向量化。此外,社交媒体的兴起改变了什么被视为自然语言的定义。现在,数十亿人通过表情符号、新发明的俚语和故意拼错的单词在线表达自己。不久之后,人们意识到,许多这些问题的解决方案自然地来自于以字符级别处理语言。在这个范式中,每个字符都将被向量化,只要人类使用可接受的字符表达自己,就可以成功生成向量特征,并成功应用算法。Zhang 等人⁹在字符级别 CNN 用于文本分类的背景下展示了这一点,并展示了对拼写错误的显著鲁棒性。
传统上,针对任何给定的问题设置——任务、领域和语言的特定组合——学习都是以完全监督或完全无监督的方式进行的,从头开始。如前所述,半监督学习早在 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 迁移学习探索的丰富类比的来源。
尽管本书的目标是自然语言处理,但将 NLP 迁移学习放在计算机视觉迁移学习的背景下进行框架化有助于理解。这样做的原因之一是,来自 AI 的这两个子领域的神经网络架构可能具有某些相似的特征,因此可以借鉴计算机视觉的方法,或者至少用它们来指导 NLP 的技术。事实上,计算机视觉领域中这些技术的可用性被认为是最近 NLP 迁移学习研究的一个重要驱动因素。研究人员可以访问一个定义良好的计算机视觉方法库,以在相对未被探索的 NLP 领域进行实验。然而,这些技术直接可转移的程度是一个开放的问题,有几个重要的区别需要注意。一个这样的区别是,NLP 神经网络通常比计算机视觉中使用的神经网络要浅。
计算机视觉或视觉机器人的目标是使计算机理解数字图像和/或视频,包括获取、处理和分析图像数据,并根据它们的派生表示做出决策。视频分析通常可以通过将视频分成帧来进行,然后可以将其视为图像分析问题。因此,理论上计算机视觉可以被提出为图像分析问题而不失一般性。
计算机视觉诞生于 20 世纪中期,与人工智能一起出现。显然,视觉是认知的重要部分,因此致力于建造智能机器人的研究人员早期就认识到它的重要性。上世纪六十年代,首批方法试图模仿人类视觉系统,而上世纪七十年代人们更加关注提取边缘和场景中形状建模。上世纪八十年代,各个方面的计算机视觉方法越来越成熟,尤其是人脸识别和图像分割,到了上世纪九十年代出现了数学严谨的方法。这个时期正值机器学习流行的时期,正如我们前面所提到的。接下来的几十年,致力于为图像开发更好的特征提取方法。在应用浅层机器学习技术之前,进行努力和重心在此。2012 年的“ImageNet 时刻”,当 GPU 加速的神经网络第一次在广受关注的 ImageNet 比赛中大幅领先时,标志着该领域的革命。
在 ImageNet 每年的标志性比赛中获胜的各个团队非常慷慨地共享了他们的预训练模型。以下是一些值得注意的 CNN 模型示例。
VGG 架构最初是在 2014 年引入的,具有 VGG16(深度为 16)和 VGG19(深度为 19 层)两个变种。为了使更深的网络在训练过程中收敛,需要首先训练较浅的网络直至收敛,然后使用它的参数初始化更深的网络。该架构被发现在训练过程中有些慢,而且参数总数相对较大——约为 1.3 亿至 1.5 亿个参数。
2015 年 ResNet 架构解决了其中一些问题。尽管更深层,但参数数量显著减少——最小的变种 ResNet50 深 50 层,约有 5000 万个参数。实现这种减少的关键是通过一种称为 最大池化 的技术进行正则化,并通过子构建块的模块化设计。
其他值得注意的例子包括 Inception 及其扩展 Xception,分别于 2015 年和 2016 年提出,旨在通过在同一网络模块中堆叠多个卷积来创建多个级别的特征提取。这两个模型都进一步显著减小了模型大小。
在图 1.6 中显示了在前馈神经网络中选择要微调的一部分层的可视化。随着目标领域中的数据量增加,阈值从输出(向输入)移动,阈值和输出之间的层被重新训练。这种变化是因为增加的数据量可以有效地用于训练更多的参数,而否则是无法完成的。此外,阈值的移动方向必须是从右到左,即远离输出端,接近输入端。这种移动方向使我们能够保留编码接近输入端的一般特征的层,同时重新训练接近输出端的层,它们编码源领域特定特征。而且,当源领域和目标领域高度不同的时候,一些阈值右侧的更具体的参数/层可以被丢弃。
另一方面,特征提取涉及仅移除网络的最后一层,该层不再产生数据标签,而是产生一组数值向量,可以通过浅层机器学习方法(如支持向量机 SVM)进行训练,就像以前一样。
在重新训练或微调方法中,先前的预训练权重并不全部保持不变,而是允许其中的一个子集根据新的标记数据进行改变。然而,重要的是要确保在有限的新数据上训练的参数数量不会导致过度拟合,这促使我们冻结一些参数以减少正在训练的参数的数量。通常是以经验的方式来选择要冻结的层数,图 1.6 中的启发式方法指导了这一点。
图 1.6 表现了在计算机视觉中适用于前馈神经网络架构的各种迁移学习启发式方法的可视化,在 NLP 中我们将尽可能利用它。随着目标领域中的训练数据的增加,阈值向左移动,它右侧的所有参数都被重新训练,除了那些由于源领域和目标领域越来越不同而被丢弃的参数。
在 CNN 中已经确定,靠近输入层的早期层—执行更一般的图像处理任务的功能,例如检测图像中的任何边缘。 靠近输出层的后期层—执行更特定于手头任务的功能,例如将最终的数值输出映射到特定标签。 这种安排导致我们首先解冻和微调靠近输出层的层,然后逐渐解冻和微调接近输入层的层,如果发现性能不满意,这个过程将继续,只要目标任务的可用标记数据集能够支持训练参数的增加。
这个过程的一个推论是,如果目标任务的标记数据集非常大,整个网络可能都需要被微调。另一方面,如果目标数据集很小,就需要仔细考虑目标数据集与源数据集的相似程度。如果非常相似,模型体系结构可以直接初始化为预训练权重进行微调。如果非常不同,当初始化时,放弃一些网络的后续层的预训练权重可能会对目标任务没有任何相关性。此外,由于数据集不是很大,在微调时应该只解冻剩余后续层的一小部分。
我们将进行计算实验,以进一步探索这些启发式方法。
现在我们已经在整体人工智能和机器学习领域的背景下框定了 NLP 的当前状态,我们可以很好地总结为什么本书的主题重要,以及为什么您作为读者应该非常关心这个主题。
到目前为止,显而易见的是,近年来这一领域的进展迅速加速。许多预训练语言模型首次提供,同时也提供了明确定义的程序,用于对其进行更具体的任务或领域的微调。人们发现可以类比于计算机视觉领域进行迁移学习的方式,一些研究小组能够迅速借鉴现有的计算机视觉技术,推动我们对 NLP 迁移学习的了解的进展。这项工作取得了重要的优势,即为那些没有大量资源的普通从业者减少了这些问题的计算和训练时间要求。
目前该领域存在着大量的激动人心的研究,并且大量的研究人员正在从事这个问题领域的研究。在这个新颖的学科中存在许多未解决的问题,这为机器学习研究人员通过帮助推动知识水平的提高而使自己出名提供了机会。同时,社交媒体已经成为人类互动中越来越重要的因素,它带来了在自然语言处理中以前未曾见过的新挑战。这些挑战包括俚语/行话和表情符号的使用,这些在通常用于训练语言模型的更正式语言中可能找不到。一个示例是在社交媒体自然语言生态系统中发现的严重漏洞——尤其是关于主权民主国家针对其他外国政府的选举干预指控,比如剑桥分析丑闻。此外,对“假新闻”问题恶化的一般感觉增加了人们对该领域的兴趣,并推动了在构建这些系统时应考虑的道德问题的讨论。所有这些,加上在各个领域不断增长的越来越复杂的聊天机器人的增加,以及相关的网络安全威胁,意味着自然语言处理中的迁移学习问题有望继续增长其重要性。
人工智能(AI)承诺着从根本上改变我们的社会。为了使这种转变的好处普及化,我们必须确保最新的进展对每个人都是可访问的,无论其语言、获取大规模计算资源的能力和出生国是什么。
机器学习是人工智能中主要的现代范式,它不是为每种可能的情况明确地编程计算机,而是通过看到许多这样对应的输入-输出对的例子,训练它将输入与输出信号关联起来。
自然语言处理(NLP)是我们将在本书中讨论的人工智能的子领域,它涉及对人类自然语言数据的分析和处理,是当今人工智能研究中最活跃的领域之一。
近年来在自然语言处理领域中流行的一种范式,迁移学习,使你能够将从一个任务或领域中获得的知识适应或迁移到另一个任务或领域。这对于自然语言处理的民主化以及更广泛地说是人工智能,是一个重要的进步,使得知识可以在新环境中以前所需资源的一小部分重新使用,而这些资源可能并不是所有人都能得到的。
关键的建模框架,使得在自然语言处理中实现迁移学习成为可能,包括 ELMo 和 BERT。
社交媒体重要性的近期上升改变了什么被认为是自然语言的定义。现在,数十亿人在网上使用表情符号、新创造的俚语和故意拼写错误的单词来表达自己。所有这些都提出了新的挑战,在开发新的自然语言处理迁移学习技术时我们必须考虑到这些挑战。
在计算机视觉中,迁移学习相对较为成熟,因此在尝试新的自然语言处理迁移技术时,我们应尽可能借鉴这一知识体系。
K. Schwab,《第四次工业革命》(日内瓦:世界经济论坛,2016 年)。
J. Devlin 等人,“BERT: 深度双向转换器的预训练”,arXiv (2018)。
F. Chollet,《Python 深度学习》(纽约:Manning Publications,2018 年)。
T. Mikolov 等人,“词表示在向量空间中的高效估计”,arXiv (2013)。
M. Pagliardini 等人,“使用组合 n-Gram 特征的句子嵌入的无监督学习”,NAACL-HLT 论文集 (2018)。
Q. V. Le 等人,“句子和文档的分布式表示”,arXiv (2014)。
I. Sutskever 等人,“序列到序列学习的神经网络”,NeurIPS 论文集 (2014)。
A. Vaswani 等人,“注意力就是一切”,NeurIPS 论文集 (2017)。
X. Zhang 等人,“用于文本分类的字符级卷积网络”,NeurIPS 论文集 (2015)。
P. Azunre 等人,“基于字符级卷积神经网络的表格数据集的语义分类”,arXiv (2019)。
M. E. Peters 等人,“深层上下文化词表示”,NAACL-HLT 论文集 (2018)。
J. Howard 等人,“通用语言模型微调用于文本分类”,第 56 届计算语言学年会论文集 (2018)。
J. Devlin 等人,“BERT: 深度双向转换器的预训练”,NAACL-HLT 论文集 (2019)。
J. Deng 等人,“ImageNet:一个大规模分层图像数据库”,NAACL-HLT 论文集 (2019)。
K. Schaffer,《数据与民主:大数据算法如何塑造观点并改变历史进程》(纽约:Apress,2019 年)。
本章包括
介绍一对自然语言处理(NLP)问题
获取和预处理用于此类问题的自然语言处理数据
使用关键的广义线性方法为这些问题建立基线
在本章中,我们直接着手解决自然语言处理问题。这将是一个分为两部分的练习,横跨本章和下一章。我们的目标是为一对具体的自然语言处理问题建立一组基线,以便稍后用于衡量利用越来越复杂的迁移学习方法获得的渐进改进。在此过程中,我们旨在提升您的一般自然语言处理直觉,并更新您对为此类问题设置问题解决流程所涉及的典型程序的理解。您将复习从分词到数据结构和模型选择等技术。我们首先从头开始训练一些传统的机器学习模型,为这些问题建立一些初步的基线。我们在第三章中完成练习,在那里我们将最简单形式的迁移学习应用于一对最近流行的深度预训练语言模型。这涉及在目标数据集上仅微调每个网络的最终几层。这项活动将作为本书主题——自然语言处理的迁移学习的实际动手介绍的一种形式。
我们将专注于一对重要的代表性示例自然语言处理问题:电子邮件的垃圾分类和电影评论的情感分类。这个练习将装备您一些重要的技能,包括一些获取、可视化和预处理数据的技巧。我们将涵盖三种主要的模型类别:广义线性模型,如逻辑回归,基于决策树的模型,如随机森林,以及基于神经网络的模型,如 ELMo。这些类别另外由具有线性核的支持向量机(SVM),梯度提升机(GBM)和 BERT 所代表。要探索的不同类型的模型如图 2.1 所示。请注意,我们不明确讨论基于规则的方法。这些方法的一个广泛使用的示例是简单的关键词匹配方法,该方法会将包含某些预先选择的短语的所有电子邮件标记为垃圾邮件,例如,“免费彩票”作为垃圾邮件,“了不起的电影”作为正面评价。这些方法通常作为许多工业应用中解决自然语言处理问题的首次尝试,但很快被发现脆弱且难以扩展。因此,我们不再深入讨论基于规则的方法。我们在本章讨论这些问题的数据及其预处理,并引入和应用广义线性方法。在下一章,作为整体练习的第二部分,我们将决策树方法和神经网络方法应用于数据。
图 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 进行对比。
在本节中,我们介绍了本章将要讨论的第一个示例数据集。在这里,我们有兴趣开发一个算法,它可以在规模上检测任何给定的电子邮件是否为垃圾邮件。为此,我们将从两个独立的来源构建数据集:流行的恩隆电子邮件语料库作为非垃圾邮件的代理,以及一系列“419”欺诈邮件作为垃圾邮件的代理。
我们将把这看作是一个监督分类任务,在这个任务中,我们将首先在一组被标记为垃圾邮件或非垃圾邮件的电子邮件上训练一个分类器。虽然在线上存在一些标记数据集用于训练和测试,与这个问题密切相关,但我们将采取另一种方式,从一些其他知名的电子邮件数据源创建我们自己的数据集。这样做的原因是更贴近实践中数据收集和预处理通常发生的方式,其中数据集首先必须被构建和筛选,而不是文献中这些过程通常被简化的方式。
尤其是,我们将采样安然公司语料库——最大的公开电子邮件收集,与臭名昭著的安然金融丑闻有关——作为非垃圾邮件的代理,以及采样“419”欺诈邮件,代表最为知名的垃圾邮件类型,作为垃圾邮件的代理。这两种类型的电子邮件都可以在 Kaggle 上公开获取,³,⁴,这是一个流行的数据科学竞赛平台,这使得在那里运行示例特别容易,而不需要太多的本地资源。
安然语料库包含大约五十万封由安然公司员工撰写的电子邮件,由联邦能源委员会收集,用于调查该公司的倒闭。这个语料库在文献中被广泛用于研究用于电子邮件应用的机器学习方法,并且通常是研究人员与电子邮件一起进行初步算法原型实验的首选数据源。在 Kaggle 上,它作为一个单列.csv 文件提供,每行一个电子邮件。请注意,与许多实际应用中可能找到的情况相比,此数据仍然更干净。
图 2.2 显示了在这个示例中将在每封电子邮件上执行的步骤序列。电子邮件的正文将首先与电子邮件的标头分开,将提取一些关于数据集的统计信息以了解数据的情况,将从电子邮件中删除停用词,然后将其分类为垃圾邮件或非垃圾邮件。
图 2.2 对输入电子邮件数据执行的预处理任务序列
我们需要做的第一件事是使用流行的 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 ID、To、From 等——被称为消息的 头信息 或简称头部。
传统的垃圾邮件分类方法是从头信息中提取特征来对消息进行分类。在这里,我们希望仅基于消息内容执行相同的任务。采用该方法的一个可能动机是,由于隐私问题和法规的原因,电子邮件训练数据在实践中经常会被去标识化,因此头部信息是不可用的。因此,我们需要在数据集中将头部信息与消息分离。我们通过下面的函数来实现这一点。它使用了 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\.
加载了 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.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_x
和train_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 数据集比我们组装的电子邮件数据集更为准备充分,因此该练习将更为简短。然而,鉴于数据按类别分开放置在不同文件夹中,这是一个突出显示不同类型预处理要求的机会。
在本节中,我们对将在本章中分析的第二个示例数据集进行预处理和探索。这第二个示例涉及将 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 笔记本中。
有了这个数值表示准备好后,我们现在继续在后续部分为这两个示例数据集构建基线分类器。我们从下一节开始使用广义线性模型。
传统上,在任何应用数学领域模型的发展都是从线性模型开始的。这些模型是保留输入和输出空间中的加法和乘法的映射。换句话说,对一对输入的净响应将是对每个单独输入的响应的总和。这个属性使得相关的统计和数学理论显著减少。
在这里,我们使用了来自统计学的线性的宽松定义,即广义线性模型。设 Y 是输出变量或响应的向量,X 是独立变量的向量,β 是要由我们的分类器进行训练的未知参数的向量。广义线性模型由图 2.3 中的方程定义。
图 2.3 广义线性模型方程
在这里,E[] 代表所包含数量的期望值,右侧在 X 中是线性的,并且 g 是将这个线性数量链接到 Y 的期望值的函数。
在本节中,我们将应用一对最广泛使用的广义线性机器学习算法到前一节介绍的一对示例问题上——逻辑回归和带有线性核的支持向量机(SVM)。其他流行的广义线性机器学习模型不包括简单的带有线性激活函数的感知器神经架构、潜在狄利克雷分配(LDA)和朴素贝叶斯。
逻辑回归通过使用逻辑函数估计概率,对分类输出变量和一组输入变量之间的关系进行建模。假设存在单个输入变量 x 和单个输出二进制变量 y,其相关概率为 P(y=1)=p,则逻辑方程可以表达为图 2.4 中的方程。
图 2.4 逻辑回归方程
这可以重新组织,以得到图 2.5 中显示的典型逻辑曲线方程。
图 2.5 重组后的典型逻辑回归方程
这个方程在图 2.6 中绘制。从历史上看,这条曲线起源于对细菌种群增长的研究,初始生长缓慢,中间爆炸性增长,随着资源耗尽,生长逐渐减弱。
图 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 分数——精确度和召回率的调和平均值——在两者之间取得平衡,对于不平衡的数据集特别有用。这是实际中最常见的情况,因此这个指标非常重要。然而,记住我们迄今为止构建的数据集大致是平衡的。因此,在我们的情况下,准确度是一个合理的度量。
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。
在任何感兴趣的问题上尝试各种算法以找到模型复杂度和性能的最佳组合以符合您特定的情况是很常见的。
基线通常从最简单的算法开始,例如逻辑回归,并逐渐变得越来越复杂,直到得到正确的性能/复杂性权衡。
机器学习实践的一大部分涉及为您的问题组装和预处理数据,目前这可能是该过程中最重要的部分。
重要的模型设计选择包括评估性能的指标、指导训练算法的损失函数以及最佳验证实践等,这些因模型和问题类型而异。
A.L. Maas 等人,“学习词向量进行情感分析”,NAACL-HLT 会议论文集 (2018)。
本章内容包括
分析一对自然语言处理(NLP)问题
使用关键的传统方法建立问题基线
使用代表性的深度预训练语言模型 ELMo 和 BERT 进行基线
在本章中,我们继续直接深入解决 NLP 问题,这是我们在上一章开始的。我们继续追求建立一套具体 NLP 问题的基线,稍后我们将能够利用这些基线来衡量从越来越复杂的迁移学习方法中获得的逐渐改进。我们完成了我们在第二章开始的练习,那里我们介绍了一对实际问题,预处理了相应的数据,并通过探索一些广义线性方法开始了基线。特别是,我们介绍了电子邮件垃圾邮件和互联网电影数据库(IMDB)电影评论分类示例,并使用了逻辑回归和支持向量机(SVM)来建立它们的基线。
在本章中,我们探讨基于决策树和基于神经网络的方法。我们研究的基于决策树的方法包括随机森林和梯度提升机。关于基于神经网络的方法,我们将最简单形式的迁移学习应用到了一对最近流行的深度预训练语言模型 ELMo 和 BERT 上。这项工作只涉及在目标数据集上对每个网络的最后几层进行微调。这项活动将作为本书主题的应用性实践介绍,即 NLP 的迁移学习。此外,我们探索通过超参数调优来优化模型的性能。
我们将在下一节中探讨基于决策树的方法。
决策树是一种决策支持工具,它将决策及其后果建模为树——一个图,其中任意两个节点都由一条路径连接。树的另一种定义是将输入值转换为输出类别的流程图。有关这种类型模型的更多详细信息,请参阅第一章。
在本节中,我们将两种最常见的基于决策树的方法——随机森林和梯度提升机——应用到我们的两个正在运行的问题上。
随机森林(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 评论问题比电子邮件分类问题更难。
这种基于决策树的机器学习算法的变体迭代地学习新的基于决策树的模型,以解决前次迭代模型的弱点。在撰写本文时,它们被普遍认为是解决非感知机器学习问题的最佳类方法。不幸的是,它们确实存在一些缺点,包括较大的模型大小、过拟合的风险更高以及比其他一些决策树模型更少的可解释性。
训练梯度提升机(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 曲线下面积可用作性能的度量。
图 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”这样的词可能会被积极的评论者使用。
注意:重要性分数在这个示例中似乎效果很好,但不应该一味地相信它们。例如,已经广泛认识到这些重要性分数可能对连续变量以及高基数分类变量有偏见。
现在我们开始应用一些神经网络模型到我们两个运行的示例中,可以说神经网络是当今自然语言处理中最重要的模型类别之一。
正如我们在第一章讨论的那样,神经网络是处理感知问题(如计算机视觉和自然语言处理)最重要的机器学习算法类别之一。
在本节中,我们将在本章和上一章中基线化的两个示例问题上训练两个代表性的预训练神经网络语言模型。我们考虑来自语言模型的嵌入 (ELMo) 和来自变压器的双向编码器表示 (BERT)。
图 3.2 在 IMDB 分类示例中由梯度提升机分类器发现的各种标记的重要性分数
ELMo 包含卷积和循环(特别是长短期记忆 [LSTM])元素,而合适命名的 BERT 是基于变压器的。这些术语在第一章中介绍过,并将在后续章节中更详细地讨论。我们采用了最简单的迁移学习微调形式,在对应的预训练嵌入之上,通过我们前几节的标签数据集训练了一个单独的密集分类层。
语言模型嵌入(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 在那个环境中能很好地工作。
图 3.3 显示了在电子邮件分类示例中训练 ELMo 模型的前五个时代的验证和训练准确性比分的趋势。
每个时代需要大约 10 秒来完成——这些信息是由我们的代码打印的。我们看到第四个时代达到了约 97.3%的验证准确性(意味着在不到一分钟的时间内达到了结果)。这个性能与 Logistic Regression 方法的性能相当,后者只稍好一点,为 97.7%(也见表 3.1)。我们注意到这个算法的行为是随机的——它的运行在每次运行时都表现出不同的行为。因此,即使在与我们使用的类似的架构上,你自己的收敛性也会有所不同。实践中通常尝试几次运行算法,并在随机和不同的结果中选择最佳的参数集。最后,我们注意到训练和验证准确性的背离表明了过拟合的开始,正如图中所示的那样。这证实了将通过增加超参数maxtokenlen
指定的标记长度以及通过maxtokens
指定的每封电子邮件的标记数量来增加信号量的数量可能会进一步提高性能的假设。自然地,通过打开Nsamp
来增加每类样本的数量也应该有助于提高性能。
对于 IMDB 示例,ELMo 模型代码产生了图 3.4 所示的收敛输出。
图 3.4 显示了在 IMDB 电影评论分类示例上训练 ELMo 模型的前五个时代的验证和训练准确性比分的趋势。
每个时代再次需要大约 10 秒,并且在第二个时代不到一分钟的时间内就实现了约 70%左右的验证准确性。我们将看到如何在本章的下一个和最后一个部分提高这些模型的性能。请注意,在第三个以及之后的时代,可以观察到一些过拟合的证据,因为训练准确性继续提高——对数据的拟合改善了,而验证准确性仍然较低。
双向编码器表示来自变换器(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% 的验证准确率(在五个时期内)。
在查看本章前几节以及上一章的各种算法的性能结果时,我们可能会立即得出关于我们研究的每个问题中哪种算法表现最佳的结论。例如,我们可能会得出结论,对于电子邮件分类问题,BERT 和逻辑回归是最佳算法,准确率约为 98%,而 ELMo 的准确率也不远,然后是基于决策树的方法和 SVM 排在最后。另一方面,对于 IMDB 电影评论分类问题,BERT 看起来是赢家,性能约为 71%,其次是 ELMo,然后才是逻辑回归。
但是我们必须记住,我们只有在最初评估算法时才能确定这一点是真实的——Nsamp
=1000
,maxtokens
=50
,maxtokenlen
=20
——以及任何特定于算法的默认参数值。要有信心地作出一般性的陈述,我们需要更彻底地探索超参数空间,通过在许多超参数设置下评估所有算法的性能,这个过程通常称为超参数调优或优化。也许通过这个过程找到的每个算法的最佳性能会改变它们的性能排名,而且一般来说,这将帮助我们为我们感兴趣的问题获得更好的准确性。
超参数调优通常最初是通过直觉进行手动操作的。我们在这里描述了这样一种方法,用于超参数Nsamp
、maxtokens
和maxtokenlen
,这些超参数在所有算法中都是通用的。
让我们首先假设初始训练数据量——比如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 电影评论分类问题进行了类似的超参数测试步骤。我们首先将maxtokens
和maxtokenlen
都增加到 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 和逻辑回归。注意,这个问题有更多的改进空间,这与我们早期观察到的这个问题比电子邮件分类问题更难的观察一致。这使我们假设,预训练知识转移对更难的问题有更大的影响,这是直观的。这个概念也符合一般建议,即在有大量标记数据可用时,神经网络模型可能优于其他方法,假设要解决的问题足够复杂,需要额外的数据。
存在一些工具用于对超参数范围进行更系统化和全面的搜索。这些包括 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%,而不会影响在前一小节中达到的每个问题的最佳分类器的结论。
通常会尝试多种算法来解决感兴趣的任何问题,以找到模型复杂性和性能的最佳组合,以适应您的情况。
基线通常从最简单的算法开始,例如逻辑回归,然后逐渐变得更复杂,直到达到正确的性能/复杂性折衷。
重要的模型设计选择包括用于评估性能的指标,用于指导训练算法的损失函数以及最佳验证实践,等等,这些可以根据模型和问题类型而异。
超参数调整是模型开发流程的重要步骤,因为初始超参数设置可能严重误代表通过调整可以找到的最佳性能。
简单模型在可用数据量不大和/或问题较简单时往往效果最佳,而复杂的神经网络模型在有更多数据可用时往往表现更好,因此值得额外复杂性,当更多数据可用时。
P. Goyal 等人,“准确的、大型小批次 SGD:在 1 小时内训练 ImageNet”,arXhiv(2018 年)。
第 4、5 和 6 章深入研究了基于浅层神经网络的一些重要迁移学习自然语言处理方法,也就是相对层数较少的神经网络。它们还开始探索深度迁移学习,通过使用递归神经网络(RNNs)作为关键功能的代表性技术,比如 ELMo。
本章包括
以半监督的方式使用预训练的词嵌入将预训练知识转移到问题中
以半监督的方式使用预训练的较大文本部分的嵌入来将预训练知识转移到问题中
使用多任务学习来开发性能更好的模型
修改目标域数据以重用来自资源丰富的源域的知识
在本章中,我们将涵盖一些重要的浅层迁移学习方法和概念。这使我们能够探索迁移学习中的一些主要主题,同时在感兴趣的最终类别——浅层神经网络类别的背景下进行。几位作者已经提出了将迁移学习方法分类到不同组别中的各种分类系统。¹,²,³ 大致来说,分类是基于迁移是否发生在不同的语言、任务或数据域之间。每种类型的分类通常相应地被称为 跨语言学习、多任务学习 和 领域自适应,如图 4.1 所示。
图 4.1 将迁移学习划分为多任务学习、领域自适应和跨语言学习的可视化分类
我们将在这里看到的方法涉及到某种程度上是神经网络的组件,但不像第三章中讨论的那样,这些神经网络没有很多层。这就是为什么标签“浅层”适合描述这些方法集合的原因。与上一章一样,我们将这些方法放在特定的实际例子的背景下,以促进您的实际自然语言处理技能的提升。跨语言学习将在本书的后续章节中讨论,因为现代神经机器翻译方法通常是深层的。我们将在本章中简要探讨另外两种迁移学习。
我们首先探讨了一种常见的半监督学习形式,它使用了预训练的词嵌入,如 word2vec,将其应用于本书前两章中的一个示例。请回忆第一章,这些方法与第三章中的方法不同,因为它们产生每个单词一个向量,而不考虑上下文。
我们重新访问了 IMDB 电影评论情感分类。回想一下,此示例涉及将 IMDB 的电影评论根据表达的情感分为积极或消极。这是一个典型的情感分析示例,在文献中被广泛使用来研究许多算法。我们将由预训练的单词嵌入生成的特征向量与一些传统的机器学习分类方法相结合,即随机森林和逻辑回归。然后,我们演示了使用更高级别的嵌入,即将更大的文本部分——句子、段落和文档级别——向量化,可以提高性能。将文本向量化,然后将传统的机器学习分类方法应用于生成的向量的一般思想在图 4.2 中可视化。
图 4.2 使用单词、句子或文档嵌入进行半监督学习的典型步骤序列
随后,我们将涵盖多任务学习,并学习如何同时训练单个系统来执行多个任务——在我们的案例中,分别由上一章的两个示例代表,即电子邮件垃圾分类和 IMDB 电影评论情感分析。你可以从多任务学习中获得几个潜在的好处。通过为多个任务训练单个机器学习模型,可以在更大更多样的来自合并数据池的数据上学习共享表示,这可能导致性能提升。此外,广泛观察到,这种共享表示具有更好的泛化能力,可以推广到未经训练的任务,而且可以在不增加模型大小的情况下实现此改进。我们在我们的示例中探索了其中一些好处。具体地,我们专注于浅层神经多任务学习,其中为设置中的每个特定任务训练了一个额外的密集层以及分类层。不同的任务还共享它们之间的一层,这种设置通常被称为硬参数共享。
最后,我们引入了一个流行的数据集作为本章的另一个运行示例。这就是多领域情感数据集,描述了Amazon.com的一组不同产品的产品评论。我们使用此数据集来探索领域自适应。假设我们有一个源领域,它可以被定义为特定任务的特定数据分布,并且已经训练好在该领域中的数据上表现良好的分类器。领域自适应的目标是修改或适应不同目标领域的数据,以使源领域的预训练知识可以帮助在目标领域中学习。我们应用了一种简单的自动编码方法来将目标领域中的样本“投影”到源领域特征空间中。
自编码器是一个系统,它通过将输入编码成一个有效的潜在表示,然后学习有效解码该表示,从而学习以非常高的准确度重构输入。它们传统上在模型减少应用中被广泛使用,因为潜在表示通常比编码发生的原始空间的维度要小,所选维度值也可以为计算效率和准确度的正确平衡而选择。⁴ 在极端情况下,在目标域中使用无标签数据进行训练可以获得改进,这通常称为 零样本域适应,其中学习发生在目标域中没有标记的数据。在我们的实验中,我们演示了一个例子。
单词嵌入的概念是自然语言处理领域的核心。它是给需要分析的每个单词产生一组实数向量的技术集合的名称。在单词嵌入设计中一个重要的考虑因素是生成向量的维度。更高维度的向量通常可以更好地代表语言中的单词,在许多任务上表现更好,但计算成本也自然更高。选择最优维度需要在这些竞争因素之间取得平衡,通常是经验性的,尽管一些最近的方法提出了更彻底的理论优化方法。⁵
如本书第一章所述,这个重要的 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 中分别使用的那样。 使用这些代码来训练和评估这些模型时,当超参数maxtokens
和maxtokenlen
分别设置为 200 和 100 时,我们发现对应的准确率分别为 77%和 66%,而Nsamp
—每个类的样本数—等于 1,000。 这些只比在前几章最初开发的基于词袋的基线稍低一些(分别对应准确率为 79%和 67%)。 我们假设这种轻微的降低可能是由于聚合个别单词向量的天真平均方法造成的。 在下一节中,我们尝试使用专门设计用于在更高文本级别嵌入的嵌入方法来执行更智能的聚合。
受 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 结合起来的这个值是一个改进,同时也是对于上一节中的平均词嵌入方法的改进。
传统上,机器学习算法一次只能训练执行一个任务,所收集和训练的数据对于每个单独的任务是独立的。这在某种程度上与人类和其他动物学习的方式相矛盾,人类和其他动物的学习方式是同时进行多个任务的训练,从而一个任务的训练信息可能会影响和加速其他任务的学习。这些额外的信息不仅可能提高当前正在训练的任务的性能,还可能提高未来任务的性能,有时甚至在没有关于这些未来任务的标记数据的情况下也可能如此。在目标域中没有标记数据的迁移学习场景通常被称为零样本迁移学习。
在机器学习中,多任务学习在许多场景中历史上出现过,从多目标优化到l2和其他形式的正则化(本身可以被构造为一种多目标优化形式)。图 4.3 展示了我们将要使用的神经多任务学习的形式,其中一些层/参数在所有任务之间共享,即硬参数共享。¹¹
图 4.3 我们将使用的神经多任务学习的一般形式——硬参数共享(在本例中有三个任务)
在另一种突出的神经多任务学习类型中,软参数共享,所有任务都有自己的层/参数,不进行共享。相反,通过对各个任务的特定层施加的各种约束,鼓励它们相似。我们不再进一步讨论这种类型的多任务学习,但了解它的存在对于您自己未来的潜在文献调研是很有好处的。
让我们继续进行本节的实例说明,通过在下一个小节中设置和基线化它。
再次考虑图 4.3,但只有两个任务——第一个任务是前两节中的 IMDB 电影评论分类,第二个任务是前一章中的电子邮件垃圾邮件分类。所得到的设置代表了我们将在本节中解决的具体示例。为了促进概念化,这个设置在图 4.4 中显示。
图 4.4 我们将使用的神经多任务硬参数共享的具体形式,显示了两个特定任务——IMDB 评论和电子邮件垃圾邮件分类
在继续之前,我们必须决定如何将输入转换为用于分析的数字。一种流行的选择是使用字符级别的独热编码对输入进行编码,其中每个字符被维度等于可能字符总数的稀疏向量替换。这个向量在与字符对应的列中包含 1,其他位置为 0。图 4.5 显示了这种方法的插图,旨在帮助您简洁地可视化独热编码过程。
图 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 结合逻辑回归。
现在我们介绍另一个任务:上一章的电子邮件垃圾分类问题。我们不会在这里重复预处理步骤和相关代码,这是一个辅助任务;有关这些详细信息,请参阅第二章。假设数据样本中的邮件对应的 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)
❷ 指定每个输入的训练和验证数据
对于这个实验,我们将超参数 maxtokens
和 maxtokenlen
都设置为 100,Nsamp
(每个类别的样本数)的值设置为 1,000(与上一节相同)。
我们发现,在训练多任务系统时,IMDB 分类性能略有下降,从清单 4.4 中单任务浅层设置的约 82% 下降到约 80%。电子邮件分类的准确性同样从 98.7% 下降到 98.2%。鉴于性能下降,人们可能会合理地问:这一切的意义何在?
首先要注意的是,训练好的模型可以独立地用于每个任务,只需将省略的任务输入替换为零以尊重预期的整体输入维度,并忽略相应的输出。此外,我们期望多任务设置中的共享预训练层 dense1
比清单 4.4 中的更容易泛化到任意新任务。这是因为它已经在更多种类和更一般的数据和任务上进行了训练以进行预测。
为了更具体地说明这一点,考虑将任务特定层中的一个或两个替换为新的层,将共享层dense1
初始化为前一个实验的预训练权重,并在新的任务数据集上对结果模型进行微调。通过观察更广泛范围的任务数据,可能与新添加的任务类似,这些共享权重更有可能包含有用信息,可用于考虑的下游任务。
我们将在本书的后面回到多任务学习的概念,这将为我们提供进一步研究和思考这些现象的机会。本节的实验希望为您提供了进一步探索所需的基础。
在本节中,我们简要探讨了域自适应的概念,这是转移学习中最古老和最显着的想法之一。机器学习实践者经常做出的一个隐含假设是,推断阶段的数据将来自用于训练的相同分布。当然,实践中很少有这种情况发生。
进入域自适应以尝试解决这个问题。让我们将域定义为针对特定任务的数据的特定分布。假设我们有一个源域和一个经过训练以在该域中表现良好的算法。域自适应的目标是修改或调整不同目标域中的数据,以便来自源域的预训练知识可以适用于更快的学习和/或直接推断目标域中的情况。已经探索了各种方法,从多任务学习(如前一节介绍的)—在不同数据分布上同时进行学习—到协同变换—这些方法能够在单个组合特征空间上进行更有效的学习—再到利用源域和目标域之间相似性度量的方法,帮助我们选择哪些数据应该用于训练。
我们采用简单的自编码方法将目标域中的样本“投射”到源域特征空间中。自编码器是一种可以学习以高准确度重构输入的系统,通常是通过将它们编码成高效的潜在表示来学习解码所述表示。描述重构输入过程的技术方法是“学习身份函数”。自编码器在模型维度缩减应用中传统上被大量使用,因为潜在表示通常比编码发生的原始空间的维度小,并且所述维度值也可以被选择为在计算效率和准确性之间达到正确平衡。在极端有利的情况下,您可以在目标域中不需要标记数据的情况下获得改进,这通常被称为零样本域自适应。
零样本迁移学习的概念在许多情境中都出现过。您可以将其视为一种转移学习的“圣杯”,因为在目标域中获取标记数据可能是一项昂贵的任务。在这里,我们探讨了一个分类器是否可以用来预测 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 迁移学习,这是一种理想的情况,因为标记数据收集可能很昂贵。
有时可能会修改或调整目标领域的数据,使其更类似于源领域的数据,例如,通过自编码器等投影方法,这可以提高性能。
S. J. Pan and Q. Yang, “迁移学习综述”,IEEE Knowledge and Data Engineering Transactions(2009)。
S. Ruder, “自然语言处理的神经迁移学习”,爱尔兰国立大学,高威(2019)。
D. Wang and T. F. Zheng, “语音和语言处理的迁移学习”,2015 年亚太信号和信息处理协会年度峰会和会议(APSIPA)。
Jing Wang, Haibo Hea Danil 和 V.Prokhorov, “用于降维的折叠神经网络自动编码器”,计算机科学会议文献 13 (2012): 120-27.
Z. Yin and Y. Shen, “关于词嵌入的维度性”,32 届神经信息处理系统会议(NeurIPS 2018),加拿大蒙特利尔。
J. Alabi 等,“大规模词嵌入与策划词嵌入对低资源语言的影响。约鲁巴语和特威语的情况”,语言资源和评估国际会议(LREC 2020),法国马赛。
S. Ruder, “自然语言处理的神经迁移学习”,爱尔兰国立大学,高威(2019)。
Hal Daumé III,“令人沮丧地简单领域自适应”,计算语言学协会第 45 届年会论文集(2007),捷克布拉格。
S. Ruder 和 B. Plank,“使用贝叶斯优化学习选择数据的迁移学习方法”,2017 年自然语言处理实证方法会议论文集(2017),丹麦哥本哈根。
Jing Wang、Haibo Hea Danil 和 V.Prokhorov,“用于降维的折叠神经网络自动编码器”,Procedia Computer Science 13(2012):120-127。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。