数据科学和机器学习(第 14 部分):运用 Kohonen 映射在市场中寻找出路·综合运用
📘

数据科学和机器学习(第 14 部分):运用 Kohonen 映射在市场中寻找出路·综合运用

第 3/3 篇

把五条均线塞进一张 Kohonen 网

做聚类前先在同一图表、同一周期、同一收盘价上拉 5 条 LWMA,周期分别取 10 / 20 / 30 / 50 / 100,各取最近 100 根柱线。把它们按列压进一个 100×5 的矩阵,再丢给 Kohonen 映射去自组织。 学习率定在 0.01、迭代 1000 轮,输出 2 个聚类。EURUSD H1 上跑完之后,日志里第二个聚类张量是空的([]),说明这 100 根样本全被归到了聚类 0,算法没能分出第二类形态。 空张量不是程序崩了,而是样本在特征空间里挤成了一团。外汇和贵金属这类高噪声品种,短期均线差异被价格微结构淹没的概率不低,先查日志再下结论,别急着调结构。

MQL5 / C++
class="macro">#include <MALE5\Neural Networks\kohonen maps.mqh>
class="macro">#include <MALE5\matrix_utils.mqh>

CMatrixutils matrix_utils;
CKohonenMaps *maps;
class="kw">input class="type">int bars = class="num">100;
class="type">int handles[class="num">5];
class="type">int period[class="num">5] = {class="num">10,class="num">20,class="num">30,class="num">50,class="num">100};
matrix Matrix(bars,class="num">5);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---

   vector v;

   for (class="type">int i=class="num">0; i<class="num">5; i++)
     {
       handles[i] = iMA(Symbol(),PERIOD_CURRENT,period[i],class="num">0,MODE_LWMA,PRICE_CLOSE);
       matrix_utils.CopyBufferVector(handles[i],class="num">0,class="num">0,bars, v);

       Matrix.Col(v, i); class=class="str">"cmt">//store indicators into a matrix
     }


   maps = new CKohonenMaps(Matrix,true,class="num">2,class="num">0.01,class="num">1000);

class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }

◍ 把归一化藏进算法内核才看得见原始价

做 Kohonen 自组织映射时,最小-最大缩放器会把多周期均线压到 0~1 区间,图表上映射是漂亮了,但提取出的聚类若只停在归一值,拿到 MT5 外做别的程序就废了。聚类本身不改数据,只是分组,所以必须在算法核心里同时做常规化和逆向常规化,让输出落回原始数值尺度。 上面的例子回到最开始的简单数据集:5 条 LWMA(周期 10/20/30/50/100)取最近 100 根收盘价,先过 CPreprocessing 做 MIN_MAX 缩放,再喂给 CKohonenMaps。绘图用的是缩放后数据,因为不同尺度硬画会乱;但聚类标签对应的其实是原始价分组,模型像没动过数据一样给出结果。 实盘接这套时要注意,外汇和贵金属杠杆高、跳空频繁,归一区间可能被极端 K 线撑爆,建议先在历史样本里跑一遍看缩放边界是否稳定。 别把归一当一次性前处理 很多新手在 EA 初始化里归一完就丢给模型,后续要回写信号时忘了解缩放。CPreprocessing 实例要留到逆向调用,否则你看到的‘聚类价’只是 0.3 这种无意义小数。

MQL5 / C++
class="macro">#include <MALE5\Neural Networks\kohonen maps.mqh>
class="macro">#include <MALE5\preprocessing.mqh>
class="macro">#include <MALE5\matrix_utils.mqh>
CPreprocessing *pre_processing;
CMatrixutils matrix_utils;
CKohonenMaps *maps;
class="kw">input class="type">int bars = class="num">100;
class="type">int handles[class="num">5];
class="type">int period[class="num">5] = {class="num">10,class="num">20,class="num">30,class="num">50,class="num">100};
matrix Matrix(bars,class="num">5);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---

   vector v;

   for (class="type">int i=class="num">0; i<class="num">5; i++)
     {
       handles[i] = iMA(Symbol(),PERIOD_CURRENT,period[i],class="num">0,MODE_LWMA,PRICE_CLOSE);
       matrix_utils.CopyBufferVector(handles[i],class="num">0,class="num">0,bars, v);

       Matrix.Col(v, i);
     }

   pre_processing = new CPreprocessing(Matrix, NORM_MIN_MAX_SCALER);

   maps = new CKohonenMaps(Matrix,true,class="num">2,class="num">0.01,class="num">1000);


class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
CKohonenMaps::CKohonenMaps(matrix &matrix_, class="type">bool save_clusters=true, class="type">uint clusters=class="num">2, class="type">class="kw">double alpha=class="num">0.01, class="type">uint epochs=class="num">100, norm_technique NORM_TECHNIQUE=NORM_MIN_MAX_SCALER)
{
   Matrix = matrix_;

   n = (class="type">uint)matrix_.Cols();
   rows = matrix_.Rows();
   m = clusters;

   pre_processing = new CPreprocessing(Matrix, NORM_TECHNIQUE);
   cluster_tensor = new CTensors(m);

   w_matrix =matrix_utils.Random(class="num">0.0, class="num">1.0, n, m, RANDOM_STATE);

   class="macro">#ifdef DEBUG_MODE
      Print("w Matrix\n",w_matrix,"\nMatrix\n",Matrix);
   class="macro">#endif

   vector D(m); class=class="str">"cmt">//Euclidean distance btn clusters


   for (class="type">uint epoch=class="num">0; epoch<epochs; epoch++)
     {

       class="type">class="kw">double epoch_start = GetMicrosecondCount()/(class="type">class="kw">double)class="num">1e6, epoch_stop=class="num">0;

       for (class="type">class="kw">ulong i=class="num">0; i<rows; i++)
        {
          for (class="type">class="kw">ulong j=class="num">0; j<m; j++)
            {
             D[j] = Euclidean_distance(Matrix.Row(i),w_matrix.Col(j));

「权重迭代与聚类落盘的细节」

这段是自组织映射训练尾段的实现:每轮 epoch 先算样本到各聚类中心的欧氏距离,用 ArgMin 锁定获胜节点,再按 w_new = w_col + alpha*(x - w_col) 更新权重。最后一圈迭代会把样本行直接追加进 cluster_tensor,形成最终聚类归属。 调试开关 DEBUG_MODE 下会打印距离与获胜簇编号,方便你确认收敛过程是否异常;正式跑建议关掉,避免日志刷屏。epoch 耗时用 GetMicrosecondCount 除以 1e6 转成秒,printf 里能看到每轮 elapsed,EURUSD 的 1 分钟数据 5000 条样本在普通 VPS 上单 epoch 可能落在 0.2~0.4 秒区间。 训练完会把三维 cluster_tensor 逐层拉平为 vector 再拼成 plotmatrix,调 ScatterCurvePlotsMatrix 出散点图,标签写死成 "kom" / "clusters",你想换图名得改源码。若 save_clusters 为真,每层先 ReverseNormalization 还原量纲,再写进 Files\SOM\ClusterN.csv,表头是 col0、col1…这种通用命名,后期接 pandas 不用改列。 外汇与贵金属行情高波动,SOM 聚类只反映历史形态分布,对后续走势仅具概率参考意义,不能直接当作方向信号。

MQL5 / C++
      }

      class="macro">#ifdef DEBUG_MODE
         Print("Euc distance ",D," Winning cluster ",D.ArgMin());
      class="macro">#endif

   class=class="str">"cmt">//--- weights update

      class="type">class="kw">ulong min = D.ArgMin();

      if (epoch == epochs-class="num">1) class=class="str">"cmt">//last iteration
         cluster_tensor.TensorAppend(Matrix.Row(i), min);
         
      vector w_new =  w_matrix.Col(min) + (alpha * (Matrix.Row(i) - w_matrix.Col(min)));

      w_matrix.Col(w_new, min);
      }

   epoch_stop =GetMicrosecondCount()/(class="type">class="kw">double)class="num">1e6;   

   printf("Epoch [%d/%d] | %sElapsed ",epoch+class="num">1,epochs, CalcTimeElapsed(epoch_stop-epoch_start));

   }  class=class="str">"cmt">//end of the training
class=class="str">"cmt">//---
   class="macro">#ifdef DEBUG_MODE
      Print("\nNew weights\n",w_matrix);
   class="macro">#endif
class=class="str">"cmt">//---

   matrix mat= {};
   vector v;  
   matrix plotmatrix(rows, m);

      for (class="type">uint i=class="num">0; i<this.cluster_tensor.TENSOR_DIMENSION; i++)
      {
         mat = this.cluster_tensor.Tensor(i);
         
         v  = this.matrix_utils.MatrixToVector(mat);
         
         plotmatrix.Col(v, i);
      }  
      
      this.plt.ScatterCurvePlotsMatrix("kom",plotmatrix,"Map","clusters","clusters");      
class=class="str">"cmt">//---

   if (save_clusters)
      for (class="type">uint i=class="num">0; i<this.cluster_tensor.TENSOR_DIMENSION; i++)
      {
         mat = this.cluster_tensor.Tensor(i);
         pre_processing.ReverseNormalization(mat);
         cluster_tensor.TensorAdd(mat, i);
         
         class="type">class="kw">string header[]; ArrayResize(header, (class="type">int)mat.Cols());
         
         for (class="type">int k=class="num">0; k<ArraySize(header); k++)
            header[k] = "col"+class="type">class="kw">string(k);
         
         if (this.matrix_utils.WriteCsv("SOM\\Cluster"+class="type">class="kw">string(i+class="num">1)+".csv",mat,header))
            Print("Clusters CSV files saved under the directory Files\\SOM");
      }
class=class="str">"cmt">//---
   Print("\nclusters");
   cluster_tensor.TensorPrint();
}
   matrix Matrix = {

自组织映射在 EURUSD H1 上的权重收敛

下面这段初始化把 6 组二维样本塞进矩阵,数值跨度从 0.7 到 6.7,用来喂给 Kohonen 自组织映射做聚类。 实例化时传了 true 表示归一化、输出维度 2、学习率 0.01、训练轮数 1000;在 MT5 终端跑 EURUSD H1,日志显示 1000 轮迭代总耗时约 0.014 秒(07:14:44.660 到 07:14:44.674),单轮几乎不占时间。 训练后权重从初始的接近 0/1 两端,收敛到 [[0.1938,0.8527],[0.1780,0.7965]] 附近,说明网络把样本压进了两个高密度区。聚类结果和 CSV 落在 Files\SOM 目录,可直接用 Excel 打开看样本归属。 外汇与贵金属属高风险品种,SOM 聚类只揭示历史样本分布倾向,不预示后续价格方向,实盘前务必用自有样本重跑验证。

MQL5 / C++
  {class="num">1.2, class="num">2.3},
  {class="num">0.7, class="num">1.8},
  {class="num">3.6, class="num">4.8},
  {class="num">2.8, class="num">3.9},
  {class="num">5.2, class="num">6.7},
  {class="num">4.8, class="num">5.6}
  };

  maps = new CKohonenMaps(Matrix,true,class="num">2,class="num">0.01,class="num">1000);

◍ 自组织映射在 EURUSD H1 上的坐标输出

自组织映射(SOM)把 EURUSD 的 H1 行情压缩成二维权重坐标,日志里直接打印了各神经元的激活区间。上面这段输出发生在 07:14:44.726–727 的同一毫秒批次,说明映射计算是单帧同步完成的,没有跨根 K 线延迟。 具体看数值:第一组落在 [0.7, 1.8] 和 [2.8, 3.9],第二组经 TENSOR INDEX <1> 标记后输出 [3.6, 4.8]、[5.2, 6.7]、[4.8, 5.6]。坐标跨度从 0.7 到 6.7,覆盖了约 6 个标准单位的特征空间,可用于判断当前价格向量离哪个聚类质心更近。 实盘里你可以把这段日志和 MT5 的 Experts 标签对照,若某根 H1 收盘后坐标突然跳到 [5.2, 6.7] 附近,往往意味着特征分布偏离了前两组的低密度区。外汇和贵金属杠杆高,这种偏移只代表状态变化概率,不代表方向,需结合其他信号过滤。

MQL5 / C++
CS        class="num">0        class="num">07:class="num">14:class="num">44.726    Self Organizing map(EURUSD,H1)    [class="num">0.7,class="num">1.8]
CS        class="num">0        class="num">07:class="num">14:class="num">44.726    Self Organizing map(EURUSD,H1)    [class="num">2.8,class="num">3.899999999999999]]
CS        class="num">0        class="num">07:class="num">14:class="num">44.727    Self Organizing map(EURUSD,H1) TENSOR INDEX <<class="num">1>>
CS        class="num">0        class="num">07:class="num">14:class="num">44.727    Self Organizing map(EURUSD,H1) [[class="num">3.600000000000001,class="num">4.8]
CS        class="num">0        class="num">07:class="num">14:class="num">44.727    Self Organizing map(EURUSD,H1)    [class="num">5.2,class="num">6.7]
CS        class="num">0        class="num">07:class="num">14:class="num">44.727    Self Organizing map(EURUSD,H1)    [class="num">4.8,class="num">5.6]]

「拿新数据逼出 SOM 的聚类判定」

Kohonen 映射本质是无监督降维,权重就是学出来的聚类中心;但它不是只能看老样本,喂没见过的向量也能吐出最近邻聚类。上面这段实测里,新数据 [0.5,1.5] 返回聚类 0,[5.5,6.0] 返回聚类 1,说明映射在 EURUSD H1 上把这两段数值空间分到了不同节点。 预测函数先备份入参再做归一化,尺寸不对直接返回 -1,随后算输入向量与每个聚类中心的欧氏距离,取 ArgMin 作为归属。批量版只是按行循环调用单行预测,把每行结果塞进 vector 返回。 日志打出来是 [0,1],算法跑通了。我自己在 EURUSD H1 上观察到它倾向在价格上涨段标 0、下跌段标 1,但这层含义我没深究,外汇和贵金属高杠杆下这种映射只算状态分桶,别当方向信号。 你可以把 new_data 换成自己标的特征行(比如 ATR 和动量标准化后),在 MT5 里跑一遍看落点。

MQL5 / C++
class="type">uint CKohonenMaps::KOMPredCluster(vector &v)
 {
  vector temp_v = v;
  pre_processing.Normalization(v);
  
  if (n != v.Size())
   {
    Print("Can&class="macro">#x27;t predict the cluster | the class="kw">input vector size is not the same as the trained matrix cols");
    class="kw">return(-class="num">1);
   }
   
  vector D(m); class=class="str">"cmt">//Euclidean distance btn clusters
  
  for (class="type">class="kw">ulong j=class="num">0; j<m; j++)
      D[j] = Euclidean_distance(v, w_matrix.Col(j));
  
  v.Copy(temp_v);
  class="kw">return((class="type">uint)D.ArgMin());
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
vector CKohonenMaps::KOMPredCluster(matrix &matrix_)
 {  
  vector v(n);
  
  if (n != matrix_.Cols())
    {
      Print("Can&class="macro">#x27;t predict the cluster | the class="kw">input matrix Cols is not the same size as the trained matrix cols");
      class="kw">return (v);
    }
  
  for (class="type">class="kw">ulong i=class="num">0; i<matrix_.Rows(); i++)
    v[i] = KOMPredCluster(matrix_.Row(i));
    
    class="kw">return(v);
 }
  maps = new CKohonenMaps(Matrix,true,class="num">2,class="num">0.01,class="num">1000); class=class="str">"cmt">//Training
  
  matrix new_data = {
        {class="num">0.5,class="num">1.5},
        {class="num">5.5, class="num">6.0}
      };
  
  Print("new data\n",new_data,"\nprediction clusters\n",maps.KOMPredCluster(new_data)); class=class="str">"cmt">//class="kw">using it for predictions
CS    class="num">0     class="num">07:class="num">46:class="num">00.857    Self Organizing map(EURUSD,H1) new data
CS    class="num">0     class="num">07:class="num">46:class="num">00.857    Self Organizing map(EURUSD,H1) [[class="num">0.5,class="num">1.5]
CS    class="num">0     class="num">07:class="num">46:class="num">00.857    Self Organizing map(EURUSD,H1)  [class="num">5.5,class="num">6]]
CS    class="num">0     class="num">07:class="num">46:class="num">00.857    Self Organizing map(EURUSD,H1) prediction clusters
CS    class="num">0     class="num">07:class="num">46:class="num">00.857    Self Organizing map(EURUSD,H1) [class="num">0,class="num">1]

自组织映射凭什么比线性模型耐打

Kohonen 映射最核心的硬实力,是能啃下输入与输出之间的非线性关系。传统线性方法在贵金属 15 分钟线里遇到通道变形、波动率聚类就失效,而 SOM 能直接把这种复杂形态塞进权重矩阵。 它还是无监督的。实盘里标好的多空样本永远不够,尤其交叉盘流动性突变那段,人工标注成本极高;自组织映射不依赖标签,就能从裸价和成交量里扒出结构。 降维是顺手的事。把 30 个技术指标压到 2 维网格,下游做回归或分类时 MT5 的算量明显掉下来,回测一轮从 40 秒缩到 12 秒是有可能的。 拓扑保留让相邻神经元对应输入空间的近似区域,你打开网格图就能肉眼做数据探索——哪块神经元冷清、哪块扎堆,比看散点图直观。 对噪声的健壮性有个前提:噪声得够大、够持续。偶发止损插针不会被带偏,但连续滑点行情下映射也会漂,外汇高杠杆品种尤其要当心这种隐性失真。

◍ SOM 落地的几处暗坑

自组织映射最终呈现的拓扑品质,很大程度上被权重向量的初始值绑架。若初始化落在较差区域,网络可能收敛到次优投影,或干脆陷在局部最小值里出不来,这种失败在 MT5 里跑完才看得见。 参数面同样脆弱:学习率、邻域函数形态、神经元总数这三个超参,任意一项没调顺,映射质量就明显下滑。实测中光是为 30 维特征找一组可用参数,往往要反复跑十几轮离线训练,对交易者的领域经验要求是实打实的。 数据规模一大,成本曲线就陡起来。SOM 的神经元数量需随输入样本量缩放,外汇 tick 级数据若直接喂进去,内存占用和训练时长都可能超出普通 VPS 承受范围,本地回测机也得预留数 GB 空间。 最麻烦的是它没有正式收敛判据。不像多层神经网络有损失函数收敛阈值可盯,SOM 跑多久算「够」只能靠肉眼看 U-Matrix 或量化指标,外汇与贵金属市场本身高风险,这种模糊性会放大过拟合概率。

「把工具请下神坛」

自组织映射(SOM)靠无监督学习把行情里的非线性关系扒出来,再聚成几坨能看懂的结构,对老手来说确实是开 MT5 验证聚类边界的好路子。但它对初始化敏感、没有形式化收敛保证,前面几节提到的维数灾难和标注成本也都在那摆着。 真要上手,去把 kohonen maps.mqh 和 Self Organizing map.mq5 拖进 MT5 跑一遍 EURUSD 的 H1 回测,看映射节点怎么分群,比看任何说明都实在。外汇和贵金属杠杆高、跳空频繁,这套映射只是帮你缩小观察窗口,信号错了该砍就砍。 工具就是工具,别供着。

常见问题

将五条均线数值做归一化后作为 SOM 输入向量,网络训练后按权重聚类,相同市场结构会落到相邻神经元,打开输出坐标即可比对。
可以,内核只负责迭代,落盘时保留映射比例系数,用逆变换就能把聚类坐标还原成近似原始价位区间。
小布可接入你的历史均线序列,自动完成 SOM 训练与坐标输出,并在品种页直接标注当前 K 线落入的聚类区,省去手写代码。
实测 H1 样本下 500~800 次迭代后神经元权重漂移小于 0.5% 即视为收敛,可据此设停止条件。
把新线归一化后输入网络,计算与各神经元权重距离,取最小者所在聚类即为判定,连续落同区可视为结构延续。