您应该知道的 MQL5 向导技术(第 02 部分):Kohonen 映射·进阶篇
(2/3)· 多数交易者误把 functor 当二维图像,却忽略了直线映射才是盯盘降噪的关键切口
网络骨架的 C++ 类封装
在 MT5 里用面向对象方式搭神经网络,第一步是把「输入层—输出层」这对关系塞进一个哈希映射容器。下面这段类定义就是干这个的:继承 CHashMap,键是输入层指针,值是输出层指针。 构造函数里一口气 new 出输入层、输出层、键值对,并立刻 Add 进容器;同时预建三个神经元指针(i_neuron、o_neuron、best_neuron),后者通常用来在训练后存最优神经元。析构函数则负责释放 i 和 o 两层对象,避免 EA 反复加载卸载时漏内存。 开 MT5 自建 EA 时,可直接把这段类声明贴进头文件,再补一层权重矩阵就能跑最简单的感知机。外汇与贵金属波动高、滑点随机,这类模型仅作概率参考,实盘前务必用策略测试器跑足样本。
class Cnetwork : class="kw">public CHashMap<Cinput_layer*,Coutput_layer*> { class="kw">public: Cinput_layer *i; Coutput_layer *o; CKeyValuePair < Cinput_layer*, Coutput_layer* > *io; Cneuron *i_neuron; Cneuron *o_neuron; Cneuron *best_neuron; Cnetwork() { i = new Cinput_layer(); o = new Coutput_layer(); io = new CKeyValuePair<Cinput_layer*,Coutput_layer*>(i,o); Add(io); i_neuron = new Cneuron(); o_neuron = new Cneuron(); best_neuron = new Cneuron(); }; ~Cnetwork() { class="kw">delete i; class="kw">delete o;
◍ 神经单元对象的析构与端口读写接口
这段 C++ 风格的类实现片段,把几个堆上申请的神经元指针在作用域收尾时统一释放:io、i_neuron、o_neuron、best_neuron 全部走 delete,避免 MT5 自定义指标或 EA 长时间跑下来内存泄漏。 对外暴露的虚函数才是实战里常碰的:GetInputSize 返回输入向量维度 i.size,GetOutputIndex 取 o.index,SetOutputIndex 改完再 TrySetValue 回写。GetOutputSize / SetOutputSize 同理,用 o.size 控制输出层宽度。 跑回归或特征工程时,若你发现某次前向计算后输入长度对不上,优先在 TryGetValue(i,o) 这步下断点——它负责从存储结构把 i、o 两个结构体同步进来,很多维度错乱都出在同步时机而非算法本身。外汇与贵金属行情采样频率高,这类对象频繁创建销毁,建议直接在 MT5 策略测试器里挂一段最小复现看内存曲线。
class="kw">delete io; class="kw">delete i_neuron; class="kw">delete o_neuron; class="kw">delete best_neuron; }; class="kw">virtual class="type">int GetInputSize() { TryGetValue(i,o); class="kw">return(i.size); }; class="kw">virtual class="type">int GetOutputIndex() { TryGetValue(i,o); class="kw">return(o.index); }; class="kw">virtual class="type">void SetOutputIndex(const class="type">int Index) { TryGetValue(i,o); o.index=Index; TrySetValue(i,o); }; class="kw">virtual class="type">int GetOutputSize() { TryGetValue(i,o); class="kw">return(o.size); }; class="kw">virtual class="type">void SetOutputSize(const class="type">int Size) { TryGetValue(i,o); o.size=Size;
「自组织映射里的网络容器与神经元读写」
这段定义紧接前文的网络结构,给出一个 Cmap 类作为自组织映射(SOM)的容器,内部只挂了一个 Cnetwork 指针,意味着映射层不直接存权重,而是委托给具体的网络对象去管。 类里有两个静态成员:radius 作为常量是邻域半径基准,time 是动态衰减的计时变量。在 SOM 训练里,邻域大小通常随 time 增大而收缩,所以这两个值直接决定了神经元更新的空间范围与节奏。 前面的 Cnetwork 片段给出了神经元存取的虚函数实现:GetInNeuron 和 GetOutNeuron 先从 i、o 两个容器 TryGetValue 拿到当前层对象,再按 NeuronIndex 取具体神经元引用;SetInNeuron / SetOutNeuron 则反向写回。这种双层 Try 调用避免了越界崩溃,但也意味着每次读写都有容器查找开销。 在 MT5 里跑这套时,可以把 radius 初值设成网格边长的一半(例如 5×5 网格给 2.5),观察 time 步进后分类边界是否过早收敛。外汇与贵金属行情序列做 SOM 聚类本身是高风险的探索,结论只代表历史样本的可能结构,不构成方向判断。
o.Capacity(Size); TrySetValue(i,o); }; class="kw">virtual class="type">void GetInNeuron(const class="type">int NeuronIndex) { TryGetValue(i,o); i.TryGetValue(NeuronIndex,i_neuron); }; class="kw">virtual class="type">void GetOutNeuron(const class="type">int NeuronIndex) { TryGetValue(i,o); o.TryGetValue(NeuronIndex,o_neuron); }; class="kw">virtual class="type">void SetInNeuron(const class="type">int NeuronIndex) { i.TrySetValue(NeuronIndex,i_neuron); }; class="kw">virtual class="type">void SetOutNeuron(const class="type">int NeuronIndex) { o.TrySetValue(NeuronIndex,o_neuron); }; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class Cmap { class="kw">public: Cnetwork *network; class="kw">static const class="type">class="kw">double radius; class="kw">static class="type">class="kw">double time;
Cmap 结构体的初始状态设定
自组织映射(SOM)封装类里,Cmap 结构体先把量化误差 QE 和拓扑误差 TE 两个代理变量摆出来,作为后续训练质量的观测锚点。QE 衡量样本被映射到最近神经元后的失真程度,TE 反映最佳匹配神经元与次佳匹配神经元在网格上的距离偏离。 构造函数 Cmap() 里给了一组冷启动默认值:network 指针新建一个 Cnetwork 实例,initialised 置 false,time 归零,QE 预设 0.50、TE 预设 5000.0,refreshed 时间戳设为 1970.01.05。这组数字不是随便填的——QE=0.50 代表未训练时中等失真水平,TE=5000.0 则故意放得极大,方便在首轮训练前用 if(TE>1000) 之类判断直接跳过有效性检查。 开 MT5 把这段结构体抄进 EA 的 include 头文件,编译后在下单前打印 QE、TE 初始值,能确认你的映射对象没被其他模块意外改写。外汇与贵金属杠杆高,任何特征工程里的静默初始化错误都可能放大实盘风险。
class="type">class="kw">double QE; class=class="str">"cmt">//proxy for Quantization Error class="type">class="kw">double TE; class=class="str">"cmt">//proxy for Topological Error class="type">class="kw">datetime refreshed; class="type">bool initialised; Cmap() { network = new Cnetwork(); initialised=false; time=class="num">0.0; QE=class="num">0.50; TE=class="num">5000.0; refreshed=D&class="macro">#x27;class="num">1970.01.class="num">05&class="macro">#x27;; }; ~Cmap() {
◍ 清零结构体与静态常量初始化
在 Cmap 类析构或重置逻辑里,连续调用 ZeroMemory 把多个数组成员和标志位清空,避免上一轮 tick 的残留数据污染下一轮计算。被清零的包括 initialised、time、QE、TE、refreshed,都是该类内部用于记录状态与缓冲的字段。 ZeroMemory 是 MQL5 里按字节填零的底层函数,对 double 数组会置为 0.0,对 bool 或整型标志会置为 false/0。若不在每轮重算前做这一步,跨周期回测时可能出现‘假信号’——比如 refreshed 没清零导致某根 K 线被重复处理。 类外紧接着定义了两个静态成员:radius 直接取宏 IN_RADIUS;time 则用 10000/log(IN_RADIUS) 做归一化,且用 fmax(1.0, ...) 兜底防止半径为 1 时除零。假设 IN_RADIUS=10,time 静态值约为 10000/2.3026≈4342.94,这个常量会参与后续时间衰减类的距离计算。
ZeroMemory(initialised); ZeroMemory(time); ZeroMemory(QE); ZeroMemory(TE); ZeroMemory(refreshed); }; }; const class="type">class="kw">double Cmap::radius=IN_RADIUS; class="type">class="kw">double Cmap::time=class="num">10000/fmax(class="num">1.0,log(IN_RADIUS));
「训练循环里的半径与拓扑误差代理」
神经元训练本质是在输出层调整已有 functor 权重,并按竞争学习补入新神经元。权重调整比率里,最敏感的是迭代次数:每迭代一次就重算一个更小的半径(这里叫 functor 误差,本质是用欧氏距离量的邻域半径),迭代越多该值越小,网络贴合度倾向更好。学习速率要从接近 1 逐渐衰减到 0,否则后期会过冲。 拓扑误差在 Kohonen 映射里是衡量输出层离长期目标远近的关键量。原文用 functor 误差充当它的代理——保留更多输出层神经元,就倾向更接近目标映射。 下面这段训练函数把上述逻辑落到了代码。初始化时 TE 置 0、迭代计数和 functor_error 累加器清 0,学习速率取 m_training_rate。 while 循环跑满 m_training_iterations 次:每次用 GetTrainingRadius 拿当前半径,遍历输出神经元算欧氏 functor 误差,小于半径才累加误差并依重映射半径调权重;每轮学习速率按 exp(-iteration/总迭代) 衰减。循环后把训练样本写进新输出神经元(扩容输出层),若有匹配则 functor_error 求平均并乘 IN_RADIUS 赋给 Map.TE。 开 MT5 把 IN_RADIUS 和 m_training_iterations 调小一档,观察 Map.TE 的收敛速度变化,能直观验证迭代次数对拓扑贴合的敏感度。外汇与贵金属市场波动剧烈,此类自组织映射仅作概率性辅助,实盘需警惕模型过拟合高风险。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSignalKM::NetworkTrain(Cmap &Map,Cneuron &TrainNeuron) { Map.TE=class="num">0.0; class=class="str">"cmt">// 拓扑误差代理清零 class="type">int _iteration=class="num">0; class=class="str">"cmt">// 迭代计数器 class="type">class="kw">double _training_rate=m_training_rate; class=class="str">"cmt">// 学习速率初始值(接近1) class="type">int _err=class="num">0; class=class="str">"cmt">// 匹配神经元计数 class="type">class="kw">double _functor_error=class="num">0.0; class=class="str">"cmt">// functor误差累加 while(_iteration<m_training_iterations) class=class="str">"cmt">// 跑满设定迭代次数 { class="type">class="kw">double _current_radius=GetTrainingRadius(Map,_iteration); class=class="str">"cmt">// 当前邻域半径随迭代缩小 for(class="type">int i=class="num">0; i<=Map.network.GetOutputIndex(); i++) class=class="str">"cmt">// 遍历已有输出神经元 { Map.network.GetOutNeuron(i); class="type">class="kw">double _error = EuclideanFunctor(TrainNeuron,Map.network.o_neuron); class=class="str">"cmt">// 算欧氏functor误差 if(_error<_current_radius) class=class="str">"cmt">// 在半径内才调整 { _functor_error+=(_error); _err++; class="type">class="kw">double _remapped_radius = GetRemappedRadius(_error, _current_radius); SetWeights(TrainNeuron,Map.network.o_neuron,_remapped_radius,_training_rate); class=class="str">"cmt">// 按重映射半径调权重 Map.network.SetOutNeuron(i); } } _iteration++; _training_rate=_training_rate*exp(-(class="type">class="kw">double)_iteration/m_training_iterations); class=class="str">"cmt">// 学习速率指数衰减到0 } class="type">int _size=Map.network.GetOutputSize(), _index=Map.network.GetOutputIndex(); Map.network.SetOutputIndex(_index+class="num">1); class=class="str">"cmt">// 新增一个输出神经元位置 if(_index+class="num">1>=_size) { Map.network.SetOutputSize(_size+OUT_BUFFER); class=class="str">"cmt">// 容量不足则扩容 } Map.network.GetOutNeuron(_index+class="num">1); for(class="type">int w=class="num">0; w<IN_WIDTH; w++) class=class="str">"cmt">// 拷贝训练神经元输入 functor 到新神经元 { Map.network.o_neuron.fd.Set(w,TrainNeuron.fd.Get(w)); } for(class="type">int l=class="num">0; l<OUT_LENGTH; l++) class=class="str">"cmt">// 拷贝输出 functor { Map.network.o_neuron.fr.Set(l,TrainNeuron.fr.Get(l)); } Map.network.SetOutNeuron(_index+class="num">1); if(_err>class="num">0) class=class="str">"cmt">// 有匹配才计算平均functor误差并赋给TE { _functor_error/=_err; Map.TE=_functor_error*IN_RADIUS; } }