news 2026/2/8 7:22:15

看一遍就懂-大模型架构及encoder-decoder详细训练和推理计算过程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
看一遍就懂-大模型架构及encoder-decoder详细训练和推理计算过程

看一遍就懂-大模型架构及encoder-decoder详细训练和推理计算过程


一、特殊Token的意思

不同模型架构的特殊token体系

BERT(Encoder-only,用于理解任务)

<CLS>:放在句首,用于分类任务,其输出向量代表整句语义 <SEP>:分隔符,用于句对任务(如问答、文本蕴含) <PAD>:填充符,用于batch内长度对齐 <MASK>:掩码符,用于预训练的完形填空任务

GPT(Decoder-only,用于生成任务)

<|endoftext|>:既是文档结束符,也用作句子间分隔符 <PAD>:填充符(但GPT很少用,因为生成任务不需要严格对齐)

T5(Encoder-Decoder,统一框架)

<pad>:填充 <eos>:句子结束符 <unk>:未知词 没有专门的<bos>,因为T5用任务前缀(如"translate English to German:")

现代Encoder-Decoder(如BART、mBART、mT5)

<s>:句子开始符(相当于<bos>) </s>:句子结束符(相当于<eos>) <pad>:填充符

我们的例子将使用经典的Seq2Seq符号体系

为了讲解清晰,我采用最经典、最直观的设定:

<bos>:Begin of Sequence,告诉decoder"我要开始生成了" <eos>:End of Sequence,告诉decoder"我生成完了,该停了" <pad>:Padding,在batch训练中让所有序列等长

二、完整训练流程的剧本(从数据准备到参数更新)

让我们以"今天天气很好"这个训练样本为主线,完整演绎一遍训练的全过程。

第一幕:数据预处理(演员准备上场)

原始训练样本
源语言(中文):今天天气很好 目标语言(中文):今天天气很好 # 这是个复述任务的例子
Token化(切词)
源序列:[今, 天, 天, 气, 很, 好] 目标序列:[今, 天, 天, 气, 很, 好]
添加特殊token(关键步骤!)

Encoder输入(源序列加结束符)

[今, 天, 天, 气, 很, 好, <eos>] 长度 = 7

为什么Encoder要加<eos>?因为这告诉模型"源句子到此结束了,没有更多信息了"。在翻译任务中,这个信号很重要,它让模型知道不要再期待更多源语言词汇。

Decoder输入(目标序列加开始符)

[<bos>, 今, 天, 天, 气, 很, 好] 长度 = 7

为什么Decoder输入要加<bos>?这是生成的"起始信号",就像告诉一个作家"请开始写作"。第一个词的生成需要一个初始上下文,<bos>就扮演这个角色。

训练标签(目标序列加结束符)

[今, 天, 天, 气, 很, 好, <eos>] 长度 = 7

为什么标签要加<eos>?因为我们要训练模型学会"什么时候停止生成"。最后一个时间步,模型应该预测<eos>而不是继续生成新词。

转换为ID(查词表)

假设我们的词表(10个词)是:

词表 = { <pad>: 0, <bos>: 1, <eos>: 2, 今: 3, 天: 4, 气: 5, 很: 6, 好: 7, 的: 8, 是: 9 }

转换后:

Encoder输入 ID: [3, 4, 4, 5, 6, 7, 2] Decoder输入 ID: [1, 3, 4, 4, 5, 6, 7] 标签 ID: [3, 4, 4, 5, 6, 7, 2]

注意Decoder输入和标签的错位关系,这就是Teacher Forcing:输入是"已知的正确答案",让模型在每一步都基于正确历史来预测下一个词。


第二幕:Encoder的计算旅程(理解源句子)

场景1:Embedding层(把ID变成向量)
/* by 01130.hk - online tools website : 01130.hk/zh/pagecode.html */ # 假设embedding维度 d_model = 4 Embedding矩阵 W_emb ∈ ℝ^(10×4) # 词表大小10,每个词4维向量 # 查表得到向量(我简化一下数值) 今 (ID=3) → [0.8, 0.1, 0.3, 0.2] 天 (ID=4) → [0.2, 0.9, 0.1, 0.4] 天 (ID=4) → [0.2, 0.9, 0.1, 0.4] # 相同的词embedding相同 气 (ID=5) → [0.1, 0.2, 0.9, 0.1] 很 (ID=6) → [0.4, 0.3, 0.2, 0.8] 好 (ID=7) → [0.5, 0.4, 0.1, 0.6] <eos>(ID=2) → [0.3, 0.5, 0.6, 0.4] Encoder输入矩阵 X_enc^(0) = [7×4]

为什么要用embedding?因为神经网络不能直接处理离散的ID数字,必须转换为连续向量才能进行微分计算。Embedding本质是一个可学习的查找表,训练过程会让语义相近的词向量也接近。

场景2:位置编码(加入顺序信息)
/* by 01130.hk - online tools website : 01130.hk/zh/pagecode.html */ # 位置编码告诉模型"这是第几个词" Position Encoding PE ∈ ℝ^(7×4) pos=0 → [0.00, 1.00, 0.00, 1.00] pos=1 → [0.84, 0.54, 0.10, 0.99] pos=2 → [0.91, -0.42, 0.20, 0.98] pos=3 → [0.14, -0.99, 0.30, 0.95] pos=4 → [-0.76, -0.65, 0.39, 0.92] pos=5 → [-0.96, 0.28, 0.48, 0.88] pos=6 → [-0.28, 0.96, 0.56, 0.83] # 叠加到embedding上 X_enc^(0) = X_enc^(0) + PE # 逐元素相加

为什么需要位置编码?因为Self-Attention是置换不变的,它只看"谁和谁相关",不管顺序。但语言是有顺序的,"今天很好"和"很好今天"意思不同。位置编码用正弦波函数给每个位置一个独特的"身份标签"。

场景3:Self-Attention Layer 1(词与词互相理解)

现在进入第一层Self-Attention,这里没有mask,因为Encoder可以看到整个句子。

# 投影矩阵(简化为单位阵) W_Q = W_K = W_V = I_4 # 计算Q, K, V Q = X_enc^(0) × W_Q = X_enc^(0) [7×4] K = X_enc^(0) × W_K = X_enc^(0) [7×4] V = X_enc^(0) × W_V = X_enc^(0) [7×4] # 计算注意力分数 Scores = Q × K^T / √4 [7×7] # 这里是关键:Encoder的attention矩阵是全连接的! # 每个词都能看到所有其他词(包括自己) 今 天 天 气 很 好 <eos> 今 [0.82, 0.69, 0.69, 0.58, 0.61, 0.72, 0.64] 天 [0.69, 0.91, 0.91, 0.78, 0.82, 0.79, 0.81] 天 [0.69, 0.91, 0.91, 0.78, 0.82, 0.79, 0.81] # 两个"天"完全一样 气 [0.58, 0.78, 0.78, 0.88, 0.76, 0.71, 0.75] 很 [0.61, 0.82, 0.82, 0.76, 0.93, 0.84, 0.79] 好 [0.72, 0.79, 0.79, 0.71, 0.84, 0.94, 0.85] <eos> [0.64, 0.81, 0.81, 0.75, 0.79, 0.85, 0.87] # Softmax(每行归一化) Attention_Weights = softmax(Scores) [7×7] # 加权求和 Output = Attention_Weights × V [7×4]

这一步的意义是什么?每个词通过关注其他所有词,把句子的全局信息融合进来。比如"天气"这个词,会同时关注"今天"和"很好",理解这是在描述今天的天气状况。

场景4:Add & Norm(残差连接和归一化)
# 残差连接:把输入直接加到输出上 X_residual = X_enc^(0) + Output # Layer Normalization:让每个样本的均值=0,方差=1 X_enc^(1) = LayerNorm(X_residual) [7×4]

为什么要残差连接?防止深层网络的梯度消失,让原始信息能够"直通"到后面的层。就像高速公路的快车道,保证重要信息不会在传递过程中丢失。

场景5:Feed-Forward层(独立处理每个位置)
# 两层全连接网络,对每个token独立处理 FFN(x) = W2 × ReLU(W1 × x + b1) + b2 假设 W1: [4×16], W2: [16×4] # 中间扩展到16维 对每个位置计算: Output_FFN = FFN(X_enc^(1)) [7×4] # 再次 Add & Norm X_enc^(2) = LayerNorm(X_enc^(1) + Output_FFN)

为什么需要FFN?Self-Attention擅长建模全局依赖,但是线性的。FFN提供非线性变换能力,让模型能学习更复杂的模式。中间维度扩展(4→16→4)增加了表达能力。

场景6:堆叠多层(加深理解)

实际的Transformer会堆叠多层(如6层),每层都重复"Self-Attention + FFN"的结构。假设我们只有2层,那么:

# 经过第2层后得到最终的Encoder输出 H_enc = X_enc^(final) [7×4] 具体数值(这是最终经过所有层后的结果): dim0 dim1 dim2 dim3 今 [0.8, 0.1, 0.3, 0.2] 天 [0.2, 0.9, 0.1, 0.4] 天 [0.3, 0.8, 0.2, 0.3] 气 [0.1, 0.2, 0.9, 0.1] 很 [0.4, 0.3, 0.2, 0.8] 好 [0.5, 0.4, 0.1, 0.6] <eos> [0.4, 0.5, 0.5, 0.5]

Encoder的最终产出是什么?一个7×4的矩阵,每一行是一个源句子token的"深度语义表示",已经融合了全句的上下文信息。这个矩阵将作为"知识库"提供给Decoder。


第三幕:Decoder的生成征程(基于源句子生成目标)

场景1:Decoder的输入准备
Decoder输入序列 ID: [1, 3, 4, 4, 5, 6, 7] 对应token: [<bos>, 今, 天, 天, 气, 很, 好] # Embedding + 位置编码 X_dec^(0) = Embedding(Decoder输入) + PE [7×4]
场景2:Masked Self-Attention(只看过去)

这是Decoder的第一个attention层,与Encoder的区别就在于加了Causal Mask。

# 计算Q, K, V Q_dec = X_dec^(0) × W_Q [7×4] K_dec = X_dec^(0) × W_K [7×4] V_dec = X_dec^(0) × W_V [7×4] # 计算原始分数 Scores = Q_dec × K_dec^T / √4 [7×7] # 应用Causal Mask(设置上三角为-∞) Mask矩阵(下三角+对角线为1,上三角为0): [[1, 0, 0, 0, 0, 0, 0], [1, 1, 0, 0, 0, 0, 0], [1, 1, 1, 0, 0, 0, 0], [1, 1, 1, 1, 0, 0, 0], [1, 1, 1, 1, 1, 0, 0], [1, 1, 1, 1, 1, 1, 0], [1, 1, 1, 1, 1, 1, 1]] Masked_Scores = 把Mask为0的位置设为-∞ # Softmax后,未来位置的权重自动为0 Attention_Weights = softmax(Masked_Scores) [7×7] 示例(第2行"今"的权重分布): [0.45, 0.55, 0.00, 0.00, 0.00, 0.00, 0.00] ↑ ↑ ↑后面全是0 bos 今 # 输出 Output_self = Attention_Weights × V_dec [7×4]

为什么Decoder要mask?因为训练时我们是并行处理整个序列的,但预测时只能一个一个生成。Mask确保训练和推理的信息流向一致,防止"作弊"。

场景3:Cross-Attention(从Encoder借鉴知识)

这是Encoder-Decoder架构的核心!

# Query来自Decoder的当前状态 Q_cross = Output_self × W_Q [7×4] # Key和Value来自Encoder的输出! K_cross = H_enc × W_K [7×4] ← 这是Encoder的最终输出 V_cross = H_enc × W_V [7×4] # 计算注意力(这里没有mask,可以看Encoder的所有位置) Scores_cross = Q_cross × K_cross^T / √4 [7×7] ↑Decoder7个位置 ↑Encoder7个位置 Attention_Weights_cross = softmax(Scores_cross) [7×7] # 示例:Decoder位置1(预测"天")对Encoder各位置的关注 位置1的权重: [0.15, 0.25, 0.20, 0.10, 0.12, 0.10, 0.08] 今 天 天 气 很 好 eos # 加权求和 Output_cross = Attention_Weights_cross × V_cross [7×4]

Cross-Attention的本质是什么?Decoder用当前生成状态作为"问题"(Query),在Encoder的语义矩阵中"检索"相关信息(Key),提取出有用的内容(Value)。就像翻译时,生成每个目标词都会回头看源句子的不同部分。

场景4:Feed-Forward + Add & Norm
# 和Encoder一样的结构 Output_FFN = FFN(Output_cross) X_dec^(1) = LayerNorm(Output_cross + Output_FFN) [7×4] # 这是Decoder第一层的最终输出
场景5:堆叠多层后的最终输出

假设Decoder也是2层,最终得到:

H_dec = X_dec^(final) [7×4] # 这个矩阵的每一行代表Decoder在该位置的"条件生成状态"

第四幕:输出层与损失计算(评判对错)

场景1:投影到词表空间
# 线性变换:4维 → 10维(词表大小) W_vocab ∈ ℝ^(4×10) Logits = H_dec × W_vocab [7×10] # 每一行是一个位置对10个词的原始分数 位置0(输入<bos>,预测"今"): logits[0] = [0.21, 0.18, 0.15, 0.89, 0.54, 0.32, 0.41, 0.38, 0.25, 0.19] pad bos eos 今↑ 天 气 很 好 的 是
场景2:Softmax得到概率分布
Probs = softmax(Logits) [7×10] # 位置0的概率分布 P[0] = softmax(logits[0]) = [0.067, 0.065, 0.063, 0.131, 0.094, 0.076, 0.083, 0.080, 0.070, 0.065] pad bos eos 今↑ 天 气 很 好 的 是 13.1% # 全部7个位置的预测 位置0预测分布:P(今)=13.1%, P(天)=9.4%, ... 位置1预测分布:P(今)=8.2%, P(天)=15.3%, ... ← 应该预测"天" 位置2预测分布:... ... 位置6预测分布:P(<eos>)=18.5%, ... ← 应该预测结束符
场景3:计算交叉熵损失
# 标签(真实答案) Labels = [3, 4, 4, 5, 6, 7, 2] # 今,天,天,气,很,好,<eos> # 对每个位置计算损失 Loss_0 = -log(P[0, 3]) = -log(0.131) = 2.03 Loss_1 = -log(P[1, 4]) = -log(0.153) = 1.88 Loss_2 = -log(P[2, 4]) = -log(0.147) = 1.92 Loss_3 = -log(P[3, 5]) = -log(0.162) = 1.82 Loss_4 = -log(P[4, 6]) = -log(0.159) = 1.84 Loss_5 = -log(P[5, 7]) = -log(0.155) = 1.86 Loss_6 = -log(P[6, 2]) = -log(0.185) = 1.69 # 平均损失 Total_Loss = (2.03 + 1.88 + 1.92 + 1.82 + 1.84 + 1.86 + 1.69) / 7 = 1.86

为什么用交叉熵?它衡量预测分布和真实分布(one-hot)的距离。预测概率越高,损失越小。模型的目标就是最小化这个损失。


第五幕:反向传播与参数更新(学习提升)

损失对各层参数的梯度
# 梯度从输出层反向流动 ∂Loss/∂W_vocab → 更新词表投影矩阵 ↓ ∂Loss/∂H_dec → 流向Decoder最后一层 ↓ ∂Loss/∂(Decoder FFN参数) → 更新FFN ↓ ∂Loss/∂(Cross-Attention参数) → 更新W_Q, W_K, W_V ↓ ↓ ↓ ↓→ ∂Loss/∂H_enc → 流向Encoder! ↓ ∂Loss/∂(Masked Self-Attention参数) ↓ ∂Loss/∂X_dec^(0) → 流向Decoder embedding

关键洞察:Encoder会被Decoder的损失训练!虽然Encoder没有直接的监督信号,但Cross-Attention建立了桥梁,让生成任务的损失能反向传播到Encoder,督促它学习"对生成有用"的表示。

参数更新(随机梯度下降)
学习率 η = 0.001 W_vocab^(new) = W_vocab^(old) - η × ∂Loss/∂W_vocab Encoder参数^(new) = Encoder参数^(old) - η × ∂Loss/∂Encoder参数 Decoder参数^(new) = Decoder参数^(old) - η × ∂Loss/∂Decoder参数

一次训练步骤完成!模型会对成千上万个这样的样本重复这个过程,逐渐学会翻译、摘要、对话等任务。


三、特殊Token的深层作用机制

现在你已经看完了完整流程,让我深入解释特殊token为什么必不可少。

<bos>的三重作用

第一重:作为生成的起点。Decoder的第一个位置必须有一个输入,<bos>提供了一个"中性的起始上下文"。就像赛跑的发令枪,它本身没有语义,但给出了"开始"的信号。

第二重:在Self-Attention中充当锚点。第一个词(如"今")的Query会同时关注<bos>和自己,<bos>的embedding会影响第一个词的生成倾向。比如在对话任务中,不同的对话类型可能训练出不同的<bos>表示。

第三重:在Cross-Attention中引导初始检索<bos>位置的Query会去Encoder中检索信息,决定"从哪里开始翻译/生成"。在翻译任务中,模型可能学会让<bos>关注源句子的开头。

<eos>的三重作用

第一重(Encoder侧):标记源句子边界。告诉Encoder"句子结束了",在Self-Attention中,<eos>能汇聚全句信息,成为"句子级表示"。很多摘要任务会专门提取Encoder的<eos>向量作为句子摘要。

第二重(Decoder侧):学习何时停止。训练时最后一个位置必须预测<eos>,这教会模型"生成到合适长度就该停了"。没有这个训练,模型可能永远不停地生成。

第三重(推理时):实际停止信号。测试时,当模型输出<eos>,解码循环就终止。这是唯一能让模型自主决定生成长度的机制。

<pad>的作用(Batch训练专用)

假设batch中有两个样本:

样本1: [今, 天, 很, 好, <eos>] 长度=5 样本2: [明, 天, 会, 下, 雨, 吧, <eos>] 长度=7

为了并行处理,必须对齐到相同长度:

样本1: [今, 天, 很, 好, <eos>, <pad>, <pad>] 长度=7 样本2: [明, 天, 会, 下, 雨, 吧, <eos>] 长度=7

在Attention计算时,<pad>位置会被mask掉(设为-∞),确保它们不参与实际计算,只是占位符。在损失计算时,<pad>位置也不计入损失。


四、完整流程的理论意义总结

Encoder-Decoder为什么如此强大?

信息的双向流动:Encoder通过双向Attention理解源句子,Decoder通过Causal Attention保证生成的自回归性,Cross-Attention则让两者协同工作。

端到端可微:从输入token到输出概率的整个链条都是可微的,可以用一个统一的损失函数端到端训练,不需要分模块训练。

Teacher Forcing的妙处:训练时给Decoder"正确答案"作为输入,让它学习"基于正确历史的预测",避免了错误累积,大幅加速收敛。但推理时用自己的输出作为下一步输入(Autoregressive),这种训练-推理的不一致被称为Exposure Bias,是当前研究的热点。

与Decoder-only(GPT)的本质区别

GPT没有Cross-Attention,只有Masked Self-Attention。它把输入和输出拼接成一个序列:

[源句子, <sep>, 目标句子]

全部用Causal Mask处理。这更简单,但无法像Encoder-Decoder那样"先全面理解再生成",而是"边看边生成"。对于需要深度理解源文本的任务(如摘要、翻译),Encoder-Decoder理论上更有优势。


五、用一个完整的比喻收尾

想象你在参加一场即兴翻译大赛:

Encoder(理解阶段):你拿到一篇中文文章,仔细阅读每个句子,在纸上做标注,理解上下文,把关键信息提取成笔记。这个笔记就是H_enc

<bos>(开始翻译):主持人说"开始",你深吸一口气,准备说出第一个英文单词。

Decoder(生成阶段):你一边回看笔记(Cross-Attention),一边注意自己已经说出的英文(Masked Self-Attention),然后说出下一个单词。每个单词都基于"源文本理解+已生成历史"做决策。

<eos>(结束):你翻译完最后一个词,说"完毕",告诉评委你结束了。

损失函数(评分):评委对照标准答案,给你的每个词打分,算出总分。

反向传播(复盘):你根据评委反馈,调整自己的理解方式(Encoder)和表达策略(Decoder),下次做得更好。

Encoder-Decoder推理过程

开篇:训练与推理的本质差异

当你完全理解了Encoder-Decoder的训练过程后,可能会产生一个错觉,认为推理只是把训练过程重新跑一遍,喂入新数据就能得到结果。但实际上,推理过程和训练过程有着本质的不同,这种差异如此重要,以至于理解它是掌握序列生成模型的关键一跃。

在训练阶段,我们拥有"上帝视角"。我们已经知道正确答案是什么,所以可以把整个目标序列一次性喂给Decoder,让它在每个位置上并行地学习预测下一个词。这种并行化训练被称为Teacher Forcing,它就像一个严格的导师站在学生旁边,每走一步都告诉学生"正确答案应该是这个",让学生基于正确的历史去学习下一步。这种方式高效、稳定,能让GPU的并行计算能力得到充分利用。

但推理阶段是截然不同的故事。现在我们不知道答案,Decoder必须真正地一个词一个词地生成,每生成一个词都要立即把它作为下一步的输入,形成一个自回归的循环。这就像蒙着眼睛走钢丝,每一步都基于之前的步伐,一旦走错一步,后续的所有步骤都可能受到影响。这个过程是串行的、渐进的、不可回退的,完全依赖模型自己的判断。

让我用一个生动的比喻来说明这个差异。训练就像在驾校练车,教练坐在副驾驶上,握着一个备用方向盘。每当你要转向时,教练会告诉你"现在应该向左打30度",你只需要学习在看到路况时做出正确的判断。而推理则是你独自开车上路,没有教练,没有提示,你必须自己观察路况、做出决策、执行操作,而且每个决策都会影响接下来的路况。

现在让我们深入到推理的每一个步骤,看看这个"独自驾驶"的过程到底是如何展开的。

推理的起点:准备输入和初始化

假设我们已经训练好了一个中英翻译模型,现在要把中文句子"今天天气很好"翻译成英文。这是一个经典的Encoder-Decoder任务,让我们从头开始看这个推理过程。

首先,我们需要准备Encoder的输入。和训练时一样,我们把源句子进行分词,添加结束符,然后转换为ID序列。这个过程完全一致,因为Encoder的工作方式在训练和推理时没有任何区别。具体来说,我们得到的序列是"今、天、天、气、很、好、<eos>",转换为ID后是"3, 4, 4, 5, 6, 7, 2",假设我们的词表中这些词对应的ID就是这样。

接下来,Encoder开始工作。它把这个ID序列转换为embedding向量,每个词变成一个四维的向量。然后加上位置编码,让模型知道每个词在句子中的位置。这个加了位置编码的向量矩阵,形状是七行四列,送入Encoder的第一层。

Encoder的每一层都执行相同的操作:首先是Self-Attention,让每个词都能看到整个句子的所有其他词,互相理解彼此的语义和关系。比如第一个"天"会注意到它前面的"今",理解这是在说"今天"而不是"天空"。然后是Feed-Forward网络,对每个位置独立地进行非线性变换,提取更复杂的特征。经过残差连接和Layer Normalization后,输出被送入下一层。

假设我们的模型有六层Encoder,那么这个七行四列的矩阵会经过六次这样的处理。每经过一层,向量中蕴含的语义就更加丰富和抽象。到最后一层输出时,我们得到的矩阵我们称之为H_enc,它的每一行都是一个源语言词的深度语义表示,已经融合了整个句子的上下文信息。这个矩阵将作为"知识库",在整个生成过程中被Decoder反复查询。

这里有一个关键的认知:Encoder的计算在推理时只需要执行一次。无论Decoder后面要生成多少个词,Encoder都不需要重新计算。这个H_enc矩阵会被保存在内存中,供后续所有时间步使用。这是一个重要的优化点,因为Encoder的计算量可能相当大,如果每生成一个词都重算一次就太浪费了。

现在Encoder已经完成了它的使命,把源句子"理解透彻"了。接下来的重点转向Decoder,这才是推理阶段真正惊心动魄的部分。

第一步生成:从<bos>到第一个单词

Decoder的推理过程从一个特殊token开始,这就是<bos>,它代表"Begin of Sequence",是生成的起点。在训练时,我们可以一次性把整个目标序列喂给Decoder,但现在我们什么都没有,只有这个起始符号。这就像作家面对空白稿纸时的第一个字,充满了不确定性。

我们把<bos>(假设它的ID是1)转换为embedding向量,加上位置编码。这时位置索引是0,因为它是第一个位置。得到的向量是一个一行四列的矩阵,记为X_dec。这个向量送入Decoder的第一层。

Decoder的第一个子层是Masked Self-Attention。你可能会问,现在只有一个token,还需要attention吗?答案是需要的,虽然这一步的attention计算会很简单。具体来说,这个<bos>向量会和它自己计算attention,Query是它自己,Key也是它自己,Value还是它自己。计算出的attention score就是一个标量,做softmax后权重是1.0,加权求和的结果还是它自己。这看起来像是一个恒等变换,但保持这个流程的一致性很重要,因为后续步骤会变得复杂。

通过Self-Attention后,我们得到一个向量,它经过Add & Norm后送入第二个子层,这就是Cross-Attention。这是Decoder真正开始"查询"Encoder知识库的地方。当前的向量作为Query,被投影成一个查询向量。而Encoder的输出H_enc,那个七行四列的矩阵,被投影成Keys和Values。

现在进行的计算是:这个Query向量(一行四列)乘以Keys矩阵的转置(四行七列),得到一个一行七列的attention score向量。这个向量的每个元素代表Decoder当前状态对Encoder各个位置的关注程度。比如第一个元素表示对源句子第一个词"今"的关注度,第二个元素表示对第二个词"天"的关注度,以此类推。

这些分数经过缩放(除以维度的平方根)然后做softmax,变成一个概率分布。假设计算出来的权重分布是"0.12, 0.18, 0.15, 0.10, 0.13, 0.20, 0.12",这意味着在生成第一个英文单词时,模型最关注源句子中的"好"(权重0.20)和"天"(权重0.18)。这是有道理的,因为翻译"今天天气很好"时,英文很可能以"Today"或"The weather"开头,而这些都和源句子的这些词相关。

这个attention权重向量(一行七列)乘以Values矩阵(七行四列),得到一个context向量(一行四列)。这个向量是Encoder所有位置信息的加权混合,代表了"为了生成当前位置的词,从源句子中提取出的最相关信息"。

Context向量经过Add & Norm后送入Feed-Forward网络,再经过一次Add & Norm,完成Decoder第一层的计算。如果模型有六层Decoder,那么这个向量会经过六次"Masked Self-Attention → Cross-Attention → FFN"的循环,每一层都进一步精炼这个向量的语义。

最终,我们得到Decoder最后一层的输出,一个一行四列的向量,记为h_dec。这个向量包含了"基于源句子理解,应该生成什么样的第一个词"的所有信息。但它还不是一个词,而是一个高维语义向量。

现在到了关键的一步:把这个四维向量投影到词表空间。我们有一个输出投影矩阵W_vocab,形状是四行乘以词表大小(假设英文词表有10000个词)。向量h_dec(一行四列)乘以这个矩阵,得到一个一行10000列的logits向量。这个向量的每个元素对应一个英文单词的原始分数。

对这个logits向量做softmax,我们得到一个概率分布。假设最高的几个概率是:"Today"对应0.35,"The"对应0.28,"Weather"对应0.12,"It"对应0.08,其他词的概率都很低。这个分布告诉我们,模型认为最可能的第一个词是"Today",概率达到35%。

在最简单的推理策略中,我们选择概率最高的词作为输出,这叫做贪心解码(Greedy Decoding)。所以我们选择"Today"作为第一个生成的词。这个词的ID(假设是8888)会被记录下来,因为它马上要作为下一步的输入。

第一步生成完成了!我们用了一个<bos>符号,通过整个Decoder网络的计算,从10000个可能的英文单词中选出了"Today"。这个过程虽然复杂,但逻辑是清晰的:用起始信号触发生成,通过Cross-Attention查询源句子,通过Self-Attention整合已有信息(虽然现在只有一个token),最后输出一个词表概率分布并选择最优的词。

第二步生成:自回归的开始

现在进入推理的核心机制:自回归循环。我们已经生成了第一个词"Today",它不仅是输出的一部分,更重要的是,它要立即成为Decoder下一步的输入。这就是"自回归"的含义,每一步的输出都被回馈到输入中,形成一个闭环。

具体来说,我们现在的Decoder输入序列是"<bos>, Today",长度变成了2。我们把这两个词都转换为embedding,加上位置编码(<bos>的位置是0,"Today"的位置是1),得到一个二行四列的矩阵。这个矩阵送入Decoder的第一层。

在Masked Self-Attention阶段,计算方式和训练时完全一样。我们计算Query、Key、Value矩阵(现在都是二行四列),然后计算Q乘以K的转置,得到一个二行二列的attention score矩阵。这个矩阵的四个元素分别代表:位置0对位置0的关注、位置0对位置1的关注、位置1对位置0的关注、位置1对位置1的关注。

关键的Causal Mask在这里起作用。虽然我们现在只有两个词,但mask的逻辑依然适用:每个位置只能看到它自己和它之前的位置。所以attention矩阵的模式是:

bos Today bos [1.0, 0.0 ] Today [0.4, 0.6 ]

位置0(<bos>)只能100%关注自己,而位置1("Today")可以同时关注<bos>(比如40%)和自己(60%)。这个mask确保了信息的单向流动,防止"看到未来"。

Softmax后的attention权重乘以Value矩阵,得到输出向量。对于位置1("Today"),这个输出向量融合了<bos>和"Today"两个embedding的信息,是一个"条件化的语义表示"。

然后进入Cross-Attention。这里有一个非常重要的细节:虽然Decoder现在有两个位置,但Encoder的输出H_enc还是那个七行四列的矩阵,没有任何变化。每个Decoder位置都会独立地和整个Encoder做attention。

具体来说,位置1("Today")的Query向量会和Encoder的七个Key向量计算相似度,得到七个attention score。假设这次的权重分布是"0.08, 0.10, 0.12, 0.15, 0.25, 0.18, 0.12",你会发现模型现在更关注源句子中的"很"(0.25)和"好"(0.18)。这很合理,因为已经生成了"Today",接下来可能要描述天气状况,所以关注"很好"这部分信息。

Cross-Attention的输出(一个四维向量)经过FFN和多层堆叠,最终得到位置1的最终表示h_dec。注意,我们只需要这个位置1的向量,因为我们要预测的是第二个词。位置0的输出我们不关心,因为那对应的是已经生成过的<bos>的后续词,已经是历史了。

这个h_dec向量再次通过W_vocab投影到词表,做softmax得到概率分布。假设这次的最高概率是:"is"对应0.42,"the"对应0.25,"was"对应0.18。模型认为第二个词最可能是"is",所以我们选择它。

现在我们的生成序列变成了"Today is",这是一个合理的英文开头。但我们还没有结束,因为模型没有输出<eos>结束符。所以循环继续。

自回归循环的完整展开:从"is"到"<eos>"

理解了第二步,你就理解了推理的全部核心。后续的每一步都是完全相同的模式:把当前已生成的序列作为Decoder输入,通过整个网络计算,输出下一个词的概率分布,选择最优词,添加到序列末尾,再次循环。

让我详细展开第三步,帮你巩固这个理解。现在Decoder的输入是"<bos>, Today, is",三个词,转换为三行四列的embedding矩阵。在Masked Self-Attention中,每个位置的attention权重分布遵循causal pattern:

bos Today is bos [1.0, 0.0, 0.0] Today [0.3, 0.7, 0.0] is [0.2, 0.4, 0.4]

位置2("is")能看到所有之前的信息,它的Query会同时关注<bos>(20%)、"Today"(40%)和自己(40%)。这让"is"的表示融合了整个已生成序列的信息。

在Cross-Attention中,位置2的Query和Encoder的七个Key计算attention。假设这次的权重分布聚焦在源句子的"天气"(0.35)和"很好"(0.40)。模型已经生成了"Today is",现在要描述具体内容,所以关注点自然转向了"天气很好"这个核心语义。

经过所有层的计算后,位置2的输出向量投影到词表,假设概率分布显示"nice"是0.38,"good"是0.25,"sunny"是0.20。我们选择"nice",序列变成"Today is nice"。

第四步,输入变成四个词,Masked Self-Attention的矩阵变成四行四列,下三角都有值,上三角都是零。Cross-Attention中,新位置的Query可能开始关注源句子的"天"(天气)或"今"(今天),因为句子结构已经建立,现在可能需要补充细节。假设生成的词是"weather",虽然这让句子语法不完美(应该是"The weather is nice"),但这正是自回归的风险:一旦前面的词选择不完美,后续就可能需要调整来适应。

第五步可能生成"today",序列变成"Today is nice weather today"。你可能注意到"today"出现了两次,这在人类看来有些冗余,但模型在每一步都是基于概率做贪心选择,无法"回头修改"。

第六步,模型终于输出了<eos>,概率达到0.55,超过了所有其他词。这个特殊符号告诉我们:生成结束了。最终的翻译结果是"Today is nice weather today",虽然不完美,但表达了源句子的核心意思。

整个过程可以总结为一个while循环的伪代码:

generated_sequence = [<bos>] while True: decoder_input = generated_sequence encoder_output = H_enc (保持不变) decoder_hidden = Decoder(decoder_input, encoder_output) last_position_hidden = decoder_hidden[-1] # 只取最后一个位置 logits = last_position_hidden @ W_vocab probs = softmax(logits) next_token = argmax(probs) # 贪心选择 generated_sequence.append(next_token) if next_token == <eos> or len(generated_sequence) >= max_length: break return generated_sequence[1:] # 去掉<bos>返回

这个循环的每一次迭代都会让输入序列增长一个词,Decoder的计算量也会随之增加。如果最终生成了十个词,那么第十步的Masked Self-Attention就要处理一个十行十列的矩阵。这就是为什么推理速度会随着生成长度增加而变慢,因为每一步都比上一步多一点计算。

推理与训练的深层对比

现在你已经看完了完整的推理流程,让我们回过头来系统对比推理和训练的差异,这些差异不仅仅是技术细节,它们反映了序列生成任务的深层挑战。

第一个差异是并行性。训练时,我们可以把整个目标序列一次性喂给Decoder,利用Causal Mask确保信息流向的正确性,让GPU的数千个核心并行计算。假设目标序列长度是十,那么十个位置的loss可以同时计算,梯度可以同时反向传播。但推理时,我们必须一个词一个词地串行生成,第二个词依赖第一个词的输出,第三个词依赖前两个词,无法并行。这导致推理速度远慢于训练,尤其是生成长文本时,可能需要几秒甚至几分钟。

第二个差异是输入的来源。训练时,Decoder的每一步输入都是ground truth,即人工标注的正确答案。即使模型在某一步预测错了,下一步依然会用正确答案作为输入,这让模型能够"重置错误",专注于学习每个独立位置的正确预测。但推理时,每一步的输入都是模型自己上一步的输出。如果第三步生成了一个不太合适的词,第四步就必须基于这个不完美的历史继续生成,错误可能累积和放大。这种训练-推理的不一致性被称为Exposure Bias,是当前研究的热点问题。

第三个差异是确定性。训练时,损失函数是确定的,给定相同的输入和参数,计算出的梯度完全相同。但推理时,我们通常有多种解码策略可选。最简单的是贪心解码,每步选概率最高的词,这是确定性的。但还有随机采样,按照概率分布随机选词,每次运行可能得到不同结果。还有Beam Search,同时保留多个候选序列,选择全局概率最高的路径,这需要更多计算但通常质量更好。不同的解码策略会导致完全不同的输出,而在训练时没有这种选择的自由度。

第四个差异是停止条件。训练时,序列长度是已知的,我们有明确的标签告诉模型"这里应该结束"。但推理时,模型必须自己决定什么时候停下来。它通过预测<eos>符号来表达"我认为已经生成完整了"。如果模型训练不够好,可能永远不输出<eos>,导致无限循环。所以实际系统中通常会设置一个最大长度限制作为保险,比如"不管怎样,生成100个词后必须停止"。

第五个差异是计算图的动态性。训练时,整个计算图的形状是固定的,比如Decoder输入是十行四列,输出也是十行一万列(词表大小),整个前向传播和反向传播的张量形状都是静态的,可以高度优化。但推理时,每一步的输入长度都不同,第一步是一行四列,第二步是两行四列,第十步是十行四列。这种动态性让优化变得困难,也是为什么现代推理引擎(如vLLM、TensorRT-LLM)花了大量精力优化KV Cache等技术,来减少重复计算。

KV Cache:推理优化的关键技术

当你理解了推理的逐步生成过程后,你可能会发现一个巨大的浪费:每一步我们都要重新计算之前所有位置的Key和Value。

具体来说,在第三步时,我们有三个输入token(<bos>, Today, is),Decoder会计算三个Query、三个Key、三个Value。但其实<bos>和"Today"的Key和Value在第二步时已经算过了!它们的值不会改变,因为它们只依赖于自己的位置和内容,不依赖未来的token。

KV Cache的思想就是:把已经计算过的Key和Value存起来,后续步骤直接复用。具体来说,第一步我们计算并缓存一个Key和一个Value(都是四维向量),第二步计算新的一个Key和一个Value,拼接到缓存上,变成两个。第三步再计算一个,拼接上去,变成三个。这样每一步只需要计算新增位置的KV,而不需要重算整个历史。

这个优化在Cross-Attention中更加显著。Encoder的Key和Value矩阵在整个生成过程中完全不变,我们可以在第一步就计算好并缓存,后续所有步骤直接使用,完全不需要重算。这大幅减少了计算量。

在Self-Attention中,KV Cache的使用稍微复杂一些。新的Query需要和所有历史的Key计算attention,所以第十步的Query要和十个Key做点积。但由于我们缓存了之前九个Key,只需要计算新的第十个Key,然后把它和缓存拼接起来。这比重新计算十个Key要快得多。

KV Cache带来的加速是巨大的,尤其是对于长序列生成任务。但它也有代价:内存占用。假设模型有六层Decoder,每层都要缓存KV,序列长度是100,那么我们需要存储六层乘以100个位置乘以两个矩阵(K和V)乘以四维,总共4800个浮点数。对于大模型(如GPT-3的12288维),这个内存开销可能达到几个GB。这就是为什么大模型推理需要大显存,很大一部分都被KV Cache占用了。
后续提出的MQA,GQA,MLA都是为了缓解KV cache。

大家有兴趣可以看这位博主讲的:https://www.bilibili.com/video/BV1BYXRYWEMj/?spm_id_from=333.1387.favlist.content.click&vd_source=fc3f7e0bcc8a0bca7d86fc8c60c5db3c

Beam Search:超越贪心的搜索策略

贪心解码虽然简单快速,但它有一个明显的缺陷:它只看一步,不考虑长远影响。假设第一步选"Today"概率是0.35,选"The"是0.28,贪心会选"Today"。但可能"The weather is nice today"这个完整句子的联合概率,比"Today is nice weather"更高。贪心因为只看局部最优,错过了全局最优。

Beam Search是一种折中方案,它不是只保留一个候选序列,而是同时保留多个(比如五个),这个数量叫做beam size。具体流程是:

第一步,从词表中选出概率最高的五个词,比如"Today"(0.35)、"The"(0.28)、"It"(0.15)、"Weather"(0.12)、"Good"(0.10)。这五个词各自开启一条生成路径。

第二步,对每条路径分别往后生成一个词。"Today"路径可能扩展出"Today is"、"Today the"、"Today was"等,"The"路径可能扩展出"The weather"、"The sky"等。现在我们有五条路径各自扩展出若干候选,总共可能有几十个。我们计算每个候选的累积概率(通常是对数概率的和),然后只保留全局最高的五个。

比如经过第二步后,保留的五个可能是:"The weather"(-0.5)、"Today is"(-0.6)、"The sky"(-0.7)、"It is"(-0.8)、"Today the"(-0.9)。注意"Today"路径的其他扩展可能被淘汰了,因为全局得分不够高。

这个过程持续进行,每一步都保留全局最优的五条路径,直到所有路径都输出了<eos>或达到最大长度。最后,我们选择累积概率最高的那条路径作为最终输出。

Beam Search的优势是它能够"回头"。如果第一步选了"Today"但后续发展不好,它可以在后续步骤中逐渐被"The"路径赶超并淘汰。这让最终结果更可能是全局较优的。但代价是计算量增加了beam_size倍,因为每一步都要并行处理多条路径。

在实践中,beam size通常取3到10之间。太小则效果接近贪心,太大则计算成本过高且容易陷入重复模式(模型可能会生成"very very very good"这样的重复序列,因为每个"very"都让概率稍微提高一点)。

推理中的其他挑战与技巧

除了上面讨论的核心机制,推理还有很多细节和技巧值得了解。

温度(Temperature)采样是控制生成多样性的常用手段。在做softmax之前,我们可以把logits除以一个温度参数T。当T接近0时,概率分布变得非常尖锐,几乎所有概率都集中在最高分词上,生成变得确定性和保守。当T大于1时,概率分布变得平缓,低概率词也有机会被选中,生成变得更随机和创造性。在创意写作任务中,高温度可以产生意想不到的表达,而在翻译等需要精确性的任务中,低温度更合适。

Top-k采样是另一种常用技巧。它在采样时只考虑概率最高的k个词,把其他词的概率置零后重新归一化。这避免了极低概率的"异常词"被偶然选中,同时保持一定的随机性。Top-p(nucleus)采样则更加动态,它选择累积概率达到p(如0.9)的最小词集进行采样,词集大小会根据概率分布的形状自适应调整。

重复惩罚是对抗生成重复的技术。如果某个词已经在生成序列中出现过,我们可以在下一步预测时降低它的logit分数,让模型倾向于选择新的词。这在对话和故事生成中很有用,可以避免模型陷入"I think I think I think"这样的循环。

长度归一化在Beam Search中很重要。因为我们用对数概率的和作为得分,长序列的得分会自然地比短序列低(因为每一步都乘以一个小于1的概率)。为了公平比较不同长度的序列,我们通常会用总对数概率除以长度,或者用一个更复杂的长度惩罚公式。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/5 2:02:26

如何快速删除 Linux 中的海量小文件:告别rm命令的缓慢困境

在 Linux 系统中&#xff0c;当一个目录下积攒了数百万个小文件&#xff08;如缓存、会话文件或日志&#xff09;时&#xff0c;传统的 rm -rf * 命令会变得极其缓慢。这是因为 rm 需要对每个文件执行系统调用&#xff0c;并逐一更新文件系统的元数据。 rm 命令删除文件时&…

作者头像 李华
网站建设 2026/2/7 16:42:31

Claude Skills 保姆级教程:无脑照做就能用出效果

大家好&#xff0c;我是六哥。 Claude Skills 我也是上周一才知道有这么个东西&#xff0c;具体是什么完全没概念&#xff0c;想想还是自己知道的太晚了。 但说实话&#xff0c;这玩意成功的引起了我的好奇心&#xff0c;所以就有了这篇文章&#xff01; 没有所谓的方法论和废话…

作者头像 李华
网站建设 2026/2/7 15:37:52

YOLOv26自行车部件检测识别系统实现

1. YOLOv26自行车部件检测识别系统实现 1.1. 系统概述 近年来&#xff0c;随着智能交通和城市共享单车系统的快速发展&#xff0c;自行车部件检测与识别技术在车辆管理、故障检测和维护保养等方面发挥着越来越重要的作用。YOLOv26作为一种先进的实时目标检测算法&#xff0c;…

作者头像 李华
网站建设 2026/2/7 22:19:19

电子标签拣货系统:高效、智能的物流分拣解决方案

电子标签拣货系统电子标签拣货系统的核心是让货架上的指示灯告诉拣货员"往这儿拿"。想象一下仓库里几百个货位同时亮灯闪烁的场景&#xff0c;像不像科幻片里的数据流动特效&#xff1f;这套系统背后藏着几个关键技术点&#xff0c;咱们边写代码边唠。硬件驱动是地基…

作者头像 李华
网站建设 2026/2/5 19:56:31

基于深度学习的防化服检测系统

目录深度学习在防化服检测中的应用核心功能模块技术优化方向典型应用场景性能指标源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;深度学习在防化服检测中的应用 深度学习技术通过卷积神经网络&#xff08;CNN&#xff09;、目标检测模…

作者头像 李华