02-大模型智能体开发工程师:Transformer架构核心原理

系列文章导航:AI系列文章导航目录-持续更新中

第02课:Transformer架构核心原理

📝 本文摘要:本文从RNN的局限性出发,详解Transformer的核心架构:输入表示层(Token Embedding + 位置编码)、Transformer Block(多头自注意力 + FFN + 残差连接 + 层归一化)、输出层;重点拆解了Self-Attention计算过程、多头注意力、FFN的"升维-激活-降维"本质(模式匹配+知识检索)、MoE演进、以及Encoder-only vs Decoder-only的分野和KV Cache加速原理。
你不需要能手推公式,但你必须能回答:Transformer为什么能工作?注意力机制在干什么?为什么它能取代RNN?如果面试官问你,你能讲清楚。这一课会把Transformer从头到脚拆开给你看。


一、Transformer之前的世界:为什么需要革命

1.1 RNN(Recurrent Neural Network,循环神经网络)

复制代码
输入:  x₁  →  x₂  →  x₃  →  ...  →  xₙ
         ↓       ↓       ↓              ↓
隐状态: h₁  →  h₂  →  h₃  →  ...  →  hₙ

怎么工作的:RNN像一个"有记忆的管道"------每读入一个词,它就把当前词和上一步的"记忆"(隐状态)融合,产生新的记忆,再传给下一步。

核心问题

  • 串行计算:h₂依赖h₁,h₃依赖h₂......一步接一步,无法并行,训练慢
  • 长距离遗忘:信息从x₁传到hₙ要经过n次变换,每变换一次信息就"稀释"一点,长序列中早期信息几乎丢失

打个比方:就像传话游戏,第1个人说的话传到第20个人,信息早就走样了。

1.2 LSTM(Long Short-Term Memory,长短期记忆网络)/ GRU(Gated Recurrent Unit,门控循环单元)

对RNN的改进,通过"门控"机制(可以理解为"阀门"------决定什么信息该记住、什么该忘记)缓解长距离遗忘,但串行计算的根本问题没解决

演进逻辑:RNN → LSTM/GRU,是在"串行处理"框架内做改良,但改良有上限。

1.3 Transformer的颠覆(2017)

核心创新:彻底抛弃递归结构(不再一步一步传信息),用**自注意力(Self-Attention,自注意力)**直接建模任意两个位置之间的关系------不管隔多远,一步直达。

  • ✅ 完全并行计算 → GPU(Graphics Processing Unit,图形处理单元/显卡)友好 → 可大规模训练
  • ✅ 任意距离直接关联 → 没有长距离遗忘
  • ✅ 计算复杂度与序列长度的关系从O(n)串行变为O(n²)并行(代价是注意力计算本身是O(n²),但可并行)

演进逻辑:RNN的串行传话 → LSTM的"选择性传话" → Transformer的"不需要传话,大家直接对话"。


二、Transformer架构全景:先看森林,再看树木

在拆解每个组件之前,你需要先理解Transformer的整体设计哲学:

2.1 核心设计哲学

复制代码
Transformer的本质 = 信息重组机器

输入:一串Token(词/子词)
处理:让每个Token和所有其他Token"对话",汇聚信息,再独立加工
输出:每个Token的新表示,包含了全局上下文信息

为什么要这样设计:因为语言的本质是"关系"------"苹果"这个词的意义取决于它周围的词(是水果还是手机?)。注意力机制就是让每个词去"看"周围所有的词,决定谁对自己最重要。

2.2 整体架构一览

复制代码
输入 Token序列
    ↓
┌─────────────────────────────────────────┐
│  1. 输入表示层                            │
│     ├─ Token Embedding(词嵌入)          │  Token → 向量
│     └─ Positional Encoding(位置编码)    │  注入位置信息
│                                          │  ⭐ 为什么需要:自注意力本身  │
│                                          │  没有顺序概念,必须显式告知   │
└──────────────────┬──────────────────────┘
                   ↓
┌─────────────────────────────────────────┐
│  2. Transformer Block ×N(堆叠N层)      │
│  ┌─────────────────────────────────────┐│
│  │ Multi-Head Self-Attention           ││  ⭐ 核心:全局关系建模
│  │ (多头自注意力)                      ││  让每个Token与所有Token对话  │
│  └───────┬─────────────────────────────┘│
│          ↓ Add & Norm(残差连接+归一化)  │  稳定训练,保留原始信息     │
│  ┌─────────────────────────────────────┐│
│  │ Feed-Forward Network (FFN)          ││  ⭐ 逐位置非线性变换        │
│  │ (前馈网络/前馈层)                    ││  对汇聚来的信息做加工       │
│  └───────┬─────────────────────────────┘│
│          ↓ Add & Norm(残差连接+归一化)  │  稳定训练,保留原始信息     │
└──────────────────┬──────────────────────┘
                   ↓ (重复N次)
┌─────────────────────────────────────────┐
│  3. 输出层                               │
│     ├─ Decoder: 自回归生成下一个Token     │
│     └─ Encoder: 取[CLS]或平均做分类      │
└─────────────────────────────────────────┘

三层结构的类比

  • 输入表示层 = 翻译官:把文字翻译成模型能理解的"数字语言",并标注"这是第几个词"
  • Transformer Block = 信息交流会+加工厂:先让所有信息交流(注意力),再各自加工(FFN)
  • 输出层 = 决策者:根据加工后的信息做最终输出

三、核心组件逐个拆解

3.1 Token Embedding(词嵌入/Token嵌入)

做什么:把离散的Token映射为连续的向量。

复制代码
"hello" → [0.23, -0.15, 0.87, ...]  (维度d_model,如4096)

为什么:神经网络只能处理数值向量,不能处理文本。Embedding就像是给每个词一个"身份证号"------不过这个号不是一个数字,而是一个高维向量,相似的词向量也相近("猫"和"狗"的向量比"猫"和"汽车"更近)。

3.2 Positional Encoding(位置编码)

问题:Self-Attention本身没有顺序概念------"猫吃鱼"和"鱼吃猫"在注意力看来是一样的(因为词和词之间的关系权重可能恰好相同)。模型必须知道每个词在句子中的位置。

解法:给每个位置加一个独特的位置向量,让模型知道"这个词在第几个位置"。

复制代码
原始Transformer用正弦/余弦函数(Sinusoidal Positional Encoding):
PE(pos, 2i)   = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

pos = 位置序号(第几个Token)
i   = 维度序号(向量的第几个分量)
d_model = 嵌入维度

现代模型常用:
- 可学习的位置编码(Learned Positional Encoding):直接让模型学位置向量
- RoPE(Rotary Position Embedding,旋转位置编码):目前主流,通过旋转矩阵编码相对位置
- ALiBi(Attention with Linear Biases,线性偏置注意力):给注意力分数加距离惩罚

演进逻辑:正弦余弦(固定公式)→ 可学习(更灵活)→ RoPE(相对位置,更优雅)→ ALiBi(更简单,外推性好)

3.3 Self-Attention(自注意力机制)⭐核心中的核心

直觉:每个Token都要"看"序列中所有其他Token,决定自己应该关注谁、关注多少。

类比:想象你在一个聚会里,你想知道谁跟你最相关。你会扫一眼所有人,根据他们的话题、表情、和你关系的远近,分配你的注意力------最相关的多看几眼,不相关的忽略。Self-Attention就是让每个Token做这件事。

计算过程

复制代码
1. 对每个Token的向量x,通过三个不同的权重矩阵生成三个向量:
   Q (Query,查询)  = X · W_Q   → "我在找什么"(我在聚会里找什么话题)
   K (Key,键)      = X · W_K   → "我能提供什么"(我能聊什么话题)
   V (Value,值)    = X · W_V   → "我实际的内容"(我真正能说的内容)

2. 计算注意力分数(Attention Score,注意力得分):
   对每个Query,和所有Key做点积,衡量"匹配程度"
   
   score(i,j) = Q_i · K_j   (第i个Token的Query与第j个Token的Key的点积)

3. 归一化得到注意力权重(Attention Weight):
   Attention(Q,K,V) = softmax(Q·Kᵀ / √d_k) · V
                      ─────────────────────
                      │              │
                  "谁和我相关"    "取他们的内容"
                  
   softmax把分数变成概率分布(所有权重之和=1)
   除以√d_k是防止点积值过大导致softmax梯度消失

4. 用权重对Value加权求和,得到最终输出:
   output_i = Σ_j weight(i,j) × V_j

具体例子

复制代码
句子:"猫 坐 在 垫子 上"

当计算"垫子"的注意力时:
  Q_垫子 分别与 K_猫、K_坐、K_在、K_垫子、K_上 做点积
  → 得到注意力权重,比如:猫0.1, 坐0.5, 在0.1, 垫子0.2, 上0.1
  → 最终"垫子"的表示 = 0.1×V_猫 + 0.5×V_坐 + 0.1×V_在 + 0.2×V_垫子 + 0.1×V_上

模型自己学会了:"垫子"和"坐"关系最密切(因为坐在垫子上是最常见的搭配)

为什么除以√d_k:当Q和K的维度d_k很大时,点积的值可能非常大(因为每个分量相乘再求和),这会导致softmax函数的梯度消失(softmax对极端大的值不敏感,输出接近one-hot)。除以√d_k相当于对点积做缩放,让softmax的输入值域合理,梯度正常。

3.4 Multi-Head Attention(多头注意力)

思想:一个注意力头只能关注一种关系模式,多头让模型同时关注多种不同的关系。

类比:就像你用不同的"视角"看同一场聚会------

  • 视角1(语法视角):关注主谓关系

  • 视角2(语义视角):关注同义词

  • 视角3(指代视角):关注代词和它指代的人/物

  • 更多功能由更多的头承担

    原始向量 X
    ├─ Head 1: Q₁=X·W_Q1, K₁=X·W_K1, V₁=X·W_V1 → Attention → output₁
    ├─ Head 2: Q₂=X·W_Q2, K₂=X·W_K2, V₂=X·W_V2 → Attention → output₂
    ├─ Head 3: Q₃=X·W_Q3, K₃=X·W_K3, V₃=X·W_V3 → Attention → output₃
    └─ ...

    最终拼接(Concatenate)所有头的输出,再做一次线性变换(Linear Projection)得到最终输出。

为什么不直接用一个大的注意力:多头提供了多样性------每个头的权重矩阵不同,学到的关系模式不同。实验证明多头比单头效果好,尤其在长序列和复杂语义任务上。

3.5 Feed-Forward Network(FFN,前馈网络/前馈层)⭐让小白也能理解

先说人话:如果说注意力层是"信息交流会"------让每个Token去听其他Token说什么;那么FFN就是"独立加工车间"------每个Token拿到交流来的信息后,关起门来自己消化、加工、提炼。

FFN到底在做什么

复制代码
FFN(x) = activation(x · W₁ + b₁) · W₂ + b₂

步骤1: x · W₁ + b₁   →  把向量从d_model维映射到d_hidden维(通常d_hidden = 4 × d_model)
步骤2: activation()   →  非线性激活函数(ReLU或GELU)
步骤3: · W₂ + b₂      →  再从d_hidden维映射回d_model维

"升维再降维"到底有什么意义? 用一个生活类比来理解:

复制代码
想象你是一个学生(维度d_model = 你的知识面)

步骤1: 升维(d_model → 4×d_model)
  相当于:你进入图书馆,把知识展开到更大的空间
  原本你知道"猫"是一个概念,现在展开为:猫的种类、猫的习性、猫的历史...
  在高维空间中,原本"挤在一起"的概念被拉开了,更容易区分

步骤2: 非线性激活(GELU/ReLU)
  相当于:你选择了性地关注某些展开的知识,忽略不相关的
  GELU函数的特点:接近0的值被严重压制,大值几乎保留
  → 这是一种"筛选",只有显著的特征能通过

步骤3: 降维(4×d_model → d_model)
  相当于:你从图书馆回来,把展开的知识重新压缩回你的知识面
  但这次压缩后的知识已经不同了------因为你在高维空间做了筛选和组合

更深的理解:FFN是模型的"知识存储器"

近年来的研究表明(如Anthropic的字典学习/Dictionary Learning工作),FFN的真正角色可能是一个Key-Value记忆系统

复制代码
FFN第一层(W₁): 学习Key------"这是什么类型的信息?"
FFN第二层(W₂): 学习Value------"对应的输出应该是什么?"

举个例子:
  输入包含"法国"的概念
  → W₁匹配到"国家/首都"这个Key
  → W₂输出"巴黎"这个Value

所以FFN本质上是在做:模式匹配 → 知识检索

现代FFN的演进

复制代码
原始FFN: 线性→激活→线性(2层,简单但容量有限)
  ↓
GLU变体(Gated Linear Unit,门控线性单元): 加入门控机制,效果更好
  SwiGLU: activation((x·W₁) ⊙ (x·W_G)) · W₂   (⊙是逐元素乘法)
  ↓
MoE(Mixture of Experts,混合专家): 把一个大FFN拆成多个小FFN(专家)
  每次只激活部分专家,总参数量大但推理成本低
  代表:Mixtral 8x7B、DeepSeek-V3(256路由专家+1共享专家)

一句话总结FFN:注意力层负责"问对人",FFN负责"想明白"。

3.6 残差连接(Residual Connection)& 层归一化(Layer Normalization)

复制代码
output = LayerNorm(x + Sublayer(x))
         ─────────  ────────────────
         归一化       残差连接:原始输入直接"跳连"到输出

残差连接:让信息可以"跳过"某些层直接传递。为什么需要?深层网络容易出现梯度消失(梯度在层层传递中越来越小,最终无法更新底层参数),残差连接提供了一个"快捷通道",确保梯度能回流到网络底层。

类比:就像高速公路旁边的应急通道------即使主路堵了(梯度消失),信息还能通过应急通道传递。

层归一化:对每个样本的特征做标准化(减均值、除标准差),稳定训练过程,加速收敛。与Batch Normalization(批归一化)不同,Layer Normalization不依赖batch内的其他样本,更适合变长序列和单条推理。

Pre-Norm vs Post-Norm

  • Post-Norm(原始Transformer):LayerNorm(x + Sublayer(x)),先加残差再归一化。训练不稳定,需要学习率warmup(预热)
  • Pre-Norm(现代主流):x + LayerNorm(Sublayer(x)),先归一化再加残差。训练更稳定,不需要warmup,目前几乎所有大模型都用Pre-Norm

演进逻辑:Post-Norm → Pre-Norm,是从"理论上更优雅"到"实践中更稳定"的调整。


四、Encoder vs Decoder:两条路线的分野

原始Transformer(2017)是Encoder-Decoder结构(用于机器翻译),后来分化为两条路线。理解这个分野,才能理解今天大模型的格局。

4.1 Encoder-only(BERT路线,2018-)

复制代码
输入 → [Encoder Blocks] → 输出
      双向注意力(每个Token看前后所有Token)

擅长 :理解任务------分类、检索、匹配、语义相似度
不适合 :生成任务(因为没有学过"如何一个接一个地生成")
代表:BERT(Bidirectional Encoder Representations from Transformers,来自Transformers的双向编码器表示)

为什么BERT火了一阵又不火了:因为"理解"最终要服务于"生成"。能理解一句话但不能生成回答,应用场景受限。

4.2 Decoder-only(GPT路线,2018-)⭐当前绝对主流

复制代码
输入 → [Decoder Blocks] → 输出
      因果注意力(Causal Attention,每个Token只能看之前的Token,不能看未来)

擅长 :生成任务------对话、续写、推理、代码生成、工具调用
关键:自回归生成(Autoregressive Generation)------每次只预测下一个Token

复制代码
输入:    "今天天气"
预测:    "很"
输入:    "今天天气很"
预测:    "好"
输入:    "今天天气很好"
预测:    "。"

为什么Decoder-only成为主流

  1. 生成是更通用的能力------理解可以看作"生成答案"的特例(给你一段文本,让你分类,本质上也是在"生成"分类标签)
  2. 训练更简单------不需要Encoder-Decoder之间的对齐,只需预测下一个Token
  3. 规模化效果更好------涌现能力(Emergent Abilities)主要在Decoder-only模型上观察到,因为生成任务迫使模型学会更深层的规律

代表:GPT系列(GPT-1/2/3/4/4o)、LLaMA系列、DeepSeek系列、Qwen系列、Mistral/Mixtral系列、Claude系列------几乎所有你能叫出名字的大模型都是Decoder-only。

4.3 Encoder-Decoder(T5/BART路线)

现在主要用于特定任务(翻译、摘要),不是主流大模型架构。T5(Text-to-Text Transfer Transformer,文本到文本迁移Transformer)把所有NLP任务统一为"文本到文本"格式。

演进逻辑:Encoder-Decoder(翻译专用)→ Encoder-only(理解任务)→ Decoder-only(通用生成+理解)→ 一统天下


五、从Transformer到大模型的关键技术

5.1 Scaling Laws(缩放定律/规模定律)

Kaplan et al. (2020) 发现

复制代码
模型性能 ≈ f(参数量N, 数据量D, 计算量C)

关键结论:
1. 更大的模型 + 更多的数据 = 更好的性能(近似幂律关系,Power Law)
2. 三个因素中,计算量是根本约束(你的GPU算力是有限的)
3. 最优方案:在固定计算预算下,训练更大的模型,但只走较少的步数

Chinchilla定律(Hoffmann et al., 2022)修正:之前大家倾向于"大模型+少数据"(比如GPT-3用300B token训练175B参数模型),实际上"适度模型+足够数据"更优------Chinchilla用70B参数+1.4T token训练,效果超过GPT-3。这就是为什么Llama用更多数据训练更小模型效果反而好。

对开发者的意义:选模型不是"越大越好",要根据你的计算预算和任务需求选最合适的规模。

5.2 MoE(Mixture of Experts,混合专家模型)

复制代码
传统Transformer: 每个Token过同一个FFN
MoE Transformer: 每个Token只激活部分FFN(专家)

          Router(路由器,决定Token走哪些专家)
         /  |  \
    Expert1 Expert2 Expert3 Expert4
        ↑              ↑
    Token A只走      Token B只走
    Expert1,Expert3  Expert2,Expert4

优势 :总参数量大(能力强),但每次推理只激活一部分(速度快、成本低)。
代表

  • Mixtral 8x7B:8个专家,每次激活2个
  • DeepSeek-V3:256个路由专家+1个共享专家,每次激活8个(总参数671B,激活37B)

演进逻辑:Dense FFN(所有Token过同一个FFN,简单但贵)→ MoE(稀疏激活,大模型低成本的关键技术)


六、推理过程详解(你的代码实际在做什么)

当你在代码里调用model.generate()或者client.chat.completions.create()时,底层发生了什么?

6.1 完整推理流程

复制代码
用户输入: "1+1等于几?"
         ↓
┌─ Step 1: Tokenization(分词)─────────────────────────────┐
│  文本 → Token ID序列                                        │
│  "1+1等于几?" → [16, 62, 16, 118, 72, ...]                │
│                                                            │
│  ⭐ 为什么要分词:模型不认识"文字",只认识"数字ID"             │
│  ⭐ 分词方式影响效率:BPE(Byte-Pair Encoding,字节对编码)  │
│    是最常用的子词分词方法,能把罕见词拆成常见子词              │
└────────────────────────────────────────────────────────────┘
         ↓
┌─ Step 2: Embedding(嵌入)────────────────────────────────┐
│  每个Token ID → d_model维向量                               │
│  加上位置编码(Positional Encoding)                         │
│                                                            │
│  ⭐ 此时的向量:包含"这个词是什么"和"它在第几个位置"两重信息 │
└────────────────────────────────────────────────────────────┘
         ↓
┌─ Step 3: Transformer Layers ×N ──────────────────────────┐
│  每一层做三件事:                                           │
│  1. Causal Self-Attention(因果自注意力)                   │
│     每个Token只能看到自己和之前的Token                       │
│     用注意力权重从前文"汇聚"相关信息                         │
│  2. FFN(前馈网络)                                         │
│     对汇聚来的信息做非线性加工                               │
│     本质:模式匹配 + 知识检索                               │
│  3. 残差连接 + 层归一化                                     │
│     稳定训练,保留原始信息                                   │
│                                                            │
│  ⭐ 每经过一层,Token的表示就更"丰富"一层                    │
│  浅层学到语法,深层学到语义,更深层学到任务逻辑              │
└────────────────────────────────────────────────────────────┘
         ↓
┌─ Step 4: Output Head(输出头)────────────────────────────┐
│  最后一层向量 → 词表大小的logits(未归一化的概率分数)       │
│  logits → softmax → 概率分布                                │
│  采样策略(Sampling Strategy):                              │
│    - Greedy(贪心):直接选概率最大的Token                  │
│    - Top-k:从概率最高的k个Token中按概率采样                │
│    - Top-p(Nucleus Sampling,核采样):选累积概率不超过p    │
│      的最小Token集合,从中采样                              │
│    - Temperature:控制分布的"尖锐程度"                      │
│      T↓更确定(趋向贪心),T↑更多样(趋向均匀)             │
│                                                            │
│  → "2"(或"等于"、"二"等,取决于概率最高的Token)           │
└────────────────────────────────────────────────────────────┘
         ↓
┌─ Step 5: 自回归重复 ─────────────────────────────────────┐
│  把预测出的Token加入输入序列,继续预测下一个Token            │
│  直到遇到<EOS>(End of Sentence,句末结束符)               │
│  或达到最大长度(max_tokens)                               │
│                                                            │
│  "1+1等于几?" → "2" → "<EOS>" → 停止                      │
│  "请写一首诗" → "春" → "风" → "吹" → ... → "<EOS>" → 停止  │
└────────────────────────────────────────────────────────────┘

6.2 KV Cache:推理加速的关键

自回归生成的特点:每一步预测新的Token时,之前所有Token的Embedding、Attention计算结果其实不变------但朴素实现会重新计算,非常浪费。

KV Cache的思路

复制代码
第1步: 输入 [T1] → 计算 T1的K和V → 缓存K1,V1 → 预测 T2
第2步: 输入 [T1,T2] → T1的K,V不变,只需算T2的K,V → 缓存K2,V2 → 预测 T3
...
第n步: 输入 [T1,...,Tn] → 只需算Tn的K,V → 预测 Tn+1

没有KV Cache: 每步 O(n²) 的注意力计算量
有KV Cache:   每步只有 O(n) 的新计算量(算新Token的K,V + 和所有缓存的K做注意力)

这就是为什么大模型推理的主要瓶颈是显存(存放KV Cache)而不是算力。长上下文(128K/1M token)需要极大的KV Cache,这也是各种压缩KV Cache的技术(如PagedAttention、MQA/GQA)被研发的原因。


七、面试高频问题速答

Q: Transformer为什么比RNN好?

A: 两个核心优势------并行计算和无长距离遗忘。RNN串行处理限制训练速度(无法利用GPU并行),长序列信息衰减严重(传话游戏效应);Transformer用自注意力直接建模任意距离关系(一步直达,不需逐步传递),且完全可并行(每个位置的注意力计算独立)。

Q: 注意力机制的复杂度?

A: 时间复杂度O(n²d),n是序列长度,d是维度。每个Token要和所有Token做点积。这是Transformer的主要瓶颈,也是长上下文研究的核心问题。各种线性注意力(Linear Attention,用核函数近似,降到O(nd))、稀疏注意力(Sparse Attention,只计算部分位置对,如Longformer的滑动窗口+全局Token)方案在尝试解决。

Q: 为什么Decoder-only是主流?

A: 三个原因------生成能力更通用(理解是生成的特例)、训练更简单(只需预测下一个Token,不需Encoder-Decoder对齐)、规模化效果更好(涌现能力主要在Decoder-only上观察到)。

Q: KV Cache是什么?为什么重要?

A: 自回归生成时,已生成Token的Key和Value向量不变,可以缓存下来避免重复计算。这是推理加速的关键技术。没有KV Cache,每次生成都要重新计算所有历史Token的注意力,复杂度从O(n)变成O(n²)。KV Cache的代价是占用显存,所以推理的主要瓶颈是显存而非算力。

Q: FFN在Transformer中起什么作用?

A: 注意力层负责"信息汇聚"(每个Token从前文收集相关信息),FFN负责"信息加工"(对汇聚来的信息做非线性变换,提取更高级特征)。近年研究发现FFN本质上是一个Key-Value记忆系统------第一层做模式匹配(这是什么类型的信息?),第二层做知识检索(对应的输出应该是什么?)。这也是MoE技术的基础:把一个大FFN拆成多个专家,每次只激活部分。


📝 作业

作业1:用代码实现一个简单的自注意力

用Python/NumPy实现Scaled Dot-Product Attention(缩放点积注意力),输入3×4的矩阵,输出注意力结果。

参考答案

python 复制代码
import numpy as np

def scaled_dot_product_attention(Q, K, V):
    """
    Scaled Dot-Product Attention(缩放点积注意力)
    
    Q: (seq_len_q, d_k) --- Query矩阵,seq_len_q个查询,每个d_k维
    K: (seq_len_k, d_k) --- Key矩阵,seq_len_k个键,每个d_k维
    V: (seq_len_k, d_v) --- Value矩阵,seq_len_k个值,每个d_v维
    
    返回: (seq_len_q, d_v) --- 注意力输出
          (seq_len_q, seq_len_k) --- 注意力权重
    """
    d_k = Q.shape[-1]  # Key的维度
    
    # Step 1: 计算注意力分数(Q和K的点积)
    # 每个Query和每个Key做点积,衡量"匹配程度"
    scores = Q @ K.T  # (seq_len_q, seq_len_k)
    
    # Step 2: 缩放(除以√d_k)
    # 防止点积值过大导致softmax梯度消失
    scores = scores / np.sqrt(d_k)
    
    # Step 3: softmax归一化
    # 把分数变成概率分布(每一行的权重之和为1)
    def softmax(x):
        # 减去最大值防止数值溢出
        exp_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
        return exp_x / exp_x.sum(axis=-1, keepdims=True)
    
    attention_weights = softmax(scores)  # (seq_len_q, seq_len_k)
    
    # Step 4: 用权重对Value加权求和
    # 每个输出位置 = 所有Value的加权平均
    output = attention_weights @ V  # (seq_len_q, d_v)
    
    return output, attention_weights


# 测试
np.random.seed(42)
Q = np.random.randn(3, 4)  # 3个Token的Query,维度4
K = np.random.randn(3, 4)  # 3个Token的Key,维度4
V = np.random.randn(3, 6)  # 3个Token的Value,维度6(d_v可以≠d_k)

output, weights = scaled_dot_product_attention(Q, K, V)
print("注意力权重:\n", weights)
print("输出:\n", output)

# 验证:每一行的权重之和应该为1
print("每行权重和:", weights.sum(axis=1))

作业2:画图理解因果注意力掩码

在纸上画一个5×5矩阵,用因果掩码(Causal Mask,下三角为0,上三角为-∞)标注,解释为什么Decoder不能看到未来Token。

参考答案

复制代码
因果掩码矩阵(5个Token的注意力掩码):

        Token1  Token2  Token3  Token4  Token5
Token1 [  0,    -∞,    -∞,    -∞,    -∞  ]
Token2 [  0,     0,    -∞,    -∞,    -∞  ]
Token3 [  0,     0,     0,    -∞,    -∞  ]
Token4 [  0,     0,     0,     0,    -∞  ]
Token5 [  0,     0,     0,     0,     0  ]

0 表示可以关注(softmax后为正值)
-∞ 表示不可关注(softmax后为0,即权重为0)

效果:
- Token1只能看自己
- Token2可以看Token1和自己
- Token3可以看Token1、Token2和自己
- Token5可以看所有之前的Token和自己
- 没有任何Token能看到"未来"的Token

这就是"因果"(Causal)的含义:生成第i个Token时,只能基于前i-1个Token的信息,
不能"偷看"第i个之后的信息(因为那些还没生成)。

在代码中实现:
mask = np.triu(np.ones((5, 5)), k=1) * (-np.inf)  # 上三角设为-∞
attention_scores = softmax(Q @ K.T / sqrt(d_k) + mask)  # 加上掩码

作业3:理解FFN的"升维-激活-降维"

用NumPy实现一个简单的FFN,观察升维和降维对数据的影响。

参考答案

python 复制代码
import numpy as np

def ffn(x, W1, b1, W2, b2):
    """
    Feed-Forward Network(前馈网络)
    
    x: (batch, d_model) --- 输入
    W1: (d_model, d_hidden) --- 第一层权重,d_hidden通常=4×d_model
    b1: (d_hidden,) --- 第一层偏置
    W2: (d_hidden, d_model) --- 第二层权重
    b2: (d_model,) --- 第二层偏置
    """
    # Step 1: 升维 + 非线性激活
    # 把d_model维向量映射到d_hidden维高维空间
    # GELU激活函数:接近0的值被压制,大值保留------起到"筛选"作用
    hidden = gelu(x @ W1 + b1)  # (batch, d_hidden)
    
    # Step 2: 降维回来
    # 从d_hidden维映射回d_model维
    output = hidden @ W2 + b2  # (batch, d_model)
    
    return output

def gelu(x):
    """
    GELU(Gaussian Error Linear Unit,高斯误差线性单元)
    公式: x * Φ(x),其中Φ是标准正态分布的累积分布函数
    
    特点:
    - x > 0 时,输出接近 x(保留)
    - x < 0 时,输出接近 0(压制)
    - x ≈ 0 时,输出很小(压制)
    
    相比ReLU(负数直接变0),GELU更平滑,训练更稳定。
    """
    return x * 0.5 * (1 + np.vectorize(math.erf)(x / np.sqrt(2)))

import math

# 测试
np.random.seed(42)
d_model = 4
d_hidden = 16  # 4×d_model

x = np.random.randn(1, d_model)  # 一个输入向量
W1 = np.random.randn(d_model, d_hidden)
b1 = np.random.randn(d_hidden)
W2 = np.random.randn(d_hidden, d_model)
b2 = np.random.randn(d_model)

result = ffn(x, W1, b1, W2, b2)
print("输入:", x)
print("FFN输出:", result)

# 观察升维后的中间结果
hidden = gelu(x @ W1 + b1)
print("升维后(d_hidden={}维):".format(d_hidden), hidden)
print("升维后非零元素数:", np.count_nonzero(hidden))

下一篇文章见:AI系列文章导航目录-持续更新中

相关推荐
stereohomology6 小时前
Antigravity cli 体验很差
大语言模型·agent·cli·antigravity
LB21126 小时前
消灭并发重复调用:基于 Agent 调用 LLM 的分布式 Single-Flight 实战
java·开发语言·redis·分布式·agent
小脑斧1236 小时前
提示词极简艺术:用最少 Token,榨干 LLM 极限输出能力
llm·提示词·特征工程·ai提示词
俊哥V6 小时前
每日 AI 研究简报 · 2026-05-23
人工智能·ai
udc小白6 小时前
EXCEL实现MLP实例
人工智能·深度学习·神经网络·机器学习
码农阿强6 小时前
Omni-Flash引擎及组件库技术解析与中转站接入实践
人工智能·ai·aigc·ai编程·ai写作·gpu算力
porschev6 小时前
Hermes Edu Skills 从 170 到 188:一次中文教育 Agent Skill Pack 的工程化升级
agent·ai agent·ai教育·openclaw·hermes agent skills
白日梦想家L_6 小时前
Claude Code 的 Hooks、Slash Command 与自动化
ai·ai编程
武雄(小星Ai)7 小时前
AI CLI 三巨头横评:Claude Code vs Codex CLI vs Gemini CLI(2026实测)
人工智能·aigc·agent