群体自适应矩估计(ADAM)优化算法(基础篇)
◍ MT5 里用 ADAM 优化参数的实测起点
群体自适应矩估计(ADAM)是一类带一阶、二阶矩自适应的优化算法,在 MT5 中可用于策略参数的批量寻优,相比固定步长网格更省计算量。
- 年 8 月 21 日发布的示例在 MT5 环境跑通,公开页面显示浏览量 637、评论 0,说明该实现已可复现但社区验证尚少。
开 MT5 后可直接载入该 ADAM 示例,先确认其算法实现与测试结果两栏是否随版本更新;外汇与贵金属品种波动剧烈,参数优化结果仅代表历史样本,实盘存在较高风险,需以概率视角看待。
「把 ADAM 从神经网络里拽出来」
- 年 D. P. Kingma 和 J. Ba 提出的 ADAM(自适应矩估计),原本是给神经网络权重做梯度优化的:它吃一阶和二阶矩的自适应估计,内存占用极低,还不依赖梯度的对角缩放,所以在大参数、大样本下比 AdaGrad、RMSProp 更稳更快。
原版 ADAM 有个硬伤——只认解析梯度。也就是说你得能写出目标函数对参数的导数表达式,否则它压根跑不起来,这把它的用场锁死在神经网络里。 我们这次改的方向,是把它变成能处理数值梯度的群体优化器:不要求你给得出解析导数,靠数值差分也能迭代。这样一来,它就可能伸到全局优化、多目标优化这些原本够不着的领域,外汇与贵金属参数寻优里的高风险非线性拟合也多了一个可试的工具。 改造后的核心收益是保留原版少内存、易调超参数的特性,同时把适用面从「有解析梯度」扩到「只有目标值也能搜」。开 MT5 用自定义优化器跑一次 EA 参数平铺,就能验证它和内置算法的收敛差异。
把确定性ADAM改造成带随机性的种群优化器
ADAM本身不是随机算法。它的所有更新方程——一阶矩 mₜ = β₁·mₜ₋₁ + (1-β₁)·gₜ、二阶矩 vₜ = β₂·vₜ₋₁ + (1-β₂)·gₜ²,以及偏差修正后的 m̂ₜ、v̂ₜ 和参数更新 θₜ = θₜ₋₁ - α·m̂ₜ/(√v̂ₜ+ε)——在同样输入和初始条件下永远给出同一结果。真正带来随机性的是数据准备阶段,而非算法内核。 在MT5里把ADAM做成独立类 C_AO_ADAM,只在种群初始坐标上撒随机种子,内部仍跑原版方程。测试函数在 10000 次运行下给出硬数据:Hilly's 地形种群 500 时结果 0.2539,Megacity's 地形种群 500 时结果 0.0950,总分 1.99941(22.22%)。这个成绩一般,但暴露了纯梯度种群互不通信、只靠初始散布找最优的弱点。 改进思路是引入混合个体:按适应度排序后,在种群尾部用高适应度个体的元素按幂律分布重组新解。C_AO_ADAMm 类新增 hybridsPercentage 与 hybridsResistance 两个参数。实测 hybridsPercentage=0.5、hybridsResistance=10 时效果最好;设为1等于关掉ADAM逻辑,设为0则无组合特性。hybridsResistance 调太低会满范围乱跳,高于20则变异趋零、只是搬运父代最优。 外汇与贵金属市场属高风险环境,这类优化器仅用于策略参数搜索的离线实验,任何回测分数都不构成实盘盈利保证,上MT5验证前请先用脚本跑测试函数确认数值微分步长。
class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">// Structure for storing gradients and moments class="kw">struct S_Gradients { class="type">class="kw">double g []; class=class="str">"cmt">// Gradients class="type">class="kw">double m []; class=class="str">"cmt">// Vectors of the first moment class="type">class="kw">double v []; class=class="str">"cmt">// Vectors of the second moment class=class="str">"cmt">// Method for initializing gradients class="type">void Init(class="type">int coords) { ArrayResize(g, coords); ArrayResize(m, coords); ArrayResize(v, coords);
◍ ADAM优化器的参数骨架
在MT5里把自适应矩估计做成可调用类时,构造阶段就要把超参数钉死。下面这段把种群规模设为50,学习率压到0.001,一阶矩衰减beta1取0.9、二阶矩beta2取0.999,epsilon给1e-8防除零——这几个数直接决定后续权重的更新节奏。 参数不是写死就完了,类里用params数组挂了5个字段,SetParams()再从数组回写成员变量,这样面板调参和代码内调参走同一通道。 Init()只声明了接口形态:接收最小/最大搜索范围、步长和epoch数,具体初始化逻辑在别处落地。外汇与贵金属波动剧烈,这类优化器跑参数搜索时过拟合概率偏高,建议先用历史片段验证再上实盘。
ArrayInitialize(g, class="num">0.0); class=class="str">"cmt">// Initialize gradients to zeros ArrayInitialize(m, class="num">0.0); class=class="str">"cmt">// Initialize the first moment with zeros ArrayInitialize(v, class="num">0.0); class=class="str">"cmt">// Initialize the second moment with zeros }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class C_AO_ADAM : class="kw">public C_AO { class="kw">public: class=class="str">"cmt">//-------------------------------------------------------------------- class=class="str">"cmt">// Class destructor ~C_AO_ADAM() { } class=class="str">"cmt">// Class constructor C_AO_ADAM() { ao_name = "ADAM"; class=class="str">"cmt">// Algorithm name ao_desc = "Adaptive Moment Estimation"; class=class="str">"cmt">// Algorithm description ao_link = "[MQL5官方文档] class=class="str">"cmt">// Link to the article popSize = class="num">50; class=class="str">"cmt">// Population size 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">1e-8; class=class="str">"cmt">// Small constant to prevent division by zero class=class="str">"cmt">// Initialize the parameter array ArrayResize(params, class="num">5); params [class="num">0].name = "popSize"; params [class="num">0].val = popSize; params [class="num">1].name = "alpha"; params [class="num">1].val = alpha; params [class="num">2].name = "beta1"; params [class="num">2].val = beta1; params [class="num">3].name = "beta2"; params [class="num">3].val = beta2; params [class="num">4].name = "epsilon"; params [class="num">4].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 alpha = params [class="num">1].val; class=class="str">"cmt">// Set the learning ratio beta1 = params [class="num">2].val; class=class="str">"cmt">// Set beta1 beta2 = params [class="num">3].val; class=class="str">"cmt">// Set beta2 epsilon = params [class="num">4].val; class=class="str">"cmt">// Set epsilon } class=class="str">"cmt">// Initialization method 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
「ADAM优化器的初始化与早期步处理」
把 ADAM 思路搬进 EA 的群体优化类,先得把超参和梯度容器备齐。类里挂了 alpha、beta1、beta2、epsilon 四个双精度量,分别对应学习率、一阶矩衰减、二阶矩衰减和数值稳定常数;grad 数组按种群规模 popSize 动态开,存每个个体的梯度快照。 Init 方法接收参数区间与步长数组,顺带一个默认 0 的 epochsP。内部先跑 StandardInit 做常规校验,再把 step 归零、t 置 1,用 ArrayResize 把 grad 拉到 popSize 长度,并对每个 i 调 grad[i].Init(coords) 完成单个体梯度结构初始化——这一步漏了,后面 Moving 取梯度会越界。 Moving 前两步是冷启动:当 step<2 时,只把每个个体的旧函数值 fP 和旧坐标 cP 存下来,新坐标用 u.RNDfromCI 在区间内随机生成,再经 u.SeInDiSp 吸附到合法离散栅格上,随后 step++ 直接 return。也就是说,算法前两次迭代不跑 ADAM 更新,纯粹在铺初始种群。外汇与贵金属波动率高,这类随机播种对初始区间敏感,实盘前建议在 MT5 策略测试器里把 rangeMin/Max 收窄做几轮验证。
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 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=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">bool C_AO_ADAM::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 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_ADAM::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 for (class="type">int i = class="num">0; i < popSize; i++) {