草庐IT

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

xiyanjoy 2023-04-09 原文

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×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,如下图所示。

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)

乍一看似乎与论文中的解释方式不太一样,怎么就输入分别乘以三个矩阵再分割呢?不是要考虑多头me?
代码的具体操作可用下图来理解,为了方便进行(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的启发,结合自身项目的要求,对模型的输入进行小调整。

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

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  3. ruby-on-rails - 在混合/模块中覆盖模型的属性访问器 - 2

    我有一个包含模块的模型。我想在模块中覆盖模型的访问器方法。例如:classBlah这显然行不通。有什么想法可以实现吗? 最佳答案 您的代码看起来是正确的。我们正在毫无困难地使用这个确切的模式。如果我没记错的话,Rails使用#method_missing作为属性setter,因此您的模块将优先,阻止ActiveRecord的setter。如果您正在使用ActiveSupport::Concern(参见thisblogpost),那么您的实例方法需要进入一个特殊的模块:classBlah

  4. ruby-on-rails - 如何验证非模型(甚至非对象)字段 - 2

    我有一个表单,其中有很多字段取自数组(而不是模型或对象)。我如何验证这些字段的存在?solve_problem_pathdo|f|%>... 最佳答案 创建一个简单的类来包装请求参数并使用ActiveModel::Validations。#definedsomewhere,atthesimplest:require'ostruct'classSolvetrue#youcouldevencheckthesolutionwithavalidatorvalidatedoerrors.add(:base,"WRONG!!!")unlesss

  5. ruby-on-rails - form_for 中不在模型中的自定义字段 - 2

    我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢

  6. ruby-on-rails - 如何将验证与模型分开 - 2

    我有一些非常大的模型,我必须将它们迁移到最新版本的Rails。这些模型有相当多的验证(User有大约50个验证)。是否可以将所有这些验证移动到另一个文件中?说app/models/validations/user_validations.rb。如果可以,有人可以提供示例吗? 最佳答案 您可以为此使用关注点:#app/models/validations/user_validations.rbrequire'active_support/concern'moduleUserValidationsextendActiveSupport:

  7. ruby-on-rails - Rails 模型——非持久类成员或属性? - 2

    对于Rails模型,是否可以/建议让一个类的成员不持久保存到数据库中?我想将用户最后选择的类型存储在session变量中。由于我无法从我的模型中设置session变量,我想将值存储在一个“虚拟”类成员中,该成员只是将值传递回Controller。你能有这样的类(class)成员吗? 最佳答案 将非持久属性添加到Rails模型就像任何其他Ruby类一样:classUser扩展解释:在Ruby中,所有实例变量都是私有(private)的,不需要在赋值前定义。attr_accessor创建一个setter和getter方法:classUs

  8. ruby-on-rails - Ruby 检查日期时间是否为 iso8601 并保存 - 2

    我需要检查DateTime是否采用有效的ISO8601格式。喜欢:#iso8601?我检查了ruby​​是否有特定方法,但没有找到。目前我正在使用date.iso8601==date来检查这个。有什么好的方法吗?编辑解释我的环境,并改变问题的范围。因此,我的项目将使用jsapiFullCalendar,这就是我需要iso8601字符串格式的原因。我想知道更好或正确的方法是什么,以正确的格式将日期保存在数据库中,或者让ActiveRecord完成它们的工作并在我需要时间信息时对其进行操作。 最佳答案 我不太明白你的问题。我假设您想检查

  9. ruby-on-rails - Rails - 从另一个模型中创建一个模型的实例 - 2

    我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案

  10. ruby-on-rails - Ruby 中的内存模型 - 2

    ruby如何管理内存。例如:如果我们在执行过程中采用C程序,则以下是内存模型。类似于这个ruby如何处理内存。C:__________________|||stack|||------------------||||------------------|||||Heap|||||__________________|||data|__________________|text|__________________Ruby:? 最佳答案 Ruby中没有“内存”这样的东西。Class#allocate分配一个对象并返回该对象。这就是程序

随机推荐