神经网络变得简单(第 63 部分):决策转换器无监督预训练(PDT)·进阶篇
🧠

神经网络变得简单(第 63 部分):决策转换器无监督预训练(PDT)·进阶篇

(2/3)· 当奖励标记稀缺、训练集难以扩展,PDT 如何仅凭过去轨迹让模型学会对未来推理

实战向 第 2/3 篇
不少团队一上手决策转换器就卡在标注回报上,以为没有奖励信号模型根本训不动。其实用次优轨迹做无监督预训练,反而能先榨出通用行为模式,省掉长期标数据的坑。

「编码器与未来嵌入的双路网络拼装」

这段代码在 MT5 的 OpenCL 环境里搭了两路神经网络:一路是主编码器(layer 4~6),另一路是 future_embedding 分支,专门处理未来窗口的嵌入表示。主编码器第 4、5 层都用 LatentCount 个 defNeuronBaseOCL 神经元,激活函数 LReLU,优化器 ADAM;第 6 层换成 defNeuronSoftMaxOCL 做多分类输出,count 等于 EmbeddingSize。 future_embedding 分支的输入层维度是 BarDescr * NBarInPattern * ValueBars,紧跟一个 batch=1000 的 BatchNorm 层做归一化。真正吃算力的是 layer 2:用了 MLMHSparseAttention(多头稀疏注意力),window=BarDescr*NBarInPattern、step=16、window_out=64、layers=4,概率模式为 Sparse。 每一层都先 new CLayerDescription,失败直接 return false;加进 planner 或 future_embedding 容器时若返回 false,则 delete descr 并退出。开 MT5 把 Sparse 改成 Dense 跑同结构,显存占用和收敛速度会有明显差异,外汇与贵金属数据噪声大,这类模型过拟合概率偏高,验证时务必用样本外切片。

MQL5 / C++
   class="kw">return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!planner.Add(descr))
      {
         class="kw">delete descr;
         class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">4
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!planner.Add(descr))
      {
         class="kw">delete descr;
         class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">5
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!planner.Add(descr))
      {
         class="kw">delete descr;
         class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">6
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!planner.Add(descr))
      {
         class="kw">delete descr;
         class="kw">return false;
      }
class=class="str">"cmt">//--- Future Embedding
   if(!future_embedding)
      {
         future_embedding = new CArrayObj();
         if(!future_embedding)
            class="kw">return false;
      }
class=class="str">"cmt">//---
   future_embedding.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = BarDescr * NBarInPattern * ValueBars;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!future_embedding.Add(descr))
      {
         class="kw">delete descr;
         class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">1
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = class="num">1000;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!future_embedding.Add(descr))
      {
         class="kw">delete descr;
         class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">2
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronMLMHSparseAttentionOCL;
   prev_count = descr.count = ValueBars;
   descr.window = BarDescr * NBarInPattern;
   descr.step = class="num">16;
   descr.window_out = class="num">64;
   descr.layers = class="num">4;
   descr.probability = Sparse;
   descr.optimization = ADAM;
   if(!future_embedding.Add(descr))
      {
         class="kw">delete descr;
         class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">3
   if(!(descr = new CLayerDescription()))

未来嵌入与账户价值网络的层描述拼装

这段逻辑在 MT5 的 OpenCL 神经网络封装里分两条线:future_embedding 负责把时序特征编码成 EmbeddingSize 维度的未来状态,value 网络则把账户描述 AccountDescr 维输入映射成价值估计。两者都靠 CLayerDescription 对象逐层 Add 进 CArrayObj,任何一层 new 失败或 Add 返回 false 就直接 delete 并退出,避免野指针。 future_embedding 从第 3 层起用 LatentCount 个 defNeuronBaseOCL 节点、LReLU 激活、ADAM 优化,连续堆两层;第 5 层节点数回到 EmbeddingSize 且激活为 None;第 6 层切换为 defNeuronSoftMaxOCL 做归一化输出。可见潜变量维度 LatentCount 是中间瓶颈,EmbeddingSize 是两端对齐的接口宽度。 value 网络入口先以 AccountDescr 作输入层(激活 None),随后接一个 batch=1000 的 defNeuronBatchNormOCL 批归一化层——这个 1000 的数值直接决定训练时滑动均值统计的窗口,调小可能让归一化对近期样本更敏感。再往后用 defNeuronConcatenate 把 LatentCount 维潜变量按步长 EmbeddingSize 拼接,并以 SIGMOID 压缩到 (0,1) 区间,供后续价值回归使用。 在 MT5 里把这段抄进 Expert Advisor 的神经网络初始化函数,改 batch 或 LatentCount 后重编译,能直观看到显存占用和训练收敛速度的变化。外汇与贵金属杠杆高,此类模型仅作概率参考,实盘前务必在策略测试器跑足量历史样本。

MQL5 / C++
   class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!future_embedding.Add(descr))
      {
         class="kw">delete descr;
         class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">4
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!future_embedding.Add(descr))
      {
         class="kw">delete descr;
         class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">5
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!future_embedding.Add(descr))
      {
         class="kw">delete descr;
         class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">6
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!future_embedding.Add(descr))
      {
         class="kw">delete descr;
         class="kw">return false;
      }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CreateValueDescriptions(CArrayObj *value)
  {
class=class="str">"cmt">//---
   CLayerDescription *descr;
class=class="str">"cmt">//---
   if(!value)
      {
         value = new CArrayObj();
         if(!value)
            class="kw">return false;
      }
class=class="str">"cmt">//--- Value
   value.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   class="type">int prev_count = descr.count = AccountDescr;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!value.Add(descr))
      {
         class="kw">delete descr;
         class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">1
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = class="num">1000;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!value.Add(descr))
      {
         class="kw">delete descr;
         class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">2
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.step = EmbeddingSize;
   descr.optimization = ADAM;
   descr.activation = SIGMOID;
   if(!value.Add(descr))
      {
         class="kw">delete descr;
         class="kw">return false;
      }

◍ 潜变量层与三网初始化落地

构建智能体网络时,第 3、4、5 层都走 CLayerDescription 动态描述。第 3 层与第 4 层类型均为 defNeuronBaseOCL,神经元数由 LatentCount 控制,优化器 ADAM,激活用 LReLU;第 5 层输出层神经元数等于 NRewards,激活设为 None,直接出原始分值。 若 LoadTotalBase 读不到训练数据,OnInit 会打印错误码并返回 INIT_FAILED,EA 不会启动。Agent、Planner、FutureEmbedding 三个 CNet 实例优先从 .nnw 文件加载,文件名后缀分别为 Act.nnw、Pln.nnw、FEm.nnw。 当三个网络任一 Load 失败,代码会新建 CArrayObj 描述容器,调用 CreateDescriptions 填充层结构,再走 Create 建网。这里若描述填充或建网失败同样返回 INIT_FAILED,实盘前应在 MT5 策略测试器里故意删掉一个 .nnw 验证该分支是否按预期报错。外汇与贵金属品种波动剧烈,此类模型网初始化失败可能导致信号中断,属高风险环节。

MQL5 / C++
class=class="str">"cmt">//--- layer class="num">3
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!value.Add(descr))
      {
       class="kw">delete descr;
       class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">4
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = LatentCount;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!value.Add(descr))
      {
       class="kw">delete descr;
       class="kw">return false;
      }
class=class="str">"cmt">//--- layer class="num">5
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = NRewards;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!value.Add(descr))
      {
       class="kw">delete descr;
       class="kw">return false;
      }
class=class="str">"cmt">//---
   class="kw">return true;
   }
CNet                Agent;
CNet                Planner;
CNet                FutureEmbedding;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   ResetLastError();
   if(!LoadTotalBase())
      {
       PrintFormat("Error of load study data: %d", GetLastError());
       class="kw">return INIT_FAILED;
      }
class=class="str">"cmt">//--- load models
   class="type">class="kw">float temp;
   if(!Agent.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) ||
      !Planner.Load(FileName + "Pln.nnw", temp, temp, temp, dtStudied, true) ||
      !FutureEmbedding.Load(FileName + "FEm.nnw", temp, temp, temp, dtStudied, true))
      {
       CArrayObj *agent = new CArrayObj();
       CArrayObj *planner = new CArrayObj();
       CArrayObj *future_embedding = new CArrayObj();
       if(!CreateDescriptions(agent, planner, future_embedding))
         {
          class="kw">delete agent;
          class="kw">delete planner;
          class="kw">delete future_embedding;
          class="kw">return INIT_FAILED;
         }
       if(!Agent.Create(agent) || !Planner.Create(planner) ||
          !FutureEmbedding.Create(future_embedding))
         {
          class="kw">delete agent;
          class="kw">delete planner;
          class="kw">delete future_embedding;
          class="kw">return INIT_FAILED;
         }

「训练循环里的样本抽取与张量初始化」

在 Train 函数里,迭代次数由 Iterations 控制,每次循环先用 MathRand() 除以 32767.0 把随机值映射到 [0,1),再乘 (total_tr-1) 选出一条训练样本 tr,total_tr 即 Buffer 数组长度。 第二条随机索引用了 MathRand() 的平方除以 32767 的平方,使 i 更偏向较小值,范围被夹在 Buffer[tr].Total - 2*HistoryBars - ValueBars 与 MathMin(Buffer[tr].Total, 20+ValueBars) 之间;若 i<0 则本次 iter 自减并跳过,避免越界。 进入有效样本后,Actions 被初始化为长度为 NActions 的全零 vector<float>,随后用 for 从 state=i 走到 MathMin(Buffer[tr].Total - ValueBars, i + HistoryBars*3),这段窗口长度直接决定每次喂给网络的历史步数。 开 MT5 把 HistoryBars 和 ValueBars 调小一档,能明显看到 i 的可选区间变窄、训练样本更集中近期行情,外汇与贵金属波动剧烈,这类参数改动须先在模拟盘验证风险。

MQL5 / C++
class="type">void Train(class="type">void)
  {
  class="type">int total_tr = ArraySize(Buffer);
  class="type">uint ticks = GetTickCount();
  class="type">bool StopFlag = false;
  for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !StopFlag); iter ++)
    {
    class="type">int tr = (class="type">int)((MathRand() / class="num">32767.0) * (total_tr - class="num">1));
    class="type">int i = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * MathMax(Buffer[tr].Total - class="num">2 * HistoryBars - ValueBars, MathMin(Buffer[tr].Total, class="num">20 + ValueBars)));
    if(i < class="num">0)
      {
      iter--;
      class="kw">continue;
      }
    Actions = vector<class="type">class="kw">float>::Zeros(NActions);
    for(class="type">int state = i; state < MathMin(Buffer[tr].Total - ValueBars, i + HistoryBars * class="num">3); state++)
      {

把账户状态塞进神经网络前的特征拼装

做强化学习训练时,喂给 Planner 的状态向量不是裸 K 线,而是把历史账户表现和时间周期编码后拼起来的复合特征。下面这段拼装逻辑直接决定了模型能不能学到‘仓位随净值波动’的非线性规律,外汇和贵金属的高杠杆环境下这种特征失真会直接放大回撤概率。 先看历史数据注入:State.AssignArray 把当前 state 的原始状态数组挂到输入容器,随后调 Planner.feedForward 做前向传播(batch=1,不训练)。如果返回 false,立刻打印函数名加行号、置 StopFlag 并 break——这意味着某根样本的状态维度对不上,整套训练循环会断。 账户描述那段是重点。PrevBalance / PrevEquity 取上一 state 的同名账户字段(state==0 时退化为自身初始值),之后连续 Add 了 8 个派生量:权益相对余额的变化率、权益余额比、权益相对权益的变化率,以及 account[2]~[6] 里原始字段和按余额归一化的三个量。注意 account[4]~[6] 全除以 PrevBalance,等于把绝对金额压成‘占初始余额比例’,避免不同账户规模导致数值尺度撕裂。 时间标签用了四个三角函数:以 2023 全年秒数、月线秒数、周线秒数、日线秒数为周期,把 account[7](时间戳)归一化后取 sin/cos。这样模型能隐式感知‘月末调仓’‘周五流动性变薄’这类周期,但 x 的基准写死 D'2024.01.01'-D'2023.01.01',换年份跑回测要手动改常数。 最后把上一动作 Actions 数组接在状态尾,Target 则取下一个 state 的向量并复制 ValueBars-1 次做多步前瞻,再送 FutureEmbedding 前向。ValueBars 若设 5,Target 长度就是单 state 的 5 倍,维度不对同样会让 feedForward 崩。

MQL5 / C++
  class=class="str">"cmt">//--- History data
  State.AssignArray(Buffer[tr].States[state].state);
  if(!Planner.feedForward(GetPointer(State), class="num">1, false))
    {
     PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
     StopFlag = true;
     break;
    }
  class=class="str">"cmt">//--- Account description
  class="type">class="kw">float PrevBalance = (state == class="num">0 ? Buffer[tr].States[state].account[class="num">0] : Buffer[tr].States[state - class="num">1].account[class="num">0]);
  class="type">class="kw">float PrevEquity = (state == class="num">0 ? Buffer[tr].States[state].account[class="num">1] : Buffer[tr].States[state - class="num">1].account[class="num">1]);
  State.Add((Buffer[tr].States[state].account[class="num">0] - PrevBalance) / PrevBalance);
  State.Add(Buffer[tr].States[state].account[class="num">1] / PrevBalance);
  State.Add((Buffer[tr].States[state].account[class="num">1] - PrevEquity) / PrevEquity);
  State.Add(Buffer[tr].States[state].account[class="num">2]);
  State.Add(Buffer[tr].States[state].account[class="num">3]);
  State.Add(Buffer[tr].States[state].account[class="num">4] / PrevBalance);
  State.Add(Buffer[tr].States[state].account[class="num">5] / PrevBalance);
  State.Add(Buffer[tr].States[state].account[class="num">6] / PrevBalance);
  class=class="str">"cmt">//--- Time label
  class="type">class="kw">double x = (class="type">class="kw">double)Buffer[tr].States[state].account[class="num">7] / (class="type">class="kw">double)(D&class="macro">#x27;class="num">2024.01.class="num">01&class="macro">#x27; - D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;);
  State.Add((class="type">class="kw">float)MathSin(class="num">2.0 * M_PI * x));
  x = (class="type">class="kw">double)Buffer[tr].States[state].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1);
  State.Add((class="type">class="kw">float)MathCos(class="num">2.0 * M_PI * x));
  x = (class="type">class="kw">double)Buffer[tr].States[state].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1);
  State.Add((class="type">class="kw">float)MathSin(class="num">2.0 * M_PI * x));
  x = (class="type">class="kw">double)Buffer[tr].States[state].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1);
  State.Add((class="type">class="kw">float)MathSin(class="num">2.0 * M_PI * x));
  class=class="str">"cmt">//--- Prev action
  State.AddArray(Actions);
  class=class="str">"cmt">//--- Target
  Result.AssignArray(Buffer[tr].States[state + class="num">1].state);
  for(class="type">int s = class="num">1; s < ValueBars; s++)
    Result.AddArray(Buffer[tr].States[state + class="num">1].state);
  if(!FutureEmbedding.feedForward(Result, class="num">1, false))
    {

◍ 训练循环里的容错与日志节奏

这段是强化学习智能体在 MT5 里跑训练迭代的核心收口逻辑。每一轮先取未来状态嵌入结果,再做策略前馈与规划器、策略网络的反向传播;任一环节返回 false 就立刻打印函数名与行号、置 StopFlag 并 break,避免脏梯度继续污染权重。 误差监控没有每 tick 都刷,而是用 GetTickCount() 判断距上次输出是否超过 500 毫秒,超了才用 Comment 同时打出 Agent 与 Planner 的进度百分比和近期平均误差(getRecentAverageError,精度 15.8f)。这种节流写法在回测长周期时能把图表卡顿概率压下来。 全部迭代结束后清空 Comment,并用 PrintFormat 以 10.7f 精度把两个网络的最终平均误差写到日志,随后调 ExpertRemove() 自卸载。外汇与贵金属行情下这类自训练 EA 实盘风险极高,训练误差低不代表样本外能存活,上真金前务必在策略测试器用历史数据先跑通。

MQL5 / C++
PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
StopFlag = true;
break;
     }
     FutureEmbedding.getResults(Result);
     class=class="str">"cmt">//--- Policy Feed Forward
     if(!Agent.feedForward(GetPointer(State), class="num">1, false, Result))
       {
        PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
        StopFlag = true;
        break;
       }
     class=class="str">"cmt">//--- Planner Study
     if(!Planner.backProp(Result, NULL, NULL))
       {
        PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
        StopFlag = true;
        break;
       }
     class=class="str">"cmt">//--- Policy study
     Actions.Assign(Buffer[tr].States[state].action);
     vector<class="type">class="kw">float> result;
     Agent.getResults(result);
     Result.AssignArray(CAGrad(Actions - result) + result);
     if(!Agent.backProp(Result, GetPointer(FutureEmbedding)) ||
        !FutureEmbedding.backPropGradient((CBufferFloat *)NULL))
       {
        PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
        StopFlag = true;
        break;
       }
     class=class="str">"cmt">//---
     if(GetTickCount() - ticks > class="num">500)
       {
        class="type">class="kw">string str = StringFormat("%-15s %class="num">5.2f%% -> Error %class="num">15.8f\n", "Agent", iter * class="num">100.0 / (class="type">class="kw">double)(Iterations), Agent.getRecentAverageError());
        str += StringFormat("%-15s %class="num">5.2f%% -> Error %class="num">15.8f\n", "Planner", iter * class="num">100.0 / (class="type">class="kw">double)(Iterations), Planner.getRecentAverageError());
        Comment(str);
        ticks = GetTickCount();
       }
     }
   }
 Comment("");
class=class="str">"cmt">//---
   PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Agent", Agent.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Planner", Planner.getRecentAverageError());
   ExpertRemove();
class=class="str">"cmt">//---
  }
把预训练轨迹诊断交给小布
这些离线轨迹嵌入质量与潜在状态一致性的诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到,你只管调优下游任务。

常见问题

扮演者基于前序轨迹预测动作,目标预测模型与未来编码器分别由当前状态和未来片段映射至同一潜在空间 Z,构成跨时域自动编码器,预训练阶段不依赖回报标记。
目前小布内置的是诊断与看板能力,模型权重需自行在 MT5 环境训练导出;小布可承接嵌入空间稳定性监控,降低人工巡检成本。
这样得到的未来先验与任务无关,下游接入奖励预测网络后,能快速把潜在状态关联到具体目标回报,适应不同任务规范。
k 取决于品种波动节律,偏高会模糊局部结构、偏低丢趋势依赖,实盘前建议在历史次优数据上做敏感度扫描,外汇贵金属均属高风险,结果仅作概率参考。