详解谷歌最强NLP模型BERT(理论+实战)
导语
Google BERT 模型最近横扫了各大评测任务,在多项任务中取得了最好的结果,而且很多任务比之前最好的系统都提高了非常多,可以说是深度学习最近几年在 NLP的一大突破。但它并不是凭空出现的,最近一年大家都非常关注的 UnsupervisedSentence Embedding 取得了很大的进展,包括 ELMo 和 OpenAI GPT 等模型都取得了很好的结果。而 BERT 在它们的基础上改进了语言模型单向信息流的问题,并且借助 Google 强大的工程能力和计算资源的优势,从而取得了巨大的突破。
本文从理论和编程实战角度详细的介绍 BERT 和它之前的相关的模型,包括
Transformer 模型。希望读者阅读本文之后既能理解模型的原理,同时又能很快的把模型用于解决实际问题。本文假设读者了解基本的深度学习知识包括 RNN/LSTM、Encoder-Decoder 和 Attention 等。
Sentence Embedding 简介
前面我们介绍了 Word Embedding,怎么把一个词表示成一个稠密的向量。Embedding几乎是在 NLP 任务使用深度学习的标准步骤。我们可以通过 Word2Vec、GloVe 等从未标注数据无监督的学习到词的 Embedding,然后把它用到不同的特定任务中。这种方法得到的 Embedding 叫作预训练的 (pretrained)Embedding。如果特定任务训练数据较多,那么我们可以用预训练的 Embedding 来初始化模型的 Embedding,然后用特定任务的监督数据来 fine-tuning。如果监督数据较少,我们可以固定 (fix)Embedding,只让模型学习其它的参数。这也可以看成一种 Transfer Learning。
但是 NLP 任务的输入通常是句子,比如情感分类,输入是一个句子,输出是正向或者负向的情感。我们需要一种机制表示一个句子,最常见的方法是使用 CNN 或者 RNN 对句子进行编码。用来编码的模块叫作编码器 (Encoder),编码的输出是一个向量。和词向量一样,我们期望这个向量能够很好的把一个句子映射到一个语义空间,相似的句子映射到相近的地方。编码句子比编码词更加复杂,因为词组成句子是有结构的 (我们之前的 Paring 其实就是寻找这种结构),两个句子即使词完全相同但是词的顺序不同,语义也可能相差很大。
传统的编码器都是用特定任务的监督数据训练出来的,它编码的目的是为了优化具体这个任务。因此它编码出的向量是适合这个任务的——如果这个任务很关注词序,那么它在编码的使用也会关注词序;如果这个任务关注构词法,那么学到的编码器也需要关注构词法。
但是监督数据总是很少的,获取的成本也极高。因此最近 (2018 年上半年),无监督的通用 (universal) 的句子编码器成为热点并且有了一些进展。无监督的意思是可以使用未标注的原始数据来学习编码器 (的参数),而通用的意思是学习到的编码器不需要 (太多的)fine-tuning 就可以直接用到所有 (只是是很多) 不同的任务中,并且能得到很好的效果。
评测工具
在介绍 Unsupervised Sentence Embedding 的具体算法之前我们先介绍两个评测工具(平台)。
SentEval
- 简介
Sentence Embedding(包括 Word Embedding) 通常有两类评价方法:intrinsic 和 ex-trinsic。前者只评价 Embedding 本身,比如让人来主观评价。而后者通过下游 (Downstream) 的任务间接的来评价 Embedding 的好坏。前一种方法耗费人力,而且我们学习 Embedding 的目的也是为了解决后面的真实问题,因此 extrinsic 的评价更加重要。但是下游的任务通常很复杂,Embedding 只是其中的一个环节,因此很难说明最终效果的提高就是由于 Embedding 带来的,也许只是某个预处理或者超参数的调节带来的提高,但是却可能被作者认为是 Embedding 的功劳。另外下游任务很多,很多文章的结果也很难比较。
为了解决这些问题,Facebook 做了 SentEval 这个工具。这是一个用于评估Universal Sentence Representation 的工具,所谓的 Universal Sentence Representation是指与特定任务无关的通用的句子表示 (Embedding) 方法。为了保证公平公正,这个工具只评价句子的 Embedding,对于具体的任务,大家都使用相同的预处理,网络结构和后处理,从而能够保证比较公平的评测。
SentEval 任务分类
SentEval 任务分为如下类别:
- 分类问题 (包括二分类和多分类)
- Natural Language Inference
- 语义相似度计算
- 图像检索 (Image Retrieval)
图 17.1: SentEval 的分类任务
分类很简单,输入是一个字符串 (一个句子或者文章),输出是一个分类标签。所以任务如图17.1所示。包括情感分类、句子类型分类等等任务。
Natural Language Inference(NLI) 任务也叫 recognizing textual entailment(RTE),它的输入是两个句子,需要机器判断第一个句子和第二个句子的关系。它们的关系通常有 3 种:矛盾 (contradiction)、无关 (neutral) 和蕴含 (entailment)。
SNLI(https://nlp.stanford.edu/projects/snli/) 是很常用的 NLI 数据集,示例是来自这个数据集的例子。比如下面的两个句子是矛盾的:
A man inspects the uniform of a figure in some East Asian country.The man is sleeping.一个人不能同时在观察和睡觉。而下面两个句子的关系是无关的:A smiling costumed woman is holding an umbrella.A happy woman in a fairy costume holds an umbrella.而下面两个的第一个句子蕴含了第二个句子:A soccer game with multiple males playing.Some men are playing a sport.语义相似度计算的输入是两个句子,输出是它们的相似度,一般相似度会分为几个程度,所以输出也是标签。当然最简单的是分成两类——相似与不相似,比如MRPC 就是这样的任务,这个任务又叫 Paraphrase Detection,判断两个句子是否同义复写。
Image Retrieval 的输入是一幅图片和一段文字,如果文字能很好的描述图片的内容,那么输出一个高的分值,否则输出低分。
SentEval 包括的 NLI 和图像检索任务如图17.2所示。
图 17.2: SentEval 的 NLI 和 Image Retrieval 任务
SentEval 的用法
SentEval 依赖 NumPy/SciPy、PyTorch(>=0.4.0) 和 scikit-learn(>=0.18.0)。
然后从https://github.com/facebookresearch/SentEval.git clone 代码。SentEval 提供了一些baseline 系统,包括 bow、infersent 和 skipthought 等等。读者如果实现了一种新的Sentence Embedding 算法,那么可以参考 baseline 的代码用 SentEval 来评价算法的好坏。
我们这里只介绍最简单的 bow 的用法,它就是把 Pretraining 的 Word Embedding加起来得到 Sentence Embedding。
我们首先下载 fasttext 的 Embedding:
然后运行:
main 函数代码为:
首先构造 senteval.engine.SE,然后列举需要跑的 task,最后调用 se.eval 得到结果。
构造 senteval.engine.SE 需要传入 3 个参数,params_senteval, batcher 和 prepare。params_senteval 是控制 SentEval 模型训练的一些超参数。比如 bow.py 里的:
而后两个参数是函数,我们先看 prepare:
这个函数相当于初始化的回调函数,参数会传入 params 和 samples,samples 就是所有的句子,我们需要根据这些句子来做一些初始化的工作,结果存在 params 里,后面会用到。这里我们用 samples 构造 word2id——word 到 id 的映射,另外根据word2id,从预训练的词向量里提取需要的词向量 (因为预训练的词向量有很多词,但是在某个具体任务中用到的词是有限的,我们只需要提取需要的部分),另外把词向量的维度保持到 params 里。
batcher 函数的输入参数是前面的 params 和 batch,batch 就是句子列表,我们需要对它做 Sentence Embedding,这里的实现很简单,就是把词向量加起来求平均值得到句子向量。
GLUE
Facebook 搞了个标准,Google 也要来一个,所以就有了 GLUE(https://gluebenchmark.com/)。GLUE 是 General Language Understanding Evaluation 的缩写。它们之间很多的任务都是一样的,我们这里就不详细介绍了,感兴趣的读者可以参考论文”GLUE: A Multi-Task Benchmark and Analysis Platform for Natural LanguageUnderstanding”。
Transformer
- 简介
Transformer 模型来自与论文 Attention Is All You Need(https://arxiv.org/abs/1706.03762)。
这个模型最初是为了提高机器翻译的效率,它的 Self-Attention 机制和 Position Encoding 可以替代 RNN。因为 RNN 是顺序执行的,t 时刻没有处理完成就不能处理 t+1 时刻,因此很难并行。但是后来发现 Self-Attention 效果很好,在很多其它的地方也可以是 Transformer 模型。
- 图解
我们首先通过图的方式直观的解释 Transformer 模型的基本原理,这部分内容主要来自文章 The Illustrated Transformer(http://jalammar.github.io/illustratedtransformer/)。
模型概览
我们首先把模型看成一个黑盒子,如图15.51所示,对于机器翻译来说,它的输入是源语言 (法语) 的句子,输出是目标语言 (英语) 的句子。
把黑盒子稍微打开一点,Transformer(或者任何的 NMT 系统) 都可以分成
Encoder 和 Decoder 两个部分,如图15.52所示。
再展开一点,Encoder 由很多 (6 个) 结构一样的 Encoder 堆叠 (stack) 而成,Decoder 也是一样。如图15.53所示。注意:每一个 Encoder 的输入是下一层 Encoder输出,最底层 Encoder 的输入是原始的输入 (法语句子);Decoder 也是类似,但是最后一层 Encoder 的输出会输入给每一个 Decoder 层,这是 Attention 机制的要求。
每一层的 Encoder 都是相同的结构,它有一个 Self-Attention 层和一个前馈网络(全连接网络) 组成,15.54如图所示。每一层的 Decoder 也是相同的结果,它除了 Self-Attention 层和全连接层之外还多了一个普通的 Attention 层,这个 Attention 层使得 Decoder 在解码时会考虑最后一层 Encoder 所有时刻的输出。它的结构如图17.3所示。
加入 Tensor
前面的图示只是说明了 Transformer 的模块,接下来我们加入 Tensor,了解这些模块是怎么串联起来的。
输入的句子是一个词 (ID) 的序列,我们首先通过 Embedding 把它变成一个连续稠密的向量,如图17.4所示。
Embedding 之后的序列会输入 Encoder,首先经过 Self-Attention 层然后再经过全连接层,如图17.5所示。
我们在计算 zi 是需要依赖所有时刻的输入 x1, ..., xn,不过我们可以用矩阵运算一下子把所有的 zi 计算出来 (后面介绍)。而全连接网络的计算则完全是独立的,计算 i 时刻的输出只需要输入 zi 就足够了,因此很容易并行计算。
图17.6更加明确的表达了这一点。图中 Self-Attention 层是一个大的方框,表示它的输入是所有的 x1, ..., xn,输出是 z1, ..., zn。而全连接层每个时刻是一个方框,表示计算 ri 只需要 zi。此外,前一层的输出 r1, ...,rn 直接输入到下一层。
Self-Attention 简介
比如我们要翻译如下句子”The animal didn’t cross the street because it was too tired”(这个动物无法穿越马路,因为它太类了)。这里的 it 到底指代什么呢,是animal 还是 street?要知道具体的指代,我们需要在理解 it 的时候同时关注所有的单词,重点是 animal、street 和 tired,然后根据知识 (常识) 我们知道只有 animal 才能tired,而 stree 是不能 tired 的。Self-Attention 运行 Encoder 在编码一个词的时候考虑句子中所有其它的词,从而确定怎么编码当前词。如果把 tired 换成 narrow,那么it 就指代的是 street 了。
而 LSTM(即使是双向的) 是无法实现上面的逻辑的。为什么呢?比如前向的
LSTM,我们在编码 it 的时候根本没有看到后面是 tired 还是 narrow,所有它无法把it 编码成哪个词。而后向的 LSTM 呢?当然它看到了 tired,但是到 it 的时候它还没有看到 animal 和 street 这两个单词,当然就更无法编码 it 的内容了。
当然多层的 LSTM 理论上是可以编码这个语义的,它需要下层的 LSTM 同时编码了 animal 和 street 以及 tired 三个词的语义,然后由更高层的 LSTM 来把 it 编码成 animal 的语义。但是这样模型更加复杂。
下图17.7是模型的最上一层 (下标 0 是第一层,5 是第六层)Encoder 的Attention可视化图。这是 tensor2tensor 这个工具输出的内容。我们可以看到,在编码 it 的时候有一个 Attention Head(后面会讲到) 注意到了Animal,因此编码后的 it 有 Animal的语义。
Self-Attention 详细介绍
下面我们详细的介绍 Self-Attention 是怎么计算的,首先介绍向量的形式逐个时刻计算,这便于理解,接下来我们把它写出矩阵的形式一次计算所有时刻的结果。
对于输入的每一个向量 (第一层是词的 Embedding,其它层是前一层的输出),我们首先需要生成 3 个新的向量 Q、K 和 V,分别代表查询 (Query) 向量、Key 向量和 Value 向量。Q 表示为了编码当前词,需要去注意 (attend to) 其它 (其实也包括它自己) 的词,我们需要有一个查询向量。而 Key 向量可以任务是这个词的关键的用于被检索的信息,而 Value 向量是真正的内容。
我们对比一些普通的 Attention(Luong 2015),使用内积计算 energy 的情况。如图17.8所示,在这里,每个向量的 Key 和 Value 向量都是它本身,而 Q 是当前隐状态 ht,计算 energy etj 的时候我们计算 Q(ht) 和Key(barhj)。然后用 softmax 变成概率,最后把所有的 barhj 加权平均得到 context 向量。
而 Self-Attention 里的 Query 不是隐状态,并且来自当前输入向量本身,因此叫作 Self-Attention。另外 Key 和 Value 都不是输入向量,而是输入向量做了一下线性变换。
当然理论上这个线性变换矩阵可以是 Identity 矩阵,也就是使得Key=Value=输入向量。因此可以认为普通的 Attention 是这里的特例。这样做的好处是系统可以学习的,这样它可以根据数据从输入向量中提取最适合作为 Key(可以看成一种索引)和 Value 的部分。类似的,Query 也是对输入向量做一下线性变换,它让系统可以根据任务学习出最适合的 Query,从而可以注意到 (attend to) 特定的内容。
具体的计算过程如图17.9所示。比如图中的输入是两个词”thinking” 和”machines”,我们对它们进行Embedding(这是第一层,如果是后面的层,直接输入就是向量了),得到向量 x1, x2。接着我们用 3 个矩阵分别对它们进行变换,得到向量 q1, k1, v1 和q2, k2, v2。比如 q1 = x1WQ,图中 x1 的 shape 是 1x4,WQ 是 4x3,得到的 q1 是 1x3。其它的计算也是类似的,为了能够使得 Key 和 Query 可以内积,我们要求 WK 和WQ 的 shape 是一样的,但是并不要求 WV 和它们一定一样 (虽然实际论文实现是一样的)。每个时刻 t 都计算出 Qt, Kt, Vt 之后,我们就可以来计算 Self-Attention 了。以第一个时刻为了,我们首先计算 q1 和 k1, k2 的内积,得到 score,过程如图17.10所示。
接下来使用 softmax 把得分变成概率,注意这里把得分除以
之后再计算的 softmax,根据论文的说法,这样计算梯度时会更加文档 (stable)。计算过程如图17.11所示。
接下来用 softmax 得到的概率对所有时刻的 V 求加权平均,这样就可以认为得到的向量根据 Self-Attention 的概率综合考虑了所有时刻的输入信息,计算过程如图17.12所示。
这里只是演示了计算第一个时刻的过程,计算其它时刻的过程是完全一样的。
矩阵计算
前面介绍的方法需要一个循环遍历所有的时刻 t 计算得到 zt,我们可以把上面的向量计算变成矩阵的形式,从而一次计算出所有时刻的输出,这样的矩阵运算可以充分利用硬件资源 (包括一些软件的优化),从而效率更高。
第一步还是计算 Q、K 和 V,不过不是计算某个时刻的 qt, kt, vt 了,而是一次计算所有时刻的 Q、K 和 V。
计算过程如图17.13所示。这里的输入是一个矩阵,矩阵的第 i 行表示第 i 个时刻的输入 xi。
接下来就是计算 Q 和 K 得到 score,然后除以
,然后再 softmax,最后加权平均得到输出。全过程如图17.14所示。
Multi-Head Attention
这篇论文还提出了 Multi-Head Attention 的概念。其实很简单,前面定义的一组 Q、K 和 V 可以让一个词 attend to 相关的词,我们可以定义多组 Q、K 和 V,它们分别可以关注不同的上下文。
计算 Q、K 和 V 的过程还是一样,这不过现在变换矩阵从一组
变成了多组
,
,...。如图所示。
对于输入矩阵 (time_step, num_input),每一组 Q、K 和 V 都可以得到一个输
出矩阵 Z(time_step, num_features)。如图17.16所示。
但是后面的全连接网络需要的输入是一个矩阵而不是多个矩阵,因此我们可以
把多个 head 输出的 Z 按照第二个维度拼接起来,但是这样的特征有一些多,因此Transformer 又用了一个线性变换 (矩阵 WO) 对它进行了压缩。这个过程如图17.17所示。
上面的步骤涉及很多步骤和矩阵运算,我们用一张大图把整个过程表示出来,如图17.18所示。我们已经学习过来 Transformer 的 Self-Attention 机制,下面我们通过一个具体的例子来看看不同的 Attention Head 到底学习到了什么样的语义。
图17.19是一个 Attention Head 学习到的语义,我们可以看到对于 it 一个 Head会注意到”the animal” 而另外一个 Head 会注意到”tired”。
如果把所有的 Head 混在一起,如图17.20所示,那么就很难理解它到底注意的是什么内容。从上面两图的对比也能看出使用多个 Head 的好处——每个 Head(在数据的驱动下) 学习到不同的语义。
位置编码 (Positional Encoding)
注意:这是原始论文使用的位置编码方法,而在 BERT 模型里,使用的是简单的可以学习的 Embedding,和 Word Embedding 一样,只不过输入是位置而不是词而已。
我们的目的是用 Self-Attention 替代 RNN,RNN 能够记住过去的信息,这可以通过 Self-Attention“实时”的注意相关的任何词来实现等价 (甚至更好) 的效果。RNN还有一个特定就是能考虑词的顺序 (位置) 关系,一个句子即使词完全是相同的但是语义可能完全不同,比如” 北京到上海的机票” 与” 上海到北京的机票”,它们的语义就有很大的差别。我们上面的介绍的 Self-Attention 是不考虑词的顺序的,如果模型参数固定了,上面两个句子的北京都会被编码成相同的向量。但是实际上我们可以期望这两个北京编码的结果不同,前者可能需要编码出发城市的语义,而后者需要包含目的城市的语义。而 RNN 是可以 (至少是可能) 学到这一点的。当然 RNN 为了实现这一点的代价就是顺序处理,很难并行。
为了解决这个问题,我们需要引入位置编码,也就是 t 时刻的输入,除了Embedding 之外 (这是与位置无关的),我们还引入一个向量,这个向量是与 t 有关的,我们把 Embedding 和位置编码向量加起来作为模型的输入。这样的话如果两个词在不同的位置出现了,虽然它们的 Embedding 是相同的,但是由于位置编码不同,最终得到的向量也是不同的。
位置编码有很多方法,其中需要考虑的一个重要因素就是需要它编码的是相对位置的关系。比如两个句子:” 北京到上海的机票” 和” 你好,我们要一张北京到上海的机票”。显然加入位置编码之后,两个北京的向量是不同的了,两个上海的向量也是不同的了,但是我们期望 Query(北京 1)*Key(上海 1) 却是等于 Query(北京 2)*Key(上海 2) 的。具体的编码算法我们在代码部分再介绍。
位置编码加入模型如图17.21所示。
一个具体的位置编码的例子如图17.22所示。
残差连接
每个 Self-Attention 层都会加一个残差连接,然后是一个 LayerNorm 层,如图17.23所示。
图17.24展示了更多细节:输入 x1, x2 经 self-attention 层之后变成 z1, z2,然后和残差连接的输入 x1, x2 加起来,然后经过 LayerNorm 层输出给全连接层。全连接层也是有一个残差连接和一个 LayerNorm 层,最后再输出给上一层。
Decoder 和 Encoder 是类似的,如图17.25所示,区别在于它多了一个EncoderDecoder Attention 层,这个层的输入除了来自 Self-Attention 之外还有 Encoder 最后一层的所有时刻的输出。
Encoder-Decoder Attention 层的 Query 来自下一层,而 Key 和 Value 则来自Encoder 的输出。
代码
本节内容来自
http://nlp.seas.harvard.edu/2018/04/03/attention.html。读者可以从https://github.com/harvardnlp/annotated-transformer.git 下载代码。这篇文章原名叫作《The Annotated Transformer》。相当于原始论文的读书笔记,但是不同之处在于它不但详细的解释论文,而且还用代码实现了论文的模型。
注意:本书并不没有完全翻译这篇文章,而是根据作者自己的理解来分析和阅读其源代码。而 Transformer 的原来在前面的图解部分已经分析的很详细了,因此这里关注的重点是代码。网上有很多 Transformer 的源代码,也有一些比较大的库包含了Transformer 的实现,比如 Tensor2Tensor 和 OpenNMT 等等。作者选择这个实现的原因是它是一个单独的 ipynb 文件,如果我们要实际使用非常简单,复制粘贴代码就行了。而 Tensor2Tensor 或者 OpenNMT 包含了太多其它的东西,做了过多的抽象。
虽然代码质量和重用性更好,但是对于理解论文来说这是不必要的,并且增加了理解的难度。
运行
这里的代码需要 PyTorch-0.3.0,所以建议读者使用 virtualenv 安装。另外为了在Jupyter notebook 里使用这个 virtualenv,需要执行如下命令:
背景介绍
前面提到过 RNN 等模型的缺点是需要顺序计算,从而很难并行。因此出现了Extended Neural GPU、ByteNet 和 ConvS2S 等网络模型。这些模型都是以 CNN 为基础,这比较容易并行。但是和 RNN 相比,它较难学习到长距离的依赖关系。
本文的 Transformer 使用了 Self-Attention 机制,它在编码每一词的时候都能够注意 (attend to) 整个句子,从而可以解决长距离依赖的问题,同时计算 Self-Attention可以用矩阵乘法一次计算所有的时刻,因此可以充分利用计算资源 (CPU/GPU 上的矩阵运算都是充分优化和高度并行的)。
模型结构
目前的主流神经序列转换 (neural sequence transduction) 模型都是基于 EncoderDecoder 结构的。所谓的序列转换模型就是把一个输入序列转换成另外一个输出序列,它们的长度很可能是不同的。比如基于神经网络的机器翻译,输入是法语句子,输出是英语句子,这就是一个序列转换模型。类似的包括文本摘要、对话等问题都可以看成序列转换问题。我们这里主要关注机器翻译,但是任何输入是一个序列输出是另外一个序列的问题都可以考虑使用 Encoder-Decoder 模型。
Encoder 讲输入序列 (x1, ..., xn) 映射 (编码) 成一个连续的序列 z = (z1, ..., zn)。而Decoder 根据 z 来解码得到输出序列 y1, ..., ym。Decoder 是自回归的 (auto-regressive)——它会把前一个时刻的输出作为当前时刻的输入。
Encoder-Decoder 结构模型的代码如下:
EncoderDecoder 定义了一种通用的 Encoder-Decoder 架构,具体的 Encoder、Decoder、src_embed、target_embed 和 generator 都是构造函数传入的参数。这样我们做实验更换不同的组件就会更加方便。
注意:Generator 返回的是 softmax 的 log 值。在 PyTorch 里为了计算交叉熵损失,有两种方法。第一种方法是使用 nn.CrossEntropyLoss(),一种是使用 NLLLoss()。第一种方法更加容易懂,但是在很多开源代码里第二种更常见,原因可能是它后来才有,大家都习惯了使用 NLLLoss。
我们先看 CrossEntropyLoss,它就是计算交叉熵损失函数,比如:
比如上面的代码,假设是 5 分类问题,x 表示模型的输出 logits(batch=1),而 y 是真实分类的下标 (0-4)。
实际的计算过程为:
比如 logits 是 [0,1,2,3,4],真实分类是 3,那么上式就是:
因此我们也可以使用 NLLLoss() 配合 F.log_softmax 函数 (或者nn.LogSoftmax,这不是一个函数而是一个 Module 了) 来实现一样的效果:
NLLLoss(Negative Log Likelihood Loss) 是计算负 log 似然损失。它输入的 x 是log_softmax 之后的结果 (长度为 5 的数组),y 是真实分类 (0-4),输出就是 x[y]。因此上面的代码
Transformer 模型也是遵循上面的架构,只不过它的 Encoder 是 N(6) 个 EncoderLayer 组成,每个 EncoderLayer 包含一个 Self-Attention SubLayer 层和一个全连接SubLayer 层。而它的 Decoder 也是 N(6) 个 DecoderLayer 组成,每个 DecoderLayer包含一个 Self-Attention SubLayer 层、Attention SubLayer 层和全连接 SubLayer 层。如图17.26所示。
未完......
福利
想获得关于《深度学习理论与实战》的更多内容,请在微信公众号会话回复:深度学习,获取本文的PDF文件。
(*本文仅代表作者观点,转载请联系原作者)