当前位置:   article > 正文

Pytorch编写Transformer_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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

基础的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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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)])
  • 1
  • 2
  • 3
'
运行

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这里的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 ) ) ˆz(l)=z(l)μ(l)σ(l)2+ϵγ+βLNγ,β(z(l))

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)))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

每一层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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这里的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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

单层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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

对于单层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
  • 1
  • 2
  • 3
  • 4
'
运行
plt.figure(figsize=(5,5))
plt.imshow(subsequent_mask(20)[0])
  • 1
  • 2

在这里插入图片描述

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
'
运行

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

  • 加性模型 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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

上面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))))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

位置编码

由于我们的模型不包含循环和卷积,为了让模型利用序列的顺序,我们必须加入一些序列中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 ) PE(pos,2i)=sin(pos/100002i/dmodel)PE(pos,2i+1)=cos(pos/100002i/dmodel)

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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

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   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在这里插入图片描述

完整模型

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

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
'
运行

训练

首先,我们定义一个批处理对象,其中包含用于训练的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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
'
运行

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
'
运行

训练数据和批处理

我们在包含约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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
'
运行

硬件和训练时间

我们在一台配备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))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
'
运行

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

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在这里插入图片描述

正则化

标签平滑

在训练过程中,我们使用的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))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

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

# 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在这里插入图片描述

print(crit.true_dist)
  • 1

在这里插入图片描述
由于标签平滑的存在,如果模型对于某个单词特别有信心,输出特别大的概率,会被惩罚。如下代码所示,随着输入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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

在这里插入图片描述

实例

我们可以从尝试一个简单的复制任务开始。给定来自小词汇表的一组随机输入符号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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
'
运行

损失函数计算

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
'
运行

贪婪解码

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)))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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

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))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

维度变换

以上述例子为例,我们取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 + residual Connection) + (FFN + norm + residual Connection)) * 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 + residual Connection) + (FFN + norm + residual Connection)) * 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训练,所以这里仅使用合成的数据对其进行了训练和预测。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/886951
推荐阅读
相关标签
  

闽ICP备14008679号