查看全课大纲(第 46 / 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.跨媒体信息检索与未来展望

注意力模型实现原理与匹配函数

小象实战讲义 · 自然语言处理

在上一节我们了解了注意力机制的基本思想和宏观架构。本节课,我们将深入其内部,详细拆解注意力模型的具体实现原理,并介绍一个关键组件——匹配函数(Match Function)。理解这些细节,是掌握从传统Seq2Seq+Attention到现代Transformer自注意力机制的关键一步。

💡 核心导读

  • 注意力计算流程:掌握从查询(Query)与键(Key)的相似度计算,到权重归一化(Softmax),再到加权求和得到上下文向量(Context Vector)的完整三步流程。
  • 点积作为相似度度量:理解为何在向量化表示下,点积(Dot Product)能有效衡量语义相关性,并作为注意力分数的基础。
  • 匹配函数的作用:了解匹配函数在更复杂的注意力架构(如记忆网络)中,如何用于计算查询与外部记忆(Memory)之间的关联强度。
  • 从原理到应用:通过机器翻译的实例,直观感受注意力权重如何动态分配,并理解其相对于传统编码器-解码器模型的优势。

注意力机制的计算原理

让我们从一个具体的场景出发:基于注意力机制的序列到序列(Seq2Seq)模型,常用于机器翻译。假设我们要将中文“我爱中国”翻译成英文“I love China”。

在传统的编码器-解码器模型中,编码器将整个源语句压缩成一个固定长度的上下文向量,解码器仅基于这个单一的向量生成目标语句。这就像要求你在只记住一句话的“中心思想”后,逐词复述出另一种语言的版本,对于长句子来说信息损失严重。

注意力机制的引入,让解码器在生成每一个目标词时,都能“回顾”编码器对所有源词的处理结果,并动态决定当前应该“关注”源语句的哪些部分。

1. 核心计算步骤

注意力计算可以分解为三个清晰的步骤:评分(Score)、归一化(Softmax)、加权求和(Weighted Sum)。

我们定义一些符号:

  • 编码器隐藏状态:源语句经过编码器(如双向RNN/LSTM)处理后,每个输入词 i 都对应一个隐藏状态向量 h_i。这些向量共同构成了注意力可查询的“记忆库”。
  • 解码器上一时刻隐藏状态:在生成目标词 t 的时刻,解码器上一时刻(t-1)的隐藏状态 s_{t-1} 被用作查询(Query),因为它包含了到目前生成为止的上下文信息。
  • 目标:计算一个上下文向量(Context Vector) c_t,它是对所有编码器隐藏状态 h_i 的加权和,权重反映了 s_{t-1} 与每个 h_i 的相关性。

步骤一:计算注意力分数(Score) 对于每一个编码器隐藏状态 h_i,我们计算其与查询 s_{t-1} 的相似度得分 e_{t,i}。 一种最直接且常用的方法是点积(Dot Product): e_{t,i} = s_{t-1}^T \cdot h_i 为什么用点积?因为 h_i 和 s_{t-1} 都是经过网络学习得到的向量,其维度相同。点积同时包含方向与模长两方面的信息:a⋅b=∥a∥∥b∥cos⁡θ\mathbf{a}\cdot\mathbf{b}=\lVert\mathbf{a}\rVert\lVert\mathbf{b}\rVert\cos\theta。在模长大致相当时,点积越大通常表示方向越接近、夹角越小,语义上可能越相关;但模长较大的向量即使方向并非更接近,也可能得到更大的点积,因此点积大小不能直接等同于夹角大小。结果越小(甚至为负),则表示方向越偏离,相关性越低。这为我们提供了一种衡量“关注度”的原始分数。

步骤二:权重归一化(Softmax) 原始分数 e_{t,i} 的大小范围不确定,我们需要将其转换为一个概率分布,即所有权重之和为1。这通过Softmax函数实现: \alpha_{t,i} = \frac{\exp(e_{t,i})}{\sum_{j=1}^{N} \exp(e_{t,j})} 这里 N 是源语句长度。\alpha_{t,i} 就是最终的注意力权重,它表示在生成第 t 个目标词时,模型对第 i 个源词的关注程度。例如,\alpha_{t,2}=0.7 意味着模型认为当前目标词70%的信息应来源于第二个源词。

步骤三:计算上下文向量(Weighted Sum) 最后,我们将所有编码器隐藏状态 h_i 按其对应的注意力权重 \alpha_{t,i} 进行加权求和,得到上下文向量 c_t: c_t = \sum_{i=1}^{N} \alpha_{t,i} h_i 这个 c_t 不再是整个句子的笼统概括,而是针对当前生成步骤动态聚焦后的源端信息摘要。解码器将 c_t 与自身的当前状态结合,来预测下一个目标词。

2. 一个简化的数值示例

让我们通过一个极度简化的例子来直观感受这个过程。假设向量维度为3,我们有一个查询向量和两个“记忆”向量。

import numpy as np

# 固定随机种子以确保输出可复现
np.random.seed(42)

# 定义向量
# s_{t-1}: 解码器上一时刻的隐藏状态 (Query)
s_prev = np.array([0.8, -0.2, 0.5])
# h1, h2: 编码器对两个源词的隐藏状态 (Keys/Values)
h1 = np.array([0.1, 0.9, 0.3])
h2 = np.array([0.7, 0.2, -0.5])

print("查询向量 s_prev:", s_prev)
print("编码器状态 h1:", h1)
print("编码器状态 h2:", h2)
print()

# 步骤1: 计算点积分数
score1 = np.dot(s_prev, h1)
score2 = np.dot(s_prev, h2)
print("原始注意力分数 e_t1 (s·h1):", round(score1, 4))
print("原始注意力分数 e_t2 (s·h2):", round(score2, 4))
print()

# 步骤2: Softmax 归一化得到权重
e_scores = np.array([score1, score2])
attention_weights = np.exp(e_scores) / np.sum(np.exp(e_scores))
print("Softmax 后的注意力权重 alpha:")
print("  alpha_t1 (对h1):", round(attention_weights[0], 4))
print("  alpha_t2 (对h2):", round(attention_weights[1], 4))
print("  权重和:", round(np.sum(attention_weights), 4))
print()

# 步骤3: 计算上下文向量 (加权和)
context_vector = attention_weights[0] * h1 + attention_weights[1] * h2
print("上下文向量 c_t (alpha1*h1 + alpha2*h2):")
print("  ", np.round(context_vector, 4))
查询向量 s_prev: [ 0.8 -0.2  0.5]
编码器状态 h1: [0.1 0.9 0.3]
编码器状态 h2: [ 0.7  0.2 -0.5]

原始注意力分数 e_t1 (s·h1): 0.05
原始注意力分数 e_t2 (s·h2): 0.27

Softmax 后的注意力权重 alpha:
  alpha_t1 (对h1): 0.4452
  alpha_t2 (对h2): 0.5548
  权重和: 1.0

上下文向量 c_t (alpha1*h1 + alpha2*h2):
   [ 0.4329  0.5117 -0.1438]

从结果可以看出,查询向量 s_prev 与 h2 的点积分数(0.27)高于与 h1 的分数(0.05),因此经过Softmax后,h2 获得了更高的注意力权重(0.5548),h1 为 0.4452,两者差距并不大。最终的上下文向量 c_t 是 h1 和 h2 的加权混合,但更偏向于 h2。

匹配函数(Match Function)的作用

在上述标准注意力中,匹配函数就是点积操作。它定义了如何计算查询(Query)与键(Key)之间的相似度。然而,在更复杂的架构中,例如记忆网络(Memory Networks),匹配函数的设计可以更加灵活和强大。

记忆网络显式地维护一个外部记忆矩阵,用于存储事实、知识或上下文信息。当有一个查询(如一个问题)输入时,模型需要在这个记忆库中找到最相关的信息片段来生成答案。这个过程通常涉及多跳(Multi-hop)推理。

匹配函数的设计

在记忆网络中,匹配函数 M(q, m) 负责计算查询向量 q 与记忆槽向量 m 之间的匹配分数。这个函数不限于点积,可以是:

  1. 点积(Dot Product):M(q, m) = q^T m。简单高效,假设向量在同一空间。
  2. 双线性变换(Bilinear):M(q, m) = q^T W m。其中 W 是一个可学习的权重矩阵。这允许模型学习查询空间和记忆空间之间更复杂的交互关系。
  3. 多层感知机(MLP):M(q, m) = MLP(concat(q, m))。将查询和记忆向量拼接后送入一个神经网络,输出一个标量分数。表达能力最强,但参数更多。

匹配函数的输出分数会经过Softmax,转化为从记忆库中读取不同记忆槽的概率权重,类似于标准注意力中的权重 \alpha。

多跳推理示例

考虑一个简单的阅读理解任务,知识(记忆)如下:

  1. John在操场。
  2. John捡起了足球。
  3. Bob去了厨房。

问题:足球在哪里?

单跳匹配(仅用问题匹配记忆):

  • 匹配函数计算“Where is the football?”与每个记忆的分数。
  • 记忆2(“John picked up the football”)可能得分最高,因为它直接包含“football”。
  • 但记忆2并没有直接给出位置。

多跳推理(两跳):

  • 第一跳:问题匹配记忆库,找到最相关的记忆(记忆2)。
  • 第二跳:将第一跳的输出(一个关于记忆2的向量表示)与原始问题结合,形成一个新的、更丰富的查询向量。
  • 用这个新查询再次匹配记忆库。此时,由于新查询包含了“John”和“football”的联合信息,它可能更强烈地匹配记忆1(“John在操场”),从而得出答案“操场”。
# 伪代码示意多跳记忆网络的匹配过程(非可执行)
# 记忆库 M = [m1, m2, m3]  # 每个mi是一个向量
# 初始查询 q0 = embed("Where is the football?")
# 可学习的匹配函数 match(q, m) -> score

# 第一跳
scores_hop1 = [match(q0, mi) for mi in M]
weights_hop1 = softmax(scores_hop1)
o1 = sum(weights_hop1[i] * M[i] for i in range(len(M))) # 读取的向量

# 更新查询:q1 = R(q0 + o1),其中R是一个可学习的变换(如全连接层)
q1 = transform(q0 + o1)

# 第二跳
scores_hop2 = [match(q1, mi) for mi in M]
weights_hop2 = softmax(scores_hop2)
o2 = sum(weights_hop2[i] * M[i] for i in range(len(M)))

# 最终答案基于 o2 预测
answer = predict_answer(o2)

匹配函数在这里是核心,它决定了模型如何评估查询与每一条记忆的相关性,从而引导多跳推理的路径。

注意力权重的可视化意义

注意力机制不仅有效,而且具有较好的可解释性。通过将注意力权重 \alpha_{t,i} 可视化,我们可以直观地看到在翻译过程中,目标词与源词之间的“对齐”关系。

例如,在翻译“I love China”时,生成“love”的时刻,注意力权重可能会在“爱”和“热爱”等词上形成高峰。而生成“China”时,高峰则集中在“中国”上。对于更复杂的句子,如将“我热爱这个国家”翻译成“I love this country”,生成“love”时的注意力可能会同时分布在“热”和“爱”上,体现了翻译的非一一对应性。

这种动态的、软性的对齐,是统计机器翻译中硬对齐的巨大进步,也是神经机器翻译成功的关键因素之一。

📝 动手练一练

  1. 概念辨析:在标准的Seq2Seq注意力模型中,计算注意力权重时,为什么要在点积分数上使用Softmax函数,而不是直接使用归一化后的点积值(如除以最大值)?
👉 点击查看参考答案

Softmax函数能够将一组实数(点积分数)转换为一个合法的概率分布,即所有输出值在(0,1)区间且和为1。这具有两个关键优势:

  • 概率解释:权重可以被解释为“概率”,表示模型将多少“注意力”分配给对应的源词,非常直观。
  • 突出主要项:Softmax的指数运算会按相对分差放大高分值与低分值之间的差距。分布是否尖锐取决于分差相对于温度(缩放系数)的大小:分差较大时最大项概率被明显抬高;分差较小时输出仍可接近均匀,如本节 0.05 与 0.27 的示例,Softmax 后也只得到 0.4452 与 0.5548,并非“一个赢家全拿”。而简单的线性归一化(如除以和或最大值)不具备这种非线性放大效果,可能导致注意力过于分散。
  1. 手算练习:假设在某个解码时刻,查询向量 q = [1, 0, -1],三个键向量分别为 k1 = [0, 1, 0], k2 = [1, 0, 1], k3 = [-1, 0, -1]。请手动计算(或编写一小段Python代码验证)使用点积注意力时,每个键的注意力权重 \alpha_i。
👉 点击查看参考答案

首先计算点积分数:

  • e1 = q·k1 = 1*0 + 0*1 + (-1)*0 = 0
  • e2 = q·k2 = 1*1 + 0*0 + (-1)*1 = 0
  • e3 = q·k3 = 1*(-1) + 0*0 + (-1)*(-1) = 0

三个分数都是0。应用Softmax:exp(0)=1,分母为 1+1+1=3。 因此,每个键的注意力权重均为 \alpha1 = \alpha2 = \alpha3 = 1/3 ≈ 0.3333。 这里点积为 0 只说明查询向量与这些键向量在当前表示和这种打分方式下正交(夹角为 90°),并不等同于语义上完全无关;而注意力被均匀分配,直接原因是三个分数恰好相等(都为 0),Softmax 对相等分数给出均匀权重。这也说明了为什么需要让模型学习到有意义的向量表示,使得相关的查询和键能有更高的点积分数。

import numpy as np
q = np.array([1, 0, -1])
keys = np.array([[0,1,0], [1,0,1], [-1,0,-1]])
scores = np.dot(keys, q) # 计算点积
weights = np.exp(scores) / np.sum(np.exp(scores))
print("点积分数:", scores)
print("注意力权重:", np.round(weights, 4))
点积分数: [0 0 0]
注意力权重: [0.3333 0.3333 0.3333]

本章小结

本节我们深入剖析了注意力模型的核心实现原理。关键在于理解其三步计算流程:评分-归一化-加权求和。点积作为最基础的匹配函数,因其计算简单且能有效反映向量相似度而被广泛使用。我们还探讨了在记忆网络等复杂架构中,匹配函数的扩展形式及其在多跳推理中的作用。

注意力机制的本质是让模型学会动态的、有选择性的信息检索。它解决了传统Seq2Seq模型信息瓶颈的问题,为处理长序列、复杂对齐任务提供了强大工具,并直接启发了后续Transformer模型中自注意力机制的诞生。

📋 行动清单

  • 在白板或纸上手动推导一遍注意力计算的三步流程,确保理解每个步骤的输入、输出和数学形式。
  • 尝试修改上一节中的注意力计算示例,将点积匹配函数替换为其他形式(如计算余弦相似度),并观察权重分布的变化。
  • 思考:在聊天机器人或文本摘要任务中,注意力机制可以帮助模型关注用户输入或原文的哪些关键部分?

—— 小象教研组