神经网络变得轻松(第二十三部分):构建迁移学习工具·综合运用
(3/3)·从零搭一个能拆装神经层、重存模型的工具,省下重复训练的开销
◍ 给 OpenCL 神经元动态扩输出维度的坑
在 MT5 用 OpenCL 加速的神经网络里,CNeuronBaseOCL::numOutputs 负责按新输出数重算权重缓冲。它先校验 outputs>0,若权重对象无效就 new 一个 CBufferFloat,失败直接返回 false;随后 BufferFree + Clear 清掉旧权重,按 (Output.Total()+1)*outputs 算出总连接数并 Reserve。 权重初始化用了 1/sqrt(Output.Total()+1) 做缩放系数 k,循环里写 (2*GenerateWeight()*k - k)*WeightsMultiplier。这意味着输出节点越多,单条连接权重初值越小,能缓解宽层梯度爆炸,但 GenerateWeight() 分布若不近似均匀,实际初始化可能偏离预期。 CNetModify::AddLayers 拼接新层时会取最后一层首个神经元,调 numOutputs 把旧层输出口接到新层 count 上。若新层类型不是 defNeuron / defNeuronBaseOCL,outputs 传 0,numOutputs 因 outputs>0 不成立直接返回 false,导致整次加层失败——这是静默断网的高频诱因。 保存模型走 CNetCreatorPanel::OnClickSaveButton,用 FileSelectDialog 限定 *.nnw 扩展名,默认 NewModel.nnw。若返回文件数 <=0 就 Print('File not selected') 并退出,实盘里点保存却没弹窗选路时,先查终端文件访问权限而非怀疑网络结构。
class="type">bool CNeuronBaseOCL::numOutputs(class="kw">const class="type">uint outputs, ENUM_OPTIMIZATION optimization_type) { if(outputs > class="num">0) { if(CheckPointer(Weights) == POINTER_INVALID) { Weights = new CBufferFloat(); if(CheckPointer(Weights) == POINTER_INVALID) class="kw">return class="kw">false; } Weights.BufferFree(); Weights.Clear(); class="type">int count = (class="type">int)((Output.Total() + class="num">1) * outputs); if(!Weights.Reserve(count)) class="kw">return class="kw">false; class="type">class="kw">float k = (class="type">class="kw">float)(class="num">1 / sqrt(Output.Total() + class="num">1)); for(class="type">int i = class="num">0; i < count; i++) { if(!Weights.Add((class="num">2 * GenerateWeight()*k - k)*WeightsMultiplier)) class="kw">return class="kw">false; } if(!Weights.BufferCreate(OpenCL)) class="kw">return class="kw">false; class="type">bool CNetModify::AddLayers(CArrayObj *new_layers) { if(!new_layers || new_layers.Total() <= class="num">0) class="kw">return class="kw">false; class=class="str">"cmt">//--- if(!layers || LayersTotal() <= class="num">0) { Create(new_layers); class="kw">return true; } CLayerDescription *desc = NULL, *next = NULL; CLayer *temp; class="type">int outputs; class="type">int shift = (class="type">int)LayersTotal() - class="num">1; CLayer* last_layer = layers.At(shift); if(!last_layer) class="kw">return class="kw">false; class=class="str">"cmt">//--- CNeuronBaseOCL* neuron = last_layer.At(class="num">0); if(!neuron) class="kw">return class="kw">false; class=class="str">"cmt">//--- desc = neuron.GetLayerInfo(); next = new_layers.At(class="num">0); outputs = (next == NULL || (next.type != defNeuron && next.type != defNeuronBaseOCL) ? class="num">0 : next.count); if(!neuron.numOutputs(outputs, next.optimization)) class="kw">return class="kw">false; class="kw">delete desc; class="type">bool CNetCreatorPanel::OnClickSaveButton(class="type">void) { class="type">class="kw">string filenames[]; if(FileSelectDialog("Select files to save", NULL, "Neuron Net(*.nnw)|*.nnw|All files(*.*)|*.*", FSD_WRITE_FILE, filenames, "NewModel.nnw") <= class="num">0) { Print("File not selected"); class="kw">return class="kw">false; } class="type">class="kw">string file_name = filenames[class="num">0]; if(StringLen(file_name) - StringLen(EXTENSION) > StringFind(file_name, EXTENSION))
把训练好的网络另存为可加载模型
这段逻辑干的事很直接:把当前在内存里的神经网络复制一份,关掉训练模式后存成文件,再立刻重新加载回界面指定的路径。外汇与贵金属行情的高波动会让过拟合模型快速失效,因此这种「训练—固化—重载」的闭环适合在 MT5 实盘前做离线验证。 代码先拼好带扩展名的文件名,new 一个 CNetModify 容器;若分配失败直接返 false。随后用 m_spPTModelLayers.Value() 拿到总层数,逐层把原模型里的 CLayer 取出来,调用 neuron.TrainMode(false) 切到推理态,再 AddLayer 进新模型。任意一层取空或添加失败,result 置 false 并 break。 若基础层拷贝成功且 m_arAddLayers 里有附加层(Total()>0),就通过 GetPointer 把附加层也塞进去。最后以 Save(file_name, 1.0e37f, 100, 0, 0, false) 落盘——第二个参数 1.0e37f 是归一化上限,100 为保留精度位数,后三项为偏移与校验开关。存完 delete 掉临时对象,调 LoadModel 按输入框路径重载,返回 result。你可在 MT5 里改 100 这个精度参数看模型文件体积与加载耗时的变化。
file_name += EXTENSION; CNetModify* new_model = new CNetModify(); if(!new_model) class="kw">return class="kw">false; class="type">int total = m_spPTModelLayers.Value(); class="type">bool result = true; for(class="type">int i = class="num">0; i < total && result; i++) { CLayer* temp = m_Model.GetLayer((class="type">uint)i); if(!temp) { result = class="kw">false; class="kw">break; } CNeuronBaseOCL* neuron = temp.At(class="num">0); neuron.TrainMode(class="kw">false); if(!new_model.AddLayer(temp)) result = class="kw">false; } new_model.SetOpenCL(m_Model.GetOpenCL()); if(result && m_arAddLayers.Total() > class="num">0) if(!new_model.AddLayers(GetPointer(m_arAddLayers))) result = class="kw">false; if(result && !new_model.Save(file_name, class="num">1.0e37f, class="num">100, class="num">0, class="num">0, class="kw">false)) result = class="kw">false; class=class="str">"cmt">//--- if(!!new_model) class="kw">delete new_model; LoadModel(m_edPTModel.Text()); class=class="str">"cmt">//--- class="kw">return result; }
「把面板类挂进EA跑通迁移」
验证这套神经网络搭建工具,最直接的方式是写一个极简 EA 把面板类接进来。NetCreator.mq5 本身没多少逻辑,核心只是把前面写好的 CNetCreatorPanel 在三个生命周期钩子里串起来:初始化时建面板、退出时销毁、图表事件转发给面板。 实际加载到 MT5 图表后,神经层确实能从一个模型结构迁到另一个,也能继续往上叠新层;更实用的是,它能从零拉出一个全新模型,并把模型描述直接写进程序代码里,方便后续复用。外汇与贵金属行情高波动,这类实验请在策略测试器或模拟环境先跑,实盘接入倾向谨慎。 下面这段就是 EA 里全部集成点,逐行看很轻量: #include "NetCreatorPanel.mqh" 引入面板类头文件;CNetCreatorPanel Panel 声明一个面板实例。 OnInit 里 Panel.Create(0,"NetCreator",0,50,50) 在坐标(50,50)创建名为 NetCreator 的面板,失败则返回 INIT_FAILED;Panel.Run() 启动面板,同样失败即退出。 OnDeinit 只调 Panel.Destroy(reason) 做注销清理。 OnChartEvent 里先判断 CHARTEVENT_OBJECT_CLICK 点一下 Sleep(0) 让出线程,再把所有事件参数丢给 Panel.ChartEvent 处理。
class="macro">#include "NetCreatorPanel.mqh" CNetCreatorPanel Panel; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- if(!Panel.Create(class="num">0, "NetCreator", class="num">0, class="num">50, class="num">50)) class="kw">return INIT_FAILED; if(!Panel.Run()) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- Panel.Destroy(reason); } class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- if(id == CHARTEVENT_OBJECT_CLICK) Sleep(class="num">0); Panel.ChartEvent(id, lparam, dparam, sparam); }
◍ 把新架构塞进旧模型试试
前面几篇搭好的迁移工具,核心用途是把一个模型里训练好的部分神经层直接搬进另一个模型,而且新层的数量和架构都能自定义。对做 MT5 信号模型的交易者来说,这意味着你不用每次都从零训网络。 一个值得动手的做法:保留源数据层和结果层的原有架构,只把一套全新中间层「推」进已有的智能系统,再跑训练。这样能比较纯粹架构变动带来的误差差异,而不是被数据分布变化干扰。 建议你翻出以前训过的 .net 或自有模型文件,用这套工具做三组对照——原架构、加两层全连接、换成不同激活函数,记录每组的验证误差。外汇与贵金属模型对市场状态极敏感,这类实验仅反映历史样本表现,实盘仍属高风险。
把这条线请下神坛
这套 LSTM 预测方案落到工程层,就是四个文件在 MT5 里各司其职:NetCreator.mq5 是 EA 形态的模型构建入口,NetCreatotPanel.mqh 管创建工具的界面逻辑,NeuroNet.mqh 封装神经网络结构,NeuroNet.cl 把计算丢给 OpenCL 跑。随文附的 MQL5.zip 约 71.47 KB,解压后直接拖进 MT5 的 MQL5 目录就能编译验证。 外汇与贵金属市场高杠杆、高波动,神经网络给出的只是概率倾向,不是方向保证;真要上实盘,先拿历史数据回测过拟合程度。 代码能跑通和能赚钱之间隔着的,是样本外泛化和风控,这条线从来不是圣杯,只是你工具箱里多一把尺。