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 | pe = torch.zeros(1, max_len, embed_dim) |
数学背景
公式
在 Transformer 的位置编码中,我们对每个位置 pos 和每个维度 i 的编码如下定义:
其中
- pos:表示序列中的位置(position)
- i:维度索引
- d:嵌入维度(embed_dim)
div_term 是什么?
从上述公式看,核心项是:
可以令:
取对数再指数还原:
对应代码实现
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) |
等价于:
最后 torch.exp(…):
关键运算机制
1 | position * div_term |
注意是 element-wise逐元素法,而不是矩阵乘法
首先:我们想要得到什么?
我们希望构造一个矩阵
即:每个位置 i,每一维 j 的值是:位置 × 频率衰减因子, 即上述代码
position * div_term 是逐元素广播乘法(Element-wise)
两个张量形状:
1 | position.shape = (max_len, 1) # 第 i 个位置,列向量 |
经过广播机制计算:
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 | a = torch.tensor([[1, 2], [3, 4]]) # shape: (2, 2) |
2. 广播下的外积情况(关键点在 shape)
1 | a = torch.tensor([[1], [2], [3]]) # shape: (3, 1) |
外积和词嵌入(word embedding)的对比
1 | def word_embedding_forward(x, W): |
词嵌入代码原理
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 | captions_embed = self.embedding(captions) # (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 | class LearnedPositionalEncoding(nn.Module): |
与三角函数对比
比较项 | sin/cos encoding | learned embedding |
---|---|---|
计算方式 | 公式计算(不可训练) | 查表 + 可训练参数 |
参数量 | 0 | max_len × embed_dim |
是否泛化 | 可用于任意长句子 | 只能用于训练过的位置长度 |
是否适合视觉模型 | 一般 | 很常用(如 ViT) |
实际表现 | 很好(特别是在低资源) | 更灵活(但需更多数据训练) |
哪个更好?
- sin/cos 是“硬编码的位置规律”,训练稳定,适用于小模型、小数据;
- learned embedding 是“自由学习的位置理解”,适用于大模型、大数据(GPT、BERT);
- ViT(Vision Transformer)也用的是 learned 位置编码。
1 |
|