神经网络在交易中的应用:多元时间序列的双重聚类(DUET)
概述
多元时间序列是一系列数据,其中每个时间戳都包含几个相互关联的变量,用于描述复杂的过程。它们被广泛应用于经济分析、风险管理以及其他需要预测多元数据的领域。与单变量时间序列不同,多元序列能够考虑变量之间的相关性,从而能够创建更准确的预测模型。
在金融市场中,多元时间序列分析被应用于资产价格预测、波动率估计、趋势检测以及交易策略的制定。例如,在预测股票价格时,会考虑交易量、利率、宏观经济指标和新闻等因素。所有这些参数都是相互关联的,对它们进行联合分析可以识别出单独考虑每个变量时无法检测到的模式。
处理多元时间序列的一个关键挑战在于开发出能够识别时间和跨通道依赖关系的方法。然而,在实践中,由于数据变异性,会出现各种困难。在经济危机期间,资产之间的相关性结构会发生变化,这使得传统模型的使用变得复杂。
现有的数据处理方法可以分为三类。第一种方法是独立分析每个通道,但这忽略了变量之间的关系。第二种方法将所有通道结合起来;但是,这可能会引入冗余信息并降低准确性。第三种方法是变量聚类,但它限制了模型的灵活性。
为了解决这些问题,论文 "DUET:Dual Clustering Enhanced Multivariate Time Series Forecasting"(DUET:双聚类增强多元时间序列预测)的作者提出了 DUET 方法,该方法结合了两种聚类方式:时间聚类和通道聚类。时间聚类(TCM)根据相似特征对数据进行分组,并允许模型随时间变化而调整。在金融市场分析中,这使模型能够纳入经济周期的不同阶段。通道聚类( CCM )识别关键变量,消除噪声,提高预测精度。它揭示了资产之间的稳定关系,这对于构建多元化的投资组合尤为重要。
之后,融合模块( FM )将结果整合起来,同步有关时间模式和跨通道依赖关系的信息。这种方法能够更准确地预测金融市场等复杂系统。该框架的作者所进行的实验表明,DUET 的性能优于现有方法,能够提供更准确的预测结果。它考虑了异质性的时间模式以及跨通道关系的动态变化,并能够适应数据的变异性。
DUET 算法
DUET 框架架构代表了一种预测多元时间序列的创新方法,它利用输入数据在时间和通道维度上的双聚类。这提高了模型的性能,并使其结果更易于解释。这种方法可以与经验丰富的分析师的工作相媲美,分析师将复杂的数据系统划分为独立的模块,首先分别进行分析,然后再进行整体分析,以获得更深入的理解。DUET 框架包含几个关键模块,每个模块在数据分析过程中都发挥着专门的作用:
- 实例归一化
- 时间聚类模块 — TCM
- 通道聚类模块 — CCM
- 融合模块 — FM
- 预测模块
输入数据归一化可以去除异常值并平滑剧烈波动,使模型对训练数据集和测试数据集之间的差异更具鲁棒性。这在金融数据分析中尤为重要,因为高频噪声会掩盖有意义的趋势。归一化还有助于使来自不同来源的单个时间序列的统计特征保持一致,从而减少异常值的影响。
时间聚类模块(TCM)分析时间依赖性并将序列分组到簇中,这与金融分析师根据资产的波动性、流动性和历史特征对资产进行分类非常相似。TCM 的核心是一个由多个并行编码器(专家混合模型 — MoE )组成的架构,它根据时间序列的先前聚类,动态地为每个分析片段选择最合适的编码器。这确保了时间序列的准确表示,因为不同的数据组可能需要独特的处理方法。MoE 机制可在编码器之间自适应切换,使模型能够高效地处理不同性质的时间序列,包括高频市场数据。
编码器分析以隐含特征形式表示的时间序列,然后将其分解为长期趋势和短期趋势。这使得识别隐藏模式成为可能,从而提高对金融市场未来价格走势的预测能力。
通道聚类模块( CCM )利用信号的频率特性执行通道聚类。该模块评估通道之间的相关性,识别关键依赖关系,并排除冗余或不重要的组件。就像金融分析师在筛选掉随机市场波动的同时,选择有意义的宏观经济和技术指标一样, CCM 有助于找出最有价值的信号。
分析通道频率特性中振幅矢量之间的距离,可以识别相关信号并消除噪声现象。这在金融市场中尤其有用,因为可以利用资产之间的隐藏关系来构建套利策略或识别系统性风险。
融合模块( FM )使用掩蔽注意力机制整合时间和通道表示。这一过程类似于分析师综合来自不同来源的信息以获得全局视角时,对各种市场因素之间复杂关系的分析。FM 识别最重要的聚类并过滤掉无关信号,从而提高预测准确性。掩蔽注意力机制的使用允许动态调整不同数据成分的重要性,使处理更具适应性。这在金融应用中至关重要,因为资产之间的依赖结构可能会受到宏观经济事件的影响而发生变化。
在最后阶段,预测模块使用聚合特征来预测时间序列的未来值。这一过程可以与专业投资者的行为相类比,后者会根据历史市场数据对未来价格变动做出明智的预测。预测模块采用神经网络方法,能够捕捉复杂的非线性关系并适应数据中潜在的结构变化。最终预测结果会进行逆归一化处理,以便在原始数据的尺度上进行解释。
通过应用掩蔽注意力机制、频域分析和潜在表示聚类等先进的机器学习技术, DUET 提供了较高的预测准确性和可解释性。它有助于发现复杂时间序列中隐藏的模式,并将这些见解应用于优化交易策略,而传统方法往往证明效果不足。与需要大量人工调整和专家干预的传统方法相比, DUET 可以自动检测数据的结构特征并实时进行调整。这使得它在分析高频时间序列和在瞬息万变的市场环境中运作时特别有用。
下面提供了 DUET 框架的原始可视化图。

使用 MQL5 实现
在详细研究了 DUET 框架的理论方面后,我们进入实践环节,使用 MQL5 实现我们对这些方法的一种具体实现方案。
DUET 的模块化架构使其便于逐步开发:每个功能模块都可以被视为系统的独立元素。将架构划分为独立的模块,可以简化调试、测试和后续优化。我们首先实现时间聚类模块。
时间聚类模块
如前所述,时间聚类模块包含多个并行运行的编码器。在本工作的范围内,我们将构建一个尽可能简单的编码器架构,该架构由两个顺序的全连接层组成,并在它们之间使用激活函数引入非线性。然而,值得注意的是,每个编码器都使用其自身的可训练参数来处理独立的片段。为了组织这样的处理过程,我们将使用卷积层。通过将整个输入数据序列输入到该层中,我们定义了分析窗口的大小,并将步长设置为等于分段大小。因此,卷积层的参数将作为编码器全连接层的参数,确保所有序列片段的并行处理。为了增加并行运行的编码器数量,只需按比例增加卷积层中的滤波器数量即可。
这样,并行编码器操作的组织方式已确定。但是,值得注意的是,DUET 框架的作者建议只使用最相关的编码器。假设时间序列遵循潜在正态分布。众所周知,正态分布的特征在于其均值和方差。为了选择 k 个最可能的潜在分布,作者使用了噪声门控方法,该方法可以表示如下:
![]()
添加正态分布噪声( ε )可稳定训练,而 Softplus 函数可确保方差保持为正。
接下来,我们选择 k 个最可能的潜在分布,并使用 SoftMax 函数计算它们的权重。这样,属于同一 k 个最可能潜在分布的时间序列就由一组共享的编码器进行处理。将得到的掩码与编码器的输出相乘,可以得到一个加权结果,并消除不相关滤波器的影响。
在确定了架构解决方案后,我们便开始着手实现。首先,我们实现选择 k 个最相关编码器的算法。利用卷积层对各个分段的分布参数进行参数化。然而,选择 k 个最相关编码器的算法是在 OpenCL 上下文中实现的。为此,我们创建了 TopKgates 内核。
__kernel void TopKgates(__global const float *inputs, __global const float *noises, __global float *gates, const uint k) { size_t idx = get_local_id(0); size_t var = get_global_id(1); size_t window = get_local_size(0); size_t vars = get_global_size(1);
内核参数包括指向三个数据缓冲区(输入数据、噪声和结果)的指针以及要选择的元素数量。
在内核体中,像往常一样,我们首先在任务空间中识别当前线程。在这种情况下,我们使用了一个二维任务空间,并沿第一维度将其划分为若干个局部组。这一维度将与同一段相关的线程分组在一起,并与模型所使用的编码器数量相对应。
接下来,我们确定本地数据缓冲区内的偏移量。
const int shift_logit = var * 2 * window + idx; const int shift_std = shift_logit + window; const int shift_gate = var * window + idx;
并加载相应的输入数据。
float logit = IsNaNOrInf(inputs[shift_logit], MIN_VALUE); float noise = IsNaNOrInf(noises[shift_gate], 0); if(noise != 0) { noise *= Activation(inputs[shift_std], 3); logit += IsNaNOrInf(noise, 0); }
如果噪声值不等于 0,则根据方差和噪声调整 logit 变量的值。
接下来,我们必须确定单个工作组内最大的 k 个 logit 值。为了实现这一目标,我们在本地内存中创建一个数组,作为工作组中线程之间的数据交换媒介,并声明辅助的本地变量。
__local float temp[LOCAL_ARRAY_SIZE]; //--- const uint ls = min((uint)window, (uint)LOCAL_ARRAY_SIZE); uint bigger = 0; float max_logit = logit;
然后,我们定义一个循环,该循环以本地数组的大小为步长,遍历工作组中的元素。
//--- Top K #pragma unroll for(int i = 0; i < window; i += ls) { if(idx >= i && idx < (i + ls)) temp[idx % ls] = logit; barrier(CLK_LOCAL_MEM_FENCE);
在循环内部,当前窗口的元素将其值存储在本地数组中,然后工作组内的线程进行强制同步。
之后,我们创建一个嵌套循环。在迭代过程中,每个线程都会计算本地数组中有多少元素大于当前线程的 logit 值。
for(int i1 = 0; (i1 < min((int)ls,(int)(window-i)) && bigger <= k); i1++) { if(temp[i1] > logit) bigger++; if(temp[i1] > max_logit) max_logit = temp[i1]; } barrier(CLK_LOCAL_MEM_FENCE); }
同时,我们在局部组内寻找最大值。
在完成嵌套循环的所有迭代后,我们再次同步工作组的线程,然后才继续进行外层循环的下一迭代。
很容易看出,只有具有最大 logit 值的 k 个线程没有超过较大元素的阈值数量。这些值存储在结果缓冲区中。
if(bigger <= k) gates[shift_gate] = logit - max_logit; else gates[shift_gate] = MIN_VALUE; }
在所有其他情况下,表示最小值的常量将被写入结果缓冲区。在后续应用 SoftMax 函数时,该值会导致影响系数为零。
上述内核组织了该过程的前向传播,以便在每个具体情况下选择 k 个最相关的编码器。然而,为了构建一个真正具有自适应性的模型,我们还必须组织训练过程以进行编码器选择。当然,上面描述的内核不包含可训练的参数。然而,这些参数被用来生成内核所使用的输入数据。因此,我们必须将误差梯度传播回输入数据层面。该过程在 TopKgatesGrad 内核中实现。在其参数结构中,我们添加了指向包含相应误差梯度的缓冲区的指针。
__kernel void TopKgatesGrad(__global const float *inputs, __global float *grad_inputs, __global const float *noises, __global const float *gates, __global float *grad_gates) { size_t idx = get_global_id(0); size_t var = get_global_id(1); size_t window = get_global_size(0); size_t vars = get_global_size(1);
在内核体中,我们在二维任务空间中识别当前正在执行的线程。任务空间结构继承自前馈内核,但不同之处在于,在此情况下,线程并未被分组到工作组中。
接下来,我们确定全局数据缓冲区中的偏移量,这与前向传递算法类似。
const int shift_logit = var * 2 * window + idx; const int shift_std = shift_logit + window; const int shift_gate = var * window + idx;
第一步是加载与当前线程对应的前馈传递结果。
const float gate = IsNaNOrInf(gates[shift_gate], MIN_VALUE); if(gate <= MIN_VALUE) { grad_inputs[shift_logit] = 0; grad_inputs[shift_std] = 0; return; }
不难猜测,如果所得值等于最小常数,我们可以立即将零值写入包含输入数据梯度的缓冲区。该值对应于将编码器排除在后续操作之外。
否则,我们将输出级别的误差梯度值加载到输入数据梯度缓冲区的相应元素( logit 误差)中。
float grad = IsNaNOrInf(grad_gates[shift_gate], 0); grad_inputs[shift_logit] = grad;
当然,误差梯度并不会传播到噪声层。然而,我们仍需确定方差层面的误差值。在前馈传递过程中,方差与噪声相乘。因此,下一步是获取噪声值。
float noise = IsNaNOrInf(noises[shift_gate], 0); if(noise == 0) { grad_inputs[shift_std] = 0; return; }
显然,如果噪声等于 0,则方差不参与前馈操作。因此,在这种情况下,我们只需存储零梯度,而无需执行进一步的操作。
最后,在剩余情况下,我们根据噪声系数和激活函数的导数来调整误差梯度值。
grad *= noise; grad_inputs[shift_std] = Deactivation(grad, Activation(inputs[shift_std], 3), 3); }
结果值被写入全局数据缓冲区,之后内核执行完成。
上述两个内核的完整源代码可在本文附件中找到。
我们下一步的工作是在主程序方面组织这一流程。首先,我们创建 CNeuronTopKGates 对象,并在其中实现选择 k 个最相关编码器的算法。新对象的结构如下所示。
class CNeuronTopKGates : public CNeuronSoftMaxOCL { protected: int iK; CBufferFloat cbNoise; CNeuronConvOCL cProjection; CNeuronBaseOCL cGates; //--- virtual bool TopKgates(void); virtual bool TopKgatesGradient(void); //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronTopKGates(void) {}; ~CNeuronTopKGates(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint units_count, uint gates, uint top_k, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronTopKGates; } //--- virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual uint GetGates(void) const { return cProjection.GetFilters() / 2; } virtual uint GetUnits(void) const { return cProjection.GetUnits(); } };
在所展示的结构中,我们可以看到,除了通常的重写虚方法集之外,还添加了 TopKgates 和 TopKgatesGradient 。这些是前面描述的内核的包装方法,是在 OpenCL 程序端创建的。它们的实现遵循你已经熟悉的算法,所以我们在这里就不详细赘述了。
少数内部对象是静态声明的,这样我们就可以将类的构造函数和析构函数留空。所有已声明和已继承对象的初始化都在 Init 方法中执行,该方法的参数接收常量,从而可以明确地解释所创建对象的架构。
bool CNeuronTopKGates::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint units_count, uint gates, uint top_k, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronSoftMaxOCL::Init(numOutputs, myIndex, open_cl, gates * units_count, optimization_type, batch)) return false; SetHeads(units_count);
初始化方法的操作从调用父类中同名的方法开始,在该方法中,已对继承对象进行了最低限度的必要检查和初始化。
请注意,在这种情况下,我们使用 SoftMax 函数对象作为父类。这样,我们就可以将选择 k 个最相关的编码器的结果转换为概率表示,而无需创建额外的内部对象。只需使用父类的功能即可。
在成功执行父类方法的操作后,我们继续为新声明的对象构造初始化算法。在这里,我们首先初始化卷积层,该层用于投影分析出的片段的分布参数。
if(!cProjection.Init(0, 0, OpenCL, window, window, 2 * gates, units_count, 1, optimization, iBatch)) return false; cProjection.SetActivationFunction(None);
在这一层的输出端,我们期望获得模型中每个编码器的均值和方差。因此,卷积层中的滤波器数量是编码器指定数量的两倍。
这里我们还会添加一个数据缓冲区,用于生成噪声。
if(!cbNoise.BufferInit(Neurons(), 0) || !cbNoise.BufferCreate(OpenCL)) return false;
最后,我们初始化一个全连接层,该层将存储先前创建的 TopKgates 前馈内核产生的结果。
if(!cGates.Init(0, 1, OpenCL, Neurons(), optimization, iBatch)) return false; cGates.SetActivationFunction(None); //--- return true; }
之后,我们将运算的逻辑结果返回给调用程序,并终止该方法。
请注意,在这种情况下,我们不创建用于存储 Top-K 编码器概率分布的对象。我们打算利用父类的功能,将绝对 logit 值转换为概率空间。因此,支持此过程所需的所有对象都已在父类中创建和初始化。
下一步是构建前馈方法来选择 k 个最相关的编码器,该方法在 CNeuronTopKGates::feedForward 中实现。与往常一样,此方法的参数包括一个指向输入数据对象的指针,该指针会立即传递给负责生成所分析段分布统计特性的对象的同名方法。
bool CNeuronTopKGates::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cProjection.FeedForward(NeuronOCL)) return false;
其次,值得注意的是, DUET 框架的作者建议仅在训练期间向 logit 值添加噪声。因此,我们会检查模型的操作模式,并在必要时生成噪声。
if(bTrain) { double random[]; if(!Math::MathRandomNormal(0, 1, Neurons(), random)) return false; if(!cbNoise.AssignArray(random)) return false; if(!cbNoise.BufferWrite()) return false; } else if(!cbNoise.Fill(0)) return false;
否则,噪声缓冲区将填充零。
然后我们调用包装器方法,该方法负责选择 k 个最相关的编码器。
if(!TopKgates()) return false; //--- return CNeuronSoftMaxOCL::feedForward(cGates.AsObject()); }
获得的结果被传递给父类中同名的方法,该方法将绝对值转换为概率空间。
我们将执行操作的逻辑结果返回给调用程序,并完成方法的执行。
如您所见,前向传递方法采用的是线性算法。因此,反向传播方法也遵循线性结构。因此,我建议将它们的详细考察留作独立学习。该对象及其所有方法的完整代码可在本文附件中找到。
现阶段,我们已经实现了选择 k 个最相关编码器的算法,无论是在主程序端还是在 OpenCL 上下文中。现在我们可以着手构建专家混合(MoE)架构,我们在 CNeuronMoE 对象中实现该架构。新对象的结构如下所示。
class CNeuronMoE : public CNeuronBaseOCL { protected: CNeuronTopKGates cGates; CLayer cExperts; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronMoE(void) {}; ~CNeuronMoE(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_out, uint units_count, uint experts, uint top_k, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronMoE; } //--- virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual void TrainMode(bool flag) { bTrain = flag; cGates.TrainMode(bTrain); } };
在所展示的结构中,我们只能看到两个内部对象。其中之一是先前创建的对象,负责选择 k 个最相关的编码器。第二个是用于存储指向编码器对象的指针的动态数组。这两个对象都是静态声明的,这使得我们可以将构造函数和析构函数设为空。这些对象的所有初始化工作都在 Init 方法中组织起来。
初始化方法的参数传递常量,这些常量对所创建对象的架构提供明确的描述。同时,允许改变输出数据的维度。
bool CNeuronMoE::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_out, uint units_count, uint experts, uint top_k, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_out * units_count, optimization_type, batch)) return false;
该算法首先调用父类中同名的方法,在该方法中已经对继承的对象进行了初始化,并对输入数据进行了验证。
接下来,我们初始化负责选择最相关编码器的对象。
int index = 0; if(!cGates.Init(0, index, OpenCL, window, units_count, experts, top_k, optimization, iBatch)) return false;
之后,我们继续进行编码器对象的直接初始化。首先,我们准备一个动态数组和一些局部变量,用于临时存储指向已创建对象的指针。
cExperts.Clear(); cExperts.SetOpenCL(OpenCL); CNeuronConvOCL *conv = NULL; CNeuronTransposeRCDOCL *transp = NULL;
创建的第一个组件是卷积层,它作为编码器的第一层。该对象的输入将是所有编码器共用的输入数据张量。这一层中的滤波器数量等于一个编码器的结果张量大小与模型中编码器总数的乘积。这种方法使我们能够并行计算所有编码器的值。
index++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, index, OpenCL, window, window, window_out * experts, units_count, 1, optimization, iBatch) || !cExperts.Add(conv)) { delete conv; return false; } conv.SetActivationFunction(SoftPlus);
为了在编码器层之间引入非线性,我们使用 SoftPlus 作为激活函数。
接下来,我们需要添加编码器的第二层。如你所知,每个编码器都必须接收自己的一组参数。我们可以实现这一点。使用卷积层也可以实现这一点。我们只需在表示独立分析序列数量的参数中指定编码器的数量。但是,需要注意的是,第一层的输出是一个三维张量,其维度为 { Units, Encoders, Dimension }。这与我们之前创建的卷积层的操作算法不符。
为了确保正确处理,我们必须交换前两个维度。这项任务由数据转置层执行。
transp = new CNeuronTransposeRCDOCL(); index++; if(!transp || !transp.Init(0, index, OpenCL, units_count, experts, window_out, optimization, iBatch) || !cExperts.Add(transp)) { delete transp; return false; } transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation());
之后,我们可以初始化卷积层,它将作为我们独立编码器的第二层。
index++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, index, OpenCL, window_out, window_out, window_out, units_count, experts, optimization, iBatch) || !cExperts.Add(conv)) { delete conv; return false; } conv.SetActivationFunction(None);
最后,我们添加一个反向数据转置层。
transp = new CNeuronTransposeRCDOCL(); index++; if(!transp || !transp.Init(0, index, OpenCL, experts, units_count, window_out, optimization, iBatch) || !cExperts.Add(transp)) { delete transp; return false; } transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation()); //--- return true; }
至此,内部对象的初始化算法已完成。然后我们将操作的逻辑结果返回给调用者,并完成方法的执行。
完成初始化阶段后,我们继续在 CNeuronMoE::feedForward 方法中实现前向传递算法。
bool CNeuronMoE::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cGates.FeedForward(NeuronOCL)) return false;
该方法参数包括指向输入数据对象的指针,该指针会立即传递给编码器选择对象的相应方法。
接下来,我们继续处理编码器。请注意,它们使用的是在方法参数中接收到的相同输入数据对象。我们首先将此指针存储在局部变量中。
CNeuronBaseOCL *prev = NeuronOCL; int total = cExperts.Total(); for(int i = 0; i < total; i++) { CNeuronBaseOCL *neuron = cExperts[i]; if(!neuron || !neuron.FeedForward(prev)) return false; prev = neuron; }
然后我们组织一个循环,依次迭代编码器层,并调用它们的前馈方法。
循环的所有迭代完成后,我们就能得到所有编码器的全部输出。回想一下,我们之前已经获得了输入数据中每个片段最相关编码器的概率掩码。因此,为了获得每个片段的加权和,我们只需将该片段的编码器相关概率的行向量乘以与该片段对应的编码器输出矩阵。
if(!MatMul(cGates.getOutput(), prev.getOutput(), getOutput(), 1, cGates.GetGates(), Neurons() / cGates.GetUnits(), cGates.GetUnits())) return false; //--- return true; }
计算结果存储在对象的结果缓冲区中。该方法最后会向调用程序返回一个逻辑结果。
至此,我们已完成对编码器集合对象构建算法的分析。我建议将此对象的反向传播方法留作独立研究。与往常一样,本文附件中提供了此对象及其所有方法的完整源代码。
今天我们完成了大量工作,几乎用尽了本文的篇幅。然而,我们的工作尚未完成。我们将稍作休息,并在下一篇文章中继续实现我们对 DUET 框架作者提出的方法的解释。
结论
今天我们研究了 DUET 框架,该框架结合了时间聚类 ( TCM ) 和通道聚类 ( CCM ),用于多元时间序列,以提高其分析和预测的准确性。TCM 通过调整模型来适应时间变化,而 CCM 则 识别关键变量并减少噪声。
在文章的实践部分,我们介绍了时间聚类模块( TCM )的实现。下一篇文章中,我们将继续推进我们已经开始的实现工作。我们将对框架作者提出的方法进行自己的解释,并通过在真实历史数据上测试模型,为这项工作画上完整句号。
参考文献列表
本文中用到的程序
| # | 名称 | 类型 | 描述 |
|---|---|---|---|
| 1 | Research.mq5 | EA | 样本采集 EA |
| 2 | ResearchRealORL.mq5 | EA | 使用 Real-ORL 方法采集样本的 EA |
| 3 | Study.mq5 | EA | 模型训练 EA |
| 4 | Test.mq5 | EA | 模型测试 EA |
| 5 | Trajectory.mqh | 类库 | 系统状态和模型架构描述结构 |
| 6 | NeuroNet.mqh | 类库 | 用于创建神经网络的类库 |
| 7 | NeuroNet.cl | 代码库 | OpenCL 程序代码 |
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/17459
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
您应该了解的MQL5向导技巧(第六十六部分):结合点积核使用FrAMA与强力指数形态
外汇套利交易:汇率关系评估面板
新手在交易中的10个基本错误
数据科学与机器学习(第四十一部分):基于YOLOv8的外汇与股票市场图表形态检测