数据科学与机器学习(第23部分):为什么LightGBM和XGBoost能超越许多AI模型?·综合运用
「用管道跑通 LightGBM 二分类」
把 LightGBM 塞进 sklearn 的 Pipeline,配合 StandardScaler 做归一化,几行就能训出一个二分类模型。核心超参里,num_leaves 控制单树叶子数,原文示例取 25,典型区间 20–50;max_depth 设 5,常见范围 3–10;learning_rate 用 0.05,偏低更稳但需更多树;feature_fraction 0.9 表示每轮随机用 90% 特征来抑过拟合。 下面这段是原文给出的训练代码,直接 pip 装包后可在 Python 复现。注意 LGBMClassifier 被包在 pipe 里,fit 时自动走 scaler → 模型;控制台里 feature_fraction 警告只是提示 colsample_bytree 被忽略,不影响结果。 [CODE] pip install lightgbm params = { 'boosting_type': 'gbdt', # Gradient Boosting Decision Tree 'objective': 'binary', # For binary classification (use 'regression' for regression tasks) 'metric': ['auc','binary_logloss'], # Evaluation metric 'num_leaves': 25, # Number of leaves in one tree 'n_estimators' : 100, # number of trees 'max_depth': 5, 'learning_rate': 0.05, # Learning rate 'feature_fraction': 0.9 # Fraction of features to be used for each boosting round } pipe = Pipeline([ ("scaler", StandardScaler()), ("lgbm", lgb.LGBMClassifier(**params)) ]) # Fit the pipeline to the training data pipe.fit(X_train, y_train) [/CODE] 训练完用 X_test 跑预测,原文示例测试集 104 个 0 类、96 个 1 类,模型整体准确率约 69%,分类报告里 0 类 f1 0.70、1 类 f1 0.66。这个精度不算高,但说明管道化流程已通,后续可换 ONNX 丢进 MT5 做信号推断。 调参别拍脑袋:n_estimators 从 100 起手,配合早停;num_leaves 和 max_depth 用交叉验证卡过拟合。外汇与贵金属行情噪声大,这类模型实盘前务必做样本外验证,信号失效概率不低。
pip install lightgbm
params = {
&class="macro">#x27;boosting_type&class="macro">#x27;: &class="macro">#x27;gbdt&class="macro">#x27;, # Gradient Boosting Decision Tree
&class="macro">#x27;objective&class="macro">#x27;: &class="macro">#x27;binary&class="macro">#x27;, # For binary classification(use &class="macro">#x27;regression&class="macro">#x27; for regression tasks)
&class="macro">#x27;metric&class="macro">#x27;: [&class="macro">#x27;auc&class="macro">#x27;,&class="macro">#x27;binary_logloss&class="macro">#x27;], # Evaluation metric
&class="macro">#x27;num_leaves&class="macro">#x27;: class="num">25, # Number of leaves in one tree
&class="macro">#x27;n_estimators&class="macro">#x27; : class="num">100, # number of trees
&class="macro">#x27;max_depth&class="macro">#x27;: class="num">5,
&class="macro">#x27;learning_rate&class="macro">#x27;: class="num">0.05, # Learning rate
&class="macro">#x27;feature_fraction&class="macro">#x27;: class="num">0.9 # Fraction of features to be used for each boosting round
}
pipe = Pipeline([
("scaler", StandardScaler()),
("lgbm", lgb.LGBMClassifier(**params))
])
# Fit the pipeline to the training data
pipe.fit(X_train, y_train)◍ 分类报告里的 0.69 意味着什么
上面这段输出是某次二分类模型在 200 个样本上的 sklearn 风格评估表。accuracy 落在 0.69,macro avg 与 weighted avg 的 F1 都在 0.68 附近,说明模型对两类样本的平均辨识力仅略高于随机猜。 在 MT5 里拿这类指标做信号过滤时,要清楚 0.69 的准确率放到外汇小时线里,扣除点差和滑点后大概率失去正期望。贵金属同样属于高杠杆高风险品种,单看这份报告不能直接用作开仓依据。 若你想复现,把模型预测结果和真实标签喂进 ClassificationReport 类,样本量设 200 跑一遍,重点看 weighted avg 的 precision 是否随品种波动明显偏离 0.69。
accuracy class="num">0.69 class="num">200 macro avg class="num">0.68 class="num">0.68 class="num">0.68 class="num">200 weighted avg class="num">0.68 class="num">0.69 class="num">0.68 class="num">200
把 LightGBM 与 XGBoost 管线导成 ONNX
这两个 boosting 模型不像 sklearn 或 Keras 那样一行 dump 就能落地,它们各自带自定义算子,得借 skl2onnx 与 onnxmltools 的桥接转换器走一遍注册流程。 实际保存时两个模型流程完全一致:先 update_registered_converter 把分类器形状计算和转换器挂上,再 convert_sklearn 指定输入张量维度与 target_opset,最后 SerializeToString 写文件。下面代码里 EUUSD H1 的 LightGBM 落盘为 lightgbm.eurusd.h1.onnx,XGBoost 落盘为 xgboost.eurusd.h1.onnx,opset 主图 12、ml 域 2。 外汇与贵金属模型导出涉及历史样本过拟合风险,实盘加载前应在 MT5 用 out-of-sample 时段重测,概率层面验证信号衰减。
from skl2onnx.common.data_types class="kw">import FloatTensorType from skl2onnx class="kw">import convert_sklearn, to_onnx, update_registered_converter from skl2onnx.common.shape_calculator class="kw">import calculate_linear_classifier_output_shapes from onnxmltools.convert.xgboost.operator_converters.XGBoost class="kw">import convert_xgboost from onnxmltools.convert class="kw">import convert_xgboost as convert_xgboost_booster update_registered_converter( lgb.LGBMClassifier, "GBMClassifier", calculate_linear_classifier_output_shapes, convert_lightgbm, options={"nocl": [False], "zipmap": [True, False, "columns"]}, ) model_onnx = convert_sklearn( pipe, "pipeline_lightgbm", [("input", FloatTensorType([None, X_train.shape[class="num">1]]))], target_opset={"": class="num">12, "ai.onnx.ml": class="num">2}, ) # And save. with open("lightgbm.eurusd.h1.onnx", "wb") as f: f.write(model_onnx.SerializeToString()) update_registered_converter( xgb.XGBClassifier, "XGBClassifier", calculate_linear_classifier_output_shapes, convert_xgboost, options={"nocl": [False], "zipmap": [True, False, "columns"]}, ) model_onnx = convert_sklearn( pipe, "pipeline_xgboost", [("input", FloatTensorType([None, X_train.shape[class="num">1]]))], target_opset={"": class="num">12, "ai.onnx.ml": class="num">2}, ) # And save. with open("xgboost.eurusd.h1.onnx", "wb") as f: f.write(model_onnx.SerializeToString())
「GBDT模型在MT5里的输入层坑点」
把 LightGBM 导出的 ONNX 塞进 MT5,表面流程和别的模型差不多,实际卡点集中在张量维度声明。日志里写得很直白:输入层 shape 是 [-1, 8],输出层有两个,一个是 output_label(shape [-1]),另一个是 output_probability,类型是 ONNX_TYPE_SEQUENCE,底层是 ZipMap,也就是带标签和概率的两个一维数组打包成的映射。 用 Netron 打开模型能确认,第二输出层这种序列套映射的结构,用 OnnxSetOutputShape 去硬改尺寸会报 Err=5802,而且抛出的警告信息并不好懂。实测把第二输出层维度强行设为 1,虽然终端会刷一条警告,但模型推理仍能跑通,EURUSD H1 上加载后显示 ONNX model Initialized 即代表可用。 外汇与贵金属杠杆高,这类模型推理只是辅助信号,实盘前务必在策略测试器用历史数据回验。下面这段是 LightGBM 类里负责枚举输入输出张量信息的核心方法,debug 模式下会把层数、名字、维度全打印出来,方便你核对模型是不是真加载对了。
class="type">bool CLightGBM::OnnxLoad(class="type">long &handle) { class=class="str">"cmt">//--- since not all sizes defined in the input tensor we must set them explicitly class=class="str">"cmt">//--- first index - batch size, second index - series size, third index - number of series(only Close) OnnxTypeInfo type_info; class=class="str">"cmt">//Getting onnx information for Reference In case you forgot what the loaded ONNX is all about class="type">long input_count=OnnxGetInputCount(handle); if (MQLInfoInteger(MQL_DEBUG)) Print("model has ",input_count," input(s)"); for(class="type">long i=class="num">0; i<input_count; i++) { class="type">class="kw">string input_name=OnnxGetInputName(handle,i); if (MQLInfoInteger(MQL_DEBUG)) Print(i," input name is ",input_name); if(OnnxGetInputTypeInfo(handle,i,type_info)) { if (MQLInfoInteger(MQL_DEBUG)) PrintTypeInfo(i,"input",type_info); ArrayCopy(inputs, type_info.tensor.dimensions); } } class="type">long output_count=OnnxGetOutputCount(handle); if (MQLInfoInteger(MQL_DEBUG)) Print("model has ",output_count," output(s)"); for(class="type">long i=class="num">0; i<output_count; i++) { class="type">class="kw">string output_name=OnnxGetOutputName(handle,i); if (MQLInfoInteger(MQL_DEBUG)) Print(i," output name is ",output_name); if(OnnxGetOutputTypeInfo(handle,i,type_info)) { if (MQLInfoInteger(MQL_DEBUG)) PrintTypeInfo(i,"output",type_info); ArrayCopy(outputs, type_info.tensor.dimensions); } } class=class="str">"cmt">//---
◍ 给 ONNX 模型喂形状与跑推理的细节
在 MT5 里加载 LightGBM 导出的 ONNX 模型,第一步是把输入输出的维度告诉推理句柄。代码里用 OnnxSetInputShape 逐节点绑定 inputs 数组,若返回 false 会打印错误码并触发 DebugBreak,方便你当场抓出维度不匹配的问题。 输出侧的处理更宽松:OnnxSetOutputShape 失败时只打印 'Failed to set the Output[%d] shape',注释掉了 DebugBreak 和 return false。这意味着某个输出节点形状异常时,初始化流程仍会继续,但后续推理可能拿到错位数据,建议手动把这两行放开做严格校验。 真正跑预测靠 OnnxRun,先把 output_data 用 ArrayResize 撑到 outputs.Size() 大小,再以 ONNX_DATA_TYPE_FLOAT 类型传入特征 x_float。外汇与贵金属行情的高波动特性下,模型推理结果仅代表概率倾向,实盘使用前务必用历史 tick 回测验证维度链路。 下面这段是核心初始化与推理调用的节选,逐行拆一下关键动作:replace 把负数维度替成 UNDEFINED_REPLACE;循环设 shape 失败即停;OnnxRun 失败则返回空 proba 避免脏数据外流。
replace(inputs); replace(outputs); class=class="str">"cmt">//--- Setting the input size for (class="type">long i=class="num">0; i<input_count; i++) if (!OnnxSetInputShape(handle, i, inputs)) class=class="str">"cmt">//Giving the Onnx handle the input shape { printf("Failed to set the input shape Err=%d",GetLastError()); DebugBreak(); class="kw">return class="kw">false; } class=class="str">"cmt">//--- Setting the output size for(class="type">long i=class="num">0; i<output_count; i++) { if(!OnnxSetOutputShape(handle,i,outputs)) { printf("Failed to set the Output[%d] shape Err=%d",i,GetLastError()); class=class="str">"cmt">//DebugBreak(); class=class="str">"cmt">//class="kw">return class="kw">false; } } initialized = true; Print("ONNX model Initialized"); class="kw">return true; } class="type">class="kw">float output_data[]; class="kw">struct Map { class="type">class="kw">ulong key[]; class="type">class="kw">float value[]; } output_data_map[]; class=class="str">"cmt">//--- ArrayResize(output_data, outputs.Size()); if (!OnnxRun(onnx_handle, ONNX_DATA_TYPE_FLOAT, x_float, output_data, output_data_map)) { printf("Failed to get predictions from Onnx err %d",GetLastError()); class="kw">return proba; }
实时与测试两套预测接口怎么选
在 MT5 里做模型推理,得先分清实时预测和离线测试走的是哪套接口。给单个特征向量 x 调用 predict_bin,返回的是该样本的分类标签(长整型),适合挂在 EA 的 OnTick 里做逐根 K 线实时判定。 同名的 predict_bin 还有接收 matrix 的重载,一次吐出整批样本的标签向量,主要用在回测脚本里批量验证,而不是线上逐笔触发。 概率类接口 predict_proba 同理:vector 入参对应单样本实时概率,matrix 入参对应全样本矩阵概率。外汇与贵金属杠杆高、滑点跳空频繁,实时概率只代表模型当下的倾向,不等于成交价,实盘前务必用 matrix 版在 historical 数据上跑一遍误差分布。 打开 MT5 的 MetaEditor,把这两个重载直接塞进你的模型类声明里,编译后先用一组 EURUSD 的 H1 特征矩阵调 predict_proba(matrix),看输出维度是否等于样本数,再决定接不接实时分支。
class="kw">virtual class="type">long predict_bin(const vector &x); class=class="str">"cmt">//REturns the predictions for the current given matrix | useful in real-time prediction class="kw">virtual vector predict_proba(const vector &x); class=class="str">"cmt">//Returns the predictions in probability terms | useful in real-time prediction class="kw">virtual matrix predict_proba(const matrix &x); class=class="str">"cmt">//Returns the predicted probability for the whole matrix | useful for testing class="kw">virtual vector predict_bin(const matrix &x); class=class="str">"cmt">//gives out the vector for all the predictions | useful for testing
「GBDT模型实盘回测与样本外准确率拆解」
在 OnInit 里把 ONNX 模型加载进来后,EA 就能用和训练时同构的特征去跑推理。简单策略就是:每根新 K 线开盘时取信号,0 为空头、1 为多头,直接市价跟。 我在策略测试器跑了 EURUSD 的 H1 周期,区间 2023-01-01 到 2024-05-23。LightGBM 和 XGBoost 两个模型资金曲线都向下,全部亏损;XGBoost 比 LightGBM 少亏 8 美元,两条曲线几乎重合。外汇与贵金属杠杆交易高风险,回测亏损不代表未来,但说明裸信号直接跟单不可行。 测试器报告看不出模型对「下一根 K 线方向」的预测质量,于是另写脚本按训练方式收集样本外数据并打标签。LightGBM 在 9000 条样本上混淆矩阵为 [[2857,1503],[926,3714]],分类报告给出准确率 0.73,类 0 的 precision/recall 为 0.76/0.66,类 1 为 0.71/0.80。 样本外 73% 准确率意味着模型学到了结构,但 EA 不赚钱——因为盈利还要考虑点差、滑点、仓位与出场。这反而是一个干净起点:你可以在 MT5 里把下面这段代码的特征向量和信号分支接上自己的风控层。
class="type">int OnInit() { if (!lgbm.Init(lightgbm_onnx)) class="kw">return INIT_FAILED; } class="type">void OnTick() { class="type">int size = CopyRates(Symbol(), PERIOD_CURRENT, class="num">1, class="num">1, rates_x); class=class="str">"cmt">//We copy only one recent-closed bar class=class="str">"cmt">//--- if (NewBar()) { vector x = { rates_x[class="num">0].open, rates_x[class="num">0].high, rates_x[class="num">0].low, rates_x[class="num">0].close, rates_x[class="num">0].close-rates_x[class="num">0].open, rates_x[class="num">0].high-rates_x[class="num">0].low, rates_x[class="num">0].close-rates_x[class="num">0].low, rates_x[class="num">0].close-rates_x[class="num">0].high }; class="type">long signal = lgbm.predict_bin(x); Comment("Signal: ",signal); } if (NewBar()) class=class="str">"cmt">//Trade at the opening of a new candle { vector x = { rates_x[class="num">0].open, rates_x[class="num">0].high, rates_x[class="num">0].low, rates_x[class="num">0].close, rates_x[class="num">0].close-rates_x[class="num">0].open, rates_x[class="num">0].high-rates_x[class="num">0].low, rates_x[class="num">0].close-rates_x[class="num">0].low, rates_x[class="num">0].close-rates_x[class="num">0].high }; class="type">long signal = lgbm.predict_bin(x); Comment("Signal: ",signal); class=class="str">"cmt">//--- class="type">MqlTick ticks; SymbolInfoTick(Symbol(), ticks); if (signal==class="num">1) class=class="str">"cmt">//if the signal is bullish {
◍ 用样本外数据校验模型信号
这段逻辑把 LightGBM 的 ONNX 模型接进 MT5,靠历史 K 线做一轮脱离训练集的回测验证。信号判定很直接:若账户里没有对应方向的持仓,就按多空信号补一单,止损止盈用 Point 倍数换算,外汇与贵金属杠杆品种下这种写法爆仓风险偏高,参数需自行压低 lotsize。 OnStart 里先跳过前 1000 根bar,向后取 9000 根做特征,再偏移一根取同向 9000 根当次根收盘价标签,实际跑下来若 predictions 与 actual 偏差大,说明模型在该品种过拟合倾向明显。 特征向量只塞了 OHLC 及四个派生价差,没带成交量与跨周期量,读者可改 x 的组成看 classification_report 的精确率是否跳动。复制下面代码到 MT5 脚本,把 lightgbm_onnx 路径换掉即可跑通样本外报告。
if (!PosExists(POSITION_TYPE_BUY)) class=class="str">"cmt">//There are no buy positions m_trade.Buy(lotsize, Symbol(), ticks.ask, ticks.bid-stoploss*Point(), ticks.ask+takeprofit*Point()); class=class="str">"cmt">//Open a buy trade } else class=class="str">"cmt">//Bearish signal { if (!PosExists(POSITION_TYPE_SELL)) class=class="str">"cmt">//There are no Sell positions m_trade.Sell(lotsize, Symbol(), ticks.bid, ticks.ask+stoploss*Point(), ticks.bid-takeprofit*Point()); class=class="str">"cmt">//open a sell trade } } class="type">void OnStart() { class=class="str">"cmt">//--- if (!lgb.Init(lightgbm_onnx)) class="kw">return; class=class="str">"cmt">//--- custom out-of-sample testing class="type">int bars = class="num">9000; class="type">int start = class="num">1000; class="type">MqlRates rates_x[]; ArraySetAsSeries(rates_x, true); class="type">int size = CopyRates(Symbol(), PERIOD_CURRENT, start, bars, rates_x); class=class="str">"cmt">//We start at the bar class="num">1000 and collect class="num">9000 candles backward class="type">MqlRates rates_y[]; ArraySetAsSeries(rates_y, true); CopyRates(Symbol(), PERIOD_CURRENT, start-class="num">1, bars, rates_y); class=class="str">"cmt">//We do the same thing here but we only collect one bar forward making sure we get the prediction for the next candle class=class="str">"cmt">//--- vector actual(size), predictions(size); for (class="type">int i=class="num">0; i<size; i++) { vector x = { rates_x[i].open, rates_x[i].high, rates_x[i].low, rates_x[i].close, rates_x[i].close-rates_x[i].open, rates_x[i].high-rates_x[i].low, rates_x[i].close-rates_x[i].low, rates_x[i].close-rates_x[i].high }; actual[i] = rates_y[i].close > rates_x[i].open ? class="num">1 : class="num">0; class=class="str">"cmt">//making the target variable predictions[i] = (class="type">class="kw">double)lgb.predict_bin(x); } Metrics::classification_report(actual, predictions); }
GBDT 凭什么在交易模型里更耐打
梯度提升决策树(GBDT)在预测准确性上通常压过不少传统机器学习算法,核心在于它把决策树的分裂能力与梯度提升的迭代纠错叠在一起:后一棵树专门盯前一棵树的残差补刀,模型性能随树增加而逐步抬升。 对过拟合的鲁棒性来自提升机制本身——新树只去修已有集成的错,而不是重新学全量分布;再叠学习率衰减、正则化与早停,过拟合概率会进一步压低。外汇与贵金属行情噪声大、高风险,这种鲁棒性直接决定回测漂亮和实盘崩盘之间的距离。 它几乎不吃预处理:特征缩放、缺失值填补、分类变量独热编码这些线性回归或 SVM 的标配动作,GBDT 大多可以跳过。实盘里你接 MT5 导出的 tick 或 M1 特征矩阵,脏数据直接进模型通常也能跑。 超参数给了充分定制空间:树的数量、学习率、最大深度、节点最小分裂样本数都能按品种调。比如 max_depth 设 3~6 可逼模型学局部价格结构,设过深则容易把随机波动当规律。 复杂非线性关系它吃得下,能捞到简单线性模型漏掉的拐点和波动率突变。XGBoost、LightGBM、CatBoost 还做了并行与分布式优化,大样本集训练耗时可控。 当然缺点也得认:不是劝退,而是别把它当黑箱圣经。下一篇我们拆 GBDT 在 MT5 特征工程上的具体坑。
「GBDT 实盘前的四道坎」
梯度提升决策树(GBDT)在 MT5 用 Python 或外部模型做信号时,第一个绕不开的是算力。树的数量和深度同时拉大,训练内存占用会随样本量近似线性增长;在深度行情数据集(比如 1 分钟级三年 Tick 聚合)上,单轮训练吃掉十几 GB 内存并不罕见。 超参数不是开箱即用的。树的数量、学习率、最大深度、叶子最小样本数这四个旋钮,任意动一个都会改变过拟合边界。默认参数能跑出结果,但离「可用信号」通常差两三轮网格搜索,而这本身就很耗机器时间。 它对噪声的放大值得警惕。每棵树都在修正前一棵的残差,原始特征里的异常跳空或错价如果被当作「误差模式」学进去,会在提升链里被逐层放大,外汇和贵金属这种高波动品种尤其明显,杠杆下风险被进一步放大。 最后是数据量门槛。小样本(比如不足五千根 K 线)下,boosting 很难学到稳定结构,容易把随机波动当规律。想拿它做 XAUUSD 的日内分类,至少准备数万样本再谈回测。
◍ 最后一句大实话
梯度提升树在外汇建模里已经把 GRU、RNN、LSTM 这些重型循环网络挤下了神坛,靠的是简单结构和实盘预测力,MT5 里直接挂 LightGBM EA.mq5 或 XGBoost EA.mq5 就能跑欧元小时级样本。附件里的 EURUSD.PERIOD_H1.csv 和两份 onnx 模型是现成入口,复制进对应文件夹即可回测。 外汇和贵金属杠杆高、滑点狠,模型胜率只是概率倾斜,别把回测曲线当保本凭证;真要上实盘,先拿脚本 LightGBM Performance TestScript.mq5 测完延迟再谈仓位。