Transformer 原理与实现(一):从 Attention 到编码解码机制
“用毒毒毒蛇毒蛇会不会被毒毒死”如果让计算机理解这句话它会遇到一个核心问题每一个“毒”到底指什么谁和谁有关系这正是 Transformer 要解决的问题。一、Transformer 的本质Transformer 本质是让每个词都能“看全句”并决定该关注谁。传统模型RNN是这样理解句子的我 → 爱 → 你一步一步传而 Transformer 是所有词之间一次性建立关系这就是它强大的根本原因。二、编码器真实流程是这样的句子 → 分词token → token id数字 → embedding向量 → 加位置编码 → Transformer 输入1.分词使用分词工具分词器(Tokenizer)/分词器(Tokenizer)将句子拆分成最小的语义单位 TOKEN每个token编码成一个512位的向量。------------------------------------------- | 用毒毒毒蛇毒蛇会不会被毒毒死 | | Input Sentence | ------------------------------------------- ↓ ------------------------------------------------------------- | [用] [毒] [毒] [毒蛇] [毒蛇] [会] [不] [会] [被] [毒] [毒死] | | Tokenizer Output | ------------------------------------------------------------- ↓ ---------------------------------------------------------------- | ┌───────────┐ ┌───────────┐ ┌───────────┐ ... | | │ 512-dim │ │ 512-dim │ │ 512-dim │ | | │ vector │ │ vector │ │ vector │ | | └───────────┘ └───────────┘ └───────────┘ | ----------------------------------------------------------------2.位置编码解决“顺序问题”模型无法判断向量顺序所以需要给每个词向量加上一个位置编码表示他们在句子中的顺序。这样我们就得到形状是10*512的词向量组词向量组X10*512。本质就是给每个词加一个“坐标”让模型知道谁在前谁在后。--------------------------------- | 用毒毒毒蛇毒蛇会不会被毒毒死 | | Input Sentence | --------------------------------- ↓ --------------------------------------------------- | Tokens: [t₁] [t₂] [t₃] ... [t₁₀] | --------------------------------------------------- ↓ --------------------------------------------------- | Word Embedding | | [ 512d ] [ 512d ] ... [ 512d ] | --------------------------------------------------- --------------------------------------------------- | Position Encoding | | [ 512d ] [ 512d ] ... [ 512d ] | --------------------------------------------------- ↓ --------------------------------------------------- | Final Input X | | Shape: 10 × 512 | ---------------------------------------------------3.自注意力机制核(核心到现在为止我们得知道模型如何找到他们的联系让他们相互进行计算。模型首先用三个权重矩阵QKV,和每一个词向量相乘这个每个token就会有三个向量。Q查询向量代表当前这个词想关注什么K表示键向量代表它能够为其他词提供什么信息标签/索引V代表值向量代表这个词包含的实际内容信息┌─────────────────────────────┐ │ X (10 × 512) │ └─────────────────────────────┘ │ │ │ ┌─────┘ │ └─────┐ ↓ ↓ ↓ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Wq │ │ Wk │ │ Wv │ │ 512×512 │ │ 512×512 │ │ 512×512 │ └─────────┘ └─────────┘ └─────────┘ │ │ │ ↓ ↓ ↓ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Q 10×512 │ │ K 10×512 │ │ V 10×512 │ └─────────┘ └─────────┘ └─────────┘ │ │ │ ├──────────┼──────────┤ ↓ ↓ ↓ q₁ k₁ v₁ q₂ k₂ v₂ … q₁₀ k₁₀ v₁₀ (每个均为 512 维向量)模型用每一个词的Q向量分别和句子中所有词包括自己的K向量相乘进行点积计算得到一组注意力分数将他们压缩到0-1之间得到注意力权重代表当前这个词和每个词的关联程度[ 输入序列 X用 毒 毒 毒蛇 … 毒死 ] ↓ ┌────────────┬────────────┬────────────┐ │ Q │ K │ V │ │ 10×512 │ 10×512 │ 10×512 │ └────────────┴────────────┴────────────┘ ↑ ↑ 取一行 取出全部行 ┌──────────┐ │ q₁ “用” │ ← 查询向量 └──────────┘ × ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ │k₁ │k₂ │k₃ │k₄ │k₅ │k₆ │k₇ │k₈ │k₉ │k₁₀│ ← 所有键向量 └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ ↓ 点积注意力分数 [ s₁, s₂, s₃, s₄, ... s₁₀ ] ↓ Softmax 归一化 注意力权重 α ∈ [0,1]总和1 [ α₁, α₂, α₃, α₄, ... α₁₀ ]例如“用”这个字它和第一个“毒”字的权重就会更高。权重 [ 0.1, 0.3, 0.15, 0.08, ... ] 字 用 毒 毒 毒蛇 …… ↑ 这里“用”与第一个“毒”权重最高0.3这里算出来的权重本质就是当前词对句子里每个词的 “重视程度”。当然到这一步还是不够的因为注意力权重只是 “关注度排名”不是信息本身。要得到真正有意义的语义必须把 “关注度” × “实际内容”再混合起来。这一步只是告诉模型我看谁、不看谁谁更重要。所以必须下一步把每个词的内容V × 关注度权重 → 再加在一起大白话说就是权重表示了我应该更多的关注哪里而V向量则表示那里到底有什么东西。【第一步已经算好的注意力权重】 代表“用”对每个词的重视程度 权重[ 0.1, 0.3, 0.15, 0.08, ... ] 对应 用 毒 毒 毒蛇 毒死 【第二步每个权重 × 对应词的 V 向量】 ↓ ↓ ↓ ↓ V1×0.1 V2×0.3 V3×0.15 V4×0.08 ... 【第三步全部加起来 → 得到新向量】 V1×0.1 V2×0.3 V3×0.15 … ↓ 输出一个全新的 512 维向量 Output假如你是一个token当你有了疑问后带着疑问Q走进图书馆去和图书馆里每一本书的索引K匹配匹配最高的书说明它内容最可能解决你的问题那么你对他的关注度就会最高于是就会花更多时间读他的V反之关注度就低。相当于你对所有书的知识进行了整合。同意通过自注意力计算后每个token对其他token产生了抽象理解就存在经过加权的向量里。以上整个过程被称为自注意力机制Self-Attention4.多头注意力但是这个全新的向量对于语义理解还是不够深入的因为自注意力机制只能让一个词对其他词产生单一的关注和理解在现实中一个句子里的词语往往有多方面语义信息例如动宾关系代词和指代名词的关系语义角色情感修饰关系等。Transformer 的解决方式很简单粗暴直接开 8 个 “独立的小注意力”让它们各看各的。原本 512 维的 X → 拆成 8 个 64 维向量总维度不变8 × 64 512但切成8 份独立 subspace子空间每一份都做一套自己的 QKV、自己的注意力、自己的加权求和。所以可能就变成头 1专门学 “动作 vs 对象”头 2专门学 “逻辑关联”头 3专门学 “情感倾向”等它们互不干扰各学各的。8 个头各自算完每个头输出10×64再把它们拼起来64 64 … 64 8 次 512最终得到Z形状还是 10×512但是里面的信息完全不一样了它包含动作关系、修饰关系、逻辑关系、情感关系等等。--------------------------------------------------- | Final Input X | | Shape: 10 × 512 | --------------------------------------------------- ↓ ┌─────────────────────────────────────┐ │ Multi-Head Split │ │ 将 512 维均匀拆分为 8 组每组 64 维 │ └─────────────────────────────────────┘ Head 1 Head 2 Head 3 ... Head 8 10×64 10×64 10×64 ... 10×64每个头进行独立的自注意力计算┌─────────────────────┐ │ Head i: 10×64 │ └─────────────────────┘ ↙ ↓ ↘ ┌──────┐ ┌──────┐ ┌──────┐ │ Wq │ │ Wk │ │ Wv │ └──────┘ └──────┘ └──────┘ ↓ ↓ ↓ ┌──────┐ ┌──────┐ ┌──────┐ │ Q │ │ K │ │ V │ │ 10×64│ │ 10×64│ │ 10×64│ └──────┘ └──────┘ └──────┘ 取当前词 q ↓ 与所有 k 做点积 → 注意力分数 ↓ Softmax → 权重 ∈ [0,1]关联程度 ↓ 权重 × 对应 V 并加权求和 ↓ 单个头输出: 10×64最终8 个头结果拼接 → 输出 Z↓ ┌─────────────────────────────────────────────────────────────┐ │ Concat: Head1 Head2 ... Head8 │ │ 64 64 … 64 512 维 │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ Final Output Z │ │ Shape: 10×512 │ └─────────────────────────────────────┘总结来说的流程就是这样的句子 → 分词 → 词嵌入位置编码 → X(10×512) ↓ 拆分为 8 个 10×64 头 ↓ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │头1 │ │头2 │ │··· │ │头8 │ │QKV │ │QKV │ │QKV │ │QKV │ │注意力│ │注意力│ │注意力│ │注意力│ └─────┘ └─────┘ └─────┘ └─────┘ ↓ 8 个结果拼接 ↓ 输出 Z(10×512)5.多层结构但是到目前为止还是不够它处理一句话还OK但是如果是一段话就有点捉襟见肘。单层自注意力能做到让每个词知道“和谁相关”但问题是现实语言是“多层语义结构”的一句话中同时存在主谓关系、指代关系、语气/情感等多语义。因此模型通常设置多层结构来学习更深层次的语义信息。所以需要堆叠多层多头自注意力↓ ┌─────────────────────────────────────┐ │ Transformer Layer 1 │ │ ┌───────────────────────────────┐ │ │ │ Multi-Head Attention (8头) │ │ │ └───────────────────────────────┘ │ │ 捕捉近邻关系、动宾、修饰 │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ Transformer Layer 2 │ │ ┌───────────────────────────────┐ │ │ │ Multi-Head Attention (8头) │ │ │ └───────────────────────────────┘ │ │ 捕捉短语结构、句子内部逻辑 │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ Transformer Layer 3 │ │ ┌───────────────────────────────┐ │ │ │ Multi-Head Attention (8头) │ │ │ └───────────────────────────────┘ │ │ 捕捉长距离指代、段落级语义 │ └─────────────────────────────────────┘ ↓ … ↓ ┌─────────────────────────────────────┐ │ Transformer Layer N │ │ 逐层抽象形成深度语言理解 │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ 深度语义表示 Output │ │ Shape: 10×512 │ │ 已包含语法 逻辑 指代 意图 │ └─────────────────────────────────────┘6.残差连接当网络变深时会出现一个经典问题梯度消失Gradient Vanishing如果某一层学到了F(X) 0什么都不做那么后面的层几乎收不到梯度整个网络“只有浅层在工作”。深层网络 Layer 1 → Layer 2 → Layer 3 → ... → Layer N ↓ ↓ ↓ ↓ 有梯度 梯度变小 梯度几乎消失 完全没梯度 【问题】 后面的层无法更新等于“只有浅层在工作” 出现梯度消失 Gradient Vanishing为了解决这个问题需要引入残差连接Residual Connectionx x F(x)其实就是保留原始信息 增量学习原始理解x新学到的关系F(x)最终结果x F(x)在原有认知基础上不断修正而不是推翻重来。在残差之后会做一步归一化防止数值爆炸稳定训练过程加快收敛。上一层输出 x │ ▼ ┌─────────────────────────────┐ │ 多头自注意力 / FFN │ │ 新学习函数 F(x) │ └─────────────────────────────┘ │ ▼ ┌─────────────────────────────┐ │ 残差连接 x F(x) │ ← 关键公式 │ 保留原始信息 新增理解 │ └─────────────────────────────┘ │ ▼ ┌─────────────────────────────┐ │ LayerNorm 层归一化 │ │ 稳定数值、加速收敛、防溢出 │ └─────────────────────────────┘ │ ▼ 传入下一层放入多层堆叠中就是这样的输入句子 → 分词 → 词嵌入位置编码 → X (10×512) ┌─────────────────────────────────────┐ │ Transformer Layer 1 │ │ │ │ 多头自注意力 → F(x) │ │ ↓ │ │ x x F(x) 残差 │ │ ↓ │ │ LayerNorm 归一化 │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ Transformer Layer 2 │ │ 多头自注意力 → F(x) → 残差 → Norm │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ Transformer Layer 3 │ │ 多头自注意力 → F(x) → 残差 → Norm │ └─────────────────────────────────────┘ ↓ ... ↓ 深层语义表示 Output7.前馈网络在学习 Transformer 时会把注意力全部放在 Attention 上但其实如果没有 FFNTransformer 几乎无法表达复杂语义。所有的自注意力本质都是 “线性计算”只能画直线。加了 FFN 激活函数才能把直线掰弯变成曲线。只有曲线才能拟合世界上所有复杂的语言规律。FFN 是“逐 token 独立计算”的每个 token 单独过一遍 FFN没有 token 之间的交互。这意味着Attention 负责“信息流动”FFN 负责“信息变换”。FFN 是在对“已经融合好的信息”做特征重组。我们可以理解成Attention 输出这个词 它和其他词的关系而FFN则是把这些关系重新组合提炼出更高级语义。通过升维 非线性变换使模型能够表达复杂语义。它对每个词的向量单独做两层线性变换 中间夹一个激活函数就能把直线掰弯了。而在数学上有一个万能近似定理 —— 只要有非线性激活神经网络可以逼近任意复杂函数。输入 x │ ▼ ┌─────────────────────┐ │ 多头自注意力 │ └─────────────────────┘ │ ▼ ┌───────────┐ │ 残差 xF(x) │ └───────────┘ │ ▼ ┌───────────┐ │ LayerNorm │ └───────────┘ │ ▼ ┌─────────────────────┐ │ FFN 前馈网络 │ ← 这里加入非线性 │ 线性→激活→线性 │ └─────────────────────┘ │ ▼ ┌───────────┐ │ 残差 xF(x) │ └───────────┘ │ ▼ ┌───────────┐ │ LayerNorm │ └───────────┘ │ ▼ 下一层输入三、解码器如果说编码器Encoder负责理解输入句子那么解码器Decoder负责根据理解一步一步生成输出。解码器的核心目标就是在已知输入语义的基础上逐词预测下一个 token。1.解码器的两个输入解码器和编码器最大的不同在这里它会有两个输入输入1编码器输出 X_out对输入句子的理解输入2已经生成的 token 序列也就是说每生成一个词都要重新输入整个历史序列例如输入句子I love you 目标输出我 爱 你生成过程是第1步输入 BOS → 预测 “我” 第2步输入 BOS 我 → 预测 “爱” 第3步输入 BOS 我 爱 → 预测 “你”2.掩码多头自注意力Masked Self-Attention这和编码器中的多头自注意力区别就是多了一个 Mask掩码。因为在训练时,我们是直接把完整答案喂进去的如果不加 Mask模型可以偷看未来的答案。Mask 的作用就是屏蔽未来信息当前 token 只能看到它之前的 token。注意力分数矩阵未Mask 用 毒 毒 蛇 毒 死 用 ✅ ✅ ✅ ✅ ✅ ✅ 毒 ✅ ✅ ✅ ✅ ✅ ✅ 毒 ✅ ✅ ✅ ✅ ✅ ✅ 蛇 ✅ ✅ ✅ ✅ ✅ ✅ 毒 ✅ ✅ ✅ ✅ ✅ ✅ 死 ✅ ✅ ✅ ✅ ✅ ✅ 加 Mask 后解码器 用 毒 毒 蛇 毒 死 用 ✅ 只能看自己 毒 ✅ ✅ 只能看前面 毒 ✅ ✅ ✅ 蛇 ✅ ✅ ✅ ✅ 毒 ✅ ✅ ✅ ✅ ✅ 死 ✅ ✅ ✅ ✅ ✅ ✅和 Encoder 一样也要做残差连接 LayerNorm保证训练稳定输出A_mask已经融合“历史信息”的表示。解码器输入序列 ↓ ┌─────────────────────────────────────┐ │ 词嵌入 位置编码 │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ Masked Multi-Head Attention │ │ ┌───────────────────────────────┐ │ │ │ 多头自注意力计算 Q·Kᵀ │ │ │ └───────────────────────────────┘ │ │ ↓ │ │ ┌───────────────────────────────┐ │ │ │ Mask 掩码屏蔽未来 token │ │ │ │ 当前词只能看之前的历史信息 │ │ │ └───────────────────────────────┘ │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ 残差连接 x F(x) │ └─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────┐ │ LayerNorm 归一化 │ └─────────────────────────────────────┘ ↓ 输出A_mask仅含历史信息3.交叉注意力Cross Attention与自注意力机制相似唯一不同的是向量不和自己计算而是和编码器的输出向量X_OUT计算目的融合编码器理解的语义信息实现输入输出语义对齐。之后再进行再次残差 FFN做非线性理解最终输出Y_out解码器当前状态。交叉注意力的计算是解码器的 Q × 编码器的 K → 权重权重 × 编码器的 V → 融合语义假如输入句子用毒毒毒蛇毒蛇会不会被毒毒死编码器输出X_OUT已经理解了整句话的逻辑、动作、对象解码器在生成时每生成一个字都回头去查编码器理解好的内容确保生成的内容和原句意思一致不会跑题、不会乱编【来自编码器】 X_OUT (语义理解结果) │ ├─→ K └─→ V 【来自解码器】 A_mask (带历史信息的表示) │ └─→ Q ↓ ┌───────────────────────────────┐ │ Cross Attention 交叉注意力 │ │ │ │ 用解码器的 Q 查询编码器的 K、V │ │ 计算关联权重 → 加权融合信息 │ └───────────────────────────────┘ ↓ 残差连接 x F(x) ↓ LayerNorm 归一化 ↓ 送入后续 FFN 与下一层4.输出层从向量到单词模型内部算来算去都是512 维向量人看不懂。输出层就是把这个向量翻译成字典里的一个词。先把 512 维 → 映射到词典大小比如 3 万维再转成概率挑概率最大的那个词输出然后把新词塞回输入循环生成下一个直到出现结束符EOS停止数学过程1️⃣模型最后输出一个向量Y_out (10×512)2️⃣取当前要生成的那个词对应的向量3️⃣做线性变换logitsYoutWbW 形状512 → vocab_sizelogits 形状1 × vocab_size词典有多大这向量就多长4️⃣Softmax把 logits 压到 0~1总和 1→ 得到每个词的概率5️⃣Argmax挑概率最大的下标→ 下标对应词典里的一个汉字 / 词解码器最终输出 Y_out (512维向量) │ ▼ ┌─────────────────────────┐ │ 全连接线性层 │ │ logits Y_out·W b │ │ 512维 → 词典大小维 │ └─────────────────────────┘ │ ▼ ┌─────────────────────────┐ │ Softmax │ │ 转成 0~1 概率分布 │ └─────────────────────────┘ │ ▼ ┌─────────────────────────┐ │ Argmax │ │ 选概率最大的词 │ └─────────────────────────┘ │ ▼ 输出一个词如 “毒” autoregressive 循环 ↻ 把新词拼接到解码器输入末尾继续生成下一个 ↓ 重复直到输出 EOS 结束符四、本篇总结我们走完了Transformer的核心流程。1️⃣ 输入阶段分词Tokenizer → Token 序列 → Embedding词嵌入 Positional Encoding位置编码 → 得到输入向量 Xseq_len × d_model本质把语言转换为模型可以处理的向量表示2️⃣ 编码器Encoder理解语义每一层都在做三件事多头自注意力Multi-Head Attention → 建立 token 之间的关系残差连接 LayerNorm → 保证训练稳定前馈网络FFN → 做非线性变换提炼语义多层堆叠之后得到 X_out包含全局上下文信息的表示3️⃣ 解码器Decoder生成语言1掩码自注意力Masked Self-Attention保证生成是“自回归”的2交叉注意力Cross Attention生成时参考输入语义实现对齐3FFN 残差 Norm同编码器4️⃣ 输出阶段这是一个“预测下一个词”的分类问题512维向量→ Linear 映射到 vocab_size→ Softmax → 概率分布→ 选择下一个 token5️⃣ 自回归生成核心机制生成一个词 → 拼回输入 → 再预测下一个循环直到EOS结束符Transformer 的本质总结Transformer 并不是在“理解语言”而是在向量空间中建模 token 之间的关系。每一层都在做三件事Attention信息流动谁和谁有关FFN信息加工这些关系意味着什么Residual稳定训练防止学废与大模型的关系Encoder Decoder → 原始 Transformer翻译任务只用 Encoder → BERT理解任务只用 Decoder → GPT / LLaMA生成任务