Transformer
一、前言
2017 年 Google 提出的论文《Attention Is All You Need》引入了 Transformer 架构,如今几乎所有主流的 AI 模型(如 OpenAI 的 ChatGPT、Google 的 Bard、Meta 的 LLaMA 等等)都基于 Transformer。Transformer 由于能够同时处理局部和长程依赖,并且支持高效的并行化训练,一经问世便逐步取代了过去的 RNN 和 CNN,成为 NLP 领域新的标准范式。不仅如此,在计算机视觉(CV)领域,纯基于注意力机制的 Vision Transformer 也展现出媲美甚至超越 CNN 的性能,引发了图像识别方法的新一轮变革。
Transformer 的崛起源于对序列建模瓶颈的突破。相比 RNN 模型存在梯度消失且难以捕获长程依赖、且无法在时间步并行处理训练样本,以及 CNN 模型只能擅长局部模式难以处理长距离信息——Transformer 的自注意力机制允许模型“全局看待”序列中任意距离的元素关系,并在不使用循环的情况下直接并行计算序列表示。这种革命性的特性,使得 Transformer 模型在长序列任务上表现优异,并能充分利用现代硬件的并行计算能力,大大提升训练效率和效果。
在 NLP 任务中,Transformer 彻底改变了机器翻译、语言理解和文本生成的范式;在 CV 任务中,Transformer 通过将图像切成 patch 当作“字词”来处理,证明了注意力机制同样能胜任视觉特征提取。不夸张地说,Transformer 是近年深度学习架构的一次范式转移。学习 Transformer 不仅可以帮助我们理解当代 AI 模型(如 BERT、GPT 等)的核心原理,更能赋予我们实现这些模型的能力。本手册旨在以生动有趣的方式,深入浅出地讲解 Transformer 的工作原理和实战技巧。读者通过本手册的学习,将有望全面掌握 Transformer,从而在实践中构建出属于自己的 “Mini-GPT” 模型!🚀
1.序列建模与模型局限
想象一下,我们要教电脑玩一个游戏:词语接龙。就是你说一个词,比如“今天”,电脑要接下一个词,比如“天气”。这其实就是序列建模的核心任务之一——根据前面的信息,预测接下来最可能是什么。
这个任务不仅用于接龙,还用于:
- 机器翻译:把“I love you”这个单词序列变成“我爱你”
- 语音识别:把一段声音信号序列变成文字序列。
- 文章生成:写诗、写邮件、写代码。
那么,在2017年Transformer这个“超人”出现之前,科学家们主要派了两位“老将”去解决这个问题。
1.RNN(循环神经网络)
想象一条很长的传送带,上面依次放着序列里的每个词(“我”、“爱”、“你”)。传送带旁边站着一排工人,每个工人对应一个词。
- 工作方式:第一个工人处理“我”这个词,他记下一点笔记(隐藏状态),然后把笔记和“我”一起交给第二个工人。第二个工人看到“爱”这个词,并结合第一个工人的笔记,更新自己的笔记,再往下传……如此继续。
优点:
- 有记性:因为会传递笔记,所以理论上后面的工人知道前面所有词的信息。这很适合处理序列。
缺陷:
- 慢!不能并行:工人们必须一个接一个地工作,不能同时处理。就像单车道,车只能一辆接一辆走。这在训练时无法利用现代GPU的强大并行计算能力,速度非常慢。
- 记性差,会忘事:传送带太长时(序列很长),传到后面,笔记里的最初信息可能已经模糊不清了(梯度消失)。比如,很难记住一段话开头的“他”到底指的是谁。虽然后来有了升级版员工LSTM和GRU(带了更复杂的备忘录),但序列太长时,依然会忘记很久以前的事。
简单比喻:RNN就像一个只能逐字阅读、还容易忘开头的人。
2.CNN(卷积神经网络)
CNN本来是用来处理图像的专家,它通过一个“小窗口”(卷积核)在图像上滑动来识别局部模式(比如边缘、角落)。后来有人尝试让它来处理序列。
把这幅画想象成我们的序列。你拿着一个望远镜(卷积核),每次只能看到画面的一小部分(局部感受野)。
优点:
- 快!可以并行:我可以同时派很多人,拿着望远镜看这幅画的不同部分。因为大家互不依赖,所以计算效率很高,能充分利用GPU。
缺点:
- “鼠目寸光”:一次只能看到局部信息。如果第一句话和最后一句话有关系,靠一个小望远镜很难直接发现这种长距离依赖。
- 需要堆叠很多层:为了看到全局,唯一的办法就是把很多个望远镜叠起来(堆叠多层卷积层)。第一个人看几个词,第二个人看第一个人的结果,这样视野才能慢慢扩大。这非常复杂,而且信息在多层传递中也会有损耗。
简单比喻:CNN就像一个能同时看很多地方,但每次只能聚焦一小块区域的人,需要反复拼凑才能理解全局。
所以,在Transformer出现之前,序列建模领域面临一个核心矛盾:如何既能像CNN一样高效并行计算,又能像RNN一样有效地捕获序列中的长距离依赖关系?
这个问题,催生了一个革命性的解决方案——注意力机制(Attention)。
长依赖问题与 Attention 动机
长依赖问题
长依赖(Long-Term Dependency)问题是指:模型在处理长序列时,开头的信息往往在传播到结尾时已经“淡忘”了。
举个例子:
“穿着她上个月在巴黎买的那条漂亮的、带着精致刺绣的、在阳光下会微微反光的裙子,……(中间省略100字)……,它非常合身。
当计算机读到句末的“它”时,它需要知道“它”指的是很远很远的开头提到的“裙子”。对于RNN来说,经过中间100个词的“长途跋涉”,关于“裙子”的记忆早就模糊了,这就是长依赖问题。
Attention(注意力机制)
Attention 的核心机制可以用一个日常类比来解释:假设你在阅读一本书的过程中遇到不明白的地方,会回过头去“关注”之前相关的章节内容。对于翻译模型来说,解码某个中文句子的过程中,不需要死记硬背整句源文本的全部信息,可以在翻译每个词时动态地“查询”源句中最相关的单词信息。注意力机制正是这样的“查询”过程:解码器在生成每个词时,对编码器输出的所有隐状态赋予不同的权重(即注意力分数),从而有选择地聚焦于与当前翻译位置最相关的源语单词。通过这种方式,模型能够灵活地处理长句,避免了将整句信息压缩到固定长度向量所导致的信息丢失
想象翻译这句话:
英文输入: “The animal didn’t cross the street because it was too tired.”
中文输出: “这个动物没有过马路,因为它太累了。”
关键问题: 翻译到“它”的时候,模型怎么知道“它”指的是“动物”而不是“街道”?
-
旧方法(无Attention):死记硬背
模型需要先把整个英文句子压缩成一个“总结向量”。在长句子中,“it”所指代的“animal”这个信息很可能在压缩过程中被弱化或丢失,导致翻译错误。 -
新方法(有Attention):按需索查
Attention机制让模型在生成每一个输出词(如“它”)时,都能回头去审视所有输入词,并自动为每个输入词计算一个“关注度分数”。
当模型要输出“它”时,它会自动地、非常高度地关注输入中的 “animal” 这个词。
同时,它会忽略当时不相关的词,如 “street”, “cross”。
简单来说:Attention就是让模型在输出时,具备“按需索查”的能力——需要什么信息,就直接去输入里找什么信息,而不是依赖一个可能已经失真的总结。
为什么这是突破?
解决长依赖: 无论“animal”和“it”相隔多远,模型都能直接建立连接,距离不再是问题。
高效且强大: 这种“按需索查”的机制非常灵活,为Transformer完全摒弃RNN、实现并行计算奠定了基础。
一句话总结:Attention就是让模型在输出每个词时,学会自动在输入中“划重点”。
这个“快速扫视并锁定关键信息”的过程,就是Attention(注意力)的本质。
Encoder-Decoder 架构概述
Transformer 延续了序列转换模型常用的**Encoder-Decoder(编码器-解码器)**架构。我们先简单了解这一架构的工作流程,再深入剖析 Transformer 的特殊之处。
想象有一个专业的翻译团队,负责把英文报告翻译成中文。这个团队由两个部门组成:
- 编码器(Encoder):“理解部门”。他们的任务是深度阅读和理解整篇英文报告。
- 解码器(Decoder):“撰写部门”。他们的任务是根据理解部门的分析,逐句写出高质量的中文报告。
编码器(Encoder)的工作——“理解部门”在做什么?
传统团队(RNN模型)的做法是:第一个人读第一段,把理解告诉第二个人;第二个人结合第一个人的理解和第二段,把汇总的理解告诉第三个人……这样依次传递下去。最后一个人要努力记住整篇报告的内容,压力很大,容易忘记开头。
Transformer 团队的做法更高效:
- 集体同时阅读:“理解部门”的所有成员同时阅读整篇报告。每个人负责报告中的一个句子(或一个词)
- 激烈讨论(自注意力机制):阅读后,他们不会各自为政。相反,他们会开一个讨论会。例如,负责第3句话的成员会说:“我这句话里有个‘它’,我得问问负责第1句话的同事,这个‘它’指的是什么?” 负责第5句话的成员会说:“我这句话是个总结,需要参考第2句和第4句的细节。”
- 产出深度笔记:经过充分讨论后,每个成员都对自己负责的那句话有了结合了全文上下文的深度理解。他们各自写下一份深度笔记。
最终输出:编码器输出的不是一份总结,而是一整套深度笔记 H = (h1, h2, …, hn)。每份笔记都包含了对应词的信息,并且融入了全文所有词的上下文信息
解码器(Decoder)的工作——“撰写部门”在做什么?
“撰写部门”的任务是逐词生成中文报告。他们不能看原文,唯一的信息来源就是“理解部门”提供的那套深度笔记 H。
生成过程是逐步进行的,假设现在要写中文报告的第三句话:
-
看已写内容( masked 自注意力):负责撰写的同事首先会回顾已经写好的前两句话(“我昨天”),以此来确定现在的写作基调和水到渠成的下一个词应该是什么。关键限制是:他不能偷看自己还没写出来的内容(这就是 Masked 自注意力)。
-
查询笔记(编码器-解码器注意力):接下来,他要决定下一个词,比如是“去了”还是“买了”。他会拿着当前的问题(基于已生成的“我昨天”),去查询“理解部门”留下的那套深度笔记 H。他会问:“在我的原文中,哪个部分对生成下一个词最重要?”
- 比如,如果原文对应的是“I went to…”,那么笔记中关于“went”的部分就会给出最强的信号。
- 这个过程就是注意力机制的核心:解码器动态地、有选择地从编码器的全部输出中提取最相关的信息。
-
写下词语:结合 “已写内容” 和 “从笔记中查询到的最相关信息” ,这位同事最终确定并写下“去了”这个词。
-
重复:然后,他以“我昨天去了”为基础,重复步骤1-3,生成下一个词,直到整句话完成。
Transformer 的 Encoder-Decoder 架构精髓在于:
Encoder 像是一个理解者,通过自注意力为输入序列的每个部分生成一个包含全局信息的“深度表示”。
Decoder 像是一个生成者,它利用 Masked 自注意力关注已生成的内容,并通过编码-解码注意力像探照灯一样有选择地从 Encoder 的输出中提取信息,从而生成下一个词。
这个设计使它摆脱了RNN的顺序计算瓶颈,实现了强大的并行能力和卓越的性能,成为了当今AI领域的基石架构。
二、Transformer架构全解析
1.总体架构


2.1 Embedding 层
为什么需要Embedding层?
神经网络,包括Transformer,其核心是张量(数字矩阵)运算。它们无法直接处理人类可读的原始文本(字符串)。因此,我们需要一种方法将文本转换为数值表示。最初的尝试:One-Hot编码
- 为词汇表中的每个词分配一个唯一的整数ID(如:“猫”=1,“狗”=2,“鱼”=3)
- 将一个词表示为一个长度为词汇表大小的向量,其中只有对应ID的位置为1,其余全为0。
- 例如,词汇表V=[“猫”, “狗”, “鱼”],则"猫" = [1, 0, 0],“狗” = [0, 1, 0]
One-Hot编码的严重缺陷:
- 高维稀疏:词汇表动辄数万甚至数十万词,导致向量维度极高,且每个向量中绝大部分元素为0,计算和存储效率低下。
- 无法表达语义关系:任意两个不同的One-Hot向量之间的点积(一种相似度度量)恒为0。这意味着模型无法从表示本身得知“猫”和“狗”都是动物,比“猫”和“苹果”更相似。
Embedding层的解决方案:
Embedding层的核心思想是学习一个稠密、低维的向量表示。
- 稠密:向量的每个维度都是一个有意义的实数,而不是大部分为0
- 低维:典型的嵌入维度(如256, 768)远小于词汇表大小(如30000)。
- 可学习:这些向量作为模型参数,在训练过程中根据任务目标(如预测下一个词、分类)自动调整。语义相近的词(如“猫”、“狗”)会在向量空间中彼此靠近,从而自然捕获了词语间的语义和语法相似性。
简单来说,Embedding层的作用是将离散的符号(词语)映射到连续的语义空间,为模型提供富含语义信息的数值化输入。下面,我们详细分解这一过程。
1.分词(Tokenization)
目标:将一串连续的、人类可读的自然语言文本,切分成一个个模型能够理解的“单元”,并为每个单元分配一个唯一的数字ID。
1. 选择分词器(Tokenizer)
现代Transformer(如BERT,GPT)通常使用子词分词法(Subword Tokenization),例如WordPiece(BERT)、Byte-Pair Encoding (BPE)(GPT)、SentencePiece等。
这种方法的好处是能很好地平衡词汇表大小与未登录词(OOV)问题。它可以将陌生长词拆分成已知的、更小的子词甚至字符。
为什么使用子词分词?
-
平衡词汇表大小与未登录词(OOV)问题:如果使用单词级分词,词汇表会非常庞大,且无法处理训练时未见过的新词。字符级分词词汇表小,但序列过长,模型难以学习长距离依赖。子词分词是理想的折中方案。
-
有效处理未知词:它可以将陌生长词拆分成已知的、更小的子词甚至字符。例如,即使模型没见过“unhappiness”,但认识“un”、“happiness”,就能成功处理。
2.执行分词
例如,使用BERT的WordPiece分词器对句子进行分词:
-
原始文本: “I love natural language processing.”
-
分词后: [“I”, “love”, “natural”, “language”, “processing”, “.”]
-
对于一些语言或复杂词,可能会被进一步拆分:
- “unhappiness” -> [“un”, “##happiness”] (##表示此前缀需要与前一个token连接)
- “playing” -> [“play”, “##ing”]
3.映射到ID
每个分词器内部都维护着一个词汇表(Vocabulary),这是一个从Token字符串到唯一ID(整数)的映射。
将上一步得到的Token序列转换为ID序列:
- [“I”, “love”, “natural”, “language”, “processing”, “.”] -> [101, 2342, 12345, 3456, 5678, 102]
额外处理
通常会添加一些特殊Token:
-
[CLS]:位于序列开头,用于分类任务的总表示。
-
[SEP]:用于分隔两个句子(例如在问答或自然语言推理任务中)。
-
[PAD]:用于将不同长度的序列填充到相同的长度。
所以最终的ID序列可能是:[[CLS], 101, 2342, 12345, 3456, 5678, 102, [SEP], [PAD], [PAD]]
至此,文本已经变成了一个数字序列,但计算机还不能理解这些数字的“含义”。
嵌入(Embedding)
目标:将每个表示“符号”的ID数字,转换为一个稠密的、低维的、蕴含语义信息的向量(即词向量)
1. 嵌入层(Embedding Layer)
模型内部有一个可学习的矩阵,称为嵌入矩阵(Embedding Matrix)。它的尺寸是 (词汇表大小V, 模型维度d_model)。
- 例如,词汇表有30000个词,模型维度是768,那么这个矩阵的大小就是 30000 x 768。
这个矩阵的每一行都对应词汇表中一个ID所代表的Token的预定义向量。训练过程就是不断优化这个矩阵中的数值。
2. 查找(Lookup)
嵌入过程实际上是一个**查找表(Lookup Table)**操作。
对于上一步得到的ID序列 [101, 2342, 12345, …],我们拿着每个ID作为索引,去嵌入矩阵中找到对应的第101行、第2342行、第12345行…。
这样,每个ID都被转换为了一个d_model 维的向量。
假设 d_model = 4(实际中更大),那么转换可能如下:
-
ID: 101 -> [0.2, -1.5, 0.8, 1.0]
-
ID: 2342 -> [1.2, 0.5, -0.3, -1.2]
至此,我们得到了一个向量序列,其形状为(序列长度L, 模型维度d_model)。这个序列已经包含了Token的语义信息(因为嵌入矩阵是在训练过程中从数据中学到的)。
位置编码 (Positional Encoding)
为什么需要位置编码?
Transformer的核心——自注意力机制(Self-Attention)——在数学上是置换不变(Permutation Invariant) 的。这意味着,当它处理一个Token序列时,它只会关注Token之间的两两关系,而完全忽略了它们的出现顺序。
- 严重后果:对于自注意力层而言,输入序列 “猫吃鱼” 和 “鱼吃猫” 的计算结果可能几乎相同,因为它只看到‘猫’、‘吃’、‘鱼’这三个词存在关联,而无法区分谁在先谁在后。这显然不符合语言逻辑
解决方案:必须将每个Token在序列中的位置信息显式地注入到模型中。
1. 生成位置信息
为序列中的每个位置(0, 1, 2, …, L-1)也生成一个与词向量维度相同的向量(d_model维)。这个向量包含了该位置的绝对或相对位置信息。
- 正弦/余弦位置编码(Sinusoidal):原始Transformer论文使用的方法。通过不同频率的正弦和余弦函数计算出一个固定的、无需学习的编码。优点是能泛化到比训练时更长的序列。
正弦/余弦位置编码(Sinusoidal Positional Encoding)
设计目标
- 为模型提供绝对位置信息:让模型知道每个词是序列中的第几个。
- 能够泛化到比训练时更长的序列:因为编码方式是基于数学公式的,所以即使遇到一个从未在训练中见过的更长序列位置,模型也能计算出其位置编码。
- 为模型提供相对位置信息:通过正弦余弦波的特性,任何位置 pos + k 的编码都可以通过位置 pos 的编码线性表示,这有助于模型轻松地学习到注意力机制中的相对位置关系。
计算公式
对于位置 pos(序列中的第 pos 个词,从 0 开始)和维度 i(嵌入向量的第 i 维,i 从 0 到 d_model-1):
- pos:词在序列中的位置。
- d_model:嵌入向量的总维度(例如 512, 768)。
- i:维度索引。注意公式中,偶数维度(2i)使用正弦函数,奇数维度(2i+1)使用余弦函数。
这个公式可以理解为:每个维度对应一个不同波长(频率)的正弦波。维度越低(i 小),波长越长(频率越低);维度越高(i 大),波长越短(频率越高)。这样,每个位置的所有 d_model 个维度组合起来,就形成了一个独一无二的“位置指纹”。
优点
- 确定性:无需学习,直接计算得到。
- 外推性:可以处理比训练时更长的序列。
- 提供了良好的相对位置信息。
缺点
- 固定不变:无法根据具体任务和数据自适应地调整位置表示。
- 可学习的位置编码(Learned Positional Embedding):BERT、GPT等模型使用的方法。将其视为一个可学习的参数,为每个可能的位置(如0到512)随机初始化一个向量,并在训练中更新。这种方式更简单,且通常效果很好。
可学习的位置编码 (Learned Positional Embedding)
这是 BERT、GPT 等现代 Transformer 模型普遍采用的方法。它非常直观:将位置也视为一个需要学习的嵌入层
设计理念
既然每个词的语义可以通过一个可学习的嵌入矩阵(nn.Embedding)来获得,那么每个位置的“语义”(即位置信息)为什么不能也通过同样的方式获得呢?
实现方式
- 定义一个位置嵌入层:self.pos_embedding = nn.Embedding(max_seq_len, d_model)
- max_seq_len:模型能处理的最大序列长度(例如 512, 1024, 2048)
- d_model:嵌入维度,必须与词嵌入的维度相同。 - 这个嵌入矩阵的每一行代表一个特定位置(0, 1, 2, …, max_seq_len-1)的向量表示。
- 在前向传播时,生成一个位置序列 [0, 1, 2, …, seq_len-1],然后通过 nn.Embedding 层查表,得到所有位置的特征向量。
- 将这些位置向量与词向量相加。
优点
- 灵活性:模型可以根据特定任务和数据,学习到最合适的位置表示。也许模型会发现某些位置有特殊的语义,这是固定编码无法做到的
- 简单高效:实现非常简单,就是一个查表操作,计算速度快。
缺点
- 长度受限:模型无法处理超过 max_seq_len 的序列。这是 BERT 等模型的一个硬性限制。
- 缺乏外推性:对于比训练时长度的序列,模型表现会显著下降,因为超过 max_seq_len 的位置它从未学习过。
2. 相加融合
将每个Token的词向量与其对应的位置编码向量按元素相加。
最终输入向量 = 词向量 + 位置编码向量
这个过程可以形象地理解为:给每个词向量“染上”了它在句子中位置的颜色,使得模型能够区分“我打你”和“你打我”。。
总结:最终的输入向量 x
经过以上三步,我们得到了Transformer编码器(Encoder)第一层的真正输入 x:
-
它是一个形状为 (序列长度L, 模型维度d_model) 的矩阵。
-
矩阵中的每一行对应一个Token,这个Token的向量既包含了它的语义信息(来自Embedding),也包含了它的位置信息(来自Positional Encoding)。
这个 x 矩阵随后就会被送入Transformer的多头自注意力层和前馈神经网络层进行深层的特征提取和计算。
简单比喻:
-
分词:把一篇文章拆分成一个个单独的“乐高积木块”(Tokens),并给每个积木块贴上一个编号(ID)。
-
嵌入:根据编号,从一个盒子里找到对应的积木块,这个积木块本身有颜色和形状(向量),代表了它的含义。
-
位置编码:根据这个积木块在整体模型中的位置(第几个),给它贴上一个小标签或涂上一点特殊的颜色,标明它应该放在哪里。
-
输入向量x:最终,你手上拿着的就是一块既知道“自己是什么”(语义),又知道“自己该在哪”(位置)的准备好被组装(计算)的积木块。
A. 示例:分词 → ID → 嵌入 → + 位置(ASCII 示意)
1 | "I love NLP." |
Encoder Layer
经过准备的输入数据 now 被送入一个个编码器层。每个层都由两个核心子层构成
多头自注意力机制 (Multi-Head Self-Attention)
这是 Transformer 最伟大、最核心的思想
自注意力 (Self-Attention)
-
核心思想: 让序列中的每个词都“环顾四周”,看看其他词,然后根据其他词的重要性来重新塑造自己的表示。
-
例如:你在派对上听到有人说“它真可爱,一直在玩那个闪亮的球”,你的大脑会瞬间做注意力计算:
- 查询 (Query): 你听到“它”,想知道“它”指什么
- 键 (Key): 你回想之前听到的所有词:“好奇的”、“猫”、“追”、“闪亮的”、“球”。
- 值 (Value): 这些词本身的信息
-
你发现“它”和“猫”的关联性(相似度)最高,于是你得出结论:“它”很可能指的是“猫”。这个过程就是自注意力,你通过关注其他词(“猫”)来更新了对“它”的理解。
计算过程如下:
-
创建Q(Query),K(Key),V(Value)向量
对于输入序列中的每个单词(由其嵌入向量 x_i 表示),我们需要将其转换为三个不同的向量:Q查询向量(Query)、K键向量(Key) 和V值向量(Value)
如何创建?
通过将单词的嵌入向量 x_i 与三个在训练过程中学习得到的权重矩阵 W^Q, W^K, W^V 相乘。- Q = x_i . w_q
- K = x_i . w_k,
- V = x_i . w_v

-
计算注意力分数(Attention Scores)
现在,假设我们正在计算第一个单词 “Thinking” 的自注意力。我们需要评估输入序列中每个单词与 “Thinking” 的相关性。这个相关性通过注意力分数来衡量
如何计算? 取当前单词的查询向量(q₁) 与序列中所有单词的键向量(kᵢ) 依次进行点积(Dot Product)
- 分数₁ = q₁ · k₁ (“Thinking” 与自身的分数)
- 分数₂ = q₁ · k₂ (“Thinking” 与 “Machines” 的分数)
点积运算的结果越大,表示两个向量的相关性越高。

-
缩放(scale)
将上一步得到的分数除以一个常数(通常是键向量维度 d_k 的平方根,在您的例子中是 √64 = 8)。这是为了在反向传播时拥有更稳定的梯度。当 d_k 很大时,点积的结果可能非常大,导致softmax函数的梯度消失
-
应用Softmax
将缩放后的分数通过Softmax函数
Softmax有两个作用:- 将所有分数归一化,使得它们均为正数且总和为1
- 强化最高分数,抑制低分数
这些Softmax后的结果就是注意力权重。它代表了在编码“Thinking”这个位置时,应该给予其他每个位置的关注程度。

-
加权求和得到输出
- 加权(Weight):将每个单词的值向量(vᵢ) 乘以上一步得到的对应的注意力权重。这背后的直觉是:保留我们想要重点关注的那个单词的完整值向量,而淹没不相关的单词(通过乘以一个极小的权重)。
- 求和(Sum):将第五步中所有加权后的值向量求和。这个求和后的结果向量就是自注意力机制在当前位置(对于单词“Thinking”)的输出(z₁)。它是一个包含了序列中所有相关单词信息的全新表示.

矩阵形式的计算
- 计算Q, K, V矩阵
将整个输入序列的嵌入向量打包成矩阵 X(形状为 (序列长度, d_model)),然后一次性乘以其对应的权重矩阵。

- 合并计算步骤

掩码自注意力
为什么需要掩码
想象一下,你正在参加一场开卷考试,规则非常奇特:
1.普通自注意力:考试的规则是,你可以翻阅整本教科书(包括后面的答案)来回答任何一个问题。这当然能让你考高分,但这也是一种“作弊”,因为你提前知道了所有信息
2.掩码自注意力:现在规则变了。当你回答第3题时,你只能看书的前3章;回答第5题时,你只能看书的前5章。你无法看到未来的章节。这迫使你只能根据当前及之前出现的信息来推理和预测。
掩码主要有两个目的:
- 防止信息泄露(用于解码器):在训练时,我们需要防止模型在预测序列中的第 t 个位置时,“偷看”到 t+1 及之后位置的真实答案(即未来信息)。掩码确保了这种“作弊”不会发生,让模型只能基于已生成的内容进行预测。这被称为 因果掩码(Causal Masking) 或 前瞻掩码(Look-ahead Masking)。
- 处理变长序列(用于编码器和解码器):在实际批次训练中,句子长度不同。短的句子需要被 填充(Padding) 到和最长句子一样的长度。这些填充 token(通常是
)是无意义的。我们需要用一个 填充掩码(Padding Mask) 来告诉模型:“忽略这些填充位置,不要在这些位置上计算注意力”。
为什么需要掩码
想象一下,你正在参加一场开卷考试,规则非常奇特:
1.普通自注意力:考试的规则是,你可以翻阅整本教科书(包括后面的答案)来回答任何一个问题。这当然能让你考高分,但这也是一种“作弊”,因为你提前知道了所有信息
2.掩码自注意力:现在规则变了。当你回答第3题时,你只能看书的前3章;回答第5题时,你只能看书的前5章。你无法看到未来的章节。这迫使你只能根据当前及之前出现的信息来推理和预测。
1.因果掩码(Causal Mask)
1. 因果掩码(Causal Mask)
让我们回到那个简单的例子:
句子:[“I”, “like”, “cats”]
翻译结果(目标序列):“Je like les chats”
在训练阶段,我们已知整个目标序列。但如果模型在预测第二个词 “like” 时,就看到了第三个词 “cats” 的信息,训练就失去了意义。因为在推理时(生成时),模型是看不到未来词的。
实现步骤
1.计算注意力分数
首先,正常计算 Query 和 Key 的点积,得到注意力分数矩阵:
2.应用掩码矩阵
然后,我们加上(或者说“盖上”)一个掩码矩阵。这个掩码矩阵上三角部分(不包括对角线) 的值是一个极大的负数(如 -1e9 ),而其他位置是 0 。

- 为什么是极负数?
因为经过 Softmax:
Softmax(极大负数) ≈ 0
这样被掩码的位置注意力权重几乎为零。
掩码后的分数矩阵
| Token | I | like | cats |
|---|---|---|---|
| I | 分数 | -inf | -inf |
| like | 分数 | 分数 | -inf |
| cats | 分数 | 分数 | 分数 |
- 对于 I(位置0):只能关注自己
- 对于 like(位置1):只能关注位置 0 和 1
- 对于 cats(位置2):可以关注位置 0, 1, 2
3.Softmax + 加权求和
最后,对掩码后的分数矩阵进行 Softmax,并用它对 Value 向量加权求和,得到输出向量 Z。
这样,每个位置的表示仅依赖它之前及自身的信息,从而保证了训练与推理一致性。

2.填充掩码(Padding Mask)
假设我们有一个批次(Batch)包含两个句子:
- Sentence 1: [“I”, “like”, “cats”,
, ] (实际长度=3,填充到5) - Sentence 2: [“He”, “runs”,
, , ] (实际长度=2,填充到5)
我们希望模型忽略所有的token。
如何实现?
我们同样使用一个掩码矩阵,但逻辑与因果掩码不同。
1.创建Padding Mask向量:我们通常会有一个注意力掩码(attention_mask),其形状为 (batch_size, seq_len)。实际词的位置为1,填充位置为0。
- For Batch: attention_mask = [[1, 1, 1, 0, 0], [1, 1, 0, 0, 0]]
2.应用到注意力分数:我们需要将这个向量扩展成一个可以和注意力分数矩阵相加的形状。
-
将 attention_mask 扩展为 (batch_size, 1, 1, seq_len)。1 的维度是为了方便广播(Broadcasting)。
-
然后,我们创建一个“反向”掩码:mask = (attention_mask == 0)。即填充位置是 True(或1),实际位置是 False(或0)。
-
最后,我们执行:attention_scores = attention_scores.masked_fill(mask, -1e9)。这会将所有 mask 为 True 的位置(即填充位置)的值替换为 -1e9。
这样,在计算注意力时,模型就不会从那些无意义的
在实际应用中,因果掩码和填充掩码通常会叠加使用!
多头自注意力
想象一下,你正在阅读一本非常复杂的小说,里面有很多人物和错综复杂的关系。现在,你想彻底理解其中一句话,比如:
“哈利对赫敏施了一个咒语,因为她弄坏了他的魔杖,但罗恩觉得这很酷。”
为了完全理解这句话,你的大脑会本能地做以下几件事:
- 聚焦关键词:你会注意到“哈利”、“赫敏”、“罗恩”、“咒语”、“魔杖”这些核心词
- 分析关系:
- 谁对谁:哈利 -> 赫敏(施咒语)
- 为什么:因为赫敏 -> 哈利的魔杖(弄坏了)
- 谁怎么看:罗恩 -> 哈利的行为(觉得酷)
- 多角度分析:你可能会从情感角度(赫敏可能感到愧疚)、因果角度(弄坏魔杖是起因)、人物性格角度(罗恩喜欢冒险)等多个层面来理解这句话
多头自注意力机制就是让计算机模仿这个“多角度分析”过程
-
自注意力(Self-Attention):让句子中的每个词都去“看”一遍句子中的所有其他词(包括自己),并思考:“我和你们每个人的关联程度是多少?”然后根据这些关联程度,重新调整自己的“价值(Value)”。
-
多头(Multi-Head):我们不只做一次这种“看”的操作,而是同时进行很多次(多个头)。每个“头”就像一个专注于不同方面的“专家”。
- 头A(语法专家):可能更关注谁对谁做了什么(动词与主语、宾语的关系)。
- 头B(情感专家):可能更关注情感词(“酷”是积极的,“弄坏”是消极的)。
- 头C(指代专家):可能更关注代词指代(“他”指的是哈利,“她”指的是赫敏)。
最后,我们把所有专家的分析结果汇总起来,得到对这句话更丰富、更立体的理解

数学公式详解
- 输入投影到多个子空间
对于每个头 i(i = 1, 2, …, h),我们有独立的投影矩阵:
Q_i = X.Wq_i
K_i = X.Wk_i
V_i = X.Wv_i
W矩阵的维度是[d_model,d_k],d_k通常为d_model // h(head) - 每个头独立计算注意力

- 拼接所有头的输出

- 最终线性投影

前馈神经网络
虽然它叫“前馈神经网络”(Feed-Forward Network),但并没有完全体现出它的精髓。在Transformer里,它更像一个位于自注意力机制(Self-Attention) 之后的“超级加工厂”,它的职责是将自注意力机制提供的“相关信息”,映射为“对当前任务更有用的高级特征”。
-
自注意力机制 (Self-Attention) - “社交网络”:它的工作是让句子中的每个词都去“关注”其他相关的词,从而收集和聚合全局信息。就像你在一个派对上,和每个人交谈后,知道了“谁是谁”、“发生了什么”。最终,你对当前对话的上下文有了全面的了解。
-
前馈神经网络 (FFN) - “个人思考与创造”:它的工作不是再去收集信息,而是对自注意力提供的这堆已经聚合好的信息进行深度的、非线性的加工。就像你离开派对后,独自一人消化今晚的见闻,进行深度思考,形成自己独特的观点和见解,甚至能创造出新的想法。
一个有趣的例子:翻译句子
句子:The cat sat on the mat, which was very comfortable.
1.对于单词 which:通过自注意力,它知道了自己指代的是 mat,并且 comfortable 这个属性也在描述 mat。它现在拥有的信息是:{自身含义: ‘which’}, {指代对象: ‘mat’}, {对象属性: ‘comfortable’}。这是一堆关联信息。
2.对于单词 which:FFN接收了自注意力提供的“信息包”(which, mat, comfortable)。它的加工过程不是简单的汇总,而是一种高级的融合与转换:
- 升维:在一个更高维的“思考空间”里,将“垫子”(mat)和“舒适的”(comfortable)这些概念拆解成更基本的特征(比如:[+家居, +织物, +柔软, +让人放松…])
- 非线性激活 (ReLU):过滤掉不重要的或矛盾的特征组合,保留最核心的。比如,它可能会强化 +柔软 和 +让人放松 之间的联系。
- 降维:将这些激活后的高级特征重新组合成一个全新的、更丰富的语义表示。这个新表示不再是简单的“which”这个词,也不是“mat”和“comfortable”的简单相加,而是一个蕴含着“一个舒适的垫子”这整个概念的、准备被用于生成目标语言的上下文向量。
FFN细节
Transformer 的前馈神经网络构成很简单,是由两个线性层中间加一个 RELU 激活函数组成的,以及还加入了一个 Dropout 层来防止过拟合
FFN的设计有一个非常有趣的特点:它采用了一种“扩展-压缩”的模式,也称为“Bottleneck”结构:
-
第一步:扩展 (Expansion)
- 输入向量 x 的维度是 d_model(例如512维)。
- 第一层线性层 W₁ 将其映射到一个更高维的空间 d_ff(通常是 d_model 的4倍,即2048维)。
- 为什么这么做? 在高维空间中,模型可以更容易地学习和表示更复杂的特征组合。想象一下,把一团交织的毛线(复杂特征)放在一个更大的空间里,它就更容易被解开。
-
第二步:激活 (Activation)
- 使用ReLU或GELU等非线性函数。这一步是引入非线性的关键,它决定了高维空间中的哪些特征应该被激活(输出正数),哪些应该被抑制(输出0)。
-
第三步:压缩 (Compression)
- 第二层线性层 W₂ 将维度从 d_ff(2048维)再压缩回 d_model(512维)。
- 为什么这么做? 为了保持架构的稳定性。每个编码器层的输入和输出维度必须一致,这样它们才能被无缝地堆叠起来。最终输出的512维向量,是一个经过深度加工和提炼后的新特征,它将被送入下一个编码器层或用于最终任务。
为什么FFN如此重要
-
赋予非线性:正如前文所说,ReLU激活函数是模型理解复杂模式和关系的核心。
-
独立处理:与自注意力不同,FFN对序列中的每个位置进行独立、相同的处理。这意味着它的计算效率极高,可以完全并行化。
-
层级特征提取:每个Transformer层都有一个FFN。低层的FFN可能加工一些基础的语法特征,而高层的FFN则可能加工更抽象的语义特征(如情感、指代等),共同构成了一个深度的特征提取 pipeline。
层归一化
想象一下,你正在训练一个深度学习模型。数据在网络中流动,每一层的输出值(也称为激活值)的分布可能会发生剧烈变化,这被称为内部协变量偏移(Internal Covariate Shift)。
批量归一化(Batch Normalization, BN) 是解决这个问题的一种方法,它在一个批次(Batch) 的数据中,对每个特征通道(Feature Channel) 进行归一化(即减去均值,除以标准差)。这在CNN中效果很好。
但是,在Transformer这样的序列模型中,BN会遇到问题:
- 序列长度可变:不同句子的长度不同,用小批量训练时,计算出的均值和方差可能非常不稳定。
- 推理时的差异:训练时用一个批次的统计量,推理时用整个训练集估算的移动平均统计量,这可能会带来不一致。
于是,产生了层归一化(Layer Normalization, LN)
LN的核心思想:不再沿批次维度,而是沿特征维度进行归一化。
对于一个样本,LN会计算该样本所有特征的均值和方差,然后用它们来归一化这个样本。这意味着:
- 与批次大小无关:LN的计算不依赖于批次中其他样本,因此无论批次大小是1还是100,计算方式都一致。这完美适配了可变长度序列和推理场景。
- 稳定化输出:它将每一层的输入都稳定到一个相似的尺度(均值为0,方差为1),从而加速训练(可以使用更大的学习率),并缓解梯度消失问题。
一个有趣的例子:翻译句子
假设你是一个老师,有一个4名学生的小班(Batch Size=4),每个学生有3门成绩:语文、数学、英语(Feature Dimensions=3)。
学生A: [语文=90, 数学=60, 英语=75] -> 平均分: (90+60+75)/3 = 75
学生B: [语文=80, 数学=70, 英语=65] -> 平均分: 71.67
学生C: [语文=50, 数学=90, 英语=80] -> 平均分: 73.33
学生D: [语文=85, 数学=55, 英语=95] -> 平均分: 78.33
- 批量归一化(BN)会怎么做?
BN关心的是单科成绩在全班的分布。
它会先计算全班语文的平均分 (90+80+50+85)/4 = 76.25 和方差,然后把每个学生的语文成绩归一化。
接着计算全班数学的平均分 (60+70+90+55)/4 = 68.75 和方差,归一化数学成绩。
英语同理。
BN的核心:针对同一特征,跨样本归一化。
2. 层归一化(LN)会怎么做?
LN关心的是单个学生所有科目成绩的分布。
它先看学生A:计算他三科的平均分 75 和方差,然后用这个均值和方差去归一化他自己的三门成绩 [90, 60, 75] -> [(90-75)/σ, (60-75)/σ, (75-75)/σ]。
接着处理学生B:计算他自己的平均分 71.67 和方差,归一化他自己的成绩 [80, 70, 65]。
学生C、D同理。
LN的核心:针对同一样本,跨特征维度归一化。
在Transformer中,每个词元(Token)就像是一个“学生”,它的所有特征向量(Embedding Vector)就是它的“各科成绩”。LN的作用就是让每个词元自身的特征分布变得稳定。
数学公式
对于一个输入向量 x = (x₁, x₂, …, xₙ)(代表一个样本的所有特征),层归一化的计算如下:
计算均值和方差:
(加上一个小常数ε防止除零)
归一化:
缩放和偏移(可学习参数):
最后一步的γ(gamma) 和 β(beta) 是关键!它们是可学习的参数。
γ(缩放):允许模型决定归一化后的分布应该有多“宽”。
β(偏移):允许模型决定归一化后的分布中心应该在哪。
这赋予了模型灵活性,如果它认为归一化没必要,甚至可以通过学习将γ设为方差σ,β设为均值μ来恒等还原原始输入。这保证了网络的表达能力不会因为归一化而下降。
残差连接
- 技术核心:解决深度网络中的梯度消失/爆炸问题
深度神经网络在训练时,误差梯度需要从最后一层反向传播回第一层。这个过程涉及连续的乘法(链式法则)。如果每层的梯度值都小于1,多次连乘后,传到前面层的梯度会变得无限小(消失),导致前面的层无法有效更新(训练不动)。反之,如果梯度大于1,则会爆炸。残差连接通过加法将梯度直接“抄近道”回传,完美地缓解了这个问题。
一个有趣的例子
假设老师让你写一篇非常复杂的论文(训练一个深度网络),你需要反复修改很多次(很多网络层)。
没有残差连接的网络(普通网络):
这就像是老师要求你每次修改时,都必须重抄一遍全文,并只能在你新抄的这份上进行修改。
第一遍修改:你纠正了一些语法错误
第二遍修改:你在新抄的版本上优化了一些词句,但可能不小心把之前改好的某个语法错误又抄错了。
…
第十遍修改:你已经完全忘记了第一稿是什么样子。你也许在纠结一个段落的逻辑,但因为经过了太多层的“重抄和修改”,最初的灵感和核心观点已经变得模糊甚至丢失了。如果作文最终写得不好,老师很难判断是第几次修改出的问题。(这就是梯度消失:最初的输入信息在层层传递中丢失或扭曲,误差很难回溯到最初的修改步骤)。
有残差连接的网络:
老师换了一种更聪明的方法:他要求你永远保留着你的第一稿(原始输入 x)。每次修改时,你不需要重抄全文,而是拿出一张新的修改清单(F(x)),上面只列出你本轮想做的所有修改建议(比如“把A词换成B词”、“在C句后增加D例子”)。
最终的作文成果(输出 H(x))就是:原始第一稿 + 所有批注的修改清单。
这样做的好处是:
1.永不丢失核心:无论你修改多少遍,你最初最原始的想法和核心内容(第一稿)永远完整地保留着,不会被任何一轮糟糕的修改彻底毁掉。
2.责任清晰,高效改进:如果老师觉得最终作文里某个例子加得不好,他可以直接在你的“第五次修改清单”上画个叉,告诉你“这个修改建议不好”。这个反馈非常直接和精准。(这就是残差连接的作用:梯度可以沿着“原始稿”这条捷径直接、高速地回传,精准地告诉每一层的“修改清单”应该怎么调整)。
3.任务变简单:让网络层(你)从“写一篇全新的好文章”这个艰难任务,变成了“提供一份针对当前文章的修改建议”这个相对简单得多的任务。它只需要学习“差异”(F(x) = H(x) - x)就行了。
在Transformer中:
输入 x:就是你的“作文第一稿”。
Self-Attention/FFN 层:就是负责写出“修改清单” F(x) 的你。
残差连接 (x + F(x)):就是将原始稿和修改清单合并,得到最终成果。
Layer Normalization:就像是把合并后的作文再通读一遍,确保整体语言风格流畅统一,没有因为直接合并而产生突兀感。
这个比喻完美地解释了为什么残差连接如此强大:它保护了信息完整性,简化了学习目标,并建立了误差反向传播的“高速公路”
- 数学公式:极其简单却无比强大
残差连接的公式是Transformer乃至现代深度学习的基石之一,它简单得令人惊讶:
这里:
-
x:输入到某个模块(如Self-Attention或FFN)的原始值。
-
F(x):该模块对输入进行复杂计算后产生的变化或残差(Residual)。
-
H(x):该模块最终的输出。
网络不再学习一个完整的、全新的输出 H(x),而是学习输出与输入之间的差异 F(x) = H(x) - x。学习一个“差异”通常比学习一个完整的目标函数要容易得多。

