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 = encoder
        self.decoder = decoder
        self.src_embed = src_embed
        self.tgt_embed = tgt_embed
        self.generator = generator

    def 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):
    # 完整的Encoders
    def __init__(self, layer, N):
        super().__init__()
        self.layers = clones(layer, N)
        self.norm = LayerNorm(layer.size)

    def forward(self, x, mask):
        # 每一层的输入是x和mask
        for 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 = eps

    def 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_attn
        self.feed_forward = feed_forward
        self.sublayer = clones(SublayerConnection(size, dropout), 2)
        self.size = size

    def 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 = size
        self.self_attn = self_attn
        self.src_attn = src_attn
        self.feed_forward = feed_forward
        self.sublayer = clones(SublayerConnection(size, dropout), 3)

    def forward(self, x, memory, src_mask, tgt_mask):
        m = memory
        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)

对于单层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 == 0
        self.d_k = d_model // h
        self.h = h
        self.linears = clones(nn.Linear(d_model, d_model), 4)
        self.attn = None
        self.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_model

    def 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.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),
        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 = src
        self.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()

    @staticmethod
    def 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 = 0
    total_loss = 0
    tokens = 0
    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)
        total_loss += loss
        total_tokens += batch.ntokens
        tokens += batch.ntokens
        if i % 50 == 1:
            elapsed = time.time() - start
            print("Epoch Step: %d Loss : %f Tokens per Sec: %f" %
                 (i, loss / batch.ntokens, tokens / elapsed))
            start = time.time()
            tokens = 0
    return 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_batch
    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_tgt_in_batch = max(max_tgt_in_batch,  len(new.trg) + 2)
    src_elements = count * max_src_in_batch
    tgt_elements = count * max_tgt_in_batch
    return 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 = optimizer
        self._step = 0
        self.warmup = warmup
        self.factor = factor
        self.model_size = model_size
        self._rate = 0
        
    def step(self):
        "Update parameters and rate"
        self._step += 1
        rate = self.rate()
        for p in self.optimizer.param_groups:
            p['lr'] = rate
        self._rate = rate
        self.optimizer.step()
        
    def rate(self, step = None):
        "Implement `lrate` above"
        if step is None:
            step = self._step
        return 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_idx
        self.confidence = 1.0 - smoothing
        self.smoothing = smoothing
        self.size = size
        self.true_dist = None

    def forward(self, x, target):
        assert x.size(1) == self.size
        true_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] = 0
        mask = torch.nonzero(target.data == self.padding_idx)
        if mask.dim() > 0:
            true_dist.index_fill_(0, mask.squeeze(), 0.0)
        self.true_dist = true_dist
        return 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 * 1
    predict = 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] = 1
        src = 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 = generator
        self.criterion = criterion
        self.opt = opt

    def __call__(self, x, y, norm):
        x = self.generator(x)
        loss = self.criterion(x.contiguous().view(-1, x.size(-1)),
                             y.contiguous().view(-1)) / norm
        loss.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 ys

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

xss.haozi.me靶场通关参考

url&#xff1a;https://xss.haozi.me/ 文章目录 0x000x010x020x030x040x050x060x070x080x090x0A0x0B0x0C00xD00xE00xF0x100x110x12 0x00 先看js代码&#xff0c;第一关给你热热手&#xff0c;没给你加过 payload&#xff1a; <script>alert(1)</script>0x01 这…

Win11 删除文件时提示“找不到该项目,请重试”的解决办法

1、Win R 打开运行窗口&#xff0c;输入 notepad 并回车打开文本文档(记事本)软件&#xff0c;如下图&#xff1a; 2、在文本文档(记事本)软件中复制粘贴以下代码&#xff0c;如下图&#xff1a; del /f /a /q \\?\%1 rd /s /q \\?\%1或DEL /F /A /Q \\?\%1 RD /S /Q \\?…

4个工作学习必备的工具,请不要错过

一键改图 “一键改图”是一款简单易用的在线图片处理工具&#xff0c;提供了多种功能来帮助用户快速处理图片。具体功能包括&#xff1a; 压缩图片质量&#xff1a;可以调整图片的质量&#xff0c;使其更小但不失真。 压缩图片尺寸&#xff1a;可以根据需要调整图片的尺寸。 …

leetcode刷题日志-旋转图像

题目描述 解题思路 也就是一个找规律的题&#xff0c;可以写一个转换前后的坐标对比&#xff0c;很容易能找到规律。[i,j]->[j,length-i-1]上式就是找到的规律。在解题的时候遇到过一个问题&#xff0c;就是如果我单纯的将数组复制给一个中间变量&#xff0c;或者是将数组中…

Vue发送http请求

1.创建项目 创建一个新的 Vue 2 项目非常简单。在终端中&#xff0c;进入您希望创建项目的目录(我的目录是D:\vue)&#xff0c;并运行以下命令&#xff1a; vue create vue_test 2.切换到项目目录&#xff0c;运行项目 运行成功后&#xff0c;你将会看到以下的编译成功的提示…

力扣-两数之和

文章目录 题目题解方法1-暴力方法2-哈希 题目 原题链接&#xff1a;两数之和 题解 方法1-暴力 我最先想到的方法就是暴力&#xff0c;两层for循环&#xff0c;也能通过。&#xff08;拿到算法题在没有思路的时候暴力就是思路&#xff0c;哈哈哈&#xff09; public class T…

无需安装就能一键部署Stable Diffusion 3?

一键部署使用SD3&#xff1f;让你的创作更加便捷&#xff01; 前言 厚德云上架SD3! 距离Stable Diffusion 3的上线已经有一阵时间了。从上线至今SD3也是一直好评不断&#xff0c;各项性能的提升也让它荣获“最强开源新模型”的称号。成为了AI绘画设计师们新的香馍馍。 可对于SD…

调取Windows系统虚拟键盘

目录 一 设计原型 二 后台源码 一 设计原型 二 后台源码 using System.Diagnostics;namespace 调取Windows虚拟键盘 {public partial class Form1 : Form{public Form1(){InitializeComponent();}private void richTextBox1_DoubleClick(object sender, EventArgs e){Proces…

Elementor无需第三方插件实现高级下拉菜单/巨型菜单

使用新的嵌套功能创建美观的菜单和大型菜单。巨型菜单是具有复杂导航结构和独特设计的网站的理想选择。 Elementor-设置-特性-Menu启用 之后再去前端编辑器设计即可&#xff0c;就会有一个新的menu菜单模块了。 这个菜单的下拉则是通过Elementor直接来设计&#xff0c;也就以为…

【启明智显产品介绍】Model3工业级HMI芯片详解系列专题(一):芯片性能

Model3工业级跨界MCU是一款国产自主的基于RISC-V架构的高性能芯片&#xff0c;内置平头哥玄铁E907&#xff0c;主频480MHz&#xff0c;片上1MB大容量SRAM以及64Mb PSRAM。 Model3工业级MCU具有丰富的屏接口、高分辨率PWM和多路高精度定时器&#xff0c;可以处理各类实时数据与实…

HTML(11)——CSS三大特性

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

常见的创建型设计模式( 一 )

设计模式( 一 ) 常见的创建型设计模式 1.单例模式 : 确保一个类只有一个实例 , 为整个程序提供一个全局的访问接口。getInstance 实现方式 饿汉式&#xff0c;在调用getInstance 创建实例的时候 &#xff0c;实例已经存在了 &#xff0c;不需要我们再次去 new创建。 优点&a…

css布局之flex应用

/*父 100*/.parent-div {/* 这里添加你想要的属性 */display: flex;flex-direction: row; //行justify-content: space-between; //左右对齐align-items: center;flex-wrap: wrap; //换行}/*中 90 10 */.middle-div {/* 这里添加你想要的属性 */display: flex;flex-direction:…

jenkins安装和使用 (二)

参考视频资料 https://www.bilibili.com/video/BV1bS4y1471A?p10&vd_sourcee0dcd147bd5d730317de804d788cd6f9 安装maven插件 新建item 配置构建信息 项目地址替换为自己的实际地址 其余保持先保持默认 先然后在主页就看到了这个项目 查看控制台输出 稍等一…

小林图解系统-二.硬件结构 2.5CPU是如何执行任务的?

CPU如何读取数据的&#xff1f; CPU访问L1 Cache速度比访问内存快100倍&#xff0c;有高速缓存的目的&#xff1a;把Cache作为CPU与内存之间的缓存层&#xff0c;减少对内存的访问频率 所有CPU Cache Line是CPU从内存读取数据到Cache的单位。 64字节 CPU加载数组里面连续的多…

Kubernetes入门-Kubernetes集群构成

目录 前言 控制面板节点与工作节点 控制面板节点 工作节点 被托管的Kubernetes 用kubectl命令行工具管理Kubernetes 前言 前面说过&#xff0c;Kubernetes是云的操作系统顾名思义&#xff0c;它位于应用和基础设施之间Kubernetes运行在基础设施上&#xff0c;而应用运行…

【Linux安装Conda环境的详细教程】

&#x1f308;个人主页: 程序员不想敲代码啊 &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共…

服务器流量收发测试

文章目录 一、概述二、实现方式一&#xff1a;编码1. 主要流程2. 核心代码3. 布署 三、实现方式二&#xff1a;脚本1.脚本编写2. 新增crontab任务 四、查看结果 一、概述 我们在安装vnStat、wondershaper便想通过实际的数据收发来进行测试。 二、实现方式一&#xff1a;编码 …

外包干了2年,彻底废了...

先说一下自己的情况。大专生&#xff0c;17年通过校招进入湖南某软件公司&#xff0c;干了接近2年的点点点&#xff0c;今年年上旬&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落&#xff01;而我已经在一个企业干了五年的功能测试…

UniAudio 1.5:大型语言模型(LLMs)驱动的音频编解码器

大型语言模型&#xff08;LLMs&#xff09;在文本理解和生成方面展示了卓越的能力&#xff0c;但它们不能直接应用于跨模态任务&#xff0c;除非进行微调。本文提出了一种跨模态上下文学习方法&#xff0c;使未进行进一步训练的LLMs能够在少量示例的情况下&#xff0c;无需任何…