神经网络变得简单(第 73 部分):价格走势预测 AutoBot·进阶篇
「用 OpenCL 给转置层跑 GPU 加速」
在 MT5 的神经网络封装里,CNeuronTransposeOCL 把矩阵转置丢给 GPU 内核,避免 CPU 端逐元素搬数据。Init 里先调基类 CNeuronBaseOCL::Init,传入 count * window 作为总元素数,再把 iWindow 与 iCount 存为成员,这两个值直接决定后面内核的全局工作维度。 Transpose 内核只用两行 id 取数:r = get_global_id(0)、c = get_global_id(1),输出写回位置是 c * rows + r,等价于把输入 r * cols + c 的行列互换。feedForward 中 global_work_size 设为 {iCount, iWindow},而反向的 calcInputGradients 把维度翻成 {iWindow, iCount},转置的梯度本身也是一次转置。 实盘接这套结构时,外汇与贵金属行情的高波动可能让批量矩阵维度频繁变化,建议在 MT5 策略测试器里先打印 iCount、iWindow 确认维度匹配,否则 OpenCL.Execute 会直接返回错误码并 printf 出 CL_ERROR_DESCRIPTION。
class="type">uint window, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, count * window, optimization_type, batch)) class="kw">return false; class=class="str">"cmt">//--- iWindow = window; iCount = count; class=class="str">"cmt">//--- class="kw">return true; } __kernel class="type">void Transpose(__global class="type">float *matrix_in, class=class="str">"cmt">///<[in] Input matrix __global class="type">float *matrix_out class=class="str">"cmt">///<[out] Output matrix ) { const class="type">int r = get_global_id(class="num">0); const class="type">int c = get_global_id(class="num">1); const class="type">int rows = get_global_size(class="num">0); const class="type">int cols = get_global_size(class="num">1); class=class="str">"cmt">//--- matrix_out[c * rows + r] = matrix_in[r * cols + c]; } class="type">bool CNeuronTransposeOCL::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) class="kw">return false; class=class="str">"cmt">//--- class="type">uint global_work_offset[class="num">2] = {class="num">0, class="num">0}; class="type">uint global_work_size[class="num">2] = {iCount, iWindow}; if(!OpenCL.SetArgumentBuffer(def_k_Transpose, def_k_tr_matrix_in, NeuronOCL.getOutputIndex())) class="kw">return false; if(!OpenCL.SetArgumentBuffer(def_k_Transpose, def_k_tr_matrix_out, Output.GetIndex())) class="kw">return false; if(!OpenCL.Execute(def_k_Transpose, class="num">2, global_work_offset, global_work_size)) { class="type">class="kw">string error; CLGetInfoString(OpenCL.GetContext(), CL_ERROR_DESCRIPTION, error); printf("Error of execution kernel Transpose: %d -> %s", GetLastError(), error); class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronTransposeOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) class="kw">return false; class=class="str">"cmt">//--- class="type">uint global_work_offset[class="num">2] = {class="num">0, class="num">0}; class="type">uint global_work_size[class="num">2] = {iWindow, iCount}; if(!OpenCL.SetArgumentBuffer(def_k_Transpose, def_k_tr_matrix_out, NeuronOCL.getGradientIndex())) class="kw">return false; if(!OpenCL.SetArgumentBuffer(def_k_Transpose, def_k_tr_matrix_in, Gradient.GetIndex())) class="kw">return false; if(!OpenCL.Execute(def_k_Transpose, class="num">2, global_work_offset, global_work_size)) { class="type">class="kw">string error;
用OpenCL搭一个行情编码器的层栈
在MT5里跑价格行为模型,想把历史K线送进GPU做特征提取,得先按层把网络结构描述清楚。下面这段CreateTrajNetDescriptions函数就是干这事:先清掉旧描述,再从输入层一路堆到转置层,每一层都new一个CLayerDescription并塞进CArrayObj容器。 输入层直接用defNeuronBaseOCL,节点数等于HistoryBars乘BarDescr,激活函数设None,优化器用ADAM。紧接着接BatchNorm层,batch尺寸取MathMax(1000, GPTBars)——也就是说样本数低于1000时强制按1000跑,显存占用和收敛稳定性得自己权衡。 Embedding层把prev_count长度的窗口压成GPTBars个token,输出维度交给EmbeddingSize。后面PEOCL做位置编码,MLMHAttentionOCL才是核心:step=4、window_out=16、layers=1,多头注意力的头数隐含在窗口拆分里。最后TransposeOCL翻一下张量维度,方便下游解码。 这套层栈在外汇和贵金属行情上属于高波动数据建模,过拟合概率不低,实盘前务必用历史分笔回测验证。代码里任何一层Add失败就delete并return false,MT5终端会打出具体错因,直接照着改参数即可。
class="type">bool CreateTrajNetDescriptions(CArrayObj *autobot) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!autobot) { autobot = new CArrayObj(); if(!autobot) class="kw">return false; } class=class="str">"cmt">//--- Encoder autobot.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; class="type">int prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimization = ADAM; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = MathMax(class="num">1000,GPTBars); descr.activation = None; descr.optimization = ADAM; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronEmbeddingOCL; { class="type">int temp[] = {prev_count}; ArrayCopy(descr.windows, temp); } prev_count = descr.count = GPTBars; class="type">int prev_wout = descr.window_out = EmbeddingSize; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronPEOCL; descr.count = prev_count; descr.window = prev_wout; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronMLMHAttentionOCL; descr.count = prev_count; descr.window = prev_wout; descr.step = class="num">4; descr.window_out = class="num">16; descr.layers = class="num">1; descr.optimization = ADAM; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">5 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronTransposeOCL; descr.count = prev_count; descr.window = prev_wout; if(!autobot.Add(descr)) { class="kw">delete descr;
◍ 解码器前的多层注意力堆叠
这段构建逻辑里,从第 6 层到第 12 层连续往 autobot 里灌描述符,核心是用多头注意力(MLMHAttentionOCL)配合转置层(TransposeOCL)反复做维度置换。第 6、8、10 层结构几乎一致:count 取 prev_wout、window 取 prev_count,step 固定 4、window_out 固定 16、layers 为 1,优化器全走 ADAM。 第 7 与第 9 层是纯转置,不挂优化器,只把 count/window 对调,给注意力层制造交叉视角的输入。这种对称穿插在 GPU 上跑 OCL 内核时,显存带宽占用倾向呈现周期性抖动。 第 11 层突然切到 Concatenate,把 PrecoderBars * EmbeddingSize 作为 count,window 拉到 prev_count * prev_wout,step 等于 EmbeddingSize,激活用 LReLU。第 12 层才正式把 prev_count 重置为 PrecoderBars、prev_wout 重置为 EmbeddingSize,意味着解码器入口的张量形状在这里被锚定。 任何一层 new 失败或 Add 返回否,都立刻 delete 并 return false,调用方拿不到半残网络。开 MT5 把 step 从 4 改成 2,可能看到推理延迟下降但过拟合概率上升,贵金属品种上这种敏感度高,属典型高风险调参。
class="kw">return false; } class=class="str">"cmt">//--- layer class="num">6 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronMLMHAttentionOCL; descr.count = prev_wout; descr.window = prev_count; descr.step = class="num">4; descr.window_out = class="num">16; descr.layers = class="num">1; descr.optimization = ADAM; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">7 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronTransposeOCL; descr.count = prev_wout; descr.window = prev_count; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">8 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronMLMHAttentionOCL; descr.count = prev_count; descr.window = prev_wout; descr.step = class="num">4; descr.window_out = class="num">16; descr.layers = class="num">1; descr.optimization = ADAM; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">9 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronTransposeOCL; descr.count = prev_count; descr.window = prev_wout; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">10 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronMLMHAttentionOCL; descr.count = prev_wout; descr.window = prev_count; descr.step = class="num">4; descr.window_out = class="num">16; descr.layers = class="num">1; descr.optimization = ADAM; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- Decoder class=class="str">"cmt">//--- layer class="num">11 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConcatenate; descr.count = PrecoderBars * EmbeddingSize; descr.window = prev_count * prev_wout; descr.step = EmbeddingSize; descr.activation = LReLU; descr.optimization = ADAM; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">12 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronMLMHAttentionOCL; prev_count = descr.count = PrecoderBars; prev_wout = descr.window = EmbeddingSize; descr.step = class="num">4; descr.window_out = class="num">16; descr.layers = class="num">1; descr.optimization = ADAM; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; }
「堆叠转置与多头注意力构建特征交叉」
这段网络组装代码连续注册了第 13 到 19 层,核心思路是用转置层调换特征与窗口维度,再交替塞入多头注意力层做跨维交互。外汇与贵金属行情里,这种结构可能更擅长捕捉不同周期粒度间的隐含关联,但高杠杆下模型过拟合风险同样偏高。 第 14 层用 defNeuronMLMHAttentionOCL,step 写死为 4,window_out 固定 16,layers 为 1,优化器选 ADAM;第 16 层复用同型注意力但 count 与 window 参数对调,形成对称交叉。 第 15 层是 Concatenate,把前层输出按 prev_count * prev_wout 拼接,step 指向 EmbeddingSize,激活用 LReLU;第 19 层收口用 defNeuronBaseOCL,节点数等于 PrecoderBars * 3,激活 None。 每一层都先 new CLayerDescription,若 Add 失败立即 delete 并返回 false,这种写法能在 MT5 实盘加载时避免内存泄漏。打开你的 EA 源码把这段粘进网络初始化函数,改一下 PrecoderBars 看推理延迟变化。
class=class="str">"cmt">//--- layer class="num">13 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronTransposeOCL; descr.count = prev_count; descr.window = prev_wout; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">14 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronMLMHAttentionOCL; descr.count = prev_wout; descr.window = prev_count; descr.step = class="num">4; descr.window_out = class="num">16; descr.layers = class="num">1; descr.optimization = ADAM; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">15 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConcatenate; descr.count = prev_count * prev_wout; descr.window = descr.count; descr.step = EmbeddingSize; descr.activation = LReLU; descr.optimization = ADAM; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">16 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronMLMHAttentionOCL; descr.count = prev_count; descr.window = prev_wout; descr.step = class="num">4; descr.window_out = class="num">16; descr.layers = class="num">1; descr.optimization = ADAM; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">17 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronTransposeOCL; descr.count = prev_count; descr.window = prev_wout; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">18 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronMLMHAttentionOCL; descr.count = prev_wout; descr.window = prev_count; descr.step = class="num">4; descr.window_out = class="num">16; descr.layers = class="num">1; descr.optimization = ADAM; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">19 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = PrecoderBars * class="num">3; descr.activation = None; descr.optimization = ADAM; if(!autobot.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } CNet Autobot; class="type">int OnInit() {
初始化与退出时的模型装载校验
EA 在 OnInit 里先调 ResetLastError 清掉错误码,再尝试 LoadTotalBase 读取历史学习数据;若返回 false 就打印错误码并直接 INIT_FAILED,这一步决定了后续神经网络能否拿到训练底表。 接着用 Autobot.Load 装载 Traj.nnw 权重文件,三个 temp 浮点占位、dtStudied 传入学习时间戳。文件不存在时走 CreateTrajNetDescriptions 新建网络描述并 Autobot.Create 初始化,任何一步失败都 delete 临时对象并回 INIT_FAILED。 装载后必须做维度对账:getResults 输出总数应等于 PrecoderBars*3,第一层输出应等于 HistoryBars*BarDescr,否则打印不匹配数量并失败。这组硬校验能避免你改了输入窗口却忘了重训模型导致的静默错乱。 OpenCL 上下文从 Autobot.GetOpenCL 取得,Ones 与 Gradient 两个 buffer 分别用 1 和 0 初始化并挂到 OpenCL;State 按 HistoryBars*BarDescr 清零。最后 EventChartCustom 发 Init 事件通知图表,返回 INIT_SUCCEEDED。 OnDeinit 里只在非 INITFAILED、非 RECOMPILE 时调 Autobot.Save 落盘 Traj.nnw,并 delete Result 与 OpenCL。注意:外汇与贵金属杠杆交易高风险,模型装载失败可能让 EA 完全不交易,需在 MT5 日志里确认错误码后再排查。
ResetLastError(); if(!LoadTotalBase()) { PrintFormat("Error of load study data: %d", GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- load models class="type">float temp; if(!Autobot.Load(FileName + "Traj.nnw", temp, temp, temp, dtStudied, true)) { Print("Init new models"); CArrayObj *autobot = new CArrayObj(); if(!CreateTrajNetDescriptions(autobot)) { class="kw">delete autobot; class="kw">return INIT_FAILED; } if(!Autobot.Create(autobot)) { class="kw">delete autobot; class="kw">return INIT_FAILED; } class="kw">delete autobot; class=class="str">"cmt">//--- } Autobot.getResults(Result); if(Result.Total() != PrecoderBars * class="num">3) { PrintFormat("The scope of the Autobot does not match the precoder bars(%d <> %d)", PrecoderBars * class="num">3, Result.Total()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Autobot.GetLayerOutput(class="num">0, Result); if(Result.Total() != (HistoryBars * BarDescr)) { PrintFormat("Input size of Autobot doesn&class="macro">#x27;t match state description(%d <> %d)", Result.Total(), (HistoryBars * BarDescr)); class="kw">return INIT_FAILED; } OpenCL = Autobot.GetOpenCL(); if(!Ones.BufferInit(EmbeddingSize, class="num">1) || !Gradient.BufferInit(EmbeddingSize, class="num">0) || !Ones.BufferCreate(OpenCL) || !Gradient.BufferCreate(OpenCL)) { PrintFormat("Error of create buffers: %d", GetLastError()); class="kw">return INIT_FAILED; } State.BufferInit(HistoryBars * BarDescr, class="num">0); if(!EventChartCustom(ChartID(), class="num">1, class="num">0, class="num">0, "Init")) { PrintFormat("Error of create study event: %d", GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- if(!(reason == REASON_INITFAILED || reason == REASON_RECOMPILE)) Autobot.Save(FileName + "Traj.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true); class="kw">delete Result; class="kw">delete OpenCL; } class="type">void Train(class="type">void) { class=class="str">"cmt">//--- vector<class="type">float> probability = GetProbTrajectories(Buffer, class="num">0.9); vector<class="type">float> result, target, inp; matrix<class="type">float> targets;
◍ 训练循环里的状态差分与批次截断
这段逻辑跑在蒙特卡洛式采样的主循环里,外层以 Iterations 为上限,同时受 IsStopped() 与内部 Stop 标志约束,避免在 MT5 策略测试器点停止后还空转烧 CPU。 每次迭代先用 SampleTrajectory(probability) 抽一条轨迹,再用双重 MathRand() 平方归一化挑起始 state,把随机性压到 [0,1) 且偏向低端,样本更集中在近期区段。若算出的 state 为负就 iter-- 并重抽,不浪费计数。 batch 定为 GPTBars+50,end 用 MathMin 卡在 Buffer[tr].Total - PrecoderBars 之前,确保后面塞 PrecoderBars 根预处理棒线时不越界。delta 矩阵按 (end-state-1) 行建,每行存状态向量;行号 row>0 时写的是「上一行 delta 减当前 inp」,即一阶差分而非原始状态——网络吃的是变化量。 喂网前 Autobot.Clear() 清状态,feedForward 带 Ones 偏置矩阵且 false 表示不训练只前向;任一步返回失败就 PrintFormat 打函数行号、置 Stop=true 并 break。targets 按 PrecoderBars×3 建,若 size>BarDescr 会把整段棒线 reshape 后再裁到末行 3 列,只取最新聚合特征喂给预测头。
matrix<class="type">float> delta; STE = vector<class="type">float>::Zeros(PrecoderBars * class="num">3); class="type">int std_count = class="num">0; class="type">int batch = GPTBars + class="num">50; class="type">bool Stop = false; class="type">uint ticks = GetTickCount(); class="type">ulong size = HistoryBars * BarDescr; for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !Stop); iter ++) { class="type">int tr = SampleTrajectory(probability); class="type">int state = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (Buffer[tr].Total - class="num">3 - PrecoderBars - batch)); if(state < class="num">0) { iter--; class="kw">continue; } class="type">int end = MathMin(state + batch, Buffer[tr].Total - PrecoderBars); Autobot.Clear(); delta = matrix<class="type">float>::Zeros(end - state - class="num">1, Buffer[tr].States[state].state.Size()); for(class="type">int i = state; i < end; i++) { inp.Assign(Buffer[tr].States[i].state); State.AssignArray(inp); if(i < (end - class="num">1)) delta.Row(inp, row); if(row > class="num">0) delta.Row(delta.Row(row - class="num">1) - inp, row - class="num">1); if(!Autobot.feedForward((CBufferFloat*)GetPointer(State), class="num">1, false, (CBufferFloat*)GetPointer(Ones))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); Stop = true; break; } targets = matrix<class="type">float>::Zeros(PrecoderBars, class="num">3); for(class="type">int t = class="num">0; t < PrecoderBars; t++) { target.Assign(Buffer[tr].States[i + class="num">1 + t].state); if(size > BarDescr) { matrix<class="type">float> temp(class="num">1, size); temp.Row(target, class="num">0); temp.Reshape(size / BarDescr, BarDescr); temp.Resize(size / BarDescr, class="num">3); target = temp.Row(temp.Rows() - class="num">1); } targets.Row(target, t); } targets.Reshape(class="num">1, targets.Rows()*targets.Cols()); target = targets.Row(class="num">0);