如何从浅入深理解transformer？
 如果说「从浅入深」理解 Transformer，逐渐要到深的那部分，答案肯定短不了，希望你有耐心看完。我认为分三步： 本答案就是按照这三步「从浅入深」解读。内容改自：《麦克船长：人工智能 LLM 革命前夜：一文读懂横扫自然语言处理的 Transformer 模型》，来自我的博客 & 公众号“船长还不会游泳”。 本答案试图从技术角度搞清楚一个问题：过去一年 AIGC 爆火、过去五年 NLP（自然语言处理）领域突飞猛进的缘起是什么？ 本内容来自我写在阿里 ATA 的一篇头条文章，先点赞再收藏吧，祝点赞的人“调参数性能起飞，发文章直达顶会”！ 大家熟悉的 ChatGPT 在 2022 年年底登场了。也是因为 ChatGPT 的破圈，带来了 AIGC 在国内科技圈的关注度暴涨。很多人从去年年中开始关注「文生图，text2image」领域的明星 Stable Diffusion 开源，进而关注到了 text2image 应用的爆发，包括 Disco Diffusion、MidJourney、DALL·E 2 等等，这些都源于 CV（计算机视觉）领域因为 Diffusion 模型发展带来的技术突破。 AI 生成图片确实非常惊人。但是从第一性原理角度讲，生成图片的应用广度，远远小于生成文本。文本内容的本质是语言文字的理解与生成，人类历史有 600 万年，但是人类文明历史大概就 6000 年，文明的大发展出现在近 2000 多年的原因，主要来自 3500 多年前人类发明了文字。所以 AI 生成文本，意味着 AI 可以用人类熟悉的方式（语言文字）与人类高效协作，这必将引爆生产力革命。而这必将深入影响电商、内容、游戏、云计算、企业服务等众多领域。 浏览本答案，先对你过往的基础知识做了一些假设，如果你暂未了解，可能在阅读时遇到以下内容做一些简单地查询即可： 作为技术笔记难免有纰漏或理解错误，欢迎指正。文中自绘图片用的是 Graphviz，公式生成用的是 KaTeX，贴到 ATA 后难免有一些没有兼容的部分（发现的已做了 fix），望见谅。 NLP 的技术基础方面，我认为主要是这两部分：词表示法（Word Presentation）、语言模型（Language Model）。对于词表示法，这里不做详细介绍，基本的思路就是把词表示为向量（一维张量），最基本的 One-Hot、Word2Vec、GloVe、fastText 等。这部分的技术演进也在不断前进，比如本答案将要重点介绍的 Transformer 模型里，用到的词表示法是「引入上下文感知的词向量」。 语言模型从早期的 N 元文法（N-Gram，本答案将简要介绍的），到神经网络被提出后最早期的感知器（Perceptron），再到后来席卷计算机视觉（CV）领域的卷积神经网络（CNN），然后出现考虑序列特征的循环神经网络（RNN，包括 Encoder-Decoder 模型），直到 2017 年横空出世的 Transformer，大概分这五个主要阶段。因为本答案的重点是 Transformer，所以前面四个模型我会快速概览一下，然后介绍下最朴素的注意力（Attention）机制，基于此再详细介绍下 Transformer，并对一个完整的、精炼实现的代码实例进行精讲。 1.1、马尔科夫假设（Markov Assumption）与 N 元文法语言模型（N-gram Language Model） 下一个词出现的概率只依赖于它前面 n-1 个词，这种假设被称为「马尔科夫假设（Markov Assumption」。N 元文法，也称为 N-1 阶马尔科夫链。 通过前 t-1 个词预测时刻 t 出现某词的概率，用最大似然估计： $$ P(w_t | w_1,w_2...w_{t-1}) = \frac{C(w_1,w_2,...w_t)}{C(w_1,w_2,...w_{t-1})} $$\\ 进一步地，一组词（也就是一个句子）出现的概率就是： \begin{aligned} P(w_1,w_2,...w_t) &= P(w_t | w_1,w_2,...w_{t-1}) \cdot P(w_{t-1} | w_1,w_2,...w_{t-2})  ...  P(w_1) \\ &= \displaystyle\prod_{i=1}^{t-1}P(w_i | w_{1:i-1}) \end{aligned} \\ 为了解决句头、尾逇概率计算问题，我们再引入两个标记 <BOS> 和 <EOS> 分别表示 beginning of sentence 和 end of sentence，所以 w_0 = <BOS>、w_{length + 1} = <EOS>，其中 length 是词的数量。 具体地，比如对于 bigram，该模型表示如下： \begin{aligned} P(w_1,w_2,...w_t) &= \displaystyle\prod_{i=1}^{t-1}P(w_i | w_{i-1}) \\ P(w_t | w_{t-1}) &= \frac{C(w_{t-1}, w_t)}{C(w_{t-1})} \end{aligned} \\ 上面的第一个问题，我们引入平滑 / 回退 / 差值等方法来解决，而后面两个问题则是在神经网络模型出现后才更好解决的。  1.2、平滑（Smoothing）/ 折扣（Discounting） 虽然限定了窗口 n 大小降低了词概率为 0 的可能性，但当 n-gram 的 n 比较大的时候会有的未登录词问题（Out Of Vocabulary，OOV）。另一方面，训练数据很可能也不是 100% 完备覆盖实际中可能遇到的词的。所以为了避免 0 概率出现，就有了让零平滑过渡为非零的补丁式技术出现。 最简单的平滑技术，就是折扣法（Discounting）。这是一个非常容易想到的办法，就是把整体 100% 的概率腾出一小部分来，给这些零频词（也常把低频词一起考虑）。常见的平滑方法有：加 1 平滑、加 K 平滑、Good-Turing 平滑、Katz 平滑等。  1.2.1、加 1 平滑 / 拉普拉斯平滑（Add-One Discounting / Laplace Smoothing） 加 1 平滑，就是直接将所有词汇的出现次数都 +1，不止针对零频词、低频词。如果继续拿 bigram 举例来说，模型就会变成： P(w_i | w_{i-1}) = \frac{C_(w_{i-1},w_i) + 1}{\displaystyle\sum_{j=1}^n(C_(w_{i-1},w_j) + 1)} = \frac{C(w_{i-1}, w_i) + 1}{C(w_{i-1}) + |\mathbb{V}|} \\其中 N 表示所有词的词频之和，|\mathbb{V}| 表示词汇表的大小。 如果当词汇表中的词，很多出现次数都很小，这样对每个词的词频都 +1，结果的偏差影响其实挺大的。换句话说，+1 对于低频词很多的场景，加的太多了，应该加一个更小的数（ 1 \< δ \< 1）。所以有了下面的「δ 平滑」技术。  1.2.2、加 K 平滑 / δ 平滑（Add-K Discounting / Delta Smoothing） 把 +1 换成 δ，我们看下上面 bigram 模型应该变成上面样子： P(w_i | w{i-1}) = \frac{C_(w_{i-1},w_i) + \delta}{\displaystyle\sum_{j=1}^n(C_(w_{i-1},w_j) + \delta)} = \frac{C(w_{i-1}, w_i) + \delta}{C(w_{i-1}) + \delta|\mathbb{V}|} \\ δ 是一个超参数，确定它的值需要用到困惑度（Perplexity，一般用缩写 PPL）。另外，有些文章里也会把这个方法叫做「加 K 平滑，Add-K Smoothing」。  1.2.3、困惑度（Perplexity） 对于指定的测试集，困惑度定义为测试集中每一个词概率的几何平均数的倒数，公式如下： \operatorname{PPL}(\mathbb{D}_{test}) = \frac{1}{\sqrt[n]{P(w_1,w_2...w_n)}} \\ 把 P(w_1,w_2,...w_t) = \displaystyle\prod_{i=1}^{t-1}P(w_i|w_{i-1}) 带入上述公式，就得到了 PPL 的计算公式： \operatorname{PPL}(\mathbb{D}_{test}) = (\displaystyle\prod_{i=1}^nP(w_i|w_{1:i-1}))^{-\frac{1}{n}} \\ 1.3、回退（Back-off） 在多元文法模型中，比如以 3-gram 为例，如果出现某些三元语法概率为零，则不使用零来表示概率，而回退到 2-gram，如下。 P(w_i|w_{i-2}w_{i-1}) = \begin{cases} P(w_i|w_{i-2}w_{i-1}) & C(w_{i-2}w_{i-1}w_i) > 0 \\ P(w_i|w_{i-1}) & C(w_{i-2}w_{i-1}w_i) = 0 \enspace and \enspace C(w_{i-1}w_i) > 0 \end{cases} \\  1.4、差值（Interpolation） N 元文法模型如果用回退法，则只考虑了 n-gram 概率为 0 时回退为 n-1 gram，那么自然要问：n-gram 不为零时，是不是也可以按一定权重来考虑 n-1 gram？于是有了插值法。以 3-gram 为例，把 2-gram、1-gram 都考虑进来： P(w_i|w_{i-2}w_{i-1}) = \lambda_1 P(w_i|w_{i-2}w_{i-1}) + \lambda_2 P(w_i|w_{i-1}) + \lambda_3 P(w_i) \\ N 元文法模型的显著问题，在「马尔科夫假设与 N 元文法语言模型」小节已经提到了。这些问题基本在神经网络模型中被解决，而要了解神经网络模型，就要从感知器（Perceptron）开始。1957 年感知机模型被提出，1959 年多层感知机（MLP）模型被提出。MLP 有时候也被称为 ANN，即 Artificial Neural Network，接下来我们来深入浅出地了解一下，并有一些动手的练习。  2.1、感知器（Perceptron）：解决二元分类任务的前馈神经网 x 是一个输入向量，w 是一个权重向量（对输入向量里的而每个值分配一个权重值所组成的向量）。举一个具体任务例子，比如如果这两个向量的内积超过某个值，则判断为 1，否则为 0，这其实就是一个分类任务。那么这个最终输出值可以如下表示： y = \begin{cases} 1 & (\omega \cdot x \geq 0) \\ 0 & (\omega \cdot x \lt 0) \end{cases} \\ 这就是一个典型的感知器（Perceptron），一般用来解决分类问题。还可以再增加一个偏差项（bias），如下： y = \begin{cases} 1 & (\omega \cdot x + b \geq 0) \\ 0 & (\omega \cdot x + b \lt 0) \end{cases} \\ 感知器其实就是一个前馈神经网络，由输入层、输出层组成，没有隐藏层。而且输出是一个二元函数，用于解决二元分类问题。  2.2、线性回归（Linear Regression）：从离散值的感知器（解决类问题），到连续值的线性回归（解决回归问题） 一般来说，我们认为感知器的输出结果，是离散值。一般来说，我们认为离散值作为输出解决的问题，是分类问题；相应地，连续值解决的问题是回归（Regression）。比如对于上面的感知器，如果我们直接将 \omega \cdot x + b 作为输出值，则就变成了一个线性回归问题的模型了。 下面我们用 PyTorch 来实现一个线性回归的代码示例，首先我们要了解在 PyTorch 库里有一个非常常用的函数： 这个函数在创建时会自动初始化权值和偏置，并且可以通过调用它的 forward 函数来计算输入数据的线性变换。具体来说，当输入为 x 时，forward 函数会计算 y = \omega \cdot x + b，其中 $ W $ 和 $ b $ 分别是 nn.Linear 图层的权值和偏置。 我们来一个完整的代码示例： 上述代码，一开始先创建一个 LinearRegression 线性回归模型的类，其中有一个 forward 前向传播函数，调用时其实就是计算一下输出值 y。 主程序，一开始创建一个线性回归模型实例，然后定义一个用于评价模型效果的损失函数评价器，和用随机梯度下降（Stochastic Gradient Descent）作为优化器。 然后创建一个输入特征张量，和标签张量。用这组特征和标签进行训练，训练的过程就是根据 X 计算与测试 predictions 向量，再把它和 y 一起给评价器算出损失 loss，然后进行反向传播。注意反向传播的三行代码： 如此训练 100 次（每一次都会黑盒化地更新模型的参数，一个 epoch 就是一次训练过程，有时也称为 iteration 或者 step，不断根据 loss 训练优化模型参数。 然后我们创建了一组测试特征值张量 X_test，和测试标签张量 y_test，然后用它们测试模型性能，把测试特征得到的 predictions 与 y_test 共同传给评价器，得到 loss。在这个例子中我们会得到如下结果：  2.3、逻辑回归（Logistic Regression）：没有值域约束的线性回归，到限定在一个范围内的逻辑回归（常用于分类问题） 可以看到线性回归问题，输出值是没有范围限定的。如果限定（limit）在特定的 $ (0, L) $ 范围内，则就叫做逻辑回归了。那么如何将一个线性回归变成逻辑回归呢？一般通过如下公式变换：  y = \frac{L}{1 + e^{-k(z-z_0)}} \\ 这样原来的 z \in (-\infty, +\infty) 就被变换成了 y \in (0, L) 了。 下面给出一个基于 Python 的 scikit-learn 库的示例代码：  2.4、Sigmoid 回归（Sigmoid Regression）：归一化的逻辑回归，一般用于二元分类任务 当 L = 1, k = 1, z_0 = 0，此时的激活函数就是 Sigmoid 函数，也常表示为  \sigma 函数，如下： y = \frac{1}{1 + e^{-z}} \\ Sigmoid 回归的值域，恰好在 (0, 1) 之间，所以常备作为用来归一化的激活函数。而一个线性回归模型，再用 sigmoid 函数归一化，这种也常被称为「Sigmoid 回归」。Sigmoid 这个单词的意思也就是 S 形，我们可以看下它的函数图像如下： 因为归一化，所以也可以把输出值理解为一个概率。比如我们面对一个二元分类问题，那么输出结果就对应属于这个类别的概率。 这样一个 sigmoid 模型可以表示为： y = Sigmoid(W \cdot x + b) \\ 另外 sigmoid 函数的导数（即梯度）是很好算的：y' = y \cdot (1-y)。这非常方便用于「梯度下降算法」根据 loss 对模型参数进行优化。Sigmoid 回归，一般用于二元分类任务。那么对于超过二元的情况怎么办呢？这就引出了下面的 Softmax 回归。  2.5、Softmax 回归（Softmax Regression）：从解决二元任务的 sigmoid，到解决多元分类任务的 Softmax 相对逻辑回归，Softmax 也称为多项逻辑回归。上面说 Sigmoid 一般用于解决二元分类问题，那么多元问题就要用 Softmax 回归了。我们拿一个具体问题来解释，比如问题是对于任意输入的一个电商商品的图片，来判断这个图片所代表的的商品，属于哪个商品类目。假设我们一共有 100 个类目。那么一个图片比如说其所有像素值作为输入特征值，输出就是一个 100 维的向量 z ，输出向量中的每个值 z_i 表示属于相对应类目的概率 y_i ： y_i = Softmax(z)_i = \frac{e^{z_i}}{e^{z_1} + e^{z_2} + ... + e^{z_100}} \\ 那么最后得到的 y 向量中的每一项就对应这个输入 z 属于这 100 个类目的各自概率了。所以如果回归到一般问题，这个 Softmax 回归的模型就如下： y = Softmax(W \cdot x + b) \\ 对于上面电商商品图片的例子，假设每个图片的尺寸是 512x512，这个模型展开式如下： \begin{bmatrix} y_1 \\ y_2 \\ ... \\ y_{100} \end{bmatrix} = Softmax(\begin{bmatrix} w_{1,1}, & w_{1,2}, & ... & w_{1, 512} \\ w_{2,1}, & w_{2,2}, & ... & w_{2, 512} \\ ... & ... & ... & ... \\ w_{100,1}, & w_{100,2}, & ... & w_{100, 512} \end{bmatrix} \cdot \begin{bmatrix} x_1 \\ x_2 \\ ... \\ x_{512} \end{bmatrix} + \begin{bmatrix} b_1 \\ b_2 \\ ... \\ b_{512} \end{bmatrix}) \\ 这个对输入向量 x 执行 w \cdot x + b 运算，一般也常称为「线性映射/线性变化」。  2.6、多层感知器（Multi-Layer Perceptron） 上面我们遇到的所有任务，都是用线性模型（Linear Models）解决的。有时候问题复杂起来，我们就要引入非线性模型了。 这里我们要介绍一个新的激活函数 —— ReLU（Rectified Linear Unit）—— 一个非线性激活函数，其定义如下： ReLU(z) = max(0, z) \\ 比如对于 MNIST 数据集的手写数字分类问题，就是一个典型的非线性的分类任务，下面给出一个示例代码： 这段代码里，我们能看到 MLP 的模型定义是： 与前面的模型示例代码类似，也都用到了反向传播、损失函数评价器、优化器。如果用公式表示的话，就是如下的模型定义： \begin{aligned} &z = W_1 \cdot x + b_1 \\ &h = ReLU(z) \\ &y = W_2 \cdot h + b_2 \end{aligned} \\ 我们知道 MLP 通常是一个输入和输出长度相同的模型，但少数情况下也可以构建输入和输出长度不同的 MLP 模型，比如输入一组序列后，输出是一个离散的分类结果。  2.7、简述如何训练一个模型：正向传播与反向传播 这是个很重要的议题。但是春节时间有限，这部分只能简写了，我们更多聚焦在语言模型本身。这里简述一下，后续可能会再补全。 引申问题 —— 梯度消失、梯度爆炸问题：因为对损失函数的求偏导，是从输出层向输入层反向基于「数学上的链式法则」计算的，数学上这是个连乘计算，层数越多越容易出现这个问题。这个求导过程可能会出现梯度为零的情况，即梯度消失。也有可能出现梯度值特别大的情况。 解决梯度消失、梯度爆炸问题，又是一个重要议题，这里篇幅所限也难以展开做技术笔记。粗暴的方式比如梯度剪切，Hinton 提出的逐层预训练后再整体精调理论上也 work，本答案后续提到的 LSTM、ResNet 等也可以解决问题，我们也还能了解到业界各种解决手段，有机会再与朋友们交流学习。  2.8、MLP 的一个显著问题，帮我们引出 CNN 模型 我们可以看到，在 MLP 中，不论有多少层，某一层的输出向量 h_n 中的每个值，都会在下一层计算输出向量 h_{n+1} 的每个值时用到。具体来说，如果对于某一层的输出值如下： h_{n+1} = Softmax(W_{n+1} \cdot h_n + b_{n+1}) \\ 上一段话里所谓的「用到」，其实就是要针对 h_n 生成相应的特征值 W_{n+1} 权重矩阵中的每个行列里的数值和 b_{n+1} 偏差向量里的每个值。如果用图画出来，就是： 可以看到，输入的所有元素都被连接，即被分配权重 w 和偏差项 b，所以这被称为一个「全连接层（Fully Connected Layer）」或者「稠密层（Dense Layer）」。但是对于一些任务这样做是很蠢的，会付出大量无效的计算。 因此我们需要 focus 在更少量计算成本的模型，于是有了卷积神经网络（CNN）。  MLP 里每一层的每个元素，都要乘以一个独立参数的权重 W，再加上一个偏执 b，这样的神经网络层常被我们叫做「全连接层（Fully Connected Layer）或稠密层（Dence Layer）。但是这样有个显著问题：如果输入内容的局部重要信息只是发生轻微移动并没有丢失，在全连接层处理后，整个输出结果都会发生很大变化 —— 这不合理 于是我们会想到，如果我们用一个小一些的全连接层，只对重要的局部输入进行处理呢？其实这个思路和 n-gram 是类似的，都是用一个窗口来扫描局部。卷积神经网络（Convolutional Neural Network，CNN）就是基于此诞生的。 这样做有几个特性： CNN 主要的适用领域是计算机视觉。而在 NLP 中，文本数据的维度很高，并且语言的结构比图像更复杂。因此，CNN 一般不适用于处理 NLP 问题。  RNN（循环神经网络），这是一种强大的神经网络模型，能够预测序列数据，例如文本、语音和时间序列。我们将通过生动的代码示例和实际案例来演示如何使用 RNN，并在日常生活中真实地体验它的功能。您将学习到如何使用 RNN 解决各种机器学习问题，并动手尝试运用 RNN 解决实际问题。本答案将为您提供一个完整的 RNN 入门指南，并使您对 RNN 有更深入的了解。 RNN（Recurrent Neural Network）的 R 是 Recurrent 的意思，所以这是一个贷循环的神经网络。首先要明白一点，你并不需要搞懂 CNN 后才能学习 RNN 模型。你只要了解了 MLP 就可以学习 RNN 了。  4.1、经典结构的 RNN 上图这是一个经典结构的 RNN 示意图，Unfold 箭头右侧是展开示意。输入序列（这里用 x 表示）传递给隐藏层（hidden layer，这里用 h 表示），处理完生成输出序列（这里用 o 表示）。序列的下一个词输入时的、上一步隐藏层会一起影响这一步的输出。 U、V、W 都表示权重。在这个经典结构理，你可以看到非常重要的一点，就是输入序列长度与输出序列长度是相同的。 这种经典结构的应用场景，比如对一段普通话输入它的四川话版本，比如对视频的每一帧进行处理并输出，等等。 我们知道 RNN 是一个一个序列处理的，每个序列中的数据项都是有序的，所以对于计算一个序列内的所有数据项是无法并行的。但是计算不同序列时，不同序列各自的计算则是可以并行的。如果我们把上一个时刻 t 隐藏层输出的结果 h_{t-1} 传给一个激活函数（比如说用正切函数 tanh 函数），然后和当下时刻 t 的这个输入 x_t 一起，处理后产生一个时刻 t 的输出 h_t 。然后把隐藏层的输出通过多项逻辑回归（Softmax）生成最终的输出值 y ，我们可以如下表示这个模型： \begin{aligned} &h_t = tanh(W^{xh} \cdot x_t + b^{xh} + W^{hh} \cdot h_{t-1} + b^{hh}) \\ &y_t = Softmax(W^{hy} \cdot h_t + b^{hy}) \end{aligned} \\ 对应的示意图如下： 这种输入和输出数据项数一致的 RNN，一般叫做 N vs. N 的 RNN。如果我们用 PyTorch 来实现一个非常简单的经典 RNN 则如下： 我们来解读一下这段代码： 但是上面的代码示例，并没有自己编写一个具体的 RNN，而是用了默认的 PyTorch 的 RNN，那么下面我们就自己编写一个： 源码解读都在注释中。  4.2、N vs.1 的 RNN 上面那个图里，如果只保留最后一个输出，那就是一个 N vs. 1 的 RNN 了。这种的应用场景，比如说判断一个文本序列是英语还是德语，比如根据一个输入序列来判断是一个正向情绪内容还是负向或者中性，或者比如根据一段语音输入序列来判断是哪一首曲子（听歌识曲）。 \begin{aligned} &h_t = tanh({W^{xh}} \cdot x_t + {b^{xh}} + {W^{hh}} \cdot h_{t-1} + {b^{hh}}) \\ &y = Softmax({W^{hy}} \cdot h_n + {b^{hy}}) \end{aligned} \\ 即这个模型里，每个序列只有隐藏层对最后一个数据项进行处理时才产生输出 $ h_n $ 如果用示意图表示，则是如下结构： 4.3、1 vs. N 的 RNN 反过来，上面那个图里，如果只保留一个 x，那么就是一个 1 vs. N 的 RNN 了。这种场景的应用，比如 AI 创作音乐，还有通过一个 image 提炼或识别某些文本内容输出。 \begin{aligned} &h_t = \begin{cases} tanh(W^{xh} \cdot x + b^{xh} + 0 + b^{hh}) & (t=1) \\ tanh(0 + b^{xh} + W^{hh} \cdot h_{t-1} + b^{hh}) & (t>1) \end{cases} \\ &y_t = Softmax(W^{hy} \cdot h_t + b^{hy}) \end{aligned} \\ 示意图如下： 到这里我们可以看到，在 RNN 的隐藏层是能够存储一些有关于输入数据的一些相关内容的，所以也常把 RNN 的隐藏层叫做记忆单元。  4.4、LSTM（Long Short-Term Memory）长短时记忆网络 4.4.1、如何理解这个 Short-Term 呢？ 1997 年论文《Long Short-Term Memory》中提出 LSTM 模型。我们先从模型的定义，精确地来理解一下： \begin{aligned} &h_t = h_{t-1} + tanh(W^{xh} \cdot x_t + b^{xh} + W^{hh} \cdot h_{t-1} + b^{hh}) \\ &y_t = Softmax(W^{hy} \cdot h_t + b^{hy}) \end{aligned} \\ 上式中与经典结构的 RNN（输入与输出是 N vs. N）相比，唯一的区别是第一个式子中多了一个「 h_{t-1} 」。如果我们把第一个式子的 tanh  部分记作 u_t ： u_t = tanh(W^{xh} \cdot x_t + b^{xh} + W^{hh} \cdot h_{t-1} + b^{hh}) \\ 所以： h_t = h_{t-1} + u_t \\ 那么可以展开出如下一组式子： \begin{aligned} h_{k+1} &= h_k + u_{k+1} \\ h_{k+2} &= h_{k+1} + u_{k+2} \\ &...... \\ h_{t-1} &= h_{t-2} + u_{t-1} \\ h_t &= h_{t-1} + u_t \end{aligned} \\ 如果我们从 h_{k+1} 到 h_n 的所有式子左侧相加、右侧相加，我们就得到如下式子： \begin{aligned} &h_{k+1} + ... + h_{t-1} + h_t \\ = &h_k + h_{k+1} + ... + h_{t-2} + h_{t-1} \\+ &u_{k+1} + u_{k+2} + ... + u_{t-1} + u_t \end{aligned} \\ 进而推导出： h_t = h_k + u_{k+1} + u_{k+2} + ... + u_{t-1} + u_t \\从这里我们就可以看到，第 t 时刻的隐藏层输出，直接关联到第 k 时刻的输出，t 到 k 时刻的相关性则用 $ u_{k+1} $ 到 $ u_t $ 相加表示。也就是有 t-k 的短期（Short Term）记忆。  4.4.2、引入遗忘门 f、输入门 i、输出门 o、记忆细胞 c 如果我们为式子 $ h_t = h_{t-1} + u_t $ 右侧两项分配一个权重呢？就是隐藏层对上一个数据项本身被上一个数据项经过隐藏层计算的结果，这两者做一对权重考虑配比，如下： \begin{aligned} &f_t = sigmoid(W^{f,xh} \cdot x_t + b^{f,xh} + W^{f,hh} \cdot x_{t-1} + b^{f,hh}) \\ &h_t = f_t \odot h_{t-1} + (1 - f_t) \odot u_t \end{aligned} \\ 其中： 但这种方式，对于过去 $ h_{t-1} $ 和当下 $ u_t $ 形成了互斥，只能此消彼长。但其实过去和当下可能都很重要，有可能都恨不重要，所以我们对过去继续采用 $ f_t $ 遗忘门，对当下采用 $ i_t $ 输入门（Input Gate）： \begin{aligned} &f_t = sigmoid(W^{f,xh} \cdot x_t + b^{f,xh} + W^{f,hh} \cdot x_{t-1} + b^{f,hh}) \\ &i_t = sigmoid(W^{i,xh} \cdot x_t + b^{i,xh} + W^{i,hh} \cdot h_{t-1} + b^{i,hh}) \\ &h_t = f_t \odot h_{t-1} + i_t \odot u_t \end{aligned} \\ 其中： 再引入一个输出门： o_t = sigmoid(W^{o,xh} \cdot x_t + b^{o,xh} + W^{o,hh} \cdot x_{t-1} + b^{o,hh}) \\ 再引入记忆细胞 c_t ，它是原来 h_t 的变体，与 t-1 时刻的记忆细胞有遗忘关系（通过遗忘门），与当下时刻有输入门的关系： c_t = f_t \odot c_{t-1} + i_t \odot u_t \\ 那么此时 h_t ，我们可以把 h_t 变成： h_t = o_t \odot tanh(c_t) \\ 记忆细胞这个概念还有有一点点形象的，它存储了过去的一些信息。OK，到此我们整体的 LSTM 模型就变成了这个样子： \begin{aligned} &f_t = sigmoid(W^{f,xh} \cdot x_t + b^{f,xh} + W^{f,hh} \cdot x_{t-1} + b^{f,hh}) \\ &i_t = sigmoid(W^{i,xh} \cdot x_t + b^{i,xh} + W^{i,hh} \cdot h_{t-1} + b^{i,hh}) \\ &o_t = sigmoid(W^{o,xh} \cdot x_t + b^{o,xh} + W^{o,hh} \cdot x_{t-1} + b^{o,hh}) \\ &u_t = tanh(W^{xh} \cdot x_t + b^{xh} + W^{hh} \cdot h_{t-1} + b^{hh}) \\ &c_t = f_t \odot c_{t-1} + i_t \odot u_t \\ &h_t = o_t \odot tanh(c_t) \\ &y_t = Softmax(W^{hy} \cdot h_t + b^{hy}) \end{aligned} \\  4.5、双向循环神经网络、双向 LSTM 双向循环神经网络很好理解，就是两个方向都有，例如下图： 在 PyTorch 中使用 nn.RNN 就有参数表示双向： bidirectional：默认设置为 False。若为 True，即为双向 RNN。  4.6、堆叠循环神经网络（Stacked RNN）、堆叠长短时记忆网络（Stacked LSTM） 在 PyTorch 中使用 nn.RNN 就有参数表示双向： num_layers：隐藏层层数，默认设置为 1 层。当 num_layers >= 2 时，就是一个 stacked RNN 了。  4.7、N vs. M 的 RNN 对于输入序列长度（长度 N）和输出序列长度（长度 M）不一样的 RNN 模型结构，也可以叫做 Encoder-Decoder 模型，也可以叫 Seq2Seq 模型。首先接收输入序列的 Encoder 先将输入序列转成一个隐藏态的上下文表示 C。C 可以只与最后一个隐藏层有关，甚至可以是最后一个隐藏层生成的隐藏态直接设置为 C，C 还可以与所有隐藏层有关。 有了这个 C 之后，再用 Decoder 进行解码，也就是从把 C 作为输入状态开始，生成输出序列。 具体地，可以如下表示： \begin{aligned} &C = Encoder(X) \\ &Y = Decoder(C) \\ \end{aligned} \\ 进一步展开： \begin{aligned} e_t &= Encoder_{LSTM/GRU}(x_t, e_{t-1}) \\ C &= f_1(e_n) \\ d_t &= f_2(d_{t-1}, C) \\ y_t &= Decoder_{LSTM/GRU}(y_{t-1}, d_{t-1}, C) \end{aligned} \\ 这种的应用就非常广了，因为大多数时候输入序列与输出序列的长度都是不同的，比如最常见的应用「翻译」，从一个语言翻译成另一个语言；再比如 AI 的一个领域「语音识别」，将语音序列输入后生成所识别的文本内容；还有比如 ChatGPT 这种问答应用等等。 Seq2Seq 模型非常出色，一直到 2018 年之前 NLP 领域里该模型已成为主流。但是它有很显著的问题：  Encoder-Decoder 的一个非常严重的问题，是依赖中间那个 context 向量，则无法处理特别长的输入序列 —— 记忆力不足，会忘事儿。而忘事儿的根本原因，是没有「注意力」。 对于一般的 RNN 模型，Encoder-Decoder 结构并没有体现「注意力」—— 这句话怎么理解？当输入序列经过 Encoder 生成的中间结果（上下文 C），被喂给 Decoder 时，这些中间结果对所生成序列里的哪个词，都没有区别（没有特别关照谁）。这相当于在说：输入序列里的每个词，对于生成任何一个输出的词的影响，是一样的，而不是输出某个词时是聚焦特定的一些输入词。这就是模型没有注意力机制。 人脑的注意力模型，其实是资源分配模型。NLP 领域的注意力模型，是在 2014 年被提出的，后来逐渐成为 NLP 领域的一个广泛应用的机制。可以应用的场景，比如对于一个电商平台中很常见的白底图，其边缘的白色区域都是无用的，那么就不应该被关注（关注权重为 0）。比如机器翻译中，翻译词都是对局部输入重点关注的。 所以 Attention 机制，就是在 Decoder 时，不是所有输出都依赖相同的「上下文 C_t 」，而是时刻 t 的输出，使用 C_t ，而这个 C_t 来自对每个输入数据项根据「注意力」进行的加权。  2015 年 Dzmitry Bahdanau 等人在论文《Neural Machine Translation by Jointly Learning to Align and Translate》中提出了「Attention」机制，下面请跟着麦克船长，船长会深入浅出地为你解释清楚。 下图中 e_i 表示编码器的隐藏层输出， d_i 表示解码器的隐藏层输出 更进一步细化关于 C_t 部分，麦克船长在此引用《基于深度学习的道路短期交通状态时空序列预测》一书中的图： 这个图里的 \widetilde{h}_i 与上一个图里的 d_i 对应，h_i 与上一个图里的 e_i 对应。 针对时刻 t 要产出的输出，隐藏层每一个隐藏细胞都与 C_t 有一个权重关系 \alpha_{t,i} 其中 1\le i\le n ，这个权重值与「输入项经过编码器后隐藏层后的输出 e_i（1\le i\le n） 、解码器的前一时刻隐藏层输出 d_{t-1} 」两者有关： \begin{aligned} &s_{i,t} = score(e_i,d_{t-1}) \\ &\alpha_{i,t} = \frac{exp(s_{i,t})}{\textstyle\sum_{j=1}^n exp(s_{j,t})} \end{aligned} \\ 常用的 score 函数有： 然后上下文向量就表示成： \begin{aligned} &C_t = \displaystyle\sum_{i=1}^n \alpha_{i,t} e_i \end{aligned} \\ 还记得 RNN 那部分里麦克船长讲到的 Encoder-Decoder 模型的公式表示吗？ \begin{aligned} e_t &= Encoder_{LSTM/GRU}(x_t, e_{t-1}) \\ C &= f_1(e_n) \\ d_t &= f_2(d_{t-1}, C) \\ y_t &= Decoder_{LSTM/GRU}(y_{t-1}, d_{t-1}, C) \end{aligned} \\ 加入 Attention 机制的 Encoder-Decoder 模型如下。 \begin{aligned} e_t &= Encoder_{LSTM/GRU}(x_t, e_{t-1}) \\ C_t &= f_1(e_1,e_2...e_n,d_{t-1}) \\ d_t &= f_2(d_{t-1}, C_t) \\ y_t &= Decoder_{LSTM/GRU}(y_{t-1}, d_{t-1}, C_t) \end{aligned} \\ 这种同时考虑 Encoder、Decoder 的 Attention，就叫做「Encoder-Decoder Attention」，也常被叫做「Vanilla Attention」。可以看到上面最核心的区别是第二个公式 C_t 。加入 Attention 后，对所有数据给予不同的注意力分布。具体地，比如我们用如下的函数来定义这个模型： \begin{aligned} e &= tanh(W^{xe} \cdot x + b^{xe}) \\ s_{i,t} &= score(e_i,d_{t-1}) \\ \alpha_{i,t} &= \frac{e^{s_{i,t}}}{\textstyle\sum_{j=1}^n e^{s_{j,t}}} \\ C_t &= \displaystyle\sum_{i=1}^n \alpha_{i,t} e_i \\ d_t &= tanh(W^{dd} \cdot d_{t-1} + b^{dd} + 				 W^{yd} \cdot y_{t-1} + b^{yd} + 				 W^{cd} \cdot C_t + b^{cd}) \\ y &= Softmax(W^{dy} \cdot d + b^{dy}) \end{aligned} \\到这里你能发现注意力机制的什么问题不？**这个注意力机制忽略了位置信息**。比如 Tigers love rabbits 和 Rabbits love tigers 会产生一样的注意力分数。  麦克船长先通过一个动画来看下 Transformer 是举例示意，该图来自 Google 的博客文章 [《Transformer: A Novel Neural Network Architecture for Language Understanding》](https://ai.googleblog.com/2017/08/transformer-novel-neural-network.html)： 中文网络里找到的解释得比较好的 blogs、answers，几乎都指向了同一篇博客：Jay Alammar 的《The Illustrated Transformer》，所以建议读者搭配该篇文章阅读。 Transformer 模型中用到了自注意力（Self-Attention）、多头注意力（Multiple-Head Attention）、残差网络（ResNet）与捷径（Short-Cut）。下面我们先通过第 1 到第 4 小节把几个基本概念讲清楚，然后在第 5 小节讲解整体 Transformer 模型就会好理解很多了。最后第 6 小节我们来一段动手实践。  自注意力是理解 Transformer 的关键，原作者在论文中限于篇幅，没有给出过多的解释。以下是我自己的理解，能够比较通透、符合常识地去理解 Transformer 中的一些神来之笔的概念。 7.1、一段自然语言内容，其自身就「暗含」很多内部关联信息 在加入了 Attention 的 Encoder-Decoder 模型中，对输出序列 Y 中的一个词的注意力来自于输入序列 X，那么如果 X 和 Y 相等呢？什么场景会有这个需求？因为我们认为一段文字里某些词就是由于另外某些词而决定的，可以粗暴地理解为「完形填空」的原理。那么这样一段文字，其实就存在其中每个词的自注意力，举个例子： 对这句话里的「他」，如果基于这句话计算自注意力的话，显然应该给予「老王」最多的注意力。受此启发，我们认为： Q 就是 query 检索/查询，K、V 分别是 key、value。所以类似于我们在图书检索系统里搜索「NLP书籍」（这是 Q），得到了一本叫《自然语言处理实战》的电子书，书名就是 key，这本电子书就是 value。只是对于自然语言的理解，我们认为任何一段内容里，都自身暗含了很多潜在 Q-K-V 的关联。这是整体受到信息检索领域里 query-key-value 的启发的。 基于这个启发，我们将自注意力的公式表示为： \begin{aligned} Z = SelfAttention(X) = Attention(Q,K,V) \end{aligned} \\ X 经过自注意力计算后，得到的「暗含」了大量原数据内部信息的 Z 。然后我们拿着这个带有自注意力信息的 Z 进行后续的操作。这里要强调的是， Z 向量中的每个元素 z_i 都与 X 的所有元素有某种关联，而不是只与 x_i 有关联。  7.2、如何计算 Q、K、V Q、K、V 全部来自输入 X 的线性变换： \begin{aligned} Q &= W^Q \cdot X \\ K &= W^K \cdot X \\ V &= W^V \cdot X \end{aligned} \\ W^Q、W^K、W^V 以随机初始化开始，经过训练就会得到非常好的表现。对于 X 中的每一个词向量 x_i ，经过这个变换后得到了： \begin{aligned} q_i &= W^Q \cdot x_i \\ k_i &= W^K \cdot x_i \\ v_i &= W^V \cdot x_i \end{aligned} \\ 7.3、注意力函数：如何通过 Q、V 得到 Z 基于上面的启发，我们认为 X 经过自注意力的挖掘后，得到了： 这三组信息，分别如何表示呢？这里又需要一些启发了，因为计算机科学其实是在「模拟还原」现实世界，在 AI 的领域目前的研究方向就是模拟还原人脑的思考。所以这种「模拟还原」都是寻找某一种近似方法，因此不能按照数学、物理的逻辑推理来理解，而应该按照「工程」或者「计算科学」来理解，想想我们大学时学的「计算方法」这门课，因此常需要一些启发来找到某种「表示」。 这里 Transformer 的作者，认为 Q 和 K 两个向量之间的关联，是我们在用 Q 找其在 K 上的投影，如果 Q 、 K 是单位长度的向量，那么这个投影其实可以理解为找「 Q 和 K 向量之间的相似度」： 因此「暗含信息 1」就可以用「 Q\cdot K 」再经过 Softmax 归一化来表示。这个表示，是一个所有元素都是 0~1 的矩阵，可以理解成对应注意力机制里的「注意力分数」，也就是一个「注意力分数矩阵（Attention Score Matrix）」。 而「暗含信息 2」则是输入 X 经过的线性变换后的特征，看做 X 的另一种表示。然后我们用这个「注意力分数矩阵」来加持一下 V ，这个点积过程就表示了「暗含信息 3」了。所以我们有了如下公式： \begin{aligned} Z = Attention(Q,K,V) = Softmax(Q \cdot K^T) \cdot V \end{aligned} \\ 其实到这里，这个注意力函数已经可以用了。有时候，为了避免因为向量维度过大，导致 Q \cdot K^T 点积结果过大，我们再加一步处理： \begin{aligned} Z = Attention(Q,K,V) = Softmax(\frac{Q \cdot K^T}{\sqrt{\smash[b]{d_k}}}) \cdot V \end{aligned} \\ 这里 $ d_k $ 是 K 矩阵中向量 $ k_i $ 的维度。这一步修正还有进一步的解释，即如果经过 Softmax 归一化后模型稳定性存在问题。怎么理解？如果假设 Q 和 K 中的每个向量的每一维数据都具有零均值、单位方差，这样输入数据是具有稳定性的，那么如何让「暗含信息 1」计算后仍然具有稳定性呢？即运算结果依然保持零均值、单位方差，就是除以「$ \sqrt{\smash[b]{d_k}} $」。  7.4、其他注意力函数 为了提醒大家这种暗含信息的表示，都只是计算方法上的一种选择，好坏全靠结果评定，所以包括上面的在内，常见的注意力函数有（甚至你也可以自己定义）： Z = Attention(Q,K,V) = \begin{cases} \begin{aligned} &= Softmax(Q^T K) V \\ &= Softmax(\frac{Q K^T}{\sqrt{\smash[b]{d_k}}}) V \\ &= Softmax(\omega^T tanh(W[q;k])) V \\ &= Softmax(Q^T W K) V \\ &= cosine[Q^T K] V \end{aligned} \end{cases} \\ 到这里，我们就从原始的输入 X 得到了一个包含自注意力信息的 Z 了，后续就可以用 Z 了。  到这里我们理解了「自注意力」，而 Transformer 这篇论文通过添加「多头」注意力的机制进一步提升了注意力层。我们先看下它是什么，然后看下它的优点。从本小节开始，本答案大量插图引用自[《The Illustrated Transformer》](The Illustrated Transformer)，作者 Jay Alammar 写出一篇非常深入浅出的图解文章，被大量引用，非常出色，再次建议大家去阅读。 Transformer 中用了 8 个头，也就是 8 组不同的 Q-K-V： \begin{aligned} Q_0 = W_0^Q \cdot X ;\enspace K_0 = &W_0^K \cdot X ;\enspace V_0 = W_0^V \cdot X \\ Q_1 = W_1^Q \cdot X ;\enspace K_1 = &W_0^K \cdot X ;\enspace V_1 = W_1^V \cdot X \\ &.... \\ Q_7 = W_7^Q \cdot X ;\enspace K_7 = &W_0^K \cdot X ;\enspace V_7 = W_7^V \cdot X \end{aligned} \\这样我们就能得到 8 个 Z ： \begin{aligned} &Z_0 = Attention(Q_0,K_0,V_0) = Softmax(\frac{Q_0 \cdot K_0^T}{\sqrt{\smash[b]{d_k}}}) \cdot V_0 \\ &Z_1 = Attention(Q_1,K_1,V_1) = Softmax(\frac{Q_1 \cdot K_1^T}{\sqrt{\smash[b]{d_k}}}) \cdot V_1 \\ &... \\ &Z_7 = Attention(Q_7,K_7,V_7) = Softmax(\frac{Q_7 \cdot K_7^T}{\sqrt{\smash[b]{d_k}}}) \cdot V_7 \\ \end{aligned} \\然后我们把 Z_0 到 Z_7 沿着行数不变的方向全部连接起来，如下图所示： 我们再训练一个权重矩阵 W^O ，然后用上面拼接的 Z_{0-7} 乘以这个权重矩阵： 于是我们会得到一个 Z 矩阵： 到这里就是多头注意力机制的全部内容，与单头注意力相比，都是为了得到一个 Z 矩阵，但是多头用了多组 Q-K-V，然后经过拼接、乘以权重矩阵得到最后的 Z。我们总览一下整个过程： 通过多头注意力，每个头都会关注到不同的信息，可以如下类似表示： 这通过两种方式提高了注意力层的性能：  9.1、退化现象 对于一个 56 层的神经网路，我们很自然地会觉得应该比 20 层的神经网络的效果要好，比如说从误差率（error）的量化角度看。但是华人学者何凯明等人的论文[《Deep Residual Learning for Image Recognition》](https://arxiv.org/pdf/1512.03385.pdf)中给我们呈现了相反的结果，而这个问题的原因并不是因为层数多带来的梯度爆炸/梯度消失（毕竟已经用了归一化解决了这个问题），而是因为一种反常的现象，这种现象我们称之为「退化现象」。何凯明等人认为这是因为存在「难以优化好的网络层」。  9.2、恒等映射 如果这 36 层还帮了倒忙，那还不如没有，是不是？所以这多出来的 36 个网络层，如果对于提升性能（例如误差率）毫无影响，甚至更进一步，这 36 层前的输入数据，和经过这 36 层后的输出数据，完全相同，那么如果将这 36 层抽象成一个函数 f_{36} ，这就是一个恒等映射的函数： f_{36}(x) = x \\ 回到实际应用中。如果我们对于一个神经网络中的连续 N 层是提升性能，还是降低性能，是未知的，那么则可以建立一个跳过这些层的连接，实现： 这就像给了这 N 层神经网络一个试错的空间，待我们确认它们的性能后再决定是否采用它们。同时也可以理解成，这些层可以去单独优化，如果性能提升，则不被跳过。  9.3、残差网络（Residual Network）与捷径（Short-Cut） 如果前面 20 层已经可以实现 99% 的准确率，那么引入了这 36 层能否再提升「残差剩余那 1%」的准确率从而达到 100% 呢？所以这 36 层的网络，就被称为「残差网络（Residual Network，常简称为 ResNet）」，这个叫法非常形象。 而那个可以跳过 N 层残差网络的捷径，则常被称为 Short-Cut，也会被叫做跳跃链接（Skip Conntection），这就解决了上述深度学习中的「退化现象」。  还记得我在第二部分最后提到的吗： 10.1、Transformer 论文中的三角式位置编码（Sinusoidal Positional Encoding） 现在我们来解决这个问题，为每一个输入向量 $ x_i $ 生成一个位置编码向量 $ t_i $，这个位置编码向量的维度，与输入向量（词的嵌入式向量表示）的维度是相同的： Transformer 论文中给出了如下的公式，来计算位置编码向量的每一位的值： \begin{aligned} P_{pos,2i} &= sin(\frac{pos}{10000^{\frac{2i}{d_{model}}}}) \\ P_{pos,2i+1} &= cos(\frac{pos}{10000^{\frac{2i}{d_{model}}}}) \end{aligned} \\ 这样对于一个 embedding，如果它在输入内容中的位置是 pos，那么其编码向量就表示为： [P_{pos,0}, P_{pos,1}, ... , P_{pos,d_{x-1}}] \\ 延展开的话，位置编码其实还分为绝对位置编码（Absolute Positional Encoding）、相对位置编码（Relative Positional Encoding）。前者是专门生成位置编码，并想办法融入到输入中，我们上面看到的就是一种。后者是微调 Attention 结构，使得它可以分辨不同位置的数据。另外其实还有一些无法分类到这两种的位置编码方法。  10.2、绝对位置编码 绝对位置编码，如上面提到的，就是定义一个位置编码向量 $ t_i $，通过 $ x_i + t_i $ 就得到了一个含有位置信息的向量。  10.3、相对位置编码和其他位置编码 最早来自于 Google 的论文[《Self-Attention with Relative Position Representations》](https://arxiv.org/abs/1803.02155)相对位置编码，考虑的是当前 position 与被 attention 的 position 之前的相对位置。 到此我们都是在讲 Encoder，目前我们知道一个 Encoder 可以用如下的示意图表示： 11.1、Encoder 和 Decoder 的图示结构 先看看 Encoder： 再看看 Decoder：  11.2、Decoder 的第一个输出结果 产出第一个最终输出结果的过程： 这样我们就像前面的 Encoder-Decoder Attention 模型一样，得到第一个输出。但是最终的输出结果，还会经过一层「Linear + Softmax」。  11.3、Decoder 后续的所有输出 从产出第二个输出结果开始：  11.4、Decoder 之后的 Linear 和 Softmax 经过所有 Decoder 之后，我们得到了一大堆浮点数的结果。最后的 Linear & Softmax 就是来解决「怎么把它变成文本」的问题的。  最后我们再来整体看一下 Transformer：  Google 在其博客于 2017.08.31 发布如下测试数据：  我们来看看一个简单的 Transformer 模型，就是比较早出现的 Kyubyong 实现的 Transformer 模型，其 GitHub 仓库地址为 Kyubyong Transformer 1.2 legacy version。  下载一个「德语-英语翻译」的数据集：德英翻译数据集地址。 把 de-en 下面的 tgz 解压后放在 corpora/ 目录下。如果需要先修改超参数，需要修改 hyperparams.py。然后运行如下命令，生成词汇文件（vocabulary files），默认到 preprocessed 目录下： 然后开始训练： 也可以跳过训练，直接 下载预训练过的文件，是一个 `logdir/` 目录，把它放到项目根目录下。然后可以对训练出来的结果，运行评价程序啦： 会生成「德语-英语」测试结果文件在 results/ 目录下，内容如下： 评估结果文件的最后一行是 Bleu Score：  总计 700 多行代码。  15.1、超参数 hyperparams.py 文件中定义了 Hyperparams 超参数类，其中包含的参数我们逐一来解释一下：  15.2、预处理 文件 prepro.py 实现了预处理的过程，根据 hp.source_train 和 hp.target_train 分别创建 "de.vocab.tsv" 和 "en.vocab.tsv" 两个词汇表。  15.3、训练/测试数据集的加载 我们先看下 train.py、data_load.py、eval.py 三个文件： 下面是函数调用的流程： 将 preprocessed/de.vocab.tsv 和 preprocessed/en.vocab.tsv 中储存的德语、英语的词汇、词频，载入成 word2idx 和 idx2word。前者是通过词查询词向量，后者通过词向量查询词。 load_de_vocab 和 load_en_vocab 函数被 create_data 函数引用，该函数将输入的源语言和目标语言句子转换为索引表示，并对过长的句子进行截断或填充。详细的解释看下面代码里的注释。 下面的 get_batch_data 则从文本数据中读取并生成 batch：  15.4、构建模型并训练 Graph 的构造函数流程，就是模型的构建流程，下面麦克船长来分析这部分代码。 整体这个流程，主要涉及 train.py 文件和 modules.py 文件。所有模型所需的主要函数定义，都是在 modules.py 中实现的。我们先看下编码器（Encoder）的流程： 下面是 train.py 中实现的 Transformer 流程，其中的每一段代码，麦克船长都会做详细解释，先不用急。这个流程里，首先定义了编码器，先使用了 Embedding 层将输入数据转换为词向量，使用 Positional Encoding 层对词向量进行位置编码，使用 Dropout 层进行 dropout 操作，然后进行多层 Multihead Attention 和 Feed Forward 操作。 在构建模型前，先执行 train.py 的主程序段，首先 if __name__ == '__main__' 这句代码是在 Python 中常用的一种编写方式，它的意思是当一个文件被直接运行时，if 语句下面的代码会被执行。请看下面代码的注释。  15.4.1、编码过程 15.4.1.1、Embedding embedding 用来把输入生成词嵌入向量： 15.4.1.2、Key Masks 接着生成一个 key_masks 用于在之后的计算中屏蔽掉某些位置的信息，以便模型只关注有效的信息。 15.4.1.3、Positional Encoding 下面生成 Transformer 的位置编码： 如果超参数 hp.sinusoid=True，使用 positional_encoding 函数，通过使用正弦和余弦函数来生成位置编码，可以为输入序列添加位置信息。如果 hp.sinusoid=False，使用 embedding 函数，通过学习的词嵌入来生成位置编码。 位置编码生成后，用 key_masks 处理一下。注意 key_masks 的生成一定要用最初的 self.enc，所以在前面执行而不是这里： 这个不是矩阵乘法，而是对应元素相乘。这里乘上 key_masks 的目的是将 key_masks 中值为 0 的位置对应的 self.enc 中的元素置为 0，这样就可以排除这些位置对计算的影响。 15.4.1.4、Drop out 下面调用了 TensorFlow 的 drop out 操作： drop out 是一种在深度学习中常用的正则化技巧。它通过在训练过程中随机地「关闭」一些神经元来减少 过拟合。这样做是为了防止模型过于依赖于某些特定的特征，而导致在新数据上的表现不佳。 在这个函数中，dropout 层通过在训练过程中随机地将一些神经元的输出值设置为 0，来减少模型的过拟合。这个函数中使用了一个参数 rate，表示每个神经元被「关闭」的概率。这样做是为了防止模型过于依赖于某些特定的特征，而导致在新数据上的表现不佳。 15.4.1.5、Encoder Blocks: Multi-Head Attention & Feed Forward 然后看下 encoder blocks 代码： 上述代码是编码器（Encoder）的实现函数调用的流程，也是与麦克船长上面的模型原理介绍一致的，在定义时同样使用了 Embedding 层、Positional Encoding 层、Dropout 层、Multihead Attention 和 Feed Forward 操作。其中 Multihead Attention 在编码、解码中是不一样的，待会儿我们会在 Decoder 部分再提到，有自注意力层和 Encoder-Decoder  15.4.2、解码过程 再看一下解码的流程： Embedding 下面我们逐一看每段代码，主要关注与编码阶段的区别即可： Key Masks Positional Encoding & Drop out  Decoder Blocks: Multi-Head Attention & Feed Forward  15.4.3、Embedding、Positional Encoding、Multi-Head Attention、Feed Forward Embedding 函数实现 Positional Encoding 函数实现 Multi-Head Attention 函数实现 Feed Forward 函数实现 下面是 前馈神经网络层 的定义，这是一个非线性变换，这里用到了一些卷积神经网络（CNN）的知识，我们来看下代码再解释： 该函数实现了 Layer Normalization，用于在深度神经网络中解决数据的不稳定性问题。  15.4.4、编码和解码完成后的操作 解码器后的 Linear & Softmax： 训练集数据处理时，经过 Linear & Softmax 之后的最后处理如下。这里用到了 tf.nn.softmax_cross_entropy_with_logits 交叉熵损失，来计算模型的错误率 mean_loss，并使用 Adam 优化器 AdamOptimizer 来优化模型参数。 下面这段代码实现了一种叫做「label Smoothing」的技巧。 在训练过程中，样本的标签被表示为一个二维矩阵，其中第一维表示样本的编号，第二维表示样本的标签。这个矩阵的形状就是 (样本数, 类别数)，所以类别数对应的就是最后一维。具体到这个模型用例里，第一个维度是德语样本句子数，最后一维就是英语词汇量的大小。 用于解决在训练模型时出现的过拟合问题。在标签平滑中，我们给每个样本的标签加上一些噪声，使得模型不能完全依赖于样本的标签来进行训练，从而减少过拟合的可能性。具体来说，这段代码将输入的标签 inputs 乘上 1-epsilon，再加上 epsilon / K，其中 epsilon 是平滑因子，K 是标签类别数（英语词汇量大小）。这样就可以在训练过程中让模型对标签的预测更加平稳，并且降低过拟合的风险。 然后我们看后续的操作。  15.5、效果评价  评估结果文件的最后一行有 Bleu Score = 6.598452846670836 表示这个翻译模型的翻译结果与参考翻译重叠程度比较高，翻译质量较好。不过需要注意的是，BLEU 分数不能完全反映翻译质量，因为它不能评估语法，语义，语调等方面的问题。 另外前面我们在代码中已经将过程数据保存在 logdir 下了，就是为了后续方便可视化，我们可以用 TensorBoard 来可视化，具体使用方法如下： 然后在浏览器里查看 http://localhost:6006，示例如下： 回顾整个模型的代码实现，我们可以更直观地看到这个 Transformer 能够较好地捕捉长距离依赖关系，提高翻译质量。然而，Kyubyong Transformer 的实现存在一些问题。该 Transformer 模型在训练过程中还需要调整许多超参数，如学习率（learning rate）、batch size 等，不同的任务可能需要不同的超参数调整。  Transformer 的优势显而易见： 因此基于 Transformer 原理的模型，在众多 NLP 任务中都取得了卓越的表现。 说到底机器学习（Machine Learning）领域还是一个实验科学，并且是离工业界极近的实验科学。机器学习看待实验结果的角度，不是为了拿实验结果总结抽象后推动理论科学发展。机器学习的实验结果是要被评价的，其效果有客观量化评估标准。所以机器学习，一切以结果说话。基于 Transformer 架构 Decoder 部分诞生了 OpenAI 的 GPT 大模型，基于其架构的 Encoder 部分诞生了 Google 的 BERT 大模型，他们两个都诞生于 2018 年。这几年基于 Transformer 的各种优化思想不断出现，其集大成者便是 2022 年年底基于 GPT-3.5 或者说基于 InstructGPT 的 ChatGPT。 AI 终将颠覆各行各业，我们有责任花些时间关注前沿的发展脉搏，欢迎大家在微信（id：sinosuperman）上与我交流。 最后，祝点赞的人“调参数性能起飞，发文章直达顶会”！