Transformer 零基础解析教程,剥洋葱般层层剖析内在原理(2/4)
导航
这篇文章是关于Transformer的原理详细解读。
本博客的 Transformer 系列文章共计四篇,导航如下:
Transformer之前的翻译模型
在Transformer之前,递归神经网络(RNN)一直是处理序列数据的首选方法,大家做机器翻译用的最多的就是基于RNN的Encoder-Decoder模型。
![](https://jinglescode.github.io/assets/img/posts/illustrated-guide-transformer-02.jpg)
图1: RNN的工作方式
输入:
输入向量 \(\vec{x_t}\) (编码词)
隐藏状态向量 \(\vec{h_{t-1}}\)(包含当前块之前的序列状态)
输出:
- 输出向量 \(\vec{o_t}\)
权重:
\({W}\)—— \(\vec{x_t}\) 和 \(\vec{h_t}\) 之间的权重
\({V}\)—— \(\vec{ h_{t-1} }\) 和 \(\vec{h_t}\) 之间的权重
\({U}\)—— \(\vec{h_t}\) 和 \(\vec{o_t}\) 之间的权重
RNN的工作方式类似于前馈神经网络,它会将输入序列一个接一个地读取。因此在基于RNN的Encoder-Decoder模型中,编码器的目标是从顺序输入中提取数据,并将其编码为向量(即输入的表示形式)。而解码器代替输出固定长度向量的分类器,与单独使用输入中的每个符号的编码器一样,解码器在多个时间步长内生成每个输出符号。
![](https://jinglescode.github.io/assets/img/posts/illustrated-guide-transformer-03.jpg)
图2: Encoder-Decoder进行英-法翻译的例子
例如,在机器翻译中,输入是英文句子,输出是翻译出的法语句子。Encoder将按顺序展开每个单词,并形成输入英文句子的固定长度向量表示(也就是上篇博客中的\(C\))。然后Decoder将固定长度的向量表示作为输入,依次产生每个法语单词,形成翻译后的法语句子。
原有模型的缺陷
原有的模型,即基于RNN的Encoder-Decoder存在一些问题:
训练速度慢:输入数据需要一个接一个地顺序处理,这种串行的循环过程不适用于擅长并行计算的GPU。
难以处理长序列:
- 如果输入序列太长,会出现梯度消失和爆炸问题。一般在训练过程中会在loss中看到NaN(Not a Number)。这些也称为 RNN 中的长期依赖问题。
- 上下文向量长度固定,使用固定长度的向量表示输入序列来解码一个全新的句子是很困难的。如果输入序列很大,则上下文向量无法存储所有信息。此外,也很难区分具有相似单词但具有不同含义的句子。
第1个问题很好理解,不过第2个问题中,关于梯度消失和爆炸的问题,可以先看看以下的补充说明。
什么是梯度消失和梯度爆炸
高中数学有教过,我们可以利用微分的方法来求函数的最大值与最小值。在机器学习中,梯度是一个向量,它表示网络误差函数关于所有权重的偏导数。梯度优化算法就是通过不断计算梯度,并使用梯度优化算法来调整权重,使得代价函数Cost Function (预测结果究竟与实际答案差了多少) 越来越小,也意味着网络能够更好地适应训练数据。当网络的Cost Function达到最小值时,网络就能对新的数据进行较好的预测。
当我们的代价函数是线性函数时,我们就能够用梯度下降法(Gradient Descent)来快速的求出代价函数(在图中记为\(J(w)\))的最小值,如图:
![](https://i.imgur.com/EFs14Nt.png)
图3: 梯度下降法的示意图
而梯度消失和梯度爆炸是深度神经网络训练中的两种典型问题。
梯度消失(vanishing gradient): 指在深层网络训练中,由于梯度的较小值逐层传递,导致较深层的权值参数的更新量非常小,趋近于0。这样会导致较深层网络的参数无法得到有效更新,从而使整个网络无法学习。
梯度爆炸(exploding gradient): 指在深层网络训练中,由于梯度的较大值逐层传递,导致较深层的权值参数的更新量非常大,甚至无限大。这样会导致较深层网络的参数更新量过大,从而使整个网络无法学习。
如果有疑问,请参考下面RNN的消失和爆炸的原理说明:
RNN梯度消失和爆炸的原理
RNN的统一定义为
\[ \begin{equation}h_t = f\left(x_t, h_{t-1};\theta\right)\end{equation} \]
- \(h_t\)是每一步的输出,也就是隐藏状态,由当前输入\(x_t\)和前一时刻的输出\(h_{t-1}\)共同决定
- \(\theta\)则是可训练的参数
(在做基本分析时,我们可以假设\(h_t,x_t,\theta\)都是一维的,这可以让我们获得最直观的理解,其结果对高维情形仍有参考价值。)
RNN梯度的表达式为
\[ \begin{equation}\frac{d h_t}{d\theta} = \frac{\partial h_t}{\partial h_{t-1}}\frac{d h_{t-1}}{d\theta} + \frac{\partial h_t}{\partial \theta}\end{equation} \]
这个公式的意思是,我们可以递推地计算出每一个时间步的隐藏状态对于参数的偏导数,也就是梯度。这样就可以用梯度下降算法来更新网络中的参数,使得网络能够更好地适应训练数据。
可以看到,其实RNN的梯度也是一个RNN,当前时刻梯度\(\frac{d h_t}{d\theta}\)是前一时刻梯度\(\frac{d h_{t-1}}{d\theta}\)与当前运算梯度\(\frac{\partial h_t}{\partial \theta}\)的函数。同时,从上式我们就可以看出,其实梯度消失或者梯度爆炸现象几乎是必然存在的:
- 当\(\left|\frac{\partial h_t}{\partial h_{t-1}}\right| < 1\)时,意味着历史的梯度信息逐步衰减,因此步数多了梯度必然消失(好比\(\lim\limits_{n\to\infty} 0.9^n \to 0\));
- 当\(\left|\frac{\partial h_t}{\partial h_{t-1}}\right| > 1\)时,意味着历史的梯度信息逐步增强,因此步数多了梯度必然爆炸(好比\(\lim\limits_{n\to\infty} 1.1^n \to \infty\))
- 也有可能有些时刻大于1,有些时刻小于1,最终稳定在1附近,但这样概率很小,需要很精巧地设计模型和参数才行。
![](https://editor.analyticsvidhya.com/uploads/51317greatlearning.png)
图4: 梯度爆炸和梯度消失的示意图
提问:梯度消失就是梯度变成零吗?
并不是,我们刚刚说梯度消失是\(\left|\frac{\partial h_t}{\partial h_{t-1}}\right|\)一直小于1,历史梯度不断衰减,但不意味着总的梯度就为0了,具体来说,一直迭代下去,我们有
\[ \begin{equation}\begin{aligned}\frac{d h_t}{d\theta} =& \frac{\partial h_t}{\partial h_{t-1}}\frac{d h_{t-1}}{d\theta} + \frac{\partial h_t}{\partial \theta}\\ =& \frac{\partial h_t}{\partial \theta}+\frac{\partial h_t}{\partial h_{t-1}}\frac{\partial h_{t-1}}{\partial \theta}+\frac{\partial h_t}{\partial h_{t-1}}\frac{\partial h_{t-1}}{\partial h_{t-2}}\frac{\partial h_{t-2}}{\partial \theta}+\dots\\ \end{aligned}\end{equation} \]
显然,其实只要\(\frac{\partial h_t}{\partial \theta}\)不为0,那么总梯度为0的概率其实是很小的;但是一直迭代下去的话,那么\(\frac{\partial h_1}{\partial \theta}\)这一项前面的稀疏就是\(t-1\)项的连乘\(\frac{\partial h_t}{\partial h_{t-1}}\frac{\partial h_{t-1}}{\partial h_{t-2}}\cdots\frac{\partial h_2}{\partial h_1}\),如果它们的绝对值都小于1,那么结果就会趋于0,这样一来,\(\frac{d h_t}{d\theta}\)几乎就没有包含最初的梯度\(\frac{\partial h_1}{\partial \theta}\)的信息了。
这才是RNN中梯度消失的含义:距离当前时间步越长,那么其反馈的梯度信号越不显著,最后可能完全没有起作用。这就意味着RNN对长距离语义的捕捉能力失效了。说白了,优化过程都跟长距离的反馈没关系,那我们怎么保证学习出来的模型能有效捕捉长距离呢?
所以对于一般的RNN模型来说,步数多了,梯度消失或爆炸几乎都是不可避免的,我们只能通过让RNN执行有限的步数来缓解这个问题。直到上世纪末提出的LSTM极大地改进了这个问题。
针对梯度消失/爆炸的改进:LSTM
[论文1]: Hochreiter, Sepp, and Jürgen Schmidhuber. "Long short-term memory." Neural computation 9.8 (1997): 1735-1780. 引入了长短期记忆 (LSTM) 网络,其明确设计用于避免长期依赖问题。每个LSTM单元允许过去的信息跳过当前单元的所有处理并移动到下一个单元;这允许内存保留更长时间,并使数据能够不变地与其一起流动。LSTM 由一个决定要存储哪些新信息的输入门和一个决定要删除哪些信息的遗忘门组成。
![](https://jinglescode.github.io/assets/img/posts/illustrated-guide-transformer-05.jpg)
图5: LSTM的工作方式
当然,LSTM 具有改进的记忆力,能够处理比 RNN
更长的序列。然而,由于LSTM更加复杂,使得LSTM与RNN相比运行更慢。
针对上下文向量长度固定的改进:Attention
![](https://jinglescode.github.io/assets/img/posts/illustrated-guide-transformer-06.jpg)
图6: 长序列输入到原有模型的例子
假设有一段长文本(输入),将其记忆下来(转换为固定长度的向量),然后在不回顾这段文本的情况下,按顺序翻译出整段文本(输出)。这很难,也不是我们的目标做法。相反,当我们翻译一句话时,我们会一部分一部分地看,逐段关注句子的某一部分,从而保证翻译的准确性,这就引入了下文所提到的Attention机制。
[论文2]: Dzmitry Bahdanau, Kyunghyun Cho, Yoshua Bengio: “Neural Machine Translation by Jointly Learning to Align and Translate”, 2014; [http://arxiv.org/abs/1409.0473 arXiv:1409.0473]. 提出了一种在编码器-解码器模型中搜索与预测目标词相关的源句子部分的方法,也就是 Attention 机制,我们可以使用Attention机制翻译相对较长的句子而不影响其性能。例如,翻译成“noir”(在法语中意为“黑色”),注意力机制将关注单词可能的“black”,而忽略句子中的其他单词。
![](https://i.imgur.com/5y6SCvU.png)
图7: Attention机制让输出单词关注相关的输入部分
![](https://jinglescode.github.io/assets/img/posts/illustrated-guide-transformer-07.jpg)
图8: Bahadanau的论文模型在测试集上长序列的 BLEU 分数明显更优
由此可见,Attention机制提高了编码器-解码器网络的性能,但速度瓶颈仍然是RNN必须逐字处理的工作机制。
很自然的,我们会接着考虑:可以用更好的模型替换掉RNN这种顺序结构的模型吗?
答案是:Yes, attention is all you need!
哈哈,有点一语双关的感觉(说不定Google Brain团队当时起名的时候就是这么想的)。在2017年,我们得到了一个令人满意的答案,一款名为Transformer的“神器”横空出世,至今风头不减当年。它也是我们这篇文章要探讨的主题。
Transformer诞生,对标RNN
[论文3]: Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Lukasz Kaiser, Illia Polosukhin: “Attention Is All You Need”, 2017; [http://arxiv.org/abs/1706.03762 arXiv:1706.03762]. 第一次正式介绍了一款在翻译领域超越了RNN的新模型Transformer,Transformer是一种Encoder-Decoder架构,使用Attention机制来处理输入和生成输出。
We propose a new simple network architecture, the Transformer, based solely on attention mechanisms, dispensing with recurrence and convolutions entirely.
Reference: Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A.N., Kaiser, Ł., Polosukhin, I. (2017).
Attention is all you need.
Transformer在定义上就表明,它抛弃了传统的CNN和RNN,整个网络结构完全是由Attention机制组成。其实更准确地讲,Transformer由且仅由Self Attenion和Feed Forward Neural Network组成。采用Attention机制是因为考虑到RNN(或者LSTM,GRU等)的计算限制是顺序处理的,也就是说RNN相关算法只能从左向右依次计算或者从右向左依次计算,这种机制带来了两个问题:
时间\(t\)的计算依赖\(t-1\)时刻的计算结果,这样限制了模型的并行能力。
顺序计算的过程中RNN会因为长期依赖导致信息丢失问题。
Transformer的提出解决了上面两个问题:
不采用类似RNN的顺序结构,而是具有并行性,符合现有的GPU框架。
使用了Attention机制,将序列中的任意两个位置之间的距离缩小为一个常量,采用了和LSTM不同的思路,从而允许处理不同长度的输入序列,并且摆脱了长期依赖的影响。
这也就是为什么它在机器翻译任务中打败了以前基于RNN的Encoder-Decoder模型,并且在其他应用领域也非常受欢迎的原因。
Transformer原理
![](https://github.com/serika-onoe/web-img/raw/main/Transformer/transformer.jpg)
图9: Transformer模型的整体架构
上图是Transformer的整体架构图,结构上看起来和Encoder-Decoder模型很相似,左边是Encoder部分,右边是Decoder部分。为了方便理解,下面把Transformer分成四个部分进行详细说明:
![](https://github.com/serika-onoe/web-img/raw/main/Transformer/transformer_4parts.jpg)
图10: Transformer模型可分为4个组成部分
简单介绍一下各部分的任务:
Input:输入是单词的Embedding再加上位置编码,然后进入编码器或解码器。
Encoder:这个结构可以循环很多次(N次),也就是说有很多层(N层)。每一层又可以分成Attention层和全连接层,再额外加了一些处理,比如Skip Connection,做跳跃连接,然后还加了Normalization层。其实它本身的模型还是很简单的。
Decoder:同样可以循环N次,第一次输入是前缀信息,之后的就是上一次产出的Embedding,加入位置编码,然后进入一个可以重复很多次的模块。该模块可以分成三块来看,第一块也是Attention层,第二块是Cross Attention,不是Self Attention,第三块是全连接层。也用了跳跃连接和Normalization。
Output:最后的输出要通过Linear层(全连接层),再通过Softmax做预测。
我们可以对Transformer的工作过程1~6进行可视化,如下所示:
![](https://github.com/serika-onoe/web-img/raw/main/Transformer/transformer_process.png)
图11: 图解Transformer的工作过程
以英-中翻译为例:假设我们输入"Why do we work?"
,输出可以是"为什么我们要工作?"
。那么Transformer的工作步骤是:
- 输入自然语言序列到编码器: Why do we work?
- 编码器先输出隐藏层, 再输入到解码器;
- 输入\(<start>\)(起始)符号到解码器;
- 得到第一个字"为";
- 将得到的第一个字"为"落下来再输入到解码器;
- 得到第二个字"什";
- 将得到的第二字再落下来, 重复5、6步的相关动作依次生成“么”、“我”、“们”、“要”、“工”、“作”、“ ?”,直到解码器输出\(<end>\)(终止符), 则代表序列生成完成。
Input
![](https://github.com/serika-onoe/web-img/raw/main/Transformer/transformer_4parts(1).png)
图12: Transformer模型的输入部分
我们同样以上篇采用过的英-中翻译例子,我们输入"Tom chase Jerry",期待输出的翻译结果为"汤姆追逐杰瑞"。
Transformer的Decoder的输入处理方法与Encoder的输入处理方法步骤是相似的,但不完全一样。Encoder接受源语言数据,并通过Self-Attention和Feed-Forward网络进行编码;Decoder接受目标语言数据,并在Self-Attention、Encoder-Decoder Attention和Feed-Forward网络的基础上进行解码。在有监督训练时,Decoder的输入包括Target Embedding和一个Masked Multi-Head Attention,用于生成目标语言的下一个词。输出Embedding在预测时用于生成下一个目标词。那么下面,以Encoder为例,描述输入过程和细节分析:
首先,向Transformer输入文本
"Tom chase Jerry"
。随后,Transformer会将原始的英文句子"Tom chase Jerry"进行分词(tokenization),比如得到单词序列['<START>', 'Tom', 'chase', 'Jerry', '<END>']。请注意,分词后的token包括'<START>'和'<END>'标记。
接下来,将每个单词映射到对应的词向量上。实际上Transformer使用的是512维的向量,那么假设我们使用4维的词向量表示单词,那么对于单词'<START>', 'Tom', 'chase', 'Jerry', '<END>',它们的词向量可能是:
\(v_{<START>} : [-0.1,0.2,-0.3,0.4]\)
\(v_{Tom} : [0.5, 0.2, -0.1, 0.3]\)
\(v_{chase} : [-0.2, 0.4, 0.1, 0.6]\)
\(v_{Jerry} : [-0.2, 0.3, 0.1, -0.5]\)
\(v_{<END>} : [0.3,-0.2,0.1,-0.4]\)
- 通过使用sin和cos函数来生成位置向量,这种方式可以向模型描述各个单词之间的顺序关系,并且能够在维度空间上均匀分布位置向量。可以假设'
'的位置编号为1,"Tom"的位置编号为2,"chase"的位置编号为3,"Jerry"的位置编号为4," "的位置编号为5。那么它们位置向量可能是:
\(p_1 = [sin(\frac{1}{10000^{2*\frac{1}{4}}}), cos(\frac{1}{10000^{2*\frac{1}{4}}}),sin(\frac{2}{10000^{2*\frac{1}{4}}}), cos(\frac{2}{10000^{2*\frac{1}{4}}}) ]\)
\(p_2 = [sin(\frac{2}{10000^{2*\frac{1}{4}}}), cos(\frac{2}{10000^{2*\frac{1}{4}}}),sin(\frac{3}{10000^{2*\frac{1}{4}}}), cos(\frac{3}{10000^{2*\frac{1}{4}}}) ]\)
\(p_3 = [sin(\frac{3}{10000^{2*\frac{1}{4}}}), cos(\frac{3}{10000^{2*\frac{1}{4}}}),sin(\frac{4}{10000^{2*\frac{1}{4}}}), cos(\frac{4}{10000^{2*\frac{1}{4}}}) ]\)
\(p_4 = [sin(\frac{4}{10000^{2*\frac{1}{4}}}), cos(\frac{4}{10000^{2*\frac{1}{4}}}),sin(\frac{5}{10000^{2*\frac{1}{4}}}), cos(\frac{5}{10000^{2*\frac{1}{4}}}) ]\)
\(p_5 = [sin(\frac{5}{10000^{2*\frac{1}{4}}}), cos(\frac{5}{10000^{2*\frac{1}{4}}}),sin(\frac{6}{10000^{2*\frac{1}{4}}}), cos(\frac{6}{10000^{2*\frac{1}{4}}}) ]\)
- 最后,输入到Transformer中的序列就是由词向量和位置向量相加得到的,例如,“Tom chase Jerry”的输入序列可能是:
[\(v_ {<START>}\) + \(p_1\) , \(v_{Tom}\) + \(p_2\), \(v_{chase}\) + \(p_3\), \(v_{Jerry}\) + \(p_4\), \(v_ {<END>}\) + \(p_5\)]
下面分别介绍过程中相关的知识点:
分词(Tokenization)
什么是分词
Transformer 模型的输入通常是序列数据,如文本、语音等。这些数据在输入之前需要进行预处理,其中一个重要的步骤就是分词。分词是获取词向量之前的一个必要步骤。
分词是NLP的一个重要概念,表示将文本(text)切分成符号(token)的过程。token可以是以下三种类型:
单词 (word) —— 例如,短语“dogs like cats”由三个词标记组成:“dogs”、“like”和“cats”。
字符 (character) —— 例如,短语“your fish”由九个字符标记组成。(请注意,空格算作标记之一)
子词 (subword) —— 其中单个词可以是单个标记或多个标记。子词由词根、前缀或后缀组成。例如,使用子词作为标记的语言模型可能会将单词“dogs”视为两个标记(词根“dog”和复数后缀“s”)。相同的语言模型可能会将单个词“更高”视为两个子词(词根“high”和后缀“er”)。
分词的作用?
通过分词,模型可以将文本分成若干个单独的词汇单元,可以减少翻译系统需要处理的信息量,提高翻译效率和准确性,同时更好地维护语言的语法结构。
嵌入(Embedding)
NLP中,使用分词后的词向量作为模型输入是常见的做法,而词向量是一种特定类型的嵌入。
什么是嵌入?
嵌入是NLP中使用的一种技术,以机器学习模型能够理解的数字格式表示单词、短语甚至整个句子。
嵌入的作用?
嵌入的目标是通过机器学习模型能够理解的方式捕捉词的含义和上下文,并将词语的语义信息转化为数字,使得它们可以被计算机理解。从另一种角度来看,嵌入也可以被视为一种降维形式,将高维的单词表示转化为低维的词向量。
举个例子,此前对于单词或句子,很容易想到类似 one-hot 的编码向量表示方法,一个单词或句子在向量中仅被表示为一个非零元素,其他元素都是零,从而将单词或句子映射为高维和稀疏的向量。
![](https://github.com/serika-onoe/web-img/raw/main/Transformer/onehot.png)
图13: 图解one hot的编码方式
这种表示方法确实可以避免线性不可分的问题,但由于大多数元素为零,因此计算效率会低下。此外,使用
one-hot
编码表示的词向量不能很好地表示单词之间的语义关系,因此机器学习模型难以学习到单词的含义。因此,嵌入技术是更有效的选择。
与其相反,嵌入是一种以较低维度的密集格式表示单词或句子的方法,更适合机器学习模型。通过将单词或句子映射到一个较低的维度空间,嵌入可以捕捉到单词或句子的含义和相互关系,同时舍弃不太重要的信息。
常见的词向量编码方式是word2vec。相比于one-hot编码只允许我们将单词作为单个不同的条目来解释,word2vec允许我们寻找每个单词和其他单词的关系,从而创建更好的特征表示。
![](https://github.com/serika-onoe/web-img/raw/main/Transformer/word2.png)
图14: 图解word2vec的编码方式
word2vec模型使用神经网络来学习一个词的向量表示。它将每个词映射到一个高维向量,其中语义相似的词在向量空间中是紧密相连的。例如,在word2vec模型中,"banana"这个词的向量表示可能是[-0.2,
0.5, 0.1, 0.3,
...],代表这个词的含义和上下文,(一个词向量往往是300~1000维,向量中的每个元素代表这个词的意义或上下文的一个维度,
嵌入向量的维数通常比 one-hot 编码向量的维数低得多
)。单个数字的含义本身不可解释,只有在与其他词或句子的向量相关时才有意义
输入的embedding是否需要经过训练
将单词\(x\)的embedding输入encoder,有两种常见的选择:
使用Pre-trained的embeddings并固化,这种情况下embedding取自一个预先训练好的模型,在训练过程中不更新。实际就是一个Lookup Table(查找表)。这是bert(一种特殊的Transformer模型,专门用于自然语言处理任务)选择的做法。
对其进行随机初始化(当然也可以选择Pre-trained的结果),但设为Trainable。这样在training过程中不断地对embeddings进行改进。即End2End(端到端)训练方法,意味着模型从头到尾都被训练,所有的参数,包括嵌入都在训练过程中被更新。这也是Transformer选择的做法。
有些情况下,在Encoder的输入层之前还会使用一个词汇表(vocabulary),并且对于OOV(out-of-vocabulary)的单词使用一个特殊的embedding,例如UNK(unknown)或PAD(padding)。
位置编码(Positional Encoding)
为什么需要知道每个单词的位置,并且添加位置编码呢?
首先,咱们知道,一句话中同一个词如果的出现位置不同,意思可能发生翻天覆地的变化,就比如:我欠他100W 和 他欠我100W。这两句话的意思一个地狱一个天堂。可见获取词语出现在句子中的位置信息是一件很重要的事情。
而Transformer没有用RNN也没有卷积,它使用的注意力机制(主要是由于self attention),不能获取词语位置信息,就算打乱一句话中词语的位置,每个词还是能与其他词之间计算attention值。所以为了让模型能利用序列的顺序,必须输入序列中词的位置,所以Transformer采用的方法是给每一个词向量,包括包括'<START>'和'<END>'都需要添加位置编码。
怎么得到positional encoding呢?
Transformer使用的是正余弦位置编码。位置编码通过使用不同频率的正弦、余弦函数生成,然后和对应位置的词向量相加,位置向量维度必须和词向量的维度一致。过程如上图,PE(positional encoding)计算公式如下:
\[PE_{(pos,2i)} = sin(\frac{pos}{10000^{\frac{2 i}{d_{model}}}})\] \[PE_{(pos,2i+1)} = cos(\frac{pos}{10000^{\frac{2 i}{d_{model}}}})\]
解释一下上面的公式:
\(pos\)表示单词在句子中的绝对位置,\(pos=0, 1, 2, \dots\),例如:Jerry在"Tom chase Jerry"中的pos=2;
\(d_{model}\)表示词向量的维度,一般\(d_{model}\)=512;2i和2i+1表示奇偶性,i表示词向量中的第几维,例如这里\(d_{model}\)=512,故\(i=0, 1, 2, \dots, 255\)。
至于上面两个公式是怎么得来的,其实不重要,很有可能是作者根据经验自己造的,而且公式也不唯一,后续Google在Bert中的采用类似词向量的方法通过训练PE,说明这种求位置向量的方法还是存在一定问题滴。
为什么是将positional encoding与词向量相加,而不是拼接呢?
事实上,拼接或者相加都可以,只是词向量本身的维度(512维)就已经蛮大了。再拼接一个512维的位置向量(变成1024维)这样训练起来会相对慢一些,影响学习效率。两者既然效果差不多,那当然是选择学习难度较小的相加了。
这段代码实现了Transformer模型中的位置编码,主要用于确定输入序列中每个单词的位置信息问题。
- d_model: 定义词向量的维度。
- dropout: 一种正则化方式,随机让部分网络参数为0,以防过拟合。
- max_len: 输入句子的最大长度。
在初始化中,首先使用nn.Dropout类创建了一个dropout层。然后根据论文中的公式,预先计算出位置编码,并将其存储在pe变量中。
在forward函数中,将输入x加上pe变量中对应位置的位置编码,最后进行dropout操作(一种正则化方式,在训练时会随机将部分网络参数设置为0,从而防止过拟合)。
需要注意的是,这里的位置编码是在计算时预先计算好了并存储下来,而不是在运行时动态计算,这样可以减少计算量。
Encoder
![](https://github.com/serika-onoe/web-img/raw/main/Transformer/transformer_4parts(2).png)
图15: Transformer模型的编码器部分
在论文中,有6层编码器,即“Nx”的N=6
。Transformer的编码器的每一层(Encoder
layer)是由4个sub-layer(子层)组成的:
第一个子层是多头注意力机制 (multi-head self-attention mechanism),它通过计算输入序列中各个位置的关系,生成关于该位置的输入的新的表示。 第二个子层是残差连接 (residual connection),它将第一个子层的输出与原始输入相加,并进行归一化以维护其统计特性。 第三个子层是全连接前馈层(feed-forward layer),它通过一个多层神经网络对第二个子层的输出进行非线性变换,从而生成新的表示。 第四个子层是残差连接 (residual connection),它将第三个子层的输出与原始输入相加,并进行归一化以维护其统计特性。
这段代码实现了一个transformer编码器中的一个编码层EncoderLayer。这个编码层由两部分组成,分别是自注意力机制self-attn和前馈网络feed_forward。在初始化时,通过传入size、self_attn、feed_forward和dropout参数来初始化编码层。
在前向传播过程中,首先使用自注意力机制对输入x进行处理,然后将处理后的结果经过前馈网络进一步处理,最后返回处理结果。
具体来说,使用 \(sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))\) 调用SublayerConnection类对输入进行处理,进行自注意力机制。 之后使用 \(sublayer[1](x, self.feed_forward)\) 将结果经过前馈网络进一步处理。
Multi-Head Attention
Multi-Head Attention是Self Attention机制的进一步细化,因此先从Self Attention讲起:
从Self Attention讲起
假设下面的句子是我们要翻译的输入句子:
The animal didn't cross the street because it was too tired
这句话中的“it”指的是什么?指的是街道还是动物?这对人来说是一个简单的问题,但对算法模型来说却不那么简单。
![](https://jalammar.github.io/images/t/transformer_self-attention_visualization.png)
图16: 对单词“it”编码时Attention大部分集中在“The animal”上
在该例子中,当模型处理“it”这个词时,self attention 允许它把“it”和“animal”联系起来。而广泛地说,当模型处理每个单词(输入序列中的每个位置)时,自注意力允许它查看输入序列中的其他位置以寻找有助于更好地编码该单词的线索。
而上篇我们详细讲解过,Attention的本质,这里我们简单描述下:Attention实际上做的就是数据库中的检索操作,本质上Attention机制是对Source中元素的Value值进行加权求和,而Query和Key用来计算对应Value的权重系数。
这个类似于搜索引擎或者推荐系统的基本原理,以谷歌Google为例:
- 用户给定需查询的问题(Query)
- Google后台有各种文章标题(Key)和文章本身(Value)
通过定义并计算问题Query与文章标题Key的相关性,用户就可以找到最匹配的文章Value。当然Attention的目标不是通过Query得到Value,而是通过Query得到Value的加权和。
那么回到计算Self Attention的过程上来,这次我们以新的输入“Thinking Machines”为例进行过程描述:
单词级别-第一步:
从每个编码器的输入向量(词向量+位置编码)创建三个向量:一个Query查询向量、一个Key键向量和一个Value值向量。
![](https://jalammar.github.io/images/t/transformer_self_attention_vectors.png)
图17: qkv向量是通过将embedding分别乘以训练的三类权重矩阵而创建的
比如产生"Thinking"的三个向量的过程如下:
- 将\(X_1\)乘以\(W^Q\)权重矩阵产生\(q_1\),即与该词关联的Query向量。
- 将\(X_1\)乘以\(W^K\)权重矩阵产生\(k_1\),即与该词关联的Key向量。
- 将\(X_1\)乘以\(W^V\)权重矩阵产生\(v_1\),即与该词关联的Value向量。
请注意,qkv向量的维度小于embedding向量。它们的维数是 64,而embedding和编码器输入/输出向量的维数是 512。它们不必更小,这是一种使multi-head attention(大部分)计算保持不变的架构选择。
单词级别-第二步
计算每个单词的分数,分数是通过Query向量与当前正在评分单词的Key向量的点积计算得出的。当我们在特定位置对单词进行编码时,分数决定了将多少注意力放在输入句子的其他部分。
假设我们正在计算本例中第一个词“Thinking”的自注意力。我们需要根据这个词对输入句子的每个词进行评分:
![](https://jalammar.github.io/images/t/transformer_self_attention_score.png)
图18: 单词的分数是对应位置Query向量和Key向量的点积
单词级别-第三步
将分数除以8,这个数字是论文中使用的关键向量维度64的平方根。
上式为什么要除以\(\sqrt{d_k}\)呢?
好问题!如果不进行除以\(\sqrt{d_k}\)的操作,那么\(QK^T\)的值将取决于\(d_k\)的大小。当\(d_k\)较大时,\(QK^T\)中的元素将变得相对较大,可能会导致softmax函数的输出非常小,导致梯度消失的问题。当\(d_k\)较小时,\(QK^T\)中的元素将变得相对较小,可能会导致softmax函数的输出非常大,导致梯度爆炸的问题。为了避免这种情况,Transformer中对\(QK^T\)的每个元素都除以\(\sqrt{d_k}\),以使得在不同\(d_k\)下,\(QK^T\)的元素大小分布更加平稳,从而更稳定地进行softmax函数的计算和反向传播。因此,除以\(\sqrt{d_k}\)是一种既简单又有效的方法来缩放注意力分数,这也是为什么Transformer中会采用这种方式。 --By ChatGPT
为什么是\(\sqrt{d_k}\)而不是\(d_k\)呢?
在Transformer中,除以\(\sqrt{d_k}\)是为了缩小内积的值的范围,避免softmax函数的梯度消失和梯度爆炸问题。而为什么不使用\(d_k\)作为缩放系数呢?这是因为\(d_k\)的值通常会非常大(比如说,如果使用了512维的向量,\(d_k\)就为512),这样就会导致内积的值也变得非常大,从而使得softmax函数的梯度非常小,导致梯度消失的问题。此外,除以\(\sqrt{d_k}\)可以使得每个维度对注意力分数的影响相等,这是因为所有维度上的内积被相同的因子\(\sqrt{d_k}\)缩放。这种均衡性有助于确保注意力机制可以平衡地关注每个维度上的信息。因此,使用\(\sqrt{d_k}\)作为缩放系数是一种更好的选择。--By ChatGPT
单词级别-第四步
然后通过 softmax 操作传递结果。softmax 对分数进行归一化处理,使它们都为正且加起来为 1。
softmax 分数决定了在这个位置上,输入句子的每个单词会被投入的注意力占比。
![](https://jalammar.github.io/images/t/self-attention_softmax.png)
图19: 缩小维度并进行归一化处理。
单词级别-第五步
将每个Value向量乘以 softmax 分数(准备将它们相加)。这里的直觉是保持我们想要关注的单词的值不变,并淹没不相关的单词(例如,通过将它们乘以像 0.001 这样的小数字)。然后是对加权值向量求和,这会在该位置产生自注意层的输出。
![](https://jalammar.github.io/images/t/self-attention-output.png)
图20: 输出第一个单词的self attention计算结果
自注意力计算到此结束。生成的向量是我们可以发送到前馈神经网络的向量。然而,在实际实现中,此计算以矩阵形式完成。
为什么实际要用矩阵而不是神经网络呢?
因为矩阵运算能用GPU加速,会更快,同时参数量更少,更节省空间。
既然我们已经看到了单词级别的计算过程,那么让我们来看看Self Attention实际使用的矩阵计算:
矩阵计算-第一步
计算Query, Key和Value共计三个矩阵。为此,我们将嵌入打包到矩阵X中,然后将其乘以我们训练过的权重矩阵 \((W^Q、W^K、W^V)\)。
![](https://jalammar.github.io/images/t/self-attention-matrix-calculation.png)
图21: 计算QKV矩阵
矩阵计算-第二步
由于我们处理的是矩阵,我们可以将单词形式的第二步到第五步压缩为一个公式来计算自注意力层的输出。
![](https://jalammar.github.io/images/t/self-attention-matrix-calculation-2.png)
图22: 矩阵形式的self attention计算公式
Multi-head Attention原理
该论文通过添加一种称为Multi-head Attention的机制,进一步细化了自注意力层。主要体现在两个方面:
它扩展了模型关注不同位置的能力。比如要翻译像“The animal didn't cross the street because it was too tired”这样的句子,知道“it”和哪几个词有关会很有用。
它为注意力层提供了多个representation subspaces (表示子空间)。正如我们接下来将看到的,对于Multi-head Attention,我们有多组QKV权重矩阵,其中的每一个都是随机初始化的。训练之后,每个集合用于将输入(初始输入或来自较低编码器/解码器的向量)投影到不同的表示子空间中。
![](https://jalammar.github.io/images/t/transformer_attention_heads_qkv.png)
图23: Multi-head Attention每个头各产生不同的 QKV 矩阵
![](https://jalammar.github.io/images/t/transformer_attention_heads_z.png)
图24: 通过相同的计算过程,8个attention head最终会得到8个不同的Z矩阵
这给我们带来了一些挑战。前馈层不需要8个矩阵——它需要一个矩阵(每个单词一个向量)。所以我们需要一种方法将这8个压缩成一个矩阵。因此我们连接这些矩阵,然后将它们乘以一个额外的权重矩阵 \(W^O\)。
![](https://jalammar.github.io/images/t/transformer_attention_heads_weight_matrix_o.png)
图25: 将多个Z矩阵通过矩阵乘法合并成总的Z矩阵
![](https://jalammar.github.io/images/t/transformer_multi-headed_self-attention-recap.png)
图26: 将每个头的输出拼接在一起并通过一个Linear层,映射成类似单头的输出
现在我们已经谈到了attention
head,让我们重新审视我们之前的例子,看看当我们在示例句子中对单词“it”进行编码时,不同的attention
head集中在什么地方:
![](https://jalammar.github.io/images/t/transformer_self-attention_visualization_2.png)
图27: 不同attention head对it的注意力权重不同
当我们对“it”这个词进行编码时,一个attention head最关注“the
animal”,而另一个attention head则关注“tired”。这说明不同attention
head很可能从不同角度来理解it和其他单词的关系,比如it的指代的对象是“the
animal”,而这个对象所处的状态是“tired”。因为Attention是注意力的意思而不是表示相等的意思,那么从不同角度看待同一个事物,得到不同的答案自然也是没问题的。
Add & Normalize
可以注意到编码器/解码器的每个子层(比如self attention, ffnn)之后都带有一个 Add & Normalize。
![](https://jalammar.github.io/images/t/transformer_resideual_layer_norm.png)
图28: 每个编码器中都有2个Add & Normalize子层
我之前没有听说过残差连接,因此看着这张图好久也没看出residual这个词体现在哪里,问了“Chat老师”才明白:
Add表示残差连接,残差连接来源于[论文4]: Kaiming He, Xiangyu Zhang, Shaoqing Ren, Jian Sun: “Deep Residual Learning for Image Recognition”, 2015; [http://arxiv.org/abs/1512.03385 arXiv:1512.03385].
Norm表示LayerNorm,LayerNorm来源于[论文5]: Jimmy Lei Ba, Jamie Ryan Kiros, Geoffrey E. Hinton: “Layer Normalization”, 2016; [http://arxiv.org/abs/1607.06450 arXiv:1607.06450].。
Encoder端和Decoder端每个子模块实际的输出为:\(LayerNorm(x+Sublayer(x))\),其中\({Sublayer}(x)\)为子模块的输出。这样做有助于模型更好地捕捉长期依赖关系。
关于这一部分的更多技术细节我以问答的形式展示在下面。
问题一
什么是残差连接
残差连接是一种网络设计方法,它的作用是在每一层的输入和输出之间添加一个跨层连接(shortcut connection),并通过标准化来处理连接处的数据。在Transformer中,每个Encoder层都由一个残差连接和两个正规化步骤组成。
残差连接的公式是 y=LayerNorm(x+Sublayer(x))
- Sublayer(x)可以是任意的层,比如Multi-Head Attention或Feed-Forward层。
- LayerNorm是对每个样本的所有隐藏单元进行归一化,以防止过拟合和提高模型的鲁棒性。
- 其实也就是Add & Normolize
残差连接的好处是能够有效地减轻梯度消失问题,简化网络训练过程,并帮助网络更好地捕捉长期依赖关系。
问题二
为什么引入残差连接
![](https://img-blog.csdnimg.cn/94d2b8dbc1ea432c9ca85f62e29cb454.png#pic_center)
图29: Residual learning:a building block
- X是这一层残差块的输入值
- F(X)是经过第一层线性变化并激活后的输出,也称为残差
- 在第二层输出值激活前加入X,这条路径称作跨层连接(shortcut connection)。
《Deep Residual Learning for Image Recognition》论文中的Figure 2展示了残差学习的建筑块。这个建筑块由两个卷积层组成,每个卷积层后面跟着一个ReLU激活函数。在这个建筑块的最后,有一条跨越了两个卷积层的连接,它绕过了这两个卷积层,直接将输入信息传递到输出端。这个跨层连接被称为“残差连接”。
引入残差连接的目的就是为了防止在深度神经网络训练中发生退化问题,使得更深的网络能够更好地学习特征表达。
问题三
什么是退化,为什么深度神经网络会发生退化?
退化问题通常指的是:当神经网络层数增加时,网络的性能开始变差,表现为训练误差的增加和泛化误差的增加。一种可能的原因是,在网络较深的时候,由于神经网络的非线性层的存在,梯度传播变得困难,导致网络的训练变得困难。
而残差连接通过引入跨层连接,可以使梯度在网络中更容易地流动,从而减轻梯度消失的问题,进一步提高了网络的性能。
举个例子:假如某个神经网络的最优网络层数是18层,但是我们在设计的时候并不知道到底多少层是最优解,本着层数越深越好的理念,我们设计了32层,那么32层神经网络中有14层其实是多余的,我们要想达到18层神经网络的最优效果,必须保证这多出来的14层网络必须进行恒等映射,恒等映射的意思就是说,输入什么,输出就是什么,可以理解成F(x)=x这样的函数,因为只有进行了这样的恒等映射咱们才能保证这多出来的14层神经网络不会影响我们最优的效果。
实际上,在训练过程中,网络的参数并不是直接训练得到的。相反,网络是通过反向传播算法来更新网络的参数,因此参数的更新是从后向前进行的,要想保证训练参数能够很精确的完成F(x)=x的恒等映射其实是很困难的。这个时候大神们就提出了ResNet(残差神经网络)来解决神经网络退化的问题。
问题四
为什么添加了残差块能防止神经网络退化问题呢?
咱们再来看看添加了残差块后,咱们之前说的要完成恒等映射的函数变成什么样子了。是不是就变成h(X)=F(X)+X,我们要让h(X)=X,那么是不是相当于只需要让F(X)=0就可以了,这里就巧妙了!神经网络通过训练变成0是比变成X容易很多的,因为大家都知道,咱们一般初始化神经网络的参数的时候就是设置的[0,1]之间的随机数嘛。所以经过网络变换后很容易接近于0。举个例子:
假设该网络只经过线性变换,没有bias也没有激活函数。我们发现因为随机初始化权重一般偏向于0,那么经过该网络的输出值为[0.6 0.6],很明显会更接近与[0 0],而不是[2 1],相比与学习h(x)=x,模型要更快到学习F(x)=0。
并且ReLU能够将负数激活为0,过滤了负数的线性变化,也能够更快的使得F(x)=0。用学习残差F(x)=0更新该冗余层的参数来代替学习h(x)=x更新冗余层的参数。通过学习残差F(x)=0来让该层网络恒等映射上一层的输入,使得有了这些冗余层的网络效果与没有多余层的浅层网络相同。这样很大程度上解决了网络的退化问题。
问题五
为什么要进行Normalize呢?
在神经网络进行训练之前,都需要对于输入数据进行Normalize归一化,目的有二:
能够加快训练的速度。归一化能够加快训练速度的原因是可以使得数据在网络中传递时,每一层的输出值分布更加均匀,避免了在激活函数饱和区域的情况下,网络梯度消失或爆炸,从而保证了训练的速度和效果。
提高训练的稳定性。归一化能够提高训练的稳定性的原因是使得数据在网络中传递时,每一层的输出值都保持在一个相对小的范围内,避免了出现数据的偏移和方差过大的情况,从而减小了模型对于数据的敏感度,使得模型更加稳定。
在Transformer中,归一化层通常在每个多头自注意力和前馈网络之间进行,这些归一化层被称为"Layer Normalization"。它们在一个batch内的每个样本中对每个特征维度进行独立归一化,并且使用样本的均值和方差来标准化每个特征。
问题六
为什么使用Layer Normalization(LN)而不使用Batch Normalization(BN)呢?
先看图,LN是在同一个样本中不同神经元之间进行归一化,而BN是在同一个batch中不同样本之间的同一位置的神经元之间进行归一化。
![](https://img-blog.csdnimg.cn/20200326202939489.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RpbmsxOTk1,size_16,color_FFFFFF,t_70)
图30: Layer Normalization和Batch Normalization可视化
可以看出,相比于图像领域,NLP任务中的输入是文本数据,一般使用词向量表示,每个词向量是一个高维稠密向量。在NLP任务中,同一个batch中的不同样本可能具有相似的词向量,但每个样本的长度(即词向量的数量)不同。因此,在NLP任务中,每个词向量的不同维度可能具有不同的意义,将每个维度视为不同的特征,并对不同的特征进行归一化处理更加合理。因此,Layer Normalization更适合NLP任务中的使用。而Batch Normalization更适用于图像领域的任务,其中输入是具有相同空间维度的图像。
因此,总结原因如下:
LN更适用于序列数据:在NLP任务中,输入通常是一系列标记的序列,例如单词或字符。这种序列数据通常具有可变的长度和不同的分布。与固定大小的图像批次相比,序列的不同位置可能具有截然不同的统计属性,因此使用BN将导致不同位置的特征之间出现耦合。相比之下,LN可以更好地适应序列数据的变化。
LN避免了对小批次大小的依赖:在BN中,对于每个小批次,特征的均值和方差是在该小批次上计算的。对于小批次,可能会出现均值和方差计算上的不准确性,从而导致性能下降。相比之下,LN不涉及小批次的计算,而是仅使用样本的特征,这使得它对小批次大小的依赖更小。
LN更适用于深度网络:随着神经网络的加深,BN计算的均值和方差将越来越不可靠,这会导致性能下降。而LN则不会受到这个问题的影响,因为它在每个特征上进行归一化,而不是在整个批次上计算。
LN可以更好地适应动态计算图:在深度学习中,一些计算图是动态的,其中图的结构在运行时可以更改。BN的计算依赖于图的结构,因此在这些情况下可能会遇到困难。相比之下,LN只依赖于每个特征的值,因此在动态计算图的环境下更容易使用。
![](https://jalammar.github.io/images/t/transformer_resideual_layer_norm_2.png)
图28: 可视化Add & Normalize操作
Feed Forward
每一层经过self attention之后,还会有一个Feed Forward Network(FFN),这个FFN的作用就是空间变换。FFN包含了2层linear transformation层,中间的激活函数是ReLu。
\[FFN(x) = \max(0, xW_1 + b_1 )W_2 + b_2\]
对于每个多头自注意层的输出,最后都会和 \(W_O\) 相乘拼接成一个Z矩阵,为什么要在每个多头自注意层的输出后再增加一个2层的FFN网络?
这个全连接层的作用是对每个多头自注意力层的输出进行非线性转换(ReLu激活函数),以更好地表达输入序列中的信息。最终,全连接层的输出被与另一个权重矩阵 \(\mathbf{W}_{O}\) 相乘,以得到最终的输出。
因此,全连接层不是必须的,但它可以帮助模型更好地表达输入序列中的信息。特别地,它可以捕捉一些局部特征,从而提高模型的性能。
Decoder
![](https://github.com/serika-onoe/web-img/raw/main/Transformer/transformer_4parts(3).png)
图30: Transformer模型的解码器部分
论文中Decoder也是N=6层堆叠的结构。被分为3个sub-layer,Encoder与Decoder有三大主要的不同:
Decoder sub-layer-1使用的是“Masked” Multi-Headed Attention机制,防止为了模型看到要预测的数据,防止泄露。
sub-layer-2是一个Encoder-Decoder Multi-head Attention。
LinearLayer和SoftmaxLayer作用于sub-layer-3的输出后面,来预测对应的word的概率。
如果你弄懂了Encoder部分,Decoder部分也就没有那么可怕了:
- 输入都是 embedding + positional Encoding。
- Decoder也是N=6层堆叠的结构。被分为3个sub-layer,具体细节方面:
- masked multi-head attention:由于在机器翻译中,Decode的过程是一个顺序的过程,也就是当解码第k个位置时,我们只能看到第k - 1 及其之前的解码结果,因此加了mask,防止模型看到要预测的数据。这点和Encoder不同
- Encoder-Decoder Multi-Head Attention:和Encoder的类似,每一层Decoder都会接受Encoder最后一层输出作为key和value,而当前解码器输出作为query。然后计算输入序列和目标序列中每个位置之间的相似度,最后将所有头的结果拼接在一起得到最终的输出。
- FeedForward:和Encoder一样
- 最后都连接了LinearLayer和SoftmaxLayer
![](https://4143056590-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LpO5sn2FY1C9esHFJmo%2F-M1uVIrSPBnanwyeV0ps%2F-M1uVKtDCvJ7TGjfZPuP%2Fencoder-decoder-2.jpg?generation=1583677008527428&alt=media)
图31: Transformer每一层Decoder都会分别接受Encoder最后一层输出
由此可见,只有masked multi-head attention需要详细讲解,其余的在encoder处都已经掌握了。
Masked Multi-Head-Attention
Masked Multi-Head-Attention则是在传统的多头注意力层的基础上,在计算过程中添加了一个遮挡(mask)机制。这个遮挡机制可以避免解码器在生成目标序列时看到未来的信息。
具体来说,在计算解码器在当前位置的输出值时,如果该位置对应的输入位置在目标序列中出现的位置在当前位置之后,那么这个输入位置就会被遮挡,不会被用来计算输出值。这样做能够避免解码器在生成目标序列时看到未来的信息,提高模型的效果。
问题一
什么是mask
mask表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer模型里面涉及两种mask,分别是 padding mask和sequence mask。 其中,padding mask在所有的scaled dot-product attention 里面都需要用到,而sequence mask只有在Decoder的Self-Attention里面用到。
问题二
什么是padding mask?
因为每个批次输入序列长度是不一样的也就是说,我们要对输入序列进行对齐。具体来说,就是给在较短的序列后面填充0。但是如果输入的序列太长,则是截取左边的内容,把多余的直接舍弃。因为这些填充的位置,其实是没什么意义的,所以我们的Attention机制不应该把注意力放在这些位置上,所以我们需要进行一些处理。
具体的做法是,把这些位置的值加上一个非常大的负数(负无穷),这样的话,经过softmax,这些位置的概率就会接近0! 而我们的padding mask 实际上是一个张量,每个值都是一个Boolean,值为false的地方就是我们要进行处理的地方。
问题三
什么是sequence mask
文章前面也提到,sequence mask是为了使得Decoder不能看见未来的信息。也就是对于一个序列,在time_step为t的时刻,我们的解码输出应该只能依赖于t时刻之前的输出,而不能依赖t之后的输出。因此我们需要想一个办法,把t之后的信息给隐藏起来。 那么具体怎么做呢?也很简单:产生一个上三角矩阵,上三角的值全为0。把这个矩阵作用在每一个序列上,就可以达到我们的目的。
sequence mask的目的是防止Decoder “seeing the future”,就像防止考生偷看考试答案一样。这里mask是一个下三角矩阵,对角线以及对角线左下都是1,其余都是0。下面是个10维度的下三角矩阵:
\[ \begin{bmatrix} 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\ 1 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\ 1 & 1 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0\\ 1 & 1 & 1 & 1 & 0 & 0 & 0 & 0 & 0 & 0\\ 1 & 1 & 1 & 1 & 1 & 0 & 0 & 0 & 0 & 0\\ 1 & 1 & 1 & 1 & 1 & 1 & 0 & 0 & 0 & 0\\ 1 & 1 & 1 & 1 & 1 & 1 & 1 & 0 & 0 & 0\\ 1 & 1 & 1 & 1 & 1 & 1 & 1 & 1 & 0 & 0\\ 1 & 1 & 1 & 1 & 1 & 1 & 1 & 1 & 1 & 0\\ 1 & 1 & 1 & 1 & 1 & 1 & 1 & 1 & 1 & 1\\ \end{bmatrix} \]
对于Decoder的Self-Attention,里面使用到的scaled dot-product attention,同时需要padding mask和sequence mask作为attn_mask,具体实现就是两个mask相加作为attn_mask。
其他情况,attn_mask一律等于padding mask。
举个例子:
假设最大允许的序列长度为10,先令padding mask为
[0 0 0 0 0 0 0 0 0 0]
然后假设当前句子一共有5个单词(加一个起始标识),在输入第三个单词的时候,前面有一个开始标识和两个单词,则此刻的sequence mask为
[1 1 1 0 0 0]
然后padding mask和sequence mask相加,得
[1 1 1 0 0 0 0 0 0 0]
问题四
为什么在模型训练阶段,Decoder的初始输入需要整体右移(Shifted Right)一位?
因为\(T-1\)时刻需要预测\(T\)时刻的输出,所以Decoder的输入需要整体后移一位
举例说明:汤姆追逐杰瑞
→
Tom chase Jerry
位置关系: 0-“Tom”
1-“chase”
2-“Jerry”0-</s>【起始符】目的是为了预测下一个Token
1-“Tom”
2-“chase”
3-“Jerry”
![](https://4143056590-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LpO5sn2FY1C9esHFJmo%2F-M1uVIrSPBnanwyeV0ps%2F-M1uVKtPv2OdAZhNaaw3%2Ftransformer-process-2.gif?generation=1583677021471399&alt=media)
图32: 另一个例子说明Transformer模型的解码工作过程(省略了</s>)
Output
![](https://github.com/serika-onoe/web-img/raw/main/Transformer/transformer_4parts(4).png)
图33: Decoder之后的输出部分
Decoder最终输出的结果是一个浮点型数据的向量,我们要如何把这个向量转为一个单词呢?这个就是Linear和softmax要做的事情了。
Linear层是一个全连接的神经网络,输出神经元个数一般等于我们的词汇表大小。Decoder输出的结果会输入到Linear层,然后再用softmax进行转换,得到的是词汇表大小的向量,向量的每个值对应的是当前Decoder是对应的这个词的概率,我们只要取概率最大的词,就是当前词语Decoder的结果了。
也就是说,Decoder的输出值首先经过一次线性变换,然后Softmax得到输出的概率分布,然后通过词典,输出概率最大的对应的单词作为我们的预测输出。
Transformer训练Tricks
这里有两个训练小技巧,第一个是label平滑,第二个就是学习率要有个worm up过程,然后再下降。
1、Label Smoothing(regularization)
由传统的 \[ \begin{equation} P_i= \begin{cases} 1& \text{ $ i = y $ } \\ 0& \text{ $ i \neq y $ } \end{cases} \end{equation} \]
变为
\[ \begin{equation} P_i= \begin{cases} 1−ϵ& \text{ $ i = y $ } \\ \frac{ϵ}{K−1}& \text{ $ i \neq y $ } \end{cases} \end{equation} \]
注:\(K\)表示多分类的类别总数,\(\epsilon\)是一个较小的超参数。
Noam learning rate schedule是一种在训练深度学习模型时调整学习率的方法。这种方法是由Google AI团队的Noam Shazeer在2018年提出的。它的基本思想是,随着模型的训练进程,学习率应该逐渐降低。具体来说,学习率是根据训练步数的对数来调整的。这个方法可以帮助模型在训练初期快速收敛,并在训练后期更稳定地优化。
学习率不按照Noam Learning Rate Schedule,可能就得不到一个好的Transformer。
\[lr=d_{model}^{−0.5}⋅min(step_{num}^{−0.5}, step_{num}\cdot warmup\_steps^{−1.5})\]
公式表示学习率随着训练步数的增加而逐渐降低,在训练的前\(warmup_steps\)步中学习率是线性增长的, 之后学习率是指数下降的。如图所示:
![](https://4143056590-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LpO5sn2FY1C9esHFJmo%2F-M1uVIrSPBnanwyeV0ps%2F-M1uVKtJ7Yme6Ll5rgo9%2Flr-worm-up.jpg?generation=1583677009478369&alt=media)
图34: Noam learning rate schedule学习率随着训练步数的增加先上升后下降
Transformer特点
优点
每层计算复杂度比RNN低,但是在序列长度很长时,Transformer的计算复杂度仍然很高,尤其是Self-Attention计算的复杂度是 \(O(n^2)\),这使得Transformer在处理超长序列时仍然存在挑战。
并行计算的能力确实是Transformer的优点之一,它可以在GPU等硬件加速下实现高效计算。
从计算一个序列长度为n的信息要经过的路径长度来看, CNN需要增加卷积层数来扩大视野,RNN需要从1到n逐个进行计算,而Self-attention只需要一步矩阵计算就可以。Self-Attention可以比RNN更好地解决长时依赖问题。当然如果计算量太大,比如序列长度N大于序列维度D这种情况,也可以用窗口限制Self-Attention的计算数量。
Transformer确实比较容易进行可视化和解释,特别是在语言建模和文本生成等任务上,可以通过可视化Attention权重来观察模型的输出结果。
缺点
在原文中没有提到缺点,是后来在Universal Transformers中指出的,主要是两点:
- 在原始的Transformer中,无法处理具有变长输出的序列转换任务,这是由于在每个时间步骤输出只依赖于其之前的输入序列和位置编码。因此,Transformer不能像RNN那样处理任意长度的序列输出。这被称为"输出长度限制"问题。对于复制字符串这样的任务,RNN可以轻松处理,因为RNN可以按照输入序列中的顺序逐个生成输出序列。但是对于Transformer,由于输出长度受到限制,因此复制字符串这样的任务需要特殊处理才能实现。另一个相关问题是在推理时,可能会遇到超出训练时所见过的序列长度的输入序列。由于Transformer使用的是固定的位置编码,它在训练时只能处理长度为 \(n\) 的序列,而在推理时只能处理长度小于等于 \(n\) 的序列。如果遇到超出这个长度的输入序列,Transformer无法准确地处理这些输入,因为它从未见过这些位置的位置编码。这是Transformer的"序列长度限制"问题。
解决这些问题的方法是对Transformer进行扩展,例如在输出端使用动态解码器,允许变长输出;或使用可学习的位置编码来处理变长输入序列。此外,一些变体的Transformer,例如Universal Transformer,能够在推理时动态地生成其自身的位置编码,从而能够处理任意长度的序列。
理论上:transformers不是computationally universal(图灵完备),而RNN图灵完备,这种非RNN式的模型是非图灵完备的,无法单独完成NLP中推理、决策等计算问题(包括使用transformer的bert模型等等)。
在一些需要考虑序列顺序的任务上,如机器翻译和语音识别,Transformer仍然需要一些额外的手段来考虑顺序问题,例如在Transformer中引入位置编码。
参考链接
整体代码实现
哈佛大学NLP组的notebook,很详细文字和代码描述,用pytorch实现
Google的TensorFlow官方的,用tf keras实现
参考网站
Self-Attention和Transformer
史上最小白之Transformer详解:
史上最全Transformer面试题系列(一):灵魂20问帮你彻底搞定Transformer-干货!:
The Illustrated Transformer:
A Brief Overview of Recurrent Neural Networks (RNN):
https://www.analyticsvidhya.com/blog/2022/03/a-brief-overview-of-recurrent-neural-networks-rnn/
Practical PyTorch: Translation with a Sequence to Sequence Network and Attention:
https://notebook.community/spro/practical-pytorch/seq2seq-translation/seq2seq-translation-batched
也来谈谈RNN的梯度消失/爆炸问题:
Transformer 架构逐层功能介绍和详细解释:
https://avoid.overfit.cn/post/a895d880dab245609c177db7598446d4
Pytorch中 nn.Transformer的使用详解与Transformer的黑盒讲解:
【深度学习】Attention is All You Need : Transformer模型:
https://www.hrwhisper.me/deep-learning-attention-is-all-you-need-transformer/
Bert前篇:手把手带你详解Transformer原理:
ChatGPT3:
一文搞懂one-hot和embedding:
残差网络(Residual Network):
【经典精读】万字长文解读Transformer模型和Attention机制
https://zhuanlan.zhihu.com/p/104393915?utm_source=ZHShareTargetIDMore