神经网络变得轻松(第五十一部分):行为-指引的扮演者-评论者(BAC)·进阶篇
📘

神经网络变得轻松(第五十一部分):行为-指引的扮演者-评论者(BAC)·进阶篇

第 2/3 篇

自编码层逆向堆叠与模型冷启动

这段逻辑在把已训练好的自编码器做反向展开:第 4 层复制原自编码器第 2 层(索引 2),第 5 层复制第 1 层(索引 1),第 6 层复制第 0 层(索引 0),逐层 new 出 CLayerDescription 并 Add 进容器,任何一步失败就 delete 并 return false,保证内存不漏。 OnInit 里先尝试 Actor.Load 读 "Act.nnw",若文件不存在或读取失败,就现场 new 出 actor 与 critic 两个 CArrayObj,调用 CreateDescriptions(actor, critic, critic) 按上面的层结构搭网络,再 Actor.Create(actor) 初始化权重;任一步返回 false 就删数组并 INIT_FAILED,外汇与贵金属品种下模型冷启动失败会直接让 EA 无法加载,属高风险环节。 另一个 OnInit 版本则先 ResetLastError,再 LoadTotalBase 装载历史学习数据,失败就 PrintFormat 打出错误码并返回 INIT_FAILED,说明实盘前必须确认基础数据文件就位,否则策略不会跑。

MQL5 / C++
class=class="str">"cmt">//--- layer class="num">4
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   if(!(descr.Copy(autoencoder.At(class="num">2))))
      {
       class="kw">delete descr;
       class="kw">return false;
      }
   if(!autoencoder.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;
   if(!(descr.Copy(autoencoder.At(class="num">1))))
      {
       class="kw">delete descr;
       class="kw">return false;
      }
   if(!autoencoder.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;
   if(!(descr.Copy(autoencoder.At(class="num">0))))
      {
       class="kw">delete descr;
       class="kw">return false;
      }
   if(!autoencoder.Add(descr))
      {
       class="kw">delete descr;
       class="kw">return false;
      }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
class=class="str">"cmt">//--- load models
   class="type">float temp;
   if(!Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true))
      {
       CArrayObj *actor = new CArrayObj();
       CArrayObj *critic = new CArrayObj();
       if(!CreateDescriptions(actor, critic, critic))
         {
          class="kw">delete actor;
          class="kw">delete critic;
          class="kw">return INIT_FAILED;
         }
       if(!Actor.Create(actor))
         {
          class="kw">delete actor;
          class="kw">delete critic;
          class="kw">return INIT_FAILED;
         }
       class="kw">delete actor;
       class="kw">delete critic;
      }
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   ResetLastError();
   if(!LoadTotalBase())
      {
       PrintFormat("Error of load study data: %d", GetLastError());
       class="kw">return INIT_FAILED;
      }

「模型加载失败时的冷启动重建」

EA 初始化阶段先尝试从磁盘载入六套网络权重:Actor、Critic1、Critic2、Autoencoder 以及两份 Target 网络(Crt1/Crt2 复用同一文件)。任一 Load 返回 false,就进入冷启动分支,用 CreateDescriptions 现搭网络结构再 Create,避免无模型直接崩初始化。 冷启动里三处失败都回 INIT_FAILED:描述对象构建失败、各网络 Create 失败、Target 网络 Create 失败。注意 actor/critic/autoencoder 三个 CArrayObj 指针每次失败路径都显式 delete,防止 MT5 终端内存泄漏。 重建完后立刻用 WeightsUpdate(GetPointer(Critic1), 1.0f) 把在线 Critic 权重整份拷给 Target,保证训练初期目标网络与估值网络一致。之后统一把 OpenCL 句柄从 Actor 透传给其余五个网络,让推理走 GPU 可能更快。 维度校验卡了两道:Actor 输出节点数必须等 NActions,否则报‘scope 不匹配’;输入层展开尺寸须等于 HistoryBars * BarDescr,例如 120 根 bar 乘 5 特征就是 600。最后取 LatentLayer 输出数作 latent_state,Critic1 输入维度若不等它直接 INIT_FAILED,这套检查能在加载畸形 nnw 时把错误拦在开盘前。外汇与贵金属杠杆高,模型维度配错会导致信号全废,上线前务必在策略测试器跑一次初始化日志。

MQL5 / C++
class=class="str">"cmt">//--- load models
  class="type">float temp;
  if(!Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) ||
     !Critic1.Load(FileName + "Crt1.nnw", temp, temp, temp, dtStudied, true) ||
     !Critic2.Load(FileName + "Crt2.nnw", temp, temp, temp, dtStudied, true) ||
     !Autoencoder.Load(FileName + "AEnc.nnw", temp, temp, temp, dtStudied, true) ||
     !TargetCritic1.Load(FileName + "Crt1.nnw", temp, temp, temp, dtStudied, true) ||
     !TargetCritic2.Load(FileName + "Crt2.nnw", temp, temp, temp, dtStudied, true))
     {
      CArrayObj *actor = new CArrayObj();
      CArrayObj *critic = new CArrayObj();
      CArrayObj *autoencoder = new CArrayObj();
      if(!CreateDescriptions(actor, critic, autoencoder))
        {
         class="kw">delete actor;
         class="kw">delete critic;
         class="kw">delete autoencoder;
         class="kw">return INIT_FAILED;
        }
      if(!Actor.Create(actor) || !Critic1.Create(critic) || !Critic2.Create(critic) ||
         !Autoencoder.Create(autoencoder))
        {
         class="kw">delete actor;
         class="kw">delete critic;
         class="kw">delete autoencoder;
         class="kw">return INIT_FAILED;
        }
      if(!TargetCritic1.Create(critic) || !TargetCritic2.Create(critic))
        {
         class="kw">delete actor;
         class="kw">delete critic;
         class="kw">delete autoencoder;
         class="kw">return INIT_FAILED;
        }
      class="kw">delete actor;
      class="kw">delete critic;
      class="kw">delete autoencoder;
      class=class="str">"cmt">//---
      TargetCritic1.WeightsUpdate(GetPointer(Critic1), class="num">1.0f);
      TargetCritic2.WeightsUpdate(GetPointer(Critic2), class="num">1.0f);
     }
  OpenCL = Actor.GetOpenCL();
  Critic1.SetOpenCL(OpenCL);
  Critic2.SetOpenCL(OpenCL);
  TargetCritic1.SetOpenCL(OpenCL);
  TargetCritic2.SetOpenCL(OpenCL);
  Autoencoder.SetOpenCL(OpenCL);
  Actor.getResults(Result);
  if(Result.Total() != NActions)
     {
      PrintFormat("The scope of the actor does not match the actions count(%d <> %d)", NActions, Result.Total());
      class="kw">return INIT_FAILED;
     }
class=class="str">"cmt">//---
  Actor.GetLayerOutput(class="num">0, Result);
  if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Actor doesn&class="macro">#x27;t match state description(%d <> %d)", Result.Total(),
(HistoryBars * BarDescr));
      class="kw">return INIT_FAILED;
     }
class=class="str">"cmt">//---
  Actor.GetLayerOutput(LatentLayer, Result);
  class="type">int latent_state = Result.Total();
  Critic1.GetLayerOutput(class="num">0, Result);
  if(Result.Total() != latent_state)
     {
      PrintFormat("Input size of Critic doesn&class="macro">#x27;t match latent state Actor(%d <> %d)", Result.Total(), latent_state);

◍ 初始化校验与训练样本采样

这段逻辑卡在 EA 启动与离线训练两个节点上。Init 收尾阶段先用 Critic1 的第 1 层输出拿到 latent_state 维度,再比对 Autoencoder 第 0 层输出总数;若两者不一致(例如 Critic 压出 64 维而自编码器输入是 128 维),直接 PrintFormat 报错并返回 INIT_FAILED,模型根本不会跑。 EventChartCustom 发一个 'Init' 自定义事件到当前图表,失败就返回错误码并中止。这一步是给前端盯盘面板发信号,没收到就可能说明图表句柄异常,外汇/贵金属实盘里这种静默失败概率不低,建议手动在专家日志里确认事件是否发出。 OnDeinit 里把 Target 网络按 Tau 软更新后,把 Actor、两个 Critic、Autoencoder 各自存成 .nnw 文件,文件名带 Act/Crt1/Crt2/AEnc 前缀。注意 Crt1 和 Crt2 都调了 TargetCritic1.Save——第二个调用参数虽传了 Critic2 的平均误差,但落盘对象仍是 TargetCritic1,这行大概率是笔误,复制代码时得改掉。 Train 函数开头取 Buffer 总样本数 total_tr,用 GetTickCount 记时。采样不是顺序取:tr 用 MathRand()/32767.0 在 [0,total_tr-1] 随机选轨迹,i 用两次 MathRand 相乘再除以 32767 平方,偏向小索引采样,Buffer[tr].Total-2 保证能取 i+1 状态。这种非均匀采样在奖励稀疏的贵金属行情回放里,可能更倾向近期过渡样本。

MQL5 / C++
  class="kw">return INIT_FAILED;
    }
  Critic1.GetLayerOutput(class="num">1, Result);
  latent_state = Result.Total();
  Autoencoder.GetLayerOutput(class="num">0, Result);
  if(Result.Total() != latent_state)
    {
      PrintFormat("Input size of Autoencoder doesn&class="macro">#x27;t match latent state Critic(%d <> %d)", Result.Total(), latent_state);
      class="kw">return INIT_FAILED;
    }
  Gradient.BufferInit(AccountDescr, class="num">0);
class=class="str">"cmt">//---
  if(!EventChartCustom(ChartID(), class="num">1, class="num">0, class="num">0, "Init"))
    {
      PrintFormat("Error of create study event: %d", GetLastError());
      class="kw">return INIT_FAILED;
    }
class=class="str">"cmt">//---
  class="kw">return(INIT_SUCCEEDED);
  }
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//---
  TargetCritic1.WeightsUpdate(GetPointer(Critic1), Tau);
  TargetCritic2.WeightsUpdate(GetPointer(Critic2), Tau);
  Actor.Save(FileName + "Act.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true);
  TargetCritic1.Save(FileName + "Crt1.nnw", Critic1.getRecentAverageError(), class="num">0, class="num">0, TimeCurrent(), true);
  TargetCritic1.Save(FileName + "Crt2.nnw", Critic2.getRecentAverageError(), class="num">0, class="num">0, TimeCurrent(), true);
  Autoencoder.Save(FileName + "AEnc.nnw", Autoencoder.getRecentAverageError(), class="num">0, class="num">0, TimeCurrent(), true);
  class="kw">delete Result;
  }
class="type">void Train(class="type">void)
  {
  class="type">int total_tr = ArraySize(Buffer);
  class="type">uint ticks = GetTickCount();
class=class="str">"cmt">//---
  for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped()); 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)) * (Buffer[tr].Total - class="num">2));
      class=class="str">"cmt">//--- Target
      State.AssignArray(Buffer[tr].States[i + class="num">1].state);
      class="type">float PrevBalance = Buffer[tr].States[i].account[class="num">0];
      class="type">float PrevEquity = Buffer[tr].States[i].account[class="num">1];
      Account.Clear();
      Account.Add((Buffer[tr].States[i + class="num">1].account[class="num">0] - PrevBalance) / PrevBalance);
      Account.Add(Buffer[tr].States[i + class="num">1].account[class="num">1] / PrevBalance);
      Account.Add((Buffer[tr].States[i + class="num">1].account[class="num">1] - PrevEquity) / PrevEquity);
      Account.Add(Buffer[tr].States[i + class="num">1].account[class="num">2]);
      Account.Add(Buffer[tr].States[i + class="num">1].account[class="num">3]);
      Account.Add(Buffer[tr].States[i + class="num">1].account[class="num">4] / PrevBalance);
      Account.Add(Buffer[tr].States[i + class="num">1].account[class="num">5] / PrevBalance);

账户特征里的周期正弦编码

这段逻辑把账户状态压成一组喂给神经网络的输入向量,核心是用不同时间周期的秒数做归一,再套正弦余弦把线性时间变成周期信号。外汇与贵金属杠杆高,这类特征若直接用于实盘策略,回测拟合倾向过强,需警惕样本外失效。 先看账户权益比:用下标 6 的账户字段除以上一帧余额 PrevBalance 塞进 Account 缓冲;接着用 2023.01.01 到 2024.01.01 的秒差(约 31536000 秒)做分母,把下标 7 的累计值转成频率 x,取 2πx 的正弦。随后换 PERIOD_MN1(月线 2592000 秒)、PERIOD_W1(周线 604800 秒)、PERIOD_D1(日线 86400 秒)分别算余弦或正弦,等于把同一笔账户数据投影到月、周、日三个尺度的相位上。 前向传播一旦失败就打印函数与行号并移除 EA,说明这套 RL 推理链容错极低。奖励项用了双评论家取最小值的保守 Q 估计:reward = 本帧奖励 + 折扣因子 × (min(评论家1,评论家2) − 下一帧奖励),这是典型 TD3 式截断。最后把状态数组回填,用 Max(i-1,0) 防越界取前一帧余额权益,更新账户向量的相对变化率,完成一步特征重构。 想验证就开 MT5 把 PeriodSeconds 三个周期打印出来,确认你 broker 的周线秒数是否为 604800,某些平台夏令时偏移会让这个常量悄悄偏掉。

MQL5 / C++
  Account.Add(Buffer[tr].States[i + class="num">1].account[class="num">6] / PrevBalance);
  class="type">class="kw">double x = (class="type">class="kw">double)Buffer[tr].States[i + class="num">1].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;);
  Account.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
  x = (class="type">class="kw">double)Buffer[tr].States[i + class="num">1].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1);
  Account.Add((class="type">float)MathCos(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
  x = (class="type">class="kw">double)Buffer[tr].States[i + class="num">1].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1);
  Account.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
  x = (class="type">class="kw">double)Buffer[tr].States[i + class="num">1].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1);
  Account.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
  class=class="str">"cmt">//---
  if(Account.GetIndex() >= class="num">0)
     Account.BufferWrite();
  if(!Actor.feedForward(GetPointer(State), class="num">1, false, GetPointer(Account)))
    {
     PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
     ExpertRemove();
     class="kw">break;
    }
  class=class="str">"cmt">//---
  if(!TargetCritic1.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actor)) ||
     !TargetCritic2.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actor)))
    {
     PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
     class="kw">break;
    }
  TargetCritic1.getResults(Result);
  class="type">float reward = Result[class="num">0];
  TargetCritic2.getResults(Result);
  reward = Buffer[tr].Revards[i] + DiscFactor * (MathMin(reward, Result[class="num">0]) - Buffer[tr].Revards[i + class="num">1]);
  class=class="str">"cmt">//--- Q-function study
  State.AssignArray(Buffer[tr].States[i].state);
  PrevBalance = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">0];
  PrevEquity = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">1];
  Account.Update(class="num">0, (Buffer[tr].States[i].account[class="num">0] - PrevBalance) / PrevBalance);
  Account.Update(class="num">1, Buffer[tr].States[i].account[class="num">1] / PrevBalance);
  Account.Update(class="num">2, (Buffer[tr].States[i].account[class="num">1] - PrevEquity) / PrevEquity);

「把账户状态塞进网络前的特征构造」

这段逻辑干的事很直接:先把回放缓冲区里的账户字段逐条写进 Account 特征容器,索引 3~6 对应原始权益、浮动盈亏等绝对值,而 5~7 位除以 PrevBalance 做归一化,避免不同本金规模下量纲把梯度带偏。 周期相位特征是另一块。代码用账户累计值 account[7] 分别除以 2023 全年秒数、月线秒数、周线秒数和日线秒数,再乘 2π 送进 sin/cos,等于把‘资金曲线走过的时间长度’投影到不同周期的圆周上。x 为 0 时强制相位 0,防止除零炸网络。 喂完特征后 Actor 先前向推理拿动作,Critic1/2 双网络并行评估同一动作;误差用 reward 减 Critic1 输出,首轮直接赋值,之后用 0.99/0.01 的 EMA 平滑跟踪 AvgCriticError。双 Critic 取最小倾向能缓解 TD 过估计,但外汇与贵金属杠杆环境下过估计仍可能放大回撤,实盘前建议在 MT5 用历史 tick 重放这段代码核对误差分布。

MQL5 / C++
Account.Update(class="num">3, Buffer[tr].States[i].account[class="num">2]);
Account.Update(class="num">4, Buffer[tr].States[i].account[class="num">3]);
Account.Update(class="num">5, Buffer[tr].States[i].account[class="num">4] / PrevBalance);
Account.Update(class="num">6, Buffer[tr].States[i].account[class="num">5] / PrevBalance);
Account.Update(class="num">7, Buffer[tr].States[i].account[class="num">6] / PrevBalance);
x = (class="type">class="kw">double)Buffer[tr].States[i].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;);
Account.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
x = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1);
Account.Add((class="type">float)MathCos(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
x = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1);
Account.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
x = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1);
Account.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
Account.BufferWrite();
if(!Actor.feedForward(GetPointer(State), class="num">1, false, GetPointer(Account)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   class="kw">break;
  }
Actions.AssignArray(Buffer[tr].States[i].action);
if(Actions.GetIndex() >= class="num">0)
  Actions.BufferWrite();
class=class="str">"cmt">//---
if(!Critic1.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actions)) ||
   !Critic2.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actions)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   class="kw">break;
  }
Critic1.getResults(Result);
class="type">float error = reward - Result[class="num">0];
if(iter == class="num">0)
  {
   MaxCriticError = error;
   MinCriticError = error;
   AvgCriticError = error;
  }
else
  {
   MaxCriticError = MathMax(error, MaxCriticError);
   MinCriticError = MathMin(error, MinCriticError);
   AvgCriticError = class="num">0.99f * AvgCriticError + class="num">0.01f * error;
  }
Critic2.getResults(Result);

◍ 双评论家择优驱动策略回传

在强化学习训练循环里,评论家网络的误差跟踪直接决定策略更新的稳定性。代码用 MaxCriticError、MinCriticError 与 AvgCriticError 三个变量滚动记录偏差,其中 AvgCriticError 按 0.99f 与 0.01f 的遗忘系数做指数平滑,意味着近期单步 error 仅占权重 1%,历史惯性占 99%。 两个评论家 Critic1 与 Critic2 各自反向传播后,通过比较 getRecentAverageError() 挑出近期平均误差更低的一个作为主导 critic。这种双网络择优机制,可能缓解单网络过拟合导致的策略漂移。 策略侧利用主导 critic 的输出与自编码器重建损失,构造带温度系数的 alpha:当 Max 与 Min 误差相等时 alpha 置 0,否则按 10.0f 倍缩放 AvgCriticError 相对位置,再经 sigmoid 反转得到 1-alpha。该系数乘到 LOSS_MSE 上,作为奖励修正项叠加 PoliticAdjust。 外汇与贵金属市场高杠杆、高波动,这类自适应奖励整形只是训练框架的一环,实盘前务必在 MT5 策略测试器用历史数据跑通 backProp 链路,观察 AvgCriticError 是否收敛而非发散。

MQL5 / C++
error = reward - Result[class="num">0];
MaxCriticError = MathMax(error, MaxCriticError);
MinCriticError = MathMin(error, MinCriticError);
AvgCriticError = class="num">0.99f * AvgCriticError + class="num">0.01f * error;
Result.Update(class="num">0, reward);
if(!Critic1.backProp(Result, GetPointer(Actions), GetPointer(Gradient)) ||
   !Critic2.backProp(Result, GetPointer(Actions), GetPointer(Gradient)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   class="kw">break;
   }
class=class="str">"cmt">//--- Policy study
CNet *critic = NULL;
if(Critic1.getRecentAverageError() <= Critic2.getRecentAverageError())
   critic = GetPointer(Critic1);
else
   critic = GetPointer(Critic2);
if(!critic.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actor)) ||
   !Autoencoder.feedForward(critic, class="num">1, NULL, -class="num">1))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   class="kw">break;
   }
class="type">bool CNet::feedForward(CNet *inputNet, class="type">int inputLayer = -class="num">1, CNet *secondNet = NULL, class="type">int secondLayer = -class="num">1)
  {
class=class="str">"cmt">//---
  if(layer.At(class="num">0) != neuron)
   if(!layer.Update(class="num">0, neuron))
     {
      if(del_second)
         class="kw">delete second;
      class="kw">return false;
     }
   else
      layer.FreeMode(false);
class=class="str">"cmt">//---
  class="kw">return true;
  }
Autoencoder.getResults(AutoencoderResult);
critic.GetLayerOutput(class="num">1, Result);
Result.GetData(CriticResult);
critic.getResults(Result);
class="type">float alpha = (MaxCriticError == MinCriticError ? class="num">0 :
                                       class="num">10.0f * (AvgCriticError - MinCriticError) / (MaxCriticError - MinCriticError));
alpha = class="num">1.0f / (class="num">1.0f + MathExp(-alpha));
alpha = class="num">1 - alpha;
reward = Result[class="num">0];
reward = (reward > class="num">0 ? reward + PoliticAdjust : PoliticAdjust);
reward += AutoencoderResult.Loss(CriticResult, LOSS_MSE) * alpha;
Result.Update(class="num">0, reward);
critic.TrainMode(false);
if(!critic.backProp(Result, GetPointer(Actor)) ||
   !Actor.backPropGradient(GetPointer(Account), GetPointer(Gradient), LatentLayer) ||
   !Actor.backPropGradient(GetPointer(Account), GetPointer(Gradient)))
  {

常见问题

先清空旧权重缓存,按编码器逆序逐层初始化,再用小批量随机样本跑一遍前向校验,通过后再正式训练。
用 sin(2π·t/T) 和 cos(2π·t/T) 构造,T 取账户历史典型波动周期(如 24 或 168 根 K 线),实测再微调。
可以,小布能按你设定的周期正弦参数自动拼账户状态特征,并在模型加载失败时一键触发冷启动重建与采样校验。
取估值差最小的那个评论家主导回传,分歧超阈值则暂停更新策略,先单独微调评论家网络。
至少采 200 笔覆盖多行情段的样本,前向输出无 NaN 且损失下降即算通过,样本太少易误判。