群体自适应矩估计(ADAM)优化算法·进阶篇
🧠

群体自适应矩估计(ADAM)优化算法·进阶篇

(2/3)·原版ADAM只能吃解析梯度,这篇把它拆开重装,让无解析梯度的全局优化也能跑起来

偏理论进阶 第 2/3 篇
不少交易者把ADAM当成带随机性的黑箱优化器,调参时乱加噪声期望更鲁棒。其实它的内核完全确定性,随机只来自数据喂入方式,误用会掩盖真实收敛问题。

ADAM 梯度更新与最优个体留存

这段实现把梯度下降的动量项和二阶矩估计接进了种群迭代里。先算每个个体的目标函数变化量 ΔF,再逐坐标求 ΔX,遇到坐标无变化就垫一个 epsilon 防止除零,梯度直接取 ΔF/ΔX。 更新阶段用标准 ADAM 公式:一阶矩 m 按 beta1 衰减累加梯度,二阶矩 v 按 beta2 衰减累加梯度平方;再用迭代次数 t 做偏差校正得到 m_hat 和 v_hat,坐标沿校正后的方向以 alpha 步长推进,最后用 SeInDiSp 把参数夹回允许区间。 Revision 函数只干一件事:扫一遍种群,若某个体目标值大于已记录最优 fB 就记下索引,随后把该个体坐标整体拷进 cB。外汇与贵金属参数优化属高风险实验,回测优异不代表实盘概率占优。 代码里 beta1、beta2 通常取 0.9 与 0.999,epsilon 多为 1e-8;t 从 1 累计,MathPow(beta1,t) 越往后越接近 0,校正因子才趋于 1。

MQL5 / C++
  ΔF = a [i].f - a [i].fP;                 class=class="str">"cmt">// Calculate the change of the function
  for (class="type">int c = class="num">0; c < coords; c++)
  {
    ΔX = a [i].c [c] - a [i].cP [c]; class=class="str">"cmt">// Calculate the change in coordinates
    if (ΔX == class="num">0.0) ΔX = epsilon;     class=class="str">"cmt">// If change is zero, set it to epsilon
    grad [i].g [c] = ΔF / ΔX;        class=class="str">"cmt">// Calculate the gradient
  }
  }
  class=class="str">"cmt">// Update parameters using ADAM algorithm
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    class=class="str">"cmt">// Save the previous value of the function
    a [i].fP = a [i].f;
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      class=class="str">"cmt">// Save the previous coordinate value
      a [i].cP [c] = a [i].c [c];
      class=class="str">"cmt">// Update the biased first moment estimate
      grad [i].m [c] = beta1 * grad [i].m [c] + (class="num">1.0 - beta1) * grad [i].g [c];
      class=class="str">"cmt">// Update the biased second moment estimate
      grad [i].v [c] = beta2 * grad [i].v [c] + (class="num">1.0 - beta2) * grad [i].g [c] * grad [i].g [c];
      class=class="str">"cmt">// Calculate the adjusted first moment estimate
      class="type">class="kw">double m_hat = grad [i].m [c] / (class="num">1.0 - MathPow(beta1, t));
      class=class="str">"cmt">// Calculate the adjusted estimate of the second moment
      class="type">class="kw">double v_hat = grad [i].v [c] / (class="num">1.0 - MathPow(beta2, t));
      class=class="str">"cmt">// Update coordinates
      a [i].c [c] = a [i].c [c] + (alpha * m_hat / (MathSqrt(v_hat) + epsilon));
      class=class="str">"cmt">// Make sure the coordinates stay within the allowed range
      a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
    }
  }
  t++; class=class="str">"cmt">// Increase the iteration counter
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_ADAM::Revision()
{
  class="type">int ind = -class="num">1;     class=class="str">"cmt">// Best individual index
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if (a [i].f > fB) class=class="str">"cmt">// If the current value of the function is greater than the best one
    {
      fB = a [i].f;  class=class="str">"cmt">// Update the best value of the function
      ind = i;       class=class="str">"cmt">// Store the index of the best individual
    }
  }
  if (ind != -class="num">1) ArrayCopy(cB, a [ind].c, class="num">0, class="num">0, WHOLE_ARRAY); class=class="str">"cmt">// Copy the coordinates of the best individual
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class C_AO_ADAMm : class="kw">public C_AO
{
  class="kw">public: class=class="str">"cmt">//--------------------------------------------------------------------
  class=class="str">"cmt">// Class destructor
  ~C_AO_ADAMm() { }
  class=class="str">"cmt">// Class constructor
  C_AO_ADAMm()
  {
    ao_name = "ADAMm";                                        class=class="str">"cmt">// Algorithm name
    ao_desc = "Adaptive Moment Estimation M";                  class=class="str">"cmt">// Algorithm description

◍ 遗传优化器的参数落地写法

把自适应优化器接到 MT5 上,第一件事是把种群与学习率参数固化成数组,否则回测时每次手动改全局变量极易出错。下面这段初始化把 7 个核心量写死:种群规模 100、杂交比例 0.5、杂交抗变能力 10、学习率 0.001,以及 Adam 类更新用的 beta1=0.9、beta2=0.999、epsilon=0.1。 杂交比例设 0.5 意味着每代一半个体是混合基因,抗变参数 10 则决定它们多大程度上拒绝被梯度拽着走;这两个值直接左右外层 EA 在外汇或贵金属品种上过拟合的概率,实盘前建议在历史数据上各跑一遍对照。 SetParams 负责把数组里的浮点值回写进实际运算变量,注意 popSize 做了强制 int 转换,其余原样赋值。高风险提示:贵金属与外汇杠杆品种参数敏感,任何优化结果都只是历史倾向,不等于未来分布。

MQL5 / C++
ao_link = "[MQL5官方文档] class=class="str">"cmt">// Link to the article
popSize              = class="num">100;     class=class="str">"cmt">// Population size
hybridsPercentage = class="num">0.5;     class=class="str">"cmt">// Percentage of hybrids in the population
hybridsResistance = class="num">10;      class=class="str">"cmt">// Resistance of hybrids to changes
alpha              = class="num">0.001;  class=class="str">"cmt">// Learning ratio
beta1              = class="num">0.9;    class=class="str">"cmt">// Exponential decay ratio for the first moment
beta2              = class="num">0.999;  class=class="str">"cmt">// Exponential decay ratio for the second moment
epsilon            = class="num">0.1;    class=class="str">"cmt">// Small constant to prevent division by zero
class=class="str">"cmt">// Initialize the parameter array
ArrayResize(params, class="num">7);
params [class="num">0].name = "popSize";           params [class="num">0].val = popSize;
params [class="num">1].name = "hybridsPercentage"; params [class="num">1].val = hybridsPercentage;
params [class="num">2].name = "hybridsResistance"; params [class="num">2].val = hybridsResistance;
params [class="num">3].name = "alpha";             params [class="num">3].val = alpha;
params [class="num">4].name = "beta1";             params [class="num">4].val = beta1;
params [class="num">5].name = "beta2";             params [class="num">5].val = beta2;
params [class="num">6].name = "epsilon";           params [class="num">6].val = epsilon;
}
class=class="str">"cmt">// Method for setting parameters
class="type">void SetParams()
{
  popSize           = (class="type">int)params [class="num">0].val;   class=class="str">"cmt">// Set population size
  hybridsPercentage = params    [class="num">1].val;   class=class="str">"cmt">// Set the percentage of hybrids in the population
  hybridsResistance = params    [class="num">2].val;   class=class="str">"cmt">// Set hybrids&class="macro">#x27; resistance to change
  alpha             = params    [class="num">3].val;   class=class="str">"cmt">// Set the learning ratio
  beta1             = params    [class="num">4].val;   class=class="str">"cmt">// Set beta1
  beta2             = params    [class="num">5].val;   class=class="str">"cmt">// Set beta2
  epsilon           = params    [class="num">6].val;   class=class="str">"cmt">// Set epsilon
}

「ADAM 优化器的种群初始化拆解」

在把自适应矩估计(ADAM)思路搬进 EA 的种群训练时,Init 函数先接收三组搜索边界数组:rangeMinP、rangeMaxP 给出每个参数的最小/最大寻优区间,rangeStepP 控制网格步长;epochsP 默认 0 表示不限制训练轮次,交给外部循环控停。 类内部暴露的几个双精度字段直接决定收敛行为:alpha 是学习率,beta1/beta2 分别是一阶、二阶矩的指数衰减率,epsilon 防止除零的小常数;hybridsPercentage 与 hybridsResistance 描述杂交个体在种群中的占比及其抗扰动能力。 Init 主体里先调 StandardInit 做常规校验,失败直接返回 false。随后把 step 归零、t 置 1,并用 popSize * hybridsPercentage 算出杂交个体数;若算出来超过种群规模则截断为 popSize。在 MT5 里把 popSize 设为 200、hybridsPercentage 设为 0.3,实际杂交数会是 60,这一数值会直接影响后代多样性。 外汇与贵金属市场波动剧烈、杠杆风险高,此类优化仅用于离线参数搜索,实盘前须用历史数据交叉验证。

MQL5 / C++
class="type">bool Init(const class="type">class="kw">double &rangeMinP   [],   class=class="str">"cmt">// minimum search range
                    const class="type">class="kw">double &rangeMaxP   [],   class=class="str">"cmt">// maximum search range
                    const class="type">class="kw">double &rangeStepP [],   class=class="str">"cmt">// search step
                    const class="type">int     epochsP = class="num">0);   class=class="str">"cmt">// number of epochs
   class="type">void Moving(); class=class="str">"cmt">// Moving method
   class="type">void Revision(); class=class="str">"cmt">// Revision method
   class=class="str">"cmt">//----------------------------------------------------------------------------
   class="type">class="kw">double hybridsPercentage;  class=class="str">"cmt">// Percentage of hybrids in the population
   class="type">class="kw">double hybridsResistance;  class=class="str">"cmt">// Resistance of hybrids to changes
   class="type">class="kw">double alpha;              class=class="str">"cmt">// Learning ratio
   class="type">class="kw">double beta1;              class=class="str">"cmt">// Exponential decay ratio for the first moment
   class="type">class="kw">double beta2;              class=class="str">"cmt">// Exponential decay ratio for the second moment
   class="type">class="kw">double epsilon;            class=class="str">"cmt">// Small constant
   S_Gradients grad []; class=class="str">"cmt">// Array of gradients
   class="kw">private: class=class="str">"cmt">//-------------------------------------------------------------------
   class="type">int step;          class=class="str">"cmt">// Iteration step
   class="type">int t;             class=class="str">"cmt">// Iteration counter
   class="type">int hybridsNumber; class=class="str">"cmt">// Number of hybrids in the population
};
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">bool C_AO_ADAMm::Init(const class="type">class="kw">double &rangeMinP   [],
                    const class="type">class="kw">double &rangeMaxP   [],
                    const class="type">class="kw">double &rangeStepP [],
                    const class="type">int     epochsP = class="num">0)
{
   class=class="str">"cmt">// Standard initialization
   if (!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) class="kw">return false;
   class=class="str">"cmt">//----------------------------------------------------------------------------
   step            = class="num">0;                                                     class=class="str">"cmt">// Reset step counter
   t               = class="num">1;                                                     class=class="str">"cmt">// Reset iteration counter
   hybridsNumber = class="type">int(popSize * hybridsPercentage);                        class=class="str">"cmt">// Calculation of the number of hybrids in the population 
   if (hybridsNumber > popSize) hybridsNumber = popSize;                    class=class="str">"cmt">// Correction

ADAM 迭代里的前两步随机播种

在 C_AO_ADAMm::Moving() 中,step < 2 的前两步不走梯度,而是给每个个体随机重置坐标。这一步相当于冷启动:先把种群撒进参数空间,避免初始点全挤在局部洼地。 具体看,a[i].fP 和 a[i].cP[] 先存旧值与旧坐标,再用 u.RNDfromCI 在 [rangeMin, rangeMax] 内随机生成新坐标,最后用 u.SeInDiSp 按 rangeStep 对齐到合法离散格点。step 计数器自增后直接 return,因此前两次调用根本不计算 grad。 从第三步起才进入真正的梯度估计:ΔF = a[i].f - a[i].fP,ΔX 若等于 0 则塞入 epsilon 防除零,grad[i].g[c] = ΔF / ΔX 得到近似偏导。后面 ADAM 的 m、v 更新才接上。 你在 MT5 里跑这套优化器时,可以把 popSize 设 50、hybridsNumber 设 5,观察前两步日志里坐标是否均匀铺开;若发现 step=1 后坐标仍聚堆,优先查 rangeMin/rangeMax 边界或 u.RNDfromCI 的实现。外汇与贵金属参数优化属高风险,回测吻合不代表实盘概率占优。

MQL5 / C++
  ArrayResize(grad, popSize);                                                                     class=class="str">"cmt">// Resize the gradient array
  for (class="type">int i = class="num">0; i < popSize; i++) grad [i].Init(coords); class=class="str">"cmt">// Initialize gradients for each individual
  class="kw">return true;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_ADAMm::Moving()
{
  class=class="str">"cmt">//----------------------------------------------------------------------------
  if (step < class="num">2) class=class="str">"cmt">// If step is less than class="num">2
  {
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      a [i].fP = a [i].f; class=class="str">"cmt">// Save the previous value of the function
      for (class="type">int c = class="num">0; c < coords; c++)
      {
        a [i].cP [c] = a [i].c [c]; class=class="str">"cmt">// Save the previous coordinate value
        class=class="str">"cmt">// Generate new coordinates randomly
        a [i].c [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);
        class=class="str">"cmt">// Bringing new coordinates to acceptable values
        a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
      }
    }
    step++; class=class="str">"cmt">// Increase the step counter
    class="kw">return; class=class="str">"cmt">// Exit the method
  }
  class=class="str">"cmt">//----------------------------------------------------------------------------
  class="type">class="kw">double ΔF, ΔX; class=class="str">"cmt">// Changes in function and coordinates
  class="type">class="kw">double cNew;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    ΔF = a [i].f - a [i].fP;            class=class="str">"cmt">// Calculate the change of the function
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      ΔX = a [i].c [c] - a [i].cP [c]; class=class="str">"cmt">// Calculate the change in coordinates
      if (ΔX == class="num">0.0) ΔX = epsilon;     class=class="str">"cmt">// If change is zero, set it to epsilon
      grad [i].g [c] = ΔF / ΔX;        class=class="str">"cmt">// Calculate the gradient
    }
  }
  class=class="str">"cmt">// Update parameters using ADAM algorithm
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    class=class="str">"cmt">// Save the previous value of the function
    a [i].fP = a [i].f;
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      class=class="str">"cmt">// Save the previous coordinate value
      a [i].cP [c] = a [i].c [c];
      if (i >= popSize - hybridsNumber)
      {
        class="type">class="kw">double pr = u.RNDprobab();
        pr *= pr;
        class="type">int ind = (class="type">int)u.Scale(pr, class="num">0, class="num">1, class="num">0, popSize - class="num">1);
        cNew = u.PowerDistribution(a [ind].c [c], rangeMin [c], rangeMax [c], hybridsResistance);
      }
      else
      {
        class=class="str">"cmt">// Update the biased first moment estimate
        grad [i].m [c] = beta1 * grad [i].m [c] + (class="num">1.0 - beta1) * grad [i].g [c];
        class=class="str">"cmt">// Update the biased second moment estimate

◍ Adam坐标更新与种群择优的实现细节

这段逻辑把 Adam 优化器的二阶矩偏差校正和参数边界约束直接焊在了进化算法的坐标更新里。注意 v_hat 用的是 1.0 - MathPow(beta2, t) 做偏置修正,t 是迭代计数器,每轮结束 t++,所以早期迭代的步长会被放大、后期趋稳,外汇与贵金属参数寻优时这种机制可能缓解局部早熟。 被注释掉的那行直接原地写回 a[i].c[c],实际跑的是先算 cNew 再交给 u.SeInDiSp 做区间吸附:超出 [rangeMin, rangeMax] 会按 rangeStep 离散化。MT5 上若你的步长设得比品种点差还小,优化结果可能永远落不到合法格点上,回测只是空转。 Revision 函数负责挑出当代最优个体。fB 初始应为负无穷或首代值,循环里用 a[i].f > fB 做最大化判断,命中就记 ind 并拷贝坐标到 cB;若整代没更好则 ind 保持 -1,cB 不更新,相当于保留上一代精英。 末尾那几行背景色标出的 Sorting 调用把种群按适应度重排进 aT,但原数组 a 未被覆盖,后续选择压力怎么施加取决于外层调度。高杠杆贵金属策略跑这套容易过拟合,建议先拿 EURUSD 十五分钟线小种群验证离散步长是否合理。

MQL5 / C++
    grad [i].v [c] = beta2 * grad [i].v [c] + (class="num">1.0 - beta2) * grad [i].g [c] * grad [i].g [c];
    class=class="str">"cmt">// Calculate the adjusted first moment estimate
    class="type">class="kw">double m_hat = grad [i].m [c] / (class="num">1.0 - MathPow(beta1, t));
    class=class="str">"cmt">// Calculate the adjusted estimate of the second moment
    class="type">class="kw">double v_hat = grad [i].v [c] / (class="num">1.0 - MathPow(beta2, t));
    class=class="str">"cmt">// Update coordinates
    class=class="str">"cmt">//a [i].c [c] = a [i].c [c] + (alpha * m_hat / (MathSqrt(v_hat) + epsilon));
    cNew = a [i].c [c] + (alpha * m_hat / (MathSqrt(v_hat) + epsilon));
    }
    class=class="str">"cmt">// Make sure the coordinates stay within the allowed range
    a [i].c [c] = u.SeInDiSp(cNew, rangeMin [c], rangeMax [c], rangeStep [c]);
  }
}
t++; class=class="str">"cmt">// Increase the iteration counter
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_ADAMm::Revision()
{
  class="type">int ind = -class="num">1;      class=class="str">"cmt">// Best individual index
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if (a [i].f > fB) class=class="str">"cmt">// If the current value of the function is greater than the best one
    {
      fB = a [i].f;  class=class="str">"cmt">// Update the best value of the function
      ind = i;       class=class="str">"cmt">// Store the index of the best individual
    }
  }
  if (ind != -class="num">1) ArrayCopy(cB, a [ind].c, class="num">0, class="num">0, WHOLE_ARRAY); class=class="str">"cmt">// Copy the coordinates of the best individual
  class=class="str">"cmt">//----------------------------------------------------------------------------
  S_AO_Agent aT [];
  ArrayResize(aT, popSize);
  u.Sorting(a, aT, popSize);
}
把重装后的优化诊断交给小布
这些群体ADAM的矩估计偏差修正与收敛曲线,小布盯盘的AIGC已内置,打开对应品种页即可看到,你只管判断参数空间是否值得跑。

常见问题

不含有。其核心方程在相同输入与初值下输出完全确定,随机性仅来自外部数据准备与批次采样,理解这点才能安全改造。
目前小布内置的是诊断与可视化模块,可加载你导出的群体适应度序列做矩跟踪;实盘优化回路仍由你本地MQL5智能交易系统执行。
它依赖神经元激活函数的解析梯度更新权重,价格序列常无解析梯度,需转成群体数值梯度估计才能外延到全局优化。
因 m 与 v 初始化为零导致早期估计偏小,除以该因子可抵消初始化偏差,使矩估计在无偏状态下参与步长缩放。