基于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/
蒲公英书等