You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

将含XGBoost的VotingClassifier转ONNX时特征名错误的解决方法

解决VotingClassifier(含XGBoost)转ONNX时的特征名格式错误问题

问题场景

加载.sav格式的VotingClassifier模型(由XGBoost和NaiveBayes混合组成),目标转换为ONNX格式,但无数据集无法重训,转换时触发以下错误:

RuntimeError: Unable to interpret 'ABC', feature names should follow pattern 'f%d'.

手动修改model.feature_names_in_为['f0', 'f1', ..., 'f11']后错误仍存在,查看XGBoost模型的JSON结构发现,树节点的split字段仍保留旧特征名(如'ABC'、'BLK'):

{'nodeid': 0, 'depth': 0, 'split': 'ABC', 'split_condition': 6.25, 'yes': 1, 'no': 2, 'missing': 1, 'gain': 78.1462402, 'cover': 259.75, 'children': [{'nodeid': 1, 'depth': 1, 'split': 'BLK', 'split_condition': 0.550000012, 'yes': 3, 'no': 4, 'missing': 3, 'gain': 21.2281971, 'cover': 171.75, 'children': [{'nodeid': 3, 'depth': 2, 'split': 'ABC', 'split_condition': 4.55000019, 'yes': 7, 'no': 8, 'missing': 7, 'gain': 14.1838226, 'cover': 147.5, 'children': [{'nodeid': 7, 'depth': 3, 'split': 'BLK', 'split_condition': 0.25, 'yes': 11, 'no': 12, 'missing': 11, 'gain': 3.78608131, 'cover': 106.5, 'children': [{'nodeid': 11, 'depth': 4, 'split': 'BLK', 'split_condition': 0.150000006, 'yes': 15, 'no': 16, 'missing': 15, 'gain': 5.14517879, 'cover': 78.5, 'children': [{'nodeid': 15, 'depth': 5, 'split': 'MIN', 'split_condition': 8.64999962, 'yes': 17, 'no': 18, 'missing': 17, 'gain': 4.04689026, 'cover': 60, 'children': [{'nodeid': 17, 'leaf': -0.018082479, 'cover': 23.25}, {'nodeid': 18, 'leaf': -0.00116446253, 'cover': 36.75}]}, {'nodeid': 16, 'leaf': -0.0269777905, 'cover': 18.5}]}, {'nodeid': 12, 'leaf': 0.000966416614, 'cover': 28}]}, {'nodeid': 8, 'depth': 3, 'split': 'MIN', 'split_condition': 16.2000008, 'yes': 13, 'no': 14, 'missing': 13, 'gain': 1.36819792, 'cover': 41, 'children': [{'nodeid': 13, 'leaf': 0.00639292412, 'cover': 19}, {'nodeid': 14, 'leaf': 0.0183946956, 'cover': 22}]}]}, {'nodeid': 4, 'leaf': 0.029015895, 'cover': 24.25}]}, {'nodeid': 2, 'depth': 1, 'split': 'MIN', 'split_condition': 23.9500008, 'yes': 5, 'no': 6, 'missing': 5, 'gain': 8.23132324, 'cover': 88, 'children': [{'nodeid': 5, 'leaf': 0.0257856827, 'cover': 34}, {'nodeid': 6, 'depth': 2, 'split': 'BLK', 'split_condition': 0.350000024, 'yes': 9, 'no': 10, 'missing': 9, 'gain': 2.95942688, 'cover': 54, 'children': [{'nodeid': 9, 'leaf': 0.0367497504, 'cover': 23}, {'nodeid': 10, 'leaf': 0.0526438914, 'cover': 31}]}]}]}

用户原转换代码:

from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
from skl2onnx import get_latest_tested_opset_version
from onnxmltools.utils import save_model
import joblib
from skl2onnx import convert_sklearn, to_onnx, update_registered_converter
from skl2onnx.common.data_types import FloatTensorType
from skl2onnx.common.shape_calculator import (
        calculate_linear_classifier_output_shapes,
        calculate_linear_regressor_output_shapes,
    )
from onnxmltools.convert.xgboost.operator_converters.XGBoost import convert_xgboost
from onnxmltools.convert import convert_xgboost as convert_xgboost_booster
    
from xgboost import XGBClassifier

model = joblib.load("model.sav")

model = model.set_params(flatten_transform=False)

features_name_fixed = ['f0', 'f1', 'f2', 'f3' ,'f4', 'f5', 'f6', 'f7', 'f8' ,'f9' ,'f10', 'f11']
# print(model.feature_names_in_)

model.feature_names_in_ = features_name_fixed
    

n_features = 12
    

target_opset = get_latest_tested_opset_version()
update_registered_converter(
    XGBClassifier,
    "XGBoostXGBClassifier",
    calculate_linear_classifier_output_shapes,
    convert_xgboost,
    options={"nocl": [True, False], "zipmap": [True, False, "columns"]},
)

onnx_model = convert_sklearn(model,"gbdt_model",
initial_types=[("input", FloatTensorType([None, n_features]))],
target_opset={"": target_opset, "ai.onnx.ml": 1})

save_model(onnx_model, 'model_converted.onnx') 

原始模型使用XGBoost 1.4.2版本训练。


解决方案

问题核心是XGBoost的booster内部树结构存储了旧特征名,仅修改feature_names_in_无法同步更新树节点的split字段。需要递归修改XGBoost模型的树结构,再同步修改特征名字段:

1. 构建旧特征名到新格式的映射

从模型中提取原始特征名,生成旧特征名→f%d的映射字典:

# 获取原始特征名
old_feature_names = model.feature_names_in_
# 生成映射字典
feature_map = {name: f'f{i}' for i, name in enumerate(old_feature_names)}

2. 递归修改XGBoost树节点的split字段

定义递归函数遍历树节点,替换split字段的旧特征名:

import json

def replace_split_names(node):
    if 'split' in node:
        # 替换特征名
        node['split'] = feature_map[node['split']]
    # 递归处理子节点
    if 'children' in node:
        for child in node['children']:
            replace_split_names(child)

3. 遍历VotingClassifier中的XGBoost模型,更新booster

遍历model.estimators_,对每个XGBClassifier实例执行修改:

from xgboost import Booster

for est in model.estimators_:
    if isinstance(est, XGBClassifier):
        # 获取booster的JSON结构
        booster_json = json.loads(est.get_booster().get_dump(dump_format='json')[0])
        # 递归替换特征名
        replace_split_names(booster_json)
        # 将修改后的JSON重新加载为booster
        new_booster = Booster()
        new_booster.load_model_from_string(json.dumps(booster_json))
        # 替换原模型的booster
        est._Booster = new_booster

# 最后同步修改顶层模型的特征名字段
model.feature_names_in_ = list(feature_map.values())

4. 完整修改后的转换代码

将上述步骤整合到原代码中,替换原model.feature_names_in_ = features_name_fixed部分:

from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
from skl2onnx import get_latest_tested_opset_version
from onnxmltools.utils import save_model
import joblib
import json
from skl2onnx import convert_sklearn, to_onnx, update_registered_converter
from skl2onnx.common.data_types import FloatTensorType
from skl2onnx.common.shape_calculator import (
        calculate_linear_classifier_output_shapes,
        calculate_linear_regressor_output_shapes,
    )
from onnxmltools.convert.xgboost.operator_converters.XGBoost import convert_xgboost
from onnxmltools.convert import convert_xgboost as convert_xgboost_booster
    
from xgboost import XGBClassifier, Booster

model = joblib.load("model.sav")
model = model.set_params(flatten_transform=False)

# -------------------- 新增特征名修改逻辑 --------------------
# 获取原始特征名并生成映射
old_feature_names = model.feature_names_in_
feature_map = {name: f'f{i}' for i, name in enumerate(old_feature_names)}

def replace_split_names(node):
    if 'split' in node:
        node['split'] = feature_map[node['split']]
    if 'children' in node:
        for child in node['children']:
            replace_split_names(child)

# 更新每个XGBoost estimator的booster
for est in model.estimators_:
    if isinstance(est, XGBClassifier):
        booster_json = json.loads(est.get_booster().get_dump(dump_format='json')[0])
        replace_split_names(booster_json)
        new_booster = Booster()
        new_booster.load_model_from_string(json.dumps(booster_json))
        est._Booster = new_booster

# 同步修改模型的特征名字段
model.feature_names_in_ = list(feature_map.values())
# -------------------- 特征名修改逻辑结束 --------------------

n_features = 12
target_opset = get_latest_tested_opset_version()
update_registered_converter(
    XGBClassifier,
    "XGBoostXGBClassifier",
    calculate_linear_classifier_output_shapes,
    convert_xgboost,
    options={"nocl": [True, False], "zipmap": [True, False, "columns"]},
)

onnx_model = convert_sklearn(model,"gbdt_model",
initial_types=[("input", FloatTensorType([None, n_features]))],
target_opset={"": target_opset, "ai.onnx.ml": 1})

save_model(onnx_model, 'model_converted.onnx') 

说明

  • NaiveBayes模型不依赖特征名格式,无需修改,只要特征数量与输入一致即可正常转换。
  • 该方法直接修改XGBoost booster的内部结构,无需重新训练模型,适合无数据集的场景。

内容的提问来源于stack exchange,提问作者dev_ds

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 23:47:01