如何在独立CatBoost中复现mljar-supervised的缩放预处理逻辑
mljar-supervised CatBoost模型复现问题解答
目标变量y的处理逻辑
你的判断完全正确。
配置中scale_method: "scale_normal"对应StandardScaler标准化操作:框架仅在训练集上拟合y的标准化参数,按y_scaled = (y - mean)/scale变换后送入回归器训练;预测阶段模型输出缩放尺度的预测值后,会执行pred_original = pred_scaled * scale + mean的逆变换,还原到原始目标值尺度。
你贴出的配置中n_samples_seen: 75000刚好对应10w总样本的75%训练集占比,也印证了scaler是仅在训练集上拟合的,没有接触验证/测试集数据。
特征X的"scale"处理逻辑
columns_info里标注的"scale"不是对所有模型生效的通用StandardScaler变换:
- 该标记是mljar预处理管道的通用标识,仅对线性模型、神经网络这类对特征尺度敏感的模型启用StandardScaler;
- 对于CatBoost、XGBoost、LightGBM这类树模型,框架默认跳过特征缩放步骤——因为树模型基于特征分裂阈值训练,特征缩放完全不影响分裂结果,反而会增加不必要的计算。给树模型输入的特征仅会经过缺失值填充、分类特征编码、极端离群值截断处理,保留原始数值尺度。
复现结果差异大的核心原因与修正方案
你之前的复现代码存在3个关键偏差,直接导致结果差异:
- 预处理拟合范围错误:你在全量10w样本上拟合y和X的scaler,但框架的所有预处理参数仅在7.5w训练集样本上拟合,全量拟合得到的mean、scale参数和框架保存的参数存在偏差。
- 错误添加特征标准化步骤:你对所有X特征做了StandardScaler变换,但框架训练CatBoost时完全跳过这一步,喂入模型的特征分布和你处理后的分布完全不一致。
- 拆分逻辑与超参数不匹配:你按位置顺序切分训练/验证集,且手动设置CatBoost超参数,和框架默认的随机分层拆分、自动调优得到的超参不匹配。
正确复现步骤
- 直接使用框架保存的y缩放参数:不需要自己在数据上拟合scaler,直接用framework.json里存储的mean、scale值做y的变换和逆变换即可。
- 移除X的StandardScaler步骤:仅对X做和框架一致的基础预处理:数值列用训练集中位数填充缺失值,分类列用
<MISSING>填充缺失值并转为字符串类型,可选做1%/99%分位数的离群值截断。 - 对齐数据拆分逻辑:使用和框架一致的拆分规则:固定随机种子1234,shuffle后拆分,75%训练集、10%验证集、15%测试集,不要按位置顺序切分。
- 对齐模型超参数:不要手动设置CatBoost参数,直接从mljar输出模型目录下的
params.json中加载所有训练超参,同时固定CatBoost的随机种子和框架一致。
参考修正代码
import pandas as pd import numpy as np from catboost import CatBoostRegressor, Pool from sklearn.model_selection import train_test_split # 直接加载framework.json中保存的y缩放参数,无需自行拟合 Y_MEAN = 0.994522478250971 Y_SCALE = 0.025919186407451625 # 加载原始数据,不对X做StandardScaler df = pd.read_csv("your_raw_data.csv") # 替换为原始数据路径 X = df.drop("target", axis=1) y = df["target"] y_scaled = (y - Y_MEAN) / Y_SCALE # 对齐mljar默认拆分逻辑:固定随机种子1234,shuffle拆分 X_train_val, X_test, y_train_val, y_test = train_test_split( X, y_scaled, test_size=0.15, random_state=1234, shuffle=True ) X_train, X_val, y_train, y_val = train_test_split( X_train_val, y_train_val, test_size=0.1/0.85, random_state=1234, shuffle=True ) # 基础预处理:缺失值填充 cat_features = X.select_dtypes(include=["object", "category"]).columns.tolist() for col in X.columns: if col in cat_features: fill_val = "<MISSING>" X_train[col] = X_train[col].fillna(fill_val).astype(str) X_val[col] = X_val[col].fillna(fill_val).astype(str) else: fill_val = X_train[col].median() X_train[col] = X_train[col].fillna(fill_val) X_val[col] = X_val[col].fillna(fill_val) # 加载框架保存的CatBoost超参,不要手动设置 model_params = { "iterations": 10000, "early_stopping_rounds": 50, "random_seed": 1234, "verbose": False # 其余参数从mljar生成的params.json中补全 } # 训练模型 train_pool = Pool(X_train, y_train, cat_features=cat_features) val_pool = Pool(X_val, y_val, cat_features=cat_features) model = CatBoostRegressor(**model_params) model.fit(train_pool, eval_set=val_pool) # 预测时逆变换回原始尺度 pred_scaled = model.predict(X_val) pred_original = pred_scaled * Y_SCALE + Y_MEAN
如果对齐以上步骤后仍有微小差异,可检查framework.json中是否开启了额外的特征选择、目标编码等预处理步骤,逐一对应配置即可,最终结果差异会控制在浮点误差范围内。
内容的提问来源于stack exchange,提问作者Paul
相关产品推荐
相关产品推荐

