神经网络变得轻松(第四十六部分):条件导向目标强化学习(GCRL)·进阶篇
◍ 拼接层的权重与显存对象
在 MT5 的 OpenCL 神经网络封装里,拼接类 CNeuronConcatenate 负责把两套输入(例如价格序列特征与成交量特征)沿神经元维度合并。它继承自 CNeuronBaseOCL,除基类缓冲外,单独持有了 ConcWeights、ConcDeltaWeights、ConcFirstMomentum、ConcSecondMomentum 四块浮点缓冲,分别对应拼接分支的权重、权重增量与一阶/二阶动量。 构造函数里 i_SecondInputs 初始化为 0,四个缓冲全部 new 成 CBufferFloat;析构时逐个判空再 delete,避免重复释放。注意原文中 ConcSecondMomentum 的 new 少了括号写成 CBufferFloat,编译可能通过但风格不一致,复制时建议补成 CBufferFloat()。 Init 方法先调基类 Init 拿到输出数与优化类型,再把外部传入的 numInputs2 强转给 i_SecondInputs。若 ConcWeights 为空会再 new 一次——这段防御性代码在构造函数已分配的情况下略显冗余,实盘跑前可注释掉观察是否影响显存占用。外汇与贵金属模型用此类结构时波动率高,拼接分支权重初始化偏差可能放大回测误差,需以真实 tick 数据验证。
sum *= class="num">0.01f; break; class="kw">default: break; } matrix_o[i] = sum; } class CNeuronConcatenate : class="kw">public CNeuronBaseOCL { class="kw">protected: class="type">int i_SecondInputs; CBufferFloat *ConcWeights; CBufferFloat *ConcDeltaWeights; CBufferFloat *ConcFirstMomentum; CBufferFloat *ConcSecondMomentum; class="kw">public: CNeuronConcatenate(class="type">void); ~CNeuronConcatenate(class="type">void); class="kw">virtual class="type">bool Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint numNeurons, class="type">uint inputs1, class="type">uint inputs2, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput); class="kw">virtual class="type">bool calcHiddenGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient); class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput); class=class="str">"cmt">//--- methods for working with files class="kw">virtual class="type">bool Save(class="type">int const file_handle); class="kw">virtual class="type">bool Load(class="type">int const file_handle); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) const { class="kw">return defNeuronConcatenate; } class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); }; CNeuronConcatenate::CNeuronConcatenate(class="type">void) : i_SecondInputs(class="num">0) { ConcWeights = new CBufferFloat(); ConcDeltaWeights = new CBufferFloat(); ConcFirstMomentum = new CBufferFloat(); ConcSecondMomentum = new CBufferFloat; } CNeuronConcatenate::~CNeuronConcatenate() { if(!!ConcWeights) class="kw">delete ConcWeights; if(!!ConcDeltaWeights) class="kw">delete ConcDeltaWeights; if(!!ConcFirstMomentum) class="kw">delete ConcFirstMomentum; if(!!ConcSecondMomentum) class="kw">delete ConcSecondMomentum; } class="type">bool CNeuronConcatenate::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint numNeurons, class="type">uint numInputs1, class="type">uint numInputs2, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, numNeurons, optimization_type, batch)) class="kw">return class="kw">false; i_SecondInputs = (class="type">int)numInputs2; if(!ConcWeights) { ConcWeights = new CBufferFloat(); if(!ConcWeights)
「拼接层权重初始化的两种优化分支」
这段 CNeuronConcatenate 的初始化逻辑,核心是先按 (numInputs1+numInputs2+1)*numNeurons 算出总连接数,再为 OpenCL 缓冲分配显存。缩放系数 k 取 1/sqrt(numNeurons+1),权重被限制在 [-k, k] 区间内再乘 WeightsMultiplier,能压住深层网络里梯度爆炸的概率。 当优化器是 SGD 时,只建一个 DeltaWeights 缓冲并清零,顺手把一阶、二阶动量指针释放掉;换成其他优化器(如 Adam 类)则反过来删掉 DeltaWeights,新建 FirstMomentum 与 SecondMomentum 两个缓冲,都走 BufferInit(count,0) 再 BufferCreate(OpenCL)。 直接在 MT5 里把这段代码贴进自定义神经元类,改 numNeurons 从 16 调到 64,能肉眼看到 Reserve 申请的浮点数量从 81 倍输入维数跳到 325 倍,显存占用斜率明显变陡。外汇与贵金属模型跑这套,杠杆与滑点会放大回测误差,属高风险验证。
class="kw">return class="kw">false; } class="type">int count = (class="type">int)((numInputs1 + numInputs2 + class="num">1) * numNeurons); if(!ConcWeights.Reserve(count)) class="kw">return class="kw">false; class="type">class="kw">float k = (class="type">class="kw">float)(class="num">1.0 / sqrt(numNeurons + class="num">1.0)); for(class="type">int i = class="num">0; i < count; i++) { if(!ConcWeights.Add((class="num">2 * GenerateWeight()*k - k)*WeightsMultiplier)) class="kw">return class="kw">false; } if(!ConcWeights.BufferCreate(OpenCL)) class="kw">return class="kw">false; if(optimization == SGD) { if(!ConcDeltaWeights) { ConcDeltaWeights = new CBufferFloat(); if(!ConcDeltaWeights) class="kw">return class="kw">false; } if(!ConcDeltaWeights.BufferInit(count, class="num">0)) class="kw">return class="kw">false; if(!ConcDeltaWeights.BufferCreate(OpenCL)) class="kw">return class="kw">false; if(!!ConcFirstMomentum) class="kw">delete ConcFirstMomentum; if(!!ConcSecondMomentum) class="kw">delete ConcSecondMomentum; } else { if(!!ConcDeltaWeights) class="kw">delete ConcDeltaWeights; class=class="str">"cmt">//--- if(!ConcFirstMomentum) { ConcFirstMomentum = new CBufferFloat(); if(CheckPointer(ConcFirstMomentum) == POINTER_INVALID) class="kw">return class="kw">false; } if(!ConcFirstMomentum.BufferInit(count, class="num">0)) class="kw">return class="kw">false; if(!ConcFirstMomentum.BufferCreate(OpenCL)) class="kw">return class="kw">false; class=class="str">"cmt">//--- if(!ConcSecondMomentum) { ConcSecondMomentum = new CBufferFloat(); if(!ConcSecondMomentum) class="kw">return class="kw">false; } if(!ConcSecondMomentum.BufferInit(count, class="num">0)) class="kw">return class="kw">false; if(!ConcSecondMomentum.BufferCreate(OpenCL)) class="kw">return class="kw">false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronConcatenate::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) { if(!OpenCL || !NeuronOCL || !SecondInput) class="kw">return class="kw">false; if(SecondInput.Total() < i_SecondInputs)
拼接前馈核的参数绑定与执行
在 MT5 的 OpenCL 神经网络封装里,ConcatFeedForward 内核负责把两个输入缓冲拼接后做前向计算。调用前必须逐个把权重、两个输入矩阵和输出矩阵的 GPU 缓冲索引绑进内核参数,任何一步 SetArgumentBuffer 失败都直接返回 false 并打出错误行号。 下面这段是参数绑定的核心片段,注意 def_k_cff_matrix_w / i1 / i2 / o 四个缓冲分别对应拼接权重、第一路输出、第二路输入和本层输出: if(!OpenCL.SetArgumentBuffer(def_k_ConcatFeedForward, def_k_cff_matrix_w, ConcWeights.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgumentBuffer(def_k_ConcatFeedForward, def_k_cff_matrix_i1, NeuronOCL.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgumentBuffer(def_k_ConcatFeedForward, def_k_cff_matrix_i2, SecondInput.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgumentBuffer(def_k_ConcatFeedForward, def_k_cff_matrix_o, Output.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } 标量参数也要显式传:第一路神经元数 NeuronOCL.Neurons()、第二路长度 i_SecondInputs、激活函数类型 activation,三者用 SetArgument 以 int 形式写入。漏掉任意一个,内核启动后结果可能全零。 全局工作项数量由 Output.Total() 决定,偏移固定为 0。Execute 时若返回 false,同样打印错误码与行号——在 MT5 策略测试器日志里看到这类行号,基本就是某缓冲没创建成功或索引为负。外汇与贵金属行情下用 GPU 推理虽快,但显存分配失败概率随品种数上升而增加,属高风险调试环节。
if(SecondInput.GetIndex() < class="num">0 && !SecondInput.BufferCreate(OpenCL)) class="kw">return class="kw">false; if(!OpenCL.SetArgumentBuffer(def_k_ConcatFeedForward, def_k_cff_matrix_w, ConcWeights.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return class="kw">false; } if(!OpenCL.SetArgumentBuffer(def_k_ConcatFeedForward, def_k_cff_matrix_i1, NeuronOCL.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return class="kw">false; } if(!OpenCL.SetArgumentBuffer(def_k_ConcatFeedForward, def_k_cff_matrix_i2, SecondInput.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return class="kw">false; } if(!OpenCL.SetArgumentBuffer(def_k_ConcatFeedForward, def_k_cff_matrix_o, Output.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return class="kw">false; } if(!OpenCL.SetArgument(def_k_ConcatFeedForward, def_k_cff_inputs1, (class="type">int)NeuronOCL.Neurons())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return class="kw">false; } if(!OpenCL.SetArgument(def_k_ConcatFeedForward, def_k_cff_inputs2, (class="type">int)i_SecondInputs)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return class="kw">false; } if(!OpenCL.SetArgument(def_k_ConcatFeedForward, def_k_cff_activation, (class="type">int)activation)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return class="kw">false; } class="type">uint global_work_offset[class="num">1] = {class="num">0}; class="type">uint global_work_size[class="num">1]; global_work_size[class="num">0] = Output.Total(); if(!OpenCL.Execute(def_k_ConcatFeedForward, class="num">1, global_work_offset, global_work_size)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return class="kw">false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronBaseOCL::FeedForward(CObject *SourceObject, CBufferFloat *SecondInput = NULL) { if(CheckPointer(SourceObject) == POINTER_INVALID) class="kw">return class="kw">false; class=class="str">"cmt">//---
◍ 给行情特征网络搭骨架
在 MT5 的 OpenCL 神经网络封装里,CreateDescriptions 负责把每一层的类型、节点数、窗口与优化器写进 actor 描述数组,相当于给后续训练定下拓扑。上面这段代码从输入层一路堆到第五层,全是 defNeuron*OCL 系列,说明整套计算走的是显卡加速路径。 输入层节点数由 HistoryBars * BarDescr 决定,比如回看 50 根 K 线、每根取 6 个描述量,那 prev_count 就是 300;第一层 BatchNorm 保持同数并设 batch=1000,意味着每次送 1000 个样本做归一化。 卷积层用了 window=2、step=1、window_out=4 的配置,节点数每层减 1,激活统一 LReLU,优化器全 ADAM。Proof 层则只做 window=4、step=4 的下采样,不改变计数。 跑之前先确认 actor 指针,为空就 new 一个 CArrayObj,任意一步 Add 失败立即 delete 已建描述并返回 false——这种写法在 EA 初始化阶段能避免悬空层描述拖垮后续 feedForward。外汇与贵金属波动剧烈,用这类网络做信号需清醒:过拟合概率不低,上实盘前务必用历史数据交叉验证。
CNeuronBaseOCL *temp = NULL; if(Type() == defNeuronConcatenate) { temp = SourceObject; CNeuronConcatenate *concat = GetPointer(this); class="kw">return concat.feedForward(temp, SecondInput); } class="type">bool CreateDescriptions(CArrayObj *actor) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!actor) { actor = new CArrayObj(); if(!actor) class="kw">return class="kw">false; } class=class="str">"cmt">//--- Actor actor.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; class="type">int prev_count = descr.count = (HistoryBars * BarDescr); descr.window = class="num">0; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = class="num">1000; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronConvOCL; prev_count = descr.count = prev_count - class="num">1; descr.window = class="num">2; descr.step = class="num">1; descr.window_out = class="num">4; descr.activation = LReLU; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronProofOCL; prev_count = descr.count = prev_count; descr.window = class="num">4; descr.step = class="num">4; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronConvOCL; prev_count = descr.count = prev_count - class="num">1; descr.window = class="num">2; descr.step = class="num">1; descr.window_out = class="num">4; descr.activation = LReLU; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">5 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronProofOCL; prev_count = descr.count = prev_count; descr.window = class="num">4; descr.step = class="num">4; if(!actor.Add(descr)) {
「actor 网络后段的层堆叠与参数落点」
上面这段是 actor 网络从第 6 层到第 13 层的实际组装代码,每一层都先 new 一个 CLayerDescription,填完字段再丢给 actor.Add(),任一环节失败就 delete 并 return false,避免野指针残留。 第 6 层是 256 个 defNeuronBaseOCL 节点,优化器 ADAM、激活 TANH;第 7 层降到 128 节点、换 LReLU;第 8 层输出 2*NSkills 个节点且激活为 None,作为动作均值与方差的载体。 第 9 层用 defNeuronVAEOCL 接 NSkills 个节点做变分重参数,第 10 层 defNeuronConcatenate 把 256 节点、窗口 prev_count、步长 AccountDescr 的账户状态拼回来,再经第 11、12 层两个 256 节点 LReLU 层提炼。 第 13 层是 defNeuronFQF 类型,输出 NActions 个分位数,window_out 硬编码为 32,这套结构在 MT5 里跑时若 NSkills 或 NActions 改动,必须同步检查第 8、9、13 层的 count 依赖,否则 Add 可能静默失败。外汇与贵金属行情高波动,这类网络推理仅作概率参考,实盘须控仓。
if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; descr.count = class="num">256; descr.optimization = ADAM; descr.activation = TANH; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">7 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; descr.count = class="num">128; descr.activation = LReLU; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">8 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; descr.count = class="num">2 * NSkills; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">9 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronVAEOCL; descr.count = NSkills; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">10 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronConcatenate; descr.count = class="num">256; descr.window=prev_count; descr.step=AccountDescr; descr.optimization = ADAM; descr.activation = TANH; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">11 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; descr.count = class="num">256; descr.activation = LReLU; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">12 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; descr.count = class="num">256; descr.activation = LReLU; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">13 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronFQF; descr.count = NActions; descr.window_out = class="num">32; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr;
把持仓状态喂给模型前的最后一环
这段逻辑做的是收盘前的账户快照与特征拼装:先抓 ACCOUNT_BALANCE 和 ACCOUNT_EQUITY 写进 sState.account[0]、[1],再把当前品种的多空持仓量、浮动盈亏分别累加进 [2]~[5]。 position_discount 这一项值得盯一眼:它用 (当前时间 - 开仓时间) 乘一个 1/(60*60*10) 的系数,再乘该仓绝对值盈亏做减法。相当于给「老仓位拖着的浮亏」按小时衰减加权,10 小时权重衰减到约 0.1 倍,可能让模型更倾向忽略久远的微亏单。 随后和上一帧 Base.States 比出余额/净值变化率,全部塞进 Account 缓冲;Account.GetIndex()>=0 时落盘 BufferWrite(),失败直接 return 不往下走。最后 Actor.feedForward 把状态数组和账户特征送进网络推理——这一步若返回 false 同样直接 return,意味着本轮不生成新信号。 开 MT5 把这段嵌进你的 EA,把 multiplyer 的 10.0 改成 5.0 或 20.0,对比 discount 项在回测里的波动,能直观看到时间衰减对信号触发的松紧影响。外汇与贵金属杠杆高,这类特征权重改动可能放大回撤,先用历史数据验证再上实盘。
class="kw">return class="kw">false; } class=class="str">"cmt">//--- class="kw">return true; } sState.account[class="num">0] = (class="type">class="kw">float)AccountInfoDouble(ACCOUNT_BALANCE); sState.account[class="num">1] = (class="type">class="kw">float)AccountInfoDouble(ACCOUNT_EQUITY); class=class="str">"cmt">//--- class="type">class="kw">double buy_value = class="num">0, sell_value = class="num">0, buy_profit = class="num">0, sell_profit = class="num">0; class="type">class="kw">double position_discount = class="num">0; class="type">class="kw">double multiplyer = class="num">1.0 / (class="num">60.0 * class="num">60.0 * class="num">10.0); class="type">int total = PositionsTotal(); class="type">class="kw">datetime current = TimeCurrent(); for(class="type">int i = class="num">0; i < total; i++) { if(PositionGetSymbol(i) != Symb.Name()) class="kw">continue; class="kw">switch((class="type">int)PositionGetInteger(POSITION_TYPE)) { case POSITION_TYPE_BUY: buy_value += PositionGetDouble(POSITION_VOLUME); buy_profit += PositionGetDouble(POSITION_PROFIT); break; case POSITION_TYPE_SELL: sell_value += PositionGetDouble(POSITION_VOLUME); sell_profit += PositionGetDouble(POSITION_PROFIT); break; } position_discount -= (current - PositionGetInteger(POSITION_TIME)) * multiplyer*MathAbs(PositionGetDouble(POSITION_PROFIT)); } sState.account[class="num">2] = (class="type">class="kw">float)buy_value; sState.account[class="num">3] = (class="type">class="kw">float)sell_value; sState.account[class="num">4] = (class="type">class="kw">float)buy_profit; sState.account[class="num">5] = (class="type">class="kw">float)sell_profit; sState.account[class="num">6] = (class="type">class="kw">float)position_discount; State.AssignArray(sState.state); Account.Clear(); class="type">class="kw">float PrevBalance = (Base.Total <= class="num">0 ? sState.account[class="num">0] : Base.States[Base.Total - class="num">1].account[class="num">0]); class="type">class="kw">float PrevEquity = (Base.Total <= class="num">0 ? sState.account[class="num">1] : Base.States[Base.Total - class="num">1].account[class="num">1]); Account.Add((sState.account[class="num">0] - PrevBalance) / PrevBalance); Account.Add(sState.account[class="num">1] / PrevBalance); Account.Add((sState.account[class="num">1] - PrevEquity) / PrevEquity); Account.Add(sState.account[class="num">2]); Account.Add(sState.account[class="num">3]); Account.Add(sState.account[class="num">4] / PrevBalance); Account.Add(sState.account[class="num">5] / PrevBalance); Account.Add(sState.account[class="num">6] / PrevBalance); if(Account.GetIndex()>=class="num">0) if(!Account.BufferWrite()) class="kw">return; if(!Actor.feedForward(GetPointer(State), class="num">1, class="kw">false, GetPointer(Account))) class="kw">return;