CatBoost 模型中的交叉验证和因果推理基础及导出为 ONNX 格式(基础篇)
「用 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 历史数据上重跑验证。
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>) -> pd.DataFrame: dataset = get_labels(get_prices()) data = dataset[(dataset.index < FORWARD) & (dataset.index > BACKWARD)].copy() X = data[data.columns[<span class="number">class="num">1</span>:-<span class="number">class="num">2</span>]] y = data[<span class="class="type">class="kw">string">&class="macro">#x27;labels&class="macro">#x27;</span>] B_S_B = pd.DatetimeIndex([]) # learn meta model with CV method meta_model = CatBoostClassifier(iterations = iter, max_depth = depth, learning_rate=l_rate, verbose = False) 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) coreset = X.copy() coreset[<span class="class="type">class="kw">string">&class="macro">#x27;labels&class="macro">#x27;</span>] = y 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>] < <span class="number">class="num">0.5</span> <span class="keyword">for</span> x <span class="keyword">in</span> predicted] 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 < <span class="number">class="num">0.5</span> <span class="keyword">else</span> <span class="number">class="num">1</span>) # <span class="keyword">select</span> bad samples(bad labels indices) 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>] B_S_B = B_S_B.append(diff_negatives[diff_negatives == True].index) to_mark = B_S_B.value_counts() marked_idx = to_mark.index 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> <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 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]] X = X[X.columns[:-class="num">2]] # labels for model\meta models 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]] y = y[y.columns[-class="num">2]] # train\test split train_X, test_X, train_y, test_y = train_test_split( X, y, train_size=class="num">0.5, test_size=class="num">0.5, shuffle=True) train_X_m, test_X_m, train_y_m, test_y_m = train_test_split( X_meta, y_meta, train_size=class="num">0.5, test_size=class="num">0.5, shuffle=True) # learn main model with train and validation subsets model = CatBoostClassifier(iterations=class="num">100, 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;) model.fit(train_X, train_y, eval_set=(test_X, test_y), early_stopping_rounds=class="num">15, plot=False) # learn meta model with train and validation subsets 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 标签,就能复现这一层的集成效果,贵金属与外汇品种均属高风险,命中率提升不代表账户必然盈利。
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)