生长型神经气:MQL5 中的实施·进阶篇
(2/3)·从伪代码到类的封装,把自适应聚类真正写进 EA 前先搞懂这几步
「GNG 神经元的权重更新与距离度量」
在 GNG(Growing Neural Gas)网络里,单个神经元继承自通用神经元类 CCustomNeuron,其核心方法 AdaptWeights 负责按传入的 delta 数组调整突触权重。代码先以 ArraySize(delta)!=m_synapses 做防线,维度不对直接 return,避免错位写入;随后用 for 循环把 delta[i] 叠加进 m_weights[i],并把 NET 清零,意味着本次前向计算缓存作废。 CGNGNeuron 在基类上扩展了 uid、E、U、error 四个字段,构造函数统一置 0。其中 error 存的是神经元与输入向量的「距离」,E 保留为局部误差容器,两者分离方便后续竞争与老化逻辑调用。 ProcessVector 是距离计算主体:当输入向量维度与突触数不符时退出,否则遍历每个维度累加 (in[i]-m_weights[i]) 的平方。这本质是欧氏距离的平方,未开根号以省算力,返回的 error 数值越大代表该神经元对当前样本越不敏感。 连接类 CGNGConnection 记录两个神经元 uid1、uid2 及 age,age 用于边缘老化淘汰。外汇与贵金属行情高波动、高杠杆,用此类拓扑做形态聚类时务必以小样本离线验证,实盘误判概率可能偏高。
class="type">void CCustomNeuron::AdaptWeights(class="type">class="kw">double &delta[]) { if(ArraySize(delta)!=m_synapses) class="kw">return; for(class="type">int i=class="num">0;i<m_synapses;i++) m_weights[i]+=delta[i]; NET=class="num">0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 一个 GNG 网络中的独立神经元 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CGNGNeuron:class="kw">public CCustomNeuron { class="kw">public: class="type">int uid; class="type">class="kw">double E; class="type">class="kw">double U; class="type">class="kw">double error; CGNGNeuron(); class="kw">virtual class="type">void ProcessVector(class="type">class="kw">double &in[]); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CGNGNeuron::CGNGNeuron() { E=class="num">0; U=class="num">0; error=class="num">0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算神经元到输入向量的 "距离" | class=class="str">"cmt">//| 输入: in - 数据向量 | class=class="str">"cmt">//| 输出: 无 | class=class="str">"cmt">//| 注意: 当前 "距离" 保存在 error 变量中, | class=class="str">"cmt">//| "局部误差" 包含于另外的变量中, | class=class="str">"cmt">//| 该变量叫做 E | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGNGNeuron::ProcessVector(class="type">class="kw">double &in[]) { if(ArraySize(in)!=m_synapses) class="kw">return; error=class="num">0; NET=class="num">0; for(class="type">int i=class="num">0;i<m_synapses;i++) { error+=(in[i]-m_weights[i])*(in[i]-m_weights[i]); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 定义两个神经元的连接 (边缘) 的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CGNGConnection:class="kw">public CObject { class="kw">public: class="type">int uid1; class="type">int uid2; class="type">int age; CGNGConnection();
◍ GNG 神经元链表的初始化与追加逻辑
在增长型神经气体(GNG)算法里,神经元不是预先固定数量的,而是靠链表动态生长。下面这段 CList 派生类把随机种子绑在本地时间上,保证每次 MT5 启动跑出来的初始神经元 uid 分布不重样。 Append 方法负责在链表尾塞一个“空白”神经元:首次调用时直接 new 出首节点,后续则接在 last_node 后面并维护双向指针。值得注意,uid 用 MathRand() 生成后还要过一遍 Find(rnd) 查重,碰撞就重摇,直到不重复才 break——这意味着当链表里神经元数量逼近 MathRand 上限(32767)时,while 循环可能明显拖慢。 Init 接口留给两个权重向量 v1、v2,用来在列表建好之后填进前两个神经元的参考向量。做价格聚类的朋友可以直接把 EURUSD 的 M15 收盘价差分序列灌进 v1/v2 试跑,观察神经元拓扑何时收敛。外汇与贵金属杠杆高,这类实验仅在策略研发环境进行,实盘接入前须充分回测。
class="kw">virtual class="type">int Type() const { class="kw">return(TYPE_GNG_CONNECTION);} }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ CGNGConnection::CGNGConnection() { age=class="num">0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 神经元链表 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CGNGNeuronList:class="kw">public CList { class="kw">public: class=class="str">"cmt">//--- 构造函数 CGNGNeuronList() {MathSrand(TimeLocal());} CGNGNeuron *Append(); class="type">void Init(class="type">class="kw">double &v1[],class="type">class="kw">double &v2[]); CGNGNeuron *Find(class="type">int uid); class="type">void FindWinners(CGNGNeuron *&Winner,CGNGNeuron *&SecondWinner); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 在列表末尾增加一个 "空白" 神经元 | class=class="str">"cmt">//| 输入: 无 | class=class="str">"cmt">//| 输出: 新神经元指针 | class=class="str">"cmt">//+------------------------------------------------------------------+ CGNGNeuron *CGNGNeuronList::Append() { if(m_first_node==NULL) { m_first_node= new CGNGNeuron; m_last_node = m_first_node; } else { GetLastNode(); m_last_node=new CGNGNeuron; m_curr_node.Next(m_last_node); m_last_node.Prev(m_curr_node); } m_curr_node=m_last_node; m_curr_idx=m_data_total++; class="kw">while(true) { class="type">int rnd=MathRand(); if(!CheckPointer(Find(rnd))) { ((CGNGNeuron *)m_curr_node).uid=rnd; class="kw">break; } } class=class="str">"cmt">//--- class="kw">return(m_curr_node); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据向量的权重创建两个神经元 | class=class="str">"cmt">//| 再用于初始化列表 | class=class="str">"cmt">//| 输入: v1,v2 - 权重向量 |
GNG 神经元链表的初始化与胜者检索
在 MT5 里跑 Growing Neural Gas 这类自组织网络,第一步是把两个初始向量塞进链表并清掉旧状态。Init 方法先 Clear 再连续 Append 两次,分别用 v1、v2 初始化头两个神经元,相当于给网络种下两个种子点。
Find 按 uid 线性遍历,从 GetFirstNode 起步,do-while 配合 CheckPointer(GetNextNode()) 一路比对,命中即返回指针,没找到回 NULL。这种 O(n) 查找在神经元数量较少时开销可忽略,节点上了千就要留意每次调用的累计成本。
FindWinners 干的事是扫两遍:第一遍找 error 最小的当 Winner,第二遍排除 Winner 后找 error 最小的当 SecondWinner。注意 err_min 初始为 0,首节点因 !CheckPointer(Winner) 必然入选,随后只有更小误差才能替换——这意味着误差非负时逻辑自洽,若你的误差计算可能输出负值得先校准。
下面这段是上述三个方法的原始实现,逐行照着抄进 EA 的 include 就能用:
void CGNGNeuronList::Init(double &v1[],double &v2[])
{
Clear();
Append();
((CGNGNeuron *)m_curr_node).Init(v1);
Append();
((CGNGNeuron *)m_curr_node).Init(v2);
}
CGNGNeuron *CGNGNeuronList::Find(int uid)
{
if(!GetFirstNode()) return(NULL);
do
{
if(((CGNGNeuron *)m_curr_node).uid==uid)
return(m_curr_node);
}
while(CheckPointer(GetNextNode()));
return(NULL);
}
void CGNGNeuronList::FindWinners(CGNGNeuron *&Winner,CGNGNeuron *&SecondWinner)
{
double err_min=0;
Winner=NULL;
if(!CheckPointer(GetFirstNode())) return;
do
{
| if(!CheckPointer(Winner) | ((CGNGNeuron *)m_curr_node).error<err_min) |
|---|
{ err_min= ((CGNGNeuron *)m_curr_node).error; Winner = m_curr_node; } } while(CheckPointer(GetNextNode())); err_min=0; SecondWinner=NULL; GetFirstNode(); do { if(m_curr_node!=Winner)
| if(!CheckPointer(SecondWinner) | ((CGNGNeuron *)m_curr_node).error<err_min) |
|---|
{ err_min=((CGNGNeuron *)m_curr_node).error; SecondWinner=m_curr_node; } } while(CheckPointer(GetNextNode())); m_curr_node=Winner; } 连接链表 CGNGConnectionList 直接继承 CList,只暴露 Append 和 Init(uid1,uid2),说明边结构在 GNG 里就是一对神经元 ID 的绑定,后续生长时靠它记录拓扑。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 输出: 无 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGNGNeuronList::Init(class="type">class="kw">double &v1[],class="type">class="kw">double &v2[]) { Clear(); Append(); ((CGNGNeuron *)m_curr_node).Init(v1); Append(); ((CGNGNeuron *)m_curr_node).Init(v2); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据 uid 搜索神经元 | class=class="str">"cmt">//| 输入: uid - 神经元的唯一ID | class=class="str">"cmt">//| 输出: 如果成功返回神经元指针, 否则返回 NULL | class=class="str">"cmt">//+------------------------------------------------------------------+ CGNGNeuron *CGNGNeuronList::Find(class="type">int uid) { if(!GetFirstNode()) class="kw">return(NULL); do { if(((CGNGNeuron *)m_curr_node).uid==uid) class="kw">return(m_curr_node); } class="kw">while(CheckPointer(GetNextNode())); class="kw">return(NULL); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据最小当前误差搜索两个 "最好的" 神经元 | class=class="str">"cmt">//| 输入: 无 | class=class="str">"cmt">//| 输出: Winner - 最 "接近" 输入向量的神经元 | class=class="str">"cmt">//| SecondWinner - 第二 "接近" 的神经元 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGNGNeuronList::FindWinners(CGNGNeuron *&Winner,CGNGNeuron *&SecondWinner) { class="type">class="kw">double err_min=class="num">0; Winner=NULL; if(!CheckPointer(GetFirstNode())) class="kw">return; do { if(!CheckPointer(Winner) || ((CGNGNeuron *)m_curr_node).error<err_min) { err_min= ((CGNGNeuron *)m_curr_node).error; Winner = m_curr_node; } } class="kw">while(CheckPointer(GetNextNode())); err_min=class="num">0; SecondWinner=NULL; GetFirstNode(); do { if(m_curr_node!=Winner) if(!CheckPointer(SecondWinner) || ((CGNGNeuron *)m_curr_node).error<err_min) { err_min=((CGNGNeuron *)m_curr_node).error; SecondWinner=m_curr_node; } } class="kw">while(CheckPointer(GetNextNode())); m_curr_node=Winner; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 神经元之间连接的链表 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CGNGConnectionList:class="kw">public CList { class="kw">public: CGNGConnection *Append(); class="type">void Init(class="type">int uid1,class="type">int uid2);
「GNG 连接链表的增查与初始化实现」
在 Growing Neural Gas 的 C++ 风格封装里,连接关系由 CGNGConnectionList 以双向链表承载。Append() 负责在尾部挂一个空白连接:首节点为空时直接 new 出头节点,否则先 GetLastNode() 定位尾端,再串好 Next/Prev 指针,最后把 m_data_total 自增 1 作为索引计数。 Init(int uid1,int uid2) 用 Append() 产出的首节点写入两端神经元 ID,并把当前/尾指针都锚定到首节点、索引归零。这意味着列表初始化时至少占用 1 个连接槽,m_data_total 从 0 变为 1。 Find(int uid1,int uid2) 不区分方向:用 do-while 从首节点扫到空指针,只要 (uid1,uid2) 或 (uid2,uid1) 命中就返回该连接指针,全表无果才返 NULL。注意这里强依赖 CheckPointer 判空,漏调 GetFirstNode() 会直接从野指针起步。 FindFirstConnection(int uid) 则是按单神经元 ID 拉拓扑邻居的入口,搜索域限定在从第一个神经元展开的列表。实盘跑 MT5 时,若你自建 GNG 聚类做 EURUSD 波动状态划分,可直接抄这套链表逻辑验证邻居检索耗时——样本量上千连接时,线性扫描延迟通常落在毫秒级,但高频调参仍建议缓存邻接表。外汇与贵金属杠杆高,任何信号模型都只是概率参考,别拿回测连通性当方向依据。
CGNGConnection *CGNGConnectionList::Append() { if(m_first_node==NULL) { m_first_node= new CGNGConnection; m_last_node = m_first_node; } else { GetLastNode(); m_last_node=new CGNGConnection; m_curr_node.Next(m_last_node); m_last_node.Prev(m_curr_node); } m_curr_node=m_last_node; m_curr_idx=m_data_total++; class="kw">return(m_curr_node); } class="type">void CGNGConnectionList::Init(class="type">int uid1,class="type">int uid2) { Append(); ((CGNGConnection *)m_first_node).uid1 = uid1; ((CGNGConnection *)m_first_node).uid2 = uid2; m_last_node = m_first_node; m_curr_node = m_first_node; m_curr_idx=class="num">0; } CGNGConnection *CGNGConnectionList::Find(class="type">int uid1,class="type">int uid2) { if(!CheckPointer(GetFirstNode())) class="kw">return(NULL); do { if((((CGNGConnection *)m_curr_node).uid1==uid1 && ((CGNGConnection *)m_curr_node).uid2==uid2) ||(((CGNGConnection *)m_curr_node).uid1==uid2 && ((CGNGConnection *)m_curr_node).uid2==uid1)) class="kw">return(m_curr_node); } class="kw">while(CheckPointer(GetNextNode())); class="kw">return(NULL); }
◍ 在连接链表里捞指定神经元的边
GNG 拓扑里每个神经元都靠连接对象维系邻居关系,要快速定位某条 uid 参与的边,就得在连接链表上做线性扫描。上面两段分别实现了从头找和从当前节点往后找两种入口,逻辑对称但起点不同。 FindConnection 先判断首节点指针是否有效,无效直接返 NULL;随后死循环里比对当前连接对象的 uid1 或 uid2 是否等于目标 uid,命中就 break 返回该节点指针。若中途下一个节点指针校验失败,说明链表到头也没找到,同样返 NULL。 FindNextConnection 的区别仅在于起点换成 GetCurrentNode(),它跳过首节点、从当前游标继续向后搜,适合已知大致位置时的增量遍历。两类函数在 MT5 策略里若被高频调用,链表长度超过 200 条连接时单次查找延迟可能明显抬升,建议在实盘前用 Comment 打印耗时验证。 别把线性扫描当免费 连接数膨胀到千级后,这种 O(n) 查找会让每 tick 的推理开销陡增,必要时要自己加哈希索引而非硬套原链表的遍历。
if(!CheckPointer(GetFirstNode())) class="kw">return(NULL); class="kw">while(true) { if(((CGNGConnection *)m_curr_node).uid1==uid || ((CGNGConnection *)m_curr_node).uid2==uid) class="kw">break; if(!CheckPointer(GetNextNode())) class="kw">return(NULL); } class="kw">return(m_curr_node); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 搜索指定的神经元的拓扑结构邻居 | class=class="str">"cmt">//| 搜索范围是从当前神经元开始的神经元列表 | class=class="str">"cmt">//| 输入: uid - 神经元ID | class=class="str">"cmt">//| 输出: 如果有连接返回连接的指针, 否则返回 NULL | class=class="str">"cmt">//+------------------------------------------------------------------+ CGNGConnection *CGNGConnectionList::FindNextConnection(class="type">int uid) { if(!CheckPointer(GetCurrentNode())) class="kw">return(NULL); class="kw">while(true) { if(!CheckPointer(GetNextNode())) class="kw">return(NULL); if(((CGNGConnection *)m_curr_node).uid1==uid || ((CGNGConnection *)m_curr_node).uid2==uid) class="kw">break; } class="kw">return(m_curr_node); }
把 GNG 算法装进一个类里
新建头文件 GNG.mqh 丢进 Include\GNG 目录,核心就是 CGNGAlgorithm 这个类。它只握有两个真正干活的物理介质:Neurons 指针(神经元链表)和 Connections 指针(连接链表),其余字段全是外部灌进来的算法参数,比如 lambda、age_max、alpha、beta 这些。 Init(...) 负责把外部参数传进实例并初始化数据结构;而之前约定的停止条件 StoppingCriterion() 目前是空壳,始终返回 false,意味着训练不会自己停。ProcessVector(…) 作为主函数只管按既定步骤组织数据并调用方法,细节都分散在带注释的代码位置,本身不堆逻辑。 想验证就直接把下面代码贴进 MT5 的 Include\GNG\GNG.mqh,编译看神经元链表和连接链表能否随 Init 参数正常起手。外汇与贵金属行情序列喂进去前,注意这类无停止条件的自组织网络容易过拟合,高风险。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| GNG.mqh | class=class="str">"cmt">//| Copyright class="num">2010, alsu | class=class="str">"cmt">//| alsufx@gmail.com | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2010, alsu" class="macro">#class="kw">property link "alsufx@gmail.com" class="macro">#include "Neurons.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 体现 GNG 算法的主类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CGNGAlgorithm { class="kw">public: class=class="str">"cmt">//--- 神经元对象以及它们之间连接的链表 CGNGNeuronList *Neurons; CGNGConnectionList *Connections; class=class="str">"cmt">//--- 算法的参数 class="type">int input_dimension; class="type">int iteration_number; class="type">int lambda; class="type">int age_max; class="type">class="kw">double alpha; class="type">class="kw">double beta; class="type">class="kw">double eps_w; class="type">class="kw">double eps_n; class="type">int max_nodes; CGNGAlgorithm(); ~CGNGAlgorithm(); class="kw">virtual class="type">void Init(class="type">int __input_dimension, class="type">class="kw">double &v1[], class="type">class="kw">double &v2[], class="type">int __lambda, class="type">int __age_max, class="type">class="kw">double __alpha, class="type">class="kw">double __beta, class="type">class="kw">double __eps_w, class="type">class="kw">double __eps_n, class="type">int __max_nodes);