Transformer 可视化分析 + 大模型推理策略:非常新颖的题材,发展也是一步一个脚印,没有那种一蹴而就的浮躁感
以前的 AI 属于模式识别(有什么特征,回答什么解法),不灵活。直到谷歌提出《自注意力机制、Transformer》才为后面的 AI (GPT系列)打下基础。Google 的科学家评论 OpenAI 就是暴力 + 算力,纯大力出奇迹。海量的数据 + 海量的算力,让 Transformer 训练一遍就什么都学会了。Transformer 解决的问题:理解单个词语 + 理解词语顺序 + 理解上下文。表
Transformer 可视化分析 + 大模型推理策略:非常新颖的题材,发展也是一步一个脚印,没有那种一蹴而就的浮躁感
背景介绍
以前的 AI 属于模式识别(有什么特征,回答什么解法),不灵活。
直到谷歌提出《自注意力机制、Transformer》才为后面的 AI (GPT系列)打下基础。
Google 的科学家评论 OpenAI 就是暴力 + 算力,纯大力出奇迹。
海量的数据 + 海量的算力,让 Transformer 训练一遍就什么都学会了。
Transformer 解决的问题:理解单个词语 + 理解词语顺序 + 理解上下文。
为什么Transformer理解单个词语意思 + 理解词语顺序 + 理解上下文,就能摆脱模式识别,灵活的读懂意思?
根本原因涉及:
- 语言本身的结构特性:
- 组合性:词语组合产生新含义
- 上下文相关性:含义依赖语境
- 序列性:顺序影响意义
让我通过具体的例子来说明这三个语言特性:
- 组合性
例子:「苹果手机」vs「苹果派」vs「苹果公司」
- 「苹果」这个词与不同词组合产生完全不同的含义
- 「苹果手机」→ 一个电子设备
- 「苹果派」→ 一种甜点
- 「苹果公司」→ 一家科技公司
这展示了词语组合如何产生新的、独特的含义,而不是简单的词义叠加。
- 上下文相关性
例子:「这个球打得真好」
在不同语境下可能指:
- 在网球比赛现场 → 赞赏选手击球技术
- 在篮球场 → 称赞投篮或传球
- 在台球室 → 夸奖选手的击球角度
- 在排球赛中 → 表扬扣球或传球技巧
同样的句子,需要依靠上下文才能准确理解其具体含义。
- 序列性
例子:以下三句话使用相同的词,但顺序不同导致含义完全不同:
- 「猫追狗」vs「狗追猫」
- 「猫追狗」→ 猫是主动方,狗是被追赶的对象
- 「狗追猫」→ 狗是主动方,猫是被追赶的对象
或者:
- 「他说她错了」vs「她说他错了」
- 「他说她错了」→ 男性认为女性有错
- 「她说他错了」→ 女性认为男性有错
这些例子说明词序的改变会导致句子表达的事实完全不同,即使使用的是相同的词。
通过这些例子,我们可以看到:
- 词语的意义不是孤立的,而是在组合中产生新的含义
- 相同的语言表达在不同上下文中可能有完全不同的解释
- 词序的变化会导致意义的根本改变
这就是为什么一个成功的语言模型必须能够:
- 理解词语组合产生的新含义
- 考虑上下文信息
- 重视词序关系
这三点正是Transformer架构通过其self-attention机制、位置编码和深层网络结构所实现的核心能力。
Transformer 分为四部分:文字编码、自注意力机制、神经网络、文本输出
输入是 文字,输出是概率值(已知 猫和老,下面一个字 = 计算出概率最大的字)
完整分析:输入 - 处理 - 输出,那中间是如何处理的呢?
-
编码器(Encoder),上图左侧
输入文本经过Embedding层转换为向量,同时加入位置编码以保留位置信息。
然后进入编码器(Encoder),在这里文本首先通过多头自注意力层(语义关系信息),让每个词都能看到其他所有词并建立关联(比如"春天来了"中的"来"会关注到"春天"是动作的主语)。
接着经过Add & Norm层进行残差连接和归一化,保持信息流动和数值稳定。
再通过前馈神经网络进行特征转换,增强表达能力。
这个编码过程会重复N次,逐层提取更深层的特征。
浅层的编码器层更接近输入,可能更多地关注输入的表面特征,例如词语的形态和局部关系。
随着层数的增加,深层的编码器层能够捕捉更长距离的依赖关系和全局语义。
因此,深层的编码器层更倾向于提取输入的更抽象的语义特征,例如句子之间的逻辑关系和语义连贯性。
-
解码器(Decoder),上图右侧
开始时,输出文本也要先经过Embedding和位置编码。
然后通过带掩码的多头注意力层,只允许看到已经生成的词。
接着是编码器-解码器注意力层,将解码器当前位置与编码器的所有位置建立联系。
同样要经过Add & Norm和前馈网络,这个过程也重复N次。最后通过线性层和Softmax输出每个位置的词概率分布。
这种设计让模型能够:先充分理解输入(编码器),再一步步生成输出(解码器),同时通过多层堆叠和注意力机制保证了信息的充分提取和利用。
步步拆解版,更益懂
文字转数字解决词语理解(一词多意)
位置信息编码解决词语顺序(不同顺序不同意思)
自注意力机制(语义关系学习)解决上下文(不同上下文,不同意思)
理解单个词语
Token 化:文本转数字,编号方便找到
计算机是不懂文字的,我们会把文字向量化(变成一组数字),但变成数字之前,你还得把人类所有语言进行编号,拆解。
比如人类找东西,在左手边房间的柜子的抽屉,机器是不认识的,你必须把房间、柜子、抽屉编号(第一个房间、第二个柜子、第三个抽屉)
OpenAI 的 tiktoken,能将输入文本拆分成更小单位,再换成相应数字编号。
原文:"我喜欢强化学习!"
可能的分词结果:
["我", "喜欢", "强化", "学习", "!"]
或
["我", "喜欢", "强化学习", "!"]
合适的分词粒度可以捕捉更好的语义信息
-
医疗
专业术语保持完整(糖尿病,而不是糖、尿、病)
症状描述(胸口疼痛,而不是胸口、疼痛)
医学检查项目(心电图,而不是心、电、图)
药品名字(阿司匹林,而不是阿、…、)
时间信息(每日三次)
tiktoken 词表约10万个token,包含了中文、英文、各种符号等,每一个文字或符号对应一个 编号。
我 -> 1
请 -> 23712
客 -> 99999
! -> 77329
embedding 化:数字转矩阵,解决一词多义
为了能解决一词多义,OpenAI 把数字变成矩阵,定义了一个超参数 d_moel = 512
。
也就是说,10万的词表 * 512,每个词都能有 512 种不同语境下的变化。
比如一句话有 3 个 token,就是 3 * 512
。
直接处理整个表单
RNN/LSTM 的设计:一个时间步一个时间步地处理输入序列的每个元素
- 一次只看一个元素:RNN/LSTM 像接力跑,一步只处理序列里的一个词或数字;处理完就把结果和“接力棒” —— 隐藏状态 —— 传给下一步。
- 接力棒里装着记忆:隐藏状态(LSTM 还多一个“细胞状态”)保存了前面看到的信息,所以后面的步骤能参考前文。
- 顺序自然被记住:因为每一步都会带着上一步的记忆继续跑,模型就能理解“先发生什么、后发生什么”的关系,捕捉时间上的依赖。
但 Transformer 放弃了“逐行对数据进行处理”的方式,而是一次性处理一整张表单。
层级 | Why 问题 | 技术/理论回答 |
---|---|---|
Why 1 | 为什么要放弃 RNN 式的逐时间步计算? | 串行依赖无法并行,GPU/TPU 上计算利用率低,导致训练/推理速度成为瓶颈。 |
Why 2 | 为什么并行化如此关键? | 需要在可接受的时间内训练更大数据集、更深模型,而 Moore 定律放缓、单核性能提升有限,只能靠并行扩展吞吐量。 |
Why 3 | 为什么大规模数据和模型是必须的? | 语言等自然序列信号高度稀疏、多义且长程相关;要获得稳健泛化能力,必须依赖“规模法则”——参数量和语料规模显著提升才能进一步压低损失。 |
Why 4 | 为什么 RNN 很难处理长程依赖? | BPTT(Back‑Propagation Through Time)在长序列中梯度易消失/爆炸;即使加入 LSTM/GRU 门控,也要增加状态尺寸且序列越长计算越慢,成本呈线性增长。 |
Why 5 | 为什么 Attention 能替代递归捕获长程依赖? | 点积注意力可在 O(1) 的路径长度内让任意两 token 直接交互,避免梯度衰减;进一步用矩阵乘法实现,多头并行,天然与 GPU 张量核心匹配。 |
- 根因:串行循环结构与现代大规模并行硬件不匹配,难以在可行的时间内充分利用海量数据;同时梯度传播路径过长,限制了模型捕获长程依赖的能力。
- 关键改进:Transformer 用一组全并行的 Self‑Attention + 前馈网络 替代递归,把序列依赖显式编码进注意力权重,再借助位置编码补足缺失的位置信息。
- 连锁效应:并行化 → 更大规模 → 更强性能 → 研究与工业范式迁移 → 生成式 AI 浪潮。
理解词语顺序:加一个位置编号,语义信息 + 位置信息
“他打我” vs “我打他”,这是完全不同的意思,所以我们必须再一个位置编号!
一般的思路,就是用 1、2、3 代替位置,数字矩阵 + 位置矩阵。
我 0.2 + 1, -0.3 + 1, 0.5 + 1
请 0.4 + 2, 0.1 + 2, -0.2 + 2
客 -0.1 + 3, 0.4 + 3, 0.3 + 3
但这样也有问题,因为位置编号(越后面越大),一篇长文章可能几万字,那最初的字是1,最后的字是 3万。
因为按照 注意力机制 计算关注度的算法,就是俩个数相乘,结果越大关注度越高。
这样的位置编号,会导致,没学到真正的语义关系。
数字矩阵的范围都是 [-1, 1],这个位置信息会扰乱真实信息,你必须把位置信息,也变成 [-1, 1] 的编号范围。
位置编码需要一种特殊的函数,这个函数会为序列中的每个位置生成一个向量。
有这些函数符合:
方案 | 共性 | 差异(优/缺) |
---|---|---|
多频正余弦 (Transformer 原版) |
基函数正交、易外推 | 点积友好、参数 0,但周期性 ⇒ 模长 > 周期时失唯一 |
Rotary PE (RoPE) | 同属正余弦族 | 把 cos/sin 写成复旋转,支持相对位移、外推性更强 |
分段线性 / ALiBi | 单调斜率,各维 slope 不同 | 仅加权系数,无周期困扰;但维间相关性强于正余弦 |
Vandermonde f(i)_k = α_k · iᵏ |
任何 i 唯一 | 高次幂易数值爆炸;不对称,远距指数大 |
随机正交矩阵列 f(i) = Q·one_hot(i) |
唯一、各维独立 | 需存整张矩阵;长度受限,不可外推 |
低相关序列 (Sobol) | 长度可扩,均匀分布 | 维间统计独立但非解析正交;缺少距离可解释性 |
哈希旋转 f(i)_k = σ(⟨i,w_k⟩ mod p) |
O(1) 生成、似随机 | 理论上可能碰撞;缺梯度友好结构 |
对于一个特定的位置,这个函数生成的向量在所有维度上的值都是不同的:
- 唯一且全维差异 → 保证每个位置在所有注意力头中可判别,不丢序列顺序。
- 多频率正余弦 → 在点积空间里把“绝对位置”映射成“可推断的相对距离”,天然契合 Self‑Attention。
- 函数型编码 → 参数恒定、可外推、易改造,成为长上下文与多模态时代的基础设施。
位置编码的梳理:
阶段 | 关键问题 | 直白解释 | 关键词 |
---|---|---|---|
1. 失去顺序 | Transformer 把整句一次性塞进注意力,天然不知道第几个词排在哪里。 | 好比电影院只看到了所有观众,却没保存座位号。 | 顺序断层 |
2. 必须贴“指纹” | 如果不定位,模型会把 “屡败屡战” 与 “屡战屡败” 当同一个序列。 | 给每个位置分配一串独一无二的数字组合——位置指纹。 | 唯一标识 |
3. 设计目标 | 这个指纹要同时满足: ① 每个位置全局唯一; ② 指纹各维度都随位置变化,不能有“死灯”; ③ 生成成本低,不靠查表; ④ 点积注意力一看就能用。 |
就像给每座椅装一排彩灯:排号不同,整排灯亮度模式必不同;而且灯光还能透露“隔几排”。 | 全维互异、可计算、距离可读 |
4. 方案——正余弦波 | 用多频率的 sin / cos 波形 当彩灯:PE(i)₂k = sin(i / 10000^{2k/d}) PE(i)₂k+1 = cos(i / 10000^{2k/d}) |
- 波长从长到短,保证“远近都区分”; - 任意两排灯的相位差 = 间隔距离,注意力一做点积就读出来。 |
正余弦位置编码 |
5. 实际做法 | 把这串“灯光指纹”直接 加在词向量上,再送进第一层注意力。 | 于是模型同时知道“词义 + 座位号”。 | Embedding + PE |
6. 后续变体 | - RoPE:把 sin/cos 写成旋转操作,外推到 128 k token 也 OK; - ALiBi:斜率编码,省内存; - xPos:给 RoPE 再加缩放,让超长距离也稳定。 |
但无论怎么改,核心还是:把整数位置变成容易被点积“看懂”的指纹向量。 | RoPE / ALiBi / xPos |
层级 | Why 问题 | 技术/数学回答 |
---|---|---|
Why 1 | 为什么位置向量要“唯一”且“全维不同”? | 若不同位置出现重复或仅在部分维度重叠,注意力模块就可能把不同位置混淆,破坏序列顺序辨识;全维度差异确保对所有 token、所有注意力头都可区分。 |
Why 2 | 为什么用确定性的连续函数(如正余弦)而不直接学一张可训练矩阵? | ① 参数量固定(与序列长度无关);② 训练前即具备区分能力,不依赖梯度学习;③ 函数在任意 $n$ 位置都可解析计算,天然支持 长度外推。 |
Why 3 | 为什么选用多频率正余弦,而不是单一频率或线性编码? | 多频率叠加使不同维度呈不同波长:低频捕捉长程关系,高频捕捉局部差异;任何两位置间的相对距差 Δ \Delta Δ 都可由相位差表达,便于模型通过内积直接推断“距离”。 |
Why 4 | 为什么要让编码在内积空间里可线性推导相对位移? | Transformer 的核心运算是点积注意力;若 ⟨ P E ( i ) , P E ( j ) ⟩ \langle PE(i), PE(j)\rangle ⟨PE(i),PE(j)⟩ 的模式与 i − j i-j i−j 相关,则模型无需额外参数就可推断相对位置信息,梯度传播路径更短、更稳。 |
Why 5 | 为什么强调“函数型”编码而非离散查表对未来重要? | 随着大模型走向长上下文(10 k—1 M token),查表式位置嵌入的存储/插值成本迅速膨胀;函数型编码可 O(1) 延伸到任意长度,并能被改写成旋转、斜率等高效变体,匹配硬件 Friendly 的 Flash‑Attention 等新算子。 |
Transformer 采用 sin、cos,因为这俩的取值范围是 [-1, 1]。
句子: "我请客!"
语义矩阵(词向量),假设 6 维语义变化:
[
[0.2, -0.3, 0.5, 0.1, -0.4, 0.2], // "我"
[0.4, 0.1, -0.2, 0.3, 0.2, -0.1], // "请"
[-0.1, 0.4, 0.3, -0.2, 0.1, 0.3], // "客"
[0.3, 0.3, 0.1, 0.2, -0.1, -0.2] // "!"
]
位置矩阵(维度i使用sin/cos函数):
[
// pos=0 ("我"的位置)
[sin(0/10000^0/6), // i=0, 用sin
cos(0/10000^1/6), // i=1, 用cos
sin(0/10000^2/6), // i=2, 用sin
cos(0/10000^3/6), // i=3, 用cos
sin(0/10000^4/6), // i=4, 用sin
cos(0/10000^5/6)], // i=5, 用cos
// pos=1 ("请"的位置)
[sin(1/10000^0/6),
cos(1/10000^1/6),
sin(1/10000^2/6),
cos(1/10000^3/6),
sin(1/10000^4/6),
cos(1/10000^5/6)],
// pos=2 ("客"的位置)
[sin(2/10000^0/6),
cos(2/10000^1/6),
sin(2/10000^2/6),
cos(2/10000^3/6),
sin(2/10000^4/6),
cos(2/10000^5/6)],
为什么 Transformer 更关注的是序列中「相对位置」的差(distance),而不仅仅是绝对位置?
“我喜欢吃苹果” vs “苹果我喜欢吃”
- 两句话中"喜欢"和"吃"的相对距离都是1(相邻)
- 但它们的绝对位置不同(第2/3位 vs 第3/4位)
- 句意相近,说明相对位置差更重要
“我不喜欢吃苹果” vs “苹果我不喜欢吃”
- "不"和"喜欢"的相对位置关系决定了否定含义
- 它们具体在句子第几个位置反而不那么重要
因此,对于理解语言的语法结构和语义关系,相对位置差往往比绝对位置更关键。
任意两个位置p1和p2的sin差:
sin(p1) - sin(p2) = 2 * sin((p1-p2)/2) * cos((p1+p2)/2)
这个公式说明:
- 位置差(p1-p2)可以通过简单的向量运算得到
- 不需要专门存储"位置1距离位置2有多远"这样的信息
这个公式神奇地把"两个位置点各自的正弦值之差"转化成了"位置差的正弦值",使模型能直接学习到相对距离关系。
sin值相近时cos值往往相差较大(如sin(1)≈sin(2)但cos(1)≠cos(2)),反之亦然,这种互补性让任意两个位置的编码都更容易被区分开。
让我用"我请客"这个例子来说明sin和cos的互补性:
对三个位置的编码[sin值, cos值]:
"我"(p=0): [sin(0)=0, cos(0)=1]
"请"(p=1): [sin(1)=0.841, cos(1)=0.540]
"客"(p=2): [sin(2)=0.909, cos(2)=-0.416]
观察:
- "请"和"客"的sin值很接近(0.841≈0.909),但它们的cos值差异巨大(0.540和-0.416)
- 这就保证了即使sin值相似,整体的位置编码也能明显区分开
这就像给每个字两个不同角度的特征,更不容易认错。
双重特征让位置编码更可靠,就像用两个摄像头从不同角度拍同一个物体,大大降低了认错的可能性。
为什么把偶数维度和奇数维度区分开,用两种正交的波形函数?
sin(x) = sin(x)
cos(x) = sin(x + π/2)
就像两个人走路:
- 一个从起点开始走(sin)
- 一个提前走了四分之一圈(cos)
- 这样永远不会走到相同的位置
梳理总结:位置编码设计思路
让我们逐层深入分析位置编码采用sin/cos函数的原因:
- 表面现象:
- 观察到简单的线性位置编码(如1,2,3或归一化到[-1,1])在长序列中表现不佳
- 模型难以准确捕捉词语间的相对位置关系
- 特别是在长文本中,位置信息的表达变得模糊
- 第一层分析:
- 线性编码在长序列中,相邻位置的差值变得极小
- 例如:在1万字的文本中,归一化后第1个和第2个词的位置差可能只有0.0001
- 这种微小差值在神经网络计算中容易被忽略或淹没
- 第二层分析:
- 位置编码需要同时满足两个看似矛盾的要求:
- 能表达绝对位置(第几个词)
- 能表达相对距离(词间距离)
- 线性编码无法同时很好地满足这两点
- sin/cos函数的周期性质可以同时编码这两种信息
- 第三层分析:
- 神经网络的本质是在高维空间中寻找模式和关系
- sin/cos函数提供了一种在高维空间中保持距离的编码方式
- 通过不同频率的sin/cos组合,可以唯一确定每个位置
- 这种编码方式在数学上有良好的性质(平滑、连续、有界)
- 第四层分析:
- Transformer中的注意力机制是基于点积运算的
- sin/cos编码具有优雅的数学性质:任意位置差的编码可以通过简单的线性变换得到
- 这使得模型能够轻松学习相对位置关系,而不是死记硬背绝对位置
- 第五层分析(根本原因):
- 语言本身具有多尺度的结构特性:
- 局部关系(相邻词)
- 中程依赖(句子内)
- 长程依赖(段落间)
- sin/cos函数通过其周期性和不同频率的组合,自然地匹配了这种多尺度特性
- 这是一个数学优雅性和语言本质特性的完美结合
总结与解决方案:
根本原因是语言的多尺度结构特性要求位置编码同时具备局部精确性和全局可区分性。
偶数维度使用sin,奇数维度使用 cos 位置编码是目前最优雅的解决方案,因为它:
- 保持了数值稳定性(有界在[-1,1])
- 提供了多尺度的位置信息(通过不同频率)
- 支持高效的相对位置计算
- 与注意力机制数学上完美兼容
替代方案可能包括:
- 学习式位置编码
- 相对位置编码
- 层次化位置编码
但这些方案都还没有显著优于sin/cos编码的综合表现。
为什么Transformer 语义信息 + 位置信息,还能分别表示语义信息和位置信息?
其实位置信息是固定不变的,就是输入文本不同,但不管什么文本对应的位置都是固定的。
第 1 个 Token 的位置编码是根据公式(偶数维度使用sin,奇数维度使用 cos ) :
[ PE ( p o s , 2 i ) = sin ( p o s 10000 2 i d m o d e l ) , PE ( p o s , 2 i + 1 ) = cos ( p o s 10000 2 i d m o d e l ) , ] [ \text{PE}(pos, 2i) = \sin\left(\frac{pos}{10000^{\frac{2i}{d_{model}}}}\right), \quad \text{PE}(pos, 2i+1) = \cos\left(\frac{pos}{10000^{\frac{2i}{d_{model}}}}\right), ] [PE(pos,2i)=sin(10000dmodel2ipos),PE(pos,2i+1)=cos(10000dmodel2ipos),]
来计算的,其中 (pos) 表示第几个位置,(i) 表示向量维度中的下标。
对“第一个位置”来说 (pos=1),因此它的向量值在所有输入序列的第一个位置上都是一样的 —— 这正是位置编码“该有”的一致性:它告诉模型“这是第 1 个 Token”。
而由于每个 Token embedding(词向量)会随上下文、词汇不同而变化,并且与第 1 个位置的那一份“固定位置编码”线性相加,这样就形成了一个混合向量:“既包含这个词的语义信息,也包含它是在第 1 个位置的位置信息”。
因此,“第一个位置的编码永远固定”并不妨碍网络去理解它就是“第一个位置”。正是因为它固定,模型才更容易捕捉到这是句子的开头。
也就是说,第 2…n 个位置编码都是固定的,位置信息就是个常量。
理解上下文
为什么计算样本与样本之间的相关性,本质就是计算向量与向量之间的相关性?
一旦样本被映射到向量空间,模型对样本的全部可见信息就仅剩向量;后续任何相似性、注意力权重、检索评分、聚类距离……实质都在比较这组向量。
原始数据已“隐去”,意义通过向量几何关系体现,这就是“本质”所在。
点积兼顾方向(语义)与模长(重要性),单次操作即可产出可微分的标量。
多头注意力、层叠堆叠仍是“点积相关性”的扩展:
-
多头:在不同子空间里 重复“点积→Softmax”,捕获多种语义关系。
-
层堆叠:每层都把输出再作为下一层的输入向量,新的相关性比较仍然是 点积。
-
跨模态(文本-图像 CLIP):各模态先映射到同一向量空间后,互相关性依旧用点积度量。
为什么选择点积?
- 其他方法在表达力上可与点积旗鼓相当,甚至更强;
替代度量 | 额外操作 | 典型场景 | 难点 |
---|---|---|---|
Additive (MLP) | tanh + 线性 |
早期小型 NMT | 参数多、吞吐低 |
欧氏/余弦 | 减法、范数、sqrt | 一些对范数敏感的对比学习 | 广播成本高 |
双线性 (qᵀWk ) |
再乘一矩阵 | 图网络关系建模 | 缓存 & 带宽 |
核函数 (Gaussian) | exp , 幂次 |
理论探究 | 数值/速度问题 |
- 然而 吞吐-能耗-内存 成本大,且不能利用现成的硬件内核,自然难以在百亿、千亿参数规模复制点积的成本优势
Why 层级 | 追问 & 解释 | 同层对比:若不用点积会怎样? |
---|---|---|
Why-1 任务层 |
为什么注意力需要一个“相关性度量”? 因为模型必须知道 Query-Key 之间的匹配强度,才能按权重汇聚 Value,形成上下文敏感表示。 |
所有注意力形式都需要度量,问题只是选哪一种。 |
Why-2 理论层 |
为什么用 向量 上的度量而不是手工规则? 嵌入空间可学习、可微,允许梯度端到端调整度量,使语义关系随着任务自动塑形。 |
手工启发式(如 n-gram overlap)不可微,难以随层深度动态调整,性能和泛化受限。 |
Why-3 选择层 |
为什么在众多可微度量里首选“点积”而不是欧氏距离、MLP、双线性等? 点积自然兼容 方向+幅度 信息,且与矩阵乘法同构,可一次性并行算完全部 QK 配对。 |
- 欧氏/余弦:需显式范数或减法操作,多一步广播或 sqrt,吞吐下降。 - Additive(Bahdanau):额外 1 层 MLP,参数量↑、缓存↑、延迟↑。 |
Why-4 硬件层 |
为什么硬件友好如此重要? Transformer 需要在每一层对 n×n 配对做度量; 全球工业规模训练(n≈512–4 k)若度量耗时翻倍,训练预算翻几倍。 点积可用成熟 GEMM 核心融合 Softmax,实现常数级加速。 |
- 双线性 (q⊤Wk):多一次矩阵•矩阵乘法;若 W 尺寸大,缓存冲突导致带宽瓶颈。 - 高斯核/多项式核:需要 e^(·) 或幂次操作,显著慢且数值不稳。 |
Why-5 梯度层 |
为什么说点积梯度更稳定? 点积仅含一次乘法,梯度范围与输入成正比; 对比 距离 = q-k² 会把梯度反向放大 2 倍,深层易爆; MLP 度量依赖 ReLU/GEGLU,额外激活易饱和,长序列不划算。 |
Transformer 堆叠几十甚至上百层,梯度传播稳定性直接决定能否训练超深模型。 Transformer 选择点积,并非“只有这一条路可走”,而是在大规模深层堆叠场景里,点积以最小工程开销给出了足够的表示能力与梯度稳定性。 |
语义关系学习,就是做矩阵相乘 = 点积?
矩阵乘法本质是,一堆并行的点积。
但 “语义关系学习” 远不只是这一次乘法:它包括把离散符号映射成向量、用损失函数驱动梯度更新、叠加非线性与残差,以及整个参数空间的优化过程。
矩阵乘法只是用来读取已经学到的语义,而 “学习” 发生在不断更新那些生成 Q、K 的权重时。
矩阵乘法是语义的“读出器”,不是“创造者”。
在已训练的 Transformer 前向路径里,语义相关性分数就是 一次矩阵乘法 = 大量并行点积。
真正的语义学习发生在反向传播塑造向量空间的过程中。
句子:"我请客"
假设词向量(简化为2维):
"我" = [2, 1]
"请" = [1, 3]
"客" = [0, 2]
计算"请"和其他词的关注度:
关注度 = 向量点积 = 矩阵乘法
"请"对"我"的关注度:
[1, 3] · [2, 1] = 1×2 + 3×1 = 5
"请"对"客"的关注度:
[1, 3] · [0, 2] = 1×0 + 3×2 = 6
这说明:
- 向量越相似(方向越接近),点积越大
- 点积大意味着关注度高
- 通过矩阵乘法,自动找到了语义相关的词
本质上:矩阵乘法(点积)衡量了向量的相似度,相似度高就意味着关注度高。
QKV 的计算
为什么这几块 W 矩阵能承载语义?
计算相关性时,不会直接使用原始特征矩阵并让它与转置矩阵相乘
因为我们渴望得到的是语义的相关性,而非单纯数字上的相关性。
先在原始特征矩阵的基础上乘以一个解读语义的 w w w 参数矩阵,以生成用于询问的矩阵Q、用于应答的矩阵K以及其他可能有用的矩阵。
w w w是神经网络的参数,是由迭代获得的,因此 w w w会依据损失函数的需求不断对原始特征矩阵进行语义解读
先用可学习矩阵 W 把原始向量投影到“语义子空间”,再在这个子空间里做点积——这样 related 的 token 会“方向一致”, unrelated 的会“方向分离”,模型就能用简单、可并行的数学操作读出语义相关性。
这就是 自注意力机制的核心。
Why 层级 | 追问 & 回答 | 关键点 |
---|---|---|
Why-1 | 为什么不能直接用原始嵌入 E E E 做点积? | E E E 同时包含词义、词形、上下文位置等多种因素,且这些因素的权重对不同 token 并不等价; 直接做点积往往把“频繁共现”或“词长”等浅层特征放大,而压制真正重要的上下文线索。 |
Why-2 | 为什么一次可学习线性投影能缓解这个问题? | 线性映射相当于在原空间中选取—并加权—若干“关注维度”。 只要矩阵 W W W 可训练,它便能把与目标任务相关的方向放大、无关方向压缩,从而让后续点积主要比较“对任务有用”的分量。 |
Why-3 | 为什么要把同一嵌入拆成三套 ( Q , K , V ) (Q,K,V) (Q,K,V) 而不是一套? | Query 与 Key 需要在同一度量空间相互对照,而 Value 只负责被加权汇聚。 分头训练的 W Q , W K W_Q, W_K WQ,WK 让 “询问视角” 与 “应答视角” 可以各自强调不同语义维度; W V W_V WV 则决定最终写回的信息类型。 |
Why-4 | 为什么线性映射就够,而不必一开始就上 MLP 或 CNN? | - 线性层已可表达子空间选择与旋转,对局部几何关系足够灵活; - 参数量 = d 2 =d^2 =d2 可控,便于深度堆叠; - 若先用非线性,梯度难稳定穿越数十层注意力,加剧训练难度。 |
Why-5 | 为什么训练后说这些矩阵“语义化”了? | 反向传播会持续微调 W W W 以最小化任务损失。 沿着梯度方向,网络把“能减小损失的向量方向”保留下来——这些方向恰恰对应 “在当前任务里语义上相近就该靠近,语义上无关就该远离” 的几何关系。 因此 Q , K Q,K Q,K 空间最终对齐了任务语义。 |
So 层级 | 因此怎样 | 现实含义 |
---|---|---|
So-1 | 因此,通过微调 W Q , W K , W V W_Q,W_K,W_V WQ,WK,WV,一句话在不同上下文里会投影到不同方向。 | 同一单词在“银行/river bank”与“银行/financial bank”下生成的 Q , K Q,K Q,K 向量方向不同,注意力可以正确 disambiguate。 |
So-2 | 因此,改变损失函数即可“重塑语义空间”。 | 若用对比学习或检索型损失, W W W 会把语义距离刻画得比翻译任务更精细,直接提升下游检索效果。 |
So-3 | 因此,多头注意力 = 在并行子空间里学习多种语义投影。 | 不同头可能聚焦同义词、句法依赖、实体共指等异质关系,实现“多视角”语义捕获。 |
So-4 | 因此,可在保持点积核不变的前提下提升语义质量——途径是改进投影而不是改点积。 | RoPE、Q-former、LoRA-Attention 等方法本质都在对 W W W(或其低秩分量)施加结构性改造。 |
So-5 | 因此,跨模态模型也沿用同一范式。 | CLIP 把文本嵌入投影到与图像嵌入共享的子空间;两模态都用 dot-product,但语义对齐完全由各自的 W W W 训练而成。 |
用 3 句话先把整件事说清楚:
- 投影(W):把原始词向量“旋转-拉伸”一下,得到 问句版 (Q)、答句版 (K) 和 信息版 (V)。
- 匹配(Q·Kᵀ):在这个新坐标系里做点积,方向越接近 → 语义越相关。
- 学习(梯度):模型做错题,梯度就轻推 W,微调那次“旋转-拉伸”的角度;来回多次,直到点积真的反映语义而不是随机数字。
换一种生活化类比
阶段 | 类比 | 真实操作 |
---|---|---|
① 贴标签 | 把超市商品贴 3 类标签: 🔍“我想找什么” (Q)、 🔑“我是什么” (K)、 📦“我包含的信息” (V) |
q = W Q e , k = W K e , v = W V e \mathbf{q}=W_Q\mathbf{e},\; \mathbf{k}=W_K\mathbf{e},\; \mathbf{v}=W_V\mathbf{e} q=WQe,k=WKe,v=WVe |
② 扫码配对 | 收银机扫 购物清单 (Q) 和 货架条码 (K),条码越匹配 → 叮~越响 | 点积 → Softmax |
③ 装袋汇总 | 声音大(权重高)的商品装进购物袋 (V) | z = ∑ α v \mathbf{z}=\sum \alpha\mathbf{v} z=∑αv |
④ 店长改标签 | 顾客抱怨买错了💢 → 店长修改贴纸颜色/形状(调整 W) | 反向传播更新 W |
结论:
- 贴标签 (W) 定义了“怎么比、比什么”。
- 扫码声 (点积) 只是把标签的相似度读出来。
- 店长调标签 (梯度) 让下一次扫码更符合顾客真正想买的东西——这就是“语义化”。
卡片 | 你要记住的关键词 |
---|---|
W:语义滤镜 | 把杂乱信息筛到只剩“任务相关的一面”。 |
Q·Kᵀ:读出器 | 方向同 → 相关;方向不同 → 不相关。 |
梯度:调焦旋钮 | 每做错一次,就轻轻转动,让下次更准。 |
把这三步连起来就是整个自注意力的“贴标签 → 扫码 → 调焦”循环。
把文本 -> 矩阵
输入 4 段文本 X:
1. 风过,云逝,衔觞,乐志。
2. 花开,夜半,风过,天寒,跨驹上鞍,流离天涯之远,只为那美的不凡。
3. 自然的美,永远那么壮阔,又那么细腻,那么令人陶醉。
4. 苍山如海,残阳似血
- 分词/Tokenization
- 目的:把每条文本切成若干 token(字粒度、词粒度或 BPE 等方式),并且让每条文本最终拥有相同的 token 数(例如设为 16)。
- 对上面的 4 条文本,如果不足 16 个 token,就用
[PAD]
补齐;如果超出 16,就截断只取前 16 个 token。
这时,就有了一个形状为 [ 4 , 16 ] [4, 16] [4,16] 的「索引矩阵」:
- 4:批量大小(batch_size=4),因为有 4 条文本
- 16:序列长度(seq_len=16),对齐后每条文本 16 个 token
举例:假设第二条文本「花开,夜半,风过,天寒,跨驹上鞍…」经过分词后变成
[花, 开, ,, 夜, 半, ,, 风, 过, 天, 寒, 跨, 驹, 上, 鞍, …]
之类,若不够 16 就补[PAD]
。
- 嵌入/Embedding
- 目的:为每个 token 提供一个 512 维向量(或其它维度),也就是 d model = 512 d_{\text{model}} = 512 dmodel=512。
- 得到的三维张量形状是 [ 4 , 16 , 512 ] [4, 16, 512] [4,16,512]:
- 第 1 维:4 条文本
- 第 2 维:每条文本 16 个 token
- 第 3 维:每个 token 的隐层表示(embedding) 512 维
我们把这个结果称作 X \mathbf{X} X。这是喂给 Transformer 注意力层之前的输入。
4 段文本 X 变成了, 一个 3 维矩阵 — [ 4 , 16 , 512 ] [4, 16, 512] [4,16,512] 。
问题:Embedding 输出的维度为什么要是 [batch_size, seq_len, embedding_dim]?
- Embedding 就是一个把“索引”映射到“可训练向量”的过程。
- 我们经常处理一批(batch)句子,每个句子里多个 token。
- 因此输出形状需要对齐:对每个 batch、对句子中的每个 token、都有一个 embedding 向量,训练中会被不断更新。
大厂面试题:Transformer 的输入向量维度是怎么计算的?
- 输入 token 序列的维度: [ b a t c h , s e q l e n ] [batch, seqlen] [batch,seqlen](4, 16)
- Embedding 后的维度: [ b a t c h , s e q l e n , d model ] [batch, seqlen, d_{\text{model}} ] [batch,seqlen,dmodel]
乘以权重矩阵,生成 Q、K、V
- Q:查询向量,表示“这个 token 想找什么”。
- K:键向量,表示“这个 token 被找时的标志”。
- V:值向量,表示“这个 token 真正可以提供的内容”。
每份文本 X(矩阵形式)分别和 3 个矩阵相乘。
- ( Q , K , V ) (Q, K, V) (Q,K,V) 的来源:分别由 ( X ) (\mathbf{X}) (X) 乘以三个权重矩阵 ( W q , W k , W v ) (W_q, W_k, W_v) (Wq,Wk,Wv)。
- 权重矩阵初始化是随机的,在原始Transformer论文中使用的是Xavier均匀分布初始化,参数更新随训练更新
- 每个权重矩阵形状都是 ( [ 512 , 512 ] ) ([512,512]) ([512,512]),和 [ 16 , 512 ] [16, 512] [16,512] 矩阵相乘
- 因为 X 是 4 维,也就是重复四次 [ 16 , 512 ] ∗ [ 512 , 512 ] [16, 512] * [512,512] [16,512]∗[512,512]
- 所以输出的 Q、K、V 跟 X 的形状一样,都是 ( [ 4 , 16 , 512 ] ) ([4,16,512]) ([4,16,512])。
假设句子里有“风过、云逝、衔觞、乐志、花开…”这些 token,各自对应向量。
乘以 W q W_q Wq 后,得到的 Q \mathbf{Q} Q 是“查询向量”;乘以 ( W k ) (W_k) (Wk) 得到的 K \mathbf{K} K 是“键向量”;乘以 W v W_v Wv 得到的 V \mathbf{V} V 是“值向量”。
虽然含义不同,但三者形状都等于 [ 4 , 16 , 512 ] ) [4,16,512]) [4,16,512])。
- 投影到 Q
- 假设 [风过] 这个 token 在 X \mathbf{X} X 里是第 5 个位置,它是一个 512 维向量。
- 乘以 W q \mathbf{W_q} Wq 后,它得到的 Query 向量,就告诉模型:
“[风过] 这个词现在想重点关注哪方面的信息?(比如时空场景、动作语义、修辞氛围……)”
- 投影到 K
- [风过] 同样乘以 W k \mathbf{W_k} Wk 得到 Key 向量,表示:
“外部若要来检索我[风过],我的特征标签在哪些方面?我能被别人 match 到哪种语义上?”
- [风过] 同样乘以 W k \mathbf{W_k} Wk 得到 Key 向量,表示:
- 投影到 V
- [风过] 再乘以 W v \mathbf{W_v} Wv 得到 Value 向量,表示:
“如果有人对我很感兴趣,那我能给出的内容是什么?我有多少‘风过’这一词的实际信息可提供给对方?”
- [风过] 再乘以 W v \mathbf{W_v} Wv 得到 Value 向量,表示:
尽管含义不一样,但它们的数值都是由同一个 X 通过不同的投影矩阵得到,输出维度一致是为了后续注意力运算能够顺利匹配。
多头切分:从多个子空间去关注,从而提升对序列关系的捕捉力
为什么要多头?
之所以做多头,而不是把 512 维一次性做注意力,是因为多头能让模型并行关注不同维度的特征。
- 每个 Head 的 64 维向量可以专注于不同的语义模式或位置关系。
- 比如,在「花开,夜半,风过,天寒…」这条句子里:
- 第 1 个 Head 可能重点捕捉“事件顺序”
- 第 2 个 Head 可能关注“夜晚/寒冷”这类时空意象
- 第 3 个 Head 可能聚焦“风过 与 花开 之间”的上下文关联
- ……
- 还有,能充分利用 GPU 资源,并行计算
这样,每条句子的每个 token,就不再是“纯粹的 512 维向量”,而是被分解成 8 份、更小更易操作的 64 维向量。
每个头只需在 64 维的小空间里“搜索关联”,模型容易学到更细致、更多样的关系。
对它们分别做 Q×K、Softmax、再加权 V,能让模型并行地从不同角度理解句子中 token 之间的关系,提升表达能力与效果。
最后把 8 个 Head 的结果合并,能得到更丰富、更全面的上下文表示。
- 将 Q、K、V 拆分成多个头,在句子内部做“匹配-加权-融合”,最后输出回 [ 4 , 16 , 512 ] [4,\,16,\,512] [4,16,512]。
- 每个 token 便可获得来自同句内其他 token 的信息,形成上下文敏感的表示。
- 多头拆分 (Multi-Head)
- 将 Q , K , V \mathbf{Q}, \mathbf{K}, \mathbf{V} Q,K,V 的最后一维 (512) 平均拆成若干个头(head),比如 8 头或 4 头,每头尺寸 (64) 或 (128)。
- 原维度 ([4,16,512]) 拆分后,形状变成 [ 4 , 16 , 8 , 64 ] [4,\,16,\,8,\,64] [4,16,8,64] — 512 维向量,分成了 8 组,每组 64 维
- 再在实现里做一个维度转置为 [ 4 , 8 , 16 , 64 ] [4,\,8,\,16,\,64] [4,8,16,64],这个转置是纯粹为了编程和矩阵运算便利,并没有改变数据本质,只是换了张量的排列方式。
为什么只用 Q、K、V 最后一个维度?
前两个维度 [ 4 , 16 ] [4, 16] [4,16] 反映“有多少条句子”以及“每句有多少 token”。
最后一个维度,则是“每个 token 的向量维度”,用来存储各种语义信息。
2. 计算相似度 (Q × K)
- 每个头都会拿某个 token 的 Q 与句子内所有 token 的 K 逐一点积,得到一个注意力分数矩阵(大小 [16,16] 对单个句子)。
- 例如在「花开,夜半,风过,天寒…」这句里,第 5 个 token 是 [风过],模型会与 [花开]、[夜半]、[天寒]… 等做相似度,看谁更相关。
假设每条文本都截断/补齐到 16 个 token,那在某个头的注意力计算里,就会生成一个 [ 16 , 16 ] [16,16] [16,16] 的分数矩阵。
例如,对第 2 条文本:
行 = [花开] [,] [夜半] [,] [风过] [天寒] [跨驹上鞍] [流离天涯之远] [只] [为] [那] [美] [的] [不凡] [PAD]
… (16个)
列 = [花开] [,] [夜半] [,] [风过] [天寒] [跨驹上鞍] [流离天涯之远] [只] [为] [那] [美] [的] [不凡] [PAD]
… (16个)
句子内 token 间的相似度矩阵,每个 token 对每个 token 计算相似度:
行是Q \ 列是K | 花开 | ,(1) | 夜半 | ,(2) | 风过 | 天寒 | 跨驹上鞍 | 流离天涯之远 | 只 | 为 | 那 | 美 | 的 | 不凡 | [PAD] | … |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
花开 | 0.062 | 0.045 | 0.079 | 0.030 | 0.055 | 0.073 | 0.038 | 0.084 | 0.034 | 0.060 | 0.059 | 0.071 | 0.053 | 0.088 | 0.038 | 0.091 |
,(1) | 0.036 | 0.089 | 0.061 | 0.042 | 0.095 | 0.022 | 0.065 | 0.060 | 0.058 | 0.053 | 0.049 | 0.077 | 0.038 | 0.089 | 0.062 | 0.106 |
夜半 | 0.029 | 0.032 | 0.087 | 0.041 | 0.078 | 0.073 | 0.045 | 0.086 | 0.056 | 0.089 | 0.044 | 0.071 | 0.047 | 0.057 | 0.053 | 0.081 |
,(2) | 0.048 | 0.051 | 0.059 | 0.105 | 0.038 | 0.066 | 0.040 | 0.081 | 0.052 | 0.061 | 0.078 | 0.033 | 0.041 | 0.073 | 0.112 | 0.061 |
风过 | 0.073 | 0.052 | 0.041 | 0.058 | 0.071 | 0.022 | 0.103 | 0.046 | 0.076 | 0.062 | 0.032 | 0.087 | 0.054 | 0.057 | 0.025 | 0.084 |
天寒 | 0.034 | 0.081 | 0.024 | 0.064 | 0.026 | 0.099 | 0.039 | 0.062 | 0.074 | 0.067 | 0.059 | 0.038 | 0.040 | 0.068 | 0.106 | 0.118 |
跨驹上鞍 | 0.059 | 0.045 | 0.053 | 0.071 | 0.034 | 0.068 | 0.064 | 0.029 | 0.065 | 0.072 | 0.071 | 0.029 | 0.071 | 0.055 | 0.088 | 0.126 |
流离天涯之远 | 0.038 | 0.056 | 0.067 | 0.049 | 0.052 | 0.078 | 0.043 | 0.088 | 0.059 | 0.053 | 0.031 | 0.068 | 0.072 | 0.045 | 0.073 | 0.080 |
只 | 0.046 | 0.078 | 0.052 | 0.062 | 0.068 | 0.039 | 0.031 | 0.049 | 0.101 | 0.056 | 0.057 | 0.050 | 0.074 | 0.033 | 0.087 | 0.119 |
为 | 0.034 | 0.070 | 0.039 | 0.056 | 0.090 | 0.029 | 0.042 | 0.065 | 0.027 | 0.076 | 0.056 | 0.067 | 0.049 | 0.048 | 0.115 | 0.137 |
那 | 0.071 | 0.034 | 0.054 | 0.068 | 0.037 | 0.071 | 0.033 | 0.056 | 0.045 | 0.079 | 0.025 | 0.064 | 0.060 | 0.048 | 0.079 | 0.106 |
美 | 0.063 | 0.057 | 0.079 | 0.042 | 0.050 | 0.045 | 0.068 | 0.046 | 0.077 | 0.058 | 0.072 | 0.036 | 0.044 | 0.070 | 0.057 | 0.076 |
的 | 0.052 | 0.073 | 0.028 | 0.069 | 0.063 | 0.047 | 0.083 | 0.038 | 0.067 | 0.051 | 0.076 | 0.035 | 0.067 | 0.046 | 0.096 | 0.055 |
不凡 | 0.029 | 0.036 | 0.079 | 0.067 | 0.045 | 0.088 | 0.052 | 0.026 | 0.054 | 0.071 | 0.064 | 0.068 | 0.042 | 0.065 | 0.070 | 0.074 |
[PAD] | 0.062 | 0.041 | 0.061 | 0.081 | 0.047 | 0.074 | 0.045 | 0.088 | 0.059 | 0.036 | 0.070 | 0.052 | 0.029 | 0.091 | 0.035 | 0.080 |
… | 0.039 | 0.058 | 0.061 | 0.066 | 0.075 | 0.024 | 0.091 | 0.061 | 0.040 | 0.084 | 0.057 | 0.062 | 0.052 | 0.068 | 0.075 | 0.087 |
这个矩阵的结果,是数字,经过 softmax
变化(数字变百分比)。
表格中每个数值越大,表示行 token 越关注(或越匹配)列 token;越小则越不关注。
Q * K 矩阵相乘后,下一步是 Scale
,就是公式的 d k \sqrt{d_{k}} dk
大厂面试题:transformer 的 Q * K 矩阵相乘后,为什么要除以 d k \sqrt{d_{k}} dk?
- 限制内积的数值范围,避免数值过大:当向量维度增大时,如果直接做点积(dot-product),数值会变得非常大,导致 Softmax 输出的梯度变小,进而影响训练的稳定性。
- 便于梯度传递,防止梯度消失或爆炸:若不对点积作缩放,输入到 Softmax 的数值过大,会使得 Softmax 的梯度容易变得极小或极大,对模型的训练稳定性不利。对注意力分数进行缩放有助于在反向传播时,使梯度在合理范围内传递。
- 在自注意力中的效果更佳:原论文指出,如果不做缩放,点积会在特定情况下产生较大的数值差异,从而影响注意力机制的效果。缩放后,无论是从稳定性还是从收敛速度上,都更有优势。
这是为了对点积的结果做“标准化”,保证不同维度下都能得到合适的注意力权重,从而稳定训练并提升模型表现。
- 表面现象:
在实践中观察到,如果不除以(\sqrt{d_k}),Transformer模型的训练效果会变差,具体表现为:
- 模型收敛速度变慢
- 训练不稳定
- 最终效果次优
- 第一层分析:
直接原因是点积值过大导致的Softmax函数饱和:
- 当输入值很大时,Softmax会输出接近1或0的极端值
- 这使得注意力权重分布过于"尖锐"
- 模型难以有效区分不同token之间的相对重要性
- 第二层分析:
为什么点积值会过大?这与向量维度(d_k)有关:
- 根据概率论,假设Q和K的各个分量是独立同分布的随机变量
- 点积本质是多个随机变量的和
- 根据中心极限定理,这个和的方差会随维度(d_k)线性增长
- 因此维度越高,点积值的方差就越大
- 第三层分析:
为什么维度需要这么高?这涉及到模型的表达能力:
- 较高的维度让模型能够捕捉更丰富的特征
- 更大的参数空间有助于学习复杂的模式
- 但这种能力提升带来了数值稳定性的挑战
- 第四层分析:
追溯到更深层的原因是注意力机制的设计选择:
- 选择点积而不是其他相似度度量(如cosine)
- 使用Softmax进行归一化
- 采用多头并行计算的架构
这些选择都是为了计算效率和模型表现的权衡。
- 第五层分析:
最根本的原因是深度学习中普遍存在的维度诅咒与数值稳定性之间的矛盾:
- 需要高维来提供足够的表达能力
- 高维会带来数值不稳定问题
- 这是一个普遍存在的结构性问题
总结与解决方案:
- 根本原因是高维度下点积运算的数值稳定性问题
- 除以(\sqrt{d_k})是一个简单有效的解决方案,因为:
- 理论上可以将点积的方差控制在合理范围
- 实现简单,计算开销很小
- 不影响模型的表达能力
- 其他可能的解决方案:
- 使用其他相似度度量(如cosine similarity)
- 采用更复杂的归一化策略
- 动态调整缩放因子
但这些方案都会增加计算复杂度或引入额外的超参数,因此除以(\sqrt{d_k})成为了最佳实践。
之后,还有 Mask,意思是遮挡。
为什么要有 Mask?
- 屏蔽
[PAD]
等无效符号
- Padding Mask:在批处理时,句子长度不同,需要用
[PAD]
补齐到相同长度。 - 但是
[PAD]
这些位置对语义并没有贡献,不应让模型去“注意”它们。 - 因此会对 Q×K 结果中对应
[PAD]
位置的分数进行屏蔽(设为极小值,如 -∞),这样在 Softmax 后这些位置的注意力就几乎为 0。
- 只看「过去」或「已知」信息
- Causal Mask(或 Look-Ahead Mask):在 GPT 等自回归模型里,第 (i) 个词只能看到前 (i) 个词,不能看到“将来”的词。
- 为此,会对 Q × K T \mathbf{Q}\times\mathbf{K}^\mathsf{T} Q×KT 矩阵中 列 > 行 的位置进行屏蔽,让模型无法关注未来 token。
行是Q \ 列是K | 花开 | ,(1) | 夜半 | ,(2) | 风过 | 天寒 | 跨驹上鞍 | 流离天涯之远 | 只 | 为 | 那 | 美 | 的 | 不凡 | [PAD] | … |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1. 花开 | 1.00 | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf |
2. ,(1) | 0.70 | 0.30 | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf |
3. 夜半 | 0.20 | 0.50 | 0.30 | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf |
4. ,(2) | 0.10 | 0.10 | 0.30 | 0.50 | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf |
5. 风过 | 0.15 | 0.05 | 0.30 | 0.20 | 0.30 | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf |
6. 天寒 | 0.05 | 0.10 | 0.20 | 0.25 | 0.20 | 0.20 | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf |
7. 跨驹上鞍 | 0.10 | 0.05 | 0.10 | 0.25 | 0.10 | 0.20 | 0.20 | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf |
8. 流离天涯之远 | 0.10 | 0.10 | 0.05 | 0.15 | 0.05 | 0.20 | 0.15 | 0.20 | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf |
9. 只 | 0.10 | 0.05 | 0.10 | 0.10 | 0.05 | 0.10 | 0.10 | 0.20 | 0.20 | -inf | -inf | -inf | -inf | -inf | -inf | -inf |
10. 为 | 0.05 | 0.05 | 0.10 | 0.10 | 0.15 | 0.15 | 0.10 | 0.10 | 0.05 | 0.05 | -inf | -inf | -inf | -inf | -inf | -inf |
11. 那 | 0.05 | 0.05 | 0.05 | 0.10 | 0.10 | 0.15 | 0.10 | 0.10 | 0.05 | 0.05 | 0.10 | -inf | -inf | -inf | -inf | -inf |
12. 美 | 0.05 | 0.05 | 0.05 | 0.05 | 0.10 | 0.10 | 0.10 | 0.10 | 0.05 | 0.05 | 0.10 | 0.10 | -inf | -inf | -inf | -inf |
13. 的 | 0.06 | 0.06 | 0.06 | 0.06 | 0.06 | 0.06 | 0.06 | 0.06 | 0.06 | 0.06 | 0.06 | 0.06 | 0.28 | -inf | -inf | -inf |
14. 不凡 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.35 | -inf | -inf |
15. [PAD] | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf | -inf |
16. … | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.05 | 0.25 |
- 加权 V
- 得到分数后,通过 Softmax 变成加权系数,再对对应位置的 V 进行加权求和。
- 如果 [风过] 与 [天寒] 的相关性更强,那么最终输出里 [风过] 的向量会更多地融入 [天寒] 这个词的 Value 信息。
- 拼接各头输出
- 8 个头(或其它数目)并行计算后,将结果 concat 回去形成 [ 4 , 16 , 512 ] [4,16,512] [4,16,512] 的输出,供后续网络使用。
QKV 输出
花开,夜半,风过,天寒,跨驹上鞍,流离天涯之远,只为那美的不凡。
Q*K 是
V 是
Q * K * V 是
其他
残差连接、Dropout
1. 残差连接位置:
编码器层中:
Input → [Multi-Head Attention → Dropout] → Add & LayerNorm → [FFN → Dropout] → Add & LayerNorm
解码器层中:
Input → [Masked Multi-Head Attention → Dropout] → Add & LayerNorm
→ [Cross Attention → Dropout] → Add & LayerNorm
→ [FFN → Dropout] → Add & LayerNorm
2. 主要位置和作用:
A. 子层内部的Dropout:
- 位于注意力层输出后
- 位于FFN层输出后
- 作用:防止过拟合,提高模型鲁棒性
B. 残差连接位置:
- 每个子层的输入和输出之间
- Add & LayerNorm结构中的Add部分
- 作用:
- 缓解梯度消失问题
- 允许信息直接传递
- 帮助训练更深的网络
3. 具体流程:
# 以编码器中的一个子层为例
def sublayer(x):
# 1. 子层计算(注意力或FFN)
sublayer_output = self.sublayer(x)
# 2. Dropout
sublayer_output = self.dropout(sublayer_output)
# 3. 残差连接
residual_sum = x + sublayer_output
# 4. LayerNorm
output = self.layer_norm(residual_sum)
return output
4. 重要细节:
- Dropout率通常设为0.1
- 残差连接要求子层输入输出维度相同
- 训练时开启Dropout,推理时关闭
- LayerNorm在残差连接之后立即应用
5. Dropout的其他使用位置:
- 词嵌入层之后
- 位置编码加入之后
- 最终输出层之前
这种设计确保了:
- 信息可以无损传递(通过残差)
- 避免过拟合(通过Dropout)
- 保持数值稳定(通过LayerNorm)
归一化 Layer NorMalization
Why(为什么) | So-what(带来什么价值) |
---|---|
1. 多层残差叠加导致激活分布层层漂移 | 梯度更稳定 → 可把 Transformer 深度从 <10 层提升到几百层 |
Layer Normalization 在Transformer中的作用是:
- 将每个位置的特征归一化到相同的数值范围,这样不同层、不同位置的特征可以在相似的尺度上参与计算,让训练更稳定、收敛更快。
Layer Normalization(LN)= 对每个样本自身各特征做标准化 + 学习到的伸缩 (γ) / 平移 (β) → 与 batch 大小和序列长度无关 → 解决内部协方差偏移,允许 Transformer 在任意 batch 上稳定、快速、深层训练。
调用:
nn.LaryerNorm(bias = True) # 均值为 0,方差为 1
- 直观理解:
样本: [1, 2, 100, 3]
经过LN后: [-0.8, -0.6, 1.5, -0.1] # 归一化到均值0方差1
- 主要作用:
- 缓解梯度消失/爆炸
- 加速训练收敛
- 提高模型稳定性
- 与Batch Norm的区别:
BatchNorm: 跨样本做归一化(不适合NLP)
[样本1] : [1, 2, 3]
[样本2] : [4, 5, 6]
↓ ↓ ↓
对每列归一化
LayerNorm: 对单个样本做归一化(适合NLP)
[样本1] : [1, 2, 3] → 归一化
[样本2] : [4, 5, 6] → 归一化
本质:保证每一层的输入分布稳定,就像给每个样本"调成合适的音量"。
调参与排障:你最可能遇到的 7 个坑
调参口诀:ε=1e-4、γ=1、warm-up 长、多试大学习率。
现象 | 99 % 原因 | 快速验证 | 修复方案 |
---|---|---|---|
训练 loss 震荡、学习率再低也不收 | ε 太小 | ε=1e-5→1e-3 | 设 ε=1e-4,或改 RMSNorm |
深层梯度爆炸 | Post-LN 叠残差 | grad norm ↑↑ |
改 Pre-LN;或 SkipInit |
推理精度掉 | 推理时 µ,σ 误用动量均值 | 打印 µ,σ 分布 | LN 不需全局统计,直接用即时值 |
int8 量化丢精度 | γ、β 量化损失 | 打印 γ/β max-min | 将 γ 融合进权重再量化;β 进 bias |
多 GPU SyncBN 速度慢 | LN 写在 FusedBiasGELU 之后 | Profile op graph | 调整 op 顺序 / 选 Triton kernel |
语音模型 loss 搜天 | 特征维度不均衡 | 画特征方差 | 对梅尔谱 / pitch 分别 LN |
微调报 NAN | γ/β 冻结 + 低精度混训 | Check fp16 overflow | 解冻 γ/β 或切 fp32 accumulate |
Softmax:数字转预测概率
调用方式:
torch.softmax()
每一个文字都有一个 512 维度的权重向量。
像现在的大模型基本是 32K 上下文(ctx_length),如果输入文本是 100K,就是一个滑动窗口(32K)+ 1(滑动一步)。
这个上下文长度主要是算力制约,谷歌大模型有 100 万上下文,但那个会漏掉 40% 的内容。
大模型推理策略
- 场景选择:
- 需要确定性答案:使用Greedy或Beam Search
- 需要创造性文本:使用Top-P或Top-K配合适当的Temperature
- 需要多样化输出:调高Temperature或使用采样方法
- 参数调优:
- Beam width:通常2-10,取决于计算资源
- Top-K:通常20-100
- Top-P:通常0.9-0.95
- Temperature:通常0.7-1.0
Greedy Search:主流推理方式,一个字一个字的出,计算最快
- Greedy Search (贪心搜索)
- 原理:每一步都选择概率最高的下一个词
- 特点:
- 最简单直接的方法
- 计算速度最快
- 确定性输出:相同输入总是得到相同输出
- 缺点:
- 容易陷入局部最优
- 生成的文本可能单调重复
- 缺乏多样性
def greedy_search(logits):
return torch.argmax(logits, dim=-1)
Beam Search:需要全部推理完成,才能输出
- Beam Search (束搜索)
- 原理:维护k个最可能的序列,每步都为每个序列探索最可能的下一个词
- 参数:beam width (束宽) - 维护的候选序列数量
- 特点:
- 比贪心搜索考虑更多可能性
- 仍然是确定性的
- 可以生成多个候选答案
- 缺点:
- 计算开销较大
- 生成的文本仍然趋于保守
- 可能产生重复的输出
def beam_search(model, input_ids, beam_width=4):
sequences = [([], 0)] # (序列, 得分)
for _ in range(max_length):
candidates = []
for seq, score in sequences:
logits = model(seq)
next_token_probs = F.softmax(logits[:, -1], dim=-1)
top_k_probs, top_k_tokens = torch.topk(next_token_probs, beam_width)
for prob, token in zip(top_k_probs, top_k_tokens):
candidates.append((seq + [token], score + torch.log(prob)))
sequences = sorted(candidates, key=lambda x: x[1], reverse=True)[:beam_width]
return sequences
Top-K Sampling
- Top-K Sampling
- 原理:从概率最高的K个词中随机采样下一个词
- 参数:K值 - 候选词的数量
- 特点:
- 引入随机性,增加多样性
- 可以控制随机程度
- 避免低概率词被选中
- 缺点:
- K值难以选择
- 在不同位置使用固定的K值可能不合适
def top_k_sampling(logits, k=50):
top_k_probs, top_k_indices = torch.topk(F.softmax(logits, dim=-1), k)
# 重新归一化概率
top_k_probs = top_k_probs / top_k_probs.sum()
# 随机采样
next_token = torch.multinomial(top_k_probs, num_samples=1)
return top_k_indices[next_token]
Top-P Sampling
- Top-P (Nucleus) Sampling
- 原理:选择累积概率超过P的最小集合进行采样
- 参数:P值 - 概率阈值(通常0.9-0.95)
- 特点:
- 动态调整候选集大小
- 在不同位置自适应采样范围
- 平衡多样性和质量
- 缺点:
- 计算略微复杂
- 需要排序操作
def top_p_sampling(logits, p=0.9):
probs = F.softmax(logits, dim=-1)
sorted_probs, sorted_indices = torch.sort(probs, descending=True)
cumsum_probs = torch.cumsum(sorted_probs, dim=-1)
# 找到累积概率超过p的位置
mask = cumsum_probs <= p
mask[0] = True # 至少保留一个token
filtered_probs = sorted_probs * mask
filtered_probs = filtered_probs / filtered_probs.sum()
# 随机采样
next_token = torch.multinomial(filtered_probs, num_samples=1)
return sorted_indices[next_token]
Temperature
- Temperature (温度)
- 原理:通过缩放logits来调整概率分布的"尖锐度"
- 参数:temperature值 - 控制随机性的程度
- T < 1:使分布更尖锐(更确定性)
- T > 1:使分布更平缓(更随机)
- T = 1:保持原始分布
- 特点:
- 简单且有效
- 可以与其他采样方法结合
- 连续可调
def temperature_sampling(logits, temperature=0.7):
logits = logits / temperature
probs = F.softmax(logits, dim=-1)
next_token = torch.multinomial(probs, num_samples=1)
return next_token

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐
所有评论(0)