论文链接: Attention Is All You Need
CNN vs RNN vs Transformer
-
CNN(卷积神经网络)
- 特点:
- 局部连接:每个卷积神经元只与输入数据的局部区域连接,这有助于捕捉局部特征。
- 权重共享:卷积核的权重在整个输入数据上共享,减少了模型参数的数量,可并行计算。
- 自动特征提取:无需手动设计特征提取器,网络可以自动学习到有用的特征。
- 多尺度处理:通过池化层,可以处理不同尺度的特征。
- 缺点:对相对位置敏感,对绝对位置不敏感
- 特点:
-
RNN(循环神经网络)
- 特点:
- 序列处理:能够处理序列数据,如时间序列、文本等。对顺序敏感
- 记忆能力:通过隐藏状态传递信息,具有记忆过去信息的能力。当前时刻输出必须依赖于上一时刻运算。
- 参数共享:在序列的每个时间点上,使用相同的权重矩阵。
- 缺点:
- 梯度消失/爆炸:在长序列上训练时,梯度可能会消失或爆炸,导致训练困难。
- 串行计算耗时,每一时刻计算依赖于上一时刻计算,计算复杂度与序列长度线性关系。
- 长程建模能力弱。
- 对相对位置敏感,对绝对位置敏感。
- 特点:
-
Transformer
- 自注意力机制:每个位置的输出都与序列中所有位置有关,这使得模型能够捕捉长距离依赖关系。
- 没有局部假设,可以进行并行计算,对相对位置不敏感。
- 没有有序假设,对绝对位置不敏感,需要位置编码来反映位置变化对于特征的影响,
- 任意两个字符可以进行建模,擅长长短程建模,序列长度的平方级级别。
- 并行化处理:由于自注意力机制,Transformer可以并行处理序列中的所有元素。
- 可扩展性:容易扩展到更大的模型和更长的序列。
- Transformer由于其并行化的特性,在训练效率上通常优于RNN。
-
区别:
- 处理数据类型:CNN主要用于图像等具有网格状拓扑结构的数据,RNN和Transformer主要用于序列数据。
- 特征捕捉能力:CNN擅长捕捉局部特征,RNN擅长捕捉时间序列中的动态特征,而Transformer通过自注意力机制能够捕捉全局特征。
- 训练效率:Transformer由于其并行化的特性,在训练效率上通常优于RNN。
- 长序列处理:Transformer通过自注意力机制更好地处理长序列,而RNN可能会遇到梯度消失或爆炸的问题。
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下的注意力表征
- 这里的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函数会被推向梯度较小的区域,为了缓解这个问题,加上了这个缩放项进行量级缩小。
多头注意力机制
将注意力的计算分散到不同的子空间进行,以期望能从多方面进行注意力的学习,并行地将Q,K,V通过不同的映射矩阵映射到不同的空间(每个空间是一个头),再分别在这些空间中对应着进行单个“Scaled Dot-Product Attention”的学习,最后将得到的多头注意力表征进行拼接,经过一个额外映射层映射到原来的空间。
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。可见这里虽然分了很多头去计算,但整体的维度还是不变的,因此计算量还是一样的。
代码
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 = encoderself.decoder = decoderself.src_embed = src_embedself.tgt_embed = tgt_embedself.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 方法进行解码。 #注意要传入maskdef 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_modeldef 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:# 如果没有提供目标语言序列,则不执行任何操作。returnself.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()# 计算目标序列中非填充词的数量,用于跟踪模型训练时的词数。@staticmethoddef 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 = 0max_tgt_in_batch = 0max_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_batchreturn max(src_elements, tgt_elements)#返回源序列和目标序列总元素数中的较大值,这个值决定了批次的实际大小,确保即使在不同长度的序列中也能有效地进行填充。