在算法交易中 KOHONEN 神经网络的实际应用 第二部分优化和预测·综合运用
◍ 自组织映射的预测掩码与方向校验
一段 2016.12.27 00:00:00 的欧元美元小时差分序列给出了 10 个特征值:-0.000245、0.000472、-0.001075、-0.001237、-0.003225、-0.000592、-0.005290、-0.000883、0.004579、0.003232,正负交替说明该时段波动无单边倾向,外汇与贵金属这类高杠杆品种须警惕突变风险。 进入预测模式时,先对 Kohonen 映射的最后一项特征做掩码清零,让网络在匹配时不窥探“未来”。下面这段是核心做法: [CODE] if(ForecastMode) { KohonenMap.SetFeatureMask(KohonenMap.GetFeatureCount() - 1, 0); } if(ForecastMode) { int m = KohonenMap.GetFeatureCount(); KohonenMap.SetFeatureMask(m - 1, 0); int n = KohonenMap.GetDataCount(); double vector[]; double forecast[]; double future; int correct = 0; double error = 0; double variance = 0; for(int i = 0; i < n; i++) { KohonenMap.GetPattern(i, vector); future = vector[m - 1]; // 为将来保留 vector[m - 1] = 0; // 使网络的未来未知(由于掩码,它无论如何都不使用) KohonenMap.GetBestMatchingFeatures(vector, forecast); if(future * forecast[m - 1] > 0) // 检查方向是否匹配 { correct++; } error += (future - forecast[m - 1]) * (future - forecast[m - 1]); variance += future * future; } Print("Correct forecasts: ", correct, " out of ", n, " => ", DoubleToString(correct * 100.0 / n, 2), "%, error => ", error / variance); } [/CODE] 逐行看:GetFeatureCount()-1 是最后一列(目标值)索引,SetFeatureMask(...,0) 把它权重置零;循环里先把真实 future 存下,再把 vector 对应位填 0,双重保险让 BMU 搜索只用历史特征。方向命中靠 future*forecast 是否同号,error/variance 给出归一化均方误差,跑完 Print 出“正确率 % 和误差比”,你开 MT5 接自己品种就能复算。 SetClusterLabels 则换思路:遍历每个特征取 min/max,把各聚类中心值归一到 [0,1],best 记录该特征在哪些聚类最具区分度。外汇贵金属复盘时,这类标签能帮你把行情切成若干状态簇,但聚类结果只代表历史样本结构,对下一根 K 线仅是概率倾向。
if(ForecastMode) { KohonenMap.SetFeatureMask(KohonenMap.GetFeatureCount() - class="num">1, class="num">0); } if(ForecastMode) { class="type">int m = KohonenMap.GetFeatureCount(); KohonenMap.SetFeatureMask(m - class="num">1, class="num">0); class="type">int n = KohonenMap.GetDataCount(); class="type">class="kw">double vector[]; class="type">class="kw">double forecast[]; class="type">class="kw">double future; class="type">int correct = class="num">0; class="type">class="kw">double error = class="num">0; class="type">class="kw">double variance = class="num">0; for(class="type">int i = class="num">0; i < n; i++) { KohonenMap.GetPattern(i, vector); future = vector[m - class="num">1]; class=class="str">"cmt">// 为将来保留 vector[m - class="num">1] = class="num">0; class=class="str">"cmt">// 使网络的未来未知(由于掩码,它无论如何都不使用) KohonenMap.GetBestMatchingFeatures(vector, forecast); if(future * forecast[m - class="num">1] > class="num">0) class=class="str">"cmt">// 检查方向是否匹配 { correct++; } error += (future - forecast[m - class="num">1]) * (future - forecast[m - class="num">1]); variance += future * future; } Print("Correct forecasts: ", correct, " out of ", n, " => ", DoubleToString(correct * class="num">100.0 / n, class="num">2), "%, error => ", error / variance); } class="type">void SetClusterLabels() { class="kw">const class="type">int nclusters = KohonenMap.GetClusterCount(); class="type">class="kw">double min, max, best; class="type">class="kw">double bests[][class="num">3]; class=class="str">"cmt">// [][class="num">0 - value; class="num">1 - feature index; class="num">2 - direction] ArrayResize(bests, nclusters); ArrayInitialize(bests, class="num">0); class="type">int n = KohonenMap.GetFeatureCount(); for(class="type">int i = class="num">0; i < n; i++) { class="type">int direction = class="num">0; KohonenMap.GetFeatureBounds(i, min, max); if(max - min > class="num">0) { best = class="num">0; class="type">class="kw">double center[]; for(class="type">int j = nclusters - class="num">1; j >= class="num">0; j--) { KohonenMap.GetCluster(j, center); class="type">class="kw">double value = MathMin(MathMax((center[i] - min) / (max - min), class="num">0), class="num">1);
自组织映射的标签绑定与网格规模拐点
自组织映射(SOM)训练里,每个聚类节点要绑定一个方向标签。下面这段逻辑先按 value 是否过 0.5 切分多空:大于阈值的记为正向,否则反向并把 value 翻成 1-value,随后用 bests 数组追踪该聚类下最强响应的特征索引与方向。 [CODE]if(value > 0.5) { direction = +1; } else { direction = -1; value = 1 - value; } if(value > bests[j][0]) { bests[j][0] = value; bests[j][1] = i; bests[j][2] = direction; } [/CODE] 上面代码逐行拆:第1行判断响应强度是否过半;第3行过阈则方向赋+1;第6行不过阈则方向赋-1;第7行把 value 取补数,使弱响应也能归一比较;第10行若该响应强于聚类历史最优则进入更新;第12-14行分别存响应值、特征序号、方向进 bests[j] 的三列。 训练日志显示网格并不是越大越好。10*10 六角网在 104 次迭代停训,NMSE=0.3845;24*24 在 119 次迭代 NMSE 降到 0.1491;但 26*26 反而 NMSE 回升到 0.1670,系统据此抛出「Exit map size increments due to increased MSE」,停止继续放大地图。 验证集上,unity500xau 用 50 根样本测得 24 根方向判对,准确率 48.00%,误差 1.0215;换 unity1000xau3 的 29 维特征、100 根样本,判对 58 根,准确率 58.00%,误差 1.1312。外汇与贵金属走势受杠杆与跳空影响,这类 SOM 方向预判仅代表历史样本内的概率倾向,实盘须以 MT5 回测并控仓。 重训练触发写在指标主循环里:当 K 线总数变动或距上次训练超过 RetrainingBars 根,且 NetFileName 非空,才拉起再训练。这样能避免每根 Tick 重算整网,把算力压在低频刷新上。
if(value > class="num">0.5) { direction = +class="num">1; } else { direction = -class="num">1; value = class="num">1 - value; } if(value > bests[j][class="num">0]) { bests[j][class="num">0] = value; bests[j][class="num">1] = i; bests[j][class="num">2] = direction; }
「自组织映射的载入训练与预测落地」
这段逻辑把 SOM(自组织映射)网络在 MT5 指标里的生命周期拆成了三步:先尝试载入已训练好的图,失败就现场重训,最后用最近完成的 K 线做预测。 初始化阶段会判断 NetFileName 指定的网络文件能否 Load 成功。若返回 false,直接 Print 报错、把 initDone 置否并 return 0,指标不会继续跑——这意味着你放错路径或文件名,图表上什么信号都不会出。 [CODE] { if(!KohonenMap.Load(NetFileName)) { Print("图载入失败: ", NetFileName); initDone = false; return 0; } } else { TrainSOM(BarLimit); } prev_training = prev_calculated > 0 ? prev_calculated : rates_total; } ForecastBySOM(prev_calculated == 0); } CSOM KohonenMap; bool TrainSOM(const int limit) { KohonenMap.Reset(); LoadPatterns(limit); KohonenMap.Init(CellsX, CellsY, HexagonalCell); KohonenMap.SetFeatureMask(KohonenMap.GetFeatureCount() - 1, 0); KohonenMap.Train(EpochNumber, UseNormalization, ShowProgress); if(SaveTrainedNetworks) { KohonenMap.Save("online-" + _Symbol + CSOM::timestamp() + ".som"); } return true; } bool ForecastBySOM(const bool anew = false) { double vector[], forecast[]; int n = workCurrencies.getSize(); ArrayResize(vector, n + 1); for(int j = 0; j < n; j++) { vector[j] = GetBuffer(j, 1); // 第一个柱是最近完成的 } vector[n] = 0; KohonenMap.GetBestMatchingFeatures(vector, forecast); buffers[n][0] = forecast[n]; if(anew) buffers[n][1] = GetBuffer(n - 1, 1); return true; } [/CODE] TrainSOM 里先 Reset 再 LoadPatterns(limit),然后用 CellsX、CellsY 和 HexagonalCell 初始化网格,SetFeatureMask 把最后一个特征的权重设 0,等于忽略该维做聚类。Train 的轮数由 EpochNumber 控制,开 ShowProgress 能在日志看到迭代过程。 ForecastBySOM 把 n 个货币对的缓冲读进 vector,下标 1 代表最近完成的柱(注释已写明),末位补 0 后交给 GetBestMatchingFeatures 找最佳匹配神经元,结果写回 buffers[n][0]。外汇与贵金属波动剧烈,SOM 给出的只是基于历史形态的匹配倾向,实盘前务必用历史数据回测验证。 文件名带 CSOM::timestamp() 能保证每次保存的网络不覆盖,多品种同时跑也不会串。想快速验证,把 SaveTrainedNetworks 设 true 跑一遍,看 data 文件夹里是否生成 online-XXX.som。
{
if(!KohonenMap.Load(NetFileName))
{
Print("图载入失败: ", NetFileName);
initDone = class="kw">false;
class="kw">return class="num">0;
}
}
else
{
TrainSOM(BarLimit);
}
prev_training = prev_calculated > class="num">0 ? prev_calculated : rates_total;
}
ForecastBySOM(prev_calculated == class="num">0);
}
CSOM KohonenMap;
class="type">bool TrainSOM(class="kw">const class="type">int limit)
{
KohonenMap.Reset();
LoadPatterns(limit);
KohonenMap.Init(CellsX, CellsY, HexagonalCell);
KohonenMap.SetFeatureMask(KohonenMap.GetFeatureCount() - class="num">1, class="num">0);
KohonenMap.Train(EpochNumber, UseNormalization, ShowProgress);
if(SaveTrainedNetworks)
{
KohonenMap.Save("online-" + _Symbol + CSOM::timestamp() + ".som");
}
class="kw">return true;
}
class="type">bool ForecastBySOM(class="kw">const class="type">bool anew = class="kw">false)
{
class="type">class="kw">double vector[], forecast[];
class="type">int n = workCurrencies.getSize();
ArrayResize(vector, n + class="num">1);
for(class="type">int j = class="num">0; j < n; j++)
{
vector[j] = GetBuffer(j, class="num">1); class=class="str">"cmt">// 第一个柱是最近完成的
}
vector[n] = class="num">0;
KohonenMap.GetBestMatchingFeatures(vector, forecast);
buffers[n][class="num">0] = forecast[n];
if(anew) buffers[n][class="num">1] = GetBuffer(n - class="num">1, class="num">1);
class="kw">return true;
}◍ 把工具请下神坛
Kohonen 网络在 MT5 里不是黑箱预言机,而是一套可加载、可改参、可复跑的向量映射类。随文附带的 Kohonen2MQL5.zip 约 302.62 KB,解压后能在本地直接编译验证,不必等别人喂结论。 它真正吃功夫的地方在输入变量筛选、历史深度和参数组合——同一套 SOM,喂收盘价和喂波动率,出来的聚类边界完全不同,成功概率基本取决于你自己的预处理逻辑。 MQL5 后来引入了原生 vector 类型,旧源码里的变量名得手动改成 _vector 之类才能过编,这是 2022 年 4 月作者在原帖补的坑,开老工程前先搜一遍命名冲突。 外汇和贵金属行情高杠杆、易跳空,拿这类网络做辅助分层可以,别当信号源裸跟;跑通一次再谈适配你自己的品种节奏。