交易中的神经网络:搭配区段注意力的参数效率变换器(PSformer)
概述
多元时间序列预测是深度学习中的一项重要任务,在气象、能源、异常检测、和金融分析等领域具有实际应用。随着人工智能的快速发展,在设计提升预测准确性的创新模型方面做出了重大努力。尤其是基于变换器的架构,由于其在自然语言处理、和计算机视觉方面的有效性,已引起了相当大的关注。甚至,大规模、预训练的变换器模型在时间序列预测方面展现出了强大的性能,表明增加模型参数和训练数据能够显著强化预测能力。
同时,相比更复杂的基于变换器的架构,许多简单的线性模型亦达成了颇具竞争力的成果。它们在时间序列预测中的成功,可能是因为它们的复杂性较低,这降低了对嘈杂、或不相关数据过度拟合的风险。即使数据集有限,这些模型也能有效地捕获稳定、可靠的形态。
为了解决长期依赖关系建模、及捕获复杂时态关系的挑战,PatchTST 方式运用补片技术处理数据,提取局部语义,从而提供强大的功效。然而,PatchTST 使用与通道无关的结构,具有进一步提升建模效率的巨大潜力。甚至,多变量时间序列的独特性,其时态和空间维度与其它数据类型有很大不同,提供了许多尚未探索的机会。
在深度学习中降低模型复杂度的一条途径是参数共享(PS),其在提高计算效率的同时显著降低参数的数量。在卷积网络中,过滤器在空间位置之间共享权重,以较少的参数提取局部特征。类似地,LSTM 模型跨时间步骤共享权重矩阵,管理记忆和信息流。在自然语言处理中,参数共享已扩展到变换器,通过跨层复用权重,在不影响性能的情况下降低冗余。
在多任务学习中,任务自适应参数共享(TAPS)方法有选择地优调特定任务层,将参数共享最大化,同时以最少的特定任务调整实现高效学习。研究表明,参数共享能够降低模型大小、提升普适性,并降低不同任务的过度拟合风险。
《PSformer:搭配区段注意力的参数高效变换器,进行时间序列预测》的作者提出了一种基于变换器的创新模型,用于多元时间序列预测,其协同了参数共享原理。
它们引入了一种搭配基于两级区段注意力机制的变换器编码器,其中每个编码器层都包含一个共享参数模块。该模块包含三个含有残差连接的全连接层,允许较低的总体参数数量,同时维持模型组件之间的有效信息交换。为了将注意力专注于区段内,它们应用了一种补片方法,将可变序列拆分为单独的补片。然后,在不同变量中占据相同位置的补片被分组至区段。每区个段都成为单变量补片的空间延伸,有效地将多变量时间序列划分至多个区段。
在每个区段内,注意力机制强化了捕获局部时空关系,而跨区段信息整合提升了整体预测精度。作者还结合了 SAM 优化方法,在不降低学习性能的情况下进一步降低了过度拟合。在长期时间序列预测数据集上拓展实验表明,PSformer 提供了强大的结果。PSformer 在 8 个关键预测基准中的,有 6 个优于最新潮的模型。
PSformer 算法
多元时间序列 X ∈ RM×L 包含 M 个变量,和长度为 L 的回溯窗口。序列长度 L 被平均分成 N 个大小为 P 的非重叠补片。然后,来自 M 个变量的 P(i) 形成第 i 个区段,表示长度为 C 的横截面(其中 C=M×P)。
PSformer 的关键组件是区段注意力(SegAtt),和参数共享模块(PS)。PSformer 编码器作为模型的核心,包含 SegAtt 模块和 PS 模块。PS 模块经由参数共享为所有编码器层提供参数。
如同其它时间序列预测架构,PSformer 作者调用 RevIN 方法来有效解决分布漂移问题。
区段时空注意力(SegAtt)将来自同一位置不同通道的区段合并到一个区段之中,并建立跨区段的时空关系。具体而言,原始时间序列 X ∈ RM×L 首先被切分为区段,其中 L=P×N,然后经合并维度 M 和 P,重塑为 X ∈ R(M×P)×N。这会产生 X ∈ RC×N(其中 C=M×P),从而实现跨通道信息融合。
在所变换空间中,数据由两个具有雷同架构的连续模块处理,由 ReLU 激活分隔。每个模块都包含一个参数共享模块,和一个我们已经熟悉的自注意力机制。而计算 𝑸uery ∈ RC×N, 𝑲ey ∈ RC×N 和 𝑽alue ∈ RC×N 矩阵涉及把输入 X 沿区段转换为 N 维表示的非线性变换,缩放的点积注意力主要将焦点分布在整个 C 维度。这令模型能够跨通道和时间两者,学习时空区段之间的依赖关系。
该机制计算 Q、K 和 V, 整合来自不同区段的信息。它还捕获每个区段内的局部时空依赖关系,同时还针对跨延申时间步的长期内区段的关系进行建模。最终输出是 Xout ∈ RC×N,完成注意力过程。
PSformer 引入了一种新的参数共享模块(PS Block),由三个含有残差连接的全连接层组成。具体而言,它用到三个可学习的线性映射 Wj ∈ RN×N с j ∈ {1, 2, 3}。前两层的输出计算如下:
![]()
这种结构模拟含有残差连接的 FeedForward 模块。中间输出 𝑿out 随后当作第三次变换的输入:
![]()
总体而言,PS 模块可表示为:
![]()
PS 模块结构支持非线性变换,同时保留线性映射的轨迹。尽管 PS 模块中的三层拥有不同的参数,但整个 PS 模块在 PSformer 编码器的多个位置上重复使用,确保相同的 𝑾S 模块参数在所有这些位置上都是通用的。具体而言,PS 模块的参数在每个PSformer 编码器的三个部分共享:包括两个 SegAtt 模块,和最终的 PS 模块。这种参数共享策略降低了总体参数个数,同时保持了模型的表现力。
两阶段 SegAtt 机制可比之雏形变换器中的 FeedForward 模块,其中 MLP 由注意力运算取代。在输入和输出之间添加残差连接,并将结果传递到最终的 PS 模块。
然后应用维度变换,来获得 𝑿out ∈ RM×L,其中 C=M×P and L=P×N。
在贯穿 n 层 PSformer 之后,应用最终变换将输出投影到预测横向范围 F 上。
![]()
其中 𝑿pred ∈ RM×F 和 𝑾F ∈ RL×F 表示线性映射。
下面提供了PSformer 框架的原始可视化。

利用 MQL5 实现
在介绍了 PSformer 框架的理论层面之后,我们现在转到利用 MQL5 实际实现我们对所提议方式的愿景。我们特别感兴趣的是实现参数共享模块(PS 模块)的算法。
参数共享模块
如前所述,在作者的原始实现中,PS 模块由三个全连接层组成,其参数应用于全部所分析区段。从我们的角度来看,这没有什么复杂性。在类似情况下,我们反复使用含有非重叠分析窗口的卷积层。真正的挑战在于其它地方:跨多个区块共享参数的机制。
一方面,我们当然可在单层中多次重用同一个模块。然而,这会引入为反向传播通验保留数据的问题。当一个对象在多个前馈通验时被重用,结果缓冲区只存储新的输出,覆盖前一次前馈通验的输出。在典型的神经层工作流中,这并不是问题,因为我们始终在前馈和反向通验之间交替。每次后向通验后,不再需要前一次前馈通验的结果,故可安全地覆盖。但是,当这种交替被打乱时,我们面临着为反向传播通验保留所需的所有正确数据的问题。
在这样的情况下,我们不仅要存储模块的最终输出,还要存储所有中间值。或者我们必须重新计算它们,这增加了模型的计算复杂度。此外,还需要一种机制来同步特定点的缓冲区,以便正确计算误差梯度。
显然,实现这些需求就要修改神经层之间的数据交换接口。反过来,这将触发更广泛地修改我们的函数库函数。
第二种选项是建立一种机制,以便在几个雷同的神经层之间全面共享单一参数缓冲区。然而,这种方式并非没有其自身的“隐藏陷阱”。
回想一下,当我们探索深度确定性政策梯度框架时,我们针对目标模型实现了软参数更新算法。不过,每次更新后复制参数的计算成本很高。理想情况下,我们将相关对象中的参数缓冲区替换为共享参数矩阵。
此处,除了参数矩阵本身之外,我们还必须共享参数更新期间用到的动量缓冲区。在不同阶段使用单独的动量缓冲器,可令参数更新向量偏向于其中一个内部层。
还有另一个重点。在该实现中,反向传播通验中所用的参数,或许与前馈通验所用的参数不同。这听起来或许很不寻常,但我们用一个简单的例子来概括它,其涉及共享参数的两个连续层。在前馈通验期间,两层都用参数 W,并分别产生输出 O1 和 O2。在梯度分布阶段,我们分别计算误差梯度 G1 和 G2。故此,误差梯度传播过程是正确的。在该阶段,模型参数维持不变,所有误差梯度都能正确对应前馈参数 W。不过,如果我们更新其中一层中的参数,例如第二层,我们会得到调整后的参数 W'。我们立即遇到不匹配:误差梯度不再与更新的参数相对应。直接应用不匹配的梯度可能会令训练过程失真。
解决该问题的一种方案是基于最后一次前馈通验的输出,和相应的误差梯度,判断给定层的目标值,然后用更新的参数执行新的前馈挺严,以便计算校正后的误差梯度。如果这听起来很熟悉,那是因为这种方式与我们在之前的文章中讨论过的 SAM 优化算法非常接近。事实上,在重复执行前向通验之前,添加参数更新,我们获得了完整的 SAM 优化过程。
这正是 PSformer 框架作者推荐使用 SAM 优化的原因。它令我们能够容忍梯度与参数不匹配的风险,因为梯度是在参数更新之前重新计算的。然而,在其它场景下,这样的不匹配可能会造成严重的问题。
考虑以上所有因素,我们决定采用第二种方法 — 在相同层之间共享参数缓冲区。
如早前提醒,原始论文中的 PS 模块采用了三个全连接层,我们将其替换为卷积层。因此,我们从 CNeuronConvSAMOCL 卷积层对象开始参数共享实现。
在我们的卷积参数共享层中,我们仅替换指向参数和动量缓冲区的指针。所有其它缓冲区和内部变量仍必须与参数矩阵的维度匹配。当然,这需要调整对象的初始化方法。在行事前,我们创建了两个辅助方法:InitBufferLike 和 ReplaceBuffer。
InitBufferLike 基于给定的引用缓冲区创建一个新缓冲区,其中填充了零值。其算法非常简单。它接收两个指向数据缓冲区对象的指针作为参数。首先,它检查引用缓冲区指针(master)是否有效。有效引用指针的存在对于后续操作至关重要。如果检查失败,该方法将终止并返回 false。
bool CNeuronConvSAMOCL::InitBufferLike(CBufferFloat *&buffer, CBufferFloat *master) { if(!master) return false;
如果第一个检查点成功通过,我们检查指向所创建缓冲区的指针相关性。但于此,如果我们得到一个负值结果,我们简单地创建一个新的对象实例。
if(!buffer) { buffer = new CBufferFloat(); if(!buffer) return false; }
并且不要忘记检查新缓冲区是否已正确创建。
接下来,我们用零值初始化所需缓冲区的大小。
if(!buffer.BufferInit(master.Total(), 0)) return false;
然后我们在 OpenCL 关联环境中创建其副本。
if(!buffer.BufferCreate(master.GetOpenCL())) return false; //--- return true; }
然后,该方法将操作的逻辑结果返回给调用方,并结束。
第二个方法 ReplaceBuffer 替换指向指定缓冲区的指针。初看,我们不需要一个完整的方法来分配指向内部变量对象的指针。不过,在方法主体中,我们会检查数据缓冲区、并在必要时删除多余的。这令我们能够更有效地使用内存和 OpenCL 关联环境内存。
void CNeuronConvSAMOCL::ReplaceBuffer(CBufferFloat *&buffer, CBufferFloat *master) { if(buffer==master) return; if(!!buffer) { buffer.BufferFree(); delete buffer; } //--- buffer = master; }
创建辅助方法之后,我们继续基于引用实例 InitPS 为卷积层对象构建新的初始化算法。在该方法中,我们仅接收指向引用对象的指针,取代接收定义对象架构的完整常量集。
bool CNeuronConvSAMOCL::InitPS(CNeuronConvSAMOCL *master) { if(!master || master.Type() != Type() ) return false;
在方法主体中,我们检查所接收指针是否正确,以及对象类型是否匹配。
接下来,我们简单地从引用对象传送所有继承参数的数值,取代构建一整套父类方法。
alpha = master.alpha; iBatch = master.iBatch; t = master.t; m_myIndex = master.m_myIndex; activation = master.activation; optimization = master.optimization; iWindow = master.iWindow; iStep = master.iStep; iWindowOut = master.iWindowOut; iVariables = master.iVariables; bTrain = master.bTrain; fRho = master.fRho;
接下来,我们创建类似于引用对象中的结果和误差梯度缓冲区。
if(!InitBufferLike(Output, master.Output)) return false; if(!!master.getPrevOutput()) if(!InitBufferLike(PrevOutput, master.getPrevOutput())) return false; if(!InitBufferLike(Gradient, master.Gradient)) return false;
之后,我们首先传送到从基本全连接层继承而来的权重和动量缓冲区指针。
ReplaceBuffer(Weights, master.Weights); ReplaceBuffer(DeltaWeights, master.DeltaWeights); ReplaceBuffer(FirstMomentum, master.FirstMomentum); ReplaceBuffer(SecondMomentum, master.SecondMomentum);
我们针对卷积层参数的缓冲区、及其动量重复类似的操作。
ReplaceBuffer(WeightsConv, master.WeightsConv); ReplaceBuffer(DeltaWeightsConv, master.DeltaWeightsConv); ReplaceBuffer(FirstMomentumConv, master.FirstMomentumConv); ReplaceBuffer(SecondMomentumConv, master.SecondMomentumConv);
接下来,我们需要创建经调整参数的缓冲区。不过,在某些条件下,或许不会创建两个经调整参数的缓冲区。仅当存在传出连接时,才会创建全连接层的调整参数缓冲区。因此,我们首先检查引用对象中该缓冲区的大小。我们仅在必要时创建相关缓冲区。
if(master.cWeightsSAM.Total() > 0) { CBufferFloat *buf = GetPointer(cWeightsSAM); if(!InitBufferLike(buf, GetPointer(master.cWeightsSAM))) return false; }
否则,我们将清除该缓冲区,降低内存消耗。
else
{
cWeightsSAM.BufferFree();
cWeightsSAM.Clear();
}
当模糊区域系数大于 0 时,将创建收入连接的经调整参数的缓冲区。
if(fRho > 0) { CBufferFloat *buf = GetPointer(cWeightsSAMConv); if(!InitBufferLike(buf, GetPointer(master.cWeightsSAMConv))) return false; }
否则,我们将清除该缓冲区。
else
{
cWeightsSAMConv.BufferFree();
cWeightsSAMConv.Clear();
}
技术上,我们可以检查包含引用对象收入连接的经调整参数的缓冲区大小,取代模糊系数 — 就如同我们检查外出连接的经调整参数缓冲区。不过,我们知道,如果模糊系数大于零,则该缓冲区必须存在。因此,我们包含一个额外的控制。如果尝试创建零长度缓冲区,则该进程将失败,引发错误,并止步初始化。这有助于防止在以后的执行中出现更严重的问题。
在初始化方法结束时,我们将所有对象传送到单个 OpenCL 关联环境中,并将操作的逻辑结果返回给调用程序。
SetOpenCL(master.OpenCL); //--- return true; }
修改卷积层对象之后,我们进入下一阶段的工作。现在我们将创建参数共享模块(PS 模块)本身。为此,我们引入了一个新对象:CNeuronPSBlock。如同理论章节勾勒,PS 模块由三个顺序数据变换层组成。每个都有一个参数方阵,确保整个模块及其内部层的输入和输出张量维度保持一致。在前两层之间,应用了 GELU 激活函数。在第二层之后,残差连接被添加到原始输入当中。
为了实现该架构,新对象将包含两个内部卷积层,而最终的卷积层将直接由我们类本身的结构表示,继承卷积层类的基本功能。由于我们将在训练期间使用 SAM 优化,因此架构中的所有卷积层都会是 SAM 兼容。新类结构如下所示。
class CNeuronPSBlock : public CNeuronConvSAMOCL { protected: CNeuronConvSAMOCL acConvolution[2]; CNeuronBaseOCL cResidual; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL); virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL); public: CNeuronPSBlock(void) {}; ~CNeuronPSBlock(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_out, uint units_count, uint variables, float rho, ENUM_OPTIMIZATION optimization_type, uint batch); virtual bool InitPS(CNeuronPSBlock *master); //--- virtual int Type(void) const { return defNeuronPSBlock; } //--- methods for working with files virtual bool Save(int const file_handle); virtual bool Load(int const file_handle); //--- virtual CLayerDescription* GetLayerInfo(void); virtual void SetOpenCL(COpenCLMy *obj); };
如结构所示,新对象声明了两个初始化方法。这是有意为之。Init – 标准初始化方法,其中对象的架构由传递给该方法的参数明确定义。InitPS – 类似于卷积层类中的同名方法,它根据引用对象的结构创建一个新对象。在该过程中,将从引用中复制指向参数和动量缓冲区的指针。我们来详研构造指定方法的算法。
如上所述,Init 方法在其参数中接收一组常量,允许完全判定对象的架构。
bool CNeuronPSBlock::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_out, uint units_count, uint variables, float rho, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronConvSAMOCL::Init(numOutputs, myIndex, open_cl, window, window, window_out, units_count, variables, rho, optimization_type, batch)) return false;
在方法主体中,我们立即将所有接收到的参数转发至父类的同名方法。如您所知,父类方法已包含继承对象的必要参数验证点、及初始化逻辑。
由于 PS 模块内的所有卷积层都拥有相同的维度,故第一个内部卷积层的初始化采用完全相同的参数。
if(!acConvolution[0].Init(0, 0, OpenCL, iWindow, iWindow, iWindowOut, units_count, iVariables, fRho, optimization, iBatch)) return false; acConvolution[0].SetActivationFunction(GELU);
然后,我们遵照 PSformer 作者的建议添加 GELU 激活函数。
不过,我们也允许用户修改模块输出处的张量维度。因此,在初始化第二个内部卷积层时,后随残差连接,我们交换分析窗口大小、及过滤器数量的参数。这可确保输出维度与原始输入数据的输出维度匹配。
if(!acConvolution[1].Init(0, 1, OpenCL, iWindowOut, iWindowOut, iWindow, units_count, iVariables, fRho, optimization, iBatch)) return false; acConvolution[1].SetActivationFunction(None);
我们在此不用激活函数。
接下来,我们添加一个基础神经层来存储残差连接数据。它的大小对应于第二个嵌套卷积层的结果缓冲区。
if(!cResidual.Init(0, 2, OpenCL, acConvolution[1].Neurons(), optimization, iBatch)) return false; if(!cResidual.SetGradient(acConvolution[1].getGradient(), true)) return false; cResidual.SetActivationFunction(None);
基于引用实例创建对象之后,我们立即替换误差梯度缓冲区。这种优化令我们能够降低反向传播过程中的数据复制操作的数量。
接下来,我们明确禁用参数共享模块的激活函数,并结束该方法,将逻辑结果返回给调用者。
SetActivationFunction(None); //--- return true; }
第二种初始化方法稍微简单一些。它接收一个指向引用对象的指针,并将其直接传递至父类的同名方法。
重点要注意,当前方法中的参数类型与父类中的参数类型不同。故此,我们显式指定要传递的对象类型。
bool CNeuronPSBlock::InitPS(CNeuronPSBlock *master) { if(!CNeuronConvSAMOCL::InitPS((CNeuronConvSAMOCL*)master)) return false;
父类方法已包含必要的验证检查,以及用于复制常量、创建新缓冲区、以及存储指向参数和动量缓冲区指针的逻辑。
然后,我们迭代遍历内部卷积层,调用它们相应的初始化方法,并从相应的引用对象复制数据。
for(int i = 0; i < 2; i++) if(!acConvolution[i].InitPS(master.acConvolution[i].AsObject())) return false;
残差连接层不包含可训练参数,其大小与第二个内部卷积层的结果缓冲区匹配。因此,它的初始化逻辑完全取自主要初始化方法。
if(!cResidual.Init(0, 2, OpenCL, acConvolution[1].Neurons(), optimization, iBatch)) return false; if(!cResidual.SetGradient(acConvolution[1].getGradient(), true)) return false; cResidual.SetActivationFunction(None); //--- return true; }
如前,我们替换指向误差梯度缓冲区的指针。
初始化方法结束后,我们继续前馈算法。这部分相对直截了当。该方法接收指向输入数据对象的指针,我们将其直接传递给第一个内部卷积层的前馈方法。
bool CNeuronPSBlock::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!acConvolution[0].FeedForward(NeuronOCL)) return false;
然后将结果按顺序传递到下一个卷积层。之后,我们将结果值与原始输入相加。我们将总和保存在残差连接缓冲区当中。
if(!acConvolution[1].FeedForward(acConvolution[0].AsObject())) return false; if(!SumAndNormilize(NeuronOCL.getOutput(), acConvolution[1].getOutput(), cResidual.getOutput(), iWindow, true, 0, 0, 0, 1)) return false;
在此,我们与原始 PSformer 算法略有不同:我们将残差张量归一化,然后将其传递给最终卷积层,其功能继承自父类。
if(!CNeuronConvSAMOCL::feedForward(cResidual.AsObject())) return false; //--- return true; }
方法结束时将操作的逻辑结果返回给调用方。
误差梯度分布方法 calcInputGradients 也很简单,但却有重要的细微差别。它接收到指向源数据层对象的指针,我们必须将误差梯度传播到该对象中。
bool CNeuronPSBlock::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false;
首先,我们检查接收指针的有效性 — 如果无效,则进一步处理毫无意义。
然后,我们以相反的顺序将梯度向后传递至所有卷积层。
if(!CNeuronConvSAMOCL::calcInputGradients(cResidual.AsObject())) return false; if(!acConvolution[0].calcHiddenGradients(acConvolution[1].AsObject())) return false; if(!NeuronOCL.calcHiddenGradients(acConvolution[0].AsObject())) return false;
注意,自残差连接对象没有明确的误差梯度被传输到第二个内部卷积层。不过,得益于我们之前用指针替换了数据缓冲区,信息仍可完整传送。
经由卷积层流水线将梯度发送回源数据层之后,我们还加上来自残差连接分支的梯度。有两种可能的情况,具体取决于源数据对象是否含有激活函数。
我要提醒您,我们将误差梯度传递给残差连接对象,且无需调整激活函数的导数。我们明确指出对于该对象它不存在。
因此,鉴于源数据对象没有激活函数,我们只需要添加两个缓冲区的相应值。
if(NeuronOCL.Activation() == None) { if(!SumAndNormilize(NeuronOCL.getGradient(), cResidual.getGradient(), NeuronOCL.getGradient(), iWindow, false, 0, 0, 0, 1)) return false; }
否则,我们先用激活函数的导数,将获得的误差梯度调整到自由缓冲区当中。然后,我们将获得的结果与之前在源数据对象缓冲区中累积的结果相加。
else { if(!DeActivation(NeuronOCL.getOutput(), cResidual.getGradient(), cResidual.getPrevOutput(), NeuronOCL.Activation()) || !SumAndNormilize(NeuronOCL.getGradient(), cResidual.getPrevOutput(), NeuronOCL.getGradient(), iWindow, false, 0, 0, 0, 1)) return false; } //--- return true; }
然后我们完成该方法。
关于更新模块参数的 updateInputWeights 方法,应该说几句话。这个模块很简单 — 我们只需调用父类的相应方法,并在内部对象里包含可训练参数。
bool CNeuronPSBlock::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(!CNeuronConvSAMOCL::updateInputWeights(cResidual.AsObject())) return false; if(!acConvolution[1].UpdateInputWeights(acConvolution[0].AsObject())) return false; if(!acConvolution[0].UpdateInputWeights(NeuronOCL)) return false; //--- return true; }
然而,使用 SAM 优化对运算顺序提出了严格的要求。在 SAM 优化期间,我们按调整后的参数执行第二次前向通验。这会更新结果缓冲区,虽然这对更新当前层参数无害,但可能会中断后续层中的参数更新。这是因为它们采用前一层的前馈结果。为了防止这种情况,我们必须贯穿内部对象以相反的顺序更新参数。这将确保在另一层更替其输入缓冲区之前,调整每个层的参数。
我们对 CNeuronPSBlock 参数共享模块算法的讨论到此完毕。该类及其方法的完整源代码可在提供的附件中找到。
我们的工作还未完毕,但文章篇幅太长了。因此,我们将短暂休息,并在下一篇文章中继续工作。
结束语
在本文中,我们探讨了 PSformer 框架,其作者强调了其在时间序列预测方面的高精度,以及计算资源的有效利用。PSformer 的关键架构组件包括参数共享模块(PS),和基于区段的时空注意力(SegAtt)。它们允许对局部和全局时间序列依赖关系进行有效建模,同时在不牺牲预测品质的情况下降低参数个数。
在实践部分,我们开始利用 MQL5 实现我们自己对所提议方法的解释。我们的工作尚未完成。在下一篇文章中,我们将继续开发、并评估这些方法在与我们的特定任务相关的真实历史数据集上的有效性。
参考
文章中所用程序
| # | 名称 | 类型 | 说明 |
|---|---|---|---|
| 1 | Research.mq5 | 智能系统 | 收集样本的智能系统 |
| 2 | ResearchRealORL.mq5 | 智能系统 | 利用 Real-ORL 方法收集样本的智能系统 |
| 3 | Study.mq5 | 智能系统 | 模型训练智能系统 |
| 4 | StudyEncoder.mq5 | 智能系统 | 编码器训练智能系统 |
| 5 | Test.mq5 | 智能系统 | 模型测试智能系统 |
| 6 | Trajectory.mqh | 类库 | 系统状态描述结构 |
| 7 | NeuroNet.mqh | 类库 | 创建神经网络的类库 |
| 8 | NeuroNet.cl | 函数库 | OpenCL 程序代码库 |
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/16439
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
从基础到中级:联合(一)
构建自优化型MQL5智能交易系统(EA)(第3部分):动态趋势跟踪与均值回归策略
《精通日志记录(第二部分):格式化日志》
让新闻交易轻松上手(第六部分):执行交易(3)
我发现第二个参数 "SecondInput "未使用,因为 CNeuronBaseOCL 的 feedForward 方法有两个参数,内部调用的是单参数版本。您能确认这是否是一个错误吗?