- Sequence Transduction Model 序列转导模型
- 序列数据:具有顺序关系的数据,每个元素的顺序对于数据的整体含义非常重要 ,例如自然语言文本,数据的先后位置是有意义的
- 序列转导任务:文本翻译、文本生成、语音转文字
- Transformer 之前主流的序列转导模型结构
- 基于 RNN / CNN
- 使用 encoder - decoder 的结构
- 使用注意力机制增强
- Transformer 结构的创新
- 完全抛弃 RNN / CNN
- 仍然使用 encoder - decoder 结构
- 完全基于注意力机制
- 为什么经典的 FNN 不适合序列转导任务
- FNN 能够接收的输入是一个固定长度的向量,但是序列转导任务的输入长度不固定
- 只能通过 平均(将向量中的数值按位置相加取平均值)或者 拼接(将长度拼接成需要的输入长度)
- 但是平均会丢失词语的顺序
- 拼接则需要一个非常长的输入层,处理效率也很低,简单的拼接仍会将整个句子作为一个整体来处理,无法理解正真的 谁先谁后 的位置关系
- 为了解决 FNN 不适合序列转导任务的问题,提出了 RNN,RNN 模拟类人说话的过程,一个词一个词输入
- 能够建模词序:按时间顺序(token 顺序)逐个输入
- 能够建模上下文依赖:逐个输入词语,并且有 记忆 机制
- 支持不定长的输入
- 本质是上一个词输入后产生的结果会分为两个部分,一个是 y 即模型的输出,另一个是 h 即时间步的状态, h 会成为下一个词输入的一部分,如此迭代到最后
- 但是RNN 仍然存在输入和输出需要同等长的问题,为了解决此问题,诞生了 encoder - decoder 结构,本质是将 RNN 的上下拆开为 encoder 和 decoder
- encoder - decoder
- 核心思想:将处理输入 和 产生输出 分给了 encoder 和 decoder 来分别执行
- encoder
- 只处理输入,得到处理结果随后传入解码器
- decoder
- 接收 encoder 的输出,给出最终的输出
- decoder 中 每个输出 以及 时间状态s0 都会作为下一个时间步的输入,如此迭代得到所有时间步的最终输出
- c = 最后一个时间步的隐藏状态输出 h4 , c 包含整个输入的上下文语意信息以及位置信息
- 还有一种 decoder 的结构是为了防止 向量 c 被遗忘, 在decoder的每个时间步中重新输入 上下文向量 c
- encoder - decoder 的问题是处理长序列时会存在 遗忘
- 由于上下文向量 c 直接引用 encoder 中最后一个 token的输出,此时如果序列过长,最开始的token信息可能被遗忘,例如在h0开始,c取h100的值是,h0的值经过多轮迭代已经被遗忘了
- encoder - decoder 忽略了不同时间步对当前时刻输出的重要性可能存在差异,因为每一个时间步都使用同一个上下文向量 C
- 例如对于 decoder中的第一次输出理论来说应该是 h1 的重要性更高
- 为了让模型能关注到上下文中不同的重点,引入了 Attention Mechanism(属于 交叉注意力 Cross-Attention) 来变化上下文向量C
- 在decoder中让每一个不同时间步的上下文向量C,对 encoder中不同时间步的状态 H 有不同的权重(相关的词语权重提升)
- 例如 C0 对 h1 权重较高,C1 对 h2的权重较高
- 让 Decoder 中的输出观察到了输入的重点,也就是注意力机制
- 这样能够解决处理长序列时的遗忘问题,并且解决了不同时间步状态对当前状态输出的重要性权重不同
- 此时还存在一个效率上的问题:无法并行计算,计算是递归、串行的
- 想要得到 h4 就必须计算出 h1、h2、h3 , 想要得到 courses 就必须要先计算出 I、love、easy
- 注意力机制能捕捉序列中任意位置的依赖关系,而不依赖于递归计算和顺序性,但是绝大多数情况下,它仅仅作为一种增强,仍然和RNN一起使用,而Transformer则直接抛弃了RNN
- 为解决递归计算问题,一些变体模型引入了CNN来支持并行,但这些模型会导致难以捕捉远距离的信息(原来的长距离遗忘问题回来了)而Transformer注意力机制解决了这些问题
- Transformer是首个完全基于自注意力机制,不适用RNN或CNN的序列建模模型
- 自注意力和传统注意力机制的区别
- 传统的注意力机制,以翻译任务为例,“你好”和"hello",其表现为输出的英文应该怎样去关注输入的中文,也就是传统的注意力是一种输出面向输入的关注,是由输出驱动的,决定从输入中取什么
- 而自注意力则是输入对于内部词与词之间关系的关注 ,输入内部互相解释,丰富彼此的语义,从而经过QKV矩阵得到一个语义更加丰富的输入矩阵
- QKV全都是来自自身
- Transformer中会将每个词转换为 token 然后以全局的视角根据整个上下文的信息同时为每个 token 进行编码,跟RNN不同的是,它不会根据时序来判断 token 的位置,因此需要一个位置编码 ,最后得到包含上下文信息以及位置的矩阵
- 在进行模型训练时decoder预测时 也不需要递归计算能够同时掩码不同位置同时计算多个不同位置的预测值,因为此时已经达到了答案,可以并行的输出结果进行预测,但是在部署后的实际任务时,由于没有答案输出这是一个一个 token 进行输出
- 首先将输入的词通过 embedding 矩阵 转换为 token(512 向量)
- 这种并行化的处理过程没有办法像RNN一样使用时序带入顺序信息,需要一个显示地 位置编码 给每一个词都生成一个 512 维 的位置编码向量
- 实现方法
- 经典正弦/余弦位置编码(Vaswani et al., 2017)
- 用不同频率的正弦、余弦函数为每个维度生成周期信号,位置越大,相位越大;不同维度对应不同波长
- 可学习的绝对位置嵌入(Learned Absolute PE)
- 像词表一样,为每个位置 pos 直接学一个向量 p_pos,训练中和词向量相加
- 相对位置的几种主流做法
- Shaw 相对位置嵌入(Relative Position Embeddings)
- 在注意力分数里加入与相对距离 r=i−j 对应的嵌入项,常用“距离截断/桶化”
- T5 相对位置偏置(Relative Position Bias, RPB)
- 将相对距离分桶,再为每个桶学一个标量偏置 b_bucket(i−j),直接加到注意力 logits
- 不再“相加”位置,而是对 Q/K 的每对相邻维度做与位置相关的二维旋转
- 直观理解
- 每个位置对应一组角度(多频率),用这些角度去旋转 Q/K
- 两个 token 的相对位置就是这些角度的差
- 注意力分数因此主要由“角度差(=相对位移)”决定,同时受内容影响
- 结果:模型可以利用“相隔 X 个位置(含方向)”这一线索来建模关系
- 绝对位置的缺点
- 模型需要自己学习相对关系
- 超出训练长度就失效(训练512长度,推理1024就崩)
- 位置信息可能在深层网络中被"稀释”
- 旋转位置编码的优点
- 模型直接"看到"相对距离,学习更高效
- 训练2048长度 → 推理可扩展到8192甚至更长(这对大模型处理长文本至关重要!)
- 零参数开销
- 直接作用在Attention(
传统:embedding层加位置 → 后续层可能遗忘RoPE:每次算attention都重新应用 → 位置信息不丢失) - 为什么位置编码不能简单的使用 1,2,3 编号,早期的 BERT 就是这样做的,但是这种编码信息量太少,模型无法理解单词之间的距离关系,特别是在注意力机制中,注意力机制都是靠向量计算(例如点积)而单个数字在向量空间离几乎没有办法建立复杂的关系,因此需要把位置编码使用一个高纬度的向量来表示,来存储更多的信息
RoPE 旋转位置编码(Rotary Positional Embedding)
对比维度 | 正弦/余弦编码(Sinusoidal) | 可学习位置编码(Learned Absolute) | RoPE(旋转位置编码) |
编码方式 | 固定数学公式 PE = sin/cos(pos/10000^(2i/d)) | 可训练参数矩阵 nn.Embedding(max_len, dim) | 旋转矩阵变换 Rotate(x, pos×θ) |
位置类型 | 绝对位置 | 绝对位置 | 相对位置 |
作用位置 | 输入层相加 | 输入层相加 | Attention层的Q/K |
参数量 | ✅ 0 | ❌ max_len × dim | ✅ 0 |
长度外推 | ✅ 理论可外推 ⚠️ 实际效果一般 | ❌ 完全无法外推 | ✅✅ 优秀外推能力 |
相对位置感知 | ⚠️ 间接(需学习) | ⚠️ 间接(需学习) | ✅ 直接编码 |
训练速度 | ⚡ 快(无需学习) | 🐢 慢(需学习位置) | ⚡ 快(无需学习) |
位置信息保持 | ⚠️ 深层可能衰减 | ⚠️ 深层可能衰减 | ✅ 每层都重新应用 |
长文本性能 | 📊 中等 | 📉 差 | 📈 优秀 |
可解释性 | ✅ 高(数学公式明确) | ❌ 低(黑盒参数) | ✅ 高(几何意义清晰) |
- 把每个 token 生成好的位置编码直接通过相加的方式叠加到相应 token 的向量中
- 将每个 token 乘以 Q K V(512 * 512) 三个权重矩阵,编码上下文信息,含义相近的词语在向量空间中越接近(512 维度代表,512个特征)
q_proj:Query 投影矩阵 [分成多头]- q 会发出查询 然后根据 k 来判断是否符合查询
k_proj:Key 投影矩阵 [分成多头]- 一个标签
v_proj:Value 投影矩阵 [分成多头]o_proj:Output 投影矩阵(多头注意力输出后的线性变换)- 决定“怎么把多头信息拼起来”。把各个头的结果重新混合、旋转回模型公共空间,允许头与头之间线性组合与信息对齐。如果没有 o_proj,就只是“拼接”;有了它,模型能学到“如何利用各头的互补能力”
- 计算每个 Token 的 Attention:应用缩放点积注意力公式,然后进行缩放,最后应用 softmax 函数输出概率分布(用Q 发出查询问题,根据 K 来判断是否符合)
- 缩放点积注意力:用 Q 和 K 做点积得到一个权重(相似度分数)
- 点积后的结果值越大代表越匹配
- Attention:当前 Token 对于当前 K 矩阵(整个句子)语义信息的重要程度
- 每个 Token Attention 和 V 矩阵做矩阵乘法(加权平均)得到包含 上下文语义信息 的向量
- 将得到的 上下文语义向量拼接起来,再经过线性层,得到最终输出
- 单头自意力机制权重矩阵大小变化(以 4个 token输入为例)
- 进过 embedding → 4 * 512 → 添加位置编码 → 4 * 512 → 乘以 QKV 矩阵 → QKV都为 4 * 512 → 计算Attention ( 4*512, 512*4) → 4 * 4 → Scale * softmax → 4 * 4 → 与V做矩阵乘法 (4*4, 4*512) → 4*512
- 多头自注意力机制权重矩阵大小变化(以 4个 token输入为例)
- 进过 embedding → 4 * 512 → 添加位置编码 → 4 * 512 → 乘以 QKV 矩阵 → QKV都为 4 * 512 → 将Q、K、V分成8个头(Reshape 阶段 [序列长度, 头数, 每个头的维度]) → 4 * 8 * 64 → 将Q、K、V分成8个头(Transpose 将头数放在第一个维度) → 8 * 4 * 64 → 计算 Attention ([8, 4, 64] x [8, 64, 4] ) → 8 * 4 * 4 → Scale & softmax → 8 * 4 * 4 → 与V做矩阵乘法([8, 4, 4] × [8, 4, 64]) → [8, 4, 64] → 拼接所有头(Transpose 阶段) → 4 * 8 * 64 → Reshape → 4 * 512 → 输出线性变换(与权重矩阵 O 做矩阵乘法 [4, 512] × [512, 512] ) → [4, 512]
- 然后残差链接和归一化层
- 就是把原始输入x加到变换后的F(x)上
- F(x) + x
- 残差链接的作用
- 缓解梯度消失:提供梯度的"高速公路",让梯度可以直接流向更深的层
- 保持原始信息:确保输入信息不会在层层变换中完全丢失
- 特征融合:将原始特征与变换后的特征结合,保留多层次信息
- 稳定训练:使深层网络的训练更加稳定,避免梯度爆炸或消失(尤其是CNN中)
- 归一化层的作用
- 标准化后是标准正态分布(均值0,方差1)
- 控制数值范围,防止数值爆炸
- 稳定梯度流动,使训练更稳定
- 加速收敛,提高训练效率
- 然后进入 FeedForward 前馈神经网络,然后再一次进入残差链接和归一化,得到 encoder 最终的输出
- 如果只有Attention机制,模型主要在做"信息聚合",而FNN提供了对聚合后信息的"深度处理"能力(类似CNN的全连接层)
- 结构为 线性层 → ReLU激活 → 线性层
- 第一层通常会扩展维度(如512 → 2048)
- 第二层再压缩回原维度(2048 → 512)
- FNN的作用
- Multi-Head Attention本质上是线性变换的组合,FNN引入非线性激活函数,增强模型的表达能力
- 为对注意力机制提取的特征做进一步的"提炼"和"整合”
- 中间层的维度扩展(通常是4倍)提供了更大的参数空间,让模型能学习更复杂的模式
- 现在的模型很多使用 MLP Multi-Layer Perceptron 多层感知机制,而不是一个简单的 神经网络
- SwiGLU
- SwiGLU = Swish + Gated Linear Unit (门控线性单元) 两个部分组成
- GLU (Gated Linear Unit)
- 这是一个通用的“门控”框架。它的核心思想是:将输入信息兵分两路,一路作为“内容”,另一路经过一个激活函数后变成“门”,然后用“门”来控制“内容”有多少可以通过。就像水龙头一样,门决定了水流的大小
- Swish
- 这是一个激活函数,具体公式是
Swish(x)=x⋅σ(x),其中 σ 是 Sigmoid 函数。它被用在 GLU 框架中,充当生成“门”的激活函数
- decoder 中同样先将 答案 通过 embedding 转换为 token,然后添加位置编码,然后进入掩码多头注意力层
- 确保模型正确性:防止模型关注无效的padding位置
- 提高训练效率:避免无用计算和梯度更新
- 支持批处理:使得不同长度的序列可以一起处理
- 保证输出质量:确保模型输出不受padding影响(防止模型偷看答案)
- 进入残差链接和归一化,然后进入 交叉注意力层,然后进过残差链接和归一化
- 自注意力层主要是自己关注自己,QKV全部都是来自于自身
- 这里的多 交叉意力是 非自注意力 模块
- Q来自于decoder的自注意力层的输出(decoder自注意力层输入为答案), KV 是来自于 Encoder的输出
- 以 中翻英的例子为例 (翻译我爱水课 为 I love easy courses)
- 假设现在要预测 love 后要生成什么,首先会通过掩码遮盖 easy 和 courses,生成这个位置的时候 Q 就是 I love上下文向量的信息 KV是 我爱水课输入encoder之后输出的结果
- Part 1: Teacher Forcing 输入准备
- Part 2: 第一层 - Masked Self-Attention
- 应用因果掩码(Causal Mask)
- Part 3: 第二层 - Cross-Attention(交叉注意力)
# 中译英例子:"我爱水课" → "I love easy courses" 训练目标(完整): ["I", "love", "easy", "courses", "<end>"] Decoder实际输入: ["<start>", "I", "love", "easy", "courses"] ↑ 右移操作:去掉<end>,开头加<start>
原始注意力矩阵 (5×5): <START> I love easy courses <START> ✓ × × × × ← <start> 只能看自己 I ✓ ✓ × × × ← I 只能看 <start> 和自己 love ✓ ✓ ✓ × × ← love 能看前3个 easy ✓ ✓ ✓ ✓ × ← easy 能看前4个 courses ✓ ✓ ✓ ✓ ✓ ← courses 能看所有 ✗ 的位置 → 设为 -∞ → softmax后变成0 # 输出: 一个 5×512 的矩阵,每一行都只包含了"不看未来"的上下文信息 Attention = softmax(Masked_scores) × V # 5×512
- 再进过前馈神经网络 FNN 和残差链接 和归一化 得到最终输出
- 此时的形状还是 [token数量 , 512] 然后将这些向量传入 Linear 线性层 进行维度变换
- 将 512 维度的向量 映射到 非常大的 词汇表向量中,每个位置代表相印词汇的得分
- 最后进入 Softmax 将 词汇得分转换为概率分布
- 为什么多头注意力机制需要从512降维到64
- token 的 512 个维度代表 512个特征,以食物为例
- 第一个头可能抽取出了所有和口味相关的 64 个特征得到一个口味语义子空间,而第二个头可能抽取了64个和热量相关的特征只关注热量,第三个头可能是营养
- 因此每一个头都在自己的一个小的语义子空间内进行计算
- 在单头注意力中始终都是 512 维在参与计算
- 而多头注意力会将 512 维 映射成8组64维 再进行计算再拼接融合,所以每一个头关注的重点信息是不同的
- 从宏观上来讲,头1可能关注语法关系,头2可能关注语义相似性,头3可能关注位置关系,这样就能让模型学到更详细更丰富的表示
- 8组 [ 4 , 512 ] 直接拼起来也是 4 * 512 为什么还需要一个线性层
- 直接拼接相当于一个固定的、受限的变换,而线性层是完全可学习的变换
- 拼接只是把各个头的结果并排放在一起,彼此互不“交流” ,线性层把这些头的输出重新投影并“搅拌”在一起,让模型能在同一表示空间里融合、重权重、旋转各头的信息
- 为什么不能直接用 8 个 512 维度的矩阵
- 性能和计算量的之间的均衡
- 自注意力机制维度变化详解
- 单头自注意力机制(4个token,embedding维度512)
- 多头自注意力机制(4个token,embedding维度512,8个注意力头)
步骤 | 操作 | 维度变化 | 说明 |
1️⃣ 预处理 | ㅤ | ㅤ | ㅤ |
1.1 | Embedding | → [4, 512] | 将token转换为稠密向量表示 |
1.2 | 位置编码 | [4, 512] → [4, 512] | 添加位置信息,保持维度不变 |
2️⃣ 线性变换 | ㅤ | ㅤ | ㅤ |
2.1 | 生成Q | [4, 512] × W_Q[512, 512] → [4, 512] | 查询矩阵 |
2.2 | 生成K | [4, 512] × W_K[512, 512] → [4, 512] | 键矩阵 |
2.3 | 生成V | [4, 512] × W_V[512, 512] → [4, 512] | 值矩阵 |
3️⃣ 注意力计算 | ㅤ | ㅤ | ㅤ |
3.1 | 计算QK^T | [4, 512] × [512, 4] → [4, 4] | 计算注意力分数矩阵 |
3.2 | Scale | [4, 4] / √512 → [4, 4] | 缩放,防止梯度消失 |
3.3 | Softmax | [4, 4] → [4, 4] | 归一化注意力权重 |
4️⃣ 输出生成 | ㅤ | ㅤ | ㅤ |
4.1 | 与V相乘 | [4, 4] × [4, 512] → [4, 512] | 加权求和,得到最终输出 |
步骤 | 操作 | 维度变化 | 说明 |
1️⃣ 预处理 | ㅤ | ㅤ | ㅤ |
1.1 | Embedding | → [4, 512] | 词嵌入 |
1.2 | 位置编码 | [4, 512] → [4, 512] | 添加位置信息 |
1.3 | 线性变换 | [4, 512] → Q,K,V: [4, 512] | 生成查询、键、值矩阵 |
2️⃣ 多头分割 | ㅤ | ㅤ | ㅤ |
2.1 | Reshape | [4, 512] → [4, 8, 64] | 512 = 8头 × 64维/头 |
2.2 | Transpose | [4, 8, 64] → [8, 4, 64] | 头维度提前,便于并行计算 |
3️⃣ 注意力计算 | ㅤ | ㅤ | ㅤ |
3.1 | 计算QK^T | [8, 4, 64] × [8, 64, 4] → [8, 4, 4] | 8个头并行计算 |
3.2 | Scale & Softmax | [8, 4, 4] → [8, 4, 4] | 缩放因子: 1/√64 |
3.3 | 与V相乘 | [8, 4, 4] × [8, 4, 64] → [8, 4, 64] | 加权求和 |
4️⃣ 多头合并 | ㅤ | ㅤ | ㅤ |
4.1 | Transpose | [8, 4, 64] → [4, 8, 64] | 恢复序列维度在前 |
4.2 | Reshape | [4, 8, 64] → [4, 512] | 拼接所有头 |
4.3 | 输出投影 | [4, 512] × W_O[512, 512] → [4, 512] | 最终线性变换 |
- Why Self-Attention
- self-Attention layer 每一层的计算总复杂度低于 RNN CNN
- 具有并行计算能力
- 模型内部学习长距离依赖
