Transformer源码详解

Transformer源码详解

Transformer是用来做什么工作的

transformer提出之初适用于做机器翻译工作的,比如给一句中文“我爱你”,利用transformer将其翻译为英文“I love you”
在这里插入图片描述

Transformer架构图

transformer架构

上图展示transformer就是由两部分组成,分别为encoder和decoder,对于要被翻译的原文“我爱你”,先经过Embedding和位置编码后,输入到encoder中,encoder在实际应用过程中会经过多层,一般为6层,之后得到输出结果,再输入到decoder中,encoder向decoder的第二层的Multi-Head Attention提供K,V矩阵(K,V是由encoder编码后的结果经过线性变换得到的),同样decoder一般也为6层。具体的内部细节就不在这里赘述,毕竟咱这主要是对代码的讲解

下面我们将先给出Transformer整体架构的代码,而不对其内部实现细节做描述。其内部实现将从整体到局部,再到细节,拆解成以下几个部分逐个讲解

  • Encoder
    • LayerNorm
    • SublayerConnection
    • EncoderLayer
    • Position-wise Feed-Forward Networks
    • attention
    • MultiHeadAttention
  • Decoder
    • DecoderLayer
    • subseqent_mask
  • Generater
  • PositionalEncoding
  • Embedding
  • Mask
    • get_src_pad_mask
    • make_std_mask

transformer的整体架构代码:

class EncoderDecoder(nn.Module):

    def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
        super(EncoderDecoder, self).__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):
        "这里encode输入的src是已经经过Embedding过后的数据"
        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)

上面的代码没什么好说的就是一个简单的编码器-解码器类,实例化该类要给出我们写好的encoder,decoder,encoder和decoder输入数据的编码方式,和最后输出结果的generator,类中由两个方法encode和decode分别实现编码和解码

Encoder

class Encoder(nn.Module):
    "这个编码器将包含多个encoderlayer"
    
    def __init__(self, layer, N):
        # layer: 就是要给出的encoderlayer
        # N: 要叠加多少层的encoderlayer
        super(Encoder, self).__init__()
        
        # 首先克隆出多层encoderlayer放入属性layers中
        self.layers = clones(layer, N)
        # 每一层encoderlayer都要经过一层规范反再输入给下一层
        # 同样Encoder过后将结果给Decoder之前也要进行规范化,这里我们定义了norm就是为了给			# Decoder之前将其规范法
        self.norm = LayerNorm(layer.size)
        
    def forward(self, x, mask):
        for layer in self.layers:
            x = layer(x, mask)
        return self.norm(x)

上面的代码用到了clones函数和LayerNorm函数,它们的定义如下:

clones函数:

def clones(module, N):
    "就是将一个module复制N个次年放到一个列表中"
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])

LayerNorm

class LayerNorm(nn.Module):

    def __init__(self, features, eps=1e-6):
        super(LayerNorm, self).__init__()
        # features: [batch_size, max_len, d_model]
        self.a_2 = nn.Parameter(torch.ones(features))
        self.b_2 = nn.Parameter(torch.zeros(features))
        self.eps = eps

    def forward(self, x):
        # x: [batchsize, max_len, d_model]
        mean = x.mean(-1, keepdim=True)  # mean: [batch_size, max_len, 1]
        std = x.std(-1, keepdim=True)  # std: [batch_size, max_len, 1]
        
        # 最后的return的结果实际上在最后的一个维度上发生了广播
        # 具体是在运算self.a_2 * (x - mean) / (std + self.eps),运算乘法是发生的
        return self.a_2 * (x - mean) / (std + self.eps) + self.b_2

Layernorm的具体计算公式:

μ = 1 D ∑ i = 1 D x i \mu=\frac{1}{D}\sum_{i = 1}^{D}x_{i} μ=D1i=1Dxi
σ 2 = 1 D ∑ i = 1 D ( x i − μ ) 2 \sigma^{2}=\frac{1}{D}\sum_{i = 1}^{D}(x_{i}-\mu)^{2} σ2=D1i=1D(xiμ)2

然后进行归一化:
x ^ = x − μ σ 2 + ϵ \hat{x}=\frac{x-\mu}{\sqrt{\sigma^{2}+\epsilon}} x^=σ2+ϵ xμ

其中 ϵ \epsilon ϵ是一个很小的常数,通常取 1 e − 5 1e - 5 1e5 1 e − 6 1e - 6 1e6,用于防止分母为零。

最后进行线性变换:
y = γ x ^ + β y=\gamma\hat{x}+\beta y=γx^+β

其中 γ \gamma γ β \beta β是可学习的参数,分别用于缩放和平移归一化后的数据。

SublayerConnection

不论是encoderlayer中还是decoderlayer中内部都可以在细分子层,每一个子层中都会有一个Layernorm的过程所以我们定义一个SublayerConnection类用于实现子层的操作和Layernorm

在这里插入图片描述

就比如上图将encoder分为两个子层,将decoder分为三个子层。

class SublayerConnection(nn.Module):

    def __init__(self, size, dropout):
        super(SublayerConnection, self).__init__()
        self.norm = LayerNorm(size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, sublayer):
        # return x + self.dropout(sublayer(self.norm(x)))
        # 上面注释的代码是哈佛NLP团队的源码,我也不是很明白为什么要先norm再进行sublayer
        return x + self.dropout(self.norm(sublayer(x)))

encoderlayer

class EncoderLayer(nn.Module):
    "编码器层有两个子层,在第一个子层实现自注意力机制,第二个子层中实现前馈网络"

    def __init__(self, size, self_attn, feed_forward, dropout):
        super(EncoderLayer, self).__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):
        # 第一个sublayer实现自注意力机制,传入已经enbedding和加上位置编码的数据,和自注意		  # 力实现的的函数
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
        
        # 第二个子层实现前馈网络
        return self.sublayer[1](x, self.feed_forward)

Position-wise Feed-Forward Networks

我们的编码器和解码器包含一个完全连接的前馈网络,分别应用于每个位置。这由两个线性变换组成,其之间用ReLu激活函数连接。
F F N ( x ) = max ⁡ ( 0 , x W 1 + b 1 ) W 2 + b 2 \mathrm{FFN}(x)=\max(0, xW_1 + b_1) W_2 + b_2 FFN(x)=max(0,xW1+b1)W2+b2
其输入输出的维度为 d m o d e l = 512 d_{model}=512 dmodel=512,并且层内转换的维度 d f f = 2048 d_{ff}=2048 dff=2048

class PositionwiseFeedForward(nn.Module):
    "Implements FFN equation."

    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(self.w_1(x).relu()))

attention

在encoder自注意力实现机制中,每一条输入的x通过一层linear线性变换得到q,k,v,可见q,k,v三者它们是同源的(在decoder的第二个子层不是如此),我们一次有多个输入,所以我们用矩阵来表达:
A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T d k ) V \mathrm{Attention}(Q, K, V) = \mathrm{softmax}(\frac{QK^T}{\sqrt{d_k}})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)
    # 这里将mask矩阵中值为0的地方设置为一个极小的数,这样在经过softmax后就几乎变为0了
    # 表示当前的数据看不到后面的信息
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)
    # 对最后一个维度即某一个词对自己所在句子中其他词的相似度值做一个softmax得到重要度
    p_attn = scores.softmax(dim=-1)
    if dropout is not None:
        p_attn = dropout(p_attn)
    return torch.matmul(p_attn, value), p_attn

MultiHeadedAttention

对于多头自注意力的解释在这里我有讲解–怎么理解自注意力机制

对于代码的实现我们将每一个头的q,k,v的维度d_k设置为 d k = d m o d e l / n h e a d d_k=d_{model}/n_{head} dk=dmodel/nhead并且同时对多个头进行自注意力运算,最后将多个头得到的结果合并再经过一个线性层得到最终的结果。

在这里插入图片描述

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 where  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 ) \mathrm{MultiHead}(Q, K, V) = \mathrm{Concat}(\mathrm{head_1}, ..., \mathrm{head_h})W^O \\ \text{where}~\mathrm{head_i} = \mathrm{Attention}(QW^Q_i, KW^K_i, VW^V_i) MultiHead(Q,K,V)=Concat(head1,...,headh)WOwhere headi=Attention(QWiQ,KWiK,VWiV)

上面可训练参数的维度: W i Q ∈ R d model × d k W^Q_i \in \mathbb{R}^{d_{\text{model}} \times d_k} WiQRdmodel×dk, W i K ∈ R d model × d k W^K_i \in \mathbb{R}^{d_{\text{model}} \times d_k} WiKRdmodel×dk, W i V ∈ R d model × d v W^V_i \in \mathbb{R}^{d_{\text{model}} \times d_v} WiVRdmodel×dv and W O ∈ R h d v × d model W^O \in \mathbb{R}^{hd_v \times d_{\text{model}}} WORhdv×dmodel.这里的 h d v hd_v hdv表示每个head运算过后得到新的v之后再拼接的维度

class MultiHeadedAttention(nn.Module):
    def __init__(self, h, d_model, dropout=0.1):
        super(MultiHeadedAttention, self).__init__()
        
        # 对输入乡里那个维度//头数若有余数则报错
        assert d_model % h == 0
        # We assume d_v always equals d_k
        self.d_k = d_model // h
        
        # 头数
        self.h = h
        # 构建四个linear层,前三个用于Q,K,V的计算,最后一个用于对每个头拼接后的结果运算
        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:
            # Same mask applied to all h heads.
            # 使用unsqueeze扩展维度,代表多头中的第n头
            mask = mask.unsqueeze(1)
        # 每个头的batch_size
        nbatches = query.size(0)

        # 1) Do all the linear projections in batch from d_model => h x d_k
        # 这里分别对Q,K,V矩阵进行线性变换,三个矩阵的维度都是[nbatches, H, N,                   # d_k],这里的H表示encoder输入数据的某一句的词数即src_len,若是decoder则表示             # tgt_len
        query, key, value = [
            # x线性变换后: [nbatches, N, d_model]
            # 经过view()方法x: [nbatches, N, H, d_k],将原本的d_model切分为H快,表示H             # 个头
            # 再交换1,2两个维度,x: [nbatches, H, N, d_k]
            lin(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
            for lin, x in zip(self.linears, (query, key, value))
        ]

        # 2) Apply attention on all the projected vectors in batch.
        # 对所有的头进行自注意力计算,因为是数据是以矩阵的形势存在,所有头直接并行
        x, self.attn = attention(
            query, key, value, mask=mask, dropout=self.dropout
        )

        # 3) "Concat" using a view and apply a final linear.
        # 将x的维度进行变换,即对所有头的结果拼接
        # x: [nbatches, N, d_model]
        x = (
            x.transpose(1, 2)
            .contiguous()
            .view(nbatches, -1, self.h * self.d_k)
        )
        del query
        del key
        del value
        return self.linears[-1](x)

上面的代码中对于Q,K,V矩阵的生成是比较难理解的,其中涉及了较多的维度上的变化,至于我们为什么要交换1,2两处维度。是因为python的高维矩阵间的运算只看最后两个维度,将头数放在前面就可以实现各个头内部进行运算。可以观察下面的示意图

在这里插入图片描述

多头自注意力在模型中的使用

Transformer 以三种不同的方式使用多头注意力:

  1. 在“编码器-解码器注意力”层中,查询来自 previous decoder 层,键K和值V来自编码器的输出。encoder的输出会流向decoder的每一层decoderlayer。
  2. 编码器包含自我注意层。在自我注意层的所有键、值和查询都是同源的,为encoder中上一层的输出。每个位置可以关注前一个层。
  3. 同样地,解码器中的自注意力层的所有键、值和查询也都是同源的,允许解码器中的每个位置关注到该位置及之前的所有位置。为了保持自回归属性,我们需要阻止解码器中的左向信息流。我们在缩放点积注意力内部通过掩码(设置为 )来实现这一点,即在 softmax 的输入中屏蔽后面本不应得到的信息。

Decoder

在前面已经将Encoder解释清楚,后面的Decoder基本同理

class Decoder(nn.Module):
    "Generic N layer decoder with masking."

    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):
        """
        x: Decoder的输入,[batch_size, tgt_len, d_model]
        memory: Encoder的输出,[batch_size, src_len, d_model]
        src_mask: 对Encoder输入数据的掩码,一般就是对padding部分的掩码
        tgt_mask: 为了防止解码器看到后面的信息需要将后面的信息擦除
        """
        for layer in self.layers:
            x = layer(x, memory, src_mask, tgt_mask)
        return self.norm(x)

decoderlayer

除了每个编码器层中的两个子层之外,解码器插入第三个子层(在decoderlayer的中间),该子层对编码器堆栈的输出执行多头注意。与编码器类似,我们在每个子层周围使用残差连接,然后进行层规范化。

class DecoderLayer(nn.Module):
    "Decoder is made of self-attn, src-attn, and feed forward (defined below)"

    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):
        "Follow Figure 1 (right) for connections."
        m = memory
        # 第一层做解码器输入的自注意力机制
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))
        # 第二层,由编码器提供K,V,做与编码器的自注意力机制
        x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask))
        return self.sublayer[2](x, self.feed_forward)

subsequent_mask

对于decoderlayer的输入在第一层做自注意力时,我们需要对其做一个掩码操作,因为在真正用该模型进行机器翻译时,机器时看不到后来的信息的,所以必须利用mask将后面的信息屏蔽掉

def subsequent_mask(size):
    "Mask out subsequent positions."
    # 这里的size=tgt_len,即decoder输入的一句话的词数
    attn_shape = (1, size, size)
    subsequent_mask = torch.triu(torch.ones(attn_shape), diagonal=1).type(
        torch.uint8
    )
    return subsequent_mask == 0

上述代码运用到了torch.triu()方法,该方法的具体使用可以自己搜索一下。

最终得到的mask效果如下:

print(subsequent_mask(5))

"""
输出:
tensor([[[ True, False, False, False, False],
         [ True,  True, False, False, False],
         [ True,  True,  True, False, False],
         [ True,  True,  True,  True, False],
         [ True,  True,  True,  True,  True]]])
"""

其中False表示将该数据遮掩住。

Generator

class Generator(nn.Module):
    "Define standard linear + softmax generation step."

    def __init__(self, d_model, vocab):
        # 初始化函数的输入参数有两个,d_model代表词嵌入维度,vocab.size代表词表大小
        super(Generator, self).__init__()
        self.proj = nn.Linear(d_model, vocab)

    def forward(self, x):
        # 将预测出来的词向量经过一个linear成转化为词表的长度,表示对词表里词的预测,再通过           # softmax对每个词的概率进行预测
        return log_softmax(self.proj(x), dim=-1)

PositionalEncoding

注意一下transforme的位置编码是绝对位置编码,训练文本的每一句话分词过后的长度(词的个数)都是一样的,在位置编码函数内设置一个最大长度max_len,每句话的长度都不能超过他,所以每句话的每个词都有着其固定的位置编码,并且transformer的位置编码在训练阶段都不会发生改变,是固定的。
P E ( p o s , 2 i ) = sin ⁡ ( p o s / 10000 2 i / d model ) PE_{(pos,2i)} = \sin(pos / 10000^{2i/d_{\text{model}}}) PE(pos,2i)=sin(pos/100002i/dmodel)

P E ( p o s , 2 i + 1 ) = cos ⁡ ( p o s / 10000 2 i / d model ) PE_{(pos,2i+1)} = \cos(pos / 10000^{2i/d_{\text{model}}}) PE(pos,2i+1)=cos(pos/100002i/dmodel)

其中 p o s pos pos是位置, i i i是维度, d m o d e l d_{model} dmodel是词向量的维度,不同的 p o s pos pos会得到不同的正弦和余弦值组合由于正弦和余弦函数的周期性和单调性,在给定的序列长度内,每个位置的编码都是唯一的,不会出现重复。

class PositionalEncoding(nn.Module):
    "Implement the PE function."

    def __init__(self, d_model, dropout, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)

        # Compute the positional encodings once in log space.
        # pe: [max_len, d_model]
        pe = torch.zeros(max_len, d_model)
        # position: [max_len, 1]
        position = torch.arange(0, max_len).unsqueeze(1)
        # div_term: [256]
        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增加一个维度,使其能与三维的x相加,x相较于pe多了一个batch_size维度
        pe = pe.unsqueeze(0)
        self.register_buffer("pe", pe)

    def forward(self, x):
        # pe[:, : x.size(1)]这样写是因为x的一句话的词数并不一定有max_len这么长,所以要将         # pe切割再相加
        x = x + self.pe[:, : x.size(1)].requires_grad_(False)
        return self.dropout(x)

上面的代码就是对位置编码公式的实现,里面有着一些python的运算机制,建议将其拆分一行一行的执行,看看它输出的到底是什么事什么样的形状,才能更好地理解。

Embedding

对于输入文本,在对其分词过后,每一个词就是一个token,可以在词向量表中找到其对应的词向量

class Embeddings(nn.Module):
    def __init__(self, d_model, vocab):
        super(Embeddings, self).__init__()
        # 对于Embedding的输入参数,vocab:需要进行embedding的词数,d_model:每个词的词向         # 量维度
        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)

Mask

在transformer架构中有三处需要进项掩码操作:

  • encoder的第一个子层的自注意力计算中需要对进行过padding操作的句子做掩码,因为padding加入的词使我们认为设置的,是无效信息,自注意力不应该获取它的信息
  • decoder第一个子层的自注意力计算,对于机器翻译工作,解码器的输入应该是先给出一个开头的词,然后预测生成一个词后,在将开头词与刚刚预测出来的词再一起放入decoder的输入中,这样循环往复,所以我们需要对decoder的输入做掩码,让每个词看不到它后面词的信息,即subsequent_mask,这个我们在讲解Decoder已经实现
  • decoder的第二个子层,与encoder的输出做自注意力机制,在本文借鉴的文章(哈佛NLP团队的代码)中,这一层的掩码,将subsequent_mask和get_src_pad_mask进行了叠加,即对encoder的输出做padding掩码,也对decoder的输入做掩码,但我觉得,transformer一开始提出是为了做机器翻译的工作,Encoder的输出是固定的全局信息,无需遮挡每个词的后继信息,所以应该只需要加padding掩码。

get_src_pad_mask

def get_attn_pad_mask(seq_q, seq_k, pad):
    '''
    seq_q: [batch_size, seq_len]
    seq_k: [batch_size, seq_len]
    seq_len could be src_len or it could be tgt_len
    seq_len in seq_q and seq_len in seq_k maybe not equal
    '''
    batch_size, len_q = seq_q.size()
    batch_size, len_k = seq_k.size()
    # eq(zero) is PAD token
    pad_attn_mask = (seq_k.data != pad).unsqueeze(-2)  # [batch_size, 1, len_k], True is masked
    return pad_attn_mask.expand(batch_size, len_q, len_k)  # [batch_size, len_q, len_k]

不论是Encoder还是Decoder都可能需要对输入做一个padding的掩码,如果是在 Decoder 中调用的,seq_len就有可能等于src_len,也有可能等于tgt_len(因为 Decoder 有两次 mask)。

上面代码的核心是pad_attn_mask = (seq_k != pad).unsqueeze(-2) 即将seq_k.data里的数据中如果等于pad则替换为False,若不为pad则替换为True。

补充:上面代码之所给了两个seq输入seq_qseq_k,是因为decoder第二个子层Q,K分别来自解码器和编码器,二者的序列长度可能是不同的。

make_std_mask

def make_std_mask(tgt, pad):
    "Create a mask to hide padding and future words."
    # tgt输入同上src
    tgt_mask = (tgt != pad).unsqueeze(-2)
    tgt_mask = tgt_mask & subsequent_mask(tgt.size(-1)).type_as(
        tgt_mask.data
    )
    return tgt_mask

tgt_mask = (tgt != pad).unsqueeze(-2)这句代码中,将tgt中不等于pad的设置为True,等于pad这只为False,然后再与subsequent_mask做一个‘与’操作,就得到了既对padding也对后置信息做掩码的mask矩阵。

在哈佛团队写的源码中,他讲get_attn_pad_mask和make_std_mask放在了一个类中,因为不论是encoder还是,decoder的输入数据都可能要做掩码操作

class Batch:
    """Object for holding a batch of data with mask during training."""

    def __init__(self, src, tgt=None, pad=2):  # 2 = <blank>
        self.src = src
        # 这里对src的输入应该是形如[batch_size, seq_len]
        # 其中batch_size表示有多少个句子,sep_len表示一个句子里的token数(这里的token已         # 经经过了padding操作)
        self.src_mask = (src != pad).unsqueeze(-2)
        if tgt is not None:
            self.tgt = tgt[:, :-1]
            # 这里的tgt_y将第一个词去除,因为一般encoder和decoder输入句子首部都是s表示句			 # 子的开始,末尾是e表示句子结束
            # 所以tgt_y就表示着要往后预测的词有哪些
            self.tgt_y = tgt[:, 1:]
            self.tgt_mask = self.make_std_mask(self.tgt, pad)
            # ntokens: 还要往后预测多少个词
            self.ntokens = (self.tgt_y != pad).data.sum()

    @staticmethod
    def make_std_mask(tgt, pad):
        "Create a mask to hide padding and future words."
        # tgt输入同上src
        tgt_mask = (tgt != pad).unsqueeze(-2)
        tgt_mask = tgt_mask & subsequent_mask(tgt.size(-1)).type_as(
            tgt_mask.data
        )
        return tgt_mask

测试

下面我们对上面的代码进行一个简单的测试,encoder的输入词为src = torch.LongTensor([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]])这里面的一个数字表示的是一个token,就是一个词,将其大小输入给Embedding得到10个词向量。然后再通过transformer预测出10个新的数字。

  1. 定义自己的model

    def make_model(
        src_vocab, tgt_vocab, N=6, d_model=512, d_ff=2048, h=8, dropout=0.1
    ):
        "Helper: Construct a model from hyperparameters."
        c = copy.deepcopy
        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),
        )
    
        # This was important from their code.
        # Initialize parameters with Glorot / fan_avg.
        for p in model.parameters():
            if p.dim() > 1:
                # 对model里的参数进行一个初始化
                nn.init.xavier_uniform_(p)
        return model
    
  2. 调用model预测

    def inference_test():
        # 我们的encoder维度应为10,预测的结果也是从[1, 10]中选择,这里的src_vocab,           # tgt_vocab之所以是11,因为encoder和decoder的输入要有一个句子开头s,
        # 真正在训练师应该是12维了,因为不仅有开头s,还有结尾e
        test_model = make_model(11, 11, 2)
        test_model.eval()
        src = torch.LongTensor([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]])
        # 这里src_mask全为1,表示src无需进行padding操作
        src_mask = torch.ones(1, 1, 10)
    
        memory = test_model.encode(src, src_mask)
        # ys表示预测结果,一开始给出一个数字(这个数字也是要经过embedding的),表示句子的开始s
        ys = torch.zeros(1, 1).type_as(src)
        print('memory', ':', memory)
        print(memory.shape)
    
        # 预测出10个数字
        for i in range(9):
            out = test_model.decode(
                memory, src_mask, ys, subsequent_mask(ys.size(1)).type_as(src.data)
            )
            # print(i, ':', out)
            prob = test_model.generator(out[:, -1])
            # print(i, ':', prob)
            _, next_word = torch.max(prob, dim=1)
            next_word = next_word.data[0]
            # print(i, ':', next_word)
            ys = torch.cat(
                [ys, torch.empty(1, 1).type_as(src.data).fill_(next_word)], dim=1
            )
            # print(i, ':', ys)
    
        print("Example Untrained Model Prediction:", ys)
    
    # 进行10轮预测
    def run_tests():
        for _ in range(10):
            inference_test()
    
    
    run_tests()
    

    输出:

    Example Untrained Model Prediction: tensor([[0, 5, 9, 2, 5, 2, 5, 2, 5, 2]])
    Example Untrained Model Prediction: tensor([[0, 4, 4, 4, 4, 4, 4, 4, 4, 4]])
    Example Untrained Model Prediction: tensor([[0, 9, 9, 9, 6, 2, 0, 0, 8, 0]])
    Example Untrained Model Prediction: tensor([[0, 4, 7, 0, 4, 7, 0, 4, 4, 4]])
    Example Untrained Model Prediction: tensor([[0, 8, 3, 8, 3, 8, 3, 8, 2, 9]])
    Example Untrained Model Prediction: tensor([[0, 8, 8, 8, 0, 6, 0, 0, 6, 6]])
    Example Untrained Model Prediction: tensor([[0, 9, 7, 9, 9, 1, 9, 1, 9, 1]])
    Example Untrained Model Prediction: tensor([[0, 3, 3, 3, 3, 3, 3, 3, 3, 3]])
    Example Untrained Model Prediction: tensor([[0, 3, 5, 3, 3, 3, 1, 3, 1, 3]])
    Example Untrained Model Prediction: tensor([[0, 0, 0, 2, 0, 2, 0, 2, 0, 2]])
    

参考

  1. 哈佛NLP团队transformer代码
  2. https://wmathor.com/index.php/archives/1455/
  3. https://zhuanlan.zhihu.com/p/410878403
### Transformer模型详解及其Python源码实现 #### 1. Transformer模型概述 Transformer 是一种基于注意力机制的神经网络架构,广泛应用于自然语言处理领域。其核心组件包括编码器(Encoder)和解码器(Decoder)。两者均由多个相同结构的层堆叠构成,每一层主要依赖于 **自注意力机制 (Self-Attention)** 和 **前馈全连接网络 (Feed Forward Network)** 来完成特征提取与转换[^1]。 #### 2. 编码器(Encoder) 编码器负责接收输入序列并将其转化为高维表示向量。具体来说,它由以下几个模块组成: - **多头自注意力机制 (Multi-head Self-Attention):** 自注意力机制允许模型在同一时间关注输入序列的不同位置,从而捕获全局上下文关系。多头设计则进一步增强了模型捕捉不同子空间特征的能力。 - **前馈全连接网络 (Position-wise Feed-Forward Networks):** 这是一个简单的两层线性变换加激活函数组成的网络,用于对每个位置上的隐藏状态进行独立映射。 - **残差连接与归一化 (Residual Connection & Layer Normalization):** 每一层之后都会加入残差连接以及层归一化操作,这有助于缓解梯度消失问题并加速收敛过程。 以下是单个编码器层的部分 PyTorch 实现代码示例: ```python import torch.nn as nn class EncoderLayer(nn.Module): def __init__(self, d_model, num_heads, d_ff, dropout=0.1): super(EncoderLayer, self).__init__() # 多头自注意力机制 self.self_attn = MultiHeadAttention(d_model, num_heads) # 前馈全连接网络 self.feed_forward = PositionWiseFeedForward(d_model, d_ff, dropout) # 层归一化 self.norm1 = nn.LayerNorm(d_model) self.norm2 = nn.LayerNorm(d_model) # Dropout self.dropout = nn.Dropout(dropout) def forward(self, x, mask=None): attn_output = self.self_attn(x, x, x, mask) # 使用自注意力机制计算输出 out1 = self.norm1(x + self.dropout(attn_output)) # 添加残差连接及归一化 ff_output = self.feed_forward(out1) # 计算前馈网络输出 output = self.norm2(out1 + self.dropout(ff_output)) # 再次添加残差连接及归一化 return output ``` #### 3. 解码器(Decoder) 解码器的任务是从编码器传递过来的信息生成目标序列。它的基本单元同样包含上述提到的所有部分外还增加了一个额外的关键步骤——掩蔽未来信息的注意力建模。 - **遮挡后的多头自注意力机制 (Masked Multi-head Attention):** 防止当前位置看到后续单词的内容,确保预测时只利用已知的历史数据。 - **跨编码器-解码器注意力机制 (Enc-Dec Attention):** 利用来自编码器的最后一层输出作为键值对来增强当前时刻的理解能力。 下面是简化版的一个解码器层定义片段: ```python class DecoderLayer(nn.Module): def __init__(self, d_model, num_heads, d_ff, dropout=0.1): super(DecoderLayer, self).__init__() # Masked multi-head attention self.masked_self_attn = MultiHeadAttention(d_model, num_heads) # Enc-Dec attention self.enc_dec_attn = MultiHeadAttention(d_model, num_heads) # Feed-forward network self.ffn = PositionWiseFeedForward(d_model, d_ff, dropout) # Norm layers self.norm1 = nn.LayerNorm(d_model) self.norm2 = nn.LayerNorm(d_model) self.norm3 = nn.LayerNorm(d_model) # Dropout layer self.dropout = nn.Dropout(dropout) def forward(self, tgt_seq, enc_output, src_mask=None, tgt_mask=None): masked_attn_out = self.masked_self_attn(tgt_seq, tgt_seq, tgt_seq, tgt_mask) norm_tgt = self.norm1(tgt_seq + self.dropout(masked_attn_out)) enc_dec_attn_out = self.enc_dec_attn(norm_tgt, enc_output, enc_output, src_mask) norm_enc_dec = self.norm2(norm_tgt + self.dropout(enc_dec_attn_out)) ffn_out = self.ffn(norm_enc_dec) final_out = self.norm3(norm_enc_dec + self.dropout(ffn_out)) return final_out ``` #### 4. 整体流程总结 整个 Transformer 的工作可以分为三个阶段:嵌入、编码与解码。其中,在训练过程中,编码器接受原始句子作为输入,并将结果传送给解码器;而测试期间,则需逐词生成直至达到终止条件为止[^2]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值