CatBoost 模型中的交叉验证和因果推理基础及导出为 ONNX 格式(基础篇)
📘

CatBoost 模型中的交叉验证和因果推理基础及导出为 ONNX 格式(基础篇)

第 1/3 篇

「用 CatBoost 做交叉验证并导出 ONNX 给 MT5」

在 MT5 里跑机器学习信号,第一步是把训练好的 CatBoost 模型塞进推理环境。原生 CatBoost 的 Python 包能直接做交叉验证,但终端内只认 ONNX 这类轻量图格式,所以导出环节绕不开。 交叉验证建议用 catboost.cv,按时间序列切分而不是随机洗牌——价格序列有自相关,随机折会让验证集偷看到未来,回测虚高。一个常见做法是 TimeSeriesSplit,折数设在 5 左右,早停轮数用 50~100 收敛。 导出用 catboost.to_onnx,注意 feature 名字要和 MT5 端 EA 取的指标数组顺序严格对齐,否则推理结果错位。外汇与贵金属杠杆高、滑点跳空频繁,模型信号仅作概率参考,实盘前务必在策略测试器用真实点差重跑。

◍ 从过度拟合走向因果识别

早期用机器学习做时间序列分类的尝试里,不少模型只是把训练样本记熟了。文章顺序其实对应理解演进:一部分早期方案明显过拟合,泛化一塌糊涂。 分类器擅长概括和预测,却懒得追问因果。它把训练数据里的临时关联和真实因果关系一视同仁,新数据来了若只是联想性耦合,预测就不可靠。 上篇用双分类器交互让算法分析自身错误,区分统计显著预测与随机噪声。本篇接着往前走,目标是自训练算法既能找模式,又把过拟合压到最低,让模型判断训练集里到底有没有因果。外汇与贵金属市场高风险,这类因果判断只代表概率倾向,需 MT5 实盘验证。

用双重模型给训练集做自我纠偏

把机器学习预测当绝对真理,是外汇与贵金属实盘里最常见的坑。模型在选定数据上只训一次,既没有前向验证也没有交叉验证,它就不可能有自我控制能力;一旦遇到分布偏移,错误率会陡然抬升,而这类品种受杠杆与跳空影响,高风险会被放大。 做法是把训练集拆成多个随机子样本,分别训练并统计每个子样本的预测误差,再把所有错误累加。这类似交叉验证,但多了一步:把平均预测错误的样本从最终训练集剔除,只留模型学得稳的例子的主模型,其分类误差变小、准确率上去,但召回率会掉。 再训第二个分类器,专门区分「可预测性高」和「可预测性低」的场景。主模型只在自己擅长的区制里下单,第二个模型覆盖更广但准确率偏低、召回率偏高。主模型的错误被转移而非消失,但因为它不直接判方向,只是做区制过滤,整体系统的交易结果倾向改善。 经验上,两个模型叠加通常足以用正面输出抵消训练残差。剔除劣质样本的本质,是去平均能带来利润的行情,避开平均会亏损的局——这套流程在 MT5 里用历史中心做多次重采样就能复现。

「双层分类器的交叉验证与元标签生成」

meta_learner 是整个流程的主干,其余函数只做取数、打标之类的辅助活。它吃四个参数:交叉验证折数、基础学习器迭代次数、树深度、梯度步长;这些都不是拍脑袋定的,要么靠跑盘经验,要么走网格搜索,调参偏差会直接拖累最终信号质量。 scikit-learn 的 cross_val_predict 先给每个训练样本吐出交叉验证概率,再和原始 labels 比对。预测错的样本被收进错误集,并据此给第二层分类器打上 meta_labels——代码里 meta_labels==0 的就是被筛掉的坏样本。 第一层模型只用 meta_labels 为 1 的行训练,等于只在“好例子”上拟合买卖概率;第二层模型在全集上跑,判断“这单值不值得做”。我故意把两层迭代都锁在 100 次,避免末段过拟合。第一层任务轻,结构不用复杂;第二层要分辨噪声,复杂度可以往上加。 训练测试切分比例设成 0.5/0.5 只是起点,挪到 0.7/0.3 可能让准确率浮动几个百分点。外汇与贵金属杠杆高、滑点跳空频繁,这套机制只是概率过滤,实盘前务必在 MT5 历史数据上重跑验证。

MQL5 / C++
def meta_learner(folds_number: <span class="keyword">class="type">int</span>, iter: <span class="keyword">class="type">int</span>, depth: <span class="keyword">class="type">int</span>, l_rate: <span class="keyword">class="type">class="kw">float</span>) -&gt; pd.DataFrame:
&nbsp;&nbsp;&nbsp;&nbsp;dataset = get_labels(get_prices())
&nbsp;&nbsp;&nbsp;&nbsp;data = dataset[(dataset.index &lt; FORWARD) &amp; (dataset.index &gt; BACKWARD)].copy()
&nbsp;&nbsp;&nbsp;&nbsp;X = data[data.columns[<span class="number">class="num">1</span>:-<span class="number">class="num">2</span>]]
&nbsp;&nbsp;&nbsp;&nbsp;y = data[<span class="class="type">class="kw">string">&class="macro">#x27;labels&class="macro">#x27;</span>]
&nbsp;&nbsp;&nbsp;&nbsp;B_S_B = pd.DatetimeIndex([])
&nbsp;&nbsp;&nbsp;&nbsp;# learn meta model with CV method
&nbsp;&nbsp;&nbsp;&nbsp;meta_model = CatBoostClassifier(iterations = iter,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;max_depth = depth,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;learning_rate=l_rate,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;verbose = False)
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;predicted = cross_val_predict(meta_model, X, y, method=<span class="class="type">class="kw">string">&class="macro">#x27;predict_proba&class="macro">#x27;</span>, cv=folds_number)
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;coreset = X.copy()
&nbsp;&nbsp;&nbsp;&nbsp;coreset[<span class="class="type">class="kw">string">&class="macro">#x27;labels&class="macro">#x27;</span>] = y
&nbsp;&nbsp;&nbsp;&nbsp;coreset[<span class="class="type">class="kw">string">&class="macro">#x27;labels_pred&class="macro">#x27;</span>] = [x[<span class="number">class="num">0</span>] &lt; <span class="number">class="num">0.5</span> <span class="keyword">for</span> x <span class="keyword">in</span> predicted]
&nbsp;&nbsp;&nbsp;&nbsp;coreset[<span class="class="type">class="kw">string">&class="macro">#x27;labels_pred&class="macro">#x27;</span>] = coreset[<span class="class="type">class="kw">string">&class="macro">#x27;labels_pred&class="macro">#x27;</span>].apply(lambda x: <span class="number">class="num">0</span> <span class="keyword">if</span> x &lt; <span class="number">class="num">0.5</span> <span class="keyword">else</span> <span class="number">class="num">1</span>)
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;# <span class="keyword">select</span> bad samples(bad labels indices)
&nbsp;&nbsp;&nbsp;&nbsp;diff_negatives = coreset[<span class="class="type">class="kw">string">&class="macro">#x27;labels&class="macro">#x27;</span>] != coreset[<span class="class="type">class="kw">string">&class="macro">#x27;labels_pred&class="macro">#x27;</span>]
&nbsp;&nbsp;&nbsp;&nbsp;B_S_B = B_S_B.append(diff_negatives[diff_negatives == True].index)
&nbsp;&nbsp;&nbsp;&nbsp;to_mark = B_S_B.value_counts()
&nbsp;&nbsp;&nbsp;&nbsp;marked_idx = to_mark.index
&nbsp;&nbsp;&nbsp;&nbsp;data.loc[data.index.isin(marked_idx), <span class="class="type">class="kw">string">&class="macro">#x27;meta_labels&class="macro">#x27;</span>] = <span class="number">class="num">0.0</span>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span> data[data.columns[<span class="number">class="num">1</span>:]]
# features for model\meta models. We learn main model only on filtered labels
&nbsp;&nbsp;&nbsp;&nbsp;X, X_meta = <span style="background-class="type">class="kw">color:rgb(class="num">255, class="num">242, class="num">153);">dataset[dataset[&class="macro">#x27;meta_labels&class="macro">#x27;]==class="num">1]</span>, dataset[dataset.columns[:-class="num">2]]
&nbsp;&nbsp;&nbsp;&nbsp;X = X[X.columns[:-class="num">2]]
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;# labels for model\meta models
&nbsp;&nbsp;&nbsp;&nbsp;y, y_meta = <span style="background-class="type">class="kw">color:rgb(class="num">255, class="num">242, class="num">153);">dataset[dataset[&class="macro">#x27;meta_labels&class="macro">#x27;]==class="num">1]</span>, dataset[dataset.columns[-class="num">1]]
&nbsp;&nbsp;&nbsp;&nbsp;y = y[y.columns[-class="num">2]]
# train\test split
&nbsp;&nbsp;&nbsp;&nbsp;train_X, test_X, train_y, test_y = train_test_split(
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;X, y, train_size=class="num">0.5, test_size=class="num">0.5, shuffle=True)
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;train_X_m, test_X_m, train_y_m, test_y_m = train_test_split(
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;X_meta, y_meta, train_size=class="num">0.5, test_size=class="num">0.5, shuffle=True)
&nbsp;&nbsp;&nbsp;&nbsp;# learn main model with train and validation subsets
&nbsp;&nbsp;&nbsp;&nbsp;model = CatBoostClassifier(iterations=class="num">100,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; custom_loss=[&class="macro">#x27;Accuracy&class="macro">#x27;],
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; eval_metric=&class="macro">#x27;Accuracy&class="macro">#x27;,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; verbose=False,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; use_best_model=True,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; task_type=&class="macro">#x27;CPU&class="macro">#x27;)
&nbsp;&nbsp;&nbsp;&nbsp;model.fit(train_X, train_y, eval_set=(test_X, test_y),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;early_stopping_rounds=class="num">15, plot=False)
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;# learn meta model with train and validation subsets
&nbsp;&nbsp;&nbsp;&nbsp;meta_model = CatBoostClassifier(iterations=class="num">100,

◍ 堆叠模型的训练与早停设置

上面这段是 CatBoost 元模型在堆叠框架里的拟合代码,核心是把基模型预测出的 train_X_m、test_X_m 当作新特征,再去学一层加权逻辑。\n自定义损失和评估指标都锁死在 Accuracy 上,说明这一层更关心方向命中率而非盈亏幅度;verbose 关掉、use_best_model 打开,训练时只保留验证集上最优的那一版参数。\nearly_stopping_rounds=15 意味着连续 15 轮验证精度没提升就停,能压住过拟合;task_type='CPU' 则明确不走显卡,小样本外汇特征在普通机器上跑就够了。\n把这串直接丢进 MT5 外接的 Python 环境,换上你自己的 train_y_m 标签,就能复现这一层的集成效果,贵金属与外汇品种均属高风险,命中率提升不代表账户必然盈利。

MQL5 / C++
custom_loss=[&class="macro">#x27;Accuracy&class="macro">#x27;],
              eval_metric=&class="macro">#x27;Accuracy&class="macro">#x27;,
              verbose=False,
              use_best_model=True,
              task_type=&class="macro">#x27;CPU&class="macro">#x27;)
    meta_model.fit(train_X_m, train_y_m, eval_set=(test_X_m, test_y_m),
                  early_stopping_rounds=class="num">15, plot=False)

常见问题

按时间顺序划分,只用历史窗口训练、紧接其后的窗口验证,禁止随机打乱,避免前视偏差。
先核对特征归一化和缺失值填充是否和训练一致,ONNX 不会自动复现预处理,需手动对齐。
小布可自动按时间切分窗口、跑双层分类器并标记元标签,你只需确认特征逻辑和早停阈值。
用第一层模型预测残差,第二层学残差规律回补偏差,降低单一模型过拟合导致的系统误差。
抖动小于历史最低 1%~2% 内可视为平台期,配合元标签稳定性再决定停训,别盯单次波动。