Transformer

Transformer

Transformer Block 是什么?

它是一个神经网络结构的“积木”,专门用来处理向量序列(如文本、图像 patch、语音 token 等),核心思想是:

用 Self-Attention 建立元素间联系,再用 MLP 提取特征


结构拆解:

我们输入的是一个向量序列:x₁, x₂, x₃, x₄(比如词向量)

① Self-Attention(多头注意力层)

  • 输入整组向量 X = [x_1, x_2, …, x_n]
  • 每个向量计算出:Query、Key、Value
  • 所有向量两两交互得到 Attention Score
  • 输出每个位置的新表示(融合了其他位置的信息)

注意:Self-Attention 是唯一发生“跨向量交互”的地方,让每个 token “看到”其它 token。

② Residual Connection + Layer Norm

  • 把原输入加回来:

好处:

  • 保留输入信息
  • 缓解深层网络的梯度消失问题

③ MLP(前馈网络)

对每个向量 独立 处理:

  • 就是一个小的 2 层感知机
  • 相当于对每个 token 提取更复杂的特征

④ 再次 Residual + Layer Norm

这样我们就得到了最终输出 , , …,


整个 Transformer 是怎么堆叠的?

  • 一个 Transformer Block 是上面这个结构
  • 整个 Transformer 是多个 Block 的堆叠(比如 GPT-2 有 12 层)
  • 每一层的输出是下一层的输入

关键特性总结:

组成模块 是否 token 间通信 并行计算 是否含参数
Self-Attention 有参数
MLP(前馈层) 否(逐 token) 有参数
LayerNorm 否(逐 token) 无参数
Residual 无参数

Transformer Block 输入输出关系

Transformer Block 输入输出:

  • 输入:n × d_in 的向量序列
  • 输出:n × d_out 的向量序列
  • 数量(n)保持不变
  • 维度(d)可以自由变化,由 MLP 控制

注意输入输出维度可以调节?

Transformer block 中有一个组件:

  • 前馈层(MLP)
  • 它包含两个线性层:

所以我们可以自由设定输出维度

应用场景类比:

  • 在早期层中我们可能用较小维度(比如 256)
  • 在中间层扩大维度(比如 1024)来增强表达能力
  • 在最后一层变换为目标维度(比如 词表大小用于分类)

embedding + position encoding + projection

在 Transformer 中,我们用 词向量 + 位置编码(position encoding) 表示每个输入 token,再通过线性变换(projection)得到 Query、Key、Value 向量,输入给注意力层。


三角函数位置编码

1
2
3
4
5
6
7
8
9
10
11
pe = torch.zeros(1, max_len, embed_dim)

position = torch.arange(0, max_len).unsqueeze(1) # shape: (max_len, 1)

div_term = torch.exp(torch.arange(0, embed_dim, 2) * (-math.log(10000.0) / embed_dim)) # (d/2,)

pe[0, :, 0::2] = torch.sin(position * div_term)

pe[0, :, 1::2] = torch.cos(position * div_term)

self.register_buffer('pe', pe)

数学背景

公式

在 Transformer 的位置编码中,我们对每个位置 pos 和每个维度 i 的编码如下定义:

其中

  • pos:表示序列中的位置(position)
  • i:维度索引
  • d:嵌入维度(embed_dim)
div_term 是什么?

从上述公式看,核心项是:

可以令:

'_' allowed only in math mode \text{div_term}_i = 10000^{-\frac{2i}{d}}

取对数再指数还原:

'_' allowed only in math mode \text{div_term}_i = \exp\left(-\frac{2i}{d} \cdot \ln(10000)\right)

对应代码实现

1
torch.arange(0, embed_dim, 2)

生成 [0, 2, 4, …, embed_dim-2](只取偶数维)

1
(-math.log(10000.0) / embed_dim)

对应

1
torch.arange(0, embed_dim, 2) * (-math.log(10000.0) / embed_dim)

等价于:

Missing or unrecognized delimiter for \left \left{ -\frac{2i}{d} \cdot \ln(10000) \right}

最后 torch.exp(…):

关键运算机制

1
position * div_term

注意是 element-wise逐元素法,而不是矩阵乘法

首先:我们想要得到什么?

我们希望构造一个矩阵 '_' allowed only in math modeM \in \mathbb{R}^{\text{max_len} \times d/2} ,满足:

即:每个位置 i,每一维 j 的值是:位置 × 频率衰减因子, 即上述代码

position * div_term 是逐元素广播乘法(Element-wise)

两个张量形状:

1
2
position.shape   = (max_len, 1)         # 第 i 个位置,列向量
div_term.shape = (1, embed_dim//2) # 每个维度的频率,行向量

经过广播机制计算:

1
result[i][j] = position[i][0] * div_term[0][j] = i * 10000^{-j/d}

得到:

1
result.shape = (max_len, embed_dim//2)
本质

position * div_term 虽然代码上是逐元素乘法,但由于其两个操作数的形状是 (max_len, 1) 和 (1, d/2),广播后形成的计算过程 正好就是一个外积(outer product)

position * div_term 是 outer product,因为它在形状 (L,1) 和 (1,d) 之间进行广播乘法,这种结构自动形成了一个“位置 × 频率”的二维网格矩阵,正是我们在构造位置编码时需要的形式。

数学中,两个向量的外积是:

  • 给定:
  • 列向量
  • 行向量
  • 外积定义为:

思考:逐元素相乘法与外积的联系是巧合吗

回答:

逐元素乘法本身 ≠ 外积,
但在广播机制下,当操作数的形状恰好是 (m, 1) 和 (1, n) 时,A * B 实现的就是 outer product(外积)
所以:
position * div_term 是在广播机制下,形式等价于外积
但这不是逐元素乘法的一般性质,它是**一种“结构巧合” + 广播规则”的结果。

举例
1. 一般的逐元素乘法(相同 shape)

1
2
3
4
5
a = torch.tensor([[1, 2], [3, 4]])   # shape: (2, 2)
b = torch.tensor([[10, 20], [30, 40]]) # shape: (2, 2)
a * b
→ [[10, 40],
[90, 160]]

2. 广播下的外积情况(关键点在 shape)

1
2
3
4
5
6
a = torch.tensor([[1], [2], [3]])   # shape: (3, 1)
b = torch.tensor([[10, 20, 30]]) # shape: (1, 3)
a * b
→ [[10, 20, 30],
[20, 40, 60],
[30, 60, 90]]

外积和词嵌入(word embedding)的对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def word_embedding_forward(x, W):

"""Forward pass for word embeddings.

We operate on minibatches of size N where

each sequence has length T. We assume a vocabulary of V words, assigning each

word to a vector of dimension D.



Inputs:

- x: Integer array of shape (N, T) giving indices of words. Each element idx

of x muxt be in the range 0 <= idx < V.

- W: Weight matrix of shape (V, D) giving word vectors for all words.

Returns a tuple of:

- out: Array of shape (N, T, D) giving word vectors for all input words.

- cache: Values needed for the backward pass

"""

out, cache = None, None

##############################################################################

# TODO: Implement the forward pass for word embeddings. #

# #

# HINT: This can be done in one line using NumPy's array indexing. #

##############################################################################

# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

#NumPy 的高级索引,自动扩展维度
out = W[x]
cache = (x,W.shape)

# N = x.shape[0]

# T = x.shape[1]

# D = W.shape[1]

# out = np.zeros((N, T, D))

# for i in range(N):

# for t in range(T):

# out[i, t, :] = W[x[i, t]]

# out = np.array([W[x[i]] for i in range(x.shape[0])])

# cache = (x, W.shape)



# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

##############################################################################

# END OF YOUR CODE #

##############################################################################

return out, cache
词嵌入代码原理
1
out = W[x]

这是经典的 词嵌入(word embedding)查表操作,它的含义是:

给一个词 ID x[i][t],我们从词向量矩阵 W 中取出它对应的那一行 W[x[i][t]],作为它的词向量表示。

具体形状说明:

  • x.shape = (N, T):每个 batch 有一个长度为 T 的句子,句子由词 ID 组成;
  • W.shape = (V, D):整个词汇表的嵌入矩阵,V 是词数,D 是每个词的向量维度;
  • out.shape = (N, T, D):每个词 ID 被映射为一个 D 维向量。

这就是查表:把词 ID 映射成词向量

与 position * div_term 的本质区别
项目 word_embedding_forward position * div_term
输入 词 ID(int index) 位置 × 频率因子(数值计算)
机制 查表(index lookup) 外积广播(数值生成)
操作 W[x] → 从矩阵中取出行 position * div_term → 数学计算
输出 预训练 or 学习得到的词向量 固定函数生成的位置向量(不可训练)
参数 W 是可训练的 div_term 是常数,不参与训练

它们本质上完全不一样:

特点 词嵌入 位置编码
是查表吗? y n
含有可训练参数吗? y n(默认情况下)
能学出上下文意义吗? y n,只编码顺序
会随着训练更新吗? y n
像在哪里

它们“像”的地方在哪?

  • 都是:从低维输入(词 ID / 位置)映射到高维向量;
  • 都可以看作是从某种“离散标号”到“连续表示空间”的转换;
  • 最终输出都是:(N, T, D) 这样的 3D 向量序列。

词嵌入(Word Embedding)位置编码(Positional Encoding) 是如何结合使用的

首先:Transformer 的本质问题

Transformer 是纯注意力机制完全没有顺序感(不像 RNN 会按顺序一个词一个词地处理)。

所以我们必须:

提供词的“语义表示”(Word Embedding)

加上“顺序信息”(Positional Encoding)

这两个缺一不可!

整体流程长这样:

1
x → word embedding → positional encoding → transformer layers

也就是我们在代码中看到的:

1
2
captions_embed = self.embedding(captions)                # (N, T, D)
captions_embed = self.positional_encoding(captions_embed) # (N, T, D)

逐步解析这两步

① self.embedding(captions)

  • 把每个词的 ID 映射为一个 D 维向量;
  • 拿到的是“这个词的意思”;
  • 比如:“apple” → [0.12, -0.38, …, 0.02]

② self.positional_encoding(…)

  • 在每个词向量上叠加一组根据位置计算出来的 sin/cos 向量
  • 给模型一种“你在第几个词”的感觉;
  • 比如位置 3 的编码可能是:

[0.0, 0.5, 0.87, …, -0.2]

这两个向量相加后:

  • 每个词既有“是什么词”的语义;
  • 又有“它在句子中第几位”的顺序感。

数学上是这样:

设:

  • 是第 i 个词的词向量(来自 embedding lookup);
  • 是第 i 个位置的位置编码(来自 sin/cos);

那么送入 Transformer 的输入为:


为什么是“相加”?

因为我们希望:

  • 不改变 embedding 的维度;
  • 简单、高效;
  • 实现位置与语义的融合;
  • 原论文《Attention Is All You Need》就是这样设计的。

当然,也有更复杂的做法(如 Learned Positional Embedding),但标准就是加法。

总结

步骤 描述 输出 shape
Embedding(x) 查词向量 (N, T, D)
PositionalEncoding(…) 构造 sin/cos 编码并加到上面 (N, T, D)
Transformer(…) 处理语义 + 顺序的输入 (N, T, D)

最终每个词的表示就包含了:

  • 词语信息(语义);
  • 位置信息(顺序)。

拓展:可学习的位置嵌入(Learned Positional Embedding)

概念可学习位置嵌入是干嘛的?

和 sin/cos 不同,它不是通过公式计算出来的,而是:

像词向量一样,给每个“位置”分配一个可训练的向量。

类比理解:

类型 思维方式 举例
Word Embedding 每个词一个向量 “apple” → [0.2, -0.5, 0.9, …]
Positional Embedding 每个位置一个向量 位置 0 → [0.1, -0.4, 0.8, …]

代码实现

1
2
3
4
5
6
7
8
9
10
11
class LearnedPositionalEncoding(nn.Module):
def __init__(self, max_len, embed_dim):
super().__init__()
self.pos_embed = nn.Embedding(max_len, embed_dim)

def forward(self, x):
# x: shape (N, T, D)
N, T, D = x.shape
positions = torch.arange(T, device=x.device).unsqueeze(0) # (1, T)
pos_vectors = self.pos_embed(positions) # (1, T, D)
return x + pos_vectors

与三角函数对比

比较项 sin/cos encoding learned embedding
计算方式 公式计算(不可训练) 查表 + 可训练参数
参数量 0 max_len × embed_dim
是否泛化 可用于任意长句子 只能用于训练过的位置长度
是否适合视觉模型 一般 很常用(如 ViT)
实际表现 很好(特别是在低资源) 更灵活(但需更多数据训练)

哪个更好?

  • sin/cos 是“硬编码的位置规律”,训练稳定,适用于小模型、小数据;
  • learned embedding 是“自由学习的位置理解”,适用于大模型、大数据(GPT、BERT);
  • ViT(Vision Transformer)也用的是 learned 位置编码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

┌──────────────┐
tgt ───► │ Self-Attn │
│ + Mask │
└────┬──────────┘

┌──────────────┐
│ Add + Norm │
└────┬──────────┘

┌──────────────┐
memory ─►│ Cross-Attn │◄── tgt
└────┬──────────┘

┌──────────────┐
│ Add + Norm │
└────┬──────────┘

┌──────────────┐
│ FeedForward │
└────┬──────────┘

┌──────────────┐
│ Add + Norm │
└──────────────┘

Output tgt (N, T, W)