- 简介 =====
本文将从维度变换的视角,详细剖析BERT中输入序列的处理流程——从原始文本输入到Encoder层输出的全过程。通过追踪这些维度的变化,希望有助于更好地理解BERT模型(即Transformer的Encoder部分)的内部运作机制。#AI入门 #Transformer详解 #LLM #LLM从0到1 #原理
更多AI相关欢迎关注微信公众号"小窗幽记机器学习":
- 前置说明 =======
以bert-base-uncased模型为例,进行step by step解析:
- 输入序列长度
:
n
(例如,512 是 BERT 的典型最大长度) - 批大小
:
batch_size
(例如,32) - 词表大小
:
vocab_size
(例如,30522 对于 BERT-base) embedding_size
或d_model
:d
(例如,768 对于 BERT-base,在config.json中的名字是"hidden_size",所以这3个名字都是指同一个意思)- 注意力头数
:
h
(例如,12 对于 BERT-base) - 每个头的维度
:
d_k = d_v = d_head
(例如,d_head = d / h = 768 / 12 = 64
对于 BERT-base) - 前馈网络隐藏层维度
:
d_ff
(例如,4 * d = 3072
对于 bert-base-uncased该值存于config.json的"intermediate_size"字段)
补充说明: 之前有小伙伴问我 embedding_size
和 d_model
是什么关系,这里做一些补充说明。 其实 embedding_size = d_model
,只是称谓不同而已。
embedding_size
(也称为d_model
): 指的是你将每个输入token转换成的连续向量表示的** 维度大小** 。例如,你有一个包含 30000 个单词的词表,embedding_size=512
意味着每个单词会被表示成一个 512 维的实数向量。d_model
:在 Transformer 架构(BERT 基于此)中,d_model
是模型** 内部表示的统一维度** 。它定义了模型在处理序列时,每个时间步(每个token位置)的特征向量的长度。它是 Transformer 块(编码器/解码器层)设计的关键参数。- 关系
:在标准的 Transformer 和 BERT 实现中,
embedding_size
被设定为等于d_model
。也就是说,词嵌入向量被直接映射到模型内部工作所需的维度d_model
上。这是模型设计的一个关键点,它确保了:
- 可加性
:词嵌入(Word Embedding)、位置嵌入(Positional Embedding)、段嵌入(Segment Embedding)都是
d_model
维的,可以直接相加。 - 一致性
:所有后续线性变换(如 Q、K、V 投影)、层归一化(LayerNorm)、前馈网络(FFN)的输入和输出在序列长度
n
的每个位置上都保持d_model
维的特征向量。
- 输入数据
:一批句子。每个句子被分词、添加特殊token(如
[CLS]
,[SEP]
)并填充/截断到长度n
。 - 初始维度
:
(batch_size, n)
。这是一个整数张量,每个元素是词表vocab_size
中的token ID。
- 词嵌入 (Token Embedding) :
- 操作:将每个token ID 映射到一个
d
维向量。 - 输入维度:
(batch_size, n)
- 输出维度:
(batch_size, n, d)
。现在每个token位置都有一个d
维向量表示其语义。
- 位置嵌入 (Positional Embedding) :
- 操作:生成表示每个位置(1 到
n
)的d
维向量。可以是学习的或固定的(如正弦/余弦)。 - 输出维度:
(n, d)
或直接广播到(batch_size, n, d)
。
- 段嵌入 (Segment Embedding - BERT特有) :
- 操作:对于句子对任务(如问答、自然语言推理),区分句子 A 和句子 B。每个句子类型(A 或 B)有一个
d
维向量。 - 输入:段 ID 张量
(batch_size, n)
(通常是 0 或 1)。 - 输出维度:
(batch_size, n, d)
。
- 嵌入求和 :
- 操作:将 词嵌入
位置嵌入 + 段嵌入 (如果使用)进行 逐元素相加 。
- 为什么能相加?
因为三者维度都是
(batch_size, n, d)
。 - 输出维度:
(batch_size, n, d)
。这是进入 Transformer 编码器层的初始表示X
。
- 输入到第一层
:
X
,维度(batch_size, n, d)
步骤 2.1:多头自注意力(Multi-Head Self-Attention)
- 线性投影 (生成 Q, K, V) :
- 操作:对输入
X
进行三个不同的线性变换(三个独立的权重矩阵W_Q
,W_K
,W_V
,每个形状为(d, d_head * h) = (d, d)
),分别生成查询(Query)、键(Key)、值(Value)向量。 - 输入维度:
(batch_size, n, d)
- 输出维度:
(batch_size, n, d)
。注意:虽然投影到了d
维,但d = h * d_head
。 物理上 它是一个d
维向量, 逻辑上 我们将其视为h
个d_head
维的向量。
- 拆分成多头 :
- 操作:将上一步得到的
Q
,K
,V
张量从(batch_size, n, d)
重塑 (reshape) 为(batch_size, n, h, d_head)
。然后 交换维度 (transpose) 为(batch_size, h, n, d_head)
以便并行计算每个头的注意力。 - 输入维度:
(batch_size, n, d)
(Q, K, V 各自) - 输出维度:
(batch_size, h, n, d_head)
(Q, K, V 各自)。现在我们有h
个独立的注意力头,每个头处理n
个d_head
维的向量。
- 缩放点积注意力(Scaled Dot-Product Attention) :
-
Q_i @ K_i^T
: 维度(batch_size, n, n)
(注意力分数矩阵) -
softmax(...)
: 维度(batch_size, n, n)
(注意力权重矩阵) -
softmax(...) @ V_i
: 维度(batch_size, n, d_head)
(该头的输出) -
Q_i
:(batch_size, h, n, d_head)
-> 取头i
:(batch_size, n, d_head)
-
K_i
:(batch_size, h, n, d_head)
-> 取头i
:(batch_size, n, d_head)
-
V_i
:(batch_size, h, n, d_head)
-> 取头i
:(batch_size, n, d_head)
-
操作:对每个头
i
独立计算:Attention(Q_i, K_i, V_i) = softmax( (Q_i @ K_i^T) / sqrt(d_head) ) @ V_i
-
输入维度:
-
计算过程:
-
输出维度 (每个头):
(batch_size, n, d_head)
- 合并多头(Concatenate Heads) :
- 操作:将所有
h
个头的输出(每个(batch_size, n, d_head)
) 连接 (concat) 在一起。将维度从(batch_size, n, h, d_head)
(在合并头之前需要先堆叠) 重塑回(batch_size, n, d)
,因为h * d_head = d
。 - 输入维度 (所有头):
(batch_size, h, n, d_head)
(注意:步骤3的输出是每个头(batch_size, n, d_head)
,需要先交换维度回(batch_size, n, h, d_head)
或直接堆叠)。 - 输出维度:
(batch_size, n, d)
。这是多头注意力机制的组合输出。
- 输出投影 (线性层) :
- 操作:将合并后的多头输出通过一个线性层
W_O
(形状(d, d)
) 进行投影。 - 输入维度:
(batch_size, n, d)
- 输出维度:
(batch_size, n, d)
。这是自注意力子层的最终输出Z
。
- 残差连接与层归一化 (Add & LayerNorm) :
-
Z
:(batch_size, n, d)
-
X
:(batch_size, n, d)
-
操作:
LayerNorm(Z + X)
(X
是自注意力子层的输入) -
输入维度:
-
输出维度:
(batch_size, n, d)
。层归一化在每个token位置上独立进行,不改变维度。残差连接要求Z
和X
维度相同(都是d
维),因此成立。输出记为Y
。
步骤 2.2:前馈神经网络 (Position-wise Feed-Forward Network - FFN)
- 第一个线性层 (扩展维度) :
- 操作:将
Y
的每个token位置的d
维向量独立地通过一个线性层(权重W1
,形状(d, d_ff)
,偏置b1
)投影到更高维度d_ff
。通常d_ff = 4 * d
。 - 输入维度:
(batch_size, n, d)
- 输出维度:
(batch_size, n, d_ff)
。注意:该变换是 逐位置 (position-wise) 的,每个位置的d
维向量被独立映射到d_ff
维。
- 激活函数 (如 GELU/ReLU) :
- 操作:对
d_ff
维向量逐元素应用激活函数。 - 输入维度:
(batch_size, n, d_ff)
- 输出维度:
(batch_size, n, d_ff)
。
- 第二个线性层(降回模型维度) :
- 操作:将激活后的
d_ff
维向量通过另一个线性层(权重W2
,形状(d_ff, d)
,偏置b2
)投影回d
维。 - 输入维度:
(batch_size, n, d_ff)
- 输出维度:
(batch_size, n, d)
。这是 FFN 子层的输出F
。
- 残差连接与层归一化 (Add & LayerNorm) :
-
F
:(batch_size, n, d)
-
Y
:(batch_size, n, d)
-
操作:
LayerNorm(F + Y)
(Y
是 FFN 子层的输入,即自注意力子层的输出) -
输入维度:
-
输出维度:
(batch_size, n, d)
。这是该 Transformer 编码器层的最终输出。
- 将上一步的输出
(batch_size, n, d)
作为下一个 Transformer 编码器层的输入X
。 - 重复步骤 2.1 和 2.2
L
次(BERT-base L=12)。
- 经过
L
层编码器后,最终输出维度仍然是:(batch_size, n, d)
。 - 这个输出包含了输入序列中每个token位置 (
n
个位置) 的上下文感知的d
维向量表示。 - 对于句子级任务(如分类),通常取第一个位置(
[CLS]
token)的输出向量(batch_size, d)
作为整个序列的表示,送入任务特定的输出层(如线性分类器)。 - 对于token级任务(如命名实体识别),每个位置的输出向量
(batch_size, n, d)
都可以被送入任务特定的输出层(如线性分类器作用于每个位置)。
| 步骤 | 操作 | 输入维度 | 输出维度 | 说明 | | --- | --- | --- | --- | --- | | 输入 |
| (batch_size, n)
|
|
token ID
|
| 1. 词嵌入 |
Token Embedding
| (batch_size, n)
| (batch_size, n, d)
| d = embedding_size = d_model
|
| 1. 位置嵌入 |
Positional Embedding
|
| (batch_size, n, d)
|
|
| 1. 段嵌入 (可选) |
Segment Embedding
| (batch_size, n)
| (batch_size, n, d)
|
|
| 1. 嵌入求和 |
Sum
| (batch_size, n, d)
x 3
| (batch_size, n, d)
|
|
| 2.1.1 Q/K/V 投影 |
Linear (W_Q, W_K, W_V)
| (batch_size, n, d)
| (batch_size, n, d)
| d = h * d_head
|
| 2.1.2 拆分成多头 |
Reshape/Transpose
| (batch_size, n, d)
| (batch_size, h, n, d_head)
|
逻辑上分离头
|
| 2.1.3 注意力 (每头) | softmax(Q_i K_i^T / sqrt(d_k)) V_i
| (batch_size, n, d_head)
| (batch_size, n, d_head)
|
计算在
n x n
注意力矩阵上
|
| 2.1.4 合并多头 |
Concat/Reshape
| (batch_size, h, n, d_head)
| (batch_size, n, d)
| d = h * d_head
|
| 2.1.5 输出投影 |
Linear (W_O)
| (batch_size, n, d)
| (batch_size, n, d)
|
|
| 2.1.6 Add & Norm | LayerNorm(Z + X)
| Z: (b, n, d), X: (b, n, d)
| (batch_size, n, d)
|
|
| 2.2.1 FFN 第一层 |
Linear (W1) + Activation
| (batch_size, n, d)
| (batch_size, n, d_ff)
| d_ff
通常为
4 * d
|
| 2.2.2 FFN 第二层 |
Linear (W2)
| (batch_size, n, d_ff)
| (batch_size, n, d)
|
投影回
d_model
|
| 2.2.3 Add & Norm | LayerNorm(F + Y)
| F: (b, n, d), Y: (b, n, d)
| (batch_size, n, d)
|
该编码器层最终输出
|
| 3. 编码器输出 (L层后) |
|
... (重复 L 次) ...
| (batch_size, n, d)
|
|
- 序列长度
n
保持不变 :所有操作(嵌入、自注意力、FFN)都是在序列的每个位置(batch_size, n, ...)
上独立或交互地进行的,但不会改变序列长度n
。 - 多头注意力的维度变化 :通过将
d_model
拆分成h
个d_head
的头 (d = h * d_head
),在计算注意力时降低了复杂度(d_head
较小),并通过合并操作无缝地回到d_model
维。 - FFN 的维度膨胀 :FFN 先将特征从
d
维扩展到d_ff
(通常是4*d
)以增加模型容量,然后再压缩回d_model
维,以匹配残差连接的要求和下一层的输入。 - 残差连接 :依赖于输入
X
和子层输出Z
/F
维度严格相同 (d_model
维),这是embedding_size = d_model
设计的关键原因之一。 - 层归一化 :在序列的每个位置上 (
n
个位置) 独立地对d_model
维向量进行归一化,不改变维度。
通过这个流程,BERT 模型能够将输入的离散token序列 (batch_size, n)
逐步转化为富含上下文信息的连续向量表示 (batch_size, n, d)
,其中每个位置的 d
维向量都编码了整个输入序列的相关信息。
更多AI相关欢迎关注微信公众号"小窗幽记机器学习":