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训练,所以这里仅使用合成的数据对其进行了训练和预测。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/30398.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

前字节员工自爆:我原腾讯一哥们,跳槽去小公司做小领导,就签了竞业,又从小公司离职去了对手公司,结果被发现了,小公司要他赔80万

“世界那么大,我想去看看”,这句曾经火遍网络的辞职宣言,说出了多少职场人心中的渴望。然而,当我们真的迈出跳槽那一步时,才发现,现实远比想象中残酷得多。 最近,一起前字节跳动员工爆料的事件…

年终奖发放没几天,提离职领导指责我不厚道,我该怎么办?

“年终奖都发了,你还跳槽?太不厚道了吧!” “拿完年终奖就走人,这不是典型的‘骑驴找马’吗?” 每到岁末年初,关于“拿到年终奖后是否应该立即辞职”的话题总会引发热议。支持者认为,这是个人…

如何验证Rust中的字符串变量在超出作用域时自动释放内存?

讲动人的故事,写懂人的代码 在公司内部的Rust培训课上,讲师贾克强比较了 Rust、Java 和 C++ 三种编程语言在变量越过作用域时自动释放堆内存的不同特性。 Rust 通过所有权系统和借用检查,实现了内存安全和自动管理,从而避免了大部分内存泄漏。Rust 自动管理标准库中数据类…

PID控制算法学习笔记分享

目录 一、参数设定 二、PID计算公式 三、位置式PID代码实现 四、增量式PID代码实现 五、两种控制算法的优缺点 六、PID算法的改进 一、参数设定 比例系数(kp):P项的作用是根据当前误差的大小来产生一个控制量。它直接与误差成正比&#…

【机器学习300问】126、词嵌入(Word Embedding)是什么意思?

人类的文字,作为一种高度抽象化的符号系统,承载着丰富而复杂的信息。为了让电脑也能像人类一样理解并处理这些文字,科学家们不断探索各种方法,以期将人类的语言转化为计算机能够理解的格式。 一、One-Hot编码的不足 在自然语言处…

NSSCTF中的[WUSTCTF 2020]朴实无华、[FSCTF 2023]源码!启动! 、[LitCTF 2023]Flag点击就送! 以及相关知识点

目录 [WUSTCTF 2020]朴实无华 [FSCTF 2023]源码!启动! [LitCTF 2023]Flag点击就送! 相关知识点 1.intval 绕过 绕过的方式: 2.session伪造攻击 [WUSTCTF 2020]朴实无华 1.进入页面几乎没什么可用的信息,所以想到使用dis…

Spring MVC学习记录(基础)

目录 1.SpringMVC概述1.1 MVC介绍1.2 Spring MVC介绍1.3 Spring MVC 的核心组件1.4 SpringMVC 工作原理 2.Spring MVC入门2.1 入门案例2.2 总结 3.RequestMapping注解4.controller方法返回值4.1 返回ModelAndView4.2 返回字符串4.2.1 逻辑视图名4.2.2 Redirect重定向4.2.3 forw…

Shopee菲律宾本土店允许中途无理由退货,如何应对退货后库存混乱问题?

Shopee菲律宾本土店最近实施了一项新政策,自2024年6月10日起,允许买家在商品仍在运输途中申请退货与退款,此即“在途退货/退款”功能,主要的目的是为了提升买家的购物体验,增强市场竞争力。 图源:Shopee菲律…

一年前 LLM AGI 碎片化思考与回顾系列⑤ · 探索SystemⅡ复杂推理的未知之境

阅读提示: 本篇系列内容的是建立于自己过去一年在以LLM为代表的AIGC快速发展浪潮中结合学术界与产业界创新与进展的一些碎片化思考并记录最终沉淀完成,在内容上,与不久前刚刚完稿的那篇10万字文章 「融合RL与LLM思想,探寻世界模型…

vue3delete请求报403forbidden,前后端解决方式,cookie无效问题

在做开发时,前期已经在Controller类加上CrossOrigin(origins "*"),发送get和post请求都没问题,但遇到delete请求时,又报出跨域问题 一.前端添加proxy代理服务器(未能解决) 在vue.config.js中使…

连接Huggingface报requests.exceptions.SSLError错误

最近在学习使用 SHAP 算法解释 BERT 模型的输出结果,然而在从 Huggingface 上导入模型和数据集的过程中出现了网络连接相关的错误,本文用于记录错误类型和解决错误的方法。 1 代码示例 SHAP 官方展示的代码如下: import datasets import nu…

Linux screen命令使用

文章目录 1. 前言2. screen是什么?3. screen使用场景描述3. screen常用命令4. 小结5. 参考 1. 前言 实际开发中用到的云服务器,如果项目使用的是python,需要利用项目运行一些时间较长的项目程序脚本的话,由于我们通过ssh连接远端服务器&…

一文详解扩散模型

文章目录 1、常见的生成模型2、变分推断简介3、文生图的评价指标4、Diffusion Models5、其他技术交流群精选 节前,我们星球组织了一场算法岗技术&面试讨论会,邀请了一些互联网大厂朋友、参加社招和校招面试的同学。 针对算法岗技术趋势、大模型落地…

2024年通信安全员ABC证证考试题库及通信安全员ABC证试题解析

题库来源:安全生产模拟考试一点通公众号小程序 2024年通信安全员ABC证证考试题库及通信安全员ABC证试题解析是安全生产模拟考试一点通结合(安监局)特种作业人员操作证考试大纲和(质检局)特种设备作业人员上岗证考试大…

项目3:从0开始的RPC框架(扩展版)-3

七. 负载均衡 1. 需求分析 目前我们的RPC框架仅允许消费者读取第一个服务提供者的服务节点,但在实际应用中,同一个服务会有多个服务提供者上传节点信息。如果消费者只读取第一个,势必会增大单个节点的压力,并且也浪费了其它节点…

Jenkins+K8s实现持续集成(一)

镜像仓库的搭建 docker run -d \--restartalways \--name registry \-p 5000:5000 \-v /root/devops/registry/data:/var/lib/registry \registry安装完之后,执行下面命令可以看到镜像仓库已经安装成功 docker ps 然后在浏览器上输入下面地址进行访问 http://ip:…

车牌号识别(低级版)

import cv2 from matplotlib import pyplot as plt import os import numpy as np from paddleocr import PaddleOCR, draw_ocr from PIL import Image, ImageDraw, ImageFont# 利用paddelOCR进行文字扫描,并输出结果 def text_scan(img_path):ocr PaddleOCR(use_a…

HTML(11)——CSS三大特性

CSS拥有三大特性&#xff0c;分别是&#xff1a;继承性&#xff0c;层叠性&#xff0c;优先级 继承性 说明&#xff1a;子级标签默认继承父级标签的文字控制属性。 如果子级自己有样式&#xff0c;则父级的属性不生效 例如&#xff1a; <style> body{ font-size:30px;…

ADS1220芯片写寄存器失败

1&#xff09;场景&#xff1a;最近调试ADS1220 的芯片&#xff0c;需要读取不同通道的AD值&#xff0c;修改寄存器0的值时一直失败 但是在单片机启动时&#xff0c;写寄存器0时&#xff0c;值能正确写入&#xff0c;并正确读出&#xff0c;之后写完读取出的都是FF或其他异常值…

CobaltStrike后渗透进阶篇

0x01 网络钓鱼攻击 钓鱼攻击简介 钓鱼攻击主要通过生成的木马诱使受害者运行后上线&#xff0c;其中木马一般都伪装成正常的程序。与此同时配合钓鱼网站可帮助攻击者模拟真实网站诱骗受害者访问&#xff0c;达到获取账号密码、上线木马等目的。接下来主要介绍后门程序的生成及…