Pytorch编写Transformer

2024-06-20 01:44
文章标签 pytorch transformer 编写

本文主要是介绍Pytorch编写Transformer,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本文参考自https://github.com/datawhalechina/learn-nlp-with-transformers/blob/main/docs/
在学习了图解Transformer以后,需要用Pytorch编写Transformer,下面是写代码的过程中的总结,结构根据图解Transformer进行说明。

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import math, copy, time
from torch.autograd import Variable
import matplotlib.pyplot as plt
import seaborn
import matplotlib
seaborn.set_context(context="talk")
%matplotlib inline

Pytorch编写完整的Transformer

基础的EncoderDecoder结构

class EncoderDecoder(nn.Module):# 基础的Encoder-Decoder结构def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):super().__init__()self.encoder = encoderself.decoder = decoderself.src_embed = src_embedself.tgt_embed = tgt_embedself.generator = generatordef forward(self, src, tgt, src_mask, tgt_mask):return self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask)def encode(self, src, src_mask):return self.encoder(self.src_embed(src), src_mask)def decode(self, memory, src_mask, tgt, tgt_mask):return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)

基础的EncoderDecoder结构包含encoder部分decoder部分,src_embed源语言嵌入层,tgt_embed目标语言嵌入层,generator生成器(包含linearsoftmax),用来将decoder的输出映射到词表维度,并用softmax转换成概率,generator的代码如下:

class Generator(nn.Module):# 定义生成器, 由linear和softmax组成def __init__(self, d_model, vocab):super().__init__()self.proj = nn.Linear(d_model, vocab)def forward(self, x):return F.log_softmax(self.proj(x), dim=-1)

Transformer

Transformer的编码器和解码器都使用self-attention和全连接层堆叠而成:
在这里插入图片描述

Encoder

完整的Encoder部分由N=6个完全相同的encoder_layer组成。
clones函数用来复制层,产生N个相同的层:

def clones(module, N):# 产生N个完全相同的网络层return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])

Encoder部分就是经过N=6encoder_layer,这里在最后额外添加了一个层标准化(layer-normalization),以保持架构的一致性:

class Encoder(nn.Module):# 完整的Encodersdef __init__(self, layer, N):super().__init__()self.layers = clones(layer, N)self.norm = LayerNorm(layer.size)def forward(self, x, mask):# 每一层的输入是x和maskfor layer in self.layers:x = layer(x, mask)return self.norm(x)

Encoder的每层encoder_layer包含self attention子层和FFNN子层,每个子层都使用了残差连接,和层标准化,下面是层标准化的代码:

class LayerNorm(nn.Module):"Construct a layernorm module (See citation for details)."def __init__(self, features, eps=1e-6):super().__init__()self.a_2 = nn.Parameter(torch.ones(features))self.b_2 = nn.Parameter(torch.zeros(features))self.eps = epsdef forward(self, x):mean = x.mean(-1, keepdim=True)std = x.std(-1, keepdim=True)return self.a_2 * (x - mean) / (std + self.eps) + self.b_2

这里的a_2是可学习的参数,用于调整归一化输出的尺度,初始化为1,b_2也是可学习的参数,在归一化输出中添加偏置,初始化为0,之所以加这个附加的缩放和平移变换改变取值空间,是为了使得归一化不对网络的表示能力造成负面影响,这里使用的是层归一化,对中间层的所有神经元进行归一化,这里给出公式,令第 l l l层神经元的净输入为 z ( l ) z^{(l)} z(l),其均值和方差分别为:
μ ( l ) = 1 M l ∑ i = 1 M l z i l , \mu^{(l)}=\frac{1}{M_l}\sum^{M_l}_{i=1} z^{l}_{i}, μ(l)=Ml1i=1Mlzil,
σ ( l ) 2 = 1 M l ( z i l − μ ( l ) ) 2 , {\sigma^{(l)}}^2=\frac{1}{M_l} (z^{l}_i-\mu^{(l)})^2, σ(l)2=Ml1(zilμ(l))2,
其中 M l M_l Ml为第 l l l层神经元的数量。
层归一化定义为:
z ^ ( l ) = z ( l ) − μ ( l ) σ ( l ) 2 + ϵ ⊙ γ + β ≜ L N γ , β ( z ( l ) ) \begin{aligned} \hat{\boldsymbol{z}}^{(l)} & =\frac{\boldsymbol{z}^{(l)}-\mu^{(l)}}{\sqrt{\sigma^{(l)^2}+\epsilon}} \odot \gamma+\boldsymbol{\beta} \\ & \triangleq \mathrm{LN}_{\boldsymbol{\gamma}, \boldsymbol{\beta}}\left(\boldsymbol{z}^{(l)}\right) \end{aligned} z^(l)=σ(l)2+ϵ z(l)μ(l)γ+βLNγ,β(z(l))
其中 γ , β \gamma,\beta γ,β分别代表缩放和平移的参数向量,和 z ( l ) z^{(l)} z(l)维数相同。
层归一化和批量归一化的区别是,BatchNorm在每个批次中计算均值和方差,然后使用这些统计量对每个特征进行归一化,是对单个神经元进行操作,LayerNorm是对每个样本的特征进行归一化,通常沿着最后一个维度(特征维度)进行,是对整层的神经元进行操作。

我们称呼子层为: S u b l a y e r ( x ) Sublayer(x) Sublayer(x),每个子层的最终输出是 L a y e r N o r m ( x + S u b l a y e r ( x ) ) LayerNorm(x+Sublayer(x)) LayerNorm(x+Sublayer(x))。dropout被加载Sublayer上。为了便于残差连接,模型中的所有子层以及embedding层产生的输出的维度都为 d m o d e l = 512 d_{model}=512 dmodel=512。下面的SublayerConnection类用来处理单个Sublayer的输出,该输出将继续被输入下一个Sublayer。

class SublayerConnection(nn.Module):def __init__(self, size, dropout):super().__init__()self.norm = LayerNorm(size)self.dropout = nn.Dropout(dropout)def forward(self, x, sublayer):return x + self.dropout(sublayer(self.norm(x)))

每一层encoder都有两个子层。第一层是一个multi-head self-attention层,第二层是一个全连接前馈网络,对于这两层都需要使用SublayerConnection类进行处理,见下图。
在这里插入图片描述

class EncoderLayer(nn.Module):def __init__(self, size, self_attn, feed_forward, dropout):super().__init__()self.self_attn = self_attnself.feed_forward = feed_forwardself.sublayer = clones(SublayerConnection(size, dropout), 2)self.size = sizedef forward(self, x, mask):x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))return self.sublayer[1](x, self.feed_forward)

这里的EncoderLayer就是编码器层,由两个子层构成(这里使用clone)复制了两个sublayer。

Decoder

解码器也是由 N = 6 N=6 N=6个完全相同的decoder层组成。这里也在最后额外添加了一个层标准化(layer-normalization),以保持架构的一致性:

class Decoder(nn.Module):def __init__(self, layer, N):super(Decoder, self).__init__()self.layers = clones(layer, N)self.norm = LayerNorm(layer.size)def forward(self, x, memory, src_mask, tgt_mask):for layer in self.layers:x = layer(x, memory, src_mask, tgt_mask)return self.norm(x)

单层decoder和单层encoderdecoder还有第三个子层,该层对encoder:即encoder-decoder-attention层,q向量来自decoder上一层的输出,kv向量是encoder最后层的输出向量。与encoder类似,我们在每个子层再采用残差连接,然后进行层标准化。
在这里插入图片描述

class DecoderLayer(nn.Module):def __init__(self, size, self_attn, src_attn, feed_forward, dropout):super(DecoderLayer, self).__init__()self.size = sizeself.self_attn = self_attnself.src_attn = src_attnself.feed_forward = feed_forwardself.sublayer = clones(SublayerConnection(size, dropout), 3)def forward(self, x, memory, src_mask, tgt_mask):m = memoryx = 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)

对于单层decoder中的self-attention子层,需要使用mask机制,以防止在当前位置关注到后面的位置(这里创建了一个元素为1的上三角矩阵,然后用from_numpy将np数组转换为torch张量,并使用比较操作==0来反转掩码的逻辑,即取出掩码部分,这样上三角部分为0,表示掩码部分,主对角线和下三角部分为1,表示非掩码部分)。

def subsequent_mask(size):attn_shape = (1, size, size)subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')return torch.from_numpy(subsequent_mask) == 0
plt.figure(figsize=(5,5))
plt.imshow(subsequent_mask(20)[0])

在这里插入图片描述

Attention

Attention的功能可以描述为将query和一组key-value映射到输出,其中query、key、value和输出都是向量。输出为value的加权和,其中每个value的权重通过query与相应key的计算得到。
我们将particular attention称之为缩放的点积Attention(Scaled Dot-Product Attention)。其输入为query、key(维度是 d k d_k dk)以及values(维度是d_v)。我们计算query和所有key的点积,然后对每个除以 d k \sqrt{d_k} dk ,最后用softmax函数获得value的权重。
在这里插入图片描述
在实践中,我们同时计算一组queryattention函数,并将它们组合成一个矩阵Q。keyvalue也一起组成矩阵K和V。我们计算的输出矩阵为:
Attention ⁡ ( Q , K , V ) = softmax ⁡ ( Q K T d k ) V \operatorname{Attention}(Q, K, V)=\operatorname{softmax}\left(\frac{Q K^T}{\sqrt{d_k}}\right) V Attention(Q,K,V)=softmax(dk QKT)V

def attention(query, key, value, mask=None, dropout=None):d_k = query.size(-1)scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)if mask is not None:scores = scores.masked_fill(mask == 0, -1e9)p_attn = F.softmax(scores, dim = -1)if dropout is not None:p_attn = dropout(p_attn)return torch.matmul(p_attn, value), p_attn

常用的注意力打分函数有:

  • 加性模型 s ( x , q ) = v T t a n h ( W x + U q ) , s(\bm x,\bm q)=\bm v^Ttanh(\bm W\bm x+\bm U\bm q), s(x,q)=vTtanh(Wx+Uq),
  • 点积模型 s ( x , q ) = x T q , s(\bm x,\bm q)=\bm x^T \bm q, s(x,q)=xTq,
  • 缩放点积模型 s ( x , q ) = x T q D , s(\bm x, \bm q)=\frac{\bm x^T \bm q}{\sqrt{D}}, s(x,q)=D xTq,
  • 双线性模型 s ( x . q ) = x T W q s(\bm x. \bm q)=\bm x^T \bm W \bm q s(x.q)=xTWq

理论上,加性模型和点积模型的复杂度差不多,但是点积模型在实现上可以更好地利用矩阵乘积,从而计算效率更高,当输入向量维度D比较高时,点积模型的值通常会有比较大的方差,从而导致Softmax函数的梯度会比较小( σ ( z i ) ( 1 − σ ( z i ) ) \sigma(z_i)(1-\sigma(z_i)) σ(zi)(1σ(zi)),类别分布极度不均匀时,某些非常大概率的类别和其他小概率类别都会导致梯度消失问题),如果 q q q, k k k是独立的随机变量,均值是0,方差为1,那么它们的点积 q ⋅ k q \cdot k qk均值为0方差为 d k d_k dk,具体推导如下:
E ( q i ⋅ k i ) = E ( q i ) ⋅ E ( k i ) + C o v ( q i , k i ) = E ( q i ) ⋅ E ( k i ) E(q_i \cdot k_i)=E(q_i)\cdot E(k_i)+Cov(q_i,k_i)=E(q_i)\cdot E(k_i) E(qiki)=E(qi)E(ki)+Cov(qi,ki)=E(qi)E(ki)
V a r ( q i ⋅ k i ) = V a r ( q i ) ⋅ V a r ( k i ) + V a r ( q i ) ⋅ E ( k i ) 2 + V a r ( k i ) ⋅ E ( q i ) = V a r ( q i ) ⋅ V a r ( k i ) Var(q_i \cdot k_i)=Var(q_i) \cdot Var(k_i)+ Var(q_i)\cdot E(k_i)^2+Var(k_i)\cdot E(q_i)=Var(q_i)\cdot Var(k_i) Var(qiki)=Var(qi)Var(ki)+Var(qi)E(ki)2+Var(ki)E(qi)=Var(qi)Var(ki)
E ( q ⋅ k ) = ∑ i = 1 d k E ( q i ⋅ k i ) = 0 E(q\cdot k)=\sum_{i=1}^{d_k}E(q_i\cdot k_i)=0 E(qk)=i=1dkE(qiki)=0
V a r ( q ⋅ k ) = ∑ i = 1 d k V a r ( q i ⋅ k i ) + 2 ∑ i = 1 d k − 1 ∑ j = i + 1 d k C o v ( q i k i , q j k j ) = ∑ i = 1 d k V a r ( q i ⋅ k i ) = d k Var(q\cdot k)=\sum_{i=1}^{d_k}Var(q_i\cdot k_i)+2\sum_{i=1}^{d_k-1}\sum_{j=i+1}^{d_k}Cov(q_ik_i,q_jk_j)=\sum_{i=1}^{d_k}Var(q_i \cdot k_i)=d_k Var(qk)=i=1dkVar(qiki)+2i=1dk1j=i+1dkCov(qiki,qjkj)=i=1dkVar(qiki)=dk,所以为了抵消这种放大方差的影响,将点积缩小 1 d k \frac{1}{\sqrt{d_k}} dk 1倍。

在这里插入图片描述
Multi-head attention允许模型同时关注来自不同位置的不同表示子空间的信息,如果只有一个attention head,向量的表示能力会下降。
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 , MultiHead(Q,K,V)=Concat(head_1,...,head_h)W^O, MultiHead(Q,K,V)=Concat(head1,...,headh)WO,
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 ) . where head_i = Attention(QW_i^Q,KW_i^K,VW_i^V). whereheadi=Attention(QWiQ,KWiK,VWiV).
映射由权重矩阵完成: W i Q ∈ R d m o d e l × d k W_i^Q \in R^{d_{model}\times d_k} WiQRdmodel×dk W i K ∈ R d m o d e l × d k W_i^K \in R^{d_{model}\times d_k} WiKRdmodel×dk W i V ∈ R d m o d e l × d v W_i^V \in R^{d_{model}\times d_v} WiVRdmodel×dv W i O ∈ R h d v × d m o d e l W_i^O \in R^{hd_{v}\times d_{model}} WiORhdv×dmodel

在这项工作中,我们采用 h = 8 h=8 h=8个平行attention层或者叫head。对于这些head中的每一个,我们使用 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。由于每个head的维度减小,总计算成本与具有全部维度的单个head attention相似。

class MultiHeadedAttention(nn.Module):def __init__(self, h, d_model, dropout=0.1):super(MultiHeadedAttention, self).__init__()assert d_model % h == 0self.d_k = d_model // hself.h = hself.linears = clones(nn.Linear(d_model, d_model), 4)self.attn = Noneself.dropout = nn.Dropout(p=dropout)def forward(self, query, key, value, mask=None):if mask is not None:mask = mask.unsqueeze(1)nbatches = query.size(0)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))]x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k)return self.linears[-1](x)

上面clone了四个linears,分别用于 q , k , v q,k,v q,k,v变换和最后输出的变换,assertd_model一定能整除h,并且d_k = d_model // h,这里的四个线性层都是输入d_model输出d_model,相当于变换并没有改变维度,变换后再切割成h个头,相当于最开始先得到[...,d_model]这样的Q,K,V,然后通过view(nbatches, -1, self.h, self.d_k)这样分出h个头,做了attention之后再合并成[nbatches, -1, self.h * self.d_k]这样的维度。

模型中Attention的应用

multi-head attention在Transformer中有三种不同的使用方式:

  • 在encoder-decoder attention层中,queries来自前面的decoder层,而keys和values来自encoder的输出。这使得decoder中的每个位置都能关注到输入序列中的所有序列。这是模仿序列到序列模型中典型的编码器——解码器的attention机制。
  • encoder包含self-attention层。在self-attention层中,所有key,value和query来自同一个地方,即encoder中前一层的输出。在这种情况下,encoder中的每个位置都可以关注到encoder上一层的所有位置。
  • 类似的,decoder中的self-attention层允许decoder中的每个位置都关注到decoder层中当前位置之前的所有位置(包括当前位置)。为了保持解码器的自回归特性,需要防止解码器中的信息向左流动。我们在缩放点积attention的内部,通过屏蔽softmax输入中所有的非法连接值(设置为 − ∞ -\infty )实现这一点。

基于位置的前馈网络

除了attention子层之外,我们的编码器和解码器中的每个层都包含一个全连接的前馈网络,该网络在每个层的位置相同(都在每个encoder-layer或者decoder-layer的最后)。该前馈网络包括两个线性变换,并在两个线性变换中间有一个ReLU激活函数。
F F N ( x ) = m a x ( 0 , x W 1 + b 1 ) W 2 + b 2 FFN(x)=max(0,xW_1+b_1)W_2+b_2 FFN(x)=max(0,xW1+b1)W2+b2
尽管两层都是线性变换,但它们在层与层之间使用不同的参数。另一种描述方式是两个内核大小为1的卷积。输入和输出维度都是 d m o d e l = 512 d_{model}=512 dmodel=512,内层维度是 d f f = 2048 d_{ff}=2048 dff=2048。(也就是第一层输入512维,输出2048维;第二层输入2048维,输出512维)

class PositionwiseFeedForward(nn.Module):def __init__(self, d_model, d_ff, dropout=0.1):super(PositionwiseFeedForward, self).__init__()self.w_1 = nn.Linear(d_model, d_ff)self.w_2 = nn.Linear(d_ff, d_model)self.dropout = nn.Dropout(dropout)def forward(self, x):return self.w_2(self.dropout(F.relu(self.w_1(x))))

Embeddings and Softmax

与其他seq2seq模型类似,我们使用学习到的embedding将输入token和输出token转换为 d m o d e l d_{model} dmodel维的向量。我们还使用普通的线性变换和softmax函数将解码器输出转换为预测的下一个token的概率,在我们的模型中,两个嵌入层之间和pre-softmax线性变换共享相同的权重矩阵。在embedding层中,我们将这些权重乘以 d m o d e l \sqrt{d_{model}} dmodel (一种规范化的手段,缩放嵌入向量,防止梯度消失)。

class Embeddings(nn.Module):def __init__(self, d_model, vocab):super().__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)

位置编码

由于我们的模型不包含循环和卷积,为了让模型利用序列的顺序,我们必须加入一些序列中token的相对或者绝对位置的信息。为此,我们将”位置编码“添加到编码器和解码器堆栈底部的输入embedding中。位置编码和embedding的维度相同,也是 d m o d e l d_{model} dmodel,所以这两个向量可以相加。有多种位置编码可以选择,例如通过学习得到的位置编码和固定的位置编码。

在这项工作中,我们使用不同频率的正弦和余弦函数:
P E ( p o s , 2 i ) = sin ⁡ ( p o s / 1000 0 2 i / d m o d e l ) P E ( p o s , 2 i + 1 ) = cos ⁡ ( p o s / 1000 0 2 i / d m o d e l ) \begin{gathered} P E_{(p o s, 2 i)}=\sin \left(p o s / 10000^{2 i / d_{\mathrm{model}}}\right) \\ P E_{(p o s, 2 i+1)}=\cos \left(p o s / 10000^{2 i / d_{\mathrm{model}}}\right) \end{gathered} PE(pos,2i)=sin(pos/100002i/dmodel)PE(pos,2i+1)=cos(pos/100002i/dmodel)
其中 p o s pos pos是位置, i i i是维度。也就是说,位置编码的每个维度对应于一个正弦曲线。这些波长形成一个从 2 π 2\pi 2π 10000 ⋅ 2 π 10000 \cdot 2\pi 100002π的集合级数。我们选择这个函数是因为我们假设它会让模型很容易学习到相对位置,因为对任意确定的偏移 k k k P E p o s + k PE_{pos+k} PEpos+k可以表示为 P E p o s PE_{pos} PEpos的线性函数。

此外,我们会将编码器和解码器堆栈中的embedding和位置编码的和再加一个dropout。对于基本模型,我们使用的dropout比例是 P d r o p = 0.1 P_{drop}=0.1 Pdrop=0.1

class PositionalEncoding(nn.Module):def __init__(self, d_model, dropout, max_len=5000):super().__init__()self.dropout = nn.Dropout(p=dropout)pe = torch.zeros(max_len, d_model)position = torch.arange(0, max_len).unsqueeze(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)self.register_buffer('pe', pe)def forward(self, x):x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)return self.dropout(x)

如下图,位置编码将根据位置添加正弦波。波的频率和偏移对于每个维度都是不同的。

plt.figure(figsize=(15, 5))
pe = PositionalEncoding(20, 0)
y = pe.forward(Variable(torch.zeros(1, 100, 20)))
plt.plot(np.arange(100), y[0, :, 4:8].data.numpy())
plt.legend(["dim %d" % p for p in [4,5,6,7]])
None   

在这里插入图片描述

完整模型

在这里,我们定义了一个从超参数到完整模型的函数。

def make_model(src_vocab, tgt_vocab, N=6, d_model=512, d_ff=2048, h=8, dropout=0.1):c = copy.deepcopyattn = 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),Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N),nn.Sequential(Embeddings(d_model, src_vocab), c(position)),nn.Sequential(Embeddings(d_model, tgt_vocab), c(position)),Generator(d_model, tgt_vocab))for p in model.parameters():if p.dim() > 1:nn.init.xavier_uniform(p)return model

训练

首先,我们定义一个批处理对象,其中包含用于训练的src和目标句子,以及构建掩码。

批处理和掩码

class Batch:def __init__(self, src, trg=None, pad=0):self.src = srcself.src_mask = (src != pad).unsqueeze(-2)if trg is not None:self.trg = trg[:, :-1]self.trg_y = trg[:, 1:]self.trg_mask = self.make_std_mask(self.trg, pad)self.ntokens = (self.trg_y != pad).data.sum()@staticmethoddef make_std_mask(tgt, pad):tgt_mask = (tgt != pad).unsqueeze(-2)tgt_mask = tgt_mask & Variable(subsequent_mask(tgt.size(-1)).type_as(tgt_mask.data))return tgt_mask

Batch类接受源序列src和可选的目标序列trg,以及一个用于填充的标记pad=0self.src_mask用于标识src中哪些位置是实际数据,trg_mask是用静态方法self.make_std_mask(tgt, pad)生成的,首先创建一个基本的掩码,指示填充位置,然后使用subsequent_mask函数生成后续掩码,以确保在处理序列数据时不会泄露未来的信息。ntokens属性用于计算目标数据中非填充标记的数量,这通常用于计算模型在训练过程中处理的总单词数。

接下来我们创建一个通用的训练和评估函数来跟踪损失。我们传入一个通用的损失函数,也用它来进行参数更新。

Training Loop

def run_epoch(data_iter, model, loss_compute):start = time.time()total_tokens = 0total_loss = 0tokens = 0for 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)total_loss += losstotal_tokens += batch.ntokenstokens += batch.ntokensif i % 50 == 1:elapsed = time.time() - startprint("Epoch Step: %d Loss : %f Tokens per Sec: %f" %(i, loss / batch.ntokens, tokens / elapsed))start = time.time()tokens = 0return total_loss / total_tokens

训练数据和批处理

我们在包含约405万个句子对的标准WMT 2014英语-德语数据集上进行了训练。这些句子使用字节对编码进行编码,源语句和目标语句共享大约37000个token的词汇表。对于英语-法语翻译,我们使用了明显更大的WMT 2014英语-法语数据集,该数据集由 3600 万个句子组成,并将token拆分为32000个word-piece词表。
每个训练批次包含一组句子对,句子对按相近序列长度来分批处理。每个训练批次的句子对包含大约25000个源语言的tokens和25000个目标语言的tokens。

global max_src_in_batch, max_tgt_in_batch
def batch_size_fn(new, count, sofar):"Keep augmenting batch and calculate total number of tokens + padding."global max_src_in_batch, max_tgt_in_batchif count == 1:max_src_in_batch = 0max_tgt_in_batch = 0max_src_in_batch = max(max_src_in_batch,  len(new.src))max_tgt_in_batch = max(max_tgt_in_batch,  len(new.trg) + 2)src_elements = count * max_src_in_batchtgt_elements = count * max_tgt_in_batchreturn max(src_elements, tgt_elements)

硬件和训练时间

我们在一台配备8个 NVIDIA P100 GPU 的机器上训练我们的模型。使用论文中描述的超参数的base models,每个训练step大约需要0.4秒。我们对base models进行了总共10万steps或12小时的训练。而对于big models,每个step训练时间为1.0秒,big models训练了30万steps(3.5 天)。

Optimizer

我们使用Adam优化器,其中 β 1 = 0.9 , β 2 = 0.98 \beta_1 = 0.9, \beta_2 = 0.98 β1=0.9,β2=0.98并且 ϵ = 1 0 − 9 \epsilon = 10^{-9} ϵ=109。我们根据以下公式在训练过程中改变学习率:
lrate  = d model  − 0.5 ⋅ min ⁡ ( step  _ num  − 0.5 , step  _ num  ⋅ warmup  _ steps  − 1.5 ) \text { lrate }=d_{\text {model }}^{-0.5} \cdot \min \left(\text { step } \_ \text {num }{ }^{-0.5}, \text { step } \_ \text {num } \cdot \text { warmup } \_ \text {steps }{ }^{-1.5}\right)  lrate =dmodel 0.5min( step _num 0.5, step _num  warmup _steps 1.5)
这对应于在第一次 w a r m u p s t e p s warmup_steps warmupsteps步中线性地增加学习率,并且随后将其与步数的平方根成比例地减小。我们使用 w a r m u p s t e p s = 4000 warmup_steps=4000 warmupsteps=4000

class NoamOpt:"Optim wrapper that implements rate."def __init__(self, model_size, factor, warmup, optimizer):self.optimizer = optimizerself._step = 0self.warmup = warmupself.factor = factorself.model_size = model_sizeself._rate = 0def step(self):"Update parameters and rate"self._step += 1rate = self.rate()for p in self.optimizer.param_groups:p['lr'] = rateself._rate = rateself.optimizer.step()def rate(self, step = None):"Implement `lrate` above"if step is None:step = self._stepreturn self.factor * \(self.model_size ** (-0.5) *min(step ** (-0.5), step * self.warmup ** (-1.5)))def get_std_opt(model):return NoamOpt(model.src_embed[0].d_model, 2, 4000,torch.optim.Adam(model.parameters(), lr=0, betas=(0.9, 0.98), eps=1e-9))

以下是此模型针对不同模型大小和优化超参数的曲线示例。

opts = [NoamOpt(512, 1, 4000, None),NoamOpt(512, 1, 8000, None),NoamOpt(256, 1, 4000, None)]
plt.plot(np.arange(1, 20000), [[opt.rate(i) for opt in opts] for i in range(1, 20000)])
plt.legend(["512:4000", "512:8000", "256:4000"])
None

在这里插入图片描述

正则化

标签平滑

在训练过程中,我们使用的label平滑的值为 ϵ l s = 0.1 \epsilon_{ls}=0.1 ϵls=0.1。虽然对label进行平滑会让模型困惑,但提高了准确性和BLEU得分。

我们使用KL div损失实现标签平滑。我们没有使用one-hot独热分布,而是创建了一个分布,该分布设定目标分布为1-smoothing,将剩余概率分配给词表中的其他单词。

class LabelSmoothing(nn.Module):def __init__(self, size, padding_idx, smoothing=0.0):super().__init__()self.criterion = nn.KLDivLoss(size_average=False)self.padding_idx = padding_idxself.confidence = 1.0 - smoothingself.smoothing = smoothingself.size = sizeself.true_dist = Nonedef forward(self, x, target):assert x.size(1) == self.sizetrue_dist = x.data.clone()true_dist.fill_(self.smoothing / (self.size - 2))target = target.data.long()true_dist.scatter_(1, target.data.unsqueeze(1), self.confidence)true_dist[:, self.padding_idx] = 0mask = torch.nonzero(target.data == self.padding_idx)if mask.dim() > 0:true_dist.index_fill_(0, mask.squeeze(), 0.0)self.true_dist = true_distreturn self.criterion(x, Variable(true_dist, requires_grad=False))

下面我们看一个例子,看看平滑后的真实概率分布。

# example of label smoothing
crit = LabelSmoothing(5, 0, 0.4)
predict = torch.FloatTensor([[0, 0.2, 0.7, 0.1, 0],[0, 0.2, 0.7, 0.1, 0],[0, 0.2, 0.7, 0.1, 0]])
v = crit(Variable(predict.log()), Variable(torch.LongTensor([2, 1, 0])))plt.imshow(crit.true_dist)
None

在这里插入图片描述

print(crit.true_dist)

在这里插入图片描述
由于标签平滑的存在,如果模型对于某个单词特别有信心,输出特别大的概率,会被惩罚。如下代码所示,随着输入x的增大,x/d会越来越大,1/d会越来越小,但是loss并不是一直降低的。

crit = LabelSmoothing(5, 0, 0.1)
def loss(x):d = x + 3 * 1predict = torch.FloatTensor([[0, x / d, 1 / d, 1 / d, 1 / d],]) + 1e-10# print(Variable(predict.log()), Variable(torch.LongTensor([1])))return crit(Variable(predict.log()), Variable(torch.LongTensor([1]))).item()y = [loss(x) for x in range(1, 100)]
x = np.arange(1, 100)
plt.plot(x, y)

在这里插入图片描述

实例

我们可以从尝试一个简单的复制任务开始。给定来自小词汇表的一组随机输入符号symbols,目标是生成这些相同的符号。

合成数据

def data_gen(V, batch, nbatches):for i in range(nbatches):data = torch.from_numpy(np.random.randint(1, V, size=(batch, 10)))data[:, 0] = 1src = Variable(data, requires_grad=False)tgt = Variable(data, requires_grad=False)yield Batch(src, tgt, 0)

损失函数计算

class SimpleLossCompute:def __init__(self, generator, criterion, opt=None):self.generator = generatorself.criterion = criterionself.opt = optdef __call__(self, x, y, norm):x = self.generator(x)loss = self.criterion(x.contiguous().view(-1, x.size(-1)),y.contiguous().view(-1)) / normloss.backward()if self.opt is not None:self.opt.step()self.opt.optimizer.zero_grad()return loss.item() * norm

贪婪解码

V = 11
criterion = LabelSmoothing(size=V, padding_idx=0, smoothing=0.0)
model = make_model(V, V, N=2)
model_opt = NoamOpt(model.src_embed[0].d_model, 1, 400,torch.optim.Adam(model.parameters(), lr=0, betas=(0.9, 0.98), eps=1e-9))for epoch in range(10):model.train()run_epoch(data_gen(V, 30, 20), model,SimpleLossCompute(model.generator, criterion, model_opt))model.eval()print(run_epoch(data_gen(V, 30, 5), model,SimpleLossCompute(model.generator, criterion, None)))

在这里插入图片描述
为了简单起见,此代码使用贪婪解码来预测翻译。

def greedy_decode(model, src, src_mask, max_len, start_symbol):memory = model.encode(src, src_mask)ys = torch.ones(1, 1).fill_(start_symbol).type_as(src.data)for i in range(max_len-1):out = model.decode(memory, src_mask, Variable(ys),Variable(subsequent_mask(ys.size(1)).type_as(src.data)))prob = model.generator(out[:, -1])_, next_word = torch.max(prob, dim = 1)next_word = next_word.data[0]ys = torch.cat([ys, torch.ones(1, 1).type_as(src.data).fill_(next_word)], dim=1)return ysmodel.eval()
src = Variable(torch.LongTensor([[1,2,3,4,5,6,7,8,9,10]]))
src_mask = Variable(torch.ones(1, 1, 10))
print(greedy_decode(model, src, src_mask, max_len=10, start_symbol=1))

维度变换

以上述例子为例,我们取batch_size=30,src_length=10,tgt_length=10,在训练过程中,将目标序列的最后一个元素移除,这样0:8是当前步骤的输出依赖的之前步骤的输出,而1:9是当前步骤的输出的标签。我们来看一下维度的变化。

  • EncoderDecoder
    输入给EncoderDecodersrc.shape=torch.Size([30, 10])tgt_shape=torch.Size([30, 9])src_mask.shape=torch.Size([30, 1, 10])tgt_mask.shape=torch.Size([30, 9, 9])

  • Encoder

  • Embeddings的输入是[30, 10],输出是[30, 10, 512],将每个词变成了嵌入向量。

  • PositionalEncoding的输入是[30, 10, 512],其中的pe维度是[1, 5000, 512],输出是[30, 10, 512],将位置编码添加到了嵌入向量里,不改变维度。

  • Encoder=Encoderlayer * 6=(SublayerConnection * 2) * 6=((MultiHeadedAttention + norm + resnet) + (FFN + norm + resnet)) * 6,最终的输出维度是[30, 10, 512]

  • 在MutiHeadedAttention中,输入的q,k,v[30, 10, 512],经过线性变换以后根据head=8进行分割[30,8,10,64],其中self_attn.shape=[30,10,10],最后将得分进行合并仍是[30, 10, 512]

  • Decoder

  • Embeddings的输入是[30, 9],输出是[30, 9, 512],将每个词变成了嵌入向量,Positional同Encoder,最终输出是[30, 9, 512]

  • Decoder=Decoderlayer * 6=(SublayerConnection * 2) * 6=((MultiHeadedAttention1 + norm + resnet) + (MultiHeadedAttention2 + norm + resnet) + (FFN + norm + resnet)) * 6,最终的输出维度是[30, 9, 512]。这里第一个多头注意力是自注意力,所以输入输出都是[30, 9, 512],第二个是q来自之前的输出[30, 9, 512],k,v来自memory[30, 10, 512],最终输出[30, 9, 512]

  • 在上述例子中,首先用start_symbol=1作为decoder输入,用[1,2,3,4,5,6,7,8,9,10]作为源序列,src_mask全为1表示源序列所有位置都有效。简单的测试了一下用这个结构进行编解码。

真实场景示例

由于原始的教程的真实数据场景需要多GPU训练,所以这里仅使用合成的数据对其进行了训练和预测。

这篇关于Pytorch编写Transformer的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1076750

相关文章

基于CTPN(tensorflow)+CRNN(pytorch)+CTC的不定长文本检测和识别

转发来源:https://swift.ctolib.com/ooooverflow-chinese-ocr.html chinese-ocr 基于CTPN(tensorflow)+CRNN(pytorch)+CTC的不定长文本检测和识别 环境部署 sh setup.sh 使用环境: python 3.6 + tensorflow 1.10 +pytorch 0.4.1 注:CPU环境

PyTorch模型_trace实战:深入理解与应用

pytorch使用trace模型 1、使用trace生成torchscript模型2、使用trace的模型预测 1、使用trace生成torchscript模型 def save_trace(model, input, save_path):traced_script_model = torch.jit.trace(model, input)<

BD错误集锦8——在集成Spring MVC + MyBtis编写mapper文件时需要注意格式 You have an error in your SQL syntax

报错的文件 <?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.yuan.dao.YuanUserDao"><!

如何利用echarts编写立体的柱状图表

1、引入 import * as echarts from 'echarts' 2、创建图标容器 3、调用渲染 <template><div ref="eachrtsBox" style="width: 200px;height: 200px;"></div></template><script>import * as echarts from 'echarts'export d

ssh在本地虚拟机中的应用——解决虚拟机中编写和阅读代码不方便问题的一个小技巧

虚拟机中编程小技巧分享——ssh的使用 事情的起因是这样的:前几天一位工程师过来我这边,他看到我在主机和虚拟机运行了两个vscode环境,不经意间提了句:“这么艰苦的环境写代码啊”。 后来我一想:确实。 我长时间以来都是直接在虚拟机里写的代码,但是毕竟是虚拟机嘛,有时候编辑器没那么流畅,在文件比较多的时候跳转很麻烦,容易卡住。因此,我当晚简单思考了一下,想到了一个可行的解决方法——即用ssh

简单 使用 的makefile编写 框架

1、指定编译器,如海思平台:CROSS_COMPILE=arm-hisiv100nptl-linux-; 2、指定编译工具:GCC=$(CROSS_COMPILE)gcc   CC=$(CROSS_COMPILE)g++; 3、使用 export 导出 子makefile 要用的变量; 4、定义变量的形式  指定 工程源文件 需要使用到的 “宏”,在后面的 LDFLAGS 里面使用 -D将其

2025秋招NLP算法面试真题(二)-史上最全Transformer面试题:灵魂20问帮你彻底搞定Transformer

简单介绍 之前的20个问题的文章在这里: https://zhuanlan.zhihu.com/p/148656446 其实这20个问题不是让大家背答案,而是为了帮助大家梳理 transformer的相关知识点,所以你注意看会发现我的问题也是有某种顺序的。 本文涉及到的代码可以在这里找到: https://github.com/DA-southampton/NLP_ability 问题

(转)Sublime Text 2 (Emmet):HTML/CSS代码快速编写神器

Emmet的前身是大名鼎鼎的Zen coding,如果你从事Web前端开发的话,对该插件一定不会陌生。它使用仿CSS选择器的语法来生成代码,大大提高了HTML/CSS代码编写的速度,比如下面的演示:   Zen coding下的编码演示   去年年底,该插件已经改名为Emmet。但Emmet不只改名,还带来了一些新特性。本文就来直观地演示给你。 一、快速编写HTML代码 1.

pytorch国内镜像源安装及测试

一、安装命令:  pip install torch torchvision torchaudio -i https://pypi.tuna.tsinghua.edu.cn/simple  二、测试: import torchx = torch.rand(5, 3)print(x)

PyTorch nn.MSELoss() 均方误差损失函数详解和要点提醒

文章目录 nn.MSELoss() 均方误差损失函数参数数学公式元素版本 要点附录 参考链接 nn.MSELoss() 均方误差损失函数 torch.nn.MSELoss(size_average=None, reduce=None, reduction='mean') Creates a criterion that measures the mean squared err