Transformer架构详解(1):transformer架构总览

一、从 RNN 到 Transformer

在 Transformer 成为自然语言处理、语音识别、多模态建模等序列任务的通用基础架构之前,循环神经网络(RNN) 及其进阶变体 LSTM、GRU,长期主导序列建模领域。Transformer 的诞生并非无迹可寻的创新,而是为了系统性解决 RNN 系列架构的固有缺陷,打破传统序列模型的性能上限与算力瓶颈。

1.1 RNN 的缺陷

文本、语音、时间序列等序列数据的核心特性是时序依赖:数据的排列顺序蕴含关键信息,当前时刻的输出结果高度依赖前文的上下文信息。针对这一特性,传统 RNN 采用循环迭代、逐帧串行的核心设计思路。

简单来说,RNN 的工作模式为逐时间步遍历计算:模型按顺序读取序列中的单个元素(如文本中的单个字词),将上一时刻输出的隐藏状态与当前时刻输入融合计算,得到当前时刻的输出与全新隐藏状态。依托这种循环传递机制,RNN 理论上可以累积前文信息,完成序列上下文的基础建模。

基础 RNN 存在严重的梯度消失与梯度爆炸问题,无法适配长序列训练场景。为此,业界迭代出 LSTM(Long Short-Term Memory,长短期记忆神经网络) 与 GRU(Gate Recurrent Unit,门控循环单元) 两种优化模型,通过引入门控机制与细胞状态,有效缓解了长序列训练中的梯度衰减问题,具备了更长距离的上下文留存能力,也因此成为 Transformer 问世前,学术界与工业界序列建模的主流方案。

但是 RNN 结构存在以下几个致命的缺点:

  • RNN 的信息传递严格遵循时序递进规则,前文信息需要经过数十甚至上百次循环迭代,才能传递到后续时间步。即便 LSTM、GRU 通过门控机制保留了部分关键信息,但随着序列长度增加,信息仍会持续衰减、失真,无法精准建立远距离语义关联,从而导致模型准确率与泛化能力大幅下降
  • RNN 架构存在强时序约束:第 t 时间步的计算必须等待第 t-1 时间步计算完成后才能执行,不存在任何并行空间。完整序列的计算全程串行,无法利用 GPU、TPU 等算力芯片的并行加速优势,极大制约了模型迭代效率与性能上限
  • 模型仅能被动累积前文所有信息,无法根据语义场景自适应区分不同元素的重要程度。以语句 “虽然今天下雨,但是我还是出门了" 为例,句子核心语义由转折词 ”但是“ 决定,而非 "今天“、“下雨” 等修饰信息。但 RNN 无法动态分配语义权重,只能无差别融合所有前文信息,导致关键语义被冗余信息稀释,模型的语义理解与建模能力存在天然瓶颈

1.2 Transformer 的引入

2017 年发表的经典论文 《Attention Is All You Need》 首次提出 Transformer 架构,该方案彻底抛弃了 RNN 沿用多年的串行循环建模范式,以自注意力机制(Self-Attention) 为核心重构序列建模逻辑,一次性解决了传统循环模型的所有核心痛点,实现了序列建模技术的跨越式升级。其核心创新可归纳为三点:

  • 彻底根治长距离依赖问题。Transformer 依托自注意力机制,支持序列中任意两个元素直接建立语义关联,无需经过时序传递。无论元素在序列中距离多远,模型都能直接计算二者的关联权重,全程无信息衰减
  • Transformer 完全舍弃循环迭代结构,支持完整序列一次性输入,所有位置的特征计算、注意力权重计算可同步完成,充分发挥 GPU 的并行算力优势
  • 自注意力机制可根据输入序列的真实语义,动态计算每个元素与全局上下文的关联权重,自动聚焦核心关键信息、弱化无效冗余信息,精准捕捉文本中的转折、关联、修饰等复杂语义结构,大幅提升了模型的序列理解与生成能力

凭借上述颠覆性优势,Transformer 不仅全面取代 LSTM、GRU 成为序列建模主流方案,更逐步发展为 NLP 大模型、视觉 Transformer、多模态大模型的通用核心基座架构,支撑了当下人工智能生成技术的快速发展。

二、Transformer 架构整体介绍

2.1 Transformer 整体结构

2017 年 《Attention Is All You Need》 论文提出的原始 Transformer 由两大部分组成:(左边部分是 Encoder,右边是 Decoder)

  • Encoder: 读取输入序列,生成上下文表示。每层包含一个 Self-Attention 和一个 FFN(Feed-forward network,前馈网络),所有 token 可以互相关注(双向注意力)
  • Decoder: 基于 Encoder 的输出,自回归地生成目标序列。每层包含一个带因果掩码的 Self-Attention(只能看到已生成的 token)、一个 Cross-Attention(关注 Encoder 输出)和一个 FFN

在原文中,这里的 N 等于 6,也就是说 Encoder 和 Decoder 都包含 6 个 block,如下图所示:

2.2 Encoder 部分

Encoder 部分主要包括:

  • Input Embedding:输入嵌入
  • Positional Encoding:位置编码
  • Multi-Head Attention:多头注意力机制
  • Feed Forward:前馈网络

2.2.1 Input Embedding - 输入嵌入层

输入嵌入层的作用是将输入的单词或者符号转换成固定维度的向量表示,使其能够被模型处理(因为计算机本身并不能处理文字等信息,需要将其转为向量来处理)。

单词的 Embedding 有很多种方式可以获取,例如可以采用 Word2Vec、Glove 等算法预训练得到,也可以直接在 Transformer 中训练得到。

在论文中的实现如下:

建一个形状为 [vocab_size, d_model] 的矩阵,其中:

  • vocab_sizevocab\_size:词表大小(比如 30000)
  • dmodeld_{model}:嵌入维度(比如 512)

这个矩阵的每一行,就对应一个 token 的向量表示。要查某个词时,只需找到它的 ID(整数索引),然后取出对应行即可。

这个矩阵里的数字一开始是随机初始化的,然后在训练过程中通过反向传播不断更新,最终学到的每一行就是那个 token 的语义向量

例如句子:“Nice to meet you”,这里将其映射为一个向量矩阵(假设嵌入维度为 3,但实际中一般较大,可以是225、1024等等,也可以自己设定)

1
2
3
4
5
6
[
[0.2, 0.1, 0.3],
[0.5, 0.4, 0.2],
[0.7, 0.8, 0.1],
[0.3, 0.6, 0.9]
]

[0.5, 0.4, 0.2] 就是对应于 “to” 的嵌入向量。

2.2.2 Positional Encoding - 位置编码

Transformer 中除了单词的 Embedding,还需要使用位置 Embedding 表示单词出现在句子中的位置。**因为 Transformer 不采用 RNN 的结构,而是使用全局信息,不能利用单词的顺序信息,而这部分信息对于 NLP 来说非常重要。**所以 Transformer 中使用位置 Embedding 保存单词在序列中的相对或绝对位置。

位置编码通常是一组与嵌入向量维度相同的向量 ,PE 的维度与嵌入向量 是一样的。PE 可以通过训练得到,也可以使用某种公式计算得到。在 Transformer 中采用了后者,计算公式如下:

PosEnc(pos,2i)=sin(pos100002idmodel)PosEnc(pos,2i+1)=cos(pos100002idmodel)PosEnc_{(pos, 2i)} = \sin(\frac{pos}{10000^{\frac{2i}{d_{model}}}}) \\ PosEnc_{(pos, 2i+1)} = \cos(\frac{pos}{10000^{\frac{2i}{d_{model}}}})

  • pos:token 在序列中的位置(0,1,2,…)
  • dmodeld_{model}:PE 的维度,和嵌入向量一样
  • 2i2i+12i、2i+1:当位置编码为偶数时使用 sin函数,当位置编码为奇数时使用 cos函数,取值范围:$ 0 \leq i \leq d_model/2 $

这样可以让模型容易地通过公式计算出相对位置,对于固定长度的间距 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)Sin(A+B) = Sin(A)Cos(B) + Cos(A)Sin(B),Cos(A+B) = Cos(A)Cos(B) - Sin(A)Sin(B)

下面以刚才的 “Nice to meet you” 为例,计算一下它的位置编码:

单词 “Nice”(位置为 0)、“to”(位置为 1)、“meet”(位置为 2)、“you”(位置为 3) 的位置编码计算如下:

对于 “Nice”(位置为 0):

PosEnc(0,0)=sin(01000003)=0PosEnc(0,1)=cos(01000013)=1PosEnc(0,2)=sin(01000023)=0PosEnc_{(0, 0)} = \sin(\frac{0}{10000^{\frac{0}{3}}})=0 \\ PosEnc_{(0, 1)} = \cos(\frac{0}{10000^{\frac{1}{3}}})=1 \\ PosEnc_{(0, 2)} = \sin(\frac{0}{10000^{\frac{2}{3}}})=0

对于 “to”(位置为 1):

PosEnc(1,0)=sin(11000003)=0.84147PosEnc(1,1)=cos(11000013)=0.54030PosEnc(1,2)=sin(11000023)=0.00215PosEnc_{(1, 0)} = \sin(\frac{1}{10000^{\frac{0}{3}}})=0.84147 \\ PosEnc_{(1, 1)} = \cos(\frac{1}{10000^{\frac{1}{3}}})=0.54030 \\ PosEnc_{(1, 2)} = \sin(\frac{1}{10000^{\frac{2}{3}}})=0.00215

同理,可以计算出剩下的位置编码:

1
2
3
4
5
6
PE = [
[0, 1, 0 ],
[0.84147, 0.54030, 0.00215],
[0.90930, 0.99569, 0.00431],
[0.14112, 0.99032, 0.00646]
]

最后还要将位置编码加到嵌入向量上,得到位置感知的嵌入:

  • “Nice” 的最终嵌入:[0.2 + 0, 0.1 + 1, 0.3 + 0]
  • “to” 的最终嵌入:[0.5 + 0.84147, 0.4 + 0.54030, 0.2 + 0.00215]
  • ......

现在得到的向量就是 Transformer 的输入。

2.2.3 Multi-Head Attention - 多头注意力机制 - 待完成


暂空


2.2.4 Norm - 层归一化

Add & Norm 层由 Add 和 Norm 两部分组成,其计算公式如下:

LayerNorm(X+MultiHeadAttention(X))LayerNorm(X+FeedForward(X))LayerNorm(X + MultiHeadAttention(X)) \\ LayerNorm(X+FeedForward(X))

Transformer 中主要利用 Layer Normalization 层归一化,而并不是批归一化,因为层归一化会将每一层神经元的输入都转成均值方差都一样的值,这样可以加快收敛。这有助于避免训练过程中的梯度消失问题,提高模型的稳定性。

在处理自然语言任务时,序列的长度通常是变化的。使用层归一化因为是对单个样本中的所有特征进行归一化,所以能够更好地处理这种可变长度的情况。而批归一化则依赖于整个批次的数据统计,这在处理小批次或可变长度的序列时可能不太有效。

此外,Transformer 模型特别依赖于捕捉长期依赖关系(即序列中相隔较远的元素之间的关系),而层归一化有助于缓解训练过程中可能出现的梯度消失问题,从而更有效地学习这些长期依赖关系。

下面看一个归一化的例子:

假设我们有一个神经网络,正在处理以下 4 个样本的小批次,每个样本有 2 个特征:

1
2
3
4
5
批次数据: 
样本1: [1, 5]
样本2: [2, 6]
样本3: [3, 7]
样本4: [4, 8]
  • 批归一化

在批归一化中,我们对每个特征在整个批次中进行归一化。以第一个特征为例,其均值和标准差分别为:

1
2
均值 = (1 + 2 + 3 + 4) / 4 = 2.5 
标准差 = sqrt(((1-2.5)² + (2-2.5)² + (3-2.5)² + (4-2.5)²) / 4) ≈ 1.29

然后,对每个样本的这个特征进行归一化:

1
2
3
4
5
归一化后的特征1: 
样本1: (1 - 2.5) / 1.29 ≈ -1.16
样本2: (2 - 2.5) / 1.29 ≈ -0.39
样本3: (3 - 2.5) / 1.29 ≈ 0.39
样本4: (4 - 2.5) / 1.29 ≈ 1.16
  • 层归一化

对于同样的数据,如果我们使用层归一化,那么归一化是在每个样本内部进行的。

以样本1为例:

1
样本1: [1, 5] 

对于样本1,我们计算其所有特征的均值和标准差:

1
2
均值 = (1 + 5) / 2 = 3
标准差 = sqrt(((1-3)² + (4-3)²) / 2) ≈ 1.58

然后对样本1的每个特征进行归一化:

1
2
3
归一化后的样本1: 
特征1: (1 - 3) / 1.58 ≈ -1.26
特征2: (3 - 3) / 1.41 = 0

对于其他样本,也会重复这一过程。

简单地说:

  • 批归一化是跨样本对每个特征分别归一化,每个特征的归一化基于整个批次的统计数据
  • 层归一化是在每个样本内部进行的,每个样本的所有特征都根据该样本的统计数据进行归一化

2.2.5 残差连接

在这里的残差连接(Residual Connection),主要是将多头注意力机制的输出变量加到原始输入变量(添加过位置编码后的嵌入向量)上,再经过层归一化。

Transformer 模型中的每个子层都伴随一个残差连接,然后紧接着一个层归一化操作。具体来说,对于每个子层(例如自注意力或前馈网络),输入首先通过子层自身,然后将子层的输出与输入进行相加(残差连接),最后对这个相加的结果进行层归一化。

2.2.6 Feed Forward - 前馈网络

在 Transformer 原文中 Feed Forward 的全称是 Position-wise Feed-Forward Networks(点对点前馈神经网络,简称FFN)。

Attention 负责”信息交互”——让 token 之间互相传递信息。但仅靠信息交互还不够,模型还需要对每个 token 的信息做”深度加工”。这就是前馈网络(Feed-Forward Network,FFN)的工作。

打个比方:Attention 像一场圆桌会议,大家互相交换意见;FFN 则是会后每个人回到自己工位上,独立消化吸收这些信息并形成自己的判断。FFN 对每个 token 独立地做非线性变换,不涉及 token 之间的交互。

标准 FFN 的结构非常简洁——先”升维”再”降维”,中间夹一个非线性激活函数:

activation(XW1+b1)W2+b2activation(XW_1+b_1)W_2+b_2

  • W1W_1:(dmodel,dff)d_{model}, d_{ff}),将维度从 dmodeld_{model} 扩展到 dffd_{ff}(通常 dff=4×dmodeld_{ff}=4 \times d_{model}
  • activationactivation:激活函数
  • W2W_2:(dff,dmodel)d_{ff},d_{model}),将维度从 dffd_{ff} 压缩会 dmodeld_{model}

其实它就是两个全连接层:第一个全连接层将输入的维度扩展(例如,从 512维扩展到 2048维),接着是一个激活函数(通常是 ReLU、GELU 或 SwiGLU),然后是第二个全连接层,不使用激活函数,将维度从扩展的维度缩减回原始维度(例如,从 2048维缩减回 512维)。

前馈网络处理完后,先对其进行一个残差连接,再进行层归一化处理。

2.2.7 总结

以上就是编码器部分的所有组件,编码器的作用主要是为了将输入编码为连续表示,并带有注意力信息;有助于帮助解码器在解码过程中关注输入中的重要词汇。当然,需要注意的是,可以将编码器堆叠 N 次,以进一步编码信息,其中每一层都有机会学习不同的注意力表示,从而提高 Transformer 的预测能力;如下图所示。

2.3 Decoder 部分

Decoder 的任务是生成文本序列,需要注意的是解码器是自回归的,Decoder 部分主要包括:

  • Output Embeding、Position Encoding:输出嵌入、位置编码
  • Masked Multi-Head Attention:具有掩码的多头注意力机制
  • Multi-Head Attention:多头注意力机制
  • Feed Forward:前馈网络
  • 分类器

2.3.1 Output Embedding & Positional Encoding

这里和 Encoder 部分的类似,就不过多介绍了。

不过可以注意到,Decoder 有两个输入:

  • 编码器的输出(Encoder Output / Memory):这是整个编码器处理完源序列后得到的一组特征向量。它作为**交叉注意力(Cross-Attention)**的 KV(键和值)输入,帮助解码器从源语言中提取相关信息。例如机器翻译中,源句子的语义信息就储存在这里
  • 解码器自身的输入序列(Decoder Input / Target Sequence):在训练时,这通常是目标序列(右移一位,包括其起始符号,通常是“<sos>”或“<bos>”(start of sequence or begin of sequence)),例如翻译的目标句子,并且施加掩码保证自回归。它作为自注意力Q、K、V,让解码器关注已经生成的部分,并避免看到未来的词。在推理时,这个输入是解码器前面已经生成的 token 序列

2.3.2 Masked Multi-Head Attention - 具有掩码的多头注意力机制 - 待完成


暂空


2.3.4 Multi-Head Attention

这里的多头注意力机制的原理是和Encoder部分一样的,不过需要注意的是:这一部分主要是将解码器当前生成的序列与原始输入序列(经过 Encoder 处理过的)联系起来,用于生成下一个目标单词。这部分的注意力机制作用主要有两个:

  1. **连接源序列和目标序列:**通过关注编码器的输出,解码器可以根据需要从源序列中提取相关的上下文信息;
  2. **动态关注不同部分:**同时,在解码过程中,模型可能会选择关注输入序列的不同部分。

2.3.5 Feed Forward - 前馈网络

参考 Encoder 部分的 Feed Forward。

2.3.6 分类器

在模型输出 “<eos>”(end of sequence) 时,程序结束。

Decoder block 最后的部分是利用一个线性层和一个 Softmax 预测下一个单词,在之前的网络层我们可以得到一个最终的输出 Z,因为 Mask 的存在,使得单词 0 的输出 Z0 只包含单词 0 的信息,如下:

Softmax 根据输出矩阵的每一行预测下一个单词:

三、代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt

def make_batch(sentences):
input_batch = [[src_vocab[n] for n in sentences[0].split()]]
output_batch = [[tgt_vocab[n] for n in sentences[1].split()]]
target_batch = [[tgt_vocab[n] for n in sentences[2].split()]]
return torch.LongTensor(input_batch), torch.LongTensor(output_batch), torch.LongTensor(target_batch)

def get_sinusoid_encoding_table(n_position, d_model):
def cal_angle(position, hid_idx):
return position / np.power(10000, 2 * (hid_idx // 2) / d_model)
def get_posi_angle_vec(position):
return [cal_angle(position, hid_j) for hid_j in range(d_model)]

sinusoid_table = np.array([get_posi_angle_vec(pos_i) for pos_i in range(n_position)])
sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2]) # dim 2i
sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2]) # dim 2i+1
return torch.FloatTensor(sinusoid_table)

def get_attn_pad_mask(seq_q, seq_k):
batch_size, len_q = seq_q.size()
batch_size, len_k = seq_k.size()
# eq(zero) is PAD token
pad_attn_mask = seq_k.data.eq(0).unsqueeze(1) # batch_size x 1 x len_k(=len_q), one is masking
return pad_attn_mask.expand(batch_size, len_q, len_k) # batch_size x len_q x len_k

def get_attn_subsequent_mask(seq):
attn_shape = [seq.size(0), seq.size(1), seq.size(1)]
subsequent_mask = np.triu(np.ones(attn_shape), k=1)
subsequent_mask = torch.from_numpy(subsequent_mask).byte()
return subsequent_mask

class ScaledDotProductAttention(nn.Module):
def __init__(self):
super(ScaledDotProductAttention, self).__init__()

def forward(self, Q, K, V, attn_mask):
scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k) # scores : [batch_size x n_heads x len_q(=len_k) x len_k(=len_q)]
scores.masked_fill_(attn_mask, -1e9) # Fills elements of self tensor with value where mask is one.
attn = nn.Softmax(dim=-1)(scores)
context = torch.matmul(attn, V)
return context, attn

class MultiHeadAttention(nn.Module):
def __init__(self):
super(MultiHeadAttention, self).__init__()
self.W_Q = nn.Linear(d_model, d_k * n_heads)
self.W_K = nn.Linear(d_model, d_k * n_heads)
self.W_V = nn.Linear(d_model, d_v * n_heads)
self.linear = nn.Linear(n_heads * d_v, d_model)
self.layer_norm = nn.LayerNorm(d_model)

def forward(self, Q, K, V, attn_mask):
# q: [batch_size x len_q x d_model], k: [batch_size x len_k x d_model], v: [batch_size x len_k x d_model]
residual, batch_size = Q, Q.size(0)
# (B, S, D) -proj-> (B, S, D) -split-> (B, S, H, W) -trans-> (B, H, S, W)
q_s = self.W_Q(Q).view(batch_size, -1, n_heads, d_k).transpose(1,2) # q_s: [batch_size x n_heads x len_q x d_k]
k_s = self.W_K(K).view(batch_size, -1, n_heads, d_k).transpose(1,2) # k_s: [batch_size x n_heads x len_k x d_k]
v_s = self.W_V(V).view(batch_size, -1, n_heads, d_v).transpose(1,2) # v_s: [batch_size x n_heads x len_k x d_v]

attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1) # attn_mask : [batch_size x n_heads x len_q x len_k]

# context: [batch_size x n_heads x len_q x d_v], attn: [batch_size x n_heads x len_q(=len_k) x len_k(=len_q)]
context, attn = ScaledDotProductAttention()(q_s, k_s, v_s, attn_mask)
context = context.transpose(1, 2).contiguous().view(batch_size, -1, n_heads * d_v) # context: [batch_size x len_q x n_heads * d_v]
output = self.linear(context)
return self.layer_norm(output + residual), attn # output: [batch_size x len_q x d_model]

class PoswiseFeedForwardNet(nn.Module):
def __init__(self):
super(PoswiseFeedForwardNet, self).__init__()
self.conv1 = nn.Conv1d(in_channels=d_model, out_channels=d_ff, kernel_size=1)
self.conv2 = nn.Conv1d(in_channels=d_ff, out_channels=d_model, kernel_size=1)
self.layer_norm = nn.LayerNorm(d_model)

def forward(self, inputs):
residual = inputs # inputs : [batch_size, len_q, d_model]
output = nn.ReLU()(self.conv1(inputs.transpose(1, 2)))
output = self.conv2(output).transpose(1, 2)
return self.layer_norm(output + residual)

class EncoderLayer(nn.Module):
def __init__(self):
super(EncoderLayer, self).__init__()
self.enc_self_attn = MultiHeadAttention()
self.pos_ffn = PoswiseFeedForwardNet()

def forward(self, enc_inputs, enc_self_attn_mask):
enc_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs, enc_self_attn_mask) # enc_inputs to same Q,K,V
enc_outputs = self.pos_ffn(enc_outputs) # enc_outputs: [batch_size x len_q x d_model]
return enc_outputs, attn

class DecoderLayer(nn.Module):
def __init__(self):
super(DecoderLayer, self).__init__()
self.dec_self_attn = MultiHeadAttention()
self.dec_enc_attn = MultiHeadAttention()
self.pos_ffn = PoswiseFeedForwardNet()

def forward(self, dec_inputs, enc_outputs, dec_self_attn_mask, dec_enc_attn_mask):
dec_outputs, dec_self_attn = self.dec_self_attn(dec_inputs, dec_inputs, dec_inputs, dec_self_attn_mask)
dec_outputs, dec_enc_attn = self.dec_enc_attn(dec_outputs, enc_outputs, enc_outputs, dec_enc_attn_mask)
dec_outputs = self.pos_ffn(dec_outputs)
return dec_outputs, dec_self_attn, dec_enc_attn

"""
编码器
"""
class Encoder(nn.Module):
def __init__(self):
super(Encoder, self).__init__()
# 将输入单词进行Embedding
self.src_emb = nn.Embedding(src_vocab_size, d_model) # src_vocab_size:词表大小;d_model:嵌入维度
# 添加位置编码
self.pos_emb = nn.Embedding.from_pretrained(get_sinusoid_encoding_table(src_len+1, d_model),freeze=True)
# 前馈神经网络
self.layers = nn.ModuleList([EncoderLayer() for _ in range(n_layers)])

def forward(self, enc_inputs): # enc_inputs : [batch_size x source_len]
# 词向量 和 位置编码进行相加
enc_outputs = self.src_emb(enc_inputs) + self.pos_emb(torch.LongTensor([[1,2,3,4,0]]))
#
enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs)
enc_self_attns = []
for layer in self.layers:
enc_outputs, enc_self_attn = layer(enc_outputs, enc_self_attn_mask)
enc_self_attns.append(enc_self_attn)
return enc_outputs, enc_self_attns


class Decoder(nn.Module):
def __init__(self):
super(Decoder, self).__init__()
self.tgt_emb = nn.Embedding(tgt_vocab_size, d_model)
self.pos_emb = nn.Embedding.from_pretrained(get_sinusoid_encoding_table(tgt_len+1, d_model),freeze=True)
self.layers = nn.ModuleList([DecoderLayer() for _ in range(n_layers)])

def forward(self, dec_inputs, enc_inputs, enc_outputs): # dec_inputs : [batch_size x target_len]
dec_outputs = self.tgt_emb(dec_inputs) + self.pos_emb(torch.LongTensor([[5,1,2,3,4]]))
dec_self_attn_pad_mask = get_attn_pad_mask(dec_inputs, dec_inputs)
dec_self_attn_subsequent_mask = get_attn_subsequent_mask(dec_inputs)
dec_self_attn_mask = torch.gt((dec_self_attn_pad_mask + dec_self_attn_subsequent_mask), 0)

dec_enc_attn_mask = get_attn_pad_mask(dec_inputs, enc_inputs)

dec_self_attns, dec_enc_attns = [], []
for layer in self.layers:
dec_outputs, dec_self_attn, dec_enc_attn = layer(dec_outputs, enc_outputs, dec_self_attn_mask, dec_enc_attn_mask)
dec_self_attns.append(dec_self_attn)
dec_enc_attns.append(dec_enc_attn)
return dec_outputs, dec_self_attns, dec_enc_attns


class Transformer(nn.Module):
def __init__(self):
super(Transformer, self).__init__()
# 编码器
self.encoder = Encoder()
# 解码器
self.decoder = Decoder()
# 解码器最后的分类器,分类器的输入d_model是解码层每个token的输出维度大小,需要将其转为词表大小,再计算softmax;计算哪个词出现的概率最大
self.projection = nn.Linear(d_model, tgt_vocab_size, bias=False)

def forward(self, enc_inputs, dec_inputs):
# Transformer的两个输入,一个是编码器的输入(源序列),一个是解码器的输入(目标序列)
# 其中,enc_inputs的大小应该是 [batch_size, src_len] ; dec_inputs的大小应该是 [batch_size, dec_inputs]

"""
源数据输入到encoder之后得到 enc_outputs, enc_self_attns;
enc_outputs是需要传给decoder的矩阵,表示源数据的表示特征
enc_self_attns表示单词之间的相关性矩阵
"""
enc_outputs, enc_self_attns = self.encoder(enc_inputs)

"""
decoder的输入数据包括三部分:
1. encoder得到的表示特征enc_outputs、
2. 解码器的输入dec_inputs(目标序列)、
3. 以及enc_inputs
"""
dec_outputs, dec_self_attns, dec_enc_attns = self.decoder(dec_inputs, enc_inputs, enc_outputs)

"""
将decoder的输出映射到词表大小,最后进行softmax输出即可
"""
dec_logits = self.projection(dec_outputs) # dec_logits : [batch_size x src_vocab_size x tgt_vocab_size]
return dec_logits.view(-1, dec_logits.size(-1)), enc_self_attns, dec_self_attns, dec_enc_attns

def showgraph(attn):
attn = attn[-1].squeeze(0)[0]
attn = attn.squeeze(0).data.numpy()
fig = plt.figure(figsize=(n_heads, n_heads)) # [n_heads, n_heads]
ax = fig.add_subplot(1, 1, 1)
ax.matshow(attn, cmap='viridis')
ax.set_xticklabels(['']+sentences[0].split(), fontdict={'fontsize': 14}, rotation=90)
ax.set_yticklabels(['']+sentences[2].split(), fontdict={'fontsize': 14})
plt.show()

if __name__ == '__main__':
# 句子的输入部分
"""
第一个句子 是 编码器的输入
第二个句子 是 解码器的输入
第三个句子 是 标签

P 可以理解为 编码器输入结束的字符(Padding填充字符)
S 可以理解为 Start
E 可以理解为 End

此外,需要注意的是,由于文本内容长度往往会不一致,因此在代码实现过程中,我们往往会设置一个最大长度max_length,
- 大于max_length的句子,多余的部分将会被裁剪
- 小于max_length的句子,缺少的部分将会被填充
"""

sentences = ['ich mochte ein bier P', 'S i want a beer', 'i want a beer E']

# Transformer Parameters
# Padding Should be Zero
src_vocab = {'P': 0, 'ich': 1, 'mochte': 2, 'ein': 3, 'bier': 4}
src_vocab_size = len(src_vocab)

tgt_vocab = {'P': 0, 'i': 1, 'want': 2, 'a': 3, 'beer': 4, 'S': 5, 'E': 6}
number_dict = {i: w for i, w in enumerate(tgt_vocab)}
tgt_vocab_size = len(tgt_vocab)

src_len = 5 # length of source 输入长度
tgt_len = 5 # length of target 解码端的输入长度

d_model = 512 # Embedding Size Embedding后的长度
d_ff = 2048 # FeedForward dimension 前馈神经网络的中间维度
d_k = d_v = 64 # dimension of K(=Q), V
n_layers = 6 # number of Encoder of Decoder Layer Encoder和Decoder N的个数
n_heads = 8 # number of heads in Multi-Head Attention 多头注意力机制分为几个头

model = Transformer()

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

enc_inputs, dec_inputs, target_batch = make_batch(sentences)

for epoch in range(20):
optimizer.zero_grad()
outputs, enc_self_attns, dec_self_attns, dec_enc_attns = model(enc_inputs, dec_inputs)
loss = criterion(outputs, target_batch.contiguous().view(-1))
print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss))
loss.backward()
optimizer.step()

# Test
predict, _, _, _ = model(enc_inputs, dec_inputs)
predict = predict.data.max(1, keepdim=True)[1]
print(sentences[0], '->', [number_dict[n.item()] for n in predict.squeeze()])

print('first head of last state enc_self_attns')
showgraph(enc_self_attns)

print('first head of last state dec_self_attns')
showgraph(dec_self_attns)

print('first head of last state dec_enc_attns')
showgraph(dec_enc_attns)

Transformer架构详解(1):transformer架构总览
http://example.com/2026/06/24/Transformer架构详解-1-:transformer架构总览/
作者
Yu xin
发布于
2026年6月24日
许可协议