学习笔记:基于Transformer的时间序列预测模型

2023-10-27

1 一些准备的说明

为了便于读者理解,笔者将采取一个盾构机掘进参数预测的实际项目进行Transformer模型的说明。此外,该贴更多用于本人的学习记录,适合于对Transformer模型已经有一定了解的读者。此此次外,不定期更新中。

一些参考与图片来源:
Transformer论文链接
transformer的细节到底是怎么样的?
深入理解Transformer及其源码解读
Informer论文链接

1.1 采用的数据

具体的数据在csv中如下,这里只展示部分数据
在这里插入图片描述
在本项目中,并非所有参数都有用到,本文的示例中,仅仅用到了

"state": ["刀盘转速(r/min)",
          "刀盘压力(bar)",
          "总推进力(KN)",
          "螺机转速(r/min)"],
"action": ["A组推进压力设定(bar)",
           "B组推进压力设定(bar)",
           "C组推进压力设定(bar)",
           "D组推进压力设定(bar)",
           "推进速度2(mm/min)"],
"target": ["VMT导向垂直后(mm)",
           "VMT导向水平前(mm)",
           "VMT导向垂直前(mm)",
           "VMT导向水平后(mm)",
           "VMT导向水平趋向RP(mm)",
           "VMT导向垂直趋向RP(mm)"]

这些参数,利用pandas包进行提取。

1.2 时间序列数据的格式

接下来,我们理解一下时间序列数据的格式。
对于某一时刻的数据,应该是类似如下所示的一行1×15的tensor:

[[1.51, 86.656, 69.550, ......(共15个数据)]]

这个数据从左到右分别代表某一时刻中的刀盘转速(r/min)刀盘压力(bar)总推进力(KN)......中15个掘进参数的数据。
若是多个连续时刻的数据,如3个时刻的数据,则应是如下所示3×15的tensor:

[[1.51, 86.656, 69.550, ......(共15个数据)],
 [1.52, 86.756, 69.650, ......(共15个数据)],
 [1.53, 86.856, 69.750, ......(共15个数据)]]

注意上面这两组数据是笔者胡乱输入的,数值大小没有什么实际意义,切勿对号入座。

1.3 Transformer的输入与输出

假设我们的batch_size = 32,笔者在接下来全文模型的解读中将以训练流程为例进行说明。目的是希望通过8个过去时刻的数据(8×15)预测2个未来时刻的数据(2×6)。

  • 输入:分为encoder与decoder的输入,尺寸分别为32×8×15与32×2×15
  • 输出:只有一个,尺寸为32×2×6

其中32为batch_size,32×8×15可理解为32个batch,每个batch中带有8个过去时刻的数据,15是考虑的掘进参数的数目,如1.1图中的刀盘转速刀盘压力总推进力螺机转速……等15个参数。
在下面的整体结构示意图中,会在对应的位置标出输入与输出的数据尺寸大小,注意Encoder层的输入与输出的tensor大小是一致的,同理Decoder层的输入与输出。

2 整体结构

在这里插入图片描述
注:为了适应时间序列预测,相比于原Transformer模型,将最后一个Softmax层删除

3 输入编码

输入在输入前需进行归一化。
输入数据的流动如下图所示,重点关注维度的变化(15——>512)
请添加图片描述
在一些其他时间序列预测项目,如Informer中,还加入了Global Time Stamp,考虑如星期、月份、节假日等日期因素的影响,这些在本文中均不考虑,只考虑位置编码,具体可见Informer论文链接

3.1 Embeddings

首先利用nn.Linear(n_encoder_inputs, channels)将两个输入的维度投影为512,即尺寸大小由32×8×15和32×2×15分别变为32×8×512和32×2×512

3.2 位置编码(Positional Encoding)

利用位置编码,考虑顺序。具体公式如下:
在这里插入图片描述
其中pos=0~7或0~1i=0~512/2
图中的两个Positional Encoding分别得到尺寸大小为32×8×512和32×2×512的两个tensor
(若想深入了解位置编码的作用,可以参考知乎这篇文章transformer的细节到底是怎么样的?,本文不作过多的讨论)

3.3 Embedding With Time Signal

将3.1、3.2中的tensor相加,得到一个全新的尺寸大小为32×8×512和32×2×512的两个tensor,如下图所示。
在这里插入图片描述

3.4 代码说明

以下只展示与上面说明处相关的代码,以encoder中的为例:

 def __init__(self,
              n_encoder_inputs,
              n_decoder_inputs,
              channels=512,
              nhead=8,
              dropout=0.1,
              activation="relu",
              num_e_layer=8,
              num_d_layer=8,
              num_target=1):
        super(TimeSeriesForcasting, self).__init__()
        ...
        self.input_pos_embedding = torch.nn.Embedding(1024, embedding_dim=channels)
		...
        self.input_projection = Linear(n_encoder_inputs, channels)
		...
    def encode_src(self, src):
        src_start = self.input_projection(src).permute(1, 0, 2)

        in_sequence_len, batch_size = src_start.size(0), src_start.size(1)
        
        pos_encoder = (torch.arange(0, in_sequence_len, device=src.device).unsqueeze(0).repeat(batch_size, 1))
        pos_encoder = self.input_pos_embedding(pos_encoder).permute(1, 0, 2)
        
        src = src_start + pos_encoder
        src = self.encoder(src)
        return src
    def decode_trg(self, trg, memory):
        ...

    def forward(self, x):
        src, trg = x
        src = self.encode_src(src)
        out = self.decode_trg(trg=trg, memory=src)
        return out 

输入的src通过投影变换的了维度得到了src_start,即3.1中所述;
关于位置编码,先构造了一个pos_encoder,内为0~in_sequence_len-1(序列长度-1,也就是8-1=7),此时为[0,1,2,…,7],然后扩展维度并复制batch_size(32)次,变为

[[0,1,2,...,7],
 [0,1,2,...,7],
 ...,
 [0,1,2,...,7]]     

尺寸大小为(batch_size, in_sequence_len)即(32, 8),每个位置代表了序列中的位置。
再进行self.input_pos_embedding(pos_encoder),把值赋给pos_encoder,得到3.2中的Positional Encoding,
最后把src_startpos_encoder相加,即3.3的操作
PS:
这里补充说明下初始化时
self.input_pos_embedding = torch.nn.Embedding(1024, embedding_dim=channels)
中1024的意义
nlp任务中意思是词典的大小,也就是可以被嵌入的词汇表的大小
放在时间序列预测任务中的话,就是序列长度大小的上限,我的序列长度最多不能超过1024,否则会采取截断或补零等方法解决。

另外,在实际代码中,采用permute(1, 0, 2)把第0和第1维度转置,应该是为了后续注意力层中操作的便利,在这可暂不理解

4 Encoder

这里先展示tensor在Encoder中大小变化的过程。
在这里插入图片描述
Encoder由N个同样的层构成,每一层的输出作为下一层的输入。其中最后一层的输出进入Decoder,作为Multi-Head Attention层的K与V输入。

4.1 Multi-Head Attention层

4.1.1 Self-Attention

下图就是Self-Attention自注意力机制的过程,X为输入,Z为输出。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
关于自注意力更详细的说明可以参考其他文章,网上一搜一大堆,这里就不多赘述了。

4.1.2 Muti-Head Attention

Muti-Head Attention就是将输入转化为Q、K、V,然后按维度d_model=512切割成h=8个,维度变为d_qord_kord_v=512/8=64,分别做Self-Attention之后再合并恢复为维度d_model=512,然后再进行一次Linear投影,维度不变,得到输出。如下图所示

Muti-Head Attention容易产生迷惑的点主要有两:
1、
作者在对Muti-Head Attention介绍时,采用的输入以Q、K、V这样三个字母表示,然后再进行Linear投影,第一次读到的时候确实很容易让人感到混乱,网上几种主流的介绍通常是将输入以及进行Attention计算的Q、K、V进行区分,这使得容易与Self-Attention中的介绍产生冲突。在这里我们可以这么理解,输入(这里先写成Q、K、V)经过Linear投影后,产生了新的Q、K、V替代了原来的Q、K、V,然后进行Self-Attention计算,即上图中的Scaled Dot-Product Attention部分,这样理解就通顺了。

2、
根据目前本人的搜索结果来看,网上关于Muti-Head Attention的解读方式有两种。
(1)第一种是论文中的解释方式:
论文中的图以及公式如下
在这里插入图片描述
在这里插入图片描述
并且相信很多人都看过下面这张解释Muti-Head Attention的图
在这里插入图片描述
这两张图的解读都没有问题,但合在一块看就出问题了。
首先,关于Muti-Head Attention输入的表示方式,论文中采用Q、K、V,二图中采用X,二者对应关系存在一定问题。
这个我们在1、中已经进行了说明,其实论文中输入的Q、K、V与二图中的X是完全一致的,即Q=K=V=X
其次,最右边的WO看上去是一个长条形的矩阵,但实际上,这在论文中应该是一个512×512的矩阵。作者在绘制该图时可能是为了凸显维度的对应关系才画成如此。
那么,将几个问题解决后,重新绘图,应该是如下这一张图
在这里插入图片描述
理解了这张图后,我们再来看看源码是怎么写的

(2)第二种是源码的思路:
先上代码,代码可能与pytorch中源码有些许细节上的差别,但整体思路是一致的。

# 实现多头注意力机制的类
class MultiHeadAttention(nn.Module):
	def __init__(self, head, embedding_dim, dropout=0.1):
		# head:代表几个头的参数
		# embedding_dim:代表词嵌入的维度
		# dropout:进行Dropout操作时置零的比率
		super(MultiHeadAttention, self).__init__()
		assert embedding_dim % head == 0
		
		self.d_k = embedding_dim // head
		self.head = head
		self.embedding_dim = embedding_dim
		# 获得四个,分别是Q、K、V及最终输出WO线性层
		self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)
		# 初始化注意力张量
		self.attn = None
		self.dropout = nn.Dropout(p=dropout)

	def forward(self, query, key, value, mask=None):
		# query, key, value是注意力机制的三个输入张量,在Encoder中三者一致,mask代表掩码张量
		if mask is not none:
			# 使用suqeeze将掩码张量进行维度扩充,代表多头中的第n个头
			mask = mask.unsqueeze(1)	
		# 得到batch_size
		batch_size = query.size(0)
		
		query, key, value = \
		[model(x).view(batch_size, -1, self.head, self.d_k).transpose(1, 2) \
		for model, x in zip(self.linears), (query, key, value)]
		
		# 将每个头的输出传递到注意力层
		x, self.atten = attention(query, key, value, mask=mask, dropout=self.dropout)
		# 得到每个头的计算结果是4维张量,需要进行形状的转换
		# 前面已经将1,2两个维度进行转置,在这里要重新转置回来
		# 经历了transpose()后必须使用contiguous方法,不然无法使用view()
		x = x.transpose(1, 2).contiguout().view(batch_size, -1, self.head*self.d_k)
		# 最后将x输入线性层列表中的最后一个线性层进行处理
		return self.linears[-1](x)

乍一看似乎与论文中的解释方式不太一样,怎么就输入分别乘以三个矩阵再分割呢?不是要考虑多头么?
代码的具体操作可用下图来理解,为了方便进行(1)论文中的解释方式(2)源码的思路的对比,图中的运算先不考虑batch_size,并且将(1)、(2)放在同一张图中。
在这里插入图片描述
(1)、(2)对比可看出,(2)的思路实际上是将W0……W7合并为一个矩阵进行并行运算,本质思路是一致的。

4.2 Add & Norm层

网上非常多,偷懒略了

4.3 Feed Forward层

网上非常多,偷懒略了

5 Decoder

Decoder中除了Masked Multi-Head Attention层,其余均与Encoder中的一致,在此不多赘述,仅展示tensor在Decoder中大小变化的过程。
在这里插入图片描述

5.1 Masked Multi-Head Attention层

在代码中的作用也可参考4.1.2中的代码部分
在测试以及训练中更加详细的作用见6 模型在训练与测试时的区别
在这里插入图片描述
在这里插入图片描述

6 模型在训练与测试时的区别

本人在Transformer的学习过程中对此训练与测试时的区别疑惑还是比较大的,特此分出一小节进行说明。主要的思路是参考大佬transformer的细节到底是怎么样的?

6.1 测试时

NLP任务中,通常在Encoder中输入待翻译的句子,若句子中有3个词且翻译后为3个词(如"我""是""谁"——>"who""am""I"),则Encoder输入(先不考虑Padding Mask)的大小为(3, 512)。
而Decoder的输入输出相对不太一样。在Decoder的Multi-Head Attention层中,K和V均是Encoder的输出Memory经过线性变换后的结果(此时的Memory中包含了原始输入序列每个位置的编码信息),而Q是Decoder的Masked Multi-Head Attention层输出的隐含向量经过线性变换后的结果。在Decoder对每一个时刻进行解码时,首先需要做的便是通过Q与K进行交互(query查询),并计算得到注意力权重矩阵;然后再通过注意力权重与V进行计算得到一个权重向量,该权重向量所表示的含义就是在解码时如何将注意力分配到Memory的各个位置上。
在解码第1个时刻时,Decoder输入一个表征的向量(表示句子开头),输入大小为(1, 512),即下图中所示。得到Q、K、V后,首先Q通过与K进行交互得到权重向量,此时可以看做是Q(待解码向量)在K(本质上也就是Memory)中查询Memory中各个位置与Q有关的信息;然后将权重向量与V进行运算得到解码向量,此时这个解码向量可以看作是考虑了Memory中各个位置编码信息的输出向量,也就是说它包含了在解码当前时刻时应该将注意力放在Memory中哪些位置上的信息。进一步,Decoder得到输出结果后,再经过一次线性层然后输入到分类层中进行分类得到当前时刻的解码输出值。若模型准确,则应当得到"who"的输出结果。
在这里插入图片描述
在这里插入图片描述
当第1个时刻的解码过程完成之后,应将解码第1个时刻时的输入,以及解码第1个时刻后的输出均作为解码器的输入来解码预测第2个时刻的输出。同理第2个时刻的解码过程完成之后,应将解码第1、2个时刻时的输入,以及解码第2个时刻后的输出均作为解码器的输入来解码预测第2个时刻的输出。

  • 完整流程如下:
    第一个时刻:{<start>} ——>{who}
    第二个时刻:{<start>, who} ——>{am}
    第三个时刻:{<start>, who, am} ——>{I}
    第四个时刻:{<start>, who, am, I} ——>{<end>}

显然这时候存在一个问题。如在第三个时刻,输入了{<start>, who, am},应是一个(3, 512)的向量,那么具体计算过程如下图所示。
在这里插入图片描述
在这里插入图片描述
最后Decoder的输出应是一个和Decoder的输入大小一致的(3, 512)的tensor,而要想得到"I"的结果,Decoder的输出应该是一个(1, 512)的tensor。为此,针对Decoder输出的tensor,只会取其最后一个向量喂入到分类器中进行分类得到当前时刻的解码输出。
同理,在时间序列预测的任务中,我们想要预测2个未来时刻(t1t2)的数据

  • 完整流程如下:
    第一个时刻:{t0时刻数据} ——>{t1时刻数据}
    第二个时刻:{t0时刻数据t1时刻数据} ——>{t2时刻数据}

在第二个时刻,最后Decoder的输出应是一个和Decoder的输入大小一致的(2, 512)的tensor,而要想得到t2时刻数据,Decoder的输出应该是一个(1, 512)的tensor。为此,针对Decoder输出的tensor,只会取其最后一个向量,得到t2时刻数据

6.2 训练时

在介绍完测试时的解码过程后,下面就继续来看在网络在训练过程中是如何进行解码的。在真实预测时解码器需要将上一个时刻的输出作为下一个时刻解码的输入,然后一个时刻一个时刻的进行解码操作。显然,如果训练时也采用同样的方法那将是十分费时的。因此,在训练过程中,解码器也同编码器一样,一次接收解码时所有时刻的输入进行计算。这样做的好处,一是通过多样本并行计算能够加快网络的训练速度;二是在训练过程中直接喂入解码器正确的结果而不是上一时刻的预测值(因为训练时上一时刻的预测值可能是错误的),能够更好的训练网络。
还是以6.1中的NPL任务为例。编码器的输入便是{"我", "是", "谁"},而解码器的输入则是{<start>, who, am, I} ,对应的正确标签则是{who, am, I, <end>,} 。
假设现在解码器的输入{<start>, who, am, I} 在分别乘上一个矩阵进行线性变换后得到了Q、K、V,且Q与K作用后得到了注意力权重矩阵(此时还未进行softmax操作),如下图所示。
在这里插入图片描述
由第1行的权重向量可知,在解码第1个时刻时应该将2/9的注意力放到<start>上,1/3的注意力放到"who"上等等。不过此时有一个问题就是,模型在预测时是看不到当前时刻之后的信息。因此,Transformer中的Decoder通过加入注意力掩码机制来解决了这一问题。
如下图所示,左边依旧是通过Q和K计算得到了注意力权重矩阵(此时还未进行softmax操作),而中间的就是所谓的注意力掩码矩阵,两者在相加之后再乘上矩阵V便得到了整个自注意力机制的输出,也就是Decoder中的Masked Multi-Head Attention。
在这里插入图片描述
那为什么注意力权重矩阵加上这个注意力掩码矩阵就能够达到这样的效果呢?以图中第1行权重为例,当解码器对第1个时刻进行解码时其对应的输入只有<start>,因此这就意味着此时应该将所有的注意力放在第1个位置上(<start>位置上,尽管在训练时解码器一次喂入了所有的输入),换句话说也就是第1个位置上的权重应该是1,而其它位置则是0。从图中可以看出,第1行注意力向量在加上第1行注意力掩码,再经过softmax操作后便得到了一个类似[1,0,0,0,0]的向量。那么,通过这个向量就能够保证在解码第1个时刻时只能将注意力放在第1个位置上(<start>位置上)的特性。在解码后续的时刻也是类似的过程。此外,这个操作与6.1中提到的“只会取其最后一个向量”的操作吻合。
同理,在时间序列预测的任务中也与上述流程类似,具体不在多赘述。

7 模型的一些小改进

Informer的启发,结合自身项目的要求,对模型的输入进行小调整。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

学习笔记:基于Transformer的时间序列预测模型 的相关文章

随机推荐

  • ES6系列教程第二篇--Iterator 详解

    一 什么是for of循环 对于如下一个数组 遍历其中的值方法有哪些 var arr a b c 首先想到的可能就是如下这种 这也是js最原始的遍历方法 和java的语法一样 var arr a b c for var i 0 i
  • 手写代码-Hudi-Demo

    import org apache hudi config HoodieIndexConfig import org apache hudi index HoodieIndex import org apache hudi DataSour
  • 托马斯推荐-android源码网站

    2019独角兽企业重金招聘Python工程师标准 gt gt gt http grepcode com 转载于 https my oschina net u 3407708 blog 1587258
  • js的简单数组去重

    介绍几种数组去重的方法 举例数组 const arr 1 1 2 2 null null undefined undefined new String 1 new String 1 a a NaN NaN set function uniq
  • 轮胎企业RFID生产线管理(MES系统)应用

    1 项目背景 在轮胎生产制造企业中 轮胎生产信息的正确采集和存储将对控制轮胎的生产过程 质量检验和质量跟踪等方面起着重要作用 目前 企业MES系统依靠手工记录和条码扫描的方式进行数据采集 由于轮胎生产工序多且复杂 半成品物料就有十几种工序
  • Spring整合Hibernate实现Spring Data JPA

    在上一篇文章 Spring整合Hibernate实现JPA持久化 中 我们已经介绍了怎样在Spring容器中合理地集成Hibernate来实现JPA的ORM机制 但是 细心的读者会发现 上一篇文章中使用了EntityManager来直接与数
  • UE4 更改DerivedDataCache 位置

    Epic Games UE 4 24 Engine Config BaseEngine ini InstalledDerivedDataBackendGraph MinimumDaysToKeepFile 7 Root Type KeyLe
  • OpenCV与Eigen (C++)【学习备忘】

    可点击OpenCV来自简书 OpenCV官网 OpenCV中的单位 棋盘格边长尺寸 mm 机器视觉 OPENCV 一 各个模块简介 二 数据类型与类对象 1 数据类型 点类 size类 向量类 矩形类 指针类 异常类等 2 类对象 2 1
  • maven资源

    http blog csdn net happyteafriends article details 7449642 jsp转换为静态页面参考 http blog csdn net zklxuankai article details 81
  • 最新Anaconda保姆级安装教程:手把手带你走进数据分析Anaconda安装门槛

    Python数据分析入门 基础概念和最新Anaconda安装 综述 什么是数据分析 数据分析是用适当的方法对收集来的大量数据进行分析 帮助人们作出判断 以便采取适当行动 mermaid svg lsc3TUoKjNk4ccj7 font f
  • 【Linux】—— vim常用操作命令

    这里写目录标题 1 vim的基本概念 2 命令模式的操作 光标跳转 剪贴复制 撤销修改 3 底行模式操作 4 配置vim编辑器 使用文件配置 快速配置 1 vim的基本概念 概念 vim重点解决代码编写的问题 本质文本编辑器 是具有多模式的
  • 简单搭建frp服务(服务端与客户端)

    记录搭建frp服务的过程 首先下载frp 本人使用的是0 22 0版本 其它版本可能不适用 可以通过wget命令进行下载 wget https github com fatedier frp releases download v0 22
  • ElacsticSearch中集合了分组、排序,求和,group by sort sum sum(A*B)

    最近做了一个项目 以前是mysql中 现在使用es进行查询 mysql的查询是 返回的结果是 1 字段求和的数据 2 字段相乘再求和在除数据 这个问题卡了一天 3 根据某个字段分组 4 根据某个字段排序 根据思路因为是求和 所以需要用到聚合
  • 消息 ByteBuf 详解

    Netty提供了ByteBuf来替代Java NIO的ByteBuffer缓冲区 以操纵内存缓冲区 与Java NIO的ByteBuffer相比 ByteBuf的优势如下 Pooling 池化 这点减少了内存复制和GC 提升了效率 复合缓冲
  • SpringBoot简介

    SpringBoot 第一部分 SpringBoot应用 相关概念 约定优于配置 约定优于配置 Convention over Configuration 又称按约定编程 是一种软件设计规范 本质上是对系统 类库或框架中一些东西假定一个大众
  • 微信小程序:以7天为周期,连续签到7天功能效果

    此功能以1 2 3 4 5 6 7这样为一周期 连续签到的功能 通过计算是否为整除7天计算 每7天后切换数目 从而改变周期表 本案例只是提供案例的基本操作 进一步涉及 每日用户集的监听 日历表 签到统计 连续签到 签到中断 后端数据处理 等
  • intellij idea 双击选中一个变量而不是单词

    在keymap 里搜索 select Word at caret 然后双击并在弹出选项里选add mouse shortcut 然后选double click 再在下面click pad 区域点一下 就可以和eclipse一样双击选中完整的
  • 查看权限linux文件权限

    查看权限 rw rw r 一共有10位数 其中 最前面那个 代表的是类型 中间那三个 rw 代表的是所有者 user 然后那三个 rw 代表的是组群 group 最后那三个 r 代表的是其他人 other 然后我再解释一下后面那9位数 r
  • 全国大学生软件测试大赛 Web性能测试--实例(一)

    提示 文章写完后 目录可以自动生成 如何生成可参考右边的帮助文档 Web性能测试 实例 一 咪咕音乐测试 一 测试范围 二 测试要求 step1 a 操作流程 i 进入到咪咕音乐页面 点击 歌手 ii 对歌手进行筛选操作 点击红框内的任意按
  • 学习笔记:基于Transformer的时间序列预测模型

    1 一些准备的说明 为了便于读者理解 笔者将采取一个盾构机掘进参数预测的实际项目进行Transformer模型的说明 此外 该贴更多用于本人的学习记录 适合于对Transformer模型已经有一定了解的读者 此此次外 不定期更新中 一些参考