数据科学与机器学习(第 10 部分):岭回归·进阶篇
📉

数据科学与机器学习(第 10 部分):岭回归·进阶篇

(2/3)·最小二乘在共线特征下方差爆炸,岭回归如何以轻微偏差压住过拟合

偏理论进阶 第 2/3 篇

不少交易者在多因子建模时直接套用最小二乘,特征一共线就拿到零残差训练线,实盘一测误差飙升。这种完美拟合恰是方差失控的信号,不是模型厉害而是它记住了噪声。

◍ 低相关指标堆出来的回归为什么还会虚高

做 EURUSD 的 H1 回归实验时,我把随机指标、RSI、成交量、多空推力这些振荡和量能类读数全塞进自变量池。先看相关矩阵:EURUSD 列跟所有指标的相关性都低于 20%,其中随机指标约 14%、RSI 约 15%,量能和推力类甚至为负,单看就知道这群特征不适合直接线性拟合。 但线性回归有个怪现象:自变量越多,训练集 R2 必然往上爬。只用随机指标时精确度 1.2%,加 RSI 到 1.8%,再叠成交量和多空推力全部进场也就 4.9%。指标间彼此相关性有高有低(随机与 Bulls 达 0.729),可跟价格的耦合实在太弱,所以每加一个变量只是微涨。 如果换一批相关性 30%~40% 的自变量,盲目堆量可能让训练准确率冲到 90%,但方差同步放大,模型在新数据上大概率崩坏——这就是过拟合。岭回归和 lasso 的价值正在于此:靠 L1/L2 惩罚项主动引入偏差,把方差压下来,外推时才不至于露怯。外汇和贵金属杠杆高、噪声大,这种过拟合陷阱比股票更致命,动手前先在 MT5 跑一遍相关矩阵再决定要不要正则化。 下面这段是当时打印相关矩阵的核心调用,注意 CorrCoef(false) 算的是按列特征的相关性,输出顺序跟 csv_header 一致: ArrayPrint 负责把表头拍进日志;Print 配合 Matrix.CorrCoef(false) 输出 6×6 矩阵,最后一行 EURUSD _self 为 1,其余即各指标与价格的相关系数。

MQL5 / C++
    ArrayPrint(matrix_utils.csv_header);
    Print(Matrix.CorrCoef(class="kw">false));
CS    class="num">0    class="num">06:class="num">29:class="num">41.493    TestEA(EURUSD,H1)    "Stochastic" "Rsi"      "Volume"   "Bears"      "Bulls"      "EURUSD"    
CS    class="num">0    class="num">06:class="num">29:class="num">41.493    TestEA(EURUSD,H1)    [[class="num">1,class="num">0.680705511991766,class="num">0.02399740959375265,class="num">0.6910892641498844,class="num">0.7291018045506749,class="num">0.1490856367010467]
CS    class="num">0    class="num">06:class="num">29:class="num">41.493    TestEA(EURUSD,H1)     [class="num">0.680705511991766,class="num">1,class="num">0.07620207894739518,class="num">0.8184961346648213,class="num">0.8258569040865805,class="num">0.1567269000583347]
CS    class="num">0    class="num">06:class="num">29:class="num">41.493    TestEA(EURUSD,H1)     [class="num">0.02399740959375265,class="num">0.07620207894739518,class="num">1,class="num">0.3752014290536041,-class="num">0.1289026185114097,-class="num">0.1024017077869821]
CS    class="num">0    class="num">06:class="num">29:class="num">41.493    TestEA(EURUSD,H1)     [class="num">0.6910892641498844,class="num">0.8184961346648213,class="num">0.3752014290536041,class="num">1,class="num">0.7826404088603456,class="num">0.07283638913665436]
CS    class="num">0    class="num">06:class="num">29:class="num">41.493    TestEA(EURUSD,H1)     [class="num">0.7291018045506749,class="num">0.8258569040865805,-class="num">0.1289026185114097,class="num">0.7826404088603456,class="num">1,class="num">0.08392530400705019]
CS    class="num">0    class="num">06:class="num">29:class="num">41.493    TestEA(EURUSD,H1)     [class="num">0.1490856367010467,class="num">0.1567269000583347,-class="num">0.1024017077869821,class="num">0.07283638913665436,class="num">0.08392530400705019,class="num">1]]

多重共线性下为什么换岭回归

当解释变量之间高度相关时,普通最小二乘(OLS)估计出的系数方差会膨胀,参数变得极不稳定,样本稍变结果就跳。外汇与贵金属的高频序列里,动量、波动率类因子经常互相纠缠,直接线性回归容易过拟合,属典型高风险场景。 岭回归的思路是给系数平方加惩罚项,把估计值往零轻微收缩,用一点偏差换方差大幅下降。经验上在因子相关系数超过 0.8 的盘面,RR 估计器比 OLS 的标准误可能小 30%–50%,回测曲线也更平滑。 代价是系数不再无偏,但交易信号要的是样本外稳健,不是数学上的无偏美。倾向用岭参数做网格寻优,而不是拍脑袋定一个值。

「用岭回归给系数加道护栏」

岭回归在普通最小二乘公式里加了一项为 λI 的惩罚项:y 是目标向量、X 是设计矩阵、I 是单位矩阵,λ 取大于等于 0 的数。λ 越大,系数被压缩得越狠,过拟合的概率倾向降低,但 λ 设太大模型会欠拟合。 构造 CRidgeregression 时先对数据做标准化,再拆成 X 和 y 两个矩阵,最后按列数 k 建一个 k×k 的单位矩阵备用。L2Norm 函数严格照公式走:拼设计矩阵、转置、乘出 XᵀX、加上 λI、求逆、再乘 Xᵀy,出来的 Betas 就是带惩罚的系数。 拿 NASDAQ_DATA.csv 跑了一遍,λ 设 0.3。岭回归给出的系数是 [5.01e-16, 0.60135, 0.33815, 0.21195],同一份数据走普通最小二乘(先标准化)是 [-4.14e-14, 0.60348, 0.33635, 0.21127]。两者只差千分之几,说明估测器本身没算错。 岭回归不是独立模型,只是产系数的估测器。所以我把原有线性回归类加了一个新构造函数,允许直接把外部系数灌进去,下次换别的估测器时就不必重写训练逻辑。外汇和贵金属波动大、杠杆高,这类系数仅供回测参考,实盘信号可能随样本外数据漂移。

MQL5 / C++
CRidgeregression::CRidgeregression(matrix &_matrix)
 {


   n = _matrix.Rows();
   k = _matrix.Cols();

   pre_processing.Standardization(_matrix);
   m_dataset.Copy(_matrix);

   matrix_utils.XandYSplitMatrices(_matrix,XMatrix,yVector);

   YMatrix = matrix_utils.VectorToMatrix(yVector);

class=class="str">"cmt">//---
   Id_matrix.Resize(k,k);

   Id_matrix.Identity();
 }
vector CRidgeregression::L2Norm(class="type">class="kw">double lambda)
 {
   matrix design = matrix_utils.DesignMatrix(XMatrix);

   matrix XT = design.Transpose();

   matrix XTX = XT.MatMul(design);

   matrix lamdaxI = lambda * Id_matrix;

   class=class="str">"cmt">//Print("LambdaxI \n",lamdaxI);

   class=class="str">"cmt">//Print("XTX\n",XTX);

   matrix sum_matrix = XTX + lamdaxI;

   matrix Inverse_sum = sum_matrix.Inv();

   matrix XTy = XT.MatMul(YMatrix);

   Betas = Inverse_sum.MatMul(XTy);

   class="macro">#ifdef DEBUG_MODE
      Print("Betas\n",Betas);
   class="macro">#endif 

   class="kw">return(matrix_utils.MatrixToVector(Betas));
 } 
class="type">int OnInit()
  {
class=class="str">"cmt">//---
    matrix Matrix = matrix_utils.ReadCsv("NASDAQ_DATA.csv",","); 

    pre_processing.Standardization(Matrix);
    Linear_reg = new  CLinearRegression(Matrix);

    ridge_reg = new CRidgeregression(Matrix);

    ridge_reg.L2Norm(class="num">0.3);
  }    
    matrix Matrix = matrix_utils.ReadCsv("NASDAQ_DATA.csv",","); 

    pre_processing.Standardization(Matrix);
    Linear_reg = new  CLinearRegression(Matrix);
class CLinearRegression
  {
  class="kw">public: 
       CLinearRegression(matrix &Matrix_); class=class="str">"cmt">//Least squares estimator
       CLinearRegression(matrix<class="type">class="kw">double> &Matrix_, class="type">class="kw">double Lr, class="type">uint iters = class="num">1000); class=class="str">"cmt">//Lr by Gradient descent
       CLinearRegression(matrix &Matrix_, vector &coeff_vector);
       ~CLinearRegression(class="type">void);

◍ 岭回归和最小二乘在 EURUSD 上的实测偏差

在 EURUSD H1 全量数据上同时跑岭回归与标准线性回归,两者系数几乎重合:岭回归三项自变量系数为 0.60135 / 0.33815 / 0.21195,线性回归为 0.60348 / 0.33635 / 0.21127,R² 分别为 0.982949 与 0.982933。全样本拟合时惩罚项 0.3 的岭回归反而略欠拟合,图形上肉眼难分蓝标预测值。 切到 Train/Test 拆分后,岭回归训练精度 0.97580、测试 0.78620;线性回归训练 0.97580、测试 0.78540。小惩罚下两模型泛化力接近,说明 lambda 选值仍是开放问题。 把 lambda 提到 10,岭回归训练精度从 0.97580 掉到 0.95760,测试精度却从 0.78540 升到 0.80050。惩罚加重会牺牲训练贴合度,但测试端有微弱改善,外汇与贵金属市场高波动,这种边际变化需多周期复核。 下面这段 MT5 代码演示了两种模型在同矩阵上的系数传递与训练测试拆分流程,可直接贴进 EA 的 OnTester 里改 lambda 跑对照。

MQL5 / C++
    Print("----> Ridge regression");
    
    ridge_reg = new CRidgeregression(Matrix);
    vector coeff = ridge_reg.L2Norm(class="num">0.3);
    
    Linear_reg = new CLinearRegression(Matrix,coeff); class=class="str">"cmt">//passing the coefficients made by ridge regression
                                        class=class="str">"cmt">// to the Linear regression model
    class="type">class="kw">double acc =class="num">0;
    
    vector ridge_predictions = Linear_reg.LRModelPred(Matrix,acc); class=class="str">"cmt">//making the predictions and storing them to a vector
    
    class="kw">delete(Linear_reg); class=class="str">"cmt">//deleting that instance
    
    Print("----> Linear Regression");

    pre_processing.Standardization(Matrix);
    
    Linear_reg = new CLinearRegression(Matrix); class=class="str">"cmt">//new Linear reg instance that gets coefficients by least squares
    
    vector linear_pred = Linear_reg.LRModelPred(Matrix,acc);
    matrix_utils.TrainTestSplitMatrices(Matrix,TrainMatrix,TestMatrix);
    
    Print("----> Ridge regression | Train ");
    
    ridge_reg = new CRidgeregression(TrainMatrix);
    vector coeff = ridge_reg.L2Norm(class="num">0.3);
    
    Linear_reg = new CLinearRegression(TrainMatrix,coeff); class=class="str">"cmt">//passing the coefficients made by ridge regression
                                        class=class="str">"cmt">// to the Linear regression model
    Linear_reg.LRModelPred(TrainMatrix,acc);
    
    printf("Accuracy %.5f ",acc);
    
    Print("----> Ridge regression | Test");
    
    vector ridge_predictions = Linear_reg.LRModelPred(TestMatrix,acc); class=class="str">"cmt">//making the predictions and storing them to a vector
    
    printf("Accuracy %.5f ",acc);
    
    class="kw">delete(Linear_reg); class=class="str">"cmt">//deleting that instance
    
    Print("\n----> Linear Regression | Train ");
    
    Linear_reg = new CLinearRegression(TrainMatrix); class=class="str">"cmt">//new Linear reg instance that gets coefficients by least squares

训练集与测试集上分别看回归精度

模型跑完训练矩阵后,先打出训练集上的 acc 值,再用同一套权重去预测测试矩阵,打印测试集精度。两个数字摆在一起,才能判断是不是过拟合。 如果训练精度 0.98、测试精度 0.61,说明模型把噪声也学进去了;外汇与贵金属行情噪声大、高风险,单纯追高训练精度没有意义,重点看测试集那一行。 代码里 TrainMatrix 和 TestMatrix 必须来自同一特征工程管道,否则 acc 不可比。开 MT5 把这两行 printf 都留着,别只信训练集输出。

MQL5 / C++
  Linear_reg.LRModelPred(TrainMatrix,acc);
  
  printf("Accuracy %.5f ",acc);
  
  Print("----> Linear Regression | Test ");
  
  vector linear_pred = Linear_reg.LRModelPred(TestMatrix,acc);
  
  printf("Accuracy %.5f ",acc);
把共线诊断交给小布盯盘
小布盯盘的 AIGC 已内置特征相关性与系数稳定性扫描,打开对应品种页即可看到哪些因子在拖垮泛化,你只需决定留哪根线。

常见问题

当自变量之间存在明显共线性、最小二乘系数震荡剧烈时,岭回归通过引入 L2 惩罚压缩系数,倾向得到更稳的估测。
岭回归的 L2 惩罚保留全部特征但缩小系数,lasso 的 L1 惩罚会把部分系数直接压到零,兼具变量选择。
可以,小布盯盘的品种页内置了相关因子扫描与稳定性提示,把重复劳动交给小布,你专注决策。
两者负相关,模型简化降方差就升偏差,加复杂度和特征降偏差却放大方差,只能按数据找折中点。
行情高波动高风险,岭回归降低过拟合概率,但预测仍可能失效,需样本外滚动验证。