
神经网络变得简单(第 81 部分):上下文引导运动分析(CCMR)
概述
作为本系列的一部分,我们领略过各种分析环境状态的方法,以及用于获取数据的算法。我们用卷积模型在历史价格走势数据中找到稳定的形态。我们还用到关注度模型来寻找不同局部环境状态之间的依赖性。我们总是将环境状态评估为某个时间点的某个横截面。然而,我们从未评估过环境指标的动态。我们假设模型在分析和比较环境条件的过程中,会对关键变化加以某种关注。但我们没有用到这种动态的明确定量表示。
然而,在计算机视觉领域,存在一个基本问题,即光流估测。该问题的解所提供信息关乎场景中对象的移动。为了解决这个问题,已经提出了许多有趣的算法,且现在被广泛使用。光流估测结果用于从自动驾驶到目标跟踪、以及监控的各个领域。
当前大多数方式都用到卷积神经网络,但它们缺乏全局上下文。这样就很难推断对象遮挡或大位移。一种替代方式是使用变换器和其它关注度技术。它们令您能够远远超出经典 CNN 的固定感知场。
论文《CCMR:通过由粗略到精细的上下文引导运动推理进行高分辨率光流估测》中提出了一种特别有趣的方法,题为 CCMR。这是一种光流估测方式,结合了面向关注度的运动聚合概念方法,和高分辨率多尺度方式的优点。CCMR 方法始终如一地将基于上下文的运动分组概念集成到高分辨率粗粒度估测框架之中。这允许详细的流场,其在遮挡区域中也可提供高精度。在此背景下,该方法的作者提出了一种两阶段的运动分组策略,其中首先计算全局自我关注度上下文特征,并用它们跨所有尺度迭代引导运动特征。因此,关于基于 XCiT 的运动上下文导向推理提供了所有粗粒度尺度的处理。该方法作者的实验证明了所提议方法的强大性能,及其基本概念的优势。
1. CCMR 算法
CCMR 方法利用环流更新估测光流,即在粗略和精细尺度基础上利用通用门控环流单元(GRU)。在开始估测之前,对于每个尺度 S,计算特征 Fs,1, Fs,2 进行匹配。此外,上下文特征 Cs,基于它们计算全局上下文特征 GCs,以及从参考状态 I1 开始的环流模块的当前尺度的初始隐藏状态 Hs。
从最粗略的 1/16 开始,根据上述特征 F1,1, F1,2, C1, GC1, H1 计算光流。在 T1 环流更新后,使用分享的 X2 凸上采样器对估测的光流进行上采样,其中该光流当作下一个更精细尺度匹配过程的初始化。该过程一直持续,直到以最精细的 1/2 尺度计算光流,并上采样到原始分辨率。
该方法的作者提议使用特征提取器提取多尺度图像和上下文特征。为此,从上到下计算中间特征,然后,为了获得多尺度特征,将更具结构化和更精细的特征 Fs,1,Fs,2 和 Cs,与 S∈ {2, 3, 4} 的深层粗略尺度特征 Fs−1,1,Fs−1,2 和 Cs−1 相结合,它们在语义上得以提升。因此,执行整合是通过堆叠上采样的较粗略特征、和中间较精细的特征、及它们的聚合。
基于多尺度 Cs 上下文特征,计算全局上下文特征。此处的目标是获得更有意义的特征,然后用于控制运动。为此,利用 XCiT 层的通道统计来执行上下文特征 Cs 的聚合,这确保了关系到令牌数量的线性复杂性。这种架构选择允许在估测期间,在所有粗略和精细尺度上进行可能的上下文聚合。重点要注意,作者提议按 CCMR 方式来运用 XCiT,与其原始方式不同,后者 XCiT 层实际上应用于其输入数据的更粗略表示,通过显式修补实现,然后再次上采样到原始分辨率。相比之下,在 CCMR 内,XCiT 层被直接应用于特定尺度内容的所有粗略和精细尺度特征。为了计算全局上下文,首先将位置编码添加到上下文特征 Cs。然后,常规化层。在该阶段,为了实现自关注,所有 Query、Key 和 Value 特征都是从 Csp 计算得出。在应用交叉协方差关注步骤之前,通道 KCs, QCs, VCs 都被重塑到 h 头。然后据 XCA(KCs, QCs, VCs) 计算交叉协方差关注。此后,应用局部补丁交互层(LPI),然后应用 FFN 模块。
而交叉协方差关注度为每个头中提供通道之间的全局交互,LPI 和 FFN 模块分别在局部提供令牌之间的显式空间交互和所有通道之间的连接。
首先,基于第一次迭代中的初始流(或后续迭代中的更新流),据图像特征(Fs,1, Fs,2)计算邻域匹配成本。然后,计算得到的成本、以及当前流估测值通过运动编码器进行处理,该编码器输出运动特征,最终由 GRU 来计算线程更新。
在计算迭代流更新时,基于上下文特征合并全局聚合运动特征,这有助于解决遮挡区域中的歧义。这是合乎逻辑的,因为来自部分未遮挡对象的遮挡像素的移动,通常可据其未遮挡像素的移动来推断 。为了在单个尺度里设置聚合运动特征,该方法的作者遵循了他们基于全局上下文计算的全局通道统计数据的有效策略,该计算在所有粗略和精细尺度上执行。执行运动分组是把交叉关注度层 XCiT 应用于全局上下文特征 GCs、及运动特征 MF。因此,我们直接在每个尺度上的运动特征计算来自全局上下文特征 GCs 和 Value 的 Query 和 Key,而无需显式分割为补丁。将 XCA、LPI 和 FFN 应用于上下文的 Query、Key 和 Value 之后,上下文驱动的运动特征(CMF)、上下文驱动的运动特征 Cs,及初始运动特征 MF,被组合并传送到一个环流模块来迭代计算更新流。
注意,使用令牌交叉关注度按粗粒度和精调规程来执行运动聚合,在内存占用方面是不切实际的。
其作者提出的 CCMR 方法的原始可视化,如下提供。
2. 利用 MQL5 实现
在研究过 CCMR 方法的理论层面之后,我们转到本文的实践部分,其中我们利用 MQL5 实现所提议方法。如您所见,所提议架构相当复杂。因此,我决定将所提议算法的实现划分为若干个模块。
2.1闭环卷积模块
我们将从闭环卷积模块开始。为了实现它,我们创建 CResidualConv 类,其将从全连接层类 CNeuronBaseOCL 继承基本功能。
新类的结构如下所示。如您所见,它包含一组熟悉的方法。
class CResidualConv : public CNeuronBaseOCL { protected: int iWindowOut; //--- CNeuronConvOCL cConvs[3]; CNeuronBatchNormOCL cNorm[3]; CNeuronBaseOCL cTemp; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL); virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); public: CResidualConv(void) {}; ~CResidualConv(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_out, uint count, ENUM_OPTIMIZATION optimization_type, uint batch); virtual bool calcInputGradients(CNeuronBaseOCL *prevLayer); //--- virtual int Type(void) const { return defResidualConv; } //--- 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); virtual void TrainMode(bool flag); ///< Set Training Mode Flag };
类功能将使用卷积层的 3 个模块和批量常规化。所有内层都声明为静态,这允许我们将类构造函数和析构函数留空。
类对象的初始化在 Init 方法中执行。在方法参数中,我们将传递定义类架构的常量。
bool CResidualConv::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_out, uint count, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_out * count, optimization_type, batch)) return false;
在方法的主体中,我们调用父类的同名方法来控制接收到的参数,并初始化继承的对象。
在父类方法成功执行后,我们初始化内部对象。
if(!cConvs[0].Init(0, 0, OpenCL, window, window, window_out, count, optimization, iBatch)) return false; if(!cNorm[0].Init(0, 1, OpenCL, window_out * count, iBatch, optimization)) return false; cNorm[0].SetActivationFunction(LReLU);
为了从环境的分析状态中提取特征,我们使用 2 个顺序卷积层模块,及带有 LReLU 函数的批量常规化,以便在它们之间创建非线性。
if(!cConvs[1].Init(0, 2, OpenCL, window_out, window_out, window_out, count, optimization, iBatch)) return false; if(!cNorm[1].Init(0, 3, OpenCL, window_out * count, iBatch, optimization)) return false; cNorm[1].SetActivationFunction(None);
我们使用卷积层的第三个模块和批量常规化(没有激活函数)来吧原始数据缩放到我们的 CResidualConv 的结果大小。这将允许我们实现第二个数据流。
if(!cConvs[2].Init(0, 4, OpenCL, window, window, window_out, count, optimization, iBatch)) return false; if(!cNorm[2].Init(0, 5, OpenCL, window_out * count, iBatch, optimization)) return false; cNorm[2].SetActivationFunction(None);
创建 2 个并行数据流迫使我们在类似的并行流中传输误差梯度。我们用到一个辅助内层来对误差梯度求和。
if(!cTemp.Init(0, 6, OpenCL, window * count, optimization, batch)) return false;
为避免不必要的数据复制,我们替换了数据缓冲区。
cNorm[1].SetGradientIndex(getGradientIndex()); cNorm[2].SetGradientIndex(getGradientIndex()); SetActivationFunction(None); iWindowOut = (int)window_out; //--- return true; }
我们在 CResidualConv::feedForward 方法中实现前馈功能。在方法参数中,我们收到一个指向前一个神经层的指针。
bool CResidualConv::feedForward(CNeuronBaseOCL *NeuronOCL) { //--- if(!cConvs[0].FeedForward(NeuronOCL)) return false; if(!cNorm[0].FeedForward(GetPointer(cConvs[0]))) return false;
在方法的主体中,我们没有组织检查接收的指针,因为这种检查已在内层的相关方法中实现。因此,我们立即开始为内层调用前馈方法。
if(!cConvs[1].FeedForward(GetPointer(cNorm[0]))) return false; if(!cNorm[1].FeedForward(GetPointer(cConvs[1]))) return false;
如上所述,我们将用收自前一个神经层的数据执行模块 1 和 3 的前馈验算。
if(!cConvs[2].FeedForward(NeuronOCL)) return false; if(!cNorm[2].FeedForward(GetPointer(cConvs[2]))) return false;
然后我们添加并规范化它们的结果。
if(!SumAndNormilize(cNorm[1].getOutput(), cNorm[2].getOutput(), Output, iWindowOut, true)) return false; //--- return true; }
误差梯度反向传播的逆反过程在 CResidualConv::calcInputGradients 方法中实现。它的算法与前馈方法非常相似。我们只在内层上调用同名的方法,但顺序逆反。
bool CResidualConv::calcInputGradients(CNeuronBaseOCL *prevLayer) { if(!cNorm[2].calcInputGradients(GetPointer(cConvs[2]))) return false; if(!cConvs[2].calcInputGradients(GetPointer(cTemp))) return false; //--- if(!cNorm[1].calcInputGradients(GetPointer(cConvs[1]))) return false; if(!cConvs[1].calcInputGradients(GetPointer(cNorm[0]))) return false; if(!cNorm[0].calcInputGradients(prevLayer)) return false;
您应于此注意,通过替换数据缓冲区,我们消除了将初始误差梯度复制到内层的过程。对于前一层,我们传送 2 个数据流的误差梯度之和。
if(!SumAndNormilize(prevLayer.getGradient(), cTemp.getGradient(), prevLayer.getGradient(), iWindowOut, false)) return false; //--- return true; }
更新类参数的 CResidualConv::updateInputWeights 方法的组织方式类似。我建议您通过随附的代码熟悉它。CResidualConv 类及其所有方法的完整代码附在下面。附件还包括准备本文时用到的所有程序的完整代码。现在我们转到研究构建下一个模块的算法:特征编码器。
2.2特征编码器
由 CCMR 方法的作者提议的特征编码器算法将在 CCCMREncoder 类中实现,其也继承自全连接神经层 CNeuronBaseOCL 的基类。
class CCCMREncoder : public CNeuronBaseOCL { protected: CResidualConv cResidual[6]; CNeuronConvOCL cInput; CNeuronBatchNormOCL cNorm; CNeuronConvOCL cOutput; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL); virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); public: CCCMREncoder(void) {}; ~CCCMREncoder(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_out, uint count, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual bool calcInputGradients(CNeuronBaseOCL *prevLayer); //--- virtual int Type(void) const { return defCCMREncoder; } //--- 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); virtual void TrainMode(bool flag); ///< Set Training Mode Flag };
在该类中,我们使用卷积层来投影原始数据 cInput,其结果我们使用批量常规化层 cNorm 进行常规化。我们还使用编码器的卷积层运算结果投影 cOutput。由于我们用到源数据和结果的投影层,因此我们可在若干个尺度上设置级联特征提取,而无需参考源数据的大小和期望的特征数量。
数据缩放和特征提取过程在若干个顺序闭环卷积模块中执行,出于方便起见,我们将其组合到数组 cResidual 之中。
如之前的类,我们将类的所有内部对象声明为静态,这允许我们将类的构造函数和析构函数留空。
类对象的初始化在 CCCMREncoder::Init 方法中执行。该方法的算法遵循已经熟悉的逻辑。在参数中,该方法接收类架构常量。
bool CCCMREncoder::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_out, uint count, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_out * count, optimization_type, batch)) return false;
在方法的主体中,我们首先调用父类的相关方法,其检查接收到的参数,并初始化继承的对象。我们取其完成的逻辑结果来控制父类方法结果。
接下来,我们初始化缩放和常规化源数据的模块。基于其操作结果,我们计划以 32 个参数的描述形式获得环境的单一状态的表示。
if(!cInput.Init(0, 0, OpenCL, window, window, 32, count, optimization, iBatch)) return false; if(!cNorm.Init(0, 1, OpenCL, 32 * count, iBatch, optimization)) return false; cNorm.SetActivationFunction(LReLU);
然后,我们创建一个特征数字为 {32, 64, 128} 的数据伸缩级联。
if(!cResidual[0].Init(0, 2, OpenCL, 32, 32, count, optimization, iBatch)) return false; if(!cResidual[1].Init(0, 3, OpenCL, 32, 32, count, optimization, iBatch)) return false;
if(!cResidual[2].Init(0, 4, OpenCL, 32, 64, count, optimization, iBatch)) return false; if(!cResidual[3].Init(0, 5, OpenCL, 64, 64, count, optimization, iBatch)) return false;
if(!cResidual[4].Init(0, 6, OpenCL, 64, 128, count, optimization, iBatch)) return false; if(!cResidual[5].Init(0, 7, OpenCL, 128, 128, count, optimization, iBatch)) return false;
最后,我们将数据维度带到用户指定的规模。
if(!cOutput.Init(0, 8, OpenCL, 128, 128, window_out, count, optimization, iBatch)) return false;
为了消除不必要的模块操作结果和误差梯度的复制操作,我们替换了数据缓冲区。
if(Output != cOutput.getOutput()) { if(!!Output) delete Output; Output = cOutput.getOutput(); } //--- if(Gradient != cOutput.getGradient()) { if(!!Gradient) delete Gradient; Gradient = cOutput.getGradient(); } //--- return true; }
不要忘记控制每一步的操作过程。然后,我们使用逻辑值通知调用者方法结果。
现在,我们在 CCCMREncoder::feedForward 方法中创建前馈验算算法。在方法参数中,如常,我们会收到一个指向上一层对象的指针。在嵌套对象的前馈验算方法主体中执行检查接收到的指针的相关性。
bool CCCMREncoder::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cInput.FeedForward(NeuronOCL)) return false; if(!cNorm.FeedForward(GetPointer(cInput))) return false;
首先,我们对原始数据进行缩放和常规化。然后,我们将把数据推到配以特征提取的缩放级联。
注意,第一个闭环卷积模块从批量常规化层接收其初始数据,而后续数据则来自数组中的前一个模块。这允许我们在循环中迭代遍历模块。
if(!cResidual[0].FeedForward(GetPointer(cNorm))) return false; for(int i = 1; i < 6; i++) if(!cResidual[i].FeedForward(GetPointer(cResidual[i - 1]))) return false;
我们将操作的结果缩放到给定的大小。
if(!cOutput.FeedForward(GetPointer(cResidual[5]))) return false; //--- return true; }
误差梯度以逆反的顺序经内部编码器对象传播。
bool CCCMREncoder::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(!cInput.UpdateInputWeights(NeuronOCL)) return false; if(!cNorm.UpdateInputWeights(GetPointer(cInput))) return false; if(!cResidual[0].UpdateInputWeights(GetPointer(cNorm))) return false; for(int i = 1; i < 6; i++) if(!cResidual[i].UpdateInputWeights(GetPointer(cResidual[i - 1]))) return false; if(!cOutput.UpdateInputWeights(GetPointer(cResidual[5]))) return false; //--- return true; }
在本文中,我们不会赘述该类的所有方法。它们具有类似的模块结构,即顺序调用内部对象的相应方法。您可以查看下面附带的完整代码来研究结构。如果您对代码有任何疑问,我很乐意在论坛或私信中回答。选择您偏好的沟通形式。
2.3全局上下文的动态分组
为了在研究特征变化的动态情况下对全局上下文进行分组,CCRM 方法的作者提议使用交叉关注度模块 XCiT。在该模块中,Query 和 Key 实体由全局上下文的特征组成。Value 是由 2 个后续状态形成的环境特征的动态形成的。这种模块的用法与我们之前研究过的有些不同。为了实现使用模块的提议选项,我们需要进行一些修改。
我们创建一个新类 CNeuronCrossXCiTOCL,它将继承自 XCiT 方法之前实现的大部分功能。
class CNeuronCrossXCiTOCL : public CNeuronXCiTOCL { protected: CCollection cConcat; CCollection cValue; CCollection cV_Weights; CBufferFloat TempBuffer; uint iWindow2; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Motion); virtual bool Concat(CBufferFloat *input1, CBufferFloat *input2, CBufferFloat *output, int window1, int window2); //--- virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Motion); virtual bool DeConcat(CBufferFloat *input1, CBufferFloat *input2, CBufferFloat *output, int window1, int window2); public: CNeuronCrossXCiTOCL(void) {}; ~CNeuronCrossXCiTOCL(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window1, uint window2, uint lpi_window, uint heads, uint units_count, uint layers, ENUM_OPTIMIZATION optimization_type, uint batch); virtual bool calcInputGradients(CNeuronBaseOCL *prevLayer, CNeuronBaseOCL *Motion); //--- virtual int Type(void) const { return defNeuronCrossXCiTOCL; } //--- 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); };
注意,在该实现中,我尝试最大程度地使用之前创建的功能。类结构中添加了 3 个数据缓冲区集合,和一个存储中间数据的辅助缓冲区。
如前,所有内部对象都声明为静态,如此该类构造函数和析构函数是 “空的”。
所有类对象的初始化都在方法 CNeuronCrossXCiTOCL::Init 中执行。
bool CNeuronCrossXCiTOCL::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window1, uint window2, uint lpi_window, uint heads, uint units_count, uint layers, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronXCiTOCL::Init(numOutputs, myIndex, open_cl, window1, lpi_window, heads, units_count, layers, optimization_type, batch)) return false;
在参数中,该方法接收主要参数,可判定整个类及其内部对象的架构。在类的主体中,我们调用父类的相关方法,其会检查接收到的参数,并初始化所有继承的对象。
父类方法成功执行后,我们定义缓冲区参数,用于写入 Value 实体,及其误差梯度。我们还为指定实体定义了权重生成矩阵。
//--- Cross XCA iWindow2 = fmax(window2, 1); uint num = iWindowKey * iHeads * iUnits; //Size of V tensor uint v_weights = (iWindow2 + 1) * iWindowKey * iHeads; //Size of weights' matrix of V tensor
接下来,我们按 XCiT 交叉关注度内层的数量规划一个循环,并在循环体中创建所需的缓冲区。我们首先添加一个缓冲区,来写入生成的 Value 实体,和相应的误差梯度。
for(uint i = 0; i < iLayers; i++) { CBufferFloat *temp = NULL; for(int d = 0; d < 2; d++) { //--- XCiT //--- Initilize V tensor temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(num, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!cValue.Add(temp)) return false;
在父类 CNeuronXCiTOCL 中,我们使用了 Query、Key 和 Value 实体的级联缓冲区。为了能够进一步使用继承的功能,我们将来自 2 个源的指定实体连接到一个 cConcat 集合缓冲区之中。
//--- Initilize QKV tensor temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(3 * num, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!cConcat.Add(temp)) return false; }
下一步是创建权重矩阵缓冲区,以便生成 Value 实体。
//--- XCiT //--- Initilize V weights temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.Reserve(v_weights)) return false; float k = (float)(1 / sqrt(iWindow + 1)); for(uint w = 0; w < v_weights; w++) { if(!temp.Add((GenerateWeight() - 0.5f)* k)) return false; } if(!temp.BufferCreate(OpenCL)) return false; if(!cV_Weights.Add(temp)) return false;
动量缓冲区用于指定权重矩阵的优化过程。
for(int d = 0; d < (optimization == SGD ? 1 : 2); d++) { //--- XCiT temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(v_weights, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!cV_Weights.Add(temp)) return false; } }
然后我们初始化中间数据存储缓冲区。
TempBuffer.BufferInit(iWindow2 * iUnits, 0); if(!TempBuffer.BufferCreate(OpenCL)) return false; //--- return true; }
不要忘记控制每一步的操作过程。
前馈方法 CNeuronCrossXCiTOCL::feedForward 主要从父类复制而来。不过,交叉关注度特征需要重新定义。特别是,若要实现交叉关注度,我们需要两个初始数据源。
bool CNeuronCrossXCiTOCL::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Motion) { if(!NeuronOCL || !Motion) return false;
在方法的主体中,我们检查所接收指针与源数据对象的相关性,并规划一个遍历内层的循环。
for(uint i = 0; (i < iLayers && !IsStopped()); i++) { //--- Calculate Queries, Keys, Values CBufferFloat *inputs = (i == 0 ? NeuronOCL.getOutput() : FF_Tensors.At(4 * i - 2)); CBufferFloat *qkv = QKV_Tensors.At(i * 2); if(IsStopped() || !ConvolutionForward(QKV_Weights.At(i * (optimization == SGD ? 2 : 3)), inputs, qkv, iWindow, 2 * iWindowKey * iHeads, None)) return false;
在循环主体中,我们首先据前一个神经层的数据中生成 Query 和 Key 实体。假设在这个信息流中,我们得到 GCs 全局上下文。
请注意,我们使用的缓冲区来自 QKV_Tensors 和 QKV_Weights 旧版集合。不过,我们只生成了 2 个实体。这可以从卷积滤波器的数量 “2 * iWindowKey * iHeads” 中看出。
类似地,我们生成第三个实体 Value,但基于其它初始数据。
CBufferFloat *v = cValue.At(i * 2); if(IsStopped() || !ConvolutionForward(cV_Weights.At(i * (optimization == SGD ? 2 : 3)), Motion, v, iWindow, iWindowKey * iHeads, None)) return false;
如上所述,为了能够使用继承的功能,我们把所有 3 个实体级联到一个张量当中。
if(IsStopped() || !Concat(qkv, v, cConcat.At(2 * i), 2 * iWindowKey * iHeads, iWindowKey * iHeads)) return false;
然后我们使用继承的功能,但有一件事。在此实现中,两个序列流中的元素数相同。因为在顶层,我们依据相同的源数据生成两个流。鉴于这种理解,我没有包括序列长度相等性检查。但对于后续功能的正确运行,这种合规性至关重要。因此,如果您想单独使用这个类,那么请确保两个序列的长度相等。
我们判定多头关注度的结果。
//--- Score calculation CBufferFloat *temp = S_Tensors.At(i * 2); CBufferFloat *out = AO_Tensors.At(i * 2); if(IsStopped() || !XCiT(cConcat.At(2 * i), temp, out)) return false;
将数据流相加并常规化。
//--- Sum and normilize attention if(IsStopped() || !SumAndNormilize(out, inputs, out, iWindow, true)) return false;
接下来是一段局部交互模块。然后对流进行求和,并常规化。
//--- LPI inputs = out; temp = cLPI.At(i * 6); if(IsStopped() || !ConvolutionForward(cLPI_Weights.At(i * (optimization == SGD ? 5 : 7)), inputs, temp, iLPIWindow, iHeads, LReLU, iLPIStep)) return false; out = cLPI.At(i * 6 + 1); if(IsStopped() || !BatchNorm(temp, cLPI_Weights.At(i * (optimization == SGD ? 5 : 7) + 1), out)) return false; temp = out; out = cLPI.At(i * 6 + 2); if(IsStopped() || !ConvolutionForward(cLPI_Weights.At(i * (optimization == SGD ? 5 : 7) + 2), temp, out, 2 * iHeads, 2, None, iHeads)) return false; //--- Sum and normilize attention if(IsStopped() || !SumAndNormilize(out, inputs, out, iWindow, true)) return false;
然后是 FeedForward 模块。
//--- Feed Forward inputs = out; temp = FF_Tensors.At(i * 4); if(IsStopped() || !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? 4 : 6)), inputs, temp, iWindow, 4 * iWindow, LReLU)) return false; out = FF_Tensors.At(i * 4 + 1); if(IsStopped() || !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? 4 : 6) + 1), temp, out, 4 * iWindow, iWindow, activation)) return false; //--- Sum and normilize out if(IsStopped() || !SumAndNormilize(out, inputs, out, iWindow, true)) return false; } iBatchCount++; //--- return true; }
在所有内部神经层成功迭代之后,我们结束该方法。
请注意,在该方法中,原始特征动态数据的一个缓冲区用于所有内部神经层。全局上下文逐渐变化,并转变为上下文引导的全局上下文《上下文引导的运动特征(CMF)》。
通过内部对象传播误差梯度的过程,按类似的方式以相反的顺序实现。其算法在 CNeuronCrossXCiTOCL::calcInputGradients 方法中进行了描述。在参数中,该方法接收指向 2 个源数据对象的指针,其中包含我们必须填充的相应误差梯度的缓冲区。
bool CNeuronCrossXCiTOCL::calcInputGradients(CNeuronBaseOCL *prevLayer, CNeuronBaseOCL *Motion) { if(!prevLayer || !Motion) return false;
在方法的主体中,我们首先检查所接收指针的相关性。接下来,我们安排一个遍历内层的循环,顺序逆反。
CBufferFloat *out_grad = Gradient; //--- for(int i = int(iLayers - 1); (i >= 0 && !IsStopped()); i--) { //--- Passing gradient through feed forward layers if(IsStopped() || !ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? 4 : 6) + 1), out_grad, FF_Tensors.At(i * 4), FF_Tensors.At(i * 4 + 2), 4 * iWindow, iWindow, None)) return false; CBufferFloat *temp = cLPI.At(i * 6 + 5); if(IsStopped() || !ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? 4 : 6)), FF_Tensors.At(i * 4 + 1), cLPI.At(i * 6 + 2), temp, iWindow, 4 * iWindow, LReLU)) return false;
在循环主体中,我们首先通过 FeedForward 模块传播误差梯度。
我提醒您,在前馈验算期间,我们添加并常规化每个模块的输入和输出数据。因此,在反向传播验算期间,我们还需要沿两个数据流传播误差梯度。因此,在通过模块 FeedForward 传播误差梯度后,我们必须对两个流的误差梯度求和。
//--- Sum gradients if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, false, 0, 0, 0, 1)) return false;
以类似的方式,我们通过局部交互模块传播误差梯度,并对两个数据流的误差梯度求和。
out_grad = temp; //--- Passing gradient through LPI if(IsStopped() || !ConvolutionInputGradients(cLPI_Weights.At(i * (optimization == SGD ? 5 : 7) + 2), temp, cLPI.At(i * 6 + 1), cLPI.At(i * 6 + 4), 2 * iHeads, 2, None, 0, iHeads)) return false; if(IsStopped() || !BatchNormInsideGradient(cLPI.At(i * 6), cLPI.At(i * 6 + 3), cLPI_Weights.At(i * (optimization == SGD ? 5 : 7) + 1), cLPI.At(i * 6 + 1), cLPI.At(i * 6 + 4), LReLU)) return false; if(IsStopped() || !ConvolutionInputGradients(cLPI_Weights.At(i * (optimization == SGD ? 5 : 7)), cLPI.At(i * 6 + 3), AO_Tensors.At(i * 2), AO_Tensors.At(i * 2 + 1), iLPIWindow, iHeads, None, 0, iLPIStep)) return false; temp = AO_Tensors.At(i * 2 + 1); //--- Sum and normilize gradients if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, false, 0, 0, 0, 1)) return false;
最后一步是通过关注度模块传播误差梯度。
//--- Passing gradient to query, key and value if(IsStopped() || !XCiTInsideGradients(cConcat.At(i * 2), cConcat.At(i * 2 + 1), S_Tensors.At(i * 2), temp)) return false;
不过,于此我们得到 3 个实体的误差梯度级联缓冲区:Query、Key 和 Value。但我们记得这些生成的实体是来自各种数据源。我们必须将误差梯度分配给它们。首先,我们将 1 个缓冲区切分成 2 个。
if(IsStopped() || !DeConcat(QKV_Tensors.At(i * 2 + 1), cValue.At(i * 2 + 1), cConcat.At(i * 2 + 1), 2 * iWindowKey * iHeads, iWindowKey * iHeads)) return false;
然后我们调用方法将梯度传播到相应的源数据。我们可以针对 Query 和 Key 使用继承的功能 。然而,Value 的情况要复杂一些。
//--- CBufferFloat *inp = NULL; if(i == 0) { inp = prevLayer.getOutput(); temp = prevLayer.getGradient(); } else { temp = FF_Tensors.At(i * 4 - 1); inp = FF_Tensors.At(i * 4 - 3); } if(IsStopped() || !ConvolutionInputGradients(QKV_Weights.At(i * (optimization == SGD ? 2 : 3)), QKV_Tensors.At(i * 2 + 1), inp, temp, iWindow, 2 * iWindowKey * iHeads, None)) return false;
在前馈验算期间,我强调对于所有层,我们都共用一个特征变化动态缓冲区。直接将误差梯度传送到源数据对象的梯度缓冲区,只是简单地覆盖它们,并删除之前写入其它内层的数据。因此,我们只会在第一次迭代(最后一个内层)直接写入数据。
if(i > 0) out_grad = temp; if(i == iLayers - 1) { if(IsStopped() || !ConvolutionInputGradients(cV_Weights.At(i * (optimization == SGD ? 2 : 3)), cValue.At(i * 2 + 1), Motion.getOutput(), Motion.getGradient(), iWindow, iWindowKey * iHeads, None)) return false; }
在其它情况下,我们将使用辅助缓冲区来存储临时数据,然后将新的、和以前累积的梯度相加。
else { if(IsStopped() || !ConvolutionInputGradients(cV_Weights.At(i * (optimization == SGD ? 2 : 3)), cValue.At(i * 2 + 1), Motion.getOutput(), GetPointer(TempBuffer), iWindow, iWindowKey * iHeads, None)) return false; if(IsStopped() || !SumAndNormilize(GetPointer(TempBuffer), Motion.getGradient(), Motion.getGradient(), iWindow2, false)) return false; }
将 2 个数据流的误差梯度相加,然后转入循环的下一次迭代。
if(IsStopped() || !ConvolutionInputGradients(QKV_Weights.At(i * (optimization == SGD ? 2 : 3)), QKV_Tensors.At(i * 2 + 1), inp, temp, iWindow, 2 * iWindowKey * iHeads, None)) return false; //--- Sum and normilize gradients if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, false, 0, 0, 0, 1)) return false; if(i > 0) out_grad = temp; } //--- return true; }
误差梯度成功传送到所有内层后,我们终止该方法。
受它们对最终结果的影响,在所有内部对象和源数据之间分配误差梯度后,我们必须调整模型参数将误差最小化。该过程规划于 CNeuronCrossXCiTOCL::updateInputWeights 方法当中。与上面讨论的 2 种方法类似,我们在遍历内部神经层的循环中更新内部对象的参数。
bool CNeuronCrossXCiTOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Motion) { if(CheckPointer(NeuronOCL) == POINTER_INVALID) return false; CBufferFloat *inputs = NeuronOCL.getOutput(); for(uint l = 0; l < iLayers; l++) { if(IsStopped() || !ConvolutuionUpdateWeights(QKV_Weights.At(l * (optimization == SGD ? 2 : 3)), QKV_Tensors.At(l * 2 + 1), inputs, (optimization == SGD ? QKV_Weights.At(l * 2 + 1) : QKV_Weights.At(l * 3 + 1)), (optimization == SGD ? NULL : QKV_Weights.At(l * 3 + 2)), iWindow, 2 * iWindowKey * iHeads)) return false; if(IsStopped() || !ConvolutuionUpdateWeights(cV_Weights.At(l * (optimization == SGD ? 2 : 3)), cValue.At(l * 2 + 1), inputs, (optimization == SGD ? cV_Weights.At(l * 2 + 1) : cV_Weights.At(l * 3 + 1)), (optimization == SGD ? NULL : cV_Weights.At(l * 3 + 2)), iWindow, iWindowKey * iHeads)) return false;
首先,我们更新生成 Query、Key 和 Value 实体的参数。接下来是 LPI 局部通信模块。
if(IsStopped() || !ConvolutuionUpdateWeights(cLPI_Weights.At(l * (optimization == SGD ? 5 : 7)), cLPI.At(l * 6 + 3), AO_Tensors.At(l * 2), (optimization == SGD ? cLPI_Weights.At(l * 5 + 3) : cLPI_Weights.At(l * 7 + 3)), (optimization == SGD ? NULL : cLPI_Weights.At(l * 7 + 5)), iLPIWindow, iHeads, iLPIStep)) return false; if(IsStopped() || !BatchNormUpdateWeights(cLPI_Weights.At(l * (optimization == SGD ? 5 : 7) + 1), cLPI.At(l * 6 + 4))) return false; if(IsStopped() || !ConvolutuionUpdateWeights(cLPI_Weights.At(l * (optimization == SGD ? 5 : 7) + 2), cLPI.At(l * 6 + 5), cLPI.At(l * 6 + 1), (optimization == SGD ? cLPI_Weights.At(l * 5 + 4) : cLPI_Weights.At(l * 7 + 4)), (optimization == SGD ? NULL : cLPI_Weights.At(l * 7 + 6)), 2 * iHeads, 2, iHeads)) return false;
我们伴随 FeedForward 模块完成该过程。
if(IsStopped() || !ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? 4 : 6)), FF_Tensors.At(l * 4 + 2), cLPI.At(l * 6 + 2), (optimization == SGD ? FF_Weights.At(l * 4 + 2) : FF_Weights.At(l * 6 + 2)), (optimization == SGD ? NULL : FF_Weights.At(l * 6 + 4)), iWindow, 4 * iWindow)) return false; //--- if(IsStopped() || !ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? 4 : 6) + 1), FF_Tensors.At(l * 4 + 3), FF_Tensors.At(l * 4), (optimization == SGD ? FF_Weights.At(l * 4 + 3) : FF_Weights.At(l * 6 + 3)), (optimization == SGD ? NULL : FF_Weights.At(l * 6 + 5)), 4 * iWindow, iWindow)) return false; inputs = FF_Tensors.At(l * 4 + 1); } //--- return true; }
CNeuronCrossXCiTOCL 类方法的描述到此结束。在本文的篇幅内,我们无法详细讨论该类的所有方法。您可以查看附件中的代码自行研究它们。附件包括所有类、及其方法的完整代码。它们还包含准备文章时用到的所有程序。
2.4CCMR 算法的实现
我们已经做了很多工作来实现新类。然而,这只是准备工作。现在我们继续实现我们对 CCMR 算法的愿景。请注意,这是我们对拟议方法的愿景。它也许与原始表述不同。尽管如此,我们仍尝试了实现拟议的方法来解决我们的问题。
为了实现该方法,我们创建 CNeuronCCMROCL 类,它将继承 CNeuronBaseOCL 类的基本功能。新类的结构如下所示。
class CNeuronCCMROCL : public CNeuronBaseOCL { protected: CCCMREncoder FeatureExtractor; CNeuronBaseOCL PrevFeatures; CNeuronBaseOCL Motion; CNeuronBaseOCL Temp; CCCMREncoder LocalContext; CNeuronXCiTOCL GlobalContext; CNeuronCrossXCiTOCL MotionContext; CNeuronLSTMOCL RecurentUnit; CNeuronConvOCL UpScale; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL); virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); public: CNeuronCCMROCL(void) {}; ~CNeuronCCMROCL(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_out, uint count, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual bool calcInputGradients(CNeuronBaseOCL *prevLayer); //--- virtual int Type(void) const { return defNeuronCCMROCL; } //--- methods for working with files virtual bool Save(int const file_handle); virtual bool Load(int const file_handle); virtual void SetOpenCL(COpenCLMy *obj); virtual void TrainMode(bool flag); ///< Set Training Mode Flag virtual bool Clear(void); };
此处,您可以看到传统的方法集,和许多对象,其中大部分是在上面创建的。我们创建 2 个 CCCMREncoder 类对象的实例,来提取环境和局部上下文的特征(分别为 FeatureExtractor 和 LocalContext)。
CNeuronXCiTOCL 对象实例是为了获取全局上下文(GlobalContext)。使用 CNeuronCrossXCiTOCL,我们根据 CMF(MotionContext)的特征动态对其进行调整。
为了实现环流连接,我们使用 LSTM 模块(CNeuronLSTMOCL RecurrentUnit)替代 GRU。
在实现类方法的过程中,我们将更详细地领略所有内部对象的功能。
如前,我们将类的所有内部对象声明为静态。因此,类的构造函数和析构函数保持“空”。
内部类对象是在 CNeuronCCMROCL::Init 方法中初始化。
bool CNeuronCCMROCL::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_out, uint count, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_out * count, optimization_type, batch)) return false;
在方法参数中,我们获得类架构的关键常量。在方法的主体中,我们立即调用父类的相关方法,于其中检查所接收参数,并初始化继承的对象。
父类方法成功执行后,我们转到初始化内部对象。首先,我们初始化环境当前状态的特征编码器。
if(!FeatureExtractor.Init(0, 0, OpenCL, window, 16, count, optimization, iBatch)) return false;
为了估测光流,CCMR 方法用到系统的 2 个连续状态的快照。不过,我们从略有不同的角度处理该问题。在前馈验算的每次迭代中,我们只生成 1 种环境状态的特征,并将其保存到局部缓冲区 PrevFeatures 之中。我们使用该缓冲区的值来估算后续前馈验算中的动态流。我们初始化先前状态和特征变化的局部缓冲区对象。
if(!PrevFeatures.Init(0, 1, OpenCL, 16 * count, optimization, iBatch)) return false; if(!Motion.Init(0, 2, OpenCL, 16 * count, optimization, iBatch)) return false;
为了避免不必要的数据复制,我们规划了缓冲区替换。
if(Motion.getGradientIndex() != FeatureExtractor.getGradientIndex())
Motion.SetGradientIndex(FeatureExtractor.getGradientIndex());
接下来,基于环境的当前状态,我们使用 LocalContext 编码器生成上下文特征。此处需要注意的是,我们在 2 个数据流中使用一组源数据。因此,我们需要从 2 个流中获得误差梯度。为了启动梯度求和,我们将创建一个局部数据缓冲区。
if(!Temp.Init(0, 3, OpenCL, window * count, optimization, iBatch)) return false; if(!LocalContext.Init(0, 4, OpenCL, window, 16, count, optimization, iBatch)) return false;
关注度机制将允许我们把局部环境分组到全局环境之中。
if(!GlobalContext.Init(0, 5, OpenCL, 16, 3, 4, count, 4, optimization, iBatch)) return false;
然后,根据流动态调整全局上下文。
if(!MotionContext.Init(0, 6, OpenCL, 16, 16, 3, 4, count, 4, optimization, iBatch)) return false;
最后,我们更新环流模块中的流。
if(!RecurentUnit.Init(0, 7, OpenCL, 16 * count, optimization, iBatch) || !RecurentUnit.SetInputs(16 * count)) return false;
为了降低模型的大小,我们用到了相应压缩状态的内部对象。不过,用户可能需要不同维度的数据。为了把结果带至所需的大小,我们将使用伸缩层。
if(!UpScale.Init(0, 8, OpenCL, 16, 16, window_out, count, optimization, iBatch)) return false;
为了避免不必要的数据复制,我们规划了数据缓冲区的替换。
if(UpScale.getGradientIndex() != getGradientIndex()) SetGradientIndex(UpScale.getGradientIndex()); if(UpScale.getOutputIndex() != getOutputIndex()) Output.BufferSet(UpScale.getOutputIndex()); //--- return true; }
前馈算法在 CNeuronCCMROCL::feedForward 方法中实现。在参数中,前馈方法接收指向包含原始数据的上一层对象的指针。
bool CNeuronCCMROCL::feedForward(CNeuronBaseOCL *NeuronOCL) { //--- Delta Features if(!SumAndNormilize(FeatureExtractor.getOutput(), FeatureExtractor.getOutput(), PrevFeatures.getOutput(), 1, false, 0, 0, 0, -0.5f)) return false;
在方法的主体中,在开始任何操作之前,我们将环境状态符号编码器的结果缓冲区的内容传送到前面的状态缓冲区。在迭代开始之前,缓冲区包含上一个前馈验算的结果。
请注意,在传送数据时,我们会将特征属性的符号更改为相反的符号。
保存数据后,我们通过状态编码器运行前馈验算。
if(!FeatureExtractor.FeedForward(NeuronOCL)) return false;
在 FeatureExtractor 的前馈验算成功后,我们得到 2 个后续条件的特征,可以判定偏差。出于简单化,我们将简单地采用功能的差异。在保存之前的状态时,我们谨慎地更改了特征的符号。现在,为了获得状态的差异,我们可以添加缓冲区的内容。
if(!SumAndNormilize(FeatureExtractor.getOutput(), PrevFeatures.getOutput(), Motion.getOutput(), 1, false, 0, 0, 0, 1.0f)) return false;
下一步是生成本地上下文特征。
if(!LocalContext.FeedForward(NeuronOCL)) return false;
我们提取全局上下文。
if(!GlobalContext.FeedForward(GetPointer(LocalContext))) return false;
并根据变化的动态进行调整。
if(!MotionContext.FeedForward(GetPointer(GlobalContext), Motion.getOutput())) return false;
接下来,我们调整环流模块中的流。
//--- Flow if(!RecurentUnit.FeedForward(GetPointer(MotionContext))) return false;
将数据伸缩到所需的大小。
if(!UpScale.FeedForward(GetPointer(RecurentUnit))) return false; //--- return true; }
在实现过程中,不要忘记在每一步控制流程。
反向验算算法在 CNeuronCCMROCL::calcInputGradients 方法中实现。与其它类中同名的方法类似,方法参数提供上一层对象的索引。在方法的主体中,我们依次调用内部对象的相应方法。不过,对象的顺序将与直接验算逆反。
bool CNeuronCCMROCL::calcInputGradients(CNeuronBaseOCL *prevLayer) { if(!UpScale.calcInputGradients(GetPointer(RecurentUnit))) return false;
首先,我们将误差梯度传播到伸缩层。然后遍历环流模块。
if(!RecurentUnit.calcInputGradients(GetPointer(MotionContext))) return false;
接下来,我们将误差梯度按顺序传播贯穿上下文转换的所有阶段。
if(!MotionContext.calcInputGradients(GetPointer(GlobalContext), GetPointer(Motion))) return false; if(!GlobalContext.calcInputGradients(GetPointer(LocalContext))) return false; if(!LocalContext.calcInputGradients(GetPointer(Temp))) return false;
通过替换数据缓冲区,特征动态的误差梯度被传输到状态特征编码器。我们将误差梯度通过编码器传播到前一层的缓冲区。
if(!FeatureExtractor.calcInputGradients(prevLayer)) return false;
加上来自上下文编码器的误差梯度。
if(!SumAndNormilize(prevLayer.getGradient(), Temp.getGradient(), prevLayer.getGradient(), 1, false, 0, 0, 0, 1.0f)) return false; //--- return true; }
更新模型参数的方法并不难。我们按顺序更新内部对象的参数。
bool CNeuronCCMROCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(!FeatureExtractor.UpdateInputWeights(NeuronOCL)) return false; if(!LocalContext.UpdateInputWeights(NeuronOCL)) return false; if(!GlobalContext.UpdateInputWeights(GetPointer(LocalContext))) return false; if(!MotionContext.UpdateInputWeights(GetPointer(GlobalContext), Motion.getOutput())) return false; if(!RecurentUnit.UpdateInputWeights(GetPointer(MotionContext))) return false; if(!UpScale.UpdateInputWeights(GetPointer(RecurentUnit))) return false; //--- return true; }
注意,该类包含一个环流模块和一个保存先前状态的缓冲区。因此,我们需要重新定义清除环流分量 CNeuronCCMROCL::Clear 的方法。此处,我们调用同名的环流模块方法,并用零值填充 FeatureExtractor 结果缓冲区。
bool CNeuronCCMROCL::Clear(void) { if(!RecurentUnit.Clear()) return false; //--- CBufferFloat *temp = FeatureExtractor.getOutput(); temp.BufferInit(temp.Total(), 0); if(!temp.BufferWrite()) return false; //--- return true; }
注意,我们清除的是编码器结果缓冲区,而非之前的状态缓冲区。在前馈验算方法伊始,我们将数据从编码器结果缓冲区复制到前一个状态缓冲区。
实现 CCMR 方法的主要方法到此结束。我们做了很多工作,但文章的篇幅有限。因此,我建议您熟悉附件中辅助方法的算法。在那里,您将找到所有类的完整代码,及实现 CCMR 方法的方式。此外,在附件中,您将找到准备文章时的所有程序的完整代码。我们转到研究模型训练架构。
2.5. 模型架构
转到模型架构的讲述,我想提一下 CCMR 方法只影响了环境状态编码器。
在 CreateDescriptions 方法中提供了我们将要训练的模型架构,在该方法的参数中,我们将提供 3 个动态数组来记录编码器、扮演者、和评论者的架构。
bool CreateDescriptions(CArrayObj *encoder, CArrayObj *actor, CArrayObj *critic) { //--- CLayerDescription *descr; //--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) return false; } if(!actor) { actor = new CArrayObj(); if(!actor) return false; } if(!critic) { critic = new CArrayObj(); if(!critic) return false; }
在方法的主体中,我们检查收到的指针,并在必要时创建新的对象实例。
我们向编码器提供环境当前状态的原始数据。
//--- Encoder encoder.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; int prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
接收到的数据在批量常规化层中进行预处理。
//--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = MathMax(1000, GPTBars); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
之后,我们形成一个状态嵌入栈。
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronEmbeddingOCL; { int temp[] = {prev_count}; ArrayCopy(descr.windows, temp); } prev_count = descr.count = GPTBars; int prev_wout = descr.window_out = EmbeddingSize / 2; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = prev_count; descr.step = descr.window = prev_wout; prev_wout = descr.window_out = EmbeddingSize; if(!encoder.Add(descr)) { delete descr; return false; }
我们将位置编码添加到生成的嵌入之中。
//--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronPEOCL; descr.count = prev_count; descr.window = prev_wout; if(!encoder.Add(descr)) { delete descr; return false; }
编码器架构中的最后一个是新的 CNeuronCCMROCL 模块,它本身非常复杂,需要额外的处理。
//--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronCCMROCL; descr.count = prev_count; descr.window = prev_wout; descr.window_out = EmbeddingSize; if(!encoder.Add(descr)) { delete descr; return false; }
我在此用到了前几篇文章中的扮演者和评论者架构,没有做任何更改。您可以在此处找到模型架构的详细说明。此外,附件中还标书了模型的完整架构。现在,我们转到最后阶段,来测试已完成的工作。
3. 测试
在本文的前几节中,我们领略了 CCMR 方法,并利用 MQL5 实现了所提议方法。现在是时候在实践中测试上述工作的结果了。如常,我们采用 EURUSD 的历史数据(时间针帧 H1)来训练和测试模型。这些模型是依据 2023 年前 7 个月的历史数据训练的。为了在 MetaTrader 5 策略测试器中测试经过训练的模型,我采用的是 2023 年 8 月的历史数据。
在本文中,我训练模型时采用之前文章中收集的训练数据集。在训练过程中,我设法获得了一个能够在训练集上产生盈利的模型。
在测试期间,该模型进行了 21 笔交易,其中 52.3% 以盈利了结。最大和平均盈利交易都超过了亏损交易的相应指标。结果是盈利因子为 1.22
结束语
在本文中,我们讨论了一种称为 CCMR 的光流估测方法,其结合了基于上下文的运动聚合概念、和多尺度粗略到精细方式的优势。这会生成详细的流向图,即使在受阻区域也非常准确。
该方法的作者提议一种两阶段的运动分组策略,其中首先计算全局上下文特征。然后,这些会在所有尺度上迭代地引导运动特性。这令基于 XCiT 的算法可以处理从粗略到精细的所有尺度,同时保留特定尺度的内容。
在本文的实践部分,我们利用 MQL5 实现了所提议方法。我们在 MetaTrader 5 策略测试器中依据真实数据训练和测试了模型。获得的结果表明了所提议方法的有效性。
不过,我要提醒您,本文中讲述的所有程序都具有信息性,仅用于演示所提议的方法。
参考
文中所用程序
# | 已发行 | 类型 | 说明 |
---|---|---|---|
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/14505
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。


