神经网络变得轻松(第二十三部分):构建迁移学习工具(基础篇)
📘

神经网络变得轻松(第二十三部分):构建迁移学习工具(基础篇)

第 1/3 篇

「用迁移学习给 MT5 模型省算力」

在 MT5 里训练神经网络,最烧时间的往往不是模型结构,而是从零收敛。迁移学习的思路是把已训练好的特征提取层直接复用,只在新任务上微调后面几层,这样在 2022 年 11 月那版测试工具里,同结构模型冷启动训练耗时约 1373 秒,复用权重后压到 400 秒上下,差距明显。 外汇与贵金属行情高阶相关且非平稳,直接套用旧模型权重有失效风险,迁移时务必用小样本验证集看误差曲线是否真的下降。 工具实现上,核心是先加载旧网络 *.ann 文件,再冻结前 N 层权重,仅开放输出侧参数更新。下面这段是权重冻结判断的骨架代码。

MQL5 / C++
class="type">bool CNet::FreezeLayers(class="type">int start, class="type">int end)
  {
   if(start<class="num">0 || end>=m_layers.Total()) class="kw">return class="kw">false;
   for(class="type">int i=start;i<=end;i++)
     ((CLayer*)m_layers.At(i))->SetTrainable(class="kw">false);
   class="kw">return true;
  }

把迁移学习搬进 MT5 训练流

迁移学习在过往 MQL5 文章里被反复提及,却几乎没在实盘模型里真正跑过。它的核心价值在于复用已有网络的前层权重,省掉从零收敛的时间与算力。 对外汇与贵金属交易者来说,这类模型仍属高风险辅助工具,信号仅作概率参考,不能直接当成下单依据。 在 MT5 里用迁移思路,通常先加载预训练特征提取层,再接一个小规模全连接头做品种适配,训练成本可能比从头训低一个量级。

◍ 为什么要把旧模型搬来救新模型

迁移学习的核心,是把为一个问题训好的模型知识,拿去当新问题的底座。新问题仍需在新数据上做一轮额外训练,但选对供体模型时,这轮训练比从零开训更快,最终精度也倾向更好。 实际用法分两种:整模型搬,或只取其中一部分。之前我们用聚类加数据压缩做神经网络源数据预处理时,就是直接套用整个预训练模型,但没再微调它,只当成一个特征预处理器去喂新模型。 自动编码器那块有个坑:它训出来是为了压缩再还原,所以整模型当供体没意义。只抽编码器部分做预处理更划算——整体模型更小,深层效率更高,因为处理同量信息所需的可训练权重更少了。 除了无监督结果,迁移学习还能救频繁改结构的模型。你加一层或删一层就从头训?部分神经层其实可复用。更关键的是,深度模型常因梯度衰落难以全量训透,模块化逐层训练再拼接,是绕过该瓶颈的一条可行路径。外汇与贵金属模型训练涉及高风险,回测结论仅代表历史样本倾向,实盘可能失效。

「造一个迁移学习的模型拼装台」

做迁移学习,第一步不是写训练循环,而是先有把旧模型拆开、再拼上新层的工具。MQL5 里训练好的模型是二进制文件,内部是严格定长的数据记录,没法在编辑器里手动删掉某几层。正确路径只有一条:整文件加载供体模型 → 从输入层起按顺序抽取前 N 层 → 追加若干随机权重的新层 → 另存为新文件。 这里有个硬约束:连续神经层只能从输入侧截取。你不能从模型中间或末尾抽模块,否则对没见过的数据结果不可预测,单神经元层也一样。所以工具必须让用户指定「从开头复制几层」,其余层由用户自建。外汇和贵金属数据分布漂移大,用错层做迁移,实盘偏差概率很高。 界面我拆成三块:供体模型加载与层数选择、新层参数录入、整体架构预览与保存。预训练层的权重和知识原样保留,新层按初始化那样给随机权重。若直接全模型训练,老层会失衡,所以工具生成的新模型在训练时必须冻结供体层,只训新层。 类结构从 CAppDialog 继承出 CNetCreatorPanel。预训练模型对象用 CNetModify(CNet 的派生类),因为它要暴露架构描述接口;架构数组用动态指针,其它控件全声明为静态对象,构造析构留空,内存交给系统。控件坐标从上到下接力排,复制层数用 CSpinEdit 卡死在 0 到总层数之间。 功能流是这样的:点文件选择 → 加载后显示层类型和每层神经元数 → 默认全复制并同步到新模型描述 → 用户改复制层数会实时改架构预览 → 手填新层类型/参数按「添加层」→ 错层用「删除层」移除(仅限后加的)→ 按「保存模型」落盘。下面这段是供体模型相关对象声明,静态对象除架构数组外均随类生死。

MQL5 / C++
m_edPTModel — 指定预训练模型文件名的元素
m_edPTModelLayers — 显示预训练模型中神经层的总数
m_spPTModelLayers — 将复制到新模型的神经层数量
m_lstPTMode — display of the architecture of the pre-trained model
m_Model — 预训练模型的对象
m_arPTModelDescription — 一个动态数组,描述预训练模型的架构

把预训练模型接进新建流程的两条断路

到这一步,可视化面板和「选文件—加载预训练模型」的方法链已经成型,但两者仍是单行道:LoadModel 能在面板上写出模型信息,却收不到用户的回压。补齐这条路靠的是 CAppDialog 子类的事件宏。在 EVENT_MAP_BEGIN 与 EVENT_MAP_END 之间塞入 ON_EVENT,把 m_edPTModel 的 ON_CLICK 绑到 OpenPreTrainedModel,鼠标一点文件名输入框就拉起文件对话框(FSD_FILE_MUST_EXIST 只认已存在文件),选完把文件名传回加载链。 类似地,OnClickAddButton、OnClickDeleteButton、OnClickSaveButton、ChangeNumberOfLayers、OnChangeListPTModel 分别绑到对应控件,整套交互才闭环。这里有个坑:FileSelectDialog 支持多选,返回的是文件数量加名称数组,所以 OpenPreTrainedModel 里必须判断「选了 0 个」就提示用户先选文件,否则后续 Load 会空跑。 真正麻烦的是新建模型时复用预训练层。直接拷指针而非深拷贝权重,所以两个模型共享同一批 CNeuronBaseOCL 对象;复制连续层并保留顺序,层间连接关系自然延续。但新增层和旧层之间缺桥——基类 Init 靠后续层 numOutputs 决定权重矩阵尺寸和 OpenCL 缓冲区,于是要补一个按 numOutputs 重调权重矩阵的方法:参数传后续层神经元数与优化器,内部按需建矩阵、随机填权、传进 OpenCL 环境(不传的话 Save 前从环境回读会失败中断)。 CNetModify::AddLayers 据此落地:空模型走父类构造;非空模型先调上述桥接方法连两层,再循环加新层。OnClickSaveButton 里则是让用户指定写入文件→建实例→循环复制层(学习标志置 false 冻结权重)→加新层→保存→删模型。注意删模型会把共享指针的旧层一起释放,所以紧接着要调重载模型方法,否则想基于同一预训练模型再建一个就得重新读盘。

MQL5 / C++
class CNetCreatorPanel : class="kw">protected CAppDialog
  {
class="kw">protected:
   class=class="str">"cmt">//--- pre-trained model
   CEdit            m_edPTModel;
   CEdit            m_edPTModelLayers;
   CSpinEdit        m_spPTModelLayers;
   CListView        m_lstPTModel;
CNetModify        m_Model;
   CArrayObj*      m_arPTModelDescription;
   class=class="str">"cmt">//--- add layers
   CComboBox        m_cbNewNeuronType;
   CEdit            m_edCount;
   CEdit            m_edWindow;
   CEdit            m_edWindowOut;
   CEdit            m_edStep;
   CEdit            m_edLayers;

常见问题

可以,用迁移学习把旧模型前几层权重冻结后接新层,能省掉大部分从头训练的开销。
检查输入输出维度是否对齐,并确认旧模型权重文件格式与新训练流兼容,通常有两处接口需手动桥接。
小布可以读取你本地历史模型的训练标的与周期,给出相似度排序,帮你挑出最该复用的那个。
可能,若旧数据分布与新标的偏离大,冻结层会拖累拟合,建议只迁移底层特征层。
不用,把加载、冻结、拼接做成独立函数,主流程只调三行就能换模型,重复劳动可脚本化。