news 2026/4/15 7:20:04

彻底搞定transformer模型原理及代码!

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
彻底搞定transformer模型原理及代码!
👉学会后的收获:👈

• 基于大模型全栈工程实现(前端、后端、产品经理、设计、数据分析等),通过这门课可获得不同能力;

• 能够利用大模型解决相关实际项目需求:大数据时代,越来越多的企业和机构需要处理海量数据,利用大模型技术可以更好地处理这些数据,提高数据分析和决策的准确性。因此,掌握大模型应用开发技能,可以让程序员更好地应对实际项目需求;

• 基于大模型和企业数据AI应用开发,实现大模型理论、掌握GPU算力、硬件、LangChain开发框架和项目实战技能,学会Fine-tuning垂直训练大模型(数据准备、数据蒸馏、大模型部署)一站式掌握;

• 能够完成时下热门大模型垂直领域模型训练能力,提高程序员的编码能力:大模型应用开发需要掌握机器学习算法、深度学习框架等技术,这些技术的掌握可以提高程序员的编码能力和分析能力,让程序员更加熟练地编写高质量的代码。

👉获取方式:

😝有需要的小伙伴,可以点击文章最下方的微信名片添加免费领取【保证100%免费】🆓

  1. transformer背景

在自然语言处理领域,序列建模的核心挑战是捕捉长距离依赖关系。传统循环神经网络(RNN)及其变体(LSTM、GRU)通过隐藏状态传递信息,但存在严重的梯度消失问题。一文详解RNN及股票预测实战(Python)!

卷积神经网络(CNN)虽可并行计算,但需多层堆叠才能捕捉长距离依赖,且对序列顺序的建模能力较弱。一文弄懂CNN及图像识别(Python)

Transformer摒弃循环与卷积结构,基于注意力机制实现全局依赖建模,同时支持全并行计算,彻底解决了传统方法的核心痛点。它的出现是自然语言处理领域的突破,并为大模型架构(BERT、GPT等)奠定了模型基础。

1.2 Transformer的核心创新

注意力机制经历了从加性注意力(Bahdanau, 2015)到乘法注意力(Luong, 2015)的演进,最终在2017年Google团队《Attention is All You Need》中提出Transformer架构。其核心创新包括:

  • 自注意力机制:直接计算序列内所有位置的依赖关系,高效捕捉长距离关联;

  • 多头注意力:多子空间并行注意力计算,丰富特征表示维度;

  • 位置编码:通过正弦余弦函数注入序列顺序信息,弥补注意力机制的顺序无关性;

  • 编码器-解码器架构:模块化堆叠设计,兼顾特征编码与生成能力;

这些创新使Transformer在WMT 2014英德翻译任务中达到28.4 BLEU分数,较此前最优结果提升2+ BLEU,且训练效率提升数倍。

1.3. Transformer整体结构

Transformer是一种基于注意力机制的序列建模架构,核心目标是高效捕捉序列数据的长距离依赖关系,同时支持全并行计算。其整体结构分为**编码器(Encoder)解码器(Decoder)**两大模块,输入经词嵌入和位置编码预处理后,由编码器完成特征编码,解码器基于编码特征实现目标序列的自回归生成。

Transformer的核心组成可总结为“3大核心模块+2个预处理步骤+2个核心设计”,具体如下:

  • 预处理步骤:词嵌入(将文本token转换为固定维度的向量)、位置编码(注入序列顺序信息,解决注意力机制“顺序无关”问题);
  • 编码器模块:由N个相同的编码器层堆叠而成,每层含“多头自注意力层”和“前馈神经网络层”,核心作用是将输入序列转换为富含语义的上下文特征向量;
  • 解码器模块:由M个相同的解码器层堆叠而成,每层含“掩码多头自注意力层”“编码器-解码器注意力层”和“前馈神经网络层”,核心作用是基于编码器特征自回归生成目标序列;
  • 其他还有一些重要设计:残差连接(缓解深层网络梯度消失)、层归一化(稳定训练过程)。

如果不想探究太多细节,Transformer工作流程看如下Gemini总结的图就可以大概了解了!

  1. 输入文本→转换为词向量(词嵌入)→添加位置信息(位置编码); 2. 编码阶段:编码器通过“全局关注”输入序列的所有token,生成包含全局语义的特征向量; 3. 解码阶段:解码器先“关注自身已生成的token”(掩码注意力),再“关注编码器的特征向量”(编解码注意力),逐步生成目标序列; 4. 输出阶段:通过线性层和softmax将解码器输出转换为最终预测结果。

二、Transformer 技术原理详解

我们来通过一个文本翻译实例来简单了解 Transformer 是如何工作的:Transformer 由编码器和解码器两部分组成,首先向编码器输入一句话(原句),让其学习这句话的特征,再将特征作为输入传输给解码器。最后,此特征会通过解码器生成输出句(目标句)。

2.1 编码器结构与原理

编码器是 Transformer 的第一个核心组件,它的任务是将输入序列转换为一个语义丰富的上下文表示,后面再喂给后面的解码器。

编码器由N个相同的编码器层堆叠而成。每个编码器层包含两个子层:多头自注意力层、前反馈层。基本的流程比较简单,输入转为嵌入表示后,加入位置编码,然后输入到多头注意力层,再通过叠加归一化,再到前馈网络层。多个编码器的化依此类推。接下来逐个元素讲解下

2.1.1 多头自注意力层

多头自注意力层是编码器的最核心的组件,多头也就是多个自注意力机制。

自注意力机制简单说,就是让句子里的每个词计算和其他所有词的关系,从而更精准地计算自身特征。类似最终得到:每个单词=多少关系单词1+多少的单词2+…, 具体步骤如下

首先,生成查询、键、值矩阵

我们需要为输入序列中的每个元素生成三个向量:查询(Query, Q)、键(Key, K)和值(Value, V)。这三个矩阵是通过将输入矩阵 X 分别乘以三个不同的权重矩阵 WQ、WK、WV 得到的:

Q = X × WQ

K = X × WK

V = X × WV

这里的权重矩阵是通过模型训练学习得到的参数。

现在,让我们学习查询矩阵、键矩阵和值矩阵如何应用于自注意力机制。

要计算一个词的特征值,自注意力机制会使该词与给定句子中的所有词联系起来。还是以 I am good 这句话为例。为了计算单词 I 的特征值,我们将单词 I 与句子中的所有单词一一关联。

步骤1:Q×K点积——算出“词与词的匹配度”

首先计算查询矩阵Q和键矩阵K的点积。这个操作的核心目的是:衡量每个词和句子中所有词的相似度.

比如计算“I”的特征时,就用“I”的查询向量(Q_I)分别和“I”“am”“good”的键向量(K_I、K_am、K_good)做点积。点积结果越大,说明两个词的关联越紧密——比如Q_I和K_I的点积最大,就说明“I”和自己的关联最紧密。

同理,用“am”的查询向量匹配所有键向量,能知道“am”和其他词的相似度;用“good”的查询向量匹配,能得到“good”与其他词的关联程度。最终会得到一个“匹配度矩阵”,记录每个词和所有词的关联分数。

步骤2:除以√d_k——让梯度更稳定

第一步算出的匹配度分数可能会很大,容易导致后续训练时梯度不稳定(比如梯度爆炸)。所以要把整个匹配度矩阵除以“键向量维度(d_k)的平方根”。

步骤3:Softmax归一化——把匹配度变成“注意力权重”

经过步骤2的分数还没标准化,我们用Softmax函数处理一下,让每个词对应的一行分数都落在0~1之间,而且一行的总和是1。这一步会把“匹配度”变成“注意力权重”——权重越高,说明这个词越需要“关注”对应的那个词。

比如处理后,“I”对应的权重可能是[0.9, 0.07, 0.03],意思是:计算“I”的特征时,90%关注自己,7%关注“am”,3%关注“good”。

步骤4:权重×V——得到最终注意力特征

最后一步是用步骤3得到的“注意力权重矩阵”,乘以值矩阵V,得到最终的注意力特征矩阵。这个过程相当于:按权重整合所有词的“价值信息”,形成每个词的最终特征

假设计算结果如图

还是以“I”为例,它的最终特征就是:0.9×V_I(I的值向量)+ 0.07×V_am(am的值向量)+ 0.03×V_good(good的值向量)。

这有什么用呢?为了回答这个问题,让我们看一个例句:A dog ate the food because it was hungry(一只狗吃了食物,因为它很饿)。在这里,it 这个词表示 dog。我们将按照前面的步骤来计算 it 这个词的自注意力值。假设计算过程如图 1-19 所示。

图 1-19 单词 it 的自注意力值

从图 1-19 中可以看出,it 这个词的自注意力值包含 100% 的值向量(dog)。这有助于模型理解 it 这个词实际上指的是 dog 而不是 food。这也再次说明,通过自注意力机制,我们可以了解一个词与句子中所有词的相关程度。

现将自注意力机制的计算步骤总结如下:

  • 计算查询矩阵与键矩阵的点积,求得相似值,称为分数;
  • 除以键向量维度的平方根
  • 用 softmax 函数对分数进行归一化处理,得到分数矩阵
  • 通过将分数矩阵与值矩阵相乘,计算出注意力矩阵
2.1.2 前馈神经网络层

前馈神经网络层对序列中的每个位置独立地应用相同的两层全连接网络。这个网络的结构很简单:

FFN(x) = max(0, x × W₁ + b₁) × W₂ + b₂

其中,W₁和 W₂是权重矩阵,b₁和 b₂是偏置向量,max (0,・) 是 ReLU 激活函数。

这个网络的作用是为模型引入非线性变换能力,让模型能够学习更复杂的模式。需要注意的是,虽然叫 “前馈网络”,但它在整个结构中起到了非线性变换和特征增强的作用,是不可或缺的。

2.1.3 残差连接与层归一化

为了确保深层网络能够稳定训练,每个子层都采用了残差连接和层归一化技术。

残差连接的实现非常简单,就是将子层的输入直接叠加到输出上:

x = x + SubLayer(x)

这种设计可以让梯度直接通过网络,避免梯度消失问题。

层归一化则是对每个样本的所有特征维度进行标准化,使其均值为 0,方差为 1。层归一化的公式为:

LN(y) = γ × (y - μ) / √(σ² + ε) + β

其中,μ 和 σ² 是 y 在特征维度的均值和方差,γ 和 β 是可学习的缩放与偏移参数,ε 是防止除零的极小值。

2.1.4 位置编码

这里你可能会有疑问,自注意力机制不区分token顺序(把输入序列打乱,计算结果不变),但文本、时序等数据的顺序至关重要(比如“我吃苹果”和“苹果吃我”完全不同)。位置编码的作用就是“给每个token打一个唯一的位置标签”,将位置信息注入词嵌入,让模型知道token的顺序。

在将输入矩阵送入编码器之前,首先要将位置编码加入输入矩阵中,再将其作为输入送入编码器。

位置编码的核心思想是为序列中的每个位置生成一个唯一的向量表示,这个向量需要包含位置的顺序信息。Transformer 使用了一种基于正弦和余弦函数的位置编码方案,其公式为:

其中,pos 是位置索引,i 是维度索引,d_model 是模型的维度。

这种设计有几个重要的优点:

  1. 相对位置可表示:对于任意的偏移量 k,PE (pos+k) 可以表示为 PE (pos) 的线性函数,这使得模型能够学习到相对位置关系。
  2. 外推能力:即使遇到比训练时更长的序列,模型也能通过插值的方式生成相应的位置编码。
  3. 计算高效:位置编码可以预先计算,不需要在训练过程中学习。

在实现时,我们通常会创建一个大小为 (max_len, d_model) 的位置编码矩阵,其中每一行对应一个位置的编码。然后,将这个矩阵注册为模型的缓冲区(buffer),这样它就不会参与梯度更新。

词嵌入与位置编码的结合,这个过程可以表示为:

H = Embedding(X) + PositionalEncoding

需要注意的是,在 PyTorch 的实现中,词嵌入通常会乘以√d_model,这是为了让词嵌入和位置编码具有相同的方差,防止位置编码 “淹没” 原始的词向量信息。

最后,,编码器层的完整计算流程可以总结为:

  1. 输入叠加位置编码

  2. 经过多头自注意力层处理

  3. 输出与输入相加(残差连接)进行层归一化

  4. 经过前馈神经网络层处理

  5. 输出与输入相加(残差连接)

  6. 再次进行层归一化

2.2 解码器结构与原理

解码器的结构比编码器稍微复杂一些,它需要同时处理两个信息源:编码器的输出和已经生成的部分目标序列。

下图梳理下编码解码器整体的过程,编码器将原句的特征值(编码器的输出)作为输入传给所有解码器,而非只给第一个解码器。因此,一个解码器(第一个除外)将有两个输入:一个是来自前一个解码器的输出,另一个是编码器输出的特征值。

编码器和解码器

接下来,我们学习解码器究竟是如何生成目标句的。当时(表示时间步),解码器的输入是 ,这表示句子的开始。解码器收到 作为输入,生成目标句中的第一个词,即 Je,如图所示。

解码器在时的预测结果

同理,当时,解码器使用当前的输入和在上一步生成的单词,预测句子中的下一个单词。在本例中,解码器将 和 Je(来自上一步)作为输入,并试图生成目标句中的下一个单词,如图 所示。

解码器在时的预测结果

在每一步中,解码器都将上一步新生成的单词与输入的词结合起来,并预测下一个单词。因此,在最后一步(),解码器将 、Je、vais和 bien 作为输入,并试图生成句子中的下一个单词,如下图

解码器在时的预测结果

从中可以看到,一旦生成表示句子结束的 标记,就意味着解码器已经完成了对目标句的生成工作。

2.2.1 解码器层整体结构

每个解码器层由三个子层及对应的残差连接、层归一化组成,从输入到输出的流程为:

解码器输入+位置编码 → 带掩码的多头自注意力层 → 残差连接+层归一化 → 编码器-解码器注意力层 → 残差连接+层归一化 → 前馈神经网络层 → 残差连接+层归一化 → 解码器层输出

其中,三个子层各司其职:带掩码的多头自注意力层保障生成顺序性,编码器-解码器注意力层实现语义对齐,前馈神经网络层增强特征表达能力。

2.2.2 核心子层一:带掩码的多头自注意力层

该子层是解码器区别于编码器的核心组件之一,核心功能是处理目标序列前缀的内部依赖关系,同时通过掩码机制屏蔽未来位置信息,确保生成过程的单向性(训练与推理逻辑一致)。

(1)核心原理

自注意力机制的本质是通过“查询(Query, Q)-键(Key, K)-值(Value, V)”的交互的计算,得到每个位置的注意力权重,再通过权重对V进行加权求和,得到该位置的特征表示。而解码器的自注意力层需额外添加“下三角掩码”,将未来位置的注意力分数置为负无穷,经过Softmax后权重趋近于0,从而实现“无法偷看未来信息”的效果。

(2)输入与输出
  • 输入:目标序列前缀的词嵌入向量+位置编码(记为T,维度:[batch_size, 目标序列前缀长度, d_model])。例如生成“我想吃蛋炒饭”时,t=1输入“”(句子开始标记),t=2输入“ 我”,t=3输入“ 我 想”,以此类推;d_model为模型维度,原论文设为512。
  • 输出:经过内部注意力加权与掩码处理后的目标序列前缀特征(记为M,维度与输入一致)。
(3)具体计算步骤(结合示例)

以示例中t=3输入“ 我 想”(目标序列前缀长度=3)、d_model=512、多头注意力头数h=8为例,计算过程如下:

词嵌入与位置编码融合

将“”“我”“想”分别转换为512维词嵌入向量,再叠加位置编码(采用原论文的正弦余弦公式)。例如“我”的词嵌入向量为[0.1, 0.2, …, 0.4](512维),叠加位置编码(pos=1)后为[0.1+0.8415, 0.2+0.5403, …, 0.4+1.0000]

(位置编码计算:PE(pos,2i)=sin(pos/10000(2i/d_model)),PE(pos,2i+1)=cos(pos/10000(2i/d_model)))。最终得到输入矩阵T(维度:[1, 3, 512],batch_size=1)。

生成Q、K、V矩阵

通过三个可学习的权重矩阵W_Q、W_K、W_V(均为[512, 512])对T进行线性变换,得到Q、K、V矩阵(维度均为[1, 3, 512])。由于头数h=8,将Q、K、V按头拆分,每个头的Q、K、V维度为[1, 3, 64](512/8)。

生成掩码矩阵

创建下三角掩码矩阵(维度:[3, 3]),对角线以上元素设为-∞,对角线及以下设为0,矩阵形式为:

[[0, -∞, -∞], [0, 0, -∞], [0, 0, 0]]

该掩码确保计算“”的注意力时仅关注自身,计算“我”的注意力时关注“”和自身,计算“想”的注意力时关注“”“我”和自身,无法关注未来位置(如未生成的“吃”“蛋炒饭”)。

多头注意力计算:

步骤1:计算每个头的注意力分数:Score = Q·Kᵀ / √d_k(d_k=64,√d_k=8,用于缩放避免数值过大导致Softmax梯度消失)。此时Score维度为[1, 8, 3, 3]。

步骤2:应用掩码矩阵:将Score中对应掩码为-∞的位置置为-1e9,修正后的Score仅保留当前及历史位置的有效分数。

步骤3:Softmax归一化:对修正后的Score沿最后一维做Softmax,得到注意力权重(每行和为1)。例如“想”的权重分布为[0.1, 0.3, 0.6],表明主要关注自身,其次是“我”和“”。

步骤4:加权求和与拼接:将每个头的注意力权重与对应头的V矩阵相乘,得到每个头的输出(维度[1, 3, 64]);再将8个头的输出拼接,通过线性变换W_O([512, 512])得到最终输出M(维度[1, 3, 512])

2.2.3 核心子层二:编码器-解码器注意力层

该子层是实现“输入-输出语义对齐”的核心,功能是让解码器生成每个目标词时,精准关注输入序列中与之相关的词(如生成“我”时关注输入的“I”,生成“蛋炒饭”时关注输入的“fried rice”)。

(1)核心原理

与自注意力机制不同,编码器-解码器注意力层的Q、K、V来源不同:Q来自前一子层(带掩码的多头自注意力层)的输出M(目标序列前缀特征),K和V均来自编码器的最终输出(记忆向量Memory,输入序列全局特征)。这种设计让“目标序列查询输入序列的相关信息”,实现跨序列的语义对齐(Vaswani et al., 2017)。

(2)输入与输出
  • 输入:①前一子层输出M([batch_size, 目标序列前缀长度, d_model]);②编码器输出Memory([batch_size, 输入序列长度, d_model]),示例中输入序列“I want to eat fried rice”长度为6,故Memory维度为[1, 6, 512]。
  • 输出:融合输入序列语义特征后的目标序列前缀特征(记为C,维度与M一致)。
(3)具体计算步骤(结合示例)

生成Q、K、V矩阵

Q由M通过W_Q’([512, 512])线性变换得到(维度[1, 3, 512]);

K由Memory通过W_K’([512, 512])线性变换得到(维度[1, 6, 512]);V由Memory通过W_V’([512, 512])线性变换得到(维度[1, 6, 512])。同样按头拆分(h=8),每个头的Q、K、V维度分别为[1, 3, 64]、[1, 6, 64]、[1, 6, 64]。

注意力分数计算与归一化

Score = Q·Kᵀ / √d_k(维度[1, 8, 3, 6]),每个元素表示目标序列前缀中某个词与输入序列中某个词的相似度。例如“想”与“I”的相似度分数为12.3,与“want”的分数为15.7,与其他词的分数均低于10。对Score做Softmax,得到注意力权重(每行和为1),“想”的权重分布为[0.05, 0.8, 0.03, 0.07, 0.03, 0.02],表明主要关注输入的“want”。

加权求和与拼接

每个头的权重与对应头的V矩阵相乘,得到头输出;拼接后通过线性变换W_O’([512, 512])得到输出C(维度[1, 3, 512]),完成语义对齐。

2.2.4 核心子层三:前馈神经网络层

该子层与编码器的前馈网络完全一致,核心功能是对每个位置的特征进行独立的非线性变换,增强模型的表达能力。

2.2.5 残差连接与层归一化(Add & Norm)

解码器的每个子层均配备残差连接与层归一化,核心功能是稳定深层网络训练,缓解梯度消失问题,确保特征传递的有效性(He et al., 2016; Ba et al., 2016)。

具体实现逻辑为:对每个子层的输入x,先计算子层输出SubLayer(x),再通过残差连接将x与SubLayer(x)相加(x + SubLayer(x)),最后进行层归一化。层归一化公式为: LN(y) = γ·(y - μ)/√(σ² + ε) + β

其中,μ和σ²是y在特征维度的均值和方差,γ和β是可学习的缩放与偏移参数,ε=1e-6避免除零。原论文中采用“子层输出+残差连接→层归一化”的顺序(Post-Norm),后续研究表明“层归一化→子层输出+残差连接”(Pre-Norm)可进一步提升训练稳定性(Xiong et al., 2020)。

2.2.6 输出层:线性层与Softmax

经过N层解码器处理后,顶层输出的特征F需通过输出层转换为目标词汇表空间的概率分布,实现词的预测。

  • 线性层:通过全连接层W_out([d_model, 目标词汇表大小])将F([batch_size, 目标序列前缀长度, d_model])映射为logits([batch_size, 目标序列前缀长度, 目标词汇表大小])。示例中目标词汇表包含“我”“想”“吃”等中文词,大小设为10000,故logits维度为[1, 3, 10000]。
  • Softmax层:对logits沿最后一维做Softmax,得到每个位置的词汇概率分布(每行和为1)。选取概率最大的词作为当前时间步的输出,例如t=3时,“吃”的概率为0.92,故生成“吃”。

2.3 位置编码机制

由于Transformer无循环结构,无法通过时序依赖捕捉序列顺序信息,因此需通过对输入做位置编码为每个位置赋予唯一表示(Vaswani et al., 2017)。原论文采用正弦余弦位置编码,核心优势是:①相对位置可表示(任意偏移量k,PE(pos+k)可表示为PE(pos)的线性组合);②外推能力强(可处理训练时未见过的更长序列)。位置编码矩阵预先计算并注册为模型缓冲区,不参与梯度更新。

总结下解码器的流程:

  • 首先,我们将解码器的输入转换为嵌入矩阵,然后将位置编码加入其中,并将其作为输入送入底层的解码器(解码器 1)。
  • 解码器收到输入,并将其发送给带掩码的多头注意力层,生成注意力矩阵
  • 然后,将注意力矩阵和编码器输出的特征值作为多头注意力层(编码器−解码器注意力层)的输入,并再次输出新的注意力矩阵。
  • 把从多头注意力层得到的注意力矩阵作为输入,送入前馈网络层。前馈网络层将注意力矩阵作为输入,并将解码后的特征作为输出。
  • 最后,我们把从解码器1得到的输出作为输入,将其送入解码器 2。
  • 解码器 2 进行同样的处理,并输出目标句的特征。

我们可以将个解码器层层堆叠起来。从最后的解码器得到的输出(解码后的特征)将是目标句的特征。接下来,我们将目标句的特征送入线性层和 softmax 层,通过概率得到预测的词。

现在,我们已经详细了解了编码器和解码器的工作原理。让我们把编码器和解码器放在一起,看看 Transformer 模型是如何整体运作的。

我们可以看到整个流程,输入句子(原句),编码器会学习其特征并将特征发送给解码器,而解码器又会生成输出句(目标句)。

三、 Transformer 训练过程

训练核心目标是让模型生成的目标序列概率分布,尽可能接近真实目标序列的分布,通过“预测-计算误差-更新参数”的迭代过程优化所有可训练参数(词嵌入矩阵、注意力权重、前馈网络参数等)。

训练数据及输入

  • 数据:平行语料(如翻译任务的“英文-法文”句子对)或单语语料(如生成任务的文本序列)。
  • 输入处理:
  • 编码器输入:原始序列(如“I am good”)→词嵌入+位置编码。
  • 解码器输入:目标序列前缀(如“ Je vais”)→词嵌入+位置编码(确保生成时的时序逻辑)。
  • 解码器标签:真实目标序列(如“Je vais bien ”),与解码器输出下一个词逐位置对齐用于计算误差。

训练过程

  1. 初始化所有可训练参数(词嵌入矩阵、注意力权重、前馈网络权重等)为随机小值。
  2. 批量输入训练数据,通过编码器计算全局语义特征R,解码器基于R生成目标序列的概率分布。
  3. 用交叉熵损失计算预测分布与真实标签的差异。
  4. 通过Adam优化器和反向传播更新所有参数,最小化损失。
  5. 重复步骤2-4,迭代训练多轮(如数万次),直至损失收敛并在验证集上性能最优。

四、Transformer逐步计算详解:

为了让抽象的Transformer原理变具体,我们用中文句子“我想吃蛋炒饭”翻译为英文“I want to eat fried rice”的例子,一步步拆解计算过程。核心逻辑:先把文字转成模型能看懂的“数字向量”,再通过“注意力机制”捕捉词与词的关联,最后逐步生成目标语言句子。

4.1 示例设定

为了简化计算,我们提前约定好3个关键设定(就像做数学题前明确已知条件):

词汇表:把所有要用到的词/符号编上号,方便模型计算。

{'<pad>':0, '我':1, '想':2, '吃':3, '蛋炒饭':4, 'I':5, 'want':6, 'to':7, 'eat':8, 'fried':9, 'rice':10} (<pad>是填充符号,用于统一句子长度)

词向量维度d_model = 4:每个词最终会变成一个4个数字的“向量”(比如“我”→[0.1,1.2,0.3,1.4]),维度越小计算越简单

注意力头数n_heads = 2:相当于让模型用“两个角度”同时关注词的关联,最后综合结果;每个头的维度d_k = d_v = 2(每个角度的向量长度)

4.2 输入处理:

核心目的:模型看不懂文字,必须把“我想吃蛋炒饭”转成数字向量;同时,中文是有序的(“我想”和“想我”意思不同),还要给向量加“顺序信息”。

4.2.1 词嵌入

我们提前准备一个“词嵌入矩阵E”(11×4,11是词汇表大小,4是向量维度),相当于“词→向量”的字典:

E = [ [0, 0, 0, 0], # 0:<pad>(填充符号) [0.1, 0.2, 0.3, 0.4],# 1:我(这就是“我”的基础向量) [0.5, 0.6, 0.7, 0.8],# 2:想 [0.9, 1.0, 1.1, 1.2],# 3:吃 [1.3, 1.4, 1.5, 1.6],# 4:蛋炒饭 ... # 英文词汇的向量(此处省略,原理和中文一样) ]

通过这个矩阵,我们能直接拿到每个词的“基础向量”的初始值:比如“我”的基础向量e_我=[0.1,0.2,0.3,0.4],“想”的e_想=[0.5,0.6,0.7,0.8]。随着模型训练这个词嵌入矩阵也会随着数字更新,以获得更精确的词嵌入表示。

4.2.2 位置编码(给向量加“顺序标签”)

基础向量里没有顺序信息,所以要加“位置编码(PE)”。规则很简单:用正弦/余弦函数计算,不同位置的编码不一样,比如第1个词(位置0)和第2个词(位置1)的编码不同。

具体计算(以位置0和位置1为例):

位置0(对应“我”):PE(0,0)=sin(0/10000^(0/4))=0;PE(0,1)=cos(0)=1;PE(0,2)=sin(0)=0;PE(0,3)=cos(0)=1 → PE_0=[0,1,0,1] 位置1(对应“想”):PE(1,0)=sin(1/10000^0)=sin(1)≈0.8415;PE(1,1)=cos(1)≈0.5403;PE(1,2)=sin(1/10000^1)≈0.0001;PE(1,3)=cos(1/10000^1)≈1.0 → PE_1≈[0.8415,0.5403,0.0001,1.0000]

4.2.3 最终输入向量(基础向量+顺序标签)

把“词的基础向量”和“对应位置的编码”相加,就得到模型真正能处理的输入向量:

h_我 = e_我 + PE_0 = [0.1+0, 0.2+1, 0.3+0, 0.4+1] = [0.1,1.2,0.3,1.4] h_想 = e_想 + PE_1 ≈ [0.5+0.8415, 0.6+0.5403, 0.7+0.0001, 0.8+1.0] ≈ [1.3415,1.1403,0.7001,1.8000]

以此类推,h_吃、h_蛋炒饭,计算逻辑和上面一样,分别加位置2、位置3的编码

最终得到输入矩阵H₀(形状4×4):每一行就是一个词的“带顺序的数字向量”,4行对应“我、想、吃、蛋炒饭”4个词。

4.3 编码器计算:提取输入句子的语义关联

编码器的核心任务:分析输入句子里词与词的关系(比如“我”和“吃”有关,“吃”和“蛋炒饭”有关),把这些关系融入向量里。我们以第一个编码器层为例,拆解3个关键步骤。

4.3.1 多头自注意力:让每个词“关注”相关的词

核心逻辑:让每个词生成3个向量——Q(查询向量,相当于“我要找什么”)、K(键向量,相当于“我有什么”)、V(值向量,相当于“我能提供什么”);通过Q和K的匹配,算出每个词该“关注”其他词的程度(注意力权重),再用权重整合所有词的V,得到新的向量。

因为我们设定了2个注意力头,相当于用“两个角度”分别计算关注关系,最后合并结果。

第一步:计算头1的Q、K、V(第一个关注角度)

先给头1设定3个参数矩阵(W_Q1、W_K1、W_V1,都是4×2维度,把4维输入向量转成2维,适配每个头的维度):

W_Q1 = [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6], [0.7, 0.8]] # 查询参数矩阵 W_K1 = [[0.9, 1.0], [1.1, 1.2], [1.3, 1.4], [1.5, 1.6]] # 键参数矩阵 W_V1 = [[0.1, -0.1], [-0.2, 0.2], [0.3, -0.3], [-0.4, 0.4]] # 值参数矩阵

用输入矩阵H₀分别乘这3个参数矩阵,得到头1的Q1、K1、V1(都是4×2维度,4行对应4个词):

# 计算逻辑:矩阵相乘(比如“我”的Q1向量=h_我 × W_Q1) Q1 = H0 @ W_Q1 ≈ [ [1.38, 1.6], # 我:关注什么 [1.792, 2.056],# 想:关注什么 [2.204, 2.512],# 吃:关注什么 [2.616, 2.968] # 蛋炒饭:关注什么 ] K1 = H0 @ W_K1 ≈ [ [3.38, 3.6], # 我:有什么 [4.092, 4.356],# 想:有什么 [4.804, 5.108],# 吃:有什么 [5.516, 5.860] # 蛋炒饭:有什么 ] V1 = H0 @ W_V1 ≈ [ [-0.38, 0.38], # 我:能提供什么 [-0.492, 0.492],# 想:能提供什么 [-0.604, 0.604],# 吃:能提供什么 [-0.716, 0.716] # 蛋炒饭:能提供什么 ]
第二步:计算注意力分数(匹配程度)和权重(关注程度)
  1. 注意力分数:用Q1乘K1的转置(把K1倒过来),再除以√d_k(这里d_k=2,√2≈1.414,目的是让分数不要太大),得到每个词对其他词的“匹配程度”:
attn_scores1 = Q1 @ K1.T / 1.414 ≈ [ [7.584, 9.170, 10.756, 12.341], # 我:和“我、想、吃、蛋炒饭”的匹配度(和蛋炒饭最高) [9.847, 11.904, 13.963, 16.023],# 想:和其他词的匹配度(和蛋炒饭最高) [12.110, 14.641, 17.172, 19.703],# 吃:和其他词的匹配度(和蛋炒饭最高) [14.373, 17.389, 20.399, 23.409] # 蛋炒饭:和其他词的匹配度(和自己最高) ]
  1. 注意力权重:用Softmax函数把分数转成“概率”(所有概率加起来=1),得到每个词该“关注”其他词的程度:
attn_weights1 = softmax(attn_scores1) ≈ [ [0.006, 0.016, 0.043, 0.935], # 我:93.5%关注蛋炒饭,其他词关注很少 [0.002, 0.008, 0.028, 0.962], # 想:96.2%关注蛋炒饭 [0.001, 0.004, 0.016, 0.979], # 吃:97.9%关注蛋炒饭 [0.000, 0.002, 0.009, 0.989] # 蛋炒饭:98.9%关注自己 ]

这个结果很合理!“我想吃蛋炒饭”的核心是“蛋炒饭”,所以“我、想、吃”都主要关注“蛋炒饭”。

第三步:计算头1输出(整合关注到的信息)

用注意力权重乘以V1,相当于“按关注程度整合所有词的信息”,得到头1的输出(4×2维度):

head1 = attn_weights1 @ V1 ≈ [ [-0.686, 0.686], # 我:整合了93.5%蛋炒饭的信息 [-0.697, 0.697], # 想:整合了96.2%蛋炒饭的信息 [-0.708, 0.708], # 吃:整合了97.9%蛋炒饭的信息 [-0.717, 0.717] # 蛋炒饭:整合了98.9%自己的信息 ]
第四步:头2计算与多头合并

头2的计算逻辑和头1完全一样(只是参数矩阵不同),假设得到头2的输出:

head2 ≈ [ [0.150, -0.150], # 我:第二个角度的关注结果 [0.145, -0.145], # 想:第二个角度的关注结果 [0.140, -0.140], # 吃:第二个角度的关注结果 [0.135, -0.135] # 蛋炒饭:第二个角度的关注结果 ]

把两个头的输出“拼起来”(concat),再通过一个线性变换(W_O参数矩阵)转成4维向量,就得到多头自注意力的最终输出(attn_output,4×4维度):

multi_head = concat([head1, head2]) ≈ [ [-0.686, 0.686, 0.150, -0.150], # 我:两个角度的信息合并 [-0.700, 0.700, 0.145, -0.145], # 想:两个角度的信息合并 [-0.712, 0.712, 0.140, -0.140], # 吃:两个角度的信息合并 [-0.722, 0.722, 0.135, -0.135] # 蛋炒饭:两个角度的信息合并 ] attn_output = multi_head @ W_O ≈ [ [0.022, 0.026, 0.030, 0.034], # 我:最终的注意力输出向量 [0.006, 0.010, 0.014, 0.018], # 想:最终的注意力输出向量 [-0.010, -0.006, -0.002, 0.002], # 吃:最终的注意力输出向量 [-0.026, -0.022, -0.018, -0.014] # 蛋炒饭:最终的注意力输出向量 ]

4.3.2 残差连接和层归一化:让模型训练更稳定

  1. 残差连接:把“多头自注意力的输出”和“最初的输入向量H0”加起来(residual = H0 + attn_output)。目的是“保留原始信息”,避免模型越学越偏。
residual ≈ [ [0.122, 1.226, 0.330, 1.434], # 我:原始向量+注意力信息 [1.3475, 1.1503, 0.7141, 1.8180], # 想:原始向量+注意力信息 [2.214, 2.522, 1.124, 2.524], # 吃:原始向量+注意力信息 [2.642, 2.986, 1.538, 3.154] # 蛋炒饭:原始向量+注意力信息 ]
  1. 层归一化:把上面的结果“标准化”(让每个向量的数值范围差不多,比如都在-1到1之间),方便后续计算。计算逻辑是:先算每个向量的平均值(mean)和标准差(std),再用(向量-平均值)/标准差得到归一化结果:
norm_output ≈ [ [-1.176, 0.803, -0.803, 1.176], # 我:归一化后的向量 [0.198, -0.234, -1.190, 1.230], # 想:归一化后的向量 [0.201, -0.041, -1.656, 0.729], # 吃:归一化后的向量 [0.019, 0.248, -2.083, 0.515] # 蛋炒饭:归一化后的向量 ]

4.3.3 前馈网络:进一步加工向量信息

核心逻辑:用两个全连接层(中间加ReLU激活函数)对归一化后的向量做“非线性变换”,让向量能表达更复杂的语义。计算过程很简单:

ffn_output = ReLU(norm_output @ W1 + b1) @ W2 + b2

假设得到结果(4×4维度):

ffn_output ≈ [ [0.22, 0.33, 0.44, 0.55], # 我:加工后的最终向量 [0.20, 0.30, 0.40, 0.50], # 想:加工后的最终向量 [0.18, 0.27, 0.36, 0.45], # 吃:加工后的最终向量 [0.16, 0.24, 0.32, 0.40] # 蛋炒饭:加工后的最终向量 ]

之后,再对ffn_output做一次“残差连接+层归一化”,就得到第一个编码器层的输出。这个输出会传给下一个编码器层(如果有的话),最终得到编码器的“语义特征”(相当于把“我想吃蛋炒饭”的核心意思提炼成了一组向量)。

4.4 解码器计算:

解码器的核心任务:拿着编码器提炼的“语义特征”,就像我们要先学习好好听懂对方说的话,然后再“一个词一个词”不紧不慢生成目标语言句子。(如翻译为英文)。

4.4.1 先搞懂两个关键机制

解码器多了“掩码机制”(防止偷看未来的词)和“编码器-解码器注意力”(让生成的词和原来输入词对应上语义)

  • 掩码机制:生成句子是按顺序的(比如先出“I”,再出“want”),模型不能“作弊”看还没生成的词。掩码就像“遮挡板”,把未来位置的词遮住,让模型只能关注已经生成的词。
  • 编码器-解码器注意力:让生成的目标词(比如英文“want”)找到输入句子里对应的词(比如中文“想”),确保语义准确(这就是“翻译对齐”的核心)。

4.4.2 目标序列生成过程(自回归生成)

自回归生成:从“句子开始标记”出发,每一步只生成一个词,再把生成的词加进去,继续生成下一个,直到生成“句子结束标记”。我们以“中文→英文”为例(输入:我想吃蛋炒饭,输出:I want to eat fried rice):

t=1(第一步):输入“<sos>”→ 经过解码器的词嵌入+位置编码,再通过“掩码自注意力”(只能关注自己)、“编码器-解码器注意力”(关注输入的“我”),最后预测出概率最高的词是“I”(概率0.85)。 t=2(第二步):输入“<sos>、I”→ 掩码自注意力能关注“<sos>”和“I”,编码器-解码器注意力关注输入的“想”,预测出“want”(概率0.92)。 t=3(第三步):输入“<sos>、I、want”→ 预测出“to”(概率0.95)。 t=4(第四步):输入“<sos>、I、want、to”→ 预测出“eat”(概率0.94)。 t=5(第五步):输入“<sos>、I、want、to、eat”→ 编码器-解码器注意力关注输入的“蛋炒饭”,预测出“fried”(概率0.96)。 t=6(第六步):输入“<sos>、I、want、to、eat、fried”→ 预测出“rice”(概率0.96)。 t=7(第七步):输入所有已生成的词→ 预测出“<eos>”(句子结束),生成停止。

最终生成完整句子:“I want to eat fried rice”,和我们预期的翻译结果一致。

4.4.3 掩码矩阵的具体样子(通俗理解)

以生成到“、I、want、to、eat、fried、rice”(长度7)为例,掩码矩阵是一个7×7的下三角矩阵(1表示能关注,0表示不能关注):

mask = [ [1, 0, 0, 0, 0, 0, 0], # <sos>:只能关注自己 [1, 1, 0, 0, 0, 0, 0], # I:能关注<sos>和自己 [1, 1, 1, 0, 0, 0, 0], # want:能关注前两个词和自己 [1, 1, 1, 1, 0, 0, 0], # to:能关注前三个词和自己 [1, 1, 1, 1, 1, 0, 0], # eat:能关注前四个词和自己 [1, 1, 1, 1, 1, 1, 0], # fried:能关注前五个词和自己 [1, 1, 1, 1, 1, 1, 1] # rice:能关注所有前面的词和自己 ]

这个矩阵会加在注意力分数上,把0的位置分数设为极小值(-1e9),经过Softmax后权重几乎为0,相当于“完全不关注”。

4.5 最终输出:把向量转成具体的词

解码器生成的最后一层向量(维度:[批量大小, 目标序列长度, d_model]),还需要经过两步转换,才能变成具体的词:

4.5.1 线性变换(把向量转成词汇表大小的分数)

用一个全连接层(参数矩阵W_out,维度[4, 11],4是d_model,11是词汇表大小),把4维的词向量转成11个分数(logits):每个分数对应词汇表中一个词的“原始得分”。公式:logits = decoder_output @ W_out。

4.5.2 Softmax归一化(把分数转成概率)

对11个分数做Softmax运算,把分数转成0-1之间的概率(所有概率加起来=1)。我们选概率最大的那个词,就是当前位置的预测结果。

示例:最终概率分布

以生成的7个位置为例,每个位置的概率分布(只展示核心词,其他词概率≈0):

probs[0] ≈ [0.01, 0.02, 0.03, 0.85, 0.04, 0.03, 0.00, 0.00, 0.00, 0.00, 0.00] # 位置1:I(0.85) probs[1] ≈ [0.00, 0.00, 0.00, 0.00, 0.92, 0.03, 0.02, 0.01, 0.02, 0.00, 0.00] # 位置2:want(0.92) probs[2] ≈ [0.00, 0.00, 0.00, 0.00, 0.01, 0.95, 0.02, 0.01, 0.01, 0.00, 0.00] # 位置3:to(0.95) probs[3] ≈ [0.00, 0.00, 0.00, 0.00, 0.00, 0.02, 0.94, 0.02, 0.02, 0.00, 0.00] # 位置4:eat(0.94) probs[4] ≈ [0.00, 0.00, 0.00, 0.00, 0.00, 0.01, 0.01, 0.96, 0.02, 0.00, 0.00] # 位置5:fried(0.96) probs[5] ≈ [0.00, 0.00, 0.00, 0.00, 0.00, 0.01, 0.01, 0.02, 0.96, 0.00, 0.00] # 位置6:rice(0.96) probs[6] ≈ [0.00, 0.00, 0.98, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00] # 位置7:<eos>(0.98) ]

总结:Transformer核心逻辑回顾

  1. 输入处理:文字→词向量+位置编码→带顺序的数字向量;

  2. 编码器:通过多头自注意力捕捉词间关联,再用前馈网络加工→提炼语义特征;

  3. 解码器:用掩码防止偷看未来词,用编码器-解码器注意力对齐语义,逐词生成目标句子;

  4. 最终输出:向量→分数→概率→选概率最大的词,完成翻译。

整个过程就像“翻译官”:先读懂中文(编码器),再按顺序说出英文(解码器),每说一个词都只看前面说过的内容,同时确保和中文意思对应。

五、PyTorch 完整实现模型

import torch import torch.nn as nn import math # ============================== 3.1 基础模块实现 ============================== # 3.1.1 位置编码模块 # 作用:为序列中的每个位置生成唯一的位置编码,弥补Transformer无顺序感知的缺陷 class PositionalEncoding(nn.Module): def __init__(self, d_model: int, max_len: int = 5000): """ 初始化位置编码层 Args: d_model: 模型的特征维度(词嵌入维度) max_len: 最大序列长度,默认5000 """ super(PositionalEncoding, self).__init__() # 1. 创建位置编码矩阵,形状: [max_len, d_model] pe = torch.zeros(max_len, d_model) # 2. 生成位置索引,形状: [max_len, 1] position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) # 3. 计算频率缩放因子,避免高频震荡 # 公式:div_term = exp(2i * (-ln(10000)/d_model)),i为维度索引 div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) # 4. 偶数维度用正弦编码,奇数维度用余弦编码 pe[:, 0::2] = torch.sin(position * div_term) # 步长2,取偶数索引 pe[:, 1::2] = torch.cos(position * div_term) # 步长2,取奇数索引 # 5. 增加批次维度,形状变为 [1, max_len, d_model],适配批量输入 pe = pe.unsqueeze(0) # 6. 注册为缓冲区:随模型保存/加载,不参与梯度更新 self.register_buffer('pe', pe) def forward(self, x: torch.Tensor) -> torch.Tensor: """ 前向传播:将位置编码加到输入嵌入上 Args: x: 输入张量,形状 [batch_size, seq_len, d_model](修正原代码维度说明错误) Returns: 叠加位置编码后的张量,形状与输入一致 """ # 只取与输入序列长度匹配的位置编码,避免冗余 x = x + self.pe[:, :x.size(1), :] return x # 3.1.2 缩放点积注意力模块 # 作用:实现Transformer核心的缩放点积注意力机制,计算query-key的相似度并加权value class ScaledDotProductAttention(nn.Module): def __init__(self, dropout: float = 0.1): """ 初始化缩放点积注意力层 Args: dropout: dropout概率,默认0.1 """ super(ScaledDotProductAttention, self).__init__() self.dropout = nn.Dropout(dropout) def forward(self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor, mask: torch.Tensor = None) -> tuple[torch.Tensor, torch.Tensor]: """ 前向传播:计算缩放点积注意力 Args: query: 查询张量,形状 [batch_size, n_heads, seq_len_q, d_k] key: 键张量,形状 [batch_size, n_heads, seq_len_k, d_k] value: 值张量,形状 [batch_size, n_heads, seq_len_v, d_k](seq_len_k=seq_len_v) mask: 掩码张量,形状 [batch_size, 1, seq_len_q, seq_len_k],0表示屏蔽位置 Returns: output: 注意力加权后的输出,形状 [batch_size, n_heads, seq_len_q, d_k] attn_weights: 注意力权重,形状 [batch_size, n_heads, seq_len_q, seq_len_k] """ # 1. 获取每个头的维度 d_k = query.size(-1) # 2. 计算query-key点积相似度,并缩放(除以√d_k避免梯度爆炸) scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k) # 3. 掩码处理:屏蔽位置设为负无穷,softmax后权重趋近于0 if mask is not None: scores = scores.masked_fill(mask == 0, -1e9) # 4. 计算注意力权重(softmax归一化)+ dropout attn_weights = torch.softmax(scores, dim=-1) attn_weights = self.dropout(attn_weights) # 5. 加权求和得到最终输出 output = torch.matmul(attn_weights, value) return output, attn_weights # 3.1.3 多头注意力模块 # 作用:将输入拆分为多个头并行计算注意力,捕捉不同维度的特征 class MultiHeadAttention(nn.Module): def __init__(self, d_model: int, n_heads: int, dropout: float = 0.1): """ 初始化多头注意力层 Args: d_model: 模型总维度(需能被n_heads整除) n_heads: 注意力头数 dropout: dropout概率,默认0.1 """ super(MultiHeadAttention, self).__init__() # 校验:d_model必须能被头数整除 assert d_model % n_heads == 0, "d_model必须能被n_heads整除" self.d_model = d_model # 模型总维度 self.n_heads = n_heads # 注意力头数 self.d_k = d_model // n_heads # 每个头的维度 # 定义线性变换层:将输入投影到d_model维度 self.w_q = nn.Linear(d_model, d_model) self.w_k = nn.Linear(d_model, d_model) self.w_v = nn.Linear(d_model, d_model) self.w_o = nn.Linear(d_model, d_model) # 输出投影层 # 缩放点积注意力实例 self.attention = ScaledDotProductAttention(dropout) self.dropout = nn.Dropout(dropout) self.layer_norm = nn.LayerNorm(d_model, eps=1e-6) # 层归一化 def forward(self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor, mask: torch.Tensor = None) -> tuple[torch.Tensor, torch.Tensor]: """ 前向传播:多头注意力计算 Args: query/key/value: 输入张量,形状 [batch_size, seq_len, d_model] mask: 掩码张量,形状 [batch_size, 1, seq_len, seq_len] Returns: output: 多头注意力输出,形状 [batch_size, seq_len, d_model] attn_weights: 注意力权重,形状 [batch_size, n_heads, seq_len, seq_len] """ batch_size = query.size(0) # 1. 线性变换 + 拆分多头 # 形状变化:[batch_size, seq_len, d_model] -> [batch_size, n_heads, seq_len, d_k] Q = self.w_q(query).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2) K = self.w_k(key).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2) V = self.w_v(value).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2) # 2. 计算缩放点积注意力 x, attn_weights = self.attention(Q, K, V, mask) # 3. 合并多头输出 # 形状变化:[batch_size, n_heads, seq_len, d_k] -> [batch_size, seq_len, d_model] # contiguous()确保张量内存连续,避免view操作报错 x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model) # 4. 输出投影 + dropout x = self.dropout(self.w_o(x)) return x, attn_weights # ============================== 3.2 编码器和解码器层 ============================== # 3.2.1 编码器层 # 作用:单个编码器层,包含多头自注意力 + 前馈网络 + 残差连接 + 层归一化 class EncoderLayer(nn.Module): def __init__(self, d_model: int, n_heads: int, d_ff: int, dropout: float = 0.1): """ 初始化编码器层 Args: d_model: 模型维度 n_heads: 注意力头数 d_ff: 前馈网络隐藏层维度 dropout: dropout概率 """ super(EncoderLayer, self).__init__() # 多头自注意力层 self.self_attn = MultiHeadAttention(d_model, n_heads, dropout) # 前馈网络:两层线性变换 + ReLU激活 self.feed_forward = nn.Sequential( nn.Linear(d_model, d_ff), nn.ReLU(), nn.Linear(d_ff, d_model) ) # 层归一化(Post-Norm,与原始论文一致) self.norm1 = nn.LayerNorm(d_model, eps=1e-6) self.norm2 = nn.LayerNorm(d_model, eps=1e-6) self.dropout = nn.Dropout(dropout) def forward(self, x: torch.Tensor, mask: torch.Tensor = None) -> torch.Tensor: """ 前向传播:编码器层计算 Args: x: 输入张量,形状 [batch_size, seq_len, d_model] mask: 源序列掩码(填充掩码) Returns: 编码器层输出,形状与输入一致 """ # 1. 自注意力 + 残差连接 + 层归一化 attn_output, _ = self.self_attn(x, x, x, mask) x = self.norm1(x + attn_output) # 2. 前馈网络 + 残差连接 + 层归一化 ff_output = self.dropout(self.feed_forward(x)) x = self.norm2(x + ff_output) return x # 完整编码器(原代码缺失,补充实现) class Encoder(nn.Module): def __init__(self, src_vocab_size: int, d_model: int, n_layers: int, n_heads: int, d_ff: int, max_len: int = 5000, dropout: float = 0.1): """ 初始化完整编码器 Args: src_vocab_size: 源语言词汇表大小 d_model: 模型维度 n_layers: 编码器层数 n_heads: 注意力头数 d_ff: 前馈网络维度 max_len: 最大序列长度 dropout: dropout概率 """ super(Encoder, self).__init__() self.d_model = d_model # 词嵌入层 self.embedding = nn.Embedding(src_vocab_size, d_model) # 位置编码层 self.pos_encoding = PositionalEncoding(d_model, max_len) # 编码器层堆叠 self.layers = nn.ModuleList([ EncoderLayer(d_model, n_heads, d_ff, dropout) for _ in range(n_layers) ]) self.dropout = nn.Dropout(dropout) def forward(self, src: torch.Tensor, mask: torch.Tensor = None) -> torch.Tensor: """ 前向传播:编码器整体计算 Args: src: 源序列索引,形状 [batch_size, seq_len] mask: 源序列掩码 Returns: 编码器输出(memory),形状 [batch_size, seq_len, d_model] """ # 1. 词嵌入 + 位置编码(嵌入层乘以√d_model,原始论文操作) x = self.embedding(src) * math.sqrt(self.d_model) x = self.pos_encoding(x) x = self.dropout(x) # 2. 逐层计算编码器 for layer in self.layers: x = layer(x, mask) return x # 3.2.2 解码器层 # 作用:单个解码器层,包含掩码自注意力 + 交叉注意力 + 前馈网络 + 残差/归一化 class DecoderLayer(nn.Module): def __init__(self, d_model: int, n_heads: int, d_ff: int, dropout: float = 0.1): """ 初始化解码器层 Args: d_model: 模型维度 n_heads: 注意力头数 d_ff: 前馈网络维度 dropout: dropout概率 """ super(DecoderLayer, self).__init__() # 掩码自注意力(防止偷看未来信息) self.self_attn = MultiHeadAttention(d_model, n_heads, dropout) # 编码器-解码器交叉注意力 self.cross_attn = MultiHeadAttention(d_model, n_heads, dropout) # 前馈网络 self.feed_forward = nn.Sequential( nn.Linear(d_model, d_ff), nn.ReLU(), nn.Linear(d_ff, d_model) ) # 层归一化 self.norm1 = nn.LayerNorm(d_model, eps=1e-6) self.norm2 = nn.LayerNorm(d_model, eps=1e-6) self.norm3 = nn.LayerNorm(d_model, eps=1e-6) self.dropout = nn.Dropout(dropout) def forward(self, x: torch.Tensor, memory: torch.Tensor, src_mask: torch.Tensor = None, tgt_mask: torch.Tensor = None) -> torch.Tensor: """ 前向传播:解码器层计算 Args: x: 目标序列输入,形状 [batch_size, seq_len_tgt, d_model] memory: 编码器输出,形状 [batch_size, seq_len_src, d_model] src_mask: 源序列掩码 tgt_mask: 目标序列掩码(填充+序列掩码) Returns: 解码器层输出,形状与x一致 """ # 1. 掩码自注意力 + 残差 + 归一化 attn1_output, _ = self.self_attn(x, x, x, tgt_mask) x = self.norm1(x + attn1_output) # 2. 交叉注意力(Q来自解码器,K/V来自编码器) + 残差 + 归一化 attn2_output, _ = self.cross_attn(x, memory, memory, src_mask) x = self.norm2(x + self.dropout(attn2_output)) # 3. 前馈网络 + 残差 + 归一化 ff_output = self.dropout(self.feed_forward(x)) x = self.norm3(x + ff_output) return x # 完整解码器(原代码缺失,补充实现) class Decoder(nn.Module): def __init__(self, tgt_vocab_size: int, d_model: int, n_layers: int, n_heads: int, d_ff: int, max_len: int = 5000, dropout: float = 0.1): """ 初始化完整解码器 Args: tgt_vocab_size: 目标语言词汇表大小 d_model: 模型维度 n_layers: 解码器层数 n_heads: 注意力头数 d_ff: 前馈网络维度 max_len: 最大序列长度 dropout: dropout概率 """ super(Decoder, self).__init__() self.d_model = d_model # 词嵌入层 self.embedding = nn.Embedding(tgt_vocab_size, d_model) # 位置编码层 self.pos_encoding = PositionalEncoding(d_model, max_len) # 解码器层堆叠 self.layers = nn.ModuleList([ DecoderLayer(d_model, n_heads, d_ff, dropout) for _ in range(n_layers) ]) self.dropout = nn.Dropout(dropout) def forward(self, tgt: torch.Tensor, memory: torch.Tensor, src_mask: torch.Tensor = None, tgt_mask: torch.Tensor = None) -> torch.Tensor: """ 前向传播:解码器整体计算 Args: tgt: 目标序列索引,形状 [batch_size, seq_len_tgt] memory: 编码器输出,形状 [batch_size, seq_len_src, d_model] src_mask: 源序列掩码 tgt_mask: 目标序列掩码 Returns: 解码器输出,形状 [batch_size, seq_len_tgt, d_model] """ # 1. 词嵌入 + 位置编码 x = self.embedding(tgt) * math.sqrt(self.d_model) x = self.pos_encoding(x) x = self.dropout(x) # 2. 逐层计算解码器 for layer in self.layers: x = layer(x, memory, src_mask, tgt_mask) return x # ============================== 3.3 完整的Transformer模型 ============================== class Transformer(nn.Module): def __init__(self, src_vocab_size: int, tgt_vocab_size: int, d_model: int = 512, n_layers: int = 6, n_heads: int = 8, d_ff: int = 2048, max_len: int = 5000, dropout: float = 0.1): """ 初始化完整的Transformer模型 Args: src_vocab_size: 源语言词汇表大小 tgt_vocab_size: 目标语言词汇表大小 d_model: 模型核心维度(默认512,与论文一致) n_layers: 编码器/解码器层数(默认6) n_heads: 注意力头数(默认8) d_ff: 前馈网络隐藏层维度(默认2048) max_len: 最大序列长度 dropout: dropout概率 """ super(Transformer, self).__init__() # 编码器 self.encoder = Encoder(src_vocab_size, d_model, n_layers, n_heads, d_ff, max_len, dropout) # 解码器 self.decoder = Decoder(tgt_vocab_size, d_model, n_layers, n_heads, d_ff, max_len, dropout) # 输出层:将解码器输出映射到目标词汇表 self.fc_out = nn.Linear(d_model, tgt_vocab_size) # 初始化模型参数 self._init_parameters() def _init_parameters(self): """参数初始化:使用Xavier均匀初始化,提升训练稳定性""" for p in self.parameters(): if p.dim() > 1: # 仅对矩阵参数初始化(偏置项除外) nn.init.xavier_uniform_(p) def forward(self, src: torch.Tensor, tgt: torch.Tensor, src_mask: torch.Tensor = None, tgt_mask: torch.Tensor = None) -> torch.Tensor: """ 前向传播:完整Transformer计算 Args: src: 源序列索引,形状 [batch_size, seq_len_src] tgt: 目标序列索引,形状 [batch_size, seq_len_tgt] src_mask: 源序列掩码 tgt_mask: 目标序列掩码 Returns: 输出概率分布,形状 [batch_size, seq_len_tgt, tgt_vocab_size] """ # 1. 编码器编码源序列,得到memory memory = self.encoder(src, src_mask) # 2. 解码器基于memory解码目标序列 output = self.decoder(tgt, memory, src_mask, tgt_mask) # 3. 线性变换到目标词汇表大小 output = self.fc_out(output) return output # ============================== 3.4 掩码生成函数 ============================== def create_masks(src: torch.Tensor, tgt: torch.Tensor, pad_idx: int) -> tuple[torch.Tensor, torch.Tensor]: """ 生成Transformer所需的掩码 Args: src: 源序列,形状 [batch_size, seq_len_src] tgt: 目标序列,形状 [batch_size, seq_len_tgt] pad_idx: 填充符的索引(如<EOS>/<PAD>) Returns: src_mask: 源序列掩码,形状 [batch_size, 1, 1, seq_len_src] tgt_mask: 目标序列掩码,形状 [batch_size, 1, seq_len_tgt, seq_len_tgt] """ # 1. 源序列掩码:填充掩码(屏蔽pad_idx位置) # 形状变化:[batch_size, seq_len_src] -> [batch_size, 1, 1, seq_len_src] src_mask = (src != pad_idx).unsqueeze(1).unsqueeze(2) # 2. 目标序列掩码:填充掩码 + 序列掩码(防止偷看未来) # 2.1 填充掩码 tgt_pad_mask = (tgt != pad_idx).unsqueeze(1).unsqueeze(2) # 2.2 序列掩码:下三角矩阵(True表示可见,False表示屏蔽未来) tgt_len = tgt.size(1) tgt_sub_mask = torch.tril(torch.ones((tgt_len, tgt_len), device=tgt.device)).bool() # 2.3 组合掩码:同时屏蔽填充和未来位置 tgt_mask = tgt_pad_mask & tgt_sub_mask return src_mask, tgt_mask # ------------------------------ 测试代码(可选) ------------------------------ if __name__ == "__main__": # 测试参数 src_vocab_size = 1000 tgt_vocab_size = 1000 batch_size = 2 seq_len_src = 10 seq_len_tgt = 8 pad_idx = 0 # 创建模型实例 model = Transformer(src_vocab_size, tgt_vocab_size) # 生成测试数据 src = torch.randint(1, src_vocab_size, (batch_size, seq_len_src)) # 避免pad_idx tgt = torch.randint(1, tgt_vocab_size, (batch_size, seq_len_tgt)) # 生成掩码 src_mask, tgt_mask = create_masks(src, tgt, pad_idx) # 前向传播 output = model(src, tgt, src_mask, tgt_mask) print(f"模型输出形状: {output.shape}") # 预期: [2, 8, 1000]

六、PyTorch 实战:训练一个简单的翻译模型

我们将通过一个完整的 PyTorch 训练流程,展示如何使用 Transformer 进行一个简单的翻译任务。

#5.1 数据集准备 我们创建一个非常简单的中英翻译数据集: import torch from torch.utils.data import Dataset, DataLoader class SimpleTranslationDataset(Dataset): def __init__(self): # 源语言(中文)词汇表 self.src_vocab = { '<pad>': 0, '<sos>': 1, '<eos>': 2, '我': 3, '想': 4, '吃': 5, '蛋炒饭': 6 } self.src_idx_to_word = {v: k for k, v in self.src_vocab.items()} # 目标语言(英文)词汇表 self.tgt_vocab = { '<pad>': 0, '<sos>': 1, '<eos>': 2, 'I': 3, 'want': 4, 'to': 5, 'eat': 6, 'fried': 7, 'rice': 8 } self.tgt_idx_to_word = {v: k for k, v in self.tgt_vocab.items()} # 训练数据 self.data = [ # 中文 -> 英文 ( [1, 3, 4, 5, 6, 2], # <sos> 我 想 吃 蛋炒饭 <eos> [1, 3, 4, 5, 6, 7, 8, 2] # <sos> I want to eat fried rice <eos> ), # 添加更多示例... ] def __len__(self): return len(self.data) def __getitem__(self, idx): return self.data[idx] def src_vocab_size(self): return len(self.src_vocab) def tgt_vocab_size(self): return len(self.tgt_vocab) 5.2 模型初始化 # 超参数设置 src_vocab_size = dataset.src_vocab_size() tgt_vocab_size = dataset.tgt_vocab_size() d_model = 32 # 简化为32维 n_layers = 2 # 使用2层 n_heads = 2 d_ff = 128 max_len = 10 dropout = 0.1 # 初始化模型 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = Transformer(src_vocab_size, tgt_vocab_size, d_model, n_layers, n_heads, d_ff, max_len, dropout).to(device) # 定义优化器和损失函数 optimizer = torch.optim.Adam(model.parameters(), lr=0.001) criterion = nn.CrossEntropyLoss(ignore_index=0) # 忽略填充符 5.3 训练循环 def train(model, dataloader, optimizer, criterion, pad_idx, device, n_epochs=100): model.train() for epoch in range(n_epochs): total_loss = 0 for batch_idx, (src, tgt) in enumerate(dataloader): src = src.to(device) tgt = tgt.to(device) # 创建掩码 src_mask, tgt_mask = create_masks(src, tgt, pad_idx) src_mask = src_mask.to(device) tgt_mask = tgt_mask.to(device) # 前向传播 optimizer.zero_grad() output = model(src, tgt[:, :-1], src_mask, tgt_mask[:, :, :-1, :-1]) # 计算损失 loss = criterion(output.contiguous().view(-1, output.size(-1)), tgt[:, 1:].contiguous().view(-1)) # 反向传播 loss.backward() optimizer.step() total_loss += loss.item() # 打印训练信息 if (epoch + 1) % 10 == 0: avg_loss = total_loss / len(dataloader) print(f'Epoch {epoch+1}/{n_epochs}, Average Loss: {avg_loss:.4f}') # 创建数据加载器 dataset = SimpleTranslationDataset() dataloader = DataLoader(dataset, batch_size=2, shuffle=True) # 开始训练 train(model, dataloader, optimizer, criterion, 0, device) 5.4 模型推理 训练完成后,我们可以进行推理: def translate(model, src_sentence, src_vocab, tgt_vocab, device, max_len=10): model.eval() # 将源句子转换为索引 src_idx = [src_vocab['<sos>']] for word in src_sentence: if word in src_vocab: src_idx.append(src_vocab[word]) else: src_idx.append(src_vocab['<pad>']) src_idx.append(src_vocab['<eos>']) # 转换为张量 src_tensor = torch.tensor(src_idx, dtype=torch.long).unsqueeze(0).to(device) # 创建源掩码 src_mask = (src_tensor != 0).unsqueeze(1).unsqueeze(2).to(device) # 初始化目标序列 tgt_idx = [tgt_vocab['<sos>']] with torch.no_grad(): for i in range(max_len): # 转换为目标张量 tgt_tensor = torch.tensor(tgt_idx, dtype=torch.long).unsqueeze(0).to(device) # 创建目标掩码 tgt_mask = create_masks(tgt_tensor, tgt_tensor, 0)[1][:, :, :-1, :-1].to(device) # 推理 output = model(src_tensor, tgt_tensor, src_mask, tgt_mask) # 获取最后一个位置的预测 last_token_logits = output[0, -1, :] _, pred_token = torch.max(last_token_logits, dim=-1) # 如果预测为<eos>,则结束 if pred_token.item() == tgt_vocab['<eos>']: break # 添加到目标序列 tgt_idx.append(pred_token.item()) # 转换为单词 translated_words = [tgt_vocab[idx] for idx in tgt_idx[1:]] # 去掉<sos> return translated_words # 测试翻译 src_sentence = ['我', '想', '吃', '蛋炒饭'] translated_words = translate(model, src_sentence, dataset.src_vocab, dataset.tgt_vocab, device) print(f'源句子: {" ".join(src_sentence)}') print(f'翻译结果: {" ".join(translated_words)}')

模型训练优化建议:

过拟合:在小数据集上训练时容易过拟合。可以使用 dropout、增加训练数据、使用预训练模型等方法缓解。

内存占用大:Transformer 的内存需求与序列长度的平方成正比(因为注意力矩阵的大小是 seq_len×seq_len)。在处理长序列时需要特别注意。

收敛速度慢:相比 CNN,Transformer 的收敛速度可能较慢。可以使用学习率调度器、增加训练轮数等方法。

层归一化的位置:原始论文使用的是 post-norm(残差连接后进行层归一化),但 recent 研究表明 pre-norm(残差连接前进行层归一化)可能有更好的稳定性。

优化器选择:可以尝试使用 AdamW 优化器,它在 Adam 的基础上添加了权重衰减,通常能取得更好的效果。

学习率调度:使用 warmup 策略,在训练初期逐渐增加学习率,有助于模型的稳定收敛。

混合精度训练:使用 PyTorch 的混合精度训练可以减少内存占用,提高训练速度。

结论

本文系统地介绍了 Transformer 的技术原理与 PyTorch 实现。通过从整体架构到具体组件的详细讲解,我们了解模型是如何通过自注意力机制实现并行计算和长距离依赖建模的。

读者福利:如果大家对大模型感兴趣,这套大模型学习资料一定对你有用

对于0基础小白入门:

如果你是零基础小白,想快速入门大模型是可以考虑的。

一方面是学习时间相对较短,学习内容更全面更集中。
二方面是可以根据这些资料规划好学习计划和方向。

作为一名老互联网人,看着AI越来越火,也总想为大家做点啥。干脆把我这几年整理的AI大模型干货全拿出来了。
包括入门指南、学习路径图、精选书籍、视频课,还有我录的一些实战讲解。全部免费,不搞虚的。
学习从来都是自己的事,我能做的就是帮你把路铺平一点。资料都放在下面了,有需要的直接拿,能用到多少就看你自己了。

这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以点击文章最下方的VX名片免费领取【保真100%】

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

揭秘Agent服务数据持久化难题:如何通过Docker数据卷实现高效挂载

第一章&#xff1a;Agent服务数据持久化挑战概述在分布式系统架构中&#xff0c;Agent 服务作为边缘计算和远程管理的核心组件&#xff0c;承担着采集、处理与上报关键运行数据的职责。然而&#xff0c;由于网络不稳定、节点频繁上下线以及资源受限等特性&#xff0c;Agent 服务…

作者头像 李华
网站建设 2026/4/10 19:38:40

量子计算太慢?教你用R调用GPU实现百倍加速(实测数据支持)

第一章&#xff1a;量子计算太慢&#xff1f;重新认识R语言在高性能计算中的潜力尽管量子计算被广泛视为下一代计算范式的突破口&#xff0c;其实际应用仍受限于硬件稳定性和算法成熟度。与此同时&#xff0c;传统高性能计算&#xff08;HPC&#xff09;领域正迎来软件层面的深…

作者头像 李华
网站建设 2026/4/13 13:02:28

独家披露:顶级期刊背后的空间转录组批次校正R脚本大公开

第一章&#xff1a;空间转录组批次效应校正的挑战与意义空间转录组技术能够同时捕获组织切片中基因表达的空间位置信息&#xff0c;为解析组织微环境、细胞互作和疾病机制提供了前所未有的视角。然而&#xff0c;在多批次实验中&#xff0c;由于样本处理时间、试剂批次、测序平…

作者头像 李华
网站建设 2026/4/13 16:51:33

Dify工作流可视化编辑十大坑,90%新手都会踩(附避坑方案)

第一章&#xff1a;Dify工作流可视化编辑的核心概念Dify 工作流的可视化编辑器提供了一种直观的方式来构建和管理复杂的 AI 应用流程。通过拖拽式界面&#xff0c;开发者可以将模型调用、条件判断、数据处理等节点连接成完整的执行链路&#xff0c;而无需编写大量胶水代码。可视…

作者头像 李华