Skip to content

输入关键词开始搜索

注意力机制学习笔记:从 Bahdanau 注意力到 Transformer


本文记录我继续学习《动手学深度学习》第 10 章注意力机制时的理解过程。

前面我已经学习了 Query、Key、Value、注意力评分函数、softmax、加性注意力和缩放点积注意力。这一部分继续往后学,主要涉及 Bahdanau 注意力、多头注意力、自注意力、位置编码以及 Transformer。

刚开始看这些内容时,我觉得它们之间的关系比较容易混在一起:

Bahdanau 注意力和普通注意力有什么区别?
多头注意力为什么要分多个头?
自注意力为什么需要位置编码?
Transformer 编码器和解码器到底分别在做什么?
注意力热力图应该怎么看?

经过这一轮学习之后,我逐渐把这些知识点串了起来。现在我可以先用一句话概括:

Bahdanau 注意力解决的是解码器如何动态关注源序列的问题;多头注意力解决的是从多个子空间并行关注信息的问题;自注意力解决的是序列内部各位置如何直接建立联系的问题;位置编码解决的是自注意力缺少顺序信息的问题;Transformer 则是把这些模块组合成完整的序列到序列模型。


1. 从普通 Seq2Seq 到 Bahdanau 注意力

Section titled “1. 从普通 Seq2Seq 到 Bahdanau 注意力”

一开始我对 Bahdanau 注意力的理解比较模糊,只知道它是在编码器和解码器之间加了一个注意力机制。

后来我发现,关键问题在于传统 Seq2Seq 模型的上下文向量是固定的。

普通编码器-解码器模型通常会把整个源句子压缩成一个固定长度的上下文向量,然后解码器在生成所有目标词时都使用这个向量。

例如:

源句子:i'm home .
目标句子:je suis chez moi .

普通 Seq2Seq 的做法更像是:

编码器先把整个英文句子压缩成一个向量
解码器后面生成 jesuischezmoi 时都依赖这个固定向量

但是这样会有一个问题:

生成不同目标词时,模型真正需要关注的源词并不一样。

例如:

目标词更应该关注的源词
jei'm
suisi'm
chezhome
moihome
..

所以 Bahdanau 注意力的思想是:

解码器每生成一个目标词,都重新看一遍编码器所有时间步的隐藏状态,然后动态决定当前应该关注哪些源词。

它的上下文向量公式是:

ct=t=1Tα(st1,ht)ht\mathbf{c}*{t'}=\sum*{t=1}^{T}\alpha(\mathbf{s}_{t'-1},\mathbf{h}_t)\mathbf{h}_t

其中:

ht\mathbf{h}*t 是编码器第 tt 个时间步的隐藏状态
st1\mathbf{s}*{t'-1} 是解码器上一时间步的隐藏状态
α(st1,ht)\alpha(\mathbf{s}_{t'-1},\mathbf{h}*t) 是注意力权重
ct\mathbf{c}*{t'} 是当前解码步的上下文向量

我的理解是:

Bahdanau 注意力不是让解码器只看编码器最后一个隐藏状态,而是让解码器在每一步都可以从所有编码器隐藏状态中重新选择重点。


2. Bahdanau 注意力中的 Query、Key、Value

Section titled “2. Bahdanau 注意力中的 Query、Key、Value”

前面学过 QKV 之后,再看 Bahdanau 注意力就清楚很多。

在 Bahdanau 注意力中:

注意力概念对应内容
Query解码器上一时间步隐藏状态
Key编码器所有时间步隐藏状态
Value编码器所有时间步隐藏状态
Attention Output当前时间步上下文向量

也就是说,解码器当前想生成一个目标词时,会用自己的状态作为 Query,去和编码器每个时间步的 Key 做匹配。

匹配得分越高,对应的 Value 权重越大。

例如生成 je 时:

Query = 解码器当前状态
Key = 编码器中 i'mhome.<eos> 对应的隐藏状态
Value = 这些隐藏状态本身

如果模型判断 i'm 和当前 Query 最相关,那么 i'm 对应的隐藏状态权重就会更大。

这个地方让我意识到:

Bahdanau 注意力本质上还是 QKV 框架,只是 Query 来自解码器,Key 和 Value 来自编码器。


3. 用一个具体例子理解训练过程

Section titled “3. 用一个具体例子理解训练过程”

我一开始的问题是:

带 Bahdanau 注意力的编码器-解码器模型到底是怎么训练的?

后来我用一个具体样本把过程拆开:

源句子:i'm home .
目标句子:je suis chez moi .

训练时,目标句子会被拆成两部分:

解码器输入:<bos> je suis chez moi .
训练标签:je suis chez moi . <eos>

也就是:

时间步解码器输入模型应该预测
1<bos>je
2jesuis
3suischez
4chezmoi
5moi.
6.<eos>

这里使用的是 teacher forcing。

teacher forcing 的意思是:

训练时,解码器输入真实答案中的上一个词,而不是模型自己上一步预测出来的词。

例如训练第 2 步时,即使模型第 1 步预测错了,解码器输入仍然是真实的 je,然后让模型预测 suis

我的理解是:

teacher forcing 可以让模型在训练初期少受前面错误预测的影响,从而更稳定地学习每一步应该输出什么。


我一开始还不太清楚:

源句子进入编码器之前,token 是怎么变成数字的?

以:

i'm home .

为例,数据处理时会先加结束符 <eos>,再根据需要补 <pad>

i'm home . <eos> <pad> <pad> ...

然后根据源语言词表把每个 token 转成编号。

例如:

token编号
i'm37
home86
.4
<eos>3
<pad>1

于是源句子可能变成:

[37, 86, 4, 3, 1, 1, ...]

后来我理解到:

token 编号只是词表索引,它本身没有语义大小关系。

比如 home 的编号比 i'm 大,并不说明 home 更重要。编号只是为了让计算机能定位到词表中的某个 token。


5. token 编号怎么变成 32 维词向量

Section titled “5. token 编号怎么变成 32 维词向量”

编号本身不能直接表示词义,所以需要 Embedding 层。

如果 embed_size=32,那么每个 token 会被映射成一个 32 维向量。

例如:

i'm → 编号 37 → Embedding 矩阵第 37 行 → 32 维向量
home → 编号 86 → Embedding 矩阵第 86 行 → 32 维向量

如果源语言词表大小是 VsrcV_{src},那么源语言 Embedding 矩阵形状是:

(V_src, 32)

如果目标语言词表大小是 VtgtV_{tgt},那么目标语言 Embedding 矩阵形状是:

(V_tgt, 32)

这部分让我理解到:

Embedding 层本质上是一张可训练的查找表,token 编号只是用来查表,真正参与模型计算的是查出来的向量。


我继续追问:

编码器中的隐藏状态 h1,h2,h3,h4h_1,h_2,h_3,h_4 是怎么来的?

以源句子为例:

i'm home . <eos>

经过 Embedding 后,每个 token 都变成一个词向量:

x1 = Embedding("i'm")
x2 = Embedding("home")
x3 = Embedding(".")
x4 = Embedding("<eos>")

编码器 GRU 会按顺序处理这些词向量:

h0 = 初始隐藏状态
h1 = GRU(x1, h0)
h2 = GRU(x2, h1)
h3 = GRU(x3, h2)
h4 = GRU(x4, h3)

其中每个隐藏状态都不是一个数字,而是一个向量。

如果 num_hiddens=32,那么:

h1, h2, h3, h4 都是 32 维向量

我对隐藏状态的理解是:

h1h_1 主要包含 i'm 的信息
h2h_2 包含 i'mhome 的信息
h3h_3 包含 i'm home . 的信息
h4h_4 包含整个源句子的结束信息


GRU 不是简单地把当前输入和上一个隐藏状态相加,它内部有门控结构。

主要包括:

更新门
重置门
候选隐藏状态
最终隐藏状态

更新门:

zt=σ(Wxzxt+Whzht1+bz)z_t=\sigma(W_{xz}x_t+W_{hz}h_{t-1}+b_z)

重置门:

rt=σ(Wxrxt+Whrht1+br)r_t=\sigma(W_{xr}x_t+W_{hr}h_{t-1}+b_r)

候选隐藏状态:

h~t=tanh(Wxhxt+Whh(rtht1)+bh)\tilde{h}*t=\tanh(W*{xh}x_t+W_{hh}(r_t\odot h_{t-1})+b_h)

最终隐藏状态:

ht=ztht1+(1zt)h~th_t=z_t\odot h_{t-1}+(1-z_t)\odot\tilde{h}_t

我的理解是:

更新门决定保留多少旧信息。
重置门决定计算新信息时要遗忘多少过去信息。
候选隐藏状态表示根据当前输入和部分旧信息得到的新内容。
最终隐藏状态是旧信息和新信息的混合。

这里我还弄清楚了一点:

隐藏状态之所以是 32 维,不是因为后面又强行变成 32 维,而是因为 GRU 的参数矩阵从一开始就被设计成输出 num_hiddens=32 维。


8. 输出层的 WoW_obob_o 是哪里来的

Section titled “8. 输出层的 WoW_oWo​ 和 bob_obo​ 是哪里来的”

我后面又问了一个问题:

解码器输出 out1 之后,为什么经过全连接层就能得到目标词表概率?这里的 WoW_obob_o 是哪里来的?

假设解码器某一步输出:

out1: (32,)

如果目标词表大小是 10000,那么输出 Dense 层会有:

W_o: (10000, 32)
b_o: (10000,)

计算过程是:

logits1=Woout1+bologits_1=W_oout_1+b_o

输出:

logits1: (10000,)

也就是说,目标词表中每个 token 都会得到一个分数。

例如目标词 je 的编号是 20,那么:

logitje=Wo[20]out1+bo[20]logit_{je}=W_o[20]\cdot out_1+b_o[20]

我理解到:

WoW_obob_o 不是人工写死的,也不是从数据集中直接来的,而是 Dense 层自带的可训练参数。

它们在模型初始化时随机生成,然后通过训练不断更新。


9. 哪些是模型参数,哪些不是参数

Section titled “9. 哪些是模型参数,哪些不是参数”

我一开始容易把注意力权重、隐藏状态、logits 都当成参数。

后来整理后发现,真正需要训练的是模型中的权重和偏置,而不是前向传播时临时算出来的结果。

Bahdanau Seq2Seq 中需要训练的参数包括:

模块可训练参数
源语言 Embedding源语言词向量矩阵
编码器 GRU门控结构中的权重和偏置
Bahdanau 注意力层Query、Key 变换矩阵和打分向量
目标语言 Embedding目标语言词向量矩阵
解码器 GRU解码器门控结构中的权重和偏置
输出 Dense 层WoW_obob_o

不是训练参数的内容包括:

内容说明
token 编号词表索引,固定不变
隐藏状态前向传播临时计算
注意力权重softmax 动态计算
context 向量注意力加权求和结果
logits输出层临时计算结果
loss损失值
BLEU翻译评价指标

这部分让我理解到:

参数是训练出来的,隐藏状态、注意力权重、logits 是根据当前输入和当前参数计算出来的。


模型参数通过反向传播自动学习。

训练流程可以概括为:

前向传播
计算预测概率
和真实标签计算 loss
反向传播计算梯度
优化器更新参数

但是有些东西不是模型自己学出来的,而是训练前人为设置的,这些叫超参数。

例如:

超参数含义
embed_size词向量维度
num_hiddens隐藏状态维度
num_layers网络层数
dropout随机失活比例
batch_size每批样本数量
num_steps序列最大长度
lr学习率
num_epochs训练轮数

我现在对调参的理解是:

模型参数是通过 loss 自动更新的;超参数需要通过实验比较来调整。

例如:

loss 震荡明显,可能是学习率太大。
loss 下降很慢,可能是学习率太小。
训练 loss 很低但测试效果差,可能是过拟合。
长句效果差,可能是 num_steps 太小。


11. 多头注意力为什么要分多个头

Section titled “11. 多头注意力为什么要分多个头”

学完 Bahdanau 注意力之后,再看多头注意力,我一开始的问题是:

一个注意力已经能计算 Query 和 Key 的关系了,为什么还要多个头?

后来理解到,单个注意力头只能从一个表示空间中学习关系。

但是一句话中可能同时存在很多种关系:

词义关系
语法关系
位置关系
短语关系
长距离依赖

多头注意力的做法是:

把 Query、Key、Value 投影到多个不同子空间中,每个子空间单独做注意力。

ii 个头可以写成:

hi=f(Wi(q)q,Wi(k)k,Wi(v)v)h_i=f(W_i^{(q)}q,W_i^{(k)}k,W_i^{(v)}v)

多个头的输出拼接后,再经过输出投影:

Wo[h1,h2,,hh]W_o[h_1,h_2,\ldots,h_h]

我的理解是:

多头注意力不是把同一个注意力重复算多遍,而是让不同头在不同子空间中学习不同类型的关系。

例如:

注意力头可能关注的内容
Head 1词义相关
Head 2相邻位置
Head 3句首或句尾
Head 4长距离依赖

这些不是人工指定的,而是在训练过程中自动学出来的。


学到自注意力时,我一开始觉得它和普通注意力很像。

后来发现,区别在于 Query、Key、Value 的来源。

普通编码器-解码器注意力中:

Query 来自解码器
Key 和 Value 来自编码器

而自注意力中:

queries = X
keys = X
values = X

也就是说,Query、Key、Value 都来自同一个序列。

例如输入:

i'm home .

自注意力会让每个词都可以关注同一句子里的所有词:

i'm 可以关注 i'mhome.
home 可以关注 i'mhome.
. 可以关注 i'mhome.

自注意力的计算过程可以写成:

score(i,j)=qikjdscore(i,j)=\frac{q_i\cdot k_j}{\sqrt d} αij=softmax(score(i,j))\alpha_{ij}=softmax(score(i,j)) yi=jαijvjy_i=\sum_j\alpha_{ij}v_j

我的理解是:

自注意力让序列中每个位置都能直接和其他位置建立联系,不需要像 RNN 那样一步一步传递信息。

这也是 Transformer 能够更好处理长距离依赖的重要原因。


13. 自注意力为什么需要位置编码

Section titled “13. 自注意力为什么需要位置编码”

自注意力虽然可以让每个 token 看到所有 token,但它有一个明显问题:

自注意力本身不知道顺序。

例如:

我 爱 你
你 爱 我

这两个句子的词一样,但顺序不同,意思完全不同。

RNN 是按顺序处理的,所以天然带有顺序信息。 但是自注意力一次性看整个序列,如果没有额外的位置信息,它只知道有哪些 token,不知道谁在前、谁在后。

所以 Transformer 需要加入位置编码:

最终输入 = token embedding + positional encoding

例如:

Embedding("i'm") + Position(0)
Embedding("home") + Position(1)
Embedding(".") + Position(2)

我的理解是:

token embedding 表示“这个词是什么”,position encoding 表示“这个词在哪里”。


14. 为什么可以直接把词向量和位置编码相加

Section titled “14. 为什么可以直接把词向量和位置编码相加”

我一开始很疑惑:

x1+p0x_1+p_0 这样直接相加,不是把词信息和位置信息混在一起了吗?

后来理解到,确实是混在一起了,但模型并不需要把它们完全拆开。

模型真正需要的是一个同时包含词义和位置的信息表示。

如果词向量和位置编码维度相同,就可以逐元素相加:

x1 + p0

相加后的向量仍然保持原来的隐藏维度。

后面的线性层可以继续学习如何利用这个混合表示:

q=Wq(x+p)=Wqx+Wqpq=W_q(x+p)=W_qx+W_qp

所以虽然输入是相加的,但线性层可以从中学习词义信息和位置信息的组合方式。

我现在的理解是:

相加不是为了保留两个完全独立的向量,而是为了让每个 token 的表示同时带上“词是什么”和“词在哪里”。

如果改成拼接:

[x, p]

维度会翻倍,后续层的参数量和计算量都会增加。

所以相加是一种简单且高效的方式。


Transformer 中常用的固定位置编码使用 sin 和 cos。

偶数维:

pi,2j=sin(i100002j/d)p_{i,2j}=\sin\left(\frac{i}{10000^{2j/d}}\right)

奇数维:

pi,2j+1=cos(i100002j/d)p_{i,2j+1}=\cos\left(\frac{i}{10000^{2j/d}}\right)

其中:

ii 是位置编号
jj 是维度编号的一半
dd 是位置编码维度

我的理解是:

不同位置会得到不同的位置向量;不同维度使用不同频率的正弦和余弦函数。

低维度变化较快,可以表示细粒度的位置差异。 高维度变化较慢,可以表示较长距离的位置变化。


16. 位置编码为什么也能表示相对位置

Section titled “16. 位置编码为什么也能表示相对位置”

位置编码不只表示绝对位置,还隐含相对位置信息。

对于某一组 sin/cos 维度,有:

[pi,2j pi,2j+1]=============[sin(iωj) cos(iωj)]\begin{bmatrix} p_{i,2j}\ p_{i,2j+1} \end{bmatrix} ============= \begin{bmatrix} \sin(i\omega_j)\ \cos(i\omega_j) \end{bmatrix}

其中:

ωj=1100002j/d\omega_j=\frac{1}{10000^{2j/d}}

位置 i+δi+\delta 可以由位置 ii 通过一个只和 δ\delta 有关的矩阵变换得到:

[cos(δωj)sin(δωj) sin(δωj)cos(δωj)][pi,2j pi,2j+1]=============[pi+δ,2j pi+δ,2j+1]\begin{bmatrix} \cos(\delta\omega_j)&\sin(\delta\omega_j)\ -\sin(\delta\omega_j)&\cos(\delta\omega_j) \end{bmatrix} \begin{bmatrix} p_{i,2j}\ p_{i,2j+1} \end{bmatrix} ============= \begin{bmatrix} p_{i+\delta,2j}\ p_{i+\delta,2j+1} \end{bmatrix}

这个公式一开始看起来比较复杂,但它其实来自三角函数加法公式。

我的理解是:

如果两个位置相差同样的距离 δ\delta,那么它们之间的位置编码关系也具有相同形式的线性变换。

例如:

位置 3 到位置 5,相差 2
位置 10 到位置 12,也相差 2

这种“相隔 2 个位置”的关系,可以通过 sin/cos 编码结构被模型利用。


学到 Transformer 整体结构时,我发现它并不是一个完全陌生的新模型,而是把前面学过的模块组合了起来。

编码器每一层主要包括:

多头自注意力
残差连接 + 层归一化
基于位置的前馈网络
残差连接 + 层归一化

编码器中的注意力是自注意力:

Query = 源序列
Key = 源序列
Value = 源序列

也就是说,源句子中每个词都可以关注源句子中所有词。

例如:

i'm home .

编码器会让:

i'm 关注 i'mhome.
home 关注 i'mhome.
. 关注 i'mhome.

我的理解是:

编码器的作用是让源句子中每个位置的表示都融合整个源句子的上下文信息。


解码器每一层比编码器多一个注意力模块。

每个解码器层包括:

掩蔽多头自注意力
残差连接 + 层归一化
编码器-解码器注意力
残差连接 + 层归一化
基于位置的前馈网络
残差连接 + 层归一化

解码器中有两种注意力需要区分。

第一种是掩蔽自注意力:

Query = 目标序列
Key = 目标序列
Value = 目标序列

它的作用是:

建模已经生成的目标词,但不能看未来词。

例如生成 chez 时,只能看到:

<bos> je suis

不能看到:

moi .

第二种是编码器-解码器注意力:

Query = 解码器输出
Key = 编码器输出
Value = 编码器输出

它的作用是:

解码器在生成目标词时,从源句子中读取相关信息。

这个模块和 Bahdanau 注意力的作用很像,只不过 Transformer 使用的是多头点积注意力。


Transformer 中每层还有一个 PositionWise FFN。

它的结构是:

FFN(X)=Dense2(ReLU(Dense1(X)))FFN(X)=Dense_2(ReLU(Dense_1(X)))

这个前馈网络对每个位置单独作用,并且所有位置共享同一套参数。

我的理解是:

注意力层负责不同位置之间的信息交互,前馈网络负责对每个位置的表示做非线性变换。

也就是说,FFN 不直接混合不同 token 的信息,token 之间的信息交换主要依赖注意力层完成。


Transformer 中经常出现 Add & Norm。

它通常可以写成:

LayerNorm(X+Dropout(Sublayer(X)))LayerNorm(X+Dropout(Sublayer(X)))

其中:

XX 是子层输入
Sublayer(X)Sublayer(X) 是注意力层或前馈网络输出
X+Sublayer(X)X+Sublayer(X) 是残差连接
LayerNormLayerNorm 是层归一化

残差连接的作用是:

保留原始输入信息
缓解梯度消失
让深层网络更容易训练

层归一化的作用是:

稳定每层输出的数值分布
加快训练收敛
提高深层模型训练稳定性

我的理解是:

Transformer 不只是注意力机制本身,还依赖残差连接和层归一化来保证深层结构能够稳定训练。


Transformer 训练时仍然是 Seq2Seq 任务。

以:

源句子:i'm home .
目标句子:je suis chez moi .

为例,训练流程是:

源句子 token 编号
源语言 Embedding
加位置编码
Transformer 编码器
编码器输出
目标句子构造 teacher forcing 输入
目标语言 Embedding
加位置编码
Transformer 解码器
输出 Dense 层
目标词表 logits
softmax 概率
交叉熵 loss
反向传播更新参数

需要训练的参数包括:

模块参数
源语言 Embedding源 token 向量
目标语言 Embedding目标 token 向量
多头注意力Wq,Wk,Wv,WoW_q,W_k,W_v,W_o
前馈网络Dense 层权重和偏置
层归一化缩放参数和偏移参数
输出 Dense 层词表映射权重和偏置

固定的正弦余弦位置编码一般不训练。


22. Transformer 测试时怎么生成句子

Section titled “22. Transformer 测试时怎么生成句子”

训练时有真实目标句子,所以可以使用 teacher forcing。

但是测试时没有真实目标句子,只能让模型自己一步一步生成。

测试流程是:

输入源句子
编码器得到源句子表示
解码器从 <bos> 开始
预测第一个目标词
把预测词作为下一步输入
继续预测
直到生成 <eos> 或达到最大长度

训练和测试的区别可以整理为:

阶段解码器输入是否更新参数
训练真实目标句子的上一个词更新参数
测试模型上一步预测出的词不更新参数

我的理解是:

训练时模型是在标准答案的引导下学习;测试时模型只能依靠自己已经生成的内容继续往后生成。


在 Transformer 可视化中,第一类图是编码器自注意力图。

它表示:

源句子内部,每个源 token 关注哪些源 token。

热力图中:

横轴是 Key positions,也就是被关注的位置
纵轴是 Query positions,也就是当前正在更新的位置
颜色越深,说明注意力权重越大

如果源句子是:

i'm home . <eos> <pad> <pad> ...

那么右侧 <pad> 区域通常颜色很浅,因为 padding 位置被 mask 掉了。

我的理解是:

编码器自注意力图可以看出源句子内部不同位置之间的信息交互方式。


24. 解码器掩蔽自注意力图怎么看

Section titled “24. 解码器掩蔽自注意力图怎么看”

第二类图是解码器掩蔽自注意力图。

它表示:

目标句子内部,每个目标 token 关注哪些历史目标 token。

因为解码器不能看到未来词,所以这个图通常会呈现下三角结构。

例如目标序列是:

<bos> je suis chez moi .

那么:

第 1 个位置只能看第 1 个位置
第 2 个位置可以看第 1、2 个位置
第 3 个位置可以看第 1、2、3 个位置
后面以此类推

我的理解是:

解码器掩蔽自注意力的作用是防止模型在训练时偷看未来答案。


25. 编码器-解码器注意力图怎么看

Section titled “25. 编码器-解码器注意力图怎么看”

第三类图是编码器-解码器注意力图,也叫 cross-attention 图。

它表示:

解码器生成每个目标 token 时,关注源句子的哪些位置。

在这个图中:

横轴是源句子位置
纵轴是目标句子位置
颜色越深,表示生成该目标词时越关注对应源词

例如:

i'm home . → je suis chez moi .

理想情况下可能是:

目标词主要关注源词
jei'm
suisi'm
chezhome
moihome
..
<eos><eos>

我的理解是:

编码器-解码器注意力图可以用来观察源语言和目标语言之间的对齐关系。


26. Bahdanau 注意力和 Transformer 的关系

Section titled “26. Bahdanau 注意力和 Transformer 的关系”

学到最后,我把 Bahdanau 注意力和 Transformer 做了一个对比。

对比项Bahdanau Seq2SeqTransformer
编码器GRU / RNN多头自注意力
解码器GRU + Bahdanau 注意力掩蔽自注意力 + 编码器-解码器注意力
注意力类型加性注意力多头点积注意力
位置信息RNN 顺序结构天然包含需要位置编码
并行能力较弱较强
长距离依赖依赖循环传递任意位置可以直接关注

我的理解是:

Bahdanau 注意力是在 RNN 编码器-解码器结构中加入动态对齐;Transformer 则是进一步用自注意力替代循环结构,并用多头注意力增强表达能力。


经过这一部分学习,我对注意力机制后续内容的理解逐渐清晰。

最开始我不理解:

Bahdanau 注意力为什么要保留所有编码器隐藏状态?
token 编号怎么变成词向量?
GRU 的隐藏状态是怎么算出来的?
输出层的 WoW_obob_o 是哪里来的?
多头注意力为什么要分多个头?
自注意力为什么需要位置编码?
位置编码为什么可以直接和词向量相加?
Transformer 三种注意力图分别表示什么?

现在我的理解是:

  1. Bahdanau 注意力让解码器在每一步动态关注源序列。
  2. token 编号只是索引,Embedding 才是真正参与计算的词向量。
  3. GRU 通过更新门、重置门和候选隐藏状态逐步生成隐藏状态。
  4. 输出 Dense 层的 WoW_obob_o 是可训练参数,用来把隐藏状态映射到词表得分。
  5. 多头注意力让模型从多个子空间并行学习不同关系。
  6. 自注意力让序列内部任意两个位置可以直接建立联系。
  7. 位置编码为自注意力补充顺序信息。
  8. Transformer 编码器负责源序列内部建模,解码器负责目标端历史建模和源目标对齐。

用一句话总结我目前对这部分内容的理解:

从 Bahdanau 注意力到 Transformer,本质上都是在解决“模型应该在什么时候关注哪些信息”的问题,只是 Transformer 用多头自注意力把这种信息选择能力扩展得更彻底、更并行、更适合长距离依赖建模。


后面继续学习时,我准备重点关注以下问题:

  1. Transformer 为什么可以作为大语言模型的基础结构?
  2. 编码器-only、解码器-only、编码器-解码器结构分别适合什么任务?
  3. GPT 这类模型中的 causal mask 和这里的解码器掩蔽自注意力有什么关系?
  4. Transformer 的参数量主要集中在哪些模块?
  5. 多头注意力中的不同 Head 是否真的学到了不同语言现象?

通过这一部分学习,我感觉注意力机制已经不再只是一个公式,而是一种通用的信息选择方式。

只要问题可以抽象成:

当前目标是什么?
候选信息有哪些?
哪些候选信息和当前目标最相关?

就可以用注意力机制来建模。