神经网络变得轻松(第七部分):自适应优化方法·进阶篇
🧠

神经网络变得轻松(第七部分):自适应优化方法·进阶篇

(2/3)·固定学习率在稀有样本前为何失灵,AdaGrad 到 Adam 的数学切换给你答案

偏理论 第 2/3 篇
不少交易者把 SGD 当神经网络训练的唯一解法,遇到含噪声或稀有值的行情特征就卡在局部最优。固定学习率不会自己让步,模型在金属跳空段常常学不动。本文接着上篇,把自适应优化方法的内部逻辑拆开。

◍ 神经元基类的权重与动量缓冲布局

在 MT5 用 OpenCL 跑神经网络时,基类 CNeuronBaseOCL 把权重和梯度都塞进 CBufferDouble 指针里,默认构造函数一次性 new 出 Output、PrevOutput、Weights、DeltaWeights、Gradient、FirstMomentum、SecondMomentum 共 7 个双精度缓冲。 优化器切换靠 ENUM_OPTIMIZATION 枚举,只给了 SGD 与 ADAM 两个选项;缓冲区角色则由 ENUM_BUFFERS 定义,WEIGHTS 到 SECOND_MOMENTUM 共 6 类,和类里那一串指针一一对应。 ADAM 相关的超参在构造时写死:b1=0.9、b2=0.999、t 从 1 起始,如果你在外汇 5 分钟线上做权重更新,直接改这两个 decay 率可能让收敛节奏明显变化(高风险,回测验证后再上实盘)。 下面这段宏定义把 UpdateWeightsAdam 的内核参数槽位用数字固定下来,def_k_uwa_matrix_w 到 def_k_uwa_l 分别是 0~6,b1、b2 占 7、8——调 OpenCL 内核传参时按这个序号填,错一位会直接算飞。

MQL5 / C++
class="macro">#define def_k_UpdateWeightsAdam    class="num">4
class="macro">#define def_k_uwa_matrix_w          class="num">0
class="macro">#define def_k_uwa_matrix_g          class="num">1
class="macro">#define def_k_uwa_matrix_i          class="num">2
class="macro">#define def_k_uwa_matrix_m          class="num">3
class="macro">#define def_k_uwa_matrix_v          class="num">4
class="macro">#define def_k_uwa_inputs            class="num">5
class="macro">#define def_k_uwa_l                 class="num">6
class="macro">#define def_k_uwa_b1                class="num">7
class="macro">#define def_k_uwa_b2                class="num">8
enum ENUM_OPTIMIZATION
  {
   SGD,
   ADAM
  };
class=class="str">"cmt">//---
enum ENUM_BUFFERS
  {
   WEIGHTS,
   DELTA_WEIGHTS,
   OUTPUT,
   GRADIENT,
   FIRST_MOMENTUM,
   SECOND_MOMENTUM
  };
class CNeuronBaseOCL    :  class="kw">public CObject
  {
class="kw">protected:
   .........
   .........
   .........
   CBufferDouble      *FirstMomentum;
   CBufferDouble      *SecondMomentum;
class=class="str">"cmt">//---
   .........
   .........
   class="kw">const class="type">class="kw">double        b1;
   class="kw">const class="type">class="kw">double        b2;
   class="type">int                 t;
class=class="str">"cmt">//---
   .........
   .........
   ENUM_OPTIMIZATION optimization;
CNeuronBaseOCL::CNeuronBaseOCL(class="type">void)  :  alpha(momentum),
                                        activation(TANH),
                                        optimization(SGD),
                                        b1(class="num">0.9),
                                        b2(class="num">0.999),
                                        t(class="num">1)
  {
   OpenCL=NULL;
   Output=new CBufferDouble();
   PrevOutput=new CBufferDouble();
   Weights=new CBufferDouble();
   DeltaWeights=new CBufferDouble();
   Gradient=new CBufferDouble();
   FirstMomentum=new CBufferDouble();
   SecondMomentum=new CBufferDouble();
  }
CNeuronBaseOCL::~CNeuronBaseOCL(class="type">void)
  {
   if(CheckPointer(Output)!=POINTER_INVALID)
      class="kw">delete Output;
   if(CheckPointer(PrevOutput)!=POINTER_INVALID)
      class="kw">delete PrevOutput;
   if(CheckPointer(Weights)!=POINTER_INVALID)
      class="kw">delete Weights;
   if(CheckPointer(DeltaWeights)!=POINTER_INVALID)
      class="kw">delete DeltaWeights;

神经网络对象的指针回收与权重初始化

在 MT5 的 OpenCL 神经网络封装里,析构前必须逐个校验指针再 delete,否则在 EA 重载时可能抛出无效指针访问。下面这段清理逻辑先确认 Gradient、FirstMomentum、SecondMomentum 不是 POINTER_INVALID,才执行释放,最后把 OpenCL 置空。 if(CheckPointer(Gradient)!=POINTER_INVALID) delete Gradient; if(CheckPointer(FirstMomentum)!=POINTER_INVALID) delete FirstMomentum; if(CheckPointer(SecondMomentum)!=POINTER_INVALID) delete SecondMomentum; OpenCL=NULL; Init 函数接管权重缓冲的创建:当 numOutputs>0 时,按 (numNeurons+1)*numOutputs 算出连接数,例如 numNeurons=10、numOutputs=1 时 count=11。权重用 (MathRand()+1)/32768.0-0.5 落到约 [-0.5,0.5) 区间,若随机到 0 则强制设为 0.001 避免死权重。 若优化类型选 SGD,还会额外建 DeltaWeights 缓冲并清零,同时误把 FirstMomentum/SecondMomentum 的 POINTER_INVALID 判断后接了 delete——这里原逻辑有笔误倾向,真实写法应是无效才 new、有效才 delete,复制时务必核对。外汇与贵金属行情下跑这类 GPU 计算负荷高,显存泄漏可能让策略在实盘中断,建议开 MT5 专家日志盯 CheckPointer 返回值。

MQL5 / C++
if(CheckPointer(Gradient)!=POINTER_INVALID)
   class="kw">delete Gradient;
if(CheckPointer(FirstMomentum)!=POINTER_INVALID)
   class="kw">delete FirstMomentum;
if(CheckPointer(SecondMomentum)!=POINTER_INVALID)
   class="kw">delete SecondMomentum;
OpenCL=NULL;
}
class="type">bool CNeuronBaseOCL::Init(class="type">uint numOutputs,class="type">uint myIndex,COpenCLMy *open_cl,class="type">uint numNeurons, ENUM_OPTIMIZATION optimization_type)
  {
  if(CheckPointer(open_cl)==POINTER_INVALID || numNeurons<=class="num">0)
    class="kw">return class="kw">false;
  OpenCL=open_cl;
  optimization=optimization_type;
class=class="str">"cmt">//---
  ....................
  ....................
  ....................
  ....................
class=class="str">"cmt">//---
  if(numOutputs>class="num">0)
    {
    if(CheckPointer(Weights)==POINTER_INVALID)
      {
      Weights=new CBufferDouble();
      if(CheckPointer(Weights)==POINTER_INVALID)
        class="kw">return class="kw">false;
      }
    class="type">int count=(class="type">int)((numNeurons+class="num">1)*numOutputs);
    if(!Weights.Reserve(count))
      class="kw">return class="kw">false;
    for(class="type">int i=class="num">0;i<count;i++)
      {
      class="type">class="kw">double weigh=(MathRand()+class="num">1)/class="num">32768.0-class="num">0.5;
      if(weigh==class="num">0)
        weigh=class="num">0.001;
      if(!Weights.Add(weigh))
        class="kw">return class="kw">false;
      }
    if(!Weights.BufferCreate(OpenCL))
      class="kw">return class="kw">false;
  class=class="str">"cmt">//---
    if(optimization==SGD)
      {
      if(CheckPointer(DeltaWeights)==POINTER_INVALID)
        {
        DeltaWeights=new CBufferDouble();
        if(CheckPointer(DeltaWeights)==POINTER_INVALID)
          class="kw">return class="kw">false;
        }
      if(!DeltaWeights.BufferInit(count,class="num">0))
        class="kw">return class="kw">false;
      if(!DeltaWeights.BufferCreate(OpenCL))
        class="kw">return class="kw">false;
      if(CheckPointer(FirstMomentum)==POINTER_INVALID)
        class="kw">delete FirstMomentum;
      if(CheckPointer(SecondMomentum)==POINTER_INVALID)

「动量缓冲的指针回收与权重更新内核装配」

在神经网络基类的初始化分支里,先对 SecondMomentum 做空指针判断,若 CheckPointer 返回 POINTER_INVALID 就直接 delete,避免悬空句柄占用显存。 若走 else 分支,则依次校验 DeltaWeights 与 FirstMomentum:任一缓冲无效就 new 一个 CBufferDouble,再次 CheckPointer 失败便 return false 中断。FirstMomentum 与 SecondMomentum 都需 BufferInit(count,0) 清零,再 BufferCreate(OpenCL) 挂到 OpenCL 上下文;这两步任一返回 false 同样终止。 当最外层条件不满足时,对 Weights 与 DeltaWeights 做非空判断后 delete,确保切换优化器时不残留旧缓冲。 updateInputWeights 里先用 CheckPointer 卡住 OpenCL 与传入神经元指针,无效直接撤出。随后设 global_work_size[0]=Neurons()、[1]=NeuronOCL.Neurons(),以二维网格覆盖全连接权矩阵。SGD 优化下调用 OpenCL.SetArgumentBuffer 把权重、梯度、输出、DeltaWeights 的索引依次绑到 def_k_UpdateWeightsMomentum 内核,再设输入神经元数与学习率 eta,交由 GPU 并行更新。外汇与贵金属行情下用此类 GPU 加速网络做推断,仍属高风险,过拟合可能导致样本外表现显著衰减。

MQL5 / C++
class="kw">delete SecondMomentum;
    }
  else
    {
     if(CheckPointer(DeltaWeights)==POINTER_INVALID)
        class="kw">delete DeltaWeights;
class=class="str">"cmt">//---
     if(CheckPointer(FirstMomentum)==POINTER_INVALID)
       {
        FirstMomentum=new CBufferDouble();
        if(CheckPointer(FirstMomentum)==POINTER_INVALID)
         class="kw">return class="kw">false;
       }
     if(!FirstMomentum.BufferInit(count,class="num">0))
        class="kw">return class="kw">false;
     if(!FirstMomentum.BufferCreate(OpenCL))
        class="kw">return class="kw">false;
class=class="str">"cmt">//---
     if(CheckPointer(SecondMomentum)==POINTER_INVALID)
       {
        SecondMomentum=new CBufferDouble();
        if(CheckPointer(SecondMomentum)==POINTER_INVALID)
         class="kw">return class="kw">false;
       }
     if(!SecondMomentum.BufferInit(count,class="num">0))
        class="kw">return class="kw">false;
     if(!SecondMomentum.BufferCreate(OpenCL))
        class="kw">return class="kw">false;
    }
   }
 else
   {
    if(CheckPointer(Weights)!=POINTER_INVALID)
      class="kw">delete Weights;
    if(CheckPointer(DeltaWeights)!=POINTER_INVALID)
      class="kw">delete DeltaWeights;
   }
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronBaseOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
  if(CheckPointer(OpenCL)==POINTER_INVALID || CheckPointer(NeuronOCL)==POINTER_INVALID)
     class="kw">return class="kw">false;
  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]=Neurons();
  global_work_size[class="num">1]=NeuronOCL.Neurons();
  if(optimization==SGD)
    {
     OpenCL.SetArgumentBuffer(def_k_UpdateWeightsMomentum,def_k_uwm_matrix_w,NeuronOCL.getWeightsIndex());
     OpenCL.SetArgumentBuffer(def_k_UpdateWeightsMomentum,def_k_uwm_matrix_g,getGradientIndex());
     OpenCL.SetArgumentBuffer(def_k_UpdateWeightsMomentum,def_k_uwm_matrix_i,NeuronOCL.getOutputIndex());
     OpenCL.SetArgumentBuffer(def_k_UpdateWeightsMomentum,def_k_uwm_matrix_dw,NeuronOCL.getDeltaWeightsIndex());
     OpenCL.SetArgument(def_k_UpdateWeightsMomentum,def_k_uwm_inputs,NeuronOCL.Neurons());
     OpenCL.SetArgument(def_k_UpdateWeightsMomentum,def_k_uwm_learning_rates,eta);

◍ 动量法与Adam在GPU上的权重更新分叉

神经网络层在OpenCL下做权重更新时,按优化器类型走了两条kernel路径:动量法走 UpdateWeightsMomentum,Adam走 UpdateWeightsAdam,二者都先塞参数再Execute,任一步SetArgument/Execute失败就直接return false并打出GetLastError()。 动量分支只设了alpha一个标量参数,随后用2维global_work_size启动kernel;Adam分支则要绑定5个显存缓冲区——权重w、梯度g、输出i、一阶矩m、二阶矩v,分别对应NeuronOCL和本层的索引句柄。 Adam的学习率做了偏差修正:lt = eta * sqrt(1 - pow(b2,t)) / (1 - pow(b1,t)),t是内部步数计数,每次更新后t++。注意global_work_size[1]被4整除向上取整(rest=size%4,补1个group当rest>0),这是kernel里向量化宽度=4的硬约束。 Save方法另起炉灶,先校验file_handle!=INVALID_HANDLE,再写Type()和activation枚举(各占一个INT_VALUE),任一次FileWriteInteger返回值小于INT_VALUE即判失败。外汇/贵金属模型训练耗GPU且过拟合风险高,参数请先在离线样本验证。

MQL5 / C++
OpenCL.SetArgument(def_k_UpdateWeightsMomentum,def_k_uwm_momentum,alpha);
ResetLastError();
if(!OpenCL.Execute(def_k_UpdateWeightsMomentum,class="num">2,global_work_offset,global_work_size))
  {
   printf("Error of execution kernel UpdateWeightsMomentum: %d",GetLastError());
   class="kw">return class="kw">false;
  }
 }
else
 {
  if(!OpenCL.SetArgumentBuffer(def_k_UpdateWeightsAdam,def_k_uwa_matrix_w,NeuronOCL.getWeightsIndex()))
    class="kw">return class="kw">false;
  if(!OpenCL.SetArgumentBuffer(def_k_UpdateWeightsAdam,def_k_uwa_matrix_g,getGradientIndex()))
    class="kw">return class="kw">false;
  if(!OpenCL.SetArgumentBuffer(def_k_UpdateWeightsAdam,def_k_uwa_matrix_i,NeuronOCL.getOutputIndex()))
    class="kw">return class="kw">false;
  if(!OpenCL.SetArgumentBuffer(def_k_UpdateWeightsAdam,def_k_uwa_matrix_m,NeuronOCL.getFirstMomentumIndex()))
    class="kw">return class="kw">false;
  if(!OpenCL.SetArgumentBuffer(def_k_UpdateWeightsAdam,def_k_uwa_matrix_v,NeuronOCL.getSecondMomentumIndex()))
    class="kw">return class="kw">false;
  class="type">class="kw">double lt=eta*sqrt(class="num">1-pow(b2,t))/(class="num">1-pow(b1,t));
  if(!OpenCL.SetArgument(def_k_UpdateWeightsAdam,def_k_uwa_inputs,NeuronOCL.Neurons()))
    class="kw">return class="kw">false;
  if(!OpenCL.SetArgument(def_k_UpdateWeightsAdam,def_k_uwa_l,lt))
    class="kw">return class="kw">false;
  if(!OpenCL.SetArgument(def_k_UpdateWeightsAdam,def_k_uwa_b1,b1))
    class="kw">return class="kw">false;
  if(!OpenCL.SetArgument(def_k_UpdateWeightsAdam,def_k_uwa_b2,b2))
    class="kw">return class="kw">false;
  class="type">uint rest=global_work_size[class="num">1]%class="num">4;
  global_work_size[class="num">1]=(global_work_size[class="num">1]-rest)/class="num">4 + (rest>class="num">0 ? class="num">1 : class="num">0);
  ResetLastError();
  if(!OpenCL.Execute(def_k_UpdateWeightsAdam,class="num">2,global_work_offset,global_work_size))
    {
     printf("Error of execution kernel UpdateWeightsAdam: %d",GetLastError());
     class="kw">return class="kw">false;
    }
  t++;
 }
class=class="str">"cmt">//---
 class="kw">return NeuronOCL.Weights.BufferRead();
}
class="type">bool CNeuronBaseOCL::Save(class="kw">const class="type">int file_handle)
  {
  if(file_handle==INVALID_HANDLE)
    class="kw">return class="kw">false;
  if(FileWriteInteger(file_handle,Type())<INT_VALUE)
    class="kw">return class="kw">false;
class=class="str">"cmt">//---
  if(FileWriteInteger(file_handle,(class="type">int)activation,INT_VALUE)<INT_VALUE)
    class="kw">return class="kw">false;

把网络状态落盘时的指针与优化器分支

这段保存逻辑的核心是先写标量再写对象缓冲:optimization 和 t 各用 FileWriteInteger 以 INT_VALUE 长度落盘,返回值小于 INT_VALUE 就直接 return false,说明任何整数写入截断都会让整个存档失效。 接着对 Output、PrevOutput、Gradient 三个对象做 CheckPointer 非空判断,并依次调用各自的 BufferRead 与 Save;只要其中一个指针无效或 Save 失败,存档立即中断。 Weights 的处理带了一个标志位:指针无效时写 0 并 return true,有效时写 1 再继续存权重缓冲。这里 0/1 的写入量就是 4 字节整数,用来标记该连接是否带权重。 优化器分支在末尾才分叉:SGD 只额外存 DeltaWeights,而 Adam 类优化器要存 FirstMomentum 和 SecondMomentum(对应 mt、vt)。CConnection 里 mt、vt 初值都是 0,Save 时分别用 FileWriteDouble 写出,任一个写入字节数 ≤0 就返回 false。 在 MT5 里跑这套,建议先打印 CheckPointer 各返回值,确认训练中途存档不会因某个动量缓冲未初始化而静默丢状态。外汇与贵金属模型训练本身高风险,存档完整性只解决复现问题,不改善过拟合。

MQL5 / C++
if(FileWriteInteger(file_handle,(class="type">int)optimization,INT_VALUE)<INT_VALUE)
      class="kw">return class="kw">false;
   if(FileWriteInteger(file_handle,(class="type">int)t,INT_VALUE)<INT_VALUE)
      class="kw">return class="kw">false;
   if(CheckPointer(Output)==POINTER_INVALID || !Output.BufferRead() || !Output.Save(file_handle))
      class="kw">return class="kw">false;
   if(CheckPointer(PrevOutput)==POINTER_INVALID || !PrevOutput.BufferRead() || !PrevOutput.Save(file_handle))
      class="kw">return class="kw">false;
   if(CheckPointer(Gradient)==POINTER_INVALID || !Gradient.BufferRead() || !Gradient.Save(file_handle))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   if(CheckPointer(Weights)==POINTER_INVALID)
     {
      FileWriteInteger(file_handle,class="num">0);
      class="kw">return true;
     }
   else
      FileWriteInteger(file_handle,class="num">1);
class=class="str">"cmt">//---
   if(CheckPointer(Weights)==POINTER_INVALID || !Weights.BufferRead() || !Weights.Save(file_handle))
      class="kw">return class="kw">false;
   if(optimization==SGD)
     {
      if(CheckPointer(DeltaWeights)==POINTER_INVALID || !DeltaWeights.BufferRead() || !DeltaWeights.Save(file_handle))
         class="kw">return class="kw">false;
     }
   else
     {
      if(CheckPointer(FirstMomentum)==POINTER_INVALID || !FirstMomentum.BufferRead() || !FirstMomentum.Save(file_handle))
         class="kw">return class="kw">false;
      if(CheckPointer(SecondMomentum)==POINTER_INVALID || !SecondMomentum.BufferRead() || !SecondMomentum.Save(file_handle))
         class="kw">return class="kw">false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class CConnection : class="kw">public CObject
  {
class="kw">public:
   class="type">class="kw">double         weight;
   class="type">class="kw">double         deltaWeight;
   class="type">class="kw">double         mt;
   class="type">class="kw">double         vt;
                  CConnection(class="type">class="kw">double w) { weight=w; deltaWeight=class="num">0; mt=class="num">0; vt=class="num">0; }
class="type">bool CConnection::Save(class="type">int file_handle)
  {
  ...........
  ...........
  ...........
  if(FileWriteDouble(file_handle,mt)<=class="num">0)
      class="kw">return class="kw">false;
  if(FileWriteDouble(file_handle,vt)<=class="num">0)
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
  class="kw">return true;
  }

「权重更新里的 Adam 与 SGD 分叉」

神经元的输入权重更新在 updateInputWeights 里走两条路:当 optimization 标记为 SGD 时用最朴素的梯度下降加动量,否则进入 Adam 分支。Adam 分支里 mt 是一阶矩、vt 是二阶矩,各自按 b1、b2 衰减,权重增量用 lt*mt/sqrt(vt) 表达,lt 由 eta 与两个衰减值按 sqrt(1-b2^t)/(1-b1^t) 修正。 代码里那行 lt 的计算值得在 MT5 里单步看:eta 是学习率,b1 与 b2 是类里 const double 成员,t 从 0 开始每次更新后自增。当 t 较小,分母 (1-b1^t) 偏小,lt 会被放大,初期步长比稳态大,这可能加速早期收敛。 非 SGD 分支给 vt 加了 0.00000001 的 epsilon 防除零,这是数值稳健的常见做法。外汇与贵金属行情噪声大,用 Adam 训练这类网络过拟合概率不低,开 MT5 把 b1=0.9、b2=0.999 先跑一遍再调。

MQL5 / C++
class="type">bool CConnection::Load(class="type">int file_handle)
  {
  ............
  ............
  ............
  mt=FileReadDouble(file_handle);
  vt=FileReadDouble(file_handle);
class=class="str">"cmt">//---
  class="kw">return true;
  }
class CNeuronBase    :  class="kw">public CObject
  {
class="kw">protected:
  .........
  .........
  .........
  ENUM_OPTIMIZATION optimization;
class="kw">const class="type">class="kw">double        b1;
  class="kw">const class="type">class="kw">double        b2;
   class="type">int                t;
class="type">bool CNeuronBase::Init(class="type">uint numOutputs,class="type">uint myIndex, ENUM_OPTIMIZATION optimization_type)
  {
  optimization=optimization_type;
class="type">bool CNeuron::updateInputWeights(CLayer *&prevLayer)
  {
  if(CheckPointer(prevLayer)==POINTER_INVALID)
     class="kw">return class="kw">false;
class=class="str">"cmt">//---
  class="type">class="kw">double lt=eta*sqrt(class="num">1-pow(b2,t))/(class="num">1-pow(b1,t));
  class="type">int total=prevLayer.Total();
  for(class="type">int n=class="num">0; n<total && !IsStopped(); n++)
    {
     CNeuron *neuron= prevLayer.At(n);
     CConnection *con=neuron.Connections.At(m_myIndex);
     if(CheckPointer(con)==POINTER_INVALID)
        class="kw">continue;
     if(optimization==SGD)
        con.weight+=con.deltaWeight=(gradient!=class="num">0 ? eta*neuron.getOutputVal()*gradient : class="num">0)+(con.deltaWeight!=class="num">0 ? alpha*con.deltaWeight : class="num">0);
     else
       {
        con.mt=b1*con.mt+(class="num">1-b1)*gradient;
        con.vt=b2*con.vt+(class="num">1-b2)*pow(gradient,class="num">2)+class="num">0.00000001;
        con.weight+=con.deltaWeight=lt*con.mt/sqrt(con.vt);
t++;
       }
    }
class=class="str">"cmt">//---
  class="kw">return true;
  }
class CLayerDescription    :  class="kw">public CObject
  {
class="kw">public:
                        CLayerDescription(class="type">void);
                       ~CLayerDescription(class="type">void) {};
  class=class="str">"cmt">//---
   class="type">int                 type;
   class="type">int                 count;
   class="type">int                 window;
   class="type">int                 step;
  ENUM_ACTIVATION      activation;
  ENUM_OPTIMIZATION optimization;
  };
CNet::CNet(CArrayObj *Description)
  {
  if(CheckPointer(Description)==POINTER_INVALID)
     class="kw">return;
class=class="str">"cmt">//---
  class="type">int total=Description.Total();
  if(total<=class="num">0)
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到不同优化器在样本上的收敛差异,你专注决策而非调参。

常见问题

AdaGrad 分母用历史梯度平方和,只会单调增大,学习率趋近零导致更新停滞,贵金属稀疏行情下更明显。
RMSProp 保留显式学习率,Adadelta 用参数自身的历史更新量替代学习率,省掉一个超参但改动类代码更多。
Adam 在指数均值基础上引入一阶动量偏差校正,缓解初期梯度方向抖动,训练稳定性更高。
可以,小布盯盘的 AIGC 模块对常见优化器做了可视化封装,切换品种就能看收敛曲线,不必自己写测试脚本。
概率上存在过拟合风险,建议配合验证集早停,外汇贵金属波动剧烈本身高风险,需控制仓位。