MQL5中使用坐标下降法的弹性网络回归·进阶篇
(2/3)·从正则化原理到纯MQL5代码实现,用坐标下降把过拟合策略拉回正轨
「坐标下降类的接口与矩阵预填实测」
在 MT5 里做正则化回归,先得把坐标下降求解器的对外接口钉死。下面这组声明给出了三个核心方法:GetLambdaThreshold 按显著性水平 alpha 返回 lambda 阈值;SetData 灌入起始索引、样本数、自变量矩阵 xx_matrix 与因变量 yy;Train 与 TrainLambda 分别跑固定 lambda 训练与lambda 路径搜索。 //Set model parameters and raw input data double GetLambdaThreshold(const double alpha) ; bool SetData(const int begin, const int num_observations, double &xx_matrix[], double &yy[]) ; //Training routines void Train(const double alpha, const double lambda, const int maxits, const double convergence_criterion, const bool fast_test, const bool warm_start) ; void TrainLambda(const double alpha, const int maxits, const double convergence_criterion, const bool fast_test, const double maxlambda, const bool print_steps) ; 验证数组布局不用绕弯。直接开 4 行 3 列的扁平矩阵,用双重循环把每列填成 1、2、3,ArrayPrint 后终端会原样吐出 12 个数:1.0 2.0 3.0 重复 4 次。外汇与贵金属行情的高波动会让这类特征缩放敏感,实盘前务必标准化。 int rows=4, columns=3; double our_matrix[]; ArrayResize(our_matrix,rows*columns); /* Creating matrix with columns of 1s,2s,3s */ for(int i = 0; i<rows; i++) for(int j=0; j<columns; j++) our_matrix[i*columns+j]=j+1; ArrayPrint(our_matrix); 构造函数负责把维度与开关落进成员变量。num_predictors、num_observations、num_lambdas_to_trial 任一为负就直接置 m_initialized=false 并报警,避免后续训练在脏状态下跑出无意义系数。 CCoordinateDescent::CCoordinateDescent( const int num_predictors, const int num_observations, const bool use_covariance_updates, const int num_lambdas_to_trial ) { m_nvars = num_predictors ; m_observs = num_observations ; m_covarupdates = use_covariance_updates ; m_nlambda = num_lambdas_to_trial ; m_initialized=true; m_ymean=m_yscale=m_explained=0;
| if(m_nvars<0 | m_observs<0 | m_nlambda<0) |
|---|
{ m_initialized=false; Print("Invalid parameter value, neither num_predictors ,num_observations, nor num_lambdas_to_trial can be negative"); } }
class="type">class="kw">double GetLambdaThreshold(const class="type">class="kw">double alpha) ; class=class="str">"cmt">//Set model parameters and raw input data class="type">bool SetData(const class="type">int begin, const class="type">int num_observations, class="type">class="kw">double &xx_matrix[], class="type">class="kw">double &yy[]) ; class=class="str">"cmt">//Training routines class="type">void Train(const class="type">class="kw">double alpha, const class="type">class="kw">double lambda, const class="type">int maxits, const class="type">class="kw">double convergence_criterion, const class="type">bool fast_test, const class="type">bool warm_start) ; class="type">void TrainLambda(const class="type">class="kw">double alpha, const class="type">int maxits, const class="type">class="kw">double convergence_criterion, const class="type">bool fast_test, const class="type">class="kw">double maxlambda, const class="type">bool print_steps) ; class="type">int rows=class="num">4, columns=class="num">3; class="type">class="kw">double our_matrix[]; ArrayResize(our_matrix,rows*columns); class=class="str">"cmt">/* Creating matrix with columns of 1s,2s,3s */ for(class="type">int i = class="num">0; i<rows; i++) for(class="type">int j=class="num">0; j<columns; j++) our_matrix[i*columns+j]=j+class="num">1; ArrayPrint(our_matrix); CCoordinateDescent::CCoordinateDescent( const class="type">int num_predictors, const class="type">int num_observations, const class="type">bool use_covariance_updates, const class="type">int num_lambdas_to_trial ) { m_nvars = num_predictors ; m_observs = num_observations ; m_covarupdates = use_covariance_updates ; m_nlambda = num_lambdas_to_trial ; m_initialized=true; m_ymean=m_yscale=m_explained=class="num">0; if(m_nvars<class="num">0 || m_observs<class="num">0 || m_nlambda<class="num">0) { m_initialized=false; Print("Invalid parameter value, neither num_predictors ,num_observations, nor num_lambdas_to_trial can be negative"); }
内存分配失败如何拖垮回归训练
坐标下降回归类在初始化阶段会一次性为多个矩阵申请内存:观测数乘变量数的 X 矩阵、Y 向量、均值与缩放向量、系数 Beta、残差等。只要任意一个 ArrayResize 返回值小于预期长度,m_initialized 就被置为 false,后续所有计算直接跳过。 如果开启了协方差增量更新(m_covarupdates),还会额外分配 m_nvars*m_nvars 的内积矩阵与长度为 m_nvars 的 Y 内积向量;若 λ 路径点数 m_nlambda 大于 0,则需再分配 m_nlambda*m_nvars 的 λ 系数矩阵与长度 m_nlambda 的 λ 序列。任何一块不足都会让初始化失败并打印 GetLastError 代码。 SetData 是真正灌入行情数据的入口。它先校验 m_initialized,再检查 begin 与 num_observations 不能为负;接着要求外部传入的 xx_matrix 尺寸至少达到 m_observs*m_nvars、yy 长度至少 m_observs,否则报“数据不足”并返回 false。 在 MT5 里复现这套逻辑时,建议先打印 m_observs 与 m_nvars 的实际值,再对照 ArrayResize 的返回值。外汇与贵金属市场高杠杆、跳空频繁,样本窗口设得过长容易因内存或边界回绕出错,调小观测数往往能立刻暴露分配隐患。
class="kw">return; } if(ArrayResize(m_x_matrix,m_observs*m_nvars)<m_observs*m_nvars || ArrayResize(m_y,m_observs)<m_observs || ArrayResize(m_xmeans,m_nvars)<m_nvars || ArrayResize(m_xscales,m_nvars)<m_nvars || ArrayResize(m_beta,m_nvars)<m_nvars || ArrayResize(m_resid,m_observs)<m_observs) m_initialized=false; class=class="str">"cmt">//---conditional allocation if(m_covarupdates) { if(ArrayResize(m_xinner_matrix,m_nvars*m_nvars)<m_nvars*m_nvars|| ArrayResize(m_yinner,m_nvars)<m_nvars) m_initialized=false; } class=class="str">"cmt">//--- if(m_nlambda>class="num">0) { if(ArrayResize(m_lambdabeta_matrix,m_nlambda*m_nvars)<m_nlambda*m_nvars || ArrayResize(m_lambdas,m_nlambda)<m_nlambda) m_initialized=false; } class=class="str">"cmt">//---class="kw">return immediately if any error if(!m_initialized) Print("Memory allocation error ", GetLastError()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Get and standardize the data | class=class="str">"cmt">//| Also compute inner products if covar_update | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CCoordinateDescent::SetData( const class="type">int begin, class=class="str">"cmt">// Starting index in full database for getting m_observs of training set const class="type">int num_observations,class=class="str">"cmt">// Number of cases in full database(we wrap back to the start if needed) class="type">class="kw">double &xx_matrix[], class=class="str">"cmt">// Full database(num_observations rows, m_nvars columns) class="type">class="kw">double &yy[] class=class="str">"cmt">// Predicted variable vector, num_observations class="type">long ) { if(!m_initialized) class="kw">return false; class=class="str">"cmt">// parameter checks if(begin<class="num">0 || num_observations<class="num">0) { Print("Invalid parameter value: neither begin nor num_observations can be negative"); class="kw">return false; } class=class="str">"cmt">//--- invalid a if(ArraySize(xx_matrix)<(m_observs*m_nvars) || ArraySize(yy)<m_observs) { Print("Insufficient data supplied relative to object specification"); class="kw">return false; } class=class="str">"cmt">//--- class="type">int icase, ivar, jvar, k,xptr; class="type">class="kw">double sum, xm, xs, diff; class=class="str">"cmt">/* Standardize X */ for(ivar=class="num">0 ; ivar<m_nvars ; ivar++) {
◍ 标准化与协方差矩阵的预处理细节
做多元回归前,先把自变量和因变量都标准化,否则量纲不同的品种(比如黄金点数和美元指数)会互相压制权重。下面这段逻辑对第 ivar 个自变量先求均值 xm,再除以样本数 m_observs 得到 m_xmeans[ivar],随后用 1.e-60 兜底防止后续除以零——外汇与贵金属序列里出现常数段并不罕见,这行不能省。 方差 xs 从 1.e-60 起累加 (x-xm)^2,开平方后存为 m_xscales[ivar],再用 (x-xm)/xs 把原矩阵写回 m_x_matrix。因变量 Y 走完全一样的流程,m_yscale 同样以 1.e-60 垫底,标准化后 m_y 的均值理论为 0、标准差理论为 1。 若 m_covarupdates 为真,代码会预存内积:m_yinner[ivar] 是 Xi 与 Y 的协方差估计(除以 m_observs),而 m_xinner_matrix 存 XiXj。对角线直接置 1.0,因为 X 已标准化;下三角 jvar<ivar 时利用对称性直接复制,避免重复计算。 在 MT5 里跑这套,重点看 m_xscales 有没有卡在 1.e-60——若多个变量都触底,说明该窗口内这些序列近乎常数,回归系数会失真,贵金属窄幅震荡时段尤其要警惕这种高风险情形。
xm = class="num">0.0 ; for(icase=class="num">0 ; icase<m_observs ; icase++) { k = (icase + begin) % num_observations ; xm += xx_matrix[k*m_nvars+ivar] ; } xm /= m_observs ; m_xmeans[ivar] = xm ; xs = class="num">1.e-60 ; class=class="str">"cmt">// Prevent division by zero later for(icase=class="num">0 ; icase<m_observs ; icase++) { k = (icase + begin) % num_observations ; diff = xx_matrix[k*m_nvars+ivar] - xm ; xs += diff * diff ; } xs = sqrt(xs / m_observs) ; m_xscales[ivar] = xs ; for(icase=class="num">0 ; icase<m_observs ; icase++) { k = (icase + begin) % num_observations ; m_x_matrix[icase*m_nvars+ivar] = (xx_matrix[k*m_nvars+ivar] - xm) / xs ; } } class=class="str">"cmt">/* Standardize Y */ m_ymean = class="num">0.0 ; for(icase=class="num">0 ; icase<m_observs ; icase++) { k = (icase + begin) % num_observations ; m_ymean += yy[k] ; } m_ymean /= m_observs ; m_yscale = class="num">1.e-60 ; class=class="str">"cmt">// Prevent division by zero later for(icase=class="num">0 ; icase<m_observs ; icase++) { k = (icase + begin) % num_observations ; diff = yy[k] - m_ymean ; m_yscale += diff * diff ; } m_yscale = sqrt(m_yscale / m_observs) ; for(icase=class="num">0 ; icase<m_observs ; icase++) { k = (icase + begin) % num_observations ; m_y[icase] = (yy[k] - m_ymean) / m_yscale ; } class=class="str">"cmt">/* If user requests covariance updates, compute required inner products We store the full m_xinner_matrix matrix for faster addressing later, even though it is symmetric. We handle both unweighted and weighted cases here. */ if(m_covarupdates) { for(ivar=class="num">0 ; ivar<m_nvars ; ivar++) { xptr = ivar ; class=class="str">"cmt">// Do XiY sum = class="num">0.0 ; for(icase=class="num">0 ; icase<m_observs ; icase++) sum += m_x_matrix[xptr+icase*m_nvars] * m_y[icase] ; m_yinner[ivar] = sum / m_observs ; class=class="str">"cmt">// Do XiXj for(jvar=class="num">0 ; jvar<m_nvars ; jvar++) { if(jvar == ivar) m_xinner_matrix[ivar*m_nvars+jvar] = class="num">1.0 ; class=class="str">"cmt">// Recall that X is standardized else if(jvar < ivar) class=class="str">"cmt">// Matrix is symmetric, so just copy
「坐标下降训练里的矩阵对称与阈值初始化」
这段逻辑先处理内积矩阵的对称性:当变量下标 ivar 与 jvar 相等或已算过转置位置时,直接复制已有值,避免重复计算。 若未对称填充,则对 m_observs 条样本做点积累加,再除以样本数得到均值内积,写回 m_xinner_matrix[ivar*m_nvars+jvar]。这一步把原始特征矩阵压缩成协方差类结构,供后续回归使用。 Train() 入口先做参数护栏:alpha 必须落在 [0,1],lambda 与 maxits 必须为正数,否则 Print 报错直接返回。实际调参时 alpha 取 0 会让 lambda 递减路径出问题,建议至少留 1e-3 以上。 初始化段把软阈值算子门槛设为 S_threshold = alpha * lambda,并令 prior_crit = 1.0e60 作为首轮收敛判据的占位极大值。开 MT5 把这段抄进 EA,改 alpha 从 0.1 试到 0.9,能直接观察门槛对系数稀疏化的影响。外汇与贵金属杠杆高,回测结论仅代表历史样本倾向,实盘可能失效。
m_xinner_matrix[ivar*m_nvars+jvar] = m_xinner_matrix[jvar*m_nvars+ivar] ; else { sum = class="num">0.0 ; for(icase=class="num">0 ; icase<m_observs ; icase++) sum += m_x_matrix[xptr+icase*m_nvars] * m_x_matrix[icase*m_nvars+jvar] ; m_xinner_matrix[ivar*m_nvars+jvar] = sum / m_observs ; } } } class=class="str">"cmt">// For ivar } class=class="str">"cmt">//--- class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Core training routine | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CCoordinateDescent::Train( const class="type">class="kw">double alpha, class=class="str">"cmt">// User-specified alpha, (class="num">0,class="num">1) (class="num">0 problematic for descending lambda) const class="type">class="kw">double lambda, class=class="str">"cmt">// Can be user-specified, but usually from TrainLambda() const class="type">int maxits, class=class="str">"cmt">// Maximum iterations, for safety only const class="type">class="kw">double convergence_criterion, class=class="str">"cmt">// Convergence criterion, typically class="num">1.e-5 or so const class="type">bool fast_test, class=class="str">"cmt">// Base convergence on max m_beta change vs m_explained variance? const class="type">bool warm_start class=class="str">"cmt">// Start from existing m_beta, rather than zero? ) { if(!m_initialized) class="kw">return; if(alpha<class="num">0 || alpha>class="num">1) { Print("Invalid parameter value: Legal values for alpha are between class="num">0 and class="num">1 inclusive"); class="kw">return; } if(lambda<class="num">0) { Print("Invalid parameter value: lambda accepts only positive values"); class="kw">return; } if(maxits<=class="num">0) { Print("Invalid parameter value: maxist accepts only non zero positive values"); class="kw">return; } class="type">int i, iter, icase, ivar, kvar, do_active_only, active_set_changed, converged,xptr ; class="type">class="kw">double residual_sum, S_threshold, argument, new_beta, correction, update_factor ; class="type">class="kw">double sum, explained_variance, crit, prior_crit, penalty, max_change, Xss, YmeanSquare ; class=class="str">"cmt">/* Initialize */ S_threshold = alpha * lambda ; class=class="str">"cmt">// Threshold for the soft-thresholding class="kw">operator S() do_active_only = class="num">0 ; class=class="str">"cmt">// Begin with a complete pass prior_crit = class="num">1.0e60 ; class=class="str">"cmt">// For convergence test
弹性网回归的预热与迭代初始化
在 MT5 里跑弹性网(Elastic Net)类回归模型时,warm_start 开关决定了 beta 系数的起点。若设为 true 且未启用协方差更新(m_covarupdates 为 false),代码会按观测数 m_observs 重算残差:用当前 beta 与自变量矩阵 m_x_matrix 做内积,再用 m_y 减掉得到 m_resid,这一步在样本量小于变量数时尤其不能省。 若 warm_start 为 false,所有 m_beta 直接清零,初始残差就是因变量 m_y 本身——相当于从零开始下降。YmeanSquare 被固定为 1.0,仅用于后续给用户展示解释方差,不参与训练更新。 外层 for 循环以 maxits 为上限反复迭代,每次先清空 active_set_changed 与 max_change 标记。变量遍历时若 do_active_only 且 beta 已为 0.0 则跳过;update_factor 由标准化常量 Xss(此处为 1)加 lambda*(1.0-alpha) 构成。启用协方差更新时用 m_xinner_matrix 算 residual_sum,未启用则走逐观测的慢速定义式,适合 m_observs < m_nvars 的场景。 外汇与贵金属行情具有高杠杆、高波动风险,上述初始化逻辑仅影响模型拟合起点,不预示任何价格方向;开 MT5 把这段接进你的 EA 回测,对比 warm_start 开关节省的时间差异即可验证。
if(warm_start) class=class="str">"cmt">// Pick up with current betas? { if(! m_covarupdates) class=class="str">"cmt">// If not class="kw">using covariance updates, must recompute residuals { for(icase=class="num">0 ; icase<m_observs ; icase++) { xptr = icase * m_nvars ; sum = class="num">0.0 ; for(ivar=class="num">0 ; ivar<m_nvars ; ivar++) sum += m_beta[ivar] * m_x_matrix[xptr+ivar] ; m_resid[icase] = m_y[icase] - sum ; } } } else class=class="str">"cmt">// Not warm start, so initial betas are all zero { for(i=class="num">0 ; i<m_nvars ; i++) m_beta[i] = class="num">0.0 ; for(i=class="num">0 ; i<m_observs ; i++) class=class="str">"cmt">// Initial residuals are just the Y variable m_resid[i] = m_y[i] ; } class=class="str">"cmt">// YmeanSquare will remain fixed throughout training. class=class="str">"cmt">// Its only use is for computing m_explained variance for the user&class="macro">#x27;s edification. YmeanSquare = class="num">1.0 ; class=class="str">"cmt">/* Outmost loop iterates until converged or user&class="macro">#x27;s maxits limit hit */ for(iter=class="num">0 ; iter<maxits ; iter++) { class=class="str">"cmt">/* Pass through variables */ active_set_changed = class="num">0 ; class=class="str">"cmt">// Did any betas go to/from class="num">0.0? max_change = class="num">0.0 ; class=class="str">"cmt">// For fast convergence test for(ivar=class="num">0 ; ivar<m_nvars ; ivar++) class=class="str">"cmt">// Descend on this m_beta { if(do_active_only && m_beta[ivar] == class="num">0.0) class="kw">continue ; Xss = class="num">1 ; class=class="str">"cmt">// X was standardized update_factor = Xss + lambda * (class="num">1.0 - alpha) ; if(m_covarupdates) class=class="str">"cmt">// Any sensible user will specify this unless m_observs < m_nvars { sum = class="num">0.0 ; for(kvar=class="num">0 ; kvar<m_nvars ; kvar++) sum += m_xinner_matrix[ivar*m_nvars+kvar] * m_beta[kvar] ; residual_sum = m_yinner[ivar] - sum ; argument = residual_sum + Xss * m_beta[ivar] ; class=class="str">"cmt">// Argument to S() [MY FORMULA] } else class=class="str">"cmt">// Use slow definitional formula(okay if m_observs < m_nvars) { residual_sum = class="num">0.0 ; xptr = ivar ; class=class="str">"cmt">// Point to column of this variable
◍ 正文
<span class="keyword">for</span>(icase=<span class="number">0</span> ; icase<m_observs ; icase++) residual_sum += m_x_matrix[xptr+icase*m_nvars] * m_resid[icase] ; <span class="comment">// X_ij * RESID_i</span> residual_sum /= m_observs ; argument = residual_sum + m_beta[ivar] ; <span class="comment">// Argument to S() ; (Eq 8)</span> } <span class="comment">// Apply the soft-thresholding operator S()</span> <span class="keyword">if</span>(argument > <span class="number">0.0</span> && S_threshold < argument) new_beta = (argument - S_threshold) / update_factor ; <span class="keyword">else</span> <span class="keyword">if</span>(argument < <span class="number">0.0</span