AI 智能体开发 · Day 2 详细学习手册
主题 :Transformer 架构直觉理解------从"为什么需要 Attention"到"GPT 怎么生成文本"
总时长 :2-2.5 小时(可分两次完成)
难度 :中等偏概念(抽象内容多,代码少但重要)
产出 :能用自己的话解释 Attention + 1 个注意力可视化脚本 + 结构化笔记
前置条件 :完成 Day 1(理解 LLM 基本概念、环境已搭建、API 可调用)
建议:今天内容偏抽象,建议先看 3Blue1Brown 的 GPT 视频(15 分钟)再开始
目录
- 学习目标与知识地图
- [Part A:为什么需要 Attention(30 分钟)](#Part A:为什么需要 Attention(30 分钟))
- [Part B:Self-Attention 机制详解(40 分钟)](#Part B:Self-Attention 机制详解(40 分钟))
- [Part C:多头注意力与位置编码(25 分钟)](#Part C:多头注意力与位置编码(25 分钟))
- [Part D:Transformer 整体架构(20 分钟)](#Part D:Transformer 整体架构(20 分钟))
- [Part E:Encoder vs Decoder(15 分钟)](#Part E:Encoder vs Decoder(15 分钟))
- [Part F:代码实验------注意力可视化(20 分钟)](#Part F:代码实验——注意力可视化(20 分钟))
- 今日笔记模板
- 验收清单
- [常见问题 FAQ](#常见问题 FAQ)
- 扩展资源
- 明日预告
1. 学习目标与知识地图
你今天要达成的目标
概念层面(能用自己话讲清楚):
- RNN/LSTM 处理文本有什么问题,Attention 解决了什么
- Q、K、V 三个概念分别是什么,怎么协同工作
- 多头注意力为什么需要多个"头"
- 位置编码为什么需要(Attention 本身没有顺序概念)
- Transformer 的完整数据流(从文本到输出概率)
- GPT 为什么是 Decoder-only,和 BERT 有什么区别
操作层面(亲手做过):
- 用代码实现一个最简 Self-Attention
- 可视化注意力权重(看模型"关注"了哪些词)
- 验证 Attention 的无序性(打乱输入顺序看变化)
知识地图
Part A: 问题背景
├── RNN/LSTM 怎么处理文本
├── RNN 的三大缺陷
└── Attention 的核心思想
Part B: Self-Attention 机制
├── Q/K/V 的直觉理解
├── 逐步计算过程(手算级别)
├── 为什么用 Q·K 而不是直接比向量
└── Softmax 归一化的作用
Part C: 多头注意力 + 位置编码
├── 为什么需要多个头
├── 多头怎么并行
├── 位置编码解决的问题
└── 正弦/余弦位置编码
Part D: Transformer 架构
├── 完整数据流
├── Layer Normalization
├── Feed Forward Network
└── 残差连接
Part E: 架构对比
├── Encoder-only (BERT)
├── Decoder-only (GPT)
└── Encoder-Decoder (T5)
Part F: 代码实验
├── 最简 Self-Attention 实现
├── 注意力权重可视化
└── 输入顺序实验
2. Part A:为什么需要 Attention(30 分钟)
这部分纯概念,不需要电脑。理解"为什么"比理解"怎么算"更重要。
2.1 Transformer 之前:RNN 怎么工作(10 分钟)
在 2017 年 Transformer 出现之前,处理文本(翻译、摘要、分类)主要用 RNN (循环神经网络)和它的升级版 LSTM(长短期记忆网络)。
RNN 的工作方式------逐词处理:
输入句子: "The cat sat on the mat because it was tired"
RNN 逐词处理:
时刻 1: 读入 "The" → 隐状态 h1(记住了一些信息)
时刻 2: 读入 "cat" → h1 + "cat" → 隐状态 h2
时刻 3: 读入 "sat" → h2 + "sat" → 隐状态 h3
时刻 4: 读入 "on" → h3 + "on" → 隐状态 h4
时刻 5: 读入 "the" → h4 + "the" → 隐状态 h5
时刻 6: 读入 "mat" → h5 + "mat" → 隐状态 h6
时刻 7: 读入 "because" → h6 + "because" → 隐状态 h7
时刻 8: 读入 "it" → h7 + "it" → 隐状态 h8
↑
这时候 RNN 需要理解 "it" 指代谁
但 "cat" 的信息经过了 6 次传递,已经很模糊了
通俗比喻:
RNN 就像"传话游戏":
10 个人排成一排,第一个人看到一句话,
然后悄悄告诉第二个人,第二个人告诉第三个人...
传到最后一个人时:
- 最近的词记得清楚
- 最前面的词已经面目全非
这就是"信息衰减"问题
2.2 RNN 的三大致命缺陷(10 分钟)
缺陷 1:长距离依赖丢失
句子: "The animal that was walking through the forest and looking for food
near the river yesterday afternoon was a tiger"
问题: "was a tiger" 要和 "The animal" 建立联系
但中间隔了 15+ 个词
RNN 处理到 "tiger" 时,"animal" 的信息几乎丢失了
LSTM 用"门控机制"缓解了这个问题,但没根本解决
→ 长句子依然容易出错
→ 20 词以上的句子,LSTM 也开始力不从心
缺陷 2:无法并行计算
RNN 的处理是串行的:
必须先处理第 1 个词 → 才能处理第 2 个词 → 才能处理第 3 个词...
这意味着:
- 训练时无法利用 GPU 的并行能力
- 句子越长,训练越慢
- 1000 词的文档要串行处理 1000 步
对比 Transformer:
- 所有词同时处理(并行)
- GPU 利用率极高
- 这是 Transformer 能训练万亿参数的前提
缺陷 3:信息瓶颈
RNN 把整个句子的信息压缩到最后一个隐状态 h_n
"The cat sat on the mat" → h_6 = [0.3, -0.1, 0.8, ...]
一个固定大小的向量要编码整个句子的所有信息
句子越长,压缩损失越大
类比: 把一本 300 页的书压缩成一张 A4 纸的摘要
必然丢失大量细节
2.3 Attention 的核心思想(10 分钟)
Attention 不是某个人某天突然发明的,而是为了解决 RNN 的问题逐步演进而来。
核心思想一句话:处理每个词时,直接"看"句子中所有其他词,动态决定该关注哪些。
传统 RNN 处理 "it":
只能依赖前面传过来的、已经衰减的隐状态
"it" 指代 "cat" 还是 "mat"?------信息不够,猜不准
Attention 处理 "it":
"it" 直接看回句子中所有词:
"The" → 关注度 0.05(不太相关)
"cat" → 关注度 0.70(高度相关!"it" 可能指 cat)
"sat" → 关注度 0.05
"on" → 关注度 0.02
"the" → 关注度 0.03
"mat" → 关注度 0.10(也有可能指 mat)
"because"→ 关注度 0.02
"it" → 关注度 0.03
→ "it" 的新表示 = 所有词的加权求和(cat 权重最大)
→ "it" 现在包含了 "cat" 的语义信息
→ 指代消歧成功!
通俗比喻升级版:
你在参加一个酒会:
RNN 模式:
你只能听身边一个人说话
他告诉你之前发生了什么
但他的转述已经丢失了很多信息
Attention 模式:
你能同时感知整个房间所有人的对话
当别人提到"那个穿红衣服的人"时
你的注意力自动聚焦到红衣服的人身上
其他人变成背景噪音
→ 你直接获取了需要的信息,不需要别人转述
Attention 的三个关键特性:
| 特性 | 说明 | 好处 |
|---|---|---|
| 全局视野 | 每个词都能看到所有其他词 | 解决长距离依赖 |
| 动态权重 | 关注度根据内容自动计算 | 不同语境下关注不同部分 |
| 并行计算 | 所有词同时处理 | 训练速度大幅提升 |
自测:能回答下面两个问题就过关------
-
RNN 处理长文本的三个问题是什么?
长距离依赖丢失、无法并行、信息瓶颈
-
Attention 的核心思想是什么?用一句话说。
处理每个词时,直接看所有其他词,动态决定关注哪些。
3. Part B:Self-Attention 机制详解(40 分钟)
这是今天最重要也最抽象的部分。慢慢来,反复读。
3.1 Q/K/V 的直觉理解(15 分钟)
Self-Attention 的核心是三个矩阵变换:Q(Query)、K(Key)、V(Value)。
先不用管数学,用搜索引擎的类比理解:
你在用搜索引擎搜索信息:
1. 你输入搜索词 "Python 教程"
→ 这就是 Query (Q):你在"找"什么
2. 搜索引擎有海量的网页,每个网页有标题和关键词
→ 这些标题/关键词就是 Key (K):每个网页"能提供"什么
3. 搜索引擎用你的 Query 和每个网页的 Key 做匹配
→ 算出相关度分数(Attention 权重)
→ "Python 入门教程" → 相关度 0.9
→ "Python 爬虫实战" → 相关度 0.7
→ "Java 编程指南" → 相关度 0.05
4. 搜索引擎不是只返回最相关的那一个
而是按相关度加权返回所有网页的内容
→ 这些网页的正文内容就是 Value (V)
→ 最终结果 = 0.9×(Python入门) + 0.7×(Python爬虫) + 0.05×(Java) + ...
Self-Attention 中的 Q/K/V:
在 Self-Attention 中,Q、K、V 都来自同一个输入(所以叫"Self"):
输入: "我 喜欢 编程"
每个词通过三个不同的权重矩阵,生成三个不同的向量:
"我" → Q_我, K_我, V_我
"喜欢" → Q_喜欢, K_喜欢, V_喜欢
"编程" → Q_编程, K_编程, V_编程
处理 "编程" 这个词时:
1. 用 Q_编程("编程" 在找什么)去和每个 K 做匹配
- Q_编程 · K_我 = 0.1 ("我" 和 "编程" 关系不大)
- Q_编程 · K_喜欢 = 0.3 ("喜欢" 有点关系)
- Q_编程 · K_编程 = 0.6 (自己和自己最相关)
2. 用 Softmax 归一化(让权重加起来等于 1)
- 0.1 → 0.08
- 0.3 → 0.24
- 0.6 → 0.68
3. 用权重对 V 加权求和
- "编程"的新表示 = 0.08×V_我 + 0.24×V_喜欢 + 0.68×V_编程
- 这个新表示融合了整个句子的上下文信息
为什么需要三个不同的矩阵?
同一个词在不同角色下需要不同的表示:
"编程" 作为 Query 时:表示"我在找什么"(找和编程相关的上下文)
"编程" 作为 Key 时: 表示"我能提供什么"(我是编程相关的信息)
"编程" 作为 Value 时:表示"我的实际内容"(编程的具体语义)
如果用同一个向量,模型就无法区分这三种角色。
所以用三个不同的权重矩阵 W_Q, W_K, W_V 来变换:
Q = 输入 × W_Q
K = 输入 × W_K
V = 输入 × W_V
这些 W_Q, W_K, W_V 是模型在训练过程中学出来的参数。
3.2 逐步计算过程(15 分钟)
用一个极简例子手动走一遍计算流程,帮你建立直觉。
设定:3 个词,每个词用 2 维向量表示(实际是 768 或 12288 维)
输入(已经 Embedding 后的向量):
x1 = [1, 0] ("我")
x2 = [0, 1] ("喜欢")
x3 = [1, 1] ("编程")
假设训练好的权重矩阵(实际中这些是学出来的):
W_Q = [[1, 0], [0, 0]]
W_K = [[0, 1], [0, 0]]
W_V = [[1, 0], [0, 1]]
Step 1:计算 Q、K、V
Q = 输入 × W_Q
Q1 = [1,0] × [[1,0],[0,0]] = [1, 0] ← "我" 的 Query
Q2 = [0,1] × [[1,0],[0,0]] = [0, 0] ← "喜欢" 的 Query
Q3 = [1,1] × [[1,0],[0,0]] = [1, 0] ← "编程" 的 Query
K = 输入 × W_K
K1 = [1,0] × [[0,1],[0,0]] = [0, 1] ← "我" 的 Key
K2 = [0,1] × [[0,1],[0,0]] = [1, 0] ← "喜欢" 的 Key ← 注意这里
K3 = [1,1] × [[0,1],[0,0]] = [0, 1] ← "编程" 的 Key
V = 输入 × W_V
V1 = [1,0] × [[1,0],[0,1]] = [1, 0] ← "我" 的 Value
V2 = [0,1] × [[1,0],[0,1]] = [0, 1] ← "喜欢" 的 Value
V3 = [1,1] × [[1,0],[0,1]] = [1, 1] ← "编程" 的 Value
Step 2:计算 Attention 分数(用 Q·K 的点积)
以 "编程"(第 3 个词)为例,看它对每个词的关注度:
Score = Q3 · K_i
Score(编程, 我) = [1,0] · [0,1] = 1×0 + 0×1 = 0
Score(编程, 喜欢) = [1,0] · [1,0] = 1×1 + 0×0 = 1 ← 最高!
Score(编程, 编程) = [1,0] · [0,1] = 1×0 + 0×1 = 0
原始分数: [0, 1, 0]
Step 3:缩放 + Softmax 归一化
缩放(除以 √d_k,d_k 是 Key 的维度,这里是 2):
[0, 1, 0] / √2 = [0, 0.707, 0]
为什么缩放?当维度很大时,点积值会很大,
导致 Softmax 梯度消失。除以 √d_k 让数值稳定。
Softmax(把分数变成概率分布,加起来等于 1):
e^0 / (e^0 + e^0.707 + e^0) = 1 / (1 + 2.028 + 1) = 0.249
e^0.707 / (1 + 2.028 + 1) = 2.028 / 4.028 = 0.503
e^0 / 4.028 = 0.249
Attention 权重 = [0.249, 0.503, 0.249]
↑ ↑ ↑
对"我" 对"喜欢" 对"编程"
Step 4:加权求和
"编程"的新表示 = 0.249 × V1 + 0.503 × V2 + 0.249 × V3
= 0.249 × [1,0] + 0.503 × [0,1] + 0.249 × [1,1]
= [0.249, 0] + [0, 0.503] + [0.249, 0.249]
= [0.498, 0.752]
原始 "编程" = [1, 1]
新的 "编程" = [0.498, 0.752]
新表示融合了上下文信息("喜欢" 的权重最大,所以新表示更偏向 "喜欢" 的方向)
这就是 Self-Attention 的完整计算流程。实际中维度是 768 或更大,但流程一模一样。
3.3 为什么用点积而不是其他相似度(5 分钟)
为什么用 Q·K(点积)来算相似度?
方案 A: 余弦相似度 → 需要额外归一化,计算多一步
方案 B: 欧氏距离 → 值越小越相似,和 Attention 的"越大越关注"方向相反
方案 C: 点积 → 简单高效,两个向量越"同向",点积越大
点积的数学意义:
A · B = |A| × |B| × cos(θ)
当两个向量方向一致时(cos θ → 1),点积最大
当两个向量正交时(cos θ = 0),点积为 0
当两个向量相反时(cos θ = -1),点积最小
语义上:方向一致的向量代表"语义相关"
所以点积大 = 语义相关 = 应该多关注
3.4 Softmax 的作用(5 分钟)
原始分数: [0, 1, 0]
为什么不直接用这些分数做权重?
- 它们不一定是正数(可能是负数)
- 它们的和不是 1(无法做概率解释)
- 数值范围不可控
Softmax 做了两件事:
1. 把所有分数变成正数(e^x 永远为正)
2. 让它们加起来等于 1(变成概率分布)
[0, 1, 0] → Softmax → [0.249, 0.503, 0.249]
↑ 最大值被放大
其他值被压缩
Softmax 的"软"体现在:
- 不是 hard max(不是选一个为 1 其他为 0)
- 而是"软"地分配权重------大的更大,小的更小,但都保留
这让模型可以同时关注多个相关词,而不是只盯一个
自测:
- Q、K、V 分别是什么?用搜索引擎类比解释。
- 为什么 Attention 分数要除以 √d_k?
- Softmax 为什么叫"Soft"?
4. Part C:多头注意力与位置编码(25 分钟)
4.1 为什么需要多头注意力(10 分钟)
单个 Attention 头只能学一种"关注模式"。但语言中的关系是多维度的:
句子: "The animal that was running quickly crossed the street because it was scared"
"it" 这个词需要同时处理多种关系:
- 指代关系: "it" 指代 "animal"(一个头关注)
- 因果关系: "scared" 因为 "running quickly"(另一个头关注)
- 语法关系: "it" 是主语,"was scared" 是谓语(又一个头关注)
- 语义关系: "scared" 和 "animal" 语义相关(又一个头关注)
一个头只能学到一种关系
→ 用多个头,每个头学不同的关系
→ 最后把所有头的结果拼接起来
多头注意力的计算:
输入: "我 喜欢 编程" (每个词 6 维向量)
8 个头,每个头把 6 维切成 6/8... 不对,实际中维度更大
实际例子(GPT-3):
- 输入维度: 12288
- 头数: 96
- 每个头维度: 12288 / 96 = 128
每个头独立做 Self-Attention:
Head_1: 关注语法关系 → 输出 128 维
Head_2: 关注指代关系 → 输出 128 维
Head_3: 关注语义关系 → 输出 128 维
...
Head_96: 关注某种模式 → 输出 128 维
拼接: [Head_1 | Head_2 | ... | Head_96] → 12288 维
再经过一个线性变换 → 最终输出 12288 维
类比:
一个头就像一个人只戴了一种颜色的眼镜看世界:
- 红色眼镜:只看到温度(热/冷)
- 蓝色眼镜:只看到湿度(干/湿)
- 绿色眼镜:只看到距离(远/近)
多头就像同一个人同时戴了 96 副不同颜色的眼镜
→ 看到一个 96 维度的丰富世界
→ 对语言的理解更全面
4.2 位置编码------Attention 的"失忆症"(15 分钟)
问题发现:
Self-Attention 有一个"缺陷":它天生不知道词的顺序。
输入 A: "狗 咬 人"
输入 B: "人 咬 狗"
对 Self-Attention 来说,A 和 B 的处理方式完全一样!
因为 Attention 只看"哪些词之间相关",不看"谁在前谁在后"
但显然 "狗咬人" 和 "人咬狗" 意思完全不同
为什么 Attention 没有顺序概念?
Self-Attention 的计算:
每个词的新表示 = 所有词的加权求和
加权求和是"无序"的:
a×V1 + b×V2 + c×V3 = c×V3 + a×V1 + b×V2
改变输入顺序,只要权重也跟着变,结果可以一样
→ Attention 本身不编码位置信息
解决方案:位置编码(Positional Encoding)
给每个位置加上一个"位置标记",让模型知道每个词在第几个位置。
原始 Embedding:
"狗" → [0.5, -0.3, 0.8, ...]
"咬" → [0.2, 0.7, -0.1, ...]
"人" → [0.9, -0.2, 0.4, ...]
加上位置编码后:
位置 1: "狗" → [0.5, -0.3, 0.8, ...] + PE(1)
位置 2: "咬" → [0.2, 0.7, -0.1, ...] + PE(2)
位置 3: "人" → [0.9, -0.2, 0.4, ...] + PE(3)
PE(1), PE(2), PE(3) 是不同的向量
→ 即使同一个词出现在不同位置,加上 PE 后向量也不同
→ 模型能区分 "狗咬人" 和 "人咬狗"
位置编码怎么生成:
Transformer 原论文用正弦/余弦函数:
PE(pos, 2i) = sin(pos / 10000^(2i/d))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d))
pos: 词的位置(第几个词)
i: 向量的第几维
d: 向量总维度
直觉理解:
- 不同维度用不同频率的正弦波
- 就像时钟:秒针转得快、分针中等、时针慢
- 每个位置都有唯一的"波形指纹"
- 模型通过这些波形就能推断出位置
后来还有:
- 可学习位置编码(BERT 用的,位置编码是学出来的参数)
- 相对位置编码(T5 用的,编码词之间的相对距离)
- RoPE 旋转位置编码(Llama 用的,目前主流)
自测:
- 为什么需要多头注意力?一个头不够吗?
- Self-Attention 为什么没有顺序概念?
- 位置编码解决什么问题?
5. Part D:Transformer 整体架构(20 分钟)
5.1 完整数据流(10 分钟)
把前面所有零件组装起来,看 Transformer 的完整数据流:
输入文本: "Hello world"
│
▼
┌─────────────────────────────────┐
│ 1. Tokenization(分词) │
│ "Hello world" → [15496, 995] │
│ 文本 → Token ID 列表 │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 2. Embedding(嵌入) │
│ [15496, 995] → [[0.1,-0.3,..], [0.5,0.2,..]]
│ Token ID → 向量(每个向量 d_model 维)
│ 这个向量是模型学出来的"词义"表示
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 3. 位置编码(Positional Encoding)│
│ 向量 + 位置向量 → 带位置信息的向量
│ 让模型知道每个词在第几个位置
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 4. Transformer Block × N (重复 N 层) │
│ │
│ ┌───────────────────────────────────┐ │
│ │ 4a. Multi-Head Self-Attention │ │
│ │ 每个词"看"所有词,生成新表示 │ │
│ └───────────────────────────────────┘ │
│ │ + 残差连接(把输入直接加到输出) │
│ │ + Layer Normalization(归一化) │
│ ▼ │
│ ┌───────────────────────────────────┐ │
│ │ 4b. Feed Forward Network (FFN) │ │
│ │ 两层全连接,对每个词独立处理 │ │
│ │ 先放大维度(4倍)→ 激活 → 缩回 │ │
│ └───────────────────────────────────┘ │
│ │ + 残差连接 │
│ │ + Layer Normalization │
│ ▼ │
│ 输出 → 传给下一层 Transformer Block │
└─────────────────────────────────────────┘
│
│ (GPT-2 有 48 层, GPT-3 有 96 层, GPT-4 估计上百层)
│
▼
┌─────────────────────────────────┐
│ 5. 输出层 │
│ 最终向量 → 线性变换 → logits │
│ logits: 对词表中每个词的"分数" │
│ [0.1, -2.3, 5.7, 0.02, ...] │
│ (词表大小可能是 5万-10万) │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 6. Softmax → 概率分布 │
│ [0.001, 0.0001, 0.95, ...] │
│ 分数最高的词就是预测的下一个词 │
└─────────────────────────────────┘
│
▼
预测输出: "!" (比如补全 "Hello world!")
5.2 三个关键组件详解(10 分钟)
残差连接(Residual Connection):
不残差连接:
输入 → Attention → 输出
残差连接:
输入 → Attention → 输出 + 输入(把输入直接加到输出上)
为什么需要?
- Transformer 有几十上百层
- 深层网络容易梯度消失(训练不动)
- 残差连接让梯度可以"抄近路"直接传到前面
- 训练深层网络变得可行
类比:
你写了一篇文章,编辑改了一版
残差连接 = 编辑的修改 + 你的原文
如果编辑改得好,最终版 = 原文 + 改进
如果编辑改得不好,最终版 ≈ 原文(不会比原来更差)
Layer Normalization(层归一化):
做什么:
对一个样本的所有特征做归一化(均值=0, 方差=1)
为什么需要:
- 深层网络中,每层的输出数值范围可能越来越大或越来越小
- 导致训练不稳定(梯度爆炸或消失)
- LayerNorm 把数值拉回稳定范围
类比:
就像调音量------每经过一层,信号可能变大或变小
LayerNorm 确保每层的输出音量都在合适范围
Feed Forward Network(前馈网络):
结构:
输入 (d 维) → 升维 (4d 维) → ReLU 激活 → 降维 (d 维) → 输出
比如 d=768:
768 → 3072 → ReLU → 768
为什么需要:
- Self-Attention 做的是"词与词之间的信息交换"
- FFN 做的是"每个词内部的非线性变换"
- 两者互补: Attention 负责横向(词间),FFN 负责纵向(词内)
先放大维度再缩回: 让模型在更高维空间做非线性变换
→ 增强表达能力(就像在更大的画布上画画,再缩小回来)
ReLU 激活:
负数变 0,正数不变
→ 引入非线性(没有激活函数,再多层也等价于一层线性变换)
6. Part E:Encoder vs Decoder(15 分钟)
6.1 三种架构对比
Transformer 有三种使用方式,对应三种模型类型:
┌─────────────────────────────────────────────────────┐
│ Encoder-only(只用编码器) │
│ │
│ 输入 → [Attention: 能看所有词] → 输出 │
│ │
│ 特点: 每个词能同时看到前后所有词(双向) │
│ 适合: 理解类任务(分类、搜索、相似度) │
│ 代表: BERT, RoBERTa, DeBERTa │
│ 类比: 阅读理解------你可以反复看全文 │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ Decoder-only(只用解码器) │
│ │
│ 输入 → [Attention: 只能看前面的词] → 预测下一个词 │
│ │
│ 特点: 每个词只能看到前面的词(单向/自回归) │
│ 适合: 生成类任务(对话、写作、代码) │
│ 代表: GPT 系列, Llama, DeepSeek, Qwen │
│ 类比: 即兴演讲------你只能基于已说过的内容继续说 │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ Encoder-Decoder(编码器+解码器) │
│ │
│ 输入 → Encoder(双向) → 编码向量 │
│ 编码向量 → Decoder(单向+交叉注意力) → 输出 │
│ │
│ 特点: 编码器理解输入,解码器生成输出 │
│ 适合: 转换类任务(翻译、摘要) │
│ 代表: T5, BART, 早期 Transformer │
│ 类比: 翻译------先理解原文,再逐词生成译文 │
└─────────────────────────────────────────────────────┘
6.2 为什么 GPT 是 Decoder-only
Decoder-only 的核心特征: 自回归生成(Autoregressive)
给定 "今天 天气 真" → 预测 "好"
给定 "今天 天气 真 好" → 预测 ","
给定 "今天 天气 真 好 ," → 预测 "适合"
...一个词一个词往后生成
为什么 Decoder 用"掩码注意力"(Masked Attention)?
训练时:
输入: "今天 天气 真 好"
目标: 预测每个位置下一个词
处理 "天气" 时,不能看到 "真" 和 "好"
否则就是"作弊"------模型直接抄答案了
掩码: 把当前词后面的 Attention 权重强制设为 0
位置1 "今天": 只能看 "今天"
位置2 "天气": 只能看 "今天" "天气"
位置3 "真": 只能看 "今天" "天气" "真"
...
这就是为什么 ChatGPT 一个字一个字蹦出来:
生成每个词时,它只能基于已经生成的词
不能"偷看"未来
6.3 为什么现在是 Decoder-only 的天下
2020 年之前: BERT (Encoder-only) 是 NLP 霸主
2022 年之后: GPT (Decoder-only) 统治一切
原因:
1. 生成能力: Decoder 天生适合生成,而 LLM 的核心能力就是生成
2. 扩展性: Decoder-only 结构更简单,更容易扩展到超大参数量
3. 统一性: 生成是通用能力------翻译、摘要、问答、代码都可以用生成来做
而理解类任务(分类)也可以通过 Prompt 转成生成任务
4. In-context Learning: 大规模 Decoder 模型涌现出了"上下文学习"能力
给几个例子就能做新任务,不需要微调
自测:
- Encoder-only 和 Decoder-only 的核心区别是什么?
- 为什么 Decoder 要用 Masked Attention?
- 为什么现在主流大模型都是 Decoder-only?
7. Part F:代码实验------注意力可视化(20 分钟)
用代码把抽象概念变直观。这是今天唯一的代码部分。
7.1 最简 Self-Attention 实现
创建 code/attention_basics.py:
python
"""
Day 2 代码实验:用最简代码实现 Self-Attention,并可视化注意力权重
"""
import numpy as np
# ============================================
# 实验 1: 最简 Self-Attention(手动实现)
# ============================================
print("=" * 60)
print("实验 1: 手动实现 Self-Attention")
print("=" * 60)
# 模拟 3 个词的 Embedding(4 维向量)
# 实际中维度是 768 或 12288
np.random.seed(42)
seq_len = 4 # 句子长度(4 个词)
d_model = 8 # 每个词的向量维度
# 随机生成输入(模拟 Embedding 后的向量)
X = np.random.randn(seq_len, d_model)
print(f"\n输入形状: {X.shape} (句子长度={seq_len}, 维度={d_model})")
print(f"输入(每个词的向量):")
for i in range(seq_len):
print(f" 词{i}: {[f'{v:.3f}' for v in X[i]]}")
# 三个权重矩阵(实际中是训练学出来的)
W_Q = np.random.randn(d_model, d_model)
W_K = np.random.randn(d_model, d_model)
W_V = np.random.randn(d_model, d_model)
# Step 1: 计算 Q, K, V
Q = X @ W_Q # (4, 8) @ (8, 8) = (4, 8)
K = X @ W_K
V = X @ W_V
print(f"\nQ 形状: {Q.shape} (每个词的 Query 向量)")
print(f"K 形状: {K.shape} (每个词的 Key 向量)")
print(f"V 形状: {V.shape} (每个词的 Value 向量)")
# Step 2: 计算 Attention 分数 (Q · K^T)
# K^T 是 K 的转置
scores = Q @ K.T # (4, 8) @ (8, 4) = (4, 4)
print(f"\nAttention 分数矩阵形状: {scores.shape}")
print(f" scores[i][j] = 词i 对 词j 的关注分数")
# Step 3: 缩放(除以 √d_k)
d_k = d_model
scaled_scores = scores / np.sqrt(d_k)
# Step 4: Softmax(按行归一化)
def softmax(x):
"""对每一行做 Softmax"""
# 减去最大值防止数值溢出
x_max = np.max(x, axis=-1, keepdims=True)
exp_x = np.exp(x - x_max)
return exp_x / np.sum(exp_x, axis=-1, keepdims=True)
attention_weights = softmax(scaled_scores)
print(f"\nAttention 权重矩阵(每行加起来=1):")
print(f"{'':>8}", end="")
for j in range(seq_len):
print(f" 词{j} ", end="")
print()
for i in range(seq_len):
print(f"词{i}看 → ", end="")
for j in range(seq_len):
print(f"{attention_weights[i][j]:.3f} ", end="")
print(f" (和={attention_weights[i].sum():.3f})")
# Step 5: 加权求和
output = attention_weights @ V # (4, 4) @ (4, 8) = (4, 8)
print(f"\n输出形状: {output.shape}")
print(f"每个词的新表示融合了上下文信息")
# ============================================
# 实验 2: 可视化 Attention 权重
# ============================================
print("\n" + "=" * 60)
print("实验 2: Attention 权重可视化")
print("=" * 60)
# 用一个真实句子来演示
# 模拟 "The cat sat on the mat because it was tired"
words = ["The", "cat", "sat", "on", "the", "mat", "because", "it", "was", "tired"]
seq_len = len(words)
d_model = 16
np.random.seed(123)
X = np.random.randn(seq_len, d_model)
W_Q = np.random.randn(d_model, d_model)
W_K = np.random.randn(d_model, d_model)
W_V = np.random.randn(d_model, d_model)
Q = X @ W_Q
K = X @ W_K
V = X @ W_V
scores = Q @ K.T / np.sqrt(d_model)
weights = softmax(scores)
# 用方块字符做热力图
print(f"\n句子: {' '.join(words)}")
print(f"\nAttention 热力图(每行=一个词在看其他词,█越多=关注度越高):\n")
# 表头
print(f"{'':>10}", end="")
for w in words:
print(f"{w:>6}", end="")
print()
print("-" * (10 + len(words) * 6))
for i, word_i in enumerate(words):
print(f"{word_i:>10}", end="")
for j in range(len(words)):
w = weights[i][j]
if w > 0.3:
bar = "████"
elif w > 0.2:
bar = "███ "
elif w > 0.15:
bar = "██ "
elif w > 0.1:
bar = "█ "
else:
bar = " "
print(f" {bar}", end="")
print()
print(f"\n解读: 看每一行,█越多表示当前词越关注那个位置的词")
print(f"(注意: 随机权重的热力图不会有明显模式,真实模型会有清晰模式)")
# ============================================
# 实验 3: Attention 的无序性验证
# ============================================
print("\n" + "=" * 60)
print("实验 3: 验证 Attention 的无序性")
print("=" * 60)
# 原始输入
X_original = np.random.randn(3, 8)
W_Q = np.random.randn(8, 8)
W_K = np.random.randn(8, 8)
W_V = np.random.randn(8, 8)
def compute_attention(X, W_Q, W_K, W_V):
Q = X @ W_Q
K = X @ W_K
V = X @ W_V
scores = Q @ K.T / np.sqrt(8)
weights = softmax(scores)
output = weights @ V
return output, weights
# 原始顺序: [词0, 词1, 词2]
out_orig, w_orig = compute_attention(X_original, W_Q, W_K, W_V)
# 打乱顺序: [词1, 词0, 词2]
X_shuffled = X_original[[1, 0, 2]]
out_shuffled, w_shuffled = compute_attention(X_shuffled, W_Q, W_K, W_V)
print(f"\n原始输入顺序: 词0, 词1, 词2")
print(f"打乱输入顺序: 词1, 词0, 词2")
print(f"\n原始 Attention 权重:")
print(w_orig)
print(f"\n打乱后 Attention 权重:")
print(w_shuffled)
print(f"""
关键发现:
打乱顺序后,Attention 权重矩阵的行也跟着换了位置
但每个词对应的权重分布不变(只是换了行号)
这证明了: Self-Attention 本身不感知位置
→ 这就是为什么需要位置编码(Positional Encoding)
→ 没有位置编码,"狗咬人" 和 "人咬狗" 对模型来说一样
""")
# ============================================
# 实验 4: 多头 Attention 模拟
# ============================================
print("=" * 60)
print("实验 4: 多头 Attention(模拟 4 个头)")
print("=" * 60)
seq_len = 4
d_model = 32
n_heads = 4
d_head = d_model // n_heads # 每个头 8 维
np.random.seed(456)
X = np.random.randn(seq_len, d_model)
print(f"\n配置: {seq_len} 个词, {d_model} 维, {n_heads} 个头, 每头 {d_head} 维")
# 每个头有自己的 W_Q, W_K, W_V
heads_outputs = []
for h in range(n_heads):
W_Q_h = np.random.randn(d_model, d_head)
W_K_h = np.random.randn(d_model, d_head)
W_V_h = np.random.randn(d_model, d_head)
Q_h = X @ W_Q_h
K_h = X @ W_K_h
V_h = X @ W_V_h
scores_h = Q_h @ K_h.T / np.sqrt(d_head)
weights_h = softmax(scores_h)
output_h = weights_h @ V_h
heads_outputs.append(output_h)
print(f"\n Head {h+1} 权重矩阵:")
for i in range(seq_len):
print(f" 词{i}: {[f'{w:.2f}' for w in weights_h[i]]}")
# 拼接所有头的输出
concat = np.concatenate(heads_outputs, axis=-1)
print(f"\n拼接后形状: {concat.shape} (等于原始维度 {d_model})")
print(f"每个头学到了不同的关注模式,拼接后信息更丰富")
运行:
bash
python code/attention_basics.py
预期输出(数值会不同,但格式一致):
实验 1: 手动实现 Self-Attention
输入形状: (4, 8) (句子长度=4, 维度=8)
Attention 权重矩阵(每行加起来=1):
词0 词1 词2 词3
词0看 → 0.287 0.213 0.256 0.244 (和=1.000)
词1看 → 0.241 0.312 0.219 0.228 (和=1.000)
...
实验 2: Attention 权重可视化
句子: The cat sat on the mat because it was tired
Attention 热力图:
The cat sat on the mat because it was tired
----------------------------------------------------------------------
The █ ███ █ █ █ █ █ █ █ █
cat █ ███ █ █ █ █ █ █ █ █
...
实验 3: 验证 Attention 的无序性
关键发现: 打乱顺序后权重行跟着换,但分布不变 → 需要位置编码
实验 4: 多头 Attention(模拟 4 个头)
每个头有不同的权重分布 → 多维度理解文本
7.2 实验后思考题
在 notes/day2.md 中回答:
- 实验 1:Attention 权重矩阵的每一行为什么加起来等于 1?(答:Softmax 归一化)
- 实验 2:随机权重的热力图没有明显模式,你觉得真实模型训练后会出现什么模式?(提示:指代词会高权重指向指代对象)
- 实验 3:如果给输入加上位置编码,打乱顺序后结果还会一样吗?(答:不会,位置编码让每个位置的向量不同)
- 实验 4:4 个头的权重分布不同,这说明什么?(答:不同头学到了不同的关注模式)
8. 今日笔记模板
在 notes/day2.md 中写:
markdown
# Day 2 笔记 --- Transformer 架构直觉理解
## 日期:2026-07-XX
## 学习时长:X 小时 X 分钟
## 一、今天学到的 5 件事
1.
2.
3.
4.
5.
## 二、RNN 的三个问题(填表)
| 问题 | 说明 | Attention 怎么解决 |
|------|------|-------------------|
| | | |
| | | |
| | | |
## 三、Q/K/V 解释(用搜索引擎类比)
Q (Query):
K (Key):
V (Value):
## 四、Self-Attention 计算流程(按顺序写)
1.
2.
3.
4.
5.
## 五、多头注意力(我的理解,200 字内)
(为什么要多个头?每个头做什么?)
## 六、位置编码解决什么问题
问题:
解决方案:
## 七、三种架构对比(填表)
| 架构 | 代表模型 | 能看哪些词 | 适合什么任务 |
|------|---------|-----------|-------------|
| Encoder-only | | | |
| Decoder-only | | | |
| Encoder-Decoder | | | |
## 八、GPT 为什么一个字一个字生成?
(用 Masked Attention 解释)
## 九、代码实验记录
| 实验 | 观察到的现象 |
|------|------------|
| 实验1 手动Attention | |
| 实验2 权重可视化 | |
| 实验3 无序性验证 | |
| 实验4 多头Attention | |
## 十、还不清楚的问题
-
-
## 十一、今日代码清单
- [ ] code/attention_basics.py 运行成功
- [ ] 4 个实验全部完成
- [ ] 4 道思考题回答完
9. 验收清单
概念验收
- 能说出 RNN 的三个问题(长距离依赖、无法并行、信息瓶颈)
- 能用搜索引擎类比解释 Q、K、V
- 能按顺序说出 Self-Attention 的 5 步计算流程
- 知道为什么 Attention 分数要除以 √d_k
- 能解释 Softmax 的作用
- 知道多头注意力为什么需要多个头
- 能解释 Attention 为什么没有顺序概念
- 知道位置编码解决什么问题
- 能说出 Transformer Block 的组成部分(Attention + FFN + 残差 + LayerNorm)
- 知道残差连接和 LayerNorm 的作用
- 能区分 Encoder-only / Decoder-only / Encoder-Decoder
- 能解释 GPT 为什么是 Decoder-only(自回归 + Masked Attention)
代码验收
-
attention_basics.py运行成功 - 实验 1:看到了 Attention 权重矩阵,每行和为 1
- 实验 2:看到了热力图可视化
- 实验 3:验证了 Attention 的无序性
- 实验 4:看到了不同头的不同权重分布
笔记验收
-
notes/day2.md按模板写完 - 4 道思考题回答完
- 至少记录了 5 件学到的事
10. 常见问题 FAQ
Q1:Q、K、V 的权重矩阵是固定的吗?
A:不是。W_Q、W_K、W_V 是模型在训练过程中学出来的参数。不同模型、不同层的权重矩阵都不同。这就是模型"智能"的来源------通过海量数据训练出合适的权重,让 Attention 关注该关注的地方。
Q2:为什么除以 √d_k 而不是 d_k?
A:当 Q 和 K 的分量是均值为 0、方差为 1 的独立分布时,点积 Q·K 的方差是 d_k(每项乘积的方差之和)。除以 √d_k 让方差变回 1,防止维度增大时点积值过大导致 Softmax 饱和(梯度消失)。这是数学上的最优缩放。
Q3:多头注意力的头数怎么选?
A:常见选择:8 头、16 头、32 头、96 头。头数和维度有关------总维度 ÷ 头数 = 每头维度,每头维度一般不低于 32(太小信息不够)。GPT-3 是 96 头 × 128 维/头 = 12288 维。头数不是越多越好,太多会导致每个头维度太小,信息不足。
Q4:位置编码为什么用正弦/余弦?
A:三个原因:
- 每个位置的编码是唯一的(不同频率的正弦波组合)
- 可以推广到训练时没见过的更长序列(正弦函数可以无限延伸)
- 相对位置可以通过线性变换表示(sin(a+b) 可以用 sin(a) 和 cos(a) 表示)
不过现代大模型(Llama 等)更多用 RoPE(旋转位置编码),效果更好。
Q5:Decoder 的 Masked Attention 具体怎么实现?
A:在计算 Attention 分数后、Softmax 之前,把当前词后面的所有位置设为负无穷(-∞)。Softmax 后这些位置的权重就变成 0(e^(-∞) = 0)。这样当前词就"看不到"后面的词了。
原始分数矩阵:
[0.5, 0.3, 0.8, 0.2]
[0.1, 0.6, 0.7, 0.4]
[0.3, 0.2, 0.5, 0.9]
[0.4, 0.7, 0.1, 0.6]
加上 Mask(上三角设为 -∞):
[0.5, -∞, -∞, -∞]
[0.1, 0.6, -∞, -∞]
[0.3, 0.2, 0.5, -∞]
[0.4, 0.7, 0.1, 0.6]
Softmax 后:
[1.0, 0.0, 0.0, 0.0] ← 词0 只能看自己
[0.4, 0.6, 0.0, 0.0] ← 词1 只能看词0和词1
[0.3, 0.2, 0.5, 0.0] ← 词2 只能看词0、1、2
[0.2, 0.3, 0.1, 0.4] ← 词3 能看所有词
Q6:BERT 和 GPT 都用 Transformer,为什么能力差异这么大?
A:架构只是基础,更重要的差异是:
- 训练目标:BERT 用掩码语言模型(完形填空),GPT 用自回归生成(接龙)
- 规模:BERT 最大 3.4 亿参数,GPT-3 有 1750 亿参数,差 500 倍
- 训练数据:GPT 的数据量远大于 BERT
- 涌现能力:规模够大后,GPT 涌现出 in-context learning,BERT 没有
所以不是"Decoder 比 Encoder 好",而是大规模 + 自回归生成 + 海量数据 共同造就了 GPT 的成功。
Q7:Transformer 这么好,RNN 就完全没用了吗?
A:基本被淘汰了。但有些场景 RNN/LSTM 还有优势:
- 资源极度受限的嵌入式设备(RNN 参数少)
- 流式处理(RNN 天然逐词处理,不需要等全部输入)
- 不过这些场景也在被轻量级 Transformer 替代
11. 扩展资源
必看(和今天内容直接相关)
| 资源 | 类型 | 时长 | 价值 |
|---|---|---|---|
| 3Blue1Brown「But what is a GPT?」 | YouTube 视频 | 15 min | GPT 直觉理解最佳,可视化 Attention |
| Jay Alammar「The Illustrated Transformer」 | 英文文章 | 30 min | 图解 Transformer 最清晰,每一步都有图 |
| 李宏毅 Transformer 课程 | YouTube 视频 | 40 min | 中文讲解,直觉理解好 |
选看(有余力再看)
| 资源 | 类型 | 说明 |
|---|---|---|
| Karpathy「Let's build GPT from scratch」 | YouTube | 2 小时,用代码从零实现 GPT,硬核 |
| The Annotated Transformer | 英文文章 | 逐行代码注释 Transformer 原论文 |
| Attention Is All You Need 原论文 | 论文 | 2017 年 Transformer 原始论文,6 页 |
| Jay Alammar「Illustrated Self-Attention」 | 英文文章 | 专门讲 Self-Attention 的图解 |
交互式工具
| 工具 | 说明 |
|---|---|
| BertViz | 可视化 Transformer 的注意力权重(Jupyter 插件) |
| Transformer Visualization | 交互式 Transformer 可视化(网页版,推荐!) |
| LLM Visualization | 3D 可视化 LLM 内部结构 |
12. 明日预告
Day 3 主题:Token、Embedding、Context Window
明天你会学到:
- Token 到底是什么(不是词也不是字)
- 用 tiktoken 库做 Token 计数实验
- 中英文 Token 消耗差异
- Embedding 怎么把文字变成向量
- 用 Embedding 做语义相似度实验(RAG 的雏形)
- Context Window 的限制和应对策略
明天代码量较大,有 2 个实验脚本,是后面 RAG 学习的基础。
附录:Day 2 时间分配建议
一次性学完(约 2-2.5 小时):
00:00-00:30 Part A: 为什么需要 Attention(边读边画图)
00:30-01:10 Part B: Self-Attention 详解(最抽象,慢慢读)
01:10-01:15 休息 5 分钟
01:15-01:40 Part C: 多头注意力 + 位置编码
01:40-02:00 Part D: Transformer 整体架构
02:00-02:15 Part E: Encoder vs Decoder
02:15-02:35 Part F: 代码实验(attention_basics.py)
02:35-02:45 写笔记
分两次(推荐):
第一次(1 小时):
Part A: 为什么需要 Attention(30 min)
Part B: Self-Attention 前半部分(30 min)
第二次(1-1.5 小时):
Part B: Self-Attention 后半部分(15 min)
Part C: 多头 + 位置编码(25 min)
Part D: Transformer 架构(20 min)
Part E: Encoder vs Decoder(15 min)
Part F: 代码实验(20 min)
写笔记(10 min)
纯概念模式(不看代码,1.5 小时):
如果你今天时间不够,可以只看 Part A-E 的概念部分(约 1.5 小时),代码实验放到周末和 Day 6 一起做。概念理解比代码更重要。
附录:Day 2 核心概念速查卡
┌──────────────────────────────────────────────────────────┐
│ Transformer 核心公式 │
│ │
│ Attention(Q, K, V) = softmax(Q·K^T / √d_k) · V │
│ │
│ 多头: head_i = Attention(Q·W_i^Q, K·W_i^K, V·W_i^V) │
│ MultiHead = Concat(head_1, ..., head_h) · W^O │
│ │
│ 位置编码: PE(pos, 2i) = sin(pos / 10000^(2i/d)) │
│ PE(pos, 2i+1) = cos(pos / 10000^(2i/d)) │
│ │
│ Transformer Block = │
│ LayerNorm(x + MultiHeadAttention(x)) │
│ LayerNorm(x + FeedForward(x)) │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ 速记口诀 │
│ │
│ RNN 三病: 长距离丢、不能并行、信息瓶颈 │
│ Attention 三好: 全局视野、动态权重、并行计算 │
│ QKV 三角色: Q找信息、K提供信息、V是信息本身 │
│ 计算五步: 算QKV → 点积 → 缩放 → Softmax → 加权求和 │
│ 多头: 一个头一种关系,多个头多维度理解 │
│ 位置编码: Attention没顺序感,加PE补上 │
│ 三架构: BERT双向理解、GPT单向生成、T5理解+生成 │
│ Masked Attention: 看前面不看后面,防止作弊 │
└──────────────────────────────────────────────────────────┘