数据科学与机器学习(第 10 部分):岭回归·进阶篇
(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,其余即各指标与价格的相关系数。
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]。两者只差千分之几,说明估测器本身没算错。 岭回归不是独立模型,只是产系数的估测器。所以我把原有线性回归类加了一个新构造函数,允许直接把外部系数灌进去,下次换别的估测器时就不必重写训练逻辑。外汇和贵金属波动大、杠杆高,这类系数仅供回测参考,实盘信号可能随样本外数据漂移。
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 跑对照。
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 都留着,别只信训练集输出。
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);