基于pytorch的sque2suqe with attention实现与介绍

基于pytorch的sque2suqe with attention实现与介绍

  • 上一篇文章《基于pytorch的ConvGRU神经网络的实现与介绍》https://blog.csdn.net/qq_34992900/article/details/119514362 提到了关于GRU和LSTM神经元输出的处理问题,只采用LSTM或者GRU的话仅仅提取最后一个输出就可以,但是这样可能会造成一些信息的丢失,sque2sque with attention可以对输出进行转化,通过利用注意力机制对gru或者LSTM的输出进行重要性权重标注,提升模型的效果。

  • sque2suqe with attention由三个部分组成,编码层,解码层和attention层,以下来进行分别的介绍。

  • 1. 编码层:

    • 编码层主要是将一个不定长的序列转化为固定长度的序列,即 X : ( x 1 , x 2 , x 3 , … , x n ) X: (x_1,x_2,x_3, …, x_n) X:(x1,x2,x3,,xn) —>> C : ( c 1 , c 2 , … , c m ) C: (c_1, c_2, …, c_m) C:(c1,c2,,cm),编码层的结构较为简单,主要代码如下:
    class Encoder(nn.Module):
    
        def __init__(self, inpDim, hidDim, decDim):
            super(Encoder, self).__init__()
    
            self.rnn = nn.GRU(inpDim, hidDim, bidirectional = True)
            self.fc = nn.Linear(hidDim * 2, decDim)
        def forward(self, inputs):
            """前向传播过程
    
            function:
                output, hidSta = self.rnn(inputs)
                args:
                    inputs : [sque_len, batch_size, feature_num]
                return:
                    output: [sque_len, batch_size, hidDim * 2]
                    hidSta: [sque_len, barch_size, hidDim * 2]
                 encSta = self.fc(**args)
                 args:
                     hidSta 最后一个隐含层输出,双向所以为torch.cat(-1,-2)
                 return:
                     encSta: [batch_size, decDim]
            """
            inputs = inputs.transpose(0,1)
            encOut, hidSta = self.rnn(inputs)
    
            encSta = self.fc(torch.cat((hidSta[-2,:,:], hidSta[-1,:,:]), dim = 1))
            ### torch.cat 将两个tensor合并,dim 为合并的维度 ###
            return encOut, encSta
    
    	 * 
    
  • 从组成上来说,编码结果就是一个双向GRU + 全连接层, 双向GRU的作用是为了提取序列的前向和后向特征,全连接层则是为了将双向GRU最后一个隐含层的信息提取出来作为 attention的输入。

  • 需要注意的是不同输入的维度,由于在GRU的设置中没有设置batch_firsh = True, 输入的矩阵要求第一维度为序列长度

  • 2. attention层

    • attention层的主要作用是基于encoder的输出的信息:encOut 和 encSta,对时间序列中的不同时次对于最终输出的影响进行评估,代码如下:
    class Attention(nn.Module):
    
        def __init__(self, hidDim, decDim):
            super(Attention, self).__init__()
    
            self.att_fc = nn.Linear(hidDim*2 + decDim, decDim)
            self.eng_fc = nn.Linear(decDim, 1, bias = False)
    
        def forward(self, encOut, encSta):
            """Attention 前向传播过程
            args:
                输入数据为 encoder部分的输出
            return:
                输出的为 每一个时次的贡献
            """
    
            batSiz = encOut.shape[0]
            squLen = encOut.shape[1]
    
            encSta = encSta.unsqueeze(1).repeat(1, squLen, 1)
            ### tensor.unsqueeze 增加encSta的第二个维度 ###
            ### tensor.repeat 在第二维度上将encSta重复squLen次 ###
            energy = torch.tanh(self.att_fc(
                        torch.cat((encSta, encOut), dim = 2)))
            ### energy :[batch_size, sque_len, dec_hid_dim]) ###
            att = self.eng_fc(energy).squeeze(2)
    
            return F.softmax(att, dim=1)
    
    • 依据代码对Attention机制如何实现进行解析,从组成上来说,attention是由两个全连接层组成,分别用来计算encOut与encSta的全连接变换 和 注意力权重计算,最后由softmax转化为权重。
    • 这一部分需要格外注意输入输出的维度信息:
      • encOut : [sque_len, batch_size, enc_hid_dim*2] 双向
      • encSta : [batch_size, dec_input_dim]
      • 为了使得encSta能与 encOut 合并 使用 unsqueeze为encSta增加一个维度,同时使用repeat对encSta的信息重复sque_len次
        encSta.shape --> torch.Size([64, 5])
        encSta.unsqueeze(1).shape --> torch.Size([64, 1, 5])
        encSta.unsqueeze(1).repeat(1, squLen, 1) --> torch.Size([64, 48, 5])
        
      • 使用cat将encOut与转化后的encSta进行合并,得到的矩阵维度为[batch_size, sque_lne, enc_hid_dim*2+dec_hid_dim]
      • 而attention机制的主要作用是分别给予输入的多个时间序列权重,因此其输出的应该为[sque_len, 1],每个序列一个维度,而pytorch中nn.linear的操作是进行矩阵运算,因此计算只发生在矩阵的最后两个维度,如:
      • ten1 = torch.randn(10,5,10)
        linear1 = nn.Linear(10, 1)
        ten2 = linear1(ten1)
        ten2.shape --> torch.Size([10, 5, 1])
        
      • 因此可以发现无论 self.att_fc 这个全连接层的 第二层维度为多少,其 第二维度均为 sque_len, 为方便,直接设置为 dec_hid_dim
      • 则输出的能量矩阵大小为:[batch_size, sque_len, dec_hid_dim]
      • 最后一个全连接层就是将能量矩阵第三维度进行归一,转化为 [batch_size, sque_len, 1]
      • 输出时使用softmax进行处理,求取概率,softmax处理后 单个样本中,各个时间序列的权重求和为1
  • 3. decoder

    • decoder层的作用是对编码后的固定长度的时间序列进行解码,在解码中要利用attention输出的encoder的加权,同时也会使用到输出数据,也就是将模型预测的数据作为编码层的输入,以提高信息含量,在训练中,输入数据为实测数据,在预报中输入数据为模型预报的前一步长的数据。
    • 从代码中进行解析
    class Decoder(nn.Module):
        def __init__(self, outDim, inpDim, endHidDim, decHidDim, attention):
            super(Decoder, self).__init__()
            self.outDim = outDim
            self.attention = attention
            self.rnn = nn.GRU((endHidDim * 2) + inpDim, decHidDim)
            self.fc_out = nn.Linear((endHidDim * 2) + decHidDim + inpDim, outDim)
    
        def forward(self, decInp, encOut, encSta):
            """Decoder 前向传播过程
            args:
                decInp: 解码层的输入
                    [batch_size, feature_num]
                encOut, encSta: 与attenion输入一致
                    encOut : [batch_size, src_len, enc_hid_dim * 2]
                    encSta : [batch_size, dec_hid_dim]
            function:
                self.attention(encOut, encSta) 调用的为上方定义的Attention
                return:
                    att: [batch_size, sque_len]   
            """
            decInp = decInp.unsqueeze(1)
            # encOut = [batch_size, src_len, enc_hid_dim * 2]
            encOut = encOut.transpose(0, 1)
            att = self.attention(encOut, encSta).unsqueeze(1)
            # cm = [1, batch_size, enc_hid_dim * 2]
            cm = torch.bmm(att, encOut)
            ### toech.bmm 为tensor的对应相乘 ###
            ### 利用attention对输入的每一个样本赋予权重 ###
            rnnInp = torch.cat((decInp, cm), dim = 2).transpose(0,1)
            # decOut = [src_len(=1), batch_size, dec_hid_dim]
            # decHid = [n_layers * num_directions, batch_size, dec_hid_dim]
            decOut, decHid = self.rnn(rnnInp,encSta.unsqueeze(0))
            decOut = decOut.squeeze(0)
            cm = cm.transpose(0, 1).squeeze(0)
            decInp = decInp.squeeze(1)
            pred = self.fc_out(torch.cat((decOut, cm, decInp), dim = 1))
            return pred, decHid.squeeze(0)
    
  • 从结构上看,解码层的结构也较为简单,包括一个单向GRU,attention和一个全连接层,GRU处理的是,由attention处理过后的加权编码信息与解码层的输入信息相结合的数组,可以认为是一个过去时间和未来时间叠加在一起的时间序列,使用GRU进行时间信息的提取。

    • decInp为未来单一时间的样本,在训练中使用的为真实数据,在预报中使用的为模型本身给出的预报数据,维度为[batch_size, out_dim],使用unsqueeze添加一个时间维度,便于之后的cat
    • attention的上文已经提及,cm为经过attention处理后的固定长度的编码, torch.bmm为矩阵相乘,对encOut进行注意编码,
      • 此时att维度:[batch_size, 1, sque_len]
      • encOut维度:[batch_size, sque_len, enc_hid_dim]
      • 则cm维度:[batch_size, 1, enc_hid_dim]
    • 由此可以看出,enc_hid_dim的隐含层节点数,直接定义了编码序列的长度
    • 解码由GRU进行,输入的数据为 cm和未来时次时间序列 decInp,采用拼接在第三个维度,说明未来的时间序列相当于增加一个编码的特征
    • 为了能够在输出预测值的全连接层进行解码信息、编码信息和未来时间信息的合并,对decOut、cm、decInp进行了维度的统一,上述三种输入在样本量上(batch_size)上的维度是一致的,合并之后的维度为[batch_size, [(endHidDim * 2) + decHidDim + outDim]]
      • 其中endHidDim*2 为cm的维度, decHidDim为 decOut维度,outDim为未来时次的(输出)的特征数
  • 4. sque2sque

    • sque2sque是将encoder和decoder组合到一起,在forward中描述未来时次输入到decoder的过程,代码如下:
    
    class sque2sque(nn.Module):
    
        def __init__(self, encoder, decoder):
            super(sque2sque, self).__init__()
            self.encoder = encoder
            self.decoder = decoder
    
        def forward(self, inputs, outputs):
    
            encOut, encSta = self.encoder(inputs)
            batSiz = inputs.shape[0]
            outLen = outputs.shape[1]
            outDim = self.decoder.outDim
            decInput = torch.zeros(batSiz, outLen, outDim)
            dec_input = decInput[:,0,:]
            for t in range(1, outLen):
    
                decOut, decSta = self.decoder(dec_input, encOut, encSta)
                outputs[:,t] = decOut
                dec_input = outputs[t-1]
                
            return outputs
    
    • 这里主要说明一下forward结构,forward中 首先是编码层的编码,主要需要注意的地方在解码层的output的输入(未来时次输入)
      • 在做第一个未来时次预测的时候,应用中是没有预报数据的,所以先定义了一个 decInput的tensor,大小为[batch_size, out_sque_Len, out_dim],意义分别为 样本数(batch_size), 输出的单样本时间序列长度, out_dim输出的特征数,在第一次数据的时候输入的样本为全为零的矩阵,代表第一步长的预报中没有未来数据作为参考,在之后1~out_sque_len的计算中,decoder输入的为未来时次真实的数据,更新到dec_input中。
  • 5. train

    • 模型的训练模块相对比较简单
    def train(model, dataLoader, optimizer, criterion):
        model.train()    
        epoch_loss = 0
        for i, (x, y) in enumerate(dataLoader):
            tx, ty = x, y
            # pred = [trg_len, batch_size, pred_dim]
            pred = model(x, y)
    
            pred_dim = pred.shape[-1]
            ty = ty.view(-1)
            pred = pred.view(-1, pred_dim)
            loss = criterion(pred, ty)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            epoch_loss += loss.item()
        return epoch_loss / (len(dataLoader)*dataLoader.batch_size)
    
  • 6. 预测过程

    • 模型的预测中需要将预报数据输出到decoder中,因此需要机型一些改正
    def model_predict(model, inputs, outLen = 16):
    
        encOut, encSta = model.encoder(inputs)
    
        sample = inputs.shape[0]
        decInput = torch.zeros(sample, outLen, outDim)
    
        for t in range(1, outLen):
                dec_input = decInput[:, t-1, :]
                decOut, decSta = model.decoder(dec_input, encOut, encSta)
                decInput[:,t,:] = decOut 
        return decInput
    
    
    • 除第一个时次外,每一次将上一时次的预报结果放入到decInput中作为下一步的输入
  • 代码和测试数据我已经放入了githup中,地址为:https://github.com/Orient94/pytorch_learning/tree/main/suqence2squence

  • 现阶段模型的整体效果还不是很好,还请各位大佬多多批评指导,给予思路,谢过啦,谢过啦

  • 文章参考:
    https://wmathor.com/index.php/archives/1451/
    蒲公英书等

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值