查看全课大纲(第 50 / 69 节)
- 1.NLP 的研究领域、应用与学习路径
- 2.自然语言的基本术语
- 3.信息论基础:信息熵与交叉熵
- 4.语言的进化与语义变迁
- 5.主观概率、客观概率与条件概率
- 6.贝叶斯原理与推理
- 7.随机变量:二项式、期望方差与联合概率
- 8.常见概率分布
- 9.辛普森悖论与贝叶斯解题实例
- 10.语言模型:语言概率与词袋模型
- 11.n-gram 语言模型(二元 / 三元 / N-Gram)
- 12.语言模型评价:交叉熵、困惑度与插值
- 13.朴素贝叶斯文本分类:垃圾邮件
- 14.拼写检查
- 15.中文分词:构词法、最大匹配、最优路径与 Jieba
- 16.词表示问题与语义相似度
- 17.TF-IDF 权重处理
- 18.One-Hot 表达与词表示的局限
- 19.Word2Vec 核心思想
- 20.Word2Vec 训练过程
- 21.Skip-gram 模型详解
- 22.词性标注任务与标注方法(规则 + N-Gram)
- 23.EM 算法与混合模型
- 24.高斯混合模型 GMM
- 25.从混合模型到隐马尔可夫模型
- 26.隐马尔可夫模型(HMM)推导与维特比解码
- 27.线性链条件随机场 CRF
- 28.贝叶斯网络与图模型
- 29.隐马尔可夫链与 HMM 推导
- 30.PLSA 主题模型
- 31.LSI 与 SVD 向量表达
- 32.LDA 与狄利克雷分布
- 33.主题模型实验复现(困惑度 / 分类 / 相似度 / 延展)
- 34.上下文无关文法(CFG)与句法分析
- 35.概率上下文无关文法 PCFG
- 36.统计机器翻译:词对齐与翻译模型
- 37.统计机器翻译中的对齐模型与参数估计
- 38.从统计机器翻译到神经机器翻译
- 39.神经元与反向传播速览(指向深度学习课)
- 40.CNN 原理及其在 NLP 中的应用
- 41.循环神经网络的基本原理与 Elman / Jordan 网络
- 42.LSTM:长短期记忆网络的核心思想与分步实现
- 43.Encoder-Decoder 框架
- 44.Seq2Seq 与早期注意力机制
- 45.注意力机制的背景与动机
- 46.注意力模型实现原理与匹配函数
- 47.记忆网络:组成与计算
- 48.注意力机制的应用与历史延展
- 49.Transformer 结构:Encoder-Decoder 栈与 Self-Attention
- 50.Multi-Head Attention、FFNN 与位置编码
- 51.层归一化、损失函数与 Universal Transformer 延展
- 52.预训练范式:从 ELMo 到 BERT
- 53.BERT 与掩码语言模型
- 54.BERT 下游任务微调与 GLUE 口径
- 55.GPT 与自回归语言建模
- 56.GPT-1 到 GPT-4 的演进
- 57.指令微调与 RLHF
- 58.分词器:BPE、WordPiece 与中文词表
- 59.上下文窗口与长文本处理
- 60.能力边界、成本与适用场景
- 61.文本分类实战:从朴素贝叶斯到逻辑回归
- 62.命名实体识别实战
- 63.文本生成与摘要实战
- 64.语义检索与向量库实战
- 65.RAG 知识库问答
- 66.NLP 评估指标:BLEU / ROUGE / BERTScore 与人工评估
- 67.幻觉与事实性问题
- 68.文本与知识结合(知识图谱压缩)
- 69.跨媒体信息检索与未来展望
Multi-Head Attention、FFNN 与位置编码
小象实战讲义 · 自然语言处理
在上一节中,我们学习了自注意力(Self-Attention)机制,它让模型在处理序列时能够动态地关注不同位置的信息。然而,单一的注意力视角可能不足以捕捉词语丰富的语义内涵。例如,一个多义词在不同上下文中的含义可能截然不同。本节我们将引入多头注意力(Multi-Head Attention),它通过并行计算多个注意力“头”,从不同子空间捕获信息,极大地增强了模型的表达能力。同时,我们将剖析Transformer编码器中的前馈神经网络(Feed-Forward Neural Network, FFNN),并探讨如何通过位置编码(Positional Encoding) 为模型注入序列的顺序信息,这是Transformer摒弃循环和卷积结构后,理解序列位置关系的关键。
💡 核心导读
- 多头注意力(Multi-Head Attention):通过多组独立的权重矩阵并行计算注意力,从不同“角度”或“子空间”理解输入序列,最后将结果拼接并线性变换,以捕获更丰富的语义关系。
- 前馈神经网络(FFNN):在自注意力层之后,对每个位置的表示独立地进行非线性变换(通常是两层全连接层),用于增强模型的表示能力。
- 位置编码(Positional Encoding):将序列中每个位置的顺序信息编码成一个固定维度的向量,并与词嵌入向量相加,使模型能够感知词语的顺序。
- 编码器堆叠(Stacked Encoders):Transformer编码器由多个相同的层堆叠而成,每层包含一个多头自注意力子层和一个前馈神经网络子层,并辅以残差连接和层归一化。
从单头到多头:Multi-Head Attention
在标准的自注意力机制中,我们使用一组权重矩阵(, , )为输入序列 生成查询()、键()和值()矩阵,然后计算注意力输出 。这个过程可以看作是从一个单一的“视角”去计算序列元素之间的关联。
然而,词语的语义往往是多面的。以单词“bank”为例,它既有“银行”的含义,也有“河岸”的含义。当它与“money”计算注意力时,我们希望模型能强化“银行”这个语义;当它与“river”计算注意力时,则希望强化“河岸”的语义。单一的自注意力计算可能会将这两种语义信息混合并取平均,导致信息模糊。
多头注意力的提出正是为了解决这个问题。其核心思想是:并行地进行多次自注意力计算,每次使用不同的、可学习的权重矩阵,从而让模型能够从不同的表示子空间(或理解为不同的“角度”)关注输入信息。
多头注意力的计算过程
假设我们设置 个头(在原始Transformer论文中 )。对于第 个头(),我们有独立的权重矩阵 , , 。计算步骤如下:
生成各头的 Q, K, V:对于每个头 ,计算: 通常, 的维度设计会使得 的维度是原始词嵌入维度 的 ,即 。这保证了多头注意力总的计算量与单头注意力大致相当。
计算各头的注意力输出:对每个头独立计算缩放点积注意力:
拼接多头输出:将所有头的输出在最后一个维度(特征维度)上进行拼接:
线性投影:将拼接后的结果通过一个可学习的输出权重矩阵 进行线性变换,将其投影回原始的 维度:
这个过程可以类比于卷积神经网络(CNN)中使用多个不同的卷积核(filter)来提取图像的不同特征(如边缘、纹理、颜色),然后将这些特征图融合起来,形成更全面的图像表示。多头注意力也是让模型同时关注来自不同位置、不同表示子空间的信息。
import numpy as np
def scaled_dot_product_attention(Q, K, V):
"""
计算缩放点积注意力。
为简化演示,假设Q, K, V的维度为 (seq_len, d_k) 或 (seq_len, d_v)。
实际中通常有批次维度。
"""
d_k = Q.shape[-1]
# 计算注意力分数
scores = np.dot(Q, K.T) / np.sqrt(d_k) # (seq_len, seq_len)
# 应用softmax得到注意力权重(按行归一化)
exp_s = np.exp(scores - np.max(scores, axis=-1, keepdims=True))
attention_weights = exp_s / np.sum(exp_s, axis=-1, keepdims=True)
# 加权求和
output = np.dot(attention_weights, V) # (seq_len, d_v)
return output, attention_weights
def multi_head_attention_naive(X, h=2, d_model=4):
"""
一个简化的多头注意力计算示例,用于理解流程。
注意:此实现未严格遵循原始论文的维度划分,仅作概念演示。
"""
seq_len, _ = X.shape
# 假设每个头的维度 d_k = d_v = d_model // h
d_k = d_v = d_model // h
print(f"输入 X 形状: {X.shape}")
print(f"头数 h={h}, 每个头维度 d_k=d_v={d_k}")
# 为每个头初始化随机权重矩阵 (实际中为可学习参数)
np.random.seed(42)
all_heads_output = []
for i in range(h):
W_q = np.random.randn(d_model, d_k) * 0.1
W_k = np.random.randn(d_model, d_k) * 0.1
W_v = np.random.randn(d_model, d_v) * 0.1
W_o = np.random.randn(d_v * h, d_model) * 0.1 # 输出投影矩阵
# 计算当前头的 Q, K, V
Q_i = np.dot(X, W_q) # (seq_len, d_k)
K_i = np.dot(X, W_k) # (seq_len, d_k)
V_i = np.dot(X, W_v) # (seq_len, d_v)
# 计算当前头的注意力输出
head_i, attn_i = scaled_dot_product_attention(Q_i, K_i, V_i)
all_heads_output.append(head_i)
print(f"头 {i} 输出形状: {head_i.shape}")
# 拼接所有头的输出
concatenated = np.concatenate(all_heads_output, axis=-1) # (seq_len, d_v * h)
print(f"拼接后形状: {concatenated.shape}")
# 线性投影回 d_model 维度
output = np.dot(concatenated, W_o) # (seq_len, d_model)
print(f"最终输出 (经过W_o投影) 形状: {output.shape}")
return output
# 示例输入:两个词的嵌入,每个词维度为4
X_demo = np.array([[0.1, 0.2, 0.3, 0.4],
[0.5, 0.6, 0.7, 0.8]])
output_demo = multi_head_attention_naive(X_demo, h=2, d_model=4)输入 X 形状: (2, 4)
头数 h=2, 每个头维度 d_k=d_v=2
头 0 输出形状: (2, 2)
头 1 输出形状: (2, 2)
拼接后形状: (2, 4)
最终输出 (经过W_o投影) 形状: (2, 4)前馈神经网络(FFNN)
在Transformer的编码器和解码器层中,每个自注意力子层(或多头注意力子层)后面都跟着一个前馈神经网络(Feed-Forward Neural Network, FFNN)。这个FFNN对序列中每个位置的表示进行独立且相同的变换。
具体来说,对于一个位置 的向量表示 (来自自注意力层的输出),FFNN对其进行如下操作:
其中:
- ,
- ,
- 是前馈网络中间层的维度,通常比 大得多(例如,原始论文中 , )。
这个两层全连接网络的作用是为模型引入非线性变换,增强其表示能力。由于它对每个位置独立操作,有时也被称为位置式前馈网络(Position-wise Feed-Forward Network)。
import numpy as np
def positionwise_feedforward(z, d_model=4, d_ff=8):
"""
简化的位置式前馈网络计算。
z: 输入,形状为 (seq_len, d_model)
"""
np.random.seed(42)
# 初始化权重和偏置
W1 = np.random.randn(d_model, d_ff) * 0.1
b1 = np.random.randn(d_ff) * 0.1
W2 = np.random.randn(d_ff, d_model) * 0.1
b2 = np.random.randn(d_model) * 0.1
# 第一层线性变换 + ReLU激活
hidden = np.dot(z, W1) + b1 # (seq_len, d_ff)
hidden_relu = np.maximum(0, hidden) # ReLU激活
# 第二层线性变换
output = np.dot(hidden_relu, W2) + b2 # (seq_len, d_model)
return output
# 示例输入:两个位置、维度为 4 的向量(本块独立可运行)
z_demo = np.array([[0.1, 0.2, 0.3, 0.4],
[0.5, 0.6, 0.7, 0.8]])
ffn_output = positionwise_feedforward(z_demo, d_model=4, d_ff=8)
print(f"FFNN输入形状: {z_demo.shape}")
print(f"FFNN输出形状: {ffn_output.shape}")FFNN输入形状: (2, 4)
FFNN输出形状: (2, 4)位置编码(Positional Encoding)
自注意力机制本身是置换等变(permutation equivariant) 的:打乱输入序列的顺序,输出的序列表示也只会相应地被同等地打乱,但模型无法感知词语的原始顺序。这对于理解语言是致命的,因为“猫追老鼠”和“老鼠追猫”的含义完全不同。
为了给模型注入序列的顺序信息,Transformer引入了位置编码。其做法是:为序列中的每个位置 (从0开始计数)生成一个与词嵌入维度 相同的向量 ,然后将这个位置编码向量与对应位置的词嵌入向量 直接相加,作为编码器的实际输入:
那么,如何设计 呢?Transformer论文使用了一组正弦和余弦函数来生成位置编码:
其中 是位置, 是维度索引()。这种设计具有几个优良性质:
- 唯一性:每个位置都有唯一的编码。
- 相对位置关系可学习:对于固定的偏移量 , 可以表示为 的线性函数,这使得模型能够轻松地学习到相对位置信息。
- 值域有界:正弦余弦各分量均位于 ;位置编码与词嵌入相加时的相对尺度还取决于嵌入分布及缩放设置,并不能由“值域有界”直接推出二者尺度匹配。
- 更长序列的位置向量可按需计算:正弦/余弦位置编码由函数式生成,不受固定位置表长度限制,可以为训练时未出现过的更长位置计算位置向量;但模型对未见长度的外推效果并无保证,不能据此认为可以任意长度稳定工作。
import numpy as np
def get_positional_encoding(seq_len, d_model):
"""
生成位置编码矩阵。
seq_len: 序列长度
d_model: 模型维度/词嵌入维度
返回形状为 (seq_len, d_model) 的矩阵
"""
PE = np.zeros((seq_len, d_model))
for pos in range(seq_len):
for j in range(d_model // 2):
denominator = np.power(10000, (2 * j) / d_model)
PE[pos, 2 * j] = np.sin(pos / denominator)
PE[pos, 2 * j + 1] = np.cos(pos / denominator)
return PE
# 生成一个长度为5,维度为4的位置编码
pos_enc = get_positional_encoding(seq_len=5, d_model=4)
print("位置编码矩阵 (seq_len=5, d_model=4):")
print(np.round(pos_enc, 4))位置编码矩阵 (seq_len=5, d_model=4):
[[ 0. 1. 0. 1. ]
[ 0.8415 0.5403 0.01 1. ]
[ 0.9093 -0.4161 0.02 0.9998]
[ 0.1411 -0.99 0.03 0.9996]
[-0.7568 -0.6536 0.04 0.9992]]编码器层堆叠与信息流动
现在,我们可以将上述组件组合起来,理解一个完整的Transformer编码器层以及多层堆叠的结构。
一个编码器层(Encoder Layer) 包含两个主要子层:
- 多头自注意力子层(Multi-Head Self-Attention):输入序列自己对自己做注意力,捕获序列内部的依赖关系。
- 前馈神经网络子层(FFNN):对每个位置的表示进行非线性增强。
每个子层周围都应用了残差连接(Residual Connection) 和层归一化(Layer Normalization)。具体操作如下:
- 对于子层 ,其输出为 。
- 残差连接有助于缓解深层网络中的梯度消失问题,使模型更容易训练。
- 层归一化对每个样本的所有特征进行归一化,稳定训练过程。
一个完整的Transformer编码器由 个(原始论文中 )这样的编码器层堆叠而成。信息流动过程如下:
- 输入词序列经过词嵌入层,得到词向量 。
- 与对应位置的位置编码 相加,得到编码器的初始输入 。
- 输入到第一个编码器层,经过(带残差和归一化的)多头自注意力和FFNN,得到输出 。
- 作为输入,送入第二个编码器层,得到 ,依此类推,直到第 层,得到最终的编码器输出。
这个最终的输出包含了输入序列丰富的上下文信息,可以用于下游任务(如分类),或者作为解码器的输入(在机器翻译等序列生成任务中)。
信息流动示意图 (以两个编码器层为例):
输入词序列: [w1, w2, w3]
|
v
词嵌入层: [E1, E2, E3]
|
+ <--- 位置编码 PE
|
v
编码器输入: X = [E1+PE1, E2+PE2, E3+PE3]
|
v
[编码器层 1]
|-- 多头自注意力 (带残差 & 层归一化) --> 中间表示1
|-- 前馈网络 (带残差 & 层归一化) --> 输出表示1 (X1)
|
v
[编码器层 2]
|-- 多头自注意力 (带残差 & 层归一化) --> 中间表示2
|-- 前馈网络 (带残差 & 层归一化) --> 输出表示2 (X2,即最终编码器输出)
|
v
下游任务 / 解码器📝 动手练一练
- 概念辨析:为什么说自注意力机制本身是“置换等变”的?位置编码是如何解决这个问题的?请尝试用一句话解释正弦/余弦位置编码公式中,变量
pos和i分别代表什么。
👉 点击查看参考答案
- 置换等变性:自注意力计算中,输出 的每个位置是值向量 的加权和,权重由 和 的点积决定。如果打乱输入序列 的顺序(即置换), 的行也会被同步打乱,导致注意力权重矩阵和输出 的行发生相同的置换——输出随行置换而非保持不变,因此是置换等变;但模型本身无法区分这是原始顺序还是被打乱后的顺序。
- 位置编码的解决方式:通过为每个绝对位置添加一个独特的、可学习的或预设的向量,使得模型输入 包含了位置信息,从而打破这种置换等变性。
- 公式中的变量:在 中,
pos代表词语在序列中的绝对位置(如第0个词,第1个词),i代表位置编码向量的维度索引。
- 手算练习:假设我们有一个极简的序列,词嵌入维度
d_model=2,使用h=1的单头注意力(即标准自注意力)。输入为两个词X = [[1, 0], [0, 1]],权重矩阵W_q = [[1,0],[0,1]],W_k = [[1,0],[0,1]],W_v = [[1,0],[0,1]](即单位矩阵)。请手动计算缩放点积注意力层的输出Z(忽略softmax的数值稳定性处理,直接计算)。
👉 点击查看参考答案
计算过程:
- 计算 Q, K, V:
- Q = X * = [[1,0],[0,1]] * I = [[1,0],[0,1]]
- K = X * = [[1,0],[0,1]]
- V = X * = [[1,0],[0,1]]
- 计算注意力分数 scores = Q * / sqrt(),其中 =2。
- scores = [[1,0],[0,1]] * [[1,0],[0,1]]^T / sqrt(2) = [[1,0],[0,1]] / 1.414 ≈ [[0.7071, 0], [0, 0.7071]]
- 对scores的每一行应用softmax得到注意力权重attn_weights。
- 对于第一行 [0.7071, 0]: softmax = [exp(0.7071)/(exp(0.7071)+exp(0)), exp(0)/(exp(0.7071)+exp(0))] ≈ [0.67, 0.33]
- 对于第二行 [0, 0.7071]: softmax = [exp(0)/(exp(0)+exp(0.7071)), exp(0.7071)/(exp(0)+exp(0.7071))] ≈ [0.33, 0.67]
- attn_weights ≈ [[0.67, 0.33], [0.33, 0.67]]
- 计算输出 Z = attn_weights * V。
- Z ≈ [[0.671+0.330, 0.670+0.331], [0.331+0.670, 0.330+0.671]] = [[0.67, 0.33], [0.33, 0.67]]
因此,输出 Z ≈ [[0.67, 0.33], [0.33, 0.67]]。可以看到,每个位置的输出都融合了自身和另一个位置的信息。
本章小结
本节我们深入剖析了Transformer架构中除自注意力外的另外三个核心组件:多头注意力(Multi-Head Attention)、前馈神经网络(FFNN)和位置编码(Positional Encoding)。
- 多头注意力通过并行计算多个注意力头,使模型能够从不同的表示子空间捕获信息,增强了模型处理复杂语义关系(如多义词)的能力。
- 前馈神经网络作为编码器/解码器层中的另一个关键子层,对每个位置的表示进行独立且相同的非线性变换,进一步提升了模型的表示能力。
- 位置编码是Transformer理解序列顺序的关键。通过将基于正弦/余弦函数的绝对位置向量与词嵌入相加,模型得以感知词语在序列中的位置信息。
这些组件与自注意力机制一起,构成了Transformer编码器层。通过残差连接和层归一化的辅助,多个这样的编码器层可以稳定地堆叠,形成强大的序列编码器。这为后续学习基于Transformer的预训练模型(如BERT、GPT)奠定了坚实的基础。
📋 行动清单
- 理解多头注意力中“头”(head)的概念,并能解释其相对于单头注意力的优势。
- 能描述位置编码的作用,并理解其正弦/余弦函数设计的基本思想。
- 能够画出或描述一个Transformer编码器层(包含多头自注意力、FFNN、残差连接和层归一化)内部的信息流动图。
—— 小象教研组