改进迁移学习工具:把模型编辑从代码层搬到文件层(基础篇)
🧠

改进迁移学习工具:把模型编辑从代码层搬到文件层(基础篇)

(1/3)· 多数交易者改一次神经网络架构就要重编译程序,其实只需换一个模型文件

含代码示例 第 1/3 篇

不少人在 MetaTrader 5 里调神经网络,每次换层结构就改代码、重新编译,耗时且容易引入 bug。迁移学习工具本应让你脱离这套循环,但原版界面信息稀薄,批量试模型时根本记不住架构。本文作为系列开篇,先把工具可用性改造的根基打牢。

给迁移学习工具补上神经层透视

在 MT5 里做迁移学习,最麻烦的是改完网络结构却看不到每一层到底在跑什么。原工具只抛出最终预测,中间层的神经元数量、激活函数、连接权重范围全是黑箱,调参基本靠猜。 这一版直接把神经层完整信息打印到专家日志:层数、各层维度、当前激活状态一目了然。比如一个 10-16-8-1 的四层网,日志会逐层列出 10 输入、16 隐元、8 隐元、1 输出,以及每层所用的激活标识。 顺带把输入字段分成「已用激活」和「未用停用」两组。那些被特征选择砍掉、权重置零的维度不再混在活跃输入里,你扫一眼就知道哪几个因子真正进了模型。 还挂了键盘事件处理:在策略测试器里跑批量训练时,按指定键能实时切层显示、暂停快照,不用反复开闭窗口。外汇与贵金属行情波动剧烈、杠杆风险高,这类工具只帮你看清结构,不替你下任何方向判断。

◍ 迁移学习工具的层提取与热加载

上一篇文章里我们做出了一个能编辑已训练模型的工具,核心能力是从预训练模型里抽取任意数量的神经层。但抽取不是随意的:只能从初始数据层起连续往后取,因为神经网络只对训练时喂入的数据分布拟合良好,跳层或逆序会直接破坏前向传播链路。 这个工具不止能改旧模型,还能从零描述并生成全新网络,借此把模型架构从程序代码里解耦出来。你用工具写好结构存成文件,程序运行时加载即可跟踪使用,换架构不用改一行 C++ 代码,也不需要重新编译。 目前限制很实际:层提取的连续性约束意味着实验不同深度时,得在工具里重排文件而非代码。本文接下来就冲着把这流程做得更顺手、少踩坑去迭代。

「用群组值把神经层信息拆成可读模块」

在 MT5 里用 CListView 显示模型架构时,原生逻辑是一行一个元素,没法把一层网络的描述跨多行展示,也不支持把多个元素绑成一个组。若直接改类架构工作量过大,更务实的做法是借用现有类,给同一神经层的多行元素赋相同的数值标识,用「群组值」在程序层面划分边界。 具体落地时,LayerDescriptionToString 方法先把一层的结构拼成带反斜杠“\”分隔符的整串文本,再调用 StringSplit 按“\”拆进动态数组,数组每个元素就是列表里的一行。注意数组索引从 0 起,但赋群组 id 时取层序号 +1,因为若传 0,CListView 会自作主张把它替换成列表元素总数,导致群组识别错乱。 CListView 自带的 SelectByValue 只认第一个匹配数值的元素。用户点中某层任意一行,程序读出该行群组值后调 SelectByValue,光标就跳回该层首行——这一步让多层模型在面板里不再是一坨文本,而是用分隔符和首行高亮切开的模块。 实测下来,一层 Dense 网络展开后约占 3 行(类型、激活函数、优化器各一行),卷积层因参数多可能占 5~6 行;未知类型会落进标准描述分支,避免静默破坏模型完整性。外汇与贵金属模型试验属高风险操作,组内参数误读可能令后续训练结果偏离预期。

MQL5 / C++
class="type">bool  AddItem(
  class="kw">const class="type">class="kw">string  item,      class=class="str">"cmt">// text 
  class="kw">const class="type">long    value      class=class="str">"cmt">// value 
  )
class="type">bool  SelectByValue(
  class="kw">const class="type">long    value      class=class="str">"cmt">// value 
  )
class="type">int CNetCreatorPanel::LayerDescriptionToString(class="kw">const CLayerDescription *layer, class="type">class="kw">string& result[])
  {
   if(!layer)
      class="kw">return -class="num">1;
   class="type">class="kw">string temp;
   ArrayFree(result);
   class="kw">switch(layer.type)
     {
      case defNeuronBaseOCL:
         temp = StringFormat("Dense(outputs %d, \activation %s, \optimization %s)",
                              layer.count, EnumToString(layer.activation), EnumToString(layer.optimization));
         if(StringSplit(temp, &class="macro">#x27;\\&class="macro">#x27;, result) < class="num">0)
            class="kw">return -class="num">1;
         class="kw">break;
      case defNeuronConvOCL:
         temp = StringFormat("Convolution(outputs %d, \window %d, step %d, window out %d, \activation %s, \optimization %s)",
                              layer.count * layer.window_out, layer.window, layer.step, layer.window_out, EnumToString(layer.activation),
                              EnumToString(layer.optimization));
         if(StringSplit(temp, &class="macro">#x27;\\&class="macro">#x27;, result) < class="num">0)
            class="kw">return -class="num">1;
         class="kw">break;
      case defNeuronProofOCL:
         temp = StringFormat("Proof(outputs %d, \window %d, step %d, \optimization %s)",

注意力与正则类网络层的描述串拼装

在 MT5 的 OpenCL 神经网络封装里,不同类型的层要用不同模板拼出可读描述串,方便在日志或 UI 里直接看结构。下面这段 switch 分支覆盖了自注意力、多头注意力、多层多头注意力,以及 Dropout、BatchNorm、VAE、LSTM 等典型层。 自注意力层(defNeuronAttentionOCL)输出节点数按 layer.count * layer.window 计算,描述里带 units、window 与优化方式枚举;多头版本(defNeuronMHAttentionOCL)额外把 layer.step 当 heads 数量写入,输出同样是 count*window。 多层多头注意力(defNeuronMLMHAttentionOCL)最复杂:除 outputs、units、window 外,还暴露 key size(取 layer.window_out)、heads(layer.step)、layers(layer.layers)三项,描述串字段数达到 7 个,比单层多头多 3 个可观测维度。 Dropout 层只报 count 与 probability,BatchNorm 报 count 与 batch,VAE 仅报 outputs,LSTM 报 count 加优化枚举。每个分支都先用 StringFormat 写进 temp,再用 StringSplit(temp, '\\', result) 拆行;若拆分返回值小于 0 直接 return -1,说明层描述生成失败。 开 MT5 把这段分支塞进你自己的网络结构打印函数,改几个 layer 字段就能在专家日志里核对结构有没有写错——外汇与贵金属模型训练杠杆高、滑点难控,结构打印错一位都可能让回测结果失真。

MQL5 / C++
layer.count, layer.window, layer.step, EnumToString(layer.activation), EnumToString(layer.optimization));
         if(StringSplit(temp, &class="macro">#x27;\\&class="macro">#x27;, result) < class="num">0)
            class="kw">return -class="num">1;
         class="kw">break;
         case defNeuronAttentionOCL:
            temp = StringFormat("Self Attention(outputs %d, \\units %s, window %d, \\optimization %s)",
layer.count * layer.window, layer.count, layer.window, EnumToString(layer.optimization));
            if(StringSplit(temp, &class="macro">#x27;\\&class="macro">#x27;, result) < class="num">0)
               class="kw">return -class="num">1;
            class="kw">break;
         case defNeuronMHAttentionOCL:
            temp = StringFormat("Multi-Head Attention(outputs %d, \\units %s, window %d, heads %s, \\optimization %s)",
layer.count * layer.window, layer.count, layer.window, layer.step, EnumToString(layer.optimization));
            if(StringSplit(temp, &class="macro">#x27;\\&class="macro">#x27;, result) < class="num">0)
               class="kw">return -class="num">1;
            class="kw">break;
         case defNeuronMLMHAttentionOCL:
            temp = StringFormat("Multi-Layer MH Attention(outputs %d, \\units %s, window %d, key size %d, \\heads %s, layers %d,
\\optimization %s)",
layer.count * layer.window, layer.count, layer.window, layer.window_out, layer.step, layer.layers,
EnumToString(layer.optimization));
            if(StringSplit(temp, &class="macro">#x27;\\&class="macro">#x27;, result) < class="num">0)
               class="kw">return -class="num">1;
            class="kw">break;
         case defNeuronDropoutOCL:
            temp = StringFormat("Dropout(outputs %d, \\probability %d, \\optimization %s)",
layer.count, layer.probability, EnumToString(layer.optimization));
            if(StringSplit(temp, &class="macro">#x27;\\&class="macro">#x27;, result) < class="num">0)
               class="kw">return -class="num">1;
            class="kw">break;
         case defNeuronBatchNormOCL:
            temp = StringFormat("Batchnorm(outputs %d, \\batch size %d, \\optimization %s)",
layer.count, layer.batch, EnumToString(layer.optimization));
            if(StringSplit(temp, &class="macro">#x27;\\&class="macro">#x27;, result) < class="num">0)
               class="kw">return -class="num">1;
            class="kw">break;
         case defNeuronVAEOCL:
            temp = StringFormat("VAE(outputs %d)", layer.count);
            if(StringSplit(temp, &class="macro">#x27;\\&class="macro">#x27;, result) < class="num">0)
               class="kw">return -class="num">1;
            class="kw">break;
         case defNeuronLSTMOCL:
            temp = StringFormat("LSTM(outputs %d, \\optimization %s)", layer.count, EnumToString(layer.optimization));
            if(StringSplit(temp, &class="macro">#x27;\\&class="macro">#x27;, result) < class="num">0)
               class="kw">return -class="num">1;
            class="kw">break;
         class="kw">default:

◍ 把神经网络层结构拆成可读字符串

这段 MT5 代码负责把模型里的每一层描述转成列表控件能显示的文本。先拼一个含类型、输出节点数、激活与优化枚举的字符串,再用 StringSplit 按反斜杠拆行,拆不出就返回 -1 退出。 LayerDescriptionToString 函数末尾返回 ArraySize(result),也就是拆分后的行数;若某层指针为空或拆分失败,调用方会 printf 打出 __FUNCSIG__ 与 __LINE__ 以及 GetLastError(),方便在专家日志里定位是第几层出错。 外层循环从 i=0 跑到 total-1,逐层调用上述函数并把 '____ Layer %d ____' 和各项明细写进 m_lstPTModel。AddItem 任一步返回 false 就整体返回 false,说明界面模型树可能没建全。 事件绑定用 ON_EVENT 把两个列表的 ON_CHANGE 挂到各自处理函数;OnChangeListNewModel 只做一件事:取当前 value 再 SelectByValue 回选,避免界面闪烁。外汇与贵金属模型训练波动大、过拟合风险高,这类结构打印只帮你排查配置,不预示任何收益。

MQL5 / C++
   temp = StringFormat("Unknown type %class="macro">#x (outputs %d, \activation %s, \optimization %s)",
layer.type, layer.count, EnumToString(layer.activation), EnumToString(layer.optimization));
   if(StringSplit(temp, &class="macro">#x27;\\&class="macro">#x27;, result) < class="num">0)
      class="kw">return -class="num">1;
   class="kw">break;
   }
class=class="str">"cmt">//---
   class="kw">return ArraySize(result);
   }
   for(class="type">int i = class="num">0; i < total; i++)
     {
      CLayerDescription* temp = m_arPTModelDescription.At(i);
      if(!temp)
         class="kw">return class="kw">false;
      class="type">class="kw">string items[];
      class="type">int total_items = LayerDescriptionToString(temp, items);
      if(total_items < class="num">0)
         {
          printf("%s %d Error at layer %d: %d", __FUNCSIG__, __LINE__, i, GetLastError());
          class="kw">return class="kw">false;
         }
      if(!m_lstPTModel.AddItem(StringFormat("____ Layer %d ____", i + class="num">1), i + class="num">1))
         class="kw">return class="kw">false;
      for(class="type">int it = class="num">0; it < total_items; it++)
         if(!m_lstPTModel.AddItem(items[it], i + class="num">1))
            class="kw">return class="kw">false;
     }
ON_EVENT(ON_CHANGE, m_lstPTModel, OnChangeListPTModel)
ON_EVENT(ON_CHANGE, m_lstNewModel, OnChangeListNewModel)
class="type">bool CNetCreatorPanel::OnChangeListNewModel(class="type">void)
  {
   class="type">long value = m_lstNewModel.Value();
class=class="str">"cmt">//---
   class="kw">return m_lstNewModel.SelectByValue(value);
  }

「按层类型收敛输入字段的可用性」

给神经网络面板做输入字段时,最容易踩的坑是把不相关参数也暴露给用户。全连接层只需神经元数量、激活函数、优化方法三个参数;卷积层要补上输入窗口大小、步长和过滤器数量;LSTM 的激活函数已被模块架构锁死,再给下拉框纯属诱导误操作。外汇与贵金属模型训练本身高风险,界面若放任乱填,只会放大机械错误。 我选了“置灰不可编辑”而非“动态删字段”:字段一次性排好,用户视觉记忆位置后整体效率更高,避免每次重排对象带来的混乱。可编辑字段白底,只读字段底色与面板同色,用 CEdit 的 ReadOnly 加背景色区分。 激活函数下拉框不能整体禁用,就按层类型换列表:通用层调 ActivationListMain 填全量,LSTM / 子样本 / 关注模块调 ActivationListEmpty 只留 “None”。注意激活枚举从 -1(None)起、TANH 为 0,CListView 怕 null 标识,所以填列表时给枚举 id 加一个小常量偏移。 卷积和关注模块的输出元素数由窗口大小和步长推导,因此关闭数量输入,用 SetCounts 自动算:前层缓冲不大于 0 直接返 false;卷积/子样本用窗口÷步长公式,关注模块步长=窗口可约分,VAE 潜伏层比前层小两倍,其余层沿用前层大小。OnChangeNeuronType 在用户切换层类型时调用,按分支点亮对应字段并改文本标签(如 step 在卷积指步长、在关注指关注者数量)。 所有整数字段要求 >0,仅 Dropout 概率允 0~1 浮点。校验在输入后立即跑,把规范值写回框体让用户看到差异。窗口大小和步长改动会触发重算层大小,事件模型每事件仅一处理程序,故包一层方法先校验再按层类型调 SetCounts。这套改完,开 MT5 把 CreateLabel 改成返回 CLabel* 指针,就能直接抓那两个动态标签做实时换字。

MQL5 / C++
CLabel* CNetCreatorPanel::CreateLabel(class="kw">const class="type">int id, class="kw">const class="type">class="kw">string text,
                                      class="kw">const class="type">int x1, class="kw">const class="type">int y1,
                                      class="kw">const class="type">int x2, class="kw">const class="type">int y2
                                      )
  {
   CLabel *tmp_label = new CLabel();
   if(!tmp_label)
让小布替你跑这套
这些诊断与模型文件切换的思路,小布盯盘的 AIGC 已内置,打开对应品种页即可看到不同架构的推理表现,你把重复劳动交给小布,专注决策。

常见问题

神经网络只对训练时所用初始数据拟合良好,非连续截取会破坏前向传播的数据依赖,导致提取后的子模型推理失真。
用工具在界面描述模型并保存为文件,程序从文件加载即可,无需重编译;更换模型文件就能切换体系结构。
目前小布盯盘内置的是推理表现诊断,模型文件编辑仍建议在 MT5 工具完成,之后把结果贴回小布对应品种页做对比。
继承或重写该类工作量过大,作者借分隔符在现有类上做视觉分组,避免改动类架构也能模块化呈现每层信息。
层信息变多后水平滚动降低可读性,拆成多行加分隔符更利于快速扫读和定位停用字段。