数据科学与机器学习(第 10 部分):岭回归·综合运用
📐

数据科学与机器学习(第 10 部分):岭回归·综合运用

(3/3)·从惩罚值选择到模型落地,把前两篇的线性回归与正则化拼成完整拼图

含代码示例偏理论 第 3/3 篇

许多交易者在多指标共线时直接套最小二乘,训练集残差归零便以为模型到位,放到样本外立刻崩坏。岭回归用一点可容忍偏差换方差压降,但惩罚值乱设反而两头不靠。本篇把这套机制收口到能写进 EA 的实际操作。

「岭回归里 lambda 该怎么搜」

找 lambda 不能拍脑袋,留一交叉验证(LOOCV)是实打实可用的路子:每次从数据集抠掉一个样本,拿剩下 n-1 条训练模型,再用抠掉的那个做测试,把所有样本的误差累起来,损失函数最小的那个 lambda 倾向就是当前数据下的最优惩罚值。 先粗搜容易误判。原文里从 1 到 10 整数步长跑了一遍,最小损失对应的 lambda 偏小,说明最佳值可能落在 0 附近的小数区,而不是整数档。 于是把循环改成 0 到 10、步长 0.01,总共 1000 次迭代。实测在 MT5 上跑完约耗 5 分钟,最终拿到 lambda = 0.09 作为岭回归的最佳惩罚值,绘图也显示损失曲线在 0.09 附近触底。 别把整数搜索当终点 很多新手直接 1、2、3 往上试,结果模型欠惩罚、系数膨胀。外汇与贵金属波动高噪声,lambda 差 0.1 可能就让样本外预测偏移明显,细粒度搜索不是浪费时间。 下面这段是 LOOCV 在 MQL5 里的核心实现,重点看双层循环怎么把第 j 行踢出训练集又单独送进预测: #include <MALE5\cross_validation.mqh> CCrossValidation *cross_validation; double CCrossValidation::LeaveOneOut(double init, double step, double finale) { matrix XMatrix; vector yVector; // 从总矩阵里拆出特征矩阵 X 和标签向量 y matrix_utils.XandYSplitMatrices(Matrix,XMatrix,yVector); matrix train = Matrix; vector test = {}; // 初始化训练集副本和空测试行 int size = int(finale/step); // 按起止和步长算总迭代次数,如 0~10/0.01=1000 vector validation_output(ulong(size)); vector lambda_vector(ulong(size)); // 预留存每次 lambda 的验证结果和 lambda 序列 vector forecast(n); vector actual = yVector; // forecast 存预测值,actual 是真实标签 double lambda = init; // lambda 从初始值开始累加 for (int i=0; i<size; i++) { lambda += step; // 外层:遍历每一个候选 lambda for (ulong j=0; j<n; j++) { train.Copy(Matrix); // 每次内层循环重置训练集为全量 ZeroMemory(test); // 清空测试行 test = XMatrix.Row(j); // 取第 j 行作为被留出的样本 matrix_utils.MatrixRemoveRow(train,j); // 从训练集删掉第 j 行,实现留一 vector coeff = {}; double acc =0; // coeff 放系数,acc 暂未用 switch(selected_model) { case RIDGE_REGRESSION: ridge_regression = new CRidgeregression(train); // 用剔除后的训练集建岭回归对象 coeff = ridge_regression.L2Norm(lambda); //ridge regression // 按当前 lambda 算 L2 惩罚下的系数 Linear_reg = new CLinearRegression(train,coeff); // 拿系数建线性回归预测器 forecast[j] = Linear_reg.LRModelPred(test); // 对留出样本做预测并写回 delete (Linear_reg); delete (ridge_regression); // 释放对象,防止 MT5 内存泄漏

MQL5 / C++
class="macro">#include <MALE5\cross_validation.mqh>
CCrossValidation *cross_validation;
class="type">class="kw">double CCrossValidation::LeaveOneOut(class="type">class="kw">double init, class="type">class="kw">double step, class="type">class="kw">double finale)
{
   matrix XMatrix;
   vector yVector;
   
   matrix_utils.XandYSplitMatrices(Matrix,XMatrix,yVector);
   matrix train = Matrix; vector test = {};
   
   class="type">int size = class="type">int(finale/step);
   vector validation_output(class="type">class="kw">ulong(size));
   vector lambda_vector(class="type">class="kw">ulong(size));
   
   vector forecast(n);
   vector actual = yVector;
   
   class="type">class="kw">double lambda = init;
   
    for (class="type">int i=class="num">0; i<size; i++)
     {
        lambda += step;
        
         for (class="type">class="kw">ulong j=class="num">0; j<n; j++)
         {               
            train.Copy(Matrix);
            ZeroMemory(test);
            
            test = XMatrix.Row(j);
            
            matrix_utils.MatrixRemoveRow(train,j);
            
            vector coeff = {};
            class="type">class="kw">double acc =class="num">0;
            
             class="kw">switch(selected_model)
             {
                case  RIDGE_REGRESSION:
                  ridge_regression = new CRidgeregression(train);
                  coeff = ridge_regression.L2Norm(lambda); class=class="str">"cmt">//ridge regression
                  
                  Linear_reg = new CLinearRegression(train,coeff);  
                  forecast[j] =  Linear_reg.LRModelPred(test);  
                  
                  class=class="str">"cmt">//---
                  
                  class="kw">delete (Linear_reg);
                  class="kw">delete (ridge_regression);

留一法交叉验证里挑正则项

这段逻辑跑的是 Ridge 回归的留一交叉验证(LOOCV):对每个候选 lambda 算一遍 MSE,最后返回 validation_output 里 ArgMin 对应的那个 lambda。 代码里 validation_output[i] = forecast.Loss(actual,LOSS_MSE)/double(n) 把误差做了样本数归一,lambda_vector[i] 同步记下当前正则强度,DEBUG 模式下用 printf 打出 %.5f LOOCV mse %.5f 方便肉眼比对。 #ifdef DEBUG_MODE 那段会把 validation_output 和 lambda_vector 两列写进 LOOCV.csv,列头写死为 Validation output 和 lambda,路径拼的是 EnumToString(selected_model) 子目录,复现时别漏了模型枚举名。 OnInit 里直接 ReadCsv("NASDAQ_DATA.csv") 喂矩阵,LeaveOneOut(0,1,10) 表示从 0 到 10 步长 1 扫 lambda。日志实测:EURUSD H1 下 lambda 取 1~6 时 LOOCV mse 均停在 0.00020,说明该区间过拟合差异极小,选哪个都倾向差不多。 外汇和贵金属杠杆高、跳空频繁,这种 mse 只是样本内交叉验证结果,实盘泛化能力可能打折,参数定完务必上 MT5 用自己品种重跑一遍。

MQL5 / C++
            class="kw">break;
          }
        }
        
        validation_output[i] = forecast.Loss(actual,LOSS_MSE)/class="type">class="kw">double(n);
        
        lambda_vector[i] = lambda;
        
        class="macro">#ifdef DEBUG_MODE
          printf("%.5f LOOCV mse %.5f",lambda_vector[i],validation_output[i]);
        class="macro">#endif
      }
class=class="str">"cmt">//---
      class="macro">#ifdef DEBUG_MODE
        matrix store_matrix(size,class="num">2);
        
        store_matrix.Col(validation_output,class="num">0);
        store_matrix.Col(lambda_vector,class="num">1);
        
        class="type">class="kw">string name = EnumToString(selected_model)+"\\LOOCV.csv";
        
        class="type">class="kw">string header[class="num">2] = {"Validation output","lambda"};
        matrix_utils.WriteCsv(name,store_matrix,header);
      class="macro">#endif
      
      class="kw">return(lambda_vector[validation_output.ArgMin()]);
}
class="type">int OnInit()
  {  
    matrix Matrix = matrix_utils.ReadCsv("NASDAQ_DATA.csv",",");  
    
    ridge_reg = new CRidgeregression(Matrix);
    
    cross_validation = new CCrossValidation(Matrix,RIDGE_REGRESSION);
    
    class="type">class="kw">double best_lambda = cross_validation.LeaveOneOut(class="num">0,class="num">1,class="num">10);
    
    Print("Best lambda ",best_lambda);
  }

◍ EURUSD 上岭回归的交叉验证实况

在 MT5 策略测试器里跑 ridge_test(EURUSD,H1),lambda 从 7 到 10 逐档扫描,留一交叉验证(LOOCV)的均方误差稳定在 0.00021,说明这一区间正则强度对欧美的小时线拟合几乎没有差别。 日志末行给出 Best lambda 1.0,但注意上面 7~10 的 mse 完全相同,真实最优可能在更细的网格里,直接取 1.0 只是退出条件触发点,不是显著最优点。 岭回归在这套数据上的好处是防止过拟合、压低模型复杂度,对多变量线性回归友好且不需要无偏估计,所有预测变量都留在最终模型里。代价是无法做特征选择,系数只被收缩而不归零,本质是用偏差换方差。 外汇和贵金属属高风险品种,上述 mse 仅为样本内交叉验证值,实盘泛化能力可能下降,开 MT5 把 lambda 网格调到 0.1 步长重跑一次就能看清曲线。

MQL5 / C++
CS      class="num">0    class="num">10:class="num">12:class="num">51.987   ridge_test(EURUSD,H1)   class="num">7.00000 LOOCV mse class="num">0.00021
CS      class="num">0    class="num">10:class="num">12:class="num">52.090   ridge_test(EURUSD,H1)   class="num">8.00000 LOOCV mse class="num">0.00021
CS      class="num">0    class="num">10:class="num">12:class="num">52.201   ridge_test(EURUSD,H1)   class="num">9.00000 LOOCV mse class="num">0.00021
CS      class="num">0    class="num">10:class="num">12:class="num">52.317   ridge_test(EURUSD,H1)   class="num">10.00000 LOOCV mse class="num">0.00021
CS      class="num">0    class="num">10:class="num">12:class="num">52.319   ridge_test(EURUSD,H1)   Best lambda class="num">1.0

「多变量下的岭回归仍要手工剔噪」

在输入维度变多时,岭回归能压住过拟合,但它不会替你判断哪些因子纯属噪声。以文中所用 NASDAQ_DATA 为例,RSI 列与目标变量大概率无关,若直接喂进模型,只会稀释有效权重,这种列仍得人工删掉。 作者已把整套实现铺在 GitHub 仓库,文件分工很直接:cross_validation.mqh 做 LOOCV 类验证,Linear regression.mqh 跑最小二乘,Ridge Regression.mqh 是岭回归本体,ridge_test.mq5 一键测全部逻辑,prepare_dataset.mq5 则把振荡指标落盘成 Oscillators.csv。 外汇与贵金属市场高杠杆、高波动,这类 ML 辅助建模只作概率参考,实盘前务必在 MT5 策略测试器用 NASDAQ_DATA.csv 复跑一遍 ridge_test,确认你的编译环境与文中一致再谈调参。

把共线诊断交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到多变量相关性热区,你只需决定正则强度。

常见问题

lambda 趋近零时模型退化为普通最小二乘,高共线下方差爆炸;lambda 过大则偏差主导,信号被过度抹平,样本外同样失真。
可以,小布在品种页用 AIGC 算出特征间相关系数并高亮风险区,省去你手跑矩阵的重复劳动,你专注调参决策。
lasso 的 L1 惩罚把不重要系数压到精确零实现选择,岭回归的 L2 只收缩不归零,所有原始因子都保留但权重被均匀收紧。
这类高杠杆市场噪声大,过度拟合的训练模型概率上更易在实盘失效,任何正则结论都只是提升稳健性而非保证收益。