惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

爱范儿
爱范儿
Security Latest
Security Latest
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
C
Cybersecurity and Infrastructure Security Agency CISA
Cloudbric
Cloudbric
T
Threat Research - Cisco Blogs
大猫的无限游戏
大猫的无限游戏
C
CXSECURITY Database RSS Feed - CXSecurity.com
阮一峰的网络日志
阮一峰的网络日志
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
雷峰网
雷峰网
C
Cisco Blogs
V
Vulnerabilities – Threatpost
S
Security Archives - TechRepublic
V
Visual Studio Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
J
Java Code Geeks
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Know Your Adversary
Know Your Adversary
博客园 - 叶小钗
腾讯CDC
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
P
Privacy International News Feed
P
Palo Alto Networks Blog
博客园_首页
V
V2EX
WordPress大学
WordPress大学
Schneier on Security
Schneier on Security
月光博客
月光博客
博客园 - 司徒正美
Google DeepMind News
Google DeepMind News
TaoSecurity Blog
TaoSecurity Blog
博客园 - 聂微东
酷 壳 – CoolShell
酷 壳 – CoolShell
人人都是产品经理
人人都是产品经理
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 【当耐特】
The Cloudflare Blog
罗磊的独立博客
美团技术团队
N
News | PayPal Newsroom
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Last Week in AI
Last Week in AI
K
Kaspersky official blog
Google Online Security Blog
Google Online Security Blog
S
SegmentFault 最新的问题
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Tailwind CSS Blog

又见苍岚

COLMAP PatchMatch Stereo 算法详解 事件驱动的状态机框架:从理论到工程实践 Git 在国内网络环境下无法 Push 的排查与修复 —— 配置 Clash 代理 分段五次多项式插值原理详解 路径插值方法深度对比研究 Claude Code 使用指南 OpenClaw 记忆管理与技能创建指南 CBS(Conflict-Based Search)算法详解 A* 算法及其变种详解 OpenClaw 配置多 Agents Windows Powershell 无法加载文件,因为在此系统上禁止运行脚本问题的解决方案 MaxClaw 安装流程 大模型 AI 名词介绍 AList 网盘聚合工具简介 Protobuf 简介与测试 Claude Code 简介以及 GLM 4.7 模型接入 Github 歌词下载工具 163MusicLyrics Python __getattr__ 懒加载 Python TypedDict 机器人仿真平台 Gazebo 安装记录 机器人仿真平台 Gazebo 简介 多机器人路径规划问题(Multi-Agent Path Finding, MAPF)简介 Python exifread 读取修改过的 jpeg 信息错误问题修复 3D 坐标系变换的理解 3D 旋转矩阵基本概念 MongoDB Compass 介绍 Python 环境管理工具 uv Flutter 开发指南 Snipaste 安装下载与黑屏问题解决方案 全局路径规划算法记录 2025 Python 版本性能测试 Flutter Hello World Flutter 安装环境配置 Ubuntu VMware 硬盘扩容后 SMBus Host controller not enabled 报错问题解决 Python NetworkX 教程 Docker GPU 报错 - Failed to initialize NVML Unknown Error 解决方案 Python matplotlib 图表绘制 cuda-toolkit 安装替代 Cuda 与 Cudnn Jinja2 Python 利用 docxtpl 和 Jinja2 生成基于模板的 Word 文档 Docker 实现 CPU 核心隔离 LoFTR 基于 Transformer 的特征提取匹配算法 OmniGlue 特征匹配 SuperGlue 使用图神经网络学习特征匹配 Ubuntu 下将 xlsx 文件按照 sheet 转换为 图片 Python 使用 SQLAlchemy Python FastAPI 教程 openwrt 软路由配置安装 Nav2 地图文件(PGM/YAML)规范标准 3D OBJ 模型转换为 glb 瓦片格式 Python 源码 Redis 数据库介绍 Ubuntu 22.04 内核自动升级导致 MongoDB 7.0.12 错误记录 ubuntu 20.04 安装 ROS Noetic ubuntu 18.04 安装 ROS Melodic VMware Workstation Pro 个人免费版下载、安装、使用指南 Hybrid A-star 路径规划 Reeds-Shepp 曲线 Dubins 曲线 Linux kvm 虚拟机网络不通的问题解决方法 Ubuntu 自动内存清理 BiliBili 缓存视频转 mp4 Python 求解线性规划 3D Gaussian Splatting 官方源码实践记录 ImageMagick 教程 Ubuntu 22.04 安装 Colmap 对数几率 odds Ubuntu nmcli 网络管理工具使用指南 SuperPoint 自监督深度学习特征点提取 SyncTV Music Tag Web 在线音乐信息整理工具 ncm 格式转 mp3 MusicBrainz 音乐元数据百科数据库 Ubuntu 网络流量监控工具 私人云音乐平台 Navidrome 入门 手眼标定 四元数(Quaternions) OHTTPS 实现免费自动 https 证书申请、更新、部署 ubuntu 22.04 安装 CloudCompare 单机 KVM 虚拟机冷迁移 Ubuntu 22.04 使用 mdadm 实现软 raid 小鱼 一键安装 ROS-humble Fluid -46- 基于 Simpletex API 构建公式识别页面 公式识别 API 简介 -- Simpletex 使用 Python web 部署库 waitress 3D Gaussian Splatting for Real-Time Radiance Field Rendering Ubuntu Swap 简介与空间扩展 Ubuntu 24.04 安装 forticlient Clash Verge 使用 MongoDB 7.0.17 集群 Docker 构建源码 Error code - 2013. Lost connection to MySQL server during query 问题解决 Python 日志记录库 loguru 使用指北 Python 实现 Web 日志查看服务 MySQL LOAD DATA LOCAL INFILE 极速数据加载 Image size exceeds limit of 89478485 pixels 解决方案 Docker 使用 NVIDIA GPU 驱动错误解决 阿里云 docker 镜像仓库 Ubuntu中没有wired connected的解决方案 MinIO 简介 subconverter 代理订阅格式转换 修复 node –openssl-legacy-provider is not allowed in NODE_OPTIONS 错误
Transformer - 4 - Transformer 的细节
Yiwei Zhang · 2023-05-10 · via 又见苍岚

本文继续 大神Transformer 介绍,进入第四篇 —— Transformer 的细节。

问题

经过之前几篇的实践, 当你把这个模型应用到任务当中时,你会发现,这并不能达到论文中所描述的 SOTA 结果。 这篇文章中,我们聊一聊那些在论文中一笔带过的 tricks,这些 tricks 让 Transformer 达到了真正的高度。

  • Transformer 的输入,文本的 Tokenize 优化方法

  • Input 和 Output Embedding

  • Positional Encoding 背后的思考

    • 为什么位置信息的嵌入使用的是 sum(相加) 的方式而不是 concat(串联) 的方式?
    • 位置信息到达上层之后,不会消失吗?
    • 为什么同时使用正弦和余弦?
  • 为什么要增加残差网络?

  • MultiHead Attention 捕获到了什么?

  • 为什么要增加 NormLayer,怎么不是 BatchLayer?

  • FFN 增加到这里做什么用的,可不可以去掉?

Transformer 的输入,文本的 Tokenize 怎么做?

通常来讲,我们在做自然语言处理任务的时候,会把文本进行分词(tokenize),然后把这些词用 onehot 的形式表示。送到模型里。比如说,如果是操作英文,我们会根据情况把每个词或者是字母分割出来;处理中文的时候,我们会根据情况分割成词或者是字。

但是,在 Transformer 中,提到了一种方法,叫做 byte-pair encoding,这种方法是 wordpiece 方法族中的其中一种。为什么要这么做呢?

其实,主要是从基于 word 和 character 两种方式都会有一些弊端,使用 word 的方式会导致词表很大,并且容易出现 OOV(out of vocabulary) 的问题(目标词不在词典中);而如果使用 character 的方式,会导致序列过长,并且丧失掉语言中以词汇为单元的特征。两种都不是特别的理想,wordpiece 方法的思路就是,在 character 和 word 之间找到一种中间的分割方式,从而能够最大可能的利用这两种方式的优点。而 BPE 就是其中的一种,BPE 的思路是基于语料频率来进行统计,把出现最多的子词作为切分的依据。

我们来看下代码,到底是怎么做的?

1
2
3
4
5
6
7
8
# 字典中的 key 为 character,value 为出现的频率
vocab = {
'l o w </w>': 5,
'l o w e r </w>': 2,
'n e w e s t </w>': 6,
'w i d e s t </w>': 3,
'h a p p i e r </w>': 2
}

接着,统计字典中,每个 character 对出现的频率。

1
2
3
4
5
6
7
8
9
10
11
12
def get_pair_stats(vocab: Dict[str, int]) -> Dict[Tuple[str, str], int]:
pairs = {}
for word, frequency in vocab.items():
symbols = word.split()

# count occurrences of pairs
for i in range(len(symbols) - 1):
pair = (symbols[i], symbols[i + 1])
current_frequency = pairs.get(pair, 0)
pairs[pair] = current_frequency + frequency

return pairs

使用 get_pair_stats 处理上面的词典。

1
2
pair_stats = get_pair_stats(vocab)
pair_stats

上面这张图就是统计出来的结果,我们能看到 l 和 o 共出现了 7 次,o 和 w 共出现了 7 次,加下来我们要对出现最多的进行合并,比如上面的例子,就是让 e 和 s 变成一个字词 es。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def merge_vocab(best_pair: Tuple[str, str], vocab_in: Dict[str, int]) -> Dict[str, int]:
vocab_out = {}

# re.escape
# ensures the characters of our input pair will be handled as is and
# not get mistreated as special characters in the regular expression.
pattern = re.escape(' '.join(best_pair))
replacement = ''.join(best_pair)

for word_in in vocab_in:
# replace most frequent pair in all vocabulary
word_out = re.sub(pattern, replacement, word_in)
vocab_out[word_out] = vocab_in[word_in]

return vocab_out

我们接着使用这个方法处理上面的统计结果。

1
2
3
4
5
best_pair = max(pair_stats, key=pair_stats.get)
print(best_pair)

new_vocab = merge_vocab(best_pair, vocab)
new_vocab

接下来,就是反复的经过这两步统计、合并,直到最终形成的词典的大小符合我们所设定的大小,就完成了整个 BPE 的过程。

之后,在我们转换输入的时候,就是按照 BPE 生成的词典来进行转换。这种方式能够比较好的继承两种方法的优点。

Input 和 Output Embedding

接下来,就是把上面的 BPE 通过一层 Embedding 把 onehot 投射到一个向量空间里,在这个这个向量空间里,我们通过 BPE 切割出来的子词会通过学习得到一个合适的向量,这两的合适指的是如果他们语义相近、时态相近、语法相近或者是其他的维度的相近,都会在这个向量空间中得到体现。就像下图一样:

 subword 的映射

 词汇在空间中的二维展示

Positional Encoding 背后的思考

接下来,我们要解决之前在讲解 Self-attention 时留下的一个问题,我们的这这些输入在经过 Self-attention 处理时,是放弃了语句的顺序的,这个也很好理解,因为 Self-attention 实质上是根据注意力的权重,重新的计算了 token 的向量,但是我们在注意力的计算中,并没有考虑到顺序的问题。

但是,大家也知道,在自然语言处理中,语言的顺序是非常重要的,比如说“我爱北京”和“北京爱我”,虽然字面上是相同的,通过 Self-attention 计算后也是相同的,但是实质上,它们是有着本质上的差别的。

OK,那接下来就是怎么才能把 token 的位置顺序也建模进去呢?

这里的 Transformer 的思路也很知觉性,就是既然所有的 token 都能用向量空间里面的某个点来表示,那么位置顺序当然也可以用 向量空间里面的一个点来表示。就像下图中展示的一样:

但是,这里很有趣的一点是,作者并没有直接也上一个可学习的参数来学习这些位置的向量信息,而是使用了一个函数进行映射。这里作者应该是考虑到,如果我用可学习的参数来学习位置信息,那么为了能够让模型能够预测,那么在训练阶段,就一定也要见过预测时需要的位置信息。并且,让模型自己去学习位置的编码的信息,这也加大了模型学习的难度,就需要更多的数据,更大的计算量才能够学好位置信息,这在当时的时间点来看,可能不是特别的合适。

所以,我们来看下作者的做法。作者是使用了下面的两个函数,来对不同的位置,直接映射成对应的向量。

$$ \begin{aligned} \mathbf{P E}_{p o s, 2 i} & =\sin \left(p o s /\left(10000 \frac{2 i}{h^{w}}\right)\right) \\ \mathbf{P} \mathbf{E}_{p o s, 2 i+1} & =\cos \left(p o s /\left(10000 \frac{2 i}{h^{w}}\right)\right)\end{aligned} $$

  • N: 序列的长度
  • $ h^{w} $ : 位置/token 向量的维度大小
  • pos: 当前 token 的位置 $[0, N-1] $
  • $ \mathrm{i}: $ 维度的索引 $ \left[0, h^{w}-1\right] $

上面两个公式,是针对偶数维度索引 2i 和奇数维度索引 2i+1,比如说,我们的位置/token 向量的维度大小为 4,那我们计算不同位置的向量的方式就是这样:
$$
\left[\sin \left(p o s / 10000^{\frac{2 * 0}{4}}\right), \cos \left(p o s / 10000^{\frac{2 * 0}{4}}\right), \sin \left(p o s / 10000^{\frac{2 * 1}{4}}\right), \cos \left(p o s / 10000^{\frac{2 * 1}{4}}\right)\right]
$$
估计更让人觉得神奇的是,为什么上面这个公式就能够表示位置呢?

我们先知觉的使用热力图的方式来理解下,把上面的公式通过热力图画出来如下:

这张图的 x 轴是不同维度的索引值, y 轴是位置,但是好像这么看还是看不来有什么门道。接下来,我们要在这张图上按照 X 轴和 Y 轴进行切片,能得到下面这张图:

  • X 轴切代表:某一个位置在不同维度上的数值;
  • Y 轴切表示:同一个维度,不同位置上的数值。

通过这两个切片,我们能发现两个规律:

  1. 每个位置的编码信息都不同(左图);
  2. 低维度的数值在不同的位置区别比较大,但是在高纬度的数值变化较小,基本是一样的。

这样表示的好处是,既能够给每个位置不同的编码,又高纬度的信息一致,与原来的 token 向量相加时,不会完全覆盖掉这部分信息,这里面所蕴含的含义有点像是说每个 token 编码是由 + 这种形式来构建的,这很像一些通信里面的协议。

上面这些只能说是从直觉上的,在论文中作者有提到:

“We chose this function because we hypothesized it would allow the model to easily learn to attend by relative positions, since for any fixed offset k, $P E_{p o s+k} $ can be represented as a linear function of $P E_{\text {pos }} $ .”
我们选择这个函数是因为我们假设它可以让模型很容易地通过相对位置来学习,因为对于任何固定偏移,都可以表示为线性函数。

但这是为什么呢?接下来我们来证明 Transformer 中位置编码中相对位置之间的线性关系。

问题定义

假设一个矩阵包含$ d_{\text {model }} $ 维列向量 $E_t$ ,这个向量长度为 $n$,在输入序列中编码位置 为 $ t $ 。

$$ e(t)=\boldsymbol{E}_{t,:}:=\left[\begin{array}{c}\sin \left(\frac{t}{f_{1}}\right) \\ \cos \left(\frac{t}{f_{1}}\right) \\ \sin \left(\frac{t}{f_{2}}\right) \\ \cos \left(\frac{t}{f_{2}}\right) \\ \vdots \\ \sin \left(\frac{t}{\frac{f_{\text {model }}}{2}}\right) \\ \cos \left(\frac{t}{f_{\frac{d_{\text {model }}}{}}^{2}}\right)\end{array}\right] $$

其中频率为 :
$$
f_{m}=\frac{1}{\lambda_{m}}:=10000^{\frac{2 m}{d_{\text {model }}}}
$$
接下来证明存在某种线性变换 $ \boldsymbol{T}^{(k)} \in \mathbb{R}^{d_{\text {model }} \times d_{\text {model }}} $ 存在,能够保留序列中任何有效位置 $ t \in{1, \ldots, n-k} $ 的任何位置偏移 $ k \in{1, \ldots, n} $ 。

$$ \boldsymbol{T}^{(k)} \boldsymbol{E}_{t,:}=\boldsymbol{E}_{t+k,:} $$

推导

接下来,我们要找到不依赖于 $ \mathrm{t} $ 的 $ \boldsymbol{T}^{(k)} $

$$ \boldsymbol{T}^{(k)}=\left[\begin{array}{cccc}\boldsymbol{\Phi}_{1}^{(k)} & \mathbf{0} & \cdots & \mathbf{0} \\ \mathbf{0} & \boldsymbol{\Phi}_{2}^{(k)} & \cdots & \mathbf{0} \\ \mathbf{0} & \mathbf{0} & \ddots & \mathbf{0} \\ \mathbf{0} & \mathbf{0} & \cdots & \boldsymbol{\Phi}_{\frac{d_{\text {model }}}{2}}^{(k)}\end{array}\right] $$

其中 0 表示 $ 2 \times 2 $ 的零矩阵和 $ \frac{d_{\text {model }}}{2} $ 位于主对角线上的转置旋转矩阵 $ \boldsymbol{\Phi}^{(k)} $

$$ \boldsymbol{\Phi}_{m}^{(k)}=\left[\begin{array}{cc}\cos \left(r_{m} k\right) & -\sin \left(r_{m} k\right) \\ \sin \left(r_{m} k\right) & \cos \left(r_{m} k\right)\end{array}\right]^{\top} $$

波长 $ r_{m} $ (不要与编码波长 $ \lambda_{m} $ 混淆)。

我们要证明的是:

$$ \underbrace{\left[\begin{array}{cc}\cos \left(r_{m} k\right) & \sin \left(r_{m} k\right) \\ -\sin \left(r_{m} k\right) & \cos \left(r_{m} k\right)\end{array}\right]}_{\boldsymbol{\Phi}_{m}^{(k)}}\left[\begin{array}{c}\sin \left(\lambda_{m} t\right) \\ \cos \left(\lambda_{m} t\right)\end{array}\right]=\left[\begin{array}{l}\sin \left(\lambda_{m}(t+k)\right) \\ \cos \left(\lambda_{m}(t+k)\right)\end{array}\right] $$

我们需要依赖 $ \lambda $ 和 $ k $ 来确定 $ r $ ,并且同时消除 $ t $ , 可以使用三角函数中的和角公式来解决:

$$ \begin{aligned} \sin (\alpha+\beta) & =\sin \alpha \cos \beta+\cos \alpha \sin \beta \\ \cos (\alpha+\beta) & =\cos \alpha \cos \beta-\sin \alpha \sin \beta\end{aligned} $$

展开后发现:
$$
\begin{array}{l}\lambda k=r k \ \lambda t=\lambda t\end{array}
$$
即有 : $r=\lambda$

带回旋转矩阵:

$$ \boldsymbol{\Phi}_{m}^{(k)}=\left[\begin{array}{cc}\cos \left(\lambda_{m} k\right) & \sin \left(\lambda_{m} k\right) \\ -\sin \left(\lambda_{m} k\right) & \cos \left(\lambda_{m} k\right)\end{array}\right] $$

其中 $ \lambda_{m}=10000^{\frac{-2 m}{d_{m o d e l}}} $ 。有了它, $ \boldsymbol{T}^{(k)} $ 完全指定并仅依赖于 $ m 、 d_{\text {model }} $ 和 $ k $ 。序列中的 位置 $ t $ 不是一个参数(证明完毕)。

几个可能会困扰你的问题

也是经常被拿来做面试题的

1. 为什么位置信息的嵌入使用的是 sum(相加) 的方式而不是 concat(串联) 的方式?

我找不到这个问题的任何理论上的解释。由于求和(与串联相反)节省了模型的参数,因此可以将最初的问题改为“向单词添加位置嵌入是否可行?”。我的答案是,不一定就有用!

如果我们还记得上面的位置编码的直觉的感受,我们会发现只有整个向量的前几个维度用于存储和位置有关的信息。由于 Transfomer 中的参数是从头开始训练的,因此自动学习的参数可能会将捕获到的语义的信息存储到后面的维度中,以避免干扰位置编码。

基于同样的原因,我认为 Transformer 可以自动的将单词的语义与其位置信息分开。而且,没有理由将独立表示当成是一种优势,也许模型能够融合这些特征得到一种更有意义的特征。

2. 位置信息到达上层之后,不会消失吗?

幸运的是,Transformer 架构使用了 Residula 连接的方式。因此,来自模型输入的信息(包含位置嵌入)可以有效地传播到处理更复杂交互的其他层。

3. 为什么同时使用正弦和余弦?

我个人认为,只有同时使用正弦和余弦,我们才能将正弦(x+k)和余弦(x+k)表示为 $\sin(x)$ 和 $\cos(x)$ 的线性变换。你不能对单一的正弦或余弦做同样的事情。

最后附上 Positional Encoding 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def positional_embedding(pos, model_size):
PE = np.zeros((1, model_size))
for i in range(model_size):
if i % 2 == 0:
PE[:, i] = np.sin(pos / 10000 ** (i / model_size))
else:
PE[:, i] = np.cos(pos / 10000 ** ((i - 1) / model_size))
return PE

max_length = max(len(data_en[0]), len(data_fr_in[0]))
MODEL_SIZE = 128

pes = []
for i in range(max_length):
pes.append(positional_embedding(i, MODEL_SIZE))

pes = np.concatenate(pes, axis=0)
pes = tf.constant(pes, dtype=tf.float32)

以上就是 Transformer 的输入部分内容,接下来就开始进入到 Transformer Encoder 的结果部分,这部分我们主要探讨,为什么是要用这些结构呢?每个结构起作用的方式又是什么呢?

这个结构里,主要涉及到了几个重要的部分

  • Residual Network (Skip Connection)
  • Multi-Head Attention
  • Add & Norm
  • Feed Forward

为什么要增加残差网络

大家能够看到,在整个 Transformer 的结构中,基本上在所有的基础组件处理后,都增加了一个残差网络(或者叫做跳接网络),这是为什么呢?大家还记得我们上文在聊 Positional Encoding 的时候,有说到位置信息之所以能够向上传递有个很重要的原因就是通过残差网络,信息可以直接通过,不需要做其他的处理,这样位置信息得到了很好的传递。

接下来我们来说一下残差网络的作用,这里主要是有两个作用(也是经常被拿来做面试题的):

首先,它们有助于保持梯度的平滑,这对反向传播有很大帮助。通常我们认为,注意力是一个过滤器,这意味着当它正常工作时,它会阻止大部分试图通过它的东西。这样做的结果是,如果许多输入碰巧落入阻塞的通道中,那么许多输入的微小变化可能不会对输出产生太大的变化。这会在平坦的梯度上产生死点,但它仍然没有靠近谷底。这些鞍点和脊是反向传播的一个非常大的障碍。残差网络有助于消除这些问题。在有注意力的情况下,即使所有权重都为零,所有输入都被阻塞,残差连接也会将输入的一个副本值添加到结果中,并确保任何输入的微小变化仍会对结果产生显著的变化。这可以防止梯度下降过程中无法得到一个好的拟合结果的困境。

自从 ResNet 图像分类器问世以来,由于残差连接可以显著提高性能,因此它现在变得非常流行。现在它基本上已经是神经网络架构中的标准组件。我们可以通过比较有连接和没有连接的网络来看到残差连接的效果,如下图,这里对比了具有或不具有残差连接的 ResNet 网络。当使用残差连接时,损失函数梯度的斜率要适中且均匀得多。

跳接的第二个目的是专门为了 Transformer 结构而添加的,为了保留原始的输入序列的信号。在 Transformer 中,即使已经有很多注意力机制,也不能保证一个单词会注意到它自己的位置和周围的单词。注意力过滤器可能会完全忘记最近的单词,转而关注所有可能相关的早期单词。残差连接通过获取原始单词并手动将其添加到向下传递的信号中,这样就不会删除或者是忘记它,这给 Transformer 结构增加了信号传递的稳定性,这可能是 Transformer 在许多不同的序列任务中表现良好的原因之一。

参考资料

文章链接:
https://www.zywvvd.com/notes/study/deep-learning/transformer/transformer-intr/transformer-intr-4/