交易中的神经网络:配备注意力机制(MASAAT)的智代融汇(终章)·进阶篇
(2/3)· 从 TA 模块转置到投资组合生成,拆解多智代注意力框架的工程落地细节
智代怎么拼出自己的仓位权重
CSA 模块吐出资产间依赖,TA 模块吐出时间点间依赖,两者经注意力整合后,每个智代先形成带时间信息的资产嵌入,再过一个全连通层输出权重矢量,所有元素合计严格等于 1。实际落地时,我们没照搬原框架数学外的表述,而是把模块输出 reinterpret 成隐藏状态嵌入,方便后续接交易动作(方向、规模、止损止盈),因为原始输入里根本没有账户状态和产品动态。 核心类 CNeuronPortfolioGenerator 继承 CNeuronBaseOCL,内部对象全声明为静态,构造析构留空,初始化全挤在 Init 里。Init 必须收到 5 个大于零的参数:assets(CSA 资产数)、time_points(TA 时间点数)、dimension(嵌入维度,两模块共用)、agents(智代数)、projection(输出状态大小)。父类初始化传 projection,成功后存内部变量,再初始化内部对象——注意 TA 输出是 [智代, 时间, 嵌入] 三维张量且被用两次,所以要用 CNeuronTransposeVRCOCL 对最后两维做转置。 前馈时有两个数据源,TA 结果用两次,故设为主流。计算顺序:第二数据源乘第一数据源转置 → Softmax 归一化(每个资产每智代独立归一,头数 = 资产数 × 智代数)→ 乘原始 TA 流 → 父类全连通层投影到子空间。反向传播 calcInputGradients 完全逆着前馈走,但主流入梯度来自两条流,中间值暂存置换层辅助缓冲区,最后按主流激活函数导数调整再回传 CSA 与 TA。 参数调错一个就直接初始化失败,建议开 MT5 把 assets 和 agents 先设小(如 3 和 2)跑通前馈,再扩到实盘品种数。外汇与贵金属杠杆高,隐藏状态仅表征市场态势,不等于下单信号,需接风控层。
class CNeuronPortfolioGenerator : class="kw">public CNeuronBaseOCL { class="kw">protected: class="type">uint iAssets; class="type">uint iTimePoints; class="type">uint iAgents; class="type">uint iDimension; class=class="str">"cmt">//--- CNeuronBaseOCL cAssetTime[class="num">2]; CNeuronTransposeVRCOCL cTransposeVRC; CNeuronSoftMaxOCL cSoftMax; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override { class="kw">return class="kw">false; } class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) class="kw">override; class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *prevLayer) class="kw">override { class="kw">return class="kw">false; } class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) class="kw">override; class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">public: CNeuronPortfolioGenerator(class="type">void) {}; ~CNeuronPortfolioGenerator(class="type">void) {}; class=class="str">"cmt">//--- class="kw">virtual class="type">bool Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint assets, class="type">uint time_points, class="type">uint dimension, class="type">uint agents, class="type">uint projection,
「组合生成器的初始化与前向传播骨架」
CNeuronPortfolioGenerator 在 MT5 的 OpenCL 神经网络框架里,承担把多资产、多时间点的特征压缩成组合权重的角色。它的 Init 方法先卡死四个底线参数:assets、time_points、dimension、agents 任一为 0 直接返回 false,避免后续矩阵维度算出 0 长度缓冲。 Init 里依次挂了转置层 cTransposeVRC(维度 iAgents×iTimePoints×iDimension)、两个 AssetTime 缓冲与 SoftMax 头。注意 cSoftMax.SetHeads(iAssets * iAgents) 这行:把 softmax 拆成多个头,意味着输出不是单向量而是分资产分智能体的概率分布,调参时 head 数必须和前面 iAssets*iAgents 对齐,否则 FeedForward 会越界。 前向传播 feedForward 接收 SecondInput 作为第二路特征。流程是先转置 VRC 张量,再做 MatMul 把 SecondInput 与转置结果乘进 cAssetTime[0],维度顺序是 (iAssets, iDimension) × (iDimension, iTimePoints) 按 iAgents 并行。外汇与贵金属行情的高波动下,这种张量并行能压低单根 K 线的噪声权重,但 GPU 显存占用随 iAgents 线性放大,实盘前建议在策略测试器里先跑小 batch 验证。 下面这段是类声明与两个核心方法的原码,逐行看能少踩很多 OpenCL 缓冲绑定的坑。
ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) class="kw">const class="kw">override { class="kw">return defNeuronPortfolioGenerator; } class=class="str">"cmt">//--- class="kw">virtual class="type">bool Save(class="type">int class="kw">const file_handle) class="kw">override; class="kw">virtual class="type">bool Load(class="type">int class="kw">const file_handle) class="kw">override; class="kw">virtual class="type">bool WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau) class="kw">override; class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj) class="kw">override; }; class="type">bool CNeuronPortfolioGenerator::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint assets, class="type">uint time_points, class="type">uint dimension, class="type">uint agents, class="type">uint projection, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(assets <= class="num">0 || time_points <= class="num">0 || dimension <= class="num">0 || agents <= class="num">0) class="kw">return class="kw">false; if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, projection, optimization_type, batch)) class="kw">return class="kw">false; iAssets = assets; iTimePoints = time_points; iDimension = dimension; iAgents = agents; if(!cTransposeVRC.Init(class="num">0, class="num">0, OpenCL, iAgents, iTimePoints, iDimension, optimization, iBatch)) class="kw">return class="kw">false; if(!cAssetTime[class="num">0].Init(class="num">0, class="num">1, OpenCL, iAssets * iTimePoints * iAgents, optimization, iBatch)) class="kw">return class="kw">false; cAssetTime[class="num">0].SetActivationFunction(None); if(!cSoftMax.Init(class="num">0, class="num">2, OpenCL, cAssetTime[class="num">0].Neurons(), optimization, iBatch)) class="kw">return class="kw">false; cSoftMax.SetHeads(iAssets * iAgents); if(!cAssetTime[class="num">1].Init(Neurons(), class="num">3, OpenCL, iAssets * iDimension * iAgents, optimization, iBatch)) class="kw">return class="kw">false; cAssetTime[class="num">1].SetActivationFunction(None); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronPortfolioGenerator::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) { if(!SecondInput) class="kw">return class="kw">false; class=class="str">"cmt">//--- if(!cTransposeVRC.FeedForward(NeuronOCL)) class="kw">return class="kw">false; if(!MatMul(SecondInput, cTransposeVRC.getOutput(), cAssetTime[class="num">0].getOutput(), iAssets, iDimension, iTimePoints, iAgents)) class="kw">return class="kw">false; if(!cSoftMax.FeedForward(cAssetTime[class="num">0].AsObject()))
◍ 组合层反向传播里的矩阵梯度链
这一段是组合生成器(PortfolioGenerator)在训练时回传梯度的核心实现,直接决定了多资产权重矩阵能否在 OpenCL 上正确更新。
class="type">bool CNeuronPortfolioGenerator::calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = -class="num">1) { if(!NeuronOCL || !SecondGradient || !SecondInput) class="kw">return class="kw">false; if(!CNeuronBaseOCL::calcInputGradients(cAssetTime[class="num">1].AsObject())) class="kw">return class="kw">false; if(!MatMulGrad(cSoftMax.getOutput(), cSoftMax.getGradient(), NeuronOCL.getOutput(), cTransposeVRC.getPrevOutput(), cAssetTime[class="num">1].getGradient(), iAssets, iTimePoints, iDimension, iAgents)) class="kw">return class="kw">false; if(!cAssetTime[class="num">0].calcHiddenGradients(cSoftMax.AsObject())) class="kw">return class="kw">false; if(!MatMulGrad(SecondInput, SecondGradient, cTransposeVRC.getOutput(), cTransposeVRC.getGradient(), cAssetTime[class="num">0].getGradient(), iAssets, iDimension, iTimePoints, iAgents)) class="kw">return class="kw">false; if(SecondActivation != None) if(!DeActivation(SecondInput, SecondGradient, SecondGradient, SecondActivation)) class="kw">return class="kw">false; if(!NeuronOCL.calcHiddenGradients(cTransposeVRC.AsObject()) || !SumAndNormilize(NeuronOCL.getGradient(), cTransposeVRC.getPrevOutput(), NeuronOCL.getGradient(), iDimension, class="kw">false, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return class="kw">false; if(NeuronOCL.Activation() != None) if(!DeActivation(NeuronOCL.getOutput(), cTransposeVRC.getPrevOutput(), cTransposeVRC.getPrevOutput(), NeuronOCL.Activation())) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; }
class="type">bool CNeuronPortfolioGenerator::calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = -class="num">1) { if(!NeuronOCL || !SecondGradient || !SecondInput) class="kw">return class="kw">false; if(!CNeuronBaseOCL::calcInputGradients(cAssetTime[class="num">1].AsObject())) class="kw">return class="kw">false; if(!MatMulGrad(cSoftMax.getOutput(), cSoftMax.getGradient(), NeuronOCL.getOutput(), cTransposeVRC.getPrevOutput(), cAssetTime[class="num">1].getGradient(), iAssets, iTimePoints, iDimension, iAgents)) class="kw">return class="kw">false; if(!cAssetTime[class="num">0].calcHiddenGradients(cSoftMax.AsObject())) class="kw">return class="kw">false; if(!MatMulGrad(SecondInput, SecondGradient, cTransposeVRC.getOutput(), cTransposeVRC.getGradient(), cAssetTime[class="num">0].getGradient(), iAssets, iDimension, iTimePoints, iAgents)) class="kw">return class="kw">false; if(SecondActivation != None) if(!DeActivation(SecondInput, SecondGradient, SecondGradient, SecondActivation)) class="kw">return class="kw">false; if(!NeuronOCL.calcHiddenGradients(cTransposeVRC.AsObject()) || !SumAndNormilize(NeuronOCL.getGradient(), cTransposeVRC.getPrevOutput(), NeuronOCL.getGradient(), iDimension, class="kw">false, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return class="kw">false; if(NeuronOCL.Activation() != None) if(!DeActivation(NeuronOCL.getOutput(), cTransposeVRC.getPrevOutput(), cTransposeVRC.getPrevOutput(), NeuronOCL.Activation())) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; }
把分散模块收进一个类里
MASAAT 各个子模块写完后,要塞进同一个类才方便跑端到端。这里选 CNeuronPortfolioGenerator 当父类,因为它本来就是流程末端的组合模块,继承它就能直接复用输出逻辑,不必再在内部 new 一个对象。新类 CNeuronMASAAT 里只声明了转置、分段线性表示(PLR)、级联层、横截面分析(CSA)和时序分析(TA)这几个成员,所有方法基本就是按顺序调内部对象。 所有成员都声明为静态,构造和析构函数留空,初始化全丢给 Init 方法。Init 的参数里包含输入窗口大小、序列长度、智代头数等常量。调用父类 Init 时有个坑:原始多模态序列会被压成三段式线性表示,时间点数量缩为原来的 1/3,所以传给父类的序列长度要除以 3。 智代数量也比阈值偏差向量长度多 1。作者早先验证过,把分段线性表示和原始序列拼起来用,模型效率倾向更高,所以多分配一个智代直接在未经修改的原始序列上干活。 前向通验 feedForward 看着平铺直叙:输入指针交给转置对象,输出和原始数据级联后送进 CSA 与 TA,再喂给父类方法。但信息流背后有分叉——转置后的原始数据和级联张量都被用了两次,这给反向传播的梯度分发埋了雷。 calcInputGradients 里先调父类方法,让 CSA 和 TA 按对输出的影响分梯度;两个模块都作用在级联张量上,所以梯度要从两条流汇回来,这里用缓冲区替换技术把第二支流的梯度提出来相加。级联张量再把梯度派给转置对象和 PLR 对象,必要时按激活函数导数修正,最后回传到输入层。外汇与贵金属信号建模属高风险,回测结论仅代表历史概率。 模型接进参与者架构时,动态窗口数组指定了分析窗口大小和隐藏状态长度,三位智代的阈值用几何级数生成,其余参数走标准值。完整代码在附件,架构基本沿用前作,不用重头捋。
class CNeuronMASAAT : class="kw">public CNeuronPortfolioGenerator { class="kw">protected: CNeuronTransposeOCL cTranspose; CNeuronPLRMultiAgentsOCL cPLR; CNeuronBaseOCL cConcat; CNeuronCrossSectionalAnalysis cCrossSectionalAnalysis; CNeuronTemporalAnalysis cTemporalAnalysis; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) class="kw">override { class="kw">return feedForward(NeuronOCL); } class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *prevLayer) class="kw">override; class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) class="kw">override { class="kw">return calcInputGradients(NeuronOCL); } class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">public: CNeuronMASAAT(class="type">void) {}; ~CNeuronMASAAT(class="type">void) {}; class=class="str">"cmt">//--- class=class="str">"cmt">//--- class="kw">virtual class="type">bool Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_key, class="type">uint heads, class="type">uint units_cout, class="type">uint layers, vector<class="type">class="kw">float> &min_distance, class="type">uint projection, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//---
「MASAAT 神经元的初始化与前向传播链路」
CNeuronMASAAT 在类声明里重写了 Type、Save、Load、WeightsUpdate 和 SetOpenCL 五个虚函数,Type 直接返回 defNeuronMASAAT 常量,用于运行时类型辨识。其余几个留作外部持久化与 OpenCL 上下文注入的接口,具体实现由派生层完成。 Init 函数是这个神经元的装配核心。它先调用基类 CNeuronPortfolioGenerator::Init,注意这里把传入的 units_cout 除以 3 再喂给基类,同时用 min_distance.Size()+1 作为某一维规模——这种维度压缩在外接多资产序列时容易踩坑,调参时建议先打印确认实际张量形状。 随后依次初始化 cTranspose、cPLR、cConcat、cCrossSectionalAnalysis、cTemporalAnalysis 五个子模块,索引号从 0 到 4 硬编码在第二个参数里。cCrossSectionalAnalysis 与 cTemporalAnalysis 的 heads 维都用了 heads/2 作为中间通道数,若 heads 为奇数会截断,回测中 heads=8 时两模块各占 4 通道。 feedForward 严格按 Transpose → PLR → Concat → 双分析模块的顺序流动。Concat 调用把前两步输出按样本轴(第三参为 1)拼起来,再同时送进横截面和时间序列两个分析分支;任一步返回 false 就中断,说明中间张量尺寸不匹配是主要失败原因,开 MT5 跑自定义 EA 时可在每行后加 Print 查断点。
class="kw">virtual class="type">int Type(class="type">void) class="kw">const class="kw">override { class="kw">return defNeuronMASAAT; } class=class="str">"cmt">//--- class="kw">virtual class="type">bool Save(class="type">int class="kw">const file_handle) class="kw">override; class="kw">virtual class="type">bool Load(class="type">int class="kw">const file_handle) class="kw">override; class="kw">virtual class="type">bool WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau) class="kw">override; class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj) class="kw">override; }; class="type">bool CNeuronMASAAT::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_key, class="type">uint heads, class="type">uint units_cout, class="type">uint layers, vector<class="type">class="kw">float> &min_distance, class="type">uint projection, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronPortfolioGenerator::Init(numOutputs, myIndex, open_cl, window, units_cout / class="num">3, window_key, (class="type">uint)min_distance.Size() + class="num">1, projection, optimization_type, batch)) class="kw">return class="kw">false; if(!cTranspose.Init(class="num">0, class="num">0, OpenCL, units_cout, window, optimization, iBatch)) class="kw">return class="kw">false; if(!cPLR.Init(class="num">0, class="num">1, OpenCL, window, units_cout, class="kw">false, min_distance, optimization, iBatch)) class="kw">return class="kw">false; if(!cConcat.Init(class="num">0, class="num">2, OpenCL, cTranspose.Neurons() + cPLR.Neurons(), optimization, iBatch)) class="kw">return class="kw">false; if(!cCrossSectionalAnalysis.Init(class="num">0, class="num">3, OpenCL, units_cout, window_key, heads, heads / class="num">2, window, layers, class="num">1, iAgents, optimization, iBatch)) class="kw">return class="kw">false; if(!cTemporalAnalysis.Init(class="num">0, class="num">4, OpenCL, units_cout, window_key, heads, heads / class="num">2, window, layers, class="num">1, iAgents, optimization, iBatch)) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronMASAAT::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cTranspose.FeedForward(NeuronOCL)) class="kw">return class="kw">false; if(!cPLR.FeedForward(cTranspose.AsObject())) class="kw">return class="kw">false; if(!Concat(cTranspose.getOutput(), cPLR.getOutput(), cConcat.getOutput(), cTranspose.Neurons(), cPLR.Neurons(), class="num">1)) class="kw">return class="kw">false; if(!cCrossSectionalAnalysis.FeedForward(cConcat.AsObject())) class="kw">return class="kw">false; if(!cTemporalAnalysis.FeedForward(cConcat.AsObject())) class="kw">return class="kw">false; class=class="str">"cmt">//---