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
。
理解词语顺序:加一个位置编号,语义信息 + 位置信息
“他打我” 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 采用 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 1000 0 2 i d m o d e l ) , PE ( p o s , 2 i + 1 ) = cos ( p o s 1000 0 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 个位置编码都是固定的,位置信息就是个常量。
理解上下文
矩阵相乘 = 关注度
语义关系学习,就是做矩阵相乘。
句子:"我请客"
假设词向量(简化为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 的计算
把文本 -> 矩阵
输入 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] 。
大厂面试题: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] 的输出,供后续网络使用。
其他
残差连接、Dropout、Layer NorMalization
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在Transformer中的作用是将每个位置的特征归一化到相同的数值范围,这样不同层、不同位置的特征可以在相似的尺度上参与计算,让训练更稳定、收敛更快。
调用:
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] → 归一化
本质:保证每一层的输入分布稳定,就像给每个样本"调成合适的音量"。
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
标签:Transformer,512,16,位置,token,可视化,inf,一步一个脚印,sin
From: https://blog.csdn.net/qq_41739364/article/details/145105269