原文:
zh.annas-archive.org/md5/da86c0033427bb250532db6d61561179
译者:飞龙
第五章:处理数据以用于循环神经网络深度迁移学习实验
本章涵盖
-
循环神经网络(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 具有多个输出标签,即categorical
和int
。这还有助于处理输入列不是“干净”的情况,即它们包含多种类型。这些列可以带有所有存在的类型标签,并传递给相关解析器进行进一步清洁。
现在,我们对于这个问题有了更好的理解,让我们开始获取一些表格数据,用于本节的实验。
5.1.1 获取和可视化表格数据
我们将使用两个简单的数据集来说明下一章中的表格列类型分类示例。这两个数据集中的第一个是由 OpenML 提供的棒球球员统计数据集。⁶该数据集描述了一组球员的棒球统计数据,以及他们是否最终进入名人堂。
在 Linux 系统上,我们可以按如下方式获取数据集:
!wget https:/ /www.openml.org/data/get_csv/3622/dataset_189_baseball.arff
从以前的章节中可以回忆到,“!”符号仅在执行 Jupyter 环境(例如我们建议在这些练习中使用的 Kaggle 环境)时需要,当在终端中执行时,应该将其删除。同时请注意,对于我们的目的,.arff
格式与.csv
格式在功能上是等效的。
获取了感兴趣的数据集后,让我们像往常一样使用 Pandas 进行预览:
import pandas as pd
raw_baseball_data = pd.read_csv('dataset_189_baseball.arff', dtype=str) ❶
print(raw_baseball_data.head())
❶对于我们的目的,.arff
格式与.csv
格式在功能上是等效的。
这将显示 DataFrame 的前五行,如下所示:
Player Number_seasons Games_played At_bats Runs Hits Doubles \
0 HANK_AARON 23 3298 12364 2174 3771 624
1 JERRY_ADAIR 13 1165 4019 378 1022 163
2 SPARKY_ADAMS 13 1424 5557 844 1588 249
3 BOBBY_ADAMS 14 1281 4019 591 1082 188
4 JOE_ADCOCK 17 1959 6606 823 1832 295
Triples Home_runs RBIs Walks Strikeouts Batting_average On_base_pct \
0 98 755 2297 1402 1383 0.305 0.377
1 19 57 366 208 499 0.254 0.294
2 48 9 394 453 223 0.286 0.343
3 49 37 303 414 447 0.269 0.34
4 35 336 1122 594 1059 0.277 0.339
Slugging_pct Fielding_ave Position Hall_of_Fame
0 0.555 0.98 Outfield 1
1 0.347 0.985 Second_base 0
2 0.353 0.974 Second_base 0
3 0.368 0.955 Third_base 0
4 0.485 0.994 First_base 0
我们可以看到这是一组广告中所述的突击手棒球统计数据集。
现在我们获取另一个表格数据集。不多赘述,这个数据集将用于扩展我们的 SIMOn 分类器,超越预训练模型所设计的类别集合。这个练习将为转移学习提供一个有趣的使用案例,可以激发你自己应用的创意。
我们将要查看的第二个数据集是多年的不列颠哥伦比亚省公共图书馆统计数据集,我们从 BC 数据目录⁷获得,但也将其附加到我们的伴随 Kaggle 笔记本上,以方便你使用。要使用 Pandas 加载数据集,我们执行以下命令,其中我们 Kaggle 环境中该文件的位置应该替换为您本地的路径,如果选择在本地工作:
raw_data = pd.read_csv('../input/20022018-bc-public-libraries-open-data-v182/2002-2018-bc-public-libraries-open-data-csv-v18.2.csv', dtype=str)
我们可以使用以下命令查看数据集:
print(raw_data.head())
输出结果为:
YEAR LOCATION LIB_NAME \
0 2018 Alert Bay Public Library & Museum Alert Bay Public Library
1 2018 Beaver Valley Public Library Beaver Valley Public Library
2 2018 Bowen Island Public Library Bowen Island Public Library
3 2018 Burnaby Public Library Burnaby Public Library
4 2018 Burns Lake Public Library Burns Lake Public Library
LIB_TYPE SYMBOL Federation lib_ils \
0 Public Library Association BABM Island Link LF Evergreen Sitka
1 Public Library Association BFBV Kootenay LF Evergreen Sitka
2 Municipal Library BBI InterLINK LF Evergreen Sitka
3 Municipal Library BB InterLINK LF SirsiDynix Horizon
4 Public Library Association BBUL North Central LF Evergreen Sitka
POP_SERVED srv_pln STRAT_YR_START ... OTH_EXP TOT_EXP EXP_PER_CAPITA \
0 954 Yes 2,013 ... 2488 24439 25.6174
1 4,807 Yes 2,014 ... 15232 231314.13 48.12027
2 3,680 Yes 2,018 ... 20709 315311.17 85.68238
3 232,755 Yes 2,019 ... 237939 13794902 59.26791
4 5,763 Yes 2,018 ... NaN 292315 50.72271
TRANSFERS_TO_RESERVE AMORTIZATION EXP_ELEC_EBOOK EXP_ELEC_DB \
0 0 0 0 718
1 11026 0 1409.23 826.82
2 11176 40932 2111 54.17
3 0 2614627 132050 0
4 NaN NaN 0 0
EXP_ELEC_ELEARN EXP_ELEC_STREAM EXP_ELEC_OTHER
0 0 0 752
1 1176.11 0 1310.97
2 3241 0 0
3 0 0 180376
4 0 0 7040
[5 rows x 235 columns]
我们只对百分比和整数这一对列感兴趣,我们可以按以下方式提取并显示:
COLUMNS = ["PCT_ELEC_IN_TOT_VOLS","TOT_AV_VOLS"] ❶
raw_library_data = raw_data[COLUMNS]
print(raw_library_data)
❶这个数据集有很多列,我们只关注这两列。
这将产生以下输出,展示我们将使用的另外两列:
PCT_ELEC_IN_TOT_VOLS TOT_AV_VOLS
0 90.42% 57
1 74.83% 2,778
2 85.55% 1,590
3 9.22% 83,906
4 66.63% 4,261
... ... ...
1202 0.00% 35,215
1203 0.00% 109,499
1204 0.00% 209
1205 0.00% 18,748
1206 0.00% 2403
[1207 rows x 2 columns]
5.1.2 预处理表格数据
现在让我们将获取的表格数据预处理成 SIMOn 框架可以接受的形式。由于我们将使用一个预训练模型,该模型预先包含一个编码器,我们将应用于此目的,因此我们需要首先安装 SIMOn,使用以下命令:
!pip install git+https:/ /github.com/algorine/simon
完成这些之后,我们还需要导入一些必需的模块,如下所示:
from Simon import Simon ❶
from Simon.Encoder import Encoder ❷
❶ 导入 SIMOn 模型类
❷ 导入 SIMOn 数据编码器类,用于将输入文本转换为数字
这些导入分别代表了 SIMOn 模型类、数据编码器类、将所有输入数据标准化为固定长度的实用程序,以及生成模拟数据的类。
接下来,我们获取一个预训练的 SIMOn 模型,它带有自己的编码器,用于将文本转换为数字。该模型由两个文件组成:一个包含编码器和其他配置,另一个包含模型权重。我们使用以下命令获取这些文件:
!wget https:/ /raw.githubusercontent.com/algorine/simon/master/Simon/scripts/❶pretrained_models/Base.pkl ❶
!wget https:/ /raw.githubusercontent.com/algorine/simon/master/Simon/scripts/❷pretrained_models/text-class.17-0.04.hdf5 ❷
❶ 预训练的 SIMOn 模型配置、编码器等
❷ 对应的模型权重
在我们加载模型权重之前,首先需要加载它们的配置,这些配置包括编码器,通过以下一系列命令:
checkpoint_dir = "" ❶
execution_config = "Base.pkl" ❷
Classifier = Simon(encoder={}) ❸
config = Classifier.load_config(execution_config, checkpoint_dir) ❹
encoder = config['encoder'] ❺
checkpoint = config['checkpoint'] ❻
❶ 模型权重位于当前级别。
❷ 下载的预训练模型配置的名称
❸ 创建一个文本分类器实例,用于从模型配置中加载编码器。
❹ 加载模型配置
❺ 提取编码器
❻ 提取检查点名称
为了确保我们下载了正确的权重集,通过以下方式双重检查模型所需的权重文件:
print(checkpoint)
通过打印以下内容,应确认我们获取了正确的文件:
text-class.17-0.04.hdf5
最后,我们需要为建模表格数据指定两个关键参数。参数max_cells
指定表格每列的最大单元格数。参数max_len
指定每个单元格的最大长度。这在图 5.4 中有所体现。
图 5.4 可视化表格数据建模参数。参数max_cells
指定表格中每列的最大单元格或行数。参数max_len
指定每个单元格或行的最大长度。
每列的最大单元格数必须与训练中使用的 500 的值匹配,并且可以从编码器中提取,如下所示:
max_cells = encoder.cur_max_cells
另外,我们将max_len
设置为 20,以与预训练模型设置保持一致,并提取预训练模型支持的类别,如下所示:
max_len = 20 # maximum length of each tabular cell
Categories = encoder.categories
category_count = len(Categories) ❶
print(encoder.categories)
❶ 预训练模型支持的类别数量
我们发现处理的类别如下:
['address', 'boolean', 'datetime', 'email', 'float', 'int', 'phone', 'text', 'uri']
5.1.3 将预处理数据编码为数字
现在我们将使用编码器将表格数据转换为 SIMOn 模型可以用来进行预测的一组数字。这涉及将每个输入字符串中的每个字符转换为该字符在模型编码方案中表示的唯一整数。
因为卷积神经网络(CNNs)需要所有输入都是固定的、预先指定的长度,所以编码器还将标准化每个输入列的长度。这一步骤会复制短于max_cells
的列中的随机单元,并丢弃一些长列中的随机单元。这确保了所有列的长度恰好为max_cells
。此外,如果需要,所有单元都标准化为长度max_len
,并添加填充。我们不会过于担心这些细节,因为 SIMOn API 会在幕后为我们处理它。
我们对棒球数据集进行编码,并使用以下代码显示其形状:
X_baseball = encoder.encodeDataFrame(raw_baseball_data) ❶
print(X_baseball.shape) ❷
print(X_baseball[0]) ❸
❶ 编码数据(标准化、转置、转换为 NumPy 数组)
❷ 显示了编码数据的形状
❸ 显示了编码的第一列
执行此操作会产生以下输出,其中首先显示输出形状元组,然后显示编码的第一列:
(18, 500, 20)
[[-1 -1 -1 ... 50 37 44]
[-1 -1 -1 ... 54 41 46]
[-1 -1 -1 ... 37 52 55]
...
[-1 -1 -1 ... 49 45 46]
[-1 -1 -1 ... 51 54 43]
[-1 -1 -1 ... 38 37 43]]
我们看到每个编码列都是一个max_cells=500
乘以max_len=20
的数组,正如预期的那样。我们还注意到编码列的-1 条目代表了短于max_len
的单元的填充。
我们还对图书馆数据进行编码,以便以后使用:
X_library = encoder.encodeDataFrame(raw_library_data)
在这个阶段,我们已经将示例输入数据集转换成了适当形状的 NumPy 数组。这将文本编码为适合 SIMOn 神经网络第一阶段——生成初步输入句子嵌入的 CNN 的摄入和分析的数字。
5.2 预处理事实检验示例数据
在这一节中,我们介绍了将在本章和后续章节中研究的第二个示例数据集。在这里,我们感兴趣的是开发一种算法,用于区分事实新闻和潜在的错误信息或虚假信息。这个应用领域变得越来越重要,并经常被称为“自动假新闻检测”。
对我们来说很方便的是,Kaggle⁸上有一个适用的数据集。该数据集包含超过 40,000 篇文章,分为两类:“假”和“真”。真实的文章来自一家名声显赫的新闻网站 reuters.com。而假新闻则来自 PolitiFact 标记为不可靠的各种来源。这些文章大多涉及政治和世界新闻。
5.2.1 特殊问题考虑
可以称为假的主题无疑是一个值得讨论的敏感话题。可以肯定的是,准备训练数据标签的人的偏见可能会转移到分类系统中。在这样敏感的语境下,标签的有效性需要特别注意和考虑如何创建。
此外,尽管我们在本节的目的是开发一个基于内容的分类系统,用于区分真实文章与潜在虚假文章,但重要的是要强调,现实场景要复杂得多。换句话说,检测潜在错误信息传播只是检测影响行动问题的一个方面。要理解两者之间的区别,请考虑即使真实信息也可以用来影响意见,从而损害品牌,如果将其放在错误的上下文或不自然地放大。
检测影响行动可以自然地被构造为一个异常检测问题,⁹ 但这样的系统只有作为缓解策略的一部分时才能有效。它必须是跨平台的,尽可能监控和分析尽可能多的潜在信息渠道中的异常情况。此外,今天的大多数实用系统都嵌入了人类,即检测系统只标记聚合的可疑活动,并将最终行动呼叫留给人类分析员。
5.2.2 加载和可视化事实检查数据
现在,我们直接跳转到加载事实检查数据并使用 ELMo 建模框架对其进行分类的步骤。回想一下第 3.2.1 节,我们在那里将 ELMo 应用于垃圾邮件检测和电影评论情感分析,该模型期望将每个输入文档作为单个字符串。这使得事情变得更容易——不需要分词。还要注意,数据集已经附加到了 Kaggle 上的伴随 Jupyter 笔记本上。
我们使用列表 5.1 中的代码从数据集中加载真假数据。请注意,我们选择在此加载每种 1,000 个样本,以保持与第 3.2.1 节的一致性。
列表 5.1 加载每种 1,000 个真假文章样本
import numpy as np
import pandas as pd
DataTrue = pd.read_csv("/kaggle/input/fake-and-real-news-dataset/True.csv")❶
DataFake = pd.read_csv("/kaggle/input/fake-and-real-news-dataset/Fake.csv")❷
Nsamp =1000 ❸
DataTrue = DataTrue.sample(Nsamp)
DataFake = DataFake.sample(Nsamp)
raw_data = pd.concat([DataTrue,DataFake], axis=0).values ❹
raw_data = [sample[0].lower() + sample[1].lower() + sample[3].lower() for sample in raw_data] ❺
Categories = ['True','False'] ❻
header = ([1]*Nsamp)
header.extend(([0]*Nsamp))
❶ 将真实新闻数据读入 Pandas DataFrame
❷ 将假新闻数据读入 Pandas DataFrame
❸ 每个类别生成的样本数——真实,虚假
❹ 连接的真假样本
❺ 将标题、正文和主题组合成每个文档的一个字符串
❻ 对应的标签
其次,我们使用以下代码将数据洗牌并将其分为 70% 的训练/30% 的验证,以方便起见,这些代码在此处从第 3.2.1 节复制:
def unison_shuffle(a, b): ❶
p = np.random.permutation(len(b))
data = np.asarray(a)[p]
header = np.asarray(b)[p]
return data, header
raw_data, header = unison_shuffle(raw_data, header) ❷
idx = int(0.7*raw_data.shape[0]) ❸
train_x = raw_data[:idx] ❹
train_y = header[:idx]
test_x = raw_data[idx:] ❺
test_y = header[idx:]
❶ 一个用于与标签头一起洗牌数据的函数,以消除任何潜在的顺序偏差
❷ 通过调用先前定义的函数来洗牌数据
❸ 分成独立的 70% 训练和 30% 测试集
❹ 70% 的数据用于训练
❺ 剩余 30% 用于验证
在介绍和预处理示例问题数据之后,我们将在下一章中将章节开头概述的三个基于 RNN 的神经网络模型应用于示例问题数据。
总结
-
与单词级模型相比,字符级模型可以处理拼写错误和其他社交媒体特征,如表情符号和小众俚语。
-
双向语言建模是构建意识到其局部上下文的词嵌入的关键。
-
SIMOn 和 ELMo 都采用字符级 CNN 和双向 LSTM,后者有助于实现双向上下文建模。
-
P. Azunre 等人,“基于字符级卷积神经网络的表格数据集的语义分类”,arXiv(2019 年)。
-
M. E. Peters 等人,“Deep Contextualized Word Representations”,NAACL-HLT 会议论文集(2018 年)。
-
J. Howard 等人,“文本分类的通用语言模型微调”,第 56 届计算语言学年会论文集(2018 年)。
-
N. Dhamani 等人,“利用深度网络和迁移学习解决虚假信息问题”,AI for Social Good ICML 研讨会(2019 年)。
-
catalogue.data.gov.bc.ca/dataset/bc-public-libraries-statistics-2002-present
-
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)
❶ 模拟/伪造数据生成实用工具(使用库 Faker)
❷ 生成的列数,为了简单起见任意选择
❸ 每列的单元格/行数,为了简单说明而任意选择
❹ 不要重用数据,而是为数据集中的变化性生成新鲜数据。
❺ 打印结果
执行此代码会产生以下输出,显示各种数据类型的生成样本及其相应的标签:
SIMULATED DATA:
[['byoung@hotmail.com' 'Jesse' 'True' 'PPC' 'Lauraview']
['cindygilbert@gmail.com' 'Jason' 'True' 'Intel' 'West Brandonburgh']
['wilsonalexis@yahoo.com' 'Matthew' 'True' 'U; Intel'
'South Christopherside']
['cbrown@yahoo.com' 'Andrew' 'False' 'U; PPC' 'Loganside']
['christopher90@gmail.com' 'Devon' 'True' 'PPC' 'East Charlesview']
['deanna75@gmail.com' 'Eric' 'False' 'U; PPC' 'West Janethaven']
['james80@hotmail.com' 'Ryan' 'True' 'U; Intel' 'Loriborough']
['cookjennifer@yahoo.com' 'Richard' 'True' 'U; Intel' 'Robertsonchester']
['jonestyler@gmail.com' 'John' 'True' 'PPC' 'New Kevinfort']
['johnsonmichael@gmail.com' 'Justin' 'True' 'U; Intel' 'Victormouth']]
SIMULATED DATA HEADER:
[list(['email', 'text']) list(['text']) list(['boolean', 'text'])
list(['text']) list(['text'])]
SIMOn 仓库的顶层包含了 types.json 文件,该文件指定了从 Faker 库类到先前显示的类别的映射。例如,前一个示例中名称的第二列被标记为“文本”,因为我们不需要为我们的目的识别名称。您可以快速更改此映射,并为您自己的项目和类别集生成模拟数据。
我们这里不使用模拟数据进行训练,因为该过程可能需要几个小时,而我们已经可以访问捕捉到这些知识的预训练模型。但是,我们会进行一项说明性的迁移学习实验,涉及扩展支持的类别集合,超出了预训练模型中可用的类别。
回想一下,在第 5.1.2 节中加载了 SIMOn 分类器类以及模型配置,包括编码器。然后我们可以生成一个 Keras SIMOn 模型,将下载的权重加载到其中,并使用以下命令序列进行编译:
model = Classifier.generate_model(max_len, max_cells, category_count) ❶
Classifier.load_weights(checkpoint, None, model, checkpoint_dir) ❷
model.compile(loss='binary_crossentropy',optimizer='adam', metrics=['accuracy']) ❸
❶ 生成模型
❷ 加载权重
❸ 编译模型,使用二元交叉熵损失进行多标签分类
在继续之前,查看模型架构是个好主意,我们可以使用以下命令来做到这一点:
model.summary()
这将显示以下输出,并允许您更好地了解内部发生的情况:
______________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
======================================================================================
input_1 (InputLayer) (None, 500, 20) 0
______________________________________________________________________________________
time_distributed_1 (TimeDistrib (None, 500, 512) 3202416 input_1[0][0]
______________________________________________________________________________________
lstm_3 (LSTM) (None, 128) 328192 time_distributed_1[0][0]
______________________________________________________________________________________
lstm_4 (LSTM) (None, 128) 328192 time_distributed_1[0][0]
______________________________________________________________________________________
concatenate_2 (Concatenate) (None, 256) 0 lstm_3[0][0]
lstm_4[0][0]
______________________________________________________________________________________
dropout_5 (Dropout) (None, 256) 0 concatenate_2[0][0]
______________________________________________________________________________________
dense_1 (Dense) (None, 128) 32896 dropout_5[0][0]
______________________________________________________________________________________
dropout_6 (Dropout) (None, 128) 0 dense_1[0][0]
______________________________________________________________________________________
dense_2 (Dense) (None, 9) 1161 dropout_6[0][0]
time_distributed_1
层是应用于每个输入句子的句子编码器。我们看到其后是前向和后向的 LSTM,它们被连接在一起,一些通过 dropout 进行的正则化,以及来自 dense_2
层的输出概率。回想一下,预训练模型处理的类别数恰好为 9,这与输出 dense_2
层的维度匹配。还要注意的是,巧合的是,模型总共有 9 层。
通过执行以下一系列命令,我们已经对编译模型的架构有了一定的了解,现在让我们继续查看它认为棒球数据集列的类型是什么。我们通过执行以下命令序列来实现这一点:
p_threshold = 0.5 ❶
y = model.predict(X_baseball) ❷
result = encoder.reverse_label_encode(y,p_threshold) ❸
print("Recall that the column headers were:") ❹
print(list(raw_baseball_data))
print("The predicted classes and probabilities are respectively:")
print(result)
❶ 用于决定类成员身份的概率阈值
❷ 预测棒球数据集列的类别
❸ 将概率转换为类别标签
❹ 显示输出
对应的代码输出如下所示:
Recall that the column headers were:
['Player', 'Number_seasons', 'Games_played', 'At_bats', 'Runs', 'Hits', 'Doubles', 'Triples', 'Home_runs', 'RBIs', 'Walks', 'Strikeouts', 'Batting_average', 'On_base_pct', 'Slugging_pct', 'Fielding_ave', 'Position', 'Hall_of_Fame']
The predicted classes and probabilities are respectively:
([('text',), ('int',), ('int',), ('int',), ('int',), ('int',), ('int',), ('int',), ('int',), ('int',), ('int',), ('int',), ('float',), ('float',), ('float',), ('float',), ('text',), ('int',)], [[0.9970826506614685], [0.9877430200576782], [0.9899477362632751], [0.9903284907341003], [0.9894667267799377], [0.9854978322982788], [0.9892633557319641], [0.9895514845848083], [0.989467203617096], [0.9895854592323303], [0.9896339178085327], [0.9897230863571167], [0.9998295307159424], [0.9998230338096619], [0.9998272061347961], [0.9998039603233337], [0.9975670576095581], [0.9894945025444031]])
回顾第 5.1.1 节以显示此数据的切片,我们在此复制,我们看到模型以高置信度完全正确地获取了每一列:
Player Number_seasons Games_played At_bats Runs Hits Doubles \
0 HANK_AARON 23 3298 12364 2174 3771 624
1 JERRY_ADAIR 13 1165 4019 378 1022 163
2 SPARKY_ADAMS 13 1424 5557 844 1588 249
3 BOBBY_ADAMS 14 1281 4019 591 1082 188
4 JOE_ADCOCK 17 1959 6606 823 1832 295
Triples Home_runs RBIs Walks Strikeouts Batting_average On_base_pct \
0 98 755 2297 1402 1383 0.305 0.377
1 19 57 366 208 499 0.254 0.294
2 48 9 394 453 223 0.286 0.343
3 49 37 303 414 447 0.269 0.34
4 35 336 1122 594 1059 0.277 0.339
Slugging_pct Fielding_ave Position Hall_of_Fame
0 0.555 0.98 Outfield 1
1 0.347 0.985 Second_base 0
2 0.353 0.974 Second_base 0
3 0.368 0.955 Third_base 0
4 0.485 0.994 First_base 0
现在,假设我们有兴趣在项目中检测具有百分比值的列。我们如何快速使用预训练模型来实现这一点呢?我们可以使用上一章中准备的第二个表格数据集来调查这种情况——多年来的不列颠哥伦比亚公共图书馆统计数据集。当然,第一步是直接使用预训练模型预测这些数据。以下一系列命令实现了这一点:
X = encoder.encodeDataFrame(raw_library_data) ❶
y = model.predict(X) ❷
result = encoder.reverse_label_encode(y,p_threshold) ❸
print("Recall that the column headers were:")
print(list(raw_library_data))
print("The predicted class/probability:")
print(result)
❶ 使用原始框架对数据进行编码
❷ 预测类别
❸ 将概率转换为类标签
这将产生以下输出:
Recall that the column headers were:
['PCT_ELEC_IN_TOT_VOLS', 'TOT_AV_VOLS']
The predicted class/probability:
([('text',), ('int',)], [[0.7253058552742004], [0.7712462544441223]])
回顾 5.1.1 节的一个数据切片,我们看到整数列被正确识别,而百分比列被识别为文本:
PCT_ELEC_IN_TOT_VOLS TOT_AV_VOLS
0 90.42% 57
1 74.83% 2,778
2 85.55% 1,590
3 9.22% 83,906
4 66.63% 4,261
... ... ...
1202 0.00% 35,215
1203 0.00% 109,499
1204 0.00% 209
1205 0.00% 18,748
1206 0.00% 2403
[1207 rows x 2 columns]
那并不是不正确,但也不完全是我们正在寻找的,因为它不够具体。
我们将快速将预训练模型转移到一个非常小的包含百分比样本的训练数据集。首先让我们使用以下命令了解原始库 DataFrame 的大小:
print(raw_library_data.shape)
我们发现尺寸为(1207,2),这似乎是足够构建一个小数据集的行数!
在清单 6.1 中,我们展示了可用于将此数据集分割为许多每个 20 个单元格的更小列的脚本。数字 20 是任意选择的,是为了创建足够多的唯一列——大约 50 个——在生成的数据集中。此过程产生一个新的 DataFrame,new_raw_data
,大小为 20 行 120 列——前 60 列对应于百分比值,后 60 列对应于整数值。它还生成一个相应的header
标签列表。
清单 6.1 将长库数据转换为许多较短样本列
❶
percent_value_list = raw_library_data['PCT_ELEC_IN_TOT_VOLS'].values.tolist()
int_value_list = raw_library_data['TOT_AV_VOLS'].values.tolist()
❷
original_length = raw_data.shape[0] ❸
chunk_size = 20 # length of each newly generated column
header_list = list(range(2*original_length/ /chunk_size)) ❹
new_raw_data = pd.DataFrame(columns = header_list) ❺
for i in range(original_length/ /chunk_size): ❻
new_raw_data[i] = percent_value_list[i:i+chunk_size] ❼
new_raw_data[original_length/ /chunk_size+i] = int_value_list[i:i+chunk_size] ❽
header = [("percent",),]*(original_length/ /chunk_size) ❾
header.extend([("int",),]*(original_length/ /chunk_size))
❶ 将数据转换为两个列表
❷ 将其分解为每个样本列 20 个单元格
❸ 原始长度,1207
❹ 新列的索引列表
❺ 初始化新的 DataFrame 以保存新数据
❻ 使用新的 DataFrame 填充
❼ 使用百分比值填充 DataFrame
❽ 使用整数值填充 DataFrame
❾ 让我们为我们的训练数据创建相应的标题。
记得预训练模型的最后一层具有输出维度为 9,与处理的类的数量相匹配。要添加另一个类,我们需要将输出维度增加到大小为 10。我们还应该将这个新维度的权重初始化为文本类的权重,因为这是预训练模型处理的最相似的类。这是在我们之前使用预训练模型将百分比数据预测为文本时确定的。这是通过下一个清单中显示的脚本完成的。在脚本中,我们将百分比添加到支持的类别列表中,将输出维度增加 1 以容纳此添加,然后将相应维度的权重初始化为最接近的类别文本值的权重。
清单 6.2 创建最终输出层的新权重,包括百分比类
import numpy as np
old_weights = model.layers[8].get_weights() ❶
old_category_index = encoder.categories.index('text') ❷
encoder.categories.append("percent") ❸
encoder.categories.sort() ❹
new_category_index = encoder.categories.index('percent') ❺
new_weights = np.copy(old_weights) ❻
new_weights[0] = np.insert(new_weights[0], new_category_index, old_weights[0][:,old_category_index], axis=1) ❼
new_weights[1] = np.insert(new_weights[1], new_category_index, 0) ❽
❶ 抓取初始化的最后一层权重
❷ 找到最接近类别的旧权重索引—文本
❸ 使用新的类别列表更新编码器
❹ 对新列表按字母顺序排序
❺ 找到新类别的索引
❻ 将新权重初始化为旧权重
❼ 在百分比权重位置插入文本权重
❽ 在百分比偏差位置插入文本偏差
在执行清单 6.2 中的代码之后,您应该仔细检查数组old_weights
和new_weights
的形状。如果一切按预期进行,您应该会发现前者是(128,9),而后者是(128,10)。
现在我们已经准备好在预训练之前用来初始化新模型的权重,让我们实际构建和编译这个新模型。SIMOn API 包含以下函数,使构建模型非常容易:
model = Classifier.generate_transfer_model(max_len, max_cells, category_count, category_count+1, checkpoint, checkpoint_dir)
通过此函数返回的转移模型与我们之前构建的模型完全类似,唯一的区别是最终层现在具有新的维度,由输入category_count+1
指定。另外,因为我们没有为新创建的输出层提供任何初始化信息,所以这一层目前被初始化为全零权重。
在我们可以训练这个新的转移模型之前,让我们确保只有最终输出层是可训练的。我们通过以下代码片段完成这一点,并编译模型:
for layer in model.layers: ❶
layer.trainable = False
model.layers[-1].trainable = True ❷
model.layers[8].set_weights(new_weights) ❸
model.compile(loss='binary_crossentropy',optimizer='adam', metrics=['accuracy'])❹
❶ 开始时使所有层都不可训练
❷ 只有最后一层应该是可训练的。
❸ 将最终层的权重设置为先前确定的初始化值
❹ 编译模型
现在我们可以使用以下清单中的代码在新数据上训练构建的、初始化的和编译的转移模型。
清单 6.3 训练初始化和编译的新转移模型
import time
X = encoder.encodeDataFrame(new_raw_data) ❶
y = encoder.label_encode(header) ❷
data = Classifier.setup_test_sets(X, y) ❸
batch_size = 4
nb_epoch = 10
start = time.time()
history = Classifier.train_model(batch_size, checkpoint_dir, model, nb_epoch, data)❹
end = time.time()
print("Time for training is %f sec"%(end-start))
❶ 编码新数据(标准化、转置、转换为 NumPy 数组)
❷ 编码标签
❸ 准备预期格式的数据 -> 60/30/10 训练/验证/测试数据拆分
❹ 训练数据
我们在图 6.3 中可视化了此代码生成的收敛信息。我们看到在第七个时期实现了 100%的验证准确率,训练时间为 150 秒。看来我们的实验成功了,我们已成功地微调了预训练模型以处理新的数据类!我们注意到,为了使这个新模型能够准确地处理所有 10 个类,我们需要在转移步骤中的训练数据中包含每个类的一些样本。在这个阶段,微调的模型只适用于预测包含在转移步骤中的类——整数
和百分比
。因为我们这里的目标仅仅是说明性的,我们将此作为读者的警告,并不进一步关注。
图 6.3 百分比类转移表格数据实验收敛可视化
作为转移实验的最后一步,让我们通过比较测试集的预测标签和真实标签来深入了解其性能。可以通过以下代码片段来完成这个任务:
y = model.predict(data.X_test) ❶
result = encoder.reverse_label_encode(y,p_threshold) ❷
print("The predicted classes and probabilities are respectively:") ❸
print(result)
print("True labels/probabilities, for comparision:") print(encoder.reverse_label_encode(data.y_test,p_threshold))
❶预测类别
❷将概率转换为类标签
❸ 检查
生成的输出如下:
The predicted classes and probabilities are respectively:
([('percent',), ('percent',), ('int',), ('int',), ('percent',), ('int',), ('percent',), ('int',), ('int',), ('percent',), ('percent',), ('int',)], [[0.7889140248298645], [0.7893422842025757], [0.7004106640815735], [0.7190601229667664], [0.7961368560791016], [0.9885498881340027], [0.8160757422447205], [0.8141483068466187], [0.5697212815284729], [0.8359809517860413], [0.8188782930374146], [0.5185337066650391]])
True labels/probabilities, for comparision:
([('percent',), ('percent',), ('int',), ('int',), ('percent',), ('int',), ('percent',), ('int',), ('int',), ('percent',), ('percent',), ('int',)], [[1], [1], [1], [1], [1], [1], [1], [1], [1], [1], [1], [1]])
我们发现,微调模型已经完全正确地预测了每个例子,进一步验证了我们的迁移学习实验。
最后要记住,通过在 6.1.2 节中描述的适应过程,SIMOn 框架可以应用于任意输入文本,而不仅仅是表格数据。几个应用示例取得了有希望的结果。¹希望本节的练习已经充分准备您在自己的分类应用程序中部署它,并通过迁移学习将生成的分类器适应新情况。
现在我们将继续探讨将 ELMo 应用于虚假新闻分类示例的情况。
6.2 来自语言模型的嵌入(ELMo)
如前一章节简要提到的,来自语言模型的嵌入(ELMo)可以说是与正在进行的 NLP 迁移学习革命相关的最受欢迎的早期预训练语言模型之一。它与 SIMOn 有一些相似之处,因为它也由字符级 CNN 和双向 LSTM 组成。请参考图 5.2,这里重复了一遍,以便鸟瞰这些建模组件。
图 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)
❶ 输出 256 维特征向量的新层
❷ 分类层
❸ 损失、度量和优化器的选择
❹ 显示用于检查的模型架构
❺ 将模型拟合 10 个 epochs
让我们更仔细地查看模型结构,该结构由前述代码片段中的model.summary()
语句输出:
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 1) 0
_________________________________________________________________
elmo_embedding_layer_1 (Elmo (None, 1024) 4
_________________________________________________________________
dense_1 (Dense) (None, 256) 262400
_________________________________________________________________
dense_2 (Dense) (None, 1) 257
=================================================================
Total params: 262,661
Trainable params: 262,661
Non-trainable params: 0
dense_1
和dense_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 pooling和gradual unfreezing。
在 ULMFiT 开发时,将基于 LSTM 的语言模型的最终单元的隐藏状态传递给任务特定层是标准做法。作者建议将这些最终隐藏状态与所有时间步的最大池化和平均池化隐藏状态串联起来(尽可能多地适应内存)。在双向上下文中,他们分别为前向和后向语言模型执行此操作,并平均预测结果。他们称之为concat pooling的过程与 ELMo 描述的双向语言建模方法执行类似的功能。
为了减少微调时灾难性遗忘的风险,作者建议逐渐解冻和调整。这个过程从最后一层开始,该层包含最少的通用知识,并且在第一个 epoch 时是唯一解冻和精炼的层。在第二个 epoch 中,将解冻一个额外的层,并重复该过程。该过程持续到所有任务特定层都在该渐进解冻过程的最后迭代中解冻和微调。
作为提醒,这些技术将在第九章的代码中探讨,该章节将涵盖各种适应策略。
摘要
-
与词级模型相反,字符级模型可以处理拼写错误和其他社交媒体特征,例如表情符号和小众俚语。
-
双向语言建模对于构建具有意识到其局部上下文的词嵌入至关重要。
-
SIMOn 和 ELMo 都使用字符级 CNN 和双向 LSTM,后者有助于实现双向上下文建模。
-
将预训练语言模型适应新场景可能会受益于对模型的不同层进行不同速率的微调,这应根据倾斜三角形时间表首先增加然后减少。
-
将任务特定的层适应新情境可能会受益于逐渐解冻和微调不同的层,从最后一层开始解冻,逐渐解冻更多层,直到所有层都被精细调整。
-
ULMFiT 采用辨别微调,倾斜三角形学习率和渐进解冻来缓解微调语言模型时的过拟合和灾难性遗忘。
-
N. Dhamani 等人,“使用深度网络和迁移学习解决虚假信息问题”,AI for Social Good ICML Workshop(2019 年)。
第三部分:为基于转换器和适应策略的深度迁移学习
第七章和第八章涵盖了这一领域中可能最重要的子领域,即依赖于转换神经网络进行关键功能的深度迁移学习技术,比如 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
注意:回想一下前面的章节,感叹号(!)只在 Jupyter 环境中执行时需要,比如我们推荐的 Kaggle 环境中。在通过终端执行时,它应该被去掉。
tensor2tensor 包含了 transformers 架构的原始作者实现,以及一些可视化工具。bertviz 库是这些可视化工具对 transformers 库中大量模型的扩展。注意,要渲染可视化内容需要激活 JavaScript(我们会在相关 Kaggle 笔记本中告诉你如何做)。
transformers 库可以通过以下方式安装:
!pip install transformers
注意,它已经安装在 Kaggle 上的新笔记本中。
为了我们的可视化目的,我们看看了 BERT 编码器的自注意力。这可以说是基于 transformer 架构最流行的一种变体,类似于原始架构图 7.1 中编码器-解码器架构中的编码器。我们将在第 8.1 节的图 8.1 中明确可视化 BERT 体系结构。现在,您需要注意的是 BERT 编码器与 transformer 的编码器完全相同。
对于您想在 transformers 库中加载的任何预训练模型,需要使用以下命令加载标记器以及模型:
from transformers import BertTokenizer, BertModel ❶
model = BertModel.from_pretrained('bert-base-uncased', output_attentions=True) ❷
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True) ❸
❶ transformers BERT 标记器和模型
❷ 加载不区分大小写的 BERT 模型,确保输出注意力
❸ 加载不区分大小写的 BERT 标记器
请注意,我们在这里使用的不区分大小写的 BERT 检查点与我们在第三章(清单 3.7)中使用的相同,即当我们通过 TensorFlow Hub 首次遇到 BERT 模型时。
您可以对我们正在运行的示例句子进行标记化,将每个令牌编码为其在词汇表中的索引,并使用以下代码显示结果:
sentence = "He didnt want to talk about cells on the cell phone because he considered it boring"
inputs = tokenizer.encode(sentence, return_tensors='tf', add_special_tokens=True) ❶
print(inputs)
❶ 将 return_tensors 更改为“pt”将返回 PyTorch 张量。
这产生以下输出:
tf.Tensor(
[[ 101 2002 2134 2102 2215 2000 2831 2055 4442 2006 1996 3526
3042 2138 2002 2641 2009 11771 102]], shape=(1, 19), dtype=int32)
我们可以通过在inputs
变量上执行以下代码轻松地返回一个 PyTorch 张量,只需设置return_tensors='pt'
。要查看这些索引对应的标记,可以执行以下代码:
tokens = tokenizer.convert_ids_to_tokens(list(inputs[0])) ❶
print(tokens)
❶ 从输入列表的列表中提取批次索引 0 的示例
这产生以下输出:
['[CLS]', 'he', 'didn', '##t', 'want', 'to', 'talk', 'about', 'cells', 'on', 'the', 'cell', 'phone', 'because', 'he', 'considered', 'it', 'boring', '[SEP]']
我们立即注意到,通过编码inputs
变量时通过add_special_tokens
参数请求的“特殊令牌”指的是此案例中的'[CLS]'
和'[SEP]'
令牌。前者表示句子/序列的开头,而后者表示多个序列的分隔点或序列的结束(如在此案例中)。请注意,这些是 BERT 相关的,您应该检查您尝试的每种新架构的文档以查看它使用的特殊令牌。我们从这次标记化练习中注意到的另一件事是分词是次词—请注意didn
如何被分成didn
和##t
,即使没有撇号(’),我们刻意省略掉了。
让我们继续通过定义以下函数来可视化我们加载的 BERT 模型的自注意力层:
from bertviz.bertviz import head_view ❶
def show_head_view(model, tokenizer, sentence): ❷
input_ids = tokenizer.encode(sentence, return_tensors='pt', add_special_tokens=True) ❸
attention = model(input_ids)[-1] ❹
tokens = tokenizer.convert_ids_to_tokens(list(input_ids[0]))
head_view(attention, tokens) ❺
show_head_view(model, tokenizer, sentence) ❻
❶ bertviz 注意力头可视化方法
❷ 功能用于显示多头注意力
❸ 一定要在 bertviz 中使用 PyTorch。
❹ 获取注意力层
❺ 调用内部 bertviz 方法来显示自注意力
❻ 调用我们的函数来渲染可视化
图 7.3 显示了我们示例句子的最终 BERT 层的自注意力可视化的结果。您应该使用可视化并滚动浏览各层各个词的可视化。注意,并非所有注意力可视化都像这个示例那样容易解释,这可能需要一些练习来建立直觉。
图 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")
MarianMTModel
类是从 C++ 库 MarianNMT 移植而来的编码器-解码器变压器架构。¹⁰ 请注意,如果研究小组提供了相应的代码,你可以通过简单地更改语言代码 en
和 tw
来更改源语言和目标语言。例如,加载一个法语到英语的模型将会改变输入配置字符串为 Helsinki-NLP/opus-mt-fr-en
。
如果我们在网上与加纳的朋友聊天,并想知道如何用介绍的方式写“我的名字是保罗”,我们可以使用以下代码计算并显示翻译:
text = "My name is Paul" ❶
inputs = tokenizer.encode(text, return_tensors="pt") ❷
outputs = model.generate(inputs) ❸
decoded_output = [tokenizer.convert_ids_to_tokens(int(outputs[0][i])) for i in range(len(outputs[0]))] ❹
print("Translation:") ❺
print(decoded_output)
❶ 输入要翻译的英语句子
❷ 将输入编码为标记 ID
❸ 生成输出的标记 ID
❹ 将输出的标记 ID 解码为实际输出标记
❺ 显示翻译
运行代码后得到的输出结果如下所示:
Translation:
['<pad>', '▁Me', '▁din', '▁de', '▁Paul']
我们立即注意到的第一件事是输出中存在一个我们以前没有见过的特殊标记 <pad>
,以及每个单词前面的下划线。这与第 7.1.1 节中 BERT 分词器产生的输出不同。技术原因是 BERT 使用了一个称为 WordPiece 的分词器,而我们这里的编码器-解码器模型使用了 SentencePiece。虽然我们在这里没有详细讨论这些分词器类型之间的差异,但我们利用这个机会再次警告您,务必查阅有关您尝试的任何新分词器的文档。
翻译“Me din de Paul”恰好是完全正确的。太好了!这并不太难,是吗?然而,对于输入句子“How are things?”的重复练习却得到了翻译“Ɔkwan bɛn so na nneɛma te saa?”,它直译成“事情是什么样的?”我们可以看到,虽然这个翻译的语义看起来很接近,但翻译是错误的。然而,语义相似性表明,该模型是一个很好的基线,如果有好的平行英文-特威语数据可用,可以通过迁移学习进一步改进。此外,将输入句子改写为“How are you?”则从这个模型得到了正确的翻译“Wo ho te dɛn?”。总的来说,这个结果是非常令人鼓舞的,我们希望一些读者受到启发,致力于将这些基线模型扩展到一些以前未解决的低资源语言的优秀开源转换器模型。
接下来,我们来看一下生成式预训练转换器(GPT),这是一种基于转换器的模型,用于文本生成,在自然语言处理(NLP)社区中变得非常有名。
7.2 生成式预训练转换器
生成式预训练转换器(Generative Pretrained Transformer)[¹¹](GPT)是由 OpenAI 开发的,并且是最早将转换器架构应用于本书讨论的半监督学习场景的模型之一。通过这个,我们指的当然是在大量文本数据上无监督(或自监督)预训练语言理解模型,然后在最终感兴趣的目标数据上进行监督微调。作者发现在四种类型的语言理解任务上的性能得到了显著提升。这些任务包括自然语言推理、问答、语义相似度和文本分类。值得注意的是,在通用语言理解评估(GLUE)基准上的表现,该基准包括这些以及其他困难和多样化的任务,提高了超过 5 个百分点。
GPT 模型已经经历了几次迭代——GPT、GPT-2,以及最近的 GPT-3。事实上,在撰写本文时,GPT-3 恰好是已知的最大的预训练语言模型之一,具有 1750 亿个参数。它的前身 GPT-2 具有 15 亿个参数,在其发布的前一年也被认为是最大的。在 2020 年 6 月发布 GPT-3 之前,最大的模型是微软的图灵-NLG,该模型具有 170 亿个参数,并于 2020 年 2 月发布。在某些指标上的进展速度之快令人难以置信,并且这些记录很可能很快就会过时。事实上,当最初披露 GPT-2 时,作者认为不完全开源技术是正确的做法,考虑到可能会被恶意行为者滥用的潜力。
虽然在最初发布时,GPT 成为了大多数上述任务的最先进技术,但它通常更受青睐作为一种文本生成模型。与 BERT 及其衍生模型不同,后者已经主导了大多数其他任务,GPT 是以因果建模目标(CLM)进行训练的,其中预测下一个标记,而不是 BERT 的掩码语言建模(MLM)填空类型的预测目标,我们将在下一章更详细地介绍。
在下一小节中,我们简要描述了 GPT 架构的关键方面。接着介绍了 transformers 库中用于最常见任务的预训练模型的最小执行的pipelines API 概念。我们将此概念应用于 GPT 在其擅长的任务——文本生成方面。与前一节关于编码器-解码器变压器和翻译的内容一样,我们在此处不会明确地在更特定的目标数据上对预训练的 GPT 模型进行改进。然而,结合下一章的最后一节——我们在单语 Twi 数据上对多语言 BERT 模型进行微调——您将获得一套用于进一步文本生成迁移学习研究的强大工具。
7.2.1 架构概述
您可能还记得 7.1.1 节中我们可视化 BERT 自注意力的情况,BERT 本质上是原始编码器-解码器变压器架构的一组叠加编码器。从这个意义上讲,GPT 本质上是它的反义词,它将解码器堆叠起来。从图 7.2 中可以看出,除了编码器-解码器注意力之外,变压器解码器的另一个显著特征是其自注意力层是“掩码的”,即在计算给定标记的注意力时,“未来标记”被“掩码”了。我们复制图 7.2 供您参考,突出显示此掩码层。
图 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')
提醒一下,GPT 最初的形式非常适合于开放式文本生成,比如创造性地写出一些文本段落来补充之前的文本。让我们看看当模型以“在彩虹的那边……”为提示,生成最多 100 个标记时,模型生成了什么,通过以下命令:
gpt("Somewhere over the rainbow", max_length=100)
这生成了以下文本:
[{'generated_text': "Somewhere over the rainbow people live! I wonder how they get to know each other... They just have a wonderful community out there - but when they see each other as two of the best in school they never even realize them, just love, family, friends, and friends. I'm really proud of their talent and dedication to life. I've seen a lot of people that were raised by their mother and grandma in the Midwest and didn't understand there was such an opportunity and I truly cannot"}]
即使消息有些不连贯,这似乎在语义上非常正确。您可以想象一位创意作家使用它来生成想法以克服写作困境!现在,让我们看看是否可以用一些不太“创意”的东西来启动模型,一些更技术性的东西,以查看它的表现。让我们通过以下代码将模型启动文本设置为“迁移学习是一门研究领域”:
gpt("Transfer learning is a field of study", max_length=100)
这产生了以下输出:
[{'generated_text': "Transfer learning is a field of study that has been around for centuries, and one that requires a thorough grounding in mathematics in order to understand the complexities of these systems. If you go to the library for your high school physics course, you know you're on the right track. The only problem with this position is that people don't ask questions. The only thing they really do ask is: how do we figure out how to apply these processes to the rest of physics and other sciences?\n\nIn"}]
再次,我们可以看到,从语义连贯性、语法结构、拼写、标点等方面来看,这段文字非常好——实际上,甚至有点诡异。然而,随着它的继续,它变得可能事实上不正确。我们都可以同意,要真正理解迁移学习,需要对数学有扎实的基础,甚至可以说它已经存在了几个世纪——通过我们,人类!然而,它不是物理学的一个领域,即使在需要掌握它的技能方面可能有些类似。我们可以看到,模型的输出允许它说话的时间越长,就越不可信。
请务必进行更多实验,以了解模型的优缺点。例如,您可以尝试使用我们的示例句子提示模型,“他不想在手机上谈论细胞,因为他认为这很无聊。”我们发现这在创意写作空间和技术写作空间中都是一个可信的应用,max_length
设置为较小的数值。对许多作者来说,它已经是一个可信的辅助工具。在撰写本文时,我们只能想象 GPT-3 能够做到什么。未来确实非常令人兴奋。
玩弄文本生成后,让我们看看是否可以以某种方式使用它来创建聊天机器人。
7.2.3 聊天机器人的应用
直觉上应该能够无需对此应用进行重大修改即可采用 GPT。幸运的是,微软的人员已经通过模型 DialoGPT 完成了这一点,该模型最近也被包含在 transformers 库中。它的架构与 GPT 相同,只是增加了特殊标记,以指示对话中参与者的回合结束。在看到这样的标记后,我们可以将参与者的新贡献添加到启动上下文文本中,并通过直接应用 GPT 来生成聊天机器人的响应,迭代重复这个过程。自然地,预训练的 GPT 模型在会话文本上进行了微调,以确保响应是适当的。作者们使用 Reddit 主题进行了微调。
让我们继续构建一个聊天机器人吧!在这种情况下,我们不会使用管道,因为在撰写本文时,该模型尚未通过该 API 公开。这使我们能够对比调用这些模型进行推理的不同方法,这对你来说是一个有用的练习。
首先要做的事情是通过以下命令加载预训练模型和分词器:
from transformers import GPT2LMHeadModel, GPT2Tokenizer ❶
import torch ❷
tokenizer = GPT2Tokenizer.from_pretrained("microsoft/DialoGPT-medium")
model = GPT2LMHeadModel.from_pretrained("microsoft/DialoGPT-medium")
❶ 请注意,DialoGPT 模型使用 GPT-2 类。
❷ 我们在这里使用 Torch 而不是 TensorFlow,因为 transformers 文档中默认选择的是 Torch 平台。
此处值得强调几点。首先,请注意我们使用的是 GPT-2 模型类,这与我们先前讨论的 DialoGPT 作为该架构的直接应用是一致的。另外,请注意我们可以与这些 GPT 特定的模型类交换使用AutoModelWithLMHead
和AutoTokenizer
类。这些实用程序类会检测用于加载指定模型的最佳类别,例如,在这种情况下,它们将检测到最佳要使用的类别为GPT2LMHeadModel
和GPT2Tokenizer
。浏览 transformers 库文档时,你可能会遇到这些实用程序类,了解它们的存在对你的代码更一般化是有好处的。最后请注意,这里使用的是 GPT 的“LMHead”版本。这意味着从普通 GPT 得到的输出将通过一个线性层和一个归一化层,然后转换成一个维度等于词汇表大小的概率向量。最大值对应于模型正确训练的情况下下一个最有可能的令牌。
与我们加载的预训练 DialoGPT 模型进行对话的代码如列表 7.1 所示。我们首先指定最多五个回应的最大数量。然后,我们编码用户在每个轮次的对话,将对话添加到聊天历史记录中,并将其传递给加载的预训练 DialoGPT 模型,以生成下一个响应。
列表 7.1 与预训练 DialoGPT 模型进行对话,最多五个聊天机器人响应
conversation_length = 5 ❶
for step in range(conversation_length):
new_user_inputs_ids = tokenizer.encode(input("User: ") + tokenizer.eos_token, return_tensors='pt') ❷
bot_input_ids = torch.cat([chat_history_ids, new_user_inputs_ids], dim=1) ❸
if step > 0 else new_user_inputs_ids
chat_history_ids = model.generate(bot_input_ids, max_length=1000, pad_token_id=tokenizer.eos_token_id) ❹
print("DialoGPT: {}".format(tokenizer.decode(chat_history_ids[:, bot_input_ids.shape[-1]:][0], skip_special_tokens=True))) ❺
❶ 五行聊天
❷ 编码新用户输入,添加一个句子结束标记,并返回张量
❸ 将新输入添加到聊天历史记录中
❹ 使用聊天历史记录作为上下文生成最多 max_length 令牌的响应
❺ 显示响应
一个人可能整天都和这个机器人玩耍!我们很开心地向它提问各种问题,并以各种方式提示它。我们在这里包括了其中一个交流以供您娱乐:
User: Hi there.
DialogGPT: Hi there.
User: How are you today?
DialogGPT: I'm good, how are you?
User: Good! How much money do you have?
DialogGPT: I have about 100k.
User: What will you spend it on?
DialogGPT: I'm not sure, I'm not sure what I want to spend it on.
User: Make a decision, life is short.
DialogGPT: I'm going to go with a lot of things.
接下来这短暂对话的另一端很可能是一个人,不是吗?那是否意味着它通过了图灵测试?并非如此,下面的交流说明了这一点:
User: Hi robot.
DialogGPT: Hello, human.
User: Huh?
DialogGPT: I'm a bot.
User: OK, what is your name?
DialogGPT: Robot. I'm a robot.
User: All right then.
DialogGPT: Robot. I'm a robot.
User: Say something else.
DialogGPT: Robot. I'm a robot.
当你增加允许的对话轮次数量时,你会发现机器人会陷入重复的与话题无关的回复中。这类似于 GPT 开放式文本生成随着生成文本长度的增加变得更加荒谬。改善这一点的一个简单方法是保持固定的局部上下文大小,其中模型只受到该上下文内的对话历史的提示。当然,这意味着对话不总是考虑整个对话的上下文——这是必须对任何给定应用进行实验探索的一个权衡。
想象一下 GPT-3 在这些问题上的表现会有多好,是不是令人兴奋?在本书的最后一章中,我们将简要讨论更多关于 GPT-3 的细节,并介绍一个最近推出的更小但同样值得关注的开源替代品:EleutherAI 的 GPT-Neo。它已经可以在 transformers 库中使用,并且可以通过将 model
字符串设置为 EleutherAI 提供的模型名称之一来直接使用。[¹³]我们还附上了一个伴随笔记本,在其中展示了它在本章练习中的应用。经过检查,你应该会发现它的性能更好,但自然也会有显著更高的成本(最大模型的权重超过 10 GB!)。
在下一章中,我们将讨论变压器家族中可能最重要的成员——BERT。
总结
-
变压器架构使用自注意力机制来构建文本的双向上下文。这使得它成为了近期在自然语言处理中占主导地位的语言模型。
-
变压器允许对序列中的令牌进行独立处理。这比处理顺序的双向 LSTM 实现了更大的并行性。
-
变压器是翻译应用的不错选择。
-
在训练过程中,生成预训练变压器使用因果建模目标。这使得它成为文本生成的首选模型,例如聊天机器人应用。
-
A. Vaswani 等人,“Attention Is All You Need”,NeurIPS(2017)。
-
A. Radford 等人,“通过生成预训练来改善语言理解”,arXiv(2018)。
-
M. E. Peters 等人,“BERT:用于语言理解的深度双向变压器的预训练”,NAACL-HLT(2019)。
-
A. Radford 等人,“通过生成预训练来改善语言理解”,arXiv(2018)。
-
A. Vaswani 等人,“Attention Is All You Need”,NeurIPS(2017)。
-
A. Radford 等人,“通过生成式预训练提高语言理解能力”,arXiv(2018)。
-
Y. Zhang 等人,“DialoGPT:面向对话回应生成的大规模生成式预训练”,arXiv(2019)。
第八章:使用 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)
❶ 用于构建模型的函数
❷ 我们不重新训练任何 BERT 层,而是将预训练模型用作嵌入,并在其上重新训练一些新层。
❸ Vanilla TensorFlow 初始化调用
❹ 使用 BERT 源代码库中的函数创建兼容的分词器
❺ 使用 BERT 源代码库中的函数将数据转换为 InputExample 格式
❻ 将 InputExample 格式转换为三元 BERT 输入格式,使用 BERT 源存储库中的函数
❼ 构建模型
❽ 实例化变量
❾ 训练模型
如前一章节所述,输入 ID 只是词汇表中对应标记的整数 ID——对于 BERT 使用的 WordPiece 分词,词汇表大小为 30,000。由于变换器的输入长度是由列表 3.8 中的超参数 max_seq_length
定义的,因此需要对较短的输入进行填充,对较长的输入进行截断。输入掩码只是相同长度的二进制向量,其中 0 对应填充标记 ([PAD]
),1 对应实际输入。段 ID 与图 8.2 中描述的相同。另一方面,位置编码和输入嵌入由 TensorFlow Hub 模型在内部处理,用户无法访问。可能需要再次仔细阅读第三章才能充分理解这种比较。
尽管 TensorFlow 和 Keras 仍然是任何自然语言处理工程师工具箱中至关重要的组件——具有无与伦比的灵活性和效率——但 transformers 库无疑使这些模型对许多工程师和应用更加易于接近和使用。在接下来的小节中,我们将使用该库中的 BERT 应用于问题回答、填空和下一个句子预测等关键应用。
8.1.2 问题回答的应用
自然语言处理领域的开端以来,问题回答一直吸引着计算机科学家的想象力。它涉及让计算机在给定某些指定上下文的情况下自动回答人类提出的问题。潜在的应用场景仅受想象力限制。突出的例子包括医学诊断、事实检查和客户服务的聊天机器人。事实上,每当你在谷歌上搜索像“2010 年超级碗冠军是谁?”或“2006 年谁赢得了 FIFA 世界杯?”这样的问题时,你正在使用问题回答。
让我们更加仔细地定义问题回答。更具体地说,我们将考虑 抽取式问题回答,定义如下:给定上下文段落 p 和问题 q,问题回答的任务是产生 p 中答案所在的起始和结束整数索引。如果 p 中不存在合理的答案,系统也需要能够指示这一点。直接尝试一个简单的例子,如我们接下来使用预训练的 BERT 模型和 transformers pipelines API 做的,将帮助你更好地具体了解这一点。
我们从世界经济论坛⁵中选择了一篇有关口罩和其他封锁政策对美国 COVID-19 大流行的有效性的文章。我们选择文章摘要作为上下文段落。请注意,如果没有文章摘要可用,我们可以使用相同库中的摘要流水线快速生成一个。以下代码初始化了问答流水线和上下文。请注意,这种情况下我们使用了 BERT LARGE,它已经在斯坦福问答数据集(SQuAD)⁶上进行了微调,这是迄今为止最广泛的问答数据集。还请注意,这是 transformers 默认使用的任务,默认模型,我们不需要显式指定。但是,我们为了透明度而这样做。
from transformers import pipeline
qNa= pipeline('question-answering', model= 'bert-large-cased-whole-word-masking-finetuned-squad', tokenizer='bert-large-cased-whole-word-masking-finetuned-squad') ❶
paragraph = 'A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer deaths by the start of June. Containment policies had a large impact on the number of COVID-19 cases and deaths, directly by reducing transmission rates and indirectly by constraining people’s behaviour. They account for roughly half the observed change in the growth rates of cases and deaths.'
❶ 这些模型通常会被默认加载,但我们明确指出以保持透明度。使用已在 SQuAD 上进行了微调的模型非常重要;否则,结果将很差。
在初始化了流水线之后,让我们首先看看是否能够通过询问文章的主题来自动提取文章的精髓。我们用以下代码来实现:
ans = qNa({'question': 'What is this article about?','context': f'{paragraph}'})
print(ans)
这产生了以下输出,我们可能会认为这是一个合理的回答:
{'score': 0.47023460869354494, 'start': 148, 'end': 168, 'answer': 'Containment policies'}
注意,0.47 相对较低的分数表明答案缺少一些上下文。类似“遏制政策对 COVID-19 的影响”可能是更好的回答,但因为我们正在进行提取式问答,而这个句子不在上下文段落中,所以这是模型能做到的最好的。低分数可以帮助标记此回答进行人工双重检查和/或改进。
为什么不问一些更多的问题?让我们看看模型是否知道文章中描述的是哪个国家,使用以下代码:
ans = qNa({'question': 'Which country is this article about?',
'context': f'{paragraph}'})
print(ans)
这产生了以下输出,正如以前的分数约为 0.8 所示,完全正确:
{'score': 0.795254447990601, 'start': 34, 'end': 36, 'answer': 'US'}
讨论的是哪种疾病?
ans = qNa({'question': 'Which disease is discussed in this article?',
'context': f'{paragraph}'})
print(ans)
输出完全正确,信心甚至比之前更高,达到了 0.98,如下所示:
{'score': 0.9761025334558902, 'start': 205, 'end': 213, 'answer': 'COVID-19'}
那时间段呢?
ans = qNa({'question': 'What time period is discussed in the article?',
'context': f'{paragraph}'})
print(ans)
与输出相关联的 0.22 的低分数表明结果质量差,因为文章中讨论了 4 月至 6 月的时间范围,但从未在连续的文本块中讨论,可以为高质量答案提取,如下所示:
{'score': 0.21781831588181433, 'start': 71, 'end': 79, 'answer': '1 April,'}
然而,仅选择一个范围的端点能力已经是一个有用的结果。这里的低分数可以提醒人工检查此结果。在自动化系统中,目标是这样的较低质量答案成为少数,总体上需要很少的人工干预。
在介绍了问答之后,在下一小节中,我们将解决 BERT 训练任务的填空和下一句预测。
8.1.3 应用于填空和下一句预测任务
我们在这一节的练习中使用了上一小节的文章。让我们立即开始编写一个用于填写空白的流程,使用以下代码:
from transformers import pipeline
fill_mask = pipeline("fill-mask",model="bert-base-cased",tokenizer="bert-base-cased")
注意,在这里我们使用的是 BERT BASE 模型。这些任务对任何 BERT 模型的训练来说都是基本的,所以这是一个合理的选择,不需要特殊的微调模型。初始化适当的流程后,我们现在可以将它应用于上一小节中文章的第一句话。我们通过用适当的掩码标记[MASK]
来删除“cases”这个词,并使用以下代码向模型提供已省略的词进行预测:
fill_mask("A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer [MASK] by the start of June")
输出显示,最高的是“deaths”,这是一个可能合理的完成。即使剩下的建议也可以在不同的情境下起作用!
[{'sequence': '[CLS] A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer deaths by the start of June [SEP]',
'score': 0.19625532627105713,
'token': 6209},
{'sequence': '[CLS] A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer executions by the start of June [SEP]',
'score': 0.11479416489601135,
'token': 26107},
{'sequence': '[CLS] A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer victims by the start of June [SEP]',
'score': 0.0846652239561081,
'token': 5256},
{'sequence': '[CLS] A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer masks by the start of June [SEP]',
'score': 0.0419488325715065,
'token': 17944},
{'sequence': '[CLS] A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer arrests by the start of June [SEP]',
'score': 0.02742016687989235,
'token': 19189}]
我们鼓励你尝试从各种句子中省略各种单词,以确信这几乎总是非常有效的。在节省篇幅的情况下,我们的附带笔记本会为几个更多的句子做到这一点,但我们不在这里打印这些结果。
然后我们继续进行下一个句子预测(NSP)任务。在写作本文时,此任务尚未包含在 pipelines API 中。因此,我们将直接使用 transformers API,这也将让您更加熟悉它。我们首先需要确保已安装 transformers 3.0.0 以上的版本,因为该任务仅在该阶段的库中包含。我们使用以下代码实现这一点;在写作本文时,Kaggle 默认安装了较早的版本:
!pip install transformers==3.0.1 # upgrade transformers for NSP
升级版本后,我们可以使用以下代码加载一个 NSP-specific BERT:
from transformers import BertTokenizer, BertForNextSentencePrediction ❶
import torch
from torch.nn.functional import softmax ❷
tokenizer = BertTokenizer.from_pretrained('bert-base-cased')
model = BertForNextSentencePrediction.from_pretrained('bert-base-cased')
model.eval() ❸
❶ NSP-specific BERT
❷ 计算原始输出的最终概率
❸ PyTorch 模型默认是可训练的。为了更便宜的推断和可执行重复性,将其设置为“eval”模式,如此处所示。通过 model.train()将其设置回“train”模式。对于 TensorFlow 模型不适用!
作为健全性检查,首先我们要确定第一句和第二句是否从模型的角度来看是合理的完成。我们使用以下代码进行检查:
prompt = "A new study estimates that if the US had universally mandated masks on 1 April, there could have been nearly 40% fewer deaths by the start of June."
next_sentence = "Containment policies had a large impact on the number of COVID-19 cases and deaths, directly by reducing transmission rates and indirectly by constraining people’s behavior."
encoding = tokenizer.encode(prompt, next_sentence, return_tensors='pt')
logits = model(encoding)[0] ❶
probs = softmax(logits) ❷
print("Probabilities: [not plausible, plausible]")
print(probs)
❶ 输出是一个元组;第一项描述了我们追求的两个句子之间的关系。
❷ 从原始数字计算概率
注意代码中的术语logits
。这是 softmax 函数的原始输入。通过 softmax 将logits
传递,可以得到概率。代码的输出确认找到了正确的关系,如下所示:
Probabilities: [not plausible, plausible]
tensor([[0.1725, 0.8275]], grad_fn=<SoftmaxBackward>)
现在,让我们将第二个句子替换为一个有点随机的“Cats are independent.” 这将产生以下结果:
Probabilities: [not plausible, plausible]
tensor([0.7666, 0.2334], grad_fn=<SoftmaxBackward>)
看起来一切都如预期的那样工作!
现在,你应该已经非常清楚 BERT 在训练中解决哪些任务了。需要注意的是,本章我们还没有将 BERT 调整到任何新域或任务特定的数据上进行微调。这是有意为之的,以帮助你在没有任何干扰的情况下了解模型架构。在下一节中,我们会演示如何进行微调,通过进行跨语言迁移学习实验。对于我们已经介绍过的所有其他任务,都可以采用类似的迁移学习方式进行,通过完成下一节练习,您将有很好的发挥空间去自己实践。
8.2 基于多语言 BERT(mBERT)的跨语言学习
在本节中,我们将进行本书中第二个整体和第一个主要的跨语言实验。更具体地说,我们正在进行一个迁移学习实验,该实验涉及从多语言 BERT 模型中转移知识到其原始训练中不包含的语言。与之前一样,我们在实验中使用的语言将是 Twi 语,这是一种被认为是“低资源”的语言,因为缺乏多种任务的高质量训练数据。
多语言 BERT(mBERT)本质上是指应用前一节中所描述的 BERT,并将其应用于约 100 个连接在一起的语言维基百科⁷ 语料库。最初的语言集合是前 100 大维基百科,现已扩展到前 104 种语言。该语言集合不包括 Twi,但包括一些非洲语言,如斯瓦希里语和约鲁巴语。由于各种语言语料库的大小差异很大,因此会应用一种“指数平滑”过程来对高资源语言(如英语)进行欠采样,对低资源语言(如约鲁巴语)进行过采样。与之前一样,使用了 WordPiece 分词。对于我们而言,它足以提醒你,这种分词过程是子词级别的,正如我们在之前的章节中所看到的。唯一的例外是中文、日文的汉字和韩文汉字,它们通过在每个字符周围加上空格的方式被转换为有效的字符分词。此外,为了在精度和模型效率之间做出权衡选择,mBERT 作者消除了重音词汇。
我们可以直观地认为,一个在 100 多种语言上训练的 BERT 模型包含了可以转移到原始训练集中未包含的语言的知识。简单来说,这样的模型很可能会学习到所有语言中共同的特征。这种共同特征的一个简单例子是单词和动词-名词关系的概念。如果我们将提出的实验框架设定为多任务学习问题,正如我们在第四章中讨论的那样,我们期望对以前未见过的新场景的泛化性能得到改善。在本节中,我们将基本证明这一点。我们首先使用预训练的分词器将 mBERT 转移到单语 Twi 数据上。然后,我们通过从头开始训练相同的 mBERT/BERT 架构以及训练适当的分词器来重复实验。比较这两个实验将允许我们定性地评估多语言转移的有效性。我们为此目的使用 JW300 数据集的 Twi 子集⁸。
本节的练习对于你的技能集具有超越多语言转移的影响。这个练习将教会你如何从头开始训练你自己的分词器和基于 transformer 的模型。它还将演示如何将一个检查点转移到这样一个模型的新领域/语言数据。之前的章节和一点冒险/想象力将为你提供基于 transformer 的迁移学习超能力,无论是用于领域自适应、跨语言转移还是多任务学习。
在接下来的小节中,我们简要概述了 JW300 数据集,然后是执行跨语言转移和从头开始训练的小节。
8.2.1 JW300 数据集简介
JW300 数据集是一个面向低资源语言的广泛覆盖的平行语料库。正如之前提到的,它是一个可能具有偏见的样本,由耶和华见证人翻译的宗教文本组成。然而,对于许多低资源语言研究而言,它是一个起点,通常是唯一可用的平行数据的开放来源。然而,重要的是要记住这种偏见,并在这个语料库上进行任何训练时配备第二阶段,该阶段可以将第一阶段的模型转移到一个更少偏见和更具代表性的语言和/或任务样本。
尽管它本质上是一个平行语料库,但我们只需要 Twi 数据的单语语料库进行我们的实验。Python 包 opustools-pkg 可以用于获取给定语言对的平行语料库。为了让您的工作更容易,我们已经为英语-Twi 语对进行了这项工作,并将其托管在 Kaggle 上。⁹要为其他低资源语言重复我们的实验,您需要稍微调整一下opustools-pkg并获取一个等价的语料库(如果您这样做,请与社区分享)。我们只使用平行语料库的 Twi 部分进行我们的实验,并忽略英语部分。
让我们继续将 mBERT 转移到单语低资源语言语料库。
8.2.2 将 mBERT 转移到单语 Twi 数据与预训练的标记器
首先要做的是初始化一个 BERT 标记器到来自 mBERT 模型中的预训练检查点。这次我们使用的是大小写版本,如下代码所示:
from transformers import BertTokenizerFast ❶
tokenizer = BertTokenizerFast.from_pretrained("bert-base-multilingual-cased")❷
❶ 这只是 BertTokenizer 的一个更快的版本,你可以用这个替代它。
❷ 使用了预训练的 mBERT 标记器
准备好了标记器后,让我们按以下方法将 mBERT 检查点加载到 BERT 遮蔽语言模型中,并显示参数数量:
from transformers import BertForMaskedLM ❶
model = BertForMaskedLM.from_pretrained("bert-base-multilingual-cased") ❷
print("Number of parameters in mBERT model:")
print(model.num_parameters())
❶ 使用了遮蔽语言建模
❷ 初始化到了 mBERT 检查点
输出表明模型有 1.786 亿个参数。
接下来,我们使用 transformers 附带的方便的 LineByLineTextDataset
方法,使用单语 Twi 文本的标记器来构建数据集,如下所示:
from transformers import LineByLineTextDataset
dataset = LineByLineTextDataset(
tokenizer=tokenizer,
file_path="../input/jw300entw/jw300.en-tw.tw",
block_size=128) ❶
❶ 指示一次读取多少行
如下代码所示,接下来我们需要定义一个“data collator” —— 一个帮助方法,通过一批样本数据行(长度为block_size
)创建一个特殊对象。 这个特殊对象适用于 PyTorch 进行神经网络训练:
from transformers import DataCollatorForLanguageModeling
data_collator = DataCollatorForLanguageModeling(
tokenizer=tokenizer,
mlm=True, mlm_probability=0.15) ❶
❶ 使用了遮蔽语言建模,并以 0.15 的概率遮蔽单词
在这里,我们使用了遮蔽语言建模,就像前一节所描述的一样。在我们的输入数据中,有 15% 的单词被随机遮蔽,模型在训练期间被要求对它们进行预测。
定义标准的训练参数,比如输出目录和训练批量大小,如下所示:
from transformers import TrainingArguments
training_args = TrainingArguments(
output_dir="twimbert",
overwrite_output_dir=True,
num_train_epochs=1,
per_gpu_train_batch_size=16,
save_total_limit=1,
)
然后使用先前定义的数据集和数据收集器定义一个“训练器”来进行数据上的一个训练周期。注意,数据包含了超过 600,000 行,因此一次遍历所有数据是相当大量的训练!
trainer = Trainer(
model=model,
args=training_args,
data_collator=data_collator,
train_dataset=dataset,
prediction_loss_only=True)
训练并计算训练时间,如下所示:
import time
start = time.time()
trainer.train()
end = time.time()
print("Number of seconds for training:")
print((end-start))
模型在所示的超参数下大约需要三个小时才能完成一个周期,并且损失大约为 0.77。
按如下进行模型保存:
trainer.save_model("twimbert")
最后,我们从语料库中取出以下句子 —— “Eyi de ɔhaw kɛse baa sukuu hɔ” —— 它的翻译是 “这在学校中提出了一个大问题。” 我们遮蔽了一个单词,sukuu(在 Twi 中意思是“学校”),然后应用 pipelines API 来预测遗漏的单词,如下所示:
from transformers import pipeline
fill_mask = pipeline( ❶
"fill-mask",
model="twimbert",
tokenizer=tokenizer)
print(fill_mask("Eyi de ɔhaw kɛse baa [MASK] hɔ.")) ❷
❶ 定义了填空管道
❷ 预测被遮蔽的标记
这将产生如下输出:
[{'sequence': '[CLS] Eyi de ɔhaw kɛse baa me hɔ. [SEP]', 'score': 0.13256989419460297, 'token': 10911}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa Israel hɔ. [SEP]', 'score': 0.06816119700670242, 'token': 12991}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa ne hɔ. [SEP]', 'score': 0.06106790155172348, 'token': 10554}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa Europa hɔ. [SEP]', 'score': 0.05116277188062668, 'token': 11313}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa Eden hɔ. [SEP]', 'score': 0.033920999616384506, 'token': 35409}]
你立刻就能看到结果中的宗教偏见。“以色列”和“伊甸园”被提议为前五个完成之一。话虽如此,它们算是比较有说服力的完成 —— 因为它们都是名词。总的来说,表现可能还算不错。
如果你不会说这种语言,不用担心。在下一节中,我们将从头开始训练 BERT,并将损失值与我们在这里获得的值进行比较,以确认我们刚刚执行的转移学习实验的功效。我们希望您能尝试在其他您感兴趣的低资源语言上尝试这里概述的步骤。
8.2.3 在单语 Twi 数据上从零开始训练的 mBERT 和分词器
要从头开始训练 BERT,我们首先需要训练一个分词器。我们可以使用下一节代码中的代码初始化、训练和保存自己的分词器到磁盘。
代码清单 8.1 从头初始化、训练和保存我们自己的 Twi 分词器
from tokenizers import BertWordPieceTokenizer
paths = ['../input/jw300entw/jw300.en-tw.tw']
tokenizer = BertWordPieceTokenizer() ❶
tokenizer.train( ❷
paths,
vocab_size=10000,
min_frequency=2,
show_progress=True,
special_tokens=["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"], ❸
limit_alphabet=1000,
wordpieces_prefix="##")
!mkdir twibert ❹
tokenizer.save("twibert")
❶ 初始化分词器
❷ 自定义训练,并进行训练
❸ 标准 BERT 特殊标记
❹ 将分词器保存到磁盘
要从刚刚保存的分词器中加载分词器,我们只需要执行以下操作:
from transformers import BertTokenizerFast
tokenizer = BertTokenizerFast.from_pretrained("twibert", max_len=512) ❶
❶ 使用我们刚刚训练的语言特定的分词器,max_len=512,以保持与上一小节一致
请注意,我们使用最大序列长度为 512,以保持与上一小节一致——这也是预训练的 mBERT 使用的长度。还要注意,保存分词器将在指定文件夹中创建词汇文件 vocab.txt 文件。
从这里开始,我们只需初始化一个全新的 BERT 模型来进行掩码语言建模,如下所示:
from transformers import BertForMaskedLM, BertConfig
model = BertForMaskedLM(BertConfig()) ❶
❶ 不要初始化为预训练的;创建一个全新的。
否则,步骤与上一小节相同,我们不在此处重复代码。重复相同的步骤在一个时代后大约 1.5 小时产生大约 2.8 的损失,并在两个时代后的大约 3 小时产生 2.5 的损失。这显然不如前一小节的 0.77 损失值好,证实了在那种情况下转移学习的功效。请注意,这次实验每个时代的时间较短,因为我们构建的分词器完全专注于 Twi,因此其词汇量比 104 种语言的预训练 mBERT 词汇表小。
去吧,改变未来!
摘要
-
transformer 架构使用自注意力机制来构建文本的双向上下文以理解文本。这使得它最近在 NLP 中成为主要的语言模型。
-
transformer 允许序列中的标记独立于彼此进行处理。这比按顺序处理标记的 bi-LSTM 实现了更高的可并行性。
-
transformer 是翻译应用的一个不错选择。
-
BERT 是一种基于 transformer 的架构,对于其他任务,如分类,是一个不错的选择。
-
BERT 可以同时在多种语言上进行训练,生成多语言模型 mBERT。该模型捕获的知识可转移到原本未包含在训练中的语言。
-
A. Vaswani 等人,“注意力就是一切”,NeurIPS (2017)。
-
A. Radford 等人,“通过生成式预训练改善语言理解”,arXiv (2018)。
-
M. E. Peters et al., “BERT: Pre-Training of Deep Bidirectional Transformers for Language Understanding,” Proc. of NAACL-HLT (2019): 4171-86.
-
www.weforum.org/agenda/2020/07/口罩命令和其他封锁政策减少了在美国的 COVID-19 传播
. -
P. Rajpurkar et al., “SQuAD: 100,000+ Questions for Machine Comprehension of Text,” arXiv (2016).
-
[
www.kaggle.com/azunre/jw300entw
](https://www.kaggle.com/azunre/jw300entw括
-
实施判别微调和逐步解冻等策略。
-
在教师和学生BERT 模型之间执行知识蒸馏
在本章和下一章中,我们将介绍迄今为止已涵盖的深度NLP迁移学习建模架构的一些适应策略。换句话说,鉴于预训练架构如 ELMo、BERT 或 GPT,我们如何更有效地进行迁移学习?我们可以在这里采取几种效率措施。我们选择关注参数效率,即目标是在减少性能的同时产生尽可能少的参数模型。这样做的目的是使模型更小、更容易存储,从而更容易在智能手机设备上部署。另外,智能适应策略可能需要在某些困难的迁移情况下达到可接受的性能水平。
在第六章中,我们描述了 ULMFiT¹方法,即通用语言模型微调。该方法引入了判别微调和逐步解冻的概念。简而言之,逐步解冻逐渐增加网络中解冻或微调的子层的数量。另一方面,判别微调为网络中的每一层指定了可变的学习率,从而实现更有效的迁移。我们在第六章的代码中没有实施这些方法,因为作为适应策略,我们认为它们最适合放在本章中。在本章中,我们使用 ULMFiT 作者编写的fast.ai库来演示预训练的循环神经网络(RNN)语言模型的概念。
一些模型压缩方法通常被应用于大型神经网络以减小其大小。一些著名的方法包括权重修剪和量化。在这里,我们将重点关注适应策略,即NLP领域最近备受关注的知识蒸馏。该过程本质上试图使用显著较小的学生模型模拟来自较大的教师模型的输出。特别是,我们使用变压器库中的 DistilBERT²方法的实现来演示通过这种方法可以将 BERT 的大小减半以上。
让我们从下一节开始 ULMFiT。
9.1 逐步解冻和判别微调
在本节中,我们将在代码中实现 ULMFiT 方法,将语言模型适应于新的数据领域和任务。我们首先在第六章的最后讨论了这种方法的概念,因为从历史上看,它首先是在递归神经网络(RNNs)的背景下引入的。然而,我们将实际的编码练习推迟到现在,以强调在其核心,ULMFiT 是一组与架构无关的适应技术。这意味着它们也可以应用于基于 transformer 的模型。然而,为了与源材料保持一致,我们在 RNN-based 语言模型的背景下进行练习编码。我们将编码练习集中在我们在第六章中看到的假新闻检测示例上。
作为提醒,辨别微调指定网络中每一层的可变学习率。此外,学习率在学习过程中不是恒定的。相反,它们是倾斜三角形的——在开始时线性增加到一定程度,然后线性衰减。换句话说,这意味着快速增加学习率,直到达到最大速率,然后以较慢的速度减小。这个概念在图 6.8 中有所说明,我们在这里为了您的方便重复了它。
图 6.8(从第六章复制)建议的倾斜三角形 ULMFiT 学习率时间表,对于总迭代次数为 10,000 的情况。学习率在总迭代次数的 10%(即 1,000)上线性增加,最高达 0.01,然后线性减少到 0。
请注意,图中标有“最大学习率”的点在我们的情况下会有所不同(不是 0.01)。迭代的总数也将与图中显示的 10,000 次不同。这个时间表会产生更有效的转移和更具一般性的模型。
渐进解冻,另一方面,逐渐增加网络的子层的数量解冻,这样可以减少过拟合,同时也会产生更有效的转移和更具一般性的模型。所有这些技术在第六章的最后一节中都有详细讨论,可能在着手本节其余部分之前,简要回顾该讨论会有益处。
我们将在这里使用第 5.2 节的说明性示例——事实核查示例。回想一下,这个数据集包含超过 40,000 篇文章,分为两类:“假”和“真”。真实文章是从 reuters.com,一个声誉良好的新闻网站收集来的。另一方面,假文章则是从 PolitiFact 标记为不可靠的各种来源收集来的。在第 6.2 节,我们在预训练的 ELMo 模型导出的特征向量上训练了一个二元分类器。这个分类器预测一篇给定的文章是真实的(1)还是假的(0)。使用由每个类别的 1,000 篇文章组成的数据集,获得了 98%+ 的准确率。在这里,我们将看看是否可以通过 ULMFiT 方法取得更好的效果。
在本节中,我们将该方法分为两个小节。第一个小节涉及在目标任务数据上微调预训练语言模型的第一阶段 ULMFiT。斜三角形学习率在这里发挥作用,以及分层微调的概念。一些数据预处理和模型架构讨论也自然地融入到这个第一个小节中。第二个小节涵盖了第二阶段,涉及在目标任务数据上微调目标任务分类器——它位于微调语言模型之上——的阶段。逐步解冻程序的有效性由此得到证明。
请注意,本节中呈现的代码采用 fast.ai 版本 1 语法编写。选择这样做的原因是该库的第 2 版更改了输入数据的处理方式,提供了将其分割为训练集和验证集的内部函数,而不是允许您自己指定。为了与我们在前几章中的工作保持一致,在那里我们自己分割了数据,我们在这里坚持使用版本 1。我们还在 Kaggle 笔记本中提供了等效的 fast.ai 版本 2 语法代码³,您应该运行并与此处呈现的版本 1 代码进行比较。最后,请注意,版本 1 的文档托管在 fastai1.fast.ai/
,而版本 2 的文档托管在 docs.fast.ai/
。
9.1.1 预训练语言模型微调
第 5.2 节已经描述了我们需要对事实核查示例数据集进行的初始数据预处理步骤。特别地,我们对文章文本数据进行了洗牌,并将其加载到 NumPy 数组train_x
和test_x
中。我们还构建了相应的标签 NumPy 数组train_y
和test_y
,其中包含每篇文章是否为真实的信息,当文章为真时标记为 1,否则为 0。如同第 5.2 节一样,保持 1,000 个样本和测试/验证比例为 30%,得到的训练数组——train_x
,train_y
——长度为 1,400,测试数组——test_x
,test_y
——长度为 600。
我们需要做的第一件事是准备 fast.ai 库所期望的数据形式。其中一种数据格式是一个两列的 Pandas DataFrame,第一列包含标签,第二列包含数据。我们可以相应地构建训练和测试/验证数据框,如下所示:
train_df = pd.DataFrame(data=[train_y,train_x]).T
test_df = pd.DataFrame(data=[test_y,test_x]).T
这些数据框应该分别有 1,400 行和 600 行,每个都对应于相应数据样本中的每篇文章,并且在继续之前,最好用通常的.shape
命令检查一下,如下所示:
train_df.shape
test_df.shape
预期输出分别为(1400, 2)
和(600, 2)
。
fast.ai 中的数据使用TextLMDataBunch
类进行消耗,这些实例可以使用我们刚刚准备的 DataFrame 格式构建,使用以下命令:
data_lm = TextLMDataBunch.from_df(train_df = train_df, valid_df = test_df, path = "")
另一方面,fast.ai 中的数据由一个特定于任务的分类器使用TextClasDataBunch
类进行消耗。我们构建此类的一个实例,准备进入下一小节,使用以下类似的命令从我们的数据框中:
data_clas = TextClasDataBunch.from_df(path = "", train_df = train_df, valid_df = test_df, vocab=data_lm.train_ds.vocab)
现在我们准备在目标数据上微调我们的语言模型!为此,我们需要使用以下命令创建language_model_learner
fast.ai 类的一个实例:
learn = language_model_learner(data_lm, AWD_LSTM, drop_mult=0.3) ❶
❶ 使用 30%的权重丢失率初始化预训练的权重丢失 LSTM。这是在 WikiText-103 基准数据集上预训练的。
这里,AWD_LSTM
代表ASGD 权重丢失 LSTM。⁴这只是通常的 LSTM 架构,其中一些权重已被随机丢弃,就像通常的 dropout 层对神经网络激活所做的那样,与权重相反。这是最类似于 fast.ai 库中原始 ULMFiT 论文中所做的架构选择。此外,如果您检查上一个命令的执行日志,您应该能够确认它还从在 WikiText-103 基准数据集上训练的检查点加载预训练权重。⁶这个数据集,官方称为“WikiText 长期依赖语言建模数据集”,是一组由人类判断为“好”的维基百科文章。这是一个很好的、干净的无监督数据来源,已被许多自然语言处理论文用于基准测试。
现在我们已经加载了一个模型实例和一些预训练权重,我们将尝试确定用于微调语言模型的最佳或最优学习率。fast.ai 中一个称为lr_find
的巧妙实用方法可以自动为我们完成这项工作。它会迭代一系列学习率,并检测结果损失与学习率曲线上损失函数下降最快的点。等价地,这是损失梯度最小的地方。⁷我们可以使用我们的语言模型学习器learn
快速进行如下操作:
learn.lr_find() ❶
learn.recorder.plot(suggestion=True) ❷
❶ 寻找最佳/最优学习率
❷ 绘制它
结果的损失与学习率曲线,突出显示了最佳率,如图 9.1 所示。
图 9.1 fast.ai 库用于语言模型微调步骤中的最佳学习率找寻过程的结果,用于虚假新闻检测示例。通过多次迭代不同的学习率,并选择在曲线上损失下降最快的点作为最佳学习率。
我们可以通过以下命令程序化地检索这个学习率,并显示它:
rate = learn.recorder.min_grad_lr ❶
print(rate) ❷
❶ 检索最佳率
❷ 显示它
在我们执行代码时,返回的最佳学习率约为 4.0e-2。
找到最佳学习率后,我们现在可以使用下面的命令对我们的预训练的权重丢弃 LSTM 模型进行微调,使用 fit_one_cycle
fast.ai 命令如下所示:
learn.fit_one_cycle(1, rate) ❶
❶ 这个命令在底层使用了斜三角形学习率。它以 epochs 的数量和期望的最大学习率作为输入。
执行命令,在单个 Kaggle GPU 上进行大约 26 秒的微调,得到了 0.334 的准确度。
获得了基准值后,我们想要找出是否差异化微调能够带来改善。我们首先通过使用 unfreeze
命令解冻所有层,然后使用 slice
方法指定学习率范围的上限和下限。这个命令将最接近输出的层的最大学习率设置为上限,并通过除以一个常数因子几何地减少每个后续层的最大学习率到下限。下面展示了执行这个操作的确切代码:
learn.unfreeze() ❶
learn.fit_one_cycle(1, slice(rate/100,rate)) ❷
❶ 确保所有层都解冻以进行微调
❷ 在最终层中的最佳率和比该最佳率小两个数量级的值之间以几何方式变化
从代码可以看出,我们任意选择了将学习率从最大最优值变化到比该值小两个数量级的值。这个调度背后的直觉是,随后的层包含的信息更为一般化,与任务无关,因此它应该比最接近输出的层从这个特定目标数据集中学到的更少。
执行所提出的差异化微调代码,得到了一个准确度分数为 0.353,明显优于我们在没有使用它时得到的 0.334 的值。使用以下命令保存微调后的语言模型以供以后使用:
learn.save_encoder('fine-tuned_language_model')
通过斜三角形学习率和差异化微调调整了我们的预训练语言模型后,让我们看看我们能得到多好的目标任务分类器——也就是虚假新闻检测器。我们在下一小节对微调后的语言模型之上微调一个分类器。
9.1.2 目标任务分类器微调
请回想在前一小节中,我们创建了一个用于目标任务分类器的数据消费对象。我们将这个变量称为data_clas
。作为微调我们的目标任务分类器的下一步,我们需要实例化一个分类器学习器的实例,方法恰当地命名为text_classifier_learner
,在 fast.ai 中。下面的代码完成了这一步:
learn = text_classifier_learner(data_clas, AWD_LSTM, drop_mult=0.3) ❶
learn.load_encoder('fine-tuned_language_model') ❷
❶ 实例化目标任务分类器学习的一个实例。使用我们微调过的语言模型相同的设置,因此我们可以无问题地加载。
❷ 载入我们微调过的语言模型
作为下一步,我们再次使用实用的 fast.ai 方法lr_find
来找到最佳学习率,使用以下代码:
learn.lr_find() ❶
learn.recorder.plot(suggestion=True) ❷
❶ 寻找最佳速率
❷ 绘制它
执行该代码得到的是图 9.2 中显示的损失与学习率曲线。
图 9.2 从 fast.ai 库获取目标任务分类器微调步骤中用于找到最佳学习率的结果的过程。通过几个学习率进行迭代,并选择最佳学习率,即在曲线上损失下降最快的点。
我们看到最佳速率约为 7e-4。我们使用倾斜三角形学习率,通过以下代码对分类器学习器进行一轮训练:
rate = learn.recorder.min_grad_lr ❶
learn.fit_one_cycle(1, rate) ❷
❶ 提取最佳的最大学习率
❷ 使用确定的最大学习率在倾斜三角形学习率计划中微调目标任务分类器
执行该代码得到的准确率约为 99.5%。这已经比我们在第六章(第 6.2 节)通过在 ELMo 嵌入之上训练分类器得到的 98%+的结果更好了。我们还能做些什么来进一步提高它呢?
幸运的是,我们还有一个底牌:渐进式解冻。再次提醒,这是当我们仅解冻一层,微调它,解冻一个额外的较低层,微调它,并重复此过程一定次数时。ULMFiT 的作者发现,在目标任务分类器阶段应用此方法显着改善了结果。举个简单的例子,要执行此过程直到 2 层深度,我们需要以下代码:
depth = 2 ❶
for i in range(1,depth+1): ❷
learn.freeze_to(-i) ❸
learn.fit_one_cycle(1, rate) ❹
❶ 我们仅执行渐进式解冻,直到解冻两个层为止。
❷ 逐渐解冻更多层,首先一个,然后两个,每次使用倾斜三角形学习率进行一轮训练
❸ 此命令解冻了顶部 i 层。
❹ 执行一次倾斜三角形学习率,如已经介绍的
请注意,命令 learn.freeze_to``(-i)
冻结前 i
层对于本次操作至关重要。在我们对虚假新闻检测示例上执行代码时,我们发现在第一步中准确性达到了 99.8%,当解冻了前两层时,准确性达到了惊人的 100%。这些结果充分说明了自己,似乎表明 ULMFiT 方法是一套非常有用的技术。请注意,如果有必要,我们可以继续解冻更深层次的层次——第 3 层,第 4 层等等。
奇妙的事情!看来在我们适应新场景时,聪明地调整模型可以带来显著的好处!在接下来的章节中,我们将介绍另一种实现这一点的方法——知识蒸馏。
9.2 知识蒸馏
知识蒸馏是一种神经网络压缩方法,旨在教授一个较小的学生模型大型教师模型所包含的知识。这种方法近年来在 NLP 社区中变得流行,本质上是试图通过学生来模仿教师的输出。此方法也与模型无关——教师和学生可以是基于变压器的、基于循环神经网络的或其他结构,并且彼此之间可以完全不同。
在 NLP 领域中,对此方法的最初应用是由于对双向 LSTM(bi-LSTMs)的表示能力与基于变压器的架构之间的比较的疑问。⁸ 作者想要知道单个 bi-LSTM 层是否能够捕捉到 BERT 的多少信息。令人惊讶的是,研究人员发现,在某些情况下,预训练的基于变压器的语言模型的参数数量可以减少 100 倍,推理时间可以减少 15 倍,同时不损失标准性能指标。这是一个巨大的尺寸和时间上的减少,可以决定这些方法是否可以实际部署!知识蒸馏的过程在图 9.3 中简要概述。
图 9.3 是知识蒸馏的一般过程的示意图。教师模型产生的“软”标签被用于通过蒸馏损失鼓励学生模型表现出类似的行为。同时,学生损失被训练成与通过学生损失的标准地面真实情况行为类似。
如图所示,传统上,教师产生的标签被用于计算“软”标签,通过与学生的输出进行比较来确定蒸馏损失。这种损失促使学生模型跟踪教师模型的输出。此外,学生还通过学生损失同时学习“硬”的真实标签。我们将通过 Hugging Face 的 transformers 库来快速展示如何使用这个想法实现。
已经提出了几种架构来减小预训练的 NLP 语言模型的尺寸,包括 TinyBERT ⁹ 和 DistilBERT. ¹⁰ 我们选择专注于 DistilBERT,因为它在 transformers 库中已经准备就绪。 DistilBERT 是由 Hugging Face 开发的,这是与编写 transformers 库相同的团队。 与以前一样,我们对这个主题的覆盖并不意味着是全面的,而是举例说明。 在像这样快速发展的领域中保持进一步开发和文献的更新仍然很重要。 我们希望这里所呈现的内容能让您做到这一点。
DistilBERT 研究的目标是特别生成 BERT 模型的较小版本。 学生架构被选择为与 BERT 相同-在第 7 和第八章中描述的堆叠变压器编码器。 学生的层数减少了一半,只有六层的模型。 这是大部分尺寸节省的地方。 作者发现在这种框架中,内部隐藏维度的变化对效率几乎没有影响,因此,在教师和学生之间都是相似的。 过程的一个重要部分是将学生初始化到适当的一组权重,从中收敛会相对较快。 因为教师和学生的所有层的尺寸都是相似的,作者可以简单地使用对应层中的预训练教师权重来初始化学生,并发现这样做效果良好。
作者对 GLUE 等基准进行了广泛的实验证明,我们将在下一章中看到,并在 SQuAD 上进行了实验证明。 他们发现,由结果产生的 DistilBERT 模型在 GLUE 基准上的性能保持了 BERT 教师模型的 97% ,但参数个数只有教师的 40%. 它在 CPU 上的推理时间也快了 60%,而在 iPhone 等移动设备上快了 71%。 如您所见,这是一项明显的改进。
执行实际蒸馏的脚本可在官方 transformers 存储库中找到。¹¹ 要训练自己的 DistilBERT 模型,你需要创建一个每行一个文本样本的文件,并执行该页面提供的一系列命令,这些命令准备数据并蒸馏模型。因为作者已经提供了各种检查点可供直接加载——所有检查点都列在了该页面上——而我们的重点是迁移学习,我们在这里不重复从头开始训练的步骤。相反,我们使用了一个类似于我们在第八章中用于跨语言迁移学习实验的 mBERT 检查点。这样可以直接比较使用蒸馏架构与原始 mBERT 的性能和好处,同时还教会你如何开始在自己的项目中使用这个架构。这也为你提供了另一个机会,即在自定义语料库上微调预训练的基于 transformer 的模型——直接修改具有不同架构、预训练检查点和自定义数据集的代码应该适用于你自己的用例。
更具体地说,我们将重复我们在第 8.2.2 节中进行的实验,即通过在来自 JW300 数据集的语料库上进行微调,将 mBERT 中包含的知识转移到单语 Twi 场景中。我们执行使用检查点中包含的预训练 tokenizer 的实验变体,而不是从头开始训练一个新的,为了简单起见。
9.2.1 使用预训练 tokenizer 将 DistilmBERT 转移到单语 Twi 数据
在本小节中,我们的目标是从一个在超过 100 种语言上训练过的模型中生成一个用于加纳语 Twi 的 DistilBERT 模型,不包括 Twi 在内。BERT 的多语言等效版本称为 mBERT;因此,DistilBERT 的多语言等效版本可预见地称为 DistilmBERT。这个 DistilmBERT 模型直接类比于我们在第八章中实验过的 mBERT 模型。我们当时发现,即使 Twi 没有包含在原始训练中,从这个检查点开始是有益的。在这里,我们基本上会复制相同的步骤序列,将每个 mBERT 实例替换为 DistilmBERT。这样可以直接比较两者,并因此直观地了解知识蒸馏的好处,同时学习如何在自己的项目中使用 DistilBERT。与之前一样,我们会在 JW300 数据集的单语 Twi 子集上对模型进行微调。¹²
我们首先初始化一个 DistilBERT tokenizer,使用 DistilmBERT 模型的预训练检查点。这次我们使用 cased 版本,如下所示:
from transformers import DistilBertTokenizerFast ❶
tokenizer = DistilBertTokenizerFast.from_pretrained("distilbert-base-multilingual-cased") ❷
❶ 这只是 DistilBertTokenizer 的一个更快的版本,你可以用它来代替。
❷ 使用预训练的 DistilmBERT tokenizer
准备好 tokenizer 后,将 DistilmBERT 检查点加载到 DistilBERT 掩码语言模型中,并按照以下方式显示参数的数量:
from transformers import DistilBertForMaskedLM ❶
model = DistilBertForMaskedLM.from_pretrained("distilbert-base-multilingual-cased") ❷
print("Number of parameters in DistilmBERT model:")
print(model.num_parameters())
❶ 使用掩码语言建模
❷ 初始化为 mBERT 检查点
输出表明,与我们在第八章中发现的 BERT 模型的 178.6 百万个参数相比,该模型具有 1.355 亿个参数。 因此,DistilBERT 模型的大小仅为等效 BERT 模型的 76%。
接下来,使用 transformers 中方便的 LineByLineTextDataset
方法从单语 Twi 文本构建数据集,具体方法如下所示:
from transformers import LineByLineTextDataset
dataset = LineByLineTextDataset(
tokenizer=tokenizer,
file_path="../input/jw300entw/jw300.en-tw.tw", ❶
block_size=128) ❷
❶ 我们在第 8.2.1 节中介绍的英语到 Twi JW300 数据集
❷ 一次读取多少行
随后,按照下面的代码片段中所示的方式定义“数据集整理器”——这是一个帮助程序,它将一批样本数据行(长度为 block_size
)创建成一个特殊对象,这个特殊对象可以被 PyTorch 用于神经网络训练:
from transformers import DataCollatorForLanguageModeling
data_collator = DataCollatorForLanguageModeling(
tokenizer=tokenizer,
mlm=True, mlm_probability=0.15) ❶
❶ 使用掩码语言建模,并掩码单词的概率为 0.15
在这里,我们使用了掩码语言建模的方法——将我们输入数据中的 15% 的单词随机掩码,要求模型在训练过程中进行预测。
接下来,按照以下方式定义标准的训练参数,例如输出目录(我们选择为 twidistilmbert
)和训练批次大小:
from transformers import TrainingArguments
training_args = TrainingArguments(
output_dir="twidistilmbert",
overwrite_output_dir=True,
num_train_epochs=1,
per_gpu_train_batch_size=16,
save_total_limit=1,
)
然后,使用已定义的数据集和数据整理器定义“训练器”,并在数据上进行一个训练时代,具体方法如下。请记住,Twi 数据包含超过 600,000 行,因此在所有数据上进行一遍训练是相当费力的!
trainer = Trainer(
model=model,
args=training_args,
data_collator=data_collator,
train_dataset=dataset,
prediction_loss_only=True)
最后,按照以下方式进行训练并计算训练所需的时间:
import time
start = time.time()
trainer.train()
end = time.time()
print("Number of seconds for training:")
print((end-start))
一如既往地,一定要保存模型:
trainer.save_model("twidistilmbert")
我们发现,与第八章中等效教师完成每个时代所需的 3 小时相比,该模型花费了大约 2 小时和 15 分钟完成该时代。 因此,学生的训练时间只有老师的 75%。 显著提高!
此外,损失函数的值达到了约 0.81,而 mBERT 的等效模型在第八章中的损失为约 0.77。就绝对值而言,性能差异可以粗略地量化为大约 5%——我们看到 DistilBERT 达到了 BERT 性能的 95%。 这非常接近 DistilBERT 作者在论文中报告的基准数字 97%。
最后一步,从语料库中取出以下句子:“Eyi de ɔhaw kɛse baa sukuu h*ɔ。” 掩盖一个单词,sukuu(在 Twi 中表示“学校”),然后将管道 API 应用于以下预测所删除的单词:
from transformers import pipeline
fill_mask = pipeline( ❶
"fill-mask",
model="twidistilmbert",
tokenizer=tokenizer)
print(fill_mask("Eyi de ɔhaw kɛse baa [MASK] hɔ.")) ❷
❶ 定义了填空管道
❷ 预测掩码标记
这会产生以下输出:
[{'sequence': '[CLS] Eyi de ɔhaw kɛse baa fie hɔ. [SEP]', 'score': 0.31311026215553284, 'token': 29959}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa me hɔ. [SEP]', 'score': 0.09322386980056763, 'token': 10911}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa ne hɔ. [SEP]', 'score': 0.05879712104797363, 'token': 10554}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa too hɔ. [SEP]', 'score': 0.052420321851968765, 'token': 16683}, {'sequence': '[CLS] Eyi de ɔhaw kɛse baa no hɔ. [SEP]', 'score': 0.04025224596261978, 'token': 10192}]
这确实是可信的完成。值得注意的是,我们在第 8.2.2 节中看到的结果中的宗教偏见似乎已经在模型中得到了缓解。像“以色列”和“伊甸园”这样的完成,在第 8.2.2 节的 mBERT 等价模型中建议,现在已经不再存在了。这可以通过两者之间参数数量的显著差异来解释。由于这一点,DistilBERT 不太可能过拟合,而 BERT 则更有可能这样做。
现在你知道如何在自己的项目中使用 DistilBERT 了!我们再次强调,你刚刚进行的练习教会了你如何在自定义语料库上微调预训练的基于 transformer 的模型——只需修改代码以应用于你自己的用例,包括不同的架构、预训练检查点和自定义数据集。
在下一章的第一节中,我们将有机会在英语中再次微调一个基于 transformer 的模型,这次是在自定义语料库上进行!我们将讨论 ALBERT 架构背后的适应性思想——一种轻量级的 BERT——并将其微调到来自 Multi-Domain Sentiment Dataset 的一些评论中。回想一下,在第四章我们玩过这个数据集。这是亚马逊 25 个产品类别的评论数据集,我们将重点关注书评,就像第四章一样。
摘要
-
ULMFiT 的策略,如倾斜三角形学习率、差异微调和逐步解冻,可以导致明显更有效的迁移。
-
执行知识蒸馏一个较大的教师BERT 模型会产生一个明显更小的学生BERT 模型,性能损失最小。
-
V. Sanh 等人,“DistilBERT,BERT 的精简版本:更小、更快、更便宜、更轻”,EMC²:与 NeurIPS 合办的第 5 版(2019 年)。
-
www.kaggle.com/azunre/tlfornlp-chapter9-ulmfit-adaptation-fast-aiv2
-
S. Merity 等人,“正则化和优化 LSTM 语言模型”,ICLR(2018 年)。
-
L. Smith 等人,“神经网络超参数的一种纪律方法:第一部分——学习率、批大小、动量和权重衰减”,arXiv(2018 年)。
-
R. Tang 等人,“从 BERT 中提炼任务特定知识到简单神经网络”,arXiv(2018 年)。
-
X. Jiao 等人,“TinyBERT:BERT 的精炼”,arXiv(2020 年)。
-
V. Sanh 等人,“DistilBERT,BERT 的精简版本:更小、更快、更便宜、更轻”,EMC²:与 NeurIPS 合办的第 5 版(2019 年)。
-
github.com/huggingface/transformers/blob/master/examples/research_projects/distillation
第十章:ALBERT,适配器和多任务适配策略
本章介绍
-
对嵌入因子分解和层间参数共享进行应用
-
在多个任务上对 BERT 系列模型进行微调
-
将迁移学习实验分成多个步骤
-
对 BERT 系列模型应用适配器
在上一章中,我们开始介绍了到目前为止我们所涵盖的深度 NLP 迁移学习建模架构的一些适配策略。换句话说,给定一个预训练的架构,如 ELMo、BERT 或 GPT,如何更有效地进行迁移学习?我们涵盖了 ULMFiT 方法背后的两个关键思想,即区分性微调和逐渐解冻的概念。
我们在本章中将要讨论的第一个适配策略围绕着两个目标展开,旨在创建更有利于具有更大词汇量和更长输入长度的基于 transformer 的语言模型。第一个想法实质上涉及巧妙的因子分解,或者将更大的权重矩阵分解为两个较小的矩阵,使您可以增加一个的维度而不影响另一个的维度。第二个想法涉及在所有层之间共享参数。这两个策略是 ALBERT 方法的基础,即 A Lite BERT。我们使用 transformers 库中的实现来获得这种方法的一些实际经验。
在第四章中,我们介绍了多任务学习的概念,即模型被训练为同时执行多种任务。由此产生的模型通常对新场景更具泛化能力,并且可能导致更好的迁移效果。毫不奇怪,这个想法在预训练的 NLP 语言模型的适配策略的背景下再次出现。当面临转移场景时,没有足够的训练数据来微调给定任务时,为什么不在多个任务上进行微调呢?讨论这个想法为介绍(GLUE)数据集提供了一个很好的机会:一个包含了几个代表人类语言推理任务的数据集。这些任务包括检测句子之间的相似性、问题之间的相似性、释义、情感分析和问答。我们展示了如何利用 transformers 库快速进行多任务微调使用这个数据集。这个练习还演示了如何在一个来自这些重要问题类别的自定义数据集上类似地微调 BERT 系列模型。
在第四章中,我们还讨论了领域自适应,在那里我们发现源域和目标域的相似性对于迁移学习的有效性起着至关重要的作用。更大的相似性通常意味着更容易的迁移学习过程。当源和目标过于不相似时,你可能会发现在一个步骤中执行该过程是不可能的。在这种情况下,可以使用“顺序适应”的概念将整体所需的转移分解成更简单、更易管理的步骤。例如,一个语言工具在西非和东非之间无法转移,但可以先在西非和中非之间成功转移,然后在中非和东非之间转移成功。在本章中,我们将“填空”目标预训练 BERT 顺序适应到一个低资源句子相似度检测场景中,首先适应到一个数据丰富的问题相似度场景。
我们将探讨的最终适应策略是使用所谓的适应模块或适配器。这些是预训练神经网络层之间只有少量参数的新引入模块。对于新任务微调这个修改后的模型只需要训练这几个额外的参数。原始网络的权重保持不变。通常情况下,当每个任务只增加 3-4% 的额外参数时,与微调整个模型相比,性能几乎没有损失。这些适配器也是模块化的,并且很容易在研究人员之间共享。
10.1 嵌入因子分解和跨层参数共享
我们在本节讨论的适应策略围绕着两个想法,旨在创建具有更大词汇表和更长最大输入长度的基于 transformer 的语言模型。第一个想法基本上涉及将一个更大的权重矩阵巧妙地分解为两个较小的矩阵,使得其中一个可以在不影响另一个维度的情况下增加维度。第二个想法涉及在所有层之间共享参数。这两种策略是 ALBERT 方法的基础。我们再次使用 transformers 库中的实现来获取一些与该方法有关的实际经验。这既可以让你对所获得的改进有所了解,也可以让你有能力在自己的项目中使用它。我们将使用第四章中的 Multi-Domain Sentiment Dataset 中的亚马逊图书评论作为我们这次实验的自定义语料库。这将使您能够进一步体验在自定义语料库上微调预训练的基于 transformer 的语言模型,这次是用英语!
第一个策略,即嵌入因子分解,受到了观察的启发,即在 BERT 中,输入嵌入的大小与其隐藏层的维度密切相关。分词器为每个标记创建一个 one-hot 编码的向量——该向量在与标记对应的维度上等于 1,在其他维度上等于 0。这个 one-hot 编码向量的维度等于词汇表的大小,V。输入嵌入可以被看作是一个维度为V乘以E的矩阵,将 one-hot 编码的向量乘以它并投影到大小为E的维度中。在早期的模型(如 BERT)中,这等于隐藏层的维度H,因此这个投影直接发生在隐藏层中。
这意味着当隐藏层的大小增加时,输入嵌入的维度也必须增加,这可能非常低效。另一方面,ALBERT 的作者观察到,输入嵌入的作用是学习上下文无关的表示,而隐藏层的作用是学习上下文相关的表示——这是一个更难的问题。受此启发,他们提出将单一输入嵌入矩阵分成两个矩阵:一个是V乘以E,另一个是E乘以H,允许H和E完全独立。换句话说,one-hot 编码的向量可以首先投影到较小尺寸的中间嵌入中,然后再馈送到隐藏层。即使隐藏层的尺寸很大或需要扩展,这也使得输入嵌入可以具有显着较小的尺寸。仅此设计决策就导致将投影 one-hot 嵌入向量到隐藏层的矩阵/矩阵的尺寸减少了 80%。
第二个策略,即跨层参数共享,与我们在第四章中讨论的软参数共享多任务学习场景相关。在学习过程中,通过对它们施加适当的约束,鼓励所有层之间的相应权重彼此相似。这起到了正则化的效果,通过减少可用自由度的数量来降低过拟合的风险。这两种技术的结合使得作者能够构建出在当时(2020 年 2 月)超越了 GLUE 和 SQuAD 记录性能的预训练语言模型。与 BERT 相比,在参数大小上实现了约 90%的减少,而性能只有轻微的下降(在 SQuAD 上不到 1%)。
再次,因为多种检查点可用于直接加载,我们不在此重复从头开始的训练步骤,因为我们的重点是迁移学习。相反,我们使用类似于我们在前一章和第八章中用于我们的跨语言迁移学习实验的“基础”BERT 检查点。这使我们能够直接比较使用这种架构与原始 BERT 的性能和效益,并教你如何开始在自己的项目中使用这种架构。
10.1.1 在 MDSD 书评上对预训练的 ALBERT 进行微调
我们准备数据的步骤与第 4.4 节中的步骤相同,我们在此不再重复。这些步骤也在本书附带的 Kaggle 笔记本中重复出现。我们从列表 4.6 生成的变量data
开始。假设与第 4.4 节相同的超参数设置,这是一个由 2,000 本书评文本组成的 NumPy 数组。
使用以下代码将这个 NumPy 数组写入 Pandas 到文件中:
import pandas as pd
train_df = pd.DataFrame(data=data)
train_df.to_csv("albert_dataset.csv")
我们首先初始化一个 Albert 分词器,使用基本 ALBERT 模型中的预训练检查点,如下所示。我们使用版本 2 是因为它是目前可用的最新版本。你可以在 Hugging Face 网站上随时找到所有可用的 ALBERT 模型列表。⁶
from transformers import AlbertTokenizer ❶
tokenizer = AlbertTokenizer.from_pretrained("albert-base-v2") ❷
❶ 加载 ALBERT 分词器
❷ 使用预训练的 ALBERT 分词器
准备好分词器后,将基础 ALBERT 检查点加载到 ALBERT 遮盖语言模型中,并显示参数数量如下:
from transformers import AlbertForMaskedLM ❶
model = AlbertForMaskedLM.from_pretrained("albert-base-v2") ❷
print("Number of parameters in ALBERT model:")
print(model.num_parameters())
❶ 使用遮盖语言建模
❷ 初始化到 ALBERT 检查点
输出表明模型有 1180 万个参数——与第八章的 BERT 的 178.6 万个参数和直接 BERT 的 135.5 万个参数相比,这是一个巨大的缩小。事实上,这是与 BERT 模型相比的 15 倍缩小。哇!
然后,像之前一样,使用 transformers 中提供的方便的LineByLineTextDataset
方法,使用单语 Twi 文本中的分词器构建数据集,如下所示:
from transformers import LineByLineTextDataset
dataset = LineByLineTextDataset(
tokenizer=tokenizer,
file_path="albert_dataset.csv",
block_size=128) ❶
❶ 每次读取多少行
定义一个“数据收集器”——一个帮助方法,将一个样本数据行批量(block_size
长度)创建成一个特殊对象——如下所示。这个特殊对象可以被 PyTorch 用于神经网络训练:
from transformers import DataCollatorForLanguageModeling
data_collator = DataCollatorForLanguageModeling(
tokenizer=tokenizer,
mlm=True, mlm_probability=0.15) ❶
❶ 使用遮盖语言建模,并用 0.15 的概率遮盖单词
在这里,我们使用了以 15%的概率对我们的输入数据进行随机遮盖的遮盖语言建模,并要求模型在训练过程中对它们进行预测。
定义标准的训练参数,如输出目录和训练批量大小,如下代码片段所示。注意,这一次我们训练 10 次,因为数据集比前一章中使用的超过 60,000 个单语 Twi 样本要小得多:
from transformers import Trainer, TrainingArguments
training_args = TrainingArguments(
output_dir="albert",
overwrite_output_dir=True,
num_train_epochs=10,
per_gpu_train_batch_size=16,
save_total_limit=1,
)
然后,使用之前定义的数据集和整理器来定义一个“训练器”,以跨数据进行一个训练 epoch,如下所示:
trainer = Trainer(
model=model,
args=training_args,
data_collator=data_collator,
train_dataset=dataset,
prediction_loss_only=True,
)
按照以下步骤训练并计时训练时间:
import time
start = time.time()
trainer.train()
end = time.time()
print("Number of seconds for training:")
print((end-start))
在这个小数据集上,10 个 epochs 大约只需约五分钟就能完成训练。损失值达到约 1。
按以下方式保存模型:
trainer.save_model("albert_fine-tuned")
最后,让我们按照以下步骤应用管道 API 来预测虚构书评中的遮蔽词:
from transformers import pipeline
fill_mask = pipeline( ❶
"fill-mask",
model="albert_fine-tuned",
tokenizer=tokenizer
)
print(fill_mask("The author fails to [MASK] the plot.")) ❷
❶ 定义填空管道
❷ 预测遮蔽的标记
这产生了以下非常合理的输出:
[{'sequence': '[CLS] the author fails to describe the plot.[SEP]', 'score': 0.07632581889629364, 'token': 4996}, {'sequence': '[CLS] the author fails to appreciate the plot.[SEP]', 'score': 0.03849967569112778, 'token': 8831}, {'sequence': '[CLS] the author fails to anticipate the plot.[SEP]', 'score': 0.03471902385354042, 'token': 27967}, {'sequence': '[CLS] the author fails to demonstrate the plot.[SEP]', 'score': 0.03338927403092384, 'token': 10847}, {'sequence': '[CLS] the author fails to identify the plot.[SEP]', 'score': 0.032832834869623184, 'token': 5808}]
到目前为止,您可能已经观察到,我们在此处对自定义书评语料库对 ALBERT 进行微调的步骤序列与我们在上一章中使用 DistilBERT 的步骤序列非常相似。这一系列步骤反过来又与我们在第八章中使用的 mBERT 的步骤序列非常相似。我们再次强调,这个配方可以用作 transformers 中几乎任何其他架构的蓝图。虽然我们无法提供在每种可能的应用类型上微调的示例,但这个配方应该可以推广,或者至少作为许多用例的良好起点。例如,考虑一种情况,您想要教 GPT-2 以某种选择的风格写作。只需复制我们在这里使用的相同代码,将数据集路径指向您选择的写作风格的语料库,并将标记器和模型引用从 AlbertTokenizer
/ AlbertForMaskedLM
更改为 GPT2Tokenizer
/ GPT2LMHeadModel
。
需要注意的一点是,所有 PyTorch transformers 模型默认情况下都会解冻所有层进行训练。要冻结所有层,您可以执行以下代码片段:
for param in model.albert.parameters():
param.requires_grad = False
您可以使用类似的代码片段仅冻结一些参数。
在下一节中,我们将讨论多任务微调,我们将有另一个机会来看看这些类型模型的微调,这次是针对各种任务。
10.2 多任务微调
在第四章的第三部分中,我们介绍了多任务学习的概念,其中模型被训练执行各种任务,而不仅仅是一个任务。结果模型通常对新场景更具一般性,并且可以实现更好的转移和性能。毫不奇怪,这个想法再次出现在预训练 NLP 语言模型的适应策略的背景下,微调在多个任务上的模型观察到更加健壮和有效。⁷
我们在这里讨论这个想法提供了一个很好的机会来介绍通用语言理解评估(GLUE)数据集,⁸这是一个包含几个人类语言推理代表性任务数据的集合。这个数据集包括检测句子相似性、问题相似性、释义、情感分析和问题回答等任务。在本节中,我们演示了如何快速利用 transformers 库对我们讨论的各种基于变压器的预训练模型在 GLUE 数据集的各种任务上进行微调。这个练习还演示了如何按类似方式微调来自 BERT 系列的模型,以解决 GLUE 中包含的重要问题类别之一的自定义数据集。
我们还演示了顺序适应——将总体所需的转移实验过程分解成更简单、更易管理的步骤的过程。考虑一个假设的情景,即基于语言的工具在西非和东非之间无法完成转移——首先它可能在西非和中非之间成功转移,然后在中非和东非之间成功转移。这与多任务微调的想法相关,因为它本质上是按顺序进行的,一步接一步进行。与通常理解的多任务微调方法不同,顺序适应首先在一个任务上进行微调,然后再在另一个任务上进行微调。
在本节中,我们通过在 GLUE 数据集的几个任务上对一些预训练的基于变压器的语言模型进行多任务微调和顺序适应来演示。具体来说,我们关注的是一个被称为Quora 问题对(QQP)任务的问题相似度任务,以及用于衡量一对句子之间相似性的语义文本相似性基准(SST-B)任务。
10.2.1 通用语言理解数据集(GLUE)
通用语言理解数据集(GLUE)旨在提供一系列多样的自然语言理解任务的具有挑战性的基准数据集。这些任务被选中,以代表多年来在自然语言处理领域研究人员之间达成的一种关于什么构成有趣、具有挑战性和相关问题的隐含共识。在表 10.1 中,我们总结了数据集中可用任务和每个任务的数据计数。
表 10.1 原始通用语言理解数据集(GLUE)中提供的任务、描述和数据计数列表
任务名称 | 数据量 | 描述 |
---|---|---|
语言可接受性语料库(CoLA) | 训练 8,500,测试 1,000 | 确定一个英语句子是否符合语法规范 |
斯坦福情感树库(SST2) | 训练 67,000,测试 1800 | 检测给定句子的情感-积极或消极 |
Microsoft Research Paraphrase Corpus (MRPC) | 3,700 train, 1,700 test | 确定一个句子是否是另一个句子的释义 |
Semantic Textual Similarity Benchmark (STS-B) | 7,000 train, 1,400 test | 预测一对句子之间的相似度分数,范围在 1 到 5 之间 |
Quora Question Pairs (QQP) | 3,640,000 train, 391,000 test | 确定一对 Quora 问题是否语义上等同 |
Multi-Genre Natural Language Inference (MultiNLI) | 393,000 train, 20,000 test | 确定一个前提句子是暗示/蕴含还是与一个假设句子相矛盾 |
Question-Answering Natural Language Inference (QNLI) | 105,000 train, 5,400 test | 检测上下文句子是否包含对问题的答案 |
Recognizing Textual Entailment (RTE) | 2,500 train, 3,000 test | 测量前提和假设之间的文本蕴含关系,类似于 MultiNLI |
Winograd Schema Challenge (WNLI) | 634 train, 146 test | 确定模棱两可的代词指的是一组可能选项中的哪一个名词 |
从表中可以看出,原始的 GLUE 数据集涵盖了各种任务,并且可用的数据量不同。这是为了鼓励不同任务之间的知识共享,这也是我们在本章节中探讨的多任务微调理念的核心。接下来我们简要描述表中的各项任务。
前两个任务——Corpus of Linguistic Acceptability (CoLA) 和 Stanford Sentiment Treebank (SST2)——是单句任务。前者试图确定一个给定的英文句子是否语法正确,而后者试图检测句子中表达的情感是积极还是消极。
以下三项任务——Microsoft Research Paraphrase Corpus (MRPC)、Semantic Textual Similarity Benchmark (STS-B) 和 Quora Question Pairs (QQP)——被归类为相似性任务。这些任务涉及以各种方式比较两个句子。MRPC 试图检测一个句子是否是另一个句子的释义,即是否表达了相同的概念。STS-B 在连续范围 1 到 5 之间测量一对句子的相似度。QQP 试图检测一个 Quora 问题是否等同于另一个。
剩下的四个任务被分类为推理任务。多体裁自然语言推理(MultiNLI)任务试图确定给定的句子是否暗示另一个句子或是否与之矛盾――它衡量蕴涵。问答自然语言推理(QNLI)任务类似于我们讨论并在第八章中用于说明问答的 SQuAD⁹数据集。提醒一下,该数据集由上下文段落、对其提出的问题以及答案在上下文段落中的开始和结束位置指示符组成,如果存在的话。QNLI 基本上将这个想法转化为一个句对任务,通过将每个上下文句子与问题配对,并尝试预测答案是否在该上下文句子中。识别文本蕴涵(RTE)任务类似于 MultiNLI,因为它衡量两个句子之间的蕴涵关系。最后,Winograd Schema Challenge(WNLI)数据集试图检测一个含糊指代词在句子中指代可用选项中的哪个名词。
自 GLUE 成立以来,还引入了另一个名为 SuperGLUE¹⁰的数据集。这个新版本是必要的,因为最近的现代方法在 GLUE 的许多部分几乎达到了完美的性能。SuperGLUE 的开发是为了更具挑战性,因此为比较方法提供更多的“动态范围”。我们在这里关注 GLUE,但我们认为在您成为 NLP 专家时,牢记 SuperGLUE 的存在是很重要的。
接下来,我们将以 QQP 和 STS-B GLUE 任务做一些实验,作为本节的说明性示例。首先,在下一小节中,我们展示如何对我们提出的任何任务中的一个任务进行微调预训练的 BERT。我们强调,虽然在这种情况下,我们使用 STS-B 作为示例微调任务,但对于任何呈现的任务,相同的步骤序列直接适用。我们还提醒您,此练习是为了准备您在自己的自定义数据集上对 BERT 进行微调,该数据集来自我们提出的任何任务类别。
10.2.2 在单个 GLUE 任务上进行微调
在本小节中,我们看到如何快速微调 transformers 家族中的预训练模型,以处理 GLUE 基准集中的任务。回想一下,BERT 是在“填空”和“下一个句子预测”目标上进行预训练的。在这里,我们进一步微调这个预训练的 BERT 来处理 GLUE 数据上的 STS-B 相似性任务。这个练习作为如何在 GLUE 的任何其他任务以及属于这些重要问题类别之一的任何自定义数据集上进行操作的示例。
我们要做的第一件事是克隆 transformers 存储库,并使用以下代码安装必要的要求:
!git clone --branch v3.0.1 https:/ /github.com/huggingface/transformers ❶
!cd transformers
!pip install -r transformers/examples/requirements.txt ❷
!pip install transformers==3.0.1 ❸
❶ 克隆(指定版本的)transformers 存储库
❷ 安装必要的要求
❸ 为了可重现性,固定 transformers 版本
请注意在我们的 Kaggle 笔记本中忽略依赖冲突消息——这些消息与我们在此处使用的库无关,只要您复制我们的笔记本而不是从头开始创建一个新的。
接下来,按以下方式下载 GLUE 数据:
!mkdir GLUE
!python transformers/utils/download_glue_data.py --data_dir GLUE --tasks all❶
❶ 下载所有任务的 GLUE 数据
这会创建一个名为 GLUE 的目录,其中包含一个子目录,该子目录以每个 GLUE 任务命名,并包含该任务的数据。我们可以按以下方式查看 GLUE/STS-B 中包含的内容:
!ls GLUE/STS-B
这产生了以下输出:
LICENSE.txt dev.tsv original readme.txt test.tsv train.tsv
此外,我们可以用以下方式查看一部分 STS-B 训练数据:
!head GLUE/STS-B/train.tsv
这产生以下输出:
index genre filename year old_index source1 source2 sentence1 sentence2 score
0 main-captions MSRvid 2012test 0001 none none A plane is taking off. An air plane -is taking off. 5.000
1 main-captions MSRvid 2012test 0004 none none A man is playing a large flute. A man is playing a flute. 3.800
2 main-captions MSRvid 2012test 0005 none none A man is spreading shreddedcheese on a pizza. A man is spreading shredded cheese on an uncooked pizza. 3.800
3 main-captions MSRvid 2012test 0006 none none Three men are playing chess. Two men are playing chess. 2.600
4 main-captions MSRvid 2012test 0009 none none A man is playing the cello.A man seated is playing the cello. 4.250
5 main-captions MSRvid 2012test 0011 none none Some men are fighting. Two men are fighting. 4.250
6 main-captions MSRvid 2012test 0012 none none A man is smoking. A man is skating. 0.500
7 main-captions MSRvid 2012test 0013 none none The man is playing the piano. The man is playing the guitar. 1.600
8 main-captions MSRvid 2012test 0014 none none A man is playing on a guitar and singing. A woman is playing an acoustic guitar and singing. 2.200
在继续之前,我们注意到,为了使用这里讨论的脚本来在您自己的自定义数据上对模型进行精细调优,您只需要将您的数据转换为所示格式并指定脚本所在的位置即可!
要在 STS-B GLUE 任务上对“vanilla” bert-base-cased
BERT checkpoint 进行三轮训练——批量大小为 32,最大输入序列长度为 256,学习率为 2e-5——我们执行以下命令:
%%time ❶
!python transformers/examples/text-classification/run_glue.py --model_name_or_path bert-base-cased --task_name STS-B --do_train --do_eval --data_dir GLUE/STS-B/ --max_seq_length 256 --per_gpu_train_batch_size 32 --learning_rate 2e-5 --num_train_epochs 3.0 --output_dir /tmp/STS-B/
❶ 这是 Jupyter 笔记本中计时的“魔法”命令。
这一操作执行时间不超过 10 分钟。请注意,在代码中,我们指定了输出目录为 /tmp/STS-B/。该文件夹包含了经过精细调优的模型和评估结果。然后,为了查看所取得的性能,我们只需执行以下命令将结果打印到屏幕上:
!cat /tmp/STS-B/eval_results_sts-b.txt
这产生以下输出:
eval_loss = 0.493795601730334
eval_pearson = 0.8897041761974835
eval_spearmanr = 0.8877572577691144
eval_corr = 0.888730716983299
这些代表了用于此问题的度量标准的最终数值,即皮尔逊相关系数和斯皮尔曼相关系数。不深入细节,这些系数衡量了数据集中提供的真实相似度与我们在测试集上精细调优模型获得的相似度之间的相关性。这些系数的较高值表明了更好的模型,因为它们与真实结果的关联更大。我们看到对于这两个系数都达到了接近 89% 的性能。在撰写本文时(2020 年 10 月初),当前的 GLUE 排行榜¹¹显示,在全球范围内,排名前 20 的性能大约在 87% 到 93% 之间变化。这些排名靠前的性能也在 GLUE 的其他任务上表现良好,尽管我们目前只对一个任务进行了精细调优。但我们可以快速取得如此接近最新技术水平的性能仍然令人印象深刻。请注意从表 10.1 中得知,用于此任务的训练数据量仅为 7,000 个样本。
在下一小节中,我们将进一步在另一个任务——Quora 问题对(QQP)上对模型进行精细调优,并进一步阐明多任务学习和顺序适应的概念。
10.2.3 顺序适应
在本小节中,我们将看到在 STS-B 任务上进行微调之前,在 Quora 问答对(QQP)任务上进行微调是否会产生更好的性能。请回顾表 10.1,其中 QQP 有 364,000 个训练样本,而 STS-B 有 7,000 个样本。显然,QQP 具有更多的数据。首先在 QQP 上训练可以被解释为应用一种顺序适应多任务学习策略来处理一个低资源的场景,其中训练数据量不理想:只有 7,000 个样本。
我们开始本练习,假设已经克隆了 transformers 存储库,已安装了必要的要求,并已下载了 GLUE 数据,如前一小节所示。现在,要做的下一件事是在 QQP GLUE 任务上对“普通”的bert-base-cased
BERT 检查点进行微调,一次迭代,批处理大小为 32,最大输入序列长度为 256,学习率为 2e-5。请注意,这次我们只使用一个迭代,而不是前一小节中的三个,因为训练数据现在要大得多。现在每个迭代(涉及一次通过训练集)涵盖了 364,000 个样本,我们认为这已足够。我们使用以下代码:
!python transformers/examples/text-classification/run_glue.py --model_name_or_path bert-base-cased --task_name QQP --do_train --do_eval --data_dir GLUE/QQP/ --max_seq_length 256 --per_gpu_train_batch_size 32 --learning_rate 2e-5 --num_train_epochs 1 --output_dir /tmp/QQP/
训练时长约为 2 小时 40 分钟。与以前一样,我们可以检查 QQP 任务上的性能如下:
!cat /tmp/QQP/eval_results_qqp.txt
这达到了以下性能:
eval_loss = 0.24864352908579548
eval_acc = 0.8936433341578036
eval_f1 = 0.8581700639883898
eval_acc_and_f1 = 0.8759066990730967
epoch = 1.0
然后我们可以按以下方式加载 QQP 微调的模型:
from transformers import BertForSequenceClassification, BertConfig ❶
qqp_model = BertForSequenceClassification.from_pretrained("/tmp/QQP") ❷
❶ 初始化为我们的微调模型检查点
❷ 这次使用序列分类,因为这是问题的形式
在加载了微调模型之后,让我们提取其编码器,以便我们可以在后续模型中使用它,然后可以进一步在 STS-B 任务上进行微调。请注意,这类似于我们在第四章中分析的硬参数共享场景。我们在图 10.1 中说明了这种情况。
图 10.1 我们在本节探讨的硬参数共享多任务学习场景。模型首先在 QQP 上进行微调,这是一个数据丰富的场景,然后是 STS-B,这是一个资源稀缺的场景。这个实验的顺序性质将其分类为顺序适应。
图中清楚地显示了任务之间共享的编码器。编码器被提取并用于初始化一个模型,以进行在 STS-B 上的微调,代码片段如下:
shared_encoder = getattr(qqp_model, "bert") ❶
configuration = BertConfig()
configuration.vocab_size = qqp_model.config.vocab_size ❷
configuration.num_labels = 1 ❸
stsb_model = BertForSequenceClassification(configuration) ❹
setattr(stsb_model, "bert", shared_encoder) ❺
❶ 获取经过微调的 QQP 模型编码器
❷ 确保 STS-B 配置的词汇量和输出大小设置一致
❸ STS-B 是一个回归问题,只需要一个输出;QQP 是一个二元分类任务,因此有两个输出。
❹ 用与 QQP 类似的设置初始化 STS-B 模型
将初始化的 STS-B 模型保存以供进一步微调,方法如下:
stsb_model.save_pretrained("/tmp/STSB_pre")
确保从 QQP 模型中获取了词汇表,如下所示:
!cp /tmp/QQP/vocab.txt /tmp/STSB_pre
现在,使用与前一小节相同的设置,在 STS-B 上微调先前微调的 QQP 模型,操作如下:
!python transformers/examples/text-classification/run_glue.py --model_name_or_path /tmp/STSB_pre --task_name STS-B --do_train --do_eval --data_dir GLUE/STS-B/ --max_seq_length 256 --per_gpu_train_batch_size 32 --learning_rate 2e-5 --num_train_epochs 3 --output_dir /tmp/STS-B/
这三个训练时代只需大约七分半钟即可执行,只有 7,000 个训练集大小。像往常一样,我们使用以下内容来检查获得的性能:
!cat /tmp/STS-B/eval_results_sts-b.txt
观察到以下性能:
eval_loss = 0.49737201514158474
eval_pearson = 0.8931606380447263
eval_spearmanr = 0.8934618150816026
eval_corr = 0.8933112265631644
epoch = 3.0
我们已经取得了比之前的小节更好的表现,只是在 STS-B 上进行微调。那里的eval_corr
约为 88.9%,而我们在这里达到了 89.3%。因此,连续适应的多任务学习实验被证明具有益处,并导致性能的可衡量提高。
在下一节中,我们将探讨是否可以比我们在这里做得更加高效地将模型微调到新的情况。我们将研究在预训练语言模型的层之间引入称为适应模块或适配器的方法,以适应新情况。这种方法很有前途,因为引入的参数数量非常少,可以有效地预训练和分享 NLP 社区。
10.3 适配器
我们探索的下一个适应策略是使用所谓的适配模块或适配器。它们背后的关键思想如图 10.2 所示,介绍了它们作为第七章中图 7.6 中香草变压器编码器中的附加层。
图 10.2 在“香草”变压器编码器的图 7.6 中新引入的适配器层
如图所示,这些适配器是预训练神经网络层之间仅有几个参数的新引入模块。为了将修改后的模型微调到新任务上,只需要训练这些少量的额外参数——原始网络的权重保持不变。与微调整个模型相比,通常仅添加每个任务 3-4%的额外参数,几乎没有性能损失。¹² 实际上,这些额外参数相当于大约 1 兆字节的磁盘空间,这在现代标准下非常低。
这些适配器是模块化的,允许易于扩展和研究人员之间的经验共享。实际上,一个名为 AdapterHub¹³的项目是在我们使用的 transformers 库上构建的,旨在成为共享这些模块的中央存储库。在该部分中,我们将使用此项目构建在斯坦福情感树库(SST2)任务上微调的 BERT 模型。这相当于我们在先前的小节中微调 STS-B GLUE 子集所做的事情,将使您迅速了解适配器框架所提供的优势与我们之前所做的有何不同。
让我们按以下方式安装 AdapterHub 库:
pip install adapter-transformers
导入所需类并仅使用三行代码加载所需的适配器,如下所示:
from transformers import BertForSequenceClassification, BertTokenizer
model = BertForSequenceClassification.from_pretrained("bert-base-uncased") ❶
model.load_adapter("sentiment/sst-2@ukp") ❷
❶要微调的检查点
❷ 任务特定的适配器选择规范
适配器和使用说明在 AdapterHub 网站上列出 ¹⁴ 这就是我们要对 BERT 检查点适应 SST2 情感分类任务所做的一切。将这与上一节的微调步骤进行比较,就可以显而易见地看出适配器方法的实用性。我们无需进行微调,只需加载附加的模块即可继续前进!
请注意,在我们的代码中,我们使用了bert-base-uncased
检查点,并且我们要加载的适配器是在 UKP 句子论证挖掘语料库上进行了微调 ¹⁵,这是因为目前 AdapterHub 存储库中只有部分可用的内容。AdapterHub 是一个早期项目,我们预计随着时间的推移会提供更多的适配器。在撰写本文的 2020 年 10 月,已经有接近 200 个适配器可用。¹⁶
作为本节和本章的最终行动,让我们通过以下代码片段来确信我们构建的模型实际上作为情感分类引擎运行。我们使用以下两个句子的情感进行比较:“那是非常出色的贡献,好!”和“那对环境非常糟糕。”
import torch
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased") ❶
tokensA = tokenizer.tokenize("That was an amazing contribution, good!") ❷
input_tensorA = torch.tensor([tokenizer.convert_tokens_to_ids(tokensA)])
tokensB = tokenizer.tokenize("That is bad for the environment.") ❸
input_tensorB = torch.tensor([tokenizer.convert_tokens_to_ids(tokensB)])
outputsA = model(input_tensorA,adapter_names=['sst-2']) ❹
outputsB = model(input_tensorB,adapter_names=['sst-2']) ❺
print("The prediction for sentence A - That was an amazing contribution, good! - is:")
print(torch.nn.functional.softmax(outputsA[0][0])) ❻
print("The prediction for sentence B - That is very bad for the environment. - is:")
print(torch.nn.functional.softmax(outputsB[0][0])) ❼
❶ 使用常规预训练的分词器
❷ 句子 A
❸ 句子 B
❹ 进行了 A 的预测
❺ 进行了 B 的预测
❻ 显示了句子 A 的预测概率
❼ 显示了句子 B 的预测概率
这产生了以下输出:
The prediction for sentence A - That was an amazing contribution, good! - is:
tensor([0.0010, 0.9990], grad_fn=<SoftmaxBackward>)
The prediction for sentence B - That is very bad for the environment. - is:
tensor([0.8156, 0.1844], grad_fn=<SoftmaxBackward>)
所示的预测可以被解释为一对概率,第一个指示了输入是“负面”的概率,第二个是“正面”的概率。我们看到句子“那是非常出色的贡献,好!”有 99.9%的强烈正面概率。另一方面,句子“那对环境非常糟糕。”则是负面的,概率为 81.6%。这当然是合理的,并验证了我们的实验。
总结
-
应用嵌入因子分解和层间参数共享可以产生更加参数高效的模型。
-
在 BERT 系列模型上同时进行多任务微调,也就是多任务微调,会产生更具一般性的模型。
-
在 BERT 系列模型上使用适配器可以简化微调。
-
Z. Lan 等人,“ALBERT:自监督学习语言表示的 Lite BERT”,ICLR(2020)。
-
A. Wang 等人,“GLUE:自然语言理解多任务基准和分析平台”,ICLR(2019)。
-
N. Houlsby 等人,“NLP 参数高效迁移学习”,ICML(2019)。
-
Z. Lan 等人,“ALBERT:自监督学习语言表示的 Lite BERT”,ICLR(2020)。
-
X. Liu 等,“用于自然语言理解的多任务深度神经网络”,ACL 会议记录(2019)。
-
A. Wang 等,“GLUE:自然语言理解的多任务基准和分析平台”,ICLR(2019)。
-
P. Rajpurkar 等,“SQuAD:用于机器文本理解的 100,000+问题”,arXiv(2016)。
-
A. Wang 等,“GLUE:自然语言理解的多任务基准和分析平台”,ICLR(2019)。
-
N. Houlsby 等,“用于 NLP 的参数高效迁移学习”,ICML(2019)。
第十一章:结论
本章包括
-
总结本书涵盖的重要概念
-
总结相关的重要新兴概念
-
考虑自然语言处理中关于迁移学习方法的局限性以及环境和伦理考虑
-
展望自然语言处理中的迁移学习未来
-
跟上该领域的最新发展
在前面的章节中,我们涵盖了大量的材料——我们希望它们既具信息性又引人入胜。这一结论性章节试图对我们所做的一切进行有意义的总结,并展望该领域的未来和新兴的研究趋势。由于该领域的多产产出和快速发展的性质,我们当然没有涵盖每一个有影响力的架构或有前途的研究方向。为了减轻这一点,我们对我们在本书中没有机会涵盖的各种研究趋势进行了简要讨论,尽可能与已涵盖的材料进行联系和框架化。
在本章中,我们还试图通过涉及一些传统上没有受到太多关注的新兴问题,例如伦理考虑和各种模型的环境影响,提供更广泛的背景。这些与对这些模型的局限性的认识密切相关,我们在本章中尽可能突出这一点。
至关重要的是,我们讨论了在这样一个快速发展的领域中保持最新的各种提示。强调掌握了本书内容后,您现在只是开始您在该领域的旅程。所提出的工具和技能会随时间变化,并且它们的每个独特应用可能需要您的创造力或者尚未开发的新技术。在这样一个快速发展的领域中保持竞争优势确实是一次旅程,而不是一个目的地。我们鼓励读者对正在进行的研究保持探究的态度,并在某种程度上继续为其发展做出贡献。
让我们通过概述关键概念来开始这一最后一章。
11.1 关键概念概述
迁移学习旨在利用不同设置中的先前知识——无论是不同的任务、语言还是领域——来帮助解决手头的问题。它受到人类学习方式的启发,因为我们通常不会从头开始学习任何给定的问题,而是建立在可能相关的先前知识上。使没有实质计算资源的从业者能够达到最先进的性能被认为是向民主化获得正在进行的技术革命成果的重要一步。作为更具体的动机,考虑一下训练不同大小的 BERT 模型的代表性成本,如图 11.1 所示。¹
图 11.1 BERT 不同规模的训练成本。展示了两种代表性成本——单次运行和包括超参数调整在内的整个训练过程。15 亿参数的最大规模单次运行成本为$80k,而所有优化步骤计算下的成本为$1.6 百万!
正如图所示,最大规模的 BERT 训练成本可能高达数百万美元。迁移学习确实可以让您在几小时内,最坏情况下花费几美元用于微调,将这些宝贵的知识重复利用于您的个人计算项目中。
在计算机视觉中推广的迁移学习最近开始被自然语言处理(NLP)社区大量使用。而计算机视觉涉及教计算机如何理解和处理图像和视频,NLP 则考虑如何处理人类语音,无论是文本还是语音音频。在本书中,我们关注的是文本。我们特别感兴趣的一些 NLP 任务包括文档分类、机器翻译和问答。
尽管在历史上,这些任务最初是通过试图为每种情况制定固定规则来解决的——这种范式现在被称为符号 AI——但机器学习现在已成为主导趋势。计算机不再为每种可能的情况明确编程,而是通过看到许多这种相应的输入-输出对的示例来训练计算机将输入与输出信号相关联。传统上用于学习适当的输入-输出关系的方法包括决策树、随机森林、诸如 SVM 的核方法和神经网络。神经网络最近已成为解决感知问题(即计算机视觉和 NLP)的表示学习方法的首选。因此,这是我们在本书中探讨的最重要的方法类别。
在深入研究现代 NLP 的迁移学习方法之前,我们进行了一项关于传统机器学习方法的回顾性实验。具体来说,我们采用了以下方法:
-
逻辑回归
-
支持向量机
-
随机森林
-
梯度提升机
以解决两个重要问题:电子邮件垃圾检测和互联网电影数据库(IMDB)电影评论分类。为了将文本转换为数字,我们使用了词袋模型。该模型简单地计算了每封电子邮件中包含的单词标记的频率,从而将其表示为这些频率计数的向量。
现代自然语言处理(NLP)方法学主要集中在将文本部分(词语、子词、句子等)向量化上,采用诸如 word2vec 和 sent2vec 之类的技术。然后将得到的数值向量进一步处理,作为传统机器学习方法的特征,例如用于随机森林分类。
正如本书第一章所概述的,这一重要的自然语言处理研究子领域起源于 20 世纪 60 年代的信息检索术语向量模型。这在预训练的浅层神经网络技术方面达到了高潮,包括以下内容:
-
fastText
-
GloVe
-
word2vec,在 2010 年代中期推出了几个变体,包括连续词袋(CBOW)和 Skip-Gram
CBOW 和 Skip-Gram 都来自于训练用于各种目标的浅层神经网络。Skip-Gram 试图预测滑动窗口中任何目标词附近的单词,而 CBOW 试图预测给定邻居的目标词。GloVe,代表“全局向量”,试图通过将全局信息合并到嵌入中来扩展 word2vec。它优化了嵌入,使得单词之间的余弦乘积反映它们共同出现的次数,从而使得结果向量更具可解释性。fastText 技术试图通过对字符 n-gram(而不是单词 n-gram)重复 Skip-Gram 方法来增强 word2vec,从而能够处理以前未见过的单词。这些预训练嵌入的每个变体都有其优点和缺点。作为这类方法的数值演示,我们使用 fastText 词嵌入来重新访问 IMDB 电影分类示例,那里将词袋模型替换为 fastText 以将文本转化为数字。
几种技术受 word2vec 的启发,试图将较大的文本部分嵌入到向量空间中,以便在诱导向量空间中含义类似的文本部分彼此靠近。这使得可以在这些文本部分上进行算术运算,以进行关于类比、组合含义等推理。这样的方法包括以下内容:
-
段落向量,或doc2vec,利用了从预训练词嵌入中摘要单词的连接(而不是平均)。
-
Sent2vec扩展了 word2vec 的经典连续词袋(CBOW)—其中一个浅层神经网络被训练以从其上下文中的滑动窗口中预测一个词—到通过优化词和词 n-gram 的嵌入来对句子进行准确的平均表示。
作为这类方法的数值演示,我们使用了一个基于 fastText 而非词袋模型的 sent2vec 的实现来执行 IMDB 电影分类实验。
一些作者², ³, ⁴提出了各种分类系统,将迁移学习方法归类到不同的组别中。粗略地说,分类是基于迁移是否发生在不同的语言、任务或数据领域之间。通常,这些分类类型对应着以下内容:
-
跨语言学习
-
多任务学习
-
领域自适应
我们进行了一系列多任务迁移学习实验,使用了 IMDB 分类和电子邮件垃圾邮件检测这些熟悉的任务来说明这个概念。为了通过示例说明领域自适应,我们使用了自动编码器来调整一个在 IMDB 电影评论分类上训练的模型,以适应亚马逊图书评论的领域。这个练习还允许我们说明了零-shot 迁移学习的一个实例,即在亚马逊图书评论领域不需要微调就可以开始提供有价值的结果。
序列到序列建模的进展为诸如机器翻译之类的任务带来了革命。该设置中的编码器和解码器最初是循环神经网络(RNNs)。由于输入序列过长的问题,发展了一种称为注意力的技术,允许输出仅关注输入的相关部分。尽管最初这与 RNNs 结合使用,但它发展成为了使用自注意力构建编码器和解码器的技术。自注意力与最初的注意力公式不同,因为它寻求序列的部分与同一序列的其他部分之间的关联,而不是两个不同输入和输出序列的部分之间的关联。自注意力取代注意力的架构被称为 变压器,它在并行计算架构上比早期基于 RNN 的序列到序列模型更具可伸缩性。这种改进的可扩展性推动了它在竞争架构中的广泛采用。我们使用了一个预训练的英文到加纳语 Twi 的翻译变压器模型来探索这一重要架构的效能和其他特性。
NLP 的迁移学习的早期探索侧重于与计算机视觉的类比,而计算机视觉在此方面已经成功使用了一段时间。其中一个模型——SIMOn——采用了字符级卷积神经网络(CNNs)结合双向 LSTM 用于结构语义文本分类。SIMOn 代表 本体建模的语义推理。它是在 DARPA 的数据驱动模型发现(D3M)⁵ 计划中开发的,该计划是为了自动化数据科学家面临的一些典型任务。它展示了与计算机视觉中使用的方法直接类似的 NLP 迁移学习方法。该模型学到的特征还被证明对无监督学习任务也有用,并且在社交媒体语言数据上表现良好,这些数据可能有些特殊,与维基百科和其他大型基于书籍的数据集上的语言非常不同。列类型分类被用作该建模框架的说明性示例。
作为提醒,计算机视觉中微调的启发式大致如下:
-
随着目标域中的数据越来越多,阈值从输出中移动(并朝向输入)。在阈值和输出之间的参数被解冻并进行训练,而其余参数保持不变。这是由于增加的数据量可以有效地用于训练更多的参数,而否则无法完成。
-
另外,阈值的移动必须远离输出并朝向输入,因为这样可以保留编码靠近输入的通用特征的参数,同时重新训练更靠近输出的层,这些层编码源域特定特征。
-
此外,当源和目标高度不同时,一些更具体的参数/层可以完全丢弃。
早期嵌入方法(如 word2vec)的一个主要弱点是消歧义 - 区分一个词的各种用法,这些用法根据上下文可能有不同的含义。这些词在技术上被称为同形异义词,例如,duck(姿势)与 duck(鸟)和 fair(集会)与 fair(公平)。来自语言模型的嵌入 - 在流行的Sesame Street角色之后缩写为 ELMo - 是最早尝试开发单词的上下文化嵌入的方法之一,使用双向长短期记忆网络(bi-LSTMs)。ELMo 可以说是与正在进行的 NLP 迁移学习革命相关联的最流行的早期预训练语言模型之一。它与 SIMOn 具有许多架构相似之处,后者由字符级 CNNs 和 bi-LSTMs 组成。这个模型中一个词的嵌入取决于其上下文,ELMo 通过被训练来预测单词序列中的下一个单词来实现这一点。大量数据集,如维基百科和各种图书数据集,被用来训练这个模型。我们将 ELMo 应用于一个说明性的示例问题,即假新闻检测,作为一个实际演示。
通用语言模型微调(ULMFiT)进一步提出了一种方法,为任何特定任务微调基于神经网络的语言模型。该框架介绍并演示了一些关键的技术和概念,以更有效地适应预训练语言模型的新设置。这些包括区分性微调和渐进解冻。区分性微调规定,因为语言模型的不同层包含不同类型的信息,它们应该以不同的速率进行微调。渐进解冻描述了一种逐渐地以渐进方式微调更多参数的过程,目的是减少过拟合的风险。ULMFiT 框架还包括在适应过程中以独特方式改变学习率的创新。我们使用 fast.ai 库对这些概念进行了数值上的说明。
-
OpenAI 的生成式预训练变压器(GPT)修改了变压器的编码器-解码器架构,以实现 NLP 的可微调语言模型。它丢弃了编码器,保留了解码器及其自注意子层。它是以因果建模目标进行训练的——预测序列中的下一个词。它特别适用于文本生成。我们展示了如何使用 Hugging Face 的 transformers 库快速使用预训练的 GPT-2 模型进行文本生成,该库在本书中早已介绍过。
-
从转换器的双向编码器表示(BERT)可以说是相反的,通过保留编码器并丢弃解码器来修改转换器架构,还依赖于掩码词语,然后需要准确预测作为训练度量的这些词语。更具体地说,它是以掩码建模目标训练的——填补空白。此外,它还通过下一个句子预测任务进行训练——确定给定句子是否是目标句子之后的一个合理的跟随句子。虽然不适用于文本生成,但该模型在其他一般语言任务(如分类和问题回答)上表现非常好。我们将其应用于问题回答和文档分类的两个重要应用。文档分类用例是垃圾邮件检测。我们还展示了它在填补空白和检测一个句子是否是另一个句子的合理的下一个句子方面的应用。
-
mBERT 模型,代表“多语言 BERT”,实际上是同时在 100 多种语言上预训练的 BERT。自然地,这个模型特别适合跨语言迁移学习。我们展示了多语言预训练权重检查点如何有助于为原本未包含在多语言训练语料库中的语言创建 BERT 嵌入。BERT 和 mBERT 都是由 Google 创建的。
-
在所有这些基于语言模型的方法——ELMo、ULMFiT、GPT 和 BERT 中——都显示出生成的嵌入可以用相对较少的标记数据点进行特定下游 NLP 任务的微调。这解释了 NLP 社区对语言模型的关注:它验证了它们诱导的假设集通常是有用的。
我们还介绍了一些关于已覆盖的深度 NLP 迁移学习建模架构的适应策略。换句话说,针对预训练架构,如 ELMo、BERT 或 GPT,如何更有效地进行迁移学习?我们专注于参数效率,目标是在尽可能减少参数的情况下产生一个性能损失最小的模型。这样做的目的是让模型更小、更容易存储,从而更容易在智能手机等设备上部署。或者,智能的适应策略可能仅仅是为了在一些困难的转移情况下达到可接受的性能水平。我们介绍的适应策略有:
-
我们探索的第一种适应策略是前面提到的 ULMFiT 技术,即逐步解冻和区分微调,使用的是 fast.ai 库。
-
然后我们探索了被称为知识蒸馏的模型压缩方法,因为它最近在 NLP 领域显赫一时。这个过程本质上试图通过显著更小的学生模型来模拟更大教师模型的输出。特别是,我们使用 transformers 库中知识蒸馏方法 DistilBERT 的实现来证明通过这种方式可以将 BERT 模型的大小减少一半以上。
-
我们接触到的下一个适应策略围绕着两个想法,旨在创建更有利于更大词汇量和更长输入长度的基于变压器的语言模型。第一种方法涉及巧妙的因式分解,或者将一个更大的权重矩阵分解为两个较小的矩阵,允许你增加一个维度而不影响另一个维度。第二种想法涉及跨所有层共享参数。这两种策略是 ALBERT(A Lite BERT)方法的基础。我们使用 transformers 库中的实现来亲身体验这一方法。
因此,我们基于多任务学习的想法,即模型被训练以同时执行多种任务,并产生更具有普遍性的模型。当面临转移场景时,我们没有足够的训练数据来在给定任务上进行微调时,为什么不在多个任务上进行微调?讨论这个想法给我们提供了一个很好的机会来介绍通用语言理解评估(GLUE)数据集,这是一组代表人类语言推理的几项任务的数据集,例如检测句子之间的相似性,问题之间的相似性,释义,情感分析和问题回答。我们展示了如何使用这个数据集快速利用 transformers 库进行多任务微调。这个练习还演示了如何类似地在来自这些重要问题类别之一的自定义数据集上微调来自 BERT 系列的模型。
我们还建立在领域自适应的思想上,特别是源域和目标域的相似性对于迁移学习的有效性起着至关重要的作用。更大的相似性通常意味着一般情况下更容易进行迁移学习。当源域和目标域之间相差太大时,可能无法在单一步骤中执行该过程。在这种情况下,可能会使用顺序自适应的想法将所需的整体迁移分解为更简单、更易管理的步骤。举例来说,我们首先将一个“填空”目标预训练的 BERT 逐步适应到一个低资源的句子相似度检测场景中,再适应到一个数据丰富的问题相似度场景中。实验中两个场景的数据都来自 GLUE 数据集。
我们探讨的最终适应策略是使用所谓的适应模块或适配器。这些是仅在预训练神经网络的层之间具有少量参数的新引入的模块。对于新任务微调这个修改过的模型只需要训练这几个额外的参数。原始网络的权重保持不变。与微调整个模型相比,通常只添加了 3-4% 的额外参数时往往几乎没有性能损失。这些适配器也是模块化的,并且容易在研究人员之间共享。我们使用了 AdapterHub 框架来加载其中一些适配器,并展示它们如何用于将通用 BERT 模型适应为在情感分类任务上表现良好的模型。
11.2 其他新兴研究趋势
在整本书中,我们试图强调,在诸如 NLP 的迁移学习这样的快速发展领域中,像这样一本单独的书籍完全涵盖每种架构或创新是不可能的。相反,我们采取的方法是专注于我们认为是基础的架构和技术。未来的创新很可能在某种程度上是从这些架构和技术中派生出来的,因此读者可能通过自己的一些努力来学习它们。为了进一步促进这一点,我们将这一部分重点放在了我们在这本书中没有涵盖但在该领域中已经有些影响的各种研究趋势的简要讨论上。我们尽可能将它们置于我们已经涵盖的内容的背景中,以便您在需要时更轻松地学习这些主题。
我们首先通过概述 RoBERTa⁷——Robustly Optimized BERT Approach——来开始这个练习,它采用了一些优化技巧来提高 BERT 的效率。
11.2.1 RoBERTa
所讨论的研究试图复制 BERT,同时特别关注各种训练超参数和设置以及它们对结果的影响。总的来说,观察到通过谨慎的设计选择可以显著改善原始 BERT 的性能。这样的选择之一是去除下一句预测(NSP)任务,同时保留掩码语言建模(MLM)— 填空— 任务。换句话说,他们发现 NSP 会降低下游任务的性能,并显示去除它是有益的。其他设计选择包括在训练过程中使用较大的学习率和小批量。它是由我们在本书中介绍的 Hugging Face 的 transformers 库中实现的。
接下来,我们将看看迄今为止开发的最大语言模型之一 — GPT-3,最近引起了很多关注,并在 NeurIPS 2020 虚拟研究会议(2020 年 12 月)上获得了最佳论文奖。
11.2.2 GPT-3
您可能还记得我们的报道,GPT 模型经历了几次迭代 — GPT、GPT-2,最近是 GPT-3。在撰写时,GPT-3 恰好是拥有 1750 亿参数的最大的预训练语言模型之一。它的前身 GPT-2 拥有 15 亿参数,在发布时也被认为是最大的,仅仅在前一年发布。在 2020 年 6 月发布 GPT-3 之前,最大的模型是微软的图灵 NLG,拥有 170 亿参数,并于 2020 年 2 月发布。这些指标的进展速度之快令人震惊,这些记录往往很快就会被打破。为了比较,这些参数爆炸在图 11.2 中有所体现。
图 11.2 模型参数数量随时间增长的趋势。如图所示,模型大小的爆炸趋势似乎在加速,其中最近的一个进步 — GPT-3 — 表示了 10 倍的增长因子。
如图所示,GPT-3 相比之前最大的图灵 NLG 增长了超过 10 倍,这是一次超越以往进步的飞跃。实际上,一种称为 Switch Transformer 的架构,通过为不同的输入分配单独的 transformer 块部分利用了稀疏性,并声称在 2021 年 1 月达到了 1 万亿参数的规模。由于在撰写时仍在进行同行评审,我们没有在图 11.2 中包含这种架构。然而,很明显,这种模型大小增长的趋势似乎正在加速。
在 GPT-3 论文中,作者展示了这个巨大的模型可以在很少的样本情况下执行广泛的任务。例如,只需看几个示例翻译,它就可以被启动以将一种语言翻译成另一种语言,或者只需看几个示例垃圾邮件,即可检测垃圾邮件。 事实上,一些意想不到的应用,例如从编码的描述中编写代码,已经被广泛报道。目前,该模型尚未由 OpenAI 作者发布给公众,只有通过邀请和付费 API 向少数早期采用者提供。 OpenAI 限制接入的理由是监控使用,从而限制这种技术的任何潜在有害应用。 GPT-3 的早期采用者是一款名为 Shortly 的应用程序,它为创意写作提供了 GPT-3 访问权限,任何人都可以以少量费用尝试。
此外,一个最近更小但功能强大的 GPT-3 开源替代品已经在 transformers 库中可用:EleutherAI 的 GPT-Neo。该组织旨在构建一个与全尺寸 GPT-3 相当的模型,并在开放许可下向公众提供。他们的存储库中提供了不同大小的模型,您也可以使用 Hugging Face 托管的推理 API,通过浏览器测试这些模型。我们还提供了伴随 Kaggle 笔记本,演示了我们在第七章中进行的练习的运行情况。通过检查,您应该会发现其性能更好,但自然而然地,成本也更高。 (最大型号的重量超过 10 GB!)
关于 GPT-3 工作的一个重要事项是,作者本人在论文中认识到,使语言模型更大的好处已接近极限。即便在极限下,处理某些类型的任务(例如关于对常识物理的理解的文本生成)的性能仍然较差。因此,虽然它确实代表了一项重要的技术突破,但在建模方法(而不是简单地扩大模型规模)上的创新必须成为前进的道路。
接下来,我们将看一组旨在改善基于 transformer 的模型在更长输入序列上的性能的方法。这很重要,因为香草 transformer 模型会随着输入长度呈平方级别的运行时间和内存使用。
11.2.3 XLNet
XLNet¹³是在类似早期模型 Transformer-XL¹⁴的基础上构建的,旨在更好地处理更长的输入序列。其中一个关键组成部分的想法是因果语言建模(CLM),我们在讨论 GPT 时已经讨论过,它涉及到预测序列中下一个词的经典语言建模任务。请注意,此方法中的未来标记已被屏蔽。XLNet 论文的作者等效地将其称为自回归语言建模。XLNet 的另一个关键组成部分是对输入序列的所有可能排列执行 CLM。这个想法有时被称为排列语言建模(PLM)。通过结合 PLM 和 CLM,实现了双向性,因为所有标记都有机会在某个排列中作为过去标记被包含。XLNet 和 Transformer-XL 都没有序列长度限制,并且由 Hugging Face 的 transformers 库实现。
有了这种对 XLNet 的看法,让我们继续考虑 BigBird¹⁵,这是一种引入稀疏注意机制的创新,以实现更高的计算效率。
11.2.4 BigBird
BigBird 通过引入一种稀疏注意机制将传统基于 Transformer 的模型的二次依赖关系减少到线性,该机制被证明能够近似并保持原始完整注意力的性质。与一次应用完整注意力到整个输入序列不同,稀疏注意力逐个查看序列标记,允许它更加智能地且舍弃一些连接。可以处理长达传统基于 Transformer 模型处理的八倍长度的序列,且可在类似硬件上处理。它是由 Hugging Face 的 transformers 库实现的。
接下来,我们将介绍 Longformer¹⁶,这是对 Transformer 传统完整自注意力的另一项创新,能够更好地随着输入长度的增加而扩展。
11.2.5 Longformer
Longformer 是针对传统 Transformer 注意力的二次缩放的又一尝试。这里的创新是将局部窗口注意力与全局任务导向注意力相结合。局部注意力用于上下文表示,而全局注意力用于构建在预测中使用的完整序列表示。所达到的缩放在输入序列长度方面是线性的,类似于 BigBird。Longformer 是由 Hugging Face 的 transformers 库实现的。
我们接下来介绍 Reformer¹⁷,这是另一种减轻原始自注意力二次缩放的方法。
11.2.6 Reformer
Reformer 引入了两种技术来对抗原始 Transformer 输入长度的二次扩展的计算时间和内存消耗。用局部敏感哈希替换原始的完全自注意力机制,减少了冗余计算和时间复杂度从二次到 O(LlogL)(其中 L 是输入序列长度)。一种称为可逆层的技术允许只存储激活一次。在实践中,这意味着,与为具有N层的模型存储激活N次相比,只使用了一小部分内存。根据N的值,内存节省可能非常大。Reformer 是由 Hugging Face 实现的 transformers 库中的一个模型。
显然,使基于 Transformer 的模型在处理更长的输入长度时表现更好已成为一个元研究趋势。我们在这里可能没有包括所有关于此主题的重要研究,如果你自己深入研究,可能会发现更多。
接下来,我们将谈论最近重新出现的序列到序列建模方法。这些尝试将本书中遇到的各种问题统一到一个文本到文本建模框架中。
11.2.7 T5
你可能还记得在本书中我们讨论过,序列到序列模型在自然语言处理中发挥了重要作用。首次出现在循环神经网络(RNN)模型的背景下,它们也被翻译应用领域在原始 Transformer 架构的背景下所探索。T5,“文本到文本转换 Transformer”,是将各种自然语言处理问题统一到一个序列到序列框架中的尝试。它允许对每个任务应用相同的模型、目标、训练过程和解码过程。处理的问题类别包括从摘要到情感分析和问答等众多领域。英语和罗马尼亚语、德语、法语之间的语言对翻译被包括在训练中。一些代表性的数据转换,使得可以在多种任务上训练单一模型,如图 11.3 所示的翻译和摘要(灵感来源于 T5 论文的图 1)。
图 11.3 T5 是一个序列到序列模型,它采用了一系列转换,使得可以同时在各种任务上训练单一模型、解码过程和目标。它可以被看作是多任务学习的一个有趣变体。
如图所示,任务数据通过在原始文本数据前加上标准任务描述符来转换。训练数据包括 GLUE 和 SuperGLUE 数据集、用于抽象摘要的 CNN/Daily Mail 数据集等。目标是处理包含的多样化的自然语言理解任务,而不修改模型。从这个意义上说,它可以被看作是我们在整本书中一直提到的多任务学习思想的一个有趣变体或迭代。同时学习如此多种任务的包含可能会实现参数共享和生成模型的更好泛化能力。关键是,模型最初是在作者称之为“巨大干净爬行语料库”(C4)的数据集上使用蒙版语言建模或自动编码目标进行训练,然后在上述各种任务上进行微调。基本上,所有标记的 15% 被丢弃,结果被送入输入,而未损坏的输入被送入输出以进行预测。请注意,C4 语料库本质上是目标语言(英语)的互联网,其中过滤掉了色情材料、代码和其他“垃圾数据”。用于训练的模型架构类似于我们在第七章中用于翻译的转换器架构。由所得模型在许多包含的任务上实现了最先进的结果。
除了原始的 T5 模型之外,还开发了一个多语言版本,不足为奇地称为 mT5,通过同时对 101 种语言进行训练。T5 和 mT5 都在 Hugging Face 的 transformers 库中实现。
接下来,我们简要介绍 BART,它与 T5 类似,都是基于转换器的序列到序列建模框架。
11.2.8 BART
BART,即双向自回归转换器,可以被视为 T5 减去单一统一变换的模型,以使未经修改的模型能够应用于各种任务。相反,首先对标准的转换器编码器-解码器架构进行预训练,以通过各种噪声方法重现损坏的输入。这包括蒙版语言建模,如 BERT 和 T5,以及排列语言建模,如 XLNet 等。然后,该模型被修改用于各种任务,例如 SQuAD、摘要等,并针对每个任务分别进行微调,类似于我们对传统 BERT 所做的操作。这个模型在语言生成任务中表现特别好,例如摘要、翻译和对话。同时也开发了一个多语言版本 mBART,通过同时对 25 种语言进行训练而获得。BART 和 mBART 都在 Hugging Face 的 transformers 库中实现。
在下一个小节中,我们将审视一种最新的跨语言模型,它不仅仅是同时在多种语言上进行训练,还通过在有并行数据时修改语言建模目标来显式建模跨语言转移。
11.2.9 XLM
XLM,²² 其作者用来指代“跨语言语言模型”,是一个结合单语和并行数据的跨语言学习方法的建模框架。在不同语言上学习得到的单语嵌入可以使用已知数值表示的小词汇表进行对齐。如果有并行数据可用,作者提出了一种他们称之为翻译语言建模(TLM)的方法,并同时利用它进行跨语言学习。本质上,这涉及将并行数据的连接序列应用掩码语言建模,在两种语言的连接序列的各个部分中,让一些单词消失并预测它们。
在跨语言学习任务中观察到显著的改进。它还激发了许多类似模型的产生,特别是 XLM-R,²³ 它将 XLM 的思想与 RoBERTa 的思想结合起来,以提高性能。XLM 和 XLM-R 都在 Hugging Face 的 transformers 库中实现。
最后,我们简要谈到了一种在书中遇到的重要问题数据类别——表格数据的专门模型。
11.2.10 TAPAS
在第五章和第六章中,我们讨论了 SIMOn 方法及其对表格数据类型分类的处理——这是数据科学家通常会遇到的一个重要问题类别。TAPAS²⁴ 是尝试将基于变换器的模型的建模优势扩展到这一重要问题类别的一种尝试,通过显式地为表格数据中的问答建模和专门化。TAPAS 代表表格解析器。在第八章中,我们讨论了将 BERT 应用于问答任务。结果专门化模型的输出是输入上下文段落中感兴趣问题的潜在答案的开始和结束位置。除此之外,TAPAS 还学会检测表格中哪个单元可能包含可以从中提取答案的上下文段落,且起始和结束索引类似。与本节中讨论的其他大多数模型一样,该模型在 Hugging Face 的 transformers 库中实现。
这标志着我们对本书中尚未有机会详细分析的近期工作的概述之旅的结束。这些模型架构大多可以通过与我们在 transformers 库中使用 BERT 和 DistilBERT 非常相似的代码来使用。
在下一节中,我们将尝试对这个领域下一步可能的走向做出有根据的猜测——鉴于当前和新兴的研究趋势,哪些主题可能保持或变得流行。
11.3 NLP 中转移学习的未来
在本节中,我们尝试通过预测该领域即将出现的形态来推断前面两节描述的趋势。
对过去两节的批判性分析揭示了两个可以说是正交的元趋势——一个是将模型尽可能地变大,另一个是开发更高效的更大模型的推动。
GPT-3,目前我们观察到的参数数量迈出的最大一步——是以前的 10 倍,最初引起了一些研究人员对研究公司开始将重点放在规模而不是巧妙建模上的担忧。然而,正如我们在上一节讨论的那样,扩大模型的局限性很快变得明显,GPT-3 论文的作者们承认已经达到了可能的极限。考虑到 GPT-3 目前仅通过有限的付费 API 可用,我们可以预期空间中的其他参与者将尝试很快构建更大的模型,因为他们有货币激励这样做(我们已经提到正在进行同行评审的拥有万亿参数的 Switch Transformer)。这场竞赛很可能最终将导致谷歌和/或 Facebook 发布一个类似的模型,这很可能会推动 GPT-3 完全开源(类似的情况在 GPT-2 历史上已经发生过)。除此之外,我们预计会有更多资源开始致力于实现类似性能的更有效方法。
在 NLP 迁移学习的即将到来的有趣问题中,大多数可能都在被一些人称为TinyML的运动中。这可以被定义为一个将模型大小缩小到可以适应较小硬件的通用目标。我们在第九章演示了一个例子,即通过 DistilBERT 等方法可以将 BERT 的大小大致缩小一半,而性能损失很小。我们在第十章取得了类似的成就的另一种方法是 ALBERT,它实现了模型大小的 90%缩减。现在全球大部分人口都拥有智能手机,可以运行这些先进模型的较小版本。这给物联网(IoT)等领域带来的机会是巨大的,其中设备形成智能网络,每个节点都能独立地执行复杂功能。尽管今天许多手机应用程序可能都包含翻译和其他工具的服务器后端,用于进行实际的翻译和其他计算,但在没有互联网连接的情况下在智能手机上本地运行这些算法的能力正在成为更可行和更普遍的范式。我们预计在未来几年内,在使 BERT 及其衍生产品更小和更具参数效率的努力将继续如火如荼。
另一个你可能从前一节中注意到的趋势是对跨语言模型的日益关注。事实上,过去一年全球对所谓的“低资源”语言的方法的投资有所增加。我们在第七章中通过一个例子提到了这一点,当时我们使用了一个变压器架构将低资源的西非语言特威(Twi)翻译成了英语。许多流行的经济模型预测,非洲市场正在出现一个日益重要的消费者群体,这很可能是引起对这一领域突然兴趣和投资的至少一个推动因素。对于许多低资源语言来说,应用我们讨论的所有方法的初始障碍往往是数据的可用性。因此,我们可以预期在接下来的一年左右,适当的多语言数据开发将受到很多关注,随后将进行对语言特定方法的深入研究。值得关注的地方,特别是涉及非洲语言的地方,包括 NLP Ghana,Masakhane,EthioNLP,Zindi Africa,AfricaNLP 和 Black in AI。
语音是另一个即将迎来转折时刻的 NLP 研究前沿。直到最近,自动语音识别模型,将语音转录成文本,需要大量的平行语音文本数据才能取得良好的结果。Facebook 最近的一种架构 Wav2Vec2 展示了,同时在许多语言上对语音进行预训练可以极大地减少所需的平行数据量。这类似于我们在本书中探讨的文本中使用的 mBERT 的功能。Wav2Vec2 模型已经在 transformers 库中提供,并且可以通过只使用几个小时的标注语音数据对新语言进行微调。我们预计这将在接下来的一年内首次推动多种语言的语音识别工具的开发。此外,我们预计类似的事情也将在不久的将来出现在另一个方向上:文本到语音,也就是从文本生成语音。
在第一章中,我们描述了 NLP 中的迁移学习是如何受到计算机视觉的进展的启发的。有趣的是,最近 NLP 迁移学习的进展似乎又激发了计算机视觉的进一步发展。一个具体的例子是 DALL-E,这是一个在文本描述 - 图像对上训练的 GPT-3 的版本,它已经学会了从文本提示中生成图像。一个更广泛的趋势是构建上下文情景的对象嵌入,试图从场景中其他可观察到的对象预测缺失的对象,类似于 BERT 和类似的遮蔽语言模型所使用的填空词目标。
另一个最近似乎越来越受到关注的研究问题是:这些模型的环境和道德影响是什么?在最近的研究高潮初期,研究人员似乎满足于发布只改善技术指标的模型,但随着时间的推移,这个领域开始重视对潜在道德影响的详细探讨。与之相关的是对可解释性的提高兴趣:我们能否真正解释模型是如何做出决定的,以确保它不会歧视?我们将在下一节进一步探讨这些道德问题。
11.4 道德和环境考虑
你可能还记得我们在第五章和第六章讨论关于假新闻检测的问题时提出了所谓的假新闻是什么是一个有争议的观点。如果在数据标签的质量上不加注意,那么准备训练数据标签的人身上植入的偏见很可能会转移到分类系统上。这是我们首次遭遇到了在部署这些模型到可能会显著影响人类生活的情况下,充分意识到潜在局限性的重要性。
当我们在第八章用 Jehovah’s Witnesses 准备的 JW300 数据集对 mBERT 进行微调时,我们发现它会以有偏见的方式填补空白。当我们试图预测一个基本名词“学校”时,它会提供像伊甸园这样的词作为合理的补充。这表明了强烈的宗教偏见,这是我们在旅程中第二次被提醒到,盲目地将这些模型应用于某些数据可能会产生有偏见和意想不到的结果。
在这一部分,我们将从更广泛的角度讨论这个问题,考虑到可能需要放在从业者脑后的道德和环境考虑因素。这是一个近来受到越来越多关注的话题,但在机器学习领域并不算新鲜。
早期关于偏见的知名机器学习研究可预见地发生在计算机视觉领域。具有里程碑意义的作品,“性别和肤色”,³⁴研究了商业性别分类系统在种族和性别维度上的准确性。它发现,与较浅肤色的男性相比,这些系统在较深肤色的女性身上表现不佳,绝对百分比高达 35 个百分点。这对少数族裔社区有着巨大的实际影响,在一些地区可能会受到一些自动计算机视觉系统的监控。错误的分类或检测可能意味着错误的逮捕,即使被清除,也可能意味着在最脆弱的社区中失去工作。有多次广泛报道这种情况发生在真实的人身上。一个愤世嫉俗的力量失衡在那些打着“客观”和“科学”旗号的系统背后被揭露出来,在这些系统被开发的更富裕的社区和它们的经济利益主要是被搬走的地方并没有遭受到对较贫困的社区造成的伤害。这项工作及相关研究的影响是巨大的,最近美国国会最近出台了相关的减轻监管措施,可以说这是直接后果。像 IBM 和亚马逊这样的公司也被迫重新审视他们与执法机构分享这些技术的方式,IBM 甚至完全停止了这项服务。
最近对预训练的自然语言处理语言模型存在偏见的担忧也很高。实际上,GPT-3 论文³⁵专门包括了一个研究的部分,涵盖了几个方面,即种族、性别和宗教。最近越来越常见地看到学术文章做这样的研究,这是非常令人鼓舞的。特别是 GPT-3 的研究探讨了模型从训练数据中学到的与各个关注维度的关联。例如,他们发现通常与较高教育水平相关联的职业在填空时更倾向于与男性代词相关联。同样,暗示专业能力的提示更有可能由男性代词和说明者完成。这很可能是模型直接从互联网学到的性别偏见,我们可能无法指望互联网是一个无偏见的信息来源。另一方面,积极的描述词更有可能被“亚洲”和“白人”等词引导的名词以比“黑人”人物高得多的速度分配。同样,模型显然从互联网学到了种族偏见,而对模型的盲目应用只会传播这种偏见。在宗教维度上,“伊斯兰”一词与“恐怖主义”一词相关联,是最有可能的完成之一。作为这种偏见的直接现实影响,考虑一下那位巴勒斯坦人的善意“早上好”Facebook 帖子被错误地翻译为“攻击他们”,并导致了重大的不公平后果。³⁶
预训练的自然语言处理语言模型可能无意中影响贫困社区的另一种方式是通过气候变化。实际上,这些模型最近被发现具有相当大的碳足迹。³⁷, ³⁸虽然发现单个 BERT 模型的一次训练的碳足迹相当于纽约到旧金山的一个普通往返航班,但在微调和超参数优化期间,模型实际上会进行多次训练。如果模型通过 神经架构搜索 部署,其中各种架构超参数被详尽地变化,然后选择性能最佳的模型,研究人员发现单个模型部署的碳足迹相当于五辆普通汽车的寿命。再次强调,这是严重的,特别是因为与这些碳足迹直接相关的气候变化影响最严重的是贫困社区,而这些社区并没有体验到这些模型的直接好处。很明显,在评估这些模型时,这些成本需要纳入考虑。这一认识可以说是驱使该领域朝着更具参数效率的模型发展的力量之一。
预训练语言模型及深度学习普遍存在的一个长期批评是,这些模型往往不太可解释——很难解释模型是如何在特定情景下得出预测的。这与本节早些时候讨论的偏见问题有关——让模型解释其对教育相关联的决策是如何做出的,例如,可以帮助检测这样的决定是否基于种族或性别变量。最值得注意的最近的方法之一是 bertviz,³⁹试图在第七章中探索的注意力可视化基础上进行改进。然而,这仍然没有解决训练数据透明度缺失的问题:语言模型的训练规模如此之大,以至于研究人员几乎无法确保其是无偏的。因此,我们期望看到人们投入时间和精力开发出可以从更小、精心策划的数据集中执行相当的方法。
通过我们对一些应该记住的伦理问题的简要讨论完成后,我们在下一节中提供一些关于如何在这个快速发展的领域中保持时效性的建议。
11.5 保持时效性
正如我们在本章中一再强调的那样,NLP 中的迁移学习方法的状况更新速度很快。本书涵盖的材料应该仅被视为一个平台,用于继续跟踪最新发展。在本节中,我们提供了一些关于如何实现这一目标的基本提示。总的来说,在 Kaggle 和/或 Zindi 平台上参加各种相关竞赛可能是一个处理现实情况的好方法,但数据足够干净且与时俱进。跟踪 arXiv 上的最新论文是必不可少的,尽管新闻和社交媒体的报道可能夸张并且不可靠,但它仍然有助于及早发现有影响力的论文。
11.5.1 Kaggle 和 Zindi 竞赛
在整本书中,我们鼓励您使用 Kaggle 来运行所呈现的各种代码。尽管该平台提供的免费 GPU 计算和易用的设置立即成为其优点,但最大的好处可能直到现在才被明确说明。可以说,Kaggle 平台最强大的方面是可以访问该平台上众多持续进行和归档供后人参考的竞赛。
各种顶级公司面临各种技术挑战,使用该平台来刺激对这些问题的解决方案的研究和开发,通过提供现金奖励,有时可达数千美元的头等奖。这意味着通过跟踪这些竞赛,您可以了解到行业中最紧迫的问题是什么,同时可以访问代表性数据进行即时测试和实验。您可以按主题浏览当前和过去的竞赛,以找到测试任何想法所需的数据——您所需要做的就是将数据集附加到本书中使用的笔记本上,更改一些路径,然后您可能已经准备好产生一些初步的见解了。当然,如果您能够做到这一点,赢得竞赛是很好的,但您从实验、失败和再试中获得的学习价值才是真正无价的。实际上,根据我的经验,一个在排行榜上可能被认为是平庸的竞赛问题解决方案,如果在实践中易于部署和扩展,可能会导致真正的现实影响。我们在附录 A 中提供了一些使用 Kaggle 的具体提示,以帮助初学者入门。
我们还强调了 NLP 中对低资源语言的关注日益增加。因此,重要的是提到 Zindi Africa 平台,该平台提供与 Kaggle 类似的许多功能,但专注于非洲语言和问题。如果您是一位研究人员,想要了解您的方法在这些类型的语言中可能的表现,那么这个平台将是一个寻找相关竞赛和实验数据的好地方。
11.5.2 arXiv
机器学习,以及延伸开来的自然语言处理,可以说是当今最开放的研究领域。除了几个例外,一般来说,结果通常在一旦出现后立即在开放平台arXiv上发表。这使研究团队能够提前对任何发现进行申请,同时进行完善和论文出版手续。这意味着,如果你能找到它的话,最前沿的研究已经对你可用。arXiv由 Google Scholar 存档,因此你可以在那里设定关键词的警报,帮助你及早发现相关的论文。
arXiv平台上传的论文数量巨大,要找到与你相关的最重要的论文可能会有些困难。为了解决这个问题,我建议关注你喜欢的论文的作者在社交媒体上的动态——Twitter 似乎是这一领域的研究人员比较喜欢的平台。关注媒体报道也可能会有所帮助,只要你对所有声明持保留态度。接下来我们会多说几句关于这个问题。
11.5.3 新闻和社交媒体(Twitter)
总的来说,把科学主题的新闻和社交媒体报道看作可能具有煽动性和技术上不可靠是件好事。如果我们考虑一个媒体机构可能与报道技术相关的激励以及通常记者对主题可能没有技术背景的事实,这就说得通了。然而,经过核实的新闻可能是关于特定论文或主题的社区兴奋的一个良好指标,这总是一个需要考虑的好事。
如果你使用像谷歌新闻这样的平台,你可以在你的订阅中设置“语言模型”等主题的警报。你可能会得到很多信息,而且并非所有信息都值得关注。通常,我只会在这些论坛上深入研究一篇论文,只有在我认为一段时间内一直“可靠”的场所出现后,我才会深入研究一篇论文,这使我对这些论点至少经受了一段时间的公开评审产生了信心。GPT-3 的情况正好是个最近的例子——通过谷歌新闻上的这个启发式方法,我立刻就能体会到其影响。
关于社交媒体,Twitter 似乎是机器学习研究科学家的选择平台。事实上,许多人对他们的工作非常开放,并且如果你向他们提问,他们会很乐意在平台上直接回答你。这也是我最喜欢在这个领域工作的原因之一。请随时在@pazunre 上联系我。你最喜欢的作家或科学家可能会在他们的订阅中分享他们最新最喜欢的论文,通过关注他们,你可以直接收到这些信息。在这一领域,你可能会对以下一些受欢迎的账户感兴趣:@fchollet,@seb_ruder 和@huggingface。
除了竞赛、阅读 arXiv 上的论文、追踪新闻和社交媒体,没有什么比使用这些工具解决实际的挑战更好的了。对于许多人来说,这可能意味着在机器学习和/或自然语言处理领域拥有一份工作,并且每天都在解决一个实际的应用。实际的经验是这个领域大多数潜在雇主最重视的。如果你还没有在这个领域获得这样的实际经验,并且希望进入,那么开源项目可能是一个很好的途径——看看 TensorFlow、PyTorch、Hugging Face、NLP Ghana、Masakhane 等等。列表是无穷尽的,有很多有趣的问题可以解决和贡献,同时也可能使每个人受益。
我希望这些提示能帮助你进入你的机器学习和自然语言处理的未来,让你有能力对你的社会产生重大的积极影响。能够与你分享你旅程的一部分是我的荣幸。
11.6 最后的话
这就是了!你做到了——你已经读完了整本书。在写作过程中,我度过了难忘的时光,与许多研究人员互动,讨论思想并克服了许多挑战。我真诚地希望你享受这个旅程,就像我一样。当你带着这些工具改变世界的时候,请记得善待你周围的人,不要伤害生态系统,并且保持警惕,以防技术被滥用。通过与这个领域中一些杰出头脑互动的短暂时间内,我真诚地相信大多数人对于将这些技术突破变成善良的源头都感到兴奋。因此,我每天都迫不及待地关注研究新闻,渴望看到我们集体的人类思维将会产生什么样的惊喜。我只能希望你也能分享一些这种兴奋。
概要
-
你只是在这个迅速发展的领域中旅程的开端;保持竞争优势是一个旅程,而不是一个终点。
-
通过学习这本书所掌握的技能,使你处于一个良好的位置,能够通过持续的努力保持更新。
-
我们涵盖的一些关键的基础预训练迁移学习启用的语言建模架构包括 Transformer、BERT、mBERT、ELMo 和 GPT。
-
将这些更大的模型变得更小、更高效的愿望导致了像 ALBERT、DistilBERT 和 ULMFiT 这样的架构/技术的发展,我们也进行了介绍。
-
新兴的体系结构是前述模型的后代,书中没有详细介绍但你应该知道的,包括 BART、T5、Longformer、Reformer、XLNet 等等。
-
在实践中部署这些模型时,意识到它们可能带来的潜在的道德和环境影响是很重要的。
-
近期对道德和环境影响的关注,以及希望将模型能力应用于智能手机和物联网,很可能会在不久的将来继续推动更高效的变压器架构的开发。
-
Sharir O.等,“训练 NLP 模型的成本:简要概述”,arXiv(2020)。
-
S.J. Pan 和 Q. Yang,“迁移学习概述”,IEEE 知识与数据工程交易(2009)。
-
S. Ruder,“自然语言处理的神经迁移学习”,爱尔兰加尔韦国立大学(2019)。
-
D. Wang 和 T. F. Zheng,“语音和语言处理的迁移学习”,2015 年亚太信号与信息处理协会年度峰会和会议(APSIPA)论文集。
-
Lipmann Richard 等,“DARPA 数据驱动模型发现(D3M)计划概述”,第 29 届神经信息处理系统(NeurIPS)会议论文集(2016)。
-
Yinhan Liu 等,“RoBERTa:稳健优化的 BERT 预训练方法”,arXiv(2019)。
-
Tom B. Brown 等,“语言模型是少样本学习者”,NeurIPS(2020)。
-
W. Fedus 等,“Switch 变压器:用简单高效的稀疏性扩展到万亿参数模型”,arXiv(2021)。
-
Z. Yang 等,“XLNet:用于语言理解的广义自回归预训练”,NeurIPS(2019)。
-
Z. Dai 等,“Transformer-XL:超越固定长度上下文的关注语言模型”,ACL(2019)。
-
M. Zaheer 等,“BigBird:更长序列的变压器”,arXiv(2020)。
-
I. Beltagy 等,“Longformer:长文档变压器”,arXiv(2020)。
-
N. Kitaev 等,“Reformer:高效变压器”,arXiv(2020)。
-
C. Raffel 等,“探索统一文本到文本变压器的迁移学习极限”,arXiv(2020)。
-
L. Xue 等,“mT5:大规模多语言预训练文本到文本变压器”,arXiv(2019)。
-
M. Lewis 等,“BART:用于自然语言生成、翻译和理解的去噪序列到序列预训练”,arXiv(2020)。
-
Y. Liu 等,“用于神经机器翻译的多语言去噪预训练”,arXiv(2020)。
-
G. Lample 和 A. Conneau,“跨语言语言模型预训练”,arXiv(2019)。
-
A. Conneau 等,“规模化的无监督跨语言表示学习”,arXiv(2019)。
-
J. Herzig 等,“TaPas:通过预训练进行弱监督表解析”,arXiv(2020)。
-
A. Dosovitskiy et al, “An Image Is Worth 16x16 Words: Transformers for Image Recognition at Scale,” arXiv (2020).
-
J. Builamwini and T. Gebru, “Gender Shades: Intersectional Accuracy Disparities in Commercial Gender Classification,” Journal of Machine Learning Research 81 (2018).
-
Tom B. Brown et al., “Language Models Are Few-Shot Learners,” NeurIPS (2020).
-
E. Strubell et al., “Energy and Policy Considerations for Deep Learning in NLP,” ACL (2019).
-
E. Bender et al., “On the Dangers of Stochastic Parrots: Can Language Models Be Too Big?” FAccT (2021).
-
Jesse Vig, “A Multiscale Visualization of Attention in the Transformer Model,” ACL (2019).
附录 A:Kaggle 入门指南
Kaggle 平台为数据科学和机器学习初学者提供了一个学习基本技能的绝佳途径。通过恰当地利用该平台,你有机会在各种数据集上练习各种问题,并与其他机器学习工程师展示和讨论你的工作。这有可能帮助你扩展你的专业网络。重要的是,该平台允许你在云中直接运行 Python 笔记本,这可以显著消除初学者的系统设置障碍。它还每周提供有限的免费 GPU 计算。这进一步使得本书讨论的工具和方法更加民主化。在全书中,我们鼓励你使用 Kaggle 来运行所呈现的代码。
另一个工具——Google Colab——同样提供免费的 GPU 计算,同时与 Google Drive 集成。然而,如果你必须选择一个工具,我会推荐 Kaggle,因为它具有社交性质,可以访问数据集、讨论和竞赛,这些都是非常宝贵的学习资源。当然,在实际情况下,大多数工程师可能会在某个时候同时使用两者,例如为了增加每周的免费 GPU 配额。
在这个附录中,我们试图提供一个简要的入门指南,可以帮助初学者逐步了解 Kaggle 的各种功能。我们将其分为两个部分。首先讨论 Kaggle 内核的概念以运行笔记本,然后查看竞赛、相关讨论和 Kaggle 博客功能。
A.1 Kaggle 内核提供的免费 GPU
如前所述,你可以在 Kaggle 免费使用云中直接运行 Python 代码。这些云笔记本有时被称为Kaggle 内核。在撰写本文时(2021 年 1 月),Kaggle 每周提供约 36 小时的 GPU 时数,你可以为任何你认为可能需要的笔记本启用它。我们将通过演示如何开始,逐步介绍一个对 Python 初学者有用的简单场景。
假设你是一个初学者,并且有兴趣使用这些内核学习基本的 Python 语法。一个很好的开始地方是访问www.kaggle.com/kernels
,然后搜索“Python 教程”。这个搜索结果可能如图 A.1 所示。
图 A.1 开始学习 Kaggle 内核和启动相关笔记本来学习新知识的最佳地点。前往www.kaggle.com/kernels
,然后搜索你感兴趣的主题。在图表中,我们展示了这样一个查询的结果列表,供初学者开始学习 Python 时参考。选择最合适的结果继续。或者使用新笔记本按钮创建一个新笔记本。
如图所示,搜索将返回一系列结果,您可以选择最符合您需求的一个。在这种情况下,初学者可能希望教程直接以 NLP 为重点开始,考虑到书籍的内容,因此可能会选择突出显示的教程笔记本。点击它将会显示相关的渲染笔记本,其中代表性视图如图 A.2 所示。
图 A.2 渲染笔记本的视图,突出显示可以执行的一些关键操作
请注意,所示的视图代表了您在单击书籍存储库中我们的伴随笔记本链接之一时将遇到的第一个视图。¹ 如图所示,笔记本已呈现,这意味着即使不运行代码,您也可以滚动并查看所有代码的代表性输出。
要运行代码,请点击复制并编辑按钮以创建自己版本的笔记本。生成的笔记本将具有相同的依赖项——在 Kaggle 环境中预安装的 Python 库版本和用于生成代表性笔记本输出的库。请注意,如果您点击了图 A.1 中的新笔记本按钮而不是选择复制现有笔记本,则依赖项将是 Kaggle 指定的最新依赖项。因此,您可能需要修改原始代码以使其正常工作,这会增加难度。要完成复制和编辑,或分叉,过程,将要求您提供登录信息。您可以注册您的电子邮件地址,也可以直接使用谷歌等社交账号登录。
为了精确复制我们为本书的伴随笔记本所使用的 Kaggle 环境,我们在伴随书籍存储库中包含了需求文件。请注意,这些需求文件仅用于在 Kaggle 笔记本上复制 Kaggle 环境的目的。如果您试图在本地计算机上使用它们,根据本地架构的不同,您可能会遇到额外的问题,并且可能需要对其进行修改。我们不支持这种模式,如果您正在追求它,请仅将需求文件用作指南。还要记住,并非每个列出的要求都需要在您的本地安装中使用。
单击“复制并编辑”将带您进入主工作区,如图 A.3 所示。如图所示,您可以通过左上角的按钮运行当前选择的单元格,也可以运行笔记本中的所有代码。在右侧面板上,您可以启用或禁用您的互联网连接。下载数据或安装软件包可能需要互联网连接。此右侧面板还包含在当前笔记本中启用 GPU 加速的选项,您需要在合理的时间内训练神经网络。您还将看到当前附加到笔记本的数据集,并且可以单击其中任何一个以转到数据集的描述。单击“添加数据”将打开一个搜索查询框,您将能够按关键字搜索感兴趣的数据集,以添加到当前笔记本。对于本书的所有伴侣笔记本,必要的数据已经附加到笔记本中。
图 A.3 使用 Kaggle 内核时的主工作区。在左上角,是运行笔记本的按钮。在右上角,是共享、保存、重新启动和关闭笔记本的选项。右侧面板包含连接到互联网的选项(用于安装软件包或下载数据)、启用/禁用当前笔记本的 GPU 加速以及添加数据。
在右上角,您可以选择笔记本的共享设置——根据您的项目需要,您可以将笔记本设置为仅自己可见,与其他用户私下共享,或者对外公开。我们所有的伴侣笔记本都是公开的,这样任何人都可以访问,但您可以将它们的分支设置为私有。重要的是,也在右上角,选择保存版本将弹出对话框以保存您的工作,如图 A.4 所示。
图 A.4 笔记本的保存选项。您可以提交代码并让其后续非交互式运行以供以后检查,也可以直接快速保存代码和当前输出。
正如图所示,有两种保存模式。快速保存模式将在版本名称文本博客中保存当前代码和输出。如果当前输出需要几个小时来生成,这将是正确的选择。保存并运行所有选项将保存代码并在后台非交互式地运行它。当运行长时间的训练作业时,例如五六个小时时,这特别有用。您可以关闭会话和所有窗口,并在需要检查结果时随时返回。最近运行/保存的检查通常可以在个性化的 URL www.kaggle.com//notebooks 上进行,其中 是您的用户名。对于我的用户名 azunre
,此页面的视图如图 A.5 所示。
图 A.5 近期运行/保存的检查通常可以在个性化 URL www.kaggle.com//notebooks 进行,其中 是您的用户名(此处显示的是我的用户名 azunre
)。
我们已经介绍了您需要了解的本书练习的主要特性。我们还没有涵盖的许多其他特性,Kaggle 经常会添加更多。通常,快速的谷歌搜索和一些坚持和实验的愿望就足以弄清楚如何使用任何这样的特性。
在下一节中,我们简要讨论 Kaggle 比赛。
A.2 比赛、讨论和博客
面对技术挑战的领先企业利用 Kaggle 通过为顶级创新提供重大奖金来刺激解决方案的研究和开发。让我们通过选择任何 Kaggle 页面左侧面板上可见的奖杯图标来检查 Kaggle 比赛页面,如图 A.6 所示。
图 A.6 通过选择任何 Kaggle 页面左侧面板上的奖杯图标进入比赛页面。我们可以看到一个比赛提供了总共 $100,000 的奖金——这个问题很可能对该行业非常有价值,以激励这样的投资!
您可以追踪这些比赛,了解行业中最紧迫的问题,同时可以访问基础数据以进行即时测试和实验。您可以按主题浏览当前和过去的比赛,以找到测试您可能有的任何想法的数据。您所需要做的就是将数据集附加到上一节介绍的笔记本中,更改一些路径,然后您应该准备产生一些初步的见解了。当然,如果您能够做到,赢得比赛对于获得金钱奖励来说是很棒的,但是您从实验、失败和再次尝试中获得的学习价值才是真正无价的。事实上,在我看来,通过排行榜的位置可能被认为是中等的比赛问题的解决方案,如果在实践中更容易部署和扩展,那么可能会导致实际影响。这是我个人关心的事情,因此我倾向于将精力集中在对我最感兴趣但我了解最少的问题上,以获取最大的学习价值。
点击任何比赛都会打开一个专门页面,在这里您可以浏览其描述、数据、排行榜,以及重要的是,图 A.7 中显示的“讨论”功能。
图 A.7 讨论功能使您能够与 Kaggle 社区的其他成员就您感兴趣的特定主题进行交流。聊天,扩展您的网络!
正如您可能在图 A.7 中看到的那样,这是一个与问题相关的讨论论坛。人们发布提示和入门笔记,提出重要问题,甚至可能由竞赛组织者回答。例如,如果您遇到特定竞赛数据的任何问题,您很有可能在这里找到答案。许多比赛提供最有价值贡献的奖励——通常是通过点赞来衡量的——这激励人们提供帮助。获胜者经常发布他们的解决方案,有时甚至作为您可以直接重新利用的笔记本。您甚至可以在此建立未来挑战的团队,并建立友谊。参与社区,并回馈一些您从中得到的东西,您可能会学到比其他方式更多。归根结底,科学仍然是一项社会活动,这使得 Kaggle 的这个功能尤为宝贵。
最后,Kaggle 在medium.com/kaggle-blog
上运行一个博客。大型比赛的获胜者经常在这里接受采访,分享他们可以与他人分享的技巧。教程经常发布在各种关键主题上。及时了解这些内容,以确保了解数据科学中最新的新兴研究趋势。
我们希望这个附录是一个有用的练习,并让您跟上了进展。继续前进,Kaggle!
附录 B:初级深度学习工具介绍
本附录涵盖
-
介绍本书中使用的五种基本算法和软件工具
-
对训练神经网络使用的算法——随机梯度下降进行概述
-
以 TensorFlow 开始进行神经网络建模
-
以 PyTorch 开始进行神经网络建模
-
对较高级别的神经网络建模框架 Keras、fast.ai 和 Hugging Face transformers 进行概述
在本附录中,我们试图对本书中使用的一些基本工具和概念提供一个简要的入门。对这些工具的简要介绍并不绝对必要,以便理解并充分从本书中受益。不过,阅读它们可以帮助一个新的深度学习领域的人快速融入,并且对他们可能是最有用的。
具体来说,我们首先向读者介绍了我们目前经历的深度学习革命背后的基本算法。当然,这就是用于训练神经网络的随机梯度下降算法。接着我们介绍了两种基本的神经网络建模框架,PyTorch 和 TensorFlow。然后我们介绍了这两个建模框架之上构建的三种工具,以提供一个更高级别的接口:Keras、fast.ai 和 Hugging Face transformers。这些工具相互补充,你可能在职业生涯的某个时候都会用到它们。我们对概念的阐述并不是穷尽的;它提供了一个“鸟瞰”为什么这些工具是需要的以及它们如何相互比较和相互补充。我们涉及了介绍性的概念,并引用了精心筛选的参考资料,以便深入研究。如果你觉得自己对这些工具的经验很少,你可能需要在开始阅读本书之前深入研究一下它们。
让我们从深度学习革命背后的算法引擎开始,即随机梯度下降算法。
B.1 随机梯度下降
神经网络有一组参数,称为权重,确定它将如何将输入数据转换为输出数据。确定哪组权重允许网络最接近地逼近一组训练数据称为训练网络。随机梯度下降是实现这一目标的方法。
让我们用W
表示权重,x
表示输入数据,y
表示输出数据。我们还用y_pred
表示神经网络对输入x
预测的输出数据。损失函数,用于衡量y
与y_pred
之间的接近程度,被表示为函数f
。注意它是x
、y
和W
的函数。随机梯度下降算法被制定为一个过程,以找到f
的最小值,即预测尽可能接近训练数据的位置。如果f
的梯度,用f'
表示,存在——如果它是一个可微函数——我们知道在这样的点上f'=0
。算法试图使用以下步骤序列找到这样的点:
-
从训练集中随机抽取一个输入-输出批次
x-y
的数据。这种随机性是算法被称为随机的原因。 -
使用当前
W
的值将输入通过网络以获得y_pred
.。 -
计算相应的损失函数值
f
。 -
计算损失函数相对于
W
的相应梯度f'
。 -
稍微改变
W
的方向以降低f
。步长的大小由算法的学习率决定,这是收敛的一个非常重要的超参数。
对于过度简单的单个权重的情况,该过程在图 B.1 中的第 2 步找到了算法的最小值。这张图受到弗朗索瓦·朱利叶的优秀书籍《深度学习与 Python》(Manning Publications,2018)中图 2.11 的启发,你也应该查看这本书以获得对该算法非常直观的解释。
图 B.1 展示了随机梯度下降在单个权重的过度简单情况下的示意图。在每一步中,计算相对于W
的梯度,并采取预先确定大小的步骤,由学习率决定,沿着损失函数梯度的相反方向。在这个假设的情景中,最小值在第 2 步找到。
有许多此算法的变体存在,包括 Adam、RMSprop 和 Adagrad。这些算法倾向于专注于避免局部最小值,并以各种方式(如学习率)进行自适应,以更快地收敛。动量的概念——它表现为每一步W
更新中的额外加法项——被几种这样的变体用来避免局部最小值。以下是一些最流行的变体及简要描述。
Adagrad 根据参数遇到的频率调整学习率。罕见的参数会以更大的步长进行更新,以实现平衡。该技术被用于训练 GloVe 静态词嵌入,该词嵌入在本书的第四章中描述。在这种情况下,它需要适当处理语言中的稀有单词。
RMSprop是为了解决 Adagrad 的学习速率经常过快下降的问题而开发的。我们可以通过将更新缩放为平方梯度的指数衰减平均值来部分缓解这个问题。
Adam表示自适应矩估计,也针对不同参数变化学习率。它与 RMSprop 具有相似之处,因为它使用衰减平方梯度平均值来执行更新。衰减平方梯度平均值的第一和二个时刻被估计,更新,然后用于在每个步骤中更新参数。这是尝试解决许多问题的流行算法。
Nadam是Nesterov 加速 Adam的缩写,采用称为Nesterov 加速梯度的创新来进一步改善 Adam 的收敛性。
因为这里的介绍只是一个简短的介绍,而不是详细的处理,所以我们不会更深入地探讨这些变体。这个主题已经被许多优秀的参考文献详细覆盖了¹,²,我们鼓励您深入研究以获得更好的理解。即使您可以在不深入了解这些变体的情况下使用现代框架,更好地了解它们可以帮助您调整超参数,并最终部署更好的模型。
B.2 TensorFlow
如前一节所述,了解损失函数相对于神经网络权重的梯度对于训练网络至关重要。由于现代神经网络是巨大的,达到数十亿个参数,因此手动计算此梯度函数将是不可能的。相反,使用 TensorFlow 等基本神经网络建模工具,通过应用取导数的链式法则自动找到梯度来计算它。这个过程被称为自动微分。
Tensorflow 中的基本数据结构是张量,通过构建一个计算图来对其进行操作。在框架的 1.x 版本中,通过多种 API 调用tf.*
来构建图,并使用Session
对象编译和执行它以产生数值。示例 B.1 中演示了使用此 API 定义图形并执行其梯度计算的说明性示例。具体来说,我们需要计算矩阵乘积z = x*y
,其中x
是简单的列向量,而y
是简单的行向量。我们还希望自动计算它相对于x
和y
的梯度。
示例 B.1 使用 TensorFlow 1 计算矩阵乘积 z = x * y 及其梯度
import tensorflow as tf ❶
tf.compat.v1.disable_eager_execution() ❷
x = tf.compat.v1.placeholder(tf.float32, name = "x") ❸
y = tf.compat.v1.placeholder(tf.float32, name = "y")
z = tf.multiply(x, y) # Define vector product graph
gradient = tf.gradients(z,[x, y],grad_ys=tf.eye(2)) ❹
with tf.compat.v1.Session() as session: ❺
z = session.run(z, feed_dict={x: [[1., 1.]], y: [[5.], [5.]]}) ❻
zG = session.run(gradient,feed_dict={x: [[1.,1.]], y: [[5.],[5.]]}) ❼
print("Product:") ❽
print(z)
print("\n\n")
print("Gradient of Product:")
print(zG)
print("\n\n")
❶总是先导入 TensorFlow
❷Eager 执行在 2.0 之前作为非默认值引入,因此在此确保其关闭。
❸定义留给后面分配值的向量变量占位符
❹ 定义了乘积的向量导数图,相对于 x 和 y。参数 grad_ys
乘以输出,可用于取链导数,因此我们将其设置为单位矩阵以无效果。
❺ 使用 Session 对象执行图
❻ 运行函数,指定占位符的值
❼ 运行梯度,指定占位符的值
❽ 显示结果
执行此代码将产生以下输出。您应该能够手动验证这些值是否正确,使用您的基本线性代数知识,这是本书的先决条件。我们还在书的伴随存储库中包含了一个 Kaggle 内核笔记本,执行这些命令。³
Product:
[[5\. 5.]
[5\. 5.]]
Gradient of Product:
[array([[5., 5.]], dtype=float32), array([[1.],
[1.]], dtype=float32)]
框架的 2.0 版及更高版本将更“Pythonic”的 eager execution 模式作为默认模式,这使得框架更易于使用。它现在还包括了 Keras,使得使用各种高级功能更加容易。下一个列表中显示了使用此 API 定义和执行与列表 B.1 中相同图形的说明性示例。更易于访问性立即变得明显,eager 模式使得立即执行变得可能,而不是通过图上的 Session
对象。
列表 B.2 计算矩阵乘积 z = x*y 及其在 TensorFlow 2 中的梯度
import tensorflow as tf
x = tf.convert_to_tensor([[1., 1.]]) ❶
y = tf.convert_to_tensor([[5.], [5.]]) ❷
with tf.GradientTape() as g: ❸
g.watch(x)
z = tf.multiply(x, y)
dz_dx = g.gradient(z, x, output_gradients=tf.eye(2))
with tf.GradientTape() as g: ❹
g.watch(y)
z = tf.multiply(x, y)
dz_dy = g.gradient(z, y, output_gradients=tf.eye(2))
print("Dot Product:") ❺
print(z)
print("\n\n")
print("Gradient of Product (dz_dx):")
print(dz_dx)
print("\n\n")
print("Gradient of Product (dz_dy):")
print(dz_dy)
❶ 列向量
❷ 行向量
❸ 这是如何相对于 x 计算自动导数的。这里的“Tape”一词表示所有状态都被“记录”,可以播放回来以检索我们需要的信息。
❹ 这是如何相对于 y 计算自动导数的。参数 output_gradients
乘以输出,可用于取链导数,因此我们将其设置为单位矩阵以无效果。
❺ 显示结果
执行此代码应产生与之前相同的输出值。
框架按层次结构组织,具有高级和低级 API,如图 B.2 所示。
图 B.2 TensorFlow 框架的分层组织示意图
此图受官方 TensorFlow 文档第 1 图的影响。⁴ 如果您是初学者,并且想更详细地浏览此参考资料,这可能会有所帮助。在附录的最后一节将进一步讨论 TensorFlow 版本的 Keras,该版本也显示在图中。
更好地了解 TensorFlow 各种特性的方法是动手尝试相关的 Kaggle 内核/笔记本教程,如附录 A 中所述。特别是,只需访问 kaggle.com 并搜索“TensorFlow 教程”即可找到大量精彩的教程,您可以选择最适合您的学习风格和经验水平的内容。www.kaggle.com/akashkr/tensorflow-tutorial
上的教程似乎对初学者很有帮助。
B.3 PyTorch
这个框架在 TensorFlow 之后发布(2016 年对比 2015 年)。然而,它很快就成为许多研究人员首选的框架,如 TensorFlow 相对 PyTorch 在学术论文引用方面的相对流行度下降所证明的那样。⁵ 这种增加的流行度被普遍认为是因为该框架能够在运行时以编程方式修改各种 PyTorch 模型对象,从而在研究过程中更容易进行代码优化。事实上,TensorFlow 2.0 中的急切模式的引入被普遍认为受到了 PyTorch 成功的影响。尽管在 TensorFlow 2.0 发布后,这两个平台之间的差异变得更小了,但普遍的观点是,研究人员更喜欢 PyTorch,而 TensorFlow 更适用于在生产中部署。
作为例证,我们在 PyTorch 中执行与清单 B.1 和 B.2 相同的操作序列 —— 向量乘法及其导数,这是神经网络模型的核心 —— 并在下一个清单中展示相应的代码。
清单 B.3 在 PyTorch 中计算矩阵乘积 z = x*y 及其梯度
import torch ❶
from torch.autograd import grad ❷
import numpy as np # tensors will be built from numpy arrays
x = torch.from_numpy(np.array([[1., 1.]])) ❸
y = torch.from_numpy(np.array([[5.], [5.]])) ❹
x.requires_grad = True ❺
y.requires_grad = True
z = torch.mul(x, y) ❻
zGx = grad(outputs=z, inputs=x,grad_outputs=torch.eye(2),retain_graph=True)❼
zGy = grad(outputs=z, inputs=y,grad_outputs=torch.eye(2)) ❽
print("Dot Product") ❾
print(z)
print("Gradient of Product(dz_dx)")
print(zGx)
print("\n\n")
print("Gradient of Product (dz_dy):")
print(zGy)
❶ 总是首先导入 PyTorch。
❷ 导入 grad 函数进行自动微分
❸ 列向量
❹ 行向量
❺ 这确保了可以针对 x 计算梯度。
❻ 计算乘积
❼ 针对 x 计算自动导数。retain_graph 确保我们可以继续进行导数计算;否则,“Tape” 将被丢弃,无法回放。
❽ 针对 y 计算自动导数。参数 grad_outputs 乘以输出,可以用于进行链式导数,因此我们将其设置为单位矩阵以无效果。
❾ 显示结果
执行此代码应该产生与前一节相同的结果。我们还在本书的伴随存储库中包含了一个 Kaggle 内核笔记本,执行这些命令。
和以前一样,我们建议通过一些 Kaggle 内核来熟悉 PyTorch 的各个方面,如果你觉得自己可能需要更多经验的话。www.kaggle.com/kanncaa1/pytorch-tutorial-for-deep-learning-lovers
上的教程似乎是初学者的好选择。
B.4 由 Hugging Face 提供的 Keras、fast.ai 和 Transformers
正如在附录中早前提到的,Keras 库是一个更高级的神经网络建模框架,现在也包含在 TensorFlow 2.0 及更高版本中。通过使用它,你可以在 TensorFlow 和 PyTorch 中指定神经网络架构,只需从一个 API 中切换后端即可!它与 TensorFlow 预先打包在一起,如我们在图 B.2 中所示。与 TensorFlow 和 PyTorch 相比,其 API 相对简单,这使得它非常受欢迎。存在许多优秀的学习资源,也许最好的资源之一是作者自己的书籍。这也是学习 TensorFlow 和神经网络的绝佳参考资料,如果你觉得需要复习这些主题,我们强烈推荐它。你也可以通过一些 Kaggle 内核来学习一些基础知识,例如 www.kaggle.com/prashant111/keras-basics-for-beginners
上的教程只是一个很好的例子。
在该领域另一个流行的高级建模 API 是 fast.ai。这个库是作为同名大型在线课程(MOOC)的伴侣而开发的,并以一种极易使用的方式实现了最先进的方法。其动机之一是将这些工具普及到发展中国家。该库的一个受欢迎功能是其学习率确定实用程序,我们在本书的第九章中使用了它。该框架用于自然语言处理和计算机视觉,并运行在 PyTorch 上。自然,学习该库的最佳参考资料是 fast.ai MOOC 本身。这门免费课程涵盖了神经网络和深度学习的基础知识,是另一个我们强烈推荐的精彩资源。该库通过定义其自己的一套数据结构来实现简单化,这些数据结构处理了用户的大量样板代码。另一方面,这可能会使其对非标准用例的定制更加困难。在作者的经验中,这是一个拥有的绝佳工具。
最后,Hugging Face 的 Transformers 是一个专门针对基于 Transformer 模型的高级建模框架。这些模型已经成为现代自然语言处理中可能是最重要的架构。你将在整本书中准确了解其中的原因。这个库可能是当今领域中最受欢迎的库,因为使用它部署这些模型非常简单。在这个库存在之前,使用 Keras、TensorFlow 和/或 PyTorch 部署 Transformer 模型相当繁琐。该库在某些情况下简化了这个过程,只需几行 Python 代码,导致其受欢迎程度激增,并被认为是现代自然语言处理从业者不可或缺的工具。由于 API 的透明度和简单性,你可能只需阅读本书并通过相关示例进行工作,甚至无需任何先前的使用经验即可。如需进一步参考,请查看作者在 GitHub 上的入门笔记⁹以及官方快速入门文档¹⁰。
-
F. Chollet,《Deep Learning with Python》(Manning Publications,2018)。
-
S. Ruder,“梯度下降优化算法概述”,arXiv(2016)。
-
developers.google.com/machine-learning/crash-course/first-steps-with-tensorflow/toolkit
-
F. Chollet,《Deep Learning with Python》(Manning Publications,2018)。