神经网络变得轻松(第二十四部分):改进迁移学习工具·进阶篇
🧠

神经网络变得轻松(第二十四部分):改进迁移学习工具·进阶篇

(2/3)· 只改代码看层结构太慢?这版工具让层信息、输入状态和键盘操作一次到位

进阶 第 2/3 篇
很多人以为迁移学习工具只要能提取连续层就够用了,结果换模型就得重编译程序。把实验卡在代码层,等于主动放弃了不重启就能试结构的可能。

◍ 面板标签与下拉项的创建细节

这段代码片段来自一个神经网络创建面板的类实现,核心在做 GUI 控件的容错式构造。CreateLabel 里每次失败都先 delete 临时对象再 return NULL,避免 MT5 图表上残留不可见资源句柄。 CNetCreatorPanel::Create 用 CAppDialog 基类框定面板范围,宽高由 PANEL_WIDTH / PANEL_HEIGHT 宏决定;随后按 ly1/ly2 递增坐标,依次建出 id=8 的 Step 标签和 id=9 的 Window Out 标签,任一个返回空指针就整体返回 false。 下拉框 ActivationListMain 用 for(int i=-1; i<3; i++) 循环塞入 4 个枚举项,索引偏移 +2,默认选中 DEFAULT_ACTIVATION+2。外汇与贵金属 EA 面板若此处越界,可能在实时加载时崩对话框,属高风险调试点。 EditReedOnly 通过 ReadOnly 加背景色切换区分可写/只读编辑框,CONTROLS_DIALOG_COLOR_CLIENT_BG 与 CONTROLS_EDIT_COLOR_BG 两套色值直接决定视觉辨识度。开 MT5 把这段抄进自定义面板类,改 PANEL_WIDTH 看布局偏移最直观。

MQL5 / C++
  class="kw">return NULL;
  if(!tmp_label.Create(m_chart_id, StringFormat("%s%d", LABEL_NAME, id), m_subwin, x1, y1, x2, y2))
    {
      class="kw">delete tmp_label;
      class="kw">return NULL;
    }
  if(!tmp_label.Text(text))
    {
      class="kw">delete tmp_label;
      class="kw">return NULL;
    }
  if(!Add(tmp_label))
    {
      class="kw">delete tmp_label;
      class="kw">return NULL;
    }
class=class="str">"cmt">//---
  class="kw">return tmp_label;
  }
  CLabel*        m_lbWindowOut;
  CLabel*        m_lbStepHeads;
class="type">bool CNetCreatorPanel::Create(class="kw">const class="type">long chart, class="kw">const class="type">class="kw">string name,
                            class="kw">const class="type">int subwin, class="kw">const class="type">int x1, class="kw">const class="type">int y1)
  {
  if(!CAppDialog::Create(chart, name, subwin, x1, y1, x1 + PANEL_WIDTH, y1 + PANEL_HEIGHT))
    class="kw">return class="kw">false;
class=class="str">"cmt">//---
  ly1 = ly2 + CONTROLS_GAP_Y;
  ly2 = ly1 + EDIT_HEIGHT;
  m_lbStepHeads = CreateLabel(class="num">8, "Step", lx1, ly1, lx1 + EDIT_WIDTH, ly2);
  if(!m_lbStepHeads)
    class="kw">return class="kw">false;
class=class="str">"cmt">//---
  ly1 = ly2 + CONTROLS_GAP_Y;
  ly2 = ly1 + EDIT_HEIGHT;
  m_lbWindowOut = CreateLabel(class="num">9, "Window Out", lx1, ly1, lx1 + EDIT_WIDTH, ly2);
  if(!m_lbWindowOut)
    class="kw">return class="kw">false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNetCreatorPanel::EditReedOnly(CEdit& object, class="kw">const class="type">bool flag)
  {
  if(!object.ReadOnly(flag))
    class="kw">return class="kw">false;
  if(!object.ColorBackground(flag ? CONTROLS_DIALOG_COLOR_CLIENT_BG : CONTROLS_EDIT_COLOR_BG))
    class="kw">return class="kw">false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNetCreatorPanel::ActivationListMain(class="type">void)
  {
  if(!m_cbActivation.ItemsClear())
    class="kw">return class="kw">false;
  for(class="type">int i = -class="num">1; i < class="num">3; i++)
    if(!m_cbActivation.ItemAdd(EnumToString((ENUM_ACTIVATION)i), i + class="num">2))
      class="kw">return class="kw">false;
  if(!m_cbActivation.SelectByValue((class="type">int)DEFAULT_ACTIVATION + class="num">2))
    class="kw">return class="kw">false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNetCreatorPanel::SetCounts(class="kw">const class="type">uint position, class="kw">const class="type">uint type)
  {
  class="kw">const class="type">uint position = m_arAddLayers.Total();
  CLayerDescription *prev;
  if(position <= class="num">0)
    {

「前层输出维度决定新层计数」

在搭建 MT5 神经网络面板时,新增神经元层的可用节点数不是拍脑袋填的,而是由前一层对象的 count 与类型推导出来的。代码先判断是用主模型描述数组还是附加层数组取前层指针 prev,取不到就直接返回 false,避免空指针导致 EA 初始化崩溃。 注意力类(defNeuronAttentionOCL / MH / MLMH)会把 outputs 再乘 prev.window,卷积类乘 prev.window_out;例如前层 count=64、window=8,注意力层 outputs 会变为 512。这一步决定了后续卷积或注意力层「能吃进多少特征」。 不同类型的层计数公式差异明显:卷积类用 (outputs - window - 1 + 2*step)/step(step 须 >0),注意力类用 (outputs + window - 1)/window(window 须 >0),VAE 直接 outputs/2,其余默认等于 outputs。把 m_edWindow、m_edStep 文本框里的字符串转成 int 再套公式,结果写回 m_edCount。 改神经元类型时面板会联动锁编辑框:选 defNeuronBaseOCL 会放开 m_edCount、锁死 batch/layers/probability/step 等。开 MT5 把这段逻辑挂到 CNetCreatorPanel,调一下前层 window 参数,就能看到新增层计数实时变化。

MQL5 / C++
  if(!m_arPTModelDescription || m_spPTModelLayers.Value() <= class="num">0)
      class="kw">return class="kw">false;
   prev = m_arPTModelDescription.At(m_spPTModelLayers.Value() - class="num">1);
   if(!prev)
       class="kw">return class="kw">false;
   }
  else
   {
   if(m_arAddLayers.Total() < (class="type">int)position)
       class="kw">return class="kw">false;
   prev = m_arAddLayers.At(position - class="num">1);
   }
  if(!prev)
    class="kw">return class="kw">false;
  class="type">int outputs = prev.count;
  class="kw">switch(prev.type)
   {
   case defNeuronAttentionOCL:
   case defNeuronMHAttentionOCL:
   case defNeuronMLMHAttentionOCL:
       outputs *= prev.window;
       class="kw">break;
   case defNeuronConvOCL:
       outputs *= prev.window_out;
       class="kw">break;
   }
class=class="str">"cmt">//---
  if(outputs <= class="num">0)
    class="kw">return class="kw">false;
  class="type">int counts = class="num">0;
  class="type">int window = (class="type">int)StringToInteger(m_edWindow.Text());
  class="type">int step = (class="type">int)StringToInteger(m_edStep.Text());
  class="kw">switch(type)
   {
   case defNeuronConvOCL:
   case defNeuronProofOCL:
       if(step <= class="num">0)
         class="kw">break;
       counts = (outputs - window - class="num">1 + class="num">2 * step) / step;
       class="kw">break;
   case defNeuronAttentionOCL:
   case defNeuronMHAttentionOCL:
   case defNeuronMLMHAttentionOCL:
       if(window <= class="num">0)
         class="kw">break;
       counts = (outputs + window - class="num">1) / window;
       class="kw">break;
   case defNeuronVAEOCL:
       counts = outputs / class="num">2;
       class="kw">break;
   class="kw">default:
       counts = outputs;
       class="kw">break;
   }
class=class="str">"cmt">//---
  class="kw">return m_edCount.Text((class="type">class="kw">string)counts);
  }
class="type">bool CNetCreatorPanel::OnChangeNeuronType(class="type">void)
  {
  class="type">long type = m_cbNewNeuronType.Value();
  class="kw">switch((class="type">int)type)
   {
   case defNeuronBaseOCL:
       if(!EditReedOnly(m_edCount, class="kw">false) ||
        !EditReedOnly(m_edBatch, true) ||
        !EditReedOnly(m_edLayers, true) ||
        !EditReedOnly(m_edProbability, true) ||
        !EditReedOnly(m_edStep, true) ||

按网络类型切换输入框的只读态

在 MT5 的自定义指标 / EA 面板里,切换不同神经元网络类型时,得同步锁定或放开对应的编辑框。下面这段 switch-case 就是按 defNeuronConvOCL、defNeuronProofOCL、defNeuronLSTMOCL 等枚举分支,逐个调用 EditReedOnly 控制 m_edCount、m_edBatch 等控件的只读属性。 以 defNeuronConvOCL 为例:Count、Batch、Layers、Probability 四个框被设为 true(只读),而 Step、Window、WindowOut 设为 false(可写),同时把标签改成 Step / Window Out 并刷新激活函数下拉列表。若任意一步返回 false,整个分支直接 return false,界面状态不半途生效。 defNeuronProofOCL 与 ConvOCL 的差异在于 WindowOut 被锁为 true,且额外调用 ActivationListEmpty() 清空激活函数选项;LSTMOCL 则把 Count 放开(false)、Step 锁死(true)。这种硬约束能避免交易者在 LSTM 模式下误填神经元总数,降低模型配置出错概率。外汇与贵金属行情受杠杆与跳空影响,此类 UI 容错设计仅降低操作风险,不预示任何收益。

MQL5 / C++
if(!EditReedOnly(m_edWindow, true) ||
   !EditReedOnly(m_edWindowOut, true))
   class="kw">return class="kw">false;
 if(!ActivationListMain())
   class="kw">return class="kw">false;
 class="kw">break;
 case defNeuronConvOCL:
   if(!EditReedOnly(m_edCount, true) ||
      !EditReedOnly(m_edBatch, true) ||
      !EditReedOnly(m_edLayers, true) ||
      !EditReedOnly(m_edProbability, true) ||
      !EditReedOnly(m_edStep, class="kw">false) ||
      !EditReedOnly(m_edWindow, class="kw">false) ||
      !EditReedOnly(m_edWindowOut, class="kw">false))
      class="kw">return class="kw">false;
   if(!m_lbStepHeads.Text("Step"))
      class="kw">return class="kw">false;
   if(!m_lbWindowOut.Text("Window Out"))
      class="kw">return class="kw">false;
   if(!ActivationListMain())
      class="kw">return class="kw">false;
   if(!SetCounts(defNeuronConvOCL))
      class="kw">return class="kw">false;
   class="kw">break;
 case defNeuronProofOCL:
   if(!EditReedOnly(m_edCount, true) ||
      !EditReedOnly(m_edBatch, true) ||
      !EditReedOnly(m_edLayers, true) ||
      !EditReedOnly(m_edProbability, true) ||
      !EditReedOnly(m_edStep, class="kw">false) ||
      !EditReedOnly(m_edWindow, class="kw">false) ||
      !EditReedOnly(m_edWindowOut, true))
      class="kw">return class="kw">false;
   if(!m_lbStepHeads.Text("Step"))
      class="kw">return class="kw">false;
   if(!SetCounts(defNeuronProofOCL))
      class="kw">return class="kw">false;
   if(!ActivationListEmpty())
      class="kw">return class="kw">false;
   class="kw">break;
 case defNeuronLSTMOCL:
   if(!EditReedOnly(m_edCount, class="kw">false) ||
      !EditReedOnly(m_edBatch, true) ||
      !EditReedOnly(m_edLayers, true) ||
      !EditReedOnly(m_edProbability, true) ||
      !EditReedOnly(m_edStep, true) ||

◍ 不同网络结构下的控件只读逻辑

在 MT5 的神经网络面板里,切换不同层类型时界面对输入控件的锁定策略并不统一。下面这段 switch 分支展示了四种 OCL 网络结构各自允许的编辑状态,直接决定你能在界面上改哪些参数。 Dropout 分支里 m_edProbability 设为可编辑(false 表示非只读),其余 Count、Batch、Layers、Step、Window、WindowOut 全部只读;而 Attention 分支反过来,m_edWindow 可编辑、m_edBatch 只读。这种差异意味着跑 Attention 结构时你没法在界面上直接调批大小。 每个 case 都先批量调用 EditReedOnly 设置只读,只要有一个返回失败就直接 return false 中断;随后 SetCounts 和 ActivationListEmpty 也必须成功,否则同样返回 false。开 MT5 把这段逻辑接进你自己的 CNeuron 派生类,改几个 true/false 就能验证哪些参数被锁死。 外汇与贵金属行情下用这类模型做信号生成属高风险,参数锁错可能导致训练配置与你预期不符,实盘前务必在策略测试器里跑通。

MQL5 / C++
!EditReedOnly(m_edWindow, true) ||
!EditReedOnly(m_edWindowOut, true))
class="kw">return class="kw">false;
if(!ActivationListEmpty())
class="kw">return class="kw">false;
class="kw">break;
case defNeuronDropoutOCL:
if(!EditReedOnly(m_edCount, true) ||
!EditReedOnly(m_edBatch, true) ||
!EditReedOnly(m_edLayers, true) ||
!EditReedOnly(m_edProbability, class="kw">false) ||
!EditReedOnly(m_edStep, true) ||
!EditReedOnly(m_edWindow, true) ||
!EditReedOnly(m_edWindowOut, true))
class="kw">return class="kw">false;
if(!SetCounts(defNeuronDropoutOCL))
class="kw">return class="kw">false;
if(!ActivationListEmpty())
class="kw">return class="kw">false;
class="kw">break;
case defNeuronBatchNormOCL:
if(!EditReedOnly(m_edCount, true) ||
!EditReedOnly(m_edBatch, class="kw">false) ||
!EditReedOnly(m_edLayers, true) ||
!EditReedOnly(m_edProbability, true) ||
!EditReedOnly(m_edStep, true) ||
!EditReedOnly(m_edWindow, true) ||
!EditReedOnly(m_edWindowOut, true))
class="kw">return class="kw">false;
if(!SetCounts(defNeuronBatchNormOCL))
class="kw">return class="kw">false;
if(!ActivationListEmpty())
class="kw">return class="kw">false;
class="kw">break;
case defNeuronAttentionOCL:
if(!EditReedOnly(m_edCount, true) ||
!EditReedOnly(m_edBatch, true) ||
!EditReedOnly(m_edLayers, true) ||
!EditReedOnly(m_edProbability, true) ||
!EditReedOnly(m_edStep, true) ||
!EditReedOnly(m_edWindow, class="kw">false) ||
!EditReedOnly(m_edWindowOut, true))
class="kw">return class="kw">false;
if(!SetCounts(defNeuronAttentionOCL))
class="kw">return class="kw">false;
if(!ActivationListEmpty())
class="kw">return class="kw">false;

「注意力与变分自编码器的界面锁参逻辑」

在 MT5 自定义神经网络的面板切换里,不同模型类型对编辑框的只读状态有硬约束。以 defNeuronMHAttentionOCL 为例,Count、Batch、Layers、Probability、WindowOut 被设为只读(true),而 Step 与 Window 放开可改(false),这种组合决定了多头注意力结构下用户只能调时间窗口类参数。 defNeuronMLMHAttentionOCL 比前者多一处差异:WindowOut 也设为可改(false),同时标签被改写为「Keys size」,说明该变体把输出窗口当作键值长度来用,Layers 反而锁死,训练层深不可在界面上动。 defNeuronVAEOCL 则把所有七个编辑框全部置为只读(true),且清空激活函数列表,意味着变分自编码器类型在面板层完全固化了网络规模与窗口设定,只能靠外部代码改参。 逐行看这段 case 分支,每个类型都靠 EditReedOnly 链式判断返回值,任一框锁态失败就直接 return false 中断配置,因此改 UI 逻辑时要核对每个字段的 true/false 是否与模型数学结构匹配。

MQL5 / C++
      class="kw">break;
      case defNeuronMHAttentionOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, class="kw">false) ||
            !EditReedOnly(m_edWindow, class="kw">false) ||
            !EditReedOnly(m_edWindowOut, true))
            class="kw">return class="kw">false;
         if(!m_lbStepHeads.Text("Heads"))
            class="kw">return class="kw">false;
         if(!SetCounts(defNeuronMHAttentionOCL))
            class="kw">return class="kw">false;
         if(!ActivationListEmpty())
            class="kw">return class="kw">false;
         class="kw">break;
      case defNeuronMLMHAttentionOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, class="kw">false) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, class="kw">false) ||
            !EditReedOnly(m_edWindow, class="kw">false) ||
            !EditReedOnly(m_edWindowOut, class="kw">false))
            class="kw">return class="kw">false;
         if(!m_lbStepHeads.Text("Heads"))
            class="kw">return class="kw">false;
         if(!m_lbWindowOut.Text("Keys size"))
            class="kw">return class="kw">false;
         if(!SetCounts(defNeuronMLMHAttentionOCL))
            class="kw">return class="kw">false;
         if(!ActivationListEmpty())
            class="kw">return class="kw">false;
         class="kw">break;
      case defNeuronVAEOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            class="kw">return class="kw">false;
         if(!ActivationListEmpty())
            class="kw">return class="kw">false;

事件绑定与输入钳制的收口写法

神经网络面板在 MT5 里跑起来,最后一步是把控件事件和处理函数钉死。上面这段把 ON_CLICK、ON_CHANGE、ON_END_EDIT 全部映射到 CNetCreatorPanel 的方法,EVENT_MAP_END 收口到 CAppDialog,少一个映射控件就失活。 输入校验不能靠 UI 限制,得在代码里钳。OnEndEditProbability 用 fmax(0, fmin(1, value)) 把概率锁在 [0,1],再 DoubleToString 保留两位小数;普通整数框 OnEndEdit 只保底 fmax(1, value),避免窗口步长填 0 把特征切片搞崩。 自定义宏 ON_EVENT_CONTROL 把 CHARTEVENT_CUSTOM 偏移和控件 Id 做与判断,命中即调 handler 并 return true,省去层层 if。复制这段代码到你的 EA 面板类,改 m_edCount、m_edBatch 等成员名就能直接复用输入防呆逻辑。外汇与贵金属品种波动剧烈,这类参数边界疏忽可能引发异常下单,实盘前务必在策略测试器跑一遍边界值。

MQL5 / C++
   if(!SetCounts(defNeuronVAEOCL))
         class="kw">return class="kw">false;
      class="kw">break;
      class="kw">default:
         class="kw">return class="kw">false;
         class="kw">break;
   }
class=class="str">"cmt">//---
   class="kw">return true;
   }
EVENT_MAP_BEGIN(CNetCreatorPanel)
ON_EVENT(ON_CLICK, m_edPTModel, OpenPreTrainedModel)
ON_EVENT(ON_CLICK, m_btAddLayer, OnClickAddButton)
ON_EVENT(ON_CLICK, m_btDeleteLayer, OnClickDeleteButton)
ON_EVENT(ON_CLICK, m_btSave, OnClickSaveButton)
ON_EVENT(ON_CHANGE, m_spPTModelLayers, ChangeNumberOfLayers)
ON_EVENT(ON_CHANGE, m_lstPTModel, OnChangeListPTModel)
ON_EVENT(ON_CHANGE, m_lstNewModel, OnChangeListNewModel)
ON_EVENT(ON_CHANGE, m_cbNewNeuronType, OnChangeNeuronType)
EVENT_MAP_END(CAppDialog)
class="type">bool CNetCreatorPanel::OnEndEditProbability(class="type">void)
  {
  class="type">class="kw">double value = StringToDouble(m_edProbability.Text());
  class="kw">return m_edProbability.Text(DoubleToString(fmax(class="num">0, fmin(class="num">1, value)), class="num">2));
  }
class="type">bool CNetCreatorPanel::OnEndEdit(CEdit& object)
  {
  class="type">long value = StringToInteger(object.Text());
  class="kw">return object.Text((class="type">class="kw">string)fmax(class="num">1, value));
  }
class="macro">#define ON_EVENT_CONTROL(event,control,handler)           if(id==(event+CHARTEVENT_CUSTOM) && lparam==control.Id()) \
                                                     { handler(control); class="kw">return(true); }
class="type">bool CNetCreatorPanel::OnChangeWindowStep(class="type">void)
  {
  if(!OnEndEdit(m_edWindow) || !OnEndEdit(m_edStep))
      class="kw">return class="kw">false;
  class="kw">return SetCounts((class="type">uint)m_cbNewNeuronType.Value());
  }
EVENT_MAP_BEGIN(CNetCreatorPanel)
ON_EVENT(ON_CLICK, m_edPTModel, OpenPreTrainedModel)
ON_EVENT(ON_CLICK, m_btAddLayer, OnClickAddButton)
ON_EVENT(ON_CLICK, m_btDeleteLayer, OnClickDeleteButton)
ON_EVENT(ON_CLICK, m_btSave, OnClickSaveButton)
ON_EVENT(ON_CHANGE, m_spPTModelLayers, ChangeNumberOfLayers)
ON_EVENT(ON_CHANGE, m_lstPTModel, OnChangeListPTModel)
ON_EVENT(ON_CHANGE, m_lstNewModel, OnChangeListNewModel)
ON_EVENT(ON_CHANGE, m_cbNewNeuronType, OnChangeNeuronType)
ON_EVENT(ON_END_EDIT, m_edWindow, OnChangeWindowStep)
ON_EVENT(ON_END_EDIT, m_edStep, OnChangeWindowStep)
ON_EVENT(ON_END_EDIT, m_edProbability, OnEndEditProbability)
ON_EVENT_CONTROL(ON_END_EDIT, m_edCount, OnEndEdit)
ON_EVENT_CONTROL(ON_END_EDIT, m_edWindowOut, OnEndEdit)
ON_EVENT_CONTROL(ON_END_EDIT, m_edLayers, OnEndEdit)
ON_EVENT_CONTROL(ON_END_EDIT, m_edBatch, OnEndEdit)
EVENT_MAP_END(CAppDialog)
把层结构比对交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到模型层变动与输入停用状态,你只管判断哪段特征该保留。

常见问题

神经网络只对训练时的初始数据分布拟合良好,跳层抽取会破坏前向传播的数据连续性,导致提取后的子模型推理失真。
目前小布盯盘内置的是层结构与输入状态诊断视图,模型文件需在 MetaTrader 5 端用本文工具生成和加载,小布侧负责可视化比对。
借用已有类并插入可视分隔符将整段文本拆成独立模块,每层描述作为分组呈现,避免继承新控件类带来的大量重构。
能直观区分哪些输入通道在贡献特征、哪些已被弃用,方便在不重训的前提下剪枝或替换数据源做快速验证。