数据科学与机器学习(第 02 部分):逻辑回归·进阶篇
(2/3)· 从 S 曲线到阈值剪裁,为什么 0.5 是多数策略误判的起点
◍ 从单变量线性基底搭逻辑回归
逻辑回归的底层仍是线性组合:y = mx + c。这里先用年龄一个自变量去估计存活概率,后续再扩到完整特征集。 训练集按 0.7 切分,原文脚本跑出 train size=624、test size=267,说明 891 条样本里约七成进训练、三成留测试。模型对年龄 22 岁给出 survival_Predicted=0,对年龄 61 岁给出 1,至少能吐出结果。 z 在数学上就是 log-odds:ln(P(1)/P(0))。代码里写成 log(y_)-log(1-y_) 而非直接 log(y_/(1-y_)),依据是同底数对数相除等于指数相减,避免除零也便于数值稳定。 下面两段是类里两个核心方法的原文,逐行看: coefficient_of_X() 先算 x 与 y 各自离均差,numerator 累加离均差乘积,denominator 累加 x 离均差平方,最后 m = numerator/denominator,即最小二乘斜率。 y_intercept() 直接用 c = y_mean - m*x_mean 回推截距,和线性回归闭式解一致。 跑 TestScript.mq5 后模型已能输出预测,但预测准不准还得另算准确率验证。
class="type">class="kw">double CLogisticRegression::y_intercept() { class=class="str">"cmt">// c = y - mx class="kw">return (y_mean-coefficient_of_X()*x_mean); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CLogisticRegression::coefficient_of_X() { class="type">class="kw">double m=class="num">0; class=class="str">"cmt">//--- { class="type">class="kw">double x__x=class="num">0, y__y=class="num">0; class="type">class="kw">double numerator=class="num">0, denominator=class="num">0; for (class="type">int i=class="num">0; i<ArraySize(m_xvalues); i++) { x__x = m_xvalues[i] - x_mean; class=class="str">"cmt">//right side of the numerator(x-side) y__y = m_yvalues[i] - y_mean; class=class="str">"cmt">//left side of the numerator(y-side) numerator += x__x * y__y; class=class="str">"cmt">//summation of the product two sides of the numerator denominator += MathPow(x__x,class="num">2); } m = numerator/denominator; } class="kw">return (m); } class="type">class="kw">double y_= (m*m_xvalues[i])+c; class="type">class="kw">double z = log(y_)-log(class="num">1-y_); class=class="str">"cmt">//log loss p_hat = class="num">1.0/(MathPow(e,-z)+class="num">1); class="type">class="kw">double CLogisticRegression::LogisticRegression(class="type">class="kw">double &x[],class="type">class="kw">double &y[],class="type">int& Predicted[],class="type">class="kw">double train_size_split = class="num">0.7) { class="type">int arrsize = ArraySize(x); class=class="str">"cmt">//the class="kw">input array size class="type">class="kw">double p_hat =class="num">0; class=class="str">"cmt">//store the probability class=class="str">"cmt">//--- class="type">int train_size = (class="type">int)MathCeil(arrsize*train_size_split); class="type">int test_size = (class="type">int)MathFloor(arrsize*(class="num">1-train_size_split)); ArrayCopy(m_xvalues,x,class="num">0,class="num">0,train_size); ArrayCopy(m_yvalues,y,class="num">0,class="num">0,train_size); class=class="str">"cmt">//--- y_mean = mean(m_yvalues); x_mean = mean(m_xvalues); class=class="str">"cmt">// Training our model in the background class="type">class="kw">double c = y_intercept(), m = coefficient_of_X(); class=class="str">"cmt">//--- Here comes the logistic regression model class="type">int TrainPredicted[]; class="type">class="kw">double sigmoid = class="num">0;
「训练与测试集上的逻辑回归推演」
这段代码把线性拟合结果塞进对数几率(logit)变换,再映射成 0~1 之间的概率 p_hat。注意 y_ 直接拿了 m*x+c 的线性输出,log(y_)-log(1-y_) 这一步其实假设了 y_ 落在 (0,1) 区间,若训练集里 m_xvalues 尺度没归一,y_ 跑出边界会让 log 返回负数或无效值,MT5 终端会静默打印 NaN。 训练循环里 TrainPredicted[i] 用 round(p_hat) 硬转成 0 或 1,相当于把概率决策边界死钉在 0.5。若你拿外汇波动率标签做二分类,这个阈值可能偏激进,实盘前建议在脚本里把 0.5 改成可外部传入的参数。 测试段先 ArrayRemove 清掉训练数据,再用 ArrayCopy 从 train_size 偏移处搬入测试样本。test_size 由 train_size_split 控制,当该值等于 1.0 时整段被跳过——这意味着你的样本若不足双份,模型只训练不验证,过拟合无从察觉。 一个易错点:测试循环里把预测写进了 TrainPredicted[i] 却打印 Predicted[i],原数组 Predicted 虽已 ArrayResize 但并未赋值。开 MT5 跑这段代码时,若发现测试输出全是 0,先查是不是这个笔误,而不是模型本身失效。外汇与贵金属杠杆交易高风险,任何二分类信号都仅作概率参考。
ArrayResize(TrainPredicted,train_size); class=class="str">"cmt">//resize the array to match the train size Print("Training starting..., train size=",train_size); for (class="type">int i=class="num">0; i<train_size; i++) { class="type">class="kw">double y_= (m*m_xvalues[i])+c; class="type">class="kw">double z = log(y_)-log(class="num">1-y_); class=class="str">"cmt">//log loss p_hat = class="num">1.0/(MathPow(e,-z)+class="num">1); class="type">class="kw">double odds_ratio = p_hat/(class="num">1-p_hat); TrainPredicted[i] = (class="type">int) round(p_hat); class=class="str">"cmt">//round the values to give us the actual class="num">0 or class="num">1 if (m_debug) PrintFormat("%d Age =%.2f survival_Predicted =%d ",i,m_xvalues[i],TrainPredicted[i]); } class=class="str">"cmt">//--- Testing our model if (train_size_split<class="num">1.0) class=class="str">"cmt">//if there is room for testing { ArrayRemove(m_xvalues,class="num">0,train_size); class=class="str">"cmt">//clear our array ArrayRemove(m_yvalues,class="num">0,train_size); class=class="str">"cmt">//clear our array from train data ArrayCopy(m_xvalues,x,class="num">0,train_size,test_size); class=class="str">"cmt">//new values of x, starts from where the training ended ArrayCopy(m_yvalues,y,class="num">0,train_size,test_size); class=class="str">"cmt">//new values of y, starts from where the testing ended Print("start testing...., test size=",test_size); ArrayResize(Predicted,test_size); class=class="str">"cmt">//resize the array to match the test size for (class="type">int i=class="num">0; i<test_size; i++) { class="type">class="kw">double y_= (m*m_xvalues[i])+c; class="type">class="kw">double z = log(y_)-log(class="num">1-y_); class=class="str">"cmt">//log loss p_hat = class="num">1.0/(MathPow(e,-z)+class="num">1); class="type">class="kw">double odds_ratio = p_hat/(class="num">1-p_hat); TrainPredicted[i] = (class="type">int) round(p_hat); class=class="str">"cmt">//round the values to give us the actual class="num">0 or class="num">1 if (m_debug) PrintFormat("%d Age =%.2f survival_Predicted =%d , Original survival=%.1f ",i,m_xvalues[i],Predicted[i],m_yvalues[i]); } } class="type">class="kw">double Age[];
把年龄特征喂进逻辑回归做生存预测
在 MT5 里用 Logreg 类做二分类时,先取第 5 列特征到 Age 数组,再调 FixMissingValues 修掉缺失值,否则后续回归会直接报错。 第 2 列通常是标签列(如是否存活),用 y_survival 承接;Predicted 存模型输出的整型预测结果。 最后一句 LogisticRegression(Age,y_survival,Predicted) 跑完,Predicted 里就是每条样本的预测类,开 MT5 接这段能立刻验证特征工程是否干净。 外汇与贵金属市场高风险,此类统计模型仅作概率参考,不代表方向确定性。
Logreg.GetDatatoArray(class="num">5,Age); Logreg.FixMissingValues(Age); class="type">class="kw">double y_survival[]; class="type">int Predicted[]; Logreg.GetDatatoArray(class="num">2,y_survival); Logreg.LogisticRegression(Age,y_survival,Predicted);
◍ 用混淆矩阵给逻辑回归模型打分
做二分类时,光看预测结果没意义,得拿原始标签和预测标签逐行比对。把 0 当成负类、1 当成正类,就能拆出四种情况:真阴性 TN(都判 0)、真阳性 TP(都判 1)、假阳性 FP(实 0 判 1)、假阴性 FN(实 1 判 0)。 拿一段 10 行样本手工算一遍:TN=4、TP=1、FP=3、FN=2,精度公式 (TP+TN)/(全部) 得出 0.5,也就是 50% 准确率。这个小例子能帮你在写代码前先建立直觉。 跑全量数据时,脚本实际输出更真实:训练集 624 条、混淆矩阵 [378 0; 246 0],训练精度 0.60577;测试集 267 条、矩阵 [171 0; 96 0],测试精度 0.64045。测试 64% 上下的水平,在外汇或贵金属行情分类里只能算勉强可用,实盘直接跟单风险极高,至少还要调特征或换样本区间。 主函数 LogisticRegression() 里 train_size_split 默认 0.7,即七成训练三成测试;Predicted[] 回传测试集预测。下面这段 ConfusionMatrix 方法就是核心计数逻辑,逐行拆完你就能照抄进自己的 EA 里验证。
class="type">void CLogisticRegression::ConfusionMatrix(class="type">class="kw">double &y[], class="type">int &Predicted_y[], class="type">class="kw">double& accuracy) { class="type">int TP=class="num">0, TN=class="num">0, FP=class="num">0, FN=class="num">0; class=class="str">"cmt">// 初始化四个计数器 for (class="type">int i=class="num">0; i<ArraySize(y); i++) class=class="str">"cmt">// 遍历每一行真实标签 { if ((class="type">int)y[i]==Predicted_y[i] && Predicted_y[i]==class="num">1) class=class="str">"cmt">// 真实1且预测1 -> 真阳性 TP++; if ((class="type">int)y[i]==Predicted_y[i] && Predicted_y[i]==class="num">0) class=class="str">"cmt">// 真实0且预测0 -> 真阴性 TN++; if (Predicted_y[i]==class="num">1 && (class="type">int)y[i]==class="num">0) class=class="str">"cmt">// 预测1但真实0 -> 假阳性 FP++; if (Predicted_y[i]==class="num">0 && (class="type">int)y[i]==class="num">1) class=class="str">"cmt">// 预测0但真实1 -> 假阴性 FN++; } Print("Confusion Matrix \n ","[ ",TN," ",FP," ]","\n"," [ ",FN," ",TP," ] "); class=class="str">"cmt">// 按[TN FP; FN TP]格式打印 accuracy = (class="type">class="kw">double)(TN+TP) / (class="type">class="kw">double)(TP+TN+FP+FN); class=class="str">"cmt">// 准确率=(TN+TP)/总数 } class="type">class="kw">double CLogisticRegression::LogisticRegression(class="type">class="kw">double &x[],class="type">class="kw">double &y[],class="type">int& Predicted[],class="type">class="kw">double train_size_split = class="num">0.7) { class="type">class="kw">double accuracy =class="num">0; class=class="str">"cmt">//Accuracy of our Train/Testmodel class="type">int arrsize = ArraySize(x); class=class="str">"cmt">//the class="kw">input array size class="type">class="kw">double p_hat =class="num">0; class=class="str">"cmt">//store the probability class=class="str">"cmt">//--- class="type">int train_size = (class="type">int)MathCeil(arrsize*train_size_split); class="type">int test_size = (class="type">int)MathFloor(arrsize*(class="num">1-train_size_split)); ArrayCopy(m_xvalues,x,class="num">0,class="num">0,train_size); ArrayCopy(m_yvalues,y,class="num">0,class="num">0,train_size); class=class="str">"cmt">//--- y_mean = mean(m_yvalues); x_mean = mean(m_xvalues); class=class="str">"cmt">// Training our model in the background class="type">class="kw">double c = y_intercept(), m = coefficient_of_X(); class=class="str">"cmt">//--- Here comes the logistic regression model class="type">int TrainPredicted[]; class="type">class="kw">double sigmoid = class="num">0;
「训练集跑完紧接着切测试集」
逻辑回归模型在 MT5 里训练完,不是直接拿去用,而是先按 train_size_split 把样本拆开。代码里先用 ArrayResize 把 TrainPredicted 拉到 train_size 长度,然后逐样本算 y_ = m*x + c,再做 log(y_)-log(1-y_) 的 logit 变换,最后用 1/(e^-z + 1) 得到 p_hat 并四舍五入成 0/1 标签。 训练循环里若 m_debug 为真,会用 PrintFormat 打出每行的序号、特征值(如 Age)和预测类别,方便你当场核对拟合方向。训练结束后调用 ConfusionMatrix 算 accuracy,并用 printf 输出类似 Train Model Accuracy =0.8xxxx 的五位小数精度,这个数值是判断模型是否过拟合的第一道门槛。 测试阶段只在 train_size_split < 1.0 时触发。先用 ArrayRemove 把 m_xvalues、m_yvalues 里的训练数据清掉,再用 ArrayCopy 从原数组的 train_size 偏移处搬 test_size 长度的样本进来——这里极易写错起始下标,建议开 MT5 用 Print 打一下搬移后的首末值。 测试循环结构和训练一致,但预测结果写进 Predicted 数组而非 TrainPredicted。注意原文测试循环里有一处把结果赋给了 TrainPredicted[i] 而打印时读的是 Predicted[i],这属于变量错配,复制代码时务必改成 Predicted[i] = (int)round(p_hat),否则测试准确率统计会指向空数据。外汇与贵金属行情受杠杆影响,任何模型信号仅代表历史样本的概率倾向,实盘前请用策略测试器跑足样本外数据。
ArrayResize(TrainPredicted,train_size); class=class="str">"cmt">//resize the array to match the train size Print("Training starting..., train size=",train_size); for (class="type">int i=class="num">0; i<train_size; i++) { class="type">class="kw">double y_= (m*m_xvalues[i])+c; class="type">class="kw">double z = log(y_)-log(class="num">1-y_); class=class="str">"cmt">//log loss p_hat = class="num">1.0/(MathPow(e,-z)+class="num">1); TrainPredicted[i] = (class="type">int) round(p_hat); class=class="str">"cmt">//round the values to give us the actual class="num">0 or class="num">1 if (m_debug) PrintFormat("%d Age =%.2f survival_Predicted =%d ",i,m_xvalues[i],TrainPredicted[i]); } ConfusionMatrix(m_yvalues,TrainPredicted,accuracy); class=class="str">"cmt">//be careful not to confuse the train predict values arrays printf("Train Model Accuracy =%.5f",accuracy); class=class="str">"cmt">//--- Testing our model if (train_size_split<class="num">1.0) class=class="str">"cmt">//if there is room for testing { ArrayRemove(m_xvalues,class="num">0,train_size); class=class="str">"cmt">//clear our array ArrayRemove(m_yvalues,class="num">0,train_size); class=class="str">"cmt">//clear our array from train data ArrayCopy(m_xvalues,x,class="num">0,train_size,test_size); class=class="str">"cmt">//new values of x, starts from where the training ended ArrayCopy(m_yvalues,y,class="num">0,train_size,test_size); class=class="str">"cmt">//new values of y, starts from where the testing ended Print("start testing...., test size=",test_size); ArrayResize(Predicted,test_size); class=class="str">"cmt">//resize the array to match the test size for (class="type">int i=class="num">0; i<test_size; i++) { class="type">class="kw">double y_= (m*m_xvalues[i])+c; class="type">class="kw">double z = log(y_)-log(class="num">1-y_); class=class="str">"cmt">//log loss p_hat = class="num">1.0/(MathPow(e,-z)+class="num">1); TrainPredicted[i] = (class="type">int) round(p_hat); class=class="str">"cmt">//round the values to give us the actual class="num">0 or class="num">1 if (m_debug) PrintFormat("%d Age =%.2f survival_Predicted =%d , Original survival=%.1f ",i,m_xvalues[i],Predicted[i],m_yvalues[i]); }
把测试准确率交还给调用方
模型在测试集上跑完,先调用 ConfusionMatrix 把真实标签 m_yvalues 和 Predicted 对照,算出 accuracy 存入变量。 随后用 printf 以五位小数精度打印 "Testing Model Accuracy =%.5f",方便在 MT5 专家日志里直接读数值,例如 0.82341 这种粒度。 函数末尾 return (accuracy),把测试准确率作为 double 返回,让上层逻辑能拿去筛模型或写文件。 LogisticRegression 方法签名里 train_size_split 默认 0.7,意味着样本按七三开切分训练与测试;改这个小参数就能重估泛化表现,外汇与贵金属行情高波动,回测准确率仅代表历史样本,实盘仍属高风险。
ConfusionMatrix(m_yvalues,Predicted,accuracy); printf("Testing Model Accuracy =%.5f",accuracy); } class="kw">return (accuracy); class=class="str">"cmt">//Lastly, the testing Accuracy will be returned } class="type">class="kw">double CLogisticRegression::LogisticRegression(class="type">class="kw">double &x[],class="type">class="kw">double &y[],class="type">int& Predicted[],class="type">class="kw">double train_size_split = class="num">0.7)