Kohonen 神经网络算法交易实战:从工具修正到基础铺垫(基础篇)
(1/3)· 多数 EA 优化只把自组织映像当肉眼看图,本篇先修源码错误再补工具链
◍ 把 Kohonen 网络从看图工具变成自动决策器
MQL5 社区里已有文章介绍如何用自组织特征映像(Kohonen 映像)做市场可视化分析,但实盘里多数用法止步于 EA 优化结果拓扑图的肉眼判断。2019 年 2 月 18 日发布的该系列开篇提到,这种依赖个人眼光的做法只用到网络很少一部分能力,权重、学习率、学习范围等机制并未参与自动提取知识。 原文作者点出的缺口是:现有 Kohonen 实现大多只当“画图螺栓”,没有接入正规的参数寻优或行情预测闭环。想在 MT5 里验证这一点,直接打开一篇旧 Kohonen 文章里的 EA,跑一次优化,观察映像图是否真能替你排出参数优劣——大概率仍要靠你自己读图。 本篇后续会先改旧源码、修 bug、做增强,再谈用网络预测财经数据。若你连“神经元”“链接”“学习范围”都陌生,先补概念再跟代码,否则后续逐行拆解会像看天书。外汇与贵金属市场高杠杆、高波动,任何网络输出都只是概率倾向,不是下单指令。
「Kohonen 网络代码里的几个硬伤」
原 CSOM 和 CSOMNode 类里神经元用像素坐标当构造参数,逻辑别扭且让调试变复杂。两个格子相同、数据相同、随机种子相同的网络,仅因成像尺寸不同就学到不同结果——这是实打实的 bug。改成用数组下标检索:x、y 直接对应输出层的列号和行号,初始化换用 InitNode(x, y),可视化时改像素尺寸不影响神经元逻辑坐标。 输入没做常规化是另一处坑。当特征量纲差很大(比如优化结果里总利润几十万,而锋锐比率是个位数、恢复因子是个位数),网络基本只认大分量。用附带的随机矢量程序测过:三个分量分别设在 [0,1000]、[0,1]、[-1,+1],不常规化时网络只在第一个平面分出拓扑,后两个分量全是噪声,分析能力只剩三分之一;开了常规化三个平面才都有空间排列。 常规化用最普通的减均值除西格玛,均值归零、标准差归一到 1。CSOM::Normalize 里套用这个公式,InitNormalization 先扫一遍数据集算每个分量的均值和标准差。出口处 CSOM::Denormalize 做逆运算把权重映射回实际范围;因常规化后数据在零附近,神经元初始化权重也从 [0,1] 改成 [-1,+1],学习效率会好一些。 迭代计数也别按老办法写死。原代码把「喂一个矢量」算一次迭代,样本量一变就得重调。改成 EpochNumber 定义「学习时期」——全样本随机投喂一遍算一个时期,时长与数据集规模解耦。还可开 ValidationSetPercent(默认 0),比如设 10 就把 10% 当验证集,每个时期算常规化均方差,误差抬头就早停,缓解过拟合。 六边形网格原来只是画成六边形,计算仍按矩形邻域,错了。正确距离要按行奇偶修正横向偏移:获胜神经元在奇数行、目标在偶数行时 shiftx=+0.5,反过来 -0.5,相邻行真实距离变成 sqrt(1+0.25)=1.118。原代码里「距离平方>1 就跳过」的加速判断在六边形下会误杀邻神经元,附源码里已注释掉,真要加速请用 3 倍西格玛 阈值。当前实现仍非完全各向同性,留了进一步修正空间。外汇与贵金属模型训练涉及高风险,回测结论仅代表样本内概率倾向,实盘须自担风险。
for(class="type">int i = class="num">0; i < total_nodes; i++) { class="type">class="kw">double DistToNodeSqr = (m_som_nodes[winningnode].X() - m_som_nodes[i].X()) * (m_som_nodes[winningnode].X() - m_som_nodes[i].X()) + (m_som_nodes[winningnode].Y() - m_som_nodes[i].Y()) * (m_som_nodes[winningnode].Y() - m_som_nodes[i].Y()); class="type">bool odd = ((winningnode % m_ycells) % class="num">2) == class="num">1; for(class="type">int i = class="num">0; i < total_nodes; i++) { class="type">bool odd_i = ((i % m_ycells) % class="num">2) == class="num">1; class="type">class="kw">double shiftx = class="num">0; if(m_hexCells && odd != odd_i) { if(odd && !odd_i) { shiftx = +class="num">0.5; } else class=class="str">"cmt">// 反之亦然 (!odd && odd_i) {
SOM邻域影响半径怎么算
这段逻辑出自自组织映射(SOM)训练循环里对邻居节点的权重调整。先用获胜节点与候选节点的坐标差算出平方距离 DistToNodeSqr,其中 shiftx 在边界情况下会被设成 -0.5,用来修正网格拓扑的偏移。 当 DistToNodeSqr 小于 WS(邻域半径平方)时,用 MathExp(-DistToNodeSqr / (2*WS)) 得到高斯型影响系数 influence,再调用 AdjustWeights 按学习率和该系数更新节点权重;影响随距离衰减,离获胜节点越近改得越多。 代码里还判断了 DistToNodeSqr < 9*WS 的条件,意味着实际参与计算的潜在邻居范围可能扩展到半径的三倍(3倍距离平方),但只有落进 WS 内的才真正调权。外汇与贵金属市场波动剧烈、杠杆风险高,这类算法仅作特征聚类参考,实盘信号须自行验证。
shiftx = -class="num">0.5; } } class="type">class="kw">double DistToNodeSqr = (m_node[winningnode].GetX() - (m_node[i].GetX() + shiftx)) * (m_node[winningnode].GetX() - (m_node[i].GetX() + shiftx)) + (m_node[winningnode].GetY() - m_node[i].GetY()) * (m_node[winningnode].GetY() - m_node[i].GetY()); if(DistToNodeSqr < WS) { class="type">class="kw">double influence = MathExp(-DistToNodeSqr / (class="num">2 * WS)); m_node[i].AdjustWeights(data, learning_rate, influence); } if(DistToNodeSqr < class="num">9 * WS)
◍ 把 Kohonen 网络拆成计算与绘图两层
Kohonen 网络不一定要配界面:预测、压缩、分类这些事完全可以只吐数字(类编号或事件概率)。原文把功能拆成 CSOM 和 CSOMDisplay 两个类——前者只管计算、数据和网络文件的读写,后者专门放图形。相比把图形塞进基础类的做法,这种分层在后续选 EA 参数时更顺手:CSOMDisplay 用来挑参数,CSOM 用来做预测。 网格形态(矩形或六边形)属于基础架构,因为它直接改变距离计算方式。节点行列数、输入空间维度、网格类型都必须写进网络文件;从文件加载时也只能读文件,不能读程序设置。像像素尺寸、是否画细胞边界、是否显示字幕这类只影响视觉的参数,不进文件,训练完随便改。 未派生的 CSOM 不提供带控件的 GUI,所有配置走 MQL 程序 input。CSOMDisplay 仍保留实用能力:老示例里的 MaxPictures 输入还在,作为 maxpict 传给 CSOMDisplay::Init,决定一行塞几张平面映像。配合 ImageW、ImageH 能凑出全屏适配;但映像一多(比如扫大量 EA 设置),尺寸就被压到很难看清。 把 MaxPictures 设 0 可切到时间轴模式:映像从 OBJ_BITMAP_LABEL(像素对齐)换成 OBJ_BITMAP(时间刻度对齐),能放大到占满图表高度,用横向滚动条、滚轮或键盘拖拽。数量不再受屏幕限制,但要保证柱线数够。放大的好处是细胞里能看突触权重、命中次数、均值和离散度——默认不画,鼠标悬停弹 tooltip,里面带平面名和神经元坐标。 双击任意神经元,会在当前和所有平面里用互补色同步高亮,方便横向比对所有特征的活动。顺带一提,绘图已迁到标准类 CCanvas,Y 轴改成自上而下计数,图例因此跑到映像上方而非下方,功能无影响。
「给 Kohonen 网络补五张服务平面」
要把裸的 SOM 映像用起来,先得在 CSOMNode 里扩出 5 个附加维度。除了原本 2D 突触权重图,U 型矩阵、命中次数、量化误差、网络响应、聚类这五张服务平面才是后面能拿来做决策的底图。 U 型矩阵算的是每个神经元和四邻或六邻(正方形网格 nbhsize=4,六边形=6)的平均权重距离。权重差大的地方在调色板里是“峰”,差小的是“洼地”——洼地才是后续聚类的种子。遍历邻域用模板类 Neighborhood 的 loop 方法,它按模板方法范式留了 reset/iterate/getResult 三个抽象钩子,类型 T 参数化,避免每处都重写边界判断。 命中与量化误差在 RegisterPatternHit 里顺手算:m_hitCount++ 记密度,再取获胜神经元 code vector 算输入向量与权重的分量差平方和。CSOM::CalculateStats 在每个 epoch 收敛阶段被调,把所有量化误差求和去比 m_dataMSE,就是前面验证章说的 NMSE;返回值一旦开始抬头,Train 就停。 网络响应最简单,给每个神经元投喂信号(缺测试向量就喂零向量)记下激活偏移,用来判断获胜神经元是不是压着一群接近的对手。最后聚类平面,先用 ALGLIB 的 KMeansGenerate(头文件 <alglib/dataanalysis.mqh>,KMEANS_RETRY_NUMBER=10 次重选初值),但它必须预置聚类数、球状假设、易陷局部极小,且 ALGLIB 自带随机数状态不随 MathSrand 重置,OnInit 里不清 CMath::m_state 就难复现。 替代方案直接吃自己人:把神经元按 GetDistance()*MathSqrt(GetMSE()) 排序(原伪代码用 GetDistance()/sqrt(GetHitsCount()),乘以量化误差更稳),从最小 U 距离起建聚类,邻域检查交给 Neighborhood 的后代类,只“流”向峰值高度更低的邻居。这套不预置数量、形状随意、按重要性排序的玩法我称作 Korotky 聚类。多轮实测里 K-均值给出的中心在分析优化结果时反而更差,所以后面只接这套。外汇/贵金属样本做 SOM 聚类波动大,结论仅代表历史样本结构,实盘高风险。