Transformer模型简要分析(中篇)(代码来源D2l)

Transformer模型简要分析(中篇)(代码来源D2l)

电脑开机的时间达到了6s以内,很开心,所以今天继续更新下一篇
代码运行环境:jupyter notebook

之前提到了transformer的注意力机制基本的构件,本篇文章将剩下的注意力评分函数、多头注意力机制等内容以代码概念结合的方式简单说明,在最后,给出了transformer在文本领域的一个简单应用。

上一篇:Transformer模型简要分析(上篇)(代码来源D2l)
下一篇:transformer模型简要分析(下篇)(代码来源D2l)

5.注意力评分函数

前文简要说明了一个注意力机制模型,现在前文基础上给出应用注意力评分函数的模型构建如下:
图片来源:https://zh-v2.d2l.ai/chapter_attention-mechanisms/attention-scoring-functions.html
在这里插入图片描述
回忆上一篇提到的高斯核:
f ( x ) = ∑ i = 1 n α ( x , x i ) y i = ∑ i = 1 n exp ⁡ ( − 1 2 ( x − x i ) 2 ) ∑ j = 1 n exp ⁡ ( − 1 2 ( x − x j ) 2 ) y i = ∑ i = 1 n s o f t m a x ( − 1 2 ( x − x i ) 2 ) y i . \begin{aligned} f(x) &=\sum_{i=1}^n \alpha(x, x_i) y_i\\ &= \sum_{i=1}^n \frac{\exp\left(-\frac{1}{2}(x - x_i)^2\right)}{\sum_{j=1}^n \exp\left(-\frac{1}{2}(x - x_j)^2\right)} y_i \\&= \sum_{i=1}^n \mathrm{softmax}\left(-\frac{1}{2}(x - x_i)^2\right) y_i. \end{aligned} f(x)=i=1nα(x,xi)yi=i=1nj=1nexp(21(xxj)2)exp(21(xxi)2)yi=i=1nsoftmax(21(xxi)2)yi.
其中的指数部分可以看作是对查询x与相应的键作运算,然后得到softmax,此处的α(x,xi)=exp(-1/2(x-xi)^2),其中该部分被命名为注意力评分函数(attention scoring function), 简称评分函数(scoring function)。
用数学语言描述,假设有一个查询 q ∈ R q \mathbf{q} \in \mathbb{R}^q qRq m m m个“键-值”对 ( k 1 , v 1 ) , … , ( k m , v m ) (\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}_m, \mathbf{v}_m) (k1,v1),,(km,vm)
其中 k i ∈ R k \mathbf{k}_i \in \mathbb{R}^k kiRk v i ∈ R v \mathbf{v}_i \in \mathbb{R}^v viRv
注意力汇聚函数 f f f就被表示成值的加权和:
f ( q , ( k 1 , v 1 ) , … , ( k m , v m ) ) = ∑ i = 1 m α ( q , k i ) v i ∈ R v , f(\mathbf{q}, (\mathbf{k}_1, \mathbf{v}_1), \ldots, (\mathbf{k}_m, \mathbf{v}_m)) = \sum_{i=1}^m \alpha(\mathbf{q}, \mathbf{k}_i) \mathbf{v}_i \in \mathbb{R}^v, f(q,(k1,v1),,(km,vm))=i=1mα(q,ki)viRv,
(内容源至D2l)
注意此处的键值对维数并不相同,一个为k另一个为v

选择不同的注意力评分函数 𝑎会导致不同的注意力汇聚操作。

5.1掩蔽softmax操作

考虑一个情况:在对序列向量化的过程中,因为每个小批量的样本序列长度不一致,例如"今天的电脑开机真快!"和语句"今天的电脑宕机咯"其中的字符长度分别是10,8,故我们需要对后者进行空白填充,以便达到序列上的一致,以便于模型计算等需要。同理在处理键值对的过程中,在某些情况下,并非所有的值都应该被纳入到注意力汇聚中,某些文本序列被填充了没有意义的特殊词元
为了仅将有意义的词元作为值来获取注意力汇聚, 可以指定一个有效序列长度(即词元的个数), 以便在计算softmax时过滤掉超出指定范围的位置。
实现代码如下:

#@save
def masked_softmax(X, valid_lens):
    """通过在最后一个轴上掩蔽元素来执行softmax操作"""
    # X:3D张量,valid_lens:1D或2D张量
    if valid_lens is None:
    	#若不需要掩蔽操作,则dim=-1 表示在最后一个维度上进行 softmax 操作
        return nn.functional.softmax(X, dim=-1)
    else:
    	#保存X的维度以用于后续的恢复
        shape = X.shape
        #如果当前给定的valid_lens维度为1,则扩展该维度,例如:[2,3]-->[2,2,3,3]其中扩展的重数为X的第二维,如X.shape=(2,3,4)则取3重
        if valid_lens.dim() == 1:
            valid_lens = torch.repeat_interleave(valid_lens, shape[1])
        #如果给定的valid_lens为二维,则降维成一维,以达到直接对应行数,例如[[2,3],[4,5]]-->[2,3,4,5]
        else:
            valid_lens = valid_lens.reshape(-1)
        # 最后一轴上被掩蔽的元素使用一个非常大的负值替换,从而其softmax输出为0
        #将三维张量X降维成二维,其中保持最后一维不变,即X.shape=(3,4,5)-->(3*4,5)=(12,5)
        X = d2l.sequence_mask(X.reshape(-1, shape[-1]), valid_lens,
                              value=-1e6)
		#重新构建X恢复之前的样子
        return nn.functional.softmax(X.reshape(shape), dim=-1)
        
def sequence_mask(X, valid_len, value=0):
    """Mask irrelevant entries in sequences.
    Defined in :numref:`sec_seq2seq_decoder`"""
    #获取降维后的X的第二维(即原先的最后一维)
    maxlen = X.size(1)
    
    #掩码生成:原理是根据位置索引来生成掩码矩阵
    #如torch.arange((maxlen), dtype=torch.float32,device=X.device)用于生成X的第三维的长度序列
    #如降维X从(3,4,5)-->到(12,5)其中X.size(1)=5,则torch.arange((maxlen))=[0,1,2,3,4]
    #然后[None, :]操作表示将[0,1,2,3,4]维数扩展成[[0,1,2,3,4]]
    #同理valid_len[:, None],设valid_len=[2,3,3,3]
    #则valid_len[:, None]=[[2],[3],[3],[3]]然后利用广播机制得到例如[[0,1,2,3],[0,1,2,3],...]与[[2,2,2,2],[3,3,3,3]...]
    #最后使用小于来描述它们的位置索引大小关系,进而得到了掩码的位置索引关系bool矩阵,最后将这个mask与降维后的X运算即可
    mask = torch.arange((maxlen), dtype=torch.float32,
                        device=X.device)[None, :] < valid_len[:, None]
    X[~mask] = value
    return X
masked_softmax(torch.rand(2, 2, 4), torch.tensor([2, 3]))
masked_softmax(torch.rand(2, 2, 4), torch.tensor([[3, 3], [2, 4]]))

tensor(
[ [ [0.5183, 0.4817, 0.0000, 0.0000],
[0.4846, 0.5154, 0.0000, 0.0000]],
[ [0.4118, 0.2715, 0.3167, 0.0000],
[0.2634, 0.3763, 0.3603, 0.0000] ] ] )

tensor([[[0.2058, 0.4440, 0.3501, 0.0000],
[0.3152, 0.4114, 0.2733, 0.0000]],
[[0.5336, 0.4664, 0.0000, 0.0000],
[0.1673, 0.1868, 0.3643, 0.2816]]])

两个2×4矩阵表示的样本, 这两个样本的有效长度分别为 2和 3。经过掩蔽softmax操作,超出有效长度的值都被掩蔽为0。可见该掩蔽操作是在三维张量的最后一个轴上进行的。

5.2加性注意力

当查询和键是不同长度的矢量时,可以使用加性注意力作为评分函数。给定查询 q ∈ R q \mathbf{q} \in \mathbb{R}^q qRq和键 k ∈ R k \mathbf{k} \in \mathbb{R}^k kRk加性注意力(additive attention)的评分函数为

a ( q , k ) = w v ⊤ tanh ( W q q + W k k ) ∈ R , a(\mathbf q, \mathbf k) = \mathbf w_v^\top \text{tanh}(\mathbf W_q\mathbf q + \mathbf W_k \mathbf k) \in \mathbb{R}, a(q,k)=wvtanh(Wqq+Wkk)R,

其中可学习的参数是 W q ∈ R h × q \mathbf W_q\in\mathbb R^{h\times q} WqRh×q W k ∈ R h × k \mathbf W_k\in\mathbb R^{h\times k} WkRh×k w v ∈ R h \mathbf w_v\in\mathbb R^{h} wvRh
将查询和键连结起来后输入到一个多层感知机(MLP)中,感知机包含一个隐藏层,其隐藏单元数是一个超参数 h h h。通过使用 tanh ⁡ \tanh tanh作为激活函数,并且禁用偏置项。(引用源至D2l)
模型代码如下:

#@save
class AdditiveAttention(nn.Module):
    """加性注意力"""
    def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs):
        super(AdditiveAttention, self).__init__(**kwargs)
        #采用MLP学习参数来学习
        self.W_k = nn.Linear(key_size, num_hiddens, bias=False)
        self.W_q = nn.Linear(query_size, num_hiddens, bias=False)
        self.w_v = nn.Linear(num_hiddens, 1, bias=False)
        self.dropout = nn.Dropout(dropout)

    def forward(self, queries, keys, values, valid_lens):
        queries, keys = self.W_q(queries), self.W_k(keys)
        # 在维度扩展后,
        # queries的形状:(batch_size,查询的个数,1,num_hidden)
        # key的形状:(batch_size,1,“键-值”对的个数,num_hiddens)
        # 使用广播方式进行求和
        features = queries.unsqueeze(2) + keys.unsqueeze(1)
        #将输入的特征矩阵映射到-1到1范围内
        features = torch.tanh(features)
        #self.w_v(features)满足:(batch_size,查询的个数,“键-值”对的个数,num_hiddens)*(num_hiddens,1)=
        #(batch_size,查询的个数,“键-值”对的个数,1)
        # self.w_v仅有一个输出,因此从形状中移除最后那个维度。
        # scores的形状:(batch_size,查询的个数,“键-值”对的个数)
        scores = self.w_v(features).squeeze(-1)
        #将得分掩蔽一些无效的序列,不改变scores形状
        self.attention_weights = masked_softmax(scores, valid_lens)
        # 最后将权重与values相乘相加,且values的形状:(batch_size,“键-值”对的个数,值的维度)
        #返回一个(batch_size,查询的个数,值的维度)作为每个批量里面的多个查询得到的对应输出值
        return torch.bmm(self.dropout(self.attention_weights), values)

举个例子:

其中查询、键和值的形状为(批量大小,步数或词元序列长度,特征大小), 实际输出为 (2,1,20) 、 (2,10,2)和 (2,10,4) 。注意力汇聚输出的形状为(批量大小,查询的步数,值的维度)。

queries, keys = torch.normal(0, 1, (2, 1, 20)), torch.ones((2, 10, 2))
# values的小批量,两个值矩阵是相同的
values = torch.arange(40, dtype=torch.float32).reshape(1, 10, 4).repeat(2, 1, 1)
valid_lens = torch.tensor([3, 6])
attention = AdditiveAttention(key_size=2, query_size=20, num_hiddens=8,dropout=0.1)
attention.eval()
attention(queries, keys, values, valid_lens)

tensor([3, 6])
tensor([[[ 4.0000, 5.0000, 6.0000, 7.0000]],
[[10.0000, 11.0000, 12.0000, 13.0000]]], grad_fn=<BmmBackward0>)

现在来观察一下注意力矩阵:

#注意此处的注意力矩阵的形状是(batch_size,查询的个数,“键-值”对的个数),表示每个批量里面的对应查询与键值对的关系
d2l.show_heatmaps(attention.attention_weights.reshape((1, 1, 2, 10)),xlabel='Keys', ylabel='Queries')

在这里插入图片描述

因为使用的每个键的值相同,故不同的查询与所有键之间的注意力权重关系相同,其中表现为有效长度分别为[3,6]故有效的查询显示长度也是3和6

5.3缩放点积注意力

回忆上一篇中提到的矩阵乘法等操作,使用点积可以得到效率更好的评分函数,但点积需要查询和键的长度相同,此处设为d。假设查询和键的所有元素都是独立的随机变量, 并且都满足零均值和单位方差, 那么两个向量的点积的均值为 0,方差为 𝑑。 为确保无论向量长度如何, 点积的方差在不考虑向量长度的情况下仍然是 1, 我们再将点积除以 √d, 则缩放点积注意力(scaled dot-product attention)评分函数为:
a ( q , k ) = q ⊤ k / d . a(\mathbf q, \mathbf k) = \mathbf{q}^\top \mathbf{k} /\sqrt{d}. a(q,k)=qk/d .
在实践中,我们通常从小批量的角度来考虑提高效率,例如基于 n n n个查询和 m m m个键-值对计算注意力,其中查询和键的长度为 d d d,值的长度为 v v v。查询 Q ∈ R n × d \mathbf Q\in\mathbb R^{n\times d} QRn×d、键 K ∈ R m × d \mathbf K\in\mathbb R^{m\times d} KRm×d和值 V ∈ R m × v \mathbf V\in\mathbb R^{m\times v} VRm×v的缩放点积注意力是:
s o f t m a x ( Q K ⊤ d ) V ∈ R n × v . \mathrm{softmax}\left(\frac{\mathbf Q \mathbf K^\top }{\sqrt{d}}\right) \mathbf V \in \mathbb{R}^{n\times v}. softmax(d QK)VRn×v.(引用来源D2L)
模型代码如下:

#寒秋夜未央
#@save
class DotProductAttention(nn.Module):
    """缩放点积注意力"""
    def __init__(self, dropout, **kwargs):
        super(DotProductAttention, self).__init__(**kwargs)
        self.dropout = nn.Dropout(dropout)
    # queries的形状:(batch_size,查询的个数,d)
    # keys的形状:(batch_size,“键-值”对的个数,d)
    # values的形状:(batch_size,“键-值”对的个数,值的维度)
    # valid_lens的形状:(batch_size,)或者(batch_size,查询的个数)
    def forward(self, queries, keys, values, valid_lens=None):
        d = queries.shape[-1]
        # 设置transpose_b=True为了交换keys的最后两个维度
        scores = torch.bmm(queries, keys.transpose(1,2)) / math.sqrt(d)
        self.attention_weights = masked_softmax(scores, valid_lens)
        return torch.bmm(self.dropout(self.attention_weights), values)

举个例子:

queries = torch.normal(0, 1, (2, 1, 2))
attention = DotProductAttention(dropout=0.5)
attention.eval()
attention(queries, keys, values, valid_lens)

tensor([3, 6])
tensor([[[ 4.0000, 5.0000, 6.0000, 7.0000]],
[[10.0000, 11.0000, 12.0000, 13.0000]]])

同理注意力权重热值图如下所示:

d2l.show_heatmaps(attention.attention_weights.reshape((1, 1, 2, 10)),xlabel='Keys', ylabel='Queries')

在这里插入图片描述

6.Bahdanau 注意力

有过机器翻译问题经验的朋友应该还记得:通过设计一个基于两个循环神经网络的编码器-解码器架构, 用于序列到序列学习。
循环神经网络编码器将长度可变的序列转换为固定形状的上下文变量, 然后循环神经网络解码器根据生成的词元和上下文变量 按词元生成输出(目标)序列词元。(针对此部分若有不清楚的朋友,请参考D2L序列到序列学习(seq2seq)
基于seq2seq的模型存在的一个问题是:并非所有输入(源)词元都对解码某个词元都有用, 在每个解码步骤中仍使用编码相同的上下文变量。

我们试着从 (Graves, 2013)中找到灵感: 在为给定文本序列生成手写的挑战中, Graves设计了一种可微注意力模型, 将文本字符与更长的笔迹对齐, 其中对齐方式仅向一个方向移动。 受学习对齐想法的启发,Bahdanau等人提出了一个没有严格单向对齐限制的 可微注意力模型 (Bahdanau et al., 2014)。 在预测词元时,如果不是所有输入词元都相关,模型将仅对齐(或参与)输入序列中与当前预测相关的部分。这是通过将上下文变量视为注意力集中的输出来实现的。( 源至D2L)

6.1模型

c t ′ = ∑ t = 1 T α ( s t ′ − 1 , h t ) h t , \mathbf{c}_{t'} = \sum_{t=1}^T \alpha(\mathbf{s}_{t' - 1}, \mathbf{h}_t) \mathbf{h}_t, ct=t=1Tα(st1,ht)ht,
上下文变量 𝐜在任何解码时间步 𝑡′都会被 𝐜𝑡′替换。 假设输入序列中有 𝑇个词元, 解码时间步 𝑡′的上下文变量是注意力集中的输出,时间步 𝑡′−1时的解码器隐状态 𝐬𝑡′−1是查询, 编码器隐状态 𝐡𝑡既是键,也是值, 注意力权重 𝛼是使用定义的加性注意力打分函数计算的。模型结构如下:(针对此部分若有不清楚的朋友,请参考D2L序列到序列学习(seq2seq)
图片来源:https://zh-v2.d2l.ai/chapter_attention-mechanisms/bahdanau-attention.html
在这里插入图片描述

6.2定义注意力解码器

首先给出模型的接口如下:

#@save
class AttentionDecoder(d2l.Decoder):
    """带有注意力机制解码器的基本接口"""
    def __init__(self, **kwargs):
        super(AttentionDecoder, self).__init__(**kwargs)
    @property
    def attention_weights(self):
        raise NotImplementedError

初始化解码器的状态需要的输入有:

  • 编码器在所有时间步的最终层隐状态,将作为注意力的键和值;
  • 上一时间步的编码器全层隐状态,将作为初始化解码器的隐状态;
  • 编码器有效长度(排除在注意力池中填充词元)。

在每个解码时间步骤中,解码器上一个时间步的最终层隐状态将用作查询。 因此,注意力输出和输入嵌入都连结为循环神经网络解码器的输入。
模型代码如下:

class Seq2SeqAttentionDecoder(AttentionDecoder):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 dropout=0, **kwargs):
        super(Seq2SeqAttentionDecoder, self).__init__(**kwargs)
        #上一篇提到的注意力机制
        self.attention = d2l.AdditiveAttention(
            num_hiddens, num_hiddens, num_hiddens, dropout)
        #嵌入层,便于将特征重构
        self.embedding = nn.Embedding(vocab_size, embed_size)
        #循环神经网络层,(针对此部分若有不清楚的朋友,请参考[D2L序列到序列学习(seq2seq)](https://zh-v2.d2l.ai/chapter_recurrent-modern/seq2seq.html))
        self.rnn = nn.GRU(
            embed_size + num_hiddens, num_hiddens, num_layers,
            dropout=dropout)
        #稠密层,用于特征组合表示并输出
        self.dense = nn.Linear(num_hiddens, vocab_size)
    #解码器的状态初始化函数
    def init_state(self, enc_outputs, enc_valid_lens, *args):
        #此处的outputs来源于编码器循环神经网络的隐藏层输出,且没有连接稠密层
        # outputs的形状为(batch_size,num_steps,num_hiddens).
        # hidden_state的形状为(num_layers,batch_size,num_hiddens)
        outputs, hidden_state = enc_outputs
        #permute转换编码器将X编码后的输出形状:(`num_steps`, `batch_size`, `num_hiddens`)-->(`batch_size`,`num_steps`, `num_hiddens`)
        return (outputs.permute(1, 0, 2), hidden_state, enc_valid_lens)
    def forward(self, X, state):
        # enc_outputs的形状为(batch_size,num_steps,num_hiddens).
        # hidden_state的形状为(num_layers,batch_size,num_hiddens)
        enc_outputs, hidden_state, enc_valid_lens = state
        # 输出X的形状为(num_steps,batch_size,embed_size)
        X = self.embedding(X).permute(1, 0, 2)
        outputs, self._attention_weights = [], []
        #针对每一个解码器的输入Y(参照seq2seq的解码器输入),进行遍历(此处用X表示了)
        for x in X:
        	#注意此处query按照定义其实是来源于编码器的隐藏状态,此处使用hidden_state[-1]来去除编码器的第一层隐藏层的状态结果
        	#因为它已经包含在了第二层隐藏层的内容里面,所以相较于(num_layers,batch_size,num_hiddens),最后的结果
            #query的形状为(batch_size,1,num_hiddens)观察发现少了一层隐藏层的输出,其中代码的含义是:
            #torch.unsqueeze()是PyTorch中的函数,用于在指定维度上增加一个维度。它可以在张量的特定维度上插入一个大小为1的新维度。
            query = torch.unsqueeze(hidden_state[-1], dim=1)
            #加入注意力机制,说明一下各参数的定义
            '''
            query:是我们提到的用于注意力机制查询的编码器隐藏状态输出,形状为(batch_size,1,num_hiddens),后续被用来作为解码器初始状态
            enc_outputs:是我们提到的编码器的X的隐藏状态输出,形状为(batch_size,num_steps,num_hiddens)
            enc_valid_lens:是有效的序列长度,在测试的样例中为None
            '''
            # context的形状为(batch_size,1,num_hiddens),使用作为解码器的输出状态的query来与编码器隐藏状态键值对进行查询
            context = self.attention(query, enc_outputs, enc_outputs, enc_valid_lens)
            # 在特征维度上将上下文信息和解码器输入进行连结得到综合的输入信息
            #(针对此部分若有不清楚的朋友,请参考[D2L序列到序列学习(seq2seq)解码器](https://zh-v2.d2l.ai/chapter_recurrent-modern/seq2seq.html))
            x = torch.cat((context, torch.unsqueeze(x, dim=1)), dim=-1)
            #将x变形为(1,batch_size,embed_size+num_hiddens),hidden_state是编码器的隐藏层状态输出,得到循环神经网络的输出
            #测试代码中out.shape = [1, 4, 16]
            #注意:此处的out没有连入dense层,仅仅是rnn隐藏层的输出
            out, hidden_state = self.rnn(x.permute(1, 0, 2), hidden_state)
            outputs.append(out)
            #保存当前解码器隐状态与编码器所有时间步的隐状态键值对的权重关系
            self._attention_weights.append(self.attention.attention_weights)
        # outputs列表保存了所有的查询解码器隐藏层的输出隐藏状态(其列表元素个数为时间步的长度,测试代码中长度为7个时间步)
        # 将outputs列表的元素按第一维连接后,使用全连接层变换,其中测试代码torch.cat(outputs, dim=0)的形状为[7, 4, 16]
        # 全连接层变换后,outputs的形状为(num_steps,batch_size,vocab_size)
        outputs = self.dense(torch.cat(outputs, dim=0))
        return outputs.permute(1, 0, 2), [enc_outputs, hidden_state,enc_valid_lens]
    @property
    def attention_weights(self):
        return self._attention_weights

补充一下Seq2SeqEncoder:

class Seq2SeqEncoder(d2l.Encoder):
    """The RNN encoder for sequence to sequence learning.
    Defined in :numref:`sec_seq2seq`"""
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 dropout=0, **kwargs):
        super(Seq2SeqEncoder, self).__init__(**kwargs)
        # Embedding layer
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.rnn = nn.GRU(embed_size, num_hiddens, num_layers,
                          dropout=dropout)
    def forward(self, X, *args):
        # The output `X` shape: (`batch_size`, `num_steps`, `embed_size`)
        X = self.embedding(X)
        # In RNN models, the first axis corresponds to time steps
        X = X.permute(1, 0, 2)
        # When state is not mentioned, it defaults to zeros
        output, state = self.rnn(X)
        # `output` shape: (`num_steps`, `batch_size`, `num_hiddens`)
        # `state` shape: (`num_layers`, `batch_size`, `num_hiddens`)
        return output, state

测试模型是否正确:

#初始化一个编码器,运用了循环神经网络层,(针对此部分若有不清楚的朋友,请参考[D2L序列到序列学习(seq2seq)](https://zh-v2.d2l.ai/chapter_recurrent-modern/seq2seq.html))
encoder = d2l.Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16,num_layers=2)
encoder.eval()
#使用之前定义的解码器
decoder = Seq2SeqAttentionDecoder(vocab_size=10, embed_size=8, num_hiddens=16,num_layers=2)
decoder.eval()
#构建一个测试数据集合
X = torch.zeros((4, 7), dtype=torch.long)  # (batch_size,num_steps)
#将编码器的输出用于初始化解码器状态,参数None表示不需要定义有效长度
#state的内容有:(outputs.permute(1, 0, 2), hidden_state, enc_valid_lens)
#其中:
#outputs.permute(1, 0, 2) = (`batch_size`,`num_steps`, `num_hiddens`)
#hidden_state = (`num_layers`, `batch_size`, `num_hiddens`)
#enc_valid_lens = None
state = decoder.init_state(encoder(X), None)
#测试解码器的输出
output, state = decoder(X, state)
output.shape, len(state), state[0].shape, len(state[1]), state[1][0].shape
print(decoder._attention_weights)

[tensor([[[0.1390, 0.1395, 0.1411, 0.1430, 0.1446, 0.1459, 0.1468]],
[[0.1390, 0.1395, 0.1411, 0.1430, 0.1446, 0.1459, 0.1468]],
[[0.1390, 0.1395, 0.1411, 0.1430, 0.1446, 0.1459, 0.1468]],
[[0.1390, 0.1395, 0.1411, 0.1430, 0.1446, 0.1459, 0.1468]]],
grad_fn=), tensor([[[0.1392, 0.1395, 0.1411, 0.1429, 0.1446, 0.1459, 0.1468]],
[[0.1392, 0.1395, 0.1411, 0.1429, 0.1446, 0.1459, 0.1468]],
[[0.1392, 0.1395, 0.1411, 0.1429, 0.1446, 0.1459, 0.1468]],
[[0.1392, 0.1395, 0.1411, 0.1429, 0.1446, 0.1459, 0.1468]]],
grad_fn=), tensor([[[0.1394, 0.1396, 0.1411, 0.1429, 0.1445, 0.1458, 0.1467]],
[[0.1394, 0.1396, 0.1411, 0.1429, 0.1445, 0.1458, 0.1467]],
[[0.1394, 0.1396, 0.1411, 0.1429, 0.1445, 0.1458, 0.1467]],
[[0.1394, 0.1396, 0.1411, 0.1429, 0.1445, 0.1458, 0.1467]]],
grad_fn=), tensor([[[0.1396, 0.1397, 0.1411, 0.1429, 0.1444, 0.1457, 0.1466]],
[[0.1396, 0.1397, 0.1411, 0.1429, 0.1444, 0.1457, 0.1466]],
[[0.1396, 0.1397, 0.1411, 0.1429, 0.1444, 0.1457, 0.1466]],
[[0.1396, 0.1397, 0.1411, 0.1429, 0.1444, 0.1457, 0.1466]]],
grad_fn=), tensor([[[0.1397, 0.1397, 0.1411, 0.1429, 0.1444, 0.1456, 0.1465]],
[[0.1397, 0.1397, 0.1411, 0.1429, 0.1444, 0.1456, 0.1465]],
[[0.1397, 0.1397, 0.1411, 0.1429, 0.1444, 0.1456, 0.1465]],
[[0.1397, 0.1397, 0.1411, 0.1429, 0.1444, 0.1456, 0.1465]]],
grad_fn=), tensor([[[0.1398, 0.1398, 0.1411, 0.1428, 0.1444, 0.1456, 0.1465]],
[[0.1398, 0.1398, 0.1411, 0.1428, 0.1444, 0.1456, 0.1465]],
[[0.1398, 0.1398, 0.1411, 0.1428, 0.1444, 0.1456, 0.1465]],
[[0.1398, 0.1398, 0.1411, 0.1428, 0.1444, 0.1456, 0.1465]]],
grad_fn=), tensor([[[0.1398, 0.1398, 0.1411, 0.1428, 0.1444, 0.1456, 0.1465]],
[[0.1398, 0.1398, 0.1411, 0.1428, 0.1444, 0.1456, 0.1465]],
[[0.1398, 0.1398, 0.1411, 0.1428, 0.1444, 0.1456, 0.1465]],
[[0.1398, 0.1398, 0.1411, 0.1428, 0.1444, 0.1456, 0.1465]]],
grad_fn=)]

测试代码流程分析:

首先,encoder(X)将测试数据进行嵌入式向量表示,其内置的self.embedding = nn.Embedding(vocab_size, embed_size)将每一个时间步长度为7的序列重新表示为长度为10的嵌入向量表示,x的形状由(batch_size,num_steps)变为(batch_size, num_steps, embed_size),即每一个时间步现在由一个长度为embed_size的嵌入向量表示。然后在编码器中将转换后的X进行X = X.permute(1, 0, 2)维度重改,使得X变为( num_steps,batch_size, embed_size)即,通过这种变化来使得每一次在编码器单次时间步可以使用批量数据进行训练,提高了效率。接下来调用output, state = self.rnn(X)获取编码器的状态output和所有隐藏层的状态state输出(此处隐藏层num_layers=2即有两层),其中output shape: (num_steps, batch_size, num_hiddens) 和 state shape: (num_layers, batch_size, num_hiddens),output与我们输入的X的区别在于最后一维的长度不一致,因为循环神经网络的单个隐藏层的宽度为num_hiddens,这就是X经过编码之后的输出。注意state的第一维为num_layers,因为我们使用的是两层的循环神经网络,故具有两个,其中针对每一个batch_size我们具有一个长度为num_hiddens的输出,这是因为我们使用的X每一个批量将会得到一个最终的上下文向量。
这之后,state = decoder.init_state(encoder(X), None)使用编码器的output, state完成初始化工作,return (outputs.permute(1, 0, 2), hidden_state, enc_valid_lens)状态元组用于解码器解包:enc_outputs, hidden_state, enc_valid_lens = state,enc_outputs作为注意力模块的键值对,hidden_state在去掉多余的编码器隐藏层(第一层)后,作为解码器的初始化查询状态,然后按照公式中提到的构建方法完成context = self.attention(query, enc_outputs, enc_outputs, enc_valid_lens)构建,紧接着使用out, hidden_state = self.rnn(x.permute(1, 0, 2), hidden_state)来完成解码器的隐藏层结果输出以及状态更新,同时outputs.append(out)将解码器隐藏层的输出保存,以便后续统一处理(平展之后,用稠密层连接),再self._attention_weights.append(self.attention.attention_weights)保存当前解码器隐状态与编码器所有时间步的隐状态键值对的权重关系,最后将所有解码器的输入Y得到的outputs = self.dense(torch.cat(outputs, dim=0))进行连接并输出(其中self.dense = nn.Linear(num_hiddens, vocab_size)),最后return outputs.permute(1, 0, 2), [enc_outputs, hidden_state,enc_valid_lens], 将outputs的形状恢复,再输出。

6.3模型训练

模型训练的部分与seq2seq类似,在此略过。(针对此部分若有不清楚的朋友,请参考D2L序列到序列学习(seq2seq)
训练代码:

embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
batch_size, num_steps = 64, 10
lr, num_epochs, device = 0.005, 250, d2l.try_gpu()
train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
encoder = d2l.Seq2SeqEncoder(len(src_vocab), embed_size, num_hiddens, num_layers, dropout)
decoder = Seq2SeqAttentionDecoder(len(tgt_vocab), embed_size, num_hiddens, num_layers, dropout)
net = d2l.EncoderDecoder(encoder, decoder)
d2l.train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)

在这里插入图片描述
模型训练后,我们用它[将几个英语句子翻译成法语]并计算它们的BLEU分数。:

engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):
    translation, dec_attention_weight_seq = d2l.predict_seq2seq(
        net, eng, src_vocab, tgt_vocab, num_steps, device, True)
    print(f'{eng} => {translation}, ',
          f'bleu {d2l.bleu(translation, fra, k=2):.3f}')

go . => va !, bleu 1.000
i lost . => j’ai perdu ., bleu 1.000
he’s calm . => il est riche ., bleu 0.658
i’m home . => je suis chez moi ., bleu 1.000

6.4注意力可视化

在上述的工作后,我们得到了一个dec_attention_weight_seq列表,它是一次句子翻译过程中(翻译’he’s calm .'–>'il est calme .'才算一次),解码器根据<bos>起始符(用强制学习得到的(针对此部分若有不清楚的朋友,请参考D2L序列到序列学习(seq2seq)))来进行生成翻译后的句子(遇到<eos>停止)所需要的总查询次数所对应的注意力权重列表,其每一个列表元素代表了一次解码器生成输出时,使用当前的解码器隐状态查询注意力机制得到的结果,现给出针对’i’m home .'的翻译结果的整个流程如下。
首先给出可视化的代码如下:

print([step[0][0][0] for step in dec_attention_weight_seq])
#去掉三维,得到一维序列
attention_weights = torch.cat([step[0][0][0] for step in dec_attention_weight_seq], 0).reshape((1, 1, -1, num_steps))
print(attention_weights.shape)
print(attention_weights[:, :, :, :len(engs[-1].split()) + 1].shape)

[tensor([0.1607, 0.3239, 0.2925, 0.2229, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
0.0000], device=‘cuda:0’, grad_fn=<SelectBackward0>), tensor([0.0166, 0.2969, 0.3518, 0.3347, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
0.0000], device=‘cuda:0’, grad_fn=<SelectBackward0>), tensor([0.0330, 0.3513, 0.3336, 0.2821, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
0.0000], device=‘cuda:0’, grad_fn=<SelectBackward0>), tensor([0.0031, 0.2760, 0.3686, 0.3523, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
0.0000], device=‘cuda:0’, grad_fn=<SelectBackward0>), tensor([0.0138, 0.1721, 0.2716, 0.5425, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
0.0000], device=‘cuda:0’, grad_fn=<SelectBackward0>), tensor([0.0298, 0.2871, 0.2589, 0.4242, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
0.0000], device=‘cuda:0’, grad_fn=<SelectBackward0>)]
torch.Size([1, 1, 6, 10])
torch.Size([1, 1, 6, 4])

其中dec_attention_weight_seq.shape=(1,1,6,10)表示为 先将’i’m home .‘表示为嵌入矩阵,维度为(1,10,32)(小批量样本数目batch_size,时间步num_steps,嵌入向量维度embed_size) 然后交换0和1维度得到:(10,1,32)。然后使用编码器得到十个隐藏状态输出h1~h10,注意此处的10个输出隐藏状态被填补到10个,此处是长度为3的’i’m home .’(‘i’m’,‘home’,’ .‘)填补到10。
然后,在解码器的生成过程中,使用了六次生成查询来得到’je suis chez moi .’(包括一次初始化查询),但是针对10个填补的隐藏状态有效长度应该是3+1 故截取4作为隐藏状态数目,综上得知查询的次数为6,隐藏单元个数为4。其对应的热值图如下:

# 加上一个包含序列结束词元
#此处[:, :, :, :len(engs[-1].split()) + 1]是去掉后面的无效的填充等价于'.'
#此处dec_attention_weight_seq描述了每一次针对提供的编码序列总状态得到的解码序列每一次的查询
d2l.show_heatmaps(attention_weights[:, :, :, :len(engs[-1].split()) + 1].cpu(),xlabel='Key positions', ylabel='Query positions')

在这里插入图片描述
通过可视化注意力权重会发现,每个查询都会在键值对上分配不同的权重,这说明在每个解码步中,输入序列的不同部分被选择性地聚集在注意力池中。

  • 在预测词元时,如果不是所有输入词元都是相关的,那么具有Bahdanau注意力的循环神经网络编码器-解码器会有选择地统计输入序列的不同部分。这是通过将上下文变量视为加性注意力池化的输出来实现的。
  • 在循环神经网络编码器-解码器中,Bahdanau注意力将上一时间步的解码器隐状态视为查询,在所有时间步的编码器隐状态同时视为键和值。

上一篇:Transformer模型简要分析(上篇)(代码来源D2l)

下一篇:transformer模型简要分析(下篇)(代码来源D2l)

PS:感谢《动手学深度学习DIVE INTO DEEP LEARNING》提供的开源线上代码和阅读教程,在此对一切为了开源事业造福人类的工作人员们表示衷心的感谢!本文可随意分享,您的支持将是作者最大的更新动力,分享转载请备注来源,行文仓促,若有错误,还请不吝赐教,谢谢!

作者:寒秋夜未央

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值