生长型神经气:MQL5 中的实施(基础篇)
📘

生长型神经气:MQL5 中的实施(基础篇)

第 1/3 篇

◍ 在 MT5 里搭一个生长型神经气

生长型神经气(Growing Neural Gas, GNG)是一种无需预设神经元数量的自组织网络,能随输入数据动态增删节点,适合抓价格行为里渐变的结构。2013 年 12 月 MetaTrader 5 社区有作者贴出可运行示例,原帖获 3037 次浏览、17 条讨论,说明这套东西在量化交易者里有真实需求。 在 MQL5 中实施 GNG,核心是把「距离最近的俩神经元」和「老化计时」写进 OnTick 或独立计算线程。下面是一段最小可验证的神经元距离计算代码,你可以直接丢进 MT5 脚本里跑,看 EURUSD 五分钟 K 线聚类结果。 外汇与贵金属杠杆高、滑点大,GNG 给出的聚类只是概率上的结构倾向,不能当方向信号独用。

MQL5 / C++
class="type">class="kw">double NearestDistance(class="type">class="kw">double &vec[], class="type">class="kw">double &neurons[][class="num">2])
{
   class="type">class="kw">double min_d = class="num">1e9;
   for(class="type">int i=class="num">0;i<ArrayRange(neurons,class="num">0);i++)
   {
      class="type">class="kw">double d = (vec[class="num">0]-neurons[i][class="num">0])*(vec[class="num">0]-neurons[i][class="num">0]) +
                  (vec[class="num">1]-neurons[i][class="num">1])*(vec[class="num">1]-neurons[i][class="num">1]);
      if(d<min_d) min_d = d;
   }
   class="kw">return min_d;
}

「从压缩受阻到生长型神经气」

  • 年代做人工神经网络的人碰到一个硬问题:当输入参数维度很高、特征空间没有固定拓扑时,传统向量量化和压缩直接卡壳,语音、图像和抽象范式分类都跑不顺。

当时的解法拼图已经摆在桌上——自组织映射、赫布型学习能生成网络拓扑,软竞争学习让权重不只喂给赢家神经元、还外溢到近邻。1995 年德国科学家 Bernd Fritzke 把这些合到一起,搞出了生长型神经气(GNG)。 GNG 出来后被验证很能打,衍生出一票变体,其中监督版 S-GNG 在难分类空间里的拓扑优化能力,作者称比径向基函数网络强出一截,也比 K-means 聚类更合适。 一个冷门背景:Fritzke 2001 年接了德意志交易所的 offer,离开鲁尔大学结束学术生涯。本文选他的算法打底,这也是考量的因素之一。

生长型神经气怎么自己长节点

GNG 是一种输入数据自适应聚类算法,不止把空间切群集,还会按数据特性自己决定要多少节点。它起步只放两个神经元,靠竞争赫布学习在节点间连出贴合输入向量分布的最优边;每个神经元带一个累积“局部误差”的内部变量,节点间的连接则用 age(年龄)变量标记新旧。 伪代码里关键几步:每来一个输入向量,先找最近和第二近的两个神经元 q、r;赢家 q 的局部误差加上它与输入向量的平方距离,然后 q 和与它相连的拓扑近邻一起朝输入向量平移一小步(步长分局部和整体两档)。从 q 出来的所有连接 age 加 1,q、r 若已连则年龄归零否则新建连接;age 超阈值的边删掉,没边挂的孤儿神经元也删。 每跑 λ 次迭代且没到网络上限,就插一个新神经元:挑局部误差最大的 a 和它的近邻误差最大者 b,在 a、b 中间建节点 c,把 a-b 边换成 a-c、b-c,并下调 a、b 误差、用系数 α 衰减所有节点误差。这一步让网络“忘掉”老样本,能跟缓慢漂移的输入分布,但追不上快速突变。 第 6–8 步的边老化与删除,本质是逼拓扑贴近狄洛尼三角剖分——所有三角形最小角最大化,避免瘦长“皮包骨”三角。新节点永远落在某条边的正中,不直接当独立单元。停止条件随开发者而定:可看测试集有效性、平均误差动态或限复杂度;本文示例图省事,直接学到预定义输入耗尽为止。

◍ 别用动态数组硬扛图结构

写算法时迟早要存“集”:神经元一个集,边一个集。两者在运行期都会增删,若用对象动态数组,增删意味着大量内存拷贝搬移,程序速度会被拖垮。 更合适的抽象是图,而最简形态就是链表。基类对象内嵌一个指向同类对象的指针,从而脱离内存物理顺序、串成线性结构;再配一个 carriage 类统一封装增删插搜与排序等流程。 MT5 标准库已把 CObject 的链表实现好,代码在 MQL5\Include\Arrays\List.mqh(随终端交付)。我们直接继承 CObject 与 CList 做底层,不必从零造轮子——这是面向对象里“继承”机制最实在的用处之一。

「从抽象神经元类搭起 GNG 骨架」

写一套 Growing Neural Gas 的 MT5 程序,第一步不是急着实现算法,而是把最泛化的「神经元」用 OOP 方式钉死。惯例是从最常见、最抽象的数据结构入手:定义一个 CCustomNeuron 类,继承自 MQL5 标准库的 CObject,只保留突触数量 m_synapses、权重数组 m_weights[] 和输出 NET,以及初始化、赋权、适配权重和虚函数 ProcessVector 这几样所有神经元都逃不掉的动作。 把第一层写得尽量抽象,后面开发人员从继承层级往下加细节就行,不用翻你类里的逻辑。CCustomNeuron 里 ProcessVector 标了 virtual,运行时按实际对象类动态选流程,这比写死计算方式灵活,也省后期改代码的力气。 真正落到 GNG 专用神经元 CGNGNeuron,字段才显出差异:error 存输入向量到权重向量的距离平方,E 是累积局部误差加独享 ID,uid 用来在增删神经元后还能稳定配对连接——光靠 CList 自带的索引不够,因为节点一删编号就乱。ProcessVector 被改写去算 error,U 字段先不管,后面算法修改段再用。 两个神经元之间的边也单独成类:两端 uid1、uid2 加一个初始为零的 age。神经元列表从 CList 继承,构造函数里初始化伪随机发生器给元素发唯一 ID;Append 挂节点,Init 按 GNG 从两个神经元起网,Find 按 ID 搜,FindWinners 用引用传指针(*& 写法)捞最近邻赢家。连接列表同理,FindFirstConnection 从表头找、FindNextConnection 从当前节点往后找。数据结构铺完,算法才好进场。 下面这段是基类骨架代码,直接丢进 Include\GNG\Neurons.mqh 就能编译验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 用于引入神经元对象的基类                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CCustomNeuron:class="kw">public CObject
  {
class="kw">protected:
   class="type">int               m_synapses;
   class="type">class="kw">double            m_weights[];
class="kw">public:
   class="type">class="kw">double            NET;
                     CCustomNeuron();
                    ~CCustomNeuron(){};
   class="type">void              ZeroInit(class="type">int synapses);
   class="type">int               Synapses();
   class="type">void              Init(class="type">class="kw">double &weights[]);
   class="type">void              Weights(class="type">class="kw">double &weights[]);
   class="type">void              AdaptWeights(class="type">class="kw">double &delta[]);
   class="kw">virtual class="type">void      ProcessVector(class="type">class="kw">double &in[]) {class="kw">return;}
   class="kw">virtual class="type">int       Type() const             { class="kw">return(TYPE_CUSTOM_NEURON);}
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCustomNeuron::CCustomNeuron()
  {
   m_synapses=class="num">0;
   NET=class="num">0;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回神经元输入向量的维度            |

神经元的权重初始化与读写接口

在自研神经元类里,突触数量直接决定了权重数组的长度。Synapses() 只做一件事:返回 m_synapses 这个私有成员,让外层网络能随时知道该神经元接了几路输入。 ZeroInit(int synapses) 负责把神经元清零起步:若传入 synapses<1 直接 return 不做任何操作;否则记录突触数、把 m_weights 数组Resize到对应长度,再用 ArrayInitialize 全部填 0,顺手把 NET 累加器置 0。 如果你手里已有训练好的向量,走 Init(double &weights[]) 更高效:先判空,再按外部数组大小定 m_synapses,Resize 后 ArrayCopy 进 m_weights,同样清掉 NET。注意这里用的是引用传递,外部数组不会被改动。 Weights(double &weights[]) 是反向出口,把内部 m_weights 拷贝给外部数组,方便做快照或日志。下面这段是原类方法的核心代码,逐行看逻辑比读注释更直白。 这套接口意味着:你在 MT5 里写 EA 时,可以先用 ZeroInit(30) 起一个 30 输入神经元,跑完一轮后再用 Weights() 把权值导出来观察,外汇与贵金属行情高波动,这类小网络仅作辅助信号,实盘前务必在策略测试器跑多周期验证。

MQL5 / C++
class=class="str">"cmt">//| 输入: 无                                                                               |
class=class="str">"cmt">//| 输出: 神经元的突触数量                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CCustomNeuron::Synapses()
  {
   class="kw">return m_synapses;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 使用0向量权重初始化神经元.                                                    |
class=class="str">"cmt">//| 输入: synapses - 突触数量 (输入权重)                                          |
class=class="str">"cmt">//| 输出: 无                                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCustomNeuron::ZeroInit(class="type">int synapses)
  {
   if(synapses<class="num">1) class="kw">return;
   m_synapses=synapses;
   ArrayResize(m_weights,m_synapses);
   ArrayInitialize(m_weights,class="num">0);
   NET=class="num">0;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 使用一系列向量初始化神经元权重.                                               |
class=class="str">"cmt">//| 输入: weights - 数据矢量                                                          |
class=class="str">"cmt">//| 输出: 无                                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCustomNeuron::Init(class="type">class="kw">double &weights[])
  {
   if(ArraySize(weights)<class="num">1) class="kw">return;
   m_synapses=ArraySize(weights);
   ArrayResize(m_weights,m_synapses);
   ArrayCopy(m_weights,weights);
   NET=class="num">0;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 获得神经元权重的向量.                                                          |
class=class="str">"cmt">//| 输入: 无                                                                       |
class=class="str">"cmt">//| 输出: 权重 - 结果                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCustomNeuron::Weights(class="type">class="kw">double &weights[])
  {
   ArrayResize(weights,m_synapses);
   ArrayCopy(weights,m_weights);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 使用指定的数值修改神经元的权重                                                 |
class=class="str">"cmt">//| 输入: delta - 校正向量                                                         |
class=class="str">"cmt">//| 输出: 无                                                                       |

常见问题

会。图结构里节点和边是动态关联的,硬用动态数组容易索引错乱、内存碎片,建议用类封装神经元和连接。
倾向随机小值初始化,避免对称权重导致节点更新同步;零向量容易让生长停滞,回测中收敛更慢。
可以。小布能读取你的诊断输出,对比训练与验证集误差曲线,标出节点暴涨但泛化下降的可疑段。
先把抽象神经元类搭起来,补权重读写接口,再把「固定层」逻辑换成「按误差插节点」的逻辑。
至少留权重初始化、距离计算、局部误差累加、邻居连接增删四个方法,否则后面生长时得返工。