Transformer、BERT、BEiT等模型相关八股及代码【自用】
🔹Transformer(Attention is All You Need):
Transformer 是一种基于注意力机制(Attention)的序列建模架构,由 Google 在 2017 年提出。它完全抛弃了 RNN 和 CNN,而是通过**自注意力机制(Self-Attention)**对序列中不同位置进行建模,支持并行计算,解决了传统序列模型的长距离依赖问题。
结构上,Transformer由多个Encoder-Decoder模块组成:
- Encoder:由多层堆叠,每层包括一个多头自注意力模块和前馈网络;
- Decoder:结构相似,但每层多了一个Encoder-Decoder Attention模块,用于对编码结果进行关注,同时使用mask避免信息泄露。
Transformer的核心是多头自注意力机制(Multi-Head Attention),它允许模型从多个子空间捕捉不同位置间的关系。同时加入位置编码来补充位置信息,以及残差连接、LayerNorm等结构提升训练稳定性。
Transformer的优点是并行效率高、效果好,已经成为NLP、CV等任务的基础架构,如BERT、GPT、T5等都基于它构建。
🔹 BERT(Bidirectional Encoder Representations from Transformers)
BERT 是 Google 在 2018 年提出的一种基于 Transformer 的预训练语言模型,它只使用了 Transformer 的 Encoder 部分,通过 双向自注意力机制建模上下文信息。 - 训练目标包括:
- Masked Language Modeling(MLM):随机遮掉句子中的部分词,让模型预测被遮掉的词;
- Next Sentence Prediction(NSP):判断两个句子是否在原文中相邻。
- 用途广泛:可以 fine-tune 到情感分析、问答、命名实体识别等下游任务中。
核心亮点是:通过大规模无监督预训练获得泛化能力,然后迁移到各种下游任务
🔹 ViT(Vision Transformer)
ViT 是 Google 在 2020 年提出的将 Transformer 应用于计算机视觉的模型。不同于 CNN 直接对图像进行卷积,ViT 将图像划分成固定大小的 Patch(图像块),再像词向量一样送入 Transformer 中。 - 每个 patch 被看作一个 token,加上位置编码后输入 Transformer。
- ViT 使用 纯Transformer架构完成图像分类,且在大规模数据上优于传统CNN。
ViT 的优势在于:能更灵活地建模长距离的图像区域关系,结构上也更统一,更容易迁移预训练策略
🔹 BEiT(Bidirectional Encoder Image Transformer)
BEiT 是微软在 2021 年提出的一个图像预训练模型,它借鉴了 BERT 的思想,将 图像自编码作为预训练任务,用 Transformer 来建模图像特征。 - 训练方法:
- BEiT 的核心创新在于采用了 Masked Image Modeling(MIM),类似于 BERT 中的 Masked Language Modeling(MLM)。在训练时,BEiT 随机遮掉图像的部分区域,要求模型去恢复这些缺失的部分。
- 为了让模型理解遮掉区域的上下文,BEiT 采用了一个 视觉词汇表(Visual Vocabulary),将图像块映射到词向量空间,这个视觉词汇表是通过训练得到的。
- 优势:
- BEiT 能够通过自监督学习在大规模无标注数据上进行预训练,获得强大的表征能力,并在图像分类等下游任务上表现出色。
- 通过引入 双向上下文,模型在图像理解时能够捕捉更丰富的长距离依赖信息
🔹 MAE(Masked Autoencoders for Image)
MAE 是 Facebook AI 在 2021 年提出的图像自编码器模型,旨在提升图像预训练的效率和效果。MAE 模型的核心创新点是采用了 更高比例的遮挡,并且通过 自编码器的方式进行训练。
- 训练方法:
- MAE 的训练方式是通过 Masking 75% 的图像块,只输入 25% 的可见区域进行训练。剩余部分则被模型恢复出来。与 BEiT 不同的是,MAE 主要关注 自编码任务,即输入部分图像并要求模型还原完整图像。
- 这种方法使得 MAE 在预训练阶段大幅度减少了计算量(因为只需要处理 25% 的图像),提升了训练效率。
- 优势:
- MAE 的遮挡策略有效降低了训练的计算成本,同时依赖于 Transformer 对长程依赖关系的建模能力,能够提高图像的表示能力。
- MAE 采用了 部分输入 和 重建的训练方式,有效提升了图像理解能力,尤其在下游任务(如图像分类、目标检测等)中表现出色。
- Transformer为何使用多头注意力机制?(为什么不使用一个头)
多头注意力允许模型在不同子空间中并行学习多样化的依赖关系。单个注意力头可能只能捕捉单一模式,而多个头可以捕获不同位置间的多种关联(如局部/全局、语法/语义关系)。多头的注意力有助于网络捕捉到更丰富的特征/信息,增强模型表达能力和鲁棒性。 - 捕获多样化的上下文信息
不同的注意力头能够学习输入序列中不同类型的关系。例如:
- 一个头可能关注局部语法结构(如相邻词语的修饰关系)
- 另一个头可能捕捉长距离依赖(如主语与谓语的跨句关联)
- 某些头可能专注于特定类型的语义角色(如时间、地点等)
这种分工使得模型能够同时从多个角度理解输入序列,而单头注意力只能捕捉单一模式的信息。 - 子空间表示的多样性
每个头的查询(Q)、键(K)、值(V)矩阵被投影到不同的低维子空间:
- 假设原始维度为 dmodeld_{model}dmodel,使用 hhh个头时,每个头的维度为 dk=dmodel/hd_k = d_{model}/hdk=dmodel/h
- 这种投影相当于对特征空间进行解耦,不同子空间可能编码正交的特征(如句法、语义、位置等)
- 单头注意力在单一高维空间中的线性变换可能难以有效分离这些特征 - 模型容量的高效扩展
- 总计算量保持与单头相当:
单头计算复杂度为 O(n2⋅dmodel)O(n^2 \cdot d_{model})O(n2⋅dmodel),多头则为 O(h⋅n2⋅dk)=O(n2⋅dmodel)O(h \cdot n ^2 \cdot d_k)=O(n^2 \cdot d_{model})O(h⋅n2⋅dk)=O(n2⋅dmodel)
- 参数量仅线性增加:
单头参数为 3dmodel23d_{model}^23dmodel2,多头参数为 3h⋅(dmodel/h)2⋅h=3dmodel23h \cdot ( d_{model}/h)^2 \cdot h =3d_{model}^23h⋅(dmodel/h)2⋅h=3dmodel2(实际与原论文实现方式有关) - 增强模型鲁棒性
- 类似集成学习(Ensemble Learning)的效果,不同头可能学习到互补的注意力模式
- 即使某些头失效(如梯度消失或噪声干扰),其他头仍能提供有效信息
- 实验表明,不同头在实际任务中确实表现出注意力模式的差异化(可视化结果可见于原始论文) - 并行计算优势
各头的计算完全独立,可充分利用GPU等硬件加速
多头注意力机制通过特征空间的解耦、计算效率的平衡以及注意力模式的多样化,显著提升了模型对复杂语言现象的表征能力。尽管增加头数会带来一定程度的冗余,但这种设计在实践中的收益远超其理论上的缺陷,成为Transformer架构的核心创新之一。 - Transformer为什么Q和K使用不同的权重矩阵生成,为何不能使用同一个值进行自身的点乘? (注意和第一个问题的区别)
Transformer中为查询(Q)和键(K)使用不同的权重矩阵生成,而不共享权重或直接使用输入向量自身进行点乘,主要基于以下关键原因:
A. 打破对称性限制,捕捉方向性依赖
- 非对称关系的必要性:
语言中的依赖关系通常具有方向性(如动词→宾语、修饰词→被修饰词)。若Q和K共享权重(即Q=KQ=K Q=K),注意力分数计算将强制满足对称性(Score(qi,kj)=Score(qj,ki)Score(q_i,k_j)=Score(q_j,k_i)Score(qi,kj)=Score(qj,ki)),导致模型无法区分“A关注B”和“B关注A”的非对称语义。 - 示例:
在句子“狗追猫”中,“追”对“猫”的关注度应显著高于反向关系。若Q和K对称,模型无法有效建模这种方向性依赖。
B. 特征空间的解耦与多样性 - 独立投影的优势:
- Q和K通过不同的权重矩阵 WQW^QWQ、 WKW^KWK投影到不同的语义子空间,使二者分别专注于:
- 查询(Q):主动寻找相关信息(如“当前词需要什么上下文”)
- 键(K):被动提供匹配信息(如“其他词能提供什么特征”)
- 若共享权重 WQ=WKW^Q = W^KWQ=WK,Q和K将被约束在同一子空间,失去解耦带来的表征灵活性。
- Q和K通过不同的权重矩阵 WQW^QWQ、 WKW^KWK投影到不同的语义子空间,使二者分别专注于:
- 数学视角:
假设输入向量为x xx,则: q=WQx,k=WKxq=W^Qx,k=W^Kxq=WQx,k=WKx
若 WQ≠WKW^Q \neq W^KWQ=WK,二者投影方向不同,点积 ⟨q,k⟩⟨q,k⟩⟨q,k⟩可视为两个独立子空间的相关性度量,比单一空间的相似性计算更具区分性。
C. 避免自注意力退化为相似性检测 - 共享权重的风险:
若Q和K共享权重(即 qi=kiq_i =k_iqi=ki),注意力分数退化为输入向量在同一投影空间的自相似性(Score(xi,xj)=⟨Wxi,Wxj⟩Score(x_i,x_j)=⟨Wx_i,Wx_j⟩Score(xi,xj)=⟨Wxi,Wxj⟩)。这会导致:- 模型仅能检测“哪些位置与当前位置相似”,而无法区分“主动查询”与“被动匹配”的差异。
- 难以建模复杂交互(如因果依赖、层级结构等需非对称处理的场景)。
- 实验证据:
研究(如《Self-Attention with Relative Position Representations》)表明,强制对称性会显著降低翻译任务的性能(BLEU值下降约1.5-2分)。
D. 增强模型的表达能力 - 参数独立的增益:
WQ和WK的独立性使模型参数量加倍,理论上扩展了函数空间的覆盖范围。尽管这会增加过拟合风险,但通过正则化(如Dropout)和大量数据训练可有效缓解。 - 动态交互的灵活性:
不同投影允许Q和K在训练过程中自适应调整投影方向。例如:- 某些注意力头可能学习到“前向关注”(如预测下一个词)
- 其他头可能捕捉“后向关注”(如回指消解)
- Transformer计算attention的时候为何选择点乘而不是加法?两者计算复杂度和效果上有什么区别?
A. 点乘注意力的核心优势
(1) 计算效率更高
- 数学形式简化:
点乘注意力直接通过矩阵乘法计算相似度:
Attention(Q,K,V)=softmax(QKTdk)VAttention(Q,K,V)=\text{softmax}(\frac{QK^T}{\sqrt{d_k}})VAttention(Q,K,V)=softmax(dkQKT)V - 而加法注意力需引入额外参数和非线性变换:
Score=vTtanh(Wqq+Wkk)\text{Score}=v^T\tanh(W_qq+W_kk)Score=vTtanh(Wqq+Wkk) - 点乘避免了中间的权重矩阵Wq,Wk和激活函数计算,显著减少计算量。
- GPU并行优化:
矩阵乘法(如 QKTQK^TQKT)是高度优化的基础操作,在现代GPU/TPU上可极致并行化。加法注意力中的逐元素操作(如拼接 + 激活函数)难以充分利用硬件加速。
(2) 高维相似性更有效 - 向量内积的几何意义:
点积 ⟨q,k⟩⟨q,k⟩⟨q,k⟩本质是余弦相似度的缩放版本(当向量长度固定时)。在高维空间(如 dk=512d_k=512dk=512)中,点积能有效区分不同方向的向量。 - 维度缩放的必要性:
当 dkd_kdk较大时,点积值趋于过大,导致softmax梯度消失。缩放因子1dk\frac{1}{\sqrt{d_k}}dk1可稳定数值,避免梯度问题。
(3) 参数效率 - 无额外参数:
点乘注意力仅需学习 Q,K,VQ,K,VQ,K,V的投影矩阵,而加法注意力需额外引入参数(如 Wq,Wk,WvW_q,W_k,W_vWq,Wk,Wv)。对于大规模模型(如 dmodel=1024d_{model}=1024dmodel=1024),点乘显著减少参数量。
B. 加法注意力的特点与局限
(1) 理论优势 - 非线性交互能力:
通过激活函数(如tanh)和权重矩阵,加法注意力能学习更复杂的相似度函数,理论上表达能力更强。 - 可处理异质空间:
当查询和键的维度不同时,加法注意力可通过调整权重矩阵适配,而点乘需强制维度一致。
(2) 实际缺陷 - 计算复杂度高:
时间复杂度为O(n2⋅(d+h))(h为隐藏层维度),显著高于点乘的O(n2⋅d)。当序列长度n较大时(如n=1024),计算量差距急剧扩大。 - 难以并行化:
非线性操作(如tanh)和逐元素计算限制了并行加速能力,尤其在长序列场景下效率低下。 - 优化难度大:
更多参数和复杂计算路径易导致梯度不稳定,需更精细的初始化(如Xavier初始化)和正则化。
C. 计算复杂度与效果对比
特性
点乘注意力
加法注意力
时间复杂度
O(n2⋅d)O(n^2 \cdot d)O(n2⋅d)
O(n2⋅(d+h))O(n^2 \cdot(d+h))O(n2⋅(d+h))
空间复杂度
O(n2)O(n^2)O(n2)(存储注意力矩阵)
O(n2⋅h)O(n^2 \cdot h )O(n2⋅h)
参数量
3d23d^23d2(Q, K, V投影矩阵)
2d2+dh+h2d^2+dh+h2d2+dh+h (Q/K投影+隐藏层参数)
并行性
完全并行(矩阵乘法)
部分并行(逐元素操作限制)
实际训练速度
快(硬件优化友好)
慢(计算路径复杂)
表达能力
依赖线性相似度
更强的非线性交互能力
典型应用场景
Transformer、BERT等大规模模型
早期RNN+Attention模型(如Bahdanau et al.)
D. 结论
Transformer选择点乘注意力的根本原因在于:
- 计算效率:矩阵乘法高度并行化,适合GPU加速。
- 参数精简:减少冗余参数,降低过拟合风险。
- 高维有效性:点积在高维空间能有效区分语义相似度。
- 工程友好性:易实现、调试和扩展。
尽管加法注意力理论上具备更强的非线性建模能力,但其计算成本和优化难度在大规模数据场景下成为致命瓶颈。点乘注意力通过简洁高效的设计,在效果与效率之间取得了最优平衡,成为Transformer及其衍生模型的核心选择。 - 为什么在进行softmax之前需要对attention进行scaled(为什么除以dk的平方根),并使用公式推导进行讲解
在Transformer中,对注意力分数进行缩放(即除以dk\sqrt{d_k}dk)的核心目的是控制点积的数值范围,防止Softmax梯度消失。其数学推导与方差稳定密切相关,具体分析如下:
Attention(Q,K,V)=softmax(QKTdk)VAttention(Q,K,V)=\text{softmax}(\frac{QK^T}{\sqrt{d_k}})VAttention(Q,K,V)=softmax(dkQKT)V
为什么要除以dk\sqrt{d_k}dk:
- QKTQK^TQKT 是两个高维向量点积,若dkd_kdk较大,其值会具有较大的方差,容易导致softmax结果过于极端(趋近于0或1),使梯度消失或训练不稳定。
- 通过除以dk\sqrt{d_k}dk,可以让点积的方差趋于稳定,避免softmax饱和,提高训练效果。
推导解释:
设 qi,kj∼N(0,1)q_i,k_j∼N(0,1)qi,kj∼N(0,1),则点积 qi⋅kjq_i \cdot k_jqi⋅kj 是 dkd_kdk 项的和,其期望为 0,方差为 dkd_kdk。
若不缩放,方差为 d_k;缩放后变为 11,适配 softmax 的敏感度。
- 在计算attention score的时候如何对padding做mask操作?
使用一个 mask矩阵(通常是布尔类型或0/−∞矩阵),将padding位置的注意力打分设为非常小的负值(如−1e9),这样经过softmax后几乎为0
scores = Q @ K.T / sqrt(d_k)
scores = scores.masked_fill(padding_mask == 0, -1e9) # padding_mask中为0的位置是pad
attention = softmax(scores)
在Transformer中,对padding进行mask操作的步骤如下:
- 生成Padding Mask矩阵:
创建一个与输入序列形状相同的布尔矩阵,其中padding位置为True(或1),其余为False(或0)。例如,若输入序列的shape为(batch_size, seq_len),则mask的shape为(batch_size, 1, 1, seq_len)(扩展维度以适应多头注意力)。 - 计算注意力分数:
通过查询(Q)和键(K)的点积计算原始注意力分数: Scores=QKTdk\text{Scores}=\frac{QK^T}{\sqrt{d_k}}Scores=dkQKT,此时Scores的shape为(batch_size, num_heads, seq_len, seq_len) - 应用Padding Mask:
将Mask矩阵中True的位置替换为极小的值(如-1e9),以压制这些位置的权重:
scores = scores.masked_fill(mask, -1e9) - Softmax归一化:
对处理后的Scores应用Softmax,得到注意力权重: Attention Weights=Softmax(Scores)\text{Attention Weights}=\text{Softmax}(\text{Scores})Attention Weights=Softmax(Scores),Padding位置的权重接近0,不会参与后续对值(V)的加权求和。
关键点:
- Mask应用时机:在Softmax之前修改Scores,确保无效位置不影响概率分布。
- 广播机制:通过扩展Mask的维度(如(batch_size, 1, 1, seq_len)),使其自动对齐多头注意力的多维计算。
- 结合因果Mask(解码器):在解码器中,还需叠加前瞻Mask(禁止关注未来位置),与Padding Mask共同作用
示例:
假设输入序列长度为5,其中后2个位置是padding,则: - Padding Mask矩阵为:[ [False, False, False, True, True] ]
- 应用后,Scores矩阵的最后两列(对应padding位置)被替换为-1e9,Softmax后权重趋近于0。
通过这种方式,Transformer有效忽略了padding位置的信息,提升了模型的鲁棒性。
- 为什么在进行多头注意力的时候需要对每个head进行降维?(可以参考上面一个问题)
在Transformer的多头注意力机制中,对每个注意力头(head)进行降维的核心目的是平衡模型表达能力与计算效率,同时确保数值稳定性。具体原因如下:
- 若原始维度为 dmodeld_{model}dmodel,每个head仍使用全维度,多个head拼接后维度会暴增。
- 所以每个head降维为 dk=dmodel/hd_k=d_{model}/hdk=dmodel/h,拼接后再通过线性映射恢复原始维度。
-
大概讲一下Transformer的Encoder模块?
Transformer Encoder由N=6层堆叠模块组成,每层包括: -
多头自注意力(Multi-head Self-Attention)
-
残差连接 + LayerNorm
-
前馈神经网络(Feed Forward, 两层线性 + 激活ReLU)
-
再加残差 + LayerNorm
class EncoderLayer(nn.Module):
‘’‘Encoder层’‘’
def init(self, args):
super().init()
# 一个 Layer 中有两个 LayerNorm,分别在 Attention 之前和 MLP 之前
self.attention_norm = LayerNorm(args.n_embd)
# Encoder 不需要掩码,传入 is_causal=False
self.attention = MultiHeadAttention(args, is_causal=False)
self.fnn_norm = LayerNorm(args.n_embd)
self.feed_forward = MLP(args)def forward(self, x):
# Layer Norm
norm_x = self.attention_norm(x)
# 自注意力
h = x + self.attention.forward(norm_x, norm_x, norm_x)
# 经过前馈神经网络
out = h + self.feed_forward.forward(self.fnn_norm(h))
return out
然后我们搭建一个 Encoder,由 N 个 Encoder Layer 组成,在最后会加入一个 Layer Norm 实现规范化:
class Encoder(nn.Module):
‘’‘Encoder 块’‘’
def init(self, args):
super(Encoder, self).init()
# 一个 Encoder 由 N 个 Encoder Layer 组成
self.layers = nn.ModuleList([EncoderLayer(args) for _ in range(args.n_layer)])
self.norm = LayerNorm(args.n_embd)def forward(self, x):
“分别通过 N 层 Encoder Layer”
for layer in self.layers:
x = layer(x)
return self.norm(x)
通过 Encoder 的输出,就是输入编码之后的结果。 -
为何在获取输入词向量之后需要对矩阵乘以embedding size的开方?意义是什么?
x=Embedding(token)⋅dmodelx=\text{Embedding}(token)⋅\sqrt{d_{model}}x=Embedding(token)⋅dmodel
- 保持输入的向量方差和后续层相匹配,避免初始输入太小。
- 这是与初始化策略相对齐(如Xavier初始化)的一种手段,提高训练稳定性。
- 简单介绍一下Transformer的位置编码?有什么意义和优缺点?
Transformer是无序结构(不像RNN),需要引入位置信息弥补顺序信息缺失。
常见的位置编码方式:
- 固定正余弦位置编码(sinusoidal)
- 学习的位置编码(learnable)
优缺点:
暂时无法在飞书文档外展示此内容
注意力机制可以实现良好的并行计算,但同时,其注意力计算的方式也导致序列中相对位置的丢失。在 RNN、LSTM 中,输入序列会沿着语句本身的顺序被依次递归处理,因此输入序列的顺序提供了极其重要的信息,这也和自然语言的本身特性非常吻合。
但从上文对注意力机制的分析我们可以发现,在注意力机制的计算过程中,对于序列中的每一个 token,其他各个位置对其来说都是平等的,即“我喜欢你”和“你喜欢我”在注意力机制看来是完全相同的,但无疑这是注意力机制存在的一个巨大问题。因此,为使用序列顺序信息,保留序列中的相对位置信息,Transformer 采用了位置编码机制,该机制也在之后被多种模型沿用。
位置编码,即根据序列中 token 的相对位置对其进行编码,再将位置编码加入词向量编码中。位置编码的方式有很多,Transformer 使用了正余弦函数来进行位置编码(绝对位置编码Sinusoidal),其编码方式为:
PE(pos,2i)=sin(pos/100002i/dmodel)PE(pos,2i+1)=cos(pos/100002i/dmodel)PE(pos, 2i) = sin(pos/10000^{2i/d_{model}})\\ PE(pos, 2i+1) = cos(pos/10000^{2i/d_{model}})PE(pos,2i)=sin(pos/100002i/dmodel)PE(pos,2i+1)=cos(pos/100002i/dmodel)
上式中,pos 为 token 在句子中的位置,2i 和 2i+1 则是指示了 token 是奇数位置还是偶数位置,从上式中我们可以看出对于奇数位置的 token 和偶数位置的 token,Transformer 采用了不同的函数进行编码。
我们以一个简单的例子来说明位置编码的计算过程:假如我们输入的是一个长度为 4 的句子"I like to code",我们可以得到下面的词向量矩阵 x\rm xx,其中每一行代表的就是一个词向量, x0=[0.1,0.2,0.3,0.4]\rm x_0=[0.1,0.2,0.3,0.4]x0=[0.1,0.2,0.3,0.4]对应的就是“I”的词向量,它的pos就是为0,以此类推,第二行代表的是“like”的词向量,它的pos就是1:
x=[0.10.20.30.40.20.30.40.50.30.40.50.60.40.50.60.7]\rm x = \begin{bmatrix} 0.1 & 0.2 & 0.3 & 0.4 \\ 0.2 & 0.3 & 0.4 & 0.5 \\ 0.3 & 0.4 & 0.5 & 0.6 \\ 0.4 & 0.5 & 0.6 & 0.7 \end{bmatrix}x=0.10.20.30.40.20.30.40.50.30.40.50.60.40.50.60.7
则经过位置编码后的词向量为:
xPE=[0.10.20.30.40.20.30.40.50.30.40.50.60.40.50.60.7]+[sin(0100000)cos(0100000)sin(0100002/4)cos(0100002/4)sin(1100000)cos(1100000)sin(1100002/4)cos(1100002/4)sin(2100000)cos(2100000)sin(2100002/4)cos(2100002/4)sin(3100000)cos(3100000)sin(3100002/4)cos(3100002/4)]=[0.11.20.31.41.0410.840.411.491.209−0.0160.521.590.541−0.4890.8951.655]\rm x_{PE} = \begin{bmatrix} 0.1 & 0.2 & 0.3 & 0.4 \\ 0.2 & 0.3 & 0.4 & 0.5 \\ 0.3 & 0.4 & 0.5 & 0.6 \\ 0.4 & 0.5 & 0.6 & 0.7 \end{bmatrix} + \begin{bmatrix} \sin(\frac{0}{10000^0}) & \cos(\frac{0}{10000^0}) & \sin(\frac{0}{10000^{2/4}}) & \cos(\frac{0}{10000^{2/4}}) \\ \sin(\frac{1}{10000^0}) & \cos(\frac{1}{10000^0}) & \sin(\frac{1}{10000^{2/4}}) & \cos(\frac{1}{10000^{2/4}}) \\ \sin(\frac{2}{10000^0}) & \cos(\frac{2}{10000^0}) & \sin(\frac{2}{10000^{2/4}}) & \cos(\frac{2}{10000^{2/4}}) \\ \sin(\frac{3}{10000^0}) & \cos(\frac{3}{10000^0}) & \sin(\frac{3}{10000^{2/4}}) & \cos(\frac{3}{10000^{2/4}}) \end{bmatrix} = \begin{bmatrix} 0.1 & 1.2 & 0.3 & 1.4 \\ 1.041 & 0.84 & 0.41 & 1.49 \\ 1.209 & -0.016 & 0.52 & 1.59 \\ 0.541 & -0.489 & 0.895 & 1.655 \end{bmatrix}xPE=0.10.20.30.40.20.30.40.50.30.40.50.60.40.50.60.7+sin(1000000)sin(1000001)sin(1000002)sin(1000003)cos(1000000)cos(1000001)cos(1000002)cos(1000003)sin(100002/40)sin(100002/41)sin(100002/42)sin(100002/43)cos(100002/40)cos(100002/41)cos(100002/42)cos(100002/43)=0.11.0411.2090.5411.20.84−0.016−0.4890.30.410.520.8951.41.491.591.655
我们可以使用如下的代码来获取上述例子的位置编码:
import numpy as np
import matplotlib.pyplot as plt
def PositionEncoding(seq_len, d_model, n=10000):
P = np.zeros((seq_len, d_model))
for k in range(seq_len):
for i in np.arange(int(d_model/2)):
denominator = np.power(n, 2i/d_model)
P[k, 2i] = np.sin(k/denominator)
P[k, 2*i+1] = np.cos(k/denominator)
return P
P = PositionEncoding(seq_len=4, d_model=4, n=100)
print§
[[ 0. 1. 0. 1. ]
[ 0.84147098 0.54030231 0.09983342 0.99500417]
[ 0.90929743 -0.41614684 0.19866933 0.98006658]
[ 0.14112001 -0.9899925 0.29552021 0.95533649]]
这样的位置编码主要有两个好处:
- 使 PE 能够适应比训练集里面所有句子更长的句子,假设训练集里面最长的句子是有 20 个单词,突然来了一个长度为 21 的句子,则使用公式计算的方法可以计算出第 21 位的 Embedding。
- 可以让模型容易地计算出相对位置,对于固定长度的间距 k,PE(pos+k) 可以用 PE(pos) 计算得到。因为 Sin(A+B) = Sin(A)Cos(B) + Cos(A)Sin(B), Cos(A+B) = Cos(A)Cos(B) - Sin(A)Sin(B)。
我们也可以通过严谨的数学推导证明该编码方式的优越性。原始的 Transformer Embedding 可以表示为:
f(⋯ ,xm,⋯ ,xn,⋯ )=f(⋯ ,xn,⋯ ,xm,⋯ )\begin{equation}f(\cdots,\boldsymbol{x}_m,\cdots,\boldsymbol{x}_n,\cdots)=f(\cdots,\boldsymbol{x}_n,\cdots,\boldsymbol{x}_m,\cdots)\end{equation}f(⋯,xm,⋯,xn,⋯)=f(⋯,xn,⋯,xm,⋯)
很明显,这样的函数是不具有不对称性的,也就是无法表征相对位置信息。我们想要得到这样一种编码方式:
f~(⋯ ,xm,⋯ ,xn,⋯ )=f(⋯ ,xm+pm,⋯ ,xn+pn,⋯ )\begin{equation}\tilde{f}(\cdots,\boldsymbol{x}_m,\cdots,\boldsymbol{x}_n,\cdots)=f(\cdots,\boldsymbol{x}_m + \boldsymbol{p}_m,\cdots,\boldsymbol{x}_n + \boldsymbol{p}_n,\cdots)\end{equation}f~(⋯,xm,⋯,xn,⋯)=f(⋯,xm+pm,⋯,xn+pn,⋯)
这里加上的 pmp_mpm, pnp_npn就是位置编码。接下来我们将 f(...,xm+pm,...,xn+pn)f(...,x_m+p_m,...,x_n+p_n)f(...,xm+pm,...,xn+pn) 在 m,n 两个位置上做泰勒展开:
f~≈f+pm⊤∂f∂xm+pn⊤∂f∂xn+12pm⊤∂2f∂xm2pm+12pn⊤∂2f∂xn2pn+pm⊤∂2f∂xm∂xnpn⏟pm⊤Hpn\begin{equation}\tilde{f}\approx f + \boldsymbol{p}_m^{\top} \frac{\partial f}{\partial \boldsymbol{x}_m} + \boldsymbol{p}_n^{\top} \frac{\partial f}{\partial \boldsymbol{x}_n} + \frac{1}{2}\boldsymbol{p}_m^{\top} \frac{\partial^2 f}{\partial \boldsymbol{x}_m^2}\boldsymbol{p}_m + \frac{1}{2}\boldsymbol{p}_n^{\top} \frac{\partial^2 f}{\partial \boldsymbol{x}_n^2}\boldsymbol{p}_n + \underbrace{\boldsymbol{p}_m^{\top} \frac{\partial^2 f}{\partial \boldsymbol{x}_m \partial \boldsymbol{x}_n}\boldsymbol{p}_n}_{\boldsymbol{p}_m^{\top} \boldsymbol{\mathcal{H}} \boldsymbol{p}_n}\end{equation}f~≈f+pm⊤∂xm∂f+pn⊤∂xn∂f+21pm⊤∂xm2∂2fpm+21pn⊤∂xn2∂2fpn+pm⊤Hpnpm⊤∂xm∂xn∂2fpn
可以看到第1项与位置无关,2~5项仅依赖单一位置,第6项(f 分别对 m、n 求偏导)与两个位置有关,所以我们希望第六项( pmTHpnp_m^THp_npmTHpn)表达相对位置信息,即求一个函数 g 使得:pmTHpn=g(m−n)p_m^THp_n = g(m-n)pmTHpn=g(m−n)
我们假设 HHH 是一个单位矩阵,则:pmTHpn=pmTpn=⟨pm,pn⟩=g(m−n)p_m^THp_n = p_m^Tp_n = \langle\boldsymbol{p}_m, \boldsymbol{p}_n\rangle = g(m-n)pmTHpn=pmTpn=⟨pm,pn⟩=g(m−n)
通过将向量 [x,y] 视为复数 x+yi,基于复数的运算法则构建方程:
⟨pm,pn⟩=Re[pmpn∗]\begin{equation}\langle\boldsymbol{p}_m, \boldsymbol{p}_n\rangle = \text{Re}[\boldsymbol{p}_m \boldsymbol{p}_n^*]\end{equation}⟨pm,pn⟩=Re[pmpn∗]
再假设存在复数 qm−nq_{m-n}qm−n 使得:
pmpn∗=qm−n\begin{equation}\boldsymbol{p}_m \boldsymbol{p}_n^* = \boldsymbol{q}_{m-n}\end{equation}pmpn∗=qm−n
使用复数的指数形式求解这个方程,得到二维情形下位置编码的解:
pm=eimθ⇔pm=(cosmθsinmθ)\begin{equation}\boldsymbol{p}_m = e^{\text{i}m\theta}\quad\Leftrightarrow\quad \boldsymbol{p}_m=\begin{pmatrix}\cos m\theta \\ \sin m\theta\end{pmatrix}\end{equation}pm=eimθ⇔pm=(cosmθsinmθ)
由于内积满足线性叠加性,所以更高维的偶数维位置编码,我们可以表示为多个二维位置编码的组合:
pm=(eimθ0eimθ1⋮eimθd/2−1)⇔pm=(cosmθ0sinmθ0cosmθ1sinmθ1⋮cosmθd/2−1sinmθd/2−1)\begin{equation}\boldsymbol{p}_m = \begin{pmatrix}e^{\text{i}m\theta_0} \\ e^{\text{i}m\theta_1} \\ \vdots \\ e^{\text{i}m\theta_{d/2-1}}\end{pmatrix}\quad\Leftrightarrow\quad \boldsymbol{p}_m=\begin{pmatrix}\cos m\theta_0 \\ \sin m\theta_0 \\ \cos m\theta_1 \\ \sin m\theta_1 \\ \vdots \\ \cos m\theta_{d/2-1} \\ \sin m\theta_{d/2-1} \end{pmatrix}\end{equation}pm=eimθ0eimθ1⋮eimθd/2−1⇔pm=cosmθ0sinmθ0cosmθ1sinmθ1⋮cosmθd/2−1sinmθd/2−1
再取 θi=10000−2i/d\theta_i = 10000^{-2i/d}θi=10000−2i/d(该形式可以使得随着|m−n|的增大,⟨pm,pn⟩有着趋于零的趋势,这一点可以通过对位置编码做积分来证明,而 base 取为 10000 是实验结果),就得到了上文的编码方式。
当 HHH 不是一个单位矩阵时,因为模型的 Embedding 层所形成的 d 维向量之间任意两个维度的相关性比较小,满足一定的解耦性,我们可以将其视作对角矩阵,那么使用上述编码:
pm⊤Hpn=∑i=1d/2H2i,2icosmθicosnθi+H2i+1,2i+1sinmθisinnθi\begin{equation}\boldsymbol{p}_m^{\top} \boldsymbol{\mathcal{H}} \boldsymbol{p}_n=\sum_{i=1}^{d/2} \boldsymbol{\mathcal{H}}_{2i,2i} \cos m\theta_i \cos n\theta_i + \boldsymbol{\mathcal{H}}_{2i+1,2i+1} \sin m\theta_i \sin n\theta_i\end{equation}pm⊤Hpn=i=1∑d/2H2i,2icosmθicosnθi+H2i+1,2i+1sinmθisinnθi
通过积化和差:
∑i=1d/212(H2i,2i+H2i+1,2i+1)cos(m−n)θi+12(H2i,2i−H2i+1,2i+1)cos(m+n)θi\begin{equation}\sum_{i=1}^{d/2} \frac{1}{2}\left(\boldsymbol{\mathcal{H}}_{2i,2i} + \boldsymbol{\mathcal{H}}_{2i+1,2i+1}\right) \cos (m-n)\theta_i + \frac{1}{2}\left(\boldsymbol{\mathcal{H}}_{2i,2i} - \boldsymbol{\mathcal{H}}_{2i+1,2i+1}\right) \cos (m+n)\theta_i \end{equation}i=1∑d/221(H2i,2i+H2i+1,2i+1)cos(m−n)θi+21(H2i,2i−H2i+1,2i+1)cos(m+n)θi
说明该编码仍然可以表示相对位置。
基于上述原理,我们实现一个位置编码层:
class PositionalEncoding(nn.Module):
‘’‘位置编码模块’‘’
def __init__(self, args):super(PositionalEncoding, self).__init__()# Dropout 层self.dropout = nn.Dropout(p=args.dropout)# block size 是序列的最大长度pe = torch.zeros(args.block_size, args.n_embd)position = torch.arange(0, args.block_size).unsqueeze(1)# 计算 thetadiv_term = torch.exp(torch.arange(0, args.n_embd, 2) * -(math.log(10000.0) / args.n_embd))# 分别计算 sin、cos 结果pe[:, 0::2] = torch.sin(position * div_term)pe[:, 1::2] = torch.cos(position * div_term)pe = pe.unsqueeze(0)self.register_buffer("pe", pe)def forward(self, x):# 将位置编码加到 Embedding 结果上x = x + self.pe[:, : x.size(1)].requires_grad_(False)return self.dropout(x)
- 你还了解哪些关于位置编码的技术,各自的优缺点是什么?
- 相对位置编码(Relative Positional Encoding)
- 如Transformer-XL、T5
- 优点:更灵活,不依赖绝对位置,适合长序列泛化
- 缺点:实现复杂,效率略低
- Rotary Position Embedding(RoPE)
- GPT-NeoX, LLaMA等使用
- 优点:自然结合相对位置信息,适合自回归建模
- 缺点:数学复杂,难以调试
- ALiBi(Attention with Linear Biases)
- 线性偏置引入位置信息
- 优点:简单高效,训练稳定
- 缺点:理论支持少,适应性差
- 简单讲一下Transformer中的残差结构以及意义。
由于 Transformer 模型结构较复杂、层数较深,为了避免模型退化,Transformer 采用了残差连接的思想来连接每一个子层。残差连接,即下一层的输入不仅是上一层的输出,还包括上一层的输入。残差连接允许最底层信息直接传到最高层,让高层专注于残差的学习。
例如,在 Encoder 中,在第一个子层,输入进入多头自注意力层的同时会直接传递到该层的输出,然后该层的输出会与原输入相加,再进行标准化。在第二个子层也是一样。即:
x=x+MultiHeadSelfAttention(LayerNorm(x))x = x + MultiHeadSelfAttention(LayerNorm(x))x=x+MultiHeadSelfAttention(LayerNorm(x))
output=x+FNN(LayerNorm(x))output = x + FNN(LayerNorm(x))output=x+FNN(LayerNorm(x))
我们在代码实现中,通过在层的 forward 计算中加上原值来实现残差连接:
注意力计算
h = x + self.attention.forward(self.attention_norm(x))
经过前馈神经网络
out = h + self.feed_forward.forward(self.fnn_norm(h))
在上文代码中,self.attention_norm 和 self.fnn_norm 都是 LayerNorm 层,self.attn 是注意力层,而 self.feed_forward 是前馈神经网络。
每个子层(Self-Attention、Feed-Forward)后都有一个残差连接: Output=LayerNorm(x+Sublayer(x))Output=LayerNorm(x+Sublayer(x))Output=LayerNorm(x+Sublayer(x))
意义:
- 缓解深层网络的梯度消失问题
- 保留原始输入信息
- 有助于训练收敛
- 为什么transformer块使用LayerNorm而不是BatchNorm?LayerNorm 在Transformer的位置是哪里?
-
原因: BatchNorm依赖batch维度,在NLP中sequence长度和内容差异大,不稳定。而LayerNorm是对每个样本的特征维度归一化,更适合序列建模。
-
位置: 在每个子层的输出后:
out = x + Sublayer(x)
out = LayerNorm(out)
层归一化,也就是 Layer Norm,是深度学习中经典的归一化操作。神经网络主流的归一化一般有两种,批归一化(Batch Norm)和层归一化(Layer Norm)。
归一化核心是为了让不同层输入的取值范围或者分布能够比较一致。由于深度神经网络中每一层的输入都是上一层的输出,因此多层传递下,对网络中较高的层,之前的所有神经层的参数变化会导致其输入的分布发生较大的改变。也就是说,随着神经网络参数的更新,各层的输出分布是不相同的,且差异会随着网络深度的增大而增大。但是,需要预测的条件分布始终是相同的,从而也就造成了预测的误差。
因此,在深度神经网络中,往往需要归一化操作,将每一层的输入都归一化成标准正态分布。批归一化是指在一个 mini-batch 上进行归一化,相当于对一个 batch 对样本拆分出来一部分,首先计算样本的均值:
μj=1m∑i=1mZji\mu_j = \frac{1}{m}\sum^{m}_{i=1}Z_j^{i}μj=m1i=1∑mZji
其中, ZjiZ_j^{i}Zji 是样本 i 在第 j 个维度上的值,m 就是 mini-batch 的大小。
再计算样本的方差:σ2=1m∑i=1m(Zji−μj)2\sigma^2 = \frac{1}{m}\sum^{m}_{i=1}(Z_j^i - \mu_j)^2σ2=m1i=1∑m(Zji−μj)2
最后,对每个样本的值减去均值再除以标准差来将这一个 mini-batch 的样本的分布转化为标准正态分布:
Zj~=Zj−μjσ2+ϵ\widetilde{Z_j} = \frac{Z_j - \mu_j}{\sqrt{\sigma^2 + \epsilon}}Zj=σ2+ϵZj−μj
此处加上 ϵ\epsilonϵ 这一极小量是为了避免分母为0。
但是,批归一化存在一些缺陷,例如: -
当显存有限,mini-batch 较小时,Batch Norm 取的样本的均值和方差不能反映全局的统计分布信息,从而导致效果变差;
-
对于在时间维度展开的 RNN,不同句子的同一分布大概率不同,所以 Batch Norm 的归一化会失去意义;
-
在训练时,Batch Norm 需要保存每个 step 的统计信息(均值和方差)。在测试时,由于变长句子的特性,测试集可能出现比训练集更长的句子,所以对于后面位置的 step,是没有训练的统计量使用的;
-
应用 Batch Norm,每个 step 都需要去保存和计算 batch 统计量,耗时又耗力
因此,出现了在深度神经网络中更常用、效果更好的层归一化(Layer Norm)。相较于 Batch Norm 在每一层统计所有样本的均值和方差,Layer Norm 在每个样本上计算其所有层的均值和方差,从而使每个样本的分布达到稳定。Layer Norm 的归一化方式其实和 Batch Norm 是完全一样的,只是统计统计量的维度不同。
基于上述进行归一化的公式,我们可以简单地实现一个 Layer Norm 层:
class LayerNorm(nn.Module):
‘’’ Layer Norm 层’‘’
def init(self, features, eps=1e-6):
super(LayerNorm, self).init()线性矩阵做映射
self.a_2 = nn.Parameter(torch.ones(features))self.b_2 = nn.Parameter(torch.zeros(features))self.eps = eps
def forward(self, x):
# 在统计每个样本所有维度的值,求均值和方差
mean = x.mean(-1, keepdim=True) # mean: [bsz, max_len, 1]
std = x.std(-1, keepdim=True) # std: [bsz, max_len, 1]注意这里也在最后一个维度发生了广播
return self.a_2 * (x - mean) / (std + self.eps) + self.b_2
注意,在我们上文实现的 Layer Norm 层中,有两个线性矩阵进行映射。
13. 简单讲一下BatchNorm技术,以及它的优缺点。
核心思想: 对batch中每一维特征归一化,提高训练速度与稳定性。
在深度神经网络中,往往需要归一化操作,将每一层的输入都归一化成标准正态分布。批归一化是指在一个 mini-batch 上进行归一化,相当于对一个 batch 对样本拆分出来一部分,首先计算样本的均值: μj=1m∑i=1mZji\mu_j = \frac{1}{m}\sum^{m}_{i=1}Z_j^{i}μj=m1i=1∑mZji
再计算样本的方差: σ2=1m∑i=1m(Zji−μj)2\sigma^2 = \frac{1}{m}\sum^{m}_{i=1}(Z_j^i - \mu_j)^2σ2=m1i=1∑m(Zji−μj)2
最后,对每个样本的值减去均值再除以标准差来将这一个 mini-batch 的样本的分布转化为标准正态分布: Zj~=Zj−μjσ2+ϵ\widetilde{Z_j} = \frac{Z_j - \mu_j}{\sqrt{\sigma^2 + \epsilon}}Zj=σ2+ϵZj−μj,此处加上 ϵ\epsilonϵ 这一极小量是为了避免分母为0。
优点:
- 减少内部协变量偏移
- 加速收敛
- 可做轻微正则化
批归一化存在一些缺陷,例如: - 当显存有限,mini-batch 较小时,Batch Norm 取的样本的均值和方差不能反映全局的统计分布信息,从而导致效果变差;
- 对于在时间维度展开的 RNN,不同句子的同一分布大概率不同,所以 Batch Norm 的归一化会失去意义;
- 在训练时,Batch Norm 需要保存每个 step 的统计信息(均值和方差)。在测试时,由于变长句子的特性,测试集可能出现比训练集更长的句子,所以对于后面位置的 step,是没有训练的统计量使用的;
- 应用 Batch Norm,每个 step 都需要去保存和计算 batch 统计量,耗时又耗力
-
简单描述一下Transformer中的前馈神经网络?使用了什么激活函数?相关优缺点?
前馈神经网络(Feed Forward Neural Network,下简称 FFN),也就是我们在上一节提过的每一层的神经元都和上下两层的每一个神经元完全连接的网络结构。每一个 Encoder Layer 都包含一个上文讲的注意力机制和一个前馈神经网络。前馈神经网络的实现是较为简单的:
Transformer 的前馈神经网络是由两个线性层中间加一个 RELU 激活函数组成的,以及前馈神经网络还加入了一个 Dropout 层来防止过拟合。
class MLP(nn.Module):
‘’‘前馈神经网络’‘’
def init(self, dim: int, hidden_dim: int, dropout: float):
super().init()
# 定义第一层线性变换,从输入维度到隐藏维度
self.w1 = nn.Linear(dim, hidden_dim, bias=False)
# 定义第二层线性变换,从隐藏维度到输入维度
self.w2 = nn.Linear(hidden_dim, dim, bias=False)
# 定义dropout层,用于防止过拟合
self.dropout = nn.Dropout(dropout)def forward(self, x):
# 前向传播函数
# 首先,输入x通过第一层线性变换和RELU激活函数
# 然后,结果乘以输入x通过第三层线性变换的结果
# 最后,通过第二层线性变换和dropout层
return self.dropout(self.w2(F.relu(self.w1(x))))
FFN(x) = max(0, xW1 + b1)W2 + b2
- 通常使用ReLU或GELU作为激活函数
- 优点: 提供非线性表达能力,增强模型容量
- 缺点: 参数多、计算成本高(尤其在大模型中)
- Encoder端和Decoder端是如何进行交互的?(在这里可以问一下关于seq2seq的attention知识)
- Decoder中有一个Encoder-Decoder Attention模块,其Query来自Decoder,Key/Value来自Encoder输出。
这是典型的Seq2Seq Attention结构,用于: - 让Decoder关注输入句子的不同部分
- 提供跨序列的对齐信息
Seq2Seq,即序列到序列,是一种经典 NLP 任务。具体而言,是指模型输入的是一个自然语言序列 input=(x1,x2,x3...xn)input = (x_1, x_2, x_3...x_n)input=(x1,x2,x3...xn),输出的是一个可能不等长的自然语言序列 output=(y1,y2,y3...ym)output = (y_1, y_2, y_3...y_m)output=(y1,y2,y3...ym)。事实上,Seq2Seq 是 NLP 最经典的任务,几乎所有的 NLP 任务都可以视为 Seq2Seq 任务。例如文本分类任务,可以视为输出长度为 1 的目标序列(如在上式中 mmm = 1);词性标注任务,可以视为输出与输入序列等长的目标序列(如在上式中 mmm = nnn)。
机器翻译任务即是一个经典的 Seq2Seq 任务,例如,我们的输入可能是“今天天气真好”,输出是“Today is a good day.”。Transformer 是一个经典的 Seq2Seq 模型,即模型的输入为文本序列,输出为另一个文本序列。事实上,Transformer 一开始正是应用在机器翻译任务上的。
对于 Seq2Seq 任务,一般的思路是对自然语言序列进行编码再解码。所谓编码,就是将输入的自然语言序列通过隐藏层编码成能够表征语义的向量(或矩阵),可以简单理解为更复杂的词向量表示。而解码,就是对输入的自然语言序列编码得到的向量或矩阵通过隐藏层输出,再解码成对应的自然语言目标序列。通过编码再解码,就可以实现 Seq2Seq 任务。
Transformer 中的 Encoder,就是用于上述的编码过程;Decoder 则用于上述的解码过程。
- Decoder阶段的多头自注意力和encoder的多头自注意力有什么区别?(为什么需要decoder自注意力需要进行 sequence mask)
- Encoder自注意力: 没有限制,可以看到整个输入
- Decoder自注意力: 使用了未来位置的mask(causal mask),防止看到还未生成的token,保持自回归特性。
- Transformer的并行化体现在哪个地方?Decoder端可以做并行化吗?
- Encoder是完全可并行的,每个位置可同时计算。
- Decoder中的自注意力是顺序的,因需mask未来token,不可并行生成(但训练时可并行计算所有位置,使用mask)。
- Transformer训练的时候学习率是如何设定的?Dropout是如何设定的,位置在哪里?Dropout 在测试的需要有什么需要注意的吗?
- 学习率调度: 采用warm-up策略:
lr=dmodel−0.5⋅min(step−0.5,step⋅warmup−1.5)l_r=d_{model}^{−0.5} \cdot min(step^{−0.5},step \cdot warmup^{−1.5})lr=dmodel−0.5⋅min(step−0.5,step⋅warmup−1.5) - Dropout位置:
- Attention权重之后
- FFN中间层
- 残差连接后
- 测试时注意: Dropout应关闭,以使用完整模型能力。
- 解码端的残差结构有没有把后续未被看见的mask信息添加进来,造成信息的泄露。
没有。Decoder中自注意力层前使用了严格的causal mask(上三角mask),即便加上残差也不会看到未来信息,因为mask作用于attention score本身。
import torch
import torch.nn as nnclass SelfAttention(nn.Module):def __init__(self, embed_size, heads):super(SelfAttention, self).__init__()self.embed_size = embed_size # 输入特征维度self.heads = heads # 注意力头的数量self.head_dim = embed_size // heads # 每个头的维度# 确保embed_size能被heads整除assert (self.head_dim * heads == embed_size), "Embed size needs to be div by heads"# 定义Q、K、V的线性变换层(无偏置)self.values = nn.Linear(self.head_dim, self.head_dim, bias=False)self.keys = nn.Linear(self.head_dim, self.head_dim, bias=False)self.queries = nn.Linear(self.head_dim, self.head_dim, bias=False)# 多头注意力拼接后的输出层self.fc_out = nn.Linear(heads * self.head_dim, embed_size)def forward(self, values, keys, query, mask):N = query.shape[0] # 批大小(样本数量)value_len, key_len, query_len = values.shape[1], keys.shape[1], query.shape[1] # 获取序列长度# 将输入拆分为多头(reshape成多头形式),reshape成(N, seq_len, heads, head_dim)values = values.reshape(N, value_len, self.heads, self.head_dim)keys = keys.reshape(N, key_len, self.heads, self.head_dim)queries = query.reshape(N, query_len, self.heads, self.head_dim)# 对Q、K、V进行线性变换values = self.values(values)keys = self.keys(keys)queries = self.queries(queries)# 计算注意力分数(query和key的点积)energy = torch.einsum("nqhd,nkhd->nhqk", [queries, keys])# 输出形状:(N, heads, query_len, key_len)# 应用mask(将mask为0的位置填充负无穷)if mask is not None:energy = energy.masked_fill(mask == 0, float("-1e20"))# 计算注意力权重(softmax归一化)attention = torch.softmax(energy / (self.embed_size ** (1 / 2)), dim=3)# 注意力权重与value相乘得到输出out = torch.einsum("nhql,nlhd->nqhd", [attention, values])out = out.reshape(N, query_len, self.heads * self.head_dim) # 拼接多头结果# 通过输出层进行线性变换out = self.fc_out(out)return outclass TransformerBlock(nn.Module):def __init__(self, embed_size, heads, dropout, forward_expansion):# 参数说明# embed_size: 输入特征维度# heads: 注意力头的数量# dropout: Dropout层的概率# forward_expansion: 前馈神经网络的扩展倍数super(TransformerBlock, self).__init__()# 初始化自注意力机制层self.attention = SelfAttention(embed_size, heads)# 初始化两个LayerNorm层,用于残差连接后的归一化self.norm1 = nn.LayerNorm(embed_size)self.norm2 = nn.LayerNorm(embed_size)# 前馈神经网络(FFN)部分self.feed_forward = nn.Sequential(nn.Linear(embed_size, forward_expansion * embed_size), # 扩展维度nn.ReLU(), # 激活函数nn.Linear(forward_expansion * embed_size, embed_size) # 恢复原始维度)# Dropout层,用于防止过拟合self.dropout = nn.Dropout(dropout)def forward(self, value, key, query, mask):# 1. 计算自注意力attention = self.attention(value, key, query, mask)# 2. 残差连接 + LayerNorm + Dropout# 注意:原始Transformer论文是先Norm再残差,这里实现的是先残差再Norm的变体x = self.dropout(self.norm1(attention + query))# 3. 前馈神经网络forward = self.feed_forward(x)# 4. 第二次残差连接 + LayerNorm + Dropoutout = self.dropout(self.norm2(forward + x))return outclass Encoder(nn.Module):def __init__(self,src_vocab_size, # (int) 源语言词汇表大小,决定词嵌入矩阵的行数embed_size, # (int) 词嵌入维度,每个词/位置将被映射到的向量维度num_layers, # (int) Transformer编码器堆叠的层数heads, # (int) 多头注意力机制的头数device, # (torch.device) 计算设备 (CPU/GPU)forward_expansion, # (int) 前馈网络隐藏层的扩展倍数,决定FFN中间层维度dropout, # (float) Dropout概率,用于正则化max_length # (int) 最大序列长度,决定位置嵌入矩阵的行数):super(Encoder, self).__init__()# 初始化参数self.embed_size = embed_size # (int) 保存词嵌入维度self.device = device # (torch.device) 保存计算设备# 词嵌入层:将词汇索引映射为embed_size维向量# 参数:# - num_embeddings: (int) 词汇表大小(src_vocab_size)# - embedding_dim: (int) 嵌入维度(embed_size)self.word_embedding = nn.Embedding(src_vocab_size, embed_size)# 位置嵌入层:将位置索引映射为embed_size维向量# 参数:# - num_embeddings: (int) 最大序列长度(max_length)# - embedding_dim: (int) 嵌入维度(embed_size)self.position_embedding = nn.Embedding(max_length, embed_size)# 创建num_layers个TransformerBlock组成的编码器堆叠# 每个TransformerBlock参数:# - embed_size: (int) 输入/输出维度# - heads: (int) 注意力头数# - dropout: (float) Dropout概率# - forward_expansion: (int) FFN扩展倍数self.layers = nn.ModuleList([TransformerBlock(embed_size,heads,dropout=dropout,forward_expansion=forward_expansion)for _ in range(num_layers)] # 循环num_layers次)# Dropout层# 参数:# - p: (float) Dropout概率(dropout)self.dropout = nn.Dropout(dropout)def forward(self, x, mask):"""前向传播参数:x: (torch.Tensor) 输入张量,形状为(N, seq_length),N是batch大小,seq_length是序列长度,包含词汇索引mask: (torch.Tensor) 掩码张量,用于屏蔽无效位置(如padding位置)返回:(torch.Tensor) 编码后的输出张量,形状为(N, seq_length, embed_size),包含序列中每个位置的上下文感知表示"""# 获取batch大小(N)和序列长度(seq_length)N, seq_lengh = x.shape# 生成位置索引: [0, 1, 2, ..., seq_length-1]# 并扩展到batch维度 -> (N, seq_length)positions = torch.arange(0, seq_lengh).expand(N, seq_lengh).to(self.device)# 1. 词嵌入: (N, seq_length) -> (N, seq_length, embed_size)# 2. 位置嵌入: (N, seq_length) -> (N, seq_length, embed_size)# 3. 相加后进行dropoutout = self.dropout(self.word_embedding(x) + self.position_embedding(positions))# 逐层通过Transformer编码器for layer in self.layers:# 每层的输入作为Q,K,V (自注意力机制)# mask用于屏蔽无效位置out = layer(out, out, out, mask) # Q=K=V=out# 返回编码后的表示 (N, seq_length, embed_size)return outclass DecoderBlock(nn.Module):def __init__(self, embed_size, heads, forward_expansion, dropout, device):"""解码器块初始化参数:embed_size: 嵌入维度大小heads: 注意力头的数量forward_expansion: 前向扩展因子(用于TransformerBlock)dropout: dropout概率device: 计算设备(CPU/GPU)"""super(DecoderBlock, self).__init__()# 自注意力机制self.attention = SelfAttention(embed_size, heads)# 层归一化self.norm = nn.LayerNorm(embed_size)# Transformer块(包含多头注意力、前馈网络等)self.transformer_block = TransformerBlock(embed_size, heads, dropout, forward_expansion)# Dropout层self.dropout = nn.Dropout(dropout)def forward(self, x, value, key, src_mask, trg_mask):"""前向传播参数:x: 输入张量value: 来自编码器的valuekey: 来自编码器的keysrc_mask: 源序列掩码(用于屏蔽padding等)trg_mask: 目标序列掩码(用于防止未来信息泄露)返回:解码后的输出"""# 自注意力计算(使用目标序列掩码)attention = self.attention(x, x, x, trg_mask)# 残差连接 + 层归一化 + dropoutquery = self.dropout(self.norm(attention + x))# 通过Transformer块(使用编码器的key和value)out = self.transformer_block(value, key, query, src_mask)return outclass Decoder(nn.Module):def __init__(self,trg_vocab_size,embed_size,num_layers,heads,forward_expansion,dropout,device,max_length):"""解码器初始化参数:trg_vocab_size: 目标词汇表大小embed_size: 嵌入维度num_layers: 解码器层数heads: 注意力头数量forward_expansion: 前向扩展因子dropout: dropout概率device: 计算设备max_length: 最大序列长度(用于位置编码)"""super(Decoder, self).__init__()self.device = device# 词嵌入层self.word_embedding = nn.Embedding(trg_vocab_size, embed_size)# 位置嵌入层self.position_embedding = nn.Embedding(max_length, embed_size)# 创建多层解码器块self.layers = nn.ModuleList([DecoderBlock(embed_size, heads, forward_expansion, dropout, device)for _ in range(num_layers)])# 输出全连接层(将嵌入维度映射回词汇表大小)self.fc_out = nn.Linear(embed_size, trg_vocab_size)# Dropout层self.dropout = nn.Dropout(dropout)def forward(self, x, enc_out, src_mask, trg_mask):"""前向传播参数:x: 输入序列(目标语言)enc_out: 编码器输出(用于key和value)src_mask: 源序列掩码trg_mask: 目标序列掩码返回:解码后的输出(词汇表概率分布)"""# 获取batch大小和序列长度N, seq_length = x.shape# 创建位置索引(0到seq_length-1)positions = torch.arange(0, seq_length).expand(N, seq_length).to(self.device)# 词嵌入 + 位置嵌入 + dropoutx = self.dropout((self.word_embedding(x) + self.position_embedding(positions)))# 通过所有解码器层for layer in self.layers:x = layer(x, enc_out, enc_out, src_mask, trg_mask)# 通过全连接层得到输出(词汇表概率分布)out = self.fc_out(x)return outclass Transformer(nn.Module):def __init__(self,src_vocab_size, # 源语言词汇表大小trg_vocab_size, # 目标语言词汇表大小src_pad_idx, # 源语言padding索引trg_pad_idx, # 目标语言padding索引embed_size=256, # 嵌入维度(默认256)num_layers=6, # 编码器/解码器层数(默认6)forward_expansion=4, # 前向扩展因子(默认4)heads=8, # 多头注意力头数(默认8)dropout=0, # dropout率(默认0)device="cuda", # 计算设备(默认cuda)max_length=100 # 最大序列长度(默认100)):super(Transformer, self).__init__()# 初始化编码器self.encoder = Encoder(src_vocab_size,embed_size,num_layers,heads,device,forward_expansion,dropout,max_length)# 初始化解码器self.decoder = Decoder(trg_vocab_size,embed_size,num_layers,heads,forward_expansion,dropout,device,max_length)# 保存padding索引和设备信息self.src_pad_idx = src_pad_idxself.trg_pad_idx = trg_pad_idxself.device = devicedef make_src_mask(self, src):"""创建源语言掩码参数:src: 源语言输入序列返回:src_mask: 源语言掩码张量(N, 1, 1, src_len)其中padding位置为False,其他为True"""# 创建布尔掩码(padding位置为False)src_mask = (src != self.src_pad_idx).unsqueeze(1).unsqueeze(2)return src_mask.to(self.device)def make_trg_mask(self, trg):"""创建目标语言掩码参数:trg: 目标语言输入序列返回:trg_mask: 目标语言掩码张量(N, 1, trg_len, trg_len)包含1)防止看到未来信息的三角掩码2) padding位置的掩码"""N, trg_len = trg.shape# 创建下三角矩阵(防止看到未来信息)trg_mask = torch.tril(torch.ones((trg_len, trg_len))).expand(N, 1, trg_len, trg_len)return trg_mask.to(self.device)def forward(self, src, trg):"""前向传播参数:src: 源语言输入序列trg: 目标语言输入序列返回:out: 模型输出(目标语言词汇表上的概率分布)"""# 创建源语言掩码src_mask = self.make_src_mask(src)# 创建目标语言掩码trg_mask = self.make_trg_mask(trg)# 编码器处理源语言enc_src = self.encoder(src, src_mask)# 解码器生成目标语言out = self.decoder(trg, enc_src, src_mask, trg_mask)return outif __name__ == "__main__":device = torch.device("cuda" if torch.cuda.is_available() else "cpu")print(device)x = torch.tensor([[1, 5, 6, 4, 3, 9, 5, 2, 0], [1, 8, 7, 3, 4, 5, 6, 7, 2]]).to(device)trg = torch.tensor([[1, 7, 4, 3, 5, 9, 2, 0], [1, 5, 6, 2, 4, 7, 6, 2]]).to(device)src_pad_idx = 0 # 源语言padding索引trg_pad_idx = 0 # 目标语言padding索引src_vocab_size = 10 # 源语言词汇表大小trg_vocab_size = 10 # 目标语言词汇表大小model = Transformer(src_vocab_size, trg_vocab_size, src_pad_idx, trg_pad_idx, device=device).to(device)out = model(x, trg[:, :-1])print(out.shape)