神经网络变得轻松(第十六部分):聚类运用实践·进阶篇
🧠

神经网络变得轻松(第十六部分):聚类运用实践·进阶篇

(2/3)·聚类不是终点,如何独立决策或喂给下一层模型,这篇给你可跑的代码路径

含代码示例 第 2/3 篇
很多交易者训完聚类模型就卡在“然后呢”——直接拿聚类编号当信号,等于把边界样本和中心样本一视同仁。先想清楚是用它独立出信号,还是当特征送进下一层,能少走几周弯路。

「把聚类距离喂给二级模型」

k-means 跑完之后,第二种用法是把聚类结果直接当成另一个模型的输入。这个距离向量不是原始坐标,而是到各个聚类中心距离的归一化表达,下游可以接任意监督模型,比如三层全连接感知器。 在 OpenCL 程序里我们新增内核 KmeansSoftMax,不重算距离(那活儿 KmeanscucDistance 已经干了),只做归一化。它接收 distance 源缓冲区和 softmax 结果缓冲区,两个都是「样本数 × 聚类数」的矩阵布局,按一维任务空间每线程处理一个样本。 归一化逻辑很直白:先在线程内找本行最大距离 m(距离都是平方偏差,必为正),再算每个元素的 exp(1 - d/m) 并累加得 sum,最后整行除以 sum。用私密变量暂存能少碰全局内存。下面这段就是内核本体。 [CODE] __kernel void KmeansSoftMax(__global double *distance, __global double *softmax, int total_k) { int i = get_global_id(0); int shift = i * total_k; double m=distance[shift]; for(int k = 1; k < total_k; k++) m = max(distance[shift + k],m); double sum = 0; for(int k = 0; k < total_k; k++) { double value = exp(1-distance[shift + k]/m); sum += value; softmax[shift + k] = value; } for(int k = 0; k < total_k; k++) softmax[shift + k] /= sum; } #define def_k_kmeans_softmax 5 #define def_k_kmsm_distance 0 [/CODE] 逐行拆解:第 1–4 行是内核签名,distance 进、softmax 出、total_k 是聚类总数;第 6 行取线程 ID 当作样本序号;第 7 行算该行在缓冲区的起始偏移;第 8–10 行用首元素初始化最大值 m 再扫一遍找最大距离;第 12–18 行边算指数边累加 sum 并写回 softmax;第 20–21 行整行除以 sum 完成归一化。 CKmeans 类里加 SoftMax 方法:先查模型训过没,没训过就限制样本规模、训完解限;指针和缓冲区大小都要复核,避免拿无效指针往下跑崩。调用顺序就是先 KmeanscultDistance 算距、再 KmeansSoftMax 归一。 实测智能系统 kmeans_net.mq5 把 500 个预训练聚类模型接进三隐藏层感知器,历史数据转成时间序列再送进去。训练图显示前 20 世代损失平缓下降(Adam 优化、动量累积),之后陡降到极小值;比起直接监督的注意力模型转折更利落,说明先聚类再监督对简单模型效率提升明显。外汇与贵金属行情高风险,这套管线只解决特征表达,不承诺任何方向概率优势,请自行在 MT5 用附件代码回测验证。

MQL5 / C++
__kernel <span class="keyword">class="type">void</span> KmeansSoftMax(__global <span class="keyword">class="type">class="kw">double</span> *distance,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;__global <span class="keyword">class="type">class="kw">double</span> *softmax,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">inсt</span> total_k
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; )
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">class="type">int</span> i = get_global_id(<span class="number">class="num">0</span>);
&nbsp;&nbsp; <span class="keyword">class="type">int</span> shift = i * total_k;
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span> m=distance[shift];
&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> k = <span class="number">class="num">1</span>; k &lt; total_k; k++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m =&nbsp;&nbsp;max(distance[shift + k],m);
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span> sum = <span class="number">class="num">0</span>;
&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> k = <span class="number">class="num">0</span>; k &lt; total_k; k++)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> value =&nbsp;&nbsp;exp(<span class="number">class="num">1</span>-distance[shift + k]/m);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sum += value;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;softmax[shift + k] = value;
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> k = <span class="number">class="num">0</span>; k &lt; total_k; k++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;softmax[shift + k] /= sum;
&nbsp;&nbsp;}
<span class="preprocessor">class="macro">#define </span>def_k_kmeans_softmax&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">class="num">5</span>
<span class="preprocessor">class="macro">#define </span>def_k_kmsm_distance&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">class="num">0</span>

在 OpenCL 里跑 Kmeans 距离与 SoftMax 分配

把 K 均值聚类搬到 GPU 上算,核心是先建好 OpenCL 上下文并注册 6 个 kernel,其中第 2 类 kernel 专门负责 SoftMax 概率化。下面这段工厂函数里,SetKernelsCount(6) 失败就直接 delete 并返回 NULL,KernelCreate 绑定名为 "KmeansSoftMax" 的入口,少一个都进不了后续计算。 SoftMax 方法本身先做防御:未训练且 Study 失败、指针无效、数据条数不能被向量维数整除,全部直接返 NULL。它按 total / m_iVectorSize 算出样本行数 rows,给距离矩阵和 SoftMax 矩阵各开 rows * m_iClusters 的双精度缓冲并清零。 真正下发任务前,要把数据缓冲、均值缓冲、距离缓冲、SoftMax 缓冲全部 BufferCreate 到 OpenCL 设备,再用 SetArgumentBuffer 把索引绑到对应 kernel 参数槽。最后设 global_work_offset 为 {0,0},global_work_size[0] 取 rows——意味着每个样本行派一个 work item 并行算距离。外汇与贵金属行情高频,这种并行在 30 分钟以上周期做状态聚类可能降低 CPU 占用,但模型误分概率仍受训练样本偏差影响,属高风险用法。 别把缓冲创建当免检环节 哪怕 CPU 端 new 成功,BufferCreate 到 OpenCL 失败也会静默返 NULL;实盘前应在 MT5 策略测试器里故意传空指针跑一遍,确认所有早退分支都按预期触发而不是崩终端。

MQL5 / C++
class="macro">#define def_k_kmsm_softmax        class="num">1
class="macro">#define def_k_kmsm_total_k        class="num">2
COpenCLMy *OpenCLCreate(class="type">class="kw">string programm)
  {
  ...............
class=class="str">"cmt">//---
   if(!result.SetKernelsCount(class="num">6))
     {
      class="kw">delete result;
     .class="kw">return NULL;
     }
class=class="str">"cmt">//---
   ...............
class=class="str">"cmt">//---
   if(!result.KernelCreate(def_k_kmeans_softmax, "KmeansSoftMax"))
     {
      class="kw">delete result;
     .class="kw">return NULL;
     }
class=class="str">"cmt">//---
   class="kw">return result;
  }
CBufferDouble *CKmeans::SoftMax(CBufferDouble *data)
  {
   if(!m_bTrained && !Study(data, (c_aMeans.Maximum() == class="num">0)))
     .class="kw">return NULL;
   if(CheckPointer(data) == POINTER_INVALID || CheckPointer(c_OpenCL) == POINTER_INVALID)
     .class="kw">return NULL;
   class="type">int total = data.Total();
   if(total <= class="num">0 || m_iClusters < class="num">2 || (total % m_iVectorSize) != class="num">0)
     .class="kw">return NULL;
   class="type">int rows = total / m_iVectorSize;
   if(rows < class="num">1)
     .class="kw">return NULL;
   if(CheckPointer(c_aDistance) == POINTER_INVALID)
     {
      c_aDistance = new CBufferDouble();
      if(CheckPointer(c_aDistance) == POINTER_INVALID)
        .class="kw">return NULL;
     }
   c_aDistance.BufferFree();
   if(!c_aDistance.BufferInit(rows * m_iClusters, class="num">0))
     .class="kw">return NULL;
   if(CheckPointer(c_aSoftMax) == POINTER_INVALID)
     {
      c_aSoftMax = new CBufferDouble();
      if(CheckPointer(c_aSoftMax) == POINTER_INVALID)
        .class="kw">return NULL;
     }
   c_aSoftMax.BufferFree();
   if(!c_aSoftMax.BufferInit(rows * m_iClusters, class="num">0))
     .class="kw">return NULL;
   if(!data.BufferCreate(c_OpenCL) ||
      !c_aMeans.BufferCreate(c_OpenCL) ||
      !c_aDistance.BufferCreate(c_OpenCL) ||
      !c_aSoftMax.BufferCreate(c_OpenCL))
     .class="kw">return NULL;
   if(!c_OpenCL.SetArgumentBuffer(def_k_kmeans_distance, def_k_kmd_data, data.GetIndex()))
     .class="kw">return NULL;
   if(!c_OpenCL.SetArgumentBuffer(def_k_kmeans_distance, def_k_kmd_means, c_aMeans.GetIndex()))
     .class="kw">return NULL;
   if(!c_OpenCL.SetArgumentBuffer(def_k_kmeans_distance, def_k_kmd_distance, c_aDistance.GetIndex()))
     .class="kw">return NULL;
   if(!c_OpenCL.SetArgument(def_k_kmeans_distance, def_k_kmd_vector_size, m_iVectorSize))
     .class="kw">return NULL;
   class="type">uint global_work_offset[class="num">2] = {class="num">0, class="num">0};
   class="type">uint global_work_size[class="num">2];
   global_work_size[class="num">0] = rows;

◍ GPU 上跑完距离与软最大化再载回权重

这段逻辑先在 OpenCL 设备里算完样本到各聚类中心的距离,再调 softmax 核把距离转成概率分布,最后把结果读回主机内存。global_work_size[1] 被设成聚类数 m_iClusters,二维调度里第二维正好覆盖全部簇,避免 CPU 端逐簇循环。 执行 def_k_kmeans_distance 后若 BufferRead 失败直接返 NULL,说明显存到内存的回读断了,MT5 终端里会看到指标算不出值。softmax 核只用一维、global_work_size1[0]=rows,即按样本行并行,和前面距离核的二维结构不一样,写核函数时维度别抄错。 Train 函数里先按 StudyPeriod 年回拨起点,比如 StudyPeriod=10 且当前 2024 年,起点年份会被推到 2014;CopyRates 拉到的 bars 数量直接决定 RSI/CCI/ATR/MACD 四个缓冲的 resize 大小,少一个 resize 成功就 ExpertRemove 自毙。 模型文件用 kmeans_%d.net 的二进制名,Clusters 不同文件名就不同;FileReadInteger 读出的类型码和 Kmeans.Type() 对不上也会直接停 EA。total = bars - HistoryBars - 1 这一行给后续训练游标留了边界,HistoryBars 设太大可能让 total 变负,回测时容易无声崩掉。

MQL5 / C++
  global_work_size[class="num">1] = m_iClusters;
  if(!c_OpenCL.Execute(def_k_kmeans_distance, class="num">2, global_work_offset, global_work_size))
     class="kw">return NULL;
  if(!c_aDistance.BufferRead())
     class="kw">return NULL;
  if(!c_OpenCL.SetArgumentBuffer(def_k_kmeans_softmax, def_k_kmsm_distance, c_aDistance.GetIndex()))
     class="kw">return NULL;
  if(!c_OpenCL.SetArgumentBuffer(def_k_kmeans_softmax, def_k_kmsm_softmax, c_aSoftMax.GetIndex()))
     class="kw">return NULL;
  if(!c_OpenCL.SetArgument(def_k_kmeans_softmax, def_k_kmsm_total_k, m_iClusters))
     class="kw">return NULL;
  class="type">uint global_work_offset1[class="num">1] = {class="num">0};
  class="type">uint global_work_size1[class="num">1];
  global_work_size1[class="num">0] = rows;
  if(!c_OpenCL.Execute(def_k_kmeans_softmax, class="num">1, global_work_offset1, global_work_size1))
     class="kw">return NULL;
  if(!c_aSoftMax.BufferRead())
     class="kw">return NULL;
  data.BufferFree();
  c_aDistance.BufferFree();
class=class="str">"cmt">//---
  class="kw">return c_aSoftMax;
  }
class="type">void Train(class="type">class="kw">datetime StartTrainBar = class="num">0)
  {
  COpenCLMy *opencl = OpenCLCreate(cl_unsupervised);
  if(CheckPointer(opencl) == POINTER_INVALID)
     {
       ExpertRemove();
       class="kw">return;
     }
  if(!Kmeans.SetOpenCL(opencl))
     {
       class="kw">delete opencl;
       ExpertRemove();
       class="kw">return;
     }
  class="type">MqlDateTime start_time;
  TimeCurrent(start_time);
  start_time.year -= StudyPeriod;
  if(start_time.year <= class="num">0)
     start_time.year = class="num">1900;
  class="type">class="kw">datetime st_time = StructToTime(start_time);
  class="type">int bars = CopyRates(Symb.Name(), TimeFrame, st_time, TimeCurrent(), Rates);
  if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
     {
       ExpertRemove();
       class="kw">return;
     }
  if(!ArraySetAsSeries(Rates, true))
     {
       ExpertRemove();
       class="kw">return;
     }
  RSI.Refresh();
  CCI.Refresh();
  ATR.Refresh();
  MACD.Refresh();
  class="type">int handl = FileOpen(StringFormat("kmeans_%d.net", Clusters), FILE_READ | FILE_BIN);
  if(handl == INVALID_HANDLE)
     {
       ExpertRemove();
       class="kw">return;
     }
  if(FileReadInteger(handl) != Kmeans.Type())
     {
       ExpertRemove();
       class="kw">return;
     }
  class="type">bool result = Kmeans.Load(handl);
  FileClose(handl);
  if(!result)
     {
       ExpertRemove();
       class="kw">return;
     }
  class="type">int total = bars - (class="type">int)HistoryBars - class="num">1;

「把K线特征塞进训练缓冲区的写法」

做行情模式识别时,第一步是把每根历史bar的几何与指标特征铺平成定长向量。下面这段逻辑给 data 数组按 total*8*HistoryBars 扩容,给 fractals 按 total*3 扩容;只要任一 ArrayResize 返回 ≤0 就直接 ExpertRemove 退出,避免后面越界写崩 EA。 外层循环 i 跑 total 个样本,内层 b 跑 HistoryBars 根 bar,每个样本在 data 里占 8 个 double 槽位:开盘到最低/最高/收盘的偏移各 1 个,再加 RSI、CCI、ATR、MACD主线、MACD信号线共 5 个指标值。shift 计算成 (i*HistoryBars+b)*8,读者开 MT5 把 HistoryBars 设成 20、total 设成 500,就能直观看到缓冲区被撑到 80000 个 double。 fractals 每样本占 3 位:上分形、下分形用相邻三根 bar 的高低比较得出,第三位写的是「上下分形都不成立」的标记(注意原文 fractals[shift]+fractals[shift] 是笔误,本意应是上+下分形之和为0)。循环里用 Comment 打印进度,并用 IsStopped 做中断兜底,手动停止测试时不会卡死在万级循环里。 最后把两个裸数组 AssignArray 给 CBufferDouble,再调 Kmeans.SoftMax 拿到概率分布;任一步指针无效就 printf 错误码并移除 EA。外汇与贵金属杠杆高,这类离线特征构建只在历史数据上跑,实盘前务必用小资金验证聚类稳定性。

MQL5 / C++
  class="type">class="kw">double data[], fractals[];
  if(ArrayResize(data, total * class="num">8 * HistoryBars) <= class="num">0 ||
     ArrayResize(fractals, total * class="num">3) <= class="num">0)
    {
     ExpertRemove();
     class="kw">return;
    }
class=class="str">"cmt">//---
   for(class="type">int i = class="num">0; (i < total && !IsStopped()); i++)
     {
      Comment(StringFormat("Create data: %d of %d", i, total));
      for(class="type">int b = class="num">0; b < (class="type">int)HistoryBars; b++)
        {
         class="type">int bar = i + b;
         class="type">int shift = (i * (class="type">int)HistoryBars + b) * class="num">8;
         class="type">class="kw">double open = Rates[bar].open;
         data[shift] = open - Rates[bar].low;
         data[shift + class="num">1] = Rates[bar].high - open;
         data[shift + class="num">2] = Rates[bar].close - open;
         data[shift + class="num">3] = RSI.GetData(MAIN_LINE, bar);
         data[shift + class="num">4] = CCI.GetData(MAIN_LINE, bar);
         data[shift + class="num">5] = ATR.GetData(MAIN_LINE, bar);
         data[shift + class="num">6] = MACD.GetData(MAIN_LINE, bar);
         data[shift + class="num">7] = MACD.GetData(SIGNAL_LINE, bar);
        }
      class="type">int shift = i * class="num">3;
      class="type">int bar = i + class="num">1;
      fractals[shift] = (class="type">int)(Rates[bar - class="num">1].high <= Rates[bar].high && Rates[bar + class="num">1].high < Rates[bar].high);
      fractals[shift + class="num">1] = (class="type">int)(Rates[bar - class="num">1].low >= Rates[bar].low && Rates[bar + class="num">1].low > Rates[bar].low);
      fractals[shift + class="num">2] = (class="type">int)((fractals[shift] + fractals[shift]) == class="num">0);
     }
   if(IsStopped())
     {
      ExpertRemove();
      class="kw">return;
     }
   CBufferDouble *Data = new CBufferDouble();
   if(CheckPointer(Data) == POINTER_INVALID ||
     !Data.AssignArray(data))
      class="kw">return;
   CBufferDouble *Fractals = new CBufferDouble();
   if(CheckPointer(Fractals) == POINTER_INVALID ||
     !Fractals.AssignArray(fractals))
      class="kw">return;
   ResetLastError();
   CBufferDouble *softmax = Kmeans.SoftMax(Data);
   if(CheckPointer(softmax) == POINTER_INVALID)
     {
      printf("Runtime error %d", GetLastError());
      ExpertRemove();
      class="kw">return;
     }
  if(CheckPointer(TempData) == POINTER_INVALID)
    {
     TempData = new CArrayDouble();
     if(CheckPointer(TempData) == POINTER_INVALID)
       {
        ExpertRemove();

回测循环里的内存自清与随机抽桩

这段逻辑跑在 EA 的回测主循环里,先 delete opencl 释放上一次的开收盘对象,再把 dUndefine、dForecast、dError、dPrevSignal 全部归零,count 从 0 起步,准备进入 do-while 的迭代。 循环体先用 prev_un / prev_for / prev_er 暂存上轮信号数值,随后用 IsStopped() 探一次中止标志;只要终端点了停止,后面的双重 for 直接不进。 内层 for 的遍历上限是 total - 300,意味着回测刻意留了最后 300 根 K 线不碰——这是为了避免样本尾部过拟合,实操里你可以把 300 改成 500 看曲线是否更稳。 抽桩位置 i 不是顺序走的,而是用 MathRand() 的平方除以 32767 的平方再做缩放,等于在 [300, total) 区间做非线性随机采样;这种加权随机更偏向中前段数据。 TempData.Reserve(Clusters) 一旦失败,代码会连环 delete Data / Fractals / softmax / opencl 四个指针对象,清 Comment 后调 ExpertRemove() 自卸 EA——说明内存预约失败在这里被视作致命错误,不是打个日志就跳过。

MQL5 / C++
   class="kw">return;
      }
     }
   }
   class="kw">delete opencl;
   class="type">class="kw">double prev_un, prev_for, prev_er;
   dUndefine = class="num">0;
   dForecast = class="num">0;
   dError = -class="num">1;
   dPrevSignal = class="num">0;
   class="type">bool stop = class="kw">false;
   class="type">int count = class="num">0;
   do
     {
       prev_un = dUndefine;
       prev_for = dForecast;
       prev_er = dError;
       ENUM_SIGNAL bar = Undefine;
       class=class="str">"cmt">//---
       stop = IsStopped();
       for(class="type">int it = class="num">0; (it < total - class="num">300 && !IsStopped()); it++)
         {
          class="type">int i = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (total - class="num">300)) + class="num">300;
          TempData.Clear();
          class="type">int shift = i * Clusters;
          if(!TempData.Reserve(Clusters))
            {
             if(CheckPointer(Data) == POINTER_DYNAMIC)
                class="kw">delete Data;
             if(CheckPointer(Fractals) == POINTER_DYNAMIC)
                class="kw">delete Fractals;
             if(CheckPointer(softmax) == POINTER_DYNAMIC)
                class="kw">delete softmax;
             if(CheckPointer(opencl) == POINTER_DYNAMIC)
                class="kw">delete opencl;
             Comment("");
             class=class="str">"cmt">//---
             ExpertRemove();
             class="kw">return;
            }
          for(class="type">int c = class="num">0; c < Clusters; c++)
          if(!TempData.Add(softmax.At(shift + c)))
            {
             if(CheckPointer(Data) == POINTER_DYNAMIC)
                class="kw">delete Data;
             if(CheckPointer(Fractals) == POINTER_DYNAMIC)
                class="kw">delete Fractals;
             if(CheckPointer(softmax) == POINTER_DYNAMIC)
                class="kw">delete softmax;
             if(CheckPointer(opencl) == POINTER_DYNAMIC)
                class="kw">delete opencl;

◍ 神经网络推理失败时的资源回收

Net.feedForward(TempData) 返回 false,说明前向传播没跑通,EA 必须立刻清场退出,否则悬空指针会拖垮后续 tick。这里用 CheckPointer 逐个确认动态对象再 delete,是 MT5 里防内存泄漏的标准动作。 清理顺序覆盖了 Data、Fractals、softmax、opencl 四个动态指针,确认类型等于 POINTER_DYNAMIC 才释放,避免对栈对象误删。清完调 Comment("") 抹掉图表上的旧文字,再 ExpertRemove() 自卸载并 return。 正常分支里,前向通过后先用 getResults 取原始输出,随后手动做 softmax:对 3 个节点取 exp 累加得 sum,再逐项除以 sum,得到的概率和恒为 1。若 sum 为 0 则跳过归一化,防止除零崩 EA。 TempData.Maximum(0,3) 挑出最大概率下标,case 1 视为偏多信号、case 2 偏空、其余归零,信号值直接取对应节点概率。外汇与贵金属波动剧烈,这类模型输出只代表概率倾向,实盘仍需严格风控。

MQL5 / C++
Comment("");
class=class="str">"cmt">//---
ExpertRemove();
class="kw">return;
}
if(!Net.feedForward(TempData))
  {
   if(CheckPointer(Data) == POINTER_DYNAMIC)
     class="kw">delete Data;
   if(CheckPointer(Fractals) == POINTER_DYNAMIC)
     class="kw">delete Fractals;
   if(CheckPointer(softmax) == POINTER_DYNAMIC)
     class="kw">delete softmax;
   if(CheckPointer(opencl) == POINTER_DYNAMIC)
     class="kw">delete opencl;
   Comment("");
   class=class="str">"cmt">//---
   ExpertRemove();
   class="kw">return;
  }
Net.getResults(TempData);
class="type">class="kw">double sum = class="num">0;
for(class="type">int res = class="num">0; res < class="num">3; res++)
  {
   class="type">class="kw">double temp = exp(TempData.At(res));
   sum += temp;
   TempData.Update(res, temp);
  }
for(class="type">int res = class="num">0; (res < class="num">3 && sum > class="num">0); res++)
  TempData.Update(res, TempData.At(res) / sum);
class="kw">switch(TempData.Maximum(class="num">0, class="num">3))
  {
   case class="num">1:
     dPrevSignal = (TempData[class="num">1] != TempData[class="num">2] ? TempData[class="num">1] : class="num">0);
     class="kw">break;
   case class="num">2:
     dPrevSignal = -TempData[class="num">2];
     class="kw">break;
   class="kw">default:
     dPrevSignal = class="num">0;
     class="kw">break;
  }
class="type">class="kw">string s = StringFormat("Study -> Era %d -> %.2f -> Undefine %.2f%% foracast %.2f%%\n %d of %d -> %.2f%%
交给小布盯盘看聚类边界
小布盯盘的 AIGC 已内置品种聚类边界提示,打开对应外汇或贵金属页就能看到当前价态落在哪一类,省去自己跑距离计算。把重复劳动交给小布,你专注决策。

常见问题

标记集可比训练集小很多,但须有代表性;第一次遍历收集统计即可,不需多世代迭代,过度拟合风险也低。
只输编号最多得到和统计法相当的結果,额外成本不划算;改输到聚类中心的距离并做 Softmax 归一化才更有信息量。
可以,小布盯盘对应品种页内置了聚类归属与边界提示,外汇贵金属均适用,但聚类仅作概率参考,实盘仍属高风险。
靠近中心的“理想形态”和边界样本会被同等对待,可通过增聚类数压低最大距离,但若按损失函数选对数量,提升很有限。
不能,预测价格是回归任务,聚类只建概率形态模型;它适合在形态出现后统计历史价格行为倾向。