查看全课大纲(第 50 / 69 节)
  1. 1.NLP 的研究领域、应用与学习路径
  2. 2.自然语言的基本术语
  3. 3.信息论基础:信息熵与交叉熵
  4. 4.语言的进化与语义变迁
  5. 5.主观概率、客观概率与条件概率
  6. 6.贝叶斯原理与推理
  7. 7.随机变量:二项式、期望方差与联合概率
  8. 8.常见概率分布
  9. 9.辛普森悖论与贝叶斯解题实例
  10. 10.语言模型:语言概率与词袋模型
  11. 11.n-gram 语言模型(二元 / 三元 / N-Gram)
  12. 12.语言模型评价:交叉熵、困惑度与插值
  13. 13.朴素贝叶斯文本分类:垃圾邮件
  14. 14.拼写检查
  15. 15.中文分词:构词法、最大匹配、最优路径与 Jieba
  16. 16.词表示问题与语义相似度
  17. 17.TF-IDF 权重处理
  18. 18.One-Hot 表达与词表示的局限
  19. 19.Word2Vec 核心思想
  20. 20.Word2Vec 训练过程
  21. 21.Skip-gram 模型详解
  22. 22.词性标注任务与标注方法(规则 + N-Gram)
  23. 23.EM 算法与混合模型
  24. 24.高斯混合模型 GMM
  25. 25.从混合模型到隐马尔可夫模型
  26. 26.隐马尔可夫模型(HMM)推导与维特比解码
  27. 27.线性链条件随机场 CRF
  28. 28.贝叶斯网络与图模型
  29. 29.隐马尔可夫链与 HMM 推导
  30. 30.PLSA 主题模型
  31. 31.LSI 与 SVD 向量表达
  32. 32.LDA 与狄利克雷分布
  33. 33.主题模型实验复现(困惑度 / 分类 / 相似度 / 延展)
  34. 34.上下文无关文法(CFG)与句法分析
  35. 35.概率上下文无关文法 PCFG
  36. 36.统计机器翻译:词对齐与翻译模型
  37. 37.统计机器翻译中的对齐模型与参数估计
  38. 38.从统计机器翻译到神经机器翻译
  39. 39.神经元与反向传播速览(指向深度学习课)
  40. 40.CNN 原理及其在 NLP 中的应用
  41. 41.循环神经网络的基本原理与 Elman / Jordan 网络
  42. 42.LSTM:长短期记忆网络的核心思想与分步实现
  43. 43.Encoder-Decoder 框架
  44. 44.Seq2Seq 与早期注意力机制
  45. 45.注意力机制的背景与动机
  46. 46.注意力模型实现原理与匹配函数
  47. 47.记忆网络:组成与计算
  48. 48.注意力机制的应用与历史延展
  49. 49.Transformer 结构:Encoder-Decoder 栈与 Self-Attention
  50. 50.Multi-Head Attention、FFNN 与位置编码
  51. 51.层归一化、损失函数与 Universal Transformer 延展
  52. 52.预训练范式:从 ELMo 到 BERT
  53. 53.BERT 与掩码语言模型
  54. 54.BERT 下游任务微调与 GLUE 口径
  55. 55.GPT 与自回归语言建模
  56. 56.GPT-1 到 GPT-4 的演进
  57. 57.指令微调与 RLHF
  58. 58.分词器:BPE、WordPiece 与中文词表
  59. 59.上下文窗口与长文本处理
  60. 60.能力边界、成本与适用场景
  61. 61.文本分类实战:从朴素贝叶斯到逻辑回归
  62. 62.命名实体识别实战
  63. 63.文本生成与摘要实战
  64. 64.语义检索与向量库实战
  65. 65.RAG 知识库问答
  66. 66.NLP 评估指标:BLEU / ROUGE / BERTScore 与人工评估
  67. 67.幻觉与事实性问题
  68. 68.文本与知识结合(知识图谱压缩)
  69. 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

在标准的自注意力机制中,我们使用一组权重矩阵(WQW^Q, WKW^K, WVW^V)为输入序列 XX 生成查询(QQ)、键(KK)和值(VV)矩阵,然后计算注意力输出 Z=Attention(Q,K,V)Z = \text{Attention}(Q, K, V)。这个过程可以看作是从一个单一的“视角”去计算序列元素之间的关联。

然而,词语的语义往往是多面的。以单词“bank”为例,它既有“银行”的含义,也有“河岸”的含义。当它与“money”计算注意力时,我们希望模型能强化“银行”这个语义;当它与“river”计算注意力时,则希望强化“河岸”的语义。单一的自注意力计算可能会将这两种语义信息混合并取平均,导致信息模糊。

多头注意力的提出正是为了解决这个问题。其核心思想是:并行地进行多次自注意力计算,每次使用不同的、可学习的权重矩阵,从而让模型能够从不同的表示子空间(或理解为不同的“角度”)关注输入信息。

多头注意力的计算过程

假设我们设置 hh 个头(在原始Transformer论文中 h=8h=8)。对于第 ii 个头(i=0,1,...,h−1i=0,1,...,h-1),我们有独立的权重矩阵 WiQW_i^Q, WiKW_i^K, WiVW_i^V。计算步骤如下:

  1. 生成各头的 Q, K, V:对于每个头 ii,计算: Qi=XWiQ,Ki=XWiK,Vi=XWiVQ_i = X W_i^Q, \quad K_i = X W_i^K, \quad V_i = X W_i^V 通常,WiQ,WiK,WiVW_i^Q, W_i^K, W_i^V 的维度设计会使得 Qi,Ki,ViQ_i, K_i, V_i 的维度是原始词嵌入维度 dmodeld_{model} 的 1/h1/h,即 dk=dv=dmodel/hd_k = d_v = d_{model}/h。这保证了多头注意力总的计算量与单头注意力大致相当。

  2. 计算各头的注意力输出:对每个头独立计算缩放点积注意力: headi=Attention(Qi,Ki,Vi)=softmax(QiKiTdk)Vi\text{head}_i = \text{Attention}(Q_i, K_i, V_i) = \text{softmax}(\frac{Q_i K_i^T}{\sqrt{d_k}}) V_i

  3. 拼接多头输出:将所有头的输出在最后一个维度(特征维度)上进行拼接: MultiHead(Q,K,V)=Concat(head0,head1,...,headh−1)\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_0, \text{head}_1, ..., \text{head}_{h-1})

  4. 线性投影:将拼接后的结果通过一个可学习的输出权重矩阵 WOW^O 进行线性变换,将其投影回原始的 dmodeld_{model} 维度: Output=MultiHead(Q,K,V)WO\text{Output} = \text{MultiHead}(Q, K, V) W^O

这个过程可以类比于卷积神经网络(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对序列中每个位置的表示进行独立且相同的变换。

具体来说,对于一个位置 ii 的向量表示 ziz_i(来自自注意力层的输出),FFNN对其进行如下操作: FFN(zi)=ReLU(ziW1+b1)W2+b2\text{FFN}(z_i) = \text{ReLU}(z_i W_1 + b_1) W_2 + b_2

其中:

  • W1∈Rdmodel×dffW_1 \in \mathbb{R}^{d_{model} \times d_{ff}}, b1∈Rdffb_1 \in \mathbb{R}^{d_{ff}}
  • W2∈Rdff×dmodelW_2 \in \mathbb{R}^{d_{ff} \times d_{model}}, b2∈Rdmodelb_2 \in \mathbb{R}^{d_{model}}
  • dffd_{ff} 是前馈网络中间层的维度,通常比 dmodeld_{model} 大得多(例如,原始论文中 dmodel=512d_{model}=512, dff=2048d_{ff}=2048)。

这个两层全连接网络的作用是为模型引入非线性变换,增强其表示能力。由于它对每个位置独立操作,有时也被称为位置式前馈网络(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引入了位置编码。其做法是:为序列中的每个位置 pospos(从0开始计数)生成一个与词嵌入维度 dmodeld_{model} 相同的向量 PE(pos)PE(pos),然后将这个位置编码向量与对应位置的词嵌入向量 E(word)E(word) 直接相加,作为编码器的实际输入: X(pos)=E(wordpos)+PE(pos)X^{(pos)} = E(word_{pos}) + PE(pos)

那么,如何设计 PE(pos)PE(pos) 呢?Transformer论文使用了一组正弦和余弦函数来生成位置编码: PE(pos,2i)=sin⁡(pos/100002i/dmodel)PE_{(pos, 2i)} = \sin(pos / 10000^{2i/d_{model}}) PE(pos,2i+1)=cos⁡(pos/100002i/dmodel)PE_{(pos, 2i+1)} = \cos(pos / 10000^{2i/d_{model}})

其中 pospos 是位置,ii 是维度索引(0≤i<dmodel/20 \le i < d_{model}/2)。这种设计具有几个优良性质:

  1. 唯一性:每个位置都有唯一的编码。
  2. 相对位置关系可学习:对于固定的偏移量 kk,PE(pos+k)PE(pos+k) 可以表示为 PE(pos)PE(pos) 的线性函数,这使得模型能够轻松地学习到相对位置信息。
  3. 值域有界:正弦余弦各分量均位于 [−1,1][-1, 1];位置编码与词嵌入相加时的相对尺度还取决于嵌入分布及缩放设置,并不能由“值域有界”直接推出二者尺度匹配。
  4. 更长序列的位置向量可按需计算:正弦/余弦位置编码由函数式生成,不受固定位置表长度限制,可以为训练时未出现过的更长位置计算位置向量;但模型对未见长度的外推效果并无保证,不能据此认为可以任意长度稳定工作。
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) 包含两个主要子层:

  1. 多头自注意力子层(Multi-Head Self-Attention):输入序列自己对自己做注意力,捕获序列内部的依赖关系。
  2. 前馈神经网络子层(FFNN):对每个位置的表示进行非线性增强。

每个子层周围都应用了残差连接(Residual Connection) 和层归一化(Layer Normalization)。具体操作如下:

  • 对于子层 Sublayer(x)Sublayer(x),其输出为 LayerNorm(x+Sublayer(x))\text{LayerNorm}(x + \text{Sublayer}(x))。
  • 残差连接有助于缓解深层网络中的梯度消失问题,使模型更容易训练。
  • 层归一化对每个样本的所有特征进行归一化,稳定训练过程。

一个完整的Transformer编码器由 NN 个(原始论文中 N=6N=6)这样的编码器层堆叠而成。信息流动过程如下:

  1. 输入词序列经过词嵌入层,得到词向量 EE。
  2. EE 与对应位置的位置编码 PEPE 相加,得到编码器的初始输入 X=E+PEX = E + PE。
  3. XX 输入到第一个编码器层,经过(带残差和归一化的)多头自注意力和FFNN,得到输出 X1X_1。
  4. X1X_1 作为输入,送入第二个编码器层,得到 X2X_2,依此类推,直到第 NN 层,得到最终的编码器输出。

这个最终的输出包含了输入序列丰富的上下文信息,可以用于下游任务(如分类),或者作为解码器的输入(在机器翻译等序列生成任务中)。

信息流动示意图 (以两个编码器层为例):

输入词序列: [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
下游任务 / 解码器

📝 动手练一练

  1. 概念辨析:为什么说自注意力机制本身是“置换等变”的?位置编码是如何解决这个问题的?请尝试用一句话解释正弦/余弦位置编码公式中,变量 pos 和 i 分别代表什么。
👉 点击查看参考答案
  • 置换等变性:自注意力计算中,输出 ZZ 的每个位置是值向量 VV 的加权和,权重由 QQ 和 KK 的点积决定。如果打乱输入序列 XX 的顺序(即置换),Q,K,VQ, K, V 的行也会被同步打乱,导致注意力权重矩阵和输出 ZZ 的行发生相同的置换——输出随行置换而非保持不变,因此是置换等变;但模型本身无法区分这是原始顺序还是被打乱后的顺序。
  • 位置编码的解决方式:通过为每个绝对位置添加一个独特的、可学习的或预设的向量,使得模型输入 E+PEE+PE 包含了位置信息,从而打破这种置换等变性。
  • 公式中的变量:在 PE(pos,2i)=sin⁡(pos/100002i/dmodel)PE_{(pos, 2i)} = \sin(pos / 10000^{2i/d_{model}}) 中,pos 代表词语在序列中的绝对位置(如第0个词,第1个词),i 代表位置编码向量的维度索引。
  1. 手算练习:假设我们有一个极简的序列,词嵌入维度 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的数值稳定性处理,直接计算)。
👉 点击查看参考答案

计算过程:

  1. 计算 Q, K, V:
    • Q = X * WqW_q = [[1,0],[0,1]] * I = [[1,0],[0,1]]
    • K = X * WkW_k = [[1,0],[0,1]]
    • V = X * WvW_v = [[1,0],[0,1]]
  2. 计算注意力分数 scores = Q * KTK^T / sqrt(dkd_k),其中 dkd_k=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]]
  3. 对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]]
  4. 计算输出 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、残差连接和层归一化)内部的信息流动图。

—— 小象教研组