数据科学和机器学习(第 14 部分):运用 Kohonen 映射在市场中寻找出路·进阶篇
「Kohonen 映射怎么靠抢赢家来聚类」
自组织映射跟常规神经网络不一样,它不靠反向传播纠错,而是让神经元之间打擂台——谁离输入数据最近谁就是赢家,这叫竞争性学习。每个输入样本进来,都算一遍和所有神经元权重向量的相似度,欧几里得距离最小的那个胜出,成为最佳匹配单元(BMU)。 赢家及其邻域会按 Kohonen 规则更新权重,朝输入向量靠拢。公式就是:新权重 = 旧权重 + 学习率 ×(输入向量 − 旧权重)。注意它只动获胜聚类的权重,不像多层感知机那样全层参与。 下面这段是权重更新和 BMU 选取的核心循环,默认聚类数 m=2、学习率 alpha=0.01、 epochs=100: 代码里 D[j] 存每个聚类到当前样本的欧距,D.ArgMin() 直接返回赢家下标。w_matrix.Col(min) 取出胜出聚类权重,加完增量再写回。DEBUG_MODE 下会打印距离和赢家,方便你在 MT5 里核对每一步。 跑完后得到两个聚集:一个 2×2(4 个值)、一个 4×2(8 个值)。这跟 K-Means 那篇一样,聚类尺寸差太大,可视化很麻烦,所以后面得另想办法。外汇与贵金属行情用这类无监督映射做状态划分时,样本非平稳,高风险,结论仅作概率参考。
vector D(m); class=class="str">"cmt">//Euclidean distance btn clusters | Remember m is the number of clusters selected 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)); } 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(); class=class="str">"cmt">//winning cluster vector w_new = w_matrix.Col(min) + (alpha * (Matrix.Row(i) - w_matrix.Col(min))); w_matrix.Col(w_new, min); 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) { Matrix = matrix_; n = (class="type">uint)matrix_.Cols(); rows = matrix_.Rows(); m = clusters; cluster_tensor = new CTensors(m); w_matrix =matrix_utils.Random(class="num">0.0, class="num">1.0, n, m, RANDOM_STATE); 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)); } 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(); 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;
训练日志里看聚类收敛过程
自组织映射(SOM)在 EURUSD H1 上跑训练时,终端会逐条吐出欧氏距离和获胜聚类编号。从日志看,第 1 轮 epoch 的 100 次迭代里,前 6 个样本欧氏距离分别落在 [2.12,1.82]、[1.43,1.10]、[5.57,5.26]、[4.37,4.00]、[8.05,7.65]、[6.97,6.50],且全部判给 cluster 1,说明初始权重下样本被粗略推到同一侧。 到后面 epoch 临近结束,同一毫秒(04:13:26.622)输出的距离开始出现分化:例如 [0.081,4.734] 归 cluster 0,[4.183,0.564] 归 cluster 1,最小距离压到 0.081,表明权重矩阵在迭代中把样本空间撕开成两个可分辨区。 调试宏 DEBUG_MODE 打开时,每轮训练后 Print 出 w_matrix 新权重,配合上面这种日志能直接核对聚类边界是否随 epoch 推移而稳定。外汇与贵金属波动大、SOM 聚类仅描述历史形态,后续信号出现概率倾向而非确定。 开 MT5 把这段日志逻辑接进 Expert Advisor,把 epochs 调到 100 以上,观察 winning cluster 从单侧占优转向 0/1 交替的临界点,比只看最终权重更有用。
printf("Epoch [%d/%d] | %sElapsed ",epoch+class="num">1,epochs, CalcTimeElapsed(epoch_stop-epoch_start)); } class=class="str">"cmt">//end of training class=class="str">"cmt">//--- class="macro">#ifdef DEBUG_MODE Print("\nNew weights\n",w_matrix); class="macro">#endif
◍ SOM 训练完毕后的权重与聚类落点
在 MT5 专家日志里跑完 Self Organizing map 后,EURUSD H1 上 100/100 轮迭代只耗了 0.000 秒,说明这种小样本二维映射对算力几乎没压力。日志末尾打印出的 New weights 是两个二维向量:[0.75086979456201, 4.028060179594681] 与 [1.737580668068743, 5.173650598091957],它们就是两个聚类中心的最终坐标。 下面这段矩阵是把 6 组观测值按距离归到了哪个中心:前两组 {1.2,2.3}、{0.7,1.8} 离第一个权重更近,标进 cluster 0;剩下四组从 {3.6,4.8} 到 {4.8,5.6} 全部落入 cluster 1。 外汇与贵金属品种用 SOM 做状态压缩时,要意识到 H1 这种低频切片容易让权重被少数极端 K 线带偏,聚类边界可能失真。开 MT5 把上面矩阵贴进脚本,改几个输入值就能直观看到重新分配结果。
matrix Matrix = {
{class="num">1.2, class="num">2.3}, class=class="str">"cmt">//Into cluster class="num">0
{class="num">0.7, class="num">1.8}, class=class="str">"cmt">//Into cluster class="num">0
{class="num">3.6, class="num">4.8}, class=class="str">"cmt">//Into cluster class="num">1
{class="num">2.8, class="num">3.9}, class=class="str">"cmt">//Into cluster class="num">1
{class="num">5.2, class="num">6.7}, class=class="str">"cmt">//Into cluster class="num">1
{class="num">4.8, class="num">5.6} class=class="str">"cmt">//Into cluster class="num">1
};「用张量收口聚类结果」
张量本质是向量和矩阵的多维推广,可以理解为套着多层矩阵的数组容器。在 MT5 里做自组织映射时,导入 Tensors.mqh 之后,我们建一个长度为聚类数 m 的张量对象,训练每跑完一行样本,就把归属簇的向量追加进对应张量槽位。 上面那段代码里,cluster_tensor 在构造函数中通过 new CTensors(m) 生成,训练末轮用 TensorAppend(Matrix.Row(i), min) 把样本塞进胜出簇。这样跑完 100 个 epoch(默认 epochs=100、alpha=0.01)后,各簇的聚集就分别躺在自己的张量里,不再散落在权重矩阵中。 接下来就能直接对这些张量做距离比较或特征统计,不必再回原矩阵捞行。外汇与贵金属市场波动剧烈、杠杆风险高,这类聚类仅作概率参考,实盘前务必在 MT5 策略测试器用历史数据复核。
# create tensor from numpy class="kw">import array T = array([ [[class="num">1,class="num">2,class="num">3], [class="num">4,class="num">5,class="num">6], [class="num">7,class="num">8,class="num">9]], [[class="num">11,class="num">12,class="num">13], [class="num">14,class="num">15,class="num">16], [class="num">17,class="num">18,class="num">19]], [[class="num">21,class="num">22,class="num">23], [class="num">24,class="num">25,class="num">26], [class="num">27,class="num">28,class="num">29]], ]) 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) { Matrix = matrix_; n = (class="type">uint)matrix_.Cols(); rows = matrix_.Rows(); m = clusters; cluster_tensor = new CTensors(m); w_matrix =matrix_utils.Random(class="num">0.0, class="num">1.0, n, m, RANDOM_STATE); Print("w Matrix\n",w_matrix,"\nMatrix\n",Matrix); 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)); } 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); class=class="str">"cmt">//Print("New w_Matrix\n ",w_matrix); } 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
把聚类结果从日志里读出来
自组织映射跑完之后,真正有用的不是训练过程,而是最终落进 cluster_tensor 里的聚类中心。上面这段日志来自 EURUSD H1 实跑,Print 把张量按索引打印出来,你能直接看到每个簇的坐标矩阵。 从输出看,索引 0 的簇是 2×2 结构:[[1.2,2.3],[0.7,1.8]];索引 1 的簇扩成 4×2:[[3.6,4.8],[2.8,3.9],[5.2,6.7],[4.8,5.6]]。这种维度差异说明输入样本在特征空间里分布不均,外汇 EURUSD 小时线的高波动段倾向被分到更大簇。 开 MT5 加载同款 EA,把 Experts 日志里的 cluster_tensor 输出拷出来,对照自己品种看看簇数和每行向量。外汇与贵金属杠杆高、跳空频繁,聚类仅描述历史形态,对后续走势只具概率参考。
Print("\nNew weights\n",w_matrix); class="macro">#endif class=class="str">"cmt">//--- Print("\nclusters"); cluster_tensor.TensorPrint(); } CS class="num">0 class="num">04:class="num">13:class="num">26.624 Self Organizing map(EURUSD,H1) clusters CS class="num">0 class="num">04:class="num">13:class="num">26.624 Self Organizing map(EURUSD,H1) TENSOR INDEX <<class="num">0>> CS class="num">0 class="num">04:class="num">13:class="num">26.624 Self Organizing map(EURUSD,H1) [[class="num">1.2,class="num">2.3] CS class="num">0 class="num">04:class="num">13:class="num">26.624 Self Organizing map(EURUSD,H1) [class="num">0.7,class="num">1.8]] CS class="num">0 class="num">04:class="num">13:class="num">26.624 Self Organizing map(EURUSD,H1) TENSOR INDEX <<class="num">1>> CS class="num">0 class="num">04:class="num">13:class="num">26.624 Self Organizing map(EURUSD,H1) [[class="num">3.6,class="num">4.8] CS class="num">0 class="num">04:class="num">13:class="num">26.624 Self Organizing map(EURUSD,H1) [class="num">2.8,class="num">3.9] CS class="num">0 class="num">04:class="num">13:class="num">26.624 Self Organizing map(EURUSD,H1) [class="num">5.2,class="num">6.7] CS class="num">0 class="num">04:class="num">13:class="num">26.624 Self Organizing map(EURUSD,H1) [class="num">4.8,class="num">5.6]]
◍ 把聚类落盘并用曲线图看映射
自组织映射跑完之后,真正能拿去用的不是那堆内存里的张量,而是先把聚类结果导出成 CSV,再画出来肉眼校验。文件会落在终端 Files 父目录下的 SOM 子目录,命名规则是 Cluster1.csv、Cluster2.csv 这样顺着张量维度递增。 下面这段 MQL5 逻辑干了两件事:遍历 cluster_tensor 的每个维度矩阵写成 CSV;再把所有矩阵拉直成向量拼进一张 plotmatrix,交给 ScatterCurvePlotsMatrix 画成名为 kom 的曲线聚类图。 [CODE] matrix mat= {}; if (save_clusters) for (uint i=0; i<this.cluster_tensor.TENSOR_DIMENSION; i++) { mat = this.cluster_tensor.Tensor(i); //Obtain a matrix located at I index in a cluster tensor string header[]; ArrayResize(header, (int)mat.Cols()); for (int k=0; k<ArraySize(header); k++) header[k] = "col"+string(k); if (this.matrix_utils.WriteCsv("SOM\\Cluster"+string(i+1)+".csv",mat,header)) Print("Clusters CSV files saved under the directory Files\\SOM"); } vector v; matrix plotmatrix(rows, m); for (uint i=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"); [/CODE] 逐行拆一下:mat 初始为空矩阵;save_clusters 为真时按 TENSOR_DIMENSION 循环,Tensor(i) 取出第 i 个聚类矩阵;header 按列数生成 col0、col1… 的表头;WriteCsv 把矩阵写进 SOM\\Cluster(i+1).csv,成功就打印保存路径。后半段重新循环把每个矩阵用 MatrixToVector 拉直,按列塞进 plotmatrix,最后 ScatterCurvePlotsMatrix 出图。 开 MT5 跑一遍,若日志出现 Files\\SOM 的保存提示且曲线图按预期散开,说明聚类提取链路是通的;外汇与贵金属行情受杠杆与跳空影响大,这类特征聚类仅用于辅助概率判断,不能直接当方向依据。
matrix mat= {};
if (save_clusters)
for (class="type">uint i=class="num">0; i<this.cluster_tensor.TENSOR_DIMENSION; i++)
{
mat = this.cluster_tensor.Tensor(i); class=class="str">"cmt">//Obtain a matrix located at I index in a cluster tensor
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");
}
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");