【Transformer 深度解析】自注意力机制(Self-Attention)
1. 引言本系列文章基于Transformer 深度解析 | 菜鸟教程创作对其中晦涩难懂的点进行了梳理。Transformer 是 2017 年 Google 提出的序列建模架构《Attention Is All You Need》其的出现彻底改变了AI领域今天的 GPT、Claude、Gemini、Llama……几乎所有主流大语言模型本质上都是 Transformer 的变体。理解了 Transformer就理解了现代 LLM 的 90%。实际上Transformer 最开始设计出来的目的只是将机器翻译做得更好、更快。在此之前我们通常使用 RNN 等专门处理序列数据的神经网络来实现机器翻译而 Transformer 用「自注意力机制」彻底替代了 RNN/CNN解决了长序列依赖难学、无法并行训练的老大难问题。要理解 Transformer我们可以从 RNN 入手。2. RNN循环神经网络2.1 原理解释RNN 是一类专门处理序列数据的神经网络它的核心特点是当前时刻的输出不仅取决于当前输入还依赖于之前所有时刻的“记忆”。在 RNN 之前还有 CNN、MLP 等普通的前馈神经网络这些神经网络有个隐含假设输入之间相互独立。但实际世界里大量数据是有顺序、有关联的场景序列关系自然语言“我 吃 了 苹果”时间序列股价、气温依赖历史走势语音/音频当前帧的声音靠前后帧才听得懂视频帧与帧之间有动作连续性前馈网络处理不了这种“上下文”RNN 就是为解决这个问题而生的其核心思想就是循环迭代。即将历史输入压缩成一个隐性记忆每次处理新的输入时都带上上一时刻的隐性记忆RNN 每一时刻 t 的计算逻辑如下符号含义时刻 t 的输入时刻 t 的隐藏状态记忆上一时刻传下来的记忆时刻 t 的输出、、输入权重、隐藏状态权重、输出权重所有时刻共享实际上RNN 很像一个状态机隐性记忆的作用就是保存其当前状态。不过与状态机不同的是每次移动RNN 的状态信息都会有一定程度的丢失。2.2 优缺点相比较于普通的前馈神经网络RNN 的优点在于天然建模时序依赖参数共享不随序列长度膨胀输入长度灵活。然而RNN 也有其局限性长期依赖问题早期记忆随着循环迭代计算的进行被不断压缩直到对输出的影响忽略不计。LSTM 和 GRU 缓解了这个问题但没有彻底解决。无法并行计算必须一步步往后推训练慢。梯度消失/梯度爆炸RNN 是链式结构每一步的梯度要反向传播回第一步。多次相乘后梯度会指数级衰减每次都乘1的数或指数级暴涨每次都乘1的数。3. 自注意力机制3.1 原理解释我们可以理解为RNN 实际上是在尝试让各部分输入之间产生联系能够结合上下文来丰富或明确当前输入的实际含义。例如我们在翻译“小明是个运动达人他平时很爱打篮球”时结合隐性记忆我们能知道 “他” 这个词的实际含义是 “小明” 。自注意力是 Transformer 的核心。它的思想很简单并行计算每个词向量与其他词之间的关联度对于每个词将其他词的向量按与其的关联度加权求和形成具有上下文语义的新向量使用这个新向量代替原本的向量参与后续计算。这个计算过程就相当于把 “小明是个运动达人他平时很爱打篮球” 预处理成了 “小明是个运动达人小明爱打篮球的运动达人平时很爱打篮球”。当然「小明爱打篮球的运动达人」只是举个例子其实际表示加权求和了句子当中其他词向量的新向量。这样一来我们就可以并行地独立处理所有词了这在根源上解决了 RNN 的缺陷。问题在于词与词之间的关联度如何计算呢当然是靠训练模型来实现在 Transformer 架构中训练模型的参数包括三个权重矩阵、、。将每个词的向量X分别点乘这三个矩阵可得到三个参数角色计算方式干的事解释QQuery查询我想找啥用于查询的向量KKey键我身上贴啥标签方便被搜给别人查询的向量VValue值搜到了之后实际抄走的内容实际参与加权求和的向量计算过程是每个词用Q去和所有词的K做匹配点乘得到注意力分数关联度用 Softmax 把分数归一化总和为 1得到权重百分比用归一化后的分数加权求和所有词的V按权重求和得到新向量注意力分数计算公式看到这里你肯定有很多疑问这是正常的我们将会逐步解答你可能会问的问题。3.2 流程详解3.2.1 V的作用Q 和 K 的作用我们很好理解用于计算注意力分数。但是为什么要计算一个 V 出来呢用 token 的原始向量进行加权求和不可以吗《Attention Is All You Need》中其实并没有说明为什么设计 QKV 三个向量。博主能想到的唯一的作用就是将原始向量的维度转换为与Q、K统一但这一设计到底有什么优势可能要在后续的学习中才能体现出来。如果有大佬明确知道原因还请在评论区不吝赐教。3.2.2 注意力分数公式注意力分数的计算公式为除以看上去并不必要为什么这里要加上这个操作呢这是一个重要的细节。假设 Q 和 K 的每个元素都是均值 0、方差 1 的随机变量那么它们的点积的方差是。如果很大比如 512点积的值会很大导致 Softmax 进入饱和区公式涉及指数大值会引发指数爆炸——梯度几乎为 0训练不动。除以后方差变回 1Softmax 的梯度就健康了。缩放点积是 Transformer 训练稳定的关键技巧之一。3.2.3 Softmax归一化对于每个向量按照归一化的定义向量转换为权重我们可以理解这个公式的含义。但是问题在于为什么要使用 e 指数的形式来计算权重呢按直觉来说我们会得出这样的公式在这个公式的基础上引入 e 主要有三个原因负数问题可能为负数计算结果不符合“权重”的语义且分母的计算结果可能为0z [-1, 2, -0.5] sum 0.5 p1 -2.0 ← 负权重 p2 4.0 ← 超过1了 p3 -1.0线性归一化没有“放大重要项”的能力原始值线性归一化指数归一化1, 2, 31/6, 2/6, 3/6 0.17, 0.33, 0.502.7,7.4,20.1 →0.09, 0.25, 0.66平移不变性梯度好算而且和交叉熵绝配这个之后再说。为什么选择 e而不选择其他指数底呢实际上这里完全可以用其他指数底例如:之所以使用 e主要有如下的这些原因求导最省事链式法则不额外乘系数信息论/统计里 log 和 e 天然一对玻尔兹曼分布来的历史惯性4. 代码示例# # 纯 Python 实现自注意力Self-Attention # import math def softmax(x: list) - list: 计算 Softmax把一组数变成概率分布总和为 1 # 减去最大值防止数值溢出 max_val max(x) exp_x [math.exp(v - max_val) for v in x] sum_exp sum(exp_x) return [v / sum_exp for v in exp_x] def matrix_multiply(A: list, B: list) - list: 矩阵乘法A 是 m×nB 是 n×p输出 m×p m len(A) n len(B) p len(B[0]) result [[0.0 for _ in range(p)] for _ in range(m)] for i in range(m): for j in range(p): for k in range(n): result[i][j] A[i][k] * B[k][j] return result def transpose(matrix: list) - list: 矩阵转置 return list(map(list, zip(*matrix))) class SelfAttention: 纯 Python 实现的自注意力层 def __init__(self, d_model: int, d_k: int): 初始化自注意力层 d_model: 输入/输出维度 d_k: Q/K 的维度通常 d_k d_model / num_heads import random random.seed(42) # runoob固定随机种子 self.d_model d_model self.d_k d_k # 初始化 Q, K, V 的投影矩阵 # 实际中这些是训练出来的 self.Wq [[random.normalvariate(0, 0.1) for _ in range(d_k)] for _ in range(d_model)] self.Wk [[random.normalvariate(0, 0.1) for _ in range(d_k)] for _ in range(d_model)] self.Wv [[random.normalvariate(0, 0.1) for _ in range(d_k)] for _ in range(d_model)] def forward(self, X: list) - list: 自注意力前向传播 X: 输入序列形状 [seq_len, d_model] 返回: 输出序列形状 [seq_len, d_k] seq_len len(X) # 步骤 1计算 Q, K, V # Q X·Wq, K X·Wk, V X·Wv Q matrix_multiply(X, self.Wq) K matrix_multiply(X, self.Wk) V matrix_multiply(X, self.Wv) print( Q 的形状:, len(Q), ×, len(Q[0])) print( K 的形状:, len(K), ×, len(K[0])) print( V 的形状:, len(V), ×, len(V[0])) # 步骤 2计算注意力分数 Q·Kᵀ K_transposed transpose(K) scores matrix_multiply(Q, K_transposed) print(\n 原始注意力分数:) for i, row in enumerate(scores): print(f 第 {i} 行: {[f{v:.4f} for v in row]}) # 步骤 3缩放除以 √d_k # 这是为了防止 Softmax 进入梯度饱和区 scale_factor math.sqrt(self.d_k) scores_scaled [[v / scale_factor for v in row] for row in scores] print(\n 缩放后的注意力分数:) for i, row in enumerate(scores_scaled): print(f 第 {i} 行: {[f{v:.4f} for v in row]}) # 步骤 4Softmax 归一化 attention_weights [] for row in scores_scaled: attention_weights.append(softmax(row)) print(\n 注意力权重Softmax 后:) for i, row in enumerate(attention_weights): print(f 第 {i} 行: {[f{v:.4f} for v in row]}) # 步骤 5加权求和 V output matrix_multiply(attention_weights, V) return output, attention_weights # # 演示用一个简单例子运行自注意力 # # 假设我们有一个 4 词的句子每个词用 8 维向量表示 # 为了演示我们用 one-hot 小随机噪声 d_model 8 d_k 6 # 输入序列4 个词每个 8 维 X [ [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], # 词 0 [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], # 词 1 [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], # 词 2 [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], # 词 3 ] print( * 50) print(Runoob 自注意力演示) print( * 50) print(f输入序列长度: {len(X)}) print(f输入维度: {len(X[0])}) print() attn SelfAttention(d_modeld_model, d_kd_k) output, attention_weights attn.forward(X) print(\n * 50) print(自注意力输出:) print( * 50) for i, row in enumerate(output): print(f词 {i} 的输出: {[f{v:.4f} for v in row]})下一篇【Transformer 深度解析】多头注意力Multi-Head Attention-CSDN博客