Transformer架构
参考:https://blog.csdn.net/weixin_42475060/article/details/121101749
- 输入层:将输入序列转换为模型可以理解的格式。
- 编码器(Encoder):处理输入序列,提取特征。
- inputEmbedding和position Encoding作为输入,状态作为输出。由于残差链接的存在,位置信息可以进行充分的传递。
- encoder由很多个block组成
- 自注意力层(Self-Attention Layer):允许模型在处理当前词时考虑序列中的所有词。
- 前馈网络(Feed-Forward Neural Network):一个简单的神经网络,用于进一步处理自注意力层的输出。
- 在每一部分上,都使用残差+layer normalization来进行处理
- 解码器(Decoder):根据编码器的输出和之前生成的输出序列生成最终的输出序列。
-
outputEmbedding和position Encoding作为输入,输出预测概率。
-
掩码多头自注意力层(Masked Multi-Head Self-Attention):与编码器中的自注意力类似,但添加了一个掩码来防止未来位置的信息流入当前位置。
-
编码器-解码器注意力层(Multi-head Attention):允许解码器层关注编码器的输出。
- 这一步是解码器与编码器交互的过程。解码器使用编码器的输出作为键(Key)和值(Value),而解码器当前位置的输出作为查询(Query)。
- 这允许解码器在生成每个输出词时,能够关注编码器处理过的整个输入序列,从而更好地理解输入序列的上下文。
-
前馈网络FFN:与编码器中的前馈网络相同,用于进一步处理自注意力层的输出。
- F F N ( x ) = max ( 0 , x W 1 + b 1 ) W 2 + b 2 \mathrm{FFN}(x)=\max(0,xW_1+b_1)W_2+b_2 FFN(x)=max(0,xW1+b1)W2+b2
- FFN 包含两个线性变换,这两个变换是使用不同的权重矩阵和偏置向量进行的
- 前馈全连接网络也可以被描述为两次卷积操作,其中卷积核的大小为 1。这种描述方式强调了 FFN 在处理序列数据时的局部性,即每个位置的处理只依赖于它自己的输入。
-
Encoder
- input word embedding:由稀疏的one-hot进入一个不带bias的FFN(全连接网络)中得到一个稠密的连续向量,可以结存内存,表征更丰富。
- position encoding
- 通过sin/cos来固定表征 :每个位置的position encoding是确定性的,对于不同句子,相同位置距离一致,可以推广到更长的句子。
- pe(pos+k)可以写成pe(pos)的线性组合,从而在测试集中可以推广到更长的句子。‘
- 通过残差链接使位置信息流入深层
- multi-head self-attention:
- 多头使建模能力更强
- 多组K,Q,V构成,每组单独计算attention向量,把每组的attention向量拼接起来,并进入一个FNN得到最终向量
- feed-forward network:前馈神经网络只是对每个单独位置进行建模,只考虑每个位置的字符,不同位置参数是共享的。每个embedding各自维度进行融合。 F F N ( x ) = max ( 0 , x W 1 + b 1 ) W 2 + b 2 \mathrm{FFN}(x)=\max(0,xW_1+b_1)W_2+b_2 FFN(x)=max(0,xW1+b1)W2+b2
Decoder
- output word embedding:由稀疏的one-hot进入一个不带bias的FFN(全连接网络)中得到一个稠密的连续向量,可以结存内存,表征更丰富。
- position encoding
- 通过sin/cos来固定表征 :每个位置的position encoding是确定性的,对于不同句子,相同位置距离一致,可以推广到更长的句子。
- pe(pos+k)可以写成pe(pos)的线性组合,从而在测试集中可以推广到更长的句子。‘
- 通过残差链接使位置信息流入深层
- masked multi-head self-attention:
- 多头使建模能力更强
- 多组K,Q,V构成,每组单独计算attention向量,把每组的attention向量拼接起来,并进入一个FNN得到最终向量
- 添加了一个掩码来防止未来位置的信息流入当前位置。
- feed-forward network:前馈神经网络只是对每个单独位置进行建模,只考虑每个位置的字符,不同位置参数是共享的。每个embedding各自维度进行融合。
- F F N ( x ) = max ( 0 , x W 1 + b 1 ) W 2 + b 2 \mathrm{FFN}(x)=\max(0,xW_1+b_1)W_2+b_2 FFN(x)=max(0,xW1+b1)W2+b2
- 这里的全连接是Position-wise逐位置的,即设前面的attention输出的维度为 B a t c h ∗ L e n g t h ∗ d m o d e l Batch * Length * d_{model} Batch∗Length∗dmodel ,则变换时,实际上是只针对 d m o d e l d_{model} dmodel进行变换,对于每个位置(Length维度)上,都使用同样的变换矩阵,这意味着对于序列中的每个元素,网络都会应用相同的线性变换和激活函数
- 在论文中,这里的 d m o d e l d_{model} dmodel仍然是512,两层全连接的中间隐层单元数为 d f f = 2048 d_{ff} = 2048 dff=2048。这意味着 FFN 会首先将输入从 512 维扩展到 2048 维,然后通过 ReLU 激活函数,最后再将维度从 2048 维压缩回 512 维。
- softmax:概率输出
位置编码
它对于每个位置pos进行编码,然后与相应位置的word embedding进行相加,构成当前位置的新word embedding
P E ( p o s , 2 i ) = s i n ( p o s / 1000 0 2 i / d m o d e l ) P E ( p o s , 2 i + 1 ) = c o s ( p o s / 1000 0 2 i / d m o d e l ) PE_{(pos,2i)}=sin(pos/10000^{2i/d_{\mathrm{model}}})\\PE_{(pos,2i+1)}=cos(pos/10000^{2i/d_{\mathrm{model}}}) PE(pos,2i)=sin(pos/100002i/dmodel)PE(pos,2i+1)=cos(pos/100002i/dmodel)
- i:embedding向量中的位置,即 d m o d e l d_{model} dmodel中每一维
- sin/cos函数好处:可以不用训练,直接编码即可,且不论什么长度都能得到结果;可以表示相对位置 P E P O S + K PE_{POS+K} PEPOS+K可以表示为 P E P O S PE_{POS} PEPOS的线性变换。
注意力机制
一般的attention机制,可以抽象为输入一个查询(query),去查询键值对(key-value pair)中的key,然后得到一个概率分布,再据此对value进行加权相加,获取当前query下的注意力表征。而我们的query,往往是Decoder中某一个step的输出,key-value pair往往是encoder的输出。
自注意力机制
-
在self-attention中其query、key、value都是由encoder的输出经过不同的变换而来,也即self-attention,所有的东西都是自己。他们定义了一种叫“Scaled Dot-Product Attention”的计算方式,用于计算给定query、key和value下的注意力表征
Attention ( Q , K , V ) = softmax ( Q K T d k ) V \text{Attention}(Q,K,V)=\text{softmax}(\frac{QK^T}{\sqrt{d_k}})V Attention(Q,K,V)=softmax(dkQKT)V-
这里的Q、K和V分别表示query、key和value矩阵,它们的维度分别为 L q ∗ d k 、 L k ∗ d k 、 L k ∗ d v L_q*d_k、L_k*d_k、L_k*d_v Lq∗dk、Lk∗dk、Lk∗dv
-
缩放因子 d k d_k dk :这里为何要进行缩放呢?论文中给出了解释:在 d k d_k dk 比较小的时候,不加缩放的效果和加性attention的效果差不多,但当 d k d_k dk 比较大的时候,不加缩放的效果就明显比加性attention的效果要差,怀疑是当 d k d_k dk 增长的时候,内积的量级也会增长,导致softmax函数会被推向梯度较小的区域,为了缓解这个问题,加上了这个缩放项进行量级缩小。
-
-
STEPS:假设有一个长度为n的序列 x 1 , x 2 , . . . x n x_1,x_2,...x_n x1,x2,...xn,每个元素都具有相同的特征维度 d d d
- 我们可以使用三个不同的线性变换
W
Q
,
W
k
,
W
v
W_Q,W_k,W_v
WQ,Wk,Wv,将这些特征转换为相应的查询向量
q
i
q_i
qi,键向量
k
j
k_j
kj,值向量
v
j
v_j
vj:
q i = W Q x i ; k j = W K x j ; v j = W V x j q_i=W_Qx_i;k_j=W_Kx_j;v_j=W_Vx_j qi=WQxi;kj=WKxj;vj=WVxj - 接下来,我们需要计算每个查询向量与所有键向量之间的点积,得到注意力权重矩阵。 a i j a_{ij} aij表示序列中第 i 个查询与 第 j 个键的相似度。然后,我们将注意力权重进行归一化,得到最终的注意力分布: a i j = q i ⊤ k j / d k a_{ij}=q_i^\top k_j/\sqrt{d_k} aij=qi⊤kj/dk p i = s o f t m a x ( a i 1 , a i 2 , . . . , a i n ) 。 p_i=\mathrm{softmax}(a_{i1},a_{i2},...,a_{in})。 pi=softmax(ai1,ai2,...,ain)。
- 最后,对于每个位置i,我们根据其对应的注意力分布
p
i
p_i
pi加权平均所有值向量
v
1
,
v
2
.
.
.
v
n
v_1,v_2...v_n
v1,v2...vn,得到该位置的新表示
y
i
y_i
yi
y i = ∑ j = 1 n p i j v j y_i=\sum_{j=1}^np_{ij}v_j yi=j=1∑npijvj
- 我们可以使用三个不同的线性变换
W
Q
,
W
k
,
W
v
W_Q,W_k,W_v
WQ,Wk,Wv,将这些特征转换为相应的查询向量
q
i
q_i
qi,键向量
k
j
k_j
kj,值向量
v
j
v_j
vj:
多头注意力机制
-
与自注意力机制相比,将注意力的计算分散到不同的子空间进行.具体来说,我们将原始的查询向量 q i q_i qi、键向量 k j k_j kj和值向量 v j v_j vj分别投影到 h h h个独立的子空间中,每个子空间都有自己的参数集 W Q h , W K h , W V h W_Q^h,W_K^h,W_V^h WQh,WKh,WVh。这样一来,我们就能够同时考虑不同子空间中的信息流动情况了。
M u l t i H e a d ( Q , K , V ) = C o n c a t ( h e a d 1 , . . . , h e a d h ) W O w h e r e h e a d i = A t t e n t i o n ( Q W i Q , K W i K , V W i V ) \boxed{\mathrm{MultiHead}(Q,K,V)=\mathrm{Concat}(\mathrm{head}_{1},...,\mathrm{head}_{\mathrm{h}})W^{O}}\\\mathrm{where~head_{i}}=\mathrm{Attention}(QW_{i}^{Q},KW_{i}^{K},VW_{i}^{V}) MultiHead(Q,K,V)=Concat(head1,...,headh)WOwhere headi=Attention(QWiQ,KWiK,VWiV)- d m o d e l d_{model} dmodel是原始维度,而 d k d_k dk和 d v d_v dv 是投影后的键和值的维度。h 是头的数目,即要进行的注意力计算的次数。 d k = d v = d m o d e l / h d_k=d_v=d_{model}/h dk=dv=dmodel/h
- W i Q ∈ R d m o d e l ∗ d k , W i K ∈ R d m o d e l ∗ d k , W i V ∈ R d m o d e l ∗ d v , W O ∈ R h d v ∗ d m o d e l W_i^Q\in R^{d_{model}*d_k},W_i^K\in R^{d_{model}*d_k},W_i^V\in R^{d_{model}*d_v},W^O\in R^{hd_v*d_{model}} WiQ∈Rdmodel∗dk,WiK∈Rdmodel∗dk,WiV∈Rdmodel∗dv,WO∈Rhdv∗dmodel
- h e a d i head_i headi:表示第 i i i个头的变换矩阵, h h h表示头的个数,这是第 i 个头的自注意力输出。每个头都独立地执行自注意力操作。
- 在论文里面, h = 8 h=8 h=8,并且 d k = d v = d m o d e l / h = 64 d_k=d_v=d_{model}/h=64 dk=dv=dmodel/h=64。可见这里虽然分了很多头去计算,但整体的维度还是不变的,因此计算量还是一样的。
-
STEPS:假设有 h h h个子空间
- 则第
h
h
h个子空间中,
q
i
h
q_i^h
qih、键向量
k
j
h
k_j^h
kjh和值向量
v
j
h
v_j^h
vjh的计算方式如下所示:
q i h = W Q h x i ; k j = W K h x j ; v j = W V h x j q_i^h=W_Q^hx_i;k_j=W_K^hx_j;v_j=W_V^hx_j qih=WQhxi;kj=WKhxj;vj=WVhxj - 接着,我们可以按照单头自注意力的方式对每个子空间内的信息进行加权平均,从而得到每个子空间的输出结果
o
i
h
o_i^h
oih:
α i j h = softmax ( q i h ( k j h ) ⊤ d k ) σ i h = ∑ j = 1 n α i j h v j h \alpha_{ij}^h=\text{softmax}(\frac{q_i^h(k_j^h)^\top}{\sqrt{d_k}})\\ \sigma_i^h=\sum_{j=1}^n\alpha_{ij}^hv_j^h αijh=softmax(dkqih(kjh)⊤)σih=j=1∑nαijhvjh - 最后,我们将所有的子空间输出结果拼接起来,形成一个新的向量表示 x i ′ = [ o i 1 , o i 2 , . . . , o i h ] x_i^{\prime}=[o_i^1,o_i^2,...,o_i^h] xi′=[oi1,oi2,...,oih],作为多头注意力机制的输出
- 则第
h
h
h个子空间中,
q
i
h
q_i^h
qih、键向量
k
j
h
k_j^h
kjh和值向量
v
j
h
v_j^h
vjh的计算方式如下所示:
交叉注意力
-
交叉注意力是指将两个序列的信息进行交互的过程,也被称为外注意力(external attention)。与自注意力机制类似,交叉注意力也可以用来提取序列之间的关联性和共现模式。不同之处在于,它的目标是建立不同序列之间的联系,而非关注同一个序列内部的不同部分之间的关系。
-
具体而言,假设有两个长度分别为 m m m和 n n n的序列 x 1 , x 2 , . . . , x m x_1,x_2,...,x_m x1,x2,...,xm和 y 1 , y 2 , . . . , y n y_1,y_2,...,y_n y1,y2,...,yn,它们分别代表不同的语义单元或实体。可以根据对应的 W Q , W K , W V W^Q,W^K,W^V WQ,WK,WV对新的向量 q i x , q j y q_i^x,q_j^y qix,qjy, v i x , v j y v_i^x,v_j^y vix,vjy, k i x , k j y k_i^x,k_j^y kix,kjy分别对应两个不同序列的查询向量。
- Q 1 = { q 1 x , q 2 x , . . q m x } Q_1=\{q_1^x,q_2^x, ..q_m^x\} Q1={q1x,q2x,..qmx}, Q 2 = { q 1 y , q 2 y , . . q m y } Q_2=\{q_1^y,q_2^y, ..q_m^y\} Q2={q1y,q2y,..qmy}
- K 1 = { k 1 x , k 2 x , . . k m x } K_1=\{k_1^x,k_2^x, ..k_m^x\} K1={k1x,k2x,..kmx}, K 2 = { k 1 y , k 2 y , . . k m y } K_2=\{k_1^y,k_2^y, ..k_m^y\} K2={k1y,k2y,..kmy}
- V 1 = { v 1 x , v 2 x , . . v m x } V_1=\{v_1^x,v_2^x, ..v_m^x\} V1={v1x,v2x,..vmx}, V 2 = { v 1 y , v 2 y , . . v m y } V_2=\{v_1^y,v_2^y, ..v_m^y\} V2={v1y,v2y,..vmy}
- 接下来,我们可以按照自注意力机制的方式计算出每个查询向量与所有键向量之间的点积,得到注意力权重矩阵
A
x
,
A
y
A^x,A^y
Ax,Ay,
a
i
j
x
a_{ij}^x
aijx表示 x 序列中第 i 个查询与 y 序列中第 j 个键的相似度:
a i j x = q i x ( k j y ) ⊤ / d k ( ) a i j y = q i y ( k j x ) ⊤ / d k a_{ij}^x=q_i^x(k_j^y)^\top/\sqrt{d_k}() \\a_{ij}^y=q_i^y(k_j^x)^\top/\sqrt{d_k} aijx=qix(kjy)⊤/dk()aijy=qiy(kjx)⊤/dk - 然后,我们可以根据注意力权重对所有值向量进行加权平均,得到每个查询向量的新表示 z i x , z j y z_i^x,z_j^y zix,zjy:
z i x = ∑ j = 1 n p i j x v j y , p i j x = softmax ( a i 1 x , a i 2 x , . . . , a i m x ) z j n = ∑ i = 1 m p i j n v i m , p i j n = softmax ( a 1 j n , a 2 j n , . . . , a n j n ) z_i^x=\sum_{j=1}^{n}p_{ij}^xv_j^y,p_{ij}^x=\text{softmax}(a_{i1}^x,a_{i2}^x,...,a_{im}^x) \\z_j^n=\sum_{i=1}^{m}p_{ij}^nv_i^m,p_{ij}^n=\text{softmax}(a_{1j}^n,a_{2j}^n,...,a_{nj}^n) zix=j=1∑npijxvjy,pijx=softmax(ai1x,ai2x,...,aimx)zjn=i=1∑mpijnvim,pijn=softmax(a1jn,a2jn,...,anjn)
代码
https://blog.csdn.net/Magical_Bubble/article/details/89083225
https://nlp.seas.harvard.edu/2018/04/03/attention.html
Model Architecture
编码器解码器(通用架构)
-
编码器的功能:编码器的作用是将输入的符号序列 ( x 1 , … , x n ) (x_1, \ldots, x_n) (x1,…,xn)映射到一系列连续的表示 z = ( z 1 , … , z n ) \mathbf{z} = (z_1, \ldots, z_n) z=(z1,…,zn)。这里的符号可以是单词、字符或其他任何形式的标记。
-
连续表示:编码器输出的 z \mathbf{z} z是连续的向量表示,它们捕捉了输入序列的语义信息和结构信息。
-
解码器的功能:给定编码器的输出 z \mathbf{z} z,解码器生成一个符号序列 ( ( y 1 , … , y m ) ((y_1, \ldots, y_m) ((y1,…,ym),这个过程是逐步进行的,一次生成一个符号。
-
自回归特性:在生成输出序列的过程中,模型是自回归的(auto-regressive)。这意味着在生成下一个符号时,模型会考虑之前已经生成的所有符号。这种特性使得模型能够生成连贯和语法正确的输出。
class EncoderDecoder(nn.Module):
"""
A standard Encoder-Decoder architecture. Base for this and many
other models.
"""
# 定义了一个名为 EncoderDecoder 的类,它继承自 PyTorch 的 nn.Module,
# 表示这是一个神经网络模块,EncoderDecoder 可以作为许多其他模型的基础结构。
def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
super(EncoderDecoder, self).__init__()
self.encoder = encoder
self.decoder = decoder
self.src_embed = src_embed
self.tgt_embed = tgt_embed
self.generator = generator
# 它们分别代表编码器、解码器、源语言嵌入层、目标语言嵌入层和输出生成器。
# super() 调用确保了父类 nn.Module 的初始化过程被正确执行。
def forward(self, src, tgt, src_mask, tgt_mask):
"Take in and process masked src and target sequences."
return self.decode(self.encode(src, src_mask), src_mask,
tgt, tgt_mask)
# src 是源序列,tgt 是目标序列,src_mask 和 tgt_mask 是对应的掩码,
# 用于在处理序列时忽略 padding 部分。
# 方法首先调用 encode 处理源序列,然后使用 decode 方法进行解码。
#注意要传入mask
def encode(self, src, src_mask):
return self.encoder(self.src_embed(src), src_mask)
# encode 方法定义了编码过程,它接收源序列 src 和对应的掩码 src_mask。
# 使用 src_embed 将源序列转换为嵌入表示,然后传递给编码器。
def decode(self, memory, src_mask, tgt, tgt_mask):
return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)
# decode 方法定义了解码过程,它接收经过编码器处理后的记忆(memory),
# 源掩码 src_mask,目标序列 tgt 和目标掩码 tgt_mask。
# 使用 tgt_embed 将目标序列转换为嵌入表示,然后传递给解码器。
ENCODER
class Encoder(nn.Module):
"Core encoder is a stack of N layers"
# 定义一个Encoder类,继承自PyTorch的nn.Module,作为模型的核心编码器,由N层堆叠而成。
def __init__(self, layer, N):
super(Encoder, self).__init__()
# 初始化Encoder,调用父类nn.Module的构造函数。
self.layers = clones(layer, N)
# 创建N个layer的副本,堆叠成编码器的层。
self.norm = LayerNorm(layer.size)
# 添加一个层归一化(Layer Normalization)模块。
def forward(self, x, mask):
# 定义前向传播函数,接收输入x和掩码mask。
"Pass the input (and mask) through each layer in turn."
# 将输入和掩码逐层传递。
for layer in self.layers:
x = layer(x, mask)
# 对于每一层,使用前一层的输出作为当前层的输入。
return self.norm(x)
# 经过所有层之后,使用归一化层处理最终的输出。
class LayerNorm(nn.Module):
#LayerNorm(x+sublayer(x))
"Construct a layernorm module (See citation for details)."
# 定义一个LayerNorm类,用于实现层归一化。
def __init__(self, features, eps=1e-6):
super(LayerNorm, self).__init__()
# 初始化LayerNorm模块。
self.a_2 = nn.Parameter(torch.ones(features))
# 创建一个可学习的参数a_2,用于归一化后的缩放。
self.b_2 = nn.Parameter(torch.zeros(features))
# 创建一个可学习的参数b_2,用于归一化后的偏移。
self.eps = eps
# 小的常数eps,用于数值稳定性。
def forward(self, x):
mean = x.mean(-1, keepdim=True)
# 计算x在最后一个维度上的平均值。
std = x.std(-1, keepdim=True)
# 计算x在最后一个维度上的标准差。
return self.a_2 * (x - mean) / (std + self.eps) + self.b_2
# 应用归一化公式,并使用参数a_2和b_2进行缩放和偏移。
class SublayerConnection(nn.Module):
"""
A residual connection followed by a layer norm.
Note for code simplicity the norm is first as opposed to last.
"""
# 定义一个SublayerConnection类,实现残差连接后跟一个层归一化。
def __init__(self, size, dropout):
super(SublayerConnection, self).__init__()
# 初始化SublayerConnection。
self.norm = LayerNorm(size)
# 添加一个与输入尺寸相同的层归一化模块。
self.dropout = nn.Dropout(dropout)
# 添加一个dropout层,用于正则化。
def forward(self, x, sublayer):
# 定义前向传播函数,接收输入x和一个子层函数sublayer。
"Apply residual connection to any sublayer with the same size."
# 将残差连接应用于具有相同尺寸的任何子层。
return x + self.dropout(sublayer(self.norm(x)))
# 将归一化后的输入x与经过dropout处理的子层输出相加,实现残差连接。
class EncoderLayer(nn.Module):
"Encoder is made up of self-attn and feed forward (defined below)"
# 定义一个EncoderLayer类,编码器层由自注意力机制和前馈网络组成。
def __init__(self, size, self_attn, feed_forward, dropout):
super(EncoderLayer, self).__init__()
# 初始化EncoderLayer。
self.self_attn = self_attn
# 添加自注意力机制模块。
self.feed_forward = feed_forward
# 添加前馈网络模块。
self.sublayer = clones(SublayerConnection(size, dropout), 2)
''' def clones(module, N):
return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])'''
# 创建两个SublayerConnection的副本,用于实现两个残差连接。
self.size = size
# 保存层的尺寸。
def forward(self, x, mask):
# 定义前向传播函数。
"Follow Figure 1 (left) for connections."
# 按照文献中的图1(左)来连接各个组件。
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
# 应用第一个残差连接和自注意力机制。
return self.sublayer[1](x, self.feed_forward)
# 应用第二个残差连接和前馈网络。
DECODER
class Decoder(nn.Module):
"Generic N layer decoder with masking."
# 定义一个Decoder类,继承自PyTorch的nn.Module,是一个具有N层的通用解码器,支持掩码操作。
def __init__(self, layer, N):
super(Decoder, self).__init__()
self.layers = clones(layer, N)
# 创建N个layer的副本,构成解码器的层堆栈。
self.norm = LayerNorm(layer.size)
# 添加一个层归一化模块。
def forward(self, x, memory, src_mask, tgt_mask):
# 前向传播函数,接收目标序列的输入x,源序列的编码器输出memory,以及源序列和目标序列的掩码。
for layer in self.layers:
x = layer(x, memory, src_mask, tgt_mask)
# 对每个解码器层应用前向传播,并将结果传递给下一层。
return self.norm(x)
# 经过所有层后,使用层归一化处理最终输出。
def subsequent_mask(size):
"Mask out subsequent positions."
# 定义一个函数,用于生成一个掩码,以屏蔽目标序列中后续的位置。
attn_shape = (1, size, size)
# 定义注意力矩阵的形状。
subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
# 使用NumPy生成一个上三角矩阵,并将数据类型转换为uint8。
return torch.from_numpy(subsequent_mask) == 0
# 将NumPy数组转换为PyTorch张量,并返回一个布尔张量,上三角部分为False,其余为True。
class DecoderLayer(nn.Module):
"Decoder is made of self-attn, src-attn, and feed forward (defined below)"
# 定义一个DecoderLayer类,继承自PyTorch的nn.Module,解码器层由自注意力机制、源注意力机制和前馈网络组成。
def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
super(DecoderLayer, self).__init__()
# 调用父类构造函数,初始化DecoderLayer。
self.size = size
# 保存解码器层的尺寸。
self.self_attn = self_attn
# 保存自注意力机制的实例。
self.src_attn = src_attn
# 保存源注意力机制的实例。
self.feed_forward = feed_forward
# 保存前馈网络的实例。
self.sublayer = clones(SublayerConnection(size, dropout), 3)
# 创建3个SublayerConnection的副本,用于解码器层中的3个残差连接。
def forward(self, x, memory, src_mask, tgt_mask):
# 前向传播函数,接收目标序列的输入x,编码器的输出memory,以及源序列和目标序列的掩码。
"Follow Figure 1 (right) for connections."
# 根据文献中的图1(右)来连接解码器层的组件。
m = memory
# 将编码器的输出赋值给变量m,以简化代码。
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))
# 应用自注意力机制,并通过第一个残差连接和层归一化。
x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask))
# 应用源注意力机制,并通过第二个残差连接和层归一化。
return self.sublayer[2](x, self.feed_forward)
# 应用前馈网络,并通过第三个残差连接和层归一化,返回最终输出。
Attention(多头注意力)
Transformer 以三种不同的方式使用多头注意力:
- 1)在“编码器-解码器注意”层中,查询Q来自前面的解码器层,memory键key和值value来自输出的编码器。这允许解码器中的每个位置都参加所有 输入序列中的位置。这模拟了典型的编码器-解码器 序列到序列模型中的注意力机制
- 2)编码器encoder包含自注意力层。在自我关注层(self-attention)中,所有 键K、值V和查询Q来自同一个地方,在本例中为输出编码器中的上一层。编码器中的每个位置都可以参加到编码器前一层的所有位置。
- 3)类似地,解码器中的自注意力层允许每个位置在 解码器处理解码器中的所有位置,包括该 位置。我们需要防止解码器中的信息向左流动 保留自动回归属性。我们在缩放点内实现了这一点。 通过mask来关注产品(设置为−∞) 输入中的所有值 对应于非法连接的 Softmax。
#这个函数实际上就是“Scaled Dot Product Attention”这个模块的计算
def SDPattention(query, key, value, mask=None, dropout=None):
"Compute 'Scaled Dot Product Attention'"
d_k = query.size(-1)
# 获取query的最后一个维度的大小,即d_k,它代表键(key)和值(value)的维度。
scores = torch.matmul(query, key.transpose(-2, -1)) \
/ math.sqrt(d_k)
# 使用torch.matmul计算query和key的转置(-2, -1)的点积,得到注意力分数的原始值。
# 然后,通过sqrt(d_k)进行缩放,以防止梯度消失或爆炸问题。
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
# 如果提供了掩码,使用.masked_fill将掩码为0的位置替换为一个非常大的负数(-1e9),这在softmax操作中相当于将这些位置的概率设置为0。
p_attn = F.softmax(scores, dim = -1)
# 应用softmax函数对缩放后的分数进行归一化,得到每个位置的注意力权重。dim=-1表示在最后一个维度上应用softmax。
if dropout is not None:
p_attn = dropout(p_attn)
# 如果提供了dropout,将其应用于注意力权重,以进行正则化。
return torch.matmul(p_attn, value), p_attn
# 最后,使用归一化的注意力权重和value计算加权和,得到最终的注意力输出。同时返回注意力权重用于可能的后续分析。
class MultiHeadedAttention(nn.Module):
def __init__(self, h, d_model, dropout=0.1):
"Take in model size and number of heads."
# 初始化MultiHeadedAttention类,继承自PyTorch的nn.Module。
super(MultiHeadedAttention, self).__init__()
# 确保模型的维度(隐层单元数)d_model可以被头数h整除。
assert d_model % h == 0
# 假设值向量d_v的维度等于键向量d_k的维度。
self.d_k = d_model // h
# 保存每个头的键和值的维度。
self.h = h
# 保存头的数量。
self.linears = clones(nn.Linear(d_model, d_model), 4)#3+1
# 创建4个线性变换层的副本,用于query, key, value的线性变换和最终的输出变换。
#其中的3个分别是Q、K和V的变换矩阵,最后一个是用于最后将多头concat之后进行变换的矩阵。
self.attn = None
# 保存注意力分数,初始化为None。
self.dropout = nn.Dropout(p=dropout)
# 创建一个dropout层实例。
def forward(self, query, key, value, mask=None):
"Implements Figure 2"
# 前向传播函数,实现多头注意力机制。
if mask is not None:
# 如果提供了掩码,则将其扩展到适合多头注意力的维度。
mask = mask.unsqueeze(1)
# 扩展掩码的维度,以应用于所有头。
nbatches = query.size(0)
# 获取输入query的批量大小。
# 1) Do all the linear projections in batch from d_model => h x d_k ,进行线性变换
query, key, value = \
[l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
for l, x in zip(self.linears, (query, key, value))]
# 对linears中的每个linear中的query, key, value分别应用线性变换,然后重塑和转置以准备多头注意力。
# 2) Apply attention on all the projected vectors in batch.
x, self.attn = SDPattention(query, key, value, mask=mask,
dropout=self.dropout)
# 应用多头注意力机制,计算加权的value,同时保存注意力分数和应用dropout。
# 3) "Concat" using a view and apply a final linear.
x = x.transpose(1, 2).contiguous() \
.view(nbatches, -1, self.h * self.d_k)
# 将注意力加权的value转置和重塑,准备进行最终的线性变换。
return self.linears[-1](x)
# 应用最后一个线性变换层,得到最终的输出。
前馈网络
前馈网络实际上就是两层全连接
class PositionwiseFeedForward(nn.Module):
"Implements FFN equation."
# 定义一个位置感知前馈网络类,继承自PyTorch的nn.Module。
def __init__(self, d_model, d_ff, dropout=0.1):
super(PositionwiseFeedForward, self).__init__()
# 初始化FFN。
self.w_1 = nn.Linear(d_model, d_ff)
# 第一个线性层,将输入从d_model维度变换到d_ff维度。
self.w_2 = nn.Linear(d_ff, d_model)
# 第二个线性层,将输入从d_ff维度变换回d_model维度。
self.dropout = nn.Dropout(dropout)
# Dropout层,用于正则化。
def forward(self, x):
# 前向传播函数,实现FFN的计算。
return self.w_2(self.dropout(F.relu(self.w_1(x))))
# 应用第一个线性层,然后是ReLU激活函数,接着是dropout,最后是第二个线性层。
位置编码
class PositionalEncoding(nn.Module):
"Implement the PE function."
# 定义一个位置编码类,继承自PyTorch的nn.Module。
def __init__(self, d_model, dropout, max_len=5000):
super(PositionalEncoding, self).__init__()
# 初始化位置编码。
self.dropout = nn.Dropout(p=dropout)
# Dropout层,用于正则化。
# Compute the positional encodings once in log space.
pe = torch.zeros(max_len, d_model)
# 初始化位置编码矩阵,大小为max_len(最大序列长度)乘以d_model(模型维度)。
position = torch.arange(0, max_len).unsqueeze(1)
# 生成0到max_len-1的序列,然后增加一个维度。
div_term = torch.exp(torch.arange(0, d_model, 2) *
-(math.log(10000.0) / d_model))
# 计算除数项,用于正弦和余弦函数的频率调整。
pe[:, 0::2] = torch.sin(position * div_term)
# 将正弦函数应用于偶数索引的位置编码。
pe[:, 1::2] = torch.cos(position * div_term)
# 将余弦函数应用于奇数索引的位置编码。
pe = pe.unsqueeze(0)
# 增加一个维度,以匹配batch的维度。
self.register_buffer('pe', pe)
# 将位置编码注册为一个不需要梯度的buffer。
def forward(self, x):
# 前向传播函数,将位置编码添加到输入x。
x = x + Variable(self.pe[:, :x.size(1)],
requires_grad=False)
# 将位置编码添加到输入x,Variable确保pe不会进行梯度计算。
return self.dropout(x)
# 应用dropout后返回结果。
可见,这里首先是按照最大长度max_len生成一个位置,而后根据公式计算出所有的向量,在forward函数中根据长度取用即可,非常方便。注意要设置requires_grad=False,因其不参与训练。
Embedding and Softmax
class Embeddings(nn.Module):
def __init__(self, d_model, vocab):
super(Embeddings, self).__init__()
self.lut = nn.Embedding(vocab, d_model)
self.d_model = d_model
def forward(self, x):
return self.lut(x) * math.sqrt(self.d_model)
Full Model
def make_model(src_vocab, tgt_vocab, N=6,
d_model=512, d_ff=2048, h=8, dropout=0.1):
"Helper: Construct a model from hyperparameters."
# 定义一个辅助函数,根据超参数构建模型。
c = copy.deepcopy
# 使用deepcopy函数,以便在后续创建模块副本时保留原始模块的引用。
attn = MultiHeadedAttention(h, d_model)
# 创建多头注意力机制模块。
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
# 创建位置感知前馈网络模块。
position = PositionalEncoding(d_model, dropout)
# 创建位置编码模块。
model = EncoderDecoder(
Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),
# 创建编码器,包含N个编码器层。使用 deepcopy 创建 attn 和 ff 的副本,确保每个 EncoderLayer 使用独立的注意力和前馈网络模块,而不是所有层共享同一个实例
Decoder(DecoderLayer(d_model, c(attn), c(attn),
c(ff), dropout), N),
# 创建解码器,包含N个解码器层,src_vocab 表示源语言词汇大小
nn.Sequential(Embeddings(d_model, src_vocab), c(position)),
# 创建源序列的嵌入层和位置编码层,tgt_vocab表示目标语言词汇大小
nn.Sequential(Embeddings(d_model, tgt_vocab), c(position)),
# 创建目标序列的嵌入层和位置编码层。
Generator(d_model, tgt_vocab)
# 创建输出层,用于生成最终的输出序列。
)
# Initialize parameters with Glorot / fan_avg.
# 使用Glorot初始化(也称为Xavier均匀初始化)初始化模型参数。
for p in model.parameters():
if p.dim() > 1:
# 如果参数张量的维度大于1,则应用Xavier均匀初始化。
nn.init.xavier_uniform_(p)
return model
# 返回构建好的模型。
-
在这个函数中,src_vocab 和 tgt_vocab 分别代表源语言和目标语言的词汇表大小。N 是编码器和解码器层的数量,d_model 是模型的维度,d_ff 是前馈网络的维度,h 是多头注意力中头的数量,dropout 是dropout率。
-
Encoder 和 Decoder 是构建编码器和解码器的组件,它们分别由多个 EncoderLayer 和 DecoderLayer 组成。EncoderLayer 和 DecoderLayer 层内部使用 MultiHeadedAttention 和 PositionwiseFeedForward 模块。
-
Embeddings 是嵌入层,用于将输入序列的单词索引转换为连续的向量表示。PositionalEncoding 添加了位置信息到嵌入的向量中。
-
Generator 是输出层,通常是一个线性层后接一个softmax函数,用于生成最终的输出序列的概率分布。
Training
Batches and Masking
class Batch:
"Object for holding a batch of data with mask during training."
# 定义一个Batch类,用于在训练时存储一批数据及其掩码。
def __init__(self, src, trg=None, pad=0):
self.src = src # 保存源语言序列数据。
self.src_mask = (src != pad).unsqueeze(-2)
# 为源语言序列创建掩码,`unsqueeze(-2)` 在序列倒数第二个维度上增加一个维度,以匹配注意力机制的维度需求。
if trg is None:
# 如果没有提供目标语言序列,则不执行任何操作。
return
self.trg = trg[:, :-1]
# 保存目标语言序列数据,但去掉最后一个时间步,因为解码器在生成第t个词的时候只能看到前t-1个词。
self.trg_y = trg[:, 1:]
# 保存目标语言序列的下一个时间步,用于训练时的监督信号(用于计算损失)
self.trg_mask = self.make_std_mask(self.trg, pad)
# 为目标语言序列创建掩码,使用静态方法 `make_std_mask`。
self.ntokens = (self.trg_y != pad).data.sum()
# 计算目标序列中非填充词的数量,用于跟踪模型训练时的词数。
@staticmethod
def make_std_mask(tgt, pad):
"Create a mask to hide padding and future words."
# 静态方法,用于创建掩码以隐藏填充词和未来词。
tgt_mask = (tgt != pad).unsqueeze(-2)
# 为目标语言序列创建掩码,类似于源序列掩码的创建。
tgt_mask = tgt_mask & Variable(
subsequent_mask(tgt.size(-1)).type_as(tgt_mask.data))
# 结合上述掩码和通过 `subsequent_mask` 创建的三角掩码,以确保解码器在生成第t个词时不会看到未来的词。
return tgt_mask
# 返回最终的掩码。
- src 代表源序列数据,trg 代表目标序列数据,pad 是填充标记的值
- self.src_mask 是源序列的掩码,用于在处理注意力机制时忽略填充词pad。
- 如果提供了 trg,则 self.trg 是目标序列,去掉了序列的最后一个词,因为解码器在生成序列时是自回归的,即在生成第 t 个词时只能看到前 t-1 个词,不能发生数据泄露。
- self.trg_y 是目标序列的下一个时间步,用于计算损失。
- self.trg_mask 是目标序列的掩码,它结合了填充词掩码和未来词掩码。
Training Loop
def run_epoch(data_iter, model, loss_compute):
"Standard Training and Logging Function"
# 定义一个函数,用于执行模型训练的标准步骤和记录日志。
start = time.time()
# 记录训练开始的时间。
total_tokens = 0
# 初始化用于累加的token计数器。
total_loss = 0
# 初始化用于累加的损失值。
tokens = 0
# 初始化token计数器,用于计算一段时间内的平均性能。
for i, batch in enumerate(data_iter):
# 遍历数据迭代器产生的批次数据。
out = model.forward(batch.src, batch.trg,
batch.src_mask, batch.trg_mask)
# 将当前批次数据传递给模型的前向传播函数,获取模型的输出。
loss = loss_compute(out, batch.trg_y, batch.ntokens)
# 使用损失计算函数,根据模型输出、目标序列的下一个词以及批次中的token数计算损失。
total_loss += loss
# 累加损失值。
total_tokens += batch.ntokens
# 累加批次中的token数。
tokens += batch.ntokens
# 增加当前时间段内的token计数。
if i % 50 == 1:
# 每50个批次记录一次训练进度。
elapsed = time.time() - start
# 计算从上一次记录开始到现在经过的时间。
print("Epoch Step: %d Loss: %f Tokens per Sec: %f" %
(i, loss / batch.ntokens, tokens / elapsed))
# 打印当前周期步数、平均损失以及每秒处理的token数。
start = time.time()
# 重置计时器,以便计算下一个时间段的性能。
tokens = 0
# 重置token计数器。
return total_loss / total_tokens
# 在整个周期结束时,返回平均损失。
Training Data and Batching
- 数据集特征
- WMT 2014 英德数据集:标准的英德机器翻译数据集,包含约450万句子对。
- 字节对编码(BPE):一种词汇编码技术,用于减少词汇表的大小,同时保留句子的语义信息。
- 共享的源-目标词汇表:英德数据集使用一个共享的词汇表,大小约为37000个令牌。
- WMT 2014 英语-法语数据集:更大的数据集,包含3600万句子对,词汇表大小为32000个单词。
- 批处理策略
- 按序列长度分组:句子对根据近似的序列长度批量组合,这有助于提高计算效率,因为相似长度的序列可以更均匀地分配计算资源。
- 每批次令牌数量:每个训练批次的目标是包含约25000个源令牌和25000个目标令牌,这有助于保持批次的计算负荷相对稳定。
- 使用 torchtext 进行批处理:使用 torchtext 的 batch_size_fn 函数动态确定每个批次的大小,这允许模型根据当前批次的实际数据量调整批次大小。
global max_src_in_batch, max_tgt_in_batch
def batch_size_fn(new, count, sofar):
#new(当前处理的批次),count(当前批次是第几个批次),sofar(到目前为止处理的总元素数量)
"Keep augmenting batch and calculate total number of tokens + padding."
global max_src_in_batch, max_tgt_in_batch
#如果是处理新批次的第一个批次(count == 1),则重置最长源序列和目标序列的长度计数器。
if count == 1:
max_src_in_batch = 0
max_tgt_in_batch = 0
max_src_in_batch = max(max_src_in_batch, len(new.src))
#更新 max_src_in_batch 为当前批次中的最长源序列长度和之前记录的最长长度中的较大值。
max_tgt_in_batch = max(max_tgt_in_batch, len(new.trg) + 2)
#更新 max_tgt_in_batch 为当前批次中最长的目标序列长度加2(可能为了包括开始和结束标记)和之前记录的最长长度中的较大值。
src_elements = count * max_src_in_batch #计算算当前批次中源序列的总元素数,包括填充的元素。
tgt_elements = count * max_tgt_in_batch
return max(src_elements, tgt_elements)#返回源序列和目标序列总元素数中的较大值,这个值决定了批次的实际大小,确保即使在不同长度的序列中也能有效地进行填充。
CNN vs RNN vs Transformer
-
CNN(卷积神经网络)
- 特点:
- 局部连接:每个卷积神经元只与输入数据的局部区域连接,这有助于捕捉局部特征。
- 权重共享:卷积核的权重在整个输入数据上共享,减少了模型参数的数量,可并行计算。
- 自动特征提取:无需手动设计特征提取器,网络可以自动学习到有用的特征。
- 多尺度处理:通过池化层,可以处理不同尺度的特征。
- 缺点:对相对位置敏感,对绝对位置不敏感
- 特点:
-
RNN(循环神经网络)
- 特点:
- 序列处理:能够处理序列数据,如时间序列、文本等。对顺序敏感
- 记忆能力:通过隐藏状态传递信息,具有记忆过去信息的能力。当前时刻输出必须依赖于上一时刻运算。
- 参数共享:在序列的每个时间点上,使用相同的权重矩阵。
- 缺点:
- 梯度消失/爆炸:在长序列上训练时,梯度可能会消失或爆炸,导致训练困难。
- 串行计算耗时,每一时刻计算依赖于上一时刻计算,计算复杂度与序列长度线性关系。
- 长程建模能力弱。
- 对相对位置敏感,对绝对位置敏感。
- 特点:
-
Transformer
- 自注意力机制:每个位置的输出都与序列中所有位置有关,这使得模型能够捕捉长距离依赖关系。
- 没有局部假设,可以进行并行计算,对相对位置不敏感。
- 没有有序假设,对绝对位置不敏感,需要位置编码来反映位置变化对于特征的影响,
- 任意两个字符可以进行建模,擅长长短程建模,序列长度的平方级级别。
- 并行化处理:由于自注意力机制,Transformer可以并行处理序列中的所有元素。
- 可扩展性:容易扩展到更大的模型和更长的序列。
- Transformer由于其并行化的特性,在训练效率上通常优于RNN。
-
区别:
- 处理数据类型:CNN主要用于图像等具有网格状拓扑结构的数据,RNN和Transformer主要用于序列数据。
- 特征捕捉能力:CNN擅长捕捉局部特征,RNN擅长捕捉时间序列中的动态特征,而Transformer通过自注意力机制能够捕捉全局特征。
- 训练效率:Transformer由于其并行化的特性,在训练效率上通常优于RNN。
- 长序列处理:Transformer通过自注意力机制更好地处理长序列,而RNN可能会遇到梯度消失或爆炸的问题。