群体自适应矩估计(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。
Δ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 转换,其余原样赋值。高风险提示:贵金属与外汇杠杆品种参数敏感,任何优化结果都只是历史倾向,不等于未来分布。
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,这一数值会直接影响后代多样性。 外汇与贵金属市场波动剧烈、杠杆风险高,此类优化仅用于离线参数搜索,实盘前须用历史数据交叉验证。
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 的实现。外汇与贵金属参数优化属高风险,回测吻合不代表实盘概率占优。
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 十五分钟线小种群验证离散步长是否合理。
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); }