当前位置:   article > 正文

面向自然语言处理的迁移学习(二)_“数据驱动的模式发现”(d3m)项目

“数据驱动的模式发现”(d3m)项目

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

译者:飞龙

协议:CC BY-NC-SA 4.0

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

本章涵盖

  • 循环神经网络(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 来首先为句子中的字符构建内部上下文,然后为文档中的句子构建外部上下文。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

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

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

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

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

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

5.1.1 获取和可视化表格数据

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

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

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

从以前的章节中可以回忆到,“!”符号仅在执行 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())
  • 1
  • 2
  • 3

❶对于我们的目的,.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  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

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

现在我们获取另一个表格数据集。不多赘述,这个数据集将用于扩展我们的 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)
  • 1

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

print(raw_data.head())
  • 1

输出结果为:

   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]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

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

COLUMNS = ["PCT_ELEC_IN_TOT_VOLS","TOT_AV_VOLS"]    ❶
raw_library_data = raw_data[COLUMNS]
print(raw_library_data)
  • 1
  • 2
  • 3

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

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

     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]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

5.1.2 预处理表格数据

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

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

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

from Simon import Simon              ❶
from Simon.Encoder import Encoder    ❷
  • 1
  • 2

❶ 导入 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                               ❷
  • 1
  • 2

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

❷ 对应的模型权重

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

checkpoint_dir = ""                                                    ❶
execution_config = "Base.pkl"                                          ❷
Classifier = Simon(encoder={})                                         ❸
config = Classifier.load_config(execution_config, checkpoint_dir)      ❹
encoder = config['encoder']                                            ❺
checkpoint = config['checkpoint']
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

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

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

❹ 加载模型配置

❺ 提取编码器

❻ 提取检查点名称

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

print(checkpoint)
  • 1

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

text-class.17-0.04.hdf5
  • 1

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

max_cells = encoder.cur_max_cells
  • 1

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

max_len = 20 # maximum length of each tabular cell
Categories = encoder.categories
category_count = len(Categories)print(encoder.categories)
  • 1
  • 2
  • 3
  • 4

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

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

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

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])
  • 1
  • 2
  • 3

❶ 编码数据(标准化、转置、转换为 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]]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

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

X_library = encoder.encodeDataFrame(raw_library_data)
  • 1

在这个阶段,我们已经将示例输入数据集转换成了适当形状的 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))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

❶ 将真实新闻数据读入 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:]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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

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

❸ 分成独立的 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 所示,输入只需要是一个有效的字符,这有助于处理拼写错误。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

6.1.1 通用神经架构概述

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

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

6.1.2 对表格数据进行建模

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

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

❶ 模拟/伪造数据生成实用工具(使用库 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'])]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

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'])
  • 1
  • 2
  • 3

❶ 生成模型

❷ 加载权重

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

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

model.summary() 
  • 1

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

______________________________________________________________________________________
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]        
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

❶ 用于决定类成员身份的概率阈值

❷ 预测棒球数据集列的类别

❸ 将概率转换为类别标签

❹ 显示输出

对应的代码输出如下所示:

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]])
  • 1
  • 2
  • 3
  • 4

回顾第 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  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

现在,假设我们有兴趣在项目中检测具有百分比值的列。我们如何快速使用预训练模型来实现这一点呢?我们可以使用上一章中准备的第二个表格数据集来调查这种情况——多年来的不列颠哥伦比亚公共图书馆统计数据集。当然,第一步是直接使用预训练模型预测这些数据。以下一系列命令实现了这一点:

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

❶ 使用原始框架对数据进行编码

❷ 预测类别

❸ 将概率转换为类标签

这将产生以下输出:

Recall that the column headers were:
['PCT_ELEC_IN_TOT_VOLS', 'TOT_AV_VOLS']
The predicted class/probability:
([('text',), ('int',)], [[0.7253058552742004], [0.7712462544441223]])
  • 1
  • 2
  • 3
  • 4

回顾 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]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

那并不是不正确,但也不完全是我们正在寻找的,因为它不够具体。

我们将快速将预训练模型转移到一个非常小的包含百分比样本的训练数据集。首先让我们使用以下命令了解原始库 DataFrame 的大小:

print(raw_library_data.shape)
  • 1

我们发现尺寸为(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))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

❶ 将数据转换为两个列表

❷ 将其分解为每个样本列 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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

❶ 抓取初始化的最后一层权重

❷ 找到最接近类别的旧权重索引—文本

❸ 使用新的类别列表更新编码器

❹ 对新列表按字母顺序排序

❺ 找到新类别的索引

❻ 将新权重初始化为旧权重

❼ 在百分比权重位置插入文本权重

❽ 在百分比偏差位置插入文本偏差

在执行清单 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)
  • 1

通过此函数返回的转移模型与我们之前构建的模型完全类似,唯一的区别是最终层现在具有新的维度,由输入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'])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

❶ 开始时使所有层都不可训练

❷ 只有最后一层应该是可训练的。

❸ 将最终层的权重设置为先前确定的初始化值

❹ 编译模型

现在我们可以使用以下清单中的代码在新数据上训练构建的、初始化的和编译的转移模型。

清单 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)) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

❷ 编码标签

❸ 准备预期格式的数据 -> 60/30/10 训练/验证/测试数据拆分

❹ 训练数据

我们在图 6.3 中可视化了此代码生成的收敛信息。我们看到在第七个时期实现了 100%的验证准确率,训练时间为 150 秒。看来我们的实验成功了,我们已成功地微调了预训练模型以处理新的数据类!我们注意到,为了使这个新模型能够准确地处理所有 10 个类,我们需要在转移步骤中的训练数据中包含每个类的一些样本。在这个阶段,微调的模型只适用于预测包含在转移步骤中的类——整数百分比。因为我们这里的目标仅仅是说明性的,我们将此作为读者的警告,并不进一步关注。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

❶预测类别

❷将概率转换为类标签

❸ 检查

生成的输出如下:

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]])
  • 1
  • 2
  • 3
  • 4

我们发现,微调模型已经完全正确地预测了每个例子,进一步验证了我们的迁移学习实验。

最后要记住,通过在 6.1.2 节中描述的适应过程,SIMOn 框架可以应用于任意输入文本,而不仅仅是表格数据。几个应用示例取得了有希望的结果。¹希望本节的练习已经充分准备您在自己的分类应用程序中部署它,并通过迁移学习将生成的分类器适应新情况。

现在我们将继续探讨将 ELMo 应用于虚假新闻分类示例的情况。

6.2 来自语言模型的嵌入(ELMo)

如前一章节简要提到的,来自语言模型的嵌入(ELMo)可以说是与正在进行的 NLP 迁移学习革命相关的最受欢迎的早期预训练语言模型之一。它与 SIMOn 有一些相似之处,因为它也由字符级 CNN 和双向 LSTM 组成。请参考图 5.2,这里重复了一遍,以便鸟瞰这些建模组件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 中的公式,前向语言模型计算句子的概率为第一个词在句子中是“”的概率乘以第二个词是“可以”的概率,假设第一个词是“”,再乘以第三个词是“”的概率,假设前两个词是“你可以”。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.4 前向语言模型方程

一个以单词为单位的反向语言模型做的是相同的事情,但是反过来,如图 6.5 中的方程所示。它通过对每个令牌在右到左令牌历史条件下的概率的乘积来建模序列的联合概率。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.5 反向语言模型方程。

再次考虑这个简短的句子,“你可以”。根据图 6.5 中的公式,反向语言模型计算句子的概率为最后一个词在句子中是“”的概率乘以第二个词是“可以”的概率,假设最后一个词是“”,再乘以第一个词是“”的概率,假设其他两个词是“可以是”。

一个双向语言模型结合了前向和后向模型。ELMo 模型特别寻求最大化两个方向的联合对数似然——在图 6.6 中显示的量。请注意,尽管为前向和后向语言模型保留了单独的参数,但令牌向量和最终层参数在两者之间是共享的。这是第四章讨论的软参数共享多任务学习场景的一个例子。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

❶ 输出 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

dense_1dense_2层是添加到第 3.4 节产生的预训练嵌入之上的新的全连接层。预训练嵌入是elmo_embedding_layer_1。请注意,打印的模型摘要显示它有四个可训练参数。这四个参数是前面子节中描述的内部双向 LSTM 状态的线性组合中的权重。如果您像我们这样使用 TensorFlow Hub 方法使用预训练的 ELMo 模型,则 ELMo 模型的其余部分不可训练。然而,可以使用模型库的另一个版本构建一个完全可训练的基于 TensorFlow 的 ELMo 模型。

当我们在假新闻数据集上执行前述代码时所达到的收敛结果如图 6.7 所示。我们看到,达到了超过 98%的准确率。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 的情况。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7.1:转换器架构的高级表示,显示堆叠的编码器、解码器、输入/输出嵌入和位置编码

我们从图中可以看到,在架构的编码或左侧上堆叠了相同的编码器。堆叠编码器的数量是一个可调的超参数,原始论文中使用了六个。同样,在解码或右侧上,堆叠了六个相同的解码器。我们还看到,使用所选的嵌入算法将输入和输出转换为向量。这可以是诸如 word2vec 的词嵌入算法,甚至可以是应用于使用 one-hot 编码的字符向量的类似于我们在前一章中遇到的那些卷积神经网络。此外,我们使用位置编码来编码输入和输出的顺序性。这使我们可以舍弃循环组件,同时保持顺序感知性。

每个编码器都可以粗略地分解为一个自注意层,紧随其后是一个前馈神经网络,如图 7.2 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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
  • 1
  • 2

注意:回想一下前面的章节,感叹号(!)只在 Jupyter 环境中执行时需要,比如我们推荐的 Kaggle 环境中。在通过终端执行时,它应该被去掉。

tensor2tensor 包含了 transformers 架构的原始作者实现,以及一些可视化工具。bertviz 库是这些可视化工具对 transformers 库中大量模型的扩展。注意,要渲染可视化内容需要激活 JavaScript(我们会在相关 Kaggle 笔记本中告诉你如何做)。

transformers 库可以通过以下方式安装:

!pip install transformers
  • 1

注意,它已经安装在 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)
  • 1
  • 2
  • 3

❶ 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)
  • 1
  • 2
  • 3

❶ 将 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)
  • 1
  • 2
  • 3

我们可以通过在inputs变量上执行以下代码轻松地返回一个 PyTorch 张量,只需设置return_tensors='pt'。要查看这些索引对应的标记,可以执行以下代码:

tokens = tokenizer.convert_ids_to_tokens(list(inputs[0]))print(tokens)
  • 1
  • 2

❶ 从输入列表的列表中提取批次索引 0 的示例

这产生以下输出:

['[CLS]', 'he', 'didn', '##t', 'want', 'to', 'talk', 'about', 'cells', 'on', 'the', 'cell', 'phone', 'because', 'he', 'considered', 'it', 'boring', '[SEP]']
  • 1

我们立即注意到,通过编码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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

❶ bertviz 注意力头可视化方法

❷ 功能用于显示多头注意力

❸ 一定要在 bertviz 中使用 PyTorch。

❹ 获取注意力层

❺ 调用内部 bertviz 方法来显示自注意力

❻ 调用我们的函数来渲染可视化

图 7.3 显示了我们示例句子的最终 BERT 层的自注意力可视化的结果。您应该使用可视化并滚动浏览各层各个词的可视化。注意,并非所有注意力可视化都像这个示例那样容易解释,这可能需要一些练习来建立直觉。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7.3 我们示例句子的预训练非大小写 BERT 模型的最终编码层中的自注意可视化。它显示“细胞”与“它”和“无聊”相关联。请注意,这是一个多头视图,每个单列中的阴影代表一个头。多头注意力在第 7.1.2 节中详细讨论。

就是这样!现在我们对自注意力的作用有了一定的了解,通过在图 7.3 中进行可视化,让我们进入它的数学细节。我们首先从下一小节中的自注意力开始,然后在之后将我们的知识扩展到完整的多头上下文中。

7.1.2 自注意力

再次考虑例句,“他不想谈论手机上的细胞,因为他认为这很无聊。”假设我们想弄清楚形容词“boring”描述的是哪个名词。能够回答这样的问题是机器需要具备的理解上下文的重要能力。我们知道它指的是“它”,而“它”指的是“细胞”,很自然。这在我们在图 7.3 中的可视化中得到了证实。机器需要被教会这种上下文意识。自注意力是在变压器中实现这一点的方法。当输入中的每个标记被处理时,自注意力会查看所有其他标记以检测可能的依赖关系。回想一下,在上一章中我们通过双向 LSTM 实现了相同的功能。

那么自注意力是如何实际工作以实现这一目标的呢?我们在图 7.4 中可视化了这个关键思想。在图中,我们正在计算单词“boring”的自注意力权重。在进一步详细说明之前,请注意一旦获取了各个单词的各种查询、键和值向量,它们就可以被独立处理。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7.4 我们示例句子中单词“boring”的自注意力权重计算的可视化。请注意,一旦创建了键、值和查询向量,可以独立地计算这些单词的不同权重的计算。这是变压器在循环模型之上增加的可并行性的根源。注意系数是图 7.3 中多头注意力中任何给定列的阴影强度的可视化。

每个单词都与一个查询向量(q)、一个向量(k)和一个向量(v)相关联。这些向量是通过将输入嵌入向量与在训练过程中学习到的三个矩阵相乘得到的。这些矩阵在所有输入标记中都是固定的。如图所示,当前单词 “boring” 的查询向量与每个单词的键向量进行点积。结果被一个固定常数——键和值向量维度的平方根——进行缩放,并输入到一个 softmax 函数中。输出向量产生的注意力系数表示当前标记 “boring” 与序列中每个其他标记之间关系的强度。请注意,该向量的条目表示我们在图 7.3 中可视化的多头注意力中任何给定单列中阴影的强度。接下来,为了方便起见,我们重复了图 7.3,这样您就可以检查不同行之间阴影变化的可变性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7.3(重复)预训练的不分大小写 BERT 模型在我们示例句子的最终编码层中的自注意可视化。它显示了 “cells” 与 “it” 和 “boring” 相关联。请注意,这是一个多头视图,每个单列中的阴影代表一个头。

现在我们有了足够的条件来理解为什么变压器比循环模型更具并行性。回想一下我们的介绍,不同单词的自注意力权重的计算可以在创建键、值和查询向量后独立进行。这意味着对于长输入序列,可以并行化这些计算。回想一下,循环模型本质上是顺序的——任何给定位置 t 处的内部隐藏状态取决于前一个位置 t-1 处的隐藏状态。这意味着无法在循环模型中并行处理长输入序列,因为步骤必须依次执行。另一方面,对于这样的输入序列,跨序列的并行化很快就会遇到 GPU 内存限制。变压器模型比循环模型的另一个优势是由注意力可视化提供的增加的可解释性,比如图 7.3 中的可视化。

请注意,可以独立地计算序列中每个标记的权重,尽管通过键和值向量存在一些计算之间的依赖关系。这意味着我们可以使用矩阵对整体计算进行向量化,如图 7.5 所示。在该方程中,矩阵 Q、K 和 V 简单地是由查询、键和值向量堆叠在一起形成的矩阵。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7.5 使用矩阵对整个输入序列进行向量化的自注意力计算

到底多头注意力有什么作用?既然我们已经介绍了自注意力,那么现在是一个很好的时机来解决这个问题。从单列的角度,我们已经将多头注意力隐式地作为自注意力的一般化呈现,如图 7.3 中的阴影部分,变为了多列。让我们思考一下,当我们寻找与“无聊”相关的名词时,我们具体做了什么。从技术上讲,我们是在寻找名词-形容词的关系。假设我们有一个跟踪这类关系的自注意力机制。如果我们还需要跟踪主-谓关系呢?还有其他可能的关系呢?多头注意力通过提供多个表示维度来解决这个问题,而不仅仅是一个。

7.1.3 残差连接、编码器-解码器注意力和位置编码

Transformer 是一种复杂的架构,具有许多特性,我们将不像自注意力那样详细介绍。精通这些细节对于您开始将 Transformer 应用于自己的问题并不是至关重要的。因此,我们在这里只是简要总结它们,并鼓励您随着获得更多经验和直觉的时间不断深入学习原始资源材料。

作为第一个这样的特性,我们注意到图 7.2 中简化的编码器表示中没有显示编码器中每个自注意层和接下来的规范化层之间的附加残差连接。这在图 7.6 中有所说明。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7.6 更详细和准确地拆分每个 Transformer 编码器,现在包括残差连接和规范化层

如图所示,每个前馈层在其后都有一个残差连接和一个规范化层。类似的说明也适用于解码器。这些残差连接使得梯度能够跳过层内的非线性激活函数,缓解了梯度消失和/或梯度爆炸的问题。简单地说,规范化确保所有层的输入特征的尺度大致相同。

在解码器端,回顾图 7.2 中编码器-解码器注意力层的存在,这一点我们还没有讨论到。接下来,我们复制图 7.2 并突出显示该层以方便您查看。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7.2(复制,突出显示编码器-解码器注意力)将编码器和解码器简化为自注意、编码器-解码器注意力和前馈神经网络的分解形式

它的工作方式类似于所描述的自我关注层。重要的区别在于,表示键和值的每个解码器的输入向量来自编码器堆栈的顶部,而查询向量来自直接位于其下面的层。如果您再次查看图 7.4,并记住这个更新后的信息,您应该会发现这种变化的效果是计算每个输出标记和每个输入标记之间的注意力,而不是像自我关注层的情况那样在输入序列的所有标记之间计算。接下来我们将复制图 7.4——稍作调整以适用于编码器-解码器注意力——让您自己看一看。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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")
  • 1
  • 2
  • 3
  • 4

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

❶ 输入要翻译的英语句子

❷ 将输入编码为标记 ID

❸ 生成输出的标记 ID

❹ 将输出的标记 ID 解码为实际输出标记

❺ 显示翻译

运行代码后得到的输出结果如下所示:

Translation:
['<pad>', '▁Me', '▁din', '▁de', '▁Paul']
  • 1
  • 2

我们立即注意到的第一件事是输出中存在一个我们以前没有见过的特殊标记 <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 供您参考,突出显示此掩码层。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7.2(重复,突出显示掩码层)将编码器和解码器简化为自注意力、编码器-解码器注意力和前馈神经网络的分解形式

在我们在图 7.3 中经历的注意力计算中,这只意味着在计算中只包括“他不想谈论细胞”中的标记,并忽略其余的标记。我们稍后复制图 7.3,稍作修改,以便您清楚地看到未来标记被掩盖的情况。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7.3(再次重复,为掩码自注意力修改)我们示例句子的掩码自注意力可视化,显示了因果关系中未来标记的被掩盖情况。

这为系统引入了因果关系的感觉,并适用于文本生成,或预测下一个标记。由于没有编码器,编码器-解码器注意力也被删除了。考虑到这些因素,我们在图 7.7 中展示了 GPT 的架构。

请注意图 7.7 中,同样的输出可以用于一些其他任务的文本预测/生成和分类。事实上,作者设计了一个输入转换方案,使得多个任务可以通过相同的架构处理,而不需要任何架构更改。例如,考虑到 文本蕴涵 任务,它大致对应于确定一个 前提 陈述是否暗示另一个 假设 陈述。输入转换会将前提和假设陈述连接起来,用一个特殊的分隔符标记分隔,然后将结果的单一连续字符串馈送到相同的未修改架构,以分类是否存在蕴涵。另一方面,考虑到重要的问答应用。在这里,给定一些上下文文档、一个问题和一组可能的答案,任务是确定哪个答案是问题的最佳潜在答案。在这里,输入转换是将上下文、问题和每个可能的答案连接在一起,然后通过相同的模型将每个结果的连续字符串传递,并对相应的输出执行 softmax,以确定最佳答案。类似的输入转换也适用于句子相似性任务。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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')
  • 1
  • 2

提醒一下,GPT 最初的形式非常适合于开放式文本生成,比如创造性地写出一些文本段落来补充之前的文本。让我们看看当模型以“在彩虹的那边……”为提示,生成最多 100 个标记时,模型生成了什么,通过以下命令:

gpt("Somewhere over the rainbow", max_length=100)
  • 1

这生成了以下文本:

[{'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"}]
  • 1

即使消息有些不连贯,这似乎在语义上非常正确。您可以想象一位创意作家使用它来生成想法以克服写作困境!现在,让我们看看是否可以用一些不太“创意”的东西来启动模型,一些更技术性的东西,以查看它的表现。让我们通过以下代码将模型启动文本设置为“迁移学习是一门研究领域”:

gpt("Transfer learning is a field of study", max_length=100)
  • 1

这产生了以下输出:

[{'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"}]
  • 1

再次,我们可以看到,从语义连贯性、语法结构、拼写、标点等方面来看,这段文字非常好——实际上,甚至有点诡异。然而,随着它的继续,它变得可能事实上不正确。我们都可以同意,要真正理解迁移学习,需要对数学有扎实的基础,甚至可以说它已经存在了几个世纪——通过我们,人类!然而,它不是物理学的一个领域,即使在需要掌握它的技能方面可能有些类似。我们可以看到,模型的输出允许它说话的时间越长,就越不可信。

请务必进行更多实验,以了解模型的优缺点。例如,您可以尝试使用我们的示例句子提示模型,“他不想在手机上谈论细胞,因为他认为这很无聊。”我们发现这在创意写作空间和技术写作空间中都是一个可信的应用,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")
  • 1
  • 2
  • 3
  • 4
  • 5

❶ 请注意,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 = 5for 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)))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

❶ 五行聊天

❷ 编码新用户输入,添加一个句子结束标记,并返回张量

❸ 将新输入添加到聊天历史记录中

❹ 使用聊天历史记录作为上下文生成最多 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.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

接下来这短暂对话的另一端很可能是一个人,不是吗?那是否意味着它通过了图灵测试?并非如此,下面的交流说明了这一点:

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.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

当你增加允许的对话轮次数量时,你会发现机器人会陷入重复的与话题无关的回复中。这类似于 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 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8.1 BERT 架构的高级表示,显示堆叠的编码器、输入嵌入和位置编码。顶部的输出在训练期间用于下一句预测和填空遮蔽语言建模目标。

正如我们在介绍中讨论的,并且如图所示,在训练期间,我们使用下一句预测(NSP)和遮蔽语言建模(MSM)目标。BERT 最初以两种风味呈现,BASE 和 LARGE。如图 8.1 所示,BASE 堆叠了 12 个编码器,而 LARGE 堆叠了 24 个编码器。与之前一样——在 GPT 和原始 Transformer 中——通过输入嵌入将输入转换为向量,并向它们添加位置编码,以给出输入序列中每个标记的位置感。为了考虑下一句预测任务,其中输入是句子 A 和 B 的一对,添加了额外的段编码步骤。段嵌入指示给定标记属于哪个句子,并添加到输入和位置编码中,以产生输入到编码器堆栈的输出。我们的示例句对的整个输入转换在图 8.2 中可视化:“他不想在手机上谈论细胞。他认为这个主题非常无聊。”

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

❶ 用于构建模型的函数

❷ 我们不重新训练任何 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.'
  • 1
  • 2
  • 3
  • 4
  • 5

❶ 这些模型通常会被默认加载,但我们明确指出以保持透明度。使用已在 SQuAD 上进行了微调的模型非常重要;否则,结果将很差。

在初始化了流水线之后,让我们首先看看是否能够通过询问文章的主题来自动提取文章的精髓。我们用以下代码来实现:

ans = qNa({'question': 'What is this article about?','context': f'{paragraph}'})
print(ans)
  • 1
  • 2

这产生了以下输出,我们可能会认为这是一个合理的回答:

{'score': 0.47023460869354494, 'start': 148, 'end': 168, 'answer': 'Containment policies'}
  • 1

注意,0.47 相对较低的分数表明答案缺少一些上下文。类似“遏制政策对 COVID-19 的影响”可能是更好的回答,但因为我们正在进行提取式问答,而这个句子不在上下文段落中,所以这是模型能做到的最好的。低分数可以帮助标记此回答进行人工双重检查和/或改进。

为什么不问一些更多的问题?让我们看看模型是否知道文章中描述的是哪个国家,使用以下代码:

ans = qNa({'question': 'Which country is this article about?',
           'context': f'{paragraph}'})
print(ans)
  • 1
  • 2
  • 3

这产生了以下输出,正如以前的分数约为 0.8 所示,完全正确:

{'score': 0.795254447990601, 'start': 34, 'end': 36, 'answer': 'US'}
  • 1

讨论的是哪种疾病?

ans = qNa({'question': 'Which disease is discussed in this article?',
           'context': f'{paragraph}'})
print(ans)
  • 1
  • 2
  • 3

输出完全正确,信心甚至比之前更高,达到了 0.98,如下所示:

{'score': 0.9761025334558902, 'start': 205, 'end': 213, 'answer': 'COVID-19'}
  • 1

那时间段呢?

ans = qNa({'question': 'What time period is discussed in the article?',
           'context': f'{paragraph}'})
print(ans)
  • 1
  • 2
  • 3

与输出相关联的 0.22 的低分数表明结果质量差,因为文章中讨论了 4 月至 6 月的时间范围,但从未在连续的文本块中讨论,可以为高质量答案提取,如下所示:

{'score': 0.21781831588181433, 'start': 71, 'end': 79, 'answer': '1 April,'}
  • 1

然而,仅选择一个范围的端点能力已经是一个有用的结果。这里的低分数可以提醒人工检查此结果。在自动化系统中,目标是这样的较低质量答案成为少数,总体上需要很少的人工干预。

在介绍了问答之后,在下一小节中,我们将解决 BERT 训练任务的填空和下一句预测。

8.1.3 应用于填空和下一句预测任务

我们在这一节的练习中使用了上一小节的文章。让我们立即开始编写一个用于填写空白的流程,使用以下代码:

from transformers import pipeline

fill_mask = pipeline("fill-mask",model="bert-base-cased",tokenizer="bert-base-cased")
  • 1
  • 2
  • 3

注意,在这里我们使用的是 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")
  • 1

输出显示,最高的是“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}] 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

我们鼓励你尝试从各种句子中省略各种单词,以确信这几乎总是非常有效的。在节省篇幅的情况下,我们的附带笔记本会为几个更多的句子做到这一点,但我们不在这里打印这些结果。

然后我们继续进行下一个句子预测(NSP)任务。在写作本文时,此任务尚未包含在 pipelines API 中。因此,我们将直接使用 transformers API,这也将让您更加熟悉它。我们首先需要确保已安装 transformers 3.0.0 以上的版本,因为该任务仅在该阶段的库中包含。我们使用以下代码实现这一点;在写作本文时,Kaggle 默认安装了较早的版本:

!pip install transformers==3.0.1 # upgrade transformers for NSP
  • 1

升级版本后,我们可以使用以下代码加载一个 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()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

❶ 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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

❶ 输出是一个元组;第一项描述了我们追求的两个句子之间的关系。

❷ 从原始数字计算概率

注意代码中的术语logits。这是 softmax 函数的原始输入。通过 softmax 将logits传递,可以得到概率。代码的输出确认找到了正确的关系,如下所示:

Probabilities: [not plausible, plausible]
tensor([[0.1725, 0.8275]], grad_fn=<SoftmaxBackward>)
  • 1
  • 2

现在,让我们将第二个句子替换为一个有点随机的“Cats are independent.” 这将产生以下结果:

Probabilities: [not plausible, plausible]
tensor([0.7666, 0.2334], grad_fn=<SoftmaxBackward>)
  • 1
  • 2

看起来一切都如预期的那样工作!

现在,你应该已经非常清楚 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")
  • 1
  • 2

❶ 这只是 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())
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

❶ 使用了遮蔽语言建模

❷ 初始化到了 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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

❶ 指示一次读取多少行

如下代码所示,接下来我们需要定义一个“data collator” —— 一个帮助方法,通过一批样本数据行(长度为block_size)创建一个特殊对象。 这个特殊对象适用于 PyTorch 进行神经网络训练:

from transformers import DataCollatorForLanguageModeling

data_collator = DataCollatorForLanguageModeling(
    tokenizer=tokenizer,
    mlm=True, mlm_probability=0.15)
  • 1
  • 2
  • 3
  • 4
  • 5

❶ 使用了遮蔽语言建模,并以 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,
)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

然后使用先前定义的数据集和数据收集器定义一个“训练器”来进行数据上的一个训练周期。注意,数据包含了超过 600,000 行,因此一次遍历所有数据是相当大量的训练!

trainer = Trainer(
    model=model,
    args=training_args,
    data_collator=data_collator,
    train_dataset=dataset,
    prediction_loss_only=True)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

训练并计算训练时间,如下所示:

import time
start = time.time()
trainer.train()
end = time.time()
print("Number of seconds for training:")
print((end-start))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

模型在所示的超参数下大约需要三个小时才能完成一个周期,并且损失大约为 0.77。

按如下进行模型保存:

trainer.save_model("twimbert")
  • 1

最后,我们从语料库中取出以下句子 —— “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ɔ."))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

❶ 定义了填空管道

❷ 预测被遮蔽的标记

这将产生如下输出:

[{'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}]
  • 1

你立刻就能看到结果中的宗教偏见。“以色列”和“伊甸园”被提议为前五个完成之一。话虽如此,它们算是比较有说服力的完成 —— 因为它们都是名词。总的来说,表现可能还算不错。

如果你不会说这种语言,不用担心。在下一节中,我们将从头开始训练 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") 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

❶ 初始化分词器

❷ 自定义训练,并进行训练

❸ 标准 BERT 特殊标记

❹ 将分词器保存到磁盘

要从刚刚保存的分词器中加载分词器,我们只需要执行以下操作:

from transformers import BertTokenizerFast
tokenizer = BertTokenizerFast.from_pretrained("twibert", max_len=512)
  • 1
  • 2

❶ 使用我们刚刚训练的语言特定的分词器,max_len=512,以保持与上一小节一致

请注意,我们使用最大序列长度为 512,以保持与上一小节一致——这也是预训练的 mBERT 使用的长度。还要注意,保存分词器将在指定文件夹中创建词汇文件 vocab.txt 文件。

从这里开始,我们只需初始化一个全新的 BERT 模型来进行掩码语言建模,如下所示:

from transformers import BertForMaskedLM, BertConfig
model = BertForMaskedLM(BertConfig())
  • 1
  • 2

❶ 不要初始化为预训练的;创建一个全新的。

否则,步骤与上一小节相同,我们不在此处重复代码。重复相同的步骤在一个时代后大约 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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小惠珠哦/article/detail/987553
推荐阅读
相关标签
  

闽ICP备14008679号